From 2c6a4dcf34c5f4d3365f7313536686eceb263fee Mon Sep 17 00:00:00 2001 From: Dan Bucholtz Date: Thu, 24 Aug 2017 13:31:42 -0500 Subject: [PATCH] refactor(navigation): ion-nav and ion-nav-controller are separate components * nav-nav-nav-nav-nav * wipit * holy async batman --- .../animation-interface.tsx | 6 +- .../animation-controller/animator.tsx | 3 + .../nav-controller/nav-controller.tsx | 123 ++++++++++ .../nav-controller/stencil-nav-delegate.tsx | 30 +++ packages/core/src/components/nav/nav.tsx | 157 ++++++++++-- .../core/src/components/nav/test/basic.html | 2 + packages/core/src/components/page/page.scss | 8 + packages/core/src/index.d.ts | 12 +- .../navigation/nav-controller-functions.ts | 226 ++++++++---------- .../core/src/navigation/nav-interfaces.d.ts | 64 ++++- packages/core/src/navigation/nav-utils.ts | 168 ++++++++++++- .../navigation/stencil-framework-delegate.ts | 32 --- .../navigation/transitions/transition.ios.ts | 181 ++++++++++++++ .../navigation/transitions/transition.md.ts | 60 +++++ .../src/navigation/view-controller-impl.ts | 34 +-- packages/core/src/utils/helpers.ts | 12 +- packages/core/src/utils/ids.ts | 3 - packages/core/stencil.config.js | 2 +- 18 files changed, 911 insertions(+), 212 deletions(-) create mode 100644 packages/core/src/components/nav-controller/nav-controller.tsx create mode 100644 packages/core/src/components/nav-controller/stencil-nav-delegate.tsx delete mode 100644 packages/core/src/navigation/stencil-framework-delegate.ts create mode 100644 packages/core/src/navigation/transitions/transition.ios.ts create mode 100644 packages/core/src/navigation/transitions/transition.md.ts delete mode 100644 packages/core/src/utils/ids.ts diff --git a/packages/core/src/components/animation-controller/animation-interface.tsx b/packages/core/src/components/animation-controller/animation-interface.tsx index e315146333..1c3347a0df 100644 --- a/packages/core/src/components/animation-controller/animation-interface.tsx +++ b/packages/core/src/components/animation-controller/animation-interface.tsx @@ -5,6 +5,7 @@ export interface AnimationController { export interface Animation { new (): Animation; parent: Animation; + hasChildren: boolean; addElement(elm: Node|Node[]|NodeList): Animation; add(childAnimation: Animation): Animation; duration(milliseconds: number): Animation; @@ -34,11 +35,14 @@ export interface Animation { progressEnd(shouldComplete: boolean, currentStepValue: number, dur: number): void; onFinish(callback: (animation?: Animation) => void, opts?: {oneTimeCallback?: boolean, clearExistingCallacks?: boolean}): Animation; destroy(): void; + isRoot(): boolean; + create(): Animation; + hasCompleted: boolean; } export interface AnimationBuilder { - (Animation: Animation, baseElm?: HTMLElement, opts?: any): Animation; +(Animation: Animation, baseElm?: HTMLElement, opts?: any): Animation; } diff --git a/packages/core/src/components/animation-controller/animator.tsx b/packages/core/src/components/animation-controller/animator.tsx index 367e5cd9a9..4e783e5686 100644 --- a/packages/core/src/components/animation-controller/animator.tsx +++ b/packages/core/src/components/animation-controller/animator.tsx @@ -1240,4 +1240,7 @@ export class Animator { return (this._hasTweenEffect && this._hasDur && this._elementTotal ? this._elements[0] : null); } + create() { + return new Animator(); + } } diff --git a/packages/core/src/components/nav-controller/nav-controller.tsx b/packages/core/src/components/nav-controller/nav-controller.tsx new file mode 100644 index 0000000000..a45df85cb1 --- /dev/null +++ b/packages/core/src/components/nav-controller/nav-controller.tsx @@ -0,0 +1,123 @@ +import { Component, Element, Method, Prop } from '@stencil/core'; +import { AnimationController, Config } from '../..'; +import { ComponentDataPair, FrameworkDelegate, Nav, NavController, NavOptions, ViewController } from '../../navigation/nav-interfaces'; + +import { isReady } from '../../utils/helpers'; + +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 NavControllerImpl implements NavController { + + @Element() element: HTMLElement; + + @Prop({ connect: 'ion-animation-controller' }) animationCtrl: AnimationController; + @Prop({ context: 'config' }) config: Config; + @Prop() delegate: FrameworkDelegate; + + constructor() { + } + + @Method() + push(nav: Nav, component: any, data?: any, opts?: NavOptions): Promise { + return getDelegate(this).then((delegate) => { + return pushImpl(nav, delegate, component, data, opts); + }); + } + + @Method() + pop(nav: Nav, opts?: NavOptions): Promise { + return getDelegate(this).then((delegate) => { + return popImpl(nav, delegate, opts); + }); + + } + + @Method() + setRoot(nav: Nav, component: any, data?: any, opts?: NavOptions): Promise { + return getDelegate(this).then((delegate) => { + return setRootImpl(nav, delegate, component, data, opts); + }); + } + + @Method() + insert(nav: Nav, insertIndex: number, page: any, params?: any, opts?: NavOptions): Promise { + return getDelegate(this).then((delegate) => { + return insertImpl(nav, delegate, insertIndex, page, params, opts); + }); + } + + @Method() + insertPages(nav: Nav, insertIndex: number, insertPages: any[], opts?: NavOptions): Promise { + return getDelegate(this).then((delegate) => { + return insertPagesImpl(nav, delegate, insertIndex, insertPages, opts); + }); + } + + @Method() + popToRoot(nav: Nav, opts?: NavOptions): Promise { + return getDelegate(this).then((delegate) => { + return popToRootImpl(nav, delegate, opts); + }); + } + + @Method() + popTo(nav: Nav, indexOrViewCtrl: any, opts?: NavOptions): Promise { + return getDelegate(this).then((delegate) => { + return popToImpl(nav, delegate, indexOrViewCtrl, opts); + }); + } + + @Method() + remove(nav: Nav, startIndex: number, removeCount?: number, opts?: NavOptions): Promise { + return getDelegate(this).then((delegate) => { + return removeImpl(nav, delegate, startIndex, removeCount, opts); + }); + } + + @Method() + removeView(nav: Nav, viewController: ViewController, opts?: NavOptions): Promise { + return getDelegate(this).then((delegate) => { + return removeViewImpl(nav, delegate, viewController, opts); + }); + } + + @Method() + setPages(nav: Nav, componentDataPairs: ComponentDataPair[], opts? : NavOptions): Promise { + return getDelegate(this).then((delegate) => { + return setPagesImpl(nav, delegate, componentDataPairs, opts); + }); + } + + render() { + return ; + } +} + +export function getDelegate(navController: NavController): Promise { + if (navController.delegate) { + return Promise.resolve(navController.delegate); + } + // no delegate is set, so fall back to inserting the stencil-ion-nav-delegate + const element = document.createElement('stencil-ion-nav-delegate'); + document.body.appendChild(element); + return isReady(element).then(() => { + defaultDelegate = element as any as FrameworkDelegate; + return defaultDelegate; + }) +} \ No newline at end of file diff --git a/packages/core/src/components/nav-controller/stencil-nav-delegate.tsx b/packages/core/src/components/nav-controller/stencil-nav-delegate.tsx new file mode 100644 index 0000000000..2c22b524a1 --- /dev/null +++ b/packages/core/src/components/nav-controller/stencil-nav-delegate.tsx @@ -0,0 +1,30 @@ +import { Component, Method } from '@stencil/core'; + +import { StencilElement } from '../..'; +import { FrameworkDelegate, Nav, ViewController } from '../../navigation/nav-interfaces'; + +@Component({ + tag: 'stencil-ion-nav-delegate' +}) +export class StencilNavDelegate implements FrameworkDelegate { + + @Method() + attachViewToDom(nav: Nav, enteringView: ViewController): Promise { + return new Promise((resolve) => { + const usersElement = document.createElement(enteringView.component); + const ionPage = document.createElement('ion-page'); + enteringView.element = ionPage; + ionPage.appendChild(usersElement); + nav.element.appendChild(ionPage); + (ionPage as StencilElement).componentOnReady(() => { + resolve(); + }); + }); + } + + @Method() + removeViewFromDom(nav: Nav, leavingView: ViewController): Promise { + nav.element.removeChild(leavingView.element); + return Promise.resolve(); + } +} \ No newline at end of file diff --git a/packages/core/src/components/nav/nav.tsx b/packages/core/src/components/nav/nav.tsx index b14ba4cdc9..f2171ccc3c 100644 --- a/packages/core/src/components/nav/nav.tsx +++ b/packages/core/src/components/nav/nav.tsx @@ -1,16 +1,18 @@ import { Component, Element, Method, Prop } from '@stencil/core'; -import { FrameworkDelegate, NavController, NavOptions, ViewController } from '../../navigation/nav-interfaces'; -import { getNextNavId, getViews, pop, push, setRoot } from '../../navigation/nav-controller-functions'; +import { AnimationController, Config } from '../..'; +import { ComponentDataPair, FrameworkDelegate, Nav, NavController, NavOptions, ViewController } from '../../navigation/nav-interfaces'; -import { delegate as defaultStencilDelegate } from '../../navigation/stencil-framework-delegate'; +import { getActiveImpl, getFirstView, getPreviousImpl, getViews, init } from '../../navigation/nav-utils'; +import { isReady } from '../../utils/helpers'; @Component({ tag: 'ion-nav', }) -export class Nav implements NavController { +export class IonNav implements Nav { @Element() element: HTMLElement; id: number; + parent: Nav; views: ViewController[]; transitioning?: boolean; destroyed?: boolean; @@ -18,10 +20,13 @@ export class Nav implements NavController { isViewInitialized?: boolean; isPortal: boolean; swipeToGoBackTransition: any; // TODO Transition - childNavs?: NavController[]; + childNavs?: Nav[]; + navController?: NavController; @Prop() root: any; @Prop() delegate: FrameworkDelegate; + @Prop({ connect: 'ion-animation-controller' }) animationCtrl: AnimationController; + @Prop({ context: 'config' }) config: Config; constructor() { init(this); @@ -35,22 +40,79 @@ export class Nav implements NavController { return getViews(this); } - getParent(): NavController { - return null; // TODO + @Method() + push(component: any, data?: any, opts?: NavOptions) { + return pushImpl(this, component, data, opts); } @Method() - push(component: any, data?: any, opts: NavOptions = {}) { - return push(this, this.delegate || defaultStencilDelegate, component, data, opts); + pop(opts?: NavOptions) { + return popImpl(this, opts); } @Method() - pop(opts: NavOptions = {}) { - return pop(this, this.delegate || defaultStencilDelegate, opts); + setRoot(component: any, data?: any, opts?: NavOptions) { + return setRootImpl(this, component, data, opts); } - setRoot(component: any, data?: any, opts: NavOptions = {}) { - return setRoot(this, this.delegate || defaultStencilDelegate, component, data, opts); + @Method() + insert(insertIndex: number, page: any, params?: any, opts?: NavOptions) { + return insertImpl(this, insertIndex, page, params, opts); + } + + @Method() + insertPages(insertIndex: number, insertPages: any[], opts?: NavOptions) { + return insertPagesImpl(this, insertIndex, insertPages, opts); + } + + @Method() + popToRoot(opts?: NavOptions) { + return popToRootImpl(this, opts); + } + + @Method() + popTo(indexOrViewCtrl: any, opts?: NavOptions) { + return popToImpl(this, indexOrViewCtrl, opts); + } + + @Method() + remove(startIndex: number, removeCount?: number, opts?: NavOptions) { + return removeImpl(this, startIndex, removeCount, opts); + } + + @Method() + removeView(viewController: ViewController, opts?: NavOptions) { + return removeViewImpl(this, viewController, opts); + } + + @Method() + setPages(componentDataPairs: ComponentDataPair[], opts? : NavOptions) { + return setPagesImpl(this, componentDataPairs, opts); + } + + @Method() + getActive(): ViewController { + return getActiveImpl(this); + } + + @Method() + getPrevious(view?: ViewController): ViewController { + return getPreviousImpl(this, view); + } + + @Method() + canGoBack(nav: Nav) { + return nav.views && nav.views.length > 0; + } + + @Method() + canSwipeBack() { + return true; // TODO, implement this for real + } + + @Method() + getFirstView() { + return getFirstView(this); } render() { @@ -58,7 +120,70 @@ export class Nav implements NavController { } } -export function init(nav: NavController) { - nav.id = getNextNavId(); - nav.views = []; +export function pushImpl(nav: Nav, component: any, data: any, opts: NavOptions) { + return getNavController(nav).then(() => { + return nav.navController.push(nav, component, data, opts); + }); +} + +export function popImpl(nav: Nav, opts: NavOptions) { + return getNavController(nav).then(() => { + return nav.navController.pop(nav, 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 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 function insertPagesImpl(nav: Nav, insertIndex: number, insertPages: any[], opts: NavOptions) { + return getNavController(nav).then(() => { + return nav.navController.insertPages(nav, insertIndex, insertPages, opts); + }); +} + +export function popToRootImpl(nav: Nav, opts: NavOptions) { + return getNavController(nav).then(() => { + return nav.navController.popToRoot(nav, opts); + }); +} + +export function popToImpl(nav: Nav, indexOrViewCtrl: any, opts: NavOptions) { + return getNavController(nav).then(() => { + return nav.navController.popTo(nav, indexOrViewCtrl, opts); + }); +} + +export function removeImpl(nav: Nav, startIndex: number, removeCount: number, opts: NavOptions) { + return getNavController(nav).then(() => { + return nav.navController.remove(nav, startIndex, removeCount, opts); + }); +} + +export function removeViewImpl(nav: Nav, viewController: ViewController, opts?: NavOptions) { + return getNavController(nav).then(() => { + return nav.navController.removeView(nav, 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 { + 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); } \ No newline at end of file diff --git a/packages/core/src/components/nav/test/basic.html b/packages/core/src/components/nav/test/basic.html index 1947ecd028..d5ae6ba627 100644 --- a/packages/core/src/components/nav/test/basic.html +++ b/packages/core/src/components/nav/test/basic.html @@ -7,6 +7,8 @@ + + diff --git a/packages/core/src/components/page/page.scss b/packages/core/src/components/page/page.scss index 0e8c825def..999e2544bf 100644 --- a/packages/core/src/components/page/page.scss +++ b/packages/core/src/components/page/page.scss @@ -14,4 +14,12 @@ ion-page { height: 100%; contain: strict; + + // do not show, but still render so we can get dimensions + opacity: 0; } + +ion-page.show-page { + // show the page now that it's ready + opacity: 1; +} \ No newline at end of file diff --git a/packages/core/src/index.d.ts b/packages/core/src/index.d.ts index c25bf67e9b..3a8b2d7f20 100644 --- a/packages/core/src/index.d.ts +++ b/packages/core/src/index.d.ts @@ -1,4 +1,6 @@ -import { Animation, AnimationBuilder, AnimationController } from './components/animation-controller/animation-interface'; +import * as Stencil from '@stencil/core'; + +import { Animation, AnimationBuilder, AnimationController, AnimationOptions } from './components/animation-controller/animation-interface'; import { ActionSheet, ActionSheetButton, ActionSheetEvent, ActionSheetOptions } from './components/action-sheet/action-sheet'; import { ActionSheetController } from './components/action-sheet-controller/action-sheet-controller'; import { Alert, AlertButton, AlertEvent, AlertInput, AlertOptions } from './components/alert/alert'; @@ -19,12 +21,12 @@ import { PopoverController } from './components/popover-controller/popover-contr import { Scroll, ScrollCallback, ScrollDetail } from './components/scroll/scroll'; import { Segment } from './components/segment/segment'; import { SegmentButton, SegmentButtonEvent } from './components/segment-button/segment-button'; - import { Toast, ToastEvent, ToastOptions } from './components/toast/toast' import { ToastController } from './components/toast-controller/toast-controller' import * as Stencil from '@stencil/core'; +import { TransitionBuilder } from './navigation/nav-interfaces'; export interface Config { get: (key: string, fallback?: any) => any; @@ -67,6 +69,7 @@ export { Animation, AnimationBuilder, AnimationController, + AnimationOptions, Backdrop, GestureCallback, GestureDetail, @@ -91,8 +94,13 @@ export { Segment, SegmentButton, SegmentButtonEvent, + TransitionBuilder, Toast, ToastEvent, ToastOptions, ToastController } + +export interface StencilElement extends HTMLElement { + componentOnReady?: (cb: (elm?: StencilElement) => void) => void; +} \ No newline at end of file diff --git a/packages/core/src/navigation/nav-controller-functions.ts b/packages/core/src/navigation/nav-controller-functions.ts index 5cbf44fea6..40b267b85a 100644 --- a/packages/core/src/navigation/nav-controller-functions.ts +++ b/packages/core/src/navigation/nav-controller-functions.ts @@ -1,10 +1,11 @@ -import { AnimationOptions } from '../components/animation-controller/animation-interface'; +import { Animation, AnimationOptions } from '../components/animation-controller/animation-interface'; import { ComponentDataPair, FrameworkDelegate, - NavController, + Nav, NavOptions, NavResult, + Transition, TransitionInstruction, ViewController } from './nav-interfaces'; @@ -14,47 +15,31 @@ import { DIRECTION_FORWARD, STATE_ATTACHED, STATE_DESTROYED, - STATE_INITIALIZED, + STATE_NEW, + VIEW_ID_START, + destroyTransition, + getHydratedTransition, + getNextTransitionId, + getParentTransitionId, isViewController, setZIndex, - toggleHidden + toggleHidden, + transitionFactory, } from './nav-utils'; import { ViewControllerImpl } from './view-controller-impl'; import { assert, isDef, isNumber } from '../utils/helpers'; -import { NAV_ID_START, VIEW_ID_START } from '../utils/ids'; + +import { buildIOSTransition } from './transitions/transition.ios'; +import { buildMdTransition } from './transitions/transition.md'; const queueMap = new Map(); // public api -export function canGoBack(nav: NavController) { - return nav.views && nav.views.length > 0; -} -export function canSwipeBack() { - return true; - // TODO - implement this for real -} - -export function getFirstView(nav: NavController): ViewController { - return nav.views && nav.views.length > 0 ? nav.views[0] : null; -} - -export function getActiveView(nav: NavController): ViewController { - return nav.views && nav.views.length > 0 ? nav.views[nav.views.length - 1] : null; -} - -export function getActiveChildNavs(nav: NavController): NavController[] { - return nav.childNavs ? nav.childNavs : []; -} - -export function getViews(nav: NavController): ViewController[] { - return nav.views ? nav.views : []; -} - -export function push(nav: NavController, delegate: FrameworkDelegate, component: any, data?: any, opts?: NavOptions, done? : () => void): Promise { +export function push(nav: Nav, delegate: FrameworkDelegate, component: any, data?: any, opts?: NavOptions, done? : () => void): Promise { return queueTransaction({ insertStart: -1, insertViews: [{page: component, params: data}], @@ -65,7 +50,7 @@ export function push(nav: NavController, delegate: FrameworkDelegate, component: }, done); } -export function insert(nav: NavController, delegate: FrameworkDelegate, insertIndex: number, page: any, params?: any, opts?: NavOptions, done?: () => void): Promise { +export function insert(nav: Nav, delegate: FrameworkDelegate, insertIndex: number, page: any, params?: any, opts?: NavOptions, done?: () => void): Promise { return queueTransaction({ insertStart: insertIndex, insertViews: [{ page: page, params: params }], @@ -76,7 +61,7 @@ export function insert(nav: NavController, delegate: FrameworkDelegate, insertIn }, done); } -export function insertPages(nav: NavController, delegate: FrameworkDelegate, insertIndex: number, insertPages: any[], opts?: NavOptions, done?: () => void): Promise { +export function insertPages(nav: Nav, delegate: FrameworkDelegate, insertIndex: number, insertPages: any[], opts?: NavOptions, done?: () => void): Promise { return queueTransaction({ insertStart: insertIndex, insertViews: insertPages, @@ -87,7 +72,7 @@ export function insertPages(nav: NavController, delegate: FrameworkDelegate, ins }, done); } -export function pop(nav: NavController, delegate: FrameworkDelegate, opts?: NavOptions, done?: () => void): Promise { +export function pop(nav: Nav, delegate: FrameworkDelegate, opts?: NavOptions, done?: () => void): Promise { return queueTransaction({ removeStart: -1, removeCount: 1, @@ -98,7 +83,7 @@ export function pop(nav: NavController, delegate: FrameworkDelegate, opts?: NavO }, done); } -export function popToRoot(nav: NavController, delegate: FrameworkDelegate, opts?: NavOptions, done?: () => void): Promise { +export function popToRoot(nav: Nav, delegate: FrameworkDelegate, opts?: NavOptions, done?: () => void): Promise { return queueTransaction({ removeStart: 1, removeCount: -1, @@ -109,7 +94,7 @@ export function popToRoot(nav: NavController, delegate: FrameworkDelegate, opts? }, done); } -export function popTo(nav: NavController, delegate: FrameworkDelegate, indexOrViewCtrl: any, opts?: NavOptions, done?: () => void): Promise { +export function popTo(nav: Nav, delegate: FrameworkDelegate, indexOrViewCtrl: any, opts?: NavOptions, done?: () => void): Promise { const config: TransitionInstruction = { removeStart: -1, removeCount: -1, @@ -127,7 +112,7 @@ export function popTo(nav: NavController, delegate: FrameworkDelegate, indexOrVi return queueTransaction(config, done); } -export function remove(nav: NavController, delegate: FrameworkDelegate, startIndex: number, removeCount: number = 1, opts?: NavOptions, done?: () => void): Promise { +export function remove(nav: Nav, delegate: FrameworkDelegate, startIndex: number, removeCount: number = 1, opts?: NavOptions, done?: () => void): Promise { return queueTransaction({ removeStart: startIndex, removeCount: removeCount, @@ -138,7 +123,7 @@ export function remove(nav: NavController, delegate: FrameworkDelegate, startInd }, done); } -export function removeView(nav: NavController, delegate: FrameworkDelegate, viewController: ViewController, opts?: NavOptions, done?: () => void): Promise { +export function removeView(nav: Nav, delegate: FrameworkDelegate, viewController: ViewController, opts?: NavOptions, done?: () => void): Promise { return queueTransaction({ removeView: viewController, removeStart: 0, @@ -150,11 +135,11 @@ export function removeView(nav: NavController, delegate: FrameworkDelegate, view }, done); } -export function setRoot(nav: NavController, delegate: FrameworkDelegate, page: any, params?: any, opts?: NavOptions, done?: () => void): Promise { +export function setRoot(nav: Nav, delegate: FrameworkDelegate, page: any, params?: any, opts?: NavOptions, done?: () => void): Promise { return setPages(nav, delegate, [{ page: page, params: params }], opts, done); } -export function setPages(nav: NavController, delegate: FrameworkDelegate, componentDataPars: ComponentDataPair[], opts? : NavOptions, done?: () => void): Promise { +export function setPages(nav: Nav, delegate: FrameworkDelegate, componentDataPars: ComponentDataPair[], opts? : NavOptions, done?: () => void): Promise { if (!isDef(opts)) { opts = {}; } @@ -207,7 +192,7 @@ export function queueTransaction(ti: TransitionInstruction, done: () => void): P return promise; } -export function nextTransaction(nav: NavController): Promise { +export function nextTransaction(nav: Nav): Promise { if (nav.transitioning) { return Promise.resolve(); @@ -218,7 +203,13 @@ export function nextTransaction(nav: NavController): Promise { return Promise.resolve(); } - return initializeViewBeforeTransition(topTransaction).then(([enteringView, leavingView]) => { + let enteringView: ViewController; + let leavingView: ViewController; + return initializeViewBeforeTransition(topTransaction).then(([_enteringView, _leavingView]) => { + enteringView = _enteringView; + leavingView = _leavingView; + return attachViewToDom(nav, enteringView, topTransaction.delegate); + }).then(() => { return loadViewAndTransition(nav, enteringView, leavingView, topTransaction); }).then((result: NavResult) => { return successfullyTransitioned(result, topTransaction); @@ -253,7 +244,6 @@ export function successfullyTransitioned(result: NavResult, ti: TransitionInstru ); } ti.resolve(result.hasCompleted); - console.log('success'); } export function transitionFailed(error: Error, ti: TransitionInstruction) { @@ -274,7 +264,6 @@ export function transitionFailed(error: Error, ti: TransitionInstruction) { nextTransaction(ti.nav); fireError(error, ti); - console.log('fail'); } export function fireError(error: Error, ti: TransitionInstruction) { @@ -288,7 +277,7 @@ export function fireError(error: Error, ti: TransitionInstruction) { } } -export function loadViewAndTransition(nav: NavController, enteringView: ViewController, leavingView: ViewController, ti: TransitionInstruction) { +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 @@ -300,8 +289,9 @@ export function loadViewAndTransition(nav: NavController, enteringView: ViewCont }); } - // TODO - transitionId - nav.transitionId = 1; //getRootTransitionId(nav) || nextId(); + let transition: Transition = null; + const transitionId = getParentTransitionId(nav); + nav.transitionId = transitionId >= 0 ? transitionId : getNextTransitionId(); // create the transition options const animationOpts: AnimationOptions = { @@ -313,47 +303,33 @@ export function loadViewAndTransition(nav: NavController, enteringView: ViewCont ev: ti.opts.event, }; - // TODO - need transition here - const transition: any = { - opts: animationOpts - };// new DanTransition(enteringView, leavingView, animationOpts); + return nav.animationCtrl.create().then((animation: Animation) => { + const emptyTransition = transitionFactory(animation); + console.log('nav.config: ', nav.config); + console.log('mode: ', nav.config.get('mode')); + transition = getHydratedTransition(animationOpts.animation, nav.config, nav.transitionId, emptyTransition, enteringView, leavingView, animationOpts, buildMdTransition); - //const transition = getTransition(stateData.transitionId, enteringView, animationOpts); - - 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; - } - - // use the resolve function of this promise to trigger the - // beginTransitioning method - const promiseToReturn = new Promise((resolve) => { - transition.registerStart(resolve); - }); - - - return attachViewToDom(nav, enteringView, ti.delegate).then(() => { - if (!transition.hasChildren) { - // lowest level transition, so kick it off and let it bubble up to start all of them - transition.start(); + if (nav.swipeToGoBackTransition) { + nav.swipeToGoBackTransition.destroy(); + nav.swipeToGoBackTransition = null; } - return promiseToReturn; + + // it's a swipe to go back transition + if (transition.isRoot() && ti.opts.progressAnimation) { + nav.swipeToGoBackTransition = transition; + } + + transition.start(); }).then(() => { - // TODO - get the shouldAnimate param from the config - return executeAsyncTransition(nav, transition, enteringView, leavingView, ti.opts, false); + return executeAsyncTransition(nav, transition, enteringView, leavingView, ti.delegate, ti.opts, ti.nav.config.getBoolean('animate')); }); } // TODO - transition type -export function executeAsyncTransition(nav: NavController, transition: any, enteringView: ViewController, leavingView: ViewController, opts: NavOptions, configShouldAnimate: boolean): Promise { +export function executeAsyncTransition(nav: Nav, transition: Transition, enteringView: ViewController, leavingView: ViewController, delegate: FrameworkDelegate, opts: NavOptions, configShouldAnimate: boolean): Promise { assert(nav.transitioning, 'must be transitioning'); nav.transitionId = null; - setZIndex(nav.isPortal, enteringView, leavingView, opts.direction); + setZIndex(nav, enteringView, leavingView, opts.direction); // always ensure the entering view is viewable // ******** DOM WRITE **************** @@ -364,15 +340,13 @@ export function executeAsyncTransition(nav: NavController, transition: any, ente // ******** DOM WRITE **************** leavingView && toggleHidden(leavingView.element, true, true); - // initialize the transition - transition.init() - - const shouldNotAnimate = (!nav.isViewInitialized && nav.views.length === 1) && !nav.isPortal; - if (configShouldAnimate === false || shouldNotAnimate) { + const isFirstPage = !nav.isViewInitialized && nav.views.length === 1; + const shouldNotAnimate = isFirstPage && !nav.isPortal; + if (configShouldAnimate || shouldNotAnimate) { opts.animate = false; } - if (!opts.animate) { + if (opts.animate === false) { // if it was somehow set to not animation, then make the duration zero transition.duration(0); } @@ -412,55 +386,61 @@ export function executeAsyncTransition(nav: NavController, transition: any, ente } return transitionCompletePromise.then(() => { - return transitionFinish(nav, transition, opts); + return transitionFinish(nav, transition, delegate, opts); }); } -// TODO - transition type -export function transitionFinish(nav: NavController, transition: any, opts: NavOptions): NavResult { +export function transitionFinish(nav: Nav, transition: Transition, delegate: FrameworkDelegate, opts: NavOptions): Promise { + + let promise: Promise = null; + if (transition.hasCompleted) { transition.enteringView && transition.enteringView.didEnter(); transition.leavingView && transition.leavingView.didLeave(); - cleanUpView(nav, transition.enteringView); + promise = cleanUpView(nav, delegate, transition.enteringView); } else { - cleanUpView(nav, transition.leavingView); + promise = cleanUpView(nav, delegate, transition.leavingView); } - if (transition.isRoot()) { + return promise.then(() => { + if (transition.isRoot()) { - // TODO - destroy the transition object - //destroy(transition.transitionId); + destroyTransition(transition.transitionId); - // TODO - enable app + // TODO - enable app - nav.transitioning = false; + nav.transitioning = false; - // TODO - navChange on the deep linker used to be called here + // TODO - navChange on the deep linker used to be called here - if (opts.keyboardClose) { - // TODO - close the keyboard + if (opts.keyboardClose) { + // TODO - close the keyboard + } } - } - return { - hasCompleted: transition.hasCompleted, - requiresTransition: true, - direction: opts.direction - } + return { + hasCompleted: transition.hasCompleted, + requiresTransition: true, + direction: opts.direction + } + }); } -export function cleanUpView(nav: NavController, activeViewController: ViewController) { +export function cleanUpView(nav: Nav, delegate: FrameworkDelegate, activeViewController: ViewController): Promise { + if (nav.destroyed) { - return; + return Promise.resolve(); } + const activeIndex = nav.views.indexOf(activeViewController); + const promises: Promise[] = []; 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(); - destroyView(nav, inactiveViewController); + 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 @@ -468,6 +448,7 @@ export function cleanUpView(nav: NavController, activeViewController: ViewContro } // TODO - review existing z index code! } + return Promise.all(promises); } @@ -476,8 +457,14 @@ export function fireViewWillLifecycles(enteringView: ViewController, leavingView enteringView && enteringView.willEnter(); } -export function attachViewToDom(nav: NavController, enteringView: ViewController, delegate: FrameworkDelegate) { - return delegate.attachViewToDom(nav, enteringView); +export function attachViewToDom(nav: Nav, enteringView: ViewController, delegate: FrameworkDelegate) { + if (enteringView && enteringView.state === STATE_NEW) { + return delegate.attachViewToDom(nav, enteringView).then(() => { + enteringView.state = STATE_ATTACHED; + }); + } + // it's in the wrong state, so don't attach and just return + return Promise.resolve(); } export function initializeViewBeforeTransition(ti: TransitionInstruction): Promise { @@ -486,7 +473,7 @@ export function initializeViewBeforeTransition(ti: TransitionInstruction): Promi return startTransaction(ti).then(() => { const viewControllers = convertComponentToViewController(ti); ti.insertViews = viewControllers; - leavingView = getActiveView(ti.nav); + leavingView = ti.nav.getActive(); enteringView = getEnteringView(ti, ti.nav, leavingView); if (!leavingView && !enteringView) { @@ -494,7 +481,7 @@ export function initializeViewBeforeTransition(ti: TransitionInstruction): Promi } // mark state as initialized - enteringView.state = STATE_INITIALIZED; + //enteringView.state = STATE_INITIALIZED; ti.requiresTransition = (ti.enteringRequiresTransition || ti.leavingRequiresTransition) && enteringView !== leavingView; return testIfViewsCanLeaveAndEnter(enteringView, leavingView, ti); }).then(() => { @@ -570,7 +557,7 @@ export function updateNavStacks(enteringView: ViewController, leavingView: ViewC const destroyQueuePromises: Promise[] = []; for (const viewController of destroyQueue) { - destroyQueuePromises.push(destroyView(ti.nav, viewController)); + destroyQueuePromises.push(destroyView(ti.nav, ti.delegate, viewController)); } return Promise.all(destroyQueuePromises); } @@ -587,13 +574,13 @@ export function updateNavStacks(enteringView: ViewController, leavingView: ViewC }); } -export function destroyView(nav: NavController, viewController: ViewController) { - return viewController.destroy().then(() => { +export function destroyView(nav: Nav, delegate: FrameworkDelegate, viewController: ViewController) { + return viewController.destroy(delegate).then(() => { return removeViewFromList(nav, viewController); }); } -export function removeViewFromList(nav: NavController, viewController: 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'); @@ -602,7 +589,7 @@ export function removeViewFromList(nav: NavController, viewController: ViewContr } } -export function insertViewIntoNav(nav: NavController, view: ViewController, index: number) { +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!! @@ -709,7 +696,7 @@ export function startTransaction(ti: TransitionInstruction): Promise { return Promise.resolve(); } -export function getEnteringView(ti: TransitionInstruction, nav: NavController, leavingView: ViewController): ViewController { +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 @@ -732,8 +719,7 @@ export function convertViewsToViewControllers(views: any[]): ViewController[] { if (isViewController(view)) { return view as ViewController; } - // TODO - make this clean - return (new ViewControllerImpl(view.page, view.params) as any) as ViewController; + return new ViewControllerImpl(view.page, view.params); } return null; }).filter(view => !!view); @@ -786,10 +772,6 @@ export function getTopTransaction(id: number) { return toReturn; } -export function getNextNavId() { - return navControllerIds++; -} -let navControllerIds = NAV_ID_START; let viewIds = VIEW_ID_START; const DISABLE_APP_MINIMUM_DURATION = 64; \ No newline at end of file diff --git a/packages/core/src/navigation/nav-interfaces.d.ts b/packages/core/src/navigation/nav-interfaces.d.ts index 3ca803d501..182828ed18 100644 --- a/packages/core/src/navigation/nav-interfaces.d.ts +++ b/packages/core/src/navigation/nav-interfaces.d.ts @@ -1,21 +1,47 @@ +import { + Animation, + AnimationController, + AnimationOptions, + Config +} from '..'; + export interface FrameworkDelegate { - attachViewToDom(navController: NavController, enteringView: ViewController): Promise; - removeViewFromDom(navController: NavController, leavingView: ViewController): Promise; - destroy(viewController: ViewController): Promise; + attachViewToDom(navController: Nav, enteringView: ViewController): Promise; + removeViewFromDom(navController: Nav, leavingView: ViewController): Promise; } -export interface NavController { - id: number; - element: HTMLElement; +export interface Nav { + id?: number; + element?: HTMLElement; views?: ViewController[]; transitioning?: boolean; destroyed?: boolean; transitionId?: number; isViewInitialized?: boolean; isPortal?: boolean; + zIndexOffset?: number; swipeToGoBackTransition?: any; // TODO Transition - getParent(): NavController; - childNavs?: NavController[]; // TODO - make nav container + navController?: NavController; + parent?: Nav; + getActive(): ViewController; + getPrevious(view?: ViewController): ViewController; + childNavs?: Nav[]; // TODO - make nav container + animationCtrl?: AnimationController; + config?: Config; +} + +export interface NavController { + push(nav: Nav, component: any, data: any, opts: NavOptions): Promise; + pop(nav: Nav, opts: NavOptions): Promise; + setRoot(nav: Nav, component: any, data: any, opts: NavOptions): Promise; + insert(nav: Nav, insertIndex: number, page: any, params: any, opts: NavOptions): Promise; + insertPages(nav: Nav, insertIndex: number, insertPages: any[], opts?: NavOptions): Promise; + popToRoot(nav: Nav, opts: NavOptions): Promise; + popTo(nav: Nav, indexOrViewCtrl: any, opts?: NavOptions): Promise; + remove(nav: Nav, startIndex: number, removeCount: number, opts: NavOptions): Promise; + removeView(nav: Nav, viewController: ViewController, opts?: NavOptions): Promise; + setPages(nav: Nav, componentDataPairs: ComponentDataPair[], opts? : NavOptions): Promise; + delegate?: FrameworkDelegate; } export interface ViewController { @@ -25,9 +51,9 @@ export interface ViewController { element: HTMLElement; instance: any; state: number; - nav: NavController; - frameworkDelegate: FrameworkDelegate; + nav: Nav; dismissProxy?: any; + zIndex: number; // life cycle events willLeave(unload: boolean): void; @@ -38,7 +64,7 @@ export interface ViewController { didLoad(): void; willUnload():void; - destroy(): Promise; + destroy(delegate?: FrameworkDelegate): Promise; getTransitionName(direction: string): string; onDidDismiss: (data: any, role: string) => void; onWillDismiss: (data: any, role: string) => void; @@ -81,7 +107,7 @@ export interface TransitionInstruction { enteringRequiresTransition?: boolean; requiresTransition?: boolean; id?: number; - nav?: NavController; + nav?: Nav; delegate?: FrameworkDelegate; } @@ -95,3 +121,17 @@ export interface ComponentDataPair { page: any; params: any; } + +export interface Transition extends Animation { + enteringView?: ViewController; + leavingView?: ViewController; + transitionStartFunction?: Function; + transitionId?: number; + registerTransitionStart(callback: Function): void; + start(): void; + originalDestroy(): void; // this is intended to be private, don't use this bad boy +} + +export interface TransitionBuilder { + (rootTransition: Transition, enteringView: ViewController, leavingView: ViewController, opts: AnimationOptions ): Transition; +} \ No newline at end of file diff --git a/packages/core/src/navigation/nav-utils.ts b/packages/core/src/navigation/nav-utils.ts index cc0be049a6..7fe0d87afd 100644 --- a/packages/core/src/navigation/nav-utils.ts +++ b/packages/core/src/navigation/nav-utils.ts @@ -1,4 +1,6 @@ -import { ViewController } from './nav-interfaces'; +import { Nav, Transition, ViewController } from './nav-interfaces'; +import { Animation, AnimationOptions, Config, TransitionBuilder } from '..'; +import { isDef } from '../utils/helpers' export const STATE_NEW = 1; export const STATE_INITIALIZED = 2; @@ -6,6 +8,7 @@ export const STATE_ATTACHED = 3; export const STATE_DESTROYED = 4; export const INIT_ZINDEX = 100; +export const PORTAL_Z_INDEX_OFFSET = 0; export const DIRECTION_BACK = 'back'; export const DIRECTION_FORWARD = 'forward'; @@ -14,18 +17,175 @@ export const DIRECTION_SWITCH = 'switch'; export const NAV = 'nav'; export const TABS = 'tabs'; +export let NAV_ID_START = 1000; +export let VIEW_ID_START = 2000; +let transitionIds = 0; +let activeTransitions = new Map(); + +let portalZindex = 9999; export function isViewController(object: any): boolean { return !!(object && object.didLoad && object.willUnload); } -export function setZIndex(_isPortal: boolean, _enteringView: ViewController, _leavingView: ViewController, _direction: string) { - // TODO +export function setZIndex(nav: Nav, enteringView: ViewController, leavingView: ViewController, direction: string) { + if (enteringView) { + if (nav.isPortal) { + if (direction === DIRECTION_FORWARD) { + updateZIndex(enteringView, nav.zIndexOffset + portalZindex); + } + portalZindex++; + return; + } + + leavingView = leavingView || nav.getPrevious(enteringView); + + if (leavingView && isDef(leavingView.zIndex)) { + if (direction === DIRECTION_BACK) { + updateZIndex(enteringView, leavingView.zIndex - 1); + + } else { + updateZIndex(enteringView, leavingView.zIndex + 1); + } + + } else { + updateZIndex(enteringView, INIT_ZINDEX + nav.zIndexOffset); + } + } +} + +export function updateZIndex(viewController: ViewController, newZIndex: number) { + if (newZIndex !== viewController.zIndex) { + viewController.zIndex = newZIndex; + viewController.element.style.zIndex = '' + newZIndex; + } } export function toggleHidden(element: HTMLElement, isVisible: Boolean, shouldBeVisible: boolean) { if (isVisible !== shouldBeVisible) { element.hidden = shouldBeVisible; } -} \ No newline at end of file +} + +export function canNavGoBack(nav: Nav) { + if (!nav) { + return false; + } + return !!nav.getPrevious(); +} + +export function transitionFactory(animation: Animation): Transition { + (animation as any).registerTransitionStart = (callback: Function) => { + (animation as any).transitionStartFunction = callback; + } + + (animation as any).start = function() { + this.transitionStartFunction && this.transitionStartFunction(); + this.transitionStartFunction = null; + transitionStartImpl(animation as Transition); + }; + + (animation as any).originalDestroy = animation.destroy; + + (animation as any).destroy = function() { + transitionDestroyImpl(animation as Transition); + }; + + return animation as Transition; +} + +export function transitionStartImpl(transition: Transition) { + transition.transitionStartFunction && transition.transitionStartFunction(); + transition.transitionStartFunction = null; + transition.parent && (transition.parent as Transition).start(); +} + +export function transitionDestroyImpl(transition: Transition) { + transition.originalDestroy(); + transition.parent = transition.enteringView = transition.leavingView = transition.transitionStartFunction = null; +} + +export function getParentTransitionId(nav: Nav) { + nav = nav.parent + while (nav) { + const transitionId = nav.transitionId; + if (isDef(transitionId)) { + return transitionId; + } + nav = nav.parent + } + return -1; +} + +export function getNextTransitionId() { + return transitionIds++; +} + +export function destroyTransition(transitionId: number) { + const transition = activeTransitions.get(transitionId); + if (transition) { + transition.destroy(); + activeTransitions.delete(transitionId); + } +} + +export function getHydratedTransition(name: string, config: Config, transitionId: number, emptyTransition: Transition, enteringView: ViewController, leavingView: ViewController, opts: AnimationOptions, defaultTransitionFactory: TransitionBuilder) { + + const transitionFactory = config.get(name) as TransitionBuilder || defaultTransitionFactory; + const hydratedTransition = transitionFactory(emptyTransition, enteringView, leavingView, opts); + hydratedTransition.transitionId = transitionId; + + if (!activeTransitions.has(transitionId)) { + // sweet, this is the root transition + activeTransitions.set(transitionId, hydratedTransition); + } else { + // we've got a parent transition going + // just append this transition to the existing one + activeTransitions.get(transitionId).add(hydratedTransition); + } + + return hydratedTransition; +} + +export function canGoBack(nav: Nav) { + return nav.views && nav.views.length > 0; +} + +export function canSwipeBack(_nav: Nav) { + return true; +} + +export function getFirstView(nav: Nav): ViewController { + return nav.views && nav.views.length > 0 ? nav.views[0] : null; +} + +export function getActiveChildNavs(nav: Nav): Nav[] { + return nav.childNavs ? nav.childNavs : []; +} + +export function getViews(nav: Nav): ViewController[] { + return nav.views ? nav.views : []; +} + +export function init(nav: Nav) { + nav.id = getNextNavId(); + nav.views = []; +} + +export function getActiveImpl(nav: Nav): ViewController { + return nav.views && nav.views.length > 0 ? nav.views[nav.views.length - 1] : null; +} + +export function getPreviousImpl(nav: Nav, viewController: ViewController): ViewController { + if (!viewController) { + viewController = nav.getActive(); + } + return nav.views[nav.views.indexOf(viewController) - 1]; +} + +export function getNextNavId() { + return navControllerIds++; +} + +let navControllerIds = NAV_ID_START; \ No newline at end of file diff --git a/packages/core/src/navigation/stencil-framework-delegate.ts b/packages/core/src/navigation/stencil-framework-delegate.ts deleted file mode 100644 index d4186b7f5c..0000000000 --- a/packages/core/src/navigation/stencil-framework-delegate.ts +++ /dev/null @@ -1,32 +0,0 @@ -import { NavController, ViewController } from './nav-interfaces'; - -export function attachViewToDom(nav: NavController, enteringView: ViewController): Promise { - return new Promise((resolve) => { - const usersElement = document.createElement(enteringView.component); - const ionPage = document.createElement('ion-page'); - enteringView.element = ionPage; - ionPage.appendChild(usersElement); - (ionPage as any).componentDidLoad = () => { - resolve(); - }; - - nav.element.appendChild(ionPage); - }); -} - -export function removeViewFromDom(nav: NavController, leavingView: ViewController): Promise { - nav.element.removeChild(leavingView.element); - return Promise.resolve(); -} - -export function destroy(_viewController: ViewController) { - return Promise.resolve(); -} - -const delegate = { - attachViewToDom: attachViewToDom, - removeViewFromDom: removeViewFromDom, - destroy: destroy -}; - -export { delegate }; \ No newline at end of file diff --git a/packages/core/src/navigation/transitions/transition.ios.ts b/packages/core/src/navigation/transitions/transition.ios.ts new file mode 100644 index 0000000000..9e93248652 --- /dev/null +++ b/packages/core/src/navigation/transitions/transition.ios.ts @@ -0,0 +1,181 @@ +import { AnimationOptions } from '../..'; +import { Transition, ViewController } from '../nav-interfaces'; +import { canNavGoBack } from '../nav-utils'; + +import { isDef } from '../../utils/helpers'; + +const DURATION = 500; +const EASING = 'cubic-bezier(0.36,0.66,0.04,1)'; +const OPACITY = 'opacity'; +const TRANSFORM = 'transform'; +const TRANSLATEX = 'translateX'; +const CENTER = '0%'; +const OFF_OPACITY = 0.8; +const SHOW_BACK_BTN_CSS = 'show-back-button'; + +export function buildIOSTransition(rootTransition: Transition, enteringView: ViewController, leavingView: ViewController, opts: AnimationOptions): Transition { + + rootTransition.enteringView = enteringView; + rootTransition.leavingView = leavingView; + + const isRTL = document.dir === 'rtl'; + const OFF_RIGHT = isRTL ? '-99.5%' : '99.5%'; + const OFF_LEFT = isRTL ? '33%' : '-33%'; + + rootTransition.duration(isDef(opts.duration) ? opts.duration : DURATION); + rootTransition.easing(isDef(opts.easing) ? opts.easing : EASING); + + + rootTransition.addElement(enteringView.element); + rootTransition.beforeAddClass('show-page'); + + const backDirection = (opts.direction === 'back'); + + if (enteringView) { + const enteringContent = rootTransition.create(); + enteringContent.addElement(enteringView.element.querySelectorAll('ion-header > *:not(ion-navbar),ion-footer > *')); + + rootTransition.add(enteringContent); + + if (backDirection) { + enteringContent.fromTo(TRANSLATEX, OFF_LEFT, CENTER, true).fromTo(OPACITY, OFF_OPACITY, 1, true); + } else { + // entering content, forward direction + enteringContent.beforeClearStyles([OPACITY]).fromTo(TRANSLATEX, OFF_RIGHT, CENTER, true); + } + + const enteringNavbarEle = enteringView.element.querySelector('ion-navbar'); + if (enteringNavbarEle) { + const enteringNavBar = rootTransition.create(); + enteringNavBar.addElement(enteringNavbarEle); + + rootTransition.add(enteringNavBar); + + const enteringTitle = rootTransition.create(); + enteringTitle.addElement(enteringNavbarEle.querySelector('ion-title')); + const enteringNavbarItems = rootTransition.create(); + enteringNavbarItems.addElement(enteringNavbarEle.querySelectorAll('ion-buttons,[menuToggle]')); + const enteringNavbarBg = rootTransition.create(); + enteringNavbarBg.addElement(enteringNavbarEle.querySelector('.toolbar-background')); + const enteringBackButton = rootTransition.create(); + enteringBackButton.addElement(enteringNavbarEle.querySelector('.back-button')); + + enteringNavBar + .add(enteringTitle) + .add(enteringNavbarItems) + .add(enteringNavbarBg) + .add(enteringBackButton); + + enteringTitle.fromTo(OPACITY, 0.01, 1, true); + enteringNavbarItems.fromTo(OPACITY, 0.01, 1, true); + + if (backDirection) { + enteringTitle.fromTo(TRANSLATEX, OFF_LEFT, CENTER, true); + + if (canNavGoBack(enteringView.nav)) { + // back direction, entering page has a back button + enteringBackButton.beforeAddClass(SHOW_BACK_BTN_CSS).fromTo(OPACITY, 0.01, 1, true); + } + } else { + // entering navbar, forward direction + enteringTitle.fromTo(TRANSLATEX, OFF_RIGHT, CENTER, true); + + enteringNavbarBg.beforeClearStyles([OPACITY]).fromTo(TRANSLATEX, OFF_RIGHT, CENTER, true); + + if (canNavGoBack(enteringView.nav)) { + // forward direction, entering page has a back button + enteringBackButton.beforeAddClass(SHOW_BACK_BTN_CSS).fromTo(OPACITY, 0.01, 1, true); + + + const enteringBackBtnText = rootTransition.create(); + enteringBackBtnText.addElement(enteringNavbarEle.querySelector('.back-button-text')); + + enteringBackBtnText.fromTo(TRANSLATEX, (isRTL ? '-100px' : '100px'), '0px'); + enteringNavBar.add(enteringBackBtnText); + + } else { + enteringBackButton.beforeRemoveClass(SHOW_BACK_BTN_CSS); + } + } + } + } + + // setup leaving view + if (leavingView) { + + const leavingContent = rootTransition.create(); + leavingContent.addElement(leavingView.element); + leavingContent.addElement(leavingView.element.querySelectorAll('ion-header > *:not(ion-navbar),ion-footer > *')); + + rootTransition.add(leavingContent); + + if (backDirection) { + // leaving content, back direction + leavingContent.beforeClearStyles([OPACITY]).fromTo(TRANSLATEX, CENTER, (isRTL ? '-100%' : '100%')); + + } else { + // leaving content, forward direction + leavingContent + .fromTo(TRANSLATEX, CENTER, OFF_LEFT) + .fromTo(OPACITY, 1, OFF_OPACITY) + .afterClearStyles([TRANSFORM, OPACITY]); + } + + const leavingNavbarEle = leavingView.element.querySelector('ion-navbar'); + if (leavingNavbarEle) { + const leavingNavBar = rootTransition.create(); + leavingNavBar.addElement(leavingNavbarEle) + + const leavingTitle = rootTransition.create(); + leavingTitle.addElement(leavingNavbarEle.querySelector('ion-title')); + + const leavingNavbarItems = rootTransition.create(); + leavingNavbarItems.addElement(leavingNavbarEle.querySelectorAll('ion-buttons,[menuToggle]')); + + const leavingNavbarBg = rootTransition.create(); + leavingNavbarBg.addElement(leavingNavbarEle.querySelector('.toolbar-background')); + + const leavingBackButton = rootTransition.create(); + leavingBackButton.addElement(leavingNavbarEle.querySelector('.back-button')); + + leavingNavBar + .add(leavingTitle) + .add(leavingNavbarItems) + .add(leavingBackButton) + .add(leavingNavbarBg); + this.add(leavingNavBar); + + // fade out leaving navbar items + leavingBackButton.fromTo(OPACITY, 0.99, 0); + leavingTitle.fromTo(OPACITY, 0.99, 0); + leavingNavbarItems.fromTo(OPACITY, 0.99, 0); + + if (backDirection) { + // leaving navbar, back direction + leavingTitle.fromTo(TRANSLATEX, CENTER, (isRTL ? '-100%' : '100%')); + + // leaving navbar, back direction, and there's no entering navbar + // should just slide out, no fading out + leavingNavbarBg + .beforeClearStyles([OPACITY]) + .fromTo(TRANSLATEX, CENTER, (isRTL ? '-100%' : '100%')); + + const leavingBackBtnText = rootTransition.create(); + leavingBackBtnText.addElement(leavingNavbarEle.querySelector('.back-button-text')); + leavingBackBtnText.fromTo(TRANSLATEX, CENTER, (isRTL ? -300 : 300) + 'px'); + leavingNavBar.add(leavingBackBtnText); + + } else { + // leaving navbar, forward direction + leavingTitle + .fromTo(TRANSLATEX, CENTER, OFF_LEFT) + .afterClearStyles([TRANSFORM]); + + leavingBackButton.afterClearStyles([OPACITY]); + leavingTitle.afterClearStyles([OPACITY]); + leavingNavbarItems.afterClearStyles([OPACITY]); + } + } + } + return rootTransition; +} \ No newline at end of file diff --git a/packages/core/src/navigation/transitions/transition.md.ts b/packages/core/src/navigation/transitions/transition.md.ts new file mode 100644 index 0000000000..b906ae9313 --- /dev/null +++ b/packages/core/src/navigation/transitions/transition.md.ts @@ -0,0 +1,60 @@ +import { AnimationOptions } from '../..'; +import { Transition, ViewController } from '../nav-interfaces'; +import { canNavGoBack } from '../nav-utils'; + +import { isDef } from '../../utils/helpers'; + +const TRANSLATEY = 'translateY'; +const OFF_BOTTOM = '40px'; +const CENTER = '0px'; +const SHOW_BACK_BTN_CSS = 'show-back-button'; + +export function buildMdTransition(rootTransition: Transition, enteringView: ViewController, leavingView: ViewController, opts: AnimationOptions): Transition { + + rootTransition.enteringView = enteringView; + rootTransition.leavingView = leavingView; + + rootTransition.addElement(enteringView.element); + rootTransition.beforeAddClass('show-page'); + + const backDirection = (opts.direction === 'back'); + if (enteringView) { + if (backDirection) { + rootTransition.duration(isDef(opts.duration) ? opts.duration : 200).easing('cubic-bezier(0.47,0,0.745,0.715)'); + } else { + rootTransition.duration(isDef(opts.duration) ? opts.duration : 280).easing('cubic-bezier(0.36,0.66,0.04,1)'); + + rootTransition + .fromTo(TRANSLATEY, OFF_BOTTOM, CENTER, true) + .fromTo('opacity', 0.01, 1, true); + } + + const enteringNavbarEle = enteringView.element.querySelector('ion-navbar'); + if (enteringNavbarEle) { + const enteringNavBar = rootTransition.create(); + enteringNavBar.addElement(enteringNavbarEle); + rootTransition.add(enteringNavBar); + + const enteringBackButton = rootTransition.create(); + enteringBackButton.addElement(enteringNavbarEle.querySelector('.back-button')); + rootTransition.add(enteringBackButton); + + if (canNavGoBack(enteringView.nav)) { + enteringBackButton.beforeAddClass(SHOW_BACK_BTN_CSS); + } else { + enteringBackButton.beforeRemoveClass(SHOW_BACK_BTN_CSS); + } + } + } + + // setup leaving view + if (leavingView && backDirection) { + // leaving content + rootTransition.duration(opts.duration || 200).easing('cubic-bezier(0.47,0,0.745,0.715)'); + const leavingPage = rootTransition.create(); + leavingPage.addElement(leavingView.element); + rootTransition.add(leavingPage.fromTo(TRANSLATEY, CENTER, OFF_BOTTOM).fromTo('opacity', 1, 0)); + } + + return rootTransition; +} \ No newline at end of file diff --git a/packages/core/src/navigation/view-controller-impl.ts b/packages/core/src/navigation/view-controller-impl.ts index 96a9b87762..86f37784b3 100644 --- a/packages/core/src/navigation/view-controller-impl.ts +++ b/packages/core/src/navigation/view-controller-impl.ts @@ -1,5 +1,5 @@ -import { FrameworkDelegate, NavController, ViewController } from './nav-interfaces'; -import { STATE_ATTACHED, STATE_DESTROYED, STATE_INITIALIZED } from './nav-utils'; +import { FrameworkDelegate, Nav, ViewController } from './nav-interfaces'; +import { STATE_ATTACHED, STATE_DESTROYED, STATE_NEW, STATE_INITIALIZED } from './nav-utils'; import { assert } from '../utils/helpers'; @@ -10,16 +10,17 @@ export class ViewControllerImpl implements ViewController { element: HTMLElement; instance: any; state: number; - nav: NavController; + nav: Nav; overlay: boolean; + zIndex: number; dismissProxy: any; - frameworkDelegate: FrameworkDelegate; + onDidDismiss: (data: any, role: string) => void; onWillDismiss: (data: any, role: string) => void; constructor(public component: any, data?: any) { - this.data = data || {}; + initializeNewViewController(this, data); } /** @@ -62,8 +63,8 @@ export class ViewControllerImpl implements ViewController { willUnloadImpl(this); } - destroy(): Promise { - return destroy(this); + destroy(delegate?: FrameworkDelegate): Promise { + return destroy(this, delegate); } getTransitionName(_direction: string): string { @@ -96,21 +97,15 @@ export function dismiss(navCtrl: any, dismissProxy: any, data?: any, role?: stri return navCtrl.removeView(this, options).then(() => data); } -export function destroy(viewController: ViewController): Promise { - return Promise.resolve().then(() => { - assert(viewController.state !== STATE_DESTROYED, 'view state must be attached'); +export function destroy(viewController: ViewController, delegate?: FrameworkDelegate): Promise { + assert(viewController.state !== STATE_DESTROYED, 'view state must be attached'); + return delegate ? delegate.removeViewFromDom(viewController.nav, viewController) : Promise.resolve().then(() => { if (viewController.component) { // TODO - consider removing classes and styles as thats what we do in ionic-angular } - if (viewController.frameworkDelegate) { - return viewController.frameworkDelegate.destroy(viewController); - } - - return null; - }).then(() => { - viewController.id = viewController.data = viewController.element = viewController.instance = viewController.nav = viewController.dismissProxy = viewController.frameworkDelegate = null; + viewController.id = viewController.data = viewController.element = viewController.instance = viewController.nav = viewController.dismissProxy = null; viewController.state = STATE_DESTROYED; }); } @@ -161,4 +156,9 @@ export function willUnloadImpl(viewController: ViewController) { export function didLoadImpl(viewController: ViewController) { assert(viewController.state === STATE_ATTACHED, 'view state must be ATTACHED'); callLifeCycleFunction(viewController.instance, 'ionViewDidLoad'); +} + +export function initializeNewViewController(viewController: ViewController, data: any) { + viewController.state = STATE_NEW; + viewController.data = data || {}; } \ No newline at end of file diff --git a/packages/core/src/utils/helpers.ts b/packages/core/src/utils/helpers.ts index 6acbf46209..169320436f 100644 --- a/packages/core/src/utils/helpers.ts +++ b/packages/core/src/utils/helpers.ts @@ -1,3 +1,5 @@ +import { StencilElement } from '..'; + export function isDef(v: any): boolean { return v !== undefined && v !== null; } export function isUndef(v: any): boolean { return v === undefined || v === null; } @@ -155,10 +157,16 @@ export function swipeShouldReset(isResetDirection: boolean, isMovingFast: boolea // 1 | 1 | 0 || 1 // 1 | 1 | 1 || 1 // The resulting expression was generated by resolving the K-map (Karnaugh map): - let shouldClose = (!isMovingFast && isOnResetZone) || (isResetDirection && isMovingFast); - return shouldClose; + return (!isMovingFast && isOnResetZone) || (isResetDirection && isMovingFast); } +export function isReady(element: HTMLElement) { + return new Promise((resolve) => { + (element as StencilElement).componentOnReady((elm: HTMLElement) => { + resolve(elm); + }); + }); + /** @hidden */ export function deepCopy(obj: any) { return JSON.parse(JSON.stringify(obj)); diff --git a/packages/core/src/utils/ids.ts b/packages/core/src/utils/ids.ts deleted file mode 100644 index 67be116d62..0000000000 --- a/packages/core/src/utils/ids.ts +++ /dev/null @@ -1,3 +0,0 @@ - -export let NAV_ID_START = 1000; -export let VIEW_ID_START = 2000; diff --git a/packages/core/stencil.config.js b/packages/core/stencil.config.js index 94aef0d4d0..fe61401680 100644 --- a/packages/core/stencil.config.js +++ b/packages/core/stencil.config.js @@ -29,8 +29,8 @@ exports.config = { { components: ['ion-spinner'] }, { components: ['ion-tabs', 'ion-tab', 'ion-tab-bar', 'ion-tab-button', 'ion-tab-highlight'] }, { components: ['ion-toggle'] }, + { components: ['ion-nav', 'ion-nav-controller', 'stencil-ion-nav-delegate','page-one', 'page-two', 'page-three'] }, { components: ['ion-toast', 'ion-toast-controller'] }, - { components: ['ion-nav', 'page-one', 'page-two', 'page-three'] } ], preamble: '(C) Ionic http://ionicframework.com - MIT License', global: 'src/global/ionic-global.ts'