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