diff --git a/apps/client/src/layouts/layout_commons.tsx b/apps/client/src/layouts/layout_commons.tsx
index 610f31dda..26f8ea232 100644
--- a/apps/client/src/layouts/layout_commons.tsx
+++ b/apps/client/src/layouts/layout_commons.tsx
@@ -29,8 +29,9 @@ import PromotedAttributesWidget from "../widgets/promoted_attributes.js";
import NoteDetailWidget from "../widgets/note_detail.js";
import CallToActionDialog from "../widgets/dialogs/call_to_action.jsx";
import NoteTitleWidget from "../widgets/note_title.jsx";
-import { PopupEditorFormattingToolbar } from "../widgets/ribbon/FormattingToolbar.js";
+import FormattingToolbar from "../widgets/ribbon/FormattingToolbar.js";
import NoteList from "../widgets/collections/NoteList.jsx";
+import StandaloneRibbonAdapter from "../widgets/ribbon/components/StandaloneRibbonAdapter.jsx";
export function applyModals(rootContainer: RootContainer) {
rootContainer
@@ -63,7 +64,7 @@ export function applyModals(rootContainer: RootContainer) {
.cssBlock(".title-row > * { margin: 5px; }")
.child()
.child())
- .child()
+ .child()
.child(new PromotedAttributesWidget())
.child(new NoteDetailWidget())
.child())
diff --git a/apps/client/src/widgets/ribbon/FormattingToolbar.tsx b/apps/client/src/widgets/ribbon/FormattingToolbar.tsx
index 3282ce5af..8828b15ee 100644
--- a/apps/client/src/widgets/ribbon/FormattingToolbar.tsx
+++ b/apps/client/src/widgets/ribbon/FormattingToolbar.tsx
@@ -1,4 +1,5 @@
-import { useNoteContext, useTriliumOption } from "../react/hooks";
+import { useTriliumOption } from "../react/hooks";
+import { TabContext } from "./ribbon-interface";
/**
* Handles the editing toolbar when the CKEditor is in decoupled mode.
@@ -6,19 +7,13 @@ import { useNoteContext, useTriliumOption } from "../react/hooks";
* This toolbar is only enabled if the user has selected the classic CKEditor.
*
* The ribbon item is active by default for text notes, as long as they are not in read-only mode.
- *
+ *
* ! The toolbar is not only used in the ribbon, but also in the quick edit feature.
*/
-export default function FormattingToolbar({ hidden }: { hidden?: boolean }) {
+export default function FormattingToolbar({ hidden }: TabContext) {
const [ textNoteEditorType ] = useTriliumOption("textNoteEditorType");
return (textNoteEditorType === "ckeditor-classic" &&
)
};
-
-export function PopupEditorFormattingToolbar() {
- // TODO: Integrate this directly once we migrate away from class components.
- const { note } = useNoteContext();
- return ;
-}
\ No newline at end of file
diff --git a/apps/client/src/widgets/ribbon/Ribbon.tsx b/apps/client/src/widgets/ribbon/Ribbon.tsx
index 72c627988..1c5b8a7bb 100644
--- a/apps/client/src/widgets/ribbon/Ribbon.tsx
+++ b/apps/client/src/widgets/ribbon/Ribbon.tsx
@@ -1,163 +1,15 @@
import { useCallback, useEffect, useMemo, useRef, useState } from "preact/hooks";
-import { t } from "../../services/i18n";
import { useNoteContext, useNoteProperty, useStaticTooltipWithKeyboardShortcut, useTriliumEvents } from "../react/hooks";
import "./style.css";
-import { VNode } from "preact";
-import BasicPropertiesTab from "./BasicPropertiesTab";
-import FormattingToolbar from "./FormattingToolbar";
+
import { numberObjectsInPlace } from "../../services/utils";
-import { TabContext } from "./ribbon-interface";
-import options from "../../services/options";
import { EventNames } from "../../components/app_context";
-import FNote from "../../entities/fnote";
-import ScriptTab from "./ScriptTab";
-import EditedNotesTab from "./EditedNotesTab";
-import NotePropertiesTab from "./NotePropertiesTab";
-import NoteInfoTab from "./NoteInfoTab";
-import SimilarNotesTab from "./SimilarNotesTab";
-import FilePropertiesTab from "./FilePropertiesTab";
-import ImagePropertiesTab from "./ImagePropertiesTab";
-import NotePathsTab from "./NotePathsTab";
-import NoteMapTab from "./NoteMapTab";
-import OwnedAttributesTab from "./OwnedAttributesTab";
-import InheritedAttributesTab from "./InheritedAttributesTab";
-import CollectionPropertiesTab from "./CollectionPropertiesTab";
-import SearchDefinitionTab from "./SearchDefinitionTab";
import NoteActions from "./NoteActions";
import { KeyboardActionNames } from "@triliumnext/commons";
+import { RIBBON_TAB_DEFINITIONS } from "./RibbonDefinition";
+import { TabConfiguration, TitleContext } from "./ribbon-interface";
-interface TitleContext {
- note: FNote | null | undefined;
-}
-
-interface TabConfiguration {
- title: string | ((context: TitleContext) => string);
- icon: string;
- content: (context: TabContext) => VNode | false;
- show: boolean | ((context: TitleContext) => boolean | null | undefined);
- toggleCommand?: KeyboardActionNames;
- activate?: boolean | ((context: TitleContext) => boolean);
- /**
- * By default the tab content will not be rendered unless the tab is active (i.e. selected by the user). Setting to `true` will ensure that the tab is rendered even when inactive, for cases where the tab needs to be accessible at all times (e.g. for the detached editor toolbar) or if event handling is needed.
- */
- stayInDom?: boolean;
-}
-
-const TAB_CONFIGURATION = numberObjectsInPlace([
- {
- title: t("classic_editor_toolbar.title"),
- icon: "bx bx-text",
- show: ({ note }) => note?.type === "text" && options.get("textNoteEditorType") === "ckeditor-classic",
- toggleCommand: "toggleRibbonTabClassicEditor",
- content: FormattingToolbar,
- activate: true,
- stayInDom: true
- },
- {
- title: ({ note }) => note?.isTriliumSqlite() ? t("script_executor.query") : t("script_executor.script"),
- icon: "bx bx-play",
- content: ScriptTab,
- activate: true,
- show: ({ note }) => note &&
- (note.isTriliumScript() || note.isTriliumSqlite()) &&
- (note.hasLabel("executeDescription") || note.hasLabel("executeButton"))
- },
- {
- title: t("search_definition.search_parameters"),
- icon: "bx bx-search",
- content: SearchDefinitionTab,
- activate: true,
- show: ({ note }) => note?.type === "search"
- },
- {
- title: t("edited_notes.title"),
- icon: "bx bx-calendar-edit",
- content: EditedNotesTab,
- show: ({ note }) => note?.hasOwnedLabel("dateNote"),
- activate: ({ note }) => (note?.getPromotedDefinitionAttributes().length === 0 || !options.is("promotedAttributesOpenInRibbon")) && options.is("editedNotesOpenInRibbon")
- },
- {
- title: t("book_properties.book_properties"),
- icon: "bx bx-book",
- content: CollectionPropertiesTab,
- show: ({ note }) => note?.type === "book" || note?.type === "search",
- toggleCommand: "toggleRibbonTabBookProperties"
- },
- {
- title: t("note_properties.info"),
- icon: "bx bx-info-square",
- content: NotePropertiesTab,
- show: ({ note }) => !!note?.getLabelValue("pageUrl"),
- activate: true
- },
- {
- title: t("file_properties.title"),
- icon: "bx bx-file",
- content: FilePropertiesTab,
- show: ({ note }) => note?.type === "file",
- toggleCommand: "toggleRibbonTabFileProperties",
- activate: ({ note }) => note?.mime !== "application/pdf"
- },
- {
- title: t("image_properties.title"),
- icon: "bx bx-image",
- content: ImagePropertiesTab,
- show: ({ note }) => note?.type === "image",
- toggleCommand: "toggleRibbonTabImageProperties",
- activate: true,
- },
- {
- // BasicProperties
- title: t("basic_properties.basic_properties"),
- icon: "bx bx-slider",
- content: BasicPropertiesTab,
- show: ({note}) => !note?.isLaunchBarConfig(),
- toggleCommand: "toggleRibbonTabBasicProperties"
- },
- {
- title: t("owned_attribute_list.owned_attributes"),
- icon: "bx bx-list-check",
- content: OwnedAttributesTab,
- show: ({note}) => !note?.isLaunchBarConfig(),
- toggleCommand: "toggleRibbonTabOwnedAttributes",
- stayInDom: true
- },
- {
- title: t("inherited_attribute_list.title"),
- icon: "bx bx-list-plus",
- content: InheritedAttributesTab,
- show: ({note}) => !note?.isLaunchBarConfig(),
- toggleCommand: "toggleRibbonTabInheritedAttributes"
- },
- {
- title: t("note_paths.title"),
- icon: "bx bx-collection",
- content: NotePathsTab,
- show: true,
- toggleCommand: "toggleRibbonTabNotePaths"
- },
- {
- title: t("note_map.title"),
- icon: "bx bxs-network-chart",
- content: NoteMapTab,
- show: true,
- toggleCommand: "toggleRibbonTabNoteMap"
- },
- {
- title: t("similar_notes.title"),
- icon: "bx bx-bar-chart",
- show: ({ note }) => note?.type !== "search" && !note?.isLabelTruthy("similarNotesWidgetDisabled"),
- content: SimilarNotesTab,
- toggleCommand: "toggleRibbonTabSimilarNotes"
- },
- {
- title: t("note_info_widget.title"),
- icon: "bx bx-info-circle",
- show: ({ note }) => !!note,
- content: NoteInfoTab,
- toggleCommand: "toggleRibbonTabNoteInfo"
- }
-]);
+const TAB_CONFIGURATION = numberObjectsInPlace(RIBBON_TAB_DEFINITIONS);
export default function Ribbon() {
const { note, ntxId, hoistedNoteId, notePath, noteContext, componentId } = useNoteContext();
diff --git a/apps/client/src/widgets/ribbon/RibbonDefinition.ts b/apps/client/src/widgets/ribbon/RibbonDefinition.ts
new file mode 100644
index 000000000..8f37053dc
--- /dev/null
+++ b/apps/client/src/widgets/ribbon/RibbonDefinition.ts
@@ -0,0 +1,134 @@
+import ScriptTab from "./ScriptTab";
+import EditedNotesTab from "./EditedNotesTab";
+import NotePropertiesTab from "./NotePropertiesTab";
+import NoteInfoTab from "./NoteInfoTab";
+import SimilarNotesTab from "./SimilarNotesTab";
+import FilePropertiesTab from "./FilePropertiesTab";
+import ImagePropertiesTab from "./ImagePropertiesTab";
+import NotePathsTab from "./NotePathsTab";
+import NoteMapTab from "./NoteMapTab";
+import OwnedAttributesTab from "./OwnedAttributesTab";
+import InheritedAttributesTab from "./InheritedAttributesTab";
+import CollectionPropertiesTab from "./CollectionPropertiesTab";
+import SearchDefinitionTab from "./SearchDefinitionTab";
+import BasicPropertiesTab from "./BasicPropertiesTab";
+import FormattingToolbar from "./FormattingToolbar";
+import options from "../../services/options";
+import { t } from "../../services/i18n";
+import { TabConfiguration } from "./ribbon-interface";
+
+export const RIBBON_TAB_DEFINITIONS: TabConfiguration[] = [
+ {
+ title: t("classic_editor_toolbar.title"),
+ icon: "bx bx-text",
+ show: ({ note }) => note?.type === "text" && options.get("textNoteEditorType") === "ckeditor-classic",
+ toggleCommand: "toggleRibbonTabClassicEditor",
+ content: FormattingToolbar,
+ activate: true,
+ stayInDom: true
+ },
+ {
+ title: ({ note }) => note?.isTriliumSqlite() ? t("script_executor.query") : t("script_executor.script"),
+ icon: "bx bx-play",
+ content: ScriptTab,
+ activate: true,
+ show: ({ note }) => note &&
+ (note.isTriliumScript() || note.isTriliumSqlite()) &&
+ (note.hasLabel("executeDescription") || note.hasLabel("executeButton"))
+ },
+ {
+ title: t("search_definition.search_parameters"),
+ icon: "bx bx-search",
+ content: SearchDefinitionTab,
+ activate: true,
+ show: ({ note }) => note?.type === "search"
+ },
+ {
+ title: t("edited_notes.title"),
+ icon: "bx bx-calendar-edit",
+ content: EditedNotesTab,
+ show: ({ note }) => note?.hasOwnedLabel("dateNote"),
+ activate: ({ note }) => (note?.getPromotedDefinitionAttributes().length === 0 || !options.is("promotedAttributesOpenInRibbon")) && options.is("editedNotesOpenInRibbon")
+ },
+ {
+ title: t("book_properties.book_properties"),
+ icon: "bx bx-book",
+ content: CollectionPropertiesTab,
+ show: ({ note }) => note?.type === "book" || note?.type === "search",
+ toggleCommand: "toggleRibbonTabBookProperties"
+ },
+ {
+ title: t("note_properties.info"),
+ icon: "bx bx-info-square",
+ content: NotePropertiesTab,
+ show: ({ note }) => !!note?.getLabelValue("pageUrl"),
+ activate: true
+ },
+ {
+ title: t("file_properties.title"),
+ icon: "bx bx-file",
+ content: FilePropertiesTab,
+ show: ({ note }) => note?.type === "file",
+ toggleCommand: "toggleRibbonTabFileProperties",
+ activate: ({ note }) => note?.mime !== "application/pdf"
+ },
+ {
+ title: t("image_properties.title"),
+ icon: "bx bx-image",
+ content: ImagePropertiesTab,
+ show: ({ note }) => note?.type === "image",
+ toggleCommand: "toggleRibbonTabImageProperties",
+ activate: true,
+ },
+ {
+ // BasicProperties
+ title: t("basic_properties.basic_properties"),
+ icon: "bx bx-slider",
+ content: BasicPropertiesTab,
+ show: ({note}) => !note?.isLaunchBarConfig(),
+ toggleCommand: "toggleRibbonTabBasicProperties"
+ },
+ {
+ title: t("owned_attribute_list.owned_attributes"),
+ icon: "bx bx-list-check",
+ content: OwnedAttributesTab,
+ show: ({note}) => !note?.isLaunchBarConfig(),
+ toggleCommand: "toggleRibbonTabOwnedAttributes",
+ stayInDom: true
+ },
+ {
+ title: t("inherited_attribute_list.title"),
+ icon: "bx bx-list-plus",
+ content: InheritedAttributesTab,
+ show: ({note}) => !note?.isLaunchBarConfig(),
+ toggleCommand: "toggleRibbonTabInheritedAttributes"
+ },
+ {
+ title: t("note_paths.title"),
+ icon: "bx bx-collection",
+ content: NotePathsTab,
+ show: true,
+ toggleCommand: "toggleRibbonTabNotePaths"
+ },
+ {
+ title: t("note_map.title"),
+ icon: "bx bxs-network-chart",
+ content: NoteMapTab,
+ show: true,
+ toggleCommand: "toggleRibbonTabNoteMap"
+ },
+ {
+ title: t("similar_notes.title"),
+ icon: "bx bx-bar-chart",
+ show: ({ note }) => note?.type !== "search" && !note?.isLabelTruthy("similarNotesWidgetDisabled"),
+ content: SimilarNotesTab,
+ toggleCommand: "toggleRibbonTabSimilarNotes"
+ },
+ {
+ title: t("note_info_widget.title"),
+ icon: "bx bx-info-circle",
+ show: ({ note }) => !!note,
+ content: NoteInfoTab,
+ toggleCommand: "toggleRibbonTabNoteInfo"
+ }
+];
diff --git a/apps/client/src/widgets/ribbon/components/StandaloneRibbonAdapter.tsx b/apps/client/src/widgets/ribbon/components/StandaloneRibbonAdapter.tsx
new file mode 100644
index 000000000..54a0f1af0
--- /dev/null
+++ b/apps/client/src/widgets/ribbon/components/StandaloneRibbonAdapter.tsx
@@ -0,0 +1,43 @@
+import { ComponentChildren } from "preact";
+import { useNoteContext } from "../../react/hooks";
+import { TabContext, TitleContext } from "../ribbon-interface";
+import { useEffect, useMemo, useState } from "preact/hooks";
+import { RIBBON_TAB_DEFINITIONS } from "../RibbonDefinition";
+
+interface StandaloneRibbonAdapterProps {
+ component: (props: TabContext) => ComponentChildren;
+}
+
+/**
+ * Takes in any ribbon tab component and renders it in standalone mod using the note context, thus requiring no inputs.
+ * Especially useful on mobile to detach components that would normally fit in the ribbon.
+ */
+export default function StandaloneRibbonAdapter({ component }: StandaloneRibbonAdapterProps) {
+ const Component = component;
+ const { note, ntxId, hoistedNoteId, notePath, noteContext, componentId } = useNoteContext();
+ const definition = useMemo(() => RIBBON_TAB_DEFINITIONS.find(def => def.content === component), [ component ]);
+ const [ shown, setShown ] = useState(unwrapShown(definition?.show, { note }));
+
+ useEffect(() => {
+ setShown(unwrapShown(definition?.show, { note }));
+ }, [ note ]);
+
+ return (
+ {}}
+ />
+ );
+}
+
+function unwrapShown(value: boolean | ((context: TitleContext) => boolean | null | undefined) | undefined, context: TitleContext) {
+ if (!value) return true;
+ if (typeof value === "boolean") return value;
+ return !!value(context);
+}
diff --git a/apps/client/src/widgets/ribbon/ribbon-interface.ts b/apps/client/src/widgets/ribbon/ribbon-interface.ts
index c40daf2c7..2fbc40612 100644
--- a/apps/client/src/widgets/ribbon/ribbon-interface.ts
+++ b/apps/client/src/widgets/ribbon/ribbon-interface.ts
@@ -1,5 +1,7 @@
+import { KeyboardActionNames } from "@triliumnext/commons";
import NoteContext from "../../components/note_context";
import FNote from "../../entities/fnote";
+import { VNode } from "preact";
export interface TabContext {
note: FNote | null | undefined;
@@ -11,3 +13,20 @@ export interface TabContext {
componentId: string;
activate(): void;
}
+
+export interface TitleContext {
+ note: FNote | null | undefined;
+}
+
+export interface TabConfiguration {
+ title: string | ((context: TitleContext) => string);
+ icon: string;
+ content: (context: TabContext) => VNode | false;
+ show: boolean | ((context: TitleContext) => boolean | null | undefined);
+ toggleCommand?: KeyboardActionNames;
+ activate?: boolean | ((context: TitleContext) => boolean);
+ /**
+ * By default the tab content will not be rendered unless the tab is active (i.e. selected by the user). Setting to `true` will ensure that the tab is rendered even when inactive, for cases where the tab needs to be accessible at all times (e.g. for the detached editor toolbar) or if event handling is needed.
+ */
+ stayInDom?: boolean;
+}