How to Wrap Part of a Text in a Node with JavaScript

How to wrap part of a text in a node with JavaScript

Here are two ways to deal with this.

I don't know if the following will exactly match your needs. It's a simple enough solution to the problem, but at least it doesn't use RegEx to manipulate HTML tags. It performs pattern matching against the raw text and then uses the DOM to manipulate the content.


First approach

This approach creates only one <span> tag per match, leveraging some less common browser APIs.

(See the main problem of this approach below the demo, and if not sure, use the second approach).

The Range class represents a text fragment. It has a surroundContents function that lets you wrap a range in an element. Except it has a caveat:

This method is nearly equivalent to newNode.appendChild(range.extractContents()); range.insertNode(newNode). After surrounding, the boundary points of the range include newNode.

An exception will be thrown, however, if the Range splits a non-Text node with only one of its boundary points. That is, unlike the alternative above, if there are partially selected nodes, they will not be cloned and instead the operation will fail.

Well, the workaround is provided in the MDN, so all's good.

So here's an algorithm:

  • Make a list of Text nodes and keep their start indices in the text
  • Concatenate these nodes' values to get the text
  • Find matches over the text, and for each match:

    • Find the start and end nodes of the match, comparing the the nodes' start indices to the match position
    • Create a Range over the match
    • Let the browser do the dirty work using the trick above
    • Rebuild the node list since the last action changed the DOM

Here's my implementation with a demo:

function highlight(element, regex) {    var document = element.ownerDocument;        var getNodes = function() {        var nodes = [],            offset = 0,            node,            nodeIterator = document.createNodeIterator(element, NodeFilter.SHOW_TEXT, null, false);                    while (node = nodeIterator.nextNode()) {            nodes.push({                textNode: node,                start: offset,                length: node.nodeValue.length            });            offset += node.nodeValue.length        }        return nodes;    }        var nodes = getNodes(nodes);    if (!nodes.length)        return;        var text = "";    for (var i = 0; i < nodes.length; ++i)        text += nodes[i].textNode.nodeValue;
var match; while (match = regex.exec(text)) { // Prevent empty matches causing infinite loops if (!match[0].length) { regex.lastIndex++; continue; } // Find the start and end text node var startNode = null, endNode = null; for (i = 0; i < nodes.length; ++i) { var node = nodes[i]; if (node.start + node.length <= match.index) continue; if (!startNode) startNode = node; if (node.start + node.length >= match.index + match[0].length) { endNode = node; break; } } var range = document.createRange(); range.setStart(startNode.textNode, match.index - startNode.start); range.setEnd(endNode.textNode, match.index + match[0].length - endNode.start); var spanNode = document.createElement("span"); spanNode.className = "highlight";
spanNode.appendChild(range.extractContents()); range.insertNode(spanNode); nodes = getNodes(); }}
// Test codevar testDiv = document.getElementById("test-cases");var originalHtml = testDiv.innerHTML;function test() { testDiv.innerHTML = originalHtml; try { var regex = new RegExp(document.getElementById("regex").value, "g"); highlight(testDiv, regex); } catch(e) { testDiv.innerText = e; }}document.getElementById("runBtn").onclick = test;test();
.highlight {  background-color: yellow;  border: 1px solid orange;  border-radius: 5px;}
.section { border: 1px solid gray; padding: 10px; margin: 10px;}
<form class="section">  RegEx: <input id="regex" type="text" value="[A-Z].*?\." /> <button id="runBtn">Highlight</button></form>
<div id="test-cases" class="section"> <div>foo bar baz</div> <p> <b>HTML</b> is a language used to make <b>websites.</b> It was developed by <i>CERN</i> employees in the early 90s. <p> <p> This program is <a href="beta.html">not stable yet. Do not use this in production yet.</a> </p> <div>foo bar baz</div></div>

How to wrap part of the text in a node that has 'display: flex and justify-content: space-between' styling?

Flex is always responsible for self- and child-layouts. You have to add a wrapper around the child-nodes (even if they are text-nodes), if you don't want them to be affected by the parents flex-settings.

The specification describes a CSS box model optimized for user interface design. In the flex layout model, the children of a flex container can be laid out in any direction, and can “flex” their sizes, either growing to fill unused space or shrinking to avoid overflowing the parent. Both horizontal and vertical alignment of the children can be easily manipulated. Nesting of these boxes (horizontal inside vertical, or vertical inside horizontal) can be used to build layouts in two dimensions.

Source: https://drafts.csswg.org/css-flexbox-1/

The browser marks the highlight from the search (CTRL+F) on a completely different level and does not inject html code.

That what you want to do, can be solved with the Selection API, but keep in mind, that only Firefox supports multiple selection-ranges at once.

Check out the example here: https://developer.mozilla.org/en-US/docs/Web/API/Selection/addRange#Result

  • Firefox: https://i.imgur.com/DnhZJ5y.png
  • Webkit Browsers: https://i.imgur.com/sfKriEl.png

How to wrap part of all text_node nodeValue in an html element?

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>

How do to wrap a span around a section of text without using jQuery

First, you need some way of accessing the paragraph. You might want to give it an id attribute, such as "foo":

<p id="foo">Lorem Ipsum <a href="#">Link</a> <div ... </div> </p>

Then, you can use document.getElementById to access that element and replace its children as required:

var p = document.getElementById('foo'),
firstTextNode = p.firstChild,
newSpan = document.createElement('span');

// Append "Lorem Ipsum" text to new span:
newSpan.appendChild( document.createTextNode(firstTextNode.nodeValue) );

// Replace old text node with new span:
p.replaceChild( newSpan, firstTextNode );

To make it more reliable, you might want to call p.normalize() before accessing the first child, to ensure that all text nodes before the anchor are merged as one.


