From 5567191a45ab8e162e65c2b50c1ae324465455a9 Mon Sep 17 00:00:00 2001 From: "Manu Mtz.-Almeida" Date: Fri, 2 Dec 2016 11:55:26 +0100 Subject: [PATCH] fix(modal): app.navPop() can dismiss modals fixes #8692 --- src/components/app/app-root.ts | 39 +++++++++++++++++++++ src/components/app/app.ts | 57 ++++++++++++++++++------------- src/navigation/view-controller.ts | 35 ++++++++++--------- src/util/mock-providers.ts | 1 + 4 files changed, 91 insertions(+), 41 deletions(-) diff --git a/src/components/app/app-root.ts b/src/components/app/app-root.ts index 319bd8806e4..07800a44880 100644 --- a/src/components/app/app-root.ts +++ b/src/components/app/app-root.ts @@ -6,6 +6,7 @@ import { Ion } from '../ion'; import { OverlayPortal } from '../nav/overlay-portal'; import { Platform } from '../../platform/platform'; import { nativeTimeout } from '../../util/dom'; +import { assert } from '../../util/util'; export const AppRootToken = new OpaqueToken('USERROOT'); @@ -103,12 +104,50 @@ export class IonicApp extends Ion implements OnInit { if (portal === AppPortal.TOAST) { return this._toastPortal; } + // Modals need their own overlay becuase we don't want an ActionSheet + // or Alert to trigger lifecycle events inside a modal if (portal === AppPortal.MODAL) { return this._modalPortal; } return this._overlayPortal; } + /** + * @private + */ + _getActivePortal(): OverlayPortal { + const defaultPortal = this._overlayPortal; + const modalPortal = this._modalPortal; + + assert(defaultPortal, 'default must be valid'); + assert(modalPortal, 'modal must be valid'); + + const hasModal = modalPortal.length() > 0; + const hasDefault = defaultPortal.length() > 0; + + if (!hasModal && !hasDefault) { + return null; + + } else if (hasModal && hasDefault) { + var defaultIndex = defaultPortal.getActive().getZIndex(); + var modalIndex = modalPortal.getActive().getZIndex(); + + if (defaultIndex > modalIndex) { + return defaultPortal; + } else { + assert(modalIndex > defaultIndex, 'modal and default zIndex can not be equal'); + return modalPortal; + } + + } if (hasModal) { + return modalPortal; + + } else if (hasDefault) { + return defaultPortal; + } + + } + /** * @private */ diff --git a/src/components/app/app.ts b/src/components/app/app.ts index ccea638f0b5..c08f5d9af78 100644 --- a/src/components/app/app.ts +++ b/src/components/app/app.ts @@ -3,6 +3,7 @@ import { Title } from '@angular/platform-browser'; import { AppPortal, IonicApp } from './app-root'; import { ClickBlock } from '../../util/click-block'; +import { runInDev } from '../../util/util'; import { Config } from '../../config/config'; import { isNav, isTabs, NavOptions, DIRECTION_FORWARD, DIRECTION_BACK } from '../../navigation/nav-util'; import { NavController } from '../../navigation/nav-controller'; @@ -72,6 +73,14 @@ export class App { // register this back button action with a default priority _platform.registerBackButtonAction(this.navPop.bind(this)); this._disableScrollAssist = _config.getBoolean('disableScrollAssist', false); + + runInDev(() => { + // During developement, navPop can be triggered by calling + // window.ClickBackButton(); + if (!window['HWBackButton']) { + window['HWBackButton'] = this.navPop.bind(this); + } + }); } /** @@ -228,6 +237,10 @@ export class App { * @private */ navPop(): Promise { + if (!this._rootNav || !this.isEnabled()) { + return Promise.resolve(); + } + // function used to climb up all parent nav controllers function navPop(nav: any): Promise { if (nav) { @@ -259,34 +272,30 @@ export class App { // app must be enabled and there must be a // root nav controller for go back to work - if (this._rootNav && this.isEnabled()) { - const portal = this._appRoot._getPortal(); - - // first check if the root navigation has any overlays - // opened in it's portal, like alert/actionsheet/popup - if (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(); - } + const portal = this._appRoot._getActivePortal(); + + // first check if the root navigation has any overlays + // opened in it's portal, like alert/actionsheet/popup + if (portal) { + // 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(); - } + // 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(); } - - return navPromise; } - return Promise.resolve(); + return navPromise; } } diff --git a/src/navigation/view-controller.ts b/src/navigation/view-controller.ts index 0cda7dd2f7d..f1ee623f253 100644 --- a/src/navigation/view-controller.ts +++ b/src/navigation/view-controller.ts @@ -44,31 +44,31 @@ export class ViewController { * Observable to be subscribed to when the current component will become active * @returns {Observable} Returns an observable */ - willEnter: EventEmitter; + willEnter: EventEmitter = new EventEmitter(); /** * Observable to be subscribed to when the current component has become active * @returns {Observable} Returns an observable */ - didEnter: EventEmitter; + didEnter: EventEmitter = new EventEmitter(); /** * Observable to be subscribed to when the current component will no longer be active * @returns {Observable} Returns an observable */ - willLeave: EventEmitter; + willLeave: EventEmitter = new EventEmitter(); /** * Observable to be subscribed to when the current component is no long active * @returns {Observable} Returns an observable */ - didLeave: EventEmitter; + didLeave: EventEmitter = new EventEmitter(); /** * Observable to be subscribed to when the current component has been destroyed * @returns {Observable} Returns an observable */ - willUnload: EventEmitter; + willUnload: EventEmitter = new EventEmitter(); /** @private */ data: any; @@ -105,12 +105,6 @@ export class ViewController { this.data = (data instanceof NavParams ? data.data : (isPresent(data) ? data : {})); this._cssClass = rootCssClass; - - this.willEnter = new EventEmitter(); - this.didEnter = new EventEmitter(); - this.willLeave = new EventEmitter(); - this.didLeave = new EventEmitter(); - this.willUnload = new EventEmitter(); } /** @@ -220,13 +214,13 @@ export class ViewController { */ enableBack(): boolean { // update if it's possible to go back from this nav item - if (this._nav) { - let previousItem = this._nav.getPrevious(this); - // the previous view may exist, but if it's about to be destroyed - // it shouldn't be able to go back to - return !!(previousItem); + if (!this._nav) { + return false; } - return false; + // the previous view may exist, but if it's about to be destroyed + // it shouldn't be able to go back to + const previousItem = this._nav.getPrevious(this); + return !!(previousItem); } /** @@ -278,6 +272,13 @@ export class ViewController { } } + /** + * @private + */ + getZIndex(): number { + return this._zIndex; + } + /** * @private * DOM WRITE diff --git a/src/util/mock-providers.ts b/src/util/mock-providers.ts index 160ee9c502e..075cd0860c2 100644 --- a/src/util/mock-providers.ts +++ b/src/util/mock-providers.ts @@ -55,6 +55,7 @@ export const mockIonicApp = function(app: App, config: Config, platform: Platfor appRoot._loadingPortal = mockOverlayPortal(app, config, platform); appRoot._toastPortal = mockOverlayPortal(app, config, platform); appRoot._overlayPortal = mockOverlayPortal(app, config, platform); + appRoot._modalPortal = mockOverlayPortal(app, config, platform); return appRoot; };