tizen beta release
[profile/ivi/webkit-efl.git] / Source / WebCore / inspector / front-end / ElementsTreeOutline.js
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
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 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.
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 /**
32  * @constructor
33  * @extends {TreeOutline}
34  * @param {boolean=} omitRootDOMNode
35  * @param {boolean=} selectEnabled
36  * @param {boolean=} showInElementsPanelEnabled
37  * @param {function(WebInspector.ContextMenu, WebInspector.DOMNode)=} contextMenuCallback
38  */
39 WebInspector.ElementsTreeOutline = function(omitRootDOMNode, selectEnabled, showInElementsPanelEnabled, contextMenuCallback)
40 {
41     this.element = document.createElement("ol");
42     this.element.addEventListener("mousedown", this._onmousedown.bind(this), false);
43     this.element.addEventListener("mousemove", this._onmousemove.bind(this), false);
44     this.element.addEventListener("mouseout", this._onmouseout.bind(this), false);
45     this.element.addEventListener("dragstart", this._ondragstart.bind(this), false);
46     this.element.addEventListener("dragover", this._ondragover.bind(this), false);
47     this.element.addEventListener("dragleave", this._ondragleave.bind(this), false);
48     this.element.addEventListener("drop", this._ondrop.bind(this), false);
49     this.element.addEventListener("dragend", this._ondragend.bind(this), false);
50
51     TreeOutline.call(this, this.element);
52
53     this._includeRootDOMNode = !omitRootDOMNode;
54     this._selectEnabled = selectEnabled;
55     this._showInElementsPanelEnabled = showInElementsPanelEnabled;
56     this._rootDOMNode = null;
57     this._selectDOMNode = null;
58     this._eventSupport = new WebInspector.Object();
59     this._editing = false;
60
61     this._visible = false;
62
63     this.element.addEventListener("contextmenu", this._contextMenuEventFired.bind(this), true);
64     this._contextMenuCallback = contextMenuCallback;
65 }
66
67 WebInspector.ElementsTreeOutline.Events = {
68     SelectedNodeChanged: "SelectedNodeChanged"
69 }
70
71 WebInspector.ElementsTreeOutline.prototype = {
72     wireToDomAgent: function()
73     {
74         this._elementsTreeUpdater = new WebInspector.ElementsTreeUpdater(this);
75     },
76
77     setVisible: function(visible)
78     {
79         this._visible = visible;
80         if (!this._visible)
81             return;
82
83         this._updateModifiedNodes();
84         if (this._selectedDOMNode)
85             this._revealAndSelectNode(this._selectedDOMNode, false);
86     },
87
88     addEventListener: function(eventType, listener, thisObject)
89     {
90         this._eventSupport.addEventListener(eventType, listener, thisObject);
91     },
92
93     removeEventListener: function(eventType, listener, thisObject)
94     {
95         this._eventSupport.removeEventListener(eventType, listener, thisObject);
96     },
97
98     get rootDOMNode()
99     {
100         return this._rootDOMNode;
101     },
102
103     set rootDOMNode(x)
104     {
105         if (this._rootDOMNode === x)
106             return;
107
108         this._rootDOMNode = x;
109
110         this._isXMLMimeType = x && x.isXMLNode();
111
112         this.update();
113     },
114
115     get isXMLMimeType()
116     {
117         return this._isXMLMimeType;
118     },
119
120     selectedDOMNode: function()
121     {
122         return this._selectedDOMNode;
123     },
124
125     selectDOMNode: function(node, focus)
126     {
127         if (this._selectedDOMNode === node) {
128             this._revealAndSelectNode(node, !focus);
129             return;
130         }
131
132         this._selectedDOMNode = node;
133         this._revealAndSelectNode(node, !focus);
134
135         // The _revealAndSelectNode() method might find a different element if there is inlined text,
136         // and the select() call would change the selectedDOMNode and reenter this setter. So to
137         // avoid calling _selectedNodeChanged() twice, first check if _selectedDOMNode is the same
138         // node as the one passed in.
139         if (this._selectedDOMNode === node)
140             this._selectedNodeChanged();
141     },
142
143     get editing()
144     {
145         return this._editing;
146     },
147
148     update: function()
149     {
150         var selectedNode = this.selectedTreeElement ? this.selectedTreeElement.representedObject : null;
151
152         this.removeChildren();
153
154         if (!this.rootDOMNode)
155             return;
156
157         var treeElement;
158         if (this._includeRootDOMNode) {
159             treeElement = new WebInspector.ElementsTreeElement(this.rootDOMNode);
160             treeElement.selectable = this._selectEnabled;
161             this.appendChild(treeElement);
162         } else {
163             // FIXME: this could use findTreeElement to reuse a tree element if it already exists
164             var node = this.rootDOMNode.firstChild;
165             while (node) {
166                 treeElement = new WebInspector.ElementsTreeElement(node);
167                 treeElement.selectable = this._selectEnabled;
168                 this.appendChild(treeElement);
169                 node = node.nextSibling;
170             }
171         }
172
173         if (selectedNode)
174             this._revealAndSelectNode(selectedNode, true);
175     },
176
177     updateSelection: function()
178     {
179         if (!this.selectedTreeElement)
180             return;
181         var element = this.treeOutline.selectedTreeElement;
182         element.updateSelection();
183     },
184
185     _selectedNodeChanged: function()
186     {
187         this._eventSupport.dispatchEventToListeners(WebInspector.ElementsTreeOutline.Events.SelectedNodeChanged);
188     },
189
190     findTreeElement: function(node)
191     {
192         function isAncestorNode(ancestor, node)
193         {
194             return ancestor.isAncestor(node);
195         }
196
197         function parentNode(node)
198         {
199             return node.parentNode;
200         }
201
202         var treeElement = TreeOutline.prototype.findTreeElement.call(this, node, isAncestorNode, parentNode);
203         if (!treeElement && node.nodeType() === Node.TEXT_NODE) {
204             // The text node might have been inlined if it was short, so try to find the parent element.
205             treeElement = TreeOutline.prototype.findTreeElement.call(this, node.parentNode, isAncestorNode, parentNode);
206         }
207
208         return treeElement;
209     },
210
211     createTreeElementFor: function(node)
212     {
213         var treeElement = this.findTreeElement(node);
214         if (treeElement)
215             return treeElement;
216         if (!node.parentNode)
217             return null;
218
219         treeElement = this.createTreeElementFor(node.parentNode);
220         if (treeElement && treeElement.showChild(node.index))
221             return treeElement.children[node.index];
222
223         return null;
224     },
225
226     set suppressRevealAndSelect(x)
227     {
228         if (this._suppressRevealAndSelect === x)
229             return;
230         this._suppressRevealAndSelect = x;
231     },
232
233     _revealAndSelectNode: function(node, omitFocus)
234     {
235         if (!node || this._suppressRevealAndSelect)
236             return;
237
238         var treeElement = this.createTreeElementFor(node);
239         if (!treeElement)
240             return;
241
242         treeElement.revealAndSelect(omitFocus);
243     },
244
245     _treeElementFromEvent: function(event)
246     {
247         var scrollContainer = this.element.parentElement;
248
249         // We choose this X coordinate based on the knowledge that our list
250         // items extend at least to the right edge of the outer <ol> container.
251         // In the no-word-wrap mode the outer <ol> may be wider than the tree container
252         // (and partially hidden), in which case we are left to use only its right boundary.
253         var x = scrollContainer.totalOffsetLeft() + scrollContainer.offsetWidth - 36;
254
255         var y = event.pageY;
256
257         // Our list items have 1-pixel cracks between them vertically. We avoid
258         // the cracks by checking slightly above and slightly below the mouse
259         // and seeing if we hit the same element each time.
260         var elementUnderMouse = this.treeElementFromPoint(x, y);
261         var elementAboveMouse = this.treeElementFromPoint(x, y - 2);
262         var element;
263         if (elementUnderMouse === elementAboveMouse)
264             element = elementUnderMouse;
265         else
266             element = this.treeElementFromPoint(x, y + 2);
267
268         return element;
269     },
270
271     _onmousedown: function(event)
272     {
273         var element = this._treeElementFromEvent(event);
274
275         if (!element || element.isEventWithinDisclosureTriangle(event))
276             return;
277
278         element.select();
279     },
280
281     _onmousemove: function(event)
282     {
283         var element = this._treeElementFromEvent(event);
284         if (element && this._previousHoveredElement === element)
285             return;
286
287         if (this._previousHoveredElement) {
288             this._previousHoveredElement.hovered = false;
289             delete this._previousHoveredElement;
290         }
291
292         if (element) {
293             element.hovered = true;
294             this._previousHoveredElement = element;
295
296             // Lazily compute tag-specific tooltips.
297             if (element.representedObject && !element.tooltip)
298                 element._createTooltipForNode();
299         }
300
301         WebInspector.domAgent.highlightDOMNode(element ? element.representedObject.id : 0);
302     },
303
304     _onmouseout: function(event)
305     {
306         var nodeUnderMouse = document.elementFromPoint(event.pageX, event.pageY);
307         if (nodeUnderMouse && nodeUnderMouse.isDescendant(this.element))
308             return;
309
310         if (this._previousHoveredElement) {
311             this._previousHoveredElement.hovered = false;
312             delete this._previousHoveredElement;
313         }
314
315         WebInspector.domAgent.hideDOMNodeHighlight();
316     },
317
318     _ondragstart: function(event)
319     {
320         var treeElement = this._treeElementFromEvent(event);
321         if (!treeElement)
322             return false;
323
324         if (!this._isValidDragSourceOrTarget(treeElement))
325             return false;
326
327         if (treeElement.representedObject.nodeName() === "BODY" || treeElement.representedObject.nodeName() === "HEAD")
328             return false;
329
330         event.dataTransfer.setData("text/plain", treeElement.listItemElement.textContent);
331         event.dataTransfer.effectAllowed = "copyMove";
332         this._nodeBeingDragged = treeElement.representedObject;
333
334         WebInspector.domAgent.hideDOMNodeHighlight();
335
336         return true;
337     },
338
339     _ondragover: function(event)
340     {
341         if (!this._nodeBeingDragged)
342             return false;
343
344         var treeElement = this._treeElementFromEvent(event);
345         if (!this._isValidDragSourceOrTarget(treeElement))
346             return false;
347
348         var node = treeElement.representedObject;
349         while (node) {
350             if (node === this._nodeBeingDragged)
351                 return false;
352             node = node.parentNode;
353         }
354
355         treeElement.updateSelection();
356         treeElement.listItemElement.addStyleClass("elements-drag-over");
357         this._dragOverTreeElement = treeElement;
358         event.preventDefault();
359         event.dataTransfer.dropEffect = 'move';
360         return false;
361     },
362
363     _ondragleave: function(event)
364     {
365         this._clearDragOverTreeElementMarker();
366         event.preventDefault();
367         return false;
368     },
369
370     _isValidDragSourceOrTarget: function(treeElement)
371     {
372         if (!treeElement)
373             return false;
374
375         var node = treeElement.representedObject;
376         if (!(node instanceof WebInspector.DOMNode))
377             return false;
378
379         if (!node.parentNode || node.parentNode.nodeType() !== Node.ELEMENT_NODE)
380             return false;
381
382         return true;
383     },
384
385     _ondrop: function(event)
386     {
387         event.preventDefault();
388         var treeElement = this._treeElementFromEvent(event);
389         if (this._nodeBeingDragged && treeElement) {
390             var parentNode;
391             var anchorNode;
392
393             if (treeElement._elementCloseTag) {
394                 // Drop onto closing tag -> insert as last child.
395                 parentNode = treeElement.representedObject;
396             } else {
397                 var dragTargetNode = treeElement.representedObject;
398                 parentNode = dragTargetNode.parentNode;
399                 anchorNode = dragTargetNode;
400             }
401
402             function callback(error, newNodeId)
403             {
404                 if (error)
405                     return;
406
407                 this._updateModifiedNodes();
408                 var newNode = WebInspector.domAgent.nodeForId(newNodeId);
409                 if (newNode)
410                     this.selectDOMNode(newNode, true);
411             }
412             this._nodeBeingDragged.moveTo(parentNode, anchorNode, callback.bind(this));
413         }
414
415         delete this._nodeBeingDragged;
416     },
417
418     _ondragend: function(event)
419     {
420         event.preventDefault();
421         this._clearDragOverTreeElementMarker();
422         delete this._nodeBeingDragged;
423     },
424
425     _clearDragOverTreeElementMarker: function()
426     {
427         if (this._dragOverTreeElement) {
428             this._dragOverTreeElement.updateSelection();
429             this._dragOverTreeElement.listItemElement.removeStyleClass("elements-drag-over");
430             delete this._dragOverTreeElement;
431         }
432     },
433
434     _contextMenuEventFired: function(event)
435     {
436         if (!this._showInElementsPanelEnabled)
437             return;
438
439         var treeElement = this._treeElementFromEvent(event);
440         if (!treeElement)
441             return;
442
443         function focusElement()
444         {
445             WebInspector.domAgent.inspectElement(treeElement.representedObject.id);
446         }
447         var contextMenu = new WebInspector.ContextMenu();
448         contextMenu.appendItem(WebInspector.UIString("Reveal in Elements Panel"), focusElement.bind(this));
449         contextMenu.show(event);
450     },
451
452     populateContextMenu: function(contextMenu, event)
453     {
454         var treeElement = this._treeElementFromEvent(event);
455         if (!treeElement)
456             return false;
457
458         var tag = event.target.enclosingNodeOrSelfWithClass("webkit-html-tag");
459         var textNode = event.target.enclosingNodeOrSelfWithClass("webkit-html-text-node");
460         var commentNode = event.target.enclosingNodeOrSelfWithClass("webkit-html-comment");
461         var populated = WebInspector.populateHrefContextMenu(contextMenu, this.selectedDOMNode(), event);
462         if (tag && treeElement._populateTagContextMenu) {
463             if (populated)
464                 contextMenu.appendSeparator();
465             treeElement._populateTagContextMenu(contextMenu, event);
466             populated = true;
467         } else if (textNode && treeElement._populateTextContextMenu) {
468             if (populated)
469                 contextMenu.appendSeparator();
470             treeElement._populateTextContextMenu(contextMenu, textNode);
471             populated = true;
472         } else if (commentNode && treeElement._populateNodeContextMenu) {
473             if (populated)
474                 contextMenu.appendSeparator();
475             treeElement._populateNodeContextMenu(contextMenu, textNode);
476             populated = true;
477         }
478
479         return populated;
480     },
481
482     adjustCollapsedRange: function()
483     {
484     },
485
486     _updateModifiedNodes: function()
487     {
488         if (this._elementsTreeUpdater)
489             this._elementsTreeUpdater._updateModifiedNodes();
490     },
491
492     _populateContextMenu: function(contextMenu, node)
493     {
494         if (this._contextMenuCallback)
495             this._contextMenuCallback(contextMenu, node);
496     }
497 }
498
499 WebInspector.ElementsTreeOutline.prototype.__proto__ = TreeOutline.prototype;
500
501 /**
502  * @constructor
503  * @extends {TreeElement}
504  * @param {boolean=} elementCloseTag
505  */
506 WebInspector.ElementsTreeElement = function(node, elementCloseTag)
507 {
508     this._elementCloseTag = elementCloseTag;
509     var hasChildrenOverride = !elementCloseTag && node.hasChildNodes() && !this._showInlineText(node);
510
511     // The title will be updated in onattach.
512     TreeElement.call(this, "", node, hasChildrenOverride);
513
514     if (this.representedObject.nodeType() == Node.ELEMENT_NODE && !elementCloseTag)
515         this._canAddAttributes = true;
516     this._searchQuery = null;
517     this._expandedChildrenLimit = WebInspector.ElementsTreeElement.InitialChildrenLimit;
518 }
519
520 WebInspector.ElementsTreeElement.InitialChildrenLimit = 500;
521
522 // A union of HTML4 and HTML5-Draft elements that explicitly
523 // or implicitly (for HTML5) forbid the closing tag.
524 // FIXME: Revise once HTML5 Final is published.
525 WebInspector.ElementsTreeElement.ForbiddenClosingTagElements = [
526     "area", "base", "basefont", "br", "canvas", "col", "command", "embed", "frame",
527     "hr", "img", "input", "isindex", "keygen", "link", "meta", "param", "source"
528 ].keySet();
529
530 // These tags we do not allow editing their tag name.
531 WebInspector.ElementsTreeElement.EditTagBlacklist = [
532     "html", "head", "body"
533 ].keySet();
534
535 WebInspector.ElementsTreeElement.prototype = {
536     highlightSearchResults: function(searchQuery)
537     {
538         if (this._searchQuery !== searchQuery) {
539             this._updateSearchHighlight(false);
540             delete this._highlightResult; // A new search query.
541         }
542
543         this._searchQuery = searchQuery;
544         this._searchHighlightsVisible = true;
545         this.updateTitle(true);
546     },
547
548     hideSearchHighlights: function()
549     {
550         delete this._searchHighlightsVisible;
551         this._updateSearchHighlight(false);
552     },
553
554     _updateSearchHighlight: function(show)
555     {
556         if (!this._highlightResult)
557             return;
558
559         function updateEntryShow(entry)
560         {
561             switch (entry.type) {
562                 case "added":
563                     entry.parent.insertBefore(entry.node, entry.nextSibling);
564                     break;
565                 case "changed":
566                     entry.node.textContent = entry.newText;
567                     break;
568             }
569         }
570
571         function updateEntryHide(entry)
572         {
573             switch (entry.type) {
574                 case "added":
575                     if (entry.node.parentElement)
576                         entry.node.parentElement.removeChild(entry.node);
577                     break;
578                 case "changed":
579                     entry.node.textContent = entry.oldText;
580                     break;
581             }
582         }
583
584         var updater = show ? updateEntryShow : updateEntryHide;
585
586         for (var i = 0, size = this._highlightResult.length; i < size; ++i)
587             updater(this._highlightResult[i]);
588     },
589
590     get hovered()
591     {
592         return this._hovered;
593     },
594
595     set hovered(x)
596     {
597         if (this._hovered === x)
598             return;
599
600         this._hovered = x;
601
602         if (this.listItemElement) {
603             if (x) {
604                 this.updateSelection();
605                 this.listItemElement.addStyleClass("hovered");
606             } else {
607                 this.listItemElement.removeStyleClass("hovered");
608             }
609         }
610     },
611
612     get expandedChildrenLimit()
613     {
614         return this._expandedChildrenLimit;
615     },
616
617     set expandedChildrenLimit(x)
618     {
619         if (this._expandedChildrenLimit === x)
620             return;
621
622         this._expandedChildrenLimit = x;
623         if (this.treeOutline && !this._updateChildrenInProgress)
624             this._updateChildren(true);
625     },
626
627     get expandedChildCount()
628     {
629         var count = this.children.length;
630         if (count && this.children[count - 1]._elementCloseTag)
631             count--;
632         if (count && this.children[count - 1].expandAllButton)
633             count--;
634         return count;
635     },
636
637     showChild: function(index)
638     {
639         if (this._elementCloseTag)
640             return;
641
642         if (index >= this.expandedChildrenLimit) {
643             this._expandedChildrenLimit = index + 1;
644             this._updateChildren(true);
645         }
646
647         // Whether index-th child is visible in the children tree
648         return this.expandedChildCount > index;
649     },
650
651     _createTooltipForNode: function()
652     {
653         var node = this.representedObject;
654         if (!node.nodeName() || node.nodeName().toLowerCase() !== "img")
655             return;
656
657         function setTooltip(result)
658         {
659             if (!result || result.type !== "string")
660                 return;
661
662             try {
663                 var properties = JSON.parse(result.description);
664                 var offsetWidth = properties[0];
665                 var offsetHeight = properties[1];
666                 var naturalWidth = properties[2];
667                 var naturalHeight = properties[3];
668                 if (offsetHeight === naturalHeight && offsetWidth === naturalWidth)
669                     this.tooltip = WebInspector.UIString("%d \xd7 %d pixels", offsetWidth, offsetHeight);
670                 else
671                     this.tooltip = WebInspector.UIString("%d \xd7 %d pixels (Natural: %d \xd7 %d pixels)", offsetWidth, offsetHeight, naturalWidth, naturalHeight);
672             } catch (e) {
673                 console.error(e);
674             }
675         }
676
677         function resolvedNode(object)
678         {
679             if (!object)
680                 return;
681
682             function dimensions()
683             {
684                 return "[" + this.offsetWidth + "," + this.offsetHeight + "," + this.naturalWidth + "," + this.naturalHeight + "]";
685             }
686
687             object.callFunction(dimensions, setTooltip.bind(this));
688             object.release();
689         }
690         WebInspector.RemoteObject.resolveNode(node, "", resolvedNode.bind(this));
691     },
692
693     updateSelection: function()
694     {
695         var listItemElement = this.listItemElement;
696         if (!listItemElement)
697             return;
698
699         if (document.body.offsetWidth <= 0) {
700             // The stylesheet hasn't loaded yet or the window is closed,
701             // so we can't calculate what is need. Return early.
702             return;
703         }
704
705         if (!this.selectionElement) {
706             this.selectionElement = document.createElement("div");
707             this.selectionElement.className = "selection selected";
708             listItemElement.insertBefore(this.selectionElement, listItemElement.firstChild);
709         }
710
711         this.selectionElement.style.height = listItemElement.offsetHeight + "px";
712     },
713
714     onattach: function()
715     {
716         if (this._hovered) {
717             this.updateSelection();
718             this.listItemElement.addStyleClass("hovered");
719         }
720
721         this.updateTitle();
722         this._preventFollowingLinksOnDoubleClick();
723         this.listItemElement.draggable = true;
724     },
725
726     _preventFollowingLinksOnDoubleClick: function()
727     {
728         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");
729         if (!links)
730             return;
731
732         for (var i = 0; i < links.length; ++i)
733             links[i].preventFollowOnDoubleClick = true;
734     },
735
736     onpopulate: function()
737     {
738         if (this.children.length || this._showInlineText(this.representedObject) || this._elementCloseTag)
739             return;
740
741         this.updateChildren();
742     },
743
744     /**
745      * @param {boolean=} fullRefresh
746      */
747     updateChildren: function(fullRefresh)
748     {
749         if (this._elementCloseTag)
750             return;
751         this.representedObject.getChildNodes(this._updateChildren.bind(this, fullRefresh));
752     },
753
754     /**
755      * @param {boolean=} closingTag
756      */
757     insertChildElement: function(child, index, closingTag)
758     {
759         var newElement = new WebInspector.ElementsTreeElement(child, closingTag);
760         newElement.selectable = this.treeOutline._selectEnabled;
761         this.insertChild(newElement, index);
762         return newElement;
763     },
764
765     moveChild: function(child, targetIndex)
766     {
767         var wasSelected = child.selected;
768         this.removeChild(child);
769         this.insertChild(child, targetIndex);
770         if (wasSelected)
771             child.select();
772     },
773
774     /**
775      * @param {boolean=} fullRefresh
776      */
777     _updateChildren: function(fullRefresh)
778     {
779         if (this._updateChildrenInProgress || !this.treeOutline._visible)
780             return;
781
782         this._updateChildrenInProgress = true;
783         var selectedNode = this.treeOutline.selectedDOMNode();
784         var originalScrollTop = 0;
785         if (fullRefresh) {
786             var treeOutlineContainerElement = this.treeOutline.element.parentNode;
787             originalScrollTop = treeOutlineContainerElement.scrollTop;
788             var selectedTreeElement = this.treeOutline.selectedTreeElement;
789             if (selectedTreeElement && selectedTreeElement.hasAncestor(this))
790                 this.select();
791             this.removeChildren();
792         }
793
794         var treeElement = this;
795         var treeChildIndex = 0;
796         var elementToSelect;
797
798         function updateChildrenOfNode(node)
799         {
800             var treeOutline = treeElement.treeOutline;
801             var child = node.firstChild;
802             while (child) {
803                 var currentTreeElement = treeElement.children[treeChildIndex];
804                 if (!currentTreeElement || currentTreeElement.representedObject !== child) {
805                     // Find any existing element that is later in the children list.
806                     var existingTreeElement = null;
807                     for (var i = (treeChildIndex + 1), size = treeElement.expandedChildCount; i < size; ++i) {
808                         if (treeElement.children[i].representedObject === child) {
809                             existingTreeElement = treeElement.children[i];
810                             break;
811                         }
812                     }
813
814                     if (existingTreeElement && existingTreeElement.parent === treeElement) {
815                         // If an existing element was found and it has the same parent, just move it.
816                         treeElement.moveChild(existingTreeElement, treeChildIndex);
817                     } else {
818                         // No existing element found, insert a new element.
819                         if (treeChildIndex < treeElement.expandedChildrenLimit) {
820                             var newElement = treeElement.insertChildElement(child, treeChildIndex);
821                             if (child === selectedNode)
822                                 elementToSelect = newElement;
823                             if (treeElement.expandedChildCount > treeElement.expandedChildrenLimit)
824                                 treeElement.expandedChildrenLimit++;
825                         }
826                     }
827                 }
828
829                 child = child.nextSibling;
830                 ++treeChildIndex;
831             }
832         }
833
834         // Remove any tree elements that no longer have this node (or this node's contentDocument) as their parent.
835         for (var i = (this.children.length - 1); i >= 0; --i) {
836             var currentChild = this.children[i];
837             var currentNode = currentChild.representedObject;
838             var currentParentNode = currentNode.parentNode;
839
840             if (currentParentNode === this.representedObject)
841                 continue;
842
843             var selectedTreeElement = this.treeOutline.selectedTreeElement;
844             if (selectedTreeElement && (selectedTreeElement === currentChild || selectedTreeElement.hasAncestor(currentChild)))
845                 this.select();
846
847             this.removeChildAtIndex(i);
848         }
849
850         updateChildrenOfNode(this.representedObject);
851         this.adjustCollapsedRange();
852
853         var lastChild = this.children[this.children.length - 1];
854         if (this.representedObject.nodeType() == Node.ELEMENT_NODE && (!lastChild || !lastChild._elementCloseTag))
855             this.insertChildElement(this.representedObject, this.children.length, true);
856
857         // We want to restore the original selection and tree scroll position after a full refresh, if possible.
858         if (fullRefresh && elementToSelect) {
859             elementToSelect.select();
860             if (treeOutlineContainerElement && originalScrollTop <= treeOutlineContainerElement.scrollHeight)
861                 treeOutlineContainerElement.scrollTop = originalScrollTop;
862         }
863
864         delete this._updateChildrenInProgress;
865     },
866
867     adjustCollapsedRange: function()
868     {
869         // Ensure precondition: only the tree elements for node children are found in the tree
870         // (not the Expand All button or the closing tag).
871         if (this.expandAllButtonElement && this.expandAllButtonElement.__treeElement.parent)
872             this.removeChild(this.expandAllButtonElement.__treeElement);
873
874         const node = this.representedObject;
875         if (!node.children)
876             return;
877         const childNodeCount = node.children.length;
878
879         // In case some nodes from the expanded range were removed, pull some nodes from the collapsed range into the expanded range at the bottom.
880         for (var i = this.expandedChildCount, limit = Math.min(this.expandedChildrenLimit, childNodeCount); i < limit; ++i)
881             this.insertChildElement(node.children[i], i);
882
883         const expandedChildCount = this.expandedChildCount;
884         if (childNodeCount > this.expandedChildCount) {
885             var targetButtonIndex = expandedChildCount;
886             if (!this.expandAllButtonElement) {
887                 var button = document.createElement("button");
888                 button.className = "show-all-nodes";
889                 button.value = "";
890                 var item = new TreeElement(button, null, false);
891                 item.selectable = false;
892                 item.expandAllButton = true;
893                 this.insertChild(item, targetButtonIndex);
894                 this.expandAllButtonElement = item.listItemElement.firstChild;
895                 this.expandAllButtonElement.__treeElement = item;
896                 this.expandAllButtonElement.addEventListener("click", this.handleLoadAllChildren.bind(this), false);
897             } else if (!this.expandAllButtonElement.__treeElement.parent)
898                 this.insertChild(this.expandAllButtonElement.__treeElement, targetButtonIndex);
899             this.expandAllButtonElement.textContent = WebInspector.UIString("Show All Nodes (%d More)", childNodeCount - expandedChildCount);
900         } else if (this.expandAllButtonElement)
901             delete this.expandAllButtonElement;
902     },
903
904     handleLoadAllChildren: function()
905     {
906         this.expandedChildrenLimit = Math.max(this.representedObject._childNodeCount, this.expandedChildrenLimit + WebInspector.ElementsTreeElement.InitialChildrenLimit);
907     },
908
909     onexpand: function()
910     {
911         if (this._elementCloseTag)
912             return;
913
914         this.updateTitle();
915         this.treeOutline.updateSelection();
916     },
917
918     oncollapse: function()
919     {
920         if (this._elementCloseTag)
921             return;
922
923         this.updateTitle();
924         this.treeOutline.updateSelection();
925     },
926
927     onreveal: function()
928     {
929         if (this.listItemElement) {
930             var tagSpans = this.listItemElement.getElementsByClassName("webkit-html-tag-name");
931             if (tagSpans.length)
932                 tagSpans[0].scrollIntoViewIfNeeded(false);
933             else
934                 this.listItemElement.scrollIntoViewIfNeeded(false);
935         }
936     },
937
938     onselect: function(treeElement, selectedByUser)
939     {
940         this.treeOutline.suppressRevealAndSelect = true;
941         this.treeOutline.selectDOMNode(this.representedObject, selectedByUser);
942         if (selectedByUser)
943             WebInspector.domAgent.highlightDOMNode(this.representedObject.id);
944         this.updateSelection();
945         this.treeOutline.suppressRevealAndSelect = false;
946     },
947
948     ondelete: function()
949     {
950         var startTagTreeElement = this.treeOutline.findTreeElement(this.representedObject);
951         startTagTreeElement ? startTagTreeElement.remove() : this.remove();
952         return true;
953     },
954
955     onenter: function()
956     {
957         // On Enter or Return start editing the first attribute
958         // or create a new attribute on the selected element.
959         if (this.treeOutline.editing)
960             return false;
961
962         this._startEditing();
963
964         // prevent a newline from being immediately inserted
965         return true;
966     },
967
968     selectOnMouseDown: function(event)
969     {
970         TreeElement.prototype.selectOnMouseDown.call(this, event);
971
972         if (this._editing)
973             return;
974
975         if (this.treeOutline._showInElementsPanelEnabled) {
976             WebInspector.showPanel("elements");
977             this.treeOutline.selectDOMNode(this.representedObject, true);
978         }
979
980         // Prevent selecting the nearest word on double click.
981         if (event.detail >= 2)
982             event.preventDefault();
983     },
984
985     ondblclick: function(event)
986     {
987         if (this._editing || this._elementCloseTag)
988             return;
989
990         if (this._startEditingTarget(event.target))
991             return;
992
993         if (this.hasChildren && !this.expanded)
994             this.expand();
995     },
996
997     _insertInLastAttributePosition: function(tag, node)
998     {
999         if (tag.getElementsByClassName("webkit-html-attribute").length > 0)
1000             tag.insertBefore(node, tag.lastChild);
1001         else {
1002             var nodeName = tag.textContent.match(/^<(.*?)>$/)[1];
1003             tag.textContent = '';
1004             tag.appendChild(document.createTextNode('<'+nodeName));
1005             tag.appendChild(node);
1006             tag.appendChild(document.createTextNode('>'));
1007         }
1008
1009         this.updateSelection();
1010     },
1011
1012     _startEditingTarget: function(eventTarget)
1013     {
1014         if (this.treeOutline.selectedDOMNode() != this.representedObject)
1015             return;
1016
1017         if (this.representedObject.nodeType() != Node.ELEMENT_NODE && this.representedObject.nodeType() != Node.TEXT_NODE)
1018             return false;
1019
1020         var textNode = eventTarget.enclosingNodeOrSelfWithClass("webkit-html-text-node");
1021         if (textNode)
1022             return this._startEditingTextNode(textNode);
1023
1024         var attribute = eventTarget.enclosingNodeOrSelfWithClass("webkit-html-attribute");
1025         if (attribute)
1026             return this._startEditingAttribute(attribute, eventTarget);
1027
1028         var tagName = eventTarget.enclosingNodeOrSelfWithClass("webkit-html-tag-name");
1029         if (tagName)
1030             return this._startEditingTagName(tagName);
1031
1032         var newAttribute = eventTarget.enclosingNodeOrSelfWithClass("add-attribute");
1033         if (newAttribute)
1034             return this._addNewAttribute();
1035
1036         return false;
1037     },
1038
1039     _populateTagContextMenu: function(contextMenu, event)
1040     {
1041         var attribute = event.target.enclosingNodeOrSelfWithClass("webkit-html-attribute");
1042         var newAttribute = event.target.enclosingNodeOrSelfWithClass("add-attribute");
1043
1044         // Add attribute-related actions.
1045         contextMenu.appendItem(WebInspector.UIString(WebInspector.useLowerCaseMenuTitles() ? "Add attribute" : "Add Attribute"), this._addNewAttribute.bind(this));
1046         if (attribute && !newAttribute)
1047             contextMenu.appendItem(WebInspector.UIString(WebInspector.useLowerCaseMenuTitles() ? "Edit attribute" : "Edit Attribute"), this._startEditingAttribute.bind(this, attribute, event.target));
1048         contextMenu.appendSeparator();
1049
1050         this._populateNodeContextMenu(contextMenu);
1051         this.treeOutline._populateContextMenu(contextMenu, this.representedObject);
1052     },
1053
1054     _populateTextContextMenu: function(contextMenu, textNode)
1055     {
1056         contextMenu.appendItem(WebInspector.UIString(WebInspector.useLowerCaseMenuTitles() ? "Edit text" : "Edit Text"), this._startEditingTextNode.bind(this, textNode));
1057         this._populateNodeContextMenu(contextMenu);
1058     },
1059
1060     _populateNodeContextMenu: function(contextMenu)
1061     {
1062         // Add free-form node-related actions.
1063         contextMenu.appendItem(WebInspector.UIString("Edit as HTML"), this._editAsHTML.bind(this));
1064         contextMenu.appendItem(WebInspector.UIString("Copy as HTML"), this._copyHTML.bind(this));
1065         contextMenu.appendItem(WebInspector.UIString(WebInspector.useLowerCaseMenuTitles() ? "Delete node" : "Delete Node"), this.remove.bind(this));
1066     },
1067
1068     _startEditing: function()
1069     {
1070         if (this.treeOutline.selectedDOMNode() !== this.representedObject)
1071             return;
1072
1073         var listItem = this._listItemNode;
1074
1075         if (this._canAddAttributes) {
1076             var attribute = listItem.getElementsByClassName("webkit-html-attribute")[0];
1077             if (attribute)
1078                 return this._startEditingAttribute(attribute, attribute.getElementsByClassName("webkit-html-attribute-value")[0]);
1079
1080             return this._addNewAttribute();
1081         }
1082
1083         if (this.representedObject.nodeType() === Node.TEXT_NODE) {
1084             var textNode = listItem.getElementsByClassName("webkit-html-text-node")[0];
1085             if (textNode)
1086                 return this._startEditingTextNode(textNode);
1087             return;
1088         }
1089     },
1090
1091     _addNewAttribute: function()
1092     {
1093         // Cannot just convert the textual html into an element without
1094         // a parent node. Use a temporary span container for the HTML.
1095         var container = document.createElement("span");
1096         this._buildAttributeDOM(container, " ", "");
1097         var attr = container.firstChild;
1098         attr.style.marginLeft = "2px"; // overrides the .editing margin rule
1099         attr.style.marginRight = "2px"; // overrides the .editing margin rule
1100
1101         var tag = this.listItemElement.getElementsByClassName("webkit-html-tag")[0];
1102         this._insertInLastAttributePosition(tag, attr);
1103         return this._startEditingAttribute(attr, attr);
1104     },
1105
1106     _triggerEditAttribute: function(attributeName)
1107     {
1108         var attributeElements = this.listItemElement.getElementsByClassName("webkit-html-attribute-name");
1109         for (var i = 0, len = attributeElements.length; i < len; ++i) {
1110             if (attributeElements[i].textContent === attributeName) {
1111                 for (var elem = attributeElements[i].nextSibling; elem; elem = elem.nextSibling) {
1112                     if (elem.nodeType !== Node.ELEMENT_NODE)
1113                         continue;
1114
1115                     if (elem.hasStyleClass("webkit-html-attribute-value"))
1116                         return this._startEditingAttribute(elem.parentNode, elem);
1117                 }
1118             }
1119         }
1120     },
1121
1122     _startEditingAttribute: function(attribute, elementForSelection)
1123     {
1124         if (WebInspector.isBeingEdited(attribute))
1125             return true;
1126
1127         var attributeNameElement = attribute.getElementsByClassName("webkit-html-attribute-name")[0];
1128         if (!attributeNameElement)
1129             return false;
1130
1131         var attributeName = attributeNameElement.textContent;
1132
1133         function removeZeroWidthSpaceRecursive(node)
1134         {
1135             if (node.nodeType === Node.TEXT_NODE) {
1136                 node.nodeValue = node.nodeValue.replace(/\u200B/g, "");
1137                 return;
1138             }
1139
1140             if (node.nodeType !== Node.ELEMENT_NODE)
1141                 return;
1142
1143             for (var child = node.firstChild; child; child = child.nextSibling)
1144                 removeZeroWidthSpaceRecursive(child);
1145         }
1146
1147         // Remove zero-width spaces that were added by nodeTitleInfo.
1148         removeZeroWidthSpaceRecursive(attribute);
1149
1150         var config = new WebInspector.EditingConfig(this._attributeEditingCommitted.bind(this), this._editingCancelled.bind(this), attributeName);
1151         this._editing = WebInspector.startEditing(attribute, config);
1152
1153         window.getSelection().setBaseAndExtent(elementForSelection, 0, elementForSelection, 1);
1154
1155         return true;
1156     },
1157
1158     _startEditingTextNode: function(textNode)
1159     {
1160         if (WebInspector.isBeingEdited(textNode))
1161             return true;
1162
1163         var config = new WebInspector.EditingConfig(this._textNodeEditingCommitted.bind(this), this._editingCancelled.bind(this));
1164         this._editing = WebInspector.startEditing(textNode, config);
1165         window.getSelection().setBaseAndExtent(textNode, 0, textNode, 1);
1166
1167         return true;
1168     },
1169
1170     _startEditingTagName: function(tagNameElement)
1171     {
1172         if (!tagNameElement) {
1173             tagNameElement = this.listItemElement.getElementsByClassName("webkit-html-tag-name")[0];
1174             if (!tagNameElement)
1175                 return false;
1176         }
1177
1178         var tagName = tagNameElement.textContent;
1179         if (WebInspector.ElementsTreeElement.EditTagBlacklist[tagName.toLowerCase()])
1180             return false;
1181
1182         if (WebInspector.isBeingEdited(tagNameElement))
1183             return true;
1184
1185         var closingTagElement = this._distinctClosingTagElement();
1186
1187         function keyupListener(event)
1188         {
1189             if (closingTagElement)
1190                 closingTagElement.textContent = "</" + tagNameElement.textContent + ">";
1191         }
1192
1193         function editingComitted(element, newTagName)
1194         {
1195             tagNameElement.removeEventListener('keyup', keyupListener, false);
1196             this._tagNameEditingCommitted.apply(this, arguments);
1197         }
1198
1199         function editingCancelled()
1200         {
1201             tagNameElement.removeEventListener('keyup', keyupListener, false);
1202             this._editingCancelled.apply(this, arguments);
1203         }
1204
1205         tagNameElement.addEventListener('keyup', keyupListener, false);
1206
1207         var config = new WebInspector.EditingConfig(editingComitted.bind(this), editingCancelled.bind(this), tagName);
1208         this._editing = WebInspector.startEditing(tagNameElement, config);
1209         window.getSelection().setBaseAndExtent(tagNameElement, 0, tagNameElement, 1);
1210         return true;
1211     },
1212
1213     _startEditingAsHTML: function(commitCallback, error, initialValue)
1214     {
1215         if (error)
1216             return;
1217         if (this._htmlEditElement && WebInspector.isBeingEdited(this._htmlEditElement))
1218             return;
1219
1220         this._htmlEditElement = document.createElement("div");
1221         this._htmlEditElement.className = "source-code elements-tree-editor";
1222         this._htmlEditElement.textContent = initialValue;
1223
1224         // Hide header items.
1225         var child = this.listItemElement.firstChild;
1226         while (child) {
1227             child.style.display = "none";
1228             child = child.nextSibling;
1229         }
1230         // Hide children item.
1231         if (this._childrenListNode)
1232             this._childrenListNode.style.display = "none";
1233         // Append editor.
1234         this.listItemElement.appendChild(this._htmlEditElement);
1235
1236         this.updateSelection();
1237
1238         function commit()
1239         {
1240             commitCallback(this._htmlEditElement.textContent);
1241             dispose.call(this);
1242         }
1243
1244         function dispose()
1245         {
1246             this._editing = false;
1247
1248             // Remove editor.
1249             this.listItemElement.removeChild(this._htmlEditElement);
1250             delete this._htmlEditElement;
1251             // Unhide children item.
1252             if (this._childrenListNode)
1253                 this._childrenListNode.style.removeProperty("display");
1254             // Unhide header items.
1255             var child = this.listItemElement.firstChild;
1256             while (child) {
1257                 child.style.removeProperty("display");
1258                 child = child.nextSibling;
1259             }
1260
1261             this.updateSelection();
1262         }
1263
1264         var config = new WebInspector.EditingConfig(commit.bind(this), dispose.bind(this));
1265         config.setMultiline(true);
1266         this._editing = WebInspector.startEditing(this._htmlEditElement, config);
1267     },
1268
1269     _attributeEditingCommitted: function(element, newText, oldText, attributeName, moveDirection)
1270     {
1271         this._editing = false;
1272
1273         var treeOutline = this.treeOutline;
1274         function moveToNextAttributeIfNeeded(error)
1275         {
1276             if (error)
1277                 this._editingCancelled(element, attributeName);
1278
1279             if (!moveDirection)
1280                 return;
1281
1282             treeOutline._updateModifiedNodes();
1283
1284             // Search for the attribute's position, and then decide where to move to.
1285             var attributes = this.representedObject.attributes();
1286             for (var i = 0; i < attributes.length; ++i) {
1287                 if (attributes[i].name !== attributeName)
1288                     continue;
1289
1290                 if (moveDirection === "backward") {
1291                     if (i === 0)
1292                         this._startEditingTagName();
1293                     else
1294                         this._triggerEditAttribute(attributes[i - 1].name);
1295                 } else {
1296                     if (i === attributes.length - 1)
1297                         this._addNewAttribute();
1298                     else
1299                         this._triggerEditAttribute(attributes[i + 1].name);
1300                 }
1301                 return;
1302             }
1303
1304             // Moving From the "New Attribute" position.
1305             if (moveDirection === "backward") {
1306                 if (newText === " ") {
1307                     // Moving from "New Attribute" that was not edited
1308                     if (attributes.length > 0)
1309                         this._triggerEditAttribute(attributes[attributes.length - 1].name);
1310                 } else {
1311                     // Moving from "New Attribute" that holds new value
1312                     if (attributes.length > 1)
1313                         this._triggerEditAttribute(attributes[attributes.length - 2].name);
1314                 }
1315             } else if (moveDirection === "forward") {
1316                 if (!/^\s*$/.test(newText))
1317                     this._addNewAttribute();
1318                 else
1319                     this._startEditingTagName();
1320             }
1321         }
1322
1323         this.representedObject.setAttribute(attributeName, newText, moveToNextAttributeIfNeeded.bind(this));
1324     },
1325
1326     _tagNameEditingCommitted: function(element, newText, oldText, tagName, moveDirection)
1327     {
1328         this._editing = false;
1329         var self = this;
1330
1331         function cancel()
1332         {
1333             var closingTagElement = self._distinctClosingTagElement();
1334             if (closingTagElement)
1335                 closingTagElement.textContent = "</" + tagName + ">";
1336
1337             self._editingCancelled(element, tagName);
1338             moveToNextAttributeIfNeeded.call(self);
1339         }
1340
1341         function moveToNextAttributeIfNeeded()
1342         {
1343             if (moveDirection !== "forward") {
1344                 this._addNewAttribute();
1345                 return;
1346             }
1347
1348             var attributes = this.representedObject.attributes();
1349             if (attributes.length > 0)
1350                 this._triggerEditAttribute(attributes[0].name);
1351             else
1352                 this._addNewAttribute();
1353         }
1354
1355         newText = newText.trim();
1356         if (newText === oldText) {
1357             cancel();
1358             return;
1359         }
1360
1361         var treeOutline = this.treeOutline;
1362         var wasExpanded = this.expanded;
1363
1364         function changeTagNameCallback(error, nodeId)
1365         {
1366             if (error || !nodeId) {
1367                 cancel();
1368                 return;
1369             }
1370
1371             var node = WebInspector.domAgent.nodeForId(nodeId);
1372
1373             // Select it and expand if necessary. We force tree update so that it processes dom events and is up to date.
1374             treeOutline._updateModifiedNodes();
1375             treeOutline.selectDOMNode(node, true);
1376
1377             var newTreeItem = treeOutline.findTreeElement(node);
1378             if (wasExpanded)
1379                 newTreeItem.expand();
1380
1381             moveToNextAttributeIfNeeded.call(newTreeItem);
1382         }
1383
1384         this.representedObject.setNodeName(newText, changeTagNameCallback);
1385     },
1386
1387     _textNodeEditingCommitted: function(element, newText)
1388     {
1389         this._editing = false;
1390
1391         var textNode;
1392         if (this.representedObject.nodeType() === Node.ELEMENT_NODE) {
1393             // We only show text nodes inline in elements if the element only
1394             // has a single child, and that child is a text node.
1395             textNode = this.representedObject.firstChild;
1396         } else if (this.representedObject.nodeType() == Node.TEXT_NODE)
1397             textNode = this.representedObject;
1398
1399         textNode.setNodeValue(newText, this.updateTitle.bind(this));
1400     },
1401
1402     _editingCancelled: function(element, context)
1403     {
1404         this._editing = false;
1405
1406         // Need to restore attributes structure.
1407         this.updateTitle();
1408     },
1409
1410     _distinctClosingTagElement: function()
1411     {
1412         // FIXME: Improve the Tree Element / Outline Abstraction to prevent crawling the DOM
1413
1414         // For an expanded element, it will be the last element with class "close"
1415         // in the child element list.
1416         if (this.expanded) {
1417             var closers = this._childrenListNode.querySelectorAll(".close");
1418             return closers[closers.length-1];
1419         }
1420
1421         // Remaining cases are single line non-expanded elements with a closing
1422         // tag, or HTML elements without a closing tag (such as <br>). Return
1423         // null in the case where there isn't a closing tag.
1424         var tags = this.listItemElement.getElementsByClassName("webkit-html-tag");
1425         return (tags.length === 1 ? null : tags[tags.length-1]);
1426     },
1427
1428     /**
1429      * @param {boolean=} onlySearchQueryChanged
1430      */
1431     updateTitle: function(onlySearchQueryChanged)
1432     {
1433         // If we are editing, return early to prevent canceling the edit.
1434         // After editing is committed updateTitle will be called.
1435         if (this._editing)
1436             return;
1437
1438         if (onlySearchQueryChanged) {
1439             if (this._highlightResult)
1440                 this._updateSearchHighlight(false);
1441         } else {
1442             var highlightElement = document.createElement("span");
1443             highlightElement.className = "highlight";
1444             highlightElement.appendChild(this._nodeTitleInfo(WebInspector.linkifyURLAsNode).titleDOM);
1445             this.title = highlightElement;
1446             delete this._highlightResult;
1447         }
1448
1449         delete this.selectionElement;
1450         this.updateSelection();
1451         this._preventFollowingLinksOnDoubleClick();
1452         this._highlightSearchResults();
1453     },
1454
1455     /**
1456      * @param {WebInspector.DOMNode=} node
1457      * @param {function(string, string, string, boolean=, string=)=} linkify
1458      */
1459     _buildAttributeDOM: function(parentElement, name, value, node, linkify)
1460     {
1461         var hasText = (value.length > 0);
1462         var attrSpanElement = parentElement.createChild("span", "webkit-html-attribute");
1463         var attrNameElement = attrSpanElement.createChild("span", "webkit-html-attribute-name");
1464         attrNameElement.textContent = name;
1465
1466         if (hasText)
1467             attrSpanElement.appendChild(document.createTextNode("=\u200B\""));
1468
1469         if (linkify && (name === "src" || name === "href")) {
1470             var rewrittenHref = WebInspector.resourceURLForRelatedNode(node, value);
1471             value = value.replace(/([\/;:\)\]\}])/g, "$1\u200B");
1472             if (rewrittenHref === null) {
1473                 var attrValueElement = attrSpanElement.createChild("span", "webkit-html-attribute-value");
1474                 attrValueElement.textContent = value;
1475             } else {
1476                 if (value.indexOf("data:") === 0)
1477                     value = value.trimMiddle(60);
1478                 attrSpanElement.appendChild(linkify(rewrittenHref, value, "webkit-html-attribute-value", node.nodeName().toLowerCase() === "a"));
1479             }
1480         } else {
1481             value = value.replace(/([\/;:\)\]\}])/g, "$1\u200B");
1482             var attrValueElement = attrSpanElement.createChild("span", "webkit-html-attribute-value");
1483             attrValueElement.textContent = value;
1484         }
1485
1486         if (hasText)
1487             attrSpanElement.appendChild(document.createTextNode("\""));
1488     },
1489
1490     /**
1491      * @param {function(string, string, string, boolean=, string=)=} linkify
1492      */
1493     _buildTagDOM: function(parentElement, tagName, isClosingTag, isDistinctTreeElement, linkify)
1494     {
1495         var node = /** @type WebInspector.DOMNode */ this.representedObject;
1496         var classes = [ "webkit-html-tag" ];
1497         if (isClosingTag && isDistinctTreeElement)
1498             classes.push("close");
1499         var tagElement = parentElement.createChild("span", classes.join(" "));
1500         tagElement.appendChild(document.createTextNode("<"));
1501         var tagNameElement = tagElement.createChild("span", isClosingTag ? "" : "webkit-html-tag-name");
1502         tagNameElement.textContent = (isClosingTag ? "/" : "") + tagName;
1503         if (!isClosingTag && node.hasAttributes()) {
1504             var attributes = node.attributes();
1505             for (var i = 0; i < attributes.length; ++i) {
1506                 var attr = attributes[i];
1507                 tagElement.appendChild(document.createTextNode(" "));
1508                 this._buildAttributeDOM(tagElement, attr.name, attr.value, node, linkify);
1509             }
1510         }
1511         tagElement.appendChild(document.createTextNode(">"));
1512         parentElement.appendChild(document.createTextNode("\u200B"));
1513     },
1514
1515     _nodeTitleInfo: function(linkify)
1516     {
1517         var node = this.representedObject;
1518         var info = {titleDOM: document.createDocumentFragment(), hasChildren: this.hasChildren};
1519
1520         switch (node.nodeType()) {
1521             case Node.DOCUMENT_NODE:
1522                 info.titleDOM.appendChild(document.createTextNode("Document"));
1523                 break;
1524
1525             case Node.DOCUMENT_FRAGMENT_NODE:
1526                 info.titleDOM.appendChild(document.createTextNode("Document Fragment"));
1527                 break;
1528
1529             case Node.ATTRIBUTE_NODE:
1530                 var value = node.value || "\u200B"; // Zero width space to force showing an empty value.
1531                 this._buildAttributeDOM(info.titleDOM, node.name, value);
1532                 break;
1533
1534             case Node.ELEMENT_NODE:
1535                 var tagName = node.nodeNameInCorrectCase();
1536                 if (this._elementCloseTag) {
1537                     this._buildTagDOM(info.titleDOM, tagName, true, true);
1538                     info.hasChildren = false;
1539                     break;
1540                 }
1541
1542                 this._buildTagDOM(info.titleDOM, tagName, false, false, linkify);
1543
1544                 var textChild = this._singleTextChild(node);
1545                 var showInlineText = textChild && textChild.nodeValue().length < Preferences.maxInlineTextChildLength;
1546
1547                 if (!this.expanded && (!showInlineText && (this.treeOutline.isXMLMimeType || !WebInspector.ElementsTreeElement.ForbiddenClosingTagElements[tagName]))) {
1548                     if (this.hasChildren) {
1549                         var textNodeElement = info.titleDOM.createChild("span", "webkit-html-text-node");
1550                         textNodeElement.textContent = "\u2026";
1551                         info.titleDOM.appendChild(document.createTextNode("\u200B"));
1552                     }
1553                     this._buildTagDOM(info.titleDOM, tagName, true, false);
1554                 }
1555
1556                 // If this element only has a single child that is a text node,
1557                 // just show that text and the closing tag inline rather than
1558                 // create a subtree for them
1559                 if (showInlineText) {
1560                     var textNodeElement = info.titleDOM.createChild("span", "webkit-html-text-node");
1561                     textNodeElement.textContent = textChild.nodeValue();
1562                     info.titleDOM.appendChild(document.createTextNode("\u200B"));
1563                     this._buildTagDOM(info.titleDOM, tagName, true, false);
1564                     info.hasChildren = false;
1565                 }
1566                 break;
1567
1568             case Node.TEXT_NODE:
1569                 if (node.parentNode && node.parentNode.nodeName().toLowerCase() === "script") {
1570                     var newNode = info.titleDOM.createChild("span", "webkit-html-text-node webkit-html-js-node");
1571                     newNode.textContent = node.nodeValue();
1572
1573                     var javascriptSyntaxHighlighter = new WebInspector.DOMSyntaxHighlighter("text/javascript", true);
1574                     javascriptSyntaxHighlighter.syntaxHighlightNode(newNode);
1575                 } else if (node.parentNode && node.parentNode.nodeName().toLowerCase() === "style") {
1576                     var newNode = info.titleDOM.createChild("span", "webkit-html-text-node webkit-html-css-node");
1577                     newNode.textContent = node.nodeValue();
1578
1579                     var cssSyntaxHighlighter = new WebInspector.DOMSyntaxHighlighter("text/css", true);
1580                     cssSyntaxHighlighter.syntaxHighlightNode(newNode);
1581                 } else {
1582                     info.titleDOM.appendChild(document.createTextNode("\""));
1583                     var textNodeElement = info.titleDOM.createChild("span", "webkit-html-text-node");
1584                     textNodeElement.textContent = node.nodeValue();
1585                     info.titleDOM.appendChild(document.createTextNode("\""));
1586                 }
1587                 break;
1588
1589             case Node.COMMENT_NODE:
1590                 var commentElement = info.titleDOM.createChild("span", "webkit-html-comment");
1591                 commentElement.appendChild(document.createTextNode("<!--" + node.nodeValue() + "-->"));
1592                 break;
1593
1594             case Node.DOCUMENT_TYPE_NODE:
1595                 var docTypeElement = info.titleDOM.createChild("span", "webkit-html-doctype");
1596                 docTypeElement.appendChild(document.createTextNode("<!DOCTYPE " + node.nodeName()));
1597                 if (node.publicId) {
1598                     docTypeElement.appendChild(document.createTextNode(" PUBLIC \"" + node.publicId + "\""));
1599                     if (node.systemId)
1600                         docTypeElement.appendChild(document.createTextNode(" \"" + node.systemId + "\""));
1601                 } else if (node.systemId)
1602                     docTypeElement.appendChild(document.createTextNode(" SYSTEM \"" + node.systemId + "\""));
1603
1604                 if (node.internalSubset)
1605                     docTypeElement.appendChild(document.createTextNode(" [" + node.internalSubset + "]"));
1606
1607                 docTypeElement.appendChild(document.createTextNode(">"));
1608                 break;
1609
1610             case Node.CDATA_SECTION_NODE:
1611                 var cdataElement = info.titleDOM.createChild("span", "webkit-html-text-node");
1612                 cdataElement.appendChild(document.createTextNode("<![CDATA[" + node.nodeValue() + "]]>"));
1613                 break;
1614             default:
1615                 var defaultElement = info.titleDOM.appendChild(document.createTextNode(node.nodeNameInCorrectCase().collapseWhitespace()));
1616         }
1617
1618         return info;
1619     },
1620
1621     _singleTextChild: function(node)
1622     {
1623         if (!node)
1624             return null;
1625
1626         var firstChild = node.firstChild;
1627         if (!firstChild || firstChild.nodeType() !== Node.TEXT_NODE)
1628             return null;
1629
1630         var sibling = firstChild.nextSibling;
1631         return sibling ? null : firstChild;
1632     },
1633
1634     _showInlineText: function(node)
1635     {
1636         if (node.nodeType() === Node.ELEMENT_NODE) {
1637             var textChild = this._singleTextChild(node);
1638             if (textChild && textChild.nodeValue().length < Preferences.maxInlineTextChildLength)
1639                 return true;
1640         }
1641         return false;
1642     },
1643
1644     remove: function()
1645     {
1646         var parentElement = this.parent;
1647         if (!parentElement)
1648             return;
1649
1650         var self = this;
1651         function removeNodeCallback(error, removedNodeId)
1652         {
1653             if (error)
1654                 return;
1655
1656             parentElement.removeChild(self);
1657             parentElement.adjustCollapsedRange();
1658         }
1659
1660         this.representedObject.removeNode(removeNodeCallback);
1661     },
1662
1663     _editAsHTML: function()
1664     {
1665         var treeOutline = this.treeOutline;
1666         var node = this.representedObject;
1667         var wasExpanded = this.expanded;
1668
1669         function selectNode(error, nodeId)
1670         {
1671             if (error || !nodeId)
1672                 return;
1673
1674             var node = WebInspector.domAgent.nodeForId(nodeId);
1675             // Select it and expand if necessary. We force tree update so that it processes dom events and is up to date.
1676             treeOutline._updateModifiedNodes();
1677             treeOutline.selectDOMNode(node, true);
1678
1679             if (wasExpanded) {
1680                 var newTreeItem = treeOutline.findTreeElement(node);
1681                 if (newTreeItem)
1682                     newTreeItem.expand();
1683             }
1684         }
1685
1686         function commitChange(value)
1687         {
1688             node.setOuterHTML(value, selectNode);
1689         }
1690
1691         node.getOuterHTML(this._startEditingAsHTML.bind(this, commitChange));
1692     },
1693
1694     _copyHTML: function()
1695     {
1696         this.representedObject.copyNode();
1697     },
1698
1699     _highlightSearchResults: function()
1700     {
1701         if (!this._searchQuery || !this._searchHighlightsVisible)
1702             return;
1703         if (this._highlightResult) {
1704             this._updateSearchHighlight(true);
1705             return;
1706         }
1707
1708         var text = this.listItemElement.textContent;
1709         var regexObject = createPlainTextSearchRegex(this._searchQuery, "gi");
1710
1711         var offset = 0;
1712         var match = regexObject.exec(text);
1713         var matchRanges = [];
1714         while (match) {
1715             matchRanges.push({ offset: match.index, length: match[0].length });
1716             match = regexObject.exec(text);
1717         }
1718
1719         // Fall back for XPath, etc. matches.
1720         if (!matchRanges.length)
1721             matchRanges.push({ offset: 0, length: text.length });
1722
1723         this._highlightResult = [];
1724         highlightSearchResults(this.listItemElement, matchRanges, this._highlightResult);
1725     }
1726 }
1727
1728 WebInspector.ElementsTreeElement.prototype.__proto__ = TreeElement.prototype;
1729
1730 /**
1731  * @constructor
1732  */
1733 WebInspector.ElementsTreeUpdater = function(treeOutline)
1734 {
1735     WebInspector.domAgent.addEventListener(WebInspector.DOMAgent.Events.NodeInserted, this._nodeInserted, this);
1736     WebInspector.domAgent.addEventListener(WebInspector.DOMAgent.Events.NodeRemoved, this._nodeRemoved, this);
1737     WebInspector.domAgent.addEventListener(WebInspector.DOMAgent.Events.AttrModified, this._attributesUpdated, this);
1738     WebInspector.domAgent.addEventListener(WebInspector.DOMAgent.Events.AttrRemoved, this._attributesUpdated, this);
1739     WebInspector.domAgent.addEventListener(WebInspector.DOMAgent.Events.CharacterDataModified, this._characterDataModified, this);
1740     WebInspector.domAgent.addEventListener(WebInspector.DOMAgent.Events.DocumentUpdated, this._documentUpdated, this);
1741     WebInspector.domAgent.addEventListener(WebInspector.DOMAgent.Events.ChildNodeCountUpdated, this._childNodeCountUpdated, this);
1742
1743     this._treeOutline = treeOutline;
1744     this._recentlyModifiedNodes = [];
1745 }
1746
1747 WebInspector.ElementsTreeUpdater.prototype = {
1748     _documentUpdated: function(event)
1749     {
1750         var inspectedRootDocument = event.data;
1751
1752         this._reset();
1753
1754         if (!inspectedRootDocument)
1755             return;
1756
1757         this._treeOutline.rootDOMNode = inspectedRootDocument;
1758     },
1759
1760     _attributesUpdated: function(event)
1761     {
1762         this._recentlyModifiedNodes.push({node: event.data.node, updated: true});
1763         if (this._treeOutline._visible)
1764             this._updateModifiedNodesSoon();
1765     },
1766
1767     _characterDataModified: function(event)
1768     {
1769         this._recentlyModifiedNodes.push({node: event.data, updated: true});
1770         if (this._treeOutline._visible)
1771             this._updateModifiedNodesSoon();
1772     },
1773
1774     _nodeInserted: function(event)
1775     {
1776         this._recentlyModifiedNodes.push({node: event.data, parent: event.data.parentNode, inserted: true});
1777         if (this._treeOutline._visible)
1778             this._updateModifiedNodesSoon();
1779     },
1780
1781     _nodeRemoved: function(event)
1782     {
1783         this._recentlyModifiedNodes.push({node: event.data.node, parent: event.data.parent, removed: true});
1784         if (this._treeOutline._visible)
1785             this._updateModifiedNodesSoon();
1786     },
1787
1788     _childNodeCountUpdated: function(event)
1789     {
1790         var treeElement = this._treeOutline.findTreeElement(event.data);
1791         if (treeElement)
1792             treeElement.hasChildren = event.data.hasChildNodes();
1793     },
1794
1795     _updateModifiedNodesSoon: function()
1796     {
1797         if (this._updateModifiedNodesTimeout)
1798             return;
1799         this._updateModifiedNodesTimeout = setTimeout(this._updateModifiedNodes.bind(this), 0);
1800     },
1801
1802     _updateModifiedNodes: function()
1803     {
1804         if (this._updateModifiedNodesTimeout) {
1805             clearTimeout(this._updateModifiedNodesTimeout);
1806             delete this._updateModifiedNodesTimeout;
1807         }
1808
1809         var updatedParentTreeElements = [];
1810
1811         for (var i = 0; i < this._recentlyModifiedNodes.length; ++i) {
1812             var parent = this._recentlyModifiedNodes[i].parent;
1813             var node = this._recentlyModifiedNodes[i].node;
1814
1815             if (this._recentlyModifiedNodes[i].updated) {
1816                 var nodeItem = this._treeOutline.findTreeElement(node);
1817                 if (nodeItem)
1818                     nodeItem.updateTitle();
1819                 continue;
1820             }
1821
1822             if (!parent)
1823                 continue;
1824
1825             var parentNodeItem = this._treeOutline.findTreeElement(parent);
1826             if (parentNodeItem && !parentNodeItem.alreadyUpdatedChildren) {
1827                 parentNodeItem.updateChildren();
1828                 parentNodeItem.alreadyUpdatedChildren = true;
1829                 updatedParentTreeElements.push(parentNodeItem);
1830             }
1831         }
1832
1833         for (var i = 0; i < updatedParentTreeElements.length; ++i)
1834             delete updatedParentTreeElements[i].alreadyUpdatedChildren;
1835
1836         this._recentlyModifiedNodes = [];
1837     },
1838
1839     _reset: function()
1840     {
1841         this._treeOutline.rootDOMNode = null;
1842         this._treeOutline.selectDOMNode(null, false);
1843         WebInspector.domAgent.hideDOMNodeHighlight();
1844         this._recentlyModifiedNodes = [];
1845     }
1846 }