mirror of
https://github.com/ionic-team/ionic-framework.git
synced 2025-08-20 12:29:55 +08:00
1097 lines
35 KiB
TypeScript
1097 lines
35 KiB
TypeScript
import { ComponentRef, ComponentFactoryResolver, ElementRef, EventEmitter, NgZone, ReflectiveInjector, Renderer, ViewContainerRef } from '@angular/core';
|
|
|
|
import { AnimationOptions } from '../animations/animation';
|
|
import { App } from '../components/app/app';
|
|
import { Config } from '../config/config';
|
|
import { convertToView, convertToViews, NavOptions, DIRECTION_BACK, DIRECTION_FORWARD, INIT_ZINDEX,
|
|
TransitionResolveFn, TransitionInstruction, ViewState } from './nav-util';
|
|
import { setZIndex } from './nav-util';
|
|
import { DeepLinker } from './deep-linker';
|
|
import { DomController } from '../platform/dom-controller';
|
|
import { GestureController } from '../gestures/gesture-controller';
|
|
import { isBlank, isNumber, isPresent, assert, removeArrayItem } from '../util/util';
|
|
import { isViewController, ViewController } from './view-controller';
|
|
import { Ion } from '../components/ion';
|
|
import { Keyboard } from '../platform/keyboard';
|
|
import { NavController } from './nav-controller';
|
|
import { NavParams } from './nav-params';
|
|
import { Platform } from '../platform/platform';
|
|
import { SwipeBackGesture } from './swipe-back';
|
|
import { Transition } from '../transitions/transition';
|
|
import { TransitionController } from '../transitions/transition-controller';
|
|
|
|
/**
|
|
* @private
|
|
* This class is for internal use only. It is not exported publicly.
|
|
*/
|
|
export class NavControllerBase extends Ion implements NavController {
|
|
|
|
_children: any[] = [];
|
|
_ids: number = -1;
|
|
_init = false;
|
|
_isPortal: boolean;
|
|
_queue: TransitionInstruction[] = [];
|
|
_sbEnabled: boolean;
|
|
_sbGesture: SwipeBackGesture;
|
|
_sbTrns: Transition;
|
|
_trnsId: number = null;
|
|
_trnsTm: boolean = false;
|
|
_viewport: ViewContainerRef;
|
|
_views: ViewController[] = [];
|
|
_zIndexOffset: number = 0;
|
|
|
|
viewDidLoad: EventEmitter<any> = new EventEmitter();
|
|
viewWillEnter: EventEmitter<any> = new EventEmitter();
|
|
viewDidEnter: EventEmitter<any> = new EventEmitter();
|
|
viewWillLeave: EventEmitter<any> = new EventEmitter();
|
|
viewDidLeave: EventEmitter<any> = new EventEmitter();
|
|
viewWillUnload: EventEmitter<any> = new EventEmitter();
|
|
|
|
id: string;
|
|
|
|
constructor(
|
|
public parent: any,
|
|
public _app: App,
|
|
public config: Config,
|
|
public plt: Platform,
|
|
public _keyboard: Keyboard,
|
|
elementRef: ElementRef,
|
|
public _zone: NgZone,
|
|
renderer: Renderer,
|
|
public _cfr: ComponentFactoryResolver,
|
|
public _gestureCtrl: GestureController,
|
|
public _trnsCtrl: TransitionController,
|
|
public _linker: DeepLinker,
|
|
private _domCtrl: DomController
|
|
) {
|
|
super(config, elementRef, renderer);
|
|
|
|
this._sbEnabled = config.getBoolean('swipeBackEnabled');
|
|
|
|
this.id = 'n' + (++ctrlIds);
|
|
}
|
|
|
|
push(page: any, params?: any, opts?: NavOptions, done?: Function): Promise<any> {
|
|
return this._queueTrns({
|
|
insertStart: -1,
|
|
insertViews: [convertToView(this._linker, page, params)],
|
|
opts: opts,
|
|
}, done);
|
|
}
|
|
|
|
insert(insertIndex: number, page: any, params?: any, opts?: NavOptions, done?: Function): Promise<any> {
|
|
return this._queueTrns({
|
|
insertStart: insertIndex,
|
|
insertViews: [convertToView(this._linker, page, params)],
|
|
opts: opts,
|
|
}, done);
|
|
}
|
|
|
|
insertPages(insertIndex: number, insertPages: any[], opts?: NavOptions, done?: Function): Promise<any> {
|
|
return this._queueTrns({
|
|
insertStart: insertIndex,
|
|
insertViews: convertToViews(this._linker, insertPages),
|
|
opts: opts,
|
|
}, done);
|
|
}
|
|
|
|
pop(opts?: NavOptions, done?: Function): Promise<any> {
|
|
return this._queueTrns({
|
|
removeStart: -1,
|
|
removeCount: 1,
|
|
opts: opts,
|
|
}, done);
|
|
}
|
|
|
|
popTo(indexOrViewCtrl: any, opts?: NavOptions, done?: Function): Promise<any> {
|
|
let config: TransitionInstruction = {
|
|
removeStart: -1,
|
|
removeCount: -1,
|
|
opts: opts
|
|
};
|
|
if (isViewController(indexOrViewCtrl)) {
|
|
config.removeView = indexOrViewCtrl;
|
|
config.removeStart = 1;
|
|
} else if (isNumber(indexOrViewCtrl)) {
|
|
config.removeStart = indexOrViewCtrl + 1;
|
|
}
|
|
return this._queueTrns(config, done);
|
|
}
|
|
|
|
popToRoot(opts?: NavOptions, done?: Function): Promise<any> {
|
|
return this._queueTrns({
|
|
removeStart: 1,
|
|
removeCount: -1,
|
|
opts: opts,
|
|
}, done);
|
|
}
|
|
|
|
popAll(): Promise<any[]> {
|
|
let promises: any[] = [];
|
|
for (var i = this._views.length - 1; i >= 0; i--) {
|
|
promises.push(this.pop(null));
|
|
}
|
|
return Promise.all(promises);
|
|
}
|
|
|
|
remove(startIndex: number, removeCount: number = 1, opts?: NavOptions, done?: Function): Promise<any> {
|
|
return this._queueTrns({
|
|
removeStart: startIndex,
|
|
removeCount: removeCount,
|
|
opts: opts,
|
|
}, done);
|
|
}
|
|
|
|
removeView(viewController: ViewController, opts?: NavOptions, done?: Function): Promise<any> {
|
|
return this._queueTrns({
|
|
removeView: viewController,
|
|
removeStart: 0,
|
|
removeCount: 1,
|
|
opts: opts,
|
|
}, done);
|
|
}
|
|
|
|
setRoot(pageOrViewCtrl: any, params?: any, opts?: NavOptions, done?: Function): Promise<any> {
|
|
const viewControllers = [convertToView(this._linker, pageOrViewCtrl, params)];
|
|
return this._setPages(viewControllers, opts, done);
|
|
}
|
|
|
|
setPages(pages: any[], opts?: NavOptions, done?: Function): Promise<any> {
|
|
const viewControllers = convertToViews(this._linker, pages);
|
|
return this._setPages(viewControllers, opts, done);
|
|
}
|
|
|
|
_setPages(viewControllers: ViewController[], opts?: NavOptions, done?: Function): Promise<any> {
|
|
if (isBlank(opts)) {
|
|
opts = {};
|
|
}
|
|
// if animation wasn't set to true then default it to NOT animate
|
|
if (opts.animate !== true) {
|
|
opts.animate = false;
|
|
}
|
|
return this._queueTrns({
|
|
insertStart: 0,
|
|
insertViews: viewControllers,
|
|
removeStart: 0,
|
|
removeCount: -1,
|
|
opts: opts
|
|
}, done);
|
|
}
|
|
|
|
// _queueTrns() adds a navigation stack change to the queue and schedules it to run:
|
|
// 1. _nextTrns(): consumes the next transition in the queue
|
|
// 2. _viewInit(): initializes enteringView if required
|
|
// 3. _viewTest(): ensures canLeave/canEnter returns true, so the operation can continue
|
|
// 4. _postViewInit(): add/remove the views from the navigation stack
|
|
// 5. _transitionInit(): initializes the visual transition if required and schedules it to run
|
|
// 6. _viewAttachToDOM(): attaches the enteringView to the DOM
|
|
// 7. _transitionStart(): called once the transition actually starts, it initializes the Animation underneath.
|
|
// 8. _transitionFinish(): called once the transition finishes
|
|
// 9. _cleanup(): syncs the navigation internal state with the DOM. For example it removes the pages from the DOM or hides/show them.
|
|
_queueTrns(ti: TransitionInstruction, done: Function): Promise<any> {
|
|
let promise: Promise<any>;
|
|
let resolve: Function = done;
|
|
let reject: Function = done;
|
|
|
|
if (done === undefined) {
|
|
// only create a promise if a done callback wasn't provided
|
|
// done can be a null, which avoids any functions
|
|
promise = new Promise((res, rej) => {
|
|
resolve = res;
|
|
reject = rej;
|
|
});
|
|
}
|
|
|
|
// ti.resolve() is called when the navigation transition is finished successfully
|
|
ti.resolve = (hasCompleted: boolean, isAsync: boolean, enteringName: string, leavingName: string, direction: string) => {
|
|
this._trnsId = null;
|
|
this._init = true;
|
|
resolve && resolve(hasCompleted, isAsync, enteringName, leavingName, direction);
|
|
|
|
// let's see if there's another to kick off
|
|
this.setTransitioning(false);
|
|
this._swipeBackCheck();
|
|
this._nextTrns();
|
|
};
|
|
|
|
// ti.reject() is called when the navigation transition fails. ie. it is rejected at some point.
|
|
ti.reject = (rejectReason: any, transition: Transition) => {
|
|
this._trnsId = null;
|
|
this._queue.length = 0;
|
|
|
|
// walk through the transition views so they are destroyed
|
|
while (transition) {
|
|
var enteringView = transition.enteringView;
|
|
if (enteringView && (enteringView._state === ViewState.ATTACHED)) {
|
|
this._destroyView(enteringView);
|
|
}
|
|
if (transition.isRoot()) {
|
|
this._trnsCtrl.destroy(transition.trnsId);
|
|
break;
|
|
}
|
|
transition = transition.parent;
|
|
}
|
|
|
|
reject && reject(false, false, rejectReason);
|
|
|
|
// let's see if there's another to kick off
|
|
this.setTransitioning(false);
|
|
this._swipeBackCheck();
|
|
this._nextTrns();
|
|
};
|
|
|
|
if (ti.insertViews) {
|
|
// ensure we've got good views to insert
|
|
ti.insertViews = ti.insertViews.filter(v => v !== null);
|
|
if (ti.insertViews.length === 0) {
|
|
ti.reject('invalid views to insert');
|
|
return promise;
|
|
}
|
|
|
|
} else if (isPresent(ti.removeStart) && this._views.length === 0 && !this._isPortal) {
|
|
ti.reject('no views in the stack to be removed');
|
|
return promise;
|
|
}
|
|
|
|
this._queue.push(ti);
|
|
|
|
// if there isn't a transition already happening
|
|
// then this will kick off this transition
|
|
this._nextTrns();
|
|
|
|
// promise is undefined if a done callbacks was provided
|
|
return promise;
|
|
}
|
|
|
|
_nextTrns(): boolean {
|
|
// this is the framework's bread 'n butta function
|
|
// only one transition is allowed at any given time
|
|
if (this.isTransitioning()) {
|
|
return false;
|
|
}
|
|
|
|
// there is no transition happening right now
|
|
// get the next instruction
|
|
const ti = this._nextTI();
|
|
if (!ti) {
|
|
return false;
|
|
}
|
|
|
|
// ensure any of the inserted view are used
|
|
const insertViews = ti.insertViews;
|
|
if (insertViews) {
|
|
for (var i = 0; i < insertViews.length; i++) {
|
|
var nav = insertViews[i]._nav;
|
|
if (nav && nav !== this || insertViews[i]._state === ViewState.DESTROYED) {
|
|
ti.reject('leavingView and enteringView are null. stack is already empty');
|
|
return false;
|
|
}
|
|
}
|
|
}
|
|
|
|
// get entering and leaving views
|
|
const leavingView = this.getActive();
|
|
const enteringView = this._getEnteringView(ti, leavingView);
|
|
|
|
if (!leavingView && !enteringView) {
|
|
ti.reject('leavingView and enteringView are null. stack is already empty');
|
|
return false;
|
|
}
|
|
|
|
// set that this nav is actively transitioning
|
|
this.setTransitioning(true);
|
|
|
|
// Initialize enteringView
|
|
if (enteringView && enteringView._state === ViewState.NEW) {
|
|
// render the entering view, and all child navs and views
|
|
// ******** DOM WRITE ****************
|
|
this._viewInit(enteringView);
|
|
}
|
|
|
|
// Only test canLeave/canEnter if there is transition
|
|
const requiresTransition = ti.requiresTransition = (ti.enteringRequiresTransition || ti.leavingRequiresTransition) && enteringView !== leavingView;
|
|
if (requiresTransition) {
|
|
// views have been initialized, now let's test
|
|
// to see if the transition is even allowed or not
|
|
return this._viewTest(enteringView, leavingView, ti);
|
|
} else {
|
|
return this._postViewInit(enteringView, leavingView, ti);
|
|
}
|
|
}
|
|
|
|
_nextTI(): TransitionInstruction {
|
|
const ti = this._queue.shift();
|
|
if (!ti) {
|
|
return null;
|
|
}
|
|
const viewsLength = this._views.length;
|
|
|
|
if (isPresent(ti.removeView)) {
|
|
assert(isPresent(ti.removeStart), 'removeView needs removeStart');
|
|
assert(isPresent(ti.removeCount), 'removeView needs removeCount');
|
|
|
|
var index = this._views.indexOf(ti.removeView);
|
|
if (index >= 0) {
|
|
ti.removeStart += index;
|
|
}
|
|
}
|
|
if (isPresent(ti.removeStart)) {
|
|
if (ti.removeStart < 0) {
|
|
ti.removeStart = (viewsLength - 1);
|
|
}
|
|
if (ti.removeCount < 0) {
|
|
ti.removeCount = (viewsLength - ti.removeStart);
|
|
}
|
|
ti.leavingRequiresTransition = ((ti.removeStart + ti.removeCount) === viewsLength);
|
|
}
|
|
|
|
if (ti.insertViews) {
|
|
// 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 (ti.insertStart < 0 || ti.insertStart > viewsLength) {
|
|
ti.insertStart = viewsLength;
|
|
}
|
|
ti.enteringRequiresTransition = (ti.insertStart === viewsLength);
|
|
}
|
|
return ti;
|
|
}
|
|
|
|
_getEnteringView(ti: TransitionInstruction, leavingView: ViewController): ViewController {
|
|
const insertViews = ti.insertViews;
|
|
if (insertViews) {
|
|
// grab the very last view of the views to be inserted
|
|
// and initialize it as the new entering view
|
|
return insertViews[insertViews.length - 1];
|
|
}
|
|
|
|
const removeStart = ti.removeStart;
|
|
if (isPresent(removeStart)) {
|
|
var views = this._views;
|
|
var removeEnd = removeStart + ti.removeCount;
|
|
var i: number;
|
|
var view: ViewController;
|
|
for (i = views.length - 1; i >= 0; i--) {
|
|
view = views[i];
|
|
if ((i < removeStart || i >= removeEnd) && view !== leavingView) {
|
|
return view;
|
|
}
|
|
}
|
|
}
|
|
return null;
|
|
}
|
|
|
|
_postViewInit(enteringView: ViewController, leavingView: ViewController, ti: TransitionInstruction) {
|
|
assert(leavingView || enteringView, 'Both leavingView and enteringView are null');
|
|
assert(ti.resolve, 'resolve must be valid');
|
|
assert(ti.reject, 'reject must be valid');
|
|
|
|
const opts = ti.opts || {};
|
|
const insertViews = ti.insertViews;
|
|
const removeStart = ti.removeStart;
|
|
const removeCount = ti.removeCount;
|
|
let view: ViewController;
|
|
let i: number;
|
|
let destroyQueue: ViewController[];
|
|
|
|
// there are views to remove
|
|
if (isPresent(removeStart)) {
|
|
assert(removeStart >= 0, 'removeStart can not be negative');
|
|
assert(removeCount >= 0, 'removeCount can not be negative');
|
|
|
|
destroyQueue = [];
|
|
for (i = 0; i < removeCount; i++) {
|
|
view = this._views[i + removeStart];
|
|
if (view && view !== enteringView && view !== leavingView) {
|
|
destroyQueue.push(view);
|
|
}
|
|
}
|
|
// default the direction to "back"
|
|
opts.direction = opts.direction || DIRECTION_BACK;
|
|
}
|
|
|
|
const finalBalance = this._views.length + (insertViews ? insertViews.length : 0) - (removeCount ? removeCount : 0);
|
|
assert(finalBalance >= 0, 'final balance can not be negative');
|
|
if (finalBalance === 0 && !this._isPortal) {
|
|
console.warn(`You can't remove all the pages in the navigation stack. nav.pop() is probably called too many times.`,
|
|
this, this.getNativeElement());
|
|
|
|
ti.reject('navigation stack needs at least one root page');
|
|
return false;
|
|
}
|
|
|
|
// there are views to insert
|
|
if (insertViews) {
|
|
// manually set the new view's id if an id was passed in the options
|
|
if (isPresent(opts.id)) {
|
|
enteringView.id = opts.id;
|
|
}
|
|
|
|
// add the views to the
|
|
for (i = 0; i < insertViews.length; i++) {
|
|
view = insertViews[i];
|
|
this._insertViewAt(view, ti.insertStart + i);
|
|
}
|
|
|
|
if (ti.enteringRequiresTransition) {
|
|
// default to forward if not already set
|
|
opts.direction = opts.direction || DIRECTION_FORWARD;
|
|
}
|
|
}
|
|
|
|
// if the views to be removed are in the beginning or middle
|
|
// and there is not a view that needs to visually transition out
|
|
// then just destroy them and don't transition anything
|
|
// batch all of lifecycles together
|
|
// let's make sure, callbacks are zoned
|
|
if (destroyQueue && destroyQueue.length > 0) {
|
|
this._zone.run(() => {
|
|
for (i = 0; i < destroyQueue.length; i++) {
|
|
view = destroyQueue[i];
|
|
this._willLeave(view, true);
|
|
this._didLeave(view);
|
|
this._willUnload(view);
|
|
}
|
|
});
|
|
|
|
// once all lifecycle events has been delivered, we can safely detroy the views
|
|
for (i = 0; i < destroyQueue.length; i++) {
|
|
this._destroyView(destroyQueue[i]);
|
|
}
|
|
}
|
|
|
|
if (!ti.requiresTransition) {
|
|
// transition is not required, so we are already done!
|
|
// they're inserting/removing the views somewhere in the middle or
|
|
// beginning, so visually nothing needs to animate/transition
|
|
// resolve immediately because there's no animation that's happening
|
|
ti.resolve(true, false);
|
|
return true;
|
|
}
|
|
|
|
// set which animation it should use if it wasn't set yet
|
|
if (!opts.animation) {
|
|
if (isPresent(ti.removeStart)) {
|
|
opts.animation = (leavingView || enteringView).getTransitionName(opts.direction);
|
|
} else {
|
|
opts.animation = (enteringView || leavingView).getTransitionName(opts.direction);
|
|
}
|
|
}
|
|
|
|
// huzzah! let us transition these views
|
|
this._transitionInit(enteringView, leavingView, opts, ti.resolve);
|
|
return true;
|
|
}
|
|
|
|
/**
|
|
* DOM WRITE
|
|
*/
|
|
_viewInit(enteringView: ViewController) {
|
|
assert(enteringView, 'enteringView must be non null');
|
|
assert(enteringView._state === ViewState.NEW, 'enteringView state must be NEW');
|
|
|
|
// entering view has not been initialized yet
|
|
const componentProviders = ReflectiveInjector.resolve([
|
|
{ provide: NavController, useValue: this },
|
|
{ provide: ViewController, useValue: enteringView },
|
|
{ provide: NavParams, useValue: enteringView.getNavParams() }
|
|
]);
|
|
const componentFactory = this._cfr.resolveComponentFactory(enteringView.component);
|
|
const childInjector = ReflectiveInjector.fromResolvedProviders(componentProviders, this._viewport.parentInjector);
|
|
|
|
// create ComponentRef and set it to the entering view
|
|
enteringView.init(componentFactory.create(childInjector, []));
|
|
enteringView._state = ViewState.INITIALIZED;
|
|
this._preLoad(enteringView);
|
|
}
|
|
|
|
_viewAttachToDOM(view: ViewController, componentRef: ComponentRef<any>, viewport: ViewContainerRef) {
|
|
assert(view._state === ViewState.INITIALIZED, 'view state must be INITIALIZED');
|
|
|
|
// fire willLoad before change detection runs
|
|
this._willLoad(view);
|
|
|
|
// render the component ref instance to the DOM
|
|
// ******** DOM WRITE ****************
|
|
viewport.insert(componentRef.hostView, viewport.length);
|
|
view._state = ViewState.ATTACHED;
|
|
|
|
if (view._cssClass) {
|
|
// the ElementRef of the actual ion-page created
|
|
var pageElement = componentRef.location.nativeElement;
|
|
|
|
// ******** DOM WRITE ****************
|
|
this._renderer.setElementClass(pageElement, view._cssClass, true);
|
|
}
|
|
|
|
componentRef.changeDetectorRef.detectChanges();
|
|
|
|
// successfully finished loading the entering view
|
|
// fire off the "didLoad" lifecycle events
|
|
this._zone.run(this._didLoad.bind(this, view));
|
|
}
|
|
|
|
_viewTest(enteringView: ViewController, leavingView: ViewController, ti: TransitionInstruction): boolean {
|
|
const promises: Promise<any>[] = [];
|
|
|
|
if (leavingView) {
|
|
var leavingTestResult = leavingView._lifecycleTest('Leave');
|
|
|
|
if (leavingTestResult === false) {
|
|
// synchronous reject
|
|
ti.reject((leavingTestResult !== false ? leavingTestResult : `ionViewCanLeave rejected`));
|
|
return false;
|
|
} else if (leavingTestResult instanceof Promise) {
|
|
// async promise
|
|
promises.push(leavingTestResult);
|
|
}
|
|
}
|
|
|
|
if (enteringView) {
|
|
var enteringTestResult = enteringView._lifecycleTest('Enter');
|
|
|
|
if (enteringTestResult === false) {
|
|
// synchronous reject
|
|
ti.reject((enteringTestResult !== false ? enteringTestResult : `ionViewCanEnter rejected`));
|
|
return false;
|
|
} else if (enteringTestResult instanceof Promise) {
|
|
// async promise
|
|
promises.push(enteringTestResult);
|
|
}
|
|
}
|
|
|
|
if (promises.length) {
|
|
// darn, async promises, gotta wait for them to resolve
|
|
Promise.all(promises).then((values: any[]) => {
|
|
if (values.some(result => result === false)) {
|
|
ti.reject(`ionViewCanEnter rejected`);
|
|
} else {
|
|
this._postViewInit(enteringView, leavingView, ti);
|
|
}
|
|
}).catch(ti.reject);
|
|
return true;
|
|
} else {
|
|
// synchronous and all tests passed! let's move on already
|
|
return this._postViewInit(enteringView, leavingView, ti);
|
|
}
|
|
}
|
|
|
|
_transitionInit(enteringView: ViewController, leavingView: ViewController, opts: NavOptions, resolve: TransitionResolveFn) {
|
|
// figure out if this transition is the root one or a
|
|
// child of a parent nav that has the root transition
|
|
this._trnsId = this._trnsCtrl.getRootTrnsId(this);
|
|
if (this._trnsId === null) {
|
|
// this is the root transition, meaning all child navs and their views
|
|
// should be added as a child transition to this one
|
|
this._trnsId = this._trnsCtrl.nextId();
|
|
}
|
|
|
|
// create the transition options
|
|
const animationOpts: AnimationOptions = {
|
|
animation: opts.animation,
|
|
direction: opts.direction,
|
|
duration: (opts.animate === false ? 0 : opts.duration),
|
|
easing: opts.easing,
|
|
isRTL: this._config.plt.isRTL(),
|
|
ev: opts.ev,
|
|
};
|
|
|
|
// create the transition animation from the TransitionController
|
|
// this will either create the root transition, or add it as a child transition
|
|
const transition = this._trnsCtrl.get(this._trnsId, enteringView, leavingView, animationOpts);
|
|
|
|
// ensure any swipeback transitions are cleared out
|
|
this._sbTrns && this._sbTrns.destroy();
|
|
this._sbTrns = null;
|
|
|
|
// swipe to go back root transition
|
|
if (transition.isRoot() && opts.progressAnimation) {
|
|
this._sbTrns = transition;
|
|
}
|
|
|
|
// transition start has to be registered before attaching the view to the DOM!
|
|
transition.registerStart(() => {
|
|
this._transitionStart(transition, enteringView, leavingView, opts, resolve);
|
|
if (transition.parent) {
|
|
transition.parent.start();
|
|
}
|
|
});
|
|
|
|
if (enteringView && (enteringView._state === ViewState.INITIALIZED)) {
|
|
// render the entering component in the DOM
|
|
// this would also render new child navs/views
|
|
// which may have their very own async canEnter/Leave tests
|
|
// ******** DOM WRITE ****************
|
|
this._viewAttachToDOM(enteringView, enteringView._cmp, this._viewport);
|
|
}
|
|
|
|
if (!transition.hasChildren) {
|
|
// lowest level transition, so kick it off and let it bubble up to start all of them
|
|
transition.start();
|
|
}
|
|
}
|
|
|
|
_transitionStart(transition: Transition, enteringView: ViewController, leavingView: ViewController, opts: NavOptions, resolve: TransitionResolveFn) {
|
|
assert(this.isTransitioning(), 'isTransitioning() has to be true');
|
|
|
|
this._trnsId = null;
|
|
|
|
// set the correct zIndex for the entering and leaving views
|
|
// ******** DOM WRITE ****************
|
|
setZIndex(this, enteringView, leavingView, opts.direction, this._renderer);
|
|
|
|
// always ensure the entering view is viewable
|
|
// ******** DOM WRITE ****************
|
|
enteringView && enteringView._domShow(true, this._renderer);
|
|
|
|
// always ensure the leaving view is viewable
|
|
// ******** DOM WRITE ****************
|
|
leavingView && leavingView._domShow(true, this._renderer);
|
|
|
|
// initialize the transition
|
|
transition.init();
|
|
|
|
// we should animate (duration > 0) if the pushed page is not the first one (startup)
|
|
// or if it is a portal (modal, actionsheet, etc.)
|
|
const isFirstPage = !this._init && this._views.length === 1;
|
|
const shouldNotAnimate = isFirstPage && !this._isPortal;
|
|
const canNotAnimate = this._config.get('animate') === false;
|
|
if (shouldNotAnimate || canNotAnimate) {
|
|
opts.animate = false;
|
|
}
|
|
|
|
if (opts.animate === false) {
|
|
// if it was somehow set to not animation, then make the duration zero
|
|
transition.duration(0);
|
|
}
|
|
|
|
// create a callback that needs to run within zone
|
|
// that will fire off the willEnter/Leave lifecycle events at the right time
|
|
transition.beforeAddRead(this._viewsWillLifecycles.bind(this, enteringView, leavingView));
|
|
|
|
// create a callback for when the animation is done
|
|
transition.onFinish(() => {
|
|
// transition animation has ended
|
|
this._zone.run(this._transitionFinish.bind(this, transition, opts, resolve));
|
|
});
|
|
|
|
// get the set duration of this transition
|
|
const duration = transition.getDuration();
|
|
|
|
if (transition.isRoot()) {
|
|
// this is the top most, or only active transition, so disable the app
|
|
// add XXms to the duration the app is disabled when the keyboard is open
|
|
|
|
if (duration > DISABLE_APP_MINIMUM_DURATION && opts.disableApp !== false) {
|
|
// if this transition has a duration and this is the root transition
|
|
// then set that the app is actively disabled
|
|
this._app.setEnabled(false, duration + ACTIVE_TRANSITION_OFFSET, opts.minClickBlockDuration);
|
|
} else {
|
|
console.debug('transition is running but app has not been disabled');
|
|
}
|
|
|
|
// 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
|
|
transition.progressStart();
|
|
|
|
} else {
|
|
// only the top level transition should actually start "play"
|
|
// kick it off and let it play through
|
|
// ******** DOM WRITE ****************
|
|
transition.play();
|
|
}
|
|
}
|
|
}
|
|
|
|
_transitionFinish(transition: Transition, opts: NavOptions, resolve: TransitionResolveFn) {
|
|
const hasCompleted = transition.hasCompleted;
|
|
const enteringView = transition.enteringView;
|
|
const leavingView = transition.leavingView;
|
|
|
|
// mainly for testing
|
|
let enteringName: string;
|
|
let leavingName: string;
|
|
|
|
if (hasCompleted) {
|
|
// transition has completed (went from 0 to 1)
|
|
if (enteringView) {
|
|
enteringName = enteringView.name;
|
|
this._didEnter(enteringView);
|
|
}
|
|
|
|
if (leavingView) {
|
|
leavingName = leavingView.name;
|
|
this._didLeave(leavingView);
|
|
}
|
|
|
|
this._cleanup(enteringView);
|
|
} else {
|
|
// If transition does not complete, we have to cleanup anyway, because
|
|
// previous pages in the stack are not hidden probably.
|
|
this._cleanup(leavingView);
|
|
}
|
|
|
|
if (transition.isRoot()) {
|
|
// this is the root transition
|
|
// it's safe to destroy this transition
|
|
this._trnsCtrl.destroy(transition.trnsId);
|
|
|
|
// it's safe to enable the app again
|
|
this._app.setEnabled(true);
|
|
|
|
if (opts.updateUrl !== false) {
|
|
// notify deep linker of the nav change
|
|
// if a direction was provided and should update url
|
|
this._linker.navChange(opts.direction);
|
|
}
|
|
|
|
if (opts.keyboardClose !== false) {
|
|
// the keyboard is still open!
|
|
// no problem, let's just close for them
|
|
this._keyboard.close();
|
|
}
|
|
}
|
|
|
|
// congrats, we did it!
|
|
resolve(hasCompleted, true, enteringName, leavingName, opts.direction);
|
|
}
|
|
|
|
_viewsWillLifecycles(enteringView: ViewController, leavingView: ViewController) {
|
|
if (enteringView || leavingView) {
|
|
this._zone.run(() => {
|
|
// Here, the order is important. WillLeave must be called before WillEnter.
|
|
leavingView && this._willLeave(leavingView, !enteringView);
|
|
enteringView && this._willEnter(enteringView);
|
|
});
|
|
}
|
|
}
|
|
|
|
_insertViewAt(view: ViewController, index: number) {
|
|
const existingIndex = this._views.indexOf(view);
|
|
if (existingIndex > -1) {
|
|
// this view is already in the stack!!
|
|
// move it to its new location
|
|
assert(view._nav === this, 'view is not part of the nav');
|
|
this._views.splice(index, 0, this._views.splice(existingIndex, 1)[0]);
|
|
} else {
|
|
assert(!view._nav || (this._isPortal && view._nav === this), 'nav is used');
|
|
// this is a new view to add to the stack
|
|
// create the new entering view
|
|
view._setNav(this);
|
|
|
|
// give this inserted view an ID
|
|
this._ids++;
|
|
if (!view.id) {
|
|
view.id = `${this.id}-${this._ids}`;
|
|
}
|
|
|
|
// insert the entering view into the correct index in the stack
|
|
this._views.splice(index, 0, view);
|
|
}
|
|
}
|
|
|
|
_removeView(view: ViewController) {
|
|
assert(view._state === ViewState.ATTACHED || view._state === ViewState.DESTROYED, 'view state should be loaded or destroyed');
|
|
|
|
const views = this._views;
|
|
const index = views.indexOf(view);
|
|
assert(index > -1, 'view must be part of the stack');
|
|
if (index >= 0) {
|
|
views.splice(index, 1);
|
|
}
|
|
}
|
|
|
|
_destroyView(view: ViewController) {
|
|
view._destroy(this._renderer);
|
|
this._removeView(view);
|
|
}
|
|
|
|
/**
|
|
* DOM WRITE
|
|
*/
|
|
_cleanup(activeView: ViewController) {
|
|
// ok, cleanup time!! Destroy all of the views that are
|
|
// INACTIVE and come after the active view
|
|
const activeViewIndex = this._views.indexOf(activeView);
|
|
const views = this._views;
|
|
let reorderZIndexes = false;
|
|
let view: ViewController;
|
|
let i: number;
|
|
|
|
for (i = views.length - 1; i >= 0; i--) {
|
|
view = views[i];
|
|
if (i > activeViewIndex) {
|
|
// this view comes after the active view
|
|
// let's unload it
|
|
this._willUnload(view);
|
|
this._destroyView(view);
|
|
|
|
} else if (i < activeViewIndex && !this._isPortal) {
|
|
// this view comes before the active view
|
|
// and it is not a portal then ensure it is hidden
|
|
view._domShow(false, this._renderer);
|
|
}
|
|
if (view._zIndex <= 0) {
|
|
reorderZIndexes = true;
|
|
}
|
|
}
|
|
|
|
if (!this._isPortal && reorderZIndexes) {
|
|
for (i = 0; i < views.length; i++) {
|
|
view = views[i];
|
|
// ******** DOM WRITE ****************
|
|
view._setZIndex(view._zIndex + INIT_ZINDEX + 1, this._renderer);
|
|
}
|
|
}
|
|
}
|
|
|
|
_preLoad(view: ViewController) {
|
|
assert(this.isTransitioning(), 'nav controller should be transitioning');
|
|
|
|
view._preLoad();
|
|
}
|
|
|
|
_willLoad(view: ViewController) {
|
|
assert(this.isTransitioning(), 'nav controller should be transitioning');
|
|
|
|
view._willLoad();
|
|
}
|
|
|
|
_didLoad(view: ViewController) {
|
|
assert(this.isTransitioning(), 'nav controller should be transitioning');
|
|
assert(NgZone.isInAngularZone(), 'callback should be zoned');
|
|
|
|
view._didLoad();
|
|
this.viewDidLoad.emit(view);
|
|
this._app.viewDidLoad.emit(view);
|
|
}
|
|
|
|
_willEnter(view: ViewController) {
|
|
assert(this.isTransitioning(), 'nav controller should be transitioning');
|
|
assert(NgZone.isInAngularZone(), 'callback should be zoned');
|
|
|
|
view._willEnter();
|
|
this.viewWillEnter.emit(view);
|
|
this._app.viewWillEnter.emit(view);
|
|
}
|
|
|
|
_didEnter(view: ViewController) {
|
|
assert(this.isTransitioning(), 'nav controller should be transitioning');
|
|
assert(NgZone.isInAngularZone(), 'callback should be zoned');
|
|
|
|
view._didEnter();
|
|
this.viewDidEnter.emit(view);
|
|
this._app.viewDidEnter.emit(view);
|
|
}
|
|
|
|
_willLeave(view: ViewController, willUnload: boolean) {
|
|
assert(this.isTransitioning(), 'nav controller should be transitioning');
|
|
assert(NgZone.isInAngularZone(), 'callback should be zoned');
|
|
|
|
view._willLeave(willUnload);
|
|
this.viewWillLeave.emit(view);
|
|
this._app.viewWillLeave.emit(view);
|
|
}
|
|
|
|
_didLeave(view: ViewController) {
|
|
assert(this.isTransitioning(), 'nav controller should be transitioning');
|
|
assert(NgZone.isInAngularZone(), 'callback should be zoned');
|
|
|
|
view._didLeave();
|
|
this.viewDidLeave.emit(view);
|
|
this._app.viewDidLeave.emit(view);
|
|
}
|
|
|
|
_willUnload(view: ViewController) {
|
|
assert(this.isTransitioning(), 'nav controller should be transitioning');
|
|
assert(NgZone.isInAngularZone(), 'callback should be zoned');
|
|
|
|
view._willUnload();
|
|
this.viewWillUnload.emit(view);
|
|
this._app.viewWillUnload.emit(view);
|
|
}
|
|
|
|
getActiveChildNav(): any {
|
|
return this._children[this._children.length - 1];
|
|
}
|
|
|
|
registerChildNav(nav: any) {
|
|
this._children.push(nav);
|
|
}
|
|
|
|
unregisterChildNav(nav: any) {
|
|
removeArrayItem(this._children, nav);
|
|
}
|
|
|
|
destroy() {
|
|
const views = this._views;
|
|
let view: ViewController;
|
|
for (var i = 0; i < views.length; i++) {
|
|
view = views[i];
|
|
view._willUnload();
|
|
view._destroy(this._renderer);
|
|
}
|
|
|
|
// release swipe back gesture and transition
|
|
this._sbGesture && this._sbGesture.destroy();
|
|
this._sbTrns && this._sbTrns.destroy();
|
|
this._queue = this._views = this._sbGesture = this._sbTrns = null;
|
|
|
|
// Unregister navcontroller
|
|
if (this.parent && this.parent.unregisterChildNav) {
|
|
this.parent.unregisterChildNav(this);
|
|
}
|
|
}
|
|
|
|
swipeBackStart() {
|
|
if (this.isTransitioning() || this._queue.length > 0) {
|
|
return;
|
|
}
|
|
|
|
// default the direction to "back";
|
|
const opts: NavOptions = {
|
|
direction: DIRECTION_BACK,
|
|
progressAnimation: true
|
|
};
|
|
|
|
this._queueTrns({
|
|
removeStart: -1,
|
|
removeCount: 1,
|
|
opts: opts,
|
|
}, null);
|
|
}
|
|
|
|
swipeBackProgress(stepValue: number) {
|
|
if (this._sbTrns && this._sbGesture) {
|
|
// continue to disable the app while actively dragging
|
|
this._app.setEnabled(false, ACTIVE_TRANSITION_DEFAULT);
|
|
this.setTransitioning(true);
|
|
|
|
// set the transition animation's progress
|
|
this._sbTrns.progressStep(stepValue);
|
|
}
|
|
}
|
|
|
|
swipeBackEnd(shouldComplete: boolean, currentStepValue: number, velocity: number) {
|
|
if (this._sbTrns && this._sbGesture) {
|
|
// the swipe back gesture has ended
|
|
var dur = this._sbTrns.getDuration() / (Math.abs(velocity) + 1);
|
|
this._sbTrns.progressEnd(shouldComplete, currentStepValue, dur);
|
|
}
|
|
}
|
|
|
|
_swipeBackCheck() {
|
|
if (this.canSwipeBack()) {
|
|
if (!this._sbGesture) {
|
|
this._sbGesture = new SwipeBackGesture(this.plt, this, this._gestureCtrl, this._domCtrl);
|
|
}
|
|
this._sbGesture.listen();
|
|
|
|
} else if (this._sbGesture) {
|
|
this._sbGesture.unlisten();
|
|
}
|
|
}
|
|
|
|
canSwipeBack(): boolean {
|
|
return (this._sbEnabled &&
|
|
!this._isPortal &&
|
|
this._children.length <= 1 &&
|
|
!this.isTransitioning() &&
|
|
this._app.isEnabled() &&
|
|
this.canGoBack());
|
|
}
|
|
|
|
canGoBack(): boolean {
|
|
const activeView = this.getActive();
|
|
return !!(activeView && activeView.enableBack());
|
|
}
|
|
|
|
isTransitioning(): boolean {
|
|
return this._trnsTm;
|
|
}
|
|
|
|
setTransitioning(isTransitioning: boolean) {
|
|
this._trnsTm = isTransitioning;
|
|
}
|
|
|
|
getActive(): ViewController {
|
|
return this._views[this._views.length - 1];
|
|
}
|
|
|
|
isActive(view: ViewController): boolean {
|
|
return (view === this.getActive());
|
|
}
|
|
|
|
getByIndex(index: number): ViewController {
|
|
return this._views[index];
|
|
}
|
|
|
|
getPrevious(view?: ViewController): ViewController {
|
|
// returns the view controller which is before the given view controller.
|
|
if (!view) {
|
|
view = this.getActive();
|
|
}
|
|
const views = this._views;
|
|
return views[views.indexOf(view) - 1];
|
|
}
|
|
|
|
first(): ViewController {
|
|
// returns the first view controller in this nav controller's stack.
|
|
return this._views[0];
|
|
}
|
|
|
|
last(): ViewController {
|
|
// returns the last page in this nav controller's stack.
|
|
return this._views[this._views.length - 1];
|
|
}
|
|
|
|
indexOf(view: ViewController): number {
|
|
// returns the index number of the given view controller.
|
|
return this._views.indexOf(view);
|
|
}
|
|
|
|
length(): number {
|
|
return this._views.length;
|
|
}
|
|
|
|
/**
|
|
* Return the stack of views in this NavController.
|
|
*/
|
|
getViews(): Array<ViewController> {
|
|
return this._views;
|
|
}
|
|
|
|
isSwipeBackEnabled(): boolean {
|
|
return this._sbEnabled;
|
|
}
|
|
|
|
dismissPageChangeViews() {
|
|
for (let view of this._views) {
|
|
if (view.data && view.data.dismissOnPageChange) {
|
|
view.dismiss().catch(null);
|
|
}
|
|
}
|
|
}
|
|
|
|
setViewport(val: ViewContainerRef) {
|
|
this._viewport = val;
|
|
}
|
|
|
|
resize() {
|
|
const active = this.getActive();
|
|
if (!active) {
|
|
return;
|
|
}
|
|
const content = active.getIONContent();
|
|
content && content.resize();
|
|
}
|
|
|
|
}
|
|
|
|
let ctrlIds = -1;
|
|
|
|
const DISABLE_APP_MINIMUM_DURATION = 64;
|
|
const ACTIVE_TRANSITION_DEFAULT = 5000;
|
|
const ACTIVE_TRANSITION_OFFSET = 2000;
|