Oook, So you want to replace a part of a text node with an element. Here's how I'd do it:

function giveMeDOM(html) {

var div = document.createElement('div'),
frag = document.createDocumentFragment();

div.innerHTML = html;

while (div.firstChild) {
frag.appendChild( div.firstChild );
}

return frag;
}

var p = document.getElementById('foo'),
firstChild = p.firstChild;

// Merge adjacent text nodes:
p.normalize();

// Get new DOM structure:
var newStructure = giveMeDOM( firstChild.nodeValue.replace(/Lorem Ipsum/i, '<span>$&</span>') );

// Replace first child with new DOM structure:
p.replaceChild( newStructure, firstChild );

Working with nodes at the low level is a bit of a nasty situation to be in; especially without any abstraction to help you out. I've tried to retain a sense of normality by creating a DOM node out of an HTML string produced from the replaced "Lorem Ipsum" phrase. Purists probably don't like this solution, but I find it perfectly suitable.


EDIT: Now using a document fragment! Thanks Crescent Fresh!

How to wrap text inside multiple nodes with a html tag

This is not simple nor elegant but works as expected, without additional markup and is the best I can think of.

Basically, you have to traverse the dom tree instersecting with the selection range, and collect sub ranges only composed of text node during traversal.

Look into Range documentation for information on startContainer and endContainer.
To put it simply they are the same when selecting into a single text node, and they give you the starting and ending point of your traversal otherwise.

Once you have collected those ranges, you can wrap them in the tag of your liking.

It works pretty well but unfortunately I wasn't able to preserve the initial selection after bolding (tried everything with selection.setRange(..) with no luck):

document.getElementById("bold").onclick = function() {    var selection = document.getSelection(),    range = selection.getRangeAt(0).cloneRange();    // start and end are always text nodes    var start = range.startContainer;    var end = range.endContainer;    var ranges = [];
// if start === end then it's fine we have selected a portion of a text node while (start !== end) {
var startText = start.nodeValue; var currentRange = range.cloneRange(); // pin the range at the end of this text node currentRange.setEnd(start, startText.length); // keep the range for later ranges.push(currentRange);
var sibling = start; do { if (sibling.hasChildNodes()) { // if it has children then it's not a text node, go deeper sibling = sibling.firstChild; } else if (sibling.nextSibling) { // it has a sibling, go for it sibling = sibling.nextSibling; } else { // we're into a corner, we have to go up one level and go for next sibling while (!sibling.nextSibling && sibling.parentNode) { sibling = sibling.parentNode; } if (sibling) { sibling = sibling.nextSibling; } } } while (sibling !== null && sibling.nodeValue === null); if (!sibling) { // out of nodes! break; } // move range start to the identified next text node (sibling) range.setStart(sibling, 0); start = range.startContainer; } // surround all collected range by the b tag for (var i = 0; i < ranges.length; i++) { var currentRange = ranges[i]; currentRange.surroundContents(document.createElement("b")); } // surround the remaining range by a b tag range.surroundContents(document.createElement("b"));
// unselect everything because I can't presere the original selection selection.removeAllRanges();
}
<div contenteditable="true" id="div">This is the editor. If you embolden only **this**, it will work. If you try <font color="red">to embolden **this <i>and this**</i>, it will work <font color="green">because</font> we are traversing the</font> nodes<table rules="all"><tr><td>it</td><td>will<td>even</td><td>work</td></tr><tr><td>in</td><td>more</td><td>complicated</td><td><i>markup</i></td></tr></table></div><button id="bold">Bold</button>

What is a wrap method to wrap only a part of a string?

One way to do it would be - instead of wrap, replace the content of the h3 with the slice up manipulated version.

See this JSFiddle http://jsfiddle.net/9LeL3f3n/21/

<div>
<h3>How are you? Fine?</h3>
</div>

$(document).ready(function() {

var highlight = function(str, start, end) {
return str.slice(0,start-1) +
'<span style="color:#ffff00">' +
str.substring(start-1, end) +
'</span>' +
str.slice(-1 * (str.length - end));
};

var n = 5;
var m = 12;

$('h3').html(highlight($('h3').html(),n,m));

});

Javascript wrap all text nodes in tags

It seems to me, that all you need to do is switch around these two lines:

element.appendChild(curNode);
target.insertBefore(element, curNode);

so it becomes:

target.insertBefore(element, curNode);
element.appendChild(curNode);

Because right now, you append the curNode to the new div, before placing the div in the correct position. This results in:

<div>"hey"</div>

That's all great, but then in order to put the div on the right place, you can not place it before "hey" anymore, since you just put that inside the div.

By swapping the two lines, you first place the div in front of the "hey":

<div></div>"hey"

And then you position "hey" correctly inside the div:

<div>"hey"</div>

I hope this helps!

Wrap part of text into new element tag

Use jquery's .contents() to get the contents of your <span>. Then filter to only text nodes (nodeType of 3). Then loop through each, and wrap it with an <a> tag if it is not empty. (If you don't perform the trim and empty check, you'll end up with <a> tags for any newline characters inside your span).

See the working example below.

$(document).ready(function() {  var count = 0;  $('#my_span').contents()    .filter(function() {      return this.nodeType === 3    })    .each(function() {      if ($.trim($(this).text()).length > 0) {        $(this).wrap('<a href id="added_a_tag_' + (++count) + '"></a>');      }    });});
<script src="https://ajax.googleapis.com/ajax/libs/jquery/2.1.1/jquery.min.js"></script><span id="my_span">  John<input type="hidden" name="firstname" value="John" id="my_input1">  <br>  Smith<input type="hidden" name="lastname" value="Smith" id="my_input2"></span>

To wrap a part of innerHTML with new element

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">


Related Topics



Leave a reply



Submit