Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
38 commits
Select commit Hold shift + click to select a range
e011f99
client: add support for custom menu items
adoriandoran Nov 17, 2025
5291a68
client: create a placeholder for a color picker menu item
adoriandoran Nov 17, 2025
441c55e
client/note color picker menu item: add initial implementation
adoriandoran Nov 17, 2025
8729fe4
client/note color picker menu item: add support to operate with note …
adoriandoran Nov 17, 2025
e239bca
client/note color picker menu item: fix data type
adoriandoran Nov 17, 2025
1ac7ce0
client/note color picker menu item: add to the calendar item context …
adoriandoran Nov 17, 2025
69ad40c
client/note color picker menu item: add to the board item context menu
adoriandoran Nov 17, 2025
7983087
client/note color picker menu item: add to the geo map item context menu
adoriandoran Nov 17, 2025
87fcc0a
client/note color picker menu item: add to the table row context menu
adoriandoran Nov 17, 2025
e5ac8a0
client/note color picker menu item: refactor
adoriandoran Nov 17, 2025
870fef3
client/note color picker menu item: fix a typo
adoriandoran Nov 17, 2025
87afc64
client/note color picker menu item: fix current selection
adoriandoran Nov 17, 2025
72051c8
client/note color picker menu item: add a separator to the tree conte…
adoriandoran Nov 17, 2025
e684735
client/note color picker menu item: improve the integration with the …
adoriandoran Nov 18, 2025
d441bcc
client/note color picker menu item: refactor
adoriandoran Nov 18, 2025
01d6dee
client/note color picker menu item: improve label handling
adoriandoran Nov 18, 2025
5ecd8b4
client/note color picker menu item: add support to select a custom color
adoriandoran Nov 18, 2025
4574718
client/refactor: extract the debouncer to a separate module
adoriandoran Nov 18, 2025
c81aef6
client/note color picker menu item: update the color palette
adoriandoran Nov 18, 2025
79dc5e4
client/note color picker menu item: tweak style
adoriandoran Nov 18, 2025
c25859c
client/debouncer: report pending updates before destroying
adoriandoran Nov 18, 2025
828a786
client/note color picker menu item: improve
adoriandoran Nov 19, 2025
c91eec8
client/note color picker menu item: add a new color to the palette
adoriandoran Nov 20, 2025
fb16336
client/note color picker menu item: refactor
adoriandoran Nov 20, 2025
422b324
client/note color picker menu item: add tooltips
adoriandoran Nov 20, 2025
f15e048
client/note color picker menu item: refactor stylesheet
adoriandoran Nov 20, 2025
1de9f71
client/note color picker: refactor
adoriandoran Nov 20, 2025
b0476c7
client/note color picker: refactor
adoriandoran Nov 20, 2025
e4c928a
client/note color picker: refactor
adoriandoran Nov 20, 2025
926f0f8
client/note color picker: refactor
adoriandoran Nov 20, 2025
a5c5486
client/note color picker: tweak style
adoriandoran Nov 20, 2025
1b2d922
client/note color picker: tweak style
adoriandoran Nov 20, 2025
d42f911
client/note color picker: dismiss the menu when a color is clicked
adoriandoran Nov 20, 2025
e53a225
client/tree context menu: relocate the note color picker
adoriandoran Nov 20, 2025
36350bd
Merge branch 'main' of https://github.com/TriliumNext/Trilium into fe…
adoriandoran Nov 20, 2025
e9796c9
client/note color picker: fix the custom color picker on Safari
adoriandoran Nov 20, 2025
0db08f4
client/note color picker: decrease the debouncer interval
adoriandoran Nov 20, 2025
d87e8b7
client/note color picker/clear color cell: fix icon alignment
adoriandoran Nov 21, 2025
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions apps/client/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@
"autocomplete.js": "0.38.1",
"bootstrap": "5.3.8",
"boxicons": "2.1.4",
"clsx": "2.1.1",
"color": "5.0.3",
"dayjs": "1.11.19",
"dayjs-plugin-utc": "0.1.2",
Expand Down
227 changes: 124 additions & 103 deletions apps/client/src/menus/context_menu.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import { KeyboardActionNames } from "@triliumnext/commons";
import keyboardActionService, { getActionSync } from "../services/keyboard_actions.js";
import note_tooltip from "../services/note_tooltip.js";
import utils from "../services/utils.js";
import { should } from "vitest";
import { h, JSX, render } from "preact";

