mirror of
https://github.com/ionic-team/ionic-framework.git
synced 2025-08-19 03:32:21 +08:00
1407 lines
40 KiB
TypeScript
1407 lines
40 KiB
TypeScript
import {ChangeDetectorRef, Compiler, ElementRef, Injector, provide, NgZone, AppViewManager, Renderer} from 'angular2/angular2';
|
|
import {wtfLeave, wtfCreateScope, WtfScopeFn} from 'angular2/angular2';
|
|
|
|
import {Ion} from '../ion';
|
|
import {IonicApp} from '../app/app';
|
|
import {Config} from '../../config/config';
|
|
import {Keyboard} from '../../util/keyboard';
|
|
import {ViewController} from './view-controller';
|
|
import {Animation} from '../../animations/animation';
|
|
import {SwipeBackGesture} from './swipe-back';
|
|
import {isBoolean, array} from '../../util/util';
|
|
import {rafFrames} from '../../util/dom';
|
|
|
|
/**
|
|
* _For examples on the basic usage of NavController, check out the
|
|
* [Navigation section](../../../../components/#navigation) of the Component
|
|
* docs._
|
|
*
|
|
* NavController is the base class for navigation controller components like
|
|
* [`Nav`](../Nav/) and [`Tab`](../../Tabs/Tab/). You use navigation controllers
|
|
* to navigate to [pages](#creating_pages) in your app. At a basic level, a
|
|
* navigation controller is an array of pages representing a particular history
|
|
* (of a Tab for example). This array can be manipulated to navigate throughout
|
|
* an app by pushing and popping pages or inserting and removing them at
|
|
* arbitrary locations in history.
|
|
*
|
|
* The current page is the last one in the array, or the top of the stack if we
|
|
* think of it that way. [Pushing](#push) a new page onto the top of the
|
|
* navigation stack causes the new page to be animated in, while [popping](#pop)
|
|
* the current page will navigate to the previous page in the stack.
|
|
*
|
|
* Unless you are using a directive like [NavPush](../NavPush/), or need a
|
|
* specific NavController, most times you will inject and use a reference to the
|
|
* nearest NavController to manipulate the navigation stack.
|
|
*
|
|
* <h3 id="injecting_nav_controller">Injecting NavController</h3>
|
|
* Injecting NavController will always get you an instance of the nearest
|
|
* NavController, regardless of whether it is a Tab or a Nav.
|
|
*
|
|
* Behind the scenes, when Ionic instantiates a new NavController, it creates an
|
|
* injector with NavController bound to that instance (usually either a Nav or
|
|
* Tab) and adds the injector to its own providers. For more information on
|
|
* providers and dependency injection, see [Providers and DI]().
|
|
*
|
|
* Instead, you can inject NavController and know that it is the correct
|
|
* navigation controller for most situations (for more advanced situations, see
|
|
* [Menu](../../Menu/Menu/) and [Tab](../../Tab/Tab/)).
|
|
*
|
|
* ```ts
|
|
* class MyComponent {
|
|
* constructor(nav: NavController) {
|
|
* this.nav = nav;
|
|
* }
|
|
* }
|
|
* ```
|
|
*
|
|
* <h2 id="creating_pages">Page creation</h2>
|
|
* _For more information on the `@Page` decorator see the [@Page API
|
|
* reference](../../../config/Page/)._
|
|
*
|
|
* Pages are created when they are added to the navigation stack. For methods
|
|
* like [push()](#push), the NavController takes any component class that is
|
|
* decorated with `@Page` as its first argument. The NavController then
|
|
* compiles that component, adds it to the app and animates it into view.
|
|
*
|
|
* By default, pages are cached and left in the DOM if they are navigated away
|
|
* from but still in the navigation stack (the exiting page on a `push()` for
|
|
* example). They are destroyed when removed from the navigation stack (on
|
|
* [pop()](#pop) or [setRoot()](#setRoot)).
|
|
*
|
|
*
|
|
* <h2 id="Lifecycle">Lifecycle events</h2>
|
|
* Lifecycle events are fired during various stages of navigation. They can be
|
|
* defined in any `@Page` decorated component class.
|
|
*
|
|
* ```ts
|
|
* @Page({
|
|
* template: 'Hello World'
|
|
* })
|
|
* class HelloWorld {
|
|
* onPageLoaded() {
|
|
* console.log("I'm alive!");
|
|
* }
|
|
* onPageWillLeave() {
|
|
* console.log("Looks like I'm about to leave :(");
|
|
* }
|
|
* }
|
|
* ```
|
|
*
|
|
*
|
|
*
|
|
* - `onPageLoaded` - Runs when the page has loaded. This event only happens once per page being created and added to the DOM. If a page leaves but is cached, then this event will not fire again on a subsequent viewing. The `onPageLoaded` event is good place to put your setup code for the page.
|
|
* - `onPageWillEnter` - Runs when the page is about to enter and become the active page.
|
|
* - `onPageDidEnter` - Runs when the page has fully entered and is now the active page. This event will fire, whether it was the first load or a cached page.
|
|
* - `onPageWillLeave` - Runs when the page is about to leave and no longer be the active page.
|
|
* - `onPageDidLeave` - Runs when the page has finished leaving and is no longer the active page.
|
|
* - `onPageWillUnload` - Runs when the page is about to be destroyed and have its elements removed.
|
|
* - `onPageDidUnload` - Runs after the page has been destroyed and its elements have been removed.
|
|
*
|
|
*/
|
|
export class NavController extends Ion {
|
|
|
|
/** @internal */
|
|
static _tranitionScope: WtfScopeFn = wtfCreateScope('ionic.NavController#_transition()');
|
|
static _loadPageScope: WtfScopeFn = wtfCreateScope('ionic.NavController#loadPage()');
|
|
static _transCompleteScope: WtfScopeFn = wtfCreateScope('ionic.NavController#_transComplete()');
|
|
|
|
|
|
constructor(
|
|
parentnavCtrl: NavController,
|
|
app: IonicApp,
|
|
config: Config,
|
|
keyboard: Keyboard,
|
|
elementRef: ElementRef,
|
|
compiler: Compiler,
|
|
viewManager: AppViewManager,
|
|
zone: NgZone,
|
|
renderer: Renderer,
|
|
cd: ChangeDetectorRef
|
|
) {
|
|
super(elementRef, config);
|
|
|
|
this.parent = parentnavCtrl;
|
|
this.app = app;
|
|
this.config = config;
|
|
this.keyboard = keyboard;
|
|
|
|
this._compiler = compiler;
|
|
this._viewManager = viewManager;
|
|
this._zone = zone;
|
|
this._renderer = renderer;
|
|
this._cd = cd;
|
|
|
|
this._views = [];
|
|
this._trnsTime = 0;
|
|
|
|
this._sbTrans = null;
|
|
this._sbEnabled = config.get('swipeBackEnabled') || false;
|
|
this._sbThreshold = config.get('swipeBackThreshold') || 40;
|
|
|
|
this.id = ++ctrlIds;
|
|
this._ids = -1;
|
|
|
|
// build a new injector for child ViewControllers to use
|
|
this.providers = Injector.resolve([
|
|
provide(NavController, {useValue: this})
|
|
]);
|
|
}
|
|
|
|
/**
|
|
* Boolean if the nav controller is actively transitioning or not.
|
|
* @private
|
|
* @return {bool}
|
|
*/
|
|
isTransitioning() {
|
|
return (this._trnsTime > Date.now());
|
|
}
|
|
|
|
/**
|
|
* Boolean if the nav controller is actively transitioning or not.
|
|
* @private
|
|
* @return {bool}
|
|
*/
|
|
|
|
setTransitioning(isTransitioning, fallback=700) {
|
|
this._trnsTime = (isTransitioning ? Date.now() + fallback : 0);
|
|
}
|
|
|
|
/**
|
|
* Push is how we can pass components and navigate to them. We push the component we want to navigate to on to the navigation stack.
|
|
*
|
|
* ```typescript
|
|
* class MyClass{
|
|
* constructor(nav:NavController){
|
|
* this.nav = nav;
|
|
* }
|
|
*
|
|
* pushPage(){
|
|
* this.nav.push(SecondView);
|
|
* }
|
|
* }
|
|
* ```
|
|
*
|
|
* We can also pass along parameters to the next view, such as data that we have on the current view. This is a similar concept to to V1 apps with `$stateParams`.
|
|
*
|
|
* ```typescript
|
|
* class MyClass{
|
|
* constructor(nav:NavController){
|
|
* this.nav = nav;
|
|
* }
|
|
*
|
|
* pushPage(user){
|
|
* this.nav.push(SecondView,{
|
|
* // user is an object we have in our view
|
|
* // typically this comes from an ngFor or some array
|
|
* // here we can create an object with a property of
|
|
* // paramUser, and set it's value to the user object we passed in
|
|
* paramUser: user
|
|
* });
|
|
* }
|
|
* }
|
|
* ```
|
|
*
|
|
* We'll look at how we can access that data in the `SecondView` in the navParam docs
|
|
*
|
|
* We can also pass any options to the transtion from that same method
|
|
*
|
|
* ```typescript
|
|
* class MyClass{
|
|
* constructor(nav:NavController){
|
|
* this.nav = nav;
|
|
* }
|
|
*
|
|
* pushPage(user){
|
|
* this.nav.push(SecondView,{
|
|
* // user is an object we have in our view
|
|
* // typically this comes from an ngFor or some array
|
|
* // here we can create an object with a property of
|
|
* // paramUser, and set it's value to the user object we passed in
|
|
* paramUser: user
|
|
* },{
|
|
* // here we can configure things like the animations direction or
|
|
* // or if the view should animate at all.
|
|
* animate: true,
|
|
* direction: back
|
|
* });
|
|
* }
|
|
* }
|
|
* ```
|
|
* @param {Any} component The name of the component you want to push on the navigation stack
|
|
* @param {Object} [params={}] Any nav-params you want to pass along to the next view
|
|
* @param {Object} [opts={}] Any options you want to use pass to transtion
|
|
* @returns {Promise} Returns a promise when the transition is completed
|
|
*/
|
|
push(componentType, params = {}, opts = {}, callback) {
|
|
if (!componentType) {
|
|
return Promise.reject('invalid componentType to push');
|
|
}
|
|
|
|
if (typeof componentType !== 'function') {
|
|
throw 'Loading component must be a component class, not "' + componentType.toString() + '"';
|
|
}
|
|
|
|
if (this.isTransitioning()) {
|
|
return Promise.reject('nav controller actively transitioning');
|
|
}
|
|
|
|
this.setTransitioning(true, 500);
|
|
|
|
let promise = null;
|
|
if (!callback) {
|
|
promise = new Promise(res => { callback = res; });
|
|
}
|
|
|
|
// do not animate if this is the first in the stack
|
|
if (!this._views.length && !opts.animateFirst) {
|
|
opts.animate = false;
|
|
}
|
|
|
|
// default the direction to "forward"
|
|
opts.direction = opts.direction || 'forward';
|
|
|
|
// the active view is going to be the leaving one (if one exists)
|
|
let leavingView = this.getActive() || new ViewController();
|
|
leavingView.shouldCache = (isBoolean(opts.cacheLeavingView) ? opts.cacheLeavingView : true);
|
|
leavingView.shouldDestroy = !leavingView.shouldCache;
|
|
if (leavingView.shouldDestroy) {
|
|
leavingView.willUnload();
|
|
}
|
|
|
|
// create a new ViewController
|
|
let enteringView = new ViewController(this, componentType, params);
|
|
enteringView.shouldDestroy = false;
|
|
enteringView.shouldCache = false;
|
|
enteringView.pageType = opts.pageType;
|
|
enteringView.handle = opts.handle || null;
|
|
|
|
// add the view to the stack
|
|
this._add(enteringView);
|
|
|
|
if (this.router) {
|
|
// notify router of the state change
|
|
this.router.stateChange('push', enteringView, params);
|
|
}
|
|
|
|
// start the transition
|
|
this._transition(enteringView, leavingView, opts, callback);
|
|
|
|
return promise;
|
|
}
|
|
|
|
/**
|
|
* If you wanted to navigate back from a current view, you can use the back-button or programatically call `pop()`
|
|
* Similar to `push()`, you can pass animation options.
|
|
*
|
|
* ```typescript
|
|
* class SecondView{
|
|
* constructor(nav:NavController){
|
|
* this.nav = nav;
|
|
* }
|
|
*
|
|
* goBack(){
|
|
* this.nav.pop({
|
|
* animate: true,
|
|
* direction: back
|
|
* });
|
|
* }
|
|
* }
|
|
* ```
|
|
*
|
|
* @param {Object} [opts={}] Any options you want to use pass to transtion
|
|
* @returns {Promise} Returns a promise when the transition is completed
|
|
*/
|
|
pop(opts = {}) {
|
|
if (!opts.animateFirst && !this.canGoBack()) {
|
|
return Promise.reject('pop cannot go back');
|
|
}
|
|
|
|
if (this.isTransitioning()) {
|
|
return Promise.reject('nav controller actively transitioning');
|
|
}
|
|
|
|
this.setTransitioning(true, 500);
|
|
|
|
let resolve = null;
|
|
let promise = new Promise(res => { resolve = res; });
|
|
|
|
// default the direction to "back"
|
|
opts.direction = opts.direction || 'back';
|
|
|
|
// get the active view and set that it is staged to be leaving
|
|
// was probably the one popped from the stack
|
|
let leavingView = this.getActive() || new ViewController();
|
|
leavingView.shouldCache = (isBoolean(opts.cacheLeavingView) ? opts.cacheLeavingView : false);
|
|
leavingView.shouldDestroy = !leavingView.shouldCache;
|
|
if (leavingView.shouldDestroy) {
|
|
leavingView.willUnload();
|
|
}
|
|
|
|
// the entering view is now the new last view
|
|
// Note: we might not have an entering view if this is the
|
|
// only view on the history stack.
|
|
let enteringView = this.getPrevious(leavingView);
|
|
if (this.router) {
|
|
// notify router of the state change
|
|
this.router.stateChange('pop', enteringView);
|
|
}
|
|
|
|
// start the transition
|
|
this._transition(enteringView, leavingView, opts, resolve);
|
|
|
|
return promise;
|
|
}
|
|
|
|
/**
|
|
* @private
|
|
* Pop to a specific view in the history stack
|
|
* @param view {ViewController} to pop to
|
|
* @param {Object} [opts={}] Any options you want to use pass to transtion
|
|
*/
|
|
popTo(viewCtrl, opts = {}) {
|
|
|
|
// Get the target index of the view to pop to
|
|
let viewIndex = this._views.indexOf(viewCtrl);
|
|
let targetIndex = viewIndex + 1;
|
|
|
|
// Don't pop to the view if it wasn't found, or the target is beyond the view list
|
|
if (viewIndex < 0 || targetIndex > this._views.length - 1) {
|
|
return Promise.resolve();
|
|
}
|
|
|
|
// ensure the entering view is shown
|
|
this._renderView(viewCtrl, true);
|
|
|
|
let resolve = null;
|
|
let promise = new Promise(res => { resolve = res; });
|
|
opts.direction = opts.direction || 'back';
|
|
|
|
let leavingView = this.getActive() || new ViewController();
|
|
|
|
// get the views to auto remove without having to do a transiton for each
|
|
// the last view (the currently active one) will do a normal transition out
|
|
if (this._views.length > 1) {
|
|
let autoRemoveItems = this._views.slice(targetIndex, this._views.length);
|
|
let popView;
|
|
for (let i = 0; i < autoRemoveItems.length; i++) {
|
|
popView = autoRemoveItems[i];
|
|
popView.shouldDestroy = true;
|
|
popView.shouldCache = false;
|
|
popView.willUnload();
|
|
|
|
// only the leaving view should be shown, all others hide
|
|
this._renderView(popView, (popView === leavingView));
|
|
}
|
|
}
|
|
|
|
if (this.router) {
|
|
this.router.stateChange('pop', viewCtrl);
|
|
}
|
|
|
|
this._transition(viewCtrl, leavingView, opts, resolve);
|
|
|
|
return promise;
|
|
}
|
|
|
|
/**
|
|
* Similar to `pop()`, this method let's you navigate back to the root of the stack, no matter how many views that is
|
|
* @param {Object} [opts={}] Any options you want to use pass to transtion
|
|
*/
|
|
popToRoot(opts = {}) {
|
|
return this.popTo(this.first(), opts);
|
|
}
|
|
|
|
/**
|
|
* Inserts a view into the nav stack at the specified index.
|
|
* This is useful if you need to add a view at any point in your navigation stack
|
|
*
|
|
* ```typescript
|
|
* export class Detail {
|
|
* constructor(nav: NavController) {
|
|
* this.nav = nav;
|
|
* }
|
|
* insertView(){
|
|
* this.nav.insert(1,Info)
|
|
* }
|
|
* }
|
|
* ```
|
|
*
|
|
* This will insert the `Info` view into the second slot of our navigation stack
|
|
*
|
|
* @param {Number} index The index where you want to insert the view
|
|
* @param {Any} component The name of the component you want to insert into the nav stack
|
|
* @returns {Promise} Returns a promise when the view has been inserted into the navigation stack
|
|
*/
|
|
insert(index, componentType, params = {}, opts = {}) {
|
|
if (!componentType || index < 0) {
|
|
return Promise.reject('invalid insert');
|
|
}
|
|
|
|
// push it onto the end
|
|
if (index >= this._views.length) {
|
|
return this.push(componentType, params, opts);
|
|
}
|
|
|
|
// create new ViewController, but don't render yet
|
|
let viewCtrl = new ViewController(this, componentType, params);
|
|
viewCtrl.state = CACHED_STATE;
|
|
viewCtrl.shouldDestroy = false;
|
|
viewCtrl.shouldCache = false;
|
|
|
|
this._incrementId(viewCtrl);
|
|
this._views.splice(index, 0, viewCtrl);
|
|
|
|
this._cleanup();
|
|
|
|
return Promise.resolve();
|
|
}
|
|
|
|
/**
|
|
* Removes a view from the nav stack at the specified index.
|
|
*
|
|
* ```typescript
|
|
* export class Detail {
|
|
* constructor(nav: NavController) {
|
|
* this.nav = nav;
|
|
* }
|
|
* removeView(){
|
|
* this.nav.remove(1)
|
|
* }
|
|
* }
|
|
* ```
|
|
*
|
|
* @param {Number} index Remove the view from the nav stack at that index
|
|
* @param {Object} [opts={}] Any options you want to use pass to transtion
|
|
* @returns {Promise} Returns a promise when the view has been removed
|
|
*/
|
|
remove(index, opts = {}) {
|
|
if (index < 0 || index >= this._views.length) {
|
|
return Promise.reject("index out of range");
|
|
}
|
|
|
|
let viewToRemove = this._views[index];
|
|
if (this.isActive(viewToRemove)){
|
|
return this.pop(opts);
|
|
}
|
|
|
|
viewToRemove.shouldDestroy = true;
|
|
this._cleanup();
|
|
return Promise.resolve();
|
|
}
|
|
|
|
/**
|
|
* @private
|
|
*/
|
|
setViews(components, opts = {}) {
|
|
console.warn('setViews() deprecated, use setPages() instead');
|
|
this.setPages(components, opts);
|
|
}
|
|
|
|
/**
|
|
* You can set the views of the current navigation stack and navigate to the last view past
|
|
*
|
|
*
|
|
*```typescript
|
|
* import {Page, NavController} from 'ionic/ionic'
|
|
* import {Detail} from '../detail/detail'
|
|
* import {Info} from '../info/info'
|
|
*
|
|
* export class Home {
|
|
* constructor(nav: NavController) {
|
|
* this.nav = nav;
|
|
* }
|
|
* setView() {
|
|
* this.nav.setViews([List,Detail, Info]);
|
|
* }
|
|
* }
|
|
*```
|
|
*
|
|
*
|
|
*In this example, we're giving the current nav stack an array of pages. Then the navigation stack will navigate to the last view in the array and remove the orignal view you came from.
|
|
*
|
|
*By default, animations are disabled, but they can be enabled by passing options to the navigation controller
|
|
*
|
|
*
|
|
*```typescript
|
|
* import {Page, NavController} from 'ionic/ionic'
|
|
* import {Detail} from '../detail/detail'
|
|
* import {Info} from '../info/info'
|
|
*
|
|
* export class Home {
|
|
* constructor(nav: NavController) {
|
|
* this.nav = nav;
|
|
* }
|
|
* setView() {
|
|
* this.nav.setViews([List,Detail, Info],{
|
|
* animate: true
|
|
* });
|
|
* }
|
|
* }
|
|
*```
|
|
*
|
|
*
|
|
*You can also pass any navigation params to the individual pages in the array.
|
|
*
|
|
*
|
|
*```typescript
|
|
* import {Page, NavController} from 'ionic/ionic'
|
|
* import {Detail} from '../detail/detail'
|
|
* import {Info} from '../info/info'
|
|
*
|
|
* export class Home {
|
|
* constructor(nav: NavController) {
|
|
* this.nav = nav;
|
|
* }
|
|
* setView() {
|
|
* this.nav.setViews([{
|
|
* componentType: List,
|
|
* params: {id: 43}
|
|
* }, {
|
|
* componentType: Detail,
|
|
* params: {id: 45}
|
|
* },{
|
|
* componentType: Info,
|
|
* params: {id: 5}
|
|
* } ],{
|
|
* animate: true
|
|
* });
|
|
* }
|
|
* }
|
|
*```
|
|
*
|
|
* @param {Array} component an arry of components to load in the stack
|
|
* @param {Object} [opts={}] Any options you want to use pass
|
|
* @returns {Promise} Returns a promise when the views are set
|
|
*/
|
|
setPages(components, opts = {}) {
|
|
if (!components || !components.length) {
|
|
return Promise.resolve();
|
|
}
|
|
|
|
let leavingView = this.getActive() || new ViewController();
|
|
|
|
// if animate has not been set then default to false
|
|
opts.animate = opts.animate || false;
|
|
|
|
// ensure leaving views are not cached, and should be destroyed
|
|
opts.cacheLeavingView = false;
|
|
|
|
// get the views to auto remove without having to do a transiton for each
|
|
// the last view (the currently active one) will do a normal transition out
|
|
if (this._views.length > 1) {
|
|
let autoRemoveItems = this._views.slice(0, this._views.length - 1);
|
|
let popView;
|
|
for (let i = 0; i < autoRemoveItems.length; i++) {
|
|
popView = autoRemoveItems[i];
|
|
popView.shouldDestroy = true;
|
|
popView.shouldCache = false;
|
|
popView.willUnload();
|
|
|
|
if (opts.animate) {
|
|
// only the leaving view should be shown, all others hide
|
|
this._renderView(popView, (popView === leavingView));
|
|
}
|
|
}
|
|
}
|
|
|
|
let componentObj = null;
|
|
let componentType = null;
|
|
let viewCtrl = null;
|
|
|
|
// create the ViewControllers that go before the new active ViewController
|
|
// in the stack, but the previous views shouldn't render yet
|
|
if (components.length > 1) {
|
|
let newBeforeItems = components.slice(0, components.length - 1);
|
|
for (let j = 0; j < newBeforeItems.length; j++) {
|
|
componentObj = newBeforeItems[j];
|
|
|
|
if (componentObj) {
|
|
// could be an object with a componentType property, or it is a componentType
|
|
componentType = componentObj.componentType || componentObj;
|
|
|
|
viewCtrl = new ViewController(this, componentType, componentObj.params);
|
|
viewCtrl.state = CACHED_STATE;
|
|
viewCtrl.shouldDestroy = false;
|
|
viewCtrl.shouldCache = false;
|
|
|
|
// add the item to the stack
|
|
this._add(viewCtrl);
|
|
}
|
|
}
|
|
}
|
|
|
|
// get the component that will become the active item
|
|
// it'll be the last one in the given components array
|
|
componentObj = components[ components.length - 1 ];
|
|
componentType = componentObj.componentType || componentObj;
|
|
|
|
// transition the leaving and entering
|
|
return this.push(componentType, componentObj.params, opts);
|
|
}
|
|
|
|
/**
|
|
* Set the root for the current navigation stack
|
|
* @param {Component} The name of the component you want to push on the navigation stack
|
|
* @param {Object} [params={}] Any nav-params you want to pass along to the next view
|
|
* @param {Object} [opts={}] Any options you want to use pass to transtion
|
|
* @returns {Promise} Returns a promise when done
|
|
*/
|
|
setRoot(componentType, params = {}, opts = {}) {
|
|
return this.setPages([{
|
|
componentType,
|
|
params
|
|
}], opts);
|
|
}
|
|
|
|
/**
|
|
*
|
|
* @private
|
|
* @param {TODO} enteringView TODO
|
|
* @param {TODO} leavingView TODO
|
|
* @param {TODO} opts TODO
|
|
* @param {Function} done TODO
|
|
* @returns {any} TODO
|
|
*/
|
|
_transition(enteringView, leavingView, opts, done) {
|
|
if (enteringView === leavingView) {
|
|
return done(enteringView);
|
|
}
|
|
|
|
if (!opts.animation) {
|
|
opts.animation = this.config.get('pageTransition');
|
|
}
|
|
if (this.config.get('animate') === false) {
|
|
opts.animate = false;
|
|
}
|
|
|
|
if (!enteringView) {
|
|
// if no entering view then create a bogus one
|
|
enteringView = new ViewController()
|
|
enteringView.loaded();
|
|
}
|
|
|
|
console.time('_transition ' + (enteringView.componentType && enteringView.componentType.name));
|
|
|
|
this._stage(enteringView, opts, () => {
|
|
if (enteringView.shouldDestroy) {
|
|
// already marked as a view that will be destroyed, don't continue
|
|
return done(enteringView);
|
|
}
|
|
|
|
this._cd.detectChanges();
|
|
|
|
this._zone.runOutsideAngular(() => {
|
|
this._setZIndex(enteringView, leavingView, opts.direction);
|
|
|
|
enteringView.shouldDestroy = false;
|
|
enteringView.shouldCache = false;
|
|
|
|
this._postRender(enteringView, opts, () => {
|
|
|
|
if (!opts.preload) {
|
|
enteringView.willEnter();
|
|
leavingView.willLeave();
|
|
}
|
|
|
|
// set that the new view pushed on the stack is staged to be entering/leaving
|
|
// staged state is important for the transition to find the correct view
|
|
enteringView.state = STAGED_ENTERING_STATE;
|
|
leavingView.state = STAGED_LEAVING_STATE;
|
|
|
|
// init the transition animation
|
|
opts.renderDelay = opts.transitionDelay || this.config.get('pageTransitionDelay');
|
|
|
|
let transAnimation = Animation.createTransition(this._getStagedEntering(),
|
|
this._getStagedLeaving(),
|
|
opts);
|
|
if (opts.animate === false) {
|
|
// force it to not animate the elements, just apply the "to" styles
|
|
transAnimation.clearDuration();
|
|
transAnimation.duration(0);
|
|
}
|
|
|
|
let duration = transAnimation.duration();
|
|
let enableApp = (duration < 64);
|
|
// block any clicks during the transition and provide a
|
|
// fallback to remove the clickblock if something goes wrong
|
|
this.app.setEnabled(enableApp, duration);
|
|
this.setTransitioning(!enableApp, duration);
|
|
|
|
if (opts.pageType) {
|
|
transAnimation.before.addClass(opts.pageType);
|
|
}
|
|
|
|
// start the transition
|
|
transAnimation.play(() => {
|
|
// transition has completed, update each view's state
|
|
enteringView.state = ACTIVE_STATE;
|
|
leavingView.state = CACHED_STATE;
|
|
|
|
// dispose any views that shouldn't stay around
|
|
transAnimation.dispose();
|
|
|
|
if (!opts.preload) {
|
|
enteringView.didEnter();
|
|
leavingView.didLeave();
|
|
}
|
|
|
|
this._zone.run(() => {
|
|
if (this.keyboard.isOpen()) {
|
|
this.keyboard.onClose(() => {
|
|
this._transComplete();
|
|
console.timeEnd('_transition ' + (enteringView.componentType && enteringView.componentType.name));
|
|
done(enteringView);
|
|
}, 32);
|
|
|
|
} else {
|
|
this._transComplete();
|
|
console.timeEnd('_transition ' + (enteringView.componentType && enteringView.componentType.name));
|
|
done(enteringView);
|
|
}
|
|
});
|
|
});
|
|
});
|
|
|
|
});
|
|
|
|
});
|
|
}
|
|
|
|
/**
|
|
* @private
|
|
*/
|
|
_stage(viewCtrl, opts, done) {
|
|
if (viewCtrl.isLoaded() || viewCtrl.shouldDestroy) {
|
|
// already compiled this view
|
|
return done();
|
|
}
|
|
|
|
// get the pane the NavController wants to use
|
|
// the pane is where all this content will be placed into
|
|
this.loadPage(viewCtrl, null, opts, () => {
|
|
if (viewCtrl.onReady) {
|
|
viewCtrl.onReady(() => {
|
|
viewCtrl.loaded();
|
|
done();
|
|
});
|
|
|
|
} else {
|
|
viewCtrl.loaded();
|
|
done();
|
|
}
|
|
});
|
|
}
|
|
|
|
/**
|
|
* @private
|
|
*/
|
|
loadPage(viewCtrl, navbarContainerRef, opts, done) {
|
|
// guts of DynamicComponentLoader#loadIntoLocation
|
|
this._compiler.compileInHost(viewCtrl.componentType).then(hostProtoViewRef => {
|
|
let wtfScope = NavController._loadPageScope();
|
|
|
|
let providers = this.providers.concat(Injector.resolve([
|
|
provide(ViewController, {useValue: viewCtrl}),
|
|
provide(NavParams, {useValue: viewCtrl.params})
|
|
]));
|
|
|
|
let location = this._viewManager.getNamedElementInComponentView(this.elementRef, 'contents');
|
|
|
|
let viewContainer = this._viewManager.getViewContainer(location);
|
|
let hostViewRef =
|
|
viewContainer.createHostView(hostProtoViewRef, viewContainer.length, providers);
|
|
let newLocation = this._viewManager.getHostElement(hostViewRef);
|
|
let component = this._viewManager.getComponent(newLocation);
|
|
|
|
viewCtrl.addDestroy(() => {
|
|
let index = viewContainer.indexOf(hostViewRef);
|
|
if (index !== -1) {
|
|
viewContainer.remove(index);
|
|
}
|
|
});
|
|
|
|
// a new ComponentRef has been created
|
|
// set the ComponentRef's instance to this ViewController
|
|
viewCtrl.setInstance(component);
|
|
|
|
// remember the ElementRef to the ion-page elementRef that was just created
|
|
viewCtrl.setPageRef(newLocation);
|
|
|
|
if (!navbarContainerRef) {
|
|
navbarContainerRef = viewCtrl.getNavbarViewRef();
|
|
}
|
|
|
|
let navbarTemplateRef = viewCtrl.getNavbarTemplateRef();
|
|
if (navbarContainerRef && navbarTemplateRef) {
|
|
let navbarView = navbarContainerRef.createEmbeddedView(navbarTemplateRef);
|
|
|
|
viewCtrl.addDestroy(() => {
|
|
let index = navbarContainerRef.indexOf(navbarView);
|
|
if (index > -1) {
|
|
navbarContainerRef.remove(index);
|
|
}
|
|
});
|
|
}
|
|
|
|
opts.postLoad && opts.postLoad(viewCtrl);
|
|
|
|
if (this._views.length === 1) {
|
|
this._zone.runOutsideAngular(() => {
|
|
rafFrames(38, () => {
|
|
this._renderer.setElementClass(this.elementRef, 'has-views', true);
|
|
});
|
|
});
|
|
}
|
|
|
|
wtfLeave(wtfScope);
|
|
|
|
done(viewCtrl);
|
|
});
|
|
}
|
|
|
|
_postRender(enteringView, opts, done) {
|
|
enteringView.postRender();
|
|
|
|
if (opts.animate === false) {
|
|
done();
|
|
|
|
} else {
|
|
rafFrames(2, done);
|
|
}
|
|
}
|
|
|
|
_setZIndex(enteringView, leavingView, direction) {
|
|
let enteringPageRef = enteringView && enteringView.pageRef();
|
|
if (enteringPageRef) {
|
|
if (!leavingView || !leavingView.isLoaded()) {
|
|
enteringView.zIndex = 10;
|
|
|
|
} else if (direction === 'back') {
|
|
// moving back
|
|
enteringView.zIndex = leavingView.zIndex - 1;
|
|
|
|
} else {
|
|
// moving forward
|
|
enteringView.zIndex = leavingView.zIndex + 1;
|
|
}
|
|
|
|
if (enteringView.zIndex !== enteringView._zIndex) {
|
|
this._renderer.setElementStyle(enteringPageRef, 'z-index', enteringView.zIndex);
|
|
enteringView._zIndex = enteringView.zIndex;
|
|
}
|
|
}
|
|
}
|
|
|
|
_renderView(viewCtrl, shouldShow) {
|
|
// using hidden element attribute to display:none and not render views
|
|
// renderAttr of '' means the hidden attribute will be added
|
|
// renderAttr of null means the hidden attribute will be removed
|
|
// doing checks to make sure we only make an update to the element when needed
|
|
if (shouldShow && viewCtrl._hdnAttr === '' ||
|
|
!shouldShow && viewCtrl._hdnAttr !== '') {
|
|
viewCtrl._hdnAttr = (shouldShow ? null : '');
|
|
this._renderer.setElementAttribute(viewCtrl.pageRef(), 'hidden', viewCtrl._hdnAttr);
|
|
|
|
let navbarRef = viewCtrl.navbarRef();
|
|
if (navbarRef) {
|
|
this._renderer.setElementAttribute(navbarRef, 'hidden', viewCtrl._hdnAttr);
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* @private
|
|
*/
|
|
swipeBackStart() {
|
|
return;
|
|
if (!this.app.isEnabled() || !this.canSwipeBack()) {
|
|
return;
|
|
}
|
|
|
|
// disables the app during the transition
|
|
this.app.setEnabled(false);
|
|
this.setTransitioning(true);
|
|
|
|
// default the direction to "back"
|
|
let opts = {
|
|
direction: 'back'
|
|
};
|
|
|
|
// get the active view and set that it is staged to be leaving
|
|
// was probably the one popped from the stack
|
|
let leavingView = this.getActive() || new ViewController();
|
|
leavingView.shouldDestroy = true;
|
|
leavingView.shouldCache = false;
|
|
leavingView.willLeave();
|
|
leavingView.willUnload();
|
|
|
|
// the entering view is now the new last view
|
|
let enteringView = this.getPrevious(leavingView);
|
|
enteringView.shouldDestroy = false;
|
|
enteringView.shouldCache = false;
|
|
enteringView.willEnter();
|
|
|
|
// wait for the new view to complete setup
|
|
enteringView._stage(enteringView, {}, () => {
|
|
|
|
this._zone.runOutsideAngular(() => {
|
|
// set that the new view pushed on the stack is staged to be entering/leaving
|
|
// staged state is important for the transition to find the correct view
|
|
enteringView.state = STAGED_ENTERING_STATE;
|
|
leavingView.state = STAGED_LEAVING_STATE;
|
|
|
|
// init the swipe back transition animation
|
|
this._sbTrans = Transition.create(this, opts);
|
|
this._sbTrans.easing('linear').progressStart();
|
|
|
|
});
|
|
});
|
|
|
|
}
|
|
|
|
/**
|
|
* @private
|
|
*/
|
|
swipeBackProgress(value) {
|
|
return;
|
|
if (this._sbTrans) {
|
|
// continue to disable the app while actively dragging
|
|
this.app.setEnabled(false, 4000);
|
|
this.setTransitioning(true, 4000);
|
|
|
|
// set the transition animation's progress
|
|
this._sbTrans.progress(value);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* @private
|
|
*/
|
|
swipeBackEnd(completeSwipeBack, rate) {
|
|
return;
|
|
if (!this._sbTrans) return;
|
|
|
|
// disables the app during the transition
|
|
this.app.setEnabled(false);
|
|
this.setTransitioning(true);
|
|
|
|
this._sbTrans.progressEnd(completeSwipeBack, rate).then(() => {
|
|
|
|
this._zone.run(() => {
|
|
// find the views that were entering and leaving
|
|
let enteringView = this._getStagedEntering();
|
|
let leavingView = this._getStagedLeaving();
|
|
|
|
if (enteringView && leavingView) {
|
|
// finish up the animation
|
|
|
|
if (completeSwipeBack) {
|
|
// swipe back has completed navigating back
|
|
// update each view's state
|
|
enteringView.state = ACTIVE_STATE;
|
|
leavingView.state = CACHED_STATE;
|
|
|
|
enteringView.didEnter();
|
|
leavingView.didLeave();
|
|
|
|
if (this.router) {
|
|
// notify router of the pop state change
|
|
this.router.stateChange('pop', enteringView);
|
|
}
|
|
|
|
} else {
|
|
// cancelled the swipe back, they didn't end up going back
|
|
// return views to their original state
|
|
leavingView.state = ACTIVE_STATE;
|
|
enteringView.state = CACHED_STATE;
|
|
|
|
leavingView.willEnter();
|
|
leavingView.didEnter();
|
|
enteringView.didLeave();
|
|
|
|
leavingView.shouldDestroy = false;
|
|
enteringView.shouldDestroy = false;
|
|
}
|
|
}
|
|
|
|
// empty out and dispose the swipe back transition animation
|
|
this._sbTrans && this._sbTrans.dispose();
|
|
this._sbTrans = null;
|
|
|
|
// all done!
|
|
this._transComplete();
|
|
|
|
});
|
|
});
|
|
|
|
}
|
|
|
|
/**
|
|
* @private
|
|
*/
|
|
_sbComplete() {
|
|
return;
|
|
if (this.canSwipeBack()) {
|
|
// it is possible to swipe back
|
|
|
|
if (this.sbGesture) {
|
|
// this is already an active gesture, don't create another one
|
|
return;
|
|
}
|
|
|
|
let opts = {
|
|
edge: 'left',
|
|
threshold: this._sbThreshold
|
|
};
|
|
this.sbGesture = new SwipeBackGesture(this.getNativeElement(), opts, this);
|
|
console.debug('SwipeBackGesture listen');
|
|
this.sbGesture.listen();
|
|
|
|
|
|
} else if (this.sbGesture) {
|
|
// it is not possible to swipe back and there is an
|
|
// active sbGesture, so unlisten it
|
|
console.debug('SwipeBackGesture unlisten');
|
|
this.sbGesture.unlisten();
|
|
this.sbGesture = null;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Check to see if swipe-to-go-back is enabled
|
|
* @param {boolean=} isSwipeBackEnabled Set whether or not swipe-to-go-back is enabled
|
|
* @returns {boolean} Whether swipe-to-go-back is enabled
|
|
*/
|
|
isSwipeBackEnabled(val) {
|
|
if (arguments.length) {
|
|
this._sbEnabled = !!val;
|
|
}
|
|
return this._sbEnabled;
|
|
}
|
|
|
|
/**
|
|
* If it's possible to use swipe back or not. If it's not possible
|
|
* to go back, or swipe back is not enable then this will return false.
|
|
* If it is possible to go back, and swipe back is enabled, then this
|
|
* will return true.
|
|
* @returns {boolean} Whether you can swipe to go back
|
|
*/
|
|
canSwipeBack() {
|
|
return (this._sbEnabled && this.canGoBack());
|
|
}
|
|
|
|
/**
|
|
* Returns `true` if there's a valid previous view that we can pop back to.
|
|
* Otherwise returns false.
|
|
* @returns {boolean} Whether there is a view to go back to
|
|
*/
|
|
canGoBack() {
|
|
let activeView = this.getActive();
|
|
if (activeView) {
|
|
return activeView.enableBack();
|
|
}
|
|
return false;
|
|
}
|
|
|
|
/**
|
|
* @private
|
|
*/
|
|
_transComplete() {
|
|
let wtfScope = NavController._transCompleteScope();
|
|
|
|
this._views.forEach(view => {
|
|
if (view) {
|
|
if (view.shouldDestroy) {
|
|
view.didUnload();
|
|
|
|
} else if (view.state === CACHED_STATE && view.shouldCache) {
|
|
view.shouldCache = false;
|
|
}
|
|
}
|
|
});
|
|
|
|
// allow clicks again, but still set an enable time
|
|
// meaning nothing with this view controller can happen for XXms
|
|
this.app.setEnabled(true);
|
|
this.setTransitioning(false);
|
|
|
|
this._sbComplete();
|
|
|
|
this._cleanup();
|
|
|
|
wtfLeave(wtfScope);
|
|
}
|
|
|
|
/**
|
|
* @private
|
|
*/
|
|
_cleanup(activeView) {
|
|
// the active view, and the previous view, should be rendered in dom and ready to go
|
|
// all others, like a cached page 2 back, should be display: none and not rendered
|
|
let destroys = [];
|
|
activeView = activeView || this.getActive();
|
|
let previousView = this.getPrevious(activeView);
|
|
|
|
this._views.forEach(view => {
|
|
if (view) {
|
|
if (view.shouldDestroy) {
|
|
destroys.push(view);
|
|
|
|
} else if (view.isLoaded()) {
|
|
let shouldShow = (view === activeView) || (view === previousView);
|
|
this._renderView(view, shouldShow);
|
|
}
|
|
}
|
|
});
|
|
|
|
// all views being destroyed should be removed from the list of views
|
|
// and completely removed from the dom
|
|
destroys.forEach(view => {
|
|
this._remove(view);
|
|
view.destroy();
|
|
});
|
|
}
|
|
|
|
/**
|
|
* @private
|
|
*/
|
|
navbarViewContainer(nbContainer) {
|
|
if (nbContainer) {
|
|
this._nbContainer = nbContainer;
|
|
}
|
|
if (this._nbContainer) {
|
|
return this._nbContainer;
|
|
}
|
|
if (this.parent) {
|
|
return this.parent.navbarViewContainer();
|
|
}
|
|
}
|
|
|
|
/**
|
|
* @private
|
|
* @returns {TODO} TODO
|
|
*/
|
|
anchorElementRef() {
|
|
if (arguments.length) {
|
|
this._anchorER = arguments[0];
|
|
}
|
|
return this._anchorER;
|
|
}
|
|
|
|
/**
|
|
* @private
|
|
*/
|
|
_add(viewCtrl) {
|
|
this._incrementId(viewCtrl);
|
|
this._views.push(viewCtrl);
|
|
}
|
|
|
|
/**
|
|
* @private
|
|
*/
|
|
_incrementId(viewCtrl) {
|
|
viewCtrl.id = this.id + '-' + (++this._ids);
|
|
}
|
|
|
|
/**
|
|
* @private
|
|
*/
|
|
_remove(viewOrIndex) {
|
|
array.remove(this._views, viewOrIndex);
|
|
}
|
|
|
|
/**
|
|
* @private
|
|
*/
|
|
_getStagedEntering() {
|
|
for (let i = 0, ii = this._views.length; i < ii; i++) {
|
|
if (this._views[i].state === STAGED_ENTERING_STATE) {
|
|
return this._views[i];
|
|
}
|
|
}
|
|
return null;
|
|
}
|
|
|
|
/**
|
|
* @private
|
|
*/
|
|
_getStagedLeaving() {
|
|
for (let i = 0, ii = this._views.length; i < ii; i++) {
|
|
if (this._views[i].state === STAGED_LEAVING_STATE) {
|
|
return this._views[i];
|
|
}
|
|
}
|
|
return null;
|
|
}
|
|
|
|
/**
|
|
* @private
|
|
* @returns {Component} TODO
|
|
*/
|
|
getActive() {
|
|
for (let i = this._views.length - 1; i >= 0; i--) {
|
|
if (this._views[i].state === ACTIVE_STATE && !this._views[i].shouldDestroy) {
|
|
return this._views[i];
|
|
}
|
|
}
|
|
return null;
|
|
}
|
|
|
|
/**
|
|
* @param {Index} The index of the view you want to get
|
|
* @returns {Component} Returns the component that matches the index given
|
|
*/
|
|
getByIndex(index) {
|
|
if (index < this._views.length && index > -1) {
|
|
return this._views[index];
|
|
}
|
|
return null;
|
|
}
|
|
|
|
/**
|
|
* @private
|
|
* @param {Handle} The handle of the view you want to get
|
|
* @returns {Component} Returns the component that matches the handle given
|
|
*/
|
|
getByHandle(handle) {
|
|
for (let i = 0, ii = this._views.length; i < ii; i++) {
|
|
if (this._views[i].handle === handle) {
|
|
return this._views[i];
|
|
}
|
|
}
|
|
return null;
|
|
}
|
|
|
|
/**
|
|
* @private
|
|
* @param {TODO} pageType TODO
|
|
* @returns {TODO} TODO
|
|
*/
|
|
getByType(pageType) {
|
|
for (let i = 0, ii = this._views.length; i < ii; i++) {
|
|
if (this._views[i].pageType === pageType) {
|
|
return this._views[i];
|
|
}
|
|
}
|
|
return null;
|
|
}
|
|
|
|
/**
|
|
* @private
|
|
* @param {TODO} view TODO
|
|
* @returns {TODO} TODO
|
|
*/
|
|
getPrevious(viewCtrl) {
|
|
if (viewCtrl) {
|
|
let viewIndex = this._views.indexOf(viewCtrl);
|
|
|
|
for (let i = viewIndex - 1; i >= 0; i--) {
|
|
if (!this._views[i].shouldDestroy) {
|
|
return this._views[i];
|
|
}
|
|
}
|
|
}
|
|
return null;
|
|
}
|
|
|
|
/**
|
|
* First view in this nav controller's stack. This would
|
|
* not return an view which is about to be destroyed.
|
|
* @returns {Component} Returns the first component view in the current stack
|
|
*/
|
|
first() {
|
|
for (let i = 0, l = this._views.length; i < l; i++) {
|
|
if (!this._views[i].shouldDestroy) {
|
|
return this._views[i];
|
|
}
|
|
}
|
|
return null;
|
|
}
|
|
|
|
/**
|
|
* Last view in this nav controller's stack. This would
|
|
* not return an view which is about to be destroyed.
|
|
* @returns {Component} Returns the last component view in the current stack
|
|
*/
|
|
last() {
|
|
for (let i = this._views.length - 1; i >= 0; i--) {
|
|
if (!this._views[i].shouldDestroy) {
|
|
return this._views[i];
|
|
}
|
|
}
|
|
return null;
|
|
}
|
|
|
|
/**
|
|
* @private
|
|
* @param {TODO} view TODO
|
|
* @returns {TODO} TODO
|
|
*/
|
|
indexOf(viewCtrl) {
|
|
return this._views.indexOf(viewCtrl);
|
|
}
|
|
|
|
/**
|
|
* Number of sibling views in the nav controller. This does
|
|
* not include views which are about to be destroyed.
|
|
* @returns {Number} The number of views in stack, including the current view
|
|
*/
|
|
length() {
|
|
let len = 0;
|
|
for (let i = 0, l = this._views.length; i < l; i++) {
|
|
if (!this._views[i].shouldDestroy) {
|
|
len++;
|
|
}
|
|
}
|
|
return len;
|
|
}
|
|
|
|
/**
|
|
* @private
|
|
* IS RETURNING UNDEFIND
|
|
* @param {TODO} view TODO
|
|
* @returns {TODO} TODO
|
|
*/
|
|
isActive(viewCtrl) {
|
|
return (viewCtrl && viewCtrl.state === ACTIVE_STATE);
|
|
}
|
|
|
|
/**
|
|
* @private
|
|
* @param {TODO} router TODO
|
|
*/
|
|
registerRouter(router) {
|
|
this.router = router;
|
|
}
|
|
|
|
}
|
|
|
|
const ACTIVE_STATE = 1;
|
|
const CACHED_STATE = 2;
|
|
const STAGED_ENTERING_STATE = 3;
|
|
const STAGED_LEAVING_STATE = 4;
|
|
|
|
let ctrlIds = -1;
|
|
|
|
|
|
/**
|
|
* TODO
|
|
*/
|
|
export class NavParams {
|
|
/**
|
|
* @private
|
|
* @param {TODO} data TODO
|
|
*/
|
|
constructor(data) {
|
|
this.data = data || {};
|
|
}
|
|
|
|
/**
|
|
* TODO
|
|
* @param {string} Which param you want to look up
|
|
*/
|
|
get(param) {
|
|
return this.data[param];
|
|
}
|
|
}
|