diff --git a/src/components/app/app.ts b/src/components/app/app.ts index b3df0d51d27..a0f4753b017 100644 --- a/src/components/app/app.ts +++ b/src/components/app/app.ts @@ -1,9 +1,11 @@ import {Injectable, Injector} from '@angular/core'; import {Title} from '@angular/platform-browser'; -import {Config} from '../../config/config'; import {ClickBlock} from '../../util/click-block'; +import {Config} from '../../config/config'; +import {NavController} from '../nav/nav-controller'; import {Platform} from '../../platform/platform'; +import {Tabs} from '../tabs/tabs'; /** @@ -15,24 +17,17 @@ export class App { private _scrollTime: number = 0; private _title: string = ''; private _titleSrv: Title = new Title(); - private _rootNav: any = null; + private _rootNav: NavController = null; private _appInjector: Injector; constructor( private _config: Config, private _clickBlock: ClickBlock, - platform: Platform + private _platform: Platform ) { - platform.backButton.subscribe(() => { - let activeNav = this.getActiveNav(); - if (activeNav) { - if (activeNav.length() === 1) { - platform.exitApp(); - } else { - activeNav.pop(); - } - } - }); + // listen for hardware back button events + // register this back button action with a default priority + _platform.registerBackButtonAction(this.navPop.bind(this)); } /** @@ -100,7 +95,7 @@ export class App { /** * @private */ - getActiveNav(): any { + getActiveNav(): NavController { var nav = this._rootNav || null; var activeChildNav: any; @@ -118,7 +113,7 @@ export class App { /** * @private */ - getRootNav(): any { + getRootNav(): NavController { return this._rootNav; } @@ -129,6 +124,72 @@ export class App { this._rootNav = nav; } + /** + * @private + */ + navPop(): Promise { + // function used to climb up all parent nav controllers + function navPop(nav: any): Promise { + if (nav) { + if (nav.length && nav.length() > 1) { + // this nav controller has more than one view + // pop the current view on this nav and we're done here + console.debug('app, goBack pop nav'); + return nav.pop(); + + } else if (nav.previousTab) { + // FYI, using "nav instanceof Tabs" throws a Promise runtime error for whatever reason, idk + // this is a Tabs container + // see if there is a valid previous tab to go to + let prevTab = nav.previousTab(true); + if (prevTab) { + console.debug('app, goBack previous tab'); + nav.select(prevTab); + return Promise.resolve(); + } + } + + // try again using the parent nav (if there is one) + return navPop(nav.parent); + } + + // nerp, never found nav that could pop off a view + return null; + } + + // app must be enabled and there must be a + // root nav controller for go back to work + if (this._rootNav && this.isEnabled()) { + + // first check if the root navigation has any overlays + // opened in it's portal, like alert/actionsheet/popup + let portal = this._rootNav.getPortal && this._rootNav.getPortal(); + if (portal && portal.length() > 0) { + // there is an overlay view in the portal + // let's pop this one off to go back + console.debug('app, goBack pop overlay'); + return portal.pop(); + } + + // next get the active nav, check itself and climb up all + // of its parent navs until it finds a nav that can pop + let navPromise = navPop(this.getActiveNav()); + if (navPromise === null) { + // no views to go back to + // let's exit the app + if (this._config.getBoolean('navExitApp', true)) { + console.debug('app, goBack exitApp'); + this._platform.exitApp(); + } + + } else { + return navPromise; + } + } + + return Promise.resolve(); + } + /** * @private */ diff --git a/src/components/app/test/app.spec.ts b/src/components/app/test/app.spec.ts index 44b63a56bf5..9ec924992b9 100644 --- a/src/components/app/test/app.spec.ts +++ b/src/components/app/test/app.spec.ts @@ -1,9 +1,277 @@ +import {Component} from '@angular/core'; import {App, Nav, Tabs, Tab, NavOptions, Config, ViewController, Platform} from '../../../../src'; export function run() { -describe('IonicApp', () => { +describe('App', () => { + + describe('navPop', () => { + + it('should select the previous tab', () => { + let nav = mockNav(); + let portal = mockNav(); + nav.setPortal(portal); + app.setRootNav(nav); + + let tabs = mockTabs(); + let tab1 = mockTab(tabs); + let tab2 = mockTab(tabs); + nav.registerChildNav(tabs); + + tabs.select(tab1); + tabs.select(tab2); + + expect(tabs.selectHistory).toEqual([tab1.id, tab2.id]); + + spyOn(platform, 'exitApp'); + spyOn(tabs, 'select'); + spyOn(tab1, 'pop'); + spyOn(tab2, 'pop'); + spyOn(portal, 'pop'); + + app.navPop(); + + expect(tabs.select).toHaveBeenCalledWith(tab1); + expect(tab1.pop).not.toHaveBeenCalled(); + expect(tab2.pop).not.toHaveBeenCalled(); + expect(portal.pop).not.toHaveBeenCalled(); + expect(platform.exitApp).not.toHaveBeenCalled(); + }); + + it('should pop from the active tab, when tabs is nested is the root nav', () => { + let nav = mockNav(); + let portal = mockNav(); + nav.setPortal(portal); + app.setRootNav(nav); + + let tabs = mockTabs(); + let tab1 = mockTab(tabs); + let tab2 = mockTab(tabs); + let tab3 = mockTab(tabs); + nav.registerChildNav(tabs); + + tab2.setSelected(true); + + spyOn(platform, 'exitApp'); + spyOn(tab2, 'pop'); + spyOn(portal, 'pop'); + + let view1 = new ViewController(); + let view2 = new ViewController(); + tab2._views = [view1, view2]; + + app.navPop(); + + expect(tab2.pop).toHaveBeenCalled(); + expect(portal.pop).not.toHaveBeenCalled(); + expect(platform.exitApp).not.toHaveBeenCalled(); + }); + + it('should pop from the active tab, when tabs is the root', () => { + let tabs = mockTabs(); + let tab1 = mockTab(tabs); + let tab2 = mockTab(tabs); + let tab3 = mockTab(tabs); + app.setRootNav(tabs); + + tab2.setSelected(true); + + spyOn(platform, 'exitApp'); + spyOn(tab2, 'pop'); + + let view1 = new ViewController(); + let view2 = new ViewController(); + tab2._views = [view1, view2]; + + app.navPop(); + + expect(tab2.pop).toHaveBeenCalled(); + expect(platform.exitApp).not.toHaveBeenCalled(); + }); + + it('should pop the root nav when nested nav has less than 2 views', () => { + let rootNav = mockNav(); + let nestedNav = mockNav(); + let portal = mockNav(); + rootNav.setPortal(portal); + rootNav.registerChildNav(nestedNav); + nestedNav.parent = rootNav; + app.setRootNav(rootNav); + + spyOn(platform, 'exitApp'); + spyOn(rootNav, 'pop'); + spyOn(nestedNav, 'pop'); + spyOn(portal, 'pop'); + + let rootView1 = new ViewController(); + let rootView2 = new ViewController(); + rootNav._views = [rootView1, rootView2]; + + let nestedView1 = new ViewController(); + nestedNav._views = [nestedView1]; + + app.navPop(); + + expect(portal.pop).not.toHaveBeenCalled(); + expect(rootNav.pop).toHaveBeenCalled(); + expect(nestedNav.pop).not.toHaveBeenCalled(); + expect(platform.exitApp).not.toHaveBeenCalled(); + }); + + it('should pop a view from the nested nav that has more than 1 view', () => { + let rootNav = mockNav(); + let nestedNav = mockNav(); + let portal = mockNav(); + rootNav.setPortal(portal); + app.setRootNav(rootNav); + rootNav.registerChildNav(nestedNav); + + spyOn(platform, 'exitApp'); + spyOn(rootNav, 'pop'); + spyOn(nestedNav, 'pop'); + spyOn(portal, 'pop'); + + let rootView1 = new ViewController(); + let rootView2 = new ViewController(); + rootNav._views = [rootView1, rootView2]; + + let nestedView1 = new ViewController(); + let nestedView2 = new ViewController(); + nestedNav._views = [nestedView1, nestedView2]; + + app.navPop(); + + expect(portal.pop).not.toHaveBeenCalled(); + expect(rootNav.pop).not.toHaveBeenCalled(); + expect(nestedNav.pop).toHaveBeenCalled(); + expect(platform.exitApp).not.toHaveBeenCalled(); + }); + + it('should pop the overlay in the portal of the root nav', () => { + let nav = mockNav(); + let portal = mockNav(); + nav.setPortal(portal); + app.setRootNav(nav); + + spyOn(platform, 'exitApp'); + spyOn(nav, 'pop'); + spyOn(portal, 'pop'); + + let view1 = new ViewController(); + let view2 = new ViewController(); + nav._views = [view1, view2]; + + let overlay = new ViewController(); + portal._views = [overlay]; + + app.navPop(); + + expect(portal.pop).toHaveBeenCalled(); + expect(nav.pop).not.toHaveBeenCalled(); + expect(platform.exitApp).not.toHaveBeenCalled(); + }); + + it('should pop the second view in the root nav', () => { + let nav = mockNav(); + let portal = mockNav(); + nav.setPortal(portal); + app.setRootNav(nav); + + spyOn(platform, 'exitApp'); + spyOn(nav, 'pop'); + spyOn(portal, 'pop'); + + let view1 = new ViewController(); + let view2 = new ViewController(); + nav._views = [view1, view2]; + + app.navPop(); + + expect(portal.pop).not.toHaveBeenCalled(); + expect(nav.pop).toHaveBeenCalled(); + expect(platform.exitApp).not.toHaveBeenCalled(); + }); + + it('should exit app when only one view in the root nav', () => { + let nav = mockNav(); + let portal = mockNav(); + nav.setPortal(portal); + app.setRootNav(nav); + + spyOn(platform, 'exitApp'); + spyOn(nav, 'pop'); + spyOn(portal, 'pop'); + + let view1 = new ViewController(); + nav._views = [view1]; + + expect(app.getActiveNav()).toBe(nav); + expect(nav.first()).toBe(view1); + + app.navPop(); + + expect(portal.pop).not.toHaveBeenCalled(); + expect(nav.pop).not.toHaveBeenCalled(); + expect(platform.exitApp).toHaveBeenCalled(); + }); + + it('should not exit app when only one view in the root nav, but navExitApp config set', () => { + let nav = mockNav(); + let portal = mockNav(); + nav.setPortal(portal); + app.setRootNav(nav); + + spyOn(platform, 'exitApp'); + spyOn(nav, 'pop'); + spyOn(portal, 'pop'); + + config.set('navExitApp', false); + + let view1 = new ViewController(); + nav._views = [view1]; + + expect(app.getActiveNav()).toBe(nav); + expect(nav.first()).toBe(view1); + + app.navPop(); + + expect(portal.pop).not.toHaveBeenCalled(); + expect(nav.pop).not.toHaveBeenCalled(); + expect(platform.exitApp).not.toHaveBeenCalled(); + }); + + it('should not go back if app is not enabled', () => { + let nav = mockNav(); + let portal = mockNav(); + nav.setPortal(portal); + app.setRootNav(nav); + + spyOn(platform, 'exitApp'); + spyOn(nav, 'pop'); + spyOn(portal, 'pop'); + + let view1 = new ViewController(); + nav._views = [view1]; + + app.setEnabled(false, 10000); + + app.navPop(); + + expect(portal.pop).not.toHaveBeenCalled(); + expect(nav.pop).not.toHaveBeenCalled(); + expect(platform.exitApp).not.toHaveBeenCalled(); + }); + + it('should not go back if there is no root nav', () => { + spyOn(platform, 'exitApp'); + + app.navPop(); + + expect(platform.exitApp).not.toHaveBeenCalled(); + }); + + }); describe('getActiveNav', () => { @@ -27,7 +295,7 @@ describe('IonicApp', () => { expect(app.getActiveNav()).toBe(nav3); }); - it('should get active NavController when using tabs', () => { + it('should get active NavController when using tabs, nested in a root nav', () => { let nav = mockNav(); app.setRootNav(nav); @@ -46,6 +314,22 @@ describe('IonicApp', () => { expect(app.getActiveNav()).toBe(tab3); }); + it('should get active tab NavController when using tabs, and tabs is the root', () => { + let tabs = mockTabs(); + let tab1 = mockTab(tabs); + let tab2 = mockTab(tabs); + let tab3 = mockTab(tabs); + app.setRootNav(tabs); + + tab2.setSelected(true); + + expect(app.getActiveNav()).toBe(tab2); + + tab2.setSelected(false); + tab3.setSelected(true); + expect(app.getActiveNav()).toBe(tab3); + }); + it('should get active NavController when nested 3 deep', () => { let nav1 = mockNav(); let nav2 = mockNav(); @@ -170,9 +454,18 @@ describe('IonicApp', () => { } function mockTab(parentTabs: Tabs): Tab { - return new Tab(parentTabs, app, config, null, null, null, null, null, _cd); + var tab = new Tab(parentTabs, app, config, null, null, null, null, null, _cd); + parentTabs.add(tab); + tab.root = SomePage; + tab.load = function(opts: any, cb: Function) { + cb(); + }; + return tab; } + @Component({}) + class SomePage {} + beforeEach(() => { config = new Config(); platform = new Platform(); diff --git a/src/components/modal/modal.ts b/src/components/modal/modal.ts index 7bc4f353cef..2b415962a6f 100644 --- a/src/components/modal/modal.ts +++ b/src/components/modal/modal.ts @@ -118,6 +118,7 @@ export class Modal extends ViewController { this.modalViewType = componentType.name; this.viewType = 'modal'; this.isOverlay = true; + this.usePortal = true; } /** diff --git a/src/components/nav/nav-controller.ts b/src/components/nav/nav-controller.ts index 61b391bfca8..5edd378dbd4 100644 --- a/src/components/nav/nav-controller.ts +++ b/src/components/nav/nav-controller.ts @@ -6,8 +6,8 @@ import {Config} from '../../config/config'; import {Ion} from '../ion'; import {isBlank, pascalCaseToDashCase} from '../../util/util'; import {Keyboard} from '../../util/keyboard'; -import {NavParams} from './nav-params'; import {MenuController} from '../menu/menu-controller'; +import {NavParams} from './nav-params'; import {NavPortal} from './nav-portal'; import {SwipeBackGesture} from './swipe-back'; import {Transition} from '../../transitions/transition'; @@ -245,6 +245,13 @@ export class NavController extends Ion { this.viewDidUnload = new EventEmitter(); } + /** + * @private + */ + getPortal(): NavController { + return this._portal; + } + /** * @private */ diff --git a/src/components/nav/test/nested/index.ts b/src/components/nav/test/nested/index.ts index 31868369531..663d120ac4a 100644 --- a/src/components/nav/test/nested/index.ts +++ b/src/components/nav/test/nested/index.ts @@ -1,6 +1,6 @@ import {Component, ViewChild} from '@angular/core'; -import {ionicBootstrap, NavParams, NavController, ViewController, MenuController} from '../../../../../src'; -import {Config, Nav} from '../../../../../src'; +import {ionicBootstrap, NavController, MenuController} from '../../../../../src'; +import {Config, Nav, App} from '../../../../../src'; @Component({ @@ -9,16 +9,21 @@ import {Config, Nav} from '../../../../../src'; Login - +

+

` }) export class Login { - constructor(private nav: NavController) {} + constructor(private nav: NavController, private app: App) {} goToAccount() { this.nav.push(Account); } + + goBack() { + this.app.navPop(); + } } @@ -39,21 +44,22 @@ export class Login { + - + ` }) export class Account { - @ViewChild('account-nav') accountNav: Nav; + @ViewChild('accountNav') accountNav: Nav; - rootPage = Dashboard; + root = Dashboard; - constructor(private menu: MenuController, private nav: NavController) { - - } + constructor(private menu: MenuController, private app: App) {} goToProfile() { this.accountNav.setRoot(Profile).then(() => { @@ -68,7 +74,13 @@ export class Account { } logOut() { - this.nav.parent.setRoot(Login, null, { animate: true }); + this.accountNav.setRoot(Login, null, { animate: true }).then(() => { + this.menu.close(); + }); + } + + goBack() { + this.app.navPop(); } } @@ -84,21 +96,27 @@ export class Account {

+

` }) export class Dashboard { - constructor(private nav: NavController) {} + constructor(private nav: NavController, private app: App) {} goToProfile() { this.nav.push(Profile); } + logOut() { this.nav.parent.setRoot(Login, null, { animate: true, direction: 'back' }); } + + goBack() { + this.app.navPop(); + } } @@ -113,11 +131,12 @@ export class Dashboard {

+

` }) export class Profile { - constructor(private nav: NavController) {} + constructor(private nav: NavController, private app: App) {} goToDashboard() { this.nav.push(Dashboard); @@ -129,6 +148,10 @@ export class Profile { direction: 'back' }); } + + goBack() { + this.app.navPop(); + } } diff --git a/src/components/tabs/test/basic/index.ts b/src/components/tabs/test/basic/index.ts index 2351767a4e3..a9fd393064c 100644 --- a/src/components/tabs/test/basic/index.ts +++ b/src/components/tabs/test/basic/index.ts @@ -1,5 +1,5 @@ import {Component} from '@angular/core'; -import {ionicBootstrap, NavController, Alert, Modal, ViewController} from '../../../../../src'; +import {ionicBootstrap, NavController, App, Alert, Modal, ViewController, Tab, Tabs} from '../../../../../src'; // // Modal @@ -34,6 +34,9 @@ import {ionicBootstrap, NavController, Alert, Modal, ViewController} from '../.. + ` @@ -41,7 +44,7 @@ import {ionicBootstrap, NavController, Alert, Modal, ViewController} from '../.. class MyModal { items: any[] = []; - constructor(private viewCtrl: ViewController) { + constructor(private viewCtrl: ViewController, private app: App) { for (var i = 1; i <= 10; i++) { this.items.push(i); } @@ -52,6 +55,10 @@ class MyModal { // can "dismiss" itself and pass back data this.viewCtrl.dismiss(); } + + appNavPop() { + this.app.navPop(); + } } // @@ -69,17 +76,31 @@ class MyModal { Item {{i}} {{i}} {{i}} {{i}} +

+ +

+

+ +

` }) export class Tab1 { items: any[] = []; - constructor() { + constructor(private tabs: Tabs, private app: App) { for (var i = 1; i <= 250; i++) { this.items.push(i); } } + + selectPrevious() { + this.tabs.select(this.tabs.previousTab()); + } + + appNavPop() { + this.app.navPop(); + } } // @@ -103,13 +124,19 @@ export class Tab1 { +

+ +

+

+ +

` }) export class Tab2 { sessions: any[] = []; - constructor() { + constructor(private tabs: Tabs, private app: App) { for (var i = 1; i <= 250; i++) { this.sessions.push({ name: 'Name ' + i, @@ -117,6 +144,14 @@ export class Tab2 { }); } } + + selectPrevious() { + this.tabs.select(this.tabs.previousTab()); + } + + appNavPop() { + this.app.navPop(); + } } // @@ -136,11 +171,17 @@ export class Tab2 {

+

+ +

+

+ +

` }) export class Tab3 { - constructor(private nav: NavController) {} + constructor(private nav: NavController, private tabs: Tabs, private app: App) {} presentAlert() { let alert = Alert.create({ @@ -154,6 +195,14 @@ export class Tab3 { let modal = Modal.create(MyModal); this.nav.present(modal); } + + selectPrevious() { + this.tabs.select(this.tabs.previousTab()); + } + + appNavPop() { + this.app.navPop(); + } } @@ -184,11 +233,11 @@ export class TabsPage { root2 = Tab2; root3 = Tab3; - onChange(ev) { + onChange(ev: Tab) { console.log("Changed tab", ev); } - onSelect(ev) { + onSelect(ev: Tab) { console.log("Selected tab", ev); } }