ember-mobile-menu
Version:
A draggable sidebar menu for Ember.
339 lines (335 loc) • 9.56 kB
JavaScript
import "./mobile-menu.css"
import Component from '@glimmer/component';
import { action } from '@ember/object';
import { tracked } from '@glimmer/tracking';
import { assert } from '@ember/debug';
import { htmlSafe } from '@ember/template';
import { registerDestructor } from '@ember/destroyable';
import MaskComponent from './mobile-menu/mask.js';
import TrayComponent from './mobile-menu/tray.js';
import { hash } from '@ember/helper';
import { effect } from './utils.js';
import { createCache, getValue } from '@glimmer/validator';
import { precompileTemplate } from '@ember/template-compilation';
import { setComponentTemplate } from '@ember/component';
import { g, i, n } from 'decorator-transforms/runtime';
;
const _fn = function () {};
class StateResource {
static {
g(this.prototype, "_open", [tracked], function () {
return false;
});
}
#_open = (i(this, "_open"), void 0);
static {
g(this.prototype, "_closed", [tracked], function () {
return true;
});
}
#_closed = (i(this, "_closed"), void 0);
static {
g(this.prototype, "_dragging", [tracked], function () {
return false;
});
}
#_dragging = (i(this, "_dragging"), void 0);
static {
g(this.prototype, "_transitioning", [tracked], function () {
return false;
});
}
#_transitioning = (i(this, "_transitioning"), void 0);
_useState;
constructor(owner, useState = () => {}) {
this._useState = useState;
registerDestructor(owner, () => {
this._useState = undefined;
});
}
currentCache = createCache(() => {
let state = this._useState();
return this.calculateCurrent(...state);
});
get current() {
return getValue(this.currentCache);
}
calculateCurrent(position, isDragging, width) {
this._dragging = position !== 0 && isDragging;
let open = !this._dragging && Math.abs(position) === width;
let closed = !this._dragging && position === 0;
effect(() => {
this.maybeToggle(open, closed);
});
this._transitioning = !this._dragging && !this._open && !this._closed;
return {
open: this._open,
closed: this._closed,
dragging: this._dragging,
transitioning: this._transitioning
};
}
get open() {
return this.current.open;
}
get closed() {
return this.current.closed;
}
get dragging() {
return this.current.dragging;
}
get transitioning() {
return this.current.transitioning;
}
maybeToggle(open, closed) {
if (this._open !== open) {
this._open = open;
} else if (this.closed !== closed) {
this._closed = closed;
}
}
}
/**
* Menu component
*
* @class MobileMenu
* @public
*/
class MobileMenu extends Component {
state = new StateResource(this, () => [this.position, this.args.isDragging, this._width]);
/**
* The type of menu. Currently 'left' and 'right' are supported.
*
* @argument type
* @type String
* @default 'left'
*/
get type() {
return this.args.type ?? 'left';
}
/**
* Sets the mode of the menu. Currently 'default', 'push', 'ios', 'reveal', 'squeeze' and 'squeeze-reveal' are supported.
*
* @argument mode
* @type string
* @default 'default'
*/
get mode() {
return this.args.mode ?? 'default';
}
/**
* The percentage of the screen the menu will take when opened.
*
* @argument width
* @type Number [0-100]
* @default 85
*/
get width() {
return this.args.width ?? 85;
}
/**
* The maximum width of the menu in pixels. Set to -1 to disable;
*
* @argument maxWidth
* @type Number
* @default 300
*/
get maxWidth() {
return this.args.maxWidth ?? 300;
}
/**
* Whether or not a mask is added when the menu is opened.
*
* @argument maskEnabled
* @type Boolean
* @default true
*/
get maskEnabled() {
return this.args.maskEnabled ?? true;
}
/**
* Whether or not a shadow is added to the menu.
*
* @argument shadowEnabled
* @type Boolean
* @default true
*/
get shadowEnabled() {
return this.args.shadowEnabled ?? true;
}
/**
* The default swipe velocity needed to fully open the menu.
*
* @argument triggerVelocity
* @type Number
* @default 0.3
*/
get triggerVelocity() {
return this.args.triggerVelocity ?? 0.3;
}
/**
* @argument isOpen
* @type boolean
* @default false
*/ /**
* Hook which is called after the transition with the new menu isOpen state.
*
* @argument onToggle
* @type Function
*/
get onToggle() {
return this.args.onToggle ?? _fn;
}
/**
* @argument embed
* @type boolean
* @default false
* @protected
*/
get embed() {
return this.args.embed ?? false;
}
/**
* Hook fired when the menu is opened. You can pass in an action. The menu instance will be passed to the action.
*
* @argument onOpen
* @type Function
* @protected
*/
get onOpen() {
return this.args.onOpen ?? _fn;
}
/**
* Hook fired when the menu is closed. You can pass in an action. The menu instance will be passed to the action.
*
* @argument onClose
* @type Action
* @protected
*/
get onClose() {
return this.args.onClose ?? _fn;
}
/**
* @argument position
* @type number
* @protected
*/
get position() {
return this.isLeft && this.args.position > 0 || this.isRight && this.args.position < 0 ? this.args.position : 0;
}
/**
* @argument isDragging
* @type boolean
* @protected
*/
constructor() {
super(...arguments);
assert('register function argument not passed. You should not be using <MobileMenu/> directly.', typeof this.args.register === 'function');
assert('unregister function argument not passed. You should not be using <MobileMenu/> directly.', typeof this.args.unregister === 'function');
if (this.args.parent?.isFastBoot && this.args.isOpen) {
this.args.parent._activeMenu = this;
this.open(false);
}
}
willDestroy() {
this.args.unregister(this);
super.willDestroy(...arguments);
}
get renderMenu() {
return this.args.parent?.isFastBoot || this.args.parentBoundingClientRect;
}
get classNames() {
let classes = `mobile-menu mobile-menu--${this.mode}`;
if (this.isLeft) classes += ' mobile-menu--left';
if (this.isRight) classes += ' mobile-menu--right';
if (this.state.dragging) classes += ' mobile-menu--dragging';
if (this.state.open) classes += ' mobile-menu--open';
if (this.state.transitioning) classes += ' mobile-menu--transitioning';
return classes;
}
get isLeft() {
return this.type === 'left';
}
get isRight() {
return this.type === 'right';
}
get relativePosition() {
return Math.abs(this.position) / this._width;
}
get invertOpacity() {
return ['ios', 'reveal', 'squeeze-reveal'].includes(this.args.mode);
}
/**
* Current menu width in px
*
* @property _width
* @return {Boolean}
* @private
*/
get _width() {
const width = this.args.parentBoundingClientRect ? this.width / 100 * this.args.parentBoundingClientRect.width : this.maxWidth;
return this.maxWidth === -1 ? width : Math.min(width, this.maxWidth);
}
get style() {
let styles = '';
if (!this.maskEnabled && this.state.open) {
styles = `width: ${this._width}px;`;
}
return htmlSafe(styles);
}
open(animate) {
this.onOpen(this, 0, animate);
}
static {
n(this.prototype, "open", [action]);
}
close(animate) {
if (!this.hasRendered) return;
this.onClose(this, 0, animate);
}
static {
n(this.prototype, "close", [action]);
}
hasRendered = false;
openOrClose(open) {
let animate = this.hasRendered;
if (open) {
this.open(animate);
} else {
this.close(animate);
}
this.onToggle(open);
}
static {
n(this.prototype, "openOrClose", [action]);
}
closeFromLinkTo() {
if (!['squeeze', 'squeeze-reveal'].includes(this.mode)) {
this.close();
}
}
static {
n(this.prototype, "closeFromLinkTo", [action]);
}
setRendered() {
if (!this.hasRendered) {
this.hasRendered = true;
}
}
static {
n(this.prototype, "setRendered", [action]);
}
static {
setComponentTemplate(precompileTemplate("\n {{#if this.renderMenu}}\n {{effect @register this}}\n {{effect this.openOrClose @isOpen}}\n {{effect this.close this.type}}\n {{effect this.setRendered}}\n\n <div class={{this.classNames}} style={{this.style}} aria-hidden={{if this.state.closed \"true\"}}>\n {{#if this.maskEnabled}}\n <MaskComponent @isOpen={{this.state.open}} @position={{this.relativePosition}} @invertOpacity={{this.invertOpacity}} @onClick={{@onClose}} @onPanStart={{@onPanStart}} @onPan={{@onPan}} @onPanEnd={{@onPanEnd}} @capture={{@capture}} @preventScroll={{@preventScroll}} />\n {{/if}}\n\n <TrayComponent ...attributes @width={{this._width}} @isLeft={{this.isLeft}} @position={{this.position}} @shadowEnabled={{this.shadowEnabled}} @mode={{@mode}} @embed={{@embed}} @isClosed={{this.state.closed}} @onPanStart={{@onPanStart}} @onPan={{@onPan}} @onPanEnd={{@onPanEnd}} @capture={{@capture}} @preventScroll={{@preventScroll}}>\n {{yield (hash actions=(hash open=this.open close=this.close))}}\n </TrayComponent>\n </div>\n {{/if}}\n ", {
strictMode: true,
scope: () => ({
effect,
MaskComponent,
TrayComponent,
hash
})
}), this);
}
}
export { MobileMenu as default };
//# sourceMappingURL=mobile-menu.js.map