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 00000000000..e69de29bb2d 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 00000000000..9ca007dab0a --- /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 00000000000..98a7b6c4437 --- /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 00000000000..d3a3e87585f --- /dev/null +++ b/src/components/app/test/gesture-collision/page1.html @@ -0,0 +1,84 @@ + + + + + + + + Menu + + + + + + + + + + + + + + + + + +

Page 1

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + RANGE + + + + + + SLIDING ITEM + RANGE + + + + + + + + + + + + + +
diff --git a/src/components/item/item-reorder-gesture.ts b/src/components/item/item-reorder-gesture.ts index 50052f07719..a580de81835 100644 --- a/src/components/item/item-reorder-gesture.ts +++ b/src/components/item/item-reorder-gesture.ts @@ -25,10 +25,9 @@ export class ItemReorderGesture { private events: UIEventManager = new UIEventManager(false); - constructor(public list: ItemReorder) { - let element = this.list.getNativeElement(); + constructor(public reorderList: ItemReorder) { this.events.pointerEvents({ - element: element, + element: this.reorderList.getNativeElement(), pointerDown: this.onDragStart.bind(this), pointerMove: this.onDragMove.bind(this), pointerUp: this.onDragEnd.bind(this) @@ -46,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) { @@ -62,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; } @@ -96,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; @@ -127,7 +126,7 @@ export class ItemReorderGesture { } else { reorderInactive(); } - this.list.reorderEmit(fromIndex, toIndex); + this.reorderList.reorderEmit(fromIndex, toIndex); } private itemForCoord(coord: Coordinates): HTMLElement { @@ -136,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; } @@ -150,7 +149,7 @@ export class ItemReorderGesture { this.onDragEnd(); this.events.unlistenAll(); this.events = null; - this.list = null; + this.reorderList = null; } } diff --git a/src/components/item/item-sliding-gesture.ts b/src/components/item/item-sliding-gesture.ts index f746434fa8a..b97a1c9c9af 100644 --- a/src/components/item/item-sliding-gesture.ts +++ b/src/components/item/item-sliding-gesture.ts @@ -3,6 +3,7 @@ import { List } from '../list/list'; import { closest, Coordinates, pointerCoord } from '../../util/dom'; import { PointerEvents, UIEventManager } from '../../util/ui-event-manager'; +import { GestureDelegate, GestureOptions, GesturePriority } from '../../gestures/gesture-controller'; const DRAG_THRESHOLD = 10; const MAX_ATTACK_ANGLE = 20; @@ -16,8 +17,13 @@ export class ItemSlidingGesture { private pointerEvents: PointerEvents; private firstCoordX: number; private firstTimestamp: number; + private gesture: GestureDelegate; constructor(public list: List) { + this.gesture = list.gestureCtrl.create('item-sliding', { + priority: GesturePriority.Interactive, + }); + this.pointerEvents = this.events.pointerEvents({ element: list.getNativeElement(), pointerDown: this.pointerStart.bind(this), @@ -36,11 +42,18 @@ export class ItemSlidingGesture { this.closeOpened(); return false; } + // Close open container if it is not the selected one. if (container !== this.openContainer && this.closeOpened()) { return false; } + // Try to start gesture + if (!this.gesture.start()) { + this.gesture.release(); + return false; + } + let coord = pointerCoord(ev); this.preSelectedContainer = container; this.panDetector.start(coord); @@ -56,16 +69,19 @@ export class ItemSlidingGesture { } let coord = pointerCoord(ev); if (this.panDetector.detect(coord)) { - if (!this.panDetector.isPanX()) { - this.pointerEvents.stop(); - this.closeOpened(); - } else { - this.onDragStart(ev, coord); + if (this.panDetector.isPanX() && this.gesture.capture()) { + this.onDragStart(ev, coord); + return; } + + // Detection/capturing was not successful, aborting! + this.closeOpened(); + this.pointerEvents.stop(); } } private pointerEnd(ev: any) { + this.gesture.release(); if (this.selectedContainer) { this.onDragEnd(ev); } else { @@ -103,18 +119,21 @@ export class ItemSlidingGesture { } closeOpened(): boolean { - if (!this.openContainer) { - return false; - } - this.openContainer.close(); - this.openContainer = null; this.selectedContainer = null; - return true; + this.gesture.release(); + + if (this.openContainer) { + this.openContainer.close(); + this.openContainer = null; + return true; + } + return false; } - unlisten() { - this.closeOpened(); + destroy() { + this.gesture.destroy(); this.events.unlistenAll(); + this.closeOpened(); this.list = null; this.preSelectedContainer = null; diff --git a/src/components/list/list.ts b/src/components/list/list.ts index 06cdd3d88ad..18a0eb6f862 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,11 +82,11 @@ 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) { diff --git a/src/components/menu/menu-gestures.ts b/src/components/menu/menu-gestures.ts index f8066d990fb..60c274b98e5 100644 --- a/src/components/menu/menu-gestures.ts +++ b/src/components/menu/menu-gestures.ts @@ -1,27 +1,39 @@ -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 { GestureDelegate, GesturePriority } from '../../gestures/gesture-controller'; /** * Gesture attached to the content which the menu is assigned to */ export class MenuContentGesture extends SlideEdgeGesture { + gesture: GestureDelegate; constructor(public menu: Menu, contentEle: HTMLElement, options: any = {}) { - super(contentEle, assign({ direction: 'x', edge: menu.side, threshold: 0, maxEdgeStart: menu.maxEdgeStart || 75 }, options)); + + this.gesture = menu.gestureCtrl.create('menu-swipe', { + priority: GesturePriority.NavigationOptional, + }); } - canStart(ev: any) { - let menu = this.menu; + canStart(ev: any): boolean { + if (this.shouldStart(ev)) { + return this.gesture.capture(); + } + this.gesture.release(); + return false; + } + shouldStart(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; @@ -33,40 +45,23 @@ export class MenuContentGesture extends SlideEdgeGesture { return false; } - console.debug('menu canStart,', menu.side, 'isOpen', menu.isOpen, 'angle', ev.angle, 'distance', ev.distance); + console.debug('menu shouldCapture,', menu.side, 'isOpen', menu.isOpen, 'angle', ev.angle, 'distance', ev.distance); + + if (menu.isOpen) { + return true; + } 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); - } + 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); - } + if (ev.angle > -40 && ev.angle < 40) { + return super.canStart(ev); } - } - - // didn't pass the test, don't open this menu return false; } - // Set CSS, then wait one frame for it to apply before sliding starts onSlideBeforeStart(slide: SlideData, ev: any) { console.debug('menu gesture, onSlideBeforeStart', this.menu.side); @@ -83,16 +78,18 @@ export class MenuContentGesture extends SlideEdgeGesture { } onSlideEnd(slide: SlideData, ev: any) { + this.gesture.release(); + let z = (this.menu.side === 'right' ? slide.min : slide.max); let currentStepValue = (slide.distance / z); 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); - + console.debug( 'menu gesture, onSlide', this.menu.side, 'distance', slide.distance, @@ -103,7 +100,6 @@ export class MenuContentGesture extends SlideEdgeGesture { 'shouldCompleteLeft', shouldCompleteLeft, 'shouldCompleteRight', shouldCompleteRight, 'currentStepValue', currentStepValue); - this.menu.swipeEnd(shouldCompleteLeft, shouldCompleteRight, currentStepValue); } @@ -132,6 +128,16 @@ export class MenuContentGesture extends SlideEdgeGesture { max: this.menu.width() }; } + + unlisten() { + this.gesture.release(); + super.unlisten(); + } + + destroy() { + this.gesture.destroy(); + super.destroy(); + } } @@ -143,5 +149,6 @@ export class MenuTargetGesture extends MenuContentGesture { super(menu, menuEle, { maxEdgeStart: 0 }); + this.gesture.priority++; } } diff --git a/src/components/menu/menu.ts b/src/components/menu/menu.ts index 119eb100067..11f8e0e7b05 100644 --- a/src/components/menu/menu.ts +++ b/src/components/menu/menu.ts @@ -9,6 +9,7 @@ import { MenuContentGesture, MenuTargetGesture } from './menu-gestures'; import { MenuController } from './menu-controller'; import { MenuType } from './menu-types'; import { Platform } from '../../platform/platform'; +import { GestureController } from '../../gestures/gesture-controller'; /** @@ -302,7 +303,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); } diff --git a/src/components/menu/test/basic/main.html b/src/components/menu/test/basic/main.html index 3ce5cb68384..e56fb8c3d07 100644 --- a/src/components/menu/test/basic/main.html +++ b/src/components/menu/test/basic/main.html @@ -148,6 +148,6 @@ - +
diff --git a/src/components/menu/test/basic/page1.html b/src/components/menu/test/basic/page1.html index 036d3d4b6a4..ae6905f8272 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/nav/nav-controller.ts b/src/components/nav/nav-controller.ts index abe56111dca..9bba7c94825 100644 --- a/src/components/nav/nav-controller.ts +++ b/src/components/nav/nav-controller.ts @@ -3,10 +3,10 @@ import { ComponentResolver, ElementRef, EventEmitter, NgZone, provide, Reflectiv 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'; @@ -247,7 +247,7 @@ export class NavController extends Ion { protected _zone: NgZone, protected _renderer: Renderer, protected _compiler: ComponentResolver, - protected _menuCtrl: MenuController + private _gestureCtrl: GestureController ) { super(elementRef); @@ -1379,7 +1379,7 @@ export class NavController extends Ion { edge: 'left', threshold: this._sbThreshold }; - this._sbGesture = new SwipeBackGesture(this.getNativeElement(), opts, this, this._menuCtrl); + this._sbGesture = new SwipeBackGesture(this.getNativeElement(), opts, this, this._gestureCtrl); } if (this.canSwipeBack()) { diff --git a/src/components/nav/nav-portal.ts b/src/components/nav/nav-portal.ts index d7bf91b1fd5..54b6e55c697 100644 --- a/src/components/nav/nav-portal.ts +++ b/src/components/nav/nav-portal.ts @@ -2,8 +2,8 @@ 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'; /** @@ -21,10 +21,10 @@ 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); + 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.ts b/src/components/nav/nav.ts index c49123f595e..efe41d5d33e 100644 --- a/src/components/nav/nav.ts +++ b/src/components/nav/nav.ts @@ -3,8 +3,8 @@ 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 { ViewController } from './view-controller'; @@ -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 diff --git a/src/components/nav/swipe-back.ts b/src/components/nav/swipe-back.ts index 04ab0906279..8a162d7e2ad 100644 --- a/src/components/nav/swipe-back.ts +++ b/src/components/nav/swipe-back.ts @@ -1,4 +1,5 @@ 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 { SlideData } from '../../gestures/slide-gesture'; @@ -7,39 +8,43 @@ import { SlideEdgeGesture } from '../../gestures/slide-edge-gesture'; export class SwipeBackGesture extends SlideEdgeGesture { + private gesture: GestureDelegate; + constructor( element: HTMLElement, options: any, private _nav: NavController, - private _menuCtrl: MenuController + gestureCtlr: GestureController ) { super(element, assign({ direction: 'x', maxEdgeStart: 75 }, options)); + + this.gesture = gestureCtlr.create('goback-swipe', { + priority: GesturePriority.Navigation, + }); } - canStart(ev: any) { + canStart(ev: any): boolean { + this.gesture.release(); + // 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 ( + ev.angle > -40 && + ev.angle < 40 && + ev.distance < 50 && + this._nav.canSwipeBack() && + super.canStart(ev) && + this.gesture.capture() + ); } onSlideBeforeStart(slideData: SlideData, ev: any) { console.debug('swipeBack, onSlideBeforeStart', ev.srcEvent.type); this._nav.swipeBackStart(); - - this._menuCtrl.tempDisable(true); } onSlide(slide: SlideData) { @@ -57,7 +62,17 @@ export class SwipeBackGesture extends SlideEdgeGesture { this._nav.swipeBackEnd(shouldComplete, currentStepValue); - this._menuCtrl.tempDisable(false); + this.gesture.release(); + } + + unlisten() { + this.gesture.release(); + super.unlisten(); + } + + destroy() { + this.gesture.destroy(); + super.destroy(); } } diff --git a/src/components/range/range.ts b/src/components/range/range.ts index 14777f70898..ddb9e911691 100644 --- a/src/components/range/range.ts +++ b/src/components/range/range.ts @@ -431,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 active knob's position - this._active.position(); - this._pressed = this._active.pressed = true; + // 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; } /** diff --git a/src/components/refresher/refresher.ts b/src/components/refresher/refresher.ts index 6c1f5d8b229..03081e14da3 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.Interactive, + }); } 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 @@ -484,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 6daf15f543f..8ddb21a1e84 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/loop/index.ts b/src/components/slides/test/loop/index.ts index ef3480cd2dd..8b95e2cd428 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 a91aeb9166c..635d1270a98 100644 --- a/src/components/tabs/tab.ts +++ b/src/components/tabs/tab.ts @@ -2,9 +2,9 @@ import { ChangeDetectorRef, Component, ComponentResolver, ElementRef, EventEmitt 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 { NavOptions} from '../nav/nav-interfaces'; import { TabButton} from './tab-button'; @@ -229,10 +229,10 @@ 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); diff --git a/src/config/providers.ts b/src/config/providers.ts index 9ed2c313035..434f9de509e 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'; @@ -77,6 +78,7 @@ export function ionicProviders(customProviders?: Array, config?: any): any[ TapClick, ToastController, Translate, + GestureController, ]; if (isPresent(customProviders)) { diff --git a/src/gestures/drag-gesture.ts b/src/gestures/drag-gesture.ts index d94d5b18d08..87dd7a2624f 100644 --- a/src/gestures/drag-gesture.ts +++ b/src/gestures/drag-gesture.ts @@ -1,5 +1,5 @@ -import {Gesture} from './gesture'; -import {defaults} from '../util'; +import { Gesture } from './gesture'; +import { defaults } from '../util'; /** * @private diff --git a/src/gestures/gesture-controller.ts b/src/gestures/gesture-controller.ts new file mode 100644 index 00000000000..715c757f8cc --- /dev/null +++ b/src/gestures/gesture-controller.ts @@ -0,0 +1,215 @@ + +import { Injectable } from '@angular/core'; + +import { App } from '../components/app/app'; + +export const enum GesturePriority { + Minimun = -10000, + NavigationOptional = -20, + Navigation = -10, + Normal = 0, + Interactive = 10, + Input = 20, +} + +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 appRoot: App; + private capturedID: number = null; + + create(name: string, opts: GestureOptions = {}): GestureDelegate { + let id = this.id; this.id++; + return new GestureDelegate(name, id, this, opts); + } + + 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 (isEnabled && this.isScrollDisabled()) { + // this.appRoot.disableScroll = true; + } + } + + enableScroll(id: number) { + let isDisabled = this.isScrollDisabled(); + this.disabledScroll.delete(id); + if (isDisabled && !this.isScrollDisabled()) { + // this.appRoot.disableScroll = 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/test/gesture-controller.spec.ts b/src/gestures/test/gesture-controller.spec.ts new file mode 100644 index 00000000000..6456ea63648 --- /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(); + expect(c.isCaptured()).toEqual(false); + expect(c.isScrollDisabled()).toEqual(false); + }); + + it('should test scrolling enable/disable stack', () => { + let c = new GestureController(); + 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(); + 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(); + 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(); + 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(); + 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(); + 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(); + 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(); + 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(); + + 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(); + 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/index.ts b/src/index.ts index f0aa6922e30..5043478fb84 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';