forked from mirrors/gecko-dev
		
	 b503616295
			
		
	
	
		b503616295
		
	
	
	
	
		
			
			# ignore-this-changeset Differential Revision: https://phabricator.services.mozilla.com/D36056 --HG-- extra : source : 2616392f26053ee376b9126fbca696de5d4bb15b
		
			
				
	
	
		
			327 lines
		
	
	
	
		
			9 KiB
		
	
	
	
		
			JavaScript
		
	
	
	
	
	
			
		
		
	
	
			327 lines
		
	
	
	
		
			9 KiB
		
	
	
	
		
			JavaScript
		
	
	
	
	
	
| /* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
 | |
| /* vim: set ts=2 et sw=2 tw=80: */
 | |
| /* This Source Code Form is subject to the terms of the Mozilla Public
 | |
|  * License, v. 2.0. If a copy of the MPL was not distributed with this
 | |
|  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
 | |
| 
 | |
| "use strict";
 | |
| 
 | |
| var EXPORTED_SYMBOLS = [
 | |
|   "findAllCssSelectors",
 | |
|   "findCssSelector",
 | |
|   "getCssPath",
 | |
|   "getXPath",
 | |
| ];
 | |
| 
 | |
| /**
 | |
|  * Traverse getBindingParent until arriving upon the bound element
 | |
|  * responsible for the generation of the specified node.
 | |
|  * See https://developer.mozilla.org/en-US/docs/XBL/XBL_1.0_Reference/DOM_Interfaces#getBindingParent.
 | |
|  *
 | |
|  * @param {DOMNode} node
 | |
|  * @return {DOMNode}
 | |
|  *         If node is not anonymous, this will return node. Otherwise,
 | |
|  *         it will return the bound element
 | |
|  *
 | |
|  */
 | |
| function getRootBindingParent(node) {
 | |
|   let doc = node.ownerDocument;
 | |
|   if (!doc) {
 | |
|     return node;
 | |
|   }
 | |
| 
 | |
|   let parent;
 | |
|   while ((parent = doc.getBindingParent(node))) {
 | |
|     node = parent;
 | |
|   }
 | |
|   return node;
 | |
| }
 | |
| 
 | |
| /**
 | |
|  * Return the node's parent shadow root if the node in shadow DOM, null
 | |
|  * otherwise.
 | |
|  */
 | |
| function getShadowRoot(node) {
 | |
|   let doc = node.ownerDocument;
 | |
|   if (!doc) {
 | |
|     return null;
 | |
|   }
 | |
| 
 | |
|   const parent = doc.getBindingParent(node);
 | |
|   const shadowRoot = parent && parent.openOrClosedShadowRoot;
 | |
|   if (shadowRoot) {
 | |
|     return shadowRoot;
 | |
|   }
 | |
| 
 | |
|   return null;
 | |
| }
 | |
| 
 | |
| /**
 | |
|  * Find the position of [element] in [nodeList].
 | |
|  * @returns an index of the match, or -1 if there is no match
 | |
|  */
 | |
| function positionInNodeList(element, nodeList) {
 | |
|   for (let i = 0; i < nodeList.length; i++) {
 | |
|     if (element === nodeList[i]) {
 | |
|       return i;
 | |
|     }
 | |
|   }
 | |
|   return -1;
 | |
| }
 | |
| 
 | |
| /**
 | |
|  * For a provided node, find the appropriate container/node couple so that
 | |
|  * container.contains(node) and a CSS selector can be created from the
 | |
|  * container to the node.
 | |
|  */
 | |
| function findNodeAndContainer(node) {
 | |
|   const shadowRoot = getShadowRoot(node);
 | |
|   if (shadowRoot) {
 | |
|     // If the node is under a shadow root, the shadowRoot contains the node and
 | |
|     // we can find the node via shadowRoot.querySelector(path).
 | |
|     return {
 | |
|       containingDocOrShadow: shadowRoot,
 | |
|       node,
 | |
|     };
 | |
|   }
 | |
| 
 | |
|   // Otherwise, get the root binding parent to get a non anonymous element that
 | |
|   // will be accessible from the ownerDocument.
 | |
|   const bindingParent = getRootBindingParent(node);
 | |
|   return {
 | |
|     containingDocOrShadow: bindingParent.ownerDocument,
 | |
|     node: bindingParent,
 | |
|   };
 | |
| }
 | |
| 
 | |
| /**
 | |
|  * Find a unique CSS selector for a given element
 | |
|  * @returns a string such that:
 | |
|  *   - ele.containingDocOrShadow.querySelector(reply) === ele
 | |
|  *   - ele.containingDocOrShadow.querySelectorAll(reply).length === 1
 | |
|  */
 | |
