Web Inspector: Convert TreeElement classes to ES6
[WebKit-https.git] / Source / WebInspectorUI / UserInterface / Views / DOMTreeElement.js
1 /*
2  * Copyright (C) 2007, 2008, 2013, 2015 Apple Inc.  All rights reserved.
3  * Copyright (C) 2008 Matt Lilek <webkit@mattlilek.com>
4  * Copyright (C) 2009 Joseph Pecoraro
5  *
6  * Redistribution and use in source and binary forms, with or without
7  * modification, are permitted provided that the following conditions
8  * are met:
9  *
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 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.
18  *
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.
29  */
30
31 WebInspector.DOMTreeElement = class DOMTreeElement extends WebInspector.TreeElement
32 {
33     constructor(node, elementCloseTag)
34     {
35         super("", node);
36
37         this._elementCloseTag = elementCloseTag;
38         this.hasChildren = !elementCloseTag && node.hasChildNodes() && !this._showInlineText(node);
39
40         if (this.representedObject.nodeType() === Node.ELEMENT_NODE && !elementCloseTag)
41             this._canAddAttributes = true;
42         this._searchQuery = null;
43         this._expandedChildrenLimit = WebInspector.DOMTreeElement.InitialChildrenLimit;
44     }
45
46     isCloseTag()
47     {
48         return this._elementCloseTag;
49     }
50
51     highlightSearchResults(searchQuery)
52     {
53         if (this._searchQuery !== searchQuery) {
54             this._updateSearchHighlight(false);
55             delete this._highlightResult; // A new search query.
56         }
57
58         this._searchQuery = searchQuery;
59         this._searchHighlightsVisible = true;
60         this.updateTitle(true);
61     }
62
63     hideSearchHighlights()
64     {
65         delete this._searchHighlightsVisible;
66         this._updateSearchHighlight(false);
67     }
68
69     emphasizeSearchHighlight()
70     {
71         var highlightElement = this.title.querySelector("." + WebInspector.DOMTreeElement.SearchHighlightStyleClassName);
72         console.assert(highlightElement);
73         if (!highlightElement)
74             return;
75
76         if (this._bouncyHighlightElement)
77             this._bouncyHighlightElement.remove();
78
79         this._bouncyHighlightElement = document.createElement("div");
80         this._bouncyHighlightElement.className = WebInspector.DOMTreeElement.BouncyHighlightStyleClassName;
81         this._bouncyHighlightElement.textContent = highlightElement.textContent;
82
83         // Position and show the bouncy highlight adjusting the coordinates to be inside the TreeOutline's space.
84         var highlightElementRect = highlightElement.getBoundingClientRect();
85         var treeOutlineRect = this.treeOutline.element.getBoundingClientRect();
86         this._bouncyHighlightElement.style.top = (highlightElementRect.top - treeOutlineRect.top) + "px";
87         this._bouncyHighlightElement.style.left = (highlightElementRect.left - treeOutlineRect.left) + "px";
88         this.title.appendChild(this._bouncyHighlightElement);
89
90         function animationEnded()
91         {
92             if (!this._bouncyHighlightElement)
93                 return;
94
95             this._bouncyHighlightElement.remove();
96             delete this._bouncyHighlightElement;
97         }
98
99         this._bouncyHighlightElement.addEventListener("webkitAnimationEnd", animationEnded.bind(this));
100     }
101
102     _updateSearchHighlight(show)
103     {
104         if (!this._highlightResult)
105             return;
106
107         function updateEntryShow(entry)
108         {
109             switch (entry.type) {
110                 case "added":
111                     entry.parent.insertBefore(entry.node, entry.nextSibling);
112                     break;
113                 case "changed":
114                     entry.node.textContent = entry.newText;
115                     break;
116             }
117         }
118
119         function updateEntryHide(entry)
120         {
121             switch (entry.type) {
122                 case "added":
123                     if (entry.node.parentElement)
124                         entry.node.parentElement.removeChild(entry.node);
125                     break;
126                 case "changed":
127                     entry.node.textContent = entry.oldText;
128                     break;
129             }
130         }
131
132         var updater = show ? updateEntryShow : updateEntryHide;
133
134         for (var i = 0, size = this._highlightResult.length; i < size; ++i)
135             updater(this._highlightResult[i]);
136     }
137
138     get hovered()
139     {
140         return this._hovered;
141     }
142
143     set hovered(x)
144     {
145         if (this._hovered === x)
146             return;
147
148         this._hovered = x;
149
150         if (this.listItemElement) {
151             if (x) {
152                 this.updateSelection();
153                 this.listItemElement.classList.add("hovered");
154             } else {
155                 this.listItemElement.classList.remove("hovered");
156             }
157         }
158     }
159
160     get expandedChildrenLimit()
161     {
162         return this._expandedChildrenLimit;
163     }
164
165     set expandedChildrenLimit(x)
166     {
167         if (this._expandedChildrenLimit === x)
168             return;
169
170         this._expandedChildrenLimit = x;
171         if (this.treeOutline && !this._updateChildrenInProgress)
172             this._updateChildren(true);
173     }
174
175     get expandedChildCount()
176     {
177         var count = this.children.length;
178         if (count && this.children[count - 1]._elementCloseTag)
179             count--;
180         if (count && this.children[count - 1].expandAllButton)
181             count--;
182         return count;
183     }
184
185     showChild(index)
186     {
187         console.assert(!this._elementCloseTag);
188         if (this._elementCloseTag)
189             return false;
190
191         if (index >= this.expandedChildrenLimit) {
192             this._expandedChildrenLimit = index + 1;
193             this._updateChildren(true);
194         }
195
196         // Whether index-th child is visible in the children tree
197         return this.expandedChildCount > index;
198     }
199
200     _createTooltipForNode()
201     {
202         var node = this.representedObject;
203         if (!node.nodeName() || node.nodeName().toLowerCase() !== "img")
204             return;
205
206         function setTooltip(error, result, wasThrown)
207         {
208             if (error || wasThrown || !result || result.type !== "string")
209                 return;
210
211             try {
212                 var properties = JSON.parse(result.description);
213                 var offsetWidth = properties[0];
214                 var offsetHeight = properties[1];
215                 var naturalWidth = properties[2];
216                 var naturalHeight = properties[3];
217                 if (offsetHeight === naturalHeight && offsetWidth === naturalWidth)
218                     this.tooltip = WebInspector.UIString("%d \xd7 %d pixels").format(offsetWidth, offsetHeight);
219                 else
220                     this.tooltip = WebInspector.UIString("%d \xd7 %d pixels (Natural: %d \xd7 %d pixels)").format(offsetWidth, offsetHeight, naturalWidth, naturalHeight);
221             } catch (e) {
222                 console.error(e);
223             }
224         }
225
226         function resolvedNode(object)
227         {
228             if (!object)
229                 return;
230
231             function dimensions()
232             {
233                 return "[" + this.offsetWidth + "," + this.offsetHeight + "," + this.naturalWidth + "," + this.naturalHeight + "]";
234             }
235
236             object.callFunction(dimensions, undefined, false, setTooltip.bind(this));
237             object.release();
238         }
239         WebInspector.RemoteObject.resolveNode(node, "", resolvedNode.bind(this));
240     }
241
242     updateSelection()
243     {
244         var listItemElement = this.listItemElement;
245         if (!listItemElement)
246             return;
247
248         if (document.body.offsetWidth <= 0) {
249             // The stylesheet hasn't loaded yet or the window is closed,
250             // so we can't calculate what is need. Return early.
251             return;
252         }
253
254         if (!this.selectionElement) {
255             this.selectionElement = document.createElement("div");
256             this.selectionElement.className = "selection selected";
257             listItemElement.insertBefore(this.selectionElement, listItemElement.firstChild);
258         }
259
260         this.selectionElement.style.height = listItemElement.offsetHeight + "px";
261     }
262
263     onattach()
264     {
265         if (this._hovered) {
266             this.updateSelection();
267             this.listItemElement.classList.add("hovered");
268         }
269
270         this.updateTitle();
271         this.listItemElement.draggable = true;
272         this.listItemElement.addEventListener("dragstart", this);
273     }
274
275     onpopulate()
276     {
277         if (this.children.length || this._showInlineText(this.representedObject) || this._elementCloseTag)
278             return;
279
280         this.updateChildren();
281     }
282
283     expandRecursively()
284     {
285         this.representedObject.getSubtree(-1, super.expandRecursively.bind(this, Number.MAX_VALUE));
286     }
287
288     updateChildren(fullRefresh)
289     {
290         if (this._elementCloseTag)
291             return;
292         this.representedObject.getChildNodes(this._updateChildren.bind(this, fullRefresh));
293     }
294
295     insertChildElement(child, index, closingTag)
296     {
297         var newElement = new WebInspector.DOMTreeElement(child, closingTag);
298         newElement.selectable = this.treeOutline._selectEnabled;
299         this.insertChild(newElement, index);
300         return newElement;
301     }
302
303     moveChild(child, targetIndex)
304     {
305         var wasSelected = child.selected;
306         this.removeChild(child);
307         this.insertChild(child, targetIndex);
308         if (wasSelected)
309             child.select();
310     }
311
312     _updateChildren(fullRefresh)
313     {
314         if (this._updateChildrenInProgress || !this.treeOutline._visible)
315             return;
316
317         this._updateChildrenInProgress = true;
318         var selectedNode = this.treeOutline.selectedDOMNode();
319         var originalScrollTop = 0;
320         if (fullRefresh) {
321             var treeOutlineContainerElement = this.treeOutline.element.parentNode;
322             originalScrollTop = treeOutlineContainerElement.scrollTop;
323             var selectedTreeElement = this.treeOutline.selectedTreeElement;
324             if (selectedTreeElement && selectedTreeElement.hasAncestor(this))
325                 this.select();
326             this.removeChildren();
327         }
328
329         var treeElement = this;
330         var treeChildIndex = 0;
331         var elementToSelect;
332
333         function updateChildrenOfNode(node)
334         {
335             var treeOutline = treeElement.treeOutline;
336             var child = node.firstChild;
337             while (child) {
338                 var currentTreeElement = treeElement.children[treeChildIndex];
339                 if (!currentTreeElement || currentTreeElement.representedObject !== child) {
340                     // Find any existing element that is later in the children list.
341                     var existingTreeElement = null;
342                     for (var i = (treeChildIndex + 1), size = treeElement.expandedChildCount; i < size; ++i) {
343                         if (treeElement.children[i].representedObject === child) {
344                             existingTreeElement = treeElement.children[i];
345                             break;
346                         }
347                     }
348
349                     if (existingTreeElement && existingTreeElement.parent === treeElement) {
350                         // If an existing element was found and it has the same parent, just move it.
351                         treeElement.moveChild(existingTreeElement, treeChildIndex);
352                     } else {
353                         // No existing element found, insert a new element.
354                         if (treeChildIndex < treeElement.expandedChildrenLimit) {
355                             var newElement = treeElement.insertChildElement(child, treeChildIndex);
356                             if (child === selectedNode)
357                                 elementToSelect = newElement;
358                             if (treeElement.expandedChildCount > treeElement.expandedChildrenLimit)
359                                 treeElement.expandedChildrenLimit++;
360                         }
361                     }
362                 }
363
364                 child = child.nextSibling;
365                 ++treeChildIndex;
366             }
367         }
368
369         // Remove any tree elements that no longer have this node (or this node's contentDocument) as their parent.
370         for (var i = (this.children.length - 1); i >= 0; --i) {
371             var currentChild = this.children[i];
372             var currentNode = currentChild.representedObject;
373             var currentParentNode = currentNode.parentNode;
374
375             if (currentParentNode === this.representedObject)
376                 continue;
377
378             var selectedTreeElement = this.treeOutline.selectedTreeElement;
379             if (selectedTreeElement && (selectedTreeElement === currentChild || selectedTreeElement.hasAncestor(currentChild)))
380                 this.select();
381
382             this.removeChildAtIndex(i);
383         }
384
385         updateChildrenOfNode(this.representedObject);
386         this.adjustCollapsedRange();
387
388         var lastChild = this.children.lastValue;
389         if (this.representedObject.nodeType() === Node.ELEMENT_NODE && (!lastChild || !lastChild._elementCloseTag))
390             this.insertChildElement(this.representedObject, this.children.length, true);
391
392         // We want to restore the original selection and tree scroll position after a full refresh, if possible.
393         if (fullRefresh && elementToSelect) {
394             elementToSelect.select();
395             if (treeOutlineContainerElement && originalScrollTop <= treeOutlineContainerElement.scrollHeight)
396                 treeOutlineContainerElement.scrollTop = originalScrollTop;
397         }
398
399         delete this._updateChildrenInProgress;
400     }
401
402     adjustCollapsedRange()
403     {
404         // Ensure precondition: only the tree elements for node children are found in the tree
405         // (not the Expand All button or the closing tag).
406         if (this.expandAllButtonElement && this.expandAllButtonElement.__treeElement.parent)
407             this.removeChild(this.expandAllButtonElement.__treeElement);
408
409         var node = this.representedObject;
410         if (!node.children)
411             return;
412         var childNodeCount = node.children.length;
413
414         // In case some nodes from the expanded range were removed, pull some nodes from the collapsed range into the expanded range at the bottom.
415         for (var i = this.expandedChildCount, limit = Math.min(this.expandedChildrenLimit, childNodeCount); i < limit; ++i)
416             this.insertChildElement(node.children[i], i);
417
418         var expandedChildCount = this.expandedChildCount;
419         if (childNodeCount > this.expandedChildCount) {
420             var targetButtonIndex = expandedChildCount;
421             if (!this.expandAllButtonElement) {
422                 var button = document.createElement("button");
423                 button.className = "show-all-nodes";
424                 button.value = "";
425
426                 var item = new WebInspector.TreeElement(button, null, false);
427                 item.selectable = false;
428                 item.expandAllButton = true;
429
430                 this.insertChild(item, targetButtonIndex);
431                 this.expandAllButtonElement = button;
432                 this.expandAllButtonElement.__treeElement = item;
433                 this.expandAllButtonElement.addEventListener("click", this.handleLoadAllChildren.bind(this), false);
434             } else if (!this.expandAllButtonElement.__treeElement.parent)
435                 this.insertChild(this.expandAllButtonElement.__treeElement, targetButtonIndex);
436
437             this.expandAllButtonElement.textContent = WebInspector.UIString("Show All Nodes (%d More)").format(childNodeCount - expandedChildCount);
438         } else if (this.expandAllButtonElement)
439             delete this.expandAllButtonElement;
440     }
441
442     handleLoadAllChildren()
443     {
444         this.expandedChildrenLimit = Math.max(this.representedObject.childNodeCount, this.expandedChildrenLimit + WebInspector.DOMTreeElement.InitialChildrenLimit);
445     }
446
447     onexpand()
448     {
449         if (this._elementCloseTag)
450             return;
451
452         this.updateTitle();
453         this.treeOutline.updateSelection();
454     }
455
456     oncollapse()
457     {
458         if (this._elementCloseTag)
459             return;
460
461         this.updateTitle();
462         this.treeOutline.updateSelection();
463     }
464
465     onreveal()
466     {
467         if (this.listItemElement) {
468             var tagSpans = this.listItemElement.getElementsByClassName("html-tag-name");
469             if (tagSpans.length)
470                 tagSpans[0].scrollIntoViewIfNeeded(false);
471             else
472                 this.listItemElement.scrollIntoViewIfNeeded(false);
473         }
474     }
475
476     onselect(treeElement, selectedByUser)
477     {
478         this.treeOutline.suppressRevealAndSelect = true;
479         this.treeOutline.selectDOMNode(this.representedObject, selectedByUser);
480         if (selectedByUser)
481             WebInspector.domTreeManager.highlightDOMNode(this.representedObject.id);
482         this.updateSelection();
483         this.treeOutline.suppressRevealAndSelect = false;
484     }
485
486     ondeselect(treeElement)
487     {
488         this.treeOutline.selectDOMNode(null);
489     }
490
491     ondelete()
492     {
493         if (this.representedObject.isInShadowTree())
494             return false;
495
496         var startTagTreeElement = this.treeOutline.findTreeElement(this.representedObject);
497         if (startTagTreeElement)
498             startTagTreeElement.remove();
499         else
500             this.remove();
501         return true;
502     }
503
504     onenter()
505     {
506         // On Enter or Return start editing the first attribute
507         // or create a new attribute on the selected element.
508         if (this.treeOutline.editing)
509             return false;
510
511         this._startEditing();
512
513         // prevent a newline from being immediately inserted
514         return true;
515     }
516
517     selectOnMouseDown(event)
518     {
519         super.selectOnMouseDown(event);
520
521         if (this._editing)
522             return;
523
524         // Prevent selecting the nearest word on double click.
525         if (event.detail >= 2)
526             event.preventDefault();
527     }
528
529     ondblclick(event)
530     {
531         if (this._editing || this._elementCloseTag)
532             return;
533
534         if (this._startEditingTarget(event.target))
535             return;
536
537         if (this.hasChildren && !this.expanded)
538             this.expand();
539     }
540
541     _insertInLastAttributePosition(tag, node)
542     {
543         if (tag.getElementsByClassName("html-attribute").length > 0)
544             tag.insertBefore(node, tag.lastChild);
545         else {
546             var nodeName = tag.textContent.match(/^<(.*?)>$/)[1];
547             tag.textContent = "";
548             tag.appendChild(document.createTextNode("<" + nodeName));
549             tag.appendChild(node);
550             tag.appendChild(document.createTextNode(">"));
551         }
552
553         this.updateSelection();
554     }
555
556     _startEditingTarget(eventTarget)
557     {
558         if (this.treeOutline.selectedDOMNode() !== this.representedObject)
559             return false;
560
561         if (this.representedObject.isInShadowTree())
562             return false;
563
564         if (this.representedObject.nodeType() !== Node.ELEMENT_NODE && this.representedObject.nodeType() !== Node.TEXT_NODE)
565             return false;
566
567         var textNode = eventTarget.enclosingNodeOrSelfWithClass("html-text-node");
568         if (textNode)
569             return this._startEditingTextNode(textNode);
570
571         var attribute = eventTarget.enclosingNodeOrSelfWithClass("html-attribute");
572         if (attribute)
573             return this._startEditingAttribute(attribute, eventTarget);
574
575         var tagName = eventTarget.enclosingNodeOrSelfWithClass("html-tag-name");
576         if (tagName)
577             return this._startEditingTagName(tagName);
578
579         var newAttribute = eventTarget.enclosingNodeOrSelfWithClass("add-attribute");
580         if (newAttribute)
581             return this._addNewAttribute();
582
583         return false;
584     }
585
586     _populateTagContextMenu(contextMenu, event)
587     {
588         var node = this.representedObject;
589         if (!node.isInShadowTree()) {
590             var attribute = event.target.enclosingNodeOrSelfWithClass("html-attribute");
591             var newAttribute = event.target.enclosingNodeOrSelfWithClass("add-attribute");
592
593             // Add attribute-related actions.
594             contextMenu.appendItem(WebInspector.UIString("Add Attribute"), this._addNewAttribute.bind(this));
595             if (attribute && !newAttribute)
596                 contextMenu.appendItem(WebInspector.UIString("Edit Attribute"), this._startEditingAttribute.bind(this, attribute, event.target));
597             contextMenu.appendSeparator();
598
599             if (WebInspector.cssStyleManager.canForcePseudoClasses()) {
600                 var pseudoSubMenu = contextMenu.appendSubMenuItem(WebInspector.UIString("Forced Pseudo-Classes"));
601                 this._populateForcedPseudoStateItems(pseudoSubMenu);
602                 contextMenu.appendSeparator();
603             }
604         }
605
606         this._populateNodeContextMenu(contextMenu);
607         this.treeOutline._populateContextMenu(contextMenu, this.representedObject);
608     }
609
610     _populateForcedPseudoStateItems(subMenu)
611     {
612         var node = this.representedObject;
613         var enabledPseudoClasses = node.enabledPseudoClasses;
614         // These strings don't need to be localized as they are CSS pseudo-classes.
615         WebInspector.CSSStyleManager.ForceablePseudoClasses.forEach(function(pseudoClass) {
616             var label = pseudoClass.capitalize();
617             var enabled = enabledPseudoClasses.contains(pseudoClass);
618             subMenu.appendCheckboxItem(label, function() {
619                 node.setPseudoClassEnabled(pseudoClass, !enabled);
620             }, enabled, false);
621         });
622     }
623
624     _populateTextContextMenu(contextMenu, textNode)
625     {
626         var node = this.representedObject;
627         if (!node.isInShadowTree())
628             contextMenu.appendItem(WebInspector.UIString("Edit Text"), this._startEditingTextNode.bind(this, textNode));
629
630         this._populateNodeContextMenu(contextMenu);
631     }
632
633     _populateNodeContextMenu(contextMenu)
634     {
635         // Add free-form node-related actions.
636         var node = this.representedObject;
637         if (!node.isInShadowTree())
638             contextMenu.appendItem(WebInspector.UIString("Edit as HTML"), this._editAsHTML.bind(this));
639         contextMenu.appendItem(WebInspector.UIString("Copy as HTML"), this._copyHTML.bind(this));
640         if (!node.isInShadowTree())
641             contextMenu.appendItem(WebInspector.UIString("Delete Node"), this.remove.bind(this));
642     }
643
644     _startEditing()
645     {
646         if (this.treeOutline.selectedDOMNode() !== this.representedObject)
647             return false;
648
649         if (this.representedObject.isInShadowTree())
650             return false;
651
652         var listItem = this._listItemNode;
653
654         if (this._canAddAttributes) {
655             var attribute = listItem.getElementsByClassName("html-attribute")[0];
656             if (attribute)
657                 return this._startEditingAttribute(attribute, attribute.getElementsByClassName("html-attribute-value")[0]);
658
659             return this._addNewAttribute();
660         }
661
662         if (this.representedObject.nodeType() === Node.TEXT_NODE) {
663             var textNode = listItem.getElementsByClassName("html-text-node")[0];
664             if (textNode)
665                 return this._startEditingTextNode(textNode);
666             return false;
667         }
668     }
669
670     _addNewAttribute()
671     {
672         // Cannot just convert the textual html into an element without
673         // a parent node. Use a temporary span container for the HTML.
674         var container = document.createElement("span");
675         this._buildAttributeDOM(container, " ", "");
676         var attr = container.firstChild;
677         attr.style.marginLeft = "2px"; // overrides the .editing margin rule
678         attr.style.marginRight = "2px"; // overrides the .editing margin rule
679
680         var tag = this.listItemElement.getElementsByClassName("html-tag")[0];
681         this._insertInLastAttributePosition(tag, attr);
682         return this._startEditingAttribute(attr, attr);
683     }
684
685     _triggerEditAttribute(attributeName)
686     {
687         var attributeElements = this.listItemElement.getElementsByClassName("html-attribute-name");
688         for (var i = 0, len = attributeElements.length; i < len; ++i) {
689             if (attributeElements[i].textContent === attributeName) {
690                 for (var elem = attributeElements[i].nextSibling; elem; elem = elem.nextSibling) {
691                     if (elem.nodeType !== Node.ELEMENT_NODE)
692                         continue;
693
694                     if (elem.classList.contains("html-attribute-value"))
695                         return this._startEditingAttribute(elem.parentNode, elem);
696                 }
697             }
698         }
699     }
700
701     _startEditingAttribute(attribute, elementForSelection)
702     {
703         if (WebInspector.isBeingEdited(attribute))
704             return true;
705
706         var attributeNameElement = attribute.getElementsByClassName("html-attribute-name")[0];
707         if (!attributeNameElement)
708             return false;
709
710         var attributeName = attributeNameElement.textContent;
711
712         function removeZeroWidthSpaceRecursive(node)
713         {
714             if (node.nodeType === Node.TEXT_NODE) {
715                 node.nodeValue = node.nodeValue.replace(/\u200B/g, "");
716                 return;
717             }
718
719             if (node.nodeType !== Node.ELEMENT_NODE)
720                 return;
721
722             for (var child = node.firstChild; child; child = child.nextSibling)
723                 removeZeroWidthSpaceRecursive(child);
724         }
725
726         // Remove zero-width spaces that were added by nodeTitleInfo.
727         removeZeroWidthSpaceRecursive(attribute);
728
729         var config = new WebInspector.EditingConfig(this._attributeEditingCommitted.bind(this), this._editingCancelled.bind(this), attributeName);
730         this._editing = WebInspector.startEditing(attribute, config);
731
732         window.getSelection().setBaseAndExtent(elementForSelection, 0, elementForSelection, 1);
733
734         return true;
735     }
736
737     _startEditingTextNode(textNode)
738     {
739         if (WebInspector.isBeingEdited(textNode))
740             return true;
741
742         var config = new WebInspector.EditingConfig(this._textNodeEditingCommitted.bind(this), this._editingCancelled.bind(this));
743         config.spellcheck = true;
744         this._editing = WebInspector.startEditing(textNode, config);
745         window.getSelection().setBaseAndExtent(textNode, 0, textNode, 1);
746
747         return true;
748     }
749
750     _startEditingTagName(tagNameElement)
751     {
752         if (!tagNameElement) {
753             tagNameElement = this.listItemElement.getElementsByClassName("html-tag-name")[0];
754             if (!tagNameElement)
755                 return false;
756         }
757
758         var tagName = tagNameElement.textContent;
759         if (WebInspector.DOMTreeElement.EditTagBlacklist[tagName.toLowerCase()])
760             return false;
761
762         if (WebInspector.isBeingEdited(tagNameElement))
763             return true;
764
765         var closingTagElement = this._distinctClosingTagElement();
766
767         function keyupListener(event)
768         {
769             if (closingTagElement)
770                 closingTagElement.textContent = "</" + tagNameElement.textContent + ">";
771         }
772
773         function editingComitted(element, newTagName)
774         {
775             tagNameElement.removeEventListener("keyup", keyupListener, false);
776             this._tagNameEditingCommitted.apply(this, arguments);
777         }
778
779         function editingCancelled()
780         {
781             tagNameElement.removeEventListener("keyup", keyupListener, false);
782             this._editingCancelled.apply(this, arguments);
783         }
784
785         tagNameElement.addEventListener("keyup", keyupListener, false);
786
787         var config = new WebInspector.EditingConfig(editingComitted.bind(this), editingCancelled.bind(this), tagName);
788         this._editing = WebInspector.startEditing(tagNameElement, config);
789         window.getSelection().setBaseAndExtent(tagNameElement, 0, tagNameElement, 1);
790         return true;
791     }
792
793     _startEditingAsHTML(commitCallback, error, initialValue)
794     {
795         if (error)
796             return;
797         if (this._htmlEditElement && WebInspector.isBeingEdited(this._htmlEditElement))
798             return;
799
800         this._htmlEditElement = document.createElement("div");
801         this._htmlEditElement.className = "source-code elements-tree-editor";
802         this._htmlEditElement.textContent = initialValue;
803
804         // Hide header items.
805         var child = this.listItemElement.firstChild;
806         while (child) {
807             child.style.display = "none";
808             child = child.nextSibling;
809         }
810         // Hide children item.
811         if (this._childrenListNode)
812             this._childrenListNode.style.display = "none";
813         // Append editor.
814         this.listItemElement.appendChild(this._htmlEditElement);
815
816         this.updateSelection();
817
818         function commit()
819         {
820             commitCallback(this._htmlEditElement.textContent);
821             dispose.call(this);
822         }
823
824         function dispose()
825         {
826             this._editing = false;
827
828             // Remove editor.
829             this.listItemElement.removeChild(this._htmlEditElement);
830             delete this._htmlEditElement;
831             // Unhide children item.
832             if (this._childrenListNode)
833                 this._childrenListNode.style.removeProperty("display");
834             // Unhide header items.
835             var child = this.listItemElement.firstChild;
836             while (child) {
837                 child.style.removeProperty("display");
838                 child = child.nextSibling;
839             }
840
841             this.updateSelection();
842         }
843
844         var config = new WebInspector.EditingConfig(commit.bind(this), dispose.bind(this));
845         config.setMultiline(true);
846         this._editing = WebInspector.startEditing(this._htmlEditElement, config);
847     }
848
849     _attributeEditingCommitted(element, newText, oldText, attributeName, moveDirection)
850     {
851         this._editing = false;
852
853         var treeOutline = this.treeOutline;
854         function moveToNextAttributeIfNeeded(error)
855         {
856             if (error)
857                 this._editingCancelled(element, attributeName);
858
859             if (!moveDirection)
860                 return;
861
862             treeOutline._updateModifiedNodes();
863
864             // Search for the attribute's position, and then decide where to move to.
865             var attributes = this.representedObject.attributes();
866             for (var i = 0; i < attributes.length; ++i) {
867                 if (attributes[i].name !== attributeName)
868                     continue;
869
870                 if (moveDirection === "backward") {
871                     if (i === 0)
872                         this._startEditingTagName();
873                     else
874                         this._triggerEditAttribute(attributes[i - 1].name);
875                 } else {
876                     if (i === attributes.length - 1)
877                         this._addNewAttribute();
878                     else
879                         this._triggerEditAttribute(attributes[i + 1].name);
880                 }
881                 return;
882             }
883
884             // Moving From the "New Attribute" position.
885             if (moveDirection === "backward") {
886                 if (newText === " ") {
887                     // Moving from "New Attribute" that was not edited
888                     if (attributes.length)
889                         this._triggerEditAttribute(attributes.lastValue.name);
890                 } else {
891                     // Moving from "New Attribute" that holds new value
892                     if (attributes.length > 1)
893                         this._triggerEditAttribute(attributes[attributes.length - 2].name);
894                 }
895             } else if (moveDirection === "forward") {
896                 if (!/^\s*$/.test(newText))
897                     this._addNewAttribute();
898                 else
899                     this._startEditingTagName();
900             }
901         }
902
903         this.representedObject.setAttribute(attributeName, newText, moveToNextAttributeIfNeeded.bind(this));
904     }
905
906     _tagNameEditingCommitted(element, newText, oldText, tagName, moveDirection)
907     {
908         this._editing = false;
909         var self = this;
910
911         function cancel()
912         {
913             var closingTagElement = self._distinctClosingTagElement();
914             if (closingTagElement)
915                 closingTagElement.textContent = "</" + tagName + ">";
916
917             self._editingCancelled(element, tagName);
918             moveToNextAttributeIfNeeded.call(self);
919         }
920
921         function moveToNextAttributeIfNeeded()
922         {
923             if (moveDirection !== "forward") {
924                 this._addNewAttribute();
925                 return;
926             }
927
928             var attributes = this.representedObject.attributes();
929             if (attributes.length > 0)
930                 this._triggerEditAttribute(attributes[0].name);
931             else
932                 this._addNewAttribute();
933         }
934
935         newText = newText.trim();
936         if (newText === oldText) {
937             cancel();
938             return;
939         }
940
941         var treeOutline = this.treeOutline;
942         var wasExpanded = this.expanded;
943
944         function changeTagNameCallback(error, nodeId)
945         {
946             if (error || !nodeId) {
947                 cancel();
948                 return;
949             }
950
951             var node = WebInspector.domTreeManager.nodeForId(nodeId);
952
953             // Select it and expand if necessary. We force tree update so that it processes dom events and is up to date.
954             treeOutline._updateModifiedNodes();
955             treeOutline.selectDOMNode(node, true);
956
957             var newTreeItem = treeOutline.findTreeElement(node);
958             if (wasExpanded)
959                 newTreeItem.expand();
960
961             moveToNextAttributeIfNeeded.call(newTreeItem);
962         }
963
964         this.representedObject.setNodeName(newText, changeTagNameCallback);
965     }
966
967     _textNodeEditingCommitted(element, newText)
968     {
969         this._editing = false;
970
971         var textNode;
972         if (this.representedObject.nodeType() === Node.ELEMENT_NODE) {
973             // We only show text nodes inline in elements if the element only
974             // has a single child, and that child is a text node.
975             textNode = this.representedObject.firstChild;
976         } else if (this.representedObject.nodeType() === Node.TEXT_NODE)
977             textNode = this.representedObject;
978
979         textNode.setNodeValue(newText, this.updateTitle.bind(this));
980     }
981
982     _editingCancelled(element, context)
983     {
984         this._editing = false;
985
986         // Need to restore attributes structure.
987         this.updateTitle();
988     }
989
990     _distinctClosingTagElement()
991     {
992         // FIXME: Improve the Tree Element / Outline Abstraction to prevent crawling the DOM
993
994         // For an expanded element, it will be the last element with class "close"
995         // in the child element list.
996         if (this.expanded) {
997             var closers = this._childrenListNode.querySelectorAll(".close");
998             return closers[closers.length - 1];
999         }
1000
1001         // Remaining cases are single line non-expanded elements with a closing
1002         // tag, or HTML elements without a closing tag (such as <br>). Return
1003         // null in the case where there isn't a closing tag.
1004         var tags = this.listItemElement.getElementsByClassName("html-tag");
1005         return (tags.length === 1 ? null : tags[tags.length - 1]);
1006     }
1007
1008     updateTitle(onlySearchQueryChanged)
1009     {
1010         // If we are editing, return early to prevent canceling the edit.
1011         // After editing is committed updateTitle will be called.
1012         if (this._editing)
1013             return;
1014
1015         if (onlySearchQueryChanged) {
1016             if (this._highlightResult)
1017                 this._updateSearchHighlight(false);
1018         } else {
1019             this.title = document.createElement("span");
1020             this.title.appendChild(this._nodeTitleInfo().titleDOM);
1021             delete this._highlightResult;
1022         }
1023
1024         delete this.selectionElement;
1025         this.updateSelection();
1026         this._highlightSearchResults();
1027     }
1028
1029     _buildAttributeDOM(parentElement, name, value, node)
1030     {
1031         var hasText = (value.length > 0);
1032         var attrSpanElement = parentElement.createChild("span", "html-attribute");
1033         var attrNameElement = attrSpanElement.createChild("span", "html-attribute-name");
1034         attrNameElement.textContent = name;
1035
1036         if (hasText)
1037             attrSpanElement.appendChild(document.createTextNode("=\u200B\""));
1038
1039         if (name === "src" || name === "href") {
1040             var baseURL = node.ownerDocument ? node.ownerDocument.documentURL : null;
1041             var rewrittenURL = absoluteURL(value, baseURL);
1042
1043             value = value.insertWordBreakCharacters();
1044
1045             if (!rewrittenURL) {
1046                 var attrValueElement = attrSpanElement.createChild("span", "html-attribute-value");
1047                 attrValueElement.textContent = value;
1048             } else {
1049                 if (value.startsWith("data:"))
1050                     value = value.trimMiddle(60);
1051
1052                 var linkElement = document.createElement("a");
1053                 linkElement.href = rewrittenURL;
1054                 linkElement.textContent = value;
1055
1056                 attrSpanElement.appendChild(linkElement);
1057             }
1058         } else {
1059             value = value.insertWordBreakCharacters();
1060             var attrValueElement = attrSpanElement.createChild("span", "html-attribute-value");
1061             attrValueElement.textContent = value;
1062         }
1063
1064         if (hasText)
1065             attrSpanElement.appendChild(document.createTextNode("\""));
1066     }
1067
1068     _buildTagDOM(parentElement, tagName, isClosingTag, isDistinctTreeElement)
1069     {
1070         var node = this.representedObject;
1071         var classes = [ "html-tag" ];
1072         if (isClosingTag && isDistinctTreeElement)
1073             classes.push("close");
1074         if (node.isInShadowTree())
1075             classes.push("shadow");
1076         var tagElement = parentElement.createChild("span", classes.join(" "));
1077         tagElement.appendChild(document.createTextNode("<"));
1078         var tagNameElement = tagElement.createChild("span", isClosingTag ? "" : "html-tag-name");
1079         tagNameElement.textContent = (isClosingTag ? "/" : "") + tagName;
1080         if (!isClosingTag && node.hasAttributes()) {
1081             var attributes = node.attributes();
1082             for (var i = 0; i < attributes.length; ++i) {
1083                 var attr = attributes[i];
1084                 tagElement.appendChild(document.createTextNode(" "));
1085                 this._buildAttributeDOM(tagElement, attr.name, attr.value, node);
1086             }
1087         }
1088         tagElement.appendChild(document.createTextNode(">"));
1089         parentElement.appendChild(document.createTextNode("\u200B"));
1090     }
1091
1092     _nodeTitleInfo()
1093     {
1094         var node = this.representedObject;
1095         var info = {titleDOM: document.createDocumentFragment(), hasChildren: this.hasChildren};
1096
1097         function trimedNodeValue()
1098         {
1099             // Trim empty lines from the beginning and extra space at the end since most style and script tags begin with a newline
1100             // and end with a newline and indentation for the end tag.
1101             return node.nodeValue().replace(/^[\n\r]*/, "").replace(/\s*$/, "");
1102         }
1103
1104         switch (node.nodeType()) {
1105             case Node.DOCUMENT_FRAGMENT_NODE:
1106                 var fragmentElement = info.titleDOM.createChild("span", "webkit-html-fragment");
1107                 if (node.isInShadowTree()) {
1108                     fragmentElement.textContent = WebInspector.UIString("Shadow Content");
1109                     fragmentElement.classList.add("shadow");
1110                 } else
1111                     fragmentElement.textContent = WebInspector.UIString("Document Fragment");
1112                 break;
1113
1114             case Node.ATTRIBUTE_NODE:
1115                 var value = node.value || "\u200B"; // Zero width space to force showing an empty value.
1116                 this._buildAttributeDOM(info.titleDOM, node.name, value);
1117                 break;
1118
1119             case Node.ELEMENT_NODE:
1120                 var tagName = node.nodeNameInCorrectCase();
1121                 if (this._elementCloseTag) {
1122                     this._buildTagDOM(info.titleDOM, tagName, true, true);
1123                     info.hasChildren = false;
1124                     break;
1125                 }
1126
1127                 this._buildTagDOM(info.titleDOM, tagName, false, false);
1128
1129                 var textChild = this._singleTextChild(node);
1130                 var showInlineText = textChild && textChild.nodeValue().length < WebInspector.DOMTreeElement.MaximumInlineTextChildLength;
1131
1132                 if (!this.expanded && (!showInlineText && (this.treeOutline.isXMLMimeType || !WebInspector.DOMTreeElement.ForbiddenClosingTagElements[tagName]))) {
1133                     if (this.hasChildren) {
1134                         var textNodeElement = info.titleDOM.createChild("span", "html-text-node");
1135                         textNodeElement.textContent = "\u2026";
1136                         info.titleDOM.appendChild(document.createTextNode("\u200B"));
1137                     }
1138                     this._buildTagDOM(info.titleDOM, tagName, true, false);
1139                 }
1140
1141                 // If this element only has a single child that is a text node,
1142                 // just show that text and the closing tag inline rather than
1143                 // create a subtree for them
1144                 if (showInlineText) {
1145                     var textNodeElement = info.titleDOM.createChild("span", "html-text-node");
1146                     var nodeNameLowerCase = node.nodeName().toLowerCase();
1147
1148                     if (nodeNameLowerCase === "script")
1149                         textNodeElement.appendChild(WebInspector.syntaxHighlightStringAsDocumentFragment(textChild.nodeValue().trim(), "text/javascript"));
1150                     else if (nodeNameLowerCase === "style")
1151                         textNodeElement.appendChild(WebInspector.syntaxHighlightStringAsDocumentFragment(textChild.nodeValue().trim(), "text/css"));
1152                     else
1153                         textNodeElement.textContent = textChild.nodeValue();
1154
1155                     info.titleDOM.appendChild(document.createTextNode("\u200B"));
1156
1157                     this._buildTagDOM(info.titleDOM, tagName, true, false);
1158                     info.hasChildren = false;
1159                 }
1160                 break;
1161
1162             case Node.TEXT_NODE:
1163                 if (node.parentNode && node.parentNode.nodeName().toLowerCase() === "script") {
1164                     var newNode = info.titleDOM.createChild("span", "html-text-node large");
1165                     newNode.appendChild(WebInspector.syntaxHighlightStringAsDocumentFragment(trimedNodeValue(), "text/javascript"));
1166                 } else if (node.parentNode && node.parentNode.nodeName().toLowerCase() === "style") {
1167                     var newNode = info.titleDOM.createChild("span", "html-text-node large");
1168                     newNode.appendChild(WebInspector.syntaxHighlightStringAsDocumentFragment(trimedNodeValue(), "text/css"));
1169                 } else {
1170                     info.titleDOM.appendChild(document.createTextNode("\""));
1171                     var textNodeElement = info.titleDOM.createChild("span", "html-text-node");
1172                     textNodeElement.textContent = node.nodeValue();
1173                     info.titleDOM.appendChild(document.createTextNode("\""));
1174                 }
1175                 break;
1176
1177             case Node.COMMENT_NODE:
1178                 var commentElement = info.titleDOM.createChild("span", "html-comment");
1179                 commentElement.appendChild(document.createTextNode("<!--" + node.nodeValue() + "-->"));
1180                 break;
1181
1182             case Node.DOCUMENT_TYPE_NODE:
1183                 var docTypeElement = info.titleDOM.createChild("span", "html-doctype");
1184                 docTypeElement.appendChild(document.createTextNode("<!DOCTYPE " + node.nodeName()));
1185                 if (node.publicId) {
1186                     docTypeElement.appendChild(document.createTextNode(" PUBLIC \"" + node.publicId + "\""));
1187                     if (node.systemId)
1188                         docTypeElement.appendChild(document.createTextNode(" \"" + node.systemId + "\""));
1189                 } else if (node.systemId)
1190                     docTypeElement.appendChild(document.createTextNode(" SYSTEM \"" + node.systemId + "\""));
1191
1192                 if (node.internalSubset)
1193                     docTypeElement.appendChild(document.createTextNode(" [" + node.internalSubset + "]"));
1194
1195                 docTypeElement.appendChild(document.createTextNode(">"));
1196                 break;
1197
1198             case Node.CDATA_SECTION_NODE:
1199                 var cdataElement = info.titleDOM.createChild("span", "html-text-node");
1200                 cdataElement.appendChild(document.createTextNode("<![CDATA[" + node.nodeValue() + "]]>"));
1201                 break;
1202
1203             case Node.PROCESSING_INSTRUCTION_NODE:
1204                 var processingInstructionElement = info.titleDOM.createChild("span", "html-processing-instruction");
1205                 var data = node.nodeValue();
1206                 var dataString = data.length ? " " + data : "";
1207                 var title = "<?" + node.nodeNameInCorrectCase() + dataString + "?>";
1208                 processingInstructionElement.appendChild(document.createTextNode(title));
1209                 break;
1210
1211             default:
1212                 var defaultElement = info.titleDOM.appendChild(document.createTextNode(node.nodeNameInCorrectCase().collapseWhitespace()));
1213         }
1214
1215         return info;
1216     }
1217
1218     _singleTextChild(node)
1219     {
1220         if (!node)
1221             return null;
1222
1223         var firstChild = node.firstChild;
1224         if (!firstChild || firstChild.nodeType() !== Node.TEXT_NODE)
1225             return null;
1226
1227         if (node.hasShadowRoots())
1228             return null;
1229
1230         var sibling = firstChild.nextSibling;
1231         return sibling ? null : firstChild;
1232     }
1233
1234     _showInlineText(node)
1235     {
1236         if (node.nodeType() === Node.ELEMENT_NODE) {
1237             var textChild = this._singleTextChild(node);
1238             if (textChild && textChild.nodeValue().length < WebInspector.DOMTreeElement.MaximumInlineTextChildLength)
1239                 return true;
1240         }
1241         return false;
1242     }
1243
1244     remove()
1245     {
1246         var parentElement = this.parent;
1247         if (!parentElement)
1248             return;
1249
1250         var self = this;
1251         function removeNodeCallback(error, removedNodeId)
1252         {
1253             if (error)
1254                 return;
1255
1256             if (!self.parent)
1257                 return;
1258
1259             parentElement.removeChild(self);
1260             parentElement.adjustCollapsedRange();
1261         }
1262
1263         this.representedObject.removeNode(removeNodeCallback);
1264     }
1265
1266     _editAsHTML()
1267     {
1268         var treeOutline = this.treeOutline;
1269         var node = this.representedObject;
1270         var parentNode = node.parentNode;
1271         var index = node.index;
1272         var wasExpanded = this.expanded;
1273
1274         function selectNode(error, nodeId)
1275         {
1276             if (error)
1277                 return;
1278
1279             // Select it and expand if necessary. We force tree update so that it processes dom events and is up to date.
1280             treeOutline._updateModifiedNodes();
1281
1282             var newNode = parentNode ? parentNode.children[index] || parentNode : null;
1283             if (!newNode)
1284                 return;
1285
1286             treeOutline.selectDOMNode(newNode, true);
1287
1288             if (wasExpanded) {
1289                 var newTreeItem = treeOutline.findTreeElement(newNode);
1290                 if (newTreeItem)
1291                     newTreeItem.expand();
1292             }
1293         }
1294
1295         function commitChange(value)
1296         {
1297             node.setOuterHTML(value, selectNode);
1298         }
1299
1300         node.getOuterHTML(this._startEditingAsHTML.bind(this, commitChange));
1301     }
1302
1303     _copyHTML()
1304     {
1305         this.representedObject.copyNode();
1306     }
1307
1308     _highlightSearchResults()
1309     {
1310         if (!this.title || !this._searchQuery || !this._searchHighlightsVisible)
1311             return;
1312
1313         if (this._highlightResult) {
1314             this._updateSearchHighlight(true);
1315             return;
1316         }
1317
1318         var text = this.title.textContent;
1319         var searchRegex = new RegExp(this._searchQuery.escapeForRegExp(), "gi");
1320
1321         var offset = 0;
1322         var match = searchRegex.exec(text);
1323         var matchRanges = [];
1324         while (match) {
1325             matchRanges.push({ offset: match.index, length: match[0].length });
1326             match = searchRegex.exec(text);
1327         }
1328
1329         // Fall back for XPath, etc. matches.
1330         if (!matchRanges.length)
1331             matchRanges.push({ offset: 0, length: text.length });
1332
1333         this._highlightResult = [];
1334         WebInspector.highlightRangesWithStyleClass(this.title, matchRanges, WebInspector.DOMTreeElement.SearchHighlightStyleClassName, this._highlightResult);
1335     }
1336
1337     handleEvent(event)
1338     {
1339         if (event.type === "dragstart" && this._editing)
1340             event.preventDefault();
1341     }
1342 };
1343
1344 WebInspector.DOMTreeElement.InitialChildrenLimit = 500;
1345 WebInspector.DOMTreeElement.MaximumInlineTextChildLength = 80;
1346
1347 // A union of HTML4 and HTML5-Draft elements that explicitly
1348 // or implicitly (for HTML5) forbid the closing tag.
1349 WebInspector.DOMTreeElement.ForbiddenClosingTagElements = [
1350     "area", "base", "basefont", "br", "canvas", "col", "command", "embed", "frame",
1351     "hr", "img", "input", "isindex", "keygen", "link", "meta", "param", "source",
1352     "wbr", "track", "menuitem"
1353 ].keySet();
1354
1355 // These tags we do not allow editing their tag name.
1356 WebInspector.DOMTreeElement.EditTagBlacklist = [
1357     "html", "head", "body"
1358 ].keySet();
1359
1360 WebInspector.DOMTreeElement.SearchHighlightStyleClassName = "search-highlight";
1361 WebInspector.DOMTreeElement.BouncyHighlightStyleClassName = "bouncy-highlight";