From 687b37ad3e3237b874473817bb7b59143ac113ce Mon Sep 17 00:00:00 2001 From: "Manu Mtz.-Almeida" Date: Fri, 27 Oct 2017 18:22:13 +0200 Subject: [PATCH] fix(ion-menu): finish ion-menu and ion-split-pane --- packages/core/src/components.d.ts | 7 +- .../core/src/components/gesture/gesture.tsx | 25 +- .../src/components/menu/animations/overlay.ts | 6 +- .../src/components/menu/animations/push.ts | 6 +- .../src/components/menu/animations/reveal.ts | 4 +- .../src/components/menu/menu-controller.ts | 84 +-- .../core/src/components/menu/menu.ios.scss | 11 +- .../core/src/components/menu/menu.md.scss | 9 +- packages/core/src/components/menu/menu.scss | 18 +- packages/core/src/components/menu/menu.tsx | 591 +++++++----------- .../core/src/components/menu/tests/basic.html | 23 +- 11 files changed, 350 insertions(+), 434 deletions(-) diff --git a/packages/core/src/components.d.ts b/packages/core/src/components.d.ts index 7fa01e3701..eee92ae352 100644 --- a/packages/core/src/components.d.ts +++ b/packages/core/src/components.d.ts @@ -1541,7 +1541,7 @@ declare global { _register?: any, _unregister?: any, _setActiveMenu?: any, - create?: any, + createAnimation?: any, animationCtrl?: any } } @@ -1572,6 +1572,11 @@ declare global { mode?: string, color?: string, + isOpen?: any, + setOpen?: any, + open?: any, + close?: any, + toggle?: any, lazyMenuCtrl?: any, content?: string, menuId?: string, diff --git a/packages/core/src/components/gesture/gesture.tsx b/packages/core/src/components/gesture/gesture.tsx index 142afeba06..427fe0d4dc 100644 --- a/packages/core/src/components/gesture/gesture.tsx +++ b/packages/core/src/components/gesture/gesture.tsx @@ -8,7 +8,6 @@ import { PanRecognizer } from './recognizers'; }) export class Gesture { - @Element() private el: HTMLElement; private detail: GestureDetail = {}; private positions: number[] = []; private ctrl: GestureController; @@ -21,13 +20,8 @@ export class Gesture { private hasFiredStart = true; private isMoveQueued = false; private blocker: BlockerDelegate; - private fireOnMoveFunc: any; - @Event() private ionGestureMove: EventEmitter; - @Event() private ionGestureStart: EventEmitter; - @Event() private ionGestureEnd: EventEmitter; - @Event() private ionGestureNotCaptured: EventEmitter; - @Event() private ionPress: EventEmitter; + @Element() private el: HTMLElement; @Prop() enabled: boolean = true; @Prop() attachTo: ElementRef = 'child'; @@ -49,9 +43,12 @@ export class Gesture { @Prop() onPress: GestureCallback; @Prop() notCaptured: GestureCallback; - constructor() { - this.fireOnMoveFunc = this.fireOnMove.bind(this); - } + @Event() private ionGestureMove: EventEmitter; + @Event() private ionGestureStart: EventEmitter; + @Event() private ionGestureEnd: EventEmitter; + @Event() private ionGestureNotCaptured: EventEmitter; + @Event() private ionPress: EventEmitter; + protected ionViewDidLoad() { // in this case, we already know the GestureController and Gesture are already @@ -203,7 +200,7 @@ export class Gesture { if (!this.isMoveQueued && this.hasFiredStart) { this.isMoveQueued = true; this.calcGestureData(ev); - Context.dom.write(this.fireOnMoveFunc); + Context.dom.write(this.fireOnMove.bind(this)); } return; } @@ -221,6 +218,11 @@ export class Gesture { } private fireOnMove() { + // Since fireOnMove is called inside a RAF, onEnd() might be called, + // we must double check hasCapturedPan + if (!this.hasCapturedPan) { + return; + } const detail = this.detail; this.isMoveQueued = false; if (this.onMove) { @@ -312,6 +314,7 @@ export class Gesture { private reset() { this.hasCapturedPan = false; this.hasStartedPan = false; + this.isMoveQueued = false; this.hasFiredStart = true; this.gesture && this.gesture.release(); } diff --git a/packages/core/src/components/menu/animations/overlay.ts b/packages/core/src/components/menu/animations/overlay.ts index d00f119b1d..8a5de27710 100644 --- a/packages/core/src/components/menu/animations/overlay.ts +++ b/packages/core/src/components/menu/animations/overlay.ts @@ -9,7 +9,7 @@ import baseAnimation from './base'; */ export default function(Animation: Animation, _: HTMLElement, menu: Menu): Animation { let closedX: string, openedX: string; - const width = menu.getWidth(); + const width = menu.width; if (menu.isRightSide) { // right side closedX = 8 + width + 'px'; @@ -22,11 +22,11 @@ export default function(Animation: Animation, _: HTMLElement, menu: Menu): Anima } const menuAni = new Animation() - .addElement(menu.getMenuElement()) + .addElement(menu.menuInnerEl) .fromTo('translateX', closedX, openedX); const backdropApi = new Animation() - .addElement(menu.getBackdropElement()) + .addElement(menu.backdropEl) .fromTo('opacity', 0.01, 0.35); return baseAnimation(Animation) diff --git a/packages/core/src/components/menu/animations/push.ts b/packages/core/src/components/menu/animations/push.ts index 98d9a0f36a..0c2e339cdb 100644 --- a/packages/core/src/components/menu/animations/push.ts +++ b/packages/core/src/components/menu/animations/push.ts @@ -10,7 +10,7 @@ import baseAnimation from './base'; export default function(Animation: Animation, _: HTMLElement, menu: Menu): Animation { let contentOpenedX: string, menuClosedX: string, menuOpenedX: string; - const width = menu.getWidth(); + const width = menu.width; if (menu.isRightSide) { contentOpenedX = -width + 'px'; @@ -23,11 +23,11 @@ export default function(Animation: Animation, _: HTMLElement, menu: Menu): Anima menuClosedX = -width + 'px'; } const menuAni = new Animation() - .addElement(menu.getMenuElement()) + .addElement(menu.menuInnerEl) .fromTo('translateX', menuClosedX, menuOpenedX); const contentAni = new Animation() - .addElement(menu.getContentElement()) + .addElement(menu.contentEl) .fromTo('translateX', '0px', contentOpenedX); return baseAnimation(Animation) diff --git a/packages/core/src/components/menu/animations/reveal.ts b/packages/core/src/components/menu/animations/reveal.ts index 0ddfcce6cd..3c7306f9ab 100644 --- a/packages/core/src/components/menu/animations/reveal.ts +++ b/packages/core/src/components/menu/animations/reveal.ts @@ -8,10 +8,10 @@ import baseAnimation from './base'; * 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 openedX = (menu.width * (menu.isRightSide ? -1 : 1)) + 'px'; const contentOpen = new Animation() - .addElement(menu.getContentElement()) + .addElement(menu.contentEl) .fromTo('translateX', '0px', openedX); return baseAnimation(Animation) diff --git a/packages/core/src/components/menu/menu-controller.ts b/packages/core/src/components/menu/menu-controller.ts index f8fbbac842..6cb12855bc 100644 --- a/packages/core/src/components/menu/menu-controller.ts +++ b/packages/core/src/components/menu/menu-controller.ts @@ -1,5 +1,6 @@ import { Animation, AnimationBuilder, AnimationController, Menu } from '../../index'; import { Component, Method, Prop } from '@stencil/core'; +import { HTMLIonMenuElement } from '../../index'; import MenuOverlayAnimation from './animations/overlay'; import MenuRevealAnimation from './animations/reveal'; @@ -48,22 +49,13 @@ export class MenuController { */ @Method() close(menuId?: string): Promise { - let menu: Menu; - - if (menuId) { - // find the menu by its id - menu = this.get(menuId); - - } else { - // find the menu that is open - menu = this.getOpen(); - } + const menu = (menuId) + ? this.get(menuId) + : this.getOpen(); if (menu) { - // close the menu return menu.close(); } - return Promise.resolve(false); } @@ -96,9 +88,12 @@ export class MenuController { * @return {Menu} Returns the instance of the menu, which is useful for chaining. */ @Method() - enable(shouldEnable: boolean, menuId?: string): Menu { + enable(shouldEnable: boolean, menuId?: string): HTMLIonMenuElement { const menu = this.get(menuId); - return (menu && menu.enable(shouldEnable)) || null; + if (menu) { + menu.enabled = shouldEnable; + } + return menu; } /** @@ -108,9 +103,12 @@ export class MenuController { * @return {Menu} Returns the instance of the menu, which is useful for chaining. */ @Method() - swipeEnable(shouldEnable: boolean, menuId?: string): Menu { + swipeEnable(shouldEnable: boolean, menuId?: string): HTMLIonMenuElement { const menu = this.get(menuId); - return (menu && menu.swipeEnable(shouldEnable)) || null; + if (menu) { + menu.swipeEnabled = shouldEnable; + } + return menu; } /** @@ -123,9 +121,8 @@ export class MenuController { if (menuId) { var menu = this.get(menuId); return menu && menu.isOpen() || false; - } else { - return !!this.getOpen(); } + return !!this.getOpen(); } /** @@ -135,7 +132,10 @@ export class MenuController { @Method() isEnabled(menuId?: string): boolean { const menu = this.get(menuId); - return menu && menu.enabled || false; + if (menu) { + return menu.enabled; + } + return false; } /** @@ -148,7 +148,7 @@ export class MenuController { * @return {Menu} Returns the instance of the menu if found, otherwise `null`. */ @Method() - get(menuId?: string): Menu { + get(menuId?: string): HTMLIonMenuElement { var menu: Menu; if (menuId === 'left' || menuId === 'right') { @@ -156,43 +156,43 @@ export class MenuController { // so first try to get the enabled one menu = this.menus.find(m => m.side === menuId && m.enabled); if (menu) { - return menu; + return menu.el; } // 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.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.menuId === menuId) || null; + return this.find(m => m.menuId === menuId) || null; } // return the first enabled menu menu = this.menus.find(m => m.enabled); if (menu) { - return menu; + return menu.el; } // get the first menu in the array, if one exists - return (this.menus.length > 0 ? this.menus[0] : null); + return (this.menus.length > 0 ? this.menus[0].el : null); } /** * @return {Menu} Returns the instance of the menu already opened, otherwise `null`. */ @Method() - getOpen(): Menu { - return this.menus.find(m => m.isOpen()); + getOpen(): HTMLIonMenuElement { + return this.find(m => m.isOpen()); } /** * @return {Array} Returns an array of all menu instances. */ @Method() - getMenus(): Menu[] { - return this.menus; + getMenus(): HTMLIonMenuElement[] { + return this.menus.map(menu => menu.el); } /** @@ -201,7 +201,7 @@ export class MenuController { */ @Method() isAnimating(): boolean { - return this.menus.some(menu => menu.isAnimating()); + return this.menus.some(menu => menu.isAnimating); } /** @@ -236,24 +236,28 @@ export class MenuController { const side = menu.side; this.menus .filter(m => m.side === side && m !== menu) - .map(m => m.enable(false)); - } - - - /** - * @hidden - */ - registerAnimation(name: string, cls: AnimationBuilder) { - this.menuAnimations[name] = cls; + .map(m => m.enabled = false); } /** * @hidden */ @Method() - create(type: string, menuCmp: Menu): Promise { + createAnimation(type: string, menuCmp: Menu): Promise { const animationBuilder = this.menuAnimations[type]; return this.animationCtrl.create(animationBuilder, null, menuCmp); } + private registerAnimation(name: string, cls: AnimationBuilder) { + this.menuAnimations[name] = cls; + } + + private find(predicate: (menu: Menu) => boolean): HTMLIonMenuElement { + const instance = this.menus.find(predicate); + if (instance) { + return instance.el; + } + return null; + } + } diff --git a/packages/core/src/components/menu/menu.ios.scss b/packages/core/src/components/menu/menu.ios.scss index bb82a5434c..3042b8aef5 100644 --- a/packages/core/src/components/menu/menu.ios.scss +++ b/packages/core/src/components/menu/menu.ios.scss @@ -15,18 +15,21 @@ $menu-ios-box-shadow-color: rgba(0, 0, 0, .25) !default; $menu-ios-box-shadow: 0 0 10px $menu-ios-box-shadow-color !default; -.menu-ios { +.menu-ios .menu-inner { background: $menu-ios-background; } -.menu-ios .menu-content-reveal { +.menu-ios.menu-type-overlay .menu-inner { box-shadow: $menu-ios-box-shadow; } -.menu-ios .menu-content-push { +// iOS Menu Content +// -------------------------------------------------- + +.app-ios .menu-content-reveal { box-shadow: $menu-ios-box-shadow; } -ion-menu[type=overlay] .menu-ios { +.app-ios .menu-content-push { 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 d24210af71..1c2f4baa0c 100644 --- a/packages/core/src/components/menu/menu.md.scss +++ b/packages/core/src/components/menu/menu.md.scss @@ -19,14 +19,17 @@ $menu-md-box-shadow: 0 0 10px $menu-md-box-shadow-color !default; background: $menu-md-background; } -.menu-md .menu-content-reveal { +.menu-md.menu-type-overlay .menu-inner { box-shadow: $menu-md-box-shadow; } -.menu-md .menu-content-push { +// MD Menu Content +// -------------------------------------------------- + +.app-md .menu-content-reveal { box-shadow: $menu-md-box-shadow; } -ion-menu[type=overlay] .menu-md { +.app-md .menu-content-push { box-shadow: $menu-md-box-shadow; } diff --git a/packages/core/src/components/menu/menu.scss b/packages/core/src/components/menu/menu.scss index f4a896ffde..dedc5a8d91 100644 --- a/packages/core/src/components/menu/menu.scss +++ b/packages/core/src/components/menu/menu.scss @@ -46,7 +46,7 @@ ion-menu.show-menu { position: absolute; } -ion-menu[side=left] > .menu-inner { +.menu-side-left > .menu-inner { @include multi-dir() { // scss-lint:disable PropertySpelling right: auto; @@ -54,7 +54,7 @@ ion-menu[side=left] > .menu-inner { } } -ion-menu[side=right] > .menu-inner { +.menu-side-right > .menu-inner { @include multi-dir() { // scss-lint:disable PropertySpelling right: 0; @@ -62,10 +62,6 @@ ion-menu[side=right] > .menu-inner { } } -ion-menu[side=end] > .menu-inner { - @include position-horizontal(auto, 0); -} - ion-menu ion-backdrop { z-index: -1; display: none; @@ -84,6 +80,7 @@ ion-menu ion-backdrop { } .menu-content-open ion-pane, +.menu-content-open .ion-pane, .menu-content-open ion-content, .menu-content-open .toolbar { // the containing element itself should be clickable but @@ -106,11 +103,11 @@ ion-menu ion-backdrop { // The content slides over to reveal the menu underneath. // The menu itself, which is under the content, does not move. -ion-menu[type=reveal] { +ion-menu.menu-type-reveal { z-index: 0; } -ion-menu[type=reveal].show-menu .menu-inner { +ion-menu.menu-type-reveal.show-menu .menu-inner { @include transform(translate3d(0, 0, 0)); } @@ -120,10 +117,11 @@ ion-menu[type=reveal].show-menu .menu-inner { // The menu slides over the content. The content // itself, which is under the menu, does not move. -ion-menu[type=overlay] { +ion-menu.menu-type-overlay { z-index: $z-index-menu-overlay; } -ion-menu[type=overlay] .show-backdrop { +ion-menu.menu-type-overlay .show-backdrop { display: block; + cursor: pointer; } diff --git a/packages/core/src/components/menu/menu.tsx b/packages/core/src/components/menu/menu.tsx index 9d72b0882f..c2148f297b 100644 --- a/packages/core/src/components/menu/menu.tsx +++ b/packages/core/src/components/menu/menu.tsx @@ -1,5 +1,5 @@ -import { Component, Element, Event, EventEmitter, Listen, Prop, PropDidChange } from '@stencil/core'; -import { Animation, Config, SplitPaneAlert } from '../../index'; +import { Component, Element, Event, EventEmitter, Listen, Method, Prop, PropDidChange, PropWillChange } from '@stencil/core'; +import { Animation, Config, GestureDetail, HTMLIonMenuElement, SplitPaneAlert } from '../../index'; import { MenuController } from './menu-controller'; import { Side, assert, checkEdgeSide, isRightSide } from '../../utils/helpers'; @@ -20,38 +20,27 @@ export type Lazy = T & }) export class Menu { - private _backdropEle: HTMLElement; - private _menuInnerEle: HTMLElement; - private _unregCntClick: Function; - private _unregBdClick: Function; - private _activeBlock: string; - - private _cntElm: HTMLElement; - private _animation: Animation; - private _init = false; - private _isPane = false; - private _isAnimating: boolean = false; + private gestureBlocker: string; + private animation: Animation; + private isPane = false; private _isOpen: boolean = false; - private _width: number = null; + private lastOnEnd = 0; mode: string; color: string; - - /** - * @hidden - */ + isAnimating: boolean = false; isRightSide: boolean = false; + width: number = null; - @Element() private el: HTMLElement; + backdropEl: HTMLElement; + menuInnerEl: HTMLElement; + contentEl: HTMLElement; + menuCtrl: MenuController; - @Event() ionDrag: EventEmitter; - @Event() ionOpen: EventEmitter; - @Event() ionClose: EventEmitter; + @Element() el: HTMLIonMenuElement; @Prop({ context: 'config' }) config: Config; - @Prop({ connect: 'ion-menu-controller' }) lazyMenuCtrl: Lazy; - menuCtrl: MenuController; /** * @input {string} The content's id the menu should use. @@ -68,22 +57,48 @@ export class Menu { * see the `menuType` in the [config](../../config/Config). Available options: * `"overlay"`, `"reveal"`, `"push"`. */ - @Prop() type: string = 'overlay'; + @Prop({ mutable: true }) type: string = 'overlay'; + @PropWillChange('type') + typeChanged(type: string) { + if (this.contentEl) { + this.contentEl.classList.remove('menu-content-' + this.type); + this.contentEl.classList.add('menu-content-' + type); + this.contentEl.removeAttribute('style'); + } + if (this.menuInnerEl) { + // Remove effects of previous animations + this.menuInnerEl.removeAttribute('style'); + } + this.animation = null; + } /** * @input {boolean} If true, the menu is enabled. Default `true`. */ @Prop({ mutable: true }) enabled: boolean; + @PropDidChange('enabled') + enabledChanged() { + this.updateState(); + } /** * @input {string} Which side of the view the menu should be placed. Default `"start"`. */ @Prop() side: Side = 'start'; + @PropDidChange('side') + sideChanged() { + const isRTL = false; + this.isRightSide = isRightSide(this.side, isRTL); + } /** * @input {boolean} If true, swiping the menu is enabled. Default `true`. */ @Prop() swipeEnabled: boolean = true; + @PropDidChange('swipeEnabled') + swipeEnabledChange() { + this.updateState(); + } /** * @input {boolean} If true, the menu will persist on child pages. @@ -96,57 +111,36 @@ export class Menu { @Prop() maxEdgeStart: number = 50; - // @PropDidChange('side') - // sideChanged(side: Side) { - // // TODO: const isRTL = this._plt.isRTL; - // const isRTL = false; - // // this.isRightSide = isRightSide(side, isRTL); - // } - @Listen('body:ionSplitPaneDidChange') - splitPaneChanged(ev: SplitPaneAlert) { - this._isPane = ev.detail.splitPane.isPane(this.el); - this._updateState(); - } - - @PropDidChange('enabled') - enabledChanged() { - this._updateState(); - } - - @PropDidChange('swipeEnabled') - swipeEnabledChange() { - this._updateState(); - } + @Event() ionDrag: EventEmitter; + @Event() ionOpen: EventEmitter; + @Event() ionClose: EventEmitter; protected ionViewWillLoad() { return this.lazyMenuCtrl.componentOnReady() .then(menu => this.menuCtrl = menu); } - /** - * @hidden - */ protected ionViewDidLoad() { assert(!!this.menuCtrl, 'menucontroller was not initialized'); - this._menuInnerEle = this.el.querySelector('.menu-inner') as HTMLElement; - this._backdropEle = this.el.querySelector('.menu-backdrop') as HTMLElement; - + const el = this.el; const contentQuery = (this.content) - ? '> #' + this.content + ? '#' + this.content : '[main]'; - const parent = this.el.parentElement; - const content = this._cntElm = parent.querySelector(contentQuery) as HTMLElement; + const parent = el.parentElement; + const content = this.contentEl = 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); + this.menuInnerEl = el.querySelector('.menu-inner') as HTMLElement; + this.backdropEl = el.querySelector('.menu-backdrop') as HTMLElement; // add menu's content classes content.classList.add('menu-content'); - content.classList.add('menu-content-' + this.type); + + this.typeChanged(this.type); + this.sideChanged(); let isEnabled = this.enabled; if (isEnabled === true || typeof isEnabled === 'undefined') { @@ -159,100 +153,88 @@ export class Menu { this.menuCtrl._register(this); // mask it as enabled / disabled - this.enable(isEnabled); - this._init = true; + this.enabled = isEnabled; } - hostData() { - return { - 'role': 'navigation', - 'side': this.getSide(), - 'type': this.type, - class: { - 'menu-enabled': this._canOpen() - } - }; + protected ionViewDidUnload() { + this.menuCtrl._unregister(this); + this.animation && this.animation.destroy(); + + this.menuCtrl = this.animation = null; + this.contentEl = this.backdropEl = this.menuInnerEl = null; } - getSide(): string { - return this.isRightSide ? 'right' : 'left'; + @Listen('body:ionSplitPaneDidChange') + splitPaneChanged(ev: SplitPaneAlert) { + this.isPane = ev.detail.splitPane.isPane(this.el); + this.updateState(); } - protected render() { - return ([ - , - , - - ]); - } - - /** - * @hidden - */ + @Listen('body:click', { enabled: false, capture: true }) onBackdropClick(ev: UIEvent) { - ev.preventDefault(); - ev.stopPropagation(); - this.close(); + const el = ev.target as HTMLElement; + if (!el.closest('.menu-inner') && this.lastOnEnd < (ev.timeStamp - 100)) { + ev.preventDefault(); + ev.stopPropagation(); + this.close(); + } } - /** - * @hidden - */ - private prepareAnimation(): Promise { - const width = this._menuInnerEle.offsetWidth; - if (width === this._width) { + @Method() + isOpen(): boolean { + return this._isOpen; + } + + @Method() + setOpen(shouldOpen: boolean, animated: boolean = true): Promise { + // If the menu is disabled or it is currenly being animated, let's do nothing + if (!this.isActive() || this.isAnimating || (shouldOpen === this._isOpen)) { + return Promise.resolve(this._isOpen); + } + + this.beforeAnimation(); + return this.loadAnimation() + .then(() => this.startAnimation(shouldOpen, animated)) + .then(() => this.afterAnimation(shouldOpen)); + } + + @Method() + open(): Promise { + return this.setOpen(true); + } + + @Method() + close(): Promise { + return this.setOpen(false); + } + + @Method() + toggle(): Promise { + return this.setOpen(!this._isOpen); + } + + private loadAnimation(): Promise { + // Menu swipe animation takes the menu's inner width as parameter, + // If `offsetWidth` changes, we need to create a new animation. + const width = this.menuInnerEl.offsetWidth; + if (width === this.width && this.animation !== null) { return Promise.resolve(); } - if (this._animation) { - this._animation.destroy(); - this._animation = null; - } - this._width = width; - return this.menuCtrl.create(this.type, this).then(ani => { - this._animation = ani; + // Destroy existing animation + this.animation && this.animation.destroy(); + this.animation = null; + this.width = width; + + // Create new animation + return this.menuCtrl.createAnimation(this.type, this).then(ani => { + this.animation = ani; }); } - /** - * @hidden - */ - 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); - } - this._before(); - return this.prepareAnimation() - .then(() => this._startAnimation(shouldOpen, animated)) - .then(() => { - this._after(shouldOpen); - return this._isOpen; - }); - } - - _startAnimation(shouldOpen: boolean, animated: boolean): Promise { + private startAnimation(shouldOpen: boolean, animated: boolean): Promise { let done; const promise = new Promise(resolve => done = resolve); - const ani = this._animation + const ani = this.animation .onFinish(done, {oneTimeCallback: true, clearExistingCallacks: true }) .reverse(!shouldOpen); @@ -265,98 +247,86 @@ export class Menu { return promise; } - _forceClosing() { - assert(this._isOpen, 'menu cannot be closed'); - - this._isAnimating = true; - this._startAnimation(false, false); - this._after(false); + private isActive(): boolean { + return this.enabled && !this.isPane; } - getWidth(): number { - return this._width; - } - - /** - * @hidden - */ - canSwipe(): boolean { + private canSwipe(): boolean { return this.swipeEnabled && - !this._isAnimating && - this._canOpen(); - // TODO: && this._app.isEnabled(); + !this.isAnimating && + this.isActive(); } - /** - * @hidden - */ - isAnimating(): boolean { - return this._isAnimating; + private canStart(detail: GestureDetail): boolean { + if (!this.canSwipe()) { + return false; + } + if (this._isOpen) { + return true; + } else if (this.menuCtrl.getOpen()) { + return false; + } + return checkEdgeSide(detail.currentX, this.isRightSide, this.maxEdgeStart); } - /** - * @hidden - */ - isOpen(): boolean { - return this._isOpen; + private onWillStart(): Promise { + this.beforeAnimation(); + return this.loadAnimation(); } - _swipeWillStart(): Promise { - this._before(); - return this.prepareAnimation(); - } - - _swipeStart() { - assert(!!this._animation, '_type is undefined'); - if (!this._isAnimating) { - assert(false, '_isAnimating has to be true'); + private onDragStart() { + assert(!!this.animation, '_type is undefined'); + if (!this.isAnimating) { + assert(false, 'isAnimating has to be true'); return; } // the cloned animation should not use an easing curve during seek - this._animation + this.animation .reverse(this._isOpen) .progressStart(); } - _swipeProgress(slide: any) { - assert(!!this._animation, '_type is undefined'); - if (!this._isAnimating) { - assert(false, '_isAnimating has to be true'); + private onDragMove(detail: GestureDetail) { + assert(!!this.animation, '_type is undefined'); + if (!this.isAnimating) { + assert(false, 'isAnimating has to be true'); return; } - const delta = computeDelta(slide.deltaX, this._isOpen, this.isRightSide); - const stepValue = delta / this._width; - this._animation.progressStep(stepValue); + const delta = computeDelta(detail.deltaX, this._isOpen, this.isRightSide); + const stepValue = delta / this.width; + this.animation.progressStep(stepValue); } - _swipeEnd(slide: any) { - assert(!!this._animation, '_type is undefined'); - if (!this._isAnimating) { - assert(false, '_isAnimating has to be true'); + private onDragEnd(detail: GestureDetail) { + assert(!!this.animation, '_type is undefined'); + if (!this.isAnimating) { + assert(false, 'isAnimating has to be true'); return; } + console.log('end'); + + const isOpen = this._isOpen; const isRightSide = this.isRightSide; - const delta = computeDelta(slide.deltaX, this._isOpen, isRightSide); - const width = this._width; + const delta = computeDelta(detail.deltaX, isOpen, isRightSide); + const width = this.width; const stepValue = delta / width; - const velocity = slide.velocityX; - const z = width / 2; + const velocity = detail.velocityX; + const z = width / 2.0; const shouldCompleteRight = (velocity >= 0) - && (velocity > 0.2 || slide.deltaX > z); + && (velocity > 0.2 || detail.deltaX > z); const shouldCompleteLeft = (velocity <= 0) - && (velocity < -0.2 || slide.deltaX < -z); + && (velocity < -0.2 || detail.deltaX < -z); - const opening = !this._isOpen; - const shouldComplete = (opening) - ? isRightSide ? shouldCompleteLeft : shouldCompleteRight - : isRightSide ? shouldCompleteRight : shouldCompleteLeft; + const shouldComplete = (isOpen) + ? isRightSide ? shouldCompleteRight : shouldCompleteLeft + : isRightSide ? shouldCompleteLeft : shouldCompleteRight; - let isOpen = (opening && shouldComplete); - if (!opening && !shouldComplete) { - isOpen = true; + let shouldOpen = (!isOpen && shouldComplete); + if (isOpen && !shouldComplete) { + shouldOpen = true; } const missing = shouldComplete ? 1 - stepValue : stepValue; @@ -367,25 +337,24 @@ export class Menu { realDur = Math.min(dur, 380); } - this._animation - .onFinish(() => this._after(isOpen), { clearExistingCallacks: true }) + this.lastOnEnd = detail.timeStamp; + this.animation + .onFinish(() => this.afterAnimation(shouldOpen), { clearExistingCallacks: true }) .progressEnd(shouldComplete, stepValue, realDur); } - private _before() { - assert(!this._isAnimating, '_before() should not be called while animating'); + private beforeAnimation() { + 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._backdropEle.classList.add('show-backdrop'); - - this.resize(); - this._isAnimating = true; + this.el.classList.add(SHOW_MENU); + this.backdropEl.classList.add(SHOW_BACKDROP); + this.isAnimating = true; } - private _after(isOpen: boolean) { - assert(this._isAnimating, '_before() should be called while animating'); + private afterAnimation(isOpen: boolean): boolean { + assert(this.isAnimating, '_before() should be called while animating'); // TODO: this._app.setEnabled(false, 100); @@ -394,193 +363,105 @@ export class Menu { // and only remove listeners/css if it's not open // emit opened/closed events this._isOpen = isOpen; - this._isAnimating = false; + this.isAnimating = false; // add/remove backdrop click listeners - this._backdropClick(isOpen); + Context.enableListener(this, 'body:click', isOpen); if (isOpen) { // disable swipe to go back gesture - this._activeBlock = GESTURE_BLOCKER; + this.gestureBlocker = GESTURE_BLOCKER; // add css class - this._cntElm.classList.add('menu-content-open'); + this.contentEl.classList.add(MENU_CONTENT_OPEN); // emit open event this.ionOpen.emit({ menu: this }); } else { // enable swipe to go back gesture - this._activeBlock = null; + this.gestureBlocker = null; // remove css classes - this.el.classList.remove('show-menu'); - this._cntElm.classList.remove('menu-content-open'); - this._backdropEle.classList.remove('show-menu'); + this.el.classList.remove(SHOW_MENU); + this.contentEl.classList.remove(MENU_CONTENT_OPEN); + this.backdropEl.classList.remove(SHOW_BACKDROP); // emit close event this.ionClose.emit({ menu: this }); } + return isOpen; } - /** - * @hidden - */ - open(): Promise { - return this.setOpen(true); - } - - /** - * @hidden - */ - close(): Promise { - return this.setOpen(false); - } - - /** - * @hidden - */ - resize() { - // TODO - // const content: Content | Nav = this.menuContent - // ? this.menuContent - // : this.menuNav; - // 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); - } - - _canOpen(): boolean { - return this.enabled && !this._isPane; - } - - /** - * @hidden - */ - // @PropDidChange('swipeEnabled') - // @PropDidChange('enabled') - _updateState() { - const canOpen = this._canOpen(); + private updateState() { + const isActive = this.isActive(); // Close menu inmediately - if (!canOpen && this._isOpen) { - assert(this._init, 'menu must be initialized'); + if (!isActive && this._isOpen) { // close if this menu is open, and should not be enabled - this._forceClosing(); + this.forceClosing(); } if (this.enabled && this.menuCtrl) { this.menuCtrl._setActiveMenu(this); } - - if (!this._init) { - return; - } - - if (this._isOpen || (this._isPane && this.enabled)) { - this.resize(); - } - assert(!this._isAnimating, 'can not be animating'); + assert(!this.isAnimating, 'can not be animating'); } - /** - * @hidden - */ - enable(shouldEnable: boolean): Menu { - this.enabled = shouldEnable; - return this; + private forceClosing() { + assert(this._isOpen, 'menu cannot be closed'); + + this.isAnimating = true; + this.startAnimation(false, false); + this.afterAnimation(false); } - /** - * @internal - */ - initPane(): boolean { - return false; + protected hostData() { + const typeClass = 'menu-type-' + this.type; + return { + role: 'navigation', + class: { + 'menu-enabled': this.isActive(), + 'menu-side-right': this.isRightSide, + 'menu-side-left': !this.isRightSide, + [typeClass]: true, + } + }; } - /** - * @hidden - */ - swipeEnable(shouldEnable: boolean): Menu { - this.swipeEnabled = shouldEnable; - return this; + protected render() { + return ([ + , + , + + ]); } - - /** - * @hidden - */ - getMenuElement(): HTMLElement { - return this.el.querySelector('.menu-inner') as HTMLElement; - } - - /** - * @hidden - */ - getContentElement(): HTMLElement { - return this._cntElm; - } - - /** - * @hidden - */ - getBackdropElement(): HTMLElement { - return this._backdropEle; - } - - /** - * @hidden - */ - getMenuController(): MenuController { - return this.menuCtrl; - } - - private _backdropClick(shouldAdd: boolean) { - const onBackdropClick = this.onBackdropClick.bind(this); - - if (shouldAdd && !this._unregBdClick) { - this._unregBdClick = Context.addListener(this._backdropEle, 'click', onBackdropClick, { capture: true }); - this._unregCntClick = Context.addListener(this._backdropEle, 'click', onBackdropClick, { capture: true }); - - } else if (!shouldAdd && this._unregBdClick) { - this._unregBdClick(); - this._unregCntClick(); - this._unregBdClick = this._unregCntClick = null; - } - } - - /** - * @hidden - */ - protected ionViewDidUnload() { - this._backdropClick(false); - - this.menuCtrl._unregister(this); - this._animation && this._animation.destroy(); - - this.menuCtrl = this._animation = this._cntElm = this._backdropEle = null; - } - } function computeDelta(deltaX: number, isOpen: boolean, isRightSide: boolean): number { return Math.max(0, (isOpen !== isRightSide) ? -deltaX : deltaX); } +const SHOW_MENU = 'show-menu'; +const SHOW_BACKDROP = 'show-backdrop'; +const MENU_CONTENT_OPEN = 'menu-content-open'; const GESTURE_BLOCKER = 'goback-swipe'; diff --git a/packages/core/src/components/menu/tests/basic.html b/packages/core/src/components/menu/tests/basic.html index 7271383c83..56bc3c3a66 100644 --- a/packages/core/src/components/menu/tests/basic.html +++ b/packages/core/src/components/menu/tests/basic.html @@ -64,8 +64,15 @@ - Open left menu - Open right menu +

+ Open left menu + Open right menu +

+

+ Set Push + Set Overlay + Set Reveal +

@@ -83,6 +90,18 @@ console.log('Open right menu'); menu.open('right'); } + function setPush() { + menu.get('left').type = 'push'; + menu.get('right').type = 'push'; + } + function setOverlay() { + menu.get('left').type = 'overlay'; + menu.get('right').type = 'overlay'; + } + function setReveal() { + menu.get('left').type = 'reveal'; + menu.get('right').type = 'reveal'; + }