зеркало из https://github.com/mozilla/gecko-dev.git
328 строки
9.0 KiB
JavaScript
328 строки
9.0 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("/") : "";
|
|
}
|