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