refactor(react/type_widgets): deduplicate containers

This commit is contained in:
Elian Doran 2025-10-05 16:36:59 +03:00
parent 22069d0aef
commit c209a699ea
No known key found for this signature in database
21 changed files with 206 additions and 183 deletions

View file

@ -103,6 +103,7 @@ export default function NoteDetail() {
key={type}
type={type as ExtendedNoteType}
isVisible={activeNoteType === type}
isFullHeight={isFullHeight}
props={props}
/>
})}
@ -114,7 +115,7 @@ export default function NoteDetail() {
* Wraps a single note type widget, in order to keep it in the DOM even after the user has switched away to another note type. This allows faster loading of the same note type again. The properties are cached, so that they are updated only
* while the widget is visible, to avoid rendering in the background. When not visible, the DOM element is simply hidden.
*/
function NoteDetailWrapper({ Element, type, isVisible, props }: { Element: (props: TypeWidgetProps) => VNode, type: ExtendedNoteType, isVisible: boolean, props: TypeWidgetProps }) {
function NoteDetailWrapper({ Element, type, isVisible, isFullHeight, props }: { Element: (props: TypeWidgetProps) => VNode, type: ExtendedNoteType, isVisible: boolean, isFullHeight: boolean, props: TypeWidgetProps }) {
const [ cachedProps, setCachedProps ] = useState(props);
useEffect(() => {
@ -125,10 +126,15 @@ function NoteDetailWrapper({ Element, type, isVisible, props }: { Element: (prop
}
}, [ isVisible ]);
const typeMapping = TYPE_MAPPINGS[type];
return (
<div className={`note-detail-${type}`} style={{
display: !isVisible ? "none" : ""
}}>
<div
className={`${typeMapping.className} ${typeMapping.printable ? "note-detail-printable" : ""}`}
style={{
display: !isVisible ? "none" : "",
height: isFullHeight ? "100%" : ""
}}
>
{ <Element {...cachedProps} /> }
</div>
);

View file

@ -18,73 +18,117 @@ type NoteTypeView = () => Promise<{ default: TypeWidget } | TypeWidget> | ((prop
interface NoteTypeMapping {
view: NoteTypeView;
printable?: boolean;
/** The class name to assign to the note type wrapper */
className: string;
}
export const TYPE_MAPPINGS: Record<ExtendedNoteType, NoteTypeMapping> = {
empty: {
view: () => import("./type_widgets/Empty"),
className: "note-detail-empty",
printable: true
},
doc: {
view: () => import("./type_widgets/Doc")
view: () => import("./type_widgets/Doc"),
className: "note-detail-doc",
printable: true
},
search: {
view: () => <div className="note-detail-none note-detail-printable" />
view: () => <></>,
className: "note-detail-none",
printable: true
},
protectedSession: {
view: () => import("./type_widgets/ProtectedSession")
view: () => import("./type_widgets/ProtectedSession"),
className: "protected-session-password-component"
},
book: {
view: () => import("./type_widgets/Book")
view: () => import("./type_widgets/Book"),
className: "note-detail-book",
printable: true,
},
contentWidget: {
view: () => import("./type_widgets/ContentWidget")
view: () => import("./type_widgets/ContentWidget"),
className: "note-detail-content-widget",
printable: true
},
webView: {
view: () => import("./type_widgets/WebView")
view: () => import("./type_widgets/WebView"),
className: "note-detail-web-view",
printable: true
},
file: {
view: () => import("./type_widgets/File")
view: () => import("./type_widgets/File"),
className: "note-detail-file",
printable: true
},
image: {
view: () => import("./type_widgets/Image")
view: () => import("./type_widgets/Image"),
className: "note-detail-image",
printable: true
},
readOnlyCode: {
view: async () => (await import("./type_widgets/code/Code")).ReadOnlyCode
view: async () => (await import("./type_widgets/code/Code")).ReadOnlyCode,
className: "note-detail-readonly-code",
printable: true
},
editableCode: {
view: async () => (await import("./type_widgets/code/Code")).EditableCode
view: async () => (await import("./type_widgets/code/Code")).EditableCode,
className: "note-detail-code",
printable: true
},
mermaid: {
view: () => import("./type_widgets/Mermaid")
view: () => import("./type_widgets/Mermaid"),
className: "note-detail-mermaid",
printable: true
},
mindMap: {
view: () => import("./type_widgets/MindMap")
view: () => import("./type_widgets/MindMap"),
className: "note-detail-mind-map",
printable: true
},
attachmentList: {
view: async () => (await import("./type_widgets/Attachment")).AttachmentList
view: async () => (await import("./type_widgets/Attachment")).AttachmentList,
className: "attachment-list",
printable: true
},
attachmentDetail: {
view: async () => (await import("./type_widgets/Attachment")).AttachmentDetail
view: async () => (await import("./type_widgets/Attachment")).AttachmentDetail,
className: "attachment-detail",
printable: true
},
readOnlyText: {
view: () => import("./type_widgets/text/ReadOnlyText")
view: () => import("./type_widgets/text/ReadOnlyText"),
className: "note-detail-readonly-text"
},
editableText: {
view: () => import("./type_widgets/text/EditableText")
view: () => import("./type_widgets/text/EditableText"),
className: "note-detail-editable-text",
printable: true
},
render: {
view: () => import("./type_widgets/Render")
view: () => import("./type_widgets/Render"),
className: "note-detail-render",
printable: true
},
canvas: {
view: () => import("./type_widgets/Canvas")
view: () => import("./type_widgets/Canvas"),
className: "note-detail-canvas",
printable: true
},
relationMap: {
view: () => import("./type_widgets/relation_map/RelationMap")
view: () => import("./type_widgets/relation_map/RelationMap"),
className: "note-detail-relation-map",
printable: true
},
noteMap: {
view: () => import("./type_widgets/NoteMap")
view: () => import("./type_widgets/NoteMap"),
className: "note-detail-note-map",
printable: true
},
aiChat: {
view: () => import("./type_widgets/AiChat")
view: () => import("./type_widgets/AiChat"),
className: "ai-chat-widget-container"
}
};

View file

@ -31,7 +31,6 @@ export default function AiChat({ note, noteContext }: TypeWidgetProps) {
return llmChatPanel;
}, {
noteContext,
containerClassName: "ai-chat-widget-container",
containerStyle: {
height: "100%"
}

View file

@ -46,7 +46,7 @@ export function AttachmentList({ note }: TypeWidgetProps) {
});
return (
<div className="attachment-list note-detail-printable">
<>
<AttachmentListHeader noteId={note.noteId} />
<div className="attachment-list-wrapper">
@ -58,7 +58,7 @@ export function AttachmentList({ note }: TypeWidgetProps) {
</Alert>
)}
</div>
</div>
</>
)
}
@ -99,7 +99,7 @@ export function AttachmentDetail({ note, viewScope }: TypeWidgetProps) {
}, [ viewScope ]);
return (
<div className="attachment-detail note-detail-printable">
<>
<div className="links-wrapper use-tn-links">
{t("attachment_detail.owning_note")}{" "}
<NoteLink notePath={note.noteId} />
@ -122,7 +122,7 @@ export function AttachmentDetail({ note, viewScope }: TypeWidgetProps) {
<strong>{t("attachment_detail.attachment_deleted")}</strong>
)}
</div>
</div>
</>
)
}

View file

@ -23,13 +23,9 @@ export default function Book({ note }: TypeWidgetProps) {
}
});
return (
<div className="note-detail-book note-detail-printable">
{(shouldDisplayNoChildrenWarning && (
<Alert type="warning" className="note-detail-book-empty-help">
<RawHtml html={t("book.no_children_help")} />
</Alert>
))}
</div>
return (shouldDisplayNoChildrenWarning &&
<Alert type="warning" className="note-detail-book-empty-help">
<RawHtml html={t("book.no_children_help")} />
</Alert>
)
}

