import { VNode } from "preact"; import appContext, { EventData, EventNames } from "../components/app_context"; import Component from "../components/component"; import NoteContext from "../components/note_context"; import FNote from "../entities/fnote"; import ActionButton, { ActionButtonProps } from "./react/ActionButton"; import { useNoteLabelBoolean, useTriliumEvent, useTriliumOption, useWindowSize } from "./react/hooks"; import { useEffect, useLayoutEffect, useMemo, useRef, useState } from "preact/hooks"; import { createImageSrcUrl, openInAppHelpFromUrl } from "../services/utils"; import server from "../services/server"; import { BacklinkCountResponse, BacklinksResponse, SaveSqlConsoleResponse } from "@triliumnext/commons"; import toast from "../services/toast"; import { t } from "../services/i18n"; import { copyImageReferenceToClipboard } from "../services/image"; import tree from "../services/tree"; import protected_session_holder from "../services/protected_session_holder"; import options from "../services/options"; import { getHelpUrlForNote } from "../services/in_app_help"; import froca from "../services/froca"; import NoteLink from "./react/NoteLink"; import RawHtml from "./react/RawHtml"; import { ViewTypeOptions } from "./collections/interface"; export interface FloatingButtonContext { parentComponent: Component; note: FNote; noteContext: NoteContext; isDefaultViewMode: boolean; isReadOnly: boolean; /** Shorthand for triggering an event from the parent component. The `ntxId` is automatically handled for convenience. */ triggerEvent(name: T, data?: Omit, "ntxId">): void; viewType?: ViewTypeOptions | null; } function FloatingButton({ className, ...props }: ActionButtonProps) { return } export type FloatingButtonsList = ((context: FloatingButtonContext) => false | VNode)[]; export const DESKTOP_FLOATING_BUTTONS: FloatingButtonsList = [ RefreshBackendLogButton, SwitchSplitOrientationButton, ToggleReadOnlyButton, EditButton, ShowTocWidgetButton, ShowHighlightsListWidgetButton, RunActiveNoteButton, OpenTriliumApiDocsButton, SaveToNoteButton, RelationMapButtons, GeoMapButtons, CopyImageReferenceButton, ExportImageButtons, InAppHelpButton, Backlinks ]; export const MOBILE_FLOATING_BUTTONS: FloatingButtonsList = [ RefreshBackendLogButton, EditButton, RelationMapButtons, ExportImageButtons, Backlinks ] function RefreshBackendLogButton({ note, parentComponent, noteContext, isDefaultViewMode }: FloatingButtonContext) { const isEnabled = note.noteId === "_backendLog" && isDefaultViewMode; return isEnabled && parentComponent.triggerEvent("refreshData", { ntxId: noteContext.ntxId })} /> } function SwitchSplitOrientationButton({ note, isReadOnly, isDefaultViewMode }: FloatingButtonContext) { const isEnabled = note.type === "mermaid" && note.isContentAvailable() && !isReadOnly && isDefaultViewMode; const [ splitEditorOrientation, setSplitEditorOrientation ] = useTriliumOption("splitEditorOrientation"); const upcomingOrientation = splitEditorOrientation === "horizontal" ? "vertical" : "horizontal"; return isEnabled && setSplitEditorOrientation(upcomingOrientation)} /> } function ToggleReadOnlyButton({ note, viewType, isDefaultViewMode }: FloatingButtonContext) { const [ isReadOnly, setReadOnly ] = useNoteLabelBoolean(note, "readOnly"); const isEnabled = (note.type === "mermaid" || viewType === "geoMap") && note.isContentAvailable() && isDefaultViewMode; return isEnabled && setReadOnly(!isReadOnly)} /> } function EditButton({ note, noteContext, isDefaultViewMode }: FloatingButtonContext) { const [ animationClass, setAnimationClass ] = useState(""); const [ isEnabled, setIsEnabled ] = useState(false); useEffect(() => { noteContext.isReadOnly().then(isReadOnly => { setIsEnabled( isDefaultViewMode && (!note.isProtected || protected_session_holder.isProtectedSessionAvailable()) && !options.is("databaseReadonly") && isReadOnly ); }); }, [ note ]); useTriliumEvent("readOnlyTemporarilyDisabled", ({ noteContext: eventNoteContext }) => { if (noteContext?.ntxId === eventNoteContext.ntxId) { setIsEnabled(false); } }); // make the edit button stand out on the first display, otherwise // it's difficult to notice that the note is readonly useEffect(() => { if (isEnabled) { setAnimationClass("bx-tada bx-lg"); setTimeout(() => { setAnimationClass(""); }, 1700); } }, [ isEnabled ]); return isEnabled && { if (noteContext.viewScope) { noteContext.viewScope.readOnlyTemporarilyDisabled = true; appContext.triggerEvent("readOnlyTemporarilyDisabled", { noteContext }); } }} /> } function ShowTocWidgetButton({ note, noteContext, isDefaultViewMode }: FloatingButtonContext) { const [ isEnabled, setIsEnabled ] = useState(false); useTriliumEvent("reEvaluateTocWidgetVisibility", () => { setIsEnabled(note.type === "text" && isDefaultViewMode && !!noteContext.viewScope?.tocTemporarilyHidden); }); return isEnabled && { if (noteContext?.viewScope && noteContext.noteId) { noteContext.viewScope.tocTemporarilyHidden = false; appContext.triggerEvent("showTocWidget", { noteId: noteContext.noteId }); } }} /> } function ShowHighlightsListWidgetButton({ note, noteContext, isDefaultViewMode }: FloatingButtonContext) { const [ isEnabled, setIsEnabled ] = useState(false); useTriliumEvent("reEvaluateHighlightsListWidgetVisibility", () => { setIsEnabled(note.type === "text" && isDefaultViewMode && !!noteContext.viewScope?.highlightsListTemporarilyHidden); }); return isEnabled && { if (noteContext?.viewScope && noteContext.noteId) { noteContext.viewScope.highlightsListTemporarilyHidden = false; appContext.triggerEvent("showHighlightsListWidget", { noteId: noteContext.noteId }); } }} /> } function RunActiveNoteButton({ note }: FloatingButtonContext) { const isEnabled = note.mime.startsWith("application/javascript") || note.mime === "text/x-sqlite;schema=trilium"; return isEnabled && } function OpenTriliumApiDocsButton({ note }: FloatingButtonContext) { const isEnabled = note.mime.startsWith("application/javascript;env="); return isEnabled && openInAppHelpFromUrl(note.mime.endsWith("frontend") ? "Q2z6av6JZVWm" : "MEtfsqa5VwNi")} /> } function SaveToNoteButton({ note }: FloatingButtonContext) { const isEnabled = note.mime === "text/x-sqlite;schema=trilium" && note.isHiddenCompletely(); return isEnabled && { 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); } }} /> } function RelationMapButtons({ note, triggerEvent }: FloatingButtonContext) { const isEnabled = (note.type === "relationMap"); return isEnabled && ( <> triggerEvent("relationMapCreateChildNote")} /> triggerEvent("relationMapResetPanZoom")} />
triggerEvent("relationMapResetZoomIn")} /> triggerEvent("relationMapResetZoomOut")} />
) } function GeoMapButtons({ triggerEvent, viewType, isReadOnly }: FloatingButtonContext) { const isEnabled = viewType === "geoMap" && !isReadOnly; return isEnabled && ( triggerEvent("geoMapCreateChildNote")} /> ); } function CopyImageReferenceButton({ note, isDefaultViewMode }: FloatingButtonContext) { const hiddenImageCopyRef = useRef(null); const isEnabled = ["mermaid", "canvas", "mindMap"].includes(note?.type ?? "") && note?.isContentAvailable() && isDefaultViewMode; return isEnabled && ( <> { if (!hiddenImageCopyRef.current) return; const imageEl = document.createElement("img"); imageEl.src = createImageSrcUrl(note); hiddenImageCopyRef.current.replaceChildren(imageEl); copyImageReferenceToClipboard($(hiddenImageCopyRef.current)); hiddenImageCopyRef.current.removeChild(imageEl); }} />