UNPKG

js-text-highlighter

Version:

A framework-agnostic text highlighting library that supports virtualized texts

113 lines (112 loc) 4.79 kB
export class Highlighter { constructor(options) { this.observer = null; this.options = options; } highlight(element, observe = true) { this.highlightElement(element); if (observe) { this.observeChanges(element); } } updateOptions(options) { this.options = Object.assign(Object.assign({}, this.options), options); this.rehighlightAll(); } destroy() { if (this.observer) { this.observer.disconnect(); } } /** * Highlights the text content within a given element. * @param element The element in which to highlight text. */ highlightElement(element) { const { texts, colors, caseSensitive } = this.options; // Create a tree walker to iterate over text nodes const walk = document.createTreeWalker(element, NodeFilter.SHOW_TEXT); let node; while ((node = walk.nextNode())) { const textNode = node; const content = textNode.nodeValue || ""; // Skip empty text nodes or nodes without a parent if (!content.trim() || !textNode.parentElement) continue; const matches = []; // Iterate through each text to search and highlight for (const [i, text] of texts.entries()) { // Create a regular expression to match the text, considering case sensitivity const regex = new RegExp(text.replace(/[-/\\^$*+?.()|[\]{}]/g, "\\$&"), caseSensitive ? "g" : "gi"); let match; while ((match = regex.exec(content)) !== null) { // Collect matches with their start and end positions and the corresponding color matches.push({ start: match.index, end: match.index + match[0].length, color: colors[i % colors.length], }); } } matches.sort((a, b) => a.start - b.start); // Create a document fragment to replace the original text node const fragment = document.createDocumentFragment(); let lastIndex = 0; for (const match of matches) { if (match.start >= lastIndex) { if (match.start > lastIndex) { fragment.appendChild( // Add non-matching content document.createTextNode(content.slice(lastIndex, match.start))); } // Create a span for the matched text with the specified background color const span = document.createElement("span"); span.textContent = content.slice(match.start, match.end); span.style.backgroundColor = match.color; fragment.appendChild(span); lastIndex = match.end; // Update the last index to avoid overlapping matches } } // Append any remaining content that wasn't part of a match if (lastIndex < content.length) { fragment.appendChild(document.createTextNode(content.slice(lastIndex))); } // Replace the original text node with the newly created fragment if (fragment.childNodes.length > 0) { textNode.replaceWith(fragment); } } } /** * Observes changes in the DOM (like new nodes being added) and highlights them as needed. * @param element The element to observe for changes. */ observeChanges(element) { this.observer = new MutationObserver((mutations) => { var _a; (_a = this.observer) === null || _a === void 0 ? void 0 : _a.disconnect(); mutations.forEach((mutation) => { if (mutation.type === "childList") { mutation.addedNodes.forEach((node) => { if (node instanceof HTMLElement) { this.highlightElement(node); } }); } }); this.observeChanges(element); }); this.observer.observe(element, { childList: true, subtree: true }); } /** * Reapplies highlighting to all previously highlighted elements. */ rehighlightAll() { const highlightedElements = document.querySelectorAll('[data-highlighted="true"]'); highlightedElements.forEach((element) => { if (element instanceof HTMLElement) { this.highlightElement(element); // Re-highlight each element marked as highlighted } }); } }