Web Inspector: Move a few remaining global WI settings to WI.settings
[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 WI.DOMTreeOutline = class DOMTreeOutline extends WI.TreeOutline
32 {
33     constructor(omitRootDOMNode, selectEnabled, excludeRevealElementContextMenu)
34     {
35         super();
36
37         this.element.addEventListener("mousedown", this._onmousedown.bind(this), false);
38         this.element.addEventListener("mousemove", this._onmousemove.bind(this), false);
39         this.element.addEventListener("mouseout", this._onmouseout.bind(this), false);
40         this.element.addEventListener("dragstart", this._ondragstart.bind(this), false);
41         this.element.addEventListener("dragover", this._ondragover.bind(this), false);
42         this.element.addEventListener("dragleave", this._ondragleave.bind(this), false);
43         this.element.addEventListener("drop", this._ondrop.bind(this), false);
44         this.element.addEventListener("dragend", this._ondragend.bind(this), false);
45
46         this.element.classList.add("dom", WI.SyntaxHighlightedStyleClassName);
47
48         this._includeRootDOMNode = !omitRootDOMNode;
49         this._selectEnabled = selectEnabled;
50         this._excludeRevealElementContextMenu = excludeRevealElementContextMenu;
51         this._rootDOMNode = null;
52         this._selectedDOMNode = null;
53
54         this._editable = false;
55         this._editing = false;
56         this._visible = false;
57
58         this._hideElementKeyboardShortcut = new WI.KeyboardShortcut(null, "H", this._hideElement.bind(this), this.element);
59         this._hideElementKeyboardShortcut.implicitlyPreventsDefault = false;
60
61         WI.settings.showShadowDOM.addEventListener(WI.Setting.Event.Changed, this._showShadowDOMSettingChanged, this);
62     }
63
64     // Public
65
66     wireToDomAgent()
67     {
68         this._elementsTreeUpdater = new WI.DOMTreeUpdater(this);
69     }
70
71     close()
72     {
73         WI.settings.showShadowDOM.removeEventListener(null, null, this);
74
75         if (this._elementsTreeUpdater) {
76             this._elementsTreeUpdater.close();
77             this._elementsTreeUpdater = null;
78         }
79     }
80
81     setVisible(visible, omitFocus)
82     {
83         this._visible = visible;
84         if (!this._visible)
85             return;
86
87         this._updateModifiedNodes();
88
89         if (this._selectedDOMNode)
90             this._revealAndSelectNode(this._selectedDOMNode, omitFocus);
91
92         this.update();
93     }
94
95     get rootDOMNode()
96     {
97         return this._rootDOMNode;
98     }
99
100     set rootDOMNode(x)
101     {
102         if (this._rootDOMNode === x)
103             return;
104
105         this._rootDOMNode = x;
106
107         this._isXMLMimeType = x && x.isXMLNode();
108
109         this.update();
110     }
111
112     get isXMLMimeType()
113     {
114         return this._isXMLMimeType;
115     }
116
117     selectedDOMNode()
118     {
119         return this._selectedDOMNode;
120     }
121
122     selectDOMNode(node, focus)
123     {
124         if (this._selectedDOMNode === node) {
125             this._revealAndSelectNode(node, !focus);
126             return;
127         }
128
129         this._selectedDOMNode = node;
130         this._revealAndSelectNode(node, !focus);
131
132         // The _revealAndSelectNode() method might find a different element if there is inlined text,
133         // and the select() call would change the selectedDOMNode and reenter this setter. So to
134         // avoid calling _selectedNodeChanged() twice, first check if _selectedDOMNode is the same
135         // node as the one passed in.
136         // Note that _revealAndSelectNode will not do anything for a null node.
137         if (!node || this._selectedDOMNode === node)
138             this._selectedNodeChanged();
139     }
140
141     get editable()
142     {
143         return this._editable;
144     }
145
146     set editable(x)
147     {
148         this._editable = x;
149     }
150
151     get editing()
152     {
153         return this._editing;
154     }
155
156     update()
157     {
158         if (!this.rootDOMNode)
159             return;
160
161         let selectedNode = this.selectedTreeElement ? this.selectedTreeElement.representedObject : null;
162
163         this.removeChildren();
164
165         var treeElement;
166         if (this._includeRootDOMNode) {
167             treeElement = new WI.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 WI.DOMTreeElement(node);
175                 treeElement.selectable = this._selectEnabled;
176                 this.appendChild(treeElement);
177                 node = node.nextSibling;
178
179                 if (treeElement.hasChildren && !treeElement.expanded)
180                     treeElement.expand();
181             }
182         }
183
184         if (selectedNode)
185             this._revealAndSelectNode(selectedNode, true);
186     }
187
188     updateSelection()
189     {
190         // This will miss updating selection areas used for the hovered tree element and
191         // and those used to show forced pseudo class indicators, but this should be okay.
192         // The hovered element will update when user moves the mouse, and indicators don't need the
193         // selection area height to be accurate since they use ::before to place the indicator.
194         if (this.selectedTreeElement)
195             this.selectedTreeElement.updateSelectionArea();
196     }
197
198     _selectedNodeChanged()
199     {
200         this.dispatchEventToListeners(WI.DOMTreeOutline.Event.SelectedNodeChanged);
201     }
202
203     findTreeElement(node)
204     {
205         let isAncestorNode = (ancestor, node) => ancestor.isAncestor(node);
206         let parentNode = (node) => node.parentNode;
207         let treeElement = super.findTreeElement(node, isAncestorNode, parentNode);
208         if (!treeElement && node.nodeType() === Node.TEXT_NODE) {
209             // The text node might have been inlined if it was short, so try to find the parent element.
210             treeElement = super.findTreeElement(node.parentNode, isAncestorNode, parentNode);
211         }
212
213         return treeElement;
214     }
215
216     createTreeElementFor(node)
217     {
218         var treeElement = this.findTreeElement(node);
219         if (treeElement)
220             return treeElement;
221
222         if (!node.parentNode)
223             return null;
224
225         treeElement = this.createTreeElementFor(node.parentNode);
226         if (!treeElement)
227             return null;
228
229         return treeElement.showChildNode(node);
230     }
231
232     set suppressRevealAndSelect(x)
233     {
234         if (this._suppressRevealAndSelect === x)
235             return;
236         this._suppressRevealAndSelect = x;
237     }
238
239     populateContextMenu(contextMenu, event, treeElement)
240     {
241         let tag = event.target.enclosingNodeOrSelfWithClass("html-tag");
242         let textNode = event.target.enclosingNodeOrSelfWithClass("html-text-node");
243         let commentNode = event.target.enclosingNodeOrSelfWithClass("html-comment");
244         let pseudoElement = event.target.enclosingNodeOrSelfWithClass("html-pseudo-element");
245
246         let subMenus = {
247             add: new WI.ContextSubMenuItem(contextMenu, WI.UIString("Add")),
248             edit: new WI.ContextSubMenuItem(contextMenu, WI.UIString("Edit")),
249             copy: new WI.ContextSubMenuItem(contextMenu, WI.UIString("Copy")),
250             delete: new WI.ContextSubMenuItem(contextMenu, WI.UIString("Delete")),
251         };
252
253         if (tag && treeElement._populateTagContextMenu)
254             treeElement._populateTagContextMenu(contextMenu, event, subMenus);
255         else if (textNode && treeElement._populateTextContextMenu)
256             treeElement._populateTextContextMenu(contextMenu, textNode, subMenus);
257         else if ((commentNode || pseudoElement) && treeElement._populateNodeContextMenu)
258             treeElement._populateNodeContextMenu(contextMenu, subMenus);
259
260         let options = {
261             excludeRevealElement: this._excludeRevealElementContextMenu,
262             copySubMenu: subMenus.copy,
263         };
264         WI.appendContextMenuItemsForDOMNode(contextMenu, treeElement.representedObject, options);
265
266         super.populateContextMenu(contextMenu, event, treeElement);
267     }
268
269     adjustCollapsedRange()
270     {
271     }
272
273     // Private
274
275     _revealAndSelectNode(node, omitFocus)
276     {
277         if (!node || this._suppressRevealAndSelect)
278             return;
279
280         if (!WI.settings.showShadowDOM.value) {
281             while (node && node.isInShadowTree())
282                 node = node.parentNode;
283             if (!node)
284                 return;
285         }
286
287         var treeElement = this.createTreeElementFor(node);
288         if (!treeElement)
289             return;
290
291         treeElement.revealAndSelect(omitFocus);
292     }
293
294     _onmousedown(event)
295     {
296         let element = this.treeElementFromEvent(event);
297         if (!element || element.isEventWithinDisclosureTriangle(event)) {
298             event.preventDefault();
299             return;
300         }
301
302         element.select();
303     }
304
305     _onmousemove(event)
306     {
307         let element = this.treeElementFromEvent(event);
308         if (element && this._previousHoveredElement === element)
309             return;
310
311         if (this._previousHoveredElement) {
312             this._previousHoveredElement.hovered = false;
313             this._previousHoveredElement = null;
314         }
315
316         if (element) {
317             element.hovered = true;
318             this._previousHoveredElement = element;
319
320             // Lazily compute tag-specific tooltips.
321             if (element.representedObject && !element.tooltip && element._createTooltipForNode)
322                 element._createTooltipForNode();
323         }
324
325         WI.domManager.highlightDOMNode(element ? element.representedObject.id : 0);
326     }
327
328     _onmouseout(event)
329     {
330         var nodeUnderMouse = document.elementFromPoint(event.pageX, event.pageY);
331         if (nodeUnderMouse && this.element.contains(nodeUnderMouse))
332             return;
333
334         if (this._previousHoveredElement) {
335             this._previousHoveredElement.hovered = false;
336             this._previousHoveredElement = null;
337         }
338
339         WI.domManager.hideDOMNodeHighlight();
340     }
341
342     _ondragstart(event)
343     {
344         let treeElement = this.treeElementFromEvent(event);
345         if (!treeElement)
346             return false;
347
348         if (!this._isValidDragSourceOrTarget(treeElement))
349             return false;
350
351         if (treeElement.representedObject.nodeName() === "BODY" || treeElement.representedObject.nodeName() === "HEAD")
352             return false;
353
354         event.dataTransfer.setData("text/plain", treeElement.listItemElement.textContent);
355         event.dataTransfer.effectAllowed = "copyMove";
356         this._nodeBeingDragged = treeElement.representedObject;
357
358         WI.domManager.hideDOMNodeHighlight();
359
360         return true;
361     }
362
363     _ondragover(event)
364     {
365         if (event.dataTransfer.types.includes(WI.GeneralStyleDetailsSidebarPanel.ToggledClassesDragType)) {
366             event.preventDefault();
367             event.dataTransfer.dropEffect = "copy";
368             return false;
369         }
370
371         if (!this._nodeBeingDragged)
372             return false;
373
374         let treeElement = this.treeElementFromEvent(event);
375         if (!this._isValidDragSourceOrTarget(treeElement))
376             return false;
377
378         let node = treeElement.representedObject;
379         while (node) {
380             if (node === this._nodeBeingDragged)
381                 return false;
382             node = node.parentNode;
383         }
384
385         this.dragOverTreeElement = treeElement;
386         treeElement.listItemElement.classList.add("elements-drag-over");
387         treeElement.updateSelectionArea();
388
389         event.preventDefault();
390         event.dataTransfer.dropEffect = "move";
391         return false;
392     }
393
394     _ondragleave(event)
395     {
396         this._clearDragOverTreeElementMarker();
397         event.preventDefault();
398         return false;
399     }
400
401     _isValidDragSourceOrTarget(treeElement)
402     {
403         if (!treeElement)
404             return false;
405
406         var node = treeElement.representedObject;
407         if (!(node instanceof WI.DOMNode))
408             return false;
409
410         if (!node.parentNode || node.parentNode.nodeType() !== Node.ELEMENT_NODE)
411             return false;
412
413         return true;
414     }
415
416     _ondrop(event)
417     {
418         event.preventDefault();
419
420         function callback(error, newNodeId)
421         {
422             if (error)
423                 return;
424
425             this._updateModifiedNodes();
426             var newNode = WI.domManager.nodeForId(newNodeId);
427             if (newNode)
428                 this.selectDOMNode(newNode, true);
429         }
430
431         let treeElement = this.treeElementFromEvent(event);
432         if (this._nodeBeingDragged && treeElement) {
433             let parentNode = null;
434             let anchorNode = null;
435
436             if (treeElement._elementCloseTag) {
437                 // Drop onto closing tag -> insert as last child.
438                 parentNode = treeElement.representedObject;
439             } else {
440                 let dragTargetNode = treeElement.representedObject;
441                 parentNode = dragTargetNode.parentNode;
442                 anchorNode = dragTargetNode;
443             }
444
445             this._nodeBeingDragged.moveTo(parentNode, anchorNode, callback.bind(this));
446         } else {
447             let className = event.dataTransfer.getData(WI.GeneralStyleDetailsSidebarPanel.ToggledClassesDragType);
448             if (className && treeElement)
449                 treeElement.representedObject.toggleClass(className, true);
450         }
451
452         delete this._nodeBeingDragged;
453     }
454
455     _ondragend(event)
456     {
457         event.preventDefault();
458         this._clearDragOverTreeElementMarker();
459         delete this._nodeBeingDragged;
460     }
461
462     _clearDragOverTreeElementMarker()
463     {
464         if (this.dragOverTreeElement) {
465             let element = this.dragOverTreeElement;
466             this.dragOverTreeElement = null;
467
468             element.listItemElement.classList.remove("elements-drag-over");
469             element.updateSelectionArea();
470         }
471     }
472
473     _updateModifiedNodes()
474     {
475         if (this._elementsTreeUpdater)
476             this._elementsTreeUpdater._updateModifiedNodes();
477     }
478
479     _showShadowDOMSettingChanged(event)
480     {
481         var nodeToSelect = this.selectedTreeElement ? this.selectedTreeElement.representedObject : null;
482         while (nodeToSelect) {
483             if (!nodeToSelect.isInShadowTree())
484                 break;
485             nodeToSelect = nodeToSelect.parentNode;
486         }
487
488         this.children.forEach(function(child) {
489             child.updateChildren(true);
490         });
491
492         if (nodeToSelect)
493             this.selectDOMNode(nodeToSelect);
494     }
495
496     _hideElement(event, keyboardShortcut)
497     {
498         if (!this.selectedTreeElement || WI.isEditingAnyField())
499             return;
500
501         event.preventDefault();
502
503         this.selectedTreeElement.toggleElementVisibility();
504     }
505 };
506
507 WI.DOMTreeOutline.Event = {
508     SelectedNodeChanged: "dom-tree-outline-selected-node-changed"
509 };