Merge Chromium + Blink git repositories
[chromium-blink-merge.git] / third_party / WebKit / Source / devtools / front_end / components / DOMPresentationUtils.js
blob1d2d126f210c54e76cf29fbf8bad995c92f0fb5e
1 /*
2 * Copyright (C) 2011 Google Inc. All rights reserved.
3 * Copyright (C) 2007, 2008 Apple Inc. All rights reserved.
4 * Copyright (C) 2008 Matt Lilek <webkit@mattlilek.com>
5 * Copyright (C) 2009 Joseph Pecoraro
7 * Redistribution and use in source and binary forms, with or without
8 * modification, are permitted provided that the following conditions
9 * are met:
11 * 1. Redistributions of source code must retain the above copyright
12 * notice, this list of conditions and the following disclaimer.
13 * 2. Redistributions in binary form must reproduce the above copyright
14 * notice, this list of conditions and the following disclaimer in the
15 * documentation and/or other materials provided with the distribution.
16 * 3. Neither the name of Apple Computer, Inc. ("Apple") nor the names of
17 * its contributors may be used to endorse or promote products derived
18 * from this software without specific prior written permission.
20 * THIS SOFTWARE IS PROVIDED BY APPLE AND ITS CONTRIBUTORS "AS IS" AND ANY
21 * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
22 * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
23 * DISCLAIMED. IN NO EVENT SHALL APPLE OR ITS CONTRIBUTORS BE LIABLE FOR ANY
24 * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
25 * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
26 * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
27 * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
28 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF
29 * THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
32 WebInspector.DOMPresentationUtils = {}
34 WebInspector.DOMPresentationUtils.decorateNodeLabel = function(node, parentElement)
36 var title = node.nodeNameInCorrectCase();
38 var nameElement = createElement("span");
39 nameElement.textContent = title;
40 parentElement.appendChild(nameElement);
42 var idAttribute = node.getAttribute("id");
43 if (idAttribute) {
44 var idElement = createElement("span");
45 parentElement.appendChild(idElement);
47 var part = "#" + idAttribute;
48 title += part;
49 idElement.createTextChild(part);
51 // Mark the name as extra, since the ID is more important.
52 nameElement.className = "extra";
55 var classAttribute = node.getAttribute("class");
56 if (classAttribute) {
57 var classes = classAttribute.split(/\s+/);
58 var foundClasses = {};
60 if (classes.length) {
61 var classesElement = createElement("span");
62 classesElement.className = "extra";
63 parentElement.appendChild(classesElement);
65 for (var i = 0; i < classes.length; ++i) {
66 var className = classes[i];
67 if (className && !(className in foundClasses)) {
68 var part = "." + className;
69 title += part;
70 classesElement.createTextChild(part);
71 foundClasses[className] = true;
76 parentElement.title = title;
79 /**
80 * @param {!Element} container
81 * @param {string} nodeTitle
83 WebInspector.DOMPresentationUtils.createSpansForNodeTitle = function(container, nodeTitle)
85 var match = nodeTitle.match(/([^#.]+)(#[^.]+)?(\..*)?/);
86 container.createChild("span", "webkit-html-tag-name").textContent = match[1];
87 if (match[2])
88 container.createChild("span", "webkit-html-attribute-value").textContent = match[2];
89 if (match[3])
90 container.createChild("span", "webkit-html-attribute-name").textContent = match[3];
93 /**
94 * @param {?WebInspector.DOMNode} node
95 * @return {!Node}
97 WebInspector.DOMPresentationUtils.linkifyNodeReference = function(node)
99 if (!node)
100 return createTextNode(WebInspector.UIString("<node>"));
102 var root = createElement("span");
103 var shadowRoot = WebInspector.createShadowRootWithCoreStyles(root);
104 shadowRoot.appendChild(WebInspector.Widget.createStyleElement("components/domUtils.css"));
105 var link = shadowRoot.createChild("div", "node-link");
107 WebInspector.DOMPresentationUtils.decorateNodeLabel(node, link);
109 link.addEventListener("click", WebInspector.Revealer.reveal.bind(WebInspector.Revealer, node, undefined), false);
110 link.addEventListener("mouseover", node.highlight.bind(node, undefined, undefined), false);
111 link.addEventListener("mouseleave", WebInspector.DOMModel.hideDOMNodeHighlight.bind(WebInspector.DOMModel), false);
113 return root;
117 * @param {!WebInspector.DeferredDOMNode} deferredNode
118 * @return {!Node}
120 WebInspector.DOMPresentationUtils.linkifyDeferredNodeReference = function(deferredNode)
122 var root = createElement("div");
123 var shadowRoot = WebInspector.createShadowRootWithCoreStyles(root);
124 shadowRoot.appendChild(WebInspector.Widget.createStyleElement("components/domUtils.css"));
125 var link = shadowRoot.createChild("div", "node-link");
126 link.createChild("content");
127 link.addEventListener("click", deferredNode.resolve.bind(deferredNode, onDeferredNodeResolved), false);
128 link.addEventListener("mousedown", consumeEvent, false);
131 * @param {?WebInspector.DOMNode} node
133 function onDeferredNodeResolved(node)
135 WebInspector.Revealer.reveal(node);
138 return root;
142 * @param {!WebInspector.Target} target
143 * @param {string} originalImageURL
144 * @param {boolean} showDimensions
145 * @param {function(!Element=)} userCallback
146 * @param {!Object=} precomputedFeatures
148 WebInspector.DOMPresentationUtils.buildImagePreviewContents = function(target, originalImageURL, showDimensions, userCallback, precomputedFeatures)
150 var resource = target.resourceTreeModel.resourceForURL(originalImageURL);
151 var imageURL = originalImageURL;
152 if (!isImageResource(resource) && precomputedFeatures && precomputedFeatures.currentSrc) {
153 imageURL = precomputedFeatures.currentSrc;
154 resource = target.resourceTreeModel.resourceForURL(imageURL);
156 if (!isImageResource(resource)) {
157 userCallback();
158 return;
161 var imageElement = createElement("img");
162 imageElement.addEventListener("load", buildContent, false);
163 imageElement.addEventListener("error", errorCallback, false);
164 resource.populateImageSource(imageElement);
166 function errorCallback()
168 // Drop the event parameter when invoking userCallback.
169 userCallback();
173 * @param {?WebInspector.Resource} resource
174 * @return {boolean}
176 function isImageResource(resource)
178 return !!resource && resource.resourceType() === WebInspector.resourceTypes.Image;
181 function buildContent()
183 var container = createElement("table");
184 container.className = "image-preview-container";
185 var naturalWidth = precomputedFeatures ? precomputedFeatures.naturalWidth : imageElement.naturalWidth;
186 var naturalHeight = precomputedFeatures ? precomputedFeatures.naturalHeight : imageElement.naturalHeight;
187 var offsetWidth = precomputedFeatures ? precomputedFeatures.offsetWidth : naturalWidth;
188 var offsetHeight = precomputedFeatures ? precomputedFeatures.offsetHeight : naturalHeight;
189 var description;
190 if (showDimensions) {
191 if (offsetHeight === naturalHeight && offsetWidth === naturalWidth)
192 description = WebInspector.UIString("%d \xd7 %d pixels", offsetWidth, offsetHeight);
193 else
194 description = WebInspector.UIString("%d \xd7 %d pixels (Natural: %d \xd7 %d pixels)", offsetWidth, offsetHeight, naturalWidth, naturalHeight);
197 container.createChild("tr").createChild("td", "image-container").appendChild(imageElement);
198 if (description)
199 container.createChild("tr").createChild("td").createChild("span", "description").textContent = description;
200 if (imageURL !== originalImageURL)
201 container.createChild("tr").createChild("td").createChild("span", "description").textContent = String.sprintf("currentSrc: %s", imageURL.trimMiddle(100));
202 userCallback(container);
207 * @param {!WebInspector.Target} target
208 * @param {!WebInspector.Linkifier} linkifier
209 * @param {!Array.<!ConsoleAgent.CallFrame>=} stackTrace
210 * @param {!ConsoleAgent.AsyncStackTrace=} asyncStackTrace
211 * @return {!Element}
213 WebInspector.DOMPresentationUtils.buildStackTracePreviewContents = function(target, linkifier, stackTrace, asyncStackTrace)
215 var element = createElement("span");
216 element.style.display = "inline-block";
217 var shadowRoot = WebInspector.createShadowRootWithCoreStyles(element);
219 shadowRoot.appendChild(WebInspector.Widget.createStyleElement("components/domUtils.css"));
220 var contentElement = shadowRoot.createChild("table", "stack-preview-container");
223 * @param {!Array.<!ConsoleAgent.CallFrame>} stackTrace
225 function appendStackTrace(stackTrace)
227 for (var stackFrame of stackTrace) {
228 var row = createElement("tr");
229 row.createChild("td").textContent = WebInspector.beautifyFunctionName(stackFrame.functionName);
230 row.createChild("td").textContent = " @ ";
231 row.createChild("td").appendChild(linkifier.linkifyConsoleCallFrame(target, stackFrame));
232 contentElement.appendChild(row);
236 if (stackTrace)
237 appendStackTrace(stackTrace);
239 while (asyncStackTrace) {
240 var callFrames = asyncStackTrace.callFrames;
241 if (!callFrames || !callFrames.length)
242 break;
243 var row = contentElement.createChild("tr");
244 row.createChild("td", "stack-preview-async-description").textContent = WebInspector.asyncStackTraceLabel(asyncStackTrace.description);
245 row.createChild("td");
246 row.createChild("td");
247 appendStackTrace(callFrames);
248 asyncStackTrace = asyncStackTrace.asyncStackTrace;
251 return element;
255 * @param {!WebInspector.DOMNode} node
256 * @param {boolean=} justSelector
257 * @return {string}
259 WebInspector.DOMPresentationUtils.fullQualifiedSelector = function(node, justSelector)
261 if (node.nodeType() !== Node.ELEMENT_NODE)
262 return node.localName() || node.nodeName().toLowerCase();
263 return WebInspector.DOMPresentationUtils.cssPath(node, justSelector);
267 * @param {!WebInspector.DOMNode} node
268 * @return {string}
270 WebInspector.DOMPresentationUtils.simpleSelector = function(node)
272 var lowerCaseName = node.localName() || node.nodeName().toLowerCase();
273 if (node.nodeType() !== Node.ELEMENT_NODE)
274 return lowerCaseName;
275 if (lowerCaseName === "input" && node.getAttribute("type") && !node.getAttribute("id") && !node.getAttribute("class"))
276 return lowerCaseName + "[type=\"" + node.getAttribute("type") + "\"]";
277 if (node.getAttribute("id"))
278 return lowerCaseName + "#" + node.getAttribute("id");
279 if (node.getAttribute("class"))
280 return (lowerCaseName === "div" ? "" : lowerCaseName) + "." + node.getAttribute("class").trim().replace(/\s+/g, ".");
281 return lowerCaseName;
285 * @param {!WebInspector.DOMNode} node
286 * @param {boolean=} optimized
287 * @return {string}
289 WebInspector.DOMPresentationUtils.cssPath = function(node, optimized)
291 if (node.nodeType() !== Node.ELEMENT_NODE)
292 return "";
294 var steps = [];
295 var contextNode = node;
296 while (contextNode) {
297 var step = WebInspector.DOMPresentationUtils._cssPathStep(contextNode, !!optimized, contextNode === node);
298 if (!step)
299 break; // Error - bail out early.
300 steps.push(step);
301 if (step.optimized)
302 break;
303 contextNode = contextNode.parentNode;
306 steps.reverse();
307 return steps.join(" > ");
311 * @param {!WebInspector.DOMNode} node
312 * @param {boolean} optimized
313 * @param {boolean} isTargetNode
314 * @return {?WebInspector.DOMNodePathStep}
316 WebInspector.DOMPresentationUtils._cssPathStep = function(node, optimized, isTargetNode)
318 if (node.nodeType() !== Node.ELEMENT_NODE)
319 return null;
321 var id = node.getAttribute("id");
322 if (optimized) {
323 if (id)
324 return new WebInspector.DOMNodePathStep(idSelector(id), true);
325 var nodeNameLower = node.nodeName().toLowerCase();
326 if (nodeNameLower === "body" || nodeNameLower === "head" || nodeNameLower === "html")
327 return new WebInspector.DOMNodePathStep(node.nodeNameInCorrectCase(), true);
329 var nodeName = node.nodeNameInCorrectCase();
331 if (id)
332 return new WebInspector.DOMNodePathStep(nodeName + idSelector(id), true);
333 var parent = node.parentNode;
334 if (!parent || parent.nodeType() === Node.DOCUMENT_NODE)
335 return new WebInspector.DOMNodePathStep(nodeName, true);
338 * @param {!WebInspector.DOMNode} node
339 * @return {!Array.<string>}
341 function prefixedElementClassNames(node)
343 var classAttribute = node.getAttribute("class");
344 if (!classAttribute)
345 return [];
347 return classAttribute.split(/\s+/g).filter(Boolean).map(function(name) {
348 // The prefix is required to store "__proto__" in a object-based map.
349 return "$" + name;
354 * @param {string} id
355 * @return {string}
357 function idSelector(id)
359 return "#" + escapeIdentifierIfNeeded(id);
363 * @param {string} ident
364 * @return {string}
366 function escapeIdentifierIfNeeded(ident)
368 if (isCSSIdentifier(ident))
369 return ident;
370 var shouldEscapeFirst = /^(?:[0-9]|-[0-9-]?)/.test(ident);
371 var lastIndex = ident.length - 1;
372 return ident.replace(/./g, function(c, i) {
373 return ((shouldEscapeFirst && i === 0) || !isCSSIdentChar(c)) ? escapeAsciiChar(c, i === lastIndex) : c;
378 * @param {string} c
379 * @param {boolean} isLast
380 * @return {string}
382 function escapeAsciiChar(c, isLast)
384 return "\\" + toHexByte(c) + (isLast ? "" : " ");
388 * @param {string} c
390 function toHexByte(c)
392 var hexByte = c.charCodeAt(0).toString(16);
393 if (hexByte.length === 1)
394 hexByte = "0" + hexByte;
395 return hexByte;
399 * @param {string} c
400 * @return {boolean}
402 function isCSSIdentChar(c)
404 if (/[a-zA-Z0-9_-]/.test(c))
405 return true;
406 return c.charCodeAt(0) >= 0xA0;
410 * @param {string} value
411 * @return {boolean}
413 function isCSSIdentifier(value)
415 return /^-?[a-zA-Z_][a-zA-Z0-9_-]*$/.test(value);
418 var prefixedOwnClassNamesArray = prefixedElementClassNames(node);
419 var needsClassNames = false;
420 var needsNthChild = false;
421 var ownIndex = -1;
422 var elementIndex = -1;
423 var siblings = parent.children();
424 for (var i = 0; (ownIndex === -1 || !needsNthChild) && i < siblings.length; ++i) {
425 var sibling = siblings[i];
426 if (sibling.nodeType() !== Node.ELEMENT_NODE)
427 continue;
428 elementIndex += 1;
429 if (sibling === node) {
430 ownIndex = elementIndex;
431 continue;
433 if (needsNthChild)
434 continue;
435 if (sibling.nodeNameInCorrectCase() !== nodeName)
436 continue;
438 needsClassNames = true;
439 var ownClassNames = prefixedOwnClassNamesArray.keySet();
440 var ownClassNameCount = 0;
441 for (var name in ownClassNames)
442 ++ownClassNameCount;
443 if (ownClassNameCount === 0) {
444 needsNthChild = true;
445 continue;
447 var siblingClassNamesArray = prefixedElementClassNames(sibling);
448 for (var j = 0; j < siblingClassNamesArray.length; ++j) {
449 var siblingClass = siblingClassNamesArray[j];
450 if (!ownClassNames.hasOwnProperty(siblingClass))
451 continue;
452 delete ownClassNames[siblingClass];
453 if (!--ownClassNameCount) {
454 needsNthChild = true;
455 break;
460 var result = nodeName;
461 if (isTargetNode && nodeName.toLowerCase() === "input" && node.getAttribute("type") && !node.getAttribute("id") && !node.getAttribute("class"))
462 result += "[type=\"" + node.getAttribute("type") + "\"]";
463 if (needsNthChild) {
464 result += ":nth-child(" + (ownIndex + 1) + ")";
465 } else if (needsClassNames) {
466 for (var prefixedName in prefixedOwnClassNamesArray.keySet())
467 result += "." + escapeIdentifierIfNeeded(prefixedName.substr(1));
470 return new WebInspector.DOMNodePathStep(result, false);
474 * @param {!WebInspector.DOMNode} node
475 * @param {boolean=} optimized
476 * @return {string}
478 WebInspector.DOMPresentationUtils.xPath = function(node, optimized)
480 if (node.nodeType() === Node.DOCUMENT_NODE)
481 return "/";
483 var steps = [];
484 var contextNode = node;
485 while (contextNode) {
486 var step = WebInspector.DOMPresentationUtils._xPathValue(contextNode, optimized);
487 if (!step)
488 break; // Error - bail out early.
489 steps.push(step);
490 if (step.optimized)
491 break;
492 contextNode = contextNode.parentNode;
495 steps.reverse();
496 return (steps.length && steps[0].optimized ? "" : "/") + steps.join("/");
500 * @param {!WebInspector.DOMNode} node
501 * @param {boolean=} optimized
502 * @return {?WebInspector.DOMNodePathStep}
504 WebInspector.DOMPresentationUtils._xPathValue = function(node, optimized)
506 var ownValue;
507 var ownIndex = WebInspector.DOMPresentationUtils._xPathIndex(node);
508 if (ownIndex === -1)
509 return null; // Error.
511 switch (node.nodeType()) {
512 case Node.ELEMENT_NODE:
513 if (optimized && node.getAttribute("id"))
514 return new WebInspector.DOMNodePathStep("//*[@id=\"" + node.getAttribute("id") + "\"]", true);
515 ownValue = node.localName();
516 break;
517 case Node.ATTRIBUTE_NODE:
518 ownValue = "@" + node.nodeName();
519 break;
520 case Node.TEXT_NODE:
521 case Node.CDATA_SECTION_NODE:
522 ownValue = "text()";
523 break;
524 case Node.PROCESSING_INSTRUCTION_NODE:
525 ownValue = "processing-instruction()";
526 break;
527 case Node.COMMENT_NODE:
528 ownValue = "comment()";
529 break;
530 case Node.DOCUMENT_NODE:
531 ownValue = "";
532 break;
533 default:
534 ownValue = "";
535 break;
538 if (ownIndex > 0)
539 ownValue += "[" + ownIndex + "]";
541 return new WebInspector.DOMNodePathStep(ownValue, node.nodeType() === Node.DOCUMENT_NODE);
545 * @param {!WebInspector.DOMNode} node
546 * @return {number}
548 WebInspector.DOMPresentationUtils._xPathIndex = function(node)
550 // Returns -1 in case of error, 0 if no siblings matching the same expression, <XPath index among the same expression-matching sibling nodes> otherwise.
551 function areNodesSimilar(left, right)
553 if (left === right)
554 return true;
556 if (left.nodeType() === Node.ELEMENT_NODE && right.nodeType() === Node.ELEMENT_NODE)
557 return left.localName() === right.localName();
559 if (left.nodeType() === right.nodeType())
560 return true;
562 // XPath treats CDATA as text nodes.
563 var leftType = left.nodeType() === Node.CDATA_SECTION_NODE ? Node.TEXT_NODE : left.nodeType();
564 var rightType = right.nodeType() === Node.CDATA_SECTION_NODE ? Node.TEXT_NODE : right.nodeType();
565 return leftType === rightType;
568 var siblings = node.parentNode ? node.parentNode.children() : null;
569 if (!siblings)
570 return 0; // Root node - no siblings.
571 var hasSameNamedElements;
572 for (var i = 0; i < siblings.length; ++i) {
573 if (areNodesSimilar(node, siblings[i]) && siblings[i] !== node) {
574 hasSameNamedElements = true;
575 break;
578 if (!hasSameNamedElements)
579 return 0;
580 var ownIndex = 1; // XPath indices start with 1.
581 for (var i = 0; i < siblings.length; ++i) {
582 if (areNodesSimilar(node, siblings[i])) {
583 if (siblings[i] === node)
584 return ownIndex;
585 ++ownIndex;
588 return -1; // An error occurred: |node| not found in parent's children.
592 * @constructor
593 * @param {string} value
594 * @param {boolean} optimized
596 WebInspector.DOMNodePathStep = function(value, optimized)
598 this.value = value;
599 this.optimized = optimized || false;
602 WebInspector.DOMNodePathStep.prototype = {
604 * @override
605 * @return {string}
607 toString: function()
609 return this.value;
614 * @interface
616 WebInspector.DOMPresentationUtils.MarkerDecorator = function()
620 WebInspector.DOMPresentationUtils.MarkerDecorator.prototype = {
622 * @param {!WebInspector.DOMNode} node
623 * @return {?{title: string, color: string}}
625 decorate: function(node) { }