I want to update the innerHTML of a div, but want to handle text differently if it already belongs to a span
<div id="a">
Here is some text
<span>Here is some more span text</span>
More Text...
<span>contains TARGET</span>
Another Line with the TARGET
</div>
My goal is to replace all instances of TARGET
with <span class="target-class">TARGET</span>
if it isn’t already in a span. My code currently loops through children and before doing the replacement checks if the tag = SPAN and the span contains the target-class
.
<div id="a">
Here is some text
<span>Here is some more span text</span>
More Text...
<span>contains TARGET</span>
Another Line with the <span class="target-class">TARGET</span>
</div>
When I call .innerHTML
on #a
it returns the full div HTML. When I loop through #a
‘s children it loops through the spans — I’m hoping there’s some way to loop through what I’ll loosely call the text fragments
so I can replace their innerHTML
Current Code:
Wrapping logic to loop through document elements
export async function updateLabels(includedItems: Set<string>, excludedItems: Set<string>, allItems: Set<string>) {
visitChildren(
document.body,
[
(element: HTMLElement) => replaceItemWithTag(element, allItems, `<span class="custom-label">$1</span>`),
],
)
// Ignore this - it loops through all elements w/ the "custom-label" class and adds another class "custom-label--include"/"custom-label--exclude" depending on which set it belongs to
// setItemClassifications(includedItems, excludedItems)
}
dom walk code:
export function visitChildren(
element: HTMLElement,
visitors: ((element: Element) => [boolean, string[]])[],
) {
for (let i = 0; i < element.children.length; i++) {
if (element.children[i].tagName === "SCRIPT") {
// skip scripts
continue;
}
if (element.children[i].tagName === "STYLE") {
// skip style
continue;
}
const childVisitors: ((element: Element) => [boolean, string[]])[] = [];
const childVisitorReasons: [boolean, string[]][] = [];
for (let j = 0; j < visitors.length; j++) {
const visitor = visitors[j];
const [result, reasons] = visitor(element.children[i]);
childVisitorReasons.push([result, reasons]);
if (result) {
childVisitors.push(visitor);
}
}
if (childVisitors.length > 0) {
visitChildren(element.children[i] as HTMLElement, childVisitors);
}
}
}
replacement code for this instance:
export function replaceItemWithTag(element: HTMLElement, targets: Set<string>, replaceWith: string): [boolean, string[]] {
const targetArray = Array.from(targets);
const reg = RegExp(`\b(${targetArray.join("|")})\b`, "ig");
const reason: string[] = [
`targets: ${targetArray.join(",")}`,
`regex: ${reg}`,
];
// skip element if it's already been replaced
if (element.classList.contains("custom-label")) {
reason.push("already replaced")
return [false, reason]
}
let innerHTMLIncludesATarget = false;
for (const target of targetArray) {
if (element.innerHTML.toLowerCase().includes(target)) {
innerHTMLIncludesATarget = true;
break;
}
}
if (!innerHTMLIncludesATarget) {
reason.push("no target in innerHTML")
if (debugPrint) { console.log({ element, innerHTML: element.innerHTML, innerText: element.innerText, filter: false, reason }) }
return [false, reason];
}
reason.push("target in innerHTML")
let innerTextIncludesATarget = false;
for (const target of targetArray) {
if (element.innerText.toLowerCase().includes(target)) {
innerTextIncludesATarget = true;
break;
}
}
if (innerTextIncludesATarget) {
reason.push("target is in innerText")
// The issue is here -- I'm not checking the text, only the sub - spans
if (element.children.length === 0) {
reason.push("no children")
element.innerHTML = element.innerHTML.replace(reg, replaceWith);
reason.push("replaced")
return [false, reason]; // don't visit children if we've already replaced the string
}
if (debugPrint) { console.log({ element, innerHTML: element.innerHTML, innerText: element.innerText, filter: true, reason }) }
return [true, reason]
}
return [true, reason]
}
2
Answers
You can loop over the
childNodes
of the container to find all the text nodes, then usereplaceWith
to replace the node with a new set of nodes.With this I expanded @Unmitigated answer how the same can be applied with a recursive function to also apply the logic to child nodes.
So I wrapped the logic in a function and call that from within the function on childnodes (when elements).