mirror of
https://github.com/ionic-team/ionic-framework.git
synced 2025-08-22 13:32:54 +08:00
chore(): moved themes into ionic and created components folder.
This commit is contained in:
508
packages/ionic/src/components/menu/menu.tsx
Normal file
508
packages/ionic/src/components/menu/menu.tsx
Normal file
@ -0,0 +1,508 @@
|
||||
import { Component, h, Ionic, Prop, Watch } from '../index';
|
||||
import { VNodeData, GlobalNamespace, Menu as IMenu } from '../../util/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';
|
Reference in New Issue
Block a user