Devtools: Add force element state menu to the elements toolbar
[chromium-blink-merge.git] / third_party / WebKit / Source / devtools / front_end / elements / ElementsTreeElement.js
blob1c99db89405cca39ccb74dcc479820415bfe46ca
1 /*
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
8 * are met:
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.
31 /**
32 * @constructor
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);
41 this._node = node;
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"
58 ].keySet();
60 // These tags we do not allow editing their tag name.
61 WebInspector.ElementsTreeElement.EditTagBlacklist = [
62 "html", "head", "body"
63 ].keySet();
65 /**
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");
74 /**
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);
84 /**
85 * @param {!WebInspector.DOMNode} root
87 function filter(root)
89 return root.shadowRootType() === WebInspector.DOMNode.ShadowRootTypes.Author;
91 return roots;
94 /**
95 * @param {!WebInspector.DOMNode} node
96 * @return {boolean}
98 WebInspector.ElementsTreeElement.canShowInlineText = function(node)
100 if (node.importedDocument() || node.templateContent() || WebInspector.ElementsTreeElement.visibleShadowRoots(node).length || node.hasPseudoElements())
101 return false;
102 if (node.nodeType() !== Node.ELEMENT_NODE)
103 return false;
104 if (!node.firstChild || node.firstChild !== node.lastChild || node.firstChild.nodeType() !== Node.TEXT_NODE)
105 return false;
106 var textChild = node.firstChild;
107 var maxInlineTextChildLength = 80;
108 if (textChild.nodeValue().length < maxInlineTextChildLength)
109 return true;
110 return false;
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 = {
138 * @return {boolean}
140 isClosingTag: function()
142 return !!this._elementCloseTag;
146 * @return {!WebInspector.DOMNode}
148 node: function()
150 return this._node;
154 * @return {boolean}
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)
183 return;
185 function updateEntryHide(entry)
187 switch (entry.type) {
188 case "added":
189 entry.node.remove();
190 break;
191 case "changed":
192 entry.node.textContent = entry.oldText;
193 break;
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)
209 return;
210 this._inClipboard = inClipboard;
211 this.listItemElement.classList.toggle("in-clipboard", inClipboard);
214 get hovered()
216 return this._hovered;
219 set hovered(x)
221 if (this._hovered === x)
222 return;
224 this._hovered = x;
226 if (this.listItemElement) {
227 if (x) {
228 this.updateSelection();
229 this.listItemElement.classList.add("hovered");
230 } else {
231 this.listItemElement.classList.remove("hovered");
237 * @return {number}
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)
256 return;
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";
268 * @override
270 onbind: function()
272 if (!this._elementCloseTag)
273 this._node[this.treeOutline.treeElementSymbol()] = this;
277 * @override
279 onunbind: function()
281 if (this._node[this.treeOutline.treeElementSymbol()] === this)
282 this._node[this.treeOutline.treeElementSymbol()] = null;
286 * @override
288 onattach: function()
290 if (this._hovered) {
291 this.updateSelection();
292 this.listItemElement.classList.add("hovered");
295 this.updateTitle();
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");
303 if (!links)
304 return;
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}
321 function callback()
323 TreeElement.prototype.expandRecursively.call(this, Number.MAX_VALUE);
326 this._node.getSubtree(-1, callback.bind(this));
330 * @override
332 onexpand: function()
334 if (this._elementCloseTag)
335 return;
337 this.updateTitle();
338 this.treeOutline.updateSelection();
341 oncollapse: function()
343 if (this._elementCloseTag)
344 return;
346 this.updateTitle();
347 this.treeOutline.updateSelection();
351 * @override
353 onreveal: function()
355 if (this.listItemElement) {
356 var tagSpans = this.listItemElement.getElementsByClassName("webkit-html-tag-name");
357 if (tagSpans.length)
358 tagSpans[0].scrollIntoViewIfNeeded(true);
359 else
360 this.listItemElement.scrollIntoViewIfNeeded(true);
365 * @override
366 * @param {boolean=} omitFocus
367 * @param {boolean=} selectedByUser
368 * @return {boolean}
370 select: function(omitFocus, selectedByUser)
372 if (this._editing)
373 return false;
374 if (selectedByUser && this.treeOutline.handlePickNode(this.title, this._node))
375 return true;
376 return TreeElement.prototype.select.call(this, omitFocus, selectedByUser);
380 * @override
381 * @param {boolean=} selectedByUser
382 * @return {boolean}
384 onselect: function(selectedByUser)
386 this.treeOutline.suppressRevealAndSelect = true;
387 this.treeOutline.selectDOMNode(this._node, selectedByUser);
388 if (selectedByUser)
389 this._node.highlight();
390 this.updateSelection();
391 this.treeOutline.suppressRevealAndSelect = false;
392 return true;
396 * @override
397 * @return {boolean}
399 ondelete: function()
401 var startTagTreeElement = this.treeOutline.findTreeElement(this._node);
402 startTagTreeElement ? startTagTreeElement.remove() : this.remove();
403 return true;
407 * @override
408 * @return {boolean}
410 onenter: function()
412 // On Enter or Return start editing the first attribute
413 // or create a new attribute on the selected element.
414 if (this._editing)
415 return false;
417 this._startEditing();
419 // prevent a newline from being immediately inserted
420 return true;
423 selectOnMouseDown: function(event)
425 TreeElement.prototype.selectOnMouseDown.call(this, event);
427 if (this._editing)
428 return;
430 // Prevent selecting the nearest word on double click.
431 if (event.detail >= 2)
432 event.preventDefault();
436 * @override
437 * @return {boolean}
439 ondblclick: function(event)
441 if (this._editing || this._elementCloseTag)
442 return false;
444 if (this._startEditingTarget(/** @type {!Element} */(event.target)))
445 return false;
447 if (this.isExpandable() && !this.expanded)
448 this.expand();
449 return false;
453 * @return {boolean}
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);
464 else {
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
477 * @return {boolean}
479 _startEditingTarget: function(eventTarget)
481 if (this.treeOutline.selectedDOMNode() != this._node)
482 return false;
484 if (this._node.nodeType() != Node.ELEMENT_NODE && this._node.nodeType() != Node.TEXT_NODE)
485 return false;
487 if (this.treeOutline.pickNodeMode())
488 return false;
490 var textNode = eventTarget.enclosingNodeOrSelfWithClass("webkit-html-text-node");
491 if (textNode)
492 return this._startEditingTextNode(textNode);
494 var attribute = eventTarget.enclosingNodeOrSelfWithClass("webkit-html-attribute");
495 if (attribute)
496 return this._startEditingAttribute(attribute, eventTarget);
498 var tagName = eventTarget.enclosingNodeOrSelfWithClass("webkit-html-tag-name");
499 if (tagName)
500 return this._startEditingTagName(tagName);
502 var newAttribute = eventTarget.enclosingNodeOrSelfWithClass("add-attribute");
503 if (newAttribute)
504 return this._addNewAttribute();
506 return false;
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)
542 if (!this._editing)
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));
559 if (!isShadowRoot)
560 contextMenu.appendItem(WebInspector.UIString("Copy XPath"), this._copyXPath.bind(this));
561 if (!isShadowRoot) {
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));
569 if (isEditable)
570 contextMenu.appendItem(WebInspector.UIString("Delete"), this.remove.bind(this));
571 contextMenu.appendSeparator();
574 _startEditing: function()
576 if (this.treeOutline.selectedDOMNode() !== this._node)
577 return;
579 var listItem = this._listItemNode;
581 if (this._canAddAttributes) {
582 var attribute = listItem.getElementsByClassName("webkit-html-attribute")[0];
583 if (attribute)
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];
591 if (textNode)
592 return this._startEditingTextNode(textNode);
593 return;
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)
620 continue;
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))
634 return true;
636 var attributeNameElement = attribute.getElementsByClassName("webkit-html-attribute-name")[0];
637 if (!attributeNameElement)
638 return false;
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, "");
650 return;
653 if (node.nodeType !== Node.ELEMENT_NODE)
654 return;
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
671 * @return {string}
673 function postKeyDownFinishHandler(event)
675 WebInspector.handleElementValueModifications(event, attribute);
676 return "";
678 config.setPostKeydownFinishHandler(postKeyDownFinishHandler);
680 this._editing = WebInspector.InplaceEditor.startEditing(attribute, config);
682 this.listItemElement.getComponentSelection().setBaseAndExtent(elementForSelection, 0, elementForSelection, 1);
684 return true;
688 * @param {!Element} textNodeElement
690 _startEditingTextNode: function(textNodeElement)
692 if (WebInspector.isBeingEdited(textNodeElement))
693 return true;
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");
702 if (container)
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);
708 return true;
712 * @param {!Element=} tagNameElement
714 _startEditingTagName: function(tagNameElement)
716 if (!tagNameElement) {
717 tagNameElement = this.listItemElement.getElementsByClassName("webkit-html-tag-name")[0];
718 if (!tagNameElement)
719 return false;
722 var tagName = tagNameElement.textContent;
723 if (WebInspector.ElementsTreeElement.EditTagBlacklist[tagName.toLowerCase()])
724 return false;
726 if (WebInspector.isBeingEdited(tagNameElement))
727 return true;
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);
765 return true;
769 * @param {function(string, string)} commitCallback
770 * @param {?Protocol.Error} error
771 * @param {string} initialValue
773 _startEditingAsHTML: function(commitCallback, error, initialValue)
775 if (error)
776 return;
777 if (this._editing)
778 return;
780 function consume(event)
782 if (event.eventPhase === Event.AT_TARGET)
783 event.consume(true);
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;
793 while (child) {
794 child.style.display = "none";
795 child = child.nextSibling;
797 // Hide children item.
798 if (this._childrenListNode)
799 this._childrenListNode.style.display = "none";
800 // Append editor.
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);
814 dispose.call(this);
818 * @this {WebInspector.ElementsTreeElement}
820 function dispose()
822 delete this._editing;
823 this.treeOutline.setMultilineEditing(null);
825 // Remove editor.
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;
833 while (child) {
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)
871 if (error)
872 this._editingCancelled(element, attributeName);
874 if (!moveDirection)
875 return;
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)
883 continue;
885 if (moveDirection === "backward") {
886 if (i === 0)
887 this._startEditingTagName();
888 else
889 this._triggerEditAttribute(attributes[i - 1].name);
890 } else {
891 if (i === attributes.length - 1)
892 this._addNewAttribute();
893 else
894 this._triggerEditAttribute(attributes[i + 1].name);
896 return;
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);
905 } else {
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();
913 else
914 this._startEditingTagName();
919 if ((attributeName.trim() || newText.trim()) && oldText !== newText) {
920 this._node.setAttribute(attributeName, newText, moveToNextAttributeIfNeeded.bind(this));
921 return;
924 this.updateTitle();
925 moveToNextAttributeIfNeeded.call(this);
928 _tagNameEditingCommitted: function(element, newText, oldText, tagName, moveDirection)
930 delete this._editing;
931 var self = this;
933 function cancel()
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();
950 return;
953 var attributes = this._node.attributes();
954 if (attributes.length > 0)
955 this._triggerEditAttribute(attributes[0].name);
956 else
957 this._addNewAttribute();
960 newText = newText.trim();
961 if (newText === oldText) {
962 cancel();
963 return;
966 var treeOutline = this.treeOutline;
967 var wasExpanded = this.expanded;
969 function changeTagNameCallback(error, nodeId)
971 if (error || !nodeId) {
972 cancel();
973 return;
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}
993 function callback()
995 this.updateTitle();
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.
1009 this.updateTitle();
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.
1041 if (this._editing)
1042 return;
1044 if (onlySearchQueryChanged) {
1045 this._hideSearchHighlight();
1046 } else {
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");
1050 var depth = 4;
1051 for (var node = this._node; depth && node; node = node.parentNode) {
1052 if (node.nodeType() === Node.DOCUMENT_FRAGMENT_NODE)
1053 depth--;
1055 if (!depth)
1056 this.childrenListElement.classList.add("shadow-root-deep");
1057 else
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;
1069 if (this.selected)
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);
1087 if (message) {
1088 decoratorMessages.push(message);
1089 continue;
1092 if (this.expanded || this._elementCloseTag)
1093 continue;
1095 message = decorator.decorateAncestor(node);
1096 if (message)
1097 parentDecoratorMessages.push(message)
1099 if (!decoratorMessages.length && !parentDecoratorMessages.length)
1100 return null;
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;
1131 var highlightCount;
1132 var additionalHighlightOffset = 0;
1133 var result;
1136 * @param {string} match
1137 * @param {number} replaceOffset
1138 * @return {string}
1140 function replacer(match, replaceOffset) {
1141 while (highlightIndex < highlightCount && result.entityRanges[highlightIndex].offset < replaceOffset) {
1142 result.entityRanges[highlightIndex].offset += additionalHighlightOffset;
1143 ++highlightIndex;
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;
1161 ++highlightIndex;
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;
1172 if (hasText)
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);
1191 return span;
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;
1198 return anchor;
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) {
1206 if (i > 0)
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);
1215 } else {
1216 setValueWithEntities.call(this, attrValueElement, value);
1219 if (hasText)
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);
1260 if (updateRecord) {
1261 var hasUpdates = updateRecord.hasRemovedAttributes() || updateRecord.hasRemovedChildren();
1262 hasUpdates |= !this.expanded && updateRecord.hasChangedChildren();
1263 if (hasUpdates)
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)
1278 var result = "";
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;
1292 if (result)
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);
1309 break;
1311 case Node.ELEMENT_NODE:
1312 var pseudoType = node.pseudoType();
1313 if (pseudoType) {
1314 this._buildPseudoElementDOM(titleDOM, pseudoType);
1315 break;
1318 var tagName = node.nodeNameInCorrectCase();
1319 if (this._elementCloseTag) {
1320 this._buildTagDOM(titleDOM, tagName, true, true, updateRecord);
1321 break;
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);
1333 break;
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");
1347 break;
1350 if (this.treeOutline.isXMLMimeType || !WebInspector.ElementsTreeElement.ForbiddenClosingTagElements[tagName])
1351 this._buildTagDOM(titleDOM, tagName, true, false, updateRecord);
1352 break;
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));
1367 } else {
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");
1377 break;
1379 case Node.COMMENT_NODE:
1380 var commentElement = titleDOM.createChild("span", "webkit-html-comment");
1381 commentElement.createTextChild("<!--" + node.nodeValue() + "-->");
1382 break;
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 + "\"");
1389 if (node.systemId)
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(">");
1398 break;
1400 case Node.CDATA_SECTION_NODE:
1401 var cdataElement = titleDOM.createChild("span", "webkit-html-text-node");
1402 cdataElement.createTextChild("<![CDATA[" + node.nodeValue() + "]]>");
1403 break;
1405 case Node.DOCUMENT_FRAGMENT_NODE:
1406 var fragmentElement = titleDOM.createChild("span", "webkit-html-fragment");
1407 fragmentElement.textContent = node.nodeNameInCorrectCase().collapseWhitespace();
1408 break;
1409 default:
1410 titleDOM.createTextChild(node.nodeNameInCorrectCase().collapseWhitespace());
1414 * @this {WebInspector.ElementsTreeElement}
1416 function updateSearchHighlight()
1418 delete this._highlightResult;
1419 this._highlightSearchResults();
1422 return titleDOM;
1425 remove: function()
1427 if (this._node.pseudoType())
1428 return;
1429 var parentElement = this.parent;
1430 if (!parentElement)
1431 return;
1433 if (!this._node.parentNode || this._node.parentNode.nodeType() === Node.DOCUMENT_NODE)
1434 return;
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();
1446 return;
1449 if (startEditing === false)
1450 return;
1453 * @param {?Protocol.Error} error
1455 function selectNode(error)
1457 if (callback)
1458 callback(!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)
1488 return;
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 = [];
1496 while (match) {
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
1515 * @this {!Element}
1517 function scrollIntoView()
1519 this.scrollIntoViewIfNeeded(true);
1522 if (object)
1523 object.callFunction(scrollIntoView);
1526 this._node.resolveToObject("", scrollIntoViewCallback);
1529 __proto__: TreeElement.prototype