mirror of
https://github.com/ionic-team/ionic-framework.git
synced 2025-08-20 20:33:32 +08:00
792 lines
25 KiB
TypeScript
792 lines
25 KiB
TypeScript
import { Animation, AnimationOptions, Config, FrameworkDelegate, Nav, NavOptions, Transition} from '../index';
|
|
import {
|
|
ComponentDataPair,
|
|
NavResult,
|
|
TransitionInstruction,
|
|
} from './nav-interfaces';
|
|
|
|
import {
|
|
DIRECTION_BACK,
|
|
DIRECTION_FORWARD,
|
|
STATE_ATTACHED,
|
|
STATE_DESTROYED,
|
|
STATE_NEW,
|
|
VIEW_ID_START,
|
|
destroyTransition,
|
|
getHydratedTransition,
|
|
getNextTransitionId,
|
|
getParentTransitionId,
|
|
isViewController,
|
|
resolveRoute,
|
|
setZIndex,
|
|
toggleHidden,
|
|
transitionFactory,
|
|
} from './nav-utils';
|
|
|
|
|
|
import { ViewController } from './view-controller';
|
|
|
|
import { assert, focusOutActiveElement, isDef, isNumber } from '../utils/helpers';
|
|
|
|
import { buildIOSTransition } from './transitions/transition.ios';
|
|
import { buildMdTransition } from './transitions/transition.md';
|
|
|
|
const queueMap = new Map<number, TransitionInstruction[]>();
|
|
|
|
// public api
|
|
|
|
export function push(nav: Nav, delegate: FrameworkDelegate, animation: Animation, component: any, data?: any, opts?: NavOptions, done?: () => void): Promise<any> {
|
|
return queueTransaction({
|
|
insertStart: -1,
|
|
insertViews: [{page: component, params: data}],
|
|
opts: opts,
|
|
nav: nav,
|
|
delegate: delegate,
|
|
id: nav.navId,
|
|
animation: animation
|
|
}, done);
|
|
}
|
|
|
|
export function insert(nav: Nav, delegate: FrameworkDelegate, animation: Animation, insertIndex: number, page: any, params?: any, opts?: NavOptions, done?: () => void): Promise<any> {
|
|
return queueTransaction({
|
|
insertStart: insertIndex,
|
|
insertViews: [{ page: page, params: params }],
|
|
opts: opts,
|
|
nav: nav,
|
|
delegate: delegate,
|
|
id: nav.navId,
|
|
animation: animation
|
|
}, done);
|
|
}
|
|
|
|
export function insertPages(nav: Nav, delegate: FrameworkDelegate, animation: Animation, insertIndex: number, insertPages: any[], opts?: NavOptions, done?: () => void): Promise<any> {
|
|
return queueTransaction({
|
|
insertStart: insertIndex,
|
|
insertViews: insertPages,
|
|
opts: opts,
|
|
nav: nav,
|
|
delegate: delegate,
|
|
id: nav.navId,
|
|
animation: animation
|
|
}, done);
|
|
}
|
|
|
|
export function pop(nav: Nav, delegate: FrameworkDelegate, animation: Animation, opts?: NavOptions, done?: () => void): Promise<any> {
|
|
return queueTransaction({
|
|
removeStart: -1,
|
|
removeCount: 1,
|
|
opts: opts,
|
|
nav: nav,
|
|
delegate: delegate,
|
|
id: nav.navId,
|
|
animation: animation
|
|
}, done);
|
|
}
|
|
|
|
export function popToRoot(nav: Nav, delegate: FrameworkDelegate, animation: Animation, opts?: NavOptions, done?: () => void): Promise<any> {
|
|
return queueTransaction({
|
|
removeStart: 1,
|
|
removeCount: -1,
|
|
opts: opts,
|
|
nav: nav,
|
|
delegate: delegate,
|
|
id: nav.navId,
|
|
animation: animation
|
|
}, done);
|
|
}
|
|
|
|
export function popTo(nav: Nav, delegate: FrameworkDelegate, animation: Animation, indexOrViewCtrl: any, opts?: NavOptions, done?: () => void): Promise<any> {
|
|
const config: TransitionInstruction = {
|
|
removeStart: -1,
|
|
removeCount: -1,
|
|
opts: opts,
|
|
nav: nav,
|
|
delegate: delegate,
|
|
id: nav.navId,
|
|
animation: animation
|
|
};
|
|
if (isViewController(indexOrViewCtrl)) {
|
|
config.removeView = indexOrViewCtrl;
|
|
config.removeStart = 1;
|
|
} else if (isNumber(indexOrViewCtrl)) {
|
|
config.removeStart = indexOrViewCtrl + 1;
|
|
}
|
|
return queueTransaction(config, done);
|
|
}
|
|
|
|
export function remove(nav: Nav, delegate: FrameworkDelegate, animation: Animation, startIndex: number, removeCount: number = 1, opts?: NavOptions, done?: () => void): Promise<any> {
|
|
return queueTransaction({
|
|
removeStart: startIndex,
|
|
removeCount: removeCount,
|
|
opts: opts,
|
|
nav: nav,
|
|
delegate: delegate,
|
|
id: nav.navId,
|
|
animation: animation
|
|
}, done);
|
|
}
|
|
|
|
export function removeView(nav: Nav, delegate: FrameworkDelegate, animation: Animation, viewController: ViewController, opts?: NavOptions, done?: () => void): Promise<any> {
|
|
return queueTransaction({
|
|
removeView: viewController,
|
|
removeStart: 0,
|
|
removeCount: 1,
|
|
opts: opts,
|
|
nav: nav,
|
|
delegate: delegate,
|
|
id: nav.navId,
|
|
animation: animation
|
|
}, done);
|
|
}
|
|
|
|
export function setRoot(nav: Nav, delegate: FrameworkDelegate, animation: Animation, page: any, params?: any, opts?: NavOptions, done?: () => void): Promise<any> {
|
|
return setPages(nav, delegate, animation, [{ page: page, params: params }], opts, done);
|
|
}
|
|
|
|
export function setPages(nav: Nav, delegate: FrameworkDelegate, animation: Animation, componentDataPars: ComponentDataPair[], opts?: NavOptions, done?: () => void): Promise<any> {
|
|
if (!isDef(opts)) {
|
|
opts = {};
|
|
}
|
|
if (opts.animate !== true) {
|
|
opts.animate = false;
|
|
}
|
|
return queueTransaction({
|
|
insertStart: 0,
|
|
insertViews: componentDataPars,
|
|
removeStart: 0,
|
|
removeCount: -1,
|
|
opts: opts,
|
|
nav: nav,
|
|
delegate: delegate,
|
|
id: nav.navId,
|
|
animation: animation
|
|
}, done);
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
// private api, exported for testing
|
|
export function queueTransaction(ti: TransitionInstruction, done: () => void): Promise<boolean> {
|
|
const promise = new Promise<boolean>((resolve, reject) => {
|
|
ti.resolve = resolve;
|
|
ti.reject = reject;
|
|
});
|
|
ti.done = done;
|
|
|
|
// Normalize empty
|
|
if (ti.insertViews && ti.insertViews.length === 0) {
|
|
ti.insertViews = undefined;
|
|
}
|
|
|
|
// Normalize empty
|
|
if (ti.insertViews && ti.insertViews.length === 0) {
|
|
ti.insertViews = undefined;
|
|
}
|
|
|
|
// Enqueue transition instruction
|
|
addToQueue(ti);
|
|
|
|
// if there isn't a transition already happening
|
|
// then this will kick off this transition
|
|
nextTransaction(ti.nav);
|
|
|
|
return promise;
|
|
}
|
|
|
|
export function nextTransaction(nav: Nav): Promise<any> {
|
|
|
|
if (nav.transitioning) {
|
|
return Promise.resolve();
|
|
}
|
|
|
|
const topTransaction = getTopTransaction(nav.navId);
|
|
if (!topTransaction) {
|
|
return Promise.resolve();
|
|
}
|
|
|
|
let enteringView: ViewController;
|
|
let leavingView: ViewController;
|
|
return initializeViewBeforeTransition(nav, topTransaction).then(([_enteringView, _leavingView]) => {
|
|
enteringView = _enteringView;
|
|
leavingView = _leavingView;
|
|
return attachViewToDom(nav, enteringView, topTransaction.delegate);
|
|
}).then(() => {
|
|
return loadViewAndTransition(nav, enteringView, leavingView, topTransaction);
|
|
}).then((result: NavResult) => {
|
|
nav.ionNavChanged.emit({ isPop: false });
|
|
return successfullyTransitioned(result, topTransaction);
|
|
}).catch((err: Error) => {
|
|
return transitionFailed(err, topTransaction);
|
|
});
|
|
}
|
|
|
|
export function successfullyTransitioned(result: NavResult, ti: TransitionInstruction) {
|
|
const queue = getQueue(ti.id);
|
|
if (!queue) {
|
|
// TODO, make throw error in the future
|
|
return fireError(new Error('Queue is null, the nav must have been destroyed'), ti);
|
|
}
|
|
|
|
ti.nav.isViewInitialized = true;
|
|
ti.nav.transitionId = null;
|
|
ti.nav.transitioning = false;
|
|
|
|
// TODO - check if it's a swipe back
|
|
|
|
// kick off next transition for this nav I guess
|
|
nextTransaction(ti.nav);
|
|
|
|
if (ti.done) {
|
|
ti.done(
|
|
result.hasCompleted,
|
|
result.requiresTransition,
|
|
result.enteringName,
|
|
result.leavingName,
|
|
result.direction
|
|
);
|
|
}
|
|
ti.resolve(result.hasCompleted);
|
|
}
|
|
|
|
export function transitionFailed(error: Error, ti: TransitionInstruction) {
|
|
const queue = getQueue(ti.nav.navId);
|
|
if (!queue) {
|
|
// TODO, make throw error in the future
|
|
return fireError(new Error('Queue is null, the nav must have been destroyed'), ti);
|
|
}
|
|
|
|
ti.nav.transitionId = null;
|
|
resetQueue(ti.nav.navId);
|
|
|
|
ti.nav.transitioning = false;
|
|
|
|
// TODO - check if it's a swipe back
|
|
|
|
// kick off next transition for this nav I guess
|
|
nextTransaction(ti.nav);
|
|
|
|
fireError(error, ti);
|
|
}
|
|
|
|
export function fireError(error: Error, ti: TransitionInstruction) {
|
|
if (ti.done) {
|
|
ti.done(false, false, error.message);
|
|
}
|
|
if (ti.reject && !ti.nav.destroyed) {
|
|
ti.reject(error);
|
|
} else {
|
|
ti.resolve(false);
|
|
}
|
|
}
|
|
|
|
export function loadViewAndTransition(nav: Nav, enteringView: ViewController, leavingView: ViewController, ti: TransitionInstruction) {
|
|
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
|
|
return Promise.resolve({
|
|
hasCompleted: true,
|
|
requiresTransition: false
|
|
});
|
|
}
|
|
|
|
let transition: Transition = null;
|
|
const transitionId = getParentTransitionId(nav);
|
|
nav.transitionId = transitionId >= 0 ? transitionId : getNextTransitionId();
|
|
|
|
// create the transition options
|
|
const animationOpts: AnimationOptions = {
|
|
animation: ti.opts.animation,
|
|
direction: ti.opts.direction,
|
|
duration: (ti.opts.animate === false ? 0 : ti.opts.duration),
|
|
easing: ti.opts.easing,
|
|
isRTL: false, // TODO
|
|
ev: ti.opts.event,
|
|
};
|
|
|
|
|
|
|
|
const emptyTransition = transitionFactory(ti.animation);
|
|
transition = getHydratedTransition(animationOpts.animation, nav.config, nav.transitionId, emptyTransition, enteringView, leavingView, animationOpts, getDefaultTransition(nav.config));
|
|
|
|
if (nav.swipeToGoBackTransition) {
|
|
nav.swipeToGoBackTransition.destroy();
|
|
nav.swipeToGoBackTransition = null;
|
|
}
|
|
|
|
// it's a swipe to go back transition
|
|
if (transition.isRoot() && ti.opts.progressAnimation) {
|
|
nav.swipeToGoBackTransition = transition;
|
|
}
|
|
|
|
transition.start();
|
|
|
|
return executeAsyncTransition(nav, transition, enteringView, leavingView, ti.delegate, ti.opts, ti.nav.config.getBoolean('animate'));
|
|
}
|
|
|
|
// TODO - transition type
|
|
export function executeAsyncTransition(nav: Nav, transition: Transition, enteringView: ViewController, leavingView: ViewController, delegate: FrameworkDelegate, opts: NavOptions, configShouldAnimate: boolean): Promise<NavResult> {
|
|
assert(nav.transitioning, 'must be transitioning');
|
|
nav.transitionId = null;
|
|
setZIndex(nav, enteringView, leavingView, opts.direction);
|
|
|
|
// always ensure the entering view is viewable
|
|
// ******** DOM WRITE ****************
|
|
// TODO, figure out where we want to read this data from
|
|
enteringView && toggleHidden(enteringView.element, true, true);
|
|
|
|
// always ensure the leaving view is viewable
|
|
// ******** DOM WRITE ****************
|
|
leavingView && toggleHidden(leavingView.element, true, true);
|
|
|
|
const isFirstPage = !nav.isViewInitialized && nav.views.length === 1;
|
|
const shouldNotAnimate = isFirstPage && !nav.isPortal;
|
|
if (configShouldAnimate || shouldNotAnimate) {
|
|
opts.animate = false;
|
|
}
|
|
|
|
if (opts.animate === false) {
|
|
// if it was somehow set to not animation, then make the duration zero
|
|
transition.duration(0);
|
|
}
|
|
|
|
transition.beforeAddRead(() => {
|
|
fireViewWillLifecycles(enteringView, leavingView);
|
|
});
|
|
|
|
// get the set duration of this transition
|
|
const duration = transition.getDuration();
|
|
|
|
// create a callback for when the animation is done
|
|
const transitionCompletePromise = new Promise(resolve => {
|
|
transition.onFinish(resolve);
|
|
});
|
|
|
|
if (transition.isRoot()) {
|
|
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);
|
|
|
|
// TODO - figure out how to disable the app
|
|
}
|
|
|
|
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();
|
|
}
|
|
}
|
|
|
|
return transitionCompletePromise.then(() => {
|
|
return transitionFinish(nav, transition, delegate, opts);
|
|
});
|
|
}
|
|
|
|
export function transitionFinish(nav: Nav, transition: Transition, delegate: FrameworkDelegate, opts: NavOptions): Promise<NavResult> {
|
|
|
|
let promise: Promise<any> = null;
|
|
|
|
if (transition.hasCompleted) {
|
|
transition.enteringView && transition.enteringView.didEnter();
|
|
transition.leavingView && transition.leavingView.didLeave();
|
|
|
|
promise = cleanUpView(nav, delegate, transition.enteringView);
|
|
} else {
|
|
promise = cleanUpView(nav, delegate, transition.leavingView);
|
|
}
|
|
|
|
return promise.then(() => {
|
|
if (transition.isRoot()) {
|
|
|
|
destroyTransition(transition.transitionId);
|
|
|
|
// TODO - enable app
|
|
|
|
nav.transitioning = false;
|
|
|
|
// TODO - navChange on the deep linker used to be called here
|
|
|
|
if (opts.keyboardClose !== false) {
|
|
focusOutActiveElement();
|
|
}
|
|
}
|
|
|
|
return {
|
|
hasCompleted: transition.hasCompleted,
|
|
requiresTransition: true,
|
|
direction: opts.direction
|
|
};
|
|
});
|
|
}
|
|
|
|
export function cleanUpView(nav: Nav, delegate: FrameworkDelegate, activeViewController: ViewController): Promise<any> {
|
|
|
|
if (nav.destroyed) {
|
|
return Promise.resolve();
|
|
}
|
|
|
|
const activeIndex = nav.views.indexOf(activeViewController);
|
|
const promises: Promise<any>[] = [];
|
|
for (let i = nav.views.length - 1; i >= 0; i--) {
|
|
const inactiveViewController = nav.views[i];
|
|
if (i > activeIndex) {
|
|
// this view comes after the active view
|
|
inactiveViewController.willUnload();
|
|
promises.push(destroyView(nav, delegate, inactiveViewController));
|
|
} else if ( i < activeIndex && !nav.isPortal) {
|
|
// this view comes before the active view
|
|
// and it is not a portal then ensure it is hidden
|
|
toggleHidden(inactiveViewController.element, true, false);
|
|
}
|
|
// TODO - review existing z index code!
|
|
}
|
|
return Promise.all(promises);
|
|
}
|
|
|
|
|
|
export function fireViewWillLifecycles(enteringView: ViewController, leavingView: ViewController) {
|
|
leavingView && leavingView.willLeave(!enteringView);
|
|
enteringView && enteringView.willEnter();
|
|
}
|
|
|
|
export function attachViewToDom(nav: Nav, enteringView: ViewController, delegate: FrameworkDelegate) {
|
|
if (enteringView && enteringView.state === STATE_NEW) {
|
|
return delegate.attachViewToDom(nav, enteringView).then(() => {
|
|
enteringView.state = STATE_ATTACHED;
|
|
});
|
|
}
|
|
// it's in the wrong state, so don't attach and just return
|
|
return Promise.resolve();
|
|
}
|
|
|
|
export function initializeViewBeforeTransition(nav: Nav, ti: TransitionInstruction): Promise<ViewController[]> {
|
|
let leavingView: ViewController = null;
|
|
let enteringView: ViewController = null;
|
|
return startTransaction(ti).then(() => {
|
|
const viewControllers = convertComponentToViewController(nav, ti);
|
|
ti.insertViews = viewControllers;
|
|
leavingView = ti.nav.getActive() as ViewController;
|
|
enteringView = getEnteringView(ti, ti.nav, leavingView);
|
|
|
|
if (!leavingView && !enteringView) {
|
|
return Promise.reject(new Error('No views in the stack to remove'));
|
|
}
|
|
|
|
// mark state as initialized
|
|
// enteringView.state = STATE_INITIALIZED;
|
|
ti.requiresTransition = (ti.enteringRequiresTransition || ti.leavingRequiresTransition) && enteringView !== leavingView;
|
|
return testIfViewsCanLeaveAndEnter(enteringView, leavingView, ti);
|
|
}).then(() => {
|
|
return updateNavStacks(enteringView, leavingView, ti);
|
|
}).then(() => {
|
|
return [enteringView, leavingView];
|
|
});
|
|
}
|
|
|
|
// called _postViewInit in old world
|
|
export function updateNavStacks(enteringView: ViewController, leavingView: ViewController, ti: TransitionInstruction): Promise<any> {
|
|
return Promise.resolve().then(() => {
|
|
assert(!!(leavingView || enteringView), 'Both leavingView and enteringView are null');
|
|
assert(!!ti.resolve, 'resolve must be valid');
|
|
assert(!!ti.reject, 'reject must be valid');
|
|
|
|
const destroyQueue: ViewController[] = [];
|
|
|
|
ti.opts = ti.opts || {};
|
|
|
|
if (isDef(ti.removeStart)) {
|
|
assert(ti.removeStart >= 0, 'removeStart can not be negative');
|
|
assert(ti.removeStart >= 0, 'removeCount can not be negative');
|
|
|
|
for (let i = 0; i < ti.removeCount; i++) {
|
|
const view = ti.nav.views[i + ti.removeStart];
|
|
if (view && view !== enteringView && view !== leavingView) {
|
|
destroyQueue.push(view);
|
|
}
|
|
}
|
|
|
|
ti.opts.direction = ti.opts.direction || DIRECTION_BACK;
|
|
}
|
|
|
|
const finalBalance = ti.nav.views.length + (ti.insertViews ? ti.insertViews.length : 0) - (ti.removeCount ? ti.removeCount : 0);
|
|
assert(finalBalance >= 0, 'final balance can not be negative');
|
|
if (finalBalance === 0 && !ti.nav.isPortal) {
|
|
console.warn(`You can't remove all the pages in the navigation stack. nav.pop() is probably called too many times.`);
|
|
throw new Error('Navigation stack needs at least one root page');
|
|
}
|
|
|
|
// At this point the transition can not be rejected, any throw should be an error
|
|
// there are views to insert
|
|
if (ti.insertViews) {
|
|
// manually set the new view's id if an id was passed in the options
|
|
if (isDef(ti.opts.id)) {
|
|
enteringView.id = ti.opts.id;
|
|
}
|
|
|
|
// add the views to the stack
|
|
for (let i = 0; i < ti.insertViews.length; i++) {
|
|
insertViewIntoNav(ti.nav, ti.insertViews[i], ti.insertStart + i);
|
|
}
|
|
|
|
if (ti.enteringRequiresTransition) {
|
|
// default to forward if not already set
|
|
ti.opts.direction = ti.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
|
|
if (destroyQueue && destroyQueue.length) {
|
|
// TODO, figure out how the zone stuff should work in angular
|
|
for (let i = 0; i < destroyQueue.length; i++) {
|
|
const view = destroyQueue[i];
|
|
view.willLeave(true);
|
|
view.didLeave();
|
|
view.willUnload();
|
|
}
|
|
|
|
const destroyQueuePromises: Promise<any>[] = [];
|
|
for (const viewController of destroyQueue) {
|
|
destroyQueuePromises.push(destroyView(ti.nav, ti.delegate, viewController));
|
|
}
|
|
return Promise.all(destroyQueuePromises);
|
|
}
|
|
return null;
|
|
}).then(() => {
|
|
// set which animation it should use if it wasn't set yet
|
|
if (ti.requiresTransition && !ti.opts.animation) {
|
|
if (isDef(ti.removeStart)) {
|
|
ti.opts.animation = (leavingView || enteringView).getTransitionName(ti.opts.direction);
|
|
} else {
|
|
ti.opts.animation = (enteringView || leavingView).getTransitionName(ti.opts.direction);
|
|
}
|
|
}
|
|
});
|
|
}
|
|
|
|
export function destroyView(nav: Nav, delegate: FrameworkDelegate, viewController: ViewController) {
|
|
return viewController.destroy(delegate).then(() => {
|
|
return removeViewFromList(nav, viewController);
|
|
});
|
|
}
|
|
|
|
export function removeViewFromList(nav: Nav, viewController: ViewController) {
|
|
assert(viewController.state === STATE_ATTACHED || viewController.state === STATE_DESTROYED, 'view state should be loaded or destroyed');
|
|
const index = nav.views.indexOf(viewController);
|
|
assert(index > -1, 'view must be part of the stack');
|
|
if (index >= 0) {
|
|
nav.views.splice(index, 1);
|
|
}
|
|
}
|
|
|
|
export function insertViewIntoNav(nav: Nav, view: ViewController, index: number) {
|
|
const existingIndex = nav.views.indexOf(view);
|
|
if (existingIndex > -1) {
|
|
// this view is already in the stack!!
|
|
// move it to its new location
|
|
assert(view.nav === nav, 'view is not part of the nav');
|
|
nav.views.splice(index, 0, nav.views.splice(existingIndex, 1)[0]);
|
|
} else {
|
|
assert(!view.nav || (nav.isPortal && view.nav === nav), 'nav is used');
|
|
// this is a new view to add to the stack
|
|
// create the new entering view
|
|
view.nav = nav;
|
|
|
|
// give this inserted view an ID
|
|
viewIds++;
|
|
if (!view.id) {
|
|
view.id = `${nav.navId}-${viewIds}`;
|
|
}
|
|
|
|
// insert the entering view into the correct index in the stack
|
|
nav.views.splice(index, 0, view);
|
|
}
|
|
}
|
|
|
|
export function testIfViewsCanLeaveAndEnter(enteringView: ViewController, leavingView: ViewController, ti: TransitionInstruction) {
|
|
if (!ti.requiresTransition) {
|
|
return Promise.resolve();
|
|
}
|
|
|
|
const promises: Promise<any>[] = [];
|
|
|
|
|
|
if (leavingView) {
|
|
promises.push(lifeCycleTest(leavingView, 'Leave'));
|
|
}
|
|
if (enteringView) {
|
|
promises.push(lifeCycleTest(enteringView, 'Enter'));
|
|
}
|
|
|
|
if (promises.length === 0) {
|
|
return Promise.resolve();
|
|
}
|
|
|
|
// darn, async promises, gotta wait for them to resolve
|
|
return Promise.all(promises).then((values: any[]) => {
|
|
if (values.some(result => result === false)) {
|
|
ti.reject = null;
|
|
throw new Error('canEnter/Leave returned false');
|
|
}
|
|
});
|
|
}
|
|
|
|
export function lifeCycleTest(viewController: ViewController, enterOrLeave: string) {
|
|
const methodName = `ionViewCan${enterOrLeave}`;
|
|
if (viewController.instance && viewController.instance[methodName]) {
|
|
try {
|
|
const result = viewController.instance[methodName];
|
|
if (result instanceof Promise) {
|
|
return result;
|
|
}
|
|
return Promise.resolve(result !== false);
|
|
} catch (e) {
|
|
return Promise.reject(new Error(`Unexpected error when calling ${methodName}: ${e.message}`));
|
|
}
|
|
}
|
|
return Promise.resolve(true);
|
|
}
|
|
|
|
export function startTransaction(ti: TransitionInstruction): Promise<any> {
|
|
|
|
const viewsLength = ti.nav.views ? ti.nav.views.length : 0;
|
|
|
|
if (isDef(ti.removeView)) {
|
|
assert(isDef(ti.removeStart), 'removeView needs removeStart');
|
|
assert(isDef(ti.removeCount), 'removeView needs removeCount');
|
|
|
|
const index = ti.nav.views.indexOf(ti.removeView());
|
|
if (index < 0) {
|
|
return Promise.reject(new Error('The removeView was not found'));
|
|
}
|
|
ti.removeStart += index;
|
|
}
|
|
|
|
if (isDef(ti.removeStart)) {
|
|
if (ti.removeStart < 0) {
|
|
ti.removeStart = (viewsLength - 1);
|
|
}
|
|
if (ti.removeCount < 0) {
|
|
ti.removeCount = (viewsLength - ti.removeStart);
|
|
}
|
|
ti.leavingRequiresTransition = (ti.removeCount > 0) && ((ti.removeStart + ti.removeCount) === viewsLength);
|
|
}
|
|
|
|
if (isDef(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);
|
|
}
|
|
|
|
ti.nav.transitioning = true;
|
|
|
|
return Promise.resolve();
|
|
}
|
|
|
|
export function getEnteringView(ti: TransitionInstruction, nav: Nav, leavingView: ViewController): ViewController {
|
|
if (ti.insertViews && ti.insertViews.length) {
|
|
// grab the very last view of the views to be inserted
|
|
// and initialize it as the new entering view
|
|
return ti.insertViews[ti.insertViews.length - 1];
|
|
}
|
|
if (isDef(ti.removeStart)) {
|
|
var removeEnd = ti.removeStart + ti.removeCount;
|
|
for (let i = nav.views.length - 1; i >= 0; i--) {
|
|
if ((i < ti.removeStart || i >= removeEnd) && nav.views[i] !== leavingView) {
|
|
return nav.views[i];
|
|
}
|
|
}
|
|
}
|
|
return null;
|
|
}
|
|
|
|
export function convertViewsToViewControllers(views: any[]): ViewController[] {
|
|
return views.map(view => {
|
|
if (view) {
|
|
if (isViewController(view)) {
|
|
return view as ViewController;
|
|
}
|
|
return new ViewController(view.page, view.params);
|
|
}
|
|
return null;
|
|
}).filter(view => !!view);
|
|
}
|
|
|
|
export function convertComponentToViewController(nav: Nav, ti: TransitionInstruction): ViewController[] {
|
|
if (ti.insertViews) {
|
|
assert(ti.insertViews.length > 0, 'length can not be zero');
|
|
const viewControllers = convertViewsToViewControllers(ti.insertViews);
|
|
assert(ti.insertViews.length === viewControllers.length, 'lengths does not match');
|
|
if (viewControllers.length === 0) {
|
|
throw new Error('No views to insert');
|
|
}
|
|
|
|
for (const viewController of viewControllers) {
|
|
if (viewController.nav && viewController.nav.navId !== ti.id) {
|
|
throw new Error('The view has already inserted into a different nav');
|
|
}
|
|
if (viewController.state === STATE_DESTROYED) {
|
|
throw new Error('The view has already been destroyed');
|
|
}
|
|
if (nav.useRouter && !resolveRoute(nav, viewController.component)) {
|
|
throw new Error('Route not specified for ' + viewController.component);
|
|
}
|
|
}
|
|
return viewControllers;
|
|
}
|
|
return [];
|
|
}
|
|
|
|
export function addToQueue(ti: TransitionInstruction) {
|
|
const list = queueMap.get(ti.id) || [];
|
|
list.push(ti);
|
|
queueMap.set(ti.id, list);
|
|
}
|
|
|
|
export function getQueue(id: number) {
|
|
return queueMap.get(id) || [];
|
|
}
|
|
|
|
export function resetQueue(id: number) {
|
|
queueMap.set(id, []);
|
|
}
|
|
|
|
export function getTopTransaction(id: number) {
|
|
const queue = getQueue(id);
|
|
if (!queue.length) {
|
|
return null;
|
|
}
|
|
const tmp = queue.concat();
|
|
const toReturn = tmp.shift();
|
|
queueMap.set(id, tmp);
|
|
return toReturn;
|
|
}
|
|
|
|
export function getDefaultTransition(config: Config) {
|
|
return config.get('mode') === 'md' ? buildMdTransition : buildIOSTransition;
|
|
}
|
|
|
|
let viewIds = VIEW_ID_START;
|
|
const DISABLE_APP_MINIMUM_DURATION = 64;
|