Consider the following example HTML which I need to highlight a search text 192.168.1.1
. This is just an example, the text search and HTML structure are totally arbitrary because it will be run on end-user websites with their inputs. The program will also perform additional operations on the inserted span
, not just highlighting (for example displaying a tooltip when hovering but this is not an issue as long as I have a span
inserted).
EDIT: I just found out a new line would result in space in between which textContent
won't be correct anymore. However I will keep them there for better visual. You can check the snippet below for correct HTML code
<div>
<span >The computer address</span>
<em>is</em>
<span >192</span>.
<span >168</span>.
<span >1</span>.
<span >1</span>
</div>
Using textContent
or innerText
, it's easy to find the closest element with the subtext I need (the div
in the example has the full 192.168.1.1
text). However the next step I want to do wrap those parts without breaking the original structure or other HTML/CSS class
or properties. Ideally, I'd like to have the final HTML to be:
<div>
<span >The computer address</span>
<em>is</em>
<span ><span >192</span>.
<span >168</span>.
<span >1</span>.
<span >1</span></span>
</div>
Using deep-first search I can easily get to the div
, but then I have no idea how to proceed from there. There's also this tricky case:
<div>
<span >The computer address</span>
<em>is
<span >192</span>.</em>
<span >168</span>.
<span >1</span>.
<span >1</span>
</div>
I think if there is a solution, this should be the easiest way to achieve it:
<div>
<span >The computer address</span>
<em>is
<span ><span >192</span>.</span></em><span >
<span >168</span>.
<span >1</span>.
<span >1</span></span>
</div>
The code will be executed in Chrome extension environment if it's relevance but I believe this should be a pure browser Javascript problem.
Here's a snippet so you can try out:
function transform(el, text) {
// Transform this Element to highlight text
}
const text = "192.168.1.1";
transform(document.querySelector("#case-1"), text);
transform(document.querySelector("#case-2"), text);
transform(document.querySelector("#case-simple"), text);
.my-highlight-span {
background-color: cornflowerblue;
}
<p>Case 1:</p>
<div id="case-1">
<span >The computer address</span>
<em>is</em>
<span >192</span>.<span >168</span>.<span >1</span>.<span >1</span>
</div>
<p>Case 2:</p>
<div id="case-2">
<span >The computer address</span>
<em>is
<span >192</span>.</em><span >168</span>.<span >1</span>.<span >1</span>
</div>
<p>Simple case:</p>
<div id="case-simple">
The computer address is 192.168.1.1
</div>
<hr />
<p>Desired Result:</p>
<div id="case-1-result">
<span >The computer address</span>
<em>is</em>
<span ><span >192</span>.<span >168</span>.<span >1</span>.<span >1</span></span>
</div>
<div id="case-2-result">
<span >The computer address</span>
<em>is
<span ><span >192</span>.</span></em><span >
<span >168</span>.<span >1</span>.<span >1</span></span>
</div>
<div id="case-simple-result">
The computer address is <span >192.168.1.1</span>
</div>
CodePudding user response:
I am finally get the answer using Range
API, credit to awesome explanation here and a subtle workaround of Range.surroundContents()
.
Using
scanForText
method I recursively scan fortextContent
ofTEXT_NODE
s to determine the start and end of a search term.I then use
Range
API to select and extract the content and insert it back after surrounding it with myspan
.
There is some limitation obviously but this should work for most cases. For example, in case 2, my code would generate an extra empty <span >
inside <em>
.
function scanForText(el, expectingStart, expectingEnd, result) {
if (el.nodeType === Node.TEXT_NODE) {
const nodeContent = el.textContent;
result[4] = nodeContent;
const currText = result[4];
if (expectingStart < currText.length && !result[0]) {
result[0] = el;
result[1] = expectingStart - (currText.length - nodeContent.length);
}
if (expectingEnd <= currText.length && !result[2]) {
result[2] = el;
result[3] = expectingEnd - (currText.length - nodeContent.length);
return;
}
} else {
for (let childEl of el.childNodes) {
scanForText(childEl, expectingStart, expectingEnd, result);
if (result[2]) { // When already found ending, return
return;
}
}
}
}
function transform(el, text) {
const fullText = el.textContent;
const startIndex = fullText.indexOf(text);
const endIndex = startIndex text.length;
const scanState = [null, -1, null, -1, ""];
scanForText(el, startIndex, endIndex, scanState);
const [startNode, startRangeIndex, endNode, endRangeIndex] = scanState;
if (!startNode || !endNode) {
console.warn("This should not be happening");
return;
}
const range = new Range();
range.setStart(startNode, startRangeIndex);
range.setEnd(endNode, endRangeIndex);
const surrounding = document.createElement("span");
surrounding.className = "my-highlight-span";
// This can't be used when cutting one text boundary.
// Alternative is offered at https://developer.mozilla.org/en-US/docs/Web/API/Range/surroundContents
// range.surroundContents(surrounding)
const extracted = range.extractContents();
surrounding.appendChild(extracted);
range.insertNode(surrounding);
}
const text = "192.168.1.1";
transform(document.querySelector("#case-1"), text);
transform(document.querySelector("#case-2"), text);
transform(document.querySelector("#case-simple"), text);
.my-highlight-span {
background-color: cornflowerblue;
}
<p>Case 1:</p>
<div id="case-1">
<span >The computer address</span>
<em>is</em>
<span >192</span>.<span >168</span>.<span >1</span>.<span >1</span>
</div>
<p>Case 2:</p>
<div id="case-2">
<span >The computer address</span>
<em>is
<span >192</span>.</em><span >168</span>.<span >1</span>.<span >1</span>
</div>
<p>Simple case:</p>
<div id="case-simple">
The computer address is 192.168.1.1
</div>
<hr />
<p>Desired Result:</p>
<div id="case-1-result">
<span >The computer address</span>
<em>is</em>
<span ><span >192</span>.<span >168</span>.<span >1</span>.<span >1</span></span>
</div>
<div id="case-2-result">
<span >The computer address</span>
<em>is
<span ><span >192</span>.</span></em><span >
<span >168</span>.<span >1</span>.<span >1</span></span>
</div>
<div id="case-simple-result">
The computer address is <span >192.168.1.1</span>
</div>