mirror of
https://github.com/ionic-team/ionic-framework.git
synced 2025-08-22 05:21:52 +08:00
510 lines
11 KiB
TypeScript
510 lines
11 KiB
TypeScript
import { Component, h, Prop, Watch } from '@stencil/core';
|
|
import { Ionic } from '../../utils/interfaces';
|
|
import { VNodeData, GlobalNamespace, Menu as IMenu } from '../../utils/interfaces';
|
|
import { MenuController } from './menu-controller';
|
|
import { MenuType } from './menu-types';
|
|
|
|
|
|
@Component({
|
|
tag: 'ion-menu',
|
|
styleUrls: {
|
|
ios: 'menu.ios.scss',
|
|
md: 'menu.md.scss',
|
|
wp: 'menu.wp.scss'
|
|
},
|
|
host: {
|
|
theme: 'menu'
|
|
}
|
|
})
|
|
export class Menu implements IMenu {
|
|
private $el: HTMLElement;
|
|
private _backdropElm: HTMLElement;
|
|
private _ctrl: MenuController;
|
|
private _unregCntClick: Function;
|
|
private _unregBdClick: Function;
|
|
private _activeBlock: string;
|
|
|
|
private _cntElm: HTMLElement;
|
|
private _type: MenuType;
|
|
private _init = false;
|
|
private _isPane = false;
|
|
|
|
mode: string;
|
|
color: string;
|
|
|
|
/**
|
|
* @hidden
|
|
*/
|
|
@Prop() isOpen: boolean = false;
|
|
|
|
/**
|
|
* @hidden
|
|
*/
|
|
@Prop() isAnimating: boolean = false;
|
|
|
|
/**
|
|
* @hidden
|
|
*/
|
|
isRightSide: boolean = false;
|
|
|
|
/**
|
|
* @input {any} A reference to the content element the menu should use.
|
|
*/
|
|
@Prop() content: any;
|
|
|
|
/**
|
|
* @input {string} An id for the menu.
|
|
*/
|
|
@Prop() id: string;
|
|
|
|
/**
|
|
* @input {string} The display type of the menu. Default varies based on the mode,
|
|
* see the `menuType` in the [config](../../config/Config). Available options:
|
|
* `"overlay"`, `"reveal"`, `"push"`.
|
|
*/
|
|
@Prop() type: string;
|
|
|
|
/**
|
|
* @input {boolean} If true, the menu is enabled. Default `true`.
|
|
*/
|
|
@Prop() enabled: boolean;
|
|
|
|
/**
|
|
* @input {string} Which side of the view the menu should be placed. Default `"start"`.
|
|
*/
|
|
@Prop() side: string = 'start';
|
|
|
|
/**
|
|
* @input {boolean} If true, swiping the menu is enabled. Default `true`.
|
|
*/
|
|
@Prop() swipeEnabled: boolean;
|
|
|
|
@Watch('swipeEnabled')
|
|
swipeEnabledChange(isEnabled: boolean) {
|
|
this.swipeEnable(isEnabled);
|
|
}
|
|
|
|
/**
|
|
* @input {boolean} If true, the menu will persist on child pages.
|
|
*/
|
|
@Prop() persistent: boolean = false;
|
|
|
|
/**
|
|
* @hidden
|
|
*/
|
|
@Prop() maxEdgeStart: number;
|
|
|
|
|
|
constructor() {
|
|
// get or create the MenuController singleton
|
|
this._ctrl = (Ionic as GlobalNamespace).controllers.menu = ((Ionic as GlobalNamespace).controllers.menu || new MenuController());
|
|
}
|
|
|
|
|
|
/**
|
|
* @hidden
|
|
*/
|
|
ionViewDidLoad() {
|
|
this._backdropElm = this.$el.querySelector('.menu-backdrop') as HTMLElement;
|
|
|
|
this._init = true;
|
|
|
|
if (this.content) {
|
|
if ((this.content).tagName as HTMLElement) {
|
|
this._cntElm = this.content;
|
|
} else if (typeof this.content === 'string') {
|
|
this._cntElm = document.querySelector(this.content) as any;
|
|
}
|
|
}
|
|
|
|
if (!this._cntElm || !this._cntElm.tagName) {
|
|
// requires content element
|
|
return console.error('Menu: must have a "content" element to listen for drag events on.');
|
|
}
|
|
|
|
// add menu's content classes
|
|
this._cntElm.classList.add('menu-content');
|
|
this._cntElm.classList.add('menu-content-' + this.type);
|
|
|
|
let isEnabled = this.enabled;
|
|
if (isEnabled === true || typeof isEnabled === 'undefined') {
|
|
// check if more than one menu is on the same side
|
|
isEnabled = !this._ctrl.getMenus().some(m => {
|
|
return m.side === this.side && m.enabled;
|
|
});
|
|
}
|
|
// register this menu with the app's menu controller
|
|
this._ctrl._register(this);
|
|
|
|
// mask it as enabled / disabled
|
|
this.enable(isEnabled);
|
|
}
|
|
|
|
hostData(): VNodeData {
|
|
return {
|
|
attrs: {
|
|
'role': 'navigation',
|
|
'side': this.side,
|
|
'type': this.type
|
|
},
|
|
class: {
|
|
'menu-enabled': this.enabled
|
|
}
|
|
};
|
|
}
|
|
|
|
render() {
|
|
// normalize the "type"
|
|
if (!this.type) {
|
|
this.type = Ionic.config.get('menuType', 'overlay');
|
|
}
|
|
|
|
return [
|
|
<div class='menu-inner'>
|
|
<slot></slot>
|
|
</div>,
|
|
<ion-gesture class='menu-backdrop' props={{
|
|
// 'canStart': this.canStart.bind(this),
|
|
// 'onStart': this.onDragStart.bind(this),
|
|
// 'onMove': this.onDragMove.bind(this),
|
|
// 'onEnd': this.onDragEnd.bind(this),
|
|
'gestureName': 'menu-swipe',
|
|
'gesturePriority': 10,
|
|
'type': 'pan',
|
|
'direction': 'x',
|
|
'threshold': 5,
|
|
'attachTo': 'body',
|
|
'disableScroll': true,
|
|
'block': this._activeBlock
|
|
}}></ion-gesture>
|
|
];
|
|
}
|
|
|
|
/**
|
|
* @hidden
|
|
*/
|
|
onBackdropClick(ev: UIEvent) {
|
|
ev.preventDefault();
|
|
ev.stopPropagation();
|
|
this._ctrl.close();
|
|
}
|
|
|
|
/**
|
|
* @hidden
|
|
*/
|
|
private _getType(): MenuType {
|
|
if (!this._type) {
|
|
this._type = this._ctrl.create(this.type, this);
|
|
|
|
if (Ionic.config.getBoolean('animate') === false) {
|
|
this._type.ani.duration(0);
|
|
}
|
|
}
|
|
return this._type;
|
|
}
|
|
|
|
/**
|
|
* @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);
|
|
}
|
|
return new Promise(resolve => {
|
|
this._before();
|
|
this._getType().setOpen(shouldOpen, animated, () => {
|
|
this._after(shouldOpen);
|
|
resolve(this.isOpen);
|
|
});
|
|
});
|
|
}
|
|
|
|
_forceClosing() {
|
|
this.isAnimating = true;
|
|
this._getType().setOpen(false, false, () => {
|
|
this._after(false);
|
|
});
|
|
}
|
|
|
|
/**
|
|
* @hidden
|
|
*/
|
|
canSwipe(): boolean {
|
|
return this.swipeEnabled &&
|
|
!this.isAnimating &&
|
|
this._canOpen();
|
|
// TODO: && this._app.isEnabled();
|
|
}
|
|
|
|
|
|
_swipeBeforeStart() {
|
|
if (!this.canSwipe()) {
|
|
return;
|
|
}
|
|
this._before();
|
|
}
|
|
|
|
_swipeStart() {
|
|
if (!this.isAnimating) {
|
|
return;
|
|
}
|
|
|
|
this._getType().setProgressStart(this.isOpen);
|
|
}
|
|
|
|
_swipeProgress(stepValue: number) {
|
|
if (!this.isAnimating) {
|
|
return;
|
|
}
|
|
|
|
this._getType().setProgessStep(stepValue);
|
|
|
|
Ionic.emit(this, 'ionDrag', { detail: { menu: this } });
|
|
}
|
|
|
|
_swipeEnd(shouldCompleteLeft: boolean, shouldCompleteRight: boolean, stepValue: number, velocity: number) {
|
|
if (!this.isAnimating) {
|
|
return;
|
|
}
|
|
|
|
// user has finished dragging the menu
|
|
const isRightSide = this.isRightSide;
|
|
const opening = !this.isOpen;
|
|
const shouldComplete = (opening)
|
|
? isRightSide ? shouldCompleteLeft : shouldCompleteRight
|
|
: isRightSide ? shouldCompleteRight : shouldCompleteLeft;
|
|
|
|
this._getType().setProgressEnd(shouldComplete, stepValue, velocity, (isOpen: boolean) => {
|
|
console.debug('menu, swipeEnd', this.side);
|
|
this._after(isOpen);
|
|
});
|
|
}
|
|
|
|
private _before() {
|
|
// this places the menu into the correct location before it animates in
|
|
// this css class doesn't actually kick off any animations
|
|
this.$el.classList.add('show-menu');
|
|
this._backdropElm.classList.add('show-backdrop');
|
|
|
|
this.resize();
|
|
|
|
// TODO: this._keyboard.close();
|
|
|
|
this.isAnimating = true;
|
|
}
|
|
|
|
private _after(isOpen: boolean) {
|
|
// TODO: this._app.setEnabled(false, 100);
|
|
|
|
// keep opening/closing the menu disabled for a touch more yet
|
|
// only add listeners/css if it's enabled and isOpen
|
|
// and only remove listeners/css if it's not open
|
|
// emit opened/closed events
|
|
this.isOpen = isOpen;
|
|
this.isAnimating = false;
|
|
|
|
// add/remove backdrop click listeners
|
|
this._backdropClick(isOpen);
|
|
|
|
if (isOpen) {
|
|
// disable swipe to go back gesture
|
|
this._activeBlock = GESTURE_BLOCKER;
|
|
|
|
// add css class
|
|
Ionic.dom.write(() => {
|
|
this._cntElm.classList.add('menu-content-open');
|
|
});
|
|
|
|
// emit open event
|
|
Ionic.emit(this, 'ionOpen', { detail: { menu: this } });
|
|
|
|
} else {
|
|
// enable swipe to go back gesture
|
|
this._activeBlock = null;
|
|
|
|
// remove css classes
|
|
Ionic.dom.write(() => {
|
|
this._cntElm.classList.remove('menu-content-open');
|
|
this._cntElm.classList.remove('show-menu');
|
|
this._backdropElm.classList.remove('show-menu');
|
|
});
|
|
|
|
// emit close event
|
|
Ionic.emit(this, 'ionClose', { detail: { menu: this } });
|
|
}
|
|
}
|
|
|
|
/**
|
|
* @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();
|
|
}
|
|
|
|
/**
|
|
* @hidden
|
|
*/
|
|
toggle(): Promise<boolean> {
|
|
return this.setOpen(!this.isOpen);
|
|
}
|
|
|
|
_canOpen(): boolean {
|
|
return this.enabled && !this._isPane;
|
|
}
|
|
|
|
/**
|
|
* @hidden
|
|
*/
|
|
_updateState() {
|
|
const canOpen = this._canOpen();
|
|
|
|
// Close menu inmediately
|
|
if (!canOpen && this.isOpen) {
|
|
// close if this menu is open, and should not be enabled
|
|
this._forceClosing();
|
|
}
|
|
|
|
if (this.enabled && this._ctrl) {
|
|
this._ctrl._setActiveMenu(this);
|
|
}
|
|
|
|
if (!this._init) {
|
|
return;
|
|
}
|
|
|
|
// TODO
|
|
// const gesture = this._gesture;
|
|
// // only listen/unlisten if the menu has initialized
|
|
// if (canOpen && this.swipeEnabled && !gesture.isListening) {
|
|
// // should listen, but is not currently listening
|
|
// console.debug('menu, gesture listen', this.side);
|
|
// gesture.listen();
|
|
|
|
// } else if (gesture.isListening && (!canOpen || !this.swipeEnabled)) {
|
|
// // should not listen, but is currently listening
|
|
// console.debug('menu, gesture unlisten', this.side);
|
|
// gesture.unlisten();
|
|
// }
|
|
|
|
if (this.isOpen || (this._isPane && this.enabled)) {
|
|
this.resize();
|
|
}
|
|
}
|
|
|
|
/**
|
|
* @hidden
|
|
*/
|
|
enable(shouldEnable: boolean): Menu {
|
|
this.enabled = shouldEnable;
|
|
this._updateState();
|
|
return this;
|
|
}
|
|
|
|
/**
|
|
* @internal
|
|
*/
|
|
initPane(): boolean {
|
|
return false;
|
|
}
|
|
|
|
/**
|
|
* @internal
|
|
*/
|
|
paneChanged(isPane: boolean) {
|
|
this._isPane = isPane;
|
|
this._updateState();
|
|
}
|
|
|
|
/**
|
|
* @hidden
|
|
*/
|
|
swipeEnable(shouldEnable: boolean): Menu {
|
|
this.swipeEnabled = shouldEnable;
|
|
this._updateState();
|
|
return this;
|
|
}
|
|
|
|
/**
|
|
* @hidden
|
|
*/
|
|
getMenuElement(): HTMLElement {
|
|
return this.$el.querySelector('.menu-inner') as HTMLElement;
|
|
}
|
|
|
|
/**
|
|
* @hidden
|
|
*/
|
|
getContentElement(): HTMLElement {
|
|
return this._cntElm;
|
|
}
|
|
|
|
/**
|
|
* @hidden
|
|
*/
|
|
getBackdropElement(): HTMLElement {
|
|
return this._backdropElm;
|
|
}
|
|
|
|
/**
|
|
* @hidden
|
|
*/
|
|
width(): number {
|
|
return this.getMenuElement().offsetWidth;
|
|
}
|
|
|
|
/**
|
|
* @hidden
|
|
*/
|
|
getMenuController(): MenuController {
|
|
return this._ctrl;
|
|
}
|
|
|
|
private _backdropClick(shouldAdd: boolean) {
|
|
const onBackdropClick = this.onBackdropClick.bind(this);
|
|
|
|
if (shouldAdd && !this._unregBdClick) {
|
|
this._unregBdClick = Ionic.listener.add(this._cntElm, 'click', onBackdropClick, { capture: true });
|
|
this._unregCntClick = Ionic.listener.add(this._cntElm, 'click', onBackdropClick, { capture: true });
|
|
|
|
} else if (!shouldAdd && this._unregBdClick) {
|
|
this._unregBdClick();
|
|
this._unregCntClick();
|
|
this._unregBdClick = this._unregCntClick = null;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* @hidden
|
|
*/
|
|
ionViewDidUnload() {
|
|
this._backdropClick(false);
|
|
|
|
this._ctrl._unregister(this);
|
|
this._type && this._type.destroy();
|
|
|
|
this._ctrl = this._type = this._cntElm = this._backdropElm = null;
|
|
}
|
|
|
|
}
|
|
|
|
const GESTURE_BLOCKER = 'goback-swipe';
|