From d6c7a586a67e7c8149c1070607863206c93d862e Mon Sep 17 00:00:00 2001
From: Dani Palou <dani@moodle.com>
Date: Wed, 16 May 2018 08:15:58 +0200
Subject: [PATCH 1/4] MOBILE-2416 core: Implement and apply core-ion-tabs

---
 src/app/app.scss                            |   5 +
 src/components/components.module.ts         |  10 +-
 src/components/ion-tabs/ion-tab.ts          |  61 ++++++
 src/components/ion-tabs/ion-tabs.html       |  13 ++
 src/components/ion-tabs/ion-tabs.scss       |  93 ++++++++++
 src/components/ion-tabs/ion-tabs.ts         | 196 ++++++++++++++++++++
 src/core/login/pages/init/init.scss         |   2 +-
 src/core/mainmenu/pages/menu/menu.html      |   9 +-
 src/core/mainmenu/pages/menu/menu.module.ts |   2 +
 src/core/mainmenu/pages/menu/menu.ts        | 105 +++++------
 src/core/mainmenu/providers/delegate.ts     |   8 +
 11 files changed, 439 insertions(+), 65 deletions(-)
 create mode 100644 src/components/ion-tabs/ion-tab.ts
 create mode 100644 src/components/ion-tabs/ion-tabs.html
 create mode 100644 src/components/ion-tabs/ion-tabs.scss
 create mode 100644 src/components/ion-tabs/ion-tabs.ts

diff --git a/src/app/app.scss b/src/app/app.scss
index efc43f114..8d03df376 100644
--- a/src/app/app.scss
+++ b/src/app/app.scss
@@ -661,3 +661,8 @@ canvas[core-chart] {
     height: 100% !important;
   }
 }
+
+// For some reason, in iOS the pages don't inherit the background-color from ion-app, set it explicitly.
+.ion-page {
+  background-color: $background-color;
+}
\ No newline at end of file
diff --git a/src/components/components.module.ts b/src/components/components.module.ts
index 2e834aa3b..afd9e2d4a 100644
--- a/src/components/components.module.ts
+++ b/src/components/components.module.ts
@@ -45,6 +45,8 @@ import { CoreRecaptchaComponent } from './recaptcha/recaptcha';
 import { CoreRecaptchaModalComponent } from './recaptcha/recaptchamodal';
 import { CoreNavigationBarComponent } from './navigation-bar/navigation-bar';
 import { CoreAttachmentsComponent } from './attachments/attachments';
+import { CoreIonTabsComponent } from './ion-tabs/ion-tabs';
+import { CoreIonTabComponent } from './ion-tabs/ion-tab';
 
 @NgModule({
     declarations: [
@@ -75,7 +77,9 @@ import { CoreAttachmentsComponent } from './attachments/attachments';
         CoreRecaptchaComponent,
         CoreRecaptchaModalComponent,
         CoreNavigationBarComponent,
-        CoreAttachmentsComponent
+        CoreAttachmentsComponent,
+        CoreIonTabsComponent,
+        CoreIonTabComponent
     ],
     entryComponents: [
         CoreContextMenuPopoverComponent,
@@ -113,7 +117,9 @@ import { CoreAttachmentsComponent } from './attachments/attachments';
         CoreTimerComponent,
         CoreRecaptchaComponent,
         CoreNavigationBarComponent,
-        CoreAttachmentsComponent
+        CoreAttachmentsComponent,
+        CoreIonTabsComponent,
+        CoreIonTabComponent
     ]
 })
 export class CoreComponentsModule {}
