fix(gestures): gesture controller handled by components

* fix(gestures): gesture controller is handled by components

fixes #9046

* fix(gestures): adds hybrid disable scroll assistance

fixes #9130
fixes #9052
fixes #7444
This commit is contained in:
Manu Mtz.-Almeida
2016-11-16 17:03:51 +01:00
committed by Adam Bradley
parent 339857af1e
commit 32ab817181
26 changed files with 534 additions and 272 deletions

View File

@ -5,7 +5,8 @@ import { Form } from '../../util/form';
import { Key } from '../../util/key'; import { Key } from '../../util/key';
import { NavParams } from '../../navigation/nav-params'; import { NavParams } from '../../navigation/nav-params';
import { ViewController } from '../../navigation/view-controller'; import { ViewController } from '../../navigation/view-controller';
import { BlockerDelegate, GestureController, BLOCK_ALL } from '../../gestures/gesture-controller';
import { assert } from '../../util/util';
/** /**
* @private * @private
@ -53,15 +54,18 @@ export class ActionSheetCmp {
hdrId: string; hdrId: string;
id: number; id: number;
mode: string; mode: string;
gestureBlocker: BlockerDelegate;
constructor( constructor(
private _viewCtrl: ViewController, private _viewCtrl: ViewController,
private _config: Config, private _config: Config,
private _elementRef: ElementRef, private _elementRef: ElementRef,
private _form: Form, private _form: Form,
gestureCtrl: GestureController,
params: NavParams, params: NavParams,
renderer: Renderer renderer: Renderer
) { ) {
this.gestureBlocker = gestureCtrl.createBlocker(BLOCK_ALL);
this.d = params.data; this.d = params.data;
this.mode = _config.get('mode'); this.mode = _config.get('mode');
renderer.setElementClass(_elementRef.nativeElement, `action-sheet-${this.mode}`, true); renderer.setElementClass(_elementRef.nativeElement, `action-sheet-${this.mode}`, true);
@ -110,6 +114,14 @@ export class ActionSheetCmp {
this.d.buttons = buttons; this.d.buttons = buttons;
} }
ionViewWillEnter() {
this.gestureBlocker.block();
}
ionViewDidLeave() {
this.gestureBlocker.unblock();
}
ionViewDidEnter() { ionViewDidEnter() {
this._form.focusOut(); this._form.focusOut();
@ -166,6 +178,11 @@ export class ActionSheetCmp {
dismiss(role: any): Promise<any> { dismiss(role: any): Promise<any> {
return this._viewCtrl.dismiss(null, role); return this._viewCtrl.dismiss(null, role);
} }
ngOnDestroy() {
assert(this.gestureBlocker.blocked === false, 'gesture blocker must be already unblocked');
this.gestureBlocker.destroy();
}
} }
let actionSheetIds = -1; let actionSheetIds = -1;

View File

@ -1,11 +1,11 @@
import { Component, ElementRef, HostListener, Renderer, ViewEncapsulation } from '@angular/core'; import { Component, ElementRef, HostListener, Renderer, ViewEncapsulation } from '@angular/core';
import { Config } from '../../config/config'; import { Config } from '../../config/config';
import { isPresent } from '../../util/util'; import { isPresent, assert } from '../../util/util';
import { Key } from '../../util/key'; import { Key } from '../../util/key';
import { NavParams } from '../../navigation/nav-params'; import { NavParams } from '../../navigation/nav-params';
import { ViewController } from '../../navigation/view-controller'; import { ViewController } from '../../navigation/view-controller';
import { GestureController, BlockerDelegate, BLOCK_ALL } from '../../gestures/gesture-controller';
/** /**
* @private * @private
@ -86,14 +86,18 @@ export class AlertCmp {
msgId: string; msgId: string;
subHdrId: string; subHdrId: string;
mode: string; mode: string;
gestureBlocker: BlockerDelegate;
constructor( constructor(
public _viewCtrl: ViewController, public _viewCtrl: ViewController,
public _elementRef: ElementRef, public _elementRef: ElementRef,
public _config: Config, public _config: Config,
gestureCtrl: GestureController,
params: NavParams, params: NavParams,
renderer: Renderer renderer: Renderer
) { ) {
// gesture blocker is used to disable gestures dynamically
this.gestureBlocker = gestureCtrl.createBlocker(BLOCK_ALL);
this.d = params.data; this.d = params.data;
this.mode = _config.get('mode'); this.mode = _config.get('mode');
renderer.setElementClass(_elementRef.nativeElement, `alert-${this.mode}`, true); renderer.setElementClass(_elementRef.nativeElement, `alert-${this.mode}`, true);
@ -172,6 +176,27 @@ export class AlertCmp {
} }
} }
ionViewWillEnter() {
this.gestureBlocker.block();
}
ionViewDidLeave() {
this.gestureBlocker.unblock();
}
ionViewDidEnter() {
let activeElement: any = document.activeElement;
if (document.activeElement) {
activeElement.blur();
}
let focusableEle = this._elementRef.nativeElement.querySelector('input,button');
if (focusableEle) {
focusableEle.focus();
}
this.enabled = true;
}
@HostListener('body:keyup', ['$event']) @HostListener('body:keyup', ['$event'])
keyUp(ev: KeyboardEvent) { keyUp(ev: KeyboardEvent) {
if (this.enabled && this._viewCtrl.isLast()) { if (this.enabled && this._viewCtrl.isLast()) {
@ -193,19 +218,6 @@ export class AlertCmp {
} }
} }
ionViewDidEnter() {
let activeElement: any = document.activeElement;
if (document.activeElement) {
activeElement.blur();
}
let focusableEle = this._elementRef.nativeElement.querySelector('input,button');
if (focusableEle) {
focusableEle.focus();
}
this.enabled = true;
}
btnClick(button: any, dismissDelay?: number) { btnClick(button: any, dismissDelay?: number) {
if (!this.enabled) { if (!this.enabled) {
return; return;
@ -293,6 +305,11 @@ export class AlertCmp {
}); });
return values; return values;
} }
ngOnDestroy() {
assert(this.gestureBlocker.blocked === false, 'gesture blocker must be already unblocked');
this.gestureBlocker.destroy();
}
} }
let alertIds = -1; let alertIds = -1;

View File

@ -5,6 +5,7 @@ import { Config } from '../../config/config';
import { Ion } from '../ion'; import { Ion } from '../ion';
import { OverlayPortal } from '../nav/overlay-portal'; import { OverlayPortal } from '../nav/overlay-portal';
import { Platform } from '../../platform/platform'; import { Platform } from '../../platform/platform';
import { nativeTimeout } from '../../util/dom';
export const AppRootToken = new OpaqueToken('USERROOT'); export const AppRootToken = new OpaqueToken('USERROOT');
@ -23,6 +24,8 @@ export const AppRootToken = new OpaqueToken('USERROOT');
}) })
export class IonicApp extends Ion implements OnInit { export class IonicApp extends Ion implements OnInit {
private _stopScrollPlugin: any;
private _rafId: number;
@ViewChild('viewport', {read: ViewContainerRef}) _viewport: ViewContainerRef; @ViewChild('viewport', {read: ViewContainerRef}) _viewport: ViewContainerRef;
@ViewChild('modalPortal', { read: OverlayPortal }) _modalPortal: OverlayPortal; @ViewChild('modalPortal', { read: OverlayPortal }) _modalPortal: OverlayPortal;
@ -45,6 +48,7 @@ export class IonicApp extends Ion implements OnInit {
super(config, elementRef, renderer); super(config, elementRef, renderer);
// register with App that this is Ionic's appRoot component. tada! // register with App that this is Ionic's appRoot component. tada!
app._appRoot = this; app._appRoot = this;
this._stopScrollPlugin = window['IonicStopScroll'];
} }
ngOnInit() { ngOnInit() {
@ -109,7 +113,26 @@ export class IonicApp extends Ion implements OnInit {
* @private * @private
*/ */
_disableScroll(shouldDisableScroll: boolean) { _disableScroll(shouldDisableScroll: boolean) {
this.setElementClass('disable-scroll', shouldDisableScroll); console.log('App Root: Scroll Disable Assist', shouldDisableScroll);
if (shouldDisableScroll) {
this.stopScroll().then(() => {
this._rafId = nativeTimeout(() => this.setElementClass('disable-scroll', true), 16 * 2);
});
} else {
cancelAnimationFrame(this._rafId);
this.setElementClass('disable-scroll', false);
}
}
stopScroll(): Promise<boolean> {
if (this._stopScrollPlugin) {
return new Promise((resolve, reject) => {
this._stopScrollPlugin.stop(() => resolve(true));
});
} else {
return Promise.resolve(false);
}
} }
} }

