https://bugs.webkit.org/show_bug.cgi?id=39224
[WebKit-https.git] / 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 WebInspector.ElementsTreeOutline = function() {
32     this.element = document.createElement("ol");
33     this.element.addEventListener("mousedown", this._onmousedown.bind(this), false);
34     this.element.addEventListener("mousemove", this._onmousemove.bind(this), false);
35     this.element.addEventListener("mouseout", this._onmouseout.bind(this), false);
36
37     TreeOutline.call(this, this.element);
38
39     this.includeRootDOMNode = true;
40     this.selectEnabled = false;
41     this.showInElementsPanelEnabled = false;
42     this.rootDOMNode = null;
43     this.focusedDOMNode = null;
44
45     this.element.addEventListener("contextmenu", this._contextMenuEventFired.bind(this), true);
46 }
47
48 WebInspector.ElementsTreeOutline.prototype = {
49     get rootDOMNode()
50     {
51         return this._rootDOMNode;
52     },
53
54     set rootDOMNode(x)
55     {
56         if (this._rootDOMNode === x)
57             return;
58
59         this._rootDOMNode = x;
60
61         this._isXMLMimeType = !!(WebInspector.mainResource && WebInspector.mainResource.mimeType && WebInspector.mainResource.mimeType.match(/x(?:ht)?ml/i));
62
63         this.update();
64     },
65
66     get isXMLMimeType()
67     {
68         return this._isXMLMimeType;
69     },
70
71     nodeNameToCorrectCase: function(nodeName)
72     {
73         return this.isXMLMimeType ? nodeName : nodeName.toLowerCase();
74     },
75
76     get focusedDOMNode()
77     {
78         return this._focusedDOMNode;
79     },
80
81     set focusedDOMNode(x)
82     {
83         if (this._focusedDOMNode === x) {
84             this.revealAndSelectNode(x);
85             return;
86         }
87
88         this._focusedDOMNode = x;
89
90         this.revealAndSelectNode(x);
91
92         // The revealAndSelectNode() method might find a different element if there is inlined text,
93         // and the select() call would change the focusedDOMNode and reenter this setter. So to
94         // avoid calling focusedNodeChanged() twice, first check if _focusedDOMNode is the same
95         // node as the one passed in.
96         if (this._focusedDOMNode === x) {
97             this.focusedNodeChanged();
98
99             if (x && !this.suppressSelectHighlight) {
100                 InspectorBackend.highlightDOMNode(x.id);
101
102                 if ("_restorePreviousHighlightNodeTimeout" in this)
103                     clearTimeout(this._restorePreviousHighlightNodeTimeout);
104
105                 function restoreHighlightToHoveredNode()
106                 {
107                     var hoveredNode = WebInspector.hoveredDOMNode;
108                     if (hoveredNode)
109                         InspectorBackend.highlightDOMNode(hoveredNode.id);
110                     else
111                         InspectorBackend.hideDOMNodeHighlight();
112                 }
113
114                 this._restorePreviousHighlightNodeTimeout = setTimeout(restoreHighlightToHoveredNode, 2000);
115             }
116         }
117     },
118
119     get editing()
120     {
121         return this._editing;
122     },
123
124     update: function()
125     {
126         var selectedNode = this.selectedTreeElement ? this.selectedTreeElement.representedObject : null;
127
128         this.removeChildren();
129
130         if (!this.rootDOMNode)
131             return;
132
133         var treeElement;
134         if (this.includeRootDOMNode) {
135             treeElement = new WebInspector.ElementsTreeElement(this.rootDOMNode);
136             treeElement.selectable = this.selectEnabled;
137             this.appendChild(treeElement);
138         } else {
139             // FIXME: this could use findTreeElement to reuse a tree element if it already exists
140             var node = this.rootDOMNode.firstChild;
141             while (node) {
142                 treeElement = new WebInspector.ElementsTreeElement(node);
143                 treeElement.selectable = this.selectEnabled;
144                 this.appendChild(treeElement);
145                 node = node.nextSibling;
146             }
147         }
148
149         if (selectedNode)
150             this.revealAndSelectNode(selectedNode);
151     },
152
153     updateSelection: function()
154     {
155         if (!this.selectedTreeElement)
156             return;
157         var element = this.treeOutline.selectedTreeElement;
158         element.updateSelection();
159     },
160
161     focusedNodeChanged: function(forceUpdate) {},
162
163     findTreeElement: function(node)
164     {
165         var treeElement = TreeOutline.prototype.findTreeElement.call(this, node, isAncestorNode, parentNode);
166         if (!treeElement && node.nodeType === Node.TEXT_NODE) {
167             // The text node might have been inlined if it was short, so try to find the parent element.
168             treeElement = TreeOutline.prototype.findTreeElement.call(this, node.parentNode, isAncestorNode, parentNode);
169         }
170
171         return treeElement;
172     },
173
174     createTreeElementFor: function(node)
175     {
176         var treeElement = this.findTreeElement(node);
177         if (treeElement)
178             return treeElement;
179         if (!node.parentNode)
180             return null;
181
182         var treeElement = this.createTreeElementFor(node.parentNode);
183         if (treeElement && treeElement.showChild(node.index))
184             return treeElement.children[node.index];
185
186         return null;
187     },
188
189     set suppressRevealAndSelect(x)
190     {
191         if (this._suppressRevealAndSelect === x)
192             return;
193         this._suppressRevealAndSelect = x;
194     },
195
196     revealAndSelectNode: function(node)
197     {
198         if (!node || this._suppressRevealAndSelect)
199             return;
200
201         var treeElement = this.createTreeElementFor(node);
202         if (!treeElement)
203             return;
204
205         treeElement.reveal();
206         treeElement.select();
207     },
208
209     _treeElementFromEvent: function(event)
210     {
211         var root = this.element;
212
213         // We choose this X coordinate based on the knowledge that our list
214         // items extend nearly to the right edge of the outer <ol>.
215         var x = root.totalOffsetLeft + root.offsetWidth - 20;
216
217         var y = event.pageY;
218
219         // Our list items have 1-pixel cracks between them vertically. We avoid
220         // the cracks by checking slightly above and slightly below the mouse
221         // and seeing if we hit the same element each time.
222         var elementUnderMouse = this.treeElementFromPoint(x, y);
223         var elementAboveMouse = this.treeElementFromPoint(x, y - 2);
224         var element;
225         if (elementUnderMouse === elementAboveMouse)
226             element = elementUnderMouse;
227         else
228             element = this.treeElementFromPoint(x, y + 2);
229
230         return element;
231     },
232
233     _onmousedown: function(event)
234     {
235         var element = this._treeElementFromEvent(event);
236
237         if (!element || element.isEventWithinDisclosureTriangle(event))
238             return;
239
240         element.select();
241     },
242
243     _onmousemove: function(event)
244     {
245         var element = this._treeElementFromEvent(event);
246         if (element && this._previousHoveredElement === element)
247             return;
248
249         if (this._previousHoveredElement) {
250             this._previousHoveredElement.hovered = false;
251             delete this._previousHoveredElement;
252         }
253
254         if (element) {
255             element.hovered = true;
256             this._previousHoveredElement = element;
257
258             // Lazily compute tag-specific tooltips.
259             if (element.representedObject && !element.tooltip)
260                 element._createTooltipForNode();
261         }
262
263         WebInspector.hoveredDOMNode = (element ? element.representedObject : null);
264     },
265
266     _onmouseout: function(event)
267     {
268         var nodeUnderMouse = document.elementFromPoint(event.pageX, event.pageY);
269         if (nodeUnderMouse && nodeUnderMouse.isDescendant(this.element))
270             return;
271
272         if (this._previousHoveredElement) {
273             this._previousHoveredElement.hovered = false;
274             delete this._previousHoveredElement;
275         }
276
277         WebInspector.hoveredDOMNode = null;
278     },
279
280     _contextMenuEventFired: function(event)
281     {
282         var listItem = event.target.enclosingNodeOrSelfWithNodeName("LI");
283         if (!listItem || !listItem.treeElement)
284             return;
285
286         var contextMenu = new WebInspector.ContextMenu();
287
288         var tag = event.target.enclosingNodeOrSelfWithClass("webkit-html-tag");
289         var textNode = event.target.enclosingNodeOrSelfWithClass("webkit-html-text-node");
290         if (tag && listItem.treeElement._populateTagContextMenu)
291             listItem.treeElement._populateTagContextMenu(contextMenu, event);
292         else if (textNode && listItem.treeElement._populateTextContextMenu)
293             listItem.treeElement._populateTextContextMenu(contextMenu, textNode);
294         contextMenu.show(event);
295     }
296 }
297
298 WebInspector.ElementsTreeOutline.prototype.__proto__ = TreeOutline.prototype;
299
300 WebInspector.ElementsTreeElement = function(node, elementCloseTag)
301 {
302     this._elementCloseTag = elementCloseTag;
303     var hasChildrenOverride = !elementCloseTag && node.hasChildNodes() && !this._showInlineText(node);
304
305     // The title will be updated in onattach.
306     TreeElement.call(this, "", node, hasChildrenOverride);
307
308     if (this.representedObject.nodeType == Node.ELEMENT_NODE && !elementCloseTag)
309         this._canAddAttributes = true;
310     this._searchQuery = null;
311     this._expandedChildrenLimit = WebInspector.ElementsTreeElement.InitialChildrenLimit;
312 }
313
314 WebInspector.ElementsTreeElement.InitialChildrenLimit = 500;
315
316 // A union of HTML4 and HTML5-Draft elements that explicitly
317 // or implicitly (for HTML5) forbid the closing tag.
318 // FIXME: Revise once HTML5 Final is published.
319 WebInspector.ElementsTreeElement.ForbiddenClosingTagElements = [
320     "area", "base", "basefont", "br", "canvas", "col", "command", "embed", "frame",
321     "hr", "img", "input", "isindex", "keygen", "link", "meta", "param", "source"
322 ].keySet();
323
324 // These tags we do not allow editing their tag name.
325 WebInspector.ElementsTreeElement.EditTagBlacklist = [
326     "html", "head", "body"
327 ].keySet();
328
329 WebInspector.ElementsTreeElement.prototype = {
330     highlightSearchResults: function(searchQuery)
331     {
332         if (this._searchQuery === searchQuery)
333             return;
334
335         this._searchQuery = searchQuery;
336         this.updateTitle();
337     },
338
339     get hovered()
340     {
341         return this._hovered;
342     },
343
344     set hovered(x)
345     {
346         if (this._hovered === x)
347             return;
348
349         this._hovered = x;
350
351         if (this.listItemElement) {
352             if (x) {
353                 this.updateSelection();
354                 this.listItemElement.addStyleClass("hovered");
355             } else {
356                 this.listItemElement.removeStyleClass("hovered");
357             }
358         }
359     },
360
361     get expandedChildrenLimit()
362     {
363         return this._expandedChildrenLimit;
364     },
365
366     set expandedChildrenLimit(x)
367     {
368         if (this._expandedChildrenLimit === x)
369             return;
370
371         this._expandedChildrenLimit = x;
372         if (this.treeOutline && !this._updateChildrenInProgress)
373             this._updateChildren(true);
374     },
375
376     get expandedChildCount()
377     {
378         var count = this.children.length;
379         if (count && this.children[count - 1]._elementCloseTag)
380             count--;
381         if (count && this.children[count - 1].expandAllButton)
382             count--;
383         return count;
384     },
385
386     showChild: function(index)
387     {
388         if (this._elementCloseTag)
389             return;
390
391         if (index >= this.expandedChildrenLimit) {
392             this._expandedChildrenLimit = index + 1;
393             this._updateChildren(true);
394         }
395
396         // Whether index-th child is visible in the children tree
397         return this.expandedChildCount > index;
398     },
399
400     _createTooltipForNode: function()
401     {
402         var node = this.representedObject;
403         if (!node.nodeName || node.nodeName.toLowerCase() !== "img")
404             return;
405         
406         function setTooltip(properties)
407         {
408             if (!properties)
409                 return;
410
411             if (properties.offsetHeight === properties.naturalHeight && properties.offsetWidth === properties.naturalWidth)
412                 this.tooltip = WebInspector.UIString("%d × %d pixels", properties.offsetWidth, properties.offsetHeight);
413             else
414                 this.tooltip = WebInspector.UIString("%d × %d pixels (Natural: %d × %d pixels)", properties.offsetWidth, properties.offsetHeight, properties.naturalWidth, properties.naturalHeight);
415         }
416         var objectProxy = new WebInspector.ObjectProxy(node.injectedScriptId, node.id);
417         WebInspector.ObjectProxy.getPropertiesAsync(objectProxy, ["naturalHeight", "naturalWidth", "offsetHeight", "offsetWidth"], setTooltip.bind(this));
418     },
419
420     updateSelection: function()
421     {
422         var listItemElement = this.listItemElement;
423         if (!listItemElement)
424             return;
425
426         if (document.body.offsetWidth <= 0) {
427             // The stylesheet hasn't loaded yet or the window is closed,
428             // so we can't calculate what is need. Return early.
429             return;
430         }
431
432         if (!this.selectionElement) {
433             this.selectionElement = document.createElement("div");
434             this.selectionElement.className = "selection selected";
435             listItemElement.insertBefore(this.selectionElement, listItemElement.firstChild);
436         }
437
438         this.selectionElement.style.height = listItemElement.offsetHeight + "px";
439     },
440
441     onattach: function()
442     {
443         if (this._hovered) {
444             this.updateSelection();
445             this.listItemElement.addStyleClass("hovered");
446         }
447
448         this.updateTitle();
449
450         this._preventFollowingLinksOnDoubleClick();
451     },
452
453     _preventFollowingLinksOnDoubleClick: function()
454     {
455         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");
456         if (!links)
457             return;
458
459         for (var i = 0; i < links.length; ++i)
460             links[i].preventFollowOnDoubleClick = true;
461     },
462
463     onpopulate: function()
464     {
465         if (this.children.length || this._showInlineText(this.representedObject) || this._elementCloseTag)
466             return;
467
468         this.updateChildren();
469     },
470
471     updateChildren: function(fullRefresh)
472     {
473         if (this._elementCloseTag)
474             return;
475
476         WebInspector.domAgent.getChildNodesAsync(this.representedObject, this._updateChildren.bind(this, fullRefresh));
477     },
478
479     insertChildElement: function(child, index, closingTag)
480     {
481         var newElement = new WebInspector.ElementsTreeElement(child, closingTag);
482         newElement.selectable = this.treeOutline.selectEnabled;
483         this.insertChild(newElement, index);
484         return newElement;
485     },
486
487     moveChild: function(child, targetIndex)
488     {
489         var wasSelected = child.selected;
490         this.removeChild(child);
491         this.insertChild(child, targetIndex);
492         if (wasSelected)
493             child.select();
494     },
495
496     _updateChildren: function(fullRefresh)
497     {
498         if (this._updateChildrenInProgress)
499             return;
500
501         this._updateChildrenInProgress = true;
502         var focusedNode = this.treeOutline.focusedDOMNode;
503         var originalScrollTop;
504         if (fullRefresh) {
505             var treeOutlineContainerElement = this.treeOutline.element.parentNode;
506             originalScrollTop = treeOutlineContainerElement.scrollTop;
507             var selectedTreeElement = this.treeOutline.selectedTreeElement;
508             if (selectedTreeElement && selectedTreeElement.hasAncestor(this))
509                 this.select();
510             this.removeChildren();
511         }
512
513         var treeElement = this;
514         var treeChildIndex = 0;
515         var elementToSelect;
516
517         function updateChildrenOfNode(node)
518         {
519             var treeOutline = treeElement.treeOutline;
520             var child = node.firstChild;
521             while (child) {
522                 var currentTreeElement = treeElement.children[treeChildIndex];
523                 if (!currentTreeElement || currentTreeElement.representedObject !== child) {
524                     // Find any existing element that is later in the children list.
525                     var existingTreeElement = null;
526                     for (var i = (treeChildIndex + 1), size = treeElement.expandedChildCount; i < size; ++i) {
527                         if (treeElement.children[i].representedObject === child) {
528                             existingTreeElement = treeElement.children[i];
529                             break;
530                         }
531                     }
532
533                     if (existingTreeElement && existingTreeElement.parent === treeElement) {
534                         // If an existing element was found and it has the same parent, just move it.
535                         treeElement.moveChild(existingTreeElement, treeChildIndex);
536                     } else {
537                         // No existing element found, insert a new element.
538                         if (treeChildIndex < treeElement.expandedChildrenLimit) {
539                             var newElement = treeElement.insertChildElement(child, treeChildIndex);
540                             if (child === focusedNode)
541                                 elementToSelect = newElement;
542                             if (treeElement.expandedChildCount > treeElement.expandedChildrenLimit)
543                                 treeElement.expandedChildrenLimit++;
544                         }
545                     }
546                 }
547
548                 child = child.nextSibling;
549                 ++treeChildIndex;
550             }
551         }
552
553         // Remove any tree elements that no longer have this node (or this node's contentDocument) as their parent.
554         for (var i = (this.children.length - 1); i >= 0; --i) {
555             var currentChild = this.children[i];
556             var currentNode = currentChild.representedObject;
557             var currentParentNode = currentNode.parentNode;
558
559             if (currentParentNode === this.representedObject)
560                 continue;
561
562             var selectedTreeElement = this.treeOutline.selectedTreeElement;
563             if (selectedTreeElement && (selectedTreeElement === currentChild || selectedTreeElement.hasAncestor(currentChild)))
564                 this.select();
565
566             this.removeChildAtIndex(i);
567         }
568
569         updateChildrenOfNode(this.representedObject);
570         this.adjustCollapsedRange(false);
571
572         var lastChild = this.children[this.children.length - 1];
573         if (this.representedObject.nodeType == Node.ELEMENT_NODE && (!lastChild || !lastChild._elementCloseTag))
574             this.insertChildElement(this.representedObject, this.children.length, true);
575
576         // We want to restore the original selection and tree scroll position after a full refresh, if possible.
577         if (fullRefresh && elementToSelect) {
578             elementToSelect.select();
579             if (treeOutlineContainerElement && originalScrollTop <= treeOutlineContainerElement.scrollHeight)
580                 treeOutlineContainerElement.scrollTop = originalScrollTop;
581         }
582
583         delete this._updateChildrenInProgress;
584     },
585
586     adjustCollapsedRange: function()
587     {
588         // Ensure precondition: only the tree elements for node children are found in the tree
589         // (not the Expand All button or the closing tag).
590         if (this.expandAllButtonElement && this.expandAllButtonElement.__treeElement.parent)
591             this.removeChild(this.expandAllButtonElement.__treeElement);
592
593         const node = this.representedObject;
594         if (!node.children)
595             return;
596         const childNodeCount = node.children.length;
597
598         // In case some nodes from the expanded range were removed, pull some nodes from the collapsed range into the expanded range at the bottom.
599         for (var i = this.expandedChildCount, limit = Math.min(this.expandedChildrenLimit, childNodeCount); i < limit; ++i)
600             this.insertChildElement(node.children[i], i);
601
602         const expandedChildCount = this.expandedChildCount;
603         if (childNodeCount > this.expandedChildCount) {
604             var targetButtonIndex = expandedChildCount;
605             if (!this.expandAllButtonElement) {
606                 var title = "<button class=\"show-all-nodes\" value=\"\" />";
607                 var item = new TreeElement(title, null, false);
608                 item.selectable = false;
609                 item.expandAllButton = true;
610                 this.insertChild(item, targetButtonIndex);
611                 this.expandAllButtonElement = item.listItemElement.firstChild;
612                 this.expandAllButtonElement.__treeElement = item;
613                 this.expandAllButtonElement.addEventListener("click", this.handleLoadAllChildren.bind(this), false);
614             } else if (!this.expandAllButtonElement.__treeElement.parent)
615                 this.insertChild(this.expandAllButtonElement.__treeElement, targetButtonIndex);
616             this.expandAllButtonElement.textContent = WebInspector.UIString("Show All Nodes (%d More)", childNodeCount - expandedChildCount);
617         } else if (this.expandAllButtonElement)
618             delete this.expandAllButtonElement;
619     },
620
621     handleLoadAllChildren: function()
622     {
623         this.expandedChildrenLimit = Math.max(this.representedObject._childNodeCount, this.expandedChildrenLimit + WebInspector.ElementsTreeElement.InitialChildrenLimit);
624     },
625
626     onexpand: function()
627     {
628         if (this._elementCloseTag)
629             return;
630
631         this.updateTitle();
632         this.treeOutline.updateSelection();
633     },
634
635     oncollapse: function()
636     {
637         if (this._elementCloseTag)
638             return;
639
640         this.updateTitle();
641         this.treeOutline.updateSelection();
642     },
643
644     onreveal: function()
645     {
646         if (this.listItemElement)
647             this.listItemElement.scrollIntoViewIfNeeded(false);
648     },
649
650     onselect: function()
651     {
652         this.treeOutline.suppressRevealAndSelect = true;
653         this.treeOutline.focusedDOMNode = this.representedObject;
654         this.updateSelection();
655         this.treeOutline.suppressRevealAndSelect = false;
656     },
657
658     ondelete: function()
659     {
660         var startTagTreeElement = this.treeOutline.findTreeElement(this.representedObject);
661         startTagTreeElement ? startTagTreeElement.remove() : this.remove();
662         return true;
663     },
664
665     onenter: function()
666     {
667         // On Enter or Return start editing the first attribute
668         // or create a new attribute on the selected element.
669         if (this.treeOutline.editing)
670             return false;
671
672         this._startEditing();
673
674         // prevent a newline from being immediately inserted
675         return true;
676     },
677
678     selectOnMouseDown: function(event)
679     {
680         TreeElement.prototype.selectOnMouseDown.call(this, event);
681
682         if (this._editing)
683             return;
684
685         if (this.treeOutline.showInElementsPanelEnabled) {
686             WebInspector.showElementsPanel();
687             WebInspector.panels.elements.focusedDOMNode = this.representedObject;
688         }
689
690         // Prevent selecting the nearest word on double click.
691         if (event.detail >= 2)
692             event.preventDefault();
693     },
694
695     ondblclick: function(event)
696     {
697         if (this._editing || this._elementCloseTag)
698             return;
699
700         if (this._startEditingTarget(event.target))
701             return;
702
703         if (this.hasChildren && !this.expanded)
704             this.expand();
705     },
706
707     _insertInLastAttributePosition: function(tag, node)
708     {
709         if (tag.getElementsByClassName("webkit-html-attribute").length > 0)
710             tag.insertBefore(node, tag.lastChild);
711         else {
712             var nodeName = tag.textContent.match(/^<(.*?)>$/)[1];
713             tag.textContent = '';
714             tag.appendChild(document.createTextNode('<'+nodeName));
715             tag.appendChild(node);
716             tag.appendChild(document.createTextNode('>'));
717         }
718
719         this.updateSelection();
720     },
721
722     _startEditingTarget: function(eventTarget)
723     {
724         if (this.treeOutline.focusedDOMNode != this.representedObject)
725             return;
726
727         if (this.representedObject.nodeType != Node.ELEMENT_NODE && this.representedObject.nodeType != Node.TEXT_NODE)
728             return false;
729
730         var textNode = eventTarget.enclosingNodeOrSelfWithClass("webkit-html-text-node");
731         if (textNode)
732             return this._startEditingTextNode(textNode);
733
734         var attribute = eventTarget.enclosingNodeOrSelfWithClass("webkit-html-attribute");
735         if (attribute)
736             return this._startEditingAttribute(attribute, eventTarget);
737
738         var tagName = eventTarget.enclosingNodeOrSelfWithClass("webkit-html-tag-name");
739         if (tagName)
740             return this._startEditingTagName(tagName);
741
742         var newAttribute = eventTarget.enclosingNodeOrSelfWithClass("add-attribute");
743         if (newAttribute)
744             return this._addNewAttribute();
745
746         return false;
747     },
748
749     _populateTagContextMenu: function(contextMenu, event)
750     {
751         var attribute = event.target.enclosingNodeOrSelfWithClass("webkit-html-attribute");
752         var newAttribute = event.target.enclosingNodeOrSelfWithClass("add-attribute");
753
754         // Add attribute-related actions.
755         contextMenu.appendItem(WebInspector.UIString("Add Attribute"), this._addNewAttribute.bind(this));
756         if (attribute && !newAttribute)
757             contextMenu.appendItem(WebInspector.UIString("Edit Attribute"), this._startEditingAttribute.bind(this, attribute, event.target));
758         contextMenu.appendSeparator();
759
760         // Add free-form node-related actions.
761         contextMenu.appendItem(WebInspector.UIString("Edit as HTML"), this._editAsHTML.bind(this));
762         contextMenu.appendItem(WebInspector.UIString("Copy as HTML"), this._copyHTML.bind(this));
763         contextMenu.appendItem(WebInspector.UIString("Delete Node"), this.remove.bind(this));
764     },
765
766     _populateTextContextMenu: function(contextMenu, textNode)
767     {
768         contextMenu.appendItem(WebInspector.UIString("Edit Text"), this._startEditingTextNode.bind(this, textNode));
769     },
770
771     _startEditing: function()
772     {
773         if (this.treeOutline.focusedDOMNode !== this.representedObject)
774             return;
775
776         var listItem = this._listItemNode;
777
778         if (this._canAddAttributes) {
779             var attribute = listItem.getElementsByClassName("webkit-html-attribute")[0];
780             if (attribute)
781                 return this._startEditingAttribute(attribute, attribute.getElementsByClassName("webkit-html-attribute-value")[0]);
782
783             return this._addNewAttribute();
784         }
785
786         if (this.representedObject.nodeType === Node.TEXT_NODE) {
787             var textNode = listItem.getElementsByClassName("webkit-html-text-node")[0];
788             if (textNode)
789                 return this._startEditingTextNode(textNode);
790             return;
791         }
792     },
793
794     _addNewAttribute: function()
795     {
796         // Cannot just convert the textual html into an element without
797         // a parent node. Use a temporary span container for the HTML.
798         var container = document.createElement("span");
799         container.innerHTML = this._attributeHTML(" ", "");
800         var attr = container.firstChild;
801         attr.style.marginLeft = "2px"; // overrides the .editing margin rule
802         attr.style.marginRight = "2px"; // overrides the .editing margin rule
803
804         var tag = this.listItemElement.getElementsByClassName("webkit-html-tag")[0];
805         this._insertInLastAttributePosition(tag, attr);
806         return this._startEditingAttribute(attr, attr);
807     },
808
809     _triggerEditAttribute: function(attributeName)
810     {
811         var attributeElements = this.listItemElement.getElementsByClassName("webkit-html-attribute-name");
812         for (var i = 0, len = attributeElements.length; i < len; ++i) {
813             if (attributeElements[i].textContent === attributeName) {
814                 for (var elem = attributeElements[i].nextSibling; elem; elem = elem.nextSibling) {
815                     if (elem.nodeType !== Node.ELEMENT_NODE)
816                         continue;
817
818                     if (elem.hasStyleClass("webkit-html-attribute-value"))
819                         return this._startEditingAttribute(elem.parentNode, elem);
820                 }
821             }
822         }
823     },
824
825     _startEditingAttribute: function(attribute, elementForSelection)
826     {
827         if (WebInspector.isBeingEdited(attribute))
828             return true;
829
830         var attributeNameElement = attribute.getElementsByClassName("webkit-html-attribute-name")[0];
831         if (!attributeNameElement)
832             return false;
833
834         var attributeName = attributeNameElement.innerText;
835
836         function removeZeroWidthSpaceRecursive(node)
837         {
838             if (node.nodeType === Node.TEXT_NODE) {
839                 node.nodeValue = node.nodeValue.replace(/\u200B/g, "");
840                 return;
841             }
842
843             if (node.nodeType !== Node.ELEMENT_NODE)
844                 return;
845
846             for (var child = node.firstChild; child; child = child.nextSibling)
847                 removeZeroWidthSpaceRecursive(child);
848         }
849
850         // Remove zero-width spaces that were added by nodeTitleInfo.
851         removeZeroWidthSpaceRecursive(attribute);
852
853         this._editing = WebInspector.startEditing(attribute, this._attributeEditingCommitted.bind(this), this._editingCancelled.bind(this), attributeName);
854         window.getSelection().setBaseAndExtent(elementForSelection, 0, elementForSelection, 1);
855
856         return true;
857     },
858
859     _startEditingTextNode: function(textNode)
860     {
861         if (WebInspector.isBeingEdited(textNode))
862             return true;
863
864         this._editing = WebInspector.startEditing(textNode, this._textNodeEditingCommitted.bind(this), this._editingCancelled.bind(this));
865         window.getSelection().setBaseAndExtent(textNode, 0, textNode, 1);
866
867         return true;
868     },
869
870     _startEditingTagName: function(tagNameElement)
871     {
872         if (!tagNameElement) {
873             tagNameElement = this.listItemElement.getElementsByClassName("webkit-html-tag-name")[0];
874             if (!tagNameElement)
875                 return false;
876         }
877
878         var tagName = tagNameElement.textContent;
879         if (WebInspector.ElementsTreeElement.EditTagBlacklist[tagName.toLowerCase()])
880             return false;
881
882         if (WebInspector.isBeingEdited(tagNameElement))
883             return true;
884
885         var closingTagElement = this._distinctClosingTagElement();
886
887         function keyupListener(event)
888         {
889             if (closingTagElement)
890                 closingTagElement.textContent = "</" + tagNameElement.textContent + ">";
891         }
892
893         function editingComitted(element, newTagName)
894         {
895             tagNameElement.removeEventListener('keyup', keyupListener, false);
896             this._tagNameEditingCommitted.apply(this, arguments);
897         }
898
899         function editingCancelled()
900         {
901             tagNameElement.removeEventListener('keyup', keyupListener, false);
902             this._editingCancelled.apply(this, arguments);
903         }
904
905         tagNameElement.addEventListener('keyup', keyupListener, false);
906
907         this._editing = WebInspector.startEditing(tagNameElement, editingComitted.bind(this), editingCancelled.bind(this), tagName);
908         window.getSelection().setBaseAndExtent(tagNameElement, 0, tagNameElement, 1);
909         return true;
910     },
911
912     _startEditingAsHTML: function(commitCallback, initialValue)
913     {
914         if (this._htmlEditElement && WebInspector.isBeingEdited(this._htmlEditElement))
915             return true;
916
917         this._htmlEditElement = document.createElement("div");
918         this._htmlEditElement.className = "source-code elements-tree-editor";
919         this._htmlEditElement.textContent = initialValue;
920
921         // Hide header items.
922         var child = this.listItemElement.firstChild;
923         while (child) {
924             child.style.display = "none";
925             child = child.nextSibling;
926         }
927         // Hide children item.
928         if (this._childrenListNode)
929             this._childrenListNode.style.display = "none";
930         // Append editor.
931         this.listItemElement.appendChild(this._htmlEditElement);
932
933         this.updateSelection();
934
935         function commit()
936         {
937             commitCallback(this._htmlEditElement.textContent);
938             dispose.call(this);
939         }
940
941         function dispose()
942         {
943             delete this._editing;
944
945             // Remove editor.
946             this.listItemElement.removeChild(this._htmlEditElement);
947             delete this._htmlEditElement;
948             // Unhide children item.
949             if (this._childrenListNode)
950                 this._childrenListNode.style.removeProperty("display");
951             // Unhide header items.
952             var child = this.listItemElement.firstChild;
953             while (child) {
954                 child.style.removeProperty("display");
955                 child = child.nextSibling;
956             }
957
958             this.updateSelection();
959         }
960
961         this._editing = WebInspector.startEditing(this._htmlEditElement, commit.bind(this), dispose.bind(this), null, true);
962     },
963
964     _attributeEditingCommitted: function(element, newText, oldText, attributeName, moveDirection)
965     {
966         delete this._editing;
967
968         // Before we do anything, determine where we should move
969         // next based on the current element's settings
970         var moveToAttribute, moveToTagName, moveToNewAttribute;
971         if (moveDirection) {
972             var found = false;
973
974             // Search for the attribute's position, and then decide where to move to.
975             var attributes = this.representedObject.attributes;
976             for (var i = 0; i < attributes.length; ++i) {
977                 if (attributes[i].name === attributeName) {
978                     found = true;
979                     if (moveDirection === "backward") {
980                         if (i === 0)
981                             moveToTagName = true;
982                         else
983                             moveToAttribute = attributes[i - 1].name;
984                     } else if (moveDirection === "forward") {
985                         if (i === attributes.length - 1)
986                             moveToNewAttribute = true;
987                         else
988                             moveToAttribute = attributes[i + 1].name;
989                     }
990                 }
991             }
992
993             // Moving From the "New Attribute" position.
994             if (!found) {
995                 if (moveDirection === "backward" && attributes.length > 0)
996                     moveToAttribute = attributes[attributes.length - 1].name;
997                 else if (moveDirection === "forward" && !/^\s*$/.test(newText))
998                     moveToNewAttribute = true;
999             }
1000         }
1001
1002         function moveToNextAttributeIfNeeded()
1003         {
1004             // Cleanup empty new attribute sections.
1005             if (element.textContent.trim().length === 0)
1006                 element.parentNode.removeChild(element);
1007
1008             // Make the move.
1009             if (moveToAttribute)
1010                 this._triggerEditAttribute(moveToAttribute);
1011             else if (moveToNewAttribute)
1012                 this._addNewAttribute();
1013             else if (moveToTagName)
1014                 this._startEditingTagName();
1015         }
1016
1017         function regenerateStyledAttribute(name, value)
1018         {
1019             var previous = element.previousSibling;
1020             if (!previous || previous.nodeType !== Node.TEXT_NODE)
1021                 element.parentNode.insertBefore(document.createTextNode(" "), element);
1022             element.outerHTML = this._attributeHTML(name, value);
1023         }
1024
1025         var parseContainerElement = document.createElement("span");
1026         parseContainerElement.innerHTML = "<span " + newText + "></span>";
1027         var parseElement = parseContainerElement.firstChild;
1028
1029         if (!parseElement) {
1030             this._editingCancelled(element, attributeName);
1031             moveToNextAttributeIfNeeded.call(this);
1032             return;
1033         }
1034
1035         if (!parseElement.hasAttributes()) {
1036             this.representedObject.removeAttribute(attributeName);
1037             moveToNextAttributeIfNeeded.call(this);
1038             return;
1039         }
1040
1041         var foundOriginalAttribute = false;
1042         for (var i = 0; i < parseElement.attributes.length; ++i) {
1043             var attr = parseElement.attributes[i];
1044             foundOriginalAttribute = foundOriginalAttribute || attr.name === attributeName;
1045             try {
1046                 this.representedObject.setAttribute(attr.name, attr.value);
1047                 regenerateStyledAttribute.call(this, attr.name, attr.value);
1048             } catch(e) {} // ignore invalid attribute (innerHTML doesn't throw errors, but this can)
1049         }
1050
1051         if (!foundOriginalAttribute)
1052             this.representedObject.removeAttribute(attributeName);
1053
1054         this.treeOutline.focusedNodeChanged(true);
1055
1056         moveToNextAttributeIfNeeded.call(this);
1057     },
1058
1059     _tagNameEditingCommitted: function(element, newText, oldText, tagName, moveDirection)
1060     {
1061         delete this._editing;
1062         var self = this;
1063
1064         function cancel()
1065         {
1066             var closingTagElement = self._distinctClosingTagElement();
1067             if (closingTagElement)
1068                 closingTagElement.textContent = "</" + tagName + ">";
1069
1070             self._editingCancelled(element, tagName);
1071             moveToNextAttributeIfNeeded.call(self);
1072         }
1073
1074         function moveToNextAttributeIfNeeded()
1075         {
1076             if (moveDirection !== "forward")
1077                 return;
1078
1079             var attributes = this.representedObject.attributes;
1080             if (attributes.length > 0)
1081                 this._triggerEditAttribute(attributes[0].name);
1082             else
1083                 this._addNewAttribute();
1084         }
1085
1086         newText = newText.trim();
1087         if (newText === oldText) {
1088             cancel();
1089             return;
1090         }
1091
1092         var treeOutline = this.treeOutline;
1093         var wasExpanded = this.expanded;
1094
1095         function changeTagNameCallback(nodeId)
1096         {
1097             if (nodeId === -1) {
1098                 cancel();
1099                 return;
1100             }
1101
1102             // Select it and expand if necessary. We force tree update so that it processes dom events and is up to date.
1103             WebInspector.panels.elements.updateModifiedNodes();
1104
1105             WebInspector.updateFocusedNode(nodeId);
1106             var newTreeItem = treeOutline.findTreeElement(WebInspector.domAgent.nodeForId(nodeId));
1107             if (wasExpanded)
1108                 newTreeItem.expand();
1109
1110             moveToNextAttributeIfNeeded.call(newTreeItem);
1111         }
1112
1113         var callId = WebInspector.Callback.wrap(changeTagNameCallback);
1114         InspectorBackend.changeTagName(callId, this.representedObject.id, newText, wasExpanded);
1115     },
1116
1117     _textNodeEditingCommitted: function(element, newText)
1118     {
1119         delete this._editing;
1120
1121         var textNode;
1122         if (this.representedObject.nodeType == Node.ELEMENT_NODE) {
1123             // We only show text nodes inline in elements if the element only
1124             // has a single child, and that child is a text node.
1125             textNode = this.representedObject.firstChild;
1126         } else if (this.representedObject.nodeType == Node.TEXT_NODE)
1127             textNode = this.representedObject;
1128
1129         textNode.nodeValue = newText;
1130
1131         // Need to restore attributes / node structure.
1132         this.updateTitle();
1133     },
1134
1135     _editingCancelled: function(element, context)
1136     {
1137         delete this._editing;
1138
1139         // Need to restore attributes structure.
1140         this.updateTitle();
1141     },
1142
1143     _distinctClosingTagElement: function()
1144     {
1145         // FIXME: Improve the Tree Element / Outline Abstraction to prevent crawling the DOM
1146
1147         // For an expanded element, it will be the last element with class "close"
1148         // in the child element list.
1149         if (this.expanded) {
1150             var closers = this._childrenListNode.querySelectorAll(".close");
1151             return closers[closers.length-1];
1152         }
1153
1154         // Remaining cases are single line non-expanded elements with a closing
1155         // tag, or HTML elements without a closing tag (such as <br>). Return
1156         // null in the case where there isn't a closing tag.
1157         var tags = this.listItemElement.getElementsByClassName("webkit-html-tag");
1158         return (tags.length === 1 ? null : tags[tags.length-1]);
1159     },
1160
1161     updateTitle: function()
1162     {
1163         // If we are editing, return early to prevent canceling the edit.
1164         // After editing is committed updateTitle will be called.
1165         if (this._editing)
1166             return;
1167
1168         var title = this._nodeTitleInfo(WebInspector.linkifyURL).title;
1169         this.title = "<span class=\"highlight\">" + title + "</span>";
1170         delete this.selectionElement;
1171         this.updateSelection();
1172         this._preventFollowingLinksOnDoubleClick();
1173         this._highlightSearchResults();
1174     },
1175
1176     _rewriteAttrHref: function(node, hrefValue)
1177     {
1178         if (!hrefValue || hrefValue.indexOf("://") > 0)
1179             return hrefValue;
1180
1181         for (var frameOwnerCandidate = node; frameOwnerCandidate; frameOwnerCandidate = frameOwnerCandidate.parentNode) {
1182             if (frameOwnerCandidate.documentURL) {
1183                 var result = WebInspector.completeURL(frameOwnerCandidate.documentURL, hrefValue);
1184                 if (result)
1185                     return result;
1186                 break;
1187             }
1188         }
1189
1190         // documentURL not found or has bad value
1191         for (var url in WebInspector.resourceURLMap) {
1192             var match = url.match(WebInspector.URLRegExp);
1193             if (match && match[4] === hrefValue)
1194                 return url;
1195         }
1196         return hrefValue;
1197     },
1198
1199     _attributeHTML: function(name, value, node, linkify)
1200     {
1201         var hasText = (value.length > 0);
1202         var html = "<span class=\"webkit-html-attribute\"><span class=\"webkit-html-attribute-name\">" + name.escapeHTML() + "</span>";
1203
1204         if (hasText)
1205             html += "=&#8203;\"";
1206
1207         if (linkify && (name === "src" || name === "href")) {
1208             value = value.replace(/([\/;:\)\]\}])/g, "$1\u200B");
1209             html += linkify(this._rewriteAttrHref(node, value), value, "webkit-html-attribute-value", node.nodeName.toLowerCase() === "a");
1210         } else {
1211             value = value.escapeHTML().replace(/([\/;:\)\]\}])/g, "$1&#8203;");
1212             html += "<span class=\"webkit-html-attribute-value\">" + value + "</span>";
1213         }
1214
1215         if (hasText)
1216             html += "\"";
1217
1218         html += "</span>";
1219         return html;
1220     },
1221
1222     _tagHTML: function(tagName, isClosingTag, isDistinctTreeElement, linkify)
1223     {
1224         var node = this.representedObject;
1225         var result = "<span class=\"webkit-html-tag" + (isClosingTag && isDistinctTreeElement ? " close" : "")  + "\">&lt;";
1226         result += "<span " + (isClosingTag ? "" : "class=\"webkit-html-tag-name\"") + ">" + (isClosingTag ? "/" : "") + tagName + "</span>";
1227         if (!isClosingTag && node.hasAttributes()) {
1228             for (var i = 0; i < node.attributes.length; ++i) {
1229                 var attr = node.attributes[i];
1230                 result += " " + this._attributeHTML(attr.name, attr.value, node, linkify);
1231             }
1232         }
1233         result += "&gt;</span>&#8203;";
1234
1235         return result;
1236     },
1237
1238     _nodeTitleInfo: function(linkify)
1239     {
1240         var node = this.representedObject;
1241         var info = {title: "", hasChildren: this.hasChildren};
1242
1243         switch (node.nodeType) {
1244             case Node.DOCUMENT_NODE:
1245                 info.title = "Document";
1246                 break;
1247
1248             case Node.DOCUMENT_FRAGMENT_NODE:
1249                 info.title = "Document Fragment";
1250                 break;
1251
1252             case Node.ELEMENT_NODE:
1253                 var tagName = this.treeOutline.nodeNameToCorrectCase(node.nodeName).escapeHTML();
1254                 if (this._elementCloseTag) {
1255                     info.title = this._tagHTML(tagName, true, true);
1256                     info.hasChildren = false;
1257                     break;
1258                 }
1259
1260                 info.title = this._tagHTML(tagName, false, false, linkify);
1261
1262                 var textChild = onlyTextChild.call(node);
1263                 var showInlineText = textChild && textChild.textContent.length < Preferences.maxInlineTextChildLength;
1264
1265                 if (!this.expanded && (!showInlineText && (this.treeOutline.isXMLMimeType || !WebInspector.ElementsTreeElement.ForbiddenClosingTagElements[tagName]))) {
1266                     if (this.hasChildren)
1267                         info.title += "<span class=\"webkit-html-text-node\">&#8230;</span>&#8203;";
1268                     info.title += this._tagHTML(tagName, true, false);
1269                 }
1270
1271                 // If this element only has a single child that is a text node,
1272                 // just show that text and the closing tag inline rather than
1273                 // create a subtree for them
1274                 if (showInlineText) {
1275                     info.title += "<span class=\"webkit-html-text-node\">" + textChild.nodeValue.escapeHTML() + "</span>&#8203;" + this._tagHTML(tagName, true, false);
1276                     info.hasChildren = false;
1277                 }
1278                 break;
1279
1280             case Node.TEXT_NODE:
1281                 if (isNodeWhitespace.call(node))
1282                     info.title = "(whitespace)";
1283                 else {
1284                     if (node.parentNode && node.parentNode.nodeName.toLowerCase() === "script") {
1285                         var newNode = document.createElement("span");
1286                         newNode.textContent = node.textContent;
1287
1288                         var javascriptSyntaxHighlighter = new WebInspector.DOMSyntaxHighlighter("text/javascript");
1289                         javascriptSyntaxHighlighter.syntaxHighlightNode(newNode);
1290
1291                         info.title = "<span class=\"webkit-html-text-node webkit-html-js-node\">" + newNode.innerHTML.replace(/^[\n\r]*/, "").replace(/\s*$/, "") + "</span>";
1292                     } else if (node.parentNode && node.parentNode.nodeName.toLowerCase() === "style") {
1293                         var newNode = document.createElement("span");
1294                         newNode.textContent = node.textContent;
1295
1296                         var cssSyntaxHighlighter = new WebInspector.DOMSyntaxHighlighter("text/css");
1297                         cssSyntaxHighlighter.syntaxHighlightNode(newNode);
1298
1299                         info.title = "<span class=\"webkit-html-text-node webkit-html-css-node\">" + newNode.innerHTML.replace(/^[\n\r]*/, "").replace(/\s*$/, "") + "</span>";
1300                     } else {
1301                         info.title = "\"<span class=\"webkit-html-text-node\">" + node.nodeValue.escapeHTML() + "</span>\"";
1302                     }
1303                 }
1304                 break;
1305
1306             case Node.COMMENT_NODE:
1307                 info.title = "<span class=\"webkit-html-comment\">&lt;!--" + node.nodeValue.escapeHTML() + "--&gt;</span>";
1308                 break;
1309
1310             case Node.DOCUMENT_TYPE_NODE:
1311                 info.title = "<span class=\"webkit-html-doctype\">&lt;!DOCTYPE " + node.nodeName;
1312                 if (node.publicId) {
1313                     info.title += " PUBLIC \"" + node.publicId + "\"";
1314                     if (node.systemId)
1315                         info.title += " \"" + node.systemId + "\"";
1316                 } else if (node.systemId)
1317                     info.title += " SYSTEM \"" + node.systemId + "\"";
1318                 if (node.internalSubset)
1319                     info.title += " [" + node.internalSubset + "]";
1320                 info.title += "&gt;</span>";
1321                 break;
1322             default:
1323                 info.title = this.treeOutline.nodeNameToCorrectCase(node.nodeName).collapseWhitespace().escapeHTML();
1324         }
1325
1326         return info;
1327     },
1328
1329     _showInlineText: function(node)
1330     {
1331         if (node.nodeType === Node.ELEMENT_NODE) {
1332             var textChild = onlyTextChild.call(node);
1333             if (textChild && textChild.textContent.length < Preferences.maxInlineTextChildLength)
1334                 return true;
1335         }
1336         return false;
1337     },
1338
1339     remove: function()
1340     {
1341         var parentElement = this.parent;
1342         if (!parentElement)
1343             return;
1344
1345         var self = this;
1346         function removeNodeCallback(removedNodeId)
1347         {
1348             // -1 is an error code, which means removing the node from the DOM failed,
1349             // so we shouldn't remove it from the tree.
1350             if (removedNodeId === -1)
1351                 return;
1352
1353             parentElement.removeChild(self);
1354             parentElement.adjustCollapsedRange(true);
1355         }
1356
1357         var callId = WebInspector.Callback.wrap(removeNodeCallback);
1358         InspectorBackend.removeNode(callId, this.representedObject.id);
1359     },
1360
1361     _editAsHTML: function()
1362     {
1363         var treeOutline = this.treeOutline;
1364         var node = this.representedObject;
1365         var wasExpanded = this.expanded;
1366
1367         function selectNode(nodeId)
1368         {
1369             if (!nodeId)
1370                 return;
1371
1372             // Select it and expand if necessary. We force tree update so that it processes dom events and is up to date.
1373             WebInspector.panels.elements.updateModifiedNodes();
1374
1375             WebInspector.updateFocusedNode(nodeId);
1376             if (wasExpanded) {
1377                 var newTreeItem = treeOutline.findTreeElement(WebInspector.domAgent.nodeForId(nodeId));
1378                 if (newTreeItem)
1379                     newTreeItem.expand();
1380             }
1381         }
1382
1383         function commitChange(value)
1384         {
1385             InjectedScriptAccess.get(node.injectedScriptId).setOuterHTML(node.id, value, wasExpanded, selectNode.bind(this));
1386         }
1387
1388         InjectedScriptAccess.get(node.injectedScriptId).getNodePropertyValue(node.id, "outerHTML", this._startEditingAsHTML.bind(this, commitChange));
1389     },
1390
1391     _copyHTML: function()
1392     {
1393         InspectorBackend.copyNode(this.representedObject.id);
1394     },
1395
1396     _highlightSearchResults: function()
1397     {
1398         if (!this._searchQuery)
1399             return;
1400         var text = this.listItemElement.textContent;
1401         var regexObject = createSearchRegex(this._searchQuery);
1402
1403         var offset = 0;
1404         var match = regexObject.exec(text);
1405         while (match) {
1406             highlightSearchResult(this.listItemElement, offset + match.index, match[0].length);
1407             offset += match.index + 1;
1408             text = text.substring(match.index + 1);
1409             match = regexObject.exec(text);
1410         }
1411     }
1412 }
1413
1414 WebInspector.ElementsTreeElement.prototype.__proto__ = TreeElement.prototype;