diff --git a/apps/client/src/entities/fnote.ts b/apps/client/src/entities/fnote.ts index 39015cb24..b80e8e3fb 100644 --- a/apps/client/src/entities/fnote.ts +++ b/apps/client/src/entities/fnote.ts @@ -64,7 +64,7 @@ export interface NoteMetaData { /** * Note is the main node and concept in Trilium. */ -class FNote { +export default class FNote { private froca: Froca; noteId!: string; @@ -1035,5 +1035,3 @@ class FNote { return await server.get(`notes/${this.noteId}/metadata`); } } - -export default FNote; diff --git a/apps/client/src/layouts/desktop_layout.tsx b/apps/client/src/layouts/desktop_layout.tsx index fece6efd0..f9ad398e1 100644 --- a/apps/client/src/layouts/desktop_layout.tsx +++ b/apps/client/src/layouts/desktop_layout.tsx @@ -22,40 +22,26 @@ 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"; +import { DESKTOP_FLOATING_BUTTONS } from "../widgets/FloatingButtonsDefinitions.jsx"; export default class DesktopLayout { @@ -145,24 +131,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/layouts/mobile_layout.tsx b/apps/client/src/layouts/mobile_layout.tsx index 4d0fb4143..18c3d4a2e 100644 --- a/apps/client/src/layouts/mobile_layout.tsx +++ b/apps/client/src/layouts/mobile_layout.tsx @@ -7,12 +7,6 @@ import ToggleSidebarButtonWidget from "../widgets/mobile_widgets/toggle_sidebar_ import MobileDetailMenuWidget from "../widgets/mobile_widgets/mobile_detail_menu.js"; import ScreenContainer from "../widgets/mobile_widgets/screen_container.js"; import ScrollingContainer from "../widgets/containers/scrolling_container.js"; -import FloatingButtons from "../widgets/floating_buttons/floating_buttons.js"; -import EditButton from "../widgets/floating_buttons/edit_button.js"; -import RelationMapButtons from "../widgets/floating_buttons/relation_map_buttons.js"; -import SvgExportButton from "../widgets/floating_buttons/svg_export_button.js"; -import BacklinksWidget from "../widgets/floating_buttons/zpetne_odkazy.js"; -import HideFloatingButtonsButton from "../widgets/floating_buttons/hide_floating_buttons_button.js"; import NoteListWidget from "../widgets/note_list.js"; import GlobalMenuWidget from "../widgets/buttons/global_menu.js"; import LauncherContainer from "../widgets/containers/launcher_container.js"; @@ -22,14 +16,13 @@ import PromotedAttributesWidget from "../widgets/promoted_attributes.js"; import SidebarContainer from "../widgets/mobile_widgets/sidebar_container.js"; import type AppContext from "../components/app_context.js"; import TabRowWidget from "../widgets/tab_row.js"; -import RefreshButton from "../widgets/floating_buttons/refresh_button.js"; import MobileEditorToolbar from "../widgets/type_widgets/ckeditor/mobile_editor_toolbar.js"; import { applyModals } from "./layout_commons.js"; import CloseZenButton from "../widgets/close_zen_button.js"; import FilePropertiesTab from "../widgets/ribbon/FilePropertiesTab.jsx"; import { useNoteContext } from "../widgets/react/hooks.jsx"; -import { useContext } from "preact/hooks"; -import { ParentComponent } from "../widgets/react/react_utils.jsx"; +import FloatingButtons from "../widgets/FloatingButtons.jsx"; +import { MOBILE_FLOATING_BUTTONS } from "../widgets/FloatingButtonsDefinitions.jsx"; const MOBILE_CSS = ` - - - - - - -`; - -// TODO: Deduplicate with server. -interface SaveSqlConsoleResponse { - notePath: string; -} - -export default class CodeButtonsWidget extends NoteContextAwareWidget { - - private $openTriliumApiDocsButton!: JQuery; - private $executeButton!: 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.$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"); - 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); - - this.contentSized(); - - super.doRender(); - } - - 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=")); - } - - async noteTypeMimeChangedEvent({ noteId }: EventData<"noteTypeMimeChanged">) { - if (this.isNote(noteId)) { - await this.refresh(); - } - } -} diff --git a/apps/client/src/widgets/floating_buttons/copy_image_reference_button.ts b/apps/client/src/widgets/floating_buttons/copy_image_reference_button.ts deleted file mode 100644 index dba261297..000000000 --- a/apps/client/src/widgets/floating_buttons/copy_image_reference_button.ts +++ /dev/null @@ -1,42 +0,0 @@ -import { t } from "../../services/i18n.js"; -import NoteContextAwareWidget from "../note_context_aware_widget.js"; -import utils from "../../services/utils.js"; -import imageService from "../../services/image.js"; - -const TPL = /*html*/` -`; - -export default class CopyImageReferenceButton extends NoteContextAwareWidget { - - private $hiddenImageCopy!: JQuery; - - isEnabled() { - return super.isEnabled() && ["mermaid", "canvas", "mindMap"].includes(this.note?.type ?? "") && this.note?.isContentAvailable() && this.noteContext?.viewScope?.viewMode === "default"; - } - - doRender() { - super.doRender(); - - this.$widget = $(TPL); - this.$hiddenImageCopy = this.$widget.find(".hidden-image-copy"); - - this.$widget.on("click", () => { - if (!this.note) { - return; - } - - this.$hiddenImageCopy.empty().append($("").attr("src", utils.createImageSrcUrl(this.note))); - - imageService.copyImageReferenceToClipboard(this.$hiddenImageCopy); - - this.$hiddenImageCopy.empty(); - }); - this.contentSized(); - } -} 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(); - } - } -} diff --git a/apps/client/src/widgets/floating_buttons/floating_buttons.ts b/apps/client/src/widgets/floating_buttons/floating_buttons.ts deleted file mode 100644 index e75072d4e..000000000 --- a/apps/client/src/widgets/floating_buttons/floating_buttons.ts +++ /dev/null @@ -1,147 +0,0 @@ -import NoteContextAwareWidget from "../note_context_aware_widget.js"; -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)); - } - - toggle(show: boolean) { - this.$widget.find(".floating-buttons-children").toggleClass("temporarily-hidden", !show); - } - - hideFloatingButtonsCommand() { - this.toggle(false); - } -} diff --git a/apps/client/src/widgets/floating_buttons/geo_map_button.ts b/apps/client/src/widgets/floating_buttons/geo_map_button.ts deleted file mode 100644 index 7e59eeaf2..000000000 --- a/apps/client/src/widgets/floating_buttons/geo_map_button.ts +++ /dev/null @@ -1,38 +0,0 @@ -import { t } from "../../services/i18n.js"; -import NoteContextAwareWidget from "../note_context_aware_widget.js"; - -const TPL = /*html*/`\ -
- - -
`; - -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 })); - } - -} diff --git a/apps/client/src/widgets/floating_buttons/help_button.ts b/apps/client/src/widgets/floating_buttons/help_button.ts deleted file mode 100644 index b7f6a8fd2..000000000 --- a/apps/client/src/widgets/floating_buttons/help_button.ts +++ /dev/null @@ -1,78 +0,0 @@ -import { type EventData } from "../../components/app_context.js"; -import type FNote from "../../entities/fnote.js"; -import type { NoteType } from "../../entities/fnote.js"; -import { t } from "../../services/i18n.js"; -import type { ViewTypeOptions } from "../../services/note_list_renderer.js"; -import NoteContextAwareWidget from "../note_context_aware_widget.js"; - -const TPL = /*html*/` - -`; - -export const byNoteType: Record, string | null> = { - canvas: null, - code: null, - contentWidget: null, - doc: null, - file: null, - image: null, - launcher: null, - mermaid: null, - mindMap: null, - noteMap: null, - relationMap: null, - render: null, - search: null, - text: null, - webView: null, - aiChat: null -}; - -export const byBookType: Record = { - list: "mULW0Q3VojwY", - grid: "8QqnMzx393bx", - calendar: "xWbu3jpNWapp", - table: "2FvYrpmOXm29", - geoMap: "81SGnPGMk7Xc", - board: "CtBQqbwXDx1w" -}; - -export default class ContextualHelpButton extends NoteContextAwareWidget { - - isEnabled() { - if (!super.isEnabled()) { - return false; - } - - return !!ContextualHelpButton.#getUrlToOpen(this.note); - } - - doRender() { - this.$widget = $(TPL); - } - - static #getUrlToOpen(note: FNote | null | undefined) { - if (note && note.type !== "book" && byNoteType[note.type]) { - return byNoteType[note.type]; - } else if (note?.hasLabel("calendarRoot")) { - return "l0tKav7yLHGF"; - } else if (note?.hasLabel("textSnippet")) { - return "pwc194wlRzcH"; - } else if (note && note.type === "book") { - return byBookType[note.getAttributeValue("label", "viewType") as ViewTypeOptions ?? ""] - } - } - - async refreshWithNote(note: FNote | null | undefined): Promise { - this.$widget.attr("data-in-app-help", ContextualHelpButton.#getUrlToOpen(this.note) ?? ""); - } - - entitiesReloadedEvent({ loadResults }: EventData<"entitiesReloaded">) { - if (this.note?.type === "book" && loadResults.getAttributeRows().find((attr) => attr.noteId === this.noteId && attr.name === "viewType")) { - this.refresh(); - } - } - -} diff --git a/apps/client/src/widgets/floating_buttons/hide_floating_buttons_button.ts b/apps/client/src/widgets/floating_buttons/hide_floating_buttons_button.ts deleted file mode 100644 index 19fbde261..000000000 --- a/apps/client/src/widgets/floating_buttons/hide_floating_buttons_button.ts +++ /dev/null @@ -1,43 +0,0 @@ -import { t } from "../../services/i18n.js"; -import NoteContextAwareWidget from "../note_context_aware_widget.js"; - -const TPL = /*html*/` -
- - - -
-`; - -export default class HideFloatingButtonsButton extends NoteContextAwareWidget { - doRender() { - super.doRender(); - - this.$widget = $(TPL); - this.$widget.on("click", () => this.triggerCommand("hideFloatingButtons")); - this.contentSized(); - } -} diff --git a/apps/client/src/widgets/floating_buttons/png_export_button.ts b/apps/client/src/widgets/floating_buttons/png_export_button.ts deleted file mode 100644 index 8f5ccba9c..000000000 --- a/apps/client/src/widgets/floating_buttons/png_export_button.ts +++ /dev/null @@ -1,24 +0,0 @@ -import { t } from "../../services/i18n.js"; -import NoteContextAwareWidget from "../note_context_aware_widget.js"; - -const TPL = /*html*/` - -`; - -export default class PngExportButton extends NoteContextAwareWidget { - isEnabled() { - return super.isEnabled() && ["mermaid", "mindMap"].includes(this.note?.type ?? "") && this.note?.isContentAvailable() && this.noteContext?.viewScope?.viewMode === "default"; - } - - doRender() { - super.doRender(); - - this.$widget = $(TPL); - this.$widget.on("click", () => this.triggerEvent("exportPng", { ntxId: this.ntxId })); - this.contentSized(); - } -} 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"; - } - -} 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(); - } -} diff --git a/apps/client/src/widgets/floating_buttons/svg_export_button.ts b/apps/client/src/widgets/floating_buttons/svg_export_button.ts deleted file mode 100644 index 12e8d0520..000000000 --- a/apps/client/src/widgets/floating_buttons/svg_export_button.ts +++ /dev/null @@ -1,24 +0,0 @@ -import { t } from "../../services/i18n.js"; -import NoteContextAwareWidget from "../note_context_aware_widget.js"; - -const TPL = /*html*/` - -`; - -export default class SvgExportButton extends NoteContextAwareWidget { - isEnabled() { - return super.isEnabled() && ["mermaid", "mindMap"].includes(this.note?.type ?? "") && this.note?.isContentAvailable() && this.noteContext?.viewScope?.viewMode === "default"; - } - - doRender() { - super.doRender(); - - this.$widget = $(TPL); - this.$widget.on("click", () => this.triggerEvent("exportSvg", { ntxId: this.ntxId })); - this.contentSized(); - } -} 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"; - } -} 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"); - } - -} diff --git a/apps/client/src/widgets/floating_buttons/zpetne_odkazy.ts b/apps/client/src/widgets/floating_buttons/zpetne_odkazy.ts deleted file mode 100644 index 29dff9124..000000000 --- a/apps/client/src/widgets/floating_buttons/zpetne_odkazy.ts +++ /dev/null @@ -1,167 +0,0 @@ -/** - * !!! Filename is intentionally mangled, because some adblockers don't like the word "backlinks". - */ - -import { t } from "../../services/i18n.js"; -import NoteContextAwareWidget from "../note_context_aware_widget.js"; -import linkService from "../../services/link.js"; -import server from "../../services/server.js"; -import froca from "../../services/froca.js"; -import type FNote from "../../entities/fnote.js"; - -const TPL = /*html*/` - -`; - -// TODO: Deduplicate with server -interface Backlink { - noteId: string; - relationName?: string; - excerpts?: string[]; -} - -export default class BacklinksWidget extends NoteContextAwareWidget { - - private $count!: JQuery; - private $items!: JQuery; - private $ticker!: JQuery; - - doRender() { - this.$widget = $(TPL); - this.$count = this.$widget.find(".backlinks-count"); - this.$items = this.$widget.find(".backlinks-items"); - this.$ticker = this.$widget.find(".backlinks-ticker"); - - this.$count.on("click", () => { - this.$items.toggle(); - this.$items.css("max-height", ($(window).height() ?? 0) - (this.$items.offset()?.top ?? 0) - 10); - - if (this.$items.is(":visible")) { - this.renderBacklinks(); - } - }); - - this.contentSized(); - } - - async refreshWithNote(note: FNote) { - this.clearItems(); - - if (this.noteContext?.viewScope?.viewMode !== "default") { - this.toggle(false); - return; - } - - // can't use froca since that would count only relations from loaded notes - // TODO: Deduplicate response type - const resp = await server.get<{ count: number }>(`note-map/${this.noteId}/backlink-count`); - - if (!resp || !resp.count) { - this.toggle(false); - return; - } - - this.toggle(true); - this.$count.text( - // i18next plural - `${t("zpetne_odkazy.backlink", { count: resp.count })}` - ); - } - - toggle(show: boolean) { - this.$widget.toggleClass("hidden-no-content", !show) - .toggleClass("visible", !!show); - } - - clearItems() { - this.$items.empty().hide(); - } - - async renderBacklinks() { - if (!this.note) { - return; - } - - this.$items.empty(); - - const backlinks = await server.get(`note-map/${this.noteId}/backlinks`); - - if (!backlinks.length) { - return; - } - - await froca.getNotes(backlinks.map((bl) => bl.noteId)); // prefetch all - - for (const backlink of backlinks) { - const $item = $("
"); - - $item.append( - await linkService.createLink(backlink.noteId, { - showNoteIcon: true, - showNotePath: true, - showTooltip: false - }) - ); - - if (backlink.relationName) { - $item.append($("

").text(`${t("zpetne_odkazy.relation")}: ${backlink.relationName}`)); - } else { - $item.append(...(backlink.excerpts ?? [])); - } - - this.$items.append($item); - } - } -} 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 }); } } diff --git a/apps/client/src/widgets/react/ActionButton.tsx b/apps/client/src/widgets/react/ActionButton.tsx index 9fa0e69b4..41c4abbed 100644 --- a/apps/client/src/widgets/react/ActionButton.tsx +++ b/apps/client/src/widgets/react/ActionButton.tsx @@ -1,18 +1,37 @@ +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 { +export 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; + noIconActionClass?: boolean; } -export default function ActionButton({ text, icon, className, onClick, triggerCommand }: ActionButtonProps) { +export default function ActionButton({ text, icon, className, onClick, triggerCommand, titlePosition, noIconActionClass }: 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