diff --git a/src/components/ion-tabs/ion-tab.ts b/src/components/ion-tabs/ion-tab.ts
new file mode 100644
index 000000000..a8c409f33
--- /dev/null
+++ b/src/components/ion-tabs/ion-tab.ts
@@ -0,0 +1,61 @@
+// (C) Copyright 2015 Martin Dougiamas
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+import {
+    Component, Optional, ElementRef, NgZone, Renderer, ComponentFactoryResolver, ChangeDetectorRef, ErrorHandler, OnInit,
+    OnDestroy, ViewEncapsulation
+} from '@angular/core';
+import { Tab, App, Config, Platform, GestureController, DeepLinker, DomController } from 'ionic-angular';
+import { TransitionController } from 'ionic-angular/transitions/transition-controller';
+import { CoreIonTabsComponent } from './ion-tabs';
+
+/**
+ * Equivalent to ion-tab, but to be used inside core-ion-tabs.
+ */
+@Component({
+    selector: 'core-ion-tab',
+    template: '<div #viewport></div><div class="nav-decor"></div>',
+    host: {
+        '[attr.id]': '_tabId',
+        '[attr.aria-labelledby]': '_btnId',
+        'role': 'tabpanel'
+    },
+    encapsulation: ViewEncapsulation.None,
+})
+export class CoreIonTabComponent extends Tab implements OnInit, OnDestroy {
+
+    constructor(parent: CoreIonTabsComponent, app: App, config: Config, plt: Platform, elementRef: ElementRef, zone: NgZone,
+            renderer: Renderer, cfr: ComponentFactoryResolver, _cd: ChangeDetectorRef, gestureCtrl: GestureController,
+            transCtrl: TransitionController, @Optional() linker: DeepLinker, _dom: DomController, errHandler: ErrorHandler) {
+        super(parent, app, config, plt, elementRef, zone, renderer, cfr, _cd, gestureCtrl, transCtrl, linker, _dom, errHandler);
+    }
+
+    /**
+     * Component being initialized.
+     */
+    ngOnInit(): void {
+        super.ngOnInit();
+
+        this.parent.add(this, true);
+    }
+
+    /**
+     * Component destroyed.
+     */
+    ngOnDestroy(): void {
+        super.ngOnDestroy();
+
+        this.parent.remove(this);
+    }
+}
diff --git a/src/components/ion-tabs/ion-tabs.html b/src/components/ion-tabs/ion-tabs.html
new file mode 100644
index 000000000..8a94426df
--- /dev/null
+++ b/src/components/ion-tabs/ion-tabs.html
@@ -0,0 +1,13 @@
+<div class="tabbar" role="tablist" #tabbar>
+    <a [hidden]="_loaded === false" *ngFor="let t of _tabs" [tab]="t" class="tab-button" role="tab" href="#" (ionSelect)="select(t)"></a>
+    <div class="tab-highlight"></div>
+    <div *ngIf="_loaded === false" class="core-ion-tabs-loading">
+        <span class="core-ion-tabs-loading-spinner">
+            <ion-spinner></ion-spinner>
+        </span>
+    </div>
+</div>
+<div #originalTabs>
+    <ng-content></ng-content>
+</div>
+<div #portal tab-portal></div>
diff --git a/src/components/ion-tabs/ion-tabs.scss b/src/components/ion-tabs/ion-tabs.scss
new file mode 100644
index 000000000..1cb04e2f4
--- /dev/null
+++ b/src/components/ion-tabs/ion-tabs.scss
@@ -0,0 +1,93 @@
+core-ion-tabs {
+    .tabbar {
+        z-index: 101; // For some reason, the regular z-index isn't enough with our tabs, use a higher one.
+
+        .core-ion-tabs-loading {
+            width: 100%;
+            display: table;
+
+            .core-ion-tabs-loading-spinner {
+                display: table-cell;
+                text-align: center;
+                vertical-align: middle;
+
+                .spinner circle, .spinner line {
+                    stroke: $white;
+                }
+            }
+        }
+
+    }
+}
+
+.ios core-ion-tabs .core-ion-tabs-loading {
+    min-height: $tabs-ios-tab-min-height;
+}
+
+.md core-ion-tabs .core-ion-tabs-loading {
+    min-height: $tabs-md-tab-min-height;
+}
+
+.wp core-ion-tabs .core-ion-tabs-loading {
+    min-height: $tabs-wp-tab-min-height;
+}
+
+// Copy some styles from ion-tabs and ion-tab.
+core-ion-tabs, core-ion-tab {
+    @include position(0, null, null, 0);
+
+    position: absolute;
+    z-index: $z-index-page-container;
+    display: block;
+
+    width: 100%;
+    height: 100%;
+    overflow: hidden;
+    contain: strict;
+}
+
+core-ion-tab {
+    display: none;
+}
+
+core-ion-tab.show-tab {
+    display: block;
+}
+
+@mixin core-ion-tabs-statusbar-padding($toolbar-height, $toolbar-padding, $content-padding, $cordova-statusbar-padding, $modal-max-width, $style-title: false) {
+
+    core-ion-tab > .ion-page,
+    core-ion-tab > .ion-page > ion-header,
+    core-ion-tabs > .ion-page.tab-subpage > ion-header {
+        @include toolbar-statusbar-padding($toolbar-height, $toolbar-padding, $content-padding, $cordova-statusbar-padding);
+
+        // If we should style the title elements in the toolbar
+        @if ($style-title) {
+            @include toolbar-title-statusbar-padding($toolbar-height, $toolbar-padding, $content-padding, $cordova-statusbar-padding);
+        }
+    }
+
+    @media only screen and (max-width: $modal-max-width) {
+        .modal-wrapper > .ion-page > ion-header {
+            @include toolbar-statusbar-padding($toolbar-height, $toolbar-padding, $content-padding, $cordova-statusbar-padding);
+
+            // If we should style the title elements in the toolbar
+            @if ($style-title) {
+                @include toolbar-title-statusbar-padding($toolbar-height, $toolbar-padding, $content-padding, $cordova-statusbar-padding);
+            }
+        }
+    }
+
+}
+
+.ios {
+    @include core-ion-tabs-statusbar-padding($toolbar-ios-height, $toolbar-ios-padding, $content-ios-padding, $cordova-ios-statusbar-padding, $cordova-ios-statusbar-padding-modal-max-width, true);
+}
+
+.md {
+   @include core-ion-tabs-statusbar-padding($toolbar-md-height, $toolbar-md-padding, $content-md-padding, $cordova-md-statusbar-padding, $cordova-md-statusbar-padding-modal-max-width);
+}
+
+.wp {
+    @include core-ion-tabs-statusbar-padding($toolbar-wp-height, $toolbar-wp-padding, $content-wp-padding, $cordova-wp-statusbar-padding, $cordova-wp-statusbar-padding-modal-max-width);
+}
diff --git a/src/components/ion-tabs/ion-tabs.ts b/src/components/ion-tabs/ion-tabs.ts
new file mode 100644
index 000000000..19f78a4fa
--- /dev/null
+++ b/src/components/ion-tabs/ion-tabs.ts
@@ -0,0 +1,196 @@
+// (C) Copyright 2015 Martin Dougiamas
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+import { Component, Optional, ElementRef, Renderer, ViewEncapsulation, forwardRef, ViewChild, Input } from '@angular/core';
+import { Tabs, NavController, ViewController, App, Config, Platform, DeepLinker, Keyboard, RootNode } from 'ionic-angular';
+import { CoreIonTabComponent } from './ion-tab';
+import { CoreUtilsProvider } from '@providers/utils/utils';
+
+/**
+ * Equivalent to ion-tabs. It has 2 improvements:
+ *     - If a core-ion-tab is added or removed, it will be reflected in the tab bar in the right position.
+ *     - It supports a loaded input to tell when are the tabs ready.
+ */
+@Component({
+    selector: 'core-ion-tabs',
+    templateUrl: 'ion-tabs.html',
+    encapsulation: ViewEncapsulation.None,
+    providers: [{provide: RootNode, useExisting: forwardRef(() => CoreIonTabsComponent) }]
+})
+export class CoreIonTabsComponent extends Tabs {
+
+    /**
+     * Whether the tabs have been loaded. If defined, tabs won't be initialized until it's set to true.
+     */
+    @Input() set loaded(val: boolean) {
+        this._loaded = this.utils.isTrueOrOne(val);
+
+        if (this.viewInit && !this.initialized) {
+            // Use a setTimeout to make sure the tabs have been loaded.
+            setTimeout(() => {
+                this.initTabs();
+            });
+        }
+    }
+
+    @ViewChild('originalTabs') originalTabsRef: ElementRef;
+
+    _loaded: boolean; // Whether tabs have been loaded.
+
+    /**
+     * List of tabs that haven't been initialized yet. This is required because IonTab calls add() on the constructor,
+     * but we need it to be called in OnInit to be able to determine the tab position.
+     * @type {CoreIonTabComponent[]}
+     */
+    protected tabsNotInit: CoreIonTabComponent[] = [];
+
+    protected tabsIds: string[] = []; // An array to keep the order of tab IDs when they're sorted.
+    protected tabsNotInitIds: string[] = []; // An array to keep the order of tab IDs for non-init tabs.
+    protected viewInit = false; // Whether the view has been initialized.
+    protected initialized = false; // Whether tabs have been initialized.
+
+    constructor(protected utils: CoreUtilsProvider, @Optional() parent: NavController, @Optional() viewCtrl: ViewController,
+            _app: App, config: Config, elementRef: ElementRef, _plt: Platform, renderer: Renderer, _linker: DeepLinker,
+            keyboard?: Keyboard) {
+        super(parent, viewCtrl, _app, config, elementRef, _plt, renderer, _linker, keyboard);
+    }
+
+    /**
+     * View has been initialized.
+     */
+    ngAfterViewInit(): void {
+        this.viewInit = true;
+
+        super.ngAfterViewInit();
+    }
+
+    /**
+     * Add a new tab if it isn't already in the list of tabs.
+     *
+     * @param {CoreIonTabComponent} tab The tab to add.
+     * @param {boolean} [isInit] Whether the tab has been initialized.
+     * @return {string} The tab ID.
+     */
+    add(tab: CoreIonTabComponent, isInit?: boolean): string {
+        // Check if tab is already in the list of initialized tabs.
+        let position = this._tabs.indexOf(tab);
+
+        if (position != -1) {
+            return this.tabsIds[position];
+        }
+
+        // Now check if the tab is in the not init list.
+        position = this.tabsNotInit.indexOf(tab);
+        if (position != -1) {
+            if (!isInit) {
+                return this.tabsNotInitIds[position];
+            }
+
+            // The tab wasn't initialized but now it is. Move it from one array to the other.
+            const tabId = this.tabsNotInitIds[position];
+            this.tabsNotInit.splice(position, 1);
+            this.tabsNotInitIds.splice(position, 1);
+
+            this._tabs.push(tab);
+            this.tabsIds.push(tabId);
+
+            this.sortTabs();
+
+            return tabId;
+        }
+
+        // Tab is new. In this case isInit should always be false, but check it just in case.
+        const id = this.id + '-' + (++this._ids);
+
+        if (isInit) {
+            this._tabs.push(tab);
+            this.tabsIds.push(id);
+
+            this.sortTabs();
+        } else {
+            this.tabsNotInit.push(tab);
+            this.tabsNotInitIds.push(id);
+        }
+
+        return id;
+    }
+
+    /**
+     * Initialize the tabs.
+     *
+     * @return {Promise<any>} Promise resolved when done.
+     */
+    initTabs(): Promise<any> {
+        if (!this.initialized && (this._loaded || typeof this._loaded == 'undefined')) {
+            this.initialized = true;
+
+            return super.initTabs();
+        } else {
+            // Tabs not loaded yet. Set the tab bar position so the tab bar is shown, it'll have a spinner.
+            this.setTabbarPosition(-1, 0);
+
+            return Promise.resolve();
+        }
+    }
+
+    /**
+     * Remove a tab from the list of tabs.
+     *
+     * @param {CoreIonTabComponent} tab The tab to remove.
+     */
+    remove(tab: CoreIonTabComponent): void {
+        // First search in the list of initialized tabs.
+        let index = this._tabs.indexOf(tab);
+
+        if (index != -1) {
+            this._tabs.splice(index, 1);
+            this.tabsIds.splice(index, 1);
+        } else {
+            // Not found, search in the list of non-init tabs.
+            index = this.tabsNotInit.indexOf(tab);
+
+            if (index != -1) {
+                this.tabsNotInit.splice(index, 1);
+                this.tabsNotInitIds.splice(index, 1);
+            }
+        }
+    }
+
+    /**
+     * Sort the tabs, keeping the same order as in the original list.
+     */
+    sortTabs(): void {
+        if (this.originalTabsRef) {
+            const newTabs = [],
+                newTabsIds = [],
+                originalTabsEl = this.originalTabsRef.nativeElement;
+
+            this._tabs.forEach((tab, index) => {
+                const originalIndex = Array.prototype.indexOf.call(originalTabsEl.children, tab.getNativeElement());
+                if (originalIndex != -1) {
+                    newTabs[originalIndex] = tab;
+                    newTabsIds[originalIndex] = this.tabsIds[index];
+                }
+            });
+
+            // Remove undefined values. It can happen if the view has some tabs that were destroyed but weren't removed yet.
+            this._tabs = newTabs.filter((tab) => {
+                return typeof tab != 'undefined';
+            });
+            this.tabsIds = newTabsIds.filter((id) => {
+                return typeof id != 'undefined';
+            });
+        }
+    }
+}
diff --git a/src/core/login/pages/init/init.scss b/src/core/login/pages/init/init.scss
index 20e54ae89..543fd5070 100644
--- a/src/core/login/pages/init/init.scss
+++ b/src/core/login/pages/init/init.scss
@@ -28,7 +28,7 @@ page-core-login-init {
             margin-bottom: 30px;
         }
 
