fix(menu): open/close race condition

fixes #7629 #8001
This commit is contained in:
Manu Mtz.-Almeida
2016-09-14 22:49:11 +02:00
committed by Adam Bradley
parent 6506cd5982
commit 8585427fb4
4 changed files with 109 additions and 95 deletions

View File

@ -24,7 +24,7 @@ export class MenuContentGesture extends SlideEdgeGesture {
canStart(ev: any): boolean { canStart(ev: any): boolean {
let menu = this.menu; let menu = this.menu;
if (!menu.enabled || !menu.swipeEnabled) { if (!menu.canSwipe()) {
return false; return false;
} }
if (menu.isOpen) { if (menu.isOpen) {
@ -36,7 +36,7 @@ export class MenuContentGesture extends SlideEdgeGesture {
} }
// Set CSS, then wait one frame for it to apply before sliding starts // Set CSS, then wait one frame for it to apply before sliding starts
onSlideBeforeStart(slide: SlideData, ev: any) { onSlideBeforeStart(ev: any) {
console.debug('menu gesture, onSlideBeforeStart', this.menu.side); console.debug('menu gesture, onSlideBeforeStart', this.menu.side);
this.menu.swipeStart(); this.menu.swipeStart();
} }
@ -44,7 +44,15 @@ export class MenuContentGesture extends SlideEdgeGesture {
onSlide(slide: SlideData, ev: any) { onSlide(slide: SlideData, ev: any) {
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);
console.debug(
'menu gesture, onSlide', this.menu.side,
'distance', slide.distance,
'min', slide.min,
'max', slide.max,
'z', z,
'stepValue', stepValue);
ev.preventDefault(); ev.preventDefault();
this.menu.swipeProgress(stepValue); this.menu.swipeProgress(stepValue);
} }
@ -70,28 +78,25 @@ export class MenuContentGesture extends SlideEdgeGesture {
'shouldCompleteLeft', shouldCompleteLeft, 'shouldCompleteLeft', shouldCompleteLeft,
'shouldCompleteRight', shouldCompleteRight, 'shouldCompleteRight', shouldCompleteRight,
'currentStepValue', currentStepValue); 'currentStepValue', currentStepValue);
this.menu.swipeEnd(shouldCompleteLeft, shouldCompleteRight, currentStepValue); this.menu.swipeEnd(shouldCompleteLeft, shouldCompleteRight, currentStepValue);
} }
getElementStartPos(slide: SlideData, ev: any) { getElementStartPos(slide: SlideData, ev: any) {
if (this.menu.side === 'right') { if (this.menu.side === 'right') {
// right menu
return this.menu.isOpen ? slide.min : slide.max; return this.menu.isOpen ? slide.min : slide.max;
} }
// left menu // left menu
return this.menu.isOpen ? slide.max : slide.min; return this.menu.isOpen ? slide.max : slide.min;
} }
getSlideBoundaries(): {min: number, max: number} { getSlideBoundaries(): {min: number, max: number} {
if (this.menu.side === 'right') { if (this.menu.side === 'right') {
// right menu
return { return {
min: -this.menu.width(), min: -this.menu.width(),
max: 0 max: 0
}; };
} }
// left menu // left menu
return { return {
min: 0, min: 0,

View File

@ -186,16 +186,17 @@ import { GestureController } from '../../gestures/gesture-controller';
encapsulation: ViewEncapsulation.None, encapsulation: ViewEncapsulation.None,
}) })
export class Menu { export class Menu {
private _preventTime: number = 0;
private _cntEle: HTMLElement; private _cntEle: HTMLElement;
private _cntGesture: MenuContentGesture; private _cntGesture: MenuContentGesture;
private _type: MenuType; private _type: MenuType;
private _resizeUnreg: Function; private _resizeUnreg: Function;
private _isEnabled: boolean = true; private _isEnabled: boolean = true;
private _isSwipeEnabled: boolean = true; private _isSwipeEnabled: boolean = true;
private _isAnimating: boolean = false;
private _isPers: boolean = false; private _isPers: boolean = false;
private _init: boolean = false; private _init: boolean = false;
/** /**
* @private * @private
*/ */
@ -378,21 +379,20 @@ export class Menu {
* @private * @private
*/ */
private _setListeners() { private _setListeners() {
let self = this; if (!this._init) {
return;
}
if (self._init) { // only listen/unlisten if the menu has initialized
// only listen/unlisten if the menu has initialized if (this._isEnabled && this._isSwipeEnabled && !this._cntGesture.isListening) {
// should listen, but is not currently listening
console.debug('menu, gesture listen', this.side);
this._cntGesture.listen();
if (self._isEnabled && self._isSwipeEnabled && !self._cntGesture.isListening) { } else if (this._cntGesture.isListening && (!this._isEnabled || !this._isSwipeEnabled)) {
// should listen, but is not currently listening // should not listen, but is currently listening
console.debug('menu, gesture listen', self.side); console.debug('menu, gesture unlisten', this.side);
self._cntGesture.listen(); this._cntGesture.unlisten();
} else if (self._cntGesture.isListening && (!self._isEnabled || !self._isSwipeEnabled)) {
// should not listen, but is currently listening
console.debug('menu, gesture unlisten', self.side);
self._cntGesture.unlisten();
}
} }
} }
@ -416,7 +416,7 @@ export class Menu {
setOpen(shouldOpen: boolean, animated: boolean = true): Promise<boolean> { setOpen(shouldOpen: boolean, animated: boolean = true): Promise<boolean> {
// _isPrevented is used to prevent unwanted opening/closing after swiping open/close // _isPrevented is used to prevent unwanted opening/closing after swiping open/close
// or swiping open the menu while pressing down on the MenuToggle button // or swiping open the menu while pressing down on the MenuToggle button
if ((shouldOpen && this.isOpen) || this._isPrevented()) { if ((shouldOpen && this.isOpen) || !this._isEnabled || this._isAnimating) {
return Promise.resolve(this.isOpen); return Promise.resolve(this.isOpen);
} }
@ -430,12 +430,20 @@ export class Menu {
}); });
} }
/**
* @private
*/
canSwipe(): boolean {
return this._isEnabled && this._isSwipeEnabled && !this._isAnimating;
}
/** /**
* @private * @private
*/ */
swipeStart() { swipeStart() {
// user started swiping the menu open/close // user started swiping the menu open/close
if (this._isEnabled && this._isSwipeEnabled && !this._isPrevented()) { if (this.canSwipe()) {
this._before(); this._before();
this._getType().setProgressStart(this.isOpen); this._getType().setProgressStart(this.isOpen);
} }
@ -446,46 +454,42 @@ export class Menu {
*/ */
swipeProgress(stepValue: number) { swipeProgress(stepValue: number) {
// user actively dragging the menu // user actively dragging the menu
if (this._isEnabled && this._isSwipeEnabled) { if (!this._isAnimating) {
this._prevent(); return;
this._getType().setProgessStep(stepValue);
this.ionDrag.emit(stepValue);
} }
this._getType().setProgessStep(stepValue);
this.ionDrag.emit(stepValue);
} }
/** /**
* @private * @private
*/ */
swipeEnd(shouldCompleteLeft: boolean, shouldCompleteRight: boolean, stepValue: number) { swipeEnd(shouldCompleteLeft: boolean, shouldCompleteRight: boolean, stepValue: number) {
// user has finished dragging the menu if (!this._isAnimating) {
if (this._isEnabled && this._isSwipeEnabled) { return;
this._prevent();
let opening = !this.isOpen;
let shouldComplete = false;
if (opening) {
shouldComplete = (this.side === 'right') ? shouldCompleteLeft : shouldCompleteRight;
} else {
shouldComplete = (this.side === 'right') ? shouldCompleteRight : shouldCompleteLeft;
}
this._getType().setProgressEnd(shouldComplete, stepValue, (isOpen: boolean) => {
console.debug('menu, swipeEnd', this.side);
this._after(isOpen);
});
} }
// user has finished dragging the menu
let opening = !this.isOpen;
let shouldComplete = false;
if (opening) {
shouldComplete = (this.side === 'right') ? shouldCompleteLeft : shouldCompleteRight;
} else {
shouldComplete = (this.side === 'right') ? shouldCompleteRight : shouldCompleteLeft;
}
this._getType().setProgressEnd(shouldComplete, stepValue, (isOpen: boolean) => {
console.debug('menu, swipeEnd', this.side);
this._after(isOpen);
});
} }
private _before() { private _before() {
// this places the menu into the correct location before it animates in // this places the menu into the correct location before it animates in
// this css class doesn't actually kick off any animations // this css class doesn't actually kick off any animations
if (this._isEnabled) { this.getNativeElement().classList.add('show-menu');
this.getNativeElement().classList.add('show-menu'); this.getBackdropElement().classList.add('show-backdrop');
this.getBackdropElement().classList.add('show-backdrop'); this._keyboard.close();
this._isAnimating = true;
this._prevent();
this._keyboard.close();
}
} }
private _after(isOpen: boolean) { private _after(isOpen: boolean) {
@ -493,37 +497,24 @@ export class Menu {
// only add listeners/css if it's enabled and isOpen // only add listeners/css if it's enabled and isOpen
// and only remove listeners/css if it's not open // and only remove listeners/css if it's not open
// emit opened/closed events // emit opened/closed events
if ((this._isEnabled && isOpen) || !isOpen) { this.isOpen = isOpen;
this._prevent(); this._isAnimating = false;
this.isOpen = isOpen; (<any>this._cntEle.classList)[isOpen ? 'add' : 'remove']('menu-content-open');
(<any>this._cntEle.classList)[isOpen ? 'add' : 'remove']('menu-content-open'); this._cntEle.removeEventListener('click', this.onContentClick);
this._cntEle.removeEventListener('click', this.onContentClick); if (isOpen) {
this._cntEle.addEventListener('click', this.onContentClick);
this.ionOpen.emit(true);
if (isOpen) { } else {
this._cntEle.addEventListener('click', this.onContentClick); this.getNativeElement().classList.remove('show-menu');
this.ionOpen.emit(true); this.getBackdropElement().classList.remove('show-backdrop');
this.ionClose.emit(true);
} else {
this.getNativeElement().classList.remove('show-menu');
this.getBackdropElement().classList.remove('show-backdrop');
this.ionClose.emit(true);
}
} }
} }
private _prevent() {
// used to prevent unwanted opening/closing after swiping open/close
// or swiping open the menu while pressing down on the MenuToggle
this._preventTime = Date.now() + 20;
}
private _isPrevented() {
return this._preventTime > Date.now();
}
/** /**
* @private * @private
*/ */
@ -564,6 +555,9 @@ export class Menu {
.map(m => m.enabled = false); .map(m => m.enabled = false);
} }
// TODO
// what happens if menu is disabled while swipping?
return this; return this;
} }
@ -572,6 +566,8 @@ export class Menu {
*/ */
swipeEnable(shouldEnable: boolean): Menu { swipeEnable(shouldEnable: boolean): Menu {
this.swipeEnabled = shouldEnable; this.swipeEnabled = shouldEnable;
// TODO
// what happens if menu swipe is disabled while swipping?
return this; return this;
} }
@ -619,7 +615,11 @@ export class Menu {
this._cntGesture && this._cntGesture.destroy(); this._cntGesture && this._cntGesture.destroy();
this._type && this._type.destroy(); this._type && this._type.destroy();
this._resizeUnreg && this._resizeUnreg(); this._resizeUnreg && this._resizeUnreg();
this._cntGesture = null;
this._type = null;
this._cntEle = null; this._cntEle = null;
this._resizeUnreg = null;
} }
} }

