diff --git a/packages/core/package-lock.json b/packages/core/package-lock.json index c04c71d754..ef0ed8967e 100644 --- a/packages/core/package-lock.json +++ b/packages/core/package-lock.json @@ -1501,14 +1501,6 @@ } } }, - "string_decoder": { - "version": "1.0.1", - "bundled": true, - "dev": true, - "requires": { - "safe-buffer": "5.0.1" - } - }, "string-width": { "version": "1.0.2", "bundled": true, @@ -1519,6 +1511,14 @@ "strip-ansi": "3.0.1" } }, + "string_decoder": { + "version": "1.0.1", + "bundled": true, + "dev": true, + "requires": { + "safe-buffer": "5.0.1" + } + }, "stringstream": { "version": "0.0.5", "bundled": true, @@ -3136,15 +3136,6 @@ "integrity": "sha1-NbCYdbT/SfJqd35QmzCQoyJr8ks=", "dev": true }, - "string_decoder": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.0.3.tgz", - "integrity": "sha512-4AH6Z5fzNNBcH+6XDMfA/BTt87skxqJlO0lAh3Dker5zThcAxG6mKz+iGu308UKoPPQ8Dcqx/4JhujzltRa+hQ==", - "dev": true, - "requires": { - "safe-buffer": "5.1.1" - } - }, "string-template": { "version": "0.2.1", "resolved": "https://registry.npmjs.org/string-template/-/string-template-0.2.1.tgz", @@ -3162,6 +3153,15 @@ "strip-ansi": "3.0.1" } }, + "string_decoder": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.0.3.tgz", + "integrity": "sha512-4AH6Z5fzNNBcH+6XDMfA/BTt87skxqJlO0lAh3Dker5zThcAxG6mKz+iGu308UKoPPQ8Dcqx/4JhujzltRa+hQ==", + "dev": true, + "requires": { + "safe-buffer": "5.1.1" + } + }, "stringstream": { "version": "0.0.5", "resolved": "https://registry.npmjs.org/stringstream/-/stringstream-0.0.5.tgz", diff --git a/packages/core/src/components/animation-controller/animator.tsx b/packages/core/src/components/animation-controller/animator.tsx index 4e783e5686..ca3fdc9f9a 100644 --- a/packages/core/src/components/animation-controller/animator.tsx +++ b/packages/core/src/components/animation-controller/animator.tsx @@ -5,6 +5,7 @@ import { transitionEnd } from './transition-end'; export class Animator { + private _afterAddClasses: string[]; private _afterRemoveClasses: string[]; private _afterStyles: { [property: string]: any; }; @@ -639,7 +640,7 @@ export class Animator { // flip the number if we're going in reverse if (this._isReverse) { - stepValue = ((stepValue * -1) + 1); + stepValue = 1 - stepValue; } var i = 0; var j = 0; @@ -1023,12 +1024,6 @@ export class Animator { children[i].progressStep(stepValue); } - if (this._isReverse) { - // if the animation is going in reverse then - // flip the step value: 0 becomes 1, 1 becomes 0 - stepValue = ((stepValue * -1) + 1); - } - // ******** DOM WRITE **************** this._progress(stepValue); } diff --git a/packages/core/src/components/gesture/gesture.tsx b/packages/core/src/components/gesture/gesture.tsx index c697d0319c..a6fe7e0877 100644 --- a/packages/core/src/components/gesture/gesture.tsx +++ b/packages/core/src/components/gesture/gesture.tsx @@ -1,13 +1,13 @@ -import { applyStyles, getElementReference, pointerCoordX, pointerCoordY } from '../../utils/helpers'; +import { applyStyles, getElementReference, ElementRef, updateDetail, assert } from '../../utils/helpers'; import { BlockerDelegate, GestureController, GestureDelegate, BLOCK_ALL } from '../gesture-controller/gesture-controller'; import { Component, Element, Event, EventEmitter, Listen, Prop, PropDidChange } from '@stencil/core'; import { PanRecognizer } from './recognizers'; - @Component({ tag: 'ion-gesture' }) export class Gesture { + @Element() private el: HTMLElement; private detail: GestureDetail = {}; private positions: number[] = []; @@ -18,9 +18,10 @@ export class Gesture { private hasCapturedPan = false; private hasPress = false; private hasStartedPan = false; - private requiresMove = false; + private hasFiredStart = true; private isMoveQueued = false; private blocker: BlockerDelegate; + private fireOnMoveFunc: any; @Event() private ionGestureMove: EventEmitter; @Event() private ionGestureStart: EventEmitter; @@ -28,7 +29,8 @@ export class Gesture { @Event() private ionGestureNotCaptured: EventEmitter; @Event() private ionPress: EventEmitter; - @Prop() attachTo: string = 'child'; + @Prop() enabled: boolean = true; + @Prop() attachTo: ElementRef = 'child'; @Prop() autoBlockAll: boolean = false; @Prop() block: string = null; @Prop() disableScroll: boolean = false; @@ -40,12 +42,16 @@ export class Gesture { @Prop() type: string = 'pan'; @Prop() canStart: GestureCallback; + @Prop() onWillStart: (_: GestureDetail) => Promise; @Prop() onStart: GestureCallback; @Prop() onMove: GestureCallback; @Prop() onEnd: GestureCallback; @Prop() onPress: GestureCallback; @Prop() notCaptured: GestureCallback; + constructor() { + this.fireOnMoveFunc = this.fireOnMove.bind(this); + } ionViewDidLoad() { // in this case, we already know the GestureController and Gesture are already @@ -55,17 +61,13 @@ export class Gesture { this.gesture = this.ctrl.createGesture(this.gestureName, this.gesturePriority, this.disableScroll); const types = this.type.replace(/\s/g, '').toLowerCase().split(','); - if (types.indexOf('pan') > -1) { this.pan = new PanRecognizer(this.direction, this.threshold, this.maxAngle); - this.requiresMove = true; } this.hasPress = (types.indexOf('press') > -1); + this.enabledChange(true); if (this.pan || this.hasPress) { - Context.enableListener(this, 'touchstart', true, this.attachTo); - Context.enableListener(this, 'mousedown', true, this.attachTo); - Context.dom.write(() => { applyStyles(getElementReference(this.el, this.attachTo), GESTURE_INLINE_STYLES); }); @@ -77,6 +79,19 @@ export class Gesture { } } + @PropDidChange('enabled') + enabledChange(isEnabled: boolean) { + if (!this.gesture) { + return; + } + if (this.pan || this.hasPress) { + Context.enableListener(this, 'touchstart', isEnabled, this.attachTo); + Context.enableListener(this, 'mousedown', isEnabled, this.attachTo); + if (!isEnabled) { + this.abortGesture(); + } + } + } @PropDidChange('block') blockChange(block: string) { @@ -94,10 +109,12 @@ export class Gesture { onTouchStart(ev: TouchEvent) { this.lastTouch = now(ev); - this.enableMouse(false); - this.enableTouch(true); - - this.pointerDown(ev, this.lastTouch); + if (this.pointerDown(ev, this.lastTouch)) { + this.enableMouse(false); + this.enableTouch(true); + } else { + this.abortGesture(); + } } @@ -106,29 +123,36 @@ export class Gesture { const timeStamp = now(ev); if (this.lastTouch === 0 || (this.lastTouch + MOUSE_WAIT < timeStamp)) { - this.enableMouse(true); - this.enableTouch(false); - - this.pointerDown(ev, timeStamp); + if (this.pointerDown(ev, timeStamp)) { + this.enableMouse(true); + this.enableTouch(false); + } else { + this.abortGesture(); + } } } - private pointerDown(ev: UIEvent, timeStamp: number): boolean { - if (!this.gesture || this.hasStartedPan) { + if (!this.gesture || this.hasStartedPan || !this.hasFiredStart) { return false; } - const detail = this.detail; - detail.startX = detail.currentX = pointerCoordX(ev); - detail.startY = detail.currentY = pointerCoordY(ev); + updateDetail(ev, detail); + detail.startX = detail.currentX; + detail.startY = detail.currentY; detail.startTimeStamp = detail.timeStamp = timeStamp; detail.velocityX = detail.velocityY = detail.deltaX = detail.deltaY = 0; - detail.directionX = detail.directionY = detail.velocityDirectionX = detail.velocityDirectionY = null; detail.event = ev; this.positions.length = 0; + assert(this.hasFiredStart, 'fired start must be false'); + assert(!this.hasStartedPan, 'pan can be started at this point'); + assert(!this.hasCapturedPan, 'pan can be started at this point') + assert(!this.isMoveQueued, 'some move is still queued'); + assert(this.positions.length === 0, 'positions must be emprty'); + + // Check if gesture can start if (this.canStart && this.canStart(detail) === false) { return false; } @@ -145,11 +169,8 @@ export class Gesture { if (this.pan) { this.hasStartedPan = true; - this.hasCapturedPan = false; - this.pan.start(detail.startX, detail.startY); } - return true; } @@ -159,7 +180,6 @@ export class Gesture { @Listen('touchmove', { passive: true, enabled: false }) onTouchMove(ev: TouchEvent) { this.lastTouch = this.detail.timeStamp = now(ev); - this.pointerMove(ev); } @@ -167,7 +187,6 @@ export class Gesture { @Listen('document:mousemove', { passive: true, enabled: false }) onMoveMove(ev: TouchEvent) { const timeStamp = now(ev); - if (this.lastTouch === 0 || (this.lastTouch + MOUSE_WAIT < timeStamp)) { this.detail.timeStamp = timeStamp; this.pointerMove(ev); @@ -175,107 +194,124 @@ export class Gesture { } private pointerMove(ev: UIEvent) { + assert(!!this.pan, 'pan must be non null'); + + if (this.hasCapturedPan) { + if (!this.isMoveQueued && this.hasFiredStart) { + this.isMoveQueued = true; + this.calcGestureData(ev); + Context.dom.write(this.fireOnMoveFunc); + } + return; + } + const detail = this.detail; this.calcGestureData(ev); - - if (this.pan) { - if (this.hasCapturedPan) { - - if (!this.isMoveQueued) { - this.isMoveQueued = true; - - Context.dom.write(() => { - this.isMoveQueued = false; - detail.type = 'pan'; - - if (this.onMove) { - this.onMove(detail); - } else { - this.ionGestureMove.emit(this.detail); - } - }); - } - - } else if (this.pan.detect(detail.currentX, detail.currentY)) { - if (this.pan.isGesture() !== 0) { - if (!this.tryToCapturePan(ev)) { - this.abortGesture(); - } + if (this.pan.detect(detail.currentX, detail.currentY)) { + if (this.pan.isGesture() !== 0) { + if (!this.tryToCapturePan(ev)) { + this.abortGesture(); } } } } + private fireOnMove() { + const detail = this.detail; + this.isMoveQueued = false; + if (this.onMove) { + this.onMove(detail); + } else { + this.ionGestureMove.emit(detail); + } + } + private calcGestureData(ev: UIEvent) { const detail = this.detail; - detail.currentX = pointerCoordX(ev); - detail.currentY = pointerCoordY(ev); - detail.deltaX = (detail.currentX - detail.startX); - detail.deltaY = (detail.currentY - detail.startY); + updateDetail(ev, detail); + + const currentX = detail.currentX; + const currentY = detail.currentY; + const timestamp = detail.timeStamp; + detail.deltaX = currentX - detail.startX; + detail.deltaY = currentY - detail.startY; detail.event = ev; - // figure out which direction we're movin' - detail.directionX = detail.velocityDirectionX = (detail.deltaX > 0 ? 'left' : (detail.deltaX < 0 ? 'right' : null)); - detail.directionY = detail.velocityDirectionY = (detail.deltaY > 0 ? 'up' : (detail.deltaY < 0 ? 'down' : null)); - + const timeRange = timestamp - 100; const positions = this.positions; - positions.push(detail.currentX, detail.currentY, detail.timeStamp); - - var endPos = (positions.length - 1); - var startPos = endPos; - var timeRange = (detail.timeStamp - 100); + let startPos = positions.length - 1; // move pointer to position measured 100ms ago - for (var i = endPos; i > 0 && positions[i] > timeRange; i -= 3) { - startPos = i; - } + for (; + startPos > 0 && positions[startPos] > timeRange; + startPos -= 3) { } - if (startPos !== endPos) { + if (startPos > 1) { // compute relative movement between these two points - var movedX = (positions[startPos - 2] - positions[endPos - 2]); - var movedY = (positions[startPos - 1] - positions[endPos - 1]); - var factor = 16.67 / (positions[endPos] - positions[startPos]); + var frequency = 1 / (positions[startPos] - timestamp); + var movedY = positions[startPos - 1] - currentY; + var movedX = positions[startPos - 2] - currentX; // based on XXms compute the movement to apply for each render step - detail.velocityX = movedX * factor; - detail.velocityY = movedY * factor; - - detail.velocityDirectionX = (movedX > 0 ? 'left' : (movedX < 0 ? 'right' : null)); - detail.velocityDirectionY = (movedY > 0 ? 'up' : (movedY < 0 ? 'down' : null)); + // velocity = space/time = s*(1/t) = s*frequency + detail.velocityX = movedX * frequency; + detail.velocityY = movedY * frequency; + } else { + detail.velocityX = 0; + detail.velocityY = 0; } + positions.push(currentX, currentY, timestamp); } private tryToCapturePan(ev: UIEvent): boolean { if (this.gesture && !this.gesture.capture()) { return false; } + this.hasCapturedPan = true; + this.hasFiredStart = false; + this.calcGestureData(ev); + if (this.onWillStart) { + this.onWillStart(this.detail).then(this.fireOnStart.bind(this)); + } else { + this.fireOnStart(); + } + return true; + } - this.detail.event = ev; - + private fireOnStart() { + assert(!this.hasFiredStart, 'has fired must be false'); if (this.onStart) { this.onStart(this.detail); } else { this.ionGestureStart.emit(this.detail); } - - this.hasCapturedPan = true; - - return true; + this.hasFiredStart = true; } private abortGesture() { - this.hasStartedPan = false; - this.hasCapturedPan = false; - - this.gesture && this.gesture.release(); - + this.reset(); this.enable(false); this.notCaptured && this.notCaptured(this.detail); } + private reset() { + this.hasCapturedPan = false; + this.hasStartedPan = false; + this.hasFiredStart = true; + this.gesture && this.gesture.release(); + } // END ************************* + @Listen('touchcancel', { passive: true, enabled: false }) + onTouchCancel(ev: TouchEvent) { + this.lastTouch = this.detail.timeStamp = now(ev); + + this.pointerUp(ev); + this.enableTouch(false); + } + + @Listen('touchend', { passive: true, enabled: false }) onTouchEnd(ev: TouchEvent) { this.lastTouch = this.detail.timeStamp = now(ev); @@ -298,47 +334,46 @@ export class Gesture { private pointerUp(ev: UIEvent) { + const hasCaptured = this.hasCapturedPan; + const hasFiredStart = this.hasFiredStart; + this.reset(); + + if (!hasFiredStart) { + return; + } const detail = this.detail; - - this.gesture && this.gesture.release(); - - detail.event = ev; - this.calcGestureData(ev); - if (this.pan) { - if (this.hasCapturedPan) { - detail.type = 'pan'; - if (this.onEnd) { - this.onEnd(detail); - } else { - this.ionGestureEnd.emit(detail); - } - - } else if (this.hasPress) { - this.detectPress(); - + // Try to capture press + if (hasCaptured) { + detail.type = 'pan'; + if (this.onEnd) { + this.onEnd(detail); } else { - if (this.notCaptured) { - this.notCaptured(detail); - } else { - this.ionGestureNotCaptured.emit(detail); - } + this.ionGestureEnd.emit(detail); } - - } else if (this.hasPress) { - this.detectPress(); + return; } - this.hasCapturedPan = false; - this.hasStartedPan = false; + // Try to capture press + if (this.hasPress && this.detectPress()) { + return; + } + + // Not captured any event + if (this.notCaptured) { + this.notCaptured(detail); + } else { + this.ionGestureNotCaptured.emit(detail); + } } - - private detectPress() { + private detectPress(): boolean { const detail = this.detail; - - if (Math.abs(detail.startX - detail.currentX) < 10 && Math.abs(detail.startY - detail.currentY) < 10) { + const vecX = detail.deltaX; + const vecY = detail.deltaY; + const dis = vecX * vecX + vecY * vecY; + if (dis < 100) { detail.type = 'press'; if (this.onPress) { @@ -346,25 +381,27 @@ export class Gesture { } else { this.ionPress.emit(detail); } + return true; } + return false; } - // ENABLE LISTENERS ************************* private enableMouse(shouldEnable: boolean) { - if (this.requiresMove) { - Context.enableListener(this, 'document:mousemove', shouldEnable); + if (this.pan) { + Context.enableListener(this, 'document:mousemove', shouldEnable, this.attachTo); } - Context.enableListener(this, 'document:mouseup', shouldEnable); + Context.enableListener(this, 'document:mouseup', shouldEnable, this.attachTo); } private enableTouch(shouldEnable: boolean) { - if (this.requiresMove) { - Context.enableListener(this, 'touchmove', shouldEnable); + if (this.pan) { + Context.enableListener(this, 'touchmove', shouldEnable, this.attachTo); } - Context.enableListener(this, 'touchend', shouldEnable); + Context.enableListener(this, 'touchcancel', shouldEnable, this.attachTo); + Context.enableListener(this, 'touchend', shouldEnable, this.attachTo); } @@ -413,10 +450,6 @@ export interface GestureDetail { velocityY?: number; deltaX?: number; deltaY?: number; - directionX?: 'left'|'right'; - directionY?: 'up'|'down'; - velocityDirectionX?: 'left'|'right'; - velocityDirectionY?: 'up'|'down'; timeStamp?: number; } diff --git a/packages/core/src/components/menu/animations/base.ts b/packages/core/src/components/menu/animations/base.ts new file mode 100644 index 0000000000..af900ec38b --- /dev/null +++ b/packages/core/src/components/menu/animations/base.ts @@ -0,0 +1,15 @@ +import { Animation } from '../../../index'; + +/** + * @hidden + * Menu Type + * Base class which is extended by the various types. Each + * type will provide their own animations for open and close + * and registers itself with Menu. + */ +export default function baseAnimation(Animation: Animation): Animation { + return new Animation() + .easing('cubic-bezier(0.0, 0.0, 0.2, 1)') + .easingReverse('cubic-bezier(0.4, 0.0, 0.6, 1)') + .duration(280); +} diff --git a/packages/core/src/components/menu/animations/overlay.ts b/packages/core/src/components/menu/animations/overlay.ts new file mode 100644 index 0000000000..8f6f6f62ed --- /dev/null +++ b/packages/core/src/components/menu/animations/overlay.ts @@ -0,0 +1,35 @@ +import { Animation, Menu } from '../../../index'; +import baseAnimation from './base'; + +/** + * @hidden + * Menu Overlay Type + * The menu slides over the content. The content + * itself, which is under the menu, does not move. + */ +export default function(Animation: Animation, _: HTMLElement, menu: Menu): Animation { + let closedX: string, openedX: string; + const width = menu.getWidth(); + if (menu.isRightSide) { + // right side + closedX = 8 + width + 'px'; + openedX = '0px'; + + } else { + // left side + closedX = -(8 + width) + 'px'; + openedX = '0px'; + } + + const menuAni = new Animation() + .addElement(menu.getMenuElement()) + .fromTo('translateX', closedX, openedX); + + const backdropApi = new Animation() + .addElement(menu.getBackdropElement()) + .fromTo('opacity', 0.01, 0.35) + + return baseAnimation(Animation) + .add(menuAni) + .add(backdropApi); +} diff --git a/packages/core/src/components/menu/animations/push.ts b/packages/core/src/components/menu/animations/push.ts new file mode 100644 index 0000000000..98d9a0f36a --- /dev/null +++ b/packages/core/src/components/menu/animations/push.ts @@ -0,0 +1,36 @@ +import { Animation, Menu } from '../../../index'; +import baseAnimation from './base'; + +/** + * @hidden + * Menu Push Type + * The content slides over to reveal the menu underneath. + * The menu itself also slides over to reveal its bad self. + */ +export default function(Animation: Animation, _: HTMLElement, menu: Menu): Animation { + + let contentOpenedX: string, menuClosedX: string, menuOpenedX: string; + const width = menu.getWidth(); + + if (menu.isRightSide) { + contentOpenedX = -width + 'px'; + menuClosedX = width + 'px'; + menuOpenedX = '0px'; + + } else { + contentOpenedX = width + 'px'; + menuOpenedX = '0px'; + menuClosedX = -width + 'px'; + } + const menuAni = new Animation() + .addElement(menu.getMenuElement()) + .fromTo('translateX', menuClosedX, menuOpenedX); + + const contentAni = new Animation() + .addElement(menu.getContentElement()) + .fromTo('translateX', '0px', contentOpenedX); + + return baseAnimation(Animation) + .add(menuAni) + .add(contentAni); +} diff --git a/packages/core/src/components/menu/animations/reveal.ts b/packages/core/src/components/menu/animations/reveal.ts new file mode 100644 index 0000000000..0ddfcce6cd --- /dev/null +++ b/packages/core/src/components/menu/animations/reveal.ts @@ -0,0 +1,19 @@ +import { Animation, Menu } from '../../../index'; +import baseAnimation from './base'; + +/** + * @hidden + * Menu Reveal Type + * The content slides over to reveal the menu underneath. + * The menu itself, which is under the content, does not move. + */ +export default function(Animation: Animation, _: HTMLElement, menu: Menu): Animation { + const openedX = (menu.getWidth() * (menu.isRightSide ? -1 : 1)) + 'px'; + + const contentOpen = new Animation() + .addElement(menu.getContentElement()) + .fromTo('translateX', '0px', openedX); + + return baseAnimation(Animation) + .add(contentOpen); +} diff --git a/packages/core/src/components/menu/menu-controller.ts b/packages/core/src/components/menu/menu-controller.ts index ddc6bb1b08..0d8924da56 100644 --- a/packages/core/src/components/menu/menu-controller.ts +++ b/packages/core/src/components/menu/menu-controller.ts @@ -1,15 +1,29 @@ -import { Menu, MenuType } from '../../index'; -import { MenuRevealType, MenuPushType, MenuOverlayType } from './menu-types'; +import { Menu, AnimationController, AnimationBuilder, Animation } from '../../index'; +import { Component, Method, Prop } from '@stencil/core'; +import MenuOverlayAnimation from './animations/overlay'; +import MenuRevealAnimation from './animations/reveal'; +import MenuPushAnimation from './animations/push'; +@Component({ + tag: 'ion-menu-controller' +}) export class MenuController { - private _menus: Array = []; - private _menuTypes: { [name: string]: new(...args: any[]) => MenuType } = {}; + + private menus: Menu[] = []; + private menuAnimations: { [name: string]: AnimationBuilder } = {}; + + @Prop({ connect: 'ion-animation-controller' }) animationCtrl: AnimationController; constructor() { - this.registerType('reveal', MenuRevealType); - this.registerType('push', MenuPushType); - this.registerType('overlay', MenuOverlayType); + this.registerAnimation('reveal', MenuRevealAnimation); + this.registerAnimation('push', MenuPushAnimation); + this.registerAnimation('overlay', MenuOverlayAnimation); + } + + @Method() + getInstance(): MenuController { + return this; } /** @@ -17,6 +31,7 @@ export class MenuController { * @param {string} [menuId] Optionally get the menu by its id, or side. * @return {Promise} returns a promise when the menu is fully opened */ + @Method() open(menuId?: string): Promise { const menu = this.get(menuId); if (menu && !this.isAnimating()) { @@ -36,6 +51,7 @@ export class MenuController { * @param {string} [menuId] Optionally get the menu by its id, or side. * @return {Promise} returns a promise when the menu is fully closed */ + @Method() close(menuId?: string): Promise { let menu: Menu; @@ -63,6 +79,7 @@ export class MenuController { * @param {string} [menuId] Optionally get the menu by its id, or side. * @return {Promise} returns a promise when the menu has been toggled */ + @Method() toggle(menuId?: string): Promise { const menu = this.get(menuId); if (menu && !this.isAnimating()) { @@ -83,6 +100,7 @@ export class MenuController { * @param {string} [menuId] Optionally get the menu by its id, or side. * @return {Menu} Returns the instance of the menu, which is useful for chaining. */ + @Method() enable(shouldEnable: boolean, menuId?: string): Menu { const menu = this.get(menuId); return (menu && menu.enable(shouldEnable)) || null; @@ -94,6 +112,7 @@ export class MenuController { * @param {string} [menuId] Optionally get the menu by its id, or side. * @return {Menu} Returns the instance of the menu, which is useful for chaining. */ + @Method() swipeEnable(shouldEnable: boolean, menuId?: string): Menu { const menu = this.get(menuId); return (menu && menu.swipeEnable(shouldEnable)) || null; @@ -104,10 +123,11 @@ export class MenuController { * @return {boolean} Returns true if the specified menu is currently open, otherwise false. * If the menuId is not specified, it returns true if ANY menu is currenly open. */ + @Method() isOpen(menuId?: string): boolean { if (menuId) { var menu = this.get(menuId); - return menu && menu.isOpen || false; + return menu && menu.isOpen() || false; } else { return !!this.getOpen(); } @@ -117,6 +137,7 @@ export class MenuController { * @param {string} [menuId] Optionally get the menu by its id, or side. * @return {boolean} Returns true if the menu is currently enabled, otherwise false. */ + @Method() isEnabled(menuId?: string): boolean { const menu = this.get(menuId); return menu && menu.enabled || false; @@ -131,87 +152,94 @@ export class MenuController { * @param {string} [menuId] Optionally get the menu by its id, or side. * @return {Menu} Returns the instance of the menu if found, otherwise `null`. */ + @Method() get(menuId?: string): Menu { var menu: Menu; if (menuId === 'left' || menuId === 'right') { // there could be more than one menu on the same side // so first try to get the enabled one - menu = this._menus.find(m => m.side === menuId && m.enabled); + menu = this.menus.find(m => m.side === menuId && m.enabled); if (menu) { return menu; } // didn't find a menu side that is enabled // so try to get the first menu side found - return this._menus.find(m => m.side === menuId) || null; + return this.menus.find(m => m.side === menuId) || null; } else if (menuId) { // the menuId was not left or right // so try to get the menu by its "id" - return this._menus.find(m => m.id === menuId) || null; + return this.menus.find(m => m.id === menuId) || null; } // return the first enabled menu - menu = this._menus.find(m => m.enabled); + menu = this.menus.find(m => m.enabled); if (menu) { return menu; } // get the first menu in the array, if one exists - return (this._menus.length ? this._menus[0] : null); + return (this.menus.length > 0 ? this.menus[0] : null); } /** * @return {Menu} Returns the instance of the menu already opened, otherwise `null`. */ + @Method() getOpen(): Menu { - return this._menus.find(m => m.isOpen); + return this.menus.find(m => m.isOpen()); } /** * @return {Array} Returns an array of all menu instances. */ - getMenus(): Array { - return this._menus; + @Method() + getMenus(): Menu[] { + return this.menus; } /** * @hidden * @return {boolean} if any menu is currently animating */ + @Method() isAnimating(): boolean { - return this._menus.some(menu => menu.isAnimating); + return this.menus.some(menu => menu.isAnimating()); } /** * @hidden */ + @Method() _register(menu: Menu) { - if (this._menus.indexOf(menu) < 0) { - this._menus.push(menu); + if (this.menus.indexOf(menu) < 0) { + this.menus.push(menu); } } /** * @hidden */ + @Method() _unregister(menu: Menu) { - const index = this._menus.indexOf(menu); + const index = this.menus.indexOf(menu); if (index > -1) { - this._menus.splice(index, 1); + this.menus.splice(index, 1); } } /** * @hidden */ + @Method() _setActiveMenu(menu: Menu) { // if this menu should be enabled // then find all the other menus on this same side // and automatically disable other same side menus const side = menu.side; - this._menus + this.menus .filter(m => m.side === side && m !== menu) .map(m => m.enable(false)); } @@ -220,15 +248,17 @@ export class MenuController { /** * @hidden */ - registerType(name: string, cls: new(...args: any[]) => MenuType) { - this._menuTypes[name] = cls; + registerAnimation(name: string, cls: AnimationBuilder) { + this.menuAnimations[name] = cls; } /** * @hidden */ - create(type: string, menuCmp: Menu) { - return new this._menuTypes[type](menuCmp); + @Method() + create(type: string, menuCmp: Menu): Promise { + const animationBuilder = this.menuAnimations[type]; + return this.animationCtrl.create(animationBuilder, null, menuCmp); } } diff --git a/packages/core/src/components/menu/menu-gestures.ts b/packages/core/src/components/menu/menu-gestures.ts deleted file mode 100644 index cf4b299b7b..0000000000 --- a/packages/core/src/components/menu/menu-gestures.ts +++ /dev/null @@ -1,113 +0,0 @@ -// import { Menu } from './menu'; -// import { DomController } from '../../platform/dom-controller'; -// import { GestureController, GESTURE_PRIORITY_MENU_SWIPE, GESTURE_MENU_SWIPE } from '../../gestures/gesture-controller'; -// import { Platform } from '../../platform/platform'; -// import { SlideEdgeGesture } from '../../gestures/slide-edge-gesture'; -// import { SlideData } from '../../gestures/slide-gesture'; - -// /** -// * Gesture attached to the content which the menu is assigned to -// */ -// export class MenuContentGesture extends SlideEdgeGesture { - -// constructor( -// plt: Platform, -// public menu: Menu, -// gestureCtrl: GestureController, -// domCtrl: DomController, -// ) { -// super(plt, plt.doc().body, { -// direction: 'x', -// edge: menu.side, -// threshold: 5, -// maxEdgeStart: menu.maxEdgeStart || 50, -// zone: false, -// passive: true, -// domController: domCtrl, -// gesture: gestureCtrl.createGesture({ -// name: GESTURE_MENU_SWIPE, -// priority: GESTURE_PRIORITY_MENU_SWIPE, -// disableScroll: true -// }) -// }); -// } - -// canStart(ev: any): boolean { -// const menu = this.menu; -// if (!menu.canSwipe()) { -// return false; -// } -// if (menu.isOpen) { -// return true; -// } else if (menu.getMenuController().getOpen()) { -// return false; -// } -// return super.canStart(ev); -// } - -// // Set CSS, then wait one frame for it to apply before sliding starts -// onSlideBeforeStart(ev: any) { -// console.debug('menu gesture, onSlideBeforeStart', this.menu.side); -// this.menu._swipeBeforeStart(); -// } - -// onSlideStart() { -// console.debug('menu gesture, onSlideStart', this.menu.side); -// this.menu._swipeStart(); -// } - -// onSlide(slide: SlideData, ev: any) { -// const z = (this.menu.isRightSide ? slide.min : slide.max); -// const stepValue = (slide.distance / z); - -// this.menu._swipeProgress(stepValue); -// } - -// onSlideEnd(slide: SlideData, ev: any) { -// let z = (this.menu.isRightSide ? slide.min : slide.max); -// const currentStepValue = (slide.distance / z); -// const velocity = slide.velocity; -// z = Math.abs(z * 0.5); -// const shouldCompleteRight = (velocity >= 0) -// && (velocity > 0.2 || slide.delta > z); - -// const shouldCompleteLeft = (velocity <= 0) -// && (velocity < -0.2 || slide.delta < -z); - -// console.debug('menu gesture, onSlideEnd', this.menu.side, -// 'distance', slide.distance, -// 'delta', slide.delta, -// 'velocity', velocity, -// 'min', slide.min, -// 'max', slide.max, -// 'shouldCompleteLeft', shouldCompleteLeft, -// 'shouldCompleteRight', shouldCompleteRight, -// 'currentStepValue', currentStepValue); - -// this.menu._swipeEnd(shouldCompleteLeft, shouldCompleteRight, currentStepValue, velocity); -// } - -// getElementStartPos(slide: SlideData, ev: any) { -// const menu = this.menu; -// if (menu.isRightSide) { -// return menu.isOpen ? slide.min : slide.max; -// } -// // left menu -// return menu.isOpen ? slide.max : slide.min; -// } - -// getSlideBoundaries(): { min: number, max: number } { -// const menu = this.menu; -// if (menu.isRightSide) { -// return { -// min: -menu.width(), -// max: 0 -// }; -// } -// // left menu -// return { -// min: 0, -// max: menu.width() -// }; -// } -// } diff --git a/packages/core/src/components/menu/menu-types.ts b/packages/core/src/components/menu/menu-types.ts deleted file mode 100644 index 9c0481b0ea..0000000000 --- a/packages/core/src/components/menu/menu-types.ts +++ /dev/null @@ -1,169 +0,0 @@ -import { Animation } from '../../index'; - - -/** - * @hidden - * Menu Type - * Base class which is extended by the various types. Each - * type will provide their own animations for open and close - * and registers itself with Menu. - */ -export class MenuType { - - ani: Animation; - isOpening: boolean; - - constructor() { - // Ionic.createAnimation().then(Animation => { - // this.ani = new Animation(); - // });; - // this.ani - // .easing('cubic-bezier(0.0, 0.0, 0.2, 1)') - // .easingReverse('cubic-bezier(0.4, 0.0, 0.6, 1)') - // .duration(280); - } - - setOpen(shouldOpen: boolean, animated: boolean, done: (animation: Animation) => void) { - const ani = this.ani - .onFinish(done, {oneTimeCallback: true, clearExistingCallacks: true }) - .reverse(!shouldOpen); - - if (animated) { - ani.play(); - } else { - ani.syncPlay(); - } - } - - setProgressStart(isOpen: boolean) { - this.isOpening = !isOpen; - - // the cloned animation should not use an easing curve during seek - this.ani - .reverse(isOpen) - .progressStart(); - } - - setProgessStep(stepValue: number) { - // adjust progress value depending if it opening or closing - this.ani.progressStep(stepValue); - } - - setProgressEnd(shouldComplete: boolean, currentStepValue: number, velocity: number, done: Function) { - let isOpen = (this.isOpening && shouldComplete); - if (!this.isOpening && !shouldComplete) { - isOpen = true; - } - const ani = this.ani; - ani.onFinish(() => { - this.isOpening = false; - done(isOpen); - }, { clearExistingCallacks: true }); - - const factor = 1 - Math.min(Math.abs(velocity) / 4, 0.7); - const dur = ani.getDuration() * factor; - - ani.progressEnd(shouldComplete, currentStepValue, dur); - } - - destroy() { - this.ani.destroy(); - this.ani = null; - } - -} - - -/** - * @hidden - * Menu Reveal Type - * The content slides over to reveal the menu underneath. - * The menu itself, which is under the content, does not move. - */ -export class MenuRevealType extends MenuType { - - // constructor(menu: Menu) { - // super(); - - // const openedX = (menu.width() * (menu.isRightSide ? -1 : 1)) + 'px'; - // const contentOpen = Ionic.createAnimation(menu.getContentElement()); - // contentOpen.fromTo('translateX', '0px', openedX); - // this.ani.add(contentOpen); - // } - -} - - -/** - * @hidden - * Menu Push Type - * The content slides over to reveal the menu underneath. - * The menu itself also slides over to reveal its bad self. - */ -export class MenuPushType extends MenuType { - - // constructor(menu: Menu) { - // super(); - - // let contentOpenedX: string, menuClosedX: string, menuOpenedX: string; - // const width = menu.width(); - - // if (menu.isRightSide) { - // // right side - // contentOpenedX = -width + 'px'; - // menuClosedX = width + 'px'; - // menuOpenedX = '0px'; - - // } else { - // contentOpenedX = width + 'px'; - // menuOpenedX = '0px'; - // menuClosedX = -width + 'px'; - // } - - // const menuAni = Ionic.createAnimation(menu.getMenuElement()); - // menuAni.fromTo('translateX', menuClosedX, menuOpenedX); - // this.ani.add(menuAni); - - // const contentApi = Ionic.createAnimation(menu.getContentElement()); - // contentApi.fromTo('translateX', '0px', contentOpenedX); - // this.ani.add(contentApi); - // } - -} - - -/** - * @hidden - * Menu Overlay Type - * The menu slides over the content. The content - * itself, which is under the menu, does not move. - */ -export class MenuOverlayType extends MenuType { - - // constructor(menu: Menu) { - // super(); - - // let closedX: string, openedX: string; - // const width = menu.width(); - - // if (menu.isRightSide) { - // // right side - // closedX = 8 + width + 'px'; - // openedX = '0px'; - - // } else { - // // left side - // closedX = -(8 + width) + 'px'; - // openedX = '0px'; - // } - - // const menuAni = Ionic.createAnimation(menu.getMenuElement()); - // menuAni.fromTo('translateX', closedX, openedX); - // this.ani.add(menuAni); - - // const backdropApi = Ionic.createAnimation(menu.getBackdropElement()); - // backdropApi.fromTo('opacity', 0.01, 0.35); - // this.ani.add(backdropApi); - // } - -} diff --git a/packages/core/src/components/menu/menu.ios.scss b/packages/core/src/components/menu/menu.ios.scss index 59ef4c4fee..bb82a5434c 100644 --- a/packages/core/src/components/menu/menu.ios.scss +++ b/packages/core/src/components/menu/menu.ios.scss @@ -19,14 +19,14 @@ $menu-ios-box-shadow: 0 0 10px $menu-ios-box-shadow-color !default; background: $menu-ios-background; } -.ios .menu-content-reveal { +.menu-ios .menu-content-reveal { box-shadow: $menu-ios-box-shadow; } -.ios .menu-content-push { +.menu-ios .menu-content-push { box-shadow: $menu-ios-box-shadow; } -.ios ion-menu[type=overlay] .menu-inner { +ion-menu[type=overlay] .menu-ios { box-shadow: $menu-ios-box-shadow; } diff --git a/packages/core/src/components/menu/menu.md.scss b/packages/core/src/components/menu/menu.md.scss index 3dfb143d31..d24210af71 100644 --- a/packages/core/src/components/menu/menu.md.scss +++ b/packages/core/src/components/menu/menu.md.scss @@ -15,7 +15,7 @@ $menu-md-box-shadow-color: rgba(0, 0, 0, .25) !default; $menu-md-box-shadow: 0 0 10px $menu-md-box-shadow-color !default; -.menu-md { +.menu-md .menu-inner { background: $menu-md-background; } diff --git a/packages/core/src/components/menu/menu.tsx b/packages/core/src/components/menu/menu.tsx index d7b5e5cf65..0e2fc71d21 100644 --- a/packages/core/src/components/menu/menu.tsx +++ b/packages/core/src/components/menu/menu.tsx @@ -1,8 +1,10 @@ import { Component, Element, Event, EventEmitter, Prop, PropDidChange } from '@stencil/core'; -import { Config } from '../../index'; +import { Config, Animation } from '../../index'; import { MenuController } from './menu-controller'; -import { MenuType } from './menu-types'; +import { isRightSide, Side, assert, checkEdgeSide } from '../../utils/helpers'; +export type Lazy = + {[P in keyof T]: (...args: any[]) => Promise; }; @Component({ tag: 'ion-menu', @@ -16,46 +18,44 @@ import { MenuType } from './menu-types'; } }) export class Menu { - @Element() private el: HTMLElement; - private _backdropElm: HTMLElement; - private _ctrl: MenuController; + + private _backdropEle: HTMLElement; + private _menuInnerEle: HTMLElement; private _unregCntClick: Function; private _unregBdClick: Function; private _activeBlock: string; private _cntElm: HTMLElement; - private _type: MenuType; + private _animation: Animation; private _init = false; private _isPane = false; + private _isAnimating: boolean = false; + private _isOpen: boolean = false; + private _width: number = null; mode: string; color: string; + /** + * @hidden + */ + isRightSide: boolean = false; + + @Element() private el: HTMLElement; + @Event() ionDrag: EventEmitter; @Event() ionOpen: EventEmitter; @Event() ionClose: EventEmitter; @Prop({ context: 'config' }) config: Config; - /** - * @hidden - */ - @Prop() isOpen: boolean = false; + @Prop({ connect: 'ion-menu-controller' }) lazyMenuCtrl: Lazy; + menuCtrl: MenuController; /** - * @hidden + * @input {string} The content's id the menu should use. */ - @Prop() isAnimating: boolean = false; - - /** - * @hidden - */ - isRightSide: boolean = false; - - /** - * @input {any} A reference to the content element the menu should use. - */ - @Prop() content: any; + @Prop() content: string; /** * @input {string} An id for the menu. @@ -67,27 +67,22 @@ export class Menu { * see the `menuType` in the [config](../../config/Config). Available options: * `"overlay"`, `"reveal"`, `"push"`. */ - @Prop() type: string; + @Prop() type: string = 'overlay'; /** * @input {boolean} If true, the menu is enabled. Default `true`. */ - @Prop() enabled: boolean; + @Prop({ mutable: true }) enabled: boolean; /** * @input {string} Which side of the view the menu should be placed. Default `"start"`. */ - @Prop() side: string = 'start'; + @Prop() side: Side = 'start'; /** * @input {boolean} If true, swiping the menu is enabled. Default `true`. */ - @Prop() swipeEnabled: boolean; - - @PropDidChange('swipeEnabled') - swipeEnabledChange(isEnabled: boolean) { - this.swipeEnable(isEnabled); - } + @Prop() swipeEnabled: boolean = true; /** * @input {boolean} If true, the menu will persist on child pages. @@ -97,43 +92,64 @@ export class Menu { /** * @hidden */ - @Prop() maxEdgeStart: number; + @Prop() maxEdgeStart: number = 50; + // @PropDidChange('side') + // sideChanged(side: Side) { + // // TODO: const isRTL = this._plt.isRTL; + // const isRTL = false; + // // this.isRightSide = isRightSide(side, isRTL); + // } + + @PropDidChange('enabled') + enabledChanged() { + this._updateState(); + } + + @PropDidChange('swipeEnabled') + swipeEnabledChange() { + this._updateState(); + } + + ionViewWillLoad() { + return this.lazyMenuCtrl.getInstance().then(menu => this.menuCtrl = menu); + } + /** * @hidden */ ionViewDidLoad() { - this._backdropElm = this.el.querySelector('.menu-backdrop') as HTMLElement; + assert(!!this.menuCtrl, "menucontroller was not initialized"); - this._init = true; + this._menuInnerEle = this.el.querySelector('.menu-inner') as HTMLElement; + this._backdropEle = this.el.querySelector('.menu-backdrop') as HTMLElement; - if (this.content) { - if ((this.content).tagName as HTMLElement) { - this._cntElm = this.content; - } else if (typeof this.content === 'string') { - this._cntElm = document.querySelector(this.content) as any; - } - } - - if (!this._cntElm || !this._cntElm.tagName) { + const contentQuery = (this.content) + ? '> #' + this.content + : '[main]'; + const parent = this.el.parentElement; + const content = this._cntElm = parent.querySelector(contentQuery) as HTMLElement; + if (!content || !content.tagName) { // requires content element return console.error('Menu: must have a "content" element to listen for drag events on.'); } + // TODO: make PropDidChange work + this.isRightSide = isRightSide(this.side, false); // add menu's content classes - this._cntElm.classList.add('menu-content'); - this._cntElm.classList.add('menu-content-' + this.type); + content.classList.add('menu-content'); + content.classList.add('menu-content-' + this.type); let isEnabled = this.enabled; if (isEnabled === true || typeof isEnabled === 'undefined') { - // check if more than one menu is on the same side - isEnabled = !this._ctrl.getMenus().some(m => { + const menus = this.menuCtrl.getMenus(); + isEnabled = !menus.some(m => { return m.side === this.side && m.enabled; }); } // register this menu with the app's menu controller - this._ctrl._register(this); + this.menuCtrl._register(this); // mask it as enabled / disabled this.enable(isEnabled); @@ -143,30 +159,34 @@ export class Menu { return { attrs: { 'role': 'navigation', - 'side': this.side, + 'side': this.getSide(), 'type': this.type }, class: { - 'menu-enabled': this.enabled + 'menu-enabled': this._canOpen() } }; } - render() { - // normalize the "type" - if (!this.type) { - this.type = this.config.get('menuType', 'overlay'); - } + getSide(): string { + return this.isRightSide ? 'right' : 'left'; + } - return [ + render() { + return ([ , - , + - ]; + ]); } /** @@ -185,21 +205,25 @@ export class Menu { onBackdropClick(ev: UIEvent) { ev.preventDefault(); ev.stopPropagation(); - this._ctrl.close(); + this.menuCtrl.close(); } /** * @hidden */ - private _getType(): MenuType { - if (!this._type) { - this._type = this._ctrl.create(this.type, this); - - if (this.config.getBoolean('animate') === false) { - this._type.ani.duration(0); - } + private prepareAnimation(): Promise { + const width = this._menuInnerEle.offsetWidth; + if (width === this._width) { + return Promise.resolve(); } - return this._type; + if (this._animation) { + this._animation.destroy(); + this._animation = null; + } + this._width = width; + return this.menuCtrl.create(this.type, this).then(ani => { + this._animation = ani; + }); } /** @@ -207,23 +231,44 @@ export class Menu { */ setOpen(shouldOpen: boolean, animated: boolean = true): Promise { // If the menu is disabled or it is currenly being animated, let's do nothing - if ((shouldOpen === this.isOpen) || !this._canOpen() || this.isAnimating) { - return Promise.resolve(this.isOpen); + if ((shouldOpen === this._isOpen) || !this._canOpen() || this._isAnimating) { + return Promise.resolve(this._isOpen); } - return new Promise(resolve => { - this._before(); - this._getType().setOpen(shouldOpen, animated, () => { + this._before(); + this.prepareAnimation() + .then(() => this._startAnimation(shouldOpen, animated)) + .then(() => { this._after(shouldOpen); - resolve(this.isOpen); + return this._isOpen; }); - }); + } + + _startAnimation(shouldOpen: boolean, animated: boolean): Promise { + let done; + const promise = new Promise(resolve => done = resolve); + const ani = this._animation + .onFinish(done, {oneTimeCallback: true, clearExistingCallacks: true }) + .reverse(!shouldOpen); + + if (animated) { + ani.play(); + } else { + ani.syncPlay(); + } + + return promise; } _forceClosing() { - this.isAnimating = true; - this._getType().setOpen(false, false, () => { - this._after(false); - }); + assert(this._isOpen, 'menu cannot be closed'); + + this._isAnimating = true; + this._startAnimation(false, false) + .then(() => this._after(false)); + } + + getWidth(): number { + return this._width; } /** @@ -231,77 +276,120 @@ export class Menu { */ canSwipe(): boolean { return this.swipeEnabled && - !this.isAnimating && + !this._isAnimating && this._canOpen(); // TODO: && this._app.isEnabled(); } + /** + * @hidden + */ + isAnimating(): boolean { + return this._isAnimating; + } - _swipeBeforeStart() { - if (!this.canSwipe()) { - return; - } + /** + * @hidden + */ + isOpen(): boolean { + return this._isOpen; + } + + _swipeWillStart(): Promise { this._before(); + return this.prepareAnimation(); } _swipeStart() { - if (!this.isAnimating) { + assert(!!this._animation, '_type is undefined'); + if (!this._isAnimating) { + assert(false, '_isAnimating has to be true'); return; } - this._getType().setProgressStart(this.isOpen); + // the cloned animation should not use an easing curve during seek + this._animation + .reverse(this._isOpen) + .progressStart(); } - _swipeProgress(stepValue: number) { - if (!this.isAnimating) { + _swipeProgress(slide: any) { + assert(!!this._animation, '_type is undefined'); + if (!this._isAnimating) { + assert(false, '_isAnimating has to be true'); return; } - this._getType().setProgessStep(stepValue); + // const isRTL = false; + const z = this._width; + // const z = (this.isRightSide !== isRTL ? slide.min : slide.max); + const stepValue = (Math.abs(slide.deltaX) / z); - this.ionDrag.emit({ menu: this }); + this._animation.progressStep(stepValue); + // TODO: this.ionDrag.emit({ menu: this }); } - _swipeEnd(shouldCompleteLeft: boolean, shouldCompleteRight: boolean, stepValue: number, velocity: number) { - if (!this.isAnimating) { + _swipeEnd(slide: any) { + assert(!!this._animation, '_type is undefined'); + if (!this._isAnimating) { + assert(false, '_isAnimating has to be true'); return; } + const width = this._width; + const delta = Math.abs(slide.deltaX) + const stepValue = delta / width; + const velocity = slide.velocityX; + const z = width / 2; + const shouldCompleteRight = (velocity >= 0) + && (velocity > 0.2 || slide.deltaX > z); + + const shouldCompleteLeft = (velocity <= 0) + && (velocity < -0.2 || slide.deltaX < -z); - // user has finished dragging the menu const isRightSide = this.isRightSide; - const opening = !this.isOpen; + const opening = !this._isOpen; const shouldComplete = (opening) ? isRightSide ? shouldCompleteLeft : shouldCompleteRight : isRightSide ? shouldCompleteRight : shouldCompleteLeft; - this._getType().setProgressEnd(shouldComplete, stepValue, velocity, (isOpen: boolean) => { - console.debug('menu, swipeEnd', this.side); - this._after(isOpen); - }); + let isOpen = (opening && shouldComplete); + if (!opening && !shouldComplete) { + isOpen = true; + } + + const missing = shouldComplete ? 1 - stepValue : stepValue; + const missingDistance = missing * width; + const dur = missingDistance / Math.abs(velocity); + const realDur = Math.min(dur, 380); + + this._animation + .onFinish(() => this._after(isOpen), { clearExistingCallacks: true }) + .progressEnd(shouldComplete, stepValue, realDur); } private _before() { + assert(!this._isAnimating, '_before() should not be called while animating'); + // this places the menu into the correct location before it animates in // this css class doesn't actually kick off any animations this.el.classList.add('show-menu'); - this._backdropElm.classList.add('show-backdrop'); + this._backdropEle.classList.add('show-backdrop'); this.resize(); - - // TODO: this._keyboard.close(); - - this.isAnimating = true; + this._isAnimating = true; } private _after(isOpen: boolean) { + assert(this._isAnimating, '_before() should be called while animating'); + // TODO: this._app.setEnabled(false, 100); // keep opening/closing the menu disabled for a touch more yet // only add listeners/css if it's enabled and isOpen // and only remove listeners/css if it's not open // emit opened/closed events - this.isOpen = isOpen; - this.isAnimating = false; + this._isOpen = isOpen; + this._isAnimating = false; // add/remove backdrop click listeners this._backdropClick(isOpen); @@ -311,9 +399,7 @@ export class Menu { this._activeBlock = GESTURE_BLOCKER; // add css class - Context.dom.write(() => { - this._cntElm.classList.add('menu-content-open'); - }); + this._cntElm.classList.add('menu-content-open'); // emit open event this.ionOpen.emit({ menu: this }); @@ -323,11 +409,9 @@ export class Menu { this._activeBlock = null; // remove css classes - Context.dom.write(() => { - this._cntElm.classList.remove('menu-content-open'); - this._cntElm.classList.remove('show-menu'); - this._backdropElm.classList.remove('show-menu'); - }); + this.el.classList.remove('show-menu'); + this._cntElm.classList.remove('menu-content-open'); + this._backdropEle.classList.remove('show-menu'); // emit close event this.ionClose.emit({ menu: this }); @@ -359,11 +443,23 @@ export class Menu { // content && content.resize(); } + canStart(detail: any): boolean { + if (!this.canSwipe()) { + return false; + } + if (this._isOpen) { + return true; + } else if (this.getMenuController().getOpen()) { + return false; + } + return checkEdgeSide(detail.currentX, this.isRightSide, this.maxEdgeStart); + } + /** * @hidden */ toggle(): Promise { - return this.setOpen(!this.isOpen); + return this.setOpen(!this._isOpen); } _canOpen(): boolean { @@ -373,40 +469,30 @@ export class Menu { /** * @hidden */ + // @PropDidChange('swipeEnabled') + // @PropDidChange('enabled') _updateState() { const canOpen = this._canOpen(); // Close menu inmediately - if (!canOpen && this.isOpen) { + if (!canOpen && this._isOpen) { + assert(this._init, 'menu must be initialized'); // close if this menu is open, and should not be enabled this._forceClosing(); } - if (this.enabled && this._ctrl) { - this._ctrl._setActiveMenu(this); + if (this.enabled && this.menuCtrl) { + this.menuCtrl._setActiveMenu(this); } if (!this._init) { return; } - // TODO - // const gesture = this._gesture; - // // only listen/unlisten if the menu has initialized - // if (canOpen && this.swipeEnabled && !gesture.isListening) { - // // should listen, but is not currently listening - // console.debug('menu, gesture listen', this.side); - // gesture.listen(); - - // } else if (gesture.isListening && (!canOpen || !this.swipeEnabled)) { - // // should not listen, but is currently listening - // console.debug('menu, gesture unlisten', this.side); - // gesture.unlisten(); - // } - - if (this.isOpen || (this._isPane && this.enabled)) { + if (this._isOpen || (this._isPane && this.enabled)) { this.resize(); } + assert(!this._isAnimating, 'can not be animating'); } /** @@ -414,7 +500,6 @@ export class Menu { */ enable(shouldEnable: boolean): Menu { this.enabled = shouldEnable; - this._updateState(); return this; } @@ -438,7 +523,6 @@ export class Menu { */ swipeEnable(shouldEnable: boolean): Menu { this.swipeEnabled = shouldEnable; - this._updateState(); return this; } @@ -460,21 +544,14 @@ export class Menu { * @hidden */ getBackdropElement(): HTMLElement { - return this._backdropElm; - } - - /** - * @hidden - */ - width(): number { - return this.getMenuElement().offsetWidth; + return this._backdropEle; } /** * @hidden */ getMenuController(): MenuController { - return this._ctrl; + return this.menuCtrl; } private _backdropClick(shouldAdd: boolean) { @@ -497,10 +574,10 @@ export class Menu { ionViewDidUnload() { this._backdropClick(false); - this._ctrl._unregister(this); - this._type && this._type.destroy(); + this.menuCtrl._unregister(this); + this._animation && this._animation.destroy(); - this._ctrl = this._type = this._cntElm = this._backdropElm = null; + this.menuCtrl = this._animation = this._cntElm = this._backdropEle = null; } } diff --git a/packages/core/src/components/menu/tests/basic.html b/packages/core/src/components/menu/tests/basic.html new file mode 100644 index 0000000000..7ca42a32aa --- /dev/null +++ b/packages/core/src/components/menu/tests/basic.html @@ -0,0 +1,135 @@ + + + + + + Ionic Item Sliding + + + + + + + + + + Left Menu + + + + + + + + + Open Right Menu + + + + Close Menu + + + + Close Menu + + + + Close Menu + + + + Close Menu + + + + Close Menu + + + + Close Menu + + + + Close Menu + + + + Close Menu + + + + Close Menu + + + + Close Menu + + + + Close Menu + + + + Close Menu + + + + + + + + Footer + + + + + + + + + + Hola + + + + + hola macho + + + + + + + + Menu Basic Test + + + + + + Open left menu + + + Open right menu + + + + + + + + + + diff --git a/packages/core/src/components/scroll/scroll.tsx b/packages/core/src/components/scroll/scroll.tsx index 3b60ec4168..f6fc53164c 100644 --- a/packages/core/src/components/scroll/scroll.tsx +++ b/packages/core/src/components/scroll/scroll.tsx @@ -82,9 +82,6 @@ export class Scroll { } } - detail.directionX = detail.velocityDirectionX = (detail.deltaX > 0 ? 'left' : (detail.deltaX < 0 ? 'right' : null)); - detail.directionY = detail.velocityDirectionY = (detail.deltaY > 0 ? 'up' : (detail.deltaY < 0 ? 'down' : null)); - // actively scrolling positions.push(detail.scrollTop, detail.scrollLeft, detail.timeStamp); @@ -106,15 +103,11 @@ export class Scroll { // compute relative movement between these two points var movedTop = (positions[startPos - 2] - positions[endPos - 2]); var movedLeft = (positions[startPos - 1] - positions[endPos - 1]); - var factor = 16.67 / (positions[endPos] - positions[startPos]); + var factor = 16.67 / (positions[startPos] - positions[endPos]); // based on XXms compute the movement to apply for each render step detail.velocityY = movedTop * factor; detail.velocityX = movedLeft * factor; - - // figure out which direction we're scrolling - detail.velocityDirectionX = (movedLeft > 0 ? 'left' : (movedLeft < 0 ? 'right' : null)); - detail.velocityDirectionY = (movedTop > 0 ? 'up' : (movedTop < 0 ? 'down' : null)); } } diff --git a/packages/core/src/index.d.ts b/packages/core/src/index.d.ts index a6734ab26e..01e55ac47c 100644 --- a/packages/core/src/index.d.ts +++ b/packages/core/src/index.d.ts @@ -10,7 +10,6 @@ import { Loading, LoadingEvent, LoadingOptions } from './components/loading/load import { LoadingController } from './components/loading-controller/loading-controller'; import { GestureDetail, GestureCallback } from './components/gesture/gesture'; import { Menu } from './components/menu/menu'; -import { MenuType } from './components/menu/menu-types'; import { MenuController } from './components/menu/menu-controller'; import { Modal, ModalOptions, ModalEvent } from './components/modal/modal'; import { ModalController } from './components/modal-controller/modal-controller'; @@ -77,7 +76,6 @@ export { LoadingEvent, Menu, MenuController, - MenuType, Modal, ModalController, ModalOptions, diff --git a/packages/core/src/utils/helpers.ts b/packages/core/src/utils/helpers.ts index 2e262e323e..0689bb65df 100644 --- a/packages/core/src/utils/helpers.ts +++ b/packages/core/src/utils/helpers.ts @@ -41,7 +41,7 @@ export function assert(bool: boolean, msg: string) { if (!bool) { console.error(msg); } -}; +} export function toDashCase(str: string) { return str.replace(/([A-Z])/g, (g) => '-' + g[0].toLowerCase()); @@ -64,6 +64,26 @@ export function pointerCoordX(ev: any): number { return 0; } +export function updateDetail(ev: any, detail: any) { + // get X coordinates for either a mouse click + // or a touch depending on the given event + let x = 0; + let y = 0; + if (ev) { + var changedTouches = ev.changedTouches; + if (changedTouches && changedTouches.length > 0) { + var touch = changedTouches[0]; + x = touch.clientX; + y = touch.clientY; + }else if (ev.pageX !== undefined) { + x = ev.pageX; + y = ev.pageY; + } + } + detail.currentX = x; + detail.currentY = y; +} + export function pointerCoordY(ev: any): number { // get Y coordinates for either a mouse click // or a touch depending on the given event @@ -79,7 +99,9 @@ export function pointerCoordY(ev: any): number { return 0; } -export function getElementReference(elm: any, ref: string) { +export type ElementRef = 'child' | 'parent' | 'body' | 'document' | 'window'; + +export function getElementReference(elm: any, ref: ElementRef) { if (ref === 'child') { return elm.firstElementChild; } @@ -139,9 +161,16 @@ export function getToolbarHeight(toolbarTagName: string, pageChildren: HTMLEleme return ''; } -/** @hidden */ export type Side = 'left' | 'right' | 'start' | 'end'; +export function checkEdgeSide(posX: number, isRightSide: boolean, maxEdgeStart: number): boolean { + if (isRightSide) { + return posX >= window.innerWidth - maxEdgeStart; + } else { + return posX <= maxEdgeStart; + } +} + /** * @hidden * Given a side, return if it should be on the right diff --git a/packages/core/stencil.config.js b/packages/core/stencil.config.js index e872f03dcc..9a46273131 100644 --- a/packages/core/stencil.config.js +++ b/packages/core/stencil.config.js @@ -19,7 +19,7 @@ exports.config = { { components: ['ion-item', 'ion-item-divider', 'ion-item-sliding', 'ion-item-options', 'ion-item-option', 'ion-label', 'ion-list', 'ion-list-header', 'ion-skeleton-text'] }, { components: ['ion-input', 'ion-textarea'] }, { components: ['ion-loading', 'ion-loading-controller'] }, - { components: ['ion-menu'], priority: 'low' }, + { components: ['ion-menu', 'ion-menu-controller'], priority: 'low' }, { components: ['ion-modal', 'ion-modal-controller'] }, { components: ['ion-popover', 'ion-popover-controller'] }, { components: ['ion-radio', 'ion-radio-group'] },