1 /* 2 * Copyright (C) 2007, 2008 Apple Inc. All rights reserved. 3 * Copyright (C) 2008 Matt Lilek <webkit (at) 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 31 /** 32 * @constructor 33 * @extends {TreeOutline} 34 * @param {boolean=} omitRootDOMNode 35 * @param {boolean=} selectEnabled 36 * @param {boolean=} showInElementsPanelEnabled 37 * @param {function(WebInspector.ContextMenu, WebInspector.DOMNode)=} contextMenuCallback 38 * @param {function(DOMAgent.NodeId, string, boolean)=} setPseudoClassCallback 39 */ 40 WebInspector.ElementsTreeOutline = function(omitRootDOMNode, selectEnabled, showInElementsPanelEnabled, contextMenuCallback, setPseudoClassCallback) 41 { 42 this.element = document.createElement("ol"); 43 this.element.className = "elements-tree-outline"; 44 this.element.addEventListener("mousedown", this._onmousedown.bind(this), false); 45 this.element.addEventListener("mousemove", this._onmousemove.bind(this), false); 46 this.element.addEventListener("mouseout", this._onmouseout.bind(this), false); 47 this.element.addEventListener("dragstart", this._ondragstart.bind(this), false); 48 this.element.addEventListener("dragover", this._ondragover.bind(this), false); 49 this.element.addEventListener("dragleave", this._ondragleave.bind(this), false); 50 this.element.addEventListener("drop", this._ondrop.bind(this), false); 51 this.element.addEventListener("dragend", this._ondragend.bind(this), false); 52 this.element.addEventListener("keydown", this._onkeydown.bind(this), false); 53 54 TreeOutline.call(this, this.element); 55 56 this._includeRootDOMNode = !omitRootDOMNode; 57 this._selectEnabled = selectEnabled; 58 this._showInElementsPanelEnabled = showInElementsPanelEnabled; 59 this._rootDOMNode = null; 60 this._selectDOMNode = null; 61 this._eventSupport = new WebInspector.Object(); 62 63 this._visible = false; 64 65 this.element.addEventListener("contextmenu", this._contextMenuEventFired.bind(this), true); 66 this._contextMenuCallback = contextMenuCallback; 67 this._setPseudoClassCallback = setPseudoClassCallback; 68 this._createNodeDecorators(); 69 } 70 71 WebInspector.ElementsTreeOutline.Events = { 72 SelectedNodeChanged: "SelectedNodeChanged" 73 } 74 75 WebInspector.ElementsTreeOutline.MappedCharToEntity = { 76 "\u00a0": "nbsp", 77 "\u2002": "ensp", 78 "\u2003": "emsp", 79 "\u2009": "thinsp", 80 "\u200a": "#8202", // Hairspace 81 "\u200b": "#8203", // ZWSP 82 "\u200c": "zwnj", 83 "\u200d": "zwj", 84 "\u200e": "lrm", 85 "\u200f": "rlm", 86 "\u202a": "#8234", // LRE 87 "\u202b": "#8235", // RLE 88 "\u202c": "#8236", // PDF 89 "\u202d": "#8237", // LRO 90 "\u202e": "#8238" // RLO 91 } 92 93 WebInspector.ElementsTreeOutline.prototype = { 94 _createNodeDecorators: function() 95 { 96 this._nodeDecorators = []; 97 this._nodeDecorators.push(new WebInspector.ElementsTreeOutline.PseudoStateDecorator()); 98 }, 99 100 wireToDomAgent: function() 101 { 102 this._elementsTreeUpdater = new WebInspector.ElementsTreeUpdater(this); 103 }, 104 105 setVisible: function(visible) 106 { 107 this._visible = visible; 108 if (!this._visible) 109 return; 110 111 this._updateModifiedNodes(); 112 if (this._selectedDOMNode) 113 this._revealAndSelectNode(this._selectedDOMNode, false); 114 }, 115 116 addEventListener: function(eventType, listener, thisObject) 117 { 118 this._eventSupport.addEventListener(eventType, listener, thisObject); 119 }, 120 121 removeEventListener: function(eventType, listener, thisObject) 122 { 123 this._eventSupport.removeEventListener(eventType, listener, thisObject); 124 }, 125 126 get rootDOMNode() 127 { 128 return this._rootDOMNode; 129 }, 130 131 set rootDOMNode(x) 132 { 133 if (this._rootDOMNode === x) 134 return; 135 136 this._rootDOMNode = x; 137 138 this._isXMLMimeType = x && x.isXMLNode(); 139 140 this.update(); 141 }, 142 143 get isXMLMimeType() 144 { 145 return this._isXMLMimeType; 146 }, 147 148 selectedDOMNode: function() 149 { 150 return this._selectedDOMNode; 151 }, 152 153 selectDOMNode: function(node, focus) 154 { 155 if (this._selectedDOMNode === node) { 156 this._revealAndSelectNode(node, !focus); 157 return; 158 } 159 160 this._selectedDOMNode = node; 161 this._revealAndSelectNode(node, !focus); 162 163 // The _revealAndSelectNode() method might find a different element if there is inlined text, 164 // and the select() call would change the selectedDOMNode and reenter this setter. So to 165 // avoid calling _selectedNodeChanged() twice, first check if _selectedDOMNode is the same 166 // node as the one passed in. 167 if (this._selectedDOMNode === node) 168 this._selectedNodeChanged(); 169 }, 170 171 /** 172 * @return {boolean} 173 */ 174 editing: function() 175 { 176 var node = this.selectedDOMNode(); 177 if (!node) 178 return false; 179 var treeElement = this.findTreeElement(node); 180 if (!treeElement) 181 return false; 182 return treeElement._editing || false; 183 }, 184 185 update: function() 186 { 187 var selectedNode = this.selectedTreeElement ? this.selectedTreeElement._node : null; 188 189 this.removeChildren(); 190 191 if (!this.rootDOMNode) 192 return; 193 194 var treeElement; 195 if (this._includeRootDOMNode) { 196 treeElement = new WebInspector.ElementsTreeElement(this.rootDOMNode); 197 treeElement.selectable = this._selectEnabled; 198 this.appendChild(treeElement); 199 } else { 200 // FIXME: this could use findTreeElement to reuse a tree element if it already exists 201 var node = this.rootDOMNode.firstChild; 202 while (node) { 203 treeElement = new WebInspector.ElementsTreeElement(node); 204 treeElement.selectable = this._selectEnabled; 205 this.appendChild(treeElement); 206 node = node.nextSibling; 207 } 208 } 209 210 if (selectedNode) 211 this._revealAndSelectNode(selectedNode, true); 212 }, 213 214 updateSelection: function() 215 { 216 if (!this.selectedTreeElement) 217 return; 218 var element = this.treeOutline.selectedTreeElement; 219 element.updateSelection(); 220 }, 221 222 /** 223 * @param {WebInspector.DOMNode} node 224 */ 225 updateOpenCloseTags: function(node) 226 { 227 var treeElement = this.findTreeElement(node); 228 if (treeElement) 229 treeElement.updateTitle(); 230 var children = treeElement.children; 231 var closingTagElement = children[children.length - 1]; 232 if (closingTagElement && closingTagElement._elementCloseTag) 233 closingTagElement.updateTitle(); 234 }, 235 236 _selectedNodeChanged: function() 237 { 238 this._eventSupport.dispatchEventToListeners(WebInspector.ElementsTreeOutline.Events.SelectedNodeChanged, this._selectedDOMNode); 239 }, 240 241 /** 242 * @param {WebInspector.DOMNode} node 243 */ 244 findTreeElement: function(node) 245 { 246 function isAncestorNode(ancestor, node) 247 { 248 return ancestor.isAncestor(node); 249 } 250 251 function parentNode(node) 252 { 253 return node.parentNode; 254 } 255 256 var treeElement = TreeOutline.prototype.findTreeElement.call(this, node, isAncestorNode, parentNode); 257 if (!treeElement && node.nodeType() === Node.TEXT_NODE) { 258 // The text node might have been inlined if it was short, so try to find the parent element. 259 treeElement = TreeOutline.prototype.findTreeElement.call(this, node.parentNode, isAncestorNode, parentNode); 260 } 261 262 return treeElement; 263 }, 264 265 /** 266 * @param {WebInspector.DOMNode} node 267 */ 268 createTreeElementFor: function(node) 269 { 270 var treeElement = this.findTreeElement(node); 271 if (treeElement) 272 return treeElement; 273 if (!node.parentNode) 274 return null; 275 276 treeElement = this.createTreeElementFor(node.parentNode); 277 return treeElement ? treeElement._showChild(node) : null; 278 }, 279 280 set suppressRevealAndSelect(x) 281 { 282 if (this._suppressRevealAndSelect === x) 283 return; 284 this._suppressRevealAndSelect = x; 285 }, 286 287 _revealAndSelectNode: function(node, omitFocus) 288 { 289 if (this._suppressRevealAndSelect) 290 return; 291 292 if (!this._includeRootDOMNode && node === this.rootDOMNode && this.rootDOMNode) 293 node = this.rootDOMNode.firstChild; 294 if (!node) 295 return; 296 var treeElement = this.createTreeElementFor(node); 297 if (!treeElement) 298 return; 299 300 treeElement.revealAndSelect(omitFocus); 301 }, 302 303 _treeElementFromEvent: function(event) 304 { 305 var scrollContainer = this.element.parentElement; 306 307 // We choose this X coordinate based on the knowledge that our list 308 // items extend at least to the right edge of the outer <ol> container. 309 // In the no-word-wrap mode the outer <ol> may be wider than the tree container 310 // (and partially hidden), in which case we are left to use only its right boundary. 311 var x = scrollContainer.totalOffsetLeft() + scrollContainer.offsetWidth - 36; 312 313 var y = event.pageY; 314 315 // Our list items have 1-pixel cracks between them vertically. We avoid 316 // the cracks by checking slightly above and slightly below the mouse 317 // and seeing if we hit the same element each time. 318 var elementUnderMouse = this.treeElementFromPoint(x, y); 319 var elementAboveMouse = this.treeElementFromPoint(x, y - 2); 320 var element; 321 if (elementUnderMouse === elementAboveMouse) 322 element = elementUnderMouse; 323 else 324 element = this.treeElementFromPoint(x, y + 2); 325 326 return element; 327 }, 328 329 _onmousedown: function(event) 330 { 331 var element = this._treeElementFromEvent(event); 332 333 if (!element || element.isEventWithinDisclosureTriangle(event)) 334 return; 335 336 element.select(); 337 }, 338 339 _onmousemove: function(event) 340 { 341 var element = this._treeElementFromEvent(event); 342 if (element && this._previousHoveredElement === element) 343 return; 344 345 if (this._previousHoveredElement) { 346 this._previousHoveredElement.hovered = false; 347 delete this._previousHoveredElement; 348 } 349 350 if (element) { 351 element.hovered = true; 352 this._previousHoveredElement = element; 353 } 354 355 WebInspector.domAgent.highlightDOMNode(element && element._node ? element._node.id : 0); 356 }, 357 358 _onmouseout: function(event) 359 { 360 var nodeUnderMouse = document.elementFromPoint(event.pageX, event.pageY); 361 if (nodeUnderMouse && nodeUnderMouse.isDescendant(this.element)) 362 return; 363 364 if (this._previousHoveredElement) { 365 this._previousHoveredElement.hovered = false; 366 delete this._previousHoveredElement; 367 } 368 369 WebInspector.domAgent.hideDOMNodeHighlight(); 370 }, 371 372 _ondragstart: function(event) 373 { 374 if (!window.getSelection().isCollapsed) 375 return false; 376 if (event.target.nodeName === "A") 377 return false; 378 379 var treeElement = this._treeElementFromEvent(event); 380 if (!treeElement) 381 return false; 382 383 if (!this._isValidDragSourceOrTarget(treeElement)) 384 return false; 385 386 if (treeElement._node.nodeName() === "BODY" || treeElement._node.nodeName() === "HEAD") 387 return false; 388 389 event.dataTransfer.setData("text/plain", treeElement.listItemElement.textContent); 390 event.dataTransfer.effectAllowed = "copyMove"; 391 this._treeElementBeingDragged = treeElement; 392 393 WebInspector.domAgent.hideDOMNodeHighlight(); 394 395 return true; 396 }, 397 398 _ondragover: function(event) 399 { 400 if (!this._treeElementBeingDragged) 401 return false; 402 403 var treeElement = this._treeElementFromEvent(event); 404 if (!this._isValidDragSourceOrTarget(treeElement)) 405 return false; 406 407 var node = treeElement._node; 408 while (node) { 409 if (node === this._treeElementBeingDragged._node) 410 return false; 411 node = node.parentNode; 412 } 413 414 treeElement.updateSelection(); 415 treeElement.listItemElement.addStyleClass("elements-drag-over"); 416 this._dragOverTreeElement = treeElement; 417 event.preventDefault(); 418 event.dataTransfer.dropEffect = 'move'; 419 return false; 420 }, 421 422 _ondragleave: function(event) 423 { 424 this._clearDragOverTreeElementMarker(); 425 event.preventDefault(); 426 return false; 427 }, 428 429 _isValidDragSourceOrTarget: function(treeElement) 430 { 431 if (!treeElement) 432 return false; 433 434 var node = treeElement.representedObject; 435 if (!(node instanceof WebInspector.DOMNode)) 436 return false; 437 438 if (!node.parentNode || node.parentNode.nodeType() !== Node.ELEMENT_NODE) 439 return false; 440 441 return true; 442 }, 443 444 _ondrop: function(event) 445 { 446 event.preventDefault(); 447 var treeElement = this._treeElementFromEvent(event); 448 if (treeElement) 449 this._doMove(treeElement); 450 }, 451 452 _doMove: function(treeElement) 453 { 454 if (!this._treeElementBeingDragged) 455 return; 456 457 var parentNode; 458 var anchorNode; 459 460 if (treeElement._elementCloseTag) { 461 // Drop onto closing tag -> insert as last child. 462 parentNode = treeElement._node; 463 } else { 464 var dragTargetNode = treeElement._node; 465 parentNode = dragTargetNode.parentNode; 466 anchorNode = dragTargetNode; 467 } 468 469 var wasExpanded = this._treeElementBeingDragged.expanded; 470 this._treeElementBeingDragged._node.moveTo(parentNode, anchorNode, this._selectNodeAfterEdit.bind(this, null, wasExpanded)); 471 472 delete this._treeElementBeingDragged; 473 }, 474 475 _ondragend: function(event) 476 { 477 event.preventDefault(); 478 this._clearDragOverTreeElementMarker(); 479 delete this._treeElementBeingDragged; 480 }, 481 482 _clearDragOverTreeElementMarker: function() 483 { 484 if (this._dragOverTreeElement) { 485 this._dragOverTreeElement.updateSelection(); 486 this._dragOverTreeElement.listItemElement.removeStyleClass("elements-drag-over"); 487 delete this._dragOverTreeElement; 488 } 489 }, 490 491 /** 492 * @param {Event} event 493 */ 494 _onkeydown: function(event) 495 { 496 var keyboardEvent = /** @type {KeyboardEvent} */ (event); 497 var node = this.selectedDOMNode(); 498 var treeElement = this.getCachedTreeElement(node); 499 if (!treeElement) 500 return; 501 502 if (!treeElement._editing && WebInspector.KeyboardShortcut.hasNoModifiers(keyboardEvent) && keyboardEvent.keyCode === WebInspector.KeyboardShortcut.Keys.H.code) { 503 this._toggleHideShortcut(node); 504 event.consume(true); 505 return; 506 } 507 }, 508 509 _contextMenuEventFired: function(event) 510 { 511 if (!this._showInElementsPanelEnabled) 512 return; 513 514 var treeElement = this._treeElementFromEvent(event); 515 if (!treeElement) 516 return; 517 518 function focusElement() 519 { 520 // Force elements module load. 521 WebInspector.showPanel("elements"); 522 WebInspector.domAgent.inspectElement(treeElement._node.id); 523 } 524 var contextMenu = new WebInspector.ContextMenu(event); 525 contextMenu.appendItem(WebInspector.UIString(WebInspector.useLowerCaseMenuTitles() ? "Reveal in Elements panel" : "Reveal in Elements Panel"), focusElement.bind(this)); 526 contextMenu.show(); 527 }, 528 529 populateContextMenu: function(contextMenu, event) 530 { 531 var treeElement = this._treeElementFromEvent(event); 532 if (!treeElement) 533 return; 534 535 var isTag = treeElement._node.nodeType() === Node.ELEMENT_NODE; 536 var textNode = event.target.enclosingNodeOrSelfWithClass("webkit-html-text-node"); 537 if (textNode && textNode.hasStyleClass("bogus")) 538 textNode = null; 539 var commentNode = event.target.enclosingNodeOrSelfWithClass("webkit-html-comment"); 540 contextMenu.appendApplicableItems(event.target); 541 if (textNode) { 542 contextMenu.appendSeparator(); 543 treeElement._populateTextContextMenu(contextMenu, textNode); 544 } else if (isTag) { 545 contextMenu.appendSeparator(); 546 treeElement._populateTagContextMenu(contextMenu, event); 547 } else if (commentNode) { 548 contextMenu.appendSeparator(); 549 treeElement._populateNodeContextMenu(contextMenu, textNode); 550 } 551 }, 552 553 _updateModifiedNodes: function() 554 { 555 if (this._elementsTreeUpdater) 556 this._elementsTreeUpdater._updateModifiedNodes(); 557 }, 558 559 _populateContextMenu: function(contextMenu, node) 560 { 561 if (this._contextMenuCallback) 562 this._contextMenuCallback(contextMenu, node); 563 }, 564 565 handleShortcut: function(event) 566 { 567 var node = this.selectedDOMNode(); 568 var treeElement = this.getCachedTreeElement(node); 569 if (!node || !treeElement) 570 return; 571 572 if (event.keyIdentifier === "F2") { 573 this._toggleEditAsHTML(node); 574 event.handled = true; 575 return; 576 } 577 578 if (WebInspector.KeyboardShortcut.eventHasCtrlOrMeta(event) && node.parentNode) { 579 if (event.keyIdentifier === "Up" && node.previousSibling) { 580 node.moveTo(node.parentNode, node.previousSibling, this._selectNodeAfterEdit.bind(this, null, treeElement.expanded)); 581 event.handled = true; 582 return; 583 } 584 if (event.keyIdentifier === "Down" && node.nextSibling) { 585 node.moveTo(node.parentNode, node.nextSibling.nextSibling, this._selectNodeAfterEdit.bind(this, null, treeElement.expanded)); 586 event.handled = true; 587 return; 588 } 589 } 590 }, 591 592 _toggleEditAsHTML: function(node) 593 { 594 var treeElement = this.getCachedTreeElement(node); 595 if (!treeElement) 596 return; 597 598 if (treeElement._editing && treeElement._htmlEditElement && WebInspector.isBeingEdited(treeElement._htmlEditElement)) 599 treeElement._editing.commit(); 600 else 601 treeElement._editAsHTML(); 602 }, 603 604 _selectNodeAfterEdit: function(fallbackNode, wasExpanded, error, nodeId) 605 { 606 if (error) 607 return; 608 609 // Select it and expand if necessary. We force tree update so that it processes dom events and is up to date. 610 this._updateModifiedNodes(); 611 612 var newNode = WebInspector.domAgent.nodeForId(nodeId) || fallbackNode; 613 if (!newNode) 614 return; 615 616 this.selectDOMNode(newNode, true); 617 618 var newTreeItem = this.findTreeElement(newNode); 619 if (wasExpanded) { 620 if (newTreeItem) 621 newTreeItem.expand(); 622 } 623 return newTreeItem; 624 }, 625 626 /** 627 * Runs a script on the node's remote object that toggles a class name on 628 * the node and injects a stylesheet into the head of the node's document 629 * containing a rule to set "visibility: hidden" on the class and all it's 630 * ancestors. 631 * 632 * @param {WebInspector.DOMNode} node 633 * @param {function(?WebInspector.RemoteObject)=} userCallback 634 */ 635 _toggleHideShortcut: function(node, userCallback) 636 { 637 function resolvedNode(object) 638 { 639 if (!object) 640 return; 641 642 function toggleClassAndInjectStyleRule() 643 { 644 const className = "__web-inspector-hide-shortcut__"; 645 const styleTagId = "__web-inspector-hide-shortcut-style__"; 646 const styleRule = ".__web-inspector-hide-shortcut__, .__web-inspector-hide-shortcut__ * { visibility: hidden !important; }"; 647 648 this.classList.toggle(className); 649 650 var style = document.head.querySelector("style#" + styleTagId); 651 if (style) 652 return; 653 654 style = document.createElement("style"); 655 style.id = styleTagId; 656 style.type = "text/css"; 657 style.innerHTML = styleRule; 658 document.head.appendChild(style); 659 } 660 661 object.callFunction(toggleClassAndInjectStyleRule, undefined, userCallback); 662 object.release(); 663 } 664 665 WebInspector.RemoteObject.resolveNode(node, "", resolvedNode); 666 }, 667 668 __proto__: TreeOutline.prototype 669 } 670 671 WebInspector.ElementsTreeOutline.showShadowDOM = function() 672 { 673 return WebInspector.settings.showShadowDOM.get() || WebInspector.ElementsTreeOutline["showShadowDOMForTest"]; 674 } 675 676 677 /** 678 * @interface 679 */ 680 WebInspector.ElementsTreeOutline.ElementDecorator = function() 681 { 682 } 683 684 WebInspector.ElementsTreeOutline.ElementDecorator.prototype = { 685 /** 686 * @param {WebInspector.DOMNode} node 687 */ 688 decorate: function(node) 689 { 690 }, 691 692 /** 693 * @param {WebInspector.DOMNode} node 694 */ 695 decorateAncestor: function(node) 696 { 697 } 698 } 699 700 /** 701 * @constructor 702 * @implements {WebInspector.ElementsTreeOutline.ElementDecorator} 703 */ 704 WebInspector.ElementsTreeOutline.PseudoStateDecorator = function() 705 { 706 WebInspector.ElementsTreeOutline.ElementDecorator.call(this); 707 } 708 709 WebInspector.ElementsTreeOutline.PseudoStateDecorator.PropertyName = "pseudoState"; 710 711 WebInspector.ElementsTreeOutline.PseudoStateDecorator.prototype = { 712 decorate: function(node) 713 { 714 if (node.nodeType() !== Node.ELEMENT_NODE) 715 return null; 716 var propertyValue = node.getUserProperty(WebInspector.ElementsTreeOutline.PseudoStateDecorator.PropertyName); 717 if (!propertyValue) 718 return null; 719 return WebInspector.UIString("Element state: %s", ":" + propertyValue.join(", :")); 720 }, 721 722 decorateAncestor: function(node) 723 { 724 if (node.nodeType() !== Node.ELEMENT_NODE) 725 return null; 726 727 var descendantCount = node.descendantUserPropertyCount(WebInspector.ElementsTreeOutline.PseudoStateDecorator.PropertyName); 728 if (!descendantCount) 729 return null; 730 if (descendantCount === 1) 731 return WebInspector.UIString("%d descendant with forced state", descendantCount); 732 return WebInspector.UIString("%d descendants with forced state", descendantCount); 733 }, 734 735 __proto__: WebInspector.ElementsTreeOutline.ElementDecorator.prototype 736 } 737 738 /** 739 * @constructor 740 * @extends {TreeElement} 741 * @param {boolean=} elementCloseTag 742 */ 743 WebInspector.ElementsTreeElement = function(node, elementCloseTag) 744 { 745 // The title will be updated in onattach. 746 TreeElement.call(this, "", node); 747 this._node = node; 748 749 this._elementCloseTag = elementCloseTag; 750 this._updateHasChildren(); 751 752 if (this._node.nodeType() == Node.ELEMENT_NODE && !elementCloseTag) 753 this._canAddAttributes = true; 754 this._searchQuery = null; 755 this._expandedChildrenLimit = WebInspector.ElementsTreeElement.InitialChildrenLimit; 756 } 757 758 WebInspector.ElementsTreeElement.InitialChildrenLimit = 500; 759 760 // A union of HTML4 and HTML5-Draft elements that explicitly 761 // or implicitly (for HTML5) forbid the closing tag. 762 // FIXME: Revise once HTML5 Final is published. 763 WebInspector.ElementsTreeElement.ForbiddenClosingTagElements = [ 764 "area", "base", "basefont", "br", "canvas", "col", "command", "embed", "frame", 765 "hr", "img", "input", "isindex", "keygen", "link", "meta", "param", "source" 766 ].keySet(); 767 768 // These tags we do not allow editing their tag name. 769 WebInspector.ElementsTreeElement.EditTagBlacklist = [ 770 "html", "head", "body" 771 ].keySet(); 772 773 WebInspector.ElementsTreeElement.prototype = { 774 highlightSearchResults: function(searchQuery) 775 { 776 if (this._searchQuery !== searchQuery) { 777 this._updateSearchHighlight(false); 778 delete this._highlightResult; // A new search query. 779 } 780 781 this._searchQuery = searchQuery; 782 this._searchHighlightsVisible = true; 783 this.updateTitle(true); 784 }, 785 786 hideSearchHighlights: function() 787 { 788 delete this._searchHighlightsVisible; 789 this._updateSearchHighlight(false); 790 }, 791 792 _updateSearchHighlight: function(show) 793 { 794 if (!this._highlightResult) 795 return; 796 797 function updateEntryShow(entry) 798 { 799 switch (entry.type) { 800 case "added": 801 entry.parent.insertBefore(entry.node, entry.nextSibling); 802 break; 803 case "changed": 804 entry.node.textContent = entry.newText; 805 break; 806 } 807 } 808 809 function updateEntryHide(entry) 810 { 811 switch (entry.type) { 812 case "added": 813 entry.node.remove(); 814 break; 815 case "changed": 816 entry.node.textContent = entry.oldText; 817 break; 818 } 819 } 820 821 // Preserve the semantic of node by following the order of updates for hide and show. 822 if (show) { 823 for (var i = 0, size = this._highlightResult.length; i < size; ++i) 824 updateEntryShow(this._highlightResult[i]); 825 } else { 826 for (var i = (this._highlightResult.length - 1); i >= 0; --i) 827 updateEntryHide(this._highlightResult[i]); 828 } 829 }, 830 831 get hovered() 832 { 833 return this._hovered; 834 }, 835 836 set hovered(x) 837 { 838 if (this._hovered === x) 839 return; 840 841 this._hovered = x; 842 843 if (this.listItemElement) { 844 if (x) { 845 this.updateSelection(); 846 this.listItemElement.addStyleClass("hovered"); 847 } else { 848 this.listItemElement.removeStyleClass("hovered"); 849 } 850 } 851 }, 852 853 get expandedChildrenLimit() 854 { 855 return this._expandedChildrenLimit; 856 }, 857 858 set expandedChildrenLimit(x) 859 { 860 if (this._expandedChildrenLimit === x) 861 return; 862 863 this._expandedChildrenLimit = x; 864 if (this.treeOutline && !this._updateChildrenInProgress) 865 this._updateChildren(true); 866 }, 867 868 get expandedChildCount() 869 { 870 var count = this.children.length; 871 if (count && this.children[count - 1]._elementCloseTag) 872 count--; 873 if (count && this.children[count - 1].expandAllButton) 874 count--; 875 return count; 876 }, 877 878 /** 879 * @param {WebInspector.DOMNode} child 880 * @return {?WebInspector.ElementsTreeElement} 881 */ 882 _showChild: function(child) 883 { 884 if (this._elementCloseTag) 885 return null; 886 887 var index = this._visibleChildren().indexOf(child); 888 if (index === -1) 889 return null; 890 891 if (index >= this.expandedChildrenLimit) { 892 this._expandedChildrenLimit = index + 1; 893 this._updateChildren(true); 894 } 895 896 // Whether index-th child is visible in the children tree 897 return this.expandedChildCount > index ? this.children[index] : null; 898 }, 899 900 updateSelection: function() 901 { 902 var listItemElement = this.listItemElement; 903 if (!listItemElement) 904 return; 905 906 if (!this._readyToUpdateSelection) { 907 if (document.body.offsetWidth > 0) 908 this._readyToUpdateSelection = true; 909 else { 910 // The stylesheet hasn't loaded yet or the window is closed, 911 // so we can't calculate what we need. Return early. 912 return; 913 } 914 } 915 916 if (!this.selectionElement) { 917 this.selectionElement = document.createElement("div"); 918 this.selectionElement.className = "selection selected"; 919 listItemElement.insertBefore(this.selectionElement, listItemElement.firstChild); 920 } 921 922 this.selectionElement.style.height = listItemElement.offsetHeight + "px"; 923 }, 924 925 onattach: function() 926 { 927 if (this._hovered) { 928 this.updateSelection(); 929 this.listItemElement.addStyleClass("hovered"); 930 } 931 932 this.updateTitle(); 933 this._preventFollowingLinksOnDoubleClick(); 934 this.listItemElement.draggable = true; 935 }, 936 937 _preventFollowingLinksOnDoubleClick: function() 938 { 939 var links = this.listItemElement.querySelectorAll("li > .webkit-html-tag > .webkit-html-attribute > .webkit-html-external-link, li > .webkit-html-tag > .webkit-html-attribute > .webkit-html-resource-link"); 940 if (!links) 941 return; 942 943 for (var i = 0; i < links.length; ++i) 944 links[i].preventFollowOnDoubleClick = true; 945 }, 946 947 onpopulate: function() 948 { 949 if (this.children.length || this._showInlineText() || this._elementCloseTag) 950 return; 951 952 this.updateChildren(); 953 }, 954 955 /** 956 * @param {boolean=} fullRefresh 957 */ 958 updateChildren: function(fullRefresh) 959 { 960 if (this._elementCloseTag) 961 return; 962 this._node.getChildNodes(this._updateChildren.bind(this, fullRefresh)); 963 }, 964 965 /** 966 * @param {boolean=} closingTag 967 */ 968 insertChildElement: function(child, index, closingTag) 969 { 970 var newElement = new WebInspector.ElementsTreeElement(child, closingTag); 971 newElement.selectable = this.treeOutline._selectEnabled; 972 this.insertChild(newElement, index); 973 return newElement; 974 }, 975 976 moveChild: function(child, targetIndex) 977 { 978 var wasSelected = child.selected; 979 this.removeChild(child); 980 this.insertChild(child, targetIndex); 981 if (wasSelected) 982 child.select(); 983 }, 984 985 /** 986 * @param {boolean=} fullRefresh 987 */ 988 _updateChildren: function(fullRefresh) 989 { 990 if (this._updateChildrenInProgress || !this.treeOutline._visible) 991 return; 992 993 this._updateChildrenInProgress = true; 994 var selectedNode = this.treeOutline.selectedDOMNode(); 995 var originalScrollTop = 0; 996 if (fullRefresh) { 997 var treeOutlineContainerElement = this.treeOutline.element.parentNode; 998 originalScrollTop = treeOutlineContainerElement.scrollTop; 999 var selectedTreeElement = this.treeOutline.selectedTreeElement; 1000 if (selectedTreeElement && selectedTreeElement.hasAncestor(this)) 1001 this.select(); 1002 this.removeChildren(); 1003 } 1004 1005 var treeElement = this; 1006 var treeChildIndex = 0; 1007 var elementToSelect; 1008 1009 function updateChildrenOfNode() 1010 { 1011 var treeOutline = treeElement.treeOutline; 1012 var visibleChildren = this._visibleChildren(); 1013 1014 for (var i = 0; i < visibleChildren.length; ++i) { 1015 var child = visibleChildren[i]; 1016 var currentTreeElement = treeElement.children[treeChildIndex]; 1017 if (!currentTreeElement || currentTreeElement._node !== child) { 1018 // Find any existing element that is later in the children list. 1019 var existingTreeElement = null; 1020 for (var j = (treeChildIndex + 1), size = treeElement.expandedChildCount; j < size; ++j) { 1021 if (treeElement.children[j]._node === child) { 1022 existingTreeElement = treeElement.children[j]; 1023 break; 1024 } 1025 } 1026 1027 if (existingTreeElement && existingTreeElement.parent === treeElement) { 1028 // If an existing element was found and it has the same parent, just move it. 1029 treeElement.moveChild(existingTreeElement, treeChildIndex); 1030 } else { 1031 // No existing element found, insert a new element. 1032 if (treeChildIndex < treeElement.expandedChildrenLimit) { 1033 var newElement = treeElement.insertChildElement(child, treeChildIndex); 1034 if (child === selectedNode) 1035 elementToSelect = newElement; 1036 if (treeElement.expandedChildCount > treeElement.expandedChildrenLimit) 1037 treeElement.expandedChildrenLimit++; 1038 } 1039 } 1040 } 1041 1042 ++treeChildIndex; 1043 } 1044 } 1045 1046 // Remove any tree elements that no longer have this node (or this node's contentDocument) as their parent. 1047 for (var i = (this.children.length - 1); i >= 0; --i) { 1048 var currentChild = this.children[i]; 1049 var currentNode = currentChild._node; 1050 if (!currentNode) 1051 continue; 1052 var currentParentNode = currentNode.parentNode; 1053 1054 if (currentParentNode === this._node) 1055 continue; 1056 1057 var selectedTreeElement = this.treeOutline.selectedTreeElement; 1058 if (selectedTreeElement && (selectedTreeElement === currentChild || selectedTreeElement.hasAncestor(currentChild))) 1059 this.select(); 1060 1061 this.removeChildAtIndex(i); 1062 } 1063 1064 updateChildrenOfNode.call(this); 1065 this._adjustCollapsedRange(); 1066 1067 var lastChild = this.children[this.children.length - 1]; 1068 if (this._node.nodeType() == Node.ELEMENT_NODE && (!lastChild || !lastChild._elementCloseTag)) 1069 this.insertChildElement(this._node, this.children.length, true); 1070 1071 // We want to restore the original selection and tree scroll position after a full refresh, if possible. 1072 if (fullRefresh && elementToSelect) { 1073 elementToSelect.select(); 1074 if (treeOutlineContainerElement && originalScrollTop <= treeOutlineContainerElement.scrollHeight) 1075 treeOutlineContainerElement.scrollTop = originalScrollTop; 1076 } 1077 1078 delete this._updateChildrenInProgress; 1079 }, 1080 1081 _adjustCollapsedRange: function() 1082 { 1083 var visibleChildren = this._visibleChildren(); 1084 // Ensure precondition: only the tree elements for node children are found in the tree 1085 // (not the Expand All button or the closing tag). 1086 if (this.expandAllButtonElement && this.expandAllButtonElement.__treeElement.parent) 1087 this.removeChild(this.expandAllButtonElement.__treeElement); 1088 1089 const childNodeCount = visibleChildren.length; 1090 1091 // In case some nodes from the expanded range were removed, pull some nodes from the collapsed range into the expanded range at the bottom. 1092 for (var i = this.expandedChildCount, limit = Math.min(this.expandedChildrenLimit, childNodeCount); i < limit; ++i) 1093 this.insertChildElement(visibleChildren[i], i); 1094 1095 const expandedChildCount = this.expandedChildCount; 1096 if (childNodeCount > this.expandedChildCount) { 1097 var targetButtonIndex = expandedChildCount; 1098 if (!this.expandAllButtonElement) { 1099 var button = document.createElement("button"); 1100 button.className = "show-all-nodes"; 1101 button.value = ""; 1102 var item = new TreeElement(button, null, false); 1103 item.selectable = false; 1104 item.expandAllButton = true; 1105 this.insertChild(item, targetButtonIndex); 1106 this.expandAllButtonElement = item.listItemElement.firstChild; 1107 this.expandAllButtonElement.__treeElement = item; 1108 this.expandAllButtonElement.addEventListener("click", this.handleLoadAllChildren.bind(this), false); 1109 } else if (!this.expandAllButtonElement.__treeElement.parent) 1110 this.insertChild(this.expandAllButtonElement.__treeElement, targetButtonIndex); 1111 this.expandAllButtonElement.textContent = WebInspector.UIString("Show All Nodes (%d More)", childNodeCount - expandedChildCount); 1112 } else if (this.expandAllButtonElement) 1113 delete this.expandAllButtonElement; 1114 }, 1115 1116 handleLoadAllChildren: function() 1117 { 1118 this.expandedChildrenLimit = Math.max(this._visibleChildCount(), this.expandedChildrenLimit + WebInspector.ElementsTreeElement.InitialChildrenLimit); 1119 }, 1120 1121 expandRecursively: function() 1122 { 1123 function callback() 1124 { 1125 TreeElement.prototype.expandRecursively.call(this, Number.MAX_VALUE); 1126 } 1127 1128 this._node.getSubtree(-1, callback.bind(this)); 1129 }, 1130 1131 onexpand: function() 1132 { 1133 if (this._elementCloseTag) 1134 return; 1135 1136 this.updateTitle(); 1137 this.treeOutline.updateSelection(); 1138 }, 1139 1140 oncollapse: function() 1141 { 1142 if (this._elementCloseTag) 1143 return; 1144 1145 this.updateTitle(); 1146 this.treeOutline.updateSelection(); 1147 }, 1148 1149 onreveal: function() 1150 { 1151 if (this.listItemElement) { 1152 var tagSpans = this.listItemElement.getElementsByClassName("webkit-html-tag-name"); 1153 if (tagSpans.length) 1154 tagSpans[0].scrollIntoViewIfNeeded(false); 1155 else 1156 this.listItemElement.scrollIntoViewIfNeeded(false); 1157 } 1158 }, 1159 1160 onselect: function(selectedByUser) 1161 { 1162 this.treeOutline.suppressRevealAndSelect = true; 1163 this.treeOutline.selectDOMNode(this._node, selectedByUser); 1164 if (selectedByUser) 1165 WebInspector.domAgent.highlightDOMNode(this._node.id); 1166 this.updateSelection(); 1167 this.treeOutline.suppressRevealAndSelect = false; 1168 return true; 1169 }, 1170 1171 ondelete: function() 1172 { 1173 var startTagTreeElement = this.treeOutline.findTreeElement(this._node); 1174 startTagTreeElement ? startTagTreeElement.remove() : this.remove(); 1175 return true; 1176 }, 1177 1178 onenter: function() 1179 { 1180 // On Enter or Return start editing the first attribute 1181 // or create a new attribute on the selected element. 1182 if (this._editing) 1183 return false; 1184 1185 this._startEditing(); 1186 1187 // prevent a newline from being immediately inserted 1188 return true; 1189 }, 1190 1191 selectOnMouseDown: function(event) 1192 { 1193 TreeElement.prototype.selectOnMouseDown.call(this, event); 1194 1195 if (this._editing) 1196 return; 1197 1198 if (this.treeOutline._showInElementsPanelEnabled) { 1199 WebInspector.showPanel("elements"); 1200 this.treeOutline.selectDOMNode(this._node, true); 1201 } 1202 1203 // Prevent selecting the nearest word on double click. 1204 if (event.detail >= 2) 1205 event.preventDefault(); 1206 }, 1207 1208 ondblclick: function(event) 1209 { 1210 if (this._editing || this._elementCloseTag) 1211 return; 1212 1213 if (this._startEditingTarget(event.target)) 1214 return; 1215 1216 if (this.hasChildren && !this.expanded) 1217 this.expand(); 1218 }, 1219 1220 _insertInLastAttributePosition: function(tag, node) 1221 { 1222 if (tag.getElementsByClassName("webkit-html-attribute").length > 0) 1223 tag.insertBefore(node, tag.lastChild); 1224 else { 1225 var nodeName = tag.textContent.match(/^<(.*?)>$/)[1]; 1226 tag.textContent = ''; 1227 tag.appendChild(document.createTextNode('<'+nodeName)); 1228 tag.appendChild(node); 1229 tag.appendChild(document.createTextNode('>')); 1230 } 1231 1232 this.updateSelection(); 1233 }, 1234 1235 _startEditingTarget: function(eventTarget) 1236 { 1237 if (this.treeOutline.selectedDOMNode() != this._node) 1238 return; 1239 1240 if (this._node.nodeType() != Node.ELEMENT_NODE && this._node.nodeType() != Node.TEXT_NODE) 1241 return false; 1242 1243 var textNode = eventTarget.enclosingNodeOrSelfWithClass("webkit-html-text-node"); 1244 if (textNode) 1245 return this._startEditingTextNode(textNode); 1246 1247 var attribute = eventTarget.enclosingNodeOrSelfWithClass("webkit-html-attribute"); 1248 if (attribute) 1249 return this._startEditingAttribute(attribute, eventTarget); 1250 1251 var tagName = eventTarget.enclosingNodeOrSelfWithClass("webkit-html-tag-name"); 1252 if (tagName) 1253 return this._startEditingTagName(tagName); 1254 1255 var newAttribute = eventTarget.enclosingNodeOrSelfWithClass("add-attribute"); 1256 if (newAttribute) 1257 return this._addNewAttribute(); 1258 1259 return false; 1260 }, 1261 1262 _populateTagContextMenu: function(contextMenu, event) 1263 { 1264 var attribute = event.target.enclosingNodeOrSelfWithClass("webkit-html-attribute"); 1265 var newAttribute = event.target.enclosingNodeOrSelfWithClass("add-attribute"); 1266 1267 // Add attribute-related actions. 1268 var treeElement = this._elementCloseTag ? this.treeOutline.findTreeElement(this._node) : this; 1269 contextMenu.appendItem(WebInspector.UIString(WebInspector.useLowerCaseMenuTitles() ? "Add attribute" : "Add Attribute"), this._addNewAttribute.bind(treeElement)); 1270 if (attribute && !newAttribute) 1271 contextMenu.appendItem(WebInspector.UIString(WebInspector.useLowerCaseMenuTitles() ? "Edit attribute" : "Edit Attribute"), this._startEditingAttribute.bind(this, attribute, event.target)); 1272 contextMenu.appendSeparator(); 1273 if (this.treeOutline._setPseudoClassCallback) { 1274 var pseudoSubMenu = contextMenu.appendSubMenuItem(WebInspector.UIString(WebInspector.useLowerCaseMenuTitles() ? "Force element state" : "Force Element State")); 1275 this._populateForcedPseudoStateItems(pseudoSubMenu); 1276 contextMenu.appendSeparator(); 1277 } 1278 1279 this._populateNodeContextMenu(contextMenu); 1280 this.treeOutline._populateContextMenu(contextMenu, this._node); 1281 1282 contextMenu.appendSeparator(); 1283 contextMenu.appendItem(WebInspector.UIString(WebInspector.useLowerCaseMenuTitles() ? "Scroll into view" : "Scroll into View"), this._scrollIntoView.bind(this)); 1284 }, 1285 1286 _populateForcedPseudoStateItems: function(subMenu) 1287 { 1288 const pseudoClasses = ["active", "hover", "focus", "visited"]; 1289 var node = this._node; 1290 var forcedPseudoState = (node ? node.getUserProperty("pseudoState") : null) || []; 1291 for (var i = 0; i < pseudoClasses.length; ++i) { 1292 var pseudoClassForced = forcedPseudoState.indexOf(pseudoClasses[i]) >= 0; 1293 subMenu.appendCheckboxItem(":" + pseudoClasses[i], this.treeOutline._setPseudoClassCallback.bind(null, node.id, pseudoClasses[i], !pseudoClassForced), pseudoClassForced, false); 1294 } 1295 }, 1296 1297 _populateTextContextMenu: function(contextMenu, textNode) 1298 { 1299 contextMenu.appendItem(WebInspector.UIString(WebInspector.useLowerCaseMenuTitles() ? "Edit text" : "Edit Text"), this._startEditingTextNode.bind(this, textNode)); 1300 this._populateNodeContextMenu(contextMenu); 1301 }, 1302 1303 _populateNodeContextMenu: function(contextMenu) 1304 { 1305 // Add free-form node-related actions. 1306 var openTagElement = this.treeOutline.getCachedTreeElement(this.representedObject) || this; 1307 contextMenu.appendItem(WebInspector.UIString("Edit as HTML"), openTagElement._editAsHTML.bind(openTagElement)); 1308 contextMenu.appendItem(WebInspector.UIString("Copy as HTML"), this._copyHTML.bind(this)); 1309 contextMenu.appendItem(WebInspector.UIString("Copy XPath"), this._copyXPath.bind(this)); 1310 contextMenu.appendItem(WebInspector.UIString(WebInspector.useLowerCaseMenuTitles() ? "Delete node" : "Delete Node"), this.remove.bind(this)); 1311 contextMenu.appendItem(WebInspector.UIString(WebInspector.useLowerCaseMenuTitles() ? "Inspect DOM properties" : "Inspect DOM Properties"), this._inspectDOMProperties.bind(this)); 1312 }, 1313 1314 _startEditing: function() 1315 { 1316 if (this.treeOutline.selectedDOMNode() !== this._node) 1317 return; 1318 1319 var listItem = this._listItemNode; 1320 1321 if (this._canAddAttributes) { 1322 var attribute = listItem.getElementsByClassName("webkit-html-attribute")[0]; 1323 if (attribute) 1324 return this._startEditingAttribute(attribute, attribute.getElementsByClassName("webkit-html-attribute-value")[0]); 1325 1326 return this._addNewAttribute(); 1327 } 1328 1329 if (this._node.nodeType() === Node.TEXT_NODE) { 1330 var textNode = listItem.getElementsByClassName("webkit-html-text-node")[0]; 1331 if (textNode) 1332 return this._startEditingTextNode(textNode); 1333 return; 1334 } 1335 }, 1336 1337 _addNewAttribute: function() 1338 { 1339 // Cannot just convert the textual html into an element without 1340 // a parent node. Use a temporary span container for the HTML. 1341 var container = document.createElement("span"); 1342 this._buildAttributeDOM(container, " ", ""); 1343 var attr = container.firstChild; 1344 attr.style.marginLeft = "2px"; // overrides the .editing margin rule 1345 attr.style.marginRight = "2px"; // overrides the .editing margin rule 1346 1347 var tag = this.listItemElement.getElementsByClassName("webkit-html-tag")[0]; 1348 this._insertInLastAttributePosition(tag, attr); 1349 attr.scrollIntoViewIfNeeded(true); 1350 return this._startEditingAttribute(attr, attr); 1351 }, 1352 1353 _triggerEditAttribute: function(attributeName) 1354 { 1355 var attributeElements = this.listItemElement.getElementsByClassName("webkit-html-attribute-name"); 1356 for (var i = 0, len = attributeElements.length; i < len; ++i) { 1357 if (attributeElements[i].textContent === attributeName) { 1358 for (var elem = attributeElements[i].nextSibling; elem; elem = elem.nextSibling) { 1359 if (elem.nodeType !== Node.ELEMENT_NODE) 1360 continue; 1361 1362 if (elem.hasStyleClass("webkit-html-attribute-value")) 1363 return this._startEditingAttribute(elem.parentNode, elem); 1364 } 1365 } 1366 } 1367 }, 1368 1369 _startEditingAttribute: function(attribute, elementForSelection) 1370 { 1371 if (WebInspector.isBeingEdited(attribute)) 1372 return true; 1373 1374 var attributeNameElement = attribute.getElementsByClassName("webkit-html-attribute-name")[0]; 1375 if (!attributeNameElement) 1376 return false; 1377 1378 var attributeName = attributeNameElement.textContent; 1379 var attributeValueElement = attribute.getElementsByClassName("webkit-html-attribute-value")[0]; 1380 1381 function removeZeroWidthSpaceRecursive(node) 1382 { 1383 if (node.nodeType === Node.TEXT_NODE) { 1384 node.nodeValue = node.nodeValue.replace(/\u200B/g, ""); 1385 return; 1386 } 1387 1388 if (node.nodeType !== Node.ELEMENT_NODE) 1389 return; 1390 1391 for (var child = node.firstChild; child; child = child.nextSibling) 1392 removeZeroWidthSpaceRecursive(child); 1393 } 1394 1395 var domNode; 1396 var listItemElement = attribute.enclosingNodeOrSelfWithNodeName("li"); 1397 if (attributeName && attributeValueElement && listItemElement && listItemElement.treeElement) 1398 domNode = listItemElement.treeElement.representedObject; 1399 var attributeValue = domNode ? domNode.getAttribute(attributeName) : undefined; 1400 if (typeof attributeValue !== "undefined") 1401 attributeValueElement.textContent = attributeValue; 1402 1403 // Remove zero-width spaces that were added by nodeTitleInfo. 1404 removeZeroWidthSpaceRecursive(attribute); 1405 1406 var config = new WebInspector.EditingConfig(this._attributeEditingCommitted.bind(this), this._editingCancelled.bind(this), attributeName); 1407 1408 function handleKeyDownEvents(event) 1409 { 1410 var isMetaOrCtrl = WebInspector.isMac() ? 1411 event.metaKey && !event.shiftKey && !event.ctrlKey && !event.altKey : 1412 event.ctrlKey && !event.shiftKey && !event.metaKey && !event.altKey; 1413 if (isEnterKey(event) && (event.isMetaOrCtrlForTest || !config.multiline || isMetaOrCtrl)) 1414 return "commit"; 1415 else if (event.keyCode === WebInspector.KeyboardShortcut.Keys.Esc.code || event.keyIdentifier === "U+001B") 1416 return "cancel"; 1417 else if (event.keyIdentifier === "U+0009") // Tab key 1418 return "move-" + (event.shiftKey ? "backward" : "forward"); 1419 else { 1420 WebInspector.handleElementValueModifications(event, attribute); 1421 return ""; 1422 } 1423 } 1424 1425 config.customFinishHandler = handleKeyDownEvents.bind(this); 1426 1427 this._editing = WebInspector.startEditing(attribute, config); 1428 1429 window.getSelection().setBaseAndExtent(elementForSelection, 0, elementForSelection, 1); 1430 1431 return true; 1432 }, 1433 1434 /** 1435 * @param {Element} textNodeElement 1436 */ 1437 _startEditingTextNode: function(textNodeElement) 1438 { 1439 if (WebInspector.isBeingEdited(textNodeElement)) 1440 return true; 1441 1442 var textNode = this._node; 1443 // We only show text nodes inline in elements if the element only 1444 // has a single child, and that child is a text node. 1445 if (textNode.nodeType() === Node.ELEMENT_NODE && textNode.firstChild) 1446 textNode = textNode.firstChild; 1447 1448 var container = textNodeElement.enclosingNodeOrSelfWithClass("webkit-html-text-node"); 1449 if (container) 1450 container.textContent = textNode.nodeValue(); // Strip the CSS or JS highlighting if present. 1451 var config = new WebInspector.EditingConfig(this._textNodeEditingCommitted.bind(this, textNode), this._editingCancelled.bind(this)); 1452 this._editing = WebInspector.startEditing(textNodeElement, config); 1453 window.getSelection().setBaseAndExtent(textNodeElement, 0, textNodeElement, 1); 1454 1455 return true; 1456 }, 1457 1458 /** 1459 * @param {Element=} tagNameElement 1460 */ 1461 _startEditingTagName: function(tagNameElement) 1462 { 1463 if (!tagNameElement) { 1464 tagNameElement = this.listItemElement.getElementsByClassName("webkit-html-tag-name")[0]; 1465 if (!tagNameElement) 1466 return false; 1467 } 1468 1469 var tagName = tagNameElement.textContent; 1470 if (WebInspector.ElementsTreeElement.EditTagBlacklist[tagName.toLowerCase()]) 1471 return false; 1472 1473 if (WebInspector.isBeingEdited(tagNameElement)) 1474 return true; 1475 1476 var closingTagElement = this._distinctClosingTagElement(); 1477 1478 function keyupListener(event) 1479 { 1480 if (closingTagElement) 1481 closingTagElement.textContent = "</" + tagNameElement.textContent + ">"; 1482 } 1483 1484 function editingComitted(element, newTagName) 1485 { 1486 tagNameElement.removeEventListener('keyup', keyupListener, false); 1487 this._tagNameEditingCommitted.apply(this, arguments); 1488 } 1489 1490 function editingCancelled() 1491 { 1492 tagNameElement.removeEventListener('keyup', keyupListener, false); 1493 this._editingCancelled.apply(this, arguments); 1494 } 1495 1496 tagNameElement.addEventListener('keyup', keyupListener, false); 1497 1498 var config = new WebInspector.EditingConfig(editingComitted.bind(this), editingCancelled.bind(this), tagName); 1499 this._editing = WebInspector.startEditing(tagNameElement, config); 1500 window.getSelection().setBaseAndExtent(tagNameElement, 0, tagNameElement, 1); 1501 return true; 1502 }, 1503 1504 _startEditingAsHTML: function(commitCallback, error, initialValue) 1505 { 1506 if (error) 1507 return; 1508 if (this._editing) 1509 return; 1510 1511 function consume(event) 1512 { 1513 if (event.eventPhase === Event.AT_TARGET) 1514 event.consume(true); 1515 } 1516 1517 initialValue = this._convertWhitespaceToEntities(initialValue); 1518 1519 this._htmlEditElement = document.createElement("div"); 1520 this._htmlEditElement.className = "source-code elements-tree-editor"; 1521 1522 // Hide header items. 1523 var child = this.listItemElement.firstChild; 1524 while (child) { 1525 child.style.display = "none"; 1526 child = child.nextSibling; 1527 } 1528 // Hide children item. 1529 if (this._childrenListNode) 1530 this._childrenListNode.style.display = "none"; 1531 // Append editor. 1532 this.listItemElement.appendChild(this._htmlEditElement); 1533 this.treeOutline.childrenListElement.parentElement.addEventListener("mousedown", consume, false); 1534 1535 this.updateSelection(); 1536 1537 /** 1538 * @param {Element} element 1539 * @param {string} newValue 1540 */ 1541 function commit(element, newValue) 1542 { 1543 commitCallback(initialValue, newValue); 1544 dispose.call(this); 1545 } 1546 1547 function dispose() 1548 { 1549 delete this._editing; 1550 1551 // Remove editor. 1552 this.listItemElement.removeChild(this._htmlEditElement); 1553 delete this._htmlEditElement; 1554 // Unhide children item. 1555 if (this._childrenListNode) 1556 this._childrenListNode.style.removeProperty("display"); 1557 // Unhide header items. 1558 var child = this.listItemElement.firstChild; 1559 while (child) { 1560 child.style.removeProperty("display"); 1561 child = child.nextSibling; 1562 } 1563 1564 this.treeOutline.childrenListElement.parentElement.removeEventListener("mousedown", consume, false); 1565 this.updateSelection(); 1566 } 1567 1568 var config = new WebInspector.EditingConfig(commit.bind(this), dispose.bind(this)); 1569 config.setMultilineOptions(initialValue, { name: "xml", htmlMode: true }, "web-inspector-html", WebInspector.settings.domWordWrap.get(), true); 1570 this._editing = WebInspector.startEditing(this._htmlEditElement, config); 1571 }, 1572 1573 _attributeEditingCommitted: function(element, newText, oldText, attributeName, moveDirection) 1574 { 1575 delete this._editing; 1576 1577 var treeOutline = this.treeOutline; 1578 /** 1579 * @param {Protocol.Error=} error 1580 */ 1581 function moveToNextAttributeIfNeeded(error) 1582 { 1583 if (error) 1584 this._editingCancelled(element, attributeName); 1585 1586 if (!moveDirection) 1587 return; 1588 1589 treeOutline._updateModifiedNodes(); 1590 1591 // Search for the attribute's position, and then decide where to move to. 1592 var attributes = this._node.attributes(); 1593 for (var i = 0; i < attributes.length; ++i) { 1594 if (attributes[i].name !== attributeName) 1595 continue; 1596 1597 if (moveDirection === "backward") { 1598 if (i === 0) 1599 this._startEditingTagName(); 1600 else 1601 this._triggerEditAttribute(attributes[i - 1].name); 1602 } else { 1603 if (i === attributes.length - 1) 1604 this._addNewAttribute(); 1605 else 1606 this._triggerEditAttribute(attributes[i + 1].name); 1607 } 1608 return; 1609 } 1610 1611 // Moving From the "New Attribute" position. 1612 if (moveDirection === "backward") { 1613 if (newText === " ") { 1614 // Moving from "New Attribute" that was not edited 1615 if (attributes.length > 0) 1616 this._triggerEditAttribute(attributes[attributes.length - 1].name); 1617 } else { 1618 // Moving from "New Attribute" that holds new value 1619 if (attributes.length > 1) 1620 this._triggerEditAttribute(attributes[attributes.length - 2].name); 1621 } 1622 } else if (moveDirection === "forward") { 1623 if (!/^\s*$/.test(newText)) 1624 this._addNewAttribute(); 1625 else 1626 this._startEditingTagName(); 1627 } 1628 } 1629 1630 if (!attributeName.trim() && !newText.trim()) { 1631 element.remove(); 1632 moveToNextAttributeIfNeeded.call(this); 1633 return; 1634 } 1635 1636 if (oldText !== newText) { 1637 this._node.setAttribute(attributeName, newText, moveToNextAttributeIfNeeded.bind(this)); 1638 return; 1639 } 1640 1641 this.updateTitle(); 1642 moveToNextAttributeIfNeeded.call(this); 1643 }, 1644 1645 _tagNameEditingCommitted: function(element, newText, oldText, tagName, moveDirection) 1646 { 1647 delete this._editing; 1648 var self = this; 1649 1650 function cancel() 1651 { 1652 var closingTagElement = self._distinctClosingTagElement(); 1653 if (closingTagElement) 1654 closingTagElement.textContent = "</" + tagName + ">"; 1655 1656 self._editingCancelled(element, tagName); 1657 moveToNextAttributeIfNeeded.call(self); 1658 } 1659 1660 function moveToNextAttributeIfNeeded() 1661 { 1662 if (moveDirection !== "forward") { 1663 this._addNewAttribute(); 1664 return; 1665 } 1666 1667 var attributes = this._node.attributes(); 1668 if (attributes.length > 0) 1669 this._triggerEditAttribute(attributes[0].name); 1670 else 1671 this._addNewAttribute(); 1672 } 1673 1674 newText = newText.trim(); 1675 if (newText === oldText) { 1676 cancel(); 1677 return; 1678 } 1679 1680 var treeOutline = this.treeOutline; 1681 var wasExpanded = this.expanded; 1682 1683 function changeTagNameCallback(error, nodeId) 1684 { 1685 if (error || !nodeId) { 1686 cancel(); 1687 return; 1688 } 1689 var newTreeItem = treeOutline._selectNodeAfterEdit(null, wasExpanded, error, nodeId); 1690 moveToNextAttributeIfNeeded.call(newTreeItem); 1691 } 1692 1693 this._node.setNodeName(newText, changeTagNameCallback); 1694 }, 1695 1696 /** 1697 * @param {WebInspector.DOMNode} textNode 1698 * @param {Element} element 1699 * @param {string} newText 1700 */ 1701 _textNodeEditingCommitted: function(textNode, element, newText) 1702 { 1703 delete this._editing; 1704 1705 function callback() 1706 { 1707 this.updateTitle(); 1708 } 1709 textNode.setNodeValue(newText, callback.bind(this)); 1710 }, 1711 1712 /** 1713 * @param {Element} element 1714 * @param {*} context 1715 */ 1716 _editingCancelled: function(element, context) 1717 { 1718 delete this._editing; 1719 1720 // Need to restore attributes structure. 1721 this.updateTitle(); 1722 }, 1723 1724 _distinctClosingTagElement: function() 1725 { 1726 // FIXME: Improve the Tree Element / Outline Abstraction to prevent crawling the DOM 1727 1728 // For an expanded element, it will be the last element with class "close" 1729 // in the child element list. 1730 if (this.expanded) { 1731 var closers = this._childrenListNode.querySelectorAll(".close"); 1732 return closers[closers.length-1]; 1733 } 1734 1735 // Remaining cases are single line non-expanded elements with a closing 1736 // tag, or HTML elements without a closing tag (such as <br>). Return 1737 // null in the case where there isn't a closing tag. 1738 var tags = this.listItemElement.getElementsByClassName("webkit-html-tag"); 1739 return (tags.length === 1 ? null : tags[tags.length-1]); 1740 }, 1741 1742 /** 1743 * @param {boolean=} onlySearchQueryChanged 1744 */ 1745 updateTitle: function(onlySearchQueryChanged) 1746 { 1747 // If we are editing, return early to prevent canceling the edit. 1748 // After editing is committed updateTitle will be called. 1749 if (this._editing) 1750 return; 1751 1752 if (onlySearchQueryChanged) { 1753 if (this._highlightResult) 1754 this._updateSearchHighlight(false); 1755 } else { 1756 var highlightElement = document.createElement("span"); 1757 highlightElement.className = "highlight"; 1758 highlightElement.appendChild(this._nodeTitleInfo(WebInspector.linkifyURLAsNode).titleDOM); 1759 this.title = highlightElement; 1760 this._updateDecorations(); 1761 delete this._highlightResult; 1762 } 1763 1764 delete this.selectionElement; 1765 if (this.selected) 1766 this.updateSelection(); 1767 this._preventFollowingLinksOnDoubleClick(); 1768 this._highlightSearchResults(); 1769 }, 1770 1771 _createDecoratorElement: function() 1772 { 1773 var node = this._node; 1774 var decoratorMessages = []; 1775 var parentDecoratorMessages = []; 1776 for (var i = 0; i < this.treeOutline._nodeDecorators.length; ++i) { 1777 var decorator = this.treeOutline._nodeDecorators[i]; 1778 var message = decorator.decorate(node); 1779 if (message) { 1780 decoratorMessages.push(message); 1781 continue; 1782 } 1783 1784 if (this.expanded || this._elementCloseTag) 1785 continue; 1786 1787 message = decorator.decorateAncestor(node); 1788 if (message) 1789 parentDecoratorMessages.push(message) 1790 } 1791 if (!decoratorMessages.length && !parentDecoratorMessages.length) 1792 return null; 1793 1794 var decoratorElement = document.createElement("div"); 1795 decoratorElement.addStyleClass("elements-gutter-decoration"); 1796 if (!decoratorMessages.length) 1797 decoratorElement.addStyleClass("elements-has-decorated-children"); 1798 decoratorElement.title = decoratorMessages.concat(parentDecoratorMessages).join("\n"); 1799 return decoratorElement; 1800 }, 1801 1802 _updateDecorations: function() 1803 { 1804 if (this._decoratorElement) 1805 this._decoratorElement.remove(); 1806 this._decoratorElement = this._createDecoratorElement(); 1807 if (this._decoratorElement && this.listItemElement) 1808 this.listItemElement.insertBefore(this._decoratorElement, this.listItemElement.firstChild); 1809 }, 1810 1811 /** 1812 * @param {WebInspector.DOMNode=} node 1813 * @param {function(string, string, string, boolean=, string=)=} linkify 1814 */ 1815 _buildAttributeDOM: function(parentElement, name, value, node, linkify) 1816 { 1817 var hasText = (value.length > 0); 1818 var attrSpanElement = parentElement.createChild("span", "webkit-html-attribute"); 1819 var attrNameElement = attrSpanElement.createChild("span", "webkit-html-attribute-name"); 1820 attrNameElement.textContent = name; 1821 1822 if (hasText) 1823 attrSpanElement.appendChild(document.createTextNode("=\u200B\"")); 1824 1825 if (linkify && (name === "src" || name === "href")) { 1826 var rewrittenHref = node.resolveURL(value); 1827 value = value.replace(/([\/;:\)\]\}])/g, "$1\u200B"); 1828 if (rewrittenHref === null) { 1829 var attrValueElement = attrSpanElement.createChild("span", "webkit-html-attribute-value"); 1830 attrValueElement.textContent = value; 1831 } else { 1832 if (value.startsWith("data:")) 1833 value = value.trimMiddle(60); 1834 attrSpanElement.appendChild(linkify(rewrittenHref, value, "webkit-html-attribute-value", node.nodeName().toLowerCase() === "a")); 1835 } 1836 } else { 1837 value = value.replace(/([\/;:\)\]\}])/g, "$1\u200B"); 1838 var attrValueElement = attrSpanElement.createChild("span", "webkit-html-attribute-value"); 1839 attrValueElement.textContent = value; 1840 } 1841 1842 if (hasText) 1843 attrSpanElement.appendChild(document.createTextNode("\"")); 1844 }, 1845 1846 /** 1847 * @param {function(string, string, string, boolean=, string=)=} linkify 1848 */ 1849 _buildTagDOM: function(parentElement, tagName, isClosingTag, isDistinctTreeElement, linkify) 1850 { 1851 var node = this._node; 1852 var classes = [ "webkit-html-tag" ]; 1853 if (isClosingTag && isDistinctTreeElement) 1854 classes.push("close"); 1855 if (node.isInShadowTree()) 1856 classes.push("shadow"); 1857 var tagElement = parentElement.createChild("span", classes.join(" ")); 1858 tagElement.appendChild(document.createTextNode("<")); 1859 var tagNameElement = tagElement.createChild("span", isClosingTag ? "" : "webkit-html-tag-name"); 1860 tagNameElement.textContent = (isClosingTag ? "/" : "") + tagName; 1861 if (!isClosingTag && node.hasAttributes()) { 1862 var attributes = node.attributes(); 1863 for (var i = 0; i < attributes.length; ++i) { 1864 var attr = attributes[i]; 1865 tagElement.appendChild(document.createTextNode(" ")); 1866 this._buildAttributeDOM(tagElement, attr.name, attr.value, node, linkify); 1867 } 1868 } 1869 tagElement.appendChild(document.createTextNode(">")); 1870 parentElement.appendChild(document.createTextNode("\u200B")); 1871 }, 1872 1873 _convertWhitespaceToEntities: function(text) 1874 { 1875 var result = ""; 1876 var lastIndexAfterEntity = 0; 1877 var charToEntity = WebInspector.ElementsTreeOutline.MappedCharToEntity; 1878 for (var i = 0, size = text.length; i < size; ++i) { 1879 var char = text.charAt(i); 1880 if (charToEntity[char]) { 1881 result += text.substring(lastIndexAfterEntity, i) + "&" + charToEntity[char] + ";"; 1882 lastIndexAfterEntity = i + 1; 1883 } 1884 } 1885 if (result) { 1886 result += text.substring(lastIndexAfterEntity); 1887 return result; 1888 } 1889 return text; 1890 }, 1891 1892 _nodeTitleInfo: function(linkify) 1893 { 1894 var node = this._node; 1895 var info = {titleDOM: document.createDocumentFragment(), hasChildren: this.hasChildren}; 1896 1897 switch (node.nodeType()) { 1898 case Node.ATTRIBUTE_NODE: 1899 var value = node.value || "\u200B"; // Zero width space to force showing an empty value. 1900 this._buildAttributeDOM(info.titleDOM, node.name, value); 1901 break; 1902 1903 case Node.ELEMENT_NODE: 1904 var tagName = node.nodeNameInCorrectCase(); 1905 if (this._elementCloseTag) { 1906 this._buildTagDOM(info.titleDOM, tagName, true, true); 1907 info.hasChildren = false; 1908 break; 1909 } 1910 1911 this._buildTagDOM(info.titleDOM, tagName, false, false, linkify); 1912 1913 var showInlineText = this._showInlineText() && !this.hasChildren; 1914 if (!this.expanded && (!showInlineText && (this.treeOutline.isXMLMimeType || !WebInspector.ElementsTreeElement.ForbiddenClosingTagElements[tagName]))) { 1915 if (this.hasChildren) { 1916 var textNodeElement = info.titleDOM.createChild("span", "webkit-html-text-node bogus"); 1917 textNodeElement.textContent = "\u2026"; 1918 info.titleDOM.appendChild(document.createTextNode("\u200B")); 1919 } 1920 this._buildTagDOM(info.titleDOM, tagName, true, false); 1921 } 1922 1923 // If this element only has a single child that is a text node, 1924 // just show that text and the closing tag inline rather than 1925 // create a subtree for them 1926 if (showInlineText) { 1927 var textNodeElement = info.titleDOM.createChild("span", "webkit-html-text-node"); 1928 textNodeElement.textContent = this._convertWhitespaceToEntities(node.firstChild.nodeValue()); 1929 info.titleDOM.appendChild(document.createTextNode("\u200B")); 1930 this._buildTagDOM(info.titleDOM, tagName, true, false); 1931 info.hasChildren = false; 1932 } 1933 break; 1934 1935 case Node.TEXT_NODE: 1936 if (node.parentNode && node.parentNode.nodeName().toLowerCase() === "script") { 1937 var newNode = info.titleDOM.createChild("span", "webkit-html-text-node webkit-html-js-node"); 1938 newNode.textContent = node.nodeValue(); 1939 1940 var javascriptSyntaxHighlighter = new WebInspector.DOMSyntaxHighlighter("text/javascript", true); 1941 javascriptSyntaxHighlighter.syntaxHighlightNode(newNode); 1942 } else if (node.parentNode && node.parentNode.nodeName().toLowerCase() === "style") { 1943 var newNode = info.titleDOM.createChild("span", "webkit-html-text-node webkit-html-css-node"); 1944 newNode.textContent = node.nodeValue(); 1945 1946 var cssSyntaxHighlighter = new WebInspector.DOMSyntaxHighlighter("text/css", true); 1947 cssSyntaxHighlighter.syntaxHighlightNode(newNode); 1948 } else { 1949 info.titleDOM.appendChild(document.createTextNode("\"")); 1950 var textNodeElement = info.titleDOM.createChild("span", "webkit-html-text-node"); 1951 textNodeElement.textContent = this._convertWhitespaceToEntities(node.nodeValue()); 1952 info.titleDOM.appendChild(document.createTextNode("\"")); 1953 } 1954 break; 1955 1956 case Node.COMMENT_NODE: 1957 var commentElement = info.titleDOM.createChild("span", "webkit-html-comment"); 1958 commentElement.appendChild(document.createTextNode("<!--" + node.nodeValue() + "-->")); 1959 break; 1960 1961 case Node.DOCUMENT_TYPE_NODE: 1962 var docTypeElement = info.titleDOM.createChild("span", "webkit-html-doctype"); 1963 docTypeElement.appendChild(document.createTextNode("<!DOCTYPE " + node.nodeName())); 1964 if (node.publicId) { 1965 docTypeElement.appendChild(document.createTextNode(" PUBLIC \"" + node.publicId + "\"")); 1966 if (node.systemId) 1967 docTypeElement.appendChild(document.createTextNode(" \"" + node.systemId + "\"")); 1968 } else if (node.systemId) 1969 docTypeElement.appendChild(document.createTextNode(" SYSTEM \"" + node.systemId + "\"")); 1970 1971 if (node.internalSubset) 1972 docTypeElement.appendChild(document.createTextNode(" [" + node.internalSubset + "]")); 1973 1974 docTypeElement.appendChild(document.createTextNode(">")); 1975 break; 1976 1977 case Node.CDATA_SECTION_NODE: 1978 var cdataElement = info.titleDOM.createChild("span", "webkit-html-text-node"); 1979 cdataElement.appendChild(document.createTextNode("<![CDATA[" + node.nodeValue() + "]]>")); 1980 break; 1981 case Node.DOCUMENT_FRAGMENT_NODE: 1982 var fragmentElement = info.titleDOM.createChild("span", "webkit-html-fragment"); 1983 fragmentElement.textContent = node.nodeNameInCorrectCase().collapseWhitespace(); 1984 if (node.isInShadowTree()) 1985 fragmentElement.addStyleClass("shadow"); 1986 break; 1987 default: 1988 info.titleDOM.appendChild(document.createTextNode(node.nodeNameInCorrectCase().collapseWhitespace())); 1989 } 1990 return info; 1991 }, 1992 1993 _showInlineText: function() 1994 { 1995 if (this._node.templateContent() || (WebInspector.ElementsTreeOutline.showShadowDOM() && this._node.hasShadowRoots())) 1996 return false; 1997 if (this._node.nodeType() !== Node.ELEMENT_NODE) 1998 return false; 1999 if (!this._node.firstChild || this._node.firstChild !== this._node.lastChild || this._node.firstChild.nodeType() !== Node.TEXT_NODE) 2000 return false; 2001 var textChild = this._node.firstChild; 2002 if (textChild.nodeValue().length < Preferences.maxInlineTextChildLength) 2003 return true; 2004 return false; 2005 }, 2006 2007 remove: function() 2008 { 2009 var parentElement = this.parent; 2010 if (!parentElement) 2011 return; 2012 2013 var self = this; 2014 function removeNodeCallback(error, removedNodeId) 2015 { 2016 if (error) 2017 return; 2018 2019 parentElement.removeChild(self); 2020 parentElement._adjustCollapsedRange(); 2021 } 2022 2023 if (!this._node.parentNode || this._node.parentNode.nodeType() === Node.DOCUMENT_NODE) 2024 return; 2025 this._node.removeNode(removeNodeCallback); 2026 }, 2027 2028 _editAsHTML: function() 2029 { 2030 var treeOutline = this.treeOutline; 2031 var node = this._node; 2032 var parentNode = node.parentNode; 2033 var index = node.index; 2034 var wasExpanded = this.expanded; 2035 2036 function selectNode(error, nodeId) 2037 { 2038 if (error) 2039 return; 2040 2041 // Select it and expand if necessary. We force tree update so that it processes dom events and is up to date. 2042 treeOutline._updateModifiedNodes(); 2043 2044 var newNode = parentNode ? parentNode.children()[index] || parentNode : null; 2045 if (!newNode) 2046 return; 2047 2048 treeOutline.selectDOMNode(newNode, true); 2049 2050 if (wasExpanded) { 2051 var newTreeItem = treeOutline.findTreeElement(newNode); 2052 if (newTreeItem) 2053 newTreeItem.expand(); 2054 } 2055 } 2056 2057 function commitChange(initialValue, value) 2058 { 2059 if (initialValue !== value) 2060 node.setOuterHTML(value, selectNode); 2061 else 2062 return; 2063 } 2064 2065 node.getOuterHTML(this._startEditingAsHTML.bind(this, commitChange)); 2066 }, 2067 2068 _copyHTML: function() 2069 { 2070 this._node.copyNode(); 2071 }, 2072 2073 _copyXPath: function() 2074 { 2075 this._node.copyXPath(true); 2076 }, 2077 2078 _inspectDOMProperties: function() 2079 { 2080 WebInspector.RemoteObject.resolveNode(this._node, "console", callback); 2081 2082 /** 2083 * @param {WebInspector.RemoteObject} nodeObject 2084 */ 2085 function callback(nodeObject) 2086 { 2087 if (!nodeObject) 2088 return; 2089 2090 var message = WebInspector.ConsoleMessage.create(WebInspector.ConsoleMessage.MessageSource.ConsoleAPI, WebInspector.ConsoleMessage.MessageLevel.Log, "", WebInspector.ConsoleMessage.MessageType.Dir, undefined, undefined, undefined, undefined, [nodeObject]); 2091 WebInspector.console.addMessage(message); 2092 WebInspector.showConsole(); 2093 } 2094 }, 2095 2096 _highlightSearchResults: function() 2097 { 2098 if (!this._searchQuery || !this._searchHighlightsVisible) 2099 return; 2100 if (this._highlightResult) { 2101 this._updateSearchHighlight(true); 2102 return; 2103 } 2104 2105 var text = this.listItemElement.textContent; 2106 var regexObject = createPlainTextSearchRegex(this._searchQuery, "gi"); 2107 2108 var offset = 0; 2109 var match = regexObject.exec(text); 2110 var matchRanges = []; 2111 while (match) { 2112 matchRanges.push({ offset: match.index, length: match[0].length }); 2113 match = regexObject.exec(text); 2114 } 2115 2116 // Fall back for XPath, etc. matches. 2117 if (!matchRanges.length) 2118 matchRanges.push({ offset: 0, length: text.length }); 2119 2120 this._highlightResult = []; 2121 WebInspector.highlightSearchResults(this.listItemElement, matchRanges, this._highlightResult); 2122 }, 2123 2124 _scrollIntoView: function() 2125 { 2126 function scrollIntoViewCallback(object) 2127 { 2128 function scrollIntoView() 2129 { 2130 this.scrollIntoViewIfNeeded(true); 2131 } 2132 2133 if (object) 2134 object.callFunction(scrollIntoView); 2135 } 2136 2137 WebInspector.RemoteObject.resolveNode(this._node, "", scrollIntoViewCallback); 2138 }, 2139 2140 /** 2141 * @return {Array.<WebInspector.DOMNode>} visibleChildren 2142 */ 2143 _visibleChildren: function() 2144 { 2145 var visibleChildren = WebInspector.ElementsTreeOutline.showShadowDOM() ? this._node.shadowRoots() : []; 2146 if (this._node.templateContent()) 2147 visibleChildren.push(this._node.templateContent()); 2148 if (this._node.childNodeCount()) 2149 visibleChildren = visibleChildren.concat(this._node.children()); 2150 return visibleChildren; 2151 }, 2152 2153 /** 2154 * @return {Array.<WebInspector.DOMNode>} visibleChildren 2155 */ 2156 _visibleChildCount: function() 2157 { 2158 var childCount = this._node.childNodeCount(); 2159 if (this._node.templateContent()) 2160 childCount++; 2161 if (WebInspector.ElementsTreeOutline.showShadowDOM()) 2162 childCount += this._node.shadowRoots().length; 2163 return childCount; 2164 }, 2165 2166 _updateHasChildren: function() 2167 { 2168 this.hasChildren = !this._elementCloseTag && !this._showInlineText() && this._visibleChildCount() > 0; 2169 }, 2170 2171 __proto__: TreeElement.prototype 2172 } 2173 2174 /** 2175 * @constructor 2176 */ 2177 WebInspector.ElementsTreeUpdater = function(treeOutline) 2178 { 2179 WebInspector.domAgent.addEventListener(WebInspector.DOMAgent.Events.NodeInserted, this._nodeInserted, this); 2180 WebInspector.domAgent.addEventListener(WebInspector.DOMAgent.Events.NodeRemoved, this._nodeRemoved, this); 2181 WebInspector.domAgent.addEventListener(WebInspector.DOMAgent.Events.AttrModified, this._attributesUpdated, this); 2182 WebInspector.domAgent.addEventListener(WebInspector.DOMAgent.Events.AttrRemoved, this._attributesUpdated, this); 2183 WebInspector.domAgent.addEventListener(WebInspector.DOMAgent.Events.CharacterDataModified, this._characterDataModified, this); 2184 WebInspector.domAgent.addEventListener(WebInspector.DOMAgent.Events.DocumentUpdated, this._documentUpdated, this); 2185 WebInspector.domAgent.addEventListener(WebInspector.DOMAgent.Events.ChildNodeCountUpdated, this._childNodeCountUpdated, this); 2186 2187 this._treeOutline = treeOutline; 2188 this._recentlyModifiedNodes = new Map(); 2189 } 2190 2191 WebInspector.ElementsTreeUpdater.prototype = { 2192 2193 /** 2194 * @param {!WebInspector.DOMNode} node 2195 * @param {boolean} isUpdated 2196 * @param {WebInspector.DOMNode=} parentNode 2197 */ 2198 _nodeModified: function(node, isUpdated, parentNode) 2199 { 2200 if (this._treeOutline._visible) 2201 this._updateModifiedNodesSoon(); 2202 2203 var entry = /** @type {WebInspector.ElementsTreeUpdater.UpdateEntry} */ (this._recentlyModifiedNodes.get(node)); 2204 if (!entry) { 2205 entry = new WebInspector.ElementsTreeUpdater.UpdateEntry(isUpdated, parentNode); 2206 this._recentlyModifiedNodes.put(node, entry); 2207 return; 2208 } 2209 2210 entry.isUpdated |= isUpdated; 2211 if (parentNode) 2212 entry.parent = parentNode; 2213 }, 2214 2215 _documentUpdated: function(event) 2216 { 2217 var inspectedRootDocument = event.data; 2218 2219 this._reset(); 2220 2221 if (!inspectedRootDocument) 2222 return; 2223 2224 this._treeOutline.rootDOMNode = inspectedRootDocument; 2225 }, 2226 2227 _attributesUpdated: function(event) 2228 { 2229 this._nodeModified(event.data.node, true); 2230 }, 2231 2232 _characterDataModified: function(event) 2233 { 2234 this._nodeModified(event.data, true); 2235 }, 2236 2237 _nodeInserted: function(event) 2238 { 2239 this._nodeModified(event.data, false, event.data.parentNode); 2240 }, 2241 2242 _nodeRemoved: function(event) 2243 { 2244 this._nodeModified(event.data.node, false, event.data.parent); 2245 }, 2246 2247 _childNodeCountUpdated: function(event) 2248 { 2249 var treeElement = this._treeOutline.findTreeElement(event.data); 2250 if (treeElement) 2251 treeElement._updateHasChildren(); 2252 }, 2253 2254 _updateModifiedNodesSoon: function() 2255 { 2256 if (this._updateModifiedNodesTimeout) 2257 return; 2258 this._updateModifiedNodesTimeout = setTimeout(this._updateModifiedNodes.bind(this), 50); 2259 }, 2260 2261 _updateModifiedNodes: function() 2262 { 2263 if (this._updateModifiedNodesTimeout) { 2264 clearTimeout(this._updateModifiedNodesTimeout); 2265 delete this._updateModifiedNodesTimeout; 2266 } 2267 2268 var updatedParentTreeElements = []; 2269 2270 var hidePanelWhileUpdating = this._recentlyModifiedNodes.size() > 10; 2271 if (hidePanelWhileUpdating) { 2272 var treeOutlineContainerElement = this._treeOutline.element.parentNode; 2273 this._treeOutline.element.addStyleClass("hidden"); 2274 var originalScrollTop = treeOutlineContainerElement ? treeOutlineContainerElement.scrollTop : 0; 2275 } 2276 2277 var keys = this._recentlyModifiedNodes.keys(); 2278 for (var i = 0, size = keys.length; i < size; ++i) { 2279 var node = keys[i]; 2280 var entry = this._recentlyModifiedNodes.get(node); 2281 var parent = entry.parent; 2282 2283 if (parent === this._treeOutline._rootDOMNode) { 2284 // Document's children have changed, perform total update. 2285 this._treeOutline.update(); 2286 this._treeOutline.element.removeStyleClass("hidden"); 2287 return; 2288 } 2289 2290 if (entry.isUpdated) { 2291 var nodeItem = this._treeOutline.findTreeElement(node); 2292 if (nodeItem) 2293 nodeItem.updateTitle(); 2294 } 2295 2296 if (!parent) 2297 continue; 2298 2299 var parentNodeItem = this._treeOutline.findTreeElement(parent); 2300 if (parentNodeItem && !parentNodeItem.alreadyUpdatedChildren) { 2301 parentNodeItem.updateChildren(); 2302 parentNodeItem.alreadyUpdatedChildren = true; 2303 updatedParentTreeElements.push(parentNodeItem); 2304 } 2305 } 2306 2307 for (var i = 0; i < updatedParentTreeElements.length; ++i) 2308 delete updatedParentTreeElements[i].alreadyUpdatedChildren; 2309 2310 if (hidePanelWhileUpdating) { 2311 this._treeOutline.element.removeStyleClass("hidden"); 2312 if (originalScrollTop) 2313 treeOutlineContainerElement.scrollTop = originalScrollTop; 2314 this._treeOutline.updateSelection(); 2315 } 2316 this._recentlyModifiedNodes.clear(); 2317 }, 2318 2319 _reset: function() 2320 { 2321 this._treeOutline.rootDOMNode = null; 2322 this._treeOutline.selectDOMNode(null, false); 2323 WebInspector.domAgent.hideDOMNodeHighlight(); 2324 this._recentlyModifiedNodes.clear(); 2325 } 2326 } 2327 2328 /** 2329 * @constructor 2330 * @param {boolean} isUpdated 2331 * @param {WebInspector.DOMNode=} parent 2332 */ 2333 WebInspector.ElementsTreeUpdater.UpdateEntry = function(isUpdated, parent) 2334 { 2335 this.isUpdated = isUpdated; 2336 if (parent) 2337 this.parent = parent; 2338 } 2339