UNPKG

@ionic/core

Version:
459 lines (458 loc) • 15.1 kB
import { GESTURE_CONTROLLER } from '../../utils/gesture'; import { assert, isEndSide as isEnd } from '../../utils/helpers'; export class Menu { constructor() { this.lastOnEnd = 0; this.blocker = GESTURE_CONTROLLER.createBlocker({ disableScroll: true }); this.isAnimating = false; this._isOpen = false; this.isPaneVisible = false; this.isEndSide = false; this.disabled = false; this.side = 'start'; this.swipeGesture = true; this.maxEdgeStart = 50; } typeChanged(type, oldType) { const contentEl = this.contentEl; if (contentEl) { if (oldType !== undefined) { contentEl.classList.remove(`menu-content-${oldType}`); } contentEl.classList.add(`menu-content-${type}`); contentEl.removeAttribute('style'); } if (this.menuInnerEl) { this.menuInnerEl.removeAttribute('style'); } this.animation = undefined; } disabledChanged() { this.updateState(); this.ionMenuChange.emit({ disabled: this.disabled, open: this._isOpen }); } sideChanged() { this.isEndSide = isEnd(this.win, this.side); } swipeGestureChanged() { this.updateState(); } async componentWillLoad() { if (this.type === undefined) { this.type = this.config.get('menuType', this.mode === 'ios' ? 'reveal' : 'overlay'); } if (this.isServer) { this.disabled = true; return; } const menuCtrl = this.menuCtrl = await this.lazyMenuCtrl.componentOnReady().then(p => p._getInstance()); const el = this.el; const parent = el.parentNode; const content = this.contentId !== undefined ? document.getElementById(this.contentId) : parent && parent.querySelector && parent.querySelector('[main]'); if (!content || !content.tagName) { console.error('Menu: must have a "content" element to listen for drag events on.'); return; } this.contentEl = content; content.classList.add('menu-content'); this.typeChanged(this.type, undefined); this.sideChanged(); menuCtrl._register(this); this.gesture = (await import('../../utils/gesture')).createGesture({ el: this.doc, queue: this.queue, gestureName: 'menu-swipe', gesturePriority: 30, threshold: 10, canStart: ev => this.canStart(ev), onWillStart: () => this.onWillStart(), onStart: () => this.onStart(), onMove: ev => this.onMove(ev), onEnd: ev => this.onEnd(ev), }); this.updateState(); } componentDidLoad() { this.ionMenuChange.emit({ disabled: this.disabled, open: this._isOpen }); } componentDidUnload() { this.blocker.destroy(); this.menuCtrl._unregister(this); if (this.animation) { this.animation.destroy(); } if (this.gesture) { this.gesture.destroy(); this.gesture = undefined; } this.animation = undefined; this.contentEl = this.backdropEl = this.menuInnerEl = undefined; } onSplitPaneChanged(ev) { this.isPaneVisible = ev.detail.isPane(this.el); this.updateState(); } onBackdropClick(ev) { if (this.lastOnEnd < ev.timeStamp - 100) { const shouldClose = (ev.composedPath) ? !ev.composedPath().includes(this.menuInnerEl) : false; if (shouldClose) { ev.preventDefault(); ev.stopPropagation(); this.close(); } } } isOpen() { return Promise.resolve(this._isOpen); } isActive() { return Promise.resolve(this._isActive()); } open(animated = true) { return this.setOpen(true, animated); } close(animated = true) { return this.setOpen(false, animated); } toggle(animated = true) { return this.setOpen(!this._isOpen, animated); } setOpen(shouldOpen, animated = true) { return this.menuCtrl._setOpen(this, shouldOpen, animated); } async _setOpen(shouldOpen, animated = true) { if (!this._isActive() || this.isAnimating || shouldOpen === this._isOpen) { return false; } this.beforeAnimation(shouldOpen); await this.loadAnimation(); await this.startAnimation(shouldOpen, animated); this.afterAnimation(shouldOpen); return true; } async loadAnimation() { const width = this.menuInnerEl.offsetWidth; if (width === this.width && this.animation !== undefined) { return; } this.width = width; if (this.animation) { this.animation.destroy(); this.animation = undefined; } this.animation = await this.menuCtrl._createAnimation(this.type, this); } async startAnimation(shouldOpen, animated) { const ani = this.animation.reverse(!shouldOpen); if (animated) { await ani.playAsync(); } else { ani.playSync(); } } _isActive() { return !this.disabled && !this.isPaneVisible; } canSwipe() { return this.swipeGesture && !this.isAnimating && this._isActive(); } canStart(detail) { if (!this.canSwipe()) { return false; } if (this._isOpen) { return true; } else if (this.menuCtrl.getOpenSync()) { return false; } return checkEdgeSide(this.win, detail.currentX, this.isEndSide, this.maxEdgeStart); } onWillStart() { this.beforeAnimation(!this._isOpen); return this.loadAnimation(); } onStart() { if (!this.isAnimating || !this.animation) { assert(false, 'isAnimating has to be true'); return; } this.animation.reverse(this._isOpen).progressStart(); } onMove(detail) { if (!this.isAnimating || !this.animation) { assert(false, 'isAnimating has to be true'); return; } const delta = computeDelta(detail.deltaX, this._isOpen, this.isEndSide); const stepValue = delta / this.width; this.animation.progressStep(stepValue); } onEnd(detail) { if (!this.isAnimating || !this.animation) { assert(false, 'isAnimating has to be true'); return; } const isOpen = this._isOpen; const isEndSide = this.isEndSide; const delta = computeDelta(detail.deltaX, isOpen, isEndSide); const width = this.width; const stepValue = delta / width; const velocity = detail.velocityX; const z = width / 2.0; const shouldCompleteRight = velocity >= 0 && (velocity > 0.2 || detail.deltaX > z); const shouldCompleteLeft = velocity <= 0 && (velocity < -0.2 || detail.deltaX < -z); const shouldComplete = isOpen ? isEndSide ? shouldCompleteRight : shouldCompleteLeft : isEndSide ? shouldCompleteLeft : shouldCompleteRight; let shouldOpen = !isOpen && shouldComplete; if (isOpen && !shouldComplete) { shouldOpen = true; } const missing = shouldComplete ? 1 - stepValue : stepValue; const missingDistance = missing * width; let realDur = 0; if (missingDistance > 5) { const dur = missingDistance / Math.abs(velocity); realDur = Math.min(dur, 300); } this.lastOnEnd = detail.timeStamp; this.animation .onFinish(() => this.afterAnimation(shouldOpen), { clearExistingCallbacks: true, oneTimeCallback: true }) .progressEnd(shouldComplete, stepValue, realDur); } beforeAnimation(shouldOpen) { assert(!this.isAnimating, '_before() should not be called while animating'); this.el.classList.add(SHOW_MENU); if (this.backdropEl) { this.backdropEl.classList.add(SHOW_BACKDROP); } this.blocker.block(); this.isAnimating = true; if (shouldOpen) { this.ionWillOpen.emit(); } else { this.ionWillClose.emit(); } } afterAnimation(isOpen) { assert(this.isAnimating, '_before() should be called while animating'); this._isOpen = isOpen; this.isAnimating = false; if (!this._isOpen) { this.blocker.unblock(); } this.enableListener(this, 'click', isOpen); if (isOpen) { if (this.contentEl) { this.contentEl.classList.add(MENU_CONTENT_OPEN); } this.ionDidOpen.emit(); } else { this.el.classList.remove(SHOW_MENU); if (this.contentEl) { this.contentEl.classList.remove(MENU_CONTENT_OPEN); } if (this.backdropEl) { this.backdropEl.classList.remove(SHOW_BACKDROP); } this.ionDidClose.emit(); } } updateState() { const isActive = this._isActive(); if (this.gesture) { this.gesture.setDisabled(!isActive || !this.swipeGesture); } if (!isActive && this._isOpen) { this.forceClosing(); } if (!this.disabled && this.menuCtrl) { this.menuCtrl._setActiveMenu(this); } assert(!this.isAnimating, 'can not be animating'); } forceClosing() { assert(this._isOpen, 'menu cannot be closed'); this.isAnimating = true; const ani = this.animation.reverse(true); ani.playSync(); this.afterAnimation(false); } hostData() { const { isEndSide, type, disabled, isPaneVisible } = this; return { role: 'complementary', class: { [`menu-type-${type}`]: true, 'menu-enabled': !disabled, 'menu-side-end': isEndSide, 'menu-side-start': !isEndSide, 'menu-pane-visible': isPaneVisible } }; } render() { return [ h("div", { class: "menu-inner", ref: el => this.menuInnerEl = el }, h("slot", null)), h("ion-backdrop", { ref: el => this.backdropEl = el, class: "menu-backdrop", tappable: false, stopPropagation: false }) ]; } static get is() { return "ion-menu"; } static get encapsulation() { return "shadow"; } static get properties() { return { "close": { "method": true }, "config": { "context": "config" }, "contentId": { "type": String, "attr": "content-id" }, "disabled": { "type": Boolean, "attr": "disabled", "mutable": true, "watchCallbacks": ["disabledChanged"] }, "doc": { "context": "document" }, "el": { "elementRef": true }, "enableListener": { "context": "enableListener" }, "isActive": { "method": true }, "isEndSide": { "state": true }, "isOpen": { "method": true }, "isPaneVisible": { "state": true }, "isServer": { "context": "isServer" }, "lazyMenuCtrl": { "connect": "ion-menu-controller" }, "maxEdgeStart": { "type": Number, "attr": "max-edge-start" }, "menuId": { "type": String, "attr": "menu-id" }, "open": { "method": true }, "queue": { "context": "queue" }, "setOpen": { "method": true }, "side": { "type": String, "attr": "side", "reflectToAttr": true, "watchCallbacks": ["sideChanged"] }, "swipeGesture": { "type": Boolean, "attr": "swipe-gesture", "watchCallbacks": ["swipeGestureChanged"] }, "toggle": { "method": true }, "type": { "type": String, "attr": "type", "mutable": true, "watchCallbacks": ["typeChanged"] }, "win": { "context": "window" } }; } static get events() { return [{ "name": "ionWillOpen", "method": "ionWillOpen", "bubbles": true, "cancelable": true, "composed": true }, { "name": "ionWillClose", "method": "ionWillClose", "bubbles": true, "cancelable": true, "composed": true }, { "name": "ionDidOpen", "method": "ionDidOpen", "bubbles": true, "cancelable": true, "composed": true }, { "name": "ionDidClose", "method": "ionDidClose", "bubbles": true, "cancelable": true, "composed": true }, { "name": "ionMenuChange", "method": "ionMenuChange", "bubbles": true, "cancelable": true, "composed": true }]; } static get listeners() { return [{ "name": "body:ionSplitPaneVisible", "method": "onSplitPaneChanged" }, { "name": "click", "method": "onBackdropClick", "capture": true, "disabled": true }]; } static get style() { return "/**style-placeholder:ion-menu:**/"; } static get styleMode() { return "/**style-id-placeholder:ion-menu:**/"; } } function computeDelta(deltaX, isOpen, isEndSide) { return Math.max(0, isOpen !== isEndSide ? -deltaX : deltaX); } function checkEdgeSide(win, posX, isEndSide, maxEdgeStart) { if (isEndSide) { return posX >= win.innerWidth - maxEdgeStart; } else { return posX <= maxEdgeStart; } } const SHOW_MENU = 'show-menu'; const SHOW_BACKDROP = 'show-backdrop'; const MENU_CONTENT_OPEN = 'menu-content-open';