Skip to content

Instantly share code, notes, and snippets.

@shash7
Created July 14, 2020 11:46

Revisions

  1. shash7 created this gist Jul 14, 2020.
    214 changes: 214 additions & 0 deletions range-utils.js
    Original 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];
    }