Created
July 14, 2020 11:46
Revisions
-
shash7 created this gist
Jul 14, 2020 .There are no files selected for viewing
This file contains hidden or 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 charactersOriginal file line number Diff line number Diff line change @@ -0,0 +1,214 @@ 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]; }