414 lines
13 KiB
JavaScript
414 lines
13 KiB
JavaScript
/*
|
|
* Copyright (C) 2011 Google Inc. All rights reserved.
|
|
* Copyright (C) 2007, 2008, 2013 Apple Inc. All rights reserved.
|
|
* Copyright (C) 2008 Matt Lilek <webkit@mattlilek.com>
|
|
* Copyright (C) 2009 Joseph Pecoraro
|
|
*
|
|
* Redistribution and use in source and binary forms, with or without
|
|
* modification, are permitted provided that the following conditions
|
|
* are met:
|
|
*
|
|
* 1. Redistributions of source code must retain the above copyright
|
|
* notice, this list of conditions and the following disclaimer.
|
|
* 2. Redistributions in binary form must reproduce the above copyright
|
|
* notice, this list of conditions and the following disclaimer in the
|
|
* documentation and/or other materials provided with the distribution.
|
|
* 3. Neither the name of Apple Inc. ("Apple") nor the names of
|
|
* its contributors may be used to endorse or promote products derived
|
|
* from this software without specific prior written permission.
|
|
*
|
|
* THIS SOFTWARE IS PROVIDED BY APPLE AND ITS CONTRIBUTORS "AS IS" AND ANY
|
|
* EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
|
|
* WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
|
|
* DISCLAIMED. IN NO EVENT SHALL APPLE OR ITS CONTRIBUTORS BE LIABLE FOR ANY
|
|
* DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
|
|
* (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
|
|
* LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
|
|
* ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
|
|
* (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF
|
|
* THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
|
*/
|
|
|
|
WI.roleSelectorForNode = function(node)
|
|
{
|
|
// This is proposed syntax for CSS 4 computed role selector :role(foo) and subject to change.
|
|
// See http://lists.w3.org/Archives/Public/www-style/2013Jul/0104.html
|
|
var title = "";
|
|
var role = node.computedRole();
|
|
if (role)
|
|
title = ":role(" + role + ")";
|
|
return title;
|
|
};
|
|
|
|
WI.linkifyAccessibilityNodeReference = function(node)
|
|
{
|
|
if (!node)
|
|
return null;
|
|
// Same as linkifyNodeReference except the link text has the classnames removed...
|
|
// ...for list brevity, and both text and title have roleSelectorForNode appended.
|
|
var link = WI.linkifyNodeReference(node);
|
|
var tagIdSelector = link.title;
|
|
var classSelectorIndex = tagIdSelector.indexOf(".");
|
|
if (classSelectorIndex > -1)
|
|
tagIdSelector = tagIdSelector.substring(0, classSelectorIndex);
|
|
var roleSelector = WI.roleSelectorForNode(node);
|
|
link.textContent = tagIdSelector + roleSelector;
|
|
link.title += roleSelector;
|
|
return link;
|
|
};
|
|
|
|
WI.linkifyStyleable = function(styleable)
|
|
{
|
|
console.assert(styleable instanceof WI.DOMStyleable, styleable);
|
|
let displayName = styleable.displayName;
|
|
let link = document.createElement("span");
|
|
link.append(displayName);
|
|
return WI.linkifyNodeReferenceElement(styleable.node, link, {displayName});
|
|
};
|
|
|
|
WI.linkifyNodeReference = function(node, options = {})
|
|
{
|
|
let displayName = node.displayName;
|
|
if (!isNaN(options.maxLength))
|
|
displayName = displayName.truncate(options.maxLength);
|
|
|
|
let link = document.createElement("span");
|
|
link.append(displayName);
|
|
return WI.linkifyNodeReferenceElement(node, link, {...options, displayName});
|
|
};
|
|
|
|
WI.linkifyNodeReferenceElement = function(node, element, options = {})
|
|
{
|
|
element.setAttribute("role", "link");
|
|
element.title = options.displayName || node.displayName;
|
|
|
|
let nodeType = node.nodeType();
|
|
if (!options.ignoreClick && (nodeType !== Node.DOCUMENT_NODE || node.parentNode) && nodeType !== Node.TEXT_NODE)
|
|
element.classList.add("node-link");
|
|
|
|
WI.bindInteractionsForNodeToElement(node, element, options);
|
|
|
|
return element;
|
|
};
|
|
|
|
WI.bindInteractionsForNodeToElement = function(node, element, options = {}) {
|
|
if (!options.ignoreClick) {
|
|
element.addEventListener("click", (event) => {
|
|
WI.domManager.inspectElement(node.id, {
|
|
initiatorHint: WI.TabBrowser.TabNavigationInitiator.LinkClick,
|
|
});
|
|
});
|
|
}
|
|
|
|
element.addEventListener("mouseover", (event) => {
|
|
node.highlight();
|
|
});
|
|
|
|
element.addEventListener("mouseout", (event) => {
|
|
WI.domManager.hideDOMNodeHighlight();
|
|
});
|
|
|
|
element.addEventListener("contextmenu", (event) => {
|
|
let contextMenu = WI.ContextMenu.createFromEvent(event);
|
|
WI.appendContextMenuItemsForDOMNode(contextMenu, node, options);
|
|
});
|
|
};
|
|
|
|
function createSVGElement(tagName)
|
|
{
|
|
return document.createElementNS("http://www.w3.org/2000/svg", tagName);
|
|
}
|
|
|
|
WI.cssPath = function(node, options = {})
|
|
{
|
|
console.assert(node instanceof WI.DOMNode, "Expected a DOMNode.");
|
|
if (node.nodeType() !== Node.ELEMENT_NODE)
|
|
return "";
|
|
|
|
let suffix = "";
|
|
if (node.isPseudoElement()) {
|
|
suffix = "::" + node.pseudoType();
|
|
node = node.parentNode;
|
|
}
|
|
|
|
let components = [];
|
|
while (node) {
|
|
let component = WI.cssPathComponent(node, options);
|
|
if (!component)
|
|
break;
|
|
components.push(component);
|
|
if (component.done)
|
|
break;
|
|
node = node.parentNode;
|
|
}
|
|
|
|
components.reverse();
|
|
return components.map((x) => x.value).join(" > ") + suffix;
|
|
};
|
|
|
|
WI.cssPathComponent = function(node, options = {})
|
|
{
|
|
console.assert(node instanceof WI.DOMNode, "Expected a DOMNode.");
|
|
console.assert(!node.isPseudoElement());
|
|
if (node.nodeType() !== Node.ELEMENT_NODE)
|
|
return null;
|
|
|
|
let nodeName = node.nodeNameInCorrectCase();
|
|
|
|
// Root node does not have siblings.
|
|
if (!node.parentNode || node.parentNode.nodeType() === Node.DOCUMENT_NODE)
|
|
return {value: nodeName, done: true};
|
|
|
|
if (options.full) {
|
|
function getUniqueAttributes(domNode) {
|
|
let uniqueAttributes = new Map;
|
|
for (let attribute of domNode.attributes()) {
|
|
let values = [attribute.value];
|
|
if (attribute.name === "id" || attribute.name === "class")
|
|
values = attribute.value.split(/\s+/);
|
|
uniqueAttributes.set(attribute.name, new Set(values));
|
|
}
|
|
return uniqueAttributes;
|
|
}
|
|
|
|
let nodeIndex = 0;
|
|
let needsNthChild = false;
|
|
let uniqueAttributes = getUniqueAttributes(node);
|
|
node.parentNode.children.forEach((child, i) => {
|
|
if (child.nodeType() !== Node.ELEMENT_NODE)
|
|
return;
|
|
|
|
if (child === node) {
|
|
nodeIndex = i;
|
|
return;
|
|
}
|
|
|
|
if (needsNthChild || child.nodeNameInCorrectCase() !== nodeName)
|
|
return;
|
|
|
|
let childUniqueAttributes = getUniqueAttributes(child);
|
|
let subsetCount = 0;
|
|
for (let [name, values] of uniqueAttributes) {
|
|
let childValues = childUniqueAttributes.get(name);
|
|
if (childValues && values.size <= childValues.size && values.isSubsetOf(childValues))
|
|
++subsetCount;
|
|
}
|
|
|
|
if (subsetCount === uniqueAttributes.size)
|
|
needsNthChild = true;
|
|
});
|
|
|
|
function selectorForAttribute(values, prefix = "", shouldCSSEscape = false) {
|
|
if (!values || !values.size)
|
|
return "";
|
|
values = Array.from(values);
|
|
values = values.filter((value) => value && value.length);
|
|
if (!values.length)
|
|
return "";
|
|
values = values.map((value) => shouldCSSEscape ? CSS.escape(value) : value.escapeCharacters("\""));
|
|
return prefix + values.join(prefix);
|
|
}
|
|
|
|
let selector = nodeName;
|
|
selector += selectorForAttribute(uniqueAttributes.get("id"), "#", true);
|
|
selector += selectorForAttribute(uniqueAttributes.get("class"), ".", true);
|
|
for (let [attribute, values] of uniqueAttributes) {
|
|
if (attribute !== "id" && attribute !== "class")
|
|
selector += `[${attribute}="${selectorForAttribute(values)}"]`;
|
|
}
|
|
|
|
if (needsNthChild)
|
|
selector += `:nth-child(${nodeIndex + 1})`;
|
|
|
|
return {value: selector, done: false};
|
|
}
|
|
|
|
let lowerNodeName = node.nodeName().toLowerCase();
|
|
|
|
// html, head, and body are unique nodes.
|
|
if (lowerNodeName === "body" || lowerNodeName === "head" || lowerNodeName === "html")
|
|
return {value: nodeName, done: true};
|
|
|
|
// #id is unique.
|
|
let id = node.getAttribute("id");
|
|
if (id)
|
|
return {value: node.escapedIdSelector, done: true};
|
|
|
|
// Find uniqueness among siblings.
|
|
// - look for a unique className
|
|
// - look for a unique tagName
|
|
// - fallback to nth-child()
|
|
|
|
function classNames(node) {
|
|
let classAttribute = node.getAttribute("class");
|
|
return classAttribute ? classAttribute.trim().split(/\s+/) : [];
|
|
}
|
|
|
|
let nthChildIndex = -1;
|
|
let hasUniqueTagName = true;
|
|
let uniqueClasses = new Set(classNames(node));
|
|
|
|
let siblings = node.parentNode.children;
|
|
let elementIndex = 0;
|
|
for (let sibling of siblings) {
|
|
if (sibling.nodeType() !== Node.ELEMENT_NODE)
|
|
continue;
|
|
|
|
elementIndex++;
|
|
if (sibling === node) {
|
|
nthChildIndex = elementIndex;
|
|
continue;
|
|
}
|
|
|
|
if (sibling.nodeNameInCorrectCase() === nodeName)
|
|
hasUniqueTagName = false;
|
|
|
|
if (uniqueClasses.size) {
|
|
let siblingClassNames = classNames(sibling);
|
|
for (let className of siblingClassNames)
|
|
uniqueClasses.delete(className);
|
|
}
|
|
}
|
|
|
|
let selector = nodeName;
|
|
if (lowerNodeName === "input" && node.getAttribute("type") && !uniqueClasses.size)
|
|
selector += `[type="${node.getAttribute("type")}"]`;
|
|
if (!hasUniqueTagName) {
|
|
if (uniqueClasses.size)
|
|
selector += node.escapedClassSelector;
|
|
else
|
|
selector += `:nth-child(${nthChildIndex})`;
|
|
}
|
|
|
|
return {value: selector, done: false};
|
|
};
|
|
|
|
WI.xpath = function(node)
|
|
{
|
|
console.assert(node instanceof WI.DOMNode, "Expected a DOMNode.");
|
|
|
|
if (node.nodeType() === Node.DOCUMENT_NODE)
|
|
return "/";
|
|
|
|
let components = [];
|
|
while (node) {
|
|
let component = WI.xpathComponent(node);
|
|
if (!component)
|
|
break;
|
|
components.push(component);
|
|
if (component.done)
|
|
break;
|
|
node = node.parentNode;
|
|
}
|
|
|
|
components.reverse();
|
|
|
|
let prefix = components.length && components[0].done ? "" : "/";
|
|
return prefix + components.map((x) => x.value).join("/");
|
|
};
|
|
|
|
WI.xpathComponent = function(node)
|
|
{
|
|
console.assert(node instanceof WI.DOMNode, "Expected a DOMNode.");
|
|
|
|
let index = WI.xpathIndex(node);
|
|
if (index === -1)
|
|
return null;
|
|
|
|
let value;
|
|
|
|
switch (node.nodeType()) {
|
|
case Node.DOCUMENT_NODE:
|
|
return {value: "", done: true};
|
|
case Node.ELEMENT_NODE:
|
|
var id = node.getAttribute("id");
|
|
if (id)
|
|
return {value: `//*[@id="${id}"]`, done: true};
|
|
value = node.localName();
|
|
break;
|
|
case Node.ATTRIBUTE_NODE:
|
|
value = `@${node.nodeName()}`;
|
|
break;
|
|
case Node.TEXT_NODE:
|
|
case Node.CDATA_SECTION_NODE:
|
|
value = "text()";
|
|
break;
|
|
case Node.COMMENT_NODE:
|
|
value = "comment()";
|
|
break;
|
|
case Node.PROCESSING_INSTRUCTION_NODE:
|
|
value = "processing-instruction()";
|
|
break;
|
|
default:
|
|
value = "";
|
|
break;
|
|
}
|
|
|
|
if (index > 0)
|
|
value += `[${index}]`;
|
|
|
|
return {value, done: false};
|
|
};
|
|
|
|
WI.xpathIndex = function(node)
|
|
{
|
|
// Root node.
|
|
if (!node.parentNode)
|
|
return 0;
|
|
|
|
// No siblings.
|
|
let siblings = node.parentNode.children;
|
|
if (siblings.length <= 1)
|
|
return 0;
|
|
|
|
// Find uniqueness among siblings.
|
|
// - look for a unique localName
|
|
// - fallback to index
|
|
|
|
function isSimiliarNode(a, b) {
|
|
if (a === b)
|
|
return true;
|
|
|
|
let aType = a.nodeType();
|
|
let bType = b.nodeType();
|
|
|
|
if (aType === Node.ELEMENT_NODE && bType === Node.ELEMENT_NODE)
|
|
return a.localName() === b.localName();
|
|
|
|
// XPath CDATA and text() are the same.
|
|
if (aType === Node.CDATA_SECTION_NODE)
|
|
return aType === Node.TEXT_NODE;
|
|
if (bType === Node.CDATA_SECTION_NODE)
|
|
return bType === Node.TEXT_NODE;
|
|
|
|
return aType === bType;
|
|
}
|
|
|
|
let unique = true;
|
|
let xPathIndex = -1;
|
|
|
|
let xPathIndexCounter = 1; // XPath indices start at 1.
|
|
for (let sibling of siblings) {
|
|
if (!isSimiliarNode(node, sibling))
|
|
continue;
|
|
|
|
if (node === sibling) {
|
|
xPathIndex = xPathIndexCounter;
|
|
if (!unique)
|
|
return xPathIndex;
|
|
} else {
|
|
unique = false;
|
|
if (xPathIndex !== -1)
|
|
return xPathIndex;
|
|
}
|
|
|
|
xPathIndexCounter++;
|
|
}
|
|
|
|
if (unique)
|
|
return 0;
|
|
|
|
console.assert(xPathIndex > 0, "Should have found the node.");
|
|
return xPathIndex;
|
|
};
|