885f784e37dfa4574b92b80b4b908219b5864883
[WebKit-https.git] / Source / WebInspectorUI / UserInterface / Views / DOMTreeOutline.js
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
31 WebInspector.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
66 // FIXME: Move to a WebInspector.Object subclass and we can remove this.
67 WebInspector.Object.deprecatedAddConstructorFunctions(WebInspector.DOMTreeOutline);
68
69 WebInspector.DOMTreeOutline.StyleClassName = "dom-tree-outline";
70
71 WebInspector.DOMTreeOutline.Event = {
72     SelectedNodeChanged: "dom-tree-outline-selected-node-changed"
73 };
74
75 WebInspector.DOMTreeOutline.prototype = {
76     constructor: WebInspector.DOMTreeOutline,
77
78     wireToDomAgent: function()
79     {
80         this._elementsTreeUpdater = new WebInspector.DOMTreeUpdater(this);
81     },
82
83     close: function()
84     {
85         if (this._elementsTreeUpdater) {
86             this._elementsTreeUpdater.close();
87             this._elementsTreeUpdater = null;
88         }
89     },
90
91     setVisible: function(visible, omitFocus)
92     {
93         this._visible = visible;
94         if (!this._visible)
95             return;
96
97         this._updateModifiedNodes();
98         if (this._selectedDOMNode)
99             this._revealAndSelectNode(this._selectedDOMNode, omitFocus);
100     },
101
102     addEventListener: function(eventType, listener, thisObject)
103     {
104         this._eventSupport.addEventListener(eventType, listener, thisObject);
105     },
106
107     removeEventListener: function(eventType, listener, thisObject)
108     {
109         this._eventSupport.removeEventListener(eventType, listener, thisObject);
110     },
111
112     get rootDOMNode()
113     {
114         return this._rootDOMNode;
115     },
116
117     set rootDOMNode(x)
118     {
119         if (this._rootDOMNode === x)
120             return;
121
122         this._rootDOMNode = x;
123
124         this._isXMLMimeType = x && x.isXMLNode();
125
126         this.update();
127     },
128
129     get isXMLMimeType()
130     {
131         return this._isXMLMimeType;
132     },
133
134     selectedDOMNode: function()
135     {
136         return this._selectedDOMNode;
137     },
138
139     selectDOMNode: function(node, focus)
140     {
141         if (this._selectedDOMNode === node) {
142             this._revealAndSelectNode(node, !focus);
143             return;
144         }
145
146         this._selectedDOMNode = node;
147         this._revealAndSelectNode(node, !focus);
148
149         // The _revealAndSelectNode() method might find a different element if there is inlined text,
150         // and the select() call would change the selectedDOMNode and reenter this setter. So to
151         // avoid calling _selectedNodeChanged() twice, first check if _selectedDOMNode is the same
152         // node as the one passed in.
153         // Note that _revealAndSelectNode will not do anything for a null node.
154         if (!node || this._selectedDOMNode === node)
155             this._selectedNodeChanged();
156     },
157
158     get editing()
159     {
160         return this._editing;
161     },
162
163     update: function()
164     {
165         var selectedNode = this.selectedTreeElement ? this.selectedTreeElement.representedObject : null;
166
167         this.removeChildren();
168
169         if (!this.rootDOMNode)
170             return;
171
172         var treeElement;
173         if (this._includeRootDOMNode) {
174             treeElement = new WebInspector.DOMTreeElement(this.rootDOMNode);
175             treeElement.selectable = this._selectEnabled;
176             this.appendChild(treeElement);
177         } else {
178             // FIXME: this could use findTreeElement to reuse a tree element if it already exists
179             var node = this.rootDOMNode.firstChild;
180             while (node) {
181                 treeElement = new WebInspector.DOMTreeElement(node);
182                 treeElement.selectable = this._selectEnabled;
183                 this.appendChild(treeElement);
184                 node = node.nextSibling;
185             }
186         }
187
188         if (selectedNode)
189             this._revealAndSelectNode(selectedNode, true);
190     },
191
192     updateSelection: function()
193     {
194         if (!this.selectedTreeElement)
195             return;
196         var element = this.treeOutline.selectedTreeElement;
197         element.updateSelection();
198     },
199
200     _selectedNodeChanged: function()
201     {
202         this._eventSupport.dispatchEventToListeners(WebInspector.DOMTreeOutline.Event.SelectedNodeChanged);
203     },
204
205     findTreeElement: function(node)
206     {
207         function isAncestorNode(ancestor, node)
208         {
209             return ancestor.isAncestor(node);
210         }
211
212         function parentNode(node)
213         {
214             return node.parentNode;
215         }
216
217         var treeElement = TreeOutline.prototype.findTreeElement.call(this, node, isAncestorNode, parentNode);
218         if (!treeElement && node.nodeType() === Node.TEXT_NODE) {
219             // The text node might have been inlined if it was short, so try to find the parent element.
220             treeElement = TreeOutline.prototype.findTreeElement.call(this, node.parentNode, isAncestorNode, parentNode);
221         }
222
223         return treeElement;
224     },
225
226     createTreeElementFor: function(node)
227     {
228         var treeElement = this.findTreeElement(node);
229         if (treeElement)
230             return treeElement;
231         if (!node.parentNode)
232             return null;
233
234         treeElement = this.createTreeElementFor(node.parentNode);
235         if (treeElement && treeElement.showChild(node.index))
236             return treeElement.children[node.index];
237
238         return null;
239     },
240
241     set suppressRevealAndSelect(x)
242     {
243         if (this._suppressRevealAndSelect === x)
244             return;
245         this._suppressRevealAndSelect = x;
246     },
247
248     _revealAndSelectNode: function(node, omitFocus)
249     {
250         if (!node || this._suppressRevealAndSelect)
251             return;
252
253         var treeElement = this.createTreeElementFor(node);
254         if (!treeElement)
255             return;
256
257         treeElement.revealAndSelect(omitFocus);
258     },
259
260     _treeElementFromEvent: function(event)
261     {
262         var scrollContainer = this.element.parentElement;
263
264         // We choose this X coordinate based on the knowledge that our list
265         // items extend at least to the right edge of the outer <ol> container.
266         // In the no-word-wrap mode the outer <ol> may be wider than the tree container
267         // (and partially hidden), in which case we are left to use only its right boundary.
268         var x = scrollContainer.totalOffsetLeft + scrollContainer.offsetWidth - 36;
269
270         var y = event.pageY;
271
272         // Our list items have 1-pixel cracks between them vertically. We avoid
273         // the cracks by checking slightly above and slightly below the mouse
274         // and seeing if we hit the same element each time.
275         var elementUnderMouse = this.treeElementFromPoint(x, y);
276         var elementAboveMouse = this.treeElementFromPoint(x, y - 2);
277         var element;
278         if (elementUnderMouse === elementAboveMouse)
279             element = elementUnderMouse;
280         else
281             element = this.treeElementFromPoint(x, y + 2);
282
283         return element;
284     },
285
286     _onmousedown: function(event)
287     {
288         var element = this._treeElementFromEvent(event);
289         if (!element || element.isEventWithinDisclosureTriangle(event)) {
290             event.preventDefault();
291             return;
292         }
293
294         element.select();
295     },
296
297     _onmousemove: function(event)
298     {
299         var element = this._treeElementFromEvent(event);
300         if (element && this._previousHoveredElement === element)
301             return;
302
303         if (this._previousHoveredElement) {
304             this._previousHoveredElement.hovered = false;
305             delete this._previousHoveredElement;
306         }
307
308         if (element) {
309             element.hovered = true;
310             this._previousHoveredElement = element;
311
312             // Lazily compute tag-specific tooltips.
313             if (element.representedObject && !element.tooltip && element._createTooltipForNode)
314                 element._createTooltipForNode();
315         }
316
317         WebInspector.domTreeManager.highlightDOMNode(element ? element.representedObject.id : 0);
318     },
319
320     _onmouseout: function(event)
321     {
322         var nodeUnderMouse = document.elementFromPoint(event.pageX, event.pageY);
323         if (nodeUnderMouse && nodeUnderMouse.isDescendant(this.element))
324             return;
325
326         if (this._previousHoveredElement) {
327             this._previousHoveredElement.hovered = false;
328             delete this._previousHoveredElement;
329         }
330
331         WebInspector.domTreeManager.hideDOMNodeHighlight();
332     },
333
334     _ondragstart: function(event)
335     {
336         var treeElement = this._treeElementFromEvent(event);
337         if (!treeElement)
338             return false;
339
340         if (!this._isValidDragSourceOrTarget(treeElement))
341             return false;
342
343         if (treeElement.representedObject.nodeName() === "BODY" || treeElement.representedObject.nodeName() === "HEAD")
344             return false;
345
346         event.dataTransfer.setData("text/plain", treeElement.listItemElement.textContent);
347         event.dataTransfer.effectAllowed = "copyMove";
348         this._nodeBeingDragged = treeElement.representedObject;
349
350         WebInspector.domTreeManager.hideDOMNodeHighlight();
351
352         return true;
353     },
354
355     _ondragover: function(event)
356     {
357         if (!this._nodeBeingDragged)
358             return false;
359
360         var treeElement = this._treeElementFromEvent(event);
361         if (!this._isValidDragSourceOrTarget(treeElement))
362             return false;
363
364         var node = treeElement.representedObject;
365         while (node) {
366             if (node === this._nodeBeingDragged)
367                 return false;
368             node = node.parentNode;
369         }
370
371         treeElement.updateSelection();
372         treeElement.listItemElement.classList.add("elements-drag-over");
373         this._dragOverTreeElement = treeElement;
374         event.preventDefault();
375         event.dataTransfer.dropEffect = "move";
376         return false;
377     },
378
379     _ondragleave: function(event)
380     {
381         this._clearDragOverTreeElementMarker();
382         event.preventDefault();
383         return false;
384     },
385
386     _isValidDragSourceOrTarget: function(treeElement)
387     {
388         if (!treeElement)
389             return false;
390
391         var node = treeElement.representedObject;
392         if (!(node instanceof WebInspector.DOMNode))
393             return false;
394
395         if (!node.parentNode || node.parentNode.nodeType() !== Node.ELEMENT_NODE)
396             return false;
397
398         return true;
399     },
400
401     _ondrop: function(event)
402     {
403         event.preventDefault();
404         var treeElement = this._treeElementFromEvent(event);
405         if (this._nodeBeingDragged && treeElement) {
406             var parentNode;
407             var anchorNode;
408
409             if (treeElement._elementCloseTag) {
410                 // Drop onto closing tag -> insert as last child.
411                 parentNode = treeElement.representedObject;
412             } else {
413                 var dragTargetNode = treeElement.representedObject;
414                 parentNode = dragTargetNode.parentNode;
415                 anchorNode = dragTargetNode;
416             }
417
418             function callback(error, newNodeId)
419             {
420                 if (error)
421                     return;
422
423                 this._updateModifiedNodes();
424                 var newNode = WebInspector.domTreeManager.nodeForId(newNodeId);
425                 if (newNode)
426                     this.selectDOMNode(newNode, true);
427             }
428             this._nodeBeingDragged.moveTo(parentNode, anchorNode, callback.bind(this));
429         }
430
431         delete this._nodeBeingDragged;
432     },
433
434     _ondragend: function(event)
435     {
436         event.preventDefault();
437         this._clearDragOverTreeElementMarker();
438         delete this._nodeBeingDragged;
439     },
440
441     _clearDragOverTreeElementMarker: function()
442     {
443         if (this._dragOverTreeElement) {
444             this._dragOverTreeElement.updateSelection();
445             this._dragOverTreeElement.listItemElement.classList.remove("elements-drag-over");
446             delete this._dragOverTreeElement;
447         }
448     },
449
450     _contextMenuEventFired: function(event)
451     {
452         var treeElement = this._treeElementFromEvent(event);
453         if (!treeElement)
454             return;
455
456         var contextMenu = new WebInspector.ContextMenu(event);
457         this.populateContextMenu(contextMenu, event, treeElement);
458         contextMenu.show();
459     },
460
461     populateContextMenu: function(contextMenu, event, treeElement)
462     {
463         var tag = event.target.enclosingNodeOrSelfWithClass("html-tag");
464         var textNode = event.target.enclosingNodeOrSelfWithClass("html-text-node");
465         var commentNode = event.target.enclosingNodeOrSelfWithClass("html-comment");
466
467         var populated = false;
468         if (tag && treeElement._populateTagContextMenu) {
469             if (populated)
470                 contextMenu.appendSeparator();
471             treeElement._populateTagContextMenu(contextMenu, event);
472             populated = true;
473         } else if (textNode && treeElement._populateTextContextMenu) {
474             if (populated)
475                 contextMenu.appendSeparator();
476             treeElement._populateTextContextMenu(contextMenu, textNode);
477             populated = true;
478         } else if (commentNode && treeElement._populateNodeContextMenu) {
479             if (populated)
480                 contextMenu.appendSeparator();
481             treeElement._populateNodeContextMenu(contextMenu, textNode);
482             populated = true;
483         }
484
485         return populated;
486     },
487
488     adjustCollapsedRange: function()
489     {
490     },
491
492     _updateModifiedNodes: function()
493     {
494         if (this._elementsTreeUpdater)
495             this._elementsTreeUpdater._updateModifiedNodes();
496     },
497
498     _populateContextMenu: function(contextMenu, domNode)
499     {
500         if (!this._showInElementsPanelEnabled)
501             return;
502
503         function revealElement()
504         {
505             WebInspector.domTreeManager.inspectElement(domNode.id);
506         }
507
508         contextMenu.appendSeparator();
509         contextMenu.appendItem(WebInspector.UIString("Reveal in DOM Tree"), revealElement);
510     },
511
512     _showShadowDOMSettingChanged: function(event)
513     {
514         var nodeToSelect = this.selectedTreeElement ? this.selectedTreeElement.representedObject : null;
515         while (nodeToSelect) {
516             if (!nodeToSelect.isInShadowTree())
517                 break;
518             nodeToSelect = nodeToSelect.parentNode;
519         }
520
521         this.children.forEach(function(child) {
522             child.updateChildren(true);
523         });
524
525         if (nodeToSelect)
526             this.selectDOMNode(nodeToSelect);
527     },
528
529     _hideElement: function(event, keyboardShortcut)
530     {
531         if (!this.selectedTreeElement || WebInspector.isEditingAnyField())
532             return;
533
534         event.preventDefault();
535
536         var selectedNode = this.selectedTreeElement.representedObject;
537         console.assert(selectedNode);
538         if (!selectedNode)
539             return;
540
541         if (selectedNode.nodeType() !== Node.ELEMENT_NODE)
542             return;
543
544         if (this._togglePending)
545             return;
546         this._togglePending = true;
547
548         function toggleProperties()
549         {
550             nodeStyles.removeEventListener(WebInspector.DOMNodeStyles.Event.Refreshed, toggleProperties, this);
551
552             var opacityProperty = nodeStyles.inlineStyle.propertyForName("opacity");
553             opacityProperty.value = "0";
554             opacityProperty.important = true;
555
556             var pointerEventsProperty = nodeStyles.inlineStyle.propertyForName("pointer-events");
557             pointerEventsProperty.value = "none";
558             pointerEventsProperty.important = true;
559
560             if (opacityProperty.enabled && pointerEventsProperty.enabled) {
561                 opacityProperty.remove();
562                 pointerEventsProperty.remove();
563             } else {
564                 opacityProperty.add();
565                 pointerEventsProperty.add();
566             }
567
568             delete this._togglePending;
569         }
570
571         var nodeStyles = WebInspector.cssStyleManager.stylesForNode(selectedNode);
572         if (nodeStyles.needsRefresh) {
573             nodeStyles.addEventListener(WebInspector.DOMNodeStyles.Event.Refreshed, toggleProperties, this);
574             nodeStyles.refresh();
575         } else
576             toggleProperties.call(this);
577     }
578 };
579
580 WebInspector.DOMTreeOutline.prototype.__proto__ = TreeOutline.prototype;