Home > other >  RegEx/JS: Wrap innerHTML text elements in span including nested HTML elements
RegEx/JS: Wrap innerHTML text elements in span including nested HTML elements

Time:06-24

Given the innerHTML of an element, I'm trying to wrap each word in a span, and if the word is already wrapped, wrap the element.

e.g. Such that Here is a paragraph <span >which</span> <div id="typed-effect">might</div> have nested elements

Becomes <span>Here</span> <span>is</span> <span>a</span> <span>paragraph</span> <span >which</span> <span><div id="typed-effect">might</div></span> <span>have </span> <span>nested</span> <span>elements</span>

The components I have are:

  1. Extract the innerHTML and wrap in spans:
function wrapElementText(elementSelector) {
   const element = document.querySelector(elementSelector);
   const rawHTML = element.innerHTML;
   // Do magic to create list
   element.innerHTML = "";
   list.forEach((i, item) => {
     if (!item.contains(`<span`)) {
        element.innerHTML  = `<span >${item}</span>`
      }
   }
};
  1. My best attempt is this ((\<.*?\>)|((\s?).*?(\s))) but it's returning e.g. <div id="typed-effect"> and might</div> as seperate groups when it should be one group <div id="typed-effect">might</div>.

Thanks so much in advance!

CodePudding user response:

Ok so i came up with these solutions:

  1. the regex approach(not recommended but what you asked for):
function wrapElementTextRegexp(elementSelector) {
    const element = document.querySelector(elementSelector);
    const chunks = element.innerHTML.match(/[a-zA-Z] |(<[a-z][^>] >. <\/[^>] >)/g);
    element.innerHTML = "";
    for(const chunk of chunks) {
        element.innerHTML  = !chunk.startsWith('<span')
            ? `<span >${chunk}</span>`
            : chunk
    }
}

Here, the regexp is searching for a whole text or for a html tag pattern(simplified), in case it finds something that is not a span it wraps it in a span, otherwise it just return the original span

  1. the childNodes approach(imo better):
function wrapElementTextNodes(elementSelector) {
    const element = document.querySelector(elementSelector);
    const parsedInnerHTML = [...element.childNodes]
        .map(node => node instanceof HTMLElement
            ? node instanceof HTMLSpanElement
                ? node.outerHTML
                : `<span >${node.outerHTML}</span>`
            : node.textContent
                  .trim()
                  .split(/\s /)
                  .map(word => `<span >${word}</span>`))
        .flat()
        .filter(chunk => !chunk.match(/><\//g))
        .join('')
  element.innerHTML = parsedInnerHTML;
}

Here we have access to the instances and we don't have to split the innerHTML since we're using childNodes, in the example i mapped the nodes by checking if the node was an actual HTMLElement and if yes if it was a HTMLSpanElement. After the first map i used a filter to remove from the array every empty node generated after the map(filtering out nodes like <span ></span>) and finally i join() the elements.

IMPORTANT NOTE wrapping a <div> element inside a <p> it's a bad practice, javascript won't recognize any childNode after the div(included) so for example if you have

<p>
    some
    <div>bad</div>
    example
</p>

the div and the node example will be omitted in element.innerHTML that, in this case, will return some. So make sure you correct your markup

  • Related