fix(ribbon): formatting toolbar displayed in read-only notes

This commit is contained in:
Elian Doran 2025-11-09 20:03:43 +02:00
parent 8589f7f164
commit 532df6559a
No known key found for this signature in database
4 changed files with 41 additions and 15 deletions

View File

@ -841,7 +841,7 @@ export function arrayEqual<T>(a: T[], b: T[]) {
return true; return true;
} }
type Indexed<T extends object> = T & { index: number }; export type Indexed<T extends object> = T & { index: number };
/** /**
* Given an object array, alters every object in the array to have an index field assigned to it. * Given an object array, alters every object in the array to have an index field assigned to it.

View File

@ -2,7 +2,7 @@ import { useCallback, useEffect, useMemo, useRef, useState } from "preact/hooks"
import { useNoteContext, useNoteProperty, useStaticTooltipWithKeyboardShortcut, useTriliumEvents } from "../react/hooks"; import { useNoteContext, useNoteProperty, useStaticTooltipWithKeyboardShortcut, useTriliumEvents } from "../react/hooks";
import "./style.css"; import "./style.css";
import { numberObjectsInPlace } from "../../services/utils"; import { Indexed, numberObjectsInPlace } from "../../services/utils";
import { EventNames } from "../../components/app_context"; import { EventNames } from "../../components/app_context";
import NoteActions from "./NoteActions"; import NoteActions from "./NoteActions";
import { KeyboardActionNames } from "@triliumnext/commons"; import { KeyboardActionNames } from "@triliumnext/commons";
@ -11,23 +11,39 @@ import { TabConfiguration, TitleContext } from "./ribbon-interface";
const TAB_CONFIGURATION = numberObjectsInPlace<TabConfiguration>(RIBBON_TAB_DEFINITIONS); const TAB_CONFIGURATION = numberObjectsInPlace<TabConfiguration>(RIBBON_TAB_DEFINITIONS);
interface ComputedTab extends Indexed<TabConfiguration> {
shouldShow: boolean;
}
export default function Ribbon() { export default function Ribbon() {
const { note, ntxId, hoistedNoteId, notePath, noteContext, componentId } = useNoteContext(); const { note, ntxId, hoistedNoteId, notePath, noteContext, componentId } = useNoteContext();
const noteType = useNoteProperty(note, "type"); const noteType = useNoteProperty(note, "type");
const titleContext: TitleContext = { note };
const [ activeTabIndex, setActiveTabIndex ] = useState<number | undefined>(); const [ activeTabIndex, setActiveTabIndex ] = useState<number | undefined>();
const computedTabs = useMemo( const [ computedTabs, setComputedTabs ] = useState<ComputedTab[]>();
() => TAB_CONFIGURATION.map(tab => { const titleContext: TitleContext = useMemo(() => ({
const shouldShow = typeof tab.show === "boolean" ? tab.show : tab.show?.(titleContext); note,
return { 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, ...tab,
shouldShow shouldShow: !!shouldShow
} });
}), }
[ titleContext, note, noteType ]); setComputedTabs(computedTabs);
}
useEffect(() => {
refresh();
}, [ note, noteType ]);
// Automatically activate the first ribbon tab that needs to be activated whenever a note changes. // Automatically activate the first ribbon tab that needs to be activated whenever a note changes.
useEffect(() => { useEffect(() => {
if (!computedTabs) return;
const tabToActivate = computedTabs.find(tab => tab.shouldShow && (typeof tab.activate === "boolean" ? tab.activate : tab.activate?.(titleContext))); const tabToActivate = computedTabs.find(tab => tab.shouldShow && (typeof tab.activate === "boolean" ? tab.activate : tab.activate?.(titleContext)));
setActiveTabIndex(tabToActivate?.index); setActiveTabIndex(tabToActivate?.index);
}, [ note?.noteId ]); }, [ note?.noteId ]);
@ -35,6 +51,7 @@ export default function Ribbon() {
// Register keyboard shortcuts. // Register keyboard shortcuts.
const eventsToListenTo = useMemo(() => TAB_CONFIGURATION.filter(config => config.toggleCommand).map(config => config.toggleCommand) as EventNames[], []); const eventsToListenTo = useMemo(() => TAB_CONFIGURATION.filter(config => config.toggleCommand).map(config => config.toggleCommand) as EventNames[], []);
useTriliumEvents(eventsToListenTo, useCallback((e, toggleCommand) => { useTriliumEvents(eventsToListenTo, useCallback((e, toggleCommand) => {
if (!computedTabs) return;
const correspondingTab = computedTabs.find(tab => tab.toggleCommand === toggleCommand); const correspondingTab = computedTabs.find(tab => tab.toggleCommand === toggleCommand);
if (correspondingTab) { if (correspondingTab) {
if (activeTabIndex !== correspondingTab.index) { if (activeTabIndex !== correspondingTab.index) {
@ -51,7 +68,7 @@ export default function Ribbon() {
<> <>
<div className="ribbon-top-row"> <div className="ribbon-top-row">
<div className="ribbon-tab-container"> <div className="ribbon-tab-container">
{computedTabs.map(({ title, icon, index, toggleCommand, shouldShow }) => ( {computedTabs && computedTabs.map(({ title, icon, index, toggleCommand, shouldShow }) => (
shouldShow && <RibbonTab shouldShow && <RibbonTab
icon={icon} icon={icon}
title={typeof title === "string" ? title : title(titleContext)} title={typeof title === "string" ? title : title(titleContext)}
@ -74,7 +91,7 @@ export default function Ribbon() {
</div> </div>
<div className="ribbon-body-container"> <div className="ribbon-body-container">
{computedTabs.map(tab => { {computedTabs && computedTabs.map(tab => {
const isActive = tab.index === activeTabIndex; const isActive = tab.index === activeTabIndex;
if (!isActive && !tab.stayInDom) { if (!isActive && !tab.stayInDom) {
return; return;
@ -129,3 +146,9 @@ function RibbonTab({ icon, title, active, onClick, toggleCommand }: { icon: stri
) )
} }
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);
}

View File

@ -21,7 +21,9 @@ export const RIBBON_TAB_DEFINITIONS: TabConfiguration[] = [
{ {
title: t("classic_editor_toolbar.title"), title: t("classic_editor_toolbar.title"),
icon: "bx bx-text", icon: "bx bx-text",
show: ({ note }) => note?.type === "text" && options.get("textNoteEditorType") === "ckeditor-classic", show: async ({ note, noteContext }) => note?.type === "text"
&& options.get("textNoteEditorType") === "ckeditor-classic"
&& !(await noteContext?.isReadOnly()),
toggleCommand: "toggleRibbonTabClassicEditor", toggleCommand: "toggleRibbonTabClassicEditor",
content: FormattingToolbar, content: FormattingToolbar,
activate: true, activate: true,

View File

@ -16,13 +16,14 @@ export interface TabContext {
export interface TitleContext { export interface TitleContext {
note: FNote | null | undefined; note: FNote | null | undefined;
noteContext: NoteContext | undefined;
} }
export interface TabConfiguration { export interface TabConfiguration {
title: string | ((context: TitleContext) => string); title: string | ((context: TitleContext) => string);
icon: string; icon: string;
content: (context: TabContext) => VNode | false; content: (context: TabContext) => VNode | false;
show: boolean | ((context: TitleContext) => boolean | null | undefined); show: boolean | ((context: TitleContext) => Promise<boolean | null | undefined> | boolean | null | undefined);
toggleCommand?: KeyboardActionNames; toggleCommand?: KeyboardActionNames;
activate?: boolean | ((context: TitleContext) => boolean); activate?: boolean | ((context: TitleContext) => boolean);
/** /**