Skip to content

Commit 33b9e6d

Browse files
authored
Add support for changing note colors via UI (#7795)
2 parents 63d430c + d87e8b7 commit 33b9e6d

File tree

16 files changed

+521
-107
lines changed

16 files changed

+521
-107
lines changed

apps/client/package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,7 @@
3636
"autocomplete.js": "0.38.1",
3737
"bootstrap": "5.3.8",
3838
"boxicons": "2.1.4",
39+
"clsx": "2.1.1",
3940
"color": "5.0.3",
4041
"dayjs": "1.11.19",
4142
"dayjs-plugin-utc": "0.1.2",

apps/client/src/menus/context_menu.ts

Lines changed: 124 additions & 103 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ import { KeyboardActionNames } from "@triliumnext/commons";
22
import keyboardActionService, { getActionSync } from "../services/keyboard_actions.js";
33
import note_tooltip from "../services/note_tooltip.js";
44
import utils from "../services/utils.js";
5-
import { should } from "vitest";
5+
import { h, JSX, render } from "preact";
66

77
export interface ContextMenuOptions<T> {
88
x: number;
@@ -15,6 +15,11 @@ export interface ContextMenuOptions<T> {
1515
onHide?: () => void;
1616
}
1717

18+
export interface CustomMenuItem {
19+
kind: "custom",
20+
componentFn: () => JSX.Element | null;
21+
}
22+
1823
export interface MenuSeparatorItem {
1924
kind: "separator";
2025
}
@@ -51,7 +56,7 @@ export interface MenuCommandItem<T> {
5156
columns?: number;
5257
}
5358

54-
export type MenuItem<T> = MenuCommandItem<T> | MenuSeparatorItem | MenuHeader;
59+
export type MenuItem<T> = MenuCommandItem<T> | CustomMenuItem | MenuSeparatorItem | MenuHeader;
5560
export type MenuHandler<T> = (item: MenuCommandItem<T>, e: JQuery.MouseDownEvent<HTMLElement, undefined, HTMLElement, HTMLElement>) => void;
5661
export type ContextMenuEvent = PointerEvent | MouseEvent | JQuery.ContextMenuEvent;
5762

@@ -202,126 +207,142 @@ class ContextMenu {
202207
$group.append($("<h6>").addClass("dropdown-header").text(item.title));
203208
shouldResetGroup = true;
204209
} else {
205-
const $icon = $("<span>");
206-
207-
if ("uiIcon" in item || "checked" in item) {
208-
const icon = (item.checked ? "bx bx-check" : item.uiIcon);
209-
if (icon) {
210-
$icon.addClass(icon);
211-
} else {
212-
$icon.append("&nbsp;");
213-
}
210+
if ("kind" in item && item.kind === "custom") {
211+
// Custom menu item
212+
$group.append(this.createCustomMenuItem(item));
213+
} else {
214+
// Standard menu item
215+
$group.append(this.createMenuItem(item));
214216
}
215217

216-
const $link = $("<span>")
217-
.append($icon)
218-
.append(" &nbsp; ") // some space between icon and text
219-
.append(item.title);
218+
// After adding a menu item, if the previous item was a separator or header,
219+
// reset the group so that the next item will be appended directly to the parent.
220+
if (shouldResetGroup) {
221+
$group = $parent;
222+
shouldResetGroup = false;
223+
};
224+
}
225+
}
226+
}
227+
228+
private createCustomMenuItem(item: CustomMenuItem) {
229+
const element = document.createElement("li");
230+
element.classList.add("dropdown-custom-item");
231+
render(h(item.componentFn, {}), element);
232+
return element;
233+
}
234+
235+
private createMenuItem(item: MenuCommandItem<any>) {
236+
const $icon = $("<span>");
220237

221-
if ("badges" in item && item.badges) {
222-
for (let badge of item.badges) {
223-
const badgeElement = $(`<span class="badge">`).text(badge.title);
238+
if ("uiIcon" in item || "checked" in item) {
239+
const icon = (item.checked ? "bx bx-check" : item.uiIcon);
240+
if (icon) {
241+
$icon.addClass(icon);
242+
} else {
243+
$icon.append("&nbsp;");
244+
}
245+
}
224246

225-
if (badge.className) {
226-
badgeElement.addClass(badge.className);
227-
}
247+
const $link = $("<span>")
248+
.append($icon)
249+
.append(" &nbsp; ") // some space between icon and text
250+
.append(item.title);
228251

229-
$link.append(badgeElement);
230-
}
252+
if ("badges" in item && item.badges) {
253+
for (let badge of item.badges) {
254+
const badgeElement = $(`<span class="badge">`).text(badge.title);
255+
256+
if (badge.className) {
257+
badgeElement.addClass(badge.className);
231258
}
232259

233-
if ("keyboardShortcut" in item && item.keyboardShortcut) {
234-
const shortcuts = getActionSync(item.keyboardShortcut).effectiveShortcuts;
235-
if (shortcuts) {
236-
const allShortcuts: string[] = [];
237-
for (const effectiveShortcut of shortcuts) {
238-
allShortcuts.push(effectiveShortcut.split("+")
239-
.map(key => `<kbd>${key}</kbd>`)
240-
.join("+"));
241-
}
242-
243-
if (allShortcuts.length) {
244-
const container = $("<span>").addClass("keyboard-shortcut");
245-
container.append($(allShortcuts.join(",")));
246-
$link.append(container);
247-
}
248-
}
249-
} else if ("shortcut" in item && item.shortcut) {
250-
$link.append($("<kbd>").text(item.shortcut));
260+
$link.append(badgeElement);
261+
}
262+
}
263+
264+
if ("keyboardShortcut" in item && item.keyboardShortcut) {
265+
const shortcuts = getActionSync(item.keyboardShortcut).effectiveShortcuts;
266+
if (shortcuts) {
267+
const allShortcuts: string[] = [];
268+
for (const effectiveShortcut of shortcuts) {
269+
allShortcuts.push(effectiveShortcut.split("+")
270+
.map(key => `<kbd>${key}</kbd>`)
271+
.join("+"));
251272
}
252273

253-
const $item = $("<li>")
254-
.addClass("dropdown-item")
255-
.append($link)
256-
.on("contextmenu", (e) => false)
257-
// important to use mousedown instead of click since the former does not change focus
258-
// (especially important for focused text for spell check)
259-
.on("mousedown", (e) => {
260-
e.stopPropagation();
261-
262-
if (e.which !== 1) {
263-
// only left click triggers menu items
264-
return false;
265-
}
266-
267-
if (this.isMobile && "items" in item && item.items) {
268-
const $item = $(e.target).closest(".dropdown-item");
269-
270-
$item.toggleClass("submenu-open");
271-
$item.find("ul.dropdown-menu").toggleClass("show");
272-
return false;
273-
}
274-
275-
if ("handler" in item && item.handler) {
276-
item.handler(item, e);
277-
}
278-
279-
this.options?.selectMenuItemHandler(item, e);
280-
281-
// it's important to stop the propagation especially for sub-menus, otherwise the event
282-
// might be handled again by top-level menu
283-
return false;
284-
});
285-
286-
$item.on("mouseup", (e) => {
287-
// Prevent submenu from failing to expand on mobile
288-
if (!this.isMobile || !("items" in item && item.items)) {
289-
e.stopPropagation();
290-
// Hide the content menu on mouse up to prevent the mouse event from propagating to the elements below.
291-
this.hide();
292-
return false;
293-
}
294-
});
295-
296-
if ("enabled" in item && item.enabled !== undefined && !item.enabled) {
297-
$item.addClass("disabled");
274+
if (allShortcuts.length) {
275+
const container = $("<span>").addClass("keyboard-shortcut");
276+
container.append($(allShortcuts.join(",")));
277+
$link.append(container);
298278
}
279+
}
280+
} else if ("shortcut" in item && item.shortcut) {
281+
$link.append($("<kbd>").text(item.shortcut));
282+
}
299283

300-
if ("items" in item && item.items) {
301-
$item.addClass("dropdown-submenu");
302-
$link.addClass("dropdown-toggle");
284+
const $item = $("<li>")
285+
.addClass("dropdown-item")
286+
.append($link)
287+
.on("contextmenu", (e) => false)
288+
// important to use mousedown instead of click since the former does not change focus
289+
// (especially important for focused text for spell check)
290+
.on("mousedown", (e) => {
291+
e.stopPropagation();
292+
293+
if (e.which !== 1) {
294+
// only left click triggers menu items
295+
return false;
296+
}
303297

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

310-
this.addItems($subMenu, item.items, hasColumns);
301+
$item.toggleClass("submenu-open");
302+
$item.find("ul.dropdown-menu").toggleClass("show");
303+
return false;
304+
}
311305

312-
$item.append($subMenu);
306+
if ("handler" in item && item.handler) {
307+
item.handler(item, e);
313308
}
314309

315-
$group.append($item);
310+
this.options?.selectMenuItemHandler(item, e);
316311

317-
// After adding a menu item, if the previous item was a separator or header,
318-
// reset the group so that the next item will be appended directly to the parent.
319-
if (shouldResetGroup) {
320-
$group = $parent;
321-
shouldResetGroup = false;
322-
};
312+
// it's important to stop the propagation especially for sub-menus, otherwise the event
313+
// might be handled again by top-level menu
314+
return false;
315+
});
316+
317+
$item.on("mouseup", (e) => {
318+
// Prevent submenu from failing to expand on mobile
319+
if (!this.isMobile || !("items" in item && item.items)) {
320+
e.stopPropagation();
321+
// Hide the content menu on mouse up to prevent the mouse event from propagating to the elements below.
322+
this.hide();
323+
return false;
324+
}
325+
});
326+
327+
if ("enabled" in item && item.enabled !== undefined && !item.enabled) {
328+
$item.addClass("disabled");
329+
}
330+
331+
if ("items" in item && item.items) {
332+
$item.addClass("dropdown-submenu");
333+
$link.addClass("dropdown-toggle");
334+
335+
const $subMenu = $("<ul>").addClass("dropdown-menu");
336+
const hasColumns = !!item.columns && item.columns > 1;
337+
if (!this.isMobile && hasColumns) {
338+
$subMenu.css("column-count", item.columns!);
323339
}
340+
341+
this.addItems($subMenu, item.items, hasColumns);
342+
343+
$item.append($subMenu);
324344
}
345+
return $item;
325346
}
326347

327348
async hide() {
Lines changed: 86 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,86 @@
1+
:root {
2+
--note-color-picker-clear-color-cell-background: var(--primary-button-background-color);
3+
--note-color-picker-clear-color-cell-color: var(--main-background-color);
4+
--note-color-picker-clear-color-cell-selection-outline-color: var(--primary-button-border-color);
5+
}
6+
7+
.note-color-picker {
8+
display: flex;
9+
gap: 8px;
10+
justify-content: space-between;
11+
}
12+
13+
.note-color-picker .color-cell {
14+
--color-picker-cell-size: 14px;
15+
16+
width: var(--color-picker-cell-size);
17+
height: var(--color-picker-cell-size);
18+
border-radius: 4px;
19+
background-color: var(--color);
20+
}
21+
22+
.note-color-picker .color-cell:not(.selected):hover {
23+
transform: scale(1.2);
24+
}
25+
26+
.note-color-picker .color-cell.disabled-color-cell {
27+
cursor: not-allowed;
28+
}
29+
30+
.note-color-picker .color-cell.selected {
31+
outline: 2px solid var(--outline-color, var(--color));
32+
outline-offset: 2px;
33+
}
34+
35+
/*
36+
* RESET COLOR CELL
37+
*/
38+
39+
.note-color-picker .color-cell-reset {
40+
--color: var(--note-color-picker-clear-color-cell-background);
41+
--outline-color: var(--note-color-picker-clear-color-cell-selection-outline-color);
42+
43+
position: relative;
44+
display: flex;
45+
justify-content: center;
46+
align-items: center;
47+
}
48+
49+
.note-color-picker .color-cell-reset svg {
50+
width: var(--color-picker-cell-size);
51+
height: var(--color-picker-cell-size);
52+
fill: var(--note-color-picker-clear-color-cell-color);
53+
}
54+
55+
/*
56+
* CUSTOM COLOR CELL
57+
*/
58+
59+
.note-color-picker .custom-color-cell::before {
60+
position: absolute;
61+
content: "\ed35";
62+
display: flex;
63+
top: 0;
64+
left: 0;
65+
right: 0;
66+
bottom: 0;
67+
font-size: calc(var(--color-picker-cell-size) * 1.3);
68+
justify-content: center;
69+
align-items: center;
70+
font-family: boxicons;
71+
font-size: 16px;
72+
color: var(--foreground);
73+
}
74+
75+
.note-color-picker .custom-color-cell {
76+
position: relative;
77+
display: flex;
78+
justify-content: center;
79+
80+
}
81+
82+
.note-color-picker .custom-color-cell.custom-color-cell-empty {
83+
background-image: url(./custom-color.png);
84+
background-size: cover;
85+
--foreground: transparent;
86+
}

0 commit comments

Comments
 (0)