diff --git a/src/app/app.module.ts b/src/app/app.module.ts index cec2dc5da..37abd9b1d 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'; @@ -246,5 +247,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 + ( 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 = 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 = 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(); + }; } } 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..a3a6bb2cc --- /dev/null +++ b/src/components/ion-tabs/ion-tabs.ts @@ -0,0 +1,207 @@ +// (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(); + }); + } + } + + @Input() selectedDisabled: boolean; // Whether the initial tab selected can be a disabled tab. + + @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().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); + + 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/course/pages/section/section.ts b/src/core/course/pages/section/section.ts index 2d31e9a50..27f983028 100644 --- a/src/core/course/pages/section/section.ts +++ b/src/core/course/pages/section/section.ts @@ -347,13 +347,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/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/login/providers/helper.ts b/src/core/login/providers/helper.ts index 8f20a9b08..a2f704e28 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} Promise resolved when done. */ goToSiteInitialPage(): Promise { + // 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 }); } @@ -866,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, @@ -914,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 }); } /** diff --git a/src/core/mainmenu/pages/menu/menu.html b/src/core/mainmenu/pages/menu/menu.html index 54839b8ee..b12930640 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; });