-        .spinner circle {
+        .spinner circle, .spinner line {
             stroke: $core-init-screen-spinner-color;
         }
     }
diff --git a/src/core/mainmenu/pages/menu/menu.html b/src/core/mainmenu/pages/menu/menu.html
index 54839b8ee..6dcee56ce 100644
--- a/src/core/mainmenu/pages/menu/menu.html
+++ b/src/core/mainmenu/pages/menu/menu.html
@@ -1,4 +1,5 @@
-<ion-tabs *ngIf="loaded" #mainTabs [selectedIndex]="initialTab" tabsPlacement="bottom" tabsLayout="title-hide">
-    <ion-tab [enabled]="false" [show]="false" [root]="redirectPage" [rootParams]="redirectParams"></ion-tab>
-    <ion-tab *ngFor="let tab of tabs" [root]="tab.page" [rootParams]="tab.pageParams" [tabTitle]="tab.title | translate" [tabIcon]="tab.icon" [tabBadge]="tab.badge" class="{{tab.class}}"></ion-tab>
-</ion-tabs>
\ No newline at end of file
+<core-ion-tabs #mainTabs [hidden]="!showTabs" [loaded]="loaded" [selectedIndex]="initialTab" tabsPlacement="bottom" tabsLayout="title-hide">
+    <core-ion-tab [enabled]="false" [show]="false" [root]="redirectPage" [rootParams]="redirectParams"></core-ion-tab>
+    <core-ion-tab *ngFor="let tab of tabs" [root]="tab.page" [rootParams]="tab.pageParams" [tabTitle]="tab.title | translate" [tabIcon]="tab.icon" [tabBadge]="tab.badge" class="{{tab.class}}"></core-ion-tab>
+    <core-ion-tab root="CoreMainMenuMorePage" [tabTitle]="'core.more' | translate" tabIcon="more"></core-ion-tab>
+</core-ion-tabs>
diff --git a/src/core/mainmenu/pages/menu/menu.module.ts b/src/core/mainmenu/pages/menu/menu.module.ts
index 668c58c97..23a7ab49d 100644
--- a/src/core/mainmenu/pages/menu/menu.module.ts
+++ b/src/core/mainmenu/pages/menu/menu.module.ts
@@ -15,6 +15,7 @@
 import { NgModule } from '@angular/core';
 import { IonicPageModule } from 'ionic-angular';
 import { TranslateModule } from '@ngx-translate/core';
