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