6

I am iterating over all the text node in an html document in order to surround some words with a specific span.

Changing the nodeValue doesn't allow me to insert html. The span is escaped to be shown in plain text and I do not want that.

Here is what I have so far :

var elements = document.getElementsByTagName('*');

for (var i = 0; i < elements.length; i++) {
  var element = elements[i];

  for (var j = 0; j < element.childNodes.length; j++) {
    var node = element.childNodes[j];

    if (node.nodeType === Node.TEXT_NODE) {
      node.nodeValue = node.nodeValue.replace(/Questions/, "<span>Questions</span>");
    }
  }
}
<p>Questions1</p>
<p>Questions 2</p>
<p>Questions 3</p>
<p>Questions 4</p>

4
  • 2
    you cant add markup to the value, you would have to append a new child to the p element. Commented Aug 6, 2016 at 7:40
  • See stackoverflow.com/questions/6328718/…, stackoverflow.com/questions/4040495/…, and many other questions. Commented Aug 6, 2016 at 8:16
  • They went even shorter on this thread : stackoverflow.com/questions/1144783/… Commented Aug 6, 2016 at 9:43
  • 2
    @technico this could be dangerous depending on which text you're trying to wrap, if the word is a reserved word as tags or attributes it'll be a mess! Commented Aug 6, 2016 at 9:58

3 Answers 3

4

I think that you need to recurse all the DOM and each match... have a look here:

function replacer(node, parent) { 
  var r = /Questions/g;
  var result = r.exec(node.nodeValue);
  if(!result) { return; }
  
  var newNode = this.createElement('span');
  
  newNode.innerHTML = node
    .nodeValue
    .replace(r, '<span class="replaced">$&</span>')
  ;
  
  parent.replaceChild(newNode, node);
}


document.addEventListener('DOMContentLoaded', () => {
  function textNodesIterator(e, cb) {
    if (e.childNodes.length) {
      return Array
        .prototype
        .forEach
        .call(e.childNodes, i => textNodesIterator(i, cb))
      ;
    } 

    if (e.nodeType == Node.TEXT_NODE && e.nodeValue) {
      cb.call(document, e, e.parentNode);
    }
  }

  document
    .getElementById('highlight')
    .onclick = () => textNodesIterator(
    document.body, replacer
  );
});
.replaced {background: yellow; }
.replaced .replaced {background: lightseagreen; }
.replaced .replaced .replaced {background: lightcoral; }
<button id="highlight">Highlight</button>
<hr>
<p>Questions1</p>
<p>Questions 2</p>
<p>Questions 3</p>
<p>Questions 4</p>
<p>Questions 5 Questions 6</p>
<div>
  <h1>Nesting</h1>
  Questions <strong>Questions 4</strong>
  <div> Questions <strong>Questions 4</strong></div>
  
  
  <div> 
    Questions <strong>Questions 4</strong>
    
  <div> Questions <strong>Questions 4</strong></div>
  </div>
</div>

Sign up to request clarification or add additional context in comments.

4 Comments

would it work for multi-level nested elements, an HTML structure like in this jsfiddle.net/7pnn2fb7 ??
Nice, here's a codepen showing your work :), the only disadvantage of this is it wrap each text node within a span too in addition to the span needed for the keyword. upvoted!
yep, I know... by the way... my work can be executed pressing run the code script here...
Oh, that big blue button, didn't notice it all these years, thanks mate :P.. well some people here, include me, like to provide both code snippets and external link from jsfiddle.net, or codepen.io , or jsbin.com, or plnkr.co, etc
1

Finally I could do this without adding extra markup except the needed span:

Updated

jsFiddle

var elements = document.body.getElementsByTagName('*');;

for (var i = 0; i < elements.length; i++) {
  var element = elements[i];

  for (var j = 0; j < element.childNodes.length; j++) {
    var node = element.childNodes[j],
      par = node.parentElement;

    // as well as checking the nodeType as text, we make sure the 
    // parent element doesn't have the class "foo", so that we only
    // wrap the keyword once, instead of being in a loop to infinity
    if (node.nodeType === Node.TEXT_NODE && !par.classList.contains('foo')) {
      updateText(node, par);
    }
  }
}

function updateText(el, par) {
  var nv = el.nodeValue,
    txt = nv.replace(/Questions/g, '<span class="foo">Questions</span> ');

  // replace the whole old text node with the new modified one
  // and inject it as parent HTML
  par.innerHTML = par.innerHTML.replace(nv, txt);
}
.foo {color: white; background-color: green; padding: 5px;}
<div id="wrapper">
  this is test looking for the the word Questions.
  <br>
  <div id="test">
    <p>Lorem ipsum dolor Questions sit amet, <strong>consectetur</strong> adipisicing elit.</p>
    <p>Questions 1</p>
    <p>Questions 2</p>
    <p>Questions 3</p>
    <p>Questions 4</p>
  </div>
  <div>Lorem ipsum dolor sit amet, Questions consectetur adipisicing elit. Ipsa sed Questions ratione dolorem at repellendus animi eveniet similique repellat, sequi rem numquam debitis sit reprehenderit laborum dicta omnis iure quidem atque?</div>
</div>

Comments

-2

Seen comment, don't use this, as it can seriously mess up the page.

Kept for posterity, don't use :

document.body.innerHTML = document.body.innerHTML.replaceAll(myVar, "<"+myTag+">"+myVar+</"+myTag+">");

More info on https://stackoverflow.com/a/17606289/6660122 and in comments.

String.prototype.replaceAll = function(search, replacement) {
var target = this;
return target.split(search).join(replacement);
};

document.body.innerHTML = document.body.innerHTML.replaceAll("Questions", "<b>Questions</b>");
<p>Questions1</p>
<p>Questions 2</p>
<p>Questions 3</p>
<p>Questions 4</p>

Nice case to study !

3 Comments

No! this is dangerous, I've already followed a similar way in my "deleted" answer using regex, it works that's right.. but the disadvantage is if the text you need to wrap with span exists in html as reserved words, like if you want to wrap the word class, or strong or list, in this case you'll replace these too and mess it all up!
That's a bad way because you loose every listener, and impact on each tag, including which they don't need...
Thank you all for pointing this. One-line solution often come with big drawbacks.

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.