summaryrefslogtreecommitdiff
path: root/Source/WebInspectorUI/UserInterface/DOMTreeElement.js
diff options
context:
space:
mode:
authorAllan Sandfeld Jensen <allan.jensen@digia.com>2013-09-13 12:51:20 +0200
committerThe Qt Project <gerrit-noreply@qt-project.org>2013-09-19 20:50:05 +0200
commitd441d6f39bb846989d95bcf5caf387b42414718d (patch)
treee367e64a75991c554930278175d403c072de6bb8 /Source/WebInspectorUI/UserInterface/DOMTreeElement.js
parent0060b2994c07842f4c59de64b5e3e430525c4b90 (diff)
downloadqtwebkit-d441d6f39bb846989d95bcf5caf387b42414718d.tar.gz
Import Qt5x2 branch of QtWebkit for Qt 5.2
Importing a new snapshot of webkit. Change-Id: I2d01ad12cdc8af8cb015387641120a9d7ea5f10c Reviewed-by: Allan Sandfeld Jensen <allan.jensen@digia.com>
Diffstat (limited to 'Source/WebInspectorUI/UserInterface/DOMTreeElement.js')
-rw-r--r--Source/WebInspectorUI/UserInterface/DOMTreeElement.js1307
1 files changed, 1307 insertions, 0 deletions
diff --git a/Source/WebInspectorUI/UserInterface/DOMTreeElement.js b/Source/WebInspectorUI/UserInterface/DOMTreeElement.js
new file mode 100644
index 000000000..d1af2ae68
--- /dev/null
+++ b/Source/WebInspectorUI/UserInterface/DOMTreeElement.js
@@ -0,0 +1,1307 @@
+/*
+ * Copyright (C) 2007, 2008, 2013 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 {TreeElement}
+ * @param {boolean=} elementCloseTag
+ */
+WebInspector.DOMTreeElement = function(node, elementCloseTag)
+{
+ this._elementCloseTag = elementCloseTag;
+ var hasChildrenOverride = !elementCloseTag && node.hasChildNodes() && !this._showInlineText(node);
+
+ // The title will be updated in onattach.
+ TreeElement.call(this, "", node, hasChildrenOverride);
+
+ if (this.representedObject.nodeType() == Node.ELEMENT_NODE && !elementCloseTag)
+ this._canAddAttributes = true;
+ this._searchQuery = null;
+ this._expandedChildrenLimit = WebInspector.DOMTreeElement.InitialChildrenLimit;
+}
+
+WebInspector.DOMTreeElement.InitialChildrenLimit = 500;
+WebInspector.DOMTreeElement.MaximumInlineTextChildLength = 80;
+
+// 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.DOMTreeElement.ForbiddenClosingTagElements = [
+ "area", "base", "basefont", "br", "canvas", "col", "command", "embed", "frame",
+ "hr", "img", "input", "isindex", "keygen", "link", "meta", "param", "source"
+].keySet();
+
+// These tags we do not allow editing their tag name.
+WebInspector.DOMTreeElement.EditTagBlacklist = [
+ "html", "head", "body"
+].keySet();
+
+WebInspector.DOMTreeElement.prototype = {
+ isCloseTag: function()
+ {
+ return this._elementCloseTag;
+ },
+
+ 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":
+ if (entry.node.parentElement)
+ entry.node.parentElement.removeChild(entry.node);
+ break;
+ case "changed":
+ entry.node.textContent = entry.oldText;
+ break;
+ }
+ }
+
+ var updater = show ? updateEntryShow : updateEntryHide;
+
+ for (var i = 0, size = this._highlightResult.length; i < size; ++i)
+ updater(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;
+ },
+
+ showChild: function(index)
+ {
+ if (this._elementCloseTag)
+ return;
+
+ 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;
+ },
+
+ _createTooltipForNode: function()
+ {
+ var node = /** @type {WebInspector.DOMNode} */ this.representedObject;
+ if (!node.nodeName() || node.nodeName().toLowerCase() !== "img")
+ return;
+
+ function setTooltip(result)
+ {
+ if (!result || result.type !== "string")
+ return;
+
+ try {
+ var properties = JSON.parse(result.description);
+ var offsetWidth = properties[0];
+ var offsetHeight = properties[1];
+ var naturalWidth = properties[2];
+ var naturalHeight = properties[3];
+ if (offsetHeight === naturalHeight && offsetWidth === naturalWidth)
+ this.tooltip = WebInspector.UIString("%d \xd7 %d pixels").format(offsetWidth, offsetHeight);
+ else
+ this.tooltip = WebInspector.UIString("%d \xd7 %d pixels (Natural: %d \xd7 %d pixels)").format(offsetWidth, offsetHeight, naturalWidth, naturalHeight);
+ } catch (e) {
+ console.error(e);
+ }
+ }
+
+ function resolvedNode(object)
+ {
+ if (!object)
+ return;
+
+ function dimensions()
+ {
+ return "[" + this.offsetWidth + "," + this.offsetHeight + "," + this.naturalWidth + "," + this.naturalHeight + "]";
+ }
+
+ object.callFunction(dimensions, undefined, setTooltip.bind(this));
+ object.release();
+ }
+ WebInspector.RemoteObject.resolveNode(node, "", resolvedNode.bind(this));
+ },
+
+ updateSelection: function()
+ {
+ var listItemElement = this.listItemElement;
+ if (!listItemElement)
+ return;
+
+ if (document.body.offsetWidth <= 0) {
+ // The stylesheet hasn't loaded yet or the window is closed,
+ // so we can't calculate what is 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.listItemElement.draggable = true;
+ },
+
+ onpopulate: function()
+ {
+ if (this.children.length || this._showInlineText(this.representedObject) || this._elementCloseTag)
+ return;
+
+ this.updateChildren();
+ },
+
+ expandRecursively: function()
+ {
+ function callback()
+ {
+ TreeElement.prototype.expandRecursively.call(this, Number.MAX_VALUE);
+ }
+
+ this.representedObject.getSubtree(-1, callback.bind(this));
+ },
+
+ /**
+ * @param {boolean=} fullRefresh
+ */
+ updateChildren: function(fullRefresh)
+ {
+ if (this._elementCloseTag)
+ return;
+ this.representedObject.getChildNodes(this._updateChildren.bind(this, fullRefresh));
+ },
+
+ /**
+ * @param {boolean=} closingTag
+ */
+ insertChildElement: function(child, index, closingTag)
+ {
+ var newElement = new WebInspector.DOMTreeElement(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();
+ }
+
+ var treeElement = this;
+ var treeChildIndex = 0;
+ var elementToSelect;
+
+ function updateChildrenOfNode(node)
+ {
+ var treeOutline = treeElement.treeOutline;
+ var child = node.firstChild;
+ while (child) {
+ var currentTreeElement = treeElement.children[treeChildIndex];
+ if (!currentTreeElement || currentTreeElement.representedObject !== child) {
+ // Find any existing element that is later in the children list.
+ var existingTreeElement = null;
+ for (var i = (treeChildIndex + 1), size = treeElement.expandedChildCount; i < size; ++i) {
+ if (treeElement.children[i].representedObject === child) {
+ existingTreeElement = treeElement.children[i];
+ break;
+ }
+ }
+
+ if (existingTreeElement && existingTreeElement.parent === treeElement) {
+ // If an existing element was found and it has the same parent, just move it.
+ treeElement.moveChild(existingTreeElement, treeChildIndex);
+ } else {
+ // No existing element found, insert a new element.
+ if (treeChildIndex < treeElement.expandedChildrenLimit) {
+ var newElement = treeElement.insertChildElement(child, treeChildIndex);
+ if (child === selectedNode)
+ elementToSelect = newElement;
+ if (treeElement.expandedChildCount > treeElement.expandedChildrenLimit)
+ treeElement.expandedChildrenLimit++;
+ }
+ }
+ }
+
+ child = child.nextSibling;
+ ++treeChildIndex;
+ }
+ }
+
+ // 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.representedObject;
+ var currentParentNode = currentNode.parentNode;
+
+ if (currentParentNode === this.representedObject)
+ continue;
+
+ var selectedTreeElement = this.treeOutline.selectedTreeElement;
+ if (selectedTreeElement && (selectedTreeElement === currentChild || selectedTreeElement.hasAncestor(currentChild)))
+ this.select();
+
+ this.removeChildAtIndex(i);
+ }
+
+ updateChildrenOfNode(this.representedObject);
+ this.adjustCollapsedRange();
+
+ var lastChild = this.children.lastValue;
+ if (this.representedObject.nodeType() == Node.ELEMENT_NODE && (!lastChild || !lastChild._elementCloseTag))
+ this.insertChildElement(this.representedObject, 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()
+ {
+ // 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 node = this.representedObject;
+ if (!node.children)
+ return;
+ const childNodeCount = node.children.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(node.children[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)").format(childNodeCount - expandedChildCount);
+ } else if (this.expandAllButtonElement)
+ delete this.expandAllButtonElement;
+ },
+
+ handleLoadAllChildren: function()
+ {
+ this.expandedChildrenLimit = Math.max(this.representedObject.childNodeCount, this.expandedChildrenLimit + WebInspector.DOMTreeElement.InitialChildrenLimit);
+ },
+
+ onexpand: function()
+ {
+ if (this._elementCloseTag)
+ return;
+
+ this.updateTitle();
+ this.treeOutline.updateSelection();
+ },
+
+ oncollapse: function()
+ {
+ if (this._elementCloseTag)
+ return;
+
+ this.updateTitle();
+ this.treeOutline.updateSelection();
+ },
+
+ onreveal: function()
+ {
+ if (this.listItemElement) {
+ var tagSpans = this.listItemElement.getElementsByClassName("html-tag-name");
+ if (tagSpans.length)
+ tagSpans[0].scrollIntoViewIfNeeded(false);
+ else
+ this.listItemElement.scrollIntoViewIfNeeded(false);
+ }
+ },
+
+ onselect: function(treeElement, selectedByUser)
+ {
+ this.treeOutline.suppressRevealAndSelect = true;
+ this.treeOutline.selectDOMNode(this.representedObject, selectedByUser);
+ if (selectedByUser)
+ WebInspector.domTreeManager.highlightDOMNode(this.representedObject.id);
+ this.updateSelection();
+ this.treeOutline.suppressRevealAndSelect = false;
+ },
+
+ ondelete: function()
+ {
+ var startTagTreeElement = this.treeOutline.findTreeElement(this.representedObject);
+ startTagTreeElement ? startTagTreeElement.remove() : this.remove();
+ return true;
+ },
+
+ onenter: function()
+ {
+ // On Enter or Return start editing the first attribute
+ // or create a new attribute on the selected element.
+ if (this.treeOutline.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;
+
+ // Prevent selecting the nearest word on double click.
+ if (event.detail >= 2)
+ event.preventDefault();
+ },
+
+ ondblclick: function(event)
+ {
+ if (this._editing || this._elementCloseTag)
+ return;
+
+ if (this._startEditingTarget(event.target))
+ return;
+
+ if (this.hasChildren && !this.expanded)
+ this.expand();
+ },
+
+ _insertInLastAttributePosition: function(tag, node)
+ {
+ if (tag.getElementsByClassName("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.representedObject)
+ return;
+
+ if (this.representedObject.nodeType() != Node.ELEMENT_NODE && this.representedObject.nodeType() != Node.TEXT_NODE)
+ return false;
+
+ var textNode = eventTarget.enclosingNodeOrSelfWithClass("html-text-node");
+ if (textNode)
+ return this._startEditingTextNode(textNode);
+
+ var attribute = eventTarget.enclosingNodeOrSelfWithClass("html-attribute");
+ if (attribute)
+ return this._startEditingAttribute(attribute, eventTarget);
+
+ var tagName = eventTarget.enclosingNodeOrSelfWithClass("html-tag-name");
+ if (tagName)
+ return this._startEditingTagName(tagName);
+
+ var newAttribute = eventTarget.enclosingNodeOrSelfWithClass("add-attribute");
+ if (newAttribute)
+ return this._addNewAttribute();
+
+ return false;
+ },
+
+ _populateTagContextMenu: function(contextMenu, event)
+ {
+ var attribute = event.target.enclosingNodeOrSelfWithClass("html-attribute");
+ var newAttribute = event.target.enclosingNodeOrSelfWithClass("add-attribute");
+
+ // Add attribute-related actions.
+ contextMenu.appendItem(WebInspector.UIString("Add Attribute"), this._addNewAttribute.bind(this));
+ if (attribute && !newAttribute)
+ contextMenu.appendItem(WebInspector.UIString("Edit Attribute"), this._startEditingAttribute.bind(this, attribute, event.target));
+ contextMenu.appendSeparator();
+
+ if (WebInspector.cssStyleManager.canForcePseudoClasses()) {
+ var pseudoSubMenu = contextMenu.appendSubMenuItem(WebInspector.UIString("Forced Pseudo-Classes"));
+ this._populateForcedPseudoStateItems(pseudoSubMenu);
+ contextMenu.appendSeparator();
+ }
+
+ this._populateNodeContextMenu(contextMenu);
+ this.treeOutline._populateContextMenu(contextMenu, this.representedObject);
+ },
+
+ _populateForcedPseudoStateItems: function(subMenu)
+ {
+ var node = this.representedObject;
+ var enabledPseudoClasses = node.enabledPseudoClasses;
+ // These strings don't need to be localized as they are CSS pseudo-classes.
+ WebInspector.CSSStyleManager.ForceablePseudoClasses.forEach(function(pseudoClass) {
+ var label = pseudoClass.capitalize();
+ var enabled = enabledPseudoClasses.contains(pseudoClass);
+ subMenu.appendCheckboxItem(label, function() {
+ node.setPseudoClassEnabled(pseudoClass, !enabled);
+ }, enabled, false);
+ });
+ },
+
+ _populateTextContextMenu: function(contextMenu, textNode)
+ {
+ contextMenu.appendItem(WebInspector.UIString("Edit Text"), this._startEditingTextNode.bind(this, textNode));
+ this._populateNodeContextMenu(contextMenu);
+ },
+
+ _populateNodeContextMenu: function(contextMenu)
+ {
+ // Add free-form node-related actions.
+ contextMenu.appendItem(WebInspector.UIString("Edit as HTML"), this._editAsHTML.bind(this));
+ contextMenu.appendItem(WebInspector.UIString("Copy as HTML"), this._copyHTML.bind(this));
+ contextMenu.appendItem(WebInspector.UIString("Delete Node"), this.remove.bind(this));
+ },
+
+ _startEditing: function()
+ {
+ if (this.treeOutline.selectedDOMNode() !== this.representedObject)
+ return;
+
+ var listItem = this._listItemNode;
+
+ if (this._canAddAttributes) {
+ var attribute = listItem.getElementsByClassName("html-attribute")[0];
+ if (attribute)
+ return this._startEditingAttribute(attribute, attribute.getElementsByClassName("html-attribute-value")[0]);
+
+ return this._addNewAttribute();
+ }
+
+ if (this.representedObject.nodeType() === Node.TEXT_NODE) {
+ var textNode = listItem.getElementsByClassName("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.firstChild;
+ attr.style.marginLeft = "2px"; // overrides the .editing margin rule
+ attr.style.marginRight = "2px"; // overrides the .editing margin rule
+
+ var tag = this.listItemElement.getElementsByClassName("html-tag")[0];
+ this._insertInLastAttributePosition(tag, attr);
+ return this._startEditingAttribute(attr, attr);
+ },
+
+ _triggerEditAttribute: function(attributeName)
+ {
+ var attributeElements = this.listItemElement.getElementsByClassName("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("html-attribute-value"))
+ return this._startEditingAttribute(elem.parentNode, elem);
+ }
+ }
+ }
+ },
+
+ _startEditingAttribute: function(attribute, elementForSelection)
+ {
+ if (WebInspector.isBeingEdited(attribute))
+ return true;
+
+ var attributeNameElement = attribute.getElementsByClassName("html-attribute-name")[0];
+ if (!attributeNameElement)
+ return false;
+
+ var attributeName = attributeNameElement.textContent;
+
+ 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);
+ }
+
+ // Remove zero-width spaces that were added by nodeTitleInfo.
+ removeZeroWidthSpaceRecursive(attribute);
+
+ var config = new WebInspector.EditingConfig(this._attributeEditingCommitted.bind(this), this._editingCancelled.bind(this), attributeName);
+ this._editing = WebInspector.startEditing(attribute, config);
+
+ window.getSelection().setBaseAndExtent(elementForSelection, 0, elementForSelection, 1);
+
+ return true;
+ },
+
+ _startEditingTextNode: function(textNode)
+ {
+ if (WebInspector.isBeingEdited(textNode))
+ return true;
+
+ var config = new WebInspector.EditingConfig(this._textNodeEditingCommitted.bind(this), this._editingCancelled.bind(this));
+ config.spellcheck = true;
+ this._editing = WebInspector.startEditing(textNode, config);
+ window.getSelection().setBaseAndExtent(textNode, 0, textNode, 1);
+
+ return true;
+ },
+
+ _startEditingTagName: function(tagNameElement)
+ {
+ if (!tagNameElement) {
+ tagNameElement = this.listItemElement.getElementsByClassName("html-tag-name")[0];
+ if (!tagNameElement)
+ return false;
+ }
+
+ var tagName = tagNameElement.textContent;
+ if (WebInspector.DOMTreeElement.EditTagBlacklist[tagName.toLowerCase()])
+ return false;
+
+ if (WebInspector.isBeingEdited(tagNameElement))
+ return true;
+
+ var closingTagElement = this._distinctClosingTagElement();
+
+ function keyupListener(event)
+ {
+ if (closingTagElement)
+ closingTagElement.textContent = "</" + tagNameElement.textContent + ">";
+ }
+
+ function editingComitted(element, newTagName)
+ {
+ tagNameElement.removeEventListener('keyup', keyupListener, false);
+ this._tagNameEditingCommitted.apply(this, arguments);
+ }
+
+ function editingCancelled()
+ {
+ tagNameElement.removeEventListener('keyup', keyupListener, false);
+ this._editingCancelled.apply(this, arguments);
+ }
+
+ tagNameElement.addEventListener('keyup', keyupListener, false);
+
+ var config = new WebInspector.EditingConfig(editingComitted.bind(this), editingCancelled.bind(this), tagName);
+ this._editing = WebInspector.startEditing(tagNameElement, config);
+ window.getSelection().setBaseAndExtent(tagNameElement, 0, tagNameElement, 1);
+ return true;
+ },
+
+ _startEditingAsHTML: function(commitCallback, error, initialValue)
+ {
+ if (error)
+ return;
+ if (this._htmlEditElement && WebInspector.isBeingEdited(this._htmlEditElement))
+ return;
+
+ this._htmlEditElement = document.createElement("div");
+ this._htmlEditElement.className = "source-code elements-tree-editor";
+ this._htmlEditElement.textContent = initialValue;
+
+ // 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.updateSelection();
+
+ function commit()
+ {
+ commitCallback(this._htmlEditElement.textContent);
+ dispose.call(this);
+ }
+
+ function dispose()
+ {
+ this._editing = false;
+
+ // 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.updateSelection();
+ }
+
+ var config = new WebInspector.EditingConfig(commit.bind(this), dispose.bind(this));
+ config.setMultiline(true);
+ this._editing = WebInspector.startEditing(this._htmlEditElement, config);
+ },
+
+ _attributeEditingCommitted: function(element, newText, oldText, attributeName, moveDirection)
+ {
+ this._editing = false;
+
+ var treeOutline = this.treeOutline;
+ 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.representedObject.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)
+ this._triggerEditAttribute(attributes.lastValue.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();
+ }
+ }
+
+ this.representedObject.setAttribute(attributeName, newText, moveToNextAttributeIfNeeded.bind(this));
+ },
+
+ _tagNameEditingCommitted: function(element, newText, oldText, tagName, moveDirection)
+ {
+ this._editing = false;
+ var self = this;
+
+ function cancel()
+ {
+ var closingTagElement = self._distinctClosingTagElement();
+ if (closingTagElement)
+ closingTagElement.textContent = "</" + tagName + ">";
+
+ self._editingCancelled(element, tagName);
+ moveToNextAttributeIfNeeded.call(self);
+ }
+
+ function moveToNextAttributeIfNeeded()
+ {
+ if (moveDirection !== "forward") {
+ this._addNewAttribute();
+ return;
+ }
+
+ var attributes = this.representedObject.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 node = WebInspector.domTreeManager.nodeForId(nodeId);
+
+ // Select it and expand if necessary. We force tree update so that it processes dom events and is up to date.
+ treeOutline._updateModifiedNodes();
+ treeOutline.selectDOMNode(node, true);
+
+ var newTreeItem = treeOutline.findTreeElement(node);
+ if (wasExpanded)
+ newTreeItem.expand();
+
+ moveToNextAttributeIfNeeded.call(newTreeItem);
+ }
+
+ this.representedObject.setNodeName(newText, changeTagNameCallback);
+ },
+
+ _textNodeEditingCommitted: function(element, newText)
+ {
+ this._editing = false;
+
+ var textNode;
+ if (this.representedObject.nodeType() === Node.ELEMENT_NODE) {
+ // We only show text nodes inline in elements if the element only
+ // has a single child, and that child is a text node.
+ textNode = this.representedObject.firstChild;
+ } else if (this.representedObject.nodeType() == Node.TEXT_NODE)
+ textNode = this.representedObject;
+
+ textNode.setNodeValue(newText, this.updateTitle.bind(this));
+ },
+
+ _editingCancelled: function(element, context)
+ {
+ this._editing = false;
+
+ // Need to restore attributes structure.
+ this.updateTitle();
+ },
+
+ _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("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 highlightElement = document.createElement("span");
+ highlightElement.className = "highlight";
+ highlightElement.appendChild(this._nodeTitleInfo().titleDOM);
+ this.title = highlightElement;
+ delete this._highlightResult;
+ }
+
+ delete this.selectionElement;
+ this.updateSelection();
+ this._highlightSearchResults();
+ },
+
+ /**
+ * @param {WebInspector.DOMNode=} node
+ */
+ _buildAttributeDOM: function(parentElement, name, value, node)
+ {
+ var hasText = (value.length > 0);
+ var attrSpanElement = parentElement.createChild("span", "html-attribute");
+ var attrNameElement = attrSpanElement.createChild("span", "html-attribute-name");
+ attrNameElement.textContent = name;
+
+ if (hasText)
+ attrSpanElement.appendChild(document.createTextNode("=\u200B\""));
+
+ if (name === "src" || name === "href") {
+ var baseURL = node.ownerDocument ? node.ownerDocument.documentURL : null;
+ var rewrittenURL = absoluteURL(value, baseURL);
+
+ value = value.insertWordBreakCharacters();
+
+ if (!rewrittenURL) {
+ var attrValueElement = attrSpanElement.createChild("span", "html-attribute-value");
+ attrValueElement.textContent = value;
+ } else {
+ if (value.startsWith("data:"))
+ value = value.trimMiddle(60);
+
+ var linkElement = document.createElement("a");
+ linkElement.href = rewrittenURL;
+ linkElement.textContent = value;
+
+ attrSpanElement.appendChild(linkElement);
+ }
+ } else {
+ value = value.insertWordBreakCharacters();
+ var attrValueElement = attrSpanElement.createChild("span", "html-attribute-value");
+ attrValueElement.textContent = value;
+ }
+
+ if (hasText)
+ attrSpanElement.appendChild(document.createTextNode("\""));
+ },
+
+ _buildTagDOM: function(parentElement, tagName, isClosingTag, isDistinctTreeElement)
+ {
+ var node = /** @type WebInspector.DOMNode */ this.representedObject;
+ var classes = [ "html-tag" ];
+ if (isClosingTag && isDistinctTreeElement)
+ classes.push("close");
+ if (node.isInShadowTree())
+ classes.push("shadow");
+ var tagElement = parentElement.createChild("span", classes.join(" "));
+ tagElement.appendChild(document.createTextNode("<"));
+ var tagNameElement = tagElement.createChild("span", isClosingTag ? "" : "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, node);
+ }
+ }
+ tagElement.appendChild(document.createTextNode(">"));
+ parentElement.appendChild(document.createTextNode("\u200B"));
+ },
+
+ _nodeTitleInfo: function()
+ {
+ var node = this.representedObject;
+ var info = {titleDOM: document.createDocumentFragment(), hasChildren: this.hasChildren};
+
+ switch (node.nodeType()) {
+ case Node.DOCUMENT_FRAGMENT_NODE:
+ var fragmentElement = info.titleDOM.createChild("span", "webkit-html-fragment");
+ if (node.isInShadowTree()) {
+ fragmentElement.textContent = WebInspector.UIString("Shadow Content");
+ fragmentElement.classList.add("shadow");
+ } else
+ fragmentElement.textContent = WebInspector.UIString("Document Fragment");
+ break;
+
+ case Node.ATTRIBUTE_NODE:
+ var value = node.value || "\u200B"; // Zero width space to force showing an empty value.
+ this._buildAttributeDOM(info.titleDOM, node.name, value);
+ break;
+
+ case Node.ELEMENT_NODE:
+ 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);
+
+ var textChild = this._singleTextChild(node);
+ var showInlineText = textChild && textChild.nodeValue().length < WebInspector.DOMTreeElement.MaximumInlineTextChildLength;
+
+ if (!this.expanded && (!showInlineText && (this.treeOutline.isXMLMimeType || !WebInspector.DOMTreeElement.ForbiddenClosingTagElements[tagName]))) {
+ if (this.hasChildren) {
+ var textNodeElement = info.titleDOM.createChild("span", "html-text-node");
+ 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", "html-text-node");
+ var nodeNameLowerCase = node.nodeName().toLowerCase();
+
+ if (nodeNameLowerCase === "script")
+ textNodeElement.appendChild(WebInspector.syntaxHighlightStringAsDocumentFragment(textChild.nodeValue().trim(), "text/javascript"));
+ else if (nodeNameLowerCase === "style")
+ textNodeElement.appendChild(WebInspector.syntaxHighlightStringAsDocumentFragment(textChild.nodeValue().trim(), "text/css"));
+ else
+ textNodeElement.textContent = textChild.nodeValue();
+
+ info.titleDOM.appendChild(document.createTextNode("\u200B"));
+
+ this._buildTagDOM(info.titleDOM, tagName, true, false);
+ info.hasChildren = false;
+ }
+ break;
+
+ case Node.TEXT_NODE:
+ function trimedNodeValue()
+ {
+ // Trim empty lines from the beginning and extra space at the end since most style and script tags begin with a newline
+ // and end with a newline and indentation for the end tag.
+ return node.nodeValue().replace(/^[\n\r]*/, "").replace(/\s*$/, "");
+ }
+
+ if (node.parentNode && node.parentNode.nodeName().toLowerCase() === "script") {
+ var newNode = info.titleDOM.createChild("span", "html-text-node large");
+ newNode.appendChild(WebInspector.syntaxHighlightStringAsDocumentFragment(trimedNodeValue(), "text/javascript"));
+ } else if (node.parentNode && node.parentNode.nodeName().toLowerCase() === "style") {
+ var newNode = info.titleDOM.createChild("span", "html-text-node large");
+ newNode.appendChild(WebInspector.syntaxHighlightStringAsDocumentFragment(trimedNodeValue(), "text/css"));
+ } else {
+ info.titleDOM.appendChild(document.createTextNode("\""));
+ var textNodeElement = info.titleDOM.createChild("span", "html-text-node");
+ textNodeElement.textContent = node.nodeValue();
+ info.titleDOM.appendChild(document.createTextNode("\""));
+ }
+ break;
+
+ case Node.COMMENT_NODE:
+ var commentElement = info.titleDOM.createChild("span", "html-comment");
+ commentElement.appendChild(document.createTextNode("<!--" + node.nodeValue() + "-->"));
+ break;
+
+ case Node.DOCUMENT_TYPE_NODE:
+ var docTypeElement = info.titleDOM.createChild("span", "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", "html-text-node");
+ cdataElement.appendChild(document.createTextNode("<![CDATA[" + node.nodeValue() + "]]>"));
+ break;
+ default:
+ var defaultElement = info.titleDOM.appendChild(document.createTextNode(node.nodeNameInCorrectCase().collapseWhitespace()));
+ }
+
+ return info;
+ },
+
+ _singleTextChild: function(node)
+ {
+ if (!node)
+ return null;
+
+ var firstChild = node.firstChild;
+ if (!firstChild || firstChild.nodeType() !== Node.TEXT_NODE)
+ return null;
+
+ if (node.hasShadowRoots())
+ return null;
+
+ var sibling = firstChild.nextSibling;
+ return sibling ? null : firstChild;
+ },
+
+ _showInlineText: function(node)
+ {
+ if (node.nodeType() === Node.ELEMENT_NODE) {
+ var textChild = this._singleTextChild(node);
+ if (textChild && textChild.nodeValue().length < WebInspector.DOMTreeElement.MaximumInlineTextChildLength)
+ return true;
+ }
+ return false;
+ },
+
+ remove: function()
+ {
+ var parentElement = this.parent;
+ if (!parentElement)
+ return;
+
+ var self = this;
+ function removeNodeCallback(error, removedNodeId)
+ {
+ if (error)
+ return;
+
+ if (!self.parent)
+ return;
+
+ parentElement.removeChild(self);
+ parentElement.adjustCollapsedRange();
+ }
+
+ this.representedObject.removeNode(removeNodeCallback);
+ },
+
+ _editAsHTML: function()
+ {
+ var treeOutline = this.treeOutline;
+ var node = this.representedObject;
+ var parentNode = node.parentNode;
+ var index = node.index;
+ var wasExpanded = this.expanded;
+
+ function selectNode(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.
+ 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();
+ }
+ }
+
+ function commitChange(value)
+ {
+ node.setOuterHTML(value, selectNode);
+ }
+
+ node.getOuterHTML(this._startEditingAsHTML.bind(this, commitChange));
+ },
+
+ _copyHTML: function()
+ {
+ this.representedObject.copyNode();
+ },
+
+ _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({ offset: match.index, length: match[0].length });
+ match = regexObject.exec(text);
+ }
+
+ // Fall back for XPath, etc. matches.
+ if (!matchRanges.length)
+ matchRanges.push({ offset: 0, length: text.length });
+
+ this._highlightResult = [];
+ highlightSearchResults(this.listItemElement, matchRanges, this._highlightResult);
+ }
+}
+
+WebInspector.DOMTreeElement.prototype.__proto__ = TreeElement.prototype;