import { t } from "i18next"; import "./FloatingButtons.css"; import Button from "./react/Button"; 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 { 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 { 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; note: FNote; noteContext: NoteContext; } interface FloatingButtonDefinition { component: (context: FloatingButtonContext) => VNode; isEnabled: (context: FloatingButtonContext) => boolean | Promise; } 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" }, { component: ToggleReadOnlyButton, isEnabled: ({ note, noteContext }) => (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() }, { component: ShowTocWidgetButton, isEnabled: ({ note, noteContext }) => 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 }, { 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=") }, { component: SaveToNoteButton, isEnabled: ({ note }) => note.mime === "text/x-sqlite;schema=trilium" && note.isHiddenCompletely() }, { component: RelationMapButtons, isEnabled: ({ note }) => note.type === "relationMap" } ]; 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: * * 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() { const { note, noteContext } = useNoteContext(); const parentComponent = useContext(ParentComponent); const context = useMemo(() => { if (!note || !noteContext || !parentComponent) return null; return { note, noteContext, parentComponent }; }, [ note, noteContext, parentComponent ]); // 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); } }); useTriliumEvent("readOnlyTemporarilyDisabled", ({ noteContext: eventNoteContext }) => { if (noteContext?.ntxId === eventNoteContext.ntxId) { setRefreshCounter(refreshCounter + 1); } }); useTriliumEvents(["reEvaluateTocWidgetVisibility", "reEvaluateHighlightsListWidgetVisibility"], ({ noteId }) => { if (noteId === note?.noteId) { setRefreshCounter(refreshCounter + 1); } }); // 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 (
{context && definitions.map(({ component: Component }) => ( ))}
) } function RefreshBackendLogButton({ parentComponent, noteContext }: FloatingButtonContext) { return parentComponent.triggerEvent("refreshData", { ntxId: noteContext.ntxId })} /> } function SwitchSplitOrientationButton({ }: FloatingButtonContext) { const [ splitEditorOrientation, setSplitEditorOrientation ] = useTriliumOption("splitEditorOrientation"); const upcomingOrientation = splitEditorOrientation === "horizontal" ? "vertical" : "horizontal"; return setSplitEditorOrientation(upcomingOrientation)} /> } function ToggleReadOnlyButton({ note }: FloatingButtonContext) { const [ isReadOnly, setReadOnly ] = useNoteLabelBoolean(note, "readOnly"); return setReadOnly(!isReadOnly)} /> } 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 }); } }} /> } function ShowTocWidgetButton({ noteContext }: FloatingButtonContext) { return { if (noteContext?.viewScope && noteContext.noteId) { noteContext.viewScope.tocTemporarilyHidden = false; appContext.triggerEvent("showTocWidget", { noteId: noteContext.noteId }); } }} /> } function ShowHighlightsListWidgetButton({ noteContext }: FloatingButtonContext) { return { if (noteContext?.viewScope && noteContext.noteId) { noteContext.viewScope.highlightsListTemporarilyHidden = false; appContext.triggerEvent("showHighlightsListWidget", { noteId: noteContext.noteId }); } }} /> } function RunActiveNoteButton() { return } function OpenTriliumApiDocsButton({ note }: FloatingButtonContext) { return openInAppHelpFromUrl(note.mime.endsWith("frontend") ? "Q2z6av6JZVWm" : "MEtfsqa5VwNi")} /> } 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); } }} /> } 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 })} />
) } function FloatingButton({ className, ...props }: ActionButtonProps) { return } /** * Show button that displays floating button after click on close button */ function ShowFloatingButton() { return (
); }