View file

@ -60,28 +60,26 @@ export default function Canvas({ note }: TypeWidgetProps) {
}, []);
return (
<div className="canvas-widget note-detail-canvas note-detail-printable note-detail full-height" onWheel={onWheel}>
<div className="canvas-render">
<div className="excalidraw-wrapper">
<Excalidraw
excalidrawAPI={api => apiRef.current = api}
theme={themeStyle}
viewModeEnabled={isReadOnly || options.is("databaseReadonly")}
zenModeEnabled={false}
isCollaborating={false}
detectScroll={false}
handleKeyboardGlobally={false}
autoFocus={false}
UIOptions={{
canvasActions: {
saveToActiveFile: false,
export: false
}
}}
onLinkOpen={onLinkOpen}
{...persistence}
/>
</div>
<div className="canvas-render" onWheel={onWheel}>
<div className="excalidraw-wrapper">
<Excalidraw
excalidrawAPI={api => apiRef.current = api}
theme={themeStyle}
viewModeEnabled={isReadOnly || options.is("databaseReadonly")}
zenModeEnabled={false}
isCollaborating={false}
detectScroll={false}
handleKeyboardGlobally={false}
autoFocus={false}
UIOptions={{
canvasActions: {
saveToActiveFile: false,
export: false
}
}}
onLinkOpen={onLinkOpen}
{...persistence}
/>
</div>
</div>
)

