UNPKG

@vime/core

Version:

Customizable, extensible, accessible and framework agnostic media player.

621 lines (620 loc) 18.2 kB
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) { function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); } return new (P || (P = Promise))(function (resolve, reject) { function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } } function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } } function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); } step((generator = generator.apply(thisArg, _arguments || [])).next()); }); }; import { Component, Element, Event, h, Listen, Method, Prop, State, Watch, writeTask, } from '@stencil/core'; import { isUndefined } from '../../../../utils/unit'; import { withComponentRegistry } from '../../../core/player/withComponentRegistry'; import { menuItemHunter } from './menuItemHunter'; /** * A multi-purpose interactable element inside a menu. The behaviour and style of the item depends * on the props set. * * - **Default:** By default, the menu item only contains a label and optional hint/badge text that is * displayed on the right-hand side of the item. * * - **Navigation:** If the `menu` prop is set, the item behaves as a navigational control and displays * arrows to indicate whether clicking the control will navigate forwards/backwards. * * - **Radio:** If the `checked` prop is set, the item behaves as a radio button and displays a * checkmark icon to indicate whether it is checked or not. * * ## Visual * * <img * src="https://raw.githubusercontent.com/vime-js/vime/master/packages/core/src/components/ui/settings/menu-item/menu-item.png" * alt="Vime settings menu item component" * /> * * @slot - Used to pass in the body of the menu which usually contains menu items, radio groups * and/or submenus. */ export class Menu { constructor() { this.hasDisconnected = false; /** * Whether the menu is open/visible. */ this.active = false; withComponentRegistry(this); } onActiveMenuitemChange() { this.vmActiveMenuItemChange.emit(this.activeMenuItem); } onActiveSubmenuChange() { this.vmActiveSubmenuChange.emit(this.activeSubmenu); } onActiveChange() { var _a; if (this.hasDisconnected) return; this.active ? this.vmOpen.emit(this.host) : this.vmClose.emit(this.host); if (((_a = this.controller) === null || _a === void 0 ? void 0 : _a.tagName.toLowerCase()) === 'vm-menu-item') { this.controller.expanded = true; } } connectedCallback() { this.hasDisconnected = false; } componentDidRender() { writeTask(() => { if (!this.hasDisconnected) this.calculateHeight(); }); } disconnectedCallback() { this.controller = undefined; this.hasDisconnected = true; } /** * Focuses the menu. */ focusMenu() { var _a; return __awaiter(this, void 0, void 0, function* () { (_a = this.menu) === null || _a === void 0 ? void 0 : _a.focus(); }); } /** * Removes focus from the menu. */ blurMenu() { var _a; return __awaiter(this, void 0, void 0, function* () { (_a = this.menu) === null || _a === void 0 ? void 0 : _a.blur(); }); } /** * Returns the currently focused menu item. */ getActiveMenuItem() { return __awaiter(this, void 0, void 0, function* () { return this.activeMenuItem; }); } /** * Sets the currently focused menu item. */ setActiveMenuItem(item) { return __awaiter(this, void 0, void 0, function* () { item === null || item === void 0 ? void 0 : item.focusItem(); this.activeMenuItem = item; }); } /** * Calculates the height of the settings menu based on its children. */ calculateHeight() { var _a, _b; return __awaiter(this, void 0, void 0, function* () { let height = 0; if (this.activeSubmenu) { const submenu = yield this.activeSubmenu.getMenu(); height = (_a = (yield (submenu === null || submenu === void 0 ? void 0 : submenu.calculateHeight()))) !== null && _a !== void 0 ? _a : 0; height += yield this.activeSubmenu.getControllerHeight(); } else { const children = ((_b = this.container) === null || _b === void 0 ? void 0 : _b.firstChild).assignedElements({ flatten: true }); children === null || children === void 0 ? void 0 : children.forEach(child => { height += parseFloat(window.getComputedStyle(child).height); }); } this.vmMenuHeightChange.emit(height); return height; }); } onOpenSubmenu(event) { event.stopPropagation(); if (!isUndefined(this.activeSubmenu)) this.activeSubmenu.active = false; this.activeSubmenu = event.detail; this.getChildren().forEach(child => { if (child !== this.activeSubmenu) { child.style.opacity = '0'; child.style.visibility = 'hidden'; } }); writeTask(() => { this.activeSubmenu.active = true; }); } onCloseSubmenu(event) { event === null || event === void 0 ? void 0 : event.stopPropagation(); if (!isUndefined(this.activeSubmenu)) this.activeSubmenu.active = false; this.getChildren().forEach(child => { if (child !== this.activeSubmenu) { child.style.opacity = ''; child.style.visibility = ''; } }); writeTask(() => { this.activeSubmenu = undefined; }); } onWindowClick() { this.onCloseSubmenu(); this.onClose(); } onWindowKeyDown(event) { if (this.active && event.key === 'Escape') { this.onCloseSubmenu(); this.onClose(); this.focusController(); } } getChildren() { var _a; const assignedElements = (_a = this.host .shadowRoot.querySelector('slot')) === null || _a === void 0 ? void 0 : _a.assignedElements({ flatten: true }); return (assignedElements !== null && assignedElements !== void 0 ? assignedElements : []); } getMenuItems() { var _a; const assignedElements = (_a = this.host .shadowRoot.querySelector('slot')) === null || _a === void 0 ? void 0 : _a.assignedElements({ flatten: true }); return menuItemHunter(assignedElements); } focusController() { var _a, _b, _c, _d, _e; if (!isUndefined((_a = this.controller) === null || _a === void 0 ? void 0 : _a.focusItem)) { (_b = this.controller) === null || _b === void 0 ? void 0 : _b.focusItem(); } else if (!isUndefined((_c = this.controller) === null || _c === void 0 ? void 0 : _c.focusControl)) { (_d = this.controller) === null || _d === void 0 ? void 0 : _d.focusControl(); } else { (_e = this.controller) === null || _e === void 0 ? void 0 : _e.focus(); } } triggerMenuItem() { var _a; if (isUndefined(this.activeMenuItem)) return; this.activeMenuItem.click(); // If it controls a menu then focus it essentially opening it. (_a = this.activeMenuItem.menu) === null || _a === void 0 ? void 0 : _a.focusMenu(); } onClose() { this.activeMenuItem = undefined; this.active = false; } onClick(event) { // Stop the event from propagating while playing with menu so that when it is clicked outside // the menu we can close it in the `onWindowClick` handler above. event.stopPropagation(); } onFocus() { var _a; this.active = true; [this.activeMenuItem] = this.getMenuItems(); (_a = this.activeMenuItem) === null || _a === void 0 ? void 0 : _a.focusItem(); this.vmFocus.emit(); } onBlur() { this.vmBlur.emit(); } foucsMenuItem(items, index) { if (index < 0) index = items.length - 1; if (index > items.length - 1) index = 0; this.activeMenuItem = items[index]; this.activeMenuItem.focusItem(); } onKeyDown(event) { if (!this.active) return; event.preventDefault(); event.stopPropagation(); const items = this.getMenuItems(); let index = items.findIndex(item => item === this.activeMenuItem); switch (event.key) { case 'Escape': this.onClose(); this.focusController(); break; case 'ArrowDown': case 'Tab': this.foucsMenuItem(items, (index += 1)); break; case 'ArrowUp': this.foucsMenuItem(items, (index -= 1)); break; case 'ArrowLeft': this.onClose(); this.focusController(); break; case 'ArrowRight': case 'Enter': case ' ': this.triggerMenuItem(); break; case 'Home': case 'PageUp': this.foucsMenuItem(items, 0); break; case 'End': case 'PageDown': this.foucsMenuItem(items, items.length - 1); break; } } render() { var _a, _b, _c; return (h("div", { id: this.identifier, class: { menu: true, slideIn: !isUndefined(this.slideInDirection), slideInFromLeft: this.slideInDirection === 'left', slideInFromRight: this.slideInDirection === 'right', }, role: "menu", tabindex: "-1", "aria-labelledby": (_b = (_a = this.controller) === null || _a === void 0 ? void 0 : _a.identifier) !== null && _b !== void 0 ? _b : (_c = this.controller) === null || _c === void 0 ? void 0 : _c.id, "aria-hidden": !this.active ? 'true' : 'false', onFocus: this.onFocus.bind(this), onBlur: this.onBlur.bind(this), onClick: this.onClick.bind(this), onKeyDown: this.onKeyDown.bind(this), ref: el => { this.menu = el; } }, h("div", { class: "container", ref: el => { this.container = el; } }, h("slot", null)))); } static get is() { return "vm-menu"; } static get encapsulation() { return "shadow"; } static get originalStyleUrls() { return { "$": ["menu.css"] }; } static get styleUrls() { return { "$": ["menu.css"] }; } static get properties() { return { "active": { "type": "boolean", "mutable": true, "complexType": { "original": "boolean", "resolved": "boolean", "references": {} }, "required": false, "optional": false, "docs": { "tags": [], "text": "Whether the menu is open/visible." }, "attribute": "active", "reflect": true, "defaultValue": "false" }, "identifier": { "type": "string", "mutable": false, "complexType": { "original": "string", "resolved": "string", "references": {} }, "required": true, "optional": false, "docs": { "tags": [], "text": "The `id` attribute of the menu." }, "attribute": "identifier", "reflect": false }, "controller": { "type": "unknown", "mutable": false, "complexType": { "original": "HTMLElement", "resolved": "HTMLElement | undefined", "references": { "HTMLElement": { "location": "global" } } }, "required": false, "optional": true, "docs": { "tags": [], "text": "Reference to the controller DOM element that is responsible for opening/closing this menu." } }, "slideInDirection": { "type": "string", "mutable": false, "complexType": { "original": "'left' | 'right'", "resolved": "\"left\" | \"right\" | undefined", "references": {} }, "required": false, "optional": true, "docs": { "tags": [], "text": "The direction the menu should slide in from." }, "attribute": "slide-in-direction", "reflect": false } }; } static get states() { return { "activeMenuItem": {}, "activeSubmenu": {} }; } static get events() { return [{ "method": "vmOpen", "name": "vmOpen", "bubbles": true, "cancelable": true, "composed": true, "docs": { "tags": [], "text": "Emitted when the menu is open/active." }, "complexType": { "original": "HTMLVmMenuElement", "resolved": "HTMLVmMenuElement", "references": { "HTMLVmMenuElement": { "location": "global" } } } }, { "method": "vmClose", "name": "vmClose", "bubbles": true, "cancelable": true, "composed": true, "docs": { "tags": [], "text": "Emitted when the menu has closed/is not active." }, "complexType": { "original": "HTMLVmMenuElement", "resolved": "HTMLVmMenuElement", "references": { "HTMLVmMenuElement": { "location": "global" } } } }, { "method": "vmFocus", "name": "vmFocus", "bubbles": true, "cancelable": true, "composed": true, "docs": { "tags": [], "text": "Emitted when the menu is focused." }, "complexType": { "original": "void", "resolved": "void", "references": {} } }, { "method": "vmBlur", "name": "vmBlur", "bubbles": true, "cancelable": true, "composed": true, "docs": { "tags": [], "text": "Emitted when the menu loses focus." }, "complexType": { "original": "void", "resolved": "void", "references": {} } }, { "method": "vmActiveSubmenuChange", "name": "vmActiveSubmenuChange", "bubbles": true, "cancelable": true, "composed": true, "docs": { "tags": [], "text": "Emitted when the active submenu changes." }, "complexType": { "original": "HTMLVmSubmenuElement | undefined", "resolved": "HTMLVmSubmenuElement | undefined", "references": { "HTMLVmSubmenuElement": { "location": "global" } } } }, { "method": "vmActiveMenuItemChange", "name": "vmActiveMenuItemChange", "bubbles": true, "cancelable": true, "composed": true, "docs": { "tags": [], "text": "Emitted when the currently focused menu item changes." }, "complexType": { "original": "HTMLVmMenuItemElement | undefined", "resolved": "HTMLVmMenuItemElement | undefined", "references": { "HTMLVmMenuItemElement": { "location": "global" } } } }, { "method": "vmMenuHeightChange", "name": "vmMenuHeightChange", "bubbles": false, "cancelable": true, "composed": true, "docs": { "tags": [], "text": "Emitted when the height of the menu changes." }, "complexType": { "original": "number", "resolved": "number", "references": {} } }]; } static get methods() { return { "focusMenu": { "complexType": { "signature": "() => Promise<void>", "parameters": [], "references": { "Promise": { "location": "global" } }, "return": "Promise<void>" }, "docs": { "text": "Focuses the menu.", "tags": [] } }, "blurMenu": { "complexType": { "signature": "() => Promise<void>", "parameters": [], "references": { "Promise": { "location": "global" } }, "return": "Promise<void>" }, "docs": { "text": "Removes focus from the menu.", "tags": [] } }, "getActiveMenuItem": { "complexType": { "signature": "() => Promise<HTMLVmMenuItemElement | undefined>", "parameters": [], "references": { "Promise": { "location": "global" }, "HTMLVmMenuItemElement": { "location": "global" } }, "return": "Promise<HTMLVmMenuItemElement | undefined>" }, "docs": { "text": "Returns the currently focused menu item.", "tags": [] } }, "setActiveMenuItem": { "complexType": { "signature": "(item?: HTMLVmMenuItemElement | undefined) => Promise<void>", "parameters": [{ "tags": [], "text": "" }], "references": { "Promise": { "location": "global" }, "HTMLVmMenuItemElement": { "location": "global" } }, "return": "Promise<void>" }, "docs": { "text": "Sets the currently focused menu item.", "tags": [] } }, "calculateHeight": { "complexType": { "signature": "() => Promise<number>", "parameters": [], "references": { "Promise": { "location": "global" }, "HTMLSlotElement": { "location": "global" } }, "return": "Promise<number>" }, "docs": { "text": "Calculates the height of the settings menu based on its children.", "tags": [] } } }; } static get elementRef() { return "host"; } static get watchers() { return [{ "propName": "activeMenuItem", "methodName": "onActiveMenuitemChange" }, { "propName": "activeSubmenu", "methodName": "onActiveSubmenuChange" }, { "propName": "active", "methodName": "onActiveChange" }]; } static get listeners() { return [{ "name": "vmOpenSubmenu", "method": "onOpenSubmenu", "target": undefined, "capture": false, "passive": false }, { "name": "vmCloseSubmenu", "method": "onCloseSubmenu", "target": undefined, "capture": false, "passive": false }, { "name": "click", "method": "onWindowClick", "target": "window", "capture": false, "passive": false }, { "name": "keydown", "method": "onWindowKeyDown", "target": "window", "capture": false, "passive": false }]; } }