+import { CoreComponentsModule } from '@components/components.module';
 import { CoreMainMenuPage } from './menu';
 
 @NgModule({
@@ -22,6 +23,7 @@ import { CoreMainMenuPage } from './menu';
         CoreMainMenuPage,
     ],
     imports: [
+        CoreComponentsModule,
         IonicPageModule.forChild(CoreMainMenuPage),
         TranslateModule.forChild()
     ],
diff --git a/src/core/mainmenu/pages/menu/menu.ts b/src/core/mainmenu/pages/menu/menu.ts
index dd5872b6c..043cf3965 100644
--- a/src/core/mainmenu/pages/menu/menu.ts
+++ b/src/core/mainmenu/pages/menu/menu.ts
@@ -12,11 +12,11 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
-import { Component, OnDestroy, ViewChild } from '@angular/core';
-import { IonicPage, NavController, NavParams, Tabs } from 'ionic-angular';
+import { Component, OnDestroy } from '@angular/core';
+import { IonicPage, NavController, NavParams } from 'ionic-angular';
 import { CoreSitesProvider } from '@providers/sites';
 import { CoreMainMenuProvider } from '../../providers/mainmenu';
-import { CoreMainMenuDelegate, CoreMainMenuHandlerData } from '../../providers/delegate';
+import { CoreMainMenuDelegate, CoreMainMenuHandlerToDisplay } from '../../providers/delegate';
 
 /**
  * Page that displays the main menu of the app.
@@ -27,41 +27,14 @@ import { CoreMainMenuDelegate, CoreMainMenuHandlerData } from '../../providers/d
     templateUrl: 'menu.html',
 })
 export class CoreMainMenuPage implements OnDestroy {
-    // Use a setter to wait for ion-tabs to be loaded because it's inside a ngIf.
-    @ViewChild('mainTabs') set mainTabs(ionTabs: Tabs) {
-        if (ionTabs && this.redirectPage && !this.redirectPageLoaded) {
-            // Tabs ready and there is a redirect page set. Load it.
-            this.redirectPageLoaded = true;
-
-            // Check if the page is the root page of any of the tabs.
-            let indexToSelect = 0;
-            for (let i = 0; i < this.tabs.length; i++) {
-                if (this.tabs[i].page == this.redirectPage) {
-                    indexToSelect = i + 1;
-                    break;
-                }
-            }
-
-            // Use a setTimeout, otherwise loading the first tab opens a new state for some reason.
-            setTimeout(() => {
-                ionTabs.select(indexToSelect);
-            });
-        }
-    }
-
-    tabs: CoreMainMenuHandlerData[] = [];
-    loaded: boolean;
+    tabs: CoreMainMenuHandlerToDisplay[] = [];
+    loaded = false;
     redirectPage: string;
     redirectParams: any;
     initialTab: number;
+    showTabs = false;
 
     protected subscription;
-    protected moreTabData = {
-        page: 'CoreMainMenuMorePage',
-        title: 'core.more',
-        icon: 'more'
-    };
-    protected moreTabAdded = false;
     protected redirectPageLoaded = false;
 
     constructor(private menuDelegate: CoreMainMenuDelegate, private sitesProvider: CoreSitesProvider, navParams: NavParams,
@@ -80,42 +53,58 @@ export class CoreMainMenuPage implements OnDestroy {
             return;
         }
 
+        this.showTabs = true;
+
         const site = this.sitesProvider.getCurrentSite(),
             displaySiteHome = site.getInfo() && site.getInfo().userhomepage === 0;
 
         this.subscription = this.menuDelegate.getHandlers().subscribe((handlers) => {
             handlers = handlers.slice(0, CoreMainMenuProvider.NUM_MAIN_HANDLERS); // Get main handlers.
 
-            // Check if handlers are already in tabs. Add the ones that aren't.
-            // @todo: https://github.com/ionic-team/ionic/issues/13633
+            // Re-build the list of tabs. If a handler is already in the list, use existing object to prevent re-creating the tab.
+            const newTabs = [];
+
             for (let i = 0; i < handlers.length; i++) {
-                const handler = handlers[i],
-                    shouldSelect = (displaySiteHome && handler.name == 'CoreSiteHome') ||
-                                   (!displaySiteHome && handler.name == 'CoreCourses');
-                let found = false;
+                const handler = handlers[i];
 
-                for (let j = 0; j < this.tabs.length; j++) {
-                    const tab = this.tabs[j];
-                    if (tab.title == handler.title && tab.icon == handler.icon) {
-                        found = true;
-                        if (shouldSelect) {
-                            this.initialTab = j;
-                        }
-                        break;
-                    }
-                }
+                // Check if the handler is already in the tabs list. If so, use it.
+                const tab = this.tabs.find((tab) => {
+                    return tab.title == handler.title && tab.icon == handler.icon;
+                });
 
-                if (!found) {
-                    this.tabs.push(handler);
-                    if (shouldSelect) {
-                        this.initialTab = this.tabs.length;
-                    }
-                }
+                newTabs.push(tab || handler);
             }
 
-            if (!this.moreTabAdded) {
-                this.moreTabAdded = true;
-                this.tabs.push(this.moreTabData); // Add "More" tab.
+            this.tabs = newTabs;
+
+            // Sort them by priority so new handlers are in the right position.
+            this.tabs.sort((a, b) => {
+                return b.priority - a.priority;
+            });
+
+            if (typeof this.initialTab == 'undefined' && !this.loaded) {
+                // Calculate the tab to load.
+                if (this.redirectPage) {
+                    // Check if the redirect page is the root page of any of the tabs.
+                    this.initialTab = 0;
+
+                    for (let i = 0; i < this.tabs.length; i++) {
+                        if (this.tabs[i].page == this.redirectPage) {
+                            this.initialTab = i + 1;
+                            break;
+                        }
+                    }
+                } else {
+                    // By default, course overview will be loaded (3.3+). Check if we need to select Site Home or My Courses.
+                    for (let i = 0; i < this.tabs.length; i++) {
+                        const handler = handlers[i];
+                        if ((displaySiteHome && handler.name == 'CoreSiteHome') ||
+                                (!displaySiteHome && handler.name == 'CoreCourses')) {
+                            this.initialTab = i;
+                            break;
+                        }
+                    }
+                }
             }
 
             this.loaded = this.menuDelegate.areHandlersLoaded();
diff --git a/src/core/mainmenu/providers/delegate.ts b/src/core/mainmenu/providers/delegate.ts
index ea23cad25..cc6c39a04 100644
--- a/src/core/mainmenu/providers/delegate.ts
+++ b/src/core/mainmenu/providers/delegate.ts
@@ -99,6 +99,12 @@ export interface CoreMainMenuHandlerToDisplay extends CoreMainMenuHandlerData {
      * @type {string}
      */
     name?: string;
+
+    /**
+     * Priority of the handler.
+     * @type {number}
+     */
+    priority?: number;
 }
 
 /**
@@ -168,7 +174,9 @@ export class CoreMainMenuDelegate extends CoreDelegate {
 
         // Return only the display data.
         const displayData = handlersData.map((item) => {
+            // Move the name and the priority to the display data.
             item.data.name = item.name;
+            item.data.priority = item.priority;
 
             return item.data;
         });

From e3aee5c3d1f51ceba17722bc286b8afed8d1369c Mon Sep 17 00:00:00 2001
From: Dani Palou <dani@moodle.com>
Date: Wed, 16 May 2018 08:27:17 +0200
Subject: [PATCH 2/4] MOBILE-2416 core: Decorate ion-content to support
 core-ion-tabs

---
 src/app/app.module.ts | 168 +++++++++++++++++++++++++++++++++++++++++-
 1 file changed, 167 insertions(+), 1 deletion(-)

diff --git a/src/app/app.module.ts b/src/app/app.module.ts
index 3f85cb6bf..5bcee3d80 100644
--- a/src/app/app.module.ts
+++ b/src/app/app.module.ts
@@ -15,7 +15,8 @@
 import { BrowserModule } from '@angular/platform-browser';
 import { BrowserAnimationsModule } from '@angular/platform-browser/animations';
 import { NgModule } from '@angular/core';
-import { IonicApp, IonicModule, Platform } from 'ionic-angular';
+import { IonicApp, IonicModule, Platform, Content, ScrollEvent } from 'ionic-angular';
+import { assert } from 'ionic-angular/util/util';
 import { HttpModule } from '@angular/http';
 import { HttpClient, HttpClientModule, HTTP_INTERCEPTORS } from '@angular/common/http';
 
@@ -244,5 +245,170 @@ export class AppModule {
 
         // Execute the init processes.
         initDelegate.executeInitProcesses();
+
+        // Decorate ion-content.
+        this.decorateIonContent();
+    }
+
+    /**
+     * Decorate ion-content to make our ion-tabs work.
+     * https://github.com/ionic-team/ionic/issues/14483
+     */
+    protected decorateIonContent(): void {
+
+        const parsePxUnit = (val: string): number => {
+            return (val.indexOf('px') > 0) ? parseInt(val, 10) : 0;
+        };
+
+        // We need to convert the prototype to any because _readDimensions is private.
+        // tslint:disable: typedef
+        (<any> Content.prototype)._readDimensions = function() {
+            const cachePaddingTop = this._pTop;
+            const cachePaddingRight = this._pRight;
+            const cachePaddingBottom = this._pBottom;
+            const cachePaddingLeft = this._pLeft;
+            const cacheHeaderHeight = this._hdrHeight;
+            const cacheFooterHeight = this._ftrHeight;
+            const cacheTabsPlacement = this._tabsPlacement;
+            let tabsTop = 0;
+            let scrollEvent: ScrollEvent;
+            this._pTop = 0;
+            this._pRight = 0;
+            this._pBottom = 0;
+            this._pLeft = 0;
+            this._hdrHeight = 0;
+            this._ftrHeight = 0;
+            this._tabsPlacement = null;
+            this._tTop = 0;
+            this._fTop = 0;
+            this._fBottom = 0;
+
+            // In certain cases this._scroll is undefined, if that is the case then we should just return.
+            if (!this._scroll) {
+                return;
+            }
+
+            scrollEvent = this._scroll.ev;
+
+            let ele: HTMLElement = this.getNativeElement();
+            if (!ele) {
+                assert(false, 'ele should be valid');
+
+                return;
+            }
+
+            let computedStyle: any;
+            let tagName: string;
+            const parentEle: HTMLElement = ele.parentElement;
+            const children = parentEle.children;
+            for (let i = children.length - 1; i >= 0; i--) {
+                ele = <HTMLElement> children[i];
+                tagName = ele.tagName;
+                if (tagName === 'ION-CONTENT') {
+                    scrollEvent.contentElement = ele;
+
+                    if (this._fullscreen) {
+                    // ******** DOM READ ****************
+                        computedStyle = getComputedStyle(ele);
+                        this._pTop = parsePxUnit(computedStyle.paddingTop);
+                        this._pBottom = parsePxUnit(computedStyle.paddingBottom);
+                        this._pRight = parsePxUnit(computedStyle.paddingRight);
+                        this._pLeft = parsePxUnit(computedStyle.paddingLeft);
+                    }
+
+                } else if (tagName === 'ION-HEADER') {
+                    scrollEvent.headerElement = ele;
+
+                    // ******** DOM READ ****************
+                    this._hdrHeight = ele.clientHeight;
+
+                } else if (tagName === 'ION-FOOTER') {
+                    scrollEvent.footerElement = ele;
+
+                    // ******** DOM READ ****************
+                    this._ftrHeight = ele.clientHeight;
+                    this._footerEle = ele;
+                }
+            }
+
+            ele = parentEle;
+            let tabbarEle: HTMLElement;
+
+            while (ele && ele.tagName !== 'ION-MODAL' && !ele.classList.contains('tab-subpage')) {
+
+                if (ele.tagName.indexOf('ION-TABS') != -1) {
+                    tabbarEle = <HTMLElement> ele.firstElementChild;
+                    // ******** DOM READ ****************
+                    this._tabbarHeight = tabbarEle.clientHeight;
+
+                    if (this._tabsPlacement === null) {
+                        // This is the first tabbar found, remember its position.
+                        this._tabsPlacement = ele.getAttribute('tabsplacement');
+                    }
+                }
+
+                ele = ele.parentElement;
+            }
+
+            // Tabs top
+            if (this._tabs && this._tabsPlacement === 'top') {
+                this._tTop = this._hdrHeight;
+                tabsTop = this._tabs._top;
+            }
+
+            // Toolbar height
+            this._cTop = this._hdrHeight;
+            this._cBottom = this._ftrHeight;
+
+            // Tabs height
+            if (this._tabsPlacement === 'top') {
+                this._cTop += this._tabbarHeight;
+
+            } else if (this._tabsPlacement === 'bottom') {
+                this._cBottom += this._tabbarHeight;
+            }
+
+            // Refresher uses a border which should be hidden unless pulled
+            if (this._hasRefresher) {
+                this._cTop -= 1;
+            }
+
+            // Fixed content shouldn't include content padding
+            this._fTop = this._cTop;
+            this._fBottom = this._cBottom;
+
+            // Handle fullscreen viewport (padding vs margin)
+            if (this._fullscreen) {
+                this._cTop += this._pTop;
+                this._cBottom += this._pBottom;
+            }
+
+            // ******** DOM READ ****************
+            const contentDimensions = this.getContentDimensions();
+            scrollEvent.scrollHeight = contentDimensions.scrollHeight;
+            scrollEvent.scrollWidth = contentDimensions.scrollWidth;
+            scrollEvent.contentHeight = contentDimensions.contentHeight;
+            scrollEvent.contentWidth = contentDimensions.contentWidth;
+            scrollEvent.contentTop = contentDimensions.contentTop;
+            scrollEvent.contentBottom = contentDimensions.contentBottom;
+
+            this._dirty = (
+                cachePaddingTop !== this._pTop ||
+                cachePaddingBottom !== this._pBottom ||
+                cachePaddingLeft !== this._pLeft ||
+                cachePaddingRight !== this._pRight ||
+                cacheHeaderHeight !== this._hdrHeight ||
+                cacheFooterHeight !== this._ftrHeight ||
+                cacheTabsPlacement !== this._tabsPlacement ||
+                tabsTop !== this._tTop ||
+                this._cTop !== this.contentTop ||
+                this._cBottom !== this.contentBottom
+            );
+
+            this._scroll.init(this.getScrollElement(), this._cTop, this._cBottom);
+
+            // Initial imgs refresh.
+            this.imgsUpdate();
+        };
     }
 }

