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: '
',
+ 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 @@
+
+
+
+
+
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} Promise resolved when done.
+ */
+ initTabs(): Promise {
+ 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 @@
-
-
-
-
\ No newline at end of file
+
+
+
+
+
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;
});