mirror of
https://github.com/zadam/trilium.git
synced 2025-11-27 02:54:25 +01:00
155 lines
6.8 KiB
TypeScript
155 lines
6.8 KiB
TypeScript
import { useCallback, useEffect, useMemo, useRef, useState } from "preact/hooks";
|
|
import { useNoteContext, useNoteProperty, useStaticTooltipWithKeyboardShortcut, useTriliumEvents } from "../react/hooks";
|
|
import "./style.css";
|
|
|
|
import { Indexed, numberObjectsInPlace } from "../../services/utils";
|
|
import { EventNames } from "../../components/app_context";
|
|
import NoteActions from "./NoteActions";
|
|
import { KeyboardActionNames } from "@triliumnext/commons";
|
|
import { RIBBON_TAB_DEFINITIONS } from "./RibbonDefinition";
|
|
import { TabConfiguration, TitleContext } from "./ribbon-interface";
|
|
|
|
const TAB_CONFIGURATION = numberObjectsInPlace<TabConfiguration>(RIBBON_TAB_DEFINITIONS);
|
|
|
|
interface ComputedTab extends Indexed<TabConfiguration> {
|
|
shouldShow: boolean;
|
|
}
|
|
|
|
export default function Ribbon() {
|
|
const { note, ntxId, hoistedNoteId, notePath, noteContext, componentId, isReadOnlyTemporarilyDisabled } = useNoteContext();
|
|
const noteType = useNoteProperty(note, "type");
|
|
const [ activeTabIndex, setActiveTabIndex ] = useState<number | undefined>();
|
|
const [ computedTabs, setComputedTabs ] = useState<ComputedTab[]>();
|
|
const titleContext: TitleContext = useMemo(() => ({
|
|
note,
|
|
noteContext
|
|
}), [ note, noteContext ]);
|
|
|
|
async function refresh() {
|
|
const computedTabs: ComputedTab[] = [];
|
|
for (const tab of TAB_CONFIGURATION) {
|
|
const shouldShow = await shouldShowTab(tab.show, titleContext);
|
|
computedTabs.push({
|
|
...tab,
|
|
shouldShow: !!shouldShow
|
|
});
|
|
}
|
|
setComputedTabs(computedTabs);
|
|
}
|
|
|
|
useEffect(() => {
|
|
refresh();
|
|
}, [ note, noteType, isReadOnlyTemporarilyDisabled ]);
|
|
|
|
// Automatically activate the first ribbon tab that needs to be activated whenever a note changes.
|
|
useEffect(() => {
|
|
if (!computedTabs) return;
|
|
const tabToActivate = computedTabs.find(tab => tab.shouldShow && (typeof tab.activate === "boolean" ? tab.activate : tab.activate?.(titleContext)));
|
|
setActiveTabIndex(tabToActivate?.index);
|
|
}, [ computedTabs, 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) => {
|
|
if (!computedTabs) return;
|
|
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 && 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 && 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}`}
|
|
/>
|
|
|
|
{ active && <span class="ribbon-tab-title-label">{title}</span> }
|
|
</div>
|
|
|
|
<div class="ribbon-tab-spacer" />
|
|
</>
|
|
)
|
|
}
|
|
|
|
export async function shouldShowTab(showConfig: boolean | ((context: TitleContext) => Promise<boolean | null | undefined> | boolean | null | undefined), context: TitleContext) {
|
|
if (showConfig === null || showConfig === undefined) return true;
|
|
if (typeof showConfig === "boolean") return showConfig;
|
|
if ("then" in showConfig) return await showConfig(context);
|
|
return showConfig(context);
|
|
}
|