@vime/core
Version:
Customizable, extensible, accessible and framework agnostic media player.
621 lines (620 loc) • 18.2 kB
JavaScript
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
}]; }
}