Skip to content

Commit

Permalink
Desktop: Resolves laurent22#6088: Highlight search terms in the rich …
Browse files Browse the repository at this point in the history
…text editor
  • Loading branch information
personalizedrefrigerator committed Jan 26, 2024
1 parent 4826974 commit 52bef58
Show file tree
Hide file tree
Showing 6 changed files with 137 additions and 3 deletions.
1 change: 1 addition & 0 deletions .eslintignore
Original file line number Diff line number Diff line change
Expand Up @@ -277,6 +277,7 @@ packages/app-desktop/gui/NoteEditor/NoteBody/TinyMCE/utils/shouldPasteResources.
packages/app-desktop/gui/NoteEditor/NoteBody/TinyMCE/utils/shouldPasteResources.js
packages/app-desktop/gui/NoteEditor/NoteBody/TinyMCE/utils/types.js
packages/app-desktop/gui/NoteEditor/NoteBody/TinyMCE/utils/useContextMenu.js
packages/app-desktop/gui/NoteEditor/NoteBody/TinyMCE/utils/useHighlightedSearchTerms.js
packages/app-desktop/gui/NoteEditor/NoteBody/TinyMCE/utils/useScroll.js
packages/app-desktop/gui/NoteEditor/NoteEditor.js
packages/app-desktop/gui/NoteEditor/NoteTitle/NoteTitleBar.js
Expand Down
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -257,6 +257,7 @@ packages/app-desktop/gui/NoteEditor/NoteBody/TinyMCE/utils/shouldPasteResources.
packages/app-desktop/gui/NoteEditor/NoteBody/TinyMCE/utils/shouldPasteResources.js
packages/app-desktop/gui/NoteEditor/NoteBody/TinyMCE/utils/types.js
packages/app-desktop/gui/NoteEditor/NoteBody/TinyMCE/utils/useContextMenu.js
packages/app-desktop/gui/NoteEditor/NoteBody/TinyMCE/utils/useHighlightedSearchTerms.js
packages/app-desktop/gui/NoteEditor/NoteBody/TinyMCE/utils/useScroll.js
packages/app-desktop/gui/NoteEditor/NoteEditor.js
packages/app-desktop/gui/NoteEditor/NoteTitle/NoteTitleBar.js
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ import { joplinCommandToTinyMceCommands, TinyMceCommand } from './utils/joplinCo
import shouldPasteResources from './utils/shouldPasteResources';
import lightTheme from '@joplin/lib/themes/light';
import { Options as NoteStyleOptions } from '@joplin/renderer/noteStyle';
import useHighlightedSearchTerms from './utils/useHighlightedSearchTerms';
const md5 = require('md5');
const { clipboard } = require('electron');
const supportedLocales = require('./supportedLocales');
Expand Down Expand Up @@ -938,6 +939,8 @@ const TinyMCE = (props: NoteBodyEditorProps, ref: any) => {
};
}, [editor, onEditorContentClick]);

useHighlightedSearchTerms(editor, props.searchMarkers.keywords, props.themeId);

// This is to handle dropping notes on the editor. In this case, we add an
// overlay over the editor, which makes it a valid drop target. This in
// turn makes NoteEditor get the drop event and dispatch it.
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,128 @@
import SearchEngine from '@joplin/lib/services/search/SearchEngine';
import { themeStyle } from '@joplin/lib/theme';
import { Theme } from '@joplin/lib/themes/type';
import { useEffect, useMemo } from 'react';
import { Editor } from 'tinymce';

// TODO: Remove after upgrading TypeScript.
// NOTE: While Highlight is Set-like, its API may be slightly different.
declare global {
interface Window {
Highlight: any;
Range: any;
CSS: any;
}
}

