mirror of
https://github.com/zadam/trilium.git
synced 2025-11-21 08:04:24 +01:00
client: add support for custom menu items
This commit is contained in:
parent
fc8042aa25
commit
e011f99161
@ -2,7 +2,7 @@ import { KeyboardActionNames } from "@triliumnext/commons";
|
|||||||
import keyboardActionService, { getActionSync } from "../services/keyboard_actions.js";
|
import keyboardActionService, { getActionSync } from "../services/keyboard_actions.js";
|
||||||
import note_tooltip from "../services/note_tooltip.js";
|
import note_tooltip from "../services/note_tooltip.js";
|
||||||
import utils from "../services/utils.js";
|
import utils from "../services/utils.js";
|
||||||
import { should } from "vitest";
|
import { h, JSX, render } from "preact";
|
||||||
|
|
||||||
export interface ContextMenuOptions<T> {
|
export interface ContextMenuOptions<T> {
|
||||||
x: number;
|
x: number;
|
||||||
@ -15,6 +15,11 @@ export interface ContextMenuOptions<T> {
|
|||||||
onHide?: () => void;
|
onHide?: () => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface CustomMenuItem {
|
||||||
|
kind: "custom",
|
||||||
|
componentFn: () => JSX.Element;
|
||||||
|
}
|
||||||
|
|
||||||
export interface MenuSeparatorItem {
|
export interface MenuSeparatorItem {
|
||||||
kind: "separator";
|
kind: "separator";
|
||||||
}
|
}
|
||||||
@ -51,7 +56,7 @@ export interface MenuCommandItem<T> {
|
|||||||
columns?: number;
|
columns?: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
export type MenuItem<T> = MenuCommandItem<T> | MenuSeparatorItem | MenuHeader;
|
export type MenuItem<T> = MenuCommandItem<T> | CustomMenuItem | MenuSeparatorItem | MenuHeader;
|
||||||
export type MenuHandler<T> = (item: MenuCommandItem<T>, e: JQuery.MouseDownEvent<HTMLElement, undefined, HTMLElement, HTMLElement>) => void;
|
export type MenuHandler<T> = (item: MenuCommandItem<T>, e: JQuery.MouseDownEvent<HTMLElement, undefined, HTMLElement, HTMLElement>) => void;
|
||||||
export type ContextMenuEvent = PointerEvent | MouseEvent | JQuery.ContextMenuEvent;
|
export type ContextMenuEvent = PointerEvent | MouseEvent | JQuery.ContextMenuEvent;
|
||||||
|
|
||||||
@ -202,118 +207,14 @@ class ContextMenu {
|
|||||||
$group.append($("<h6>").addClass("dropdown-header").text(item.title));
|
$group.append($("<h6>").addClass("dropdown-header").text(item.title));
|
||||||
shouldResetGroup = true;
|
shouldResetGroup = true;
|
||||||
} else {
|
} else {
|
||||||
const $icon = $("<span>");
|
if ("kind" in item && item.kind === "custom") {
|
||||||
|
// Custom menu item
|
||||||
if ("uiIcon" in item || "checked" in item) {
|
$group.append(this.createCustomMenuItem(item));
|
||||||
const icon = (item.checked ? "bx bx-check" : item.uiIcon);
|
} else {
|
||||||
if (icon) {
|
// Standard menu item
|
||||||
$icon.addClass(icon);
|
$group.append(this.createMenuItem(item));
|
||||||
} else {
|
|
||||||
$icon.append(" ");
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const $link = $("<span>")
|
|
||||||
.append($icon)
|
|
||||||
.append(" ") // some space between icon and text
|
|
||||||
.append(item.title);
|
|
||||||
|
|
||||||
if ("badges" in item && item.badges) {
|
|
||||||
for (let badge of item.badges) {
|
|
||||||
const badgeElement = $(`<span class="badge">`).text(badge.title);
|
|
||||||
|
|
||||||
if (badge.className) {
|
|
||||||
badgeElement.addClass(badge.className);
|
|
||||||
}
|
|
||||||
|
|
||||||
$link.append(badgeElement);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if ("keyboardShortcut" in item && item.keyboardShortcut) {
|
|
||||||
const shortcuts = getActionSync(item.keyboardShortcut).effectiveShortcuts;
|
|
||||||
if (shortcuts) {
|
|
||||||
const allShortcuts: string[] = [];
|
|
||||||
for (const effectiveShortcut of shortcuts) {
|
|
||||||
allShortcuts.push(effectiveShortcut.split("+")
|
|
||||||
.map(key => `<kbd>${key}</kbd>`)
|
|
||||||
.join("+"));
|
|
||||||
}
|
|
||||||
|
|
||||||
if (allShortcuts.length) {
|
|
||||||
const container = $("<span>").addClass("keyboard-shortcut");
|
|
||||||
container.append($(allShortcuts.join(",")));
|
|
||||||
$link.append(container);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else if ("shortcut" in item && item.shortcut) {
|
|
||||||
$link.append($("<kbd>").text(item.shortcut));
|
|
||||||
}
|
|
||||||
|
|
||||||
const $item = $("<li>")
|
|
||||||
.addClass("dropdown-item")
|
|
||||||
.append($link)
|
|
||||||
.on("contextmenu", (e) => false)
|
|
||||||
// important to use mousedown instead of click since the former does not change focus
|
|
||||||
// (especially important for focused text for spell check)
|
|
||||||
.on("mousedown", (e) => {
|
|
||||||
e.stopPropagation();
|
|
||||||
|
|
||||||
if (e.which !== 1) {
|
|
||||||
// only left click triggers menu items
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (this.isMobile && "items" in item && item.items) {
|
|
||||||
const $item = $(e.target).closest(".dropdown-item");
|
|
||||||
|
|
||||||
$item.toggleClass("submenu-open");
|
|
||||||
$item.find("ul.dropdown-menu").toggleClass("show");
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
if ("handler" in item && item.handler) {
|
|
||||||
item.handler(item, e);
|
|
||||||
}
|
|
||||||
|
|
||||||
this.options?.selectMenuItemHandler(item, e);
|
|
||||||
|
|
||||||
// it's important to stop the propagation especially for sub-menus, otherwise the event
|
|
||||||
// might be handled again by top-level menu
|
|
||||||
return false;
|
|
||||||
});
|
|
||||||
|
|
||||||
$item.on("mouseup", (e) => {
|
|
||||||
// Prevent submenu from failing to expand on mobile
|
|
||||||
if (!this.isMobile || !("items" in item && item.items)) {
|
|
||||||
e.stopPropagation();
|
|
||||||
// Hide the content menu on mouse up to prevent the mouse event from propagating to the elements below.
|
|
||||||
this.hide();
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
if ("enabled" in item && item.enabled !== undefined && !item.enabled) {
|
|
||||||
$item.addClass("disabled");
|
|
||||||
}
|
|
||||||
|
|
||||||
if ("items" in item && item.items) {
|
|
||||||
$item.addClass("dropdown-submenu");
|
|
||||||
$link.addClass("dropdown-toggle");
|
|
||||||
|
|
||||||
const $subMenu = $("<ul>").addClass("dropdown-menu");
|
|
||||||
const hasColumns = !!item.columns && item.columns > 1;
|
|
||||||
if (!this.isMobile && hasColumns) {
|
|
||||||
$subMenu.css("column-count", item.columns!);
|
|
||||||
}
|
|
||||||
|
|
||||||
this.addItems($subMenu, item.items, hasColumns);
|
|
||||||
|
|
||||||
$item.append($subMenu);
|
|
||||||
}
|
|
||||||
|
|
||||||
$group.append($item);
|
|
||||||
|
|
||||||
// After adding a menu item, if the previous item was a separator or header,
|
// After adding a menu item, if the previous item was a separator or header,
|
||||||
// reset the group so that the next item will be appended directly to the parent.
|
// reset the group so that the next item will be appended directly to the parent.
|
||||||
if (shouldResetGroup) {
|
if (shouldResetGroup) {
|
||||||
@ -324,6 +225,126 @@ class ContextMenu {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private createCustomMenuItem(item: CustomMenuItem) {
|
||||||
|
const element = document.createElement("li");
|
||||||
|
element.classList.add("dropdown-custom-item");
|
||||||
|
render(h(item.componentFn, {}), element);
|
||||||
|
return element;
|
||||||
|
}
|
||||||
|
|
||||||
|
private createMenuItem(item: MenuCommandItem<any>) {
|
||||||
|
const $icon = $("<span>");
|
||||||
|
|
||||||
|
if ("uiIcon" in item || "checked" in item) {
|
||||||
|
const icon = (item.checked ? "bx bx-check" : item.uiIcon);
|
||||||
|
if (icon) {
|
||||||
|
$icon.addClass(icon);
|
||||||
|
} else {
|
||||||
|
$icon.append(" ");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const $link = $("<span>")
|
||||||
|
.append($icon)
|
||||||
|
.append(" ") // some space between icon and text
|
||||||
|
.append(item.title);
|
||||||
|
|
||||||
|
if ("badges" in item && item.badges) {
|
||||||
|
for (let badge of item.badges) {
|
||||||
|
const badgeElement = $(`<span class="badge">`).text(badge.title);
|
||||||
|
|
||||||
|
if (badge.className) {
|
||||||
|
badgeElement.addClass(badge.className);
|
||||||
|
}
|
||||||
|
|
||||||
|
$link.append(badgeElement);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if ("keyboardShortcut" in item && item.keyboardShortcut) {
|
||||||
|
const shortcuts = getActionSync(item.keyboardShortcut).effectiveShortcuts;
|
||||||
|
if (shortcuts) {
|
||||||
|
const allShortcuts: string[] = [];
|
||||||
|
for (const effectiveShortcut of shortcuts) {
|
||||||
|
allShortcuts.push(effectiveShortcut.split("+")
|
||||||
|
.map(key => `<kbd>${key}</kbd>`)
|
||||||
|
.join("+"));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (allShortcuts.length) {
|
||||||
|
const container = $("<span>").addClass("keyboard-shortcut");
|
||||||
|
container.append($(allShortcuts.join(",")));
|
||||||
|
$link.append(container);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else if ("shortcut" in item && item.shortcut) {
|
||||||
|
$link.append($("<kbd>").text(item.shortcut));
|
||||||
|
}
|
||||||
|
|
||||||
|
const $item = $("<li>")
|
||||||
|
.addClass("dropdown-item")
|
||||||
|
.append($link)
|
||||||
|
.on("contextmenu", (e) => false)
|
||||||
|
// important to use mousedown instead of click since the former does not change focus
|
||||||
|
// (especially important for focused text for spell check)
|
||||||
|
.on("mousedown", (e) => {
|
||||||
|
e.stopPropagation();
|
||||||
|
|
||||||
|
if (e.which !== 1) {
|
||||||
|
// only left click triggers menu items
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.isMobile && "items" in item && item.items) {
|
||||||
|
const $item = $(e.target).closest(".dropdown-item");
|
||||||
|
|
||||||
|
$item.toggleClass("submenu-open");
|
||||||
|
$item.find("ul.dropdown-menu").toggleClass("show");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ("handler" in item && item.handler) {
|
||||||
|
item.handler(item, e);
|
||||||
|
}
|
||||||
|
|
||||||
|
this.options?.selectMenuItemHandler(item, e);
|
||||||
|
|
||||||
|
// it's important to stop the propagation especially for sub-menus, otherwise the event
|
||||||
|
// might be handled again by top-level menu
|
||||||
|
return false;
|
||||||
|
});
|
||||||
|
|
||||||
|
$item.on("mouseup", (e) => {
|
||||||
|
// Prevent submenu from failing to expand on mobile
|
||||||
|
if (!this.isMobile || !("items" in item && item.items)) {
|
||||||
|
e.stopPropagation();
|
||||||
|
// Hide the content menu on mouse up to prevent the mouse event from propagating to the elements below.
|
||||||
|
this.hide();
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
if ("enabled" in item && item.enabled !== undefined && !item.enabled) {
|
||||||
|
$item.addClass("disabled");
|
||||||
|
}
|
||||||
|
|
||||||
|
if ("items" in item && item.items) {
|
||||||
|
$item.addClass("dropdown-submenu");
|
||||||
|
$link.addClass("dropdown-toggle");
|
||||||
|
|
||||||
|
const $subMenu = $("<ul>").addClass("dropdown-menu");
|
||||||
|
const hasColumns = !!item.columns && item.columns > 1;
|
||||||
|
if (!this.isMobile && hasColumns) {
|
||||||
|
$subMenu.css("column-count", item.columns!);
|
||||||
|
}
|
||||||
|
|
||||||
|
this.addItems($subMenu, item.items, hasColumns);
|
||||||
|
|
||||||
|
$item.append($subMenu);
|
||||||
|
}
|
||||||
|
return $item;
|
||||||
|
}
|
||||||
|
|
||||||
async hide() {
|
async hide() {
|
||||||
this.options?.onHide?.();
|
this.options?.onHide?.();
|
||||||
this.$widget.removeClass("show");
|
this.$widget.removeClass("show");
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user