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.ContextMenu, !WebInspector.DOMNode)=} contextMenuCallback
38 * @param {function(!WebInspector.DOMNode, string, boolean)=} setPseudoClassCallback
40 WebInspector.ElementsTreeOutline = function(target, omitRootDOMNode, selectEnabled, contextMenuCallback, setPseudoClassCallback)
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);
56 TreeOutline.call(this, this.element);
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();
66 this._visible = false;
68 this.element.addEventListener("contextmenu", this._contextMenuEventFired.bind(this), true);
69 this._contextMenuCallback = contextMenuCallback;
70 this._setPseudoClassCallback = setPseudoClassCallback;
71 this._createNodeDecorators();
74 /** @typedef {{node: !WebInspector.DOMNode, isCut: boolean}} */
75 WebInspector.ElementsTreeOutline.ClipboardData;
80 WebInspector.ElementsTreeOutline.Events = {
81 SelectedNodeChanged: "SelectedNodeChanged",
82 ElementsTreeUpdated: "ElementsTreeUpdated"
87 * @type {!Object.<string, string>}
89 WebInspector.ElementsTreeOutline.MappedCharToEntity = {
94 "\u200a": "#8202", // Hairspace
95 "\u200b": "#8203", // ZWSP
100 "\u202a": "#8234", // LRE
101 "\u202b": "#8235", // RLE
102 "\u202c": "#8236", // PDF
103 "\u202d": "#8237", // LRO
104 "\u202e": "#8238" // RLO
107 WebInspector.ElementsTreeOutline.prototype = {
109 * @return {!WebInspector.Target}
117 * @return {!WebInspector.DOMModel}
121 return this._domModel;
125 * @param {number} width
127 setVisibleWidth: function(width)
129 this._visibleWidth = width;
130 if (this._multilineEditing)
131 this._multilineEditing.setWidth(this._visibleWidth);
134 _createNodeDecorators: function()
136 this._nodeDecorators = [];
137 this._nodeDecorators.push(new WebInspector.ElementsTreeOutline.PseudoStateDecorator());
140 wireToDOMModel: function()
142 this._elementsTreeUpdater = new WebInspector.ElementsTreeUpdater(this._target.domModel, this);
145 unwireFromDOMModel: function()
147 if (this._elementsTreeUpdater)
148 this._elementsTreeUpdater.dispose();
152 * @param {?WebInspector.ElementsTreeOutline.ClipboardData} data
154 _setClipboardData: function(data)
156 if (this._clipboardNodeData) {
157 var treeElement = this.findTreeElement(this._clipboardNodeData.node);
159 treeElement.setInClipboard(false);
160 delete this._clipboardNodeData;
164 var treeElement = this.findTreeElement(data.node);
166 treeElement.setInClipboard(true);
167 this._clipboardNodeData = data;
172 * @param {!WebInspector.DOMNode} removedNode
174 _resetClipboardIfNeeded: function(removedNode)
176 if (this._clipboardNodeData && this._clipboardNodeData.node === removedNode)
177 this._setClipboardData(null);
181 * @param {boolean} isCut
182 * @param {!Event} event
184 handleCopyOrCutKeyboardEvent: function(isCut, event)
186 this._setClipboardData(null);
188 // Don't prevent the normal copy if the user has a selection.
189 if (!window.getSelection().isCollapsed)
192 // Do not interfere with text editing.
193 var currentFocusElement = WebInspector.currentFocusElement();
194 if (currentFocusElement && WebInspector.isBeingEdited(currentFocusElement))
197 var targetNode = this.selectedDOMNode();
201 event.clipboardData.clearData();
202 event.preventDefault();
204 this._performCopyOrCut(isCut, targetNode);
208 * @param {boolean} isCut
209 * @param {?WebInspector.DOMNode} node
211 _performCopyOrCut: function(isCut, node)
213 if (isCut && (node.isShadowRoot() || node.ancestorUserAgentShadowRoot()))
217 this._setClipboardData({ node: node, isCut: isCut });
221 * @param {!WebInspector.DOMNode} targetNode
224 _canPaste: function(targetNode)
226 if (targetNode.isShadowRoot() || targetNode.ancestorUserAgentShadowRoot())
229 if (!this._clipboardNodeData)
232 var node = this._clipboardNodeData.node;
233 if (this._clipboardNodeData.isCut && (node === targetNode || node.isAncestor(targetNode)))
236 if (targetNode.target() !== node.target())
242 * @param {!WebInspector.DOMNode} targetNode
244 _pasteNode: function(targetNode)
246 if (this._canPaste(targetNode))
247 this._performPaste(targetNode);
251 * @param {!Event} event
253 handlePasteKeyboardEvent: function(event)
255 // Do not interfere with text editing.
256 var currentFocusElement = WebInspector.currentFocusElement();
257 if (currentFocusElement && WebInspector.isBeingEdited(currentFocusElement))
260 var targetNode = this.selectedDOMNode();
261 if (!targetNode || !this._canPaste(targetNode))
264 event.preventDefault();
265 this._performPaste(targetNode);
269 * @param {!WebInspector.DOMNode} targetNode
271 _performPaste: function(targetNode)
273 if (this._clipboardNodeData.isCut) {
274 this._clipboardNodeData.node.moveTo(targetNode, null, expandCallback.bind(this));
275 this._setClipboardData(null);
277 this._clipboardNodeData.node.copyTo(targetNode, null, expandCallback.bind(this));
281 * @param {?Protocol.Error} error
282 * @param {!DOMAgent.NodeId} nodeId
283 * @this {WebInspector.ElementsTreeOutline}
285 function expandCallback(error, nodeId)
289 var pastedNode = this._domModel.nodeForId(nodeId);
292 this.selectDOMNode(pastedNode);
297 * @param {boolean} visible
299 setVisible: function(visible)
301 this._visible = visible;
305 this._updateModifiedNodes();
306 if (this._selectedDOMNode)
307 this._revealAndSelectNode(this._selectedDOMNode, false);
310 addEventListener: function(eventType, listener, thisObject)
312 this._eventSupport.addEventListener(eventType, listener, thisObject);
315 removeEventListener: function(eventType, listener, thisObject)
317 this._eventSupport.removeEventListener(eventType, listener, thisObject);
322 return this._rootDOMNode;
327 if (this._rootDOMNode === x)
330 this._rootDOMNode = x;
332 this._isXMLMimeType = x && x.isXMLNode();
339 return this._isXMLMimeType;
343 * @return {?WebInspector.DOMNode}
345 selectedDOMNode: function()
347 return this._selectedDOMNode;
351 * @param {?WebInspector.DOMNode} node
352 * @param {boolean=} focus
354 selectDOMNode: function(node, focus)
356 if (this._selectedDOMNode === node) {
357 this._revealAndSelectNode(node, !focus);
361 this._selectedDOMNode = node;
362 this._revealAndSelectNode(node, !focus);
364 // The _revealAndSelectNode() method might find a different element if there is inlined text,
365 // and the select() call would change the selectedDOMNode and reenter this setter. So to
366 // avoid calling _selectedNodeChanged() twice, first check if _selectedDOMNode is the same
367 // node as the one passed in.
368 if (this._selectedDOMNode === node)
369 this._selectedNodeChanged();
377 var node = this.selectedDOMNode();
380 var treeElement = this.findTreeElement(node);
383 return treeElement._editing || false;
388 var selectedNode = this.selectedTreeElement ? this.selectedTreeElement._node : null;
390 this.removeChildren();
392 if (!this.rootDOMNode)
396 if (this._includeRootDOMNode) {
397 treeElement = new WebInspector.ElementsTreeElement(this.rootDOMNode);
398 treeElement.selectable = this._selectEnabled;
399 this.appendChild(treeElement);
401 // FIXME: this could use findTreeElement to reuse a tree element if it already exists
402 var node = this.rootDOMNode.firstChild;
404 treeElement = new WebInspector.ElementsTreeElement(node);
405 treeElement.selectable = this._selectEnabled;
406 this.appendChild(treeElement);
407 node = node.nextSibling;
412 this._revealAndSelectNode(selectedNode, true);
415 updateSelection: function()
417 if (!this.selectedTreeElement)
419 var element = this.treeOutline.selectedTreeElement;
420 element.updateSelection();
424 * @param {!WebInspector.DOMNode} node
426 updateOpenCloseTags: function(node)
428 var treeElement = this.findTreeElement(node);
430 treeElement.updateTitle();
431 var children = treeElement.children;
432 var closingTagElement = children[children.length - 1];
433 if (closingTagElement && closingTagElement._elementCloseTag)
434 closingTagElement.updateTitle();
437 _selectedNodeChanged: function()
439 this._eventSupport.dispatchEventToListeners(WebInspector.ElementsTreeOutline.Events.SelectedNodeChanged, this._selectedDOMNode);
443 * @param {!Array.<!WebInspector.DOMNode>} nodes
445 _fireElementsTreeUpdated: function(nodes)
447 this._eventSupport.dispatchEventToListeners(WebInspector.ElementsTreeOutline.Events.ElementsTreeUpdated, nodes);
451 * @param {!WebInspector.DOMNode} node
452 * @return {?TreeElement}
454 findTreeElement: function(node)
456 function parentNode(node)
458 return node.parentNode;
461 var treeElement = TreeOutline.prototype.findTreeElement.call(this, node, parentNode);
462 if (!treeElement && node.nodeType() === Node.TEXT_NODE) {
463 // The text node might have been inlined if it was short, so try to find the parent element.
464 treeElement = TreeOutline.prototype.findTreeElement.call(this, node.parentNode, parentNode);
471 * @param {!WebInspector.DOMNode} node
472 * @return {?TreeElement}
474 createTreeElementFor: function(node)
476 var treeElement = this.findTreeElement(node);
479 if (!node.parentNode)
482 treeElement = this.createTreeElementFor(node.parentNode);
483 return treeElement ? treeElement._showChild(node) : null;
486 set suppressRevealAndSelect(x)
488 if (this._suppressRevealAndSelect === x)
490 this._suppressRevealAndSelect = x;
494 * @param {?WebInspector.DOMNode} node
495 * @param {boolean} omitFocus
497 _revealAndSelectNode: function(node, omitFocus)
499 if (this._suppressRevealAndSelect)
502 if (!this._includeRootDOMNode && node === this.rootDOMNode && this.rootDOMNode)
503 node = this.rootDOMNode.firstChild;
506 var treeElement = this.createTreeElementFor(node);
510 treeElement.revealAndSelect(omitFocus);
514 * @return {?TreeElement}
516 _treeElementFromEvent: function(event)
518 var scrollContainer = this.element.parentElement;
520 // We choose this X coordinate based on the knowledge that our list
521 // items extend at least to the right edge of the outer <ol> container.
522 // In the no-word-wrap mode the outer <ol> may be wider than the tree container
523 // (and partially hidden), in which case we are left to use only its right boundary.
524 var x = scrollContainer.totalOffsetLeft() + scrollContainer.offsetWidth - 36;
528 // Our list items have 1-pixel cracks between them vertically. We avoid
529 // the cracks by checking slightly above and slightly below the mouse
530 // and seeing if we hit the same element each time.
531 var elementUnderMouse = this.treeElementFromPoint(x, y);
532 var elementAboveMouse = this.treeElementFromPoint(x, y - 2);
534 if (elementUnderMouse === elementAboveMouse)
535 element = elementUnderMouse;
537 element = this.treeElementFromPoint(x, y + 2);
542 _onmousedown: function(event)
544 var element = this._treeElementFromEvent(event);
546 if (!element || element.isEventWithinDisclosureTriangle(event))
552 _onmousemove: function(event)
554 var element = this._treeElementFromEvent(event);
555 if (element && this._previousHoveredElement === element)
558 if (this._previousHoveredElement) {
559 this._previousHoveredElement.hovered = false;
560 delete this._previousHoveredElement;
564 element.hovered = true;
565 this._previousHoveredElement = element;
568 if (element && element._node)
569 this._domModel.highlightDOMNodeWithConfig(element._node.id, { mode: "all", showInfo: !WebInspector.KeyboardShortcut.eventHasCtrlOrMeta(event) });
571 this._domModel.hideDOMNodeHighlight();
574 _onmouseout: function(event)
576 var nodeUnderMouse = document.elementFromPoint(event.pageX, event.pageY);
577 if (nodeUnderMouse && nodeUnderMouse.isDescendant(this.element))
580 if (this._previousHoveredElement) {
581 this._previousHoveredElement.hovered = false;
582 delete this._previousHoveredElement;
585 this._domModel.hideDOMNodeHighlight();
588 _ondragstart: function(event)
590 if (!window.getSelection().isCollapsed)
592 if (event.target.nodeName === "A")
595 var treeElement = this._treeElementFromEvent(event);
599 if (!this._isValidDragSourceOrTarget(treeElement))
602 if (treeElement._node.nodeName() === "BODY" || treeElement._node.nodeName() === "HEAD")
605 event.dataTransfer.setData("text/plain", treeElement.listItemElement.textContent);
606 event.dataTransfer.effectAllowed = "copyMove";
607 this._treeElementBeingDragged = treeElement;
609 this._domModel.hideDOMNodeHighlight();
614 _ondragover: function(event)
616 if (!this._treeElementBeingDragged)
619 var treeElement = this._treeElementFromEvent(event);
620 if (!this._isValidDragSourceOrTarget(treeElement))
623 var node = treeElement._node;
625 if (node === this._treeElementBeingDragged._node)
627 node = node.parentNode;
630 treeElement.updateSelection();
631 treeElement.listItemElement.classList.add("elements-drag-over");
632 this._dragOverTreeElement = treeElement;
633 event.preventDefault();
634 event.dataTransfer.dropEffect = 'move';
638 _ondragleave: function(event)
640 this._clearDragOverTreeElementMarker();
641 event.preventDefault();
646 * @param {?TreeElement} treeElement
649 _isValidDragSourceOrTarget: function(treeElement)
654 var node = treeElement.representedObject;
655 if (!(node instanceof WebInspector.DOMNode))
658 if (!node.parentNode || node.parentNode.nodeType() !== Node.ELEMENT_NODE)
664 _ondrop: function(event)
666 event.preventDefault();
667 var treeElement = this._treeElementFromEvent(event);
669 this._doMove(treeElement);
673 * @param {!TreeElement} treeElement
675 _doMove: function(treeElement)
677 if (!this._treeElementBeingDragged)
683 if (treeElement._elementCloseTag) {
684 // Drop onto closing tag -> insert as last child.
685 parentNode = treeElement._node;
687 var dragTargetNode = treeElement._node;
688 parentNode = dragTargetNode.parentNode;
689 anchorNode = dragTargetNode;
692 var wasExpanded = this._treeElementBeingDragged.expanded;
693 this._treeElementBeingDragged._node.moveTo(parentNode, anchorNode, this._selectNodeAfterEdit.bind(this, wasExpanded));
695 delete this._treeElementBeingDragged;
698 _ondragend: function(event)
700 event.preventDefault();
701 this._clearDragOverTreeElementMarker();
702 delete this._treeElementBeingDragged;
705 _clearDragOverTreeElementMarker: function()
707 if (this._dragOverTreeElement) {
708 this._dragOverTreeElement.updateSelection();
709 this._dragOverTreeElement.listItemElement.classList.remove("elements-drag-over");
710 delete this._dragOverTreeElement;
715 * @param {!Event} event
717 _onkeydown: function(event)
719 var keyboardEvent = /** @type {!KeyboardEvent} */ (event);
720 var node = /** @type {!WebInspector.DOMNode} */ (this.selectedDOMNode());
721 console.assert(node);
722 var treeElement = this.getCachedTreeElement(node);
726 if (!treeElement._editing && WebInspector.KeyboardShortcut.hasNoModifiers(keyboardEvent) && keyboardEvent.keyCode === WebInspector.KeyboardShortcut.Keys.H.code) {
727 this._toggleHideShortcut(node);
733 _contextMenuEventFired: function(event)
735 var treeElement = this._treeElementFromEvent(event);
739 var contextMenu = new WebInspector.ContextMenu(event);
740 contextMenu.appendApplicableItems(treeElement._node);
744 populateContextMenu: function(contextMenu, event)
746 var treeElement = this._treeElementFromEvent(event);
750 var isPseudoElement = !!treeElement._node.pseudoType();
751 var isTag = treeElement._node.nodeType() === Node.ELEMENT_NODE && !isPseudoElement;
752 var textNode = event.target.enclosingNodeOrSelfWithClass("webkit-html-text-node");
753 if (textNode && textNode.classList.contains("bogus"))
755 var commentNode = event.target.enclosingNodeOrSelfWithClass("webkit-html-comment");
756 contextMenu.appendApplicableItems(event.target);
758 contextMenu.appendSeparator();
759 treeElement._populateTextContextMenu(contextMenu, textNode);
761 contextMenu.appendSeparator();
762 treeElement._populateTagContextMenu(contextMenu, event);
763 } else if (commentNode) {
764 contextMenu.appendSeparator();
765 treeElement._populateNodeContextMenu(contextMenu);
766 } else if (isPseudoElement) {
767 treeElement._populateScrollIntoView(contextMenu);
768 } else if (treeElement._node.isShadowRoot()) {
769 this.treeOutline._populateContextMenu(contextMenu, treeElement._node);
773 _updateModifiedNodes: function()
775 if (this._elementsTreeUpdater)
776 this._elementsTreeUpdater._updateModifiedNodes();
779 _populateContextMenu: function(contextMenu, node)
781 if (this._contextMenuCallback)
782 this._contextMenuCallback(contextMenu, node);
785 handleShortcut: function(event)
787 var node = this.selectedDOMNode();
788 var treeElement = this.getCachedTreeElement(node);
789 if (!node || !treeElement)
792 if (event.keyIdentifier === "F2" && treeElement.hasEditableNode()) {
793 this._toggleEditAsHTML(node);
794 event.handled = true;
798 if (WebInspector.KeyboardShortcut.eventHasCtrlOrMeta(event) && node.parentNode) {
799 if (event.keyIdentifier === "Up" && node.previousSibling) {
800 node.moveTo(node.parentNode, node.previousSibling, this._selectNodeAfterEdit.bind(this, treeElement.expanded));
801 event.handled = true;
804 if (event.keyIdentifier === "Down" && node.nextSibling) {
805 node.moveTo(node.parentNode, node.nextSibling.nextSibling, this._selectNodeAfterEdit.bind(this, treeElement.expanded));
806 event.handled = true;
813 * @param {!WebInspector.DOMNode} node
815 _toggleEditAsHTML: function(node)
817 var treeElement = this.getCachedTreeElement(node);
821 if (treeElement._editing && treeElement._htmlEditElement && WebInspector.isBeingEdited(treeElement._htmlEditElement))
822 treeElement._editing.commit();
824 treeElement._editAsHTML();
828 * @param {boolean} wasExpanded
829 * @param {?Protocol.Error} error
830 * @param {!DOMAgent.NodeId=} nodeId
832 _selectNodeAfterEdit: function(wasExpanded, error, nodeId)
837 // Select it and expand if necessary. We force tree update so that it processes dom events and is up to date.
838 this._updateModifiedNodes();
840 var newNode = nodeId ? this._domModel.nodeForId(nodeId) : null;
844 this.selectDOMNode(newNode, true);
846 var newTreeItem = this.findTreeElement(newNode);
849 newTreeItem.expand();
855 * Runs a script on the node's remote object that toggles a class name on
856 * the node and injects a stylesheet into the head of the node's document
857 * containing a rule to set "visibility: hidden" on the class and all it's
860 * @param {!WebInspector.DOMNode} node
861 * @param {function(?WebInspector.RemoteObject, boolean=)=} userCallback
863 _toggleHideShortcut: function(node, userCallback)
865 var pseudoType = node.pseudoType();
866 var effectiveNode = pseudoType ? node.parentNode : node;
870 function resolvedNode(object)
876 * @param {?string} pseudoType
877 * @suppressReceiverCheck
880 function toggleClassAndInjectStyleRule(pseudoType)
882 const classNamePrefix = "__web-inspector-hide";
883 const classNameSuffix = "-shortcut__";
884 const styleTagId = "__web-inspector-hide-shortcut-style__";
885 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; }";
887 var className = classNamePrefix + (pseudoType || "") + classNameSuffix;
888 this.classList.toggle(className);
890 var style = document.head.querySelector("style#" + styleTagId);
894 style = document.createElement("style");
895 style.id = styleTagId;
896 style.type = "text/css";
897 style.textContent = styleRules;
898 document.head.appendChild(style);
901 object.callFunction(toggleClassAndInjectStyleRule, [{ value: pseudoType }], userCallback);
905 effectiveNode.resolveToObject("", resolvedNode);
908 __proto__: TreeOutline.prototype
914 WebInspector.ElementsTreeOutline.ElementDecorator = function()
918 WebInspector.ElementsTreeOutline.ElementDecorator.prototype = {
920 * @param {!WebInspector.DOMNode} node
923 decorate: function(node)
928 * @param {!WebInspector.DOMNode} node
931 decorateAncestor: function(node)
938 * @implements {WebInspector.ElementsTreeOutline.ElementDecorator}
940 WebInspector.ElementsTreeOutline.PseudoStateDecorator = function()
942 WebInspector.ElementsTreeOutline.ElementDecorator.call(this);
945 WebInspector.ElementsTreeOutline.PseudoStateDecorator.prototype = {
947 * @param {!WebInspector.DOMNode} node
950 decorate: function(node)
952 if (node.nodeType() !== Node.ELEMENT_NODE)
954 var propertyValue = node.getUserProperty(WebInspector.CSSStyleModel.PseudoStatePropertyName);
957 return WebInspector.UIString("Element state: %s", ":" + propertyValue.join(", :"));
961 * @param {!WebInspector.DOMNode} node
964 decorateAncestor: function(node)
966 if (node.nodeType() !== Node.ELEMENT_NODE)
969 var descendantCount = node.descendantUserPropertyCount(WebInspector.CSSStyleModel.PseudoStatePropertyName);
970 if (!descendantCount)
972 if (descendantCount === 1)
973 return WebInspector.UIString("%d descendant with forced state", descendantCount);
974 return WebInspector.UIString("%d descendants with forced state", descendantCount);
980 * @extends {TreeElement}
981 * @param {!WebInspector.DOMNode} node
982 * @param {boolean=} elementCloseTag
984 WebInspector.ElementsTreeElement = function(node, elementCloseTag)
986 // The title will be updated in onattach.
987 TreeElement.call(this, "", node);
990 this._elementCloseTag = elementCloseTag;
991 this._updateHasChildren();
993 if (this._node.nodeType() == Node.ELEMENT_NODE && !elementCloseTag)
994 this._canAddAttributes = true;
995 this._searchQuery = null;
996 this._expandedChildrenLimit = WebInspector.ElementsTreeElement.InitialChildrenLimit;
999 WebInspector.ElementsTreeElement.InitialChildrenLimit = 500;
1001 // A union of HTML4 and HTML5-Draft elements that explicitly
1002 // or implicitly (for HTML5) forbid the closing tag.
1003 // FIXME: Revise once HTML5 Final is published.
1004 WebInspector.ElementsTreeElement.ForbiddenClosingTagElements = [
1005 "area", "base", "basefont", "br", "canvas", "col", "command", "embed", "frame",
1006 "hr", "img", "input", "keygen", "link", "meta", "param", "source"
1009 // These tags we do not allow editing their tag name.
1010 WebInspector.ElementsTreeElement.EditTagBlacklist = [
1011 "html", "head", "body"
1014 WebInspector.ElementsTreeElement.prototype = {
1015 highlightSearchResults: function(searchQuery)
1017 if (this._searchQuery !== searchQuery) {
1018 this._updateSearchHighlight(false);
1019 delete this._highlightResult; // A new search query.
1022 this._searchQuery = searchQuery;
1023 this._searchHighlightsVisible = true;
1024 this.updateTitle(true);
1027 hideSearchHighlights: function()
1029 delete this._searchHighlightsVisible;
1030 this._updateSearchHighlight(false);
1033 _updateSearchHighlight: function(show)
1035 if (!this._highlightResult)
1038 function updateEntryShow(entry)
1040 switch (entry.type) {
1042 entry.parent.insertBefore(entry.node, entry.nextSibling);
1045 entry.node.textContent = entry.newText;
1050 function updateEntryHide(entry)
1052 switch (entry.type) {
1054 entry.node.remove();
1057 entry.node.textContent = entry.oldText;
1062 // Preserve the semantic of node by following the order of updates for hide and show.
1064 for (var i = 0, size = this._highlightResult.length; i < size; ++i)
1065 updateEntryShow(this._highlightResult[i]);
1067 for (var i = (this._highlightResult.length - 1); i >= 0; --i)
1068 updateEntryHide(this._highlightResult[i]);
1073 * @param {boolean} inClipboard
1075 setInClipboard: function(inClipboard)
1077 if (this._inClipboard === inClipboard)
1079 this._inClipboard = inClipboard;
1080 this.listItemElement.classList.toggle("in-clipboard", inClipboard);
1085 return this._hovered;
1090 if (this._hovered === x)
1095 if (this.listItemElement) {
1097 this.updateSelection();
1098 this.listItemElement.classList.add("hovered");
1100 this.listItemElement.classList.remove("hovered");
1105 get expandedChildrenLimit()
1107 return this._expandedChildrenLimit;
1110 set expandedChildrenLimit(x)
1112 if (this._expandedChildrenLimit === x)
1115 this._expandedChildrenLimit = x;
1116 if (this.treeOutline && !this._updateChildrenInProgress)
1117 this._updateChildren(true);
1120 get expandedChildCount()
1122 var count = this.children.length;
1123 if (count && this.children[count - 1]._elementCloseTag)
1125 if (count && this.children[count - 1].expandAllButton)
1131 * @param {!WebInspector.DOMNode} child
1132 * @return {?WebInspector.ElementsTreeElement}
1134 _showChild: function(child)
1136 if (this._elementCloseTag)
1139 var index = this._visibleChildren().indexOf(child);
1143 if (index >= this.expandedChildrenLimit) {
1144 this._expandedChildrenLimit = index + 1;
1145 this._updateChildren(true);
1148 // Whether index-th child is visible in the children tree
1149 return this.expandedChildCount > index ? this.children[index] : null;
1152 updateSelection: function()
1154 var listItemElement = this.listItemElement;
1155 if (!listItemElement)
1158 if (!this._readyToUpdateSelection) {
1159 if (document.body.offsetWidth > 0)
1160 this._readyToUpdateSelection = true;
1162 // The stylesheet hasn't loaded yet or the window is closed,
1163 // so we can't calculate what we need. Return early.
1168 if (!this.selectionElement) {
1169 this.selectionElement = document.createElement("div");
1170 this.selectionElement.className = "selection selected";
1171 listItemElement.insertBefore(this.selectionElement, listItemElement.firstChild);
1174 this.selectionElement.style.height = listItemElement.offsetHeight + "px";
1177 onattach: function()
1179 if (this._hovered) {
1180 this.updateSelection();
1181 this.listItemElement.classList.add("hovered");
1185 this._preventFollowingLinksOnDoubleClick();
1186 this.listItemElement.draggable = true;
1189 _preventFollowingLinksOnDoubleClick: function()
1191 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");
1195 for (var i = 0; i < links.length; ++i)
1196 links[i].preventFollowOnDoubleClick = true;
1199 onpopulate: function()
1201 this.populated = true;
1202 if (this.children.length || !this.hasChildren)
1205 this.updateChildren();
1209 * @param {boolean=} fullRefresh
1211 updateChildren: function(fullRefresh)
1213 if (!this.hasChildren)
1215 console.assert(!this._elementCloseTag);
1216 this._node.getChildNodes(this._updateChildren.bind(this, fullRefresh));
1220 * @param {!WebInspector.DOMNode} child
1221 * @param {number} index
1222 * @param {boolean=} closingTag
1223 * @return {!WebInspector.ElementsTreeElement}
1225 insertChildElement: function(child, index, closingTag)
1227 var newElement = new WebInspector.ElementsTreeElement(child, closingTag);
1228 newElement.selectable = this.treeOutline._selectEnabled;
1229 this.insertChild(newElement, index);
1233 moveChild: function(child, targetIndex)
1235 var wasSelected = child.selected;
1236 this.removeChild(child);
1237 this.insertChild(child, targetIndex);
1243 * @param {boolean=} fullRefresh
1245 _updateChildren: function(fullRefresh)
1247 if (this._updateChildrenInProgress || !this.treeOutline._visible)
1250 this._updateChildrenInProgress = true;
1251 var selectedNode = this.treeOutline.selectedDOMNode();
1252 var originalScrollTop = 0;
1254 var treeOutlineContainerElement = this.treeOutline.element.parentNode;
1255 originalScrollTop = treeOutlineContainerElement.scrollTop;
1256 var selectedTreeElement = this.treeOutline.selectedTreeElement;
1257 if (selectedTreeElement && selectedTreeElement.hasAncestor(this))
1259 this.removeChildren();
1263 * @this {WebInspector.ElementsTreeElement}
1264 * @return {?WebInspector.ElementsTreeElement}
1266 function updateChildrenOfNode()
1268 var treeOutline = this.treeOutline;
1269 var visibleChildren = this._visibleChildren();
1270 var treeChildIndex = 0;
1271 var elementToSelect = null;
1273 for (var i = 0; i < visibleChildren.length; ++i) {
1274 var child = visibleChildren[i];
1275 var currentTreeElement = this.children[treeChildIndex];
1276 if (!currentTreeElement || currentTreeElement._node !== child) {
1277 // Find any existing element that is later in the children list.
1278 var existingTreeElement = null;
1279 for (var j = (treeChildIndex + 1), size = this.expandedChildCount; j < size; ++j) {
1280 if (this.children[j]._node === child) {
1281 existingTreeElement = this.children[j];
1286 if (existingTreeElement && existingTreeElement.parent === this) {
1287 // If an existing element was found and it has the same parent, just move it.
1288 this.moveChild(existingTreeElement, treeChildIndex);
1290 // No existing element found, insert a new element.
1291 if (treeChildIndex < this.expandedChildrenLimit) {
1292 var newElement = this.insertChildElement(child, treeChildIndex);
1293 if (child === selectedNode)
1294 elementToSelect = newElement;
1295 if (this.expandedChildCount > this.expandedChildrenLimit)
1296 this.expandedChildrenLimit++;
1303 return elementToSelect;
1306 // Remove any tree elements that no longer have this node (or this node's contentDocument) as their parent.
1307 for (var i = (this.children.length - 1); i >= 0; --i) {
1308 var currentChild = this.children[i];
1309 var currentNode = currentChild._node;
1312 var currentParentNode = currentNode.parentNode;
1314 if (currentParentNode === this._node)
1317 var selectedTreeElement = this.treeOutline.selectedTreeElement;
1318 if (selectedTreeElement && (selectedTreeElement === currentChild || selectedTreeElement.hasAncestor(currentChild)))
1321 this.removeChildAtIndex(i);
1324 var elementToSelect = updateChildrenOfNode.call(this);
1326 this._adjustCollapsedRange();
1328 var lastChild = this.children[this.children.length - 1];
1329 if (this._node.nodeType() === Node.ELEMENT_NODE && this.hasChildren)
1330 this.insertChildElement(this._node, this.children.length, true);
1332 // We want to restore the original selection and tree scroll position after a full refresh, if possible.
1333 if (fullRefresh && elementToSelect) {
1334 elementToSelect.select();
1335 if (treeOutlineContainerElement && originalScrollTop <= treeOutlineContainerElement.scrollHeight)
1336 treeOutlineContainerElement.scrollTop = originalScrollTop;
1339 delete this._updateChildrenInProgress;
1342 _adjustCollapsedRange: function()
1344 var visibleChildren = this._visibleChildren();
1345 // Ensure precondition: only the tree elements for node children are found in the tree
1346 // (not the Expand All button or the closing tag).
1347 if (this.expandAllButtonElement && this.expandAllButtonElement.__treeElement.parent)
1348 this.removeChild(this.expandAllButtonElement.__treeElement);
1350 const childNodeCount = visibleChildren.length;
1352 // In case some nodes from the expanded range were removed, pull some nodes from the collapsed range into the expanded range at the bottom.
1353 for (var i = this.expandedChildCount, limit = Math.min(this.expandedChildrenLimit, childNodeCount); i < limit; ++i)
1354 this.insertChildElement(visibleChildren[i], i);
1356 const expandedChildCount = this.expandedChildCount;
1357 if (childNodeCount > this.expandedChildCount) {
1358 var targetButtonIndex = expandedChildCount;
1359 if (!this.expandAllButtonElement) {
1360 var button = document.createElement("button");
1361 button.className = "text-button";
1363 var item = new TreeElement(button, null, false);
1364 item.selectable = false;
1365 item.expandAllButton = true;
1366 this.insertChild(item, targetButtonIndex);
1367 this.expandAllButtonElement = item.listItemElement.firstChild;
1368 this.expandAllButtonElement.__treeElement = item;
1369 this.expandAllButtonElement.addEventListener("click", this.handleLoadAllChildren.bind(this), false);
1370 } else if (!this.expandAllButtonElement.__treeElement.parent)
1371 this.insertChild(this.expandAllButtonElement.__treeElement, targetButtonIndex);
1372 this.expandAllButtonElement.textContent = WebInspector.UIString("Show All Nodes (%d More)", childNodeCount - expandedChildCount);
1373 } else if (this.expandAllButtonElement)
1374 delete this.expandAllButtonElement;
1377 handleLoadAllChildren: function()
1379 this.expandedChildrenLimit = Math.max(this._visibleChildCount(), this.expandedChildrenLimit + WebInspector.ElementsTreeElement.InitialChildrenLimit);
1382 expandRecursively: function()
1385 * @this {WebInspector.ElementsTreeElement}
1389 TreeElement.prototype.expandRecursively.call(this, Number.MAX_VALUE);
1392 this._node.getSubtree(-1, callback.bind(this));
1398 onexpand: function()
1400 if (this._elementCloseTag)
1404 this.treeOutline.updateSelection();
1407 oncollapse: function()
1409 if (this._elementCloseTag)
1413 this.treeOutline.updateSelection();
1419 onreveal: function()
1421 if (this.listItemElement) {
1422 var tagSpans = this.listItemElement.getElementsByClassName("webkit-html-tag-name");
1423 if (tagSpans.length)
1424 tagSpans[0].scrollIntoViewIfNeeded(true);
1426 this.listItemElement.scrollIntoViewIfNeeded(true);
1432 * @param {boolean=} selectedByUser
1435 onselect: function(selectedByUser)
1437 this.treeOutline.suppressRevealAndSelect = true;
1438 this.treeOutline.selectDOMNode(this._node, selectedByUser);
1440 this._node.highlight();
1441 this.updateSelection();
1442 this.treeOutline.suppressRevealAndSelect = false;
1450 ondelete: function()
1452 var startTagTreeElement = this.treeOutline.findTreeElement(this._node);
1453 startTagTreeElement ? startTagTreeElement.remove() : this.remove();
1463 // On Enter or Return start editing the first attribute
1464 // or create a new attribute on the selected element.
1468 this._startEditing();
1470 // prevent a newline from being immediately inserted
1474 selectOnMouseDown: function(event)
1476 TreeElement.prototype.selectOnMouseDown.call(this, event);
1481 if (this.treeOutline._showInElementsPanelEnabled) {
1482 WebInspector.inspectorView.showPanel("elements");
1483 this.treeOutline.selectDOMNode(this._node, true);
1486 // Prevent selecting the nearest word on double click.
1487 if (event.detail >= 2)
1488 event.preventDefault();
1495 ondblclick: function(event)
1497 if (this._editing || this._elementCloseTag)
1500 if (this._startEditingTarget(event.target))
1503 if (this.hasChildren && !this.expanded)
1511 hasEditableNode: function()
1513 return !this.representedObject.isShadowRoot() && !this.representedObject.ancestorUserAgentShadowRoot();
1516 _insertInLastAttributePosition: function(tag, node)
1518 if (tag.getElementsByClassName("webkit-html-attribute").length > 0)
1519 tag.insertBefore(node, tag.lastChild);
1521 var nodeName = tag.textContent.match(/^<(.*?)>$/)[1];
1522 tag.textContent = '';
1523 tag.appendChild(document.createTextNode('<'+nodeName));
1524 tag.appendChild(node);
1525 tag.appendChild(document.createTextNode('>'));
1528 this.updateSelection();
1531 _startEditingTarget: function(eventTarget)
1533 if (this.treeOutline.selectedDOMNode() != this._node)
1536 if (this._node.nodeType() != Node.ELEMENT_NODE && this._node.nodeType() != Node.TEXT_NODE)
1539 var textNode = eventTarget.enclosingNodeOrSelfWithClass("webkit-html-text-node");
1541 return this._startEditingTextNode(textNode);
1543 var attribute = eventTarget.enclosingNodeOrSelfWithClass("webkit-html-attribute");
1545 return this._startEditingAttribute(attribute, eventTarget);
1547 var tagName = eventTarget.enclosingNodeOrSelfWithClass("webkit-html-tag-name");
1549 return this._startEditingTagName(tagName);
1551 var newAttribute = eventTarget.enclosingNodeOrSelfWithClass("add-attribute");
1553 return this._addNewAttribute();
1559 * @param {!WebInspector.ContextMenu} contextMenu
1560 * @param {!Event} event
1562 _populateTagContextMenu: function(contextMenu, event)
1564 // Add attribute-related actions.
1565 var treeElement = this._elementCloseTag ? this.treeOutline.findTreeElement(this._node) : this;
1566 contextMenu.appendItem(WebInspector.UIString(WebInspector.useLowerCaseMenuTitles() ? "Add attribute" : "Add Attribute"), treeElement._addNewAttribute.bind(treeElement));
1568 var attribute = event.target.enclosingNodeOrSelfWithClass("webkit-html-attribute");
1569 var newAttribute = event.target.enclosingNodeOrSelfWithClass("add-attribute");
1570 if (attribute && !newAttribute)
1571 contextMenu.appendItem(WebInspector.UIString(WebInspector.useLowerCaseMenuTitles() ? "Edit attribute" : "Edit Attribute"), this._startEditingAttribute.bind(this, attribute, event.target));
1572 contextMenu.appendSeparator();
1573 if (this.treeOutline._setPseudoClassCallback) {
1574 var pseudoSubMenu = contextMenu.appendSubMenuItem(WebInspector.UIString(WebInspector.useLowerCaseMenuTitles() ? "Force element state" : "Force Element State"));
1575 this._populateForcedPseudoStateItems(pseudoSubMenu);
1576 contextMenu.appendSeparator();
1578 this._populateNodeContextMenu(contextMenu);
1579 this.treeOutline._populateContextMenu(contextMenu, this._node);
1580 this._populateScrollIntoView(contextMenu);
1584 * @param {!WebInspector.ContextMenu} contextMenu
1586 _populateScrollIntoView: function(contextMenu)
1588 contextMenu.appendSeparator();
1589 contextMenu.appendItem(WebInspector.UIString(WebInspector.useLowerCaseMenuTitles() ? "Scroll into view" : "Scroll into View"), this._scrollIntoView.bind(this));
1592 _populateForcedPseudoStateItems: function(subMenu)
1594 const pseudoClasses = ["active", "hover", "focus", "visited"];
1595 var node = this._node;
1596 var forcedPseudoState = (node ? node.getUserProperty("pseudoState") : null) || [];
1597 for (var i = 0; i < pseudoClasses.length; ++i) {
1598 var pseudoClassForced = forcedPseudoState.indexOf(pseudoClasses[i]) >= 0;
1599 subMenu.appendCheckboxItem(":" + pseudoClasses[i], this.treeOutline._setPseudoClassCallback.bind(null, node, pseudoClasses[i], !pseudoClassForced), pseudoClassForced, false);
1603 _populateTextContextMenu: function(contextMenu, textNode)
1606 contextMenu.appendItem(WebInspector.UIString(WebInspector.useLowerCaseMenuTitles() ? "Edit text" : "Edit Text"), this._startEditingTextNode.bind(this, textNode));
1607 this._populateNodeContextMenu(contextMenu);
1610 _populateNodeContextMenu: function(contextMenu)
1612 // Add free-form node-related actions.
1613 var openTagElement = this.treeOutline.getCachedTreeElement(this.representedObject) || this;
1614 var isEditable = this.hasEditableNode();
1615 if (isEditable && !this._editing)
1616 contextMenu.appendItem(WebInspector.UIString("Edit as HTML"), openTagElement._editAsHTML.bind(openTagElement));
1617 var isShadowRoot = this.representedObject.isShadowRoot();
1619 // Place it here so that all "Copy"-ing items stick together.
1620 if (this.representedObject.nodeType() === Node.ELEMENT_NODE)
1621 contextMenu.appendItem(WebInspector.UIString(WebInspector.useLowerCaseMenuTitles() ? "Copy CSS path" : "Copy CSS Path"), this._copyCSSPath.bind(this));
1623 contextMenu.appendItem(WebInspector.UIString("Copy XPath"), this._copyXPath.bind(this));
1624 if (!isShadowRoot) {
1625 var treeOutline = this.treeOutline;
1626 contextMenu.appendItem(WebInspector.UIString("Copy"), treeOutline._performCopyOrCut.bind(treeOutline, false, this.representedObject));
1627 contextMenu.appendItem(WebInspector.UIString("Cut"), treeOutline._performCopyOrCut.bind(treeOutline, true, this.representedObject), !this.hasEditableNode());
1628 contextMenu.appendItem(WebInspector.UIString("Paste"), treeOutline._pasteNode.bind(treeOutline, this.representedObject), !treeOutline._canPaste(this.representedObject));
1632 contextMenu.appendItem(WebInspector.UIString(WebInspector.useLowerCaseMenuTitles() ? "Delete node" : "Delete Node"), this.remove.bind(this));
1635 _startEditing: function()
1637 if (this.treeOutline.selectedDOMNode() !== this._node)
1640 var listItem = this._listItemNode;
1642 if (this._canAddAttributes) {
1643 var attribute = listItem.getElementsByClassName("webkit-html-attribute")[0];
1645 return this._startEditingAttribute(attribute, attribute.getElementsByClassName("webkit-html-attribute-value")[0]);
1647 return this._addNewAttribute();
1650 if (this._node.nodeType() === Node.TEXT_NODE) {
1651 var textNode = listItem.getElementsByClassName("webkit-html-text-node")[0];
1653 return this._startEditingTextNode(textNode);
1658 _addNewAttribute: function()
1660 // Cannot just convert the textual html into an element without
1661 // a parent node. Use a temporary span container for the HTML.
1662 var container = document.createElement("span");
1663 this._buildAttributeDOM(container, " ", "");
1664 var attr = container.firstElementChild;
1665 attr.style.marginLeft = "2px"; // overrides the .editing margin rule
1666 attr.style.marginRight = "2px"; // overrides the .editing margin rule
1668 var tag = this.listItemElement.getElementsByClassName("webkit-html-tag")[0];
1669 this._insertInLastAttributePosition(tag, attr);
1670 attr.scrollIntoViewIfNeeded(true);
1671 return this._startEditingAttribute(attr, attr);
1674 _triggerEditAttribute: function(attributeName)
1676 var attributeElements = this.listItemElement.getElementsByClassName("webkit-html-attribute-name");
1677 for (var i = 0, len = attributeElements.length; i < len; ++i) {
1678 if (attributeElements[i].textContent === attributeName) {
1679 for (var elem = attributeElements[i].nextSibling; elem; elem = elem.nextSibling) {
1680 if (elem.nodeType !== Node.ELEMENT_NODE)
1683 if (elem.classList.contains("webkit-html-attribute-value"))
1684 return this._startEditingAttribute(elem.parentNode, elem);
1690 _startEditingAttribute: function(attribute, elementForSelection)
1692 if (WebInspector.isBeingEdited(attribute))
1695 var attributeNameElement = attribute.getElementsByClassName("webkit-html-attribute-name")[0];
1696 if (!attributeNameElement)
1699 var attributeName = attributeNameElement.textContent;
1700 var attributeValueElement = attribute.getElementsByClassName("webkit-html-attribute-value")[0];
1702 function removeZeroWidthSpaceRecursive(node)
1704 if (node.nodeType === Node.TEXT_NODE) {
1705 node.nodeValue = node.nodeValue.replace(/\u200B/g, "");
1709 if (node.nodeType !== Node.ELEMENT_NODE)
1712 for (var child = node.firstChild; child; child = child.nextSibling)
1713 removeZeroWidthSpaceRecursive(child);
1717 var listItemElement = attribute.enclosingNodeOrSelfWithNodeName("li");
1718 if (attributeName && attributeValueElement && listItemElement && listItemElement.treeElement)
1719 domNode = listItemElement.treeElement.representedObject;
1720 var attributeValue = domNode ? domNode.getAttribute(attributeName) : undefined;
1721 if (typeof attributeValue !== "undefined")
1722 attributeValueElement.textContent = attributeValue;
1724 // Remove zero-width spaces that were added by nodeTitleInfo.
1725 removeZeroWidthSpaceRecursive(attribute);
1727 var config = new WebInspector.InplaceEditor.Config(this._attributeEditingCommitted.bind(this), this._editingCancelled.bind(this), attributeName);
1729 function handleKeyDownEvents(event)
1731 var isMetaOrCtrl = WebInspector.isMac() ?
1732 event.metaKey && !event.shiftKey && !event.ctrlKey && !event.altKey :
1733 event.ctrlKey && !event.shiftKey && !event.metaKey && !event.altKey;
1734 if (isEnterKey(event) && (event.isMetaOrCtrlForTest || !config.multiline || isMetaOrCtrl))
1736 else if (event.keyCode === WebInspector.KeyboardShortcut.Keys.Esc.code || event.keyIdentifier === "U+001B")
1738 else if (event.keyIdentifier === "U+0009") // Tab key
1739 return "move-" + (event.shiftKey ? "backward" : "forward");
1741 WebInspector.handleElementValueModifications(event, attribute);
1746 config.customFinishHandler = handleKeyDownEvents;
1748 this._editing = WebInspector.InplaceEditor.startEditing(attribute, config);
1750 window.getSelection().setBaseAndExtent(elementForSelection, 0, elementForSelection, 1);
1756 * @param {!Element} textNodeElement
1758 _startEditingTextNode: function(textNodeElement)
1760 if (WebInspector.isBeingEdited(textNodeElement))
1763 var textNode = this._node;
1764 // We only show text nodes inline in elements if the element only
1765 // has a single child, and that child is a text node.
1766 if (textNode.nodeType() === Node.ELEMENT_NODE && textNode.firstChild)
1767 textNode = textNode.firstChild;
1769 var container = textNodeElement.enclosingNodeOrSelfWithClass("webkit-html-text-node");
1771 container.textContent = textNode.nodeValue(); // Strip the CSS or JS highlighting if present.
1772 var config = new WebInspector.InplaceEditor.Config(this._textNodeEditingCommitted.bind(this, textNode), this._editingCancelled.bind(this));
1773 this._editing = WebInspector.InplaceEditor.startEditing(textNodeElement, config);
1774 window.getSelection().setBaseAndExtent(textNodeElement, 0, textNodeElement, 1);
1780 * @param {!Element=} tagNameElement
1782 _startEditingTagName: function(tagNameElement)
1784 if (!tagNameElement) {
1785 tagNameElement = this.listItemElement.getElementsByClassName("webkit-html-tag-name")[0];
1786 if (!tagNameElement)
1790 var tagName = tagNameElement.textContent;
1791 if (WebInspector.ElementsTreeElement.EditTagBlacklist[tagName.toLowerCase()])
1794 if (WebInspector.isBeingEdited(tagNameElement))
1797 var closingTagElement = this._distinctClosingTagElement();
1800 * @param {!Event} event
1802 function keyupListener(event)
1804 if (closingTagElement)
1805 closingTagElement.textContent = "</" + tagNameElement.textContent + ">";
1809 * @param {!Element} element
1810 * @param {string} newTagName
1811 * @this {WebInspector.ElementsTreeElement}
1813 function editingComitted(element, newTagName)
1815 tagNameElement.removeEventListener('keyup', keyupListener, false);
1816 this._tagNameEditingCommitted.apply(this, arguments);
1820 * @this {WebInspector.ElementsTreeElement}
1822 function editingCancelled()
1824 tagNameElement.removeEventListener('keyup', keyupListener, false);
1825 this._editingCancelled.apply(this, arguments);
1828 tagNameElement.addEventListener('keyup', keyupListener, false);
1830 var config = new WebInspector.InplaceEditor.Config(editingComitted.bind(this), editingCancelled.bind(this), tagName);
1831 this._editing = WebInspector.InplaceEditor.startEditing(tagNameElement, config);
1832 window.getSelection().setBaseAndExtent(tagNameElement, 0, tagNameElement, 1);
1837 * @param {function(string, string)} commitCallback
1838 * @param {?Protocol.Error} error
1839 * @param {string} initialValue
1841 _startEditingAsHTML: function(commitCallback, error, initialValue)
1848 function consume(event)
1850 if (event.eventPhase === Event.AT_TARGET)
1851 event.consume(true);
1854 initialValue = this._convertWhitespaceToEntities(initialValue).text;
1856 this._htmlEditElement = document.createElement("div");
1857 this._htmlEditElement.className = "source-code elements-tree-editor";
1859 // Hide header items.
1860 var child = this.listItemElement.firstChild;
1862 child.style.display = "none";
1863 child = child.nextSibling;
1865 // Hide children item.
1866 if (this._childrenListNode)
1867 this._childrenListNode.style.display = "none";
1869 this.listItemElement.appendChild(this._htmlEditElement);
1870 this.treeOutline.childrenListElement.parentElement.addEventListener("mousedown", consume, false);
1872 this.updateSelection();
1875 * @param {!Element} element
1876 * @param {string} newValue
1877 * @this {WebInspector.ElementsTreeElement}
1879 function commit(element, newValue)
1881 commitCallback(initialValue, newValue);
1886 * @this {WebInspector.ElementsTreeElement}
1890 delete this._editing;
1891 delete this.treeOutline._multilineEditing;
1894 this.listItemElement.removeChild(this._htmlEditElement);
1895 delete this._htmlEditElement;
1896 // Unhide children item.
1897 if (this._childrenListNode)
1898 this._childrenListNode.style.removeProperty("display");
1899 // Unhide header items.
1900 var child = this.listItemElement.firstChild;
1902 child.style.removeProperty("display");
1903 child = child.nextSibling;
1906 this.treeOutline.childrenListElement.parentElement.removeEventListener("mousedown", consume, false);
1907 this.updateSelection();
1908 this.treeOutline.element.focus();
1911 var config = new WebInspector.InplaceEditor.Config(commit.bind(this), dispose.bind(this));
1912 config.setMultilineOptions(initialValue, { name: "xml", htmlMode: true }, "web-inspector-html", WebInspector.settings.domWordWrap.get(), true);
1913 this._editing = WebInspector.InplaceEditor.startEditing(this._htmlEditElement, config);
1914 this._editing.setWidth(this.treeOutline._visibleWidth);
1915 this.treeOutline._multilineEditing = this._editing;
1918 _attributeEditingCommitted: function(element, newText, oldText, attributeName, moveDirection)
1920 delete this._editing;
1922 var treeOutline = this.treeOutline;
1925 * @param {?Protocol.Error=} error
1926 * @this {WebInspector.ElementsTreeElement}
1928 function moveToNextAttributeIfNeeded(error)
1931 this._editingCancelled(element, attributeName);
1936 treeOutline._updateModifiedNodes();
1938 // Search for the attribute's position, and then decide where to move to.
1939 var attributes = this._node.attributes();
1940 for (var i = 0; i < attributes.length; ++i) {
1941 if (attributes[i].name !== attributeName)
1944 if (moveDirection === "backward") {
1946 this._startEditingTagName();
1948 this._triggerEditAttribute(attributes[i - 1].name);
1950 if (i === attributes.length - 1)
1951 this._addNewAttribute();
1953 this._triggerEditAttribute(attributes[i + 1].name);
1958 // Moving From the "New Attribute" position.
1959 if (moveDirection === "backward") {
1960 if (newText === " ") {
1961 // Moving from "New Attribute" that was not edited
1962 if (attributes.length > 0)
1963 this._triggerEditAttribute(attributes[attributes.length - 1].name);
1965 // Moving from "New Attribute" that holds new value
1966 if (attributes.length > 1)
1967 this._triggerEditAttribute(attributes[attributes.length - 2].name);
1969 } else if (moveDirection === "forward") {
1970 if (!/^\s*$/.test(newText))
1971 this._addNewAttribute();
1973 this._startEditingTagName();
1978 if ((attributeName.trim() || newText.trim()) && oldText !== newText) {
1979 this._node.setAttribute(attributeName, newText, moveToNextAttributeIfNeeded.bind(this));
1984 moveToNextAttributeIfNeeded.call(this);
1987 _tagNameEditingCommitted: function(element, newText, oldText, tagName, moveDirection)
1989 delete this._editing;
1994 var closingTagElement = self._distinctClosingTagElement();
1995 if (closingTagElement)
1996 closingTagElement.textContent = "</" + tagName + ">";
1998 self._editingCancelled(element, tagName);
1999 moveToNextAttributeIfNeeded.call(self);
2003 * @this {WebInspector.ElementsTreeElement}
2005 function moveToNextAttributeIfNeeded()
2007 if (moveDirection !== "forward") {
2008 this._addNewAttribute();
2012 var attributes = this._node.attributes();
2013 if (attributes.length > 0)
2014 this._triggerEditAttribute(attributes[0].name);
2016 this._addNewAttribute();
2019 newText = newText.trim();
2020 if (newText === oldText) {
2025 var treeOutline = this.treeOutline;
2026 var wasExpanded = this.expanded;
2028 function changeTagNameCallback(error, nodeId)
2030 if (error || !nodeId) {
2034 var newTreeItem = treeOutline._selectNodeAfterEdit(wasExpanded, error, nodeId);
2035 moveToNextAttributeIfNeeded.call(newTreeItem);
2038 this._node.setNodeName(newText, changeTagNameCallback);
2042 * @param {!WebInspector.DOMNode} textNode
2043 * @param {!Element} element
2044 * @param {string} newText
2046 _textNodeEditingCommitted: function(textNode, element, newText)
2048 delete this._editing;
2051 * @this {WebInspector.ElementsTreeElement}
2057 textNode.setNodeValue(newText, callback.bind(this));
2061 * @param {!Element} element
2062 * @param {*} context
2064 _editingCancelled: function(element, context)
2066 delete this._editing;
2068 // Need to restore attributes structure.
2073 * @return {!Element}
2075 _distinctClosingTagElement: function()
2077 // FIXME: Improve the Tree Element / Outline Abstraction to prevent crawling the DOM
2079 // For an expanded element, it will be the last element with class "close"
2080 // in the child element list.
2081 if (this.expanded) {
2082 var closers = this._childrenListNode.querySelectorAll(".close");
2083 return closers[closers.length-1];
2086 // Remaining cases are single line non-expanded elements with a closing
2087 // tag, or HTML elements without a closing tag (such as <br>). Return
2088 // null in the case where there isn't a closing tag.
2089 var tags = this.listItemElement.getElementsByClassName("webkit-html-tag");
2090 return (tags.length === 1 ? null : tags[tags.length-1]);
2094 * @param {boolean=} onlySearchQueryChanged
2096 updateTitle: function(onlySearchQueryChanged)
2098 // If we are editing, return early to prevent canceling the edit.
2099 // After editing is committed updateTitle will be called.
2103 if (onlySearchQueryChanged) {
2104 if (this._highlightResult)
2105 this._updateSearchHighlight(false);
2107 var nodeInfo = this._nodeTitleInfo(WebInspector.linkifyURLAsNode);
2108 if (nodeInfo.shadowRoot)
2109 this.listItemElement.classList.add("shadow-root");
2110 var highlightElement = document.createElement("span");
2111 highlightElement.className = "highlight";
2112 highlightElement.appendChild(nodeInfo.titleDOM);
2113 this.title = highlightElement;
2114 this._updateDecorations();
2115 delete this._highlightResult;
2118 delete this.selectionElement;
2120 this.updateSelection();
2121 this._preventFollowingLinksOnDoubleClick();
2122 this._highlightSearchResults();
2126 * @return {?Element}
2128 _createDecoratorElement: function()
2130 var node = this._node;
2131 var decoratorMessages = [];
2132 var parentDecoratorMessages = [];
2133 for (var i = 0; i < this.treeOutline._nodeDecorators.length; ++i) {
2134 var decorator = this.treeOutline._nodeDecorators[i];
2135 var message = decorator.decorate(node);
2137 decoratorMessages.push(message);
2141 if (this.expanded || this._elementCloseTag)
2144 message = decorator.decorateAncestor(node);
2146 parentDecoratorMessages.push(message)
2148 if (!decoratorMessages.length && !parentDecoratorMessages.length)
2151 var decoratorElement = document.createElement("div");
2152 decoratorElement.classList.add("elements-gutter-decoration");
2153 if (!decoratorMessages.length)
2154 decoratorElement.classList.add("elements-has-decorated-children");
2155 decoratorElement.title = decoratorMessages.concat(parentDecoratorMessages).join("\n");
2156 return decoratorElement;
2159 _updateDecorations: function()
2161 if (this._decoratorElement)
2162 this._decoratorElement.remove();
2163 this._decoratorElement = this._createDecoratorElement();
2164 if (this._decoratorElement && this.listItemElement)
2165 this.listItemElement.insertBefore(this._decoratorElement, this.listItemElement.firstChild);
2169 * @param {!Node} parentElement
2170 * @param {string} name
2171 * @param {string} value
2172 * @param {boolean=} forceValue
2173 * @param {!WebInspector.DOMNode=} node
2174 * @param {function(string, string, string, boolean=, string=)=} linkify
2176 _buildAttributeDOM: function(parentElement, name, value, forceValue, node, linkify)
2178 var closingPunctuationRegex = /[\/;:\)\]\}]/g;
2179 var highlightIndex = 0;
2181 var additionalHighlightOffset = 0;
2185 * @param {string} match
2186 * @param {number} replaceOffset
2189 function replacer(match, replaceOffset) {
2190 while (highlightIndex < highlightCount && result.entityRanges[highlightIndex].offset < replaceOffset) {
2191 result.entityRanges[highlightIndex].offset += additionalHighlightOffset;
2194 additionalHighlightOffset += 1;
2195 return match + "\u200B";
2199 * @param {!Element} element
2200 * @param {string} value
2201 * @this {WebInspector.ElementsTreeElement}
2203 function setValueWithEntities(element, value)
2205 var attrValueElement = element.createChild("span", "webkit-html-attribute-value");
2206 result = this._convertWhitespaceToEntities(value);
2207 highlightCount = result.entityRanges.length;
2208 value = result.text.replace(closingPunctuationRegex, replacer);
2209 while (highlightIndex < highlightCount) {
2210 result.entityRanges[highlightIndex].offset += additionalHighlightOffset;
2213 attrValueElement.textContent = value;
2214 WebInspector.highlightRangesWithStyleClass(attrValueElement, result.entityRanges, "webkit-html-entity-value");
2217 var hasText = (forceValue || value.length > 0);
2218 var attrSpanElement = parentElement.createChild("span", "webkit-html-attribute");
2219 var attrNameElement = attrSpanElement.createChild("span", "webkit-html-attribute-name");
2220 attrNameElement.textContent = name;
2223 attrSpanElement.appendChild(document.createTextNode("=\u200B\""));
2225 if (linkify && (name === "src" || name === "href")) {
2226 var rewrittenHref = node.resolveURL(value);
2227 if (rewrittenHref === null) {
2228 setValueWithEntities.call(this, attrSpanElement, value);
2230 value = value.replace(closingPunctuationRegex, "$&\u200B");
2231 if (value.startsWith("data:"))
2232 value = value.trimMiddle(60);
2233 attrSpanElement.appendChild(linkify(rewrittenHref, value, "webkit-html-attribute-value", node.nodeName().toLowerCase() === "a"));
2236 setValueWithEntities.call(this, attrSpanElement, value);
2240 attrSpanElement.appendChild(document.createTextNode("\""));
2244 * @param {!Node} parentElement
2245 * @param {string} pseudoElementName
2247 _buildPseudoElementDOM: function(parentElement, pseudoElementName)
2249 var pseudoElement = parentElement.createChild("span", "webkit-html-pseudo-element");
2250 pseudoElement.textContent = "::" + pseudoElementName;
2251 parentElement.appendChild(document.createTextNode("\u200B"));
2255 * @param {!Node} parentElement
2256 * @param {string} tagName
2257 * @param {boolean} isClosingTag
2258 * @param {boolean} isDistinctTreeElement
2259 * @param {function(string, string, string, boolean=, string=)=} linkify
2261 _buildTagDOM: function(parentElement, tagName, isClosingTag, isDistinctTreeElement, linkify)
2263 var node = this._node;
2264 var classes = [ "webkit-html-tag" ];
2265 if (isClosingTag && isDistinctTreeElement)
2266 classes.push("close");
2267 var tagElement = parentElement.createChild("span", classes.join(" "));
2268 tagElement.appendChild(document.createTextNode("<"));
2269 var tagNameElement = tagElement.createChild("span", isClosingTag ? "" : "webkit-html-tag-name");
2270 tagNameElement.textContent = (isClosingTag ? "/" : "") + tagName;
2271 if (!isClosingTag && node.hasAttributes()) {
2272 var attributes = node.attributes();
2273 for (var i = 0; i < attributes.length; ++i) {
2274 var attr = attributes[i];
2275 tagElement.appendChild(document.createTextNode(" "));
2276 this._buildAttributeDOM(tagElement, attr.name, attr.value, false, node, linkify);
2279 tagElement.appendChild(document.createTextNode(">"));
2280 parentElement.appendChild(document.createTextNode("\u200B"));
2284 * @param {string} text
2285 * @return {!{text: string, entityRanges: !Array.<!WebInspector.SourceRange>}}
2287 _convertWhitespaceToEntities: function(text)
2290 var resultLength = 0;
2291 var lastIndexAfterEntity = 0;
2292 var entityRanges = [];
2293 var charToEntity = WebInspector.ElementsTreeOutline.MappedCharToEntity;
2294 for (var i = 0, size = text.length; i < size; ++i) {
2295 var char = text.charAt(i);
2296 if (charToEntity[char]) {
2297 result += text.substring(lastIndexAfterEntity, i);
2298 var entityValue = "&" + charToEntity[char] + ";";
2299 entityRanges.push({offset: result.length, length: entityValue.length});
2300 result += entityValue;
2301 lastIndexAfterEntity = i + 1;
2305 result += text.substring(lastIndexAfterEntity);
2306 return {text: result || text, entityRanges: entityRanges};
2310 * @param {function(string, string, string, boolean=, string=)=} linkify
2312 _nodeTitleInfo: function(linkify)
2314 var node = this._node;
2315 var info = {titleDOM: document.createDocumentFragment(), hasChildren: this.hasChildren};
2317 switch (node.nodeType()) {
2318 case Node.ATTRIBUTE_NODE:
2319 this._buildAttributeDOM(info.titleDOM, /** @type {string} */ (node.name), /** @type {string} */ (node.value), true);
2322 case Node.ELEMENT_NODE:
2323 var pseudoType = node.pseudoType();
2325 this._buildPseudoElementDOM(info.titleDOM, pseudoType);
2326 info.hasChildren = false;
2330 var tagName = node.nodeNameInCorrectCase();
2331 if (this._elementCloseTag) {
2332 this._buildTagDOM(info.titleDOM, tagName, true, true);
2333 info.hasChildren = false;
2337 this._buildTagDOM(info.titleDOM, tagName, false, false, linkify);
2339 var showInlineText = this._showInlineText() && !this.hasChildren;
2340 if (!this.expanded && !showInlineText && (this.treeOutline.isXMLMimeType || !WebInspector.ElementsTreeElement.ForbiddenClosingTagElements[tagName])) {
2341 if (this.hasChildren) {
2342 var textNodeElement = info.titleDOM.createChild("span", "webkit-html-text-node bogus");
2343 textNodeElement.textContent = "\u2026";
2344 info.titleDOM.appendChild(document.createTextNode("\u200B"));
2346 this._buildTagDOM(info.titleDOM, tagName, true, false);
2349 // If this element only has a single child that is a text node,
2350 // just show that text and the closing tag inline rather than
2351 // create a subtree for them
2352 if (showInlineText) {
2353 console.assert(!this.hasChildren);
2354 var textNodeElement = info.titleDOM.createChild("span", "webkit-html-text-node");
2355 var result = this._convertWhitespaceToEntities(node.firstChild.nodeValue());
2356 textNodeElement.textContent = result.text;
2357 WebInspector.highlightRangesWithStyleClass(textNodeElement, result.entityRanges, "webkit-html-entity-value");
2358 info.titleDOM.appendChild(document.createTextNode("\u200B"));
2359 this._buildTagDOM(info.titleDOM, tagName, true, false);
2360 info.hasChildren = false;
2364 case Node.TEXT_NODE:
2365 if (node.parentNode && node.parentNode.nodeName().toLowerCase() === "script") {
2366 var newNode = info.titleDOM.createChild("span", "webkit-html-text-node webkit-html-js-node");
2367 newNode.textContent = node.nodeValue();
2369 var javascriptSyntaxHighlighter = new WebInspector.DOMSyntaxHighlighter("text/javascript", true);
2370 javascriptSyntaxHighlighter.syntaxHighlightNode(newNode);
2371 } else if (node.parentNode && node.parentNode.nodeName().toLowerCase() === "style") {
2372 var newNode = info.titleDOM.createChild("span", "webkit-html-text-node webkit-html-css-node");
2373 newNode.textContent = node.nodeValue();
2375 var cssSyntaxHighlighter = new WebInspector.DOMSyntaxHighlighter("text/css", true);
2376 cssSyntaxHighlighter.syntaxHighlightNode(newNode);
2378 info.titleDOM.appendChild(document.createTextNode("\""));
2379 var textNodeElement = info.titleDOM.createChild("span", "webkit-html-text-node");
2380 var result = this._convertWhitespaceToEntities(node.nodeValue());
2381 textNodeElement.textContent = result.text;
2382 WebInspector.highlightRangesWithStyleClass(textNodeElement, result.entityRanges, "webkit-html-entity-value");
2383 info.titleDOM.appendChild(document.createTextNode("\""));
2387 case Node.COMMENT_NODE:
2388 var commentElement = info.titleDOM.createChild("span", "webkit-html-comment");
2389 commentElement.appendChild(document.createTextNode("<!--" + node.nodeValue() + "-->"));
2392 case Node.DOCUMENT_TYPE_NODE:
2393 var docTypeElement = info.titleDOM.createChild("span", "webkit-html-doctype");
2394 docTypeElement.appendChild(document.createTextNode("<!DOCTYPE " + node.nodeName()));
2395 if (node.publicId) {
2396 docTypeElement.appendChild(document.createTextNode(" PUBLIC \"" + node.publicId + "\""));
2398 docTypeElement.appendChild(document.createTextNode(" \"" + node.systemId + "\""));
2399 } else if (node.systemId)
2400 docTypeElement.appendChild(document.createTextNode(" SYSTEM \"" + node.systemId + "\""));
2402 if (node.internalSubset)
2403 docTypeElement.appendChild(document.createTextNode(" [" + node.internalSubset + "]"));
2405 docTypeElement.appendChild(document.createTextNode(">"));
2408 case Node.CDATA_SECTION_NODE:
2409 var cdataElement = info.titleDOM.createChild("span", "webkit-html-text-node");
2410 cdataElement.appendChild(document.createTextNode("<![CDATA[" + node.nodeValue() + "]]>"));
2412 case Node.DOCUMENT_FRAGMENT_NODE:
2413 var fragmentElement = info.titleDOM.createChild("span", "webkit-html-fragment");
2414 if (node.isInShadowTree()) {
2415 var shadowRootType = node.shadowRootType();
2416 if (shadowRootType) {
2417 info.shadowRoot = true;
2418 fragmentElement.classList.add("shadow-root");
2421 fragmentElement.textContent = node.nodeNameInCorrectCase().collapseWhitespace();
2424 info.titleDOM.appendChild(document.createTextNode(node.nodeNameInCorrectCase().collapseWhitespace()));
2432 _showInlineText: function()
2434 if (this._node.importedDocument() || this._node.templateContent() || this._visibleShadowRoots().length > 0 || this._node.hasPseudoElements())
2436 if (this._node.nodeType() !== Node.ELEMENT_NODE)
2438 if (!this._node.firstChild || this._node.firstChild !== this._node.lastChild || this._node.firstChild.nodeType() !== Node.TEXT_NODE)
2440 var textChild = this._node.firstChild;
2441 var maxInlineTextChildLength = 80;
2442 if (textChild.nodeValue().length < maxInlineTextChildLength)
2449 if (this._node.pseudoType())
2451 var parentElement = this.parent;
2456 function removeNodeCallback(error)
2461 parentElement.removeChild(self);
2462 parentElement._adjustCollapsedRange();
2465 if (!this._node.parentNode || this._node.parentNode.nodeType() === Node.DOCUMENT_NODE)
2467 this._node.removeNode(removeNodeCallback);
2470 _editAsHTML: function()
2472 var node = this._node;
2473 if (node.pseudoType())
2476 var treeOutline = this.treeOutline;
2477 var parentNode = node.parentNode;
2478 var index = node.index;
2479 var wasExpanded = this.expanded;
2482 * @param {?Protocol.Error} error
2484 function selectNode(error)
2489 // Select it and expand if necessary. We force tree update so that it processes dom events and is up to date.
2490 treeOutline._updateModifiedNodes();
2492 var newNode = parentNode ? parentNode.children()[index] || parentNode : null;
2496 treeOutline.selectDOMNode(newNode, true);
2499 var newTreeItem = treeOutline.findTreeElement(newNode);
2501 newTreeItem.expand();
2506 * @param {string} initialValue
2507 * @param {string} value
2509 function commitChange(initialValue, value)
2511 if (initialValue !== value)
2512 node.setOuterHTML(value, selectNode);
2515 node.getOuterHTML(this._startEditingAsHTML.bind(this, commitChange));
2518 _copyCSSPath: function()
2520 InspectorFrontendHost.copyText(WebInspector.DOMPresentationUtils.cssPath(this._node, true));
2523 _copyXPath: function()
2525 InspectorFrontendHost.copyText(WebInspector.DOMPresentationUtils.xPath(this._node, true));
2528 _highlightSearchResults: function()
2530 if (!this._searchQuery || !this._searchHighlightsVisible)
2532 if (this._highlightResult) {
2533 this._updateSearchHighlight(true);
2537 var text = this.listItemElement.textContent;
2538 var regexObject = createPlainTextSearchRegex(this._searchQuery, "gi");
2541 var match = regexObject.exec(text);
2542 var matchRanges = [];
2544 matchRanges.push(new WebInspector.SourceRange(match.index, match[0].length));
2545 match = regexObject.exec(text);
2548 // Fall back for XPath, etc. matches.
2549 if (!matchRanges.length)
2550 matchRanges.push(new WebInspector.SourceRange(0, text.length));
2552 this._highlightResult = [];
2553 WebInspector.highlightSearchResults(this.listItemElement, matchRanges, this._highlightResult);
2556 _scrollIntoView: function()
2558 function scrollIntoViewCallback(object)
2561 * @suppressReceiverCheck
2564 function scrollIntoView()
2566 this.scrollIntoViewIfNeeded(true);
2570 object.callFunction(scrollIntoView);
2573 this._node.resolveToObject("", scrollIntoViewCallback);
2577 * @return {!Array.<!WebInspector.DOMNode>}
2579 _visibleShadowRoots: function()
2581 var roots = this._node.shadowRoots();
2582 if (roots.length && !WebInspector.settings.showUAShadowDOM.get()) {
2583 roots = roots.filter(function(root) {
2584 return root.shadowRootType() === WebInspector.DOMNode.ShadowRootTypes.Author;
2591 * @return {!Array.<!WebInspector.DOMNode>} visibleChildren
2593 _visibleChildren: function()
2595 var visibleChildren = this._visibleShadowRoots();
2596 if (this._node.importedDocument())
2597 visibleChildren.push(this._node.importedDocument());
2598 if (this._node.templateContent())
2599 visibleChildren.push(this._node.templateContent());
2600 var pseudoElements = this._node.pseudoElements();
2601 if (pseudoElements[WebInspector.DOMNode.PseudoElementNames.Before])
2602 visibleChildren.push(pseudoElements[WebInspector.DOMNode.PseudoElementNames.Before]);
2603 if (this._node.childNodeCount())
2604 visibleChildren = visibleChildren.concat(this._node.children());
2605 if (pseudoElements[WebInspector.DOMNode.PseudoElementNames.After])
2606 visibleChildren.push(pseudoElements[WebInspector.DOMNode.PseudoElementNames.After]);
2607 return visibleChildren;
2613 _visibleChildCount: function()
2615 var childCount = this._node.childNodeCount() + this._visibleShadowRoots().length;
2616 if (this._node.importedDocument())
2618 if (this._node.templateContent())
2620 for (var pseudoType in this._node.pseudoElements())
2625 _updateHasChildren: function()
2627 this.hasChildren = !this._elementCloseTag && !this._showInlineText() && this._visibleChildCount() > 0;
2630 __proto__: TreeElement.prototype
2635 * @param {!WebInspector.DOMModel} domModel
2636 * @param {!WebInspector.ElementsTreeOutline} treeOutline
2638 WebInspector.ElementsTreeUpdater = function(domModel, treeOutline)
2640 domModel.addEventListener(WebInspector.DOMModel.Events.NodeInserted, this._nodeInserted, this);
2641 domModel.addEventListener(WebInspector.DOMModel.Events.NodeRemoved, this._nodeRemoved, this);
2642 domModel.addEventListener(WebInspector.DOMModel.Events.AttrModified, this._attributesUpdated, this);
2643 domModel.addEventListener(WebInspector.DOMModel.Events.AttrRemoved, this._attributesUpdated, this);
2644 domModel.addEventListener(WebInspector.DOMModel.Events.CharacterDataModified, this._characterDataModified, this);
2645 domModel.addEventListener(WebInspector.DOMModel.Events.DocumentUpdated, this._documentUpdated, this);
2646 domModel.addEventListener(WebInspector.DOMModel.Events.ChildNodeCountUpdated, this._childNodeCountUpdated, this);
2648 this._domModel = domModel;
2649 this._treeOutline = treeOutline;
2650 /** @type {!Set.<!WebInspector.DOMNode>} */
2651 this._recentlyModifiedNodes = new Set();
2652 /** @type {!Set.<!WebInspector.DOMNode>} */
2653 this._recentlyModifiedParentNodes = new Set();
2656 WebInspector.ElementsTreeUpdater.prototype = {
2659 this._domModel.removeEventListener(WebInspector.DOMModel.Events.NodeInserted, this._nodeInserted, this);
2660 this._domModel.removeEventListener(WebInspector.DOMModel.Events.NodeRemoved, this._nodeRemoved, this);
2661 this._domModel.removeEventListener(WebInspector.DOMModel.Events.AttrModified, this._attributesUpdated, this);
2662 this._domModel.removeEventListener(WebInspector.DOMModel.Events.AttrRemoved, this._attributesUpdated, this);
2663 this._domModel.removeEventListener(WebInspector.DOMModel.Events.CharacterDataModified, this._characterDataModified, this);
2664 this._domModel.removeEventListener(WebInspector.DOMModel.Events.DocumentUpdated, this._documentUpdated, this);
2665 this._domModel.removeEventListener(WebInspector.DOMModel.Events.ChildNodeCountUpdated, this._childNodeCountUpdated, this);
2669 * @param {?WebInspector.DOMNode} parentNode
2671 _parentNodeModified: function(parentNode)
2675 this._recentlyModifiedParentNodes.add(parentNode);
2677 var treeElement = this._treeOutline.findTreeElement(parentNode);
2679 var oldHasChildren = treeElement.hasChildren;
2680 var oldShowInlineText = treeElement._showInlineText();
2681 treeElement._updateHasChildren();
2682 if (treeElement.hasChildren !== oldHasChildren || oldShowInlineText || treeElement._showInlineText())
2683 this._nodeModified(parentNode);
2686 if (this._treeOutline._visible)
2687 this._updateModifiedNodesSoon();
2691 * @param {!WebInspector.DOMNode} node
2693 _nodeModified: function(node)
2695 this._recentlyModifiedNodes.add(node);
2696 if (this._treeOutline._visible)
2697 this._updateModifiedNodesSoon();
2701 * @param {!WebInspector.Event} event
2703 _documentUpdated: function(event)
2705 var inspectedRootDocument = event.data;
2709 if (!inspectedRootDocument)
2712 this._treeOutline.rootDOMNode = inspectedRootDocument;
2716 * @param {!WebInspector.Event} event
2718 _attributesUpdated: function(event)
2720 var node = /** @type {!WebInspector.DOMNode} */ (event.data.node);
2721 this._nodeModified(node);
2725 * @param {!WebInspector.Event} event
2727 _characterDataModified: function(event)
2729 var node = /** @type {!WebInspector.DOMNode} */ (event.data);
2730 this._parentNodeModified(node.parentNode);
2731 this._nodeModified(node);
2735 * @param {!WebInspector.Event} event
2737 _nodeInserted: function(event)
2739 var node = /** @type {!WebInspector.DOMNode} */ (event.data);
2740 this._parentNodeModified(node.parentNode);
2744 * @param {!WebInspector.Event} event
2746 _nodeRemoved: function(event)
2748 var node = /** @type {!WebInspector.DOMNode} */ (event.data.node);
2749 var parentNode = /** @type {!WebInspector.DOMNode} */ (event.data.parent);
2750 this._treeOutline._resetClipboardIfNeeded(node);
2751 this._parentNodeModified(parentNode);
2755 * @param {!WebInspector.Event} event
2757 _childNodeCountUpdated: function(event)
2759 var node = /** @type {!WebInspector.DOMNode} */ (event.data);
2760 this._parentNodeModified(node);
2763 _updateModifiedNodesSoon: function()
2765 if (this._updateModifiedNodesTimeout)
2767 this._updateModifiedNodesTimeout = setTimeout(this._updateModifiedNodes.bind(this), 50);
2770 _updateModifiedNodes: function()
2772 if (this._updateModifiedNodesTimeout) {
2773 clearTimeout(this._updateModifiedNodesTimeout);
2774 delete this._updateModifiedNodesTimeout;
2777 var updatedNodes = this._recentlyModifiedNodes.values().concat(this._recentlyModifiedParentNodes.values());
2778 var hidePanelWhileUpdating = updatedNodes.length > 10;
2779 if (hidePanelWhileUpdating) {
2780 var treeOutlineContainerElement = this._treeOutline.element.parentNode;
2781 var originalScrollTop = treeOutlineContainerElement ? treeOutlineContainerElement.scrollTop : 0;
2782 this._treeOutline.element.classList.add("hidden");
2785 if (this._treeOutline._rootDOMNode && this._recentlyModifiedParentNodes.contains(this._treeOutline._rootDOMNode)) {
2786 // Document's children have changed, perform total update.
2787 this._treeOutline.update();
2789 var nodes = this._recentlyModifiedNodes.values();
2790 for (var i = 0, size = nodes.length; i < size; ++i) {
2791 var nodeItem = this._treeOutline.findTreeElement(nodes[i]);
2793 nodeItem.updateTitle();
2796 var parentNodes = this._recentlyModifiedParentNodes.values();
2797 for (var i = 0, size = parentNodes.length; i < size; ++i) {
2798 var parentNodeItem = this._treeOutline.findTreeElement(parentNodes[i]);
2799 if (parentNodeItem && parentNodeItem.populated)
2800 parentNodeItem.updateChildren();
2804 if (hidePanelWhileUpdating) {
2805 this._treeOutline.element.classList.remove("hidden");
2806 if (originalScrollTop)
2807 treeOutlineContainerElement.scrollTop = originalScrollTop;
2808 this._treeOutline.updateSelection();
2810 this._recentlyModifiedNodes.clear();
2811 this._recentlyModifiedParentNodes.clear();
2812 this._treeOutline._fireElementsTreeUpdated(updatedNodes);
2817 this._treeOutline.rootDOMNode = null;
2818 this._treeOutline.selectDOMNode(null, false);
2819 this._domModel.hideDOMNodeHighlight();
2820 this._recentlyModifiedNodes.clear();
2821 this._recentlyModifiedParentNodes.clear();
2822 delete this._treeOutline._clipboardNodeData;
2828 * @implements {WebInspector.Renderer}
2830 WebInspector.ElementsTreeOutline.Renderer = function()
2834 WebInspector.ElementsTreeOutline.Renderer.prototype = {
2836 * @param {!Object} object
2837 * @return {?Element}
2839 render: function(object)
2841 if (!(object instanceof WebInspector.DOMNode))
2843 var node = /** @type {!WebInspector.DOMNode} */ (object);
2844 var treeOutline = new WebInspector.ElementsTreeOutline(node.target(), false, false);
2845 treeOutline.rootDOMNode = node;
2846 treeOutline.element.classList.add("outline-disclosure");
2847 if (!treeOutline.children[0].hasChildren)
2848 treeOutline.element.classList.add("single-node");
2849 treeOutline.setVisible(true);
2850 treeOutline.element.treeElementForTest = treeOutline.children[0];
2851 return treeOutline.element;