| const findCssSelector = function(ele) {
 | |
|   const { node, containingDocOrShadow } = findNodeAndContainer(ele);
 | |
|   ele = node;
 | |
| 
 | |
|   if (!containingDocOrShadow || !containingDocOrShadow.contains(ele)) {
 | |
|     // findCssSelector received element not inside container.
 | |
|     return "";
 | |
|   }
 | |
| 
 | |
|   let cssEscape = ele.ownerGlobal.CSS.escape;
 | |
| 
 | |
|   // document.querySelectorAll("#id") returns multiple if elements share an ID
 | |
|   if (
 | |
|     ele.id &&
 | |
|     containingDocOrShadow.querySelectorAll("#" + cssEscape(ele.id)).length === 1
 | |
|   ) {
 | |
|     return "#" + cssEscape(ele.id);
 | |
|   }
 | |
| 
 | |
|   // Inherently unique by tag name
 | |
|   let tagName = ele.localName;
 | |
|   if (tagName === "html") {
 | |
|     return "html";
 | |
|   }
 | |
|   if (tagName === "head") {
 | |
|     return "head";
 | |
|   }
 | |
|   if (tagName === "body") {
 | |
|     return "body";
 | |
|   }
 | |
| 
 | |
|   // We might be able to find a unique class name
 | |
|   let selector, index, matches;
 | |
|   for (let i = 0; i < ele.classList.length; i++) {
 | |
|     // Is this className unique by itself?
 | |
|     selector = "." + cssEscape(ele.classList.item(i));
 | |
|     matches = containingDocOrShadow.querySelectorAll(selector);
 | |
|     if (matches.length === 1) {
 | |
|       return selector;
 | |
|     }
 | |
|     // Maybe it's unique with a tag name?
 | |
|     selector = cssEscape(tagName) + selector;
 | |
|     matches = containingDocOrShadow.querySelectorAll(selector);
 | |
|     if (matches.length === 1) {
 | |
|       return selector;
 | |
|     }
 | |
|     // Maybe it's unique using a tag name and nth-child
 | |
|     index = positionInNodeList(ele, ele.parentNode.children) + 1;
 | |
|     selector = selector + ":nth-child(" + index + ")";
 | |
|     matches = containingDocOrShadow.querySelectorAll(selector);
 | |
|     if (matches.length === 1) {
 | |
|       return selector;
 | |
|     }
 | |
|   }
 | |
| 
 | |
|   // Not unique enough yet.
 | |
|   index = positionInNodeList(ele, ele.parentNode.children) + 1;
 | |
|   selector = cssEscape(tagName) + ":nth-child(" + index + ")";
 | |
|   if (ele.parentNode !== containingDocOrShadow) {
 | |
|     selector = findCssSelector(ele.parentNode) + " > " + selector;
 | |
|   }
 | |
|   return selector;
 | |
| };
 | |
| 
 | |
| /**
 | |
|  * If the element is in a frame or under a shadowRoot, return the corresponding
 | |
|  * element.
 | |
|  */
 | |
| function getSelectorParent(node) {
 | |
|   const shadowRoot = getShadowRoot(node);
 | |
|   if (shadowRoot) {
 | |
|     // The element is in a shadowRoot, return the host component.
 | |
|     return shadowRoot.host;
 | |
|   }
 | |
| 
 | |
|   // Otherwise return the parent frameElement.
 | |
|   return node.ownerGlobal.frameElement;
 | |
| }
 | |
| 
 | |
| /**
 | |
|  * Retrieve the array of CSS selectors corresponding to the provided node.
 | |
|  *
 | |
|  * The selectors are ordered starting with the root document and ending with the deepest
 | |
|  * nested frame. Additional items are used if the node is inside a frame or a shadow root,
 | |
|  * each representing the CSS selector for finding the frame or root element in its parent
 | |
|  * document.
 | |
|  *
 | |
|  * This format is expected by DevTools in order to handle the Inspect Node context menu
 | |
|  * item.
 | |
|  *
 | |
|  * @param  {node}
 | |
|  *         The node for which the CSS selectors should be computed
 | |
|  * @return {Array}
 | |
|  *         An array of CSS selectors to find the target node. Several selectors can be
 | |
|  *         needed if the element is nested in frames and not directly in the root
 | |
|  *         document. The selectors are ordered starting with the root document and
 | |
|  *         ending with the deepest nested frame or shadow root.
 | |
|  */
 | |
| const findAllCssSelectors = function(node) {
 | |
|   let selectors = [];
 | |
|   while (node) {
 | |
|     selectors.unshift(findCssSelector(node));
 | |
|     node = getSelectorParent(node);
 | |
|   }
 | |
| 
 | |
|   return selectors;
 | |
| };
 | |
| 
 | |