View File

@ -33,23 +33,32 @@ export class SlideGesture extends PanGesture {
} }
onDragStart(ev: any) { onDragStart(ev: any) {
this.slide = {}; this.onSlideBeforeStart(ev);
this.onSlideBeforeStart(this.slide, ev);
let {min, max} = this.getSlideBoundaries(this.slide, ev);
let coord = <any>pointerCoord(ev); let coord = <any>pointerCoord(ev);
let pos = coord[this.direction];
this.slide = {
min: 0,
max: 0,
pointerStartPos: pos,
pos: pos,
timestamp: Date.now(),
elementStartPos: 0,
started: true,
delta: 0,
distance: 0,
velocity: 0,
};
let {min, max} = this.getSlideBoundaries(this.slide, ev);
this.slide.min = min; this.slide.min = min;
this.slide.max = max; this.slide.max = max;
this.slide.elementStartPos = this.getElementStartPos(this.slide, ev); this.slide.elementStartPos = this.getElementStartPos(this.slide, ev);
this.slide.pos = this.slide.pointerStartPos = coord[this.direction];
this.slide.timestamp = Date.now();
this.slide.started = true;
this.slide.velocity = 0;
this.onSlideStart(this.slide, ev); this.onSlideStart(this.slide, ev);
} }
onDragMove(ev: any) { onDragMove(ev: any) {
let slide = this.slide; let slide: SlideData = this.slide;
let coord = <any>pointerCoord(ev); let coord = <any>pointerCoord(ev);
let newPos = coord[this.direction]; let newPos = coord[this.direction];
let newTimestamp = Date.now(); let newTimestamp = Date.now();
@ -74,7 +83,7 @@ export class SlideGesture extends PanGesture {
this.slide = null; this.slide = null;
} }
onSlideBeforeStart(slide?: SlideData, ev?: any): void {} onSlideBeforeStart(ev?: any): void {}
onSlideStart(slide?: SlideData, ev?: any): void {} onSlideStart(slide?: SlideData, ev?: any): void {}
onSlide(slide?: SlideData, ev?: any): void {} onSlide(slide?: SlideData, ev?: any): void {}
onSlideEnd(slide?: SlideData, ev?: any): void {} onSlideEnd(slide?: SlideData, ev?: any): void {}
@ -84,14 +93,14 @@ export class SlideGesture extends PanGesture {
* @private * @private
*/ */
export interface SlideData { export interface SlideData {
min?: number; min: number;
max?: number; max: number;
distance?: number; distance: number;
delta?: number; delta: number;
started?: boolean; started: boolean;
pos?: any; pos: any;
timestamp?: number; timestamp: number;
pointerStartPos?: number; pointerStartPos: number;
elementStartPos?: number; elementStartPos: number;
velocity?: number; velocity: number;
} }

View File

@ -33,7 +33,7 @@ export class SwipeBackGesture extends SlideEdgeGesture {
} }
onSlideBeforeStart(slideData: SlideData, ev: any) { onSlideBeforeStart(ev: any) {
console.debug('swipeBack, onSlideBeforeStart', ev.type); console.debug('swipeBack, onSlideBeforeStart', ev.type);
this._nav.swipeBackStart(); this._nav.swipeBackStart();
} }