skip to Main Content

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


  1. You can loop over the childNodes of the container to find all the text nodes, then use replaceWith to replace the node with a new set of nodes.

    const el = document.querySelector('#a');
    for (const node of el.childNodes) 
      if (node.nodeType === Node.TEXT_NODE) {
        node.replaceWith(document.createRange().createContextualFragment(
          node.textContent.replaceAll('TARGET', '<span class="target-class">$&</span>')
        ));
      }
    .target-class { background-color: yellow; }
    <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>
    Login or Signup to reply.
  2. 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).

    wrapTextWithTag(document.querySelector('#a').childNodes, 'TARGET', '<span class="target-class">$&</span>');
    
    function wrapTextWithTag(nodelist, text, tag) {
      for (const node of nodelist) {
        if (node.nodeType === Node.TEXT_NODE) {
          node.replaceWith(document.createRange().createContextualFragment(
            node.textContent.replaceAll(text, tag)
          ));
        } else if (node.nodeType === Node.ELEMENT_NODE) {
          wrapTextWithTag(node.childNodes, text, tag);
        }
      }
    }
    .target-class {
      background-color: yellow;
    }
    <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>
        Here is some text
        <span>Here is some more span text</span>
        More Text...
        <span>contains TARGET</span>
        Another Line with the TARGET
        <div>
          Here is some text
          <span>Here is some more span text</span>
          More Text...
          <span>contains TARGET</span>
          Another Line with the TARGET
        </div>
      </div>
    </div>
    Login or Signup to reply.
Please signup or login to give your own answer.
Back To Top
Search