blob: 5bb521d586b87b8cfa8315334e3e46ebe7081022 [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");
element.addEventListener("click", WI.domTreeManager.inspectElement.bind(WI.domTreeManager, node.id));
element.addEventListener("mouseover", WI.domTreeManager.highlightDOMNode.bind(WI.domTreeManager, node.id, "all"));
element.addEventListener("mouseout", WI.domTreeManager.hideDOMNodeHighlight.bind(WI.domTreeManager));
element.addEventListener("contextmenu", (event) => {
let contextMenu = WI.ContextMenu.createFromEvent(event);
WI.appendContextMenuItemsForDOMNode(contextMenu, node, options);
});
return element;
};
function createSVGElement(tagName)
{
return document.createElementNS("http://www.w3.org/2000/svg", tagName);
}
WI.cssPath = function(node)
{
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);
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)
{
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();
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};
// Root node does not have siblings.
if (!node.parentNode || node.parentNode.nodeType() === Node.DOCUMENT_NODE)
return {value: nodeName, 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;
};