1/*
2 * Copyright (C) 2007, 2008, 2013 Apple Inc.  All rights reserved.
3 * Copyright (C) 2008 Matt Lilek <webkit@mattlilek.com>
4 * Copyright (C) 2009 Joseph Pecoraro
5 *
6 * Redistribution and use in source and binary forms, with or without
7 * modification, are permitted provided that the following conditions
8 * are met:
9 *
10 * 1.  Redistributions of source code must retain the above copyright
11 *     notice, this list of conditions and the following disclaimer.
12 * 2.  Redistributions in binary form must reproduce the above copyright
13 *     notice, this list of conditions and the following disclaimer in the
14 *     documentation and/or other materials provided with the distribution.
15 * 3.  Neither the name of Apple Inc. ("Apple") nor the names of
16 *     its contributors may be used to endorse or promote products derived
17 *     from this software without specific prior written permission.
18 *
19 * THIS SOFTWARE IS PROVIDED BY APPLE AND ITS CONTRIBUTORS "AS IS" AND ANY
20 * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
21 * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
22 * DISCLAIMED. IN NO EVENT SHALL APPLE OR ITS CONTRIBUTORS BE LIABLE FOR ANY
23 * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
24 * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
25 * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
26 * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
27 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF
28 * THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
29 */
30
31WebInspector.DOMTreeOutline = function(omitRootDOMNode, selectEnabled, showInElementsPanelEnabled)
32{
33    this.element = document.createElement("ol");
34    this.element.addEventListener("mousedown", this._onmousedown.bind(this), false);
35    this.element.addEventListener("mousemove", this._onmousemove.bind(this), false);
36    this.element.addEventListener("mouseout", this._onmouseout.bind(this), false);
37    this.element.addEventListener("dragstart", this._ondragstart.bind(this), false);
38    this.element.addEventListener("dragover", this._ondragover.bind(this), false);
39    this.element.addEventListener("dragleave", this._ondragleave.bind(this), false);
40    this.element.addEventListener("drop", this._ondrop.bind(this), false);
41    this.element.addEventListener("dragend", this._ondragend.bind(this), false);
42
43    this.element.classList.add(WebInspector.DOMTreeOutline.StyleClassName);
44    this.element.classList.add(WebInspector.SyntaxHighlightedStyleClassName);
45
46    TreeOutline.call(this, this.element);
47
48    this._includeRootDOMNode = !omitRootDOMNode;
49    this._selectEnabled = selectEnabled;
50    this._showInElementsPanelEnabled = showInElementsPanelEnabled;
51    this._rootDOMNode = null;
52    this._selectedDOMNode = null;
53    this._eventSupport = new WebInspector.Object();
54    this._editing = false;
55
56    this._visible = false;
57
58    this.element.addEventListener("contextmenu", this._contextMenuEventFired.bind(this), true);
59
60    this._hideElementKeyboardShortcut = new WebInspector.KeyboardShortcut(null, "H", this._hideElement.bind(this), this.element);
61    this._hideElementKeyboardShortcut.implicitlyPreventsDefault = false;
62
63    WebInspector.showShadowDOMSetting.addEventListener(WebInspector.Setting.Event.Changed, this._showShadowDOMSettingChanged, this);
64}
65
66WebInspector.Object.addConstructorFunctions(WebInspector.DOMTreeOutline);
67
68WebInspector.DOMTreeOutline.StyleClassName = "dom-tree-outline";
69
70WebInspector.DOMTreeOutline.Event = {
71    SelectedNodeChanged: "dom-tree-outline-selected-node-changed"
72}
73
74WebInspector.DOMTreeOutline.prototype = {
75    constructor: WebInspector.DOMTreeOutline,
76
77    wireToDomAgent: function()
78    {
79        this._elementsTreeUpdater = new WebInspector.DOMTreeUpdater(this);
80    },
81
82    close: function()
83    {
84        if (this._elementsTreeUpdater) {
85            this._elementsTreeUpdater.close();
86            this._elementsTreeUpdater = null;
87        }
88    },
89
90    setVisible: function(visible, omitFocus)
91    {
92        this._visible = visible;
93        if (!this._visible)
94            return;
95
96        this._updateModifiedNodes();
97        if (this._selectedDOMNode)
98            this._revealAndSelectNode(this._selectedDOMNode, omitFocus);
99    },
100
101    addEventListener: function(eventType, listener, thisObject)
102    {
103        this._eventSupport.addEventListener(eventType, listener, thisObject);
104    },
105
106    removeEventListener: function(eventType, listener, thisObject)
107    {
108        this._eventSupport.removeEventListener(eventType, listener, thisObject);
109    },
110
111    get rootDOMNode()
112    {
113        return this._rootDOMNode;
114    },
115
116    set rootDOMNode(x)
117    {
118        if (this._rootDOMNode === x)
119            return;
120
121        this._rootDOMNode = x;
122
123        this._isXMLMimeType = x && x.isXMLNode();
124
125        this.update();
126    },
127
128    get isXMLMimeType()
129    {
130        return this._isXMLMimeType;
131    },
132
133    selectedDOMNode: function()
134    {
135        return this._selectedDOMNode;
136    },
137
138    selectDOMNode: function(node, focus)
139    {
140        if (this._selectedDOMNode === node) {
141            this._revealAndSelectNode(node, !focus);
142            return;
143        }
144
145        this._selectedDOMNode = node;
146        this._revealAndSelectNode(node, !focus);
147
148        // The _revealAndSelectNode() method might find a different element if there is inlined text,
149        // and the select() call would change the selectedDOMNode and reenter this setter. So to
150        // avoid calling _selectedNodeChanged() twice, first check if _selectedDOMNode is the same
151        // node as the one passed in.
152        // Note that _revealAndSelectNode will not do anything for a null node.
153        if (!node || this._selectedDOMNode === node)
154            this._selectedNodeChanged();
155    },
156
157    get editing()
158    {
159        return this._editing;
160    },
161
162    update: function()
163    {
164        var selectedNode = this.selectedTreeElement ? this.selectedTreeElement.representedObject : null;
165
166        this.removeChildren();
167
168        if (!this.rootDOMNode)
169            return;
170
171        var treeElement;
172        if (this._includeRootDOMNode) {
173            treeElement = new WebInspector.DOMTreeElement(this.rootDOMNode);
174            treeElement.selectable = this._selectEnabled;
175            this.appendChild(treeElement);
176        } else {
177            // FIXME: this could use findTreeElement to reuse a tree element if it already exists
178            var node = this.rootDOMNode.firstChild;
179            while (node) {
180                treeElement = new WebInspector.DOMTreeElement(node);
181                treeElement.selectable = this._selectEnabled;
182                this.appendChild(treeElement);
183                node = node.nextSibling;
184            }
185        }
186
187        if (selectedNode)
188            this._revealAndSelectNode(selectedNode, true);
189    },
190
191    updateSelection: function()
192    {
193        if (!this.selectedTreeElement)
194            return;
195        var element = this.treeOutline.selectedTreeElement;
196        element.updateSelection();
197    },
198
199    _selectedNodeChanged: function()
200    {
201        this._eventSupport.dispatchEventToListeners(WebInspector.DOMTreeOutline.Event.SelectedNodeChanged);
202    },
203
204    findTreeElement: function(node)
205    {
206        function isAncestorNode(ancestor, node)
207        {
208            return ancestor.isAncestor(node);
209        }
210
211        function parentNode(node)
212        {
213            return node.parentNode;
214        }
215
216        var treeElement = TreeOutline.prototype.findTreeElement.call(this, node, isAncestorNode, parentNode);
217        if (!treeElement && node.nodeType() === Node.TEXT_NODE) {
218            // The text node might have been inlined if it was short, so try to find the parent element.
219            treeElement = TreeOutline.prototype.findTreeElement.call(this, node.parentNode, isAncestorNode, parentNode);
220        }
221
222        return treeElement;
223    },
224
225    createTreeElementFor: function(node)
226    {
227        var treeElement = this.findTreeElement(node);
228        if (treeElement)
229            return treeElement;
230        if (!node.parentNode)
231            return null;
232
233        treeElement = this.createTreeElementFor(node.parentNode);
234        if (treeElement && treeElement.showChild(node.index))
235            return treeElement.children[node.index];
236
237        return null;
238    },
239
240    set suppressRevealAndSelect(x)
241    {
242        if (this._suppressRevealAndSelect === x)
243            return;
244        this._suppressRevealAndSelect = x;
245    },
246
247    _revealAndSelectNode: function(node, omitFocus)
248    {
249        if (!node || this._suppressRevealAndSelect)
250            return;
251
252        var treeElement = this.createTreeElementFor(node);
253        if (!treeElement)
254            return;
255
256        treeElement.revealAndSelect(omitFocus);
257    },
258
259    _treeElementFromEvent: function(event)
260    {
261        var scrollContainer = this.element.parentElement;
262
263        // We choose this X coordinate based on the knowledge that our list
264        // items extend at least to the right edge of the outer <ol> container.
265        // In the no-word-wrap mode the outer <ol> may be wider than the tree container
266        // (and partially hidden), in which case we are left to use only its right boundary.
267        var x = scrollContainer.totalOffsetLeft + scrollContainer.offsetWidth - 36;
268
269        var y = event.pageY;
270
271        // Our list items have 1-pixel cracks between them vertically. We avoid
272        // the cracks by checking slightly above and slightly below the mouse
273        // and seeing if we hit the same element each time.
274        var elementUnderMouse = this.treeElementFromPoint(x, y);
275        var elementAboveMouse = this.treeElementFromPoint(x, y - 2);
276        var element;
277        if (elementUnderMouse === elementAboveMouse)
278            element = elementUnderMouse;
279        else
280            element = this.treeElementFromPoint(x, y + 2);
281
282        return element;
283    },
284
285    _onmousedown: function(event)
286    {
287        var element = this._treeElementFromEvent(event);
288        if (!element || element.isEventWithinDisclosureTriangle(event)) {
289            event.preventDefault();
290            return;
291        }
292
293        element.select();
294    },
295
296    _onmousemove: function(event)
297    {
298        var element = this._treeElementFromEvent(event);
299        if (element && this._previousHoveredElement === element)
300            return;
301
302        if (this._previousHoveredElement) {
303            this._previousHoveredElement.hovered = false;
304            delete this._previousHoveredElement;
305        }
306
307        if (element) {
308            element.hovered = true;
309            this._previousHoveredElement = element;
310
311            // Lazily compute tag-specific tooltips.
312            if (element.representedObject && !element.tooltip && element._createTooltipForNode)
313                element._createTooltipForNode();
314        }
315
316        WebInspector.domTreeManager.highlightDOMNode(element ? element.representedObject.id : 0);
317    },
318
319    _onmouseout: function(event)
320    {
321        var nodeUnderMouse = document.elementFromPoint(event.pageX, event.pageY);
322        if (nodeUnderMouse && nodeUnderMouse.isDescendant(this.element))
323            return;
324
325        if (this._previousHoveredElement) {
326            this._previousHoveredElement.hovered = false;
327            delete this._previousHoveredElement;
328        }
329
330        WebInspector.domTreeManager.hideDOMNodeHighlight();
331    },
332
333    _ondragstart: function(event)
334    {
335        var treeElement = this._treeElementFromEvent(event);
336        if (!treeElement)
337            return false;
338
339        if (!this._isValidDragSourceOrTarget(treeElement))
340            return false;
341
342        if (treeElement.representedObject.nodeName() === "BODY" || treeElement.representedObject.nodeName() === "HEAD")
343            return false;
344
345        event.dataTransfer.setData("text/plain", treeElement.listItemElement.textContent);
346        event.dataTransfer.effectAllowed = "copyMove";
347        this._nodeBeingDragged = treeElement.representedObject;
348
349        WebInspector.domTreeManager.hideDOMNodeHighlight();
350
351        return true;
352    },
353
354    _ondragover: function(event)
355    {
356        if (!this._nodeBeingDragged)
357            return false;
358
359        var treeElement = this._treeElementFromEvent(event);
360        if (!this._isValidDragSourceOrTarget(treeElement))
361            return false;
362
363        var node = treeElement.representedObject;
364        while (node) {
365            if (node === this._nodeBeingDragged)
366                return false;
367            node = node.parentNode;
368        }
369
370        treeElement.updateSelection();
371        treeElement.listItemElement.classList.add("elements-drag-over");
372        this._dragOverTreeElement = treeElement;
373        event.preventDefault();
374        event.dataTransfer.dropEffect = "move";
375        return false;
376    },
377
378    _ondragleave: function(event)
379    {
380        this._clearDragOverTreeElementMarker();
381        event.preventDefault();
382        return false;
383    },
384
385    _isValidDragSourceOrTarget: function(treeElement)
386    {
387        if (!treeElement)
388            return false;
389
390        var node = treeElement.representedObject;
391        if (!(node instanceof WebInspector.DOMNode))
392            return false;
393
394        if (!node.parentNode || node.parentNode.nodeType() !== Node.ELEMENT_NODE)
395            return false;
396
397        return true;
398    },
399
400    _ondrop: function(event)
401    {
402        event.preventDefault();
403        var treeElement = this._treeElementFromEvent(event);
404        if (this._nodeBeingDragged && treeElement) {
405            var parentNode;
406            var anchorNode;
407
408            if (treeElement._elementCloseTag) {
409                // Drop onto closing tag -> insert as last child.
410                parentNode = treeElement.representedObject;
411            } else {
412                var dragTargetNode = treeElement.representedObject;
413                parentNode = dragTargetNode.parentNode;
414                anchorNode = dragTargetNode;
415            }
416
417            function callback(error, newNodeId)
418            {
419                if (error)
420                    return;
421
422                this._updateModifiedNodes();
423                var newNode = WebInspector.domTreeManager.nodeForId(newNodeId);
424                if (newNode)
425                    this.selectDOMNode(newNode, true);
426            }
427            this._nodeBeingDragged.moveTo(parentNode, anchorNode, callback.bind(this));
428        }
429
430        delete this._nodeBeingDragged;
431    },
432
433    _ondragend: function(event)
434    {
435        event.preventDefault();
436        this._clearDragOverTreeElementMarker();
437        delete this._nodeBeingDragged;
438    },
439
440    _clearDragOverTreeElementMarker: function()
441    {
442        if (this._dragOverTreeElement) {
443            this._dragOverTreeElement.updateSelection();
444            this._dragOverTreeElement.listItemElement.classList.remove("elements-drag-over");
445            delete this._dragOverTreeElement;
446        }
447    },
448
449    _contextMenuEventFired: function(event)
450    {
451        var treeElement = this._treeElementFromEvent(event);
452        if (!treeElement)
453            return;
454
455        var contextMenu = new WebInspector.ContextMenu(event);
456        this.populateContextMenu(contextMenu, event);
457        contextMenu.show();
458    },
459
460    populateContextMenu: function(contextMenu, event)
461    {
462        var treeElement = this._treeElementFromEvent(event);
463        if (!treeElement)
464            return false;
465
466        var tag = event.target.enclosingNodeOrSelfWithClass("html-tag");
467        var textNode = event.target.enclosingNodeOrSelfWithClass("html-text-node");
468        var commentNode = event.target.enclosingNodeOrSelfWithClass("html-comment");
469        var populated = false;
470        if (tag && treeElement._populateTagContextMenu) {
471            if (populated)
472                contextMenu.appendSeparator();
473            treeElement._populateTagContextMenu(contextMenu, event);
474            populated = true;
475        } else if (textNode && treeElement._populateTextContextMenu) {
476            if (populated)
477                contextMenu.appendSeparator();
478            treeElement._populateTextContextMenu(contextMenu, textNode);
479            populated = true;
480        } else if (commentNode && treeElement._populateNodeContextMenu) {
481            if (populated)
482                contextMenu.appendSeparator();
483            treeElement._populateNodeContextMenu(contextMenu, textNode);
484            populated = true;
485        }
486
487        return populated;
488    },
489
490    adjustCollapsedRange: function()
491    {
492    },
493
494    _updateModifiedNodes: function()
495    {
496        if (this._elementsTreeUpdater)
497            this._elementsTreeUpdater._updateModifiedNodes();
498    },
499
500    _populateContextMenu: function(contextMenu, domNode)
501    {
502        if (!this._showInElementsPanelEnabled)
503            return;
504
505        function revealElement()
506        {
507            WebInspector.domTreeManager.inspectElement(domNode.id);
508        }
509
510        contextMenu.appendSeparator();
511        contextMenu.appendItem(WebInspector.UIString("Reveal in DOM Tree"), revealElement);
512    },
513
514    _showShadowDOMSettingChanged: function(event)
515    {
516        var nodeToSelect = this.selectedTreeElement ? this.selectedTreeElement.representedObject : null;
517        while (nodeToSelect) {
518            if (!nodeToSelect.isInShadowTree())
519                break;
520            nodeToSelect = nodeToSelect.parentNode;
521        }
522
523        this.children.forEach(function(child) {
524            child.updateChildren(true);
525        });
526
527        if (nodeToSelect)
528            this.selectDOMNode(nodeToSelect);
529    },
530
531    _hideElement: function(event, keyboardShortcut)
532    {
533        if (!this.selectedTreeElement || WebInspector.isEditingAnyField())
534            return;
535
536        event.preventDefault();
537
538        var selectedNode = this.selectedTreeElement.representedObject;
539        console.assert(selectedNode);
540        if (!selectedNode)
541            return;
542
543        if (selectedNode.nodeType() !== Node.ELEMENT_NODE)
544            return;
545
546        if (this._togglePending)
547            return;
548        this._togglePending = true;
549
550        function toggleProperties()
551        {
552            nodeStyles.removeEventListener(WebInspector.DOMNodeStyles.Event.Refreshed, toggleProperties, this);
553
554            var opacityProperty = nodeStyles.inlineStyle.propertyForName("opacity");
555            opacityProperty.value = "0";
556            opacityProperty.important = true;
557
558            var pointerEventsProperty = nodeStyles.inlineStyle.propertyForName("pointer-events");
559            pointerEventsProperty.value = "none";
560            pointerEventsProperty.important = true;
561
562            if (opacityProperty.enabled && pointerEventsProperty.enabled) {
563                opacityProperty.remove();
564                pointerEventsProperty.remove();
565            } else {
566                opacityProperty.add();
567                pointerEventsProperty.add();
568            }
569
570            delete this._togglePending;
571        }
572
573        var nodeStyles = WebInspector.cssStyleManager.stylesForNode(selectedNode);
574        if (nodeStyles.needsRefresh) {
575            nodeStyles.addEventListener(WebInspector.DOMNodeStyles.Event.Refreshed, toggleProperties, this);
576            nodeStyles.refresh();
577        } else
578            toggleProperties.call(this);
579    }
580}
581
582WebInspector.DOMTreeOutline.prototype.__proto__ = TreeOutline.prototype;
583