react-highlight-me 1.2.1 → 2.0.1
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 +2 -2
- package/dist/cjs/index.js +161 -65
- package/dist/cjs/index.js.map +1 -0
- package/dist/esm/index.d.ts +2 -2
- package/dist/esm/index.js +128 -62
- package/dist/esm/index.js.map +1 -0
- package/package.json +27 -15
- package/src/index.tsx +190 -0
package/dist/cjs/index.d.ts
CHANGED
|
@@ -4,8 +4,8 @@ type Props = {
|
|
|
4
4
|
words?: string[] | string | RegExp | RegExp[];
|
|
5
5
|
highlightStyle?: React.CSSProperties;
|
|
6
6
|
caseSensitive?: boolean;
|
|
7
|
-
isEscapePattern?: boolean;
|
|
8
7
|
isWordBoundary?: boolean;
|
|
8
|
+
isDebug?: boolean;
|
|
9
9
|
};
|
|
10
|
-
declare const TextHighlighter: ({ children, words, highlightStyle, caseSensitive,
|
|
10
|
+
declare const TextHighlighter: ({ children, words, highlightStyle, caseSensitive, isWordBoundary, isDebug, }: Props) => React.JSX.Element;
|
|
11
11
|
export default TextHighlighter;
|
package/dist/cjs/index.js
CHANGED
|
@@ -1,77 +1,173 @@
|
|
|
1
1
|
"use strict";
|
|
2
|
-
var
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
var TextHighlighter = function (_a) {
|
|
8
|
-
var _b = _a.children, children = _b === void 0 ? '' : _b, _c = _a.words, words = _c === void 0 ? [] : _c, _d = _a.highlightStyle, highlightStyle = _d === void 0 ? { backgroundColor: 'yellow', fontWeight: 'bold' } : _d, _e = _a.caseSensitive, caseSensitive = _e === void 0 ? false : _e, _f = _a.isEscapePattern, isEscapePattern = _f === void 0 ? true : _f, _g = _a.isWordBoundary, isWordBoundary = _g === void 0 ? true : _g;
|
|
9
|
-
// Convert words to array if it's a string
|
|
10
|
-
var wordsArray = Array.isArray(words) ? words : [words];
|
|
11
|
-
// If no words to highlight, return original content
|
|
12
|
-
if (!wordsArray.length || wordsArray.every(function (word) { return !word; })) {
|
|
13
|
-
return children;
|
|
2
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
+
if (k2 === undefined) k2 = k;
|
|
4
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
14
7
|
}
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
8
|
+
Object.defineProperty(o, k2, desc);
|
|
9
|
+
}) : (function(o, m, k, k2) {
|
|
10
|
+
if (k2 === undefined) k2 = k;
|
|
11
|
+
o[k2] = m[k];
|
|
12
|
+
}));
|
|
13
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
14
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
15
|
+
}) : function(o, v) {
|
|
16
|
+
o["default"] = v;
|
|
17
|
+
});
|
|
18
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
19
|
+
var ownKeys = function(o) {
|
|
20
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
21
|
+
var ar = [];
|
|
22
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
23
|
+
return ar;
|
|
24
|
+
};
|
|
25
|
+
return ownKeys(o);
|
|
26
|
+
};
|
|
27
|
+
return function (mod) {
|
|
28
|
+
if (mod && mod.__esModule) return mod;
|
|
29
|
+
var result = {};
|
|
30
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
31
|
+
__setModuleDefault(result, mod);
|
|
32
|
+
return result;
|
|
33
|
+
};
|
|
34
|
+
})();
|
|
35
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
36
|
+
const react_1 = __importStar(require("react"));
|
|
37
|
+
// Alternative approach: Use a single observer that never disconnects
|
|
38
|
+
const TextHighlighter = ({ children, words = [], highlightStyle = { backgroundColor: 'yellow', fontWeight: 'bold' }, caseSensitive = false, isWordBoundary = true, isDebug = false, }) => {
|
|
39
|
+
const containerRef = (0, react_1.useRef)(null);
|
|
40
|
+
const observerRef = (0, react_1.useRef)(null);
|
|
41
|
+
const lastHighlightSignature = (0, react_1.useRef)('');
|
|
42
|
+
const [isInitiallyReady, setIsInitiallyReady] = (0, react_1.useState)(false);
|
|
43
|
+
const propsRef = (0, react_1.useRef)({ words, highlightStyle, caseSensitive, isWordBoundary });
|
|
44
|
+
propsRef.current = { words, highlightStyle, caseSensitive, isWordBoundary };
|
|
45
|
+
const getTextSignature = (0, react_1.useCallback)((element) => {
|
|
46
|
+
var _a;
|
|
47
|
+
// Create a signature of all text content to detect real changes
|
|
48
|
+
const walker = document.createTreeWalker(element, NodeFilter.SHOW_TEXT, null);
|
|
49
|
+
const textParts = [];
|
|
50
|
+
let node;
|
|
51
|
+
while ((node = walker.nextNode())) {
|
|
52
|
+
if (!((_a = node.parentElement) === null || _a === void 0 ? void 0 : _a.hasAttribute('data-highlighter'))) {
|
|
53
|
+
textParts.push(node.textContent || '');
|
|
54
|
+
}
|
|
22
55
|
}
|
|
23
|
-
|
|
24
|
-
|
|
56
|
+
return textParts.join('|');
|
|
57
|
+
}, []);
|
|
58
|
+
const highlightTextInElement = (0, react_1.useCallback)((element) => {
|
|
59
|
+
const { words, highlightStyle, caseSensitive, isWordBoundary } = propsRef.current;
|
|
60
|
+
const wordsArray = Array.isArray(words) ? words : [words];
|
|
61
|
+
isDebug && console.log('Highlighting with words:', wordsArray);
|
|
62
|
+
// Remove existing highlights
|
|
63
|
+
const existingMarks = element.querySelectorAll('mark[data-highlighter="true"]');
|
|
64
|
+
existingMarks.forEach(mark => {
|
|
65
|
+
var _a;
|
|
66
|
+
const textContent = mark.textContent || '';
|
|
67
|
+
const textNode = document.createTextNode(textContent);
|
|
68
|
+
(_a = mark.parentNode) === null || _a === void 0 ? void 0 : _a.replaceChild(textNode, mark);
|
|
69
|
+
});
|
|
70
|
+
element.normalize(); // Ensure text nodes are merged
|
|
71
|
+
if (!wordsArray.length || wordsArray.every(word => !word)) {
|
|
72
|
+
lastHighlightSignature.current = getTextSignature(element);
|
|
73
|
+
return;
|
|
25
74
|
}
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
.join('|');
|
|
33
|
-
if (!pattern) {
|
|
34
|
-
return children;
|
|
35
|
-
}
|
|
36
|
-
var regex = new RegExp("(".concat(pattern, ")"), caseSensitive ? 'g' : 'gi');
|
|
37
|
-
// Function to highlight text content
|
|
38
|
-
var highlightText = function (textContent) {
|
|
39
|
-
if (!textContent || typeof textContent !== 'string') {
|
|
40
|
-
return textContent;
|
|
75
|
+
// Apply highlighting (same logic as before)
|
|
76
|
+
const walker = document.createTreeWalker(element, NodeFilter.SHOW_TEXT, null);
|
|
77
|
+
const textNodes = [];
|
|
78
|
+
let node;
|
|
79
|
+
while ((node = walker.nextNode())) {
|
|
80
|
+
textNodes.push(node);
|
|
41
81
|
}
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
82
|
+
textNodes.forEach(textNode => {
|
|
83
|
+
var _a;
|
|
84
|
+
const text = textNode.textContent || '';
|
|
85
|
+
if (!text.trim())
|
|
86
|
+
return;
|
|
87
|
+
const pattern = wordsArray
|
|
88
|
+
.filter(word => word)
|
|
89
|
+
.map(word => {
|
|
45
90
|
if (word instanceof RegExp) {
|
|
46
|
-
return word.
|
|
91
|
+
return word.source;
|
|
47
92
|
}
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
93
|
+
let term = word.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
94
|
+
if (isWordBoundary) {
|
|
95
|
+
term = `\\b${term}\\b`;
|
|
96
|
+
}
|
|
97
|
+
return term;
|
|
98
|
+
})
|
|
99
|
+
.join('|');
|
|
100
|
+
if (!pattern)
|
|
101
|
+
return;
|
|
102
|
+
const regex = new RegExp(`(${pattern})`, caseSensitive ? 'g' : 'gi');
|
|
103
|
+
const parts = text.split(regex);
|
|
104
|
+
if (parts.length > 1) {
|
|
105
|
+
const fragment = document.createDocumentFragment();
|
|
106
|
+
parts.forEach(part => {
|
|
107
|
+
if (!part)
|
|
108
|
+
return;
|
|
109
|
+
const shouldHighlight = wordsArray.some(word => {
|
|
110
|
+
if (word instanceof RegExp) {
|
|
111
|
+
const testRegex = new RegExp(word.source, caseSensitive ? word.flags : word.flags + 'i');
|
|
112
|
+
return testRegex.test(part);
|
|
113
|
+
}
|
|
114
|
+
return caseSensitive
|
|
115
|
+
? part === word.trim()
|
|
116
|
+
: part.toLowerCase() === word.trim().toLowerCase();
|
|
117
|
+
});
|
|
118
|
+
if (shouldHighlight) {
|
|
119
|
+
const mark = document.createElement('mark');
|
|
120
|
+
mark.setAttribute('data-highlighter', 'true');
|
|
121
|
+
Object.assign(mark.style, highlightStyle);
|
|
122
|
+
mark.textContent = part;
|
|
123
|
+
fragment.appendChild(mark);
|
|
124
|
+
}
|
|
125
|
+
else {
|
|
126
|
+
fragment.appendChild(document.createTextNode(part));
|
|
127
|
+
}
|
|
128
|
+
});
|
|
129
|
+
(_a = textNode.parentNode) === null || _a === void 0 ? void 0 : _a.replaceChild(fragment, textNode);
|
|
130
|
+
}
|
|
53
131
|
});
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
if (
|
|
69
|
-
|
|
132
|
+
// Update signature after highlighting
|
|
133
|
+
lastHighlightSignature.current = getTextSignature(element);
|
|
134
|
+
}, [getTextSignature]);
|
|
135
|
+
(0, react_1.useLayoutEffect)(() => {
|
|
136
|
+
if (!containerRef.current)
|
|
137
|
+
return;
|
|
138
|
+
isDebug && console.log('Setting up persistent MutationObserver');
|
|
139
|
+
highlightTextInElement(containerRef.current);
|
|
140
|
+
setIsInitiallyReady(true);
|
|
141
|
+
observerRef.current = new MutationObserver((mutations) => {
|
|
142
|
+
if (!containerRef.current)
|
|
143
|
+
return;
|
|
144
|
+
// Check if the text signature has actually changed
|
|
145
|
+
const currentSignature = getTextSignature(containerRef.current);
|
|
146
|
+
if (currentSignature !== lastHighlightSignature.current) {
|
|
147
|
+
isDebug && console.log('Text signature changed, re-highlighting');
|
|
148
|
+
isDebug && console.log('Old:', lastHighlightSignature.current);
|
|
149
|
+
isDebug && console.log('New:', currentSignature);
|
|
150
|
+
highlightTextInElement(containerRef.current);
|
|
151
|
+
}
|
|
152
|
+
else {
|
|
153
|
+
isDebug && console.log('Text signature unchanged, ignoring mutation');
|
|
70
154
|
}
|
|
71
|
-
return processElement(child);
|
|
72
155
|
});
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
156
|
+
observerRef.current.observe(containerRef.current, {
|
|
157
|
+
childList: true,
|
|
158
|
+
subtree: true,
|
|
159
|
+
characterData: true
|
|
160
|
+
});
|
|
161
|
+
return () => {
|
|
162
|
+
if (observerRef.current) {
|
|
163
|
+
observerRef.current.disconnect();
|
|
164
|
+
}
|
|
165
|
+
};
|
|
166
|
+
}, [words, highlightStyle, caseSensitive, isWordBoundary, highlightTextInElement, getTextSignature]);
|
|
167
|
+
return (react_1.default.createElement("div", { ref: containerRef, style: {
|
|
168
|
+
visibility: isInitiallyReady ? 'visible' : 'hidden',
|
|
169
|
+
minHeight: isInitiallyReady ? 'auto' : '1em'
|
|
170
|
+
} }, children));
|
|
76
171
|
};
|
|
77
172
|
exports.default = TextHighlighter;
|
|
173
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/index.tsx"],"names":[],"mappings":"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"}
|
package/dist/esm/index.d.ts
CHANGED
|
@@ -4,8 +4,8 @@ type Props = {
|
|
|
4
4
|
words?: string[] | string | RegExp | RegExp[];
|
|
5
5
|
highlightStyle?: React.CSSProperties;
|
|
6
6
|
caseSensitive?: boolean;
|
|
7
|
-
isEscapePattern?: boolean;
|
|
8
7
|
isWordBoundary?: boolean;
|
|
8
|
+
isDebug?: boolean;
|
|
9
9
|
};
|
|
10
|
-
declare const TextHighlighter: ({ children, words, highlightStyle, caseSensitive,
|
|
10
|
+
declare const TextHighlighter: ({ children, words, highlightStyle, caseSensitive, isWordBoundary, isDebug, }: Props) => React.JSX.Element;
|
|
11
11
|
export default TextHighlighter;
|
package/dist/esm/index.js
CHANGED
|
@@ -1,72 +1,138 @@
|
|
|
1
|
-
import React from 'react';
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
}
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
1
|
+
import React, { useLayoutEffect, useRef, useCallback, useState } from 'react';
|
|
2
|
+
// Alternative approach: Use a single observer that never disconnects
|
|
3
|
+
const TextHighlighter = ({ children, words = [], highlightStyle = { backgroundColor: 'yellow', fontWeight: 'bold' }, caseSensitive = false, isWordBoundary = true, isDebug = false, }) => {
|
|
4
|
+
const containerRef = useRef(null);
|
|
5
|
+
const observerRef = useRef(null);
|
|
6
|
+
const lastHighlightSignature = useRef('');
|
|
7
|
+
const [isInitiallyReady, setIsInitiallyReady] = useState(false);
|
|
8
|
+
const propsRef = useRef({ words, highlightStyle, caseSensitive, isWordBoundary });
|
|
9
|
+
propsRef.current = { words, highlightStyle, caseSensitive, isWordBoundary };
|
|
10
|
+
const getTextSignature = useCallback((element) => {
|
|
11
|
+
var _a;
|
|
12
|
+
// Create a signature of all text content to detect real changes
|
|
13
|
+
const walker = document.createTreeWalker(element, NodeFilter.SHOW_TEXT, null);
|
|
14
|
+
const textParts = [];
|
|
15
|
+
let node;
|
|
16
|
+
while ((node = walker.nextNode())) {
|
|
17
|
+
if (!((_a = node.parentElement) === null || _a === void 0 ? void 0 : _a.hasAttribute('data-highlighter'))) {
|
|
18
|
+
textParts.push(node.textContent || '');
|
|
19
|
+
}
|
|
17
20
|
}
|
|
18
|
-
|
|
19
|
-
|
|
21
|
+
return textParts.join('|');
|
|
22
|
+
}, []);
|
|
23
|
+
const highlightTextInElement = useCallback((element) => {
|
|
24
|
+
const { words, highlightStyle, caseSensitive, isWordBoundary } = propsRef.current;
|
|
25
|
+
const wordsArray = Array.isArray(words) ? words : [words];
|
|
26
|
+
isDebug && console.log('Highlighting with words:', wordsArray);
|
|
27
|
+
// Remove existing highlights
|
|
28
|
+
const existingMarks = element.querySelectorAll('mark[data-highlighter="true"]');
|
|
29
|
+
existingMarks.forEach(mark => {
|
|
30
|
+
var _a;
|
|
31
|
+
const textContent = mark.textContent || '';
|
|
32
|
+
const textNode = document.createTextNode(textContent);
|
|
33
|
+
(_a = mark.parentNode) === null || _a === void 0 ? void 0 : _a.replaceChild(textNode, mark);
|
|
34
|
+
});
|
|
35
|
+
element.normalize(); // Ensure text nodes are merged
|
|
36
|
+
if (!wordsArray.length || wordsArray.every(word => !word)) {
|
|
37
|
+
lastHighlightSignature.current = getTextSignature(element);
|
|
38
|
+
return;
|
|
20
39
|
}
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
.join('|');
|
|
28
|
-
if (!pattern) {
|
|
29
|
-
return children;
|
|
30
|
-
}
|
|
31
|
-
var regex = new RegExp("(".concat(pattern, ")"), caseSensitive ? 'g' : 'gi');
|
|
32
|
-
// Function to highlight text content
|
|
33
|
-
var highlightText = function (textContent) {
|
|
34
|
-
if (!textContent || typeof textContent !== 'string') {
|
|
35
|
-
return textContent;
|
|
40
|
+
// Apply highlighting (same logic as before)
|
|
41
|
+
const walker = document.createTreeWalker(element, NodeFilter.SHOW_TEXT, null);
|
|
42
|
+
const textNodes = [];
|
|
43
|
+
let node;
|
|
44
|
+
while ((node = walker.nextNode())) {
|
|
45
|
+
textNodes.push(node);
|
|
36
46
|
}
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
47
|
+
textNodes.forEach(textNode => {
|
|
48
|
+
var _a;
|
|
49
|
+
const text = textNode.textContent || '';
|
|
50
|
+
if (!text.trim())
|
|
51
|
+
return;
|
|
52
|
+
const pattern = wordsArray
|
|
53
|
+
.filter(word => word)
|
|
54
|
+
.map(word => {
|
|
40
55
|
if (word instanceof RegExp) {
|
|
41
|
-
return word.
|
|
56
|
+
return word.source;
|
|
42
57
|
}
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
58
|
+
let term = word.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
59
|
+
if (isWordBoundary) {
|
|
60
|
+
term = `\\b${term}\\b`;
|
|
61
|
+
}
|
|
62
|
+
return term;
|
|
63
|
+
})
|
|
64
|
+
.join('|');
|
|
65
|
+
if (!pattern)
|
|
66
|
+
return;
|
|
67
|
+
const regex = new RegExp(`(${pattern})`, caseSensitive ? 'g' : 'gi');
|
|
68
|
+
const parts = text.split(regex);
|
|
69
|
+
if (parts.length > 1) {
|
|
70
|
+
const fragment = document.createDocumentFragment();
|
|
71
|
+
parts.forEach(part => {
|
|
72
|
+
if (!part)
|
|
73
|
+
return;
|
|
74
|
+
const shouldHighlight = wordsArray.some(word => {
|
|
75
|
+
if (word instanceof RegExp) {
|
|
76
|
+
const testRegex = new RegExp(word.source, caseSensitive ? word.flags : word.flags + 'i');
|
|
77
|
+
return testRegex.test(part);
|
|
78
|
+
}
|
|
79
|
+
return caseSensitive
|
|
80
|
+
? part === word.trim()
|
|
81
|
+
: part.toLowerCase() === word.trim().toLowerCase();
|
|
82
|
+
});
|
|
83
|
+
if (shouldHighlight) {
|
|
84
|
+
const mark = document.createElement('mark');
|
|
85
|
+
mark.setAttribute('data-highlighter', 'true');
|
|
86
|
+
Object.assign(mark.style, highlightStyle);
|
|
87
|
+
mark.textContent = part;
|
|
88
|
+
fragment.appendChild(mark);
|
|
89
|
+
}
|
|
90
|
+
else {
|
|
91
|
+
fragment.appendChild(document.createTextNode(part));
|
|
92
|
+
}
|
|
93
|
+
});
|
|
94
|
+
(_a = textNode.parentNode) === null || _a === void 0 ? void 0 : _a.replaceChild(fragment, textNode);
|
|
95
|
+
}
|
|
48
96
|
});
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
if (
|
|
64
|
-
|
|
97
|
+
// Update signature after highlighting
|
|
98
|
+
lastHighlightSignature.current = getTextSignature(element);
|
|
99
|
+
}, [getTextSignature]);
|
|
100
|
+
useLayoutEffect(() => {
|
|
101
|
+
if (!containerRef.current)
|
|
102
|
+
return;
|
|
103
|
+
isDebug && console.log('Setting up persistent MutationObserver');
|
|
104
|
+
highlightTextInElement(containerRef.current);
|
|
105
|
+
setIsInitiallyReady(true);
|
|
106
|
+
observerRef.current = new MutationObserver((mutations) => {
|
|
107
|
+
if (!containerRef.current)
|
|
108
|
+
return;
|
|
109
|
+
// Check if the text signature has actually changed
|
|
110
|
+
const currentSignature = getTextSignature(containerRef.current);
|
|
111
|
+
if (currentSignature !== lastHighlightSignature.current) {
|
|
112
|
+
isDebug && console.log('Text signature changed, re-highlighting');
|
|
113
|
+
isDebug && console.log('Old:', lastHighlightSignature.current);
|
|
114
|
+
isDebug && console.log('New:', currentSignature);
|
|
115
|
+
highlightTextInElement(containerRef.current);
|
|
116
|
+
}
|
|
117
|
+
else {
|
|
118
|
+
isDebug && console.log('Text signature unchanged, ignoring mutation');
|
|
65
119
|
}
|
|
66
|
-
return processElement(child);
|
|
67
120
|
});
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
121
|
+
observerRef.current.observe(containerRef.current, {
|
|
122
|
+
childList: true,
|
|
123
|
+
subtree: true,
|
|
124
|
+
characterData: true
|
|
125
|
+
});
|
|
126
|
+
return () => {
|
|
127
|
+
if (observerRef.current) {
|
|
128
|
+
observerRef.current.disconnect();
|
|
129
|
+
}
|
|
130
|
+
};
|
|
131
|
+
}, [words, highlightStyle, caseSensitive, isWordBoundary, highlightTextInElement, getTextSignature]);
|
|
132
|
+
return (React.createElement("div", { ref: containerRef, style: {
|
|
133
|
+
visibility: isInitiallyReady ? 'visible' : 'hidden',
|
|
134
|
+
minHeight: isInitiallyReady ? 'auto' : '1em'
|
|
135
|
+
} }, children));
|
|
71
136
|
};
|
|
72
137
|
export default TextHighlighter;
|
|
138
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/index.tsx"],"names":[],"mappings":"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"}
|
package/package.json
CHANGED
|
@@ -1,32 +1,44 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "react-highlight-me",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "2.0.1",
|
|
4
4
|
"description": "Highlight words in React components or text",
|
|
5
5
|
"main": "dist/cjs/index.js",
|
|
6
6
|
"module": "dist/esm/index.js",
|
|
7
7
|
"exports": {
|
|
8
8
|
".": {
|
|
9
|
-
"import":
|
|
10
|
-
|
|
11
|
-
|
|
9
|
+
"import": {
|
|
10
|
+
"development": "./src/index.tsx",
|
|
11
|
+
"types": "./dist/esm/index.d.ts",
|
|
12
|
+
"default": "./dist/esm/index.js"
|
|
13
|
+
},
|
|
14
|
+
"require": {
|
|
15
|
+
"development": "./src/index.tsx",
|
|
16
|
+
"types": "./dist/cjs/index.d.ts",
|
|
17
|
+
"default": "./dist/cjs/index.js"
|
|
18
|
+
}
|
|
19
|
+
},
|
|
20
|
+
"./src/*": "./src/*"
|
|
12
21
|
},
|
|
13
22
|
"files": [
|
|
14
|
-
"dist"
|
|
23
|
+
"dist",
|
|
24
|
+
"src"
|
|
15
25
|
],
|
|
16
|
-
"scripts": {
|
|
17
|
-
"test": "echo \"Error: no test specified\" && exit 1",
|
|
18
|
-
"build": "npm run build:cjs && npm run build:esm",
|
|
19
|
-
"build:cjs": "tsc -p tsconfig.cjs.json",
|
|
20
|
-
"build:esm": "tsc -p tsconfig.esm.json",
|
|
21
|
-
"clean": "rm -rf dist",
|
|
22
|
-
"prepublishOnly": "npm run build"
|
|
23
|
-
},
|
|
24
26
|
"keywords": [],
|
|
25
27
|
"author": "Andrey Hohutkin <Andrey.Hohutkin@gmail.com",
|
|
28
|
+
"repository": {
|
|
29
|
+
"type": "git",
|
|
30
|
+
"url": "git+https://github.com/AHgPuK/react-highlight-me"
|
|
31
|
+
},
|
|
26
32
|
"license": "ISC",
|
|
27
|
-
"packageManager": "pnpm@10.9.0",
|
|
28
33
|
"devDependencies": {
|
|
29
34
|
"@types/react": "^19.1.8",
|
|
30
35
|
"typescript": "^5.3.3"
|
|
36
|
+
},
|
|
37
|
+
"scripts": {
|
|
38
|
+
"test": "echo \"Error: no test specified\" && exit 1",
|
|
39
|
+
"build": "npm run build:cjs && npm run build:esm",
|
|
40
|
+
"build:cjs": "tsc -p tsconfig.cjs.json",
|
|
41
|
+
"build:esm": "tsc -p tsconfig.esm.json",
|
|
42
|
+
"clean": "rm -rf dist"
|
|
31
43
|
}
|
|
32
|
-
}
|
|
44
|
+
}
|
package/src/index.tsx
ADDED
|
@@ -0,0 +1,190 @@
|
|
|
1
|
+
import React, { useLayoutEffect, useRef, useCallback, useState } from 'react';
|
|
2
|
+
|
|
3
|
+
type Props = {
|
|
4
|
+
children?: React.ReactNode;
|
|
5
|
+
words?: string[] | string | RegExp | RegExp[];
|
|
6
|
+
highlightStyle?: React.CSSProperties;
|
|
7
|
+
caseSensitive?: boolean;
|
|
8
|
+
isWordBoundary?: boolean;
|
|
9
|
+
isDebug?: boolean;
|
|
10
|
+
}
|
|
11
|
+
// Alternative approach: Use a single observer that never disconnects
|
|
12
|
+
const TextHighlighter = ({
|
|
13
|
+
children,
|
|
14
|
+
words = [],
|
|
15
|
+
highlightStyle = { backgroundColor: 'yellow', fontWeight: 'bold' },
|
|
16
|
+
caseSensitive = false,
|
|
17
|
+
isWordBoundary = true,
|
|
18
|
+
isDebug = false,
|
|
19
|
+
}: Props) => {
|
|
20
|
+
const containerRef = useRef<HTMLDivElement>(null);
|
|
21
|
+
const observerRef = useRef<MutationObserver | null>(null);
|
|
22
|
+
const lastHighlightSignature = useRef<string>('');
|
|
23
|
+
const [isInitiallyReady, setIsInitiallyReady] = useState(false);
|
|
24
|
+
|
|
25
|
+
const propsRef = useRef({ words, highlightStyle, caseSensitive, isWordBoundary });
|
|
26
|
+
propsRef.current = { words, highlightStyle, caseSensitive, isWordBoundary };
|
|
27
|
+
|
|
28
|
+
const getTextSignature = useCallback((element: HTMLElement): string => {
|
|
29
|
+
// Create a signature of all text content to detect real changes
|
|
30
|
+
const walker = document.createTreeWalker(
|
|
31
|
+
element,
|
|
32
|
+
NodeFilter.SHOW_TEXT,
|
|
33
|
+
null
|
|
34
|
+
);
|
|
35
|
+
|
|
36
|
+
const textParts: string[] = [];
|
|
37
|
+
let node;
|
|
38
|
+
while ((node = walker.nextNode())) {
|
|
39
|
+
if (!(node as Text).parentElement?.hasAttribute('data-highlighter')) {
|
|
40
|
+
textParts.push((node as Text).textContent || '');
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
return textParts.join('|');
|
|
45
|
+
}, []);
|
|
46
|
+
|
|
47
|
+
const highlightTextInElement = useCallback((element: HTMLElement) => {
|
|
48
|
+
const { words, highlightStyle, caseSensitive, isWordBoundary } = propsRef.current;
|
|
49
|
+
const wordsArray = Array.isArray(words) ? words : [words];
|
|
50
|
+
|
|
51
|
+
isDebug && console.log('Highlighting with words:', wordsArray);
|
|
52
|
+
|
|
53
|
+
// Remove existing highlights
|
|
54
|
+
const existingMarks = element.querySelectorAll('mark[data-highlighter="true"]');
|
|
55
|
+
existingMarks.forEach(mark => {
|
|
56
|
+
const textContent = mark.textContent || '';
|
|
57
|
+
const textNode = document.createTextNode(textContent);
|
|
58
|
+
mark.parentNode?.replaceChild(textNode, mark);
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
element.normalize(); // Ensure text nodes are merged
|
|
62
|
+
|
|
63
|
+
if (!wordsArray.length || wordsArray.every(word => !word)) {
|
|
64
|
+
lastHighlightSignature.current = getTextSignature(element);
|
|
65
|
+
return;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
// Apply highlighting (same logic as before)
|
|
69
|
+
const walker = document.createTreeWalker(
|
|
70
|
+
element,
|
|
71
|
+
NodeFilter.SHOW_TEXT,
|
|
72
|
+
null
|
|
73
|
+
);
|
|
74
|
+
|
|
75
|
+
const textNodes: Text[] = [];
|
|
76
|
+
let node;
|
|
77
|
+
while ((node = walker.nextNode())) {
|
|
78
|
+
textNodes.push(node as Text);
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
textNodes.forEach(textNode => {
|
|
82
|
+
const text = textNode.textContent || '';
|
|
83
|
+
if (!text.trim()) return;
|
|
84
|
+
|
|
85
|
+
const pattern = wordsArray
|
|
86
|
+
.filter(word => word)
|
|
87
|
+
.map(word => {
|
|
88
|
+
if (word instanceof RegExp) {
|
|
89
|
+
return word.source;
|
|
90
|
+
}
|
|
91
|
+
let term = word.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
92
|
+
if (isWordBoundary) {
|
|
93
|
+
term = `\\b${term}\\b`;
|
|
94
|
+
}
|
|
95
|
+
return term;
|
|
96
|
+
})
|
|
97
|
+
.join('|');
|
|
98
|
+
|
|
99
|
+
if (!pattern) return;
|
|
100
|
+
|
|
101
|
+
const regex = new RegExp(`(${pattern})`, caseSensitive ? 'g' : 'gi');
|
|
102
|
+
const parts = text.split(regex);
|
|
103
|
+
|
|
104
|
+
if (parts.length > 1) {
|
|
105
|
+
const fragment = document.createDocumentFragment();
|
|
106
|
+
|
|
107
|
+
parts.forEach(part => {
|
|
108
|
+
if (!part) return;
|
|
109
|
+
|
|
110
|
+
const shouldHighlight = wordsArray.some(word => {
|
|
111
|
+
if (word instanceof RegExp) {
|
|
112
|
+
const testRegex = new RegExp(word.source, caseSensitive ? word.flags : word.flags + 'i');
|
|
113
|
+
return testRegex.test(part);
|
|
114
|
+
}
|
|
115
|
+
return caseSensitive
|
|
116
|
+
? part === word.trim()
|
|
117
|
+
: part.toLowerCase() === word.trim().toLowerCase();
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
if (shouldHighlight) {
|
|
121
|
+
const mark = document.createElement('mark');
|
|
122
|
+
mark.setAttribute('data-highlighter', 'true');
|
|
123
|
+
Object.assign(mark.style, highlightStyle);
|
|
124
|
+
mark.textContent = part;
|
|
125
|
+
fragment.appendChild(mark);
|
|
126
|
+
} else {
|
|
127
|
+
fragment.appendChild(document.createTextNode(part));
|
|
128
|
+
}
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
textNode.parentNode?.replaceChild(fragment, textNode);
|
|
132
|
+
}
|
|
133
|
+
});
|
|
134
|
+
|
|
135
|
+
// Update signature after highlighting
|
|
136
|
+
lastHighlightSignature.current = getTextSignature(element);
|
|
137
|
+
}, [getTextSignature]);
|
|
138
|
+
|
|
139
|
+
useLayoutEffect(() => {
|
|
140
|
+
if (!containerRef.current) return;
|
|
141
|
+
|
|
142
|
+
isDebug && console.log('Setting up persistent MutationObserver');
|
|
143
|
+
|
|
144
|
+
highlightTextInElement(containerRef.current);
|
|
145
|
+
setIsInitiallyReady(true);
|
|
146
|
+
|
|
147
|
+
observerRef.current = new MutationObserver((mutations) => {
|
|
148
|
+
if (!containerRef.current) return;
|
|
149
|
+
|
|
150
|
+
// Check if the text signature has actually changed
|
|
151
|
+
const currentSignature = getTextSignature(containerRef.current);
|
|
152
|
+
|
|
153
|
+
if (currentSignature !== lastHighlightSignature.current) {
|
|
154
|
+
isDebug && console.log('Text signature changed, re-highlighting');
|
|
155
|
+
isDebug && console.log('Old:', lastHighlightSignature.current);
|
|
156
|
+
isDebug && console.log('New:', currentSignature);
|
|
157
|
+
|
|
158
|
+
highlightTextInElement(containerRef.current);
|
|
159
|
+
} else {
|
|
160
|
+
isDebug && console.log('Text signature unchanged, ignoring mutation');
|
|
161
|
+
}
|
|
162
|
+
});
|
|
163
|
+
|
|
164
|
+
observerRef.current.observe(containerRef.current, {
|
|
165
|
+
childList: true,
|
|
166
|
+
subtree: true,
|
|
167
|
+
characterData: true
|
|
168
|
+
});
|
|
169
|
+
|
|
170
|
+
return () => {
|
|
171
|
+
if (observerRef.current) {
|
|
172
|
+
observerRef.current.disconnect();
|
|
173
|
+
}
|
|
174
|
+
};
|
|
175
|
+
}, [words, highlightStyle, caseSensitive, isWordBoundary, highlightTextInElement, getTextSignature]);
|
|
176
|
+
|
|
177
|
+
return (
|
|
178
|
+
<div
|
|
179
|
+
ref={containerRef}
|
|
180
|
+
style={{
|
|
181
|
+
visibility: isInitiallyReady ? 'visible' : 'hidden',
|
|
182
|
+
minHeight: isInitiallyReady ? 'auto' : '1em'
|
|
183
|
+
}}
|
|
184
|
+
>
|
|
185
|
+
{children}
|
|
186
|
+
</div>
|
|
187
|
+
);
|
|
188
|
+
};
|
|
189
|
+
|
|
190
|
+
export default TextHighlighter;
|