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>
AlexB
  • 3,518
  • 4
  • 29
  • 46
  • 2
    you cant add markup to the value, you would have to append a new child to the p element. – thsorens Aug 06 '16 at 07:40
  • See http://stackoverflow.com/questions/6328718/wrapping-a-selected-text-node-with-span, http://stackoverflow.com/questions/4040495/dom-wrapping-a-substring-in-textnode-with-a-new-span-node, and many other questions. –  Aug 06 '16 at 08:16
  • They went even shorter on this thread : http://stackoverflow.com/questions/1144783/replacing-all-occurrences-of-a-string-in-javascript – technico Aug 06 '16 at 09: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! – Mi-Creativity Aug 06 '16 at 09:58

3 Answers3

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>
Hitmands
  • 13,491
  • 4
  • 34
  • 69
  • would it work for multi-level nested elements, an HTML structure like in this https://jsfiddle.net/7pnn2fb7/ ?? – Mi-Creativity Aug 06 '16 at 10:40
  • Nice, here's a [codepen](http://codepen.io/Mi_Creativity/pen/rLqPRO) 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! – Mi-Creativity Aug 06 '16 at 11:52
  • yep, I know... by the way... my work can be executed pressing `run the code script` here... – Hitmands Aug 06 '16 at 11:56
  • 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 – Mi-Creativity Aug 06 '16 at 12:07
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>
Mi-Creativity
  • 9,554
  • 10
  • 38
  • 47
-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 !

Community
  • 1
  • 1
technico
  • 1,192
  • 1
  • 12
  • 22
  • 1
    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! – Mi-Creativity Aug 06 '16 at 09:56
  • 1
    That's a bad way because you loose every listener, and impact on each tag, including which they don't need... – Hitmands Aug 06 '16 at 10:38
  • Thank you all for pointing this. One-line solution often come with big drawbacks. – technico Aug 08 '16 at 20:57