From e80f5a1301b34a6c82385a466bc2c60a7100c3b7 Mon Sep 17 00:00:00 2001
From: Dani Palou <dani@moodle.com>
Date: Wed, 16 May 2018 10:14:56 +0200
Subject: [PATCH 3/4] MOBILE-2416 core: Fix redirect to main menu

---
 src/components/ion-tabs/ion-tabs.ts      | 13 ++++++++++++-
 src/core/course/pages/section/section.ts |  4 ++--
 src/core/login/providers/helper.ts       | 12 +++++++++++-
 src/core/mainmenu/pages/menu/menu.html   |  2 +-
 4 files changed, 26 insertions(+), 5 deletions(-)

diff --git a/src/components/ion-tabs/ion-tabs.ts b/src/components/ion-tabs/ion-tabs.ts
index 19f78a4fa..a3a6bb2cc 100644
--- a/src/components/ion-tabs/ion-tabs.ts
+++ b/src/components/ion-tabs/ion-tabs.ts
@@ -44,6 +44,8 @@ export class CoreIonTabsComponent extends Tabs {
         }
     }
 
+    @Input() selectedDisabled: boolean; // Whether the initial tab selected can be a disabled tab.
+
     @ViewChild('originalTabs') originalTabsRef: ElementRef;
 
     _loaded: boolean; // Whether tabs have been loaded.
