fix(ion-menu): finish ion-menu and ion-split-pane

This commit is contained in:
Manu Mtz.-Almeida
2017-10-27 18:22:13 +02:00
parent d9d0150b4c
commit 687b37ad3e
11 changed files with 350 additions and 434 deletions

View File

@ -1541,7 +1541,7 @@ declare global {
_register?: any, _register?: any,
_unregister?: any, _unregister?: any,
_setActiveMenu?: any, _setActiveMenu?: any,
create?: any, createAnimation?: any,
animationCtrl?: any animationCtrl?: any
} }
} }
@ -1572,6 +1572,11 @@ declare global {
mode?: string, mode?: string,
color?: string, color?: string,
isOpen?: any,
setOpen?: any,
open?: any,
close?: any,
toggle?: any,
lazyMenuCtrl?: any, lazyMenuCtrl?: any,
content?: string, content?: string,
menuId?: string, menuId?: string,

View File

@ -8,7 +8,6 @@ import { PanRecognizer } from './recognizers';
}) })
export class Gesture { export class Gesture {
@Element() private el: HTMLElement;
private detail: GestureDetail = {}; private detail: GestureDetail = {};
private positions: number[] = []; private positions: number[] = [];
private ctrl: GestureController; private ctrl: GestureController;
@ -21,13 +20,8 @@ export class Gesture {
private hasFiredStart = true; private hasFiredStart = true;
private isMoveQueued = false; private isMoveQueued = false;
private blocker: BlockerDelegate; private blocker: BlockerDelegate;
private fireOnMoveFunc: any;
@Event() private ionGestureMove: EventEmitter; @Element() private el: HTMLElement;
@Event() private ionGestureStart: EventEmitter;
@Event() private ionGestureEnd: EventEmitter;
@Event() private ionGestureNotCaptured: EventEmitter;
@Event() private ionPress: EventEmitter;
@Prop() enabled: boolean = true; @Prop() enabled: boolean = true;
@Prop() attachTo: ElementRef = 'child'; @Prop() attachTo: ElementRef = 'child';
@ -49,9 +43,12 @@ export class Gesture {
@Prop() onPress: GestureCallback; @Prop() onPress: GestureCallback;
@Prop() notCaptured: GestureCallback; @Prop() notCaptured: GestureCallback;
constructor() { @Event() private ionGestureMove: EventEmitter;
this.fireOnMoveFunc = this.fireOnMove.bind(this); @Event() private ionGestureStart: EventEmitter;
} @Event() private ionGestureEnd: EventEmitter;
@Event() private ionGestureNotCaptured: EventEmitter;
@Event() private ionPress: EventEmitter;
protected ionViewDidLoad() { protected ionViewDidLoad() {
// in this case, we already know the GestureController and Gesture are already // in this case, we already know the GestureController and Gesture are already
@ -203,7 +200,7 @@ export class Gesture {
if (!this.isMoveQueued && this.hasFiredStart) { if (!this.isMoveQueued && this.hasFiredStart) {
this.isMoveQueued = true; this.isMoveQueued = true;
this.calcGestureData(ev); this.calcGestureData(ev);
Context.dom.write(this.fireOnMoveFunc); Context.dom.write(this.fireOnMove.bind(this));
} }
return; return;
} }
@ -221,6 +218,11 @@ export class Gesture {
} }
private fireOnMove() { 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; const detail = this.detail;
this.isMoveQueued = false; this.isMoveQueued = false;
if (this.onMove) { if (this.onMove) {
@ -312,6 +314,7 @@ export class Gesture {
private reset() { private reset() {
this.hasCapturedPan = false; this.hasCapturedPan = false;
this.hasStartedPan = false; this.hasStartedPan = false;
this.isMoveQueued = false;
this.hasFiredStart = true; this.hasFiredStart = true;
this.gesture && this.gesture.release(); this.gesture && this.gesture.release();
} }

View File

@ -9,7 +9,7 @@ import baseAnimation from './base';
*/ */
export default function(Animation: Animation, _: HTMLElement, menu: Menu): Animation { export default function(Animation: Animation, _: HTMLElement, menu: Menu): Animation {
let closedX: string, openedX: string; let closedX: string, openedX: string;
const width = menu.getWidth(); const width = menu.width;
if (menu.isRightSide) { if (menu.isRightSide) {
// right side // right side
closedX = 8 + width + 'px'; closedX = 8 + width + 'px';
@ -22,11 +22,11 @@ export default function(Animation: Animation, _: HTMLElement, menu: Menu): Anima
} }
const menuAni = new Animation() const menuAni = new Animation()
.addElement(menu.getMenuElement()) .addElement(menu.menuInnerEl)
.fromTo('translateX', closedX, openedX); .fromTo('translateX', closedX, openedX);
const backdropApi = new Animation() const backdropApi = new Animation()
.addElement(menu.getBackdropElement()) .addElement(menu.backdropEl)
.fromTo('opacity', 0.01, 0.35); .fromTo('opacity', 0.01, 0.35);
return baseAnimation(Animation) return baseAnimation(Animation)

View File

@ -10,7 +10,7 @@ import baseAnimation from './base';
export default function(Animation: Animation, _: HTMLElement, menu: Menu): Animation { export default function(Animation: Animation, _: HTMLElement, menu: Menu): Animation {
let contentOpenedX: string, menuClosedX: string, menuOpenedX: string; let contentOpenedX: string, menuClosedX: string, menuOpenedX: string;
const width = menu.getWidth(); const width = menu.width;
if (menu.isRightSide) { if (menu.isRightSide) {
contentOpenedX = -width + 'px'; contentOpenedX = -width + 'px';
@ -23,11 +23,11 @@ export default function(Animation: Animation, _: HTMLElement, menu: Menu): Anima
menuClosedX = -width + 'px'; menuClosedX = -width + 'px';
} }
const menuAni = new Animation() const menuAni = new Animation()
.addElement(menu.getMenuElement()) .addElement(menu.menuInnerEl)
.fromTo('translateX', menuClosedX, menuOpenedX); .fromTo('translateX', menuClosedX, menuOpenedX);
const contentAni = new Animation() const contentAni = new Animation()
.addElement(menu.getContentElement()) .addElement(menu.contentEl)
.fromTo('translateX', '0px', contentOpenedX); .fromTo('translateX', '0px', contentOpenedX);
return baseAnimation(Animation) return baseAnimation(Animation)

View File

@ -8,10 +8,10 @@ import baseAnimation from './base';
* The menu itself, which is under the content, does not move. * The menu itself, which is under the content, does not move.
*/ */
export default function(Animation: Animation, _: HTMLElement, menu: Menu): Animation { 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() const contentOpen = new Animation()
.addElement(menu.getContentElement()) .addElement(menu.contentEl)
.fromTo('translateX', '0px', openedX); .fromTo('translateX', '0px', openedX);
return baseAnimation(Animation) return baseAnimation(Animation)

View File

@ -1,5 +1,6 @@
import { Animation, AnimationBuilder, AnimationController, Menu } from '../../index'; import { Animation, AnimationBuilder, AnimationController, Menu } from '../../index';
import { Component, Method, Prop } from '@stencil/core'; import { Component, Method, Prop } from '@stencil/core';
import { HTMLIonMenuElement } from '../../index';
import MenuOverlayAnimation from './animations/overlay'; import MenuOverlayAnimation from './animations/overlay';
import MenuRevealAnimation from './animations/reveal'; import MenuRevealAnimation from './animations/reveal';
@ -48,22 +49,13 @@ export class MenuController {
*/ */
@Method() @Method()
close(menuId?: string): Promise<boolean> { close(menuId?: string): Promise<boolean> {
let menu: Menu; const menu = (menuId)
? this.get(menuId)
if (menuId) { : this.getOpen();
// find the menu by its id
menu = this.get(menuId);
} else {
// find the menu that is open
menu = this.getOpen();
}
if (menu) { if (menu) {
// close the menu
return menu.close(); return menu.close();
} }
return Promise.resolve(false); return Promise.resolve(false);
} }
@ -96,9 +88,12 @@ export class MenuController {
* @return {Menu} Returns the instance of the menu, which is useful for chaining. * @return {Menu} Returns the instance of the menu, which is useful for chaining.
*/ */
@Method() @Method()
enable(shouldEnable: boolean, menuId?: string): Menu { enable(shouldEnable: boolean, menuId?: string): HTMLIonMenuElement {
const menu = this.get(menuId); 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. * @return {Menu} Returns the instance of the menu, which is useful for chaining.
*/ */
@Method() @Method()
swipeEnable(shouldEnable: boolean, menuId?: string): Menu { swipeEnable(shouldEnable: boolean, menuId?: string): HTMLIonMenuElement {
const menu = this.get(menuId); 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) { if (menuId) {
var menu = this.get(menuId); var menu = this.get(menuId);
return menu && menu.isOpen() || false; return menu && menu.isOpen() || false;
} else {
return !!this.getOpen();
} }
return !!this.getOpen();
} }
/** /**
@ -135,7 +132,10 @@ export class MenuController {
@Method() @Method()
isEnabled(menuId?: string): boolean { isEnabled(menuId?: string): boolean {
const menu = this.get(menuId); 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`. * @return {Menu} Returns the instance of the menu if found, otherwise `null`.
*/ */
@Method() @Method()
get(menuId?: string): Menu { get(menuId?: string): HTMLIonMenuElement {
var menu: Menu; var menu: Menu;
if (menuId === 'left' || menuId === 'right') { if (menuId === 'left' || menuId === 'right') {
@ -156,43 +156,43 @@ export class MenuController {
// so first try to get the enabled one // 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) { if (menu) {
return menu; return menu.el;
} }
// didn't find a menu side that is enabled // didn't find a menu side that is enabled
// so try to get the first menu side found // 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) { } else if (menuId) {
// the menuId was not left or right // the menuId was not left or right
// so try to get the menu by its "id" // 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 // return the first enabled menu
menu = this.menus.find(m => m.enabled); menu = this.menus.find(m => m.enabled);
if (menu) { if (menu) {
return menu; return menu.el;
} }
// get the first menu in the array, if one exists // 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`. * @return {Menu} Returns the instance of the menu already opened, otherwise `null`.
*/ */
@Method() @Method()
getOpen(): Menu { getOpen(): HTMLIonMenuElement {
return this.menus.find(m => m.isOpen()); return this.find(m => m.isOpen());
} }
/** /**
* @return {Array<Menu>} Returns an array of all menu instances. * @return {Array<Menu>} Returns an array of all menu instances.
*/ */
@Method() @Method()
getMenus(): Menu[] { getMenus(): HTMLIonMenuElement[] {
return this.menus; return this.menus.map(menu => menu.el);
} }
/** /**
@ -201,7 +201,7 @@ export class MenuController {
*/ */
@Method() @Method()
isAnimating(): boolean { 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; const side = menu.side;
this.menus this.menus
.filter(m => m.side === side && m !== menu) .filter(m => m.side === side && m !== menu)
.map(m => m.enable(false)); .map(m => m.enabled = false);
}
/**
* @hidden
*/
registerAnimation(name: string, cls: AnimationBuilder) {
this.menuAnimations[name] = cls;
} }
/** /**
* @hidden * @hidden
*/ */
@Method() @Method()
create(type: string, menuCmp: Menu): Promise<Animation> { createAnimation(type: string, menuCmp: Menu): Promise<Animation> {
const animationBuilder = this.menuAnimations[type]; const animationBuilder = this.menuAnimations[type];
return this.animationCtrl.create(animationBuilder, null, menuCmp); 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;
}
} }

View File

@ -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-box-shadow: 0 0 10px $menu-ios-box-shadow-color !default;
.menu-ios { .menu-ios .menu-inner {
background: $menu-ios-background; background: $menu-ios-background;
} }
.menu-ios .menu-content-reveal { .menu-ios.menu-type-overlay .menu-inner {
box-shadow: $menu-ios-box-shadow; 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; box-shadow: $menu-ios-box-shadow;
} }
ion-menu[type=overlay] .menu-ios { .app-ios .menu-content-push {
box-shadow: $menu-ios-box-shadow; box-shadow: $menu-ios-box-shadow;
} }

View File

@ -19,14 +19,17 @@ $menu-md-box-shadow: 0 0 10px $menu-md-box-shadow-color !default;
background: $menu-md-background; background: $menu-md-background;
} }
.menu-md .menu-content-reveal { .menu-md.menu-type-overlay .menu-inner {
box-shadow: $menu-md-box-shadow; 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; box-shadow: $menu-md-box-shadow;
} }
ion-menu[type=overlay] .menu-md { .app-md .menu-content-push {
box-shadow: $menu-md-box-shadow; box-shadow: $menu-md-box-shadow;
} }

View File

@ -46,7 +46,7 @@ ion-menu.show-menu {
position: absolute; position: absolute;
} }
ion-menu[side=left] > .menu-inner { .menu-side-left > .menu-inner {
@include multi-dir() { @include multi-dir() {
// scss-lint:disable PropertySpelling // scss-lint:disable PropertySpelling
right: auto; 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() { @include multi-dir() {
// scss-lint:disable PropertySpelling // scss-lint:disable PropertySpelling
right: 0; 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 { ion-menu ion-backdrop {
z-index: -1; z-index: -1;
display: none; display: none;
@ -84,6 +80,7 @@ ion-menu ion-backdrop {
} }
.menu-content-open ion-pane, .menu-content-open ion-pane,
.menu-content-open .ion-pane,
.menu-content-open ion-content, .menu-content-open ion-content,
.menu-content-open .toolbar { .menu-content-open .toolbar {
// the containing element itself should be clickable but // 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 content slides over to reveal the menu underneath.
// The menu itself, which is under the content, does not move. // The menu itself, which is under the content, does not move.
ion-menu[type=reveal] { ion-menu.menu-type-reveal {
z-index: 0; 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)); @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 // The menu slides over the content. The content
// itself, which is under the menu, does not move. // itself, which is under the menu, does not move.
ion-menu[type=overlay] { ion-menu.menu-type-overlay {
z-index: $z-index-menu-overlay; z-index: $z-index-menu-overlay;
} }
ion-menu[type=overlay] .show-backdrop { ion-menu.menu-type-overlay .show-backdrop {
display: block; display: block;
cursor: pointer;
} }

View File

@ -1,5 +1,5 @@
import { Component, Element, Event, EventEmitter, Listen, Prop, PropDidChange } from '@stencil/core'; import { Component, Element, Event, EventEmitter, Listen, Method, Prop, PropDidChange, PropWillChange } from '@stencil/core';
import { Animation, Config, SplitPaneAlert } from '../../index'; import { Animation, Config, GestureDetail, HTMLIonMenuElement, SplitPaneAlert } from '../../index';
import { MenuController } from './menu-controller'; import { MenuController } from './menu-controller';
import { Side, assert, checkEdgeSide, isRightSide } from '../../utils/helpers'; import { Side, assert, checkEdgeSide, isRightSide } from '../../utils/helpers';
@ -20,38 +20,27 @@ export type Lazy<T> = T &
}) })
export class Menu { export class Menu {
private _backdropEle: HTMLElement; private gestureBlocker: string;
private _menuInnerEle: HTMLElement; private animation: Animation;
private _unregCntClick: Function; private isPane = false;
private _unregBdClick: Function;
private _activeBlock: string;
private _cntElm: HTMLElement;
private _animation: Animation;
private _init = false;
private _isPane = false;
private _isAnimating: boolean = false;
private _isOpen: boolean = false; private _isOpen: boolean = false;
private _width: number = null; private lastOnEnd = 0;
mode: string; mode: string;
color: string; color: string;
isAnimating: boolean = false;
/**
* @hidden
*/
isRightSide: boolean = false; isRightSide: boolean = false;
width: number = null;
@Element() private el: HTMLElement; backdropEl: HTMLElement;
menuInnerEl: HTMLElement;
contentEl: HTMLElement;
menuCtrl: MenuController;
@Event() ionDrag: EventEmitter; @Element() el: HTMLIonMenuElement;
@Event() ionOpen: EventEmitter;
@Event() ionClose: EventEmitter;
@Prop({ context: 'config' }) config: Config; @Prop({ context: 'config' }) config: Config;
@Prop({ connect: 'ion-menu-controller' }) lazyMenuCtrl: Lazy<MenuController>; @Prop({ connect: 'ion-menu-controller' }) lazyMenuCtrl: Lazy<MenuController>;
menuCtrl: MenuController;
/** /**
* @input {string} The content's id the menu should use. * @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: * see the `menuType` in the [config](../../config/Config). Available options:
* `"overlay"`, `"reveal"`, `"push"`. * `"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`. * @input {boolean} If true, the menu is enabled. Default `true`.
*/ */
@Prop({ mutable: true }) enabled: boolean; @Prop({ mutable: true }) enabled: boolean;
@PropDidChange('enabled')
enabledChanged() {
this.updateState();
}
/** /**
* @input {string} Which side of the view the menu should be placed. Default `"start"`. * @input {string} Which side of the view the menu should be placed. Default `"start"`.
*/ */
@Prop() side: Side = '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`. * @input {boolean} If true, swiping the menu is enabled. Default `true`.
*/ */
@Prop() swipeEnabled: boolean = true; @Prop() swipeEnabled: boolean = true;
@PropDidChange('swipeEnabled')
swipeEnabledChange() {
this.updateState();
}
/** /**
* @input {boolean} If true, the menu will persist on child pages. * @input {boolean} If true, the menu will persist on child pages.
@ -96,57 +111,36 @@ export class Menu {
@Prop() maxEdgeStart: number = 50; @Prop() maxEdgeStart: number = 50;
// @PropDidChange('side') @Event() ionDrag: EventEmitter;
// sideChanged(side: Side) { @Event() ionOpen: EventEmitter;
// // TODO: const isRTL = this._plt.isRTL; @Event() ionClose: EventEmitter;
// 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();
}
protected ionViewWillLoad() { protected ionViewWillLoad() {
return this.lazyMenuCtrl.componentOnReady() return this.lazyMenuCtrl.componentOnReady()
.then(menu => this.menuCtrl = menu); .then(menu => this.menuCtrl = menu);
} }
/**
* @hidden
*/
protected ionViewDidLoad() { protected ionViewDidLoad() {
assert(!!this.menuCtrl, 'menucontroller was not initialized'); assert(!!this.menuCtrl, 'menucontroller was not initialized');
this._menuInnerEle = this.el.querySelector('.menu-inner') as HTMLElement; const el = this.el;
this._backdropEle = this.el.querySelector('.menu-backdrop') as HTMLElement;
const contentQuery = (this.content) const contentQuery = (this.content)
? '> #' + this.content ? '#' + this.content
: '[main]'; : '[main]';
const parent = this.el.parentElement; const parent = el.parentElement;
const content = this._cntElm = parent.querySelector(contentQuery) as HTMLElement; const content = this.contentEl = parent.querySelector(contentQuery) as HTMLElement;
if (!content || !content.tagName) { if (!content || !content.tagName) {
// requires content element // requires content element
return console.error('Menu: must have a "content" element to listen for drag events on.'); return console.error('Menu: must have a "content" element to listen for drag events on.');
} }
// TODO: make PropDidChange work this.menuInnerEl = el.querySelector('.menu-inner') as HTMLElement;
this.isRightSide = isRightSide(this.side, false); this.backdropEl = el.querySelector('.menu-backdrop') as HTMLElement;
// add menu's content classes // add menu's content classes
content.classList.add('menu-content'); content.classList.add('menu-content');
content.classList.add('menu-content-' + this.type);
this.typeChanged(this.type);
this.sideChanged();
let isEnabled = this.enabled; let isEnabled = this.enabled;
if (isEnabled === true || typeof isEnabled === 'undefined') { if (isEnabled === true || typeof isEnabled === 'undefined') {
@ -159,100 +153,88 @@ export class Menu {
this.menuCtrl._register(this); this.menuCtrl._register(this);
// mask it as enabled / disabled // mask it as enabled / disabled
this.enable(isEnabled); this.enabled = isEnabled;
this._init = true;
} }
hostData() { protected ionViewDidUnload() {
return { this.menuCtrl._unregister(this);
'role': 'navigation', this.animation && this.animation.destroy();
'side': this.getSide(),
'type': this.type, this.menuCtrl = this.animation = null;
class: { this.contentEl = this.backdropEl = this.menuInnerEl = null;
'menu-enabled': this._canOpen()
}
};
} }
getSide(): string { @Listen('body:ionSplitPaneDidChange')
return this.isRightSide ? 'right' : 'left'; splitPaneChanged(ev: SplitPaneAlert) {
this.isPane = ev.detail.splitPane.isPane(this.el);
this.updateState();
} }
protected render() { @Listen('body:click', { enabled: false, capture: true })
return ([
<div class='menu-inner'>
<slot></slot>
</div>,
<ion-backdrop class='menu-backdrop'></ion-backdrop> ,
<ion-gesture {...{
'canStart': this.canStart.bind(this),
'onWillStart': this._swipeWillStart.bind(this),
'onStart': this._swipeStart.bind(this),
'onMove': this._swipeProgress.bind(this),
'onEnd': this._swipeEnd.bind(this),
'maxEdgeStart': this.maxEdgeStart,
'edge': this.side,
'enabled': this._canOpen() && this.swipeEnabled,
'gestureName': 'menu-swipe',
'gesturePriority': 10,
'type': 'pan',
'direction': 'x',
'threshold': 10,
'attachTo': 'body',
'disableScroll': true,
'block': this._activeBlock
}}></ion-gesture>
]);
}
/**
* @hidden
*/
onBackdropClick(ev: UIEvent) { onBackdropClick(ev: UIEvent) {
ev.preventDefault(); const el = ev.target as HTMLElement;
ev.stopPropagation(); if (!el.closest('.menu-inner') && this.lastOnEnd < (ev.timeStamp - 100)) {
this.close(); ev.preventDefault();
ev.stopPropagation();
this.close();
}
} }
/** @Method()
* @hidden isOpen(): boolean {
*/ return this._isOpen;
private prepareAnimation(): Promise<void> { }
const width = this._menuInnerEle.offsetWidth;
if (width === this._width) { @Method()
setOpen(shouldOpen: boolean, animated: boolean = true): Promise<boolean> {
// 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<boolean> {
return this.setOpen(true);
}
@Method()
close(): Promise<boolean> {
return this.setOpen(false);
}
@Method()
toggle(): Promise<boolean> {
return this.setOpen(!this._isOpen);
}
private loadAnimation(): Promise<void> {
// 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(); return Promise.resolve();
} }
if (this._animation) { // Destroy existing animation
this._animation.destroy(); this.animation && this.animation.destroy();
this._animation = null; this.animation = null;
} this.width = width;
this._width = width;
return this.menuCtrl.create(this.type, this).then(ani => { // Create new animation
this._animation = ani; return this.menuCtrl.createAnimation(this.type, this).then(ani => {
this.animation = ani;
}); });
} }
/** private startAnimation(shouldOpen: boolean, animated: boolean): Promise<Animation> {
* @hidden
*/
setOpen(shouldOpen: boolean, animated: boolean = true): Promise<boolean> {
// 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<Animation> {
let done; let done;
const promise = new Promise<Animation>(resolve => done = resolve); const promise = new Promise<Animation>(resolve => done = resolve);
const ani = this._animation const ani = this.animation
.onFinish(done, {oneTimeCallback: true, clearExistingCallacks: true }) .onFinish(done, {oneTimeCallback: true, clearExistingCallacks: true })
.reverse(!shouldOpen); .reverse(!shouldOpen);
@ -265,98 +247,86 @@ export class Menu {
return promise; return promise;
} }
_forceClosing() { private isActive(): boolean {
assert(this._isOpen, 'menu cannot be closed'); return this.enabled && !this.isPane;
this._isAnimating = true;
this._startAnimation(false, false);
this._after(false);
} }
getWidth(): number { private canSwipe(): boolean {
return this._width;
}
/**
* @hidden
*/
canSwipe(): boolean {
return this.swipeEnabled && return this.swipeEnabled &&
!this._isAnimating && !this.isAnimating &&
this._canOpen(); this.isActive();
// TODO: && this._app.isEnabled();
} }
/** private canStart(detail: GestureDetail): boolean {
* @hidden if (!this.canSwipe()) {
*/ return false;
isAnimating(): boolean { }
return this._isAnimating; if (this._isOpen) {
return true;
} else if (this.menuCtrl.getOpen()) {
return false;
}
return checkEdgeSide(detail.currentX, this.isRightSide, this.maxEdgeStart);
} }
/** private onWillStart(): Promise<void> {
* @hidden this.beforeAnimation();
*/ return this.loadAnimation();
isOpen(): boolean {
return this._isOpen;
} }
_swipeWillStart(): Promise<void> { private onDragStart() {
this._before(); assert(!!this.animation, '_type is undefined');
return this.prepareAnimation(); if (!this.isAnimating) {
} assert(false, 'isAnimating has to be true');
_swipeStart() {
assert(!!this._animation, '_type is undefined');
if (!this._isAnimating) {
assert(false, '_isAnimating has to be true');
return; return;
} }
// the cloned animation should not use an easing curve during seek // the cloned animation should not use an easing curve during seek
this._animation this.animation
.reverse(this._isOpen) .reverse(this._isOpen)
.progressStart(); .progressStart();
} }
_swipeProgress(slide: any) { private onDragMove(detail: GestureDetail) {
assert(!!this._animation, '_type is undefined'); assert(!!this.animation, '_type is undefined');
if (!this._isAnimating) { if (!this.isAnimating) {
assert(false, '_isAnimating has to be true'); assert(false, 'isAnimating has to be true');
return; return;
} }
const delta = computeDelta(slide.deltaX, this._isOpen, this.isRightSide); const delta = computeDelta(detail.deltaX, this._isOpen, this.isRightSide);
const stepValue = delta / this._width; const stepValue = delta / this.width;
this._animation.progressStep(stepValue); this.animation.progressStep(stepValue);
} }
_swipeEnd(slide: any) { private onDragEnd(detail: GestureDetail) {
assert(!!this._animation, '_type is undefined'); assert(!!this.animation, '_type is undefined');
if (!this._isAnimating) { if (!this.isAnimating) {
assert(false, '_isAnimating has to be true'); assert(false, 'isAnimating has to be true');
return; return;
} }
console.log('end');
const isOpen = this._isOpen;
const isRightSide = this.isRightSide; const isRightSide = this.isRightSide;
const delta = computeDelta(slide.deltaX, this._isOpen, isRightSide); const delta = computeDelta(detail.deltaX, isOpen, isRightSide);
const width = this._width; const width = this.width;
const stepValue = delta / width; const stepValue = delta / width;
const velocity = slide.velocityX; const velocity = detail.velocityX;
const z = width / 2; const z = width / 2.0;
const shouldCompleteRight = (velocity >= 0) const shouldCompleteRight = (velocity >= 0)
&& (velocity > 0.2 || slide.deltaX > z); && (velocity > 0.2 || detail.deltaX > z);
const shouldCompleteLeft = (velocity <= 0) const shouldCompleteLeft = (velocity <= 0)
&& (velocity < -0.2 || slide.deltaX < -z); && (velocity < -0.2 || detail.deltaX < -z);
const opening = !this._isOpen; const shouldComplete = (isOpen)
const shouldComplete = (opening) ? isRightSide ? shouldCompleteRight : shouldCompleteLeft
? isRightSide ? shouldCompleteLeft : shouldCompleteRight : isRightSide ? shouldCompleteLeft : shouldCompleteRight;
: isRightSide ? shouldCompleteRight : shouldCompleteLeft;
let isOpen = (opening && shouldComplete); let shouldOpen = (!isOpen && shouldComplete);
if (!opening && !shouldComplete) { if (isOpen && !shouldComplete) {
isOpen = true; shouldOpen = true;
} }
const missing = shouldComplete ? 1 - stepValue : stepValue; const missing = shouldComplete ? 1 - stepValue : stepValue;
@ -367,25 +337,24 @@ export class Menu {
realDur = Math.min(dur, 380); realDur = Math.min(dur, 380);
} }
this._animation this.lastOnEnd = detail.timeStamp;
.onFinish(() => this._after(isOpen), { clearExistingCallacks: true }) this.animation
.onFinish(() => this.afterAnimation(shouldOpen), { clearExistingCallacks: true })
.progressEnd(shouldComplete, stepValue, realDur); .progressEnd(shouldComplete, stepValue, realDur);
} }
private _before() { private beforeAnimation() {
assert(!this._isAnimating, '_before() should not be called while animating'); assert(!this.isAnimating, '_before() should not be called while animating');
// this places the menu into the correct location before it animates in // this places the menu into the correct location before it animates in
// this css class doesn't actually kick off any animations // this css class doesn't actually kick off any animations
this.el.classList.add('show-menu'); this.el.classList.add(SHOW_MENU);
this._backdropEle.classList.add('show-backdrop'); this.backdropEl.classList.add(SHOW_BACKDROP);
this.isAnimating = true;
this.resize();
this._isAnimating = true;
} }
private _after(isOpen: boolean) { private afterAnimation(isOpen: boolean): boolean {
assert(this._isAnimating, '_before() should be called while animating'); assert(this.isAnimating, '_before() should be called while animating');
// TODO: this._app.setEnabled(false, 100); // TODO: this._app.setEnabled(false, 100);
@ -394,193 +363,105 @@ export class Menu {
// and only remove listeners/css if it's not open // and only remove listeners/css if it's not open
// emit opened/closed events // emit opened/closed events
this._isOpen = isOpen; this._isOpen = isOpen;
this._isAnimating = false; this.isAnimating = false;
// add/remove backdrop click listeners // add/remove backdrop click listeners
this._backdropClick(isOpen); Context.enableListener(this, 'body:click', isOpen);
if (isOpen) { if (isOpen) {
// disable swipe to go back gesture // disable swipe to go back gesture
this._activeBlock = GESTURE_BLOCKER; this.gestureBlocker = GESTURE_BLOCKER;
// add css class // add css class
this._cntElm.classList.add('menu-content-open'); this.contentEl.classList.add(MENU_CONTENT_OPEN);
// emit open event // emit open event
this.ionOpen.emit({ menu: this }); this.ionOpen.emit({ menu: this });
} else { } else {
// enable swipe to go back gesture // enable swipe to go back gesture
this._activeBlock = null; this.gestureBlocker = null;
// remove css classes // remove css classes
this.el.classList.remove('show-menu'); this.el.classList.remove(SHOW_MENU);
this._cntElm.classList.remove('menu-content-open'); this.contentEl.classList.remove(MENU_CONTENT_OPEN);
this._backdropEle.classList.remove('show-menu'); this.backdropEl.classList.remove(SHOW_BACKDROP);
// emit close event // emit close event
this.ionClose.emit({ menu: this }); this.ionClose.emit({ menu: this });
} }
return isOpen;
} }
/** private updateState() {
* @hidden const isActive = this.isActive();
*/
open(): Promise<boolean> {
return this.setOpen(true);
}
/**
* @hidden
*/
close(): Promise<boolean> {
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<boolean> {
return this.setOpen(!this._isOpen);
}
_canOpen(): boolean {
return this.enabled && !this._isPane;
}
/**
* @hidden
*/
// @PropDidChange('swipeEnabled')
// @PropDidChange('enabled')
_updateState() {
const canOpen = this._canOpen();
// Close menu inmediately // Close menu inmediately
if (!canOpen && this._isOpen) { if (!isActive && this._isOpen) {
assert(this._init, 'menu must be initialized');
// close if this menu is open, and should not be enabled // close if this menu is open, and should not be enabled
this._forceClosing(); this.forceClosing();
} }
if (this.enabled && this.menuCtrl) { if (this.enabled && this.menuCtrl) {
this.menuCtrl._setActiveMenu(this); this.menuCtrl._setActiveMenu(this);
} }
assert(!this.isAnimating, 'can not be animating');
if (!this._init) {
return;
}
if (this._isOpen || (this._isPane && this.enabled)) {
this.resize();
}
assert(!this._isAnimating, 'can not be animating');
} }
/** private forceClosing() {
* @hidden assert(this._isOpen, 'menu cannot be closed');
*/
enable(shouldEnable: boolean): Menu { this.isAnimating = true;
this.enabled = shouldEnable; this.startAnimation(false, false);
return this; this.afterAnimation(false);
} }
/** protected hostData() {
* @internal const typeClass = 'menu-type-' + this.type;
*/ return {
initPane(): boolean { role: 'navigation',
return false; class: {
'menu-enabled': this.isActive(),
'menu-side-right': this.isRightSide,
'menu-side-left': !this.isRightSide,
[typeClass]: true,
}
};
} }
/** protected render() {
* @hidden return ([
*/ <div class='menu-inner page-inner'>
swipeEnable(shouldEnable: boolean): Menu { <slot></slot>
this.swipeEnabled = shouldEnable; </div>,
return this; <ion-backdrop class='menu-backdrop'></ion-backdrop> ,
<ion-gesture {...{
'canStart': this.canStart.bind(this),
'onWillStart': this.onWillStart.bind(this),
'onStart': this.onDragStart.bind(this),
'onMove': this.onDragMove.bind(this),
'onEnd': this.onDragEnd.bind(this),
'maxEdgeStart': this.maxEdgeStart,
'edge': this.side,
'enabled': this.isActive() && this.swipeEnabled,
'gestureName': 'menu-swipe',
'gesturePriority': 10,
'type': 'pan',
'direction': 'x',
'threshold': 10,
'attachTo': 'body',
'disableScroll': true,
'block': this.gestureBlocker
}}></ion-gesture>
]);
} }
/**
* @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 { function computeDelta(deltaX: number, isOpen: boolean, isRightSide: boolean): number {
return Math.max(0, (isOpen !== isRightSide) ? -deltaX : deltaX); 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'; const GESTURE_BLOCKER = 'goback-swipe';

View File

@ -64,8 +64,15 @@
</ion-header> </ion-header>
<ion-content padding> <ion-content padding>
<ion-button onclick="openLeft()">Open left menu</ion-button> <p>
<ion-button onclick="openRight()">Open right menu</ion-button> <ion-button onclick="openLeft()">Open left menu</ion-button>
<ion-button onclick="openRight()">Open right menu</ion-button>
</p>
<p>
<ion-button onclick="setPush()">Set Push</ion-button>
<ion-button onclick="setOverlay()">Set Overlay</ion-button>
<ion-button onclick="setReveal()">Set Reveal</ion-button>
</p>
</ion-content> </ion-content>
</ion-page> </ion-page>
@ -83,6 +90,18 @@
console.log('Open right menu'); console.log('Open right menu');
menu.open('right'); 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';
}
</script> </script>
</body> </body>
</html> </html>