View File

@ -22,7 +22,7 @@ export class App {
private _title: string = ''; private _title: string = '';
private _titleSrv: Title = new Title(); private _titleSrv: Title = new Title();
private _rootNav: NavController = null; private _rootNav: NavController = null;
private _canDisableScroll: boolean; private _disableScrollAssist: boolean;
/** /**
* @private * @private
@ -71,7 +71,7 @@ export class App {
// listen for hardware back button events // listen for hardware back button events
// register this back button action with a default priority // register this back button action with a default priority
_platform.registerBackButtonAction(this.navPop.bind(this)); _platform.registerBackButtonAction(this.navPop.bind(this));
this._canDisableScroll = _config.get('canDisableScroll', false); this._disableScrollAssist = _config.getBoolean('disableScrollAssist', false);
} }
/** /**
@ -124,7 +124,7 @@ export class App {
* scrolling is enabled. When set to `true`, scrolling is disabled. * scrolling is enabled. When set to `true`, scrolling is disabled.
*/ */
setScrollDisabled(disableScroll: boolean) { setScrollDisabled(disableScroll: boolean) {
if (this._canDisableScroll) { if (this._disableScrollAssist) {
this._appRoot._disableScroll(disableScroll); this._appRoot._disableScroll(disableScroll);
} }
} }

View File

@ -19,7 +19,3 @@ ion-backdrop {
opacity: .01; opacity: .01;
transform: translateZ(0); transform: translateZ(0);
} }
ion-backdrop.hide-backdrop {
display: none;
}

View File

@ -1,8 +1,4 @@
import { Directive, ElementRef, Input, Renderer } from '@angular/core'; import { Directive, ElementRef, Renderer } from '@angular/core';
import { GestureController } from '../../gestures/gesture-controller';
import { isTrueProperty } from '../../util/util';
/** /**
* @private * @private
@ -16,26 +12,11 @@ import { isTrueProperty } from '../../util/util';
}, },
}) })
export class Backdrop { export class Backdrop {
private _gestureID: number = null;
@Input() disableScroll = true;
constructor( constructor(
private _gestureCtrl: GestureController,
private _elementRef: ElementRef, private _elementRef: ElementRef,
private _renderer: Renderer) { } private _renderer: Renderer
) { }
ngOnInit() {
if (isTrueProperty(this.disableScroll)) {
this._gestureID = this._gestureCtrl.newID();
this._gestureCtrl.disableScroll(this._gestureID);
}
}
ngOnDestroy() {
if (this._gestureID) {
this._gestureCtrl.enableScroll(this._gestureID);
}
}
getNativeElement(): HTMLElement { getNativeElement(): HTMLElement {
return this._elementRef.nativeElement; return this._elementRef.nativeElement;

View File

@ -51,7 +51,7 @@ ion-content.js-scroll > .scroll-content {
will-change: initial; will-change: initial;
} }
.disable-scroll .ion-page .scroll-content { .disable-scroll .ion-page {
pointer-events: none; pointer-events: none;
} }

View File

@ -1,14 +1,14 @@
import { ItemSliding } from './item-sliding'; import { ItemSliding } from './item-sliding';
import { List } from '../list/list'; import { List } from '../list/list';
import { GesturePriority } from '../../gestures/gesture-controller'; import { GestureController, GesturePriority, GESTURE_ITEM_SWIPE } from '../../gestures/gesture-controller';
import { PanGesture } from '../../gestures/drag-gesture'; import { PanGesture } from '../../gestures/drag-gesture';
import { pointerCoord } from '../../util/dom'; import { pointerCoord } from '../../util/dom';
import { NativeRafDebouncer } from '../../util/debouncer'; import { NativeRafDebouncer } from '../../util/debouncer';
const DRAG_THRESHOLD = 10; /**
const MAX_ATTACK_ANGLE = 20; * @private
*/
export class ItemSlidingGesture extends PanGesture { export class ItemSlidingGesture extends PanGesture {
private preSelectedContainer: ItemSliding = null; private preSelectedContainer: ItemSliding = null;
@ -17,14 +17,16 @@ export class ItemSlidingGesture extends PanGesture {
private firstCoordX: number; private firstCoordX: number;
private firstTimestamp: number; private firstTimestamp: number;
constructor(public list: List) { constructor(public list: List, gestureCtrl: GestureController) {
super(list.getNativeElement(), { super(list.getNativeElement(), {
maxAngle: MAX_ATTACK_ANGLE, maxAngle: 20,
threshold: DRAG_THRESHOLD, threshold: 10,
zone: false, zone: false,
debouncer: new NativeRafDebouncer(), debouncer: new NativeRafDebouncer(),
gesture: list._gestureCtrl.create('item-sliding', { gesture: gestureCtrl.createGesture({
name: GESTURE_ITEM_SWIPE,
priority: GesturePriority.SlidingItem, priority: GesturePriority.SlidingItem,
disableScroll: false // TODO: set true
}) })
}); });
} }

View File

@ -95,7 +95,7 @@ export class List extends Ion {
} else if (!this._slidingGesture) { } else if (!this._slidingGesture) {
console.debug('enableSlidingItems'); console.debug('enableSlidingItems');
this._slidingGesture = new ItemSlidingGesture(this); this._slidingGesture = new ItemSlidingGesture(this, this._gestureCtrl);
this._slidingGesture.listen(); this._slidingGesture.listen();
} }
} }

View File

@ -1,10 +1,11 @@
import { Component, ElementRef, Renderer, ViewEncapsulation } from '@angular/core'; import { Component, ElementRef, Renderer, ViewEncapsulation } from '@angular/core';
import { Config } from '../../config/config'; import { Config } from '../../config/config';
import { isDefined, isUndefined } from '../../util/util'; import { isDefined, isUndefined, assert } from '../../util/util';
import { NavParams } from '../../navigation/nav-params'; import { NavParams } from '../../navigation/nav-params';
import { ViewController } from '../../navigation/view-controller'; import { ViewController } from '../../navigation/view-controller';
import { LoadingOptions } from './loading-options'; import { LoadingOptions } from './loading-options';
import { BlockerDelegate, GestureController, BLOCK_ALL } from '../../gestures/gesture-controller';
/** /**
* @private * @private
@ -12,7 +13,7 @@ import { LoadingOptions } from './loading-options';
@Component({ @Component({
selector: 'ion-loading', selector: 'ion-loading',
template: template:
'<ion-backdrop [class.hide-backdrop]="!d.showBackdrop"></ion-backdrop>' + '<ion-backdrop [hidden]="!d.showBackdrop"></ion-backdrop>' +
'<div class="loading-wrapper">' + '<div class="loading-wrapper">' +
'<div *ngIf="showSpinner" class="loading-spinner">' + '<div *ngIf="showSpinner" class="loading-spinner">' +
'<ion-spinner [name]="d.spinner"></ion-spinner>' + '<ion-spinner [name]="d.spinner"></ion-spinner>' +
@ -29,14 +30,17 @@ export class LoadingCmp {
id: number; id: number;
showSpinner: boolean; showSpinner: boolean;
durationTimeout: number; durationTimeout: number;
gestureBlocker: BlockerDelegate;
constructor( constructor(
private _viewCtrl: ViewController, private _viewCtrl: ViewController,
private _config: Config, private _config: Config,
private _elementRef: ElementRef, private _elementRef: ElementRef,
gestureCtrl: GestureController,
params: NavParams, params: NavParams,
renderer: Renderer renderer: Renderer
) { ) {
this.gestureBlocker = gestureCtrl.createBlocker(BLOCK_ALL);
this.d = params.data; this.d = params.data;
renderer.setElementClass(_elementRef.nativeElement, `loading-${_config.get('mode')}`, true); renderer.setElementClass(_elementRef.nativeElement, `loading-${_config.get('mode')}`, true);
@ -62,17 +66,21 @@ export class LoadingCmp {
this.showSpinner = isDefined(this.d.spinner) && this.d.spinner !== 'hide'; this.showSpinner = isDefined(this.d.spinner) && this.d.spinner !== 'hide';
} }
ionViewWillEnter() {
this.gestureBlocker.block();
}
ionViewDidLeave() {
this.gestureBlocker.unblock();
}
ionViewDidEnter() { ionViewDidEnter() {
let activeElement: any = document.activeElement; let activeElement: any = document.activeElement;
if (document.activeElement) { activeElement && activeElement.blur();
activeElement.blur();
}
// If there is a duration, dismiss after that amount of time // If there is a duration, dismiss after that amount of time
if ( this.d && this.d.duration ) { if ( this.d && this.d.duration ) {
this.durationTimeout = (<any> setTimeout( () => { this.durationTimeout = setTimeout(() => this.dismiss('backdrop'), this.d.duration);
this.dismiss('backdrop');
}, this.d.duration));
} }
} }
@ -83,6 +91,11 @@ export class LoadingCmp {
} }
return this._viewCtrl.dismiss(null, role); return this._viewCtrl.dismiss(null, role);
} }
ngOnDestroy() {
assert(this.gestureBlocker.blocked === false, 'gesture blocker must be already unblocked');
this.gestureBlocker.destroy();
}
} }
let loadingIds = -1; let loadingIds = -1;

View File

@ -2,7 +2,7 @@ import { Menu } from './menu';
import { SlideEdgeGesture } from '../../gestures/slide-edge-gesture'; import { SlideEdgeGesture } from '../../gestures/slide-edge-gesture';
import { SlideData } from '../../gestures/slide-gesture'; import { SlideData } from '../../gestures/slide-gesture';
import { assign } from '../../util/util'; import { assign } from '../../util/util';
import { GestureController, GesturePriority } from '../../gestures/gesture-controller'; import { GestureController, GesturePriority, GESTURE_MENU_SWIPE } from '../../gestures/gesture-controller';
import { NativeRafDebouncer } from '../../util/debouncer'; import { NativeRafDebouncer } from '../../util/debouncer';
/** /**
@ -14,17 +14,19 @@ export class MenuContentGesture extends SlideEdgeGesture {
public menu: Menu, public menu: Menu,
contentEle: HTMLElement, contentEle: HTMLElement,
gestureCtrl: GestureController, gestureCtrl: GestureController,
options: any = {}) { options: any = {}
) {
super(contentEle, assign({ super(contentEle, assign({
direction: 'x', direction: 'x',
edge: menu.side, edge: menu.side,
threshold: 0, threshold: 0,
maxEdgeStart: menu.maxEdgeStart || 50, maxEdgeStart: menu.maxEdgeStart || 50,
maxAngle: 40,
zone: false, zone: false,
debouncer: new NativeRafDebouncer(), debouncer: new NativeRafDebouncer(),
gesture: gestureCtrl.create('menu-swipe', { gesture: gestureCtrl.createGesture({
name: GESTURE_MENU_SWIPE,
priority: GesturePriority.MenuSwipe, priority: GesturePriority.MenuSwipe,
disableScroll: true
}) })
}, options)); }, options));
} }
@ -52,13 +54,6 @@ export class MenuContentGesture extends SlideEdgeGesture {
let z = (this.menu.side === 'right' ? slide.min : slide.max); let z = (this.menu.side === 'right' ? slide.min : slide.max);
let stepValue = (slide.distance / z); let stepValue = (slide.distance / z);
console.debug('menu gesture, onSlide', this.menu.side,
'distance', slide.distance,
'min', slide.min,
'max', slide.max,
'z', z,
'stepValue', stepValue);
this.menu.swipeProgress(stepValue); this.menu.swipeProgress(stepValue);
} }

View File

@ -8,7 +8,7 @@ import { MenuContentGesture } from './menu-gestures';
import { MenuController } from './menu-controller'; import { MenuController } from './menu-controller';
import { MenuType } from './menu-types'; import { MenuType } from './menu-types';
import { Platform } from '../../platform/platform'; import { Platform } from '../../platform/platform';
import { GestureController } from '../../gestures/gesture-controller'; import { BlockerDelegate, GestureController, GESTURE_GO_BACK_SWIPE } from '../../gestures/gesture-controller';
import { UIEventManager } from '../../util/ui-event-manager'; import { UIEventManager } from '../../util/ui-event-manager';
import { Content } from '../content/content'; import { Content } from '../content/content';
@ -181,7 +181,7 @@ import { Content } from '../content/content';
selector: 'ion-menu', selector: 'ion-menu',
template: template:
'<div class="menu-inner"><ng-content></ng-content></div>' + '<div class="menu-inner"><ng-content></ng-content></div>' +
'<ion-backdrop disableScroll="false"></ion-backdrop>', '<ion-backdrop></ion-backdrop>',
host: { host: {
'role': 'navigation' 'role': 'navigation'
}, },
@ -198,7 +198,7 @@ export class Menu {
private _isPers: boolean = false; private _isPers: boolean = false;
private _init: boolean = false; private _init: boolean = false;
private _events: UIEventManager = new UIEventManager(); private _events: UIEventManager = new UIEventManager();
private _gestureID: number = 0; private _gestureBlocker: BlockerDelegate;
/** /**
* @private * @private
@ -305,9 +305,9 @@ export class Menu {
private _zone: NgZone, private _zone: NgZone,
private _gestureCtrl: GestureController private _gestureCtrl: GestureController
) { ) {
if (_gestureCtrl) { this._gestureBlocker = _gestureCtrl.createBlocker({
this._gestureID = _gestureCtrl.newID(); disable: [GESTURE_GO_BACK_SWIPE]
} });
} }
/** /**
@ -503,7 +503,7 @@ export class Menu {
this._events.unlistenAll(); this._events.unlistenAll();
if (isOpen) { if (isOpen) {
// Disable swipe to go back gesture // Disable swipe to go back gesture
this._gestureCtrl.disableGesture('goback-swipe', this._gestureID); this._gestureBlocker.block();
this._cntEle.classList.add('menu-content-open'); this._cntEle.classList.add('menu-content-open');
let callback = this.onBackdropClick.bind(this); let callback = this.onBackdropClick.bind(this);
@ -519,7 +519,7 @@ export class Menu {
} else { } else {
// Enable swipe to go back gesture // Enable swipe to go back gesture
this._gestureCtrl.enableGesture('goback-swipe', this._gestureID); this._gestureBlocker.unblock();
this._cntEle.classList.remove('menu-content-open'); this._cntEle.classList.remove('menu-content-open');
this.setElementClass('show-menu', false); this.setElementClass('show-menu', false);

View File

@ -1,12 +1,16 @@
import { Component, ViewChild, NgModule } from '@angular/core'; import { Component, ViewChild, NgModule } from '@angular/core';
import { IonicApp, IonicModule, MenuController, NavController, AlertController, Nav } from '../../../..'; import { AlertController, IonicApp, IonicModule, MenuController, ModalController, NavController, Nav, ViewController } from '../../../..';
@Component({ @Component({
templateUrl: 'page1.html' templateUrl: 'page1.html'
}) })
export class Page1 { export class Page1 {
constructor(public navCtrl: NavController, public alertCtrl: AlertController) {} constructor(
public navCtrl: NavController,
public alertCtrl: AlertController,
public modalCtrl: ModalController
) { }
presentAlert() { presentAlert() {
let alert = this.alertCtrl.create({ let alert = this.alertCtrl.create({
@ -18,11 +22,24 @@ export class Page1 {
alert.present(); alert.present();
} }
presentModal() {
let modal = this.modalCtrl.create(Modal);
modal.present();
}
goToPage2() { goToPage2() {
this.navCtrl.push(Page2); this.navCtrl.push(Page2);
} }
} }
@Component({templateUrl: 'modal.html'})
export class Modal {
constructor(public viewController: ViewController) {}
close() {
this.viewController.dismiss();
}
}
@Component({templateUrl: 'page3.html'}) @Component({templateUrl: 'page3.html'})
export class Page3 {} export class Page3 {}
@ -106,7 +123,8 @@ export class E2EApp {
E2EPage, E2EPage,
Page1, Page1,
Page2, Page2,
Page3 Page3,
Modal
], ],
imports: [ imports: [
IonicModule.forRoot(E2EApp) IonicModule.forRoot(E2EApp)
@ -117,7 +135,8 @@ export class E2EApp {
E2EPage, E2EPage,
Page1, Page1,
Page2, Page2,
Page3 Page3,
Modal
] ]
}) })
export class AppModule {} export class AppModule {}

View File

@ -0,0 +1,20 @@
<ion-header>
<ion-navbar>
<ion-title>
MODAL
</ion-title>
</ion-navbar>
</ion-header>
<ion-content padding>
<p>
<button ion-button (click)="close()">Close</button>
</p>
<div f></div><div f></div><div f></div><div f></div><div f></div><div f></div><div f></div><div f></div>
</ion-content>

View File

@ -64,6 +64,10 @@
<button ion-button (click)="presentAlert()">Open alert</button> <button ion-button (click)="presentAlert()">Open alert</button>
</p> </p>
<p>
<button ion-button (click)="presentModal()">Open modal</button>
</p>
<p> <p>
<button ion-button (click)="goToPage2()">Go to Page 2</button> <button ion-button (click)="goToPage2()">Go to Page 2</button>
</p> </p>

View File

@ -3,7 +3,8 @@ import { Component, ComponentFactoryResolver, HostListener, Renderer, ViewChild,
import { Key } from '../../util/key'; import { Key } from '../../util/key';
import { NavParams } from '../../navigation/nav-params'; import { NavParams } from '../../navigation/nav-params';
import { ViewController } from '../../navigation/view-controller'; import { ViewController } from '../../navigation/view-controller';
import { GestureController, BlockerDelegate, GESTURE_MENU_SWIPE, GESTURE_GO_BACK_SWIPE } from '../../gestures/gesture-controller';
import { assert } from '../../util/util';
/** /**
* @private * @private
@ -11,7 +12,7 @@ import { ViewController } from '../../navigation/view-controller';
@Component({ @Component({
selector: 'ion-modal', selector: 'ion-modal',
template: template:
'<ion-backdrop disableScroll="false" (click)="_bdClick()"></ion-backdrop>' + '<ion-backdrop (click)="_bdClick()"></ion-backdrop>' +
'<div class="modal-wrapper">' + '<div class="modal-wrapper">' +
'<div #viewport nav-viewport></div>' + '<div #viewport nav-viewport></div>' +
'</div>' '</div>'
@ -20,13 +21,20 @@ export class ModalCmp {
@ViewChild('viewport', { read: ViewContainerRef }) _viewport: ViewContainerRef; @ViewChild('viewport', { read: ViewContainerRef }) _viewport: ViewContainerRef;
/** @private */
_bdDismiss: boolean; _bdDismiss: boolean;
/** @private */
_enabled: boolean; _enabled: boolean;
_gestureBlocker: BlockerDelegate;
constructor(public _cfr: ComponentFactoryResolver, public _renderer: Renderer, public _navParams: NavParams, public _viewCtrl: ViewController) { constructor(
public _cfr: ComponentFactoryResolver,
public _renderer: Renderer,
public _navParams: NavParams,
public _viewCtrl: ViewController,
gestureCtrl: GestureController
) {
this._gestureBlocker = gestureCtrl.createBlocker({
disable: [GESTURE_MENU_SWIPE, GESTURE_GO_BACK_SWIPE]
});
this._bdDismiss = _navParams.data.opts.enableBackdropDismiss; this._bdDismiss = _navParams.data.opts.enableBackdropDismiss;
} }
@ -46,9 +54,20 @@ export class ModalCmp {
this._setCssClass(componentRef, 'ion-page'); this._setCssClass(componentRef, 'ion-page');
this._setCssClass(componentRef, 'show-page'); this._setCssClass(componentRef, 'show-page');
this._enabled = true; this._enabled = true;
this._viewCtrl.willEnter.subscribe(this._viewWillEnter.bind(this));
this._viewCtrl.didLeave.subscribe(this._viewDidLeave.bind(this));
} }
} }
_viewWillEnter() {
this._gestureBlocker.block();
}
_viewDidLeave() {
this._gestureBlocker.unblock();
}
/** @private */ /** @private */
_setCssClass(componentRef: any, className: string) { _setCssClass(componentRef: any, className: string) {
this._renderer.setElementClass(componentRef.location.nativeElement, className, true); this._renderer.setElementClass(componentRef.location.nativeElement, className, true);
@ -66,4 +85,9 @@ export class ModalCmp {
this._bdClick(); this._bdClick();
} }
} }
ngOnDestroy() {
assert(this._gestureBlocker.blocked === false, 'gesture blocker must be already unblocked');
this._gestureBlocker.destroy();
}
} }

View File

@ -834,7 +834,9 @@ export const deepLinkConfig: DeepLinkConfig = {
TabItemPage TabItemPage
], ],
imports: [ imports: [
IonicModule.forRoot(E2EApp, null, deepLinkConfig) IonicModule.forRoot(E2EApp, {
swipeBackEnabled: true
}, deepLinkConfig)
], ],
bootstrap: [IonicApp], bootstrap: [IonicApp],
entryComponents: [ entryComponents: [

View File

@ -2,7 +2,7 @@ import { Component, ElementRef, EventEmitter, Input, HostListener, NgZone, Outpu
import { DomSanitizer } from '@angular/platform-browser'; import { DomSanitizer } from '@angular/platform-browser';
import { CSS, cancelRaf, pointerCoord, nativeRaf } from '../../util/dom'; import { CSS, cancelRaf, pointerCoord, nativeRaf } from '../../util/dom';
import { clamp, isNumber, isPresent, isString } from '../../util/util'; import { clamp, isNumber, isPresent, isString, assert } from '../../util/util';
import { Config } from '../../config/config'; import { Config } from '../../config/config';
import { Key } from '../../util/key'; import { Key } from '../../util/key';
import { NavParams } from '../../navigation/nav-params'; import { NavParams } from '../../navigation/nav-params';
@ -12,6 +12,7 @@ import { Haptic } from '../../util/haptic';
import { UIEventManager } from '../../util/ui-event-manager'; import { UIEventManager } from '../../util/ui-event-manager';
import { ViewController } from '../../navigation/view-controller'; import { ViewController } from '../../navigation/view-controller';
import { Debouncer, NativeRafDebouncer } from '../../util/debouncer'; import { Debouncer, NativeRafDebouncer } from '../../util/debouncer';
import { GestureController, BlockerDelegate, BLOCK_ALL } from '../../gestures/gesture-controller';
/** /**
* @private * @private
@ -454,14 +455,17 @@ export class PickerCmp {
lastClick: number; lastClick: number;
id: number; id: number;
mode: string; mode: string;
_gestureBlocker: BlockerDelegate;
constructor( constructor(
private _viewCtrl: ViewController, private _viewCtrl: ViewController,
private _elementRef: ElementRef, private _elementRef: ElementRef,
private _config: Config, private _config: Config,
gestureCtrl: GestureController,
params: NavParams, params: NavParams,
renderer: Renderer renderer: Renderer
) { ) {
this._gestureBlocker = gestureCtrl.createBlocker(BLOCK_ALL);
this.d = params.data; this.d = params.data;
this.mode = _config.get('mode'); this.mode = _config.get('mode');
renderer.setElementClass(_elementRef.nativeElement, `picker-${this.mode}`, true); renderer.setElementClass(_elementRef.nativeElement, `picker-${this.mode}`, true);
@ -523,6 +527,14 @@ export class PickerCmp {
}); });
} }
ionViewWillEnter() {
this._gestureBlocker.block();
}
ionViewDidLeave() {
this._gestureBlocker.unblock();
}
refresh() { refresh() {
this._cols.forEach(column => { this._cols.forEach(column => {
column.refresh(); column.refresh();
@ -617,6 +629,12 @@ export class PickerCmp {
}); });
return selected; return selected;
} }
ngOnDestroy() {
assert(this._gestureBlocker.blocked === false, 'gesture blocker must be already unblocked');
this._gestureBlocker.destroy();
}
} }
let pickerIds = -1; let pickerIds = -1;

View File

@ -4,7 +4,8 @@ import { Config } from '../../config/config';
import { Key } from '../../util/key'; import { Key } from '../../util/key';
import { NavParams } from '../../navigation/nav-params'; import { NavParams } from '../../navigation/nav-params';
import { ViewController } from '../../navigation/view-controller'; import { ViewController } from '../../navigation/view-controller';
import { GestureController, BlockerDelegate, BLOCK_ALL } from '../../gestures/gesture-controller';
import { assert } from '../../util/util';
/** /**
* @private * @private
@ -12,7 +13,7 @@ import { ViewController } from '../../navigation/view-controller';
@Component({ @Component({
selector: 'ion-popover', selector: 'ion-popover',
template: template:
'<ion-backdrop (click)="_bdClick()" [class.hide-backdrop]="!d.showBackdrop"></ion-backdrop>' + '<ion-backdrop (click)="_bdClick()" [hidden]="!d.showBackdrop"></ion-backdrop>' +
'<div class="popover-wrapper">' + '<div class="popover-wrapper">' +
'<div class="popover-arrow"></div>' + '<div class="popover-arrow"></div>' +
'<div class="popover-content">' + '<div class="popover-content">' +
@ -32,10 +33,9 @@ export class PopoverCmp {
enableBackdropDismiss?: boolean; enableBackdropDismiss?: boolean;
}; };
/** @private */
_enabled: boolean; _enabled: boolean;
_gestureBlocker: BlockerDelegate;
/** @private */
id: number; id: number;
constructor( constructor(
@ -44,8 +44,10 @@ export class PopoverCmp {
public _renderer: Renderer, public _renderer: Renderer,
public _config: Config, public _config: Config,
public _navParams: NavParams, public _navParams: NavParams,
public _viewCtrl: ViewController public _viewCtrl: ViewController,
gestureCtrl: GestureController,
) { ) {
this._gestureBlocker = gestureCtrl.createBlocker(BLOCK_ALL);
this.d = _navParams.data.opts; this.d = _navParams.data.opts;
_renderer.setElementClass(_elementRef.nativeElement, `popover-${_config.get('mode')}`, true); _renderer.setElementClass(_elementRef.nativeElement, `popover-${_config.get('mode')}`, true);
@ -62,13 +64,11 @@ export class PopoverCmp {
ionViewPreLoad() { ionViewPreLoad() {
let activeElement: any = document.activeElement; let activeElement: any = document.activeElement;
if (document.activeElement) { activeElement && activeElement.blur();
activeElement.blur();
}
this._load(this._navParams.data.component); this._load(this._navParams.data.component);
} }
/** @private */
_load(component: any) { _load(component: any) {
if (component) { if (component) {
const componentFactory = this._cfr.resolveComponentFactory(component); const componentFactory = this._cfr.resolveComponentFactory(component);
@ -76,12 +76,23 @@ export class PopoverCmp {
// ******** DOM WRITE **************** // ******** DOM WRITE ****************
const componentRef = this._viewport.createComponent(componentFactory, this._viewport.length, this._viewport.parentInjector, []); const componentRef = this._viewport.createComponent(componentFactory, this._viewport.length, this._viewport.parentInjector, []);
this._viewCtrl._setInstance(componentRef.instance); this._viewCtrl._setInstance(componentRef.instance);
this._enabled = true; this._enabled = true;
// Subscribe to events in order to block gestures
// TODO, should we unsubscribe? memory leak?
this._viewCtrl.willEnter.subscribe(this._viewWillEnter.bind(this));
this._viewCtrl.didLeave.subscribe(this._viewDidLeave.bind(this));
} }
} }
/** @private */ _viewWillEnter() {
this._gestureBlocker.block();
}
_viewDidLeave() {
this._gestureBlocker.unblock();
}
_setCssClass(componentRef: any, className: string) { _setCssClass(componentRef: any, className: string) {
this._renderer.setElementClass(componentRef.location.nativeElement, className, true); this._renderer.setElementClass(componentRef.location.nativeElement, className, true);
} }
@ -98,6 +109,11 @@ export class PopoverCmp {
this._bdClick(); this._bdClick();
} }
} }
ngOnDestroy() {
assert(this._gestureBlocker.blocked === false, 'gesture blocker must be already unblocked');
this._gestureBlocker.destroy();
}
} }
let popoverIds = -1; let popoverIds = -1;

View File

@ -2,7 +2,7 @@ import { Directive, EventEmitter, Host, Input, Output, NgZone } from '@angular/c
import { Content } from '../content/content'; import { Content } from '../content/content';
import { CSS, pointerCoord } from '../../util/dom'; import { CSS, pointerCoord } from '../../util/dom';
import { GestureController, GestureDelegate, GesturePriority } from '../../gestures/gesture-controller'; import { GestureController, GestureDelegate, GesturePriority, GESTURE_REFRESHER } from '../../gestures/gesture-controller';
import { isTrueProperty } from '../../util/util'; import { isTrueProperty } from '../../util/util';
import { PointerEvents, UIEventManager } from '../../util/ui-event-manager'; import { PointerEvents, UIEventManager } from '../../util/ui-event-manager';
@ -200,7 +200,8 @@ export class Refresher {
constructor(@Host() private _content: Content, private _zone: NgZone, gestureCtrl: GestureController) { constructor(@Host() private _content: Content, private _zone: NgZone, gestureCtrl: GestureController) {
_content.setElementClass('has-refresher', true); _content.setElementClass('has-refresher', true);
this._gesture = gestureCtrl.create('refresher', { this._gesture = gestureCtrl.createGesture({
name: GESTURE_REFRESHER,
priority: GesturePriority.Refresher, priority: GesturePriority.Refresher,
}); });
} }

View File

@ -53,10 +53,10 @@
.back-button { .back-button {
display: none; display: none;
}
&.show-back-button { .back-button.show-back-button {
display: inline-block; display: inline-block;
}
} }
.back-button-text { .back-button-text {

View File

@ -1,6 +1,22 @@
import { forwardRef, Inject, Injectable } from '@angular/core'; import { forwardRef, Inject, Injectable } from '@angular/core';
import { App } from '../components/app/app'; import { App } from '../components/app/app';
import { assert } from '../util/util';
/** @private */
export const GESTURE_GO_BACK_SWIPE = 'goback-swipe';
/** @private */
export const GESTURE_MENU_SWIPE = 'menu-swipe';
/** @private */
export const GESTURE_ITEM_SWIPE = 'item-swipe';
/** @private */
export const GESTURE_REFRESHER = 'refresher';
/**
* @private
*/
export const enum GesturePriority { export const enum GesturePriority {
Minimun = -10000, Minimun = -10000,
VeryLow = -20, VeryLow = -20,
@ -15,23 +31,37 @@ export const enum GesturePriority {
Refresher = Normal, Refresher = Normal,
} }
export const enum DisableScroll { /**
Never, * @private
DuringCapture, */
Always,
}
export interface GestureOptions { export interface GestureOptions {
disable?: string[]; name: string;
disableScroll?: DisableScroll; disableScroll?: boolean;
priority?: number; priority?: number;
} }
/**
* @private
*/
export interface BlockerOptions {
disableScroll?: boolean;
disable?: string[];
}
/**
* @private
*/
export const BLOCK_ALL: BlockerOptions = {
disable: [GESTURE_MENU_SWIPE, GESTURE_GO_BACK_SWIPE],
disableScroll: true
};
/** /**
* @private * @private
*/ */
@Injectable() @Injectable()
export class GestureController { export class GestureController {
private id: number = 1; private id: number = 1;
private requestedStart: { [eventId: number]: number } = {}; private requestedStart: { [eventId: number]: number } = {};
private disabledGestures: { [eventName: string]: Set<number> } = {}; private disabledGestures: { [eventName: string]: Set<number> } = {};
@ -40,8 +70,21 @@ export class GestureController {
constructor(@Inject(forwardRef(() => App)) private _app: App) { } constructor(@Inject(forwardRef(() => App)) private _app: App) { }
create(name: string, opts: GestureOptions = {}): GestureDelegate { createGesture(opts: GestureOptions): GestureDelegate {
return new GestureDelegate(name, this.newID(), this, opts); if (!opts.name) {
throw new Error('name is undefined');
}
return new GestureDelegate(opts.name, this.newID(), this,
opts.priority || 0,
!!opts.disableScroll
);
}
createBlocker(opts: BlockerOptions = {}): BlockerDelegate {
return new BlockerDelegate(this.newID(), this,
opts.disable,
!!opts.disableScroll
);
} }
newID(): number { newID(): number {
@ -127,6 +170,7 @@ export class GestureController {
} }
if (this.isDisabled(gestureName)) { if (this.isDisabled(gestureName)) {
console.debug('GestureController: Disabled', gestureName);
return false; return false;
} }
return true; return true;
@ -154,33 +198,18 @@ export class GestureController {
* @private * @private
*/ */
export class GestureDelegate { export class GestureDelegate {
private disable: string[];
private disableScroll: DisableScroll;
public priority: number = 0;
constructor( constructor(
private name: string, private name: string,
private id: number, private id: number,
private controller: GestureController, private controller: GestureController,
opts: GestureOptions private priority: number,
) { private disableScroll: boolean
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 { canStart(): boolean {
if (!this.controller) { if (!this.controller) {
assert(false, 'delegate was destroyed');
return false; return false;
} }
return this.controller.canStart(this.name); return this.controller.canStart(this.name);
@ -188,6 +217,7 @@ export class GestureDelegate {
start(): boolean { start(): boolean {
if (!this.controller) { if (!this.controller) {
assert(false, 'delegate was destroyed');
return false; return false;
} }
return this.controller.start(this.name, this.id, this.priority); return this.controller.start(this.name, this.id, this.priority);
@ -195,10 +225,11 @@ export class GestureDelegate {
capture(): boolean { capture(): boolean {
if (!this.controller) { if (!this.controller) {
assert(false, 'delegate was destroyed');
return false; return false;
} }
let captured = this.controller.capture(this.name, this.id, this.priority); let captured = this.controller.capture(this.name, this.id, this.priority);
if (captured && this.disableScroll === DisableScroll.DuringCapture) { if (captured && this.disableScroll) {
this.controller.disableScroll(this.id); this.controller.disableScroll(this.id);
} }
return captured; return captured;
@ -206,26 +237,70 @@ export class GestureDelegate {
release() { release() {
if (!this.controller) { if (!this.controller) {
assert(false, 'delegate was destroyed');
return; return;
} }
this.controller.release(this.id); this.controller.release(this.id);
if (this.disableScroll === DisableScroll.DuringCapture) { if (this.disableScroll) {
this.controller.enableScroll(this.id); this.controller.enableScroll(this.id);
} }
} }
destroy() { destroy() {
if (!this.controller) {
return;
}
this.release(); this.release();
this.controller = null;
for (let disabled of this.disable) { }
this.controller.enableGesture(disabled, this.id); }
}
if (this.disableScroll === DisableScroll.Always) { /**
this.controller.enableScroll(this.id); * @private
} */
export class BlockerDelegate {
blocked: boolean = false;
constructor(
private id: number,
private controller: GestureController,
private disable: string[],
private disableScroll: boolean
) { }
block() {
if (!this.controller) {
assert(false, 'delegate was destroyed');
return;
}
if (this.disable) {
this.disable.forEach(gesture => {
this.controller.disableGesture(gesture, this.id);
});
}
if (this.disableScroll) {
this.controller.disableScroll(this.id);
}
this.blocked = true;
}
unblock() {
if (!this.controller) {
assert(false, 'delegate was destroyed');
return;
}
if (this.disable) {
this.disable.forEach(gesture => {
this.controller.enableGesture(gesture, this.id);
});
}
if (this.disableScroll) {
this.controller.enableScroll(this.id);
}
this.blocked = false;
}
destroy() {
this.unblock();
this.controller = null; this.controller = null;
} }
} }

View File

@ -1,4 +1,4 @@
import { GestureController, DisableScroll } from '../gesture-controller'; import { GestureController, GestureOptions } from '../gesture-controller';
describe('gesture controller', () => { describe('gesture controller', () => {
it('should create an instance of GestureController', () => { it('should create an instance of GestureController', () => {
@ -80,41 +80,51 @@ describe('gesture controller', () => {
it('should initialize a delegate without options', () => { it('should throw error if initializing without a name a gesture delegate without options', () => {
let c = new GestureController(null); let c = new GestureController(null);
let g = c.create('event'); expect(() => {
let a = {};
c.createGesture(<GestureOptions>a);
}).toThrowError();
});
it('should initialize without options', () => {
let c = new GestureController(null);
let g = c.createGesture({
name: 'event',
});
expect(g['name']).toEqual('event'); expect(g['name']).toEqual('event');
expect(g.priority).toEqual(0); expect(g['priority']).toEqual(0);
expect(g['disable']).toEqual([]); expect(g['disableScroll']).toEqual(false);
expect(g['disableScroll']).toEqual(DisableScroll.Never);
expect(g['controller']).toEqual(c); expect(g['controller']).toEqual(c);
expect(g['id']).toEqual(1); expect(g['id']).toEqual(1);
let g2 = c.create('event2'); let g2 = c.createGesture({ name: 'event2' });
expect(g2['id']).toEqual(2); expect(g2['id']).toEqual(2);
}); });
it('should initialize a delegate with options', () => { it('should initialize a delegate with options', () => {
let c = new GestureController(null); let c = new GestureController(null);
let g = c.create('swipe', { let g = c.createGesture({
name: 'swipe',
priority: -123, priority: -123,
disableScroll: DisableScroll.DuringCapture, disableScroll: true,
disable: ['event2']
}); });
expect(g['name']).toEqual('swipe'); expect(g['name']).toEqual('swipe');
expect(g.priority).toEqual(-123); expect(g['priority']).toEqual(-123);
expect(g['disable']).toEqual(['event2']); expect(g['disableScroll']).toEqual(true);
expect(g['disableScroll']).toEqual(DisableScroll.DuringCapture);
expect(g['controller']).toEqual(c); expect(g['controller']).toEqual(c);
expect(g['id']).toEqual(1); expect(g['id']).toEqual(1);
}); });
it('should test if several gestures can be started', () => { it('should test if several gestures can be started', () => {
let c = new GestureController(null); let c = new GestureController(null);
let g1 = c.create('swipe'); let g1 = c.createGesture({ name: 'swipe' });
let g2 = c.create('swipe1', {priority: 3}); let g2 = c.createGesture({name: 'swipe1', priority: 3});
let g3 = c.create('swipe2', {priority: 4}); let g3 = c.createGesture({name: 'swipe2', priority: 4});
for (var i = 0; i < 10; i++) { for (var i = 0; i < 10; i++) {
expect(g1.start()).toEqual(true); expect(g1.start()).toEqual(true);
@ -137,6 +147,7 @@ describe('gesture controller', () => {
expect(g1.start()).toEqual(true); expect(g1.start()).toEqual(true);
expect(g2.start()).toEqual(true); expect(g2.start()).toEqual(true);
g3.destroy(); g3.destroy();
expect(g3['controller']).toBeNull();
expect(c['requestedStart']).toEqual({ expect(c['requestedStart']).toEqual({
1: 0, 1: 0,
@ -147,11 +158,11 @@ describe('gesture controller', () => {
it('should test if several gestures try to capture at the same time', () => { it('should test if several gestures try to capture at the same time', () => {
let c = new GestureController(null); let c = new GestureController(null);
let g1 = c.create('swipe1'); let g1 = c.createGesture({name: 'swipe1'});
let g2 = c.create('swipe2', { priority: 2 }); let g2 = c.createGesture({name: 'swipe2', priority: 2 });
let g3 = c.create('swipe3', { priority: 3 }); let g3 = c.createGesture({name: 'swipe3', priority: 3 });
let g4 = c.create('swipe4', { priority: 4 }); let g4 = c.createGesture({name: 'swipe4', priority: 4 });
let g5 = c.create('swipe5', { priority: 5 }); let g5 = c.createGesture({name: 'swipe5', priority: 5 });
// Low priority capture() returns false // Low priority capture() returns false
expect(g2.start()).toEqual(true); expect(g2.start()).toEqual(true);
@ -196,90 +207,13 @@ describe('gesture controller', () => {
expect(g1.capture()).toEqual(true); expect(g1.capture()).toEqual(true);
}); });
it('should destroy correctly', () => {
let c = new GestureController(null);
let g = c.create('swipe', {
priority: 123,
disableScroll: DisableScroll.Always,
disable: ['event2']
});
expect(c.isScrollDisabled()).toEqual(true);
// Capturing
expect(g.capture()).toEqual(true);
expect(c.isCaptured()).toEqual(true);
expect(g.capture()).toEqual(false);
expect(c.isScrollDisabled()).toEqual(true);
// Releasing
g.release();
expect(c.isCaptured()).toEqual(false);
expect(c.isScrollDisabled()).toEqual(true);
expect(g.capture()).toEqual(true);
expect(c.isCaptured()).toEqual(true);
// Destroying
g.destroy();
expect(c.isCaptured()).toEqual(false);
expect(g['controller']).toBeNull();
// it should return false and not crash
expect(g.start()).toEqual(false);
expect(g.capture()).toEqual(false);
g.release();
});
it('should disable some events', () => {
let c = new GestureController(null);
let goback = c.create('goback');
expect(goback.canStart()).toEqual(true);
let g2 = c.create('goback2');
expect(g2.canStart()).toEqual(true);
let g3 = c.create('swipe', {
disable: ['range', 'goback', 'something']
});
let g4 = c.create('swipe2', {
disable: ['range']
});
// it should be noop
g3.release();
// goback is disabled
expect(c.isDisabled('range')).toEqual(true);
expect(c.isDisabled('goback')).toEqual(true);
expect(c.isDisabled('something')).toEqual(true);
expect(c.isDisabled('goback2')).toEqual(false);
expect(goback.canStart()).toEqual(false);
expect(goback.start()).toEqual(false);
expect(goback.capture()).toEqual(false);
expect(g3.canStart()).toEqual(true);
// Once g3 is destroyed, goback and something should be enabled
g3.destroy();
expect(c.isDisabled('range')).toEqual(true);
expect(c.isDisabled('goback')).toEqual(false);
expect(c.isDisabled('something')).toEqual(false);
expect(g3.canStart()).toEqual(false);
// Once g4 is destroyed, range is also enabled
g4.destroy();
expect(c.isDisabled('range')).toEqual(false);
expect(g4.canStart()).toEqual(false);
});
it('should disable scrolling on capture', () => { it('should disable scrolling on capture', () => {
let c = new GestureController(null); let c = new GestureController(null);
let g = c.create('goback', { let g = c.createGesture({
disableScroll: DisableScroll.DuringCapture, name: 'goback',
disableScroll: true,
}); });
let g1 = c.create('swipe'); let g1 = c.createGesture({ name: 'swipe' });
g.start(); g.start();
expect(c.isScrollDisabled()).toEqual(false); expect(c.isScrollDisabled()).toEqual(false);
@ -294,19 +228,116 @@ describe('gesture controller', () => {
g.capture(); g.capture();
expect(c.isScrollDisabled()).toEqual(true); 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(); g.destroy();
expect(c.isScrollDisabled()).toEqual(false); expect(c.isScrollDisabled()).toEqual(false);
}); });
describe('BlockerDelegate', () => {
it('create one', () => {
let c = new GestureController(null);
let b = c.createBlocker({
disableScroll: true,
disable: ['event1', 'event2', 'event3', 'event4']
});
expect(b['disable']).toEqual(['event1', 'event2', 'event3', 'event4']);
expect(b['disableScroll']).toEqual(true);
expect(b['controller']).toEqual(c);
expect(b['id']).toEqual(1);
let b2 = c.createBlocker({
disable: ['event2', 'event3', 'event4', 'event5']
});
expect(b2['disable']).toEqual(['event2', 'event3', 'event4', 'event5']);
expect(b2['disableScroll']).toEqual(false);
expect(b2['controller']).toEqual(c);
expect(b2['id']).toEqual(2);
expect(c.isDisabled('event1')).toBeFalsy();
expect(c.isDisabled('event2')).toBeFalsy();
expect(c.isDisabled('event3')).toBeFalsy();
expect(c.isDisabled('event4')).toBeFalsy();
expect(c.isDisabled('event5')).toBeFalsy();
b.block();
b.block();
expect(c.isDisabled('event1')).toBeTruthy();
expect(c.isDisabled('event2')).toBeTruthy();
expect(c.isDisabled('event3')).toBeTruthy();
expect(c.isDisabled('event4')).toBeTruthy();
expect(c.isDisabled('event5')).toBeFalsy();
b2.block();
b2.block();
b2.block();
expect(c.isDisabled('event1')).toBeTruthy();
expect(c.isDisabled('event2')).toBeTruthy();
expect(c.isDisabled('event3')).toBeTruthy();
expect(c.isDisabled('event4')).toBeTruthy();
expect(c.isDisabled('event5')).toBeTruthy();
b.unblock();
expect(c.isDisabled('event1')).toBeFalsy();
expect(c.isDisabled('event2')).toBeTruthy();
expect(c.isDisabled('event3')).toBeTruthy();
expect(c.isDisabled('event4')).toBeTruthy();
expect(c.isDisabled('event5')).toBeTruthy();
b2.destroy();
expect(b2['controller']).toBeNull();
expect(c.isDisabled('event1')).toBeFalsy();
expect(c.isDisabled('event2')).toBeFalsy();
expect(c.isDisabled('event3')).toBeFalsy();
expect(c.isDisabled('event4')).toBeFalsy();
expect(c.isDisabled('event5')).toBeFalsy();
});
it('should disable some events', () => {
let c = new GestureController(null);
let goback = c.createGesture({ name: 'goback' });
expect(goback.canStart()).toEqual(true);
let g2 = c.createGesture({ name: 'goback2' });
expect(g2.canStart()).toEqual(true);
let g3 = c.createBlocker({
disable: ['range', 'goback', 'something']
});
let g4 = c.createBlocker({
disable: ['range']
});
g3.block();
g4.block();
// 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);
// 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);
// Once g4 is destroyed, range is also enabled
g4.unblock();
expect(c.isDisabled('range')).toEqual(false);
});
});
}); });

View File

@ -1,11 +1,15 @@
import { assign, swipeShouldReset } from '../util/util'; import { assign, swipeShouldReset } from '../util/util';
import { GestureController, GesturePriority, DisableScroll } from '../gestures/gesture-controller'; import { GestureController, GesturePriority, GESTURE_GO_BACK_SWIPE } from '../gestures/gesture-controller';
import { NavControllerBase } from './nav-controller-base'; import { NavControllerBase } from './nav-controller-base';
import { SlideData } from '../gestures/slide-gesture'; import { SlideData } from '../gestures/slide-gesture';
import { SlideEdgeGesture } from '../gestures/slide-edge-gesture'; import { SlideEdgeGesture } from '../gestures/slide-edge-gesture';
import { NativeRafDebouncer } from '../util/debouncer'; import { NativeRafDebouncer } from '../util/debouncer';
/**
* @private
*/
export class SwipeBackGesture extends SlideEdgeGesture { export class SwipeBackGesture extends SlideEdgeGesture {
constructor( constructor(
private _nav: NavControllerBase, private _nav: NavControllerBase,
element: HTMLElement, element: HTMLElement,
@ -17,11 +21,11 @@ export class SwipeBackGesture extends SlideEdgeGesture {
maxEdgeStart: 75, maxEdgeStart: 75,
zone: false, zone: false,
threshold: 0, threshold: 0,
maxAngle: 40,
debouncer: new NativeRafDebouncer(), debouncer: new NativeRafDebouncer(),
gesture: gestureCtlr.create('goback-swipe', { gesture: gestureCtlr.createGesture({
name: GESTURE_GO_BACK_SWIPE,
priority: GesturePriority.GoBackSwipe, priority: GesturePriority.GoBackSwipe,
disableScroll: DisableScroll.DuringCapture disableScroll: true
}) })
}, options)); }, options));
} }

View File

@ -110,6 +110,7 @@ export const PLATFORM_CONFIGS: {[key: string]: PlatformConfig} = {
swipeBackThreshold: 40, swipeBackThreshold: 40,
tapPolyfill: isIOSDevice, tapPolyfill: isIOSDevice,
virtualScrollEventAssist: !(window.indexedDB), virtualScrollEventAssist: !(window.indexedDB),
disableScrollAssist: isIOSDevice,
}, },
isMatch(p: Platform) { isMatch(p: Platform) {
return p.isPlatformMatch('ios', ['iphone', 'ipad', 'ipod'], ['windows phone']); return p.isPlatformMatch('ios', ['iphone', 'ipad', 'ipod'], ['windows phone']);

View File

@ -374,8 +374,11 @@ export const mockTabs = function(app?: App): Tabs {
return new Tabs(null, null, app, config, elementRef, platform, renderer, linker); return new Tabs(null, null, app, config, elementRef, platform, renderer, linker);
}; };
export const mockMenu = function(): Menu {
return new Menu(null, null, null, null, null, null, null, null); export const mockMenu = function (): Menu {
let app = mockApp();
let gestureCtrl = new GestureController(app);
return new Menu(null, null, null, null, null, null, null, gestureCtrl);
}; };
export const mockDeepLinkConfig = function(links?: any[]): DeepLinkConfig { export const mockDeepLinkConfig = function(links?: any[]): DeepLinkConfig {