From afe3904ea3647010b66415fe778082a57746d4d2 Mon Sep 17 00:00:00 2001 From: Elian Doran Date: Wed, 20 Aug 2025 21:50:06 +0300 Subject: [PATCH 001/191] feat(react): render raw react components --- apps/client/src/desktop.ts | 1 - .../{layout_commons.ts => layout_commons.tsx} | 4 +- apps/client/src/widgets/basic_widget.ts | 36 +++++++++++++++- .../widgets/{note_title.ts => note_title.bak} | 0 apps/client/src/widgets/note_title.tsx | 7 ++++ apps/client/src/widgets/react/Modal.tsx | 2 +- .../src/widgets/react/ReactBasicWidget.tsx | 31 +------------- apps/client/src/widgets/react/hooks.tsx | 2 +- apps/client/src/widgets/react/react_utils.ts | 15 ------- apps/client/src/widgets/react/react_utils.tsx | 42 +++++++++++++++++++ .../ribbon_widgets/search_definition.ts | 2 +- .../client/src/widgets/type_widgets/canvas.ts | 2 +- .../widgets/type_widgets/content_widget.tsx | 2 +- 13 files changed, 92 insertions(+), 54 deletions(-) rename apps/client/src/layouts/{layout_commons.ts => layout_commons.tsx} (97%) rename apps/client/src/widgets/{note_title.ts => note_title.bak} (100%) create mode 100644 apps/client/src/widgets/note_title.tsx delete mode 100644 apps/client/src/widgets/react/react_utils.ts create mode 100644 apps/client/src/widgets/react/react_utils.tsx diff --git a/apps/client/src/desktop.ts b/apps/client/src/desktop.ts index 2791f0577..57ff4084e 100644 --- a/apps/client/src/desktop.ts +++ b/apps/client/src/desktop.ts @@ -8,7 +8,6 @@ import electronContextMenu from "./menus/electron_context_menu.js"; import glob from "./services/glob.js"; import { t } from "./services/i18n.js"; import options from "./services/options.js"; -import server from "./services/server.js"; import type ElectronRemote from "@electron/remote"; import type Electron from "electron"; import "./stylesheets/bootstrap.scss"; diff --git a/apps/client/src/layouts/layout_commons.ts b/apps/client/src/layouts/layout_commons.tsx similarity index 97% rename from apps/client/src/layouts/layout_commons.ts rename to apps/client/src/layouts/layout_commons.tsx index 5ee261317..e3be51a3a 100644 --- a/apps/client/src/layouts/layout_commons.ts +++ b/apps/client/src/layouts/layout_commons.tsx @@ -25,12 +25,12 @@ import IncorrectCpuArchDialog from "../widgets/dialogs/incorrect_cpu_arch.js"; import PopupEditorDialog from "../widgets/dialogs/popup_editor.js"; import FlexContainer from "../widgets/containers/flex_container.js"; import NoteIconWidget from "../widgets/note_icon.js"; -import NoteTitleWidget from "../widgets/note_title.js"; import ClassicEditorToolbar from "../widgets/ribbon_widgets/classic_editor_toolbar.js"; import PromotedAttributesWidget from "../widgets/ribbon_widgets/promoted_attributes.js"; import NoteDetailWidget from "../widgets/note_detail.js"; import NoteListWidget from "../widgets/note_list.js"; import { CallToActionDialog } from "../widgets/dialogs/call_to_action.jsx"; +import NoteTitleWidget from "../widgets/note_title.jsx"; export function applyModals(rootContainer: RootContainer) { rootContainer @@ -62,7 +62,7 @@ export function applyModals(rootContainer: RootContainer) { .css("align-items", "center") .cssBlock(".title-row > * { margin: 5px; }") .child(new NoteIconWidget()) - .child(new NoteTitleWidget())) + .child()) .child(new ClassicEditorToolbar()) .child(new PromotedAttributesWidget()) .child(new NoteDetailWidget()) diff --git a/apps/client/src/widgets/basic_widget.ts b/apps/client/src/widgets/basic_widget.ts index be7b0bbd7..7b5c8e29c 100644 --- a/apps/client/src/widgets/basic_widget.ts +++ b/apps/client/src/widgets/basic_widget.ts @@ -1,7 +1,9 @@ +import { isValidElement, VNode } from "preact"; import Component, { TypedComponent } from "../components/component.js"; import froca from "../services/froca.js"; import { t } from "../services/i18n.js"; import toastService from "../services/toast.js"; +import { renderReactWidget } from "./react/react_utils.jsx"; export class TypedBasicWidget> extends TypedComponent { protected attrs: Record; @@ -22,11 +24,14 @@ export class TypedBasicWidget> extends TypedCompon this.childPositionCounter = 10; } - child(...components: T[]) { - if (!components) { + child(..._components: (T | VNode)[]) { + if (!_components) { return this; } + // Convert any React components to legacy wrapped components. + const components = wrapReactWidgets(_components); + super.child(...components); for (const component of components) { @@ -258,3 +263,30 @@ export class TypedBasicWidget> extends TypedCompon * For information on using widgets, see the tutorial {@tutorial widget_basics}. */ export default class BasicWidget extends TypedBasicWidget {} + +export function wrapReactWidgets>(components: (T | VNode)[]) { + const wrappedResult: T[] = []; + for (const component of components) { + if (isValidElement(component)) { + wrappedResult.push(new ReactWrappedWidget(component) as unknown as T); + } else { + wrappedResult.push(component); + } + } + return wrappedResult; +} + +class ReactWrappedWidget extends BasicWidget { + + private el: VNode; + + constructor(el: VNode) { + super(); + this.el = el; + } + + doRender() { + this.$widget = renderReactWidget(this, this.el); + } + +} diff --git a/apps/client/src/widgets/note_title.ts b/apps/client/src/widgets/note_title.bak similarity index 100% rename from apps/client/src/widgets/note_title.ts rename to apps/client/src/widgets/note_title.bak diff --git a/apps/client/src/widgets/note_title.tsx b/apps/client/src/widgets/note_title.tsx new file mode 100644 index 000000000..b1409d5e8 --- /dev/null +++ b/apps/client/src/widgets/note_title.tsx @@ -0,0 +1,7 @@ +export default function NoteTitleWidget() { + return ( + <> +

Hi

+ + ); +} diff --git a/apps/client/src/widgets/react/Modal.tsx b/apps/client/src/widgets/react/Modal.tsx index 362d3ab38..02aeedd9b 100644 --- a/apps/client/src/widgets/react/Modal.tsx +++ b/apps/client/src/widgets/react/Modal.tsx @@ -3,7 +3,7 @@ import { t } from "../../services/i18n"; import { ComponentChildren } from "preact"; import type { CSSProperties, RefObject } from "preact/compat"; import { openDialog } from "../../services/dialog"; -import { ParentComponent } from "./ReactBasicWidget"; +import { ParentComponent } from "./react_utils"; import { Modal as BootstrapModal } from "bootstrap"; import { memo } from "preact/compat"; diff --git a/apps/client/src/widgets/react/ReactBasicWidget.tsx b/apps/client/src/widgets/react/ReactBasicWidget.tsx index b813be3bd..744d06736 100644 --- a/apps/client/src/widgets/react/ReactBasicWidget.tsx +++ b/apps/client/src/widgets/react/ReactBasicWidget.tsx @@ -1,9 +1,6 @@ -import { createContext, JSX, render } from "preact"; +import { JSX } from "preact"; import BasicWidget from "../basic_widget.js"; -import Component from "../../components/component.js"; - -export const ParentComponent = createContext(null); - +import { renderReactWidget } from "./react_utils.jsx"; export default abstract class ReactBasicWidget extends BasicWidget { abstract get component(): JSX.Element; @@ -13,27 +10,3 @@ export default abstract class ReactBasicWidget extends BasicWidget { } } - -/** - * Renders a React component and returns the corresponding DOM element wrapped in JQuery. - * - * @param parentComponent the parent Trilium component for the component to be able to handle events. - * @param el the JSX element to render. - * @returns the rendered wrapped DOM element. - */ -export function renderReactWidget(parentComponent: Component, el: JSX.Element) { - return renderReactWidgetAtElement(parentComponent, el, new DocumentFragment()).children(); -} - -export function renderReactWidgetAtElement(parentComponent: Component, el: JSX.Element, container: Element | DocumentFragment) { - render(( - - {el} - - ), container); - return $(container) as JQuery; -} - -export function disposeReactWidget(container: Element) { - render(null, container); -} \ No newline at end of file diff --git a/apps/client/src/widgets/react/hooks.tsx b/apps/client/src/widgets/react/hooks.tsx index d254b930c..adf6a8576 100644 --- a/apps/client/src/widgets/react/hooks.tsx +++ b/apps/client/src/widgets/react/hooks.tsx @@ -1,6 +1,6 @@ import { useCallback, useContext, useEffect, useMemo, useRef, useState } from "preact/hooks"; import { EventData, EventNames } from "../../components/app_context"; -import { ParentComponent } from "./ReactBasicWidget"; +import { ParentComponent } from "./react_utils"; import SpacedUpdate from "../../services/spaced_update"; import { OptionNames } from "@triliumnext/commons"; import options, { type OptionValue } from "../../services/options"; diff --git a/apps/client/src/widgets/react/react_utils.ts b/apps/client/src/widgets/react/react_utils.ts deleted file mode 100644 index 14357e124..000000000 --- a/apps/client/src/widgets/react/react_utils.ts +++ /dev/null @@ -1,15 +0,0 @@ -import type { RefObject } from "preact"; - -/** - * Takes in a React ref and returns a corresponding JQuery selector. - * - * @param ref the React ref from which to obtain the jQuery selector. - * @returns the corresponding jQuery selector. - */ -export function refToJQuerySelector(ref: RefObject | null): JQuery { - if (ref?.current) { - return $(ref.current); - } else { - return $(); - } -} diff --git a/apps/client/src/widgets/react/react_utils.tsx b/apps/client/src/widgets/react/react_utils.tsx new file mode 100644 index 000000000..e8b0752b4 --- /dev/null +++ b/apps/client/src/widgets/react/react_utils.tsx @@ -0,0 +1,42 @@ +import { createContext, render, type JSX, type RefObject } from "preact"; +import Component from "../../components/component"; + +export const ParentComponent = createContext(null); + +/** + * Takes in a React ref and returns a corresponding JQuery selector. + * + * @param ref the React ref from which to obtain the jQuery selector. + * @returns the corresponding jQuery selector. + */ +export function refToJQuerySelector(ref: RefObject | null): JQuery { + if (ref?.current) { + return $(ref.current); + } else { + return $(); + } +} + +/** + * Renders a React component and returns the corresponding DOM element wrapped in JQuery. + * + * @param parentComponent the parent Trilium component for the component to be able to handle events. + * @param el the JSX element to render. + * @returns the rendered wrapped DOM element. + */ +export function renderReactWidget(parentComponent: Component, el: JSX.Element) { + return renderReactWidgetAtElement(parentComponent, el, new DocumentFragment()).children(); +} + +export function renderReactWidgetAtElement(parentComponent: Component, el: JSX.Element, container: Element | DocumentFragment) { + render(( + + {el} + + ), container); + return $(container) as JQuery; +} + +export function disposeReactWidget(container: Element) { + render(null, container); +} \ No newline at end of file diff --git a/apps/client/src/widgets/ribbon_widgets/search_definition.ts b/apps/client/src/widgets/ribbon_widgets/search_definition.ts index 2439b590f..99ac91c50 100644 --- a/apps/client/src/widgets/ribbon_widgets/search_definition.ts +++ b/apps/client/src/widgets/ribbon_widgets/search_definition.ts @@ -19,7 +19,7 @@ import bulkActionService from "../../services/bulk_action.js"; import { Dropdown } from "bootstrap"; import type FNote from "../../entities/fnote.js"; import type { AttributeType } from "../../entities/fattribute.js"; -import { renderReactWidget } from "../react/ReactBasicWidget.jsx"; +import { renderReactWidget } from "../react/react_utils.jsx"; const TPL = /*html*/`
diff --git a/apps/client/src/widgets/type_widgets/canvas.ts b/apps/client/src/widgets/type_widgets/canvas.ts index e81e8a36f..42314f055 100644 --- a/apps/client/src/widgets/type_widgets/canvas.ts +++ b/apps/client/src/widgets/type_widgets/canvas.ts @@ -6,7 +6,7 @@ import type { LibraryItem } from "@excalidraw/excalidraw/types"; import type { Theme } from "@excalidraw/excalidraw/element/types"; import type Canvas from "./canvas_el.js"; import { CanvasContent } from "./canvas_el.js"; -import { renderReactWidget } from "../react/ReactBasicWidget.jsx"; +import { renderReactWidget } from "../react/react_utils.jsx"; const TPL = /*html*/`
diff --git a/apps/client/src/widgets/type_widgets/content_widget.tsx b/apps/client/src/widgets/type_widgets/content_widget.tsx index be803018f..98175a0c8 100644 --- a/apps/client/src/widgets/type_widgets/content_widget.tsx +++ b/apps/client/src/widgets/type_widgets/content_widget.tsx @@ -5,7 +5,7 @@ import { t } from "../../services/i18n.js"; import type BasicWidget from "../basic_widget.js"; import type { JSX } from "preact/jsx-runtime"; import AppearanceSettings from "./options/appearance.jsx"; -import { disposeReactWidget, renderReactWidget, renderReactWidgetAtElement } from "../react/ReactBasicWidget.jsx"; +import { disposeReactWidget, renderReactWidgetAtElement } from "../react/react_utils.jsx"; import ImageSettings from "./options/images.jsx"; import AdvancedSettings from "./options/advanced.jsx"; import InternationalizationOptions from "./options/i18n.jsx"; From 59486cd55db5481a2285c2c0690d20499a49c7e8 Mon Sep 17 00:00:00 2001 From: Elian Doran Date: Wed, 20 Aug 2025 23:53:13 +0300 Subject: [PATCH 002/191] feat(react): basic handling of note context aware --- .../{desktop_layout.ts => desktop_layout.tsx} | 4 +- apps/client/src/widgets/basic_widget.ts | 10 ++++- apps/client/src/widgets/note_title.tsx | 6 ++- apps/client/src/widgets/react/hooks.tsx | 40 ++++++++++++++++++- 4 files changed, 55 insertions(+), 5 deletions(-) rename apps/client/src/layouts/{desktop_layout.ts => desktop_layout.tsx} (99%) diff --git a/apps/client/src/layouts/desktop_layout.ts b/apps/client/src/layouts/desktop_layout.tsx similarity index 99% rename from apps/client/src/layouts/desktop_layout.ts rename to apps/client/src/layouts/desktop_layout.tsx index 33c1135c8..85dcb531f 100644 --- a/apps/client/src/layouts/desktop_layout.ts +++ b/apps/client/src/layouts/desktop_layout.tsx @@ -4,7 +4,7 @@ import TabRowWidget from "../widgets/tab_row.js"; import TitleBarButtonsWidget from "../widgets/title_bar_buttons.js"; import LeftPaneContainer from "../widgets/containers/left_pane_container.js"; import NoteTreeWidget from "../widgets/note_tree.js"; -import NoteTitleWidget from "../widgets/note_title.js"; +import NoteTitleWidget from "../widgets/note_title.jsx"; import OwnedAttributeListWidget from "../widgets/ribbon_widgets/owned_attribute_list.js"; import NoteActionsWidget from "../widgets/buttons/note_actions.js"; import NoteDetailWidget from "../widgets/note_detail.js"; @@ -152,7 +152,7 @@ export default class DesktopLayout { .css("align-items", "center") .cssBlock(".title-row > * { margin: 5px; }") .child(new NoteIconWidget()) - .child(new NoteTitleWidget()) + .child() .child(new SpacerWidget(0, 1)) .child(new MovePaneButton(true)) .child(new MovePaneButton(false)) diff --git a/apps/client/src/widgets/basic_widget.ts b/apps/client/src/widgets/basic_widget.ts index 7b5c8e29c..22d1864fb 100644 --- a/apps/client/src/widgets/basic_widget.ts +++ b/apps/client/src/widgets/basic_widget.ts @@ -4,6 +4,7 @@ import froca from "../services/froca.js"; import { t } from "../services/i18n.js"; import toastService from "../services/toast.js"; import { renderReactWidget } from "./react/react_utils.jsx"; +import { EventNames, EventData } from "../components/app_context.js"; export class TypedBasicWidget> extends TypedComponent { protected attrs: Record; @@ -276,9 +277,10 @@ export function wrapReactWidgets>(components: (T | return wrappedResult; } -class ReactWrappedWidget extends BasicWidget { +export class ReactWrappedWidget extends BasicWidget { private el: VNode; + listeners: Record void> = {}; constructor(el: VNode) { super(); @@ -289,4 +291,10 @@ class ReactWrappedWidget extends BasicWidget { this.$widget = renderReactWidget(this, this.el); } + handleEvent(name: T, data: EventData): Promise | null | undefined { + const listener = this.listeners[name]; + console.log("Handle ", name, listener); + listener?.(data); + } + } diff --git a/apps/client/src/widgets/note_title.tsx b/apps/client/src/widgets/note_title.tsx index b1409d5e8..7fe057f28 100644 --- a/apps/client/src/widgets/note_title.tsx +++ b/apps/client/src/widgets/note_title.tsx @@ -1,7 +1,11 @@ +import { useNoteContext } from "./react/hooks"; + export default function NoteTitleWidget() { + const { ntxId, noteId, note } = useNoteContext(); + return ( <> -

Hi

+

{ ntxId }{ noteId }

); } diff --git a/apps/client/src/widgets/react/hooks.tsx b/apps/client/src/widgets/react/hooks.tsx index adf6a8576..925ac24f1 100644 --- a/apps/client/src/widgets/react/hooks.tsx +++ b/apps/client/src/widgets/react/hooks.tsx @@ -6,7 +6,8 @@ import { OptionNames } from "@triliumnext/commons"; import options, { type OptionValue } from "../../services/options"; import utils, { reloadFrontendApp } from "../../services/utils"; import Component from "../../components/component"; -import server from "../../services/server"; +import NoteContext from "../../components/note_context"; +import { ReactWrappedWidget } from "../basic_widget"; type TriliumEventHandler = (data: EventData) => void; const registeredHandlers: Map[]>> = new Map(); @@ -83,6 +84,11 @@ export default function useTriliumEvent(eventName: T, hand }, [ eventName, parentWidget, handler ]); } +export function useTriliumEventBeta(eventName: T, handler: TriliumEventHandler) { + const parentComponent = useContext(ParentComponent) as ReactWrappedWidget; + parentComponent.listeners[eventName] = handler; +} + export function useSpacedUpdate(callback: () => Promise, interval = 1000) { const callbackRef = useRef(callback); const spacedUpdateRef = useRef(); @@ -218,4 +224,36 @@ export function useTriliumOptions(...names: T[]) { */ export function useUniqueName(prefix?: string) { return useMemo(() => (prefix ? prefix + "-" : "") + utils.randomString(10), [ prefix ]); +} + +export function useNoteContext() { + + const [ noteContext, setNoteContext ] = useState(); + const [ notePath, setNotePath ] = useState(); + + useTriliumEvent("activeContextChanged", ({ noteContext }) => { + console.log("Active context changed."); + setNoteContext(noteContext); + }); + useTriliumEventBeta("setNoteContext", ({ noteContext }) => { + console.log("Set note context", noteContext, noteContext.noteId); + setNoteContext(noteContext); + }); + useTriliumEvent("noteSwitchedAndActivated", ({ noteContext }) => { + console.log("Note switched and activated") + setNoteContext(noteContext); + }); + useTriliumEvent("noteSwitched", ({ noteContext, notePath }) => { + console.warn("Note switched", notePath); + setNotePath(notePath); + }); + + return { + note: noteContext?.note, + noteId: noteContext?.note?.noteId, + notePath: noteContext?.notePath, + hoistedNoteId: noteContext?.hoistedNoteId, + ntxId: noteContext?.ntxId + }; + } \ No newline at end of file From 799e705ff8b861b805d4db867b1d93d01039b8b9 Mon Sep 17 00:00:00 2001 From: Elian Doran Date: Thu, 21 Aug 2025 09:17:47 +0300 Subject: [PATCH 003/191] fix(react): note context not always updated --- apps/client/src/widgets/basic_widget.ts | 1 - apps/client/src/widgets/react/hooks.tsx | 8 ++++---- 2 files changed, 4 insertions(+), 5 deletions(-) diff --git a/apps/client/src/widgets/basic_widget.ts b/apps/client/src/widgets/basic_widget.ts index 22d1864fb..3eb2e86f5 100644 --- a/apps/client/src/widgets/basic_widget.ts +++ b/apps/client/src/widgets/basic_widget.ts @@ -293,7 +293,6 @@ export class ReactWrappedWidget extends BasicWidget { handleEvent(name: T, data: EventData): Promise | null | undefined { const listener = this.listeners[name]; - console.log("Handle ", name, listener); listener?.(data); } diff --git a/apps/client/src/widgets/react/hooks.tsx b/apps/client/src/widgets/react/hooks.tsx index 925ac24f1..d43d25f61 100644 --- a/apps/client/src/widgets/react/hooks.tsx +++ b/apps/client/src/widgets/react/hooks.tsx @@ -231,19 +231,19 @@ export function useNoteContext() { const [ noteContext, setNoteContext ] = useState(); const [ notePath, setNotePath ] = useState(); - useTriliumEvent("activeContextChanged", ({ noteContext }) => { - console.log("Active context changed."); + useTriliumEventBeta("activeContextChanged", ({ noteContext }) => { setNoteContext(noteContext); + setNotePath(noteContext.notePath); }); useTriliumEventBeta("setNoteContext", ({ noteContext }) => { console.log("Set note context", noteContext, noteContext.noteId); setNoteContext(noteContext); }); - useTriliumEvent("noteSwitchedAndActivated", ({ noteContext }) => { + useTriliumEventBeta("noteSwitchedAndActivated", ({ noteContext }) => { console.log("Note switched and activated") setNoteContext(noteContext); }); - useTriliumEvent("noteSwitched", ({ noteContext, notePath }) => { + useTriliumEventBeta("noteSwitched", ({ noteContext, notePath }) => { console.warn("Note switched", notePath); setNotePath(notePath); }); From ca40360f7d9bc5464660e061107803c238df724e Mon Sep 17 00:00:00 2001 From: Elian Doran Date: Thu, 21 Aug 2025 10:05:53 +0300 Subject: [PATCH 004/191] feat(react): basic implementation of note title --- apps/client/src/widgets/basic_widget.ts | 39 +++++++++++++++++++++++-- apps/client/src/widgets/note_title.bak | 10 +------ apps/client/src/widgets/note_title.tsx | 33 ++++++++++++++++++--- apps/client/src/widgets/react/hooks.tsx | 9 ++++-- 4 files changed, 73 insertions(+), 18 deletions(-) diff --git a/apps/client/src/widgets/basic_widget.ts b/apps/client/src/widgets/basic_widget.ts index 3eb2e86f5..ff3ea276c 100644 --- a/apps/client/src/widgets/basic_widget.ts +++ b/apps/client/src/widgets/basic_widget.ts @@ -5,6 +5,7 @@ import { t } from "../services/i18n.js"; import toastService from "../services/toast.js"; import { renderReactWidget } from "./react/react_utils.jsx"; import { EventNames, EventData } from "../components/app_context.js"; +import { Handler } from "leaflet"; export class TypedBasicWidget> extends TypedComponent { protected attrs: Record; @@ -277,10 +278,12 @@ export function wrapReactWidgets>(components: (T | return wrappedResult; } +type EventHandler = ((data: any) => void); + export class ReactWrappedWidget extends BasicWidget { private el: VNode; - listeners: Record void> = {}; + private listeners: Record = {}; constructor(el: VNode) { super(); @@ -292,8 +295,38 @@ export class ReactWrappedWidget extends BasicWidget { } handleEvent(name: T, data: EventData): Promise | null | undefined { - const listener = this.listeners[name]; - listener?.(data); + if (!this.listeners[name]) { + return; + } + + for (const listener of this.listeners[name]) { + listener(data); + } + } + + registerHandler(name: T, handler: EventHandler) { + if (!this.listeners[name]) { + this.listeners[name] = []; + } + + if (this.listeners[name].includes(handler)) { + return; + } + + this.listeners[name].push(handler); + } + + removeHandler(name: T, handler: EventHandler) { + if (!this.listeners[name]?.includes(handler)) { + return; + } + + this.listeners[name] = this.listeners[name] + .filter(listener => listener !== handler); + + if (!this.listeners[name].length) { + delete this.listeners[name]; + } } } diff --git a/apps/client/src/widgets/note_title.bak b/apps/client/src/widgets/note_title.bak index ebe7ad809..e07b8380e 100644 --- a/apps/client/src/widgets/note_title.bak +++ b/apps/client/src/widgets/note_title.bak @@ -37,8 +37,6 @@ const TPL = /*html*/` text-shadow: 4px 4px 4px var(--muted-text-color); } - -
`; export default class NoteTitleWidget extends NoteContextAwareWidget { @@ -51,13 +49,7 @@ export default class NoteTitleWidget extends NoteContextAwareWidget { super(); this.spacedUpdate = new SpacedUpdate(async () => { - const title = this.$noteTitle.val(); - - if (this.note) { - protectedSessionHolder.touchProtectedSessionIfNecessary(this.note); - } - - await server.put(`notes/${this.noteId}/title`, { title }, this.componentId); + }); this.deleteNoteOnEscape = false; diff --git a/apps/client/src/widgets/note_title.tsx b/apps/client/src/widgets/note_title.tsx index 7fe057f28..d74779dfa 100644 --- a/apps/client/src/widgets/note_title.tsx +++ b/apps/client/src/widgets/note_title.tsx @@ -1,11 +1,36 @@ -import { useNoteContext } from "./react/hooks"; +import { useEffect, useRef, useState } from "preact/hooks"; +import { t } from "../services/i18n"; +import FormTextBox from "./react/FormTextBox"; +import { useNoteContext, useSpacedUpdate } from "./react/hooks"; +import protected_session_holder from "../services/protected_session_holder"; +import server from "../services/server"; export default function NoteTitleWidget() { - const { ntxId, noteId, note } = useNoteContext(); - + const { note, noteId, componentId } = useNoteContext(); + const [ title, setTitle ] = useState(note?.title); + useEffect(() => setTitle(note?.title), [ note?.title ]); + + const spacedUpdate = useSpacedUpdate(async () => { + if (!note) { + return; + } + protected_session_holder.touchProtectedSessionIfNecessary(note); + await server.put(`notes/${noteId}/title`, { title: title }, componentId); + }); + return ( <> -

{ ntxId }{ noteId }

+ { + setTitle(newValue); + spacedUpdate.scheduleUpdate(); + }} + /> ); } diff --git a/apps/client/src/widgets/react/hooks.tsx b/apps/client/src/widgets/react/hooks.tsx index d43d25f61..d592a48a1 100644 --- a/apps/client/src/widgets/react/hooks.tsx +++ b/apps/client/src/widgets/react/hooks.tsx @@ -86,7 +86,9 @@ export default function useTriliumEvent(eventName: T, hand export function useTriliumEventBeta(eventName: T, handler: TriliumEventHandler) { const parentComponent = useContext(ParentComponent) as ReactWrappedWidget; - parentComponent.listeners[eventName] = handler; + parentComponent.registerHandler(eventName, handler); + + return (() => parentComponent.removeHandler(eventName, handler)); } export function useSpacedUpdate(callback: () => Promise, interval = 1000) { @@ -247,13 +249,16 @@ export function useNoteContext() { console.warn("Note switched", notePath); setNotePath(notePath); }); + + const parentComponent = useContext(ParentComponent) as ReactWrappedWidget; return { note: noteContext?.note, noteId: noteContext?.note?.noteId, notePath: noteContext?.notePath, hoistedNoteId: noteContext?.hoistedNoteId, - ntxId: noteContext?.ntxId + ntxId: noteContext?.ntxId, + componentId: parentComponent.componentId }; } \ No newline at end of file From 9a4fdcaef2bad892ee301bd93ec4de395262656d Mon Sep 17 00:00:00 2001 From: Elian Doran Date: Thu, 21 Aug 2025 10:34:30 +0300 Subject: [PATCH 005/191] chore(react/note_title): bring back styles --- apps/client/src/widgets/note_title.bak | 49 -------------------------- apps/client/src/widgets/note_title.css | 24 +++++++++++++ apps/client/src/widgets/note_title.tsx | 22 +++++++++--- 3 files changed, 41 insertions(+), 54 deletions(-) create mode 100644 apps/client/src/widgets/note_title.css diff --git a/apps/client/src/widgets/note_title.bak b/apps/client/src/widgets/note_title.bak index e07b8380e..059e4b4d1 100644 --- a/apps/client/src/widgets/note_title.bak +++ b/apps/client/src/widgets/note_title.bak @@ -9,36 +9,6 @@ import shortcutService from "../services/shortcuts.js"; import utils from "../services/utils.js"; import type FNote from "../entities/fnote.js"; -const TPL = /*html*/` -
- -
`; - export default class NoteTitleWidget extends NoteContextAwareWidget { private $noteTitle!: JQuery; @@ -48,10 +18,6 @@ export default class NoteTitleWidget extends NoteContextAwareWidget { constructor() { super(); - this.spacedUpdate = new SpacedUpdate(async () => { - - }); - this.deleteNoteOnEscape = false; appContext.addBeforeUnloadListener(this); @@ -92,10 +58,6 @@ export default class NoteTitleWidget extends NoteContextAwareWidget { this.setProtectedStatus(note); } - setProtectedStatus(note: FNote) { - this.$noteTitle.toggleClass("protected", !!note.isProtected); - } - async beforeNoteSwitchEvent({ noteContext }: EventData<"beforeNoteSwitch">) { if (this.isNoteContext(noteContext.ntxId)) { await this.spacedUpdate.updateNowIfNecessary(); @@ -122,17 +84,6 @@ export default class NoteTitleWidget extends NoteContextAwareWidget { } } - entitiesReloadedEvent({ loadResults }: EventData<"entitiesReloaded">) { - if (loadResults.isNoteReloaded(this.noteId) && this.note) { - // not updating the title specifically since the synced title might be older than what the user is currently typing - this.setProtectedStatus(this.note); - } - - if (loadResults.isNoteReloaded(this.noteId, this.componentId)) { - this.refresh(); - } - } - beforeUnloadEvent() { return this.spacedUpdate.isAllSavedAndTriggerUpdate(); } diff --git a/apps/client/src/widgets/note_title.css b/apps/client/src/widgets/note_title.css new file mode 100644 index 000000000..294e2a01f --- /dev/null +++ b/apps/client/src/widgets/note_title.css @@ -0,0 +1,24 @@ +.note-title-widget { + flex-grow: 1000; + height: 100%; +} + +.note-title-widget input.note-title { + font-size: 110%; + border: 0; + margin: 2px 0px; + min-width: 5em; + width: 100%; +} + +.note-title-widget input.note-title.protected { + text-shadow: 4px 4px 4px var(--muted-text-color); +} + +body.mobile .note-title-widget input.note-title { + padding: 0; +} + +body.desktop .note-title-widget input.note-title { + font-size: 180%; +} \ No newline at end of file diff --git a/apps/client/src/widgets/note_title.tsx b/apps/client/src/widgets/note_title.tsx index d74779dfa..55769cfee 100644 --- a/apps/client/src/widgets/note_title.tsx +++ b/apps/client/src/widgets/note_title.tsx @@ -1,14 +1,17 @@ import { useEffect, useRef, useState } from "preact/hooks"; import { t } from "../services/i18n"; import FormTextBox from "./react/FormTextBox"; -import { useNoteContext, useSpacedUpdate } from "./react/hooks"; +import { useNoteContext, useSpacedUpdate, useTriliumEventBeta } from "./react/hooks"; import protected_session_holder from "../services/protected_session_holder"; import server from "../services/server"; +import "./note_title.css"; export default function NoteTitleWidget() { const { note, noteId, componentId } = useNoteContext(); const [ title, setTitle ] = useState(note?.title); - useEffect(() => setTitle(note?.title), [ note?.title ]); + const [ isProtected, setProtected ] = useState(note?.isProtected); + useEffect(() => setTitle(note?.title), [ note?.noteId ]); + useEffect(() => setProtected(note?.isProtected), [ note?.isProtected ]); const spacedUpdate = useSpacedUpdate(async () => { if (!note) { @@ -18,19 +21,28 @@ export default function NoteTitleWidget() { await server.put(`notes/${noteId}/title`, { title: title }, componentId); }); + useTriliumEventBeta("entitiesReloaded", ({ loadResults }) => { + if (loadResults.isNoteReloaded(noteId) && note) { + setProtected(note.isProtected); + } + if (loadResults.isNoteReloaded(noteId, componentId)) { + setTitle(note?.title); + } + }); + return ( - <> +
{ setTitle(newValue); spacedUpdate.scheduleUpdate(); }} /> - +
); } From db2bf537eaa6f039c6df4e812b5a13e81c9661c1 Mon Sep 17 00:00:00 2001 From: Elian Doran Date: Thu, 21 Aug 2025 10:44:58 +0300 Subject: [PATCH 006/191] refactor(react/note_title): use hook for listening to note property --- apps/client/src/widgets/note_title.tsx | 10 +++------- apps/client/src/widgets/react/hooks.tsx | 16 ++++++++++++++++ 2 files changed, 19 insertions(+), 7 deletions(-) diff --git a/apps/client/src/widgets/note_title.tsx b/apps/client/src/widgets/note_title.tsx index 55769cfee..eefb71184 100644 --- a/apps/client/src/widgets/note_title.tsx +++ b/apps/client/src/widgets/note_title.tsx @@ -1,7 +1,7 @@ import { useEffect, useRef, useState } from "preact/hooks"; import { t } from "../services/i18n"; import FormTextBox from "./react/FormTextBox"; -import { useNoteContext, useSpacedUpdate, useTriliumEventBeta } from "./react/hooks"; +import { useNoteContext, useNoteProperty, useSpacedUpdate, useTriliumEventBeta } from "./react/hooks"; import protected_session_holder from "../services/protected_session_holder"; import server from "../services/server"; import "./note_title.css"; @@ -9,9 +9,8 @@ import "./note_title.css"; export default function NoteTitleWidget() { const { note, noteId, componentId } = useNoteContext(); const [ title, setTitle ] = useState(note?.title); - const [ isProtected, setProtected ] = useState(note?.isProtected); + const isProtected = useNoteProperty(note, "isProtected"); useEffect(() => setTitle(note?.title), [ note?.noteId ]); - useEffect(() => setProtected(note?.isProtected), [ note?.isProtected ]); const spacedUpdate = useSpacedUpdate(async () => { if (!note) { @@ -21,10 +20,7 @@ export default function NoteTitleWidget() { await server.put(`notes/${noteId}/title`, { title: title }, componentId); }); - useTriliumEventBeta("entitiesReloaded", ({ loadResults }) => { - if (loadResults.isNoteReloaded(noteId) && note) { - setProtected(note.isProtected); - } + useTriliumEventBeta("entitiesReloaded", ({ loadResults }) => { if (loadResults.isNoteReloaded(noteId, componentId)) { setTitle(note?.title); } diff --git a/apps/client/src/widgets/react/hooks.tsx b/apps/client/src/widgets/react/hooks.tsx index d592a48a1..bc96b780b 100644 --- a/apps/client/src/widgets/react/hooks.tsx +++ b/apps/client/src/widgets/react/hooks.tsx @@ -8,6 +8,7 @@ import utils, { reloadFrontendApp } from "../../services/utils"; import Component from "../../components/component"; import NoteContext from "../../components/note_context"; import { ReactWrappedWidget } from "../basic_widget"; +import FNote from "../../entities/fnote"; type TriliumEventHandler = (data: EventData) => void; const registeredHandlers: Map[]>> = new Map(); @@ -261,4 +262,19 @@ export function useNoteContext() { componentId: parentComponent.componentId }; +} + +export function useNoteProperty(note: FNote | null | undefined, property: T) { + if (!note) { + return null; + } + + const [ value, setValue ] = useState(note[property]); + useEffect(() => setValue(value), [ note[property] ]); + useTriliumEventBeta("entitiesReloaded", ({ loadResults }) => { + if (loadResults.isNoteReloaded(note.noteId)) { + setValue(note[property]); + } + }); + return value; } \ No newline at end of file From 4da3e8a4d88caf9be7bdbb42d78e25b4618a5159 Mon Sep 17 00:00:00 2001 From: Elian Doran Date: Thu, 21 Aug 2025 10:53:59 +0300 Subject: [PATCH 007/191] refactor(react/note_title): use note property for title as well --- apps/client/src/widgets/note_title.tsx | 20 +++++++------------- apps/client/src/widgets/react/hooks.tsx | 21 +++++++++++++++++---- 2 files changed, 24 insertions(+), 17 deletions(-) diff --git a/apps/client/src/widgets/note_title.tsx b/apps/client/src/widgets/note_title.tsx index eefb71184..c23cea833 100644 --- a/apps/client/src/widgets/note_title.tsx +++ b/apps/client/src/widgets/note_title.tsx @@ -1,41 +1,35 @@ -import { useEffect, useRef, useState } from "preact/hooks"; +import { useRef } from "preact/hooks"; import { t } from "../services/i18n"; import FormTextBox from "./react/FormTextBox"; -import { useNoteContext, useNoteProperty, useSpacedUpdate, useTriliumEventBeta } from "./react/hooks"; +import { useNoteContext, useNoteProperty, useSpacedUpdate } from "./react/hooks"; import protected_session_holder from "../services/protected_session_holder"; import server from "../services/server"; import "./note_title.css"; export default function NoteTitleWidget() { const { note, noteId, componentId } = useNoteContext(); - const [ title, setTitle ] = useState(note?.title); + const title = useNoteProperty(note, "title", componentId); const isProtected = useNoteProperty(note, "isProtected"); - useEffect(() => setTitle(note?.title), [ note?.noteId ]); + const newTitle = useRef(""); const spacedUpdate = useSpacedUpdate(async () => { if (!note) { return; } protected_session_holder.touchProtectedSessionIfNecessary(note); - await server.put(`notes/${noteId}/title`, { title: title }, componentId); + await server.put(`notes/${noteId}/title`, { title: newTitle.current }, componentId); }); - useTriliumEventBeta("entitiesReloaded", ({ loadResults }) => { - if (loadResults.isNoteReloaded(noteId, componentId)) { - setTitle(note?.title); - } - }); - return (
{ - setTitle(newValue); + newTitle.current = newValue; spacedUpdate.scheduleUpdate(); }} /> diff --git a/apps/client/src/widgets/react/hooks.tsx b/apps/client/src/widgets/react/hooks.tsx index bc96b780b..1097f7d9c 100644 --- a/apps/client/src/widgets/react/hooks.tsx +++ b/apps/client/src/widgets/react/hooks.tsx @@ -264,17 +264,30 @@ export function useNoteContext() { } -export function useNoteProperty(note: FNote | null | undefined, property: T) { +/** + * Allows a React component to listen to obtain a property of a {@link FNote} while also automatically watching for changes, either via the user changing to a different note or the property being changed externally. + * + * @param note the {@link FNote} whose property to obtain. + * @param property a property of a {@link FNote} to obtain the value from (e.g. `title`, `isProtected`). + * @param componentId optionally, constricts the refresh of the value if an update occurs externally via the component ID of a legacy widget. This can be used to avoid external data replacing fresher, user-inputted data. + * @returns the value of the requested property. + */ +export function useNoteProperty(note: FNote | null | undefined, property: T, componentId?: string) { if (!note) { return null; } - + const [ value, setValue ] = useState(note[property]); - useEffect(() => setValue(value), [ note[property] ]); + + // Watch for note changes. + useEffect(() => setValue(note[property]), [ note[property] ]); + + // Watch for external changes. useTriliumEventBeta("entitiesReloaded", ({ loadResults }) => { - if (loadResults.isNoteReloaded(note.noteId)) { + if (loadResults.isNoteReloaded(note.noteId, componentId)) { setValue(note[property]); } }); + return value; } \ No newline at end of file From be576176c506739339cbf119547454c91607256d Mon Sep 17 00:00:00 2001 From: Elian Doran Date: Thu, 21 Aug 2025 11:08:33 +0300 Subject: [PATCH 008/191] feat(react/note_title): bring back navigation title --- apps/client/src/services/utils.ts | 2 +- apps/client/src/widgets/note_title.bak | 12 ----------- apps/client/src/widgets/note_title.tsx | 28 +++++++++++++++++++++---- apps/client/src/widgets/react/hooks.tsx | 6 ++++-- 4 files changed, 29 insertions(+), 19 deletions(-) diff --git a/apps/client/src/services/utils.ts b/apps/client/src/services/utils.ts index aeb9abe47..065969df8 100644 --- a/apps/client/src/services/utils.ts +++ b/apps/client/src/services/utils.ts @@ -740,7 +740,7 @@ function isUpdateAvailable(latestVersion: string | null | undefined, currentVers return compareVersions(latestVersion, currentVersion) > 0; } -function isLaunchBarConfig(noteId: string) { +export function isLaunchBarConfig(noteId: string) { return ["_lbRoot", "_lbAvailableLaunchers", "_lbVisibleLaunchers", "_lbMobileRoot", "_lbMobileAvailableLaunchers", "_lbMobileVisibleLaunchers"].includes(noteId); } diff --git a/apps/client/src/widgets/note_title.bak b/apps/client/src/widgets/note_title.bak index 059e4b4d1..4b7218760 100644 --- a/apps/client/src/widgets/note_title.bak +++ b/apps/client/src/widgets/note_title.bak @@ -46,18 +46,6 @@ export default class NoteTitleWidget extends NoteContextAwareWidget { }); } - async refreshWithNote(note: FNote) { - const isReadOnly = - (note.isProtected && !protectedSessionHolder.isProtectedSessionAvailable()) - || utils.isLaunchBarConfig(note.noteId) - || this.noteContext?.viewScope?.viewMode !== "default"; - - this.$noteTitle.val(isReadOnly ? (await this.noteContext?.getNavigationTitle()) || "" : note.title); - this.$noteTitle.prop("readonly", isReadOnly); - - this.setProtectedStatus(note); - } - async beforeNoteSwitchEvent({ noteContext }: EventData<"beforeNoteSwitch">) { if (this.isNoteContext(noteContext.ntxId)) { await this.spacedUpdate.updateNowIfNecessary(); diff --git a/apps/client/src/widgets/note_title.tsx b/apps/client/src/widgets/note_title.tsx index c23cea833..267d8ff27 100644 --- a/apps/client/src/widgets/note_title.tsx +++ b/apps/client/src/widgets/note_title.tsx @@ -1,17 +1,36 @@ -import { useRef } from "preact/hooks"; +import { useEffect, useRef, useState } from "preact/hooks"; import { t } from "../services/i18n"; import FormTextBox from "./react/FormTextBox"; import { useNoteContext, useNoteProperty, useSpacedUpdate } from "./react/hooks"; import protected_session_holder from "../services/protected_session_holder"; import server from "../services/server"; import "./note_title.css"; +import { isLaunchBarConfig } from "../services/utils"; export default function NoteTitleWidget() { - const { note, noteId, componentId } = useNoteContext(); - const title = useNoteProperty(note, "title", componentId); + const { note, noteId, componentId, viewScope, noteContext } = useNoteContext(); + const title = useNoteProperty(note, "title", componentId); const isProtected = useNoteProperty(note, "isProtected"); const newTitle = useRef(""); + const [ isReadOnly, setReadOnly ] = useState(false); + const [ navigationTitle, setNavigationTitle ] = useState(null); + + useEffect(() => { + const isReadOnly = note === null + || note === undefined + || (note.isProtected && !protected_session_holder.isProtectedSessionAvailable()) + || isLaunchBarConfig(note.noteId) + || viewScope?.viewMode !== "default"; + setReadOnly(isReadOnly); + }, [ note?.noteId, note?.isProtected, viewScope?.viewMode ]); + + useEffect(() => { + if (isReadOnly) { + noteContext?.getNavigationTitle().then(setNavigationTitle); + } + }, [isReadOnly]); + const spacedUpdate = useSpacedUpdate(async () => { if (!note) { return; @@ -24,10 +43,11 @@ export default function NoteTitleWidget() {
{ newTitle.current = newValue; spacedUpdate.scheduleUpdate(); diff --git a/apps/client/src/widgets/react/hooks.tsx b/apps/client/src/widgets/react/hooks.tsx index 1097f7d9c..7526bbd61 100644 --- a/apps/client/src/widgets/react/hooks.tsx +++ b/apps/client/src/widgets/react/hooks.tsx @@ -259,7 +259,9 @@ export function useNoteContext() { notePath: noteContext?.notePath, hoistedNoteId: noteContext?.hoistedNoteId, ntxId: noteContext?.ntxId, - componentId: parentComponent.componentId + viewScope: noteContext?.viewScope, + componentId: parentComponent.componentId, + noteContext: noteContext }; } @@ -288,6 +290,6 @@ export function useNoteProperty(note: FNote | null | unde setValue(note[property]); } }); - + return value; } \ No newline at end of file From 033e90f8b7185e8a712087dd371c57c676983564 Mon Sep 17 00:00:00 2001 From: Elian Doran Date: Thu, 21 Aug 2025 12:13:30 +0300 Subject: [PATCH 009/191] fix(react/note_title): not refreshing on protected session --- apps/client/src/widgets/note_title.tsx | 6 +++--- apps/client/src/widgets/react/hooks.tsx | 20 +++++++++++++++----- 2 files changed, 18 insertions(+), 8 deletions(-) diff --git a/apps/client/src/widgets/note_title.tsx b/apps/client/src/widgets/note_title.tsx index 267d8ff27..8c0cfae7d 100644 --- a/apps/client/src/widgets/note_title.tsx +++ b/apps/client/src/widgets/note_title.tsx @@ -12,10 +12,10 @@ export default function NoteTitleWidget() { const title = useNoteProperty(note, "title", componentId); const isProtected = useNoteProperty(note, "isProtected"); const newTitle = useRef(""); - + const [ isReadOnly, setReadOnly ] = useState(false); const [ navigationTitle, setNavigationTitle ] = useState(null); - + useEffect(() => { const isReadOnly = note === null || note === undefined @@ -23,7 +23,7 @@ export default function NoteTitleWidget() { || isLaunchBarConfig(note.noteId) || viewScope?.viewMode !== "default"; setReadOnly(isReadOnly); - }, [ note?.noteId, note?.isProtected, viewScope?.viewMode ]); + }, [ note, note?.noteId, note?.isProtected, viewScope?.viewMode ]); useEffect(() => { if (isReadOnly) { diff --git a/apps/client/src/widgets/react/hooks.tsx b/apps/client/src/widgets/react/hooks.tsx index 7526bbd61..05821754b 100644 --- a/apps/client/src/widgets/react/hooks.tsx +++ b/apps/client/src/widgets/react/hooks.tsx @@ -9,6 +9,7 @@ import Component from "../../components/component"; import NoteContext from "../../components/note_context"; import { ReactWrappedWidget } from "../basic_widget"; import FNote from "../../entities/fnote"; +import froca from "../../services/froca"; type TriliumEventHandler = (data: EventData) => void; const registeredHandlers: Map[]>> = new Map(); @@ -233,10 +234,15 @@ export function useNoteContext() { const [ noteContext, setNoteContext ] = useState(); const [ notePath, setNotePath ] = useState(); + const [ note, setNote ] = useState(); + + useEffect(() => { + setNote(noteContext?.note); + }, [ notePath ]); useTriliumEventBeta("activeContextChanged", ({ noteContext }) => { setNoteContext(noteContext); - setNotePath(noteContext.notePath); + setNotePath(noteContext.notePath); }); useTriliumEventBeta("setNoteContext", ({ noteContext }) => { console.log("Set note context", noteContext, noteContext.noteId); @@ -250,11 +256,14 @@ export function useNoteContext() { console.warn("Note switched", notePath); setNotePath(notePath); }); + useTriliumEventBeta("frocaReloaded", () => { + setNote(noteContext?.note); + }); const parentComponent = useContext(ParentComponent) as ReactWrappedWidget; return { - note: noteContext?.note, + note: note, noteId: noteContext?.note?.noteId, notePath: noteContext?.notePath, hoistedNoteId: noteContext?.hoistedNoteId, @@ -280,16 +289,17 @@ export function useNoteProperty(note: FNote | null | unde } const [ value, setValue ] = useState(note[property]); + const refreshValue = () => setValue(note[property]); // Watch for note changes. - useEffect(() => setValue(note[property]), [ note[property] ]); + useEffect(() => refreshValue(), [ note, note[property] ]); // Watch for external changes. useTriliumEventBeta("entitiesReloaded", ({ loadResults }) => { if (loadResults.isNoteReloaded(note.noteId, componentId)) { - setValue(note[property]); + refreshValue(); } }); - return value; + return note[property]; } \ No newline at end of file From 9e947f742d83638b6076251d1624fcb275900f8b Mon Sep 17 00:00:00 2001 From: Elian Doran Date: Thu, 21 Aug 2025 12:15:12 +0300 Subject: [PATCH 010/191] fix(react/note_title): title shown on empty widget pane --- apps/client/src/widgets/note_title.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/apps/client/src/widgets/note_title.tsx b/apps/client/src/widgets/note_title.tsx index 8c0cfae7d..963523fda 100644 --- a/apps/client/src/widgets/note_title.tsx +++ b/apps/client/src/widgets/note_title.tsx @@ -41,7 +41,7 @@ export default function NoteTitleWidget() { return (
- + />}
); } From b93fa332d30cdb66810207443f1a1c2397484afc Mon Sep 17 00:00:00 2001 From: Elian Doran Date: Thu, 21 Aug 2025 12:49:03 +0300 Subject: [PATCH 011/191] fix(client): please wait for save showing up multiple times --- apps/client/src/components/app_context.ts | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/apps/client/src/components/app_context.ts b/apps/client/src/components/app_context.ts index 4c750a544..80af6adf7 100644 --- a/apps/client/src/components/app_context.ts +++ b/apps/client/src/components/app_context.ts @@ -676,14 +676,12 @@ $(window).on("beforeunload", () => { if (!component.beforeUnloadEvent()) { console.log(`Component ${component.componentId} is not finished saving its state.`); - - toast.showMessage(t("app_context.please_wait_for_save"), 10000); - allSaved = false; } } if (!allSaved) { + toast.showMessage(t("app_context.please_wait_for_save"), 10000); return "some string"; } }); From 945e180a6f01f6e4b5ce18f922092093a0dfae2c Mon Sep 17 00:00:00 2001 From: Elian Doran Date: Thu, 21 Aug 2025 12:55:33 +0300 Subject: [PATCH 012/191] chore(react/note_title): add before unload listener --- apps/client/src/components/app_context.ts | 36 +++++++++++++++-------- apps/client/src/widgets/note_title.bak | 14 --------- apps/client/src/widgets/note_title.tsx | 7 ++++- apps/client/src/widgets/react/hooks.tsx | 5 ++-- 4 files changed, 32 insertions(+), 30 deletions(-) diff --git a/apps/client/src/components/app_context.ts b/apps/client/src/components/app_context.ts index 80af6adf7..35b8b8585 100644 --- a/apps/client/src/components/app_context.ts +++ b/apps/client/src/components/app_context.ts @@ -40,7 +40,7 @@ interface RootWidget extends Component { render: () => JQuery; } -interface BeforeUploadListener extends Component { +export interface BeforeUploadListener extends Component { beforeUnloadEvent(): boolean; } @@ -526,7 +526,7 @@ export type FilteredCommandNames = keyof Pick[]; + beforeUnloadListeners: (WeakRef | (() => boolean))[]; tabManager!: TabManager; layout?: Layout; noteTreeWidget?: NoteTreeWidget; @@ -649,13 +649,17 @@ export class AppContext extends Component { return $(el).closest(".component").prop("component"); } - addBeforeUnloadListener(obj: BeforeUploadListener) { + addBeforeUnloadListener(obj: BeforeUploadListener | (() => boolean)) { if (typeof WeakRef !== "function") { // older browsers don't support WeakRef return; } - this.beforeUnloadListeners.push(new WeakRef(obj)); + if (typeof obj === "object") { + this.beforeUnloadListeners.push(new WeakRef(obj)); + } else { + this.beforeUnloadListeners.push(obj); + } } } @@ -665,18 +669,24 @@ const appContext = new AppContext(window.glob.isMainWindow); $(window).on("beforeunload", () => { let allSaved = true; - appContext.beforeUnloadListeners = appContext.beforeUnloadListeners.filter((wr) => !!wr.deref()); + appContext.beforeUnloadListeners = appContext.beforeUnloadListeners.filter((wr) => typeof wr === "function" || !!wr.deref()); - for (const weakRef of appContext.beforeUnloadListeners) { - const component = weakRef.deref(); + for (const listener of appContext.beforeUnloadListeners) { + if (typeof listener === "object") { + const component = listener.deref(); - if (!component) { - continue; - } + if (!component) { + continue; + } - if (!component.beforeUnloadEvent()) { - console.log(`Component ${component.componentId} is not finished saving its state.`); - allSaved = false; + if (!component.beforeUnloadEvent()) { + console.log(`Component ${component.componentId} is not finished saving its state.`); + allSaved = false; + } + } else { + if (!listener()) { + allSaved = false; + } } } diff --git a/apps/client/src/widgets/note_title.bak b/apps/client/src/widgets/note_title.bak index 4b7218760..f1c0fc593 100644 --- a/apps/client/src/widgets/note_title.bak +++ b/apps/client/src/widgets/note_title.bak @@ -1,13 +1,8 @@ -import { t } from "../services/i18n.js"; import NoteContextAwareWidget from "./note_context_aware_widget.js"; -import protectedSessionHolder from "../services/protected_session_holder.js"; -import server from "../services/server.js"; import SpacedUpdate from "../services/spaced_update.js"; import appContext, { type EventData } from "../components/app_context.js"; import branchService from "../services/branches.js"; import shortcutService from "../services/shortcuts.js"; -import utils from "../services/utils.js"; -import type FNote from "../entities/fnote.js"; export default class NoteTitleWidget extends NoteContextAwareWidget { @@ -19,16 +14,11 @@ export default class NoteTitleWidget extends NoteContextAwareWidget { super(); this.deleteNoteOnEscape = false; - - appContext.addBeforeUnloadListener(this); } doRender() { this.$widget = $(TPL); this.$noteTitle = this.$widget.find(".note-title"); - - this.$noteTitle.on("input", () => this.spacedUpdate.scheduleUpdate()); - this.$noteTitle.on("blur", () => { this.spacedUpdate.updateNowIfNecessary(); @@ -71,8 +61,4 @@ export default class NoteTitleWidget extends NoteContextAwareWidget { this.deleteNoteOnEscape = isNewNote; } } - - beforeUnloadEvent() { - return this.spacedUpdate.isAllSavedAndTriggerUpdate(); - } } diff --git a/apps/client/src/widgets/note_title.tsx b/apps/client/src/widgets/note_title.tsx index 963523fda..6f1f82e32 100644 --- a/apps/client/src/widgets/note_title.tsx +++ b/apps/client/src/widgets/note_title.tsx @@ -1,11 +1,12 @@ import { useEffect, useRef, useState } from "preact/hooks"; import { t } from "../services/i18n"; import FormTextBox from "./react/FormTextBox"; -import { useNoteContext, useNoteProperty, useSpacedUpdate } from "./react/hooks"; +import { useBeforeUnload, useNoteContext, useNoteProperty, useSpacedUpdate } from "./react/hooks"; import protected_session_holder from "../services/protected_session_holder"; import server from "../services/server"; import "./note_title.css"; import { isLaunchBarConfig } from "../services/utils"; +import appContext from "../components/app_context"; export default function NoteTitleWidget() { const { note, noteId, componentId, viewScope, noteContext } = useNoteContext(); @@ -39,6 +40,10 @@ export default function NoteTitleWidget() { await server.put(`notes/${noteId}/title`, { title: newTitle.current }, componentId); }); + useEffect(() => { + appContext.addBeforeUnloadListener(() => spacedUpdate.isAllSavedAndTriggerUpdate()); + }, []); + return (
{note && = (data: EventData) => void; const registeredHandlers: Map[]>> = new Map(); @@ -302,4 +303,4 @@ export function useNoteProperty(note: FNote | null | unde }); return note[property]; -} \ No newline at end of file +} From 8a543d45132d70f2cff613ecac1b85d27b4460a9 Mon Sep 17 00:00:00 2001 From: Elian Doran Date: Thu, 21 Aug 2025 13:00:05 +0300 Subject: [PATCH 013/191] chore(react/note_title): focus content on enter --- apps/client/src/widgets/note_title.bak | 3 --- apps/client/src/widgets/note_title.tsx | 12 ++++++++++-- apps/client/src/widgets/react/hooks.tsx | 3 ++- 3 files changed, 12 insertions(+), 6 deletions(-) diff --git a/apps/client/src/widgets/note_title.bak b/apps/client/src/widgets/note_title.bak index f1c0fc593..e79bd7517 100644 --- a/apps/client/src/widgets/note_title.bak +++ b/apps/client/src/widgets/note_title.bak @@ -31,9 +31,6 @@ export default class NoteTitleWidget extends NoteContextAwareWidget { } }); - shortcutService.bindElShortcut(this.$noteTitle, "return", () => { - this.triggerCommand("focusOnDetail", { ntxId: this.noteContext?.ntxId }); - }); } async beforeNoteSwitchEvent({ noteContext }: EventData<"beforeNoteSwitch">) { diff --git a/apps/client/src/widgets/note_title.tsx b/apps/client/src/widgets/note_title.tsx index 6f1f82e32..6f3581bbf 100644 --- a/apps/client/src/widgets/note_title.tsx +++ b/apps/client/src/widgets/note_title.tsx @@ -9,7 +9,7 @@ import { isLaunchBarConfig } from "../services/utils"; import appContext from "../components/app_context"; export default function NoteTitleWidget() { - const { note, noteId, componentId, viewScope, noteContext } = useNoteContext(); + const { note, noteId, componentId, viewScope, noteContext, parentComponent } = useNoteContext(); const title = useNoteProperty(note, "title", componentId); const isProtected = useNoteProperty(note, "isProtected"); const newTitle = useRef(""); @@ -41,7 +41,7 @@ export default function NoteTitleWidget() { }); useEffect(() => { - appContext.addBeforeUnloadListener(() => spacedUpdate.isAllSavedAndTriggerUpdate()); + appContext.addBeforeUnloadListener(() => spacedUpdate.isAllSavedAndTriggerUpdate()); }, []); return ( @@ -57,6 +57,14 @@ export default function NoteTitleWidget() { newTitle.current = newValue; spacedUpdate.scheduleUpdate(); }} + onKeyDown={(e) => { + // Focus on the note content when pressing enter. + if (e.key === "Enter") { + e.preventDefault(); + parentComponent.triggerCommand("focusOnDetail", { ntxId: noteContext?.ntxId }); + return; + } + }} />}
); diff --git a/apps/client/src/widgets/react/hooks.tsx b/apps/client/src/widgets/react/hooks.tsx index 56b5cb865..b08c4dac1 100644 --- a/apps/client/src/widgets/react/hooks.tsx +++ b/apps/client/src/widgets/react/hooks.tsx @@ -271,7 +271,8 @@ export function useNoteContext() { ntxId: noteContext?.ntxId, viewScope: noteContext?.viewScope, componentId: parentComponent.componentId, - noteContext: noteContext + noteContext, + parentComponent }; } From 51e8a80ca3e1feefe79fe76f18b71916228ada91 Mon Sep 17 00:00:00 2001 From: Elian Doran Date: Thu, 21 Aug 2025 13:13:48 +0300 Subject: [PATCH 014/191] chore(react/note_title): delete new notes on escape --- apps/client/src/widgets/note_title.bak | 24 +------------------ apps/client/src/widgets/note_title.tsx | 32 ++++++++++++++++++++++++- apps/client/src/widgets/react/hooks.tsx | 2 +- 3 files changed, 33 insertions(+), 25 deletions(-) diff --git a/apps/client/src/widgets/note_title.bak b/apps/client/src/widgets/note_title.bak index e79bd7517..412934e9d 100644 --- a/apps/client/src/widgets/note_title.bak +++ b/apps/client/src/widgets/note_title.bak @@ -8,7 +8,6 @@ export default class NoteTitleWidget extends NoteContextAwareWidget { private $noteTitle!: JQuery; private deleteNoteOnEscape: boolean; - private spacedUpdate: SpacedUpdate; constructor() { super(); @@ -20,15 +19,7 @@ export default class NoteTitleWidget extends NoteContextAwareWidget { this.$widget = $(TPL); this.$noteTitle = this.$widget.find(".note-title"); this.$noteTitle.on("blur", () => { - this.spacedUpdate.updateNowIfNecessary(); - - this.deleteNoteOnEscape = false; - }); - - shortcutService.bindElShortcut(this.$noteTitle, "esc", () => { - if (this.deleteNoteOnEscape && this.noteContext?.isActive() && this.noteContext?.note) { - branchService.deleteNotes(Object.values(this.noteContext.note.parentToBranch)); - } + }); } @@ -45,17 +36,4 @@ export default class NoteTitleWidget extends NoteContextAwareWidget { } } - focusOnTitleEvent() { - if (this.noteContext && this.noteContext.isActive()) { - this.$noteTitle.trigger("focus"); - } - } - - focusAndSelectTitleEvent({ isNewNote } = { isNewNote: false }) { - if (this.noteContext && this.noteContext.isActive()) { - this.$noteTitle.trigger("focus").trigger("select"); - - this.deleteNoteOnEscape = isNewNote; - } - } } diff --git a/apps/client/src/widgets/note_title.tsx b/apps/client/src/widgets/note_title.tsx index 6f3581bbf..d824d796e 100644 --- a/apps/client/src/widgets/note_title.tsx +++ b/apps/client/src/widgets/note_title.tsx @@ -1,12 +1,13 @@ import { useEffect, useRef, useState } from "preact/hooks"; import { t } from "../services/i18n"; import FormTextBox from "./react/FormTextBox"; -import { useBeforeUnload, useNoteContext, useNoteProperty, useSpacedUpdate } from "./react/hooks"; +import { useNoteContext, useNoteProperty, useSpacedUpdate, useTriliumEventBeta } from "./react/hooks"; import protected_session_holder from "../services/protected_session_holder"; import server from "../services/server"; import "./note_title.css"; import { isLaunchBarConfig } from "../services/utils"; import appContext from "../components/app_context"; +import branches from "../services/branches"; export default function NoteTitleWidget() { const { note, noteId, componentId, viewScope, noteContext, parentComponent } = useNoteContext(); @@ -17,6 +18,7 @@ export default function NoteTitleWidget() { const [ isReadOnly, setReadOnly ] = useState(false); const [ navigationTitle, setNavigationTitle ] = useState(null); + // Manage read-only useEffect(() => { const isReadOnly = note === null || note === undefined @@ -26,12 +28,14 @@ export default function NoteTitleWidget() { setReadOnly(isReadOnly); }, [ note, note?.noteId, note?.isProtected, viewScope?.viewMode ]); + // Manage the title for read-only notes useEffect(() => { if (isReadOnly) { noteContext?.getNavigationTitle().then(setNavigationTitle); } }, [isReadOnly]); + // Save changes to title. const spacedUpdate = useSpacedUpdate(async () => { if (!note) { return; @@ -40,13 +44,31 @@ export default function NoteTitleWidget() { await server.put(`notes/${noteId}/title`, { title: newTitle.current }, componentId); }); + // Prevent user from navigating away if the spaced update is not done. useEffect(() => { appContext.addBeforeUnloadListener(() => spacedUpdate.isAllSavedAndTriggerUpdate()); }, []); + // Manage focus. + const textBoxRef = useRef(null); + const isNewNote = useRef(); + useTriliumEventBeta("focusOnTitle", () => { + if (noteContext?.isActive() && textBoxRef.current) { + console.log(textBoxRef.current); + textBoxRef.current.focus(); + } + }); + useTriliumEventBeta("focusAndSelectTitle", ({ isNewNote: _isNewNote } ) => { + if (noteContext?.isActive() && textBoxRef.current) { + textBoxRef.current.focus(); + isNewNote.current = _isNewNote; + } + }); + return (
{note && { + spacedUpdate.updateNowIfNecessary(); + isNewNote.current = false; }} />}
diff --git a/apps/client/src/widgets/react/hooks.tsx b/apps/client/src/widgets/react/hooks.tsx index b08c4dac1..4f06041fe 100644 --- a/apps/client/src/widgets/react/hooks.tsx +++ b/apps/client/src/widgets/react/hooks.tsx @@ -101,7 +101,7 @@ export function useSpacedUpdate(callback: () => Promise, interval = 1000) // Update callback ref when it changes useEffect(() => { callbackRef.current = callback; - }); + }, [callback]); // Create SpacedUpdate instance only once if (!spacedUpdateRef.current) { From bea352855a4e38c15406fac12e2f7a24654ff56d Mon Sep 17 00:00:00 2001 From: Elian Doran Date: Thu, 21 Aug 2025 13:17:28 +0300 Subject: [PATCH 015/191] refactor(react): allow binding multiple events at once --- apps/client/src/widgets/note_title.tsx | 10 ++-------- apps/client/src/widgets/react/hooks.tsx | 17 ++++++++++++++--- 2 files changed, 16 insertions(+), 11 deletions(-) diff --git a/apps/client/src/widgets/note_title.tsx b/apps/client/src/widgets/note_title.tsx index d824d796e..9d1522740 100644 --- a/apps/client/src/widgets/note_title.tsx +++ b/apps/client/src/widgets/note_title.tsx @@ -52,16 +52,10 @@ export default function NoteTitleWidget() { // Manage focus. const textBoxRef = useRef(null); const isNewNote = useRef(); - useTriliumEventBeta("focusOnTitle", () => { - if (noteContext?.isActive() && textBoxRef.current) { - console.log(textBoxRef.current); - textBoxRef.current.focus(); - } - }); - useTriliumEventBeta("focusAndSelectTitle", ({ isNewNote: _isNewNote } ) => { + useTriliumEventBeta([ "focusOnTitle", "focusAndSelectTitle" ], (e) => { if (noteContext?.isActive() && textBoxRef.current) { textBoxRef.current.focus(); - isNewNote.current = _isNewNote; + isNewNote.current = ("isNewNote" in e ? e.isNewNote : false); } }); diff --git a/apps/client/src/widgets/react/hooks.tsx b/apps/client/src/widgets/react/hooks.tsx index 4f06041fe..9269e0605 100644 --- a/apps/client/src/widgets/react/hooks.tsx +++ b/apps/client/src/widgets/react/hooks.tsx @@ -87,11 +87,22 @@ export default function useTriliumEvent(eventName: T, hand }, [ eventName, parentWidget, handler ]); } -export function useTriliumEventBeta(eventName: T, handler: TriliumEventHandler) { +export function useTriliumEventBeta(eventName: T | T[], handler: TriliumEventHandler) { const parentComponent = useContext(ParentComponent) as ReactWrappedWidget; - parentComponent.registerHandler(eventName, handler); - return (() => parentComponent.removeHandler(eventName, handler)); + if (Array.isArray(eventName)) { + for (const eventSingleName of eventName) { + parentComponent.registerHandler(eventSingleName, handler); + } + return (() => { + for (const eventSingleName of eventName) { + parentComponent.removeHandler(eventSingleName, handler) + } + }); + } else { + parentComponent.registerHandler(eventName, handler); + return (() => parentComponent.removeHandler(eventName, handler)); + } } export function useSpacedUpdate(callback: () => Promise, interval = 1000) { From 009fd63ce98f598223ab4300c6b58b8bb14d700e Mon Sep 17 00:00:00 2001 From: Elian Doran Date: Thu, 21 Aug 2025 13:18:39 +0300 Subject: [PATCH 016/191] chore(react): finalize note title porting --- apps/client/src/widgets/note_title.bak | 39 -------------------------- apps/client/src/widgets/note_title.tsx | 1 + 2 files changed, 1 insertion(+), 39 deletions(-) delete mode 100644 apps/client/src/widgets/note_title.bak diff --git a/apps/client/src/widgets/note_title.bak b/apps/client/src/widgets/note_title.bak deleted file mode 100644 index 412934e9d..000000000 --- a/apps/client/src/widgets/note_title.bak +++ /dev/null @@ -1,39 +0,0 @@ -import NoteContextAwareWidget from "./note_context_aware_widget.js"; -import SpacedUpdate from "../services/spaced_update.js"; -import appContext, { type EventData } from "../components/app_context.js"; -import branchService from "../services/branches.js"; -import shortcutService from "../services/shortcuts.js"; - -export default class NoteTitleWidget extends NoteContextAwareWidget { - - private $noteTitle!: JQuery; - private deleteNoteOnEscape: boolean; - - constructor() { - super(); - - this.deleteNoteOnEscape = false; - } - - doRender() { - this.$widget = $(TPL); - this.$noteTitle = this.$widget.find(".note-title"); - this.$noteTitle.on("blur", () => { - - }); - - } - - async beforeNoteSwitchEvent({ noteContext }: EventData<"beforeNoteSwitch">) { - if (this.isNoteContext(noteContext.ntxId)) { - await this.spacedUpdate.updateNowIfNecessary(); - } - } - - async beforeNoteContextRemoveEvent({ ntxIds }: EventData<"beforeNoteContextRemove">) { - if (this.isNoteContext(ntxIds)) { - await this.spacedUpdate.updateNowIfNecessary(); - } - } - -} diff --git a/apps/client/src/widgets/note_title.tsx b/apps/client/src/widgets/note_title.tsx index 9d1522740..adef54bc9 100644 --- a/apps/client/src/widgets/note_title.tsx +++ b/apps/client/src/widgets/note_title.tsx @@ -48,6 +48,7 @@ export default function NoteTitleWidget() { useEffect(() => { appContext.addBeforeUnloadListener(() => spacedUpdate.isAllSavedAndTriggerUpdate()); }, []); + useTriliumEventBeta([ "beforeNoteSwitch", "beforeNoteContextRemove" ], () => spacedUpdate.updateNowIfNecessary()); // Manage focus. const textBoxRef = useRef(null); From aa608510d0ee2572473556fdb4e871b0d3078069 Mon Sep 17 00:00:00 2001 From: Elian Doran Date: Thu, 21 Aug 2025 14:41:59 +0300 Subject: [PATCH 017/191] feat(react): port note icon --- apps/client/src/layouts/desktop_layout.tsx | 4 +- apps/client/src/layouts/layout_commons.tsx | 4 +- apps/client/src/widgets/icon_list.ts | 2 +- apps/client/src/widgets/note_icon.css | 59 ++++++++++ .../{note_icon.ts => note_icon.ts.bak} | 102 ------------------ apps/client/src/widgets/note_icon.tsx | 99 +++++++++++++++++ apps/client/src/widgets/react/Dropdown.tsx | 27 ++++- 7 files changed, 185 insertions(+), 112 deletions(-) create mode 100644 apps/client/src/widgets/note_icon.css rename apps/client/src/widgets/{note_icon.ts => note_icon.ts.bak} (60%) create mode 100644 apps/client/src/widgets/note_icon.tsx diff --git a/apps/client/src/layouts/desktop_layout.tsx b/apps/client/src/layouts/desktop_layout.tsx index 85dcb531f..4b094ba3b 100644 --- a/apps/client/src/layouts/desktop_layout.tsx +++ b/apps/client/src/layouts/desktop_layout.tsx @@ -18,7 +18,7 @@ import SqlTableSchemasWidget from "../widgets/sql_table_schemas.js"; import FilePropertiesWidget from "../widgets/ribbon_widgets/file_properties.js"; import ImagePropertiesWidget from "../widgets/ribbon_widgets/image_properties.js"; import NotePropertiesWidget from "../widgets/ribbon_widgets/note_properties.js"; -import NoteIconWidget from "../widgets/note_icon.js"; +import NoteIconWidget from "../widgets/note_icon.jsx"; import SearchResultWidget from "../widgets/search_result.js"; import ScrollingContainer from "../widgets/containers/scrolling_container.js"; import RootContainer from "../widgets/containers/root_container.js"; @@ -151,7 +151,7 @@ export default class DesktopLayout { .css("min-height", "50px") .css("align-items", "center") .cssBlock(".title-row > * { margin: 5px; }") - .child(new NoteIconWidget()) + .child() .child() .child(new SpacerWidget(0, 1)) .child(new MovePaneButton(true)) diff --git a/apps/client/src/layouts/layout_commons.tsx b/apps/client/src/layouts/layout_commons.tsx index e3be51a3a..87d446e18 100644 --- a/apps/client/src/layouts/layout_commons.tsx +++ b/apps/client/src/layouts/layout_commons.tsx @@ -24,7 +24,7 @@ import InfoDialog from "../widgets/dialogs/info.js"; import IncorrectCpuArchDialog from "../widgets/dialogs/incorrect_cpu_arch.js"; import PopupEditorDialog from "../widgets/dialogs/popup_editor.js"; import FlexContainer from "../widgets/containers/flex_container.js"; -import NoteIconWidget from "../widgets/note_icon.js"; +import NoteIconWidget from "../widgets/note_icon"; import ClassicEditorToolbar from "../widgets/ribbon_widgets/classic_editor_toolbar.js"; import PromotedAttributesWidget from "../widgets/ribbon_widgets/promoted_attributes.js"; import NoteDetailWidget from "../widgets/note_detail.js"; @@ -61,7 +61,7 @@ export function applyModals(rootContainer: RootContainer) { .class("title-row") .css("align-items", "center") .cssBlock(".title-row > * { margin: 5px; }") - .child(new NoteIconWidget()) + .child() .child()) .child(new ClassicEditorToolbar()) .child(new PromotedAttributesWidget()) diff --git a/apps/client/src/widgets/icon_list.ts b/apps/client/src/widgets/icon_list.ts index 7de49017b..6282d8b60 100644 --- a/apps/client/src/widgets/icon_list.ts +++ b/apps/client/src/widgets/icon_list.ts @@ -1,6 +1,6 @@ // taken from the HTML source of https://boxicons.com/ -interface Category { +export interface Category { name: string; id: number; } diff --git a/apps/client/src/widgets/note_icon.css b/apps/client/src/widgets/note_icon.css new file mode 100644 index 000000000..67eeadecf --- /dev/null +++ b/apps/client/src/widgets/note_icon.css @@ -0,0 +1,59 @@ +.note-icon-widget { + padding-top: 3px; + padding-left: 7px; + margin-right: 0; + width: 50px; + height: 50px; +} + +.note-icon-widget button.note-icon { + font-size: 180%; + background-color: transparent; + border: 1px solid transparent; + cursor: pointer; + padding: 6px; + color: var(--main-text-color); +} + +.note-icon-widget button.note-icon:hover { + border: 1px solid var(--main-border-color); +} + +.note-icon-widget .dropdown-menu { + border-radius: 10px; + border-width: 2px; + box-shadow: 10px 10px 93px -25px black; + padding: 10px 15px 10px 15px !important; +} + +.note-icon-widget .filter-row { + padding-top: 10px; + padding-bottom: 10px; + padding-right: 20px; + display: flex; + align-items: baseline; +} + +.note-icon-widget .filter-row span { + display: block; + padding-left: 15px; + padding-right: 15px; + font-weight: bold; +} + +.note-icon-widget .icon-list { + height: 500px; + overflow: auto; +} + +.note-icon-widget .icon-list span { + display: inline-block; + padding: 10px; + cursor: pointer; + border: 1px solid transparent; + font-size: 180%; +} + +.note-icon-widget .icon-list span:hover { + border: 1px solid var(--main-border-color); +} \ No newline at end of file diff --git a/apps/client/src/widgets/note_icon.ts b/apps/client/src/widgets/note_icon.ts.bak similarity index 60% rename from apps/client/src/widgets/note_icon.ts rename to apps/client/src/widgets/note_icon.ts.bak index b5623db87..50a846bc3 100644 --- a/apps/client/src/widgets/note_icon.ts +++ b/apps/client/src/widgets/note_icon.ts.bak @@ -7,86 +7,6 @@ import type { EventData } from "../components/app_context.js"; import type { Icon } from "./icon_list.js"; import { Dropdown } from "bootstrap"; -const TPL = /*html*/` -`; - -interface IconToCountCache { - iconClassToCountMap: Record; -} - export default class NoteIconWidget extends NoteContextAwareWidget { private dropdown!: bootstrap.Dropdown; @@ -94,12 +14,8 @@ export default class NoteIconWidget extends NoteContextAwareWidget { private $iconList!: JQuery; private $iconCategory!: JQuery; private $iconSearch!: JQuery; - private iconToCountCache!: Promise | null; doRender() { - this.$widget = $(TPL); - this.dropdown = Dropdown.getOrCreateInstance(this.$widget.find("[data-bs-toggle='dropdown']")[0]); - this.$icon = this.$widget.find("button.note-icon"); this.$iconList = this.$widget.find(".icon-list"); this.$iconList.on("click", "span", async (e) => { @@ -153,9 +69,6 @@ export default class NoteIconWidget extends NoteContextAwareWidget { } async renderDropdown() { - const iconToCount = await this.getIconToCountMap(); - const { icons } = (await import("./icon_list.js")).default; - this.$iconList.empty(); if (this.getIconLabels().length > 0) { @@ -205,21 +118,6 @@ export default class NoteIconWidget extends NoteContextAwareWidget { this.$iconSearch.focus(); } - async getIconToCountMap() { - if (!this.iconToCountCache) { - this.iconToCountCache = server.get("other/icon-usage"); - setTimeout(() => (this.iconToCountCache = null), 20000); // invalidate cache after 20 seconds - } - - return (await this.iconToCountCache)?.iconClassToCountMap; - } - - renderIcon(icon: Icon) { - return $("") - .addClass("bx " + icon.className) - .attr("title", icon.name); - } - getIconLabels() { if (!this.note) { return []; diff --git a/apps/client/src/widgets/note_icon.tsx b/apps/client/src/widgets/note_icon.tsx new file mode 100644 index 000000000..7d11bf341 --- /dev/null +++ b/apps/client/src/widgets/note_icon.tsx @@ -0,0 +1,99 @@ +import Dropdown from "./react/Dropdown"; +import "./note_icon.css"; +import { t } from "i18next"; +import { useNoteContext } from "./react/hooks"; +import { useCallback, useEffect, useState } from "preact/hooks"; +import server from "../services/server"; +import type { Category, Icon } from "./icon_list"; +import FormTextBox from "./react/FormTextBox"; + +interface IconToCountCache { + iconClassToCountMap: Record; +} + +interface IconData { + iconToCount: Record; + icons: Icon[]; +} + +let fullIconData: { + categories: Category[]; + icons: Icon[]; +}; +let iconToCountCache!: Promise | null; + +export default function NoteIcon() { + const { note } = useNoteContext(); + const [ icon, setIcon ] = useState("bx bx-empty"); + + const refreshIcon = useCallback(() => { + if (note) { + setIcon(note.getIcon()); + } + }, [ note ]); + + useEffect(refreshIcon, [ note ]); + + return ( + + + + ) +} + +function NoteIconList() { + const [ filter, setFilter ] = useState(); + const [ iconData, setIconData ] = useState(); + + useEffect(() => { + async function loadIcons() { + const iconToCount = await getIconToCountMap(); + if (!fullIconData) { + fullIconData = (await import("./icon_list.js")).default; + } + + setIconData({ + iconToCount, + icons: fullIconData.icons + }) + } + + loadIcons(); + }, []); + + return ( + <> +
+ {t("note_icon.category")} + + {t("note_icon.search")} + +
+ +
+ {(iconData?.icons ?? []).map(({className, name}) => ( + + ))} +
+ + ); +} + +async function getIconToCountMap() { + if (!iconToCountCache) { + iconToCountCache = server.get("other/icon-usage"); + setTimeout(() => (iconToCountCache = null), 20000); // invalidate cache after 20 seconds + } + + return (await iconToCountCache).iconClassToCountMap; +} \ No newline at end of file diff --git a/apps/client/src/widgets/react/Dropdown.tsx b/apps/client/src/widgets/react/Dropdown.tsx index 58a3bd654..1d64d40fd 100644 --- a/apps/client/src/widgets/react/Dropdown.tsx +++ b/apps/client/src/widgets/react/Dropdown.tsx @@ -1,14 +1,20 @@ import { Dropdown as BootstrapDropdown } from "bootstrap"; import { ComponentChildren } from "preact"; +import { CSSProperties } from "preact/compat"; import { useEffect, useRef } from "preact/hooks"; +import { useUniqueName } from "./hooks"; interface DropdownProps { className?: string; + buttonClassName?: string; isStatic?: boolean; children: ComponentChildren; + title?: string; + dropdownContainerStyle?: CSSProperties; + hideToggleArrow?: boolean; } -export default function Dropdown({ className, isStatic, children }: DropdownProps) { +export default function Dropdown({ className, buttonClassName, isStatic, children, title, dropdownContainerStyle, hideToggleArrow }: DropdownProps) { const dropdownRef = useRef(null); const triggerRef = useRef(null); @@ -35,16 +41,27 @@ export default function Dropdown({ className, isStatic, children }: DropdownProp }; }, []); // Add dependency array + const ariaId = useUniqueName("button"); + return ( -