Created
July 14, 2020 11:46
-
-
Save shash7/7abe3b9756804640b756268d12d656c8 to your computer and use it in GitHub Desktop.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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