react-highlight-me 2.0.4 → 2.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +28 -0
- package/dist/cjs/index.d.ts +9 -1
- package/dist/cjs/index.js +81 -23
- package/dist/cjs/index.js.map +1 -1
- package/dist/esm/index.d.ts +9 -1
- package/dist/esm/index.js +81 -24
- package/dist/esm/index.js.map +1 -1
- package/package.json +1 -1
- package/src/index.tsx +115 -37
package/README.md
CHANGED
|
@@ -124,6 +124,33 @@ function App() {
|
|
|
124
124
|
</TextHighlighter>
|
|
125
125
|
```
|
|
126
126
|
|
|
127
|
+
### Gettting highlighted words
|
|
128
|
+
|
|
129
|
+
```jsx
|
|
130
|
+
import TextHighlighter from 'react-highlight-me';
|
|
131
|
+
function App() {
|
|
132
|
+
const highlightedWords = ['React', 'JavaScript'];
|
|
133
|
+
|
|
134
|
+
return (
|
|
135
|
+
<>
|
|
136
|
+
<TextHighlighter words={highlightedWords}>
|
|
137
|
+
<div>
|
|
138
|
+
React is a JavaScript library for building user interfaces.
|
|
139
|
+
</div>
|
|
140
|
+
</TextHighlighter>
|
|
141
|
+
<div>
|
|
142
|
+
<h2>Highlighted Words:</h2>
|
|
143
|
+
<ul>
|
|
144
|
+
{Array.from(document.querySelectorAll(TextHighlighter.MARKS_IN_SCOPE_SELECTOR)).map((mark, index) => (
|
|
145
|
+
<li key={index}>{mark.textContent}</li>
|
|
146
|
+
))}
|
|
147
|
+
</ul>
|
|
148
|
+
</div>
|
|
149
|
+
</>
|
|
150
|
+
);
|
|
151
|
+
}
|
|
152
|
+
```
|
|
153
|
+
|
|
127
154
|
## Props
|
|
128
155
|
|
|
129
156
|
| Prop | Type | Default | Description |
|
|
@@ -133,6 +160,7 @@ function App() {
|
|
|
133
160
|
| `highlightStyle` | `React.CSSProperties` | `{ backgroundColor: 'yellow', fontWeight: 'bold' }` | CSS styles to apply to highlighted text |
|
|
134
161
|
| `caseSensitive` | `boolean` | `false` | Whether to perform case-sensitive matching |
|
|
135
162
|
| `isWordBoundary` | `boolean` | `false` | Match words only at word boundaries |
|
|
163
|
+
| `escapeRegex` | `RegExp` | `/[.*+?^${}()\|[\]\\]/g` | Escape special regex characters |
|
|
136
164
|
|
|
137
165
|
## Examples
|
|
138
166
|
|
package/dist/cjs/index.d.ts
CHANGED
|
@@ -6,6 +6,14 @@ type Props = {
|
|
|
6
6
|
caseSensitive?: boolean;
|
|
7
7
|
isWordBoundary?: boolean;
|
|
8
8
|
isDebug?: boolean;
|
|
9
|
+
escapeRegex?: RegExp;
|
|
9
10
|
};
|
|
10
|
-
declare const
|
|
11
|
+
export declare const MARK_SELECTOR = "mark[data-highlighter=\"true\"]";
|
|
12
|
+
export declare const MARKS_IN_SCOPE_SELECTOR = ":scope mark[data-highlighter=\"true\"]:not(:scope [data-id=\"react-highlight-me\"] mark[data-highlighter=\"true\"])";
|
|
13
|
+
interface TextHighlighterComponent extends React.ForwardRefExoticComponent<Props & React.RefAttributes<HTMLDivElement>> {
|
|
14
|
+
MARKS_IN_SCOPE_SELECTOR: string;
|
|
15
|
+
MARK_SELECTOR: string;
|
|
16
|
+
ROOT_ELEMENT_SELECTOR: string;
|
|
17
|
+
}
|
|
18
|
+
declare const TextHighlighter: TextHighlighterComponent;
|
|
11
19
|
export default TextHighlighter;
|
package/dist/cjs/index.js
CHANGED
|
@@ -33,34 +33,50 @@ var __importStar = (this && this.__importStar) || (function () {
|
|
|
33
33
|
};
|
|
34
34
|
})();
|
|
35
35
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
36
|
+
exports.MARKS_IN_SCOPE_SELECTOR = exports.MARK_SELECTOR = void 0;
|
|
36
37
|
const react_1 = __importStar(require("react"));
|
|
37
|
-
|
|
38
|
-
const
|
|
38
|
+
const ROOT_ELEMENT_ID = 'react-highlight-me';
|
|
39
|
+
const ROOT_ELEMENT_ATTR = 'data-id';
|
|
40
|
+
const ROOT_ELEMENT_SELECTOR = `[${ROOT_ELEMENT_ATTR}="${ROOT_ELEMENT_ID}"]`;
|
|
41
|
+
const MARK_ATTRIBUTE = 'data-highlighter';
|
|
42
|
+
exports.MARK_SELECTOR = `mark[${MARK_ATTRIBUTE}="true"]`;
|
|
43
|
+
exports.MARKS_IN_SCOPE_SELECTOR = `:scope ${exports.MARK_SELECTOR}:not(:scope ${ROOT_ELEMENT_SELECTOR} ${exports.MARK_SELECTOR})`;
|
|
44
|
+
const TextHighlighter = (0, react_1.forwardRef)(({ children, words = [], highlightStyle = { backgroundColor: 'yellow', fontWeight: 'bold' }, caseSensitive = false, isWordBoundary = false, isDebug = false, escapeRegex = /[.*+?^${}()|[\]\\]/g, }, ref) => {
|
|
39
45
|
const containerRef = (0, react_1.useRef)(null);
|
|
40
46
|
const observerRef = (0, react_1.useRef)(null);
|
|
41
47
|
const lastHighlightSignature = (0, react_1.useRef)('');
|
|
42
48
|
const [isInitiallyReady, setIsInitiallyReady] = (0, react_1.useState)(false);
|
|
43
|
-
const
|
|
44
|
-
propsRef
|
|
49
|
+
const currentProps = { words, highlightStyle, caseSensitive, isWordBoundary, escapeRegex };
|
|
50
|
+
const propsRef = (0, react_1.useRef)(currentProps);
|
|
51
|
+
propsRef.current = currentProps;
|
|
52
|
+
const nodeFilter = {
|
|
53
|
+
acceptNode: (node) => {
|
|
54
|
+
if (!containerRef.current)
|
|
55
|
+
return NodeFilter.FILTER_SKIP;
|
|
56
|
+
return isInMyScope(containerRef.current, node, ROOT_ELEMENT_SELECTOR)
|
|
57
|
+
? NodeFilter.FILTER_ACCEPT
|
|
58
|
+
: NodeFilter.FILTER_SKIP;
|
|
59
|
+
}
|
|
60
|
+
};
|
|
45
61
|
const getTextSignature = (0, react_1.useCallback)((element) => {
|
|
46
62
|
var _a;
|
|
47
63
|
// Create a signature of all text content to detect real changes
|
|
48
|
-
const walker = document.createTreeWalker(element, NodeFilter.SHOW_TEXT,
|
|
64
|
+
const walker = document.createTreeWalker(element, NodeFilter.SHOW_TEXT, nodeFilter);
|
|
49
65
|
const textParts = [];
|
|
50
66
|
let node;
|
|
51
67
|
while ((node = walker.nextNode())) {
|
|
52
|
-
if (!((_a = node.parentElement) === null || _a === void 0 ? void 0 : _a.hasAttribute(
|
|
68
|
+
if (!((_a = node.parentElement) === null || _a === void 0 ? void 0 : _a.hasAttribute(MARK_ATTRIBUTE))) {
|
|
53
69
|
textParts.push(node.textContent || '');
|
|
54
70
|
}
|
|
55
71
|
}
|
|
56
72
|
return textParts.join('|');
|
|
57
73
|
}, []);
|
|
58
74
|
const highlightTextInElement = (0, react_1.useCallback)((element) => {
|
|
59
|
-
const { words, highlightStyle, caseSensitive, isWordBoundary } = propsRef.current;
|
|
75
|
+
const { words, highlightStyle, caseSensitive, isWordBoundary, escapeRegex } = propsRef.current;
|
|
60
76
|
const wordsArray = Array.isArray(words) ? words : [words];
|
|
61
77
|
isDebug && console.log('Highlighting with words:', wordsArray);
|
|
62
78
|
// Remove existing highlights
|
|
63
|
-
const existingMarks = element.querySelectorAll(
|
|
79
|
+
const existingMarks = element.querySelectorAll(TextHighlighter.MARKS_IN_SCOPE_SELECTOR);
|
|
64
80
|
existingMarks.forEach(mark => {
|
|
65
81
|
var _a;
|
|
66
82
|
const textContent = mark.textContent || '';
|
|
@@ -72,8 +88,9 @@ const TextHighlighter = ({ children, words = [], highlightStyle = { backgroundCo
|
|
|
72
88
|
lastHighlightSignature.current = getTextSignature(element);
|
|
73
89
|
return;
|
|
74
90
|
}
|
|
91
|
+
isDebug && console.log('Text signature:', getTextSignature(element));
|
|
75
92
|
// Apply highlighting (same logic as before)
|
|
76
|
-
const walker = document.createTreeWalker(element, NodeFilter.SHOW_TEXT,
|
|
93
|
+
const walker = document.createTreeWalker(element, NodeFilter.SHOW_TEXT, nodeFilter);
|
|
77
94
|
const textNodes = [];
|
|
78
95
|
let node;
|
|
79
96
|
while ((node = walker.nextNode())) {
|
|
@@ -82,7 +99,7 @@ const TextHighlighter = ({ children, words = [], highlightStyle = { backgroundCo
|
|
|
82
99
|
textNodes.forEach(textNode => {
|
|
83
100
|
var _a;
|
|
84
101
|
const text = textNode.textContent || '';
|
|
85
|
-
if (!text
|
|
102
|
+
if (!text)
|
|
86
103
|
return;
|
|
87
104
|
const pattern = wordsArray
|
|
88
105
|
.filter(word => word)
|
|
@@ -90,7 +107,7 @@ const TextHighlighter = ({ children, words = [], highlightStyle = { backgroundCo
|
|
|
90
107
|
if (word instanceof RegExp) {
|
|
91
108
|
return word.source;
|
|
92
109
|
}
|
|
93
|
-
let term = word.replace(
|
|
110
|
+
let term = escapeRegex ? word.replace(escapeRegex, '\\$&') : word;
|
|
94
111
|
if (isWordBoundary) {
|
|
95
112
|
term = `\\b${term}\\b`;
|
|
96
113
|
}
|
|
@@ -113,12 +130,12 @@ const TextHighlighter = ({ children, words = [], highlightStyle = { backgroundCo
|
|
|
113
130
|
return testRegex.test(part);
|
|
114
131
|
}
|
|
115
132
|
return caseSensitive
|
|
116
|
-
? part === word
|
|
117
|
-
: part.toLowerCase() === word.
|
|
133
|
+
? part === word
|
|
134
|
+
: part.toLowerCase() === word.toLowerCase();
|
|
118
135
|
});
|
|
119
136
|
if (shouldHighlight) {
|
|
120
137
|
const mark = document.createElement('mark');
|
|
121
|
-
mark.setAttribute(
|
|
138
|
+
mark.setAttribute(MARK_ATTRIBUTE, 'true');
|
|
122
139
|
Object.assign(mark.style, highlightStyle);
|
|
123
140
|
mark.textContent = part;
|
|
124
141
|
fragment.appendChild(mark);
|
|
@@ -134,27 +151,41 @@ const TextHighlighter = ({ children, words = [], highlightStyle = { backgroundCo
|
|
|
134
151
|
lastHighlightSignature.current = getTextSignature(element);
|
|
135
152
|
}, [getTextSignature]);
|
|
136
153
|
(0, react_1.useLayoutEffect)(() => {
|
|
137
|
-
|
|
154
|
+
const currentRef = ref && 'current' in ref ? ref.current : containerRef.current;
|
|
155
|
+
if (!currentRef)
|
|
138
156
|
return;
|
|
139
157
|
isDebug && console.log('Setting up persistent MutationObserver');
|
|
140
|
-
highlightTextInElement(
|
|
158
|
+
highlightTextInElement(currentRef);
|
|
141
159
|
setIsInitiallyReady(true);
|
|
142
160
|
observerRef.current = new MutationObserver((mutations) => {
|
|
143
|
-
if (!
|
|
161
|
+
if (!currentRef)
|
|
162
|
+
return;
|
|
163
|
+
const myMutations = mutations.filter((mutation) => {
|
|
164
|
+
if (isInMyScope(currentRef, mutation.target, ROOT_ELEMENT_SELECTOR)) {
|
|
165
|
+
// This mutation is in my scope - process it
|
|
166
|
+
isDebug && console.log('Processing mutation in my scope:', currentRef.getAttribute(ROOT_ELEMENT_ATTR));
|
|
167
|
+
return true;
|
|
168
|
+
}
|
|
169
|
+
// Otherwise ignore - it's either from outer scope or nested scope
|
|
170
|
+
return false;
|
|
171
|
+
});
|
|
172
|
+
if (myMutations.length === 0) {
|
|
173
|
+
isDebug && console.log('No relevant mutations in my scope, skipping');
|
|
144
174
|
return;
|
|
175
|
+
}
|
|
145
176
|
// Check if the text signature has actually changed
|
|
146
|
-
const currentSignature = getTextSignature(
|
|
177
|
+
const currentSignature = getTextSignature(currentRef);
|
|
147
178
|
if (currentSignature !== lastHighlightSignature.current) {
|
|
148
179
|
isDebug && console.log('Text signature changed, re-highlighting');
|
|
149
180
|
isDebug && console.log('Old:', lastHighlightSignature.current);
|
|
150
181
|
isDebug && console.log('New:', currentSignature);
|
|
151
|
-
highlightTextInElement(
|
|
182
|
+
highlightTextInElement(currentRef);
|
|
152
183
|
}
|
|
153
184
|
else {
|
|
154
185
|
isDebug && console.log('Text signature unchanged, ignoring mutation');
|
|
155
186
|
}
|
|
156
187
|
});
|
|
157
|
-
observerRef.current.observe(
|
|
188
|
+
observerRef.current.observe(currentRef, {
|
|
158
189
|
childList: true,
|
|
159
190
|
subtree: true,
|
|
160
191
|
characterData: true
|
|
@@ -164,11 +195,38 @@ const TextHighlighter = ({ children, words = [], highlightStyle = { backgroundCo
|
|
|
164
195
|
observerRef.current.disconnect();
|
|
165
196
|
}
|
|
166
197
|
};
|
|
167
|
-
}, [words, highlightStyle, caseSensitive, isWordBoundary, highlightTextInElement, getTextSignature]);
|
|
168
|
-
return (react_1.default.createElement("div", {
|
|
198
|
+
}, [words, highlightStyle, caseSensitive, isWordBoundary, highlightTextInElement, getTextSignature, ref, escapeRegex]);
|
|
199
|
+
return (react_1.default.createElement("div", { [ROOT_ELEMENT_ATTR]: ROOT_ELEMENT_ID, ref: (node) => {
|
|
200
|
+
containerRef.current = node;
|
|
201
|
+
if (typeof ref === 'function') {
|
|
202
|
+
ref(node);
|
|
203
|
+
}
|
|
204
|
+
else if (ref) {
|
|
205
|
+
ref.current = node;
|
|
206
|
+
}
|
|
207
|
+
}, style: {
|
|
169
208
|
visibility: isInitiallyReady ? 'visible' : 'hidden',
|
|
170
209
|
minHeight: isInitiallyReady ? 'auto' : '1em'
|
|
171
210
|
} }, children));
|
|
172
|
-
};
|
|
211
|
+
});
|
|
212
|
+
TextHighlighter.MARK_SELECTOR = exports.MARK_SELECTOR;
|
|
213
|
+
TextHighlighter.ROOT_ELEMENT_SELECTOR = ROOT_ELEMENT_SELECTOR;
|
|
214
|
+
TextHighlighter.MARKS_IN_SCOPE_SELECTOR = exports.MARKS_IN_SCOPE_SELECTOR;
|
|
173
215
|
exports.default = TextHighlighter;
|
|
216
|
+
function isInMyScope(rootElem, target, rootSelector) {
|
|
217
|
+
// Type guard: handle non-Element nodes
|
|
218
|
+
const targetElement = target instanceof Element ? target : target.parentElement;
|
|
219
|
+
if (!targetElement) {
|
|
220
|
+
return false;
|
|
221
|
+
}
|
|
222
|
+
// First check if target is within my root
|
|
223
|
+
if (!rootElem.contains(targetElement)) {
|
|
224
|
+
return false;
|
|
225
|
+
}
|
|
226
|
+
// Select only direct nested roots using :scope
|
|
227
|
+
const directNestedRootsSelector = `:scope ${rootSelector}:not(:scope ${rootSelector} ${rootSelector})`;
|
|
228
|
+
const directNestedRoots = rootElem.querySelectorAll(directNestedRootsSelector);
|
|
229
|
+
// Check if target is NOT within any direct nested root
|
|
230
|
+
return !Array.from(directNestedRoots).some(nestedRoot => nestedRoot.contains(targetElement));
|
|
231
|
+
}
|
|
174
232
|
//# sourceMappingURL=index.js.map
|
package/dist/cjs/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/index.tsx"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/index.tsx"],"names":[],"mappings":"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"}
|
package/dist/esm/index.d.ts
CHANGED
|
@@ -6,6 +6,14 @@ type Props = {
|
|
|
6
6
|
caseSensitive?: boolean;
|
|
7
7
|
isWordBoundary?: boolean;
|
|
8
8
|
isDebug?: boolean;
|
|
9
|
+
escapeRegex?: RegExp;
|
|
9
10
|
};
|
|
10
|
-
declare const
|
|
11
|
+
export declare const MARK_SELECTOR = "mark[data-highlighter=\"true\"]";
|
|
12
|
+
export declare const MARKS_IN_SCOPE_SELECTOR = ":scope mark[data-highlighter=\"true\"]:not(:scope [data-id=\"react-highlight-me\"] mark[data-highlighter=\"true\"])";
|
|
13
|
+
interface TextHighlighterComponent extends React.ForwardRefExoticComponent<Props & React.RefAttributes<HTMLDivElement>> {
|
|
14
|
+
MARKS_IN_SCOPE_SELECTOR: string;
|
|
15
|
+
MARK_SELECTOR: string;
|
|
16
|
+
ROOT_ELEMENT_SELECTOR: string;
|
|
17
|
+
}
|
|
18
|
+
declare const TextHighlighter: TextHighlighterComponent;
|
|
11
19
|
export default TextHighlighter;
|
package/dist/esm/index.js
CHANGED
|
@@ -1,31 +1,46 @@
|
|
|
1
|
-
import React, { useLayoutEffect, useRef, useCallback, useState } from 'react';
|
|
2
|
-
|
|
3
|
-
const
|
|
1
|
+
import React, { useLayoutEffect, useRef, useCallback, useState, forwardRef } from 'react';
|
|
2
|
+
const ROOT_ELEMENT_ID = 'react-highlight-me';
|
|
3
|
+
const ROOT_ELEMENT_ATTR = 'data-id';
|
|
4
|
+
const ROOT_ELEMENT_SELECTOR = `[${ROOT_ELEMENT_ATTR}="${ROOT_ELEMENT_ID}"]`;
|
|
5
|
+
const MARK_ATTRIBUTE = 'data-highlighter';
|
|
6
|
+
export const MARK_SELECTOR = `mark[${MARK_ATTRIBUTE}="true"]`;
|
|
7
|
+
export const MARKS_IN_SCOPE_SELECTOR = `:scope ${MARK_SELECTOR}:not(:scope ${ROOT_ELEMENT_SELECTOR} ${MARK_SELECTOR})`;
|
|
8
|
+
const TextHighlighter = forwardRef(({ children, words = [], highlightStyle = { backgroundColor: 'yellow', fontWeight: 'bold' }, caseSensitive = false, isWordBoundary = false, isDebug = false, escapeRegex = /[.*+?^${}()|[\]\\]/g, }, ref) => {
|
|
4
9
|
const containerRef = useRef(null);
|
|
5
10
|
const observerRef = useRef(null);
|
|
6
11
|
const lastHighlightSignature = useRef('');
|
|
7
12
|
const [isInitiallyReady, setIsInitiallyReady] = useState(false);
|
|
8
|
-
const
|
|
9
|
-
propsRef
|
|
13
|
+
const currentProps = { words, highlightStyle, caseSensitive, isWordBoundary, escapeRegex };
|
|
14
|
+
const propsRef = useRef(currentProps);
|
|
15
|
+
propsRef.current = currentProps;
|
|
16
|
+
const nodeFilter = {
|
|
17
|
+
acceptNode: (node) => {
|
|
18
|
+
if (!containerRef.current)
|
|
19
|
+
return NodeFilter.FILTER_SKIP;
|
|
20
|
+
return isInMyScope(containerRef.current, node, ROOT_ELEMENT_SELECTOR)
|
|
21
|
+
? NodeFilter.FILTER_ACCEPT
|
|
22
|
+
: NodeFilter.FILTER_SKIP;
|
|
23
|
+
}
|
|
24
|
+
};
|
|
10
25
|
const getTextSignature = useCallback((element) => {
|
|
11
26
|
var _a;
|
|
12
27
|
// Create a signature of all text content to detect real changes
|
|
13
|
-
const walker = document.createTreeWalker(element, NodeFilter.SHOW_TEXT,
|
|
28
|
+
const walker = document.createTreeWalker(element, NodeFilter.SHOW_TEXT, nodeFilter);
|
|
14
29
|
const textParts = [];
|
|
15
30
|
let node;
|
|
16
31
|
while ((node = walker.nextNode())) {
|
|
17
|
-
if (!((_a = node.parentElement) === null || _a === void 0 ? void 0 : _a.hasAttribute(
|
|
32
|
+
if (!((_a = node.parentElement) === null || _a === void 0 ? void 0 : _a.hasAttribute(MARK_ATTRIBUTE))) {
|
|
18
33
|
textParts.push(node.textContent || '');
|
|
19
34
|
}
|
|
20
35
|
}
|
|
21
36
|
return textParts.join('|');
|
|
22
37
|
}, []);
|
|
23
38
|
const highlightTextInElement = useCallback((element) => {
|
|
24
|
-
const { words, highlightStyle, caseSensitive, isWordBoundary } = propsRef.current;
|
|
39
|
+
const { words, highlightStyle, caseSensitive, isWordBoundary, escapeRegex } = propsRef.current;
|
|
25
40
|
const wordsArray = Array.isArray(words) ? words : [words];
|
|
26
41
|
isDebug && console.log('Highlighting with words:', wordsArray);
|
|
27
42
|
// Remove existing highlights
|
|
28
|
-
const existingMarks = element.querySelectorAll(
|
|
43
|
+
const existingMarks = element.querySelectorAll(TextHighlighter.MARKS_IN_SCOPE_SELECTOR);
|
|
29
44
|
existingMarks.forEach(mark => {
|
|
30
45
|
var _a;
|
|
31
46
|
const textContent = mark.textContent || '';
|
|
@@ -37,8 +52,9 @@ const TextHighlighter = ({ children, words = [], highlightStyle = { backgroundCo
|
|
|
37
52
|
lastHighlightSignature.current = getTextSignature(element);
|
|
38
53
|
return;
|
|
39
54
|
}
|
|
55
|
+
isDebug && console.log('Text signature:', getTextSignature(element));
|
|
40
56
|
// Apply highlighting (same logic as before)
|
|
41
|
-
const walker = document.createTreeWalker(element, NodeFilter.SHOW_TEXT,
|
|
57
|
+
const walker = document.createTreeWalker(element, NodeFilter.SHOW_TEXT, nodeFilter);
|
|
42
58
|
const textNodes = [];
|
|
43
59
|
let node;
|
|
44
60
|
while ((node = walker.nextNode())) {
|
|
@@ -47,7 +63,7 @@ const TextHighlighter = ({ children, words = [], highlightStyle = { backgroundCo
|
|
|
47
63
|
textNodes.forEach(textNode => {
|
|
48
64
|
var _a;
|
|
49
65
|
const text = textNode.textContent || '';
|
|
50
|
-
if (!text
|
|
66
|
+
if (!text)
|
|
51
67
|
return;
|
|
52
68
|
const pattern = wordsArray
|
|
53
69
|
.filter(word => word)
|
|
@@ -55,7 +71,7 @@ const TextHighlighter = ({ children, words = [], highlightStyle = { backgroundCo
|
|
|
55
71
|
if (word instanceof RegExp) {
|
|
56
72
|
return word.source;
|
|
57
73
|
}
|
|
58
|
-
let term = word.replace(
|
|
74
|
+
let term = escapeRegex ? word.replace(escapeRegex, '\\$&') : word;
|
|
59
75
|
if (isWordBoundary) {
|
|
60
76
|
term = `\\b${term}\\b`;
|
|
61
77
|
}
|
|
@@ -78,12 +94,12 @@ const TextHighlighter = ({ children, words = [], highlightStyle = { backgroundCo
|
|
|
78
94
|
return testRegex.test(part);
|
|
79
95
|
}
|
|
80
96
|
return caseSensitive
|
|
81
|
-
? part === word
|
|
82
|
-
: part.toLowerCase() === word.
|
|
97
|
+
? part === word
|
|
98
|
+
: part.toLowerCase() === word.toLowerCase();
|
|
83
99
|
});
|
|
84
100
|
if (shouldHighlight) {
|
|
85
101
|
const mark = document.createElement('mark');
|
|
86
|
-
mark.setAttribute(
|
|
102
|
+
mark.setAttribute(MARK_ATTRIBUTE, 'true');
|
|
87
103
|
Object.assign(mark.style, highlightStyle);
|
|
88
104
|
mark.textContent = part;
|
|
89
105
|
fragment.appendChild(mark);
|
|
@@ -99,27 +115,41 @@ const TextHighlighter = ({ children, words = [], highlightStyle = { backgroundCo
|
|
|
99
115
|
lastHighlightSignature.current = getTextSignature(element);
|
|
100
116
|
}, [getTextSignature]);
|
|
101
117
|
useLayoutEffect(() => {
|
|
102
|
-
|
|
118
|
+
const currentRef = ref && 'current' in ref ? ref.current : containerRef.current;
|
|
119
|
+
if (!currentRef)
|
|
103
120
|
return;
|
|
104
121
|
isDebug && console.log('Setting up persistent MutationObserver');
|
|
105
|
-
highlightTextInElement(
|
|
122
|
+
highlightTextInElement(currentRef);
|
|
106
123
|
setIsInitiallyReady(true);
|
|
107
124
|
observerRef.current = new MutationObserver((mutations) => {
|
|
108
|
-
if (!
|
|
125
|
+
if (!currentRef)
|
|
126
|
+
return;
|
|
127
|
+
const myMutations = mutations.filter((mutation) => {
|
|
128
|
+
if (isInMyScope(currentRef, mutation.target, ROOT_ELEMENT_SELECTOR)) {
|
|
129
|
+
// This mutation is in my scope - process it
|
|
130
|
+
isDebug && console.log('Processing mutation in my scope:', currentRef.getAttribute(ROOT_ELEMENT_ATTR));
|
|
131
|
+
return true;
|
|
132
|
+
}
|
|
133
|
+
// Otherwise ignore - it's either from outer scope or nested scope
|
|
134
|
+
return false;
|
|
135
|
+
});
|
|
136
|
+
if (myMutations.length === 0) {
|
|
137
|
+
isDebug && console.log('No relevant mutations in my scope, skipping');
|
|
109
138
|
return;
|
|
139
|
+
}
|
|
110
140
|
// Check if the text signature has actually changed
|
|
111
|
-
const currentSignature = getTextSignature(
|
|
141
|
+
const currentSignature = getTextSignature(currentRef);
|
|
112
142
|
if (currentSignature !== lastHighlightSignature.current) {
|
|
113
143
|
isDebug && console.log('Text signature changed, re-highlighting');
|
|
114
144
|
isDebug && console.log('Old:', lastHighlightSignature.current);
|
|
115
145
|
isDebug && console.log('New:', currentSignature);
|
|
116
|
-
highlightTextInElement(
|
|
146
|
+
highlightTextInElement(currentRef);
|
|
117
147
|
}
|
|
118
148
|
else {
|
|
119
149
|
isDebug && console.log('Text signature unchanged, ignoring mutation');
|
|
120
150
|
}
|
|
121
151
|
});
|
|
122
|
-
observerRef.current.observe(
|
|
152
|
+
observerRef.current.observe(currentRef, {
|
|
123
153
|
childList: true,
|
|
124
154
|
subtree: true,
|
|
125
155
|
characterData: true
|
|
@@ -129,11 +159,38 @@ const TextHighlighter = ({ children, words = [], highlightStyle = { backgroundCo
|
|
|
129
159
|
observerRef.current.disconnect();
|
|
130
160
|
}
|
|
131
161
|
};
|
|
132
|
-
}, [words, highlightStyle, caseSensitive, isWordBoundary, highlightTextInElement, getTextSignature]);
|
|
133
|
-
return (React.createElement("div", {
|
|
162
|
+
}, [words, highlightStyle, caseSensitive, isWordBoundary, highlightTextInElement, getTextSignature, ref, escapeRegex]);
|
|
163
|
+
return (React.createElement("div", { [ROOT_ELEMENT_ATTR]: ROOT_ELEMENT_ID, ref: (node) => {
|
|
164
|
+
containerRef.current = node;
|
|
165
|
+
if (typeof ref === 'function') {
|
|
166
|
+
ref(node);
|
|
167
|
+
}
|
|
168
|
+
else if (ref) {
|
|
169
|
+
ref.current = node;
|
|
170
|
+
}
|
|
171
|
+
}, style: {
|
|
134
172
|
visibility: isInitiallyReady ? 'visible' : 'hidden',
|
|
135
173
|
minHeight: isInitiallyReady ? 'auto' : '1em'
|
|
136
174
|
} }, children));
|
|
137
|
-
};
|
|
175
|
+
});
|
|
176
|
+
TextHighlighter.MARK_SELECTOR = MARK_SELECTOR;
|
|
177
|
+
TextHighlighter.ROOT_ELEMENT_SELECTOR = ROOT_ELEMENT_SELECTOR;
|
|
178
|
+
TextHighlighter.MARKS_IN_SCOPE_SELECTOR = MARKS_IN_SCOPE_SELECTOR;
|
|
138
179
|
export default TextHighlighter;
|
|
180
|
+
function isInMyScope(rootElem, target, rootSelector) {
|
|
181
|
+
// Type guard: handle non-Element nodes
|
|
182
|
+
const targetElement = target instanceof Element ? target : target.parentElement;
|
|
183
|
+
if (!targetElement) {
|
|
184
|
+
return false;
|
|
185
|
+
}
|
|
186
|
+
// First check if target is within my root
|
|
187
|
+
if (!rootElem.contains(targetElement)) {
|
|
188
|
+
return false;
|
|
189
|
+
}
|
|
190
|
+
// Select only direct nested roots using :scope
|
|
191
|
+
const directNestedRootsSelector = `:scope ${rootSelector}:not(:scope ${rootSelector} ${rootSelector})`;
|
|
192
|
+
const directNestedRoots = rootElem.querySelectorAll(directNestedRootsSelector);
|
|
193
|
+
// Check if target is NOT within any direct nested root
|
|
194
|
+
return !Array.from(directNestedRoots).some(nestedRoot => nestedRoot.contains(targetElement));
|
|
195
|
+
}
|
|
139
196
|
//# sourceMappingURL=index.js.map
|
package/dist/esm/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/index.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,EAAE,eAAe,EAAE,MAAM,EAAE,WAAW,EAAE,QAAQ,EAAE,MAAM,OAAO,CAAC;
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/index.tsx"],"names":[],"mappings":"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"}
|
package/package.json
CHANGED
package/src/index.tsx
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import React, { useLayoutEffect, useRef, useCallback, useState } from 'react';
|
|
1
|
+
import React, { useLayoutEffect, useRef, useCallback, useState, forwardRef } from 'react';
|
|
2
2
|
|
|
3
3
|
type Props = {
|
|
4
4
|
children?: React.ReactNode;
|
|
@@ -7,36 +7,62 @@ type Props = {
|
|
|
7
7
|
caseSensitive?: boolean;
|
|
8
8
|
isWordBoundary?: boolean;
|
|
9
9
|
isDebug?: boolean;
|
|
10
|
+
escapeRegex?: RegExp;
|
|
10
11
|
}
|
|
11
|
-
|
|
12
|
-
const
|
|
12
|
+
|
|
13
|
+
const ROOT_ELEMENT_ID = 'react-highlight-me';
|
|
14
|
+
const ROOT_ELEMENT_ATTR = 'data-id';
|
|
15
|
+
const ROOT_ELEMENT_SELECTOR = `[${ROOT_ELEMENT_ATTR}="${ROOT_ELEMENT_ID}"]`;
|
|
16
|
+
const MARK_ATTRIBUTE = 'data-highlighter';
|
|
17
|
+
export const MARK_SELECTOR = `mark[${MARK_ATTRIBUTE}="true"]`;
|
|
18
|
+
export const MARKS_IN_SCOPE_SELECTOR = `:scope ${MARK_SELECTOR}:not(:scope ${ROOT_ELEMENT_SELECTOR} ${MARK_SELECTOR})`;
|
|
19
|
+
|
|
20
|
+
interface TextHighlighterComponent extends React.ForwardRefExoticComponent<Props & React.RefAttributes<HTMLDivElement>> {
|
|
21
|
+
MARKS_IN_SCOPE_SELECTOR: string;
|
|
22
|
+
MARK_SELECTOR: string;
|
|
23
|
+
ROOT_ELEMENT_SELECTOR: string;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
const TextHighlighter = forwardRef<HTMLDivElement, Props>(({
|
|
13
27
|
children,
|
|
14
28
|
words = [],
|
|
15
29
|
highlightStyle = { backgroundColor: 'yellow', fontWeight: 'bold' },
|
|
16
30
|
caseSensitive = false,
|
|
17
31
|
isWordBoundary = false,
|
|
18
32
|
isDebug = false,
|
|
19
|
-
|
|
20
|
-
|
|
33
|
+
escapeRegex = /[.*+?^${}()|[\]\\]/g,
|
|
34
|
+
}, ref) => {
|
|
35
|
+
const containerRef = useRef<HTMLElement | null>(null);
|
|
21
36
|
const observerRef = useRef<MutationObserver | null>(null);
|
|
22
37
|
const lastHighlightSignature = useRef<string>('');
|
|
23
38
|
const [isInitiallyReady, setIsInitiallyReady] = useState(false);
|
|
24
39
|
|
|
25
|
-
const
|
|
26
|
-
propsRef
|
|
40
|
+
const currentProps = { words, highlightStyle, caseSensitive, isWordBoundary, escapeRegex };
|
|
41
|
+
const propsRef = useRef(currentProps);
|
|
42
|
+
propsRef.current = currentProps;
|
|
43
|
+
|
|
44
|
+
const nodeFilter = {
|
|
45
|
+
acceptNode: (node: Node) => {
|
|
46
|
+
if (!containerRef.current) return NodeFilter.FILTER_SKIP;
|
|
47
|
+
|
|
48
|
+
return isInMyScope(containerRef.current, node, ROOT_ELEMENT_SELECTOR)
|
|
49
|
+
? NodeFilter.FILTER_ACCEPT
|
|
50
|
+
: NodeFilter.FILTER_SKIP;
|
|
51
|
+
}
|
|
52
|
+
}
|
|
27
53
|
|
|
28
54
|
const getTextSignature = useCallback((element: HTMLElement): string => {
|
|
29
55
|
// Create a signature of all text content to detect real changes
|
|
30
56
|
const walker = document.createTreeWalker(
|
|
31
57
|
element,
|
|
32
58
|
NodeFilter.SHOW_TEXT,
|
|
33
|
-
|
|
59
|
+
nodeFilter,
|
|
34
60
|
);
|
|
35
61
|
|
|
36
62
|
const textParts: string[] = [];
|
|
37
63
|
let node;
|
|
38
64
|
while ((node = walker.nextNode())) {
|
|
39
|
-
if (!(node as Text).parentElement?.hasAttribute(
|
|
65
|
+
if (!(node as Text).parentElement?.hasAttribute(MARK_ATTRIBUTE)) {
|
|
40
66
|
textParts.push((node as Text).textContent || '');
|
|
41
67
|
}
|
|
42
68
|
}
|
|
@@ -45,13 +71,13 @@ const TextHighlighter = ({
|
|
|
45
71
|
}, []);
|
|
46
72
|
|
|
47
73
|
const highlightTextInElement = useCallback((element: HTMLElement) => {
|
|
48
|
-
const { words, highlightStyle, caseSensitive, isWordBoundary } = propsRef.current;
|
|
74
|
+
const { words, highlightStyle, caseSensitive, isWordBoundary, escapeRegex } = propsRef.current;
|
|
49
75
|
const wordsArray = Array.isArray(words) ? words : [words];
|
|
50
76
|
|
|
51
77
|
isDebug && console.log('Highlighting with words:', wordsArray);
|
|
52
78
|
|
|
53
79
|
// Remove existing highlights
|
|
54
|
-
const existingMarks = element.querySelectorAll(
|
|
80
|
+
const existingMarks = element.querySelectorAll(TextHighlighter.MARKS_IN_SCOPE_SELECTOR);
|
|
55
81
|
existingMarks.forEach(mark => {
|
|
56
82
|
const textContent = mark.textContent || '';
|
|
57
83
|
const textNode = document.createTextNode(textContent);
|
|
@@ -65,11 +91,13 @@ const TextHighlighter = ({
|
|
|
65
91
|
return;
|
|
66
92
|
}
|
|
67
93
|
|
|
94
|
+
isDebug && console.log('Text signature:', getTextSignature(element));
|
|
95
|
+
|
|
68
96
|
// Apply highlighting (same logic as before)
|
|
69
97
|
const walker = document.createTreeWalker(
|
|
70
98
|
element,
|
|
71
99
|
NodeFilter.SHOW_TEXT,
|
|
72
|
-
|
|
100
|
+
nodeFilter,
|
|
73
101
|
);
|
|
74
102
|
|
|
75
103
|
const textNodes: Text[] = [];
|
|
@@ -80,21 +108,21 @@ const TextHighlighter = ({
|
|
|
80
108
|
|
|
81
109
|
textNodes.forEach(textNode => {
|
|
82
110
|
const text = textNode.textContent || '';
|
|
83
|
-
if (!text
|
|
111
|
+
if (!text) return;
|
|
84
112
|
|
|
85
113
|
const pattern = wordsArray
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
114
|
+
.filter(word => word)
|
|
115
|
+
.map(word => {
|
|
116
|
+
if (word instanceof RegExp) {
|
|
117
|
+
return word.source;
|
|
118
|
+
}
|
|
119
|
+
let term = escapeRegex ? word.replace(escapeRegex, '\\$&') : word;
|
|
120
|
+
if (isWordBoundary) {
|
|
121
|
+
term = `\\b${term}\\b`;
|
|
122
|
+
}
|
|
123
|
+
return term;
|
|
124
|
+
})
|
|
125
|
+
.join('|');
|
|
98
126
|
|
|
99
127
|
if (!pattern) return;
|
|
100
128
|
|
|
@@ -114,13 +142,13 @@ const TextHighlighter = ({
|
|
|
114
142
|
return testRegex.test(part);
|
|
115
143
|
}
|
|
116
144
|
return caseSensitive
|
|
117
|
-
? part === word
|
|
118
|
-
: part.toLowerCase() === word.
|
|
145
|
+
? part === word
|
|
146
|
+
: part.toLowerCase() === word.toLowerCase();
|
|
119
147
|
});
|
|
120
148
|
|
|
121
149
|
if (shouldHighlight) {
|
|
122
150
|
const mark = document.createElement('mark');
|
|
123
|
-
mark.setAttribute(
|
|
151
|
+
mark.setAttribute(MARK_ATTRIBUTE, 'true');
|
|
124
152
|
Object.assign(mark.style, highlightStyle);
|
|
125
153
|
mark.textContent = part;
|
|
126
154
|
fragment.appendChild(mark);
|
|
@@ -138,31 +166,47 @@ const TextHighlighter = ({
|
|
|
138
166
|
}, [getTextSignature]);
|
|
139
167
|
|
|
140
168
|
useLayoutEffect(() => {
|
|
141
|
-
|
|
169
|
+
const currentRef = ref && 'current' in ref ? ref.current : containerRef.current;
|
|
170
|
+
if (!currentRef) return;
|
|
142
171
|
|
|
143
172
|
isDebug && console.log('Setting up persistent MutationObserver');
|
|
144
173
|
|
|
145
|
-
highlightTextInElement(
|
|
174
|
+
highlightTextInElement(currentRef);
|
|
146
175
|
setIsInitiallyReady(true);
|
|
147
176
|
|
|
148
177
|
observerRef.current = new MutationObserver((mutations) => {
|
|
149
|
-
if (!
|
|
178
|
+
if (!currentRef) return;
|
|
179
|
+
|
|
180
|
+
const myMutations = mutations.filter((mutation) => {
|
|
181
|
+
if (isInMyScope(currentRef, mutation.target, ROOT_ELEMENT_SELECTOR)) {
|
|
182
|
+
// This mutation is in my scope - process it
|
|
183
|
+
isDebug && console.log('Processing mutation in my scope:', currentRef.getAttribute(ROOT_ELEMENT_ATTR));
|
|
184
|
+
return true;
|
|
185
|
+
}
|
|
186
|
+
// Otherwise ignore - it's either from outer scope or nested scope
|
|
187
|
+
return false;
|
|
188
|
+
});
|
|
189
|
+
|
|
190
|
+
if (myMutations.length === 0) {
|
|
191
|
+
isDebug && console.log('No relevant mutations in my scope, skipping');
|
|
192
|
+
return;
|
|
193
|
+
}
|
|
150
194
|
|
|
151
195
|
// Check if the text signature has actually changed
|
|
152
|
-
const currentSignature = getTextSignature(
|
|
196
|
+
const currentSignature = getTextSignature(currentRef);
|
|
153
197
|
|
|
154
198
|
if (currentSignature !== lastHighlightSignature.current) {
|
|
155
199
|
isDebug && console.log('Text signature changed, re-highlighting');
|
|
156
200
|
isDebug && console.log('Old:', lastHighlightSignature.current);
|
|
157
201
|
isDebug && console.log('New:', currentSignature);
|
|
158
202
|
|
|
159
|
-
highlightTextInElement(
|
|
203
|
+
highlightTextInElement(currentRef);
|
|
160
204
|
} else {
|
|
161
205
|
isDebug && console.log('Text signature unchanged, ignoring mutation');
|
|
162
206
|
}
|
|
163
207
|
});
|
|
164
208
|
|
|
165
|
-
observerRef.current.observe(
|
|
209
|
+
observerRef.current.observe(currentRef, {
|
|
166
210
|
childList: true,
|
|
167
211
|
subtree: true,
|
|
168
212
|
characterData: true
|
|
@@ -173,11 +217,19 @@ const TextHighlighter = ({
|
|
|
173
217
|
observerRef.current.disconnect();
|
|
174
218
|
}
|
|
175
219
|
};
|
|
176
|
-
}, [words, highlightStyle, caseSensitive, isWordBoundary, highlightTextInElement, getTextSignature]);
|
|
220
|
+
}, [words, highlightStyle, caseSensitive, isWordBoundary, highlightTextInElement, getTextSignature, ref, escapeRegex]);
|
|
177
221
|
|
|
178
222
|
return (
|
|
179
223
|
<div
|
|
180
|
-
|
|
224
|
+
{...{ [ROOT_ELEMENT_ATTR]: ROOT_ELEMENT_ID }}
|
|
225
|
+
ref={(node) => {
|
|
226
|
+
containerRef.current = node;
|
|
227
|
+
if (typeof ref === 'function') {
|
|
228
|
+
ref(node);
|
|
229
|
+
} else if (ref) {
|
|
230
|
+
ref.current = node;
|
|
231
|
+
}
|
|
232
|
+
}}
|
|
181
233
|
style={{
|
|
182
234
|
visibility: isInitiallyReady ? 'visible' : 'hidden',
|
|
183
235
|
minHeight: isInitiallyReady ? 'auto' : '1em'
|
|
@@ -186,6 +238,32 @@ const TextHighlighter = ({
|
|
|
186
238
|
{children}
|
|
187
239
|
</div>
|
|
188
240
|
);
|
|
189
|
-
};
|
|
241
|
+
}) as TextHighlighterComponent;
|
|
242
|
+
|
|
243
|
+
TextHighlighter.MARK_SELECTOR = MARK_SELECTOR;
|
|
244
|
+
TextHighlighter.ROOT_ELEMENT_SELECTOR = ROOT_ELEMENT_SELECTOR;
|
|
245
|
+
TextHighlighter.MARKS_IN_SCOPE_SELECTOR = MARKS_IN_SCOPE_SELECTOR;
|
|
190
246
|
|
|
191
247
|
export default TextHighlighter;
|
|
248
|
+
|
|
249
|
+
function isInMyScope(rootElem: Element, target: Node, rootSelector: string): boolean {
|
|
250
|
+
// Type guard: handle non-Element nodes
|
|
251
|
+
const targetElement = target instanceof Element ? target : target.parentElement;
|
|
252
|
+
if (!targetElement) {
|
|
253
|
+
return false;
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
// First check if target is within my root
|
|
257
|
+
if (!rootElem.contains(targetElement)) {
|
|
258
|
+
return false;
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
// Select only direct nested roots using :scope
|
|
262
|
+
const directNestedRootsSelector = `:scope ${rootSelector}:not(:scope ${rootSelector} ${rootSelector})`;
|
|
263
|
+
const directNestedRoots = rootElem.querySelectorAll(directNestedRootsSelector);
|
|
264
|
+
|
|
265
|
+
// Check if target is NOT within any direct nested root
|
|
266
|
+
return !Array.from(directNestedRoots).some(nestedRoot =>
|
|
267
|
+
nestedRoot.contains(targetElement)
|
|
268
|
+
);
|
|
269
|
+
}
|