summaryrefslogtreecommitdiffstats
path: root/chromium/third_party/WebKit/Source/devtools/front_end/elements/ElementsTreeOutline.js
diff options
context:
space:
mode:
Diffstat (limited to 'chromium/third_party/WebKit/Source/devtools/front_end/elements/ElementsTreeOutline.js')
-rw-r--r--chromium/third_party/WebKit/Source/devtools/front_end/elements/ElementsTreeOutline.js2685
1 files changed, 2685 insertions, 0 deletions
diff --git a/chromium/third_party/WebKit/Source/devtools/front_end/elements/ElementsTreeOutline.js b/chromium/third_party/WebKit/Source/devtools/front_end/elements/ElementsTreeOutline.js
new file mode 100644
index 00000000000..38e66d51700
--- /dev/null
+++ b/chromium/third_party/WebKit/Source/devtools/front_end/elements/ElementsTreeOutline.js
@@ -0,0 +1,2685 @@
+/*
+ * Copyright (C) 2007, 2008 Apple Inc. All rights reserved.
+ * Copyright (C) 2008 Matt Lilek <webkit@mattlilek.com>
+ * Copyright (C) 2009 Joseph Pecoraro
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions
+ * are met:
+ *
+ * 1. Redistributions of source code must retain the above copyright
+ * notice, this list of conditions and the following disclaimer.
+ * 2. Redistributions in binary form must reproduce the above copyright
+ * notice, this list of conditions and the following disclaimer in the
+ * documentation and/or other materials provided with the distribution.
+ * 3. Neither the name of Apple Computer, Inc. ("Apple") nor the names of
+ * its contributors may be used to endorse or promote products derived
+ * from this software without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY APPLE AND ITS CONTRIBUTORS "AS IS" AND ANY
+ * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+ * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL APPLE OR ITS CONTRIBUTORS BE LIABLE FOR ANY
+ * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
+ * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+ * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
+ * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF
+ * THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+/**
+ * @constructor
+ * @extends {TreeOutline}
+ * @param {!WebInspector.Target} target
+ * @param {boolean=} omitRootDOMNode
+ * @param {boolean=} selectEnabled
+ * @param {function(!WebInspector.ContextMenu, !WebInspector.DOMNode)=} contextMenuCallback
+ * @param {function(!WebInspector.DOMNode, string, boolean)=} setPseudoClassCallback
+ */
+WebInspector.ElementsTreeOutline = function(target, omitRootDOMNode, selectEnabled, contextMenuCallback, setPseudoClassCallback)
+{
+ this._target = target;
+ this._domModel = target.domModel;
+ this.element = document.createElement("ol");
+ this.element.className = "elements-tree-outline";
+ this.element.addEventListener("mousedown", this._onmousedown.bind(this), false);
+ this.element.addEventListener("mousemove", this._onmousemove.bind(this), false);
+ this.element.addEventListener("mouseout", this._onmouseout.bind(this), false);
+ this.element.addEventListener("dragstart", this._ondragstart.bind(this), false);
+ this.element.addEventListener("dragover", this._ondragover.bind(this), false);
+ this.element.addEventListener("dragleave", this._ondragleave.bind(this), false);
+ this.element.addEventListener("drop", this._ondrop.bind(this), false);
+ this.element.addEventListener("dragend", this._ondragend.bind(this), false);
+ this.element.addEventListener("keydown", this._onkeydown.bind(this), false);
+
+ TreeOutline.call(this, this.element);
+
+ this._includeRootDOMNode = !omitRootDOMNode;
+ this._selectEnabled = selectEnabled;
+ /** @type {?WebInspector.DOMNode} */
+ this._rootDOMNode = null;
+ /** @type {?WebInspector.DOMNode} */
+ this._selectedDOMNode = null;
+ this._eventSupport = new WebInspector.Object();
+
+ this._visible = false;
+
+ this.element.addEventListener("contextmenu", this._contextMenuEventFired.bind(this), true);
+ this._contextMenuCallback = contextMenuCallback;
+ this._setPseudoClassCallback = setPseudoClassCallback;
+ this._createNodeDecorators();
+}
+
+/**
+ * @enum {string}
+ */
+WebInspector.ElementsTreeOutline.Events = {
+ SelectedNodeChanged: "SelectedNodeChanged",
+ ElementsTreeUpdated: "ElementsTreeUpdated"
+}
+
+/**
+ * @const
+ * @type {!Object.<string, string>}
+ */
+WebInspector.ElementsTreeOutline.MappedCharToEntity = {
+ "\u00a0": "nbsp",
+ "\u2002": "ensp",
+ "\u2003": "emsp",
+ "\u2009": "thinsp",
+ "\u200a": "#8202", // Hairspace
+ "\u200b": "#8203", // ZWSP
+ "\u200c": "zwnj",
+ "\u200d": "zwj",
+ "\u200e": "lrm",
+ "\u200f": "rlm",
+ "\u202a": "#8234", // LRE
+ "\u202b": "#8235", // RLE
+ "\u202c": "#8236", // PDF
+ "\u202d": "#8237", // LRO
+ "\u202e": "#8238" // RLO
+}
+
+WebInspector.ElementsTreeOutline.prototype = {
+ /**
+ * @return {!WebInspector.Target}
+ */
+ target: function()
+ {
+ return this._target;
+ },
+
+ /**
+ * @return {!WebInspector.DOMModel}
+ */
+ domModel: function()
+ {
+ return this._domModel;
+ },
+
+ /**
+ * @param {number} width
+ */
+ setVisibleWidth: function(width)
+ {
+ this._visibleWidth = width;
+ if (this._multilineEditing)
+ this._multilineEditing.setWidth(this._visibleWidth);
+ },
+
+ _createNodeDecorators: function()
+ {
+ this._nodeDecorators = [];
+ this._nodeDecorators.push(new WebInspector.ElementsTreeOutline.PseudoStateDecorator());
+ },
+
+ wireToDOMModel: function()
+ {
+ this._elementsTreeUpdater = new WebInspector.ElementsTreeUpdater(this._target.domModel, this);
+ },
+
+ unwireFromDOMModel: function()
+ {
+ if (this._elementsTreeUpdater)
+ this._elementsTreeUpdater.dispose();
+ },
+ /**
+ * @param {boolean} visible
+ */
+ setVisible: function(visible)
+ {
+ this._visible = visible;
+ if (!this._visible)
+ return;
+
+ this._updateModifiedNodes();
+ if (this._selectedDOMNode)
+ this._revealAndSelectNode(this._selectedDOMNode, false);
+ },
+
+ addEventListener: function(eventType, listener, thisObject)
+ {
+ this._eventSupport.addEventListener(eventType, listener, thisObject);
+ },
+
+ removeEventListener: function(eventType, listener, thisObject)
+ {
+ this._eventSupport.removeEventListener(eventType, listener, thisObject);
+ },
+
+ get rootDOMNode()
+ {
+ return this._rootDOMNode;
+ },
+
+ set rootDOMNode(x)
+ {
+ if (this._rootDOMNode === x)
+ return;
+
+ this._rootDOMNode = x;
+
+ this._isXMLMimeType = x && x.isXMLNode();
+
+ this.update();
+ },
+
+ get isXMLMimeType()
+ {
+ return this._isXMLMimeType;
+ },
+
+ /**
+ * @return {?WebInspector.DOMNode}
+ */
+ selectedDOMNode: function()
+ {
+ return this._selectedDOMNode;
+ },
+
+ /**
+ * @param {?WebInspector.DOMNode} node
+ * @param {boolean=} focus
+ */
+ selectDOMNode: function(node, focus)
+ {
+ if (this._selectedDOMNode === node) {
+ this._revealAndSelectNode(node, !focus);
+ return;
+ }
+
+ this._selectedDOMNode = node;
+ this._revealAndSelectNode(node, !focus);
+
+ // The _revealAndSelectNode() method might find a different element if there is inlined text,
+ // and the select() call would change the selectedDOMNode and reenter this setter. So to
+ // avoid calling _selectedNodeChanged() twice, first check if _selectedDOMNode is the same
+ // node as the one passed in.
+ if (this._selectedDOMNode === node)
+ this._selectedNodeChanged();
+ },
+
+ /**
+ * @return {boolean}
+ */
+ editing: function()
+ {
+ var node = this.selectedDOMNode();
+ if (!node)
+ return false;
+ var treeElement = this.findTreeElement(node);
+ if (!treeElement)
+ return false;
+ return treeElement._editing || false;
+ },
+
+ update: function()
+ {
+ var selectedNode = this.selectedTreeElement ? this.selectedTreeElement._node : null;
+
+ this.removeChildren();
+
+ if (!this.rootDOMNode)
+ return;
+
+ var treeElement;
+ if (this._includeRootDOMNode) {
+ treeElement = new WebInspector.ElementsTreeElement(this.rootDOMNode);
+ treeElement.selectable = this._selectEnabled;
+ this.appendChild(treeElement);
+ } else {
+ // FIXME: this could use findTreeElement to reuse a tree element if it already exists
+ var node = this.rootDOMNode.firstChild;
+ while (node) {
+ treeElement = new WebInspector.ElementsTreeElement(node);
+ treeElement.selectable = this._selectEnabled;
+ this.appendChild(treeElement);
+ node = node.nextSibling;
+ }
+ }
+
+ if (selectedNode)
+ this._revealAndSelectNode(selectedNode, true);
+ },
+
+ updateSelection: function()
+ {
+ if (!this.selectedTreeElement)
+ return;
+ var element = this.treeOutline.selectedTreeElement;
+ element.updateSelection();
+ },
+
+ /**
+ * @param {!WebInspector.DOMNode} node
+ */
+ updateOpenCloseTags: function(node)
+ {
+ var treeElement = this.findTreeElement(node);
+ if (treeElement)
+ treeElement.updateTitle();
+ var children = treeElement.children;
+ var closingTagElement = children[children.length - 1];
+ if (closingTagElement && closingTagElement._elementCloseTag)
+ closingTagElement.updateTitle();
+ },
+
+ _selectedNodeChanged: function()
+ {
+ this._eventSupport.dispatchEventToListeners(WebInspector.ElementsTreeOutline.Events.SelectedNodeChanged, this._selectedDOMNode);
+ },
+
+ /**
+ * @param {!Array.<!WebInspector.DOMNode>} nodes
+ */
+ _fireElementsTreeUpdated: function(nodes)
+ {
+ this._eventSupport.dispatchEventToListeners(WebInspector.ElementsTreeOutline.Events.ElementsTreeUpdated, nodes);
+ },
+
+ /**
+ * @param {!WebInspector.DOMNode} node
+ * @return {?TreeElement}
+ */
+ findTreeElement: function(node)
+ {
+ function parentNode(node)
+ {
+ return node.parentNode;
+ }
+
+ var treeElement = TreeOutline.prototype.findTreeElement.call(this, node, parentNode);
+ if (!treeElement && node.nodeType() === Node.TEXT_NODE) {
+ // The text node might have been inlined if it was short, so try to find the parent element.
+ treeElement = TreeOutline.prototype.findTreeElement.call(this, node.parentNode, parentNode);
+ }
+
+ return treeElement;
+ },
+
+ /**
+ * @param {!WebInspector.DOMNode} node
+ * @return {?TreeElement}
+ */
+ createTreeElementFor: function(node)
+ {
+ var treeElement = this.findTreeElement(node);
+ if (treeElement)
+ return treeElement;
+ if (!node.parentNode)
+ return null;
+
+ treeElement = this.createTreeElementFor(node.parentNode);
+ return treeElement ? treeElement._showChild(node) : null;
+ },
+
+ set suppressRevealAndSelect(x)
+ {
+ if (this._suppressRevealAndSelect === x)
+ return;
+ this._suppressRevealAndSelect = x;
+ },
+
+ /**
+ * @param {?WebInspector.DOMNode} node
+ * @param {boolean} omitFocus
+ */
+ _revealAndSelectNode: function(node, omitFocus)
+ {
+ if (this._suppressRevealAndSelect)
+ return;
+
+ if (!this._includeRootDOMNode && node === this.rootDOMNode && this.rootDOMNode)
+ node = this.rootDOMNode.firstChild;
+ if (!node)
+ return;
+ var treeElement = this.createTreeElementFor(node);
+ if (!treeElement)
+ return;
+
+ treeElement.revealAndSelect(omitFocus);
+ },
+
+ /**
+ * @return {?TreeElement}
+ */
+ _treeElementFromEvent: function(event)
+ {
+ var scrollContainer = this.element.parentElement;
+
+ // We choose this X coordinate based on the knowledge that our list
+ // items extend at least to the right edge of the outer <ol> container.
+ // In the no-word-wrap mode the outer <ol> may be wider than the tree container
+ // (and partially hidden), in which case we are left to use only its right boundary.
+ var x = scrollContainer.totalOffsetLeft() + scrollContainer.offsetWidth - 36;
+
+ var y = event.pageY;
+
+ // Our list items have 1-pixel cracks between them vertically. We avoid
+ // the cracks by checking slightly above and slightly below the mouse
+ // and seeing if we hit the same element each time.
+ var elementUnderMouse = this.treeElementFromPoint(x, y);
+ var elementAboveMouse = this.treeElementFromPoint(x, y - 2);
+ var element;
+ if (elementUnderMouse === elementAboveMouse)
+ element = elementUnderMouse;
+ else
+ element = this.treeElementFromPoint(x, y + 2);
+
+ return element;
+ },
+
+ _onmousedown: function(event)
+ {
+ var element = this._treeElementFromEvent(event);
+
+ if (!element || element.isEventWithinDisclosureTriangle(event))
+ return;
+
+ element.select();
+ },
+
+ _onmousemove: function(event)
+ {
+ var element = this._treeElementFromEvent(event);
+ if (element && this._previousHoveredElement === element)
+ return;
+
+ if (this._previousHoveredElement) {
+ this._previousHoveredElement.hovered = false;
+ delete this._previousHoveredElement;
+ }
+
+ if (element) {
+ element.hovered = true;
+ this._previousHoveredElement = element;
+ }
+
+ if (element && element._node)
+ this._domModel.highlightDOMNodeWithConfig(element._node.id, { mode: "all", showInfo: !WebInspector.KeyboardShortcut.eventHasCtrlOrMeta(event) });
+ else
+ this._domModel.hideDOMNodeHighlight();
+ },
+
+ _onmouseout: function(event)
+ {
+ var nodeUnderMouse = document.elementFromPoint(event.pageX, event.pageY);
+ if (nodeUnderMouse && nodeUnderMouse.isDescendant(this.element))
+ return;
+
+ if (this._previousHoveredElement) {
+ this._previousHoveredElement.hovered = false;
+ delete this._previousHoveredElement;
+ }
+
+ this._domModel.hideDOMNodeHighlight();
+ },
+
+ _ondragstart: function(event)
+ {
+ if (!window.getSelection().isCollapsed)
+ return false;
+ if (event.target.nodeName === "A")
+ return false;
+
+ var treeElement = this._treeElementFromEvent(event);
+ if (!treeElement)
+ return false;
+
+ if (!this._isValidDragSourceOrTarget(treeElement))
+ return false;
+
+ if (treeElement._node.nodeName() === "BODY" || treeElement._node.nodeName() === "HEAD")
+ return false;
+
+ event.dataTransfer.setData("text/plain", treeElement.listItemElement.textContent);
+ event.dataTransfer.effectAllowed = "copyMove";
+ this._treeElementBeingDragged = treeElement;
+
+ this._domModel.hideDOMNodeHighlight();
+
+ return true;
+ },
+
+ _ondragover: function(event)
+ {
+ if (!this._treeElementBeingDragged)
+ return false;
+
+ var treeElement = this._treeElementFromEvent(event);
+ if (!this._isValidDragSourceOrTarget(treeElement))
+ return false;
+
+ var node = treeElement._node;
+ while (node) {
+ if (node === this._treeElementBeingDragged._node)
+ return false;
+ node = node.parentNode;
+ }
+
+ treeElement.updateSelection();
+ treeElement.listItemElement.classList.add("elements-drag-over");
+ this._dragOverTreeElement = treeElement;
+ event.preventDefault();
+ event.dataTransfer.dropEffect = 'move';
+ return false;
+ },
+
+ _ondragleave: function(event)
+ {
+ this._clearDragOverTreeElementMarker();
+ event.preventDefault();
+ return false;
+ },
+
+ /**
+ * @param {?TreeElement} treeElement
+ * @return {boolean}
+ */
+ _isValidDragSourceOrTarget: function(treeElement)
+ {
+ if (!treeElement)
+ return false;
+
+ var node = treeElement.representedObject;
+ if (!(node instanceof WebInspector.DOMNode))
+ return false;
+
+ if (!node.parentNode || node.parentNode.nodeType() !== Node.ELEMENT_NODE)
+ return false;
+
+ return true;
+ },
+
+ _ondrop: function(event)
+ {
+ event.preventDefault();
+ var treeElement = this._treeElementFromEvent(event);
+ if (treeElement)
+ this._doMove(treeElement);
+ },
+
+ /**
+ * @param {!TreeElement} treeElement
+ */
+ _doMove: function(treeElement)
+ {
+ if (!this._treeElementBeingDragged)
+ return;
+
+ var parentNode;
+ var anchorNode;
+
+ if (treeElement._elementCloseTag) {
+ // Drop onto closing tag -> insert as last child.
+ parentNode = treeElement._node;
+ } else {
+ var dragTargetNode = treeElement._node;
+ parentNode = dragTargetNode.parentNode;
+ anchorNode = dragTargetNode;
+ }
+
+ var wasExpanded = this._treeElementBeingDragged.expanded;
+ this._treeElementBeingDragged._node.moveTo(parentNode, anchorNode, this._selectNodeAfterEdit.bind(this, wasExpanded));
+
+ delete this._treeElementBeingDragged;
+ },
+
+ _ondragend: function(event)
+ {
+ event.preventDefault();
+ this._clearDragOverTreeElementMarker();
+ delete this._treeElementBeingDragged;
+ },
+
+ _clearDragOverTreeElementMarker: function()
+ {
+ if (this._dragOverTreeElement) {
+ this._dragOverTreeElement.updateSelection();
+ this._dragOverTreeElement.listItemElement.classList.remove("elements-drag-over");
+ delete this._dragOverTreeElement;
+ }
+ },
+
+ /**
+ * @param {?Event} event
+ */
+ _onkeydown: function(event)
+ {
+ var keyboardEvent = /** @type {!KeyboardEvent} */ (event);
+ var node = /** @type {!WebInspector.DOMNode} */ (this.selectedDOMNode());
+ console.assert(node);
+ var treeElement = this.getCachedTreeElement(node);
+ if (!treeElement)
+ return;
+
+ if (!treeElement._editing && WebInspector.KeyboardShortcut.hasNoModifiers(keyboardEvent) && keyboardEvent.keyCode === WebInspector.KeyboardShortcut.Keys.H.code) {
+ this._toggleHideShortcut(node);
+ event.consume(true);
+ return;
+ }
+ },
+
+ _contextMenuEventFired: function(event)
+ {
+ var treeElement = this._treeElementFromEvent(event);
+ if (!treeElement)
+ return;
+
+ var contextMenu = new WebInspector.ContextMenu(event);
+ contextMenu.appendApplicableItems(treeElement._node);
+ contextMenu.show();
+ },
+
+ populateContextMenu: function(contextMenu, event)
+ {
+ var treeElement = this._treeElementFromEvent(event);
+ if (!treeElement)
+ return;
+
+ var isPseudoElement = !!treeElement._node.pseudoType();
+ var isTag = treeElement._node.nodeType() === Node.ELEMENT_NODE && !isPseudoElement;
+ var textNode = event.target.enclosingNodeOrSelfWithClass("webkit-html-text-node");
+ if (textNode && textNode.classList.contains("bogus"))
+ textNode = null;
+ var commentNode = event.target.enclosingNodeOrSelfWithClass("webkit-html-comment");
+ contextMenu.appendApplicableItems(event.target);
+ if (textNode) {
+ contextMenu.appendSeparator();
+ treeElement._populateTextContextMenu(contextMenu, textNode);
+ } else if (isTag) {
+ contextMenu.appendSeparator();
+ treeElement._populateTagContextMenu(contextMenu, event);
+ } else if (commentNode) {
+ contextMenu.appendSeparator();
+ treeElement._populateNodeContextMenu(contextMenu, textNode);
+ } else if (isPseudoElement) {
+ treeElement._populateScrollIntoView(contextMenu);
+ } else if (treeElement._node.isShadowRoot()) {
+ this.treeOutline._populateContextMenu(contextMenu, treeElement._node);
+ }
+ },
+
+ _updateModifiedNodes: function()
+ {
+ if (this._elementsTreeUpdater)
+ this._elementsTreeUpdater._updateModifiedNodes();
+ },
+
+ _populateContextMenu: function(contextMenu, node)
+ {
+ if (this._contextMenuCallback)
+ this._contextMenuCallback(contextMenu, node);
+ },
+
+ handleShortcut: function(event)
+ {
+ var node = this.selectedDOMNode();
+ var treeElement = this.getCachedTreeElement(node);
+ if (!node || !treeElement)
+ return;
+
+ if (event.keyIdentifier === "F2" && treeElement.hasEditableNode()) {
+ this._toggleEditAsHTML(node);
+ event.handled = true;
+ return;
+ }
+
+ if (WebInspector.KeyboardShortcut.eventHasCtrlOrMeta(event) && node.parentNode) {
+ if (event.keyIdentifier === "Up" && node.previousSibling) {
+ node.moveTo(node.parentNode, node.previousSibling, this._selectNodeAfterEdit.bind(this, treeElement.expanded));
+ event.handled = true;
+ return;
+ }
+ if (event.keyIdentifier === "Down" && node.nextSibling) {
+ node.moveTo(node.parentNode, node.nextSibling.nextSibling, this._selectNodeAfterEdit.bind(this, treeElement.expanded));
+ event.handled = true;
+ return;
+ }
+ }
+ },
+
+ /**
+ * @param {!WebInspector.DOMNode} node
+ */
+ _toggleEditAsHTML: function(node)
+ {
+ var treeElement = this.getCachedTreeElement(node);
+ if (!treeElement)
+ return;
+
+ if (treeElement._editing && treeElement._htmlEditElement && WebInspector.isBeingEdited(treeElement._htmlEditElement))
+ treeElement._editing.commit();
+ else
+ treeElement._editAsHTML();
+ },
+
+ /**
+ * @param {boolean} wasExpanded
+ * @param {?Protocol.Error} error
+ * @param {!DOMAgent.NodeId=} nodeId
+ */
+ _selectNodeAfterEdit: function(wasExpanded, error, nodeId)
+ {
+ if (error)
+ return;
+
+ // Select it and expand if necessary. We force tree update so that it processes dom events and is up to date.
+ this._updateModifiedNodes();
+
+ var newNode = nodeId ? this._domModel.nodeForId(nodeId) : null;
+ if (!newNode)
+ return;
+
+ this.selectDOMNode(newNode, true);
+
+ var newTreeItem = this.findTreeElement(newNode);
+ if (wasExpanded) {
+ if (newTreeItem)
+ newTreeItem.expand();
+ }
+ return newTreeItem;
+ },
+
+ /**
+ * Runs a script on the node's remote object that toggles a class name on
+ * the node and injects a stylesheet into the head of the node's document
+ * containing a rule to set "visibility: hidden" on the class and all it's
+ * ancestors.
+ *
+ * @param {!WebInspector.DOMNode} node
+ * @param {function(?WebInspector.RemoteObject, boolean=)=} userCallback
+ */
+ _toggleHideShortcut: function(node, userCallback)
+ {
+ var pseudoType = node.pseudoType();
+ var effectiveNode = pseudoType ? node.parentNode : node;
+ if (!effectiveNode)
+ return;
+
+ function resolvedNode(object)
+ {
+ if (!object)
+ return;
+
+ /**
+ * @param {?string} pseudoType
+ * @suppressReceiverCheck
+ * @this {!Element}
+ */
+ function toggleClassAndInjectStyleRule(pseudoType)
+ {
+ const classNamePrefix = "__web-inspector-hide";
+ const classNameSuffix = "-shortcut__";
+ const styleTagId = "__web-inspector-hide-shortcut-style__";
+ 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; }";
+
+ var className = classNamePrefix + (pseudoType || "") + classNameSuffix;
+ this.classList.toggle(className);
+
+ var style = document.head.querySelector("style#" + styleTagId);
+ if (style)
+ return;
+
+ style = document.createElement("style");
+ style.id = styleTagId;
+ style.type = "text/css";
+ style.textContent = styleRules;
+ document.head.appendChild(style);
+ }
+
+ object.callFunction(toggleClassAndInjectStyleRule, [{ value: pseudoType }], userCallback);
+ object.release();
+ }
+
+ effectiveNode.resolveToObject("", resolvedNode);
+ },
+
+ __proto__: TreeOutline.prototype
+}
+
+/**
+ * @interface
+ */
+WebInspector.ElementsTreeOutline.ElementDecorator = function()
+{
+}
+
+WebInspector.ElementsTreeOutline.ElementDecorator.prototype = {
+ /**
+ * @param {!WebInspector.DOMNode} node
+ * @return {?string}
+ */
+ decorate: function(node)
+ {
+ },
+
+ /**
+ * @param {!WebInspector.DOMNode} node
+ * @return {?string}
+ */
+ decorateAncestor: function(node)
+ {
+ }
+}
+
+/**
+ * @constructor
+ * @implements {WebInspector.ElementsTreeOutline.ElementDecorator}
+ */
+WebInspector.ElementsTreeOutline.PseudoStateDecorator = function()
+{
+ WebInspector.ElementsTreeOutline.ElementDecorator.call(this);
+}
+
+WebInspector.ElementsTreeOutline.PseudoStateDecorator.prototype = {
+ /**
+ * @param {!WebInspector.DOMNode} node
+ * @return {?string}
+ */
+ decorate: function(node)
+ {
+ if (node.nodeType() !== Node.ELEMENT_NODE)
+ return null;
+ var propertyValue = node.getUserProperty(WebInspector.CSSStyleModel.PseudoStatePropertyName);
+ if (!propertyValue)
+ return null;
+ return WebInspector.UIString("Element state: %s", ":" + propertyValue.join(", :"));
+ },
+
+ /**
+ * @param {!WebInspector.DOMNode} node
+ * @return {?string}
+ */
+ decorateAncestor: function(node)
+ {
+ if (node.nodeType() !== Node.ELEMENT_NODE)
+ return null;
+
+ var descendantCount = node.descendantUserPropertyCount(WebInspector.CSSStyleModel.PseudoStatePropertyName);
+ if (!descendantCount)
+ return null;
+ if (descendantCount === 1)
+ return WebInspector.UIString("%d descendant with forced state", descendantCount);
+ return WebInspector.UIString("%d descendants with forced state", descendantCount);
+ }
+}
+
+/**
+ * @constructor
+ * @extends {TreeElement}
+ * @param {!WebInspector.DOMNode} node
+ * @param {boolean=} elementCloseTag
+ */
+WebInspector.ElementsTreeElement = function(node, elementCloseTag)
+{
+ // The title will be updated in onattach.
+ TreeElement.call(this, "", node);
+ this._node = node;
+
+ this._elementCloseTag = elementCloseTag;
+ this._updateHasChildren();
+
+ if (this._node.nodeType() == Node.ELEMENT_NODE && !elementCloseTag)
+ this._canAddAttributes = true;
+ this._searchQuery = null;
+ this._expandedChildrenLimit = WebInspector.ElementsTreeElement.InitialChildrenLimit;
+}
+
+WebInspector.ElementsTreeElement.InitialChildrenLimit = 500;
+
+// A union of HTML4 and HTML5-Draft elements that explicitly
+// or implicitly (for HTML5) forbid the closing tag.
+// FIXME: Revise once HTML5 Final is published.
+WebInspector.ElementsTreeElement.ForbiddenClosingTagElements = [
+ "area", "base", "basefont", "br", "canvas", "col", "command", "embed", "frame",
+ "hr", "img", "input", "keygen", "link", "meta", "param", "source"
+].keySet();
+
+// These tags we do not allow editing their tag name.
+WebInspector.ElementsTreeElement.EditTagBlacklist = [
+ "html", "head", "body"
+].keySet();
+
+WebInspector.ElementsTreeElement.prototype = {
+ highlightSearchResults: function(searchQuery)
+ {
+ if (this._searchQuery !== searchQuery) {
+ this._updateSearchHighlight(false);
+ delete this._highlightResult; // A new search query.
+ }
+
+ this._searchQuery = searchQuery;
+ this._searchHighlightsVisible = true;
+ this.updateTitle(true);
+ },
+
+ hideSearchHighlights: function()
+ {
+ delete this._searchHighlightsVisible;
+ this._updateSearchHighlight(false);
+ },
+
+ _updateSearchHighlight: function(show)
+ {
+ if (!this._highlightResult)
+ return;
+
+ function updateEntryShow(entry)
+ {
+ switch (entry.type) {
+ case "added":
+ entry.parent.insertBefore(entry.node, entry.nextSibling);
+ break;
+ case "changed":
+ entry.node.textContent = entry.newText;
+ break;
+ }
+ }
+
+ function updateEntryHide(entry)
+ {
+ switch (entry.type) {
+ case "added":
+ entry.node.remove();
+ break;
+ case "changed":
+ entry.node.textContent = entry.oldText;
+ break;
+ }
+ }
+
+ // Preserve the semantic of node by following the order of updates for hide and show.
+ if (show) {
+ for (var i = 0, size = this._highlightResult.length; i < size; ++i)
+ updateEntryShow(this._highlightResult[i]);
+ } else {
+ for (var i = (this._highlightResult.length - 1); i >= 0; --i)
+ updateEntryHide(this._highlightResult[i]);
+ }
+ },
+
+ get hovered()
+ {
+ return this._hovered;
+ },
+
+ set hovered(x)
+ {
+ if (this._hovered === x)
+ return;
+
+ this._hovered = x;
+
+ if (this.listItemElement) {
+ if (x) {
+ this.updateSelection();
+ this.listItemElement.classList.add("hovered");
+ } else {
+ this.listItemElement.classList.remove("hovered");
+ }
+ }
+ },
+
+ get expandedChildrenLimit()
+ {
+ return this._expandedChildrenLimit;
+ },
+
+ set expandedChildrenLimit(x)
+ {
+ if (this._expandedChildrenLimit === x)
+ return;
+
+ this._expandedChildrenLimit = x;
+ if (this.treeOutline && !this._updateChildrenInProgress)
+ this._updateChildren(true);
+ },
+
+ get expandedChildCount()
+ {
+ var count = this.children.length;
+ if (count && this.children[count - 1]._elementCloseTag)
+ count--;
+ if (count && this.children[count - 1].expandAllButton)
+ count--;
+ return count;
+ },
+
+ /**
+ * @param {!WebInspector.DOMNode} child
+ * @return {?WebInspector.ElementsTreeElement}
+ */
+ _showChild: function(child)
+ {
+ if (this._elementCloseTag)
+ return null;
+
+ var index = this._visibleChildren().indexOf(child);
+ if (index === -1)
+ return null;
+
+ if (index >= this.expandedChildrenLimit) {
+ this._expandedChildrenLimit = index + 1;
+ this._updateChildren(true);
+ }
+
+ // Whether index-th child is visible in the children tree
+ return this.expandedChildCount > index ? this.children[index] : null;
+ },
+
+ updateSelection: function()
+ {
+ var listItemElement = this.listItemElement;
+ if (!listItemElement)
+ return;
+
+ if (!this._readyToUpdateSelection) {
+ if (document.body.offsetWidth > 0)
+ this._readyToUpdateSelection = true;
+ else {
+ // The stylesheet hasn't loaded yet or the window is closed,
+ // so we can't calculate what we need. Return early.
+ return;
+ }
+ }
+
+ if (!this.selectionElement) {
+ this.selectionElement = document.createElement("div");
+ this.selectionElement.className = "selection selected";
+ listItemElement.insertBefore(this.selectionElement, listItemElement.firstChild);
+ }
+
+ this.selectionElement.style.height = listItemElement.offsetHeight + "px";
+ },
+
+ onattach: function()
+ {
+ if (this._hovered) {
+ this.updateSelection();
+ this.listItemElement.classList.add("hovered");
+ }
+
+ this.updateTitle();
+ this._preventFollowingLinksOnDoubleClick();
+ this.listItemElement.draggable = true;
+ },
+
+ _preventFollowingLinksOnDoubleClick: function()
+ {
+ 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");
+ if (!links)
+ return;
+
+ for (var i = 0; i < links.length; ++i)
+ links[i].preventFollowOnDoubleClick = true;
+ },
+
+ onpopulate: function()
+ {
+ if (this.children.length || this._showInlineText() || this._elementCloseTag)
+ return;
+
+ this.updateChildren();
+ },
+
+ /**
+ * @param {boolean=} fullRefresh
+ */
+ updateChildren: function(fullRefresh)
+ {
+ if (this._elementCloseTag)
+ return;
+ this._node.getChildNodes(this._updateChildren.bind(this, fullRefresh));
+ },
+
+ /**
+ * @param {!WebInspector.DOMNode} child
+ * @param {number} index
+ * @param {boolean=} closingTag
+ * @return {!WebInspector.ElementsTreeElement}
+ */
+ insertChildElement: function(child, index, closingTag)
+ {
+ var newElement = new WebInspector.ElementsTreeElement(child, closingTag);
+ newElement.selectable = this.treeOutline._selectEnabled;
+ this.insertChild(newElement, index);
+ return newElement;
+ },
+
+ moveChild: function(child, targetIndex)
+ {
+ var wasSelected = child.selected;
+ this.removeChild(child);
+ this.insertChild(child, targetIndex);
+ if (wasSelected)
+ child.select();
+ },
+
+ /**
+ * @param {boolean=} fullRefresh
+ */
+ _updateChildren: function(fullRefresh)
+ {
+ if (this._updateChildrenInProgress || !this.treeOutline._visible)
+ return;
+
+ this._updateChildrenInProgress = true;
+ var selectedNode = this.treeOutline.selectedDOMNode();
+ var originalScrollTop = 0;
+ if (fullRefresh) {
+ var treeOutlineContainerElement = this.treeOutline.element.parentNode;
+ originalScrollTop = treeOutlineContainerElement.scrollTop;
+ var selectedTreeElement = this.treeOutline.selectedTreeElement;
+ if (selectedTreeElement && selectedTreeElement.hasAncestor(this))
+ this.select();
+ this.removeChildren();
+ }
+
+ /**
+ * @this {WebInspector.ElementsTreeElement}
+ * @return {?WebInspector.ElementsTreeElement}
+ */
+ function updateChildrenOfNode()
+ {
+ var treeOutline = this.treeOutline;
+ var visibleChildren = this._visibleChildren();
+ var treeChildIndex = 0;
+ var elementToSelect = null;
+
+ for (var i = 0; i < visibleChildren.length; ++i) {
+ var child = visibleChildren[i];
+ var currentTreeElement = this.children[treeChildIndex];
+ if (!currentTreeElement || currentTreeElement._node !== child) {
+ // Find any existing element that is later in the children list.
+ var existingTreeElement = null;
+ for (var j = (treeChildIndex + 1), size = this.expandedChildCount; j < size; ++j) {
+ if (this.children[j]._node === child) {
+ existingTreeElement = this.children[j];
+ break;
+ }
+ }
+
+ if (existingTreeElement && existingTreeElement.parent === this) {
+ // If an existing element was found and it has the same parent, just move it.
+ this.moveChild(existingTreeElement, treeChildIndex);
+ } else {
+ // No existing element found, insert a new element.
+ if (treeChildIndex < this.expandedChildrenLimit) {
+ var newElement = this.insertChildElement(child, treeChildIndex);
+ if (child === selectedNode)
+ elementToSelect = newElement;
+ if (this.expandedChildCount > this.expandedChildrenLimit)
+ this.expandedChildrenLimit++;
+ }
+ }
+ }
+
+ ++treeChildIndex;
+ }
+ return elementToSelect;
+ }
+
+ // Remove any tree elements that no longer have this node (or this node's contentDocument) as their parent.
+ for (var i = (this.children.length - 1); i >= 0; --i) {
+ var currentChild = this.children[i];
+ var currentNode = currentChild._node;
+ if (!currentNode)
+ continue;
+ var currentParentNode = currentNode.parentNode;
+
+ if (currentParentNode === this._node)
+ continue;
+
+ var selectedTreeElement = this.treeOutline.selectedTreeElement;
+ if (selectedTreeElement && (selectedTreeElement === currentChild || selectedTreeElement.hasAncestor(currentChild)))
+ this.select();
+
+ this.removeChildAtIndex(i);
+ }
+
+ var elementToSelect = updateChildrenOfNode.call(this);
+ this.updateTitle();
+ this._adjustCollapsedRange();
+
+ var lastChild = this.children[this.children.length - 1];
+ if (this._node.nodeType() == Node.ELEMENT_NODE && (!lastChild || !lastChild._elementCloseTag))
+ this.insertChildElement(this._node, this.children.length, true);
+
+ // We want to restore the original selection and tree scroll position after a full refresh, if possible.
+ if (fullRefresh && elementToSelect) {
+ elementToSelect.select();
+ if (treeOutlineContainerElement && originalScrollTop <= treeOutlineContainerElement.scrollHeight)
+ treeOutlineContainerElement.scrollTop = originalScrollTop;
+ }
+
+ delete this._updateChildrenInProgress;
+ },
+
+ _adjustCollapsedRange: function()
+ {
+ var visibleChildren = this._visibleChildren();
+ // Ensure precondition: only the tree elements for node children are found in the tree
+ // (not the Expand All button or the closing tag).
+ if (this.expandAllButtonElement && this.expandAllButtonElement.__treeElement.parent)
+ this.removeChild(this.expandAllButtonElement.__treeElement);
+
+ const childNodeCount = visibleChildren.length;
+
+ // In case some nodes from the expanded range were removed, pull some nodes from the collapsed range into the expanded range at the bottom.
+ for (var i = this.expandedChildCount, limit = Math.min(this.expandedChildrenLimit, childNodeCount); i < limit; ++i)
+ this.insertChildElement(visibleChildren[i], i);
+
+ const expandedChildCount = this.expandedChildCount;
+ if (childNodeCount > this.expandedChildCount) {
+ var targetButtonIndex = expandedChildCount;
+ if (!this.expandAllButtonElement) {
+ var button = document.createElement("button");
+ button.className = "show-all-nodes";
+ button.value = "";
+ var item = new TreeElement(button, null, false);
+ item.selectable = false;
+ item.expandAllButton = true;
+ this.insertChild(item, targetButtonIndex);
+ this.expandAllButtonElement = item.listItemElement.firstChild;
+ this.expandAllButtonElement.__treeElement = item;
+ this.expandAllButtonElement.addEventListener("click", this.handleLoadAllChildren.bind(this), false);
+ } else if (!this.expandAllButtonElement.__treeElement.parent)
+ this.insertChild(this.expandAllButtonElement.__treeElement, targetButtonIndex);
+ this.expandAllButtonElement.textContent = WebInspector.UIString("Show All Nodes (%d More)", childNodeCount - expandedChildCount);
+ } else if (this.expandAllButtonElement)
+ delete this.expandAllButtonElement;
+ },
+
+ handleLoadAllChildren: function()
+ {
+ this.expandedChildrenLimit = Math.max(this._visibleChildCount(), this.expandedChildrenLimit + WebInspector.ElementsTreeElement.InitialChildrenLimit);
+ },
+
+ expandRecursively: function()
+ {
+ /**
+ * @this {WebInspector.ElementsTreeElement}
+ */
+ function callback()
+ {
+ TreeElement.prototype.expandRecursively.call(this, Number.MAX_VALUE);
+ }
+
+ this._node.getSubtree(-1, callback.bind(this));
+ },
+
+ /**
+ * @override
+ */
+ onexpand: function()
+ {
+ if (this._elementCloseTag)
+ return;
+
+ this.updateTitle();
+ this.treeOutline.updateSelection();
+ },
+
+ oncollapse: function()
+ {
+ if (this._elementCloseTag)
+ return;
+
+ this.updateTitle();
+ this.treeOutline.updateSelection();
+ },
+
+ /**
+ * @override
+ */
+ onreveal: function()
+ {
+ if (this.listItemElement) {
+ var tagSpans = this.listItemElement.getElementsByClassName("webkit-html-tag-name");
+ if (tagSpans.length)
+ tagSpans[0].scrollIntoViewIfNeeded(true);
+ else
+ this.listItemElement.scrollIntoViewIfNeeded(true);
+ }
+ },
+
+ /**
+ * @override
+ * @param {boolean=} selectedByUser
+ * @return {boolean}
+ */
+ onselect: function(selectedByUser)
+ {
+ this.treeOutline.suppressRevealAndSelect = true;
+ this.treeOutline.selectDOMNode(this._node, selectedByUser);
+ if (selectedByUser)
+ this._node.highlight();
+ this.updateSelection();
+ this.treeOutline.suppressRevealAndSelect = false;
+ return true;
+ },
+
+ /**
+ * @override
+ * @return {boolean}
+ */
+ ondelete: function()
+ {
+ var startTagTreeElement = this.treeOutline.findTreeElement(this._node);
+ startTagTreeElement ? startTagTreeElement.remove() : this.remove();
+ return true;
+ },
+
+ /**
+ * @override
+ * @return {boolean}
+ */
+ onenter: function()
+ {
+ // On Enter or Return start editing the first attribute
+ // or create a new attribute on the selected element.
+ if (this._editing)
+ return false;
+
+ this._startEditing();
+
+ // prevent a newline from being immediately inserted
+ return true;
+ },
+
+ selectOnMouseDown: function(event)
+ {
+ TreeElement.prototype.selectOnMouseDown.call(this, event);
+
+ if (this._editing)
+ return;
+
+ if (this.treeOutline._showInElementsPanelEnabled) {
+ WebInspector.inspectorView.showPanel("elements");
+ this.treeOutline.selectDOMNode(this._node, true);
+ }
+
+ // Prevent selecting the nearest word on double click.
+ if (event.detail >= 2)
+ event.preventDefault();
+ },
+
+ /**
+ * @override
+ * @return {boolean}
+ */
+ ondblclick: function(event)
+ {
+ if (this._editing || this._elementCloseTag)
+ return false;
+
+ if (this._startEditingTarget(event.target))
+ return false;
+
+ if (this.hasChildren && !this.expanded)
+ this.expand();
+ return false;
+ },
+
+ /**
+ * @return {boolean}
+ */
+ hasEditableNode: function()
+ {
+ return !this.representedObject.isShadowRoot() && !this.representedObject.ancestorUserAgentShadowRoot();
+ },
+
+ _insertInLastAttributePosition: function(tag, node)
+ {
+ if (tag.getElementsByClassName("webkit-html-attribute").length > 0)
+ tag.insertBefore(node, tag.lastChild);
+ else {
+ var nodeName = tag.textContent.match(/^<(.*?)>$/)[1];
+ tag.textContent = '';
+ tag.appendChild(document.createTextNode('<'+nodeName));
+ tag.appendChild(node);
+ tag.appendChild(document.createTextNode('>'));
+ }
+
+ this.updateSelection();
+ },
+
+ _startEditingTarget: function(eventTarget)
+ {
+ if (this.treeOutline.selectedDOMNode() != this._node)
+ return;
+
+ if (this._node.nodeType() != Node.ELEMENT_NODE && this._node.nodeType() != Node.TEXT_NODE)
+ return false;
+
+ var textNode = eventTarget.enclosingNodeOrSelfWithClass("webkit-html-text-node");
+ if (textNode)
+ return this._startEditingTextNode(textNode);
+
+ var attribute = eventTarget.enclosingNodeOrSelfWithClass("webkit-html-attribute");
+ if (attribute)
+ return this._startEditingAttribute(attribute, eventTarget);
+
+ var tagName = eventTarget.enclosingNodeOrSelfWithClass("webkit-html-tag-name");
+ if (tagName)
+ return this._startEditingTagName(tagName);
+
+ var newAttribute = eventTarget.enclosingNodeOrSelfWithClass("add-attribute");
+ if (newAttribute)
+ return this._addNewAttribute();
+
+ return false;
+ },
+
+ /**
+ * @param {!WebInspector.ContextMenu} contextMenu
+ * @param {?Event} event
+ */
+ _populateTagContextMenu: function(contextMenu, event)
+ {
+ // Add attribute-related actions.
+ var treeElement = this._elementCloseTag ? this.treeOutline.findTreeElement(this._node) : this;
+ contextMenu.appendItem(WebInspector.UIString(WebInspector.useLowerCaseMenuTitles() ? "Add attribute" : "Add Attribute"), treeElement._addNewAttribute.bind(treeElement));
+
+ var attribute = event.target.enclosingNodeOrSelfWithClass("webkit-html-attribute");
+ var newAttribute = event.target.enclosingNodeOrSelfWithClass("add-attribute");
+ if (attribute && !newAttribute)
+ contextMenu.appendItem(WebInspector.UIString(WebInspector.useLowerCaseMenuTitles() ? "Edit attribute" : "Edit Attribute"), this._startEditingAttribute.bind(this, attribute, event.target));
+ contextMenu.appendSeparator();
+ if (this.treeOutline._setPseudoClassCallback) {
+ var pseudoSubMenu = contextMenu.appendSubMenuItem(WebInspector.UIString(WebInspector.useLowerCaseMenuTitles() ? "Force element state" : "Force Element State"));
+ this._populateForcedPseudoStateItems(pseudoSubMenu);
+ contextMenu.appendSeparator();
+ }
+ this._populateNodeContextMenu(contextMenu);
+ this.treeOutline._populateContextMenu(contextMenu, this._node);
+ this._populateScrollIntoView(contextMenu);
+ },
+
+ /**
+ * @param {!WebInspector.ContextMenu} contextMenu
+ */
+ _populateScrollIntoView: function(contextMenu)
+ {
+ contextMenu.appendSeparator();
+ contextMenu.appendItem(WebInspector.UIString(WebInspector.useLowerCaseMenuTitles() ? "Scroll into view" : "Scroll into View"), this._scrollIntoView.bind(this));
+ },
+
+ _populateForcedPseudoStateItems: function(subMenu)
+ {
+ const pseudoClasses = ["active", "hover", "focus", "visited"];
+ var node = this._node;
+ var forcedPseudoState = (node ? node.getUserProperty("pseudoState") : null) || [];
+ for (var i = 0; i < pseudoClasses.length; ++i) {
+ var pseudoClassForced = forcedPseudoState.indexOf(pseudoClasses[i]) >= 0;
+ subMenu.appendCheckboxItem(":" + pseudoClasses[i], this.treeOutline._setPseudoClassCallback.bind(null, node, pseudoClasses[i], !pseudoClassForced), pseudoClassForced, false);
+ }
+ },
+
+ _populateTextContextMenu: function(contextMenu, textNode)
+ {
+ if (!this._editing)
+ contextMenu.appendItem(WebInspector.UIString(WebInspector.useLowerCaseMenuTitles() ? "Edit text" : "Edit Text"), this._startEditingTextNode.bind(this, textNode));
+ this._populateNodeContextMenu(contextMenu);
+ },
+
+ _populateNodeContextMenu: function(contextMenu)
+ {
+ // Add free-form node-related actions.
+ var openTagElement = this.treeOutline.getCachedTreeElement(this.representedObject) || this;
+ var isEditable = this.hasEditableNode();
+ if (isEditable && !this._editing)
+ contextMenu.appendItem(WebInspector.UIString("Edit as HTML"), openTagElement._editAsHTML.bind(openTagElement));
+ var isShadowRoot = this.representedObject.isShadowRoot();
+ if (!isShadowRoot)
+ contextMenu.appendItem(WebInspector.UIString("Copy as HTML"), this._copyHTML.bind(this));
+
+ // Place it here so that all "Copy"-ing items stick together.
+ if (this.representedObject.nodeType() === Node.ELEMENT_NODE)
+ contextMenu.appendItem(WebInspector.UIString(WebInspector.useLowerCaseMenuTitles() ? "Copy CSS path" : "Copy CSS Path"), this._copyCSSPath.bind(this));
+ if (!isShadowRoot)
+ contextMenu.appendItem(WebInspector.UIString("Copy XPath"), this._copyXPath.bind(this));
+ if (isEditable)
+ contextMenu.appendItem(WebInspector.UIString(WebInspector.useLowerCaseMenuTitles() ? "Delete node" : "Delete Node"), this.remove.bind(this));
+ },
+
+ _startEditing: function()
+ {
+ if (this.treeOutline.selectedDOMNode() !== this._node)
+ return;
+
+ var listItem = this._listItemNode;
+
+ if (this._canAddAttributes) {
+ var attribute = listItem.getElementsByClassName("webkit-html-attribute")[0];
+ if (attribute)
+ return this._startEditingAttribute(attribute, attribute.getElementsByClassName("webkit-html-attribute-value")[0]);
+
+ return this._addNewAttribute();
+ }
+
+ if (this._node.nodeType() === Node.TEXT_NODE) {
+ var textNode = listItem.getElementsByClassName("webkit-html-text-node")[0];
+ if (textNode)
+ return this._startEditingTextNode(textNode);
+ return;
+ }
+ },
+
+ _addNewAttribute: function()
+ {
+ // Cannot just convert the textual html into an element without
+ // a parent node. Use a temporary span container for the HTML.
+ var container = document.createElement("span");
+ this._buildAttributeDOM(container, " ", "");
+ var attr = container.firstElementChild;
+ attr.style.marginLeft = "2px"; // overrides the .editing margin rule
+ attr.style.marginRight = "2px"; // overrides the .editing margin rule
+
+ var tag = this.listItemElement.getElementsByClassName("webkit-html-tag")[0];
+ this._insertInLastAttributePosition(tag, attr);
+ attr.scrollIntoViewIfNeeded(true);
+ return this._startEditingAttribute(attr, attr);
+ },
+
+ _triggerEditAttribute: function(attributeName)
+ {
+ var attributeElements = this.listItemElement.getElementsByClassName("webkit-html-attribute-name");
+ for (var i = 0, len = attributeElements.length; i < len; ++i) {
+ if (attributeElements[i].textContent === attributeName) {
+ for (var elem = attributeElements[i].nextSibling; elem; elem = elem.nextSibling) {
+ if (elem.nodeType !== Node.ELEMENT_NODE)
+ continue;
+
+ if (elem.classList.contains("webkit-html-attribute-value"))
+ return this._startEditingAttribute(elem.parentNode, elem);
+ }
+ }
+ }
+ },
+
+ _startEditingAttribute: function(attribute, elementForSelection)
+ {
+ if (WebInspector.isBeingEdited(attribute))
+ return true;
+
+ var attributeNameElement = attribute.getElementsByClassName("webkit-html-attribute-name")[0];
+ if (!attributeNameElement)
+ return false;
+
+ var attributeName = attributeNameElement.textContent;
+ var attributeValueElement = attribute.getElementsByClassName("webkit-html-attribute-value")[0];
+
+ function removeZeroWidthSpaceRecursive(node)
+ {
+ if (node.nodeType === Node.TEXT_NODE) {
+ node.nodeValue = node.nodeValue.replace(/\u200B/g, "");
+ return;
+ }
+
+ if (node.nodeType !== Node.ELEMENT_NODE)
+ return;
+
+ for (var child = node.firstChild; child; child = child.nextSibling)
+ removeZeroWidthSpaceRecursive(child);
+ }
+
+ var domNode;
+ var listItemElement = attribute.enclosingNodeOrSelfWithNodeName("li");
+ if (attributeName && attributeValueElement && listItemElement && listItemElement.treeElement)
+ domNode = listItemElement.treeElement.representedObject;
+ var attributeValue = domNode ? domNode.getAttribute(attributeName) : undefined;
+ if (typeof attributeValue !== "undefined")
+ attributeValueElement.textContent = attributeValue;
+
+ // Remove zero-width spaces that were added by nodeTitleInfo.
+ removeZeroWidthSpaceRecursive(attribute);
+
+ var config = new WebInspector.InplaceEditor.Config(this._attributeEditingCommitted.bind(this), this._editingCancelled.bind(this), attributeName);
+
+ function handleKeyDownEvents(event)
+ {
+ var isMetaOrCtrl = WebInspector.isMac() ?
+ event.metaKey && !event.shiftKey && !event.ctrlKey && !event.altKey :
+ event.ctrlKey && !event.shiftKey && !event.metaKey && !event.altKey;
+ if (isEnterKey(event) && (event.isMetaOrCtrlForTest || !config.multiline || isMetaOrCtrl))
+ return "commit";
+ else if (event.keyCode === WebInspector.KeyboardShortcut.Keys.Esc.code || event.keyIdentifier === "U+001B")
+ return "cancel";
+ else if (event.keyIdentifier === "U+0009") // Tab key
+ return "move-" + (event.shiftKey ? "backward" : "forward");
+ else {
+ WebInspector.handleElementValueModifications(event, attribute);
+ return "";
+ }
+ }
+
+ config.customFinishHandler = handleKeyDownEvents;
+
+ this._editing = WebInspector.InplaceEditor.startEditing(attribute, config);
+
+ window.getSelection().setBaseAndExtent(elementForSelection, 0, elementForSelection, 1);
+
+ return true;
+ },
+
+ /**
+ * @param {!Element} textNodeElement
+ */
+ _startEditingTextNode: function(textNodeElement)
+ {
+ if (WebInspector.isBeingEdited(textNodeElement))
+ return true;
+
+ var textNode = this._node;
+ // We only show text nodes inline in elements if the element only
+ // has a single child, and that child is a text node.
+ if (textNode.nodeType() === Node.ELEMENT_NODE && textNode.firstChild)
+ textNode = textNode.firstChild;
+
+ var container = textNodeElement.enclosingNodeOrSelfWithClass("webkit-html-text-node");
+ if (container)
+ container.textContent = textNode.nodeValue(); // Strip the CSS or JS highlighting if present.
+ var config = new WebInspector.InplaceEditor.Config(this._textNodeEditingCommitted.bind(this, textNode), this._editingCancelled.bind(this));
+ this._editing = WebInspector.InplaceEditor.startEditing(textNodeElement, config);
+ window.getSelection().setBaseAndExtent(textNodeElement, 0, textNodeElement, 1);
+
+ return true;
+ },
+
+ /**
+ * @param {!Element=} tagNameElement
+ */
+ _startEditingTagName: function(tagNameElement)
+ {
+ if (!tagNameElement) {
+ tagNameElement = this.listItemElement.getElementsByClassName("webkit-html-tag-name")[0];
+ if (!tagNameElement)
+ return false;
+ }
+
+ var tagName = tagNameElement.textContent;
+ if (WebInspector.ElementsTreeElement.EditTagBlacklist[tagName.toLowerCase()])
+ return false;
+
+ if (WebInspector.isBeingEdited(tagNameElement))
+ return true;
+
+ var closingTagElement = this._distinctClosingTagElement();
+
+ /**
+ * @param {?Event} event
+ */
+ function keyupListener(event)
+ {
+ if (closingTagElement)
+ closingTagElement.textContent = "</" + tagNameElement.textContent + ">";
+ }
+
+ /**
+ * @param {!Element} element
+ * @param {string} newTagName
+ * @this {WebInspector.ElementsTreeElement}
+ */
+ function editingComitted(element, newTagName)
+ {
+ tagNameElement.removeEventListener('keyup', keyupListener, false);
+ this._tagNameEditingCommitted.apply(this, arguments);
+ }
+
+ /**
+ * @this {WebInspector.ElementsTreeElement}
+ */
+ function editingCancelled()
+ {
+ tagNameElement.removeEventListener('keyup', keyupListener, false);
+ this._editingCancelled.apply(this, arguments);
+ }
+
+ tagNameElement.addEventListener('keyup', keyupListener, false);
+
+ var config = new WebInspector.InplaceEditor.Config(editingComitted.bind(this), editingCancelled.bind(this), tagName);
+ this._editing = WebInspector.InplaceEditor.startEditing(tagNameElement, config);
+ window.getSelection().setBaseAndExtent(tagNameElement, 0, tagNameElement, 1);
+ return true;
+ },
+
+ /**
+ * @param {function(string, string)} commitCallback
+ * @param {?Protocol.Error} error
+ * @param {string} initialValue
+ */
+ _startEditingAsHTML: function(commitCallback, error, initialValue)
+ {
+ if (error)
+ return;
+ if (this._editing)
+ return;
+
+ function consume(event)
+ {
+ if (event.eventPhase === Event.AT_TARGET)
+ event.consume(true);
+ }
+
+ initialValue = this._convertWhitespaceToEntities(initialValue).text;
+
+ this._htmlEditElement = document.createElement("div");
+ this._htmlEditElement.className = "source-code elements-tree-editor";
+
+ // Hide header items.
+ var child = this.listItemElement.firstChild;
+ while (child) {
+ child.style.display = "none";
+ child = child.nextSibling;
+ }
+ // Hide children item.
+ if (this._childrenListNode)
+ this._childrenListNode.style.display = "none";
+ // Append editor.
+ this.listItemElement.appendChild(this._htmlEditElement);
+ this.treeOutline.childrenListElement.parentElement.addEventListener("mousedown", consume, false);
+
+ this.updateSelection();
+
+ /**
+ * @param {!Element} element
+ * @param {string} newValue
+ * @this {WebInspector.ElementsTreeElement}
+ */
+ function commit(element, newValue)
+ {
+ commitCallback(initialValue, newValue);
+ dispose.call(this);
+ }
+
+ /**
+ * @this {WebInspector.ElementsTreeElement}
+ */
+ function dispose()
+ {
+ delete this._editing;
+ delete this.treeOutline._multilineEditing;
+
+ // Remove editor.
+ this.listItemElement.removeChild(this._htmlEditElement);
+ delete this._htmlEditElement;
+ // Unhide children item.
+ if (this._childrenListNode)
+ this._childrenListNode.style.removeProperty("display");
+ // Unhide header items.
+ var child = this.listItemElement.firstChild;
+ while (child) {
+ child.style.removeProperty("display");
+ child = child.nextSibling;
+ }
+
+ this.treeOutline.childrenListElement.parentElement.removeEventListener("mousedown", consume, false);
+ this.updateSelection();
+ this.treeOutline.element.focus();
+ }
+
+ var config = new WebInspector.InplaceEditor.Config(commit.bind(this), dispose.bind(this));
+ config.setMultilineOptions(initialValue, { name: "xml", htmlMode: true }, "web-inspector-html", WebInspector.settings.domWordWrap.get(), true);
+ this._editing = WebInspector.InplaceEditor.startEditing(this._htmlEditElement, config);
+ this._editing.setWidth(this.treeOutline._visibleWidth);
+ this.treeOutline._multilineEditing = this._editing;
+ },
+
+ _attributeEditingCommitted: function(element, newText, oldText, attributeName, moveDirection)
+ {
+ delete this._editing;
+
+ var treeOutline = this.treeOutline;
+
+ /**
+ * @param {?Protocol.Error=} error
+ * @this {WebInspector.ElementsTreeElement}
+ */
+ function moveToNextAttributeIfNeeded(error)
+ {
+ if (error)
+ this._editingCancelled(element, attributeName);
+
+ if (!moveDirection)
+ return;
+
+ treeOutline._updateModifiedNodes();
+
+ // Search for the attribute's position, and then decide where to move to.
+ var attributes = this._node.attributes();
+ for (var i = 0; i < attributes.length; ++i) {
+ if (attributes[i].name !== attributeName)
+ continue;
+
+ if (moveDirection === "backward") {
+ if (i === 0)
+ this._startEditingTagName();
+ else
+ this._triggerEditAttribute(attributes[i - 1].name);
+ } else {
+ if (i === attributes.length - 1)
+ this._addNewAttribute();
+ else
+ this._triggerEditAttribute(attributes[i + 1].name);
+ }
+ return;
+ }
+
+ // Moving From the "New Attribute" position.
+ if (moveDirection === "backward") {
+ if (newText === " ") {
+ // Moving from "New Attribute" that was not edited
+ if (attributes.length > 0)
+ this._triggerEditAttribute(attributes[attributes.length - 1].name);
+ } else {
+ // Moving from "New Attribute" that holds new value
+ if (attributes.length > 1)
+ this._triggerEditAttribute(attributes[attributes.length - 2].name);
+ }
+ } else if (moveDirection === "forward") {
+ if (!/^\s*$/.test(newText))
+ this._addNewAttribute();
+ else
+ this._startEditingTagName();
+ }
+ }
+
+ if (!attributeName.trim() && !newText.trim()) {
+ element.remove();
+ moveToNextAttributeIfNeeded.call(this);
+ return;
+ }
+
+ if (oldText !== newText) {
+ this._node.setAttribute(attributeName, newText, moveToNextAttributeIfNeeded.bind(this));
+ return;
+ }
+
+ this.updateTitle();
+ moveToNextAttributeIfNeeded.call(this);
+ },
+
+ _tagNameEditingCommitted: function(element, newText, oldText, tagName, moveDirection)
+ {
+ delete this._editing;
+ var self = this;
+
+ function cancel()
+ {
+ var closingTagElement = self._distinctClosingTagElement();
+ if (closingTagElement)
+ closingTagElement.textContent = "</" + tagName + ">";
+
+ self._editingCancelled(element, tagName);
+ moveToNextAttributeIfNeeded.call(self);
+ }
+
+ /**
+ * @this {WebInspector.ElementsTreeElement}
+ */
+ function moveToNextAttributeIfNeeded()
+ {
+ if (moveDirection !== "forward") {
+ this._addNewAttribute();
+ return;
+ }
+
+ var attributes = this._node.attributes();
+ if (attributes.length > 0)
+ this._triggerEditAttribute(attributes[0].name);
+ else
+ this._addNewAttribute();
+ }
+
+ newText = newText.trim();
+ if (newText === oldText) {
+ cancel();
+ return;
+ }
+
+ var treeOutline = this.treeOutline;
+ var wasExpanded = this.expanded;
+
+ function changeTagNameCallback(error, nodeId)
+ {
+ if (error || !nodeId) {
+ cancel();
+ return;
+ }
+ var newTreeItem = treeOutline._selectNodeAfterEdit(wasExpanded, error, nodeId);
+ moveToNextAttributeIfNeeded.call(newTreeItem);
+ }
+
+ this._node.setNodeName(newText, changeTagNameCallback);
+ },
+
+ /**
+ * @param {!WebInspector.DOMNode} textNode
+ * @param {!Element} element
+ * @param {string} newText
+ */
+ _textNodeEditingCommitted: function(textNode, element, newText)
+ {
+ delete this._editing;
+
+ /**
+ * @this {WebInspector.ElementsTreeElement}
+ */
+ function callback()
+ {
+ this.updateTitle();
+ }
+ textNode.setNodeValue(newText, callback.bind(this));
+ },
+
+ /**
+ * @param {!Element} element
+ * @param {*} context
+ */
+ _editingCancelled: function(element, context)
+ {
+ delete this._editing;
+
+ // Need to restore attributes structure.
+ this.updateTitle();
+ },
+
+ /**
+ * @return {!Element}
+ */
+ _distinctClosingTagElement: function()
+ {
+ // FIXME: Improve the Tree Element / Outline Abstraction to prevent crawling the DOM
+
+ // For an expanded element, it will be the last element with class "close"
+ // in the child element list.
+ if (this.expanded) {
+ var closers = this._childrenListNode.querySelectorAll(".close");
+ return closers[closers.length-1];
+ }
+
+ // Remaining cases are single line non-expanded elements with a closing
+ // tag, or HTML elements without a closing tag (such as <br>). Return
+ // null in the case where there isn't a closing tag.
+ var tags = this.listItemElement.getElementsByClassName("webkit-html-tag");
+ return (tags.length === 1 ? null : tags[tags.length-1]);
+ },
+
+ /**
+ * @param {boolean=} onlySearchQueryChanged
+ */
+ updateTitle: function(onlySearchQueryChanged)
+ {
+ // If we are editing, return early to prevent canceling the edit.
+ // After editing is committed updateTitle will be called.
+ if (this._editing)
+ return;
+
+ if (onlySearchQueryChanged) {
+ if (this._highlightResult)
+ this._updateSearchHighlight(false);
+ } else {
+ var nodeInfo = this._nodeTitleInfo(WebInspector.linkifyURLAsNode);
+ if (nodeInfo.shadowRoot)
+ this.listItemElement.classList.add("shadow-root");
+ var highlightElement = document.createElement("span");
+ highlightElement.className = "highlight";
+ highlightElement.appendChild(nodeInfo.titleDOM);
+ this.title = highlightElement;
+ this._updateDecorations();
+ delete this._highlightResult;
+ }
+
+ delete this.selectionElement;
+ if (this.selected)
+ this.updateSelection();
+ this._preventFollowingLinksOnDoubleClick();
+ this._highlightSearchResults();
+ },
+
+ /**
+ * @return {?Element}
+ */
+ _createDecoratorElement: function()
+ {
+ var node = this._node;
+ var decoratorMessages = [];
+ var parentDecoratorMessages = [];
+ for (var i = 0; i < this.treeOutline._nodeDecorators.length; ++i) {
+ var decorator = this.treeOutline._nodeDecorators[i];
+ var message = decorator.decorate(node);
+ if (message) {
+ decoratorMessages.push(message);
+ continue;
+ }
+
+ if (this.expanded || this._elementCloseTag)
+ continue;
+
+ message = decorator.decorateAncestor(node);
+ if (message)
+ parentDecoratorMessages.push(message)
+ }
+ if (!decoratorMessages.length && !parentDecoratorMessages.length)
+ return null;
+
+ var decoratorElement = document.createElement("div");
+ decoratorElement.classList.add("elements-gutter-decoration");
+ if (!decoratorMessages.length)
+ decoratorElement.classList.add("elements-has-decorated-children");
+ decoratorElement.title = decoratorMessages.concat(parentDecoratorMessages).join("\n");
+ return decoratorElement;
+ },
+
+ _updateDecorations: function()
+ {
+ if (this._decoratorElement)
+ this._decoratorElement.remove();
+ this._decoratorElement = this._createDecoratorElement();
+ if (this._decoratorElement && this.listItemElement)
+ this.listItemElement.insertBefore(this._decoratorElement, this.listItemElement.firstChild);
+ },
+
+ /**
+ * @param {!Node} parentElement
+ * @param {string} name
+ * @param {string} value
+ * @param {boolean=} forceValue
+ * @param {!WebInspector.DOMNode=} node
+ * @param {function(string, string, string, boolean=, string=)=} linkify
+ */
+ _buildAttributeDOM: function(parentElement, name, value, forceValue, node, linkify)
+ {
+ var closingPunctuationRegex = /[\/;:\)\]\}]/g;
+ var highlightIndex = 0;
+ var highlightCount;
+ var additionalHighlightOffset = 0;
+ var result;
+
+ /**
+ * @param {string} match
+ * @param {number} replaceOffset
+ * @return {string}
+ */
+ function replacer(match, replaceOffset) {
+ while (highlightIndex < highlightCount && result.entityRanges[highlightIndex].offset < replaceOffset) {
+ result.entityRanges[highlightIndex].offset += additionalHighlightOffset;
+ ++highlightIndex;
+ }
+ additionalHighlightOffset += 1;
+ return match + "\u200B";
+ }
+
+ /**
+ * @param {!Element} element
+ * @param {string} value
+ * @this {WebInspector.ElementsTreeElement}
+ */
+ function setValueWithEntities(element, value)
+ {
+ var attrValueElement = element.createChild("span", "webkit-html-attribute-value");
+ result = this._convertWhitespaceToEntities(value);
+ highlightCount = result.entityRanges.length;
+ value = result.text.replace(closingPunctuationRegex, replacer);
+ while (highlightIndex < highlightCount) {
+ result.entityRanges[highlightIndex].offset += additionalHighlightOffset;
+ ++highlightIndex;
+ }
+ attrValueElement.textContent = value;
+ WebInspector.highlightRangesWithStyleClass(attrValueElement, result.entityRanges, "webkit-html-entity-value");
+ }
+
+ var hasText = (forceValue || value.length > 0);
+ var attrSpanElement = parentElement.createChild("span", "webkit-html-attribute");
+ var attrNameElement = attrSpanElement.createChild("span", "webkit-html-attribute-name");
+ attrNameElement.textContent = name;
+
+ if (hasText)
+ attrSpanElement.appendChild(document.createTextNode("=\u200B\""));
+
+ if (linkify && (name === "src" || name === "href")) {
+ var rewrittenHref = node.resolveURL(value);
+ if (rewrittenHref === null) {
+ setValueWithEntities.call(this, attrSpanElement, value);
+ } else {
+ value = value.replace(closingPunctuationRegex, "$&\u200B");
+ if (value.startsWith("data:"))
+ value = value.trimMiddle(60);
+ attrSpanElement.appendChild(linkify(rewrittenHref, value, "webkit-html-attribute-value", node.nodeName().toLowerCase() === "a"));
+ }
+ } else {
+ setValueWithEntities.call(this, attrSpanElement, value);
+ }
+
+ if (hasText)
+ attrSpanElement.appendChild(document.createTextNode("\""));
+ },
+
+ /**
+ * @param {!Node} parentElement
+ * @param {string} pseudoElementName
+ */
+ _buildPseudoElementDOM: function(parentElement, pseudoElementName)
+ {
+ var pseudoElement = parentElement.createChild("span", "webkit-html-pseudo-element");
+ pseudoElement.textContent = "::" + pseudoElementName;
+ parentElement.appendChild(document.createTextNode("\u200B"));
+ },
+
+ /**
+ * @param {!Node} parentElement
+ * @param {string} tagName
+ * @param {boolean} isClosingTag
+ * @param {boolean} isDistinctTreeElement
+ * @param {function(string, string, string, boolean=, string=)=} linkify
+ */
+ _buildTagDOM: function(parentElement, tagName, isClosingTag, isDistinctTreeElement, linkify)
+ {
+ var node = this._node;
+ var classes = [ "webkit-html-tag" ];
+ if (isClosingTag && isDistinctTreeElement)
+ classes.push("close");
+ var tagElement = parentElement.createChild("span", classes.join(" "));
+ tagElement.appendChild(document.createTextNode("<"));
+ var tagNameElement = tagElement.createChild("span", isClosingTag ? "" : "webkit-html-tag-name");
+ tagNameElement.textContent = (isClosingTag ? "/" : "") + tagName;
+ if (!isClosingTag && node.hasAttributes()) {
+ var attributes = node.attributes();
+ for (var i = 0; i < attributes.length; ++i) {
+ var attr = attributes[i];
+ tagElement.appendChild(document.createTextNode(" "));
+ this._buildAttributeDOM(tagElement, attr.name, attr.value, false, node, linkify);
+ }
+ }
+ tagElement.appendChild(document.createTextNode(">"));
+ parentElement.appendChild(document.createTextNode("\u200B"));
+ },
+
+ /**
+ * @param {string} text
+ * @return {!{text: string, entityRanges: !Array.<!WebInspector.SourceRange>}}
+ */
+ _convertWhitespaceToEntities: function(text)
+ {
+ var result = "";
+ var resultLength = 0;
+ var lastIndexAfterEntity = 0;
+ var entityRanges = [];
+ var charToEntity = WebInspector.ElementsTreeOutline.MappedCharToEntity;
+ for (var i = 0, size = text.length; i < size; ++i) {
+ var char = text.charAt(i);
+ if (charToEntity[char]) {
+ result += text.substring(lastIndexAfterEntity, i);
+ var entityValue = "&" + charToEntity[char] + ";";
+ entityRanges.push({offset: result.length, length: entityValue.length});
+ result += entityValue;
+ lastIndexAfterEntity = i + 1;
+ }
+ }
+ if (result)
+ result += text.substring(lastIndexAfterEntity);
+ return {text: result || text, entityRanges: entityRanges};
+ },
+
+ /**
+ * @param {function(string, string, string, boolean=, string=)=} linkify
+ */
+ _nodeTitleInfo: function(linkify)
+ {
+ var node = this._node;
+ var info = {titleDOM: document.createDocumentFragment(), hasChildren: this.hasChildren};
+
+ switch (node.nodeType()) {
+ case Node.ATTRIBUTE_NODE:
+ this._buildAttributeDOM(info.titleDOM, /** @type {string} */ (node.name), /** @type {string} */ (node.value), true);
+ break;
+
+ case Node.ELEMENT_NODE:
+ var pseudoType = node.pseudoType();
+ if (pseudoType) {
+ this._buildPseudoElementDOM(info.titleDOM, pseudoType);
+ info.hasChildren = false;
+ break;
+ }
+
+ var tagName = node.nodeNameInCorrectCase();
+ if (this._elementCloseTag) {
+ this._buildTagDOM(info.titleDOM, tagName, true, true);
+ info.hasChildren = false;
+ break;
+ }
+
+ this._buildTagDOM(info.titleDOM, tagName, false, false, linkify);
+
+ var showInlineText = this._showInlineText() && !this.hasChildren;
+ if (!this.expanded && !showInlineText && (this.treeOutline.isXMLMimeType || !WebInspector.ElementsTreeElement.ForbiddenClosingTagElements[tagName])) {
+ if (this.hasChildren) {
+ var textNodeElement = info.titleDOM.createChild("span", "webkit-html-text-node bogus");
+ textNodeElement.textContent = "\u2026";
+ info.titleDOM.appendChild(document.createTextNode("\u200B"));
+ }
+ this._buildTagDOM(info.titleDOM, tagName, true, false);
+ }
+
+ // If this element only has a single child that is a text node,
+ // just show that text and the closing tag inline rather than
+ // create a subtree for them
+ if (showInlineText) {
+ var textNodeElement = info.titleDOM.createChild("span", "webkit-html-text-node");
+ var result = this._convertWhitespaceToEntities(node.firstChild.nodeValue());
+ textNodeElement.textContent = result.text;
+ WebInspector.highlightRangesWithStyleClass(textNodeElement, result.entityRanges, "webkit-html-entity-value");
+ info.titleDOM.appendChild(document.createTextNode("\u200B"));
+ this._buildTagDOM(info.titleDOM, tagName, true, false);
+ info.hasChildren = false;
+ }
+ break;
+
+ case Node.TEXT_NODE:
+ if (node.parentNode && node.parentNode.nodeName().toLowerCase() === "script") {
+ var newNode = info.titleDOM.createChild("span", "webkit-html-text-node webkit-html-js-node");
+ newNode.textContent = node.nodeValue();
+
+ var javascriptSyntaxHighlighter = new WebInspector.DOMSyntaxHighlighter("text/javascript", true);
+ javascriptSyntaxHighlighter.syntaxHighlightNode(newNode);
+ } else if (node.parentNode && node.parentNode.nodeName().toLowerCase() === "style") {
+ var newNode = info.titleDOM.createChild("span", "webkit-html-text-node webkit-html-css-node");
+ newNode.textContent = node.nodeValue();
+
+ var cssSyntaxHighlighter = new WebInspector.DOMSyntaxHighlighter("text/css", true);
+ cssSyntaxHighlighter.syntaxHighlightNode(newNode);
+ } else {
+ info.titleDOM.appendChild(document.createTextNode("\""));
+ var textNodeElement = info.titleDOM.createChild("span", "webkit-html-text-node");
+ var result = this._convertWhitespaceToEntities(node.nodeValue());
+ textNodeElement.textContent = result.text;
+ WebInspector.highlightRangesWithStyleClass(textNodeElement, result.entityRanges, "webkit-html-entity-value");
+ info.titleDOM.appendChild(document.createTextNode("\""));
+ }
+ break;
+
+ case Node.COMMENT_NODE:
+ var commentElement = info.titleDOM.createChild("span", "webkit-html-comment");
+ commentElement.appendChild(document.createTextNode("<!--" + node.nodeValue() + "-->"));
+ break;
+
+ case Node.DOCUMENT_TYPE_NODE:
+ var docTypeElement = info.titleDOM.createChild("span", "webkit-html-doctype");
+ docTypeElement.appendChild(document.createTextNode("<!DOCTYPE " + node.nodeName()));
+ if (node.publicId) {
+ docTypeElement.appendChild(document.createTextNode(" PUBLIC \"" + node.publicId + "\""));
+ if (node.systemId)
+ docTypeElement.appendChild(document.createTextNode(" \"" + node.systemId + "\""));
+ } else if (node.systemId)
+ docTypeElement.appendChild(document.createTextNode(" SYSTEM \"" + node.systemId + "\""));
+
+ if (node.internalSubset)
+ docTypeElement.appendChild(document.createTextNode(" [" + node.internalSubset + "]"));
+
+ docTypeElement.appendChild(document.createTextNode(">"));
+ break;
+
+ case Node.CDATA_SECTION_NODE:
+ var cdataElement = info.titleDOM.createChild("span", "webkit-html-text-node");
+ cdataElement.appendChild(document.createTextNode("<![CDATA[" + node.nodeValue() + "]]>"));
+ break;
+ case Node.DOCUMENT_FRAGMENT_NODE:
+ var fragmentElement = info.titleDOM.createChild("span", "webkit-html-fragment");
+ if (node.isInShadowTree()) {
+ var shadowRootType = node.shadowRootType();
+ if (shadowRootType) {
+ info.shadowRoot = true;
+ fragmentElement.classList.add("shadow-root");
+ }
+ }
+ fragmentElement.textContent = node.nodeNameInCorrectCase().collapseWhitespace();
+ break;
+ default:
+ info.titleDOM.appendChild(document.createTextNode(node.nodeNameInCorrectCase().collapseWhitespace()));
+ }
+ return info;
+ },
+
+ /**
+ * @return {boolean}
+ */
+ _showInlineText: function()
+ {
+ if (this._node.importedDocument() || this._node.templateContent() || this._visibleShadowRoots().length > 0 || this._node.hasPseudoElements())
+ return false;
+ if (this._node.nodeType() !== Node.ELEMENT_NODE)
+ return false;
+ if (!this._node.firstChild || this._node.firstChild !== this._node.lastChild || this._node.firstChild.nodeType() !== Node.TEXT_NODE)
+ return false;
+ var textChild = this._node.firstChild;
+ var maxInlineTextChildLength = 80;
+ if (textChild.nodeValue().length < maxInlineTextChildLength)
+ return true;
+ return false;
+ },
+
+ remove: function()
+ {
+ if (this._node.pseudoType())
+ return;
+ var parentElement = this.parent;
+ if (!parentElement)
+ return;
+
+ var self = this;
+ function removeNodeCallback(error)
+ {
+ if (error)
+ return;
+
+ parentElement.removeChild(self);
+ parentElement._adjustCollapsedRange();
+ }
+
+ if (!this._node.parentNode || this._node.parentNode.nodeType() === Node.DOCUMENT_NODE)
+ return;
+ this._node.removeNode(removeNodeCallback);
+ },
+
+ _editAsHTML: function()
+ {
+ var node = this._node;
+ if (node.pseudoType())
+ return;
+
+ var treeOutline = this.treeOutline;
+ var parentNode = node.parentNode;
+ var index = node.index;
+ var wasExpanded = this.expanded;
+
+ /**
+ * @param {?Protocol.Error} error
+ */
+ function selectNode(error)
+ {
+ if (error)
+ return;
+
+ // Select it and expand if necessary. We force tree update so that it processes dom events and is up to date.
+ treeOutline._updateModifiedNodes();
+
+ var newNode = parentNode ? parentNode.children()[index] || parentNode : null;
+ if (!newNode)
+ return;
+
+ treeOutline.selectDOMNode(newNode, true);
+
+ if (wasExpanded) {
+ var newTreeItem = treeOutline.findTreeElement(newNode);
+ if (newTreeItem)
+ newTreeItem.expand();
+ }
+ }
+
+ /**
+ * @param {string} initialValue
+ * @param {string} value
+ */
+ function commitChange(initialValue, value)
+ {
+ if (initialValue !== value)
+ node.setOuterHTML(value, selectNode);
+ }
+
+ node.getOuterHTML(this._startEditingAsHTML.bind(this, commitChange));
+ },
+
+ _copyHTML: function()
+ {
+ this._node.copyNode();
+ },
+
+ _copyCSSPath: function()
+ {
+ InspectorFrontendHost.copyText(WebInspector.DOMPresentationUtils.cssPath(this._node, true));
+ },
+
+ _copyXPath: function()
+ {
+ InspectorFrontendHost.copyText(WebInspector.DOMPresentationUtils.xPath(this._node, true));
+ },
+
+ _highlightSearchResults: function()
+ {
+ if (!this._searchQuery || !this._searchHighlightsVisible)
+ return;
+ if (this._highlightResult) {
+ this._updateSearchHighlight(true);
+ return;
+ }
+
+ var text = this.listItemElement.textContent;
+ var regexObject = createPlainTextSearchRegex(this._searchQuery, "gi");
+
+ var offset = 0;
+ var match = regexObject.exec(text);
+ var matchRanges = [];
+ while (match) {
+ matchRanges.push(new WebInspector.SourceRange(match.index, match[0].length));
+ match = regexObject.exec(text);
+ }
+
+ // Fall back for XPath, etc. matches.
+ if (!matchRanges.length)
+ matchRanges.push(new WebInspector.SourceRange(0, text.length));
+
+ this._highlightResult = [];
+ WebInspector.highlightSearchResults(this.listItemElement, matchRanges, this._highlightResult);
+ },
+
+ _scrollIntoView: function()
+ {
+ function scrollIntoViewCallback(object)
+ {
+ /**
+ * @suppressReceiverCheck
+ * @this {!Element}
+ */
+ function scrollIntoView()
+ {
+ this.scrollIntoViewIfNeeded(true);
+ }
+
+ if (object)
+ object.callFunction(scrollIntoView);
+ }
+
+ this._node.resolveToObject("", scrollIntoViewCallback);
+ },
+
+ /**
+ * @return {!Array.<!WebInspector.DOMNode>}
+ */
+ _visibleShadowRoots: function()
+ {
+ var roots = this._node.shadowRoots();
+ if (roots.length && !WebInspector.settings.showUAShadowDOM.get()) {
+ roots = roots.filter(function(root) {
+ return root.shadowRootType() === WebInspector.DOMNode.ShadowRootTypes.Author;
+ });
+ }
+ return roots;
+ },
+
+ /**
+ * @return {!Array.<!WebInspector.DOMNode>} visibleChildren
+ */
+ _visibleChildren: function()
+ {
+ var visibleChildren = this._visibleShadowRoots();
+ if (this._node.importedDocument())
+ visibleChildren.push(this._node.importedDocument());
+ if (this._node.templateContent())
+ visibleChildren.push(this._node.templateContent());
+ var pseudoElements = this._node.pseudoElements();
+ if (pseudoElements[WebInspector.DOMNode.PseudoElementNames.Before])
+ visibleChildren.push(pseudoElements[WebInspector.DOMNode.PseudoElementNames.Before]);
+ if (this._node.childNodeCount())
+ visibleChildren = visibleChildren.concat(this._node.children());
+ if (pseudoElements[WebInspector.DOMNode.PseudoElementNames.After])
+ visibleChildren.push(pseudoElements[WebInspector.DOMNode.PseudoElementNames.After]);
+ return visibleChildren;
+ },
+
+ /**
+ * @return {number}
+ */
+ _visibleChildCount: function()
+ {
+ var childCount = this._node.childNodeCount() + this._visibleShadowRoots().length;
+ if (this._node.importedDocument())
+ ++childCount;
+ if (this._node.templateContent())
+ ++childCount;
+ for (var pseudoType in this._node.pseudoElements())
+ ++childCount;
+ return childCount;
+ },
+
+ _updateHasChildren: function()
+ {
+ this.hasChildren = !this._elementCloseTag && !this._showInlineText() && this._visibleChildCount() > 0;
+ },
+
+ __proto__: TreeElement.prototype
+}
+
+/**
+ * @constructor
+ * @param {!WebInspector.DOMModel} domModel
+ * @param {!WebInspector.ElementsTreeOutline} treeOutline
+ */
+WebInspector.ElementsTreeUpdater = function(domModel, treeOutline)
+{
+ domModel.addEventListener(WebInspector.DOMModel.Events.NodeInserted, this._nodeInserted, this);
+ domModel.addEventListener(WebInspector.DOMModel.Events.NodeRemoved, this._nodeRemoved, this);
+ domModel.addEventListener(WebInspector.DOMModel.Events.AttrModified, this._attributesUpdated, this);
+ domModel.addEventListener(WebInspector.DOMModel.Events.AttrRemoved, this._attributesUpdated, this);
+ domModel.addEventListener(WebInspector.DOMModel.Events.CharacterDataModified, this._characterDataModified, this);
+ domModel.addEventListener(WebInspector.DOMModel.Events.DocumentUpdated, this._documentUpdated, this);
+ domModel.addEventListener(WebInspector.DOMModel.Events.ChildNodeCountUpdated, this._childNodeCountUpdated, this);
+
+ this._domModel = domModel;
+ this._treeOutline = treeOutline;
+ /** @type {!Map.<!WebInspector.DOMNode, !WebInspector.ElementsTreeUpdater.UpdateEntry>} */
+ this._recentlyModifiedNodes = new Map();
+}
+
+WebInspector.ElementsTreeUpdater.prototype = {
+ dispose: function()
+ {
+ this._domModel.removeEventListener(WebInspector.DOMModel.Events.NodeInserted, this._nodeInserted, this);
+ this._domModel.removeEventListener(WebInspector.DOMModel.Events.NodeRemoved, this._nodeRemoved, this);
+ this._domModel.removeEventListener(WebInspector.DOMModel.Events.AttrModified, this._attributesUpdated, this);
+ this._domModel.removeEventListener(WebInspector.DOMModel.Events.AttrRemoved, this._attributesUpdated, this);
+ this._domModel.removeEventListener(WebInspector.DOMModel.Events.CharacterDataModified, this._characterDataModified, this);
+ this._domModel.removeEventListener(WebInspector.DOMModel.Events.DocumentUpdated, this._documentUpdated, this);
+ this._domModel.removeEventListener(WebInspector.DOMModel.Events.ChildNodeCountUpdated, this._childNodeCountUpdated, this);
+ },
+
+ /**
+ * @param {!WebInspector.DOMNode} node
+ * @param {boolean} isUpdated
+ * @param {!WebInspector.DOMNode=} parentNode
+ */
+ _nodeModified: function(node, isUpdated, parentNode)
+ {
+ if (this._treeOutline._visible)
+ this._updateModifiedNodesSoon();
+
+ var entry = this._recentlyModifiedNodes.get(node);
+ if (!entry) {
+ entry = new WebInspector.ElementsTreeUpdater.UpdateEntry(isUpdated, parentNode);
+ this._recentlyModifiedNodes.put(node, entry);
+ return;
+ }
+
+ entry.isUpdated |= isUpdated;
+ if (parentNode)
+ entry.parent = parentNode;
+ },
+
+ _documentUpdated: function(event)
+ {
+ var inspectedRootDocument = event.data;
+
+ this._reset();
+
+ if (!inspectedRootDocument)
+ return;
+
+ this._treeOutline.rootDOMNode = inspectedRootDocument;
+ },
+
+ _attributesUpdated: function(event)
+ {
+ this._nodeModified(event.data.node, true);
+ },
+
+ _characterDataModified: function(event)
+ {
+ this._nodeModified(event.data, true);
+ },
+
+ _nodeInserted: function(event)
+ {
+ this._nodeModified(event.data, false, event.data.parentNode);
+ },
+
+ _nodeRemoved: function(event)
+ {
+ this._nodeModified(event.data.node, false, event.data.parent);
+ },
+
+ _childNodeCountUpdated: function(event)
+ {
+ var treeElement = this._treeOutline.findTreeElement(event.data);
+ if (treeElement) {
+ var oldHasChildren = treeElement.hasChildren;
+ treeElement._updateHasChildren();
+ if (treeElement.hasChildren !== oldHasChildren)
+ treeElement.updateTitle();
+ }
+ },
+
+ _updateModifiedNodesSoon: function()
+ {
+ if (this._updateModifiedNodesTimeout)
+ return;
+ this._updateModifiedNodesTimeout = setTimeout(this._updateModifiedNodes.bind(this), 50);
+ },
+
+ _updateModifiedNodes: function()
+ {
+ if (this._updateModifiedNodesTimeout) {
+ clearTimeout(this._updateModifiedNodesTimeout);
+ delete this._updateModifiedNodesTimeout;
+ }
+
+ var updatedParentTreeElements = [];
+
+ var hidePanelWhileUpdating = this._recentlyModifiedNodes.size() > 10;
+ if (hidePanelWhileUpdating) {
+ var treeOutlineContainerElement = this._treeOutline.element.parentNode;
+ var originalScrollTop = treeOutlineContainerElement ? treeOutlineContainerElement.scrollTop : 0;
+ this._treeOutline.element.classList.add("hidden");
+ }
+
+ var nodes = this._recentlyModifiedNodes.keys();
+ for (var i = 0, size = nodes.length; i < size; ++i) {
+ var node = nodes[i];
+ var entry = this._recentlyModifiedNodes.get(node);
+ var parent = entry.parent;
+
+ if (parent === this._treeOutline._rootDOMNode) {
+ // Document's children have changed, perform total update.
+ this._treeOutline.update();
+ this._treeOutline.element.classList.remove("hidden");
+ return;
+ }
+
+ if (entry.isUpdated) {
+ var nodeItem = this._treeOutline.findTreeElement(node);
+ if (nodeItem)
+ nodeItem.updateTitle();
+ }
+
+ var parentNodeItem = parent ? this._treeOutline.findTreeElement(parent) : null;
+ if (parentNodeItem && !parentNodeItem.alreadyUpdatedChildren) {
+ parentNodeItem.updateChildren();
+ parentNodeItem.alreadyUpdatedChildren = true;
+ updatedParentTreeElements.push(parentNodeItem);
+ }
+ }
+
+ for (var i = 0; i < updatedParentTreeElements.length; ++i)
+ delete updatedParentTreeElements[i].alreadyUpdatedChildren;
+
+ if (hidePanelWhileUpdating) {
+ this._treeOutline.element.classList.remove("hidden");
+ if (originalScrollTop)
+ treeOutlineContainerElement.scrollTop = originalScrollTop;
+ this._treeOutline.updateSelection();
+ }
+ this._recentlyModifiedNodes.clear();
+
+ this._treeOutline._fireElementsTreeUpdated(nodes);
+ },
+
+ _reset: function()
+ {
+ this._treeOutline.rootDOMNode = null;
+ this._treeOutline.selectDOMNode(null, false);
+ this._domModel.hideDOMNodeHighlight();
+ this._recentlyModifiedNodes.clear();
+ }
+}
+
+/**
+ * @constructor
+ * @param {boolean} isUpdated
+ * @param {!WebInspector.DOMNode=} parent
+ */
+WebInspector.ElementsTreeUpdater.UpdateEntry = function(isUpdated, parent)
+{
+ this.isUpdated = isUpdated;
+ if (parent)
+ this.parent = parent;
+}
+
+/**
+ * @constructor
+ * @implements {WebInspector.Renderer}
+ */
+WebInspector.ElementsTreeOutline.Renderer = function()
+{
+}
+
+WebInspector.ElementsTreeOutline.Renderer.prototype = {
+ /**
+ * @param {!Object} object
+ * @return {?Element}
+ */
+ render: function(object)
+ {
+ if (!(object instanceof WebInspector.DOMNode))
+ return null;
+ var node = /** @type {!WebInspector.DOMNode} */ (object);
+ var treeOutline = new WebInspector.ElementsTreeOutline(node.target(), false, false);
+ treeOutline.rootDOMNode = node;
+ treeOutline.element.classList.add("outline-disclosure");
+ if (!treeOutline.children[0].hasChildren)
+ treeOutline.element.classList.add("single-node");
+ treeOutline.setVisible(true);
+ treeOutline.element.treeElementForTest = treeOutline.children[0];
+ return treeOutline.element;
+ }
+}