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