@@ -135,7 +137,16 @@ export class CoreIonTabsComponent extends Tabs {
         if (!this.initialized && (this._loaded || typeof this._loaded == 'undefined')) {
             this.initialized = true;
 
-            return super.initTabs();
+            return super.initTabs().then(() => {
+                // Tabs initialized. Force select the tab if it's not enabled.
+                if (this.selectedDisabled && typeof this.selectedIndex != 'undefined') {
+                    const tab = this.getByIndex(this.selectedIndex);
+
+                    if (tab && (!tab.enabled || !tab.show)) {
+                        this.select(tab);
+                    }
+                }
+            });
         } else {
             // Tabs not loaded yet. Set the tab bar position so the tab bar is shown, it'll have a spinner.
             this.setTabbarPosition(-1, 0);
diff --git a/src/core/course/pages/section/section.ts b/src/core/course/pages/section/section.ts
index 3c5f55f39..fd17c61ca 100644
--- a/src/core/course/pages/section/section.ts
+++ b/src/core/course/pages/section/section.ts
@@ -333,13 +333,13 @@ export class CoreCourseSectionPage implements OnDestroy {
      * User entered the page.
      */
     ionViewDidEnter(): void {
-        this.formatComponent.ionViewDidEnter();
+        this.formatComponent && this.formatComponent.ionViewDidEnter();
     }
 
     /**
      * User left the page.
      */
     ionViewDidLeave(): void {
-        this.formatComponent.ionViewDidLeave();
+        this.formatComponent && this.formatComponent.ionViewDidLeave();
     }
 }
diff --git a/src/core/login/providers/helper.ts b/src/core/login/providers/helper.ts
index 8f20a9b08..c295ff14f 100644
--- a/src/core/login/providers/helper.ts
+++ b/src/core/login/providers/helper.ts
@@ -13,6 +13,7 @@
 // limitations under the License.
 
 import { Injectable } from '@angular/core';
+import { Location } from '@angular/common';
 import { Platform } from 'ionic-angular';
 import { TranslateService } from '@ngx-translate/core';
 import { CoreAppProvider } from '@providers/app';
@@ -81,7 +82,8 @@ export class CoreLoginHelperProvider {
             private wsProvider: CoreWSProvider, private translate: TranslateService, private textUtils: CoreTextUtilsProvider,
             private eventsProvider: CoreEventsProvider, private appProvider: CoreAppProvider, private utils: CoreUtilsProvider,
             private urlUtils: CoreUrlUtilsProvider, private configProvider: CoreConfigProvider, private platform: Platform,
-            private initDelegate: CoreInitDelegate, private sitePluginsProvider: CoreSitePluginsProvider) {
+            private initDelegate: CoreInitDelegate, private sitePluginsProvider: CoreSitePluginsProvider,
+            private location: Location) {
         this.logger = logger.getInstance('CoreLoginHelper');
     }
 
@@ -408,6 +410,10 @@ export class CoreLoginHelperProvider {
      * @return {Promise<any>} Promise resolved when done.
      */
     goToSiteInitialPage(): Promise<any> {
+        // Due to DeepLinker, we need to remove the path from the URL before going to main menu.
+        // IonTabs checks the URL to determine which path to load for deep linking, so we clear the URL.
+        this.location.replaceState('');
+
         return this.appProvider.getRootNavController().setRoot('CoreMainMenuPage');
     }
 
@@ -597,6 +603,10 @@ export class CoreLoginHelperProvider {
      * @param {any} params Params to pass to the page.
      */
     protected loadPageInMainMenu(page: string, params: any): void {
+        // Due to DeepLinker, we need to remove the path from the URL before going to main menu.
+        // IonTabs checks the URL to determine which path to load for deep linking, so we clear the URL.
+        this.location.replaceState('');
+
         this.appProvider.getRootNavController().setRoot('CoreMainMenuPage', { redirectPage: page, redirectParams: params });
     }
 
diff --git a/src/core/mainmenu/pages/menu/menu.html b/src/core/mainmenu/pages/menu/menu.html
index 6dcee56ce..b12930640 100644
--- a/src/core/mainmenu/pages/menu/menu.html
+++ b/src/core/mainmenu/pages/menu/menu.html
@@ -1,4 +1,4 @@
-<core-ion-tabs #mainTabs [hidden]="!showTabs" [loaded]="loaded" [selectedIndex]="initialTab" tabsPlacement="bottom" tabsLayout="title-hide">
+<core-ion-tabs #mainTabs [hidden]="!showTabs" [loaded]="loaded" [selectedIndex]="initialTab" [selectedDisabled]="!!redirectPage" tabsPlacement="bottom" tabsLayout="title-hide">
     <core-ion-tab [enabled]="false" [show]="false" [root]="redirectPage" [rootParams]="redirectParams"></core-ion-tab>
     <core-ion-tab *ngFor="let tab of tabs" [root]="tab.page" [rootParams]="tab.pageParams" [tabTitle]="tab.title | translate" [tabIcon]="tab.icon" [tabBadge]="tab.badge" class="{{tab.class}}"></core-ion-tab>
     <core-ion-tab root="CoreMainMenuMorePage" [tabTitle]="'core.more' | translate" tabIcon="more"></core-ion-tab>

From 6f65ed86bccba9d413fa0f9d5133bdfec6d76018 Mon Sep 17 00:00:00 2001
From: Dani Palou <dani@moodle.com>
Date: Wed, 16 May 2018 10:28:56 +0200
Subject: [PATCH 4/4] MOBILE-2416 core: Fix blink in reconnect and site policy

---
 src/core/login/providers/helper.ts | 20 ++++++++++++++++++--
 1 file changed, 18 insertions(+), 2 deletions(-)

diff --git a/src/core/login/providers/helper.ts b/src/core/login/providers/helper.ts
index c295ff14f..a2f704e28 100644
--- a/src/core/login/providers/helper.ts
+++ b/src/core/login/providers/helper.ts
@@ -876,7 +876,15 @@ export class CoreLoginHelperProvider {
             } else {
                 const info = currentSite.getInfo();
                 if (typeof info != 'undefined' && typeof info.username != 'undefined') {
-                    this.appProvider.getRootNavController().setRoot('CoreLoginReconnectPage', {
+                    const rootNavCtrl = this.appProvider.getRootNavController(),
+                        activePage = rootNavCtrl.getActive();
+
+                    // If current page is already reconnect, stop.
+                    if (activePage && activePage.component && activePage.component.name == 'CoreLoginReconnectPage') {
+                        return;
+                    }
+
+                    rootNavCtrl.setRoot('CoreLoginReconnectPage', {
                         infoSiteUrl: info.siteurl,
                         siteUrl: result.siteUrl,
                         siteId: siteId,
@@ -924,7 +932,15 @@ export class CoreLoginHelperProvider {
             return;
         }
 
-        this.appProvider.getRootNavController().setRoot('CoreLoginSitePolicyPage', { siteId: siteId });
+        const rootNavCtrl = this.appProvider.getRootNavController(),
+            activePage = rootNavCtrl.getActive();
+
+        // If current page is already site policy, stop.
+        if (activePage && activePage.component && activePage.component.name == 'CoreLoginSitePolicyPage') {
+            return;
+        }
+
+        rootNavCtrl.setRoot('CoreLoginSitePolicyPage', { siteId: siteId });
     }
 
     /**