From 0a7d865975b5093bc791b36bab90acdfb36a63ff Mon Sep 17 00:00:00 2001 From: Adam Bradley Date: Fri, 15 Jul 2016 15:54:56 -0500 Subject: [PATCH] refactor(nav): create NavControllerBase and public abstract class Use NavController as the public API, and NavControllerBase as the internal API. Refactored all app/nav/tabs unit tests and created centralized mocking functions. --- src/components/app/app.ts | 20 +- src/components/app/test/app.spec.ts | 97 +- src/components/input/input-base.ts | 5 +- src/components/nav/nav-controller-base.ts | 1303 +++++++ src/components/nav/nav-controller.ts | 1352 +------ src/components/nav/nav-interfaces.ts | 3 + src/components/nav/nav-pop.ts | 28 +- src/components/nav/nav-portal.ts | 6 +- src/components/nav/nav-push.ts | 90 +- src/components/nav/nav.ts | 9 +- src/components/nav/swipe-back.ts | 4 +- .../nav/test/nav-controller.spec.ts | 3331 ++++++++--------- src/components/tabs/tab.ts | 12 +- src/components/tabs/tabs.ts | 116 +- src/components/tabs/test/tabs.spec.ts | 142 +- src/util/mock-providers.ts | 171 + src/util/util.ts | 2 + 17 files changed, 3384 insertions(+), 3307 deletions(-) create mode 100644 src/components/nav/nav-controller-base.ts create mode 100644 src/util/mock-providers.ts diff --git a/src/components/app/app.ts b/src/components/app/app.ts index c84e3b5bd8..1b724719d8 100644 --- a/src/components/app/app.ts +++ b/src/components/app/app.ts @@ -4,6 +4,7 @@ import { Title } from '@angular/platform-browser'; import { ClickBlock } from '../../util/click-block'; import { Config } from '../../config/config'; import { NavController } from '../nav/nav-controller'; +import { isTabs, isNav } from '../nav/nav-controller-base'; import { NavOptions } from '../nav/nav-interfaces'; import { NavPortal } from '../nav/nav-portal'; import { Platform } from '../../platform/platform'; @@ -195,13 +196,7 @@ export class App { // 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) { + if (isTabs(nav)) { // 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 @@ -211,6 +206,12 @@ export class App { nav.select(prevTab); return Promise.resolve(); } + + } else if (isNav(nav) && 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(); } // try again using the parent nav (if there is one) @@ -244,10 +245,9 @@ export class App { console.debug('app, goBack exitApp'); this._platform.exitApp(); } - - } else { - return navPromise; } + + return navPromise; } return Promise.resolve(); diff --git a/src/components/app/test/app.spec.ts b/src/components/app/test/app.spec.ts index 64442d3c3a..4a429bf55f 100644 --- a/src/components/app/test/app.spec.ts +++ b/src/components/app/test/app.spec.ts @@ -1,16 +1,16 @@ -import {Component} from '@angular/core'; -import {App, Nav, Tabs, Tab, NavOptions, Config, ViewController, Platform} from '../../../../src'; +import { Component } from '@angular/core'; +import { App, Config, Nav, NavOptions, Platform, Tab, Tabs, ViewController } from '../../../../src'; +import { mockNavController, mockTab, mockTabs } from '../../../../src/util/mock-providers'; export function run() { - describe('App', () => { describe('navPop', () => { it('should select the previous tab', () => { - let nav = mockNav(); - let portal = mockNav(); + let nav = mockNavController(); + let portal = mockNavController(); app.setPortal(portal); app.setRootNav(nav); @@ -40,8 +40,8 @@ describe('App', () => { }); it('should pop from the active tab, when tabs is nested is the root nav', () => { - let nav = mockNav(); - let portal = mockNav(); + let nav = mockNavController(); + let portal = mockNavController(); app.setPortal(portal); app.setRootNav(nav); @@ -91,9 +91,9 @@ describe('App', () => { }); it('should pop the root nav when nested nav has less than 2 views', () => { - let rootNav = mockNav(); - let nestedNav = mockNav(); - let portal = mockNav(); + let rootNav = mockNavController(); + let nestedNav = mockNavController(); + let portal = mockNavController(); app.setPortal(portal); rootNav.registerChildNav(nestedNav); nestedNav.parent = rootNav; @@ -120,9 +120,9 @@ describe('App', () => { }); it('should pop a view from the nested nav that has more than 1 view', () => { - let rootNav = mockNav(); - let nestedNav = mockNav(); - let portal = mockNav(); + let rootNav = mockNavController(); + let nestedNav = mockNavController(); + let portal = mockNavController(); app.setPortal(portal); app.setRootNav(rootNav); rootNav.registerChildNav(nestedNav); @@ -149,8 +149,8 @@ describe('App', () => { }); it('should pop the overlay in the portal of the root nav', () => { - let nav = mockNav(); - let portal = mockNav(); + let nav = mockNavController(); + let portal = mockNavController(); app.setPortal(portal); app.setRootNav(nav); @@ -173,8 +173,8 @@ describe('App', () => { }); it('should pop the second view in the root nav', () => { - let nav = mockNav(); - let portal = mockNav(); + let nav = mockNavController(); + let portal = mockNavController(); app.setPortal(portal); app.setRootNav(nav); @@ -194,8 +194,8 @@ describe('App', () => { }); it('should exit app when only one view in the root nav', () => { - let nav = mockNav(); - let portal = mockNav(); + let nav = mockNavController(); + let portal = mockNavController(); app.setPortal(portal); app.setRootNav(nav); @@ -217,8 +217,8 @@ describe('App', () => { }); it('should not exit app when only one view in the root nav, but navExitApp config set', () => { - let nav = mockNav(); - let portal = mockNav(); + let nav = mockNavController(); + let portal = mockNavController(); app.setPortal(portal); app.setRootNav(nav); @@ -242,8 +242,8 @@ describe('App', () => { }); it('should not go back if app is not enabled', () => { - let nav = mockNav(); - let portal = mockNav(); + let nav = mockNavController(); + let portal = mockNavController(); app.setPortal(portal); app.setRootNav(nav); @@ -276,7 +276,7 @@ describe('App', () => { describe('getActiveNav', () => { it('should get active NavController when using tabs with nested nav', () => { - let nav = mockNav(); + let nav = mockNavController(); app.setRootNav(nav); let tabs = mockTabs(); @@ -285,9 +285,9 @@ describe('App', () => { nav.registerChildNav(tabs); tab2.setSelected(true); - let nav2 = mockNav(); - let nav3 = mockNav(); - let nav4 = mockNav(); + let nav2 = mockNavController(); + let nav3 = mockNavController(); + let nav4 = mockNavController(); tab1.registerChildNav(nav4); tab2.registerChildNav(nav2); tab2.registerChildNav(nav3); @@ -296,7 +296,7 @@ describe('App', () => { }); it('should get active NavController when using tabs, nested in a root nav', () => { - let nav = mockNav(); + let nav = mockNavController(); app.setRootNav(nav); let tabs = mockTabs(); @@ -331,9 +331,9 @@ describe('App', () => { }); it('should get active NavController when nested 3 deep', () => { - let nav1 = mockNav(); - let nav2 = mockNav(); - let nav3 = mockNav(); + let nav1 = mockNavController(); + let nav2 = mockNavController(); + let nav3 = mockNavController(); app.setRootNav(nav1); nav1.registerChildNav(nav2); @@ -343,8 +343,8 @@ describe('App', () => { }); it('should get active NavController when nested 2 deep', () => { - let nav1 = mockNav(); - let nav2 = mockNav(); + let nav1 = mockNavController(); + let nav2 = mockNavController(); app.setRootNav(nav1); nav1.registerChildNav(nav2); @@ -352,13 +352,13 @@ describe('App', () => { }); it('should get active NavController when only one nav controller', () => { - let nav = mockNav(); + let nav = mockNavController(); app.setRootNav(nav); expect(app.getActiveNav()).toBe(nav); }); it('should set/get the root nav controller', () => { - let nav = mockNav(); + let nav = mockNavController(); app.setRootNav(nav); expect(app.getRootNav()).toBe(nav); }); @@ -443,40 +443,13 @@ describe('App', () => { var app: App; var config: Config; var platform: Platform; - var _cd: any; - - function mockNav(): Nav { - return new Nav(null, null, null, config, null, null, null, null, null, null); - } - - function mockTabs(): Tabs { - return new Tabs(null, null, null, config, null, null, null); - } - - function mockTab(parentTabs: Tabs): Tab { - var tab = new Tab(parentTabs, app, config, null, null, null, null, null, _cd, null); - 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(); app = new App(config, platform); - _cd = { - reattach: function(){}, - detach: function(){} - }; }); }); - } diff --git a/src/components/input/input-base.ts b/src/components/input/input-base.ts index f331d2bfa6..8a9c944e21 100644 --- a/src/components/input/input-base.ts +++ b/src/components/input/input-base.ts @@ -10,6 +10,7 @@ import { isTrueProperty } from '../../util/util'; import { Item } from '../item/item'; import { NativeInput, NextInput } from './native-input'; import { NavController } from '../nav/nav-controller'; +import { NavControllerBase } from '../nav/nav-controller-base'; import { Platform } from '../../platform/platform'; @@ -27,6 +28,7 @@ export class InputBase { protected _autoFocusAssist: string; protected _autoComplete: string; protected _autoCorrect: string; + protected _nav: NavControllerBase; inputControl: NgControl; @@ -44,9 +46,10 @@ export class InputBase { protected _platform: Platform, protected _elementRef: ElementRef, protected _scrollView: Content, - protected _nav: NavController, + nav: NavController, ngControl: NgControl ) { + this._nav = nav; this._useAssist = config.getBoolean('scrollAssist', false); this._usePadding = config.getBoolean('scrollPadding', this._useAssist); this._keyboardHeight = config.getNumber('keyboardHeight'); diff --git a/src/components/nav/nav-controller-base.ts b/src/components/nav/nav-controller-base.ts new file mode 100644 index 0000000000..5f68c8dafc --- /dev/null +++ b/src/components/nav/nav-controller-base.ts @@ -0,0 +1,1303 @@ +import { ComponentResolver, ElementRef, EventEmitter, NgZone, provide, ReflectiveInjector, Renderer, ViewContainerRef } from '@angular/core'; + +import { addSelector } from '../../config/bootstrap'; +import { App } from '../app/app'; +import { Config } from '../../config/config'; +import { GestureController } from '../../gestures/gesture-controller'; +import { Ion } from '../ion'; +import { isBlank, isPresent, pascalCaseToDashCase } from '../../util/util'; +import { Keyboard } from '../../util/keyboard'; +import { NavController } from './nav-controller'; +import { NavOptions, DIRECTION_BACK, DIRECTION_FORWARD } from './nav-interfaces'; +import { NavParams } from './nav-params'; +import { SwipeBackGesture } from './swipe-back'; +import { Transition } from '../../transitions/transition'; +import { ViewController } from './view-controller'; + + +/** + * This class is for internal use only. It is not exported publicly. + */ +export class NavControllerBase extends Ion implements NavController { + _transIds = 0; + _init = false; + _isPortal: boolean; + _trans: Transition; + _sbGesture: SwipeBackGesture; + _sbThreshold: number; + _viewport: ViewContainerRef; + _children: any[] = []; + _sbEnabled: boolean; + _ids: number = -1; + _trnsDelay: any; + _views: ViewController[] = []; + + viewDidLoad: EventEmitter; + viewWillEnter: EventEmitter; + viewDidEnter: EventEmitter; + viewWillLeave: EventEmitter; + viewDidLeave: EventEmitter; + viewWillUnload: EventEmitter; + viewDidUnload: EventEmitter; + + id: string; + parent: any; + config: Config; + trnsTime: number = 0; + + constructor( + parent: any, + public _app: App, + config: Config, + public _keyboard: Keyboard, + elementRef: ElementRef, + public _zone: NgZone, + public _renderer: Renderer, + public _compiler: ComponentResolver, + public _gestureCtrl: GestureController + ) { + super(elementRef); + + this.parent = parent; + this.config = config; + + this._trnsDelay = config.get('pageTransitionDelay'); + + this._sbEnabled = config.getBoolean('swipeBackEnabled'); + this._sbThreshold = config.getNumber('swipeBackThreshold', 40); + + this.id = 'n' + (++ctrlIds); + + this.viewDidLoad = new EventEmitter(); + this.viewWillEnter = new EventEmitter(); + this.viewDidEnter = new EventEmitter(); + this.viewWillLeave = new EventEmitter(); + this.viewDidLeave = new EventEmitter(); + this.viewWillUnload = new EventEmitter(); + this.viewDidUnload = new EventEmitter(); + } + + setViewport(val: ViewContainerRef) { + this._viewport = val; + } + + setRoot(page: any, params?: any, opts?: NavOptions): Promise { + return this.setPages([{page, params}], opts); + } + + setPages(pages: Array<{page: any, params?: any}>, opts?: NavOptions): Promise { + if (!pages || !pages.length) { + return Promise.resolve(false); + } + + if (isBlank(opts)) { + opts = {}; + } + + // remove existing views + let leavingView = this._remove(0, this._views.length); + + // create view controllers out of the pages and insert the new views + let views = pages.map(p => new ViewController(p.page, p.params)); + let enteringView = this._insert(0, views); + + // if animation wasn't set to true then default it to NOT animate + if (opts.animate !== true) { + opts.animate = false; + } + + // set the nav direction to "back" if it wasn't set + opts.direction = opts.direction || DIRECTION_BACK; + + let resolve: any; + let promise = new Promise(res => { resolve = res; }); + + // start the transition, fire resolve when done... + this._transition(enteringView, leavingView, opts, (hasCompleted: boolean) => { + // transition has completed!! + resolve(hasCompleted); + }); + + return promise; + } + + push(page: any, params?: any, opts?: NavOptions, done?: Function): Promise { + return this.insertPages(-1, [{page: page, params: params}], opts, done); + } + + /** + * DEPRECATED: Please use inject the overlays controller and use the present method on the instance instead. + */ + private present(enteringView: ViewController, opts?: NavOptions): Promise { + // deprecated warning: added beta.11 2016-06-27 + console.warn('nav.present() has been deprecated.\n' + + 'Please inject the overlay\'s controller and use the present method on the instance instead.'); + return Promise.resolve(); + } + + insert(insertIndex: number, page: any, params?: any, opts?: NavOptions, done?: Function): Promise { + return this.insertPages(insertIndex, [{page: page, params: params}], opts, done); + } + + insertPages(insertIndex: number, insertPages: Array<{page: any, params?: any}>, opts?: NavOptions, done?: Function): Promise { + let views = insertPages.map(p => new ViewController(p.page, p.params)); + return this.insertViews(insertIndex, views, opts, done); + } + + insertViews(insertIndex: number, insertViews: ViewController[], opts: NavOptions = {}, done?: Function) { + let promise: Promise; + if (!done) { + // only create a promise if a done callback wasn't provided + promise = new Promise(res => { done = res; }); + } + + if (!insertViews || !insertViews.length) { + done(false); + return promise; + } + + if (isBlank(opts)) { + opts = {}; + } + + // insert the new page into the stack + // returns the newly created entering view + let enteringView = this._insert(insertIndex, insertViews); + + // manually set the new view's id if an id was passed in the options + if (isPresent(opts.id)) { + enteringView.id = opts.id; + } + + // set the nav direction to "forward" if it wasn't set + opts.direction = opts.direction || 'forward'; + + // set which animation it should use if it wasn't set yet + if (!opts.animation) { + opts.animation = enteringView.getTransitionName(opts.direction); + } + + // it's possible that the newly added view doesn't need to + // transition in, but was simply inserted somewhere in the stack + // go backwards through the stack and find the first active view + // which could be active or one ready to enter + for (var i = this._views.length - 1; i >= 0; i--) { + if (this._views[i].state === STATE_ACTIVE || this._views[i].state === STATE_INIT_ENTER) { + // found the view at the end of the stack that's either + // already active or it is about to enter + + if (this._views[i] === enteringView) { + // cool, so the last valid view is also our entering view!! + // this means we should animate that bad boy in so it's the active view + // return a promise and resolve when the transition has completed + + // get the leaving view which the _insert() already set + let leavingView = this.getByState(STATE_INIT_LEAVE); + + // start the transition, fire resolve when done... + this._transition(enteringView, leavingView, opts, done); + return promise; + } + break; + } + } + + // the page was not pushed onto the end of the stack + // but rather inserted somewhere in the middle or beginning + // Since there are views after this new one, don't transition in + // auto resolve cuz there was is no need for an animation + done(enteringView); + + return promise; + } + + _insert(insertIndex: number, insertViews: ViewController[]): ViewController { + // when this is done, there should only be at most + // 1 STATE_INIT_ENTER and 1 STATE_INIT_LEAVE + // there should not be any that are STATE_ACTIVE after this is done + + // allow -1 to be passed in to auto push it on the end + // and clean up the index if it's larger then the size of the stack + if (insertIndex < 0 || insertIndex > this._views.length) { + insertIndex = this._views.length; + } + + // first see if there's an active view + let view = this.getActive(); + if (view) { + // there's an active view, set that it's initialized to leave + view.state = STATE_INIT_LEAVE; + + } else if (view = this.getByState(STATE_INIT_ENTER)) { + // oh no, there's already a transition initalized ready to enter! + // but it actually hasn't entered yet at all so lets + // just keep it in the array, but not render or animate it in + view.state = STATE_INACTIVE; + } + + // insert each of the views in the pages array + let insertView: ViewController = null; + + insertViews.forEach((view, i) => { + insertView = view; + + // create the new entering view + view.setNav(this); + view.state = STATE_INACTIVE; + + // give this inserted view an ID + view.id = this.id + '-' + (++this._ids); + + // insert the entering view into the correct index in the stack + this._views.splice(insertIndex + i, 0, view); + }); + + if (insertView) { + insertView.state = STATE_INIT_ENTER; + } + + return insertView; + } + + pop(opts?: NavOptions, done?: Function): Promise { + // get the index of the active view + // which will become the view to be leaving + let activeView = this.getByState(STATE_TRANS_ENTER) || + this.getByState(STATE_INIT_ENTER) || + this.getActive(); + + return this.remove(this.indexOf(activeView), 1, opts, done); + } + + popToRoot(opts?: NavOptions, done?: Function): Promise { + return this.popTo(this.first(), opts, done); + } + + popTo(view: ViewController, opts?: NavOptions, done?: Function): Promise { + let startIndex = this.indexOf(view); + if (startIndex < 0) { + return Promise.reject('View not found to pop to'); + } + + let activeView = this.getByState(STATE_TRANS_ENTER) || + this.getByState(STATE_INIT_ENTER) || + this.getActive(); + let removeCount = this.indexOf(activeView) - startIndex; + + return this.remove(startIndex + 1, removeCount, opts, done); + } + + remove(startIndex: number = -1, removeCount: number = 1, opts?: NavOptions, done?: Function): Promise { + let promise: Promise; + + if (!done) { + promise = new Promise(resolve => { done = resolve; }); + } + + if (startIndex === -1) { + startIndex = (this._views.length - 1); + + } else if (startIndex < 0 || startIndex >= this._views.length) { + console.error('index out of range removing view from nav'); + done(false); + return promise; + } + + if (isBlank(opts)) { + opts = {}; + } + + // if not set, by default climb up the nav controllers if + // there isn't a previous view in this nav controller + if (isBlank(opts.climbNav)) { + opts.climbNav = true; + } + + // default the direction to "back" + opts.direction = opts.direction || DIRECTION_BACK; + + // figure out the states of each view in the stack + let leavingView = this._remove(startIndex, removeCount); + + if (!leavingView) { + let forcedActive = this.getByState(STATE_FORCE_ACTIVE); + if (forcedActive) { + // this scenario happens when a remove is going on + // during a transition + if (this._trans) { + this._trans.stop(); + this._trans.destroy(); + this._trans = null; + this._cleanup(); + } + + done(false); + return promise; + } + } + + if (leavingView) { + // there is a view ready to leave, meaning that a transition needs + // to happen and the previously active view is going to animate out + + // get the view thats ready to enter + let enteringView = this.getByState(STATE_INIT_ENTER); + + if (!enteringView && !this._isPortal) { + // oh nos! no entering view to go to! + // if there is no previous view that would enter in this nav stack + // and the option is set to climb up the nav parent looking + // for the next nav we could transition to instead + if (opts.climbNav) { + let parentNav: NavController = this.parent; + while (parentNav) { + if (!isTabs(parentNav)) { + // Tabs can be a parent, but it is not a collection of views + // only we're looking for an actual NavController w/ stack of views + leavingView.fireWillLeave(); + this.viewWillLeave.emit(leavingView); + this._app.viewWillLeave.emit(leavingView); + + return parentNav.pop(opts).then((rtnVal: boolean) => { + leavingView.fireDidLeave(); + this.viewDidLeave.emit(leavingView); + this._app.viewDidLeave.emit(leavingView); + return rtnVal; + }); + } + parentNav = parentNav.parent; + } + } + + // there's no previous view and there's no valid parent nav + // to climb to so this shouldn't actually remove the leaving + // view because there's nothing that would enter, eww + leavingView.state = STATE_ACTIVE; + done(false); + + return promise; + } + + if (!opts.animation) { + opts.animation = leavingView.getTransitionName(opts.direction); + } + + // start the transition, fire resolve when done... + this._transition(enteringView, leavingView, opts, done); + + return promise; + } + + // no need to transition when the active view isn't being removed + // there's still an active view after _remove() figured out states + // so this means views that were only removed before the active + // view, so auto-resolve since no transition needs to happen + done(false); + return promise; + } + + /** + * @private + */ + _remove(startIndex: number, removeCount: number): ViewController { + // when this is done, there should only be at most + // 1 STATE_INIT_ENTER and 1 STATE_INIT_LEAVE + // there should not be any that are STATE_ACTIVE after this is done + let view: ViewController = null; + + // loop through each view that is set to be removed + for (var i = startIndex, ii = removeCount + startIndex; i < ii; i++) { + view = this.getByIndex(i); + if (!view) break; + + if (view.state === STATE_TRANS_ENTER || view.state === STATE_TRANS_LEAVE) { + // oh no!!! this view should be removed, but it's + // actively transitioning in at the moment!! + // since it's viewable right now, let's just set that + // it should be removed after the transition + view.state = STATE_REMOVE_AFTER_TRANS; + + } else if (view.state === STATE_INIT_ENTER) { + // asked to be removed before it even entered! + view.state = STATE_CANCEL_ENTER; + + } else { + // if this view is already leaving then no need to immediately + // remove it, otherwise set the remove state + // this is useful if the view being removed isn't going to + // animate out, but just removed from the stack, no transition + view.state = STATE_REMOVE; + } + } + + if (view = this.getByState(STATE_INIT_LEAVE)) { + // looks like there's already an active leaving view + + // reassign previous entering view to just be inactive + let enteringView = this.getByState(STATE_INIT_ENTER); + if (enteringView) { + enteringView.state = STATE_INACTIVE; + } + + // from the index of the leaving view, go backwards and + // find the first view that is inactive + for (var i = this.indexOf(view) - 1; i >= 0; i--) { + if (this._views[i].state === STATE_INACTIVE) { + this._views[i].state = STATE_INIT_ENTER; + break; + } + } + + } else if (view = this.getByState(STATE_TRANS_LEAVE)) { + // an active transition is happening, but a new transition + // still needs to happen force this view to be the active one + view.state = STATE_FORCE_ACTIVE; + + } else if (view = this.getByState(STATE_REMOVE)) { + // there is no active transition about to happen + // find the first view that is supposed to be removed and + // set that it is the init leaving view + // the first view to be removed, it should init leave + view.state = STATE_INIT_LEAVE; + view.fireWillUnload(); + this.viewWillUnload.emit(view); + this._app.viewWillUnload.emit(view); + + // from the index of the leaving view, go backwards and + // find the first view that is inactive so it can be the entering + for (var i = this.indexOf(view) - 1; i >= 0; i--) { + if (this._views[i].state === STATE_INACTIVE) { + this._views[i].state = STATE_INIT_ENTER; + break; + } + } + } + + // if there is still an active view, then it wasn't one that was + // set to be removed, so there actually won't be a transition at all + view = this.getActive(); + if (view) { + // the active view remains untouched, so all the removes + // must have happened before it, so really no need for transition + view = this.getByState(STATE_INIT_ENTER); + if (view) { + // if it was going to enter, then just make inactive + view.state = STATE_INACTIVE; + } + view = this.getByState(STATE_INIT_LEAVE); + if (view) { + // this was going to leave, so just remove it completely + view.state = STATE_REMOVE; + } + } + + // remove views that have been set to be removed, but not + // apart of any transitions that will eventually happen + this._views.filter(v => v.state === STATE_REMOVE).forEach(view => { + view.fireWillLeave(); + view.fireDidLeave(); + this._views.splice(this.indexOf(view), 1); + view.destroy(); + }); + + return this.getByState(STATE_INIT_LEAVE); + } + + /** + * @private + */ + _transition(enteringView: ViewController, leavingView: ViewController, opts: NavOptions, done: Function) { + let transId = ++this._transIds; + + if (enteringView === leavingView) { + // if the entering view and leaving view are the same thing don't continue + this._transFinish(transId, enteringView, leavingView, null, false, false); + done(false); + return; + } + + if (isBlank(opts)) { + opts = {}; + } + + this._setAnimate(opts); + + if (!leavingView) { + // if no leaving view then create a bogus one + leavingView = new ViewController(); + } + + if (!enteringView) { + // if no entering view then create a bogus one + enteringView = new ViewController(); + enteringView.fireLoaded(); + } + + /* Async steps to complete a transition + 1. _render: compile the view and render it in the DOM. Load page if it hasn't loaded already. When done call postRender + 2. _postRender: Run willEnter/willLeave, then wait a frame (change detection happens), then call beginTransition + 3. _beforeTrans: Create the transition's animation, play the animation, wait for it to end + 4. _afterTrans: Run didEnter/didLeave, call _transComplete() + 5. _transComplete: Cleanup, remove cache views, then call the final callback + */ + + // begin the multiple async process of transitioning to the entering view + this._render(transId, enteringView, leavingView, opts, (hasCompleted: boolean) => { + this._transFinish(transId, enteringView, leavingView, opts.direction, false, hasCompleted); + done(hasCompleted); + }); + } + + /** + * @private + */ + _setAnimate(opts: NavOptions) { + if ((this._views.length === 1 && !this._init && !this._isPortal) || this.config.get('animate') === false) { + opts.animate = false; + } + } + + /** + * @private + */ + _render(transId: number, enteringView: ViewController, leavingView: ViewController, opts: NavOptions, done: Function) { + // compile/load the view into the DOM + + if (enteringView.state === STATE_INACTIVE) { + // this entering view is already set to inactive, so this + // transition must be canceled, so don't continue + return done(); + } + + enteringView.state = STATE_INIT_ENTER; + leavingView.state = STATE_INIT_LEAVE; + + // remember if this nav is already transitioning or not + let isAlreadyTransitioning = this.isTransitioning(); + + if (enteringView.isLoaded()) { + // already compiled this view, do not load again and continue + this._postRender(transId, enteringView, leavingView, isAlreadyTransitioning, opts, done); + + } else { + // view has not been compiled/loaded yet + // continue once the view has finished compiling + // DOM WRITE + this.setTransitioning(true, 500); + + this.loadPage(enteringView, this._viewport, opts, () => { + enteringView.fireLoaded(); + this.viewDidLoad.emit(enteringView); + this._app.viewDidLoad.emit(enteringView); + + this._postRender(transId, enteringView, leavingView, isAlreadyTransitioning, opts, done); + }); + } + } + + /** + * @private + */ + _postRender(transId: number, enteringView: ViewController, leavingView: ViewController, isAlreadyTransitioning: boolean, opts: NavOptions, done: Function) { + // called after _render has completed and the view is compiled/loaded + + if (enteringView.state === STATE_INACTIVE) { + // this entering view is already set to inactive, so this + // transition must be canceled, so don't continue + return done(); + } + + if (!opts.preload) { + // the enteringView will become the active view, and is not being preloaded + + // set the correct zIndex for the entering and leaving views + // if there's already another trans_enter happening then + // the zIndex for the entering view should go off of that one + // DOM WRITE + let lastestLeavingView = this.getByState(STATE_TRANS_ENTER) || leavingView; + this._setZIndex(enteringView, lastestLeavingView, opts.direction); + + // make sure the entering and leaving views are showing + // DOM WRITE + if (isAlreadyTransitioning) { + // the previous transition was still going when this one started + // so to be safe, only update showing the entering/leaving + // don't hide the others when they could still be transitioning + enteringView.domShow(true, this._renderer); + leavingView.domShow(true, this._renderer); + + } else { + // there are no other transitions happening but this one + // only entering/leaving should show, all others hidden + // also if a view is an overlay or the previous view is an + // overlay then always show the overlay and the view before it + this._views.forEach(view => { + view.domShow(this._isPortal || (view === enteringView) || (view === leavingView), this._renderer); + }); + } + + // call each view's lifecycle events + if (leavingView.fireOtherLifecycles) { + // only fire entering lifecycle if the leaving + // view hasn't explicitly set not to + enteringView.fireWillEnter(); + this.viewWillEnter.emit(enteringView); + this._app.viewWillEnter.emit(enteringView); + } + + if (enteringView.fireOtherLifecycles) { + // only fire leaving lifecycle if the entering + // view hasn't explicitly set not to + leavingView.fireWillLeave(); + this.viewWillLeave.emit(leavingView); + this._app.viewWillLeave.emit(leavingView); + } + + } else { + // this view is being preloaded, don't call lifecycle events + // transition does not need to animate + opts.animate = false; + } + + this._beforeTrans(enteringView, leavingView, opts, done); + } + + /** + * @private + */ + _beforeTrans(enteringView: ViewController, leavingView: ViewController, opts: NavOptions, done: Function) { + // called after one raf from postRender() + // create the transitions animation, play the animation + // when the transition ends call wait for it to end + + if (enteringView.state === STATE_INACTIVE || enteringView.state === STATE_CANCEL_ENTER) { + // this entering view is already set to inactive or has been canceled + // so this transition must not begin, so don't continue + return done(); + } + + enteringView.state = STATE_TRANS_ENTER; + leavingView.state = STATE_TRANS_LEAVE; + + // everything during the transition should runOutsideAngular + this._zone.runOutsideAngular(() => { + + // init the transition animation + let transitionOpts = { + animation: opts.animation, + direction: opts.direction, + duration: opts.duration, + easing: opts.easing, + renderDelay: opts.transitionDelay || this._trnsDelay, + isRTL: this.config.platform.isRTL(), + ev: opts.ev, + }; + + let transAnimation = this._createTrans(enteringView, leavingView, transitionOpts); + + this._trans && this._trans.destroy(); + this._trans = transAnimation; + + if (opts.animate === false) { + // force it to not animate the elements, just apply the "to" styles + transAnimation.duration(0); + } + + // check if a parent is transitioning and get the time that it ends + let parentTransitionEndTime = this.getLongestTrans(Date.now()); + if (parentTransitionEndTime > 0) { + // the parent is already transitioning and has disabled the app + // so just update the local transitioning information + let duration = parentTransitionEndTime - Date.now(); + this.setTransitioning(true, duration); + + } else { + // this is the only active transition (for now), so disable the app + let keyboardDurationPadding = 0; + if (this._keyboard.isOpen()) { + // add XXms to the duration the app is disabled when the keyboard is open + keyboardDurationPadding = 600; + } + let duration = transAnimation.getDuration() + keyboardDurationPadding; + let enableApp = (duration < 64); + this._app.setEnabled(enableApp, duration); + this.setTransitioning(!enableApp, duration); + } + + // create a callback for when the animation is done + transAnimation.onFinish((trans: Transition) => { + // transition animation has ended + + // destroy the animation and it's element references + trans.destroy(); + + this._afterTrans(enteringView, leavingView, opts, trans.hasCompleted, done); + }); + + // cool, let's do this, start the transition + if (opts.progressAnimation) { + // this is a swipe to go back, just get the transition progress ready + // kick off the swipe animation start + transAnimation.progressStart(); + + } else { + + // this is a normal animation + // kick it off and let it play through + transAnimation.play(); + } + }); + } + + /** + * @private + */ + _afterTrans(enteringView: ViewController, leavingView: ViewController, opts: NavOptions, hasCompleted: boolean, done: Function) { + // transition has completed, update each view's state + // place back into the zone, run didEnter/didLeave + // call the final callback when done + + // run inside of the zone again + this._zone.run(() => { + + if (!opts.preload && hasCompleted) { + if (leavingView.fireOtherLifecycles) { + // only fire entering lifecycle if the leaving + // view hasn't explicitly set not to + enteringView.fireDidEnter(); + this.viewDidEnter.emit(enteringView); + this._app.viewDidEnter.emit(enteringView); + } + + if (enteringView.fireOtherLifecycles && this._init) { + // only fire leaving lifecycle if the entering + // view hasn't explicitly set not to + // and after the nav has initialized + leavingView.fireDidLeave(); + this.viewDidLeave.emit(leavingView); + this._app.viewDidLeave.emit(leavingView); + } + } + + if (enteringView.state === STATE_INACTIVE) { + // this entering view is already set to inactive, so this + // transition must be canceled, so don't continue + return done(hasCompleted); + } + + if (opts.keyboardClose !== false && this._keyboard.isOpen()) { + // the keyboard is still open! + // no problem, let's just close for them + this._keyboard.close(); + this._keyboard.onClose(() => { + + // keyboard has finished closing, transition complete + done(hasCompleted); + }, 32); + + } else { + // all good, transition complete + done(hasCompleted); + } + }); + } + + /** + * @private + */ + _transFinish(transId: number, enteringView: ViewController, leavingView: ViewController, direction: string, updateUrl: boolean, hasCompleted: boolean) { + // a transition has completed, but not sure if it's the last one or not + // check if this transition is the most recent one or not + + if (enteringView.state === STATE_CANCEL_ENTER) { + // this view was told to leave before it finished entering + this.remove(enteringView.index, 1); + } + + if (transId === this._transIds) { + // ok, good news, there were no other transitions that kicked + // off during the time this transition started and ended + + if (hasCompleted) { + // this transition has completed as normal + // so the entering one is now the active view + // and the leaving view is now just inactive + if (enteringView.state !== STATE_REMOVE_AFTER_TRANS) { + enteringView.state = STATE_ACTIVE; + } + if (leavingView.state !== STATE_REMOVE_AFTER_TRANS) { + leavingView.state = STATE_INACTIVE; + } + + // only need to do all this clean up if the transition + // completed, otherwise nothing actually changed + // destroy all of the views that come after the active view + this._cleanup(); + + // make sure only this entering view and PREVIOUS view are the + // only two views that are not display:none + // do not make any changes to the stack's current visibility + // if there is an overlay somewhere in the stack + leavingView = this.getPrevious(enteringView); + if (this._isPortal) { + // ensure the entering view is showing + enteringView.domShow(true, this._renderer); + + } else { + // only possibly hide a view if there are no overlays in the stack + this._views.forEach(view => { + view.domShow((view === enteringView) || (view === leavingView), this._renderer); + }); + } + + // this check only needs to happen once, which will add the css + // class to the nav when it's finished its first transition + this._init = true; + + } else { + // this transition has not completed, meaning the + // entering view did not end up as the active view + // this would happen when swipe to go back started + // but the user did not complete the swipe and the + // what was the active view stayed as the active view + leavingView.state = STATE_ACTIVE; + enteringView.state = STATE_INACTIVE; + } + + // check if there is a parent actively transitioning + let transitionEndTime = this.getLongestTrans(Date.now()); + // if transitionEndTime is greater than 0, there is a parent transition occurring + // so delegate enabling the app to the parent. If it <= 0, go ahead and enable the app + if (transitionEndTime <= 0) { + this._app && this._app.setEnabled(true); + } + + // update that this nav is not longer actively transitioning + this.setTransitioning(false); + + // see if we should add the swipe back gesture listeners or not + this._sbCheck(); + + } else { + // darn, so this wasn't the most recent transition + // so while this one did end, there's another more recent one + // still going on. Because a new transition is happening, + // then this entering view isn't actually going to be the active + // one, so only update the state to active/inactive if the state + // wasn't already updated somewhere else during its transition + if (enteringView.state === STATE_TRANS_ENTER) { + enteringView.state = STATE_INACTIVE; + } + if (leavingView.state === STATE_TRANS_LEAVE) { + leavingView.state = STATE_INACTIVE; + } + } + } + + /** + *@private + * This method is just a wrapper to the Transition function of same name + * to make it easy/possible to mock the method call by overriding the function. + * In testing we don't want to actually do the animation, we want to return a stub instead + */ + _createTrans(enteringView: ViewController, leavingView: ViewController, transitionOpts: any): Transition { + return Transition.createTransition(enteringView, leavingView, transitionOpts); + } + + _cleanup() { + // ok, cleanup time!! Destroy all of the views that are + // INACTIVE and come after the active view + let activeViewIndex = this.indexOf(this.getActive()); + let destroys = this._views.filter(v => v.state === STATE_REMOVE_AFTER_TRANS); + + for (var i = activeViewIndex + 1; i < this._views.length; i++) { + if (this._views[i].state === STATE_INACTIVE) { + destroys.push(this._views[i]); + } + } + + // all pages being destroyed should be removed from the list of + // pages and completely removed from the dom + destroys.forEach(view => { + this._views.splice(this.indexOf(view), 1); + view.destroy(); + this.viewDidUnload.emit(view); + this._app.viewDidUnload.emit(view); + }); + + // if any z-index goes under 0, then reset them all + let shouldResetZIndex = this._views.some(v => v.zIndex < 0); + if (shouldResetZIndex) { + this._views.forEach(view => { + view.setZIndex(view.zIndex + INIT_ZINDEX + 1, this._renderer); + }); + } + } + + getActiveChildNav(): any { + return this._children[this._children.length - 1]; + } + + /** + * @private + */ + registerChildNav(nav: any) { + this._children.push(nav); + } + + /** + * @private + */ + unregisterChildNav(nav: any) { + let index = this._children.indexOf(nav); + if (index > -1) { + this._children.splice(index, 1); + } + } + + /** + * @private + */ + ngOnDestroy() { + for (var i = this._views.length - 1; i >= 0; i--) { + this._views[i].destroy(); + } + this._views.length = 0; + + if (this.parent && this.parent.unregisterChildNav) { + this.parent.unregisterChildNav(this); + } + } + + /** + * @private + */ + loadPage(view: ViewController, viewport: ViewContainerRef, opts: NavOptions, done: Function) { + if (!viewport || !view.componentType) { + return; + } + + // TEMPORARY: automatically set selector w/ dah reflector + // TODO: use componentFactory.create once fixed + addSelector(view.componentType, 'ion-page'); + + this._compiler.resolveComponent(view.componentType).then(componentFactory => { + + if (view.state === STATE_CANCEL_ENTER) { + // view may have already been removed from the stack + // if so, don't even bother adding it + view.destroy(); + this._views.splice(view.index, 1); + return; + } + + // add more providers to just this page + let componentProviders = ReflectiveInjector.resolve([ + provide(NavController, {useValue: this}), + provide(ViewController, {useValue: view}), + provide(NavParams, {useValue: view.getNavParams()}) + ]); + + let childInjector = ReflectiveInjector.fromResolvedProviders(componentProviders, this._viewport.parentInjector); + + let componentRef = componentFactory.create(childInjector, null, null); + + viewport.insert(componentRef.hostView, viewport.length); + + // a new ComponentRef has been created + // set the ComponentRef's instance to its ViewController + view.setInstance(componentRef.instance); + + // the component has been loaded, so call the view controller's loaded method to load any dependencies into the dom + view.loaded(() => { + + // the ElementRef of the actual ion-page created + let pageElementRef = componentRef.location; + + // remember the ChangeDetectorRef for this ViewController + view.setChangeDetector(componentRef.changeDetectorRef); + + // remember the ElementRef to the ion-page elementRef that was just created + view.setPageRef(pageElementRef); + + // auto-add page css className created from component JS class name + let cssClassName = pascalCaseToDashCase(view.componentType.name); + this._renderer.setElementClass(pageElementRef.nativeElement, cssClassName, true); + + view.onDestroy(() => { + // ensure the element is cleaned up for when the view pool reuses this element + this._renderer.setElementAttribute(pageElementRef.nativeElement, 'class', null); + this._renderer.setElementAttribute(pageElementRef.nativeElement, 'style', null); + componentRef.destroy(); + }); + + // our job is done here + done(view); + }); + }); + } + + /** + * @private + */ + swipeBackStart() { + // default the direction to "back" + let opts: NavOptions = { + direction: DIRECTION_BACK, + progressAnimation: true + }; + + // figure out the states of each view in the stack + let leavingView = this._remove(this._views.length - 1, 1); + + if (leavingView) { + opts.animation = leavingView.getTransitionName(opts.direction); + + // get the view thats ready to enter + let enteringView = this.getByState(STATE_INIT_ENTER); + + // start the transition, fire callback when done... + this._transition(enteringView, leavingView, opts, (hasCompleted: boolean) => { + // swipe back has finished!! + console.debug('swipeBack, hasCompleted', hasCompleted); + }); + } + } + + /** + * @private + */ + swipeBackProgress(stepValue: number) { + if (this._trans && this._sbGesture) { + // continue to disable the app while actively dragging + this._app.setEnabled(false, 4000); + this.setTransitioning(true, 4000); + + // set the transition animation's progress + this._trans.progressStep(stepValue); + } + } + + /** + * @private + */ + swipeBackEnd(shouldComplete: boolean, currentStepValue: number) { + if (this._trans && this._sbGesture) { + // the swipe back gesture has ended + this._trans.progressEnd(shouldComplete, currentStepValue); + } + } + + /** + * @private + */ + _sbCheck() { + if (this._sbEnabled) { + // this nav controller can have swipe to go back + + if (!this._sbGesture) { + // create the swipe back gesture if we haven't already + let opts = { + edge: 'left', + threshold: this._sbThreshold + }; + this._sbGesture = new SwipeBackGesture(this.getNativeElement(), opts, this, this._gestureCtrl); + } + + if (this.canSwipeBack()) { + // it is be possible to swipe back + if (!this._sbGesture.isListening) { + this._zone.runOutsideAngular(() => { + // start listening if it's not already + console.debug('swipeBack gesture, listen'); + this._sbGesture.listen(); + }); + } + + } else if (this._sbGesture.isListening) { + // it should not be possible to swipe back + // but the gesture is still listening + console.debug('swipeBack gesture, unlisten'); + this._sbGesture.unlisten(); + } + } + } + + canSwipeBack(): boolean { + return (this._sbEnabled && !this.isTransitioning() && this._app.isEnabled() && this.canGoBack()); + } + + canGoBack(): boolean { + let activeView = this.getActive(); + if (activeView) { + return activeView.enableBack(); + } + return false; + } + + isTransitioning(includeAncestors?: boolean): boolean { + let now = Date.now(); + if (includeAncestors && this.getLongestTrans(now) > 0) { + return true; + } + return (this.trnsTime > now); + } + + setTransitioning(isTransitioning: boolean, fallback: number = 700) { + this.trnsTime = (isTransitioning ? Date.now() + fallback : 0); + } + + getLongestTrans(now: number) { + // traverses parents upwards and looks at the time the + // transition ends (if it's transitioning) and returns the + // value that is the furthest into the future thus giving us + // the longest transition duration + let parentNav = this.parent; + let transitionEndTime = -1; + while (parentNav) { + if (parentNav.trnsTime > transitionEndTime) { + transitionEndTime = parentNav.trnsTime; + } + parentNav = parentNav.parent; + } + + // only check if the transitionTime is greater than the current time once + return transitionEndTime > 0 && transitionEndTime > now ? transitionEndTime : 0; + } + + getByState(state: number): ViewController { + for (var i = this._views.length - 1; i >= 0; i--) { + if (this._views[i].state === state) { + return this._views[i]; + } + } + return null; + } + + getByIndex(index: number): ViewController { + return (index < this._views.length && index > -1 ? this._views[index] : null); + } + + getActive(): ViewController { + return this.getByState(STATE_ACTIVE); + } + + isActive(view: ViewController): boolean { + // returns if the given view is the active view or not + return !!(view && view.state === STATE_ACTIVE); + } + + getPrevious(view: ViewController): ViewController { + // returns the view controller which is before the given view controller. + return this.getByIndex(this.indexOf(view) - 1); + } + + first(): ViewController { + // returns the first view controller in this nav controller's stack. + return (this._views.length ? this._views[0] : null); + } + + last(): ViewController { + // returns the last page in this nav controller's stack. + return (this._views.length ? this._views[this._views.length - 1] : null); + } + + indexOf(view: ViewController): number { + // returns the index number of the given view controller. + return this._views.indexOf(view); + } + + length(): number { + return this._views.length; + } + + isSwipeBackEnabled(): boolean { + return this._sbEnabled; + } + + /** + * DEPRECATED: Please use app.getRootNav() instead + */ + private get rootNav(): NavController { + // deprecated 07-14-2016 beta.11 + console.warn('nav.rootNav() has been deprecated, please use app.getRootNav() instead'); + return this._app.getRootNav(); + } + + /** + * @private + * Dismiss all pages which have set the `dismissOnPageChange` property. + */ + dismissPageChangeViews() { + this._views.forEach(view => { + if (view.data && view.data.dismissOnPageChange) { + view.dismiss(); + } + }); + } + + /** + * @private + */ + _setZIndex(enteringView: ViewController, leavingView: ViewController, direction: string) { + if (enteringView) { + // get the leaving view, which could be in various states + if (!leavingView || !leavingView.isLoaded()) { + // the leavingView is a mocked view, either we're + // actively transitioning or it's the initial load + + var previousView = this.getPrevious(enteringView); + if (previousView && previousView.isLoaded()) { + // we found a better previous view to reference + // use this one instead + enteringView.setZIndex(previousView.zIndex + 1, this._renderer); + + } else { + // this is the initial view + enteringView.setZIndex(this._isPortal ? PORTAL_ZINDEX : INIT_ZINDEX, this._renderer); + } + + } else if (direction === DIRECTION_BACK) { + // moving back + enteringView.setZIndex(leavingView.zIndex - 1, this._renderer); + + } else { + // moving forward + enteringView.setZIndex(leavingView.zIndex + 1, this._renderer); + } + } + } + +} + +export const isTabs = (nav: any) => { + // Tabs (ion-tabs) + return !!nav.getSelected; +}; + +export const isTab = (nav: any) => { + // Tab (ion-tab) + return isPresent(nav._tabId); +}; + +export const isNav = function(nav: any) { + // Nav (ion-nav), Tab (ion-tab), Portal (ion-portal) + return isPresent(nav.push); +}; + + +export const STATE_ACTIVE = 1; +export const STATE_INACTIVE = 2; +export const STATE_INIT_ENTER = 3; +export const STATE_INIT_LEAVE = 4; +export const STATE_TRANS_ENTER = 5; +export const STATE_TRANS_LEAVE = 6; +export const STATE_REMOVE = 7; +export const STATE_REMOVE_AFTER_TRANS = 8; +export const STATE_CANCEL_ENTER = 9; +export const STATE_FORCE_ACTIVE = 10; + +const INIT_ZINDEX = 100; +const PORTAL_ZINDEX = 9999; + +let ctrlIds = -1; \ No newline at end of file diff --git a/src/components/nav/nav-controller.ts b/src/components/nav/nav-controller.ts index 9bba7c9482..604925995b 100644 --- a/src/components/nav/nav-controller.ts +++ b/src/components/nav/nav-controller.ts @@ -1,16 +1,11 @@ -import { ComponentResolver, ElementRef, EventEmitter, NgZone, provide, ReflectiveInjector, Renderer, ViewContainerRef } from '@angular/core'; +import { EventEmitter } from '@angular/core'; -import { addSelector } from '../../config/bootstrap'; -import { App } from '../app/app'; import { Config } from '../../config/config'; import { GestureController } from '../../gestures/gesture-controller'; import { Ion } from '../ion'; import { isBlank, pascalCaseToDashCase } from '../../util/util'; import { Keyboard } from '../../util/keyboard'; import { NavOptions } from './nav-interfaces'; -import { NavParams } from './nav-params'; -import { SwipeBackGesture } from './swipe-back'; -import { Transition } from '../../transitions/transition'; import { ViewController } from './view-controller'; @@ -157,19 +152,7 @@ import { ViewController } from './view-controller'; * * @see {@link /docs/v2/components#navigation Navigation Component Docs} */ -export class NavController extends Ion { - private _transIds = 0; - private _init = false; - private _trans: Transition; - private _sbGesture: SwipeBackGesture; - private _sbThreshold: number; - private _viewport: ViewContainerRef; - private _children: any[] = []; - - protected _sbEnabled: boolean; - protected _ids: number = -1; - protected _trnsDelay: any; - protected _views: ViewController[] = []; +export abstract class NavController { /** * Observable to be subscribed to when a component is loaded. @@ -219,7 +202,9 @@ export class NavController extends Ion { id: string; /** - * @private + * The parent navigation instance. If this is the root nav, then + * it'll be `null`. A `Tab` instance's parent is `Tabs`, otherwise + * the parent would be another nav, if it's not already the root nav. */ parent: any; @@ -228,55 +213,6 @@ export class NavController extends Ion { */ config: Config; - /** - * @private - */ - isPortal: boolean = false; - - /** - * @private - */ - trnsTime: number = 0; - - constructor( - parent: any, - protected _app: App, - config: Config, - protected _keyboard: Keyboard, - elementRef: ElementRef, - protected _zone: NgZone, - protected _renderer: Renderer, - protected _compiler: ComponentResolver, - private _gestureCtrl: GestureController - ) { - super(elementRef); - - this.parent = parent; - this.config = config; - - this._trnsDelay = config.get('pageTransitionDelay'); - - this._sbEnabled = config.getBoolean('swipeBackEnabled'); - this._sbThreshold = config.getNumber('swipeBackThreshold', 40); - - this.id = (++ctrlIds).toString(); - - this.viewDidLoad = new EventEmitter(); - this.viewWillEnter = new EventEmitter(); - this.viewDidEnter = new EventEmitter(); - this.viewWillLeave = new EventEmitter(); - this.viewDidLeave = new EventEmitter(); - this.viewWillUnload = new EventEmitter(); - this.viewDidUnload = new EventEmitter(); - } - - /** - * @private - */ - setViewport(val: ViewContainerRef) { - this._viewport = val; - } - /** * Set the root for the current navigation stack. * @param {Page} page The name of the component you want to push on the navigation stack. @@ -284,9 +220,7 @@ export class NavController extends Ion { * @param {object} [opts={}] Any options you want to use pass to transtion. * @returns {Promise} Returns a promise which is resolved when the transition has completed. */ - setRoot(page: any, params?: any, opts?: NavOptions): Promise { - return this.setPages([{page, params}], opts); - } + abstract setRoot(page: any, params?: any, opts?: NavOptions, done?: Function): Promise; /** * Set the views of the current navigation stack and navigate to the @@ -298,41 +232,7 @@ export class NavController extends Ion { * @param {object} [opts={}] Nav options to go with this transition. * @returns {Promise} Returns a promise which is resolved when the transition has completed. */ - setPages(pages: Array<{page: any, params?: any}>, opts?: NavOptions): Promise { - if (!pages || !pages.length) { - return Promise.resolve(false); - } - - if (isBlank(opts)) { - opts = {}; - } - - // remove existing views - let leavingView = this._remove(0, this._views.length); - - // create view controllers out of the pages and insert the new views - let views = pages.map(p => new ViewController(p.page, p.params)); - let enteringView = this._insert(0, views); - - // if animation wasn't set to true then default it to NOT animate - if (opts.animate !== true) { - opts.animate = false; - } - - // set the nav direction to "back" if it wasn't set - opts.direction = opts.direction || DIRECTION_BACK; - - let resolve: any; - let promise = new Promise(res => { resolve = res; }); - - // start the transition, fire resolve when done... - this._transition(enteringView, leavingView, opts, (hasCompleted: boolean) => { - // transition has completed!! - resolve(hasCompleted); - }); - - return promise; - } + abstract setPages(pages: Array<{page: any, params?: any}>, opts?: NavOptions, done?: Function): Promise; /** * Push a new component onto the current navication stack. Pass any aditional information @@ -343,20 +243,7 @@ export class NavController extends Ion { * @param {object} [opts={}] Nav options to go with this transition. * @returns {Promise} Returns a promise which is resolved when the transition has completed. */ - push(page: any, params?: any, opts?: NavOptions) { - return this.insertPages(-1, [{page: page, params: params}], opts); - } - - /** - * @private - * DEPRECATED: Please use inject the overlays controller and use the present method on the instance instead. - */ - private present(enteringView: ViewController, opts?: NavOptions): Promise { - // deprecated warning: added beta.11 2016-06-27 - console.warn('nav.present() has been deprecated.\n' + - 'Please inject the overlay\'s controller and use the present method on the instance instead.'); - return Promise.resolve(); - } + abstract push(page: any, params?: any, opts?: NavOptions, done?: Function): Promise; /** * Inserts a component into the nav stack at the specified index. This is useful if @@ -369,137 +256,19 @@ export class NavController extends Ion { * @param {object} [opts={}] Nav options to go with this transition. * @returns {Promise} Returns a promise which is resolved when the transition has completed. */ - insert(insertIndex: number, page: any, params?: any, opts?: NavOptions): Promise { - return this.insertPages(insertIndex, [{page: page, params: params}], opts); - } + abstract insert(insertIndex: number, page: any, params?: any, opts?: NavOptions, done?: Function): Promise; /** * Inserts an array of components into the nav stack at the specified index. - * The last component in the array will animate in and become the active component + * The last component in the array will become instantiated as a view, + * and animate in to become the active view. * * @param {number} insertIndex The index where you want to insert the page. * @param {array<{page: Page, params=: any}>} insertPages An array of objects, each with a `page` and optionally `params` property. * @param {object} [opts={}] Nav options to go with this transition. * @returns {Promise} Returns a promise which is resolved when the transition has completed. */ - insertPages(insertIndex: number, insertPages: Array<{page: any, params?: any}>, opts?: NavOptions): Promise { - let views = insertPages.map(p => new ViewController(p.page, p.params)); - return this.insertViews(insertIndex, views, opts); - } - - /** - * @private - */ - insertViews(insertIndex: number, insertViews: ViewController[], opts?: NavOptions): Promise { - if (!insertViews || !insertViews.length) { - return Promise.reject('invalid pages'); - } - - if (isBlank(opts)) { - opts = {}; - } - - // insert the new page into the stack - // returns the newly created entering view - let enteringView = this._insert(insertIndex, insertViews); - - // set the nav direction to "forward" if it wasn't set - opts.direction = opts.direction || 'forward'; - - // set which animation it should use if it wasn't set yet - if (!opts.animation) { - opts.animation = enteringView.getTransitionName(opts.direction); - } - - let resolve: any; - let promise = new Promise(res => { resolve = res; }); - - // it's possible that the newly added view doesn't need to - // transition in, but was simply inserted somewhere in the stack - // go backwards through the stack and find the first active view - // which could be active or one ready to enter - for (var i = this._views.length - 1; i >= 0; i--) { - if (this._views[i].state === STATE_ACTIVE || this._views[i].state === STATE_INIT_ENTER) { - // found the view at the end of the stack that's either - // already active or it is about to enter - - if (this._views[i] === enteringView) { - // cool, so the last valid view is also our entering view!! - // this means we should animate that bad boy in so it's the active view - // return a promise and resolve when the transition has completed - - // get the leaving view which the _insert() already set - let leavingView = this.getByState(STATE_INIT_LEAVE); - - // start the transition, fire resolve when done... - this._transition(enteringView, leavingView, opts, (hasCompleted: boolean) => { - // transition has completed!! - resolve(hasCompleted); - }); - - return promise; - } - break; - } - } - - // the page was not pushed onto the end of the stack - // but rather inserted somewhere in the middle or beginning - // Since there are views after this new one, don't transition in - // auto resolve cuz there was is no need for an animation - return Promise.resolve(enteringView); - } - - /** - * @private - */ - private _insert(insertIndex: number, insertViews: Array): ViewController { - // when this is done, there should only be at most - // 1 STATE_INIT_ENTER and 1 STATE_INIT_LEAVE - // there should not be any that are STATE_ACTIVE after this is done - - // allow -1 to be passed in to auto push it on the end - // and clean up the index if it's larger then the size of the stack - if (insertIndex < 0 || insertIndex > this._views.length) { - insertIndex = this._views.length; - } - - // first see if there's an active view - let view = this.getActive(); - if (view) { - // there's an active view, set that it's initialized to leave - view.state = STATE_INIT_LEAVE; - - } else if (view = this.getByState(STATE_INIT_ENTER)) { - // oh no, there's already a transition initalized ready to enter! - // but it actually hasn't entered yet at all so lets - // just keep it in the array, but not render or animate it in - view.state = STATE_INACTIVE; - } - - // insert each of the views in the pages array - let insertView: ViewController = null; - - insertViews.forEach((view, i) => { - insertView = view; - - // create the new entering view - view.setNav(this); - view.state = STATE_INACTIVE; - - // give this inserted view an ID - view.id = this.id + '-' + (++this._ids); - - // insert the entering view into the correct index in the stack - this._views.splice(insertIndex + i, 0, view); - }); - - if (insertView) { - insertView.state = STATE_INIT_ENTER; - } - - return insertView; - } + abstract insertPages(insertIndex: number, insertPages: Array<{page: any, params?: any}>, opts?: NavOptions, done?: Function): Promise; /** * Call to navigate back from a current component. Similar to `push()`, you @@ -508,24 +277,7 @@ export class NavController extends Ion { * @param {object} [opts={}] Nav options to go with this transition. * @returns {Promise} Returns a promise which is resolved when the transition has completed. */ - pop(opts?: NavOptions): Promise { - // get the index of the active view - // which will become the view to be leaving - let activeView = this.getByState(STATE_TRANS_ENTER) || - this.getByState(STATE_INIT_ENTER) || - this.getActive(); - - if (isBlank(opts)) { - opts = {}; - } - - // if not set, by default climb up the nav controllers if - // there isn't a previous view in this nav controller - if (isBlank(opts.climbNav)) { - opts.climbNav = true; - } - return this.remove(this.indexOf(activeView), 1, opts); - } + abstract pop(opts?: NavOptions, done?: Function): Promise; /** * Navigate back to the root of the stack, no matter how far back that is. @@ -533,9 +285,7 @@ export class NavController extends Ion { * @param {object} [opts={}] Nav options to go with this transition. * @returns {Promise} Returns a promise which is resolved when the transition has completed. */ - popToRoot(opts?: NavOptions): Promise { - return this.popTo(this.first(), opts); - } + abstract popToRoot(opts?: NavOptions, done?: Function): Promise; /** * Pop to a specific view in the history stack. @@ -544,19 +294,7 @@ export class NavController extends Ion { * @param {object} [opts={}] Nav options to go with this transition. * @returns {Promise} Returns a promise which is resolved when the transition has completed. */ - popTo(view: ViewController, opts?: NavOptions): Promise { - let startIndex = this.indexOf(view); - if (startIndex < 0) { - return Promise.reject('View not found to pop to'); - } - - let activeView = this.getByState(STATE_TRANS_ENTER) || - this.getByState(STATE_INIT_ENTER) || - this.getActive(); - let removeCount = this.indexOf(activeView) - startIndex; - - return this.remove(startIndex + 1, removeCount, opts); - } + abstract popTo(view: ViewController, opts?: NavOptions, done?: Function): Promise; /** * Removes a page from the nav stack at the specified index. @@ -566,840 +304,68 @@ export class NavController extends Ion { * @param {object} [opts={}] Any options you want to use pass to transtion. * @returns {Promise} Returns a promise which is resolved when the transition has completed. */ - remove(startIndex: number = -1, removeCount: number = 1, opts?: NavOptions): Promise { - if (startIndex === -1) { - startIndex = this._views.length - 1; - - } else if (startIndex < 0 || startIndex >= this._views.length) { - return Promise.reject('remove index out of range'); - } - - if (isBlank(opts)) { - opts = {}; - } - - // default the direction to "back" - opts.direction = opts.direction || DIRECTION_BACK; - - // figure out the states of each view in the stack - let leavingView = this._remove(startIndex, removeCount); - - if (!leavingView) { - let forcedActive = this.getByState(STATE_FORCE_ACTIVE); - if (forcedActive) { - // this scenario happens when a remove is going on - // during a transition - if (this._trans) { - this._trans.stop(); - this._trans.destroy(); - this._trans = null; - this._cleanup(); - } - - return Promise.resolve(false); - } - } - - if (leavingView) { - // there is a view ready to leave, meaning that a transition needs - // to happen and the previously active view is going to animate out - - // get the view thats ready to enter - let enteringView = this.getByState(STATE_INIT_ENTER); - - if (!enteringView && !this.isPortal) { - // oh nos! no entering view to go to! - // if there is no previous view that would enter in this nav stack - // and the option is set to climb up the nav parent looking - // for the next nav we could transition to instead - if (opts.climbNav) { - let parentNav: NavController = this.parent; - while (parentNav) { - if (!parentNav['_tabs']) { - // Tabs can be a parent, but it is not a collection of views - // only we're looking for an actual NavController w/ stack of views - leavingView.fireWillLeave(); - this.viewWillLeave.emit(leavingView); - this._app.viewWillLeave.emit(leavingView); - - return parentNav.pop(opts).then((rtnVal: boolean) => { - leavingView.fireDidLeave(); - this.viewDidLeave.emit(leavingView); - this._app.viewDidLeave.emit(leavingView); - return rtnVal; - }); - } - parentNav = parentNav.parent; - } - } - - // there's no previous view and there's no valid parent nav - // to climb to so this shouldn't actually remove the leaving - // view because there's nothing that would enter, eww - leavingView.state = STATE_ACTIVE; - return Promise.resolve(false); - } - - let resolve: any; - let promise = new Promise(res => { resolve = res; }); - - if (!opts.animation) { - opts.animation = leavingView.getTransitionName(opts.direction); - } - - // start the transition, fire resolve when done... - this._transition(enteringView, leavingView, opts, (hasCompleted: boolean) => { - // transition has completed!! - resolve(hasCompleted); - }); - - return promise; - } - - // no need to transition when the active view isn't being removed - // there's still an active view after _remove() figured out states - // so this means views that were only removed before the active - // view, so auto-resolve since no transition needs to happen - return Promise.resolve(false); - } + abstract remove(startIndex: number, removeCount?: number, opts?: NavOptions, done?: Function): Promise; /** - * @private + * @param {number} index The index of the page to get. + * @returns {ViewController} Returns the view controller that matches the given index. */ - private _remove(startIndex: number, removeCount: number): ViewController { - // when this is done, there should only be at most - // 1 STATE_INIT_ENTER and 1 STATE_INIT_LEAVE - // there should not be any that are STATE_ACTIVE after this is done - let view: ViewController = null; - - // loop through each view that is set to be removed - for (var i = startIndex, ii = removeCount + startIndex; i < ii; i++) { - view = this.getByIndex(i); - if (!view) break; - - if (view.state === STATE_TRANS_ENTER || view.state === STATE_TRANS_LEAVE) { - // oh no!!! this view should be removed, but it's - // actively transitioning in at the moment!! - // since it's viewable right now, let's just set that - // it should be removed after the transition - view.state = STATE_REMOVE_AFTER_TRANS; - - } else if (view.state === STATE_INIT_ENTER) { - // asked to be removed before it even entered! - view.state = STATE_CANCEL_ENTER; - - } else { - // if this view is already leaving then no need to immediately - // remove it, otherwise set the remove state - // this is useful if the view being removed isn't going to - // animate out, but just removed from the stack, no transition - view.state = STATE_REMOVE; - } - } - - if (view = this.getByState(STATE_INIT_LEAVE)) { - // looks like there's already an active leaving view - - // reassign previous entering view to just be inactive - let enteringView = this.getByState(STATE_INIT_ENTER); - if (enteringView) { - enteringView.state = STATE_INACTIVE; - } - - // from the index of the leaving view, go backwards and - // find the first view that is inactive - for (var i = this.indexOf(view) - 1; i >= 0; i--) { - if (this._views[i].state === STATE_INACTIVE) { - this._views[i].state = STATE_INIT_ENTER; - break; - } - } - - } else if (view = this.getByState(STATE_TRANS_LEAVE)) { - // an active transition is happening, but a new transition - // still needs to happen force this view to be the active one - view.state = STATE_FORCE_ACTIVE; - - } else if (view = this.getByState(STATE_REMOVE)) { - // there is no active transition about to happen - // find the first view that is supposed to be removed and - // set that it is the init leaving view - // the first view to be removed, it should init leave - view.state = STATE_INIT_LEAVE; - view.fireWillUnload(); - this.viewWillUnload.emit(view); - this._app.viewWillUnload.emit(view); - - // from the index of the leaving view, go backwards and - // find the first view that is inactive so it can be the entering - for (var i = this.indexOf(view) - 1; i >= 0; i--) { - if (this._views[i].state === STATE_INACTIVE) { - this._views[i].state = STATE_INIT_ENTER; - break; - } - } - } - - // if there is still an active view, then it wasn't one that was - // set to be removed, so there actually won't be a transition at all - view = this.getActive(); - if (view) { - // the active view remains untouched, so all the removes - // must have happened before it, so really no need for transition - view = this.getByState(STATE_INIT_ENTER); - if (view) { - // if it was going to enter, then just make inactive - view.state = STATE_INACTIVE; - } - view = this.getByState(STATE_INIT_LEAVE); - if (view) { - // this was going to leave, so just remove it completely - view.state = STATE_REMOVE; - } - } - - // remove views that have been set to be removed, but not - // apart of any transitions that will eventually happen - this._views.filter(v => v.state === STATE_REMOVE).forEach(view => { - view.fireWillLeave(); - view.fireDidLeave(); - this._views.splice(this.indexOf(view), 1); - view.destroy(); - }); - - return this.getByState(STATE_INIT_LEAVE); - } + abstract getByIndex(index: number): ViewController; /** - * @private + * @returns {ViewController} Returns the active page's view controller. */ - private _transition(enteringView: ViewController, leavingView: ViewController, opts: NavOptions, done: Function) { - let transId = ++this._transIds; - - if (enteringView === leavingView) { - // if the entering view and leaving view are the same thing don't continue - this._transFinish(transId, enteringView, leavingView, null, false); - return done(false); - } - - if (isBlank(opts)) { - opts = {}; - } - - this._setAnimate(opts); - - if (!leavingView) { - // if no leaving view then create a bogus one - leavingView = new ViewController(); - } - - if (!enteringView) { - // if no entering view then create a bogus one - enteringView = new ViewController(); - enteringView.fireLoaded(); - } - - /* Async steps to complete a transition - 1. _render: compile the view and render it in the DOM. Load page if it hasn't loaded already. When done call postRender - 2. _postRender: Run willEnter/willLeave, then wait a frame (change detection happens), then call beginTransition - 3. _beforeTrans: Create the transition's animation, play the animation, wait for it to end - 4. _afterTrans: Run didEnter/didLeave, call _transComplete() - 5. _transComplete: Cleanup, remove cache views, then call the final callback - */ - - // begin the multiple async process of transitioning to the entering view - this._render(transId, enteringView, leavingView, opts, (hasCompleted: boolean) => { - this._transFinish(transId, enteringView, leavingView, opts.direction, hasCompleted); - done(hasCompleted); - }); - } + abstract getActive(): ViewController; /** - * @private + * Returns if the given view is the active view or not. + * @param {ViewController} view + * @returns {boolean} */ - private _setAnimate(opts: NavOptions) { - if ((this._views.length === 1 && !this._init && !this.isPortal) || this.config.get('animate') === false) { - opts.animate = false; - } - } + abstract isActive(view: ViewController): boolean; /** - * @private + * Returns the view controller which is before the given view controller. + * @param {ViewController} view + * @returns {viewController} */ - private _render(transId: number, enteringView: ViewController, leavingView: ViewController, opts: NavOptions, done: Function) { - // compile/load the view into the DOM - - if (enteringView.state === STATE_INACTIVE) { - // this entering view is already set to inactive, so this - // transition must be canceled, so don't continue - return done(); - } - - enteringView.state = STATE_INIT_ENTER; - leavingView.state = STATE_INIT_LEAVE; - - // remember if this nav is already transitioning or not - let isAlreadyTransitioning = this.isTransitioning(); - - if (enteringView.isLoaded()) { - // already compiled this view, do not load again and continue - this._postRender(transId, enteringView, leavingView, isAlreadyTransitioning, opts, done); - - } else { - // view has not been compiled/loaded yet - // continue once the view has finished compiling - // DOM WRITE - this.setTransitioning(true, 500); - - this.loadPage(enteringView, this._viewport, opts, () => { - enteringView.fireLoaded(); - this.viewDidLoad.emit(enteringView); - this._app.viewDidLoad.emit(enteringView); - - this._postRender(transId, enteringView, leavingView, isAlreadyTransitioning, opts, done); - }); - } - } + abstract getPrevious(view: ViewController): ViewController; /** - * @private + * Returns the first view controller in this nav controller's stack. + * @returns {ViewController} */ - private _postRender(transId: number, enteringView: ViewController, leavingView: ViewController, isAlreadyTransitioning: boolean, opts: NavOptions, done: Function) { - // called after _render has completed and the view is compiled/loaded - - if (enteringView.state === STATE_INACTIVE) { - // this entering view is already set to inactive, so this - // transition must be canceled, so don't continue - return done(); - } - - if (!opts.preload) { - // the enteringView will become the active view, and is not being preloaded - - // set the correct zIndex for the entering and leaving views - // if there's already another trans_enter happening then - // the zIndex for the entering view should go off of that one - // DOM WRITE - let lastestLeavingView = this.getByState(STATE_TRANS_ENTER) || leavingView; - this._setZIndex(enteringView, lastestLeavingView, opts.direction); - - // make sure the entering and leaving views are showing - // DOM WRITE - if (isAlreadyTransitioning) { - // the previous transition was still going when this one started - // so to be safe, only update showing the entering/leaving - // don't hide the others when they could still be transitioning - enteringView.domShow(true, this._renderer); - leavingView.domShow(true, this._renderer); - - } else { - // there are no other transitions happening but this one - // only entering/leaving should show, all others hidden - // also if a view is an overlay or the previous view is an - // overlay then always show the overlay and the view before it - var view: ViewController; - var shouldShow: boolean; - - for (var i = 0, ii = this._views.length; i < ii; i++) { - view = this._views[i]; - shouldShow = (view === enteringView) || - (view === leavingView) || - view.isOverlay || - (i < ii - 1 ? this._views[i + 1].isOverlay : false); - view.domShow(shouldShow, this._renderer); - } - } - - // call each view's lifecycle events - if (leavingView.fireOtherLifecycles) { - // only fire entering lifecycle if the leaving - // view hasn't explicitly set not to - enteringView.fireWillEnter(); - this.viewWillEnter.emit(enteringView); - this._app.viewWillEnter.emit(enteringView); - } - - if (enteringView.fireOtherLifecycles) { - // only fire leaving lifecycle if the entering - // view hasn't explicitly set not to - leavingView.fireWillLeave(); - this.viewWillLeave.emit(leavingView); - this._app.viewWillLeave.emit(leavingView); - } - - } else { - // this view is being preloaded, don't call lifecycle events - // transition does not need to animate - opts.animate = false; - } - - this._beforeTrans(enteringView, leavingView, opts, done); - } + abstract first(): ViewController; /** - * @private + * Returns the last page in this nav controller's stack. + * @returns {ViewController} */ - private _beforeTrans(enteringView: ViewController, leavingView: ViewController, opts: NavOptions, done: Function) { - // called after one raf from postRender() - // create the transitions animation, play the animation - // when the transition ends call wait for it to end - - if (enteringView.state === STATE_INACTIVE || enteringView.state === STATE_CANCEL_ENTER) { - // this entering view is already set to inactive or has been canceled - // so this transition must not begin, so don't continue - return done(); - } - - enteringView.state = STATE_TRANS_ENTER; - leavingView.state = STATE_TRANS_LEAVE; - - // everything during the transition should runOutsideAngular - this._zone.runOutsideAngular(() => { - - // init the transition animation - let transitionOpts = { - animation: opts.animation, - direction: opts.direction, - duration: opts.duration, - easing: opts.easing, - renderDelay: opts.transitionDelay || this._trnsDelay, - isRTL: this.config.platform.isRTL(), - ev: opts.ev, - }; - - let transAnimation = this._createTrans(enteringView, leavingView, transitionOpts); - - this._trans && this._trans.destroy(); - this._trans = transAnimation; - - if (opts.animate === false) { - // force it to not animate the elements, just apply the "to" styles - transAnimation.duration(0); - } - - // check if a parent is transitioning and get the time that it ends - let parentTransitionEndTime = this._getLongestTrans(Date.now()); - if (parentTransitionEndTime > 0) { - // the parent is already transitioning and has disabled the app - // so just update the local transitioning information - let duration = parentTransitionEndTime - Date.now(); - this.setTransitioning(true, duration); - - } else { - // this is the only active transition (for now), so disable the app - let keyboardDurationPadding = 0; - if (this._keyboard.isOpen()) { - // add XXms to the duration the app is disabled when the keyboard is open - keyboardDurationPadding = 600; - } - let duration = transAnimation.getDuration() + keyboardDurationPadding; - let enableApp = (duration < 64); - this._app.setEnabled(enableApp, duration); - this.setTransitioning(!enableApp, duration); - } - - // create a callback for when the animation is done - transAnimation.onFinish((trans: Transition) => { - // transition animation has ended - - // destroy the animation and it's element references - trans.destroy(); - - this._afterTrans(enteringView, leavingView, opts, trans.hasCompleted, done); - }); - - // cool, let's do this, start the transition - if (opts.progressAnimation) { - // this is a swipe to go back, just get the transition progress ready - // kick off the swipe animation start - transAnimation.progressStart(); - - } else { - - // this is a normal animation - // kick it off and let it play through - transAnimation.play(); - } - }); - } + abstract last(): ViewController; /** - * @private + * Returns the index number of the given view controller. + * @param {ViewController} view + * @returns {number} */ - private _afterTrans(enteringView: ViewController, leavingView: ViewController, opts: NavOptions, hasCompleted: boolean, done: Function) { - // transition has completed, update each view's state - // place back into the zone, run didEnter/didLeave - // call the final callback when done - - // run inside of the zone again - this._zone.run(() => { - - if (!opts.preload && hasCompleted) { - if (leavingView.fireOtherLifecycles) { - // only fire entering lifecycle if the leaving - // view hasn't explicitly set not to - enteringView.fireDidEnter(); - this.viewDidEnter.emit(enteringView); - this._app.viewDidEnter.emit(enteringView); - } - - if (enteringView.fireOtherLifecycles && this._init) { - // only fire leaving lifecycle if the entering - // view hasn't explicitly set not to - // and after the nav has initialized - leavingView.fireDidLeave(); - this.viewDidLeave.emit(leavingView); - this._app.viewDidLeave.emit(leavingView); - } - } - - if (enteringView.state === STATE_INACTIVE) { - // this entering view is already set to inactive, so this - // transition must be canceled, so don't continue - return done(hasCompleted); - } - - if (opts.keyboardClose !== false && this._keyboard.isOpen()) { - // the keyboard is still open! - // no problem, let's just close for them - this._keyboard.close(); - this._keyboard.onClose(() => { - - // keyboard has finished closing, transition complete - done(hasCompleted); - }, 32); - - } else { - // all good, transition complete - done(hasCompleted); - } - }); - } + abstract indexOf(view: ViewController): number; /** - * @private + * Returns the number of views in this nav controller. + * @returns {number} The number of views in this stack, including the current view. */ - private _transFinish(transId: number, enteringView: ViewController, leavingView: ViewController, direction: string, hasCompleted: boolean) { - // a transition has completed, but not sure if it's the last one or not - // check if this transition is the most recent one or not - - if (enteringView.state === STATE_CANCEL_ENTER) { - // this view was told to leave before it finished entering - this.remove(enteringView.index, 1); - } - - if (transId === this._transIds) { - // ok, good news, there were no other transitions that kicked - // off during the time this transition started and ended - - if (hasCompleted) { - // this transition has completed as normal - // so the entering one is now the active view - // and the leaving view is now just inactive - if (enteringView.state !== STATE_REMOVE_AFTER_TRANS) { - enteringView.state = STATE_ACTIVE; - } - if (leavingView.state !== STATE_REMOVE_AFTER_TRANS) { - leavingView.state = STATE_INACTIVE; - } - - // only need to do all this clean up if the transition - // completed, otherwise nothing actually changed - // destroy all of the views that come after the active view - this._cleanup(); - - // make sure only this entering view and PREVIOUS view are the - // only two views that are not display:none - // do not make any changes to the stack's current visibility - // if there is an overlay somewhere in the stack - leavingView = this.getPrevious(enteringView); - if (this.hasOverlay()) { - // ensure the entering view is showing - enteringView.domShow(true, this._renderer); - - } else { - // only possibly hide a view if there are no overlays in the stack - this._views.forEach(view => { - let shouldShow = (view === enteringView) || (view === leavingView); - view.domShow(shouldShow, this._renderer); - }); - } - - // this check only needs to happen once, which will add the css - // class to the nav when it's finished its first transition - this._init = true; - - } else { - // this transition has not completed, meaning the - // entering view did not end up as the active view - // this would happen when swipe to go back started - // but the user did not complete the swipe and the - // what was the active view stayed as the active view - leavingView.state = STATE_ACTIVE; - enteringView.state = STATE_INACTIVE; - } - - // check if there is a parent actively transitioning - let transitionEndTime = this._getLongestTrans(Date.now()); - // if transitionEndTime is greater than 0, there is a parent transition occurring - // so delegate enabling the app to the parent. If it <= 0, go ahead and enable the app - if (transitionEndTime <= 0) { - this._app && this._app.setEnabled(true); - } - - // update that this nav is not longer actively transitioning - this.setTransitioning(false); - - // see if we should add the swipe back gesture listeners or not - this._sbCheck(); - - } else { - // darn, so this wasn't the most recent transition - // so while this one did end, there's another more recent one - // still going on. Because a new transition is happening, - // then this entering view isn't actually going to be the active - // one, so only update the state to active/inactive if the state - // wasn't already updated somewhere else during its transition - if (enteringView.state === STATE_TRANS_ENTER) { - enteringView.state = STATE_INACTIVE; - } - if (leavingView.state === STATE_TRANS_LEAVE) { - leavingView.state = STATE_INACTIVE; - } - } - } + abstract length(): number; /** - *@private - * This method is just a wrapper to the Transition function of same name - * to make it easy/possible to mock the method call by overriding the function. - * In testing we don't want to actually do the animation, we want to return a stub instead + * Returns the active child navigation. */ - private _createTrans(enteringView: ViewController, leavingView: ViewController, transitionOpts: any) { - return Transition.createTransition(enteringView, leavingView, transitionOpts); - } - - private _cleanup() { - // ok, cleanup time!! Destroy all of the views that are - // INACTIVE and come after the active view - let activeViewIndex = this.indexOf(this.getActive()); - let destroys = this._views.filter(v => v.state === STATE_REMOVE_AFTER_TRANS); - - for (var i = activeViewIndex + 1; i < this._views.length; i++) { - if (this._views[i].state === STATE_INACTIVE) { - destroys.push(this._views[i]); - } - } - - // all pages being destroyed should be removed from the list of - // pages and completely removed from the dom - destroys.forEach(view => { - this._views.splice(this.indexOf(view), 1); - view.destroy(); - this.viewDidUnload.emit(view); - this._app.viewDidUnload.emit(view); - }); - - // if any z-index goes under 0, then reset them all - let shouldResetZIndex = this._views.some(v => v.zIndex < 0); - if (shouldResetZIndex) { - this._views.forEach(view => { - view.setZIndex(view.zIndex + INIT_ZINDEX + 1, this._renderer); - }); - } - } + abstract getActiveChildNav(): any; /** - * @private + * Returns if the nav controller is actively transitioning or not. + * @return {boolean} */ - getActiveChildNav(): any { - return this._children[this._children.length - 1]; - } - - /** - * @private - */ - registerChildNav(nav: any) { - this._children.push(nav); - } - - /** - * @private - */ - unregisterChildNav(nav: any) { - let index = this._children.indexOf(nav); - if (index > -1) { - this._children.splice(index, 1); - } - } - - /** - * @private - */ - ngOnDestroy() { - for (var i = this._views.length - 1; i >= 0; i--) { - this._views[i].destroy(); - } - this._views.length = 0; - - if (this.parent && this.parent.unregisterChildNav) { - this.parent.unregisterChildNav(this); - } - } - - /** - * @private - */ - loadPage(view: ViewController, viewport: ViewContainerRef, opts: NavOptions, done: Function) { - if (!viewport || !view.componentType) { - return; - } - - // TEMPORARY: automatically set selector w/ dah reflector - // TODO: use componentFactory.create once fixed - addSelector(view.componentType, 'ion-page'); - - this._compiler.resolveComponent(view.componentType).then(componentFactory => { - - if (view.state === STATE_CANCEL_ENTER) { - // view may have already been removed from the stack - // if so, don't even bother adding it - view.destroy(); - this._views.splice(view.index, 1); - return; - } - - // add more providers to just this page - let componentProviders = ReflectiveInjector.resolve([ - provide(NavController, {useValue: this}), - provide(ViewController, {useValue: view}), - provide(NavParams, {useValue: view.getNavParams()}) - ]); - - let childInjector = ReflectiveInjector.fromResolvedProviders(componentProviders, this._viewport.parentInjector); - - let componentRef = componentFactory.create(childInjector, null, null); - - viewport.insert(componentRef.hostView, viewport.length); - - // a new ComponentRef has been created - // set the ComponentRef's instance to its ViewController - view.setInstance(componentRef.instance); - - // the component has been loaded, so call the view controller's loaded method to load any dependencies into the dom - view.loaded(() => { - - // the ElementRef of the actual ion-page created - let pageElementRef = componentRef.location; - - // remember the ChangeDetectorRef for this ViewController - view.setChangeDetector(componentRef.changeDetectorRef); - - // remember the ElementRef to the ion-page elementRef that was just created - view.setPageRef(pageElementRef); - - // auto-add page css className created from component JS class name - let cssClassName = pascalCaseToDashCase(view.componentType.name); - this._renderer.setElementClass(pageElementRef.nativeElement, cssClassName, true); - - view.onDestroy(() => { - // ensure the element is cleaned up for when the view pool reuses this element - this._renderer.setElementAttribute(pageElementRef.nativeElement, 'class', null); - this._renderer.setElementAttribute(pageElementRef.nativeElement, 'style', null); - componentRef.destroy(); - }); - - // our job is done here - done(view); - }); - }); - } - - /** - * @private - */ - swipeBackStart() { - // default the direction to "back" - let opts: NavOptions = { - direction: DIRECTION_BACK, - progressAnimation: true - }; - - // figure out the states of each view in the stack - let leavingView = this._remove(this._views.length - 1, 1); - - if (leavingView) { - opts.animation = leavingView.getTransitionName(opts.direction); - - // get the view thats ready to enter - let enteringView = this.getByState(STATE_INIT_ENTER); - - // start the transition, fire callback when done... - this._transition(enteringView, leavingView, opts, (hasCompleted: boolean) => { - // swipe back has finished!! - console.debug('swipeBack, hasCompleted', hasCompleted); - }); - } - } - - /** - * @private - */ - swipeBackProgress(stepValue: number) { - if (this._trans && this._sbGesture) { - // continue to disable the app while actively dragging - this._app.setEnabled(false, 4000); - this.setTransitioning(true, 4000); - - // set the transition animation's progress - this._trans.progressStep(stepValue); - } - } - - /** - * @private - */ - swipeBackEnd(shouldComplete: boolean, currentStepValue: number) { - if (this._trans && this._sbGesture) { - // the swipe back gesture has ended - this._trans.progressEnd(shouldComplete, currentStepValue); - } - } - - /** - * @private - */ - private _sbCheck() { - if (this._sbEnabled) { - // this nav controller can have swipe to go back - - if (!this._sbGesture) { - // create the swipe back gesture if we haven't already - let opts = { - edge: 'left', - threshold: this._sbThreshold - }; - this._sbGesture = new SwipeBackGesture(this.getNativeElement(), opts, this, this._gestureCtrl); - } - - if (this.canSwipeBack()) { - // it is be possible to swipe back - if (!this._sbGesture.isListening) { - this._zone.runOutsideAngular(() => { - // start listening if it's not already - console.debug('swipeBack gesture, listen'); - this._sbGesture.listen(); - }); - } - - } else if (this._sbGesture.isListening) { - // it should not be possible to swipe back - // but the gesture is still listening - console.debug('swipeBack gesture, unlisten'); - this._sbGesture.unlisten(); - } - } - } + abstract isTransitioning(includeAncestors?: boolean): boolean /** * If it's possible to use swipe back or not. If it's not possible @@ -1408,231 +374,13 @@ export class NavController extends Ion { * will return `true`. * @returns {boolean} */ - canSwipeBack(): boolean { - return (this._sbEnabled && !this.isTransitioning() && this._app.isEnabled() && this.canGoBack()); - } + abstract canSwipeBack(): boolean; /** * Returns `true` if there's a valid previous page that we can pop * back to. Otherwise returns `false`. * @returns {boolean} */ - canGoBack(): boolean { - let activeView = this.getActive(); - if (activeView) { - return activeView.enableBack(); - } - return false; - } + abstract canGoBack(): boolean; - /** - * Returns if the nav controller is actively transitioning or not. - * @return {boolean} - */ - isTransitioning(includeAncestors?: boolean): boolean { - let now = Date.now(); - if (includeAncestors && this._getLongestTrans(now) > 0) { - return true; - } - return (this.trnsTime > now); - } - - /** - * @private - */ - setTransitioning(isTransitioning: boolean, fallback: number = 700) { - this.trnsTime = (isTransitioning ? Date.now() + fallback : 0); - } - - /** - * @private - * This method traverses the tree of parents upwards - * and looks at the time the transition ends (if it's transitioning) - * and returns the value that is the furthest into the future - * thus giving us the longest transition duration - */ - private _getLongestTrans(now: number) { - let parentNav = this.parent; - let transitionEndTime = -1; - while (parentNav) { - if (parentNav.trnsTime > transitionEndTime) { - transitionEndTime = parentNav.trnsTime; - } - parentNav = parentNav.parent; - } - // only check if the transitionTime is greater than the current time once - return transitionEndTime > 0 && transitionEndTime > now ? transitionEndTime : 0; - } - - /** - * @private - */ - hasOverlay(): boolean { - for (var i = this._views.length - 1; i >= 0; i--) { - if (this._views[i].isOverlay) { - return true; - } - } - return false; - } - - /** - * @private - */ - getByState(state: number): ViewController { - for (var i = this._views.length - 1; i >= 0; i--) { - if (this._views[i].state === state) { - return this._views[i]; - } - } - return null; - } - - /** - * @param {number} index The index of the page to get. - * @returns {ViewController} Returns the view controller that matches the given index. - */ - getByIndex(index: number): ViewController { - return (index < this._views.length && index > -1 ? this._views[index] : null); - } - - /** - * @returns {ViewController} Returns the active page's view controller. - */ - getActive(): ViewController { - return this.getByState(STATE_ACTIVE); - } - - /** - * @param {ViewController} view - * @returns {boolean} - */ - isActive(view: ViewController): boolean { - return !!(view && view.state === STATE_ACTIVE); - } - - /** - * Returns the view controller which is before the given view controller. - * @param {ViewController} view - * @returns {viewController} - */ - getPrevious(view: ViewController): ViewController { - return this.getByIndex(this.indexOf(view) - 1); - } - - /** - * Returns the first view controller in this nav controller's stack. - * @returns {ViewController} - */ - first(): ViewController { - return (this._views.length ? this._views[0] : null); - } - - /** - * Returns the last page in this nav controller's stack. - * @returns {ViewController} - */ - last(): ViewController { - return (this._views.length ? this._views[this._views.length - 1] : null); - } - - /** - * Returns the index number of the given view controller. - * @param {ViewController} view - * @returns {number} - */ - indexOf(view: ViewController): number { - return this._views.indexOf(view); - } - - /** - * Returns the number of views in this nav controller. - * @returns {number} The number of views in this stack, including the current view. - */ - length(): number { - return this._views.length; - } - - /** - * @private - */ - isSwipeBackEnabled(): boolean { - return this._sbEnabled; - } - - /** - * Returns the root `NavController`. - * @returns {NavController} - */ - get rootNav(): NavController { - let nav = this; - while (nav.parent) { - nav = nav.parent; - } - return nav; - } - - /** - * @private - * Dismiss all pages which have set the `dismissOnPageChange` property. - */ - dismissPageChangeViews() { - this._views.forEach(view => { - if (view.data && view.data.dismissOnPageChange) { - view.dismiss(); - } - }); - } - - /** - * @private - */ - private _setZIndex(enteringView: ViewController, leavingView: ViewController, direction: string) { - if (enteringView) { - // get the leaving view, which could be in various states - if (!leavingView || !leavingView.isLoaded()) { - // the leavingView is a mocked view, either we're - // actively transitioning or it's the initial load - - var previousView = this.getPrevious(enteringView); - if (previousView && previousView.isLoaded()) { - // we found a better previous view to reference - // use this one instead - enteringView.setZIndex(previousView.zIndex + 1, this._renderer); - - } else { - // this is the initial view - enteringView.setZIndex(this.isPortal ? PORTAL_ZINDEX : INIT_ZINDEX, this._renderer); - } - - } else if (direction === DIRECTION_BACK) { - // moving back - enteringView.setZIndex(leavingView.zIndex - 1, this._renderer); - - } else { - // moving forward - enteringView.setZIndex(leavingView.zIndex + 1, this._renderer); - } - } - } - -} - -const STATE_ACTIVE = 1; -const STATE_INACTIVE = 2; -const STATE_INIT_ENTER = 3; -const STATE_INIT_LEAVE = 4; -const STATE_TRANS_ENTER = 5; -const STATE_TRANS_LEAVE = 6; -const STATE_REMOVE = 7; -const STATE_REMOVE_AFTER_TRANS = 8; -const STATE_CANCEL_ENTER = 9; -const STATE_FORCE_ACTIVE = 10; - -export const DIRECTION_BACK = 'back'; -export const DIRECTION_FORWARD = 'forward'; - -const INIT_ZINDEX = 100; -const PORTAL_ZINDEX = 9999; - -let ctrlIds = -1; \ No newline at end of file +} \ No newline at end of file diff --git a/src/components/nav/nav-interfaces.ts b/src/components/nav/nav-interfaces.ts index 6337470c28..55f2278053 100644 --- a/src/components/nav/nav-interfaces.ts +++ b/src/components/nav/nav-interfaces.ts @@ -13,3 +13,6 @@ export interface NavOptions { climbNav?: boolean; ev?: any; } + +export const DIRECTION_BACK = 'back'; +export const DIRECTION_FORWARD = 'forward'; diff --git a/src/components/nav/nav-pop.ts b/src/components/nav/nav-pop.ts index 6a3bbed73b..3ed4e37076 100644 --- a/src/components/nav/nav-pop.ts +++ b/src/components/nav/nav-pop.ts @@ -1,11 +1,12 @@ -import { Directive, Optional } from '@angular/core'; +import { Directive, HostListener, Input, Optional } from '@angular/core'; import { NavController } from './nav-controller'; - +import { noop } from '../../util/util'; /** * @name NavPop * @description - * Directive for declaratively pop the current page off from the navigation stack. + * Directive to declaratively pop the current page off from the + * navigation stack. * * @usage * ```html @@ -22,11 +23,7 @@ import { NavController } from './nav-controller'; * @see {@link ../NavPush NavPush API Docs} */ @Directive({ - selector: '[nav-pop]', - host: { - '(click)': 'onClick()', - 'role': 'link' - } + selector: '[navPop]' }) export class NavPop { @@ -36,10 +33,15 @@ export class NavPop { } } - /** - * @private - */ - onClick() { - this._nav && this._nav.pop(); + @HostListener('click') + onClick(): boolean { + // If no target, or if target is _self, prevent default browser behavior + if (this._nav) { + this._nav.pop(null, noop); + return false; + } + + return true; } + } diff --git a/src/components/nav/nav-portal.ts b/src/components/nav/nav-portal.ts index 54b6e55c69..38ee8a4b6b 100644 --- a/src/components/nav/nav-portal.ts +++ b/src/components/nav/nav-portal.ts @@ -4,7 +4,7 @@ import { App } from '../app/app'; import { Config } from '../../config/config'; import { GestureController } from '../../gestures/gesture-controller'; import { Keyboard } from '../../util/keyboard'; -import { NavController } from '../nav/nav-controller'; +import { NavControllerBase } from '../nav/nav-controller-base'; /** * @private @@ -12,7 +12,7 @@ import { NavController } from '../nav/nav-controller'; @Directive({ selector: '[nav-portal]' }) -export class NavPortal extends NavController { +export class NavPortal extends NavControllerBase { constructor( @Inject(forwardRef(() => App)) app: App, config: Config, @@ -25,7 +25,7 @@ export class NavPortal extends NavController { viewPort: ViewContainerRef ) { super(null, app, config, keyboard, elementRef, zone, renderer, compiler, gestureCtrl); - this.isPortal = true; + this._isPortal = true; this.setViewport(viewPort); app.setPortal(this); diff --git a/src/components/nav/nav-push.ts b/src/components/nav/nav-push.ts index 8437077aa2..c22414b9d0 100644 --- a/src/components/nav/nav-push.ts +++ b/src/components/nav/nav-push.ts @@ -1,28 +1,35 @@ -import { Directive, Input, Optional } from '@angular/core'; +import { Directive, HostListener, Input, Optional } from '@angular/core'; import { NavController } from './nav-controller'; +import { noop } from '../../util/util'; /** * @name NavPush * @description - * Directive for declaratively linking to a new page instead of using - * {@link ../NavController/#push NavController.push}. Similar to ui-router's `ui-sref`. + * Directive to declaratively push a new page to the current nav + * stack. * * @usage * ```html * * ``` - * To specify parameters you can use array syntax or the `nav-params` property: + * + * To specify parameters you can use array syntax or the `navParams` + * property: + * * ```html - * + * * ``` - * Where `pushPage` and `params` are specified in your component, and `pushPage` - * contains a reference to a [@Page component](../../../config/Page/): + * + * Where `pushPage` and `params` are specified in your component, + * and `pushPage` contains a reference to a + * [@Page component](../../../config/Page/): * * ```ts - * import {LoginPage} from 'login'; + * import { LoginPage } from './login'; + * * @Component({ - * template: `` + * template: `` * }) * class MyPage { * constructor(){ @@ -32,61 +39,42 @@ import { NavController } from './nav-controller'; * } * ``` * - * ### Alternate syntax - * You can also use syntax similar to Angular2's router, passing an array to - * NavPush: - * ```html - * - * ``` * @demo /docs/v2/demos/navigation/ * @see {@link /docs/v2/components#navigation Navigation Component Docs} * @see {@link ../NavPop NavPop API Docs} + * */ @Directive({ - selector: '[navPush]', - host: { - '(click)': 'onClick()', - 'role': 'link' - } + selector: '[navPush]' }) export class NavPush { /** - * @input {Page} the page you want to push - */ - @Input() navPush: any; - - /** - * @input {any} Any parameters you want to pass along - */ - @Input() navParams: any; - - constructor( - @Optional() private _nav: NavController - ) { - if (!_nav) { - console.error('nav-push must be within a NavController'); - } - } - - /** - * @private + * @input {Page} The Page to push onto the Nav. */ - onClick() { - let destination: any, params: any; + @Input() navPush: any[]|string; - if (this.navPush instanceof Array) { - if (this.navPush.length > 2) { - throw 'Too many [navPush] arguments, expects [View, { params }]'; - } - destination = this.navPush[0]; - params = this.navPush[1] || this.navParams; + /** + * @input {any} Parameters to pass to the page. + */ + @Input() navParams: {[k: string]: any}; - } else { - destination = this.navPush; - params = this.navParams; + + constructor(@Optional() private _nav: NavController) { + if (!_nav) { + console.error('navPush must be within a NavController'); + } + } + + @HostListener('click') + onClick(): boolean { + // If no target, or if target is _self, prevent default browser behavior + if (this._nav) { + this._nav.push(this.navPush, this.navParams, noop); + return false; } - this._nav && this._nav.push(destination, params); + return true; } + } diff --git a/src/components/nav/nav.ts b/src/components/nav/nav.ts index efe41d5d33..1b6ad476a4 100644 --- a/src/components/nav/nav.ts +++ b/src/components/nav/nav.ts @@ -5,7 +5,7 @@ import { Config } from '../../config/config'; import { Keyboard } from '../../util/keyboard'; import { GestureController } from '../../gestures/gesture-controller'; import { isTrueProperty } from '../../util/util'; -import { NavController } from './nav-controller'; +import { NavControllerBase } from './nav-controller-base'; import { ViewController } from './view-controller'; /** @@ -114,13 +114,13 @@ import { ViewController } from './view-controller'; `, encapsulation: ViewEncapsulation.None, }) -export class Nav extends NavController implements AfterViewInit { +export class Nav extends NavControllerBase implements AfterViewInit { private _root: any; private _hasInit: boolean = false; constructor( @Optional() viewCtrl: ViewController, - @Optional() parent: NavController, + @Optional() parent: NavControllerBase, app: App, config: Config, keyboard: Keyboard, @@ -164,9 +164,6 @@ export class Nav extends NavController implements AfterViewInit { this._hasInit = true; if (this._root) { - if (typeof this._root !== 'function') { - throw 'The [root] property in must be given a reference to a component class from within the constructor.'; - } this.push(this._root); } } diff --git a/src/components/nav/swipe-back.ts b/src/components/nav/swipe-back.ts index 32a7458292..ebd34cfda0 100644 --- a/src/components/nav/swipe-back.ts +++ b/src/components/nav/swipe-back.ts @@ -1,7 +1,7 @@ import { assign } from '../../util/util'; import { GestureController, GestureDelegate, GesturePriority } from '../../gestures/gesture-controller'; import { MenuController } from '../menu/menu-controller'; -import { NavController } from './nav-controller'; +import { NavControllerBase } from './nav-controller-base'; import { SlideData } from '../../gestures/slide-gesture'; import { SlideEdgeGesture } from '../../gestures/slide-edge-gesture'; @@ -11,7 +11,7 @@ export class SwipeBackGesture extends SlideEdgeGesture { constructor( element: HTMLElement, options: any, - private _nav: NavController, + private _nav: NavControllerBase, gestureCtlr: GestureController ) { super(element, assign({ diff --git a/src/components/nav/test/nav-controller.spec.ts b/src/components/nav/test/nav-controller.spec.ts index 213a1238e0..d609791eba 100644 --- a/src/components/nav/test/nav-controller.spec.ts +++ b/src/components/nav/test/nav-controller.spec.ts @@ -1,1739 +1,1616 @@ -import { NavController, Tabs, NavOptions, Config, ViewController, App, Platform } from '../../../../src'; +import { Renderer } from '@angular/core'; +import { App, Config, Form, Keyboard, MenuController, NavOptions, Platform, Tabs, ViewController } from '../../../../src'; +import { NavControllerBase } from '../../../../src/components/nav/nav-controller-base'; +import { STATE_ACTIVE, STATE_INACTIVE, STATE_INIT_ENTER, STATE_INIT_LEAVE, STATE_TRANS_ENTER, STATE_TRANS_LEAVE, STATE_REMOVE, STATE_REMOVE_AFTER_TRANS, STATE_CANCEL_ENTER, STATE_FORCE_ACTIVE } from '../../../../src/components/nav/nav-controller-base'; +import { mockNavController, mockElementRef, mockTransition } from '../../../../src/util/mock-providers'; export function run() { - describe('NavController', () => { - describe('pop', () => { +describe('NavController', () => { - it('should do nothing if its the first view in the stack', () => { - let view1 = new ViewController(Page1); - view1.state = STATE_ACTIVE; - nav.views = [view1]; + describe('pop', () => { - expect(nav.length()).toBe(1); + it('should do nothing if its the first view in the stack', () => { + let view1 = new ViewController(Page1); + view1.state = STATE_ACTIVE; + nav._views = [view1]; - nav.pop(); + expect(nav.length()).toBe(1); - expect(nav.length()).toBe(1); - expect(nav.getByIndex(0).state).toBe(STATE_ACTIVE); - expect(nav.getByIndex(0).componentType).toBe(Page1); - }); + nav.pop(); + expect(nav.length()).toBe(1); + expect(nav.getByIndex(0).state).toBe(STATE_ACTIVE); + expect(nav.getByIndex(0).componentType).toBe(Page1); }); - describe('popToRoot', () => { - - it('should go back to root', () => { - let view1 = new ViewController(Page1); - view1.state = STATE_INACTIVE; - let view2 = new ViewController(Page2); - view2.state = STATE_INACTIVE; - let view3 = new ViewController(Page3); - view3.state = STATE_INACTIVE; - let view4 = new ViewController(Page4); - view4.state = STATE_ACTIVE; - nav.views = [view1, view2, view3, view4]; - - nav.popToRoot(); - expect(nav.length()).toBe(2); - expect(nav.getByIndex(0).state).toBe(STATE_INIT_ENTER); - expect(nav.getByIndex(0).componentType).toBe(Page1); - expect(nav.getByIndex(1).state).toBe(STATE_INIT_LEAVE); - expect(nav.getByIndex(1).componentType).toBe(Page4); - - expect(view2.state).toBe(STATE_REMOVE); - expect(view3.state).toBe(STATE_REMOVE); - }); - - }); - - describe('popTo', () => { - - it('should go back two views', () => { - let view1 = new ViewController(Page1); - view1.state = STATE_INACTIVE; - let view2 = new ViewController(Page2); - view2.state = STATE_INACTIVE; - let view3 = new ViewController(Page3); - view3.state = STATE_INACTIVE; - let view4 = new ViewController(Page4); - view4.state = STATE_ACTIVE; - nav.views = [view1, view2, view3, view4]; - - nav.popTo(view2); - - expect(nav.length()).toBe(3); - expect(nav.getByIndex(0).state).toBe(STATE_INACTIVE); - expect(nav.getByIndex(0).componentType).toBe(Page1); - expect(nav.getByIndex(1).state).toBe(STATE_INIT_ENTER); - expect(nav.getByIndex(1).componentType).toBe(Page2); - expect(view3.state).toBe(STATE_REMOVE); - expect(nav.getByIndex(2).state).toBe(STATE_INIT_LEAVE); - expect(nav.getByIndex(2).componentType).toBe(Page4); - }); - - }); - - describe('remove', () => { - - it('should create opts if passed in arg is undefined or null', () => { - let view1 = new ViewController(Page1); - view1.state = STATE_INACTIVE; - let view2 = new ViewController(Page2); - view2.state = STATE_ACTIVE; - nav.views = [view1, view2]; - - nav.remove(1, 1, null); - }); - - }); - - describe('_remove', () => { - - it('should reassign activily transitioning leave that isnt getting removed, to become force active', () => { - let view1 = new ViewController(Page1); - view1.state = STATE_INACTIVE; - let view2 = new ViewController(Page2); - view2.state = STATE_TRANS_LEAVE; - let view3 = new ViewController(Page3); - view3.state = STATE_TRANS_ENTER; - nav.views = [view1, view2, view3]; - - nav._remove(2, 1); - - expect(nav.getByIndex(0).state).toBe(STATE_INACTIVE); - expect(nav.getByIndex(0).componentType).toBe(Page1); - expect(nav.getByIndex(1).state).toBe(STATE_FORCE_ACTIVE); - expect(nav.getByIndex(1).componentType).toBe(Page2); - expect(nav.getByIndex(2).state).toBe(STATE_REMOVE_AFTER_TRANS); - expect(nav.getByIndex(2).componentType).toBe(Page3); - }); - - it('should reassign activily transitioning views that should be removed to STATE_REMOVE_AFTER_TRANS', () => { - let view1 = new ViewController(Page1); - view1.state = STATE_INACTIVE; - let view2 = new ViewController(Page2); - view2.state = STATE_TRANS_ENTER; - let view3 = new ViewController(Page3); - view3.state = STATE_TRANS_LEAVE; - nav.views = [view1, view2, view3]; - - nav._remove(1, 2); - expect(nav.getByIndex(0).state).toBe(STATE_INACTIVE); - expect(nav.getByIndex(0).componentType).toBe(Page1); - expect(nav.getByIndex(1).state).toBe(STATE_REMOVE_AFTER_TRANS); - expect(nav.getByIndex(1).componentType).toBe(Page2); - expect(nav.getByIndex(2).state).toBe(STATE_REMOVE_AFTER_TRANS); - expect(nav.getByIndex(2).componentType).toBe(Page3); - }); - - it('should keep same init leave, but set previous init enter to inactive', () => { - let view1 = new ViewController(Page1); - view1.state = STATE_INACTIVE; - let view2 = new ViewController(Page2); - view2.state = STATE_INIT_ENTER; - let view3 = new ViewController(Page3); - view3.state = STATE_INIT_LEAVE; - nav.views = [view1, view2, view3]; - - nav._remove(1, 1); - expect(nav.length()).toBe(3); - expect(view1.state).toBe(STATE_INIT_ENTER); - expect(view2.state).toBe(STATE_CANCEL_ENTER); - expect(view3.state).toBe(STATE_INIT_LEAVE); - - expect(nav.getByIndex(0).state).toBe(STATE_INIT_ENTER); - expect(nav.getByIndex(0).componentType).toBe(Page1); - expect(nav.getByIndex(1).state).toBe(STATE_CANCEL_ENTER); - expect(nav.getByIndex(1).componentType).toBe(Page2); - expect(nav.getByIndex(2).state).toBe(STATE_INIT_LEAVE); - expect(nav.getByIndex(2).componentType).toBe(Page3); - }); - - it('should set to pop the active and enter the previous', () => { - let view1 = new ViewController(Page1); - view1.state = STATE_INACTIVE; - let view2 = new ViewController(Page2); - view2.state = STATE_ACTIVE; - nav.views = [view1, view2]; - - nav._remove(1, 1); - expect(view1.state).toBe(STATE_INIT_ENTER); - expect(view2.state).toBe(STATE_INIT_LEAVE); - }); - - it('should set to remove 2 views before active one, active stays the same', () => { - let view1 = new ViewController(Page1); - view1.state = STATE_INACTIVE; - let view2 = new ViewController(Page2); - view2.state = STATE_INACTIVE; - let view3 = new ViewController(Page3); - view3.state = STATE_INACTIVE; - let view4 = new ViewController(Page4); - view4.state = STATE_INACTIVE; - let view5 = new ViewController(Page5); - view5.state = STATE_ACTIVE; - nav.views = [view1, view2, view3, view4, view5]; - - nav._remove(2, 2); - expect(nav.length()).toBe(3); - expect(view1.state).toBe(STATE_INACTIVE); - expect(view2.state).toBe(STATE_INACTIVE); - expect(view3.state).toBe(STATE_REMOVE); - expect(view4.state).toBe(STATE_REMOVE); - expect(view5.state).toBe(STATE_ACTIVE); - - expect(nav.getByIndex(0).state).toBe(STATE_INACTIVE); - expect(nav.getByIndex(0).componentType).toBe(Page1); - expect(nav.getByIndex(1).state).toBe(STATE_INACTIVE); - expect(nav.getByIndex(1).componentType).toBe(Page2); - expect(nav.getByIndex(2).state).toBe(STATE_ACTIVE); - expect(nav.getByIndex(2).componentType).toBe(Page5); - }); - - it('should set to remove all views other than the first', () => { - let view1 = new ViewController(Page1); - view1.state = STATE_INACTIVE; - let view2 = new ViewController(Page2); - view2.state = STATE_INACTIVE; - let view3 = new ViewController(Page3); - view3.state = STATE_INACTIVE; - let view4 = new ViewController(Page4); - view4.state = STATE_ACTIVE; - nav.views = [view1, view2, view3, view4]; - - nav._remove(1, 9999); - expect(nav.length()).toBe(2); - expect(view1.state).toBe(STATE_INIT_ENTER); - expect(view2.state).toBe(STATE_REMOVE); - expect(view3.state).toBe(STATE_REMOVE); - expect(view4.state).toBe(STATE_INIT_LEAVE); - - expect(nav.getByIndex(0).state).toBe(STATE_INIT_ENTER); - expect(nav.getByIndex(0).componentType).toBe(Page1); - expect(nav.getByIndex(1).state).toBe(STATE_INIT_LEAVE); - expect(nav.getByIndex(1).componentType).toBe(Page4); - }); - - it('should set to remove 3 views and enter the first inactive one, remove includes active one', () => { - let view1 = new ViewController(Page1); - view1.state = STATE_INACTIVE; - let view2 = new ViewController(Page2); - view2.state = STATE_INACTIVE; - let view3 = new ViewController(Page3); - view3.state = STATE_INACTIVE; - let view4 = new ViewController(Page4); - view4.state = STATE_ACTIVE; - nav.views = [view1, view2, view3, view4]; - - nav._remove(1, 3); - expect(nav.length()).toBe(2); - expect(view1.state).toBe(STATE_INIT_ENTER); - expect(view2.state).toBe(STATE_REMOVE); - expect(view3.state).toBe(STATE_REMOVE); - expect(view4.state).toBe(STATE_INIT_LEAVE); - - expect(nav.getByIndex(0).state).toBe(STATE_INIT_ENTER); - expect(nav.getByIndex(0).componentType).toBe(Page1); - expect(nav.getByIndex(1).state).toBe(STATE_INIT_LEAVE); - expect(nav.getByIndex(1).componentType).toBe(Page4); - }); - - it('should set to remove the active and enter the previous', () => { - let view1 = new ViewController(Page1); - view1.state = STATE_INACTIVE; - let view2 = new ViewController(Page2); - view2.state = STATE_ACTIVE; - nav.views = [view1, view2]; - - nav._remove(1, 1); - expect(view1.state).toBe(STATE_INIT_ENTER); - expect(view2.state).toBe(STATE_INIT_LEAVE); - }); - - it('should set to remove the only view in the stack', () => { - let view1 = new ViewController(Page1); - view1.state = STATE_ACTIVE; - nav.views = [view1]; - - nav._remove(0, 1); - expect(nav.getByIndex(0).state).toBe(STATE_INIT_LEAVE); - }); - - it('should call willLeave/didLeave/destroy on views with STATE_REMOVE', () => { - let view1 = new ViewController(Page1); - view1.state = STATE_INACTIVE; - let view2 = new ViewController(Page2); - view2.state = STATE_INACTIVE; - let view3 = new ViewController(Page3); - view3.state = STATE_INACTIVE; - let view4 = new ViewController(Page4); - view4.state = STATE_ACTIVE; - nav.views = [view1, view2, view3, view4]; - - spyOn(view1, 'fireWillLeave'); - spyOn(view1, 'fireDidLeave'); - spyOn(view1, 'destroy'); - - spyOn(view2, 'fireWillLeave'); - spyOn(view2, 'fireDidLeave'); - spyOn(view2, 'destroy'); - - spyOn(view3, 'fireWillLeave'); - spyOn(view3, 'fireDidLeave'); - spyOn(view3, 'destroy'); - - spyOn(view4, 'fireWillLeave'); - spyOn(view4, 'fireDidLeave'); - spyOn(view4, 'destroy'); - - nav._remove(1, 3); - expect(nav.length()).toBe(2); - expect(view1.state).toBe(STATE_INIT_ENTER); - expect(view2.state).toBe(STATE_REMOVE); - expect(view3.state).toBe(STATE_REMOVE); - expect(view4.state).toBe(STATE_INIT_LEAVE); - - expect(view1.fireWillLeave).not.toHaveBeenCalled(); - expect(view1.fireDidLeave).not.toHaveBeenCalled(); - expect(view1.destroy).not.toHaveBeenCalled(); - - expect(view2.fireWillLeave).toHaveBeenCalled(); - expect(view2.fireDidLeave).toHaveBeenCalled(); - expect(view2.destroy).toHaveBeenCalled(); - - expect(view3.fireWillLeave).toHaveBeenCalled(); - expect(view3.fireDidLeave).toHaveBeenCalled(); - expect(view3.destroy).toHaveBeenCalled(); - - expect(view4.fireWillLeave).not.toHaveBeenCalled(); - expect(view4.fireDidLeave).not.toHaveBeenCalled(); - expect(view4.destroy).not.toHaveBeenCalled(); - }); - }); - - describe('_cleanup', () => { - it('should destroy views that are inactive after the active view', () => { - let view1 = new ViewController(Page1); - view1.state = STATE_INACTIVE; - let view2 = new ViewController(Page2); - view2.state = STATE_ACTIVE; - let view3 = new ViewController(Page3); - view3.state = STATE_INACTIVE; - let view4 = new ViewController(Page4); - view4.state = STATE_TRANS_ENTER; - let view5 = new ViewController(Page5); - view5.state = STATE_INACTIVE; - nav.views = [view1, view2, view3, view4, view5]; - nav._cleanup(); - - expect(nav.length()).toBe(3); - expect(nav.getByIndex(0).state).toBe(STATE_INACTIVE); - expect(nav.getByIndex(0).componentType).toBe(Page1); - expect(nav.getByIndex(1).state).toBe(STATE_ACTIVE); - expect(nav.getByIndex(1).componentType).toBe(Page2); - expect(nav.getByIndex(2).state).toBe(STATE_TRANS_ENTER); - expect(nav.getByIndex(2).componentType).toBe(Page4); - }); - - it('should not destroy any views since the last is active', () => { - let view1 = new ViewController(Page1); - view1.state = STATE_INACTIVE; - let view2 = new ViewController(Page2); - view2.state = STATE_ACTIVE; - nav.views = [view1, view2]; - nav._cleanup(); - expect(nav.length()).toBe(2); - }); - - it('should call destroy for each view to be destroyed', () => { - let view1 = new ViewController(Page1); - view1.state = STATE_ACTIVE; - let view2 = new ViewController(Page2); - view2.state = STATE_INACTIVE; - let view3 = new ViewController(Page3); - view3.state = STATE_INACTIVE; - nav.views = [view1, view2, view3]; - - spyOn(view1, 'destroy'); - spyOn(view2, 'destroy'); - spyOn(view3, 'destroy'); - - nav._cleanup(); - - expect(nav.length()).toBe(1); - expect(view1.destroy).not.toHaveBeenCalled(); - expect(view2.destroy).toHaveBeenCalled(); - expect(view3.destroy).toHaveBeenCalled(); - }); - - it('should reset zIndexes if their is a negative zindex', () => { - let view1 = new ViewController(Page1); - view1.setPageRef( getElementRef() ); - view1.state = STATE_INACTIVE; - view1.zIndex = -1; - - let view2 = new ViewController(Page2); - view2.setPageRef( getElementRef() ); - view2.state = STATE_INACTIVE; - view2.zIndex = 0; - - let view3 = new ViewController(Page3); - view3.setPageRef( getElementRef() ); - view3.state = STATE_ACTIVE; - view3.zIndex = 1; - - nav.views = [view1, view2, view3]; - nav._cleanup(); - - expect(view1.zIndex).toEqual(100); - expect(view2.zIndex).toEqual(101); - expect(view3.zIndex).toEqual(102); - }); - }); - - describe('_postRender', () => { - it('should immediately call done when enteringView state is inactive', () => { - let view1 = new ViewController(Page1); - view1.state = STATE_INACTIVE; - var wasCalled = false; - var done = () => { - wasCalled = true; - }; - nav._beforeTrans = () => {}; //prevent running beforeTrans for tests - - nav._postRender(1, view1, null, false, null, done); - - expect(wasCalled).toBe(true); - }); - - it('should call willEnter on entering view', () => { - let enteringView = new ViewController(Page1); - let leavingView = new ViewController(Page2); - var navOptions: NavOptions = {}; - var done = () => {}; - nav._beforeTrans = () => {}; //prevent running beforeTrans for tests - - spyOn(enteringView, 'fireWillEnter'); - - nav._postRender(1, enteringView, leavingView, false, navOptions, done); - - expect(enteringView.fireWillEnter).toHaveBeenCalled(); - }); - - it('should not call willEnter on entering view when it is being preloaded', () => { - let enteringView = new ViewController(Page1); - let leavingView = new ViewController(Page2); - var navOptions: NavOptions = { - preload: true - }; - var done = () => {}; - nav._beforeTrans = () => {}; //prevent running beforeTrans for tests - - spyOn(enteringView, 'fireWillEnter'); - - nav._postRender(1, enteringView, leavingView, false, navOptions, done); - - expect(enteringView.fireWillEnter).not.toHaveBeenCalled(); - }); - - it('should call willLeave on leaving view', () => { - let enteringView = new ViewController(Page1); - let leavingView = new ViewController(Page2); - var navOptions: NavOptions = {}; - var done = () => {}; - nav._beforeTrans = () => {}; //prevent running beforeTrans for tests - - spyOn(leavingView, 'fireWillLeave'); - - nav._postRender(1, enteringView, leavingView, false, navOptions, done); - - expect(leavingView.fireWillLeave).toHaveBeenCalled(); - }); - - it('should not call willEnter when the leaving view has fireOtherLifecycles not true', () => { - let enteringView = new ViewController(Page1); - let leavingView = new ViewController(Page2); - var navOptions: NavOptions = {}; - var done = () => {}; - nav._beforeTrans = () => {}; //prevent running beforeTrans for tests - - spyOn(enteringView, 'fireWillEnter'); - spyOn(leavingView, 'fireWillLeave'); - - leavingView.fireOtherLifecycles = false; - - nav._postRender(1, enteringView, leavingView, false, navOptions, done); - - expect(enteringView.fireWillEnter).not.toHaveBeenCalled(); - expect(leavingView.fireWillLeave).toHaveBeenCalled(); - }); - - it('should not call willLeave when the entering view has fireOtherLifecycles not true', () => { - let enteringView = new ViewController(Page1); - let leavingView = new ViewController(Page2); - var navOptions: NavOptions = {}; - var done = () => {}; - nav._beforeTrans = () => {}; //prevent running beforeTrans for tests - - spyOn(enteringView, 'fireWillEnter'); - spyOn(leavingView, 'fireWillLeave'); - - enteringView.fireOtherLifecycles = false; - - nav._postRender(1, enteringView, leavingView, false, navOptions, done); - - expect(enteringView.fireWillEnter).toHaveBeenCalled(); - expect(leavingView.fireWillLeave).not.toHaveBeenCalled(); - }); - - it('should not call willLeave on leaving view when it is being preloaded', () => { - let enteringView = new ViewController(Page1); - let leavingView = new ViewController(Page2); - var navOptions: NavOptions = { - preload: true - }; - var done = () => {}; - nav._beforeTrans = () => {}; //prevent running beforeTrans for tests - - spyOn(leavingView, 'fireWillLeave'); - - nav._postRender(1, enteringView, leavingView, false, navOptions, done); - - expect(leavingView.fireWillLeave).not.toHaveBeenCalled(); - }); - - it('should set animate false when preloading', () => { - let enteringView = new ViewController(Page1); - let leavingView = new ViewController(Page2); - var navOptions: NavOptions = { - preload: true - }; - var done = () => {}; - nav._beforeTrans = () => {}; //prevent running beforeTrans for tests - - nav._postRender(1, enteringView, leavingView, false, navOptions, done); - - expect(navOptions.animate).toBe(false); - }); - - it('should set domShow true when isAlreadyTransitioning', () => { - let enteringView = new ViewController(Page1); - let leavingView = new ViewController(Page2); - let isAlreadyTransitioning = true; - var navOptions: NavOptions = {}; - var done = () => {}; - nav._beforeTrans = () => {}; //prevent running beforeTrans for tests - nav._renderer = null; - - spyOn(enteringView, 'domShow'); - spyOn(leavingView, 'domShow'); - - nav._postRender(1, enteringView, leavingView, isAlreadyTransitioning, navOptions, done); - - expect(enteringView.domShow).toHaveBeenCalledWith(true, nav._renderer); - expect(leavingView.domShow).toHaveBeenCalledWith(true, nav._renderer); - }); - - it('should set domShow true when isAlreadyTransitioning false for the entering/leaving views', () => { - let view1 = new ViewController(Page1); - let view2 = new ViewController(Page2); - let view3 = new ViewController(Page3); - let isAlreadyTransitioning = false; - var navOptions: NavOptions = {}; - var done = () => {}; - nav._beforeTrans = () => {}; //prevent running beforeTrans for tests - nav._renderer = null; - nav.views = [view1, view2, view3]; - - spyOn(view1, 'domShow'); - spyOn(view2, 'domShow'); - spyOn(view3, 'domShow'); - - nav._postRender(1, view3, view2, isAlreadyTransitioning, navOptions, done); - - expect(view1.domShow).toHaveBeenCalledWith(false, nav._renderer); - expect(view2.domShow).toHaveBeenCalledWith(true, nav._renderer); - expect(view3.domShow).toHaveBeenCalledWith(true, nav._renderer); - }); - - it('should set domShow true when isAlreadyTransitioning false for views when a view has isOverlay=true', () => { - let view1 = new ViewController(Page1); - let view2 = new ViewController(Page2); - let view3 = new ViewController(Page3); - let view4 = new ViewController(Page4); - let isAlreadyTransitioning = false; - var navOptions: NavOptions = {}; - var done = () => {}; - nav._beforeTrans = () => {}; //prevent running beforeTrans for tests - nav._renderer = null; - nav.views = [view1, view2, view3, view4]; - - view3.isOverlay = true; - - spyOn(view1, 'domShow'); - spyOn(view2, 'domShow'); - spyOn(view3, 'domShow'); - spyOn(view4, 'domShow'); - - nav._postRender(1, view4, view3, isAlreadyTransitioning, navOptions, done); - - expect(view1.domShow).toHaveBeenCalledWith(false, nav._renderer); - expect(view2.domShow).toHaveBeenCalledWith(true, nav._renderer); - expect(view3.domShow).toHaveBeenCalledWith(true, nav._renderer); - expect(view4.domShow).toHaveBeenCalledWith(true, nav._renderer); - }); - - }); - - describe('_setZIndex', () => { - - it('should set zIndex off of the previous view to the entering view is loaded and the leavingView is not loaded', () => { - let leavingView = new ViewController(); - leavingView.zIndex = 100; - leavingView._loaded = true; - let enteringView = new ViewController(); - enteringView.setPageRef({}); - - nav.views = [leavingView, enteringView]; - - nav._setZIndex(enteringView, leavingView, 'forward'); - expect(enteringView.zIndex).toEqual(101); - }); - - it('should set zIndex 100 when leaving view is not loaded', () => { - let leavingView = new ViewController(); - leavingView._loaded = false; - let enteringView = new ViewController(); - enteringView.setPageRef({}); - - nav.views = [leavingView, enteringView]; - - nav._setZIndex(enteringView, leavingView, 'forward'); - expect(enteringView.zIndex).toEqual(100); - }); - - it('should set zIndex 100 on first entering view', () => { - let enteringView = new ViewController(); - enteringView.setPageRef({}); - nav._setZIndex(enteringView, null, 'forward'); - expect(enteringView.zIndex).toEqual(100); - }); - - it('should set zIndex 1 on second entering view', () => { - let leavingView = new ViewController(); - leavingView.zIndex = 0; - leavingView._loaded = true; - let enteringView = new ViewController(); - enteringView.setPageRef({}); - nav._setZIndex(enteringView, leavingView, 'forward'); - expect(enteringView.zIndex).toEqual(1); - }); - - it('should set zIndex 0 on entering view going back', () => { - let leavingView = new ViewController(); - leavingView.zIndex = 1; - leavingView._loaded = true; - let enteringView = new ViewController(); - enteringView.setPageRef({}); - nav._setZIndex(enteringView, leavingView, 'back'); - expect(enteringView.zIndex).toEqual(0); - }); - - it('should set zIndex 9999 on first entering portal view', () => { - let enteringView = new ViewController(); - enteringView.setPageRef({}); - nav.isPortal = true; - nav._setZIndex(enteringView, null, 'forward'); - expect(enteringView.zIndex).toEqual(9999); - }); - - it('should set zIndex 10000 on second entering portal view', () => { - let leavingView = new ViewController(); - leavingView.zIndex = 9999; - leavingView._loaded = true; - let enteringView = new ViewController(); - enteringView.setPageRef({}); - nav._portal = null; - nav._setZIndex(enteringView, leavingView, 'forward'); - expect(enteringView.zIndex).toEqual(10000); - }); - - it('should set zIndex 9999 on entering portal view going back', () => { - let leavingView = new ViewController(); - leavingView.zIndex = 10000; - leavingView._loaded = true; - let enteringView = new ViewController(); - enteringView.setPageRef({}); - nav._portal = null; - nav._setZIndex(enteringView, leavingView, 'back'); - expect(enteringView.zIndex).toEqual(9999); - }); - - }); - - describe('_setAnimate', () => { - - it('should be unchanged when the nav is a portal', () => { - nav.views = [new ViewController()]; - nav._init = false; - nav.isPortal = true; - let opts: NavOptions = {}; - nav._setAnimate(opts); - expect(opts.animate).toBeUndefined(); - }); - - it('should not animate when theres only 1 view, and nav hasnt initialized yet', () => { - nav.views = [new ViewController()]; - nav._init = false; - let opts: NavOptions = {}; - nav._setAnimate(opts); - expect(opts.animate).toEqual(false); - }); - - it('should be unchanged when theres only 1 view, and nav has already initialized', () => { - nav.views = [new ViewController()]; - nav._init = true; - let opts: NavOptions = {}; - nav._setAnimate(opts); - expect(opts.animate).toBeUndefined(); - }); - - it('should not animate with config animate = false, and has initialized', () => { - config.set('animate', false); - nav._init = true; - let opts: NavOptions = {}; - nav._setAnimate(opts); - expect(opts.animate).toEqual(false); - }); - - it('should not animate with config animate = false, and has not initialized', () => { - config.set('animate', false); - nav._init = false; - let opts: NavOptions = {}; - nav._setAnimate(opts); - expect(opts.animate).toEqual(false); - }); - - }); - - describe('_afterTrans', () => { - - it('should call didEnter/didLeave', () => { - let enteringView = new ViewController(); - let leavingView = new ViewController(); - let navOpts: NavOptions = {}; - let hasCompleted = true; - let doneCalled = false; - let done = () => {doneCalled = true;} - - spyOn(enteringView, 'fireDidEnter'); - spyOn(leavingView, 'fireDidLeave'); - - nav._init = true; - nav._afterTrans(enteringView, leavingView, navOpts, hasCompleted, done); - - expect(enteringView.fireDidEnter).toHaveBeenCalled(); - expect(leavingView.fireDidLeave).toHaveBeenCalled(); - expect(doneCalled).toBe(true); - }); - - it('should not call didEnter/didLeave when preloaded', () => { - let enteringView = new ViewController(); - let leavingView = new ViewController(); - let navOpts: NavOptions = { - preload: true - }; - let hasCompleted = true; - let doneCalled = false; - let done = () => {doneCalled = true;} - - spyOn(enteringView, 'fireDidEnter'); - spyOn(leavingView, 'fireDidLeave'); - - nav._init = true; - nav._afterTrans(enteringView, leavingView, navOpts, hasCompleted, done); - - expect(enteringView.fireDidEnter).not.toHaveBeenCalled(); - expect(leavingView.fireDidLeave).not.toHaveBeenCalled(); - expect(doneCalled).toBe(true); - }); - - it('should not call didLeave when enteringView set fireOtherLifecycles to false', () => { - let enteringView = new ViewController(); - let leavingView = new ViewController(); - let navOpts: NavOptions = {}; - let hasCompleted = true; - let doneCalled = false; - let done = () => {doneCalled = true;} - - enteringView.fireOtherLifecycles = false; - - spyOn(enteringView, 'fireDidEnter'); - spyOn(leavingView, 'fireDidLeave'); - - nav._afterTrans(enteringView, leavingView, navOpts, hasCompleted, done); - - expect(enteringView.fireDidEnter).toHaveBeenCalled(); - expect(leavingView.fireDidLeave).not.toHaveBeenCalled(); - expect(doneCalled).toBe(true); - }); - - it('should not call didEnter when leavingView set fireOtherLifecycles to false', () => { - let enteringView = new ViewController(); - let leavingView = new ViewController(); - let navOpts: NavOptions = {}; - let hasCompleted = true; - let doneCalled = false; - let done = () => {doneCalled = true;} - - leavingView.fireOtherLifecycles = false; - - spyOn(enteringView, 'fireDidEnter'); - spyOn(leavingView, 'fireDidLeave'); - - nav._init = true; - nav._afterTrans(enteringView, leavingView, navOpts, hasCompleted, done); - - expect(enteringView.fireDidEnter).not.toHaveBeenCalled(); - expect(leavingView.fireDidLeave).toHaveBeenCalled(); - expect(doneCalled).toBe(true); - }); - - it('should not call didEnter/didLeave when not hasCompleted', () => { - let enteringView = new ViewController(); - let leavingView = new ViewController(); - let navOpts: NavOptions = {}; - let hasCompleted = false; - let doneCalled = false; - let done = () => {doneCalled = true;} - - spyOn(enteringView, 'fireDidEnter'); - spyOn(leavingView, 'fireDidLeave'); - - nav._afterTrans(enteringView, leavingView, navOpts, hasCompleted, done); - - expect(enteringView.fireDidEnter).not.toHaveBeenCalled(); - expect(leavingView.fireDidLeave).not.toHaveBeenCalled(); - expect(doneCalled).toBe(true); - }); - - }); - - describe('_transFinish', () => { - - it('should remove entering view if it was already set to cancel', () => { - let enteringView = new ViewController(Page1); - let leavingView = new ViewController(Page2); - enteringView.state = STATE_CANCEL_ENTER; - - spyOn(nav, 'remove'); - - nav._transFinish(1, enteringView, leavingView, 'forward', true); - - expect(nav.remove).toHaveBeenCalled(); - expect(enteringView.state).toBe(STATE_CANCEL_ENTER); - }); - - it('should not entering/leaving state, after transition that isnt the most recent, and state already changed', () => { - let enteringView = new ViewController(Page1); - enteringView.state = 'somethingelse'; - let leavingView = new ViewController(Page2); - leavingView.state = 'somethingelse'; - - nav._transIds = 2; - - nav._transFinish(1, enteringView, leavingView, 'forward', true); - - expect(enteringView.state).toBe('somethingelse'); - expect(leavingView.state).toBe('somethingelse'); - }); - - it('should set entering/leaving to inactive, after transition that isnt the most recent', () => { - let enteringView = new ViewController(Page1); - enteringView.state = STATE_TRANS_ENTER; - let leavingView = new ViewController(Page2); - leavingView.state = STATE_TRANS_LEAVE; - - nav._transIds = 2; - - nav._transFinish(1, enteringView, leavingView, 'forward', true); - - expect(enteringView.state).toBe(STATE_INACTIVE); - expect(leavingView.state).toBe(STATE_INACTIVE); - }); - - it('should set entering active, leaving inactive, after transition', () => { - let enteringView = new ViewController(Page1); - enteringView.state = STATE_TRANS_ENTER; - let leavingView = new ViewController(Page2); - leavingView.state = STATE_TRANS_LEAVE; - - nav._transIds = 1; - - nav._transFinish(1, enteringView, leavingView, 'forward', true); - - expect(enteringView.state).toBe(STATE_ACTIVE); - expect(leavingView.state).toBe(STATE_INACTIVE); - }); - - it('should set entering inactive, leaving active, after transition has not completed', () => { - let enteringView = new ViewController(Page1); - enteringView.state = STATE_TRANS_ENTER; - let leavingView = new ViewController(Page2); - leavingView.state = STATE_TRANS_LEAVE; - - nav._transIds = 1; - - nav._transFinish(1, enteringView, leavingView, 'back', false); - - expect(enteringView.state).toBe(STATE_INACTIVE); - expect(leavingView.state).toBe(STATE_ACTIVE); - }); - - it('should run cleanup when most recent transition and has completed', () => { - let enteringView = new ViewController(Page1); - enteringView.state = STATE_TRANS_ENTER; - let leavingView = new ViewController(Page2); - leavingView.state = STATE_TRANS_LEAVE; - let hasCompleted = true; - - spyOn(nav, '_cleanup'); - - nav._transIds = 1; - - nav._transFinish(1, enteringView, leavingView, 'back', hasCompleted); - - expect(nav._cleanup).toHaveBeenCalled(); - }); - - it('should not run cleanup when most not recent transition', () => { - let enteringView = new ViewController(Page1); - enteringView.state = STATE_TRANS_ENTER; - let leavingView = new ViewController(Page2); - leavingView.state = STATE_TRANS_LEAVE; - let hasCompleted = true; - - spyOn(nav, '_cleanup'); - - nav._transIds = 1; - - nav._transFinish(2, enteringView, leavingView, 'back', hasCompleted); - - expect(nav._cleanup).not.toHaveBeenCalled(); - }); - - it('should not run cleanup when it hasnt completed transition, but is the most recent', () => { - let enteringView = new ViewController(Page1); - enteringView.state = STATE_TRANS_ENTER; - let leavingView = new ViewController(Page2); - leavingView.state = STATE_TRANS_LEAVE; - let hasCompleted = false; - - spyOn(nav, '_cleanup'); - - nav._transIds = 1; - - nav._transFinish(1, enteringView, leavingView, 'back', hasCompleted); - - expect(nav._cleanup).not.toHaveBeenCalled(); - }); - - it('should set transitioning is over when most recent transition finishes', () => { - let enteringView = new ViewController(Page1); - enteringView.state = STATE_TRANS_ENTER; - let leavingView = new ViewController(Page2); - leavingView.state = STATE_TRANS_LEAVE; - let hasCompleted = true; - - spyOn(nav, 'setTransitioning'); - - nav._transIds = 1; - - nav._transFinish(1, enteringView, leavingView, 'back', hasCompleted); - - expect(nav.setTransitioning).toHaveBeenCalledWith(false); - }); - - it('should set transitioning is not over if its not the most recent transition', () => { - let enteringView = new ViewController(Page1); - enteringView.state = STATE_TRANS_ENTER; - let leavingView = new ViewController(Page2); - leavingView.state = STATE_TRANS_LEAVE; - let hasCompleted = true; - - spyOn(nav, 'setTransitioning'); - - nav._transIds = 2; - - nav._transFinish(1, enteringView, leavingView, 'back', hasCompleted); - - expect(nav.setTransitioning).not.toHaveBeenCalled(); - }); - - it('should set not run domShow when when any view in the stack has isOverlay=true', () => { - let view1 = new ViewController(Page1); - let view2 = new ViewController(Page2); - let view3 = new ViewController(Page3); - let view4 = new ViewController(Page4); - let hasCompleted = true; - nav.views = [view1, view2, view3, view4]; - - view1.isOverlay = true; - - nav._transIds = 1; - - spyOn(view1, 'domShow'); - spyOn(view2, 'domShow'); - spyOn(view3, 'domShow'); - spyOn(view4, 'domShow'); - - nav._transFinish(1, view4, view3, 'forward', hasCompleted); - - expect(view1.domShow).not.toHaveBeenCalled(); - expect(view2.domShow).not.toHaveBeenCalled(); - expect(view3.domShow).not.toHaveBeenCalled(); - expect(view4.domShow).toHaveBeenCalled(); - }); - - it('should re-enable the app when transition time <= 0', () => { - // arrange - let enteringView = new ViewController(Page1); - enteringView.state = 'somethingelse'; - let leavingView = new ViewController(Page2); - leavingView.state = 'somethingelse'; - nav._transIds = 1; - nav._app = { - setEnabled: () => {} - }; - - spyOn(nav._app, 'setEnabled'); - spyOn(nav, 'setTransitioning'); - - // act - nav._transFinish(nav._transIds, enteringView, leavingView, 'forward', true); - - // assert - expect(nav._app.setEnabled).toHaveBeenCalledWith(true); - expect(nav.setTransitioning).toHaveBeenCalledWith(false); - }); - - it('should not re-enable app when transition time > 0', () => { - // arrange - let enteringView = new ViewController(Page1); - enteringView.state = 'somethingelse'; - let leavingView = new ViewController(Page2); - leavingView.state = 'somethingelse'; - nav._transIds = 1; - nav._app = { - setEnabled: () => {} - }; - - spyOn(nav._app, 'setEnabled'); - spyOn(nav, 'setTransitioning'); - - nav._getLongestTrans = () => { return 50 }; - - // act - nav._transFinish(nav._transIds, enteringView, leavingView, 'forward', true); - - // assert - expect(nav._app.setEnabled).not.toHaveBeenCalled(); - expect(nav.setTransitioning).toHaveBeenCalledWith(false); - }); - - }); - - describe('_insert', () => { - - it('should push page when previous transition is still actively transitioning', () => { - let view1 = new ViewController(Page1); - view1.state = STATE_TRANS_ENTER; - let view2 = new ViewController(Page2); - view2.state = STATE_TRANS_LEAVE; - nav.views = [view1, view2]; - - let view3 = new ViewController(Page3); - nav._insert(-1, [view3]); - - expect(nav.getByIndex(0).state).toBe(STATE_TRANS_ENTER); - expect(nav.getByIndex(0).componentType).toBe(Page1); - expect(nav.getByIndex(1).state).toBe(STATE_TRANS_LEAVE); - expect(nav.getByIndex(1).componentType).toBe(Page2); - expect(nav.getByIndex(2).state).toBe(STATE_INIT_ENTER); - expect(nav.getByIndex(2).componentType).toBe(Page3); - }); - - it('should push page when previous transition views init, but havent transitioned yet', () => { - let view1 = new ViewController(Page1); - view1.state = STATE_INIT_LEAVE; - let view2 = new ViewController(Page2); - view2.state = STATE_INIT_ENTER; - nav.views = [view1, view2]; - - let view3 = new ViewController(Page3); - nav._insert(-1, [view3]); - - expect(nav.getByIndex(0).state).toBe(STATE_INIT_LEAVE); - expect(nav.getByIndex(0).componentType).toBe(Page1); - expect(nav.getByIndex(1).state).toBe(STATE_INACTIVE); - expect(nav.getByIndex(1).componentType).toBe(Page2); - expect(nav.getByIndex(2).state).toBe(STATE_INIT_ENTER); - expect(nav.getByIndex(2).componentType).toBe(Page3); - }); - - it('should insert multiple pages, back to back, with a starting active page', () => { - let view1 = new ViewController(Page1); - view1.state = STATE_ACTIVE; - nav.views = [view1]; - - let view2 = new ViewController(Page2); - nav._insert(-1, [view2]); - - expect(nav.getByIndex(0).state).toBe(STATE_INIT_LEAVE); - expect(nav.getByIndex(0).componentType).toBe(Page1); - expect(nav.getByIndex(1).state).toBe(STATE_INIT_ENTER); - expect(nav.getByIndex(1).componentType).toBe(Page2); - - let view3 = new ViewController(Page3); - nav._insert(-1, [view3]); - - expect(nav.getByIndex(0).state).toBe(STATE_INIT_LEAVE); - expect(nav.getByIndex(0).componentType).toBe(Page1); - expect(nav.getByIndex(1).state).toBe(STATE_INACTIVE); - expect(nav.getByIndex(1).componentType).toBe(Page2); - expect(nav.getByIndex(2).state).toBe(STATE_INIT_ENTER); - expect(nav.getByIndex(2).componentType).toBe(Page3); - }); - - it('should insert multiple pages, back to back, no starting active page', () => { - let view1 = new ViewController(Page1); - nav._insert(-1, [view1]); - expect(nav.getByIndex(0).state).toBe(STATE_INIT_ENTER); - expect(nav.getByIndex(0).componentType).toBe(Page1); - - let view2 = new ViewController(Page2); - nav._insert(-1, [view2]); - - expect(nav.getByIndex(0).state).toBe(STATE_INACTIVE); - expect(nav.getByIndex(0).componentType).toBe(Page1); - expect(nav.getByIndex(1).state).toBe(STATE_INIT_ENTER); - expect(nav.getByIndex(1).componentType).toBe(Page2); - - let view3 = new ViewController(Page3); - nav._insert(1, [view3]); - - expect(nav.getByIndex(0).state).toBe(STATE_INACTIVE); - expect(nav.getByIndex(0).componentType).toBe(Page1); - expect(nav.getByIndex(1).state).toBe(STATE_INIT_ENTER); - expect(nav.getByIndex(1).componentType).toBe(Page3); - expect(nav.getByIndex(2).state).toBe(STATE_INACTIVE); - expect(nav.getByIndex(2).componentType).toBe(Page2); - }); - - it('should push a page, and abort previous init', () => { - let view1 = new ViewController(Page1); - view1.state = STATE_INIT_LEAVE; - let view2 = new ViewController(Page2); - view2.state = STATE_INIT_ENTER; - nav.views = [view1, view2]; - - let view3 = new ViewController(Page3); - nav._insert(-1, [view3]); - expect(nav.length()).toBe(3); - - expect(nav.getByIndex(0).state).toBe(STATE_INIT_LEAVE); - expect(nav.getByIndex(0).componentType).toBe(Page1); - expect(nav.getByIndex(1).state).toBe(STATE_INACTIVE); - expect(nav.getByIndex(1).componentType).toBe(Page2); - expect(nav.getByIndex(2).state).toBe(STATE_INIT_ENTER); - expect(nav.getByIndex(2).componentType).toBe(Page3); - }); - - it('should insert a page between the first and second', () => { - let view1 = new ViewController(Page1); - view1.state = STATE_INACTIVE; - let view2 = new ViewController(Page2); - view2.state = STATE_ACTIVE; - nav.views = [view1, view2]; - - let view3 = new ViewController(Page3); - nav._insert(1, [view3]); - expect(nav.length()).toBe(3); - - expect(nav.getByIndex(0).state).toBe(STATE_INACTIVE); - expect(nav.getByIndex(0).componentType).toBe(Page1); - expect(nav.getByIndex(1).state).toBe(STATE_INIT_ENTER); - expect(nav.getByIndex(1).componentType).toBe(Page3); - expect(nav.getByIndex(2).state).toBe(STATE_INIT_LEAVE); - expect(nav.getByIndex(2).componentType).toBe(Page2); - }); - - it('should insert a page before the first', () => { - let view1 = new ViewController(Page1); - view1.state = STATE_ACTIVE; - nav.views = [view1]; - - let view2 = new ViewController(Page2); - nav._insert(0, [view2]); - expect(nav.getByIndex(0).state).toBe(STATE_INIT_ENTER); - expect(nav.getByIndex(0).componentType).toBe(Page2); - expect(nav.getByIndex(1).state).toBe(STATE_INIT_LEAVE); - expect(nav.getByIndex(1).componentType).toBe(Page1); - }); - - it('should insert 3 pages', () => { - let view1 = new ViewController(Page1); - view1.state = STATE_ACTIVE; - nav.views = [view1]; - - let insertViews = [ - new ViewController(Page2), - new ViewController(Page3), - new ViewController(Page4) - ]; - nav._insert(-1, insertViews); - expect(nav.length()).toBe(4); - expect(nav.getByIndex(0).state).toBe(STATE_INIT_LEAVE); - expect(nav.getByIndex(0).componentType).toBe(Page1); - expect(nav.getByIndex(1).state).toBe(STATE_INACTIVE); - expect(nav.getByIndex(1).componentType).toBe(Page2); - expect(nav.getByIndex(2).state).toBe(STATE_INACTIVE); - expect(nav.getByIndex(2).componentType).toBe(Page3); - expect(nav.getByIndex(3).state).toBe(STATE_INIT_ENTER); - expect(nav.getByIndex(3).componentType).toBe(Page4); - }); - - it('should push the second page', () => { - let view1 = new ViewController(Page1); - view1.state = STATE_ACTIVE; - nav.views = [view1]; - - let view2 = new ViewController(Page2) - nav._insert(-1, [view2]); - expect(nav.length()).toBe(2); - expect(nav.getByIndex(0).state).toBe(STATE_INIT_LEAVE); - expect(nav.getByIndex(0).componentType).toBe(Page1); - expect(nav.getByIndex(1).state).toBe(STATE_INIT_ENTER); - expect(nav.getByIndex(1).componentType).toBe(Page2); - }); - - it('should push the first page, using a number greater than the length', () => { - let view1 = new ViewController(Page1) - nav._insert(8675309, [view1]); - - expect(nav.length()).toBe(1); - expect(nav.getByIndex(0).state).toBe(STATE_INIT_ENTER); - }); - - it('should push the first page, using -1', () => { - let view1 = new ViewController(Page1) - nav._insert(-1, [view1]); - - expect(nav.getByIndex(0).id).toBeDefined(); - expect(nav.length()).toBe(1); - expect(nav.getByIndex(0).state).toBe(STATE_INIT_ENTER); - }); - - }); - - describe('getActive', () => { - it('should getActive()', () => { - expect(nav.getActive()).toBe(null); - let view1 = new ViewController(Page1); - view1.state = STATE_INIT_ENTER; - nav.views = [view1]; - expect(nav.getActive()).toBe(null); - view1.state = STATE_ACTIVE; - expect(nav.getActive()).toBe(view1); - }); - }); - - describe('getByState', () => { - it('should getByState()', () => { - expect(nav.getByState(null)).toBe(null); - - let view1 = new ViewController(Page1); - view1.state = STATE_INIT_ENTER; - let view2 = new ViewController(Page2); - view2.state = STATE_INIT_ENTER; - nav.views = [view1, view2]; - - expect(nav.getByState('whatever')).toBe(null); - expect(nav.getByState(STATE_INIT_ENTER)).toBe(view2); - - view2.state = STATE_INACTIVE; - expect(nav.getByState(STATE_INIT_ENTER)).toBe(view1); - - view2.state = STATE_ACTIVE; - expect(nav.getActive()).toBe(view2); - }); - }); - - describe('getPrevious', () => { - it('should getPrevious()', () => { - expect(nav.getPrevious(null)).toBe(null); - - let view1 = new ViewController(Page1); - let view2 = new ViewController(Page2); - nav.views = [view1, view2]; - - expect(nav.getPrevious(view1)).toBe(null); - expect(nav.getPrevious(view2)).toBe(view1); - }); - }); - - describe('first', () => { - it('should get first()', () => { - expect(nav.first()).toBe(null); - let view1 = new ViewController(Page1); - view1.setNav(nav); - let view2 = new ViewController(Page2); - view2.setNav(nav); - nav.views = [view1]; - - expect(nav.first()).toBe(view1); - expect(view1.isFirst()).toBe(true); - - nav.views = [view1, view2]; - expect(nav.first()).toBe(view1); - expect(view1.isFirst()).toBe(true); - expect(view2.isFirst()).toBe(false); - }); - }); - - describe('last', () => { - it('should get last()', () => { - expect(nav.last()).toBe(null); - let view1 = new ViewController(Page1); - view1.setNav(nav); - let view2 = new ViewController(Page2); - view2.setNav(nav); - nav.views = [view1]; - - expect(nav.last()).toBe(view1); - expect(view1.isLast()).toBe(true); - - nav.views = [view1, view2]; - expect(nav.last()).toBe(view2); - expect(view1.isLast()).toBe(false); - expect(view2.isLast()).toBe(true); - }); - }); - - describe('indexOf', () => { - it('should get indexOf()', () => { - let view1 = new ViewController(Page1); - let view2 = new ViewController(Page2); - - expect(nav.length()).toBe(0); - expect(nav.indexOf(view1)).toBe(-1); - - nav.views = [view1, view2]; - expect(nav.indexOf(view1)).toBe(0); - expect(nav.indexOf(view2)).toBe(1); - expect(nav.length()).toBe(2); - }); - }); - - describe('getByIndex', () => { - it('should get getByIndex()', () => { - expect(nav.getByIndex(-99)).toBe(null); - expect(nav.getByIndex(99)).toBe(null); - - let view1 = new ViewController(Page1); - let view2 = new ViewController(Page2); - nav.views = [view1, view2]; - - expect(nav.getByIndex(-1)).toBe(null); - expect(nav.getByIndex(0)).toBe(view1); - expect(nav.getByIndex(1)).toBe(view2); - expect(nav.getByIndex(2)).toBe(null); - }); - }); - - /* private method */ - describe('_beforeTrans', () => { - - it('shouldnt disable app on short transition', () => { - // arrange - let executeAssertions = () => { - // assertions triggerd by callbacks - expect(app.setEnabled).toHaveBeenCalledWith(true, 50); - expect(nav.setTransitioning).toHaveBeenCalledWith(false, 50); - }; - let mockTransition = { - play: () => { - executeAssertions(); - }, - getDuration: () => { return 50}, - onFinish: () => {} - }; - nav._createTrans = () => { - return mockTransition; - }; - nav.config = { - platform : { - isRTL: () => {} - } - }; - let app = { - setEnabled: () => {} - }; - nav._app = app; - - spyOn(app, 'setEnabled'); - spyOn(nav, 'setTransitioning'); - - let view1 = new ViewController(Page1); - let view2 = new ViewController(Page2); - - // act - nav._beforeTrans(view1, view2, {}, () => {}); - }); - - it('should disable app on longer transition', () => { - // arrange - let executeAssertions = () => { - // assertions triggerd by callbacks - expect(app.setEnabled).toHaveBeenCalledWith(false, 200); - expect(nav.setTransitioning).toHaveBeenCalledWith(true, 200); - }; - let mockTransition = { - play: () => { - executeAssertions(); - }, - getDuration: () => { return 200}, - onFinish: () => {} - }; - nav._createTrans = () => { - return mockTransition; - }; - nav.config = { - platform : { - isRTL: () => {} - } - }; - let app = { - setEnabled: () => {} - }; - nav._app = app; - - spyOn(app, 'setEnabled'); - spyOn(nav, 'setTransitioning'); - - let view1 = new ViewController(Page1); - let view2 = new ViewController(Page2); - - // act - nav._beforeTrans(view1, view2, {}, () => {}); - }); - - it('should disable app w/ padding when keyboard is open', () => { - // arrange - let executeAssertions = () => { - // assertions triggerd by callbacks - expect(app.setEnabled.calls.mostRecent().args[0]).toEqual(false); - expect(app.setEnabled.calls.mostRecent().args[1]).toBeGreaterThan(200); - - expect(nav.setTransitioning.calls.mostRecent().args[0]).toEqual(true); - expect(nav.setTransitioning.calls.mostRecent().args[1]).toBeGreaterThan(200); - }; - let mockTransition = { - play: () => { - executeAssertions(); - }, - getDuration: () => { return 200}, - onFinish: () => {} - }; - nav._createTrans = () => { - return mockTransition; - }; - nav.config = { - platform : { - isRTL: () => {} - } - }; - let app = { - setEnabled: () => {} - }; - nav._app = app; - nav._keyboard = { - isOpen: () => true - }; - - spyOn(app, 'setEnabled'); - spyOn(nav, 'setTransitioning'); - - let view1 = new ViewController(Page1); - let view2 = new ViewController(Page2); - - // act - nav._beforeTrans(view1, view2, {}, () => {}); - }); - - it('shouldnt update app enabled when parent transition is occurring', () => { - // arrange - let executeAssertions = () => { - // assertions triggerd by callbacks - expect(app.setEnabled).not.toHaveBeenCalled(); - expect(nav.setTransitioning.calls.mostRecent().args[0]).toEqual(true); - }; - let mockTransition = { - play: () => { - executeAssertions(); - }, - getDuration: () => { return 200}, - onFinish: () => {} - }; - nav._createTrans = () => { - return mockTransition; - }; - nav.config = { - platform : { - isRTL: () => {} - } - }; - let app = { - setEnabled: () => {} - }; - nav._app = app; - - spyOn(app, 'setEnabled'); - spyOn(nav, 'setTransitioning'); - - nav._getLongestTrans = () => { return Date.now() + 100 }; - - let view1 = new ViewController(Page1); - let view2 = new ViewController(Page2); - - // act - nav._beforeTrans(view1, view2, {}, () => {}); - }); - - it('should not begin transition when entering stated is inactive', () => { - let view1 = new ViewController(Page1); - view1.state = STATE_INACTIVE; - - let wasDoneCalled = false; - let done = () => { - wasDoneCalled = true; - }; - - nav._beforeTrans(view1, null, {}, done); - - expect(wasDoneCalled).toEqual(true); - expect(view1.state).toEqual(STATE_INACTIVE); - }); - - it('should not begin transition when entering state is canceled', () => { - let view1 = new ViewController(Page1); - view1.state = STATE_CANCEL_ENTER; - - let wasDoneCalled = false; - let done = () => { - wasDoneCalled = true; - }; - - nav._beforeTrans(view1, null, {}, done); - - expect(wasDoneCalled).toEqual(true); - expect(view1.state).toEqual(STATE_CANCEL_ENTER); - }); - }); - - /* private method */ - describe('_getLongestTrans', () => { - it('should return 0 when transition end time is less than 0', () => { - // arrange - nav.parent = null; - // act - let returnedValue = nav._getLongestTrans(Date.now()); - // asssert - expect(returnedValue).toEqual(0); - }); - - it('should return 0 when transition end time is less than now', () => { - // arrange - nav.parent = { - trnsTime: Date.now() - 5 - }; - // act - let returnedValue = nav._getLongestTrans(Date.now()); - // asssert - expect(returnedValue).toEqual(0); - }); - - it('should return 0 when parent transition time not set', () => { - // arrange - nav.parent = { - trnsTime: undefined - }; - // act - let returnedValue = nav._getLongestTrans(Date.now()); - // asssert - expect(returnedValue).toEqual(0); - }); - - it('should return transitionEndTime when transition end time is greater than now', () => { - // arrange - let expectedReturnValue = Date.now() + 100; - nav.parent = { - trnsTime: expectedReturnValue - }; - // act - let returnedValue = nav._getLongestTrans(Date.now()); - // asssert - expect(returnedValue).toEqual(expectedReturnValue); - }); - - it('should return the greatest end of transition time if found on first parent', () => { - // arrange - let expectedReturnValue = Date.now() + 100; - let firstParent = { - trnsTime: expectedReturnValue - }; - let secondParent = { - trnsTime: Date.now() + 50 - }; - let thirdParent = { - trnsTime: Date.now() - }; - let fourthParent = { - trnsTime: Date.now() + 20 - }; - firstParent.parent = secondParent; - secondParent.parent = thirdParent; - thirdParent.parent = fourthParent; - nav.parent = firstParent; - // act - let returnedValue = nav._getLongestTrans(Date.now()); - // asssert - expect(returnedValue).toEqual(expectedReturnValue); - }); - - it('should return the greatest end of transition time if found on middle parent', () => { - // arrange - let expectedReturnValue = Date.now() + 100; - let firstParent = { - trnsTime: Date.now() - }; - let secondParent = { - trnsTime: Date.now() + 50 - }; - let thirdParent = { - trnsTime: expectedReturnValue - }; - let fourthParent = { - trnsTime: Date.now() + 20 - }; - firstParent.parent = secondParent; - secondParent.parent = thirdParent; - thirdParent.parent = fourthParent; - nav.parent = firstParent; - // act - let returnedValue = nav._getLongestTrans(Date.now()); - // asssert - expect(returnedValue).toEqual(expectedReturnValue); - }); - - it('should return the greatest end of transition time if found on last parent', () => { - // arrange - let expectedReturnValue = Date.now() + 100; - let firstParent = { - trnsTime: Date.now() - }; - let secondParent = { - trnsTime: Date.now() + 50 - }; - let thirdParent = { - trnsTime: Date.now() + 20 - }; - let fourthParent = { - trnsTime: expectedReturnValue - }; - firstParent.parent = secondParent; - secondParent.parent = thirdParent; - thirdParent.parent = fourthParent; - nav.parent = firstParent; - // act - let returnedValue = nav._getLongestTrans(Date.now()); - // asssert - expect(returnedValue).toEqual(expectedReturnValue); - }); - }); - - // setup stuff - let nav: MockNavController; - let config = new Config(); - let platform = new Platform(); - - class Page1 {} - class Page2 {} - class Page3 {} - class Page4 {} - class Page5 {} - - beforeEach(() => { - nav = mockNav(); - }); - - function mockNav(): MockNavController { - let elementRef = getElementRef(); - - let app = new App(config, platform); - let nav = new MockNavController(null, app, config, null, elementRef, null, null, null); - - nav._keyboard = { - isOpen: function() { - return false; - } - }; - nav._zone = { - run: function(cb) { - cb(); - }, - runOutsideAngular: function(cb) { - cb(); - } - }; - nav._renderer = { - setElementAttribute: function(){}, - setElementClass: function(){}, - setElementStyle: function(){} - }; - - return nav; - } - - function getElementRef() { - return { - nativeElement: document.createElement('div') - } - } - }); + + describe('popToRoot', () => { + + it('should go back to root', () => { + let view1 = new ViewController(Page1); + view1.state = STATE_INACTIVE; + let view2 = new ViewController(Page2); + view2.state = STATE_INACTIVE; + let view3 = new ViewController(Page3); + view3.state = STATE_INACTIVE; + let view4 = new ViewController(Page4); + view4.state = STATE_ACTIVE; + nav._views = [view1, view2, view3, view4]; + + nav.popToRoot(); + expect(nav.length()).toBe(2); + expect(nav.getByIndex(0).state).toBe(STATE_INIT_ENTER); + expect(nav.getByIndex(0).componentType).toBe(Page1); + expect(nav.getByIndex(1).state).toBe(STATE_INIT_LEAVE); + expect(nav.getByIndex(1).componentType).toBe(Page4); + + expect(view2.state).toBe(STATE_REMOVE); + expect(view3.state).toBe(STATE_REMOVE); + }); + + }); + + describe('popTo', () => { + + it('should go back two views', () => { + let view1 = new ViewController(Page1); + view1.state = STATE_INACTIVE; + let view2 = new ViewController(Page2); + view2.state = STATE_INACTIVE; + let view3 = new ViewController(Page3); + view3.state = STATE_INACTIVE; + let view4 = new ViewController(Page4); + view4.state = STATE_ACTIVE; + nav._views = [view1, view2, view3, view4]; + + nav.popTo(view2); + + expect(nav.length()).toBe(3); + expect(nav.getByIndex(0).state).toBe(STATE_INACTIVE); + expect(nav.getByIndex(0).componentType).toBe(Page1); + expect(nav.getByIndex(1).state).toBe(STATE_INIT_ENTER); + expect(nav.getByIndex(1).componentType).toBe(Page2); + expect(view3.state).toBe(STATE_REMOVE); + expect(nav.getByIndex(2).state).toBe(STATE_INIT_LEAVE); + expect(nav.getByIndex(2).componentType).toBe(Page4); + }); + + }); + + describe('remove', () => { + + it('should create opts if passed in arg is undefined or null', () => { + let view1 = new ViewController(Page1); + view1.state = STATE_INACTIVE; + let view2 = new ViewController(Page2); + view2.state = STATE_ACTIVE; + nav._views = [view1, view2]; + + nav.remove(1, 1, null); + }); + + }); + + describe('_remove', () => { + + it('should reassign activily transitioning leave that isnt getting removed, to become force active', () => { + let view1 = new ViewController(Page1); + view1.state = STATE_INACTIVE; + let view2 = new ViewController(Page2); + view2.state = STATE_TRANS_LEAVE; + let view3 = new ViewController(Page3); + view3.state = STATE_TRANS_ENTER; + nav._views = [view1, view2, view3]; + + nav._remove(2, 1); + + expect(nav.getByIndex(0).state).toBe(STATE_INACTIVE); + expect(nav.getByIndex(0).componentType).toBe(Page1); + expect(nav.getByIndex(1).state).toBe(STATE_FORCE_ACTIVE); + expect(nav.getByIndex(1).componentType).toBe(Page2); + expect(nav.getByIndex(2).state).toBe(STATE_REMOVE_AFTER_TRANS); + expect(nav.getByIndex(2).componentType).toBe(Page3); + }); + + it('should reassign activily transitioning views that should be removed to STATE_REMOVE_AFTER_TRANS', () => { + let view1 = new ViewController(Page1); + view1.state = STATE_INACTIVE; + let view2 = new ViewController(Page2); + view2.state = STATE_TRANS_ENTER; + let view3 = new ViewController(Page3); + view3.state = STATE_TRANS_LEAVE; + nav._views = [view1, view2, view3]; + + nav._remove(1, 2); + expect(nav.getByIndex(0).state).toBe(STATE_INACTIVE); + expect(nav.getByIndex(0).componentType).toBe(Page1); + expect(nav.getByIndex(1).state).toBe(STATE_REMOVE_AFTER_TRANS); + expect(nav.getByIndex(1).componentType).toBe(Page2); + expect(nav.getByIndex(2).state).toBe(STATE_REMOVE_AFTER_TRANS); + expect(nav.getByIndex(2).componentType).toBe(Page3); + }); + + it('should keep same init leave, but set previous init enter to inactive', () => { + let view1 = new ViewController(Page1); + view1.state = STATE_INACTIVE; + let view2 = new ViewController(Page2); + view2.state = STATE_INIT_ENTER; + let view3 = new ViewController(Page3); + view3.state = STATE_INIT_LEAVE; + nav._views = [view1, view2, view3]; + + nav._remove(1, 1); + expect(nav.length()).toBe(3); + expect(view1.state).toBe(STATE_INIT_ENTER); + expect(view2.state).toBe(STATE_CANCEL_ENTER); + expect(view3.state).toBe(STATE_INIT_LEAVE); + + expect(nav.getByIndex(0).state).toBe(STATE_INIT_ENTER); + expect(nav.getByIndex(0).componentType).toBe(Page1); + expect(nav.getByIndex(1).state).toBe(STATE_CANCEL_ENTER); + expect(nav.getByIndex(1).componentType).toBe(Page2); + expect(nav.getByIndex(2).state).toBe(STATE_INIT_LEAVE); + expect(nav.getByIndex(2).componentType).toBe(Page3); + }); + + it('should set to pop the active and enter the previous', () => { + let view1 = new ViewController(Page1); + view1.state = STATE_INACTIVE; + let view2 = new ViewController(Page2); + view2.state = STATE_ACTIVE; + nav._views = [view1, view2]; + + nav._remove(1, 1); + expect(view1.state).toBe(STATE_INIT_ENTER); + expect(view2.state).toBe(STATE_INIT_LEAVE); + }); + + it('should set to remove 2 views before active one, active stays the same', () => { + let view1 = new ViewController(Page1); + view1.state = STATE_INACTIVE; + let view2 = new ViewController(Page2); + view2.state = STATE_INACTIVE; + let view3 = new ViewController(Page3); + view3.state = STATE_INACTIVE; + let view4 = new ViewController(Page4); + view4.state = STATE_INACTIVE; + let view5 = new ViewController(Page5); + view5.state = STATE_ACTIVE; + nav._views = [view1, view2, view3, view4, view5]; + + nav._remove(2, 2); + expect(nav.length()).toBe(3); + expect(view1.state).toBe(STATE_INACTIVE); + expect(view2.state).toBe(STATE_INACTIVE); + expect(view3.state).toBe(STATE_REMOVE); + expect(view4.state).toBe(STATE_REMOVE); + expect(view5.state).toBe(STATE_ACTIVE); + + expect(nav.getByIndex(0).state).toBe(STATE_INACTIVE); + expect(nav.getByIndex(0).componentType).toBe(Page1); + expect(nav.getByIndex(1).state).toBe(STATE_INACTIVE); + expect(nav.getByIndex(1).componentType).toBe(Page2); + expect(nav.getByIndex(2).state).toBe(STATE_ACTIVE); + expect(nav.getByIndex(2).componentType).toBe(Page5); + }); + + it('should set to remove all views other than the first', () => { + let view1 = new ViewController(Page1); + view1.state = STATE_INACTIVE; + let view2 = new ViewController(Page2); + view2.state = STATE_INACTIVE; + let view3 = new ViewController(Page3); + view3.state = STATE_INACTIVE; + let view4 = new ViewController(Page4); + view4.state = STATE_ACTIVE; + nav._views = [view1, view2, view3, view4]; + + nav._remove(1, 9999); + expect(nav.length()).toBe(2); + expect(view1.state).toBe(STATE_INIT_ENTER); + expect(view2.state).toBe(STATE_REMOVE); + expect(view3.state).toBe(STATE_REMOVE); + expect(view4.state).toBe(STATE_INIT_LEAVE); + + expect(nav.getByIndex(0).state).toBe(STATE_INIT_ENTER); + expect(nav.getByIndex(0).componentType).toBe(Page1); + expect(nav.getByIndex(1).state).toBe(STATE_INIT_LEAVE); + expect(nav.getByIndex(1).componentType).toBe(Page4); + }); + + it('should set to remove 3 views and enter the first inactive one, remove includes active one', () => { + let view1 = new ViewController(Page1); + view1.state = STATE_INACTIVE; + let view2 = new ViewController(Page2); + view2.state = STATE_INACTIVE; + let view3 = new ViewController(Page3); + view3.state = STATE_INACTIVE; + let view4 = new ViewController(Page4); + view4.state = STATE_ACTIVE; + nav._views = [view1, view2, view3, view4]; + + nav._remove(1, 3); + expect(nav.length()).toBe(2); + expect(view1.state).toBe(STATE_INIT_ENTER); + expect(view2.state).toBe(STATE_REMOVE); + expect(view3.state).toBe(STATE_REMOVE); + expect(view4.state).toBe(STATE_INIT_LEAVE); + + expect(nav.getByIndex(0).state).toBe(STATE_INIT_ENTER); + expect(nav.getByIndex(0).componentType).toBe(Page1); + expect(nav.getByIndex(1).state).toBe(STATE_INIT_LEAVE); + expect(nav.getByIndex(1).componentType).toBe(Page4); + }); + + it('should set to remove the active and enter the previous', () => { + let view1 = new ViewController(Page1); + view1.state = STATE_INACTIVE; + let view2 = new ViewController(Page2); + view2.state = STATE_ACTIVE; + nav._views = [view1, view2]; + + nav._remove(1, 1); + expect(view1.state).toBe(STATE_INIT_ENTER); + expect(view2.state).toBe(STATE_INIT_LEAVE); + }); + + it('should set to remove the only view in the stack', () => { + let view1 = new ViewController(Page1); + view1.state = STATE_ACTIVE; + nav._views = [view1]; + + nav._remove(0, 1); + expect(nav.getByIndex(0).state).toBe(STATE_INIT_LEAVE); + }); + + it('should call willLeave/didLeave/destroy on views with STATE_REMOVE', () => { + let view1 = new ViewController(Page1); + view1.state = STATE_INACTIVE; + let view2 = new ViewController(Page2); + view2.state = STATE_INACTIVE; + let view3 = new ViewController(Page3); + view3.state = STATE_INACTIVE; + let view4 = new ViewController(Page4); + view4.state = STATE_ACTIVE; + nav._views = [view1, view2, view3, view4]; + + spyOn(view1, 'fireWillLeave'); + spyOn(view1, 'fireDidLeave'); + spyOn(view1, 'destroy'); + + spyOn(view2, 'fireWillLeave'); + spyOn(view2, 'fireDidLeave'); + spyOn(view2, 'destroy'); + + spyOn(view3, 'fireWillLeave'); + spyOn(view3, 'fireDidLeave'); + spyOn(view3, 'destroy'); + + spyOn(view4, 'fireWillLeave'); + spyOn(view4, 'fireDidLeave'); + spyOn(view4, 'destroy'); + + nav._remove(1, 3); + expect(nav.length()).toBe(2); + expect(view1.state).toBe(STATE_INIT_ENTER); + expect(view2.state).toBe(STATE_REMOVE); + expect(view3.state).toBe(STATE_REMOVE); + expect(view4.state).toBe(STATE_INIT_LEAVE); + + expect(view1.fireWillLeave).not.toHaveBeenCalled(); + expect(view1.fireDidLeave).not.toHaveBeenCalled(); + expect(view1.destroy).not.toHaveBeenCalled(); + + expect(view2.fireWillLeave).toHaveBeenCalled(); + expect(view2.fireDidLeave).toHaveBeenCalled(); + expect(view2.destroy).toHaveBeenCalled(); + + expect(view3.fireWillLeave).toHaveBeenCalled(); + expect(view3.fireDidLeave).toHaveBeenCalled(); + expect(view3.destroy).toHaveBeenCalled(); + + expect(view4.fireWillLeave).not.toHaveBeenCalled(); + expect(view4.fireDidLeave).not.toHaveBeenCalled(); + expect(view4.destroy).not.toHaveBeenCalled(); + }); + }); + + describe('_cleanup', () => { + it('should destroy views that are inactive after the active view', () => { + let view1 = new ViewController(Page1); + view1.state = STATE_INACTIVE; + let view2 = new ViewController(Page2); + view2.state = STATE_ACTIVE; + let view3 = new ViewController(Page3); + view3.state = STATE_INACTIVE; + let view4 = new ViewController(Page4); + view4.state = STATE_TRANS_ENTER; + let view5 = new ViewController(Page5); + view5.state = STATE_INACTIVE; + nav._views = [view1, view2, view3, view4, view5]; + nav._cleanup(); + + expect(nav.length()).toBe(3); + expect(nav.getByIndex(0).state).toBe(STATE_INACTIVE); + expect(nav.getByIndex(0).componentType).toBe(Page1); + expect(nav.getByIndex(1).state).toBe(STATE_ACTIVE); + expect(nav.getByIndex(1).componentType).toBe(Page2); + expect(nav.getByIndex(2).state).toBe(STATE_TRANS_ENTER); + expect(nav.getByIndex(2).componentType).toBe(Page4); + }); + + it('should not destroy any views since the last is active', () => { + let view1 = new ViewController(Page1); + view1.state = STATE_INACTIVE; + let view2 = new ViewController(Page2); + view2.state = STATE_ACTIVE; + nav._views = [view1, view2]; + nav._cleanup(); + expect(nav.length()).toBe(2); + }); + + it('should call destroy for each view to be destroyed', () => { + let view1 = new ViewController(Page1); + view1.state = STATE_ACTIVE; + let view2 = new ViewController(Page2); + view2.state = STATE_INACTIVE; + let view3 = new ViewController(Page3); + view3.state = STATE_INACTIVE; + nav._views = [view1, view2, view3]; + + spyOn(view1, 'destroy'); + spyOn(view2, 'destroy'); + spyOn(view3, 'destroy'); + + nav._cleanup(); + + expect(nav.length()).toBe(1); + expect(view1.destroy).not.toHaveBeenCalled(); + expect(view2.destroy).toHaveBeenCalled(); + expect(view3.destroy).toHaveBeenCalled(); + }); + + it('should reset zIndexes if their is a negative zindex', () => { + let view1 = new ViewController(Page1); + view1.setPageRef( mockElementRef() ); + view1.state = STATE_INACTIVE; + view1.zIndex = -1; + + let view2 = new ViewController(Page2); + view2.setPageRef( mockElementRef() ); + view2.state = STATE_INACTIVE; + view2.zIndex = 0; + + let view3 = new ViewController(Page3); + view3.setPageRef( mockElementRef() ); + view3.state = STATE_ACTIVE; + view3.zIndex = 1; + + nav._views = [view1, view2, view3]; + nav._cleanup(); + + expect(view1.zIndex).toEqual(100); + expect(view2.zIndex).toEqual(101); + expect(view3.zIndex).toEqual(102); + }); + }); + + describe('_postRender', () => { + it('should immediately call done when enteringView state is inactive', () => { + let view1 = new ViewController(Page1); + view1.state = STATE_INACTIVE; + var wasCalled = false; + var done = () => { + wasCalled = true; + }; + nav._beforeTrans = () => {}; //prevent running beforeTrans for tests + + nav._postRender(1, view1, null, false, null, done); + + expect(wasCalled).toBe(true); + }); + + it('should call willEnter on entering view', () => { + let enteringView = new ViewController(Page1); + let leavingView = new ViewController(Page2); + var navOptions: NavOptions = {}; + var done = () => {}; + nav._beforeTrans = () => {}; //prevent running beforeTrans for tests + + spyOn(enteringView, 'fireWillEnter'); + + nav._postRender(1, enteringView, leavingView, false, navOptions, done); + + expect(enteringView.fireWillEnter).toHaveBeenCalled(); + }); + + it('should not call willEnter on entering view when it is being preloaded', () => { + let enteringView = new ViewController(Page1); + let leavingView = new ViewController(Page2); + var navOptions: NavOptions = { + preload: true + }; + var done = () => {}; + nav._beforeTrans = () => {}; //prevent running beforeTrans for tests + + spyOn(enteringView, 'fireWillEnter'); + + nav._postRender(1, enteringView, leavingView, false, navOptions, done); + + expect(enteringView.fireWillEnter).not.toHaveBeenCalled(); + }); + + it('should call willLeave on leaving view', () => { + let enteringView = new ViewController(Page1); + let leavingView = new ViewController(Page2); + var navOptions: NavOptions = {}; + var done = () => {}; + nav._beforeTrans = () => {}; //prevent running beforeTrans for tests + + spyOn(leavingView, 'fireWillLeave'); + + nav._postRender(1, enteringView, leavingView, false, navOptions, done); + + expect(leavingView.fireWillLeave).toHaveBeenCalled(); + }); + + it('should not call willEnter when the leaving view has fireOtherLifecycles not true', () => { + let enteringView = new ViewController(Page1); + let leavingView = new ViewController(Page2); + var navOptions: NavOptions = {}; + var done = () => {}; + nav._beforeTrans = () => {}; //prevent running beforeTrans for tests + + spyOn(enteringView, 'fireWillEnter'); + spyOn(leavingView, 'fireWillLeave'); + + leavingView.fireOtherLifecycles = false; + + nav._postRender(1, enteringView, leavingView, false, navOptions, done); + + expect(enteringView.fireWillEnter).not.toHaveBeenCalled(); + expect(leavingView.fireWillLeave).toHaveBeenCalled(); + }); + + it('should not call willLeave when the entering view has fireOtherLifecycles not true', () => { + let enteringView = new ViewController(Page1); + let leavingView = new ViewController(Page2); + var navOptions: NavOptions = {}; + var done = () => {}; + nav._beforeTrans = () => {}; //prevent running beforeTrans for tests + + spyOn(enteringView, 'fireWillEnter'); + spyOn(leavingView, 'fireWillLeave'); + + enteringView.fireOtherLifecycles = false; + + nav._postRender(1, enteringView, leavingView, false, navOptions, done); + + expect(enteringView.fireWillEnter).toHaveBeenCalled(); + expect(leavingView.fireWillLeave).not.toHaveBeenCalled(); + }); + + it('should not call willLeave on leaving view when it is being preloaded', () => { + let enteringView = new ViewController(Page1); + let leavingView = new ViewController(Page2); + var navOptions: NavOptions = { + preload: true + }; + var done = () => {}; + nav._beforeTrans = () => {}; //prevent running beforeTrans for tests + + spyOn(leavingView, 'fireWillLeave'); + + nav._postRender(1, enteringView, leavingView, false, navOptions, done); + + expect(leavingView.fireWillLeave).not.toHaveBeenCalled(); + }); + + it('should set animate false when preloading', () => { + let enteringView = new ViewController(Page1); + let leavingView = new ViewController(Page2); + var navOptions: NavOptions = { + preload: true + }; + var done = () => {}; + nav._beforeTrans = () => {}; //prevent running beforeTrans for tests + + nav._postRender(1, enteringView, leavingView, false, navOptions, done); + + expect(navOptions.animate).toBe(false); + }); + + it('should set domShow true when isAlreadyTransitioning', () => { + let enteringView = new ViewController(Page1); + let leavingView = new ViewController(Page2); + let isAlreadyTransitioning = true; + var navOptions: NavOptions = {}; + var done = () => {}; + nav._beforeTrans = () => {}; //prevent running beforeTrans for tests + nav._renderer = null; + + spyOn(enteringView, 'domShow'); + spyOn(leavingView, 'domShow'); + + nav._postRender(1, enteringView, leavingView, isAlreadyTransitioning, navOptions, done); + + expect(enteringView.domShow).toHaveBeenCalledWith(true, nav._renderer); + expect(leavingView.domShow).toHaveBeenCalledWith(true, nav._renderer); + }); + + it('should set domShow true when isAlreadyTransitioning false for the entering/leaving views', () => { + let view1 = new ViewController(Page1); + let view2 = new ViewController(Page2); + let view3 = new ViewController(Page3); + let isAlreadyTransitioning = false; + var navOptions: NavOptions = {}; + var done = () => {}; + nav._beforeTrans = () => {}; //prevent running beforeTrans for tests + nav._renderer = null; + nav._views = [view1, view2, view3]; + + spyOn(view1, 'domShow'); + spyOn(view2, 'domShow'); + spyOn(view3, 'domShow'); + + nav._postRender(1, view3, view2, isAlreadyTransitioning, navOptions, done); + + expect(view1.domShow).toHaveBeenCalledWith(false, nav._renderer); + expect(view2.domShow).toHaveBeenCalledWith(true, nav._renderer); + expect(view3.domShow).toHaveBeenCalledWith(true, nav._renderer); + }); + + it('should set domShow true when isAlreadyTransitioning false for views when nav is a portal', () => { + let view1 = new ViewController(Page1); + let view2 = new ViewController(Page2); + let view3 = new ViewController(Page3); + let view4 = new ViewController(Page4); + let isAlreadyTransitioning = false; + var navOptions: NavOptions = {}; + var done = () => {}; + nav._beforeTrans = () => {}; //prevent running beforeTrans for tests + nav._renderer = null; + nav._views = [view1, view2, view3, view4]; + + nav._isPortal = true + + spyOn(view1, 'domShow'); + spyOn(view2, 'domShow'); + spyOn(view3, 'domShow'); + spyOn(view4, 'domShow'); + + nav._postRender(1, view4, view3, isAlreadyTransitioning, navOptions, done); + + expect(view1.domShow).toHaveBeenCalledWith(true, nav._renderer); + expect(view2.domShow).toHaveBeenCalledWith(true, nav._renderer); + expect(view3.domShow).toHaveBeenCalledWith(true, nav._renderer); + expect(view4.domShow).toHaveBeenCalledWith(true, nav._renderer); + }); + + }); + + describe('_setZIndex', () => { + + it('should set zIndex off of the previous view to the entering view is loaded and the leavingView is not loaded', () => { + let leavingView = new ViewController(); + leavingView.zIndex = 100; + leavingView.fireLoaded(); + let enteringView = new ViewController(); + enteringView.setPageRef(mockElementRef()); + + nav._views = [leavingView, enteringView]; + + nav._setZIndex(enteringView, leavingView, 'forward'); + expect(enteringView.zIndex).toEqual(101); + }); + + it('should set zIndex 100 when leaving view is not loaded', () => { + let leavingView = new ViewController(); + let enteringView = new ViewController(); + enteringView.setPageRef(mockElementRef()); + + nav._views = [leavingView, enteringView]; + + nav._setZIndex(enteringView, leavingView, 'forward'); + expect(enteringView.zIndex).toEqual(100); + }); + + it('should set zIndex 100 on first entering view', () => { + let enteringView = new ViewController(); + enteringView.setPageRef(mockElementRef()); + nav._setZIndex(enteringView, null, 'forward'); + expect(enteringView.zIndex).toEqual(100); + }); + + it('should set zIndex 1 on second entering view', () => { + let leavingView = new ViewController(); + leavingView.zIndex = 0; + leavingView.fireLoaded(); + let enteringView = new ViewController(); + enteringView.setPageRef(mockElementRef()); + nav._setZIndex(enteringView, leavingView, 'forward'); + expect(enteringView.zIndex).toEqual(1); + }); + + it('should set zIndex 0 on entering view going back', () => { + let leavingView = new ViewController(); + leavingView.zIndex = 1; + leavingView.fireLoaded(); + let enteringView = new ViewController(); + enteringView.setPageRef(mockElementRef()); + nav._setZIndex(enteringView, leavingView, 'back'); + expect(enteringView.zIndex).toEqual(0); + }); + + it('should set zIndex 9999 on first entering portal view', () => { + let enteringView = new ViewController(); + enteringView.setPageRef(mockElementRef()); + nav._isPortal = true; + nav._setZIndex(enteringView, null, 'forward'); + expect(enteringView.zIndex).toEqual(9999); + }); + + it('should set zIndex 10000 on second entering portal view', () => { + let leavingView = new ViewController(); + leavingView.zIndex = 9999; + leavingView.fireLoaded(); + let enteringView = new ViewController(); + enteringView.setPageRef(mockElementRef()); + nav._isPortal = true; + nav._setZIndex(enteringView, leavingView, 'forward'); + expect(enteringView.zIndex).toEqual(10000); + }); + + it('should set zIndex 9999 on entering portal view going back', () => { + let leavingView = new ViewController(); + leavingView.zIndex = 10000; + leavingView.fireLoaded(); + let enteringView = new ViewController(); + enteringView.setPageRef(mockElementRef()); + nav._isPortal = true; + nav._setZIndex(enteringView, leavingView, 'back'); + expect(enteringView.zIndex).toEqual(9999); + }); + + }); + + describe('_setAnimate', () => { + + it('should be unchanged when the nav is a portal', () => { + nav._views = [new ViewController()]; + nav._init = false; + nav._isPortal = true; + let opts: NavOptions = {}; + nav._setAnimate(opts); + expect(opts.animate).toBeUndefined(); + }); + + it('should not animate when theres only 1 view, and nav hasnt initialized yet', () => { + nav._views = [new ViewController()]; + nav._init = false; + let opts: NavOptions = {}; + nav._setAnimate(opts); + expect(opts.animate).toEqual(false); + }); + + it('should be unchanged when theres only 1 view, and nav has already initialized', () => { + nav._views = [new ViewController()]; + nav._init = true; + let opts: NavOptions = {}; + nav._setAnimate(opts); + expect(opts.animate).toBeUndefined(); + }); + + it('should not animate with config animate = false, and has initialized', () => { + nav.config.set('animate', false); + nav._init = true; + let opts: NavOptions = {}; + nav._setAnimate(opts); + expect(opts.animate).toEqual(false); + }); + + it('should not animate with config animate = false, and has not initialized', () => { + nav.config.set('animate', false); + nav._init = false; + let opts: NavOptions = {}; + nav._setAnimate(opts); + expect(opts.animate).toEqual(false); + }); + + }); + + describe('_afterTrans', () => { + + it('should call didEnter/didLeave', () => { + let enteringView = new ViewController(); + let leavingView = new ViewController(); + let navOpts: NavOptions = {}; + let hasCompleted = true; + let doneCalled = false; + let done = () => {doneCalled = true;} + + spyOn(enteringView, 'fireDidEnter'); + spyOn(leavingView, 'fireDidLeave'); + + nav._init = true; + nav._afterTrans(enteringView, leavingView, navOpts, hasCompleted, done); + + expect(enteringView.fireDidEnter).toHaveBeenCalled(); + expect(leavingView.fireDidLeave).toHaveBeenCalled(); + expect(doneCalled).toBe(true); + }); + + it('should not call didEnter/didLeave when preloaded', () => { + let enteringView = new ViewController(); + let leavingView = new ViewController(); + let navOpts: NavOptions = { + preload: true + }; + let hasCompleted = true; + let doneCalled = false; + let done = () => {doneCalled = true;} + + spyOn(enteringView, 'fireDidEnter'); + spyOn(leavingView, 'fireDidLeave'); + + nav._init = true; + nav._afterTrans(enteringView, leavingView, navOpts, hasCompleted, done); + + expect(enteringView.fireDidEnter).not.toHaveBeenCalled(); + expect(leavingView.fireDidLeave).not.toHaveBeenCalled(); + expect(doneCalled).toBe(true); + }); + + it('should not call didLeave when enteringView set fireOtherLifecycles to false', () => { + let enteringView = new ViewController(); + let leavingView = new ViewController(); + let navOpts: NavOptions = {}; + let hasCompleted = true; + let doneCalled = false; + let done = () => {doneCalled = true;} + + enteringView.fireOtherLifecycles = false; + + spyOn(enteringView, 'fireDidEnter'); + spyOn(leavingView, 'fireDidLeave'); + + nav._afterTrans(enteringView, leavingView, navOpts, hasCompleted, done); + + expect(enteringView.fireDidEnter).toHaveBeenCalled(); + expect(leavingView.fireDidLeave).not.toHaveBeenCalled(); + expect(doneCalled).toBe(true); + }); + + it('should not call didEnter when leavingView set fireOtherLifecycles to false', () => { + let enteringView = new ViewController(); + let leavingView = new ViewController(); + let navOpts: NavOptions = {}; + let hasCompleted = true; + let doneCalled = false; + let done = () => {doneCalled = true;} + + leavingView.fireOtherLifecycles = false; + + spyOn(enteringView, 'fireDidEnter'); + spyOn(leavingView, 'fireDidLeave'); + + nav._init = true; + nav._afterTrans(enteringView, leavingView, navOpts, hasCompleted, done); + + expect(enteringView.fireDidEnter).not.toHaveBeenCalled(); + expect(leavingView.fireDidLeave).toHaveBeenCalled(); + expect(doneCalled).toBe(true); + }); + + it('should not call didEnter/didLeave when not hasCompleted', () => { + let enteringView = new ViewController(); + let leavingView = new ViewController(); + let navOpts: NavOptions = {}; + let hasCompleted = false; + let doneCalled = false; + let done = () => {doneCalled = true;} + + spyOn(enteringView, 'fireDidEnter'); + spyOn(leavingView, 'fireDidLeave'); + + nav._afterTrans(enteringView, leavingView, navOpts, hasCompleted, done); + + expect(enteringView.fireDidEnter).not.toHaveBeenCalled(); + expect(leavingView.fireDidLeave).not.toHaveBeenCalled(); + expect(doneCalled).toBe(true); + }); + + }); + + describe('_transFinish', () => { + + it('should remove entering view if it was already set to cancel', () => { + let enteringView = new ViewController(Page1); + let leavingView = new ViewController(Page2); + enteringView.state = STATE_CANCEL_ENTER; + let direction = 'foward'; + let updateUrl = false; + let hasCompleted = true; + + spyOn(nav, 'remove'); + + nav._transFinish(1, enteringView, leavingView, direction, updateUrl, hasCompleted); + + expect(nav.remove).toHaveBeenCalled(); + expect(enteringView.state).toBe(STATE_CANCEL_ENTER); + }); + + it('should not entering/leaving state, after transition that isnt the most recent, and state already changed', () => { + let enteringView = new ViewController(Page1); + enteringView.state = 234234; + let leavingView = new ViewController(Page2); + leavingView.state = 234234; + let direction = 'foward'; + let updateUrl = false; + let hasCompleted = true; + + nav._transIds = 2; + + nav._transFinish(1, enteringView, leavingView, direction, updateUrl, hasCompleted); + + expect(enteringView.state).toBe(234234); + expect(leavingView.state).toBe(234234); + }); + + it('should set entering/leaving to inactive, after transition that isnt the most recent', () => { + let enteringView = new ViewController(Page1); + enteringView.state = STATE_TRANS_ENTER; + let leavingView = new ViewController(Page2); + leavingView.state = STATE_TRANS_LEAVE; + let direction = 'foward'; + let updateUrl = false; + let hasCompleted = true; + + nav._transIds = 2; + + nav._transFinish(1, enteringView, leavingView, direction, updateUrl, hasCompleted); + + expect(enteringView.state).toBe(STATE_INACTIVE); + expect(leavingView.state).toBe(STATE_INACTIVE); + }); + + it('should set entering active, leaving inactive, after transition', () => { + let enteringView = new ViewController(Page1); + enteringView.state = STATE_TRANS_ENTER; + let leavingView = new ViewController(Page2); + leavingView.state = STATE_TRANS_LEAVE; + let direction = 'foward'; + let updateUrl = false; + let hasCompleted = true; + + nav._transIds = 1; + + nav._transFinish(1, enteringView, leavingView, direction, updateUrl, hasCompleted); + + expect(enteringView.state).toBe(STATE_ACTIVE); + expect(leavingView.state).toBe(STATE_INACTIVE); + }); + + it('should set entering inactive, leaving active, after transition has not completed', () => { + let enteringView = new ViewController(Page1); + enteringView.state = STATE_TRANS_ENTER; + let leavingView = new ViewController(Page2); + leavingView.state = STATE_TRANS_LEAVE; + let direction = 'back'; + let updateUrl = false; + let hasCompleted = false; + + nav._transIds = 1; + + nav._transFinish(1, enteringView, leavingView, direction, updateUrl, hasCompleted); + + expect(enteringView.state).toBe(STATE_INACTIVE); + expect(leavingView.state).toBe(STATE_ACTIVE); + }); + + it('should run cleanup when most recent transition and has completed', () => { + let enteringView = new ViewController(Page1); + enteringView.state = STATE_TRANS_ENTER; + let leavingView = new ViewController(Page2); + leavingView.state = STATE_TRANS_LEAVE; + let direction = 'back'; + let updateUrl = false; + let hasCompleted = true; + + spyOn(nav, '_cleanup'); + + nav._transIds = 1; + + nav._transFinish(1, enteringView, leavingView, direction, updateUrl, hasCompleted); + + expect(nav._cleanup).toHaveBeenCalled(); + }); + + it('should not run cleanup when most not recent transition', () => { + let enteringView = new ViewController(Page1); + enteringView.state = STATE_TRANS_ENTER; + let leavingView = new ViewController(Page2); + leavingView.state = STATE_TRANS_LEAVE; + let direction = 'back'; + let updateUrl = false; + let hasCompleted = true; + + spyOn(nav, '_cleanup'); + + nav._transIds = 1; + + nav._transFinish(2, enteringView, leavingView, direction, updateUrl, hasCompleted); + + expect(nav._cleanup).not.toHaveBeenCalled(); + }); + + it('should not run cleanup when it hasnt completed transition, but is the most recent', () => { + let enteringView = new ViewController(Page1); + enteringView.state = STATE_TRANS_ENTER; + let leavingView = new ViewController(Page2); + leavingView.state = STATE_TRANS_LEAVE; + let direction = 'back'; + let updateUrl = false; + let hasCompleted = false; + + spyOn(nav, '_cleanup'); + + nav._transIds = 1; + + nav._transFinish(1, enteringView, leavingView, direction, updateUrl, hasCompleted); + + expect(nav._cleanup).not.toHaveBeenCalled(); + }); + + it('should set transitioning is over when most recent transition finishes', () => { + let enteringView = new ViewController(Page1); + enteringView.state = STATE_TRANS_ENTER; + let leavingView = new ViewController(Page2); + leavingView.state = STATE_TRANS_LEAVE; + let direction = 'back'; + let updateUrl = false; + let hasCompleted = true; + + spyOn(nav, 'setTransitioning'); + + nav._transIds = 1; + + nav._transFinish(1, enteringView, leavingView, direction, updateUrl, hasCompleted); + + expect(nav.setTransitioning).toHaveBeenCalledWith(false); + }); + + it('should set transitioning is not over if its not the most recent transition', () => { + let enteringView = new ViewController(Page1); + enteringView.state = STATE_TRANS_ENTER; + let leavingView = new ViewController(Page2); + leavingView.state = STATE_TRANS_LEAVE; + let direction = 'back'; + let updateUrl = false; + let hasCompleted = true; + + spyOn(nav, 'setTransitioning'); + + nav._transIds = 2; + + nav._transFinish(1, enteringView, leavingView, direction, updateUrl, hasCompleted); + + expect(nav.setTransitioning).not.toHaveBeenCalled(); + }); + + it('should re-enable the app when transition time <= 0', () => { + // arrange + let enteringView = new ViewController(Page1); + enteringView.state = 234234; + let leavingView = new ViewController(Page2); + leavingView.state = 234234; + nav._transIds = 1; + + let direction = 'forward'; + let updateUrl = false; + let hasCompleted = true; + + spyOn(nav._app, 'setEnabled'); + spyOn(nav, 'setTransitioning'); + + // act + nav._transFinish(nav._transIds, enteringView, leavingView, direction, updateUrl, hasCompleted); + + // assert + expect(nav._app.setEnabled).toHaveBeenCalledWith(true); + expect(nav.setTransitioning).toHaveBeenCalledWith(false); + }); + + it('should not re-enable app when transition time > 0', () => { + // arrange + let enteringView = new ViewController(Page1); + enteringView.state = 235234; + let leavingView = new ViewController(Page2); + leavingView.state = 235234; + nav._transIds = 1; + + let direction = 'forward'; + let updateUrl = false; + let hasCompleted = true; + + spyOn(nav._app, 'setEnabled'); + spyOn(nav, 'setTransitioning'); + + nav.getLongestTrans = () => { return 50 }; + + // act + nav._transFinish(nav._transIds, enteringView, leavingView, direction, updateUrl, hasCompleted); + + // assert + expect(nav._app.setEnabled).not.toHaveBeenCalled(); + expect(nav.setTransitioning).toHaveBeenCalledWith(false); + }); + + }); + + describe('_insert', () => { + + it('should push page when previous transition is still actively transitioning', () => { + let view1 = new ViewController(Page1); + view1.state = STATE_TRANS_ENTER; + let view2 = new ViewController(Page2); + view2.state = STATE_TRANS_LEAVE; + nav._views = [view1, view2]; + + let view3 = new ViewController(Page3); + nav._insert(-1, [view3]); + + expect(nav.getByIndex(0).state).toBe(STATE_TRANS_ENTER); + expect(nav.getByIndex(0).componentType).toBe(Page1); + expect(nav.getByIndex(1).state).toBe(STATE_TRANS_LEAVE); + expect(nav.getByIndex(1).componentType).toBe(Page2); + expect(nav.getByIndex(2).state).toBe(STATE_INIT_ENTER); + expect(nav.getByIndex(2).componentType).toBe(Page3); + }); + + it('should push page when previous transition views init, but havent transitioned yet', () => { + let view1 = new ViewController(Page1); + view1.state = STATE_INIT_LEAVE; + let view2 = new ViewController(Page2); + view2.state = STATE_INIT_ENTER; + nav._views = [view1, view2]; + + let view3 = new ViewController(Page3); + nav._insert(-1, [view3]); + + expect(nav.getByIndex(0).state).toBe(STATE_INIT_LEAVE); + expect(nav.getByIndex(0).componentType).toBe(Page1); + expect(nav.getByIndex(1).state).toBe(STATE_INACTIVE); + expect(nav.getByIndex(1).componentType).toBe(Page2); + expect(nav.getByIndex(2).state).toBe(STATE_INIT_ENTER); + expect(nav.getByIndex(2).componentType).toBe(Page3); + }); + + it('should insert multiple pages, back to back, with a starting active page', () => { + let view1 = new ViewController(Page1); + view1.state = STATE_ACTIVE; + nav._views = [view1]; + + let view2 = new ViewController(Page2); + nav._insert(-1, [view2]); + + expect(nav.getByIndex(0).state).toBe(STATE_INIT_LEAVE); + expect(nav.getByIndex(0).componentType).toBe(Page1); + expect(nav.getByIndex(1).state).toBe(STATE_INIT_ENTER); + expect(nav.getByIndex(1).componentType).toBe(Page2); + + let view3 = new ViewController(Page3); + nav._insert(-1, [view3]); + + expect(nav.getByIndex(0).state).toBe(STATE_INIT_LEAVE); + expect(nav.getByIndex(0).componentType).toBe(Page1); + expect(nav.getByIndex(1).state).toBe(STATE_INACTIVE); + expect(nav.getByIndex(1).componentType).toBe(Page2); + expect(nav.getByIndex(2).state).toBe(STATE_INIT_ENTER); + expect(nav.getByIndex(2).componentType).toBe(Page3); + }); + + it('should insert multiple pages, back to back, no starting active page', () => { + let view1 = new ViewController(Page1); + nav._insert(-1, [view1]); + expect(nav.getByIndex(0).state).toBe(STATE_INIT_ENTER); + expect(nav.getByIndex(0).componentType).toBe(Page1); + + let view2 = new ViewController(Page2); + nav._insert(-1, [view2]); + + expect(nav.getByIndex(0).state).toBe(STATE_INACTIVE); + expect(nav.getByIndex(0).componentType).toBe(Page1); + expect(nav.getByIndex(1).state).toBe(STATE_INIT_ENTER); + expect(nav.getByIndex(1).componentType).toBe(Page2); + + let view3 = new ViewController(Page3); + nav._insert(1, [view3]); + + expect(nav.getByIndex(0).state).toBe(STATE_INACTIVE); + expect(nav.getByIndex(0).componentType).toBe(Page1); + expect(nav.getByIndex(1).state).toBe(STATE_INIT_ENTER); + expect(nav.getByIndex(1).componentType).toBe(Page3); + expect(nav.getByIndex(2).state).toBe(STATE_INACTIVE); + expect(nav.getByIndex(2).componentType).toBe(Page2); + }); + + it('should push a page, and abort previous init', () => { + let view1 = new ViewController(Page1); + view1.state = STATE_INIT_LEAVE; + let view2 = new ViewController(Page2); + view2.state = STATE_INIT_ENTER; + nav._views = [view1, view2]; + + let view3 = new ViewController(Page3); + nav._insert(-1, [view3]); + expect(nav.length()).toBe(3); + + expect(nav.getByIndex(0).state).toBe(STATE_INIT_LEAVE); + expect(nav.getByIndex(0).componentType).toBe(Page1); + expect(nav.getByIndex(1).state).toBe(STATE_INACTIVE); + expect(nav.getByIndex(1).componentType).toBe(Page2); + expect(nav.getByIndex(2).state).toBe(STATE_INIT_ENTER); + expect(nav.getByIndex(2).componentType).toBe(Page3); + }); + + it('should insert a page between the first and second', () => { + let view1 = new ViewController(Page1); + view1.state = STATE_INACTIVE; + let view2 = new ViewController(Page2); + view2.state = STATE_ACTIVE; + nav._views = [view1, view2]; + + let view3 = new ViewController(Page3); + nav._insert(1, [view3]); + expect(nav.length()).toBe(3); + + expect(nav.getByIndex(0).state).toBe(STATE_INACTIVE); + expect(nav.getByIndex(0).componentType).toBe(Page1); + expect(nav.getByIndex(1).state).toBe(STATE_INIT_ENTER); + expect(nav.getByIndex(1).componentType).toBe(Page3); + expect(nav.getByIndex(2).state).toBe(STATE_INIT_LEAVE); + expect(nav.getByIndex(2).componentType).toBe(Page2); + }); + + it('should insert a page before the first', () => { + let view1 = new ViewController(Page1); + view1.state = STATE_ACTIVE; + nav._views = [view1]; + + let view2 = new ViewController(Page2); + nav._insert(0, [view2]); + expect(nav.getByIndex(0).state).toBe(STATE_INIT_ENTER); + expect(nav.getByIndex(0).componentType).toBe(Page2); + expect(nav.getByIndex(1).state).toBe(STATE_INIT_LEAVE); + expect(nav.getByIndex(1).componentType).toBe(Page1); + }); + + it('should insert 3 pages', () => { + let view1 = new ViewController(Page1); + view1.state = STATE_ACTIVE; + nav._views = [view1]; + + let insertViews = [ + new ViewController(Page2), + new ViewController(Page3), + new ViewController(Page4) + ]; + nav._insert(-1, insertViews); + expect(nav.length()).toBe(4); + expect(nav.getByIndex(0).state).toBe(STATE_INIT_LEAVE); + expect(nav.getByIndex(0).componentType).toBe(Page1); + expect(nav.getByIndex(1).state).toBe(STATE_INACTIVE); + expect(nav.getByIndex(1).componentType).toBe(Page2); + expect(nav.getByIndex(2).state).toBe(STATE_INACTIVE); + expect(nav.getByIndex(2).componentType).toBe(Page3); + expect(nav.getByIndex(3).state).toBe(STATE_INIT_ENTER); + expect(nav.getByIndex(3).componentType).toBe(Page4); + }); + + it('should push the second page', () => { + let view1 = new ViewController(Page1); + view1.state = STATE_ACTIVE; + nav._views = [view1]; + + let view2 = new ViewController(Page2) + nav._insert(-1, [view2]); + expect(nav.length()).toBe(2); + expect(nav.getByIndex(0).state).toBe(STATE_INIT_LEAVE); + expect(nav.getByIndex(0).componentType).toBe(Page1); + expect(nav.getByIndex(1).state).toBe(STATE_INIT_ENTER); + expect(nav.getByIndex(1).componentType).toBe(Page2); + }); + + it('should push the first page, using a number greater than the length', () => { + let view1 = new ViewController(Page1) + nav._insert(8675309, [view1]); + + expect(nav.length()).toBe(1); + expect(nav.getByIndex(0).state).toBe(STATE_INIT_ENTER); + }); + + it('should push the first page, using -1', () => { + let view1 = new ViewController(Page1) + nav._insert(-1, [view1]); + + expect(nav.getByIndex(0).id).toBeDefined(); + expect(nav.length()).toBe(1); + expect(nav.getByIndex(0).state).toBe(STATE_INIT_ENTER); + }); + + }); + + describe('getActive', () => { + it('should getActive()', () => { + expect(nav.getActive()).toBe(null); + let view1 = new ViewController(Page1); + view1.state = STATE_INIT_ENTER; + nav._views = [view1]; + expect(nav.getActive()).toBe(null); + view1.state = STATE_ACTIVE; + expect(nav.getActive()).toBe(view1); + }); + }); + + describe('getByState', () => { + it('should getByState()', () => { + expect(nav.getByState(null)).toBe(null); + + let view1 = new ViewController(Page1); + view1.state = STATE_INIT_ENTER; + let view2 = new ViewController(Page2); + view2.state = STATE_INIT_ENTER; + nav._views = [view1, view2]; + + expect(nav.getByState(8675309)).toBe(null); + expect(nav.getByState(STATE_INIT_ENTER)).toBe(view2); + + view2.state = STATE_INACTIVE; + expect(nav.getByState(STATE_INIT_ENTER)).toBe(view1); + + view2.state = STATE_ACTIVE; + expect(nav.getActive()).toBe(view2); + }); + }); + + describe('getPrevious', () => { + it('should getPrevious()', () => { + expect(nav.getPrevious(null)).toBe(null); + + let view1 = new ViewController(Page1); + let view2 = new ViewController(Page2); + nav._views = [view1, view2]; + + expect(nav.getPrevious(view1)).toBe(null); + expect(nav.getPrevious(view2)).toBe(view1); + }); + }); + + describe('first', () => { + it('should get first()', () => { + expect(nav.first()).toBe(null); + let view1 = new ViewController(Page1); + view1.setNav(nav); + let view2 = new ViewController(Page2); + view2.setNav(nav); + nav._views = [view1]; + + expect(nav.first()).toBe(view1); + expect(view1.isFirst()).toBe(true); + + nav._views = [view1, view2]; + expect(nav.first()).toBe(view1); + expect(view1.isFirst()).toBe(true); + expect(view2.isFirst()).toBe(false); + }); + }); + + describe('last', () => { + it('should get last()', () => { + expect(nav.last()).toBe(null); + let view1 = new ViewController(Page1); + view1.setNav(nav); + let view2 = new ViewController(Page2); + view2.setNav(nav); + nav._views = [view1]; + + expect(nav.last()).toBe(view1); + expect(view1.isLast()).toBe(true); + + nav._views = [view1, view2]; + expect(nav.last()).toBe(view2); + expect(view1.isLast()).toBe(false); + expect(view2.isLast()).toBe(true); + }); + }); + + describe('indexOf', () => { + it('should get indexOf()', () => { + let view1 = new ViewController(Page1); + let view2 = new ViewController(Page2); + + expect(nav.length()).toBe(0); + expect(nav.indexOf(view1)).toBe(-1); + + nav._views = [view1, view2]; + expect(nav.indexOf(view1)).toBe(0); + expect(nav.indexOf(view2)).toBe(1); + expect(nav.length()).toBe(2); + }); + }); + + describe('getByIndex', () => { + it('should get getByIndex()', () => { + expect(nav.getByIndex(-99)).toBe(null); + expect(nav.getByIndex(99)).toBe(null); + + let view1 = new ViewController(Page1); + let view2 = new ViewController(Page2); + nav._views = [view1, view2]; + + expect(nav.getByIndex(-1)).toBe(null); + expect(nav.getByIndex(0)).toBe(view1); + expect(nav.getByIndex(1)).toBe(view2); + expect(nav.getByIndex(2)).toBe(null); + }); + }); + + /* private method */ + describe('_beforeTrans', () => { + + it('shouldnt disable app on short transition', () => { + // arrange + let executeAssertions = () => { + // assertions triggerd by callbacks + expect(nav._app.setEnabled).toHaveBeenCalledWith(true, 50); + expect(nav.setTransitioning).toHaveBeenCalledWith(false, 50); + }; + nav._createTrans = mockTransition(executeAssertions, 50); + + spyOn(nav._app, 'setEnabled'); + spyOn(nav, 'setTransitioning'); + + let view1 = new ViewController(Page1); + let view2 = new ViewController(Page2); + + // act + nav._beforeTrans(view1, view2, {}, () => {}); + }); + + it('should disable app on longer transition', () => { + // arrange + let executeAssertions = () => { + // assertions triggerd by callbacks + expect(nav._app.setEnabled).toHaveBeenCalledWith(false, 200); + expect(nav.setTransitioning).toHaveBeenCalledWith(true, 200); + }; + + nav._createTrans = mockTransition(executeAssertions, 200); + + spyOn(nav._app, 'setEnabled'); + spyOn(nav, 'setTransitioning'); + + let view1 = new ViewController(Page1); + let view2 = new ViewController(Page2); + + // act + nav._beforeTrans(view1, view2, {}, () => {}); + }); + + it('should disable app w/ padding when keyboard is open', () => { + // arrange + let executeAssertions = () => { + // assertions triggerd by callbacks + expect(nav._app.setEnabled.calls.mostRecent().args[0]).toEqual(false); + expect(nav._app.setEnabled.calls.mostRecent().args[1]).toBeGreaterThan(200); + + expect(nav.setTransitioning.calls.mostRecent().args[0]).toEqual(true); + expect(nav.setTransitioning.calls.mostRecent().args[1]).toBeGreaterThan(200); + }; + + nav._createTrans = mockTransition(executeAssertions, 200); + + nav._keyboard.isOpen = () => true; + + spyOn(nav._app, 'setEnabled'); + spyOn(nav, 'setTransitioning'); + + let view1 = new ViewController(Page1); + let view2 = new ViewController(Page2); + + // act + nav._beforeTrans(view1, view2, {}, () => {}); + }); + + it('shouldnt update app enabled when parent transition is occurring', () => { + // arrange + let executeAssertions = () => { + // assertions triggerd by callbacks + expect(nav._app.setEnabled).not.toHaveBeenCalled(); + expect(nav.setTransitioning.calls.mostRecent().args[0]).toEqual(true); + }; + + nav._createTrans = mockTransition(executeAssertions, 200); + + spyOn(nav._app, 'setEnabled'); + spyOn(nav, 'setTransitioning'); + + nav.getLongestTrans = () => { return Date.now() + 100 }; + + let view1 = new ViewController(Page1); + let view2 = new ViewController(Page2); + + // act + nav._beforeTrans(view1, view2, {}, () => {}); + }); + + it('should not begin transition when entering stated is inactive', () => { + let view1 = new ViewController(Page1); + view1.state = STATE_INACTIVE; + + let wasDoneCalled = false; + let done = () => { + wasDoneCalled = true; + }; + + nav._beforeTrans(view1, null, {}, done); + + expect(wasDoneCalled).toEqual(true); + expect(view1.state).toEqual(STATE_INACTIVE); + }); + + it('should not begin transition when entering state is canceled', () => { + let view1 = new ViewController(Page1); + view1.state = STATE_CANCEL_ENTER; + + let wasDoneCalled = false; + let done = () => { + wasDoneCalled = true; + }; + + nav._beforeTrans(view1, null, {}, done); + + expect(wasDoneCalled).toEqual(true); + expect(view1.state).toEqual(STATE_CANCEL_ENTER); + }); + }); + + /* private method */ + describe('getLongestTrans', () => { + it('should return 0 when transition end time is less than 0', () => { + // arrange + nav.parent = null; + // act + let returnedValue = nav.getLongestTrans(Date.now()); + // asssert + expect(returnedValue).toEqual(0); + }); + + it('should return 0 when transition end time is less than now', () => { + // arrange + nav.parent = { + trnsTime: Date.now() - 5 + }; + // act + let returnedValue = nav.getLongestTrans(Date.now()); + // asssert + expect(returnedValue).toEqual(0); + }); + + it('should return 0 when parent transition time not set', () => { + // arrange + nav.parent = { + trnsTime: undefined + }; + // act + let returnedValue = nav.getLongestTrans(Date.now()); + // asssert + expect(returnedValue).toEqual(0); + }); + + it('should return transitionEndTime when transition end time is greater than now', () => { + // arrange + let expectedReturnValue = Date.now() + 100; + nav.parent = { + trnsTime: expectedReturnValue + }; + // act + let returnedValue = nav.getLongestTrans(Date.now()); + // asssert + expect(returnedValue).toEqual(expectedReturnValue); + }); + + it('should return the greatest end of transition time if found on first parent', () => { + // arrange + let expectedReturnValue = Date.now() + 100; + let firstParent: any = { + trnsTime: expectedReturnValue + }; + let secondParent: any = { + trnsTime: Date.now() + 50 + }; + let thirdParent: any = { + trnsTime: Date.now() + }; + let fourthParent: any = { + trnsTime: Date.now() + 20 + }; + firstParent.parent = secondParent; + secondParent.parent = thirdParent; + thirdParent.parent = fourthParent; + nav.parent = firstParent; + // act + let returnedValue = nav.getLongestTrans(Date.now()); + // asssert + expect(returnedValue).toEqual(expectedReturnValue); + }); + + it('should return the greatest end of transition time if found on middle parent', () => { + // arrange + let expectedReturnValue = Date.now() + 100; + let firstParent: any = { + trnsTime: Date.now() + }; + let secondParent: any = { + trnsTime: Date.now() + 50 + }; + let thirdParent: any = { + trnsTime: expectedReturnValue + }; + let fourthParent = { + trnsTime: Date.now() + 20 + }; + firstParent.parent = secondParent; + secondParent.parent = thirdParent; + thirdParent.parent = fourthParent; + nav.parent = firstParent; + // act + let returnedValue = nav.getLongestTrans(Date.now()); + // asssert + expect(returnedValue).toEqual(expectedReturnValue); + }); + + it('should return the greatest end of transition time if found on last parent', () => { + // arrange + let expectedReturnValue = Date.now() + 100; + let firstParent: any = { + trnsTime: Date.now() + }; + let secondParent: any = { + trnsTime: Date.now() + 50 + }; + let thirdParent: any = { + trnsTime: Date.now() + 20 + }; + let fourthParent = { + trnsTime: expectedReturnValue + }; + firstParent.parent = secondParent; + secondParent.parent = thirdParent; + thirdParent.parent = fourthParent; + nav.parent = firstParent; + // act + let returnedValue = nav.getLongestTrans(Date.now()); + // asssert + expect(returnedValue).toEqual(expectedReturnValue); + }); + }); + + // setup stuff + let nav: NavControllerBase; + + class Page1 {} + class Page2 {} + class Page3 {} + class Page4 {} + class Page5 {} + + beforeEach(() => { + nav = mockNavController(); + }); + +}); } - -class MockNavController extends NavController { - - get views(): ViewController[] { - return this._views; - } - set views(views: ViewController[]) { - this._views = views; - } - -} - - -const STATE_ACTIVE = 1; -const STATE_INACTIVE = 2; -const STATE_INIT_ENTER = 3; -const STATE_INIT_LEAVE = 4; -const STATE_TRANS_ENTER = 5; -const STATE_TRANS_LEAVE = 6; -const STATE_REMOVE = 7; -const STATE_REMOVE_AFTER_TRANS = 8; -const STATE_CANCEL_ENTER = 9; -const STATE_FORCE_ACTIVE = 10; diff --git a/src/components/tabs/tab.ts b/src/components/tabs/tab.ts index 482d14de2d..e04ab0a3aa 100644 --- a/src/components/tabs/tab.ts +++ b/src/components/tabs/tab.ts @@ -1,11 +1,11 @@ -import { ChangeDetectorRef, Component, ComponentResolver, ElementRef, EventEmitter, forwardRef, Input, Inject, NgZone, Output, Renderer, ViewChild, ViewEncapsulation, ViewContainerRef } from '@angular/core'; +import { ChangeDetectorRef, Component, ComponentResolver, ElementRef, EventEmitter, forwardRef, Input, Inject, NgZone, Optional, Output, Renderer, ViewChild, ViewEncapsulation, ViewContainerRef } from '@angular/core'; import { App } from '../app/app'; import { Config } from '../../config/config'; import { GestureController } from '../../gestures/gesture-controller'; import { isTrueProperty} from '../../util/util'; import { Keyboard} from '../../util/keyboard'; -import { NavController } from '../nav/nav-controller'; +import { NavControllerBase } from '../nav/nav-controller-base'; import { NavOptions} from '../nav/nav-interfaces'; import { TabButton} from './tab-button'; import { Tabs} from './tabs'; @@ -128,7 +128,7 @@ import { ViewController} from '../nav/view-controller'; template: '
', encapsulation: ViewEncapsulation.None, }) -export class Tab extends NavController { +export class Tab extends NavControllerBase { private _isInitial: boolean; private _isEnabled: boolean = true; private _isShown: boolean = true; @@ -236,10 +236,6 @@ export class Tab extends NavController { parent.add(this); - if (parent.rootNav) { - this._sbEnabled = parent.rootNav.isSwipeBackEnabled(); - } - this._tabId = 'tabpanel-' + this.id; this._btnId = 'tab-' + this.id; } @@ -264,7 +260,7 @@ export class Tab extends NavController { */ load(opts: NavOptions, done?: Function) { if (!this._loaded && this.root) { - this.push(this.root, this.rootParams, opts).then(() => { + this.push(this.root, this.rootParams, opts, () => { done(true); }); this._loaded = true; diff --git a/src/components/tabs/tabs.ts b/src/components/tabs/tabs.ts index 350b53887c..dffb4b3542 100644 --- a/src/components/tabs/tabs.ts +++ b/src/components/tabs/tabs.ts @@ -7,9 +7,11 @@ import { Config } from '../../config/config'; import { Content } from '../content/content'; import { Icon } from '../icon/icon'; import { Ion } from '../ion'; -import { isBlank, isTrueProperty } from '../../util/util'; +import { isBlank, isPresent, isTrueProperty } from '../../util/util'; import { nativeRaf } from '../../util/dom'; -import { NavController, DIRECTION_FORWARD } from '../nav/nav-controller'; +import { NavController } from '../nav/nav-controller'; +import { NavControllerBase } from '../nav/nav-controller-base'; +import { NavOptions, DIRECTION_FORWARD } from '../nav/nav-interfaces'; import { Platform } from '../../platform/platform'; import { Tab } from './tab'; import { TabButton } from './tab-button'; @@ -164,7 +166,7 @@ export class Tabs extends Ion { /** * @private */ - id: number; + id: string; /** * @private @@ -219,7 +221,7 @@ export class Tabs extends Ion { /** * @private */ - parent: NavController; + parent: NavControllerBase; constructor( @Optional() parent: NavController, @@ -232,8 +234,8 @@ export class Tabs extends Ion { ) { super(_elementRef); - this.parent = parent; - this.id = ++tabIds; + this.parent = parent; + this.id = 't' + (++tabIds); this._sbPadding = _config.getBoolean('statusbarPadding'); this._useHighlight = _config.getBoolean('tabsHighlight'); @@ -248,9 +250,9 @@ export class Tabs extends Ion { this._useHighlight = _config.getBoolean('tabbarHighlight'); } - if (parent) { + if (this.parent) { // this Tabs has a parent Nav - parent.registerChildNav(this); + this.parent.registerChildNav(this); } else if (this._app) { // this is the root navcontroller for the entire app @@ -320,41 +322,32 @@ export class Tabs extends Ion { * @private */ initTabs() { - // first check if preloadTab is set as an input @Input, then check the config - let preloadTabs = (isBlank(this.preloadTabs) ? this._config.getBoolean('preloadTabs') : isTrueProperty(this.preloadTabs)); + // get the selected index from the input + // otherwise default it to use the first index + let selectedIndex = (isBlank(this.selectedIndex) ? 0 : parseInt(this.selectedIndex, 10)); - // get the selected index - let selectedIndex = this.selectedIndex ? parseInt(this.selectedIndex, 10) : 0; - - // ensure the selectedIndex isn't a hidden or disabled tab - // also find the first available index incase we need it later - let availableIndex = -1; - this._tabs.forEach((tab, index) => { - if (tab.enabled && tab.show && availableIndex < 0) { - // we know this tab index is safe to show - availableIndex = index; - } - - if (index === selectedIndex && (!tab.enabled || !tab.show)) { - // the selectedIndex is not safe to show - selectedIndex = -1; - } - }); - - if (selectedIndex < 0) { - // the selected index wasn't safe to show - // instead use an available index found to be safe to show - selectedIndex = availableIndex; + // get the selectedIndex and ensure it isn't hidden or disabled + let selectedTab = this._tabs.find((t, i) => i === selectedIndex && t.enabled && t.show); + if (!selectedTab) { + // wasn't able to select the tab they wanted + // try to find the first tab that's available + selectedTab = this._tabs.find(t => t.enabled && t.show); } - this._tabs.forEach((tab, index) => { - if (index === selectedIndex) { - this.select(tab); + if (selectedTab) { + // we found a tab to select + this.select(selectedTab); + } - } else if (preloadTabs) { - tab.preload(1000 * index); - } - }); + // check if preloadTab is set as an input @Input + // otherwise check the preloadTabs config + let shouldPreloadTabs = (isBlank(this.preloadTabs) ? this._config.getBoolean('preloadTabs') : isTrueProperty(this.preloadTabs)); + if (shouldPreloadTabs) { + // preload all the tabs which isn't the selected tab + this._tabs.filter((t) => t !== selectedTab).forEach((tab, index) => { + tab.preload(this._config.getNumber('tabsPreloadDelay', 1000) * index); + }); + } } /** @@ -379,31 +372,33 @@ export class Tabs extends Ion { /** * @param {number|Tab} tabOrIndex Index, or the Tab instance, of the tab to select. */ - select(tabOrIndex: number | Tab) { + select(tabOrIndex: number | Tab, opts: NavOptions = {}, done?: Function): Promise { + let promise: Promise; + if (!done) { + promise = new Promise(res => { done = res; }); + } + let selectedTab: Tab = (typeof tabOrIndex === 'number' ? this.getByIndex(tabOrIndex) : tabOrIndex); if (isBlank(selectedTab)) { - return; + return Promise.resolve(); } let deselectedTab = this.getSelected(); - if (selectedTab === deselectedTab) { // no change - return this._touchActive(selectedTab); + this._touchActive(selectedTab); + return Promise.resolve(); } - console.debug(`Tabs, select: ${selectedTab.id}`); - let opts = { - animate: false - }; - let deselectedPage: ViewController; if (deselectedTab) { deselectedPage = deselectedTab.getActive(); deselectedPage && deselectedPage.fireWillLeave(); } + opts.animate = false; + let selectedPage = selectedTab.getActive(); selectedPage && selectedPage.fireWillEnter(); @@ -451,7 +446,11 @@ export class Tabs extends Ion { }); } } + + done(); }); + + return promise; } /** @@ -513,6 +512,13 @@ export class Tabs extends Ion { return this._tabs.indexOf(tab); } + /** + * @private + */ + length(): number { + return this._tabs.length; + } + /** * @private * "Touch" the active tab, going back to the root view of the tab @@ -548,20 +554,6 @@ export class Tabs extends Ion { return Promise.resolve(); } - /** - * @private - * Returns the root NavController. Returns `null` if Tabs is not - * within a NavController. - * @returns {NavController} - */ - get rootNav(): NavController { - let nav = this.parent; - while (nav && nav.parent) { - nav = nav.parent; - } - return nav; - } - /** * @private * DOM WRITE diff --git a/src/components/tabs/test/tabs.spec.ts b/src/components/tabs/test/tabs.spec.ts index 025ed668fe..219bb84ca1 100644 --- a/src/components/tabs/test/tabs.spec.ts +++ b/src/components/tabs/test/tabs.spec.ts @@ -1,10 +1,89 @@ -import {Component} from '@angular/core'; -import {App, Nav, Tabs, Tab, NavOptions, Config, ViewController, Platform} from '../../../../src'; +import { Component } from '@angular/core'; +import { App, Config, Nav, NavOptions, Platform, Tab, Tabs, ViewController } from '../../../../src'; +import { mockTab, mockTabs } from '../../../../src/util/mock-providers'; export function run() { describe('Tabs', () => { + describe('initTabs', () => { + + it('should preload all tabs', () => { + var tabs = mockTabs(); + var tab0 = mockTab(tabs); + var tab1 = mockTab(tabs); + tab0.root = SomePage; + tab1.root = SomePage; + + tab0.preload = () => {}; + tab1.preload = () => {}; + + spyOn(tab0, 'preload'); + spyOn(tab1, 'preload'); + + tabs.preloadTabs = true; + + tabs.initTabs(); + + expect(tab0.isSelected).toEqual(true); + expect(tab1.isSelected).toEqual(false); + + expect(tab0.preload).not.toHaveBeenCalled(); + expect(tab1.preload).toHaveBeenCalled(); + }); + + it('should not select a hidden or disabled tab', () => { + var tabs = mockTabs(); + var tab0 = mockTab(tabs); + var tab1 = mockTab(tabs); + tab0.root = SomePage; + tab1.root = SomePage; + + tab1.enabled = false; + tab1.show = false; + + tabs.selectedIndex = '1'; + tabs.initTabs(); + + expect(tab0.isSelected).toEqual(true); + expect(tab1.isSelected).toEqual(false); + }); + + it('should select the second tab from selectedIndex input', () => { + var tabs = mockTabs(); + var tab0 = mockTab(tabs); + var tab1 = mockTab(tabs); + tab0.root = SomePage; + tab1.root = SomePage; + + tabs.selectedIndex = '1'; + tabs.initTabs(); + + expect(tab0.isSelected).toEqual(false); + expect(tab1.isSelected).toEqual(true); + }); + + it('should select the first tab by default', () => { + var tabs = mockTabs(); + var tab0 = mockTab(tabs); + var tab1 = mockTab(tabs); + tab0.root = SomePage; + tab1.root = SomePage; + + spyOn(tab0, 'preload'); + spyOn(tab1, 'preload'); + + tabs.initTabs(); + + expect(tab0.isSelected).toEqual(true); + expect(tab1.isSelected).toEqual(false); + + expect(tab0.preload).not.toHaveBeenCalled(); + expect(tab1.preload).not.toHaveBeenCalled(); + }); + + }); + describe('previousTab', () => { it('should find the previous tab when there has been 3 selections', () => { @@ -12,9 +91,6 @@ describe('Tabs', () => { var tab0 = mockTab(tabs); var tab1 = mockTab(tabs); var tab2 = mockTab(tabs); - tabs.add(tab0); - tabs.add(tab1); - tabs.add(tab2); tab0.root = SomePage; tab1.root = SomePage; tab2.root = SomePage; @@ -36,8 +112,6 @@ describe('Tabs', () => { var tabs = mockTabs(); var tab0 = mockTab(tabs); var tab1 = mockTab(tabs); - tabs.add(tab0); - tabs.add(tab1); tab0.root = SomePage; tab1.root = SomePage; @@ -56,8 +130,6 @@ describe('Tabs', () => { var tabs = mockTabs(); var tab0 = mockTab(tabs); var tab1 = mockTab(tabs); - tabs.add(tab0); - tabs.add(tab1); tab0.root = SomePage; tab1.root = SomePage; @@ -87,8 +159,6 @@ describe('Tabs', () => { var tabs = mockTabs(); var tab0 = mockTab(tabs); var tab1 = mockTab(tabs); - tabs.add(tab0); - tabs.add(tab1); tab0.root = SomePage; tab1.root = SomePage; @@ -103,12 +173,11 @@ describe('Tabs', () => { var tabs = mockTabs(); var tab0 = mockTab(tabs); var tab1 = mockTab(tabs); - tabs.add(tab0); - tabs.add(tab1); tab0.root = SomePage; tab1.root = SomePage; + expect(tabs.length()).toEqual(2); expect(tab0.isSelected).toBeUndefined(); expect(tab1.isSelected).toBeUndefined(); @@ -118,16 +187,6 @@ describe('Tabs', () => { expect(tab1.isSelected).toEqual(false); }); - it('should not select an invalid tab index', () => { - var tabs = mockTabs(); - var tab0 = mockTab(tabs); - var tab1 = mockTab(tabs); - tabs.add(tab0); - tabs.add(tab1); - - expect(tabs.select(22)).toBeUndefined(); - }); - }); describe('getByIndex', () => { @@ -137,8 +196,6 @@ describe('Tabs', () => { var tab0 = mockTab(tabs); tab0.setRoot({}); var tab1 = mockTab(tabs); - tabs.add(tab0); - tabs.add(tab1); expect(tabs.getIndex(tab0)).toEqual(0); expect(tabs.getIndex(tab1)).toEqual(1); @@ -152,8 +209,6 @@ describe('Tabs', () => { var tabs = mockTabs(); var tab0 = mockTab(tabs); var tab1 = mockTab(tabs); - tabs.add(tab0); - tabs.add(tab1); tab1.setSelected(true); @@ -164,48 +219,15 @@ describe('Tabs', () => { var tabs = mockTabs(); var tab0 = mockTab(tabs); var tab1 = mockTab(tabs); - tabs.add(tab0); - tabs.add(tab1); expect(tabs.getSelected()).toEqual(null); }); }); - var app: App; - var config: Config; - var platform: Platform; - var _cd: any; - - function mockNav(): Nav { - return new Nav(null, null, null, config, null, null, null, null, null); - } - - function mockTabs(): Tabs { - return new Tabs(null, null, null, config, null, null, null); - } - - function mockTab(parentTabs: Tabs): Tab { - var tab = new Tab(parentTabs, app, config, null, null, null, null, null, _cd); - tab.load = function(opts: any, cb: Function) { - cb(); - }; - return tab; - } - @Component({}) class SomePage {} - beforeEach(() => { - config = new Config(); - platform = new Platform(); - app = new App(config, platform); - _cd = { - reattach: function(){}, - detach: function(){} - }; - }); - }); diff --git a/src/util/mock-providers.ts b/src/util/mock-providers.ts new file mode 100644 index 0000000000..acdb194a79 --- /dev/null +++ b/src/util/mock-providers.ts @@ -0,0 +1,171 @@ +import { ChangeDetectorRef, ElementRef, NgZone, Renderer } from '@angular/core'; +import { Location } from '@angular/common'; + +import { App, Config, Form, GestureController, Keyboard, MenuController, NavOptions, Platform, Tab, Tabs, Transition, ViewController } from '../../src'; +import { NavControllerBase } from '../../src/components/nav/nav-controller-base'; + + +export const mockConfig = function(config?: any) { + return new Config(config); +}; + +export const mockPlatform = function(platforms?: string[]) { + return new Platform(platforms); +}; + +export const mockApp = function(config?: Config, platform?: Platform) { + config = config || mockConfig(); + platform = platform || mockPlatform(); + return new App(config, platform); +}; + +export const mockZone = function(): NgZone { + let zone: any = { + run: function(cb: any) { + cb(); + }, + runOutsideAngular: function(cb: any) { + cb(); + } + }; + return zone; +}; + +export const mockChangeDetectorRef = function(): ChangeDetectorRef { + let cd: any = { + reattach: () => {}, + detach: () => {} + }; + return cd; +}; + +export const mockElementRef = function(): ElementRef { + return { + nativeElement: document.createElement('div') + }; +}; + +export const mockRenderer = function(): Renderer { + let renderer: any = { + setElementAttribute: () => {}, + setElementClass: () => {}, + setElementStyle: () => {} + }; + return renderer; +}; + +export const mockLocation = function(): Location { + let location: any = { + path: () => { return ''; }, + subscribe: () => {}, + go: () => {}, + back: () => {} + }; + return location; +}; + +export const mockTransition = function(playCallback: Function, duration: number) { + return function _createTrans(enteringView: ViewController, leavingView: ViewController, transitionOpts: any): Transition { + let transition: any = { + play: () => { + playCallback(); + }, + getDuration: () => { return duration; }, + onFinish: () => {} + }; + return transition; + }; +}; + +export const mockNavController = function(): NavControllerBase { + let platform = mockPlatform(); + + let config = mockConfig(); + config.setPlatform(platform); + + let app = mockApp(config, platform); + + let form = new Form(); + + let zone = mockZone(); + + let keyboard = new Keyboard(config, form, zone); + + let elementRef = mockElementRef(); + + let renderer = mockRenderer(); + + let compiler: any = null; + + let gestureCtrl = new GestureController(app); + + let location = mockLocation(); + + return new NavControllerBase( + null, + app, + config, + keyboard, + elementRef, + zone, + renderer, + compiler, + gestureCtrl + ); +}; + +export const mockTab = function(parentTabs: Tabs): Tab { + let platform = mockPlatform(); + + let config = mockConfig(); + config.setPlatform(platform); + + let app = (parentTabs)._app || mockApp(config, platform); + + let form = new Form(); + + let zone = mockZone(); + + let keyboard = new Keyboard(config, form, zone); + + let elementRef = mockElementRef(); + + let renderer = mockRenderer(); + + let changeDetectorRef = mockChangeDetectorRef(); + + let compiler: any = null; + + let gestureCtrl = new GestureController(app); + + let location = mockLocation(); + + let tab = new Tab( + parentTabs, + app, + config, + keyboard, + elementRef, + zone, + renderer, + compiler, + changeDetectorRef, + gestureCtrl + ); + + tab.load = (opts: any, cb: Function) => { + cb(); + }; + + return tab; +}; + +export const mockTabs = function(app?: App): Tabs { + let config = mockConfig(); + let platform = mockPlatform(); + app = app || mockApp(config, platform); + let elementRef = mockElementRef(); + let renderer = mockRenderer(); + + return new Tabs(null, null, app, config, elementRef, platform, renderer); +}; diff --git a/src/util/util.ts b/src/util/util.ts index 459b618e7c..84000a4047 100644 --- a/src/util/util.ts +++ b/src/util/util.ts @@ -1,4 +1,6 @@ +export function noop() {} + /** * Given a min and max, restrict the given number * to the range.