Skip to content

Instantly share code, notes, and snippets.

@shash7
Created July 14, 2020 11:46
Show Gist options
  • Save shash7/7abe3b9756804640b756268d12d656c8 to your computer and use it in GitHub Desktop.
Save shash7/7abe3b9756804640b756268d12d656c8 to your computer and use it in GitHub Desktop.
class NodeUtils {
static path(node) {
let tests = []
// if the chain contains a non-(element|text) node type, we can go no further
for (;
node && (node.nodeType === Node.ELEMENT_NODE || node.nodeType === Node.TEXT_NODE);
node = node.parentNode) {
// node test predicates
let predicates = []
// format node test for current node
let test = (() => {
switch (node.nodeType) {
case Node.ELEMENT_NODE:
// naturally uppercase. I forget why I force it lower.
return node.nodeName.toLowerCase()
case Node.TEXT_NODE:
return 'text()'
default:
console.error(`invalid node type: ${node.nodeType}`)
}
})()
/**
Add a check here to see if id is valid */
if (node.nodeType === Node.ELEMENT_NODE && node.id.length > 0) {
// if the node is an element with a unique id within the *document*, it can become the root of the path,
// and since we're going from node to document root, we have all we need.
if (node.ownerDocument.querySelectorAll(`#${node.id}`).length === 1) {
// because the first item of the path array is prefixed with '/', this will become
// a double slash (select all elements). But as there's only one result, we can use [1]
// eg: //span[@id='something']/div[3]/text()
tests.unshift(`/${test}[@id="${node.id}"]`)
break
}
if (node.parentElement && !Array.prototype.slice
.call(node.parentElement.children)
.some(sibling => sibling !== node && sibling.id === node.id)) {
// There are multiple nodes with the same id, but if the node is an element with a unique id
// in the context of its parent element we can use the id for the node test
predicates.push(`@id="${node.id}"`)
}
}
if (predicates.length === 0) {
// Get node index by counting previous siblings of the same name & type
let index = 1
for (let sibling = node.previousSibling; sibling; sibling = sibling.previousSibling) {
// Skip DTD,
// Skip nodes of differing type AND name (tagName for elements, #text for text),
// as they are indexed by node type
if (sibling.nodeType === Node.DOCUMENT_TYPE_NODE ||
node.nodeType !== sibling.nodeType ||
sibling.nodeName !== node.nodeName) {
continue
}
index++
}
// nodes at index 1 (1-based) are implicitly selected
if (index > 1) {
predicates.push(`${index}`)
}
}
// format predicates
tests.unshift(test + predicates.map(p => `[${p}]`).join(''))
} // end for
// return empty path string if unable to create path
return tests.length === 0 ? "" : `/${tests.join('/')}`
}
}
/**
Utility library to serialize and deserialize DOM range api so we can save/export/import/ etc with it.
*/
exports = module.exports = {
serialize: function (range) {
return {
startContainerPath: NodeUtils.path(range.startContainer),
startOffset: range.startOffset,
endContainerPath: NodeUtils.path(range.endContainer),
endOffset: range.endOffset,
//collapsed: range.collapsed,
}
},
deserialize: function (object, document) {
document = document || window.document;
let endContainer, endOffset
const evaluator = new XPathEvaluator()
// must have legal start and end container nodes
const startContainer = evaluator.evaluate(
object.startContainerPath,
document.documentElement,
null,
XPathResult.FIRST_ORDERED_NODE_TYPE,
null
)
if (!startContainer.singleNodeValue) {
return null
}
if (object.collapsed || !object.endContainerPath) {
endContainer = startContainer
endOffset = object.startOffset
} else {
endContainer = evaluator.evaluate(
object.endContainerPath,
document.documentElement,
null,
XPathResult.FIRST_ORDERED_NODE_TYPE,
null
)
if (!endContainer.singleNodeValue) {
return null;
}
endOffset = object.endOffset;
}
// map to range object
const range = document.createRange()
range.setStart(startContainer.singleNodeValue, object.startOffset)
range.setEnd(endContainer.singleNodeValue, endOffset)
return range
}
// serialize: function (range) {
// var start = generate(range.startContainer);
// start.offset = range.startOffset;
// var end = generate(range.endContainer);
// end.offset = range.endOffset;
// return { start: start, end: end };
// },
// deserialize(result, document) {
// document = document || window.document;
// var range = document.createRange(),
// startNode = find(result.start),
// endNode = find(result.end);
// range.setStart(startNode, result.start.offset);
// range.setEnd(endNode, result.end.offset);
// return range;
// }
}
function childNodeIndexOf(parentNode, childNode) {
var childNodes = parentNode.childNodes;
for (var i = 0, l = childNodes.length; i < l; i++) {
if (childNodes[i] === childNode) { return i; }
}
}
function computedNthIndex(childElement) {
var childNodes = childElement.parentNode.childNodes,
tagName = childElement.tagName,
elementsWithSameTag = 0;
for (var i = 0, l = childNodes.length; i < l; i++) {
if (childNodes[i] === childElement) { return elementsWithSameTag + 1; }
if (childNodes[i].tagName === tagName) { elementsWithSameTag++; }
}
}
function generate(node) {
var textNodeIndex = childNodeIndexOf(node.parentNode, node),
currentNode = node,
tagNames = [];
while (currentNode) {
var tagName = currentNode.tagName;
if (tagName) {
var nthIndex = computedNthIndex(currentNode);
var selector = tagName;
if (nthIndex > 1) {
selector += ":nth-of-type(" + nthIndex + ")";
}
tagNames.push(selector);
}
currentNode = currentNode.parentNode;
}
return { selector: tagNames.reverse().join(" > ").toLowerCase(), childNodeIndex: textNodeIndex };
}
function find(result) {
var element = document.querySelector(result.selector);
if (!element) { throw new Error('Unable to find element with selector: ' + result.selector); }
return element.childNodes[result.childNodeIndex];
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment