diff --git a/src/components/action-sheet/action-sheet-component.ts b/src/components/action-sheet/action-sheet-component.ts index d523dfa9f5c..1e04bc9bc26 100644 --- a/src/components/action-sheet/action-sheet-component.ts +++ b/src/components/action-sheet/action-sheet-component.ts @@ -5,7 +5,8 @@ import { Form } from '../../util/form'; import { Key } from '../../util/key'; import { NavParams } from '../../navigation/nav-params'; import { ViewController } from '../../navigation/view-controller'; - +import { BlockerDelegate, GestureController, BLOCK_ALL } from '../../gestures/gesture-controller'; +import { assert } from '../../util/util'; /** * @private @@ -53,15 +54,18 @@ export class ActionSheetCmp { hdrId: string; id: number; mode: string; + gestureBlocker: BlockerDelegate; constructor( private _viewCtrl: ViewController, private _config: Config, private _elementRef: ElementRef, private _form: Form, + gestureCtrl: GestureController, params: NavParams, renderer: Renderer ) { + this.gestureBlocker = gestureCtrl.createBlocker(BLOCK_ALL); this.d = params.data; this.mode = _config.get('mode'); renderer.setElementClass(_elementRef.nativeElement, `action-sheet-${this.mode}`, true); @@ -110,6 +114,14 @@ export class ActionSheetCmp { this.d.buttons = buttons; } + ionViewWillEnter() { + this.gestureBlocker.block(); + } + + ionViewDidLeave() { + this.gestureBlocker.unblock(); + } + ionViewDidEnter() { this._form.focusOut(); @@ -166,6 +178,11 @@ export class ActionSheetCmp { dismiss(role: any): Promise { return this._viewCtrl.dismiss(null, role); } + + ngOnDestroy() { + assert(this.gestureBlocker.blocked === false, 'gesture blocker must be already unblocked'); + this.gestureBlocker.destroy(); + } } let actionSheetIds = -1; diff --git a/src/components/alert/alert-component.ts b/src/components/alert/alert-component.ts index 3a83fb9141c..59f14da03ed 100644 --- a/src/components/alert/alert-component.ts +++ b/src/components/alert/alert-component.ts @@ -1,11 +1,11 @@ import { Component, ElementRef, HostListener, Renderer, ViewEncapsulation } from '@angular/core'; import { Config } from '../../config/config'; -import { isPresent } from '../../util/util'; +import { isPresent, assert } from '../../util/util'; import { Key } from '../../util/key'; import { NavParams } from '../../navigation/nav-params'; import { ViewController } from '../../navigation/view-controller'; - +import { GestureController, BlockerDelegate, BLOCK_ALL } from '../../gestures/gesture-controller'; /** * @private @@ -86,14 +86,18 @@ export class AlertCmp { msgId: string; subHdrId: string; mode: string; + gestureBlocker: BlockerDelegate; constructor( public _viewCtrl: ViewController, public _elementRef: ElementRef, public _config: Config, + gestureCtrl: GestureController, params: NavParams, renderer: Renderer ) { + // gesture blocker is used to disable gestures dynamically + this.gestureBlocker = gestureCtrl.createBlocker(BLOCK_ALL); this.d = params.data; this.mode = _config.get('mode'); 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']) keyUp(ev: KeyboardEvent) { 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) { if (!this.enabled) { return; @@ -293,6 +305,11 @@ export class AlertCmp { }); return values; } + + ngOnDestroy() { + assert(this.gestureBlocker.blocked === false, 'gesture blocker must be already unblocked'); + this.gestureBlocker.destroy(); + } } let alertIds = -1; diff --git a/src/components/app/app-root.ts b/src/components/app/app-root.ts index 29dc797d837..210fccdd999 100644 --- a/src/components/app/app-root.ts +++ b/src/components/app/app-root.ts @@ -5,6 +5,7 @@ import { Config } from '../../config/config'; import { Ion } from '../ion'; import { OverlayPortal } from '../nav/overlay-portal'; import { Platform } from '../../platform/platform'; +import { nativeTimeout } from '../../util/dom'; export const AppRootToken = new OpaqueToken('USERROOT'); @@ -23,6 +24,8 @@ export const AppRootToken = new OpaqueToken('USERROOT'); }) export class IonicApp extends Ion implements OnInit { + private _stopScrollPlugin: any; + private _rafId: number; @ViewChild('viewport', {read: ViewContainerRef}) _viewport: ViewContainerRef; @ViewChild('modalPortal', { read: OverlayPortal }) _modalPortal: OverlayPortal; @@ -45,6 +48,7 @@ export class IonicApp extends Ion implements OnInit { super(config, elementRef, renderer); // register with App that this is Ionic's appRoot component. tada! app._appRoot = this; + this._stopScrollPlugin = window['IonicStopScroll']; } ngOnInit() { @@ -109,7 +113,26 @@ export class IonicApp extends Ion implements OnInit { * @private */ _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 { + if (this._stopScrollPlugin) { + return new Promise((resolve, reject) => { + this._stopScrollPlugin.stop(() => resolve(true)); + }); + } else { + return Promise.resolve(false); + } } } diff --git a/src/components/app/app.ts b/src/components/app/app.ts index 4719271b7f8..e317521c927 100644 --- a/src/components/app/app.ts +++ b/src/components/app/app.ts @@ -22,7 +22,7 @@ export class App { private _title: string = ''; private _titleSrv: Title = new Title(); private _rootNav: NavController = null; - private _canDisableScroll: boolean; + private _disableScrollAssist: boolean; /** * @private @@ -71,7 +71,7 @@ export class App { // listen for hardware back button events // register this back button action with a default priority _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. */ setScrollDisabled(disableScroll: boolean) { - if (this._canDisableScroll) { + if (this._disableScrollAssist) { this._appRoot._disableScroll(disableScroll); } } diff --git a/src/components/backdrop/backdrop.scss b/src/components/backdrop/backdrop.scss index 76a7d2ae294..0799e69204b 100644 --- a/src/components/backdrop/backdrop.scss +++ b/src/components/backdrop/backdrop.scss @@ -19,7 +19,3 @@ ion-backdrop { opacity: .01; transform: translateZ(0); } - -ion-backdrop.hide-backdrop { - display: none; -} diff --git a/src/components/backdrop/backdrop.ts b/src/components/backdrop/backdrop.ts index 28ee9fcb42d..1f8d5c7ea7c 100644 --- a/src/components/backdrop/backdrop.ts +++ b/src/components/backdrop/backdrop.ts @@ -1,8 +1,4 @@ -import { Directive, ElementRef, Input, Renderer } from '@angular/core'; - -import { GestureController } from '../../gestures/gesture-controller'; -import { isTrueProperty } from '../../util/util'; - +import { Directive, ElementRef, Renderer } from '@angular/core'; /** * @private @@ -16,26 +12,11 @@ import { isTrueProperty } from '../../util/util'; }, }) export class Backdrop { - private _gestureID: number = null; - @Input() disableScroll = true; constructor( - private _gestureCtrl: GestureController, private _elementRef: ElementRef, - 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); - } - } + private _renderer: Renderer + ) { } getNativeElement(): HTMLElement { return this._elementRef.nativeElement; diff --git a/src/components/content/content.scss b/src/components/content/content.scss index 7acd661b7ed..79a20b4ad07 100644 --- a/src/components/content/content.scss +++ b/src/components/content/content.scss @@ -51,7 +51,7 @@ ion-content.js-scroll > .scroll-content { will-change: initial; } -.disable-scroll .ion-page .scroll-content { +.disable-scroll .ion-page { pointer-events: none; } diff --git a/src/components/item/item-sliding-gesture.ts b/src/components/item/item-sliding-gesture.ts index 1e0f66d6479..64e506fe91c 100644 --- a/src/components/item/item-sliding-gesture.ts +++ b/src/components/item/item-sliding-gesture.ts @@ -1,14 +1,14 @@ import { ItemSliding } from './item-sliding'; 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 { pointerCoord } from '../../util/dom'; import { NativeRafDebouncer } from '../../util/debouncer'; -const DRAG_THRESHOLD = 10; -const MAX_ATTACK_ANGLE = 20; - +/** + * @private + */ export class ItemSlidingGesture extends PanGesture { private preSelectedContainer: ItemSliding = null; @@ -17,14 +17,16 @@ export class ItemSlidingGesture extends PanGesture { private firstCoordX: number; private firstTimestamp: number; - constructor(public list: List) { + constructor(public list: List, gestureCtrl: GestureController) { super(list.getNativeElement(), { - maxAngle: MAX_ATTACK_ANGLE, - threshold: DRAG_THRESHOLD, + maxAngle: 20, + threshold: 10, zone: false, debouncer: new NativeRafDebouncer(), - gesture: list._gestureCtrl.create('item-sliding', { + gesture: gestureCtrl.createGesture({ + name: GESTURE_ITEM_SWIPE, priority: GesturePriority.SlidingItem, + disableScroll: false // TODO: set true }) }); } diff --git a/src/components/list/list.ts b/src/components/list/list.ts index 6d8d1fc7963..136ba56eece 100644 --- a/src/components/list/list.ts +++ b/src/components/list/list.ts @@ -95,7 +95,7 @@ export class List extends Ion { } else if (!this._slidingGesture) { console.debug('enableSlidingItems'); - this._slidingGesture = new ItemSlidingGesture(this); + this._slidingGesture = new ItemSlidingGesture(this, this._gestureCtrl); this._slidingGesture.listen(); } } diff --git a/src/components/loading/loading-component.ts b/src/components/loading/loading-component.ts index 347c767b311..a86ed891532 100644 --- a/src/components/loading/loading-component.ts +++ b/src/components/loading/loading-component.ts @@ -1,10 +1,11 @@ import { Component, ElementRef, Renderer, ViewEncapsulation } from '@angular/core'; 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 { ViewController } from '../../navigation/view-controller'; import { LoadingOptions } from './loading-options'; +import { BlockerDelegate, GestureController, BLOCK_ALL } from '../../gestures/gesture-controller'; /** * @private @@ -12,7 +13,7 @@ import { LoadingOptions } from './loading-options'; @Component({ selector: 'ion-loading', template: - '' + + '' + '
' + '
' + '' + @@ -29,14 +30,17 @@ export class LoadingCmp { id: number; showSpinner: boolean; durationTimeout: number; + gestureBlocker: BlockerDelegate; constructor( private _viewCtrl: ViewController, private _config: Config, private _elementRef: ElementRef, + gestureCtrl: GestureController, params: NavParams, renderer: Renderer ) { + this.gestureBlocker = gestureCtrl.createBlocker(BLOCK_ALL); this.d = params.data; 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'; } + ionViewWillEnter() { + this.gestureBlocker.block(); + } + + ionViewDidLeave() { + this.gestureBlocker.unblock(); + } + ionViewDidEnter() { let activeElement: any = document.activeElement; - if (document.activeElement) { - activeElement.blur(); - } + activeElement && activeElement.blur(); // If there is a duration, dismiss after that amount of time if ( this.d && this.d.duration ) { - this.durationTimeout = ( setTimeout( () => { - this.dismiss('backdrop'); - }, this.d.duration)); + this.durationTimeout = setTimeout(() => this.dismiss('backdrop'), this.d.duration); } } @@ -83,6 +91,11 @@ export class LoadingCmp { } return this._viewCtrl.dismiss(null, role); } + + ngOnDestroy() { + assert(this.gestureBlocker.blocked === false, 'gesture blocker must be already unblocked'); + this.gestureBlocker.destroy(); + } } let loadingIds = -1; diff --git a/src/components/menu/menu-gestures.ts b/src/components/menu/menu-gestures.ts index 21d6e02c2d3..7a3ae1c41fd 100644 --- a/src/components/menu/menu-gestures.ts +++ b/src/components/menu/menu-gestures.ts @@ -2,7 +2,7 @@ import { Menu } from './menu'; import { SlideEdgeGesture } from '../../gestures/slide-edge-gesture'; import { SlideData } from '../../gestures/slide-gesture'; 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'; /** @@ -14,17 +14,19 @@ export class MenuContentGesture extends SlideEdgeGesture { public menu: Menu, contentEle: HTMLElement, gestureCtrl: GestureController, - options: any = {}) { + options: any = {} + ) { super(contentEle, assign({ direction: 'x', edge: menu.side, threshold: 0, maxEdgeStart: menu.maxEdgeStart || 50, - maxAngle: 40, zone: false, debouncer: new NativeRafDebouncer(), - gesture: gestureCtrl.create('menu-swipe', { + gesture: gestureCtrl.createGesture({ + name: GESTURE_MENU_SWIPE, priority: GesturePriority.MenuSwipe, + disableScroll: true }) }, options)); } @@ -52,13 +54,6 @@ export class MenuContentGesture extends SlideEdgeGesture { let z = (this.menu.side === 'right' ? slide.min : slide.max); let stepValue = (slide.distance / z); - console.debug('menu gesture, onSlide', this.menu.side, - 'distance', slide.distance, - 'min', slide.min, - 'max', slide.max, - 'z', z, - 'stepValue', stepValue); - this.menu.swipeProgress(stepValue); } diff --git a/src/components/menu/menu.ts b/src/components/menu/menu.ts index 215956dbcf1..12e3afe2cb4 100644 --- a/src/components/menu/menu.ts +++ b/src/components/menu/menu.ts @@ -8,7 +8,7 @@ import { MenuContentGesture } from './menu-gestures'; import { MenuController } from './menu-controller'; import { MenuType } from './menu-types'; import { Platform } from '../../platform/platform'; -import { GestureController } from '../../gestures/gesture-controller'; +import { BlockerDelegate, GestureController, GESTURE_GO_BACK_SWIPE } from '../../gestures/gesture-controller'; import { UIEventManager } from '../../util/ui-event-manager'; import { Content } from '../content/content'; @@ -181,7 +181,7 @@ import { Content } from '../content/content'; selector: 'ion-menu', template: '' + - '', + '', host: { 'role': 'navigation' }, @@ -198,7 +198,7 @@ export class Menu { private _isPers: boolean = false; private _init: boolean = false; private _events: UIEventManager = new UIEventManager(); - private _gestureID: number = 0; + private _gestureBlocker: BlockerDelegate; /** * @private @@ -305,9 +305,9 @@ export class Menu { private _zone: NgZone, private _gestureCtrl: GestureController ) { - if (_gestureCtrl) { - this._gestureID = _gestureCtrl.newID(); - } + this._gestureBlocker = _gestureCtrl.createBlocker({ + disable: [GESTURE_GO_BACK_SWIPE] + }); } /** @@ -503,7 +503,7 @@ export class Menu { this._events.unlistenAll(); if (isOpen) { // Disable swipe to go back gesture - this._gestureCtrl.disableGesture('goback-swipe', this._gestureID); + this._gestureBlocker.block(); this._cntEle.classList.add('menu-content-open'); let callback = this.onBackdropClick.bind(this); @@ -519,7 +519,7 @@ export class Menu { } else { // Enable swipe to go back gesture - this._gestureCtrl.enableGesture('goback-swipe', this._gestureID); + this._gestureBlocker.unblock(); this._cntEle.classList.remove('menu-content-open'); this.setElementClass('show-menu', false); diff --git a/src/components/menu/test/basic/app-module.ts b/src/components/menu/test/basic/app-module.ts index f71fc9be597..5757bae7f87 100644 --- a/src/components/menu/test/basic/app-module.ts +++ b/src/components/menu/test/basic/app-module.ts @@ -1,12 +1,16 @@ 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({ templateUrl: 'page1.html' }) export class Page1 { - constructor(public navCtrl: NavController, public alertCtrl: AlertController) {} + constructor( + public navCtrl: NavController, + public alertCtrl: AlertController, + public modalCtrl: ModalController + ) { } presentAlert() { let alert = this.alertCtrl.create({ @@ -18,11 +22,24 @@ export class Page1 { alert.present(); } + presentModal() { + let modal = this.modalCtrl.create(Modal); + modal.present(); + } + goToPage2() { this.navCtrl.push(Page2); } } +@Component({templateUrl: 'modal.html'}) +export class Modal { + constructor(public viewController: ViewController) {} + close() { + this.viewController.dismiss(); + } +} + @Component({templateUrl: 'page3.html'}) export class Page3 {} @@ -106,7 +123,8 @@ export class E2EApp { E2EPage, Page1, Page2, - Page3 + Page3, + Modal ], imports: [ IonicModule.forRoot(E2EApp) @@ -117,7 +135,8 @@ export class E2EApp { E2EPage, Page1, Page2, - Page3 + Page3, + Modal ] }) export class AppModule {} diff --git a/src/components/menu/test/basic/modal.html b/src/components/menu/test/basic/modal.html new file mode 100644 index 00000000000..bfa5f352b23 --- /dev/null +++ b/src/components/menu/test/basic/modal.html @@ -0,0 +1,20 @@ + + + + + MODAL + + + + + + + + +

+ +

+ +
+ +
diff --git a/src/components/menu/test/basic/page1.html b/src/components/menu/test/basic/page1.html index c340af5241a..80fcac47149 100644 --- a/src/components/menu/test/basic/page1.html +++ b/src/components/menu/test/basic/page1.html @@ -64,6 +64,10 @@

Page 1

+

+ +

+

diff --git a/src/components/modal/modal-component.ts b/src/components/modal/modal-component.ts index 642646a83e9..a034ba6d8cb 100644 --- a/src/components/modal/modal-component.ts +++ b/src/components/modal/modal-component.ts @@ -3,7 +3,8 @@ import { Component, ComponentFactoryResolver, HostListener, Renderer, ViewChild, import { Key } from '../../util/key'; import { NavParams } from '../../navigation/nav-params'; 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 @@ -11,7 +12,7 @@ import { ViewController } from '../../navigation/view-controller'; @Component({ selector: 'ion-modal', template: - '' + + '' + '' @@ -20,13 +21,20 @@ export class ModalCmp { @ViewChild('viewport', { read: ViewContainerRef }) _viewport: ViewContainerRef; - /** @private */ _bdDismiss: boolean; - - /** @private */ _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; } @@ -46,9 +54,20 @@ export class ModalCmp { this._setCssClass(componentRef, 'ion-page'); this._setCssClass(componentRef, 'show-page'); 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 */ _setCssClass(componentRef: any, className: string) { this._renderer.setElementClass(componentRef.location.nativeElement, className, true); @@ -66,4 +85,9 @@ export class ModalCmp { this._bdClick(); } } + + ngOnDestroy() { + assert(this._gestureBlocker.blocked === false, 'gesture blocker must be already unblocked'); + this._gestureBlocker.destroy(); + } } diff --git a/src/components/nav/test/basic/app-module.ts b/src/components/nav/test/basic/app-module.ts index 5aea111e4ae..3f94ffd1156 100644 --- a/src/components/nav/test/basic/app-module.ts +++ b/src/components/nav/test/basic/app-module.ts @@ -834,7 +834,9 @@ export const deepLinkConfig: DeepLinkConfig = { TabItemPage ], imports: [ - IonicModule.forRoot(E2EApp, null, deepLinkConfig) + IonicModule.forRoot(E2EApp, { + swipeBackEnabled: true + }, deepLinkConfig) ], bootstrap: [IonicApp], entryComponents: [ diff --git a/src/components/picker/picker-component.ts b/src/components/picker/picker-component.ts index 0dd8899631d..11db09db7a5 100644 --- a/src/components/picker/picker-component.ts +++ b/src/components/picker/picker-component.ts @@ -2,7 +2,7 @@ import { Component, ElementRef, EventEmitter, Input, HostListener, NgZone, Outpu import { DomSanitizer } from '@angular/platform-browser'; 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 { Key } from '../../util/key'; import { NavParams } from '../../navigation/nav-params'; @@ -12,6 +12,7 @@ import { Haptic } from '../../util/haptic'; import { UIEventManager } from '../../util/ui-event-manager'; import { ViewController } from '../../navigation/view-controller'; import { Debouncer, NativeRafDebouncer } from '../../util/debouncer'; +import { GestureController, BlockerDelegate, BLOCK_ALL } from '../../gestures/gesture-controller'; /** * @private @@ -454,14 +455,17 @@ export class PickerCmp { lastClick: number; id: number; mode: string; + _gestureBlocker: BlockerDelegate; constructor( private _viewCtrl: ViewController, private _elementRef: ElementRef, private _config: Config, + gestureCtrl: GestureController, params: NavParams, renderer: Renderer ) { + this._gestureBlocker = gestureCtrl.createBlocker(BLOCK_ALL); this.d = params.data; this.mode = _config.get('mode'); renderer.setElementClass(_elementRef.nativeElement, `picker-${this.mode}`, true); @@ -523,6 +527,14 @@ export class PickerCmp { }); } + ionViewWillEnter() { + this._gestureBlocker.block(); + } + + ionViewDidLeave() { + this._gestureBlocker.unblock(); + } + refresh() { this._cols.forEach(column => { column.refresh(); @@ -617,6 +629,12 @@ export class PickerCmp { }); return selected; } + + ngOnDestroy() { + assert(this._gestureBlocker.blocked === false, 'gesture blocker must be already unblocked'); + this._gestureBlocker.destroy(); + + } } let pickerIds = -1; diff --git a/src/components/popover/popover-component.ts b/src/components/popover/popover-component.ts index 47cb678b9dc..49102cdd187 100644 --- a/src/components/popover/popover-component.ts +++ b/src/components/popover/popover-component.ts @@ -4,7 +4,8 @@ import { Config } from '../../config/config'; import { Key } from '../../util/key'; import { NavParams } from '../../navigation/nav-params'; import { ViewController } from '../../navigation/view-controller'; - +import { GestureController, BlockerDelegate, BLOCK_ALL } from '../../gestures/gesture-controller'; +import { assert } from '../../util/util'; /** * @private @@ -12,7 +13,7 @@ import { ViewController } from '../../navigation/view-controller'; @Component({ selector: 'ion-popover', template: - '' + + '' + '
' + '
' + '
' + @@ -32,10 +33,9 @@ export class PopoverCmp { enableBackdropDismiss?: boolean; }; - /** @private */ _enabled: boolean; + _gestureBlocker: BlockerDelegate; - /** @private */ id: number; constructor( @@ -44,8 +44,10 @@ export class PopoverCmp { public _renderer: Renderer, public _config: Config, public _navParams: NavParams, - public _viewCtrl: ViewController + public _viewCtrl: ViewController, + gestureCtrl: GestureController, ) { + this._gestureBlocker = gestureCtrl.createBlocker(BLOCK_ALL); this.d = _navParams.data.opts; _renderer.setElementClass(_elementRef.nativeElement, `popover-${_config.get('mode')}`, true); @@ -62,13 +64,11 @@ export class PopoverCmp { ionViewPreLoad() { let activeElement: any = document.activeElement; - if (document.activeElement) { - activeElement.blur(); - } + activeElement && activeElement.blur(); + this._load(this._navParams.data.component); } - /** @private */ _load(component: any) { if (component) { const componentFactory = this._cfr.resolveComponentFactory(component); @@ -76,12 +76,23 @@ export class PopoverCmp { // ******** DOM WRITE **************** const componentRef = this._viewport.createComponent(componentFactory, this._viewport.length, this._viewport.parentInjector, []); this._viewCtrl._setInstance(componentRef.instance); - 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) { this._renderer.setElementClass(componentRef.location.nativeElement, className, true); } @@ -98,6 +109,11 @@ export class PopoverCmp { this._bdClick(); } } + + ngOnDestroy() { + assert(this._gestureBlocker.blocked === false, 'gesture blocker must be already unblocked'); + this._gestureBlocker.destroy(); + } } let popoverIds = -1; diff --git a/src/components/refresher/refresher.ts b/src/components/refresher/refresher.ts index bfc465b833a..f9464c0ebc8 100644 --- a/src/components/refresher/refresher.ts +++ b/src/components/refresher/refresher.ts @@ -2,7 +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 { GestureController, GestureDelegate, GesturePriority, GESTURE_REFRESHER } from '../../gestures/gesture-controller'; import { isTrueProperty } from '../../util/util'; 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) { _content.setElementClass('has-refresher', true); - this._gesture = gestureCtrl.create('refresher', { + this._gesture = gestureCtrl.createGesture({ + name: GESTURE_REFRESHER, priority: GesturePriority.Refresher, }); } diff --git a/src/components/toolbar/toolbar-button.scss b/src/components/toolbar/toolbar-button.scss index e7549abce8d..bb9dca7f309 100644 --- a/src/components/toolbar/toolbar-button.scss +++ b/src/components/toolbar/toolbar-button.scss @@ -53,10 +53,10 @@ .back-button { display: none; +} - &.show-back-button { - display: inline-block; - } +.back-button.show-back-button { + display: inline-block; } .back-button-text { diff --git a/src/gestures/gesture-controller.ts b/src/gestures/gesture-controller.ts index 13287b6b65c..647b4639f53 100644 --- a/src/gestures/gesture-controller.ts +++ b/src/gestures/gesture-controller.ts @@ -1,6 +1,22 @@ import { forwardRef, Inject, Injectable } from '@angular/core'; 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 { Minimun = -10000, VeryLow = -20, @@ -15,23 +31,37 @@ export const enum GesturePriority { Refresher = Normal, } -export const enum DisableScroll { - Never, - DuringCapture, - Always, +/** +* @private +*/ +export interface GestureOptions { + name: string; + disableScroll?: boolean; + priority?: number; } -export interface GestureOptions { +/** +* @private +*/ +export interface BlockerOptions { + disableScroll?: boolean; disable?: string[]; - disableScroll?: DisableScroll; - priority?: number; } +/** +* @private +*/ +export const BLOCK_ALL: BlockerOptions = { + disable: [GESTURE_MENU_SWIPE, GESTURE_GO_BACK_SWIPE], + disableScroll: true +}; + /** * @private */ @Injectable() export class GestureController { + private id: number = 1; private requestedStart: { [eventId: number]: number } = {}; private disabledGestures: { [eventName: string]: Set } = {}; @@ -40,8 +70,21 @@ export class GestureController { constructor(@Inject(forwardRef(() => App)) private _app: App) { } - create(name: string, opts: GestureOptions = {}): GestureDelegate { - return new GestureDelegate(name, this.newID(), this, opts); + createGesture(opts: GestureOptions): GestureDelegate { + 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 { @@ -127,6 +170,7 @@ export class GestureController { } if (this.isDisabled(gestureName)) { + console.debug('GestureController: Disabled', gestureName); return false; } return true; @@ -154,33 +198,18 @@ export class GestureController { * @private */ 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); - } - } + private priority: number, + private disableScroll: boolean + ) { } canStart(): boolean { if (!this.controller) { + assert(false, 'delegate was destroyed'); return false; } return this.controller.canStart(this.name); @@ -188,6 +217,7 @@ export class GestureDelegate { start(): boolean { if (!this.controller) { + assert(false, 'delegate was destroyed'); return false; } return this.controller.start(this.name, this.id, this.priority); @@ -195,10 +225,11 @@ export class GestureDelegate { capture(): boolean { if (!this.controller) { + assert(false, 'delegate was destroyed'); return false; } 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); } return captured; @@ -206,26 +237,70 @@ export class GestureDelegate { release() { if (!this.controller) { + assert(false, 'delegate was destroyed'); return; } this.controller.release(this.id); - if (this.disableScroll === DisableScroll.DuringCapture) { + if (this.disableScroll) { this.controller.enableScroll(this.id); } } destroy() { + this.release(); + this.controller = null; + } +} + +/** +* @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; } - this.release(); + if (this.disable) { + this.disable.forEach(gesture => { + this.controller.disableGesture(gesture, this.id); + }); + } + + if (this.disableScroll) { + this.controller.disableScroll(this.id); + } + this.blocked = true; + } - for (let disabled of this.disable) { - this.controller.enableGesture(disabled, this.id); + 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 === DisableScroll.Always) { + if (this.disableScroll) { this.controller.enableScroll(this.id); } + this.blocked = false; + } + + destroy() { + this.unblock(); this.controller = null; } } diff --git a/src/gestures/test/gesture-controller.spec.ts b/src/gestures/test/gesture-controller.spec.ts index d19962b150e..792e2a1a79c 100644 --- a/src/gestures/test/gesture-controller.spec.ts +++ b/src/gestures/test/gesture-controller.spec.ts @@ -1,4 +1,4 @@ -import { GestureController, DisableScroll } from '../gesture-controller'; +import { GestureController, GestureOptions } from '../gesture-controller'; describe('gesture controller', () => { 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 g = c.create('event'); + expect(() => { + let a = {}; + c.createGesture(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.priority).toEqual(0); - expect(g['disable']).toEqual([]); - expect(g['disableScroll']).toEqual(DisableScroll.Never); + expect(g['priority']).toEqual(0); + expect(g['disableScroll']).toEqual(false); expect(g['controller']).toEqual(c); expect(g['id']).toEqual(1); - let g2 = c.create('event2'); + let g2 = c.createGesture({ name: 'event2' }); expect(g2['id']).toEqual(2); }); it('should initialize a delegate with options', () => { let c = new GestureController(null); - let g = c.create('swipe', { + let g = c.createGesture({ + name: 'swipe', priority: -123, - disableScroll: DisableScroll.DuringCapture, - disable: ['event2'] + disableScroll: true, }); expect(g['name']).toEqual('swipe'); - expect(g.priority).toEqual(-123); - expect(g['disable']).toEqual(['event2']); - expect(g['disableScroll']).toEqual(DisableScroll.DuringCapture); + expect(g['priority']).toEqual(-123); + expect(g['disableScroll']).toEqual(true); expect(g['controller']).toEqual(c); expect(g['id']).toEqual(1); }); it('should test if several gestures can be started', () => { let c = new GestureController(null); - let g1 = c.create('swipe'); - let g2 = c.create('swipe1', {priority: 3}); - let g3 = c.create('swipe2', {priority: 4}); + let g1 = c.createGesture({ name: 'swipe' }); + let g2 = c.createGesture({name: 'swipe1', priority: 3}); + let g3 = c.createGesture({name: 'swipe2', priority: 4}); for (var i = 0; i < 10; i++) { expect(g1.start()).toEqual(true); @@ -137,6 +147,7 @@ describe('gesture controller', () => { expect(g1.start()).toEqual(true); expect(g2.start()).toEqual(true); g3.destroy(); + expect(g3['controller']).toBeNull(); expect(c['requestedStart']).toEqual({ 1: 0, @@ -147,11 +158,11 @@ describe('gesture controller', () => { it('should test if several gestures try to capture at the same time', () => { let c = new GestureController(null); - let g1 = c.create('swipe1'); - let g2 = c.create('swipe2', { priority: 2 }); - let g3 = c.create('swipe3', { priority: 3 }); - let g4 = c.create('swipe4', { priority: 4 }); - let g5 = c.create('swipe5', { priority: 5 }); + let g1 = c.createGesture({name: 'swipe1'}); + let g2 = c.createGesture({name: 'swipe2', priority: 2 }); + let g3 = c.createGesture({name: 'swipe3', priority: 3 }); + let g4 = c.createGesture({name: 'swipe4', priority: 4 }); + let g5 = c.createGesture({name: 'swipe5', priority: 5 }); // Low priority capture() returns false expect(g2.start()).toEqual(true); @@ -196,90 +207,13 @@ describe('gesture controller', () => { expect(g1.capture()).toEqual(true); }); - - it('should destroy correctly', () => { - let c = new GestureController(null); - let g = c.create('swipe', { - priority: 123, - disableScroll: DisableScroll.Always, - disable: ['event2'] - }); - expect(c.isScrollDisabled()).toEqual(true); - - // Capturing - expect(g.capture()).toEqual(true); - expect(c.isCaptured()).toEqual(true); - expect(g.capture()).toEqual(false); - expect(c.isScrollDisabled()).toEqual(true); - - // Releasing - g.release(); - expect(c.isCaptured()).toEqual(false); - expect(c.isScrollDisabled()).toEqual(true); - expect(g.capture()).toEqual(true); - expect(c.isCaptured()).toEqual(true); - - // Destroying - g.destroy(); - expect(c.isCaptured()).toEqual(false); - expect(g['controller']).toBeNull(); - - // it should return false and not crash - expect(g.start()).toEqual(false); - expect(g.capture()).toEqual(false); - g.release(); - }); - - - it('should disable some events', () => { - let c = new GestureController(null); - - let goback = c.create('goback'); - expect(goback.canStart()).toEqual(true); - - let g2 = c.create('goback2'); - expect(g2.canStart()).toEqual(true); - - let g3 = c.create('swipe', { - disable: ['range', 'goback', 'something'] - }); - - let g4 = c.create('swipe2', { - disable: ['range'] - }); - - // it should be noop - g3.release(); - - // goback is disabled - expect(c.isDisabled('range')).toEqual(true); - expect(c.isDisabled('goback')).toEqual(true); - expect(c.isDisabled('something')).toEqual(true); - expect(c.isDisabled('goback2')).toEqual(false); - expect(goback.canStart()).toEqual(false); - expect(goback.start()).toEqual(false); - expect(goback.capture()).toEqual(false); - expect(g3.canStart()).toEqual(true); - - // Once g3 is destroyed, goback and something should be enabled - g3.destroy(); - expect(c.isDisabled('range')).toEqual(true); - expect(c.isDisabled('goback')).toEqual(false); - expect(c.isDisabled('something')).toEqual(false); - expect(g3.canStart()).toEqual(false); - - // Once g4 is destroyed, range is also enabled - g4.destroy(); - expect(c.isDisabled('range')).toEqual(false); - expect(g4.canStart()).toEqual(false); - }); - it('should disable scrolling on capture', () => { let c = new GestureController(null); - let g = c.create('goback', { - disableScroll: DisableScroll.DuringCapture, + let g = c.createGesture({ + name: 'goback', + disableScroll: true, }); - let g1 = c.create('swipe'); + let g1 = c.createGesture({ name: 'swipe' }); g.start(); expect(c.isScrollDisabled()).toEqual(false); @@ -294,19 +228,116 @@ describe('gesture controller', () => { g.capture(); expect(c.isScrollDisabled()).toEqual(true); - let g2 = c.create('swipe2', { - disableScroll: DisableScroll.Always, + g.destroy(); + 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(); }); - g.release(); - expect(c.isScrollDisabled()).toEqual(true); - g2.destroy(); - expect(c.isScrollDisabled()).toEqual(false); - g.capture(); - expect(c.isScrollDisabled()).toEqual(true); + it('should disable some events', () => { + let c = new GestureController(null); - g.destroy(); - expect(c.isScrollDisabled()).toEqual(false); + 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); + }); }); }); + diff --git a/src/navigation/swipe-back.ts b/src/navigation/swipe-back.ts index d511035d083..44674c1205c 100644 --- a/src/navigation/swipe-back.ts +++ b/src/navigation/swipe-back.ts @@ -1,11 +1,15 @@ 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 { SlideData } from '../gestures/slide-gesture'; import { SlideEdgeGesture } from '../gestures/slide-edge-gesture'; import { NativeRafDebouncer } from '../util/debouncer'; +/** + * @private + */ export class SwipeBackGesture extends SlideEdgeGesture { + constructor( private _nav: NavControllerBase, element: HTMLElement, @@ -17,11 +21,11 @@ export class SwipeBackGesture extends SlideEdgeGesture { maxEdgeStart: 75, zone: false, threshold: 0, - maxAngle: 40, debouncer: new NativeRafDebouncer(), - gesture: gestureCtlr.create('goback-swipe', { + gesture: gestureCtlr.createGesture({ + name: GESTURE_GO_BACK_SWIPE, priority: GesturePriority.GoBackSwipe, - disableScroll: DisableScroll.DuringCapture + disableScroll: true }) }, options)); } diff --git a/src/platform/platform-registry.ts b/src/platform/platform-registry.ts index f87edad9bb5..b2761c8bd68 100644 --- a/src/platform/platform-registry.ts +++ b/src/platform/platform-registry.ts @@ -110,6 +110,7 @@ export const PLATFORM_CONFIGS: {[key: string]: PlatformConfig} = { swipeBackThreshold: 40, tapPolyfill: isIOSDevice, virtualScrollEventAssist: !(window.indexedDB), + disableScrollAssist: isIOSDevice, }, isMatch(p: Platform) { return p.isPlatformMatch('ios', ['iphone', 'ipad', 'ipod'], ['windows phone']); diff --git a/src/util/mock-providers.ts b/src/util/mock-providers.ts index 43578b91086..d08177bc38b 100644 --- a/src/util/mock-providers.ts +++ b/src/util/mock-providers.ts @@ -374,8 +374,11 @@ export const mockTabs = function(app?: App): Tabs { 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 {