mirror of
https://github.com/zadam/trilium.git
synced 2025-11-21 16:14:23 +01:00
Add support for changing note colors via UI (#7795)
This commit is contained in:
commit
33b9e6d0c1
@ -36,6 +36,7 @@
|
|||||||
"autocomplete.js": "0.38.1",
|
"autocomplete.js": "0.38.1",
|
||||||
"bootstrap": "5.3.8",
|
"bootstrap": "5.3.8",
|
||||||
"boxicons": "2.1.4",
|
"boxicons": "2.1.4",
|
||||||
|
"clsx": "2.1.1",
|
||||||
"color": "5.0.3",
|
"color": "5.0.3",
|
||||||
"dayjs": "1.11.19",
|
"dayjs": "1.11.19",
|
||||||
"dayjs-plugin-utc": "0.1.2",
|
"dayjs-plugin-utc": "0.1.2",
|
||||||
|
|||||||
@ -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 | null;
|
||||||
|
}
|
||||||
|
|
||||||
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,6 +207,32 @@ 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 {
|
||||||
|
if ("kind" in item && item.kind === "custom") {
|
||||||
|
// Custom menu item
|
||||||
|
$group.append(this.createCustomMenuItem(item));
|
||||||
|
} else {
|
||||||
|
// Standard menu item
|
||||||
|
$group.append(this.createMenuItem(item));
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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>");
|
const $icon = $("<span>");
|
||||||
|
|
||||||
if ("uiIcon" in item || "checked" in item) {
|
if ("uiIcon" in item || "checked" in item) {
|
||||||
@ -311,17 +342,7 @@ class ContextMenu {
|
|||||||
|
|
||||||
$item.append($subMenu);
|
$item.append($subMenu);
|
||||||
}
|
}
|
||||||
|
return $item;
|
||||||
$group.append($item);
|
|
||||||
|
|
||||||
// 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;
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async hide() {
|
async hide() {
|
||||||
|
|||||||
86
apps/client/src/menus/custom-items/NoteColorPicker.css
Normal file
86
apps/client/src/menus/custom-items/NoteColorPicker.css
Normal file
@ -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;
|
||||||
|
}
|
||||||
204
apps/client/src/menus/custom-items/NoteColorPicker.tsx
Normal file
204
apps/client/src/menus/custom-items/NoteColorPicker.tsx
Normal file
@ -0,0 +1,204 @@
|
|||||||
|
import "./NoteColorPicker.css";
|
||||||
|
import { t } from "../../services/i18n";
|
||||||
|
import { useCallback, useEffect, useRef, useState} from "preact/hooks";
|
||||||
|
import {ComponentChildren} from "preact";
|
||||||
|
import attributes from "../../services/attributes";
|
||||||
|
import clsx from "clsx";
|
||||||
|
import Color, { ColorInstance } from "color";
|
||||||
|
import Debouncer from "../../utils/debouncer";
|
||||||
|
import FNote from "../../entities/fnote";
|
||||||
|
import froca from "../../services/froca";
|
||||||
|
|
||||||
|
const COLOR_PALETTE = [
|
||||||
|
"#e64d4d", "#e6994d", "#e5e64d", "#99e64d", "#4de64d", "#4de699",
|
||||||
|
"#4de5e6", "#4d99e6", "#4d4de6", "#994de6", "#e64db3"
|
||||||
|
];
|
||||||
|
|
||||||
|
export interface NoteColorPickerProps {
|
||||||
|
/** The target Note instance or its ID string. */
|
||||||
|
note: FNote | string | null;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function NoteColorPicker(props: NoteColorPickerProps) {
|
||||||
|
if (!props.note) return null;
|
||||||
|
|
||||||
|
const [note, setNote] = useState<FNote | null>(null);
|
||||||
|
const [currentColor, setCurrentColor] = useState<string | null>(null);
|
||||||
|
const [isCustomColor, setIsCustomColor] = useState<boolean>(false);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const retrieveNote = async (noteId: string) => {
|
||||||
|
const noteInstance = await froca.getNote(noteId, true);
|
||||||
|
if (noteInstance) {
|
||||||
|
setNote(noteInstance);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (typeof props.note === "string") {
|
||||||
|
retrieveNote(props.note); // Get the note from the given ID string
|
||||||
|
} else {
|
||||||
|
setNote(props.note);
|
||||||
|
}
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const colorLabel = note?.getLabel("color")?.value ?? null;
|
||||||
|
if (colorLabel) {
|
||||||
|
let color = tryParseColor(colorLabel);
|
||||||
|
if (color) {
|
||||||
|
setCurrentColor(color.hex().toLowerCase());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}, [note]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
setIsCustomColor(currentColor !== null && COLOR_PALETTE.indexOf(currentColor) === -1);
|
||||||
|
}, [currentColor])
|
||||||
|
|
||||||
|
const onColorCellClicked = useCallback((color: string | null) => {
|
||||||
|
if (note) {
|
||||||
|
if (color !== null) {
|
||||||
|
attributes.setLabel(note.noteId, "color", color);
|
||||||
|
} else {
|
||||||
|
attributes.removeOwnedLabelByName(note, "color");
|
||||||
|
}
|
||||||
|
|
||||||
|
setCurrentColor(color);
|
||||||
|
}
|
||||||
|
}, [note, currentColor]);
|
||||||
|
|
||||||
|
return <div className="note-color-picker">
|
||||||
|
|
||||||
|
<ColorCell className="color-cell-reset"
|
||||||
|
tooltip={t("note-color.clear-color")}
|
||||||
|
color={null}
|
||||||
|
isSelected={(currentColor === null)}
|
||||||
|
isDisabled={(note === null)}
|
||||||
|
onSelect={onColorCellClicked}>
|
||||||
|
|
||||||
|
{/* https://pictogrammers.com/library/mdi/icon/close/ */}
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24">
|
||||||
|
<path d="M19,6.41L17.59,5L12,10.59L6.41,5L5,6.41L10.59,12L5,17.59L6.41,19L12,13.41L17.59,19L19,17.59L13.41,12L19,6.41Z" />
|
||||||
|
</svg>
|
||||||
|
</ColorCell>
|
||||||
|
|
||||||
|
|
||||||
|
{COLOR_PALETTE.map((color) => (
|
||||||
|
<ColorCell key={color}
|
||||||
|
tooltip={t("note-color.set-color")}
|
||||||
|
color={color}
|
||||||
|
isSelected={(color === currentColor)}
|
||||||
|
isDisabled={(note === null)}
|
||||||
|
onSelect={onColorCellClicked} />
|
||||||
|
))}
|
||||||
|
|
||||||
|
<CustomColorCell tooltip={t("note-color.set-custom-color")}
|
||||||
|
color={currentColor}
|
||||||
|
isSelected={isCustomColor}
|
||||||
|
isDisabled={(note === null)}
|
||||||
|
onSelect={onColorCellClicked} />
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
|
||||||
|
interface ColorCellProps {
|
||||||
|
children?: ComponentChildren,
|
||||||
|
className?: string,
|
||||||
|
tooltip?: string,
|
||||||
|
color: string | null,
|
||||||
|
isSelected: boolean,
|
||||||
|
isDisabled?: boolean,
|
||||||
|
onSelect?: (color: string | null) => void
|
||||||
|
}
|
||||||
|
|
||||||
|
function ColorCell(props: ColorCellProps) {
|
||||||
|
return <div className={clsx(props.className, {
|
||||||
|
"color-cell": true,
|
||||||
|
"selected": props.isSelected,
|
||||||
|
"disabled-color-cell": props.isDisabled
|
||||||
|
})}
|
||||||
|
style={`${(props.color !== null) ? `--color: ${props.color}` : ""}`}
|
||||||
|
title={props.tooltip}
|
||||||
|
onClick={() => props.onSelect?.(props.color)}>
|
||||||
|
{props.children}
|
||||||
|
</div>;
|
||||||
|
}
|
||||||
|
|
||||||
|
function CustomColorCell(props: ColorCellProps) {
|
||||||
|
const [pickedColor, setPickedColor] = useState<string | null>(null);
|
||||||
|
const colorInput = useRef<HTMLInputElement>(null);
|
||||||
|
const colorInputDebouncer = useRef<Debouncer<string | null> | null>(null);
|
||||||
|
const callbackRef = useRef(props.onSelect);
|
||||||
|
const isSafari = useRef(/^((?!chrome|android).)*safari/i.test(navigator.userAgent));
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
colorInputDebouncer.current = new Debouncer(250, (color) => {
|
||||||
|
callbackRef.current?.(color);
|
||||||
|
setPickedColor(color);
|
||||||
|
});
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
colorInputDebouncer.current?.destroy();
|
||||||
|
}
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (props.isSelected && pickedColor === null) {
|
||||||
|
setPickedColor(props.color);
|
||||||
|
}
|
||||||
|
}, [props.isSelected])
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
callbackRef.current = props.onSelect;
|
||||||
|
}, [props.onSelect]);
|
||||||
|
|
||||||
|
const onSelect = useCallback(() => {
|
||||||
|
if (pickedColor !== null) {
|
||||||
|
callbackRef.current?.(pickedColor);
|
||||||
|
}
|
||||||
|
|
||||||
|
colorInput.current?.click();
|
||||||
|
}, [pickedColor]);
|
||||||
|
|
||||||
|
return <div style={`--foreground: ${getForegroundColor(props.color)};`}
|
||||||
|
onClick={(e) => {
|
||||||
|
// The color picker dropdown will close on Safari if the parent context menu is
|
||||||
|
// dismissed, so stop the click propagation to prevent dismissing the menu.
|
||||||
|
isSafari.current && e.stopPropagation();
|
||||||
|
}}>
|
||||||
|
<ColorCell {...props}
|
||||||
|
color={pickedColor}
|
||||||
|
className={clsx("custom-color-cell", {
|
||||||
|
"custom-color-cell-empty": (pickedColor === null)
|
||||||
|
})}
|
||||||
|
onSelect={onSelect}>
|
||||||
|
|
||||||
|
<input ref={colorInput}
|
||||||
|
type="color"
|
||||||
|
value={pickedColor ?? props.color ?? "#40bfbf"}
|
||||||
|
onChange={() => {colorInputDebouncer.current?.updateValue(colorInput.current?.value ?? null)}}
|
||||||
|
style="width: 0; height: 0; opacity: 0" />
|
||||||
|
</ColorCell>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
|
||||||
|
function getForegroundColor(backgroundColor: string | null) {
|
||||||
|
if (backgroundColor === null) return "inherit";
|
||||||
|
|
||||||
|
const colorHsl = tryParseColor(backgroundColor)?.hsl();
|
||||||
|
if (colorHsl) {
|
||||||
|
let l = colorHsl.lightness();
|
||||||
|
return colorHsl.saturationl(0).lightness(l >= 50 ? 0 : 100).hex();
|
||||||
|
} else {
|
||||||
|
return "inherit";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function tryParseColor(colorStr: string): ColorInstance | null {
|
||||||
|
try {
|
||||||
|
return new Color(colorStr);
|
||||||
|
} catch(ex) {
|
||||||
|
console.error(ex);
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
BIN
apps/client/src/menus/custom-items/custom-color.png
Normal file
BIN
apps/client/src/menus/custom-items/custom-color.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 1.2 KiB |
@ -1,3 +1,4 @@
|
|||||||
|
import NoteColorPicker from "./custom-items/NoteColorPicker.jsx";
|
||||||
import treeService from "../services/tree.js";
|
import treeService from "../services/tree.js";
|
||||||
import froca from "../services/froca.js";
|
import froca from "../services/froca.js";
|
||||||
import clipboard from "../services/clipboard.js";
|
import clipboard from "../services/clipboard.js";
|
||||||
@ -243,6 +244,19 @@ export default class TreeContextMenu implements SelectMenuItemEventListener<Tree
|
|||||||
|
|
||||||
{ kind: "separator"},
|
{ kind: "separator"},
|
||||||
|
|
||||||
|
{
|
||||||
|
kind: "custom",
|
||||||
|
componentFn: () => {
|
||||||
|
if (notOptionsOrHelp && selectedNotes.length === 1) {
|
||||||
|
return NoteColorPicker({note});
|
||||||
|
} else {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
{ kind: "separator" },
|
||||||
|
|
||||||
{ title: t("tree-context-menu.import-into-note"), command: "importIntoNote", uiIcon: "bx bx-import", enabled: notSearch && noSelectedNotes && notOptionsOrHelp },
|
{ title: t("tree-context-menu.import-into-note"), command: "importIntoNote", uiIcon: "bx bx-import", enabled: notSearch && noSelectedNotes && notOptionsOrHelp },
|
||||||
|
|
||||||
{ title: t("tree-context-menu.export"), command: "exportNote", uiIcon: "bx bx-export", enabled: notSearch && noSelectedNotes && notOptionsOrHelp },
|
{ title: t("tree-context-menu.export"), command: "exportNote", uiIcon: "bx bx-export", enabled: notSearch && noSelectedNotes && notOptionsOrHelp },
|
||||||
|
|||||||
@ -494,6 +494,10 @@ body #context-menu-container .dropdown-item > span {
|
|||||||
width: 100%;
|
width: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.dropdown-menu .note-color-picker {
|
||||||
|
padding: 4px 12px 8px 12px;
|
||||||
|
}
|
||||||
|
|
||||||
.cm-editor {
|
.cm-editor {
|
||||||
height: 100%;
|
height: 100%;
|
||||||
outline: none !important;
|
outline: none !important;
|
||||||
|
|||||||
@ -347,6 +347,12 @@ li.dropdown-item a.dropdown-item-button:focus-visible {
|
|||||||
outline: 2px solid var(--input-focus-outline-color) !important;
|
outline: 2px solid var(--input-focus-outline-color) !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
:root .dropdown-menu .note-color-picker {
|
||||||
|
padding: 4px 10px;
|
||||||
|
--note-color-picker-clear-color-cell-background: var(--main-text-color);
|
||||||
|
--note-color-picker-clear-color-cell-selection-outline-color: var(--main-text-color);
|
||||||
|
}
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* TOASTS
|
* TOASTS
|
||||||
*/
|
*/
|
||||||
|
|||||||
@ -2093,5 +2093,10 @@
|
|||||||
},
|
},
|
||||||
"collections": {
|
"collections": {
|
||||||
"rendering_error": "Unable to show content due to an error."
|
"rendering_error": "Unable to show content due to an error."
|
||||||
|
},
|
||||||
|
"note-color": {
|
||||||
|
"clear-color": "Clear note color",
|
||||||
|
"set-color": "Set note color",
|
||||||
|
"set-custom-color": "Set custom note color"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -2095,5 +2095,10 @@
|
|||||||
},
|
},
|
||||||
"calendar_view": {
|
"calendar_view": {
|
||||||
"delete_note": "Șterge notița..."
|
"delete_note": "Șterge notița..."
|
||||||
|
},
|
||||||
|
"note-color": {
|
||||||
|
"clear-color": "Înlăturați culoarea notiței",
|
||||||
|
"set-color": "Setați culoarea notiței",
|
||||||
|
"set-custom-color": "Setați culoare personalizată pentru notiță"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
35
apps/client/src/utils/debouncer.ts
Normal file
35
apps/client/src/utils/debouncer.ts
Normal file
@ -0,0 +1,35 @@
|
|||||||
|
export type DebouncerCallback<T> = (value: T) => void;
|
||||||
|
|
||||||
|
export default class Debouncer<T> {
|
||||||
|
|
||||||
|
private debounceInterval: number;
|
||||||
|
private callback: DebouncerCallback<T>;
|
||||||
|
private lastValue: T | undefined;
|
||||||
|
private timeoutId: any | null = null;
|
||||||
|
|
||||||
|
constructor(debounceInterval: number, onUpdate: DebouncerCallback<T>) {
|
||||||
|
this.debounceInterval = debounceInterval;
|
||||||
|
this.callback = onUpdate;
|
||||||
|
}
|
||||||
|
|
||||||
|
updateValue(value: T) {
|
||||||
|
this.lastValue = value;
|
||||||
|
if (this.timeoutId !== null) {
|
||||||
|
clearTimeout(this.timeoutId);
|
||||||
|
}
|
||||||
|
this.timeoutId = setTimeout(this.reportUpdate.bind(this), this.debounceInterval);
|
||||||
|
}
|
||||||
|
|
||||||
|
destroy() {
|
||||||
|
if (this.timeoutId !== null) {
|
||||||
|
this.reportUpdate();
|
||||||
|
clearTimeout(this.timeoutId);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private reportUpdate() {
|
||||||
|
if (this.lastValue !== undefined) {
|
||||||
|
this.callback(this.lastValue);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -1,4 +1,5 @@
|
|||||||
import FNote from "../../../entities/fnote";
|
import FNote from "../../../entities/fnote";
|
||||||
|
import NoteColorPicker from "../../../menus/custom-items/NoteColorPicker";
|
||||||
import contextMenu, { ContextMenuEvent } from "../../../menus/context_menu";
|
import contextMenu, { ContextMenuEvent } from "../../../menus/context_menu";
|
||||||
import link_context_menu from "../../../menus/link_context_menu";
|
import link_context_menu from "../../../menus/link_context_menu";
|
||||||
import attributes from "../../../services/attributes";
|
import attributes from "../../../services/attributes";
|
||||||
@ -74,6 +75,11 @@ export function openNoteContextMenu(api: Api, event: ContextMenuEvent, note: FNo
|
|||||||
uiIcon: "bx bx-trash",
|
uiIcon: "bx bx-trash",
|
||||||
handler: () => branches.deleteNotes([ branchId ], false, false)
|
handler: () => branches.deleteNotes([ branchId ], false, false)
|
||||||
},
|
},
|
||||||
|
{ kind: "separator" },
|
||||||
|
{
|
||||||
|
kind: "custom",
|
||||||
|
componentFn: () => NoteColorPicker({note})
|
||||||
|
}
|
||||||
],
|
],
|
||||||
selectMenuItemHandler: ({ command }) => link_context_menu.handleLinkContextMenuItem(command, note.noteId),
|
selectMenuItemHandler: ({ command }) => link_context_menu.handleLinkContextMenuItem(command, note.noteId),
|
||||||
});
|
});
|
||||||
|
|||||||
@ -1,8 +1,10 @@
|
|||||||
|
import NoteColorPicker from "../../../menus/custom-items/NoteColorPicker";
|
||||||
import FNote from "../../../entities/fnote";
|
import FNote from "../../../entities/fnote";
|
||||||
import contextMenu, { ContextMenuEvent } from "../../../menus/context_menu";
|
import contextMenu, { ContextMenuEvent } from "../../../menus/context_menu";
|
||||||
import link_context_menu from "../../../menus/link_context_menu";
|
import link_context_menu from "../../../menus/link_context_menu";
|
||||||
import branches from "../../../services/branches";
|
import branches from "../../../services/branches";
|
||||||
import froca from "../../../services/froca";
|
import froca from "../../../services/froca";
|
||||||
|
import { note } from "mermaid/dist/rendering-util/rendering-elements/shapes/note.js";
|
||||||
import { t } from "../../../services/i18n";
|
import { t } from "../../../services/i18n";
|
||||||
|
|
||||||
export function openCalendarContextMenu(e: ContextMenuEvent, noteId: string, parentNote: FNote) {
|
export function openCalendarContextMenu(e: ContextMenuEvent, noteId: string, parentNote: FNote) {
|
||||||
@ -34,6 +36,11 @@ export function openCalendarContextMenu(e: ContextMenuEvent, noteId: string, par
|
|||||||
await branches.deleteNotes([ branchIdToDelete ], false, false);
|
await branches.deleteNotes([ branchIdToDelete ], false, false);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
{ kind: "separator" },
|
||||||
|
{
|
||||||
|
kind: "custom",
|
||||||
|
componentFn: () => NoteColorPicker({note: noteId})
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
selectMenuItemHandler: ({ command }) => link_context_menu.handleLinkContextMenuItem(command, noteId),
|
selectMenuItemHandler: ({ command }) => link_context_menu.handleLinkContextMenuItem(command, noteId),
|
||||||
|
|||||||
@ -2,6 +2,7 @@ import type { LatLng, LeafletMouseEvent } from "leaflet";
|
|||||||
import appContext, { type CommandMappings } from "../../../components/app_context.js";
|
import appContext, { type CommandMappings } from "../../../components/app_context.js";
|
||||||
import contextMenu, { type MenuItem } from "../../../menus/context_menu.js";
|
import contextMenu, { type MenuItem } from "../../../menus/context_menu.js";
|
||||||
import linkContextMenu from "../../../menus/link_context_menu.js";
|
import linkContextMenu from "../../../menus/link_context_menu.js";
|
||||||
|
import NoteColorPicker from "../../../menus/custom-items/NoteColorPicker.jsx";
|
||||||
import { t } from "../../../services/i18n.js";
|
import { t } from "../../../services/i18n.js";
|
||||||
import { createNewNote } from "./api.js";
|
import { createNewNote } from "./api.js";
|
||||||
import { copyTextWithToast } from "../../../services/clipboard_ext.js";
|
import { copyTextWithToast } from "../../../services/clipboard_ext.js";
|
||||||
@ -18,7 +19,12 @@ export default function openContextMenu(noteId: string, e: LeafletMouseEvent, is
|
|||||||
items = [
|
items = [
|
||||||
...items,
|
...items,
|
||||||
{ kind: "separator" },
|
{ kind: "separator" },
|
||||||
{ title: t("geo-map-context.remove-from-map"), command: "deleteFromMap", uiIcon: "bx bx-trash" }
|
{ title: t("geo-map-context.remove-from-map"), command: "deleteFromMap", uiIcon: "bx bx-trash" },
|
||||||
|
{ kind: "separator"},
|
||||||
|
{
|
||||||
|
kind: "custom",
|
||||||
|
componentFn: () => NoteColorPicker({note: noteId})
|
||||||
|
}
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -7,6 +7,7 @@ import link_context_menu from "../../../menus/link_context_menu.js";
|
|||||||
import froca from "../../../services/froca.js";
|
import froca from "../../../services/froca.js";
|
||||||
import branches from "../../../services/branches.js";
|
import branches from "../../../services/branches.js";
|
||||||
import Component from "../../../components/component.js";
|
import Component from "../../../components/component.js";
|
||||||
|
import NoteColorPicker from "../../../menus/custom-items/NoteColorPicker.jsx";
|
||||||
import { RefObject } from "preact";
|
import { RefObject } from "preact";
|
||||||
|
|
||||||
export function useContextMenu(parentNote: FNote, parentComponent: Component | null | undefined, tabulator: RefObject<Tabulator>): Partial<EventCallBackMethods> {
|
export function useContextMenu(parentNote: FNote, parentComponent: Component | null | undefined, tabulator: RefObject<Tabulator>): Partial<EventCallBackMethods> {
|
||||||
@ -219,6 +220,11 @@ export function showRowContextMenu(parentComponent: Component, e: MouseEvent, ro
|
|||||||
title: t("table_context_menu.delete_row"),
|
title: t("table_context_menu.delete_row"),
|
||||||
uiIcon: "bx bx-trash",
|
uiIcon: "bx bx-trash",
|
||||||
handler: () => branches.deleteNotes([ rowData.branchId ], false, false)
|
handler: () => branches.deleteNotes([ rowData.branchId ], false, false)
|
||||||
|
},
|
||||||
|
{ kind: "separator"},
|
||||||
|
{
|
||||||
|
kind: "custom",
|
||||||
|
componentFn: () => NoteColorPicker({note: rowData.noteId})
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
selectMenuItemHandler: ({ command }) => link_context_menu.handleLinkContextMenuItem(command, rowData.noteId),
|
selectMenuItemHandler: ({ command }) => link_context_menu.handleLinkContextMenuItem(command, rowData.noteId),
|
||||||
|
|||||||
12
pnpm-lock.yaml
generated
12
pnpm-lock.yaml
generated
@ -220,6 +220,9 @@ importers:
|
|||||||
boxicons:
|
boxicons:
|
||||||
specifier: 2.1.4
|
specifier: 2.1.4
|
||||||
version: 2.1.4
|
version: 2.1.4
|
||||||
|
clsx:
|
||||||
|
specifier: 2.1.1
|
||||||
|
version: 2.1.1
|
||||||
color:
|
color:
|
||||||
specifier: 5.0.3
|
specifier: 5.0.3
|
||||||
version: 5.0.3
|
version: 5.0.3
|
||||||
@ -4749,6 +4752,11 @@ packages:
|
|||||||
resolution: {integrity: sha512-prmU+rDddxHOH0oNcwemL+SwnzcG65sBF2yXRO7aeXIn/xTlq2pX7JLVbkBnVLowHLg4/OL4+jBmv9hVrVGS+w==}
|
resolution: {integrity: sha512-prmU+rDddxHOH0oNcwemL+SwnzcG65sBF2yXRO7aeXIn/xTlq2pX7JLVbkBnVLowHLg4/OL4+jBmv9hVrVGS+w==}
|
||||||
engines: {node: '>=18.0.0'}
|
engines: {node: '>=18.0.0'}
|
||||||
|
|
||||||
|
'@smithy/core@3.18.3':
|
||||||
|
resolution: {integrity: sha512-qqpNskkbHOSfrbFbjhYj5o8VMXO26fvN1K/+HbCzUNlTuxgNcPRouUDNm+7D6CkN244WG7aK533Ne18UtJEgAA==}
|
||||||
|
engines: {node: '>=18.0.0'}
|
||||||
|
deprecated: Please upgrade your lockfile to use the latest 3.x version of @smithy/core for various fixes, see https://github.com/smithy-lang/smithy-typescript/blob/main/packages/core/CHANGELOG.md
|
||||||
|
|
||||||
'@smithy/core@3.18.4':
|
'@smithy/core@3.18.4':
|
||||||
resolution: {integrity: sha512-o5tMqPZILBvvROfC8vC+dSVnWJl9a0u9ax1i1+Bq8515eYjUJqqk5XjjEsDLoeL5dSqGSh6WGdVx1eJ1E/Nwhw==}
|
resolution: {integrity: sha512-o5tMqPZILBvvROfC8vC+dSVnWJl9a0u9ax1i1+Bq8515eYjUJqqk5XjjEsDLoeL5dSqGSh6WGdVx1eJ1E/Nwhw==}
|
||||||
engines: {node: '>=18.0.0'}
|
engines: {node: '>=18.0.0'}
|
||||||
@ -15713,6 +15721,8 @@ snapshots:
|
|||||||
'@ckeditor/ckeditor5-core': 47.2.0
|
'@ckeditor/ckeditor5-core': 47.2.0
|
||||||
'@ckeditor/ckeditor5-utils': 47.2.0
|
'@ckeditor/ckeditor5-utils': 47.2.0
|
||||||
ckeditor5: 47.2.0(patch_hash=8331a09d41443b39ea1c784daaccfeb0da4f9065ed556e7de92e9c77edd9eb41)
|
ckeditor5: 47.2.0(patch_hash=8331a09d41443b39ea1c784daaccfeb0da4f9065ed556e7de92e9c77edd9eb41)
|
||||||
|
transitivePeerDependencies:
|
||||||
|
- supports-color
|
||||||
|
|
||||||
'@ckeditor/ckeditor5-code-block@47.2.0(patch_hash=2361d8caad7d6b5bddacc3a3b4aa37dbfba260b1c1b22a450413a79c1bb1ce95)':
|
'@ckeditor/ckeditor5-code-block@47.2.0(patch_hash=2361d8caad7d6b5bddacc3a3b4aa37dbfba260b1c1b22a450413a79c1bb1ce95)':
|
||||||
dependencies:
|
dependencies:
|
||||||
@ -15777,8 +15787,6 @@ snapshots:
|
|||||||
'@ckeditor/ckeditor5-utils': 47.2.0
|
'@ckeditor/ckeditor5-utils': 47.2.0
|
||||||
'@ckeditor/ckeditor5-watchdog': 47.2.0
|
'@ckeditor/ckeditor5-watchdog': 47.2.0
|
||||||
es-toolkit: 1.39.5
|
es-toolkit: 1.39.5
|
||||||
transitivePeerDependencies:
|
|
||||||
- supports-color
|
|
||||||
|
|
||||||
'@ckeditor/ckeditor5-dev-build-tools@43.1.0(@swc/helpers@0.5.17)(tslib@2.8.1)(typescript@5.9.3)':
|
'@ckeditor/ckeditor5-dev-build-tools@43.1.0(@swc/helpers@0.5.17)(tslib@2.8.1)(typescript@5.9.3)':
|
||||||
dependencies:
|
dependencies:
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user