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 6 * 7 * Redistribution and use in source and binary forms, with or without 8 * modification, are permitted provided that the following conditions 9 * are met: 10 * 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. 19 * 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. 30 */ 31 32WebInspector.DOMPresentationUtils = {} 33 34WebInspector.DOMPresentationUtils.decorateNodeLabel = function(node, parentElement) 35{ 36 var title = node.nodeNameInCorrectCase(); 37 38 var nameElement = document.createElement("span"); 39 nameElement.textContent = title; 40 parentElement.appendChild(nameElement); 41 42 var idAttribute = node.getAttribute("id"); 43 if (idAttribute) { 44 var idElement = document.createElement("span"); 45 parentElement.appendChild(idElement); 46 47 var part = "#" + idAttribute; 48 title += part; 49 idElement.appendChild(document.createTextNode(part)); 50 51 // Mark the name as extra, since the ID is more important. 52 nameElement.className = "extra"; 53 } 54 55 var classAttribute = node.getAttribute("class"); 56 if (classAttribute) { 57 var classes = classAttribute.split(/\s+/); 58 var foundClasses = {}; 59 60 if (classes.length) { 61 var classesElement = document.createElement("span"); 62 classesElement.className = "extra"; 63 parentElement.appendChild(classesElement); 64 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.appendChild(document.createTextNode(part)); 71 foundClasses[className] = true; 72 } 73 } 74 } 75 } 76 parentElement.title = title; 77} 78 79/** 80 * @param {!Element} container 81 * @param {string} nodeTitle 82 */ 83WebInspector.DOMPresentationUtils.createSpansForNodeTitle = function(container, nodeTitle) 84{ 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]; 91} 92 93WebInspector.DOMPresentationUtils.linkifyNodeReference = function(node) 94{ 95 var link = document.createElement("span"); 96 link.className = "node-link"; 97 WebInspector.DOMPresentationUtils.decorateNodeLabel(node, link); 98 99 link.addEventListener("click", WebInspector.domAgent.inspectElement.bind(WebInspector.domAgent, node.id), false); 100 link.addEventListener("mouseover", WebInspector.domAgent.highlightDOMNode.bind(WebInspector.domAgent, node.id, "", undefined), false); 101 link.addEventListener("mouseout", WebInspector.domAgent.hideDOMNodeHighlight.bind(WebInspector.domAgent), false); 102 103 return link; 104} 105 106WebInspector.DOMPresentationUtils.linkifyNodeById = function(nodeId) 107{ 108 var node = WebInspector.domAgent.nodeForId(nodeId); 109 if (!node) 110 return document.createTextNode(WebInspector.UIString("<node>")); 111 return WebInspector.DOMPresentationUtils.linkifyNodeReference(node); 112} 113 114/** 115 * @param {string} imageURL 116 * @param {boolean} showDimensions 117 * @param {function(!Element=)} userCallback 118 * @param {!Object=} precomputedDimensions 119 */ 120WebInspector.DOMPresentationUtils.buildImagePreviewContents = function(imageURL, showDimensions, userCallback, precomputedDimensions) 121{ 122 var resource = WebInspector.resourceTreeModel.resourceForURL(imageURL); 123 if (!resource) { 124 userCallback(); 125 return; 126 } 127 128 var imageElement = document.createElement("img"); 129 imageElement.addEventListener("load", buildContent, false); 130 imageElement.addEventListener("error", errorCallback, false); 131 resource.populateImageSource(imageElement); 132 133 function errorCallback() 134 { 135 // Drop the event parameter when invoking userCallback. 136 userCallback(); 137 } 138 139 function buildContent() 140 { 141 var container = document.createElement("table"); 142 container.className = "image-preview-container"; 143 var naturalWidth = precomputedDimensions ? precomputedDimensions.naturalWidth : imageElement.naturalWidth; 144 var naturalHeight = precomputedDimensions ? precomputedDimensions.naturalHeight : imageElement.naturalHeight; 145 var offsetWidth = precomputedDimensions ? precomputedDimensions.offsetWidth : naturalWidth; 146 var offsetHeight = precomputedDimensions ? precomputedDimensions.offsetHeight : naturalHeight; 147 var description; 148 if (showDimensions) { 149 if (offsetHeight === naturalHeight && offsetWidth === naturalWidth) 150 description = WebInspector.UIString("%d \xd7 %d pixels", offsetWidth, offsetHeight); 151 else 152 description = WebInspector.UIString("%d \xd7 %d pixels (Natural: %d \xd7 %d pixels)", offsetWidth, offsetHeight, naturalWidth, naturalHeight); 153 } 154 155 container.createChild("tr").createChild("td", "image-container").appendChild(imageElement); 156 if (description) 157 container.createChild("tr").createChild("td").createChild("span", "description").textContent = description; 158 userCallback(container); 159 } 160} 161 162/** 163 * @param {!WebInspector.DOMNode} node 164 * @param {boolean=} justSelector 165 * @return {string} 166 */ 167WebInspector.DOMPresentationUtils.appropriateSelectorFor = function(node, justSelector) 168{ 169 var lowerCaseName = node.localName() || node.nodeName().toLowerCase(); 170 if (node.nodeType() !== Node.ELEMENT_NODE) 171 return lowerCaseName; 172 if (lowerCaseName === "input" && node.getAttribute("type") && !node.getAttribute("id") && !node.getAttribute("class")) 173 return lowerCaseName + "[type=\"" + node.getAttribute("type") + "\"]"; 174 175 return WebInspector.DOMPresentationUtils.cssPath(node, justSelector); 176} 177 178/** 179 * @param {!WebInspector.DOMNode} node 180 * @param {boolean=} optimized 181 * @return {string} 182 */ 183WebInspector.DOMPresentationUtils.cssPath = function(node, optimized) 184{ 185 if (node.nodeType() !== Node.ELEMENT_NODE) 186 return ""; 187 188 var steps = []; 189 var contextNode = node; 190 while (contextNode) { 191 var step = WebInspector.DOMPresentationUtils._cssPathValue(contextNode, optimized); 192 if (!step) 193 break; // Error - bail out early. 194 steps.push(step); 195 if (step.optimized) 196 break; 197 contextNode = contextNode.parentNode; 198 } 199 200 steps.reverse(); 201 return steps.join(" > "); 202} 203 204/** 205 * @param {!WebInspector.DOMNode} node 206 * @param {boolean=} optimized 207 * @return {?WebInspector.DOMNodePathStep} 208 */ 209WebInspector.DOMPresentationUtils._cssPathValue = function(node, optimized) 210{ 211 if (node.nodeType() !== Node.ELEMENT_NODE) 212 return null; 213 214 var id = node.getAttribute("id"); 215 if (optimized) { 216 if (id) 217 return new WebInspector.DOMNodePathStep(idSelector(id), true); 218 var nodeNameLower = node.nodeName().toLowerCase(); 219 if (nodeNameLower === "body" || nodeNameLower === "head" || nodeNameLower === "html") 220 return new WebInspector.DOMNodePathStep(node.nodeNameInCorrectCase(), true); 221 } 222 var nodeName = node.nodeNameInCorrectCase(); 223 224 if (id) 225 return new WebInspector.DOMNodePathStep(nodeName + idSelector(id), true); 226 var parent = node.parentNode; 227 if (!parent || parent.nodeType() === Node.DOCUMENT_NODE) 228 return new WebInspector.DOMNodePathStep(nodeName, true); 229 230 /** 231 * @param {!WebInspector.DOMNode} node 232 * @return {!Array.<string>} 233 */ 234 function prefixedElementClassNames(node) 235 { 236 var classAttribute = node.getAttribute("class"); 237 if (!classAttribute) 238 return []; 239 240 return classAttribute.split(/\s+/g).filter(Boolean).map(function(name) { 241 // The prefix is required to store "__proto__" in a object-based map. 242 return "$" + name; 243 }); 244 } 245 246 /** 247 * @param {string} id 248 * @return {string} 249 */ 250 function idSelector(id) 251 { 252 return "#" + escapeIdentifierIfNeeded(id); 253 } 254 255 /** 256 * @param {string} ident 257 * @return {string} 258 */ 259 function escapeIdentifierIfNeeded(ident) 260 { 261 if (isCSSIdentifier(ident)) 262 return ident; 263 var shouldEscapeFirst = /^(?:[0-9]|-[0-9-]?)/.test(ident); 264 var lastIndex = ident.length - 1; 265 return ident.replace(/./g, function(c, i) { 266 return ((shouldEscapeFirst && i === 0) || !isCSSIdentChar(c)) ? escapeAsciiChar(c, i === lastIndex) : c; 267 }); 268 } 269 270 /** 271 * @param {string} c 272 * @param {boolean} isLast 273 * @return {string} 274 */ 275 function escapeAsciiChar(c, isLast) 276 { 277 return "\\" + toHexByte(c) + (isLast ? "" : " "); 278 } 279 280 /** 281 * @param {string} c 282 */ 283 function toHexByte(c) 284 { 285 var hexByte = c.charCodeAt(0).toString(16); 286 if (hexByte.length === 1) 287 hexByte = "0" + hexByte; 288 return hexByte; 289 } 290 291 /** 292 * @param {string} c 293 * @return {boolean} 294 */ 295 function isCSSIdentChar(c) 296 { 297 if (/[a-zA-Z0-9_-]/.test(c)) 298 return true; 299 return c.charCodeAt(0) >= 0xA0; 300 } 301 302 /** 303 * @param {string} value 304 * @return {boolean} 305 */ 306 function isCSSIdentifier(value) 307 { 308 return /^-?[a-zA-Z_][a-zA-Z0-9_-]*$/.test(value); 309 } 310 311 var prefixedOwnClassNamesArray = prefixedElementClassNames(node); 312 var needsClassNames = false; 313 var needsNthChild = false; 314 var ownIndex = -1; 315 var siblings = parent.children(); 316 for (var i = 0; (ownIndex === -1 || !needsNthChild) && i < siblings.length; ++i) { 317 var sibling = siblings[i]; 318 if (sibling === node) { 319 ownIndex = i; 320 continue; 321 } 322 if (needsNthChild) 323 continue; 324 if (sibling.nodeNameInCorrectCase() !== nodeName) 325 continue; 326 327 needsClassNames = true; 328 var ownClassNames = prefixedOwnClassNamesArray.keySet(); 329 var ownClassNameCount = 0; 330 for (var name in ownClassNames) 331 ++ownClassNameCount; 332 if (ownClassNameCount === 0) { 333 needsNthChild = true; 334 continue; 335 } 336 var siblingClassNamesArray = prefixedElementClassNames(sibling); 337 for (var j = 0; j < siblingClassNamesArray.length; ++j) { 338 var siblingClass = siblingClassNamesArray[j]; 339 if (!ownClassNames.hasOwnProperty(siblingClass)) 340 continue; 341 delete ownClassNames[siblingClass]; 342 if (!--ownClassNameCount) { 343 needsNthChild = true; 344 break; 345 } 346 } 347 } 348 349 var result = nodeName; 350 if (needsNthChild) { 351 result += ":nth-child(" + (ownIndex + 1) + ")"; 352 } else if (needsClassNames) { 353 for (var prefixedName in prefixedOwnClassNamesArray.keySet()) 354 result += "." + escapeIdentifierIfNeeded(prefixedName.substr(1)); 355 } 356 357 return new WebInspector.DOMNodePathStep(result, false); 358} 359 360/** 361 * @param {!WebInspector.DOMNode} node 362 * @param {boolean=} optimized 363 * @return {string} 364 */ 365WebInspector.DOMPresentationUtils.xPath = function(node, optimized) 366{ 367 if (node.nodeType() === Node.DOCUMENT_NODE) 368 return "/"; 369 370 var steps = []; 371 var contextNode = node; 372 while (contextNode) { 373 var step = WebInspector.DOMPresentationUtils._xPathValue(contextNode, optimized); 374 if (!step) 375 break; // Error - bail out early. 376 steps.push(step); 377 if (step.optimized) 378 break; 379 contextNode = contextNode.parentNode; 380 } 381 382 steps.reverse(); 383 return (steps.length && steps[0].optimized ? "" : "/") + steps.join("/"); 384} 385 386/** 387 * @param {!WebInspector.DOMNode} node 388 * @param {boolean=} optimized 389 * @return {?WebInspector.DOMNodePathStep} 390 */ 391WebInspector.DOMPresentationUtils._xPathValue = function(node, optimized) 392{ 393 var ownValue; 394 var ownIndex = WebInspector.DOMPresentationUtils._xPathIndex(node); 395 if (ownIndex === -1) 396 return null; // Error. 397 398 switch (node.nodeType()) { 399 case Node.ELEMENT_NODE: 400 if (optimized && node.getAttribute("id")) 401 return new WebInspector.DOMNodePathStep("//*[@id=\"" + node.getAttribute("id") + "\"]", true); 402 ownValue = node.localName(); 403 break; 404 case Node.ATTRIBUTE_NODE: 405 ownValue = "@" + node.nodeName(); 406 break; 407 case Node.TEXT_NODE: 408 case Node.CDATA_SECTION_NODE: 409 ownValue = "text()"; 410 break; 411 case Node.PROCESSING_INSTRUCTION_NODE: 412 ownValue = "processing-instruction()"; 413 break; 414 case Node.COMMENT_NODE: 415 ownValue = "comment()"; 416 break; 417 case Node.DOCUMENT_NODE: 418 ownValue = ""; 419 break; 420 default: 421 ownValue = ""; 422 break; 423 } 424 425 if (ownIndex > 0) 426 ownValue += "[" + ownIndex + "]"; 427 428 return new WebInspector.DOMNodePathStep(ownValue, node.nodeType() === Node.DOCUMENT_NODE); 429}, 430 431/** 432 * @param {!WebInspector.DOMNode} node 433 * @return {number} 434 */ 435WebInspector.DOMPresentationUtils._xPathIndex = function(node) 436{ 437 // Returns -1 in case of error, 0 if no siblings matching the same expression, <XPath index among the same expression-matching sibling nodes> otherwise. 438 function areNodesSimilar(left, right) 439 { 440 if (left === right) 441 return true; 442 443 if (left.nodeType() === Node.ELEMENT_NODE && right.nodeType() === Node.ELEMENT_NODE) 444 return left.localName() === right.localName(); 445 446 if (left.nodeType() === right.nodeType()) 447 return true; 448 449 // XPath treats CDATA as text nodes. 450 var leftType = left.nodeType() === Node.CDATA_SECTION_NODE ? Node.TEXT_NODE : left.nodeType(); 451 var rightType = right.nodeType() === Node.CDATA_SECTION_NODE ? Node.TEXT_NODE : right.nodeType(); 452 return leftType === rightType; 453 } 454 455 var siblings = node.parentNode ? node.parentNode.children() : null; 456 if (!siblings) 457 return 0; // Root node - no siblings. 458 var hasSameNamedElements; 459 for (var i = 0; i < siblings.length; ++i) { 460 if (areNodesSimilar(node, siblings[i]) && siblings[i] !== node) { 461 hasSameNamedElements = true; 462 break; 463 } 464 } 465 if (!hasSameNamedElements) 466 return 0; 467 var ownIndex = 1; // XPath indices start with 1. 468 for (var i = 0; i < siblings.length; ++i) { 469 if (areNodesSimilar(node, siblings[i])) { 470 if (siblings[i] === node) 471 return ownIndex; 472 ++ownIndex; 473 } 474 } 475 return -1; // An error occurred: |node| not found in parent's children. 476} 477 478/** 479 * @constructor 480 * @param {string} value 481 * @param {boolean} optimized 482 */ 483WebInspector.DOMNodePathStep = function(value, optimized) 484{ 485 this.value = value; 486 this.optimized = optimized || false; 487} 488 489WebInspector.DOMNodePathStep.prototype = { 490 /** 491 * @return {string} 492 */ 493 toString: function() 494 { 495 return this.value; 496 } 497} 498