MOBILE-2738 tabs: Control navigation when backbutton is actioned

main
Pau Ferrer Ocaña 2018-11-21 15:33:09 +01:00
parent ac9bd47da0
commit d35f0e2ffc
3 changed files with 202 additions and 9 deletions

View File

@ -12,10 +12,12 @@
// See the License for the specific language governing permissions and // See the License for the specific language governing permissions and
// limitations under the License. // 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 { Tabs, NavController, ViewController, App, Config, Platform, DeepLinker, Keyboard, RootNode } from 'ionic-angular';
import { CoreIonTabComponent } from './ion-tab'; import { CoreIonTabComponent } from './ion-tab';
import { CoreUtilsProvider } from '@providers/utils/utils'; import { CoreUtilsProvider } from '@providers/utils/utils';
import { CoreAppProvider } from '@providers/app';
/** /**
* Equivalent to ion-tabs. It has 2 improvements: * Equivalent to ion-tabs. It has 2 improvements:
@ -28,7 +30,7 @@ import { CoreUtilsProvider } from '@providers/utils/utils';
encapsulation: ViewEncapsulation.None, encapsulation: ViewEncapsulation.None,
providers: [{provide: RootNode, useExisting: forwardRef(() => CoreIonTabsComponent) }] 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. * 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 viewInit = false; // Whether the view has been initialized.
protected initialized = false; // Whether tabs have been initialized. protected initialized = false; // Whether tabs have been initialized.
constructor(protected utils: CoreUtilsProvider, @Optional() parent: NavController, @Optional() viewCtrl: ViewController, protected firstSelectedTab: string;
_app: App, config: Config, elementRef: ElementRef, _plt: Platform, renderer: Renderer, _linker: DeepLinker, protected unregisterBackButtonAction: any;
keyboard?: Keyboard) {
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); super(parent, viewCtrl, _app, config, elementRef, _plt, renderer, _linker, keyboard);
} }
@ -75,6 +80,8 @@ export class CoreIonTabsComponent extends Tabs {
this.viewInit = true; this.viewInit = true;
super.ngAfterViewInit(); super.ngAfterViewInit();
this.registerBackButtonAction();
} }
/** /**
@ -146,6 +153,8 @@ export class CoreIonTabsComponent extends Tabs {
this.select(tab); this.select(tab);
} }
} }
this.firstSelectedTab = this._selectHistory[0] || null;
}); });
} else { } else {
// Tabs not loaded yet. Set the tab bar position so the tab bar is shown, it'll have a spinner. // 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. * 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();
}
} }

View File

@ -16,9 +16,10 @@ import {
Component, Input, Output, EventEmitter, OnInit, OnChanges, OnDestroy, AfterViewInit, ViewChild, ElementRef, Component, Input, Output, EventEmitter, OnInit, OnChanges, OnDestroy, AfterViewInit, ViewChild, ElementRef,
SimpleChange SimpleChange
} from '@angular/core'; } from '@angular/core';
import { CoreTabComponent } from './tab';
import { Content, Slides } from 'ionic-angular'; import { Content, Slides } from 'ionic-angular';
import { CoreDomUtilsProvider } from '@providers/utils/dom'; 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. * 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 isCurrentView = true;
protected shouldSlideToInitial = false; // Whether we need to slide to the initial slide because it's out of view. 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 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; this.tabBarElement = element.nativeElement;
} }
@ -128,12 +134,47 @@ export class CoreTabsComponent implements OnInit, AfterViewInit, OnChanges, OnDe
if (this.initialized) { if (this.initialized) {
this.calculateSlides(); 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. * User left the page that contains the component.
*/ */
ionViewDidLeave(): void { ionViewDidLeave(): void {
// Unregister the custom back button action for this page
this.unregisterBackButtonAction && this.unregisterBackButtonAction();
this.isCurrentView = false; this.isCurrentView = false;
} }
@ -229,6 +270,7 @@ export class CoreTabsComponent implements OnInit, AfterViewInit, OnChanges, OnDe
} }
if (selectedTab) { if (selectedTab) {
this.firstSelectedTab = selectedIndex;
this.selectTab(selectedIndex); this.selectTab(selectedIndex);
} }
@ -405,6 +447,7 @@ export class CoreTabsComponent implements OnInit, AfterViewInit, OnChanges, OnDe
this.slides.slideTo(index); this.slides.slideTo(index);
} }
this.selectHistory.push(index);
this.selected = index; this.selected = index;
newTab.selectTab(); newTab.selectTab();
this.ionChange.emit(newTab); this.ionChange.emit(newTab);

View File

@ -13,7 +13,7 @@
// limitations under the License. // limitations under the License.
import { Injectable, NgZone } from '@angular/core'; 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 { Keyboard } from '@ionic-native/keyboard';
import { Network } from '@ionic-native/network'; import { Network } from '@ionic-native/network';
@ -68,9 +68,11 @@ export class CoreAppProvider {
protected logger; protected logger;
protected ssoAuthenticationPromise: Promise<any>; protected ssoAuthenticationPromise: Promise<any>;
protected isKeyboardShown = false; protected isKeyboardShown = false;
protected backActions = [];
constructor(dbProvider: CoreDbProvider, private platform: Platform, private keyboard: Keyboard, private appCtrl: App, 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.logger = logger.getInstance('CoreAppProvider');
this.db = dbProvider.getDB(this.DBNAME); this.db = dbProvider.getDB(this.DBNAME);
@ -92,6 +94,10 @@ export class CoreAppProvider {
events.trigger(CoreEventsProvider.KEYBOARD_CHANGE, 0); 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);
};
}
} }