UNPKG

@laserware/hoverboard

Version:

Better context menus for Electron.

631 lines (617 loc) 17.9 kB
var __defProp = Object.defineProperty; var __decorateClass = (decorators, target, key, kind) => { var result = void 0 ; for (var i = decorators.length - 1, decorator; i >= 0; i--) if (decorator = decorators[i]) result = (decorator(target, key, result) ) || result; if (result) __defProp(target, key, result); return result; }; // src/elements/ContextMenuItemElement.ts function property(options) { return function(target, propertyKey) { const attributeName = options.attribute ?? propertyKey; Object.defineProperty(target, propertyKey, { get() { const value = this.getAttribute(attributeName); if (value === null) { return options.fallback ?? void 0; } switch (options.type) { case Boolean: return value === "true"; case Number: { const numericValue = Number(value); if (Number.isNaN(numericValue) && typeof options.fallback === "number") { return options.fallback; } else { return numericValue; } } default: return value; } }, set(value) { if (value === null || value === void 0) { this.removeAttribute(attributeName); } else { this.setAttribute(attributeName, String(value)); } } }); }; } var idGenerator = /* @__PURE__ */ (() => { let value = 1; return { next: () => `menu-item-${(value++).toString()}` }; })(); var ContextMenuItemElement = class extends HTMLElement { constructor(type) { super(); this.submenu = null; this.type = type; } toTemplate() { const template = {}; const id = this.getAttribute("id"); if (id !== null) { template.id = this.id; } if (this.type !== void 0) { template.type = this.type; } if (this.visible !== void 0) { template.visible = this.visible; } return template; } connectedCallback() { const id = this.getAttribute("id"); if (id === null) { this.setAttribute("id", idGenerator.next()); } } getAttribute(name) { return super.getAttribute(name); } setAttribute(name, value) { super.setAttribute(name, value); } removeAttribute(name) { super.removeAttribute(name); } }; __decorateClass([ property({ type: String }) ], ContextMenuItemElement.prototype, "id"); __decorateClass([ property({ type: Boolean }) ], ContextMenuItemElement.prototype, "visible"); // src/elements/NormalMenuItemElement.ts var NormalMenuItemElement = class extends ContextMenuItemElement { constructor(type = "normal") { super(type); } toTemplate() { const template = super.toTemplate(); if (this.accelerator !== void 0) { template.accelerator = this.accelerator; } if (this.acceleratorWorksWhenHidden !== void 0) { template.acceleratorWorksWhenHidden = this.acceleratorWorksWhenHidden; } if (this.enabled !== void 0) { template.enabled = this.enabled; } if (this.icon !== void 0) { template.icon = this.icon; } if (this.label !== void 0) { template.label = this.label; } if (this.registerAccelerator !== void 0) { template.registerAccelerator = this.registerAccelerator; } if (this.toolTip !== void 0) { template.toolTip = this.toolTip; } return template; } addEventListener(type, listener, options) { super.addEventListener( type, listener, options ); } removeEventListener(type, listener, options) { super.removeEventListener( type, listener, options ); } }; __decorateClass([ property({ type: String }) ], NormalMenuItemElement.prototype, "accelerator"); __decorateClass([ property({ attribute: "accelerator-works-when-hidden", type: Boolean }) ], NormalMenuItemElement.prototype, "acceleratorWorksWhenHidden"); __decorateClass([ property({ type: Boolean }) ], NormalMenuItemElement.prototype, "enabled"); __decorateClass([ property({ type: String }) ], NormalMenuItemElement.prototype, "icon"); __decorateClass([ property({ type: String }) ], NormalMenuItemElement.prototype, "label"); __decorateClass([ property({ attribute: "register-accelerator", type: Boolean }) ], NormalMenuItemElement.prototype, "registerAccelerator"); __decorateClass([ property({ attribute: "tooltip", type: String }) ], NormalMenuItemElement.prototype, "toolTip"); // src/elements/CheckboxMenuItemElement.ts var CheckboxMenuItemElement = class extends NormalMenuItemElement { constructor() { super("checkbox"); } toTemplate() { const template = super.toTemplate(); if (this.checked !== void 0) { template.checked = this.checked; } return template; } }; __decorateClass([ property({ type: Boolean }) ], CheckboxMenuItemElement.prototype, "checked"); // src/sandbox/globals.ts var hoverboardApiKey = "__laserware_hoverboard__"; function getHoverboardGlobals() { const windowGlobals = window[hoverboardApiKey]; if (windowGlobals === void 0) { throw new Error("Globals not found, need to use preload"); } return { showContextMenu(request) { return windowGlobals.showContextMenu(request); }, hideContextMenu(menuId) { return windowGlobals.hideContextMenu(menuId); } }; } // src/elements/ContextMenuEvent.ts var ContextMenuEvent = class extends Event { constructor(type, eventInitDict) { const { trigger, clientX, clientY, menu, menuItem, triggeredByAccelerator, ...rest } = eventInitDict; super(type, { bubbles: true, cancelable: true, composed: true, ...rest }); this.clientX = clientX ?? 0; this.clientY = clientY ?? 0; this.menuItem = menuItem; this.menu = menu; this.trigger = trigger ?? null; this.triggeredByAccelerator = triggeredByAccelerator ?? false; } }; // src/elements/SharingItemEntryElement.ts var SharingItemEntryElement = class extends HTMLElement { get entry() { const type = this.type; if (type === void 0) { throw new Error("A type must be defined on a sharing item entry"); } if (type !== "filePath" && type !== "text" && type !== "url") { throw new Error(`Invalid type ${type} specified for sharing item entry, only "filePath", "text", and "url" allowed`); } if (this.value === void 0) { throw new Error("A value must be defined on a sharing item entry"); } return { type, value: this.value }; } getAttribute(name) { return super.getAttribute(name); } }; __decorateClass([ property({ type: String }) ], SharingItemEntryElement.prototype, "type"); __decorateClass([ property({ type: String }) ], SharingItemEntryElement.prototype, "value"); // src/elements/SubmenuMenuItemElement.ts var SubmenuMenuItemElement = class extends NormalMenuItemElement { constructor() { super("submenu"); } [Symbol.iterator]() { let index = 0; return { next: () => { if (index < this.children.length) { return { value: this.children.item(index++), done: false }; } else { return { done: true }; } } }; } toTemplate() { const template = super.toTemplate(); const walker = document.createTreeWalker(this, NodeFilter.SHOW_ELEMENT); let node = walker.firstChild(); const submenu = []; while (node !== null) { if (node instanceof SharingItemEntryElement) { node = walker.nextNode(); continue; } if (node instanceof ContextMenuItemElement) { node.submenu = this; submenu.push(node.toTemplate()); } node = walker.nextNode(); } return { ...template, submenu }; } }; // src/elements/ContextMenuElement.ts var ContextMenuElement = class extends HTMLElement { #controllers = /* @__PURE__ */ new Map(); #trigger = null; constructor() { super(); this.style.display = "none"; this.attachShadow({ mode: "closed" }); } *[Symbol.iterator]() { for (let index = 0; index < this.children.length; index++) { yield this.children.item(index); } } connectedCallback() { this.setAttribute("inert", ""); if (this.getAttribute("id") === null) { this.id = window.crypto.randomUUID().substring(0, 6); } if (this.target !== void 0) { this.attachTo(this.target); } } disconnectedCallback() { this.dispose(); } getAttribute(name) { return super.getAttribute(name); } toTemplate() { const template = []; const walker = document.createTreeWalker(this, NodeFilter.SHOW_ELEMENT); let node = walker.firstChild(); while (node !== null) { if (node instanceof SharingItemEntryElement) { node = walker.nextNode(); continue; } if (node instanceof ContextMenuItemElement) { if (node.parentNode instanceof SubmenuMenuItemElement) { node = walker.nextNode(); continue; } else { template.push(node.toTemplate()); } } node = walker.nextNode(); } return template; } addEventListener(type, listener, options) { super.addEventListener( type, listener, options ); } removeEventListener(type, listener, options) { super.removeEventListener( type, listener, options ); } async hide() { const globals = getHoverboardGlobals(); await globals.hideContextMenu(this.id); this.dispatchEvent( new ContextMenuEvent("hide", { menu: this, menuItem: null, trigger: this.#trigger }) ); } async show(x, y) { const template = this.toTemplate(); const globals = getHoverboardGlobals(); const dispatchHideEvent = (menuItem2, triggeredByAccelerator) => { this.dispatchEvent( new ContextMenuEvent("hide", { clientX: x, clientY: y, menu: this, menuItem: menuItem2, trigger: this.#trigger, triggeredByAccelerator }) ); }; let linkURL; for (const element of document.elementsFromPoint(x, y)) { if (element instanceof HTMLAnchorElement && linkURL === void 0) { linkURL = element.href || void 0; } } const response = await globals.showContextMenu({ menuId: this.id, position: { x, y }, template, linkURL }); this.dispatchEvent( new ContextMenuEvent("show", { clientX: x, clientY: y, menu: this, menuItem: null, trigger: this.#trigger }) ); if (response.menuId !== this.id) { return null; } const menuItem = response.menuItemId === null ? null : this.#findMenuItem(response.menuItemId); if (!(menuItem instanceof ContextMenuItemElement)) { dispatchHideEvent(null); return null; } const clickInit = { ...response.event, clientX: x, clientY: y, menu: this, menuItem, trigger: this.#trigger }; menuItem.dispatchEvent(new ContextMenuEvent("click", clickInit)); this.dispatchEvent(new ContextMenuEvent("click", clickInit)); dispatchHideEvent(menuItem, response.event.triggeredByAccelerator); return menuItem; } #findMenuItem(id) { const walker = document.createTreeWalker(this, NodeFilter.SHOW_ELEMENT); let node = walker.firstChild(); while (node !== null) { if (node instanceof HTMLElement && "id" in node && node.id === id) { return node; } node = walker.nextNode(); } return null; } attachTo(target) { const controller = new AbortController(); const { signal } = controller; if (target instanceof HTMLElement) { this.#trigger = target; } const handleContextMenu = async (event) => { let trigger = null; if (target instanceof HTMLElement) { trigger = target; } else { const element = event.target; if (element.matches(target)) { trigger = element; } else { const { clientX: x, clientY: y } = event; for (const element2 of document.elementsFromPoint(x, y)) { if (element2.matches(target)) { trigger = element2; break; } } } } this.#trigger = trigger; if (trigger === null) { return; } event.preventDefault(); this.dispatchEvent( new ContextMenuEvent("attach", { ...event, menu: this, menuItem: null, trigger, triggeredByAccelerator: false }) ); await this.show(event.clientX, event.clientY); }; const options = { signal, capture: true }; if (target instanceof HTMLElement) { target.addEventListener("contextmenu", handleContextMenu, options); } else { window.addEventListener("contextmenu", handleContextMenu, options); } this.#controllers.set(target, controller); } detach() { for (const controller of this.#controllers.values()) { controller.abort(); } this.#controllers.clear(); this.#trigger = null; } dispose() { this.detach(); } }; __decorateClass([ property({ type: String }) ], ContextMenuElement.prototype, "id"); __decorateClass([ property({ type: String }) ], ContextMenuElement.prototype, "target"); // src/elements/RadioMenuItemElement.ts var RadioMenuItemElement = class extends NormalMenuItemElement { constructor() { super("radio"); } toTemplate() { const template = super.toTemplate(); if (this.checked !== void 0) { template.checked = this.checked; } return template; } }; __decorateClass([ property({ type: Boolean }) ], RadioMenuItemElement.prototype, "checked"); // src/elements/RoleMenuItemElement.ts var RoleMenuItemElement = class extends ContextMenuItemElement { constructor() { super(void 0); } toTemplate() { const template = super.toTemplate(); if (this.accelerator !== void 0) { template.accelerator = this.accelerator; } if (this.acceleratorWorksWhenHidden !== void 0) { template.acceleratorWorksWhenHidden = this.acceleratorWorksWhenHidden; } if (this.enabled !== void 0) { template.enabled = this.enabled; } if (this.icon !== void 0) { template.icon = this.icon; } if (this.of !== void 0) { template.role = this.of; } if (this.registerAccelerator !== void 0) { template.registerAccelerator = this.registerAccelerator; } if (this.toolTip !== void 0) { template.toolTip = this.toolTip; } return template; } }; __decorateClass([ property({ type: String }) ], RoleMenuItemElement.prototype, "accelerator"); __decorateClass([ property({ attribute: "accelerator-works-when-hidden", type: Boolean }) ], RoleMenuItemElement.prototype, "acceleratorWorksWhenHidden"); __decorateClass([ property({ type: Boolean }) ], RoleMenuItemElement.prototype, "enabled"); __decorateClass([ property({ type: String }) ], RoleMenuItemElement.prototype, "icon"); __decorateClass([ property({ type: String }) ], RoleMenuItemElement.prototype, "of"); __decorateClass([ property({ attribute: "register-accelerator", type: Boolean }) ], RoleMenuItemElement.prototype, "registerAccelerator"); __decorateClass([ property({ attribute: "tooltip", type: String }) ], RoleMenuItemElement.prototype, "toolTip"); // src/elements/SeparatorMenuItemElement.ts var SeparatorMenuItemElement = class extends ContextMenuItemElement { constructor() { super("separator"); } get template() { const { visible, type } = super.toTemplate(); const template = { type }; if (visible !== void 0) { template.visible = visible; } return template; } }; // src/elements/ShareMenuElement.ts var ShareMenuElement = class extends ContextMenuItemElement { constructor() { super(void 0); } toTemplate() { const filePaths = []; const texts = []; const urls = []; for (let index = 0; index < this.children.length; index++) { const child = this.children.item(index); if (!(child instanceof SharingItemEntryElement)) { throw new Error("Only sharing item entry is allowed in share menu"); } const { type, value } = child.entry; if (type === "filePath") { filePaths.push(value); } else if (type === "text") { texts.push(value); } else if (type === "url") { urls.push(value); } } const sharingItem = {}; if (filePaths.length !== 0) { sharingItem.filePaths = filePaths; } if (texts.length !== 0) { sharingItem.texts = texts; } if (urls.length !== 0) { sharingItem.urls = urls; } return { ...super.toTemplate(), role: "shareMenu", sharingItem }; } }; // src/elements/index.ts function registerElements() { customElements.define("checkbox-menu-item", CheckboxMenuItemElement); customElements.define("context-menu", ContextMenuElement); customElements.define("normal-menu-item", NormalMenuItemElement); customElements.define("radio-menu-item", RadioMenuItemElement); customElements.define("role-menu-item", RoleMenuItemElement); customElements.define("separator-menu-item", SeparatorMenuItemElement); customElements.define("submenu-menu-item", SubmenuMenuItemElement); customElements.define("share-menu", ShareMenuElement); customElements.define("sharing-item-entry", SharingItemEntryElement); } export { CheckboxMenuItemElement, ContextMenuElement, ContextMenuEvent, NormalMenuItemElement, RadioMenuItemElement, RoleMenuItemElement, SeparatorMenuItemElement, SubmenuMenuItemElement, registerElements }; //# sourceMappingURL=elements.mjs.map //# sourceMappingURL=elements.mjs.map