@laserware/hoverboard
Version:
Better context menus for Electron.
631 lines (617 loc) • 17.9 kB
JavaScript
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