View file

@ -50,12 +50,10 @@ const CONTENT_WIDGETS: Record<OptionPages | "_backendLog", (props: TypeWidgetPro
export default function ContentWidget({ note, ...restProps }: TypeWidgetProps) {
const Content = CONTENT_WIDGETS[note.noteId];
return (
<div className="note-detail-content-widget note-detail-printable">
<div className={`note-detail-content-widget-content ${note.noteId.startsWith("_options") ? "options" : ""}`}>
{Content
? <Content note={note} {...restProps} />
: (t("content_widget.unknown_widget", { id: note.noteId }))}
</div>
<div className={`note-detail-content-widget-content ${note.noteId.startsWith("_options") ? "options" : ""}`}>
{Content
? <Content note={note} {...restProps} />
: (t("content_widget.unknown_widget", { id: note.noteId }))}
</div>
)
}

View file

@ -18,15 +18,15 @@
border: 1px solid var(--main-border-color);
}
.note-detail-doc.contextual-help {
.note-detail-doc-content.contextual-help {
padding-bottom: 0;
}
.note-detail-doc.contextual-help h2,
.note-detail-doc.contextual-help h3,
.note-detail-doc.contextual-help h4,
.note-detail-doc.contextual-help h5,
.note-detail-doc.contextual-help h6 {
.note-detail-doc-content.contextual-help h2,
.note-detail-doc-content.contextual-help h3,
.note-detail-doc-content.contextual-help h4,
.note-detail-doc-content.contextual-help h5,
.note-detail-doc-content.contextual-help h6 {
font-size: 1.25rem;
background-color: var(--main-background-color);
position: sticky;

View file

@ -27,12 +27,10 @@ export default function Doc({ note, viewScope, ntxId }: TypeWidgetProps) {
});
return (
<div className={`note-detail-doc note-detail-printable ${viewScope?.viewMode === "contextual-help" ? "contextual-help" : ""}`}>
<RawHtmlBlock
containerRef={containerRef}
className="note-detail-doc-content ck-content"
html={html}
/>
</div>
<RawHtmlBlock
containerRef={containerRef}
className={`note-detail-doc-content ck-content ${viewScope?.viewMode === "contextual-help" ? "contextual-help" : ""}`}
html={html}
/>
);
}

View file

@ -12,10 +12,10 @@ import { TypeWidgetProps } from "./type_widget";
export default function Empty({ }: TypeWidgetProps) {
return (
<div class="note-detail-empty note-detail-printable">
<>
<WorkspaceSwitcher />
<NoteSearch />
</div>
</>
)
}

View file

@ -12,24 +12,17 @@ const TEXT_MAX_NUM_CHARS = 5000;
export default function File({ note }: TypeWidgetProps) {
const blob = useNoteBlob(note);
let preview: VNode | null = null;
if (blob?.content) {
preview = <TextPreview content={blob.content} />
return <TextPreview content={blob.content} />
} else if (note.mime === "application/pdf") {
preview = <PdfPreview note={note} />
return <PdfPreview note={note} />
} else if (note.mime.startsWith("video/")) {
preview = <VideoPreview note={note} />
return <VideoPreview note={note} />
} else if (note.mime.startsWith("audio/")) {
preview = <AudioPreview note={note} />
return <AudioPreview note={note} />
} else {
preview = <NoPreview />
return <NoPreview />
}
return (
<div className="note-detail-file note-detail-printable">
{preview}
</div>
);
}
function TextPreview({ content }: { content: string }) {

View file

@ -41,14 +41,12 @@ export default function Image({ note, ntxId }: TypeWidgetProps) {
});
return (
<div className="note-detail-image note-detail-printable">
<div ref={containerRef} className="note-detail-image-wrapper">
<img
id={uniqueId}
className="note-detail-image-view"
src={createImageSrcUrl(note)}
/>
</div>
<div ref={containerRef} className="note-detail-image-wrapper">
<img
id={uniqueId}
className="note-detail-image-view"
src={createImageSrcUrl(note)}
/>
</div>
)
}

