blob: 249322e17edb25b0c990b3725a3f2b954792e64f [file] [log] [blame]
/*
* 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.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 ((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);
});
}
element.addEventListener("mouseover", (event) => {
WI.domManager.highlightDOMNode(node.id, "all");
});
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;
};