From 03780127ffae3c7dfdeefc866d996c0c72141759 Mon Sep 17 00:00:00 2001 From: "Manu Mtz.-Almeida" Date: Tue, 27 Feb 2018 20:18:28 +0100 Subject: [PATCH] refactor(navigation): using ionic 3 navigation --- packages/core/src/components.d.ts | 14 +- .../animation-interface.tsx | 8 +- .../animation-controller/animator.tsx | 9 +- packages/core/src/components/app/app.tsx | 329 +-- packages/core/src/components/app/readme.md | 66 - .../input-shims/hacks/scroll-padding.ts | 2 +- .../core/src/components/nav-pop/nav-pop.tsx | 5 +- .../core/src/components/nav-push/nav-push.tsx | 6 +- .../components/nav-set-root/nav-set-root.tsx | 3 +- .../ios.transition.ts} | 82 +- .../md.transition.ts} | 27 +- .../core/src/components/nav/nav-interfaces.ts | 122 - packages/core/src/components/nav/nav-util.ts | 194 ++ packages/core/src/components/nav/nav-utils.ts | 194 -- packages/core/src/components/nav/nav.tsx | 2218 +++++++---------- packages/core/src/components/nav/readme.md | 90 +- .../src/components/nav/test/basic/index.html | 5 - .../nav/test/nav-controller.spec.ts | 1139 +++++++++ .../nav/test/nav-then-tabs/index.html | 384 --- .../components/nav/test/set-root/index.html | 110 + .../components/nav/transition-controller.ts | 49 + .../core/src/components/nav/transition.ts | 57 + .../src/components/nav/view-controller.ts | 397 ++- packages/core/src/components/range/readme.md | 38 +- packages/core/src/components/route/readme.md | 20 +- packages/core/src/components/route/route.tsx | 6 +- .../src/components/router/router-utils.ts | 2 +- .../components/router/test/basic/index.html | 29 +- .../src/components/status-tap/status-tap.tsx | 67 + .../src/components/tab-button/tab-button.scss | 3 +- .../src/components/tab-button/tab-button.tsx | 4 +- packages/core/src/components/tab/readme.md | 10 - packages/core/src/components/tab/tab.scss | 9 - packages/core/src/components/tab/tab.tsx | 38 +- packages/core/src/components/tabs/tabs.tsx | 30 +- .../src/components/tabs/test/basic/index.html | 53 +- .../src/components/tap-click/tap-click.tsx | 26 +- packages/core/src/index.d.ts | 1 - 38 files changed, 3008 insertions(+), 2838 deletions(-) rename packages/core/src/components/nav/{transitions/transition.ios.ts => animations/ios.transition.ts} (69%) rename packages/core/src/components/nav/{transitions/transition.md.ts => animations/md.transition.ts} (72%) delete mode 100644 packages/core/src/components/nav/nav-interfaces.ts create mode 100644 packages/core/src/components/nav/nav-util.ts delete mode 100644 packages/core/src/components/nav/nav-utils.ts create mode 100644 packages/core/src/components/nav/test/nav-controller.spec.ts delete mode 100644 packages/core/src/components/nav/test/nav-then-tabs/index.html create mode 100644 packages/core/src/components/nav/test/set-root/index.html create mode 100644 packages/core/src/components/nav/transition-controller.ts create mode 100644 packages/core/src/components/nav/transition.ts diff --git a/packages/core/src/components.d.ts b/packages/core/src/components.d.ts index 1cad8ccb66..0123c6322b 100644 --- a/packages/core/src/components.d.ts +++ b/packages/core/src/components.d.ts @@ -25,7 +25,6 @@ import { FrameworkDelegate, PickerColumn, PickerOptions, - RouterDelegate, } from './index'; import { ActionSheetButton, @@ -49,9 +48,6 @@ import { import { SelectPopoverOption, } from './components/select-popover/select-popover'; -import { - FrameworkDelegate as FrameworkDelegate2, -} from '.'; import { DomRenderFn, HeaderFn, @@ -1980,7 +1976,7 @@ declare global { import { - Nav as IonNav + NavControllerBase as IonNav } from './components/nav/nav'; declare global { @@ -2003,13 +1999,8 @@ declare global { } namespace JSXElements { export interface IonNavAttributes extends HTMLAttributes { - delegate?: FrameworkDelegate; - lazy?: boolean; - mode?: string; root?: any; - routerDelegate?: RouterDelegate; swipeBackEnabled?: boolean; - useUrls?: boolean; } } } @@ -2606,9 +2597,9 @@ declare global { } namespace JSXElements { export interface IonRouteAttributes extends HTMLAttributes { + component?: string; path?: string; props?: any; - sel?: string; } } } @@ -3202,7 +3193,6 @@ declare global { badgeStyle?: string; btnId?: string; component?: any; - delegate?: FrameworkDelegate; disabled?: boolean; icon?: string; name?: string; diff --git a/packages/core/src/components/animation-controller/animation-interface.tsx b/packages/core/src/components/animation-controller/animation-interface.tsx index 6f385546a3..b21c9230c3 100644 --- a/packages/core/src/components/animation-controller/animation-interface.tsx +++ b/packages/core/src/components/animation-controller/animation-interface.tsx @@ -1,3 +1,5 @@ +import { ViewController } from '../..'; + export interface AnimationController { create(animationBuilder?: AnimationBuilder, baseEl?: any, opts?: any): Promise; } @@ -43,7 +45,7 @@ export interface Animation { export interface AnimationBuilder { -(Animation: Animation, baseEl?: HTMLElement, opts?: any): Promise; + (Animation: Animation, baseEl?: HTMLElement, opts?: any): Promise; } @@ -54,9 +56,11 @@ export interface AnimationOptions { direction?: string; isRTL?: boolean; ev?: any; + enteringView: ViewController; + leavingView: ViewController; + nav: HTMLIonNavElement; } - export interface PlayOptions { duration?: number; promise?: boolean; diff --git a/packages/core/src/components/animation-controller/animator.tsx b/packages/core/src/components/animation-controller/animator.tsx index 16392bd429..b0bd579ac2 100644 --- a/packages/core/src/components/animation-controller/animator.tsx +++ b/packages/core/src/components/animation-controller/animator.tsx @@ -3,6 +3,7 @@ import { CSS_PROP, CSS_VALUE_REGEX, DURATION_MIN, TRANSFORM_PROPS, TRANSITION_EN import { transitionEnd } from './transition-end'; +const raf = window.requestAnimationFrame || ((f: Function) => f()); export class Animator { @@ -348,8 +349,8 @@ export class Animator { // from an input event, and just having one RAF would have this code // run within the same frame as the triggering input event, and the // input event probably already did way too much work for one frame - window.requestAnimationFrame(function() { - window.requestAnimationFrame(function() { + raf(() => { + raf(() => { self._playDomInspect(opts); }); }); @@ -425,7 +426,7 @@ export class Animator { if (self._isAsync && !this._destroyed) { // this animation has a duration so we need another RAF // for the CSS TRANSITION properties to kick in - window.requestAnimationFrame(function() { + raf(() => { self._playToStep(1); }); } @@ -1058,7 +1059,7 @@ export class Animator { // this animation has a duration so we need another RAF // for the CSS TRANSITION properties to kick in if (!this._destroyed) { - window.requestAnimationFrame(() => { + raf(() => { this._playToStep(stepValue); }); } diff --git a/packages/core/src/components/app/app.tsx b/packages/core/src/components/app/app.tsx index 6b4a8870d1..416c121645 100644 --- a/packages/core/src/components/app/app.tsx +++ b/packages/core/src/components/app/app.tsx @@ -1,10 +1,5 @@ -import { Component, Element, Event, EventEmitter, Listen, Method, Prop } from '@stencil/core'; -import { Config, NavEvent, OverlayController, PublicNav, PublicViewController } from '../../index'; - -import { getOrAppendElement } from '../../utils/helpers'; - -const rootNavs = new Map(); -let backButtonActions: BackButtonAction[] = []; +import { Component, Element, Prop } from '@stencil/core'; +import { Config } from '../../index'; @Component({ tag: 'ion-app', @@ -21,11 +16,7 @@ export class App { private isDevice = false; private deviceHacks = false; - externalNavPromise: void | Promise = null; - externalNavOccuring = false; - - @Element() element: HTMLElement; - @Event() exitApp: EventEmitter; + @Element() el: HTMLElement; @Prop({ context: 'config' }) config: Config; @@ -34,174 +25,6 @@ export class App { this.deviceHacks = this.config.getBoolean('deviceHacks', false); } - /** - * Returns the promise set by an external navigation system - * This API is not meant for public usage and could - * change at any time - */ - @Method() - getExternalNavPromise(): void | Promise { - return this.externalNavPromise; - } - - /** - * Updates the Promise set by an external navigation system - * This API is not meant for public usage and could - * change at any time - */ - @Method() - setExternalNavPromise(value: null | Promise): void { - this.externalNavPromise = value; - } - - /** - * Returns whether an external navigation event is occuring - * This API is not meant for public usage and could - * change at any time - */ - @Method() - getExternalNavOccuring(): boolean { - return this.externalNavOccuring; - } - - /** - * Updates whether an external navigation event is occuring - * This API is not meant for public usage and could - * change at any time - */ - @Method() - updateExternalNavOccuring(status: boolean) { - this.externalNavOccuring = status; - } - - @Listen('body:navInit') - protected registerRootNav(event: NavEvent) { - rootNavs.set(event.target.getId(), event.target); - } - - /** - * Returns an array of top level Navs - */ - @Method() - getRootNavs(): PublicNav[] { - const navs: PublicNav[] = []; - rootNavs.forEach((rootNav: PublicNav) => { - navs.push(rootNav); - }); - return navs; - } - - /** - * Returns whether the application is enabled or not - */ - @Method() - isEnabled(): boolean { - return true; - } - - @Method() - getTopNavs(rootNavId = -1): PublicNav[] { - return getTopNavsImpl(rootNavId); - } - - @Method() - getNavByIdOrName(nameOrId: number | string): PublicNav|null { - const navs = Array.from(rootNavs.values()); - for (const navContainer of navs) { - const match = getNavByIdOrNameImpl(navContainer, nameOrId); - if (match) { - return match; - } - } - return null; - } - - /** - * The back button event is triggered when the user presses the native - * platform's back button, also referred to as the "hardware" back button. - * This event is only used within Cordova apps running on Android and - * Windows platforms. This event is not fired on iOS since iOS doesn't come - * with a hardware back button in the same sense an Android or Windows device - * does. - * - * Registering a hardware back button action and setting a priority allows - * apps to control which action should be called when the hardware back - * button is pressed. This method decides which of the registered back button - * actions has the highest priority and should be called. - * - * @param {Function} fn Called when the back button is pressed, - * if this registered action has the highest priority. - * @param {number} priority Set the priority for this action. Only the highest priority will execute. Defaults to `0`. - * @returns {Function} A function that, when called, will unregister - * the back button action. - */ - @Method() - registerBackButtonAction(fn: Function, priority = 0): () => void { - const newAction = { - fn, - priority - }; - backButtonActions.push(newAction); - - return () => { - backButtonActions = backButtonActions.filter(bbAction => bbAction !== newAction); - }; - } - - @Listen('document:backbutton') - hardwareBackButtonPressed() { - // okay cool, we need to execute the user's custom method if they have one - const actionToExecute = backButtonActions.reduce((previous, current) => { - if (current.priority >= previous.priority) { - return current; - } - return previous; - }); - actionToExecute && actionToExecute.fn && actionToExecute.fn(); - - // okay great, we've done the user action, now do the default actions - // check if menu exists and is open - return checkIfMenuIsOpen().then((done: boolean) => { - if (!done) { - // we need to check if there is an action-sheet, alert, loading, picker, popover or toast open - // if so, just return and don't do anything - // Why? I have no idea, but that is the existing behavior in Ionic 3 - return checkIfNotModalOverlayIsOpen(); - } - return done; - }).then((done: boolean) => { - if (!done) { - // if there's a modal open, close that instead - return closeModalIfOpen(); - } - return done; - }).then((done: boolean) => { - // okay cool, it's time to pop a nav if possible - if (!done) { - return popEligibleView(); - } - return done; - }).then((done: boolean) => { - if (!done) { - // okay, we didn't find a nav that we can pop, so we should just exit the app - // since each platform exits differently, just delegate it to the platform to - // figure out how to exit - return this.exitApp.emit(); - } - return Promise.resolve(); - }); - } - - @Listen('document:paused') - appResume(): null { - return null; - } - - @Listen('document:resume') - appPaused(): null { - return null; - } - hostData() { const mode = this.config.get('mode'); const hoverCSS = this.config.getBoolean('hoverCSS', false); @@ -224,149 +47,3 @@ export class App { ]; } } - -export function getTopNavsImpl(rootNavId = -1) { - if (!rootNavs.size) { - return []; - } - - if (rootNavId !== -1) { - return findTopNavs(rootNavs.get(rootNavId)); - } - - if (rootNavs.size === 1) { - return findTopNavs(rootNavs.values().next().value); - } - - // fallback to just using all root navs - let activeNavs: PublicNav[] = []; - rootNavs.forEach(nav => { - activeNavs = activeNavs.concat(findTopNavs(nav)); - }); - return activeNavs; -} - -export function findTopNavs(nav: PublicNav): PublicNav[] { - let containers: PublicNav[] = []; - const childNavs = nav.getChildNavs(); - if (!childNavs || !childNavs.length) { - containers.push(nav); - } else { - childNavs.forEach(childNav => { - const topNavs = findTopNavs(childNav); - containers = containers.concat(topNavs); - }); - } - return containers; -} - -export function getNavByIdOrNameImpl(nav: PublicNav, id: number | string): PublicNav { - if (nav.navId === id || nav.name === id) { - return nav; - } - for (const child of nav.getChildNavs()) { - const tmp = getNavByIdOrNameImpl(child, id); - if (tmp) { - return tmp; - } - } - return null; -} - -export function getHydratedController(tagName: string): Promise { - const controller = getOrAppendElement(tagName); - return (controller as any).componentOnReady(); -} - -export function checkIfMenuIsOpen(): Promise { - return getHydratedController('ion-menu-controller').then((menuController: HTMLIonMenuControllerElement) => { - if (menuController.isOpen()) { - return menuController.close().then(() => { - return true; - }); - } - return false; - }); -} - -export function checkIfNotModalOverlayIsOpen(): Promise { - const promises: Promise[] = []; - promises.push(checkIfOverlayExists('ion-action-sheet-controller')); - promises.push(checkIfOverlayExists('ion-alert-controller')); - promises.push(checkIfOverlayExists('ion-loading-controller')); - promises.push(checkIfOverlayExists('ion-picker-controller')); - promises.push(checkIfOverlayExists('ion-popover-controller')); - promises.push(checkIfOverlayExists('ion-toast-controller')); - return Promise.all(promises).then((results: boolean[]) => { - return results.every((value: boolean) => !!value); - }); -} - -export function checkIfOverlayExists(tagName: string): Promise { - const overlayControllerElement = document.querySelector(tagName) as any as OverlayController; - if (!overlayControllerElement) { - return Promise.resolve(false); - } - return (overlayControllerElement as any).componentOnReady().then(() => { - return !!(overlayControllerElement.getTop()); - }); -} - -export function closeModalIfOpen(): Promise { - return getHydratedController('ion-modal-controller').then((modalController: HTMLIonModalControllerElement) => { - if (modalController.getTop()) { - return modalController.dismiss().then(() => { - return true; - }); - } - return false; - }); -} - -export function popEligibleView(): Promise { - let navToPop: PublicNav = null; - let mostRecentVC: PublicViewController = null; - rootNavs.forEach(nav => { - const topNavs = getTopNavsImpl(nav.navId); - const poppableNavs = topNavs.map(topNav => getPoppableNav(topNav)).filter(nav => !!nav).filter(nav => !!nav.last()); - poppableNavs.forEach(poppable => { - const topViewController = poppable.last(); - if (!mostRecentVC || topViewController.timestamp >= mostRecentVC.timestamp) { - mostRecentVC = topViewController; - navToPop = poppable; - } - }); - }); - if (navToPop) { - return navToPop.pop().then(() => { - return true; - }); - } - return Promise.resolve(false); -} - -export function getPoppableNav(nav: PublicNav): PublicNav { - if (!nav) { - return null; - } - - // to be a poppable nav, a nav must a top view, plus a view that we can pop back to - if (nav.getViews.length > 1) { - return nav; - } - - return getPoppableNav(nav.parent); -} - -export interface ExitAppEvent extends CustomEvent { - target: HTMLIonAppElement; - detail: ExitAppEventDetail; -} - -export interface ExitAppEventDetail { -} - -export interface BackButtonAction { - fn: Function; - priority: number; -} diff --git a/packages/core/src/components/app/readme.md b/packages/core/src/components/app/readme.md index 7faa1e312a..a5f662f426 100644 --- a/packages/core/src/components/app/readme.md +++ b/packages/core/src/components/app/readme.md @@ -5,72 +5,6 @@ -## Events - -#### exitApp - - -## Methods - -#### getExternalNavOccuring() - -Returns whether an external navigation event is occuring -This API is not meant for public usage and could -change at any time - - -#### getExternalNavPromise() - -Returns the promise set by an external navigation system -This API is not meant for public usage and could -change at any time - - -#### getNavByIdOrName() - - -#### getRootNavs() - -Returns an array of top level Navs - - -#### getTopNavs() - - -#### isEnabled() - -Returns whether the application is enabled or not - - -#### registerBackButtonAction() - -The back button event is triggered when the user presses the native -platform's back button, also referred to as the "hardware" back button. -This event is only used within Cordova apps running on Android and -Windows platforms. This event is not fired on iOS since iOS doesn't come -with a hardware back button in the same sense an Android or Windows device -does. - -Registering a hardware back button action and setting a priority allows -apps to control which action should be called when the hardware back -button is pressed. This method decides which of the registered back button -actions has the highest priority and should be called. - - -#### setExternalNavPromise() - -Updates the Promise set by an external navigation system -This API is not meant for public usage and could -change at any time - - -#### updateExternalNavOccuring() - -Updates whether an external navigation event is occuring -This API is not meant for public usage and could -change at any time - - ---------------------------------------------- diff --git a/packages/core/src/components/input-shims/hacks/scroll-padding.ts b/packages/core/src/components/input-shims/hacks/scroll-padding.ts index 0dc5e5a7fe..51fb97c40f 100644 --- a/packages/core/src/components/input-shims/hacks/scroll-padding.ts +++ b/packages/core/src/components/input-shims/hacks/scroll-padding.ts @@ -1,7 +1,7 @@ const PADDING_TIMER_KEY = '$ionPaddingTimer'; export default function enableScrollPadding(keyboardHeight: number) { - console.debug('Input: enableInputBlurring'); + console.debug('Input: enableScrollPadding'); function onFocusin(ev: any) { setScrollPadding(ev.target, keyboardHeight); diff --git a/packages/core/src/components/nav-pop/nav-pop.tsx b/packages/core/src/components/nav-pop/nav-pop.tsx index 8ae01f2ac7..05d31ec9e1 100644 --- a/packages/core/src/components/nav-pop/nav-pop.tsx +++ b/packages/core/src/components/nav-pop/nav-pop.tsx @@ -1,6 +1,5 @@ import { Component, Element, Listen } from '@stencil/core'; -import { NavResult } from '../..'; @Component({ tag: 'ion-nav-pop', }) @@ -9,8 +8,8 @@ export class NavPop { @Element() element: HTMLElement; @Listen('child:click') - pop(): Promise { - const nav = this.element.closest('ion-nav'); + pop(): Promise { + const nav = this.element.closest('ion-nav') as HTMLIonNavElement; if (nav) { return nav.pop(); } diff --git a/packages/core/src/components/nav-push/nav-push.tsx b/packages/core/src/components/nav-push/nav-push.tsx index 3dcd2f71f0..80cb80b724 100644 --- a/packages/core/src/components/nav-push/nav-push.tsx +++ b/packages/core/src/components/nav-push/nav-push.tsx @@ -1,5 +1,5 @@ import { Component, Element, Listen, Prop } from '@stencil/core'; -import { NavResult } from '../../index'; +// import { NavResult } from '../../index'; @Component({ tag: 'ion-nav-push', @@ -12,8 +12,8 @@ export class NavPush { @Prop() data: any; @Listen('child:click') - push(): Promise { - const nav = this.element.closest('ion-nav'); + push(): Promise { + const nav = this.element.closest('ion-nav') as HTMLIonNavElement; if (nav) { const toPush = this.url || this.component; return nav.push(toPush, this.data); diff --git a/packages/core/src/components/nav-set-root/nav-set-root.tsx b/packages/core/src/components/nav-set-root/nav-set-root.tsx index fe4df7dcc6..a6a708d750 100644 --- a/packages/core/src/components/nav-set-root/nav-set-root.tsx +++ b/packages/core/src/components/nav-set-root/nav-set-root.tsx @@ -1,5 +1,4 @@ import { Component, Element, Listen, Prop } from '@stencil/core'; -import { NavResult } from '../../index'; @Component({ tag: 'ion-nav-set-root', @@ -12,7 +11,7 @@ export class NavSetRoot { @Prop() data: any; @Listen('child:click') - push(): Promise { + push(): Promise { const nav = this.element.closest('ion-nav'); if (nav) { const toPush = this.url || this.component; diff --git a/packages/core/src/components/nav/transitions/transition.ios.ts b/packages/core/src/components/nav/animations/ios.transition.ts similarity index 69% rename from packages/core/src/components/nav/transitions/transition.ios.ts rename to packages/core/src/components/nav/animations/ios.transition.ts index b9d1678051..8f6c77b9ae 100644 --- a/packages/core/src/components/nav/transitions/transition.ios.ts +++ b/packages/core/src/components/nav/animations/ios.transition.ts @@ -1,5 +1,4 @@ -import { AnimationOptions, Transition, ViewController } from '../../../index'; -import { canNavGoBack } from '../nav-utils'; +import { Animation, AnimationOptions } from '../../../index'; import { isDef } from '../../../utils/helpers'; const DURATION = 500; @@ -11,41 +10,38 @@ 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): Promise { - // Cool we're all hydrated, and can do deep selector - rootTransition.enteringView = enteringView; - rootTransition.leavingView = leavingView; +export default function iosTransitionAnimation(Animation: Animation, _: HTMLElement, opts: AnimationOptions): Promise { - const isRTL = document.dir === 'rtl'; + const isRTL = opts.isRTL; const OFF_RIGHT = isRTL ? '-99.5%' : '99.5%'; const OFF_LEFT = isRTL ? '31%' : '-31%'; + const enteringEl = opts.enteringView ? opts.enteringView.element : undefined; + const leavingEl = opts.leavingView ? opts.leavingView.element : undefined; + const nav = opts.nav; + + const rootTransition = new Animation(); rootTransition.duration(isDef(opts.duration) ? opts.duration : DURATION); rootTransition.easing(isDef(opts.easing) ? opts.easing : EASING); - - - rootTransition.addElement(enteringView.element); + rootTransition.addElement(enteringEl); rootTransition.beforeRemoveClass('hide-page'); - if (leavingView) { - const navEl = leavingView.element.closest('ion-nav'); - if (navEl) { - const navDecor = rootTransition.create(); - navDecor.addElement(navEl).duringAddClass('show-decor'); - rootTransition.add(navDecor); - } + if (leavingEl && nav) { + const navDecor = new Animation(); + navDecor.addElement(nav.el).duringAddClass('show-decor'); + rootTransition.add(navDecor); } const backDirection = (opts.direction === 'back'); // setting up enter view - if (enteringView) { - const contentEl = enteringView.element.querySelector('ion-content'); - const headerEls = enteringView.element.querySelectorAll('ion-header > *:not(ion-toolbar),ion-footer > *'); - const enteringToolBarEle = enteringView.element.querySelector('ion-toolbar'); - const enteringContent = rootTransition.create(); + if (enteringEl) { + const contentEl = enteringEl.querySelector('ion-content'); + const headerEls = enteringEl.querySelectorAll('ion-header > *:not(ion-toolbar),ion-footer > *'); + const enteringToolBarEle = enteringEl.querySelector('ion-toolbar'); + const enteringContent = new Animation(); if (!contentEl && !enteringToolBarEle && headerEls.length === 0) { - enteringContent.addElement(enteringView.element.querySelector('ion-page,ion-nav,ion-tabs')); + enteringContent.addElement(enteringEl.querySelector('ion-page,ion-nav,ion-tabs')); } else { enteringContent.addElement(contentEl); enteringContent.addElement(headerEls); @@ -63,24 +59,23 @@ export function buildIOSTransition(rootTransition: Transition, enteringView: Vie enteringContent .beforeClearStyles([OPACITY]) .fromTo(TRANSLATEX, OFF_RIGHT, CENTER, true); - } if (enteringToolBarEle) { - const enteringToolBar = rootTransition.create(); + const enteringToolBar = new Animation(); enteringToolBar.addElement(enteringToolBarEle); rootTransition.add(enteringToolBar); - const enteringTitle = rootTransition.create(); + const enteringTitle = new Animation(); enteringTitle.addElement(enteringToolBarEle.querySelector('ion-title')); - const enteringToolBarItems = rootTransition.create(); + const enteringToolBarItems = new Animation(); enteringToolBarItems.addElement(enteringToolBarEle.querySelectorAll('ion-buttons,[menuToggle]')); - const enteringToolBarBg = rootTransition.create(); + const enteringToolBarBg = new Animation(); enteringToolBarBg.addElement(enteringToolBarEle.querySelector('.toolbar-background')); - const enteringBackButton = rootTransition.create(); + const enteringBackButton = new Animation(); enteringBackButton.addElement(enteringToolBarEle.querySelector('.back-button')); enteringToolBar @@ -95,7 +90,7 @@ export function buildIOSTransition(rootTransition: Transition, enteringView: Vie if (backDirection) { enteringTitle.fromTo(TRANSLATEX, OFF_LEFT, CENTER, true); - if (canNavGoBack(enteringView.nav, enteringView)) { + if (opts.enteringView.enableBack()) { // back direction, entering page has a back button enteringBackButton.beforeAddClass(SHOW_BACK_BTN_CSS).fromTo(OPACITY, 0.01, 1, true); } @@ -107,15 +102,14 @@ export function buildIOSTransition(rootTransition: Transition, enteringView: Vie .beforeClearStyles([OPACITY]) .fromTo(OPACITY, 0.01, 1, true); - if (canNavGoBack(enteringView.nav, enteringView)) { - + if (opts.enteringView.enableBack()) { // forward direction, entering page has a back button enteringBackButton .beforeAddClass(SHOW_BACK_BTN_CSS) .fromTo(OPACITY, 0.01, 1, true); - const enteringBackBtnText = rootTransition.create(); + const enteringBackBtnText = new Animation(); enteringBackBtnText.addElement(enteringToolBarEle.querySelector('.back-button .button-text')); enteringBackBtnText.fromTo(TRANSLATEX, (isRTL ? '-100px' : '100px'), '0px'); @@ -128,11 +122,11 @@ export function buildIOSTransition(rootTransition: Transition, enteringView: Vie } // setup leaving view - if (leavingView) { + if (leavingEl) { - const leavingContent = rootTransition.create(); - leavingContent.addElement(leavingView.element.querySelector('ion-content')); - leavingContent.addElement(leavingView.element.querySelectorAll('ion-header > *:not(ion-toolbar),ion-footer > *')); + const leavingContent = new Animation(); + leavingContent.addElement(leavingEl.querySelector('ion-content')); + leavingContent.addElement(leavingEl.querySelectorAll('ion-header > *:not(ion-toolbar),ion-footer > *')); rootTransition.add(leavingContent); if (backDirection) { @@ -149,21 +143,21 @@ export function buildIOSTransition(rootTransition: Transition, enteringView: Vie .fromTo(OPACITY, 1, OFF_OPACITY, true); } - const leavingToolBarEle = leavingView.element.querySelector('ion-toolbar'); + const leavingToolBarEle = leavingEl.querySelector('ion-toolbar'); if (leavingToolBarEle) { - const leavingToolBar = rootTransition.create(); + const leavingToolBar = new Animation(); leavingToolBar.addElement(leavingToolBarEle); - const leavingTitle = rootTransition.create(); + const leavingTitle = new Animation(); leavingTitle.addElement(leavingToolBarEle.querySelector('ion-title')); - const leavingToolBarItems = rootTransition.create(); + const leavingToolBarItems = new Animation(); leavingToolBarItems.addElement(leavingToolBarEle.querySelectorAll('ion-buttons,[menuToggle]')); - const leavingToolBarBg = rootTransition.create(); + const leavingToolBarBg = new Animation(); leavingToolBarBg.addElement(leavingToolBarEle.querySelector('.toolbar-background')); - const leavingBackButton = rootTransition.create(); + const leavingBackButton = new Animation(); leavingBackButton.addElement(leavingToolBarEle.querySelector('.back-button')); leavingToolBar @@ -189,7 +183,7 @@ export function buildIOSTransition(rootTransition: Transition, enteringView: Vie .beforeClearStyles([OPACITY]) .fromTo(OPACITY, 1, 0.01, true); - const leavingBackBtnText = rootTransition.create(); + const leavingBackBtnText = new Animation(); leavingBackBtnText.addElement(leavingToolBarEle.querySelector('.back-button .button-text')); leavingBackBtnText.fromTo(TRANSLATEX, CENTER, (isRTL ? -115 : 115) + 'px'); leavingToolBar.add(leavingBackBtnText); diff --git a/packages/core/src/components/nav/transitions/transition.md.ts b/packages/core/src/components/nav/animations/md.transition.ts similarity index 72% rename from packages/core/src/components/nav/transitions/transition.md.ts rename to packages/core/src/components/nav/animations/md.transition.ts index 1d2617d487..b4cec13e06 100644 --- a/packages/core/src/components/nav/transitions/transition.md.ts +++ b/packages/core/src/components/nav/animations/md.transition.ts @@ -1,5 +1,4 @@ -import { AnimationOptions, Transition, ViewController } from '../../../index'; -import { canNavGoBack } from '../nav-utils'; +import { Animation, AnimationOptions } from '../../../index'; import { isDef } from '../../../utils/helpers'; const TRANSLATEY = 'translateY'; @@ -7,18 +6,18 @@ 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): Promise { +export default function mdTransitionAnimation(Animation: Animation, _: HTMLElement, opts: AnimationOptions): Promise { - rootTransition.enteringView = enteringView; - rootTransition.leavingView = leavingView; - - const ionPageElement = getIonPageElement(enteringView.element); + const enteringEl = opts.enteringView ? opts.enteringView.element : undefined; + const leavingEl = opts.leavingView ? opts.leavingView.element : undefined; + const ionPageElement = getIonPageElement(enteringEl); + const rootTransition = new Animation(); rootTransition.addElement(ionPageElement); rootTransition.beforeRemoveClass('hide-page'); const backDirection = (opts.direction === 'back'); - if (enteringView) { + if (enteringEl) { // animate the component itself if (backDirection) { @@ -35,14 +34,14 @@ export function buildMdTransition(rootTransition: Transition, enteringView: View const enteringToolbarEle = ionPageElement.querySelector('ion-toolbar'); if (enteringToolbarEle) { - const enteringToolBar = rootTransition.create(); + const enteringToolBar = new Animation(); enteringToolBar.addElement(enteringToolbarEle); rootTransition.add(enteringToolBar); - const enteringBackButton = rootTransition.create(); + const enteringBackButton = new Animation(); enteringBackButton.addElement(enteringToolbarEle.querySelector('.back-button')); rootTransition.add(enteringBackButton); - if (canNavGoBack(enteringView.nav, enteringView)) { + if (opts.enteringView.enableBack()) { enteringBackButton.beforeAddClass(SHOW_BACK_BTN_CSS); } else { enteringBackButton.beforeRemoveClass(SHOW_BACK_BTN_CSS); @@ -51,11 +50,11 @@ export function buildMdTransition(rootTransition: Transition, enteringView: View } // setup leaving view - if (leavingView && backDirection) { + if (leavingEl && backDirection) { // leaving content rootTransition.duration(opts.duration || 200).easing('cubic-bezier(0.47,0,0.745,0.715)'); - const leavingPage = rootTransition.create(); - leavingPage.addElement(getIonPageElement(leavingView.element)); + const leavingPage = new Animation(); + leavingPage.addElement(getIonPageElement(leavingEl)); rootTransition.add(leavingPage.fromTo(TRANSLATEY, CENTER, OFF_BOTTOM).fromTo('opacity', 1, 0)); } diff --git a/packages/core/src/components/nav/nav-interfaces.ts b/packages/core/src/components/nav/nav-interfaces.ts deleted file mode 100644 index d43f0a9d74..0000000000 --- a/packages/core/src/components/nav/nav-interfaces.ts +++ /dev/null @@ -1,122 +0,0 @@ - -/* it is very important to keep this interface in sync with ./nav */ -import { - Animation, - AnimationOptions, - FrameworkDelegate, - FrameworkMountingData, - Nav, - NavOptions, - PublicViewController, - ViewController, -} from '../../index'; - -export interface PublicNav { - push(component: any, data?: any, opts?: NavOptions): Promise; - pop(opts?: NavOptions): Promise; - setRoot(component: any, data?: any, opts?: NavOptions): Promise; - insert(insertIndex: number, page: any, params?: any, opts?: NavOptions): Promise; - insertPages(insertIndex: number, insertPages: any[], opts?: NavOptions): Promise; - popToRoot(opts?: NavOptions): Promise; - popTo(indexOrViewCtrl: any, opts?: NavOptions): Promise; - removeIndex(startIndex: number, removeCount?: number, opts?: NavOptions): Promise; - removeView(viewController: PublicViewController, opts?: NavOptions): Promise; - setPages(componentDataPairs: any[], opts?: NavOptions): Promise; - - getActive(): PublicViewController; - getPrevious(view?: PublicViewController): PublicViewController; - canGoBack(): boolean; - canSwipeBack(): boolean; - first(): PublicViewController; - last(): PublicViewController; - getChildNavs(): PublicNav[]; - getViews(): PublicViewController[]; - - navId?: number; - name?: string; - element?: HTMLElement; - parent?: PublicNav; - initialized?: boolean; -} - -export interface NavOptions { - animate?: boolean; - animation?: string; - direction?: string; - duration?: number; - easing?: string; - id?: string; - keyboardClose?: boolean; - progressAnimation?: boolean; - disableApp?: boolean; - event?: any; - updateUrl?: boolean; - isNavRoot?: boolean; -} - -export interface TransitionInstruction { - component: any; - opts: NavOptions; - insertStart?: number; - insertViews?: ComponentDataPair[]; - viewControllers?: ViewController[]; - removeView?: any; // TODO make VC - removeStart?: number; - removeCount?: number; - resolve?: (hasCompleted: NavResult) => void; - reject?: (rejectReason: Error) => void; - leavingRequiresTransition?: boolean; - enteringRequiresTransition?: boolean; - requiresTransition?: boolean; - id?: number; - nav?: Nav; - delegate?: FrameworkDelegate; - animation?: Animation; - escapeHatch?: EscapeHatch; - method?: string; - mountingData?: FrameworkMountingData; -} - -export interface NavResult { - successful: boolean; - mountingData: FrameworkMountingData; -} - -export interface ComponentDataPair { - component: any; - data: any; -} - -export interface ExternalNavData { - url: string; - method: string; - resolve: Function; - reject: Function; -} - -export interface EscapeHatch { - fromExternalRouter?: boolean; - url?: string; -} - -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 ): Promise; -} - -export interface PublicViewController { - id?: string; - component?: any; - instance?: any; - element?: HTMLElement; - timestamp?: number; -} diff --git a/packages/core/src/components/nav/nav-util.ts b/packages/core/src/components/nav/nav-util.ts new file mode 100644 index 0000000000..ac41387006 --- /dev/null +++ b/packages/core/src/components/nav/nav-util.ts @@ -0,0 +1,194 @@ +import { ViewController, isViewController } from './view-controller'; +import { NavControllerBase } from './nav'; +import { Transition } from './transition'; + + +export type Page2 = string | HTMLElement | ViewController; + +export interface PageMeta { + page: Page2; + params?: any; +} + +export function convertToView(page: any, params: any): ViewController { + if (!page) { + return null; + } + if (isViewController(page)) { + return page; + } + return new ViewController(page, params); +} + +export function convertToViews(pages: any[]): ViewController[] { + return pages + .map(page => { + if (isViewController(page)) { + return page; + } + if ('page' in page) { + return convertToView(page.page, page.params); + } + return convertToView(page, undefined); + }) + .filter(v => v !== null); +} + + +export function setZIndex(nav: NavControllerBase, enteringView: ViewController, leavingView: ViewController, direction: string) { + if (enteringView) { + + leavingView = leavingView || nav.getPrevious(enteringView); + + if (leavingView && isPresent(leavingView._zIndex)) { + if (direction === DIRECTION_BACK) { + enteringView._setZIndex(leavingView._zIndex - 1); + + } else { + enteringView._setZIndex(leavingView._zIndex + 1); + } + + } else { + enteringView._setZIndex(INIT_ZINDEX); + } + } +} + +export function isTabs(nav: any): boolean { + // Tabs (ion-tabs) + return !!nav && !!nav.getSelected; +} + +export function isTab(nav: any): boolean { + // Tab (ion-tab) + return !!nav && isPresent(nav._tabId); +} + +export function isNav(nav: any): boolean { + // Nav (ion-nav), Tab (ion-tab), Portal (ion-portal) + return !!nav && !!nav.push && nav.getType() === 'nav'; +} + +export function linkToSegment(navId: string, type: string, secondaryId: string, link: NavLink): NavSegment { + const segment = Object.assign({}, link); + segment.navId = navId; + segment.type = type; + segment.secondaryId = secondaryId; + return segment; +} + + +// internal link interface, not exposed publicly +export interface NavLink { + component?: any; + loadChildren?: string; + name?: string; + segment?: string; + segmentParts?: string[]; + segmentPartsLen?: number; + staticLen?: number; + dataLen?: number; + dataKeys?: {[key: string]: boolean}; + defaultHistory?: any[]; +} + +export interface NavResult { + hasCompleted: boolean; + requiresTransition: boolean; + enteringName?: string; + leavingName?: string; + direction?: string; +} + +export interface NavSegment extends DehydratedSegment { + type: string; + navId: string; + secondaryId: string; + requiresExplicitNavPrefix?: boolean; +} + +export interface DehydratedSegment { + id: string; + name: string; + component?: any; + loadChildren?: string; + data: any; + defaultHistory?: NavSegment[]; + secondaryId?: string; +} + +export interface DehydratedSegmentPair { + segments: DehydratedSegment[]; + navGroup: NavGroup; +} + +export interface NavGroup { + type: string; + navId: string; + secondaryId: string; + segmentPieces?: string[]; + tabSegmentPieces?: string[]; +} + +export interface NavOptions { + animate?: boolean; + animation?: string; + direction?: string; + duration?: number; + easing?: string; + id?: string; + keyboardClose?: boolean; + progressAnimation?: boolean; + disableApp?: boolean; + minClickBlockDuration?: number; + ev?: any; + updateUrl?: boolean; + isNavRoot?: boolean; +} + +export function isPresent(val: any): val is any { return val !== undefined && val !== null; } + +export interface Page extends Function { + new (...args: any[]): any; +} + +export interface TransitionResolveFn { + (hasCompleted: boolean, requiresTransition: boolean, enteringName?: string, leavingName?: string, direction?: string): void; +} + +export interface TransitionRejectFn { + (rejectReason: any, transition?: Transition): void; +} + +export interface TransitionDoneFn { + (hasCompleted: boolean, requiresTransition: boolean, enteringName?: string, leavingName?: string, direction?: string): void; +} + +export interface TransitionInstruction { + opts: NavOptions; + insertStart?: number; + insertViews?: any[]; + removeView?: ViewController; + removeStart?: number; + removeCount?: number; + resolve?: (hasCompleted: boolean) => void; + reject?: (rejectReason: string) => void; + done?: TransitionDoneFn; + leavingRequiresTransition?: boolean; + enteringRequiresTransition?: boolean; + requiresTransition?: boolean; +} + +export const STATE_NEW = 1; +export const STATE_INITIALIZED = 2; +export const STATE_ATTACHED = 3; +export const STATE_DESTROYED = 4; + +export const INIT_ZINDEX = 100; + +export const DIRECTION_BACK = 'back'; +export const DIRECTION_FORWARD = 'forward'; +export const DIRECTION_SWITCH = 'switch'; + +export const NAV = 'nav'; +export const TABS = 'tabs'; diff --git a/packages/core/src/components/nav/nav-utils.ts b/packages/core/src/components/nav/nav-utils.ts deleted file mode 100644 index 851257efda..0000000000 --- a/packages/core/src/components/nav/nav-utils.ts +++ /dev/null @@ -1,194 +0,0 @@ -import { Transition } from './nav-interfaces'; -import { Animation, AnimationOptions, Config, Nav, TransitionBuilder, ViewController } from '../../index'; -import { isDef } from '../../utils/helpers'; - -export enum State { - New, - INITIALIZED, - ATTACHED, - DESTROYED, -} -export const STATE_NEW = 1; -export const STATE_INITIALIZED = 2; -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'; -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; -const activeTransitions = new Map(); - -export function isViewController(object: any): boolean { - return !!(object && object.didLoad && object.willUnload); -} - -export function setZIndex(nav: Nav, enteringView: ViewController, leavingView: ViewController, direction: string) { - if (enteringView) { - - leavingView = leavingView || nav.getPrevious(enteringView) as ViewController; - - if (leavingView && isDef(leavingView.zIndex)) { - if (direction === DIRECTION_BACK) { - updateZIndex(enteringView, leavingView.zIndex - 1); - - } else { - updateZIndex(enteringView, leavingView.zIndex + 1); - } - - } else { - // TODO - fix typing - updateZIndex(enteringView, INIT_ZINDEX + (nav as any).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, shouldBeHidden: boolean) { - element.hidden = shouldBeHidden; -} - -export function canNavGoBack(nav: Nav, view?: ViewController) { - if (!nav) { - return false; - } - return !!nav.getPrevious(view); -} - -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): Promise { - // Let makes sure everything is hydrated and ready to animate - const componentReadyPromise: Promise[] = []; - if (enteringView && (enteringView.element as any).componentOnReady) { - componentReadyPromise.push((enteringView.element as any).componentOnReady()); - } - if (leavingView && (leavingView.element as any).componentOnReady) { - componentReadyPromise.push((leavingView.element as any).componentOnReady()); - } - const transitionFactory = config.get(name) as TransitionBuilder || defaultTransitionFactory; - return Promise.all(componentReadyPromise) - .then(() => transitionFactory(emptyTransition, enteringView, leavingView, opts)) - .then((hydratedTransition) => { - 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 ? nav.views[0] : null; -} - -export function getLastView(nav: Nav): ViewController { - return nav.views && nav.views.length ? nav.views[nav.views.length - 1] : 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 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() as ViewController; - } - return nav.views[nav.views.indexOf(viewController) - 1]; -} - -export function getNextNavId() { - return navControllerIds++; -} - -let navControllerIds = NAV_ID_START; diff --git a/packages/core/src/components/nav/nav.tsx b/packages/core/src/components/nav/nav.tsx index 454072975d..bae73fff57 100644 --- a/packages/core/src/components/nav/nav.tsx +++ b/packages/core/src/components/nav/nav.tsx @@ -1,196 +1,203 @@ -import { Component, Element, Event, EventEmitter, Listen, Method, Prop, Watch } from '@stencil/core'; -import { - Animation, - AnimationController, - AnimationOptions, - ComponentDataPair, - Config, - EscapeHatch, - ExternalNavData, - FrameworkDelegate, - NavOptions, - NavOutlet, - NavResult, - PublicNav, - PublicViewController, - RouterDelegate, - RouterEntries, - Transition, - TransitionInstruction, -} from '../../index'; -import { ViewController } from './view-controller'; - +import { Component, Element, Event, EventEmitter, Method, Prop, Watch } from '@stencil/core'; import { DIRECTION_BACK, DIRECTION_FORWARD, + INIT_ZINDEX, + NavOptions, + NavResult, STATE_ATTACHED, STATE_DESTROYED, + STATE_INITIALIZED, STATE_NEW, - VIEW_ID_START, - destroyTransition, - getActiveImpl, - getFirstView, - getHydratedTransition, - getLastView, - getNextNavId, - getNextTransitionId, - getParentTransitionId, - getPreviousImpl, - getViews, - isViewController, - setZIndex, - toggleHidden, - transitionFactory -} from './nav-utils'; + TransitionDoneFn, + TransitionInstruction, + convertToViews, + isPresent, + setZIndex +} from './nav-util'; -import { DomFrameworkDelegate } from '../../utils/dom-framework-delegate'; -import { DomRouterDelegate } from '../../utils/dom-router-delegate'; +import { ViewController, isViewController } from './view-controller'; +import { AnimationOptions, Config, DomController, GestureDetail, NavOutlet } from '../..'; +import { assert, isBlank, isNumber } from '../../utils/helpers'; -import { - assert, - focusOutActiveElement, - isDef, - isNumber, - isParentTab, - normalizeUrl, -} from '../../utils/helpers'; +import { TransitionController } from './transition-controller'; +import { Transition } from './transition'; +import iosTransitionAnimation from './animations/ios.transition'; +import mdTransitionAnimation from './animations/md.transition'; -import { buildIOSTransition } from './transitions/transition.ios'; -import { buildMdTransition } from './transitions/transition.md'; -import { GestureDetail } from '../gesture/gesture'; +const TrnsCtrl = new TransitionController(); -const transitionQueue = new Map(); -const allTransitionsCompleteHandlerQueue = new Map(); - -/* it is very important to keep this class in sync with ./nav-interface interface */ @Component({ tag: 'ion-nav', styleUrl: 'nav.scss' }) -export class Nav implements PublicNav, NavOutlet { +export class NavControllerBase implements NavOutlet { - @Element() element: HTMLElement; - @Event() navInit: EventEmitter; - @Event() ionNavChanged: EventEmitter; + private _children: NavControllerBase[] = []; + private _ids = -1; + private _init = false; + private _queue: TransitionInstruction[] = []; + private _sbTrns: Transition; + isTransitioning = false; + private _destroyed = false; - useRouter = false; - navId = getNextNavId(); - routes: RouterEntries = []; - parent: Nav = null; - views: ViewController[] = []; - transitioning = false; - destroyed = false; - transitionId = NOT_TRANSITIONING_TRANSITION_ID; - initialized = false; - sbTrns: any; // TODO Transition - childNavs?: Nav[]; - urlExternalNavMap = new Map(); + _views: ViewController[] = []; + _trnsId: number = null; - @Prop() mode: string; + id: string; + name: string; + mode: string; + + parent: any; + + @Element() el: HTMLElement; + + @Prop({context: 'dom'}) dom: DomController; + @Prop({context: 'config'}) config: Config; + + @Prop({ connect: 'ion-animation-controller' }) animationCtrl: HTMLIonAnimationControllerElement; + @Prop({mutable: true}) swipeBackEnabled: boolean; @Prop() root: any; - @Prop() delegate: FrameworkDelegate; - @Prop() routerDelegate: RouterDelegate; - @Prop() useUrls = false; - @Prop({ context: 'config' }) config: Config; - @Prop({ connect: 'ion-animation-controller' }) animationCtrl: AnimationController; - @Prop() lazy = false; - @Prop() swipeBackEnabled = true; + @Watch('root') + rootChanged() { + if (this.root) { + const useRouter = !!document.querySelector('ion-router'); + if (!useRouter) { + this.setRoot(this.root); + } else { + console.warn(' does not support a root attribute when using ion-router.'); + } + } + } - constructor() { - this.navId = getNextNavId(); + @Event() ionNavChanged: EventEmitter; + + componentWillLoad() { + if (this.swipeBackEnabled === undefined) { + this.swipeBackEnabled = this.mode === 'ios' && this.config.getBoolean('swipeBackEnabled', true); + } } componentDidLoad() { - return componentDidLoadImpl(this); + this.id = 'n' + (++ctrlIds); + this.rootChanged(); } - @Watch('root') - updateRootComponent(): Promise { - if (this.initialized) { - return this.setRoot(this.root); + componentDidUnload() { + this.destroy(); + } + + @Method() + push(page: any, params?: any, opts?: NavOptions, done?: TransitionDoneFn): Promise { + return this._queueTrns({ + insertStart: -1, + insertViews: [{ page: page, params: params }], + opts: opts, + }, done); + } + + @Method() + insert(insertIndex: number, page: any, params?: any, opts?: NavOptions, done?: TransitionDoneFn): Promise { + return this._queueTrns({ + insertStart: insertIndex, + insertViews: [{ page: page, params: params }], + opts: opts, + }, done); + } + + @Method() + insertPages(insertIndex: number, insertPages: any[], opts?: NavOptions, done?: TransitionDoneFn): Promise { + return this._queueTrns({ + insertStart: insertIndex, + insertViews: insertPages, + opts: opts, + }, done); + } + + @Method() + pop(opts?: NavOptions, done?: TransitionDoneFn): Promise { + return this._queueTrns({ + removeStart: -1, + removeCount: 1, + opts: opts, + }, done); + } + + @Method() + popTo(indexOrViewCtrl: any, opts?: NavOptions, done?: TransitionDoneFn): Promise { + const config: TransitionInstruction = { + removeStart: -1, + removeCount: -1, + opts: opts + }; + if (isViewController(indexOrViewCtrl)) { + config.removeView = indexOrViewCtrl; + config.removeStart = 1; + } else if (isNumber(indexOrViewCtrl)) { + config.removeStart = indexOrViewCtrl + 1; } - return Promise.resolve(null); + return this._queueTrns(config, done); } @Method() - getViews(): PublicViewController[] { - return getViews(this); + popToRoot(opts?: NavOptions, done?: TransitionDoneFn): Promise { + return this._queueTrns({ + removeStart: 1, + removeCount: -1, + opts: opts, + }, done); } @Method() - push(component: any, data?: any, opts?: NavOptions, escapeHatch: EscapeHatch = getDefaultEscapeHatch()): Promise { - return pushImpl(this, component, data, opts, escapeHatch); + popAll(): Promise { + const promises: any[] = []; + for (let i = this._views.length - 1; i >= 0; i--) { + promises.push(this.pop(null)); + } + return Promise.all(promises); } @Method() - pop(opts?: NavOptions, escapeHatch: EscapeHatch = getDefaultEscapeHatch()): Promise { - return popImpl(this, opts, escapeHatch); + removeIndex(startIndex: number, removeCount = 1, opts?: NavOptions, done?: TransitionDoneFn): Promise { + return this._queueTrns({ + removeStart: startIndex, + removeCount: removeCount, + opts: opts, + }, done); } @Method() - setRoot(component: any, data?: any, opts?: NavOptions, escapeHatch: EscapeHatch = getDefaultEscapeHatch()): Promise { - return setRootImpl(this, component, data, opts, escapeHatch); + removeView(viewController: ViewController, opts?: NavOptions, done?: TransitionDoneFn): Promise { + return this._queueTrns({ + removeView: viewController, + removeStart: 0, + removeCount: 1, + opts: opts, + }, done); } @Method() - insert(insertIndex: number, page: any, params?: any, opts?: NavOptions, escapeHatch: EscapeHatch = getDefaultEscapeHatch()): Promise { - return insertImpl(this, insertIndex, page, params, opts, escapeHatch); + setRoot(pageOrViewCtrl: any, params?: any, opts?: NavOptions, done?: TransitionDoneFn): Promise { + return this.setPages([{ page: pageOrViewCtrl, params: params }], opts, done); } @Method() - insertPages(insertIndex: number, insertPages: any[], opts?: NavOptions, escapeHatch: EscapeHatch = getDefaultEscapeHatch()): Promise { - return insertPagesImpl(this, insertIndex, insertPages, opts, escapeHatch); - } - - @Method() - popToRoot(opts?: NavOptions, escapeHatch: EscapeHatch = getDefaultEscapeHatch()): Promise { - return popToRootImpl(this, opts, escapeHatch); - } - - @Method() - popTo(indexOrViewCtrl: any, opts?: NavOptions, escapeHatch: EscapeHatch = getDefaultEscapeHatch()): Promise { - return popToImpl(this, indexOrViewCtrl, opts, escapeHatch); - } - - @Method() - removeIndex(startIndex: number, removeCount?: number, opts?: NavOptions, escapeHatch: EscapeHatch = getDefaultEscapeHatch()): Promise { - return removeImpl(this, startIndex, removeCount, opts, escapeHatch); - } - - @Method() - removeView(viewController: PublicViewController, opts?: NavOptions, escapeHatch: EscapeHatch = getDefaultEscapeHatch()): Promise { - return removeViewImpl(this, viewController, opts, escapeHatch); - } - - @Method() - setPages(componentDataPairs: ComponentDataPair[], opts?: NavOptions, escapeHatch: EscapeHatch = getDefaultEscapeHatch()): Promise { - return setPagesImpl(this, componentDataPairs, opts, escapeHatch); - } - - @Method() - getActive(): PublicViewController { - return getActiveImpl(this); - } - - @Method() - getPrevious(view?: PublicViewController): PublicViewController { - return getPreviousImpl(this, view as ViewController); - } - - @Method() - canGoBack(): boolean { - return canGoBackImpl(this); - } - - @Method() - first(): PublicViewController { - return getFirstView(this); - } - - @Method() - last(): PublicViewController { - return getLastView(this); + setPages(pages: any[], opts?: NavOptions, done?: TransitionDoneFn): Promise { + if (isBlank(opts)) { + opts = {}; + } + // if animation wasn't set to true then default it to NOT animate + if (opts.animate !== true) { + opts.animate = false; + } + return this._queueTrns({ + insertStart: 0, + insertViews: pages, + removeStart: 0, + removeCount: -1, + opts: opts + }, done); } @Method() @@ -201,20 +208,12 @@ export class Nav implements PublicNav, NavOutlet { } if (direction === 1) { return this.push(id).then(() => true); - } else if (direction === -1 && this._canGoBack(id)) { + } else if (direction === -1 && this.canGoBack()) { return this.pop().then(() => true); } return this.setRoot(id).then(() => true); } - private _canGoBack(id: any) { - if (!this.canGoBack()) { - return false; - } - const view = this.views[this.views.length - 1]; - return view.component === id; - } - @Method() getRouteId(): string | null { const element = this.getContentElement(); @@ -226,7 +225,7 @@ export class Nav implements PublicNav, NavOutlet { @Method() getContentElement(): HTMLElement { - const active = getActiveImpl(this); + const active = this.getActive(); if (active) { return active.element; } @@ -234,100 +233,785 @@ export class Nav implements PublicNav, NavOutlet { } @Method() - getChildNavs(): PublicNav[] { - return this.childNavs || []; + getAllChildNavs(): any[] { + return this._children.slice(); + } + + @Method() + canGoBack(): boolean { + const activeView = this.getActive(); + return !!(activeView && activeView.enableBack()); } @Method() - isTransitioning() { - return this.transitionId >= 0; + getActive(): ViewController { + return this._views[this._views.length - 1]; } @Method() - getId() { - return this.navId; + getByIndex(index: number): ViewController { + return this._views[index]; } @Method() - setParent(parent: Nav) { - this.parent = parent; + getPrevious(view?: ViewController): ViewController { + // returns the view controller which is before the given view controller. + if (!view) { + view = this.getActive(); + } + const views = this._views; + const index = views.indexOf(view); + return (index > 0) ? views[index - 1] : null; } @Method() - onAllTransitionsComplete() { - return allTransitionsCompleteImpl(this); + getViews(): Array { + return this._views.slice(); } + /** + * Return a view controller + */ @Method() - reconcileFromExternalRouter(component: any, data: any = {}, escapeHatch: EscapeHatch, isTopLevel: boolean) { - return reconcileFromExternalRouterImpl(this, component, data, escapeHatch, isTopLevel); + getViewById(id: string): ViewController { + for (const vc of this._views) { + if (vc && vc.id === id) { + return vc; + } + } + return null; } - @Method() - activateFromTab(tabAlreadySelected: boolean) { - return activateFromTabImpl(this, tabAlreadySelected); + indexOf(viewController: ViewController) { + return this._views.indexOf(viewController); } - canSwipeBack(): boolean { - return (this.swipeBackEnabled && - // this.childNavs.length === 0 && - !this.isTransitioning() && - // this._app.isEnabled() && - this.canGoBack()); + length() { + return this._views.length; } - swipeBackStart() { + // _queueTrns() adds a navigation stack change to the queue and schedules it to run: + // 1. _nextTrns(): consumes the next transition in the queue + // 2. _viewInit(): initializes enteringView if required + // 3. _viewTest(): ensures canLeave/canEnter returns true, so the operation can continue + // 4. _postViewInit(): add/remove the views from the navigation stack + // 5. _transitionInit(): initializes the visual transition if required and schedules it to run + // 6. _viewAttachToDOM(): attaches the enteringView to the DOM + // 7. _transitionStart(): called once the transition actually starts, it initializes the Animation underneath. + // 8. _transitionFinish(): called once the transition finishes + // 9. _cleanup(): syncs the navigation internal state with the DOM. For example it removes the pages from the DOM or hides/show them. + private _queueTrns(ti: TransitionInstruction, done: TransitionDoneFn): Promise { + const promise = new Promise((resolve, reject) => { + ti.resolve = resolve; + ti.reject = reject; + }); + ti.done = done; + + // Normalize empty + if (ti.insertViews && ti.insertViews.length === 0) { + ti.insertViews = undefined; + } + + // Enqueue transition instruction + this._queue.push(ti); + + // if there isn't a transition already happening + // then this will kick off this transition + this._nextTrns(); + + return promise; + } + + private _success(result: NavResult, ti: TransitionInstruction) { + if (this._queue === null) { + this._fireError('nav controller was destroyed', ti); + return; + } + this._init = true; + this._trnsId = null; + + // ensure we're not transitioning here + this.isTransitioning = false; + // let's see if there's another to kick off + this._nextTrns(); + this.ionNavChanged.emit({ + isPop: result.direction === 'back' + }); + + if (ti.done) { + ti.done( + result.hasCompleted, + result.requiresTransition, + result.enteringName, + result.leavingName, + result.direction + ); + } + ti.resolve(result.hasCompleted); + } + + private _failed(rejectReason: any, ti: TransitionInstruction) { + if (this._queue === null) { + this._fireError('nav controller was destroyed', ti); + return; + } + this._trnsId = null; + this._queue.length = 0; + + // let's see if there's another to kick off + this.isTransitioning = false; + this._nextTrns(); + + this._fireError(rejectReason, ti); + } + + private _fireError(rejectReason: any, ti: TransitionInstruction) { + if (ti.done) { + ti.done(false, false, rejectReason); + } + if (ti.reject && !this._destroyed) { + ti.reject(rejectReason); + } else { + ti.resolve(false); + } + } + + private _nextTrns(): boolean { + // this is the framework's bread 'n butta function + // only one transition is allowed at any given time + if (this.isTransitioning) { + return false; + } + + // there is no transition happening right now + // get the next instruction + const ti = this._queue.shift(); + if (!ti) { + return false; + } + + // set that this nav is actively transitioning + let enteringView: ViewController; + let leavingView: ViewController; + + this._startTI(ti) + .then(() => { + this._prepareViewControllers(ti); + leavingView = this.getActive(); + enteringView = this._getEnteringView(ti, leavingView); + + if (!leavingView && !enteringView) { + throw new Error('no views in the stack to be removed'); + } + + // Needs transition? + ti.requiresTransition = (ti.enteringRequiresTransition || ti.leavingRequiresTransition) && enteringView !== leavingView; + + if (enteringView && enteringView._state === STATE_NEW) { + this._viewInit(enteringView); + } + }) + .then(() => this._postViewInit(enteringView, leavingView, ti)) + .then(() => this._transition(enteringView, leavingView, ti)) + .then((result) => this._success(result, ti)) + .catch((rejectReason) => this._failed(rejectReason, ti)); + + return true; + } + + private _waitUntilReady(enteringView: ViewController, leavingView: ViewController) { + const promises = []; + if (enteringView) { + promises.push(isReady(enteringView.element)); + } + if (leavingView) { + promises.push(isReady(leavingView.element)); + } + return Promise.all(promises); + } + + private _startTI(ti: TransitionInstruction): Promise { + const viewsLength = this._views.length; + + if (isPresent(ti.removeView)) { + assert(isPresent(ti.removeStart), 'removeView needs removeStart'); + assert(isPresent(ti.removeCount), 'removeView needs removeCount'); + + const index = this._views.indexOf(ti.removeView); + if (index < 0) { + return Promise.reject('removeView was not found'); + } + ti.removeStart += index; + } + if (isPresent(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 (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); + } + this.isTransitioning = true; + return Promise.resolve(); + } + + private _prepareViewControllers(ti: TransitionInstruction) { + const insertViews = ti.insertViews; + if (!insertViews) { + return; + } + assert(insertViews.length > 0, 'length can not be zero'); + const viewControllers = convertToViews(insertViews); + + if (viewControllers.length === 0) { + throw new Error('invalid views to insert'); + } + + // Check all the inserted view are correct + for (let i = 0; i < viewControllers.length; i++) { + const view = viewControllers[i]; + const nav = view._nav; + if (nav && nav !== this) { + throw new Error('inserted view was already inserted'); + } + if (view._state === STATE_DESTROYED) { + throw new Error('inserted view was already destroyed'); + } + } + ti.insertViews = viewControllers; + } + + private _getEnteringView(ti: TransitionInstruction, leavingView: ViewController): ViewController { + const insertViews = ti.insertViews; + if (insertViews) { + // grab the very last view of the views to be inserted + // and initialize it as the new entering view + return insertViews[insertViews.length - 1]; + } + + const removeStart = ti.removeStart; + if (isPresent(removeStart)) { + const views = this._views; + const removeEnd = removeStart + ti.removeCount; + for (let i = views.length - 1; i >= 0; i--) { + const view = views[i]; + if ((i < removeStart || i >= removeEnd) && view !== leavingView) { + return view; + } + } + } + return null; + } + + private _postViewInit(enteringView: ViewController, leavingView: ViewController, ti: TransitionInstruction) { + assert(leavingView || enteringView, 'Both leavingView and enteringView are null'); + assert(ti.resolve, 'resolve must be valid'); + assert(ti.reject, 'reject must be valid'); + + const opts = ti.opts || {}; + const insertViews = ti.insertViews; + const removeStart = ti.removeStart; + const removeCount = ti.removeCount; + let destroyQueue: ViewController[]; + + // there are views to remove + if (isPresent(removeStart)) { + assert(removeStart >= 0, 'removeStart can not be negative'); + assert(removeCount >= 0, 'removeCount can not be negative'); + + destroyQueue = []; + for (let i = 0; i < removeCount; i++) { + const view = this._views[i + removeStart]; + if (view && view !== enteringView && view !== leavingView) { + destroyQueue.push(view); + } + } + // default the direction to "back" + opts.direction = opts.direction || DIRECTION_BACK; + } + + const finalBalance = this._views.length + (insertViews ? insertViews.length : 0) - (removeCount ? removeCount : 0); + assert(finalBalance >= 0, 'final balance can not be negative'); + if (finalBalance === 0) { + console.warn(`You can't remove all the pages in the navigation stack. nav.pop() is probably called too many times.`, + this, this.el); + + throw new Error('navigation stack needs at least one root page'); + } + + // At this point the transition can not be rejected, any throw should be an error + // there are views to insert + if (insertViews) { + // manually set the new view's id if an id was passed in the options + if (isPresent(opts.id)) { + enteringView.id = opts.id; + } + + // add the views to the + for (let i = 0; i < insertViews.length; i++) { + const view = insertViews[i]; + this._insertViewAt(view, ti.insertStart + i); + } + + if (ti.enteringRequiresTransition) { + // default to forward if not already set + opts.direction = 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 + // let's make sure, callbacks are zoned + if (destroyQueue && destroyQueue.length > 0) { + for (let i = 0; i < destroyQueue.length; i++) { + const view = destroyQueue[i]; + view._willLeave(true); + view._didLeave(); + view._willUnload(); + } + + // once all lifecycle events has been delivered, we can safely detroy the views + for (let i = 0; i < destroyQueue.length; i++) { + this._destroyView(destroyQueue[i]); + } + } + + // set which animation it should use if it wasn't set yet + if (ti.requiresTransition && !opts.animation) { + opts.animation = isPresent(ti.removeStart) + ? (leavingView || enteringView).getTransitionName(opts.direction) + : (enteringView || leavingView).getTransitionName(opts.direction); + } + ti.opts = opts; + } + + /** + * DOM WRITE + */ + private _viewInit(enteringView: ViewController) { + assert(enteringView, 'enteringView must be non null'); + assert(enteringView._state === STATE_NEW, 'enteringView state must be NEW'); + + enteringView._state = STATE_INITIALIZED; + enteringView.init(); + enteringView._preLoad(); + } + + private _viewAttachToDOM(view: ViewController) { + assert(view._state === STATE_INITIALIZED, 'view state must be INITIALIZED'); + + // fire willLoad before change detection runs + view._willLoad(); + + // render the component ref instance to the DOM + // ******** DOM WRITE **************** + this.el.appendChild(view.element); + view._state = STATE_ATTACHED; + + // TODO: fails in test + if (view._cssClass) { + // the ElementRef of the actual ion-page created + // ******** DOM WRITE ****************+ + view.element.classList.add(view._cssClass); + } + // successfully finished loading the entering view + // fire off the "didLoad" lifecycle events + view._didLoad(); + } + + private _transition(enteringView: ViewController, leavingView: ViewController, ti: TransitionInstruction): Promise { + 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 + }); + } + + const opts = ti.opts; + + // figure out if this transition is the root one or a + // child of a parent nav that has the root transition + this._trnsId = TrnsCtrl.getRootTrnsId(this); + if (this._trnsId === null) { + // this is the root transition, meaning all child navs and their views + // should be added as a child transition to this one + this._trnsId = TrnsCtrl.nextId(); + } + + // create the transition options + const animationOpts: AnimationOptions = { + animation: opts.animation, + direction: opts.direction, + duration: (opts.animate === false ? 0 : opts.duration), + easing: opts.easing, + isRTL: document.dir === 'rtl', + ev: opts.ev, + enteringView: enteringView, + leavingView: leavingView, + nav: this as any, + }; + + const animation = this.mode === 'ios' ? iosTransitionAnimation : mdTransitionAnimation; + + const transition = new Transition( + this.animationCtrl, + animation, + enteringView, + leavingView, + animationOpts + ); + TrnsCtrl.register(this._trnsId, transition); + + // ensure any swipeback transitions are cleared out + this._sbTrns && this._sbTrns.destroy(); + this._sbTrns = null; + + // swipe to go back root transition + if (!transition.parent && opts.progressAnimation) { + this._sbTrns = transition; + } + + // transition start has to be registered before attaching the view to the DOM! + const promise = new Promise(resolve => transition.registerStart(resolve)) + .then(() => this._waitUntilReady(enteringView, leavingView)) + .then(() => this._transitionInit(transition, enteringView, leavingView, opts)) + .then(() => this._transitionStart(transition, enteringView, leavingView, opts)); + + if (enteringView && (enteringView._state === STATE_INITIALIZED)) { + // render the entering component in the DOM + // this would also render new child navs/views + // which may have their very own async canEnter/Leave tests + // ******** DOM WRITE **************** + this._viewAttachToDOM(enteringView); + } + + + // if (!transition.hasChildren) { + // lowest level transition, so kick it off and let it bubble up to start all of them + transition.start(); + // } + return promise; + } + + + private _transitionInit(transition: Transition, enteringView: ViewController, leavingView: ViewController, opts: NavOptions): Promise { + assert(this.isTransitioning, 'isTransitioning has to be true'); + + this._trnsId = null; + + // set the correct zIndex for the entering and leaving views + // ******** DOM WRITE **************** + setZIndex(this, enteringView, leavingView, opts.direction); + + // always ensure the entering view is viewable + // ******** DOM WRITE **************** + enteringView && enteringView._domShow(true); + + // always ensure the leaving view is viewable + // ******** DOM WRITE **************** + leavingView && leavingView._domShow(true); + + // initialize the transition + return transition.init(); + } + + private _transitionStart(transition: Transition, enteringView: ViewController, leavingView: ViewController, opts: NavOptions): Promise { + assert(this.isTransitioning, 'isTransitioning has to be true'); + + // we should animate (duration > 0) if the pushed page is not the first one (startup) + // or if it is a portal (modal, actionsheet, etc.) + const shouldNotAnimate = !this._init && this._views.length === 1; + const canNotAnimate = !this.config.getBoolean('animate', true); + if (shouldNotAnimate || canNotAnimate) { + opts.animate = false; + } + + if (opts.animate === false) { + // if it was somehow set to not animation, then make the duration zero + transition.ani.duration(0); + } + + // create a callback that needs to run within zone + // that will fire off the willEnter/Leave lifecycle events at the right time + transition.ani.beforeAddRead(this._viewsWillLifecycles.bind(this, enteringView, leavingView)); + + // create a callback for when the animation is done + const promise = new Promise(resolve => { + transition.ani.onFinish(resolve); + }); + + if (transition.ani.isRoot()) { + // cool, let's do this, start the transition + if (opts.progressAnimation) { + // this is a swipe to go back, just get the transition progress ready + // kick off the swipe animation start + transition.ani.progressStart(); + + } else { + // only the top level transition should actually start "play" + // kick it off and let it play through + // ******** DOM WRITE **************** + transition.ani.play(); + } + } + + return promise.then(() => { + return this._transitionFinish(transition, opts); + }); + } + + private _transitionFinish(transition: Transition, opts: NavOptions): NavResult { + + const hasCompleted = transition.ani.hasCompleted; + const enteringView = transition.enteringView; + const leavingView = transition.leavingView; + + // mainly for testing + let enteringName: string; + let leavingName: string; + + if (hasCompleted) { + // transition has completed (went from 0 to 1) + if (enteringView) { + enteringName = enteringView.name; + enteringView._didEnter(); + } + + if (leavingView) { + leavingName = leavingView.name; + leavingView._didLeave(); + } + + this._cleanup(enteringView); + } else { + // If transition does not complete, we have to cleanup anyway, because + // previous pages in the stack are not hidden probably. + this._cleanup(leavingView); + } + + if (transition.ani.isRoot()) { + // this is the root transition + // it's safe to destroy this transition + TrnsCtrl.destroy(transition.trnsId); + + // it's safe to enable the app again + // mark ourselves as not transitioning - `deepLinker navchange` requires this + // TODO - probably could be resolved in a better way + this.isTransitioning = false; + } + + return { + hasCompleted: hasCompleted, + requiresTransition: true, + enteringName: enteringName, + leavingName: leavingName, + direction: opts.direction + }; + } + + private _viewsWillLifecycles(enteringView: ViewController, leavingView: ViewController) { + if (enteringView || leavingView) { + // Here, the order is important. WillLeave must be called before WillEnter. + if (leavingView) { + const willUnload = enteringView ? leavingView.index > enteringView.index : true; + leavingView._willLeave(willUnload); + } + enteringView && enteringView._willEnter(); + } + } + + private _insertViewAt(view: ViewController, index: number) { + const existingIndex = this._views.indexOf(view); + if (existingIndex > -1) { + // this view is already in the stack!! + // move it to its new location + assert(view._nav === this, 'view is not part of the nav'); + this._views.splice(index, 0, this._views.splice(existingIndex, 1)[0]); + } else { + assert(!view._nav, 'nav is used'); + // this is a new view to add to the stack + // create the new entering view + view._setNav(this); + + // give this inserted view an ID + this._ids++; + if (!view.id) { + view.id = `${this.id}-${this._ids}`; + } + + // insert the entering view into the correct index in the stack + this._views.splice(index, 0, view); + } + } + + private _removeView(view: ViewController) { + assert(view._state === STATE_ATTACHED || view._state === STATE_DESTROYED, 'view state should be loaded or destroyed'); + + const views = this._views; + const index = views.indexOf(view); + assert(index > -1, 'view must be part of the stack'); + if (index >= 0) { + views.splice(index, 1); + } + } + + private _destroyView(view: ViewController) { + view._destroy(); + this._removeView(view); + } + + /** + * DOM WRITE + */ + private _cleanup(activeView: ViewController) { + // ok, cleanup time!! Destroy all of the views that are + // INACTIVE and come after the active view + // only do this if the views exist, though + if (!this._destroyed) { + const activeViewIndex = this._views.indexOf(activeView); + const views = this._views; + let reorderZIndexes = false; + let view: ViewController; + let i: number; + + for (i = views.length - 1; i >= 0; i--) { + view = views[i]; + if (i > activeViewIndex) { + // this view comes after the active view + // let's unload it + view._willUnload(); + this._destroyView(view); + + } else if (i < activeViewIndex) { + // this view comes before the active view + // and it is not a portal then ensure it is hidden + view._domShow(false); + } + if (view._zIndex <= 0) { + reorderZIndexes = true; + } + } + + if (reorderZIndexes) { + for (i = 0; i < views.length; i++) { + view = views[i]; + // ******** DOM WRITE **************** + view._setZIndex(view._zIndex + INIT_ZINDEX + 1); + } + } + } + } + + // registerChildNav(container: NavigationContainer) { + // this._children.push(container); + // } + + // unregisterChildNav(nav: any) { + // this._children = this._children.filter(child => child !== nav); + // } + + destroy() { + const views = this._views; + let view: ViewController; + for (let i = 0; i < views.length; i++) { + view = views[i]; + view._willUnload(); + view._destroy(); + } + + // release swipe back gesture and transition + this._sbTrns && this._sbTrns.destroy(); + this._queue = this._views = this._sbTrns = null; + + // Unregister navcontroller + if (this.parent && this.parent.unregisterChildNav) { + // TODO: event + this.parent.unregisterChildNav(this); + } + + this._destroyed = true; + } + + private swipeBackStart() { + if (this.isTransitioning || this._queue.length > 0) { + return; + } + // default the direction to "back"; const opts: NavOptions = { direction: DIRECTION_BACK, progressAnimation: true }; - return popImpl(this, opts, {}); + this._queueTrns({ + removeStart: -1, + removeCount: 1, + opts: opts, + }, null); } - swipeBackProgress(detail: GestureDetail) { - if (!this.sbTrns) { - return; - } - // continue to disable the app while actively dragging - // this._app.setEnabled(false, ACTIVE_TRANSITION_DEFAULT); - // this.setTransitioning(true); + private swipeBackProgress(detail: GestureDetail) { + if (this._sbTrns) { + // continue to disable the app while actively dragging + // TODO + // this.app.setEnabled(false, ACTIVE_TRANSITION_DEFAULT); + this.isTransitioning = true; - const delta = detail.deltaX; - const stepValue = delta / window.innerWidth; - // set the transition animation's progress - this.sbTrns.progressStep(stepValue); + // set the transition animation's progress + const delta = detail.deltaX; + const stepValue = delta / window.innerWidth; + // set the transition animation's progress + this._sbTrns.ani.progressStep(stepValue); + } } - swipeBackEnd(detail: GestureDetail) { - if (!this.sbTrns) { - return; - } - // the swipe back gesture has ended - const delta = detail.deltaX; - const width = window.innerWidth; - const stepValue = delta / width; - const velocity = detail.velocityX; - const z = width / 2.0; - const shouldComplete = (velocity >= 0) - && (velocity > 0.2 || detail.deltaX > z); + private swipeBackEnd(detail: GestureDetail) { + if (this._sbTrns) { + // the swipe back gesture has ended + const delta = detail.deltaX; + const width = window.innerWidth; + const stepValue = delta / width; + const velocity = detail.velocityX; + const z = width / 2.0; + const shouldComplete = (velocity >= 0) + && (velocity > 0.2 || detail.deltaX > z); - const missing = shouldComplete ? 1 - stepValue : stepValue; - const missingDistance = missing * width; - let realDur = 0; - if (missingDistance > 5) { - const dur = missingDistance / Math.abs(velocity); - realDur = Math.min(dur, 300); - } + const missing = shouldComplete ? 1 - stepValue : stepValue; + const missingDistance = missing * width; + let realDur = 0; + if (missingDistance > 5) { + const dur = missingDistance / Math.abs(velocity); + realDur = Math.min(dur, 300); + } - this.sbTrns.progressEnd(shouldComplete, stepValue, realDur); + this._sbTrns.ani.progressEnd(shouldComplete, stepValue, realDur); + } } - @Listen('navInit') - navInitialized(event: NavEvent) { - navInitializedImpl(this, event); + canSwipeBack(): boolean { + return ( + this.swipeBackEnabled && + this._children.length === 0 && + !this.isTransitioning && + this.canGoBack() + ); } render() { @@ -353,1105 +1037,15 @@ export class Nav implements PublicNav, NavOutlet { } } -export function componentDidLoadImpl(nav: Nav) { - if (nav.initialized) { - return; - } - nav.initialized = true; - nav.navInit.emit(); - if (!nav.useRouter) { - if (nav.root && !nav.lazy) { - nav.setRoot(nav.root); - } - } -} +let ctrlIds = -1; -export async function pushImpl(nav: Nav, component: any, data: any, opts: NavOptions, escapeHatch: EscapeHatch) { - const animation = await hydrateAnimationController(nav.animationCtrl); - return push(nav, nav.delegate, animation, component, data, opts, escapeHatch).then((navResult) => { - return navResult; - }); -} - -export async function popImpl(nav: Nav, opts: NavOptions, escapeHatch: EscapeHatch) { - const animation = await hydrateAnimationController(nav.animationCtrl); - return pop(nav, nav.delegate, animation, opts, escapeHatch).then((navResult) => { - return navResult; - }); -} - -export async function setRootImpl(nav: Nav, component: any, data: any, opts: NavOptions, escapeHatch: EscapeHatch) { - const animation = await hydrateAnimationController(nav.animationCtrl); - return setRoot(nav, nav.delegate, animation, component, data, opts, escapeHatch).then((navResult) => { - return navResult; - }); -} - -export async function insertImpl(nav: Nav, insertIndex: number, page: any, params: any, opts: NavOptions, escapeHatch: EscapeHatch) { - const animation = await hydrateAnimationController(nav.animationCtrl); - return insert(nav, nav.delegate, animation, insertIndex, page, params, opts, escapeHatch); -} - -export async function insertPagesImpl(nav: Nav, insertIndex: number, pagesToInsert: any[], opts: NavOptions, escapeHatch: EscapeHatch) { - const animation = await hydrateAnimationController(nav.animationCtrl); - return insertPages(nav, nav.delegate, animation, insertIndex, pagesToInsert, opts, escapeHatch); -} - -export async function popToRootImpl(nav: Nav, opts: NavOptions, escapeHatch: EscapeHatch) { - const animation = await hydrateAnimationController(nav.animationCtrl); - return popToRoot(nav, nav.delegate, animation, opts, escapeHatch); -} - -export async function popToImpl(nav: Nav, indexOrViewCtrl: any, opts: NavOptions, escapeHatch: EscapeHatch) { - const animation = await hydrateAnimationController(nav.animationCtrl); - return popTo(nav, nav.delegate, animation, indexOrViewCtrl, opts, escapeHatch); -} - -export async function removeImpl(nav: Nav, startIndex: number, removeCount: number, opts: NavOptions, escapeHatch: EscapeHatch) { - const animation = await hydrateAnimationController(nav.animationCtrl); - return remove(nav, nav.delegate, animation, startIndex, removeCount, opts, escapeHatch); -} - -export async function removeViewImpl(nav: Nav, viewController: PublicViewController, opts: NavOptions, escapeHatch?: any) { - const animation = await hydrateAnimationController(nav.animationCtrl); - return removeView(nav, nav.delegate, animation, viewController as ViewController, opts, escapeHatch); -} - -export async function setPagesImpl(nav: Nav, componentDataPairs: ComponentDataPair[], opts: NavOptions, escapeHatch: EscapeHatch) { - const animation = await hydrateAnimationController(nav.animationCtrl); - return setPages(nav, nav.delegate, animation, componentDataPairs, opts, escapeHatch, null); -} - -export function canGoBackImpl(nav: Nav) { - return nav.views && nav.views.length > 1; -} - -export function navInitializedImpl(potentialParent: Nav, event: NavEvent) { - if ((potentialParent.element as any as HTMLIonNavElement) !== event.target) { - // set the parent on the child nav that dispatched the event - event.target.setParent(potentialParent); - if (!potentialParent.childNavs) { - potentialParent.childNavs = []; - } - potentialParent.childNavs.push((event.detail as Nav)); - // kill the event so it doesn't propagate further - event.stopPropagation(); - } -} - -export function hydrateAnimationController(animationController: AnimationController): Promise { - return animationController.create(); -} - -// public api - -export function push(nav: Nav, delegate: FrameworkDelegate, animation: Animation, component: any, data: any, opts: NavOptions, escapeHatch: EscapeHatch): Promise { - return preprocessTransaction({ - component: component, - insertStart: -1, - insertViews: [{component, data}], - opts, - nav, - delegate, - id: nav.navId, - animation, - escapeHatch, - method: PUSH - }); -} - -export function insert(nav: Nav, delegate: FrameworkDelegate, animation: Animation, insertIndex: number, component: any, data: any, opts: NavOptions, escapeHatch: EscapeHatch): Promise { - return preprocessTransaction({ - component: component, - insertStart: insertIndex, - insertViews: [{ component, data }], - opts, - nav, - delegate, - id: nav.navId, - animation, - escapeHatch, - method: 'insert' - }); -} - -export function insertPages(nav: Nav, delegate: FrameworkDelegate, animation: Animation, insertIndex: number, insertPages: any[], opts: NavOptions, escapeHatch: EscapeHatch): Promise { - return preprocessTransaction({ - component: null, - insertStart: insertIndex, - insertViews: insertPages, - opts, - nav, - delegate, - id: nav.navId, - animation, - escapeHatch, - method: 'insertPages' - }); -} - -export function pop(nav: Nav, delegate: FrameworkDelegate, animation: Animation, opts: NavOptions, escapeHatch: EscapeHatch): Promise { - return preprocessTransaction({ - component: null, - removeStart: -1, - removeCount: 1, - opts, - nav, - delegate, - id: nav.navId, - animation, - escapeHatch, - method: POP - }); -} - -export function popToRoot(nav: Nav, delegate: FrameworkDelegate, animation: Animation, opts: NavOptions, escapeHatch: EscapeHatch): Promise { - return preprocessTransaction({ - component: null, - removeStart: 1, - removeCount: -1, - opts, - nav, - delegate, - id: nav.navId, - animation, - escapeHatch, - method: 'popToRoot' - }); -} - -export function popTo(nav: Nav, delegate: FrameworkDelegate, animation: Animation, indexOrViewCtrl: any, opts: NavOptions, escapeHatch: EscapeHatch): Promise { - const config: TransitionInstruction = { - component: null, - removeStart: -1, - removeCount: -1, - opts, - nav, - delegate, - id: nav.navId, - animation, - escapeHatch, - method: 'popTo' - }; - if (isViewController(indexOrViewCtrl)) { - config.removeView = indexOrViewCtrl; - config.removeStart = 1; - } else if (isNumber(indexOrViewCtrl)) { - config.removeStart = indexOrViewCtrl + 1; - } - return preprocessTransaction(config); -} - -export function remove(nav: Nav, delegate: FrameworkDelegate, animation: Animation, startIndex: number, removeCount = 1, opts: NavOptions, escapeHatch: EscapeHatch): Promise { - return preprocessTransaction({ - component: null, - removeStart: startIndex, - removeCount: removeCount, - opts, - nav, - delegate, - id: nav.navId, - animation, - escapeHatch, - method: 'remove' - }); -} - -export function removeView(nav: Nav, delegate: FrameworkDelegate, animation: Animation, viewController: ViewController, opts: NavOptions, escapeHatch: EscapeHatch): Promise { - return preprocessTransaction({ - component: null, - removeView: viewController, - removeStart: 0, - removeCount: 1, - opts, - nav, - delegate, - id: nav.navId, - animation, - escapeHatch, - method: 'removeView' - }); -} - -export function setRoot(nav: Nav, delegate: FrameworkDelegate, animation: Animation, component: any, data: any, opts: NavOptions, escapeHatch: EscapeHatch): Promise { - return setPages(nav, delegate, animation, [{ component, data }], opts, escapeHatch, SET_ROOT); -} - -export function setPages(nav: Nav, delegate: FrameworkDelegate, animation: Animation, componentDataPairs: ComponentDataPair[], opts: NavOptions, escapeHatch: EscapeHatch, methodName: string): Promise { - if (!isDef(opts)) { - opts = {}; - } - if (opts.animate !== true) { - opts.animate = false; - } - return preprocessTransaction({ - component: componentDataPairs.length === 1 ? componentDataPairs[0].component : null, - insertStart: 0, - insertViews: componentDataPairs, - removeStart: 0, - removeCount: -1, - opts, - nav, - delegate, - id: nav.navId, - animation, - escapeHatch, - method: methodName ? methodName : 'setPages' - }); -} - -export function preprocessTransaction(ti: TransitionInstruction): Promise { - if (isUrl(ti.component)) { - if (ti.method === PUSH || ti.method === POP || ti.method === SET_ROOT) { - return navigateToUrl(ti.nav, normalizeUrl(ti.component), ti.method); - } else { - return Promise.reject(new Error('only push, pop, and setRoot methods support urls')); - } - } - - const response = checkIfPopRedirectRequired(ti); - if (response.required) { - return navigateToUrl(ti.nav, response.url, POP); - } - - return queueTransaction(ti); -} - -export function isUrl(component: any): boolean { - return typeof component === 'string' && component.charAt(0) === '/'; -} - -export function navigateToUrl(nav: Nav, url: string, _method: string): Promise { - if (!nav.routerDelegate) { - nav.routerDelegate = new DomRouterDelegate(); - } - return nav.routerDelegate.pushUrlState(url); -} - -export function activateFromTabImpl(nav: Nav, tabAlreadySelected: boolean): Promise { - return nav.onAllTransitionsComplete().then(() => { - // if there is not a view set and it's not transitioning, - // go ahead and set the root - if ( (nav.getViews().length === 0 || tabAlreadySelected) && !nav.isTransitioning()) { - return nav.setRoot(nav.root); - } - - // okay, we have a view here, and it's almost certainly the correct view - // so what we need to do is update the browsers url to match what's in the top view - const viewController = nav.getActive() as ViewController; - return viewController && viewController.url ? navigateToUrl(nav, viewController.url, null) : Promise.resolve(); - }); -} - -export function queueTransaction(ti: TransitionInstruction): Promise { - const promise = new Promise((resolve, reject) => { - ti.resolve = resolve; - ti.reject = reject; - }); - - 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 { - - if (nav.transitioning) { +function isReady(el: HTMLElement): Promise { + if (!el) { return Promise.resolve(); } - - const topTransaction = getTopTransaction(nav.navId); - if (!topTransaction) { - // cool, there are no transitions going for this nav - processAllTransitionCompleteQueue(nav.navId); - return Promise.resolve(); - } - - return doNav(nav, topTransaction); -} - -export function checkIfPopRedirectRequired(ti: TransitionInstruction): IsRedirectRequired { - if (ti.method === POP) { - - if (ti.escapeHatch.fromExternalRouter) { - // if the pop method is called from a router, that means the redirect already happened - // so just do a normal pop because the url is in a good place. Basically, the router is telling us to - // pop - return { - required: false - }; - } - - // check if we need to redirect to a url for the pop operation - const popToIndex = ti.nav.views.length - 2; - if (popToIndex >= 0) { - const viewController = ti.nav.views[popToIndex]; - return { - required: viewController.fromExternalRouter, - url: viewController.url - }; - } - } - - return { - required: false, - }; -} - -export function processAllTransitionCompleteQueue(navId: number) { - const queue = allTransitionsCompleteHandlerQueue.get(navId) || []; - for (const callback of queue) { - callback(); - } - allTransitionsCompleteHandlerQueue.set(navId, []); -} - -export function allTransitionsCompleteImpl(nav: Nav) { - return new Promise((resolve) => { - const queue = transitionQueue.get(nav.navId) || []; - if (queue.length) { - // there are pending transitions, so queue it up and we'll be notified when it's done - const handlers = allTransitionsCompleteHandlerQueue.get(nav.navId) || []; - handlers.push(resolve); - return allTransitionsCompleteHandlerQueue.set(nav.navId, handlers); - } - // there are no pending transitions, so just resolve right away - return resolve(); - }); -} - -export function doNav(nav: Nav, ti: TransitionInstruction) { - let enteringView: ViewController; - let leavingView: ViewController; - return initializeViewBeforeTransition(ti).then(([_enteringView, _leavingView]) => { - enteringView = _enteringView; - leavingView = _leavingView; - return attachViewToDom(nav, enteringView, ti); - }).then(() => { - return loadViewAndTransition(nav, enteringView, leavingView, ti); - }).then(() => { - nav.ionNavChanged.emit({ isPop: ti.method === 'pop' }); - return successfullyTransitioned(ti); - }).catch((err: Error) => { - return transitionFailed(err, ti); - }); -} - -export function successfullyTransitioned(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.initialized = true; - ti.nav.transitionId = NOT_TRANSITIONING_TRANSITION_ID; - ti.nav.transitioning = false; - - // TODO - check if it's a swipe back - - // kick off next transition for this nav I guess - nextTransaction(ti.nav); - - ti.resolve({ - successful: true, - mountingData: ti.mountingData - }); - -} - -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.reject && !ti.nav.destroyed) { - ti.reject(error); + if ((el as any).componentOnReady) { + return (el as any).componentOnReady(); } else { - ti.resolve({ - successful: false, - mountingData: ti.mountingData - }); + return Promise.all(Array.from(el.children).map(isReady)); } } - -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(); - } - - 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); - return getHydratedTransition(animationOpts.animation, nav.config, nav.transitionId, emptyTransition, enteringView, leavingView, animationOpts, getDefaultTransition(nav.config)) - .then((transition) => { - - if (nav.sbTrns) { - nav.sbTrns.destroy(); - nav.sbTrns = null; - } - - // it's a swipe to go back transition - if (transition.isRoot() && ti.opts.progressAnimation) { - nav.sbTrns = transition; - } - - transition.start(); - - return executeAsyncTransition(nav, transition, enteringView, leavingView, ti.delegate, ti.opts, ti.nav.config.getBoolean('animate')); - }); -} - -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 = NOT_TRANSITIONING_TRANSITION_ID; - 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, false); - - // always ensure the leaving view is viewable - // ******** DOM WRITE **************** - leavingView && toggleHidden(leavingView.element, false); - - const isFirstPage = !nav.initialized && nav.views.length === 1; - const shouldNotAnimate = isFirstPage; - 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); - } else { - console.debug('transition is running but app has not been disabled'); - } - - 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 { - - let promise: Promise = 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(); - } - } - }); -} - -export function cleanUpView(nav: Nav, delegate: FrameworkDelegate, activeViewController: ViewController): Promise { - if (nav.destroyed) { - 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(); - promises.push(destroyView(nav, delegate, inactiveViewController)); - } else if ( i < activeIndex) { - // this view comes before the active view - // and it is not a portal then ensure it is hidden - toggleHidden(inactiveViewController.element, true); - } - - // 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, ti: TransitionInstruction): Promise { - if (enteringView && enteringView.state === STATE_NEW) { - return ti.delegate.attachViewToDom(nav.element, enteringView.component, enteringView.data, [], ti.escapeHatch).then((mountingData) => { - ti.mountingData = mountingData; - Object.assign(enteringView, mountingData); - mountingData.element.classList.add('ion-page'); - enteringView.state = STATE_ATTACHED; - }) - // implicit returns FTW - .then(() => waitForNewlyAttachedViewElementsToHydate(enteringView.element)); - } - // it's in the wrong state, so don't attach and just return - return Promise.resolve(); -} - -export function waitForNewlyAttachedViewElementsToHydate(element: HTMLElement) { - // the element may or may not be a Stencil element - // so check if it has an ``, ``, and `` for - // hydration - const promises: Promise[] = []; - if ((element as any).componentOnReady) { - // it's a stencil element - promises.push((element as any).componentOnReady()); - } - - const navs = element.querySelectorAll('ion-nav'); - for (let i = 0; i < navs.length; i++) { - const nav = navs.item(i); - promises.push((nav as any).componentOnReady()); - } - - // check for headers - const headers = element.querySelectorAll('ion-header'); - for (let i = 0; i < headers.length; i++) { - const header = headers.item(i); - promises.push((header as any).componentOnReady()); - } - - // check for contents - const contents = element.querySelectorAll('ion-content'); - for (let i = 0; i < contents.length; i++) { - const content = contents.item(i); - promises.push((content as any).componentOnReady()); - } - - // check for back buttons - const backButtons = element.querySelectorAll('ion-back-button'); - for (let i = 0; i < backButtons.length; i++) { - const backButton = backButtons.item(i); - promises.push((backButton as any).componentOnReady()); - } - - return Promise.all(promises); -} - -export function initializeViewBeforeTransition(ti: TransitionInstruction): Promise { - let leavingView: ViewController = null; - let enteringView: ViewController = null; - return startTransaction(ti).then(() => { - const viewControllers = convertComponentToViewController(ti); - ti.viewControllers = 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]; - }); -} - -export function updateNavStacks(enteringView: ViewController, leavingView: ViewController, ti: TransitionInstruction): Promise { - 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) { - 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.viewControllers) { - // 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.viewControllers.length; i++) { - insertViewIntoNav(ti.nav, ti.viewControllers[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 (const view of destroyQueue) { - view.willLeave(true); - view.didLeave(); - view.willUnload(); - } - - const destroyQueuePromises: Promise[] = []; - 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) { - ti.opts.animation = isDef(ti.removeStart) - ? (leavingView || enteringView).getTransitionName(ti.opts.direction) - : (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 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[] = []; - - - 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 { - - 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.viewControllers && ti.viewControllers.length) { - // grab the very last view of the views to be inserted - // and initialize it as the new entering view - return ti.viewControllers[ti.viewControllers.length - 1]; - } - if (isDef(ti.removeStart)) { - const 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(pairs: ComponentDataPair[], escapeHatch: EscapeHatch): ViewController[] { - return pairs.filter(pair => !!pair) - .map(pair => { - const applyEscapeHatch = pair === pairs[pairs.length - 1]; - return new ViewController(pair.component, pair.data, applyEscapeHatch ? escapeHatch.fromExternalRouter : false, applyEscapeHatch ? escapeHatch.url : null); - }); -} - -export function convertComponentToViewController(ti: TransitionInstruction): ViewController[] { - if (ti.insertViews) { - assert(ti.insertViews.length > 0, 'length can not be zero'); - const viewControllers = convertViewsToViewControllers(ti.insertViews, ti.escapeHatch); - 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'); - } - } - return viewControllers; - } - return []; -} - -export function addToQueue(ti: TransitionInstruction) { - const list = transitionQueue.get(ti.id) || []; - list.push(ti); - transitionQueue.set(ti.id, list); -} - -export function getQueue(id: number) { - return transitionQueue.get(id) || []; -} - -export function resetQueue(id: number) { - transitionQueue.set(id, []); -} - -export function getTopTransaction(id: number) { - const queue = getQueue(id); - if (!queue.length) { - return null; - } - const tmp = queue.concat(); - const toReturn = tmp.shift(); - transitionQueue.set(id, tmp); - return toReturn; -} - -export function getDefaultTransition(config: Config) { - return config.get('mode') === 'ios' ? buildIOSTransition : buildMdTransition; -} - -let viewIds = VIEW_ID_START; -const DISABLE_APP_MINIMUM_DURATION = 64; -const NOT_TRANSITIONING_TRANSITION_ID = -1; - -export interface NavEvent extends CustomEvent { - target: HTMLIonNavElement; - detail: NavEventDetail; -} - -export interface NavEventDetail { - isPop?: boolean; -} - -export function getDefaultEscapeHatch(): EscapeHatch { - return { - fromExternalRouter: false, - }; -} - -export function reconcileFromExternalRouterImpl(nav: Nav, component: any, data: any = {}, escapeHatch: EscapeHatch, isTopLevel: boolean): Promise { - // check if the nav has an `` as a parent - if (isParentTab(nav.element as any)) { - // check if the tab is selected - return updateTab(nav, component, data, escapeHatch, isTopLevel); - } else { - return updateNav(nav, component, data, escapeHatch, isTopLevel); - } -} - -export function updateTab(nav: Nav, component: any, data: any, escapeHatch: EscapeHatch, isTopLevel: boolean) { - - const tab = nav.element.parentElement as HTMLIonTabElement; - - // yeah yeah, I know this is kind of ugly but oh well, I know the internal structure of - const tabs = tab.parentElement.parentElement as HTMLIonTabsElement; - - return isTabSelected(tabs, tab).then((isSelected) => { - if (!isSelected) { - const promise = updateNav(nav, component, data, escapeHatch, false); - const app = document.querySelector('ion-app'); - return app.componentOnReady().then(() => { - app.setExternalNavPromise(promise); - - // okay, the tab is not selected, so we need to do a "switch" transition - // basically, we should update the nav, and then swap the tabs - return promise.then((navResult) => { - return tabs.select(tab).then(() => { - app.setExternalNavPromise(null); - return navResult; - }); - }); - }); - } - - // okay cool, the tab is already selected, so we want to see a transition - return updateNav(nav, component, data, escapeHatch, isTopLevel); - }); -} - -export function isTabSelected(tabsElement: HTMLIonTabsElement, tabElement: HTMLIonTabElement ): Promise { - const promises: Promise[] = []; - promises.push(tabsElement.componentOnReady()); - promises.push(tabElement.componentOnReady()); - return Promise.all(promises).then(() => { - return tabsElement.getSelected() === tabElement; - }); -} - -export function updateNav(nav: Nav, - component: any, data: any, escapeHatch: EscapeHatch, isTopLevel: boolean): Promise { - - - const url = location.pathname; - // check if the component is the top view - const activeViews = nav.getViews() as ViewController[]; - if (activeViews.length === 0) { - // there isn't a view in the stack, so push one - return nav.setRoot(component, data, {}, escapeHatch); - } - - const currentView = activeViews[activeViews.length - 1]; - if (currentView.url === url) { - // the top view is already the component being activated, so there is no change needed - return Promise.resolve(null); - } - - // check if the component is the previous view, if so, pop back to it - if (activeViews.length > 1) { - // there's at least two views in the stack - const previousView = activeViews[activeViews.length - 2]; - if (previousView.url === url) { - // cool, we match the previous view, so pop it - return nav.pop(null, escapeHatch); - } - } - - // check if the component is already in the stack of views, in which case we pop back to it - for (const view of activeViews) { - if (view.url === url) { - // cool, we found the match, pop back to that bad boy - return nav.popTo(view, null, escapeHatch); - } - } - - // it's the top level nav, and it's not one of those other behaviors, so do a push so the user gets a chill animation - return nav.push(component, data, { animate: isTopLevel }, escapeHatch); -} - - -export interface IsRedirectRequired { - required: boolean; - url?: string; -} - -export const POP = 'pop'; -export const PUSH = 'push'; -export const SET_ROOT = 'setRoot'; diff --git a/packages/core/src/components/nav/readme.md b/packages/core/src/components/nav/readme.md index 52aa34c278..e93d32fb64 100644 --- a/packages/core/src/components/nav/readme.md +++ b/packages/core/src/components/nav/readme.md @@ -7,115 +7,61 @@ ## Properties -#### delegate - - - - -#### lazy - -boolean - - -#### mode - -string - - #### root any -#### routerDelegate - - - - #### swipeBackEnabled boolean -#### useUrls - -boolean - - ## Attributes -#### delegate - - - - -#### lazy - -boolean - - -#### mode - -string - - #### root any -#### router-delegate - - - - #### swipe-back-enabled boolean -#### use-urls - -boolean - - ## Events #### ionNavChanged -#### navInit - - ## Methods -#### activateFromTab() - - #### canGoBack() -#### first() - - #### getActive() -#### getChildNavs() +#### getAllChildNavs() + + +#### getByIndex() #### getContentElement() -#### getId() - - #### getPrevious() #### getRouteId() +#### getViewById() + +Return a view controller + + #### getViews() @@ -125,18 +71,12 @@ boolean #### insertPages() -#### isTransitioning() - - -#### last() - - -#### onAllTransitionsComplete() - - #### pop() +#### popAll() + + #### popTo() @@ -146,9 +86,6 @@ boolean #### push() -#### reconcileFromExternalRouter() - - #### removeIndex() @@ -158,9 +95,6 @@ boolean #### setPages() -#### setParent() - - #### setRoot() diff --git a/packages/core/src/components/nav/test/basic/index.html b/packages/core/src/components/nav/test/basic/index.html index b9fdbce2bd..5437f4379f 100644 --- a/packages/core/src/components/nav/test/basic/index.html +++ b/packages/core/src/components/nav/test/basic/index.html @@ -24,7 +24,6 @@ `; } } - class PageTwo extends HTMLElement { connectedCallback() { this.innerHTML = ` @@ -44,12 +43,10 @@ Go to Page Two - `; } } - class PageThree extends HTMLElement { connectedCallback() { this.innerHTML = ` @@ -68,11 +65,9 @@ `; } } - customElements.define('page-one', PageOne); customElements.define('page-two', PageTwo); customElements.define('page-three', PageThree); - diff --git a/packages/core/src/components/nav/test/nav-controller.spec.ts b/packages/core/src/components/nav/test/nav-controller.spec.ts new file mode 100644 index 0000000000..e59566aa60 --- /dev/null +++ b/packages/core/src/components/nav/test/nav-controller.spec.ts @@ -0,0 +1,1139 @@ +import { mockDocument, mockElement } from '@stencil/core/testing'; +import { NavControllerBase } from '../nav'; +import { ViewController } from '../view-controller'; +import { AnimationControllerImpl } from '../../animation-controller/animation-controller'; +import { createConfigController } from '../../../global/config-controller'; + +import { + DIRECTION_BACK, + DIRECTION_FORWARD, + NavOptions, + STATE_INITIALIZED, +} from '../nav-util'; + + +describe('NavController', () => { + + describe('push and pop', () => { + + it('should push multiple times and pop multiple times', async () => { + const push1Done = jest.fn(); + const push2Done = jest.fn(); + const push3Done = jest.fn(); + const push4Done = jest.fn(); + const pop1Done = jest.fn(); + const pop2Done = jest.fn(); + const pop3Done = jest.fn(); + + // Push 1 + await nav.push(mockView(MockView1), null, {animate: false}, push1Done); + + const hasCompleted = true; + const requiresTransition = true; + expect(push1Done).toHaveBeenCalledWith( + hasCompleted, requiresTransition, 'mock-view1', undefined, DIRECTION_FORWARD + ); + expect(nav.length()).toEqual(1); + expect(nav.getByIndex(0).component).toEqual(MockView1); + + // Push 2 + await nav.push(mockView(MockView2), null, {animate: false}, push2Done); + + expect(push2Done).toHaveBeenCalledWith( + hasCompleted, requiresTransition, 'mock-view2', 'mock-view1', DIRECTION_FORWARD + ); + + expect(nav.length()).toEqual(2); + expect(nav.getByIndex(0).component).toEqual(MockView1); + expect(nav.getByIndex(1).component).toEqual(MockView2); + + // Push 3 + await nav.push(mockView(MockView3), null, {animate: false}, push3Done); + + expect(push3Done).toHaveBeenCalledWith( + hasCompleted, requiresTransition, 'mock-view3', 'mock-view2', DIRECTION_FORWARD + ); + expect(nav.length()).toEqual(3); + expect(nav.getByIndex(0).component).toEqual(MockView1); + expect(nav.getByIndex(1).component).toEqual(MockView2); + expect(nav.getByIndex(2).component).toEqual(MockView3); + + // Push 4 + await nav.push(mockView(MockView4), null, {animate: false}, push4Done); + expect(push4Done).toHaveBeenCalledWith( + hasCompleted, requiresTransition, 'mock-view4', 'mock-view3', DIRECTION_FORWARD + ); + expect(nav.length()).toEqual(4); + expect(nav.getByIndex(0).component).toEqual(MockView1); + expect(nav.getByIndex(1).component).toEqual(MockView2); + expect(nav.getByIndex(2).component).toEqual(MockView3); + expect(nav.getByIndex(3).component).toEqual(MockView4); + + // Pop 1 + await nav.pop({animate: false}, pop1Done); + expect(pop1Done).toHaveBeenCalledWith( + hasCompleted, requiresTransition, 'mock-view3', 'mock-view4', DIRECTION_BACK + ); + expect(nav.length()).toEqual(3); + expect(nav.getByIndex(0).component).toEqual(MockView1); + expect(nav.getByIndex(1).component).toEqual(MockView2); + expect(nav.getByIndex(2).component).toEqual(MockView3); + + // Pop 2 + await nav.pop({animate: false}, pop2Done); + expect(pop2Done).toHaveBeenCalledWith( + hasCompleted, requiresTransition, 'mock-view2', 'mock-view3', DIRECTION_BACK + ); + expect(nav.length()).toEqual(2); + expect(nav.getByIndex(0).component).toEqual(MockView1); + expect(nav.getByIndex(1).component).toEqual(MockView2); + + // Pop 3 + await nav.pop({animate: false}, pop3Done); + expect(pop3Done).toHaveBeenCalledWith( + hasCompleted, requiresTransition, 'mock-view1', 'mock-view2', DIRECTION_BACK + ); + expect(nav.length()).toEqual(1); + expect(nav.getByIndex(0).component).toEqual(MockView1); + + }, 10000); + }); + + describe('push', () => { + + it('should push a component as the first view', async () => { + + await nav.push(mockView(MockView1), null, null, trnsDone); + const hasCompleted = true; + const requiresTransition = true; + expect(trnsDone).toHaveBeenCalledWith( + hasCompleted, requiresTransition, 'mock-view1', undefined, DIRECTION_FORWARD + ); + expect(nav.length()).toEqual(1); + expect(nav.getByIndex(0).component).toEqual(MockView1); + expect(nav.isTransitioning).toEqual(false); + + + }, 10000); + + it('should push a component as the second view at the end', async () => { + mockViews(nav, [mockView(MockView1)]); + + await nav.push(mockView(MockView2), null, null, trnsDone); + + const hasCompleted = true; + const requiresTransition = true; + expect(trnsDone).toHaveBeenCalledWith( + hasCompleted, requiresTransition, 'mock-view2', 'mock-view1', DIRECTION_FORWARD + ); + expect(nav.length()).toEqual(2); + expect(nav.getByIndex(0).component).toEqual(MockView1); + expect(nav.getByIndex(1).component).toEqual(MockView2); + expect(nav.isTransitioning).toEqual(false); + + }, 10000); + + it('should push a ViewController as the second view and fire lifecycles', async () => { + const view1 = mockView(); + const view2 = mockView(); + + const instance1 = spyOnLifecycles(view1); + const instance2 = spyOnLifecycles(view2); + + mockViews(nav, [view1]); + + await nav.push(view2, null, null, trnsDone); + + expect(instance1.ionViewDidLoad).not.toHaveBeenCalled(); + // expect(instance1.ionViewCanEnter).not.toHaveBeenCalled(); + expect(instance1.ionViewWillEnter).not.toHaveBeenCalled(); + expect(instance1.ionViewDidEnter).not.toHaveBeenCalled(); + // expect(instance1.ionViewCanLeave).toHaveBeenCalled(); + expect(instance1.ionViewWillLeave).toHaveBeenCalled(); + expect(instance1.ionViewDidLeave).toHaveBeenCalled(); + expect(instance1.ionViewWillUnload).not.toHaveBeenCalled(); + + expect(instance2.ionViewDidLoad).toHaveBeenCalled(); + // expect(instance2.ionViewCanEnter).toHaveBeenCalled(); + expect(instance2.ionViewWillEnter).toHaveBeenCalled(); + expect(instance2.ionViewDidEnter).toHaveBeenCalled(); + // expect(instance2.ionViewCanLeave).not.toHaveBeenCalled(); + expect(instance2.ionViewWillLeave).not.toHaveBeenCalled(); + expect(instance2.ionViewDidLeave).not.toHaveBeenCalled(); + expect(instance2.ionViewWillUnload).not.toHaveBeenCalled(); + + const hasCompleted = true; + const requiresTransition = true; + expect(trnsDone).toHaveBeenCalledWith( + hasCompleted, requiresTransition, 'mock-view', 'mock-view', DIRECTION_FORWARD + ); + expect(nav.length()).toEqual(2); + + + }, 10000); + }); + + describe('insert', () => { + + it('should not modify the view id', async () => { + const view = mockView(MockView4); + view.id = 'custom_id'; + await nav.insert(0, view); + + expect(view.id).toEqual('custom_id'); + expect(view.id).toEqual('custom_id'); + }, 10000); + + + it('should insert at the begining with no async transition', async () => { + const view4 = mockView(MockView4); + const instance4 = spyOnLifecycles(view4); + const opts: NavOptions = {}; + + mockViews(nav, [mockView(MockView1), mockView(MockView2), mockView(MockView3)]); + + await nav.insert(0, view4, null, opts, trnsDone); + expect(instance4.ionViewDidLoad).not.toHaveBeenCalled(); + // expect(instance4.ionViewCanEnter).not.toHaveBeenCalled(); + expect(instance4.ionViewWillEnter).not.toHaveBeenCalled(); + expect(instance4.ionViewDidEnter).not.toHaveBeenCalled(); + // expect(instance4.ionViewCanLeave).not.toHaveBeenCalled(); + expect(instance4.ionViewWillLeave).not.toHaveBeenCalled(); + expect(instance4.ionViewDidLeave).not.toHaveBeenCalled(); + expect(instance4.ionViewWillUnload).not.toHaveBeenCalled(); + + const hasCompleted = true; + const requiresTransition = false; + expect(trnsDone).toHaveBeenCalledWith( + hasCompleted, requiresTransition, undefined, undefined, undefined + ); + expect(nav.length()).toEqual(4); + expect(nav._views[0].component).toEqual(MockView4); + expect(nav._views[nav._views.length - 1].component).toEqual(MockView3); + + }, 10000); + + it('should insert at the end when given -1', async () => { + const opts: NavOptions = {}; + mockViews(nav, [mockView(MockView1)]); + + await nav.insert(-1, mockView(MockView2), null, opts, trnsDone); + const hasCompleted = true; + const requiresTransition = true; + expect(trnsDone).toHaveBeenCalledWith( + hasCompleted, requiresTransition, 'mock-view2', 'mock-view1', DIRECTION_FORWARD + ); + expect(nav.length()).toEqual(2); + expect(nav._views[nav._views.length - 1].component).toEqual(MockView2); + + }, 10000); + + it('should insert at the end when given a number greater than actual length', async () => { + mockViews(nav, [mockView(MockView1)]); + + await nav.insert(9999, mockView(MockView2), null, null, trnsDone); + const hasCompleted = true; + const requiresTransition = true; + expect(trnsDone).toHaveBeenCalledWith( + hasCompleted, requiresTransition, 'mock-view2', 'mock-view1', DIRECTION_FORWARD + ); + expect(nav.length()).toEqual(2); + expect(nav._views[nav._views.length - 1].component).toEqual(MockView2); + + }, 10000); + + it('should not insert if null view', (done) => { + mockViews(nav, [mockView(MockView1)]); + + nav.insert(-1, null, null, null, trnsDone).then(() => { + fail('it should not succeed'); + done(); + + }).catch((err: Error) => { + const hasCompleted = false; + const requiresTransition = false; + const rejectReason = new Error('invalid views to insert'); + expect(err).toEqual(rejectReason); + expect(trnsDone).toHaveBeenCalledWith(hasCompleted, requiresTransition, rejectReason); + expect(nav.length()).toEqual(1); + expect(nav._views[nav._views.length - 1].component).toEqual(MockView1); + done(); + }); + }, 10000); + + // it('should not insert any view in the stack if canLeave returns false', async () => { + // const view1 = mockView(MockView1); + // const view2 = mockView(MockView2); + // const view3 = mockView(MockView3); + // mockViews(nav, [view1, view2]); + + // const instance2 = spyOnLifecycles(view2); + + // let count = 0; + // instance2.ionViewCanLeave = function () { + // count++; + // return (count === 3); + // }; + + // await nav.push(view3); + // expect(nav.length()).toEqual(2); + // await nav.push(view3); + // expect(nav.length()).toEqual(2); + // await nav.push(view3); + // expect(nav.length()).toEqual(3); + + // }, 10000); + + // it('should not remove any view from the stack if canLeave returns false', async () => { + // const view1 = mockView(MockView1); + // const view2 = mockView(MockView2); + // mockViews(nav, [view1, view2]); + + // const instance2 = spyOnLifecycles(view2); + + // let count = 0; + // instance2.ionViewCanLeave = function () { + // count++; + // return (count === 3); + // }; + + // await nav.pop(); + // expect(nav.length()).toEqual(2); + // await nav.pop(); + // expect(nav.length()).toEqual(2); + // await nav.pop(); + // expect(nav.length()).toEqual(1); + // }, 10000); + + }); + + describe('insertPages', () => { + + it('should insert all pages in the middle', async () => { + const view4 = mockView(MockView4); + const instance4 = spyOnLifecycles(view4); + mockViews(nav, [mockView(MockView1), mockView(MockView2), mockView(MockView3)]); + + await nav.insertPages(1, [view4, mockView(MockView5)], null, trnsDone); + expect(instance4.ionViewDidLoad).not.toHaveBeenCalled(); + // expect(instance4.ionViewCanEnter).not.toHaveBeenCalled(); + expect(instance4.ionViewWillEnter).not.toHaveBeenCalled(); + expect(instance4.ionViewDidEnter).not.toHaveBeenCalled(); + // expect(instance4.ionViewCanLeave).not.toHaveBeenCalled(); + expect(instance4.ionViewWillLeave).not.toHaveBeenCalled(); + expect(instance4.ionViewDidLeave).not.toHaveBeenCalled(); + expect(instance4.ionViewWillUnload).not.toHaveBeenCalled(); + + const hasCompleted = true; + const requiresTransition = false; + expect(trnsDone).toHaveBeenCalledWith( + hasCompleted, requiresTransition, undefined, undefined, undefined + ); + expect(nav.length()).toEqual(5); + expect(nav.getByIndex(0).component).toEqual(MockView1); + expect(nav.getByIndex(1).component).toEqual(MockView4); + expect(nav.getByIndex(2).component).toEqual(MockView5); + expect(nav.getByIndex(3).component).toEqual(MockView2); + expect(nav.getByIndex(4).component).toEqual(MockView3); + + expect(nav.getByIndex(1)._nav).toEqual(nav); + expect(nav.getByIndex(2)._nav).toEqual(nav); + + }, 10000); + }); + + describe('pop', () => { + + it('should not pop when no views in the stack', (done) => { + nav.pop(null, trnsDone).then(() => { + fail('it should not succeed'); + done(); + }).catch((err) => { + const hasCompleted = false; + const requiresTransition = false; + const rejectReason = new Error('no views in the stack to be removed'); + expect(trnsDone).toHaveBeenCalledWith( + hasCompleted, requiresTransition, rejectReason + ); + expect(err).toEqual(rejectReason); + expect(nav.length()).toEqual(0); + expect(nav.isTransitioning).toEqual(false); + done(); + }); + }, 10000); + + it('should remove the last view and fire lifecycles', async () => { + const view1 = mockView(MockView1); + const view2 = mockView(MockView2); + mockViews(nav, [view1, view2]); + const instance1 = spyOnLifecycles(view1); + const instance2 = spyOnLifecycles(view2); + + await nav.pop(null, trnsDone); + + expect(instance1.ionViewDidLoad).toHaveBeenCalled(); + // expect(instance1.ionViewCanEnter).toHaveBeenCalled(); + expect(instance1.ionViewWillEnter).toHaveBeenCalled(); + expect(instance1.ionViewDidEnter).toHaveBeenCalled(); + // expect(instance1.ionViewCanLeave).not.toHaveBeenCalled(); + expect(instance1.ionViewWillLeave).not.toHaveBeenCalled(); + expect(instance1.ionViewDidLeave).not.toHaveBeenCalled(); + expect(instance1.ionViewWillUnload).not.toHaveBeenCalled(); + + expect(instance2.ionViewDidLoad).not.toHaveBeenCalled(); + // expect).not.toHaveBeenCalled(); + expect(instance2.ionViewWillEnter).not.toHaveBeenCalled(); + expect(instance2.ionViewDidEnter).not.toHaveBeenCalled(); + // expect(instance2.ionViewCanLeave).toHaveBeenCalled(); + expect(instance2.ionViewWillLeave).toHaveBeenCalled(); + expect(instance2.ionViewDidLeave).toHaveBeenCalled(); + expect(instance2.ionViewWillUnload).toHaveBeenCalled(); + + const hasCompleted = true; + const requiresTransition = true; + expect(trnsDone).toHaveBeenCalledWith( + hasCompleted, requiresTransition, 'mock-view1', 'mock-view2', DIRECTION_BACK + ); + expect(nav.length()).toEqual(1); + expect(nav.getByIndex(0).component).toEqual(MockView1); + expect(nav.isTransitioning).toEqual(false); + + + }, 10000); + + }); + + describe('popTo', () => { + + it('should pop to a view', async () => { + const view1 = mockView(MockView1); + const view2 = mockView(MockView2); + const view3 = mockView(MockView3); + mockViews(nav, [view1, view2, view3]); + + await nav.popTo(view2, null, trnsDone); + + const hasCompleted = true; + const requiresTransition = true; + expect(trnsDone).toHaveBeenCalledWith( + hasCompleted, requiresTransition, 'mock-view2', 'mock-view3', DIRECTION_BACK + ); + expect(nav.length()).toEqual(2); + expect(nav.getByIndex(0).component).toEqual(MockView1); + expect(nav.getByIndex(1).component).toEqual(MockView2); + + + }, 10000); + + it('should pop to using an index number', async () => { + const view1 = mockView(MockView1); + const view2 = mockView(MockView2); + const view3 = mockView(MockView3); + const view4 = mockView(MockView4); + mockViews(nav, [view1, view2, view3, view4]); + + await nav.popTo(1, null, trnsDone); + + const hasCompleted = true; + const requiresTransition = true; + expect(trnsDone).toHaveBeenCalledWith( + hasCompleted, requiresTransition, 'mock-view2', 'mock-view4', DIRECTION_BACK + ); + expect(nav.length()).toEqual(2); + expect(nav.getByIndex(0).component).toEqual(MockView1); + expect(nav.getByIndex(1).component).toEqual(MockView2); + + + }, 10000); + + it('should pop to first using an index number', async () => { + const view1 = mockView(MockView1); + const view2 = mockView(MockView2); + const view3 = mockView(MockView3); + const view4 = mockView(MockView4); + mockViews(nav, [view1, view2, view3, view4]); + + const instance1 = spyOnLifecycles(view1); + const instance2 = spyOnLifecycles(view2); + const instance3 = spyOnLifecycles(view3); + const instance4 = spyOnLifecycles(view4); + + await nav.popTo(0, null, trnsDone); + + expect(instance1.ionViewDidLoad).toHaveBeenCalled(); + // expect(instance1.ionViewCanEnter).toHaveBeenCalled(); + expect(instance1.ionViewWillEnter).toHaveBeenCalled(); + expect(instance1.ionViewDidEnter).toHaveBeenCalled(); + // expect(instance1.ionViewCanLeave).not.toHaveBeenCalled(); + expect(instance1.ionViewWillLeave).not.toHaveBeenCalled(); + expect(instance1.ionViewDidLeave).not.toHaveBeenCalled(); + expect(instance1.ionViewWillUnload).not.toHaveBeenCalled(); + + expect(instance2.ionViewDidLoad).not.toHaveBeenCalled(); + // expect(instance2.ionViewCanEnter).not.toHaveBeenCalled(); + expect(instance2.ionViewWillEnter).not.toHaveBeenCalled(); + expect(instance2.ionViewDidEnter).not.toHaveBeenCalled(); + // expect(instance2.ionViewCanLeave).not.toHaveBeenCalled(); + expect(instance2.ionViewWillLeave).toHaveBeenCalled(); + expect(instance2.ionViewDidLeave).toHaveBeenCalled(); + expect(instance2.ionViewWillUnload).toHaveBeenCalled(); + + expect(instance3.ionViewDidLoad).not.toHaveBeenCalled(); + // expect(instance3.ionViewCanEnter).not.toHaveBeenCalled(); + expect(instance3.ionViewWillEnter).not.toHaveBeenCalled(); + expect(instance3.ionViewDidEnter).not.toHaveBeenCalled(); + // expect(instance3.ionViewCanLeave).not.toHaveBeenCalled(); + expect(instance3.ionViewWillLeave).toHaveBeenCalled(); + expect(instance3.ionViewDidLeave).toHaveBeenCalled(); + expect(instance3.ionViewWillUnload).toHaveBeenCalled(); + + expect(instance4.ionViewDidLoad).not.toHaveBeenCalled(); + // expect(instance4.ionViewCanEnter).not.toHaveBeenCalled(); + expect(instance4.ionViewWillEnter).not.toHaveBeenCalled(); + expect(instance4.ionViewDidEnter).not.toHaveBeenCalled(); + // expect(instance4.ionViewCanLeave).toHaveBeenCalled(); + expect(instance4.ionViewWillLeave).toHaveBeenCalled(); + expect(instance4.ionViewDidLeave).toHaveBeenCalled(); + expect(instance4.ionViewWillUnload).toHaveBeenCalled(); + + const hasCompleted = true; + const requiresTransition = true; + expect(trnsDone).toHaveBeenCalledWith( + hasCompleted, requiresTransition, 'mock-view1', 'mock-view4', DIRECTION_BACK + ); + expect(nav.length()).toEqual(1); + expect(nav.getByIndex(0).component).toEqual(MockView1); + + }, 10000); + + }); + + describe('popToRoot', () => { + + it('should pop to the first view', async () => { + const view1 = mockView(MockView1); + const view2 = mockView(MockView2); + const view3 = mockView(MockView3); + const view4 = mockView(MockView4); + mockViews(nav, [view1, view2, view3, view4]); + + const instance1 = spyOnLifecycles(view1); + const instance2 = spyOnLifecycles(view2); + const instance3 = spyOnLifecycles(view3); + const instance4 = spyOnLifecycles(view4); + + await nav.popToRoot(null, trnsDone); + + expect(instance1.ionViewDidLoad).toHaveBeenCalled(); + // expect(instance1.ionViewCanEnter).toHaveBeenCalled(); + expect(instance1.ionViewWillEnter).toHaveBeenCalled(); + expect(instance1.ionViewDidEnter).toHaveBeenCalled(); + // expect(instance1.ionViewCanLeave).not.toHaveBeenCalled(); + expect(instance1.ionViewWillLeave).not.toHaveBeenCalled(); + expect(instance1.ionViewDidLeave).not.toHaveBeenCalled(); + expect(instance1.ionViewWillUnload).not.toHaveBeenCalled(); + + expect(instance2.ionViewDidLoad).not.toHaveBeenCalled(); + // expect(instance2.ionViewCanEnter).not.toHaveBeenCalled(); + expect(instance2.ionViewWillEnter).not.toHaveBeenCalled(); + expect(instance2.ionViewDidEnter).not.toHaveBeenCalled(); + // expect(instance2.ionViewCanLeave).not.toHaveBeenCalled(); + expect(instance2.ionViewWillLeave).toHaveBeenCalled(); + expect(instance2.ionViewDidLeave).toHaveBeenCalled(); + expect(instance2.ionViewWillUnload).toHaveBeenCalled(); + + expect(instance3.ionViewDidLoad).not.toHaveBeenCalled(); + // expect(instance3.ionViewCanEnter).not.toHaveBeenCalled(); + expect(instance3.ionViewWillEnter).not.toHaveBeenCalled(); + expect(instance3.ionViewDidEnter).not.toHaveBeenCalled(); + // expect(instance3.ionViewCanLeave).not.toHaveBeenCalled(); + expect(instance3.ionViewWillLeave).toHaveBeenCalled(); + expect(instance3.ionViewDidLeave).toHaveBeenCalled(); + expect(instance3.ionViewWillUnload).toHaveBeenCalled(); + + expect(instance4.ionViewDidLoad).not.toHaveBeenCalled(); + // expect(instance4.ionViewCanEnter).not.toHaveBeenCalled(); + expect(instance4.ionViewWillEnter).not.toHaveBeenCalled(); + expect(instance4.ionViewDidEnter).not.toHaveBeenCalled(); + // expect(instance4.ionViewCanLeave).toHaveBeenCalled(); + expect(instance4.ionViewWillLeave).toHaveBeenCalled(); + expect(instance4.ionViewDidLeave).toHaveBeenCalled(); + expect(instance4.ionViewWillUnload).toHaveBeenCalled(); + + const hasCompleted = true; + const requiresTransition = true; + expect(trnsDone).toHaveBeenCalledWith( + hasCompleted, requiresTransition, 'mock-view1', 'mock-view4', DIRECTION_BACK + ); + expect(nav.length()).toEqual(1); + expect(nav.getByIndex(0).component).toEqual(MockView1); + + + }, 10000); + + it('should not pop first view if it\'s the only view', async () => { + const view1 = mockView(MockView1); + mockViews(nav, [view1]); + + await nav.popToRoot(null, trnsDone); + const hasCompleted = true; + const requiresTransition = false; + expect(trnsDone).toHaveBeenCalledWith( + hasCompleted, requiresTransition, undefined, undefined, undefined + ); + expect(nav.length()).toEqual(1); + expect(nav.getByIndex(0).component).toEqual(MockView1); + + + }, 10000); + + }); + + describe('remove', () => { + + it('should remove the first three views in the beginning, no last view transition', async () => { + const view1 = mockView(MockView1); + const view2 = mockView(MockView2); + const view3 = mockView(MockView3); + const view4 = mockView(MockView4); + mockViews(nav, [view1, view2, view3, view4]); + + const instance1 = spyOnLifecycles(view1); + const instance2 = spyOnLifecycles(view2); + const instance3 = spyOnLifecycles(view3); + const instance4 = spyOnLifecycles(view4); + + await nav.removeIndex(0, 3, null, trnsDone); + + expect(instance1.ionViewDidLoad).not.toHaveBeenCalled(); + // expect(instance1.ionViewCanEnter).not.toHaveBeenCalled(); + expect(instance1.ionViewWillEnter).not.toHaveBeenCalled(); + expect(instance1.ionViewDidEnter).not.toHaveBeenCalled(); + // expect(instance1.ionViewCanLeave).not.toHaveBeenCalled(); + expect(instance1.ionViewWillLeave).toHaveBeenCalled(); + expect(instance1.ionViewDidLeave).toHaveBeenCalled(); + expect(instance1.ionViewWillUnload).toHaveBeenCalled(); + + expect(instance2.ionViewDidLoad).not.toHaveBeenCalled(); + // expect(instance2.ionViewCanEnter).not.toHaveBeenCalled(); + expect(instance2.ionViewWillEnter).not.toHaveBeenCalled(); + expect(instance2.ionViewDidEnter).not.toHaveBeenCalled(); + // expect(instance2.ionViewCanLeave).not.toHaveBeenCalled(); + expect(instance2.ionViewWillLeave).toHaveBeenCalled(); + expect(instance2.ionViewDidLeave).toHaveBeenCalled(); + expect(instance2.ionViewWillUnload).toHaveBeenCalled(); + + expect(instance3.ionViewDidLoad).not.toHaveBeenCalled(); + // expect(instance3.ionViewCanEnter).not.toHaveBeenCalled(); + expect(instance3.ionViewWillEnter).not.toHaveBeenCalled(); + expect(instance3.ionViewDidEnter).not.toHaveBeenCalled(); + // expect(instance3.ionViewCanLeave).not.toHaveBeenCalled(); + expect(instance3.ionViewWillLeave).toHaveBeenCalled(); + expect(instance3.ionViewDidLeave).toHaveBeenCalled(); + expect(instance3.ionViewWillUnload).toHaveBeenCalled(); + + expect(instance4.ionViewDidLoad).not.toHaveBeenCalled(); + // expect(instance4.ionViewCanEnter).not.toHaveBeenCalled(); + expect(instance4.ionViewWillEnter).not.toHaveBeenCalled(); + expect(instance4.ionViewDidEnter).not.toHaveBeenCalled(); + // expect(instance4.ionViewCanLeave).not.toHaveBeenCalled(); + expect(instance4.ionViewWillLeave).not.toHaveBeenCalled(); + expect(instance4.ionViewDidLeave).not.toHaveBeenCalled(); + expect(instance4.ionViewWillUnload).not.toHaveBeenCalled(); + + const hasCompleted = true; + const requiresTransition = false; + expect(trnsDone).toHaveBeenCalledWith( + hasCompleted, requiresTransition, undefined, undefined, undefined + ); + expect(nav.length()).toEqual(1); + expect(nav.getByIndex(0).component).toEqual(MockView4); + + + }, 10000); + + it('should remove two views in the middle', async () => { + const view1 = mockView(MockView1); + const view2 = mockView(MockView2); + const view3 = mockView(MockView3); + const view4 = mockView(MockView4); + const view5 = mockView(MockView5); + mockViews(nav, [view1, view2, view3, view4, view5]); + + const instance1 = spyOnLifecycles(view1); + const instance2 = spyOnLifecycles(view2); + const instance3 = spyOnLifecycles(view3); + const instance4 = spyOnLifecycles(view4); + const instance5 = spyOnLifecycles(view5); + + await nav.removeIndex(2, 2, null, trnsDone); + + expect(instance1.ionViewDidLoad).not.toHaveBeenCalled(); + // expect(instance1.ionViewCanEnter).not.toHaveBeenCalled(); + expect(instance1.ionViewWillEnter).not.toHaveBeenCalled(); + expect(instance1.ionViewDidEnter).not.toHaveBeenCalled(); + // expect(instance1.ionViewCanLeave).not.toHaveBeenCalled(); + expect(instance1.ionViewWillLeave).not.toHaveBeenCalled(); + expect(instance1.ionViewDidLeave).not.toHaveBeenCalled(); + expect(instance1.ionViewWillUnload).not.toHaveBeenCalled(); + + expect(instance2.ionViewDidLoad).not.toHaveBeenCalled(); + // expect(instance2.ionViewCanEnter).not.toHaveBeenCalled(); + expect(instance2.ionViewWillEnter).not.toHaveBeenCalled(); + expect(instance2.ionViewDidEnter).not.toHaveBeenCalled(); + // expect(instance2.ionViewCanLeave).not.toHaveBeenCalled(); + expect(instance2.ionViewWillLeave).not.toHaveBeenCalled(); + expect(instance2.ionViewDidLeave).not.toHaveBeenCalled(); + expect(instance2.ionViewWillUnload).not.toHaveBeenCalled(); + + expect(instance3.ionViewDidLoad).not.toHaveBeenCalled(); + // expect(instance3.ionViewCanEnter).not.toHaveBeenCalled(); + expect(instance3.ionViewWillEnter).not.toHaveBeenCalled(); + expect(instance3.ionViewDidEnter).not.toHaveBeenCalled(); + // expect(instance3.ionViewCanLeave).not.toHaveBeenCalled(); + expect(instance3.ionViewWillLeave).toHaveBeenCalled(); + expect(instance3.ionViewDidLeave).toHaveBeenCalled(); + expect(instance3.ionViewWillUnload).toHaveBeenCalled(); + + expect(instance4.ionViewDidLoad).not.toHaveBeenCalled(); + // expect(instance4.ionViewCanEnter).not.toHaveBeenCalled(); + expect(instance4.ionViewWillEnter).not.toHaveBeenCalled(); + expect(instance4.ionViewDidEnter).not.toHaveBeenCalled(); + // expect(instance4.ionViewCanLeave).not.toHaveBeenCalled(); + expect(instance4.ionViewWillLeave).toHaveBeenCalled(); + expect(instance4.ionViewDidLeave).toHaveBeenCalled(); + expect(instance4.ionViewWillUnload).toHaveBeenCalled(); + + expect(instance5.ionViewDidLoad).not.toHaveBeenCalled(); + // expect(instance5.ionViewCanEnter).not.toHaveBeenCalled(); + expect(instance5.ionViewWillEnter).not.toHaveBeenCalled(); + expect(instance5.ionViewDidEnter).not.toHaveBeenCalled(); + // expect(instance5.ionViewCanLeave).not.toHaveBeenCalled(); + expect(instance5.ionViewWillLeave).not.toHaveBeenCalled(); + expect(instance5.ionViewDidLeave).not.toHaveBeenCalled(); + expect(instance5.ionViewWillUnload).not.toHaveBeenCalled(); + + const hasCompleted = true; + const requiresTransition = false; + expect(trnsDone).toHaveBeenCalledWith( + hasCompleted, requiresTransition, undefined, undefined, undefined + ); + expect(nav.length()).toEqual(3); + expect(nav.getByIndex(0).component).toEqual(MockView1); + expect(nav.getByIndex(1).component).toEqual(MockView2); + expect(nav.getByIndex(2).component).toEqual(MockView5); + + + }, 10000); + + it('should remove the last two views at the end', async () => { + const view1 = mockView(MockView1); + const view2 = mockView(MockView2); + const view3 = mockView(MockView3); + const view4 = mockView(MockView4); + mockViews(nav, [view1, view2, view3, view4]); + + const instance1 = spyOnLifecycles(view1); + const instance2 = spyOnLifecycles(view2); + const instance3 = spyOnLifecycles(view3); + const instance4 = spyOnLifecycles(view4); + + await nav.removeIndex(2, 2, null, trnsDone); + + expect(instance1.ionViewDidLoad).not.toHaveBeenCalled(); + // expect(instance1.ionViewCanEnter).not.toHaveBeenCalled(); + expect(instance1.ionViewWillEnter).not.toHaveBeenCalled(); + expect(instance1.ionViewDidEnter).not.toHaveBeenCalled(); + // expect(instance1.ionViewCanLeave).not.toHaveBeenCalled(); + expect(instance1.ionViewWillLeave).not.toHaveBeenCalled(); + expect(instance1.ionViewDidLeave).not.toHaveBeenCalled(); + expect(instance1.ionViewWillUnload).not.toHaveBeenCalled(); + + expect(instance2.ionViewDidLoad).toHaveBeenCalled(); + // expect(instance2.ionViewCanEnter).toHaveBeenCalled(); + expect(instance2.ionViewWillEnter).toHaveBeenCalled(); + expect(instance2.ionViewDidEnter).toHaveBeenCalled(); + // expect(instance2.ionViewCanLeave).not.toHaveBeenCalled(); + expect(instance2.ionViewWillLeave).not.toHaveBeenCalled(); + expect(instance2.ionViewDidLeave).not.toHaveBeenCalled(); + expect(instance2.ionViewWillUnload).not.toHaveBeenCalled(); + + expect(instance3.ionViewDidLoad).not.toHaveBeenCalled(); + // expect(instance3.ionViewCanEnter).not.toHaveBeenCalled(); + expect(instance3.ionViewWillEnter).not.toHaveBeenCalled(); + expect(instance3.ionViewDidEnter).not.toHaveBeenCalled(); + // expect(instance3.ionViewCanLeave).not.toHaveBeenCalled(); + expect(instance3.ionViewWillLeave).toHaveBeenCalled(); + expect(instance3.ionViewDidLeave).toHaveBeenCalled(); + expect(instance3.ionViewWillUnload).toHaveBeenCalled(); + + expect(instance4.ionViewDidLoad).not.toHaveBeenCalled(); + // expect(instance4.ionViewCanEnter).not.toHaveBeenCalled(); + expect(instance4.ionViewWillEnter).not.toHaveBeenCalled(); + expect(instance4.ionViewDidEnter).not.toHaveBeenCalled(); + // expect(instance4.ionViewCanLeave).toHaveBeenCalled(); + expect(instance4.ionViewWillLeave).toHaveBeenCalled(); + expect(instance4.ionViewDidLeave).toHaveBeenCalled(); + expect(instance4.ionViewWillUnload).toHaveBeenCalled(); + + const hasCompleted = true; + const requiresTransition = true; + expect(trnsDone).toHaveBeenCalledWith( + hasCompleted, requiresTransition, 'mock-view2', 'mock-view4', DIRECTION_BACK + ); + expect(nav.length()).toEqual(2); + expect(nav.getByIndex(0).component).toEqual(MockView1); + expect(nav.getByIndex(1).component).toEqual(MockView2); + + + }, 10000); + + }); + + describe('setRoot', () => { + + it('should set a ViewController as the root when its the last view, no transition', async () => { + const view1 = mockView(MockView1); + const view2 = mockView(MockView2); + const view3 = mockView(MockView3); + mockViews(nav, [view1, view2, view3]); + + const instance1 = spyOnLifecycles(view1); + const instance2 = spyOnLifecycles(view2); + const instance3 = spyOnLifecycles(view3); + + await nav.setRoot(view3, null, null, trnsDone); + expect(instance1.ionViewDidLoad).not.toHaveBeenCalled(); + // expect(instance1.ionViewCanEnter).not.toHaveBeenCalled(); + expect(instance1.ionViewWillEnter).not.toHaveBeenCalled(); + expect(instance1.ionViewDidEnter).not.toHaveBeenCalled(); + // expect(instance1.ionViewCanLeave).not.toHaveBeenCalled(); + expect(instance1.ionViewWillLeave).toHaveBeenCalled(); + expect(instance1.ionViewDidLeave).toHaveBeenCalled(); + expect(instance1.ionViewWillUnload).toHaveBeenCalled(); + + expect(instance2.ionViewDidLoad).not.toHaveBeenCalled(); + // expect(instance2.ionViewCanEnter).not.toHaveBeenCalled(); + expect(instance2.ionViewWillEnter).not.toHaveBeenCalled(); + expect(instance2.ionViewDidEnter).not.toHaveBeenCalled(); + // expect(instance2.ionViewCanLeave).not.toHaveBeenCalled(); + expect(instance2.ionViewWillLeave).toHaveBeenCalled(); + expect(instance2.ionViewDidLeave).toHaveBeenCalled(); + expect(instance2.ionViewWillUnload).toHaveBeenCalled(); + + expect(instance3.ionViewDidLoad).not.toHaveBeenCalled(); + // expect(instance3.ionViewCanEnter).not.toHaveBeenCalled(); + expect(instance3.ionViewWillEnter).not.toHaveBeenCalled(); + expect(instance3.ionViewDidEnter).not.toHaveBeenCalled(); + // expect(instance3.ionViewCanLeave).not.toHaveBeenCalled(); + expect(instance3.ionViewWillLeave).not.toHaveBeenCalled(); + expect(instance3.ionViewDidLeave).not.toHaveBeenCalled(); + expect(instance3.ionViewWillUnload).not.toHaveBeenCalled(); + + const hasCompleted = true; + const requiresTransition = false; + expect(trnsDone).toHaveBeenCalledWith( + hasCompleted, requiresTransition, undefined, undefined, undefined + ); + expect(nav.length()).toEqual(1); + expect(nav.getByIndex(0).component).toEqual(MockView3); + + + }, 10000); + + it('should set a ViewController as the root when its the middle view, with transition', async () => { + const view1 = mockView(MockView1); + const view2 = mockView(MockView2); + const view3 = mockView(MockView3); + mockViews(nav, [view1, view2, view3]); + + const instance1 = spyOnLifecycles(view1); + const instance2 = spyOnLifecycles(view2); + const instance3 = spyOnLifecycles(view3); + + await nav.setRoot(view2, null, null, trnsDone); + expect(instance1.ionViewDidLoad).not.toHaveBeenCalled(); + // expect(instance1.ionViewCanEnter).not.toHaveBeenCalled(); + expect(instance1.ionViewWillEnter).not.toHaveBeenCalled(); + expect(instance1.ionViewDidEnter).not.toHaveBeenCalled(); + // expect(instance1.ionViewCanLeave).not.toHaveBeenCalled(); + expect(instance1.ionViewWillLeave).toHaveBeenCalled(); + expect(instance1.ionViewDidLeave).toHaveBeenCalled(); + expect(instance1.ionViewWillUnload).toHaveBeenCalled(); + + expect(instance2.ionViewDidLoad).toHaveBeenCalled(); + // expect(instance2.ionViewCanEnter).toHaveBeenCalled(); + expect(instance2.ionViewWillEnter).toHaveBeenCalled(); + expect(instance2.ionViewDidEnter).toHaveBeenCalled(); + // expect(instance2.ionViewCanLeave).not.toHaveBeenCalled(); + expect(instance2.ionViewWillLeave).not.toHaveBeenCalled(); + expect(instance2.ionViewDidLeave).not.toHaveBeenCalled(); + expect(instance2.ionViewWillUnload).not.toHaveBeenCalled(); + + expect(instance3.ionViewDidLoad).not.toHaveBeenCalled(); + // expect(instance3.ionViewCanEnter).not.toHaveBeenCalled(); + expect(instance3.ionViewWillEnter).not.toHaveBeenCalled(); + expect(instance3.ionViewDidEnter).not.toHaveBeenCalled(); + // expect(instance3.ionViewCanLeave).toHaveBeenCalled(); + expect(instance3.ionViewWillLeave).toHaveBeenCalled(); + expect(instance3.ionViewDidLeave).toHaveBeenCalled(); + expect(instance3.ionViewWillUnload).toHaveBeenCalled(); + + const hasCompleted = true; + const requiresTransition = true; + expect(trnsDone).toHaveBeenCalledWith( + hasCompleted, requiresTransition, 'mock-view2', 'mock-view3', DIRECTION_BACK + ); + expect(nav.length()).toEqual(1); + expect(nav.getByIndex(0).component).toEqual(MockView2); + + }, 10000); + + it('should set a ViewController as the root when its the first view, with transition', async () => { + const view1 = mockView(MockView1); + const view2 = mockView(MockView2); + const view3 = mockView(MockView3); + mockViews(nav, [view1, view2, view3]); + + const instance1 = spyOnLifecycles(view1); + const instance2 = spyOnLifecycles(view2); + const instance3 = spyOnLifecycles(view3); + + await nav.setRoot(view1, null, null, trnsDone); + expect(instance1.ionViewDidLoad).toHaveBeenCalled(); + // expect(instance1.ionViewCanEnter).toHaveBeenCalled(); + expect(instance1.ionViewWillEnter).toHaveBeenCalled(); + expect(instance1.ionViewDidEnter).toHaveBeenCalled(); + // expect(instance1.ionViewCanLeave).not.toHaveBeenCalled(); + expect(instance1.ionViewWillLeave).not.toHaveBeenCalled(); + expect(instance1.ionViewDidLeave).not.toHaveBeenCalled(); + expect(instance1.ionViewWillUnload).not.toHaveBeenCalled(); + + expect(instance2.ionViewDidLoad).not.toHaveBeenCalled(); + // expect(instance2.ionViewCanEnter).not.toHaveBeenCalled(); + expect(instance2.ionViewWillEnter).not.toHaveBeenCalled(); + expect(instance2.ionViewDidEnter).not.toHaveBeenCalled(); + // expect(instance2.ionViewCanLeave).not.toHaveBeenCalled(); + expect(instance2.ionViewWillLeave).toHaveBeenCalled(); + expect(instance2.ionViewDidLeave).toHaveBeenCalled(); + expect(instance2.ionViewWillUnload).toHaveBeenCalled(); + + expect(instance3.ionViewDidLoad).not.toHaveBeenCalled(); + // expect(instance3.ionViewCanEnter).not.toHaveBeenCalled(); + expect(instance3.ionViewWillEnter).not.toHaveBeenCalled(); + expect(instance3.ionViewDidEnter).not.toHaveBeenCalled(); + // expect(instance3.ionViewCanLeave).toHaveBeenCalled(); + expect(instance3.ionViewWillLeave).toHaveBeenCalled(); + expect(instance3.ionViewDidLeave).toHaveBeenCalled(); + expect(instance3.ionViewWillUnload).toHaveBeenCalled(); + + const hasCompleted = true; + const requiresTransition = true; + expect(trnsDone).toHaveBeenCalledWith( + hasCompleted, requiresTransition, 'mock-view1', 'mock-view3', DIRECTION_BACK + ); + expect(nav.length()).toEqual(1); + expect(nav.getByIndex(0).component).toEqual(MockView1); + + + + }, 10000); + + it('should set a page component as the root, with transition', async () => { + const view1 = mockView(MockView1); + const view2 = mockView(MockView2); + const view3 = mockView(MockView3); + mockViews(nav, [view1, view2, view3]); + + const instance1 = spyOnLifecycles(view1); + const instance2 = spyOnLifecycles(view2); + const instance3 = spyOnLifecycles(view3); + + await nav.setRoot(mockView(MockView4), null, null, trnsDone); + expect(instance1.ionViewWillUnload).toHaveBeenCalled(); + expect(instance2.ionViewWillUnload).toHaveBeenCalled(); + expect(instance3.ionViewWillUnload).toHaveBeenCalled(); + + const hasCompleted = true; + const requiresTransition = true; + expect(trnsDone).toHaveBeenCalledWith( + hasCompleted, requiresTransition, 'mock-view4', 'mock-view3', DIRECTION_BACK + ); + expect(nav.length()).toEqual(1); + expect(nav.getByIndex(0).component).toEqual(MockView4); + + + }, 10000); + }); + + describe('setPages', () => { + + it('should set the pages from an array, starting at the root, with transition', async () => { + const view1 = mockView(MockView1); + const view2 = mockView(MockView2); + mockViews(nav, [view1, view2]); + + const instance1 = spyOnLifecycles(view1); + const instance2 = spyOnLifecycles(view2); + + await nav.setPages([{ + page: mockView(MockView4) + }, { + page: mockView(MockView5) + }], null, trnsDone); + expect(instance1.ionViewWillUnload).toHaveBeenCalled(); + expect(instance2.ionViewWillUnload).toHaveBeenCalled(); + + const hasCompleted = true; + const requiresTransition = true; + expect(trnsDone).toHaveBeenCalledWith( + hasCompleted, requiresTransition, 'mock-view5', 'mock-view2', DIRECTION_BACK + ); + expect(nav.length()).toEqual(2); + expect(nav.getByIndex(0).component).toEqual(MockView4); + expect(nav.getByIndex(1).component).toEqual(MockView5); + + + }, 10000); + + }); + + + describe('destroy', () => { + + it('should not crash when destroyed while transitioning', (done) => { + const view1 = mockView(MockView1); + nav.push(view1).then((succeded: boolean) => { + expect(succeded).toEqual(false); + done(); + }).catch(() => { + fail('should never get here'); + done(); + }); + nav.destroy(); + }, 10000); + }); + + describe('canSwipeBack', () => { + it('should not swipe back when its not enabled', () => { + nav.swipeBackEnabled = false; + + const view1 = mockView(); + const view2 = mockView(); + mockViews(nav, [view1, view2]); + + const result = nav.canSwipeBack(); + expect(result).toEqual(false); + }); + + it('should swipe back when has a view to go back to', () => { + nav.swipeBackEnabled = true; + const view1 = mockView(); + const view2 = mockView(); + mockViews(nav, [view1, view2]); + + const result = nav.canSwipeBack(); + expect(result).toEqual(true); + }); + }); + + + let nav: NavControllerBase; + + function spyOnLifecycles(view: ViewController) { + const element = view.element as any; + Object.assign(element, { + ionViewDidLoad: () => { + return; + }, + ionViewWillEnter: () => { + return; + }, + ionViewDidEnter: () => { + return; + }, + ionViewWillLeave: () => { + return; + }, + ionViewDidLeave: () => { + return; + }, + ionViewWillUnload: () => { + return; + }, + }); + + + const instance = { + ionViewDidLoad: jest.spyOn(element, 'ionViewDidLoad'), + ionViewWillEnter: jest.spyOn(element, 'ionViewWillEnter'), + ionViewDidEnter: jest.spyOn(element, 'ionViewDidEnter'), + ionViewWillLeave: jest.spyOn(element, 'ionViewWillLeave'), + ionViewDidLeave: jest.spyOn(element, 'ionViewDidLeave'), + ionViewWillUnload: jest.spyOn(element, 'ionViewWillUnload'), + }; + + element.addEventListener('ionViewDidLoad', element.ionViewDidLoad); + element.addEventListener('ionViewWillEnter', element.ionViewWillEnter); + element.addEventListener('ionViewDidEnter', element.ionViewDidEnter); + element.addEventListener('ionViewWillLeave', element.ionViewWillLeave); + element.addEventListener('ionViewDidLeave', element.ionViewDidLeave); + element.addEventListener('ionViewWillUnload', element.ionViewWillUnload); + return instance; + } + + let trnsDone: jest.Mock; + beforeEach(() => { + trnsDone = jest.fn(); + nav = mockNavController(); + }); + +}); + +const MockView = 'mock-view'; +const MockView1 = 'mock-view1'; +const MockView2 = 'mock-view2'; +const MockView3 = 'mock-view3'; +const MockView4 = 'mock-view4'; +const MockView5 = 'mock-view5'; +const dom = mockDocument(); + +function mockView(component ?: any, data ?: any) { + if (!component) { + component = MockView; + } + + const view = new ViewController(component, data); + view._lifecycle = function(lifecycle: string) { + const event = dom.createEvent('CustomEvent'); + event.initCustomEvent(`ionView${lifecycle}`, false, false, null); + this.element.dispatchEvent(event); + }; + view.element = mockElement(component) as HTMLElement; + return view; +} + +function mockViews(nav: NavControllerBase, views: ViewController[]) { + nav._views = views; + views.forEach(v => { + v._setNav(nav); + }); +} + +function mockNavController(): NavControllerBase { + const nav = new NavControllerBase() as any; + nav.el = mockElement('ion-nav') as HTMLElement; + nav.ionNavChanged = {emit: function() { return; } }; + nav.animationCtrl = new AnimationControllerImpl() as any; + nav.config = createConfigController({animate: false}, []); + nav._viewInit = function (enteringView: ViewController) { + if (!enteringView.element) { + console.log(enteringView.component); + enteringView.element = (typeof enteringView.component === 'string') + ? mockElement(enteringView.component) as HTMLElement + : enteringView.element = enteringView.component as HTMLElement; + } + enteringView._state = STATE_INITIALIZED; + }; + return nav; +} diff --git a/packages/core/src/components/nav/test/nav-then-tabs/index.html b/packages/core/src/components/nav/test/nav-then-tabs/index.html deleted file mode 100644 index e9e3eab0ec..0000000000 --- a/packages/core/src/components/nav/test/nav-then-tabs/index.html +++ /dev/null @@ -1,384 +0,0 @@ - - - - - Nav Then Tabs - - - - - - - - - - diff --git a/packages/core/src/components/nav/test/set-root/index.html b/packages/core/src/components/nav/test/set-root/index.html new file mode 100644 index 0000000000..823c9ed9b6 --- /dev/null +++ b/packages/core/src/components/nav/test/set-root/index.html @@ -0,0 +1,110 @@ + + + + + Nav + + + + + + + + + + + diff --git a/packages/core/src/components/nav/transition-controller.ts b/packages/core/src/components/nav/transition-controller.ts new file mode 100644 index 0000000000..e7573bd71e --- /dev/null +++ b/packages/core/src/components/nav/transition-controller.ts @@ -0,0 +1,49 @@ +import { isPresent } from './nav-util'; +import { Transition } from './transition'; +import { NavControllerBase } from './nav'; + + +export class TransitionController { + private _ids = 0; + private _trns = new Map(); + + // constructor(public plt: Platform, private _config: Config) {} + + getRootTrnsId(nav: NavControllerBase): number { + nav = nav.parent; + while (nav) { + if (isPresent(nav._trnsId)) { + return nav._trnsId; + } + nav = nav.parent; + } + return null; + } + + nextId() { + return this._ids++; + } + + register(trnsId: number, trns: Transition) { + trns.trnsId = trnsId; + + const parent = this._trns.get(trnsId); + if (!parent) { + // we haven't created the root transition yet + this._trns.set(trnsId, trns); + + } else { + // we already have a root transition created + // add this new transition as a child to the root + parent.parent = trns; + } + } + + destroy(trnsId: number) { + const trans = this._trns.get(trnsId); + if (trans) { + trans.destroy(); + this._trns.delete(trnsId); + } + } +} diff --git a/packages/core/src/components/nav/transition.ts b/packages/core/src/components/nav/transition.ts new file mode 100644 index 0000000000..00501f4907 --- /dev/null +++ b/packages/core/src/components/nav/transition.ts @@ -0,0 +1,57 @@ +import { ViewController } from './view-controller'; +import { Animation, AnimationBuilder } from '../..'; + +/** + * @hidden + * + * - play + * - Add before classes - DOM WRITE + * - Remove before classes - DOM WRITE + * - Add before inline styles - DOM WRITE + * - set inline FROM styles - DOM WRITE + * - RAF + * - read toolbar dimensions - DOM READ + * - write content top/bottom padding - DOM WRITE + * - set css transition duration/easing - DOM WRITE + * - RAF + * - set inline TO styles - DOM WRITE + */ +export class Transition { + _trnsStart: Function; + + trnsId: number; + ani: Animation; + parent: Transition; + + constructor( + private animationCtrl: HTMLIonAnimationControllerElement, + private builder: AnimationBuilder, + public enteringView: ViewController, + public leavingView: ViewController, + private opts: any + ) {} + + registerStart(trnsStart: Function) { + this._trnsStart = trnsStart; + } + + init() { + return this.animationCtrl.create(this.builder, null, this.opts).then((ani) => { + this.ani = ani; + }); + } + + start() { + this._trnsStart && this._trnsStart(); + this._trnsStart = null; + + // bubble up start + this.parent && this.parent.start(); + } + + destroy() { + this.ani && this.ani.destroy(); + this.ani = this._trnsStart = null; + } + +} diff --git a/packages/core/src/components/nav/view-controller.ts b/packages/core/src/components/nav/view-controller.ts index 75a9ca2cc4..1ac1fb02fe 100644 --- a/packages/core/src/components/nav/view-controller.ts +++ b/packages/core/src/components/nav/view-controller.ts @@ -1,137 +1,326 @@ -import { FrameworkDelegate, Nav, PublicViewController } from '../../index'; -import { STATE_ATTACHED, STATE_DESTROYED, STATE_INITIALIZED, STATE_NEW } from './nav-utils'; -import { - assert, - normalizeUrl -} from '../../utils/helpers'; +import { NavOptions, STATE_ATTACHED, STATE_DESTROYED, STATE_INITIALIZED, STATE_NEW } from './nav-util'; +import { NavControllerBase } from './nav'; +import { assert } from '../../utils/helpers'; -export class ViewController implements PublicViewController { +/** + * @name ViewController + * @description + * Access various features and information about the current view. + * @usage + * ```ts + * import { Component } from '@angular/core'; + * import { ViewController } from 'ionic-angular'; + * + * @Component({...}) + * export class MyPage{ + * + * constructor(public viewCtrl: ViewController) {} + * + * } + * ``` + */ +export class ViewController { + private _cntDir: any; + private _isHidden = false; + private _leavingOpts: NavOptions; + private _detached: boolean; + + _nav: NavControllerBase; + _zIndex: number; + _state: number = STATE_NEW; + _cssClass: string; + + /** @hidden */ id: string; - data: any; + + /** @hidden */ + isOverlay = false; + element: HTMLElement; - instance: any; - state: number; - nav: Nav; - overlay: boolean; - zIndex: number; - dismissProxy: any; - timestamp: number; - fromExternalRouter: boolean; - url: string; + /** @hidden */ + // @Output() private _emitter: EventEmitter = new EventEmitter(); - - onDidDismiss: (data: any, role: string) => void; - onWillDismiss: (data: any, role: string) => void; - - constructor(public component: any, data: any, fromExternalRouter: boolean, url: string) { - initializeNewViewController(this, data, fromExternalRouter, url); + constructor( + public component: any, + public data?: any, + rootCssClass: string = DEFAULT_CSS_CLASS + ) { + // component could be anything, never use it directly + // it could be a string, a HTMLElement + // passed in data could be NavParams, but all we care about is its data object + // this.data = (data instanceof NavParams ? data.data : (isPresent(data) ? data : {})); + this._cssClass = rootCssClass; } - - willLeave(unload: boolean): void { - willLeaveImpl(unload, this); + /** + * @hidden + */ + init() { + if (this.element) { + return; + } + const component = this.component; + this.element = (typeof component === 'string') + ? document.createElement(component) + : component; } - didLeave(): void { - didLeaveImpl(this); + _setNav(navCtrl: NavControllerBase) { + this._nav = navCtrl; } - willEnter(): void { - callLifeCycleFunction(this.instance, 'ionViewWillEnter'); + /** + * @hidden + */ + getNav(): NavControllerBase { + return this._nav; } - didEnter(): void { - didEnterImpl(this); + /** + * @hidden + */ + getTransitionName(_direction: string): string { + return this._nav && this._nav.config && this._nav.config.get('pageTransition') || 'md'; } - willLoad(): void { - willLoadImpl(this); + /** + * @hidden + */ + setLeavingOpts(opts: NavOptions) { + this._leavingOpts = opts; } - didLoad(): void { - didLoadImpl(this); + /** + * Check to see if you can go back in the navigation stack. + * @returns {boolean} Returns if it's possible to go back from this Page. + */ + enableBack(): boolean { + // update if it's possible to go back from this nav item + if (!this._nav) { + return false; + } + // the previous view may exist, but if it's about to be destroyed + // it shouldn't be able to go back to + const previousItem = this._nav.getPrevious(this); + return !!(previousItem); } - willUnload(): void { - willUnloadImpl(this); + /** + * @hidden + */ + get name(): string { + const component = this.component; + if (typeof component === 'string') { + return component; + } + if (component.tagName) { + return component.tagName; + } + return this.element ? this.element.tagName : 'unknown'; } - destroy(delegate?: FrameworkDelegate): Promise { - return destroy(this, delegate); + /** + * @hidden + * DOM WRITE + */ + _domShow(shouldShow: boolean) { + // using hidden element attribute to display:none and not render views + // doing checks to make sure we only update the DOM when actually needed + // if it should render, then the hidden attribute should not be on the element + if (this.element && shouldShow === this._isHidden) { + this._isHidden = !shouldShow; + + // ******** DOM WRITE **************** + if (shouldShow) { + this.element.removeAttribute('hidden'); + } else { + this.element.setAttribute('hidden', ''); + } + + } } -} -export function callLifecycle(instance: any, methodName: string) { - instance && instance[methodName] && instance[methodName](); -} + /** + * @hidden + */ + getZIndex(): number { + return this._zIndex; + } -export function destroy(viewController: ViewController, delegate?: FrameworkDelegate): Promise { - assert(viewController.state !== STATE_DESTROYED, 'view state must be attached'); - return delegate ? delegate.removeViewFromDom(viewController.nav.element, viewController.element) : Promise.resolve().then(() => { + /** + * @hidden + * DOM WRITE + */ + _setZIndex(zIndex: number) { + if (zIndex !== this._zIndex) { + this._zIndex = zIndex; + const pageEl = this.element; + if (pageEl) { + const el = pageEl as HTMLElement; + // ******** DOM WRITE **************** + el.style.zIndex = zIndex + ''; + } + } + } - if (viewController.component) { - // TODO - consider removing classes and styles as thats what we do in ionic-angular + _preLoad() { + assert(this._state === STATE_INITIALIZED, 'view state must be INITIALIZED'); + this._lifecycle('PreLoad'); + } + + /** + * @hidden + * The view has loaded. This event only happens once per view will be created. + * This event is fired before the component and his children have been initialized. + */ + _willLoad() { + assert(this._state === STATE_INITIALIZED, 'view state must be INITIALIZED'); + this._lifecycle('WillLoad'); + } + + /** + * @hidden + * The view has loaded. This event only happens once per view being + * created. If a view leaves but is cached, then this will not + * fire again on a subsequent viewing. This method is a good place + * to put your setup code for the view; however, it is not the + * recommended method to use when a view becomes active. + */ + _didLoad() { + assert(this._state === STATE_ATTACHED, 'view state must be ATTACHED'); + this._lifecycle('DidLoad'); + } + + /** + * @hidden + * The view is about to enter and become the active view. + */ + _willEnter() { + assert(this._state === STATE_ATTACHED, 'view state must be ATTACHED'); + + if (this._detached) { + // ensure this has been re-attached to the change detector + // TODO + // this._cmp.changeDetectorRef.reattach(); + this._detached = false; } - viewController.id = viewController.data = viewController.element = viewController.instance = viewController.nav = viewController.dismissProxy = null; - viewController.state = STATE_DESTROYED; - }); -} - -export function callLifeCycleFunction(instance: any, functionName: string) { - instance && instance[functionName] && instance[functionName](); -} - -export function willLeaveImpl(unload: boolean, viewController: ViewController) { - callLifeCycleFunction(viewController.instance, 'ionViewWillLeave'); - if (unload && viewController.onWillDismiss) { - viewController.onWillDismiss(this.dismissProxy.data, this.dismissProxy.proxy); - viewController.onWillDismiss = null; + // this.willEnter.emit(null); + this._lifecycle('WillEnter'); } + + /** + * @hidden + * The view has fully entered and is now the active view. This + * will fire, whether it was the first load or loaded from the cache. + */ + _didEnter() { + assert(this._state === STATE_ATTACHED, 'view state must be ATTACHED'); + + // this._nb && this._nb.didEnter(); + // this.didEnter.emit(null); + this._lifecycle('DidEnter'); + } + + /** + * @hidden + * The view is about to leave and no longer be the active view. + */ + _willLeave(_willUnload: boolean) { + // this.willLeave.emit(null); + this._lifecycle('WillLeave'); + } + + /** + * @hidden + * The view has finished leaving and is no longer the active view. This + * will fire, whether it is cached or unloaded. + */ + _didLeave() { + // this.didLeave.emit(null); + this._lifecycle('DidLeave'); + + // when this is not the active page + // we no longer need to detect changes + if (!this._detached) { + // TODO + // this._cmp.changeDetectorRef.detach(); + this._detached = true; + } + } + + /** + * @hidden + */ + _willUnload() { + // this.willUnload.emit(null); + this._lifecycle('WillUnload'); + } + + /** + * @hidden + * DOM WRITE + */ + _destroy() { + assert(this._state !== STATE_DESTROYED, 'view state must be ATTACHED'); + + const element = this.element; + if (element) { + // completely destroy this component. boom. + // TODO + // this._cmp.destroy(); + element.remove(); + } + + this._nav = this._cntDir = this._leavingOpts = null; + this._state = STATE_DESTROYED; + } + + /** + * Get the index of the current component in the current navigation stack. + * @returns {number} Returns the index of this page within its `NavController`. + */ + get index(): number { + return (this._nav ? this._nav.indexOf(this) : -1); + } + + /** + * @hidden + */ + _lifecycleTest(_lifecycle: string): Promise { + // const instance = this.instance; + // const methodName = 'ionViewCan' + lifecycle; + // if (instance && instance[methodName]) { + // try { + // const result = instance[methodName](); + // if (result instanceof Promise) { + // return result; + // } else { + // // Any value but explitic false, should be true + // return Promise.resolve(result !== false); + // } + + // } catch (e) { + // return Promise.reject(`${this.name} ${methodName} error: ${e.message}`); + // } + // } + return Promise.resolve(true); + } + + _lifecycle(lifecycle: string) { + const event = new CustomEvent(`ionView${lifecycle}`, { + bubbles: false, + cancelable: false + }); + this.element.dispatchEvent(event); + } + } -export function didLeaveImpl(viewController: ViewController) { - callLifeCycleFunction(viewController.instance, 'ionViewDidLeave'); - // TODO, maybe need to do something framework specific here... figure this out later - // for example, disconnecting from change detection +export function isViewController(viewCtrl: any): viewCtrl is ViewController { + return !!(viewCtrl && (viewCtrl)._didLoad && (viewCtrl)._willUnload); } -export function willEnterImpl(viewController: ViewController) { - assert(viewController.state === STATE_ATTACHED, 'view state must be ATTACHED'); - // TODO, maybe need to do something framework specific here... figure this out later - // for example, connecting to change detection - callLifeCycleFunction(viewController.instance, 'ionViewWillEnter'); -} - -export function didEnterImpl(viewController: ViewController) { - assert(viewController.state === STATE_ATTACHED, 'view state must be ATTACHED'); - // TODO - navbar didEnter here - callLifeCycleFunction(viewController.instance, 'ionViewDidEnter'); -} - -export function willLoadImpl(viewController: ViewController) { - assert(viewController.state === STATE_INITIALIZED, 'view state must be INITIALIZED'); - callLifeCycleFunction(viewController.instance, 'ionViewWillLoad'); -} - -export function willUnloadImpl(viewController: ViewController) { - callLifeCycleFunction(viewController.instance, 'ionViewWillUnLoad'); - viewController.onDidDismiss && viewController.onDidDismiss(viewController.dismissProxy.data, viewController.dismissProxy.role); - - viewController.onDidDismiss = viewController.dismissProxy = null; -} - -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, fromExternalRouter: boolean, url: string) { - viewController.timestamp = Date.now(); - viewController.state = STATE_NEW; - viewController.data = data || {}; - viewController.fromExternalRouter = fromExternalRouter; - viewController.url = url && normalizeUrl(url); -} +const DEFAULT_CSS_CLASS = 'ion-page'; diff --git a/packages/core/src/components/range/readme.md b/packages/core/src/components/range/readme.md index f48309da6a..30d825a09a 100644 --- a/packages/core/src/components/range/readme.md +++ b/packages/core/src/components/range/readme.md @@ -45,6 +45,7 @@ left or right of the range. See [usage](#usage) below for examples. + ## Properties #### color @@ -55,6 +56,7 @@ The color to use from your Sass `$colors` map. Default options are: `"primary"`, `"secondary"`, `"tertiary"`, `"success"`, `"warning"`, `"danger"`, `"light"`, `"medium"`, and `"dark"`. For more information, see [Theming your App](/docs/theming/theming-your-app). + #### debounce number @@ -62,34 +64,42 @@ number How long, in milliseconds, to wait to trigger the `ionChange` event after each change in the range value. Default `0`. + #### disabled boolean + #### dualKnobs boolean Show two knobs. Defaults to `false`. + #### max number Maximum integer value of the range. Defaults to `100`. + #### min number Minimum integer value of the range. Defaults to `0`. + #### mode + + The mode determines which platform styles to use. Possible values are: `"ios"` or `"md"`. For more information, see [Platform Styles](/docs/theming/platform-specific-styles). + #### pin boolean @@ -97,6 +107,7 @@ boolean If true, a pin with integer value is shown when the knob is pressed. Defaults to `false`. + #### snaps boolean @@ -104,18 +115,21 @@ boolean If true, the knob snaps to tick marks evenly spaced based on the step property value. Defaults to `false`. + #### step number Specifies the value granularity. Defaults to `1`. + #### value any the value of the range. + ## Attributes #### color @@ -126,6 +140,7 @@ The color to use from your Sass `$colors` map. Default options are: `"primary"`, `"secondary"`, `"tertiary"`, `"success"`, `"warning"`, `"danger"`, `"light"`, `"medium"`, and `"dark"`. For more information, see [Theming your App](/docs/theming/theming-your-app). + #### debounce number @@ -133,34 +148,42 @@ number How long, in milliseconds, to wait to trigger the `ionChange` event after each change in the range value. Default `0`. + #### disabled boolean + #### dual-knobs boolean Show two knobs. Defaults to `false`. + #### max number Maximum integer value of the range. Defaults to `100`. + #### min number Minimum integer value of the range. Defaults to `0`. + #### mode + + The mode determines which platform styles to use. Possible values are: `"ios"` or `"md"`. For more information, see [Platform Styles](/docs/theming/platform-specific-styles). + #### pin boolean @@ -168,6 +191,7 @@ boolean If true, a pin with integer value is shown when the knob is pressed. Defaults to `false`. + #### snaps boolean @@ -175,36 +199,43 @@ boolean If true, the knob snaps to tick marks evenly spaced based on the step property value. Defaults to `false`. + #### step number Specifies the value granularity. Defaults to `1`. + #### value any the value of the range. + ## Events #### ionBlur Emitted when the range loses focus. + #### ionChange Emitted when the value property has changed. + #### ionFocus Emitted when the range has focus. + #### ionStyle Emitted when the styles change. + ## Methods #### ratio() @@ -213,12 +244,15 @@ Returns the ratio of the knob's is current location, which is a number between `0` and `1`. If two knobs are used, this property represents the lower value. + #### ratioUpper() Returns the ratio of the upper value's is current location, which is a number between `0` and `1`. If there is only one knob, then this will return `null`. ---- -_Built with [StencilJS](https://stenciljs.com/)_ + +---------------------------------------------- + +*Built with [StencilJS](https://stenciljs.com/)* diff --git a/packages/core/src/components/route/readme.md b/packages/core/src/components/route/readme.md index b6c8c1a78f..aec9f098b8 100644 --- a/packages/core/src/components/route/readme.md +++ b/packages/core/src/components/route/readme.md @@ -7,6 +7,11 @@ ## Properties +#### component + +string + + #### path string @@ -17,13 +22,13 @@ string any -#### sel - -string - - ## Attributes +#### component + +string + + #### path string @@ -34,11 +39,6 @@ string any -#### sel - -string - - ---------------------------------------------- diff --git a/packages/core/src/components/route/route.tsx b/packages/core/src/components/route/route.tsx index 75e3562ccc..fdabf5c6b5 100644 --- a/packages/core/src/components/route/route.tsx +++ b/packages/core/src/components/route/route.tsx @@ -5,9 +5,7 @@ import { Component, Prop } from '@stencil/core'; tag: 'ion-route' }) export class Route { - - @Prop() path: string; - @Prop() sel: string; + @Prop() path = ''; + @Prop() component: string; @Prop() props: any = {}; - } diff --git a/packages/core/src/components/router/router-utils.ts b/packages/core/src/components/router/router-utils.ts index 1bc9cb5a51..08d32402a3 100644 --- a/packages/core/src/components/router/router-utils.ts +++ b/packages/core/src/components/router/router-utils.ts @@ -143,7 +143,7 @@ export function readRoutes(root: Element): RouterEntries { .filter(el => el.tagName === 'ION-ROUTE') .map(el => ({ path: parsePath(el.path), - id: el.sel, + id: el.component, props: el.props, subroutes: readRoutes(el) })); diff --git a/packages/core/src/components/router/test/basic/index.html b/packages/core/src/components/router/test/basic/index.html index c319649eaf..0fc2cde6d7 100644 --- a/packages/core/src/components/router/test/basic/index.html +++ b/packages/core/src/components/router/test/basic/index.html @@ -106,33 +106,30 @@ - + - - - - + + + + - - - - + + + - + icon="star"> - + icon="globe"> - diff --git a/packages/core/src/components/status-tap/status-tap.tsx b/packages/core/src/components/status-tap/status-tap.tsx index c2864070e3..4c6b81c4bd 100644 --- a/packages/core/src/components/status-tap/status-tap.tsx +++ b/packages/core/src/components/status-tap/status-tap.tsx @@ -37,5 +37,72 @@ export class StatusTap { return scroll.scrollToTop(this.duration); }); }); + + + /** + * The back button event is triggered when the user presses the native + * platform's back button, also referred to as the "hardware" back button. + * This event is only used within Cordova apps running on Android and + * Windows platforms. This event is not fired on iOS since iOS doesn't come + * with a hardware back button in the same sense an Android or Windows device + * does. + * + * Registering a hardware back button action and setting a priority allows + * apps to control which action should be called when the hardware back + * button is pressed. This method decides which of the registered back button + * actions has the highest priority and should be called. + * + * @param {Function} fn Called when the back button is pressed, + * if this registered action has the highest priority. + * @param {number} priority Set the priority for this action. Only the highest priority will execute. Defaults to `0`. + * @returns {Function} A function that, when called, will unregister + * the back button action. + */ + // @Method() + // registerBackButtonAction(fn: Function, priority = 0): () => void { + // const newAction = { + // fn, + // priority + // }; + // backButtonActions.push(newAction); + + // return () => { + // backButtonActions = backButtonActions.filter(bbAction => bbAction !== newAction); + // }; + // } + + // @Listen('document:backbutton') + // hardwareBackButtonPressed() { + // // check if menu exists and is open + // return checkIfMenuIsOpen().then((done: boolean) => { + // if (!done) { + // // we need to check if there is an action-sheet, alert, loading, picker, popover or toast open + // // if so, just return and don't do anything + // // Why? I have no idea, but that is the existing behavior in Ionic 3 + // return checkIfNotModalOverlayIsOpen(); + // } + // return done; + // }).then((done: boolean) => { + // if (!done) { + // // if there's a modal open, close that instead + // return closeModalIfOpen(); + // } + // return done; + // }).then((done: boolean) => { + // // okay cool, it's time to pop a nav if possible + // if (!done) { + // return popEligibleView(); + // } + // return done; + // }).then((done: boolean) => { + // if (!done) { + // // okay, we didn't find a nav that we can pop, so we should just exit the app + // // since each platform exits differently, just delegate it to the platform to + // // figure out how to exit + // return this.exitApp.emit(); + // } + // return Promise.resolve(); + // }); + // } } } diff --git a/packages/core/src/components/tab-button/tab-button.scss b/packages/core/src/components/tab-button/tab-button.scss index 26043e5aae..12be6e2f1b 100644 --- a/packages/core/src/components/tab-button/tab-button.scss +++ b/packages/core/src/components/tab-button/tab-button.scss @@ -48,7 +48,7 @@ ion-tab-button { } } -.tab-disabled { +.tab-btn-disabled { pointer-events: none; > .tab-cover { @@ -101,7 +101,6 @@ ion-tab-button { justify-content: center; } -.tab-hidden, .layout-icon-hide .tab-button-icon, .layout-title-hide .tab-button-text { display: none; diff --git a/packages/core/src/components/tab-button/tab-button.tsx b/packages/core/src/components/tab-button/tab-button.tsx index 80cf277a76..79497bea8c 100644 --- a/packages/core/src/components/tab-button/tab-button.tsx +++ b/packages/core/src/components/tab-button/tab-button.tsx @@ -57,6 +57,7 @@ export class TabButton { 'role': 'tab', 'id': tab.btnId, 'aria-selected': selected, + 'hidden': !tab.show, class: { 'tab-selected': selected, 'has-title': hasTitle, @@ -64,8 +65,7 @@ export class TabButton { 'has-title-only': hasTitleOnly, 'has-icon-only': hasIconOnly, 'has-badge': hasBadge, - 'tab-disabled': tab.disabled, - 'tab-hidden': tab.hidden, + 'tab-btn-disabled': tab.disabled, 'focused': this.keyFocus } }; diff --git a/packages/core/src/components/tab/readme.md b/packages/core/src/components/tab/readme.md index f2e144e262..174630a965 100644 --- a/packages/core/src/components/tab/readme.md +++ b/packages/core/src/components/tab/readme.md @@ -81,11 +81,6 @@ any The component to display inside of the tab. -#### delegate - - - - #### disabled boolean @@ -168,11 +163,6 @@ any The component to display inside of the tab. -#### delegate - - - - #### disabled boolean diff --git a/packages/core/src/components/tab/tab.scss b/packages/core/src/components/tab/tab.scss index 1b9cc4de49..5134090f09 100644 --- a/packages/core/src/components/tab/tab.scss +++ b/packages/core/src/components/tab/tab.scss @@ -4,16 +4,7 @@ ion-tab { @include position(0, null, null, 0); position: absolute; - z-index: -1; - display: none; width: 100%; height: 100%; - - contain: layout size style; -} - -ion-tab.show-tab { - z-index: $z-index-page-container; - display: block; } diff --git a/packages/core/src/components/tab/tab.tsx b/packages/core/src/components/tab/tab.tsx index 323e66cd5a..8b8c877d04 100644 --- a/packages/core/src/components/tab/tab.tsx +++ b/packages/core/src/components/tab/tab.tsx @@ -1,7 +1,5 @@ import { Component, Element, Event, EventEmitter, Method, Prop, State, Watch } from '@stencil/core'; - -import { FrameworkDelegate } from '../..'; -import { asyncRaf, getIonApp, getNavAsChildIfExists } from '../../utils/helpers'; +import { asyncRaf } from '../../utils/helpers'; @Component({ @@ -15,8 +13,6 @@ export class Tab { @State() init = false; - @Prop() delegate: FrameworkDelegate; - @Prop({ mutable: true }) active = false; @Prop() btnId: string; @@ -103,45 +99,23 @@ export class Tab { private prepareLazyLoaded(): Promise { if (!this.loaded && this.component) { this.loaded = true; - const promise = (this.delegate) - ? this.delegate.attachViewToDom(this.el, this.component) - : attachViewToDom(this.el, this.component); - - return promise.then(() => asyncRaf()); + return attachViewToDom(this.el, this.component).then(() => asyncRaf()); } return Promise.resolve(); } private showTab(): Promise { this.active = true; - const nav = getNavAsChildIfExists(this.el); - if (!nav) { - return Promise.resolve(); - } - // the tab's nav has been initialized externally - return getIonApp().then((ionApp) => { - const externalNavPromise = ionApp ? ionApp.getExternalNavPromise() : null; - if (externalNavPromise) { - return (externalNavPromise as any).then(() => { - ionApp.setExternalNavPromise(null); - }); - } - - // the tab's nav has not been initialized externally, so - // check if we need to initiailize it - return nav.componentOnReady() - .then(() => nav.activateFromTab(this.selected)); - }); + return Promise.resolve(); } hostData() { - const hidden = !this.active || !this.selected; return { - 'aria-hidden': hidden, 'aria-labelledby': this.btnId, 'role': 'tabpanel', - class: { - 'show-tab': this.active + 'hidden': !this.active, + 'class': { + 'ion-page': !this.component } }; } diff --git a/packages/core/src/components/tabs/tabs.tsx b/packages/core/src/components/tabs/tabs.tsx index bf82e660c3..756e639f24 100644 --- a/packages/core/src/components/tabs/tabs.tsx +++ b/packages/core/src/components/tabs/tabs.tsx @@ -1,7 +1,5 @@ import { Component, Element, Event, EventEmitter, Listen, Method, Prop, State } from '@stencil/core'; -import { Config, NavEventDetail, NavOutlet } from '../../index'; - -import { getIonApp } from '../../utils/helpers'; +import { Config, NavOutlet } from '../../index'; @Component({ @@ -12,7 +10,6 @@ export class Tabs implements NavOutlet { private ids = -1; private transitioning = false; private tabsId: number = (++tabIds); - initialized = false; @Element() el: HTMLElement; @@ -67,29 +64,17 @@ export class Tabs implements NavOutlet { * Emitted when the tab changes. */ @Event() ionChange: EventEmitter; - @Event() ionNavChanged: EventEmitter; + @Event() ionNavChanged: EventEmitter; - componentDidLoad() { + componentWillLoad() { this.loadConfig('tabsPlacement', 'bottom'); this.loadConfig('tabsLayout', 'icon-top'); this.loadConfig('tabsHighlight', true); + } - const promises: Promise[] = []; - promises.push(this.initTabs()); - promises.push(getIonApp()); - return Promise.all(promises).then(([_, ionApp]) => { - if (ionApp) { - return (ionApp as HTMLIonAppElement).getExternalNavOccuring(); - } - return false; - }).then((externalNavOccuring: boolean) => { - if (!externalNavOccuring) { - return this.initSelect(); - } - return null; - }).then(() => { - this.initialized = true; - }); + componentDidLoad() { + return this.initTabs() + .then(() => this.initSelect()); } componentDidUnload() { @@ -98,7 +83,6 @@ export class Tabs implements NavOutlet { } @Listen('ionTabbarClick') - // @Listen('ionSelect') protected tabChange(ev: CustomEvent) { const selectedTab = ev.detail as HTMLIonTabElement; this.select(selectedTab); diff --git a/packages/core/src/components/tabs/test/basic/index.html b/packages/core/src/components/tabs/test/basic/index.html index 83ccee926c..aac43fef91 100644 --- a/packages/core/src/components/tabs/test/basic/index.html +++ b/packages/core/src/components/tabs/test/basic/index.html @@ -5,7 +5,6 @@ Tab - Basic - @@ -13,40 +12,40 @@ - - - Tab One - - - - Tab One - + + + Tab One + + + + Tab One + - + - - - Tab Two - - - - Tab Two - + + + Tab Two + + + + Tab Two + - + - - - Tab Three - - - - Tab Three - + + + Tab Three + + + + Tab Three + diff --git a/packages/core/src/components/tap-click/tap-click.tsx b/packages/core/src/components/tap-click/tap-click.tsx index 90371979e5..cb53e085bb 100644 --- a/packages/core/src/components/tap-click/tap-click.tsx +++ b/packages/core/src/components/tap-click/tap-click.tsx @@ -7,7 +7,6 @@ import { now, pointerCoord } from '../../utils/helpers'; }) export class TapClick { - private app: HTMLIonAppElement; private lastTouch = -MOUSE_WAIT * 10; private lastActivated = 0; private cancelled = false; @@ -22,16 +21,9 @@ export class TapClick { @Element() el: HTMLElement; - componentDidLoad() { - if (this.isServer) { - return; - } - this.app = this.el.closest('ion-app'); - } - @Listen('body:click', {passive: false, capture: true}) onBodyClick(ev: Event) { - if (this.cancelled || this.shouldCancel()) { + if (this.cancelled) { ev.preventDefault(); ev.stopPropagation(); } @@ -87,11 +79,8 @@ export class TapClick { if (this.activatableEle) { return; } - this.cancelled = this.shouldCancel(); - - if (!this.cancelled) { - this.setActivatedElement(getActivatableTarget(ev.target), ev); - } + this.cancelled = false; + this.setActivatedElement(getActivatableTarget(ev.target), ev); } private pointerUp(ev: UIEvent) { @@ -167,15 +156,6 @@ export class TapClick { activatableEle.classList.remove(ACTIVATED); } } - - - private shouldCancel(): boolean { - if (!this.app.isEnabled()) { - console.debug('click prevent: appDisabled'); - return true; - } - return false; - } } function getActivatableTarget(el: HTMLElement): any { diff --git a/packages/core/src/index.d.ts b/packages/core/src/index.d.ts index 488e1d9ab5..c8aae2a6d9 100644 --- a/packages/core/src/index.d.ts +++ b/packages/core/src/index.d.ts @@ -61,7 +61,6 @@ export { export * from './components/modal/modal'; export { ModalController } from './components/modal-controller/modal-controller'; export * from './components/nav/nav'; -export * from './components/nav/nav-interfaces'; export { ViewController } from './components/nav/view-controller'; export { Note } from './components/note/note'; export { PickerColumnCmp } from './components/picker-column/picker-column';