View file

@ -7,7 +7,7 @@ import nodeMenu from "@mind-elixir/node-menu";
import "mind-elixir/style";
import "@mind-elixir/node-menu/dist/style.css";
import "./MindMap.css";
import { useEditorSpacedUpdate, useNoteLabelBoolean, useTriliumEvent, useTriliumEvents, useTriliumOptionBool } from "../react/hooks";
import { useEditorSpacedUpdate, useNoteLabelBoolean, useSyncedRef, useTriliumEvent, useTriliumEvents, useTriliumOptionBool } from "../react/hooks";
import { refToJQuerySelector } from "../react/react_utils";
import utils from "../../services/utils";
@ -16,6 +16,7 @@ const NEW_TOPIC_NAME = "";
interface MindElixirProps {
apiRef?: RefObject<MindElixirInstance>;
containerProps?: Omit<HTMLAttributes<HTMLDivElement>, "ref">;
containerRef?: RefObject<HTMLDivElement>;
editable: boolean;
content: MindElixirData;
onChange?: () => void;
@ -94,23 +95,22 @@ export default function MindMap({ note, ntxId }: TypeWidgetProps) {
}, []);
return (
<div ref={containerRef} className="note-detail-mind-map note-detail-printable">
<MindElixir
apiRef={apiRef}
content={content}
onChange={() => spacedUpdate.scheduleUpdate()}
editable={!isReadOnly}
containerProps={{
className: "mind-map-container",
onKeyDown
}}
/>
</div>
<MindElixir
containerRef={containerRef}
apiRef={apiRef}
content={content}
onChange={() => spacedUpdate.scheduleUpdate()}
editable={!isReadOnly}
containerProps={{
className: "mind-map-container",
onKeyDown
}}
/>
)
}
function MindElixir({ content, containerProps, apiRef: externalApiRef, onChange, editable }: MindElixirProps) {
const containerRef = useRef<HTMLDivElement>(null);
function MindElixir({ content, containerRef: externalContainerRef, containerProps, apiRef: externalApiRef, onChange, editable }: MindElixirProps) {
const containerRef = useSyncedRef<HTMLDivElement>(externalContainerRef, null);
const apiRef = useRef<MindElixirInstance>(null);
useEffect(() => {

View file

@ -6,7 +6,7 @@ export default function NoteMap({ note }: TypeWidgetProps) {
const containerRef = useRef<HTMLDivElement>(null);
return (
<div ref={containerRef} className="note-detail-note-map note-detail-printable">
<div ref={containerRef}>
<NoteMapEl parentRef={containerRef} note={note} widgetMode="type" />
</div>
);

View file

@ -20,23 +20,21 @@ export default function ProtectedSession() {
}, [ passwordRef ]);
return (
<div className="protected-session-password-component note-detail-printable">
<form class="protected-session-password-form" onSubmit={submitCallback}>
<FormGroup name="protected-session-password-in-detail" label={t("protected_session.enter_password_instruction")}>
<FormTextBox
type="password"
className="protected-session-password"
autocomplete="current-password"
inputRef={passwordRef}
/>
</FormGroup>
<Button
text={t("protected_session.start_session_button")}
primary
keyboardShortcut="Enter"
<form class="protected-session-password-form" onSubmit={submitCallback}>
<FormGroup name="protected-session-password-in-detail" label={t("protected_session.enter_password_instruction")}>
<FormTextBox
type="password"
className="protected-session-password"
autocomplete="current-password"
inputRef={passwordRef}
/>
</form>
</div>
</FormGroup>
<Button
text={t("protected_session.start_session_button")}
primary
keyboardShortcut="Enter"
/>
</form>
)
}

View file

@ -38,7 +38,7 @@ export default function Render({ note, noteContext, ntxId }: TypeWidgetProps) {
});
return (
<div className="note-detail-render note-detail-printable">
<>
{!renderNotesFound && (
<Alert className="note-detail-render-help" type="warning">
<p><strong>{t("render.note_detail_render_help_1")}</strong></p>
@ -47,6 +47,6 @@ export default function Render({ note, noteContext, ntxId }: TypeWidgetProps) {
)}
<div ref={contentRef} className="note-detail-render-content" />
</div>
</>
);
}

View file

@ -10,13 +10,10 @@ const isElectron = utils.isElectron();
export default function WebView({ note }: TypeWidgetProps) {
const [ webViewSrc ] = useNoteLabel(note, "webViewSrc");
return (
<div className="note-detail-web-view note-detail-printable">
{webViewSrc
? <WebViewContent src={webViewSrc} />
: <WebViewHelp />}
</div>
)
return (webViewSrc
? <WebViewContent src={webViewSrc} />
: <WebViewHelp />
);
}
function WebViewContent({ src }: { src: string }) {

View file

@ -44,15 +44,13 @@ export function ReadOnlyCode({ note, viewScope, ntxId, parentComponent }: TypeWi
}, [ blob ]);
return (
<div className="note-detail-readonly-code note-detail-printable">
<CodeEditor
ntxId={ntxId} parentComponent={parentComponent}
className="note-detail-readonly-code-content"
content={content}
mime={note.mime}
readOnly
/>
</div>
<CodeEditor
ntxId={ntxId} parentComponent={parentComponent}
className="note-detail-readonly-code-content"
content={content}
mime={note.mime}
readOnly
/>
)
}
@ -93,7 +91,7 @@ export function EditableCode({ note, ntxId, debounceUpdate, parentComponent, upd
useKeyboardShortcuts("code-detail", containerRef, parentComponent);
return (
<div className="note-detail-code note-detail-printable">
<>
<CodeEditor
ntxId={ntxId} parentComponent={parentComponent}
editorRef={editorRef} containerRef={containerRef}
@ -119,7 +117,7 @@ export function EditableCode({ note, ntxId, debounceUpdate, parentComponent, upd
<TouchBarButton icon="NSImageNameTouchBarPlayTemplate" click={() => appContext.triggerCommand("runActiveNote")} />
)}
</TouchBar>
</div>
</>
)
}

View file

@ -133,30 +133,28 @@ export default function RelationMap({ note, ntxId }: TypeWidgetProps) {
useRelationData(note.noteId, data, mapApiRef, pbApiRef);
return (
<div className="note-detail-relation-map note-detail-printable">
<div
className="relation-map-wrapper"
onClick={clickCallback}
{...dragProps}
<div
className="relation-map-wrapper"
onClick={clickCallback}
{...dragProps}
>
<JsPlumb
apiRef={pbApiRef}
containerRef={containerRef}
className="relation-map-container"
props={{
Endpoint: ["Dot", { radius: 2 }],
Connector: "StateMachine",
ConnectionOverlays: uniDirectionalOverlays,
HoverPaintStyle: { stroke: "#777", strokeWidth: 1 },
}}
onInstanceCreated={setupOverlays}
onConnection={connectionCallback}
>
<JsPlumb
apiRef={pbApiRef}
containerRef={containerRef}
className="relation-map-container"
props={{
Endpoint: ["Dot", { radius: 2 }],
Connector: "StateMachine",
ConnectionOverlays: uniDirectionalOverlays,
HoverPaintStyle: { stroke: "#777", strokeWidth: 1 },
}}
onInstanceCreated={setupOverlays}
onConnection={connectionCallback}
>
{data?.notes.map(note => (
<NoteBox {...note} mapApiRef={mapApiRef} />
))}
</JsPlumb>
</div>
{data?.notes.map(note => (
<NoteBox {...note} mapApiRef={mapApiRef} />
))}
</JsPlumb>
</div>
)
}

View file

@ -1,7 +1,7 @@
import { HTMLProps, RefObject, useEffect, useImperativeHandle, useRef, useState } from "preact/compat";
import { PopupEditor, ClassicEditor, EditorWatchdog, type WatchdogConfig, CKTextEditor, TemplateDefinition } from "@triliumnext/ckeditor5";
import { buildConfig, BuildEditorOptions } from "./config";
import { useLegacyImperativeHandlers } from "../../react/hooks";
import { useLegacyImperativeHandlers, useSyncedRef } from "../../react/hooks";
import link from "../../../services/link";
import froca from "../../../services/froca";
@ -30,10 +30,11 @@ interface CKEditorWithWatchdogProps extends Pick<HTMLProps<HTMLDivElement>, "cla
onEditorInitialized?: (editor: CKTextEditor) => void;
editorApi: RefObject<CKEditorApi>;
templates: TemplateDefinition[];
containerRef?: RefObject<HTMLDivElement>;
}
export default function CKEditorWithWatchdog({ content, contentLanguage, className, tabIndex, isClassicEditor, watchdogRef: externalWatchdogRef, watchdogConfig, onNotificationWarning, onWatchdogStateChange, onChange, onEditorInitialized, editorApi, templates }: CKEditorWithWatchdogProps) {
const containerRef = useRef<HTMLDivElement>(null);
export default function CKEditorWithWatchdog({ containerRef: externalContainerRef, content, contentLanguage, className, tabIndex, isClassicEditor, watchdogRef: externalWatchdogRef, watchdogConfig, onNotificationWarning, onWatchdogStateChange, onChange, onEditorInitialized, editorApi, templates }: CKEditorWithWatchdogProps) {
const containerRef = useSyncedRef<HTMLDivElement>(externalContainerRef, null);
const watchdogRef = useRef<EditorWatchdog>(null);
const [ editor, setEditor ] = useState<CKTextEditor>();

View file

@ -185,9 +185,10 @@ export default function EditableText({ note, parentComponent, ntxId, noteContext
});
return (
<div ref={containerRef} class={`note-detail-editable-text note-detail-printable ${codeBlockWordWrap ? "word-wrap" : ""}`}>
<>
{note && !!templates && <CKEditorWithWatchdog
className="note-detail-editable-text-editor use-tn-links"
containerRef={containerRef}
className={`note-detail-editable-text-editor use-tn-links ${codeBlockWordWrap ? "word-wrap" : ""}`}
tabIndex={300}
content={content}
contentLanguage={language}
@ -229,7 +230,7 @@ export default function EditableText({ note, parentComponent, ntxId, noteContext
/>}
<EditableTextTouchBar watchdogRef={watchdogRef} refreshTouchBarRef={refreshTouchBarRef} />
</div>
</>
)
}