export interface ContextMenuOptions<T> {
x: number;
Expand All @@ -15,6 +15,11 @@ export interface ContextMenuOptions<T> {
onHide?: () => void;
}

export interface CustomMenuItem {
kind: "custom",
componentFn: () => JSX.Element | null;
}

export interface MenuSeparatorItem {
kind: "separator";
}
Expand Down Expand Up @@ -51,7 +56,7 @@ export interface MenuCommandItem<T> {
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 ContextMenuEvent = PointerEvent | MouseEvent | JQuery.ContextMenuEvent;

Expand Down Expand Up @@ -202,126 +207,142 @@ class ContextMenu {
$group.append($("<h6>").addClass("dropdown-header").text(item.title));
shouldResetGroup = true;
} else {
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("&nbsp;");
}
if ("kind" in item && item.kind === "custom") {
// Custom menu item
$group.append(this.createCustomMenuItem(item));
} else {
// Standard menu item
$group.append(this.createMenuItem(item));
}

const $link = $("<span>")
.append($icon)
.append(" &nbsp; ") // some space between icon and text
.append(item.title);
// 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.
if (shouldResetGroup) {
$group = $parent;
shouldResetGroup = false;
};
}
}
}

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 ("badges" in item && item.badges) {
for (let badge of item.badges) {
const badgeElement = $(`<span class="badge">`).text(badge.title);
if ("uiIcon" in item || "checked" in item) {
const icon = (item.checked ? "bx bx-check" : item.uiIcon);
if (icon) {
$icon.addClass(icon);
} else {
$icon.append("&nbsp;");
}
}

if (badge.className) {
badgeElement.addClass(badge.className);
}
const $link = $("<span>")
.append($icon)
.append(" &nbsp; ") // some space between icon and text
.append(item.title);

$link.append(badgeElement);
}
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);
}

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));
$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("+"));
}

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 (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));
}

if ("items" in item && item.items) {
$item.addClass("dropdown-submenu");
$link.addClass("dropdown-toggle");
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;
}

const $subMenu = $("<ul>").addClass("dropdown-menu");
const hasColumns = !!item.columns && item.columns > 1;
if (!this.isMobile && hasColumns) {
$subMenu.css("column-count", item.columns!);
}
if (this.isMobile && "items" in item && item.items) {
const $item = $(e.target).closest(".dropdown-item");

this.addItems($subMenu, item.items, hasColumns);
$item.toggleClass("submenu-open");
$item.find("ul.dropdown-menu").toggleClass("show");
return false;
}

$item.append($subMenu);
if ("handler" in item && item.handler) {
item.handler(item, e);
}

$group.append($item);
this.options?.selectMenuItemHandler(item, e);

// 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.
if (shouldResetGroup) {
$group = $parent;
shouldResetGroup = false;
};
// 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() {
Expand Down
86 changes: 86 additions & 0 deletions apps/client/src/menus/custom-items/NoteColorPicker.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
:root {
--note-color-picker-clear-color-cell-background: var(--primary-button-background-color);
--note-color-picker-clear-color-cell-color: var(--main-background-color);
--note-color-picker-clear-color-cell-selection-outline-color: var(--primary-button-border-color);
}

.note-color-picker {
display: flex;
gap: 8px;
justify-content: space-between;
}

.note-color-picker .color-cell {
--color-picker-cell-size: 14px;

width: var(--color-picker-cell-size);
height: var(--color-picker-cell-size);
border-radius: 4px;
background-color: var(--color);
}

.note-color-picker .color-cell:not(.selected):hover {
transform: scale(1.2);
}

.note-color-picker .color-cell.disabled-color-cell {
cursor: not-allowed;
}

.note-color-picker .color-cell.selected {
outline: 2px solid var(--outline-color, var(--color));
outline-offset: 2px;
}

/*
* RESET COLOR CELL
*/

.note-color-picker .color-cell-reset {
--color: var(--note-color-picker-clear-color-cell-background);
--outline-color: var(--note-color-picker-clear-color-cell-selection-outline-color);

position: relative;
display: flex;
justify-content: center;
align-items: center;
}

.note-color-picker .color-cell-reset svg {
width: var(--color-picker-cell-size);
height: var(--color-picker-cell-size);
fill: var(--note-color-picker-clear-color-cell-color);
}

/*
* CUSTOM COLOR CELL
*/

.note-color-picker .custom-color-cell::before {
position: absolute;
content: "\ed35";
display: flex;
top: 0;
left: 0;
right: 0;
bottom: 0;
font-size: calc(var(--color-picker-cell-size) * 1.3);
justify-content: center;
align-items: center;
font-family: boxicons;
font-size: 16px;
color: var(--foreground);
}

.note-color-picker .custom-color-cell {
position: relative;
display: flex;
justify-content: center;

}

.note-color-picker .custom-color-cell.custom-color-cell-empty {
background-image: url(./custom-color.png);
background-size: cover;
--foreground: transparent;
}
Loading