const useHighlightedSearchTerms = (editor: Editor, searchTerms: string[], themeId: number) => {
const searchRegexes = useMemo(() => {
return searchTerms.map(term => {
if (typeof term === 'object') {
term = (term as any).value;
}
return new RegExp(SearchEngine.instance().queryTermToRegex(term), 'ig');
});
}, [searchTerms]);

useEffect(() => {
if (!editor) {
return () => {};
}

const theme: Theme = themeStyle(themeId);
const style = editor.dom.create('style', {}, `
::highlight(jop-search-highlight) {
background-color: ${theme.searchMarkerBackgroundColor};
color: ${theme.searchMarkerColor};
}
/* Try to work around a bug on chrome where misspellings also have the
same color as search markers. */
::spelling-error, ::highlight(none) {
color: inherit;
}
`);
editor.getDoc().head.appendChild(style);

return () => {
style.remove();
};
}, [editor, themeId]);

useEffect(() => {
if (!editor) {
return () => {};
}

const editorWindow = editor.getWin();
const ranges: Map<Node, Range[]> = new Map();
let highlight: any = undefined;

const processNode = (node: Node) => {
for (const child of node.childNodes) {
if (child.nodeName === '#text') {
for (const term of searchRegexes) {
const matches = child.textContent.matchAll(term);
const childRanges = [];

for (const match of matches) {
const range: Range = new editorWindow.Range();
range.setStart(child, match.index ?? 0);
range.setEnd(child, (match.index ?? 0) + match[0].length);
childRanges.push(range);
}

ranges.set(child, childRanges);
}
} else {
processNode(child);
}
}
};

const rebuildHighlights = (element: Node) => {
highlight?.clear();

processNode(element);

highlight = new editorWindow.Highlight(...[...ranges.values()].flat());
editorWindow.CSS.highlights.set('jop-search-highlight', highlight);
};

const onNodeChange = ({ element }: any) => {
rebuildHighlights(element);
};

const onKeyUp = (_event: KeyboardEvent) => {
// Use selectedNode and not event.target -- event.target seems to always point
// to the body.
const selectedNode = editor.selection.getNode();
if (selectedNode) {
rebuildHighlights(selectedNode);
}
};

const onSetContent = () => {
rebuildHighlights(editorWindow.document.body);
};

editor.on('NodeChange', onNodeChange);
editor.on('SetContent', onSetContent);

// NodeChange doesn't fire while typing, so we also need keyup
editor.on('keyup', onKeyUp);

rebuildHighlights(editorWindow.document.body);

return () => {
highlight?.clear();
editorWindow.CSS.highlights.delete('jop-search-highlight');

editor.off('NodeChange', onNodeChange);
editor.off('keyup', onKeyUp);
editor.off('SetContent', onSetContent);
};
}, [searchRegexes, editor]);
};

export default useHighlightedSearchTerms;
3 changes: 2 additions & 1 deletion packages/app-desktop/gui/NoteEditor/utils/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import { RenderResult, RenderResultPluginAsset } from '@joplin/renderer/types';
import { MarkupToHtmlOptions } from './useMarkupToHtml';
import { Dispatch } from 'redux';
import { ProcessResultsRow } from '@joplin/lib/services/search/SearchEngine';
import { SearchMarkers } from './useSearchMarkers';

export interface AllAssetsOptions {
contentMaxWidthTarget?: string;
Expand Down Expand Up @@ -90,7 +91,7 @@ export interface NoteBodyEditorProps {
dispatch: Function;
noteToolbar: any;
setLocalSearchResultCount(count: number): void;
searchMarkers: any;
searchMarkers: SearchMarkers;
visiblePanes: string[];
keyboardMode: string;
resourceInfos: ResourceInfos;
Expand Down
4 changes: 2 additions & 2 deletions packages/lib/services/search/SearchEngine.ts
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,7 @@ export interface ComplexTerm {
type: 'regex' | 'text';
value: string;
scriptType: any;
valueRegex?: RegExp;
valueRegex?: string;
}

export interface Terms {
Expand Down Expand Up @@ -491,7 +491,7 @@ export default class SearchEngine {
}

// https://stackoverflow.com/a/13818704/561309
public queryTermToRegex(term: any) {
public queryTermToRegex(term: any): string {
while (term.length && term.indexOf('*') === 0) {
term = term.substr(1);
}
Expand Down

0 comments on commit 52bef58

Please sign in to comment.