@ionic/core
Version:
Base components for Ionic
635 lines (634 loc) • 22 kB
JavaScript
import { assert } from '../../utils/helpers';
import { lifecycle, setPageHidden, transition } from '../../utils/transition';
import { LIFECYCLE_DID_LEAVE, LIFECYCLE_WILL_LEAVE, LIFECYCLE_WILL_UNLOAD } from './constants';
import { VIEW_STATE_ATTACHED, VIEW_STATE_DESTROYED, VIEW_STATE_NEW, convertToViews, matches } from './view-controller';
export class Nav {
constructor() {
this.transInstr = [];
this.useRouter = false;
this.isTransitioning = false;
this.destroyed = false;
this.views = [];
this.animated = true;
}
swipeGestureChanged() {
if (this.gesture) {
this.gesture.setDisabled(this.swipeGesture !== true);
}
}
rootChanged() {
const isDev = Build.isDev;
if (this.root !== undefined) {
if (!this.useRouter) {
this.setRoot(this.root, this.rootParams);
}
else if (isDev) {
console.warn('<ion-nav> does not support a root attribute when using ion-router.');
}
}
}
componentWillLoad() {
this.useRouter =
!!this.win.document.querySelector('ion-router') &&
!this.el.closest('[no-router]');
if (this.swipeGesture === undefined) {
this.swipeGesture = this.config.getBoolean('swipeBackEnabled', this.mode === 'ios');
}
this.ionNavWillLoad.emit();
}
async componentDidLoad() {
this.rootChanged();
this.gesture = (await import('../../utils/gesture/swipe-back')).createSwipeBackGesture(this.el, this.queue, this.canStart.bind(this), this.onStart.bind(this), this.onMove.bind(this), this.onEnd.bind(this));
this.swipeGestureChanged();
}
componentDidUnload() {
for (const view of this.views) {
lifecycle(view.element, LIFECYCLE_WILL_UNLOAD);
view._destroy();
}
if (this.gesture) {
this.gesture.destroy();
this.gesture = undefined;
}
this.transInstr.length = this.views.length = 0;
this.destroyed = true;
}
push(component, componentProps, opts, done) {
return this.queueTrns({
insertStart: -1,
insertViews: [{ page: component, params: componentProps }],
opts
}, done);
}
insert(insertIndex, component, componentProps, opts, done) {
return this.queueTrns({
insertStart: insertIndex,
insertViews: [{ page: component, params: componentProps }],
opts
}, done);
}
insertPages(insertIndex, insertComponents, opts, done) {
return this.queueTrns({
insertStart: insertIndex,
insertViews: insertComponents,
opts
}, done);
}
pop(opts, done) {
return this.queueTrns({
removeStart: -1,
removeCount: 1,
opts
}, done);
}
popTo(indexOrViewCtrl, opts, done) {
const config = {
removeStart: -1,
removeCount: -1,
opts
};
if (typeof indexOrViewCtrl === 'object' && indexOrViewCtrl.component) {
config.removeView = indexOrViewCtrl;
config.removeStart = 1;
}
else if (typeof indexOrViewCtrl === 'number') {
config.removeStart = indexOrViewCtrl + 1;
}
return this.queueTrns(config, done);
}
popToRoot(opts, done) {
return this.queueTrns({
removeStart: 1,
removeCount: -1,
opts
}, done);
}
removeIndex(startIndex, removeCount = 1, opts, done) {
return this.queueTrns({
removeStart: startIndex,
removeCount,
opts
}, done);
}
setRoot(component, componentProps, opts, done) {
return this.setPages([{ page: component, params: componentProps }], opts, done);
}
setPages(views, opts, done) {
if (opts == null) {
opts = {};
}
if (opts.animated !== true) {
opts.animated = false;
}
return this.queueTrns({
insertStart: 0,
insertViews: views,
removeStart: 0,
removeCount: -1,
opts
}, done);
}
setRouteId(id, params, direction) {
const active = this.getActiveSync();
if (matches(active, id, params)) {
return Promise.resolve({
changed: false,
element: active.element
});
}
let resolve;
const promise = new Promise(r => (resolve = r));
let finish;
const commonOpts = {
updateURL: false,
viewIsReady: enteringEl => {
let mark;
const p = new Promise(r => (mark = r));
resolve({
changed: true,
element: enteringEl,
markVisible: async () => {
mark();
await finish;
}
});
return p;
}
};
if (direction === 'root') {
finish = this.setRoot(id, params, commonOpts);
}
else {
const viewController = this.views.find(v => matches(v, id, params));
if (viewController) {
finish = this.popTo(viewController, Object.assign({}, commonOpts, { direction: 'back' }));
}
else if (direction === 'forward') {
finish = this.push(id, params, commonOpts);
}
else if (direction === 'back') {
finish = this.setRoot(id, params, Object.assign({}, commonOpts, { direction: 'back', animated: true }));
}
}
return promise;
}
async getRouteId() {
const active = this.getActiveSync();
return active
? {
id: active.element.tagName,
params: active.params,
element: active.element
}
: undefined;
}
getActive() {
return Promise.resolve(this.getActiveSync());
}
getByIndex(index) {
return Promise.resolve(this.views[index]);
}
canGoBack(view) {
return Promise.resolve(this.canGoBackSync(view));
}
getPrevious(view) {
return Promise.resolve(this.getPreviousSync(view));
}
getLength() {
return this.views.length;
}
getActiveSync() {
return this.views[this.views.length - 1];
}
canGoBackSync(view = this.getActiveSync()) {
return !!(view && this.getPreviousSync(view));
}
getPreviousSync(view = this.getActiveSync()) {
if (!view) {
return undefined;
}
const views = this.views;
const index = views.indexOf(view);
return index > 0 ? views[index - 1] : undefined;
}
queueTrns(ti, done) {
if (this.isTransitioning && ti.opts != null && ti.opts.skipIfBusy) {
return Promise.resolve(false);
}
const promise = new Promise((resolve, reject) => {
ti.resolve = resolve;
ti.reject = reject;
});
ti.done = done;
if (ti.insertViews && ti.insertViews.length === 0) {
ti.insertViews = undefined;
}
this.transInstr.push(ti);
this.nextTrns();
return promise;
}
success(result, ti) {
if (this.destroyed) {
this.fireError('nav controller was destroyed', ti);
return;
}
if (ti.done) {
ti.done(result.hasCompleted, result.requiresTransition, result.enteringView, result.leavingView, result.direction);
}
ti.resolve(result.hasCompleted);
if (ti.opts.updateURL !== false && this.useRouter) {
const router = this.win.document.querySelector('ion-router');
if (router) {
const direction = result.direction === 'back' ? 'back' : 'forward';
router.navChanged(direction);
}
}
}
failed(rejectReason, ti) {
if (this.destroyed) {
this.fireError('nav controller was destroyed', ti);
return;
}
this.transInstr.length = 0;
this.fireError(rejectReason, ti);
}
fireError(rejectReason, ti) {
if (ti.done) {
ti.done(false, false, rejectReason);
}
if (ti.reject && !this.destroyed) {
ti.reject(rejectReason);
}
else {
ti.resolve(false);
}
}
nextTrns() {
if (this.isTransitioning) {
return false;
}
const ti = this.transInstr.shift();
if (!ti) {
return false;
}
this.runTransition(ti);
return true;
}
async runTransition(ti) {
try {
this.ionNavWillChange.emit();
this.isTransitioning = true;
this.prepareTI(ti);
const leavingView = this.getActiveSync();
const enteringView = this.getEnteringView(ti, leavingView);
if (!leavingView && !enteringView) {
throw new Error('no views in the stack to be removed');
}
if (enteringView && enteringView.state === VIEW_STATE_NEW) {
await enteringView.init(this.el);
}
this.postViewInit(enteringView, leavingView, ti);
const requiresTransition = (ti.enteringRequiresTransition || ti.leavingRequiresTransition) &&
enteringView !== leavingView;
const result = requiresTransition
? await this.transition(enteringView, leavingView, ti)
: {
hasCompleted: true,
requiresTransition: false
};
this.success(result, ti);
this.ionNavDidChange.emit();
}
catch (rejectReason) {
this.failed(rejectReason, ti);
}
this.isTransitioning = false;
this.nextTrns();
}
prepareTI(ti) {
const viewsLength = this.views.length;
ti.opts = ti.opts || {};
if (ti.opts.delegate === undefined) {
ti.opts.delegate = this.delegate;
}
if (ti.removeView !== undefined) {
assert(ti.removeStart !== undefined, 'removeView needs removeStart');
assert(ti.removeCount !== undefined, 'removeView needs removeCount');
const index = this.views.indexOf(ti.removeView);
if (index < 0) {
throw new Error('removeView was not found');
}
ti.removeStart += index;
}
if (ti.removeStart !== undefined) {
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 (ti.insertViews) {
if (ti.insertStart < 0 || ti.insertStart > viewsLength) {
ti.insertStart = viewsLength;
}
ti.enteringRequiresTransition = ti.insertStart === viewsLength;
}
const insertViews = ti.insertViews;
if (!insertViews) {
return;
}
assert(insertViews.length > 0, 'length can not be zero');
const viewControllers = convertToViews(insertViews);
if (viewControllers.length === 0) {
throw new Error('invalid views to insert');
}
for (const view of viewControllers) {
view.delegate = ti.opts.delegate;
const nav = view.nav;
if (nav && nav !== this) {
throw new Error('inserted view was already inserted');
}
if (view.state === VIEW_STATE_DESTROYED) {
throw new Error('inserted view was already destroyed');
}
}
ti.insertViews = viewControllers;
}
getEnteringView(ti, leavingView) {
const insertViews = ti.insertViews;
if (insertViews !== undefined) {
return insertViews[insertViews.length - 1];
}
const removeStart = ti.removeStart;
if (removeStart !== undefined) {
const views = this.views;
const removeEnd = removeStart + ti.removeCount;
for (let i = views.length - 1; i >= 0; i--) {
const view = views[i];
if ((i < removeStart || i >= removeEnd) && view !== leavingView) {
return view;
}
}
}
return undefined;
}
postViewInit(enteringView, leavingView, ti) {
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 destroyQueue;
if (removeStart !== undefined && removeCount !== undefined) {
assert(removeStart >= 0, 'removeStart can not be negative');
assert(removeCount >= 0, 'removeCount can not be negative');
destroyQueue = [];
for (let i = 0; i < removeCount; i++) {
const view = this.views[i + removeStart];
if (view && view !== enteringView && view !== leavingView) {
destroyQueue.push(view);
}
}
opts.direction = opts.direction || 'back';
}
const finalBalance = this.views.length +
(insertViews !== undefined ? insertViews.length : 0) -
(removeCount !== undefined ? removeCount : 0);
assert(finalBalance >= 0, 'final balance can not be negative');
if (finalBalance === 0) {
console.warn(`You can't remove all the pages in the navigation stack. nav.pop() is probably called too many times.`, this, this.el);
throw new Error('navigation stack needs at least one root page');
}
if (insertViews) {
let insertIndex = ti.insertStart;
for (const view of insertViews) {
this.insertViewAt(view, insertIndex);
insertIndex++;
}
if (ti.enteringRequiresTransition) {
opts.direction = opts.direction || 'forward';
}
}
if (destroyQueue && destroyQueue.length > 0) {
for (const view of destroyQueue) {
lifecycle(view.element, LIFECYCLE_WILL_LEAVE);
lifecycle(view.element, LIFECYCLE_DID_LEAVE);
lifecycle(view.element, LIFECYCLE_WILL_UNLOAD);
}
for (const view of destroyQueue) {
this.destroyView(view);
}
}
}
async transition(enteringView, leavingView, ti) {
const opts = ti.opts;
const progressCallback = opts.progressAnimation
? (ani) => this.sbAni = ani
: undefined;
const enteringEl = enteringView.element;
const leavingEl = leavingView && leavingView.element;
const animationOpts = Object.assign({ mode: this.mode, showGoBack: this.canGoBackSync(enteringView), queue: this.queue, window: this.win, baseEl: this.el, animationBuilder: this.animation || opts.animationBuilder || this.config.get('navAnimation'), progressCallback, animated: this.animated && this.config.getBoolean('animated', true), enteringEl,
leavingEl }, opts);
const { hasCompleted } = await transition(animationOpts);
return this.transitionFinish(hasCompleted, enteringView, leavingView, opts);
}
transitionFinish(hasCompleted, enteringView, leavingView, opts) {
const cleanupView = hasCompleted ? enteringView : leavingView;
if (cleanupView) {
this.cleanup(cleanupView);
}
return {
hasCompleted,
requiresTransition: true,
enteringView,
leavingView,
direction: opts.direction
};
}
insertViewAt(view, index) {
const views = this.views;
const existingIndex = views.indexOf(view);
if (existingIndex > -1) {
assert(view.nav === this, 'view is not part of the nav');
views.splice(index, 0, views.splice(existingIndex, 1)[0]);
}
else {
assert(!view.nav, 'nav is used');
view.nav = this;
views.splice(index, 0, view);
}
}
removeView(view) {
assert(view.state === VIEW_STATE_ATTACHED || view.state === VIEW_STATE_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) {
view._destroy();
this.removeView(view);
}
cleanup(activeView) {
if (this.destroyed) {
return;
}
const views = this.views;
const activeViewIndex = views.indexOf(activeView);
for (let i = views.length - 1; i >= 0; i--) {
const view = views[i];
const element = view.element;
if (i > activeViewIndex) {
lifecycle(element, LIFECYCLE_WILL_UNLOAD);
this.destroyView(view);
}
else if (i < activeViewIndex) {
setPageHidden(element, true);
}
}
}
canStart() {
return (!!this.swipeGesture &&
!this.isTransitioning &&
this.transInstr.length === 0 &&
this.canGoBackSync());
}
onStart() {
this.queueTrns({
removeStart: -1,
removeCount: 1,
opts: {
direction: 'back',
progressAnimation: true
}
}, undefined);
}
onMove(stepValue) {
if (this.sbAni) {
this.sbAni.progressStep(stepValue);
}
}
onEnd(shouldComplete, stepValue, dur) {
if (this.sbAni) {
this.sbAni.progressEnd(shouldComplete, stepValue, dur);
}
}
render() {
return (h("slot", null));
}
static get is() { return "ion-nav"; }
static get encapsulation() { return "shadow"; }
static get properties() { return {
"animated": {
"type": Boolean,
"attr": "animated"
},
"animation": {
"type": "Any",
"attr": "animation"
},
"canGoBack": {
"method": true
},
"config": {
"context": "config"
},
"delegate": {
"type": "Any",
"attr": "delegate"
},
"el": {
"elementRef": true
},
"getActive": {
"method": true
},
"getByIndex": {
"method": true
},
"getPrevious": {
"method": true
},
"getRouteId": {
"method": true
},
"insert": {
"method": true
},
"insertPages": {
"method": true
},
"pop": {
"method": true
},
"popTo": {
"method": true
},
"popToRoot": {
"method": true
},
"push": {
"method": true
},
"queue": {
"context": "queue"
},
"removeIndex": {
"method": true
},
"root": {
"type": String,
"attr": "root",
"watchCallbacks": ["rootChanged"]
},
"rootParams": {
"type": "Any",
"attr": "root-params"
},
"setPages": {
"method": true
},
"setRoot": {
"method": true
},
"setRouteId": {
"method": true
},
"swipeGesture": {
"type": Boolean,
"attr": "swipe-gesture",
"mutable": true,
"watchCallbacks": ["swipeGestureChanged"]
},
"win": {
"context": "window"
}
}; }
static get events() { return [{
"name": "ionNavWillLoad",
"method": "ionNavWillLoad",
"bubbles": true,
"cancelable": true,
"composed": true
}, {
"name": "ionNavWillChange",
"method": "ionNavWillChange",
"bubbles": false,
"cancelable": true,
"composed": true
}, {
"name": "ionNavDidChange",
"method": "ionNavDidChange",
"bubbles": false,
"cancelable": true,
"composed": true
}]; }
static get style() { return "/**style-placeholder:ion-nav:**/"; }
}