chore(react/collections): content highlighting in list

This commit is contained in:
Elian Doran 2025-08-30 19:03:18 +03:00
parent 68dff71512
commit 1cee01a22a
No known key found for this signature in database
3 changed files with 29 additions and 22 deletions

View File

@ -2,7 +2,7 @@ import { useEffect, useMemo, useRef, useState } from "preact/hooks";
import FNote from "../../../entities/fnote"; import FNote from "../../../entities/fnote";
import Icon from "../../react/Icon"; import Icon from "../../react/Icon";
import { ViewModeProps } from "../interface"; import { ViewModeProps } from "../interface";
import { useNoteLabelBoolean, useNoteProperty } from "../../react/hooks"; import { useNoteLabelBoolean, useImperativeSearchHighlighlighting } from "../../react/hooks";
import NoteLink from "../../react/NoteLink"; import NoteLink from "../../react/NoteLink";
import "./ListOrGridView.css"; import "./ListOrGridView.css";
import content_renderer from "../../../services/content_renderer"; import content_renderer from "../../../services/content_renderer";
@ -75,8 +75,8 @@ function ListNoteCard({ note, parentNote, expand, highlightedTokens }: { note: F
<NoteAttributes note={note} /> <NoteAttributes note={note} />
{isExpanded && <> {isExpanded && <>
<NoteContent note={note} /> <NoteContent note={note} highlightedTokens={highlightedTokens} />
<NoteChildren note={note} parentNote={parentNote} /> <NoteChildren note={note} parentNote={parentNote} highlightedTokens={highlightedTokens} />
</>} </>}
</h5> </h5>
</div> </div>
@ -104,7 +104,7 @@ function GridNoteCard({ note, parentNote, highlightedTokens }: { note: FNote, pa
<span ref={titleRef} className="note-book-title">{noteTitle}</span> <span ref={titleRef} className="note-book-title">{noteTitle}</span>
<NoteAttributes note={note} /> <NoteAttributes note={note} />
</h5> </h5>
<NoteContent note={note} trim /> <NoteContent note={note} trim highlightedTokens={highlightedTokens} />
</div> </div>
) )
} }
@ -120,26 +120,29 @@ function NoteAttributes({ note }: { note: FNote }) {
return <span className="note-list-attributes" ref={ref} /> return <span className="note-list-attributes" ref={ref} />
} }
function NoteContent({ note, trim }: { note: FNote, trim?: boolean }) { function NoteContent({ note, trim, highlightedTokens }: { note: FNote, trim?: boolean, highlightedTokens }) {
const contentRef = useRef<HTMLDivElement>(null); const contentRef = useRef<HTMLDivElement>(null);
const highlightSearch = useImperativeSearchHighlighlighting(highlightedTokens);
useEffect(() => { useEffect(() => {
content_renderer.getRenderedContent(note, { trim }) content_renderer.getRenderedContent(note, { trim })
.then(({ $renderedContent, type }) => { .then(({ $renderedContent, type }) => {
contentRef.current?.replaceChildren(...$renderedContent); if (!contentRef.current) return;
contentRef.current?.classList.add(`type-${type}`); contentRef.current.replaceChildren(...$renderedContent);
contentRef.current.classList.add(`type-${type}`);
highlightSearch(contentRef.current);
}) })
.catch(e => { .catch(e => {
console.warn(`Caught error while rendering note '${note.noteId}' of type '${note.type}'`); console.warn(`Caught error while rendering note '${note.noteId}' of type '${note.type}'`);
console.error(e); console.error(e);
contentRef.current?.replaceChildren(t("collections.rendering_error")); contentRef.current?.replaceChildren(t("collections.rendering_error"));
}) })
}, [ note ]); }, [ note, highlightedTokens ]);
return <div ref={contentRef} className="note-book-content" />; return <div ref={contentRef} className="note-book-content" />;
} }
function NoteChildren({ note, parentNote }: { note: FNote, parentNote: FNote }) { function NoteChildren({ note, parentNote, highlightedTokens }: { note: FNote, parentNote: FNote, highlightedTokens: string[] | null | undefined }) {
const imageLinks = note.getRelations("imageLink"); const imageLinks = note.getRelations("imageLink");
const [ childNotes, setChildNotes ] = useState<FNote[]>(); const [ childNotes, setChildNotes ] = useState<FNote[]>();
@ -150,7 +153,7 @@ function NoteChildren({ note, parentNote }: { note: FNote, parentNote: FNote })
}); });
}, [ note ]); }, [ note ]);
return childNotes?.map(childNote => <ListNoteCard note={childNote} parentNote={parentNote} highlightedTokens={null} />) return childNotes?.map(childNote => <ListNoteCard note={childNote} parentNote={parentNote} highlightedTokens={highlightedTokens} />)
} }
/** /**

View File

@ -1,7 +1,6 @@
import { useEffect, useRef, useState } from "preact/hooks"; import { useEffect, useRef, useState } from "preact/hooks";
import link from "../../services/link"; import link from "../../services/link";
import RawHtml from "./RawHtml"; import { useImperativeSearchHighlighlighting } from "./hooks";
import { useSearchHighlighlighting } from "./hooks";
interface NoteLinkOpts { interface NoteLinkOpts {
className?: string; className?: string;
@ -16,15 +15,21 @@ interface NoteLinkOpts {
export default function NoteLink({ className, notePath, showNotePath, showNoteIcon, style, noPreview, noTnLink, highlightedTokens }: NoteLinkOpts) { export default function NoteLink({ className, notePath, showNotePath, showNoteIcon, style, noPreview, noTnLink, highlightedTokens }: NoteLinkOpts) {
const stringifiedNotePath = Array.isArray(notePath) ? notePath.join("/") : notePath; const stringifiedNotePath = Array.isArray(notePath) ? notePath.join("/") : notePath;
const ref = useRef<HTMLSpanElement>(null);
const [ jqueryEl, setJqueryEl ] = useState<JQuery<HTMLElement>>(); const [ jqueryEl, setJqueryEl ] = useState<JQuery<HTMLElement>>();
const containerRef = useRef<HTMLDivElement>(null); const highlightSearch = useImperativeSearchHighlighlighting(highlightedTokens);
useSearchHighlighlighting(containerRef, highlightedTokens);
useEffect(() => { useEffect(() => {
link.createLink(stringifiedNotePath, { showNotePath, showNoteIcon }) link.createLink(stringifiedNotePath, { showNotePath, showNoteIcon })
.then(setJqueryEl); .then(setJqueryEl);
}, [ stringifiedNotePath, showNotePath ]); }, [ stringifiedNotePath, showNotePath ]);
useEffect(() => {
if (!ref.current || !jqueryEl) return;
ref.current.replaceChildren(jqueryEl[0]);
highlightSearch(ref.current);
}, [ jqueryEl ]);
if (style) { if (style) {
jqueryEl?.css(style); jqueryEl?.css(style);
} }
@ -42,6 +47,6 @@ export default function NoteLink({ className, notePath, showNotePath, showNoteIc
$linkEl?.addClass(className); $linkEl?.addClass(className);
} }
return <RawHtml containerRef={containerRef} html={jqueryEl} /> return <span ref={ref} />
} }

View File

@ -550,7 +550,7 @@ export function useSyncedRef<T>(externalRef?: RefObject<T>, initialValue: T | nu
return ref; return ref;
} }
export function useSearchHighlighlighting(ref: RefObject<HTMLElement>, highlightedTokens: string[] | null | undefined) { export function useImperativeSearchHighlighlighting(highlightedTokens: string[] | null | undefined) {
const mark = useRef<Mark>(); const mark = useRef<Mark>();
const highlightRegex = useMemo(() => { const highlightRegex = useMemo(() => {
if (!highlightedTokens?.length) return null; if (!highlightedTokens?.length) return null;
@ -558,18 +558,17 @@ export function useSearchHighlighlighting(ref: RefObject<HTMLElement>, highlight
return new RegExp(regex, "gi") return new RegExp(regex, "gi")
}, [ highlightedTokens ]); }, [ highlightedTokens ]);
useEffect(() => { return (el: HTMLElement) => {
if (!ref.current || !highlightRegex) return; if (!el || !highlightRegex) return;
if (!mark.current) { if (!mark.current) {
mark.current = new Mark(ref.current); mark.current = new Mark(el);
} }
mark.current.unmark();
mark.current.markRegExp(highlightRegex, { mark.current.markRegExp(highlightRegex, {
element: "span", element: "span",
className: "ck-find-result" className: "ck-find-result"
}); });
};
return () => mark.current?.unmark();
});
} }