2 * Copyright (C) 2007, 2008 Apple Inc. All rights reserved.
3 * Copyright (C) 2008 Matt Lilek <webkit@mattlilek.com>
4 * Copyright (C) 2009 Joseph Pecoraro
6 * Redistribution and use in source and binary forms, with or without
7 * modification, are permitted provided that the following conditions
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.
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.
33 * @extends {TreeOutline}
34 * @param {!WebInspector.Target} target
35 * @param {boolean=} omitRootDOMNode
36 * @param {boolean=} selectEnabled
37 * @param {function(!WebInspector.DOMNode, string, boolean)=} setPseudoClassCallback
39 WebInspector.ElementsTreeOutline = function(target, omitRootDOMNode, selectEnabled, setPseudoClassCallback)
41 this._target = target;
42 this._domModel = target.domModel;
43 this.element = document.createElement("ol");
44 this.element.className = "elements-tree-outline";
45 this.element.addEventListener("mousedown", this._onmousedown.bind(this), false);
46 this.element.addEventListener("mousemove", this._onmousemove.bind(this), false);
47 this.element.addEventListener("mouseout", this._onmouseout.bind(this), false);
48 this.element.addEventListener("dragstart", this._ondragstart.bind(this), false);
49 this.element.addEventListener("dragover", this._ondragover.bind(this), false);
50 this.element.addEventListener("dragleave", this._ondragleave.bind(this), false);
51 this.element.addEventListener("drop", this._ondrop.bind(this), false);
52 this.element.addEventListener("dragend", this._ondragend.bind(this), false);
53 this.element.addEventListener("keydown", this._onkeydown.bind(this), false);
54 this.element.addEventListener("webkitAnimationEnd", this._onAnimationEnd.bind(this), false);
55 this.element.addEventListener("contextmenu", this._contextMenuEventFired.bind(this), false);
57 TreeOutline.call(this, this.element);
59 this._includeRootDOMNode = !omitRootDOMNode;
60 this._selectEnabled = selectEnabled;
61 /** @type {?WebInspector.DOMNode} */
62 this._rootDOMNode = null;
63 /** @type {?WebInspector.DOMNode} */
64 this._selectedDOMNode = null;
65 this._eventSupport = new WebInspector.Object();
67 this._visible = false;
68 this._pickNodeMode = false;
70 this._setPseudoClassCallback = setPseudoClassCallback;
71 this._createNodeDecorators();
74 /** @typedef {{node: !WebInspector.DOMNode, isCut: boolean}} */
75 WebInspector.ElementsTreeOutline.ClipboardData;
80 WebInspector.ElementsTreeOutline.Events = {
81 NodePicked: "NodePicked",
82 SelectedNodeChanged: "SelectedNodeChanged",
83 ElementsTreeUpdated: "ElementsTreeUpdated"
88 * @type {!Object.<string, string>}
90 WebInspector.ElementsTreeOutline.MappedCharToEntity = {
95 "\u200a": "#8202", // Hairspace
96 "\u200b": "#8203", // ZWSP
101 "\u202a": "#8234", // LRE
102 "\u202b": "#8235", // RLE
103 "\u202c": "#8236", // PDF
104 "\u202d": "#8237", // LRO
105 "\u202e": "#8238" // RLO
108 WebInspector.ElementsTreeOutline.prototype = {
110 * @param {!Event} event
112 _onAnimationEnd: function(event)
114 event.target.classList.remove("elements-tree-element-pick-node-1");
115 event.target.classList.remove("elements-tree-element-pick-node-2");
119 * @param {boolean} value
121 setPickNodeMode: function(value)
123 this._pickNodeMode = value;
124 this.element.classList.toggle("pick-node-mode", value);
128 * @param {!Element} element
129 * @param {?WebInspector.DOMNode} node
131 _handlePickNode: function(element, node)
133 if (!this._pickNodeMode)
136 this._eventSupport.dispatchEventToListeners(WebInspector.ElementsTreeOutline.Events.NodePicked, node);
137 var hasRunningAnimation = element.classList.contains("elements-tree-element-pick-node-1") || element.classList.contains("elements-tree-element-pick-node-2");
138 element.classList.toggle("elements-tree-element-pick-node-1");
139 if (hasRunningAnimation)
140 element.classList.toggle("elements-tree-element-pick-node-2");
145 * @return {!WebInspector.Target}
153 * @return {!WebInspector.DOMModel}
157 return this._domModel;
161 * @param {number} width
163 setVisibleWidth: function(width)
165 this._visibleWidth = width;
166 if (this._multilineEditing)
167 this._multilineEditing.setWidth(this._visibleWidth);
170 _createNodeDecorators: function()
172 this._nodeDecorators = [];
173 this._nodeDecorators.push(new WebInspector.ElementsTreeOutline.PseudoStateDecorator());
176 wireToDOMModel: function()
178 this._elementsTreeUpdater = new WebInspector.ElementsTreeUpdater(this._target.domModel, this);
181 unwireFromDOMModel: function()
183 if (this._elementsTreeUpdater)
184 this._elementsTreeUpdater.dispose();
188 * @param {?WebInspector.ElementsTreeOutline.ClipboardData} data
190 _setClipboardData: function(data)
192 if (this._clipboardNodeData) {
193 var treeElement = this.findTreeElement(this._clipboardNodeData.node);
195 treeElement.setInClipboard(false);
196 delete this._clipboardNodeData;
200 var treeElement = this.findTreeElement(data.node);
202 treeElement.setInClipboard(true);
203 this._clipboardNodeData = data;
208 * @param {!WebInspector.DOMNode} removedNode
210 _resetClipboardIfNeeded: function(removedNode)
212 if (this._clipboardNodeData && this._clipboardNodeData.node === removedNode)
213 this._setClipboardData(null);
217 * @param {boolean} isCut
218 * @param {!Event} event
220 handleCopyOrCutKeyboardEvent: function(isCut, event)
222 this._setClipboardData(null);
224 // Don't prevent the normal copy if the user has a selection.
225 if (!window.getSelection().isCollapsed)
228 // Do not interfere with text editing.
229 var currentFocusElement = WebInspector.currentFocusElement();
230 if (currentFocusElement && WebInspector.isBeingEdited(currentFocusElement))
233 var targetNode = this.selectedDOMNode();
237 event.clipboardData.clearData();
238 event.preventDefault();
240 this._performCopyOrCut(isCut, targetNode);
244 * @param {boolean} isCut
245 * @param {?WebInspector.DOMNode} node
247 _performCopyOrCut: function(isCut, node)
249 if (isCut && (node.isShadowRoot() || node.ancestorUserAgentShadowRoot()))
253 this._setClipboardData({ node: node, isCut: isCut });
257 * @param {!WebInspector.DOMNode} targetNode
260 _canPaste: function(targetNode)
262 if (targetNode.isShadowRoot() || targetNode.ancestorUserAgentShadowRoot())
265 if (!this._clipboardNodeData)
268 var node = this._clipboardNodeData.node;
269 if (this._clipboardNodeData.isCut && (node === targetNode || node.isAncestor(targetNode)))
272 if (targetNode.target() !== node.target())
278 * @param {!WebInspector.DOMNode} targetNode
280 _pasteNode: function(targetNode)
282 if (this._canPaste(targetNode))
283 this._performPaste(targetNode);
287 * @param {!Event} event
289 handlePasteKeyboardEvent: function(event)
291 // Do not interfere with text editing.
292 var currentFocusElement = WebInspector.currentFocusElement();
293 if (currentFocusElement && WebInspector.isBeingEdited(currentFocusElement))
296 var targetNode = this.selectedDOMNode();
297 if (!targetNode || !this._canPaste(targetNode))
300 event.preventDefault();
301 this._performPaste(targetNode);
305 * @param {!WebInspector.DOMNode} targetNode
307 _performPaste: function(targetNode)
309 if (this._clipboardNodeData.isCut) {
310 this._clipboardNodeData.node.moveTo(targetNode, null, expandCallback.bind(this));
311 this._setClipboardData(null);
313 this._clipboardNodeData.node.copyTo(targetNode, null, expandCallback.bind(this));
317 * @param {?Protocol.Error} error
318 * @param {!DOMAgent.NodeId} nodeId
319 * @this {WebInspector.ElementsTreeOutline}
321 function expandCallback(error, nodeId)
325 var pastedNode = this._domModel.nodeForId(nodeId);
328 this.selectDOMNode(pastedNode);
333 * @param {boolean} visible
335 setVisible: function(visible)
337 this._visible = visible;
341 this._updateModifiedNodes();
342 if (this._selectedDOMNode)
343 this._revealAndSelectNode(this._selectedDOMNode, false);
346 addEventListener: function(eventType, listener, thisObject)
348 this._eventSupport.addEventListener(eventType, listener, thisObject);
351 removeEventListener: function(eventType, listener, thisObject)
353 this._eventSupport.removeEventListener(eventType, listener, thisObject);
358 return this._rootDOMNode;
363 if (this._rootDOMNode === x)
366 this._rootDOMNode = x;
368 this._isXMLMimeType = x && x.isXMLNode();
375 return this._isXMLMimeType;
379 * @return {?WebInspector.DOMNode}
381 selectedDOMNode: function()
383 return this._selectedDOMNode;
387 * @param {?WebInspector.DOMNode} node
388 * @param {boolean=} focus
390 selectDOMNode: function(node, focus)
392 if (this._selectedDOMNode === node) {
393 this._revealAndSelectNode(node, !focus);
397 this._selectedDOMNode = node;
398 this._revealAndSelectNode(node, !focus);
400 // The _revealAndSelectNode() method might find a different element if there is inlined text,
401 // and the select() call would change the selectedDOMNode and reenter this setter. So to
402 // avoid calling _selectedNodeChanged() twice, first check if _selectedDOMNode is the same
403 // node as the one passed in.
404 if (this._selectedDOMNode === node)
405 this._selectedNodeChanged();
413 var node = this.selectedDOMNode();
416 var treeElement = this.findTreeElement(node);
419 return treeElement._editing || false;
424 var selectedNode = this.selectedTreeElement ? this.selectedTreeElement._node : null;
426 this.removeChildren();
428 if (!this.rootDOMNode)
432 if (this._includeRootDOMNode) {
433 treeElement = new WebInspector.ElementsTreeElement(this.rootDOMNode);
434 treeElement.selectable = this._selectEnabled;
435 this.appendChild(treeElement);
437 // FIXME: this could use findTreeElement to reuse a tree element if it already exists
438 var node = this.rootDOMNode.firstChild;
440 treeElement = new WebInspector.ElementsTreeElement(node);
441 treeElement.selectable = this._selectEnabled;
442 this.appendChild(treeElement);
443 node = node.nextSibling;
448 this._revealAndSelectNode(selectedNode, true);
451 updateSelection: function()
453 if (!this.selectedTreeElement)
455 var element = this.treeOutline.selectedTreeElement;
456 element.updateSelection();
460 * @param {!WebInspector.DOMNode} node
462 updateOpenCloseTags: function(node)
464 var treeElement = this.findTreeElement(node);
466 treeElement.updateTitle();
467 var children = treeElement.children;
468 var closingTagElement = children[children.length - 1];
469 if (closingTagElement && closingTagElement._elementCloseTag)
470 closingTagElement.updateTitle();
473 _selectedNodeChanged: function()
475 this._eventSupport.dispatchEventToListeners(WebInspector.ElementsTreeOutline.Events.SelectedNodeChanged, this._selectedDOMNode);
479 * @param {!Array.<!WebInspector.DOMNode>} nodes
481 _fireElementsTreeUpdated: function(nodes)
483 this._eventSupport.dispatchEventToListeners(WebInspector.ElementsTreeOutline.Events.ElementsTreeUpdated, nodes);
487 * @param {!WebInspector.DOMNode} node
488 * @return {?TreeElement}
490 findTreeElement: function(node)
492 function parentNode(node)
494 return node.parentNode;
497 var treeElement = TreeOutline.prototype.findTreeElement.call(this, node, parentNode);
498 if (!treeElement && node.nodeType() === Node.TEXT_NODE) {
499 // The text node might have been inlined if it was short, so try to find the parent element.
500 treeElement = TreeOutline.prototype.findTreeElement.call(this, node.parentNode, parentNode);
507 * @param {!WebInspector.DOMNode} node
508 * @return {?TreeElement}
510 createTreeElementFor: function(node)
512 var treeElement = this.findTreeElement(node);
515 if (!node.parentNode)
518 treeElement = this.createTreeElementFor(node.parentNode);
519 return treeElement ? treeElement._showChild(node) : null;
522 set suppressRevealAndSelect(x)
524 if (this._suppressRevealAndSelect === x)
526 this._suppressRevealAndSelect = x;
530 * @param {?WebInspector.DOMNode} node
531 * @param {boolean} omitFocus
533 _revealAndSelectNode: function(node, omitFocus)
535 if (this._suppressRevealAndSelect)
538 if (!this._includeRootDOMNode && node === this.rootDOMNode && this.rootDOMNode)
539 node = this.rootDOMNode.firstChild;
542 var treeElement = this.createTreeElementFor(node);
546 treeElement.revealAndSelect(omitFocus);
550 * @return {?TreeElement}
552 _treeElementFromEvent: function(event)
554 var scrollContainer = this.element.parentElement;
556 // We choose this X coordinate based on the knowledge that our list
557 // items extend at least to the right edge of the outer <ol> container.
558 // In the no-word-wrap mode the outer <ol> may be wider than the tree container
559 // (and partially hidden), in which case we are left to use only its right boundary.
560 var x = scrollContainer.totalOffsetLeft() + scrollContainer.offsetWidth - 36;
564 // Our list items have 1-pixel cracks between them vertically. We avoid
565 // the cracks by checking slightly above and slightly below the mouse
566 // and seeing if we hit the same element each time.
567 var elementUnderMouse = this.treeElementFromPoint(x, y);
568 var elementAboveMouse = this.treeElementFromPoint(x, y - 2);
570 if (elementUnderMouse === elementAboveMouse)
571 element = elementUnderMouse;
573 element = this.treeElementFromPoint(x, y + 2);
578 _onmousedown: function(event)
580 var element = this._treeElementFromEvent(event);
582 if (!element || element.isEventWithinDisclosureTriangle(event))
588 _onmousemove: function(event)
590 var element = this._treeElementFromEvent(event);
591 if (element && this._previousHoveredElement === element)
594 if (this._previousHoveredElement) {
595 this._previousHoveredElement.hovered = false;
596 delete this._previousHoveredElement;
600 element.hovered = true;
601 this._previousHoveredElement = element;
604 if (element && element._node)
605 this._domModel.highlightDOMNodeWithConfig(element._node.id, { mode: "all", showInfo: !WebInspector.KeyboardShortcut.eventHasCtrlOrMeta(event) });
607 this._domModel.hideDOMNodeHighlight();
610 _onmouseout: function(event)
612 var nodeUnderMouse = document.elementFromPoint(event.pageX, event.pageY);
613 if (nodeUnderMouse && nodeUnderMouse.isDescendant(this.element))
616 if (this._previousHoveredElement) {
617 this._previousHoveredElement.hovered = false;
618 delete this._previousHoveredElement;
621 this._domModel.hideDOMNodeHighlight();
624 _ondragstart: function(event)
626 if (!window.getSelection().isCollapsed)
628 if (event.target.nodeName === "A")
631 var treeElement = this._treeElementFromEvent(event);
635 if (!this._isValidDragSourceOrTarget(treeElement))
638 if (treeElement._node.nodeName() === "BODY" || treeElement._node.nodeName() === "HEAD")
641 event.dataTransfer.setData("text/plain", treeElement.listItemElement.textContent.replace(/\u200b/g, ""));
642 event.dataTransfer.effectAllowed = "copyMove";
643 this._treeElementBeingDragged = treeElement;
645 this._domModel.hideDOMNodeHighlight();
650 _ondragover: function(event)
652 if (!this._treeElementBeingDragged)
655 var treeElement = this._treeElementFromEvent(event);
656 if (!this._isValidDragSourceOrTarget(treeElement))
659 var node = treeElement._node;
661 if (node === this._treeElementBeingDragged._node)
663 node = node.parentNode;
666 treeElement.updateSelection();
667 treeElement.listItemElement.classList.add("elements-drag-over");
668 this._dragOverTreeElement = treeElement;
669 event.preventDefault();
670 event.dataTransfer.dropEffect = 'move';
674 _ondragleave: function(event)
676 this._clearDragOverTreeElementMarker();
677 event.preventDefault();
682 * @param {?TreeElement} treeElement
685 _isValidDragSourceOrTarget: function(treeElement)
690 var node = treeElement.representedObject;
691 if (!(node instanceof WebInspector.DOMNode))
694 if (!node.parentNode || node.parentNode.nodeType() !== Node.ELEMENT_NODE)
700 _ondrop: function(event)
702 event.preventDefault();
703 var treeElement = this._treeElementFromEvent(event);
705 this._doMove(treeElement);
709 * @param {!TreeElement} treeElement
711 _doMove: function(treeElement)
713 if (!this._treeElementBeingDragged)
719 if (treeElement._elementCloseTag) {
720 // Drop onto closing tag -> insert as last child.
721 parentNode = treeElement._node;
723 var dragTargetNode = treeElement._node;
724 parentNode = dragTargetNode.parentNode;
725 anchorNode = dragTargetNode;
728 var wasExpanded = this._treeElementBeingDragged.expanded;
729 this._treeElementBeingDragged._node.moveTo(parentNode, anchorNode, this._selectNodeAfterEdit.bind(this, wasExpanded));
731 delete this._treeElementBeingDragged;
734 _ondragend: function(event)
736 event.preventDefault();
737 this._clearDragOverTreeElementMarker();
738 delete this._treeElementBeingDragged;
741 _clearDragOverTreeElementMarker: function()
743 if (this._dragOverTreeElement) {
744 this._dragOverTreeElement.updateSelection();
745 this._dragOverTreeElement.listItemElement.classList.remove("elements-drag-over");
746 delete this._dragOverTreeElement;
751 * @param {!Event} event
753 _onkeydown: function(event)
755 var keyboardEvent = /** @type {!KeyboardEvent} */ (event);
756 var node = /** @type {!WebInspector.DOMNode} */ (this.selectedDOMNode());
757 console.assert(node);
758 var treeElement = this.getCachedTreeElement(node);
762 if (!treeElement._editing && WebInspector.KeyboardShortcut.hasNoModifiers(keyboardEvent) && keyboardEvent.keyCode === WebInspector.KeyboardShortcut.Keys.H.code) {
763 this._toggleHideShortcut(node);
769 _contextMenuEventFired: function(event)
771 var treeElement = this._treeElementFromEvent(event);
775 var contextMenu = new WebInspector.ContextMenu(event);
777 var isPseudoElement = !!treeElement._node.pseudoType();
778 var isTag = treeElement._node.nodeType() === Node.ELEMENT_NODE && !isPseudoElement;
779 var textNode = event.target.enclosingNodeOrSelfWithClass("webkit-html-text-node");
780 if (textNode && textNode.classList.contains("bogus"))
782 var commentNode = event.target.enclosingNodeOrSelfWithClass("webkit-html-comment");
783 contextMenu.appendApplicableItems(event.target);
785 contextMenu.appendSeparator();
786 treeElement._populateTextContextMenu(contextMenu, textNode);
788 contextMenu.appendSeparator();
789 treeElement._populateTagContextMenu(contextMenu, event);
790 } else if (commentNode) {
791 contextMenu.appendSeparator();
792 treeElement._populateNodeContextMenu(contextMenu);
793 } else if (isPseudoElement) {
794 treeElement._populateScrollIntoView(contextMenu);
797 contextMenu.appendApplicableItems(treeElement._node);
801 _updateModifiedNodes: function()
803 if (this._elementsTreeUpdater)
804 this._elementsTreeUpdater._updateModifiedNodes();
807 handleShortcut: function(event)
809 var node = this.selectedDOMNode();
810 var treeElement = this.getCachedTreeElement(node);
811 if (!node || !treeElement)
814 if (event.keyIdentifier === "F2" && treeElement.hasEditableNode()) {
815 this._toggleEditAsHTML(node);
816 event.handled = true;
820 if (WebInspector.KeyboardShortcut.eventHasCtrlOrMeta(event) && node.parentNode) {
821 if (event.keyIdentifier === "Up" && node.previousSibling) {
822 node.moveTo(node.parentNode, node.previousSibling, this._selectNodeAfterEdit.bind(this, treeElement.expanded));
823 event.handled = true;
826 if (event.keyIdentifier === "Down" && node.nextSibling) {
827 node.moveTo(node.parentNode, node.nextSibling.nextSibling, this._selectNodeAfterEdit.bind(this, treeElement.expanded));
828 event.handled = true;
835 * @param {!WebInspector.DOMNode} node
837 _toggleEditAsHTML: function(node)
839 var treeElement = this.getCachedTreeElement(node);
843 if (treeElement._editing && treeElement._htmlEditElement && WebInspector.isBeingEdited(treeElement._htmlEditElement))
844 treeElement._editing.commit();
846 treeElement._editAsHTML();
850 * @param {boolean} wasExpanded
851 * @param {?Protocol.Error} error
852 * @param {!DOMAgent.NodeId=} nodeId
854 _selectNodeAfterEdit: function(wasExpanded, error, nodeId)
859 // Select it and expand if necessary. We force tree update so that it processes dom events and is up to date.
860 this._updateModifiedNodes();
862 var newNode = nodeId ? this._domModel.nodeForId(nodeId) : null;
866 this.selectDOMNode(newNode, true);
868 var newTreeItem = this.findTreeElement(newNode);
871 newTreeItem.expand();
877 * Runs a script on the node's remote object that toggles a class name on
878 * the node and injects a stylesheet into the head of the node's document
879 * containing a rule to set "visibility: hidden" on the class and all it's
882 * @param {!WebInspector.DOMNode} node
883 * @param {function(?WebInspector.RemoteObject, boolean=)=} userCallback
885 _toggleHideShortcut: function(node, userCallback)
887 var pseudoType = node.pseudoType();
888 var effectiveNode = pseudoType ? node.parentNode : node;
892 function resolvedNode(object)
898 * @param {?string} pseudoType
899 * @suppressReceiverCheck
902 function toggleClassAndInjectStyleRule(pseudoType)
904 const classNamePrefix = "__web-inspector-hide";
905 const classNameSuffix = "-shortcut__";
906 const styleTagId = "__web-inspector-hide-shortcut-style__";
907 const styleRules = ".__web-inspector-hide-shortcut__, .__web-inspector-hide-shortcut__ * { visibility: hidden !important; } .__web-inspector-hidebefore-shortcut__::before { visibility: hidden !important; } .__web-inspector-hideafter-shortcut__::after { visibility: hidden !important; }";
909 var className = classNamePrefix + (pseudoType || "") + classNameSuffix;
910 this.classList.toggle(className);
912 var style = document.head.querySelector("style#" + styleTagId);
916 style = document.createElement("style");
917 style.id = styleTagId;
918 style.type = "text/css";
919 style.textContent = styleRules;
920 document.head.appendChild(style);
923 object.callFunction(toggleClassAndInjectStyleRule, [{ value: pseudoType }], userCallback);
927 effectiveNode.resolveToObject("", resolvedNode);
930 __proto__: TreeOutline.prototype
936 WebInspector.ElementsTreeOutline.ElementDecorator = function()
940 WebInspector.ElementsTreeOutline.ElementDecorator.prototype = {
942 * @param {!WebInspector.DOMNode} node
945 decorate: function(node)
950 * @param {!WebInspector.DOMNode} node
953 decorateAncestor: function(node)
960 * @implements {WebInspector.ElementsTreeOutline.ElementDecorator}
962 WebInspector.ElementsTreeOutline.PseudoStateDecorator = function()
964 WebInspector.ElementsTreeOutline.ElementDecorator.call(this);
967 WebInspector.ElementsTreeOutline.PseudoStateDecorator.prototype = {
969 * @param {!WebInspector.DOMNode} node
972 decorate: function(node)
974 if (node.nodeType() !== Node.ELEMENT_NODE)
976 var propertyValue = node.getUserProperty(WebInspector.CSSStyleModel.PseudoStatePropertyName);
979 return WebInspector.UIString("Element state: %s", ":" + propertyValue.join(", :"));
983 * @param {!WebInspector.DOMNode} node
986 decorateAncestor: function(node)
988 if (node.nodeType() !== Node.ELEMENT_NODE)
991 var descendantCount = node.descendantUserPropertyCount(WebInspector.CSSStyleModel.PseudoStatePropertyName);
992 if (!descendantCount)
994 if (descendantCount === 1)
995 return WebInspector.UIString("%d descendant with forced state", descendantCount);
996 return WebInspector.UIString("%d descendants with forced state", descendantCount);
1002 * @extends {TreeElement}
1003 * @param {!WebInspector.DOMNode} node
1004 * @param {boolean=} elementCloseTag
1006 WebInspector.ElementsTreeElement = function(node, elementCloseTag)
1008 // The title will be updated in onattach.
1009 TreeElement.call(this, "", node);
1012 this._elementCloseTag = elementCloseTag;
1013 this._updateHasChildren();
1015 if (this._node.nodeType() == Node.ELEMENT_NODE && !elementCloseTag)
1016 this._canAddAttributes = true;
1017 this._searchQuery = null;
1018 this._expandedChildrenLimit = WebInspector.ElementsTreeElement.InitialChildrenLimit;
1021 WebInspector.ElementsTreeElement.InitialChildrenLimit = 500;
1023 // A union of HTML4 and HTML5-Draft elements that explicitly
1024 // or implicitly (for HTML5) forbid the closing tag.
1025 WebInspector.ElementsTreeElement.ForbiddenClosingTagElements = [
1026 "area", "base", "basefont", "br", "canvas", "col", "command", "embed", "frame",
1027 "hr", "img", "input", "keygen", "link", "menuitem", "meta", "param", "source", "track", "wbr"
1030 // These tags we do not allow editing their tag name.
1031 WebInspector.ElementsTreeElement.EditTagBlacklist = [
1032 "html", "head", "body"
1035 WebInspector.ElementsTreeElement.prototype = {
1036 highlightSearchResults: function(searchQuery)
1038 if (this._searchQuery !== searchQuery) {
1039 this._updateSearchHighlight(false);
1040 delete this._highlightResult; // A new search query.
1043 this._searchQuery = searchQuery;
1044 this._searchHighlightsVisible = true;
1045 this.updateTitle(true);
1048 hideSearchHighlights: function()
1050 delete this._searchHighlightsVisible;
1051 this._updateSearchHighlight(false);
1054 _updateSearchHighlight: function(show)
1056 if (!this._highlightResult)
1059 function updateEntryShow(entry)
1061 switch (entry.type) {
1063 entry.parent.insertBefore(entry.node, entry.nextSibling);
1066 entry.node.textContent = entry.newText;
1071 function updateEntryHide(entry)
1073 switch (entry.type) {
1075 entry.node.remove();
1078 entry.node.textContent = entry.oldText;
1083 // Preserve the semantic of node by following the order of updates for hide and show.
1085 for (var i = 0, size = this._highlightResult.length; i < size; ++i)
1086 updateEntryShow(this._highlightResult[i]);
1088 for (var i = (this._highlightResult.length - 1); i >= 0; --i)
1089 updateEntryHide(this._highlightResult[i]);
1094 * @param {boolean} inClipboard
1096 setInClipboard: function(inClipboard)
1098 if (this._inClipboard === inClipboard)
1100 this._inClipboard = inClipboard;
1101 this.listItemElement.classList.toggle("in-clipboard", inClipboard);
1106 return this._hovered;
1111 if (this._hovered === x)
1116 if (this.listItemElement) {
1118 this.updateSelection();
1119 this.listItemElement.classList.add("hovered");
1121 this.listItemElement.classList.remove("hovered");
1126 get expandedChildrenLimit()
1128 return this._expandedChildrenLimit;
1131 set expandedChildrenLimit(x)
1133 if (this._expandedChildrenLimit === x)
1136 this._expandedChildrenLimit = x;
1137 if (this.treeOutline && !this._updateChildrenInProgress)
1138 this._updateChildren(true);
1141 get expandedChildCount()
1143 var count = this.children.length;
1144 if (count && this.children[count - 1]._elementCloseTag)
1146 if (count && this.children[count - 1].expandAllButton)
1152 * @param {!WebInspector.DOMNode} child
1153 * @return {?WebInspector.ElementsTreeElement}
1155 _showChild: function(child)
1157 if (this._elementCloseTag)
1160 var index = this._visibleChildren().indexOf(child);
1164 if (index >= this.expandedChildrenLimit) {
1165 this._expandedChildrenLimit = index + 1;
1166 this._updateChildren(true);
1169 // Whether index-th child is visible in the children tree
1170 return this.expandedChildCount > index ? this.children[index] : null;
1173 updateSelection: function()
1175 var listItemElement = this.listItemElement;
1176 if (!listItemElement)
1179 if (!this._readyToUpdateSelection) {
1180 if (document.body.offsetWidth > 0)
1181 this._readyToUpdateSelection = true;
1183 // The stylesheet hasn't loaded yet or the window is closed,
1184 // so we can't calculate what we need. Return early.
1189 if (!this.selectionElement) {
1190 this.selectionElement = document.createElement("div");
1191 this.selectionElement.className = "selection selected";
1192 listItemElement.insertBefore(this.selectionElement, listItemElement.firstChild);
1195 this.selectionElement.style.height = listItemElement.offsetHeight + "px";
1198 onattach: function()
1200 if (this._hovered) {
1201 this.updateSelection();
1202 this.listItemElement.classList.add("hovered");
1206 this._preventFollowingLinksOnDoubleClick();
1207 this.listItemElement.draggable = true;
1210 _preventFollowingLinksOnDoubleClick: function()
1212 var links = this.listItemElement.querySelectorAll("li .webkit-html-tag > .webkit-html-attribute > .webkit-html-external-link, li .webkit-html-tag > .webkit-html-attribute > .webkit-html-resource-link");
1216 for (var i = 0; i < links.length; ++i)
1217 links[i].preventFollowOnDoubleClick = true;
1220 onpopulate: function()
1222 this.populated = true;
1223 if (this.children.length || !this.hasChildren)
1226 this.updateChildren();
1230 * @param {boolean=} fullRefresh
1232 updateChildren: function(fullRefresh)
1234 if (!this.hasChildren)
1236 console.assert(!this._elementCloseTag);
1237 this._node.getChildNodes(this._updateChildren.bind(this, fullRefresh));
1241 * @param {!WebInspector.DOMNode} child
1242 * @param {number} index
1243 * @param {boolean=} closingTag
1244 * @return {!WebInspector.ElementsTreeElement}
1246 insertChildElement: function(child, index, closingTag)
1248 var newElement = new WebInspector.ElementsTreeElement(child, closingTag);
1249 newElement.selectable = this.treeOutline._selectEnabled;
1250 this.insertChild(newElement, index);
1254 moveChild: function(child, targetIndex)
1256 var wasSelected = child.selected;
1257 this.removeChild(child);
1258 this.insertChild(child, targetIndex);
1264 * @param {boolean=} fullRefresh
1266 _updateChildren: function(fullRefresh)
1268 if (this._updateChildrenInProgress || !this.treeOutline._visible)
1271 this._updateChildrenInProgress = true;
1272 var selectedNode = this.treeOutline.selectedDOMNode();
1273 var originalScrollTop = 0;
1275 var treeOutlineContainerElement = this.treeOutline.element.parentNode;
1276 originalScrollTop = treeOutlineContainerElement.scrollTop;
1277 var selectedTreeElement = this.treeOutline.selectedTreeElement;
1278 if (selectedTreeElement && selectedTreeElement.hasAncestor(this))
1280 this.removeChildren();
1284 * @this {WebInspector.ElementsTreeElement}
1285 * @return {?WebInspector.ElementsTreeElement}
1287 function updateChildrenOfNode()
1289 var treeOutline = this.treeOutline;
1290 var visibleChildren = this._visibleChildren();
1291 var treeChildIndex = 0;
1292 var elementToSelect = null;
1294 for (var i = 0; i < visibleChildren.length; ++i) {
1295 var child = visibleChildren[i];
1296 var currentTreeElement = this.children[treeChildIndex];
1297 if (!currentTreeElement || currentTreeElement._node !== child) {
1298 // Find any existing element that is later in the children list.
1299 var existingTreeElement = null;
1300 for (var j = (treeChildIndex + 1), size = this.expandedChildCount; j < size; ++j) {
1301 if (this.children[j]._node === child) {
1302 existingTreeElement = this.children[j];
1307 if (existingTreeElement && existingTreeElement.parent === this) {
1308 // If an existing element was found and it has the same parent, just move it.
1309 this.moveChild(existingTreeElement, treeChildIndex);
1311 // No existing element found, insert a new element.
1312 if (treeChildIndex < this.expandedChildrenLimit) {
1313 var newElement = this.insertChildElement(child, treeChildIndex);
1314 if (child === selectedNode)
1315 elementToSelect = newElement;
1316 if (this.expandedChildCount > this.expandedChildrenLimit)
1317 this.expandedChildrenLimit++;
1324 return elementToSelect;
1327 // Remove any tree elements that no longer have this node (or this node's contentDocument) as their parent.
1328 for (var i = (this.children.length - 1); i >= 0; --i) {
1329 var currentChild = this.children[i];
1330 var currentNode = currentChild._node;
1333 var currentParentNode = currentNode.parentNode;
1335 if (currentParentNode === this._node)
1338 var selectedTreeElement = this.treeOutline.selectedTreeElement;
1339 if (selectedTreeElement && (selectedTreeElement === currentChild || selectedTreeElement.hasAncestor(currentChild)))
1342 this.removeChildAtIndex(i);
1345 var elementToSelect = updateChildrenOfNode.call(this);
1347 this._adjustCollapsedRange();
1349 var lastChild = this.children[this.children.length - 1];
1350 if (this._node.nodeType() === Node.ELEMENT_NODE && this.hasChildren)
1351 this.insertChildElement(this._node, this.children.length, true);
1353 // We want to restore the original selection and tree scroll position after a full refresh, if possible.
1354 if (fullRefresh && elementToSelect) {
1355 elementToSelect.select();
1356 if (treeOutlineContainerElement && originalScrollTop <= treeOutlineContainerElement.scrollHeight)
1357 treeOutlineContainerElement.scrollTop = originalScrollTop;
1360 delete this._updateChildrenInProgress;
1363 _adjustCollapsedRange: function()
1365 var visibleChildren = this._visibleChildren();
1366 // Ensure precondition: only the tree elements for node children are found in the tree
1367 // (not the Expand All button or the closing tag).
1368 if (this.expandAllButtonElement && this.expandAllButtonElement.__treeElement.parent)
1369 this.removeChild(this.expandAllButtonElement.__treeElement);
1371 const childNodeCount = visibleChildren.length;
1373 // In case some nodes from the expanded range were removed, pull some nodes from the collapsed range into the expanded range at the bottom.
1374 for (var i = this.expandedChildCount, limit = Math.min(this.expandedChildrenLimit, childNodeCount); i < limit; ++i)
1375 this.insertChildElement(visibleChildren[i], i);
1377 const expandedChildCount = this.expandedChildCount;
1378 if (childNodeCount > this.expandedChildCount) {
1379 var targetButtonIndex = expandedChildCount;
1380 if (!this.expandAllButtonElement) {
1381 var button = document.createElement("button");
1382 button.className = "text-button";
1384 var item = new TreeElement(button, null, false);
1385 item.selectable = false;
1386 item.expandAllButton = true;
1387 this.insertChild(item, targetButtonIndex);
1388 this.expandAllButtonElement = item.listItemElement.firstChild;
1389 this.expandAllButtonElement.__treeElement = item;
1390 this.expandAllButtonElement.addEventListener("click", this.handleLoadAllChildren.bind(this), false);
1391 } else if (!this.expandAllButtonElement.__treeElement.parent)
1392 this.insertChild(this.expandAllButtonElement.__treeElement, targetButtonIndex);
1393 this.expandAllButtonElement.textContent = WebInspector.UIString("Show All Nodes (%d More)", childNodeCount - expandedChildCount);
1394 } else if (this.expandAllButtonElement)
1395 delete this.expandAllButtonElement;
1398 handleLoadAllChildren: function()
1400 this.expandedChildrenLimit = Math.max(this._visibleChildCount(), this.expandedChildrenLimit + WebInspector.ElementsTreeElement.InitialChildrenLimit);
1403 expandRecursively: function()
1406 * @this {WebInspector.ElementsTreeElement}
1410 TreeElement.prototype.expandRecursively.call(this, Number.MAX_VALUE);
1413 this._node.getSubtree(-1, callback.bind(this));
1419 onexpand: function()
1421 if (this._elementCloseTag)
1425 this.treeOutline.updateSelection();
1428 oncollapse: function()
1430 if (this._elementCloseTag)
1434 this.treeOutline.updateSelection();
1440 onreveal: function()
1442 if (this.listItemElement) {
1443 var tagSpans = this.listItemElement.getElementsByClassName("webkit-html-tag-name");
1444 if (tagSpans.length)
1445 tagSpans[0].scrollIntoViewIfNeeded(true);
1447 this.listItemElement.scrollIntoViewIfNeeded(true);
1452 * @param {boolean=} omitFocus
1453 * @param {boolean=} selectedByUser
1456 select: function(omitFocus, selectedByUser)
1458 if (!this.treeOutline._handlePickNode(this.title, this._node))
1460 return TreeElement.prototype.select.call(this, omitFocus, selectedByUser);
1465 * @param {boolean=} selectedByUser
1468 onselect: function(selectedByUser)
1470 this.treeOutline.suppressRevealAndSelect = true;
1471 this.treeOutline.selectDOMNode(this._node, selectedByUser);
1473 this._node.highlight();
1474 this.updateSelection();
1475 this.treeOutline.suppressRevealAndSelect = false;
1483 ondelete: function()
1485 var startTagTreeElement = this.treeOutline.findTreeElement(this._node);
1486 startTagTreeElement ? startTagTreeElement.remove() : this.remove();
1496 // On Enter or Return start editing the first attribute
1497 // or create a new attribute on the selected element.
1501 this._startEditing();
1503 // prevent a newline from being immediately inserted
1507 selectOnMouseDown: function(event)
1509 TreeElement.prototype.selectOnMouseDown.call(this, event);
1514 if (this.treeOutline._showInElementsPanelEnabled) {
1515 WebInspector.inspectorView.showPanel("elements");
1516 this.treeOutline.selectDOMNode(this._node, true);
1519 // Prevent selecting the nearest word on double click.
1520 if (event.detail >= 2)
1521 event.preventDefault();
1528 ondblclick: function(event)
1530 if (this._editing || this._elementCloseTag)
1533 if (this._startEditingTarget(/** @type {!Element} */(event.target)))
1536 if (this.hasChildren && !this.expanded)
1544 hasEditableNode: function()
1546 return !this.representedObject.isShadowRoot() && !this.representedObject.ancestorUserAgentShadowRoot();
1549 _insertInLastAttributePosition: function(tag, node)
1551 if (tag.getElementsByClassName("webkit-html-attribute").length > 0)
1552 tag.insertBefore(node, tag.lastChild);
1554 var nodeName = tag.textContent.match(/^<(.*?)>$/)[1];
1555 tag.textContent = '';
1556 tag.createTextChild('<' + nodeName);
1557 tag.appendChild(node);
1558 tag.createTextChild('>');
1561 this.updateSelection();
1565 * @param {!Element} eventTarget
1568 _startEditingTarget: function(eventTarget)
1570 if (this.treeOutline.selectedDOMNode() != this._node)
1573 if (this._node.nodeType() != Node.ELEMENT_NODE && this._node.nodeType() != Node.TEXT_NODE)
1576 if (this.treeOutline._pickNodeMode)
1579 var textNode = eventTarget.enclosingNodeOrSelfWithClass("webkit-html-text-node");
1581 return this._startEditingTextNode(textNode);
1583 var attribute = eventTarget.enclosingNodeOrSelfWithClass("webkit-html-attribute");
1585 return this._startEditingAttribute(attribute, eventTarget);
1587 var tagName = eventTarget.enclosingNodeOrSelfWithClass("webkit-html-tag-name");
1589 return this._startEditingTagName(tagName);
1591 var newAttribute = eventTarget.enclosingNodeOrSelfWithClass("add-attribute");
1593 return this._addNewAttribute();
1599 * @param {!WebInspector.ContextMenu} contextMenu
1600 * @param {!Event} event
1602 _populateTagContextMenu: function(contextMenu, event)
1604 // Add attribute-related actions.
1605 var treeElement = this._elementCloseTag ? this.treeOutline.findTreeElement(this._node) : this;
1606 contextMenu.appendItem(WebInspector.UIString(WebInspector.useLowerCaseMenuTitles() ? "Add attribute" : "Add Attribute"), treeElement._addNewAttribute.bind(treeElement));
1608 var attribute = event.target.enclosingNodeOrSelfWithClass("webkit-html-attribute");
1609 var newAttribute = event.target.enclosingNodeOrSelfWithClass("add-attribute");
1610 if (attribute && !newAttribute)
1611 contextMenu.appendItem(WebInspector.UIString(WebInspector.useLowerCaseMenuTitles() ? "Edit attribute" : "Edit Attribute"), this._startEditingAttribute.bind(this, attribute, event.target));
1612 contextMenu.appendSeparator();
1613 if (this.treeOutline._setPseudoClassCallback) {
1614 var pseudoSubMenu = contextMenu.appendSubMenuItem(WebInspector.UIString(WebInspector.useLowerCaseMenuTitles() ? "Force element state" : "Force Element State"));
1615 this._populateForcedPseudoStateItems(pseudoSubMenu);
1616 contextMenu.appendSeparator();
1618 this._populateNodeContextMenu(contextMenu);
1619 this._populateScrollIntoView(contextMenu);
1623 * @param {!WebInspector.ContextMenu} contextMenu
1625 _populateScrollIntoView: function(contextMenu)
1627 contextMenu.appendSeparator();
1628 contextMenu.appendItem(WebInspector.UIString(WebInspector.useLowerCaseMenuTitles() ? "Scroll into view" : "Scroll into View"), this._scrollIntoView.bind(this));
1631 _populateForcedPseudoStateItems: function(subMenu)
1633 const pseudoClasses = ["active", "hover", "focus", "visited"];
1634 var node = this._node;
1635 var forcedPseudoState = (node ? node.getUserProperty("pseudoState") : null) || [];
1636 for (var i = 0; i < pseudoClasses.length; ++i) {
1637 var pseudoClassForced = forcedPseudoState.indexOf(pseudoClasses[i]) >= 0;
1638 subMenu.appendCheckboxItem(":" + pseudoClasses[i], this.treeOutline._setPseudoClassCallback.bind(null, node, pseudoClasses[i], !pseudoClassForced), pseudoClassForced, false);
1642 _populateTextContextMenu: function(contextMenu, textNode)
1645 contextMenu.appendItem(WebInspector.UIString(WebInspector.useLowerCaseMenuTitles() ? "Edit text" : "Edit Text"), this._startEditingTextNode.bind(this, textNode));
1646 this._populateNodeContextMenu(contextMenu);
1649 _populateNodeContextMenu: function(contextMenu)
1651 // Add free-form node-related actions.
1652 var openTagElement = this.treeOutline.getCachedTreeElement(this.representedObject) || this;
1653 var isEditable = this.hasEditableNode();
1654 if (isEditable && !this._editing)
1655 contextMenu.appendItem(WebInspector.UIString("Edit as HTML"), openTagElement._editAsHTML.bind(openTagElement));
1656 var isShadowRoot = this.representedObject.isShadowRoot();
1658 // Place it here so that all "Copy"-ing items stick together.
1659 if (this.representedObject.nodeType() === Node.ELEMENT_NODE)
1660 contextMenu.appendItem(WebInspector.UIString(WebInspector.useLowerCaseMenuTitles() ? "Copy CSS path" : "Copy CSS Path"), this._copyCSSPath.bind(this));
1662 contextMenu.appendItem(WebInspector.UIString("Copy XPath"), this._copyXPath.bind(this));
1663 if (!isShadowRoot) {
1664 var treeOutline = this.treeOutline;
1665 contextMenu.appendItem(WebInspector.UIString("Copy"), treeOutline._performCopyOrCut.bind(treeOutline, false, this.representedObject));
1666 contextMenu.appendItem(WebInspector.UIString("Cut"), treeOutline._performCopyOrCut.bind(treeOutline, true, this.representedObject), !this.hasEditableNode());
1667 contextMenu.appendItem(WebInspector.UIString("Paste"), treeOutline._pasteNode.bind(treeOutline, this.representedObject), !treeOutline._canPaste(this.representedObject));
1671 contextMenu.appendItem(WebInspector.UIString(WebInspector.useLowerCaseMenuTitles() ? "Delete node" : "Delete Node"), this.remove.bind(this));
1674 _startEditing: function()
1676 if (this.treeOutline.selectedDOMNode() !== this._node)
1679 var listItem = this._listItemNode;
1681 if (this._canAddAttributes) {
1682 var attribute = listItem.getElementsByClassName("webkit-html-attribute")[0];
1684 return this._startEditingAttribute(attribute, attribute.getElementsByClassName("webkit-html-attribute-value")[0]);
1686 return this._addNewAttribute();
1689 if (this._node.nodeType() === Node.TEXT_NODE) {
1690 var textNode = listItem.getElementsByClassName("webkit-html-text-node")[0];
1692 return this._startEditingTextNode(textNode);
1697 _addNewAttribute: function()
1699 // Cannot just convert the textual html into an element without
1700 // a parent node. Use a temporary span container for the HTML.
1701 var container = document.createElement("span");
1702 this._buildAttributeDOM(container, " ", "");
1703 var attr = container.firstElementChild;
1704 attr.style.marginLeft = "2px"; // overrides the .editing margin rule
1705 attr.style.marginRight = "2px"; // overrides the .editing margin rule
1707 var tag = this.listItemElement.getElementsByClassName("webkit-html-tag")[0];
1708 this._insertInLastAttributePosition(tag, attr);
1709 attr.scrollIntoViewIfNeeded(true);
1710 return this._startEditingAttribute(attr, attr);
1713 _triggerEditAttribute: function(attributeName)
1715 var attributeElements = this.listItemElement.getElementsByClassName("webkit-html-attribute-name");
1716 for (var i = 0, len = attributeElements.length; i < len; ++i) {
1717 if (attributeElements[i].textContent === attributeName) {
1718 for (var elem = attributeElements[i].nextSibling; elem; elem = elem.nextSibling) {
1719 if (elem.nodeType !== Node.ELEMENT_NODE)
1722 if (elem.classList.contains("webkit-html-attribute-value"))
1723 return this._startEditingAttribute(elem.parentNode, elem);
1729 _startEditingAttribute: function(attribute, elementForSelection)
1731 if (WebInspector.isBeingEdited(attribute))
1734 var attributeNameElement = attribute.getElementsByClassName("webkit-html-attribute-name")[0];
1735 if (!attributeNameElement)
1738 var attributeName = attributeNameElement.textContent;
1739 var attributeValueElement = attribute.getElementsByClassName("webkit-html-attribute-value")[0];
1741 function removeZeroWidthSpaceRecursive(node)
1743 if (node.nodeType === Node.TEXT_NODE) {
1744 node.nodeValue = node.nodeValue.replace(/\u200B/g, "");
1748 if (node.nodeType !== Node.ELEMENT_NODE)
1751 for (var child = node.firstChild; child; child = child.nextSibling)
1752 removeZeroWidthSpaceRecursive(child);
1756 var listItemElement = attribute.enclosingNodeOrSelfWithNodeName("li");
1757 if (attributeName && attributeValueElement && listItemElement && listItemElement.treeElement)
1758 domNode = listItemElement.treeElement.representedObject;
1759 var attributeValue = domNode ? domNode.getAttribute(attributeName) : undefined;
1760 if (typeof attributeValue !== "undefined")
1761 attributeValueElement.textContent = attributeValue;
1763 // Remove zero-width spaces that were added by nodeTitleInfo.
1764 removeZeroWidthSpaceRecursive(attribute);
1766 var config = new WebInspector.InplaceEditor.Config(this._attributeEditingCommitted.bind(this), this._editingCancelled.bind(this), attributeName);
1768 function handleKeyDownEvents(event)
1770 var isMetaOrCtrl = WebInspector.isMac() ?
1771 event.metaKey && !event.shiftKey && !event.ctrlKey && !event.altKey :
1772 event.ctrlKey && !event.shiftKey && !event.metaKey && !event.altKey;
1773 if (isEnterKey(event) && (event.isMetaOrCtrlForTest || !config.multiline || isMetaOrCtrl))
1775 else if (event.keyCode === WebInspector.KeyboardShortcut.Keys.Esc.code || event.keyIdentifier === "U+001B")
1777 else if (event.keyIdentifier === "U+0009") // Tab key
1778 return "move-" + (event.shiftKey ? "backward" : "forward");
1780 WebInspector.handleElementValueModifications(event, attribute);
1785 config.customFinishHandler = handleKeyDownEvents;
1787 this._editing = WebInspector.InplaceEditor.startEditing(attribute, config);
1789 window.getSelection().setBaseAndExtent(elementForSelection, 0, elementForSelection, 1);
1795 * @param {!Element} textNodeElement
1797 _startEditingTextNode: function(textNodeElement)
1799 if (WebInspector.isBeingEdited(textNodeElement))
1802 var textNode = this._node;
1803 // We only show text nodes inline in elements if the element only
1804 // has a single child, and that child is a text node.
1805 if (textNode.nodeType() === Node.ELEMENT_NODE && textNode.firstChild)
1806 textNode = textNode.firstChild;
1808 var container = textNodeElement.enclosingNodeOrSelfWithClass("webkit-html-text-node");
1810 container.textContent = textNode.nodeValue(); // Strip the CSS or JS highlighting if present.
1811 var config = new WebInspector.InplaceEditor.Config(this._textNodeEditingCommitted.bind(this, textNode), this._editingCancelled.bind(this));
1812 this._editing = WebInspector.InplaceEditor.startEditing(textNodeElement, config);
1813 window.getSelection().setBaseAndExtent(textNodeElement, 0, textNodeElement, 1);
1819 * @param {!Element=} tagNameElement
1821 _startEditingTagName: function(tagNameElement)
1823 if (!tagNameElement) {
1824 tagNameElement = this.listItemElement.getElementsByClassName("webkit-html-tag-name")[0];
1825 if (!tagNameElement)
1829 var tagName = tagNameElement.textContent;
1830 if (WebInspector.ElementsTreeElement.EditTagBlacklist[tagName.toLowerCase()])
1833 if (WebInspector.isBeingEdited(tagNameElement))
1836 var closingTagElement = this._distinctClosingTagElement();
1839 * @param {!Event} event
1841 function keyupListener(event)
1843 if (closingTagElement)
1844 closingTagElement.textContent = "</" + tagNameElement.textContent + ">";
1848 * @param {!Element} element
1849 * @param {string} newTagName
1850 * @this {WebInspector.ElementsTreeElement}
1852 function editingComitted(element, newTagName)
1854 tagNameElement.removeEventListener('keyup', keyupListener, false);
1855 this._tagNameEditingCommitted.apply(this, arguments);
1859 * @this {WebInspector.ElementsTreeElement}
1861 function editingCancelled()
1863 tagNameElement.removeEventListener('keyup', keyupListener, false);
1864 this._editingCancelled.apply(this, arguments);
1867 tagNameElement.addEventListener('keyup', keyupListener, false);
1869 var config = new WebInspector.InplaceEditor.Config(editingComitted.bind(this), editingCancelled.bind(this), tagName);
1870 this._editing = WebInspector.InplaceEditor.startEditing(tagNameElement, config);
1871 window.getSelection().setBaseAndExtent(tagNameElement, 0, tagNameElement, 1);
1876 * @param {function(string, string)} commitCallback
1877 * @param {?Protocol.Error} error
1878 * @param {string} initialValue
1880 _startEditingAsHTML: function(commitCallback, error, initialValue)
1887 function consume(event)
1889 if (event.eventPhase === Event.AT_TARGET)
1890 event.consume(true);
1893 initialValue = this._convertWhitespaceToEntities(initialValue).text;
1895 this._htmlEditElement = document.createElement("div");
1896 this._htmlEditElement.className = "source-code elements-tree-editor";
1898 // Hide header items.
1899 var child = this.listItemElement.firstChild;
1901 child.style.display = "none";
1902 child = child.nextSibling;
1904 // Hide children item.
1905 if (this._childrenListNode)
1906 this._childrenListNode.style.display = "none";
1908 this.listItemElement.appendChild(this._htmlEditElement);
1909 this.treeOutline.childrenListElement.parentElement.addEventListener("mousedown", consume, false);
1911 this.updateSelection();
1914 * @param {!Element} element
1915 * @param {string} newValue
1916 * @this {WebInspector.ElementsTreeElement}
1918 function commit(element, newValue)
1920 commitCallback(initialValue, newValue);
1925 * @this {WebInspector.ElementsTreeElement}
1929 delete this._editing;
1930 delete this.treeOutline._multilineEditing;
1933 this.listItemElement.removeChild(this._htmlEditElement);
1934 delete this._htmlEditElement;
1935 // Unhide children item.
1936 if (this._childrenListNode)
1937 this._childrenListNode.style.removeProperty("display");
1938 // Unhide header items.
1939 var child = this.listItemElement.firstChild;
1941 child.style.removeProperty("display");
1942 child = child.nextSibling;
1945 this.treeOutline.childrenListElement.parentElement.removeEventListener("mousedown", consume, false);
1946 this.updateSelection();
1947 this.treeOutline.element.focus();
1950 var config = new WebInspector.InplaceEditor.Config(commit.bind(this), dispose.bind(this));
1951 config.setMultilineOptions(initialValue, { name: "xml", htmlMode: true }, "web-inspector-html", WebInspector.settings.domWordWrap.get(), true);
1952 this._editing = WebInspector.InplaceEditor.startEditing(this._htmlEditElement, config);
1953 this._editing.setWidth(this.treeOutline._visibleWidth);
1954 this.treeOutline._multilineEditing = this._editing;
1957 _attributeEditingCommitted: function(element, newText, oldText, attributeName, moveDirection)
1959 delete this._editing;
1961 var treeOutline = this.treeOutline;
1964 * @param {?Protocol.Error=} error
1965 * @this {WebInspector.ElementsTreeElement}
1967 function moveToNextAttributeIfNeeded(error)
1970 this._editingCancelled(element, attributeName);
1975 treeOutline._updateModifiedNodes();
1977 // Search for the attribute's position, and then decide where to move to.
1978 var attributes = this._node.attributes();
1979 for (var i = 0; i < attributes.length; ++i) {
1980 if (attributes[i].name !== attributeName)
1983 if (moveDirection === "backward") {
1985 this._startEditingTagName();
1987 this._triggerEditAttribute(attributes[i - 1].name);
1989 if (i === attributes.length - 1)
1990 this._addNewAttribute();
1992 this._triggerEditAttribute(attributes[i + 1].name);
1997 // Moving From the "New Attribute" position.
1998 if (moveDirection === "backward") {
1999 if (newText === " ") {
2000 // Moving from "New Attribute" that was not edited
2001 if (attributes.length > 0)
2002 this._triggerEditAttribute(attributes[attributes.length - 1].name);
2004 // Moving from "New Attribute" that holds new value
2005 if (attributes.length > 1)
2006 this._triggerEditAttribute(attributes[attributes.length - 2].name);
2008 } else if (moveDirection === "forward") {
2009 if (!/^\s*$/.test(newText))
2010 this._addNewAttribute();
2012 this._startEditingTagName();
2017 if ((attributeName.trim() || newText.trim()) && oldText !== newText) {
2018 this._node.setAttribute(attributeName, newText, moveToNextAttributeIfNeeded.bind(this));
2023 moveToNextAttributeIfNeeded.call(this);
2026 _tagNameEditingCommitted: function(element, newText, oldText, tagName, moveDirection)
2028 delete this._editing;
2033 var closingTagElement = self._distinctClosingTagElement();
2034 if (closingTagElement)
2035 closingTagElement.textContent = "</" + tagName + ">";
2037 self._editingCancelled(element, tagName);
2038 moveToNextAttributeIfNeeded.call(self);
2042 * @this {WebInspector.ElementsTreeElement}
2044 function moveToNextAttributeIfNeeded()
2046 if (moveDirection !== "forward") {
2047 this._addNewAttribute();
2051 var attributes = this._node.attributes();
2052 if (attributes.length > 0)
2053 this._triggerEditAttribute(attributes[0].name);
2055 this._addNewAttribute();
2058 newText = newText.trim();
2059 if (newText === oldText) {
2064 var treeOutline = this.treeOutline;
2065 var wasExpanded = this.expanded;
2067 function changeTagNameCallback(error, nodeId)
2069 if (error || !nodeId) {
2073 var newTreeItem = treeOutline._selectNodeAfterEdit(wasExpanded, error, nodeId);
2074 moveToNextAttributeIfNeeded.call(newTreeItem);
2077 this._node.setNodeName(newText, changeTagNameCallback);
2081 * @param {!WebInspector.DOMNode} textNode
2082 * @param {!Element} element
2083 * @param {string} newText
2085 _textNodeEditingCommitted: function(textNode, element, newText)
2087 delete this._editing;
2090 * @this {WebInspector.ElementsTreeElement}
2096 textNode.setNodeValue(newText, callback.bind(this));
2100 * @param {!Element} element
2101 * @param {*} context
2103 _editingCancelled: function(element, context)
2105 delete this._editing;
2107 // Need to restore attributes structure.
2112 * @return {!Element}
2114 _distinctClosingTagElement: function()
2116 // FIXME: Improve the Tree Element / Outline Abstraction to prevent crawling the DOM
2118 // For an expanded element, it will be the last element with class "close"
2119 // in the child element list.
2120 if (this.expanded) {
2121 var closers = this._childrenListNode.querySelectorAll(".close");
2122 return closers[closers.length-1];
2125 // Remaining cases are single line non-expanded elements with a closing
2126 // tag, or HTML elements without a closing tag (such as <br>). Return
2127 // null in the case where there isn't a closing tag.
2128 var tags = this.listItemElement.getElementsByClassName("webkit-html-tag");
2129 return (tags.length === 1 ? null : tags[tags.length-1]);
2133 * @param {boolean=} onlySearchQueryChanged
2135 updateTitle: function(onlySearchQueryChanged)
2137 // If we are editing, return early to prevent canceling the edit.
2138 // After editing is committed updateTitle will be called.
2142 if (onlySearchQueryChanged) {
2143 if (this._highlightResult)
2144 this._updateSearchHighlight(false);
2146 var nodeInfo = this._nodeTitleInfo(WebInspector.linkifyURLAsNode);
2147 if (nodeInfo.shadowRoot)
2148 this.listItemElement.classList.add("shadow-root");
2149 var highlightElement = document.createElement("span");
2150 highlightElement.className = "highlight";
2151 highlightElement.appendChild(nodeInfo.titleDOM);
2152 this.title = highlightElement;
2153 this._updateDecorations();
2154 delete this._highlightResult;
2157 delete this.selectionElement;
2159 this.updateSelection();
2160 this._preventFollowingLinksOnDoubleClick();
2161 this._highlightSearchResults();
2165 * @return {?Element}
2167 _createDecoratorElement: function()
2169 var node = this._node;
2170 var decoratorMessages = [];
2171 var parentDecoratorMessages = [];
2172 for (var i = 0; i < this.treeOutline._nodeDecorators.length; ++i) {
2173 var decorator = this.treeOutline._nodeDecorators[i];
2174 var message = decorator.decorate(node);
2176 decoratorMessages.push(message);
2180 if (this.expanded || this._elementCloseTag)
2183 message = decorator.decorateAncestor(node);
2185 parentDecoratorMessages.push(message)
2187 if (!decoratorMessages.length && !parentDecoratorMessages.length)
2190 var decoratorElement = document.createElement("div");
2191 decoratorElement.classList.add("elements-gutter-decoration");
2192 if (!decoratorMessages.length)
2193 decoratorElement.classList.add("elements-has-decorated-children");
2194 decoratorElement.title = decoratorMessages.concat(parentDecoratorMessages).join("\n");
2195 return decoratorElement;
2198 _updateDecorations: function()
2200 if (this._decoratorElement)
2201 this._decoratorElement.remove();
2202 this._decoratorElement = this._createDecoratorElement();
2203 if (this._decoratorElement && this.listItemElement)
2204 this.listItemElement.insertBefore(this._decoratorElement, this.listItemElement.firstChild);
2208 * @param {!Node} parentElement
2209 * @param {string} name
2210 * @param {string} value
2211 * @param {boolean=} forceValue
2212 * @param {!WebInspector.DOMNode=} node
2213 * @param {function(string, string, string, boolean=, string=)=} linkify
2215 _buildAttributeDOM: function(parentElement, name, value, forceValue, node, linkify)
2217 var closingPunctuationRegex = /[\/;:\)\]\}]/g;
2218 var highlightIndex = 0;
2220 var additionalHighlightOffset = 0;
2224 * @param {string} match
2225 * @param {number} replaceOffset
2228 function replacer(match, replaceOffset) {
2229 while (highlightIndex < highlightCount && result.entityRanges[highlightIndex].offset < replaceOffset) {
2230 result.entityRanges[highlightIndex].offset += additionalHighlightOffset;
2233 additionalHighlightOffset += 1;
2234 return match + "\u200B";
2238 * @param {!Element} element
2239 * @param {string} value
2240 * @this {WebInspector.ElementsTreeElement}
2242 function setValueWithEntities(element, value)
2244 result = this._convertWhitespaceToEntities(value);
2245 highlightCount = result.entityRanges.length;
2246 value = result.text.replace(closingPunctuationRegex, replacer);
2247 while (highlightIndex < highlightCount) {
2248 result.entityRanges[highlightIndex].offset += additionalHighlightOffset;
2251 element.textContent = value;
2252 WebInspector.highlightRangesWithStyleClass(element, result.entityRanges, "webkit-html-entity-value");
2255 var hasText = (forceValue || value.length > 0);
2256 var attrSpanElement = parentElement.createChild("span", "webkit-html-attribute");
2257 var attrNameElement = attrSpanElement.createChild("span", "webkit-html-attribute-name");
2258 attrNameElement.textContent = name;
2261 attrSpanElement.createTextChild("=\u200B\"");
2263 var attrValueElement = attrSpanElement.createChild("span", "webkit-html-attribute-value");
2266 * @this {WebInspector.ElementsTreeElement}
2267 * @param {string} value
2268 * @return {!Element}
2270 function linkifyValue(value)
2272 var rewrittenHref = node.resolveURL(value);
2273 if (rewrittenHref === null) {
2274 var span = document.createElement("span");
2275 setValueWithEntities.call(this, span, value);
2278 value = value.replace(closingPunctuationRegex, "$&\u200B");
2279 if (value.startsWith("data:"))
2280 value = value.trimMiddle(60);
2281 return linkify(rewrittenHref, value, "", node.nodeName().toLowerCase() === "a");
2284 if (linkify && (name === "src" || name === "href")) {
2285 attrValueElement.appendChild(linkifyValue.call(this, value));
2286 } else if (linkify && node.nodeName().toLowerCase() === "img" && name === "srcset") {
2287 var sources = value.split(",");
2288 for (var i = 0; i < sources.length; ++i) {
2290 attrValueElement.createTextChild(", ");
2291 var source = sources[i].trim();
2292 var indexOfSpace = source.indexOf(" ");
2293 var url = source.substring(0, indexOfSpace);
2294 var tail = source.substring(indexOfSpace);
2295 attrValueElement.appendChild(linkifyValue.call(this, url));
2296 attrValueElement.createTextChild(tail);
2299 setValueWithEntities.call(this, attrValueElement, value);
2303 attrSpanElement.createTextChild("\"");
2307 * @param {!Node} parentElement
2308 * @param {string} pseudoElementName
2310 _buildPseudoElementDOM: function(parentElement, pseudoElementName)
2312 var pseudoElement = parentElement.createChild("span", "webkit-html-pseudo-element");
2313 pseudoElement.textContent = "::" + pseudoElementName;
2314 parentElement.createTextChild("\u200B");
2318 * @param {!Node} parentElement
2319 * @param {string} tagName
2320 * @param {boolean} isClosingTag
2321 * @param {boolean} isDistinctTreeElement
2322 * @param {function(string, string, string, boolean=, string=)=} linkify
2324 _buildTagDOM: function(parentElement, tagName, isClosingTag, isDistinctTreeElement, linkify)
2326 var node = this._node;
2327 var classes = [ "webkit-html-tag" ];
2328 if (isClosingTag && isDistinctTreeElement)
2329 classes.push("close");
2330 var tagElement = parentElement.createChild("span", classes.join(" "));
2331 tagElement.createTextChild("<");
2332 var tagNameElement = tagElement.createChild("span", isClosingTag ? "" : "webkit-html-tag-name");
2333 tagNameElement.textContent = (isClosingTag ? "/" : "") + tagName;
2334 if (!isClosingTag && node.hasAttributes()) {
2335 var attributes = node.attributes();
2336 for (var i = 0; i < attributes.length; ++i) {
2337 var attr = attributes[i];
2338 tagElement.createTextChild(" ");
2339 this._buildAttributeDOM(tagElement, attr.name, attr.value, false, node, linkify);
2342 tagElement.createTextChild(">");
2343 parentElement.createTextChild("\u200B");
2347 * @param {string} text
2348 * @return {!{text: string, entityRanges: !Array.<!WebInspector.SourceRange>}}
2350 _convertWhitespaceToEntities: function(text)
2353 var resultLength = 0;
2354 var lastIndexAfterEntity = 0;
2355 var entityRanges = [];
2356 var charToEntity = WebInspector.ElementsTreeOutline.MappedCharToEntity;
2357 for (var i = 0, size = text.length; i < size; ++i) {
2358 var char = text.charAt(i);
2359 if (charToEntity[char]) {
2360 result += text.substring(lastIndexAfterEntity, i);
2361 var entityValue = "&" + charToEntity[char] + ";";
2362 entityRanges.push({offset: result.length, length: entityValue.length});
2363 result += entityValue;
2364 lastIndexAfterEntity = i + 1;
2368 result += text.substring(lastIndexAfterEntity);
2369 return {text: result || text, entityRanges: entityRanges};
2373 * @param {function(string, string, string, boolean=, string=)=} linkify
2375 _nodeTitleInfo: function(linkify)
2377 var node = this._node;
2378 var info = {titleDOM: document.createDocumentFragment(), hasChildren: this.hasChildren};
2380 switch (node.nodeType()) {
2381 case Node.ATTRIBUTE_NODE:
2382 this._buildAttributeDOM(info.titleDOM, /** @type {string} */ (node.name), /** @type {string} */ (node.value), true);
2385 case Node.ELEMENT_NODE:
2386 var pseudoType = node.pseudoType();
2388 this._buildPseudoElementDOM(info.titleDOM, pseudoType);
2389 info.hasChildren = false;
2393 var tagName = node.nodeNameInCorrectCase();
2394 if (this._elementCloseTag) {
2395 this._buildTagDOM(info.titleDOM, tagName, true, true);
2396 info.hasChildren = false;
2400 this._buildTagDOM(info.titleDOM, tagName, false, false, linkify);
2402 var showInlineText = this._showInlineText() && !this.hasChildren;
2403 if (!this.expanded && !showInlineText && (this.treeOutline.isXMLMimeType || !WebInspector.ElementsTreeElement.ForbiddenClosingTagElements[tagName])) {
2404 if (this.hasChildren) {
2405 var textNodeElement = info.titleDOM.createChild("span", "webkit-html-text-node bogus");
2406 textNodeElement.textContent = "\u2026";
2407 info.titleDOM.createTextChild("\u200B");
2409 this._buildTagDOM(info.titleDOM, tagName, true, false);
2412 // If this element only has a single child that is a text node,
2413 // just show that text and the closing tag inline rather than
2414 // create a subtree for them
2415 if (showInlineText) {
2416 console.assert(!this.hasChildren);
2417 var textNodeElement = info.titleDOM.createChild("span", "webkit-html-text-node");
2418 var result = this._convertWhitespaceToEntities(node.firstChild.nodeValue());
2419 textNodeElement.textContent = result.text;
2420 WebInspector.highlightRangesWithStyleClass(textNodeElement, result.entityRanges, "webkit-html-entity-value");
2421 info.titleDOM.createTextChild("\u200B");
2422 this._buildTagDOM(info.titleDOM, tagName, true, false);
2423 info.hasChildren = false;
2427 case Node.TEXT_NODE:
2428 if (node.parentNode && node.parentNode.nodeName().toLowerCase() === "script") {
2429 var newNode = info.titleDOM.createChild("span", "webkit-html-text-node webkit-html-js-node");
2430 newNode.textContent = node.nodeValue();
2432 var javascriptSyntaxHighlighter = new WebInspector.DOMSyntaxHighlighter("text/javascript", true);
2433 javascriptSyntaxHighlighter.syntaxHighlightNode(newNode);
2434 } else if (node.parentNode && node.parentNode.nodeName().toLowerCase() === "style") {
2435 var newNode = info.titleDOM.createChild("span", "webkit-html-text-node webkit-html-css-node");
2436 newNode.textContent = node.nodeValue();
2438 var cssSyntaxHighlighter = new WebInspector.DOMSyntaxHighlighter("text/css", true);
2439 cssSyntaxHighlighter.syntaxHighlightNode(newNode);
2441 info.titleDOM.createTextChild("\"");
2442 var textNodeElement = info.titleDOM.createChild("span", "webkit-html-text-node");
2443 var result = this._convertWhitespaceToEntities(node.nodeValue());
2444 textNodeElement.textContent = result.text;
2445 WebInspector.highlightRangesWithStyleClass(textNodeElement, result.entityRanges, "webkit-html-entity-value");
2446 info.titleDOM.createTextChild("\"");
2450 case Node.COMMENT_NODE:
2451 var commentElement = info.titleDOM.createChild("span", "webkit-html-comment");
2452 commentElement.createTextChild("<!--" + node.nodeValue() + "-->");
2455 case Node.DOCUMENT_TYPE_NODE:
2456 var docTypeElement = info.titleDOM.createChild("span", "webkit-html-doctype");
2457 docTypeElement.createTextChild("<!DOCTYPE " + node.nodeName());
2458 if (node.publicId) {
2459 docTypeElement.createTextChild(" PUBLIC \"" + node.publicId + "\"");
2461 docTypeElement.createTextChild(" \"" + node.systemId + "\"");
2462 } else if (node.systemId)
2463 docTypeElement.createTextChild(" SYSTEM \"" + node.systemId + "\"");
2465 if (node.internalSubset)
2466 docTypeElement.createTextChild(" [" + node.internalSubset + "]");
2468 docTypeElement.createTextChild(">");
2471 case Node.CDATA_SECTION_NODE:
2472 var cdataElement = info.titleDOM.createChild("span", "webkit-html-text-node");
2473 cdataElement.createTextChild("<![CDATA[" + node.nodeValue() + "]]>");
2475 case Node.DOCUMENT_FRAGMENT_NODE:
2476 var fragmentElement = info.titleDOM.createChild("span", "webkit-html-fragment");
2477 if (node.isInShadowTree()) {
2478 var shadowRootType = node.shadowRootType();
2479 if (shadowRootType) {
2480 info.shadowRoot = true;
2481 fragmentElement.classList.add("shadow-root");
2484 fragmentElement.textContent = node.nodeNameInCorrectCase().collapseWhitespace();
2487 info.titleDOM.createTextChild(node.nodeNameInCorrectCase().collapseWhitespace());
2495 _showInlineText: function()
2497 if (this._node.importedDocument() || this._node.templateContent() || this._visibleShadowRoots().length > 0 || this._node.hasPseudoElements())
2499 if (this._node.nodeType() !== Node.ELEMENT_NODE)
2501 if (!this._node.firstChild || this._node.firstChild !== this._node.lastChild || this._node.firstChild.nodeType() !== Node.TEXT_NODE)
2503 var textChild = this._node.firstChild;
2504 var maxInlineTextChildLength = 80;
2505 if (textChild.nodeValue().length < maxInlineTextChildLength)
2512 if (this._node.pseudoType())
2514 var parentElement = this.parent;
2519 function removeNodeCallback(error)
2524 parentElement.removeChild(self);
2525 parentElement._adjustCollapsedRange();
2528 if (!this._node.parentNode || this._node.parentNode.nodeType() === Node.DOCUMENT_NODE)
2530 this._node.removeNode(removeNodeCallback);
2533 _editAsHTML: function()
2535 var node = this._node;
2536 if (node.pseudoType())
2539 var treeOutline = this.treeOutline;
2540 var parentNode = node.parentNode;
2541 var index = node.index;
2542 var wasExpanded = this.expanded;
2545 * @param {?Protocol.Error} error
2547 function selectNode(error)
2552 // Select it and expand if necessary. We force tree update so that it processes dom events and is up to date.
2553 treeOutline._updateModifiedNodes();
2555 var newNode = parentNode ? parentNode.children()[index] || parentNode : null;
2559 treeOutline.selectDOMNode(newNode, true);
2562 var newTreeItem = treeOutline.findTreeElement(newNode);
2564 newTreeItem.expand();
2569 * @param {string} initialValue
2570 * @param {string} value
2572 function commitChange(initialValue, value)
2574 if (initialValue !== value)
2575 node.setOuterHTML(value, selectNode);
2578 node.getOuterHTML(this._startEditingAsHTML.bind(this, commitChange));
2581 _copyCSSPath: function()
2583 InspectorFrontendHost.copyText(WebInspector.DOMPresentationUtils.cssPath(this._node, true));
2586 _copyXPath: function()
2588 InspectorFrontendHost.copyText(WebInspector.DOMPresentationUtils.xPath(this._node, true));
2591 _highlightSearchResults: function()
2593 if (!this._searchQuery || !this._searchHighlightsVisible)
2595 if (this._highlightResult) {
2596 this._updateSearchHighlight(true);
2600 var text = this.listItemElement.textContent;
2601 var regexObject = createPlainTextSearchRegex(this._searchQuery, "gi");
2604 var match = regexObject.exec(text);
2605 var matchRanges = [];
2607 matchRanges.push(new WebInspector.SourceRange(match.index, match[0].length));
2608 match = regexObject.exec(text);
2611 // Fall back for XPath, etc. matches.
2612 if (!matchRanges.length)
2613 matchRanges.push(new WebInspector.SourceRange(0, text.length));
2615 this._highlightResult = [];
2616 WebInspector.highlightSearchResults(this.listItemElement, matchRanges, this._highlightResult);
2619 _scrollIntoView: function()
2621 function scrollIntoViewCallback(object)
2624 * @suppressReceiverCheck
2627 function scrollIntoView()
2629 this.scrollIntoViewIfNeeded(true);
2633 object.callFunction(scrollIntoView);
2636 this._node.resolveToObject("", scrollIntoViewCallback);
2640 * @return {!Array.<!WebInspector.DOMNode>}
2642 _visibleShadowRoots: function()
2644 var roots = this._node.shadowRoots();
2645 if (roots.length && !WebInspector.settings.showUAShadowDOM.get()) {
2646 roots = roots.filter(function(root) {
2647 return root.shadowRootType() === WebInspector.DOMNode.ShadowRootTypes.Author;
2654 * @return {!Array.<!WebInspector.DOMNode>} visibleChildren
2656 _visibleChildren: function()
2658 var visibleChildren = this._visibleShadowRoots();
2659 if (this._node.importedDocument())
2660 visibleChildren.push(this._node.importedDocument());
2661 if (this._node.templateContent())
2662 visibleChildren.push(this._node.templateContent());
2663 var pseudoElements = this._node.pseudoElements();
2664 if (pseudoElements[WebInspector.DOMNode.PseudoElementNames.Before])
2665 visibleChildren.push(pseudoElements[WebInspector.DOMNode.PseudoElementNames.Before]);
2666 if (this._node.childNodeCount())
2667 visibleChildren = visibleChildren.concat(this._node.children());
2668 if (pseudoElements[WebInspector.DOMNode.PseudoElementNames.After])
2669 visibleChildren.push(pseudoElements[WebInspector.DOMNode.PseudoElementNames.After]);
2670 return visibleChildren;
2676 _visibleChildCount: function()
2678 var childCount = this._node.childNodeCount() + this._visibleShadowRoots().length;
2679 if (this._node.importedDocument())
2681 if (this._node.templateContent())
2683 for (var pseudoType in this._node.pseudoElements())
2688 _updateHasChildren: function()
2690 this.hasChildren = !this._elementCloseTag && !this._showInlineText() && this._visibleChildCount() > 0;
2693 __proto__: TreeElement.prototype
2698 * @param {!WebInspector.DOMModel} domModel
2699 * @param {!WebInspector.ElementsTreeOutline} treeOutline
2701 WebInspector.ElementsTreeUpdater = function(domModel, treeOutline)
2703 domModel.addEventListener(WebInspector.DOMModel.Events.NodeInserted, this._nodeInserted, this);
2704 domModel.addEventListener(WebInspector.DOMModel.Events.NodeRemoved, this._nodeRemoved, this);
2705 domModel.addEventListener(WebInspector.DOMModel.Events.AttrModified, this._attributesUpdated, this);
2706 domModel.addEventListener(WebInspector.DOMModel.Events.AttrRemoved, this._attributesUpdated, this);
2707 domModel.addEventListener(WebInspector.DOMModel.Events.CharacterDataModified, this._characterDataModified, this);
2708 domModel.addEventListener(WebInspector.DOMModel.Events.DocumentUpdated, this._documentUpdated, this);
2709 domModel.addEventListener(WebInspector.DOMModel.Events.ChildNodeCountUpdated, this._childNodeCountUpdated, this);
2711 this._domModel = domModel;
2712 this._treeOutline = treeOutline;
2713 /** @type {!Set.<!WebInspector.DOMNode>} */
2714 this._recentlyModifiedNodes = new Set();
2715 /** @type {!Set.<!WebInspector.DOMNode>} */
2716 this._recentlyModifiedParentNodes = new Set();
2719 WebInspector.ElementsTreeUpdater.prototype = {
2722 this._domModel.removeEventListener(WebInspector.DOMModel.Events.NodeInserted, this._nodeInserted, this);
2723 this._domModel.removeEventListener(WebInspector.DOMModel.Events.NodeRemoved, this._nodeRemoved, this);
2724 this._domModel.removeEventListener(WebInspector.DOMModel.Events.AttrModified, this._attributesUpdated, this);
2725 this._domModel.removeEventListener(WebInspector.DOMModel.Events.AttrRemoved, this._attributesUpdated, this);
2726 this._domModel.removeEventListener(WebInspector.DOMModel.Events.CharacterDataModified, this._characterDataModified, this);
2727 this._domModel.removeEventListener(WebInspector.DOMModel.Events.DocumentUpdated, this._documentUpdated, this);
2728 this._domModel.removeEventListener(WebInspector.DOMModel.Events.ChildNodeCountUpdated, this._childNodeCountUpdated, this);
2732 * @param {?WebInspector.DOMNode} parentNode
2734 _parentNodeModified: function(parentNode)
2738 this._recentlyModifiedParentNodes.add(parentNode);
2740 var treeElement = this._treeOutline.findTreeElement(parentNode);
2742 var oldHasChildren = treeElement.hasChildren;
2743 var oldShowInlineText = treeElement._showInlineText();
2744 treeElement._updateHasChildren();
2745 if (treeElement.hasChildren !== oldHasChildren || oldShowInlineText || treeElement._showInlineText())
2746 this._nodeModified(parentNode);
2749 if (this._treeOutline._visible)
2750 this._updateModifiedNodesSoon();
2754 * @param {!WebInspector.DOMNode} node
2756 _nodeModified: function(node)
2758 this._recentlyModifiedNodes.add(node);
2759 if (this._treeOutline._visible)
2760 this._updateModifiedNodesSoon();
2764 * @param {!WebInspector.Event} event
2766 _documentUpdated: function(event)
2768 var inspectedRootDocument = event.data;
2772 if (!inspectedRootDocument)
2775 this._treeOutline.rootDOMNode = inspectedRootDocument;
2779 * @param {!WebInspector.Event} event
2781 _attributesUpdated: function(event)
2783 var node = /** @type {!WebInspector.DOMNode} */ (event.data.node);
2784 this._nodeModified(node);
2788 * @param {!WebInspector.Event} event
2790 _characterDataModified: function(event)
2792 var node = /** @type {!WebInspector.DOMNode} */ (event.data);
2793 this._parentNodeModified(node.parentNode);
2794 this._nodeModified(node);
2798 * @param {!WebInspector.Event} event
2800 _nodeInserted: function(event)
2802 var node = /** @type {!WebInspector.DOMNode} */ (event.data);
2803 this._parentNodeModified(node.parentNode);
2807 * @param {!WebInspector.Event} event
2809 _nodeRemoved: function(event)
2811 var node = /** @type {!WebInspector.DOMNode} */ (event.data.node);
2812 var parentNode = /** @type {!WebInspector.DOMNode} */ (event.data.parent);
2813 this._treeOutline._resetClipboardIfNeeded(node);
2814 this._parentNodeModified(parentNode);
2818 * @param {!WebInspector.Event} event
2820 _childNodeCountUpdated: function(event)
2822 var node = /** @type {!WebInspector.DOMNode} */ (event.data);
2823 this._parentNodeModified(node);
2826 _updateModifiedNodesSoon: function()
2828 if (this._updateModifiedNodesTimeout)
2830 this._updateModifiedNodesTimeout = setTimeout(this._updateModifiedNodes.bind(this), 50);
2833 _updateModifiedNodes: function()
2835 if (this._updateModifiedNodesTimeout) {
2836 clearTimeout(this._updateModifiedNodesTimeout);
2837 delete this._updateModifiedNodesTimeout;
2840 var updatedNodes = this._recentlyModifiedNodes.values().concat(this._recentlyModifiedParentNodes.values());
2841 var hidePanelWhileUpdating = updatedNodes.length > 10;
2842 if (hidePanelWhileUpdating) {
2843 var treeOutlineContainerElement = this._treeOutline.element.parentNode;
2844 var originalScrollTop = treeOutlineContainerElement ? treeOutlineContainerElement.scrollTop : 0;
2845 this._treeOutline.element.classList.add("hidden");
2848 if (this._treeOutline._rootDOMNode && this._recentlyModifiedParentNodes.contains(this._treeOutline._rootDOMNode)) {
2849 // Document's children have changed, perform total update.
2850 this._treeOutline.update();
2852 var nodes = this._recentlyModifiedNodes.values();
2853 for (var i = 0, size = nodes.length; i < size; ++i) {
2854 var nodeItem = this._treeOutline.findTreeElement(nodes[i]);
2856 nodeItem.updateTitle();
2859 var parentNodes = this._recentlyModifiedParentNodes.values();
2860 for (var i = 0, size = parentNodes.length; i < size; ++i) {
2861 var parentNodeItem = this._treeOutline.findTreeElement(parentNodes[i]);
2862 if (parentNodeItem && parentNodeItem.populated)
2863 parentNodeItem.updateChildren();
2867 if (hidePanelWhileUpdating) {
2868 this._treeOutline.element.classList.remove("hidden");
2869 if (originalScrollTop)
2870 treeOutlineContainerElement.scrollTop = originalScrollTop;
2871 this._treeOutline.updateSelection();
2873 this._recentlyModifiedNodes.clear();
2874 this._recentlyModifiedParentNodes.clear();
2875 this._treeOutline._fireElementsTreeUpdated(updatedNodes);
2880 this._treeOutline.rootDOMNode = null;
2881 this._treeOutline.selectDOMNode(null, false);
2882 this._domModel.hideDOMNodeHighlight();
2883 this._recentlyModifiedNodes.clear();
2884 this._recentlyModifiedParentNodes.clear();
2885 delete this._treeOutline._clipboardNodeData;
2891 * @implements {WebInspector.Renderer}
2893 WebInspector.ElementsTreeOutline.Renderer = function()
2897 WebInspector.ElementsTreeOutline.Renderer.prototype = {
2899 * @param {!Object} object
2900 * @return {?Element}
2902 render: function(object)
2904 if (!(object instanceof WebInspector.DOMNode))
2906 var node = /** @type {!WebInspector.DOMNode} */ (object);
2907 var treeOutline = new WebInspector.ElementsTreeOutline(node.target(), false, false);
2908 treeOutline.rootDOMNode = node;
2909 treeOutline.element.classList.add("outline-disclosure");
2910 if (!treeOutline.children[0].hasChildren)
2911 treeOutline.element.classList.add("single-node");
2912 treeOutline.setVisible(true);
2913 treeOutline.element.treeElementForTest = treeOutline.children[0];
2914 return treeOutline.element;