mirror of
https://github.com/zadam/trilium.git
synced 2025-12-17 12:54:24 +01:00
New layout: Shared formatting toolbar (#8046)
This commit is contained in:
commit
869aec778c
@ -1,18 +1,19 @@
|
|||||||
import protectedSessionHolder from "../services/protected_session_holder.js";
|
|
||||||
import server from "../services/server.js";
|
|
||||||
import utils from "../services/utils.js";
|
|
||||||
import appContext, { type EventData, type EventListener } from "./app_context.js";
|
|
||||||
import treeService from "../services/tree.js";
|
|
||||||
import Component from "./component.js";
|
|
||||||
import froca from "../services/froca.js";
|
|
||||||
import hoistedNoteService from "../services/hoisted_note.js";
|
|
||||||
import options from "../services/options.js";
|
|
||||||
import type { ViewScope } from "../services/link.js";
|
|
||||||
import type FNote from "../entities/fnote.js";
|
|
||||||
import type { CKTextEditor } from "@triliumnext/ckeditor5";
|
import type { CKTextEditor } from "@triliumnext/ckeditor5";
|
||||||
import type CodeMirror from "@triliumnext/codemirror";
|
import type CodeMirror from "@triliumnext/codemirror";
|
||||||
|
|
||||||
|
import type FNote from "../entities/fnote.js";
|
||||||
import { closeActiveDialog } from "../services/dialog.js";
|
import { closeActiveDialog } from "../services/dialog.js";
|
||||||
|
import froca from "../services/froca.js";
|
||||||
|
import hoistedNoteService from "../services/hoisted_note.js";
|
||||||
|
import type { ViewScope } from "../services/link.js";
|
||||||
|
import options from "../services/options.js";
|
||||||
|
import protectedSessionHolder from "../services/protected_session_holder.js";
|
||||||
|
import server from "../services/server.js";
|
||||||
|
import treeService from "../services/tree.js";
|
||||||
|
import utils from "../services/utils.js";
|
||||||
import { ReactWrappedWidget } from "../widgets/basic_widget.js";
|
import { ReactWrappedWidget } from "../widgets/basic_widget.js";
|
||||||
|
import appContext, { type EventData, type EventListener } from "./app_context.js";
|
||||||
|
import Component from "./component.js";
|
||||||
|
|
||||||
export interface SetNoteOpts {
|
export interface SetNoteOpts {
|
||||||
triggerSwitchEvent?: unknown;
|
triggerSwitchEvent?: unknown;
|
||||||
|
|||||||
@ -1,58 +1,57 @@
|
|||||||
import { applyModals } from "./layout_commons.js";
|
import type { AppContext } from "../components/app_context.js";
|
||||||
import { DESKTOP_FLOATING_BUTTONS } from "../widgets/FloatingButtonsDefinitions.jsx";
|
import type { WidgetsByParent } from "../services/bundle.js";
|
||||||
|
import { isExperimentalFeatureEnabled } from "../services/experimental_features.js";
|
||||||
|
import options from "../services/options.js";
|
||||||
|
import utils from "../services/utils.js";
|
||||||
import ApiLog from "../widgets/api_log.jsx";
|
import ApiLog from "../widgets/api_log.jsx";
|
||||||
import ClosePaneButton from "../widgets/buttons/close_pane_button.js";
|
import ClosePaneButton from "../widgets/buttons/close_pane_button.js";
|
||||||
import CloseZenModeButton from "../widgets/close_zen_button.jsx";
|
|
||||||
import ContentHeader from "../widgets/containers/content_header.js";
|
|
||||||
import CreatePaneButton from "../widgets/buttons/create_pane_button.js";
|
import CreatePaneButton from "../widgets/buttons/create_pane_button.js";
|
||||||
import FindWidget from "../widgets/find.js";
|
|
||||||
import FlexContainer from "../widgets/containers/flex_container.js";
|
|
||||||
import FloatingButtons from "../widgets/FloatingButtons.jsx";
|
|
||||||
import GlobalMenu from "../widgets/buttons/global_menu.jsx";
|
import GlobalMenu from "../widgets/buttons/global_menu.jsx";
|
||||||
import HighlightsListWidget from "../widgets/highlights_list.js";
|
|
||||||
import LeftPaneContainer from "../widgets/containers/left_pane_container.js";
|
|
||||||
import LeftPaneToggle from "../widgets/buttons/left_pane_toggle.js";
|
import LeftPaneToggle from "../widgets/buttons/left_pane_toggle.js";
|
||||||
import MovePaneButton from "../widgets/buttons/move_pane_button.js";
|
import MovePaneButton from "../widgets/buttons/move_pane_button.js";
|
||||||
import NoteIconWidget from "../widgets/note_icon.jsx";
|
import CloseZenModeButton from "../widgets/close_zen_button.jsx";
|
||||||
import NoteList from "../widgets/collections/NoteList.jsx";
|
import NoteList from "../widgets/collections/NoteList.jsx";
|
||||||
import NoteTitleWidget from "../widgets/note_title.jsx";
|
import ContentHeader from "../widgets/containers/content_header.js";
|
||||||
import NoteTreeWidget from "../widgets/note_tree.js";
|
import FlexContainer from "../widgets/containers/flex_container.js";
|
||||||
import NoteWrapperWidget from "../widgets/note_wrapper.js";
|
import LeftPaneContainer from "../widgets/containers/left_pane_container.js";
|
||||||
import options from "../services/options.js";
|
|
||||||
import PasswordNoteSetDialog from "../widgets/dialogs/password_not_set.js";
|
|
||||||
import QuickSearchWidget from "../widgets/quick_search.js";
|
|
||||||
import ReadOnlyNoteInfoBar from "../widgets/ReadOnlyNoteInfoBar.jsx";
|
|
||||||
import Ribbon from "../widgets/ribbon/Ribbon.jsx";
|
|
||||||
import RightPaneContainer from "../widgets/containers/right_pane_container.js";
|
import RightPaneContainer from "../widgets/containers/right_pane_container.js";
|
||||||
import RootContainer from "../widgets/containers/root_container.js";
|
import RootContainer from "../widgets/containers/root_container.js";
|
||||||
import ScrollingContainer from "../widgets/containers/scrolling_container.js";
|
import ScrollingContainer from "../widgets/containers/scrolling_container.js";
|
||||||
import ScrollPadding from "../widgets/scroll_padding.js";
|
|
||||||
import SearchResult from "../widgets/search_result.jsx";
|
|
||||||
import SharedInfo from "../widgets/shared_info.jsx";
|
|
||||||
import SplitNoteContainer from "../widgets/containers/split_note_container.js";
|
import SplitNoteContainer from "../widgets/containers/split_note_container.js";
|
||||||
import SqlResults from "../widgets/sql_result.js";
|
import PasswordNoteSetDialog from "../widgets/dialogs/password_not_set.js";
|
||||||
import SqlTableSchemas from "../widgets/sql_table_schemas.js";
|
|
||||||
import TabRowWidget from "../widgets/tab_row.js";
|
|
||||||
import TitleBarButtons from "../widgets/title_bar_buttons.jsx";
|
|
||||||
import TocWidget from "../widgets/toc.js";
|
|
||||||
import type { AppContext } from "../components/app_context.js";
|
|
||||||
import type { WidgetsByParent } from "../services/bundle.js";
|
|
||||||
import UploadAttachmentsDialog from "../widgets/dialogs/upload_attachments.js";
|
import UploadAttachmentsDialog from "../widgets/dialogs/upload_attachments.js";
|
||||||
import utils from "../services/utils.js";
|
import FindWidget from "../widgets/find.js";
|
||||||
import WatchedFileUpdateStatusWidget from "../widgets/watched_file_update_status.js";
|
import FloatingButtons from "../widgets/FloatingButtons.jsx";
|
||||||
import NoteDetail from "../widgets/NoteDetail.jsx";
|
import { DESKTOP_FLOATING_BUTTONS } from "../widgets/FloatingButtonsDefinitions.jsx";
|
||||||
import PromotedAttributes from "../widgets/PromotedAttributes.jsx";
|
import HighlightsListWidget from "../widgets/highlights_list.js";
|
||||||
import SpacerWidget from "../widgets/launch_bar/SpacerWidget.jsx";
|
|
||||||
import LauncherContainer from "../widgets/launch_bar/LauncherContainer.jsx";
|
import LauncherContainer from "../widgets/launch_bar/LauncherContainer.jsx";
|
||||||
import TabHistoryNavigationButtons from "../widgets/TabHistoryNavigationButtons.jsx";
|
import SpacerWidget from "../widgets/launch_bar/SpacerWidget.jsx";
|
||||||
import { isExperimentalFeatureEnabled } from "../services/experimental_features.js";
|
import InlineTitle from "../widgets/layout/InlineTitle.jsx";
|
||||||
import NoteActions from "../widgets/ribbon/NoteActions.jsx";
|
|
||||||
import FormattingToolbar from "../widgets/ribbon/FormattingToolbar.jsx";
|
|
||||||
import StandaloneRibbonAdapter from "../widgets/ribbon/components/StandaloneRibbonAdapter.jsx";
|
|
||||||
import NoteBadges from "../widgets/layout/NoteBadges.jsx";
|
import NoteBadges from "../widgets/layout/NoteBadges.jsx";
|
||||||
import NoteTitleActions from "../widgets/layout/NoteTitleActions.jsx";
|
import NoteTitleActions from "../widgets/layout/NoteTitleActions.jsx";
|
||||||
import StatusBar from "../widgets/layout/StatusBar.jsx";
|
import StatusBar from "../widgets/layout/StatusBar.jsx";
|
||||||
import InlineTitle from "../widgets/layout/InlineTitle.jsx";
|
import NoteIconWidget from "../widgets/note_icon.jsx";
|
||||||
|
import NoteTitleWidget from "../widgets/note_title.jsx";
|
||||||
|
import NoteTreeWidget from "../widgets/note_tree.js";
|
||||||
|
import NoteWrapperWidget from "../widgets/note_wrapper.js";
|
||||||
|
import NoteDetail from "../widgets/NoteDetail.jsx";
|
||||||
|
import PromotedAttributes from "../widgets/PromotedAttributes.jsx";
|
||||||
|
import QuickSearchWidget from "../widgets/quick_search.js";
|
||||||
|
import ReadOnlyNoteInfoBar from "../widgets/ReadOnlyNoteInfoBar.jsx";
|
||||||
|
import { FixedFormattingToolbar } from "../widgets/ribbon/FormattingToolbar.jsx";
|
||||||
|
import NoteActions from "../widgets/ribbon/NoteActions.jsx";
|
||||||
|
import Ribbon from "../widgets/ribbon/Ribbon.jsx";
|
||||||
|
import ScrollPadding from "../widgets/scroll_padding.js";
|
||||||
|
import SearchResult from "../widgets/search_result.jsx";
|
||||||
|
import SharedInfo from "../widgets/shared_info.jsx";
|
||||||
|
import SqlResults from "../widgets/sql_result.js";
|
||||||
|
import SqlTableSchemas from "../widgets/sql_table_schemas.js";
|
||||||
|
import TabRowWidget from "../widgets/tab_row.js";
|
||||||
|
import TabHistoryNavigationButtons from "../widgets/TabHistoryNavigationButtons.jsx";
|
||||||
|
import TitleBarButtons from "../widgets/title_bar_buttons.jsx";
|
||||||
|
import TocWidget from "../widgets/toc.js";
|
||||||
|
import WatchedFileUpdateStatusWidget from "../widgets/watched_file_update_status.js";
|
||||||
|
import { applyModals } from "./layout_commons.js";
|
||||||
|
|
||||||
export default class DesktopLayout {
|
export default class DesktopLayout {
|
||||||
|
|
||||||
@ -95,7 +94,7 @@ export default class DesktopLayout {
|
|||||||
|
|
||||||
const rootContainer = new RootContainer(true)
|
const rootContainer = new RootContainer(true)
|
||||||
.setParent(appContext)
|
.setParent(appContext)
|
||||||
.class((launcherPaneIsHorizontal ? "horizontal" : "vertical") + "-layout")
|
.class(`${launcherPaneIsHorizontal ? "horizontal" : "vertical" }-layout`)
|
||||||
.optChild(
|
.optChild(
|
||||||
fullWidthTabBar,
|
fullWidthTabBar,
|
||||||
new FlexContainer("row")
|
new FlexContainer("row")
|
||||||
@ -131,6 +130,7 @@ export default class DesktopLayout {
|
|||||||
.child(new TabRowWidget())
|
.child(new TabRowWidget())
|
||||||
.optChild(customTitleBarButtons, <TitleBarButtons />)
|
.optChild(customTitleBarButtons, <TitleBarButtons />)
|
||||||
.css("height", "40px"))
|
.css("height", "40px"))
|
||||||
|
.optChild(isNewLayout, <FixedFormattingToolbar />)
|
||||||
.child(
|
.child(
|
||||||
new FlexContainer("row")
|
new FlexContainer("row")
|
||||||
.filling()
|
.filling()
|
||||||
@ -141,7 +141,6 @@ export default class DesktopLayout {
|
|||||||
.filling()
|
.filling()
|
||||||
.collapsible()
|
.collapsible()
|
||||||
.id("center-pane")
|
.id("center-pane")
|
||||||
.optChild(isNewLayout, <StandaloneRibbonAdapter component={FormattingToolbar} />)
|
|
||||||
.child(
|
.child(
|
||||||
new SplitNoteContainer(() =>
|
new SplitNoteContainer(() =>
|
||||||
new NoteWrapperWidget()
|
new NoteWrapperWidget()
|
||||||
|
|||||||
@ -1,10 +1,12 @@
|
|||||||
import utils from "../services/utils.js";
|
import { NoteType } from "@triliumnext/commons";
|
||||||
|
|
||||||
|
import FAttribute from "../entities/fattribute.js";
|
||||||
|
import FBlob from "../entities/fblob.js";
|
||||||
|
import FBranch from "../entities/fbranch.js";
|
||||||
import FNote from "../entities/fnote.js";
|
import FNote from "../entities/fnote.js";
|
||||||
import froca from "../services/froca.js";
|
import froca from "../services/froca.js";
|
||||||
import FAttribute from "../entities/fattribute.js";
|
|
||||||
import noteAttributeCache from "../services/note_attribute_cache.js";
|
import noteAttributeCache from "../services/note_attribute_cache.js";
|
||||||
import FBranch from "../entities/fbranch.js";
|
import utils from "../services/utils.js";
|
||||||
import FBlob from "../entities/fblob.js";
|
|
||||||
|
|
||||||
type AttributeDefinitions = { [key in `#${string}`]: string; };
|
type AttributeDefinitions = { [key in `#${string}`]: string; };
|
||||||
type RelationDefinitions = { [key in `~${string}`]: string; };
|
type RelationDefinitions = { [key in `~${string}`]: string; };
|
||||||
@ -12,6 +14,7 @@ type RelationDefinitions = { [key in `~${string}`]: string; };
|
|||||||
interface NoteDefinition extends AttributeDefinitions, RelationDefinitions {
|
interface NoteDefinition extends AttributeDefinitions, RelationDefinitions {
|
||||||
id?: string | undefined;
|
id?: string | undefined;
|
||||||
title: string;
|
title: string;
|
||||||
|
type?: NoteType;
|
||||||
children?: NoteDefinition[];
|
children?: NoteDefinition[];
|
||||||
content?: string;
|
content?: string;
|
||||||
}
|
}
|
||||||
@ -45,7 +48,7 @@ export function buildNote(noteDef: NoteDefinition) {
|
|||||||
const note = new FNote(froca, {
|
const note = new FNote(froca, {
|
||||||
noteId: noteDef.id ?? utils.randomString(12),
|
noteId: noteDef.id ?? utils.randomString(12),
|
||||||
title: noteDef.title,
|
title: noteDef.title,
|
||||||
type: "text",
|
type: noteDef.type ?? "text",
|
||||||
mime: "text/html",
|
mime: "text/html",
|
||||||
isProtected: false,
|
isProtected: false,
|
||||||
blobId: ""
|
blobId: ""
|
||||||
|
|||||||
@ -81,5 +81,6 @@ body.prefers-centered-content .inline-title {
|
|||||||
--color: var(--input-background-color);
|
--color: var(--input-background-color);
|
||||||
color: var(--main-text-color);
|
color: var(--main-text-color);
|
||||||
font-size: 0.9rem;
|
font-size: 0.9rem;
|
||||||
|
flex-shrink: 0;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -159,12 +159,12 @@ function NoteTypeSwitcher() {
|
|||||||
const currentNoteTypeData = useMemo(() => NOTE_TYPES.find(t => t.type === currentNoteType), [ currentNoteType ]);
|
const currentNoteTypeData = useMemo(() => NOTE_TYPES.find(t => t.type === currentNoteType), [ currentNoteType ]);
|
||||||
const { builtinTemplates, collectionTemplates } = useBuiltinTemplates();
|
const { builtinTemplates, collectionTemplates } = useBuiltinTemplates();
|
||||||
|
|
||||||
return (
|
return (currentNoteType && supportedNoteTypes.has(currentNoteType) &&
|
||||||
<div
|
<div
|
||||||
className="note-type-switcher"
|
className="note-type-switcher"
|
||||||
onWheel={onWheelHorizontalScroll}
|
onWheel={onWheelHorizontalScroll}
|
||||||
>
|
>
|
||||||
{blob?.contentLength === 0 && (
|
{note && blob?.contentLength === 0 && (
|
||||||
<>
|
<>
|
||||||
<div className="intro">{t("note_title.note_type_switcher_label", { type: currentNoteTypeData?.title.toLocaleLowerCase() })}</div>
|
<div className="intro">{t("note_title.note_type_switcher_label", { type: currentNoteTypeData?.title.toLocaleLowerCase() })}</div>
|
||||||
{pinnedNoteTypes.map(noteType => noteType.type !== currentNoteType && (
|
{pinnedNoteTypes.map(noteType => noteType.type !== currentNoteType && (
|
||||||
|
|||||||
151
apps/client/src/widgets/ribbon/FormattingToolbar.spec.ts
Normal file
151
apps/client/src/widgets/ribbon/FormattingToolbar.spec.ts
Normal file
@ -0,0 +1,151 @@
|
|||||||
|
import { NoteType } from "@triliumnext/commons";
|
||||||
|
import { beforeAll, describe, expect, it, vi } from "vitest";
|
||||||
|
|
||||||
|
import NoteContext from "../../components/note_context";
|
||||||
|
import { ViewMode } from "../../services/link";
|
||||||
|
import { randomString } from "../../services/utils";
|
||||||
|
import { buildNote } from "../../test/easy-froca";
|
||||||
|
import { getFormattingToolbarState } from "./FormattingToolbar";
|
||||||
|
|
||||||
|
interface NoteContextInfo {
|
||||||
|
type: NoteType;
|
||||||
|
viewScope?: ViewMode;
|
||||||
|
isReadOnly?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
describe("Formatting toolbar logic", () => {
|
||||||
|
beforeAll(() => {
|
||||||
|
vi.mock("../../services/tree.ts", () => ({
|
||||||
|
default: {
|
||||||
|
getActiveContextNotePath() {
|
||||||
|
return "root";
|
||||||
|
},
|
||||||
|
resolveNotePath(inputNotePath: string) {
|
||||||
|
return inputNotePath;
|
||||||
|
},
|
||||||
|
getNoteIdFromUrl(url) {
|
||||||
|
return url.split("/").at(-1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}));
|
||||||
|
|
||||||
|
buildNote({
|
||||||
|
id: "root",
|
||||||
|
title: "Root"
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
async function buildConfig(noteContextInfos: NoteContextInfo[], activeIndex: number = 0) {
|
||||||
|
const noteContexts: NoteContext[] = [];
|
||||||
|
for (const noteContextData of noteContextInfos) {
|
||||||
|
const noteContext = new NoteContext(randomString(10));
|
||||||
|
const note = buildNote({
|
||||||
|
title: randomString(5),
|
||||||
|
type: noteContextData.type
|
||||||
|
});
|
||||||
|
|
||||||
|
noteContext.noteId = note.noteId;
|
||||||
|
expect(noteContext.note).toBe(note);
|
||||||
|
noteContext.viewScope = {
|
||||||
|
viewMode: noteContextData.viewScope ?? "default"
|
||||||
|
};
|
||||||
|
noteContext.isReadOnly = async () => !!noteContextData.isReadOnly;
|
||||||
|
noteContext.getSubContexts = () => [];
|
||||||
|
noteContexts.push(noteContext);
|
||||||
|
};
|
||||||
|
|
||||||
|
const mainNoteContext = noteContexts[0];
|
||||||
|
for (const noteContext of noteContexts) {
|
||||||
|
noteContext.getMainContext = () => mainNoteContext;
|
||||||
|
}
|
||||||
|
|
||||||
|
mainNoteContext.getSubContexts = () => noteContexts;
|
||||||
|
return noteContexts[activeIndex];
|
||||||
|
}
|
||||||
|
|
||||||
|
async function testSplit(noteContextInfos: NoteContextInfo[], activeIndex: number = 0, editor = "ckeditor-classic") {
|
||||||
|
const noteContext = await buildConfig(noteContextInfos, activeIndex);
|
||||||
|
return await getFormattingToolbarState(noteContext, noteContext.note, editor);
|
||||||
|
}
|
||||||
|
|
||||||
|
describe("Single split", () => {
|
||||||
|
it("should be hidden for floating toolbar", async () => {
|
||||||
|
expect(await testSplit([ { type: "text" } ], 0, "ckeditor-balloon")).toBe("hidden");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should be visible for single text note", async () => {
|
||||||
|
expect(await testSplit([ { type: "text" } ])).toBe("visible");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should be hidden for read-only text note", async () => {
|
||||||
|
expect(await testSplit([ { type: "text", isReadOnly: true } ])).toBe("hidden");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should be hidden for non-text note", async () => {
|
||||||
|
expect(await testSplit([ { type: "code" } ])).toBe("hidden");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should be hidden for wrong view mode", async () => {
|
||||||
|
expect(await testSplit([ { type: "text", viewScope: "attachments" } ])).toBe("hidden");
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("Multi split", () => {
|
||||||
|
it("should be hidden for floating toolbar", async () => {
|
||||||
|
expect(await testSplit([
|
||||||
|
{ type: "text" },
|
||||||
|
{ type: "text" },
|
||||||
|
], 0, "ckeditor-balloon")).toBe("hidden");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should be visible for two text notes", async () => {
|
||||||
|
expect(await testSplit([
|
||||||
|
{ type: "text" },
|
||||||
|
{ type: "text" },
|
||||||
|
])).toBe("visible");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should be disabled if on a non-text note", async () => {
|
||||||
|
expect(await testSplit([
|
||||||
|
{ type: "text" },
|
||||||
|
{ type: "code" },
|
||||||
|
], 1)).toBe("disabled");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should be hidden for all non-text notes", async () => {
|
||||||
|
expect(await testSplit([
|
||||||
|
{ type: "code" },
|
||||||
|
{ type: "canvas" },
|
||||||
|
])).toBe("hidden");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should be hidden for all read-only text notes", async () => {
|
||||||
|
expect(await testSplit([
|
||||||
|
{ type: "text", isReadOnly: true },
|
||||||
|
{ type: "text", isReadOnly: true },
|
||||||
|
])).toBe("hidden");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should be visible for mixed view mode", async () => {
|
||||||
|
expect(await testSplit([
|
||||||
|
{ type: "text" },
|
||||||
|
{ type: "text", viewScope: "attachments" }
|
||||||
|
])).toBe("visible");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should be hidden for all wrong view mode", async () => {
|
||||||
|
expect(await testSplit([
|
||||||
|
{ type: "text", viewScope: "attachments" },
|
||||||
|
{ type: "text", viewScope: "attachments" }
|
||||||
|
])).toBe("hidden");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should be disabled for wrong view mode", async () => {
|
||||||
|
expect(await testSplit([
|
||||||
|
{ type: "text" },
|
||||||
|
{ type: "text", viewScope: "attachments" }
|
||||||
|
], 1)).toBe("disabled");
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
});
|
||||||
@ -1,5 +1,9 @@
|
|||||||
import { useRef } from "preact/hooks";
|
import clsx from "clsx";
|
||||||
import { useTriliumEvent, useTriliumOption } from "../react/hooks";
|
import { useEffect, useRef, useState } from "preact/hooks";
|
||||||
|
|
||||||
|
import NoteContext from "../../components/note_context";
|
||||||
|
import FNote from "../../entities/fnote";
|
||||||
|
import { useActiveNoteContext, useNoteProperty, useTriliumEvent, useTriliumEvents, useTriliumOption } from "../react/hooks";
|
||||||
import { TabContext } from "./ribbon-interface";
|
import { TabContext } from "./ribbon-interface";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -33,5 +37,116 @@ export default function FormattingToolbar({ hidden, ntxId }: TabContext) {
|
|||||||
ref={containerRef}
|
ref={containerRef}
|
||||||
className={`classic-toolbar-widget ${hidden ? "hidden-ext" : ""}`}
|
className={`classic-toolbar-widget ${hidden ? "hidden-ext" : ""}`}
|
||||||
/>
|
/>
|
||||||
)
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const toolbarCache = new Map<string, HTMLElement | null | undefined>();
|
||||||
|
|
||||||
|
export function FixedFormattingToolbar() {
|
||||||
|
const containerRef = useRef<HTMLDivElement>(null);
|
||||||
|
const { note, noteContext, ntxId } = useActiveNoteContext();
|
||||||
|
const noteType = useNoteProperty(note, "type");
|
||||||
|
const renderState = useRenderState(noteContext, note);
|
||||||
|
const [ toolbarToRender, setToolbarToRender ] = useState<HTMLElement | null | undefined>();
|
||||||
|
|
||||||
|
// Populate the cache with the toolbar of every note context.
|
||||||
|
useTriliumEvent("textEditorRefreshed", ({ ntxId: eventNtxId, editor }) => {
|
||||||
|
if (!eventNtxId) return;
|
||||||
|
const toolbar = editor.ui.view.toolbar?.element;
|
||||||
|
toolbarCache.set(eventNtxId, toolbar);
|
||||||
|
// Replace on the spot if the editor crashed.
|
||||||
|
if (eventNtxId === ntxId) {
|
||||||
|
setToolbarToRender(toolbar);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Clean the cache when tabs are closed.
|
||||||
|
useTriliumEvent("noteContextRemoved", ({ ntxIds: eventNtxIds }) => {
|
||||||
|
for (const eventNtxId of eventNtxIds) {
|
||||||
|
toolbarCache.delete(eventNtxId);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Switch between the cached toolbar when user navigates to a different note context.
|
||||||
|
useEffect(() => {
|
||||||
|
if (!ntxId) return;
|
||||||
|
const toolbar = toolbarCache.get(ntxId);
|
||||||
|
if (toolbar) {
|
||||||
|
setToolbarToRender(toolbar);
|
||||||
|
}
|
||||||
|
}, [ ntxId, noteType, noteContext ]);
|
||||||
|
|
||||||
|
// Render the toolbar.
|
||||||
|
useEffect(() => {
|
||||||
|
if (toolbarToRender) {
|
||||||
|
containerRef.current?.replaceChildren(toolbarToRender);
|
||||||
|
} else {
|
||||||
|
containerRef.current?.replaceChildren();
|
||||||
|
}
|
||||||
|
}, [ toolbarToRender ]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
ref={containerRef}
|
||||||
|
className={clsx("classic-toolbar-widget", {
|
||||||
|
"hidden-ext": renderState === "hidden",
|
||||||
|
"disabled": renderState === "disabled"
|
||||||
|
})}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function useRenderState(activeNoteContext: NoteContext | undefined, activeNote: FNote | null | undefined) {
|
||||||
|
const [ textNoteEditorType ] = useTriliumOption("textNoteEditorType");
|
||||||
|
const [ state, setState ] = useState("hidden");
|
||||||
|
|
||||||
|
useTriliumEvents([ "newNoteContextCreated", "noteContextRemoved" ], () => {
|
||||||
|
getFormattingToolbarState(activeNoteContext, activeNote, textNoteEditorType).then(setState);
|
||||||
|
});
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
getFormattingToolbarState(activeNoteContext, activeNote, textNoteEditorType).then(setState);
|
||||||
|
}, [ activeNoteContext, activeNote, textNoteEditorType ]);
|
||||||
|
|
||||||
|
return state;
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function getFormattingToolbarState(activeNoteContext: NoteContext | undefined, activeNote: FNote | null | undefined, textNoteEditorType: string) {
|
||||||
|
if (!activeNoteContext || textNoteEditorType !== "ckeditor-classic") {
|
||||||
|
return "hidden";
|
||||||
|
}
|
||||||
|
|
||||||
|
const subContexts = activeNoteContext?.getMainContext().getSubContexts() ?? [];
|
||||||
|
if (subContexts.length === 1) {
|
||||||
|
if (activeNote?.type !== "text" || activeNoteContext.viewScope?.viewMode !== "default") {
|
||||||
|
return "hidden";
|
||||||
|
}
|
||||||
|
|
||||||
|
const isReadOnly = await activeNoteContext.isReadOnly();
|
||||||
|
if (isReadOnly) {
|
||||||
|
return "hidden";
|
||||||
|
}
|
||||||
|
|
||||||
|
return "visible";
|
||||||
|
}
|
||||||
|
|
||||||
|
// If there are multiple note contexts (e.g. splits), the logic is slightly different.
|
||||||
|
const textNoteContexts = subContexts.filter(s => s.note?.type === "text" && s.viewScope?.viewMode === "default");
|
||||||
|
const textNoteContextsReadOnly = await Promise.all(textNoteContexts.map(sc => sc.isReadOnly()));
|
||||||
|
|
||||||
|
// If all text notes are hidden, no need to display the toolbar at all.
|
||||||
|
if (textNoteContextsReadOnly.indexOf(false) === -1) {
|
||||||
|
return "hidden";
|
||||||
|
}
|
||||||
|
|
||||||
|
// If the current subcontext is not a text note, but there is at least an editable text then it must be disabled.
|
||||||
|
if (activeNote?.type !== "text") return "disabled";
|
||||||
|
|
||||||
|
// If the current subcontext is a text note, it must not be read-only.
|
||||||
|
const subContextIndex = textNoteContexts.indexOf(activeNoteContext);
|
||||||
|
if (subContextIndex !== -1) {
|
||||||
|
if (textNoteContextsReadOnly[subContextIndex]) return "disabled";
|
||||||
|
}
|
||||||
|
if (activeNoteContext.viewScope?.viewMode !== "default") return "disabled";
|
||||||
|
return "visible";
|
||||||
|
}
|
||||||
|
|||||||
@ -152,6 +152,15 @@
|
|||||||
opacity: 0.3;
|
opacity: 0.3;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
body.experimental-feature-new-layout .classic-toolbar-widget {
|
||||||
|
transition: opacity 250ms ease-in;
|
||||||
|
|
||||||
|
&.disabled {
|
||||||
|
opacity: 0.3;
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/* #endregion */
|
/* #endregion */
|
||||||
|
|
||||||
/* #region Script Tab */
|
/* #region Script Tab */
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user