-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy pathSearchHighlighterPlugin.tsx
82 lines (67 loc) · 2.59 KB
/
SearchHighlighterPlugin.tsx
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
import { useLexicalComposerContext } from '@lexical/react/LexicalComposerContext';
import { useEffect, useCallback } from 'react';
// doesn't work in Firefox (but is in Firefox nightly)
// https://developer.mozilla.org/en-US/docs/Web/API/CSS_Custom_Highlight_API#browser_compatibility
//
// highlights the given search terms in the editor
// then scrolls to the first search term and selects it
//
// this version of the plugin is designed to have the editor re-render if
// highlights need to be turned off. that can lead to the editor "jumping".
// to avoid that, you can have the plugin fetch search terms from a React context
// and remove the highlights if the search terms are deleted.
export function SearchHighlighterPlugin({
searchTerms
}: {
searchTerms: string[];
}): null {
const [editor] = useLexicalComposerContext();
const highlightSearchTerms = useCallback(() => {
if (!searchTerms || searchTerms.length === 0) return;
CSS.highlights?.clear();
const editorElement = editor.getRootElement();
if (!editorElement) return;
const ranges: Range[] = [];
const treeWalker = document.createTreeWalker(editorElement, NodeFilter.SHOW_TEXT);
let currentNode = treeWalker.nextNode();
while (currentNode) {
const text = currentNode.textContent?.toLowerCase() || '';
searchTerms.forEach(term => {
const termLower = term.toLowerCase();
let startPos = 0;
while (startPos < text.length) {
const index = text.indexOf(termLower, startPos);
if (index === -1) break;
if (currentNode) {
const range = new Range();
range.setStart(currentNode, index);
range.setEnd(currentNode, index + term.length);
ranges.push(range);
}
startPos = index + term.length;
}
});
currentNode = treeWalker.nextNode();
}
if (CSS.highlights && ranges.length > 0) {
const searchResultsHighlight = new Highlight(...ranges);
CSS.highlights.set("search-results", searchResultsHighlight);
if (ranges[0]) {
const firstElement = ranges[0].startContainer.parentElement;
firstElement?.scrollIntoView({ behavior: 'smooth', block: 'center' });
const selection = window.getSelection();
selection?.removeAllRanges();
selection?.addRange(ranges[0]);
}
}
}, [searchTerms, editor]);
useEffect(() => {
highlightSearchTerms();
return () => {
if (searchTerms.length > 0) {
CSS.highlights?.clear();
}
};
}, [editor, searchTerms, highlightSearchTerms]);
return null;
}