diff --git a/src/animations/animation.ts b/src/animations/animation.ts index 85d8faa42b..c4cb6a0ea6 100644 --- a/src/animations/animation.ts +++ b/src/animations/animation.ts @@ -289,11 +289,12 @@ export class Animation { var self = this; var i: number; + let dur = this._dur; if (isDefined(opts.duration)) { - self._dur = opts.duration; + dur = opts.duration; } - console.debug('Animation, play, duration', self._dur, 'easing', self._easing); + console.debug('Animation, play, duration', dur, 'easing', this._easing); // always default that an animation does not tween // a tween requires that an Animation class has an element @@ -313,7 +314,7 @@ export class Animation { // ensure all past transition end events have been cleared self._clearAsync(); - if (self._dur > 30) { + if (dur > 30) { // this animation has a duration, so it should animate // place all the elements with their FROM properties @@ -328,7 +329,7 @@ export class Animation { // set the async TRANSITION END event // and run onFinishes when the transition ends // ******** DOM WRITE **************** - self._asyncEnd(self._dur, true); + self._asyncEnd(dur, true); // begin each animation when everything is rendered in their place // and the transition duration/easing is ready to go diff --git a/src/components/alert/test/basic/index.ts b/src/components/alert/test/basic/index.ts index 9ce64108a3..b975ded10c 100644 --- a/src/components/alert/test/basic/index.ts +++ b/src/components/alert/test/basic/index.ts @@ -304,6 +304,7 @@ class E2EPage { Hi, I'm Bob, and I'm a modal. + ` }) diff --git a/src/components/app/app.ts b/src/components/app/app.ts index a505688982..1b724719d8 100644 --- a/src/components/app/app.ts +++ b/src/components/app/app.ts @@ -4,6 +4,7 @@ import { Title } from '@angular/platform-browser'; import { ClickBlock } from '../../util/click-block'; import { Config } from '../../config/config'; import { NavController } from '../nav/nav-controller'; +import { isTabs, isNav } from '../nav/nav-controller-base'; import { NavOptions } from '../nav/nav-interfaces'; import { NavPortal } from '../nav/nav-portal'; import { Platform } from '../../platform/platform'; @@ -31,6 +32,11 @@ export class App { */ clickBlock: ClickBlock; + /** + * @private + */ + appRoot: AppRoot; + viewDidLoad: EventEmitter = new EventEmitter(); viewWillEnter: EventEmitter = new EventEmitter(); viewDidEnter: EventEmitter = new EventEmitter(); @@ -86,6 +92,17 @@ export class App { } } + /** + * @private + */ + setScrollDisabled(disabled: boolean) { + if (!this.appRoot) { + console.error('appRoot is missing, scrolling can not be enabled/disabled'); + return; + } + this.appRoot.disableScroll = disabled; + } + /** * @private * Boolean if the app is actively enabled or not. @@ -179,13 +196,7 @@ export class App { // function used to climb up all parent nav controllers function navPop(nav: any): Promise { if (nav) { - if (nav.length && nav.length() > 1) { - // this nav controller has more than one view - // pop the current view on this nav and we're done here - console.debug('app, goBack pop nav'); - return nav.pop(); - - } else if (nav.previousTab) { + if (isTabs(nav)) { // FYI, using "nav instanceof Tabs" throws a Promise runtime error for whatever reason, idk // this is a Tabs container // see if there is a valid previous tab to go to @@ -195,6 +206,12 @@ export class App { nav.select(prevTab); return Promise.resolve(); } + + } else if (isNav(nav) && nav.length() > 1) { + // this nav controller has more than one view + // pop the current view on this nav and we're done here + console.debug('app, goBack pop nav'); + return nav.pop(); } // try again using the parent nav (if there is one) @@ -228,10 +245,9 @@ export class App { console.debug('app, goBack exitApp'); this._platform.exitApp(); } - - } else { - return navPromise; } + + return navPromise; } return Promise.resolve(); @@ -282,9 +298,13 @@ export class AppRoot { @ViewChild('anchor', {read: ViewContainerRef}) private _viewport: ViewContainerRef; constructor( - private _cmp: UserComponent, - private _cr: ComponentResolver, - private _renderer: Renderer) {} + private _cmp: UserComponent, + private _cr: ComponentResolver, + private _renderer: Renderer, + app: App + ) { + app.appRoot = this; + } ngAfterViewInit() { // load the user app's root component diff --git a/src/components/app/structure.scss b/src/components/app/structure.scss index 0c81a2465a..fa9e3583e4 100644 --- a/src/components/app/structure.scss +++ b/src/components/app/structure.scss @@ -219,7 +219,6 @@ ion-content.js-scroll > scroll-content { [nav-viewport], [nav-portal], -[tab-portal], .nav-decor { display: none; } diff --git a/src/components/app/test/animations/index.ts b/src/components/app/test/animations/index.ts index 49f43d2716..abaacd6699 100644 --- a/src/components/app/test/animations/index.ts +++ b/src/components/app/test/animations/index.ts @@ -6,8 +6,8 @@ import {ionicBootstrap, Config, Animation} from '../../../../../src'; templateUrl: 'main.html' }) class E2EPage { - duration; - easing; + duration: string; + easing: string; constructor(config: Config) { this.duration = '1000'; diff --git a/src/components/app/test/app.spec.ts b/src/components/app/test/app.spec.ts index 64442d3c3a..4a429bf55f 100644 --- a/src/components/app/test/app.spec.ts +++ b/src/components/app/test/app.spec.ts @@ -1,16 +1,16 @@ -import {Component} from '@angular/core'; -import {App, Nav, Tabs, Tab, NavOptions, Config, ViewController, Platform} from '../../../../src'; +import { Component } from '@angular/core'; +import { App, Config, Nav, NavOptions, Platform, Tab, Tabs, ViewController } from '../../../../src'; +import { mockNavController, mockTab, mockTabs } from '../../../../src/util/mock-providers'; export function run() { - describe('App', () => { describe('navPop', () => { it('should select the previous tab', () => { - let nav = mockNav(); - let portal = mockNav(); + let nav = mockNavController(); + let portal = mockNavController(); app.setPortal(portal); app.setRootNav(nav); @@ -40,8 +40,8 @@ describe('App', () => { }); it('should pop from the active tab, when tabs is nested is the root nav', () => { - let nav = mockNav(); - let portal = mockNav(); + let nav = mockNavController(); + let portal = mockNavController(); app.setPortal(portal); app.setRootNav(nav); @@ -91,9 +91,9 @@ describe('App', () => { }); it('should pop the root nav when nested nav has less than 2 views', () => { - let rootNav = mockNav(); - let nestedNav = mockNav(); - let portal = mockNav(); + let rootNav = mockNavController(); + let nestedNav = mockNavController(); + let portal = mockNavController(); app.setPortal(portal); rootNav.registerChildNav(nestedNav); nestedNav.parent = rootNav; @@ -120,9 +120,9 @@ describe('App', () => { }); it('should pop a view from the nested nav that has more than 1 view', () => { - let rootNav = mockNav(); - let nestedNav = mockNav(); - let portal = mockNav(); + let rootNav = mockNavController(); + let nestedNav = mockNavController(); + let portal = mockNavController(); app.setPortal(portal); app.setRootNav(rootNav); rootNav.registerChildNav(nestedNav); @@ -149,8 +149,8 @@ describe('App', () => { }); it('should pop the overlay in the portal of the root nav', () => { - let nav = mockNav(); - let portal = mockNav(); + let nav = mockNavController(); + let portal = mockNavController(); app.setPortal(portal); app.setRootNav(nav); @@ -173,8 +173,8 @@ describe('App', () => { }); it('should pop the second view in the root nav', () => { - let nav = mockNav(); - let portal = mockNav(); + let nav = mockNavController(); + let portal = mockNavController(); app.setPortal(portal); app.setRootNav(nav); @@ -194,8 +194,8 @@ describe('App', () => { }); it('should exit app when only one view in the root nav', () => { - let nav = mockNav(); - let portal = mockNav(); + let nav = mockNavController(); + let portal = mockNavController(); app.setPortal(portal); app.setRootNav(nav); @@ -217,8 +217,8 @@ describe('App', () => { }); it('should not exit app when only one view in the root nav, but navExitApp config set', () => { - let nav = mockNav(); - let portal = mockNav(); + let nav = mockNavController(); + let portal = mockNavController(); app.setPortal(portal); app.setRootNav(nav); @@ -242,8 +242,8 @@ describe('App', () => { }); it('should not go back if app is not enabled', () => { - let nav = mockNav(); - let portal = mockNav(); + let nav = mockNavController(); + let portal = mockNavController(); app.setPortal(portal); app.setRootNav(nav); @@ -276,7 +276,7 @@ describe('App', () => { describe('getActiveNav', () => { it('should get active NavController when using tabs with nested nav', () => { - let nav = mockNav(); + let nav = mockNavController(); app.setRootNav(nav); let tabs = mockTabs(); @@ -285,9 +285,9 @@ describe('App', () => { nav.registerChildNav(tabs); tab2.setSelected(true); - let nav2 = mockNav(); - let nav3 = mockNav(); - let nav4 = mockNav(); + let nav2 = mockNavController(); + let nav3 = mockNavController(); + let nav4 = mockNavController(); tab1.registerChildNav(nav4); tab2.registerChildNav(nav2); tab2.registerChildNav(nav3); @@ -296,7 +296,7 @@ describe('App', () => { }); it('should get active NavController when using tabs, nested in a root nav', () => { - let nav = mockNav(); + let nav = mockNavController(); app.setRootNav(nav); let tabs = mockTabs(); @@ -331,9 +331,9 @@ describe('App', () => { }); it('should get active NavController when nested 3 deep', () => { - let nav1 = mockNav(); - let nav2 = mockNav(); - let nav3 = mockNav(); + let nav1 = mockNavController(); + let nav2 = mockNavController(); + let nav3 = mockNavController(); app.setRootNav(nav1); nav1.registerChildNav(nav2); @@ -343,8 +343,8 @@ describe('App', () => { }); it('should get active NavController when nested 2 deep', () => { - let nav1 = mockNav(); - let nav2 = mockNav(); + let nav1 = mockNavController(); + let nav2 = mockNavController(); app.setRootNav(nav1); nav1.registerChildNav(nav2); @@ -352,13 +352,13 @@ describe('App', () => { }); it('should get active NavController when only one nav controller', () => { - let nav = mockNav(); + let nav = mockNavController(); app.setRootNav(nav); expect(app.getActiveNav()).toBe(nav); }); it('should set/get the root nav controller', () => { - let nav = mockNav(); + let nav = mockNavController(); app.setRootNav(nav); expect(app.getRootNav()).toBe(nav); }); @@ -443,40 +443,13 @@ describe('App', () => { var app: App; var config: Config; var platform: Platform; - var _cd: any; - - function mockNav(): Nav { - return new Nav(null, null, null, config, null, null, null, null, null, null); - } - - function mockTabs(): Tabs { - return new Tabs(null, null, null, config, null, null, null); - } - - function mockTab(parentTabs: Tabs): Tab { - var tab = new Tab(parentTabs, app, config, null, null, null, null, null, _cd, null); - parentTabs.add(tab); - tab.root = SomePage; - tab.load = function(opts: any, cb: Function) { - cb(); - }; - return tab; - } - - @Component({}) - class SomePage {} beforeEach(() => { config = new Config(); platform = new Platform(); app = new App(config, platform); - _cd = { - reattach: function(){}, - detach: function(){} - }; }); }); - } diff --git a/src/components/app/test/gesture-collision/e2e.ts b/src/components/app/test/gesture-collision/e2e.ts new file mode 100644 index 0000000000..e69de29bb2 diff --git a/src/components/app/test/gesture-collision/index.ts b/src/components/app/test/gesture-collision/index.ts new file mode 100644 index 0000000000..9ca007dab0 --- /dev/null +++ b/src/components/app/test/gesture-collision/index.ts @@ -0,0 +1,70 @@ +import { Component, ViewChild } from '@angular/core'; +import { ionicBootstrap, MenuController, NavController, AlertController, Nav, Refresher } from '../../../../../src'; + + +@Component({ + templateUrl: 'page1.html' +}) +class Page1 { + constructor(private nav: NavController, private alertCtrl: AlertController) {} + + presentAlert() { + let alert = this.alertCtrl.create({ + title: 'New Friend!', + message: 'Your friend, Obi wan Kenobi, just accepted your friend request!', + cssClass: 'my-alert', + buttons: ['Ok'] + }); + alert.present(); + } + + goToPage1() { + this.nav.push(Page1); + } + + doRefresh(refresher: Refresher) { + setTimeout(() => { + refresher.complete(); + }, 1000); + } +} + + +@Component({ + templateUrl: 'main.html' +}) +class E2EPage { + rootPage: any; + changeDetectionCount: number = 0; + pages: Array<{title: string, component: any}>; + @ViewChild(Nav) nav: Nav; + + constructor(private menu: MenuController) { + this.rootPage = Page1; + + this.pages = [ + { title: 'Page 1', component: Page1 }, + { title: 'Page 2', component: Page1 }, + { title: 'Page 3', component: Page1 }, + ]; + } + + openPage(page: any) { + // Reset the content nav to have just this page + // we wouldn't want the back button to show in this scenario + this.nav.setRoot(page.component).then(() => { + // wait for the root page to be completely loaded + // then close the menu + this.menu.close(); + }); + } +} + +@Component({ + template: '' +}) +class E2EApp { + rootPage = E2EPage; +} + +ionicBootstrap(E2EApp); diff --git a/src/components/app/test/gesture-collision/main.html b/src/components/app/test/gesture-collision/main.html new file mode 100644 index 0000000000..98a7b6c443 --- /dev/null +++ b/src/components/app/test/gesture-collision/main.html @@ -0,0 +1,159 @@ + + + + + Left Menu + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Footer + + + + + + + + + + + Right Menu + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/components/app/test/gesture-collision/page1.html b/src/components/app/test/gesture-collision/page1.html new file mode 100644 index 0000000000..855443938c --- /dev/null +++ b/src/components/app/test/gesture-collision/page1.html @@ -0,0 +1,100 @@ + + + + + + + + Menu + + + + + + + + + + + + + + + + + +

Page 1

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + LEFT button + + + + + + + + RIGHT button + + + + + + + + + + RANGE + + + + + + SLIDING ITEM + RANGE + + + + + + + + + + + + + +
diff --git a/src/components/backdrop/backdrop.ts b/src/components/backdrop/backdrop.ts index 66abbed167..1193dd337e 100644 --- a/src/components/backdrop/backdrop.ts +++ b/src/components/backdrop/backdrop.ts @@ -1,6 +1,6 @@ import { Directive, ElementRef, Input } from '@angular/core'; -import { AppRoot } from '../app/app'; +import { DisableScroll, GestureController, GestureDelegate } from '../../gestures/gesture-controller'; import { isTrueProperty } from '../../util/util'; @@ -16,41 +16,21 @@ import { isTrueProperty } from '../../util/util'; }, }) export class Backdrop { - private static nuBackDrops: number = 0; - - private static push(appRoot: AppRoot) { - if (this.nuBackDrops === 0) { - appRoot.disableScroll = true; - } - this.nuBackDrops++; - } - - private static pop(appRoot: AppRoot) { - if (this.nuBackDrops > 0) { - this.nuBackDrops--; - - if (this.nuBackDrops === 0) { - appRoot.disableScroll = false; - } - } - } - - private pushed: boolean = false; + private _gestureID: number = null; @Input() disableScroll = true; - constructor(private _appRoot: AppRoot, private _elementRef: ElementRef) {} + constructor(private _gestureCtrl: GestureController, private _elementRef: ElementRef) {} ngOnInit() { if (isTrueProperty(this.disableScroll)) { - Backdrop.push(this._appRoot); - this.pushed = true; + this._gestureID = this._gestureCtrl.newID(); + this._gestureCtrl.disableScroll(this._gestureID); } } ngOnDestroy() { - if (this.pushed) { - Backdrop.pop(this._appRoot); - this.pushed = false; + if (this._gestureID) { + this._gestureCtrl.enableScroll(this._gestureID); } } diff --git a/src/components/content/content.ts b/src/components/content/content.ts index 37b4220338..a4e85e4103 100644 --- a/src/components/content/content.ts +++ b/src/components/content/content.ts @@ -524,7 +524,7 @@ export class Content extends Ion { ele = parentEle; let tabbarEle: HTMLElement; - while (ele && ele.tagName !== 'ION-MODAL' && !ele.classList.contains('tab-subpage')) { + while (ele && ele.tagName !== 'ION-MODAL') { if (ele.tagName === 'ION-TABS') { tabbarEle = ele.firstElementChild; diff --git a/src/components/datetime/datetime.ts b/src/components/datetime/datetime.ts index 1ed8e67137..796f762b44 100644 --- a/src/components/datetime/datetime.ts +++ b/src/components/datetime/datetime.ts @@ -8,7 +8,6 @@ import { Form } from '../../util/form'; import { Item } from '../item/item'; import { merge, isBlank, isPresent, isTrueProperty, isArray, isString } from '../../util/util'; import { dateValueRange, renderDateTime, renderTextFormat, convertFormatToKey, getValueFromFormat, parseTemplate, parseDate, updateDate, DateTimeData, convertDataToISO, daysInMonth, dateSortValue, dateDataSortValue, LocaleData } from '../../util/datetime-util'; -import { NavController } from '../nav/nav-controller'; export const DATETIME_VALUE_ACCESSOR = new Provider( NG_VALUE_ACCESSOR, {useExisting: forwardRef(() => DateTime), multi: true}); diff --git a/src/components/infinite-scroll/test/basic/index.ts b/src/components/infinite-scroll/test/basic/index.ts index 14268d3fdd..729d9e764c 100644 --- a/src/components/infinite-scroll/test/basic/index.ts +++ b/src/components/infinite-scroll/test/basic/index.ts @@ -7,7 +7,7 @@ import {ionicBootstrap, InfiniteScroll, NavController} from '../../../../../src' }) class E2EPage1 { @ViewChild(InfiniteScroll) infiniteScroll: InfiniteScroll; - items = []; + items: number[] = []; enabled: boolean = true; constructor(private nav: NavController) { @@ -68,7 +68,7 @@ function getAsyncData(): Promise { return new Promise(resolve => { setTimeout(() => { - let data = []; + let data: number[] = []; for (var i = 0; i < 30; i++) { data.push(i); } diff --git a/src/components/input/input-base.ts b/src/components/input/input-base.ts index f331d2bfa6..8a9c944e21 100644 --- a/src/components/input/input-base.ts +++ b/src/components/input/input-base.ts @@ -10,6 +10,7 @@ import { isTrueProperty } from '../../util/util'; import { Item } from '../item/item'; import { NativeInput, NextInput } from './native-input'; import { NavController } from '../nav/nav-controller'; +import { NavControllerBase } from '../nav/nav-controller-base'; import { Platform } from '../../platform/platform'; @@ -27,6 +28,7 @@ export class InputBase { protected _autoFocusAssist: string; protected _autoComplete: string; protected _autoCorrect: string; + protected _nav: NavControllerBase; inputControl: NgControl; @@ -44,9 +46,10 @@ export class InputBase { protected _platform: Platform, protected _elementRef: ElementRef, protected _scrollView: Content, - protected _nav: NavController, + nav: NavController, ngControl: NgControl ) { + this._nav = nav; this._useAssist = config.getBoolean('scrollAssist', false); this._usePadding = config.getBoolean('scrollPadding', this._useAssist); this._keyboardHeight = config.getNumber('keyboardHeight'); diff --git a/src/components/input/input.ios.scss b/src/components/input/input.ios.scss index cb1618a42e..58d180d6dc 100644 --- a/src/components/input/input.ios.scss +++ b/src/components/input/input.ios.scss @@ -16,6 +16,14 @@ $text-input-ios-input-clear-icon-color: rgba(0, 0, 0, .5) !default; $text-input-ios-input-clear-icon-svg: "" !default; $text-input-ios-input-clear-icon-size: 18px !default; +$text-input-ios-show-focus-highlight: false !default; +$text-input-ios-show-valid-highlight: $text-input-ios-show-focus-highlight !default; +$text-input-ios-show-invalid-highlight: $text-input-ios-show-focus-highlight !default; + +$text-input-ios-highlight-color: color($colors-ios, primary) !default; +$text-input-ios-highlight-color-valid: color($colors-ios, secondary) !default; +$text-input-ios-highlight-color-invalid: color($colors-ios, danger) !default; + // iOS Default Input // -------------------------------------------------- @@ -82,3 +90,61 @@ ion-input[clearInput] { background-size: $text-input-ios-input-clear-icon-size; } + + +// iOS Highlighted Input +// -------------------------------------------------- + +// Input highlight mixin for focus, valid, and invalid states +@mixin ios-input-highlight($highlight-color) { + border-bottom-color: $highlight-color; +} + +// Show the focus highlight when the input has focus +@if ($text-input-ios-show-focus-highlight) { + // In order to get a 2px border we need to add an inset + // box-shadow 1px (this is to avoid the div resizing) + .item-input.input-has-focus .item-inner { + @include ios-input-highlight($text-input-ios-highlight-color); + } + + // The last item in a list has a border on the item, not the + // inner item, so add it to the item itself + ion-list .item-input.input-has-focus:last-child { + @include ios-input-highlight($text-input-ios-highlight-color); + + .item-inner { + box-shadow: none; + } + } +} + +// Show the valid highlight when it has the .ng-valid class and a value +@if ($text-input-ios-show-valid-highlight) { + .item-input.ng-valid.input-has-value:not(.input-has-focus) .item-inner { + @include ios-input-highlight($text-input-ios-highlight-color-valid); + } + + ion-list .item-input.ng-valid.input-has-value:not(.input-has-focus):last-child { + @include ios-input-highlight($text-input-ios-highlight-color-valid); + + .item-inner { + box-shadow: none; + } + } +} + +// Show the invalid highlight when it has the invalid class and has been touched +@if ($text-input-ios-show-invalid-highlight) { + .item-input.ng-invalid.ng-touched:not(.input-has-focus) .item-inner { + @include ios-input-highlight($text-input-ios-highlight-color-invalid); + } + + ion-list .item-input.ng-invalid.ng-touched:not(.input-has-focus):last-child { + @include ios-input-highlight($text-input-ios-highlight-color-invalid); + + .item-inner { + box-shadow: none; + } + } +} diff --git a/src/components/input/input.md.scss b/src/components/input/input.md.scss index 20762dcfc1..32d647e7b6 100644 --- a/src/components/input/input.md.scss +++ b/src/components/input/input.md.scss @@ -5,9 +5,6 @@ // -------------------------------------------------- $text-input-md-background-color: $list-md-background-color !default; -$text-input-md-highlight-color: color($colors-md, primary) !default; -$text-input-md-hightlight-color-valid: color($colors-md, secondary) !default; -$text-input-md-hightlight-color-invalid: color($colors-md, danger) !default; $text-input-md-margin-top: $item-md-padding-top !default; $text-input-md-margin-right: ($item-md-padding-right / 2) !default; @@ -19,8 +16,13 @@ $text-input-md-input-clear-icon-color: #5b5b5b !default; $text-input-md-input-clear-icon-svg: "" !default; $text-input-md-input-clear-icon-size: 22px !default; -$text-input-md-show-success-highlight: true !default; -$text-input-md-show-error-highlight: true !default; +$text-input-md-show-focus-highlight: true !default; +$text-input-md-show-valid-highlight: $text-input-md-show-focus-highlight !default; +$text-input-md-show-invalid-highlight: $text-input-md-show-focus-highlight !default; + +$text-input-md-highlight-color: color($colors-md, primary) !default; +$text-input-md-highlight-color-valid: color($colors-md, secondary) !default; +$text-input-md-highlight-color-invalid: color($colors-md, danger) !default; // Material Design Default Input @@ -46,42 +48,57 @@ $text-input-md-show-error-highlight: true !default; // Material Design Highlighted Input // -------------------------------------------------- -.item-input::after { - position: absolute; - right: 0; - bottom: 0; - left: $item-md-padding-left; - - border-bottom-width: 2px; - border-bottom-style: solid; - border-bottom-color: transparent; - content: ""; +// Input highlight mixin for focus, valid, and invalid states +@mixin md-input-highlight($highlight-color) { + border-bottom-color: $highlight-color; + box-shadow: inset 0 -1px 0 0 $highlight-color; } -.item-input.input-has-focus::after { - border-bottom-color: $text-input-md-highlight-color; -} +// Show the focus highlight when the input has focus +@if ($text-input-md-show-focus-highlight) { + // In order to get a 2px border we need to add an inset + // box-shadow 1px (this is to avoid the div resizing) + .item-input.input-has-focus .item-inner { + @include md-input-highlight($text-input-md-highlight-color); + } -@if($text-input-md-show-success-highlight) { - .item-input.ng-valid.input-has-value { - &::after { - border-bottom-color: $text-input-md-hightlight-color-valid; - } + // The last item in a list has a border on the item, not the + // inner item, so add it to the item itself + ion-list .item-input.input-has-focus:last-child { + @include md-input-highlight($text-input-md-highlight-color); - &.input-has-focus::after { - border-bottom-color: $text-input-md-highlight-color; + .item-inner { + box-shadow: none; } } } -@if($text-input-md-show-error-highlight) { - .item-input.ng-invalid.ng-touched { - &::after { - border-bottom-color: $text-input-md-hightlight-color-invalid; - } +// Show the valid highlight when it has the .ng-valid class and a value +@if ($text-input-md-show-valid-highlight) { + .item-input.ng-valid.input-has-value:not(.input-has-focus) .item-inner { + @include md-input-highlight($text-input-md-highlight-color-valid); + } - &.input-has-focus::after { - border-bottom-color: $text-input-md-highlight-color; + ion-list .item-input.ng-valid.input-has-value:not(.input-has-focus):last-child { + @include md-input-highlight($text-input-md-highlight-color-valid); + + .item-inner { + box-shadow: none; + } + } +} + +// Show the invalid highlight when it has the invalid class and has been touched +@if ($text-input-md-show-invalid-highlight) { + .item-input.ng-invalid.ng-touched:not(.input-has-focus) .item-inner { + @include md-input-highlight($text-input-md-highlight-color-invalid); + } + + ion-list .item-input.ng-invalid.ng-touched:not(.input-has-focus):last-child { + @include md-input-highlight($text-input-md-highlight-color-invalid); + + .item-inner { + box-shadow: none; } } } diff --git a/src/components/input/input.wp.scss b/src/components/input/input.wp.scss index 53e77ec959..9ecbefaa69 100644 --- a/src/components/input/input.wp.scss +++ b/src/components/input/input.wp.scss @@ -16,15 +16,18 @@ $text-input-wp-padding-vertical: 0 !default; $text-input-wp-padding-horizontal: 8px !default; $text-input-wp-line-height: 3rem !default; -$text-input-wp-highlight-color: color($colors-wp, primary) !default; -$text-input-wp-hightlight-color-valid: color($colors-wp, secondary) !default; -$text-input-wp-hightlight-color-invalid: color($colors-wp, danger) !default; - $text-input-wp-input-clear-icon-width: 30px !default; $text-input-wp-input-clear-icon-color: $input-wp-border-color !default; $text-input-wp-input-clear-icon-svg: "" !default; $text-input-wp-input-clear-icon-size: 22px !default; +$text-input-wp-show-focus-highlight: true !default; +$text-input-wp-show-valid-highlight: $text-input-wp-show-focus-highlight !default; +$text-input-wp-show-invalid-highlight: $text-input-wp-show-focus-highlight !default; + +$text-input-wp-highlight-color: color($colors-wp, primary) !default; +$text-input-wp-highlight-color-valid: color($colors-wp, secondary) !default; +$text-input-wp-highlight-color-invalid: color($colors-wp, danger) !default; // Windows Default Input @@ -53,16 +56,25 @@ $text-input-wp-input-clear-icon-size: 22px !default; // Windows Highlighted Input // -------------------------------------------------- -.input-has-focus .text-input { - border-color: $text-input-wp-highlight-color; +// Show the focus highlight when the input has focus +@if ($text-input-wp-show-focus-highlight) { + .item-input.input-has-focus .text-input { + border-color: $text-input-wp-highlight-color; + } } -ion-input.ng-valid.input-has-value .text-input { - border-color: $text-input-wp-hightlight-color-valid; +// Show the valid highlight when it has the .ng-valid class and a value +@if ($text-input-wp-show-valid-highlight) { + .item-input.ng-valid.input-has-value:not(.input-has-focus) .text-input { + border-color: $text-input-wp-highlight-color-valid; + } } -ion-input.ng-invalid.ng-touched .text-input { - border-color: $text-input-wp-hightlight-color-invalid; +// Show the invalid highlight when it has the invalid class and has been touched +@if ($text-input-wp-show-invalid-highlight) { + .item-input.ng-invalid.ng-touched:not(.input-has-focus) .text-input { + border-color: $text-input-wp-highlight-color-invalid; + } } diff --git a/src/components/input/test/highlight/e2e.ts b/src/components/input/test/highlight/e2e.ts new file mode 100644 index 0000000000..e69de29bb2 diff --git a/src/components/input/test/highlight/index.ts b/src/components/input/test/highlight/index.ts new file mode 100644 index 0000000000..3577828851 --- /dev/null +++ b/src/components/input/test/highlight/index.ts @@ -0,0 +1,54 @@ +import { Component } from '@angular/core'; +import { FormBuilder, Validators } from '@angular/common'; +import { ionicBootstrap } from '../../../../../src'; + + +@Component({ + templateUrl: 'main.html' +}) +class E2EPage { + loginForm: any; + + login = { + email: 'help@ionic.io', + username: 'admin', + }; + + submitted: boolean = false; + + constructor(fb: FormBuilder) { + this.loginForm = fb.group({ + email: ["", Validators.compose([ + Validators.required, + this.emailValidator + ])], + username: [""], + password: ["", Validators.required], + comments: ["", Validators.required], + inset: ["", Validators.required] + }); + } + + emailValidator(control: any) { + var EMAIL_REGEXP = /^[a-z0-9!#$%&'*+\/=?^_`{|}~.-]+@[a-z0-9]([a-z0-9-]*[a-z0-9])?(\.[a-z0-9]([a-z0-9-]*[a-z0-9])?)*$/i; + + if (!EMAIL_REGEXP.test(control.value)) { + return {invalidEmail: true}; + } + } + + submit(ev: UIEvent, value: any) { + console.log("Submitted", value); + this.submitted = true; + } + +} + +@Component({ + template: '' +}) +class E2EApp { + root = E2EPage; +} + +ionicBootstrap(E2EApp); diff --git a/src/components/input/test/highlight/main.html b/src/components/input/test/highlight/main.html new file mode 100644 index 0000000000..1c0e267c04 --- /dev/null +++ b/src/components/input/test/highlight/main.html @@ -0,0 +1,105 @@ + + + + Form Inputs + + + + + + +
+ + + Stacked + + + + + Floating + + + + + Fixed + + + + + Inline + Comment value + + + + Inset + + + + + + + + Stacked + + + + + + Floating + + + + + + Fixed + + + + + + Inline + Comment value + + + + + Inset + + + + + + + + + Stacked + + + + + + Floating + + + + + + Fixed + + + + + + Inline + Comment value + + + + + Inset + + + + +
+
diff --git a/src/components/item/item-reorder-gesture.ts b/src/components/item/item-reorder-gesture.ts index c6bd82c013..0fb287cb24 100644 --- a/src/components/item/item-reorder-gesture.ts +++ b/src/components/item/item-reorder-gesture.ts @@ -25,12 +25,13 @@ export class ItemReorderGesture { private events: UIEventManager = new UIEventManager(false); - constructor(public list: ItemReorder) { - let element = this.list.getNativeElement(); - this.events.pointerEvents(element, - this.onDragStart.bind(this), - this.onDragMove.bind(this), - this.onDragEnd.bind(this)); + constructor(public reorderList: ItemReorder) { + this.events.pointerEvents({ + element: this.reorderList.getNativeElement(), + pointerDown: this.onDragStart.bind(this), + pointerMove: this.onDragMove.bind(this), + pointerUp: this.onDragEnd.bind(this) + }); } private onDragStart(ev: any): boolean { @@ -44,7 +45,7 @@ export class ItemReorderGesture { console.error('ion-reorder does not contain $ionComponent'); return false; } - this.list.reorderPrepare(); + this.reorderList.reorderPrepare(); let item = reorderMark.getReorderNode(); if (!item) { @@ -60,13 +61,13 @@ export class ItemReorderGesture { this.lastToIndex = indexForItem(item); this.windowHeight = window.innerHeight - AUTO_SCROLL_MARGIN; - this.lastScrollPosition = this.list.scrollContent(0); + this.lastScrollPosition = this.reorderList.scrollContent(0); this.offset = pointerCoord(ev); this.offset.y += this.lastScrollPosition; item.classList.add(ITEM_REORDER_ACTIVE); - this.list.reorderStart(); + this.reorderList.reorderStart(); return true; } @@ -94,7 +95,7 @@ export class ItemReorderGesture { this.lastToIndex = toIndex; this.lastYcoord = posY; this.emptyZone = false; - this.list.reorderMove(fromIndex, toIndex, this.selectedItemHeight); + this.reorderList.reorderMove(fromIndex, toIndex, this.selectedItemHeight); } } else { this.emptyZone = true; @@ -125,7 +126,7 @@ export class ItemReorderGesture { } else { reorderInactive(); } - this.list.reorderEmit(fromIndex, toIndex); + this.reorderList.reorderEmit(fromIndex, toIndex); } private itemForCoord(coord: Coordinates): HTMLElement { @@ -134,9 +135,9 @@ export class ItemReorderGesture { private scroll(posY: number): number { if (posY < AUTO_SCROLL_MARGIN) { - this.lastScrollPosition = this.list.scrollContent(-SCROLL_JUMP); + this.lastScrollPosition = this.reorderList.scrollContent(-SCROLL_JUMP); } else if (posY > this.windowHeight) { - this.lastScrollPosition = this.list.scrollContent(SCROLL_JUMP); + this.lastScrollPosition = this.reorderList.scrollContent(SCROLL_JUMP); } return this.lastScrollPosition; } @@ -148,17 +149,11 @@ export class ItemReorderGesture { this.onDragEnd(); this.events.unlistenAll(); this.events = null; - this.list = null; + this.reorderList = null; } } function itemForPosition(x: number, y: number): HTMLElement { let element = document.elementFromPoint(x, y); - if (!element) { - return null; - } - if (element.nodeName !== 'ION-ITEM' && !element.hasAttribute('ion-item')) { - return null; - } return findReorderItem(element); } diff --git a/src/components/item/item-sliding-gesture.ts b/src/components/item/item-sliding-gesture.ts index 7ddde117cd..f2d5d86bb1 100644 --- a/src/components/item/item-sliding-gesture.ts +++ b/src/components/item/item-sliding-gesture.ts @@ -1,111 +1,102 @@ -import {DragGesture} from '../../gestures/drag-gesture'; -import {ItemSliding} from './item-sliding'; -import {List} from '../list/list'; +import { ItemSliding } from './item-sliding'; +import { List } from '../list/list'; -import {closest} from '../../util/dom'; +import { closest, Coordinates, pointerCoord } from '../../util/dom'; +import { PointerEvents, UIEventManager } from '../../util/ui-event-manager'; +import { GestureDelegate, GestureOptions, GesturePriority } from '../../gestures/gesture-controller'; +import { PanGesture } from '../../gestures/drag-gesture'; -const DRAG_THRESHOLD = 20; +const DRAG_THRESHOLD = 10; const MAX_ATTACK_ANGLE = 20; -export class ItemSlidingGesture extends DragGesture { - onTap: any; - selectedContainer: ItemSliding = null; - openContainer: ItemSliding = null; +export class ItemSlidingGesture extends PanGesture { + private preSelectedContainer: ItemSliding = null; + private selectedContainer: ItemSliding = null; + private openContainer: ItemSliding = null; + private firstCoordX: number; + private firstTimestamp: number; constructor(public list: List) { super(list.getNativeElement(), { - direction: 'x', - threshold: DRAG_THRESHOLD + maxAngle: MAX_ATTACK_ANGLE, + threshold: DRAG_THRESHOLD, + gesture: list.gestureCtrl.create('item-sliding', { + priority: GesturePriority.SlidingItem, + }) }); - this.listen(); } - onTapCallback(ev: any) { - if (isFromOptionButtons(ev)) { - return; + canStart(ev: any): boolean { + if (this.selectedContainer) { + return false; } - let didClose = this.closeOpened(); - if (didClose) { - console.debug('tap close sliding item, preventDefault'); - ev.preventDefault(); - } - } - - onDragStart(ev: any): boolean { - let angle = Math.abs(ev.angle); - if (angle > MAX_ATTACK_ANGLE && Math.abs(angle - 180) > MAX_ATTACK_ANGLE) { + // Get swiped sliding container + let container = getContainer(ev); + if (!container) { this.closeOpened(); return false; } - - if (this.selectedContainer) { - console.debug('onDragStart, another container is already selected'); - return false; - } - - let container = getContainer(ev); - if (!container) { - console.debug('onDragStart, no itemContainerEle'); - return false; - } - // Close open container if it is not the selected one. if (container !== this.openContainer) { this.closeOpened(); } - this.selectedContainer = container; - this.openContainer = container; - container.startSliding(ev.center.x); - + let coord = pointerCoord(ev); + this.preSelectedContainer = container; + this.firstCoordX = coord.x; + this.firstTimestamp = Date.now(); return true; } - onDrag(ev: any): boolean { - if (this.selectedContainer) { - this.selectedContainer.moveSliding(ev.center.x); - ev.preventDefault(); - } - return; + onDragStart(ev: any) { + ev.preventDefault(); + + let coord = pointerCoord(ev); + this.selectedContainer = this.openContainer = this.preSelectedContainer; + this.selectedContainer.startSliding(coord.x); + } + + onDragMove(ev: any) { + ev.preventDefault(); + + let coordX = pointerCoord(ev).x; + this.selectedContainer.moveSliding(coordX); } onDragEnd(ev: any) { - if (!this.selectedContainer) { - return; - } ev.preventDefault(); - let openAmount = this.selectedContainer.endSliding(ev.velocityX); + let coordX = pointerCoord(ev).x; + let deltaX = (coordX - this.firstCoordX); + let deltaT = (Date.now() - this.firstTimestamp); + let openAmount = this.selectedContainer.endSliding(deltaX / deltaT); this.selectedContainer = null; + this.preSelectedContainer = null; + } - // TODO: I am not sure listening for a tap event is the best idea - // we should try mousedown/touchstart - if (openAmount === 0) { - this.openContainer = null; - this.off('tap', this.onTap); - this.onTap = null; - } else if (!this.onTap) { - this.onTap = (event: any) => this.onTapCallback(event); - this.on('tap', this.onTap); - } + notCaptured(ev: any) { + this.closeOpened(); } closeOpened(): boolean { - if (!this.openContainer) { - return false; - } - this.openContainer.close(); - this.openContainer = null; this.selectedContainer = null; - this.off('tap', this.onTap); - this.onTap = null; - return true; + + if (this.openContainer) { + this.openContainer.close(); + this.openContainer = null; + return true; + } + return false; } - unlisten() { + destroy() { + super.destroy(); this.closeOpened(); - super.unlisten(); + this.list = null; + this.preSelectedContainer = null; + this.selectedContainer = null; + this.openContainer = null; } } @@ -115,12 +106,4 @@ function getContainer(ev: any): ItemSliding { return (ele)['$ionComponent']; } return null; -} - -function isFromOptionButtons(ev: any): boolean { - let button = closest(ev.target, '.button', true); - if (!button) { - return false; - } - return !!closest(button, 'ion-item-options', true); -} +} \ No newline at end of file diff --git a/src/components/item/item-sliding.ts b/src/components/item/item-sliding.ts index 652951b7bc..833e79f4d9 100644 --- a/src/components/item/item-sliding.ts +++ b/src/components/item/item-sliding.ts @@ -5,7 +5,7 @@ import { Item } from './item'; import { isPresent } from '../../util/util'; import { List } from '../list/list'; -const SWIPE_MARGIN = 20; +const SWIPE_MARGIN = 30; const ELASTIC_FACTOR = 0.55; export const enum ItemSideFlags { diff --git a/src/components/item/test/groups/index.ts b/src/components/item/test/groups/index.ts index 602d692cd5..9ea4809344 100644 --- a/src/components/item/test/groups/index.ts +++ b/src/components/item/test/groups/index.ts @@ -1,5 +1,5 @@ -import {Component} from '@angular/core'; -import {ionicBootstrap, NavController, NavParams} from '../../../../../src'; +import { Component } from '@angular/core'; +import { ionicBootstrap, NavController, NavParams } from '../../../../../src'; @Component({ diff --git a/src/components/list/list.ts b/src/components/list/list.ts index 06cdd3d88a..1e92ec822f 100644 --- a/src/components/list/list.ts +++ b/src/components/list/list.ts @@ -4,6 +4,7 @@ import { Content } from '../content/content'; import { Ion } from '../ion'; import { isTrueProperty } from '../../util/util'; import { ItemSlidingGesture } from '../item/item-sliding-gesture'; +import { GestureController } from '../../gestures/gesture-controller'; /** * The List is a widely used interface element in almost any mobile app, @@ -29,7 +30,10 @@ export class List extends Ion { private _containsSlidingItems: boolean = false; private _slidingGesture: ItemSlidingGesture; - constructor(elementRef: ElementRef, private _rendered: Renderer) { + constructor( + elementRef: ElementRef, + private _rendered: Renderer, + public gestureCtrl: GestureController) { super(elementRef); } @@ -78,16 +82,17 @@ export class List extends Ion { this._updateSlidingState(); } - + private _updateSlidingState() { let shouldSlide = this._enableSliding && this._containsSlidingItems; if (!shouldSlide) { - this._slidingGesture && this._slidingGesture.unlisten(); + this._slidingGesture && this._slidingGesture.destroy(); this._slidingGesture = null; } else if (!this._slidingGesture) { console.debug('enableSlidingItems'); this._slidingGesture = new ItemSlidingGesture(this); + this._slidingGesture.listen(); } } diff --git a/src/components/menu/menu-controller.ts b/src/components/menu/menu-controller.ts index bf0178fdcf..6c796973ef 100644 --- a/src/components/menu/menu-controller.ts +++ b/src/components/menu/menu-controller.ts @@ -125,6 +125,10 @@ export class MenuController { open(menuId?: string): Promise { let menu = this.get(menuId); if (menu) { + let openedMenu = this.getOpen(); + if (openedMenu && menu !== openedMenu) { + openedMenu.setOpen(false, false); + } return menu.open(); } @@ -147,7 +151,7 @@ export class MenuController { } else { // find the menu that is open - menu = this._menus.find(m => m.isOpen); + menu = this.getOpen(); } if (menu) { @@ -158,11 +162,6 @@ export class MenuController { return Promise.resolve(false); } - tempDisable(temporarilyDisable: boolean) { - this._menus.forEach(menu => { - menu.tempDisable(temporarilyDisable); - }); - } /** * Toggle the menu. If it's closed, it will open, and if opened, it @@ -173,6 +172,10 @@ export class MenuController { toggle(menuId?: string): Promise { let menu = this.get(menuId); if (menu) { + let openedMenu = this.getOpen(); + if (openedMenu && menu !== openedMenu) { + openedMenu.setOpen(false, false); + } return menu.toggle(); } return Promise.resolve(false); @@ -229,7 +232,7 @@ export class MenuController { * provided, then it'll try to find the menu using the menu's `id` * property. If a menu is not found then it'll return `null`. * @param {string} [menuId] Optionally get the menu by its id, or side. - * @return {Menu} Returns the instance of the menu if found, otherwise `null`. + * @return {Menu} Returns the instance of the menu if found, otherwise `null`. */ get(menuId?: string): Menu { var menu: Menu; @@ -252,12 +255,21 @@ export class MenuController { // return the first enabled menu menu = this._menus.find(m => m.enabled); - if (menu) return menu; + if (menu) { + return menu; + } // get the first menu in the array, if one exists return (this._menus.length ? this._menus[0] : null); } + /** + * @return {Menu} Returns the instance of the menu already opened, otherwise `null`. + */ + getOpen(): Menu { + return this._menus.find(m => m.isOpen); + } + /** * @return {Array} Returns an array of all menu instances. diff --git a/src/components/menu/menu-gestures.ts b/src/components/menu/menu-gestures.ts index f8066d990f..b9b277c32a 100644 --- a/src/components/menu/menu-gestures.ts +++ b/src/components/menu/menu-gestures.ts @@ -1,8 +1,8 @@ -import {Menu} from './menu'; -import {SlideEdgeGesture} from '../../gestures/slide-edge-gesture'; -import {SlideData} from '../../gestures/slide-gesture'; -import {assign} from '../../util/util'; - +import { Menu } from './menu'; +import { SlideEdgeGesture } from '../../gestures/slide-edge-gesture'; +import { SlideData } from '../../gestures/slide-gesture'; +import { assign } from '../../util/util'; +import { GesturePriority } from '../../gestures/gesture-controller'; /** * Gesture attached to the content which the menu is assigned to @@ -10,61 +10,29 @@ import {assign} from '../../util/util'; export class MenuContentGesture extends SlideEdgeGesture { constructor(public menu: Menu, contentEle: HTMLElement, options: any = {}) { - super(contentEle, assign({ direction: 'x', edge: menu.side, threshold: 0, - maxEdgeStart: menu.maxEdgeStart || 75 + maxEdgeStart: menu.maxEdgeStart || 50, + maxAngle: 40, + gesture: menu.gestureCtrl.create('menu-swipe', { + priority: GesturePriority.MenuSwipe, + }) }, options)); } - canStart(ev: any) { + canStart(ev: any): boolean { let menu = this.menu; - if (!menu.enabled || !menu.swipeEnabled) { - console.debug('menu can not start, isEnabled:', menu.enabled, 'isSwipeEnabled:', menu.swipeEnabled, 'side:', menu.side); return false; } - - if (ev.distance > 50) { - // the distance is longer than you'd expect a side menu swipe to be - console.debug('menu can not start, distance too far:', ev.distance, 'side:', menu.side); + if (menu.isOpen) { + return true; + } else if (menu.getMenuController().getOpen()) { return false; } - - console.debug('menu canStart,', menu.side, 'isOpen', menu.isOpen, 'angle', ev.angle, 'distance', ev.distance); - - if (menu.side === 'right') { - // right side - if (menu.isOpen) { - // right side, opened - return true; - - } else { - // right side, closed - if ((ev.angle > 140 && ev.angle <= 180) || (ev.angle > -140 && ev.angle <= -180)) { - return super.canStart(ev); - } - } - - } else { - // left side - if (menu.isOpen) { - // left side, opened - return true; - - } else { - // left side, closed - if (ev.angle > -40 && ev.angle < 40) { - return super.canStart(ev); - } - } - - } - - // didn't pass the test, don't open this menu - return false; + return super.canStart(ev); } // Set CSS, then wait one frame for it to apply before sliding starts @@ -77,7 +45,6 @@ export class MenuContentGesture extends SlideEdgeGesture { let z = (this.menu.side === 'right' ? slide.min : slide.max); let stepValue = (slide.distance / z); console.debug('menu gesture, onSlide', this.menu.side, 'distance', slide.distance, 'min', slide.min, 'max', slide.max, 'z', z, 'stepValue', stepValue); - ev.srcEvent.preventDefault(); ev.preventDefault(); this.menu.swipeProgress(stepValue); } @@ -85,25 +52,24 @@ export class MenuContentGesture extends SlideEdgeGesture { onSlideEnd(slide: SlideData, ev: any) { let z = (this.menu.side === 'right' ? slide.min : slide.max); let currentStepValue = (slide.distance / z); - + let velocity = slide.velocity; z = Math.abs(z * 0.5); - let shouldCompleteRight = (ev.velocityX >= 0) - && (ev.velocityX > 0.2 || slide.delta > z); - - let shouldCompleteLeft = (ev.velocityX <= 0) - && (ev.velocityX < -0.2 || slide.delta < -z); - + let shouldCompleteRight = (velocity >= 0) + && (velocity > 0.2 || slide.delta > z); + + let shouldCompleteLeft = (velocity <= 0) + && (velocity < -0.2 || slide.delta < -z); + console.debug( 'menu gesture, onSlide', this.menu.side, 'distance', slide.distance, 'delta', slide.delta, - 'velocityX', ev.velocityX, + 'velocity', velocity, 'min', slide.min, 'max', slide.max, 'shouldCompleteLeft', shouldCompleteLeft, 'shouldCompleteRight', shouldCompleteRight, 'currentStepValue', currentStepValue); - this.menu.swipeEnd(shouldCompleteLeft, shouldCompleteRight, currentStepValue); } @@ -134,14 +100,3 @@ export class MenuContentGesture extends SlideEdgeGesture { } } - -/** - * Gesture attached to the actual menu itself - */ -export class MenuTargetGesture extends MenuContentGesture { - constructor(menu: Menu, menuEle: HTMLElement) { - super(menu, menuEle, { - maxEdgeStart: 0 - }); - } -} diff --git a/src/components/menu/menu-types.ts b/src/components/menu/menu-types.ts index 032d8f233b..f4ddb53891 100644 --- a/src/components/menu/menu-types.ts +++ b/src/components/menu/menu-types.ts @@ -15,11 +15,15 @@ export class MenuType { ani: Animation = new Animation(); isOpening: boolean; - setOpen(shouldOpen: boolean, done: Function) { - this.ani - .onFinish(done, true) - .reverse(!shouldOpen) - .play(); + setOpen(shouldOpen: boolean, animated: boolean, done: Function) { + let ani = this.ani + .onFinish(done, true) + .reverse(!shouldOpen); + if (animated) { + ani.play(); + } else { + ani.play({ duration: 0 }); + } } setProgressStart(isOpen: boolean) { diff --git a/src/components/menu/menu.ts b/src/components/menu/menu.ts index 119eb10006..69214542d9 100644 --- a/src/components/menu/menu.ts +++ b/src/components/menu/menu.ts @@ -5,10 +5,11 @@ import { Config } from '../../config/config'; import { Ion } from '../ion'; import { isTrueProperty } from '../../util/util'; import { Keyboard } from '../../util/keyboard'; -import { MenuContentGesture, MenuTargetGesture } from './menu-gestures'; +import { MenuContentGesture } from './menu-gestures'; import { MenuController } from './menu-controller'; import { MenuType } from './menu-types'; import { Platform } from '../../platform/platform'; +import { GestureController } from '../../gestures/gesture-controller'; /** @@ -190,15 +191,13 @@ import { Platform } from '../../platform/platform'; export class Menu extends Ion { private _preventTime: number = 0; private _cntEle: HTMLElement; - private _cntGesture: MenuTargetGesture; - private _menuGesture: MenuContentGesture; + private _cntGesture: MenuContentGesture; private _type: MenuType; private _resizeUnreg: Function; private _isEnabled: boolean = true; private _isSwipeEnabled: boolean = true; private _isPers: boolean = false; private _init: boolean = false; - private _prevEnabled: boolean; /** * @private @@ -302,7 +301,8 @@ export class Menu extends Ion { private _platform: Platform, private _renderer: Renderer, private _keyboard: Keyboard, - private _zone: NgZone + private _zone: NgZone, + public gestureCtrl: GestureController ) { super(_elementRef); } @@ -335,8 +335,7 @@ export class Menu extends Ion { self._renderer.setElementAttribute(self._elementRef.nativeElement, 'type', self.type); // add the gestures - self._cntGesture = new MenuContentGesture(self, self.getContentElement()); - self._menuGesture = new MenuTargetGesture(self, self.getNativeElement()); + self._cntGesture = new MenuContentGesture(self, document.body); // register listeners if this menu is enabled // check if more than one menu is on the same side @@ -387,16 +386,12 @@ export class Menu extends Ion { if (self._isEnabled && self._isSwipeEnabled && !self._cntGesture.isListening) { // should listen, but is not currently listening console.debug('menu, gesture listen', self.side); - self._zone.runOutsideAngular(function() { - self._cntGesture.listen(); - self._menuGesture.listen(); - }); + self._cntGesture.listen(); } else if (self._cntGesture.isListening && (!self._isEnabled || !self._isSwipeEnabled)) { // should not listen, but is currently listening console.debug('menu, gesture unlisten', self.side); self._cntGesture.unlisten(); - self._menuGesture.unlisten(); } } } @@ -418,7 +413,7 @@ export class Menu extends Ion { /** * @private */ - setOpen(shouldOpen: boolean): Promise { + setOpen(shouldOpen: boolean, animated: boolean = true): Promise { // _isPrevented is used to prevent unwanted opening/closing after swiping open/close // or swiping open the menu while pressing down on the MenuToggle button if ((shouldOpen && this.isOpen) || this._isPrevented()) { @@ -428,7 +423,7 @@ export class Menu extends Ion { this._before(); return new Promise(resolve => { - this._getType().setOpen(shouldOpen, () => { + this._getType().setOpen(shouldOpen, animated, () => { this._after(shouldOpen); resolve(this.isOpen); }); @@ -519,21 +514,6 @@ export class Menu extends Ion { } } - /** - * @private - */ - tempDisable(temporarilyDisable: boolean) { - if (temporarilyDisable) { - this._prevEnabled = this._isEnabled; - this._getType().setProgessStep(0); - this.enable(false); - - } else { - this.enable(this._prevEnabled); - this._after(false); - } - } - private _prevent() { // used to prevent unwanted opening/closing after swiping open/close // or swiping open the menu while pressing down on the MenuToggle @@ -617,13 +597,19 @@ export class Menu extends Ion { return this.backdrop.getNativeElement(); } + /** + * @private + */ + getMenuController(): MenuController { + return this._menuCtrl; + } + /** * @private */ ngOnDestroy() { this._menuCtrl.unregister(this); this._cntGesture && this._cntGesture.destroy(); - this._menuGesture && this._menuGesture.destroy(); this._type && this._type.destroy(); this._resizeUnreg && this._resizeUnreg(); this._cntEle = null; diff --git a/src/components/menu/test/basic/index.ts b/src/components/menu/test/basic/index.ts index f54f20fb58..43e823f4df 100644 --- a/src/components/menu/test/basic/index.ts +++ b/src/components/menu/test/basic/index.ts @@ -67,6 +67,14 @@ class E2EPage { }); } + openRightMenu() { + this.menu.open('right'); + } + + openLeftMenu() { + this.menu.open('left'); + } + onDrag(ev: any) { console.log('Menu is being dragged', ev); } diff --git a/src/components/menu/test/basic/main.html b/src/components/menu/test/basic/main.html index 3ce5cb6838..aa07376973 100644 --- a/src/components/menu/test/basic/main.html +++ b/src/components/menu/test/basic/main.html @@ -14,6 +14,10 @@ {{p.title}} + + @@ -90,6 +94,10 @@ {{p.title}} + + @@ -148,6 +156,6 @@ - +
diff --git a/src/components/menu/test/basic/page1.html b/src/components/menu/test/basic/page1.html index 036d3d4b6a..ae6905f827 100644 --- a/src/components/menu/test/basic/page1.html +++ b/src/components/menu/test/basic/page1.html @@ -35,9 +35,19 @@

Page 1

-

- -

+ + + + + + + + + + + + +

diff --git a/src/components/modal/test/basic/index.ts b/src/components/modal/test/basic/index.ts index 141c5d54e7..d14b32d029 100644 --- a/src/components/modal/test/basic/index.ts +++ b/src/components/modal/test/basic/index.ts @@ -1,12 +1,12 @@ import { Component, Injectable } from '@angular/core'; -import { ActionSheetController, Config, ionicBootstrap, ModalController, NavController, NavParams, PageTransition, Platform, TransitionOptions, ViewController } from '../../../../../src'; +import { ActionSheetController, App, Config, ionicBootstrap, ModalController, NavController, NavParams, PageTransition, Platform, TransitionOptions, ViewController } from '../../../../../src'; @Injectable() class SomeComponentProvider { constructor(private config: Config) { - console.log('SomeComponentProvider constructor') + console.log('SomeComponentProvider constructor'); } getName() { @@ -17,7 +17,7 @@ class SomeComponentProvider { @Injectable() class SomeAppProvider { constructor(private config: Config) { - console.log('SomeAppProvider constructor') + console.log('SomeAppProvider constructor'); } getData() { @@ -84,7 +84,7 @@ class E2EPage { } presentModalWithInputs() { - let modal = this.modalCtrl.create(ModalWithInputs); + let modal = this.modalCtrl.create(ModalWithInputs); modal.onDidDismiss((data: any) => { console.log('Modal with inputs data:', data); }); @@ -98,7 +98,7 @@ class E2EPage { }); } - presentNavigableModal(){ + presentNavigableModal() { this.modalCtrl.create(NavigableModal).present(); } } @@ -117,11 +117,11 @@ class E2EPage { ` }) class NavigableModal { - constructor(private navController:NavController) { + constructor(private nav: NavController) { } submit(){ - this.navController.push(NavigableModal2); + this.nav.push(NavigableModal2); } } @@ -139,10 +139,10 @@ class NavigableModal { ` }) class NavigableModal2 { - constructor(private navController:NavController) { + constructor(private navController: NavController) { } - submit(){ + submit() { this.navController.pop(); } } @@ -188,23 +188,23 @@ class ModalPassData { this.viewCtrl.dismiss(this.data); } - ionViewLoaded(){ + ionViewLoaded() { console.log('ModalPassData ionViewLoaded fired'); } - ionViewWillEnter(){ + ionViewWillEnter() { console.log('ModalPassData ionViewWillEnter fired'); } - ionViewDidEnter(){ + ionViewDidEnter() { console.log('ModalPassData ionViewDidEnter fired'); } - ionViewWillLeave(){ + ionViewWillLeave() { console.log('ModalPassData ionViewWillLeave fired'); } - ionViewDidLeave(){ + ionViewDidLeave() { console.log('ModalPassData ionViewDidLeave fired'); } } @@ -375,10 +375,10 @@ class ContactUs { }) class ModalFirstPage { - private items:any[]; - constructor(private nav: NavController, private actionSheetCtrl: ActionSheetController) { + private items: any[]; + constructor(private nav: NavController, private app: App, private actionSheetCtrl: ActionSheetController) { this.items = []; - for ( let i = 0; i < 50; i++ ){ + for ( let i = 0; i < 50; i++ ) { this.items.push({ value: (i + 1) }); @@ -387,24 +387,24 @@ class ModalFirstPage { push() { let page = ModalSecondPage; - let params = { id: 8675309, myData: [1,2,3,4] }; + let params = { id: 8675309, myData: [1, 2, 3, 4] }; this.nav.push(page, params); } dismiss() { - this.nav.rootNav.pop(); + this.app.getRootNav().pop(); } - ionViewLoaded(){ + ionViewLoaded() { console.log('ModalFirstPage ionViewLoaded fired'); } - ionViewWillEnter(){ + ionViewWillEnter() { console.log('ModalFirstPage ionViewWillEnter fired'); } - ionViewDidEnter(){ + ionViewDidEnter() { console.log('ModalFirstPage ionViewDidEnter fired'); } @@ -430,8 +430,8 @@ class ModalFirstPage { // overlays are added and removed from the root navigation // find the root navigation, and pop this alert // when the alert is done animating out, then pop off the modal - this.nav.rootNav.pop().then(() => { - this.nav.rootNav.pop(); + this.app.getRootNav().pop().then(() => { + this.app.getRootNav().pop(); }); // by default an alert will dismiss itself @@ -477,15 +477,15 @@ class ModalSecondPage { console.log('Second page params:', params); } - ionViewLoaded(){ + ionViewLoaded() { console.log('ModalSecondPage ionViewLoaded'); } - ionViewWillEnter(){ + ionViewWillEnter() { console.log('ModalSecondPage ionViewWillEnter'); } - ionViewDidEnter(){ + ionViewDidEnter() { console.log('ModalSecondPage ionViewDidEnter'); } } diff --git a/src/components/nav/nav-controller-base.ts b/src/components/nav/nav-controller-base.ts new file mode 100644 index 0000000000..5f68c8dafc --- /dev/null +++ b/src/components/nav/nav-controller-base.ts @@ -0,0 +1,1303 @@ +import { ComponentResolver, ElementRef, EventEmitter, NgZone, provide, ReflectiveInjector, Renderer, ViewContainerRef } from '@angular/core'; + +import { addSelector } from '../../config/bootstrap'; +import { App } from '../app/app'; +import { Config } from '../../config/config'; +import { GestureController } from '../../gestures/gesture-controller'; +import { Ion } from '../ion'; +import { isBlank, isPresent, pascalCaseToDashCase } from '../../util/util'; +import { Keyboard } from '../../util/keyboard'; +import { NavController } from './nav-controller'; +import { NavOptions, DIRECTION_BACK, DIRECTION_FORWARD } from './nav-interfaces'; +import { NavParams } from './nav-params'; +import { SwipeBackGesture } from './swipe-back'; +import { Transition } from '../../transitions/transition'; +import { ViewController } from './view-controller'; + + +/** + * This class is for internal use only. It is not exported publicly. + */ +export class NavControllerBase extends Ion implements NavController { + _transIds = 0; + _init = false; + _isPortal: boolean; + _trans: Transition; + _sbGesture: SwipeBackGesture; + _sbThreshold: number; + _viewport: ViewContainerRef; + _children: any[] = []; + _sbEnabled: boolean; + _ids: number = -1; + _trnsDelay: any; + _views: ViewController[] = []; + + viewDidLoad: EventEmitter; + viewWillEnter: EventEmitter; + viewDidEnter: EventEmitter; + viewWillLeave: EventEmitter; + viewDidLeave: EventEmitter; + viewWillUnload: EventEmitter; + viewDidUnload: EventEmitter; + + id: string; + parent: any; + config: Config; + trnsTime: number = 0; + + constructor( + parent: any, + public _app: App, + config: Config, + public _keyboard: Keyboard, + elementRef: ElementRef, + public _zone: NgZone, + public _renderer: Renderer, + public _compiler: ComponentResolver, + public _gestureCtrl: GestureController + ) { + super(elementRef); + + this.parent = parent; + this.config = config; + + this._trnsDelay = config.get('pageTransitionDelay'); + + this._sbEnabled = config.getBoolean('swipeBackEnabled'); + this._sbThreshold = config.getNumber('swipeBackThreshold', 40); + + this.id = 'n' + (++ctrlIds); + + this.viewDidLoad = new EventEmitter(); + this.viewWillEnter = new EventEmitter(); + this.viewDidEnter = new EventEmitter(); + this.viewWillLeave = new EventEmitter(); + this.viewDidLeave = new EventEmitter(); + this.viewWillUnload = new EventEmitter(); + this.viewDidUnload = new EventEmitter(); + } + + setViewport(val: ViewContainerRef) { + this._viewport = val; + } + + setRoot(page: any, params?: any, opts?: NavOptions): Promise { + return this.setPages([{page, params}], opts); + } + + setPages(pages: Array<{page: any, params?: any}>, opts?: NavOptions): Promise { + if (!pages || !pages.length) { + return Promise.resolve(false); + } + + if (isBlank(opts)) { + opts = {}; + } + + // remove existing views + let leavingView = this._remove(0, this._views.length); + + // create view controllers out of the pages and insert the new views + let views = pages.map(p => new ViewController(p.page, p.params)); + let enteringView = this._insert(0, views); + + // if animation wasn't set to true then default it to NOT animate + if (opts.animate !== true) { + opts.animate = false; + } + + // set the nav direction to "back" if it wasn't set + opts.direction = opts.direction || DIRECTION_BACK; + + let resolve: any; + let promise = new Promise(res => { resolve = res; }); + + // start the transition, fire resolve when done... + this._transition(enteringView, leavingView, opts, (hasCompleted: boolean) => { + // transition has completed!! + resolve(hasCompleted); + }); + + return promise; + } + + push(page: any, params?: any, opts?: NavOptions, done?: Function): Promise { + return this.insertPages(-1, [{page: page, params: params}], opts, done); + } + + /** + * DEPRECATED: Please use inject the overlays controller and use the present method on the instance instead. + */ + private present(enteringView: ViewController, opts?: NavOptions): Promise { + // deprecated warning: added beta.11 2016-06-27 + console.warn('nav.present() has been deprecated.\n' + + 'Please inject the overlay\'s controller and use the present method on the instance instead.'); + return Promise.resolve(); + } + + insert(insertIndex: number, page: any, params?: any, opts?: NavOptions, done?: Function): Promise { + return this.insertPages(insertIndex, [{page: page, params: params}], opts, done); + } + + insertPages(insertIndex: number, insertPages: Array<{page: any, params?: any}>, opts?: NavOptions, done?: Function): Promise { + let views = insertPages.map(p => new ViewController(p.page, p.params)); + return this.insertViews(insertIndex, views, opts, done); + } + + insertViews(insertIndex: number, insertViews: ViewController[], opts: NavOptions = {}, done?: Function) { + let promise: Promise; + if (!done) { + // only create a promise if a done callback wasn't provided + promise = new Promise(res => { done = res; }); + } + + if (!insertViews || !insertViews.length) { + done(false); + return promise; + } + + if (isBlank(opts)) { + opts = {}; + } + + // insert the new page into the stack + // returns the newly created entering view + let enteringView = this._insert(insertIndex, insertViews); + + // manually set the new view's id if an id was passed in the options + if (isPresent(opts.id)) { + enteringView.id = opts.id; + } + + // set the nav direction to "forward" if it wasn't set + opts.direction = opts.direction || 'forward'; + + // set which animation it should use if it wasn't set yet + if (!opts.animation) { + opts.animation = enteringView.getTransitionName(opts.direction); + } + + // it's possible that the newly added view doesn't need to + // transition in, but was simply inserted somewhere in the stack + // go backwards through the stack and find the first active view + // which could be active or one ready to enter + for (var i = this._views.length - 1; i >= 0; i--) { + if (this._views[i].state === STATE_ACTIVE || this._views[i].state === STATE_INIT_ENTER) { + // found the view at the end of the stack that's either + // already active or it is about to enter + + if (this._views[i] === enteringView) { + // cool, so the last valid view is also our entering view!! + // this means we should animate that bad boy in so it's the active view + // return a promise and resolve when the transition has completed + + // get the leaving view which the _insert() already set + let leavingView = this.getByState(STATE_INIT_LEAVE); + + // start the transition, fire resolve when done... + this._transition(enteringView, leavingView, opts, done); + return promise; + } + break; + } + } + + // the page was not pushed onto the end of the stack + // but rather inserted somewhere in the middle or beginning + // Since there are views after this new one, don't transition in + // auto resolve cuz there was is no need for an animation + done(enteringView); + + return promise; + } + + _insert(insertIndex: number, insertViews: ViewController[]): ViewController { + // when this is done, there should only be at most + // 1 STATE_INIT_ENTER and 1 STATE_INIT_LEAVE + // there should not be any that are STATE_ACTIVE after this is done + + // 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 (insertIndex < 0 || insertIndex > this._views.length) { + insertIndex = this._views.length; + } + + // first see if there's an active view + let view = this.getActive(); + if (view) { + // there's an active view, set that it's initialized to leave + view.state = STATE_INIT_LEAVE; + + } else if (view = this.getByState(STATE_INIT_ENTER)) { + // oh no, there's already a transition initalized ready to enter! + // but it actually hasn't entered yet at all so lets + // just keep it in the array, but not render or animate it in + view.state = STATE_INACTIVE; + } + + // insert each of the views in the pages array + let insertView: ViewController = null; + + insertViews.forEach((view, i) => { + insertView = view; + + // create the new entering view + view.setNav(this); + view.state = STATE_INACTIVE; + + // give this inserted view an ID + view.id = this.id + '-' + (++this._ids); + + // insert the entering view into the correct index in the stack + this._views.splice(insertIndex + i, 0, view); + }); + + if (insertView) { + insertView.state = STATE_INIT_ENTER; + } + + return insertView; + } + + pop(opts?: NavOptions, done?: Function): Promise { + // get the index of the active view + // which will become the view to be leaving + let activeView = this.getByState(STATE_TRANS_ENTER) || + this.getByState(STATE_INIT_ENTER) || + this.getActive(); + + return this.remove(this.indexOf(activeView), 1, opts, done); + } + + popToRoot(opts?: NavOptions, done?: Function): Promise { + return this.popTo(this.first(), opts, done); + } + + popTo(view: ViewController, opts?: NavOptions, done?: Function): Promise { + let startIndex = this.indexOf(view); + if (startIndex < 0) { + return Promise.reject('View not found to pop to'); + } + + let activeView = this.getByState(STATE_TRANS_ENTER) || + this.getByState(STATE_INIT_ENTER) || + this.getActive(); + let removeCount = this.indexOf(activeView) - startIndex; + + return this.remove(startIndex + 1, removeCount, opts, done); + } + + remove(startIndex: number = -1, removeCount: number = 1, opts?: NavOptions, done?: Function): Promise { + let promise: Promise; + + if (!done) { + promise = new Promise(resolve => { done = resolve; }); + } + + if (startIndex === -1) { + startIndex = (this._views.length - 1); + + } else if (startIndex < 0 || startIndex >= this._views.length) { + console.error('index out of range removing view from nav'); + done(false); + return promise; + } + + if (isBlank(opts)) { + opts = {}; + } + + // if not set, by default climb up the nav controllers if + // there isn't a previous view in this nav controller + if (isBlank(opts.climbNav)) { + opts.climbNav = true; + } + + // default the direction to "back" + opts.direction = opts.direction || DIRECTION_BACK; + + // figure out the states of each view in the stack + let leavingView = this._remove(startIndex, removeCount); + + if (!leavingView) { + let forcedActive = this.getByState(STATE_FORCE_ACTIVE); + if (forcedActive) { + // this scenario happens when a remove is going on + // during a transition + if (this._trans) { + this._trans.stop(); + this._trans.destroy(); + this._trans = null; + this._cleanup(); + } + + done(false); + return promise; + } + } + + if (leavingView) { + // there is a view ready to leave, meaning that a transition needs + // to happen and the previously active view is going to animate out + + // get the view thats ready to enter + let enteringView = this.getByState(STATE_INIT_ENTER); + + if (!enteringView && !this._isPortal) { + // oh nos! no entering view to go to! + // if there is no previous view that would enter in this nav stack + // and the option is set to climb up the nav parent looking + // for the next nav we could transition to instead + if (opts.climbNav) { + let parentNav: NavController = this.parent; + while (parentNav) { + if (!isTabs(parentNav)) { + // Tabs can be a parent, but it is not a collection of views + // only we're looking for an actual NavController w/ stack of views + leavingView.fireWillLeave(); + this.viewWillLeave.emit(leavingView); + this._app.viewWillLeave.emit(leavingView); + + return parentNav.pop(opts).then((rtnVal: boolean) => { + leavingView.fireDidLeave(); + this.viewDidLeave.emit(leavingView); + this._app.viewDidLeave.emit(leavingView); + return rtnVal; + }); + } + parentNav = parentNav.parent; + } + } + + // there's no previous view and there's no valid parent nav + // to climb to so this shouldn't actually remove the leaving + // view because there's nothing that would enter, eww + leavingView.state = STATE_ACTIVE; + done(false); + + return promise; + } + + if (!opts.animation) { + opts.animation = leavingView.getTransitionName(opts.direction); + } + + // start the transition, fire resolve when done... + this._transition(enteringView, leavingView, opts, done); + + return promise; + } + + // no need to transition when the active view isn't being removed + // there's still an active view after _remove() figured out states + // so this means views that were only removed before the active + // view, so auto-resolve since no transition needs to happen + done(false); + return promise; + } + + /** + * @private + */ + _remove(startIndex: number, removeCount: number): ViewController { + // when this is done, there should only be at most + // 1 STATE_INIT_ENTER and 1 STATE_INIT_LEAVE + // there should not be any that are STATE_ACTIVE after this is done + let view: ViewController = null; + + // loop through each view that is set to be removed + for (var i = startIndex, ii = removeCount + startIndex; i < ii; i++) { + view = this.getByIndex(i); + if (!view) break; + + if (view.state === STATE_TRANS_ENTER || view.state === STATE_TRANS_LEAVE) { + // oh no!!! this view should be removed, but it's + // actively transitioning in at the moment!! + // since it's viewable right now, let's just set that + // it should be removed after the transition + view.state = STATE_REMOVE_AFTER_TRANS; + + } else if (view.state === STATE_INIT_ENTER) { + // asked to be removed before it even entered! + view.state = STATE_CANCEL_ENTER; + + } else { + // if this view is already leaving then no need to immediately + // remove it, otherwise set the remove state + // this is useful if the view being removed isn't going to + // animate out, but just removed from the stack, no transition + view.state = STATE_REMOVE; + } + } + + if (view = this.getByState(STATE_INIT_LEAVE)) { + // looks like there's already an active leaving view + + // reassign previous entering view to just be inactive + let enteringView = this.getByState(STATE_INIT_ENTER); + if (enteringView) { + enteringView.state = STATE_INACTIVE; + } + + // from the index of the leaving view, go backwards and + // find the first view that is inactive + for (var i = this.indexOf(view) - 1; i >= 0; i--) { + if (this._views[i].state === STATE_INACTIVE) { + this._views[i].state = STATE_INIT_ENTER; + break; + } + } + + } else if (view = this.getByState(STATE_TRANS_LEAVE)) { + // an active transition is happening, but a new transition + // still needs to happen force this view to be the active one + view.state = STATE_FORCE_ACTIVE; + + } else if (view = this.getByState(STATE_REMOVE)) { + // there is no active transition about to happen + // find the first view that is supposed to be removed and + // set that it is the init leaving view + // the first view to be removed, it should init leave + view.state = STATE_INIT_LEAVE; + view.fireWillUnload(); + this.viewWillUnload.emit(view); + this._app.viewWillUnload.emit(view); + + // from the index of the leaving view, go backwards and + // find the first view that is inactive so it can be the entering + for (var i = this.indexOf(view) - 1; i >= 0; i--) { + if (this._views[i].state === STATE_INACTIVE) { + this._views[i].state = STATE_INIT_ENTER; + break; + } + } + } + + // if there is still an active view, then it wasn't one that was + // set to be removed, so there actually won't be a transition at all + view = this.getActive(); + if (view) { + // the active view remains untouched, so all the removes + // must have happened before it, so really no need for transition + view = this.getByState(STATE_INIT_ENTER); + if (view) { + // if it was going to enter, then just make inactive + view.state = STATE_INACTIVE; + } + view = this.getByState(STATE_INIT_LEAVE); + if (view) { + // this was going to leave, so just remove it completely + view.state = STATE_REMOVE; + } + } + + // remove views that have been set to be removed, but not + // apart of any transitions that will eventually happen + this._views.filter(v => v.state === STATE_REMOVE).forEach(view => { + view.fireWillLeave(); + view.fireDidLeave(); + this._views.splice(this.indexOf(view), 1); + view.destroy(); + }); + + return this.getByState(STATE_INIT_LEAVE); + } + + /** + * @private + */ + _transition(enteringView: ViewController, leavingView: ViewController, opts: NavOptions, done: Function) { + let transId = ++this._transIds; + + if (enteringView === leavingView) { + // if the entering view and leaving view are the same thing don't continue + this._transFinish(transId, enteringView, leavingView, null, false, false); + done(false); + return; + } + + if (isBlank(opts)) { + opts = {}; + } + + this._setAnimate(opts); + + if (!leavingView) { + // if no leaving view then create a bogus one + leavingView = new ViewController(); + } + + if (!enteringView) { + // if no entering view then create a bogus one + enteringView = new ViewController(); + enteringView.fireLoaded(); + } + + /* Async steps to complete a transition + 1. _render: compile the view and render it in the DOM. Load page if it hasn't loaded already. When done call postRender + 2. _postRender: Run willEnter/willLeave, then wait a frame (change detection happens), then call beginTransition + 3. _beforeTrans: Create the transition's animation, play the animation, wait for it to end + 4. _afterTrans: Run didEnter/didLeave, call _transComplete() + 5. _transComplete: Cleanup, remove cache views, then call the final callback + */ + + // begin the multiple async process of transitioning to the entering view + this._render(transId, enteringView, leavingView, opts, (hasCompleted: boolean) => { + this._transFinish(transId, enteringView, leavingView, opts.direction, false, hasCompleted); + done(hasCompleted); + }); + } + + /** + * @private + */ + _setAnimate(opts: NavOptions) { + if ((this._views.length === 1 && !this._init && !this._isPortal) || this.config.get('animate') === false) { + opts.animate = false; + } + } + + /** + * @private + */ + _render(transId: number, enteringView: ViewController, leavingView: ViewController, opts: NavOptions, done: Function) { + // compile/load the view into the DOM + + if (enteringView.state === STATE_INACTIVE) { + // this entering view is already set to inactive, so this + // transition must be canceled, so don't continue + return done(); + } + + enteringView.state = STATE_INIT_ENTER; + leavingView.state = STATE_INIT_LEAVE; + + // remember if this nav is already transitioning or not + let isAlreadyTransitioning = this.isTransitioning(); + + if (enteringView.isLoaded()) { + // already compiled this view, do not load again and continue + this._postRender(transId, enteringView, leavingView, isAlreadyTransitioning, opts, done); + + } else { + // view has not been compiled/loaded yet + // continue once the view has finished compiling + // DOM WRITE + this.setTransitioning(true, 500); + + this.loadPage(enteringView, this._viewport, opts, () => { + enteringView.fireLoaded(); + this.viewDidLoad.emit(enteringView); + this._app.viewDidLoad.emit(enteringView); + + this._postRender(transId, enteringView, leavingView, isAlreadyTransitioning, opts, done); + }); + } + } + + /** + * @private + */ + _postRender(transId: number, enteringView: ViewController, leavingView: ViewController, isAlreadyTransitioning: boolean, opts: NavOptions, done: Function) { + // called after _render has completed and the view is compiled/loaded + + if (enteringView.state === STATE_INACTIVE) { + // this entering view is already set to inactive, so this + // transition must be canceled, so don't continue + return done(); + } + + if (!opts.preload) { + // the enteringView will become the active view, and is not being preloaded + + // set the correct zIndex for the entering and leaving views + // if there's already another trans_enter happening then + // the zIndex for the entering view should go off of that one + // DOM WRITE + let lastestLeavingView = this.getByState(STATE_TRANS_ENTER) || leavingView; + this._setZIndex(enteringView, lastestLeavingView, opts.direction); + + // make sure the entering and leaving views are showing + // DOM WRITE + if (isAlreadyTransitioning) { + // the previous transition was still going when this one started + // so to be safe, only update showing the entering/leaving + // don't hide the others when they could still be transitioning + enteringView.domShow(true, this._renderer); + leavingView.domShow(true, this._renderer); + + } else { + // there are no other transitions happening but this one + // only entering/leaving should show, all others hidden + // also if a view is an overlay or the previous view is an + // overlay then always show the overlay and the view before it + this._views.forEach(view => { + view.domShow(this._isPortal || (view === enteringView) || (view === leavingView), this._renderer); + }); + } + + // call each view's lifecycle events + if (leavingView.fireOtherLifecycles) { + // only fire entering lifecycle if the leaving + // view hasn't explicitly set not to + enteringView.fireWillEnter(); + this.viewWillEnter.emit(enteringView); + this._app.viewWillEnter.emit(enteringView); + } + + if (enteringView.fireOtherLifecycles) { + // only fire leaving lifecycle if the entering + // view hasn't explicitly set not to + leavingView.fireWillLeave(); + this.viewWillLeave.emit(leavingView); + this._app.viewWillLeave.emit(leavingView); + } + + } else { + // this view is being preloaded, don't call lifecycle events + // transition does not need to animate + opts.animate = false; + } + + this._beforeTrans(enteringView, leavingView, opts, done); + } + + /** + * @private + */ + _beforeTrans(enteringView: ViewController, leavingView: ViewController, opts: NavOptions, done: Function) { + // called after one raf from postRender() + // create the transitions animation, play the animation + // when the transition ends call wait for it to end + + if (enteringView.state === STATE_INACTIVE || enteringView.state === STATE_CANCEL_ENTER) { + // this entering view is already set to inactive or has been canceled + // so this transition must not begin, so don't continue + return done(); + } + + enteringView.state = STATE_TRANS_ENTER; + leavingView.state = STATE_TRANS_LEAVE; + + // everything during the transition should runOutsideAngular + this._zone.runOutsideAngular(() => { + + // init the transition animation + let transitionOpts = { + animation: opts.animation, + direction: opts.direction, + duration: opts.duration, + easing: opts.easing, + renderDelay: opts.transitionDelay || this._trnsDelay, + isRTL: this.config.platform.isRTL(), + ev: opts.ev, + }; + + let transAnimation = this._createTrans(enteringView, leavingView, transitionOpts); + + this._trans && this._trans.destroy(); + this._trans = transAnimation; + + if (opts.animate === false) { + // force it to not animate the elements, just apply the "to" styles + transAnimation.duration(0); + } + + // check if a parent is transitioning and get the time that it ends + let parentTransitionEndTime = this.getLongestTrans(Date.now()); + if (parentTransitionEndTime > 0) { + // the parent is already transitioning and has disabled the app + // so just update the local transitioning information + let duration = parentTransitionEndTime - Date.now(); + this.setTransitioning(true, duration); + + } else { + // this is the only active transition (for now), so disable the app + let keyboardDurationPadding = 0; + if (this._keyboard.isOpen()) { + // add XXms to the duration the app is disabled when the keyboard is open + keyboardDurationPadding = 600; + } + let duration = transAnimation.getDuration() + keyboardDurationPadding; + let enableApp = (duration < 64); + this._app.setEnabled(enableApp, duration); + this.setTransitioning(!enableApp, duration); + } + + // create a callback for when the animation is done + transAnimation.onFinish((trans: Transition) => { + // transition animation has ended + + // destroy the animation and it's element references + trans.destroy(); + + this._afterTrans(enteringView, leavingView, opts, trans.hasCompleted, done); + }); + + // 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 + transAnimation.progressStart(); + + } else { + + // this is a normal animation + // kick it off and let it play through + transAnimation.play(); + } + }); + } + + /** + * @private + */ + _afterTrans(enteringView: ViewController, leavingView: ViewController, opts: NavOptions, hasCompleted: boolean, done: Function) { + // transition has completed, update each view's state + // place back into the zone, run didEnter/didLeave + // call the final callback when done + + // run inside of the zone again + this._zone.run(() => { + + if (!opts.preload && hasCompleted) { + if (leavingView.fireOtherLifecycles) { + // only fire entering lifecycle if the leaving + // view hasn't explicitly set not to + enteringView.fireDidEnter(); + this.viewDidEnter.emit(enteringView); + this._app.viewDidEnter.emit(enteringView); + } + + if (enteringView.fireOtherLifecycles && this._init) { + // only fire leaving lifecycle if the entering + // view hasn't explicitly set not to + // and after the nav has initialized + leavingView.fireDidLeave(); + this.viewDidLeave.emit(leavingView); + this._app.viewDidLeave.emit(leavingView); + } + } + + if (enteringView.state === STATE_INACTIVE) { + // this entering view is already set to inactive, so this + // transition must be canceled, so don't continue + return done(hasCompleted); + } + + if (opts.keyboardClose !== false && this._keyboard.isOpen()) { + // the keyboard is still open! + // no problem, let's just close for them + this._keyboard.close(); + this._keyboard.onClose(() => { + + // keyboard has finished closing, transition complete + done(hasCompleted); + }, 32); + + } else { + // all good, transition complete + done(hasCompleted); + } + }); + } + + /** + * @private + */ + _transFinish(transId: number, enteringView: ViewController, leavingView: ViewController, direction: string, updateUrl: boolean, hasCompleted: boolean) { + // a transition has completed, but not sure if it's the last one or not + // check if this transition is the most recent one or not + + if (enteringView.state === STATE_CANCEL_ENTER) { + // this view was told to leave before it finished entering + this.remove(enteringView.index, 1); + } + + if (transId === this._transIds) { + // ok, good news, there were no other transitions that kicked + // off during the time this transition started and ended + + if (hasCompleted) { + // this transition has completed as normal + // so the entering one is now the active view + // and the leaving view is now just inactive + if (enteringView.state !== STATE_REMOVE_AFTER_TRANS) { + enteringView.state = STATE_ACTIVE; + } + if (leavingView.state !== STATE_REMOVE_AFTER_TRANS) { + leavingView.state = STATE_INACTIVE; + } + + // only need to do all this clean up if the transition + // completed, otherwise nothing actually changed + // destroy all of the views that come after the active view + this._cleanup(); + + // make sure only this entering view and PREVIOUS view are the + // only two views that are not display:none + // do not make any changes to the stack's current visibility + // if there is an overlay somewhere in the stack + leavingView = this.getPrevious(enteringView); + if (this._isPortal) { + // ensure the entering view is showing + enteringView.domShow(true, this._renderer); + + } else { + // only possibly hide a view if there are no overlays in the stack + this._views.forEach(view => { + view.domShow((view === enteringView) || (view === leavingView), this._renderer); + }); + } + + // this check only needs to happen once, which will add the css + // class to the nav when it's finished its first transition + this._init = true; + + } else { + // this transition has not completed, meaning the + // entering view did not end up as the active view + // this would happen when swipe to go back started + // but the user did not complete the swipe and the + // what was the active view stayed as the active view + leavingView.state = STATE_ACTIVE; + enteringView.state = STATE_INACTIVE; + } + + // check if there is a parent actively transitioning + let transitionEndTime = this.getLongestTrans(Date.now()); + // if transitionEndTime is greater than 0, there is a parent transition occurring + // so delegate enabling the app to the parent. If it <= 0, go ahead and enable the app + if (transitionEndTime <= 0) { + this._app && this._app.setEnabled(true); + } + + // update that this nav is not longer actively transitioning + this.setTransitioning(false); + + // see if we should add the swipe back gesture listeners or not + this._sbCheck(); + + } else { + // darn, so this wasn't the most recent transition + // so while this one did end, there's another more recent one + // still going on. Because a new transition is happening, + // then this entering view isn't actually going to be the active + // one, so only update the state to active/inactive if the state + // wasn't already updated somewhere else during its transition + if (enteringView.state === STATE_TRANS_ENTER) { + enteringView.state = STATE_INACTIVE; + } + if (leavingView.state === STATE_TRANS_LEAVE) { + leavingView.state = STATE_INACTIVE; + } + } + } + + /** + *@private + * This method is just a wrapper to the Transition function of same name + * to make it easy/possible to mock the method call by overriding the function. + * In testing we don't want to actually do the animation, we want to return a stub instead + */ + _createTrans(enteringView: ViewController, leavingView: ViewController, transitionOpts: any): Transition { + return Transition.createTransition(enteringView, leavingView, transitionOpts); + } + + _cleanup() { + // ok, cleanup time!! Destroy all of the views that are + // INACTIVE and come after the active view + let activeViewIndex = this.indexOf(this.getActive()); + let destroys = this._views.filter(v => v.state === STATE_REMOVE_AFTER_TRANS); + + for (var i = activeViewIndex + 1; i < this._views.length; i++) { + if (this._views[i].state === STATE_INACTIVE) { + destroys.push(this._views[i]); + } + } + + // all pages being destroyed should be removed from the list of + // pages and completely removed from the dom + destroys.forEach(view => { + this._views.splice(this.indexOf(view), 1); + view.destroy(); + this.viewDidUnload.emit(view); + this._app.viewDidUnload.emit(view); + }); + + // if any z-index goes under 0, then reset them all + let shouldResetZIndex = this._views.some(v => v.zIndex < 0); + if (shouldResetZIndex) { + this._views.forEach(view => { + view.setZIndex(view.zIndex + INIT_ZINDEX + 1, this._renderer); + }); + } + } + + getActiveChildNav(): any { + return this._children[this._children.length - 1]; + } + + /** + * @private + */ + registerChildNav(nav: any) { + this._children.push(nav); + } + + /** + * @private + */ + unregisterChildNav(nav: any) { + let index = this._children.indexOf(nav); + if (index > -1) { + this._children.splice(index, 1); + } + } + + /** + * @private + */ + ngOnDestroy() { + for (var i = this._views.length - 1; i >= 0; i--) { + this._views[i].destroy(); + } + this._views.length = 0; + + if (this.parent && this.parent.unregisterChildNav) { + this.parent.unregisterChildNav(this); + } + } + + /** + * @private + */ + loadPage(view: ViewController, viewport: ViewContainerRef, opts: NavOptions, done: Function) { + if (!viewport || !view.componentType) { + return; + } + + // TEMPORARY: automatically set selector w/ dah reflector + // TODO: use componentFactory.create once fixed + addSelector(view.componentType, 'ion-page'); + + this._compiler.resolveComponent(view.componentType).then(componentFactory => { + + if (view.state === STATE_CANCEL_ENTER) { + // view may have already been removed from the stack + // if so, don't even bother adding it + view.destroy(); + this._views.splice(view.index, 1); + return; + } + + // add more providers to just this page + let componentProviders = ReflectiveInjector.resolve([ + provide(NavController, {useValue: this}), + provide(ViewController, {useValue: view}), + provide(NavParams, {useValue: view.getNavParams()}) + ]); + + let childInjector = ReflectiveInjector.fromResolvedProviders(componentProviders, this._viewport.parentInjector); + + let componentRef = componentFactory.create(childInjector, null, null); + + viewport.insert(componentRef.hostView, viewport.length); + + // a new ComponentRef has been created + // set the ComponentRef's instance to its ViewController + view.setInstance(componentRef.instance); + + // the component has been loaded, so call the view controller's loaded method to load any dependencies into the dom + view.loaded(() => { + + // the ElementRef of the actual ion-page created + let pageElementRef = componentRef.location; + + // remember the ChangeDetectorRef for this ViewController + view.setChangeDetector(componentRef.changeDetectorRef); + + // remember the ElementRef to the ion-page elementRef that was just created + view.setPageRef(pageElementRef); + + // auto-add page css className created from component JS class name + let cssClassName = pascalCaseToDashCase(view.componentType.name); + this._renderer.setElementClass(pageElementRef.nativeElement, cssClassName, true); + + view.onDestroy(() => { + // ensure the element is cleaned up for when the view pool reuses this element + this._renderer.setElementAttribute(pageElementRef.nativeElement, 'class', null); + this._renderer.setElementAttribute(pageElementRef.nativeElement, 'style', null); + componentRef.destroy(); + }); + + // our job is done here + done(view); + }); + }); + } + + /** + * @private + */ + swipeBackStart() { + // default the direction to "back" + let opts: NavOptions = { + direction: DIRECTION_BACK, + progressAnimation: true + }; + + // figure out the states of each view in the stack + let leavingView = this._remove(this._views.length - 1, 1); + + if (leavingView) { + opts.animation = leavingView.getTransitionName(opts.direction); + + // get the view thats ready to enter + let enteringView = this.getByState(STATE_INIT_ENTER); + + // start the transition, fire callback when done... + this._transition(enteringView, leavingView, opts, (hasCompleted: boolean) => { + // swipe back has finished!! + console.debug('swipeBack, hasCompleted', hasCompleted); + }); + } + } + + /** + * @private + */ + swipeBackProgress(stepValue: number) { + if (this._trans && this._sbGesture) { + // continue to disable the app while actively dragging + this._app.setEnabled(false, 4000); + this.setTransitioning(true, 4000); + + // set the transition animation's progress + this._trans.progressStep(stepValue); + } + } + + /** + * @private + */ + swipeBackEnd(shouldComplete: boolean, currentStepValue: number) { + if (this._trans && this._sbGesture) { + // the swipe back gesture has ended + this._trans.progressEnd(shouldComplete, currentStepValue); + } + } + + /** + * @private + */ + _sbCheck() { + if (this._sbEnabled) { + // this nav controller can have swipe to go back + + if (!this._sbGesture) { + // create the swipe back gesture if we haven't already + let opts = { + edge: 'left', + threshold: this._sbThreshold + }; + this._sbGesture = new SwipeBackGesture(this.getNativeElement(), opts, this, this._gestureCtrl); + } + + if (this.canSwipeBack()) { + // it is be possible to swipe back + if (!this._sbGesture.isListening) { + this._zone.runOutsideAngular(() => { + // start listening if it's not already + console.debug('swipeBack gesture, listen'); + this._sbGesture.listen(); + }); + } + + } else if (this._sbGesture.isListening) { + // it should not be possible to swipe back + // but the gesture is still listening + console.debug('swipeBack gesture, unlisten'); + this._sbGesture.unlisten(); + } + } + } + + canSwipeBack(): boolean { + return (this._sbEnabled && !this.isTransitioning() && this._app.isEnabled() && this.canGoBack()); + } + + canGoBack(): boolean { + let activeView = this.getActive(); + if (activeView) { + return activeView.enableBack(); + } + return false; + } + + isTransitioning(includeAncestors?: boolean): boolean { + let now = Date.now(); + if (includeAncestors && this.getLongestTrans(now) > 0) { + return true; + } + return (this.trnsTime > now); + } + + setTransitioning(isTransitioning: boolean, fallback: number = 700) { + this.trnsTime = (isTransitioning ? Date.now() + fallback : 0); + } + + getLongestTrans(now: number) { + // traverses parents upwards and looks at the time the + // transition ends (if it's transitioning) and returns the + // value that is the furthest into the future thus giving us + // the longest transition duration + let parentNav = this.parent; + let transitionEndTime = -1; + while (parentNav) { + if (parentNav.trnsTime > transitionEndTime) { + transitionEndTime = parentNav.trnsTime; + } + parentNav = parentNav.parent; + } + + // only check if the transitionTime is greater than the current time once + return transitionEndTime > 0 && transitionEndTime > now ? transitionEndTime : 0; + } + + getByState(state: number): ViewController { + for (var i = this._views.length - 1; i >= 0; i--) { + if (this._views[i].state === state) { + return this._views[i]; + } + } + return null; + } + + getByIndex(index: number): ViewController { + return (index < this._views.length && index > -1 ? this._views[index] : null); + } + + getActive(): ViewController { + return this.getByState(STATE_ACTIVE); + } + + isActive(view: ViewController): boolean { + // returns if the given view is the active view or not + return !!(view && view.state === STATE_ACTIVE); + } + + getPrevious(view: ViewController): ViewController { + // returns the view controller which is before the given view controller. + return this.getByIndex(this.indexOf(view) - 1); + } + + first(): ViewController { + // returns the first view controller in this nav controller's stack. + return (this._views.length ? this._views[0] : null); + } + + last(): ViewController { + // returns the last page in this nav controller's stack. + return (this._views.length ? this._views[this._views.length - 1] : null); + } + + indexOf(view: ViewController): number { + // returns the index number of the given view controller. + return this._views.indexOf(view); + } + + length(): number { + return this._views.length; + } + + isSwipeBackEnabled(): boolean { + return this._sbEnabled; + } + + /** + * DEPRECATED: Please use app.getRootNav() instead + */ + private get rootNav(): NavController { + // deprecated 07-14-2016 beta.11 + console.warn('nav.rootNav() has been deprecated, please use app.getRootNav() instead'); + return this._app.getRootNav(); + } + + /** + * @private + * Dismiss all pages which have set the `dismissOnPageChange` property. + */ + dismissPageChangeViews() { + this._views.forEach(view => { + if (view.data && view.data.dismissOnPageChange) { + view.dismiss(); + } + }); + } + + /** + * @private + */ + _setZIndex(enteringView: ViewController, leavingView: ViewController, direction: string) { + if (enteringView) { + // get the leaving view, which could be in various states + if (!leavingView || !leavingView.isLoaded()) { + // the leavingView is a mocked view, either we're + // actively transitioning or it's the initial load + + var previousView = this.getPrevious(enteringView); + if (previousView && previousView.isLoaded()) { + // we found a better previous view to reference + // use this one instead + enteringView.setZIndex(previousView.zIndex + 1, this._renderer); + + } else { + // this is the initial view + enteringView.setZIndex(this._isPortal ? PORTAL_ZINDEX : INIT_ZINDEX, this._renderer); + } + + } else if (direction === DIRECTION_BACK) { + // moving back + enteringView.setZIndex(leavingView.zIndex - 1, this._renderer); + + } else { + // moving forward + enteringView.setZIndex(leavingView.zIndex + 1, this._renderer); + } + } + } + +} + +export const isTabs = (nav: any) => { + // Tabs (ion-tabs) + return !!nav.getSelected; +}; + +export const isTab = (nav: any) => { + // Tab (ion-tab) + return isPresent(nav._tabId); +}; + +export const isNav = function(nav: any) { + // Nav (ion-nav), Tab (ion-tab), Portal (ion-portal) + return isPresent(nav.push); +}; + + +export const STATE_ACTIVE = 1; +export const STATE_INACTIVE = 2; +export const STATE_INIT_ENTER = 3; +export const STATE_INIT_LEAVE = 4; +export const STATE_TRANS_ENTER = 5; +export const STATE_TRANS_LEAVE = 6; +export const STATE_REMOVE = 7; +export const STATE_REMOVE_AFTER_TRANS = 8; +export const STATE_CANCEL_ENTER = 9; +export const STATE_FORCE_ACTIVE = 10; + +const INIT_ZINDEX = 100; +const PORTAL_ZINDEX = 9999; + +let ctrlIds = -1; \ No newline at end of file diff --git a/src/components/nav/nav-controller.ts b/src/components/nav/nav-controller.ts index abe56111dc..604925995b 100644 --- a/src/components/nav/nav-controller.ts +++ b/src/components/nav/nav-controller.ts @@ -1,16 +1,11 @@ -import { ComponentResolver, ElementRef, EventEmitter, NgZone, provide, ReflectiveInjector, Renderer, ViewContainerRef } from '@angular/core'; +import { EventEmitter } from '@angular/core'; -import { addSelector } from '../../config/bootstrap'; -import { App } from '../app/app'; import { Config } from '../../config/config'; +import { GestureController } from '../../gestures/gesture-controller'; import { Ion } from '../ion'; import { isBlank, pascalCaseToDashCase } from '../../util/util'; import { Keyboard } from '../../util/keyboard'; -import { MenuController } from '../menu/menu-controller'; import { NavOptions } from './nav-interfaces'; -import { NavParams } from './nav-params'; -import { SwipeBackGesture } from './swipe-back'; -import { Transition } from '../../transitions/transition'; import { ViewController } from './view-controller'; @@ -157,19 +152,7 @@ import { ViewController } from './view-controller'; * * @see {@link /docs/v2/components#navigation Navigation Component Docs} */ -export class NavController extends Ion { - private _transIds = 0; - private _init = false; - private _trans: Transition; - private _sbGesture: SwipeBackGesture; - private _sbThreshold: number; - private _viewport: ViewContainerRef; - private _children: any[] = []; - - protected _sbEnabled: boolean; - protected _ids: number = -1; - protected _trnsDelay: any; - protected _views: ViewController[] = []; +export abstract class NavController { /** * Observable to be subscribed to when a component is loaded. @@ -219,7 +202,9 @@ export class NavController extends Ion { id: string; /** - * @private + * The parent navigation instance. If this is the root nav, then + * it'll be `null`. A `Tab` instance's parent is `Tabs`, otherwise + * the parent would be another nav, if it's not already the root nav. */ parent: any; @@ -228,55 +213,6 @@ export class NavController extends Ion { */ config: Config; - /** - * @private - */ - isPortal: boolean = false; - - /** - * @private - */ - trnsTime: number = 0; - - constructor( - parent: any, - protected _app: App, - config: Config, - protected _keyboard: Keyboard, - elementRef: ElementRef, - protected _zone: NgZone, - protected _renderer: Renderer, - protected _compiler: ComponentResolver, - protected _menuCtrl: MenuController - ) { - super(elementRef); - - this.parent = parent; - this.config = config; - - this._trnsDelay = config.get('pageTransitionDelay'); - - this._sbEnabled = config.getBoolean('swipeBackEnabled'); - this._sbThreshold = config.getNumber('swipeBackThreshold', 40); - - this.id = (++ctrlIds).toString(); - - this.viewDidLoad = new EventEmitter(); - this.viewWillEnter = new EventEmitter(); - this.viewDidEnter = new EventEmitter(); - this.viewWillLeave = new EventEmitter(); - this.viewDidLeave = new EventEmitter(); - this.viewWillUnload = new EventEmitter(); - this.viewDidUnload = new EventEmitter(); - } - - /** - * @private - */ - setViewport(val: ViewContainerRef) { - this._viewport = val; - } - /** * Set the root for the current navigation stack. * @param {Page} page The name of the component you want to push on the navigation stack. @@ -284,9 +220,7 @@ export class NavController extends Ion { * @param {object} [opts={}] Any options you want to use pass to transtion. * @returns {Promise} Returns a promise which is resolved when the transition has completed. */ - setRoot(page: any, params?: any, opts?: NavOptions): Promise { - return this.setPages([{page, params}], opts); - } + abstract setRoot(page: any, params?: any, opts?: NavOptions, done?: Function): Promise; /** * Set the views of the current navigation stack and navigate to the @@ -298,41 +232,7 @@ export class NavController extends Ion { * @param {object} [opts={}] Nav options to go with this transition. * @returns {Promise} Returns a promise which is resolved when the transition has completed. */ - setPages(pages: Array<{page: any, params?: any}>, opts?: NavOptions): Promise { - if (!pages || !pages.length) { - return Promise.resolve(false); - } - - if (isBlank(opts)) { - opts = {}; - } - - // remove existing views - let leavingView = this._remove(0, this._views.length); - - // create view controllers out of the pages and insert the new views - let views = pages.map(p => new ViewController(p.page, p.params)); - let enteringView = this._insert(0, views); - - // if animation wasn't set to true then default it to NOT animate - if (opts.animate !== true) { - opts.animate = false; - } - - // set the nav direction to "back" if it wasn't set - opts.direction = opts.direction || DIRECTION_BACK; - - let resolve: any; - let promise = new Promise(res => { resolve = res; }); - - // start the transition, fire resolve when done... - this._transition(enteringView, leavingView, opts, (hasCompleted: boolean) => { - // transition has completed!! - resolve(hasCompleted); - }); - - return promise; - } + abstract setPages(pages: Array<{page: any, params?: any}>, opts?: NavOptions, done?: Function): Promise; /** * Push a new component onto the current navication stack. Pass any aditional information @@ -343,20 +243,7 @@ export class NavController extends Ion { * @param {object} [opts={}] Nav options to go with this transition. * @returns {Promise} Returns a promise which is resolved when the transition has completed. */ - push(page: any, params?: any, opts?: NavOptions) { - return this.insertPages(-1, [{page: page, params: params}], opts); - } - - /** - * @private - * DEPRECATED: Please use inject the overlays controller and use the present method on the instance instead. - */ - private present(enteringView: ViewController, opts?: NavOptions): Promise { - // deprecated warning: added beta.11 2016-06-27 - console.warn('nav.present() has been deprecated.\n' + - 'Please inject the overlay\'s controller and use the present method on the instance instead.'); - return Promise.resolve(); - } + abstract push(page: any, params?: any, opts?: NavOptions, done?: Function): Promise; /** * Inserts a component into the nav stack at the specified index. This is useful if @@ -369,137 +256,19 @@ export class NavController extends Ion { * @param {object} [opts={}] Nav options to go with this transition. * @returns {Promise} Returns a promise which is resolved when the transition has completed. */ - insert(insertIndex: number, page: any, params?: any, opts?: NavOptions): Promise { - return this.insertPages(insertIndex, [{page: page, params: params}], opts); - } + abstract insert(insertIndex: number, page: any, params?: any, opts?: NavOptions, done?: Function): Promise; /** * Inserts an array of components into the nav stack at the specified index. - * The last component in the array will animate in and become the active component + * The last component in the array will become instantiated as a view, + * and animate in to become the active view. * * @param {number} insertIndex The index where you want to insert the page. * @param {array<{page: Page, params=: any}>} insertPages An array of objects, each with a `page` and optionally `params` property. * @param {object} [opts={}] Nav options to go with this transition. * @returns {Promise} Returns a promise which is resolved when the transition has completed. */ - insertPages(insertIndex: number, insertPages: Array<{page: any, params?: any}>, opts?: NavOptions): Promise { - let views = insertPages.map(p => new ViewController(p.page, p.params)); - return this.insertViews(insertIndex, views, opts); - } - - /** - * @private - */ - insertViews(insertIndex: number, insertViews: ViewController[], opts?: NavOptions): Promise { - if (!insertViews || !insertViews.length) { - return Promise.reject('invalid pages'); - } - - if (isBlank(opts)) { - opts = {}; - } - - // insert the new page into the stack - // returns the newly created entering view - let enteringView = this._insert(insertIndex, insertViews); - - // set the nav direction to "forward" if it wasn't set - opts.direction = opts.direction || 'forward'; - - // set which animation it should use if it wasn't set yet - if (!opts.animation) { - opts.animation = enteringView.getTransitionName(opts.direction); - } - - let resolve: any; - let promise = new Promise(res => { resolve = res; }); - - // it's possible that the newly added view doesn't need to - // transition in, but was simply inserted somewhere in the stack - // go backwards through the stack and find the first active view - // which could be active or one ready to enter - for (var i = this._views.length - 1; i >= 0; i--) { - if (this._views[i].state === STATE_ACTIVE || this._views[i].state === STATE_INIT_ENTER) { - // found the view at the end of the stack that's either - // already active or it is about to enter - - if (this._views[i] === enteringView) { - // cool, so the last valid view is also our entering view!! - // this means we should animate that bad boy in so it's the active view - // return a promise and resolve when the transition has completed - - // get the leaving view which the _insert() already set - let leavingView = this.getByState(STATE_INIT_LEAVE); - - // start the transition, fire resolve when done... - this._transition(enteringView, leavingView, opts, (hasCompleted: boolean) => { - // transition has completed!! - resolve(hasCompleted); - }); - - return promise; - } - break; - } - } - - // the page was not pushed onto the end of the stack - // but rather inserted somewhere in the middle or beginning - // Since there are views after this new one, don't transition in - // auto resolve cuz there was is no need for an animation - return Promise.resolve(enteringView); - } - - /** - * @private - */ - private _insert(insertIndex: number, insertViews: Array): ViewController { - // when this is done, there should only be at most - // 1 STATE_INIT_ENTER and 1 STATE_INIT_LEAVE - // there should not be any that are STATE_ACTIVE after this is done - - // 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 (insertIndex < 0 || insertIndex > this._views.length) { - insertIndex = this._views.length; - } - - // first see if there's an active view - let view = this.getActive(); - if (view) { - // there's an active view, set that it's initialized to leave - view.state = STATE_INIT_LEAVE; - - } else if (view = this.getByState(STATE_INIT_ENTER)) { - // oh no, there's already a transition initalized ready to enter! - // but it actually hasn't entered yet at all so lets - // just keep it in the array, but not render or animate it in - view.state = STATE_INACTIVE; - } - - // insert each of the views in the pages array - let insertView: ViewController = null; - - insertViews.forEach((view, i) => { - insertView = view; - - // create the new entering view - view.setNav(this); - view.state = STATE_INACTIVE; - - // give this inserted view an ID - view.id = this.id + '-' + (++this._ids); - - // insert the entering view into the correct index in the stack - this._views.splice(insertIndex + i, 0, view); - }); - - if (insertView) { - insertView.state = STATE_INIT_ENTER; - } - - return insertView; - } + abstract insertPages(insertIndex: number, insertPages: Array<{page: any, params?: any}>, opts?: NavOptions, done?: Function): Promise; /** * Call to navigate back from a current component. Similar to `push()`, you @@ -508,24 +277,7 @@ export class NavController extends Ion { * @param {object} [opts={}] Nav options to go with this transition. * @returns {Promise} Returns a promise which is resolved when the transition has completed. */ - pop(opts?: NavOptions): Promise { - // get the index of the active view - // which will become the view to be leaving - let activeView = this.getByState(STATE_TRANS_ENTER) || - this.getByState(STATE_INIT_ENTER) || - this.getActive(); - - if (isBlank(opts)) { - opts = {}; - } - - // if not set, by default climb up the nav controllers if - // there isn't a previous view in this nav controller - if (isBlank(opts.climbNav)) { - opts.climbNav = true; - } - return this.remove(this.indexOf(activeView), 1, opts); - } + abstract pop(opts?: NavOptions, done?: Function): Promise; /** * Navigate back to the root of the stack, no matter how far back that is. @@ -533,9 +285,7 @@ export class NavController extends Ion { * @param {object} [opts={}] Nav options to go with this transition. * @returns {Promise} Returns a promise which is resolved when the transition has completed. */ - popToRoot(opts?: NavOptions): Promise { - return this.popTo(this.first(), opts); - } + abstract popToRoot(opts?: NavOptions, done?: Function): Promise; /** * Pop to a specific view in the history stack. @@ -544,19 +294,7 @@ export class NavController extends Ion { * @param {object} [opts={}] Nav options to go with this transition. * @returns {Promise} Returns a promise which is resolved when the transition has completed. */ - popTo(view: ViewController, opts?: NavOptions): Promise { - let startIndex = this.indexOf(view); - if (startIndex < 0) { - return Promise.reject('View not found to pop to'); - } - - let activeView = this.getByState(STATE_TRANS_ENTER) || - this.getByState(STATE_INIT_ENTER) || - this.getActive(); - let removeCount = this.indexOf(activeView) - startIndex; - - return this.remove(startIndex + 1, removeCount, opts); - } + abstract popTo(view: ViewController, opts?: NavOptions, done?: Function): Promise; /** * Removes a page from the nav stack at the specified index. @@ -566,840 +304,68 @@ export class NavController extends Ion { * @param {object} [opts={}] Any options you want to use pass to transtion. * @returns {Promise} Returns a promise which is resolved when the transition has completed. */ - remove(startIndex: number = -1, removeCount: number = 1, opts?: NavOptions): Promise { - if (startIndex === -1) { - startIndex = this._views.length - 1; - - } else if (startIndex < 0 || startIndex >= this._views.length) { - return Promise.reject('remove index out of range'); - } - - if (isBlank(opts)) { - opts = {}; - } - - // default the direction to "back" - opts.direction = opts.direction || DIRECTION_BACK; - - // figure out the states of each view in the stack - let leavingView = this._remove(startIndex, removeCount); - - if (!leavingView) { - let forcedActive = this.getByState(STATE_FORCE_ACTIVE); - if (forcedActive) { - // this scenario happens when a remove is going on - // during a transition - if (this._trans) { - this._trans.stop(); - this._trans.destroy(); - this._trans = null; - this._cleanup(); - } - - return Promise.resolve(false); - } - } - - if (leavingView) { - // there is a view ready to leave, meaning that a transition needs - // to happen and the previously active view is going to animate out - - // get the view thats ready to enter - let enteringView = this.getByState(STATE_INIT_ENTER); - - if (!enteringView && !this.isPortal) { - // oh nos! no entering view to go to! - // if there is no previous view that would enter in this nav stack - // and the option is set to climb up the nav parent looking - // for the next nav we could transition to instead - if (opts.climbNav) { - let parentNav: NavController = this.parent; - while (parentNav) { - if (!parentNav['_tabs']) { - // Tabs can be a parent, but it is not a collection of views - // only we're looking for an actual NavController w/ stack of views - leavingView.fireWillLeave(); - this.viewWillLeave.emit(leavingView); - this._app.viewWillLeave.emit(leavingView); - - return parentNav.pop(opts).then((rtnVal: boolean) => { - leavingView.fireDidLeave(); - this.viewDidLeave.emit(leavingView); - this._app.viewDidLeave.emit(leavingView); - return rtnVal; - }); - } - parentNav = parentNav.parent; - } - } - - // there's no previous view and there's no valid parent nav - // to climb to so this shouldn't actually remove the leaving - // view because there's nothing that would enter, eww - leavingView.state = STATE_ACTIVE; - return Promise.resolve(false); - } - - let resolve: any; - let promise = new Promise(res => { resolve = res; }); - - if (!opts.animation) { - opts.animation = leavingView.getTransitionName(opts.direction); - } - - // start the transition, fire resolve when done... - this._transition(enteringView, leavingView, opts, (hasCompleted: boolean) => { - // transition has completed!! - resolve(hasCompleted); - }); - - return promise; - } - - // no need to transition when the active view isn't being removed - // there's still an active view after _remove() figured out states - // so this means views that were only removed before the active - // view, so auto-resolve since no transition needs to happen - return Promise.resolve(false); - } + abstract remove(startIndex: number, removeCount?: number, opts?: NavOptions, done?: Function): Promise; /** - * @private + * @param {number} index The index of the page to get. + * @returns {ViewController} Returns the view controller that matches the given index. */ - private _remove(startIndex: number, removeCount: number): ViewController { - // when this is done, there should only be at most - // 1 STATE_INIT_ENTER and 1 STATE_INIT_LEAVE - // there should not be any that are STATE_ACTIVE after this is done - let view: ViewController = null; - - // loop through each view that is set to be removed - for (var i = startIndex, ii = removeCount + startIndex; i < ii; i++) { - view = this.getByIndex(i); - if (!view) break; - - if (view.state === STATE_TRANS_ENTER || view.state === STATE_TRANS_LEAVE) { - // oh no!!! this view should be removed, but it's - // actively transitioning in at the moment!! - // since it's viewable right now, let's just set that - // it should be removed after the transition - view.state = STATE_REMOVE_AFTER_TRANS; - - } else if (view.state === STATE_INIT_ENTER) { - // asked to be removed before it even entered! - view.state = STATE_CANCEL_ENTER; - - } else { - // if this view is already leaving then no need to immediately - // remove it, otherwise set the remove state - // this is useful if the view being removed isn't going to - // animate out, but just removed from the stack, no transition - view.state = STATE_REMOVE; - } - } - - if (view = this.getByState(STATE_INIT_LEAVE)) { - // looks like there's already an active leaving view - - // reassign previous entering view to just be inactive - let enteringView = this.getByState(STATE_INIT_ENTER); - if (enteringView) { - enteringView.state = STATE_INACTIVE; - } - - // from the index of the leaving view, go backwards and - // find the first view that is inactive - for (var i = this.indexOf(view) - 1; i >= 0; i--) { - if (this._views[i].state === STATE_INACTIVE) { - this._views[i].state = STATE_INIT_ENTER; - break; - } - } - - } else if (view = this.getByState(STATE_TRANS_LEAVE)) { - // an active transition is happening, but a new transition - // still needs to happen force this view to be the active one - view.state = STATE_FORCE_ACTIVE; - - } else if (view = this.getByState(STATE_REMOVE)) { - // there is no active transition about to happen - // find the first view that is supposed to be removed and - // set that it is the init leaving view - // the first view to be removed, it should init leave - view.state = STATE_INIT_LEAVE; - view.fireWillUnload(); - this.viewWillUnload.emit(view); - this._app.viewWillUnload.emit(view); - - // from the index of the leaving view, go backwards and - // find the first view that is inactive so it can be the entering - for (var i = this.indexOf(view) - 1; i >= 0; i--) { - if (this._views[i].state === STATE_INACTIVE) { - this._views[i].state = STATE_INIT_ENTER; - break; - } - } - } - - // if there is still an active view, then it wasn't one that was - // set to be removed, so there actually won't be a transition at all - view = this.getActive(); - if (view) { - // the active view remains untouched, so all the removes - // must have happened before it, so really no need for transition - view = this.getByState(STATE_INIT_ENTER); - if (view) { - // if it was going to enter, then just make inactive - view.state = STATE_INACTIVE; - } - view = this.getByState(STATE_INIT_LEAVE); - if (view) { - // this was going to leave, so just remove it completely - view.state = STATE_REMOVE; - } - } - - // remove views that have been set to be removed, but not - // apart of any transitions that will eventually happen - this._views.filter(v => v.state === STATE_REMOVE).forEach(view => { - view.fireWillLeave(); - view.fireDidLeave(); - this._views.splice(this.indexOf(view), 1); - view.destroy(); - }); - - return this.getByState(STATE_INIT_LEAVE); - } + abstract getByIndex(index: number): ViewController; /** - * @private + * @returns {ViewController} Returns the active page's view controller. */ - private _transition(enteringView: ViewController, leavingView: ViewController, opts: NavOptions, done: Function) { - let transId = ++this._transIds; - - if (enteringView === leavingView) { - // if the entering view and leaving view are the same thing don't continue - this._transFinish(transId, enteringView, leavingView, null, false); - return done(false); - } - - if (isBlank(opts)) { - opts = {}; - } - - this._setAnimate(opts); - - if (!leavingView) { - // if no leaving view then create a bogus one - leavingView = new ViewController(); - } - - if (!enteringView) { - // if no entering view then create a bogus one - enteringView = new ViewController(); - enteringView.fireLoaded(); - } - - /* Async steps to complete a transition - 1. _render: compile the view and render it in the DOM. Load page if it hasn't loaded already. When done call postRender - 2. _postRender: Run willEnter/willLeave, then wait a frame (change detection happens), then call beginTransition - 3. _beforeTrans: Create the transition's animation, play the animation, wait for it to end - 4. _afterTrans: Run didEnter/didLeave, call _transComplete() - 5. _transComplete: Cleanup, remove cache views, then call the final callback - */ - - // begin the multiple async process of transitioning to the entering view - this._render(transId, enteringView, leavingView, opts, (hasCompleted: boolean) => { - this._transFinish(transId, enteringView, leavingView, opts.direction, hasCompleted); - done(hasCompleted); - }); - } + abstract getActive(): ViewController; /** - * @private + * Returns if the given view is the active view or not. + * @param {ViewController} view + * @returns {boolean} */ - private _setAnimate(opts: NavOptions) { - if ((this._views.length === 1 && !this._init && !this.isPortal) || this.config.get('animate') === false) { - opts.animate = false; - } - } + abstract isActive(view: ViewController): boolean; /** - * @private + * Returns the view controller which is before the given view controller. + * @param {ViewController} view + * @returns {viewController} */ - private _render(transId: number, enteringView: ViewController, leavingView: ViewController, opts: NavOptions, done: Function) { - // compile/load the view into the DOM - - if (enteringView.state === STATE_INACTIVE) { - // this entering view is already set to inactive, so this - // transition must be canceled, so don't continue - return done(); - } - - enteringView.state = STATE_INIT_ENTER; - leavingView.state = STATE_INIT_LEAVE; - - // remember if this nav is already transitioning or not - let isAlreadyTransitioning = this.isTransitioning(); - - if (enteringView.isLoaded()) { - // already compiled this view, do not load again and continue - this._postRender(transId, enteringView, leavingView, isAlreadyTransitioning, opts, done); - - } else { - // view has not been compiled/loaded yet - // continue once the view has finished compiling - // DOM WRITE - this.setTransitioning(true, 500); - - this.loadPage(enteringView, this._viewport, opts, () => { - enteringView.fireLoaded(); - this.viewDidLoad.emit(enteringView); - this._app.viewDidLoad.emit(enteringView); - - this._postRender(transId, enteringView, leavingView, isAlreadyTransitioning, opts, done); - }); - } - } + abstract getPrevious(view: ViewController): ViewController; /** - * @private + * Returns the first view controller in this nav controller's stack. + * @returns {ViewController} */ - private _postRender(transId: number, enteringView: ViewController, leavingView: ViewController, isAlreadyTransitioning: boolean, opts: NavOptions, done: Function) { - // called after _render has completed and the view is compiled/loaded - - if (enteringView.state === STATE_INACTIVE) { - // this entering view is already set to inactive, so this - // transition must be canceled, so don't continue - return done(); - } - - if (!opts.preload) { - // the enteringView will become the active view, and is not being preloaded - - // set the correct zIndex for the entering and leaving views - // if there's already another trans_enter happening then - // the zIndex for the entering view should go off of that one - // DOM WRITE - let lastestLeavingView = this.getByState(STATE_TRANS_ENTER) || leavingView; - this._setZIndex(enteringView, lastestLeavingView, opts.direction); - - // make sure the entering and leaving views are showing - // DOM WRITE - if (isAlreadyTransitioning) { - // the previous transition was still going when this one started - // so to be safe, only update showing the entering/leaving - // don't hide the others when they could still be transitioning - enteringView.domShow(true, this._renderer); - leavingView.domShow(true, this._renderer); - - } else { - // there are no other transitions happening but this one - // only entering/leaving should show, all others hidden - // also if a view is an overlay or the previous view is an - // overlay then always show the overlay and the view before it - var view: ViewController; - var shouldShow: boolean; - - for (var i = 0, ii = this._views.length; i < ii; i++) { - view = this._views[i]; - shouldShow = (view === enteringView) || - (view === leavingView) || - view.isOverlay || - (i < ii - 1 ? this._views[i + 1].isOverlay : false); - view.domShow(shouldShow, this._renderer); - } - } - - // call each view's lifecycle events - if (leavingView.fireOtherLifecycles) { - // only fire entering lifecycle if the leaving - // view hasn't explicitly set not to - enteringView.fireWillEnter(); - this.viewWillEnter.emit(enteringView); - this._app.viewWillEnter.emit(enteringView); - } - - if (enteringView.fireOtherLifecycles) { - // only fire leaving lifecycle if the entering - // view hasn't explicitly set not to - leavingView.fireWillLeave(); - this.viewWillLeave.emit(leavingView); - this._app.viewWillLeave.emit(leavingView); - } - - } else { - // this view is being preloaded, don't call lifecycle events - // transition does not need to animate - opts.animate = false; - } - - this._beforeTrans(enteringView, leavingView, opts, done); - } + abstract first(): ViewController; /** - * @private + * Returns the last page in this nav controller's stack. + * @returns {ViewController} */ - private _beforeTrans(enteringView: ViewController, leavingView: ViewController, opts: NavOptions, done: Function) { - // called after one raf from postRender() - // create the transitions animation, play the animation - // when the transition ends call wait for it to end - - if (enteringView.state === STATE_INACTIVE || enteringView.state === STATE_CANCEL_ENTER) { - // this entering view is already set to inactive or has been canceled - // so this transition must not begin, so don't continue - return done(); - } - - enteringView.state = STATE_TRANS_ENTER; - leavingView.state = STATE_TRANS_LEAVE; - - // everything during the transition should runOutsideAngular - this._zone.runOutsideAngular(() => { - - // init the transition animation - let transitionOpts = { - animation: opts.animation, - direction: opts.direction, - duration: opts.duration, - easing: opts.easing, - renderDelay: opts.transitionDelay || this._trnsDelay, - isRTL: this.config.platform.isRTL(), - ev: opts.ev, - }; - - let transAnimation = this._createTrans(enteringView, leavingView, transitionOpts); - - this._trans && this._trans.destroy(); - this._trans = transAnimation; - - if (opts.animate === false) { - // force it to not animate the elements, just apply the "to" styles - transAnimation.duration(0); - } - - // check if a parent is transitioning and get the time that it ends - let parentTransitionEndTime = this._getLongestTrans(Date.now()); - if (parentTransitionEndTime > 0) { - // the parent is already transitioning and has disabled the app - // so just update the local transitioning information - let duration = parentTransitionEndTime - Date.now(); - this.setTransitioning(true, duration); - - } else { - // this is the only active transition (for now), so disable the app - let keyboardDurationPadding = 0; - if (this._keyboard.isOpen()) { - // add XXms to the duration the app is disabled when the keyboard is open - keyboardDurationPadding = 600; - } - let duration = transAnimation.getDuration() + keyboardDurationPadding; - let enableApp = (duration < 64); - this._app.setEnabled(enableApp, duration); - this.setTransitioning(!enableApp, duration); - } - - // create a callback for when the animation is done - transAnimation.onFinish((trans: Transition) => { - // transition animation has ended - - // destroy the animation and it's element references - trans.destroy(); - - this._afterTrans(enteringView, leavingView, opts, trans.hasCompleted, done); - }); - - // 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 - transAnimation.progressStart(); - - } else { - - // this is a normal animation - // kick it off and let it play through - transAnimation.play(); - } - }); - } + abstract last(): ViewController; /** - * @private + * Returns the index number of the given view controller. + * @param {ViewController} view + * @returns {number} */ - private _afterTrans(enteringView: ViewController, leavingView: ViewController, opts: NavOptions, hasCompleted: boolean, done: Function) { - // transition has completed, update each view's state - // place back into the zone, run didEnter/didLeave - // call the final callback when done - - // run inside of the zone again - this._zone.run(() => { - - if (!opts.preload && hasCompleted) { - if (leavingView.fireOtherLifecycles) { - // only fire entering lifecycle if the leaving - // view hasn't explicitly set not to - enteringView.fireDidEnter(); - this.viewDidEnter.emit(enteringView); - this._app.viewDidEnter.emit(enteringView); - } - - if (enteringView.fireOtherLifecycles && this._init) { - // only fire leaving lifecycle if the entering - // view hasn't explicitly set not to - // and after the nav has initialized - leavingView.fireDidLeave(); - this.viewDidLeave.emit(leavingView); - this._app.viewDidLeave.emit(leavingView); - } - } - - if (enteringView.state === STATE_INACTIVE) { - // this entering view is already set to inactive, so this - // transition must be canceled, so don't continue - return done(hasCompleted); - } - - if (opts.keyboardClose !== false && this._keyboard.isOpen()) { - // the keyboard is still open! - // no problem, let's just close for them - this._keyboard.close(); - this._keyboard.onClose(() => { - - // keyboard has finished closing, transition complete - done(hasCompleted); - }, 32); - - } else { - // all good, transition complete - done(hasCompleted); - } - }); - } + abstract indexOf(view: ViewController): number; /** - * @private + * Returns the number of views in this nav controller. + * @returns {number} The number of views in this stack, including the current view. */ - private _transFinish(transId: number, enteringView: ViewController, leavingView: ViewController, direction: string, hasCompleted: boolean) { - // a transition has completed, but not sure if it's the last one or not - // check if this transition is the most recent one or not - - if (enteringView.state === STATE_CANCEL_ENTER) { - // this view was told to leave before it finished entering - this.remove(enteringView.index, 1); - } - - if (transId === this._transIds) { - // ok, good news, there were no other transitions that kicked - // off during the time this transition started and ended - - if (hasCompleted) { - // this transition has completed as normal - // so the entering one is now the active view - // and the leaving view is now just inactive - if (enteringView.state !== STATE_REMOVE_AFTER_TRANS) { - enteringView.state = STATE_ACTIVE; - } - if (leavingView.state !== STATE_REMOVE_AFTER_TRANS) { - leavingView.state = STATE_INACTIVE; - } - - // only need to do all this clean up if the transition - // completed, otherwise nothing actually changed - // destroy all of the views that come after the active view - this._cleanup(); - - // make sure only this entering view and PREVIOUS view are the - // only two views that are not display:none - // do not make any changes to the stack's current visibility - // if there is an overlay somewhere in the stack - leavingView = this.getPrevious(enteringView); - if (this.hasOverlay()) { - // ensure the entering view is showing - enteringView.domShow(true, this._renderer); - - } else { - // only possibly hide a view if there are no overlays in the stack - this._views.forEach(view => { - let shouldShow = (view === enteringView) || (view === leavingView); - view.domShow(shouldShow, this._renderer); - }); - } - - // this check only needs to happen once, which will add the css - // class to the nav when it's finished its first transition - this._init = true; - - } else { - // this transition has not completed, meaning the - // entering view did not end up as the active view - // this would happen when swipe to go back started - // but the user did not complete the swipe and the - // what was the active view stayed as the active view - leavingView.state = STATE_ACTIVE; - enteringView.state = STATE_INACTIVE; - } - - // check if there is a parent actively transitioning - let transitionEndTime = this._getLongestTrans(Date.now()); - // if transitionEndTime is greater than 0, there is a parent transition occurring - // so delegate enabling the app to the parent. If it <= 0, go ahead and enable the app - if (transitionEndTime <= 0) { - this._app && this._app.setEnabled(true); - } - - // update that this nav is not longer actively transitioning - this.setTransitioning(false); - - // see if we should add the swipe back gesture listeners or not - this._sbCheck(); - - } else { - // darn, so this wasn't the most recent transition - // so while this one did end, there's another more recent one - // still going on. Because a new transition is happening, - // then this entering view isn't actually going to be the active - // one, so only update the state to active/inactive if the state - // wasn't already updated somewhere else during its transition - if (enteringView.state === STATE_TRANS_ENTER) { - enteringView.state = STATE_INACTIVE; - } - if (leavingView.state === STATE_TRANS_LEAVE) { - leavingView.state = STATE_INACTIVE; - } - } - } + abstract length(): number; /** - *@private - * This method is just a wrapper to the Transition function of same name - * to make it easy/possible to mock the method call by overriding the function. - * In testing we don't want to actually do the animation, we want to return a stub instead + * Returns the active child navigation. */ - private _createTrans(enteringView: ViewController, leavingView: ViewController, transitionOpts: any) { - return Transition.createTransition(enteringView, leavingView, transitionOpts); - } - - private _cleanup() { - // ok, cleanup time!! Destroy all of the views that are - // INACTIVE and come after the active view - let activeViewIndex = this.indexOf(this.getActive()); - let destroys = this._views.filter(v => v.state === STATE_REMOVE_AFTER_TRANS); - - for (var i = activeViewIndex + 1; i < this._views.length; i++) { - if (this._views[i].state === STATE_INACTIVE) { - destroys.push(this._views[i]); - } - } - - // all pages being destroyed should be removed from the list of - // pages and completely removed from the dom - destroys.forEach(view => { - this._views.splice(this.indexOf(view), 1); - view.destroy(); - this.viewDidUnload.emit(view); - this._app.viewDidUnload.emit(view); - }); - - // if any z-index goes under 0, then reset them all - let shouldResetZIndex = this._views.some(v => v.zIndex < 0); - if (shouldResetZIndex) { - this._views.forEach(view => { - view.setZIndex(view.zIndex + INIT_ZINDEX + 1, this._renderer); - }); - } - } + abstract getActiveChildNav(): any; /** - * @private + * Returns if the nav controller is actively transitioning or not. + * @return {boolean} */ - getActiveChildNav(): any { - return this._children[this._children.length - 1]; - } - - /** - * @private - */ - registerChildNav(nav: any) { - this._children.push(nav); - } - - /** - * @private - */ - unregisterChildNav(nav: any) { - let index = this._children.indexOf(nav); - if (index > -1) { - this._children.splice(index, 1); - } - } - - /** - * @private - */ - ngOnDestroy() { - for (var i = this._views.length - 1; i >= 0; i--) { - this._views[i].destroy(); - } - this._views.length = 0; - - if (this.parent && this.parent.unregisterChildNav) { - this.parent.unregisterChildNav(this); - } - } - - /** - * @private - */ - loadPage(view: ViewController, viewport: ViewContainerRef, opts: NavOptions, done: Function) { - if (!viewport || !view.componentType) { - return; - } - - // TEMPORARY: automatically set selector w/ dah reflector - // TODO: use componentFactory.create once fixed - addSelector(view.componentType, 'ion-page'); - - this._compiler.resolveComponent(view.componentType).then(componentFactory => { - - if (view.state === STATE_CANCEL_ENTER) { - // view may have already been removed from the stack - // if so, don't even bother adding it - view.destroy(); - this._views.splice(view.index, 1); - return; - } - - // add more providers to just this page - let componentProviders = ReflectiveInjector.resolve([ - provide(NavController, {useValue: this}), - provide(ViewController, {useValue: view}), - provide(NavParams, {useValue: view.getNavParams()}) - ]); - - let childInjector = ReflectiveInjector.fromResolvedProviders(componentProviders, this._viewport.parentInjector); - - let componentRef = componentFactory.create(childInjector, null, null); - - viewport.insert(componentRef.hostView, viewport.length); - - // a new ComponentRef has been created - // set the ComponentRef's instance to its ViewController - view.setInstance(componentRef.instance); - - // the component has been loaded, so call the view controller's loaded method to load any dependencies into the dom - view.loaded(() => { - - // the ElementRef of the actual ion-page created - let pageElementRef = componentRef.location; - - // remember the ChangeDetectorRef for this ViewController - view.setChangeDetector(componentRef.changeDetectorRef); - - // remember the ElementRef to the ion-page elementRef that was just created - view.setPageRef(pageElementRef); - - // auto-add page css className created from component JS class name - let cssClassName = pascalCaseToDashCase(view.componentType.name); - this._renderer.setElementClass(pageElementRef.nativeElement, cssClassName, true); - - view.onDestroy(() => { - // ensure the element is cleaned up for when the view pool reuses this element - this._renderer.setElementAttribute(pageElementRef.nativeElement, 'class', null); - this._renderer.setElementAttribute(pageElementRef.nativeElement, 'style', null); - componentRef.destroy(); - }); - - // our job is done here - done(view); - }); - }); - } - - /** - * @private - */ - swipeBackStart() { - // default the direction to "back" - let opts: NavOptions = { - direction: DIRECTION_BACK, - progressAnimation: true - }; - - // figure out the states of each view in the stack - let leavingView = this._remove(this._views.length - 1, 1); - - if (leavingView) { - opts.animation = leavingView.getTransitionName(opts.direction); - - // get the view thats ready to enter - let enteringView = this.getByState(STATE_INIT_ENTER); - - // start the transition, fire callback when done... - this._transition(enteringView, leavingView, opts, (hasCompleted: boolean) => { - // swipe back has finished!! - console.debug('swipeBack, hasCompleted', hasCompleted); - }); - } - } - - /** - * @private - */ - swipeBackProgress(stepValue: number) { - if (this._trans && this._sbGesture) { - // continue to disable the app while actively dragging - this._app.setEnabled(false, 4000); - this.setTransitioning(true, 4000); - - // set the transition animation's progress - this._trans.progressStep(stepValue); - } - } - - /** - * @private - */ - swipeBackEnd(shouldComplete: boolean, currentStepValue: number) { - if (this._trans && this._sbGesture) { - // the swipe back gesture has ended - this._trans.progressEnd(shouldComplete, currentStepValue); - } - } - - /** - * @private - */ - private _sbCheck() { - if (this._sbEnabled) { - // this nav controller can have swipe to go back - - if (!this._sbGesture) { - // create the swipe back gesture if we haven't already - let opts = { - edge: 'left', - threshold: this._sbThreshold - }; - this._sbGesture = new SwipeBackGesture(this.getNativeElement(), opts, this, this._menuCtrl); - } - - if (this.canSwipeBack()) { - // it is be possible to swipe back - if (!this._sbGesture.isListening) { - this._zone.runOutsideAngular(() => { - // start listening if it's not already - console.debug('swipeBack gesture, listen'); - this._sbGesture.listen(); - }); - } - - } else if (this._sbGesture.isListening) { - // it should not be possible to swipe back - // but the gesture is still listening - console.debug('swipeBack gesture, unlisten'); - this._sbGesture.unlisten(); - } - } - } + abstract isTransitioning(includeAncestors?: boolean): boolean /** * If it's possible to use swipe back or not. If it's not possible @@ -1408,231 +374,13 @@ export class NavController extends Ion { * will return `true`. * @returns {boolean} */ - canSwipeBack(): boolean { - return (this._sbEnabled && !this.isTransitioning() && this._app.isEnabled() && this.canGoBack()); - } + abstract canSwipeBack(): boolean; /** * Returns `true` if there's a valid previous page that we can pop * back to. Otherwise returns `false`. * @returns {boolean} */ - canGoBack(): boolean { - let activeView = this.getActive(); - if (activeView) { - return activeView.enableBack(); - } - return false; - } + abstract canGoBack(): boolean; - /** - * Returns if the nav controller is actively transitioning or not. - * @return {boolean} - */ - isTransitioning(includeAncestors?: boolean): boolean { - let now = Date.now(); - if (includeAncestors && this._getLongestTrans(now) > 0) { - return true; - } - return (this.trnsTime > now); - } - - /** - * @private - */ - setTransitioning(isTransitioning: boolean, fallback: number = 700) { - this.trnsTime = (isTransitioning ? Date.now() + fallback : 0); - } - - /** - * @private - * This method traverses the tree of parents upwards - * and looks at the time the transition ends (if it's transitioning) - * and returns the value that is the furthest into the future - * thus giving us the longest transition duration - */ - private _getLongestTrans(now: number) { - let parentNav = this.parent; - let transitionEndTime = -1; - while (parentNav) { - if (parentNav.trnsTime > transitionEndTime) { - transitionEndTime = parentNav.trnsTime; - } - parentNav = parentNav.parent; - } - // only check if the transitionTime is greater than the current time once - return transitionEndTime > 0 && transitionEndTime > now ? transitionEndTime : 0; - } - - /** - * @private - */ - hasOverlay(): boolean { - for (var i = this._views.length - 1; i >= 0; i--) { - if (this._views[i].isOverlay) { - return true; - } - } - return false; - } - - /** - * @private - */ - getByState(state: number): ViewController { - for (var i = this._views.length - 1; i >= 0; i--) { - if (this._views[i].state === state) { - return this._views[i]; - } - } - return null; - } - - /** - * @param {number} index The index of the page to get. - * @returns {ViewController} Returns the view controller that matches the given index. - */ - getByIndex(index: number): ViewController { - return (index < this._views.length && index > -1 ? this._views[index] : null); - } - - /** - * @returns {ViewController} Returns the active page's view controller. - */ - getActive(): ViewController { - return this.getByState(STATE_ACTIVE); - } - - /** - * @param {ViewController} view - * @returns {boolean} - */ - isActive(view: ViewController): boolean { - return !!(view && view.state === STATE_ACTIVE); - } - - /** - * Returns the view controller which is before the given view controller. - * @param {ViewController} view - * @returns {viewController} - */ - getPrevious(view: ViewController): ViewController { - return this.getByIndex(this.indexOf(view) - 1); - } - - /** - * Returns the first view controller in this nav controller's stack. - * @returns {ViewController} - */ - first(): ViewController { - return (this._views.length ? this._views[0] : null); - } - - /** - * Returns the last page in this nav controller's stack. - * @returns {ViewController} - */ - last(): ViewController { - return (this._views.length ? this._views[this._views.length - 1] : null); - } - - /** - * Returns the index number of the given view controller. - * @param {ViewController} view - * @returns {number} - */ - indexOf(view: ViewController): number { - return this._views.indexOf(view); - } - - /** - * Returns the number of views in this nav controller. - * @returns {number} The number of views in this stack, including the current view. - */ - length(): number { - return this._views.length; - } - - /** - * @private - */ - isSwipeBackEnabled(): boolean { - return this._sbEnabled; - } - - /** - * Returns the root `NavController`. - * @returns {NavController} - */ - get rootNav(): NavController { - let nav = this; - while (nav.parent) { - nav = nav.parent; - } - return nav; - } - - /** - * @private - * Dismiss all pages which have set the `dismissOnPageChange` property. - */ - dismissPageChangeViews() { - this._views.forEach(view => { - if (view.data && view.data.dismissOnPageChange) { - view.dismiss(); - } - }); - } - - /** - * @private - */ - private _setZIndex(enteringView: ViewController, leavingView: ViewController, direction: string) { - if (enteringView) { - // get the leaving view, which could be in various states - if (!leavingView || !leavingView.isLoaded()) { - // the leavingView is a mocked view, either we're - // actively transitioning or it's the initial load - - var previousView = this.getPrevious(enteringView); - if (previousView && previousView.isLoaded()) { - // we found a better previous view to reference - // use this one instead - enteringView.setZIndex(previousView.zIndex + 1, this._renderer); - - } else { - // this is the initial view - enteringView.setZIndex(this.isPortal ? PORTAL_ZINDEX : INIT_ZINDEX, this._renderer); - } - - } else if (direction === DIRECTION_BACK) { - // moving back - enteringView.setZIndex(leavingView.zIndex - 1, this._renderer); - - } else { - // moving forward - enteringView.setZIndex(leavingView.zIndex + 1, this._renderer); - } - } - } - -} - -const STATE_ACTIVE = 1; -const STATE_INACTIVE = 2; -const STATE_INIT_ENTER = 3; -const STATE_INIT_LEAVE = 4; -const STATE_TRANS_ENTER = 5; -const STATE_TRANS_LEAVE = 6; -const STATE_REMOVE = 7; -const STATE_REMOVE_AFTER_TRANS = 8; -const STATE_CANCEL_ENTER = 9; -const STATE_FORCE_ACTIVE = 10; - -export const DIRECTION_BACK = 'back'; -export const DIRECTION_FORWARD = 'forward'; - -const INIT_ZINDEX = 100; -const PORTAL_ZINDEX = 9999; - -let ctrlIds = -1; \ No newline at end of file +} \ No newline at end of file diff --git a/src/components/nav/nav-interfaces.ts b/src/components/nav/nav-interfaces.ts index 6337470c28..55f2278053 100644 --- a/src/components/nav/nav-interfaces.ts +++ b/src/components/nav/nav-interfaces.ts @@ -13,3 +13,6 @@ export interface NavOptions { climbNav?: boolean; ev?: any; } + +export const DIRECTION_BACK = 'back'; +export const DIRECTION_FORWARD = 'forward'; diff --git a/src/components/nav/nav-pop.ts b/src/components/nav/nav-pop.ts index 6a3bbed73b..3ed4e37076 100644 --- a/src/components/nav/nav-pop.ts +++ b/src/components/nav/nav-pop.ts @@ -1,11 +1,12 @@ -import { Directive, Optional } from '@angular/core'; +import { Directive, HostListener, Input, Optional } from '@angular/core'; import { NavController } from './nav-controller'; - +import { noop } from '../../util/util'; /** * @name NavPop * @description - * Directive for declaratively pop the current page off from the navigation stack. + * Directive to declaratively pop the current page off from the + * navigation stack. * * @usage * ```html @@ -22,11 +23,7 @@ import { NavController } from './nav-controller'; * @see {@link ../NavPush NavPush API Docs} */ @Directive({ - selector: '[nav-pop]', - host: { - '(click)': 'onClick()', - 'role': 'link' - } + selector: '[navPop]' }) export class NavPop { @@ -36,10 +33,15 @@ export class NavPop { } } - /** - * @private - */ - onClick() { - this._nav && this._nav.pop(); + @HostListener('click') + onClick(): boolean { + // If no target, or if target is _self, prevent default browser behavior + if (this._nav) { + this._nav.pop(null, noop); + return false; + } + + return true; } + } diff --git a/src/components/nav/nav-portal.ts b/src/components/nav/nav-portal.ts index d7bf91b1fd..38ee8a4b6b 100644 --- a/src/components/nav/nav-portal.ts +++ b/src/components/nav/nav-portal.ts @@ -2,9 +2,9 @@ import { ComponentResolver, Directive, ElementRef, forwardRef, Inject, NgZone, O import { App } from '../app/app'; import { Config } from '../../config/config'; +import { GestureController } from '../../gestures/gesture-controller'; import { Keyboard } from '../../util/keyboard'; -import { MenuController } from '../menu/menu-controller'; -import { NavController } from '../nav/nav-controller'; +import { NavControllerBase } from '../nav/nav-controller-base'; /** * @private @@ -12,7 +12,7 @@ import { NavController } from '../nav/nav-controller'; @Directive({ selector: '[nav-portal]' }) -export class NavPortal extends NavController { +export class NavPortal extends NavControllerBase { constructor( @Inject(forwardRef(() => App)) app: App, config: Config, @@ -21,11 +21,11 @@ export class NavPortal extends NavController { zone: NgZone, renderer: Renderer, compiler: ComponentResolver, - menuCtrl: MenuController, + gestureCtrl: GestureController, viewPort: ViewContainerRef ) { - super(null, app, config, keyboard, elementRef, zone, renderer, compiler, menuCtrl); - this.isPortal = true; + super(null, app, config, keyboard, elementRef, zone, renderer, compiler, gestureCtrl); + this._isPortal = true; this.setViewport(viewPort); app.setPortal(this); diff --git a/src/components/nav/nav-push.ts b/src/components/nav/nav-push.ts index 8437077aa2..c22414b9d0 100644 --- a/src/components/nav/nav-push.ts +++ b/src/components/nav/nav-push.ts @@ -1,28 +1,35 @@ -import { Directive, Input, Optional } from '@angular/core'; +import { Directive, HostListener, Input, Optional } from '@angular/core'; import { NavController } from './nav-controller'; +import { noop } from '../../util/util'; /** * @name NavPush * @description - * Directive for declaratively linking to a new page instead of using - * {@link ../NavController/#push NavController.push}. Similar to ui-router's `ui-sref`. + * Directive to declaratively push a new page to the current nav + * stack. * * @usage * ```html * * ``` - * To specify parameters you can use array syntax or the `nav-params` property: + * + * To specify parameters you can use array syntax or the `navParams` + * property: + * * ```html - * + * * ``` - * Where `pushPage` and `params` are specified in your component, and `pushPage` - * contains a reference to a [@Page component](../../../config/Page/): + * + * Where `pushPage` and `params` are specified in your component, + * and `pushPage` contains a reference to a + * [@Page component](../../../config/Page/): * * ```ts - * import {LoginPage} from 'login'; + * import { LoginPage } from './login'; + * * @Component({ - * template: `` + * template: `` * }) * class MyPage { * constructor(){ @@ -32,61 +39,42 @@ import { NavController } from './nav-controller'; * } * ``` * - * ### Alternate syntax - * You can also use syntax similar to Angular2's router, passing an array to - * NavPush: - * ```html - * - * ``` * @demo /docs/v2/demos/navigation/ * @see {@link /docs/v2/components#navigation Navigation Component Docs} * @see {@link ../NavPop NavPop API Docs} + * */ @Directive({ - selector: '[navPush]', - host: { - '(click)': 'onClick()', - 'role': 'link' - } + selector: '[navPush]' }) export class NavPush { /** - * @input {Page} the page you want to push - */ - @Input() navPush: any; - - /** - * @input {any} Any parameters you want to pass along - */ - @Input() navParams: any; - - constructor( - @Optional() private _nav: NavController - ) { - if (!_nav) { - console.error('nav-push must be within a NavController'); - } - } - - /** - * @private + * @input {Page} The Page to push onto the Nav. */ - onClick() { - let destination: any, params: any; + @Input() navPush: any[]|string; - if (this.navPush instanceof Array) { - if (this.navPush.length > 2) { - throw 'Too many [navPush] arguments, expects [View, { params }]'; - } - destination = this.navPush[0]; - params = this.navPush[1] || this.navParams; + /** + * @input {any} Parameters to pass to the page. + */ + @Input() navParams: {[k: string]: any}; - } else { - destination = this.navPush; - params = this.navParams; + + constructor(@Optional() private _nav: NavController) { + if (!_nav) { + console.error('navPush must be within a NavController'); + } + } + + @HostListener('click') + onClick(): boolean { + // If no target, or if target is _self, prevent default browser behavior + if (this._nav) { + this._nav.push(this.navPush, this.navParams, noop); + return false; } - this._nav && this._nav.push(destination, params); + return true; } + } diff --git a/src/components/nav/nav.ts b/src/components/nav/nav.ts index c49123f595..1b6ad476a4 100644 --- a/src/components/nav/nav.ts +++ b/src/components/nav/nav.ts @@ -3,9 +3,9 @@ import { AfterViewInit, Component, ComponentResolver, ElementRef, Input, Optiona import { App } from '../app/app'; import { Config } from '../../config/config'; import { Keyboard } from '../../util/keyboard'; +import { GestureController } from '../../gestures/gesture-controller'; import { isTrueProperty } from '../../util/util'; -import { MenuController } from '../menu/menu-controller'; -import { NavController } from './nav-controller'; +import { NavControllerBase } from './nav-controller-base'; import { ViewController } from './view-controller'; /** @@ -114,13 +114,13 @@ import { ViewController } from './view-controller'; `, encapsulation: ViewEncapsulation.None, }) -export class Nav extends NavController implements AfterViewInit { +export class Nav extends NavControllerBase implements AfterViewInit { private _root: any; private _hasInit: boolean = false; constructor( @Optional() viewCtrl: ViewController, - @Optional() parent: NavController, + @Optional() parent: NavControllerBase, app: App, config: Config, keyboard: Keyboard, @@ -128,9 +128,9 @@ export class Nav extends NavController implements AfterViewInit { zone: NgZone, renderer: Renderer, compiler: ComponentResolver, - menuCtrl: MenuController + gestureCtrl: GestureController ) { - super(parent, app, config, keyboard, elementRef, zone, renderer, compiler, menuCtrl); + super(parent, app, config, keyboard, elementRef, zone, renderer, compiler, gestureCtrl); if (viewCtrl) { // an ion-nav can also act as an ion-page within a parent ion-nav @@ -164,9 +164,6 @@ export class Nav extends NavController implements AfterViewInit { this._hasInit = true; if (this._root) { - if (typeof this._root !== 'function') { - throw 'The [root] property in must be given a reference to a component class from within the constructor.'; - } this.push(this._root); } } diff --git a/src/components/nav/swipe-back.ts b/src/components/nav/swipe-back.ts index 04ab090627..ebd34cfda0 100644 --- a/src/components/nav/swipe-back.ts +++ b/src/components/nav/swipe-back.ts @@ -1,6 +1,7 @@ import { assign } from '../../util/util'; +import { GestureController, GestureDelegate, GesturePriority } from '../../gestures/gesture-controller'; import { MenuController } from '../menu/menu-controller'; -import { NavController } from './nav-controller'; +import { NavControllerBase } from './nav-controller-base'; import { SlideData } from '../../gestures/slide-gesture'; import { SlideEdgeGesture } from '../../gestures/slide-edge-gesture'; @@ -10,36 +11,32 @@ export class SwipeBackGesture extends SlideEdgeGesture { constructor( element: HTMLElement, options: any, - private _nav: NavController, - private _menuCtrl: MenuController + private _nav: NavControllerBase, + gestureCtlr: GestureController ) { super(element, assign({ direction: 'x', - maxEdgeStart: 75 + maxEdgeStart: 75, + gesture: gestureCtlr.create('goback-swipe', { + priority: GesturePriority.GoBackSwipe, + }) }, options)); } - canStart(ev: any) { + canStart(ev: any): boolean { // the gesture swipe angle must be mainly horizontal and the // gesture distance would be relatively short for a swipe back // and swipe back must be possible on this nav controller - if (ev.angle > -40 && - ev.angle < 40 && - ev.distance < 50 && - this._nav.canSwipeBack()) { - // passed the tests, now see if the super says it's cool or not - return super.canStart(ev); - } - - // nerp, not today - return false; + return ( + this._nav.canSwipeBack() && + super.canStart(ev) + ); } - onSlideBeforeStart(slideData: SlideData, ev: any) { - console.debug('swipeBack, onSlideBeforeStart', ev.srcEvent.type); - this._nav.swipeBackStart(); - this._menuCtrl.tempDisable(true); + onSlideBeforeStart(slideData: SlideData, ev: any) { + console.debug('swipeBack, onSlideBeforeStart', ev.type); + this._nav.swipeBackStart(); } onSlide(slide: SlideData) { @@ -49,15 +46,10 @@ export class SwipeBackGesture extends SlideEdgeGesture { } onSlideEnd(slide: SlideData, ev: any) { - let shouldComplete = (Math.abs(ev.velocityX) > 0.2 || Math.abs(slide.delta) > Math.abs(slide.max) * 0.5); - + let shouldComplete = (Math.abs(slide.velocity) > 0.2 || Math.abs(slide.delta) > Math.abs(slide.max) * 0.5); let currentStepValue = (slide.distance / slide.max); console.debug('swipeBack, onSlideEnd, shouldComplete', shouldComplete, 'currentStepValue', currentStepValue); - this._nav.swipeBackEnd(shouldComplete, currentStepValue); - - this._menuCtrl.tempDisable(false); } - } diff --git a/src/components/nav/test/basic/index.ts b/src/components/nav/test/basic/index.ts index 2b675d2808..12b1645f14 100644 --- a/src/components/nav/test/basic/index.ts +++ b/src/components/nav/test/basic/index.ts @@ -1,6 +1,6 @@ import { Component, ViewChild } from '@angular/core'; import { NavController, AlertController, Content } from '../../../../../src'; -import { ionicBootstrap } from '../../../../../src'; +import { ionicBootstrap, App } from '../../../../../src'; import { NavParams, ViewController } from '../../../../../src';; @@ -148,6 +148,7 @@ class FirstPage { class FullPage { constructor( private nav: NavController, + private app: App, private alertCtrl: AlertController, private params: NavParams ) {} @@ -184,8 +185,8 @@ class FullPage { // overlays are added and removed from the root navigation // ensure you using the root navigation, and pop this alert // when the alert is done animating out, then pop off the active page - this.nav.rootNav.pop().then(() => { - this.nav.rootNav.pop(); + this.app.getRootNav().pop().then(() => { + this.app.getRootNav().pop(); }); // by default an alert will dismiss itself diff --git a/src/components/nav/test/child-navs/index.ts b/src/components/nav/test/child-navs/index.ts index ae12253bbb..f1af093062 100644 --- a/src/components/nav/test/child-navs/index.ts +++ b/src/components/nav/test/child-navs/index.ts @@ -1,5 +1,5 @@ -import {Component} from '@angular/core'; -import {ionicBootstrap, NavController} from '../../../../../src'; +import { Component} from '@angular/core'; +import { ionicBootstrap, NavController } from '../../../../../src'; @Component({ template: ``, @@ -29,11 +29,11 @@ ionicBootstrap(E2EApp); }) class LandingPage{ - constructor(private _navController: NavController){ + constructor(private nav: NavController){ } goToPage(){ - this._navController.push(FirstPage); + this.nav.push(FirstPage); } } diff --git a/src/components/nav/test/insert-views/index.ts b/src/components/nav/test/insert-views/index.ts index e2a85e4c04..e0ba9b1356 100644 --- a/src/components/nav/test/insert-views/index.ts +++ b/src/components/nav/test/insert-views/index.ts @@ -1,5 +1,5 @@ -import {Component} from '@angular/core'; -import {ionicBootstrap, NavController} from '../../../../../src'; +import { Component} from '@angular/core'; +import { ionicBootstrap, NavController } from '../../../../../src'; @Component({ diff --git a/src/components/nav/test/memory/index.ts b/src/components/nav/test/memory/index.ts index d0af5d4555..232f982b91 100644 --- a/src/components/nav/test/memory/index.ts +++ b/src/components/nav/test/memory/index.ts @@ -1,5 +1,5 @@ -import {Component} from '@angular/core'; -import {ionicBootstrap, NavController} from '../../../../../src'; +import { Component} from '@angular/core'; +import { ionicBootstrap, NavController } from '../../../../../src'; let delay = 100; @@ -16,7 +16,7 @@ let count = 0; ` }) class Page1 { - tmr; + tmr: number; constructor(private nav: NavController) {} @@ -50,7 +50,7 @@ class Page1 { ` }) class Page2 { - tmr; + tmr: number; constructor(private nav: NavController) {} diff --git a/src/components/nav/test/nav-controller.spec.ts b/src/components/nav/test/nav-controller.spec.ts index 213a1238e0..d609791eba 100644 --- a/src/components/nav/test/nav-controller.spec.ts +++ b/src/components/nav/test/nav-controller.spec.ts @@ -1,1739 +1,1616 @@ -import { NavController, Tabs, NavOptions, Config, ViewController, App, Platform } from '../../../../src'; +import { Renderer } from '@angular/core'; +import { App, Config, Form, Keyboard, MenuController, NavOptions, Platform, Tabs, ViewController } from '../../../../src'; +import { NavControllerBase } from '../../../../src/components/nav/nav-controller-base'; +import { STATE_ACTIVE, STATE_INACTIVE, STATE_INIT_ENTER, STATE_INIT_LEAVE, STATE_TRANS_ENTER, STATE_TRANS_LEAVE, STATE_REMOVE, STATE_REMOVE_AFTER_TRANS, STATE_CANCEL_ENTER, STATE_FORCE_ACTIVE } from '../../../../src/components/nav/nav-controller-base'; +import { mockNavController, mockElementRef, mockTransition } from '../../../../src/util/mock-providers'; export function run() { - describe('NavController', () => { - describe('pop', () => { +describe('NavController', () => { - it('should do nothing if its the first view in the stack', () => { - let view1 = new ViewController(Page1); - view1.state = STATE_ACTIVE; - nav.views = [view1]; + describe('pop', () => { - expect(nav.length()).toBe(1); + it('should do nothing if its the first view in the stack', () => { + let view1 = new ViewController(Page1); + view1.state = STATE_ACTIVE; + nav._views = [view1]; - nav.pop(); + expect(nav.length()).toBe(1); - expect(nav.length()).toBe(1); - expect(nav.getByIndex(0).state).toBe(STATE_ACTIVE); - expect(nav.getByIndex(0).componentType).toBe(Page1); - }); + nav.pop(); + expect(nav.length()).toBe(1); + expect(nav.getByIndex(0).state).toBe(STATE_ACTIVE); + expect(nav.getByIndex(0).componentType).toBe(Page1); }); - describe('popToRoot', () => { - - it('should go back to root', () => { - let view1 = new ViewController(Page1); - view1.state = STATE_INACTIVE; - let view2 = new ViewController(Page2); - view2.state = STATE_INACTIVE; - let view3 = new ViewController(Page3); - view3.state = STATE_INACTIVE; - let view4 = new ViewController(Page4); - view4.state = STATE_ACTIVE; - nav.views = [view1, view2, view3, view4]; - - nav.popToRoot(); - expect(nav.length()).toBe(2); - expect(nav.getByIndex(0).state).toBe(STATE_INIT_ENTER); - expect(nav.getByIndex(0).componentType).toBe(Page1); - expect(nav.getByIndex(1).state).toBe(STATE_INIT_LEAVE); - expect(nav.getByIndex(1).componentType).toBe(Page4); - - expect(view2.state).toBe(STATE_REMOVE); - expect(view3.state).toBe(STATE_REMOVE); - }); - - }); - - describe('popTo', () => { - - it('should go back two views', () => { - let view1 = new ViewController(Page1); - view1.state = STATE_INACTIVE; - let view2 = new ViewController(Page2); - view2.state = STATE_INACTIVE; - let view3 = new ViewController(Page3); - view3.state = STATE_INACTIVE; - let view4 = new ViewController(Page4); - view4.state = STATE_ACTIVE; - nav.views = [view1, view2, view3, view4]; - - nav.popTo(view2); - - expect(nav.length()).toBe(3); - expect(nav.getByIndex(0).state).toBe(STATE_INACTIVE); - expect(nav.getByIndex(0).componentType).toBe(Page1); - expect(nav.getByIndex(1).state).toBe(STATE_INIT_ENTER); - expect(nav.getByIndex(1).componentType).toBe(Page2); - expect(view3.state).toBe(STATE_REMOVE); - expect(nav.getByIndex(2).state).toBe(STATE_INIT_LEAVE); - expect(nav.getByIndex(2).componentType).toBe(Page4); - }); - - }); - - describe('remove', () => { - - it('should create opts if passed in arg is undefined or null', () => { - let view1 = new ViewController(Page1); - view1.state = STATE_INACTIVE; - let view2 = new ViewController(Page2); - view2.state = STATE_ACTIVE; - nav.views = [view1, view2]; - - nav.remove(1, 1, null); - }); - - }); - - describe('_remove', () => { - - it('should reassign activily transitioning leave that isnt getting removed, to become force active', () => { - let view1 = new ViewController(Page1); - view1.state = STATE_INACTIVE; - let view2 = new ViewController(Page2); - view2.state = STATE_TRANS_LEAVE; - let view3 = new ViewController(Page3); - view3.state = STATE_TRANS_ENTER; - nav.views = [view1, view2, view3]; - - nav._remove(2, 1); - - expect(nav.getByIndex(0).state).toBe(STATE_INACTIVE); - expect(nav.getByIndex(0).componentType).toBe(Page1); - expect(nav.getByIndex(1).state).toBe(STATE_FORCE_ACTIVE); - expect(nav.getByIndex(1).componentType).toBe(Page2); - expect(nav.getByIndex(2).state).toBe(STATE_REMOVE_AFTER_TRANS); - expect(nav.getByIndex(2).componentType).toBe(Page3); - }); - - it('should reassign activily transitioning views that should be removed to STATE_REMOVE_AFTER_TRANS', () => { - let view1 = new ViewController(Page1); - view1.state = STATE_INACTIVE; - let view2 = new ViewController(Page2); - view2.state = STATE_TRANS_ENTER; - let view3 = new ViewController(Page3); - view3.state = STATE_TRANS_LEAVE; - nav.views = [view1, view2, view3]; - - nav._remove(1, 2); - expect(nav.getByIndex(0).state).toBe(STATE_INACTIVE); - expect(nav.getByIndex(0).componentType).toBe(Page1); - expect(nav.getByIndex(1).state).toBe(STATE_REMOVE_AFTER_TRANS); - expect(nav.getByIndex(1).componentType).toBe(Page2); - expect(nav.getByIndex(2).state).toBe(STATE_REMOVE_AFTER_TRANS); - expect(nav.getByIndex(2).componentType).toBe(Page3); - }); - - it('should keep same init leave, but set previous init enter to inactive', () => { - let view1 = new ViewController(Page1); - view1.state = STATE_INACTIVE; - let view2 = new ViewController(Page2); - view2.state = STATE_INIT_ENTER; - let view3 = new ViewController(Page3); - view3.state = STATE_INIT_LEAVE; - nav.views = [view1, view2, view3]; - - nav._remove(1, 1); - expect(nav.length()).toBe(3); - expect(view1.state).toBe(STATE_INIT_ENTER); - expect(view2.state).toBe(STATE_CANCEL_ENTER); - expect(view3.state).toBe(STATE_INIT_LEAVE); - - expect(nav.getByIndex(0).state).toBe(STATE_INIT_ENTER); - expect(nav.getByIndex(0).componentType).toBe(Page1); - expect(nav.getByIndex(1).state).toBe(STATE_CANCEL_ENTER); - expect(nav.getByIndex(1).componentType).toBe(Page2); - expect(nav.getByIndex(2).state).toBe(STATE_INIT_LEAVE); - expect(nav.getByIndex(2).componentType).toBe(Page3); - }); - - it('should set to pop the active and enter the previous', () => { - let view1 = new ViewController(Page1); - view1.state = STATE_INACTIVE; - let view2 = new ViewController(Page2); - view2.state = STATE_ACTIVE; - nav.views = [view1, view2]; - - nav._remove(1, 1); - expect(view1.state).toBe(STATE_INIT_ENTER); - expect(view2.state).toBe(STATE_INIT_LEAVE); - }); - - it('should set to remove 2 views before active one, active stays the same', () => { - let view1 = new ViewController(Page1); - view1.state = STATE_INACTIVE; - let view2 = new ViewController(Page2); - view2.state = STATE_INACTIVE; - let view3 = new ViewController(Page3); - view3.state = STATE_INACTIVE; - let view4 = new ViewController(Page4); - view4.state = STATE_INACTIVE; - let view5 = new ViewController(Page5); - view5.state = STATE_ACTIVE; - nav.views = [view1, view2, view3, view4, view5]; - - nav._remove(2, 2); - expect(nav.length()).toBe(3); - expect(view1.state).toBe(STATE_INACTIVE); - expect(view2.state).toBe(STATE_INACTIVE); - expect(view3.state).toBe(STATE_REMOVE); - expect(view4.state).toBe(STATE_REMOVE); - expect(view5.state).toBe(STATE_ACTIVE); - - expect(nav.getByIndex(0).state).toBe(STATE_INACTIVE); - expect(nav.getByIndex(0).componentType).toBe(Page1); - expect(nav.getByIndex(1).state).toBe(STATE_INACTIVE); - expect(nav.getByIndex(1).componentType).toBe(Page2); - expect(nav.getByIndex(2).state).toBe(STATE_ACTIVE); - expect(nav.getByIndex(2).componentType).toBe(Page5); - }); - - it('should set to remove all views other than the first', () => { - let view1 = new ViewController(Page1); - view1.state = STATE_INACTIVE; - let view2 = new ViewController(Page2); - view2.state = STATE_INACTIVE; - let view3 = new ViewController(Page3); - view3.state = STATE_INACTIVE; - let view4 = new ViewController(Page4); - view4.state = STATE_ACTIVE; - nav.views = [view1, view2, view3, view4]; - - nav._remove(1, 9999); - expect(nav.length()).toBe(2); - expect(view1.state).toBe(STATE_INIT_ENTER); - expect(view2.state).toBe(STATE_REMOVE); - expect(view3.state).toBe(STATE_REMOVE); - expect(view4.state).toBe(STATE_INIT_LEAVE); - - expect(nav.getByIndex(0).state).toBe(STATE_INIT_ENTER); - expect(nav.getByIndex(0).componentType).toBe(Page1); - expect(nav.getByIndex(1).state).toBe(STATE_INIT_LEAVE); - expect(nav.getByIndex(1).componentType).toBe(Page4); - }); - - it('should set to remove 3 views and enter the first inactive one, remove includes active one', () => { - let view1 = new ViewController(Page1); - view1.state = STATE_INACTIVE; - let view2 = new ViewController(Page2); - view2.state = STATE_INACTIVE; - let view3 = new ViewController(Page3); - view3.state = STATE_INACTIVE; - let view4 = new ViewController(Page4); - view4.state = STATE_ACTIVE; - nav.views = [view1, view2, view3, view4]; - - nav._remove(1, 3); - expect(nav.length()).toBe(2); - expect(view1.state).toBe(STATE_INIT_ENTER); - expect(view2.state).toBe(STATE_REMOVE); - expect(view3.state).toBe(STATE_REMOVE); - expect(view4.state).toBe(STATE_INIT_LEAVE); - - expect(nav.getByIndex(0).state).toBe(STATE_INIT_ENTER); - expect(nav.getByIndex(0).componentType).toBe(Page1); - expect(nav.getByIndex(1).state).toBe(STATE_INIT_LEAVE); - expect(nav.getByIndex(1).componentType).toBe(Page4); - }); - - it('should set to remove the active and enter the previous', () => { - let view1 = new ViewController(Page1); - view1.state = STATE_INACTIVE; - let view2 = new ViewController(Page2); - view2.state = STATE_ACTIVE; - nav.views = [view1, view2]; - - nav._remove(1, 1); - expect(view1.state).toBe(STATE_INIT_ENTER); - expect(view2.state).toBe(STATE_INIT_LEAVE); - }); - - it('should set to remove the only view in the stack', () => { - let view1 = new ViewController(Page1); - view1.state = STATE_ACTIVE; - nav.views = [view1]; - - nav._remove(0, 1); - expect(nav.getByIndex(0).state).toBe(STATE_INIT_LEAVE); - }); - - it('should call willLeave/didLeave/destroy on views with STATE_REMOVE', () => { - let view1 = new ViewController(Page1); - view1.state = STATE_INACTIVE; - let view2 = new ViewController(Page2); - view2.state = STATE_INACTIVE; - let view3 = new ViewController(Page3); - view3.state = STATE_INACTIVE; - let view4 = new ViewController(Page4); - view4.state = STATE_ACTIVE; - nav.views = [view1, view2, view3, view4]; - - spyOn(view1, 'fireWillLeave'); - spyOn(view1, 'fireDidLeave'); - spyOn(view1, 'destroy'); - - spyOn(view2, 'fireWillLeave'); - spyOn(view2, 'fireDidLeave'); - spyOn(view2, 'destroy'); - - spyOn(view3, 'fireWillLeave'); - spyOn(view3, 'fireDidLeave'); - spyOn(view3, 'destroy'); - - spyOn(view4, 'fireWillLeave'); - spyOn(view4, 'fireDidLeave'); - spyOn(view4, 'destroy'); - - nav._remove(1, 3); - expect(nav.length()).toBe(2); - expect(view1.state).toBe(STATE_INIT_ENTER); - expect(view2.state).toBe(STATE_REMOVE); - expect(view3.state).toBe(STATE_REMOVE); - expect(view4.state).toBe(STATE_INIT_LEAVE); - - expect(view1.fireWillLeave).not.toHaveBeenCalled(); - expect(view1.fireDidLeave).not.toHaveBeenCalled(); - expect(view1.destroy).not.toHaveBeenCalled(); - - expect(view2.fireWillLeave).toHaveBeenCalled(); - expect(view2.fireDidLeave).toHaveBeenCalled(); - expect(view2.destroy).toHaveBeenCalled(); - - expect(view3.fireWillLeave).toHaveBeenCalled(); - expect(view3.fireDidLeave).toHaveBeenCalled(); - expect(view3.destroy).toHaveBeenCalled(); - - expect(view4.fireWillLeave).not.toHaveBeenCalled(); - expect(view4.fireDidLeave).not.toHaveBeenCalled(); - expect(view4.destroy).not.toHaveBeenCalled(); - }); - }); - - describe('_cleanup', () => { - it('should destroy views that are inactive after the active view', () => { - let view1 = new ViewController(Page1); - view1.state = STATE_INACTIVE; - let view2 = new ViewController(Page2); - view2.state = STATE_ACTIVE; - let view3 = new ViewController(Page3); - view3.state = STATE_INACTIVE; - let view4 = new ViewController(Page4); - view4.state = STATE_TRANS_ENTER; - let view5 = new ViewController(Page5); - view5.state = STATE_INACTIVE; - nav.views = [view1, view2, view3, view4, view5]; - nav._cleanup(); - - expect(nav.length()).toBe(3); - expect(nav.getByIndex(0).state).toBe(STATE_INACTIVE); - expect(nav.getByIndex(0).componentType).toBe(Page1); - expect(nav.getByIndex(1).state).toBe(STATE_ACTIVE); - expect(nav.getByIndex(1).componentType).toBe(Page2); - expect(nav.getByIndex(2).state).toBe(STATE_TRANS_ENTER); - expect(nav.getByIndex(2).componentType).toBe(Page4); - }); - - it('should not destroy any views since the last is active', () => { - let view1 = new ViewController(Page1); - view1.state = STATE_INACTIVE; - let view2 = new ViewController(Page2); - view2.state = STATE_ACTIVE; - nav.views = [view1, view2]; - nav._cleanup(); - expect(nav.length()).toBe(2); - }); - - it('should call destroy for each view to be destroyed', () => { - let view1 = new ViewController(Page1); - view1.state = STATE_ACTIVE; - let view2 = new ViewController(Page2); - view2.state = STATE_INACTIVE; - let view3 = new ViewController(Page3); - view3.state = STATE_INACTIVE; - nav.views = [view1, view2, view3]; - - spyOn(view1, 'destroy'); - spyOn(view2, 'destroy'); - spyOn(view3, 'destroy'); - - nav._cleanup(); - - expect(nav.length()).toBe(1); - expect(view1.destroy).not.toHaveBeenCalled(); - expect(view2.destroy).toHaveBeenCalled(); - expect(view3.destroy).toHaveBeenCalled(); - }); - - it('should reset zIndexes if their is a negative zindex', () => { - let view1 = new ViewController(Page1); - view1.setPageRef( getElementRef() ); - view1.state = STATE_INACTIVE; - view1.zIndex = -1; - - let view2 = new ViewController(Page2); - view2.setPageRef( getElementRef() ); - view2.state = STATE_INACTIVE; - view2.zIndex = 0; - - let view3 = new ViewController(Page3); - view3.setPageRef( getElementRef() ); - view3.state = STATE_ACTIVE; - view3.zIndex = 1; - - nav.views = [view1, view2, view3]; - nav._cleanup(); - - expect(view1.zIndex).toEqual(100); - expect(view2.zIndex).toEqual(101); - expect(view3.zIndex).toEqual(102); - }); - }); - - describe('_postRender', () => { - it('should immediately call done when enteringView state is inactive', () => { - let view1 = new ViewController(Page1); - view1.state = STATE_INACTIVE; - var wasCalled = false; - var done = () => { - wasCalled = true; - }; - nav._beforeTrans = () => {}; //prevent running beforeTrans for tests - - nav._postRender(1, view1, null, false, null, done); - - expect(wasCalled).toBe(true); - }); - - it('should call willEnter on entering view', () => { - let enteringView = new ViewController(Page1); - let leavingView = new ViewController(Page2); - var navOptions: NavOptions = {}; - var done = () => {}; - nav._beforeTrans = () => {}; //prevent running beforeTrans for tests - - spyOn(enteringView, 'fireWillEnter'); - - nav._postRender(1, enteringView, leavingView, false, navOptions, done); - - expect(enteringView.fireWillEnter).toHaveBeenCalled(); - }); - - it('should not call willEnter on entering view when it is being preloaded', () => { - let enteringView = new ViewController(Page1); - let leavingView = new ViewController(Page2); - var navOptions: NavOptions = { - preload: true - }; - var done = () => {}; - nav._beforeTrans = () => {}; //prevent running beforeTrans for tests - - spyOn(enteringView, 'fireWillEnter'); - - nav._postRender(1, enteringView, leavingView, false, navOptions, done); - - expect(enteringView.fireWillEnter).not.toHaveBeenCalled(); - }); - - it('should call willLeave on leaving view', () => { - let enteringView = new ViewController(Page1); - let leavingView = new ViewController(Page2); - var navOptions: NavOptions = {}; - var done = () => {}; - nav._beforeTrans = () => {}; //prevent running beforeTrans for tests - - spyOn(leavingView, 'fireWillLeave'); - - nav._postRender(1, enteringView, leavingView, false, navOptions, done); - - expect(leavingView.fireWillLeave).toHaveBeenCalled(); - }); - - it('should not call willEnter when the leaving view has fireOtherLifecycles not true', () => { - let enteringView = new ViewController(Page1); - let leavingView = new ViewController(Page2); - var navOptions: NavOptions = {}; - var done = () => {}; - nav._beforeTrans = () => {}; //prevent running beforeTrans for tests - - spyOn(enteringView, 'fireWillEnter'); - spyOn(leavingView, 'fireWillLeave'); - - leavingView.fireOtherLifecycles = false; - - nav._postRender(1, enteringView, leavingView, false, navOptions, done); - - expect(enteringView.fireWillEnter).not.toHaveBeenCalled(); - expect(leavingView.fireWillLeave).toHaveBeenCalled(); - }); - - it('should not call willLeave when the entering view has fireOtherLifecycles not true', () => { - let enteringView = new ViewController(Page1); - let leavingView = new ViewController(Page2); - var navOptions: NavOptions = {}; - var done = () => {}; - nav._beforeTrans = () => {}; //prevent running beforeTrans for tests - - spyOn(enteringView, 'fireWillEnter'); - spyOn(leavingView, 'fireWillLeave'); - - enteringView.fireOtherLifecycles = false; - - nav._postRender(1, enteringView, leavingView, false, navOptions, done); - - expect(enteringView.fireWillEnter).toHaveBeenCalled(); - expect(leavingView.fireWillLeave).not.toHaveBeenCalled(); - }); - - it('should not call willLeave on leaving view when it is being preloaded', () => { - let enteringView = new ViewController(Page1); - let leavingView = new ViewController(Page2); - var navOptions: NavOptions = { - preload: true - }; - var done = () => {}; - nav._beforeTrans = () => {}; //prevent running beforeTrans for tests - - spyOn(leavingView, 'fireWillLeave'); - - nav._postRender(1, enteringView, leavingView, false, navOptions, done); - - expect(leavingView.fireWillLeave).not.toHaveBeenCalled(); - }); - - it('should set animate false when preloading', () => { - let enteringView = new ViewController(Page1); - let leavingView = new ViewController(Page2); - var navOptions: NavOptions = { - preload: true - }; - var done = () => {}; - nav._beforeTrans = () => {}; //prevent running beforeTrans for tests - - nav._postRender(1, enteringView, leavingView, false, navOptions, done); - - expect(navOptions.animate).toBe(false); - }); - - it('should set domShow true when isAlreadyTransitioning', () => { - let enteringView = new ViewController(Page1); - let leavingView = new ViewController(Page2); - let isAlreadyTransitioning = true; - var navOptions: NavOptions = {}; - var done = () => {}; - nav._beforeTrans = () => {}; //prevent running beforeTrans for tests - nav._renderer = null; - - spyOn(enteringView, 'domShow'); - spyOn(leavingView, 'domShow'); - - nav._postRender(1, enteringView, leavingView, isAlreadyTransitioning, navOptions, done); - - expect(enteringView.domShow).toHaveBeenCalledWith(true, nav._renderer); - expect(leavingView.domShow).toHaveBeenCalledWith(true, nav._renderer); - }); - - it('should set domShow true when isAlreadyTransitioning false for the entering/leaving views', () => { - let view1 = new ViewController(Page1); - let view2 = new ViewController(Page2); - let view3 = new ViewController(Page3); - let isAlreadyTransitioning = false; - var navOptions: NavOptions = {}; - var done = () => {}; - nav._beforeTrans = () => {}; //prevent running beforeTrans for tests - nav._renderer = null; - nav.views = [view1, view2, view3]; - - spyOn(view1, 'domShow'); - spyOn(view2, 'domShow'); - spyOn(view3, 'domShow'); - - nav._postRender(1, view3, view2, isAlreadyTransitioning, navOptions, done); - - expect(view1.domShow).toHaveBeenCalledWith(false, nav._renderer); - expect(view2.domShow).toHaveBeenCalledWith(true, nav._renderer); - expect(view3.domShow).toHaveBeenCalledWith(true, nav._renderer); - }); - - it('should set domShow true when isAlreadyTransitioning false for views when a view has isOverlay=true', () => { - let view1 = new ViewController(Page1); - let view2 = new ViewController(Page2); - let view3 = new ViewController(Page3); - let view4 = new ViewController(Page4); - let isAlreadyTransitioning = false; - var navOptions: NavOptions = {}; - var done = () => {}; - nav._beforeTrans = () => {}; //prevent running beforeTrans for tests - nav._renderer = null; - nav.views = [view1, view2, view3, view4]; - - view3.isOverlay = true; - - spyOn(view1, 'domShow'); - spyOn(view2, 'domShow'); - spyOn(view3, 'domShow'); - spyOn(view4, 'domShow'); - - nav._postRender(1, view4, view3, isAlreadyTransitioning, navOptions, done); - - expect(view1.domShow).toHaveBeenCalledWith(false, nav._renderer); - expect(view2.domShow).toHaveBeenCalledWith(true, nav._renderer); - expect(view3.domShow).toHaveBeenCalledWith(true, nav._renderer); - expect(view4.domShow).toHaveBeenCalledWith(true, nav._renderer); - }); - - }); - - describe('_setZIndex', () => { - - it('should set zIndex off of the previous view to the entering view is loaded and the leavingView is not loaded', () => { - let leavingView = new ViewController(); - leavingView.zIndex = 100; - leavingView._loaded = true; - let enteringView = new ViewController(); - enteringView.setPageRef({}); - - nav.views = [leavingView, enteringView]; - - nav._setZIndex(enteringView, leavingView, 'forward'); - expect(enteringView.zIndex).toEqual(101); - }); - - it('should set zIndex 100 when leaving view is not loaded', () => { - let leavingView = new ViewController(); - leavingView._loaded = false; - let enteringView = new ViewController(); - enteringView.setPageRef({}); - - nav.views = [leavingView, enteringView]; - - nav._setZIndex(enteringView, leavingView, 'forward'); - expect(enteringView.zIndex).toEqual(100); - }); - - it('should set zIndex 100 on first entering view', () => { - let enteringView = new ViewController(); - enteringView.setPageRef({}); - nav._setZIndex(enteringView, null, 'forward'); - expect(enteringView.zIndex).toEqual(100); - }); - - it('should set zIndex 1 on second entering view', () => { - let leavingView = new ViewController(); - leavingView.zIndex = 0; - leavingView._loaded = true; - let enteringView = new ViewController(); - enteringView.setPageRef({}); - nav._setZIndex(enteringView, leavingView, 'forward'); - expect(enteringView.zIndex).toEqual(1); - }); - - it('should set zIndex 0 on entering view going back', () => { - let leavingView = new ViewController(); - leavingView.zIndex = 1; - leavingView._loaded = true; - let enteringView = new ViewController(); - enteringView.setPageRef({}); - nav._setZIndex(enteringView, leavingView, 'back'); - expect(enteringView.zIndex).toEqual(0); - }); - - it('should set zIndex 9999 on first entering portal view', () => { - let enteringView = new ViewController(); - enteringView.setPageRef({}); - nav.isPortal = true; - nav._setZIndex(enteringView, null, 'forward'); - expect(enteringView.zIndex).toEqual(9999); - }); - - it('should set zIndex 10000 on second entering portal view', () => { - let leavingView = new ViewController(); - leavingView.zIndex = 9999; - leavingView._loaded = true; - let enteringView = new ViewController(); - enteringView.setPageRef({}); - nav._portal = null; - nav._setZIndex(enteringView, leavingView, 'forward'); - expect(enteringView.zIndex).toEqual(10000); - }); - - it('should set zIndex 9999 on entering portal view going back', () => { - let leavingView = new ViewController(); - leavingView.zIndex = 10000; - leavingView._loaded = true; - let enteringView = new ViewController(); - enteringView.setPageRef({}); - nav._portal = null; - nav._setZIndex(enteringView, leavingView, 'back'); - expect(enteringView.zIndex).toEqual(9999); - }); - - }); - - describe('_setAnimate', () => { - - it('should be unchanged when the nav is a portal', () => { - nav.views = [new ViewController()]; - nav._init = false; - nav.isPortal = true; - let opts: NavOptions = {}; - nav._setAnimate(opts); - expect(opts.animate).toBeUndefined(); - }); - - it('should not animate when theres only 1 view, and nav hasnt initialized yet', () => { - nav.views = [new ViewController()]; - nav._init = false; - let opts: NavOptions = {}; - nav._setAnimate(opts); - expect(opts.animate).toEqual(false); - }); - - it('should be unchanged when theres only 1 view, and nav has already initialized', () => { - nav.views = [new ViewController()]; - nav._init = true; - let opts: NavOptions = {}; - nav._setAnimate(opts); - expect(opts.animate).toBeUndefined(); - }); - - it('should not animate with config animate = false, and has initialized', () => { - config.set('animate', false); - nav._init = true; - let opts: NavOptions = {}; - nav._setAnimate(opts); - expect(opts.animate).toEqual(false); - }); - - it('should not animate with config animate = false, and has not initialized', () => { - config.set('animate', false); - nav._init = false; - let opts: NavOptions = {}; - nav._setAnimate(opts); - expect(opts.animate).toEqual(false); - }); - - }); - - describe('_afterTrans', () => { - - it('should call didEnter/didLeave', () => { - let enteringView = new ViewController(); - let leavingView = new ViewController(); - let navOpts: NavOptions = {}; - let hasCompleted = true; - let doneCalled = false; - let done = () => {doneCalled = true;} - - spyOn(enteringView, 'fireDidEnter'); - spyOn(leavingView, 'fireDidLeave'); - - nav._init = true; - nav._afterTrans(enteringView, leavingView, navOpts, hasCompleted, done); - - expect(enteringView.fireDidEnter).toHaveBeenCalled(); - expect(leavingView.fireDidLeave).toHaveBeenCalled(); - expect(doneCalled).toBe(true); - }); - - it('should not call didEnter/didLeave when preloaded', () => { - let enteringView = new ViewController(); - let leavingView = new ViewController(); - let navOpts: NavOptions = { - preload: true - }; - let hasCompleted = true; - let doneCalled = false; - let done = () => {doneCalled = true;} - - spyOn(enteringView, 'fireDidEnter'); - spyOn(leavingView, 'fireDidLeave'); - - nav._init = true; - nav._afterTrans(enteringView, leavingView, navOpts, hasCompleted, done); - - expect(enteringView.fireDidEnter).not.toHaveBeenCalled(); - expect(leavingView.fireDidLeave).not.toHaveBeenCalled(); - expect(doneCalled).toBe(true); - }); - - it('should not call didLeave when enteringView set fireOtherLifecycles to false', () => { - let enteringView = new ViewController(); - let leavingView = new ViewController(); - let navOpts: NavOptions = {}; - let hasCompleted = true; - let doneCalled = false; - let done = () => {doneCalled = true;} - - enteringView.fireOtherLifecycles = false; - - spyOn(enteringView, 'fireDidEnter'); - spyOn(leavingView, 'fireDidLeave'); - - nav._afterTrans(enteringView, leavingView, navOpts, hasCompleted, done); - - expect(enteringView.fireDidEnter).toHaveBeenCalled(); - expect(leavingView.fireDidLeave).not.toHaveBeenCalled(); - expect(doneCalled).toBe(true); - }); - - it('should not call didEnter when leavingView set fireOtherLifecycles to false', () => { - let enteringView = new ViewController(); - let leavingView = new ViewController(); - let navOpts: NavOptions = {}; - let hasCompleted = true; - let doneCalled = false; - let done = () => {doneCalled = true;} - - leavingView.fireOtherLifecycles = false; - - spyOn(enteringView, 'fireDidEnter'); - spyOn(leavingView, 'fireDidLeave'); - - nav._init = true; - nav._afterTrans(enteringView, leavingView, navOpts, hasCompleted, done); - - expect(enteringView.fireDidEnter).not.toHaveBeenCalled(); - expect(leavingView.fireDidLeave).toHaveBeenCalled(); - expect(doneCalled).toBe(true); - }); - - it('should not call didEnter/didLeave when not hasCompleted', () => { - let enteringView = new ViewController(); - let leavingView = new ViewController(); - let navOpts: NavOptions = {}; - let hasCompleted = false; - let doneCalled = false; - let done = () => {doneCalled = true;} - - spyOn(enteringView, 'fireDidEnter'); - spyOn(leavingView, 'fireDidLeave'); - - nav._afterTrans(enteringView, leavingView, navOpts, hasCompleted, done); - - expect(enteringView.fireDidEnter).not.toHaveBeenCalled(); - expect(leavingView.fireDidLeave).not.toHaveBeenCalled(); - expect(doneCalled).toBe(true); - }); - - }); - - describe('_transFinish', () => { - - it('should remove entering view if it was already set to cancel', () => { - let enteringView = new ViewController(Page1); - let leavingView = new ViewController(Page2); - enteringView.state = STATE_CANCEL_ENTER; - - spyOn(nav, 'remove'); - - nav._transFinish(1, enteringView, leavingView, 'forward', true); - - expect(nav.remove).toHaveBeenCalled(); - expect(enteringView.state).toBe(STATE_CANCEL_ENTER); - }); - - it('should not entering/leaving state, after transition that isnt the most recent, and state already changed', () => { - let enteringView = new ViewController(Page1); - enteringView.state = 'somethingelse'; - let leavingView = new ViewController(Page2); - leavingView.state = 'somethingelse'; - - nav._transIds = 2; - - nav._transFinish(1, enteringView, leavingView, 'forward', true); - - expect(enteringView.state).toBe('somethingelse'); - expect(leavingView.state).toBe('somethingelse'); - }); - - it('should set entering/leaving to inactive, after transition that isnt the most recent', () => { - let enteringView = new ViewController(Page1); - enteringView.state = STATE_TRANS_ENTER; - let leavingView = new ViewController(Page2); - leavingView.state = STATE_TRANS_LEAVE; - - nav._transIds = 2; - - nav._transFinish(1, enteringView, leavingView, 'forward', true); - - expect(enteringView.state).toBe(STATE_INACTIVE); - expect(leavingView.state).toBe(STATE_INACTIVE); - }); - - it('should set entering active, leaving inactive, after transition', () => { - let enteringView = new ViewController(Page1); - enteringView.state = STATE_TRANS_ENTER; - let leavingView = new ViewController(Page2); - leavingView.state = STATE_TRANS_LEAVE; - - nav._transIds = 1; - - nav._transFinish(1, enteringView, leavingView, 'forward', true); - - expect(enteringView.state).toBe(STATE_ACTIVE); - expect(leavingView.state).toBe(STATE_INACTIVE); - }); - - it('should set entering inactive, leaving active, after transition has not completed', () => { - let enteringView = new ViewController(Page1); - enteringView.state = STATE_TRANS_ENTER; - let leavingView = new ViewController(Page2); - leavingView.state = STATE_TRANS_LEAVE; - - nav._transIds = 1; - - nav._transFinish(1, enteringView, leavingView, 'back', false); - - expect(enteringView.state).toBe(STATE_INACTIVE); - expect(leavingView.state).toBe(STATE_ACTIVE); - }); - - it('should run cleanup when most recent transition and has completed', () => { - let enteringView = new ViewController(Page1); - enteringView.state = STATE_TRANS_ENTER; - let leavingView = new ViewController(Page2); - leavingView.state = STATE_TRANS_LEAVE; - let hasCompleted = true; - - spyOn(nav, '_cleanup'); - - nav._transIds = 1; - - nav._transFinish(1, enteringView, leavingView, 'back', hasCompleted); - - expect(nav._cleanup).toHaveBeenCalled(); - }); - - it('should not run cleanup when most not recent transition', () => { - let enteringView = new ViewController(Page1); - enteringView.state = STATE_TRANS_ENTER; - let leavingView = new ViewController(Page2); - leavingView.state = STATE_TRANS_LEAVE; - let hasCompleted = true; - - spyOn(nav, '_cleanup'); - - nav._transIds = 1; - - nav._transFinish(2, enteringView, leavingView, 'back', hasCompleted); - - expect(nav._cleanup).not.toHaveBeenCalled(); - }); - - it('should not run cleanup when it hasnt completed transition, but is the most recent', () => { - let enteringView = new ViewController(Page1); - enteringView.state = STATE_TRANS_ENTER; - let leavingView = new ViewController(Page2); - leavingView.state = STATE_TRANS_LEAVE; - let hasCompleted = false; - - spyOn(nav, '_cleanup'); - - nav._transIds = 1; - - nav._transFinish(1, enteringView, leavingView, 'back', hasCompleted); - - expect(nav._cleanup).not.toHaveBeenCalled(); - }); - - it('should set transitioning is over when most recent transition finishes', () => { - let enteringView = new ViewController(Page1); - enteringView.state = STATE_TRANS_ENTER; - let leavingView = new ViewController(Page2); - leavingView.state = STATE_TRANS_LEAVE; - let hasCompleted = true; - - spyOn(nav, 'setTransitioning'); - - nav._transIds = 1; - - nav._transFinish(1, enteringView, leavingView, 'back', hasCompleted); - - expect(nav.setTransitioning).toHaveBeenCalledWith(false); - }); - - it('should set transitioning is not over if its not the most recent transition', () => { - let enteringView = new ViewController(Page1); - enteringView.state = STATE_TRANS_ENTER; - let leavingView = new ViewController(Page2); - leavingView.state = STATE_TRANS_LEAVE; - let hasCompleted = true; - - spyOn(nav, 'setTransitioning'); - - nav._transIds = 2; - - nav._transFinish(1, enteringView, leavingView, 'back', hasCompleted); - - expect(nav.setTransitioning).not.toHaveBeenCalled(); - }); - - it('should set not run domShow when when any view in the stack has isOverlay=true', () => { - let view1 = new ViewController(Page1); - let view2 = new ViewController(Page2); - let view3 = new ViewController(Page3); - let view4 = new ViewController(Page4); - let hasCompleted = true; - nav.views = [view1, view2, view3, view4]; - - view1.isOverlay = true; - - nav._transIds = 1; - - spyOn(view1, 'domShow'); - spyOn(view2, 'domShow'); - spyOn(view3, 'domShow'); - spyOn(view4, 'domShow'); - - nav._transFinish(1, view4, view3, 'forward', hasCompleted); - - expect(view1.domShow).not.toHaveBeenCalled(); - expect(view2.domShow).not.toHaveBeenCalled(); - expect(view3.domShow).not.toHaveBeenCalled(); - expect(view4.domShow).toHaveBeenCalled(); - }); - - it('should re-enable the app when transition time <= 0', () => { - // arrange - let enteringView = new ViewController(Page1); - enteringView.state = 'somethingelse'; - let leavingView = new ViewController(Page2); - leavingView.state = 'somethingelse'; - nav._transIds = 1; - nav._app = { - setEnabled: () => {} - }; - - spyOn(nav._app, 'setEnabled'); - spyOn(nav, 'setTransitioning'); - - // act - nav._transFinish(nav._transIds, enteringView, leavingView, 'forward', true); - - // assert - expect(nav._app.setEnabled).toHaveBeenCalledWith(true); - expect(nav.setTransitioning).toHaveBeenCalledWith(false); - }); - - it('should not re-enable app when transition time > 0', () => { - // arrange - let enteringView = new ViewController(Page1); - enteringView.state = 'somethingelse'; - let leavingView = new ViewController(Page2); - leavingView.state = 'somethingelse'; - nav._transIds = 1; - nav._app = { - setEnabled: () => {} - }; - - spyOn(nav._app, 'setEnabled'); - spyOn(nav, 'setTransitioning'); - - nav._getLongestTrans = () => { return 50 }; - - // act - nav._transFinish(nav._transIds, enteringView, leavingView, 'forward', true); - - // assert - expect(nav._app.setEnabled).not.toHaveBeenCalled(); - expect(nav.setTransitioning).toHaveBeenCalledWith(false); - }); - - }); - - describe('_insert', () => { - - it('should push page when previous transition is still actively transitioning', () => { - let view1 = new ViewController(Page1); - view1.state = STATE_TRANS_ENTER; - let view2 = new ViewController(Page2); - view2.state = STATE_TRANS_LEAVE; - nav.views = [view1, view2]; - - let view3 = new ViewController(Page3); - nav._insert(-1, [view3]); - - expect(nav.getByIndex(0).state).toBe(STATE_TRANS_ENTER); - expect(nav.getByIndex(0).componentType).toBe(Page1); - expect(nav.getByIndex(1).state).toBe(STATE_TRANS_LEAVE); - expect(nav.getByIndex(1).componentType).toBe(Page2); - expect(nav.getByIndex(2).state).toBe(STATE_INIT_ENTER); - expect(nav.getByIndex(2).componentType).toBe(Page3); - }); - - it('should push page when previous transition views init, but havent transitioned yet', () => { - let view1 = new ViewController(Page1); - view1.state = STATE_INIT_LEAVE; - let view2 = new ViewController(Page2); - view2.state = STATE_INIT_ENTER; - nav.views = [view1, view2]; - - let view3 = new ViewController(Page3); - nav._insert(-1, [view3]); - - expect(nav.getByIndex(0).state).toBe(STATE_INIT_LEAVE); - expect(nav.getByIndex(0).componentType).toBe(Page1); - expect(nav.getByIndex(1).state).toBe(STATE_INACTIVE); - expect(nav.getByIndex(1).componentType).toBe(Page2); - expect(nav.getByIndex(2).state).toBe(STATE_INIT_ENTER); - expect(nav.getByIndex(2).componentType).toBe(Page3); - }); - - it('should insert multiple pages, back to back, with a starting active page', () => { - let view1 = new ViewController(Page1); - view1.state = STATE_ACTIVE; - nav.views = [view1]; - - let view2 = new ViewController(Page2); - nav._insert(-1, [view2]); - - expect(nav.getByIndex(0).state).toBe(STATE_INIT_LEAVE); - expect(nav.getByIndex(0).componentType).toBe(Page1); - expect(nav.getByIndex(1).state).toBe(STATE_INIT_ENTER); - expect(nav.getByIndex(1).componentType).toBe(Page2); - - let view3 = new ViewController(Page3); - nav._insert(-1, [view3]); - - expect(nav.getByIndex(0).state).toBe(STATE_INIT_LEAVE); - expect(nav.getByIndex(0).componentType).toBe(Page1); - expect(nav.getByIndex(1).state).toBe(STATE_INACTIVE); - expect(nav.getByIndex(1).componentType).toBe(Page2); - expect(nav.getByIndex(2).state).toBe(STATE_INIT_ENTER); - expect(nav.getByIndex(2).componentType).toBe(Page3); - }); - - it('should insert multiple pages, back to back, no starting active page', () => { - let view1 = new ViewController(Page1); - nav._insert(-1, [view1]); - expect(nav.getByIndex(0).state).toBe(STATE_INIT_ENTER); - expect(nav.getByIndex(0).componentType).toBe(Page1); - - let view2 = new ViewController(Page2); - nav._insert(-1, [view2]); - - expect(nav.getByIndex(0).state).toBe(STATE_INACTIVE); - expect(nav.getByIndex(0).componentType).toBe(Page1); - expect(nav.getByIndex(1).state).toBe(STATE_INIT_ENTER); - expect(nav.getByIndex(1).componentType).toBe(Page2); - - let view3 = new ViewController(Page3); - nav._insert(1, [view3]); - - expect(nav.getByIndex(0).state).toBe(STATE_INACTIVE); - expect(nav.getByIndex(0).componentType).toBe(Page1); - expect(nav.getByIndex(1).state).toBe(STATE_INIT_ENTER); - expect(nav.getByIndex(1).componentType).toBe(Page3); - expect(nav.getByIndex(2).state).toBe(STATE_INACTIVE); - expect(nav.getByIndex(2).componentType).toBe(Page2); - }); - - it('should push a page, and abort previous init', () => { - let view1 = new ViewController(Page1); - view1.state = STATE_INIT_LEAVE; - let view2 = new ViewController(Page2); - view2.state = STATE_INIT_ENTER; - nav.views = [view1, view2]; - - let view3 = new ViewController(Page3); - nav._insert(-1, [view3]); - expect(nav.length()).toBe(3); - - expect(nav.getByIndex(0).state).toBe(STATE_INIT_LEAVE); - expect(nav.getByIndex(0).componentType).toBe(Page1); - expect(nav.getByIndex(1).state).toBe(STATE_INACTIVE); - expect(nav.getByIndex(1).componentType).toBe(Page2); - expect(nav.getByIndex(2).state).toBe(STATE_INIT_ENTER); - expect(nav.getByIndex(2).componentType).toBe(Page3); - }); - - it('should insert a page between the first and second', () => { - let view1 = new ViewController(Page1); - view1.state = STATE_INACTIVE; - let view2 = new ViewController(Page2); - view2.state = STATE_ACTIVE; - nav.views = [view1, view2]; - - let view3 = new ViewController(Page3); - nav._insert(1, [view3]); - expect(nav.length()).toBe(3); - - expect(nav.getByIndex(0).state).toBe(STATE_INACTIVE); - expect(nav.getByIndex(0).componentType).toBe(Page1); - expect(nav.getByIndex(1).state).toBe(STATE_INIT_ENTER); - expect(nav.getByIndex(1).componentType).toBe(Page3); - expect(nav.getByIndex(2).state).toBe(STATE_INIT_LEAVE); - expect(nav.getByIndex(2).componentType).toBe(Page2); - }); - - it('should insert a page before the first', () => { - let view1 = new ViewController(Page1); - view1.state = STATE_ACTIVE; - nav.views = [view1]; - - let view2 = new ViewController(Page2); - nav._insert(0, [view2]); - expect(nav.getByIndex(0).state).toBe(STATE_INIT_ENTER); - expect(nav.getByIndex(0).componentType).toBe(Page2); - expect(nav.getByIndex(1).state).toBe(STATE_INIT_LEAVE); - expect(nav.getByIndex(1).componentType).toBe(Page1); - }); - - it('should insert 3 pages', () => { - let view1 = new ViewController(Page1); - view1.state = STATE_ACTIVE; - nav.views = [view1]; - - let insertViews = [ - new ViewController(Page2), - new ViewController(Page3), - new ViewController(Page4) - ]; - nav._insert(-1, insertViews); - expect(nav.length()).toBe(4); - expect(nav.getByIndex(0).state).toBe(STATE_INIT_LEAVE); - expect(nav.getByIndex(0).componentType).toBe(Page1); - expect(nav.getByIndex(1).state).toBe(STATE_INACTIVE); - expect(nav.getByIndex(1).componentType).toBe(Page2); - expect(nav.getByIndex(2).state).toBe(STATE_INACTIVE); - expect(nav.getByIndex(2).componentType).toBe(Page3); - expect(nav.getByIndex(3).state).toBe(STATE_INIT_ENTER); - expect(nav.getByIndex(3).componentType).toBe(Page4); - }); - - it('should push the second page', () => { - let view1 = new ViewController(Page1); - view1.state = STATE_ACTIVE; - nav.views = [view1]; - - let view2 = new ViewController(Page2) - nav._insert(-1, [view2]); - expect(nav.length()).toBe(2); - expect(nav.getByIndex(0).state).toBe(STATE_INIT_LEAVE); - expect(nav.getByIndex(0).componentType).toBe(Page1); - expect(nav.getByIndex(1).state).toBe(STATE_INIT_ENTER); - expect(nav.getByIndex(1).componentType).toBe(Page2); - }); - - it('should push the first page, using a number greater than the length', () => { - let view1 = new ViewController(Page1) - nav._insert(8675309, [view1]); - - expect(nav.length()).toBe(1); - expect(nav.getByIndex(0).state).toBe(STATE_INIT_ENTER); - }); - - it('should push the first page, using -1', () => { - let view1 = new ViewController(Page1) - nav._insert(-1, [view1]); - - expect(nav.getByIndex(0).id).toBeDefined(); - expect(nav.length()).toBe(1); - expect(nav.getByIndex(0).state).toBe(STATE_INIT_ENTER); - }); - - }); - - describe('getActive', () => { - it('should getActive()', () => { - expect(nav.getActive()).toBe(null); - let view1 = new ViewController(Page1); - view1.state = STATE_INIT_ENTER; - nav.views = [view1]; - expect(nav.getActive()).toBe(null); - view1.state = STATE_ACTIVE; - expect(nav.getActive()).toBe(view1); - }); - }); - - describe('getByState', () => { - it('should getByState()', () => { - expect(nav.getByState(null)).toBe(null); - - let view1 = new ViewController(Page1); - view1.state = STATE_INIT_ENTER; - let view2 = new ViewController(Page2); - view2.state = STATE_INIT_ENTER; - nav.views = [view1, view2]; - - expect(nav.getByState('whatever')).toBe(null); - expect(nav.getByState(STATE_INIT_ENTER)).toBe(view2); - - view2.state = STATE_INACTIVE; - expect(nav.getByState(STATE_INIT_ENTER)).toBe(view1); - - view2.state = STATE_ACTIVE; - expect(nav.getActive()).toBe(view2); - }); - }); - - describe('getPrevious', () => { - it('should getPrevious()', () => { - expect(nav.getPrevious(null)).toBe(null); - - let view1 = new ViewController(Page1); - let view2 = new ViewController(Page2); - nav.views = [view1, view2]; - - expect(nav.getPrevious(view1)).toBe(null); - expect(nav.getPrevious(view2)).toBe(view1); - }); - }); - - describe('first', () => { - it('should get first()', () => { - expect(nav.first()).toBe(null); - let view1 = new ViewController(Page1); - view1.setNav(nav); - let view2 = new ViewController(Page2); - view2.setNav(nav); - nav.views = [view1]; - - expect(nav.first()).toBe(view1); - expect(view1.isFirst()).toBe(true); - - nav.views = [view1, view2]; - expect(nav.first()).toBe(view1); - expect(view1.isFirst()).toBe(true); - expect(view2.isFirst()).toBe(false); - }); - }); - - describe('last', () => { - it('should get last()', () => { - expect(nav.last()).toBe(null); - let view1 = new ViewController(Page1); - view1.setNav(nav); - let view2 = new ViewController(Page2); - view2.setNav(nav); - nav.views = [view1]; - - expect(nav.last()).toBe(view1); - expect(view1.isLast()).toBe(true); - - nav.views = [view1, view2]; - expect(nav.last()).toBe(view2); - expect(view1.isLast()).toBe(false); - expect(view2.isLast()).toBe(true); - }); - }); - - describe('indexOf', () => { - it('should get indexOf()', () => { - let view1 = new ViewController(Page1); - let view2 = new ViewController(Page2); - - expect(nav.length()).toBe(0); - expect(nav.indexOf(view1)).toBe(-1); - - nav.views = [view1, view2]; - expect(nav.indexOf(view1)).toBe(0); - expect(nav.indexOf(view2)).toBe(1); - expect(nav.length()).toBe(2); - }); - }); - - describe('getByIndex', () => { - it('should get getByIndex()', () => { - expect(nav.getByIndex(-99)).toBe(null); - expect(nav.getByIndex(99)).toBe(null); - - let view1 = new ViewController(Page1); - let view2 = new ViewController(Page2); - nav.views = [view1, view2]; - - expect(nav.getByIndex(-1)).toBe(null); - expect(nav.getByIndex(0)).toBe(view1); - expect(nav.getByIndex(1)).toBe(view2); - expect(nav.getByIndex(2)).toBe(null); - }); - }); - - /* private method */ - describe('_beforeTrans', () => { - - it('shouldnt disable app on short transition', () => { - // arrange - let executeAssertions = () => { - // assertions triggerd by callbacks - expect(app.setEnabled).toHaveBeenCalledWith(true, 50); - expect(nav.setTransitioning).toHaveBeenCalledWith(false, 50); - }; - let mockTransition = { - play: () => { - executeAssertions(); - }, - getDuration: () => { return 50}, - onFinish: () => {} - }; - nav._createTrans = () => { - return mockTransition; - }; - nav.config = { - platform : { - isRTL: () => {} - } - }; - let app = { - setEnabled: () => {} - }; - nav._app = app; - - spyOn(app, 'setEnabled'); - spyOn(nav, 'setTransitioning'); - - let view1 = new ViewController(Page1); - let view2 = new ViewController(Page2); - - // act - nav._beforeTrans(view1, view2, {}, () => {}); - }); - - it('should disable app on longer transition', () => { - // arrange - let executeAssertions = () => { - // assertions triggerd by callbacks - expect(app.setEnabled).toHaveBeenCalledWith(false, 200); - expect(nav.setTransitioning).toHaveBeenCalledWith(true, 200); - }; - let mockTransition = { - play: () => { - executeAssertions(); - }, - getDuration: () => { return 200}, - onFinish: () => {} - }; - nav._createTrans = () => { - return mockTransition; - }; - nav.config = { - platform : { - isRTL: () => {} - } - }; - let app = { - setEnabled: () => {} - }; - nav._app = app; - - spyOn(app, 'setEnabled'); - spyOn(nav, 'setTransitioning'); - - let view1 = new ViewController(Page1); - let view2 = new ViewController(Page2); - - // act - nav._beforeTrans(view1, view2, {}, () => {}); - }); - - it('should disable app w/ padding when keyboard is open', () => { - // arrange - let executeAssertions = () => { - // assertions triggerd by callbacks - expect(app.setEnabled.calls.mostRecent().args[0]).toEqual(false); - expect(app.setEnabled.calls.mostRecent().args[1]).toBeGreaterThan(200); - - expect(nav.setTransitioning.calls.mostRecent().args[0]).toEqual(true); - expect(nav.setTransitioning.calls.mostRecent().args[1]).toBeGreaterThan(200); - }; - let mockTransition = { - play: () => { - executeAssertions(); - }, - getDuration: () => { return 200}, - onFinish: () => {} - }; - nav._createTrans = () => { - return mockTransition; - }; - nav.config = { - platform : { - isRTL: () => {} - } - }; - let app = { - setEnabled: () => {} - }; - nav._app = app; - nav._keyboard = { - isOpen: () => true - }; - - spyOn(app, 'setEnabled'); - spyOn(nav, 'setTransitioning'); - - let view1 = new ViewController(Page1); - let view2 = new ViewController(Page2); - - // act - nav._beforeTrans(view1, view2, {}, () => {}); - }); - - it('shouldnt update app enabled when parent transition is occurring', () => { - // arrange - let executeAssertions = () => { - // assertions triggerd by callbacks - expect(app.setEnabled).not.toHaveBeenCalled(); - expect(nav.setTransitioning.calls.mostRecent().args[0]).toEqual(true); - }; - let mockTransition = { - play: () => { - executeAssertions(); - }, - getDuration: () => { return 200}, - onFinish: () => {} - }; - nav._createTrans = () => { - return mockTransition; - }; - nav.config = { - platform : { - isRTL: () => {} - } - }; - let app = { - setEnabled: () => {} - }; - nav._app = app; - - spyOn(app, 'setEnabled'); - spyOn(nav, 'setTransitioning'); - - nav._getLongestTrans = () => { return Date.now() + 100 }; - - let view1 = new ViewController(Page1); - let view2 = new ViewController(Page2); - - // act - nav._beforeTrans(view1, view2, {}, () => {}); - }); - - it('should not begin transition when entering stated is inactive', () => { - let view1 = new ViewController(Page1); - view1.state = STATE_INACTIVE; - - let wasDoneCalled = false; - let done = () => { - wasDoneCalled = true; - }; - - nav._beforeTrans(view1, null, {}, done); - - expect(wasDoneCalled).toEqual(true); - expect(view1.state).toEqual(STATE_INACTIVE); - }); - - it('should not begin transition when entering state is canceled', () => { - let view1 = new ViewController(Page1); - view1.state = STATE_CANCEL_ENTER; - - let wasDoneCalled = false; - let done = () => { - wasDoneCalled = true; - }; - - nav._beforeTrans(view1, null, {}, done); - - expect(wasDoneCalled).toEqual(true); - expect(view1.state).toEqual(STATE_CANCEL_ENTER); - }); - }); - - /* private method */ - describe('_getLongestTrans', () => { - it('should return 0 when transition end time is less than 0', () => { - // arrange - nav.parent = null; - // act - let returnedValue = nav._getLongestTrans(Date.now()); - // asssert - expect(returnedValue).toEqual(0); - }); - - it('should return 0 when transition end time is less than now', () => { - // arrange - nav.parent = { - trnsTime: Date.now() - 5 - }; - // act - let returnedValue = nav._getLongestTrans(Date.now()); - // asssert - expect(returnedValue).toEqual(0); - }); - - it('should return 0 when parent transition time not set', () => { - // arrange - nav.parent = { - trnsTime: undefined - }; - // act - let returnedValue = nav._getLongestTrans(Date.now()); - // asssert - expect(returnedValue).toEqual(0); - }); - - it('should return transitionEndTime when transition end time is greater than now', () => { - // arrange - let expectedReturnValue = Date.now() + 100; - nav.parent = { - trnsTime: expectedReturnValue - }; - // act - let returnedValue = nav._getLongestTrans(Date.now()); - // asssert - expect(returnedValue).toEqual(expectedReturnValue); - }); - - it('should return the greatest end of transition time if found on first parent', () => { - // arrange - let expectedReturnValue = Date.now() + 100; - let firstParent = { - trnsTime: expectedReturnValue - }; - let secondParent = { - trnsTime: Date.now() + 50 - }; - let thirdParent = { - trnsTime: Date.now() - }; - let fourthParent = { - trnsTime: Date.now() + 20 - }; - firstParent.parent = secondParent; - secondParent.parent = thirdParent; - thirdParent.parent = fourthParent; - nav.parent = firstParent; - // act - let returnedValue = nav._getLongestTrans(Date.now()); - // asssert - expect(returnedValue).toEqual(expectedReturnValue); - }); - - it('should return the greatest end of transition time if found on middle parent', () => { - // arrange - let expectedReturnValue = Date.now() + 100; - let firstParent = { - trnsTime: Date.now() - }; - let secondParent = { - trnsTime: Date.now() + 50 - }; - let thirdParent = { - trnsTime: expectedReturnValue - }; - let fourthParent = { - trnsTime: Date.now() + 20 - }; - firstParent.parent = secondParent; - secondParent.parent = thirdParent; - thirdParent.parent = fourthParent; - nav.parent = firstParent; - // act - let returnedValue = nav._getLongestTrans(Date.now()); - // asssert - expect(returnedValue).toEqual(expectedReturnValue); - }); - - it('should return the greatest end of transition time if found on last parent', () => { - // arrange - let expectedReturnValue = Date.now() + 100; - let firstParent = { - trnsTime: Date.now() - }; - let secondParent = { - trnsTime: Date.now() + 50 - }; - let thirdParent = { - trnsTime: Date.now() + 20 - }; - let fourthParent = { - trnsTime: expectedReturnValue - }; - firstParent.parent = secondParent; - secondParent.parent = thirdParent; - thirdParent.parent = fourthParent; - nav.parent = firstParent; - // act - let returnedValue = nav._getLongestTrans(Date.now()); - // asssert - expect(returnedValue).toEqual(expectedReturnValue); - }); - }); - - // setup stuff - let nav: MockNavController; - let config = new Config(); - let platform = new Platform(); - - class Page1 {} - class Page2 {} - class Page3 {} - class Page4 {} - class Page5 {} - - beforeEach(() => { - nav = mockNav(); - }); - - function mockNav(): MockNavController { - let elementRef = getElementRef(); - - let app = new App(config, platform); - let nav = new MockNavController(null, app, config, null, elementRef, null, null, null); - - nav._keyboard = { - isOpen: function() { - return false; - } - }; - nav._zone = { - run: function(cb) { - cb(); - }, - runOutsideAngular: function(cb) { - cb(); - } - }; - nav._renderer = { - setElementAttribute: function(){}, - setElementClass: function(){}, - setElementStyle: function(){} - }; - - return nav; - } - - function getElementRef() { - return { - nativeElement: document.createElement('div') - } - } - }); + + describe('popToRoot', () => { + + it('should go back to root', () => { + let view1 = new ViewController(Page1); + view1.state = STATE_INACTIVE; + let view2 = new ViewController(Page2); + view2.state = STATE_INACTIVE; + let view3 = new ViewController(Page3); + view3.state = STATE_INACTIVE; + let view4 = new ViewController(Page4); + view4.state = STATE_ACTIVE; + nav._views = [view1, view2, view3, view4]; + + nav.popToRoot(); + expect(nav.length()).toBe(2); + expect(nav.getByIndex(0).state).toBe(STATE_INIT_ENTER); + expect(nav.getByIndex(0).componentType).toBe(Page1); + expect(nav.getByIndex(1).state).toBe(STATE_INIT_LEAVE); + expect(nav.getByIndex(1).componentType).toBe(Page4); + + expect(view2.state).toBe(STATE_REMOVE); + expect(view3.state).toBe(STATE_REMOVE); + }); + + }); + + describe('popTo', () => { + + it('should go back two views', () => { + let view1 = new ViewController(Page1); + view1.state = STATE_INACTIVE; + let view2 = new ViewController(Page2); + view2.state = STATE_INACTIVE; + let view3 = new ViewController(Page3); + view3.state = STATE_INACTIVE; + let view4 = new ViewController(Page4); + view4.state = STATE_ACTIVE; + nav._views = [view1, view2, view3, view4]; + + nav.popTo(view2); + + expect(nav.length()).toBe(3); + expect(nav.getByIndex(0).state).toBe(STATE_INACTIVE); + expect(nav.getByIndex(0).componentType).toBe(Page1); + expect(nav.getByIndex(1).state).toBe(STATE_INIT_ENTER); + expect(nav.getByIndex(1).componentType).toBe(Page2); + expect(view3.state).toBe(STATE_REMOVE); + expect(nav.getByIndex(2).state).toBe(STATE_INIT_LEAVE); + expect(nav.getByIndex(2).componentType).toBe(Page4); + }); + + }); + + describe('remove', () => { + + it('should create opts if passed in arg is undefined or null', () => { + let view1 = new ViewController(Page1); + view1.state = STATE_INACTIVE; + let view2 = new ViewController(Page2); + view2.state = STATE_ACTIVE; + nav._views = [view1, view2]; + + nav.remove(1, 1, null); + }); + + }); + + describe('_remove', () => { + + it('should reassign activily transitioning leave that isnt getting removed, to become force active', () => { + let view1 = new ViewController(Page1); + view1.state = STATE_INACTIVE; + let view2 = new ViewController(Page2); + view2.state = STATE_TRANS_LEAVE; + let view3 = new ViewController(Page3); + view3.state = STATE_TRANS_ENTER; + nav._views = [view1, view2, view3]; + + nav._remove(2, 1); + + expect(nav.getByIndex(0).state).toBe(STATE_INACTIVE); + expect(nav.getByIndex(0).componentType).toBe(Page1); + expect(nav.getByIndex(1).state).toBe(STATE_FORCE_ACTIVE); + expect(nav.getByIndex(1).componentType).toBe(Page2); + expect(nav.getByIndex(2).state).toBe(STATE_REMOVE_AFTER_TRANS); + expect(nav.getByIndex(2).componentType).toBe(Page3); + }); + + it('should reassign activily transitioning views that should be removed to STATE_REMOVE_AFTER_TRANS', () => { + let view1 = new ViewController(Page1); + view1.state = STATE_INACTIVE; + let view2 = new ViewController(Page2); + view2.state = STATE_TRANS_ENTER; + let view3 = new ViewController(Page3); + view3.state = STATE_TRANS_LEAVE; + nav._views = [view1, view2, view3]; + + nav._remove(1, 2); + expect(nav.getByIndex(0).state).toBe(STATE_INACTIVE); + expect(nav.getByIndex(0).componentType).toBe(Page1); + expect(nav.getByIndex(1).state).toBe(STATE_REMOVE_AFTER_TRANS); + expect(nav.getByIndex(1).componentType).toBe(Page2); + expect(nav.getByIndex(2).state).toBe(STATE_REMOVE_AFTER_TRANS); + expect(nav.getByIndex(2).componentType).toBe(Page3); + }); + + it('should keep same init leave, but set previous init enter to inactive', () => { + let view1 = new ViewController(Page1); + view1.state = STATE_INACTIVE; + let view2 = new ViewController(Page2); + view2.state = STATE_INIT_ENTER; + let view3 = new ViewController(Page3); + view3.state = STATE_INIT_LEAVE; + nav._views = [view1, view2, view3]; + + nav._remove(1, 1); + expect(nav.length()).toBe(3); + expect(view1.state).toBe(STATE_INIT_ENTER); + expect(view2.state).toBe(STATE_CANCEL_ENTER); + expect(view3.state).toBe(STATE_INIT_LEAVE); + + expect(nav.getByIndex(0).state).toBe(STATE_INIT_ENTER); + expect(nav.getByIndex(0).componentType).toBe(Page1); + expect(nav.getByIndex(1).state).toBe(STATE_CANCEL_ENTER); + expect(nav.getByIndex(1).componentType).toBe(Page2); + expect(nav.getByIndex(2).state).toBe(STATE_INIT_LEAVE); + expect(nav.getByIndex(2).componentType).toBe(Page3); + }); + + it('should set to pop the active and enter the previous', () => { + let view1 = new ViewController(Page1); + view1.state = STATE_INACTIVE; + let view2 = new ViewController(Page2); + view2.state = STATE_ACTIVE; + nav._views = [view1, view2]; + + nav._remove(1, 1); + expect(view1.state).toBe(STATE_INIT_ENTER); + expect(view2.state).toBe(STATE_INIT_LEAVE); + }); + + it('should set to remove 2 views before active one, active stays the same', () => { + let view1 = new ViewController(Page1); + view1.state = STATE_INACTIVE; + let view2 = new ViewController(Page2); + view2.state = STATE_INACTIVE; + let view3 = new ViewController(Page3); + view3.state = STATE_INACTIVE; + let view4 = new ViewController(Page4); + view4.state = STATE_INACTIVE; + let view5 = new ViewController(Page5); + view5.state = STATE_ACTIVE; + nav._views = [view1, view2, view3, view4, view5]; + + nav._remove(2, 2); + expect(nav.length()).toBe(3); + expect(view1.state).toBe(STATE_INACTIVE); + expect(view2.state).toBe(STATE_INACTIVE); + expect(view3.state).toBe(STATE_REMOVE); + expect(view4.state).toBe(STATE_REMOVE); + expect(view5.state).toBe(STATE_ACTIVE); + + expect(nav.getByIndex(0).state).toBe(STATE_INACTIVE); + expect(nav.getByIndex(0).componentType).toBe(Page1); + expect(nav.getByIndex(1).state).toBe(STATE_INACTIVE); + expect(nav.getByIndex(1).componentType).toBe(Page2); + expect(nav.getByIndex(2).state).toBe(STATE_ACTIVE); + expect(nav.getByIndex(2).componentType).toBe(Page5); + }); + + it('should set to remove all views other than the first', () => { + let view1 = new ViewController(Page1); + view1.state = STATE_INACTIVE; + let view2 = new ViewController(Page2); + view2.state = STATE_INACTIVE; + let view3 = new ViewController(Page3); + view3.state = STATE_INACTIVE; + let view4 = new ViewController(Page4); + view4.state = STATE_ACTIVE; + nav._views = [view1, view2, view3, view4]; + + nav._remove(1, 9999); + expect(nav.length()).toBe(2); + expect(view1.state).toBe(STATE_INIT_ENTER); + expect(view2.state).toBe(STATE_REMOVE); + expect(view3.state).toBe(STATE_REMOVE); + expect(view4.state).toBe(STATE_INIT_LEAVE); + + expect(nav.getByIndex(0).state).toBe(STATE_INIT_ENTER); + expect(nav.getByIndex(0).componentType).toBe(Page1); + expect(nav.getByIndex(1).state).toBe(STATE_INIT_LEAVE); + expect(nav.getByIndex(1).componentType).toBe(Page4); + }); + + it('should set to remove 3 views and enter the first inactive one, remove includes active one', () => { + let view1 = new ViewController(Page1); + view1.state = STATE_INACTIVE; + let view2 = new ViewController(Page2); + view2.state = STATE_INACTIVE; + let view3 = new ViewController(Page3); + view3.state = STATE_INACTIVE; + let view4 = new ViewController(Page4); + view4.state = STATE_ACTIVE; + nav._views = [view1, view2, view3, view4]; + + nav._remove(1, 3); + expect(nav.length()).toBe(2); + expect(view1.state).toBe(STATE_INIT_ENTER); + expect(view2.state).toBe(STATE_REMOVE); + expect(view3.state).toBe(STATE_REMOVE); + expect(view4.state).toBe(STATE_INIT_LEAVE); + + expect(nav.getByIndex(0).state).toBe(STATE_INIT_ENTER); + expect(nav.getByIndex(0).componentType).toBe(Page1); + expect(nav.getByIndex(1).state).toBe(STATE_INIT_LEAVE); + expect(nav.getByIndex(1).componentType).toBe(Page4); + }); + + it('should set to remove the active and enter the previous', () => { + let view1 = new ViewController(Page1); + view1.state = STATE_INACTIVE; + let view2 = new ViewController(Page2); + view2.state = STATE_ACTIVE; + nav._views = [view1, view2]; + + nav._remove(1, 1); + expect(view1.state).toBe(STATE_INIT_ENTER); + expect(view2.state).toBe(STATE_INIT_LEAVE); + }); + + it('should set to remove the only view in the stack', () => { + let view1 = new ViewController(Page1); + view1.state = STATE_ACTIVE; + nav._views = [view1]; + + nav._remove(0, 1); + expect(nav.getByIndex(0).state).toBe(STATE_INIT_LEAVE); + }); + + it('should call willLeave/didLeave/destroy on views with STATE_REMOVE', () => { + let view1 = new ViewController(Page1); + view1.state = STATE_INACTIVE; + let view2 = new ViewController(Page2); + view2.state = STATE_INACTIVE; + let view3 = new ViewController(Page3); + view3.state = STATE_INACTIVE; + let view4 = new ViewController(Page4); + view4.state = STATE_ACTIVE; + nav._views = [view1, view2, view3, view4]; + + spyOn(view1, 'fireWillLeave'); + spyOn(view1, 'fireDidLeave'); + spyOn(view1, 'destroy'); + + spyOn(view2, 'fireWillLeave'); + spyOn(view2, 'fireDidLeave'); + spyOn(view2, 'destroy'); + + spyOn(view3, 'fireWillLeave'); + spyOn(view3, 'fireDidLeave'); + spyOn(view3, 'destroy'); + + spyOn(view4, 'fireWillLeave'); + spyOn(view4, 'fireDidLeave'); + spyOn(view4, 'destroy'); + + nav._remove(1, 3); + expect(nav.length()).toBe(2); + expect(view1.state).toBe(STATE_INIT_ENTER); + expect(view2.state).toBe(STATE_REMOVE); + expect(view3.state).toBe(STATE_REMOVE); + expect(view4.state).toBe(STATE_INIT_LEAVE); + + expect(view1.fireWillLeave).not.toHaveBeenCalled(); + expect(view1.fireDidLeave).not.toHaveBeenCalled(); + expect(view1.destroy).not.toHaveBeenCalled(); + + expect(view2.fireWillLeave).toHaveBeenCalled(); + expect(view2.fireDidLeave).toHaveBeenCalled(); + expect(view2.destroy).toHaveBeenCalled(); + + expect(view3.fireWillLeave).toHaveBeenCalled(); + expect(view3.fireDidLeave).toHaveBeenCalled(); + expect(view3.destroy).toHaveBeenCalled(); + + expect(view4.fireWillLeave).not.toHaveBeenCalled(); + expect(view4.fireDidLeave).not.toHaveBeenCalled(); + expect(view4.destroy).not.toHaveBeenCalled(); + }); + }); + + describe('_cleanup', () => { + it('should destroy views that are inactive after the active view', () => { + let view1 = new ViewController(Page1); + view1.state = STATE_INACTIVE; + let view2 = new ViewController(Page2); + view2.state = STATE_ACTIVE; + let view3 = new ViewController(Page3); + view3.state = STATE_INACTIVE; + let view4 = new ViewController(Page4); + view4.state = STATE_TRANS_ENTER; + let view5 = new ViewController(Page5); + view5.state = STATE_INACTIVE; + nav._views = [view1, view2, view3, view4, view5]; + nav._cleanup(); + + expect(nav.length()).toBe(3); + expect(nav.getByIndex(0).state).toBe(STATE_INACTIVE); + expect(nav.getByIndex(0).componentType).toBe(Page1); + expect(nav.getByIndex(1).state).toBe(STATE_ACTIVE); + expect(nav.getByIndex(1).componentType).toBe(Page2); + expect(nav.getByIndex(2).state).toBe(STATE_TRANS_ENTER); + expect(nav.getByIndex(2).componentType).toBe(Page4); + }); + + it('should not destroy any views since the last is active', () => { + let view1 = new ViewController(Page1); + view1.state = STATE_INACTIVE; + let view2 = new ViewController(Page2); + view2.state = STATE_ACTIVE; + nav._views = [view1, view2]; + nav._cleanup(); + expect(nav.length()).toBe(2); + }); + + it('should call destroy for each view to be destroyed', () => { + let view1 = new ViewController(Page1); + view1.state = STATE_ACTIVE; + let view2 = new ViewController(Page2); + view2.state = STATE_INACTIVE; + let view3 = new ViewController(Page3); + view3.state = STATE_INACTIVE; + nav._views = [view1, view2, view3]; + + spyOn(view1, 'destroy'); + spyOn(view2, 'destroy'); + spyOn(view3, 'destroy'); + + nav._cleanup(); + + expect(nav.length()).toBe(1); + expect(view1.destroy).not.toHaveBeenCalled(); + expect(view2.destroy).toHaveBeenCalled(); + expect(view3.destroy).toHaveBeenCalled(); + }); + + it('should reset zIndexes if their is a negative zindex', () => { + let view1 = new ViewController(Page1); + view1.setPageRef( mockElementRef() ); + view1.state = STATE_INACTIVE; + view1.zIndex = -1; + + let view2 = new ViewController(Page2); + view2.setPageRef( mockElementRef() ); + view2.state = STATE_INACTIVE; + view2.zIndex = 0; + + let view3 = new ViewController(Page3); + view3.setPageRef( mockElementRef() ); + view3.state = STATE_ACTIVE; + view3.zIndex = 1; + + nav._views = [view1, view2, view3]; + nav._cleanup(); + + expect(view1.zIndex).toEqual(100); + expect(view2.zIndex).toEqual(101); + expect(view3.zIndex).toEqual(102); + }); + }); + + describe('_postRender', () => { + it('should immediately call done when enteringView state is inactive', () => { + let view1 = new ViewController(Page1); + view1.state = STATE_INACTIVE; + var wasCalled = false; + var done = () => { + wasCalled = true; + }; + nav._beforeTrans = () => {}; //prevent running beforeTrans for tests + + nav._postRender(1, view1, null, false, null, done); + + expect(wasCalled).toBe(true); + }); + + it('should call willEnter on entering view', () => { + let enteringView = new ViewController(Page1); + let leavingView = new ViewController(Page2); + var navOptions: NavOptions = {}; + var done = () => {}; + nav._beforeTrans = () => {}; //prevent running beforeTrans for tests + + spyOn(enteringView, 'fireWillEnter'); + + nav._postRender(1, enteringView, leavingView, false, navOptions, done); + + expect(enteringView.fireWillEnter).toHaveBeenCalled(); + }); + + it('should not call willEnter on entering view when it is being preloaded', () => { + let enteringView = new ViewController(Page1); + let leavingView = new ViewController(Page2); + var navOptions: NavOptions = { + preload: true + }; + var done = () => {}; + nav._beforeTrans = () => {}; //prevent running beforeTrans for tests + + spyOn(enteringView, 'fireWillEnter'); + + nav._postRender(1, enteringView, leavingView, false, navOptions, done); + + expect(enteringView.fireWillEnter).not.toHaveBeenCalled(); + }); + + it('should call willLeave on leaving view', () => { + let enteringView = new ViewController(Page1); + let leavingView = new ViewController(Page2); + var navOptions: NavOptions = {}; + var done = () => {}; + nav._beforeTrans = () => {}; //prevent running beforeTrans for tests + + spyOn(leavingView, 'fireWillLeave'); + + nav._postRender(1, enteringView, leavingView, false, navOptions, done); + + expect(leavingView.fireWillLeave).toHaveBeenCalled(); + }); + + it('should not call willEnter when the leaving view has fireOtherLifecycles not true', () => { + let enteringView = new ViewController(Page1); + let leavingView = new ViewController(Page2); + var navOptions: NavOptions = {}; + var done = () => {}; + nav._beforeTrans = () => {}; //prevent running beforeTrans for tests + + spyOn(enteringView, 'fireWillEnter'); + spyOn(leavingView, 'fireWillLeave'); + + leavingView.fireOtherLifecycles = false; + + nav._postRender(1, enteringView, leavingView, false, navOptions, done); + + expect(enteringView.fireWillEnter).not.toHaveBeenCalled(); + expect(leavingView.fireWillLeave).toHaveBeenCalled(); + }); + + it('should not call willLeave when the entering view has fireOtherLifecycles not true', () => { + let enteringView = new ViewController(Page1); + let leavingView = new ViewController(Page2); + var navOptions: NavOptions = {}; + var done = () => {}; + nav._beforeTrans = () => {}; //prevent running beforeTrans for tests + + spyOn(enteringView, 'fireWillEnter'); + spyOn(leavingView, 'fireWillLeave'); + + enteringView.fireOtherLifecycles = false; + + nav._postRender(1, enteringView, leavingView, false, navOptions, done); + + expect(enteringView.fireWillEnter).toHaveBeenCalled(); + expect(leavingView.fireWillLeave).not.toHaveBeenCalled(); + }); + + it('should not call willLeave on leaving view when it is being preloaded', () => { + let enteringView = new ViewController(Page1); + let leavingView = new ViewController(Page2); + var navOptions: NavOptions = { + preload: true + }; + var done = () => {}; + nav._beforeTrans = () => {}; //prevent running beforeTrans for tests + + spyOn(leavingView, 'fireWillLeave'); + + nav._postRender(1, enteringView, leavingView, false, navOptions, done); + + expect(leavingView.fireWillLeave).not.toHaveBeenCalled(); + }); + + it('should set animate false when preloading', () => { + let enteringView = new ViewController(Page1); + let leavingView = new ViewController(Page2); + var navOptions: NavOptions = { + preload: true + }; + var done = () => {}; + nav._beforeTrans = () => {}; //prevent running beforeTrans for tests + + nav._postRender(1, enteringView, leavingView, false, navOptions, done); + + expect(navOptions.animate).toBe(false); + }); + + it('should set domShow true when isAlreadyTransitioning', () => { + let enteringView = new ViewController(Page1); + let leavingView = new ViewController(Page2); + let isAlreadyTransitioning = true; + var navOptions: NavOptions = {}; + var done = () => {}; + nav._beforeTrans = () => {}; //prevent running beforeTrans for tests + nav._renderer = null; + + spyOn(enteringView, 'domShow'); + spyOn(leavingView, 'domShow'); + + nav._postRender(1, enteringView, leavingView, isAlreadyTransitioning, navOptions, done); + + expect(enteringView.domShow).toHaveBeenCalledWith(true, nav._renderer); + expect(leavingView.domShow).toHaveBeenCalledWith(true, nav._renderer); + }); + + it('should set domShow true when isAlreadyTransitioning false for the entering/leaving views', () => { + let view1 = new ViewController(Page1); + let view2 = new ViewController(Page2); + let view3 = new ViewController(Page3); + let isAlreadyTransitioning = false; + var navOptions: NavOptions = {}; + var done = () => {}; + nav._beforeTrans = () => {}; //prevent running beforeTrans for tests + nav._renderer = null; + nav._views = [view1, view2, view3]; + + spyOn(view1, 'domShow'); + spyOn(view2, 'domShow'); + spyOn(view3, 'domShow'); + + nav._postRender(1, view3, view2, isAlreadyTransitioning, navOptions, done); + + expect(view1.domShow).toHaveBeenCalledWith(false, nav._renderer); + expect(view2.domShow).toHaveBeenCalledWith(true, nav._renderer); + expect(view3.domShow).toHaveBeenCalledWith(true, nav._renderer); + }); + + it('should set domShow true when isAlreadyTransitioning false for views when nav is a portal', () => { + let view1 = new ViewController(Page1); + let view2 = new ViewController(Page2); + let view3 = new ViewController(Page3); + let view4 = new ViewController(Page4); + let isAlreadyTransitioning = false; + var navOptions: NavOptions = {}; + var done = () => {}; + nav._beforeTrans = () => {}; //prevent running beforeTrans for tests + nav._renderer = null; + nav._views = [view1, view2, view3, view4]; + + nav._isPortal = true + + spyOn(view1, 'domShow'); + spyOn(view2, 'domShow'); + spyOn(view3, 'domShow'); + spyOn(view4, 'domShow'); + + nav._postRender(1, view4, view3, isAlreadyTransitioning, navOptions, done); + + expect(view1.domShow).toHaveBeenCalledWith(true, nav._renderer); + expect(view2.domShow).toHaveBeenCalledWith(true, nav._renderer); + expect(view3.domShow).toHaveBeenCalledWith(true, nav._renderer); + expect(view4.domShow).toHaveBeenCalledWith(true, nav._renderer); + }); + + }); + + describe('_setZIndex', () => { + + it('should set zIndex off of the previous view to the entering view is loaded and the leavingView is not loaded', () => { + let leavingView = new ViewController(); + leavingView.zIndex = 100; + leavingView.fireLoaded(); + let enteringView = new ViewController(); + enteringView.setPageRef(mockElementRef()); + + nav._views = [leavingView, enteringView]; + + nav._setZIndex(enteringView, leavingView, 'forward'); + expect(enteringView.zIndex).toEqual(101); + }); + + it('should set zIndex 100 when leaving view is not loaded', () => { + let leavingView = new ViewController(); + let enteringView = new ViewController(); + enteringView.setPageRef(mockElementRef()); + + nav._views = [leavingView, enteringView]; + + nav._setZIndex(enteringView, leavingView, 'forward'); + expect(enteringView.zIndex).toEqual(100); + }); + + it('should set zIndex 100 on first entering view', () => { + let enteringView = new ViewController(); + enteringView.setPageRef(mockElementRef()); + nav._setZIndex(enteringView, null, 'forward'); + expect(enteringView.zIndex).toEqual(100); + }); + + it('should set zIndex 1 on second entering view', () => { + let leavingView = new ViewController(); + leavingView.zIndex = 0; + leavingView.fireLoaded(); + let enteringView = new ViewController(); + enteringView.setPageRef(mockElementRef()); + nav._setZIndex(enteringView, leavingView, 'forward'); + expect(enteringView.zIndex).toEqual(1); + }); + + it('should set zIndex 0 on entering view going back', () => { + let leavingView = new ViewController(); + leavingView.zIndex = 1; + leavingView.fireLoaded(); + let enteringView = new ViewController(); + enteringView.setPageRef(mockElementRef()); + nav._setZIndex(enteringView, leavingView, 'back'); + expect(enteringView.zIndex).toEqual(0); + }); + + it('should set zIndex 9999 on first entering portal view', () => { + let enteringView = new ViewController(); + enteringView.setPageRef(mockElementRef()); + nav._isPortal = true; + nav._setZIndex(enteringView, null, 'forward'); + expect(enteringView.zIndex).toEqual(9999); + }); + + it('should set zIndex 10000 on second entering portal view', () => { + let leavingView = new ViewController(); + leavingView.zIndex = 9999; + leavingView.fireLoaded(); + let enteringView = new ViewController(); + enteringView.setPageRef(mockElementRef()); + nav._isPortal = true; + nav._setZIndex(enteringView, leavingView, 'forward'); + expect(enteringView.zIndex).toEqual(10000); + }); + + it('should set zIndex 9999 on entering portal view going back', () => { + let leavingView = new ViewController(); + leavingView.zIndex = 10000; + leavingView.fireLoaded(); + let enteringView = new ViewController(); + enteringView.setPageRef(mockElementRef()); + nav._isPortal = true; + nav._setZIndex(enteringView, leavingView, 'back'); + expect(enteringView.zIndex).toEqual(9999); + }); + + }); + + describe('_setAnimate', () => { + + it('should be unchanged when the nav is a portal', () => { + nav._views = [new ViewController()]; + nav._init = false; + nav._isPortal = true; + let opts: NavOptions = {}; + nav._setAnimate(opts); + expect(opts.animate).toBeUndefined(); + }); + + it('should not animate when theres only 1 view, and nav hasnt initialized yet', () => { + nav._views = [new ViewController()]; + nav._init = false; + let opts: NavOptions = {}; + nav._setAnimate(opts); + expect(opts.animate).toEqual(false); + }); + + it('should be unchanged when theres only 1 view, and nav has already initialized', () => { + nav._views = [new ViewController()]; + nav._init = true; + let opts: NavOptions = {}; + nav._setAnimate(opts); + expect(opts.animate).toBeUndefined(); + }); + + it('should not animate with config animate = false, and has initialized', () => { + nav.config.set('animate', false); + nav._init = true; + let opts: NavOptions = {}; + nav._setAnimate(opts); + expect(opts.animate).toEqual(false); + }); + + it('should not animate with config animate = false, and has not initialized', () => { + nav.config.set('animate', false); + nav._init = false; + let opts: NavOptions = {}; + nav._setAnimate(opts); + expect(opts.animate).toEqual(false); + }); + + }); + + describe('_afterTrans', () => { + + it('should call didEnter/didLeave', () => { + let enteringView = new ViewController(); + let leavingView = new ViewController(); + let navOpts: NavOptions = {}; + let hasCompleted = true; + let doneCalled = false; + let done = () => {doneCalled = true;} + + spyOn(enteringView, 'fireDidEnter'); + spyOn(leavingView, 'fireDidLeave'); + + nav._init = true; + nav._afterTrans(enteringView, leavingView, navOpts, hasCompleted, done); + + expect(enteringView.fireDidEnter).toHaveBeenCalled(); + expect(leavingView.fireDidLeave).toHaveBeenCalled(); + expect(doneCalled).toBe(true); + }); + + it('should not call didEnter/didLeave when preloaded', () => { + let enteringView = new ViewController(); + let leavingView = new ViewController(); + let navOpts: NavOptions = { + preload: true + }; + let hasCompleted = true; + let doneCalled = false; + let done = () => {doneCalled = true;} + + spyOn(enteringView, 'fireDidEnter'); + spyOn(leavingView, 'fireDidLeave'); + + nav._init = true; + nav._afterTrans(enteringView, leavingView, navOpts, hasCompleted, done); + + expect(enteringView.fireDidEnter).not.toHaveBeenCalled(); + expect(leavingView.fireDidLeave).not.toHaveBeenCalled(); + expect(doneCalled).toBe(true); + }); + + it('should not call didLeave when enteringView set fireOtherLifecycles to false', () => { + let enteringView = new ViewController(); + let leavingView = new ViewController(); + let navOpts: NavOptions = {}; + let hasCompleted = true; + let doneCalled = false; + let done = () => {doneCalled = true;} + + enteringView.fireOtherLifecycles = false; + + spyOn(enteringView, 'fireDidEnter'); + spyOn(leavingView, 'fireDidLeave'); + + nav._afterTrans(enteringView, leavingView, navOpts, hasCompleted, done); + + expect(enteringView.fireDidEnter).toHaveBeenCalled(); + expect(leavingView.fireDidLeave).not.toHaveBeenCalled(); + expect(doneCalled).toBe(true); + }); + + it('should not call didEnter when leavingView set fireOtherLifecycles to false', () => { + let enteringView = new ViewController(); + let leavingView = new ViewController(); + let navOpts: NavOptions = {}; + let hasCompleted = true; + let doneCalled = false; + let done = () => {doneCalled = true;} + + leavingView.fireOtherLifecycles = false; + + spyOn(enteringView, 'fireDidEnter'); + spyOn(leavingView, 'fireDidLeave'); + + nav._init = true; + nav._afterTrans(enteringView, leavingView, navOpts, hasCompleted, done); + + expect(enteringView.fireDidEnter).not.toHaveBeenCalled(); + expect(leavingView.fireDidLeave).toHaveBeenCalled(); + expect(doneCalled).toBe(true); + }); + + it('should not call didEnter/didLeave when not hasCompleted', () => { + let enteringView = new ViewController(); + let leavingView = new ViewController(); + let navOpts: NavOptions = {}; + let hasCompleted = false; + let doneCalled = false; + let done = () => {doneCalled = true;} + + spyOn(enteringView, 'fireDidEnter'); + spyOn(leavingView, 'fireDidLeave'); + + nav._afterTrans(enteringView, leavingView, navOpts, hasCompleted, done); + + expect(enteringView.fireDidEnter).not.toHaveBeenCalled(); + expect(leavingView.fireDidLeave).not.toHaveBeenCalled(); + expect(doneCalled).toBe(true); + }); + + }); + + describe('_transFinish', () => { + + it('should remove entering view if it was already set to cancel', () => { + let enteringView = new ViewController(Page1); + let leavingView = new ViewController(Page2); + enteringView.state = STATE_CANCEL_ENTER; + let direction = 'foward'; + let updateUrl = false; + let hasCompleted = true; + + spyOn(nav, 'remove'); + + nav._transFinish(1, enteringView, leavingView, direction, updateUrl, hasCompleted); + + expect(nav.remove).toHaveBeenCalled(); + expect(enteringView.state).toBe(STATE_CANCEL_ENTER); + }); + + it('should not entering/leaving state, after transition that isnt the most recent, and state already changed', () => { + let enteringView = new ViewController(Page1); + enteringView.state = 234234; + let leavingView = new ViewController(Page2); + leavingView.state = 234234; + let direction = 'foward'; + let updateUrl = false; + let hasCompleted = true; + + nav._transIds = 2; + + nav._transFinish(1, enteringView, leavingView, direction, updateUrl, hasCompleted); + + expect(enteringView.state).toBe(234234); + expect(leavingView.state).toBe(234234); + }); + + it('should set entering/leaving to inactive, after transition that isnt the most recent', () => { + let enteringView = new ViewController(Page1); + enteringView.state = STATE_TRANS_ENTER; + let leavingView = new ViewController(Page2); + leavingView.state = STATE_TRANS_LEAVE; + let direction = 'foward'; + let updateUrl = false; + let hasCompleted = true; + + nav._transIds = 2; + + nav._transFinish(1, enteringView, leavingView, direction, updateUrl, hasCompleted); + + expect(enteringView.state).toBe(STATE_INACTIVE); + expect(leavingView.state).toBe(STATE_INACTIVE); + }); + + it('should set entering active, leaving inactive, after transition', () => { + let enteringView = new ViewController(Page1); + enteringView.state = STATE_TRANS_ENTER; + let leavingView = new ViewController(Page2); + leavingView.state = STATE_TRANS_LEAVE; + let direction = 'foward'; + let updateUrl = false; + let hasCompleted = true; + + nav._transIds = 1; + + nav._transFinish(1, enteringView, leavingView, direction, updateUrl, hasCompleted); + + expect(enteringView.state).toBe(STATE_ACTIVE); + expect(leavingView.state).toBe(STATE_INACTIVE); + }); + + it('should set entering inactive, leaving active, after transition has not completed', () => { + let enteringView = new ViewController(Page1); + enteringView.state = STATE_TRANS_ENTER; + let leavingView = new ViewController(Page2); + leavingView.state = STATE_TRANS_LEAVE; + let direction = 'back'; + let updateUrl = false; + let hasCompleted = false; + + nav._transIds = 1; + + nav._transFinish(1, enteringView, leavingView, direction, updateUrl, hasCompleted); + + expect(enteringView.state).toBe(STATE_INACTIVE); + expect(leavingView.state).toBe(STATE_ACTIVE); + }); + + it('should run cleanup when most recent transition and has completed', () => { + let enteringView = new ViewController(Page1); + enteringView.state = STATE_TRANS_ENTER; + let leavingView = new ViewController(Page2); + leavingView.state = STATE_TRANS_LEAVE; + let direction = 'back'; + let updateUrl = false; + let hasCompleted = true; + + spyOn(nav, '_cleanup'); + + nav._transIds = 1; + + nav._transFinish(1, enteringView, leavingView, direction, updateUrl, hasCompleted); + + expect(nav._cleanup).toHaveBeenCalled(); + }); + + it('should not run cleanup when most not recent transition', () => { + let enteringView = new ViewController(Page1); + enteringView.state = STATE_TRANS_ENTER; + let leavingView = new ViewController(Page2); + leavingView.state = STATE_TRANS_LEAVE; + let direction = 'back'; + let updateUrl = false; + let hasCompleted = true; + + spyOn(nav, '_cleanup'); + + nav._transIds = 1; + + nav._transFinish(2, enteringView, leavingView, direction, updateUrl, hasCompleted); + + expect(nav._cleanup).not.toHaveBeenCalled(); + }); + + it('should not run cleanup when it hasnt completed transition, but is the most recent', () => { + let enteringView = new ViewController(Page1); + enteringView.state = STATE_TRANS_ENTER; + let leavingView = new ViewController(Page2); + leavingView.state = STATE_TRANS_LEAVE; + let direction = 'back'; + let updateUrl = false; + let hasCompleted = false; + + spyOn(nav, '_cleanup'); + + nav._transIds = 1; + + nav._transFinish(1, enteringView, leavingView, direction, updateUrl, hasCompleted); + + expect(nav._cleanup).not.toHaveBeenCalled(); + }); + + it('should set transitioning is over when most recent transition finishes', () => { + let enteringView = new ViewController(Page1); + enteringView.state = STATE_TRANS_ENTER; + let leavingView = new ViewController(Page2); + leavingView.state = STATE_TRANS_LEAVE; + let direction = 'back'; + let updateUrl = false; + let hasCompleted = true; + + spyOn(nav, 'setTransitioning'); + + nav._transIds = 1; + + nav._transFinish(1, enteringView, leavingView, direction, updateUrl, hasCompleted); + + expect(nav.setTransitioning).toHaveBeenCalledWith(false); + }); + + it('should set transitioning is not over if its not the most recent transition', () => { + let enteringView = new ViewController(Page1); + enteringView.state = STATE_TRANS_ENTER; + let leavingView = new ViewController(Page2); + leavingView.state = STATE_TRANS_LEAVE; + let direction = 'back'; + let updateUrl = false; + let hasCompleted = true; + + spyOn(nav, 'setTransitioning'); + + nav._transIds = 2; + + nav._transFinish(1, enteringView, leavingView, direction, updateUrl, hasCompleted); + + expect(nav.setTransitioning).not.toHaveBeenCalled(); + }); + + it('should re-enable the app when transition time <= 0', () => { + // arrange + let enteringView = new ViewController(Page1); + enteringView.state = 234234; + let leavingView = new ViewController(Page2); + leavingView.state = 234234; + nav._transIds = 1; + + let direction = 'forward'; + let updateUrl = false; + let hasCompleted = true; + + spyOn(nav._app, 'setEnabled'); + spyOn(nav, 'setTransitioning'); + + // act + nav._transFinish(nav._transIds, enteringView, leavingView, direction, updateUrl, hasCompleted); + + // assert + expect(nav._app.setEnabled).toHaveBeenCalledWith(true); + expect(nav.setTransitioning).toHaveBeenCalledWith(false); + }); + + it('should not re-enable app when transition time > 0', () => { + // arrange + let enteringView = new ViewController(Page1); + enteringView.state = 235234; + let leavingView = new ViewController(Page2); + leavingView.state = 235234; + nav._transIds = 1; + + let direction = 'forward'; + let updateUrl = false; + let hasCompleted = true; + + spyOn(nav._app, 'setEnabled'); + spyOn(nav, 'setTransitioning'); + + nav.getLongestTrans = () => { return 50 }; + + // act + nav._transFinish(nav._transIds, enteringView, leavingView, direction, updateUrl, hasCompleted); + + // assert + expect(nav._app.setEnabled).not.toHaveBeenCalled(); + expect(nav.setTransitioning).toHaveBeenCalledWith(false); + }); + + }); + + describe('_insert', () => { + + it('should push page when previous transition is still actively transitioning', () => { + let view1 = new ViewController(Page1); + view1.state = STATE_TRANS_ENTER; + let view2 = new ViewController(Page2); + view2.state = STATE_TRANS_LEAVE; + nav._views = [view1, view2]; + + let view3 = new ViewController(Page3); + nav._insert(-1, [view3]); + + expect(nav.getByIndex(0).state).toBe(STATE_TRANS_ENTER); + expect(nav.getByIndex(0).componentType).toBe(Page1); + expect(nav.getByIndex(1).state).toBe(STATE_TRANS_LEAVE); + expect(nav.getByIndex(1).componentType).toBe(Page2); + expect(nav.getByIndex(2).state).toBe(STATE_INIT_ENTER); + expect(nav.getByIndex(2).componentType).toBe(Page3); + }); + + it('should push page when previous transition views init, but havent transitioned yet', () => { + let view1 = new ViewController(Page1); + view1.state = STATE_INIT_LEAVE; + let view2 = new ViewController(Page2); + view2.state = STATE_INIT_ENTER; + nav._views = [view1, view2]; + + let view3 = new ViewController(Page3); + nav._insert(-1, [view3]); + + expect(nav.getByIndex(0).state).toBe(STATE_INIT_LEAVE); + expect(nav.getByIndex(0).componentType).toBe(Page1); + expect(nav.getByIndex(1).state).toBe(STATE_INACTIVE); + expect(nav.getByIndex(1).componentType).toBe(Page2); + expect(nav.getByIndex(2).state).toBe(STATE_INIT_ENTER); + expect(nav.getByIndex(2).componentType).toBe(Page3); + }); + + it('should insert multiple pages, back to back, with a starting active page', () => { + let view1 = new ViewController(Page1); + view1.state = STATE_ACTIVE; + nav._views = [view1]; + + let view2 = new ViewController(Page2); + nav._insert(-1, [view2]); + + expect(nav.getByIndex(0).state).toBe(STATE_INIT_LEAVE); + expect(nav.getByIndex(0).componentType).toBe(Page1); + expect(nav.getByIndex(1).state).toBe(STATE_INIT_ENTER); + expect(nav.getByIndex(1).componentType).toBe(Page2); + + let view3 = new ViewController(Page3); + nav._insert(-1, [view3]); + + expect(nav.getByIndex(0).state).toBe(STATE_INIT_LEAVE); + expect(nav.getByIndex(0).componentType).toBe(Page1); + expect(nav.getByIndex(1).state).toBe(STATE_INACTIVE); + expect(nav.getByIndex(1).componentType).toBe(Page2); + expect(nav.getByIndex(2).state).toBe(STATE_INIT_ENTER); + expect(nav.getByIndex(2).componentType).toBe(Page3); + }); + + it('should insert multiple pages, back to back, no starting active page', () => { + let view1 = new ViewController(Page1); + nav._insert(-1, [view1]); + expect(nav.getByIndex(0).state).toBe(STATE_INIT_ENTER); + expect(nav.getByIndex(0).componentType).toBe(Page1); + + let view2 = new ViewController(Page2); + nav._insert(-1, [view2]); + + expect(nav.getByIndex(0).state).toBe(STATE_INACTIVE); + expect(nav.getByIndex(0).componentType).toBe(Page1); + expect(nav.getByIndex(1).state).toBe(STATE_INIT_ENTER); + expect(nav.getByIndex(1).componentType).toBe(Page2); + + let view3 = new ViewController(Page3); + nav._insert(1, [view3]); + + expect(nav.getByIndex(0).state).toBe(STATE_INACTIVE); + expect(nav.getByIndex(0).componentType).toBe(Page1); + expect(nav.getByIndex(1).state).toBe(STATE_INIT_ENTER); + expect(nav.getByIndex(1).componentType).toBe(Page3); + expect(nav.getByIndex(2).state).toBe(STATE_INACTIVE); + expect(nav.getByIndex(2).componentType).toBe(Page2); + }); + + it('should push a page, and abort previous init', () => { + let view1 = new ViewController(Page1); + view1.state = STATE_INIT_LEAVE; + let view2 = new ViewController(Page2); + view2.state = STATE_INIT_ENTER; + nav._views = [view1, view2]; + + let view3 = new ViewController(Page3); + nav._insert(-1, [view3]); + expect(nav.length()).toBe(3); + + expect(nav.getByIndex(0).state).toBe(STATE_INIT_LEAVE); + expect(nav.getByIndex(0).componentType).toBe(Page1); + expect(nav.getByIndex(1).state).toBe(STATE_INACTIVE); + expect(nav.getByIndex(1).componentType).toBe(Page2); + expect(nav.getByIndex(2).state).toBe(STATE_INIT_ENTER); + expect(nav.getByIndex(2).componentType).toBe(Page3); + }); + + it('should insert a page between the first and second', () => { + let view1 = new ViewController(Page1); + view1.state = STATE_INACTIVE; + let view2 = new ViewController(Page2); + view2.state = STATE_ACTIVE; + nav._views = [view1, view2]; + + let view3 = new ViewController(Page3); + nav._insert(1, [view3]); + expect(nav.length()).toBe(3); + + expect(nav.getByIndex(0).state).toBe(STATE_INACTIVE); + expect(nav.getByIndex(0).componentType).toBe(Page1); + expect(nav.getByIndex(1).state).toBe(STATE_INIT_ENTER); + expect(nav.getByIndex(1).componentType).toBe(Page3); + expect(nav.getByIndex(2).state).toBe(STATE_INIT_LEAVE); + expect(nav.getByIndex(2).componentType).toBe(Page2); + }); + + it('should insert a page before the first', () => { + let view1 = new ViewController(Page1); + view1.state = STATE_ACTIVE; + nav._views = [view1]; + + let view2 = new ViewController(Page2); + nav._insert(0, [view2]); + expect(nav.getByIndex(0).state).toBe(STATE_INIT_ENTER); + expect(nav.getByIndex(0).componentType).toBe(Page2); + expect(nav.getByIndex(1).state).toBe(STATE_INIT_LEAVE); + expect(nav.getByIndex(1).componentType).toBe(Page1); + }); + + it('should insert 3 pages', () => { + let view1 = new ViewController(Page1); + view1.state = STATE_ACTIVE; + nav._views = [view1]; + + let insertViews = [ + new ViewController(Page2), + new ViewController(Page3), + new ViewController(Page4) + ]; + nav._insert(-1, insertViews); + expect(nav.length()).toBe(4); + expect(nav.getByIndex(0).state).toBe(STATE_INIT_LEAVE); + expect(nav.getByIndex(0).componentType).toBe(Page1); + expect(nav.getByIndex(1).state).toBe(STATE_INACTIVE); + expect(nav.getByIndex(1).componentType).toBe(Page2); + expect(nav.getByIndex(2).state).toBe(STATE_INACTIVE); + expect(nav.getByIndex(2).componentType).toBe(Page3); + expect(nav.getByIndex(3).state).toBe(STATE_INIT_ENTER); + expect(nav.getByIndex(3).componentType).toBe(Page4); + }); + + it('should push the second page', () => { + let view1 = new ViewController(Page1); + view1.state = STATE_ACTIVE; + nav._views = [view1]; + + let view2 = new ViewController(Page2) + nav._insert(-1, [view2]); + expect(nav.length()).toBe(2); + expect(nav.getByIndex(0).state).toBe(STATE_INIT_LEAVE); + expect(nav.getByIndex(0).componentType).toBe(Page1); + expect(nav.getByIndex(1).state).toBe(STATE_INIT_ENTER); + expect(nav.getByIndex(1).componentType).toBe(Page2); + }); + + it('should push the first page, using a number greater than the length', () => { + let view1 = new ViewController(Page1) + nav._insert(8675309, [view1]); + + expect(nav.length()).toBe(1); + expect(nav.getByIndex(0).state).toBe(STATE_INIT_ENTER); + }); + + it('should push the first page, using -1', () => { + let view1 = new ViewController(Page1) + nav._insert(-1, [view1]); + + expect(nav.getByIndex(0).id).toBeDefined(); + expect(nav.length()).toBe(1); + expect(nav.getByIndex(0).state).toBe(STATE_INIT_ENTER); + }); + + }); + + describe('getActive', () => { + it('should getActive()', () => { + expect(nav.getActive()).toBe(null); + let view1 = new ViewController(Page1); + view1.state = STATE_INIT_ENTER; + nav._views = [view1]; + expect(nav.getActive()).toBe(null); + view1.state = STATE_ACTIVE; + expect(nav.getActive()).toBe(view1); + }); + }); + + describe('getByState', () => { + it('should getByState()', () => { + expect(nav.getByState(null)).toBe(null); + + let view1 = new ViewController(Page1); + view1.state = STATE_INIT_ENTER; + let view2 = new ViewController(Page2); + view2.state = STATE_INIT_ENTER; + nav._views = [view1, view2]; + + expect(nav.getByState(8675309)).toBe(null); + expect(nav.getByState(STATE_INIT_ENTER)).toBe(view2); + + view2.state = STATE_INACTIVE; + expect(nav.getByState(STATE_INIT_ENTER)).toBe(view1); + + view2.state = STATE_ACTIVE; + expect(nav.getActive()).toBe(view2); + }); + }); + + describe('getPrevious', () => { + it('should getPrevious()', () => { + expect(nav.getPrevious(null)).toBe(null); + + let view1 = new ViewController(Page1); + let view2 = new ViewController(Page2); + nav._views = [view1, view2]; + + expect(nav.getPrevious(view1)).toBe(null); + expect(nav.getPrevious(view2)).toBe(view1); + }); + }); + + describe('first', () => { + it('should get first()', () => { + expect(nav.first()).toBe(null); + let view1 = new ViewController(Page1); + view1.setNav(nav); + let view2 = new ViewController(Page2); + view2.setNav(nav); + nav._views = [view1]; + + expect(nav.first()).toBe(view1); + expect(view1.isFirst()).toBe(true); + + nav._views = [view1, view2]; + expect(nav.first()).toBe(view1); + expect(view1.isFirst()).toBe(true); + expect(view2.isFirst()).toBe(false); + }); + }); + + describe('last', () => { + it('should get last()', () => { + expect(nav.last()).toBe(null); + let view1 = new ViewController(Page1); + view1.setNav(nav); + let view2 = new ViewController(Page2); + view2.setNav(nav); + nav._views = [view1]; + + expect(nav.last()).toBe(view1); + expect(view1.isLast()).toBe(true); + + nav._views = [view1, view2]; + expect(nav.last()).toBe(view2); + expect(view1.isLast()).toBe(false); + expect(view2.isLast()).toBe(true); + }); + }); + + describe('indexOf', () => { + it('should get indexOf()', () => { + let view1 = new ViewController(Page1); + let view2 = new ViewController(Page2); + + expect(nav.length()).toBe(0); + expect(nav.indexOf(view1)).toBe(-1); + + nav._views = [view1, view2]; + expect(nav.indexOf(view1)).toBe(0); + expect(nav.indexOf(view2)).toBe(1); + expect(nav.length()).toBe(2); + }); + }); + + describe('getByIndex', () => { + it('should get getByIndex()', () => { + expect(nav.getByIndex(-99)).toBe(null); + expect(nav.getByIndex(99)).toBe(null); + + let view1 = new ViewController(Page1); + let view2 = new ViewController(Page2); + nav._views = [view1, view2]; + + expect(nav.getByIndex(-1)).toBe(null); + expect(nav.getByIndex(0)).toBe(view1); + expect(nav.getByIndex(1)).toBe(view2); + expect(nav.getByIndex(2)).toBe(null); + }); + }); + + /* private method */ + describe('_beforeTrans', () => { + + it('shouldnt disable app on short transition', () => { + // arrange + let executeAssertions = () => { + // assertions triggerd by callbacks + expect(nav._app.setEnabled).toHaveBeenCalledWith(true, 50); + expect(nav.setTransitioning).toHaveBeenCalledWith(false, 50); + }; + nav._createTrans = mockTransition(executeAssertions, 50); + + spyOn(nav._app, 'setEnabled'); + spyOn(nav, 'setTransitioning'); + + let view1 = new ViewController(Page1); + let view2 = new ViewController(Page2); + + // act + nav._beforeTrans(view1, view2, {}, () => {}); + }); + + it('should disable app on longer transition', () => { + // arrange + let executeAssertions = () => { + // assertions triggerd by callbacks + expect(nav._app.setEnabled).toHaveBeenCalledWith(false, 200); + expect(nav.setTransitioning).toHaveBeenCalledWith(true, 200); + }; + + nav._createTrans = mockTransition(executeAssertions, 200); + + spyOn(nav._app, 'setEnabled'); + spyOn(nav, 'setTransitioning'); + + let view1 = new ViewController(Page1); + let view2 = new ViewController(Page2); + + // act + nav._beforeTrans(view1, view2, {}, () => {}); + }); + + it('should disable app w/ padding when keyboard is open', () => { + // arrange + let executeAssertions = () => { + // assertions triggerd by callbacks + expect(nav._app.setEnabled.calls.mostRecent().args[0]).toEqual(false); + expect(nav._app.setEnabled.calls.mostRecent().args[1]).toBeGreaterThan(200); + + expect(nav.setTransitioning.calls.mostRecent().args[0]).toEqual(true); + expect(nav.setTransitioning.calls.mostRecent().args[1]).toBeGreaterThan(200); + }; + + nav._createTrans = mockTransition(executeAssertions, 200); + + nav._keyboard.isOpen = () => true; + + spyOn(nav._app, 'setEnabled'); + spyOn(nav, 'setTransitioning'); + + let view1 = new ViewController(Page1); + let view2 = new ViewController(Page2); + + // act + nav._beforeTrans(view1, view2, {}, () => {}); + }); + + it('shouldnt update app enabled when parent transition is occurring', () => { + // arrange + let executeAssertions = () => { + // assertions triggerd by callbacks + expect(nav._app.setEnabled).not.toHaveBeenCalled(); + expect(nav.setTransitioning.calls.mostRecent().args[0]).toEqual(true); + }; + + nav._createTrans = mockTransition(executeAssertions, 200); + + spyOn(nav._app, 'setEnabled'); + spyOn(nav, 'setTransitioning'); + + nav.getLongestTrans = () => { return Date.now() + 100 }; + + let view1 = new ViewController(Page1); + let view2 = new ViewController(Page2); + + // act + nav._beforeTrans(view1, view2, {}, () => {}); + }); + + it('should not begin transition when entering stated is inactive', () => { + let view1 = new ViewController(Page1); + view1.state = STATE_INACTIVE; + + let wasDoneCalled = false; + let done = () => { + wasDoneCalled = true; + }; + + nav._beforeTrans(view1, null, {}, done); + + expect(wasDoneCalled).toEqual(true); + expect(view1.state).toEqual(STATE_INACTIVE); + }); + + it('should not begin transition when entering state is canceled', () => { + let view1 = new ViewController(Page1); + view1.state = STATE_CANCEL_ENTER; + + let wasDoneCalled = false; + let done = () => { + wasDoneCalled = true; + }; + + nav._beforeTrans(view1, null, {}, done); + + expect(wasDoneCalled).toEqual(true); + expect(view1.state).toEqual(STATE_CANCEL_ENTER); + }); + }); + + /* private method */ + describe('getLongestTrans', () => { + it('should return 0 when transition end time is less than 0', () => { + // arrange + nav.parent = null; + // act + let returnedValue = nav.getLongestTrans(Date.now()); + // asssert + expect(returnedValue).toEqual(0); + }); + + it('should return 0 when transition end time is less than now', () => { + // arrange + nav.parent = { + trnsTime: Date.now() - 5 + }; + // act + let returnedValue = nav.getLongestTrans(Date.now()); + // asssert + expect(returnedValue).toEqual(0); + }); + + it('should return 0 when parent transition time not set', () => { + // arrange + nav.parent = { + trnsTime: undefined + }; + // act + let returnedValue = nav.getLongestTrans(Date.now()); + // asssert + expect(returnedValue).toEqual(0); + }); + + it('should return transitionEndTime when transition end time is greater than now', () => { + // arrange + let expectedReturnValue = Date.now() + 100; + nav.parent = { + trnsTime: expectedReturnValue + }; + // act + let returnedValue = nav.getLongestTrans(Date.now()); + // asssert + expect(returnedValue).toEqual(expectedReturnValue); + }); + + it('should return the greatest end of transition time if found on first parent', () => { + // arrange + let expectedReturnValue = Date.now() + 100; + let firstParent: any = { + trnsTime: expectedReturnValue + }; + let secondParent: any = { + trnsTime: Date.now() + 50 + }; + let thirdParent: any = { + trnsTime: Date.now() + }; + let fourthParent: any = { + trnsTime: Date.now() + 20 + }; + firstParent.parent = secondParent; + secondParent.parent = thirdParent; + thirdParent.parent = fourthParent; + nav.parent = firstParent; + // act + let returnedValue = nav.getLongestTrans(Date.now()); + // asssert + expect(returnedValue).toEqual(expectedReturnValue); + }); + + it('should return the greatest end of transition time if found on middle parent', () => { + // arrange + let expectedReturnValue = Date.now() + 100; + let firstParent: any = { + trnsTime: Date.now() + }; + let secondParent: any = { + trnsTime: Date.now() + 50 + }; + let thirdParent: any = { + trnsTime: expectedReturnValue + }; + let fourthParent = { + trnsTime: Date.now() + 20 + }; + firstParent.parent = secondParent; + secondParent.parent = thirdParent; + thirdParent.parent = fourthParent; + nav.parent = firstParent; + // act + let returnedValue = nav.getLongestTrans(Date.now()); + // asssert + expect(returnedValue).toEqual(expectedReturnValue); + }); + + it('should return the greatest end of transition time if found on last parent', () => { + // arrange + let expectedReturnValue = Date.now() + 100; + let firstParent: any = { + trnsTime: Date.now() + }; + let secondParent: any = { + trnsTime: Date.now() + 50 + }; + let thirdParent: any = { + trnsTime: Date.now() + 20 + }; + let fourthParent = { + trnsTime: expectedReturnValue + }; + firstParent.parent = secondParent; + secondParent.parent = thirdParent; + thirdParent.parent = fourthParent; + nav.parent = firstParent; + // act + let returnedValue = nav.getLongestTrans(Date.now()); + // asssert + expect(returnedValue).toEqual(expectedReturnValue); + }); + }); + + // setup stuff + let nav: NavControllerBase; + + class Page1 {} + class Page2 {} + class Page3 {} + class Page4 {} + class Page5 {} + + beforeEach(() => { + nav = mockNavController(); + }); + +}); } - -class MockNavController extends NavController { - - get views(): ViewController[] { - return this._views; - } - set views(views: ViewController[]) { - this._views = views; - } - -} - - -const STATE_ACTIVE = 1; -const STATE_INACTIVE = 2; -const STATE_INIT_ENTER = 3; -const STATE_INIT_LEAVE = 4; -const STATE_TRANS_ENTER = 5; -const STATE_TRANS_LEAVE = 6; -const STATE_REMOVE = 7; -const STATE_REMOVE_AFTER_TRANS = 8; -const STATE_CANCEL_ENTER = 9; -const STATE_FORCE_ACTIVE = 10; diff --git a/src/components/picker/picker-component.ts b/src/components/picker/picker-component.ts index b3a2ad7da8..b3f95ef404 100644 --- a/src/components/picker/picker-component.ts +++ b/src/components/picker/picker-component.ts @@ -24,7 +24,7 @@ import { ViewController } from '../nav/view-controller'; template: `

{{col.prefix}}
-
@@ -74,11 +74,12 @@ export class PickerColumnCmp { this.setSelected(this.col.selectedIndex, 0); // Listening for pointer events - this.events.pointerEventsRef(this.elementRef, - (ev: any) => this.pointerStart(ev), - (ev: any) => this.pointerMove(ev), - (ev: any) => this.pointerEnd(ev) - ); + this.events.pointerEvents({ + elementRef: this.elementRef, + pointerDown: this.pointerStart.bind(this), + pointerMove: this.pointerMove.bind(this), + pointerUp: this.pointerEnd.bind(this) + }); } ngOnDestroy() { diff --git a/src/components/range/range.ts b/src/components/range/range.ts index afc89f0abe..ddb9e91169 100644 --- a/src/components/range/range.ts +++ b/src/components/range/range.ts @@ -364,11 +364,12 @@ export class Range implements AfterViewInit, ControlValueAccessor, OnDestroy { this._renderer.setElementStyle(this._bar.nativeElement, 'right', barR); // add touchstart/mousedown listeners - this._events.pointerEventsRef(this._slider, - this.pointerDown.bind(this), - this.pointerMove.bind(this), - this.pointerUp.bind(this)); - + this._events.pointerEvents({ + elementRef: this._slider, + pointerDown: this.pointerDown.bind(this), + pointerMove: this.pointerMove.bind(this), + pointerUp: this.pointerUp.bind(this) + }); this.createTicks(); } @@ -430,18 +431,12 @@ export class Range implements AfterViewInit, ControlValueAccessor, OnDestroy { ev.preventDefault(); ev.stopPropagation(); - if (this._start !== null && this._active !== null) { - // only use pointer move if it's a valid pointer - // and we already have start coordinates + // update the ratio for the active knob + this.updateKnob(pointerCoord(ev), this._rect); - // update the ratio for the active knob - this.updateKnob(pointerCoord(ev), this._rect); - - // update the active knob's position - this._active.position(); - this._pressed = this._active.pressed = true; - - } + // update the active knob's position + this._active.position(); + this._pressed = this._active.pressed = true; } /** diff --git a/src/components/refresher/refresher.ts b/src/components/refresher/refresher.ts index eda4e1c406..7acebde8c2 100644 --- a/src/components/refresher/refresher.ts +++ b/src/components/refresher/refresher.ts @@ -2,6 +2,7 @@ import { Directive, EventEmitter, Host, Input, Output, NgZone } from '@angular/c import { Content } from '../content/content'; import { CSS, pointerCoord } from '../../util/dom'; +import { GestureController, GestureDelegate, GesturePriority } from '../../gestures/gesture-controller'; import { isTrueProperty } from '../../util/util'; import { PointerEvents, UIEventManager } from '../../util/ui-event-manager'; @@ -98,6 +99,7 @@ export class Refresher { private _didStart: boolean; private _lastCheck: number = 0; private _isEnabled: boolean = true; + private _gesture: GestureDelegate; private _events: UIEventManager = new UIEventManager(false); private _pointerEvents: PointerEvents; private _top: string = ''; @@ -196,8 +198,11 @@ export class Refresher { @Output() ionStart: EventEmitter = new EventEmitter(); - constructor(@Host() private _content: Content, private _zone: NgZone) { + constructor(@Host() private _content: Content, private _zone: NgZone, gestureCtrl: GestureController) { _content.addCssClass('has-refresher'); + this._gesture = gestureCtrl.create('refresher', { + priority: GesturePriority.Refresher, + }); } private _onStart(ev: TouchEvent): any { @@ -216,6 +221,10 @@ export class Refresher { return false; } + if (!this._gesture.canStart()) { + return false; + } + let coord = pointerCoord(ev); console.debug('Pull-to-refresh, onStart', ev.type, 'y:', coord.y); @@ -228,7 +237,7 @@ export class Refresher { this.startY = this.currentY = coord.y; this.progress = 0; - this.state = STATE_PULLING; + this.state = STATE_INACTIVE; return true; } @@ -242,6 +251,10 @@ export class Refresher { return 1; } + if (!this._gesture.canStart()) { + return 0; + } + // do nothing if it's actively refreshing // or it's in the process of closing // or this was never a startY @@ -462,10 +475,12 @@ export class Refresher { this._events.unlistenAll(); this._pointerEvents = null; if (shouldListen) { - this._pointerEvents = this._events.pointerEvents(this._content.getScrollElement(), - this._onStart.bind(this), - this._onMove.bind(this), - this._onEnd.bind(this)); + this._pointerEvents = this._events.pointerEvents({ + element: this._content.getScrollElement(), + pointerDown: this._onStart.bind(this), + pointerMove: this._onMove.bind(this), + pointerUp: this._onEnd.bind(this) + }); } } @@ -482,6 +497,7 @@ export class Refresher { * @private */ ngOnDestroy() { + this._gesture.destroy(); this._setListeners(false); } diff --git a/src/components/refresher/test/refresher.spec.ts b/src/components/refresher/test/refresher.spec.ts index 6daf15f543..8ddb21a1e8 100644 --- a/src/components/refresher/test/refresher.spec.ts +++ b/src/components/refresher/test/refresher.spec.ts @@ -1,4 +1,4 @@ -import {Refresher, Content, Config, Ion} from '../../../../src'; +import { Refresher, Content, Config, GestureController, Ion } from '../../../../src'; export function run() { @@ -218,17 +218,19 @@ describe('Refresher', () => { let refresher: Refresher; let content: Content; let contentElementRef; + let gestureController: GestureController; let zone = { - run: function(cb) {cb()}, - runOutsideAngular: function(cb) {cb()} + run: function (cb) { cb(); }, + runOutsideAngular: function (cb) { cb(); } }; beforeEach(() => { contentElementRef = mockElementRef(); - content = new Content(contentElementRef, config, null, null, null); + gestureController = new GestureController(); + content = new Content(contentElementRef, config, null, null, zone, null, null); content._scrollEle = document.createElement('scroll-content'); - refresher = new Refresher(content, zone, mockElementRef()); + refresher = new Refresher(content, zone, gestureController); }); function touchEv(y: number) { diff --git a/src/components/slides/test/intro/index.ts b/src/components/slides/test/intro/index.ts index 7913afa37a..8b3b6fa4af 100644 --- a/src/components/slides/test/intro/index.ts +++ b/src/components/slides/test/intro/index.ts @@ -1,5 +1,5 @@ -import {Component} from '@angular/core'; -import {ionicBootstrap, NavController} from '../../../../../src'; +import { Component} from '@angular/core'; +import { ionicBootstrap, NavController } from '../../../../../src'; @Component({ @@ -8,7 +8,7 @@ import {ionicBootstrap, NavController} from '../../../../../src'; class IntroPage { continueText: string = "Skip"; startingIndex: number = 1; - mySlideOptions; + mySlideOptions: any; showSlide: boolean = true; constructor(private nav: NavController) { @@ -20,16 +20,16 @@ class IntroPage { }; } - onSlideChanged(slider) { - console.log("Slide changed", slider); + onSlideChanged(slider: any) { + console.log('Slide changed', slider); } - onSlideChangeStart(slider) { - console.log("Slide change start", slider); - slider.isEnd ? this.continueText = "Continue" : this.continueText = "Skip"; + onSlideChangeStart(slider: any) { + console.log('Slide change start', slider); + slider.isEnd ? this.continueText = 'Continue' : this.continueText = "Skip"; } - onSlideMove(slider) { + onSlideMove(slider: any) { console.log("Slide move", slider); } diff --git a/src/components/slides/test/loop/index.ts b/src/components/slides/test/loop/index.ts index ef3480cd2d..8b95e2cd42 100644 --- a/src/components/slides/test/loop/index.ts +++ b/src/components/slides/test/loop/index.ts @@ -14,16 +14,16 @@ class E2EApp { constructor() { this.slides = [ { - name: "Slide 1", - class: "yellow" + name: 'Slide 1', + class: 'yellow' }, { - name: "Slide 2", - class: "red" + name: 'Slide 2', + class: 'red' }, { - name: "Slide 3", - class: "blue" + name: 'Slide 3', + class: 'blue' } ]; diff --git a/src/components/tabs/tab.ts b/src/components/tabs/tab.ts index a91aeb9166..e04ab0a3aa 100644 --- a/src/components/tabs/tab.ts +++ b/src/components/tabs/tab.ts @@ -1,11 +1,11 @@ -import { ChangeDetectorRef, Component, ComponentResolver, ElementRef, EventEmitter, forwardRef, Input, Inject, NgZone, Output, Renderer, ViewChild, ViewEncapsulation, ViewContainerRef } from '@angular/core'; +import { ChangeDetectorRef, Component, ComponentResolver, ElementRef, EventEmitter, forwardRef, Input, Inject, NgZone, Optional, Output, Renderer, ViewChild, ViewEncapsulation, ViewContainerRef } from '@angular/core'; import { App } from '../app/app'; import { Config } from '../../config/config'; +import { GestureController } from '../../gestures/gesture-controller'; import { isTrueProperty} from '../../util/util'; import { Keyboard} from '../../util/keyboard'; -import { MenuController } from '../menu/menu-controller'; -import { NavController } from '../nav/nav-controller'; +import { NavControllerBase } from '../nav/nav-controller-base'; import { NavOptions} from '../nav/nav-interfaces'; import { TabButton} from './tab-button'; import { Tabs} from './tabs'; @@ -128,7 +128,7 @@ import { ViewController} from '../nav/view-controller'; template: '
', encapsulation: ViewEncapsulation.None, }) -export class Tab extends NavController { +export class Tab extends NavControllerBase { private _isInitial: boolean; private _isEnabled: boolean = true; private _isShown: boolean = true; @@ -229,17 +229,13 @@ export class Tab extends NavController { renderer: Renderer, compiler: ComponentResolver, private _cd: ChangeDetectorRef, - menuCtrl: MenuController + gestureCtrl: GestureController ) { // A Tab is a NavController for its child pages - super(parent, app, config, keyboard, elementRef, zone, renderer, compiler, menuCtrl); + super(parent, app, config, keyboard, elementRef, zone, renderer, compiler, gestureCtrl); parent.add(this); - if (parent.rootNav) { - this._sbEnabled = parent.rootNav.isSwipeBackEnabled(); - } - this._tabId = 'tabpanel-' + this.id; this._btnId = 'tab-' + this.id; } @@ -264,7 +260,7 @@ export class Tab extends NavController { */ load(opts: NavOptions, done?: Function) { if (!this._loaded && this.root) { - this.push(this.root, this.rootParams, opts).then(() => { + this.push(this.root, this.rootParams, opts, () => { done(true); }); this._loaded = true; @@ -294,20 +290,7 @@ export class Tab extends NavController { * @private */ loadPage(viewCtrl: ViewController, viewport: ViewContainerRef, opts: NavOptions, done: Function) { - let isTabSubPage = (this.parent.subPages && viewCtrl.index > 0); - - if (isTabSubPage) { - viewport = this.parent.portal; - } - super.loadPage(viewCtrl, viewport, opts, () => { - if (isTabSubPage) { - // add the .tab-subpage css class to tabs pages that should act like subpages - let pageEleRef = viewCtrl.pageRef(); - if (pageEleRef) { - this._renderer.setElementClass(pageEleRef.nativeElement, 'tab-subpage', true); - } - } done(); }); } diff --git a/src/components/tabs/tabs.md.scss b/src/components/tabs/tabs.md.scss index 1e2007b216..695e7c0cde 100644 --- a/src/components/tabs/tabs.md.scss +++ b/src/components/tabs/tabs.md.scss @@ -12,7 +12,8 @@ $tabbar-md-item-icon-size: 2.4rem !default; $tabbar-md-item-height: 4.8rem !default; $tab-button-md-active-color: $toolbar-md-active-color !default; -$tab-button-md-inactive-color: rgba($toolbar-md-inactive-color, .7) !default; +$tab-button-md-inactive-opacity: .7 !default; +$tab-button-md-inactive-color: rgba($toolbar-md-inactive-color, $tab-button-md-inactive-opacity) !default; ion-tabbar { @@ -103,7 +104,7 @@ tab-highlight { background-color: $color-base; .tab-button { - color: $color-contrast; + color: rgba($color-contrast, $tab-button-md-inactive-opacity); } .tab-button:hover:not(.disable-hover), diff --git a/src/components/tabs/tabs.ts b/src/components/tabs/tabs.ts index 50b129c124..dffb4b3542 100644 --- a/src/components/tabs/tabs.ts +++ b/src/components/tabs/tabs.ts @@ -7,9 +7,11 @@ import { Config } from '../../config/config'; import { Content } from '../content/content'; import { Icon } from '../icon/icon'; import { Ion } from '../ion'; -import { isBlank, isTrueProperty } from '../../util/util'; +import { isBlank, isPresent, isTrueProperty } from '../../util/util'; import { nativeRaf } from '../../util/dom'; -import { NavController, DIRECTION_FORWARD } from '../nav/nav-controller'; +import { NavController } from '../nav/nav-controller'; +import { NavControllerBase } from '../nav/nav-controller-base'; +import { NavOptions, DIRECTION_FORWARD } from '../nav/nav-interfaces'; import { Platform } from '../../platform/platform'; import { Tab } from './tab'; import { TabButton } from './tab-button'; @@ -148,7 +150,6 @@ import { ViewController } from '../nav/view-controller'; -
`, directives: [Badge, Icon, NgClass, NgFor, NgIf, TabButton, TabHighlight], encapsulation: ViewEncapsulation.None, @@ -165,18 +166,13 @@ export class Tabs extends Ion { /** * @private */ - id: number; + id: string; /** * @private */ selectHistory: string[] = []; - /** - * @private - */ - subPages: boolean; - /** * @input {number} The default selected tab index when first loaded. If a selected index isn't provided then it will use `0`, the first tab. */ @@ -225,12 +221,7 @@ export class Tabs extends Ion { /** * @private */ - @ViewChild('portal', {read: ViewContainerRef}) portal: ViewContainerRef; - - /** - * @private - */ - parent: NavController; + parent: NavControllerBase; constructor( @Optional() parent: NavController, @@ -243,16 +234,14 @@ export class Tabs extends Ion { ) { super(_elementRef); - this.parent = parent; - this.id = ++tabIds; + this.parent = parent; + this.id = 't' + (++tabIds); this._sbPadding = _config.getBoolean('statusbarPadding'); - this.subPages = _config.getBoolean('tabsHideOnSubPages'); this._useHighlight = _config.getBoolean('tabsHighlight'); // TODO deprecated 07-07-2016 beta.11 if (_config.get('tabSubPages') !== null) { - console.warn('Config option "tabSubPages" has been deprecated. Please use "tabsHideOnSubPages" instead.'); - this.subPages = _config.getBoolean('tabSubPages'); + console.warn('Config option "tabSubPages" has been deprecated. The Material Design spec now supports Bottom Navigation: https://material.google.com/components/bottom-navigation.html'); } // TODO deprecated 07-07-2016 beta.11 @@ -261,9 +250,9 @@ export class Tabs extends Ion { this._useHighlight = _config.getBoolean('tabbarHighlight'); } - if (parent) { + if (this.parent) { // this Tabs has a parent Nav - parent.registerChildNav(this); + this.parent.registerChildNav(this); } else if (this._app) { // this is the root navcontroller for the entire app @@ -333,41 +322,32 @@ export class Tabs extends Ion { * @private */ initTabs() { - // first check if preloadTab is set as an input @Input, then check the config - let preloadTabs = (isBlank(this.preloadTabs) ? this._config.getBoolean('preloadTabs') : isTrueProperty(this.preloadTabs)); + // get the selected index from the input + // otherwise default it to use the first index + let selectedIndex = (isBlank(this.selectedIndex) ? 0 : parseInt(this.selectedIndex, 10)); - // get the selected index - let selectedIndex = this.selectedIndex ? parseInt(this.selectedIndex, 10) : 0; - - // ensure the selectedIndex isn't a hidden or disabled tab - // also find the first available index incase we need it later - let availableIndex = -1; - this._tabs.forEach((tab, index) => { - if (tab.enabled && tab.show && availableIndex < 0) { - // we know this tab index is safe to show - availableIndex = index; - } - - if (index === selectedIndex && (!tab.enabled || !tab.show)) { - // the selectedIndex is not safe to show - selectedIndex = -1; - } - }); - - if (selectedIndex < 0) { - // the selected index wasn't safe to show - // instead use an available index found to be safe to show - selectedIndex = availableIndex; + // get the selectedIndex and ensure it isn't hidden or disabled + let selectedTab = this._tabs.find((t, i) => i === selectedIndex && t.enabled && t.show); + if (!selectedTab) { + // wasn't able to select the tab they wanted + // try to find the first tab that's available + selectedTab = this._tabs.find(t => t.enabled && t.show); } - this._tabs.forEach((tab, index) => { - if (index === selectedIndex) { - this.select(tab); + if (selectedTab) { + // we found a tab to select + this.select(selectedTab); + } - } else if (preloadTabs) { - tab.preload(1000 * index); - } - }); + // check if preloadTab is set as an input @Input + // otherwise check the preloadTabs config + let shouldPreloadTabs = (isBlank(this.preloadTabs) ? this._config.getBoolean('preloadTabs') : isTrueProperty(this.preloadTabs)); + if (shouldPreloadTabs) { + // preload all the tabs which isn't the selected tab + this._tabs.filter((t) => t !== selectedTab).forEach((tab, index) => { + tab.preload(this._config.getNumber('tabsPreloadDelay', 1000) * index); + }); + } } /** @@ -392,31 +372,33 @@ export class Tabs extends Ion { /** * @param {number|Tab} tabOrIndex Index, or the Tab instance, of the tab to select. */ - select(tabOrIndex: number | Tab) { + select(tabOrIndex: number | Tab, opts: NavOptions = {}, done?: Function): Promise { + let promise: Promise; + if (!done) { + promise = new Promise(res => { done = res; }); + } + let selectedTab: Tab = (typeof tabOrIndex === 'number' ? this.getByIndex(tabOrIndex) : tabOrIndex); if (isBlank(selectedTab)) { - return; + return Promise.resolve(); } let deselectedTab = this.getSelected(); - if (selectedTab === deselectedTab) { // no change - return this._touchActive(selectedTab); + this._touchActive(selectedTab); + return Promise.resolve(); } - console.debug(`Tabs, select: ${selectedTab.id}`); - let opts = { - animate: false - }; - let deselectedPage: ViewController; if (deselectedTab) { deselectedPage = deselectedTab.getActive(); deselectedPage && deselectedPage.fireWillLeave(); } + opts.animate = false; + let selectedPage = selectedTab.getActive(); selectedPage && selectedPage.fireWillEnter(); @@ -464,7 +446,11 @@ export class Tabs extends Ion { }); } } + + done(); }); + + return promise; } /** @@ -526,6 +512,13 @@ export class Tabs extends Ion { return this._tabs.indexOf(tab); } + /** + * @private + */ + length(): number { + return this._tabs.length; + } + /** * @private * "Touch" the active tab, going back to the root view of the tab @@ -561,20 +554,6 @@ export class Tabs extends Ion { return Promise.resolve(); } - /** - * @private - * Returns the root NavController. Returns `null` if Tabs is not - * within a NavController. - * @returns {NavController} - */ - get rootNav(): NavController { - let nav = this.parent; - while (nav && nav.parent) { - nav = nav.parent; - } - return nav; - } - /** * @private * DOM WRITE diff --git a/src/components/tabs/tabs.wp.scss b/src/components/tabs/tabs.wp.scss index c33a5aac3c..0cbc0a291a 100644 --- a/src/components/tabs/tabs.wp.scss +++ b/src/components/tabs/tabs.wp.scss @@ -12,7 +12,8 @@ $tabbar-wp-item-icon-size: 2.4rem !default; $tabbar-wp-item-height: 4.8rem !default; $tab-button-wp-active-color: $toolbar-wp-active-color !default; -$tab-button-wp-inactive-color: rgba($toolbar-wp-inactive-color, .7) !default; +$tab-button-wp-inactive-opacity: .7 !default; +$tab-button-wp-inactive-color: rgba($toolbar-wp-inactive-color, $tab-button-wp-inactive-opacity) !default; $tab-button-wp-background-activated: rgba(0, 0, 0, .1) !default; @@ -96,7 +97,7 @@ ion-tabbar { background-color: $color-base; .tab-button { - color: $color-contrast; + color: rgba($color-contrast, $tab-button-wp-inactive-opacity); } .tab-button:hover:not(.disable-hover), diff --git a/src/components/tabs/test/advanced/index.ts b/src/components/tabs/test/advanced/index.ts index 84a24078c8..072831de21 100644 --- a/src/components/tabs/test/advanced/index.ts +++ b/src/components/tabs/test/advanced/index.ts @@ -1,6 +1,6 @@ import { Component, ViewChild } from '@angular/core'; -import { ionicBootstrap, NavController, NavParams, ModalController, ViewController, Tabs, Tab } from '../../../../../src'; +import { App, ionicBootstrap, NavController, NavParams, ModalController, ViewController, Tabs, Tab } from '../../../../../src'; @Component({ @@ -92,7 +92,7 @@ class TabsPage { class Tab1Page1 { userId: string; - constructor(private nav: NavController, private tabs: Tabs, private params: NavParams) { + constructor(private nav: NavController, private app: App, private tabs: Tabs, private params: NavParams) { this.userId = params.get('userId'); } @@ -102,7 +102,7 @@ class Tab1Page1 { goBack() { console.log('go back begin'); - this.nav.pop().then((val) => { + this.nav.pop().then((val: any) => { console.log('go back completed', val); });; } @@ -112,7 +112,7 @@ class Tab1Page1 { } logout() { - this.nav.rootNav.setRoot(SignIn, null, { animate: true, direction: 'back' }); + this.app.getRootNav().setRoot(SignIn, null, { animate: true, direction: 'back' }); } ionViewWillEnter() { @@ -323,10 +323,8 @@ class Tab3Page1 { @Component({ - template: '' + template: '' }) -class E2EApp { - root = SignIn; -} +class E2EApp {} ionicBootstrap(E2EApp); diff --git a/src/components/tabs/test/basic/index.ts b/src/components/tabs/test/basic/index.ts index 03428939db..ded7adb258 100644 --- a/src/components/tabs/test/basic/index.ts +++ b/src/components/tabs/test/basic/index.ts @@ -1,5 +1,5 @@ -import {Component} from '@angular/core'; -import {ionicBootstrap, NavController, App, AlertController, ModalController, ViewController, Tab, Tabs} from '../../../../../src'; +import { Component} from '@angular/core'; +import { ionicBootstrap, App, AlertController, ModalController, ViewController, Tab, Tabs } from '../../../../../src'; // // Modal diff --git a/src/components/tabs/test/child-navs/index.ts b/src/components/tabs/test/child-navs/index.ts index d24c27f87a..ae9239bf0b 100644 --- a/src/components/tabs/test/child-navs/index.ts +++ b/src/components/tabs/test/child-navs/index.ts @@ -1,5 +1,5 @@ -import {Component} from '@angular/core'; -import {ionicBootstrap, NavController, App, Alert, Modal, ViewController, Tab, Tabs} from '../../../../../src'; +import { Component } from '@angular/core'; +import { ionicBootstrap, NavController, App, Alert, Modal, ViewController, Tab, Tabs } from '../../../../../src'; // diff --git a/src/components/tabs/test/tabs.spec.ts b/src/components/tabs/test/tabs.spec.ts index 025ed668fe..219bb84ca1 100644 --- a/src/components/tabs/test/tabs.spec.ts +++ b/src/components/tabs/test/tabs.spec.ts @@ -1,10 +1,89 @@ -import {Component} from '@angular/core'; -import {App, Nav, Tabs, Tab, NavOptions, Config, ViewController, Platform} from '../../../../src'; +import { Component } from '@angular/core'; +import { App, Config, Nav, NavOptions, Platform, Tab, Tabs, ViewController } from '../../../../src'; +import { mockTab, mockTabs } from '../../../../src/util/mock-providers'; export function run() { describe('Tabs', () => { + describe('initTabs', () => { + + it('should preload all tabs', () => { + var tabs = mockTabs(); + var tab0 = mockTab(tabs); + var tab1 = mockTab(tabs); + tab0.root = SomePage; + tab1.root = SomePage; + + tab0.preload = () => {}; + tab1.preload = () => {}; + + spyOn(tab0, 'preload'); + spyOn(tab1, 'preload'); + + tabs.preloadTabs = true; + + tabs.initTabs(); + + expect(tab0.isSelected).toEqual(true); + expect(tab1.isSelected).toEqual(false); + + expect(tab0.preload).not.toHaveBeenCalled(); + expect(tab1.preload).toHaveBeenCalled(); + }); + + it('should not select a hidden or disabled tab', () => { + var tabs = mockTabs(); + var tab0 = mockTab(tabs); + var tab1 = mockTab(tabs); + tab0.root = SomePage; + tab1.root = SomePage; + + tab1.enabled = false; + tab1.show = false; + + tabs.selectedIndex = '1'; + tabs.initTabs(); + + expect(tab0.isSelected).toEqual(true); + expect(tab1.isSelected).toEqual(false); + }); + + it('should select the second tab from selectedIndex input', () => { + var tabs = mockTabs(); + var tab0 = mockTab(tabs); + var tab1 = mockTab(tabs); + tab0.root = SomePage; + tab1.root = SomePage; + + tabs.selectedIndex = '1'; + tabs.initTabs(); + + expect(tab0.isSelected).toEqual(false); + expect(tab1.isSelected).toEqual(true); + }); + + it('should select the first tab by default', () => { + var tabs = mockTabs(); + var tab0 = mockTab(tabs); + var tab1 = mockTab(tabs); + tab0.root = SomePage; + tab1.root = SomePage; + + spyOn(tab0, 'preload'); + spyOn(tab1, 'preload'); + + tabs.initTabs(); + + expect(tab0.isSelected).toEqual(true); + expect(tab1.isSelected).toEqual(false); + + expect(tab0.preload).not.toHaveBeenCalled(); + expect(tab1.preload).not.toHaveBeenCalled(); + }); + + }); + describe('previousTab', () => { it('should find the previous tab when there has been 3 selections', () => { @@ -12,9 +91,6 @@ describe('Tabs', () => { var tab0 = mockTab(tabs); var tab1 = mockTab(tabs); var tab2 = mockTab(tabs); - tabs.add(tab0); - tabs.add(tab1); - tabs.add(tab2); tab0.root = SomePage; tab1.root = SomePage; tab2.root = SomePage; @@ -36,8 +112,6 @@ describe('Tabs', () => { var tabs = mockTabs(); var tab0 = mockTab(tabs); var tab1 = mockTab(tabs); - tabs.add(tab0); - tabs.add(tab1); tab0.root = SomePage; tab1.root = SomePage; @@ -56,8 +130,6 @@ describe('Tabs', () => { var tabs = mockTabs(); var tab0 = mockTab(tabs); var tab1 = mockTab(tabs); - tabs.add(tab0); - tabs.add(tab1); tab0.root = SomePage; tab1.root = SomePage; @@ -87,8 +159,6 @@ describe('Tabs', () => { var tabs = mockTabs(); var tab0 = mockTab(tabs); var tab1 = mockTab(tabs); - tabs.add(tab0); - tabs.add(tab1); tab0.root = SomePage; tab1.root = SomePage; @@ -103,12 +173,11 @@ describe('Tabs', () => { var tabs = mockTabs(); var tab0 = mockTab(tabs); var tab1 = mockTab(tabs); - tabs.add(tab0); - tabs.add(tab1); tab0.root = SomePage; tab1.root = SomePage; + expect(tabs.length()).toEqual(2); expect(tab0.isSelected).toBeUndefined(); expect(tab1.isSelected).toBeUndefined(); @@ -118,16 +187,6 @@ describe('Tabs', () => { expect(tab1.isSelected).toEqual(false); }); - it('should not select an invalid tab index', () => { - var tabs = mockTabs(); - var tab0 = mockTab(tabs); - var tab1 = mockTab(tabs); - tabs.add(tab0); - tabs.add(tab1); - - expect(tabs.select(22)).toBeUndefined(); - }); - }); describe('getByIndex', () => { @@ -137,8 +196,6 @@ describe('Tabs', () => { var tab0 = mockTab(tabs); tab0.setRoot({}); var tab1 = mockTab(tabs); - tabs.add(tab0); - tabs.add(tab1); expect(tabs.getIndex(tab0)).toEqual(0); expect(tabs.getIndex(tab1)).toEqual(1); @@ -152,8 +209,6 @@ describe('Tabs', () => { var tabs = mockTabs(); var tab0 = mockTab(tabs); var tab1 = mockTab(tabs); - tabs.add(tab0); - tabs.add(tab1); tab1.setSelected(true); @@ -164,48 +219,15 @@ describe('Tabs', () => { var tabs = mockTabs(); var tab0 = mockTab(tabs); var tab1 = mockTab(tabs); - tabs.add(tab0); - tabs.add(tab1); expect(tabs.getSelected()).toEqual(null); }); }); - var app: App; - var config: Config; - var platform: Platform; - var _cd: any; - - function mockNav(): Nav { - return new Nav(null, null, null, config, null, null, null, null, null); - } - - function mockTabs(): Tabs { - return new Tabs(null, null, null, config, null, null, null); - } - - function mockTab(parentTabs: Tabs): Tab { - var tab = new Tab(parentTabs, app, config, null, null, null, null, null, _cd); - tab.load = function(opts: any, cb: Function) { - cb(); - }; - return tab; - } - @Component({}) class SomePage {} - beforeEach(() => { - config = new Config(); - platform = new Platform(); - app = new App(config, platform); - _cd = { - reattach: function(){}, - detach: function(){} - }; - }); - }); diff --git a/src/components/toast/toast-component.ts b/src/components/toast/toast-component.ts index 34852212a4..150a4babad 100644 --- a/src/components/toast/toast-component.ts +++ b/src/components/toast/toast-component.ts @@ -4,7 +4,6 @@ import { NgIf } from '@angular/common'; import { Animation } from '../../animations/animation'; import { Config } from '../../config/config'; import { isPresent } from '../../util/util'; -import { NavController } from '../nav/nav-controller'; import { NavParams } from '../nav/nav-params'; import { Transition, TransitionOptions } from '../../transitions/transition'; import { ViewController } from '../nav/view-controller'; @@ -44,7 +43,6 @@ export class ToastCmp implements AfterViewInit { private id: number; constructor( - private _nav: NavController, private _viewCtrl: ViewController, private _config: Config, private _elementRef: ElementRef, diff --git a/src/components/toggle/toggle.ts b/src/components/toggle/toggle.ts index 90926cd57c..30ceff83ed 100644 --- a/src/components/toggle/toggle.ts +++ b/src/components/toggle/toggle.ts @@ -242,11 +242,12 @@ export class Toggle implements AfterContentInit, ControlValueAccessor, OnDestroy */ ngAfterContentInit() { this._init = true; - this._events.pointerEventsRef(this._elementRef, - (ev: any) => this.pointerDown(ev), - (ev: any) => this.pointerMove(ev), - (ev: any) => this.pointerUp(ev) - ); + this._events.pointerEvents({ + elementRef: this._elementRef, + pointerDown: this.pointerDown.bind(this), + pointerMove: this.pointerMove.bind(this), + pointerUp: this.pointerUp.bind(this) + }); } /** diff --git a/src/config/config.ts b/src/config/config.ts index e34b64464f..81f7d374ce 100644 --- a/src/config/config.ts +++ b/src/config/config.ts @@ -103,7 +103,6 @@ import {isObject, isDefined, isFunction, isArray} from '../util/util'; * | `tabsHighlight` | `boolean` | Whether to show a highlight line under the tab when it is selected. | * | `tabsLayout` | `string` | The layout to use for all tabs. Available options: `"icon-top"`, `"icon-left"`, `"icon-right"`, `"icon-bottom"`, `"icon-hide"`, `"title-hide"`. | * | `tabsPlacement` | `string` | The position of the tabs relative to the content. Available options: `"top"`, `"bottom"` | - * | `tabsHideOnSubPages` | `boolean` | Whether to hide the tabs on child pages or not. If `true` it will not show the tabs on child pages. | * | `toastEnter` | `string` | The name of the transition to use while a toast is presented. | * | `toastLeave` | `string` | The name of the transition to use while a toast is dismissed. | * diff --git a/src/config/modes.ts b/src/config/modes.ts index 4e61d2694e..3e0a631377 100644 --- a/src/config/modes.ts +++ b/src/config/modes.ts @@ -39,7 +39,6 @@ Config.setModeConfig('ios', { tabsHighlight: false, tabsPlacement: 'bottom', - tabsHideOnSubPages: false, toastEnter: 'toast-slide-in', toastLeave: 'toast-slide-out', @@ -82,8 +81,7 @@ Config.setModeConfig('md', { spinner: 'crescent', tabsHighlight: true, - tabsPlacement: 'top', - tabsHideOnSubPages: true, + tabsPlacement: 'bottom', toastEnter: 'toast-md-slide-in', toastLeave: 'toast-md-slide-out', @@ -127,7 +125,6 @@ Config.setModeConfig('wp', { tabsHighlight: false, tabsPlacement: 'top', - tabsHideOnSubPages: true, toastEnter: 'toast-wp-slide-in', toastLeave: 'toast-wp-slide-out', diff --git a/src/config/providers.ts b/src/config/providers.ts index 9ed2c31303..4b4ca71f24 100644 --- a/src/config/providers.ts +++ b/src/config/providers.ts @@ -10,6 +10,7 @@ import { closest, nativeTimeout } from '../util/dom'; import { Events } from '../util/events'; import { FeatureDetect } from '../util/feature-detect'; import { Form } from '../util/form'; +import { GestureController } from '../gestures/gesture-controller'; import { IONIC_DIRECTIVES } from './directives'; import { isPresent } from '../util/util'; import { Keyboard } from '../util/keyboard'; @@ -64,6 +65,7 @@ export function ionicProviders(customProviders?: Array, config?: any): any[ provide(Events, {useValue: events}), provide(FeatureDetect, {useValue: featureDetect}), Form, + GestureController, HTTP_PROVIDERS, Keyboard, LoadingController, diff --git a/src/config/test/config.spec.ts b/src/config/test/config.spec.ts index 3e75db43f2..b734ad2ec2 100644 --- a/src/config/test/config.spec.ts +++ b/src/config/test/config.spec.ts @@ -2,12 +2,14 @@ import {Config, Platform, ionicProviders} from '../../../src'; export function run() { +describe('Config', () => { + it('should set activator setting to none for old Android Browser on a linux device', () => { let config = new Config(); let platform = new Platform(); platform.setUserAgent('Mozilla/5.0 (Linux; U; Android 4.2.2; nl-nl; GT-I9505 Build/JDQ39) AppleWebKit/534.30 (KHTML, like Gecko) Version/4.0 Mobile Safari/534.30'); platform.setNavigatorPlatform('linux'); - platform.load(null); + platform.load(); config.setPlatform(platform); expect(config.get('activator')).toEqual('none'); @@ -18,7 +20,7 @@ export function run() { let platform = new Platform(); platform.setUserAgent('Mozilla/5.0 (Linux; U; Android 4.2.2; nl-nl; GT-I9505 Build/JDQ39) AppleWebKit/534.30 (KHTML, like Gecko) Version/4.0 Mobile Safari/534.30'); platform.setNavigatorPlatform('MacIntel'); - platform.load(null); + platform.load(); config.setPlatform(platform); expect(config.get('activator')).toEqual('ripple'); @@ -29,7 +31,7 @@ export function run() { let platform = new Platform(); platform.setUserAgent('Mozilla/5.0 (Linux; Android 4.2.2; GT-I9505 Build/JDQ39) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/35.0.1650.59 Mobile Safari/537.36'); platform.setNavigatorPlatform('linux'); - platform.load(null); + platform.load(); config.setPlatform(platform); expect(config.get('activator')).toEqual('none'); @@ -40,7 +42,7 @@ export function run() { let platform = new Platform(); platform.setUserAgent('Mozilla/5.0 (Linux; Android 4.2.2; GT-I9505 Build/JDQ39) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/36.0.1650.59 Mobile Safari/537.36'); platform.setNavigatorPlatform('linux'); - platform.load(null); + platform.load(); config.setPlatform(platform); expect(config.get('activator')).toEqual('ripple'); @@ -51,7 +53,7 @@ export function run() { let platform = new Platform(); platform.setUserAgent('Mozilla/5.0 (Android 5.0; Mobile; rv:41.0) Gecko/41.0 Firefox/41.0'); platform.setNavigatorPlatform('linux'); - platform.load(null); + platform.load(); config.setPlatform(platform); expect(config.get('activator')).toEqual('ripple'); @@ -62,7 +64,7 @@ export function run() { let platform = new Platform(); platform.setUserAgent('Mozilla/5.0 (Android 4.4; Mobile; rv:41.0) Gecko/41.0 Firefox/41.0'); platform.setNavigatorPlatform('linux'); - platform.load(null); + platform.load(); config.setPlatform(platform); expect(config.get('activator')).toEqual('none'); @@ -105,7 +107,7 @@ export function run() { config.setPlatform(platform); expect(config.get('mode')).toEqual('md'); - expect(config.get('tabsPlacement')).toEqual('top'); + expect(config.get('tabsHighlight')).toEqual(true); }); it('should override mode settings from platforms setting', () => { @@ -120,7 +122,7 @@ export function run() { config.setPlatform(platform); expect(config.get('mode')).toEqual('md'); - expect(config.get('tabsPlacement')).toEqual('top'); + expect(config.get('tabsHighlight')).toEqual(true); }); it('should get boolean value from querystring', () => { @@ -259,7 +261,7 @@ export function run() { let platform = new Platform(['android']); config.setPlatform(platform); - expect(config.get('tabsPlacement')).toEqual('top'); + expect(config.get('tabsHighlight')).toEqual(true); }); it('should get setting from ios mode', () => { @@ -562,4 +564,6 @@ export function run() { expect(config.settings()).toEqual({}); }); +}); + } diff --git a/src/decorators/page.ts b/src/decorators/page.ts index 6e84f7b6f4..41a36a6813 100644 --- a/src/decorators/page.ts +++ b/src/decorators/page.ts @@ -38,7 +38,6 @@ export function Page(config: PageMetadata) { config.selector = 'ion-page'; config.host = config.host || {}; config.host['[hidden]'] = '_hidden'; - config.host['[class.tab-subpage]'] = '_tabSubPage'; var annotations = _reflect.getMetadata('annotations', cls) || []; annotations.push(new Component(config)); _reflect.defineMetadata('annotations', annotations, cls); diff --git a/src/gestures/drag-gesture.ts b/src/gestures/drag-gesture.ts index d94d5b18d0..f1ea44d07c 100644 --- a/src/gestures/drag-gesture.ts +++ b/src/gestures/drag-gesture.ts @@ -1,42 +1,142 @@ -import {Gesture} from './gesture'; -import {defaults} from '../util'; + +import { defaults } from '../util'; +import { GestureDelegate } from '../gestures/gesture-controller'; +import { PointerEvents, UIEventManager } from '../util/ui-event-manager'; +import { PanRecognizer } from './recognizers'; +import { pointerCoord, Coordinates } from '../util/dom'; /** * @private */ +export interface PanGestureConfig { + threshold?: number; + maxAngle?: number; + direction?: 'x' | 'y'; + gesture?: GestureDelegate; +} -export class DragGesture extends Gesture { - public dragging: boolean; +/** + * @private + */ +export class PanGesture { + private dragging: boolean; + private events: UIEventManager = new UIEventManager(false); + private pointerEvents: PointerEvents; + private detector: PanRecognizer; + private started: boolean = false; + private captured: boolean = false; + public isListening: boolean = false; + protected gestute: GestureDelegate; + protected direction: string; - constructor(element: HTMLElement, opts = {}) { - defaults(opts, {}); - super(element, opts); + constructor(private element: HTMLElement, opts: PanGestureConfig = {}) { + defaults(opts, { + threshold: 20, + maxAngle: 40, + direction: 'x' + }); + this.gestute = opts.gesture; + this.direction = opts.direction; + this.detector = new PanRecognizer(opts.direction, opts.threshold, opts.maxAngle); } listen() { - super.listen(); - - this.on('panstart', (ev: UIEvent) => { - if (this.onDragStart(ev) !== false) { - this.dragging = true; - } - }); - - this.on('panmove', (ev: UIEvent) => { - if (!this.dragging) return; - if (this.onDrag(ev) === false) { - this.dragging = false; - } - }); - - this.on('panend', (ev: UIEvent) => { - if (!this.dragging) return; - this.onDragEnd(ev); - this.dragging = false; - }); + if (!this.isListening) { + this.pointerEvents = this.events.pointerEvents({ + element: this.element, + pointerDown: this.pointerDown.bind(this), + pointerMove: this.pointerMove.bind(this), + pointerUp: this.pointerUp.bind(this), + }); + this.isListening = true; + } } - onDrag(ev: any): boolean { return true; } - onDragStart(ev: any): boolean { return true; } - onDragEnd(ev: any): void {} + unlisten() { + this.gestute && this.gestute.release(); + this.events.unlistenAll(); + this.isListening = false; + } + + destroy() { + this.gestute && this.gestute.destroy(); + this.unlisten(); + this.element = null; + } + + pointerDown(ev: any): boolean { + if (this.started) { + return; + } + if (!this.canStart(ev)) { + return false; + } + if (this.gestute) { + // Release fallback + this.gestute.release(); + // Start gesture + if (!this.gestute.start()) { + return false; + } + } + + let coord = pointerCoord(ev); + this.detector.start(coord); + this.started = true; + this.captured = false; + return true; + } + + pointerMove(ev: any) { + if (!this.started) { + return; + } + if (this.captured) { + this.onDragMove(ev); + return; + } + let coord = pointerCoord(ev); + if (this.detector.detect(coord)) { + + if (this.detector.pan() !== 0 && this.canCapture(ev) && + (!this.gestute || this.gestute.capture())) { + this.onDragStart(ev); + this.captured = true; + return; + } + + // Detection/capturing was not successful, aborting! + this.started = false; + this.captured = false; + this.pointerEvents.stop(); + this.notCaptured(ev); + } + } + + pointerUp(ev: any) { + if (!this.started) { + return; + } + this.gestute && this.gestute.release(); + + if (this.captured) { + this.onDragEnd(ev); + } else { + this.notCaptured(ev); + } + this.captured = false; + this.started = false; + } + + getNativeElement(): HTMLElement { + return this.element; + } + + // Implemented in a subclass + canStart(ev: any): boolean { return true; } + canCapture(ev: any): boolean { return true; } + onDragStart(ev: any) { } + onDragMove(ev: any) { } + onDragEnd(ev: any) { } + notCaptured(ev: any) { } } diff --git a/src/gestures/gesture-controller.ts b/src/gestures/gesture-controller.ts new file mode 100644 index 0000000000..f768dc252e --- /dev/null +++ b/src/gestures/gesture-controller.ts @@ -0,0 +1,226 @@ + +import { forwardRef, Inject, Injectable } from '@angular/core'; +import { App } from '../components/app/app'; + +export const enum GesturePriority { + Minimun = -10000, + VeryLow = -20, + Low = -10, + Normal = 0, + High = 10, + VeryHigh = 20, + + SlidingItem = Low, + MenuSwipe = High, + GoBackSwipe = VeryHigh, + Refresher = Normal, +} + +export const enum DisableScroll { + Never, + DuringCapture, + Always, +} + +export interface GestureOptions { + disable?: string[]; + disableScroll?: DisableScroll; + priority?: number; +} + +@Injectable() +export class GestureController { + private id: number = 1; + private requestedStart: { [eventId: number]: number } = {}; + private disabledGestures: { [eventName: string]: Set } = {}; + private disabledScroll: Set = new Set(); + private capturedID: number = null; + + constructor(@Inject(forwardRef(() => App)) private _app: App) { } + + create(name: string, opts: GestureOptions = {}): GestureDelegate { + return new GestureDelegate(name, this.newID(), this, opts); + } + + newID(): number { + let id = this.id; this.id++; + return id; + } + + start(gestureName: string, id: number, priority: number): boolean { + if (!this.canStart(gestureName)) { + delete this.requestedStart[id]; + return false; + } + + this.requestedStart[id] = priority; + return true; + } + + capture(gestureName: string, id: number, priority: number): boolean { + if (!this.start(gestureName, id, priority)) { + return false; + } + let requestedStart = this.requestedStart; + let maxPriority = GesturePriority.Minimun; + for (let gestureID in requestedStart) { + maxPriority = Math.max(maxPriority, requestedStart[gestureID]); + } + + if (maxPriority === priority) { + this.capturedID = id; + this.requestedStart = {}; + return true; + } + delete requestedStart[id]; + console.debug(`${gestureName} can not start because it is has lower priority`); + return false; + } + + release(id: number) { + delete this.requestedStart[id]; + if (this.capturedID && id === this.capturedID) { + this.capturedID = null; + } + } + + disableGesture(gestureName: string, id: number) { + let set = this.disabledGestures[gestureName]; + if (!set) { + set = new Set(); + this.disabledGestures[gestureName] = set; + } + set.add(id); + } + + enableGesture(gestureName: string, id: number) { + let set = this.disabledGestures[gestureName]; + if (set) { + set.delete(id); + } + } + + disableScroll(id: number) { + let isEnabled = !this.isScrollDisabled(); + this.disabledScroll.add(id); + if (this._app && isEnabled && this.isScrollDisabled()) { + console.debug('GestureController: Disabling scrolling'); + this._app.setScrollDisabled(true); + } + } + + enableScroll(id: number) { + let isDisabled = this.isScrollDisabled(); + this.disabledScroll.delete(id); + if (this._app && isDisabled && !this.isScrollDisabled()) { + console.debug('GestureController: Enabling scrolling'); + this._app.setScrollDisabled(false); + } + } + + canStart(gestureName: string): boolean { + if (this.capturedID) { + // a gesture already captured + return false; + } + + if (this.isDisabled(gestureName)) { + return false; + } + return true; + } + + isCaptured(): boolean { + return !!this.capturedID; + } + + isScrollDisabled(): boolean { + return this.disabledScroll.size > 0; + } + + isDisabled(gestureName: string): boolean { + let disabled = this.disabledGestures[gestureName]; + if (disabled && disabled.size > 0) { + return true; + } + return false; + } + +} + +export class GestureDelegate { + private disable: string[]; + private disableScroll: DisableScroll; + public priority: number = 0; + + constructor( + private name: string, + private id: number, + private controller: GestureController, + opts: GestureOptions + ) { + this.disable = opts.disable || []; + this.disableScroll = opts.disableScroll || DisableScroll.Never; + this.priority = opts.priority || 0; + + // Disable gestures + for (let gestureName of this.disable) { + controller.disableGesture(gestureName, id); + } + + // Disable scrolling (always) + if (this.disableScroll === DisableScroll.Always) { + controller.disableScroll(id); + } + } + + canStart(): boolean { + if (!this.controller) { + return false; + } + return this.controller.canStart(this.name); + } + + start(): boolean { + if (!this.controller) { + return false; + } + return this.controller.start(this.name, this.id, this.priority); + } + + capture(): boolean { + if (!this.controller) { + return false; + } + let captured = this.controller.capture(this.name, this.id, this.priority); + if (captured && this.disableScroll === DisableScroll.DuringCapture) { + this.controller.disableScroll(this.id); + } + return captured; + } + + release() { + if (!this.controller) { + return; + } + this.controller.release(this.id); + if (this.disableScroll === DisableScroll.DuringCapture) { + this.controller.enableScroll(this.id); + } + } + + destroy() { + if (!this.controller) { + return; + } + this.release(); + + for (let disabled of this.disable) { + this.controller.enableGesture(disabled, this.id); + } + if (this.disableScroll === DisableScroll.Always) { + this.controller.enableScroll(this.id); + } + this.controller = null; + } +} \ No newline at end of file diff --git a/src/gestures/recognizers.ts b/src/gestures/recognizers.ts new file mode 100644 index 0000000000..ad63faaebe --- /dev/null +++ b/src/gestures/recognizers.ts @@ -0,0 +1,58 @@ +import { pointerCoord, Coordinates } from '../util/dom'; + +export class PanRecognizer { + private startCoord: Coordinates; + private dirty: boolean = false; + private threshold: number; + private maxCosine: number; + private _angle: any = 0; + private _isPan: number = 0; + + constructor(private direction: string, threshold: number, maxAngle: number) { + let radians = maxAngle * (Math.PI / 180); + this.maxCosine = Math.cos(radians); + this.threshold = threshold * threshold; + } + + start(coord: Coordinates) { + this.startCoord = coord; + this._angle = 0; + this._isPan = 0; + this.dirty = true; + } + + detect(coord: Coordinates): boolean { + if (!this.dirty) { + return false; + } + let deltaX = (coord.x - this.startCoord.x); + let deltaY = (coord.y - this.startCoord.y); + let distance = deltaX * deltaX + deltaY * deltaY; + if (distance >= this.threshold) { + let angle = Math.atan2(deltaY, deltaX); + let cosine = (this.direction === 'y') + ? Math.sin(angle) + : Math.cos(angle); + + this._angle = angle; + if (cosine > this.maxCosine) { + this._isPan = 1; + } else if (cosine < -this.maxCosine) { + this._isPan = -1; + } else { + this._isPan = 0; + } + this.dirty = false; + return true; + } + return false; + } + + angle(): any { + return this._angle; + } + + pan(): number { + return this._isPan; + } +} diff --git a/src/gestures/simulator.ts b/src/gestures/simulator.ts new file mode 100644 index 0000000000..dc304bcf20 --- /dev/null +++ b/src/gestures/simulator.ts @@ -0,0 +1,163 @@ +import { pointerCoord, Coordinates } from '../util/dom'; + +interface Point { + coord: Coordinates; + duration: number; +} + +export class Simulate { + private index: number = 0; + private points: Point[] = []; + public timedelta: number = 1 / 60; + + public static from(x: any, y?: number): Simulate { + let s = new Simulate(); + return s.start(x, y); + } + + reset(): Simulate { + this.index = 0; + return this; + } + + start(x: any, y?: number): Simulate { + this.points = []; + return this.to(x, y); + } + + to(x: any, y?: number): Simulate { + this.newPoint(parseCoordinates(x, y), 1); + return this; + } + + delta(x: any, y?: number): Simulate { + let newPoint = parseCoordinates(x, y); + let prevCoord = this.getLastPoint().coord; + newPoint.x += prevCoord.x; + newPoint.y += prevCoord.y; + + this.newPoint(newPoint, 1); + return this; + } + + deltaPolar(angle: number, distance: number): Simulate { + angle *= Math.PI / 180; + let prevCoord = this.getLastPoint().coord; + let coord = { + x: prevCoord.x + (Math.cos(angle) * distance), + y: prevCoord.y + (Math.sin(angle) * distance) + }; + this.newPoint(coord, 1); + return this; + } + + toPolar(angle: number, distance: number): Simulate { + angle *= Math.PI / 180; + let coord = { + x: Math.cos(angle) * distance, + y: Math.sin(angle) * distance + }; + this.newPoint(coord, 1); + return this; + } + + duration(duration: number): Simulate { + this.getLastPoint().duration = duration; + return this; + } + + velocity(vel: number): Simulate { + let p1 = this.getLastPoint(); + let p2 = this.getPreviousPoint(); + let d = distance(p1, p2); + return this.duration(d / vel); + } + + swipeRight(maxAngle: number, distance: number): Simulate { + // x------> + let angle = randomAngle(maxAngle); + return this.deltaPolar(angle, distance); + } + + swipeLeft(maxAngle: number, distance: number): Simulate { + // <------x + let angle = randomAngle(maxAngle) + 180; + return this.deltaPolar(angle, distance); + } + + swipeTop(maxAngle: number, distance: number): Simulate { + let angle = randomAngle(maxAngle) + 90; + return this.deltaPolar(angle, distance); + } + + swipeBottom(maxAngle: number, distance: number): Simulate { + let angle = randomAngle(maxAngle) - 90; + return this.deltaPolar(angle, distance); + } + + run(callback: Function) { + let points = this.points; + let len = points.length - 1; + let i = 0; + for (; i < len; i++) { + var p1 = points[i].coord; + var p2 = points[i + 1].coord; + var duration = points[i + 1].duration; + var vectorX = p2.x - p1.x; + var vectorY = p2.y - p1.y; + var nuSteps = Math.ceil(duration / this.timedelta); + vectorX /= nuSteps; + vectorY /= nuSteps; + for (let j = 0; j < nuSteps; j++) { + callback({ + x: p1.x + vectorX * j, + y: p1.y + vectorY * j + }); + } + } + this.index = i; + + return this; + } + + + private newPoint(coord: Coordinates, duration: number) { + this.points.push({ + coord: coord, + duration: duration, + }); + } + + private getLastPoint(): Point { + let len = this.points.length; + if (len > 0) { + return this.points[len - 1]; + } + throw new Error('can not call point'); + } + + private getPreviousPoint(): Point { + let len = this.points.length; + if (len > 1) { + return this.points[len - 2]; + } + throw new Error('can not call point'); + } +} + +function randomAngle(maxAngle: number): number { + return (Math.random() * maxAngle * 2) - maxAngle; +} + +function distance(a: Coordinates, b: Coordinates): number { + let deltaX = a.x - b.x; + let deltaY = a.y - a.y; + return Math.hypot(deltaX, deltaY); +} + +function parseCoordinates(coord: Coordinates | number, y?: number): Coordinates { + if (typeof coord === 'number') { + return { x: coord, y: y }; + } + return coord; +} \ No newline at end of file diff --git a/src/gestures/slide-edge-gesture.ts b/src/gestures/slide-edge-gesture.ts index 74b5d18567..d40425f06e 100644 --- a/src/gestures/slide-edge-gesture.ts +++ b/src/gestures/slide-edge-gesture.ts @@ -1,6 +1,6 @@ -import {SlideGesture} from './slide-gesture'; -import {defaults} from '../util/util'; -import {windowDimensions} from '../util/dom'; +import { SlideGesture } from './slide-gesture'; +import { defaults } from '../util/util'; +import { pointerCoord, windowDimensions } from '../util/dom'; /** * @private @@ -22,8 +22,9 @@ export class SlideEdgeGesture extends SlideGesture { } canStart(ev: any): boolean { + let coord = pointerCoord(ev); this._d = this.getContainerDimensions(); - return this.edges.every(edge => this._checkEdge(edge, ev.center)); + return this.edges.every(edge => this._checkEdge(edge, coord)); } getContainerDimensions() { diff --git a/src/gestures/slide-gesture.ts b/src/gestures/slide-gesture.ts index dca0d0f19f..421ed1e050 100644 --- a/src/gestures/slide-gesture.ts +++ b/src/gestures/slide-gesture.ts @@ -1,16 +1,15 @@ -import {DragGesture} from './drag-gesture'; -import {clamp} from '../util'; - +import { PanGesture } from './drag-gesture'; +import { clamp } from '../util'; +import { pointerCoord } from '../util/dom'; /** * @private */ -export class SlideGesture extends DragGesture { +export class SlideGesture extends PanGesture { public slide: SlideData = null; constructor(element: HTMLElement, opts = {}) { super(element, opts); - this.element = element; } /* @@ -20,7 +19,7 @@ export class SlideGesture extends DragGesture { getSlideBoundaries(slide: SlideData, ev: any) { return { min: 0, - max: this.element.offsetWidth + max: this.getNativeElement().offsetWidth }; } @@ -33,48 +32,43 @@ export class SlideGesture extends DragGesture { return 0; } - canStart(ev: any): boolean { - return true; - } - - onDragStart(ev: any): boolean { - if (!this.canStart(ev)) { - return false; - } - + onDragStart(ev: any) { this.slide = {}; this.onSlideBeforeStart(this.slide, ev); - var {min, max} = this.getSlideBoundaries(this.slide, ev); + let {min, max} = this.getSlideBoundaries(this.slide, ev); + let coord = pointerCoord(ev); this.slide.min = min; this.slide.max = max; this.slide.elementStartPos = this.getElementStartPos(this.slide, ev); - this.slide.pointerStartPos = ev.center[this.direction]; + this.slide.pos = this.slide.pointerStartPos = coord[this.direction]; + this.slide.timestamp = Date.now(); this.slide.started = true; + this.slide.velocity = 0; this.onSlideStart(this.slide, ev); - - return true; } - onDrag(ev: any): boolean { - if (!this.slide || !this.slide.started) { - return false; - } + onDragMove(ev: any) { + let coord = pointerCoord(ev); + let newPos = coord[this.direction]; + let newTimestamp = Date.now(); + let velocity = (newPos - this.slide.pos) / (newTimestamp - this.slide.timestamp); - this.slide.pos = ev.center[this.direction]; + this.slide.pos = newPos; + this.slide.timestamp = newTimestamp; this.slide.distance = clamp( this.slide.min, - this.slide.pos - this.slide.pointerStartPos + this.slide.elementStartPos, + newPos - this.slide.pointerStartPos + this.slide.elementStartPos, this.slide.max ); - this.slide.delta = this.slide.pos - this.slide.pointerStartPos; + this.slide.velocity = velocity; + this.slide.delta = newPos - this.slide.pointerStartPos; this.onSlide(this.slide, ev); return true; } onDragEnd(ev: any) { - if (!this.slide || !this.slide.started) return; this.onSlideEnd(this.slide, ev); this.slide = null; } @@ -85,6 +79,9 @@ export class SlideGesture extends DragGesture { onSlideEnd(slide?: SlideData, ev?: any): void {} } +/** + * @private + */ export interface SlideData { min?: number; max?: number; @@ -92,6 +89,8 @@ export interface SlideData { delta?: number; started?: boolean; pos?: any; + timestamp?: number; pointerStartPos?: number; elementStartPos?: number; + velocity?: number; } diff --git a/src/gestures/test/gesture-controller.spec.ts b/src/gestures/test/gesture-controller.spec.ts new file mode 100644 index 0000000000..b930d1a95b --- /dev/null +++ b/src/gestures/test/gesture-controller.spec.ts @@ -0,0 +1,314 @@ +import { GestureController, DisableScroll } from '../../../src'; + +export function run() { + + it('should create an instance of GestureController', () => { + let c = new GestureController(null); + expect(c.isCaptured()).toEqual(false); + expect(c.isScrollDisabled()).toEqual(false); + }); + + it('should test scrolling enable/disable stack', () => { + let c = new GestureController(null); + c.enableScroll(1); + expect(c.isScrollDisabled()).toEqual(false); + + c.disableScroll(1); + expect(c.isScrollDisabled()).toEqual(true); + c.disableScroll(1); + c.disableScroll(1); + expect(c.isScrollDisabled()).toEqual(true); + + c.enableScroll(1); + expect(c.isScrollDisabled()).toEqual(false); + + for (var i = 0; i < 100; i++) { + for (var j = 0; j < 100; j++) { + c.disableScroll(j); + } + } + + for (var i = 0; i < 100; i++) { + expect(c.isScrollDisabled()).toEqual(true); + c.enableScroll(50 - i); + c.enableScroll(i); + } + expect(c.isScrollDisabled()).toEqual(false); + }); + + it('should test gesture enable/disable stack', () => { + let c = new GestureController(null); + c.enableGesture('swipe', 1); + expect(c.isDisabled('swipe')).toEqual(false); + + c.disableGesture('swipe', 1); + expect(c.isDisabled('swipe')).toEqual(true); + c.disableGesture('swipe', 1); + c.disableGesture('swipe', 1); + expect(c.isDisabled('swipe')).toEqual(true); + + c.enableGesture('swipe', 1); + expect(c.isDisabled('swipe')).toEqual(false); + + // Disabling gestures multiple times + for (var gestureName = 0; gestureName < 10; gestureName++) { + for (var i = 0; i < 50; i++) { + for (var j = 0; j < 50; j++) { + c.disableGesture(gestureName.toString(), j); + } + } + } + + for (var gestureName = 0; gestureName < 10; gestureName++) { + for (var i = 0; i < 49; i++) { + c.enableGesture(gestureName.toString(), i); + } + expect(c.isDisabled(gestureName.toString())).toEqual(true); + c.enableGesture(gestureName.toString(), 49); + expect(c.isDisabled(gestureName.toString())).toEqual(false); + } + }); + + + it('should test if canStart', () => { + let c = new GestureController(null); + expect(c.canStart('event')).toEqual(true); + expect(c.canStart('event1')).toEqual(true); + expect(c.canStart('event')).toEqual(true); + expect(c['requestedStart']).toEqual({}); + expect(c.isCaptured()).toEqual(false); + }); + + + + it('should initialize a delegate without options', () => { + let c = new GestureController(null); + let g = c.create('event'); + expect(g['name']).toEqual('event'); + expect(g.priority).toEqual(0); + expect(g['disable']).toEqual([]); + expect(g['disableScroll']).toEqual(DisableScroll.Never); + expect(g['controller']).toEqual(c); + expect(g['id']).toEqual(1); + + let g2 = c.create('event2'); + expect(g2['id']).toEqual(2); + }); + + + it('should initialize a delegate with options', () => { + let c = new GestureController(null); + let g = c.create('swipe', { + priority: -123, + disableScroll: DisableScroll.DuringCapture, + disable: ['event2'] + }); + expect(g['name']).toEqual('swipe'); + expect(g.priority).toEqual(-123); + expect(g['disable']).toEqual(['event2']); + expect(g['disableScroll']).toEqual(DisableScroll.DuringCapture); + expect(g['controller']).toEqual(c); + expect(g['id']).toEqual(1); + }); + + it('should test if several gestures can be started', () => { + let c = new GestureController(null); + let g1 = c.create('swipe'); + let g2 = c.create('swipe1', {priority: 3}); + let g3 = c.create('swipe2', {priority: 4}); + + for (var i = 0; i < 10; i++) { + expect(g1.start()).toEqual(true); + expect(g2.start()).toEqual(true); + expect(g3.start()).toEqual(true); + } + expect(c['requestedStart']).toEqual({ + 1: 0, + 2: 3, + 3: 4 + }); + + g1.release(); + g1.release(); + + expect(c['requestedStart']).toEqual({ + 2: 3, + 3: 4 + }); + expect(g1.start()).toEqual(true); + expect(g2.start()).toEqual(true); + g3.destroy(); + + expect(c['requestedStart']).toEqual({ + 1: 0, + 2: 3, + }); + }); + + + it('should test if several gestures try to capture at the same time', () => { + let c = new GestureController(null); + let g1 = c.create('swipe1'); + let g2 = c.create('swipe2', { priority: 2 }); + let g3 = c.create('swipe3', { priority: 3 }); + let g4 = c.create('swipe4', { priority: 4 }); + let g5 = c.create('swipe5', { priority: 5 }); + + // Low priority capture() returns false + expect(g2.start()).toEqual(true); + expect(g3.start()).toEqual(true); + expect(g1.capture()).toEqual(false); + expect(c['requestedStart']).toEqual({ + 2: 2, + 3: 3 + }); + + // Low priority start() + capture() returns false + expect(g2.capture()).toEqual(false); + expect(c['requestedStart']).toEqual({ + 3: 3 + }); + + // Higher priority capture() return true + expect(g4.capture()).toEqual(true); + expect(c.isScrollDisabled()).toEqual(false); + expect(c.isCaptured()).toEqual(true); + expect(c['requestedStart']).toEqual({}); + + // Higher priority can not capture because it is already capture + expect(g5.capture()).toEqual(false); + expect(g5.canStart()).toEqual(false); + expect(g5.start()).toEqual(false); + expect(c['requestedStart']).toEqual({}); + + // Only captured gesture can release + g1.release(); + g2.release(); + g3.release(); + g5.release(); + expect(c.isCaptured()).toEqual(true); + + // G4 releases + g4.release(); + expect(c.isCaptured()).toEqual(false); + + // Once it was release, any gesture can capture + expect(g1.start()).toEqual(true); + expect(g1.capture()).toEqual(true); + }); + + + it('should destroy correctly', () => { + let c = new GestureController(null); + let g = c.create('swipe', { + priority: 123, + disableScroll: DisableScroll.Always, + disable: ['event2'] + }); + expect(c.isScrollDisabled()).toEqual(true); + + // Capturing + expect(g.capture()).toEqual(true); + expect(c.isCaptured()).toEqual(true); + expect(g.capture()).toEqual(false); + expect(c.isScrollDisabled()).toEqual(true); + + // Releasing + g.release(); + expect(c.isCaptured()).toEqual(false); + expect(c.isScrollDisabled()).toEqual(true); + expect(g.capture()).toEqual(true); + expect(c.isCaptured()).toEqual(true); + + // Destroying + g.destroy(); + expect(c.isCaptured()).toEqual(false); + expect(g['controller']).toBeNull(); + + // it should return false and not crash + expect(g.start()).toEqual(false); + expect(g.capture()).toEqual(false); + g.release(); + }); + + + it('should disable some events', () => { + let c = new GestureController(null); + + let goback = c.create('goback'); + expect(goback.canStart()).toEqual(true); + + let g2 = c.create('goback2'); + expect(g2.canStart()).toEqual(true); + + let g3 = c.create('swipe', { + disable: ['range', 'goback', 'something'] + }); + + let g4 = c.create('swipe2', { + disable: ['range'] + }); + + // it should be noop + g3.release(); + + // goback is disabled + expect(c.isDisabled('range')).toEqual(true); + expect(c.isDisabled('goback')).toEqual(true); + expect(c.isDisabled('something')).toEqual(true); + expect(c.isDisabled('goback2')).toEqual(false); + expect(goback.canStart()).toEqual(false); + expect(goback.start()).toEqual(false); + expect(goback.capture()).toEqual(false); + expect(g3.canStart()).toEqual(true); + + // Once g3 is destroyed, goback and something should be enabled + g3.destroy(); + expect(c.isDisabled('range')).toEqual(true); + expect(c.isDisabled('goback')).toEqual(false); + expect(c.isDisabled('something')).toEqual(false); + expect(g3.canStart()).toEqual(false); + + // Once g4 is destroyed, range is also enabled + g4.destroy(); + expect(c.isDisabled('range')).toEqual(false); + expect(g4.canStart()).toEqual(false); + }); + + it('should disable scrolling on capture', () => { + let c = new GestureController(null); + let g = c.create('goback', { + disableScroll: DisableScroll.DuringCapture, + }); + let g1 = c.create('swipe'); + + g.start(); + expect(c.isScrollDisabled()).toEqual(false); + + g1.capture(); + g.capture(); + expect(c.isScrollDisabled()).toEqual(false); + + g1.release(); + expect(c.isScrollDisabled()).toEqual(false); + + g.capture(); + expect(c.isScrollDisabled()).toEqual(true); + + let g2 = c.create('swipe2', { + disableScroll: DisableScroll.Always, + }); + g.release(); + expect(c.isScrollDisabled()).toEqual(true); + + g2.destroy(); + expect(c.isScrollDisabled()).toEqual(false); + + g.capture(); + expect(c.isScrollDisabled()).toEqual(true); + + g.destroy(); + expect(c.isScrollDisabled()).toEqual(false); + }); + +} diff --git a/src/gestures/test/recognizers.spec.ts b/src/gestures/test/recognizers.spec.ts new file mode 100644 index 0000000000..cd3a47bcd6 --- /dev/null +++ b/src/gestures/test/recognizers.spec.ts @@ -0,0 +1,206 @@ + +import { PanRecognizer } from '../../../src/gestures/recognizers'; +import { Simulate } from '../../../src/gestures/simulator'; + +export function run() { + + it('should not fire if it did not start', () => { + let p = new PanRecognizer('x', 2, 2); + expect(p.pan()).toEqual(0); + + Simulate.from(0, 0).to(99, 0).run((coord: Coordinates) => { + expect(p.detect(coord)).toEqual(false); + }); + }); + + it('should reset', () => { + let p = new PanRecognizer('x', 2, 2); + + p.start({ x: 0, y: 0 }); + expect(p.pan()).toEqual(0); + + Simulate.from(0, 0).to(10, 0).run((coord: Coordinates) => { + p.detect(coord); + }); + expect(p.pan()).toEqual(1); + + p.start({ x: 0, y: 0 }); + expect(p.pan()).toEqual(0); + + Simulate.from(0, 0).to(-10, 0).run((coord: Coordinates) => { + p.detect(coord); + }); + expect(p.pan()).toEqual(-1); + }); + + it('should fire with large threshold', () => { + let detected = false; + let p = new PanRecognizer('x', 100, 40); + p.start({ x: 0, y: 0 }); + + Simulate + .from(0, 0).to(99, 0) + // Since threshold is 100, it should not fire yet + .run((coord: Coordinates) => expect(p.detect(coord)).toEqual(false)) + + // Now it should fire + .delta(2, 0) + .run((coord: Coordinates) => { + if (p.detect(coord)) { + // it should detect a horizontal pan + expect(p.pan()).toEqual(1); + detected = true; + } + }) + + // It should not detect again + .delta(20, 0) + .to(0, 0) + .to(102, 0) + .run((coord: Coordinates) => expect(p.detect(coord)).toEqual(false)); + + expect(detected).toEqual(true); + }); + + it('should detect swipe left', () => { + let p = new PanRecognizer('x', 20, 20); + p.start({ x: 0, y: 0 }); + Simulate + .from(0, 0).deltaPolar(19, 21).delta(-30, 0) + .run((coord: Coordinates) => p.detect(coord)); + expect(p.pan()).toEqual(1); + + p.start({ x: 0, y: 0 }); + Simulate + .from(0, 0).deltaPolar(-19, 21).delta(-30, 0) + .run((coord: Coordinates) => p.detect(coord)); + expect(p.pan()).toEqual(1); + }); + + it('should detect swipe right', () => { + let p = new PanRecognizer('x', 20, 20); + p.start({ x: 0, y: 0 }); + Simulate + .from(0, 0).deltaPolar(180 - 19, 21).delta(30, 0) + .run((coord: Coordinates) => p.detect(coord)); + expect(p.pan()).toEqual(-1); + + p.start({ x: 0, y: 0 }); + Simulate + .from(0, 0).deltaPolar(180 + 19, 21).delta(30, 0) + .run((coord: Coordinates) => p.detect(coord)); + expect(p.pan()).toEqual(-1); + }); + + it('should NOT detect swipe left', () => { + let p = new PanRecognizer('x', 20, 20); + p.start({ x: 0, y: 0 }); + Simulate + .from(0, 0).deltaPolar(21, 21).delta(-30, 0) + .run((coord: Coordinates) => p.detect(coord)); + expect(p.pan()).toEqual(0); + + p.start({ x: 0, y: 0 }); + Simulate + .from(0, 0).deltaPolar(-21, 21).delta(-30, 0) + .run((coord: Coordinates) => p.detect(coord)); + expect(p.pan()).toEqual(0); + }); + + it('should NOT detect swipe right', () => { + let p = new PanRecognizer('x', 20, 20); + p.start({ x: 0, y: 0 }); + Simulate + .from(0, 0).deltaPolar(180 - 21, 21).delta(30, 0) + .run((coord: Coordinates) => p.detect(coord)); + expect(p.pan()).toEqual(0); + + p.start({ x: 0, y: 0 }); + Simulate + .from(0, 0).deltaPolar(180 + 21, 21).delta(30, 0) + .run((coord: Coordinates) => p.detect(coord)); + expect(p.pan()).toEqual(0); + }); + + + + it('should detect swipe top', () => { + let p = new PanRecognizer('y', 20, 20); + p.start({ x: 0, y: 0 }); + Simulate + .from(0, 0).deltaPolar(90 - 19, 21).delta(-30, 0) + .run((coord: Coordinates) => p.detect(coord)); + expect(p.pan()).toEqual(1); + + p.start({ x: 0, y: 0 }); + Simulate + .from(0, 0).deltaPolar(90 + 19, 21).delta(-30, 0) + .run((coord: Coordinates) => p.detect(coord)); + expect(p.pan()).toEqual(1); + }); + + it('should detect swipe bottom', () => { + let p = new PanRecognizer('y', 20, 20); + p.start({ x: 0, y: 0 }); + Simulate + .from(0, 0).deltaPolar(-90 + 19, 21).delta(30, 0) + .run((coord: Coordinates) => p.detect(coord)); + expect(p.pan()).toEqual(-1); + + p.start({ x: 0, y: 0 }); + Simulate + .from(0, 0).deltaPolar(-90 - 19, 21).delta(30, 0) + .run((coord: Coordinates) => p.detect(coord)); + expect(p.pan()).toEqual(-1); + }); + + it('should NOT detect swipe top', () => { + let p = new PanRecognizer('y', 20, 20); + p.start({ x: 0, y: 0 }); + Simulate + .from(0, 0).deltaPolar(90 - 21, 21).delta(-30, 0) + .run((coord: Coordinates) => p.detect(coord)); + expect(p.pan()).toEqual(0); + + p.start({ x: 0, y: 0 }); + Simulate + .from(0, 0).deltaPolar(90 + 21, 21).delta(-30, 0) + .run((coord: Coordinates) => p.detect(coord)); + expect(p.pan()).toEqual(0); + }); + + it('should NOT detect swipe bottom', () => { + let p = new PanRecognizer('y', 20, 20); + p.start({ x: 0, y: 0 }); + Simulate + .from(0, 0).deltaPolar(-90 + 21, 21).delta(30, 0) + .run((coord: Coordinates) => p.detect(coord)); + expect(p.pan()).toEqual(0); + + p.start({ x: 0, y: 0 }); + Simulate + .from(0, 0).deltaPolar(-90 - 21, 21).delta(30, 0) + .run((coord: Coordinates) => p.detect(coord)); + expect(p.pan()).toEqual(0); + }); + + it('should NOT confuse between pan Y and X', () => { + let p = new PanRecognizer('x', 20, 20); + p.start({ x: 0, y: 0 }); + Simulate + .from(0, 0).deltaPolar(90, 21).delta(30, 0) + .run((coord: Coordinates) => p.detect(coord)); + + expect(p.pan()).toEqual(0); + }); + + it('should NOT confuse between pan X and Y', () => { + let p = new PanRecognizer('y', 20, 20); + p.start({ x: 0, y: 0 }); + Simulate + .from(0, 0).delta(30, 0) + .run((coord: Coordinates) => p.detect(coord)); + + expect(p.pan()).toEqual(0); + }); +} diff --git a/src/index.ts b/src/index.ts index f0aa6922e3..5043478fb8 100644 --- a/src/index.ts +++ b/src/index.ts @@ -12,6 +12,7 @@ export * from './gestures/drag-gesture'; export * from './gestures/gesture'; export * from './gestures/slide-edge-gesture'; export * from './gestures/slide-gesture'; +export * from './gestures/gesture-controller'; export * from './platform/platform'; export * from './platform/storage'; diff --git a/src/platform/cordova.ios.scss b/src/platform/cordova.ios.scss index aa2a1945ea..9e0609fae9 100644 --- a/src/platform/cordova.ios.scss +++ b/src/platform/cordova.ios.scss @@ -10,7 +10,6 @@ $cordova-ios-statusbar-padding-modal-max-width: $cordova-statusbar-paddi ion-nav > ion-page, ion-nav > ion-page > ion-header, ion-tab > ion-page > ion-header, -ion-tabs > ion-page.tab-subpage > ion-header, ion-menu { @include toolbar-statusbar-padding($toolbar-ios-height, $content-ios-padding); @include toolbar-title-statusbar-padding($toolbar-ios-height, $content-ios-padding); diff --git a/src/platform/cordova.md.scss b/src/platform/cordova.md.scss index 4897a4e278..cb8dc1d351 100644 --- a/src/platform/cordova.md.scss +++ b/src/platform/cordova.md.scss @@ -10,7 +10,6 @@ $cordova-md-statusbar-padding-modal-max-width: $cordova-statusbar-paddin ion-nav > ion-page, ion-nav > ion-page > ion-header, ion-tab > ion-page > ion-header, -ion-tabs > ion-page.tab-subpage > ion-header, ion-menu { @include toolbar-statusbar-padding($toolbar-md-height, $content-md-padding); } diff --git a/src/platform/cordova.wp.scss b/src/platform/cordova.wp.scss index 230a426fb9..b45a38f227 100644 --- a/src/platform/cordova.wp.scss +++ b/src/platform/cordova.wp.scss @@ -10,7 +10,6 @@ $cordova-wp-statusbar-padding-modal-max-width: $cordova-statusbar-paddin ion-nav > ion-page, ion-nav > ion-page > ion-header, ion-tab > ion-page > ion-header, -ion-tabs > ion-page.tab-subpage > ion-header, ion-menu { @include toolbar-statusbar-padding($toolbar-wp-height, $content-wp-padding); } diff --git a/src/util/dom.ts b/src/util/dom.ts index 5a6c61d396..9a97f5cd50 100644 --- a/src/util/dom.ts +++ b/src/util/dom.ts @@ -190,8 +190,10 @@ export function pointerCoord(ev: any): Coordinates { } export function hasPointerMoved(threshold: number, startCoord: Coordinates, endCoord: Coordinates) { - return startCoord && endCoord && - (Math.abs(startCoord.x - endCoord.x) > threshold || Math.abs(startCoord.y - endCoord.y) > threshold); + let deltaX = (startCoord.x - endCoord.x); + let deltaY = (startCoord.y - endCoord.y); + let distance = deltaX * deltaX + deltaY * deltaY; + return distance > (threshold * threshold); } export function isActive(ele: HTMLElement) { diff --git a/src/util/mock-providers.ts b/src/util/mock-providers.ts new file mode 100644 index 0000000000..acdb194a79 --- /dev/null +++ b/src/util/mock-providers.ts @@ -0,0 +1,171 @@ +import { ChangeDetectorRef, ElementRef, NgZone, Renderer } from '@angular/core'; +import { Location } from '@angular/common'; + +import { App, Config, Form, GestureController, Keyboard, MenuController, NavOptions, Platform, Tab, Tabs, Transition, ViewController } from '../../src'; +import { NavControllerBase } from '../../src/components/nav/nav-controller-base'; + + +export const mockConfig = function(config?: any) { + return new Config(config); +}; + +export const mockPlatform = function(platforms?: string[]) { + return new Platform(platforms); +}; + +export const mockApp = function(config?: Config, platform?: Platform) { + config = config || mockConfig(); + platform = platform || mockPlatform(); + return new App(config, platform); +}; + +export const mockZone = function(): NgZone { + let zone: any = { + run: function(cb: any) { + cb(); + }, + runOutsideAngular: function(cb: any) { + cb(); + } + }; + return zone; +}; + +export const mockChangeDetectorRef = function(): ChangeDetectorRef { + let cd: any = { + reattach: () => {}, + detach: () => {} + }; + return cd; +}; + +export const mockElementRef = function(): ElementRef { + return { + nativeElement: document.createElement('div') + }; +}; + +export const mockRenderer = function(): Renderer { + let renderer: any = { + setElementAttribute: () => {}, + setElementClass: () => {}, + setElementStyle: () => {} + }; + return renderer; +}; + +export const mockLocation = function(): Location { + let location: any = { + path: () => { return ''; }, + subscribe: () => {}, + go: () => {}, + back: () => {} + }; + return location; +}; + +export const mockTransition = function(playCallback: Function, duration: number) { + return function _createTrans(enteringView: ViewController, leavingView: ViewController, transitionOpts: any): Transition { + let transition: any = { + play: () => { + playCallback(); + }, + getDuration: () => { return duration; }, + onFinish: () => {} + }; + return transition; + }; +}; + +export const mockNavController = function(): NavControllerBase { + let platform = mockPlatform(); + + let config = mockConfig(); + config.setPlatform(platform); + + let app = mockApp(config, platform); + + let form = new Form(); + + let zone = mockZone(); + + let keyboard = new Keyboard(config, form, zone); + + let elementRef = mockElementRef(); + + let renderer = mockRenderer(); + + let compiler: any = null; + + let gestureCtrl = new GestureController(app); + + let location = mockLocation(); + + return new NavControllerBase( + null, + app, + config, + keyboard, + elementRef, + zone, + renderer, + compiler, + gestureCtrl + ); +}; + +export const mockTab = function(parentTabs: Tabs): Tab { + let platform = mockPlatform(); + + let config = mockConfig(); + config.setPlatform(platform); + + let app = (parentTabs)._app || mockApp(config, platform); + + let form = new Form(); + + let zone = mockZone(); + + let keyboard = new Keyboard(config, form, zone); + + let elementRef = mockElementRef(); + + let renderer = mockRenderer(); + + let changeDetectorRef = mockChangeDetectorRef(); + + let compiler: any = null; + + let gestureCtrl = new GestureController(app); + + let location = mockLocation(); + + let tab = new Tab( + parentTabs, + app, + config, + keyboard, + elementRef, + zone, + renderer, + compiler, + changeDetectorRef, + gestureCtrl + ); + + tab.load = (opts: any, cb: Function) => { + cb(); + }; + + return tab; +}; + +export const mockTabs = function(app?: App): Tabs { + let config = mockConfig(); + let platform = mockPlatform(); + app = app || mockApp(config, platform); + let elementRef = mockElementRef(); + let renderer = mockRenderer(); + + return new Tabs(null, null, app, config, elementRef, platform, renderer); +}; diff --git a/src/util/ui-event-manager.ts b/src/util/ui-event-manager.ts index 457f7ebc98..03116c7879 100644 --- a/src/util/ui-event-manager.ts +++ b/src/util/ui-event-manager.ts @@ -1,7 +1,14 @@ import {ElementRef} from '@angular/core'; - - +export interface PointerEventsConfig { + element?: HTMLElement; + elementRef?: ElementRef; + pointerDown: (ev: any) => boolean; + pointerMove: (ev: any) => void; + pointerUp: (ev: any) => void; + nativeOptions?: any; + zone?: boolean; +} /** * @private @@ -10,11 +17,15 @@ export class PointerEvents { private rmTouchStart: Function = null; private rmTouchMove: Function = null; private rmTouchEnd: Function = null; + private rmTouchCancel: Function = null; private rmMouseStart: Function = null; private rmMouseMove: Function = null; private rmMouseUp: Function = null; + private bindTouchEnd: Function; + private bindMouseUp: Function; + private lastTouchEvent: number = 0; mouseWait: number = 2 * 1000; @@ -24,10 +35,14 @@ export class PointerEvents { private pointerMove: any, private pointerUp: any, private zone: boolean, - private option: any) { + private option: any + ) { - this.rmTouchStart = listenEvent(ele, 'touchstart', zone, option, (ev: any) => this.handleTouchStart(ev)); - this.rmMouseStart = listenEvent(ele, 'mousedown', zone, option, (ev: any) => this.handleMouseDown(ev)); + this.bindTouchEnd = this.handleTouchEnd.bind(this); + this.bindMouseUp = this.handleMouseUp.bind(this); + + this.rmTouchStart = listenEvent(ele, 'touchstart', zone, option, this.handleTouchStart.bind(this)); + this.rmMouseStart = listenEvent(ele, 'mousedown', zone, option, this.handleMouseDown.bind(this)); } private handleTouchStart(ev: any) { @@ -39,7 +54,10 @@ export class PointerEvents { this.rmTouchMove = listenEvent(this.ele, 'touchmove', this.zone, this.option, this.pointerMove); } if (!this.rmTouchEnd) { - this.rmTouchEnd = listenEvent(this.ele, 'touchend', this.zone, this.option, (ev: any) => this.handleTouchEnd(ev)); + this.rmTouchEnd = listenEvent(this.ele, 'touchend', this.zone, this.option, this.bindTouchEnd); + } + if (!this.rmTouchCancel) { + this.rmTouchCancel = listenEvent(this.ele, 'touchcancel', this.zone, this.option, this.bindTouchEnd); } } @@ -55,40 +73,43 @@ export class PointerEvents { this.rmMouseMove = listenEvent(window, 'mousemove', this.zone, this.option, this.pointerMove); } if (!this.rmMouseUp) { - this.rmMouseUp = listenEvent(window, 'mouseup', this.zone, this.option, (ev: any) => this.handleMouseUp(ev)); + this.rmMouseUp = listenEvent(window, 'mouseup', this.zone, this.option, this.bindMouseUp); } } private handleTouchEnd(ev: any) { - this.rmTouchMove && this.rmTouchMove(); - this.rmTouchMove = null; - this.rmTouchEnd && this.rmTouchEnd(); - this.rmTouchEnd = null; - + this.stopTouch(); this.pointerUp(ev); } private handleMouseUp(ev: any) { - this.rmMouseMove && this.rmMouseMove(); - this.rmMouseMove = null; - this.rmMouseUp && this.rmMouseUp(); - this.rmMouseUp = null; - + this.stopMouse(); this.pointerUp(ev); } - stop() { + private stopTouch() { this.rmTouchMove && this.rmTouchMove(); this.rmTouchEnd && this.rmTouchEnd(); + this.rmTouchCancel && this.rmTouchCancel(); + this.rmTouchMove = null; this.rmTouchEnd = null; + this.rmTouchCancel = null; + } + private stopMouse() { this.rmMouseMove && this.rmMouseMove(); this.rmMouseUp && this.rmMouseUp(); + this.rmMouseMove = null; this.rmMouseUp = null; } + stop() { + this.stopTouch(); + this.stopMouse(); + } + destroy() { this.rmTouchStart && this.rmTouchStart(); this.rmTouchStart = null; @@ -120,21 +141,26 @@ export class UIEventManager { return this.listen(ref.nativeElement, eventName, callback, option); } - pointerEventsRef(ref: ElementRef, pointerStart: any, pointerMove: any, pointerEnd: any, option?: any): PointerEvents { - return this.pointerEvents(ref.nativeElement, pointerStart, pointerMove, pointerEnd, option); - } - - pointerEvents(element: any, pointerDown: any, pointerMove: any, pointerUp: any, option: any = false): PointerEvents { + pointerEvents(config: PointerEventsConfig): PointerEvents { + let element = config.element; if (!element) { + element = config.elementRef.nativeElement; + } + + if (!element || !config.pointerDown || !config.pointerMove || !config.pointerUp) { + console.error('PointerEvents config is invalid'); return; } + let zone = config.zone || this.zoneWrapped; + let options = config.nativeOptions || false; + let submanager = new PointerEvents( element, - pointerDown, - pointerMove, - pointerUp, - this.zoneWrapped, - option); + config.pointerDown, + config.pointerMove, + config.pointerUp, + zone, + options); let removeFunc = () => submanager.destroy(); this.events.push(removeFunc); diff --git a/src/util/util.ts b/src/util/util.ts index 459b618e7c..84000a4047 100644 --- a/src/util/util.ts +++ b/src/util/util.ts @@ -1,4 +1,6 @@ +export function noop() {} + /** * Given a min and max, restrict the given number * to the range. diff --git a/tooling/generators/provider/provider.tmpl.js b/tooling/generators/provider/provider.tmpl.js index f105d19921..909c5343a1 100644 --- a/tooling/generators/provider/provider.tmpl.js +++ b/tooling/generators/provider/provider.tmpl.js @@ -16,29 +16,7 @@ export class <%= jsClassName %> { constructor(http) { this.http = http; - this.data = null; } - load() { - if (this.data) { - // already loaded data - return Promise.resolve(this.data); - } - - // don't have the data yet - return new Promise(resolve => { - // We're using Angular Http provider to request the data, - // then on the response it'll map the JSON data to a parsed JS object. - // Next we process the data and resolve the promise with the new data. - this.http.get('path/to/data.json') - .map(res => res.json()) - .subscribe(data => { - // we've got back the raw data, now generate the core schedule data - // and save the data for later reference - this.data = data; - resolve(this.data); - }); - }); - } } diff --git a/tooling/generators/provider/provider.tmpl.ts b/tooling/generators/provider/provider.tmpl.ts index 3c68415f65..ac8e859c0c 100644 --- a/tooling/generators/provider/provider.tmpl.ts +++ b/tooling/generators/provider/provider.tmpl.ts @@ -10,32 +10,8 @@ import 'rxjs/add/operator/map'; */ @Injectable() export class <%= jsClassName %> { - data: any; - constructor(private http: Http) { - this.data = null; - } + constructor(private http: Http) {} - load() { - if (this.data) { - // already loaded data - return Promise.resolve(this.data); - } - - // don't have the data yet - return new Promise(resolve => { - // We're using Angular Http provider to request the data, - // then on the response it'll map the JSON data to a parsed JS object. - // Next we process the data and resolve the promise with the new data. - this.http.get('path/to/data.json') - .map(res => res.json()) - .subscribe(data => { - // we've got back the raw data, now generate the core schedule data - // and save the data for later reference - this.data = data; - resolve(this.data); - }); - }); - } }