WebInspector: Switch hide element shortcut in ElementsPanel to use a selector
[WebKit-https.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  * @param {function(DOMAgent.NodeId, string, boolean)=} setPseudoClassCallback
39  */
40 WebInspector.ElementsTreeOutline = function(omitRootDOMNode, selectEnabled, showInElementsPanelEnabled, contextMenuCallback, setPseudoClassCallback)
41 {
42     this.element = document.createElement("ol");
43     this.element.addEventListener("mousedown", this._onmousedown.bind(this), false);
44     this.element.addEventListener("mousemove", this._onmousemove.bind(this), false);
45     this.element.addEventListener("mouseout", this._onmouseout.bind(this), false);
46     this.element.addEventListener("dragstart", this._ondragstart.bind(this), false);
47     this.element.addEventListener("dragover", this._ondragover.bind(this), false);
48     this.element.addEventListener("dragleave", this._ondragleave.bind(this), false);
49     this.element.addEventListener("drop", this._ondrop.bind(this), false);
50     this.element.addEventListener("dragend", this._ondragend.bind(this), false);
51     this.element.addEventListener("keydown", this._onkeydown.bind(this), false);
52
53     TreeOutline.call(this, this.element);
54
55     this._includeRootDOMNode = !omitRootDOMNode;
56     this._selectEnabled = selectEnabled;
57     this._showInElementsPanelEnabled = showInElementsPanelEnabled;
58     this._rootDOMNode = null;
59     this._selectDOMNode = null;
60     this._eventSupport = new WebInspector.Object();
61
62     this._visible = false;
63
64     this.element.addEventListener("contextmenu", this._contextMenuEventFired.bind(this), true);
65     this._contextMenuCallback = contextMenuCallback;
66     this._setPseudoClassCallback = setPseudoClassCallback;
67     this._createNodeDecorators();
68 }
69
70 WebInspector.ElementsTreeOutline.Events = {
71     SelectedNodeChanged: "SelectedNodeChanged"
72 }
73
74 WebInspector.ElementsTreeOutline.MappedCharToEntity = {
75     "\u00a0": "nbsp",
76     "\u2002": "ensp",
77     "\u2003": "emsp",
78     "\u2009": "thinsp",
79     "\u200b": "#8203", // ZWSP
80     "\u200c": "zwnj",
81     "\u200d": "zwj",
82     "\u200e": "lrm",
83     "\u200f": "rlm",
84     "\u202a": "#8234", // LRE
85     "\u202b": "#8235", // RLE
86     "\u202c": "#8236", // PDF
87     "\u202d": "#8237", // LRO
88     "\u202e": "#8238" // RLO
89 }
90
91 WebInspector.ElementsTreeOutline.prototype = {
92     _createNodeDecorators: function()
93     {
94         this._nodeDecorators = [];
95         this._nodeDecorators.push(new WebInspector.ElementsTreeOutline.PseudoStateDecorator());
96     },
97
98     wireToDomAgent: function()
99     {
100         this._elementsTreeUpdater = new WebInspector.ElementsTreeUpdater(this);
101     },
102
103     setVisible: function(visible)
104     {
105         this._visible = visible;
106         if (!this._visible)
107             return;
108
109         this._updateModifiedNodes();
110         if (this._selectedDOMNode)
111             this._revealAndSelectNode(this._selectedDOMNode, false);
112     },
113
114     addEventListener: function(eventType, listener, thisObject)
115     {
116         this._eventSupport.addEventListener(eventType, listener, thisObject);
117     },
118
119     removeEventListener: function(eventType, listener, thisObject)
120     {
121         this._eventSupport.removeEventListener(eventType, listener, thisObject);
122     },
123
124     get rootDOMNode()
125     {
126         return this._rootDOMNode;
127     },
128
129     set rootDOMNode(x)
130     {
131         if (this._rootDOMNode === x)
132             return;
133
134         this._rootDOMNode = x;
135
136         this._isXMLMimeType = x && x.isXMLNode();
137
138         this.update();
139     },
140
141     get isXMLMimeType()
142     {
143         return this._isXMLMimeType;
144     },
145
146     selectedDOMNode: function()
147     {
148         return this._selectedDOMNode;
149     },
150
151     selectDOMNode: function(node, focus)
152     {
153         if (this._selectedDOMNode === node) {
154             this._revealAndSelectNode(node, !focus);
155             return;
156         }
157
158         this._selectedDOMNode = node;
159         this._revealAndSelectNode(node, !focus);
160
161         // The _revealAndSelectNode() method might find a different element if there is inlined text,
162         // and the select() call would change the selectedDOMNode and reenter this setter. So to
163         // avoid calling _selectedNodeChanged() twice, first check if _selectedDOMNode is the same
164         // node as the one passed in.
165         if (this._selectedDOMNode === node)
166             this._selectedNodeChanged();
167     },
168
169     update: function()
170     {
171         var selectedNode = this.selectedTreeElement ? this.selectedTreeElement.representedObject : null;
172
173         this.removeChildren();
174
175         if (!this.rootDOMNode)
176             return;
177
178         var treeElement;
179         if (this._includeRootDOMNode) {
180             treeElement = new WebInspector.ElementsTreeElement(this.rootDOMNode);
181             treeElement.selectable = this._selectEnabled;
182             this.appendChild(treeElement);
183         } else {
184             // FIXME: this could use findTreeElement to reuse a tree element if it already exists
185             var node = this.rootDOMNode.firstChild;
186             while (node) {
187                 treeElement = new WebInspector.ElementsTreeElement(node);
188                 treeElement.selectable = this._selectEnabled;
189                 this.appendChild(treeElement);
190                 node = node.nextSibling;
191             }
192         }
193
194         if (selectedNode)
195             this._revealAndSelectNode(selectedNode, true);
196     },
197
198     updateSelection: function()
199     {
200         if (!this.selectedTreeElement)
201             return;
202         var element = this.treeOutline.selectedTreeElement;
203         element.updateSelection();
204     },
205
206     /**
207      * @param {WebInspector.DOMNode} node
208      */
209     updateOpenCloseTags: function(node)
210     {
211         var treeElement = this.findTreeElement(node);
212         if (treeElement)
213             treeElement.updateTitle();
214         var children = treeElement.children;
215         var closingTagElement = children[children.length - 1];
216         if (closingTagElement && closingTagElement._elementCloseTag)
217             closingTagElement.updateTitle();
218     },
219
220     _selectedNodeChanged: function()
221     {
222         this._eventSupport.dispatchEventToListeners(WebInspector.ElementsTreeOutline.Events.SelectedNodeChanged, this._selectedDOMNode);
223     },
224
225     /**
226      * @param {WebInspector.DOMNode} node
227      */
228     findTreeElement: function(node)
229     {
230         function isAncestorNode(ancestor, node)
231         {
232             return ancestor.isAncestor(node);
233         }
234
235         function parentNode(node)
236         {
237             return node.parentNode;
238         }
239
240         var treeElement = TreeOutline.prototype.findTreeElement.call(this, node, isAncestorNode, parentNode);
241         if (!treeElement && node.nodeType() === Node.TEXT_NODE) {
242             // The text node might have been inlined if it was short, so try to find the parent element.
243             treeElement = TreeOutline.prototype.findTreeElement.call(this, node.parentNode, isAncestorNode, parentNode);
244         }
245
246         return treeElement;
247     },
248
249     /**
250      * @param {WebInspector.DOMNode} node
251      */
252     createTreeElementFor: function(node)
253     {
254         var treeElement = this.findTreeElement(node);
255         if (treeElement)
256             return treeElement;
257         if (!node.parentNode)
258             return null;
259
260         treeElement = this.createTreeElementFor(node.parentNode);
261         if (treeElement && treeElement.showChild(node.index))
262             return treeElement.children[node.index];
263
264         return null;
265     },
266
267     set suppressRevealAndSelect(x)
268     {
269         if (this._suppressRevealAndSelect === x)
270             return;
271         this._suppressRevealAndSelect = x;
272     },
273
274     _revealAndSelectNode: function(node, omitFocus)
275     {
276         if (!node || this._suppressRevealAndSelect)
277             return;
278
279         var treeElement = this.createTreeElementFor(node);
280         if (!treeElement)
281             return;
282
283         treeElement.revealAndSelect(omitFocus);
284     },
285
286     _treeElementFromEvent: function(event)
287     {
288         var scrollContainer = this.element.parentElement;
289
290         // We choose this X coordinate based on the knowledge that our list
291         // items extend at least to the right edge of the outer <ol> container.
292         // In the no-word-wrap mode the outer <ol> may be wider than the tree container
293         // (and partially hidden), in which case we are left to use only its right boundary.
294         var x = scrollContainer.totalOffsetLeft() + scrollContainer.offsetWidth - 36;
295
296         var y = event.pageY;
297
298         // Our list items have 1-pixel cracks between them vertically. We avoid
299         // the cracks by checking slightly above and slightly below the mouse
300         // and seeing if we hit the same element each time.
301         var elementUnderMouse = this.treeElementFromPoint(x, y);
302         var elementAboveMouse = this.treeElementFromPoint(x, y - 2);
303         var element;
304         if (elementUnderMouse === elementAboveMouse)
305             element = elementUnderMouse;
306         else
307             element = this.treeElementFromPoint(x, y + 2);
308
309         return element;
310     },
311
312     _onmousedown: function(event)
313     {
314         var element = this._treeElementFromEvent(event);
315
316         if (!element || element.isEventWithinDisclosureTriangle(event))
317             return;
318
319         element.select();
320     },
321
322     _onmousemove: function(event)
323     {
324         var element = this._treeElementFromEvent(event);
325         if (element && this._previousHoveredElement === element)
326             return;
327
328         if (this._previousHoveredElement) {
329             this._previousHoveredElement.hovered = false;
330             delete this._previousHoveredElement;
331         }
332
333         if (element) {
334             element.hovered = true;
335             this._previousHoveredElement = element;
336         }
337
338         WebInspector.domAgent.highlightDOMNode(element ? element.representedObject.id : 0);
339     },
340
341     _onmouseout: function(event)
342     {
343         var nodeUnderMouse = document.elementFromPoint(event.pageX, event.pageY);
344         if (nodeUnderMouse && nodeUnderMouse.isDescendant(this.element))
345             return;
346
347         if (this._previousHoveredElement) {
348             this._previousHoveredElement.hovered = false;
349             delete this._previousHoveredElement;
350         }
351
352         WebInspector.domAgent.hideDOMNodeHighlight();
353     },
354
355     _ondragstart: function(event)
356     {
357         if (!window.getSelection().isCollapsed)
358             return false;
359         if (event.target.nodeName === "A")
360             return false;
361
362         var treeElement = this._treeElementFromEvent(event);
363         if (!treeElement)
364             return false;
365
366         if (!this._isValidDragSourceOrTarget(treeElement))
367             return false;
368
369         if (treeElement.representedObject.nodeName() === "BODY" || treeElement.representedObject.nodeName() === "HEAD")
370             return false;
371
372         event.dataTransfer.setData("text/plain", treeElement.listItemElement.textContent);
373         event.dataTransfer.effectAllowed = "copyMove";
374         this._treeElementBeingDragged = treeElement;
375
376         WebInspector.domAgent.hideDOMNodeHighlight();
377
378         return true;
379     },
380
381     _ondragover: function(event)
382     {
383         if (!this._treeElementBeingDragged)
384             return false;
385
386         var treeElement = this._treeElementFromEvent(event);
387         if (!this._isValidDragSourceOrTarget(treeElement))
388             return false;
389
390         var node = treeElement.representedObject;
391         while (node) {
392             if (node === this._treeElementBeingDragged.representedObject)
393                 return false;
394             node = node.parentNode;
395         }
396
397         treeElement.updateSelection();
398         treeElement.listItemElement.addStyleClass("elements-drag-over");
399         this._dragOverTreeElement = treeElement;
400         event.preventDefault();
401         event.dataTransfer.dropEffect = 'move';
402         return false;
403     },
404
405     _ondragleave: function(event)
406     {
407         this._clearDragOverTreeElementMarker();
408         event.preventDefault();
409         return false;
410     },
411
412     _isValidDragSourceOrTarget: function(treeElement)
413     {
414         if (!treeElement)
415             return false;
416
417         var node = treeElement.representedObject;
418         if (!(node instanceof WebInspector.DOMNode))
419             return false;
420
421         if (!node.parentNode || node.parentNode.nodeType() !== Node.ELEMENT_NODE)
422             return false;
423
424         return true;
425     },
426
427     _ondrop: function(event)
428     {
429         event.preventDefault();
430         var treeElement = this._treeElementFromEvent(event);
431         if (treeElement)
432             this._doMove(treeElement);
433     },
434
435     _doMove: function(treeElement)
436     {
437         if (!this._treeElementBeingDragged)
438             return;
439
440         var parentNode;
441         var anchorNode;
442
443         if (treeElement._elementCloseTag) {
444             // Drop onto closing tag -> insert as last child.
445             parentNode = treeElement.representedObject;
446         } else {
447             var dragTargetNode = treeElement.representedObject;
448             parentNode = dragTargetNode.parentNode;
449             anchorNode = dragTargetNode;
450         }
451
452         var wasExpanded = this._treeElementBeingDragged.expanded;
453         this._treeElementBeingDragged.representedObject.moveTo(parentNode, anchorNode, this._selectNodeAfterEdit.bind(this, null, wasExpanded));
454
455         delete this._treeElementBeingDragged;
456     },
457
458     _ondragend: function(event)
459     {
460         event.preventDefault();
461         this._clearDragOverTreeElementMarker();
462         delete this._treeElementBeingDragged;
463     },
464
465     _clearDragOverTreeElementMarker: function()
466     {
467         if (this._dragOverTreeElement) {
468             this._dragOverTreeElement.updateSelection();
469             this._dragOverTreeElement.listItemElement.removeStyleClass("elements-drag-over");
470             delete this._dragOverTreeElement;
471         }
472     },
473
474     /**
475      * @param {Event} event
476      */
477     _onkeydown: function(event)
478     {
479         var keyboardEvent = /** @type {KeyboardEvent} */ (event);
480         var node = this.selectedDOMNode();
481         var treeElement = this.getCachedTreeElement(node);
482         if (!treeElement)
483             return;
484
485         if (!treeElement._editing && WebInspector.KeyboardShortcut.hasNoModifiers(keyboardEvent) && keyboardEvent.keyCode === WebInspector.KeyboardShortcut.Keys.H.code) {
486             this._toggleHideShortcut(node);
487             event.consume(true);
488             return;
489         }
490     },
491
492     _contextMenuEventFired: function(event)
493     {
494         if (!this._showInElementsPanelEnabled)
495             return;
496
497         var treeElement = this._treeElementFromEvent(event);
498         if (!treeElement)
499             return;
500
501         function focusElement()
502         {
503             // Force elements module load.
504             WebInspector.showPanel("elements");
505             WebInspector.domAgent.inspectElement(treeElement.representedObject.id);
506         }
507         var contextMenu = new WebInspector.ContextMenu(event);
508         contextMenu.appendItem(WebInspector.UIString("Reveal in Elements Panel"), focusElement.bind(this));
509         contextMenu.show();
510     },
511
512     populateContextMenu: function(contextMenu, event)
513     {
514         var treeElement = this._treeElementFromEvent(event);
515         if (!treeElement)
516             return false;
517
518         var isTag = treeElement.representedObject.nodeType() === Node.ELEMENT_NODE;
519         var textNode = event.target.enclosingNodeOrSelfWithClass("webkit-html-text-node");
520         if (textNode && textNode.hasStyleClass("bogus"))
521             textNode = null;
522         var commentNode = event.target.enclosingNodeOrSelfWithClass("webkit-html-comment");
523         contextMenu.appendApplicableItems(event.target);
524         if (textNode) {
525             contextMenu.appendSeparator();
526             treeElement._populateTextContextMenu(contextMenu, textNode);
527         } else if (isTag) {
528             contextMenu.appendSeparator();
529             treeElement._populateTagContextMenu(contextMenu, event);
530         } else if (commentNode) {
531             contextMenu.appendSeparator();
532             treeElement._populateNodeContextMenu(contextMenu, textNode);
533         }
534     },
535
536     adjustCollapsedRange: function()
537     {
538     },
539
540     _updateModifiedNodes: function()
541     {
542         if (this._elementsTreeUpdater)
543             this._elementsTreeUpdater._updateModifiedNodes();
544     },
545
546     _populateContextMenu: function(contextMenu, node)
547     {
548         if (this._contextMenuCallback)
549             this._contextMenuCallback(contextMenu, node);
550     },
551
552     handleShortcut: function(event)
553     {
554         var node = this.selectedDOMNode();
555         var treeElement = this.getCachedTreeElement(node);
556         if (!node || !treeElement)
557             return;
558
559         if (event.keyIdentifier === "F2") {
560             this._toggleEditAsHTML(node);
561             event.handled = true;
562             return;
563         }
564
565         if (WebInspector.KeyboardShortcut.eventHasCtrlOrMeta(event) && node.parentNode) {
566             if (event.keyIdentifier === "Up" && node.previousSibling) {
567                 node.moveTo(node.parentNode, node.previousSibling, this._selectNodeAfterEdit.bind(this, null, treeElement.expanded));
568                 event.handled = true;
569                 return;
570             }
571             if (event.keyIdentifier === "Down" && node.nextSibling) {
572                 node.moveTo(node.parentNode, node.nextSibling.nextSibling, this._selectNodeAfterEdit.bind(this, null, treeElement.expanded));
573                 event.handled = true;
574                 return;
575             }
576         }
577     },
578
579     _toggleEditAsHTML: function(node)
580     {
581         var treeElement = this.getCachedTreeElement(node);
582         if (!treeElement)
583             return;
584
585         if (treeElement._editing && treeElement._htmlEditElement && WebInspector.isBeingEdited(treeElement._htmlEditElement))
586             treeElement._editing.commit();
587         else
588             treeElement._editAsHTML();
589     },
590
591     _selectNodeAfterEdit: function(fallbackNode, wasExpanded, error, nodeId)
592     {
593         if (error)
594             return;
595
596         // Select it and expand if necessary. We force tree update so that it processes dom events and is up to date.
597         this._updateModifiedNodes();
598
599         var newNode = WebInspector.domAgent.nodeForId(nodeId) || fallbackNode;
600         if (!newNode)
601             return;
602
603         this.selectDOMNode(newNode, true);
604
605         var newTreeItem = this.findTreeElement(newNode);
606         if (wasExpanded) {
607             if (newTreeItem)
608                 newTreeItem.expand();
609         }
610         return newTreeItem;
611     },
612
613     /**
614      * Runs a script on the node's remote object that toggles a class name on
615      * the node and injects a stylesheet into the head of the node's document
616      * containing a rule to set "visibility: hidden" on the class and all it's
617      * ancestors.
618      *
619      * @param {WebInspector.DOMNode} node
620      * @param {function(?WebInspector.RemoteObject)=} userCallback
621      */
622     _toggleHideShortcut: function(node, userCallback)
623     {
624         function resolvedNode(object)
625         {
626             if (!object)
627                 return;
628
629             function toggleClassAndInjectStyleRule()
630             {
631                 const className = "__web-inspector-hide-shortcut__";
632                 const styleTagId = "__web-inspector-hide-shortcut-style__";
633                 const styleRule = ".__web-inspector-hide-shortcut__, .__web-inspector-hide-shortcut__ * { visibility: hidden !important; }";
634
635                 this.classList.toggle(className);
636
637                 var style = document.head.querySelector("style#" + styleTagId);
638                 if (style)
639                     return;
640
641                 style = document.createElement("style");
642                 style.id = styleTagId;
643                 style.type = "text/css";
644                 style.innerHTML = styleRule;
645                 document.head.appendChild(style);
646             }
647
648             object.callFunction(toggleClassAndInjectStyleRule, undefined, userCallback);
649             object.release();
650         }
651
652         WebInspector.RemoteObject.resolveNode(node, "", resolvedNode);
653     },
654
655     __proto__: TreeOutline.prototype
656 }
657
658 /**
659  * @interface
660  */
661 WebInspector.ElementsTreeOutline.ElementDecorator = function()
662 {
663 }
664
665 WebInspector.ElementsTreeOutline.ElementDecorator.prototype = {
666     /**
667      * @param {WebInspector.DOMNode} node
668      */
669     decorate: function(node)
670     {
671     },
672
673     /**
674      * @param {WebInspector.DOMNode} node
675      */
676     decorateAncestor: function(node)
677     {
678     }
679 }
680
681 /**
682  * @constructor
683  * @implements {WebInspector.ElementsTreeOutline.ElementDecorator}
684  */
685 WebInspector.ElementsTreeOutline.PseudoStateDecorator = function()
686 {
687     WebInspector.ElementsTreeOutline.ElementDecorator.call(this);
688 }
689
690 WebInspector.ElementsTreeOutline.PseudoStateDecorator.PropertyName = "pseudoState";
691
692 WebInspector.ElementsTreeOutline.PseudoStateDecorator.prototype = {
693     decorate: function(node)
694     {
695         if (node.nodeType() !== Node.ELEMENT_NODE)
696             return null;
697         var propertyValue = node.getUserProperty(WebInspector.ElementsTreeOutline.PseudoStateDecorator.PropertyName);
698         if (!propertyValue)
699             return null;
700         return WebInspector.UIString("Element state: %s", ":" + propertyValue.join(", :"));
701     },
702
703     decorateAncestor: function(node)
704     {
705         if (node.nodeType() !== Node.ELEMENT_NODE)
706             return null;
707
708         var descendantCount = node.descendantUserPropertyCount(WebInspector.ElementsTreeOutline.PseudoStateDecorator.PropertyName);
709         if (!descendantCount)
710             return null;
711         if (descendantCount === 1)
712             return WebInspector.UIString("%d descendant with forced state", descendantCount);
713         return WebInspector.UIString("%d descendants with forced state", descendantCount);
714     },
715
716     __proto__: WebInspector.ElementsTreeOutline.ElementDecorator.prototype
717 }
718
719 /**
720  * @constructor
721  * @extends {TreeElement}
722  * @param {boolean=} elementCloseTag
723  */
724 WebInspector.ElementsTreeElement = function(node, elementCloseTag)
725 {
726     this._elementCloseTag = elementCloseTag;
727     var hasChildrenOverride = !elementCloseTag && node.hasChildNodes() && !this._showInlineText(node);
728
729     // The title will be updated in onattach.
730     TreeElement.call(this, "", node, hasChildrenOverride);
731
732     if (this.representedObject.nodeType() == Node.ELEMENT_NODE && !elementCloseTag)
733         this._canAddAttributes = true;
734     this._searchQuery = null;
735     this._expandedChildrenLimit = WebInspector.ElementsTreeElement.InitialChildrenLimit;
736 }
737
738 WebInspector.ElementsTreeElement.InitialChildrenLimit = 500;
739
740 // A union of HTML4 and HTML5-Draft elements that explicitly
741 // or implicitly (for HTML5) forbid the closing tag.
742 // FIXME: Revise once HTML5 Final is published.
743 WebInspector.ElementsTreeElement.ForbiddenClosingTagElements = [
744     "area", "base", "basefont", "br", "canvas", "col", "command", "embed", "frame",
745     "hr", "img", "input", "isindex", "keygen", "link", "meta", "param", "source"
746 ].keySet();
747
748 // These tags we do not allow editing their tag name.
749 WebInspector.ElementsTreeElement.EditTagBlacklist = [
750     "html", "head", "body"
751 ].keySet();
752
753 WebInspector.ElementsTreeElement.prototype = {
754     highlightSearchResults: function(searchQuery)
755     {
756         if (this._searchQuery !== searchQuery) {
757             this._updateSearchHighlight(false);
758             delete this._highlightResult; // A new search query.
759         }
760
761         this._searchQuery = searchQuery;
762         this._searchHighlightsVisible = true;
763         this.updateTitle(true);
764     },
765
766     hideSearchHighlights: function()
767     {
768         delete this._searchHighlightsVisible;
769         this._updateSearchHighlight(false);
770     },
771
772     _updateSearchHighlight: function(show)
773     {
774         if (!this._highlightResult)
775             return;
776
777         function updateEntryShow(entry)
778         {
779             switch (entry.type) {
780                 case "added":
781                     entry.parent.insertBefore(entry.node, entry.nextSibling);
782                     break;
783                 case "changed":
784                     entry.node.textContent = entry.newText;
785                     break;
786             }
787         }
788
789         function updateEntryHide(entry)
790         {
791             switch (entry.type) {
792                 case "added":
793                     if (entry.node.parentElement)
794                         entry.node.parentElement.removeChild(entry.node);
795                     break;
796                 case "changed":
797                     entry.node.textContent = entry.oldText;
798                     break;
799             }
800         }
801
802         // Preserve the semantic of node by following the order of updates for hide and show.
803         if (show) {
804             for (var i = 0, size = this._highlightResult.length; i < size; ++i)
805                 updateEntryShow(this._highlightResult[i]);
806         } else {
807             for (var i = (this._highlightResult.length - 1); i >= 0; --i)
808                 updateEntryHide(this._highlightResult[i]);
809         }
810     },
811
812     get hovered()
813     {
814         return this._hovered;
815     },
816
817     set hovered(x)
818     {
819         if (this._hovered === x)
820             return;
821
822         this._hovered = x;
823
824         if (this.listItemElement) {
825             if (x) {
826                 this.updateSelection();
827                 this.listItemElement.addStyleClass("hovered");
828             } else {
829                 this.listItemElement.removeStyleClass("hovered");
830             }
831         }
832     },
833
834     get expandedChildrenLimit()
835     {
836         return this._expandedChildrenLimit;
837     },
838
839     set expandedChildrenLimit(x)
840     {
841         if (this._expandedChildrenLimit === x)
842             return;
843
844         this._expandedChildrenLimit = x;
845         if (this.treeOutline && !this._updateChildrenInProgress)
846             this._updateChildren(true);
847     },
848
849     get expandedChildCount()
850     {
851         var count = this.children.length;
852         if (count && this.children[count - 1]._elementCloseTag)
853             count--;
854         if (count && this.children[count - 1].expandAllButton)
855             count--;
856         return count;
857     },
858
859     showChild: function(index)
860     {
861         if (this._elementCloseTag)
862             return;
863
864         if (index >= this.expandedChildrenLimit) {
865             this._expandedChildrenLimit = index + 1;
866             this._updateChildren(true);
867         }
868
869         // Whether index-th child is visible in the children tree
870         return this.expandedChildCount > index;
871     },
872
873     updateSelection: function()
874     {
875         var listItemElement = this.listItemElement;
876         if (!listItemElement)
877             return;
878
879         if (!this._readyToUpdateSelection) {
880             if (document.body.offsetWidth > 0)
881                 this._readyToUpdateSelection = true;
882             else {
883                 // The stylesheet hasn't loaded yet or the window is closed,
884                 // so we can't calculate what we need. Return early.
885                 return;
886             }
887         }
888
889         if (!this.selectionElement) {
890             this.selectionElement = document.createElement("div");
891             this.selectionElement.className = "selection selected";
892             listItemElement.insertBefore(this.selectionElement, listItemElement.firstChild);
893         }
894
895         this.selectionElement.style.height = listItemElement.offsetHeight + "px";
896     },
897
898     onattach: function()
899     {
900         if (this._hovered) {
901             this.updateSelection();
902             this.listItemElement.addStyleClass("hovered");
903         }
904
905         this.updateTitle();
906         this._preventFollowingLinksOnDoubleClick();
907         this.listItemElement.draggable = true;
908     },
909
910     _preventFollowingLinksOnDoubleClick: function()
911     {
912         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");
913         if (!links)
914             return;
915
916         for (var i = 0; i < links.length; ++i)
917             links[i].preventFollowOnDoubleClick = true;
918     },
919
920     onpopulate: function()
921     {
922         if (this.children.length || this._showInlineText(this.representedObject) || this._elementCloseTag)
923             return;
924
925         this.updateChildren();
926     },
927
928     /**
929      * @param {boolean=} fullRefresh
930      */
931     updateChildren: function(fullRefresh)
932     {
933         if (this._elementCloseTag)
934             return;
935         this.representedObject.getChildNodes(this._updateChildren.bind(this, fullRefresh));
936     },
937
938     /**
939      * @param {boolean=} closingTag
940      */
941     insertChildElement: function(child, index, closingTag)
942     {
943         var newElement = new WebInspector.ElementsTreeElement(child, closingTag);
944         newElement.selectable = this.treeOutline._selectEnabled;
945         this.insertChild(newElement, index);
946         return newElement;
947     },
948
949     moveChild: function(child, targetIndex)
950     {
951         var wasSelected = child.selected;
952         this.removeChild(child);
953         this.insertChild(child, targetIndex);
954         if (wasSelected)
955             child.select();
956     },
957
958     /**
959      * @param {boolean=} fullRefresh
960      */
961     _updateChildren: function(fullRefresh)
962     {
963         if (this._updateChildrenInProgress || !this.treeOutline._visible)
964             return;
965
966         this._updateChildrenInProgress = true;
967         var selectedNode = this.treeOutline.selectedDOMNode();
968         var originalScrollTop = 0;
969         if (fullRefresh) {
970             var treeOutlineContainerElement = this.treeOutline.element.parentNode;
971             originalScrollTop = treeOutlineContainerElement.scrollTop;
972             var selectedTreeElement = this.treeOutline.selectedTreeElement;
973             if (selectedTreeElement && selectedTreeElement.hasAncestor(this))
974                 this.select();
975             this.removeChildren();
976         }
977
978         var treeElement = this;
979         var treeChildIndex = 0;
980         var elementToSelect;
981
982         function updateChildrenOfNode(node)
983         {
984             var treeOutline = treeElement.treeOutline;
985             var child = node.firstChild;
986             while (child) {
987                 var currentTreeElement = treeElement.children[treeChildIndex];
988                 if (!currentTreeElement || currentTreeElement.representedObject !== child) {
989                     // Find any existing element that is later in the children list.
990                     var existingTreeElement = null;
991                     for (var i = (treeChildIndex + 1), size = treeElement.expandedChildCount; i < size; ++i) {
992                         if (treeElement.children[i].representedObject === child) {
993                             existingTreeElement = treeElement.children[i];
994                             break;
995                         }
996                     }
997
998                     if (existingTreeElement && existingTreeElement.parent === treeElement) {
999                         // If an existing element was found and it has the same parent, just move it.
1000                         treeElement.moveChild(existingTreeElement, treeChildIndex);
1001                     } else {
1002                         // No existing element found, insert a new element.
1003                         if (treeChildIndex < treeElement.expandedChildrenLimit) {
1004                             var newElement = treeElement.insertChildElement(child, treeChildIndex);
1005                             if (child === selectedNode)
1006                                 elementToSelect = newElement;
1007                             if (treeElement.expandedChildCount > treeElement.expandedChildrenLimit)
1008                                 treeElement.expandedChildrenLimit++;
1009                         }
1010                     }
1011                 }
1012
1013                 child = child.nextSibling;
1014                 ++treeChildIndex;
1015             }
1016         }
1017
1018         // Remove any tree elements that no longer have this node (or this node's contentDocument) as their parent.
1019         for (var i = (this.children.length - 1); i >= 0; --i) {
1020             var currentChild = this.children[i];
1021             var currentNode = currentChild.representedObject;
1022             var currentParentNode = currentNode.parentNode;
1023
1024             if (currentParentNode === this.representedObject)
1025                 continue;
1026
1027             var selectedTreeElement = this.treeOutline.selectedTreeElement;
1028             if (selectedTreeElement && (selectedTreeElement === currentChild || selectedTreeElement.hasAncestor(currentChild)))
1029                 this.select();
1030
1031             this.removeChildAtIndex(i);
1032         }
1033
1034         updateChildrenOfNode(this.representedObject);
1035         this.adjustCollapsedRange();
1036
1037         var lastChild = this.children[this.children.length - 1];
1038         if (this.representedObject.nodeType() == Node.ELEMENT_NODE && (!lastChild || !lastChild._elementCloseTag))
1039             this.insertChildElement(this.representedObject, this.children.length, true);
1040
1041         // We want to restore the original selection and tree scroll position after a full refresh, if possible.
1042         if (fullRefresh && elementToSelect) {
1043             elementToSelect.select();
1044             if (treeOutlineContainerElement && originalScrollTop <= treeOutlineContainerElement.scrollHeight)
1045                 treeOutlineContainerElement.scrollTop = originalScrollTop;
1046         }
1047
1048         delete this._updateChildrenInProgress;
1049     },
1050
1051     adjustCollapsedRange: function()
1052     {
1053         // Ensure precondition: only the tree elements for node children are found in the tree
1054         // (not the Expand All button or the closing tag).
1055         if (this.expandAllButtonElement && this.expandAllButtonElement.__treeElement.parent)
1056             this.removeChild(this.expandAllButtonElement.__treeElement);
1057
1058         const node = this.representedObject;
1059         if (!node.children)
1060             return;
1061         const childNodeCount = node.children.length;
1062
1063         // In case some nodes from the expanded range were removed, pull some nodes from the collapsed range into the expanded range at the bottom.
1064         for (var i = this.expandedChildCount, limit = Math.min(this.expandedChildrenLimit, childNodeCount); i < limit; ++i)
1065             this.insertChildElement(node.children[i], i);
1066
1067         const expandedChildCount = this.expandedChildCount;
1068         if (childNodeCount > this.expandedChildCount) {
1069             var targetButtonIndex = expandedChildCount;
1070             if (!this.expandAllButtonElement) {
1071                 var button = document.createElement("button");
1072                 button.className = "show-all-nodes";
1073                 button.value = "";
1074                 var item = new TreeElement(button, null, false);
1075                 item.selectable = false;
1076                 item.expandAllButton = true;
1077                 this.insertChild(item, targetButtonIndex);
1078                 this.expandAllButtonElement = item.listItemElement.firstChild;
1079                 this.expandAllButtonElement.__treeElement = item;
1080                 this.expandAllButtonElement.addEventListener("click", this.handleLoadAllChildren.bind(this), false);
1081             } else if (!this.expandAllButtonElement.__treeElement.parent)
1082                 this.insertChild(this.expandAllButtonElement.__treeElement, targetButtonIndex);
1083             this.expandAllButtonElement.textContent = WebInspector.UIString("Show All Nodes (%d More)", childNodeCount - expandedChildCount);
1084         } else if (this.expandAllButtonElement)
1085             delete this.expandAllButtonElement;
1086     },
1087
1088     handleLoadAllChildren: function()
1089     {
1090         this.expandedChildrenLimit = Math.max(this.representedObject._childNodeCount, this.expandedChildrenLimit + WebInspector.ElementsTreeElement.InitialChildrenLimit);
1091     },
1092
1093     expandRecursively: function()
1094     {
1095         function callback()
1096         {
1097             TreeElement.prototype.expandRecursively.call(this, Number.MAX_VALUE);
1098         }
1099         
1100         this.representedObject.getSubtree(-1, callback.bind(this));
1101     },
1102
1103     onexpand: function()
1104     {
1105         if (this._elementCloseTag)
1106             return;
1107
1108         this.updateTitle();
1109         this.treeOutline.updateSelection();
1110     },
1111
1112     oncollapse: function()
1113     {
1114         if (this._elementCloseTag)
1115             return;
1116
1117         this.updateTitle();
1118         this.treeOutline.updateSelection();
1119     },
1120
1121     onreveal: function()
1122     {
1123         if (this.listItemElement) {
1124             var tagSpans = this.listItemElement.getElementsByClassName("webkit-html-tag-name");
1125             if (tagSpans.length)
1126                 tagSpans[0].scrollIntoViewIfNeeded(false);
1127             else
1128                 this.listItemElement.scrollIntoViewIfNeeded(false);
1129         }
1130     },
1131
1132     onselect: function(selectedByUser)
1133     {
1134         this.treeOutline.suppressRevealAndSelect = true;
1135         this.treeOutline.selectDOMNode(this.representedObject, selectedByUser);
1136         if (selectedByUser)
1137             WebInspector.domAgent.highlightDOMNode(this.representedObject.id);
1138         this.updateSelection();
1139         this.treeOutline.suppressRevealAndSelect = false;
1140         return true;
1141     },
1142
1143     ondelete: function()
1144     {
1145         var startTagTreeElement = this.treeOutline.findTreeElement(this.representedObject);
1146         startTagTreeElement ? startTagTreeElement.remove() : this.remove();
1147         return true;
1148     },
1149
1150     onenter: function()
1151     {
1152         // On Enter or Return start editing the first attribute
1153         // or create a new attribute on the selected element.
1154         if (this._editing)
1155             return false;
1156
1157         this._startEditing();
1158
1159         // prevent a newline from being immediately inserted
1160         return true;
1161     },
1162
1163     selectOnMouseDown: function(event)
1164     {
1165         TreeElement.prototype.selectOnMouseDown.call(this, event);
1166
1167         if (this._editing)
1168             return;
1169
1170         if (this.treeOutline._showInElementsPanelEnabled) {
1171             WebInspector.showPanel("elements");
1172             this.treeOutline.selectDOMNode(this.representedObject, true);
1173         }
1174
1175         // Prevent selecting the nearest word on double click.
1176         if (event.detail >= 2)
1177             event.preventDefault();
1178     },
1179
1180     ondblclick: function(event)
1181     {
1182         if (this._editing || this._elementCloseTag)
1183             return;
1184
1185         if (this._startEditingTarget(event.target))
1186             return;
1187
1188         if (this.hasChildren && !this.expanded)
1189             this.expand();
1190     },
1191
1192     _insertInLastAttributePosition: function(tag, node)
1193     {
1194         if (tag.getElementsByClassName("webkit-html-attribute").length > 0)
1195             tag.insertBefore(node, tag.lastChild);
1196         else {
1197             var nodeName = tag.textContent.match(/^<(.*?)>$/)[1];
1198             tag.textContent = '';
1199             tag.appendChild(document.createTextNode('<'+nodeName));
1200             tag.appendChild(node);
1201             tag.appendChild(document.createTextNode('>'));
1202         }
1203
1204         this.updateSelection();
1205     },
1206
1207     _startEditingTarget: function(eventTarget)
1208     {
1209         if (this.treeOutline.selectedDOMNode() != this.representedObject)
1210             return;
1211
1212         if (this.representedObject.nodeType() != Node.ELEMENT_NODE && this.representedObject.nodeType() != Node.TEXT_NODE)
1213             return false;
1214
1215         var textNode = eventTarget.enclosingNodeOrSelfWithClass("webkit-html-text-node");
1216         if (textNode)
1217             return this._startEditingTextNode(textNode);
1218
1219         var attribute = eventTarget.enclosingNodeOrSelfWithClass("webkit-html-attribute");
1220         if (attribute)
1221             return this._startEditingAttribute(attribute, eventTarget);
1222
1223         var tagName = eventTarget.enclosingNodeOrSelfWithClass("webkit-html-tag-name");
1224         if (tagName)
1225             return this._startEditingTagName(tagName);
1226
1227         var newAttribute = eventTarget.enclosingNodeOrSelfWithClass("add-attribute");
1228         if (newAttribute)
1229             return this._addNewAttribute();
1230
1231         return false;
1232     },
1233
1234     _populateTagContextMenu: function(contextMenu, event)
1235     {
1236         var attribute = event.target.enclosingNodeOrSelfWithClass("webkit-html-attribute");
1237         var newAttribute = event.target.enclosingNodeOrSelfWithClass("add-attribute");
1238
1239         // Add attribute-related actions.
1240         var treeElement = this._elementCloseTag ? this.treeOutline.findTreeElement(this.representedObject) : this;
1241         contextMenu.appendItem(WebInspector.UIString(WebInspector.useLowerCaseMenuTitles() ? "Add attribute" : "Add Attribute"), this._addNewAttribute.bind(treeElement));
1242         if (attribute && !newAttribute)
1243             contextMenu.appendItem(WebInspector.UIString(WebInspector.useLowerCaseMenuTitles() ? "Edit attribute" : "Edit Attribute"), this._startEditingAttribute.bind(this, attribute, event.target));
1244         contextMenu.appendSeparator();
1245         if (this.treeOutline._setPseudoClassCallback) {
1246             var pseudoSubMenu = contextMenu.appendSubMenuItem(WebInspector.UIString(WebInspector.useLowerCaseMenuTitles() ? "Force element state" : "Force Element State"));
1247             this._populateForcedPseudoStateItems(pseudoSubMenu);
1248             contextMenu.appendSeparator();
1249         }
1250
1251         this._populateNodeContextMenu(contextMenu);
1252         this.treeOutline._populateContextMenu(contextMenu, this.representedObject);
1253
1254         contextMenu.appendSeparator();
1255         contextMenu.appendItem(WebInspector.UIString(WebInspector.useLowerCaseMenuTitles() ? "Scroll into view" : "Scroll Into View"), this._scrollIntoView.bind(this)); 
1256     },
1257
1258     _populateForcedPseudoStateItems: function(subMenu)
1259     {
1260         const pseudoClasses = ["active", "hover", "focus", "visited"];
1261         var node = this.representedObject;
1262         var forcedPseudoState = (node ? node.getUserProperty("pseudoState") : null) || [];
1263         for (var i = 0; i < pseudoClasses.length; ++i) {
1264             var pseudoClassForced = forcedPseudoState.indexOf(pseudoClasses[i]) >= 0;
1265             subMenu.appendCheckboxItem(":" + pseudoClasses[i], this.treeOutline._setPseudoClassCallback.bind(null, node.id, pseudoClasses[i], !pseudoClassForced), pseudoClassForced, false);
1266         }
1267     },
1268
1269     _populateTextContextMenu: function(contextMenu, textNode)
1270     {
1271         contextMenu.appendItem(WebInspector.UIString(WebInspector.useLowerCaseMenuTitles() ? "Edit text" : "Edit Text"), this._startEditingTextNode.bind(this, textNode));
1272         this._populateNodeContextMenu(contextMenu);
1273     },
1274
1275     _populateNodeContextMenu: function(contextMenu)
1276     {
1277         // Add free-form node-related actions.
1278         contextMenu.appendItem(WebInspector.UIString("Edit as HTML"), this._editAsHTML.bind(this));
1279         contextMenu.appendItem(WebInspector.UIString("Copy as HTML"), this._copyHTML.bind(this));
1280         contextMenu.appendItem(WebInspector.UIString("Copy XPath"), this._copyXPath.bind(this));
1281         contextMenu.appendItem(WebInspector.UIString(WebInspector.useLowerCaseMenuTitles() ? "Delete node" : "Delete Node"), this.remove.bind(this));
1282     },
1283
1284     _startEditing: function()
1285     {
1286         if (this.treeOutline.selectedDOMNode() !== this.representedObject)
1287             return;
1288
1289         var listItem = this._listItemNode;
1290
1291         if (this._canAddAttributes) {
1292             var attribute = listItem.getElementsByClassName("webkit-html-attribute")[0];
1293             if (attribute)
1294                 return this._startEditingAttribute(attribute, attribute.getElementsByClassName("webkit-html-attribute-value")[0]);
1295
1296             return this._addNewAttribute();
1297         }
1298
1299         if (this.representedObject.nodeType() === Node.TEXT_NODE) {
1300             var textNode = listItem.getElementsByClassName("webkit-html-text-node")[0];
1301             if (textNode)
1302                 return this._startEditingTextNode(textNode);
1303             return;
1304         }
1305     },
1306
1307     _addNewAttribute: function()
1308     {
1309         // Cannot just convert the textual html into an element without
1310         // a parent node. Use a temporary span container for the HTML.
1311         var container = document.createElement("span");
1312         this._buildAttributeDOM(container, " ", "");
1313         var attr = container.firstChild;
1314         attr.style.marginLeft = "2px"; // overrides the .editing margin rule
1315         attr.style.marginRight = "2px"; // overrides the .editing margin rule
1316
1317         var tag = this.listItemElement.getElementsByClassName("webkit-html-tag")[0];
1318         this._insertInLastAttributePosition(tag, attr);
1319         attr.scrollIntoViewIfNeeded(true);
1320         return this._startEditingAttribute(attr, attr);
1321     },
1322
1323     _triggerEditAttribute: function(attributeName)
1324     {
1325         var attributeElements = this.listItemElement.getElementsByClassName("webkit-html-attribute-name");
1326         for (var i = 0, len = attributeElements.length; i < len; ++i) {
1327             if (attributeElements[i].textContent === attributeName) {
1328                 for (var elem = attributeElements[i].nextSibling; elem; elem = elem.nextSibling) {
1329                     if (elem.nodeType !== Node.ELEMENT_NODE)
1330                         continue;
1331
1332                     if (elem.hasStyleClass("webkit-html-attribute-value"))
1333                         return this._startEditingAttribute(elem.parentNode, elem);
1334                 }
1335             }
1336         }
1337     },
1338
1339     _startEditingAttribute: function(attribute, elementForSelection)
1340     {
1341         if (WebInspector.isBeingEdited(attribute))
1342             return true;
1343
1344         var attributeNameElement = attribute.getElementsByClassName("webkit-html-attribute-name")[0];
1345         if (!attributeNameElement)
1346             return false;
1347
1348         var attributeName = attributeNameElement.textContent;
1349
1350         function removeZeroWidthSpaceRecursive(node)
1351         {
1352             if (node.nodeType === Node.TEXT_NODE) {
1353                 node.nodeValue = node.nodeValue.replace(/\u200B/g, "");
1354                 return;
1355             }
1356
1357             if (node.nodeType !== Node.ELEMENT_NODE)
1358                 return;
1359
1360             for (var child = node.firstChild; child; child = child.nextSibling)
1361                 removeZeroWidthSpaceRecursive(child);
1362         }
1363
1364         // Remove zero-width spaces that were added by nodeTitleInfo.
1365         removeZeroWidthSpaceRecursive(attribute);
1366
1367         var config = new WebInspector.EditingConfig(this._attributeEditingCommitted.bind(this), this._editingCancelled.bind(this), attributeName);
1368         
1369         function handleKeyDownEvents(event)
1370         {
1371             var isMetaOrCtrl = WebInspector.isMac() ?
1372                 event.metaKey && !event.shiftKey && !event.ctrlKey && !event.altKey :
1373                 event.ctrlKey && !event.shiftKey && !event.metaKey && !event.altKey;
1374             if (isEnterKey(event) && (event.isMetaOrCtrlForTest || !config.multiline || isMetaOrCtrl))
1375                 return "commit";
1376             else if (event.keyCode === WebInspector.KeyboardShortcut.Keys.Esc.code || event.keyIdentifier === "U+001B")
1377                 return "cancel";
1378             else if (event.keyIdentifier === "U+0009") // Tab key
1379                 return "move-" + (event.shiftKey ? "backward" : "forward");
1380             else {
1381                 WebInspector.handleElementValueModifications(event, attribute);
1382                 return "";
1383             }
1384         }
1385
1386         config.customFinishHandler = handleKeyDownEvents.bind(this);
1387
1388         this._editing = WebInspector.startEditing(attribute, config);
1389
1390         window.getSelection().setBaseAndExtent(elementForSelection, 0, elementForSelection, 1);
1391
1392         return true;
1393     },
1394
1395     /**
1396      * @param {Element} textNodeElement
1397      */
1398     _startEditingTextNode: function(textNodeElement)
1399     {
1400         if (WebInspector.isBeingEdited(textNodeElement))
1401             return true;
1402
1403         var textNode = this.representedObject;
1404         // We only show text nodes inline in elements if the element only
1405         // has a single child, and that child is a text node.
1406         if (textNode.nodeType() === Node.ELEMENT_NODE && textNode.firstChild)
1407             textNode = textNode.firstChild;
1408
1409         var container = textNodeElement.enclosingNodeOrSelfWithClass("webkit-html-text-node");
1410         if (container)
1411             container.textContent = textNode.nodeValue(); // Strip the CSS or JS highlighting if present.
1412         var config = new WebInspector.EditingConfig(this._textNodeEditingCommitted.bind(this, textNode), this._editingCancelled.bind(this));
1413         this._editing = WebInspector.startEditing(textNodeElement, config);
1414         window.getSelection().setBaseAndExtent(textNodeElement, 0, textNodeElement, 1);
1415
1416         return true;
1417     },
1418
1419     /**
1420      * @param {Element=} tagNameElement
1421      */
1422     _startEditingTagName: function(tagNameElement)
1423     {
1424         if (!tagNameElement) {
1425             tagNameElement = this.listItemElement.getElementsByClassName("webkit-html-tag-name")[0];
1426             if (!tagNameElement)
1427                 return false;
1428         }
1429
1430         var tagName = tagNameElement.textContent;
1431         if (WebInspector.ElementsTreeElement.EditTagBlacklist[tagName.toLowerCase()])
1432             return false;
1433
1434         if (WebInspector.isBeingEdited(tagNameElement))
1435             return true;
1436
1437         var closingTagElement = this._distinctClosingTagElement();
1438
1439         function keyupListener(event)
1440         {
1441             if (closingTagElement)
1442                 closingTagElement.textContent = "</" + tagNameElement.textContent + ">";
1443         }
1444
1445         function editingComitted(element, newTagName)
1446         {
1447             tagNameElement.removeEventListener('keyup', keyupListener, false);
1448             this._tagNameEditingCommitted.apply(this, arguments);
1449         }
1450
1451         function editingCancelled()
1452         {
1453             tagNameElement.removeEventListener('keyup', keyupListener, false);
1454             this._editingCancelled.apply(this, arguments);
1455         }
1456
1457         tagNameElement.addEventListener('keyup', keyupListener, false);
1458
1459         var config = new WebInspector.EditingConfig(editingComitted.bind(this), editingCancelled.bind(this), tagName);
1460         this._editing = WebInspector.startEditing(tagNameElement, config);
1461         window.getSelection().setBaseAndExtent(tagNameElement, 0, tagNameElement, 1);
1462         return true;
1463     },
1464
1465     _startEditingAsHTML: function(commitCallback, error, initialValue)
1466     {
1467         if (error)
1468             return;
1469         if (this._htmlEditElement && WebInspector.isBeingEdited(this._htmlEditElement))
1470             return;
1471
1472         function consume(event)
1473         {
1474             if (event.eventPhase === Event.AT_TARGET)
1475                 event.consume(true);
1476         }
1477
1478         initialValue = this._convertWhitespaceToEntities(initialValue);
1479
1480         this._htmlEditElement = document.createElement("div");
1481         this._htmlEditElement.className = "source-code elements-tree-editor";
1482         this._htmlEditElement.textContent = initialValue;
1483
1484         // Hide header items.
1485         var child = this.listItemElement.firstChild;
1486         while (child) {
1487             child.style.display = "none";
1488             child = child.nextSibling;
1489         }
1490         // Hide children item.
1491         if (this._childrenListNode)
1492             this._childrenListNode.style.display = "none";
1493         // Append editor.
1494         this.listItemElement.appendChild(this._htmlEditElement);
1495         this.treeOutline.childrenListElement.parentElement.addEventListener("mousedown", consume, false);
1496
1497         this.updateSelection();
1498
1499         function commit()
1500         {
1501             commitCallback(initialValue, this._htmlEditElement.textContent);
1502             dispose.call(this);
1503         }
1504
1505         function dispose()
1506         {
1507             delete this._editing;
1508
1509             // Remove editor.
1510             this.listItemElement.removeChild(this._htmlEditElement);
1511             delete this._htmlEditElement;
1512             // Unhide children item.
1513             if (this._childrenListNode)
1514                 this._childrenListNode.style.removeProperty("display");
1515             // Unhide header items.
1516             var child = this.listItemElement.firstChild;
1517             while (child) {
1518                 child.style.removeProperty("display");
1519                 child = child.nextSibling;
1520             }
1521
1522             this.treeOutline.childrenListElement.parentElement.removeEventListener("mousedown", consume, false);
1523             this.updateSelection();
1524         }
1525
1526         var config = new WebInspector.EditingConfig(commit.bind(this), dispose.bind(this));
1527         config.setMultiline(true);
1528         this._editing = WebInspector.startEditing(this._htmlEditElement, config);
1529     },
1530
1531     _attributeEditingCommitted: function(element, newText, oldText, attributeName, moveDirection)
1532     {
1533         delete this._editing;
1534
1535         var treeOutline = this.treeOutline;
1536         /**
1537          * @param {Protocol.Error=} error
1538          */
1539         function moveToNextAttributeIfNeeded(error)
1540         {
1541             if (error)
1542                 this._editingCancelled(element, attributeName);
1543
1544             if (!moveDirection)
1545                 return;
1546
1547             treeOutline._updateModifiedNodes();
1548
1549             // Search for the attribute's position, and then decide where to move to.
1550             var attributes = this.representedObject.attributes();
1551             for (var i = 0; i < attributes.length; ++i) {
1552                 if (attributes[i].name !== attributeName)
1553                     continue;
1554
1555                 if (moveDirection === "backward") {
1556                     if (i === 0)
1557                         this._startEditingTagName();
1558                     else
1559                         this._triggerEditAttribute(attributes[i - 1].name);
1560                 } else {
1561                     if (i === attributes.length - 1)
1562                         this._addNewAttribute();
1563                     else
1564                         this._triggerEditAttribute(attributes[i + 1].name);
1565                 }
1566                 return;
1567             }
1568
1569             // Moving From the "New Attribute" position.
1570             if (moveDirection === "backward") {
1571                 if (newText === " ") {
1572                     // Moving from "New Attribute" that was not edited
1573                     if (attributes.length > 0)
1574                         this._triggerEditAttribute(attributes[attributes.length - 1].name);
1575                 } else {
1576                     // Moving from "New Attribute" that holds new value
1577                     if (attributes.length > 1)
1578                         this._triggerEditAttribute(attributes[attributes.length - 2].name);
1579                 }
1580             } else if (moveDirection === "forward") {
1581                 if (!/^\s*$/.test(newText))
1582                     this._addNewAttribute();
1583                 else
1584                     this._startEditingTagName();
1585             }
1586         }
1587
1588         if (oldText !== newText)
1589             this.representedObject.setAttribute(attributeName, newText, moveToNextAttributeIfNeeded.bind(this));
1590         else
1591             moveToNextAttributeIfNeeded.call(this);
1592     },
1593
1594     _tagNameEditingCommitted: function(element, newText, oldText, tagName, moveDirection)
1595     {
1596         delete this._editing;
1597         var self = this;
1598
1599         function cancel()
1600         {
1601             var closingTagElement = self._distinctClosingTagElement();
1602             if (closingTagElement)
1603                 closingTagElement.textContent = "</" + tagName + ">";
1604
1605             self._editingCancelled(element, tagName);
1606             moveToNextAttributeIfNeeded.call(self);
1607         }
1608
1609         function moveToNextAttributeIfNeeded()
1610         {
1611             if (moveDirection !== "forward") {
1612                 this._addNewAttribute();
1613                 return;
1614             }
1615
1616             var attributes = this.representedObject.attributes();
1617             if (attributes.length > 0)
1618                 this._triggerEditAttribute(attributes[0].name);
1619             else
1620                 this._addNewAttribute();
1621         }
1622
1623         newText = newText.trim();
1624         if (newText === oldText) {
1625             cancel();
1626             return;
1627         }
1628
1629         var treeOutline = this.treeOutline;
1630         var wasExpanded = this.expanded;
1631
1632         function changeTagNameCallback(error, nodeId)
1633         {
1634             if (error || !nodeId) {
1635                 cancel();
1636                 return;
1637             }
1638             var newTreeItem = treeOutline._selectNodeAfterEdit(null, wasExpanded, error, nodeId);
1639             moveToNextAttributeIfNeeded.call(newTreeItem);
1640         }
1641
1642         this.representedObject.setNodeName(newText, changeTagNameCallback);
1643     },
1644
1645     /**
1646      * @param {WebInspector.DOMNode} textNode
1647      * @param {Element} element
1648      * @param {string} newText
1649      */
1650     _textNodeEditingCommitted: function(textNode, element, newText)
1651     {
1652         delete this._editing;
1653
1654         function callback()
1655         {
1656             this.updateTitle();
1657         }
1658         textNode.setNodeValue(newText, callback.bind(this));
1659     },
1660
1661     /**
1662      * @param {Element} element
1663      * @param {*} context
1664      */
1665     _editingCancelled: function(element, context)
1666     {
1667         delete this._editing;
1668
1669         // Need to restore attributes structure.
1670         this.updateTitle();
1671     },
1672
1673     _distinctClosingTagElement: function()
1674     {
1675         // FIXME: Improve the Tree Element / Outline Abstraction to prevent crawling the DOM
1676
1677         // For an expanded element, it will be the last element with class "close"
1678         // in the child element list.
1679         if (this.expanded) {
1680             var closers = this._childrenListNode.querySelectorAll(".close");
1681             return closers[closers.length-1];
1682         }
1683
1684         // Remaining cases are single line non-expanded elements with a closing
1685         // tag, or HTML elements without a closing tag (such as <br>). Return
1686         // null in the case where there isn't a closing tag.
1687         var tags = this.listItemElement.getElementsByClassName("webkit-html-tag");
1688         return (tags.length === 1 ? null : tags[tags.length-1]);
1689     },
1690
1691     /**
1692      * @param {boolean=} onlySearchQueryChanged
1693      */
1694     updateTitle: function(onlySearchQueryChanged)
1695     {
1696         // If we are editing, return early to prevent canceling the edit.
1697         // After editing is committed updateTitle will be called.
1698         if (this._editing)
1699             return;
1700
1701         if (onlySearchQueryChanged) {
1702             if (this._highlightResult)
1703                 this._updateSearchHighlight(false);
1704         } else {
1705             var highlightElement = document.createElement("span");
1706             highlightElement.className = "highlight";
1707             highlightElement.appendChild(this._nodeTitleInfo(WebInspector.linkifyURLAsNode).titleDOM);
1708             this.title = highlightElement;
1709             this._updateDecorations();
1710             delete this._highlightResult;
1711         }
1712
1713         delete this.selectionElement;
1714         if (this.selected)
1715             this.updateSelection();
1716         this._preventFollowingLinksOnDoubleClick();
1717         this._highlightSearchResults();
1718     },
1719
1720     _createDecoratorElement: function()
1721     {
1722         var node = this.representedObject;
1723         var decoratorMessages = [];
1724         var parentDecoratorMessages = [];
1725         for (var i = 0; i < this.treeOutline._nodeDecorators.length; ++i) {
1726             var decorator = this.treeOutline._nodeDecorators[i];
1727             var message = decorator.decorate(node);
1728             if (message) {
1729                 decoratorMessages.push(message);
1730                 continue;
1731             }
1732
1733             if (this.expanded || this._elementCloseTag)
1734                 continue;
1735
1736             message = decorator.decorateAncestor(node);
1737             if (message)
1738                 parentDecoratorMessages.push(message)
1739         }
1740         if (!decoratorMessages.length && !parentDecoratorMessages.length)
1741             return null;
1742
1743         var decoratorElement = document.createElement("div");
1744         decoratorElement.addStyleClass("elements-gutter-decoration");
1745         if (!decoratorMessages.length)
1746             decoratorElement.addStyleClass("elements-has-decorated-children");
1747         decoratorElement.title = decoratorMessages.concat(parentDecoratorMessages).join("\n");
1748         return decoratorElement;
1749     },
1750
1751     _updateDecorations: function()
1752     {
1753         if (this._decoratorElement && this._decoratorElement.parentElement)
1754             this._decoratorElement.parentElement.removeChild(this._decoratorElement);
1755         this._decoratorElement = this._createDecoratorElement();
1756         if (this._decoratorElement && this.listItemElement)
1757             this.listItemElement.insertBefore(this._decoratorElement, this.listItemElement.firstChild);
1758     },
1759
1760     /**
1761      * @param {WebInspector.DOMNode=} node
1762      * @param {function(string, string, string, boolean=, string=)=} linkify
1763      */
1764     _buildAttributeDOM: function(parentElement, name, value, node, linkify)
1765     {
1766         var hasText = (value.length > 0);
1767         var attrSpanElement = parentElement.createChild("span", "webkit-html-attribute");
1768         var attrNameElement = attrSpanElement.createChild("span", "webkit-html-attribute-name");
1769         attrNameElement.textContent = name;
1770
1771         if (hasText)
1772             attrSpanElement.appendChild(document.createTextNode("=\u200B\""));
1773
1774         if (linkify && (name === "src" || name === "href")) {
1775             var rewrittenHref = node.resolveURL(value);
1776             value = value.replace(/([\/;:\)\]\}])/g, "$1\u200B");
1777             if (rewrittenHref === null) {
1778                 var attrValueElement = attrSpanElement.createChild("span", "webkit-html-attribute-value");
1779                 attrValueElement.textContent = value;
1780             } else {
1781                 if (value.startsWith("data:"))
1782                     value = value.trimMiddle(60);
1783                 attrSpanElement.appendChild(linkify(rewrittenHref, value, "webkit-html-attribute-value", node.nodeName().toLowerCase() === "a"));
1784             }
1785         } else {
1786             value = value.replace(/([\/;:\)\]\}])/g, "$1\u200B");
1787             var attrValueElement = attrSpanElement.createChild("span", "webkit-html-attribute-value");
1788             attrValueElement.textContent = value;
1789         }
1790
1791         if (hasText)
1792             attrSpanElement.appendChild(document.createTextNode("\""));
1793     },
1794
1795     /**
1796      * @param {function(string, string, string, boolean=, string=)=} linkify
1797      */
1798     _buildTagDOM: function(parentElement, tagName, isClosingTag, isDistinctTreeElement, linkify)
1799     {
1800         var node = /** @type WebInspector.DOMNode */ (this.representedObject);
1801         var classes = [ "webkit-html-tag" ];
1802         if (isClosingTag && isDistinctTreeElement)
1803             classes.push("close");
1804         if (node.isInShadowTree())
1805             classes.push("shadow");
1806         var tagElement = parentElement.createChild("span", classes.join(" "));
1807         tagElement.appendChild(document.createTextNode("<"));
1808         var tagNameElement = tagElement.createChild("span", isClosingTag ? "" : "webkit-html-tag-name");
1809         tagNameElement.textContent = (isClosingTag ? "/" : "") + tagName;
1810         if (!isClosingTag && node.hasAttributes()) {
1811             var attributes = node.attributes();
1812             for (var i = 0; i < attributes.length; ++i) {
1813                 var attr = attributes[i];
1814                 tagElement.appendChild(document.createTextNode(" "));
1815                 this._buildAttributeDOM(tagElement, attr.name, attr.value, node, linkify);
1816             }
1817         }
1818         tagElement.appendChild(document.createTextNode(">"));
1819         parentElement.appendChild(document.createTextNode("\u200B"));
1820     },
1821
1822     _convertWhitespaceToEntities: function(text)
1823     {
1824         var result = "";
1825         var lastIndexAfterEntity = 0;
1826         var charToEntity = WebInspector.ElementsTreeOutline.MappedCharToEntity;
1827         for (var i = 0, size = text.length; i < size; ++i) {
1828             var char = text.charAt(i);
1829             if (charToEntity[char]) {
1830                 result += text.substring(lastIndexAfterEntity, i) + "&" + charToEntity[char] + ";";
1831                 lastIndexAfterEntity = i + 1;
1832             }
1833         }
1834         if (result) {
1835             result += text.substring(lastIndexAfterEntity);
1836             return result;
1837         }
1838         return text;
1839     },
1840
1841     _nodeTitleInfo: function(linkify)
1842     {
1843         var node = this.representedObject;
1844         var info = {titleDOM: document.createDocumentFragment(), hasChildren: this.hasChildren};
1845
1846         switch (node.nodeType()) {
1847             case Node.ATTRIBUTE_NODE:
1848                 var value = node.value || "\u200B"; // Zero width space to force showing an empty value.
1849                 this._buildAttributeDOM(info.titleDOM, node.name, value);
1850                 break;
1851
1852             case Node.ELEMENT_NODE:
1853                 var tagName = node.nodeNameInCorrectCase();
1854                 if (this._elementCloseTag) {
1855                     this._buildTagDOM(info.titleDOM, tagName, true, true);
1856                     info.hasChildren = false;
1857                     break;
1858                 }
1859
1860                 this._buildTagDOM(info.titleDOM, tagName, false, false, linkify);
1861
1862                 var textChild = this._singleTextChild(node);
1863                 var showInlineText = textChild && textChild.nodeValue().length < Preferences.maxInlineTextChildLength && !this.hasChildren;
1864
1865                 if (!this.expanded && (!showInlineText && (this.treeOutline.isXMLMimeType || !WebInspector.ElementsTreeElement.ForbiddenClosingTagElements[tagName]))) {
1866                     if (this.hasChildren) {
1867                         var textNodeElement = info.titleDOM.createChild("span", "webkit-html-text-node bogus");
1868                         textNodeElement.textContent = "\u2026";
1869                         info.titleDOM.appendChild(document.createTextNode("\u200B"));
1870                     }
1871                     this._buildTagDOM(info.titleDOM, tagName, true, false);
1872                 }
1873
1874                 // If this element only has a single child that is a text node,
1875                 // just show that text and the closing tag inline rather than
1876                 // create a subtree for them
1877                 if (showInlineText) {
1878                     var textNodeElement = info.titleDOM.createChild("span", "webkit-html-text-node");
1879                     textNodeElement.textContent = this._convertWhitespaceToEntities(textChild.nodeValue());
1880                     info.titleDOM.appendChild(document.createTextNode("\u200B"));
1881                     this._buildTagDOM(info.titleDOM, tagName, true, false);
1882                     info.hasChildren = false;
1883                 }
1884                 break;
1885
1886             case Node.TEXT_NODE:
1887                 if (node.parentNode && node.parentNode.nodeName().toLowerCase() === "script") {
1888                     var newNode = info.titleDOM.createChild("span", "webkit-html-text-node webkit-html-js-node");
1889                     newNode.textContent = node.nodeValue();
1890
1891                     var javascriptSyntaxHighlighter = new WebInspector.DOMSyntaxHighlighter("text/javascript", true);
1892                     javascriptSyntaxHighlighter.syntaxHighlightNode(newNode);
1893                 } else if (node.parentNode && node.parentNode.nodeName().toLowerCase() === "style") {
1894                     var newNode = info.titleDOM.createChild("span", "webkit-html-text-node webkit-html-css-node");
1895                     newNode.textContent = node.nodeValue();
1896
1897                     var cssSyntaxHighlighter = new WebInspector.DOMSyntaxHighlighter("text/css", true);
1898                     cssSyntaxHighlighter.syntaxHighlightNode(newNode);
1899                 } else {
1900                     info.titleDOM.appendChild(document.createTextNode("\""));
1901                     var textNodeElement = info.titleDOM.createChild("span", "webkit-html-text-node");
1902                     textNodeElement.textContent = this._convertWhitespaceToEntities(node.nodeValue());
1903                     info.titleDOM.appendChild(document.createTextNode("\""));
1904                 }
1905                 break;
1906
1907             case Node.COMMENT_NODE:
1908                 var commentElement = info.titleDOM.createChild("span", "webkit-html-comment");
1909                 commentElement.appendChild(document.createTextNode("<!--" + node.nodeValue() + "-->"));
1910                 break;
1911
1912             case Node.DOCUMENT_TYPE_NODE:
1913                 var docTypeElement = info.titleDOM.createChild("span", "webkit-html-doctype");
1914                 docTypeElement.appendChild(document.createTextNode("<!DOCTYPE " + node.nodeName()));
1915                 if (node.publicId) {
1916                     docTypeElement.appendChild(document.createTextNode(" PUBLIC \"" + node.publicId + "\""));
1917                     if (node.systemId)
1918                         docTypeElement.appendChild(document.createTextNode(" \"" + node.systemId + "\""));
1919                 } else if (node.systemId)
1920                     docTypeElement.appendChild(document.createTextNode(" SYSTEM \"" + node.systemId + "\""));
1921
1922                 if (node.internalSubset)
1923                     docTypeElement.appendChild(document.createTextNode(" [" + node.internalSubset + "]"));
1924
1925                 docTypeElement.appendChild(document.createTextNode(">"));
1926                 break;
1927
1928             case Node.CDATA_SECTION_NODE:
1929                 var cdataElement = info.titleDOM.createChild("span", "webkit-html-text-node");
1930                 cdataElement.appendChild(document.createTextNode("<![CDATA[" + node.nodeValue() + "]]>"));
1931                 break;
1932             case Node.DOCUMENT_FRAGMENT_NODE:
1933                 var fragmentElement = info.titleDOM.createChild("span", "webkit-html-fragment");
1934                 fragmentElement.textContent = node.nodeNameInCorrectCase().collapseWhitespace();
1935                 if (node.isInShadowTree())
1936                     fragmentElement.addStyleClass("shadow");
1937                 break;
1938             default:
1939                 info.titleDOM.appendChild(document.createTextNode(node.nodeNameInCorrectCase().collapseWhitespace()));
1940         }
1941         return info;
1942     },
1943
1944     _singleTextChild: function(node)
1945     {
1946         if (!node)
1947             return null;
1948
1949         var firstChild = node.firstChild;
1950         if (!firstChild || firstChild.nodeType() !== Node.TEXT_NODE)
1951             return null;
1952
1953         if (node.hasShadowRoots())
1954             return null;
1955
1956         var sibling = firstChild.nextSibling;
1957         return sibling ? null : firstChild;
1958     },
1959
1960     _showInlineText: function(node)
1961     {
1962         if (node.nodeType() === Node.ELEMENT_NODE) {
1963             var textChild = this._singleTextChild(node);
1964             if (textChild && textChild.nodeValue().length < Preferences.maxInlineTextChildLength)
1965                 return true;
1966         }
1967         return false;
1968     },
1969
1970     remove: function()
1971     {
1972         var parentElement = this.parent;
1973         if (!parentElement)
1974             return;
1975
1976         var self = this;
1977         function removeNodeCallback(error, removedNodeId)
1978         {
1979             if (error)
1980                 return;
1981
1982             parentElement.removeChild(self);
1983             parentElement.adjustCollapsedRange();
1984         }
1985
1986         if (!this.representedObject.parentNode || this.representedObject.parentNode.nodeType() === Node.DOCUMENT_NODE)
1987             return;
1988         this.representedObject.removeNode(removeNodeCallback);
1989     },
1990
1991     _editAsHTML: function()
1992     {
1993         var treeOutline = this.treeOutline;
1994         var node = this.representedObject;
1995         var parentNode = node.parentNode;
1996         var index = node.index;
1997         var wasExpanded = this.expanded;
1998
1999         function selectNode(error, nodeId)
2000         {
2001             if (error)
2002                 return;
2003
2004             // Select it and expand if necessary. We force tree update so that it processes dom events and is up to date.
2005             treeOutline._updateModifiedNodes();
2006
2007             var newNode = parentNode ? parentNode.children[index] || parentNode : null;
2008             if (!newNode)
2009                 return;
2010
2011             treeOutline.selectDOMNode(newNode, true);
2012
2013             if (wasExpanded) {
2014                 var newTreeItem = treeOutline.findTreeElement(newNode);
2015                 if (newTreeItem)
2016                     newTreeItem.expand();
2017             }
2018         }
2019
2020         function commitChange(initialValue, value)
2021         {
2022             if (initialValue !== value)
2023                 node.setOuterHTML(value, selectNode);
2024             else
2025                 return;
2026         }
2027
2028         node.getOuterHTML(this._startEditingAsHTML.bind(this, commitChange));
2029     },
2030
2031     _copyHTML: function()
2032     {
2033         this.representedObject.copyNode();
2034     },
2035
2036     _copyXPath: function()
2037     {
2038         this.representedObject.copyXPath(true);
2039     },
2040
2041     _highlightSearchResults: function()
2042     {
2043         if (!this._searchQuery || !this._searchHighlightsVisible)
2044             return;
2045         if (this._highlightResult) {
2046             this._updateSearchHighlight(true);
2047             return;
2048         }
2049
2050         var text = this.listItemElement.textContent;
2051         var regexObject = createPlainTextSearchRegex(this._searchQuery, "gi");
2052
2053         var offset = 0;
2054         var match = regexObject.exec(text);
2055         var matchRanges = [];
2056         while (match) {
2057             matchRanges.push({ offset: match.index, length: match[0].length });
2058             match = regexObject.exec(text);
2059         }
2060
2061         // Fall back for XPath, etc. matches.
2062         if (!matchRanges.length)
2063             matchRanges.push({ offset: 0, length: text.length });
2064
2065         this._highlightResult = [];
2066         WebInspector.highlightSearchResults(this.listItemElement, matchRanges, this._highlightResult);
2067     },
2068
2069     _scrollIntoView: function()
2070     {
2071         function scrollIntoViewCallback(object)
2072         {
2073             function scrollIntoView()
2074             {
2075                 this.scrollIntoViewIfNeeded(true);
2076             }
2077
2078             if (object)
2079                 object.callFunction(scrollIntoView);
2080         }
2081         
2082         var node = /** @type {WebInspector.DOMNode} */ (this.representedObject);
2083         WebInspector.RemoteObject.resolveNode(node, "", scrollIntoViewCallback);
2084     },
2085
2086     __proto__: TreeElement.prototype
2087 }
2088
2089 /**
2090  * @constructor
2091  */
2092 WebInspector.ElementsTreeUpdater = function(treeOutline)
2093 {
2094     WebInspector.domAgent.addEventListener(WebInspector.DOMAgent.Events.NodeInserted, this._nodeInserted, this);
2095     WebInspector.domAgent.addEventListener(WebInspector.DOMAgent.Events.NodeRemoved, this._nodeRemoved, this);
2096     WebInspector.domAgent.addEventListener(WebInspector.DOMAgent.Events.AttrModified, this._attributesUpdated, this);
2097     WebInspector.domAgent.addEventListener(WebInspector.DOMAgent.Events.AttrRemoved, this._attributesUpdated, this);
2098     WebInspector.domAgent.addEventListener(WebInspector.DOMAgent.Events.CharacterDataModified, this._characterDataModified, this);
2099     WebInspector.domAgent.addEventListener(WebInspector.DOMAgent.Events.DocumentUpdated, this._documentUpdated, this);
2100     WebInspector.domAgent.addEventListener(WebInspector.DOMAgent.Events.ChildNodeCountUpdated, this._childNodeCountUpdated, this);
2101
2102     this._treeOutline = treeOutline;
2103     this._recentlyModifiedNodes = new Map();
2104 }
2105
2106 WebInspector.ElementsTreeUpdater.prototype = {
2107
2108     /**
2109      * @param {!WebInspector.DOMNode} node
2110      * @param {boolean} isUpdated
2111      * @param {WebInspector.DOMNode=} parentNode
2112      */
2113     _nodeModified: function(node, isUpdated, parentNode)
2114     {
2115         if (this._treeOutline._visible)
2116             this._updateModifiedNodesSoon();
2117
2118         var entry = /** @type {WebInspector.ElementsTreeUpdater.UpdateEntry} */ (this._recentlyModifiedNodes.get(node));
2119         if (!entry) {
2120             entry = new WebInspector.ElementsTreeUpdater.UpdateEntry(isUpdated, parentNode);
2121             this._recentlyModifiedNodes.put(node, entry);
2122             return;
2123         }
2124
2125         entry.isUpdated |= isUpdated;
2126         if (parentNode)
2127             entry.parent = parentNode;
2128     },
2129
2130     _documentUpdated: function(event)
2131     {
2132         var inspectedRootDocument = event.data;
2133
2134         this._reset();
2135
2136         if (!inspectedRootDocument)
2137             return;
2138
2139         this._treeOutline.rootDOMNode = inspectedRootDocument;
2140     },
2141
2142     _attributesUpdated: function(event)
2143     {
2144         this._nodeModified(event.data.node, true);
2145     },
2146
2147     _characterDataModified: function(event)
2148     {
2149         this._nodeModified(event.data, true);
2150     },
2151
2152     _nodeInserted: function(event)
2153     {
2154         this._nodeModified(event.data, false, event.data.parentNode);
2155     },
2156
2157     _nodeRemoved: function(event)
2158     {
2159         this._nodeModified(event.data.node, false, event.data.parent);
2160     },
2161
2162     _childNodeCountUpdated: function(event)
2163     {
2164         var treeElement = this._treeOutline.findTreeElement(event.data);
2165         if (treeElement)
2166             treeElement.hasChildren = event.data.hasChildNodes();
2167     },
2168
2169     _updateModifiedNodesSoon: function()
2170     {
2171         if (this._updateModifiedNodesTimeout)
2172             return;
2173         this._updateModifiedNodesTimeout = setTimeout(this._updateModifiedNodes.bind(this), 50);
2174     },
2175
2176     _updateModifiedNodes: function()
2177     {
2178         if (this._updateModifiedNodesTimeout) {
2179             clearTimeout(this._updateModifiedNodesTimeout);
2180             delete this._updateModifiedNodesTimeout;
2181         }
2182
2183         var updatedParentTreeElements = [];
2184
2185         var hidePanelWhileUpdating = this._recentlyModifiedNodes.size() > 10;
2186         if (hidePanelWhileUpdating) {
2187             var treeOutlineContainerElement = this._treeOutline.element.parentNode;
2188             this._treeOutline.element.addStyleClass("hidden");
2189             var originalScrollTop = treeOutlineContainerElement ? treeOutlineContainerElement.scrollTop : 0;
2190         }
2191
2192         var keys = this._recentlyModifiedNodes.keys();
2193         for (var i = 0, size = keys.length; i < size; ++i) {
2194             var node = keys[i];
2195             var entry = this._recentlyModifiedNodes.get(node);
2196             var parent = entry.parent;
2197
2198             if (parent === this._treeOutline._rootDOMNode) {
2199                 // Document's children have changed, perform total update.
2200                 this._treeOutline.update();
2201                 this._treeOutline.element.removeStyleClass("hidden");
2202                 return;
2203             }
2204
2205             if (entry.isUpdated) {
2206                 var nodeItem = this._treeOutline.findTreeElement(node);
2207                 if (nodeItem)
2208                     nodeItem.updateTitle();
2209             }
2210
2211             if (!parent)
2212                 continue;
2213
2214             var parentNodeItem = this._treeOutline.findTreeElement(parent);
2215             if (parentNodeItem && !parentNodeItem.alreadyUpdatedChildren) {
2216                 parentNodeItem.updateChildren();
2217                 parentNodeItem.alreadyUpdatedChildren = true;
2218                 updatedParentTreeElements.push(parentNodeItem);
2219             }
2220         }
2221
2222         for (var i = 0; i < updatedParentTreeElements.length; ++i)
2223             delete updatedParentTreeElements[i].alreadyUpdatedChildren;
2224
2225         if (hidePanelWhileUpdating) {
2226             this._treeOutline.element.removeStyleClass("hidden");
2227             if (originalScrollTop)
2228                 treeOutlineContainerElement.scrollTop = originalScrollTop;
2229             this._treeOutline.updateSelection();
2230         }
2231         this._recentlyModifiedNodes.clear();
2232     },
2233
2234     _reset: function()
2235     {
2236         this._treeOutline.rootDOMNode = null;
2237         this._treeOutline.selectDOMNode(null, false);
2238         WebInspector.domAgent.hideDOMNodeHighlight();
2239         this._recentlyModifiedNodes.clear();
2240     }
2241 }
2242
2243 /**
2244  * @constructor
2245  * @param {boolean} isUpdated
2246  * @param {WebInspector.DOMNode=} parent
2247  */
2248 WebInspector.ElementsTreeUpdater.UpdateEntry = function(isUpdated, parent)
2249 {
2250     this.isUpdated = isUpdated;
2251     if (parent)
2252         this.parent = parent;
2253 }