feat(gesture): Introducing new gesture controller

This commit is contained in:
Manu Mtz.-Almeida
2016-07-06 03:04:47 +02:00
parent d6f62bcb60
commit 9f19023cb9
25 changed files with 1035 additions and 124 deletions

View File

@ -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: '<ion-nav [root]="rootPage"></ion-nav>'
})
class E2EApp {
rootPage = E2EPage;
}
ionicBootstrap(E2EApp);

View File

@ -0,0 +1,159 @@
<ion-menu [content]="content" side="left" persistent="true">
<ion-header>
<ion-toolbar secondary>
<ion-title>Left Menu</ion-title>
</ion-toolbar>
</ion-header>
<ion-content>
<ion-list>
<ion-item-sliding *ngFor="let p of pages">
<button ion-item (click)="openPage(p)">
{{p.title}}
</button>
<ion-item-options side="left">
<button>Test</button>
</ion-item-options>
<ion-item-options>
<button>Test</button>
</ion-item-options>
</ion-item-sliding>
<button ion-item menuClose="left" class="e2eCloseLeftMenu" detail-none>
Close Menu
</button>
<button ion-item menuClose="left" detail-none>
Close Menu
</button>
<button ion-item menuClose="left" detail-none>
Close Menu
</button>
<button ion-item menuClose="left" detail-none>
Close Menu
</button>
<button ion-item menuClose="left" detail-none>
Close Menu
</button>
<button ion-item menuClose="left" detail-none>
Close Menu
</button>
<button ion-item menuClose="left" detail-none>
Close Menu
</button>
<button ion-item menuClose="left" detail-none>
Close Menu
</button>
<button ion-item menuClose="left" detail-none>
Close Menu
</button>
<button ion-item menuClose="left" detail-none>
Close Menu
</button>
<button ion-item menuClose="left" detail-none>
Close Menu
</button>
<button ion-item menuClose="left" detail-none>
Close Menu
</button>
</ion-list>
</ion-content>
<ion-footer>
<ion-toolbar secondary>
<ion-title>Footer</ion-title>
</ion-toolbar>
</ion-footer>
</ion-menu>
<ion-menu [content]="content" side="right">
<ion-header>
<ion-toolbar danger>
<ion-title>Right Menu</ion-title>
</ion-toolbar>
</ion-header>
<ion-content>
<ion-list>
<button ion-item *ngFor="let p of pages" (click)="openPage(p)">
{{p.title}}
</button>
<button ion-item menuClose="right" class="e2eCloseRightMenu" detail-none>
Close Menu
</button>
<button ion-item menuClose="right" detail-none>
Close Menu
</button>
<button ion-item menuClose="right" detail-none>
Close Menu
</button>
<button ion-item menuClose="right" detail-none>
Close Menu
</button>
<button ion-item menuClose="right" detail-none>
Close Menu
</button>
<button ion-item menuClose="right" detail-none>
Close Menu
</button>
<button ion-item menuClose="right" detail-none>
Close Menu
</button>
<button ion-item menuClose="right" detail-none>
Close Menu
</button>
<button ion-item menuClose="right" detail-none>
Close Menu
</button>
<button ion-item menuClose="right" detail-none>
Close Menu
</button>
<button ion-item menuClose="right" detail-none>
Close Menu
</button>
<button ion-item menuClose="right" detail-none>
Close Menu
</button>
<button ion-item menuClose="right" detail-none>
Close Menu
</button>
</ion-list>
</ion-content>
</ion-menu>
<ion-nav [root]="rootPage" #content swipeBackEnabled="true"></ion-nav>

View File

