chore(react/collections): title highlighting in list title

This commit is contained in:
Elian Doran 2025-08-30 18:48:34 +03:00
parent f92948d65c
commit 68dff71512
No known key found for this signature in database
8 changed files with 96 additions and 70 deletions

View File

@ -448,7 +448,7 @@ function sleep(time_ms: number) {
}); });
} }
function escapeRegExp(str: string) { export function escapeRegExp(str: string) {
return str.replace(/([.*+?^=!:${}()|\[\]\/\\])/g, "\\$1"); return str.replace(/([.*+?^=!:${}()|\[\]\/\\])/g, "\\$1");
} }

View File

@ -6,30 +6,31 @@ import { ListView, GridView } from "./legacy/ListView";
import { useEffect, useState } from "preact/hooks"; import { useEffect, useState } from "preact/hooks";
interface NoteListProps { interface NoteListProps {
note?: FNote | null;
displayOnlyCollections?: boolean; displayOnlyCollections?: boolean;
highlightedTokens?: string[] | null;
} }
export default function NoteList({ }: NoteListProps) { export default function NoteList({ note: providedNote, highlightedTokens }: NoteListProps) {
const { note } = useNoteContext(); const { note: contextNote } = useNoteContext();
const note = providedNote ?? contextNote;
const viewType = useNoteViewType(note); const viewType = useNoteViewType(note);
const noteIds = useNoteIds(note, viewType); const noteIds = useNoteIds(note, viewType);
const isEnabled = (note && !!viewType); const isEnabled = (note && !!viewType);
// Refresh note Ids
return ( return (
<div className="note-list-widget"> <div className="note-list-widget">
{isEnabled && ( {isEnabled && (
<div className="note-list-widget-content"> <div className="note-list-widget-content">
{getComponentByViewType(note, noteIds, viewType)} {getComponentByViewType(note, noteIds, viewType, highlightedTokens)}
</div> </div>
)} )}
</div> </div>
); );
} }
function getComponentByViewType(note: FNote, noteIds: string[], viewType: ViewTypeOptions) { function getComponentByViewType(note: FNote, noteIds: string[], viewType: ViewTypeOptions, highlightedTokens: string[] | null | undefined) {
const props: ViewModeProps = { note, noteIds }; const props: ViewModeProps = { note, noteIds, highlightedTokens };
switch (viewType) { switch (viewType) {
case "list": case "list":

View File

@ -11,4 +11,5 @@ export interface ViewModeProps {
* We're using noteIds so that it's not necessary to load all notes at once when paging. * We're using noteIds so that it's not necessary to load all notes at once when paging.
*/ */
noteIds: string[]; noteIds: string[];
highlightedTokens: string[] | null | undefined;
} }

View File

@ -12,7 +12,7 @@ import link from "../../../services/link";
import { t } from "../../../services/i18n"; import { t } from "../../../services/i18n";
import attribute_renderer from "../../../services/attribute_renderer"; import attribute_renderer from "../../../services/attribute_renderer";
export function ListView({ note, noteIds: unfilteredNoteIds }: ViewModeProps) { export function ListView({ note, noteIds: unfilteredNoteIds, highlightedTokens }: ViewModeProps) {
const [ isExpanded ] = useNoteLabelBoolean(note, "expanded"); const [ isExpanded ] = useNoteLabelBoolean(note, "expanded");
const noteIds = useFilteredNoteIds(note, unfilteredNoteIds); const noteIds = useFilteredNoteIds(note, unfilteredNoteIds);
const { pageNotes, ...pagination } = usePagination(note, noteIds); const { pageNotes, ...pagination } = usePagination(note, noteIds);
@ -24,7 +24,7 @@ export function ListView({ note, noteIds: unfilteredNoteIds }: ViewModeProps) {
<div class="note-list-container use-tn-links"> <div class="note-list-container use-tn-links">
{pageNotes?.map(childNote => ( {pageNotes?.map(childNote => (
<ListNoteCard note={childNote} parentNote={note} expand={isExpanded} /> <ListNoteCard note={childNote} parentNote={note} expand={isExpanded} highlightedTokens={highlightedTokens} />
))} ))}
</div> </div>
@ -34,7 +34,7 @@ export function ListView({ note, noteIds: unfilteredNoteIds }: ViewModeProps) {
); );
} }
export function GridView({ note, noteIds: unfilteredNoteIds }: ViewModeProps) { export function GridView({ note, noteIds: unfilteredNoteIds, highlightedTokens }: ViewModeProps) {
const noteIds = useFilteredNoteIds(note, unfilteredNoteIds); const noteIds = useFilteredNoteIds(note, unfilteredNoteIds);
const { pageNotes, ...pagination } = usePagination(note, noteIds); const { pageNotes, ...pagination } = usePagination(note, noteIds);
@ -45,7 +45,7 @@ export function GridView({ note, noteIds: unfilteredNoteIds }: ViewModeProps) {
<div class="note-list-container use-tn-links"> <div class="note-list-container use-tn-links">
{pageNotes?.map(childNote => ( {pageNotes?.map(childNote => (
<GridNoteCard note={childNote} parentNote={note} /> <GridNoteCard note={childNote} parentNote={note} highlightedTokens={highlightedTokens} />
))} ))}
</div> </div>
@ -55,7 +55,7 @@ export function GridView({ note, noteIds: unfilteredNoteIds }: ViewModeProps) {
); );
} }
function ListNoteCard({ note, parentNote, expand }: { note: FNote, parentNote: FNote, expand?: boolean }) { function ListNoteCard({ note, parentNote, expand, highlightedTokens }: { note: FNote, parentNote: FNote, expand?: boolean, highlightedTokens: string[] | null | undefined }) {
const [ isExpanded, setExpanded ] = useState(expand); const [ isExpanded, setExpanded ] = useState(expand);
const notePath = getNotePath(parentNote, note); const notePath = getNotePath(parentNote, note);
@ -71,7 +71,7 @@ function ListNoteCard({ note, parentNote, expand }: { note: FNote, parentNote: F
/> />
<Icon className="note-icon" icon={note.getIcon()} /> <Icon className="note-icon" icon={note.getIcon()} />
<NoteLink className="note-book-title" notePath={notePath} noPreview showNotePath={note.type === "search"} /> <NoteLink className="note-book-title" notePath={notePath} noPreview showNotePath={note.type === "search"} highlightedTokens={highlightedTokens} />
<NoteAttributes note={note} /> <NoteAttributes note={note} />
{isExpanded && <> {isExpanded && <>
@ -83,7 +83,8 @@ function ListNoteCard({ note, parentNote, expand }: { note: FNote, parentNote: F
) )
} }
function GridNoteCard({ note, parentNote }: { note: FNote, parentNote: FNote }) { function GridNoteCard({ note, parentNote, highlightedTokens }: { note: FNote, parentNote: FNote, highlightedTokens: string[] | null | undefined }) {
const titleRef = useRef<HTMLSpanElement>(null);
const [ noteTitle, setNoteTitle ] = useState<string>(); const [ noteTitle, setNoteTitle ] = useState<string>();
const notePath = getNotePath(parentNote, note); const notePath = getNotePath(parentNote, note);
@ -100,7 +101,7 @@ function GridNoteCard({ note, parentNote }: { note: FNote, parentNote: FNote })
> >
<h5 className="note-book-header"> <h5 className="note-book-header">
<Icon className="note-icon" icon={note.getIcon()} /> <Icon className="note-icon" icon={note.getIcon()} />
<span 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 />
@ -149,7 +150,7 @@ function NoteChildren({ note, parentNote }: { note: FNote, parentNote: FNote })
}); });
}, [ note ]); }, [ note ]);
return childNotes?.map(childNote => <ListNoteCard note={childNote} parentNote={parentNote} />) return childNotes?.map(childNote => <ListNoteCard note={childNote} parentNote={parentNote} highlightedTokens={null} />)
} }
/** /**

View File

@ -1,6 +1,7 @@
import { useEffect, 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 RawHtml from "./RawHtml";
import { useSearchHighlighlighting } from "./hooks";
interface NoteLinkOpts { interface NoteLinkOpts {
className?: string; className?: string;
@ -10,11 +11,14 @@ interface NoteLinkOpts {
style?: Record<string, string | number>; style?: Record<string, string | number>;
noPreview?: boolean; noPreview?: boolean;
noTnLink?: boolean; noTnLink?: boolean;
highlightedTokens?: string[] | null | undefined;
} }
export default function NoteLink({ className, notePath, showNotePath, showNoteIcon, style, noPreview, noTnLink }: 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 [ jqueryEl, setJqueryEl ] = useState<JQuery<HTMLElement>>(); const [ jqueryEl, setJqueryEl ] = useState<JQuery<HTMLElement>>();
const containerRef = useRef<HTMLDivElement>(null);
useSearchHighlighlighting(containerRef, highlightedTokens);
useEffect(() => { useEffect(() => {
link.createLink(stringifiedNotePath, { showNotePath, showNoteIcon }) link.createLink(stringifiedNotePath, { showNotePath, showNoteIcon })
@ -38,6 +42,6 @@ export default function NoteLink({ className, notePath, showNotePath, showNoteIc
$linkEl?.addClass(className); $linkEl?.addClass(className);
} }
return <RawHtml html={jqueryEl} /> return <RawHtml containerRef={containerRef} html={jqueryEl} />
} }

View File

@ -1,4 +1,4 @@
import type { CSSProperties } from "preact/compat"; import type { CSSProperties, RefObject } from "preact/compat";
type HTMLElementLike = string | HTMLElement | JQuery<HTMLElement>; type HTMLElementLike = string | HTMLElement | JQuery<HTMLElement>;
@ -9,12 +9,12 @@ interface RawHtmlProps {
onClick?: (e: MouseEvent) => void; onClick?: (e: MouseEvent) => void;
} }
export default function RawHtml(props: RawHtmlProps) { export default function RawHtml({containerRef, ...props}: RawHtmlProps & { containerRef?: RefObject<HTMLSpanElement>}) {
return <span {...getProps(props)} />; return <span ref={containerRef} {...getProps(props)} />;
} }
export function RawHtmlBlock(props: RawHtmlProps) { export function RawHtmlBlock({containerRef, ...props}: RawHtmlProps & { containerRef?: RefObject<HTMLDivElement>}) {
return <div {...getProps(props)} /> return <div ref={containerRef} {...getProps(props)} />
} }
function getProps({ className, html, style, onClick }: RawHtmlProps) { function getProps({ className, html, style, onClick }: RawHtmlProps) {
@ -38,4 +38,4 @@ export function getHtml(html: string | HTMLElement | JQuery<HTMLElement>) {
return { return {
__html: html as string __html: html as string
}; };
} }

View File

@ -4,7 +4,7 @@ import { ParentComponent } from "./react_utils";
import SpacedUpdate from "../../services/spaced_update"; import SpacedUpdate from "../../services/spaced_update";
import { KeyboardActionNames, OptionNames } from "@triliumnext/commons"; import { KeyboardActionNames, OptionNames } from "@triliumnext/commons";
import options, { type OptionValue } from "../../services/options"; import options, { type OptionValue } from "../../services/options";
import utils, { reloadFrontendApp } from "../../services/utils"; import utils, { escapeRegExp, reloadFrontendApp } from "../../services/utils";
import NoteContext from "../../components/note_context"; import NoteContext from "../../components/note_context";
import BasicWidget, { ReactWrappedWidget } from "../basic_widget"; import BasicWidget, { ReactWrappedWidget } from "../basic_widget";
import FNote from "../../entities/fnote"; import FNote from "../../entities/fnote";
@ -15,6 +15,7 @@ import { RefObject, VNode } from "preact";
import { Tooltip } from "bootstrap"; import { Tooltip } from "bootstrap";
import { CSSProperties } from "preact/compat"; import { CSSProperties } from "preact/compat";
import keyboard_actions from "../../services/keyboard_actions"; import keyboard_actions from "../../services/keyboard_actions";
import Mark from "mark.js";
export function useTriliumEvent<T extends EventNames>(eventName: T, handler: (data: EventData<T>) => void) { export function useTriliumEvent<T extends EventNames>(eventName: T, handler: (data: EventData<T>) => void) {
const parentComponent = useContext(ParentComponent); const parentComponent = useContext(ParentComponent);
@ -27,7 +28,7 @@ export function useTriliumEvent<T extends EventNames>(eventName: T, handler: (da
export function useTriliumEvents<T extends EventNames>(eventNames: T[], handler: (data: EventData<T>, eventName: T) => void) { export function useTriliumEvents<T extends EventNames>(eventNames: T[], handler: (data: EventData<T>, eventName: T) => void) {
const parentComponent = useContext(ParentComponent); const parentComponent = useContext(ParentComponent);
useLayoutEffect(() => { useLayoutEffect(() => {
const handlers: ({ eventName: T, callback: (data: EventData<T>) => void })[] = []; const handlers: ({ eventName: T, callback: (data: EventData<T>) => void })[] = [];
for (const eventName of eventNames) { for (const eventName of eventNames) {
@ -35,11 +36,11 @@ export function useTriliumEvents<T extends EventNames>(eventNames: T[], handler:
handler(data, eventName); handler(data, eventName);
}}) }})
} }
for (const { eventName, callback } of handlers) { for (const { eventName, callback } of handlers) {
parentComponent?.registerHandler(eventName, callback); parentComponent?.registerHandler(eventName, callback);
} }
return (() => { return (() => {
for (const { eventName, callback } of handlers) { for (const { eventName, callback } of handlers) {
parentComponent?.removeHandler(eventName, callback); parentComponent?.removeHandler(eventName, callback);
@ -76,10 +77,10 @@ export function useSpacedUpdate(callback: () => void | Promise<void>, interval =
/** /**
* Allows a React component to read and write a Trilium option, while also watching for external changes. * Allows a React component to read and write a Trilium option, while also watching for external changes.
* *
* Conceptually, `useTriliumOption` works just like `useState`, but the value is also automatically updated if * Conceptually, `useTriliumOption` works just like `useState`, but the value is also automatically updated if
* the option is changed somewhere else in the client. * the option is changed somewhere else in the client.
* *
* @param name the name of the option to listen for. * @param name the name of the option to listen for.
* @param needsRefresh whether to reload the frontend whenever the value is changed. * @param needsRefresh whether to reload the frontend whenever the value is changed.
* @returns an array where the first value is the current option value and the second value is the setter. * @returns an array where the first value is the current option value and the second value is the setter.
@ -115,7 +116,7 @@ export function useTriliumOption(name: OptionNames, needsRefresh?: boolean): [st
/** /**
* Similar to {@link useTriliumOption}, but the value is converted to and from a boolean instead of a string. * Similar to {@link useTriliumOption}, but the value is converted to and from a boolean instead of a string.
* *
* @param name the name of the option to listen for. * @param name the name of the option to listen for.
* @param needsRefresh whether to reload the frontend whenever the value is changed. * @param needsRefresh whether to reload the frontend whenever the value is changed.
* @returns an array where the first value is the current option value and the second value is the setter. * @returns an array where the first value is the current option value and the second value is the setter.
@ -131,7 +132,7 @@ export function useTriliumOptionBool(name: OptionNames, needsRefresh?: boolean):
/** /**
* Similar to {@link useTriliumOption}, but the value is converted to and from a int instead of a string. * Similar to {@link useTriliumOption}, but the value is converted to and from a int instead of a string.
* *
* @param name the name of the option to listen for. * @param name the name of the option to listen for.
* @param needsRefresh whether to reload the frontend whenever the value is changed. * @param needsRefresh whether to reload the frontend whenever the value is changed.
* @returns an array where the first value is the current option value and the second value is the setter. * @returns an array where the first value is the current option value and the second value is the setter.
@ -147,7 +148,7 @@ export function useTriliumOptionInt(name: OptionNames): [number, (newValue: numb
/** /**
* Similar to {@link useTriliumOption}, but the object value is parsed to and from a JSON instead of a string. * Similar to {@link useTriliumOption}, but the object value is parsed to and from a JSON instead of a string.
* *
* @param name the name of the option to listen for. * @param name the name of the option to listen for.
* @returns an array where the first value is the current option value and the second value is the setter. * @returns an array where the first value is the current option value and the second value is the setter.
*/ */
@ -161,8 +162,8 @@ export function useTriliumOptionJson<T>(name: OptionNames): [ T, (newValue: T) =
} }
/** /**
* Similar to {@link useTriliumOption}, but operates with multiple options at once. * Similar to {@link useTriliumOption}, but operates with multiple options at once.
* *
* @param names the name of the option to listen for. * @param names the name of the option to listen for.
* @returns an array where the first value is a map where the keys are the option names and the values, and the second value is the setter which takes in the same type of map and saves them all at once. * @returns an array where the first value is a map where the keys are the option names and the values, and the second value is the setter which takes in the same type of map and saves them all at once.
*/ */
@ -182,10 +183,10 @@ export function useTriliumOptions<T extends OptionNames>(...names: T[]) {
/** /**
* Generates a unique name via a random alphanumeric string of a fixed length. * Generates a unique name via a random alphanumeric string of a fixed length.
* *
* <p> * <p>
* Generally used to assign names to inputs that are unique, especially useful for widgets inside tabs. * Generally used to assign names to inputs that are unique, especially useful for widgets inside tabs.
* *
* @param prefix a prefix to add to the unique name. * @param prefix a prefix to add to the unique name.
* @returns a name with the given prefix and a random alpanumeric string appended to it. * @returns a name with the given prefix and a random alpanumeric string appended to it.
*/ */
@ -196,7 +197,7 @@ export function useUniqueName(prefix?: string) {
export function useNoteContext() { export function useNoteContext() {
const [ noteContext, setNoteContext ] = useState<NoteContext>(); const [ noteContext, setNoteContext ] = useState<NoteContext>();
const [ notePath, setNotePath ] = useState<string | null | undefined>(); const [ notePath, setNotePath ] = useState<string | null | undefined>();
const [ note, setNote ] = useState<FNote | null | undefined>(); const [ note, setNote ] = useState<FNote | null | undefined>();
const [ refreshCounter, setRefreshCounter ] = useState(0); const [ refreshCounter, setRefreshCounter ] = useState(0);
useEffect(() => { useEffect(() => {
@ -205,7 +206,7 @@ export function useNoteContext() {
useTriliumEvents([ "setNoteContext", "activeContextChanged", "noteSwitchedAndActivated", "noteSwitched" ], ({ noteContext }) => { useTriliumEvents([ "setNoteContext", "activeContextChanged", "noteSwitchedAndActivated", "noteSwitched" ], ({ noteContext }) => {
setNoteContext(noteContext); setNoteContext(noteContext);
setNotePath(noteContext.notePath); setNotePath(noteContext.notePath);
}); });
useTriliumEvent("frocaReloaded", () => { useTriliumEvent("frocaReloaded", () => {
setNote(noteContext?.note); setNote(noteContext?.note);
@ -235,7 +236,7 @@ export function useNoteContext() {
/** /**
* Allows a React component to listen to obtain a property of a {@link FNote} while also automatically watching for changes, either via the user changing to a different note or the property being changed externally. * Allows a React component to listen to obtain a property of a {@link FNote} while also automatically watching for changes, either via the user changing to a different note or the property being changed externally.
* *
* @param note the {@link FNote} whose property to obtain. * @param note the {@link FNote} whose property to obtain.
* @param property a property of a {@link FNote} to obtain the value from (e.g. `title`, `isProtected`). * @param property a property of a {@link FNote} to obtain the value from (e.g. `title`, `isProtected`).
* @param componentId optionally, constricts the refresh of the value if an update occurs externally via the component ID of a legacy widget. This can be used to avoid external data replacing fresher, user-inputted data. * @param componentId optionally, constricts the refresh of the value if an update occurs externally via the component ID of a legacy widget. This can be used to avoid external data replacing fresher, user-inputted data.
@ -287,7 +288,7 @@ export function useNoteRelation(note: FNote | undefined | null, relationName: st
/** /**
* Allows a React component to read or write a note's label while also reacting to changes in value. * Allows a React component to read or write a note's label while also reacting to changes in value.
* *
* @param note the note whose label to read/write. * @param note the note whose label to read/write.
* @param labelName the name of the label to read/write. * @param labelName the name of the label to read/write.
* @returns an array where the first element is the getter and the second element is the setter. The setter has a special behaviour for convenience: if the value is undefined, the label is created without a value (e.g. a tag), if the value is null then the label is removed. * @returns an array where the first element is the getter and the second element is the setter. The setter has a special behaviour for convenience: if the value is undefined, the label is created without a value (e.g. a tag), if the value is null then the label is removed.
@ -352,9 +353,9 @@ export function useNoteLabelBoolean(note: FNote | undefined | null, labelName: s
export function useNoteBlob(note: FNote | null | undefined): [ FBlob | null | undefined ] { export function useNoteBlob(note: FNote | null | undefined): [ FBlob | null | undefined ] {
const [ blob, setBlob ] = useState<FBlob | null>(); const [ blob, setBlob ] = useState<FBlob | null>();
function refresh() { function refresh() {
note?.getBlob().then(setBlob); note?.getBlob().then(setBlob);
} }
useEffect(refresh, [ note?.noteId ]); useEffect(refresh, [ note?.noteId ]);
@ -388,7 +389,7 @@ export function useLegacyWidget<T extends BasicWidget>(widgetFactory: () => T, {
if (noteContext && widget instanceof NoteContextAwareWidget) { if (noteContext && widget instanceof NoteContextAwareWidget) {
widget.setNoteContextEvent({ noteContext }); widget.setNoteContextEvent({ noteContext });
} }
const renderedWidget = widget.render(); const renderedWidget = widget.render();
return [ widget, renderedWidget ]; return [ widget, renderedWidget ];
}, []); }, []);
@ -415,7 +416,7 @@ export function useLegacyWidget<T extends BasicWidget>(widgetFactory: () => T, {
/** /**
* Attaches a {@link ResizeObserver} to the given ref and reads the bounding client rect whenever it changes. * Attaches a {@link ResizeObserver} to the given ref and reads the bounding client rect whenever it changes.
* *
* @param ref a ref to a {@link HTMLElement} to determine the size and observe the changes in size. * @param ref a ref to a {@link HTMLElement} to determine the size and observe the changes in size.
* @returns the size of the element, reacting to changes. * @returns the size of the element, reacting to changes.
*/ */
@ -445,7 +446,7 @@ export function useElementSize(ref: RefObject<HTMLElement>) {
/** /**
* Obtains the inner width and height of the window, as well as reacts to changes in size. * Obtains the inner width and height of the window, as well as reacts to changes in size.
* *
* @returns the width and height of the window. * @returns the width and height of the window.
*/ */
export function useWindowSize() { export function useWindowSize() {
@ -453,7 +454,7 @@ export function useWindowSize() {
windowWidth: window.innerWidth, windowWidth: window.innerWidth,
windowHeight: window.innerHeight windowHeight: window.innerHeight
}); });
useEffect(() => { useEffect(() => {
function onResize() { function onResize() {
setSize({ setSize({
@ -499,7 +500,7 @@ export function useTooltip(elRef: RefObject<HTMLElement>, config: Partial<Toolti
/** /**
* Similar to {@link useTooltip}, but doesn't expose methods to imperatively hide or show the tooltip. * Similar to {@link useTooltip}, but doesn't expose methods to imperatively hide or show the tooltip.
* *
* @param elRef the element to bind the tooltip to. * @param elRef the element to bind the tooltip to.
* @param config optionally, the tooltip configuration. * @param config optionally, the tooltip configuration.
*/ */
@ -547,4 +548,28 @@ export function useSyncedRef<T>(externalRef?: RefObject<T>, initialValue: T | nu
}, [ ref, externalRef ]); }, [ ref, externalRef ]);
return ref; return ref;
} }
export function useSearchHighlighlighting(ref: RefObject<HTMLElement>, highlightedTokens: string[] | null | undefined) {
const mark = useRef<Mark>();
const highlightRegex = useMemo(() => {
if (!highlightedTokens?.length) return null;
const regex = highlightedTokens.map((token) => escapeRegExp(token)).join("|");
return new RegExp(regex, "gi")
}, [ highlightedTokens ]);
useEffect(() => {
if (!ref.current || !highlightRegex) return;
if (!mark.current) {
mark.current = new Mark(ref.current);
}
mark.current.markRegExp(highlightRegex, {
element: "span",
className: "ck-find-result"
});
return () => mark.current?.unmark();
});
}

View File

@ -1,11 +1,12 @@
import { useEffect, useRef, useState } from "preact/hooks"; import { useEffect, useState } from "preact/hooks";
import { t } from "../services/i18n"; import { t } from "../services/i18n";
import Alert from "./react/Alert"; import Alert from "./react/Alert";
import { useNoteContext, useNoteProperty, useTriliumEvent } from "./react/hooks"; import { useNoteContext, useTriliumEvent } from "./react/hooks";
import "./search_result.css"; import "./search_result.css";
import NoteList from "./collections/NoteList";
// import NoteListRenderer from "../services/note_list_renderer"; // import NoteListRenderer from "../services/note_list_renderer";
enum SearchResultState { enum SearchResultState {
NO_RESULTS, NO_RESULTS,
NOT_EXECUTED, NOT_EXECUTED,
GOT_RESULTS GOT_RESULTS
@ -14,27 +15,18 @@ enum SearchResultState {
export default function SearchResult() { export default function SearchResult() {
const { note, ntxId } = useNoteContext(); const { note, ntxId } = useNoteContext();
const [ state, setState ] = useState<SearchResultState>(); const [ state, setState ] = useState<SearchResultState>();
const searchContainerRef = useRef<HTMLDivElement>(null); const [ highlightedTokens, setHighlightedTokens ] = useState<string[]>();
function refresh() { function refresh() {
searchContainerRef.current?.replaceChildren();
if (note?.type !== "search") { if (note?.type !== "search") {
setState(undefined); setState(undefined);
} else if (!note?.searchResultsLoaded) { } else if (!note?.searchResultsLoaded) {
setState(SearchResultState.NOT_EXECUTED); setState(SearchResultState.NOT_EXECUTED);
} else if (note.getChildNoteIds().length === 0) { } else if (note.getChildNoteIds().length === 0) {
setState(SearchResultState.NO_RESULTS); setState(SearchResultState.NO_RESULTS);
} else if (searchContainerRef.current) { } else {
setState(SearchResultState.GOT_RESULTS); setState(SearchResultState.GOT_RESULTS);
setHighlightedTokens(note.highlightedTokens);
// TODO: Fix me.
// const noteListRenderer = new NoteListRenderer({
// $parent: $(searchContainerRef.current),
// parentNote: note,
// showNotePath: true
// });
// noteListRenderer.renderList();
} }
} }
@ -60,7 +52,9 @@ export default function SearchResult() {
<Alert type="info" className="search-no-results">{t("search_result.no_notes_found")}</Alert> <Alert type="info" className="search-no-results">{t("search_result.no_notes_found")}</Alert>
)} )}
<div ref={searchContainerRef} className="search-result-widget-content" /> {state === SearchResultState.GOT_RESULTS && (
<NoteList note={note} highlightedTokens={highlightedTokens} />
)}
</div> </div>
); );
} }