Web Inspector: TypeError when hovering over "Show All Nodes" button in the Dom Tree...
[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 WebInspector.Object.addConstructorFunctions(WebInspector.DOMTreeOutline);
67
68 WebInspector.DOMTreeOutline.StyleClassName = "dom-tree-outline";
69
70 WebInspector.DOMTreeOutline.Event = {
71     SelectedNodeChanged: "dom-tree-outline-selected-node-changed"
72 }
73
74 WebInspector.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
582 WebInspector.DOMTreeOutline.prototype.__proto__ = TreeOutline.prototype;