mirror of
https://github.com/ionic-team/ionic-framework.git
synced 2025-08-16 10:01:59 +08:00
refactor(navigation): get rid of ion-nav-controller, get nav working correctly in DOM, angular, react
This commit is contained in:
@ -1,17 +1,23 @@
|
||||
import {
|
||||
ChangeDetectorRef,
|
||||
Component,
|
||||
OnInit,
|
||||
ComponentFactoryResolver,
|
||||
ComponentRef,
|
||||
ElementRef,
|
||||
Injector,
|
||||
ReflectiveInjector,
|
||||
Type,
|
||||
ViewContainerRef,
|
||||
ViewChild
|
||||
} from '@angular/core';
|
||||
|
||||
import { PublicNav } from '@ionic/core';
|
||||
import { FrameworkDelegate } from '@ionic/core';
|
||||
|
||||
import { getProviders } from '../di/di';
|
||||
import { AngularFrameworkDelegate } from '../providers/angular-framework-delegate';
|
||||
import { AngularViewController } from '../types/angular-view-controller';
|
||||
import { AngularComponentMounter } from '../providers/angular-component-mounter';
|
||||
import { AngularMountingData } from '../types/interfaces';
|
||||
|
||||
const elementToComponentRefMap = new Map<HTMLElement, ComponentRef<any>>();
|
||||
|
||||
@Component({
|
||||
selector: 'ion-nav',
|
||||
@ -19,41 +25,41 @@ import { AngularViewController } from '../types/angular-view-controller';
|
||||
<div #viewport class="ng-nav-viewport"></div>
|
||||
`
|
||||
})
|
||||
export class IonNavDelegate implements OnInit {
|
||||
export class IonNavDelegate implements FrameworkDelegate {
|
||||
|
||||
@ViewChild('viewport', { read: ViewContainerRef}) viewport: ViewContainerRef;
|
||||
|
||||
constructor(private changeDetection: ChangeDetectorRef, private angularFrameworkDelegate: AngularFrameworkDelegate) {
|
||||
constructor(private elementRef: ElementRef, private changeDetection: ChangeDetectorRef, private angularComponentMounter: AngularComponentMounter, private injector: Injector, private componentResolveFactory: ComponentFactoryResolver) {
|
||||
this.elementRef.nativeElement.delegate = this;
|
||||
|
||||
}
|
||||
|
||||
ngOnInit() {
|
||||
const controllerElement = document.querySelector('ion-nav-controller') as any;
|
||||
controllerElement.delegate = this;
|
||||
async attachViewToDom(elementOrContainerToMountTo: HTMLIonNavElement, elementOrComponentToMount: Type<any>,
|
||||
_propsOrDataObj?: any, _classesToAdd?: string[]): Promise<AngularMountingData> {
|
||||
|
||||
const componentProviders = ReflectiveInjector.resolve(getProviders(elementOrContainerToMountTo));
|
||||
console.log('componentProviders: ', componentProviders);
|
||||
|
||||
const element = document.createElement('ion-page');
|
||||
for (const clazz of _classesToAdd) {
|
||||
element.classList.add(clazz);
|
||||
}
|
||||
|
||||
elementOrContainerToMountTo.appendChild(element);
|
||||
const mountingData = await this.angularComponentMounter.attachViewToDom(element, elementOrComponentToMount, [], this.changeDetection, this.componentResolveFactory, this.injector);
|
||||
mountingData.element = element;
|
||||
|
||||
elementToComponentRefMap.set(mountingData.angularHostElement, mountingData.componentRef);
|
||||
|
||||
return mountingData;
|
||||
}
|
||||
|
||||
attachViewToDom(nav: PublicNav, enteringView: AngularViewController): Promise<any> {
|
||||
|
||||
const componentProviders = ReflectiveInjector.resolve(getProviders(nav.element as HTMLIonNavElement));
|
||||
return this.angularFrameworkDelegate.attachViewToDom(enteringView.component, this.viewport, componentProviders, this.changeDetection).then((angularMountingData) => {
|
||||
|
||||
enteringView.componentFactory = angularMountingData.componentFactory;
|
||||
enteringView.injector = angularMountingData.childInjector;
|
||||
enteringView.componentRef = angularMountingData.componentRef;
|
||||
enteringView.instance = angularMountingData.componentRef.instance;
|
||||
enteringView.angularHostElement = angularMountingData.componentRef.location.nativeElement;
|
||||
enteringView.element = angularMountingData.componentRef.location.nativeElement.querySelector('ion-page');
|
||||
});
|
||||
}
|
||||
|
||||
removeViewFromDom(_nav: PublicNav, viewController: AngularViewController) {
|
||||
return this.angularFrameworkDelegate.removeViewFromDom((viewController as any).componentRef).then(() => {
|
||||
viewController.componentFactory = null;
|
||||
viewController.injector = null;
|
||||
viewController.componentRef = null;
|
||||
viewController.instance = null;
|
||||
viewController.angularHostElement = null;
|
||||
viewController.element = null;
|
||||
});
|
||||
async removeViewFromDom(_parentElement: HTMLElement, childElement: HTMLElement) {
|
||||
const componentRef = elementToComponentRefMap.get(childElement);
|
||||
if (componentRef) {
|
||||
return this.angularComponentMounter.removeViewFromDom(componentRef);
|
||||
}
|
||||
return Promise.resolve();
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -16,7 +16,7 @@ import { IonNavDelegate } from './components/ion-nav';
|
||||
/* Providers */
|
||||
import { ActionSheetController } from './providers/action-sheet-controller';
|
||||
import { AlertController } from './providers/alert-controller';
|
||||
import { AngularFrameworkDelegate } from './providers/angular-framework-delegate';
|
||||
import { AngularComponentMounter } from './providers/angular-component-mounter';
|
||||
import { LoadingController } from './providers/loading-controller';
|
||||
import { ToastController } from './providers/toast-controller';
|
||||
|
||||
@ -48,7 +48,7 @@ export class IonicAngularModule {
|
||||
providers: [
|
||||
AlertController,
|
||||
ActionSheetController,
|
||||
AngularFrameworkDelegate,
|
||||
AngularComponentMounter,
|
||||
LoadingController,
|
||||
ToastController
|
||||
]
|
||||
|
59
packages/angular/src/providers/angular-component-mounter.ts
Normal file
59
packages/angular/src/providers/angular-component-mounter.ts
Normal file
@ -0,0 +1,59 @@
|
||||
import {
|
||||
ChangeDetectorRef,
|
||||
ComponentFactoryResolver,
|
||||
ComponentRef,
|
||||
Injectable,
|
||||
Injector,
|
||||
NgZone,
|
||||
ReflectiveInjector,
|
||||
Type,
|
||||
} from '@angular/core';
|
||||
|
||||
import { AngularMountingData } from '../types/interfaces';
|
||||
|
||||
@Injectable()
|
||||
export class AngularComponentMounter {
|
||||
|
||||
constructor(private defaultCfr: ComponentFactoryResolver, private zone: NgZone) {
|
||||
}
|
||||
|
||||
attachViewToDom(parentElement: HTMLElement, componentToMount: Type<any>, providers: any[], changeDetection: ChangeDetectorRef, componentResolveFactory: ComponentFactoryResolver, injector: Injector): Promise<AngularMountingData> {
|
||||
|
||||
return new Promise((resolve) => {
|
||||
this.zone.run(() => {
|
||||
console.log('parentElement: ', parentElement);
|
||||
const crf = componentResolveFactory ? componentResolveFactory : this.defaultCfr;
|
||||
const mountingData = attachViewToDom(crf, componentToMount, parentElement, providers, changeDetection, injector);
|
||||
resolve(mountingData);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
removeViewFromDom(componentRef: ComponentRef<any>): Promise<any> {
|
||||
return new Promise((resolve) => {
|
||||
this.zone.run(() => {
|
||||
componentRef.destroy();
|
||||
resolve();
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export function attachViewToDom(crf: ComponentFactoryResolver, componentToMount: Type<any>, element: HTMLElement, providers: any, changeDetection: ChangeDetectorRef, injector: Injector): AngularMountingData {
|
||||
const componentFactory = crf.resolveComponentFactory(componentToMount);
|
||||
const componentProviders = ReflectiveInjector.resolve(providers);
|
||||
const childInjector = ReflectiveInjector.fromResolvedProviders(componentProviders, injector);
|
||||
const componentRef = componentFactory.create(childInjector, [], element);
|
||||
|
||||
changeDetection.detectChanges();
|
||||
|
||||
return {
|
||||
componentFactory,
|
||||
childInjector: childInjector,
|
||||
componentRef: componentRef,
|
||||
instance: componentRef.instance,
|
||||
angularHostElement: componentRef.location.nativeElement,
|
||||
element: componentRef.location.nativeElement,
|
||||
};
|
||||
}
|
||||
|
@ -1,60 +0,0 @@
|
||||
import {
|
||||
ChangeDetectorRef,
|
||||
ComponentFactoryResolver,
|
||||
ComponentRef,
|
||||
Injectable,
|
||||
Injector,
|
||||
NgZone,
|
||||
ReflectiveInjector,
|
||||
Type,
|
||||
ViewContainerRef,
|
||||
} from '@angular/core';
|
||||
|
||||
import { ComponentFactory } from '@angular/core/src/linker/component_factory';
|
||||
|
||||
@Injectable()
|
||||
export class AngularFrameworkDelegate {
|
||||
|
||||
constructor(private crf: ComponentFactoryResolver, private zone: NgZone) {
|
||||
}
|
||||
|
||||
attachViewToDom(componentToMount: Type<any>, viewport: ViewContainerRef, providers: any, changeDetection: ChangeDetectorRef): Promise<AngularMountingData> {
|
||||
|
||||
return new Promise((resolve) => {
|
||||
this.zone.run(() => {
|
||||
const mountingData = attachViewToDom(this.crf, componentToMount, viewport, providers, changeDetection);
|
||||
resolve(mountingData);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
removeViewFromDom(componentRef: ComponentRef<any>): Promise<any> {
|
||||
return new Promise((resolve) => {
|
||||
this.zone.run(() => {
|
||||
componentRef.destroy();
|
||||
resolve();
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export function attachViewToDom(crf: ComponentFactoryResolver, componentToMount: Type<any>, viewport: ViewContainerRef, providers: any, changeDetection: ChangeDetectorRef): AngularMountingData{
|
||||
const componentFactory = crf.resolveComponentFactory(componentToMount);
|
||||
const componentProviders = ReflectiveInjector.resolve(providers);
|
||||
const childInjector = ReflectiveInjector.fromResolvedProviders(componentProviders, viewport.parentInjector);
|
||||
const componentRef = componentFactory.create(childInjector, []);
|
||||
viewport.insert(componentRef.hostView, viewport.length);
|
||||
changeDetection.detectChanges();
|
||||
|
||||
return {
|
||||
componentFactory,
|
||||
childInjector: childInjector,
|
||||
componentRef: componentRef
|
||||
}
|
||||
}
|
||||
|
||||
export interface AngularMountingData {
|
||||
componentFactory: ComponentFactory<any>;
|
||||
childInjector: Injector;
|
||||
componentRef: ComponentRef<any>;
|
||||
}
|
@ -4,12 +4,12 @@ import {
|
||||
Injector
|
||||
} from '@angular/core';
|
||||
|
||||
import { PublicViewController } from '@ionic/core';
|
||||
import { FrameworkMountingData } from '@ionic/core';
|
||||
|
||||
export interface AngularViewController extends PublicViewController {
|
||||
export interface AngularMountingData extends FrameworkMountingData {
|
||||
componentFactory?: ComponentFactory<any>;
|
||||
injector?: Injector;
|
||||
childInjector?: Injector;
|
||||
componentRef?: ComponentRef<any>;
|
||||
instance?: any;
|
||||
angularHostElement?: HTMLElement;
|
||||
}
|
||||
}
|
30
packages/core/src/components.d.ts
vendored
30
packages/core/src/components.d.ts
vendored
@ -1635,36 +1635,6 @@ declare global {
|
||||
}
|
||||
|
||||
|
||||
import {
|
||||
NavController as IonNavController
|
||||
} from './components/nav-controller/nav-controller';
|
||||
|
||||
declare global {
|
||||
interface HTMLIonNavControllerElement extends IonNavController, HTMLElement {
|
||||
}
|
||||
var HTMLIonNavControllerElement: {
|
||||
prototype: HTMLIonNavControllerElement;
|
||||
new (): HTMLIonNavControllerElement;
|
||||
};
|
||||
interface HTMLElementTagNameMap {
|
||||
"ion-nav-controller": HTMLIonNavControllerElement;
|
||||
}
|
||||
interface ElementTagNameMap {
|
||||
"ion-nav-controller": HTMLIonNavControllerElement;
|
||||
}
|
||||
namespace JSX {
|
||||
interface IntrinsicElements {
|
||||
"ion-nav-controller": JSXElements.IonNavControllerAttributes;
|
||||
}
|
||||
}
|
||||
namespace JSXElements {
|
||||
export interface IonNavControllerAttributes extends HTMLAttributes {
|
||||
delegate?: FrameworkDelegate;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
import {
|
||||
Nav as IonNav
|
||||
} from './components/nav/nav';
|
||||
|
@ -1,126 +0,0 @@
|
||||
import { Component, Element, Method, Prop } from '@stencil/core';
|
||||
|
||||
import { Animation, AnimationController, ComponentDataPair, FrameworkDelegate, Nav, NavOptions, ViewController } from '../../index';
|
||||
|
||||
import { DomFrameworkDelegate } from './dom-framework-delegate';
|
||||
|
||||
import {
|
||||
insert as insertImpl,
|
||||
insertPages as insertPagesImpl,
|
||||
pop as popImpl,
|
||||
popTo as popToImpl,
|
||||
popToRoot as popToRootImpl,
|
||||
push as pushImpl,
|
||||
remove as removeImpl,
|
||||
removeView as removeViewImpl,
|
||||
setPages as setPagesImpl,
|
||||
setRoot as setRootImpl,
|
||||
} from '../../navigation/nav-controller-functions';
|
||||
|
||||
let defaultDelegate: FrameworkDelegate = null;
|
||||
|
||||
@Component({
|
||||
tag: 'ion-nav-controller',
|
||||
})
|
||||
export class NavController {
|
||||
|
||||
@Element() element: HTMLElement;
|
||||
@Prop() delegate: FrameworkDelegate;
|
||||
@Prop({ connect: 'ion-animation-controller' }) animationCtrl: AnimationController;
|
||||
|
||||
|
||||
constructor() {
|
||||
}
|
||||
|
||||
@Method()
|
||||
push(nav: Nav, component: any, data?: any, opts?: NavOptions): Promise<any> {
|
||||
return hydrateDelegateAndAnimation(this).then(([delegate, animation]) => {
|
||||
return pushImpl(nav, delegate, animation, component, data, opts);
|
||||
});
|
||||
}
|
||||
|
||||
@Method()
|
||||
pop(nav: Nav, opts?: NavOptions): Promise<any> {
|
||||
return hydrateDelegateAndAnimation(this).then(([delegate, animation]) => {
|
||||
return popImpl(nav, delegate, animation, opts);
|
||||
});
|
||||
}
|
||||
|
||||
@Method()
|
||||
setRoot(nav: Nav, component: any, data?: any, opts?: NavOptions): Promise<any> {
|
||||
return hydrateDelegateAndAnimation(this).then(([delegate, animation]) => {
|
||||
return setRootImpl(nav, delegate, animation, component, data, opts);
|
||||
});
|
||||
}
|
||||
|
||||
@Method()
|
||||
insert(nav: Nav, insertIndex: number, page: any, params?: any, opts?: NavOptions): Promise<any> {
|
||||
return hydrateDelegateAndAnimation(this).then(([delegate, animation]) => {
|
||||
return insertImpl(nav, delegate, animation, insertIndex, page, params, opts);
|
||||
});
|
||||
}
|
||||
|
||||
@Method()
|
||||
insertPages(nav: Nav, insertIndex: number, insertPages: any[], opts?: NavOptions): Promise<any> {
|
||||
return hydrateDelegateAndAnimation(this).then(([delegate, animation]) => {
|
||||
return insertPagesImpl(nav, delegate, animation, insertIndex, insertPages, opts);
|
||||
});
|
||||
}
|
||||
|
||||
@Method()
|
||||
popToRoot(nav: Nav, opts?: NavOptions): Promise<any> {
|
||||
return hydrateDelegateAndAnimation(this).then(([delegate, animation]) => {
|
||||
return popToRootImpl(nav, delegate, animation, opts);
|
||||
});
|
||||
}
|
||||
|
||||
@Method()
|
||||
popTo(nav: Nav, indexOrViewCtrl: any, opts?: NavOptions): Promise<any> {
|
||||
return hydrateDelegateAndAnimation(this).then(([delegate, animation]) => {
|
||||
return popToImpl(nav, delegate, animation, indexOrViewCtrl, opts);
|
||||
});
|
||||
}
|
||||
|
||||
@Method()
|
||||
removeIndex(nav: Nav, startIndex: number, removeCount?: number, opts?: NavOptions): Promise<any> {
|
||||
return hydrateDelegateAndAnimation(this).then(([delegate, animation]) => {
|
||||
return removeImpl(nav, delegate, animation, startIndex, removeCount, opts);
|
||||
});
|
||||
}
|
||||
|
||||
@Method()
|
||||
removeView(nav: Nav, viewController: ViewController, opts?: NavOptions): Promise<any> {
|
||||
return hydrateDelegateAndAnimation(this).then(([delegate, animation]) => {
|
||||
return removeViewImpl(nav, delegate, animation, viewController, opts);
|
||||
});
|
||||
}
|
||||
|
||||
@Method()
|
||||
setPages(nav: Nav, componentDataPairs: ComponentDataPair[], opts?: NavOptions): Promise<any> {
|
||||
return hydrateDelegateAndAnimation(this).then(([delegate, animation]) => {
|
||||
return setPagesImpl(nav, delegate, animation, componentDataPairs, opts);
|
||||
});
|
||||
}
|
||||
|
||||
render() {
|
||||
return <slot></slot>;
|
||||
}
|
||||
}
|
||||
|
||||
export function hydrateDelegateAndAnimation(navController: NavController): Promise<any> {
|
||||
return Promise.all([hydrateDelegate(navController), hydrateAnimationController(navController.animationCtrl)]);
|
||||
}
|
||||
|
||||
export function hydrateDelegate(navController: NavController): Promise<FrameworkDelegate> {
|
||||
if (navController.delegate) {
|
||||
return Promise.resolve(navController.delegate);
|
||||
}
|
||||
|
||||
// no delegate is set, so fall back to using the DomFrameworkDelegate
|
||||
defaultDelegate = new DomFrameworkDelegate();
|
||||
return Promise.resolve(defaultDelegate);
|
||||
}
|
||||
|
||||
export function hydrateAnimationController(animationController: AnimationController): Promise<Animation> {
|
||||
return animationController.create();
|
||||
}
|
@ -1,57 +0,0 @@
|
||||
# ion-nav-controller
|
||||
|
||||
|
||||
|
||||
<!-- Auto Generated Below -->
|
||||
|
||||
|
||||
## Properties
|
||||
|
||||
#### delegate
|
||||
|
||||
any
|
||||
|
||||
|
||||
## Attributes
|
||||
|
||||
#### delegate
|
||||
|
||||
any
|
||||
|
||||
|
||||
## Methods
|
||||
|
||||
#### insert()
|
||||
|
||||
|
||||
#### insertPages()
|
||||
|
||||
|
||||
#### pop()
|
||||
|
||||
|
||||
#### popTo()
|
||||
|
||||
|
||||
#### popToRoot()
|
||||
|
||||
|
||||
#### push()
|
||||
|
||||
|
||||
#### removeIndex()
|
||||
|
||||
|
||||
#### removeView()
|
||||
|
||||
|
||||
#### setPages()
|
||||
|
||||
|
||||
#### setRoot()
|
||||
|
||||
|
||||
|
||||
----------------------------------------------
|
||||
|
||||
*Built by [StencilJS](https://stenciljs.com/)*
|
@ -1,24 +0,0 @@
|
||||
|
||||
/* it is very important to keep this interface in sync with ./nav */
|
||||
import { NavOptions, PublicViewController } from '../../index';
|
||||
|
||||
export interface PublicNav {
|
||||
push(component: any, data?: any, opts?: NavOptions): Promise<any>;
|
||||
pop(opts?: NavOptions): Promise<any>;
|
||||
setRoot(component: any, data?: any, opts?: NavOptions): Promise<any>;
|
||||
insert(insertIndex: number, page: any, params?: any, opts?: NavOptions): Promise<any>;
|
||||
insertPages(insertIndex: number, insertPages: any[], opts?: NavOptions): Promise<any>;
|
||||
popToRoot(opts?: NavOptions): Promise<any>;
|
||||
popTo(indexOrViewCtrl: any, opts?: NavOptions): Promise<any>;
|
||||
removeIndex(startIndex: number, removeCount?: number, opts?: NavOptions): Promise<any>;
|
||||
removeView(viewController: PublicViewController, opts?: NavOptions): Promise<any>;
|
||||
setPages(componentDataPairs: any[], opts?: NavOptions): Promise<any>;
|
||||
|
||||
getActive?(): PublicViewController;
|
||||
getPrevious?(view?: PublicViewController): PublicViewController;
|
||||
canGoBack?(): boolean;
|
||||
canSwipeBack?(): boolean;
|
||||
getFirstView?(): PublicViewController;
|
||||
|
||||
element?: HTMLElement;
|
||||
}
|
@ -1,18 +1,34 @@
|
||||
|
||||
/* it is very important to keep this interface in sync with ./nav */
|
||||
import {
|
||||
Animation,
|
||||
AnimationOptions,
|
||||
FrameworkDelegate,
|
||||
Nav,
|
||||
NavOptions,
|
||||
PublicViewController,
|
||||
ViewController
|
||||
} from '../index';
|
||||
} from '../../index';
|
||||
|
||||
export interface FrameworkDelegate {
|
||||
export interface PublicNav {
|
||||
push(component: any, data?: any, opts?: NavOptions): Promise<any>;
|
||||
pop(opts?: NavOptions): Promise<any>;
|
||||
setRoot(component: any, data?: any, opts?: NavOptions): Promise<any>;
|
||||
insert(insertIndex: number, page: any, params?: any, opts?: NavOptions): Promise<any>;
|
||||
insertPages(insertIndex: number, insertPages: any[], opts?: NavOptions): Promise<any>;
|
||||
popToRoot(opts?: NavOptions): Promise<any>;
|
||||
popTo(indexOrViewCtrl: any, opts?: NavOptions): Promise<any>;
|
||||
removeIndex(startIndex: number, removeCount?: number, opts?: NavOptions): Promise<any>;
|
||||
removeView(viewController: PublicViewController, opts?: NavOptions): Promise<any>;
|
||||
setPages(componentDataPairs: any[], opts?: NavOptions): Promise<any>;
|
||||
|
||||
attachViewToDom(elementOrContainerToMountTo: any, elementOrComponentToMount: any, propsOrDataObj?: any, classesToAdd?: string[]): Promise<FrameworkMountingData>;
|
||||
removeViewFromDom(elementOrContainerToUnmountFrom: any, elementOrComponentToUnmount: any): Promise<FrameworkMountingData>;
|
||||
}
|
||||
getActive?(): PublicViewController;
|
||||
getPrevious?(view?: PublicViewController): PublicViewController;
|
||||
canGoBack?(): boolean;
|
||||
canSwipeBack?(): boolean;
|
||||
getFirstView?(): PublicViewController;
|
||||
|
||||
export interface FrameworkMountingData {
|
||||
element: HTMLElement;
|
||||
element?: HTMLElement;
|
||||
}
|
||||
|
||||
export interface NavContainer {
|
@ -1,6 +1,6 @@
|
||||
import { Transition } from './nav-interfaces';
|
||||
import { Animation, AnimationOptions, Config, Nav, RouterEntry, TransitionBuilder, ViewController } from '..';
|
||||
import { isDef } from '../utils/helpers';
|
||||
import { Animation, AnimationOptions, Config, Nav, RouterEntry, TransitionBuilder, ViewController } from '../../index';
|
||||
import { isDef } from '../../utils/helpers';
|
||||
|
||||
export const STATE_NEW = 1;
|
||||
export const STATE_INITIALIZED = 2;
|
@ -1,25 +1,59 @@
|
||||
import { Component, Element, Event, EventEmitter, Listen, Method, Prop } from '@stencil/core';
|
||||
import {
|
||||
Animation,
|
||||
AnimationController,
|
||||
AnimationOptions,
|
||||
ComponentDataPair,
|
||||
Config,
|
||||
FrameworkDelegate,
|
||||
NavController,
|
||||
NavOptions,
|
||||
NavResult,
|
||||
NavState,
|
||||
PublicNav,
|
||||
PublicViewController,
|
||||
RouterEntries,
|
||||
ViewController
|
||||
Transition,
|
||||
TransitionInstruction,
|
||||
} from '../../index';
|
||||
import { ViewController } from './view-controller';
|
||||
|
||||
import {
|
||||
DIRECTION_BACK,
|
||||
DIRECTION_FORWARD,
|
||||
STATE_ATTACHED,
|
||||
STATE_DESTROYED,
|
||||
STATE_NEW,
|
||||
VIEW_ID_START,
|
||||
destroyTransition,
|
||||
getActiveImpl,
|
||||
getFirstView,
|
||||
getHydratedTransition,
|
||||
getNextNavId,
|
||||
getNextTransitionId,
|
||||
getParentTransitionId,
|
||||
getPreviousImpl,
|
||||
getViews,
|
||||
resolveRoute
|
||||
} from '../../navigation/nav-utils';
|
||||
import { assert, isReady } from '../../utils/helpers';
|
||||
isViewController,
|
||||
resolveRoute,
|
||||
setZIndex,
|
||||
toggleHidden,
|
||||
transitionFactory
|
||||
} from './nav-utils';
|
||||
|
||||
import { DomFrameworkDelegate } from '../../utils/dom-framework-delegate';
|
||||
|
||||
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[]>();
|
||||
|
||||
/* it is very important to keep this class in sync with ./nav-interface interface */
|
||||
@Component({
|
||||
@ -44,12 +78,12 @@ export class Nav implements PublicNav {
|
||||
isPortal: boolean;
|
||||
swipeToGoBackTransition: any; // TODO Transition
|
||||
childNavs?: Nav[];
|
||||
navController?: NavController;
|
||||
|
||||
@Prop() mode: string;
|
||||
@Prop() root: any;
|
||||
@Prop() delegate: FrameworkDelegate;
|
||||
@Prop({ context: 'config' }) config: Config;
|
||||
@Prop({ connect: 'ion-animation-controller' }) animationCtrl: AnimationController;
|
||||
|
||||
constructor() {
|
||||
this.navId = getNextNavId();
|
||||
@ -212,72 +246,54 @@ export function componentDidLoadImpl(nav: Nav) {
|
||||
}
|
||||
}
|
||||
|
||||
export function pushImpl(nav: Nav, component: any, data: any, opts: NavOptions) {
|
||||
return getNavController(nav).then(() => {
|
||||
return nav.navController.push(nav, component, data, opts);
|
||||
});
|
||||
export async function pushImpl(nav: Nav, component: any, data: any, opts: NavOptions) {
|
||||
const animation = await hydrateAnimationController(nav.animationCtrl);
|
||||
return push(nav, nav.delegate, animation, component, data, opts);
|
||||
}
|
||||
|
||||
export function popImpl(nav: Nav, opts: NavOptions) {
|
||||
return getNavController(nav).then(() => {
|
||||
return nav.navController.pop(nav, opts);
|
||||
});
|
||||
export async function popImpl(nav: Nav, opts: NavOptions) {
|
||||
const animation = await hydrateAnimationController(nav.animationCtrl);
|
||||
return pop(nav, nav.delegate, animation, opts);
|
||||
}
|
||||
|
||||
export function setRootImpl(nav: Nav, component: any, data: any, opts: NavOptions) {
|
||||
return getNavController(nav).then(() => {
|
||||
return nav.navController.setRoot(nav, component, data, opts);
|
||||
});
|
||||
export async function setRootImpl(nav: Nav, component: any, data: any, opts: NavOptions) {
|
||||
const animation = await hydrateAnimationController(nav.animationCtrl);
|
||||
return setRoot(nav, nav.delegate, animation, component, data, opts);
|
||||
}
|
||||
|
||||
export function insertImpl(nav: Nav, insertIndex: number, page: any, params: any, opts: NavOptions) {
|
||||
return getNavController(nav).then(() => {
|
||||
return nav.navController.insert(nav, insertIndex, page, params, opts);
|
||||
});
|
||||
export async function insertImpl(nav: Nav, insertIndex: number, page: any, params: any, opts: NavOptions) {
|
||||
const animation = await hydrateAnimationController(nav.animationCtrl);
|
||||
return insert(nav, nav.delegate, animation, insertIndex, page, params, opts);
|
||||
}
|
||||
|
||||
export function insertPagesImpl(nav: Nav, insertIndex: number, insertPages: any[], opts: NavOptions) {
|
||||
return getNavController(nav).then(() => {
|
||||
return nav.navController.insertPages(nav, insertIndex, insertPages, opts);
|
||||
});
|
||||
export async function insertPagesImpl(nav: Nav, insertIndex: number, pagesToInsert: any[], opts: NavOptions) {
|
||||
const animation = await hydrateAnimationController(nav.animationCtrl);
|
||||
return insertPages(nav, nav.delegate, animation, insertIndex, pagesToInsert, opts);
|
||||
}
|
||||
|
||||
export function popToRootImpl(nav: Nav, opts: NavOptions) {
|
||||
return getNavController(nav).then(() => {
|
||||
return nav.navController.popToRoot(nav, opts);
|
||||
});
|
||||
export async function popToRootImpl(nav: Nav, opts: NavOptions) {
|
||||
const animation = await hydrateAnimationController(nav.animationCtrl);
|
||||
return popToRoot(nav, nav.delegate, animation, opts);
|
||||
}
|
||||
|
||||
export function popToImpl(nav: Nav, indexOrViewCtrl: any, opts: NavOptions) {
|
||||
return getNavController(nav).then(() => {
|
||||
return nav.navController.popTo(nav, indexOrViewCtrl, opts);
|
||||
});
|
||||
export async function popToImpl(nav: Nav, indexOrViewCtrl: any, opts: NavOptions) {
|
||||
const animation = await hydrateAnimationController(nav.animationCtrl);
|
||||
return popTo(nav, nav.delegate, animation, indexOrViewCtrl, opts);
|
||||
}
|
||||
|
||||
export function removeImpl(nav: Nav, startIndex: number, removeCount: number, opts: NavOptions) {
|
||||
return getNavController(nav).then(() => {
|
||||
return nav.navController.removeIndex(nav, startIndex, removeCount, opts);
|
||||
});
|
||||
export async function removeImpl(nav: Nav, startIndex: number, removeCount: number, opts: NavOptions) {
|
||||
const animation = await hydrateAnimationController(nav.animationCtrl);
|
||||
return remove(nav, nav.delegate, animation, startIndex, removeCount, opts);
|
||||
}
|
||||
|
||||
export function removeViewImpl(nav: Nav, viewController: PublicViewController, opts?: NavOptions) {
|
||||
return getNavController(nav).then(() => {
|
||||
return nav.navController.removeView(nav, viewController as ViewController, opts);
|
||||
});
|
||||
export async function removeViewImpl(nav: Nav, viewController: PublicViewController, opts?: NavOptions) {
|
||||
const animation = await hydrateAnimationController(nav.animationCtrl);
|
||||
return removeView(nav, nav.delegate, animation, viewController as ViewController, opts);
|
||||
}
|
||||
|
||||
export function setPagesImpl(nav: Nav, componentDataPairs: ComponentDataPair[], opts?: NavOptions) {
|
||||
return getNavController(nav).then(() => {
|
||||
return nav.navController.setPages(nav, componentDataPairs, opts);
|
||||
});
|
||||
}
|
||||
|
||||
export function getNavController(nav: Nav): Promise<any> {
|
||||
if (nav.navController) {
|
||||
return Promise.resolve();
|
||||
}
|
||||
nav.navController = document.querySelector('ion-nav-controller') as any as NavController;
|
||||
return isReady(nav.navController as any as HTMLElement);
|
||||
export async function setPagesImpl(nav: Nav, componentDataPairs: ComponentDataPair[], opts?: NavOptions) {
|
||||
const animation = await hydrateAnimationController(nav.animationCtrl);
|
||||
return setPages(nav, nav.delegate, animation, componentDataPairs, opts);
|
||||
}
|
||||
|
||||
export function canGoBackImpl(nav: Nav) {
|
||||
@ -296,3 +312,770 @@ export function navInitializedImpl(potentialParent: Nav, event: CustomEvent) {
|
||||
event.stopPropagation();
|
||||
}
|
||||
}
|
||||
|
||||
export function hydrateAnimationController(animationController: AnimationController): Promise<Animation> {
|
||||
return animationController.create();
|
||||
}
|
||||
|
||||
|
||||
// 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;
|
||||
|
||||
if (!ti.delegate) {
|
||||
ti.delegate = new DomFrameworkDelegate();
|
||||
}
|
||||
|
||||
// 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.element, enteringView.component, enteringView.data, ['ion-page']).then((mountingData) => {
|
||||
Object.assign(enteringView, mountingData);
|
||||
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;
|
||||
|
@ -1,6 +1,6 @@
|
||||
import { AnimationOptions, Transition, ViewController } from '../../index';
|
||||
import { AnimationOptions, Transition, ViewController } from '../../../index';
|
||||
import { canNavGoBack } from '../nav-utils';
|
||||
import { isDef } from '../../utils/helpers';
|
||||
import { isDef } from '../../../utils/helpers';
|
||||
|
||||
const DURATION = 500;
|
||||
const EASING = 'cubic-bezier(0.36,0.66,0.04,1)';
|
@ -1,6 +1,6 @@
|
||||
import { AnimationOptions, Transition, ViewController } from '../../index';
|
||||
import { AnimationOptions, Transition, ViewController } from '../../../index';
|
||||
import { canNavGoBack } from '../nav-utils';
|
||||
import { isDef } from '../../utils/helpers';
|
||||
import { isDef } from '../../../utils/helpers';
|
||||
|
||||
const TRANSLATEY = 'translateY';
|
||||
const OFF_BOTTOM = '40px';
|
@ -1,7 +1,7 @@
|
||||
import { FrameworkDelegate, Nav, PublicViewController } from '../index';
|
||||
import { FrameworkDelegate, Nav, PublicViewController } from '../../index';
|
||||
import { STATE_ATTACHED, STATE_DESTROYED, STATE_INITIALIZED, STATE_NEW } from './nav-utils';
|
||||
|
||||
import { assert } from '../utils/helpers';
|
||||
import { assert } from '../../utils/helpers';
|
||||
|
||||
export class ViewController implements PublicViewController {
|
||||
|
16
packages/core/src/index.d.ts
vendored
16
packages/core/src/index.d.ts
vendored
@ -65,9 +65,9 @@ export {
|
||||
export * from './components/modal/modal';
|
||||
export { ModalController } from './components/modal-controller/modal-controller';
|
||||
export { Nav } from './components/nav/nav';
|
||||
export { PublicNav } from './components/nav/nav-interface';
|
||||
export * from './components/nav/nav-interfaces';
|
||||
export { ViewController } from './components/nav/view-controller';
|
||||
export { Navbar } from './components/navbar/navbar';
|
||||
export { NavController } from './components/nav-controller/nav-controller';
|
||||
export { Note } from './components/note/note';
|
||||
export { Page } from './components/page/page';
|
||||
export { PickerColumnCmp } from './components/picker-column/picker-column';
|
||||
@ -110,9 +110,6 @@ export { ToastController } from './components/toast-controller/toast-controller'
|
||||
export { Toggle } from './components/toggle/toggle';
|
||||
export { Toolbar } from './components/toolbar/toolbar';
|
||||
|
||||
export * from './navigation/nav-interfaces';
|
||||
export { ViewController } from './navigation/view-controller';
|
||||
|
||||
// export all of the component declarations that are dynamically created
|
||||
export * from './components';
|
||||
|
||||
@ -152,3 +149,12 @@ export interface OverlayDismissEventDetail {
|
||||
export interface OverlayController {
|
||||
create(): HTMLElement;
|
||||
}
|
||||
|
||||
export interface FrameworkDelegate {
|
||||
attachViewToDom(elementOrContainerToMountTo: any, elementOrComponentToMount: any, propsOrDataObj?: any, classesToAdd?: string[], escapeHatch?: any): Promise<FrameworkMountingData>;
|
||||
removeViewFromDom(elementOrContainerToUnmountFrom: any, elementOrComponentToUnmount: any, escapeHatch?: any): Promise<FrameworkMountingData>;
|
||||
}
|
||||
|
||||
export interface FrameworkMountingData {
|
||||
element: HTMLElement;
|
||||
}
|
||||
|
@ -1,771 +0,0 @@
|
||||
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.element, enteringView.component, enteringView.data, ['ion-page']).then((mountingData) => {
|
||||
Object.assign(enteringView, mountingData);
|
||||
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;
|
@ -1,9 +1,9 @@
|
||||
import { FrameworkDelegate, FrameworkMountingData, } from '../../index';
|
||||
import { isString } from '../../utils/helpers';
|
||||
import { FrameworkDelegate, FrameworkMountingData, } from '../index';
|
||||
import { isString } from './helpers';
|
||||
|
||||
export class DomFrameworkDelegate implements FrameworkDelegate {
|
||||
attachViewToDom(parentElement: HTMLElement, tagOrElement: string | HTMLElement, propsOrDataObj: any = {}, classesToAdd: string[] = []): Promise<FrameworkMountingData> {
|
||||
|
||||
attachViewToDom(parentElement: HTMLElement, tagOrElement: string | HTMLElement, propsOrDataObj: any = {}, classesToAdd: string[] = []): Promise<FrameworkMountingData> {
|
||||
return new Promise((resolve) => {
|
||||
const usersElement = (isString(tagOrElement) ? document.createElement(tagOrElement) : tagOrElement) as HTMLElement;
|
||||
|
||||
@ -21,7 +21,6 @@ export class DomFrameworkDelegate implements FrameworkDelegate {
|
||||
}
|
||||
|
||||
removeViewFromDom(parentElement: HTMLElement, childElement: HTMLElement): Promise<FrameworkMountingData> {
|
||||
|
||||
parentElement.removeChild(childElement);
|
||||
return Promise.resolve({
|
||||
element: null
|
@ -38,7 +38,7 @@ exports.config = {
|
||||
{ components: ['ion-range', 'ion-range-knob']},
|
||||
{ components: ['ion-tabs', 'ion-tab', 'ion-tabbar', 'ion-tab-button', 'ion-tab-highlight'] },
|
||||
{ components: ['ion-toggle'] },
|
||||
{ components: ['ion-nav', 'ion-nav-controller'] },
|
||||
{ components: ['ion-nav'] },
|
||||
{ components: ['ion-toast', 'ion-toast-controller'] },
|
||||
],
|
||||
collections: [
|
||||
|
@ -9,7 +9,8 @@ const routes: Routes = [
|
||||
{ path: 'alert', loadChildren: 'app/alert/alert.module#AlertModule' },
|
||||
{ path: 'actionSheet', loadChildren: 'app/action-sheet/action-sheet.module#ActionSheetModule' },
|
||||
{ path: 'toast', loadChildren: 'app/toast/toast.module#ToastModule' },
|
||||
{ path: 'loading', loadChildren: 'app/loading/loading.module#LoadingModule' }
|
||||
{ path: 'loading', loadChildren: 'app/loading/loading.module#LoadingModule' },
|
||||
{ path: 'nav', loadChildren: 'app/nav/nav.module#NavModule' }
|
||||
];
|
||||
|
||||
@NgModule({
|
||||
|
@ -21,4 +21,7 @@
|
||||
<li>
|
||||
<a href='loading'>Loading Page</a>
|
||||
</li>
|
||||
<li>
|
||||
<a href='nav'>Nav Page</a>
|
||||
</li>
|
||||
</ul>
|
||||
|
14
packages/demos/angular/src/app/nav/nav-routing.module.ts
Normal file
14
packages/demos/angular/src/app/nav/nav-routing.module.ts
Normal file
@ -0,0 +1,14 @@
|
||||
import { NgModule } from '@angular/core';
|
||||
import { Routes, RouterModule } from '@angular/router';
|
||||
|
||||
import { NavPageComponent } from './nav.component';
|
||||
|
||||
const routes: Routes = [
|
||||
{ path: '', component: NavPageComponent }
|
||||
];
|
||||
|
||||
@NgModule({
|
||||
imports: [RouterModule.forChild(routes)],
|
||||
exports: [RouterModule]
|
||||
})
|
||||
export class NavRoutingModule { }
|
20
packages/demos/angular/src/app/nav/nav.component.ts
Normal file
20
packages/demos/angular/src/app/nav/nav.component.ts
Normal file
@ -0,0 +1,20 @@
|
||||
import { Component } from '@angular/core';
|
||||
|
||||
import { PageOne } from './pages/page-one';
|
||||
|
||||
@Component({
|
||||
selector: 'app-nav-page',
|
||||
template: `
|
||||
<ion-app>
|
||||
<ion-nav [root]="pageOne"></ion-nav>
|
||||
</ion-app>
|
||||
`
|
||||
})
|
||||
export class NavPageComponent {
|
||||
|
||||
pageOne: any = PageOne;
|
||||
constructor() {
|
||||
|
||||
}
|
||||
|
||||
}
|
31
packages/demos/angular/src/app/nav/nav.module.ts
Normal file
31
packages/demos/angular/src/app/nav/nav.module.ts
Normal file
@ -0,0 +1,31 @@
|
||||
import { NgModule, CUSTOM_ELEMENTS_SCHEMA } from '@angular/core';
|
||||
import { CommonModule } from '@angular/common';
|
||||
|
||||
import { NavPageComponent } from './nav.component';
|
||||
import { NavRoutingModule } from './nav-routing.module';
|
||||
import { IonicAngularModule } from '@ionic/angular';
|
||||
|
||||
import { PageOne } from './pages/page-one';
|
||||
import { PageTwo } from './pages/page-two';
|
||||
import { PageThree } from './pages/page-three';
|
||||
|
||||
@NgModule({
|
||||
imports: [
|
||||
CommonModule,
|
||||
NavRoutingModule,
|
||||
IonicAngularModule
|
||||
],
|
||||
declarations: [
|
||||
NavPageComponent,
|
||||
PageOne,
|
||||
PageTwo,
|
||||
PageThree
|
||||
],
|
||||
entryComponents: [
|
||||
PageOne,
|
||||
PageTwo,
|
||||
PageThree
|
||||
],
|
||||
schemas: [CUSTOM_ELEMENTS_SCHEMA]
|
||||
})
|
||||
export class NavModule { }
|
62
packages/demos/angular/src/app/nav/pages/page-one.ts
Normal file
62
packages/demos/angular/src/app/nav/pages/page-one.ts
Normal file
@ -0,0 +1,62 @@
|
||||
import { Component } from '@angular/core';
|
||||
|
||||
import { PageTwo } from './page-two';
|
||||
|
||||
@Component({
|
||||
selector: 'page-one',
|
||||
template: `
|
||||
<ion-header>
|
||||
<ion-toolbar>
|
||||
<ion-title>Page One</ion-title>
|
||||
</ion-toolbar>
|
||||
</ion-header>
|
||||
<ion-content>
|
||||
Page One
|
||||
<div>
|
||||
<ion-button (click)="goToPageTwo()">Go to Page Two</ion-button>
|
||||
</div>
|
||||
<ul>
|
||||
<li>ngOnInit - {{ngOnInitDetection}}</li>
|
||||
<li>ionViewWillEnter - {{ionViewWillEnterDetection}}</li>
|
||||
<li>ionViewDidEnter - {{ionViewDidEnterDetection}}</li>
|
||||
</ul>
|
||||
</ion-content>
|
||||
`
|
||||
})
|
||||
export class PageOne {
|
||||
|
||||
ngOnInitDetection = 'initial';
|
||||
ionViewWillEnterDetection = 'initial';
|
||||
ionViewDidEnterDetection = 'initial';
|
||||
|
||||
constructor() {
|
||||
|
||||
}
|
||||
|
||||
|
||||
ngOnInit() {
|
||||
console.log('page one ngOnInit');
|
||||
setInterval(() => {
|
||||
this.ngOnInitDetection = '' + Date.now();
|
||||
}, 500);
|
||||
}
|
||||
|
||||
ionViewWillEnter() {
|
||||
console.log('page one ionViewWillEnter');
|
||||
setInterval(() => {
|
||||
this.ionViewWillEnterDetection = '' + Date.now();
|
||||
}, 500);
|
||||
}
|
||||
|
||||
ionViewDidEnter() {
|
||||
console.log('page one ionViewDidEnter');
|
||||
setInterval(() => {
|
||||
this.ionViewDidEnterDetection = '' + Date.now();
|
||||
}, 500);
|
||||
}
|
||||
|
||||
goToPageTwo() {
|
||||
const nav = document.querySelector('ion-nav') as any;
|
||||
nav.push(PageTwo).then(() => console.log('push complete'));
|
||||
}
|
||||
}
|
62
packages/demos/angular/src/app/nav/pages/page-three.ts
Normal file
62
packages/demos/angular/src/app/nav/pages/page-three.ts
Normal file
@ -0,0 +1,62 @@
|
||||
import { Component } from '@angular/core';
|
||||
|
||||
@Component({
|
||||
selector: 'page-three',
|
||||
template: `
|
||||
<ion-page>
|
||||
<ion-header>
|
||||
<ion-toolbar>
|
||||
<ion-title>Page Three</ion-title>
|
||||
</ion-toolbar>
|
||||
</ion-header>
|
||||
<ion-content>
|
||||
Page Three
|
||||
<div>
|
||||
<ion-button (click)="goBack()">Go Back</ion-button>
|
||||
</div>
|
||||
<ul>
|
||||
<li>ngOnInit - {{ngOnInitDetection}}</li>
|
||||
<li>ionViewWillEnter - {{ionViewWillEnterDetection}}</li>
|
||||
<li>ionViewDidEnter - {{ionViewDidEnterDetection}}</li>
|
||||
</ul>
|
||||
</ion-content>
|
||||
</ion-page>
|
||||
`
|
||||
})
|
||||
export class PageThree {
|
||||
|
||||
ngOnInitDetection = 'initial';
|
||||
ionViewWillEnterDetection = 'initial';
|
||||
ionViewDidEnterDetection = 'initial';
|
||||
|
||||
constructor() {
|
||||
|
||||
}
|
||||
|
||||
|
||||
ngOnInit() {
|
||||
console.log('page two ngOnInit');
|
||||
setInterval(() => {
|
||||
this.ngOnInitDetection = '' + Date.now();
|
||||
}, 500);
|
||||
}
|
||||
|
||||
ionViewWillEnter() {
|
||||
console.log('page two ionViewWillEnter');
|
||||
setInterval(() => {
|
||||
this.ionViewWillEnterDetection = '' + Date.now();
|
||||
}, 500);
|
||||
}
|
||||
|
||||
ionViewDidEnter() {
|
||||
console.log('page two ionViewDidEnter');
|
||||
setInterval(() => {
|
||||
this.ionViewDidEnterDetection = '' + Date.now();
|
||||
}, 500);
|
||||
}
|
||||
|
||||
goBack() {
|
||||
const nav = document.querySelector('ion-nav') as any;
|
||||
nav.pop().then(() => console.log('pop complete'));
|
||||
}
|
||||
}
|
72
packages/demos/angular/src/app/nav/pages/page-two.ts
Normal file
72
packages/demos/angular/src/app/nav/pages/page-two.ts
Normal file
@ -0,0 +1,72 @@
|
||||
import { Component } from '@angular/core';
|
||||
|
||||
import { PageThree } from './page-three';
|
||||
|
||||
@Component({
|
||||
selector: 'page-two',
|
||||
template: `
|
||||
<ion-page>
|
||||
<ion-header>
|
||||
<ion-toolbar>
|
||||
<ion-title>Page Two</ion-title>
|
||||
</ion-toolbar>
|
||||
</ion-header>
|
||||
<ion-content>
|
||||
Page Two
|
||||
<div>
|
||||
<ion-button (click)="goNext()">Go to Page Three</ion-button>
|
||||
</div>
|
||||
<div>
|
||||
<ion-button (click)="goBack()">Go Back</ion-button>
|
||||
</div>
|
||||
<ul>
|
||||
<li>ngOnInit - {{ngOnInitDetection}}</li>
|
||||
<li>ionViewWillEnter - {{ionViewWillEnterDetection}}</li>
|
||||
<li>ionViewDidEnter - {{ionViewDidEnterDetection}}</li>
|
||||
</ul>
|
||||
</ion-content>
|
||||
</ion-page>
|
||||
`
|
||||
})
|
||||
export class PageTwo {
|
||||
|
||||
ngOnInitDetection = 'initial';
|
||||
ionViewWillEnterDetection = 'initial';
|
||||
ionViewDidEnterDetection = 'initial';
|
||||
|
||||
constructor() {
|
||||
|
||||
}
|
||||
|
||||
|
||||
ngOnInit() {
|
||||
console.log('page two ngOnInit');
|
||||
setInterval(() => {
|
||||
this.ngOnInitDetection = '' + Date.now();
|
||||
}, 500);
|
||||
}
|
||||
|
||||
ionViewWillEnter() {
|
||||
console.log('page two ionViewWillEnter');
|
||||
setInterval(() => {
|
||||
this.ionViewWillEnterDetection = '' + Date.now();
|
||||
}, 500);
|
||||
}
|
||||
|
||||
ionViewDidEnter() {
|
||||
console.log('page two ionViewDidEnter');
|
||||
setInterval(() => {
|
||||
this.ionViewDidEnterDetection = '' + Date.now();
|
||||
}, 500);
|
||||
}
|
||||
|
||||
goNext() {
|
||||
const nav = document.querySelector('ion-nav') as any;
|
||||
nav.push(PageThree).then(() => console.log('push complete'));
|
||||
}
|
||||
|
||||
goBack() {
|
||||
const nav = document.querySelector('ion-nav') as any;
|
||||
nav.pop().then(() => console.log('pop complete'));
|
||||
}
|
||||
}
|
@ -1,6 +1,6 @@
|
||||
{
|
||||
"rulesDirectory": [
|
||||
"node_modules/codelyzer"
|
||||
// "node_modules/codelyzer"
|
||||
],
|
||||
"rules": {
|
||||
"arrow-return-shorthand": true,
|
||||
|
@ -10,11 +10,9 @@ class App extends Component {
|
||||
render() {
|
||||
return (
|
||||
<ion-app>
|
||||
<ion-nav-controller ref={wc({}, {
|
||||
delegate: Delegate
|
||||
})}></ion-nav-controller>
|
||||
<ion-nav ref={wc({},{
|
||||
root: PageOne
|
||||
root: PageOne,
|
||||
delegate: Delegate
|
||||
})}></ion-nav>
|
||||
</ion-app>
|
||||
);
|
||||
|
Reference in New Issue
Block a user