From d35f0e2ffc73a4096c5735de51d8fe353fcd99fb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pau=20Ferrer=20Oca=C3=B1a?= Date: Wed, 21 Nov 2018 15:33:09 +0100 Subject: [PATCH] MOBILE-2738 tabs: Control navigation when backbutton is actioned --- src/components/ion-tabs/ion-tabs.ts | 63 +++++++++++++++-- src/components/tabs/tabs.ts | 47 ++++++++++++- src/providers/app.ts | 101 +++++++++++++++++++++++++++- 3 files changed, 202 insertions(+), 9 deletions(-) diff --git a/src/components/ion-tabs/ion-tabs.ts b/src/components/ion-tabs/ion-tabs.ts index 997887afb..33f8f60c0 100644 --- a/src/components/ion-tabs/ion-tabs.ts +++ b/src/components/ion-tabs/ion-tabs.ts @@ -12,10 +12,12 @@ // 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 { Component, Optional, ElementRef, Renderer, ViewEncapsulation, forwardRef, ViewChild, Input, + OnDestroy } 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'; +import { CoreAppProvider } from '@providers/app'; /** * Equivalent to ion-tabs. It has 2 improvements: @@ -28,7 +30,7 @@ import { CoreUtilsProvider } from '@providers/utils/utils'; encapsulation: ViewEncapsulation.None, providers: [{provide: RootNode, useExisting: forwardRef(() => CoreIonTabsComponent) }] }) -export class CoreIonTabsComponent extends Tabs { +export class CoreIonTabsComponent extends Tabs implements OnDestroy { /** * Whether the tabs have been loaded. If defined, tabs won't be initialized until it's set to true. @@ -62,9 +64,12 @@ export class CoreIonTabsComponent extends 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) { + protected firstSelectedTab: string; + protected unregisterBackButtonAction: any; + + constructor(protected utils: CoreUtilsProvider, protected appProvider: CoreAppProvider, @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); } @@ -75,6 +80,8 @@ export class CoreIonTabsComponent extends Tabs { this.viewInit = true; super.ngAfterViewInit(); + + this.registerBackButtonAction(); } /** @@ -146,6 +153,8 @@ export class CoreIonTabsComponent extends Tabs { this.select(tab); } } + + this.firstSelectedTab = this._selectHistory[0] || null; }); } else { // Tabs not loaded yet. Set the tab bar position so the tab bar is shown, it'll have a spinner. @@ -155,6 +164,42 @@ export class CoreIonTabsComponent extends Tabs { } } + /** + * Register back button action. + */ + protected registerBackButtonAction(): void { + this.unregisterBackButtonAction = this.appProvider.registerBackButtonAction(() => { + let tab = this.previousTab(true); + if (tab) { + const selectedTab = this.getSelected(); + + // Remove curent and previous tabs from history. + this._selectHistory = this._selectHistory.filter((tabId) => { + return selectedTab.id != tabId && tab.id != tabId; + }); + + this.select(tab); + + return true; + } else { + const selected = this.getSelected(); + if (selected && this.firstSelectedTab && selected.id != this.firstSelectedTab) { + // All history is gone but we are not in the first selected tab. + this._selectHistory = []; + + tab = this._tabs.find((t) => { return t.id === this.firstSelectedTab; }); + if (tab && tab.enabled && tab.show) { + this.select(tab); + + return true; + } + } + } + + return false; + }, 250); + } + /** * Remove a tab from the list of tabs. * @@ -204,4 +249,12 @@ export class CoreIonTabsComponent extends Tabs { }); } } + + /** + * Component destroyed. + */ + ngOnDestroy(): void { + // Unregister the custom back button action for this page + this.unregisterBackButtonAction && this.unregisterBackButtonAction(); + } } diff --git a/src/components/tabs/tabs.ts b/src/components/tabs/tabs.ts index 81f2f89f5..2dc4faf73 100644 --- a/src/components/tabs/tabs.ts +++ b/src/components/tabs/tabs.ts @@ -16,9 +16,10 @@ import { Component, Input, Output, EventEmitter, OnInit, OnChanges, OnDestroy, AfterViewInit, ViewChild, ElementRef, SimpleChange } from '@angular/core'; -import { CoreTabComponent } from './tab'; import { Content, Slides } from 'ionic-angular'; import { CoreDomUtilsProvider } from '@providers/utils/dom'; +import { CoreAppProvider } from '@providers/app'; +import { CoreTabComponent } from './tab'; /** * This component displays some tabs that usually share data between them. @@ -72,8 +73,13 @@ export class CoreTabsComponent implements OnInit, AfterViewInit, OnChanges, OnDe protected isCurrentView = true; protected shouldSlideToInitial = false; // Whether we need to slide to the initial slide because it's out of view. protected hasSliddenToInitial = false; // Whether we've already slidden to the initial slide or there was no need. + protected selectHistory = []; - constructor(element: ElementRef, protected content: Content, protected domUtils: CoreDomUtilsProvider) { + protected firstSelectedTab: number; + protected unregisterBackButtonAction: any; + + constructor(element: ElementRef, protected content: Content, protected domUtils: CoreDomUtilsProvider, + protected appProvider: CoreAppProvider) { this.tabBarElement = element.nativeElement; } @@ -128,12 +134,47 @@ export class CoreTabsComponent implements OnInit, AfterViewInit, OnChanges, OnDe if (this.initialized) { this.calculateSlides(); } + + this.registerBackButtonAction(); + } + + /** + * Register back button action. + */ + protected registerBackButtonAction(): void { + this.unregisterBackButtonAction = this.appProvider.registerBackButtonAction(() => { + // The previous page in history is not the last one, we need the previous one. + if (this.selectHistory.length > 1) { + const tab = this.selectHistory[this.selectHistory.length - 2]; + + // Remove curent and previous tabs from history. + this.selectHistory = this.selectHistory.filter((tabId) => { + return this.selected != tabId && tab != tabId; + }); + + this.selectTab(tab); + + return true; + } else if (this.selected != this.firstSelectedTab) { + // All history is gone but we are not in the first selected tab. + this.selectHistory = []; + + this.selectTab(this.firstSelectedTab); + + return true; + } + + return false; + }, 750); } /** * User left the page that contains the component. */ ionViewDidLeave(): void { + // Unregister the custom back button action for this page + this.unregisterBackButtonAction && this.unregisterBackButtonAction(); + this.isCurrentView = false; } @@ -229,6 +270,7 @@ export class CoreTabsComponent implements OnInit, AfterViewInit, OnChanges, OnDe } if (selectedTab) { + this.firstSelectedTab = selectedIndex; this.selectTab(selectedIndex); } @@ -405,6 +447,7 @@ export class CoreTabsComponent implements OnInit, AfterViewInit, OnChanges, OnDe this.slides.slideTo(index); } + this.selectHistory.push(index); this.selected = index; newTab.selectTab(); this.ionChange.emit(newTab); diff --git a/src/providers/app.ts b/src/providers/app.ts index b9b1d8138..d6c6d63d8 100644 --- a/src/providers/app.ts +++ b/src/providers/app.ts @@ -13,7 +13,7 @@ // limitations under the License. import { Injectable, NgZone } from '@angular/core'; -import { Platform, App, NavController } from 'ionic-angular'; +import { Platform, App, NavController, MenuController } from 'ionic-angular'; import { Keyboard } from '@ionic-native/keyboard'; import { Network } from '@ionic-native/network'; @@ -68,9 +68,11 @@ export class CoreAppProvider { protected logger; protected ssoAuthenticationPromise: Promise; protected isKeyboardShown = false; + protected backActions = []; constructor(dbProvider: CoreDbProvider, private platform: Platform, private keyboard: Keyboard, private appCtrl: App, - private network: Network, logger: CoreLoggerProvider, events: CoreEventsProvider, zone: NgZone) { + private network: Network, logger: CoreLoggerProvider, events: CoreEventsProvider, zone: NgZone, + private menuCtrl: MenuController) { this.logger = logger.getInstance('CoreAppProvider'); this.db = dbProvider.getDB(this.DBNAME); @@ -92,6 +94,10 @@ export class CoreAppProvider { events.trigger(CoreEventsProvider.KEYBOARD_CHANGE, 0); }); }); + + this.platform.registerBackButtonAction(() => { + this.backButtonAction(); + }, 100); } /** @@ -382,4 +388,95 @@ export class CoreAppProvider { } } } + + /** + * Implement the backbutton actions pile. + */ + backButtonAction(): void { + let x = 0; + for (; x < this.backActions.length; x++) { + if (this.backActions[x].priority < 1000) { + break; + } + // Stop in the first action taken. + if (this.backActions[x].fn()) { + return; + } + } + + // Close open modals if any. + if (this.menuCtrl && this.menuCtrl.isOpen()) { + this.menuCtrl.close(); + + return; + } + + // Remaining actions will have priority less than 1000. + for (; x < this.backActions.length; x++) { + if (this.backActions[x].priority < 500) { + break; + } + // Stop in the first action taken. + if (this.backActions[x].fn()) { + return; + } + } + + // Nothing found, go back. + const navPromise = this.appCtrl.navPop(); + if (navPromise) { + return; + } + + // No views to go back to. + + // Remaining actions will have priority less than 500. + for (; x < this.backActions.length; x++) { + // Stop in the first action taken. + if (this.backActions[x].fn()) { + return; + } + } + + // Ionic will decide (exit the app). + this.appCtrl.goBack(); + } + + /** + * The back button event is triggered when the user presses the native + * platform's back button, also referred to as the "hardware" back button. + * This event is only used within Cordova apps running on Android and + * Windows platforms. This event is not fired on iOS since iOS doesn't come + * with a hardware back button in the same sense an Android or Windows device + * does. + * + * Registering a hardware back button action and setting a priority allows + * apps to control which action should be called when the hardware back + * button is pressed. This method decides which of the registered back button + * actions has the highest priority and should be called. + * + * @param {Function} fn Called when the back button is pressed, + * if this registered action has the highest priority. + * @param {number} priority Set the priority for this action. All actions sorted by priority will be executed since one of + * them returns true. + * * Priorities higher or equal than 1000 will go before closing modals + * * Priorities lower than 500 will only be executed if you are in the first state of the app (before exit). + * @returns {Function} A function that, when called, will unregister + * the back button action. + */ + registerBackButtonAction(fn: Function, priority: number = 0): Function { + const action = { fn: fn, priority: priority }; + + this.backActions.push(action); + + this.backActions.sort((a, b) => { + return b.priority - a.priority; + }); + + return (): boolean => { + const index = this.backActions.indexOf(action); + + return index >= 0 && !!this.backActions.splice(index, 1); + }; + } }