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';