280 lines
11 KiB
TypeScript

import { useCallback, useEffect, useMemo, useRef, useState } from "preact/hooks";
import { t } from "../../services/i18n";
import { useNoteContext, useNoteProperty, useStaticTooltip, useStaticTooltipWithKeyboardShortcut, useTooltip, useTriliumEvent, 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 keyboard_actions from "../../services/keyboard_actions";
import { KeyboardActionNames } from "@triliumnext/commons";
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<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,
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",
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: true
},
{
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"
}
]);
export default function Ribbon() {
const { note, ntxId, hoistedNoteId, notePath, noteContext, componentId } = useNoteContext();
const noteType = useNoteProperty(note, "type");
const titleContext: TitleContext = { note };
const [ activeTabIndex, setActiveTabIndex ] = useState<number | undefined>();
const computedTabs = useMemo(
() => TAB_CONFIGURATION.map(tab => {
const shouldShow = typeof tab.show === "boolean" ? tab.show : tab.show?.(titleContext);
return {
...tab,
shouldShow
}
}),
[ titleContext, note, noteType ]);
// Automatically activate the first ribbon tab that needs to be activated whenever a note changes.
useEffect(() => {
const tabToActivate = computedTabs.find(tab => tab.shouldShow && (typeof tab.activate === "boolean" ? tab.activate : tab.activate?.(titleContext)));
setActiveTabIndex(tabToActivate?.index);
}, [ note?.noteId ]);
// Register keyboard shortcuts.
const eventsToListenTo = useMemo(() => TAB_CONFIGURATION.filter(config => config.toggleCommand).map(config => config.toggleCommand) as EventNames[], []);
useTriliumEvents(eventsToListenTo, useCallback((e, toggleCommand) => {
const correspondingTab = computedTabs.find(tab => tab.toggleCommand === toggleCommand);
if (correspondingTab) {
if (activeTabIndex !== correspondingTab.index) {
setActiveTabIndex(correspondingTab.index);
} else {
setActiveTabIndex(undefined);
}
}
}, [ computedTabs, activeTabIndex ]));
return (
<div className="ribbon-container" style={{ contain: "none" }}>
{noteContext?.viewScope?.viewMode === "default" && (
<>
<div className="ribbon-top-row">
<div className="ribbon-tab-container">
{computedTabs.map(({ title, icon, index, toggleCommand, shouldShow }) => (
shouldShow && <RibbonTab
icon={icon}
title={typeof title === "string" ? title : title(titleContext)}
active={index === activeTabIndex}
toggleCommand={toggleCommand}
onClick={() => {
if (activeTabIndex !== index) {
setActiveTabIndex(index);
} else {
// Collapse
setActiveTabIndex(undefined);
}
}}
/>
))}
</div>
<div className="ribbon-button-container">
{ note && <NoteActions note={note} noteContext={noteContext} /> }
</div>
</div>
<div className="ribbon-body-container">
{computedTabs.map(tab => {
const isActive = tab.index === activeTabIndex;
if (!isActive && !tab.stayInDom) {
return;
}
const TabContent = tab.content;
return (
<div className={`ribbon-body ${!isActive ? "hidden-ext" : ""}`}>
<TabContent
note={note}
hidden={!isActive}
ntxId={ntxId}
hoistedNoteId={hoistedNoteId}
notePath={notePath}
noteContext={noteContext}
componentId={componentId}
activate={useCallback(() => {
setActiveTabIndex(tab.index)
}, [setActiveTabIndex])}
/>
</div>
);
})}
</div>
</>
)}
</div>
)
}
function RibbonTab({ icon, title, active, onClick, toggleCommand }: { icon: string; title: string; active: boolean, onClick: () => void, toggleCommand?: KeyboardActionNames }) {
const iconRef = useRef<HTMLDivElement>(null);
useStaticTooltipWithKeyboardShortcut(iconRef, title, toggleCommand);
return (
<>
<div
className={`ribbon-tab-title ${active ? "active" : ""}`}
onClick={onClick}
>
<span
ref={iconRef}
className={`ribbon-tab-title-icon ${icon}`}
/>
&nbsp;
{ active && <span class="ribbon-tab-title-label">{title}</span> }
</div>
<div class="ribbon-tab-spacer" />
</>
)
}