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