From 80edc4c4e0169c2646024b9f954de1264cbfa5c2 Mon Sep 17 00:00:00 2001 From: Elian Doran Date: Wed, 27 Aug 2025 21:15:54 +0300 Subject: [PATCH 01/28] feat(react): base structure for floating buttons --- apps/client/src/layouts/desktop_layout.tsx | 36 +---- apps/client/src/widgets/FloatingButtons.css | 94 +++++++++++++ apps/client/src/widgets/FloatingButtons.tsx | 37 ++++++ .../floating_buttons/floating_buttons.ts | 124 ------------------ 4 files changed, 133 insertions(+), 158 deletions(-) create mode 100644 apps/client/src/widgets/FloatingButtons.css create mode 100644 apps/client/src/widgets/FloatingButtons.tsx diff --git a/apps/client/src/layouts/desktop_layout.tsx b/apps/client/src/layouts/desktop_layout.tsx index fece6efd0..bdde92ff1 100644 --- a/apps/client/src/layouts/desktop_layout.tsx +++ b/apps/client/src/layouts/desktop_layout.tsx @@ -22,40 +22,25 @@ import LeftPaneToggleWidget from "../widgets/buttons/left_pane_toggle.js"; import CreatePaneButton from "../widgets/buttons/create_pane_button.js"; import ClosePaneButton from "../widgets/buttons/close_pane_button.js"; import RightPaneContainer from "../widgets/containers/right_pane_container.js"; -import EditButton from "../widgets/floating_buttons/edit_button.js"; -import ShowTocWidgetButton from "../widgets/buttons/show_toc_widget_button.js"; -import ShowHighlightsListWidgetButton from "../widgets/buttons/show_highlights_list_widget_button.js"; import NoteWrapperWidget from "../widgets/note_wrapper.js"; -import BacklinksWidget from "../widgets/floating_buttons/zpetne_odkazy.js"; import SharedInfoWidget from "../widgets/shared_info.js"; import FindWidget from "../widgets/find.js"; import TocWidget from "../widgets/toc.js"; import HighlightsListWidget from "../widgets/highlights_list.js"; import PasswordNoteSetDialog from "../widgets/dialogs/password_not_set.js"; -import FloatingButtons from "../widgets/floating_buttons/floating_buttons.js"; -import RelationMapButtons from "../widgets/floating_buttons/relation_map_buttons.js"; -import SvgExportButton from "../widgets/floating_buttons/svg_export_button.js"; import LauncherContainer from "../widgets/containers/launcher_container.js"; -import CodeButtonsWidget from "../widgets/floating_buttons/code_buttons.js"; import ApiLogWidget from "../widgets/api_log.js"; -import HideFloatingButtonsButton from "../widgets/floating_buttons/hide_floating_buttons_button.js"; import MovePaneButton from "../widgets/buttons/move_pane_button.js"; import UploadAttachmentsDialog from "../widgets/dialogs/upload_attachments.js"; -import CopyImageReferenceButton from "../widgets/floating_buttons/copy_image_reference_button.js"; import ScrollPaddingWidget from "../widgets/scroll_padding.js"; import options from "../services/options.js"; import utils from "../services/utils.js"; -import GeoMapButtons from "../widgets/floating_buttons/geo_map_button.js"; -import ContextualHelpButton from "../widgets/floating_buttons/help_button.js"; import CloseZenButton from "../widgets/close_zen_button.js"; import type { AppContext } from "../components/app_context.js"; import type { WidgetsByParent } from "../services/bundle.js"; -import SwitchSplitOrientationButton from "../widgets/floating_buttons/switch_layout_button.js"; -import ToggleReadOnlyButton from "../widgets/floating_buttons/toggle_read_only_button.js"; -import PngExportButton from "../widgets/floating_buttons/png_export_button.js"; -import RefreshButton from "../widgets/floating_buttons/refresh_button.js"; import { applyModals } from "./layout_commons.js"; import Ribbon from "../widgets/ribbon/Ribbon.jsx"; +import FloatingButtons from "../widgets/FloatingButtons.jsx"; export default class DesktopLayout { @@ -145,24 +130,7 @@ export default class DesktopLayout { .child() .child(new SharedInfoWidget()) .child(new WatchedFileUpdateStatusWidget()) - .child( - new FloatingButtons() - .child(new RefreshButton()) - .child(new SwitchSplitOrientationButton()) - .child(new ToggleReadOnlyButton()) - .child(new EditButton()) - .child(new ShowTocWidgetButton()) - .child(new ShowHighlightsListWidgetButton()) - .child(new CodeButtonsWidget()) - .child(new RelationMapButtons()) - .child(new GeoMapButtons()) - .child(new CopyImageReferenceButton()) - .child(new SvgExportButton()) - .child(new PngExportButton()) - .child(new BacklinksWidget()) - .child(new ContextualHelpButton()) - .child(new HideFloatingButtonsButton()) - ) + .child() .child( new ScrollingContainer() .filling() diff --git a/apps/client/src/widgets/FloatingButtons.css b/apps/client/src/widgets/FloatingButtons.css new file mode 100644 index 000000000..d254a4363 --- /dev/null +++ b/apps/client/src/widgets/FloatingButtons.css @@ -0,0 +1,94 @@ +/* #region Generic floating buttons styles */ +.floating-buttons { + position: relative; +} + +.floating-buttons-children, +.show-floating-buttons { + position: absolute; + top: 10px; + right: 10px; + display: flex; + flex-direction: row; + z-index: 100; +} + +.note-split.rtl .floating-buttons-children, +.note-split.rtl .show-floating-buttons { + right: unset; + left: 10px; +} + +.note-split.rtl .close-floating-buttons { + order: -1; +} + +.note-split.rtl .close-floating-buttons, +.note-split.rtl .show-floating-buttons { + transform: rotate(180deg); +} + +.type-canvas .floating-buttons-children { + top: 70px; +} + +.type-canvas .floating-buttons-children > * { + --border-radius: 0; /* Overridden by themes */ +} + +.floating-buttons-children > *:not(.hidden-int):not(.no-content-hidden) { + margin: 2px; +} + +.floating-buttons-children > *:not(.has-overflow) { + overflow: hidden; +} + +.floating-buttons-children > button, .floating-buttons-children .floating-button { + font-size: 150%; + padding: 5px 10px 4px 10px; + width: 40px; + cursor: pointer; + color: var(--button-text-color); + background: var(--button-background-color); + border-radius: var(--button-border-radius); + border: 1px solid transparent; + display: flex; + justify-content: space-around; +} + +.floating-buttons-children > button:hover, .floating-buttons-children .floating-button:hover { + text-decoration: none; + border-color: var(--button-border-color); +} + +.floating-buttons .floating-buttons-children.temporarily-hidden { + display: none; +} +/* #endregion */ + +/* #region Show floating button */ +.floating-buttons-children.temporarily-hidden+.show-floating-buttons { + display: block; +} + +.floating-buttons-children:not(.temporarily-hidden)+.show-floating-buttons { + display: none; +} + +.show-floating-buttons { + /* display: none;*/ + margin-left: 5px !important; +} + +.show-floating-buttons-button { + border: 1px solid transparent; + color: var(--button-text-color); + padding: 6px; + border-radius: 100px; +} + +.show-floating-buttons-button:hover { + border: 1px solid var(--button-border-color); +} +/* #endregion */ \ No newline at end of file diff --git a/apps/client/src/widgets/FloatingButtons.tsx b/apps/client/src/widgets/FloatingButtons.tsx new file mode 100644 index 000000000..d19ccfa49 --- /dev/null +++ b/apps/client/src/widgets/FloatingButtons.tsx @@ -0,0 +1,37 @@ +import { t } from "i18next"; +import "./FloatingButtons.css"; +import Button from "./react/Button"; + +/* + * Note: + * + * For floating button widgets that require content to overflow, the has-overflow CSS class should + * be applied to the root element of the widget. Additionally, this root element may need to + * properly handle rounded corners, as defined by the --border-radius CSS variable. + */ +export default function FloatingButtons() { + return ( +
+
+ +
+ + +
+ ) +} + +/** + * Show button that displays floating button after click on close button + */ +function ShowFloatingButton() { + return ( +
+
+ ); +} \ No newline at end of file diff --git a/apps/client/src/widgets/floating_buttons/floating_buttons.ts b/apps/client/src/widgets/floating_buttons/floating_buttons.ts index e75072d4e..c2900dee3 100644 --- a/apps/client/src/widgets/floating_buttons/floating_buttons.ts +++ b/apps/client/src/widgets/floating_buttons/floating_buttons.ts @@ -3,135 +3,11 @@ import { t } from "../../services/i18n.js"; import type FNote from "../../entities/fnote.js"; import type BasicWidget from "../basic_widget.js"; -/* - * Note: - * - * For floating button widgets that require content to overflow, the has-overflow CSS class should - * be applied to the root element of the widget. Additionally, this root element may need to - * properly handle rounded corners, as defined by the --border-radius CSS variable. - */ - -const TPL = /*html*/` -
- - -
- - -
- - - -
-
`; export default class FloatingButtons extends NoteContextAwareWidget { private $children!: JQuery; - doRender() { - this.$widget = $(TPL); - this.$children = this.$widget.find(".floating-buttons-children"); - - for (const widget of this.children) { - if ("render" in widget) { - this.$children.append((widget as BasicWidget).render()); - } - } - } - async refreshWithNote(note: FNote) { this.toggle(true); this.$widget.find(".show-floating-buttons-button").on("click", () => this.toggle(true)); From 4c70d72ba2143138486c9364989f51184fbdadbc Mon Sep 17 00:00:00 2001 From: Elian Doran Date: Wed, 27 Aug 2025 21:29:49 +0300 Subject: [PATCH 02/28] feat(react/floating_buttons): port refresh button --- apps/client/src/widgets/FloatingButtons.tsx | 54 ++++++++++++++++++- .../floating_buttons/refresh_button.ts | 21 -------- 2 files changed, 53 insertions(+), 22 deletions(-) delete mode 100644 apps/client/src/widgets/floating_buttons/refresh_button.ts diff --git a/apps/client/src/widgets/FloatingButtons.tsx b/apps/client/src/widgets/FloatingButtons.tsx index d19ccfa49..c0677ab9c 100644 --- a/apps/client/src/widgets/FloatingButtons.tsx +++ b/apps/client/src/widgets/FloatingButtons.tsx @@ -1,6 +1,35 @@ import { t } from "i18next"; import "./FloatingButtons.css"; import Button from "./react/Button"; +import ActionButton from "./react/ActionButton"; +import FNote from "../entities/fnote"; +import NoteContext from "../components/note_context"; +import { useNoteContext } from "./react/hooks"; +import { useContext, useEffect, useMemo } from "preact/hooks"; +import { ParentComponent } from "./react/react_utils"; +import Component from "../components/component"; + +interface FloatingButtonContext { + parentComponent: Component; + note: FNote; + noteContext: NoteContext; +} + +interface FloatingButtonDefinition { + title: string; + icon: string; + isEnabled: (context: FloatingButtonContext) => boolean; + onClick: (context: FloatingButtonContext) => void; +} + +const FLOATING_BUTTON_DEFINITIONS: FloatingButtonDefinition[] = [ + { + title: t("backend_log.refresh"), + icon: "bx bx-refresh", + isEnabled: ({ note, noteContext }) => note.noteId === "_backendLog" && noteContext.viewScope?.viewMode === "default", + onClick: ({ parentComponent, noteContext }) => parentComponent.triggerEvent("refreshData", { ntxId: noteContext.ntxId }) + } +]; /* * Note: @@ -10,10 +39,33 @@ import Button from "./react/Button"; * properly handle rounded corners, as defined by the --border-radius CSS variable. */ export default function FloatingButtons() { + const { note, noteContext } = useNoteContext(); + const parentComponent = useContext(ParentComponent); + const context = useMemo(() => { + if (!note || !noteContext || !parentComponent) return null; + + return { + note, + noteContext, + parentComponent + }; + }, [ note, noteContext, parentComponent ]); + + const definitions = useMemo(() => { + if (!context) return []; + return FLOATING_BUTTON_DEFINITIONS.filter(def => def.isEnabled(context)); + }, [ context ]); + return (
- + {context && definitions.map(({ title, icon, onClick }) => ( + onClick(context)} + /> + ))}
diff --git a/apps/client/src/widgets/floating_buttons/refresh_button.ts b/apps/client/src/widgets/floating_buttons/refresh_button.ts deleted file mode 100644 index 0f834434e..000000000 --- a/apps/client/src/widgets/floating_buttons/refresh_button.ts +++ /dev/null @@ -1,21 +0,0 @@ -import appContext from "../../components/app_context.js"; -import { t } from "../../services/i18n.js"; -import OnClickButtonWidget from "../buttons/onclick_button.js"; - -export default class RefreshButton extends OnClickButtonWidget { - constructor() { - super(); - - this - .title(t("backend_log.refresh")) - .icon("bx-refresh") - .onClick(() => this.triggerEvent("refreshData", { ntxId: this.noteContext?.ntxId })) - } - - isEnabled(): boolean | null | undefined { - return super.isEnabled() - && this.note?.noteId === "_backendLog" - && this.noteContext?.viewScope?.viewMode === "default"; - } - -} From 2d950e8f3abcc9a80a617740227a8ffdbeef68ad Mon Sep 17 00:00:00 2001 From: Elian Doran Date: Wed, 27 Aug 2025 21:37:48 +0300 Subject: [PATCH 03/28] refactor(react/floating_buttons): use component-driven approach --- apps/client/src/widgets/FloatingButtons.tsx | 25 +++++++++++---------- 1 file changed, 13 insertions(+), 12 deletions(-) diff --git a/apps/client/src/widgets/FloatingButtons.tsx b/apps/client/src/widgets/FloatingButtons.tsx index c0677ab9c..864e9449e 100644 --- a/apps/client/src/widgets/FloatingButtons.tsx +++ b/apps/client/src/widgets/FloatingButtons.tsx @@ -8,6 +8,7 @@ import { useNoteContext } from "./react/hooks"; import { useContext, useEffect, useMemo } from "preact/hooks"; import { ParentComponent } from "./react/react_utils"; import Component from "../components/component"; +import { VNode } from "preact"; interface FloatingButtonContext { parentComponent: Component; @@ -16,18 +17,14 @@ interface FloatingButtonContext { } interface FloatingButtonDefinition { - title: string; - icon: string; + component: (context: FloatingButtonContext) => VNode; isEnabled: (context: FloatingButtonContext) => boolean; - onClick: (context: FloatingButtonContext) => void; } const FLOATING_BUTTON_DEFINITIONS: FloatingButtonDefinition[] = [ { - title: t("backend_log.refresh"), - icon: "bx bx-refresh", + component: RefreshBackendLogButton, isEnabled: ({ note, noteContext }) => note.noteId === "_backendLog" && noteContext.viewScope?.viewMode === "default", - onClick: ({ parentComponent, noteContext }) => parentComponent.triggerEvent("refreshData", { ntxId: noteContext.ntxId }) } ]; @@ -59,12 +56,8 @@ export default function FloatingButtons() { return (
- {context && definitions.map(({ title, icon, onClick }) => ( - onClick(context)} - /> + {context && definitions.map(({ component: Component }) => ( + ))}
@@ -73,6 +66,14 @@ export default function FloatingButtons() { ) } +function RefreshBackendLogButton({ parentComponent, noteContext }: FloatingButtonContext) { + return parentComponent.triggerEvent("refreshData", { ntxId: noteContext.ntxId })} + /> +} + /** * Show button that displays floating button after click on close button */ From e340e6f5e354d9523b32d0f0d1699ee0e11ef801 Mon Sep 17 00:00:00 2001 From: Elian Doran Date: Wed, 27 Aug 2025 21:56:02 +0300 Subject: [PATCH 04/28] feat(react/floating_buttons): port switch split orientation --- apps/client/src/widgets/FloatingButtons.tsx | 20 +++++- .../floating_buttons/switch_layout_button.ts | 62 ------------------- 2 files changed, 18 insertions(+), 64 deletions(-) delete mode 100644 apps/client/src/widgets/floating_buttons/switch_layout_button.ts diff --git a/apps/client/src/widgets/FloatingButtons.tsx b/apps/client/src/widgets/FloatingButtons.tsx index 864e9449e..04936d1fe 100644 --- a/apps/client/src/widgets/FloatingButtons.tsx +++ b/apps/client/src/widgets/FloatingButtons.tsx @@ -4,7 +4,7 @@ import Button from "./react/Button"; import ActionButton from "./react/ActionButton"; import FNote from "../entities/fnote"; import NoteContext from "../components/note_context"; -import { useNoteContext } from "./react/hooks"; +import { useNoteContext, useNoteLabel, useNoteLabelBoolean, useTriliumOption } from "./react/hooks"; import { useContext, useEffect, useMemo } from "preact/hooks"; import { ParentComponent } from "./react/react_utils"; import Component from "../components/component"; @@ -25,6 +25,10 @@ const FLOATING_BUTTON_DEFINITIONS: FloatingButtonDefinition[] = [ { component: RefreshBackendLogButton, isEnabled: ({ note, noteContext }) => note.noteId === "_backendLog" && noteContext.viewScope?.viewMode === "default", + }, + { + component: SwitchSplitOrientationButton, + isEnabled: ({ note, noteContext }) => note.type === "mermaid" && note.isContentAvailable() && !note.hasLabel("readOnly") && noteContext.viewScope?.viewMode === "default" } ]; @@ -48,10 +52,11 @@ export default function FloatingButtons() { }; }, [ note, noteContext, parentComponent ]); + const isReadOnly = useNoteLabelBoolean(note, "readOnly"); const definitions = useMemo(() => { if (!context) return []; return FLOATING_BUTTON_DEFINITIONS.filter(def => def.isEnabled(context)); - }, [ context ]); + }, [ context, isReadOnly ]); return (
@@ -74,6 +79,17 @@ function RefreshBackendLogButton({ parentComponent, noteContext }: FloatingButto /> } +function SwitchSplitOrientationButton({ }: FloatingButtonContext) { + const [ splitEditorOrientation, setSplitEditorOrientation ] = useTriliumOption("splitEditorOrientation"); + const upcomingOrientation = splitEditorOrientation === "horizontal" ? "vertical" : "horizontal"; + + return setSplitEditorOrientation(upcomingOrientation)} + /> +} + /** * Show button that displays floating button after click on close button */ diff --git a/apps/client/src/widgets/floating_buttons/switch_layout_button.ts b/apps/client/src/widgets/floating_buttons/switch_layout_button.ts deleted file mode 100644 index f306a8491..000000000 --- a/apps/client/src/widgets/floating_buttons/switch_layout_button.ts +++ /dev/null @@ -1,62 +0,0 @@ -import type { EventData } from "../../components/app_context.js"; -import { t } from "../../services/i18n.js"; -import options from "../../services/options.js"; -import NoteContextAwareWidget from "../note_context_aware_widget.js"; - -const TPL = /*html*/` - -`; - -export default class SwitchSplitOrientationButton extends NoteContextAwareWidget { - isEnabled() { - return super.isEnabled() - && ["mermaid"].includes(this.note?.type ?? "") - && this.note?.isContentAvailable() - && !this.note?.hasLabel("readOnly") - && this.noteContext?.viewScope?.viewMode === "default"; - } - - doRender(): void { - super.doRender(); - this.$widget = $(TPL); - this.$widget.on("click", () => { - const currentOrientation = options.get("splitEditorOrientation"); - options.save("splitEditorOrientation", toggleOrientation(currentOrientation)); - }); - this.#adjustIcon(); - this.contentSized(); - } - - #adjustIcon() { - const currentOrientation = options.get("splitEditorOrientation"); - const upcomingOrientation = toggleOrientation(currentOrientation); - const $icon = this.$widget.find("span.bx"); - $icon - .toggleClass("bxs-dock-bottom", upcomingOrientation === "vertical") - .toggleClass("bxs-dock-left", upcomingOrientation === "horizontal"); - - if (upcomingOrientation === "vertical") { - this.$widget.attr("title", t("switch_layout_button.title_vertical")); - } else { - this.$widget.attr("title", t("switch_layout_button.title_horizontal")); - } - } - - entitiesReloadedEvent({ loadResults }: EventData<"entitiesReloaded">) { - if (loadResults.isOptionReloaded("splitEditorOrientation")) { - this.#adjustIcon(); - } - } - -} - -function toggleOrientation(orientation: string) { - if (orientation === "horizontal") { - return "vertical"; - } else { - return "horizontal"; - } -} From e290635ba5fc63ddf71eb1e94af021c01ac4d8d6 Mon Sep 17 00:00:00 2001 From: Elian Doran Date: Wed, 27 Aug 2025 22:09:00 +0300 Subject: [PATCH 05/28] feat(react/floating_buttons): port toggle read only button --- apps/client/src/widgets/FloatingButtons.tsx | 33 +++++++++-- .../toggle_read_only_button.ts | 58 ------------------- 2 files changed, 29 insertions(+), 62 deletions(-) delete mode 100644 apps/client/src/widgets/floating_buttons/toggle_read_only_button.ts diff --git a/apps/client/src/widgets/FloatingButtons.tsx b/apps/client/src/widgets/FloatingButtons.tsx index 04936d1fe..60ef54a74 100644 --- a/apps/client/src/widgets/FloatingButtons.tsx +++ b/apps/client/src/widgets/FloatingButtons.tsx @@ -4,11 +4,12 @@ import Button from "./react/Button"; import ActionButton from "./react/ActionButton"; import FNote from "../entities/fnote"; import NoteContext from "../components/note_context"; -import { useNoteContext, useNoteLabel, useNoteLabelBoolean, useTriliumOption } from "./react/hooks"; -import { useContext, useEffect, useMemo } from "preact/hooks"; +import { useNoteContext, useNoteLabel, useNoteLabelBoolean, useTriliumEvent, useTriliumOption, useTriliumOptionBool } from "./react/hooks"; +import { useContext, useEffect, useMemo, useState } from "preact/hooks"; import { ParentComponent } from "./react/react_utils"; import Component from "../components/component"; import { VNode } from "preact"; +import attributes from "../services/attributes"; interface FloatingButtonContext { parentComponent: Component; @@ -29,6 +30,13 @@ const FLOATING_BUTTON_DEFINITIONS: FloatingButtonDefinition[] = [ { component: SwitchSplitOrientationButton, isEnabled: ({ note, noteContext }) => note.type === "mermaid" && note.isContentAvailable() && !note.hasLabel("readOnly") && noteContext.viewScope?.viewMode === "default" + }, + { + component: ToggleReadOnlyButton, + isEnabled: ({ note, noteContext }) => + (note.type === "mermaid" || note.getLabelValue("viewType") === "geoMap") + && note.isContentAvailable() + && noteContext.viewScope?.viewMode === "default" } ]; @@ -52,11 +60,18 @@ export default function FloatingButtons() { }; }, [ note, noteContext, parentComponent ]); - const isReadOnly = useNoteLabelBoolean(note, "readOnly"); + // Refresh on any note attribute change. + const [ refreshCounter, setRefreshCounter ] = useState(0); + useTriliumEvent("entitiesReloaded", ({ loadResults }) => { + if (loadResults.getAttributeRows().find(attrRow => attributes.isAffecting(attrRow, note))) { + setRefreshCounter(refreshCounter+1); + } + }); + const definitions = useMemo(() => { if (!context) return []; return FLOATING_BUTTON_DEFINITIONS.filter(def => def.isEnabled(context)); - }, [ context, isReadOnly ]); + }, [ context, refreshCounter ]); return (
@@ -90,6 +105,16 @@ function SwitchSplitOrientationButton({ }: FloatingButtonContext) { /> } +function ToggleReadOnlyButton({ note }: FloatingButtonContext) { + const [ isReadOnly, setReadOnly ] = useNoteLabelBoolean(note, "readOnly"); + + return setReadOnly(!isReadOnly)} + /> +} + /** * Show button that displays floating button after click on close button */ diff --git a/apps/client/src/widgets/floating_buttons/toggle_read_only_button.ts b/apps/client/src/widgets/floating_buttons/toggle_read_only_button.ts deleted file mode 100644 index 571e99017..000000000 --- a/apps/client/src/widgets/floating_buttons/toggle_read_only_button.ts +++ /dev/null @@ -1,58 +0,0 @@ -import type FNote from "../../entities/fnote.js"; -import attributes from "../../services/attributes.js"; -import { t } from "../../services/i18n.js"; -import OnClickButtonWidget from "../buttons/onclick_button.js"; - -export default class ToggleReadOnlyButton extends OnClickButtonWidget { - - private isReadOnly?: boolean; - - constructor() { - super(); - - this - .title(() => this.isReadOnly ? t("toggle_read_only_button.unlock-editing") : t("toggle_read_only_button.lock-editing")) - .titlePlacement("bottom") - .icon(() => this.isReadOnly ? "bx-lock-open-alt" : "bx-lock-alt") - .onClick(() => this.#toggleReadOnly()); - } - - #toggleReadOnly() { - if (!this.noteId || !this.note) { - return; - } - - if (this.isReadOnly) { - attributes.removeOwnedLabelByName(this.note, "readOnly"); - } else { - attributes.setLabel(this.noteId, "readOnly"); - } - } - - async refreshWithNote(note: FNote | null | undefined) { - const isReadOnly = !!note?.hasLabel("readOnly"); - - if (isReadOnly !== this.isReadOnly) { - this.isReadOnly = isReadOnly; - this.refreshIcon(); - } - } - - isEnabled() { - if (!super.isEnabled()) { - return false; - } - - if (!this?.note?.isContentAvailable()) { - return false; - } - - if (this.noteContext?.viewScope?.viewMode !== "default") { - return false; - } - - return this.note.type === "mermaid" || - (this.note.getLabelValue("viewType") === "geoMap"); - } - -} From cdbb89482e0b5f90fada15a7c9f14e874a2dc892 Mon Sep 17 00:00:00 2001 From: Elian Doran Date: Wed, 27 Aug 2025 22:33:36 +0300 Subject: [PATCH 06/28] feat(react/floating_buttons): port edit button --- apps/client/src/widgets/FloatingButtons.tsx | 71 ++++++++++++++-- .../widgets/floating_buttons/edit_button.ts | 84 ------------------- 2 files changed, 63 insertions(+), 92 deletions(-) delete mode 100644 apps/client/src/widgets/floating_buttons/edit_button.ts diff --git a/apps/client/src/widgets/FloatingButtons.tsx b/apps/client/src/widgets/FloatingButtons.tsx index 60ef54a74..358ae2c62 100644 --- a/apps/client/src/widgets/FloatingButtons.tsx +++ b/apps/client/src/widgets/FloatingButtons.tsx @@ -4,12 +4,16 @@ import Button from "./react/Button"; import ActionButton from "./react/ActionButton"; import FNote from "../entities/fnote"; import NoteContext from "../components/note_context"; -import { useNoteContext, useNoteLabel, useNoteLabelBoolean, useTriliumEvent, useTriliumOption, useTriliumOptionBool } from "./react/hooks"; +import { useNoteContext, useNoteLabel, useNoteLabelBoolean, useNoteProperty, useTriliumEvent, useTriliumEvents, useTriliumOption, useTriliumOptionBool } from "./react/hooks"; import { useContext, useEffect, useMemo, useState } from "preact/hooks"; import { ParentComponent } from "./react/react_utils"; import Component from "../components/component"; import { VNode } from "preact"; import attributes from "../services/attributes"; +import appContext from "../components/app_context"; +import protected_session_holder from "../services/protected_session_holder"; +import options from "../services/options"; +import { AttributeRow } from "../services/load_results"; interface FloatingButtonContext { parentComponent: Component; @@ -19,7 +23,7 @@ interface FloatingButtonContext { interface FloatingButtonDefinition { component: (context: FloatingButtonContext) => VNode; - isEnabled: (context: FloatingButtonContext) => boolean; + isEnabled: (context: FloatingButtonContext) => boolean | Promise; } const FLOATING_BUTTON_DEFINITIONS: FloatingButtonDefinition[] = [ @@ -37,9 +41,27 @@ const FLOATING_BUTTON_DEFINITIONS: FloatingButtonDefinition[] = [ (note.type === "mermaid" || note.getLabelValue("viewType") === "geoMap") && note.isContentAvailable() && noteContext.viewScope?.viewMode === "default" + }, + { + component: EditButton, + isEnabled: async ({ note, noteContext }) => + noteContext.viewScope?.viewMode === "default" + && (!note.isProtected || protected_session_holder.isProtectedSessionAvailable()) + && !options.is("databaseReadonly") + && await noteContext?.isReadOnly() } ]; +async function getFloatingButtonDefinitions(context: FloatingButtonContext) { + const defs: FloatingButtonDefinition[] = []; + for (const def of FLOATING_BUTTON_DEFINITIONS) { + if (await def.isEnabled(context)) { + defs.push(def); + } + } + return defs; +} + /* * Note: * @@ -64,15 +86,23 @@ export default function FloatingButtons() { const [ refreshCounter, setRefreshCounter ] = useState(0); useTriliumEvent("entitiesReloaded", ({ loadResults }) => { if (loadResults.getAttributeRows().find(attrRow => attributes.isAffecting(attrRow, note))) { - setRefreshCounter(refreshCounter+1); + setRefreshCounter(refreshCounter + 1); + } + }); + useTriliumEvent("readOnlyTemporarilyDisabled", ({ noteContext: eventNoteContext }) => { + if (noteContext?.ntxId === eventNoteContext.ntxId) { + setRefreshCounter(refreshCounter + 1); } }); - - const definitions = useMemo(() => { - if (!context) return []; - return FLOATING_BUTTON_DEFINITIONS.filter(def => def.isEnabled(context)); - }, [ context, refreshCounter ]); + // Manage the list of items + const noteMime = useNoteProperty(note, "mime"); + const [ definitions, setDefinitions ] = useState([]); + useEffect(() => { + if (!context) return; + getFloatingButtonDefinitions(context).then(setDefinitions); + }, [ context, refreshCounter, noteMime ]); + return (
@@ -115,6 +145,31 @@ function ToggleReadOnlyButton({ note }: FloatingButtonContext) { /> } +function EditButton({ noteContext }: FloatingButtonContext) { + const [ animationClass, setAnimationClass ] = useState(""); + + // make the edit button stand out on the first display, otherwise + // it's difficult to notice that the note is readonly + useEffect(() => { + setAnimationClass("bx-tada bx-lg"); + setTimeout(() => { + setAnimationClass(""); + }, 1700); + }, []); + + return { + if (noteContext.viewScope) { + noteContext.viewScope.readOnlyTemporarilyDisabled = true; + appContext.triggerEvent("readOnlyTemporarilyDisabled", { noteContext }); + } + }} + /> +} + /** * Show button that displays floating button after click on close button */ diff --git a/apps/client/src/widgets/floating_buttons/edit_button.ts b/apps/client/src/widgets/floating_buttons/edit_button.ts deleted file mode 100644 index 344447f31..000000000 --- a/apps/client/src/widgets/floating_buttons/edit_button.ts +++ /dev/null @@ -1,84 +0,0 @@ -import OnClickButtonWidget from "../buttons/onclick_button.js"; -import appContext from "../../components/app_context.js"; -import attributeService from "../../services/attributes.js"; -import protectedSessionHolder from "../../services/protected_session_holder.js"; -import { t } from "../../services/i18n.js"; -import LoadResults from "../../services/load_results.js"; -import type { AttributeRow } from "../../services/load_results.js"; -import FNote from "../../entities/fnote.js"; -import options from "../../services/options.js"; - -export default class EditButton extends OnClickButtonWidget { - isEnabled(): boolean { - return Boolean(super.isEnabled() && this.note && this.noteContext?.viewScope?.viewMode === "default"); - } - - constructor() { - super(); - - this.icon("bx-pencil") - .title(t("edit_button.edit_this_note")) - .titlePlacement("bottom") - .onClick((widget) => { - if (this.noteContext?.viewScope) { - this.noteContext.viewScope.readOnlyTemporarilyDisabled = true; - appContext.triggerEvent("readOnlyTemporarilyDisabled", { noteContext: this.noteContext }); - } - }); - } - - async refreshWithNote(note: FNote): Promise { - if (options.is("databaseReadonly")) { - this.toggleInt(false); - return; - } - if (note.isProtected && !protectedSessionHolder.isProtectedSessionAvailable()) { - this.toggleInt(false); - } else { - // prevent flickering by assuming hidden before async operation - this.toggleInt(false); - - const wasVisible = this.isVisible(); - - // can't do this in isEnabled() since isReadOnly is async - const isReadOnly = await this.noteContext?.isReadOnly(); - this.toggleInt(Boolean(isReadOnly)); - - // make the edit button stand out on the first display, otherwise - // it's difficult to notice that the note is readonly - if (this.isVisible() && !wasVisible && this.$widget) { - this.$widget.addClass("bx-tada bx-lg"); - - setTimeout(() => { - this.$widget?.removeClass("bx-tada bx-lg"); - }, 1700); - } - } - - await super.refreshWithNote(note); - } - - entitiesReloadedEvent({ loadResults }: { loadResults: LoadResults }): void { - if (loadResults.getAttributeRows().find((attr: AttributeRow) => - attr.type === "label" && - attr.name?.toLowerCase().includes("readonly") && - this.note && - attributeService.isAffecting(attr, this.note) - )) { - if (this.noteContext?.viewScope) { - this.noteContext.viewScope.readOnlyTemporarilyDisabled = false; - } - this.refresh(); - } - } - - readOnlyTemporarilyDisabledEvent() { - this.refresh(); - } - - async noteTypeMimeChangedEvent({ noteId }: { noteId: string }): Promise { - if (this.isNote(noteId)) { - await this.refresh(); - } - } -} From 53e0c05290729e0e15283ebd56820517c6c23366 Mon Sep 17 00:00:00 2001 From: Elian Doran Date: Wed, 27 Aug 2025 22:44:11 +0300 Subject: [PATCH 07/28] feat(react/floating_buttons): port toc --- apps/client/src/widgets/FloatingButtons.tsx | 25 ++++++++ .../widgets/buttons/show_toc_widget_button.ts | 62 ------------------- apps/client/src/widgets/toc.ts | 1 + 3 files changed, 26 insertions(+), 62 deletions(-) delete mode 100644 apps/client/src/widgets/buttons/show_toc_widget_button.ts diff --git a/apps/client/src/widgets/FloatingButtons.tsx b/apps/client/src/widgets/FloatingButtons.tsx index 358ae2c62..fff37c785 100644 --- a/apps/client/src/widgets/FloatingButtons.tsx +++ b/apps/client/src/widgets/FloatingButtons.tsx @@ -49,6 +49,13 @@ const FLOATING_BUTTON_DEFINITIONS: FloatingButtonDefinition[] = [ && (!note.isProtected || protected_session_holder.isProtectedSessionAvailable()) && !options.is("databaseReadonly") && await noteContext?.isReadOnly() + }, + { + component: ShowTocWidgetButton, + isEnabled: ({ note, noteContext }) => + note.type === "text" + && noteContext?.viewScope?.viewMode === "default" + && !!noteContext.viewScope?.tocTemporarilyHidden } ]; @@ -94,6 +101,11 @@ export default function FloatingButtons() { setRefreshCounter(refreshCounter + 1); } }); + useTriliumEvent("reEvaluateTocWidgetVisibility", ({ noteId }) => { + if (noteId === note?.noteId) { + setRefreshCounter(refreshCounter + 1); + } + }); // Manage the list of items const noteMime = useNoteProperty(note, "mime"); @@ -170,6 +182,19 @@ function EditButton({ noteContext }: FloatingButtonContext) { /> } +function ShowTocWidgetButton({ noteContext }: FloatingButtonContext) { + return { + if (noteContext?.viewScope && noteContext.noteId) { + noteContext.viewScope.tocTemporarilyHidden = false; + appContext.triggerEvent("showTocWidget", { noteId: noteContext.noteId }); + } + }} + /> +} + /** * Show button that displays floating button after click on close button */ diff --git a/apps/client/src/widgets/buttons/show_toc_widget_button.ts b/apps/client/src/widgets/buttons/show_toc_widget_button.ts deleted file mode 100644 index 8a4a8c851..000000000 --- a/apps/client/src/widgets/buttons/show_toc_widget_button.ts +++ /dev/null @@ -1,62 +0,0 @@ -import OnClickButtonWidget from "./onclick_button.js"; -import appContext from "../../components/app_context.js"; -import attributeService from "../../services/attributes.js"; -import { t } from "../../services/i18n.js"; -import LoadResults from "../../services/load_results.js"; -import type { AttributeRow } from "../../services/load_results.js"; - -export default class ShowTocWidgetButton extends OnClickButtonWidget { - isEnabled(): boolean { - return Boolean(super.isEnabled() && this.note && this.note.type === "text" && this.noteContext?.viewScope?.viewMode === "default"); - } - - constructor() { - super(); - - this.icon("bx-tn-toc") - .title(t("show_toc_widget_button.show_toc")) - .titlePlacement("bottom") - .onClick(() => { - if (this.noteContext?.viewScope && this.noteId) { - this.noteContext.viewScope.tocTemporarilyHidden = false; - appContext.triggerEvent("showTocWidget", { noteId: this.noteId }); - } - this.toggleInt(false); - }); - } - - async refreshWithNote(): Promise { - if (this.noteContext?.viewScope) { - this.toggleInt(this.noteContext.viewScope.tocTemporarilyHidden); - } - } - - async reEvaluateTocWidgetVisibilityEvent({ noteId }: { noteId: string }): Promise { - if (noteId === this.noteId) { - await this.refresh(); - } - } - - async entitiesReloadedEvent({ loadResults }: { loadResults: LoadResults }): Promise { - if (this.noteId && loadResults.isNoteContentReloaded(this.noteId)) { - await this.refresh(); - } else if ( - loadResults - .getAttributeRows() - .find((attr: AttributeRow) => - attr.type === "label" && - (attr.name?.toLowerCase().includes("readonly") || attr.name === "toc") && - this.note && - attributeService.isAffecting(attr, this.note) - ) - ) { - await this.refresh(); - } - } - - async noteTypeMimeChangedEvent({ noteId }: { noteId: string }): Promise { - if (this.isNote(noteId)) { - await this.refresh(); - } - } -} diff --git a/apps/client/src/widgets/toc.ts b/apps/client/src/widgets/toc.ts index a5db7ff1b..e49368283 100644 --- a/apps/client/src/widgets/toc.ts +++ b/apps/client/src/widgets/toc.ts @@ -475,6 +475,7 @@ export default class TocWidget extends RightPanelWidget { if (this.noteId === noteId) { await this.refresh(); this.triggerCommand("reEvaluateRightPaneVisibility"); + appContext.triggerEvent("reEvaluateTocWidgetVisibility", { noteId: this.noteId }); } } From 401260d3ca04083166df05108fe593652f7ee242 Mon Sep 17 00:00:00 2001 From: Elian Doran Date: Wed, 27 Aug 2025 22:47:20 +0300 Subject: [PATCH 08/28] feat(react/floating_buttons): port highlights list --- apps/client/src/widgets/FloatingButtons.tsx | 24 ++++++- .../show_highlights_list_widget_button.ts | 62 ------------------- apps/client/src/widgets/highlights_list.ts | 1 + 3 files changed, 22 insertions(+), 65 deletions(-) delete mode 100644 apps/client/src/widgets/buttons/show_highlights_list_widget_button.ts diff --git a/apps/client/src/widgets/FloatingButtons.tsx b/apps/client/src/widgets/FloatingButtons.tsx index fff37c785..0e1bfc4af 100644 --- a/apps/client/src/widgets/FloatingButtons.tsx +++ b/apps/client/src/widgets/FloatingButtons.tsx @@ -53,9 +53,14 @@ const FLOATING_BUTTON_DEFINITIONS: FloatingButtonDefinition[] = [ { component: ShowTocWidgetButton, isEnabled: ({ note, noteContext }) => - note.type === "text" - && noteContext?.viewScope?.viewMode === "default" + note.type === "text" && noteContext?.viewScope?.viewMode === "default" && !!noteContext.viewScope?.tocTemporarilyHidden + }, + { + component: ShowHighlightsListWidgetButton, + isEnabled: ({ note, noteContext }) => + note.type === "text" && noteContext?.viewScope?.viewMode === "default" + && !!noteContext.viewScope?.highlightsListTemporarilyHidden } ]; @@ -101,7 +106,7 @@ export default function FloatingButtons() { setRefreshCounter(refreshCounter + 1); } }); - useTriliumEvent("reEvaluateTocWidgetVisibility", ({ noteId }) => { + useTriliumEvents(["reEvaluateTocWidgetVisibility", "reEvaluateHighlightsListWidgetVisibility"], ({ noteId }) => { if (noteId === note?.noteId) { setRefreshCounter(refreshCounter + 1); } @@ -195,6 +200,19 @@ function ShowTocWidgetButton({ noteContext }: FloatingButtonContext) { /> } +function ShowHighlightsListWidgetButton({ noteContext }: FloatingButtonContext) { + return { + if (noteContext?.viewScope && noteContext.noteId) { + noteContext.viewScope.highlightsListTemporarilyHidden = false; + appContext.triggerEvent("showHighlightsListWidget", { noteId: noteContext.noteId }); + } + }} + /> +} + /** * Show button that displays floating button after click on close button */ diff --git a/apps/client/src/widgets/buttons/show_highlights_list_widget_button.ts b/apps/client/src/widgets/buttons/show_highlights_list_widget_button.ts deleted file mode 100644 index b5c641a07..000000000 --- a/apps/client/src/widgets/buttons/show_highlights_list_widget_button.ts +++ /dev/null @@ -1,62 +0,0 @@ -import OnClickButtonWidget from "./onclick_button.js"; -import appContext from "../../components/app_context.js"; -import attributeService from "../../services/attributes.js"; -import { t } from "../../services/i18n.js"; -import LoadResults from "../../services/load_results.js"; -import type { AttributeRow } from "../../services/load_results.js"; - -export default class ShowHighlightsListWidgetButton extends OnClickButtonWidget { - isEnabled(): boolean { - return Boolean(super.isEnabled() && this.note && this.note.type === "text" && this.noteContext?.viewScope?.viewMode === "default"); - } - - constructor() { - super(); - - this.icon("bx-bookmarks") - .title(t("show_highlights_list_widget_button.show_highlights_list")) - .titlePlacement("bottom") - .onClick(() => { - if (this.noteContext?.viewScope && this.noteId) { - this.noteContext.viewScope.highlightsListTemporarilyHidden = false; - appContext.triggerEvent("showHighlightsListWidget", { noteId: this.noteId }); - } - this.toggleInt(false); - }); - } - - async refreshWithNote(): Promise { - if (this.noteContext?.viewScope) { - this.toggleInt(this.noteContext.viewScope.highlightsListTemporarilyHidden); - } - } - - async reEvaluateHighlightsListWidgetVisibilityEvent({ noteId }: { noteId: string }): Promise { - if (noteId === this.noteId) { - await this.refresh(); - } - } - - async entitiesReloadedEvent({ loadResults }: { loadResults: LoadResults }): Promise { - if (this.noteId && loadResults.isNoteContentReloaded(this.noteId)) { - await this.refresh(); - } else if ( - loadResults - .getAttributeRows() - .find((attr: AttributeRow) => - attr.type === "label" && - (attr.name?.toLowerCase().includes("readonly") || attr.name === "hideHighlightWidget") && - this.note && - attributeService.isAffecting(attr, this.note) - ) - ) { - await this.refresh(); - } - } - - async noteTypeMimeChangedEvent({ noteId }: { noteId: string }): Promise { - if (this.isNote(noteId)) { - await this.refresh(); - } - } -} diff --git a/apps/client/src/widgets/highlights_list.ts b/apps/client/src/widgets/highlights_list.ts index 38a736f7e..c9843a479 100644 --- a/apps/client/src/widgets/highlights_list.ts +++ b/apps/client/src/widgets/highlights_list.ts @@ -375,6 +375,7 @@ export default class HighlightsListWidget extends RightPanelWidget { if (this.noteId === noteId) { await this.refresh(); this.triggerCommand("reEvaluateRightPaneVisibility"); + appContext.triggerEvent("reEvaluateHighlightsListWidgetVisibility", { noteId: this.noteId }); } } From 04b7e0cde9d24e29014cac03594e6e9434a006b1 Mon Sep 17 00:00:00 2001 From: Elian Doran Date: Wed, 27 Aug 2025 22:54:05 +0300 Subject: [PATCH 09/28] feat(react/floating_buttons): port execute note button --- apps/client/src/widgets/FloatingButtons.tsx | 12 ++++++++++++ .../src/widgets/floating_buttons/code_buttons.ts | 7 ------- 2 files changed, 12 insertions(+), 7 deletions(-) diff --git a/apps/client/src/widgets/FloatingButtons.tsx b/apps/client/src/widgets/FloatingButtons.tsx index 0e1bfc4af..5ecae1b15 100644 --- a/apps/client/src/widgets/FloatingButtons.tsx +++ b/apps/client/src/widgets/FloatingButtons.tsx @@ -61,6 +61,10 @@ const FLOATING_BUTTON_DEFINITIONS: FloatingButtonDefinition[] = [ isEnabled: ({ note, noteContext }) => note.type === "text" && noteContext?.viewScope?.viewMode === "default" && !!noteContext.viewScope?.highlightsListTemporarilyHidden + }, + { + component: RunActiveNoteButton, + isEnabled: ({ note }) => note.mime.startsWith("application/javascript") || note.mime === "text/x-sqlite;schema=trilium" } ]; @@ -213,6 +217,14 @@ function ShowHighlightsListWidgetButton({ noteContext }: FloatingButtonContext) /> } +function RunActiveNoteButton() { + return +} + /** * Show button that displays floating button after click on close button */ diff --git a/apps/client/src/widgets/floating_buttons/code_buttons.ts b/apps/client/src/widgets/floating_buttons/code_buttons.ts index 63b0d92bb..712d1d9ee 100644 --- a/apps/client/src/widgets/floating_buttons/code_buttons.ts +++ b/apps/client/src/widgets/floating_buttons/code_buttons.ts @@ -17,10 +17,6 @@ const TPL = /*html*/` } - - @@ -38,7 +34,6 @@ interface SaveSqlConsoleResponse { export default class CodeButtonsWidget extends NoteContextAwareWidget { private $openTriliumApiDocsButton!: JQuery; - private $executeButton!: JQuery; private $saveToNoteButton!: JQuery; isEnabled() { @@ -78,8 +73,6 @@ export default class CodeButtonsWidget extends NoteContextAwareWidget { } async refreshWithNote(note: FNote) { - this.$executeButton.toggle(note.mime.startsWith("application/javascript") || note.mime === "text/x-sqlite;schema=trilium"); - this.$saveToNoteButton.toggle(note.mime === "text/x-sqlite;schema=trilium" && note.isHiddenCompletely()); this.$openTriliumApiDocsButton.toggle(note.mime.startsWith("application/javascript;env=")); From 08db03800e172639aa122ea752172ee233cccf98 Mon Sep 17 00:00:00 2001 From: Elian Doran Date: Wed, 27 Aug 2025 22:59:07 +0300 Subject: [PATCH 10/28] feat(react/floating_buttons): port open Trilium API docs --- apps/client/src/widgets/FloatingButtons.tsx | 13 +++++++++++++ .../src/widgets/floating_buttons/code_buttons.ts | 16 ---------------- 2 files changed, 13 insertions(+), 16 deletions(-) diff --git a/apps/client/src/widgets/FloatingButtons.tsx b/apps/client/src/widgets/FloatingButtons.tsx index 5ecae1b15..a2f3769fd 100644 --- a/apps/client/src/widgets/FloatingButtons.tsx +++ b/apps/client/src/widgets/FloatingButtons.tsx @@ -14,6 +14,7 @@ import appContext from "../components/app_context"; import protected_session_holder from "../services/protected_session_holder"; import options from "../services/options"; import { AttributeRow } from "../services/load_results"; +import { openInAppHelpFromUrl } from "../services/utils"; interface FloatingButtonContext { parentComponent: Component; @@ -65,6 +66,10 @@ const FLOATING_BUTTON_DEFINITIONS: FloatingButtonDefinition[] = [ { component: RunActiveNoteButton, isEnabled: ({ note }) => note.mime.startsWith("application/javascript") || note.mime === "text/x-sqlite;schema=trilium" + }, + { + component: OpenTriliumApiDocsButton, + isEnabled: ({ note }) => note.mime.startsWith("application/javascript;env=") } ]; @@ -225,6 +230,14 @@ function RunActiveNoteButton() { /> } +function OpenTriliumApiDocsButton({ note }: FloatingButtonContext) { + return openInAppHelpFromUrl(note.mime.endsWith("frontend") ? "Q2z6av6JZVWm" : "MEtfsqa5VwNi")} + /> +} + /** * Show button that displays floating button after click on close button */ diff --git a/apps/client/src/widgets/floating_buttons/code_buttons.ts b/apps/client/src/widgets/floating_buttons/code_buttons.ts index 712d1d9ee..c64fe7183 100644 --- a/apps/client/src/widgets/floating_buttons/code_buttons.ts +++ b/apps/client/src/widgets/floating_buttons/code_buttons.ts @@ -17,10 +17,6 @@ const TPL = /*html*/` } - - @@ -42,16 +38,6 @@ export default class CodeButtonsWidget extends NoteContextAwareWidget { doRender() { this.$widget = $(TPL); - this.$openTriliumApiDocsButton = this.$widget.find(".trilium-api-docs-button"); - this.$openTriliumApiDocsButton.on("click", () => { - toastService.showMessage(t("code_buttons.opening_api_docs_message")); - - if (this.note?.mime.endsWith("frontend")) { - window.open("https://triliumnext.github.io/Notes/Script%20API/interfaces/Frontend_Script_API.Api.html", "_blank"); - } else { - window.open("https://triliumnext.github.io/Notes/Script%20API/interfaces/Backend_Script_API.Api.html", "_blank"); - } - }); this.$executeButton = this.$widget.find(".execute-button"); this.$saveToNoteButton = this.$widget.find(".save-to-note-button"); @@ -74,8 +60,6 @@ export default class CodeButtonsWidget extends NoteContextAwareWidget { async refreshWithNote(note: FNote) { this.$saveToNoteButton.toggle(note.mime === "text/x-sqlite;schema=trilium" && note.isHiddenCompletely()); - - this.$openTriliumApiDocsButton.toggle(note.mime.startsWith("application/javascript;env=")); } async noteTypeMimeChangedEvent({ noteId }: EventData<"noteTypeMimeChanged">) { From 2085d1bbba2727cab5ecb52d380f09bac2abaf1b Mon Sep 17 00:00:00 2001 From: Elian Doran Date: Wed, 27 Aug 2025 23:14:25 +0300 Subject: [PATCH 11/28] feat(react/floating_buttons): port save to note button --- apps/client/src/widgets/FloatingButtons.tsx | 26 ++++++++++++++++++- .../widgets/floating_buttons/code_buttons.ts | 23 +--------------- apps/server/src/services/special_notes.ts | 4 +-- packages/commons/src/lib/server_api.ts | 2 ++ 4 files changed, 30 insertions(+), 25 deletions(-) diff --git a/apps/client/src/widgets/FloatingButtons.tsx b/apps/client/src/widgets/FloatingButtons.tsx index a2f3769fd..0f36c8a49 100644 --- a/apps/client/src/widgets/FloatingButtons.tsx +++ b/apps/client/src/widgets/FloatingButtons.tsx @@ -13,8 +13,11 @@ import attributes from "../services/attributes"; import appContext from "../components/app_context"; import protected_session_holder from "../services/protected_session_holder"; import options from "../services/options"; -import { AttributeRow } from "../services/load_results"; import { openInAppHelpFromUrl } from "../services/utils"; +import toast from "../services/toast"; +import server from "../services/server"; +import { SaveSqlConsoleResponse } from "@triliumnext/commons"; +import tree from "../services/tree"; interface FloatingButtonContext { parentComponent: Component; @@ -70,6 +73,10 @@ const FLOATING_BUTTON_DEFINITIONS: FloatingButtonDefinition[] = [ { component: OpenTriliumApiDocsButton, isEnabled: ({ note }) => note.mime.startsWith("application/javascript;env=") + }, + { + component: SaveToNoteButton, + isEnabled: ({ note }) => note.mime === "text/x-sqlite;schema=trilium" && note.isHiddenCompletely() } ]; @@ -238,6 +245,23 @@ function OpenTriliumApiDocsButton({ note }: FloatingButtonContext) { /> } +function SaveToNoteButton({ note }: FloatingButtonContext) { + return { + e.preventDefault(); + const { notePath } = await server.post("special-notes/save-sql-console", { sqlConsoleNoteId: note.noteId }); + if (notePath) { + toast.showMessage(t("code_buttons.sql_console_saved_message", { "note_path": await tree.getNotePathTitle(notePath) })); + // TODO: This hangs the navigation, for some reason. + //await ws.waitForMaxKnownEntityChangeId(); + await appContext.tabManager.getActiveContext()?.setNote(notePath); + } + }} + /> +} + /** * Show button that displays floating button after click on close button */ diff --git a/apps/client/src/widgets/floating_buttons/code_buttons.ts b/apps/client/src/widgets/floating_buttons/code_buttons.ts index c64fe7183..aab467c9d 100644 --- a/apps/client/src/widgets/floating_buttons/code_buttons.ts +++ b/apps/client/src/widgets/floating_buttons/code_buttons.ts @@ -17,16 +17,10 @@ const TPL = /*html*/` } -
`; -// TODO: Deduplicate with server. -interface SaveSqlConsoleResponse { - notePath: string; -} - export default class CodeButtonsWidget extends NoteContextAwareWidget { private $openTriliumApiDocsButton!: JQuery; @@ -42,13 +36,7 @@ export default class CodeButtonsWidget extends NoteContextAwareWidget { this.$executeButton = this.$widget.find(".execute-button"); this.$saveToNoteButton = this.$widget.find(".save-to-note-button"); this.$saveToNoteButton.on("click", async () => { - const { notePath } = await server.post("special-notes/save-sql-console", { sqlConsoleNoteId: this.noteId }); - await ws.waitForMaxKnownEntityChangeId(); - - await appContext.tabManager.getActiveContext()?.setNote(notePath); - - toastService.showMessage(t("code_buttons.sql_console_saved_message", { notePath: await treeService.getNotePathTitle(notePath) })); }); keyboardActionService.updateDisplayedShortcuts(this.$widget); @@ -58,13 +46,4 @@ export default class CodeButtonsWidget extends NoteContextAwareWidget { super.doRender(); } - async refreshWithNote(note: FNote) { - this.$saveToNoteButton.toggle(note.mime === "text/x-sqlite;schema=trilium" && note.isHiddenCompletely()); - } - - async noteTypeMimeChangedEvent({ noteId }: EventData<"noteTypeMimeChanged">) { - if (this.isNote(noteId)) { - await this.refresh(); - } - } } diff --git a/apps/server/src/services/special_notes.ts b/apps/server/src/services/special_notes.ts index d4c37d764..4f6d76781 100644 --- a/apps/server/src/services/special_notes.ts +++ b/apps/server/src/services/special_notes.ts @@ -10,7 +10,7 @@ import SearchContext from "./search/search_context.js"; import { LBTPL_NOTE_LAUNCHER, LBTPL_CUSTOM_WIDGET, LBTPL_SPACER, LBTPL_SCRIPT } from "./hidden_subtree.js"; import { t } from "i18next"; import { BNote } from "./backend_script_entrypoint.js"; -import { SaveSearchNoteResponse } from "@triliumnext/commons"; +import { SaveSearchNoteResponse, SaveSqlConsoleResponse } from "@triliumnext/commons"; function getInboxNote(date: string) { const workspaceNote = hoistedNoteService.getWorkspaceNote(); @@ -67,7 +67,7 @@ async function saveSqlConsole(sqlConsoleNoteId: string) { } } - return result; + return result satisfies SaveSqlConsoleResponse; } function createSearchNote(searchString: string, ancestorNoteId: string) { diff --git a/packages/commons/src/lib/server_api.ts b/packages/commons/src/lib/server_api.ts index 966e34418..0a9d36227 100644 --- a/packages/commons/src/lib/server_api.ts +++ b/packages/commons/src/lib/server_api.ts @@ -206,3 +206,5 @@ export interface CloneResponse { export interface ConvertToAttachmentResponse { attachment: AttachmentRow; } + +export type SaveSqlConsoleResponse = CloneResponse; From 28605f26874d6b7daec3099662aedf480039ec2e Mon Sep 17 00:00:00 2001 From: Elian Doran Date: Wed, 27 Aug 2025 23:20:19 +0300 Subject: [PATCH 12/28] feat(react/floating_buttons): fancy title + keyboard shortcut --- .../widgets/floating_buttons/code_buttons.ts | 49 ------------------- .../client/src/widgets/react/ActionButton.tsx | 24 +++++++-- 2 files changed, 21 insertions(+), 52 deletions(-) delete mode 100644 apps/client/src/widgets/floating_buttons/code_buttons.ts diff --git a/apps/client/src/widgets/floating_buttons/code_buttons.ts b/apps/client/src/widgets/floating_buttons/code_buttons.ts deleted file mode 100644 index aab467c9d..000000000 --- a/apps/client/src/widgets/floating_buttons/code_buttons.ts +++ /dev/null @@ -1,49 +0,0 @@ -import { t } from "../../services/i18n.js"; -import server from "../../services/server.js"; -import ws from "../../services/ws.js"; -import appContext, { type EventData } from "../../components/app_context.js"; -import toastService from "../../services/toast.js"; -import treeService from "../../services/tree.js"; -import NoteContextAwareWidget from "../note_context_aware_widget.js"; -import keyboardActionService from "../../services/keyboard_actions.js"; -import type FNote from "../../entities/fnote.js"; - -const TPL = /*html*/` -
- - - -
`; - -export default class CodeButtonsWidget extends NoteContextAwareWidget { - - private $openTriliumApiDocsButton!: JQuery; - private $saveToNoteButton!: JQuery; - - isEnabled() { - return super.isEnabled() && this.note && (this.note.mime.startsWith("application/javascript") || this.note.mime === "text/x-sqlite;schema=trilium"); - } - - doRender() { - this.$widget = $(TPL); - - this.$executeButton = this.$widget.find(".execute-button"); - this.$saveToNoteButton = this.$widget.find(".save-to-note-button"); - this.$saveToNoteButton.on("click", async () => { - - }); - - keyboardActionService.updateDisplayedShortcuts(this.$widget); - - this.contentSized(); - - super.doRender(); - } - -} diff --git a/apps/client/src/widgets/react/ActionButton.tsx b/apps/client/src/widgets/react/ActionButton.tsx index 9fa0e69b4..5636372d8 100644 --- a/apps/client/src/widgets/react/ActionButton.tsx +++ b/apps/client/src/widgets/react/ActionButton.tsx @@ -1,18 +1,36 @@ +import { useEffect, useRef, useState } from "preact/hooks"; import { CommandNames } from "../../components/app_context"; +import { useStaticTooltip } from "./hooks"; +import keyboard_actions from "../../services/keyboard_actions"; interface ActionButtonProps { text: string; - titlePosition?: "bottom"; // TODO: Use it + titlePosition?: "bottom" | "left"; // TODO: Use it icon: string; className?: string; onClick?: (e: MouseEvent) => void; triggerCommand?: CommandNames; } -export default function ActionButton({ text, icon, className, onClick, triggerCommand }: ActionButtonProps) { +export default function ActionButton({ text, icon, className, onClick, triggerCommand, titlePosition }: ActionButtonProps) { + const buttonRef = useRef(null); + const [ keyboardShortcut, setKeyboardShortcut ] = useState(); + + useStaticTooltip(buttonRef, { + title: keyboardShortcut?.length ? `${text} (${keyboardShortcut?.join(",")})` : text, + placement: titlePosition ?? "bottom", + fallbackPlacements: [ titlePosition ?? "bottom" ] + }); + + useEffect(() => { + if (triggerCommand) { + keyboard_actions.getAction(triggerCommand).then(action => setKeyboardShortcut(action?.effectiveShortcuts)); + } + }, [triggerCommand]); + return
); -} \ No newline at end of file +} diff --git a/apps/client/src/widgets/floating_buttons/relation_map_buttons.ts b/apps/client/src/widgets/floating_buttons/relation_map_buttons.ts deleted file mode 100644 index 141c42e16..000000000 --- a/apps/client/src/widgets/floating_buttons/relation_map_buttons.ts +++ /dev/null @@ -1,60 +0,0 @@ -import { t } from "../../services/i18n.js"; -import NoteContextAwareWidget from "../note_context_aware_widget.js"; - -const TPL = /*html*/` -
- - - - - - -
- - - -
-
`; - -export default class RelationMapButtons extends NoteContextAwareWidget { - - private $createChildNote!: JQuery; - private $zoomInButton!: JQuery; - private $zoomOutButton!: JQuery; - private $resetPanZoomButton!: JQuery; - - isEnabled() { - return super.isEnabled() && this.note?.type === "relationMap"; - } - - doRender() { - super.doRender(); - - this.$widget = $(TPL); - this.$createChildNote = this.$widget.find(".relation-map-create-child-note"); - this.$zoomInButton = this.$widget.find(".relation-map-zoom-in"); - this.$zoomOutButton = this.$widget.find(".relation-map-zoom-out"); - this.$resetPanZoomButton = this.$widget.find(".relation-map-reset-pan-zoom"); - - // TODO: Deduplicate object creation here. - this.$createChildNote.on("click", () => this.triggerEvent("relationMapCreateChildNote", { ntxId: this.ntxId })); - this.$resetPanZoomButton.on("click", () => this.triggerEvent("relationMapResetPanZoom", { ntxId: this.ntxId })); - - this.$zoomInButton.on("click", () => this.triggerEvent("relationMapResetZoomIn", { ntxId: this.ntxId })); - this.$zoomOutButton.on("click", () => this.triggerEvent("relationMapResetZoomOut", { ntxId: this.ntxId })); - this.contentSized(); - } -} From 40bfd827d2173bd9a21b62a8b50e88ea0ee9b5c2 Mon Sep 17 00:00:00 2001 From: Elian Doran Date: Wed, 27 Aug 2025 23:36:50 +0300 Subject: [PATCH 14/28] chore(react/floating_buttons): improve sizing --- apps/client/src/widgets/FloatingButtons.tsx | 36 +++++++++++-------- .../client/src/widgets/react/ActionButton.tsx | 7 ++-- 2 files changed, 26 insertions(+), 17 deletions(-) diff --git a/apps/client/src/widgets/FloatingButtons.tsx b/apps/client/src/widgets/FloatingButtons.tsx index 9528ede08..af66571d8 100644 --- a/apps/client/src/widgets/FloatingButtons.tsx +++ b/apps/client/src/widgets/FloatingButtons.tsx @@ -1,7 +1,7 @@ import { t } from "i18next"; import "./FloatingButtons.css"; import Button from "./react/Button"; -import ActionButton from "./react/ActionButton"; +import ActionButton, { ActionButtonProps } from "./react/ActionButton"; import FNote from "../entities/fnote"; import NoteContext from "../components/note_context"; import { useNoteContext, useNoteLabel, useNoteLabelBoolean, useNoteProperty, useTriliumEvent, useTriliumEvents, useTriliumOption, useTriliumOptionBool } from "./react/hooks"; @@ -154,7 +154,7 @@ export default function FloatingButtons() { } function RefreshBackendLogButton({ parentComponent, noteContext }: FloatingButtonContext) { - return parentComponent.triggerEvent("refreshData", { ntxId: noteContext.ntxId })} @@ -165,7 +165,7 @@ function SwitchSplitOrientationButton({ }: FloatingButtonContext) { const [ splitEditorOrientation, setSplitEditorOrientation ] = useTriliumOption("splitEditorOrientation"); const upcomingOrientation = splitEditorOrientation === "horizontal" ? "vertical" : "horizontal"; - return setSplitEditorOrientation(upcomingOrientation)} @@ -175,7 +175,7 @@ function SwitchSplitOrientationButton({ }: FloatingButtonContext) { function ToggleReadOnlyButton({ note }: FloatingButtonContext) { const [ isReadOnly, setReadOnly ] = useNoteLabelBoolean(note, "readOnly"); - return setReadOnly(!isReadOnly)} @@ -194,7 +194,7 @@ function EditButton({ noteContext }: FloatingButtonContext) { }, 1700); }, []); - return { @@ -221,7 +221,7 @@ function ShowTocWidgetButton({ noteContext }: FloatingButtonContext) { } function ShowHighlightsListWidgetButton({ noteContext }: FloatingButtonContext) { - return { @@ -234,7 +234,7 @@ function ShowHighlightsListWidgetButton({ noteContext }: FloatingButtonContext) } function RunActiveNoteButton() { - return openInAppHelpFromUrl(note.mime.endsWith("frontend") ? "Q2z6av6JZVWm" : "MEtfsqa5VwNi")} @@ -250,7 +250,7 @@ function OpenTriliumApiDocsButton({ note }: FloatingButtonContext) { } function SaveToNoteButton({ note }: FloatingButtonContext) { - return { @@ -269,26 +269,26 @@ function SaveToNoteButton({ note }: FloatingButtonContext) { function RelationMapButtons({ parentComponent, noteContext }: FloatingButtonContext) { return ( <> - parentComponent.triggerEvent("relationMapCreateChildNote", { ntxId: noteContext.ntxId })} /> - parentComponent.triggerEvent("relationMapResetPanZoom", { ntxId: noteContext.ntxId })} />
- parentComponent.triggerEvent("relationMapResetZoomIn", { ntxId: noteContext.ntxId })} /> - parentComponent.triggerEvent("relationMapResetZoomOut", { ntxId: noteContext.ntxId })} @@ -298,6 +298,14 @@ function RelationMapButtons({ parentComponent, noteContext }: FloatingButtonCont ) } +function FloatingButton({ className, ...props }: ActionButtonProps) { + return +} + /** * Show button that displays floating button after click on close button */ diff --git a/apps/client/src/widgets/react/ActionButton.tsx b/apps/client/src/widgets/react/ActionButton.tsx index 5636372d8..c97aa01ca 100644 --- a/apps/client/src/widgets/react/ActionButton.tsx +++ b/apps/client/src/widgets/react/ActionButton.tsx @@ -3,16 +3,17 @@ import { CommandNames } from "../../components/app_context"; import { useStaticTooltip } from "./hooks"; import keyboard_actions from "../../services/keyboard_actions"; -interface ActionButtonProps { +export interface ActionButtonProps { text: string; titlePosition?: "bottom" | "left"; // TODO: Use it icon: string; className?: string; onClick?: (e: MouseEvent) => void; triggerCommand?: CommandNames; + noIconActionClass?: boolean; } -export default function ActionButton({ text, icon, className, onClick, triggerCommand, titlePosition }: ActionButtonProps) { +export default function ActionButton({ text, icon, className, onClick, triggerCommand, titlePosition, noIconActionClass }: ActionButtonProps) { const buttonRef = useRef(null); const [ keyboardShortcut, setKeyboardShortcut ] = useState(); @@ -30,7 +31,7 @@ export default function ActionButton({ text, icon, className, onClick, triggerCo return
`; - -export default class GeoMapButtons extends NoteContextAwareWidget { - - isEnabled() { - return super.isEnabled() - && this.note?.getLabelValue("viewType") === "geoMap" - && !this.note.hasLabel("readOnly"); - } - - doRender() { - super.doRender(); - - this.$widget = $(TPL); - this.$widget.find(".geo-map-create-child-note").on("click", () => this.triggerEvent("geoMapCreateChildNote", { ntxId: this.ntxId })); - } - -} From 0ca30e0e87176ad271cac7b9dc269c0afa748ad5 Mon Sep 17 00:00:00 2001 From: Elian Doran Date: Wed, 27 Aug 2025 23:56:09 +0300 Subject: [PATCH 16/28] feat(react/floating_buttons): port copy image reference --- apps/client/src/services/image.ts | 2 +- apps/client/src/services/utils.ts | 4 +- .../src/stylesheets/theme-next/shell.css | 7 ---- apps/client/src/widgets/FloatingButtons.tsx | 36 +++++++++++++++- .../copy_image_reference_button.ts | 42 ------------------- 5 files changed, 37 insertions(+), 54 deletions(-) delete mode 100644 apps/client/src/widgets/floating_buttons/copy_image_reference_button.ts diff --git a/apps/client/src/services/image.ts b/apps/client/src/services/image.ts index f13a9a3c7..ca0d816c5 100644 --- a/apps/client/src/services/image.ts +++ b/apps/client/src/services/image.ts @@ -1,7 +1,7 @@ import { t } from "./i18n.js"; import toastService, { showError } from "./toast.js"; -function copyImageReferenceToClipboard($imageWrapper: JQuery) { +export function copyImageReferenceToClipboard($imageWrapper: JQuery) { try { $imageWrapper.attr("contenteditable", "true"); selectImage($imageWrapper.get(0)); diff --git a/apps/client/src/services/utils.ts b/apps/client/src/services/utils.ts index 68af11651..5462011cb 100644 --- a/apps/client/src/services/utils.ts +++ b/apps/client/src/services/utils.ts @@ -1,5 +1,6 @@ import dayjs from "dayjs"; import type { ViewScope } from "./link.js"; +import { FNote } from "./frontend_script_entrypoint.js"; const SVG_MIME = "image/svg+xml"; @@ -574,8 +575,7 @@ function copyHtmlToClipboard(content: string) { document.removeEventListener("copy", listener); } -// TODO: Set to FNote once the file is ported. -function createImageSrcUrl(note: { noteId: string; title: string }) { +export function createImageSrcUrl(note: FNote) { return `api/images/${note.noteId}/${encodeURIComponent(note.title)}?timestamp=${Date.now()}`; } diff --git a/apps/client/src/stylesheets/theme-next/shell.css b/apps/client/src/stylesheets/theme-next/shell.css index 8c460846f..d569dec91 100644 --- a/apps/client/src/stylesheets/theme-next/shell.css +++ b/apps/client/src/stylesheets/theme-next/shell.css @@ -1477,13 +1477,6 @@ div.floating-buttons-children .close-floating-buttons:has(.close-floating-button padding-inline-start: 8px; } -/* Copy image reference */ - -.floating-buttons .copy-image-reference-button .hidden-image-copy { - /* Take out of the the hidden image from flexbox to prevent the layout being affected */ - position: absolute; -} - /* Code, relation map buttons */ .floating-buttons .code-buttons-widget, diff --git a/apps/client/src/widgets/FloatingButtons.tsx b/apps/client/src/widgets/FloatingButtons.tsx index fa41bc54c..f38200072 100644 --- a/apps/client/src/widgets/FloatingButtons.tsx +++ b/apps/client/src/widgets/FloatingButtons.tsx @@ -5,7 +5,7 @@ import ActionButton, { ActionButtonProps } from "./react/ActionButton"; import FNote from "../entities/fnote"; import NoteContext from "../components/note_context"; import { useNoteContext, useNoteLabel, useNoteLabelBoolean, useNoteProperty, useTriliumEvent, useTriliumEvents, useTriliumOption, useTriliumOptionBool } from "./react/hooks"; -import { useContext, useEffect, useMemo, useState } from "preact/hooks"; +import { useContext, useEffect, useMemo, useRef, useState } from "preact/hooks"; import { ParentComponent } from "./react/react_utils"; import Component from "../components/component"; import { VNode } from "preact"; @@ -13,11 +13,12 @@ import attributes from "../services/attributes"; import appContext, { EventData, EventNames } from "../components/app_context"; import protected_session_holder from "../services/protected_session_holder"; import options from "../services/options"; -import { openInAppHelpFromUrl } from "../services/utils"; +import { createImageSrcUrl, openInAppHelpFromUrl } from "../services/utils"; import toast from "../services/toast"; import server from "../services/server"; import { SaveSqlConsoleResponse } from "@triliumnext/commons"; import tree from "../services/tree"; +import { copyImageReferenceToClipboard } from "../services/image"; interface FloatingButtonContext { parentComponent: Component; @@ -87,6 +88,12 @@ const FLOATING_BUTTON_DEFINITIONS: FloatingButtonDefinition[] = [ { component: GeoMapButtons, isEnabled: ({ note }) => note?.getLabelValue("viewType") === "geoMap" && !note.hasLabel("readOnly") + }, + { + component: CopyImageReferenceButton, + isEnabled: ({ note, noteContext }) => + ["mermaid", "canvas", "mindMap"].includes(note?.type ?? "") + && note?.isContentAvailable() && noteContext.viewScope?.viewMode === "default" } ]; @@ -320,6 +327,31 @@ function GeoMapButtons({ triggerEvent }) { ); } +function CopyImageReferenceButton({ note }: FloatingButtonContext) { + const hiddenImageCopyRef = useRef(null); + + return ( + <> + { + if (!hiddenImageCopyRef.current) return; + const imageEl = document.createElement("img"); + imageEl.src = createImageSrcUrl(note); + hiddenImageCopyRef.current.replaceChildren(imageEl); + copyImageReferenceToClipboard($(hiddenImageCopyRef.current)); + hiddenImageCopyRef.current.removeChild(imageEl); + }} + /> + +