Skip to content

Instantly share code, notes, and snippets.

@ephys
Created May 6, 2016 00:43
Show Gist options
  • Save ephys/db6b7bca4978e2dbccbcbb5a31ba116e to your computer and use it in GitHub Desktop.
Save ephys/db6b7bca4978e2dbccbcbb5a31ba116e to your computer and use it in GitHub Desktop.
Creates a Range around a piece of text in a DOM tree.
(function () {
'use strict';
/*
Result:
========= NEW TEST =========
VM1096:14 Before wrap: <p><strong>Hello</strong> <em>World</em></p>
VM1096:19 Wrapping: Hello World
VM1096:25 After wrap: <p><section><strong>Hello</strong> <em>World</em></section></p>
VM1096:13 ========= NEW TEST =========
VM1096:14 Before wrap: Hello<br> <em>World</em>
VM1096:19 Wrapping: Hello World
VM1096:25 After wrap: <section>Hello<br> <em>World</em></section>
VM1096:13 ========= NEW TEST =========
VM1096:14 Before wrap: <p> Hello World </p>
VM1096:19 Wrapping: Hello World
VM1096:25 After wrap: <p> <section>Hello World</section> </p>
VM1096:13 ========= NEW TEST =========
VM1096:14 Before wrap: <p>Hello World</p>
VM1096:19 Wrapping: Hello World
VM1096:25 After wrap: <p><section>Hello World</section></p>
*/
const tests = [
['<p><strong>Hello</strong> <em>World</em></p>', 'Hello World'],
['Hello<br> <em>World</em>', 'Hello World'],
['<p> Hello World </p>', 'Hello World'],
['<p>Hello World</p>', 'Hello World']
];
tests.forEach(([html, needle]) => {
try {
console.log('========= NEW TEST =========');
console.log('Before wrap:', html);
const test = document.createElement('div');
test.innerHTML = html;
console.log('Wrapping:', needle);
const range = find(test, needle);
const wrapper = document.createElement('section');
range.surroundContents(wrapper);
console.log('After wrap:', test.innerHTML);
} catch (e) {
console.error(e);
}
});
/**
* Creates a range that wraps the part of the node matching str, ignoring tags.
*
* @param {!Node} node The node containing the text to wrap.
* @param {!String} str The string to wrap.
* @return {Range} The range or null if the text is not part of the node.
*/
function find(node, str) {
// TODO startAt parameter
const strPos = node.textContent.indexOf(str);
if (strPos === -1) {
return null;
}
node.normalize();
// Get the text node that contains the start of [str]
const startPos = { pos: strPos };
let startNode = _findPos(node, startPos); // pos is in an object so it can be modified.
if (!startNode) {
return null;
}
// Get the text node that contains the end of [str]
const endPos = { pos: strPos + str.length };
let endNode = _findPos(node, endPos);
if (!endNode) {
return null;
}
if (startNode !== endNode) {
// edge case, for <div>matched text</div>, the range should start before <div>, not before "matched text"
if (startNode.nextSibling == null && startNode.previousSibling == null && startNode.length <= str.length) {
startNode = startNode.parentNode;
if (!startNode.previousSibling) {
startNode.parentNode.insertBefore(document.createTextNode(''), startNode);
}
startNode = startNode.previousSibling;
startNode.pos = 0;
}
// edge case, for <div>matched text</div>, the range should end after </div>, not after "matched text"
if (endNode.nextSibling == null && endNode.previousSibling == null && endNode.length === endPos.pos) {
endNode = endNode.parentNode;
if (!endNode.nextSibling) {
endNode.parentNode.appendChild(document.createTextNode(''));
}
endNode = endNode.nextSibling;
endPos.pos = 0;
}
}
const range = document.createRange();
range.setStart(startNode, startPos.pos);
range.setEnd(endNode, endPos.pos);
return range;
}
/**
* Returns the text node which is part of {node} and contains the character with index {args.pos} if {node.textContent}.
* Returns Null if {node.textContent.length < args.pos}.
* Note: args.pos will be set to the offset of the start of the string inside the text node.
*/
function _findPos(node, args) {
const pos = args.pos;
if (pos < 0) {
return null;
}
// Text node
if (node.nodeType === 3) {
if (node.length >= args.pos) {
return node;
} else {
args.pos -= node.length;
}
}
// character order in .textContent is generated In-Order, so are we: Children first, starting from left.
if (node.firstChild) {
const result = _findPos(node.firstChild, args);
if (result) {
return result;
}
}
if (node.nextSibling) {
const result = _findPos(node.nextSibling, args);
if (result) {
return result;
}
}
return null;
}
})();
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment