To wrap a part of innerHTML with new element

Viewed 407

Here is HTML on a forum:

<div class="sign">username <span class="info">info</span></div>

Here is how I need to change it by using a userscript:

<div class="sign"><a itemprop="creator">username</a> <span class="info">info</span></div>

I know how to create an a element, assign it a custom attribute, and add it to DOM.

But I don't understand how to wrap username with it. That is, how to convert username from the 1st example to <a itemprop="creator">username</a> in the second example.

edit: An a element will have href as well. I omitted it intentionally to make the code shorter.

4 Answers

You can try replacing the innerHTML:

var el = document.querySelector('.sign');
el.innerHTML = el.innerHTML.replace('username', '<a itemprop="creator">username</a>');
<div class="sign">username <span class="info">info</span></div>

this way ?

let parentElm = document.querySelector('div.sign')
  , refNod    = parentElm.childNodes[0] 
  ;
  
if ( refNod.nodeValue === 'username') 
  {
  let newNod = document.createElement('a')
  // newNod.href = '/...'
  newNod.setAttribute('itemprop','creator')
  newNod.textContent = refNod.nodeValue
 
  parentElm.replaceChild( newNod, refNod )
  }

/*****/
console.log( parentElm.innerHTML )
<div class="sign">username<span class="info">info</span></div>

Doing this with vanilla DOM APIs is a little involved, but not too hard. You will need to locate the DOM text node which contains the fragment you want to replace, split it into three parts, then replace the middle part with the node you want.

If you have a text node textNode and want to replace the text spanning from index i to index j with a node computed by replacer, you can use this function:

function spliceTextNode(textNode, i, j, replacer) {
    const parent = textNode.parentNode;
    const after = textNode.splitText(j);
    const middle = i ? textNode.splitText(i) : textNode;
    middle.remove();
    parent.insertBefore(replacer(middle), after);
}

Adapting your example, you will have to use it something like this:

function spliceTextNode(textNode, i, j, replacer) {
    const parent = textNode.parentNode;
    const after = textNode.splitText(j);
    const middle = i ? textNode.splitText(i) : textNode;
    middle.remove();
    parent.insertBefore(replacer(middle), after);
}

document.getElementById('inject').addEventListener('click', () => { 
    const textNode = document.querySelector('div.sign').firstChild;
    const m = /\w+/.exec(textNode.data);
    spliceTextNode(textNode, m.index, m.index + m[0].length, node => {
        const a = document.createElement('a');
        a.itemprop = 'creator';
        a.href = 'https://example.com/';
        a.title = "The hottest examples on the Web!";
        a.appendChild(node);
        return a;
    })
}, false);

/* this is to demonstrate other nodes underneath the <div> are untouched */
document.querySelector('.info').addEventListener('click', (ev) => {
    ev.preventDefault();
    alert('hello');
}, false);
<div class="sign">@username: haha, <a href="http://example.org" class="info">click me too</a></div>

<p> <input id="inject" type="button" value="inject link">

Note how the ‘click me too’ handler is still attached to the link after the ‘username’ link is injected; modifying innerHTML would fail to preserve this.

Find all elements with class .sign and replace it's inner HTML with a wrapped text before the span tag.

var elms = document.querySelectorAll('.sign');
for (var i = 0; i < elms.length; i++) {
  let item = elms[i];
  var html = item.innerHTML;
  item.innerHTML = html.replace(/(.*)?<span/, "<a itemprop=\"creator\" href='#'>$1</a><span");
}
<div class="sign">username <span class="info">info</span></div>
<div class="sign">other name <span class="info">info</span></div>
<div class="sign">other more and more <span class="info">info</span></div>

A little bit of regex saves the day sometimes.

If you want to preserve event attached to the span element-

var elms = document.querySelectorAll('.sign');
elms.forEach(item => {
  let span = item.querySelector('span');
  var html = item.innerHTML;
  let txt = html.match(/(.*)?<span/)[0].replace("<span", "");
  item.innerHTML = `<a itemprop="creator" href='#'>${txt}</a>`;
  item.appendChild(span);
});

Related