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