@ -0,0 +1,84 @@
<ion-header>
<ion-navbar>
<button menuToggle="left">
<ion-icon name="menu"></ion-icon>
</button>
<ion-title>
Menu
</ion-title>
<button menuToggle="right" right secondary>
<ion-icon name="menu"></ion-icon>
</button>
</ion-navbar>
</ion-header>
<ion-content>
<ion-refresher (ionRefresh)="doRefresh($event)">
<ion-refresher-content
pullingText="Pull to refresh..."
refreshingSpinner="bubbles"
refreshingText="Refreshing...">
</ion-refresher-content>
</ion-refresher>
<h3>Page 1</h3>
<ion-list padding>
<ion-item-sliding>
<button ion-item class="e2eContentToggleLeftMenu" menuToggle="left">Toggle Left Menu</button>
<ion-item-options side="left">
<button>Test</button>
</ion-item-options>
<ion-item-options>
<button>Test</button>
</ion-item-options>
</ion-item-sliding>
</ion-list>
<ion-list>
<ion-item-sliding>
<button ion-item class="e2eContentToggleRightMenu" menuToggle="right">Toggle Right Menu</button>
<ion-item-options side="left">
<button>Test</button>
</ion-item-options>
<ion-item-options>
<button>Test</button>
</ion-item-options>
</ion-item-sliding>
<button ion-item (click)="goToPage1()">Push same page</button>
<ion-item>
<ion-label>RANGE</ion-label>
<ion-range></ion-range>
</ion-item>
<ion-item-sliding>
<ion-item>
<ion-label>SLIDING ITEM + RANGE</ion-label>
<ion-range></ion-range>
</ion-item>
<ion-item-options side="left">
<button>Test</button>
</ion-item-options>
<ion-item-options>
<button>Test</button>
</ion-item-options>
</ion-item-sliding>
</ion-list>
<f></f><f></f><f></f><f></f><f></f><f></f><f></f><f></f><f></f><f></f><f></f><f></f><f></f><f></f><f></f><f></f><f></f><f></f><f></f><f></f>
</ion-content>

View File

@ -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;
}
}

View File

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

View File

@ -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) {

View File

@ -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);
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);
}
}
console.debug('menu shouldCapture,', menu.side, 'isOpen', menu.isOpen, 'angle', ev.angle, 'distance', ev.distance);
if (menu.isOpen) {
return true;
}
// didn't pass the test, don't open this menu
if (menu.side === 'right') {
if ((ev.angle > 140 && ev.angle <= 180) || (ev.angle > -140 && ev.angle <= -180)) {
return super.canStart(ev);
}
} else {
if (ev.angle > -40 && ev.angle < 40) {
return super.canStart(ev);
}
}
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++;
}
}

View File

@ -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);
}

View File

@ -148,6 +148,6 @@
</ion-menu>
<ion-nav [root]="rootPage" #content></ion-nav>
<ion-nav [root]="rootPage" #content swipeBackEnabled="true"></ion-nav>
<div [hidden]="isChangeDetecting()"></div>

View File

@ -35,9 +35,19 @@
<h3>Page 1</h3>
<p>
<button class="e2eContentToggleLeftMenu" menuToggle="left">Toggle Left Menu</button>
</p>
<ion-list>
<ion-item-sliding>
<button ion-item class="e2eContentToggleLeftMenu" menuToggle="left">Toggle Left Menu</button>
<ion-item-options side="left">
<button>Test</button>
</ion-item-options>
<ion-item-options>
<button>Test</button>
</ion-item-options>
</ion-item-sliding>
</ion-list>
<p>
<button class="e2eContentToggleRightMenu" menuToggle="right">Toggle Right Menu</button>

View File

@ -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()) {

View File

@ -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);

View File

@ -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

View File

@ -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();
}
}

View File

@ -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 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;
}
/**

View File

@ -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<Refresher> = new EventEmitter<Refresher>();
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);
}

View File

@ -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) {

View File

@ -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'
}
];

View File

@ -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);

View File

@ -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<any>, config?: any): any[
TapClick,
ToastController,
Translate,
GestureController,
];
if (isPresent(customProviders)) {

View File

@ -1,5 +1,5 @@
import {Gesture} from './gesture';
import {defaults} from '../util';
import { Gesture } from './gesture';
import { defaults } from '../util';
/**
* @private

View File

@ -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<number> } = {};
private disabledScroll: Set<number> = new Set<number>();
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<number>();
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;
}
}

View File

@ -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);
});
}

View File

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