1/* 2 * Copyright (C) 2007, 2008 Apple Inc. All rights reserved. 3 * Copyright (C) 2008 Matt Lilek <webkit@mattlilek.com> 4 * Copyright (C) 2009 Joseph Pecoraro 5 * 6 * Redistribution and use in source and binary forms, with or without 7 * modification, are permitted provided that the following conditions 8 * are met: 9 * 10 * 1. Redistributions of source code must retain the above copyright 11 * notice, this list of conditions and the following disclaimer. 12 * 2. Redistributions in binary form must reproduce the above copyright 13 * notice, this list of conditions and the following disclaimer in the 14 * documentation and/or other materials provided with the distribution. 15 * 3. Neither the name of Apple Computer, Inc. ("Apple") nor the names of 16 * its contributors may be used to endorse or promote products derived 17 * from this software without specific prior written permission. 18 * 19 * THIS SOFTWARE IS PROVIDED BY APPLE AND ITS CONTRIBUTORS "AS IS" AND ANY 20 * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED 21 * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 22 * DISCLAIMED. IN NO EVENT SHALL APPLE OR ITS CONTRIBUTORS BE LIABLE FOR ANY 23 * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES 24 * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; 25 * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND 26 * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 27 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF 28 * THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 29 */ 30 31WebInspector.ElementsPanel = function() 32{ 33 WebInspector.Panel.call(this); 34 35 this.element.addStyleClass("elements"); 36 37 this.contentElement = document.createElement("div"); 38 this.contentElement.id = "elements-content"; 39 this.contentElement.className = "outline-disclosure"; 40 41 this.treeOutline = new WebInspector.ElementsTreeOutline(); 42 this.treeOutline.panel = this; 43 this.treeOutline.includeRootDOMNode = false; 44 this.treeOutline.selectEnabled = true; 45 46 this.treeOutline.focusedNodeChanged = function(forceUpdate) 47 { 48 if (this.panel.visible && WebInspector.currentFocusElement !== document.getElementById("search")) 49 WebInspector.currentFocusElement = document.getElementById("main-panels"); 50 51 this.panel.updateBreadcrumb(forceUpdate); 52 53 for (var pane in this.panel.sidebarPanes) 54 this.panel.sidebarPanes[pane].needsUpdate = true; 55 56 this.panel.updateStyles(true); 57 this.panel.updateMetrics(); 58 this.panel.updateProperties(); 59 60 if (InspectorController.searchingForNode()) { 61 InspectorController.toggleNodeSearch(); 62 this.panel.nodeSearchButton.removeStyleClass("toggled-on"); 63 } 64 WebInspector.console.addInspectedNode(this._focusedDOMNode); 65 }; 66 67 this.contentElement.appendChild(this.treeOutline.element); 68 69 this.crumbsElement = document.createElement("div"); 70 this.crumbsElement.className = "crumbs"; 71 this.crumbsElement.addEventListener("mousemove", this._mouseMovedInCrumbs.bind(this), false); 72 this.crumbsElement.addEventListener("mouseout", this._mouseMovedOutOfCrumbs.bind(this), false); 73 74 this.sidebarPanes = {}; 75 this.sidebarPanes.styles = new WebInspector.StylesSidebarPane(); 76 this.sidebarPanes.metrics = new WebInspector.MetricsSidebarPane(); 77 this.sidebarPanes.properties = new WebInspector.PropertiesSidebarPane(); 78 79 this.sidebarPanes.styles.onexpand = this.updateStyles.bind(this); 80 this.sidebarPanes.metrics.onexpand = this.updateMetrics.bind(this); 81 this.sidebarPanes.properties.onexpand = this.updateProperties.bind(this); 82 83 this.sidebarPanes.styles.expanded = true; 84 85 this.sidebarPanes.styles.addEventListener("style edited", this._stylesPaneEdited, this); 86 this.sidebarPanes.styles.addEventListener("style property toggled", this._stylesPaneEdited, this); 87 this.sidebarPanes.metrics.addEventListener("metrics edited", this._metricsPaneEdited, this); 88 89 this.sidebarElement = document.createElement("div"); 90 this.sidebarElement.id = "elements-sidebar"; 91 92 this.sidebarElement.appendChild(this.sidebarPanes.styles.element); 93 this.sidebarElement.appendChild(this.sidebarPanes.metrics.element); 94 this.sidebarElement.appendChild(this.sidebarPanes.properties.element); 95 96 this.sidebarResizeElement = document.createElement("div"); 97 this.sidebarResizeElement.className = "sidebar-resizer-vertical"; 98 this.sidebarResizeElement.addEventListener("mousedown", this.rightSidebarResizerDragStart.bind(this), false); 99 100 this.nodeSearchButton = this.createStatusBarButton(); 101 this.nodeSearchButton.title = WebInspector.UIString("Select an element in the page to inspect it."); 102 this.nodeSearchButton.id = "node-search-status-bar-item"; 103 this.nodeSearchButton.className = "status-bar-item"; 104 this.nodeSearchButton.addEventListener("click", this._nodeSearchButtonClicked.bind(this), false); 105 106 this.searchingForNode = false; 107 108 this.element.appendChild(this.contentElement); 109 this.element.appendChild(this.sidebarElement); 110 this.element.appendChild(this.sidebarResizeElement); 111 112 this._mutationMonitoredWindows = []; 113 this._nodeInsertedEventListener = InspectorController.wrapCallback(this._nodeInserted.bind(this)); 114 this._nodeRemovedEventListener = InspectorController.wrapCallback(this._nodeRemoved.bind(this)); 115 this._contentLoadedEventListener = InspectorController.wrapCallback(this._contentLoaded.bind(this)); 116 117 this.stylesheet = null; 118 this.styles = {}; 119 120 this.reset(); 121} 122 123WebInspector.ElementsPanel.prototype = { 124 toolbarItemClass: "elements", 125 126 get toolbarItemLabel() 127 { 128 return WebInspector.UIString("Elements"); 129 }, 130 131 get statusBarItems() 132 { 133 return [this.nodeSearchButton, this.crumbsElement]; 134 }, 135 136 updateStatusBarItems: function() 137 { 138 this.updateBreadcrumbSizes(); 139 }, 140 141 show: function() 142 { 143 WebInspector.Panel.prototype.show.call(this); 144 this.sidebarResizeElement.style.right = (this.sidebarElement.offsetWidth - 3) + "px"; 145 this.updateBreadcrumb(); 146 this.treeOutline.updateSelection(); 147 if (this.recentlyModifiedNodes.length) 148 this._updateModifiedNodes(); 149 }, 150 151 hide: function() 152 { 153 WebInspector.Panel.prototype.hide.call(this); 154 155 WebInspector.hoveredDOMNode = null; 156 157 if (InspectorController.searchingForNode()) { 158 InspectorController.toggleNodeSearch(); 159 this.nodeSearchButton.removeStyleClass("toggled-on"); 160 } 161 }, 162 163 resize: function() 164 { 165 this.treeOutline.updateSelection(); 166 this.updateBreadcrumbSizes(); 167 }, 168 169 reset: function() 170 { 171 this.rootDOMNode = null; 172 this.focusedDOMNode = null; 173 174 WebInspector.hoveredDOMNode = null; 175 176 if (InspectorController.searchingForNode()) { 177 InspectorController.toggleNodeSearch(); 178 this.nodeSearchButton.removeStyleClass("toggled-on"); 179 } 180 181 this.recentlyModifiedNodes = []; 182 this.unregisterAllMutationEventListeners(); 183 184 delete this.currentQuery; 185 this.searchCanceled(); 186 187 var inspectedWindow = Preferences.useDOMAgent ? WebInspector.domAgent.inspectedWindow : InspectorController.inspectedWindow(); 188 if (!inspectedWindow || !inspectedWindow.document) 189 return; 190 191 if (!inspectedWindow.document.firstChild) { 192 function contentLoaded() 193 { 194 inspectedWindow.document.removeEventListener("DOMContentLoaded", contentLoadedCallback, false); 195 196 this.reset(); 197 } 198 199 var contentLoadedCallback = InspectorController.wrapCallback(contentLoaded.bind(this)); 200 inspectedWindow.document.addEventListener("DOMContentLoaded", contentLoadedCallback, false); 201 return; 202 } 203 204 // If the window isn't visible, return early so the DOM tree isn't built 205 // and mutation event listeners are not added. 206 if (!InspectorController.isWindowVisible()) 207 return; 208 209 this.registerMutationEventListeners(inspectedWindow); 210 211 var inspectedRootDocument = inspectedWindow.document; 212 this.rootDOMNode = inspectedRootDocument; 213 214 var canidateFocusNode = inspectedRootDocument.body || inspectedRootDocument.documentElement; 215 if (canidateFocusNode) { 216 this.treeOutline.suppressSelectHighlight = true; 217 this.focusedDOMNode = canidateFocusNode; 218 this.treeOutline.suppressSelectHighlight = false; 219 220 if (this.treeOutline.selectedTreeElement) 221 this.treeOutline.selectedTreeElement.expand(); 222 } 223 }, 224 225 includedInSearchResultsPropertyName: "__includedInInspectorSearchResults", 226 227 searchCanceled: function() 228 { 229 if (this._searchResults) { 230 const searchResultsProperty = this.includedInSearchResultsPropertyName; 231 for (var i = 0; i < this._searchResults.length; ++i) { 232 var node = this._searchResults[i]; 233 234 // Remove the searchResultsProperty since there might be an unfinished search. 235 delete node[searchResultsProperty]; 236 237 var treeElement = this.treeOutline.findTreeElement(node); 238 if (treeElement) 239 treeElement.highlighted = false; 240 } 241 } 242 243 WebInspector.updateSearchMatchesCount(0, this); 244 245 if (this._currentSearchChunkIntervalIdentifier) { 246 clearInterval(this._currentSearchChunkIntervalIdentifier); 247 delete this._currentSearchChunkIntervalIdentifier; 248 } 249 250 this._currentSearchResultIndex = 0; 251 this._searchResults = []; 252 }, 253 254 performSearch: function(query) 255 { 256 // Call searchCanceled since it will reset everything we need before doing a new search. 257 this.searchCanceled(); 258 259 const whitespaceTrimmedQuery = query.trimWhitespace(); 260 if (!whitespaceTrimmedQuery.length) 261 return; 262 263 var tagNameQuery = whitespaceTrimmedQuery; 264 var attributeNameQuery = whitespaceTrimmedQuery; 265 var startTagFound = (tagNameQuery.indexOf("<") === 0); 266 var endTagFound = (tagNameQuery.lastIndexOf(">") === (tagNameQuery.length - 1)); 267 268 if (startTagFound || endTagFound) { 269 var tagNameQueryLength = tagNameQuery.length; 270 tagNameQuery = tagNameQuery.substring((startTagFound ? 1 : 0), (endTagFound ? (tagNameQueryLength - 1) : tagNameQueryLength)); 271 } 272 273 // Check the tagNameQuery is it is a possibly valid tag name. 274 if (!/^[a-zA-Z0-9\-_:]+$/.test(tagNameQuery)) 275 tagNameQuery = null; 276 277 // Check the attributeNameQuery is it is a possibly valid tag name. 278 if (!/^[a-zA-Z0-9\-_:]+$/.test(attributeNameQuery)) 279 attributeNameQuery = null; 280 281 const escapedQuery = query.escapeCharacters("'"); 282 const escapedTagNameQuery = (tagNameQuery ? tagNameQuery.escapeCharacters("'") : null); 283 const escapedWhitespaceTrimmedQuery = whitespaceTrimmedQuery.escapeCharacters("'"); 284 const searchResultsProperty = this.includedInSearchResultsPropertyName; 285 286 var updatedMatchCountOnce = false; 287 var matchesCountUpdateTimeout = null; 288 289 function updateMatchesCount() 290 { 291 WebInspector.updateSearchMatchesCount(this._searchResults.length, this); 292 matchesCountUpdateTimeout = null; 293 updatedMatchCountOnce = true; 294 } 295 296 function updateMatchesCountSoon() 297 { 298 if (!updatedMatchCountOnce) 299 return updateMatchesCount.call(this); 300 if (matchesCountUpdateTimeout) 301 return; 302 // Update the matches count every half-second so it doesn't feel twitchy. 303 matchesCountUpdateTimeout = setTimeout(updateMatchesCount.bind(this), 500); 304 } 305 306 function addNodesToResults(nodes, length, getItem) 307 { 308 if (!length) 309 return; 310 311 for (var i = 0; i < length; ++i) { 312 var node = getItem.call(nodes, i); 313 // Skip this node if it already has the property. 314 if (searchResultsProperty in node) 315 continue; 316 317 if (!this._searchResults.length) { 318 this._currentSearchResultIndex = 0; 319 this.focusedDOMNode = node; 320 } 321 322 node[searchResultsProperty] = true; 323 this._searchResults.push(node); 324 325 // Highlight the tree element to show it matched the search. 326 // FIXME: highlight the substrings in text nodes and attributes. 327 var treeElement = this.treeOutline.findTreeElement(node); 328 if (treeElement) 329 treeElement.highlighted = true; 330 } 331 332 updateMatchesCountSoon.call(this); 333 } 334 335 function matchExactItems(doc) 336 { 337 matchExactId.call(this, doc); 338 matchExactClassNames.call(this, doc); 339 matchExactTagNames.call(this, doc); 340 matchExactAttributeNames.call(this, doc); 341 } 342 343 function matchExactId(doc) 344 { 345 const result = doc.__proto__.getElementById.call(doc, whitespaceTrimmedQuery); 346 addNodesToResults.call(this, result, (result ? 1 : 0), function() { return this }); 347 } 348 349 function matchExactClassNames(doc) 350 { 351 const result = doc.__proto__.getElementsByClassName.call(doc, whitespaceTrimmedQuery); 352 addNodesToResults.call(this, result, result.length, result.item); 353 } 354 355 function matchExactTagNames(doc) 356 { 357 if (!tagNameQuery) 358 return; 359 const result = doc.__proto__.getElementsByTagName.call(doc, tagNameQuery); 360 addNodesToResults.call(this, result, result.length, result.item); 361 } 362 363 function matchExactAttributeNames(doc) 364 { 365 if (!attributeNameQuery) 366 return; 367 const result = doc.__proto__.querySelectorAll.call(doc, "[" + attributeNameQuery + "]"); 368 addNodesToResults.call(this, result, result.length, result.item); 369 } 370 371 function matchPartialTagNames(doc) 372 { 373 if (!tagNameQuery) 374 return; 375 const result = doc.__proto__.evaluate.call(doc, "//*[contains(name(), '" + escapedTagNameQuery + "')]", doc, null, XPathResult.ORDERED_NODE_SNAPSHOT_TYPE); 376 addNodesToResults.call(this, result, result.snapshotLength, result.snapshotItem); 377 } 378 379 function matchStartOfTagNames(doc) 380 { 381 if (!tagNameQuery) 382 return; 383 const result = doc.__proto__.evaluate.call(doc, "//*[starts-with(name(), '" + escapedTagNameQuery + "')]", doc, null, XPathResult.ORDERED_NODE_SNAPSHOT_TYPE); 384 addNodesToResults.call(this, result, result.snapshotLength, result.snapshotItem); 385 } 386 387 function matchPartialTagNamesAndAttributeValues(doc) 388 { 389 if (!tagNameQuery) { 390 matchPartialAttributeValues.call(this, doc); 391 return; 392 } 393 394 const result = doc.__proto__.evaluate.call(doc, "//*[contains(name(), '" + escapedTagNameQuery + "') or contains(@*, '" + escapedQuery + "')]", doc, null, XPathResult.ORDERED_NODE_SNAPSHOT_TYPE); 395 addNodesToResults.call(this, result, result.snapshotLength, result.snapshotItem); 396 } 397 398 function matchPartialAttributeValues(doc) 399 { 400 const result = doc.__proto__.evaluate.call(doc, "//*[contains(@*, '" + escapedQuery + "')]", doc, null, XPathResult.ORDERED_NODE_SNAPSHOT_TYPE); 401 addNodesToResults.call(this, result, result.snapshotLength, result.snapshotItem); 402 } 403 404 function matchStyleSelector(doc) 405 { 406 const result = doc.__proto__.querySelectorAll.call(doc, whitespaceTrimmedQuery); 407 addNodesToResults.call(this, result, result.length, result.item); 408 } 409 410 function matchPlainText(doc) 411 { 412 const result = doc.__proto__.evaluate.call(doc, "//text()[contains(., '" + escapedQuery + "')] | //comment()[contains(., '" + escapedQuery + "')]", doc, null, XPathResult.ORDERED_NODE_SNAPSHOT_TYPE); 413 addNodesToResults.call(this, result, result.snapshotLength, result.snapshotItem); 414 } 415 416 function matchXPathQuery(doc) 417 { 418 const result = doc.__proto__.evaluate.call(doc, whitespaceTrimmedQuery, doc, null, XPathResult.ORDERED_NODE_SNAPSHOT_TYPE); 419 addNodesToResults.call(this, result, result.snapshotLength, result.snapshotItem); 420 } 421 422 function finishedSearching() 423 { 424 // Remove the searchResultsProperty now that the search is finished. 425 for (var i = 0; i < this._searchResults.length; ++i) 426 delete this._searchResults[i][searchResultsProperty]; 427 } 428 429 const mainFrameDocument = InspectorController.inspectedWindow().document; 430 const searchDocuments = [mainFrameDocument]; 431 432 if (tagNameQuery && startTagFound && endTagFound) 433 const searchFunctions = [matchExactTagNames, matchPlainText]; 434 else if (tagNameQuery && startTagFound) 435 const searchFunctions = [matchStartOfTagNames, matchPlainText]; 436 else if (tagNameQuery && endTagFound) { 437 // FIXME: we should have a matchEndOfTagNames search function if endTagFound is true but not startTagFound. 438 // This requires ends-with() support in XPath, WebKit only supports starts-with() and contains(). 439 const searchFunctions = [matchPartialTagNames, matchPlainText]; 440 } else if (whitespaceTrimmedQuery === "//*" || whitespaceTrimmedQuery === "*") { 441 // These queries will match every node. Matching everything isn't useful and can be slow for large pages, 442 // so limit the search functions list to plain text and attribute matching. 443 const searchFunctions = [matchPartialAttributeValues, matchPlainText]; 444 } else 445 const searchFunctions = [matchExactItems, matchStyleSelector, matchPartialTagNamesAndAttributeValues, matchPlainText, matchXPathQuery]; 446 447 // Find all frames, iframes and object elements to search their documents. 448 const querySelectorAllFunction = InspectorController.inspectedWindow().Document.prototype.querySelectorAll; 449 const subdocumentResult = querySelectorAllFunction.call(mainFrameDocument, "iframe, frame, object"); 450 451 for (var i = 0; i < subdocumentResult.length; ++i) { 452 var element = subdocumentResult.item(i); 453 if (element.contentDocument) 454 searchDocuments.push(element.contentDocument); 455 } 456 457 const panel = this; 458 var documentIndex = 0; 459 var searchFunctionIndex = 0; 460 var chunkIntervalIdentifier = null; 461 462 // Split up the work into chunks so we don't block the UI thread while processing. 463 464 function processChunk() 465 { 466 var searchDocument = searchDocuments[documentIndex]; 467 var searchFunction = searchFunctions[searchFunctionIndex]; 468 469 if (++searchFunctionIndex > searchFunctions.length) { 470 searchFunction = searchFunctions[0]; 471 searchFunctionIndex = 0; 472 473 if (++documentIndex > searchDocuments.length) { 474 if (panel._currentSearchChunkIntervalIdentifier === chunkIntervalIdentifier) 475 delete panel._currentSearchChunkIntervalIdentifier; 476 clearInterval(chunkIntervalIdentifier); 477 finishedSearching.call(panel); 478 return; 479 } 480 481 searchDocument = searchDocuments[documentIndex]; 482 } 483 484 if (!searchDocument || !searchFunction) 485 return; 486 487 try { 488 searchFunction.call(panel, searchDocument); 489 } catch(err) { 490 // ignore any exceptions. the query might be malformed, but we allow that. 491 } 492 } 493 494 processChunk(); 495 496 chunkIntervalIdentifier = setInterval(processChunk, 25); 497 this._currentSearchChunkIntervalIdentifier = chunkIntervalIdentifier; 498 }, 499 500 jumpToNextSearchResult: function() 501 { 502 if (!this._searchResults || !this._searchResults.length) 503 return; 504 if (++this._currentSearchResultIndex >= this._searchResults.length) 505 this._currentSearchResultIndex = 0; 506 this.focusedDOMNode = this._searchResults[this._currentSearchResultIndex]; 507 }, 508 509 jumpToPreviousSearchResult: function() 510 { 511 if (!this._searchResults || !this._searchResults.length) 512 return; 513 if (--this._currentSearchResultIndex < 0) 514 this._currentSearchResultIndex = (this._searchResults.length - 1); 515 this.focusedDOMNode = this._searchResults[this._currentSearchResultIndex]; 516 }, 517 518 inspectedWindowCleared: function(window) 519 { 520 if (InspectorController.isWindowVisible()) 521 this.updateMutationEventListeners(window); 522 }, 523 524 renameSelector: function(oldIdentifier, newIdentifier, oldSelector, newSelector) 525 { 526 // TODO: Implement Shifting the oldSelector, and its contents to a newSelector 527 }, 528 529 addStyleChange: function(identifier, style, property) 530 { 531 if (!style.parentRule) 532 return; 533 534 var selector = style.parentRule.selectorText; 535 if (!this.styles[identifier]) 536 this.styles[identifier] = {}; 537 538 if (!this.styles[identifier][selector]) 539 this.styles[identifier][selector] = {}; 540 541 if (!this.styles[identifier][selector][property]) 542 WebInspector.styleChanges += 1; 543 544 this.styles[identifier][selector][property] = style.getPropertyValue(property); 545 }, 546 547 removeStyleChange: function(identifier, style, property) 548 { 549 if (!style.parentRule) 550 return; 551 552 var selector = style.parentRule.selectorText; 553 if (!this.styles[identifier] || !this.styles[identifier][selector]) 554 return; 555 556 if (this.styles[identifier][selector][property]) { 557 delete this.styles[identifier][selector][property]; 558 WebInspector.styleChanges -= 1; 559 } 560 }, 561 562 generateStylesheet: function() 563 { 564 if (!WebInspector.styleChanges) 565 return; 566 567 // Merge Down to Just Selectors 568 var mergedSelectors = {}; 569 for (var identifier in this.styles) { 570 for (var selector in this.styles[identifier]) { 571 if (!mergedSelectors[selector]) 572 mergedSelectors[selector] = this.styles[identifier][selector]; 573 else { // merge on selector 574 var merge = {}; 575 for (var property in mergedSelectors[selector]) 576 merge[property] = mergedSelectors[selector][property]; 577 for (var property in this.styles[identifier][selector]) { 578 if (!merge[property]) 579 merge[property] = this.styles[identifier][selector][property]; 580 else { // merge on property within a selector, include comment to notify user 581 var value1 = merge[property]; 582 var value2 = this.styles[identifier][selector][property]; 583 584 if (value1 === value2) 585 merge[property] = [value1]; 586 else if (Object.type(value1) === "array") 587 merge[property].push(value2); 588 else 589 merge[property] = [value1, value2]; 590 } 591 } 592 mergedSelectors[selector] = merge; 593 } 594 } 595 } 596 597 var builder = []; 598 builder.push("/**"); 599 builder.push(" * Inspector Generated Stylesheet"); // UIString? 600 builder.push(" */\n"); 601 602 var indent = " "; 603 function displayProperty(property, value, comment) { 604 if (comment) 605 return indent + "/* " + property + ": " + value + "; */"; 606 else 607 return indent + property + ": " + value + ";"; 608 } 609 610 for (var selector in mergedSelectors) { 611 var psuedoStyle = mergedSelectors[selector]; 612 var properties = Object.properties(psuedoStyle); 613 if (properties.length) { 614 builder.push(selector + " {"); 615 for (var i = 0; i < properties.length; ++i) { 616 var property = properties[i]; 617 var value = psuedoStyle[property]; 618 if (Object.type(value) !== "array") 619 builder.push(displayProperty(property, value)); 620 else { 621 if (value.length === 1) 622 builder.push(displayProperty(property, value) + " /* merged from equivalent edits */"); // UIString? 623 else { 624 builder.push(indent + "/* There was a Conflict... There were Multiple Edits for '" + property + "' */"); // UIString? 625 for (var j = 0; j < value.length; ++j) 626 builder.push(displayProperty(property, value, true)); 627 } 628 } 629 } 630 builder.push("}\n"); 631 } 632 } 633 634 WebInspector.showConsole(); 635 var result = builder.join("\n"); 636 InspectorController.inspectedWindow().console.log(result); 637 }, 638 639 _addMutationEventListeners: function(monitoredWindow) 640 { 641 monitoredWindow.document.addEventListener("DOMNodeInserted", this._nodeInsertedEventListener, true); 642 monitoredWindow.document.addEventListener("DOMNodeRemoved", this._nodeRemovedEventListener, true); 643 if (monitoredWindow.frameElement) 644 monitoredWindow.addEventListener("DOMContentLoaded", this._contentLoadedEventListener, true); 645 }, 646 647 _removeMutationEventListeners: function(monitoredWindow) 648 { 649 if (monitoredWindow.frameElement) 650 monitoredWindow.removeEventListener("DOMContentLoaded", this._contentLoadedEventListener, true); 651 if (!monitoredWindow.document) 652 return; 653 monitoredWindow.document.removeEventListener("DOMNodeInserted", this._nodeInsertedEventListener, true); 654 monitoredWindow.document.removeEventListener("DOMNodeRemoved", this._nodeRemovedEventListener, true); 655 }, 656 657 updateMutationEventListeners: function(monitoredWindow) 658 { 659 this._addMutationEventListeners(monitoredWindow); 660 }, 661 662 registerMutationEventListeners: function(monitoredWindow) 663 { 664 if (!monitoredWindow || this._mutationMonitoredWindows.indexOf(monitoredWindow) !== -1) 665 return; 666 this._mutationMonitoredWindows.push(monitoredWindow); 667 if (InspectorController.isWindowVisible()) 668 this._addMutationEventListeners(monitoredWindow); 669 }, 670 671 unregisterMutationEventListeners: function(monitoredWindow) 672 { 673 if (!monitoredWindow || this._mutationMonitoredWindows.indexOf(monitoredWindow) === -1) 674 return; 675 this._mutationMonitoredWindows.remove(monitoredWindow); 676 this._removeMutationEventListeners(monitoredWindow); 677 }, 678 679 unregisterAllMutationEventListeners: function() 680 { 681 for (var i = 0; i < this._mutationMonitoredWindows.length; ++i) 682 this._removeMutationEventListeners(this._mutationMonitoredWindows[i]); 683 this._mutationMonitoredWindows = []; 684 }, 685 686 get rootDOMNode() 687 { 688 return this.treeOutline.rootDOMNode; 689 }, 690 691 set rootDOMNode(x) 692 { 693 this.treeOutline.rootDOMNode = x; 694 }, 695 696 get focusedDOMNode() 697 { 698 return this.treeOutline.focusedDOMNode; 699 }, 700 701 set focusedDOMNode(x) 702 { 703 this.treeOutline.focusedDOMNode = x; 704 }, 705 706 _contentLoaded: function(event) 707 { 708 this.recentlyModifiedNodes.push({node: event.target, parent: event.target.defaultView.frameElement, replaced: true}); 709 if (this.visible) 710 this._updateModifiedNodesSoon(); 711 }, 712 713 _nodeInserted: function(event) 714 { 715 this.recentlyModifiedNodes.push({node: event.target, parent: event.relatedNode, inserted: true}); 716 if (this.visible) 717 this._updateModifiedNodesSoon(); 718 }, 719 720 _nodeRemoved: function(event) 721 { 722 this.recentlyModifiedNodes.push({node: event.target, parent: event.relatedNode, removed: true}); 723 if (this.visible) 724 this._updateModifiedNodesSoon(); 725 }, 726 727 _updateModifiedNodesSoon: function() 728 { 729 if ("_updateModifiedNodesTimeout" in this) 730 return; 731 this._updateModifiedNodesTimeout = setTimeout(this._updateModifiedNodes.bind(this), 0); 732 }, 733 734 _updateModifiedNodes: function() 735 { 736 if ("_updateModifiedNodesTimeout" in this) { 737 clearTimeout(this._updateModifiedNodesTimeout); 738 delete this._updateModifiedNodesTimeout; 739 } 740 741 var updatedParentTreeElements = []; 742 var updateBreadcrumbs = false; 743 744 for (var i = 0; i < this.recentlyModifiedNodes.length; ++i) { 745 var replaced = this.recentlyModifiedNodes[i].replaced; 746 var parent = this.recentlyModifiedNodes[i].parent; 747 if (!parent) 748 continue; 749 750 var parentNodeItem = this.treeOutline.findTreeElement(parent, null, null, objectsAreSame); 751 if (parentNodeItem && !parentNodeItem.alreadyUpdatedChildren) { 752 parentNodeItem.updateChildren(replaced); 753 parentNodeItem.alreadyUpdatedChildren = true; 754 updatedParentTreeElements.push(parentNodeItem); 755 } 756 757 if (!updateBreadcrumbs && (objectsAreSame(this.focusedDOMNode, parent) || isAncestorIncludingParentFrames(this.focusedDOMNode, parent))) 758 updateBreadcrumbs = true; 759 } 760 761 for (var i = 0; i < updatedParentTreeElements.length; ++i) 762 delete updatedParentTreeElements[i].alreadyUpdatedChildren; 763 764 this.recentlyModifiedNodes = []; 765 766 if (updateBreadcrumbs) 767 this.updateBreadcrumb(true); 768 }, 769 770 _stylesPaneEdited: function() 771 { 772 this.sidebarPanes.metrics.needsUpdate = true; 773 this.updateMetrics(); 774 }, 775 776 _metricsPaneEdited: function() 777 { 778 this.sidebarPanes.styles.needsUpdate = true; 779 this.updateStyles(true); 780 }, 781 782 _mouseMovedInCrumbs: function(event) 783 { 784 var nodeUnderMouse = document.elementFromPoint(event.pageX, event.pageY); 785 var crumbElement = nodeUnderMouse.enclosingNodeOrSelfWithClass("crumb"); 786 787 WebInspector.hoveredDOMNode = (crumbElement ? crumbElement.representedObject : null); 788 789 if ("_mouseOutOfCrumbsTimeout" in this) { 790 clearTimeout(this._mouseOutOfCrumbsTimeout); 791 delete this._mouseOutOfCrumbsTimeout; 792 } 793 }, 794 795 _mouseMovedOutOfCrumbs: function(event) 796 { 797 var nodeUnderMouse = document.elementFromPoint(event.pageX, event.pageY); 798 if (nodeUnderMouse.isDescendant(this.crumbsElement)) 799 return; 800 801 WebInspector.hoveredDOMNode = null; 802 803 this._mouseOutOfCrumbsTimeout = setTimeout(this.updateBreadcrumbSizes.bind(this), 1000); 804 }, 805 806 updateBreadcrumb: function(forceUpdate) 807 { 808 if (!this.visible) 809 return; 810 811 var crumbs = this.crumbsElement; 812 813 var handled = false; 814 var foundRoot = false; 815 var crumb = crumbs.firstChild; 816 while (crumb) { 817 if (objectsAreSame(crumb.representedObject, this.rootDOMNode)) 818 foundRoot = true; 819 820 if (foundRoot) 821 crumb.addStyleClass("dimmed"); 822 else 823 crumb.removeStyleClass("dimmed"); 824 825 if (objectsAreSame(crumb.representedObject, this.focusedDOMNode)) { 826 crumb.addStyleClass("selected"); 827 handled = true; 828 } else { 829 crumb.removeStyleClass("selected"); 830 } 831 832 crumb = crumb.nextSibling; 833 } 834 835 if (handled && !forceUpdate) { 836 // We don't need to rebuild the crumbs, but we need to adjust sizes 837 // to reflect the new focused or root node. 838 this.updateBreadcrumbSizes(); 839 return; 840 } 841 842 crumbs.removeChildren(); 843 844 var panel = this; 845 846 function selectCrumbFunction(event) 847 { 848 var crumb = event.currentTarget; 849 if (crumb.hasStyleClass("collapsed")) { 850 // Clicking a collapsed crumb will expose the hidden crumbs. 851 if (crumb === panel.crumbsElement.firstChild) { 852 // If the focused crumb is the first child, pick the farthest crumb 853 // that is still hidden. This allows the user to expose every crumb. 854 var currentCrumb = crumb; 855 while (currentCrumb) { 856 var hidden = currentCrumb.hasStyleClass("hidden"); 857 var collapsed = currentCrumb.hasStyleClass("collapsed"); 858 if (!hidden && !collapsed) 859 break; 860 crumb = currentCrumb; 861 currentCrumb = currentCrumb.nextSibling; 862 } 863 } 864 865 panel.updateBreadcrumbSizes(crumb); 866 } else { 867 // Clicking a dimmed crumb or double clicking (event.detail >= 2) 868 // will change the root node in addition to the focused node. 869 if (event.detail >= 2 || crumb.hasStyleClass("dimmed")) 870 panel.rootDOMNode = crumb.representedObject.parentNode; 871 panel.focusedDOMNode = crumb.representedObject; 872 } 873 874 event.preventDefault(); 875 } 876 877 foundRoot = false; 878 for (var current = this.focusedDOMNode; current; current = parentNodeOrFrameElement(current)) { 879 if (current.nodeType === Node.DOCUMENT_NODE) 880 continue; 881 882 if (objectsAreSame(current, this.rootDOMNode)) 883 foundRoot = true; 884 885 var crumb = document.createElement("span"); 886 crumb.className = "crumb"; 887 crumb.representedObject = current; 888 crumb.addEventListener("mousedown", selectCrumbFunction, false); 889 890 var crumbTitle; 891 switch (current.nodeType) { 892 case Node.ELEMENT_NODE: 893 crumbTitle = current.nodeName.toLowerCase(); 894 895 var nameElement = document.createElement("span"); 896 nameElement.textContent = crumbTitle; 897 crumb.appendChild(nameElement); 898 899 var idAttribute = current.getAttribute("id"); 900 if (idAttribute) { 901 var idElement = document.createElement("span"); 902 crumb.appendChild(idElement); 903 904 var part = "#" + idAttribute; 905 crumbTitle += part; 906 idElement.appendChild(document.createTextNode(part)); 907 908 // Mark the name as extra, since the ID is more important. 909 nameElement.className = "extra"; 910 } 911 912 var classAttribute = current.getAttribute("class"); 913 if (classAttribute) { 914 var classes = classAttribute.split(/\s+/); 915 var foundClasses = {}; 916 917 if (classes.length) { 918 var classesElement = document.createElement("span"); 919 classesElement.className = "extra"; 920 crumb.appendChild(classesElement); 921 922 for (var i = 0; i < classes.length; ++i) { 923 var className = classes[i]; 924 if (className && !(className in foundClasses)) { 925 var part = "." + className; 926 crumbTitle += part; 927 classesElement.appendChild(document.createTextNode(part)); 928 foundClasses[className] = true; 929 } 930 } 931 } 932 } 933 934 break; 935 936 case Node.TEXT_NODE: 937 if (isNodeWhitespace.call(current)) 938 crumbTitle = WebInspector.UIString("(whitespace)"); 939 else 940 crumbTitle = WebInspector.UIString("(text)"); 941 break 942 943 case Node.COMMENT_NODE: 944 crumbTitle = "<!-->"; 945 break; 946 947 case Node.DOCUMENT_TYPE_NODE: 948 crumbTitle = "<!DOCTYPE>"; 949 break; 950 951 default: 952 crumbTitle = current.nodeName.toLowerCase(); 953 } 954 955 if (!crumb.childNodes.length) { 956 var nameElement = document.createElement("span"); 957 nameElement.textContent = crumbTitle; 958 crumb.appendChild(nameElement); 959 } 960 961 crumb.title = crumbTitle; 962 963 if (foundRoot) 964 crumb.addStyleClass("dimmed"); 965 if (objectsAreSame(current, this.focusedDOMNode)) 966 crumb.addStyleClass("selected"); 967 if (!crumbs.childNodes.length) 968 crumb.addStyleClass("end"); 969 970 crumbs.appendChild(crumb); 971 } 972 973 if (crumbs.hasChildNodes()) 974 crumbs.lastChild.addStyleClass("start"); 975 976 this.updateBreadcrumbSizes(); 977 }, 978 979 updateBreadcrumbSizes: function(focusedCrumb) 980 { 981 if (!this.visible) 982 return; 983 984 if (document.body.offsetWidth <= 0) { 985 // The stylesheet hasn't loaded yet or the window is closed, 986 // so we can't calculate what is need. Return early. 987 return; 988 } 989 990 var crumbs = this.crumbsElement; 991 if (!crumbs.childNodes.length || crumbs.offsetWidth <= 0) 992 return; // No crumbs, do nothing. 993 994 // A Zero index is the right most child crumb in the breadcrumb. 995 var selectedIndex = 0; 996 var focusedIndex = 0; 997 var selectedCrumb; 998 999 var i = 0; 1000 var crumb = crumbs.firstChild; 1001 while (crumb) { 1002 // Find the selected crumb and index. 1003 if (!selectedCrumb && crumb.hasStyleClass("selected")) { 1004 selectedCrumb = crumb; 1005 selectedIndex = i; 1006 } 1007 1008 // Find the focused crumb index. 1009 if (crumb === focusedCrumb) 1010 focusedIndex = i; 1011 1012 // Remove any styles that affect size before 1013 // deciding to shorten any crumbs. 1014 if (crumb !== crumbs.lastChild) 1015 crumb.removeStyleClass("start"); 1016 if (crumb !== crumbs.firstChild) 1017 crumb.removeStyleClass("end"); 1018 1019 crumb.removeStyleClass("compact"); 1020 crumb.removeStyleClass("collapsed"); 1021 crumb.removeStyleClass("hidden"); 1022 1023 crumb = crumb.nextSibling; 1024 ++i; 1025 } 1026 1027 // Restore the start and end crumb classes in case they got removed in coalesceCollapsedCrumbs(). 1028 // The order of the crumbs in the document is opposite of the visual order. 1029 crumbs.firstChild.addStyleClass("end"); 1030 crumbs.lastChild.addStyleClass("start"); 1031 1032 function crumbsAreSmallerThanContainer() 1033 { 1034 var rightPadding = 20; 1035 var errorWarningElement = document.getElementById("error-warning-count"); 1036 if (!WebInspector.drawer.visible && errorWarningElement) 1037 rightPadding += errorWarningElement.offsetWidth; 1038 return ((crumbs.totalOffsetLeft + crumbs.offsetWidth + rightPadding) < window.innerWidth); 1039 } 1040 1041 if (crumbsAreSmallerThanContainer()) 1042 return; // No need to compact the crumbs, they all fit at full size. 1043 1044 var BothSides = 0; 1045 var AncestorSide = -1; 1046 var ChildSide = 1; 1047 1048 function makeCrumbsSmaller(shrinkingFunction, direction, significantCrumb) 1049 { 1050 if (!significantCrumb) 1051 significantCrumb = (focusedCrumb || selectedCrumb); 1052 1053 if (significantCrumb === selectedCrumb) 1054 var significantIndex = selectedIndex; 1055 else if (significantCrumb === focusedCrumb) 1056 var significantIndex = focusedIndex; 1057 else { 1058 var significantIndex = 0; 1059 for (var i = 0; i < crumbs.childNodes.length; ++i) { 1060 if (crumbs.childNodes[i] === significantCrumb) { 1061 significantIndex = i; 1062 break; 1063 } 1064 } 1065 } 1066 1067 function shrinkCrumbAtIndex(index) 1068 { 1069 var shrinkCrumb = crumbs.childNodes[index]; 1070 if (shrinkCrumb && shrinkCrumb !== significantCrumb) 1071 shrinkingFunction(shrinkCrumb); 1072 if (crumbsAreSmallerThanContainer()) 1073 return true; // No need to compact the crumbs more. 1074 return false; 1075 } 1076 1077 // Shrink crumbs one at a time by applying the shrinkingFunction until the crumbs 1078 // fit in the container or we run out of crumbs to shrink. 1079 if (direction) { 1080 // Crumbs are shrunk on only one side (based on direction) of the signifcant crumb. 1081 var index = (direction > 0 ? 0 : crumbs.childNodes.length - 1); 1082 while (index !== significantIndex) { 1083 if (shrinkCrumbAtIndex(index)) 1084 return true; 1085 index += (direction > 0 ? 1 : -1); 1086 } 1087 } else { 1088 // Crumbs are shrunk in order of descending distance from the signifcant crumb, 1089 // with a tie going to child crumbs. 1090 var startIndex = 0; 1091 var endIndex = crumbs.childNodes.length - 1; 1092 while (startIndex != significantIndex || endIndex != significantIndex) { 1093 var startDistance = significantIndex - startIndex; 1094 var endDistance = endIndex - significantIndex; 1095 if (startDistance >= endDistance) 1096 var index = startIndex++; 1097 else 1098 var index = endIndex--; 1099 if (shrinkCrumbAtIndex(index)) 1100 return true; 1101 } 1102 } 1103 1104 // We are not small enough yet, return false so the caller knows. 1105 return false; 1106 } 1107 1108 function coalesceCollapsedCrumbs() 1109 { 1110 var crumb = crumbs.firstChild; 1111 var collapsedRun = false; 1112 var newStartNeeded = false; 1113 var newEndNeeded = false; 1114 while (crumb) { 1115 var hidden = crumb.hasStyleClass("hidden"); 1116 if (!hidden) { 1117 var collapsed = crumb.hasStyleClass("collapsed"); 1118 if (collapsedRun && collapsed) { 1119 crumb.addStyleClass("hidden"); 1120 crumb.removeStyleClass("compact"); 1121 crumb.removeStyleClass("collapsed"); 1122 1123 if (crumb.hasStyleClass("start")) { 1124 crumb.removeStyleClass("start"); 1125 newStartNeeded = true; 1126 } 1127 1128 if (crumb.hasStyleClass("end")) { 1129 crumb.removeStyleClass("end"); 1130 newEndNeeded = true; 1131 } 1132 1133 continue; 1134 } 1135 1136 collapsedRun = collapsed; 1137 1138 if (newEndNeeded) { 1139 newEndNeeded = false; 1140 crumb.addStyleClass("end"); 1141 } 1142 } else 1143 collapsedRun = true; 1144 crumb = crumb.nextSibling; 1145 } 1146 1147 if (newStartNeeded) { 1148 crumb = crumbs.lastChild; 1149 while (crumb) { 1150 if (!crumb.hasStyleClass("hidden")) { 1151 crumb.addStyleClass("start"); 1152 break; 1153 } 1154 crumb = crumb.previousSibling; 1155 } 1156 } 1157 } 1158 1159 function compact(crumb) 1160 { 1161 if (crumb.hasStyleClass("hidden")) 1162 return; 1163 crumb.addStyleClass("compact"); 1164 } 1165 1166 function collapse(crumb, dontCoalesce) 1167 { 1168 if (crumb.hasStyleClass("hidden")) 1169 return; 1170 crumb.addStyleClass("collapsed"); 1171 crumb.removeStyleClass("compact"); 1172 if (!dontCoalesce) 1173 coalesceCollapsedCrumbs(); 1174 } 1175 1176 function compactDimmed(crumb) 1177 { 1178 if (crumb.hasStyleClass("dimmed")) 1179 compact(crumb); 1180 } 1181 1182 function collapseDimmed(crumb) 1183 { 1184 if (crumb.hasStyleClass("dimmed")) 1185 collapse(crumb); 1186 } 1187 1188 if (!focusedCrumb) { 1189 // When not focused on a crumb we can be biased and collapse less important 1190 // crumbs that the user might not care much about. 1191 1192 // Compact child crumbs. 1193 if (makeCrumbsSmaller(compact, ChildSide)) 1194 return; 1195 1196 // Collapse child crumbs. 1197 if (makeCrumbsSmaller(collapse, ChildSide)) 1198 return; 1199 1200 // Compact dimmed ancestor crumbs. 1201 if (makeCrumbsSmaller(compactDimmed, AncestorSide)) 1202 return; 1203 1204 // Collapse dimmed ancestor crumbs. 1205 if (makeCrumbsSmaller(collapseDimmed, AncestorSide)) 1206 return; 1207 } 1208 1209 // Compact ancestor crumbs, or from both sides if focused. 1210 if (makeCrumbsSmaller(compact, (focusedCrumb ? BothSides : AncestorSide))) 1211 return; 1212 1213 // Collapse ancestor crumbs, or from both sides if focused. 1214 if (makeCrumbsSmaller(collapse, (focusedCrumb ? BothSides : AncestorSide))) 1215 return; 1216 1217 if (!selectedCrumb) 1218 return; 1219 1220 // Compact the selected crumb. 1221 compact(selectedCrumb); 1222 if (crumbsAreSmallerThanContainer()) 1223 return; 1224 1225 // Collapse the selected crumb as a last resort. Pass true to prevent coalescing. 1226 collapse(selectedCrumb, true); 1227 }, 1228 1229 updateStyles: function(forceUpdate) 1230 { 1231 var stylesSidebarPane = this.sidebarPanes.styles; 1232 if (!stylesSidebarPane.expanded || !stylesSidebarPane.needsUpdate) 1233 return; 1234 1235 stylesSidebarPane.update(this.focusedDOMNode, null, forceUpdate); 1236 stylesSidebarPane.needsUpdate = false; 1237 }, 1238 1239 updateMetrics: function() 1240 { 1241 var metricsSidebarPane = this.sidebarPanes.metrics; 1242 if (!metricsSidebarPane.expanded || !metricsSidebarPane.needsUpdate) 1243 return; 1244 1245 metricsSidebarPane.update(this.focusedDOMNode); 1246 metricsSidebarPane.needsUpdate = false; 1247 }, 1248 1249 updateProperties: function() 1250 { 1251 var propertiesSidebarPane = this.sidebarPanes.properties; 1252 if (!propertiesSidebarPane.expanded || !propertiesSidebarPane.needsUpdate) 1253 return; 1254 1255 propertiesSidebarPane.update(this.focusedDOMNode); 1256 propertiesSidebarPane.needsUpdate = false; 1257 }, 1258 1259 handleKeyEvent: function(event) 1260 { 1261 this.treeOutline.handleKeyEvent(event); 1262 }, 1263 1264 handleCopyEvent: function(event) 1265 { 1266 // Don't prevent the normal copy if the user has a selection. 1267 if (!window.getSelection().isCollapsed) 1268 return; 1269 1270 switch (this.focusedDOMNode.nodeType) { 1271 case Node.ELEMENT_NODE: 1272 var data = this.focusedDOMNode.outerHTML; 1273 break; 1274 1275 case Node.COMMENT_NODE: 1276 var data = "<!--" + this.focusedDOMNode.nodeValue + "-->"; 1277 break; 1278 1279 default: 1280 case Node.TEXT_NODE: 1281 var data = this.focusedDOMNode.nodeValue; 1282 } 1283 1284 event.clipboardData.clearData(); 1285 event.preventDefault(); 1286 1287 if (data) 1288 event.clipboardData.setData("text/plain", data); 1289 }, 1290 1291 rightSidebarResizerDragStart: function(event) 1292 { 1293 WebInspector.elementDragStart(this.sidebarElement, this.rightSidebarResizerDrag.bind(this), this.rightSidebarResizerDragEnd.bind(this), event, "col-resize"); 1294 }, 1295 1296 rightSidebarResizerDragEnd: function(event) 1297 { 1298 WebInspector.elementDragEnd(event); 1299 }, 1300 1301 rightSidebarResizerDrag: function(event) 1302 { 1303 var x = event.pageX; 1304 var newWidth = Number.constrain(window.innerWidth - x, Preferences.minElementsSidebarWidth, window.innerWidth * 0.66); 1305 1306 this.sidebarElement.style.width = newWidth + "px"; 1307 this.contentElement.style.right = newWidth + "px"; 1308 this.sidebarResizeElement.style.right = (newWidth - 3) + "px"; 1309 1310 this.treeOutline.updateSelection(); 1311 1312 event.preventDefault(); 1313 }, 1314 1315 _nodeSearchButtonClicked: function(event) 1316 { 1317 InspectorController.toggleNodeSearch(); 1318 1319 if (InspectorController.searchingForNode()) 1320 this.nodeSearchButton.addStyleClass("toggled-on"); 1321 else 1322 this.nodeSearchButton.removeStyleClass("toggled-on"); 1323 } 1324} 1325 1326WebInspector.ElementsPanel.prototype.__proto__ = WebInspector.Panel.prototype; 1327