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