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,
_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,

View File

@ -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();
}

View File

@ -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)

View File

@ -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)

View File

@ -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)

View File

@ -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<boolean> {
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<Menu>} 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<Animation> {
createAnimation(type: string, menuCmp: Menu): Promise<Animation> {
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;
}
}

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 {
.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;
}

View File

@ -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;
}

View File

@ -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;
}

View File

@ -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> = 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<MenuController>;
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 ([
<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
*/
@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<void> {
const width = this._menuInnerEle.offsetWidth;
if (width === this._width) {
@Method()
isOpen(): boolean {
return this._isOpen;
}
@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();
}
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<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> {
private startAnimation(shouldOpen: boolean, animated: boolean): Promise<Animation> {
let done;
const promise = new Promise<Animation>(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<void> {
this.beforeAnimation();
return this.loadAnimation();
}
_swipeWillStart(): Promise<void> {
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<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();
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 ([
<div class='menu-inner page-inner'>
<slot></slot>
</div>,
<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 {
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';

View File

@ -64,8 +64,15 @@
</ion-header>
<ion-content padding>
<ion-button onclick="openLeft()">Open left menu</ion-button>
<ion-button onclick="openRight()">Open right menu</ion-button>
<p>
<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-page>
@ -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';
}
</script>
</body>
</html>