| /**
 | |
|  * Get the full CSS path for a given element.
 | |
|  * @returns a string that can be used as a CSS selector for the element. It might not
 | |
|  * match the element uniquely. It does however, represent the full path from the root
 | |
|  * node to the element.
 | |
|  */
 | |
| function getCssPath(ele) {
 | |
|   const { node, containingDocOrShadow } = findNodeAndContainer(ele);
 | |
|   ele = node;
 | |
|   if (!containingDocOrShadow || !containingDocOrShadow.contains(ele)) {
 | |
|     // getCssPath received element not inside container.
 | |
|     return "";
 | |
|   }
 | |
| 
 | |
|   const nodeGlobal = ele.ownerGlobal.Node;
 | |
| 
 | |
|   const getElementSelector = element => {
 | |
|     if (!element.localName) {
 | |
|       return "";
 | |
|     }
 | |
| 
 | |
|     let label =
 | |
|       element.nodeName == element.nodeName.toUpperCase()
 | |
|         ? element.localName.toLowerCase()
 | |
|         : element.localName;
 | |
| 
 | |
|     if (element.id) {
 | |
|       label += "#" + element.id;
 | |
|     }
 | |
| 
 | |
|     if (element.classList) {
 | |
|       for (const cl of element.classList) {
 | |
|         label += "." + cl;
 | |
|       }
 | |
|     }
 | |
| 
 | |
|     return label;
 | |
|   };
 | |
| 
 | |
|   const paths = [];
 | |
| 
 | |
|   while (ele) {
 | |
|     if (!ele || ele.nodeType !== nodeGlobal.ELEMENT_NODE) {
 | |
|       break;
 | |
|     }
 | |
| 
 | |
|     paths.splice(0, 0, getElementSelector(ele));
 | |
|     ele = ele.parentNode;
 | |
|   }
 | |
| 
 | |
|   return paths.length ? paths.join(" ") : "";
 | |
| }
 | |
| 
 | |
| /**
 | |
|  * Get the xpath for a given element.
 | |
|  * @param {DomNode} ele
 | |
|  * @returns a string that can be used as an XPath to find the element uniquely.
 | |
|  */
 | |
| function getXPath(ele) {
 | |
|   const { node, containingDocOrShadow } = findNodeAndContainer(ele);
 | |
|   ele = node;
 | |
|   if (!containingDocOrShadow || !containingDocOrShadow.contains(ele)) {
 | |
|     // getXPath received element not inside container.
 | |
|     return "";
 | |
|   }
 | |
| 
 | |
|   // Create a short XPath for elements with IDs.
 | |
|   if (ele.id) {
 | |
|     return `//*[@id="${ele.id}"]`;
 | |
|   }
 | |
| 
 | |
|   // Otherwise walk the DOM up and create a part for each ancestor.
 | |
|   const parts = [];
 | |
| 
 | |
|   const nodeGlobal = ele.ownerGlobal.Node;
 | |
|   // Use nodeName (instead of localName) so namespace prefix is included (if any).
 | |
|   while (ele && ele.nodeType === nodeGlobal.ELEMENT_NODE) {
 | |
|     let nbOfPreviousSiblings = 0;
 | |
|     let hasNextSiblings = false;
 | |
| 
 | |
|     // Count how many previous same-name siblings the element has.
 | |
|     let sibling = ele.previousSibling;
 | |
|     while (sibling) {
 | |
|       // Ignore document type declaration.
 | |
|       if (
 | |
|         sibling.nodeType !== nodeGlobal.DOCUMENT_TYPE_NODE &&
 | |
|         sibling.nodeName == ele.nodeName
 | |
|       ) {
 | |
|         nbOfPreviousSiblings++;
 | |
|       }
 | |
| 
 | |
|       sibling = sibling.previousSibling;
 | |
|     }
 | |
| 
 | |
|     // Check if the element has at least 1 next same-name sibling.
 | |
|     sibling = ele.nextSibling;
 | |
|     while (sibling) {
 | |
|       if (sibling.nodeName == ele.nodeName) {
 | |
|         hasNextSiblings = true;
 | |
|         break;
 | |
|       }
 | |
|       sibling = sibling.nextSibling;
 | |
|     }
 | |
| 
 | |
|     const prefix = ele.prefix ? ele.prefix + ":" : "";
 | |
|     const nth =
 | |
|       nbOfPreviousSiblings || hasNextSiblings
 | |
|         ? `[${nbOfPreviousSiblings + 1}]`
 | |
|         : "";
 | |
| 
 | |
|     parts.push(prefix + ele.localName + nth);
 | |
| 
 | |
|     ele = ele.parentNode;
 | |
|   }
 | |
| 
 | |
|   return parts.length ? "/" + parts.reverse().join("/") : "";
 | |
| }
 |