Suppose I have some HTML on a forum:
<div class="sign">username <span class="info">info</span></div>
I want to write a user script that changes it to something like this:
<div class="sign"><a itemprop="creator">username</a> <span class="info">info</span></div>
(The a
element will have href
as well. I omitted it intentionally to make the code shorter.)
I know how to create an a
element, assign it a custom attribute, and add it to the DOM.
But I don't understand how to wrap username
with it. That is, how to convert username
from the 1st snippet to <a itemprop="creator">username</a>
in the second snippet.
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:
const 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', () => {
// XXX: locating the appropriate text node may vary
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> <button id="inject">inject link</button>
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.