2 * Copyright (C) 2007, 2008 Apple Inc. All rights reserved.
3 * Copyright (C) 2008 Matt Lilek <webkit@mattlilek.com>
4 * Copyright (C) 2009 Joseph Pecoraro
6 * Redistribution and use in source and binary forms, with or without
7 * modification, are permitted provided that the following conditions
10 * 1. Redistributions of source code must retain the above copyright
11 * notice, this list of conditions and the following disclaimer.
12 * 2. Redistributions in binary form must reproduce the above copyright
13 * notice, this list of conditions and the following disclaimer in the
14 * documentation and/or other materials provided with the distribution.
15 * 3. Neither the name of Apple Computer, Inc. ("Apple") nor the names of
16 * its contributors may be used to endorse or promote products derived
17 * from this software without specific prior written permission.
19 * THIS SOFTWARE IS PROVIDED BY APPLE AND ITS CONTRIBUTORS "AS IS" AND ANY
20 * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
21 * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
22 * DISCLAIMED. IN NO EVENT SHALL APPLE OR ITS CONTRIBUTORS BE LIABLE FOR ANY
23 * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
24 * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
25 * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
26 * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
27 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF
28 * THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
33 * @extends {TreeElement}
34 * @param {!WebInspector.DOMNode} node
35 * @param {boolean=} elementCloseTag
37 WebInspector
.ElementsTreeElement = function(node
, elementCloseTag
)
39 // The title will be updated in onattach.
40 TreeElement
.call(this);
43 this._elementCloseTag
= elementCloseTag
;
45 if (this._node
.nodeType() == Node
.ELEMENT_NODE
&& !elementCloseTag
)
46 this._canAddAttributes
= true;
47 this._searchQuery
= null;
48 this._expandedChildrenLimit
= WebInspector
.ElementsTreeElement
.InitialChildrenLimit
;
51 WebInspector
.ElementsTreeElement
.InitialChildrenLimit
= 500;
53 // A union of HTML4 and HTML5-Draft elements that explicitly
54 // or implicitly (for HTML5) forbid the closing tag.
55 WebInspector
.ElementsTreeElement
.ForbiddenClosingTagElements
= [
56 "area", "base", "basefont", "br", "canvas", "col", "command", "embed", "frame",
57 "hr", "img", "input", "keygen", "link", "menuitem", "meta", "param", "source", "track", "wbr"
60 // These tags we do not allow editing their tag name.
61 WebInspector
.ElementsTreeElement
.EditTagBlacklist
= [
62 "html", "head", "body"
66 * @param {!WebInspector.ElementsTreeElement} treeElement
68 WebInspector
.ElementsTreeElement
.animateOnDOMUpdate = function(treeElement
)
70 var tagName
= treeElement
.listItemElement
.querySelector(".webkit-html-tag-name");
71 WebInspector
.runCSSAnimationOnce(tagName
|| treeElement
.listItemElement
, "dom-update-highlight");
75 * @param {!WebInspector.DOMNode} node
76 * @return {!Array<!WebInspector.DOMNode>}
78 WebInspector
.ElementsTreeElement
.visibleShadowRoots = function(node
)
80 var roots
= node
.shadowRoots();
81 if (roots
.length
&& !WebInspector
.moduleSetting("showUAShadowDOM").get())
82 roots
= roots
.filter(filter
);
85 * @param {!WebInspector.DOMNode} root
89 return root
.shadowRootType() === WebInspector
.DOMNode
.ShadowRootTypes
.Author
;
95 * @param {!WebInspector.DOMNode} node
98 WebInspector
.ElementsTreeElement
.canShowInlineText = function(node
)
100 if (node
.importedDocument() || node
.templateContent() || WebInspector
.ElementsTreeElement
.visibleShadowRoots(node
).length
|| node
.hasPseudoElements())
102 if (node
.nodeType() !== Node
.ELEMENT_NODE
)
104 if (!node
.firstChild
|| node
.firstChild
!== node
.lastChild
|| node
.firstChild
.nodeType() !== Node
.TEXT_NODE
)
106 var textChild
= node
.firstChild
;
107 var maxInlineTextChildLength
= 80;
108 if (textChild
.nodeValue().length
< maxInlineTextChildLength
)
114 * @param {!WebInspector.ContextSubMenuItem} subMenu
115 * @param {!WebInspector.DOMNode} node
117 WebInspector
.ElementsTreeElement
.populateForcedPseudoStateItems = function(subMenu
, node
)
119 const pseudoClasses
= ["active", "hover", "focus", "visited"];
120 var forcedPseudoState
= node
.getUserProperty(WebInspector
.CSSStyleModel
.PseudoStatePropertyName
) || [];
121 for (var i
= 0; i
< pseudoClasses
.length
; ++i
) {
122 var pseudoClassForced
= forcedPseudoState
.indexOf(pseudoClasses
[i
]) >= 0;
123 subMenu
.appendCheckboxItem(":" + pseudoClasses
[i
], setPseudoStateCallback
.bind(null, pseudoClasses
[i
], !pseudoClassForced
), pseudoClassForced
, false);
127 * @param {string} pseudoState
128 * @param {boolean} enabled
130 function setPseudoStateCallback(pseudoState
, enabled
)
132 WebInspector
.CSSStyleModel
.fromNode(node
).forcePseudoState(node
, pseudoState
, enabled
);
136 WebInspector
.ElementsTreeElement
.prototype = {
140 isClosingTag: function()
142 return !!this._elementCloseTag
;
146 * @return {!WebInspector.DOMNode}
156 isEditing: function()
158 return !!this._editing
;
162 * @param {string} searchQuery
164 highlightSearchResults: function(searchQuery
)
166 if (this._searchQuery
!== searchQuery
)
167 this._hideSearchHighlight();
169 this._searchQuery
= searchQuery
;
170 this._searchHighlightsVisible
= true;
171 this.updateTitle(null, true);
174 hideSearchHighlights: function()
176 delete this._searchHighlightsVisible
;
177 this._hideSearchHighlight();
180 _hideSearchHighlight: function()
182 if (!this._highlightResult
)
185 function updateEntryHide(entry
)
187 switch (entry
.type
) {
192 entry
.node
.textContent
= entry
.oldText
;
197 for (var i
= (this._highlightResult
.length
- 1); i
>= 0; --i
)
198 updateEntryHide(this._highlightResult
[i
]);
200 delete this._highlightResult
;
204 * @param {boolean} inClipboard
206 setInClipboard: function(inClipboard
)
208 if (this._inClipboard
=== inClipboard
)
210 this._inClipboard
= inClipboard
;
211 this.listItemElement
.classList
.toggle("in-clipboard", inClipboard
);
216 return this._hovered
;
221 if (this._hovered
=== x
)
226 if (this.listItemElement
) {
228 this.updateSelection();
229 this.listItemElement
.classList
.add("hovered");
231 this.listItemElement
.classList
.remove("hovered");
239 expandedChildrenLimit: function()
241 return this._expandedChildrenLimit
;
245 * @param {number} expandedChildrenLimit
247 setExpandedChildrenLimit: function(expandedChildrenLimit
)
249 this._expandedChildrenLimit
= expandedChildrenLimit
;
252 updateSelection: function()
254 var listItemElement
= this.listItemElement
;
255 if (!listItemElement
)
258 if (!this.selectionElement
) {
259 this.selectionElement
= createElement("div");
260 this.selectionElement
.className
= "selection selected";
261 listItemElement
.insertBefore(this.selectionElement
, listItemElement
.firstChild
);
264 this.selectionElement
.style
.height
= listItemElement
.offsetHeight
+ "px";
272 if (!this._elementCloseTag
)
273 this._node
[this.treeOutline
.treeElementSymbol()] = this;
281 if (this._node
[this.treeOutline
.treeElementSymbol()] === this)
282 this._node
[this.treeOutline
.treeElementSymbol()] = null;
291 this.updateSelection();
292 this.listItemElement
.classList
.add("hovered");
296 this._preventFollowingLinksOnDoubleClick();
297 this.listItemElement
.draggable
= true;
300 _preventFollowingLinksOnDoubleClick: function()
302 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");
306 for (var i
= 0; i
< links
.length
; ++i
)
307 links
[i
].preventFollowOnDoubleClick
= true;
310 onpopulate: function()
312 this.populated
= true;
313 this.treeOutline
.populateTreeElement(this);
316 expandRecursively: function()
319 * @this {WebInspector.ElementsTreeElement}
323 TreeElement
.prototype.expandRecursively
.call(this, Number
.MAX_VALUE
);
326 this._node
.getSubtree(-1, callback
.bind(this));
334 if (this._elementCloseTag
)
338 this.treeOutline
.updateSelection();
341 oncollapse: function()
343 if (this._elementCloseTag
)
347 this.treeOutline
.updateSelection();
355 if (this.listItemElement
) {
356 var tagSpans
= this.listItemElement
.getElementsByClassName("webkit-html-tag-name");
358 tagSpans
[0].scrollIntoViewIfNeeded(true);
360 this.listItemElement
.scrollIntoViewIfNeeded(true);
366 * @param {boolean=} omitFocus
367 * @param {boolean=} selectedByUser
370 select: function(omitFocus
, selectedByUser
)
374 if (selectedByUser
&& this.treeOutline
.handlePickNode(this.title
, this._node
))
376 return TreeElement
.prototype.select
.call(this, omitFocus
, selectedByUser
);
381 * @param {boolean=} selectedByUser
384 onselect: function(selectedByUser
)
386 this.treeOutline
.suppressRevealAndSelect
= true;
387 this.treeOutline
.selectDOMNode(this._node
, selectedByUser
);
389 this._node
.highlight();
390 this.updateSelection();
391 this.treeOutline
.suppressRevealAndSelect
= false;
401 var startTagTreeElement
= this.treeOutline
.findTreeElement(this._node
);
402 startTagTreeElement
? startTagTreeElement
.remove() : this.remove();
412 // On Enter or Return start editing the first attribute
413 // or create a new attribute on the selected element.
417 this._startEditing();
419 // prevent a newline from being immediately inserted
423 selectOnMouseDown: function(event
)
425 TreeElement
.prototype.selectOnMouseDown
.call(this, event
);
430 // Prevent selecting the nearest word on double click.
431 if (event
.detail
>= 2)
432 event
.preventDefault();
439 ondblclick: function(event
)
441 if (this._editing
|| this._elementCloseTag
)
444 if (this._startEditingTarget(/** @type {!Element} */(event
.target
)))
447 if (this.isExpandable() && !this.expanded
)
455 hasEditableNode: function()
457 return !this._node
.isShadowRoot() && !this._node
.ancestorUserAgentShadowRoot();
460 _insertInLastAttributePosition: function(tag
, node
)
462 if (tag
.getElementsByClassName("webkit-html-attribute").length
> 0)
463 tag
.insertBefore(node
, tag
.lastChild
);
465 var nodeName
= tag
.textContent
.match(/^<(.*?)>$/)[1];
466 tag
.textContent
= '';
467 tag
.createTextChild('<' + nodeName
);
468 tag
.appendChild(node
);
469 tag
.createTextChild('>');
472 this.updateSelection();
476 * @param {!Element} eventTarget
479 _startEditingTarget: function(eventTarget
)
481 if (this.treeOutline
.selectedDOMNode() != this._node
)
484 if (this._node
.nodeType() != Node
.ELEMENT_NODE
&& this._node
.nodeType() != Node
.TEXT_NODE
)
487 if (this.treeOutline
.pickNodeMode())
490 var textNode
= eventTarget
.enclosingNodeOrSelfWithClass("webkit-html-text-node");
492 return this._startEditingTextNode(textNode
);
494 var attribute
= eventTarget
.enclosingNodeOrSelfWithClass("webkit-html-attribute");
496 return this._startEditingAttribute(attribute
, eventTarget
);
498 var tagName
= eventTarget
.enclosingNodeOrSelfWithClass("webkit-html-tag-name");
500 return this._startEditingTagName(tagName
);
502 var newAttribute
= eventTarget
.enclosingNodeOrSelfWithClass("add-attribute");
504 return this._addNewAttribute();
510 * @param {!WebInspector.ContextMenu} contextMenu
511 * @param {!Event} event
513 populateTagContextMenu: function(contextMenu
, event
)
515 // Add attribute-related actions.
516 var treeElement
= this._elementCloseTag
? this.treeOutline
.findTreeElement(this._node
) : this;
517 contextMenu
.appendItem(WebInspector
.UIString
.capitalize("Add ^attribute"), treeElement
._addNewAttribute
.bind(treeElement
));
519 var attribute
= event
.target
.enclosingNodeOrSelfWithClass("webkit-html-attribute");
520 var newAttribute
= event
.target
.enclosingNodeOrSelfWithClass("add-attribute");
521 if (attribute
&& !newAttribute
)
522 contextMenu
.appendItem(WebInspector
.UIString
.capitalize("Edit ^attribute"), this._startEditingAttribute
.bind(this, attribute
, event
.target
));
523 contextMenu
.appendSeparator();
524 var pseudoSubMenu
= contextMenu
.appendSubMenuItem(WebInspector
.UIString
.capitalize("Force ^element ^state"));
525 WebInspector
.ElementsTreeElement
.populateForcedPseudoStateItems(pseudoSubMenu
, treeElement
.node());
526 contextMenu
.appendSeparator();
527 this.populateNodeContextMenu(contextMenu
);
528 this.populateScrollIntoView(contextMenu
);
532 * @param {!WebInspector.ContextMenu} contextMenu
534 populateScrollIntoView: function(contextMenu
)
536 contextMenu
.appendSeparator();
537 contextMenu
.appendItem(WebInspector
.UIString
.capitalize("Scroll into ^view"), this._scrollIntoView
.bind(this));
540 populateTextContextMenu: function(contextMenu
, textNode
)
543 contextMenu
.appendItem(WebInspector
.UIString
.capitalize("Edit ^text"), this._startEditingTextNode
.bind(this, textNode
));
544 this.populateNodeContextMenu(contextMenu
);
547 populateNodeContextMenu: function(contextMenu
)
549 // Add free-form node-related actions.
550 var openTagElement
= this._node
[this.treeOutline
.treeElementSymbol()] || this;
551 var isEditable
= this.hasEditableNode();
552 if (isEditable
&& !this._editing
)
553 contextMenu
.appendItem(WebInspector
.UIString("Edit as HTML"), openTagElement
.toggleEditAsHTML
.bind(openTagElement
));
554 var isShadowRoot
= this._node
.isShadowRoot();
556 // Place it here so that all "Copy"-ing items stick together.
557 if (this._node
.nodeType() === Node
.ELEMENT_NODE
)
558 contextMenu
.appendItem(WebInspector
.UIString
.capitalize("Copy CSS ^path"), this._copyCSSPath
.bind(this));
560 contextMenu
.appendItem(WebInspector
.UIString("Copy XPath"), this._copyXPath
.bind(this));
562 var treeOutline
= this.treeOutline
;
563 contextMenu
.appendSeparator();
564 contextMenu
.appendItem(WebInspector
.UIString("Cut"), treeOutline
.performCopyOrCut
.bind(treeOutline
, true, this._node
), !this.hasEditableNode());
565 contextMenu
.appendItem(WebInspector
.UIString("Copy"), treeOutline
.performCopyOrCut
.bind(treeOutline
, false, this._node
));
566 contextMenu
.appendItem(WebInspector
.UIString("Paste"), treeOutline
.pasteNode
.bind(treeOutline
, this._node
), !treeOutline
.canPaste(this._node
));
570 contextMenu
.appendItem(WebInspector
.UIString("Delete"), this.remove
.bind(this));
571 contextMenu
.appendSeparator();
574 _startEditing: function()
576 if (this.treeOutline
.selectedDOMNode() !== this._node
)
579 var listItem
= this._listItemNode
;
581 if (this._canAddAttributes
) {
582 var attribute
= listItem
.getElementsByClassName("webkit-html-attribute")[0];
584 return this._startEditingAttribute(attribute
, attribute
.getElementsByClassName("webkit-html-attribute-value")[0]);
586 return this._addNewAttribute();
589 if (this._node
.nodeType() === Node
.TEXT_NODE
) {
590 var textNode
= listItem
.getElementsByClassName("webkit-html-text-node")[0];
592 return this._startEditingTextNode(textNode
);
597 _addNewAttribute: function()
599 // Cannot just convert the textual html into an element without
600 // a parent node. Use a temporary span container for the HTML.
601 var container
= createElement("span");
602 this._buildAttributeDOM(container
, " ", "", null);
603 var attr
= container
.firstElementChild
;
604 attr
.style
.marginLeft
= "2px"; // overrides the .editing margin rule
605 attr
.style
.marginRight
= "2px"; // overrides the .editing margin rule
607 var tag
= this.listItemElement
.getElementsByClassName("webkit-html-tag")[0];
608 this._insertInLastAttributePosition(tag
, attr
);
609 attr
.scrollIntoViewIfNeeded(true);
610 return this._startEditingAttribute(attr
, attr
);
613 _triggerEditAttribute: function(attributeName
)
615 var attributeElements
= this.listItemElement
.getElementsByClassName("webkit-html-attribute-name");
616 for (var i
= 0, len
= attributeElements
.length
; i
< len
; ++i
) {
617 if (attributeElements
[i
].textContent
=== attributeName
) {
618 for (var elem
= attributeElements
[i
].nextSibling
; elem
; elem
= elem
.nextSibling
) {
619 if (elem
.nodeType
!== Node
.ELEMENT_NODE
)
622 if (elem
.classList
.contains("webkit-html-attribute-value"))
623 return this._startEditingAttribute(elem
.parentNode
, elem
);
629 _startEditingAttribute: function(attribute
, elementForSelection
)
631 console
.assert(this.listItemElement
.isAncestor(attribute
));
633 if (WebInspector
.isBeingEdited(attribute
))
636 var attributeNameElement
= attribute
.getElementsByClassName("webkit-html-attribute-name")[0];
637 if (!attributeNameElement
)
640 var attributeName
= attributeNameElement
.textContent
;
641 var attributeValueElement
= attribute
.getElementsByClassName("webkit-html-attribute-value")[0];
643 // Make sure elementForSelection is not a child of attributeValueElement.
644 elementForSelection
= attributeValueElement
.isAncestor(elementForSelection
) ? attributeValueElement
: elementForSelection
;
646 function removeZeroWidthSpaceRecursive(node
)
648 if (node
.nodeType
=== Node
.TEXT_NODE
) {
649 node
.nodeValue
= node
.nodeValue
.replace(/\u200B/g, "");
653 if (node
.nodeType
!== Node
.ELEMENT_NODE
)
656 for (var child
= node
.firstChild
; child
; child
= child
.nextSibling
)
657 removeZeroWidthSpaceRecursive(child
);
660 var attributeValue
= attributeName
&& attributeValueElement
? this._node
.getAttribute(attributeName
) : undefined;
661 if (attributeValue
!== undefined)
662 attributeValueElement
.setTextContentTruncatedIfNeeded(attributeValue
, WebInspector
.UIString("<value is too large to edit>"));
664 // Remove zero-width spaces that were added by nodeTitleInfo.
665 removeZeroWidthSpaceRecursive(attribute
);
667 var config
= new WebInspector
.InplaceEditor
.Config(this._attributeEditingCommitted
.bind(this), this._editingCancelled
.bind(this), attributeName
);
670 * @param {!Event} event
673 function postKeyDownFinishHandler(event
)
675 WebInspector
.handleElementValueModifications(event
, attribute
);
678 config
.setPostKeydownFinishHandler(postKeyDownFinishHandler
);
680 this._editing
= WebInspector
.InplaceEditor
.startEditing(attribute
, config
);
682 this.listItemElement
.getComponentSelection().setBaseAndExtent(elementForSelection
, 0, elementForSelection
, 1);
688 * @param {!Element} textNodeElement
690 _startEditingTextNode: function(textNodeElement
)
692 if (WebInspector
.isBeingEdited(textNodeElement
))
695 var textNode
= this._node
;
696 // We only show text nodes inline in elements if the element only
697 // has a single child, and that child is a text node.
698 if (textNode
.nodeType() === Node
.ELEMENT_NODE
&& textNode
.firstChild
)
699 textNode
= textNode
.firstChild
;
701 var container
= textNodeElement
.enclosingNodeOrSelfWithClass("webkit-html-text-node");
703 container
.textContent
= textNode
.nodeValue(); // Strip the CSS or JS highlighting if present.
704 var config
= new WebInspector
.InplaceEditor
.Config(this._textNodeEditingCommitted
.bind(this, textNode
), this._editingCancelled
.bind(this));
705 this._editing
= WebInspector
.InplaceEditor
.startEditing(textNodeElement
, config
);
706 this.listItemElement
.getComponentSelection().setBaseAndExtent(textNodeElement
, 0, textNodeElement
, 1);
712 * @param {!Element=} tagNameElement
714 _startEditingTagName: function(tagNameElement
)
716 if (!tagNameElement
) {
717 tagNameElement
= this.listItemElement
.getElementsByClassName("webkit-html-tag-name")[0];
722 var tagName
= tagNameElement
.textContent
;
723 if (WebInspector
.ElementsTreeElement
.EditTagBlacklist
[tagName
.toLowerCase()])
726 if (WebInspector
.isBeingEdited(tagNameElement
))
729 var closingTagElement
= this._distinctClosingTagElement();
732 * @param {!Event} event
734 function keyupListener(event
)
736 if (closingTagElement
)
737 closingTagElement
.textContent
= "</" + tagNameElement
.textContent
+ ">";
741 * @param {!Element} element
742 * @param {string} newTagName
743 * @this {WebInspector.ElementsTreeElement}
745 function editingComitted(element
, newTagName
)
747 tagNameElement
.removeEventListener('keyup', keyupListener
, false);
748 this._tagNameEditingCommitted
.apply(this, arguments
);
752 * @this {WebInspector.ElementsTreeElement}
754 function editingCancelled()
756 tagNameElement
.removeEventListener('keyup', keyupListener
, false);
757 this._editingCancelled
.apply(this, arguments
);
760 tagNameElement
.addEventListener('keyup', keyupListener
, false);
762 var config
= new WebInspector
.InplaceEditor
.Config(editingComitted
.bind(this), editingCancelled
.bind(this), tagName
);
763 this._editing
= WebInspector
.InplaceEditor
.startEditing(tagNameElement
, config
);
764 this.listItemElement
.getComponentSelection().setBaseAndExtent(tagNameElement
, 0, tagNameElement
, 1);
769 * @param {function(string, string)} commitCallback
770 * @param {?Protocol.Error} error
771 * @param {string} initialValue
773 _startEditingAsHTML: function(commitCallback
, error
, initialValue
)
780 function consume(event
)
782 if (event
.eventPhase
=== Event
.AT_TARGET
)
786 initialValue
= this._convertWhitespaceToEntities(initialValue
).text
;
788 this._htmlEditElement
= createElement("div");
789 this._htmlEditElement
.className
= "source-code elements-tree-editor";
791 // Hide header items.
792 var child
= this.listItemElement
.firstChild
;
794 child
.style
.display
= "none";
795 child
= child
.nextSibling
;
797 // Hide children item.
798 if (this._childrenListNode
)
799 this._childrenListNode
.style
.display
= "none";
801 this.listItemElement
.appendChild(this._htmlEditElement
);
802 this.treeOutline
.element
.addEventListener("mousedown", consume
, false);
804 this.updateSelection();
807 * @param {!Element} element
808 * @param {string} newValue
809 * @this {WebInspector.ElementsTreeElement}
811 function commit(element
, newValue
)
813 commitCallback(initialValue
, newValue
);
818 * @this {WebInspector.ElementsTreeElement}
822 delete this._editing
;
823 this.treeOutline
.setMultilineEditing(null);
826 this.listItemElement
.removeChild(this._htmlEditElement
);
827 delete this._htmlEditElement
;
828 // Unhide children item.
829 if (this._childrenListNode
)
830 this._childrenListNode
.style
.removeProperty("display");
831 // Unhide header items.
832 var child
= this.listItemElement
.firstChild
;
834 child
.style
.removeProperty("display");
835 child
= child
.nextSibling
;
838 this.treeOutline
.element
.removeEventListener("mousedown", consume
, false);
839 this.updateSelection();
840 this.treeOutline
.focus();
843 var config
= new WebInspector
.InplaceEditor
.Config(commit
.bind(this), dispose
.bind(this));
844 config
.setMultilineOptions(initialValue
, { name
: "xml", htmlMode
: true }, "web-inspector-html", WebInspector
.moduleSetting("domWordWrap").get(), true);
845 WebInspector
.InplaceEditor
.startMultilineEditing(this._htmlEditElement
, config
).then(markAsBeingEdited
.bind(this));
848 * @param {!Object} controller
849 * @this {WebInspector.ElementsTreeElement}
851 function markAsBeingEdited(controller
)
853 this._editing
= /** @type {!WebInspector.InplaceEditor.Controller} */ (controller
);
854 this._editing
.setWidth(this.treeOutline
.visibleWidth());
855 this.treeOutline
.setMultilineEditing(this._editing
);
859 _attributeEditingCommitted: function(element
, newText
, oldText
, attributeName
, moveDirection
)
861 delete this._editing
;
863 var treeOutline
= this.treeOutline
;
866 * @param {?Protocol.Error=} error
867 * @this {WebInspector.ElementsTreeElement}
869 function moveToNextAttributeIfNeeded(error
)
872 this._editingCancelled(element
, attributeName
);
877 treeOutline
.runPendingUpdates();
879 // Search for the attribute's position, and then decide where to move to.
880 var attributes
= this._node
.attributes();
881 for (var i
= 0; i
< attributes
.length
; ++i
) {
882 if (attributes
[i
].name
!== attributeName
)
885 if (moveDirection
=== "backward") {
887 this._startEditingTagName();
889 this._triggerEditAttribute(attributes
[i
- 1].name
);
891 if (i
=== attributes
.length
- 1)
892 this._addNewAttribute();
894 this._triggerEditAttribute(attributes
[i
+ 1].name
);
899 // Moving From the "New Attribute" position.
900 if (moveDirection
=== "backward") {
901 if (newText
=== " ") {
902 // Moving from "New Attribute" that was not edited
903 if (attributes
.length
> 0)
904 this._triggerEditAttribute(attributes
[attributes
.length
- 1].name
);
906 // Moving from "New Attribute" that holds new value
907 if (attributes
.length
> 1)
908 this._triggerEditAttribute(attributes
[attributes
.length
- 2].name
);
910 } else if (moveDirection
=== "forward") {
911 if (!/^\s*$/.test(newText
))
912 this._addNewAttribute();
914 this._startEditingTagName();
919 if ((attributeName
.trim() || newText
.trim()) && oldText
!== newText
) {
920 this._node
.setAttribute(attributeName
, newText
, moveToNextAttributeIfNeeded
.bind(this));
925 moveToNextAttributeIfNeeded
.call(this);
928 _tagNameEditingCommitted: function(element
, newText
, oldText
, tagName
, moveDirection
)
930 delete this._editing
;
935 var closingTagElement
= self
._distinctClosingTagElement();
936 if (closingTagElement
)
937 closingTagElement
.textContent
= "</" + tagName
+ ">";
939 self
._editingCancelled(element
, tagName
);
940 moveToNextAttributeIfNeeded
.call(self
);
944 * @this {WebInspector.ElementsTreeElement}
946 function moveToNextAttributeIfNeeded()
948 if (moveDirection
!== "forward") {
949 this._addNewAttribute();
953 var attributes
= this._node
.attributes();
954 if (attributes
.length
> 0)
955 this._triggerEditAttribute(attributes
[0].name
);
957 this._addNewAttribute();
960 newText
= newText
.trim();
961 if (newText
=== oldText
) {
966 var treeOutline
= this.treeOutline
;
967 var wasExpanded
= this.expanded
;
969 function changeTagNameCallback(error
, nodeId
)
971 if (error
|| !nodeId
) {
975 var newTreeItem
= treeOutline
.selectNodeAfterEdit(wasExpanded
, error
, nodeId
);
976 moveToNextAttributeIfNeeded
.call(newTreeItem
);
978 this._node
.setNodeName(newText
, changeTagNameCallback
);
982 * @param {!WebInspector.DOMNode} textNode
983 * @param {!Element} element
984 * @param {string} newText
986 _textNodeEditingCommitted: function(textNode
, element
, newText
)
988 delete this._editing
;
991 * @this {WebInspector.ElementsTreeElement}
997 textNode
.setNodeValue(newText
, callback
.bind(this));
1001 * @param {!Element} element
1002 * @param {*} context
1004 _editingCancelled: function(element
, context
)
1006 delete this._editing
;
1008 // Need to restore attributes structure.
1013 * @return {!Element}
1015 _distinctClosingTagElement: function()
1017 // FIXME: Improve the Tree Element / Outline Abstraction to prevent crawling the DOM
1019 // For an expanded element, it will be the last element with class "close"
1020 // in the child element list.
1021 if (this.expanded
) {
1022 var closers
= this._childrenListNode
.querySelectorAll(".close");
1023 return closers
[closers
.length
-1];
1026 // Remaining cases are single line non-expanded elements with a closing
1027 // tag, or HTML elements without a closing tag (such as <br>). Return
1028 // null in the case where there isn't a closing tag.
1029 var tags
= this.listItemElement
.getElementsByClassName("webkit-html-tag");
1030 return (tags
.length
=== 1 ? null : tags
[tags
.length
-1]);
1034 * @param {?WebInspector.ElementsTreeOutline.UpdateRecord=} updateRecord
1035 * @param {boolean=} onlySearchQueryChanged
1037 updateTitle: function(updateRecord
, onlySearchQueryChanged
)
1039 // If we are editing, return early to prevent canceling the edit.
1040 // After editing is committed updateTitle will be called.
1044 if (onlySearchQueryChanged
) {
1045 this._hideSearchHighlight();
1047 var nodeInfo
= this._nodeTitleInfo(updateRecord
|| null);
1048 if (this._node
.nodeType() === Node
.DOCUMENT_FRAGMENT_NODE
&& this._node
.isInShadowTree() && this._node
.shadowRootType()) {
1049 this.childrenListElement
.classList
.add("shadow-root");
1051 for (var node
= this._node
; depth
&& node
; node
= node
.parentNode
) {
1052 if (node
.nodeType() === Node
.DOCUMENT_FRAGMENT_NODE
)
1056 this.childrenListElement
.classList
.add("shadow-root-deep");
1058 this.childrenListElement
.classList
.add("shadow-root-depth-" + depth
);
1060 var highlightElement
= createElement("span");
1061 highlightElement
.className
= "highlight";
1062 highlightElement
.appendChild(nodeInfo
);
1063 this.title
= highlightElement
;
1064 this._updateDecorations();
1065 delete this._highlightResult
;
1068 delete this.selectionElement
;
1070 this.updateSelection();
1071 this._preventFollowingLinksOnDoubleClick();
1072 this._highlightSearchResults();
1076 * @return {?Element}
1078 _createDecoratorElement: function()
1080 var node
= this._node
;
1081 var decoratorMessages
= [];
1082 var parentDecoratorMessages
= [];
1083 var decorators
= this.treeOutline
.nodeDecorators();
1084 for (var i
= 0; i
< decorators
.length
; ++i
) {
1085 var decorator
= decorators
[i
];
1086 var message
= decorator
.decorate(node
);
1088 decoratorMessages
.push(message
);
1092 if (this.expanded
|| this._elementCloseTag
)
1095 message
= decorator
.decorateAncestor(node
);
1097 parentDecoratorMessages
.push(message
)
1099 if (!decoratorMessages
.length
&& !parentDecoratorMessages
.length
)
1102 var decoratorElement
= createElement("div");
1103 decoratorElement
.classList
.add("elements-gutter-decoration");
1104 if (!decoratorMessages
.length
)
1105 decoratorElement
.classList
.add("elements-has-decorated-children");
1106 decoratorElement
.title
= decoratorMessages
.concat(parentDecoratorMessages
).join("\n");
1107 return decoratorElement
;
1110 _updateDecorations: function()
1112 if (this._decoratorElement
)
1113 this._decoratorElement
.remove();
1114 this._decoratorElement
= this._createDecoratorElement();
1115 if (this._decoratorElement
&& this.listItemElement
)
1116 this.listItemElement
.insertBefore(this._decoratorElement
, this.listItemElement
.firstChild
);
1120 * @param {!Node} parentElement
1121 * @param {string} name
1122 * @param {string} value
1123 * @param {?WebInspector.ElementsTreeOutline.UpdateRecord} updateRecord
1124 * @param {boolean=} forceValue
1125 * @param {!WebInspector.DOMNode=} node
1127 _buildAttributeDOM: function(parentElement
, name
, value
, updateRecord
, forceValue
, node
)
1129 var closingPunctuationRegex
= /[\/;:\)\]\}]/g;
1130 var highlightIndex
= 0;
1132 var additionalHighlightOffset
= 0;
1136 * @param {string} match
1137 * @param {number} replaceOffset
1140 function replacer(match
, replaceOffset
) {
1141 while (highlightIndex
< highlightCount
&& result
.entityRanges
[highlightIndex
].offset
< replaceOffset
) {
1142 result
.entityRanges
[highlightIndex
].offset
+= additionalHighlightOffset
;
1145 additionalHighlightOffset
+= 1;
1146 return match
+ "\u200B";
1150 * @param {!Element} element
1151 * @param {string} value
1152 * @this {WebInspector.ElementsTreeElement}
1154 function setValueWithEntities(element
, value
)
1156 result
= this._convertWhitespaceToEntities(value
);
1157 highlightCount
= result
.entityRanges
.length
;
1158 value
= result
.text
.replace(closingPunctuationRegex
, replacer
);
1159 while (highlightIndex
< highlightCount
) {
1160 result
.entityRanges
[highlightIndex
].offset
+= additionalHighlightOffset
;
1163 element
.setTextContentTruncatedIfNeeded(value
);
1164 WebInspector
.highlightRangesWithStyleClass(element
, result
.entityRanges
, "webkit-html-entity-value");
1167 var hasText
= (forceValue
|| value
.length
> 0);
1168 var attrSpanElement
= parentElement
.createChild("span", "webkit-html-attribute");
1169 var attrNameElement
= attrSpanElement
.createChild("span", "webkit-html-attribute-name");
1170 attrNameElement
.textContent
= name
;
1173 attrSpanElement
.createTextChild("=\u200B\"");
1175 var attrValueElement
= attrSpanElement
.createChild("span", "webkit-html-attribute-value");
1177 if (updateRecord
&& updateRecord
.isAttributeModified(name
))
1178 WebInspector
.runCSSAnimationOnce(hasText
? attrValueElement
: attrNameElement
, "dom-update-highlight");
1181 * @this {WebInspector.ElementsTreeElement}
1182 * @param {string} value
1183 * @return {!Element}
1185 function linkifyValue(value
)
1187 var rewrittenHref
= node
.resolveURL(value
);
1188 if (rewrittenHref
=== null) {
1189 var span
= createElement("span");
1190 setValueWithEntities
.call(this, span
, value
);
1193 value
= value
.replace(closingPunctuationRegex
, "$&\u200B");
1194 if (value
.startsWith("data:"))
1195 value
= value
.trimMiddle(60);
1196 var anchor
= WebInspector
.linkifyURLAsNode(rewrittenHref
, value
, "", node
.nodeName().toLowerCase() === "a");
1197 anchor
.preventFollow
= true;
1201 if (node
&& name
=== "src" || name
=== "href") {
1202 attrValueElement
.appendChild(linkifyValue
.call(this, value
));
1203 } else if (node
&& node
.nodeName().toLowerCase() === "img" && name
=== "srcset") {
1204 var sources
= value
.split(",");
1205 for (var i
= 0; i
< sources
.length
; ++i
) {
1207 attrValueElement
.createTextChild(", ");
1208 var source
= sources
[i
].trim();
1209 var indexOfSpace
= source
.indexOf(" ");
1210 var url
= source
.substring(0, indexOfSpace
);
1211 var tail
= source
.substring(indexOfSpace
);
1212 attrValueElement
.appendChild(linkifyValue
.call(this, url
));
1213 attrValueElement
.createTextChild(tail
);
1216 setValueWithEntities
.call(this, attrValueElement
, value
);
1220 attrSpanElement
.createTextChild("\"");
1224 * @param {!Node} parentElement
1225 * @param {string} pseudoElementName
1227 _buildPseudoElementDOM: function(parentElement
, pseudoElementName
)
1229 var pseudoElement
= parentElement
.createChild("span", "webkit-html-pseudo-element");
1230 pseudoElement
.textContent
= "::" + pseudoElementName
;
1231 parentElement
.createTextChild("\u200B");
1235 * @param {!Node} parentElement
1236 * @param {string} tagName
1237 * @param {boolean} isClosingTag
1238 * @param {boolean} isDistinctTreeElement
1239 * @param {?WebInspector.ElementsTreeOutline.UpdateRecord} updateRecord
1241 _buildTagDOM: function(parentElement
, tagName
, isClosingTag
, isDistinctTreeElement
, updateRecord
)
1243 var node
= this._node
;
1244 var classes
= [ "webkit-html-tag" ];
1245 if (isClosingTag
&& isDistinctTreeElement
)
1246 classes
.push("close");
1247 var tagElement
= parentElement
.createChild("span", classes
.join(" "));
1248 tagElement
.createTextChild("<");
1249 var tagNameElement
= tagElement
.createChild("span", isClosingTag
? "" : "webkit-html-tag-name");
1250 tagNameElement
.textContent
= (isClosingTag
? "/" : "") + tagName
;
1251 if (!isClosingTag
) {
1252 if (node
.hasAttributes()) {
1253 var attributes
= node
.attributes();
1254 for (var i
= 0; i
< attributes
.length
; ++i
) {
1255 var attr
= attributes
[i
];
1256 tagElement
.createTextChild(" ");
1257 this._buildAttributeDOM(tagElement
, attr
.name
, attr
.value
, updateRecord
, false, node
);
1261 var hasUpdates
= updateRecord
.hasRemovedAttributes() || updateRecord
.hasRemovedChildren();
1262 hasUpdates
|= !this.expanded
&& updateRecord
.hasChangedChildren();
1264 WebInspector
.runCSSAnimationOnce(tagNameElement
, "dom-update-highlight");
1268 tagElement
.createTextChild(">");
1269 parentElement
.createTextChild("\u200B");
1273 * @param {string} text
1274 * @return {!{text: string, entityRanges: !Array.<!WebInspector.SourceRange>}}
1276 _convertWhitespaceToEntities: function(text
)
1279 var lastIndexAfterEntity
= 0;
1280 var entityRanges
= [];
1281 var charToEntity
= WebInspector
.ElementsTreeOutline
.MappedCharToEntity
;
1282 for (var i
= 0, size
= text
.length
; i
< size
; ++i
) {
1283 var char = text
.charAt(i
);
1284 if (charToEntity
[char]) {
1285 result
+= text
.substring(lastIndexAfterEntity
, i
);
1286 var entityValue
= "&" + charToEntity
[char] + ";";
1287 entityRanges
.push({offset
: result
.length
, length
: entityValue
.length
});
1288 result
+= entityValue
;
1289 lastIndexAfterEntity
= i
+ 1;
1293 result
+= text
.substring(lastIndexAfterEntity
);
1294 return {text
: result
|| text
, entityRanges
: entityRanges
};
1298 * @param {?WebInspector.ElementsTreeOutline.UpdateRecord} updateRecord
1299 * @return {!DocumentFragment} result
1301 _nodeTitleInfo: function(updateRecord
)
1303 var node
= this._node
;
1304 var titleDOM
= createDocumentFragment();
1306 switch (node
.nodeType()) {
1307 case Node
.ATTRIBUTE_NODE
:
1308 this._buildAttributeDOM(titleDOM
, /** @type {string} */ (node
.name
), /** @type {string} */ (node
.value
), updateRecord
, true);
1311 case Node
.ELEMENT_NODE
:
1312 var pseudoType
= node
.pseudoType();
1314 this._buildPseudoElementDOM(titleDOM
, pseudoType
);
1318 var tagName
= node
.nodeNameInCorrectCase();
1319 if (this._elementCloseTag
) {
1320 this._buildTagDOM(titleDOM
, tagName
, true, true, updateRecord
);
1324 this._buildTagDOM(titleDOM
, tagName
, false, false, updateRecord
);
1326 if (this.isExpandable()) {
1327 if (!this.expanded
) {
1328 var textNodeElement
= titleDOM
.createChild("span", "webkit-html-text-node bogus");
1329 textNodeElement
.textContent
= "\u2026";
1330 titleDOM
.createTextChild("\u200B");
1331 this._buildTagDOM(titleDOM
, tagName
, true, false, updateRecord
);
1336 if (WebInspector
.ElementsTreeElement
.canShowInlineText(node
)) {
1337 var textNodeElement
= titleDOM
.createChild("span", "webkit-html-text-node");
1338 var result
= this._convertWhitespaceToEntities(node
.firstChild
.nodeValue());
1339 textNodeElement
.textContent
= result
.text
;
1340 WebInspector
.highlightRangesWithStyleClass(textNodeElement
, result
.entityRanges
, "webkit-html-entity-value");
1341 titleDOM
.createTextChild("\u200B");
1342 this._buildTagDOM(titleDOM
, tagName
, true, false, updateRecord
);
1343 if (updateRecord
&& updateRecord
.hasChangedChildren())
1344 WebInspector
.runCSSAnimationOnce(textNodeElement
, "dom-update-highlight");
1345 if (updateRecord
&& updateRecord
.isCharDataModified())
1346 WebInspector
.runCSSAnimationOnce(textNodeElement
, "dom-update-highlight");
1350 if (this.treeOutline
.isXMLMimeType
|| !WebInspector
.ElementsTreeElement
.ForbiddenClosingTagElements
[tagName
])
1351 this._buildTagDOM(titleDOM
, tagName
, true, false, updateRecord
);
1354 case Node
.TEXT_NODE
:
1355 if (node
.parentNode
&& node
.parentNode
.nodeName().toLowerCase() === "script") {
1356 var newNode
= titleDOM
.createChild("span", "webkit-html-text-node webkit-html-js-node");
1357 newNode
.textContent
= node
.nodeValue();
1359 var javascriptSyntaxHighlighter
= new WebInspector
.DOMSyntaxHighlighter("text/javascript", true);
1360 javascriptSyntaxHighlighter
.syntaxHighlightNode(newNode
).then(updateSearchHighlight
.bind(this));
1361 } else if (node
.parentNode
&& node
.parentNode
.nodeName().toLowerCase() === "style") {
1362 var newNode
= titleDOM
.createChild("span", "webkit-html-text-node webkit-html-css-node");
1363 newNode
.textContent
= node
.nodeValue();
1365 var cssSyntaxHighlighter
= new WebInspector
.DOMSyntaxHighlighter("text/css", true);
1366 cssSyntaxHighlighter
.syntaxHighlightNode(newNode
).then(updateSearchHighlight
.bind(this));
1368 titleDOM
.createTextChild("\"");
1369 var textNodeElement
= titleDOM
.createChild("span", "webkit-html-text-node");
1370 var result
= this._convertWhitespaceToEntities(node
.nodeValue());
1371 textNodeElement
.textContent
= result
.text
;
1372 WebInspector
.highlightRangesWithStyleClass(textNodeElement
, result
.entityRanges
, "webkit-html-entity-value");
1373 titleDOM
.createTextChild("\"");
1374 if (updateRecord
&& updateRecord
.isCharDataModified())
1375 WebInspector
.runCSSAnimationOnce(textNodeElement
, "dom-update-highlight");
1379 case Node
.COMMENT_NODE
:
1380 var commentElement
= titleDOM
.createChild("span", "webkit-html-comment");
1381 commentElement
.createTextChild("<!--" + node
.nodeValue() + "-->");
1384 case Node
.DOCUMENT_TYPE_NODE
:
1385 var docTypeElement
= titleDOM
.createChild("span", "webkit-html-doctype");
1386 docTypeElement
.createTextChild("<!DOCTYPE " + node
.nodeName());
1387 if (node
.publicId
) {
1388 docTypeElement
.createTextChild(" PUBLIC \"" + node
.publicId
+ "\"");
1390 docTypeElement
.createTextChild(" \"" + node
.systemId
+ "\"");
1391 } else if (node
.systemId
)
1392 docTypeElement
.createTextChild(" SYSTEM \"" + node
.systemId
+ "\"");
1394 if (node
.internalSubset
)
1395 docTypeElement
.createTextChild(" [" + node
.internalSubset
+ "]");
1397 docTypeElement
.createTextChild(">");
1400 case Node
.CDATA_SECTION_NODE
:
1401 var cdataElement
= titleDOM
.createChild("span", "webkit-html-text-node");
1402 cdataElement
.createTextChild("<![CDATA[" + node
.nodeValue() + "]]>");
1405 case Node
.DOCUMENT_FRAGMENT_NODE
:
1406 var fragmentElement
= titleDOM
.createChild("span", "webkit-html-fragment");
1407 fragmentElement
.textContent
= node
.nodeNameInCorrectCase().collapseWhitespace();
1410 titleDOM
.createTextChild(node
.nodeNameInCorrectCase().collapseWhitespace());
1414 * @this {WebInspector.ElementsTreeElement}
1416 function updateSearchHighlight()
1418 delete this._highlightResult
;
1419 this._highlightSearchResults();
1427 if (this._node
.pseudoType())
1429 var parentElement
= this.parent
;
1433 if (!this._node
.parentNode
|| this._node
.parentNode
.nodeType() === Node
.DOCUMENT_NODE
)
1435 this._node
.removeNode();
1439 * @param {function(boolean)=} callback
1440 * @param {boolean=} startEditing
1442 toggleEditAsHTML: function(callback
, startEditing
)
1444 if (this._editing
&& this._htmlEditElement
&& WebInspector
.isBeingEdited(this._htmlEditElement
)) {
1445 this._editing
.commit();
1449 if (startEditing
=== false)
1453 * @param {?Protocol.Error} error
1455 function selectNode(error
)
1462 * @param {string} initialValue
1463 * @param {string} value
1465 function commitChange(initialValue
, value
)
1467 if (initialValue
!== value
)
1468 node
.setOuterHTML(value
, selectNode
);
1471 var node
= this._node
;
1472 node
.getOuterHTML(this._startEditingAsHTML
.bind(this, commitChange
));
1475 _copyCSSPath: function()
1477 InspectorFrontendHost
.copyText(WebInspector
.DOMPresentationUtils
.cssPath(this._node
, true));
1480 _copyXPath: function()
1482 InspectorFrontendHost
.copyText(WebInspector
.DOMPresentationUtils
.xPath(this._node
, true));
1485 _highlightSearchResults: function()
1487 if (!this._searchQuery
|| !this._searchHighlightsVisible
)
1489 this._hideSearchHighlight();
1491 var text
= this.listItemElement
.textContent
;
1492 var regexObject
= createPlainTextSearchRegex(this._searchQuery
, "gi");
1494 var match
= regexObject
.exec(text
);
1495 var matchRanges
= [];
1497 matchRanges
.push(new WebInspector
.SourceRange(match
.index
, match
[0].length
));
1498 match
= regexObject
.exec(text
);
1501 // Fall back for XPath, etc. matches.
1502 if (!matchRanges
.length
)
1503 matchRanges
.push(new WebInspector
.SourceRange(0, text
.length
));
1505 this._highlightResult
= [];
1506 WebInspector
.highlightSearchResults(this.listItemElement
, matchRanges
, this._highlightResult
);
1509 _scrollIntoView: function()
1511 function scrollIntoViewCallback(object
)
1514 * @suppressReceiverCheck
1517 function scrollIntoView()
1519 this.scrollIntoViewIfNeeded(true);
1523 object
.callFunction(scrollIntoView
);
1526 this._node
.resolveToObject("", scrollIntoViewCallback
);
1529 __proto__
: TreeElement
.prototype