gecko-dev/toolkit/modules/css-selector.js

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("/") : "";
}