85481d7a95d424e4c63408b864645036794ef2d8
[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({selectable, omitRootDOMNode, excludeRevealElementContextMenu, showLastSelected} = {})
34     {
35         super(selectable);
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         this.element.dir = "ltr";
48
49         if (showLastSelected)
50             this.element.classList.add("show-last-selected");
51
52         this._includeRootDOMNode = !omitRootDOMNode;
53         this._excludeRevealElementContextMenu = excludeRevealElementContextMenu;
54         this._rootDOMNode = null;
55         this._selectedDOMNode = null;
56         this._treeElementsToRemove = null;
57
58         this._editable = false;
59         this._editing = false;
60         this._visible = false;
61
62         this._hideElementsKeyboardShortcut = new WI.KeyboardShortcut(null, "H", this._hideElements.bind(this), this.element);
63         this._hideElementsKeyboardShortcut.implicitlyPreventsDefault = false;
64
65         WI.settings.showShadowDOM.addEventListener(WI.Setting.Event.Changed, this._showShadowDOMSettingChanged, this);
66     }
67
68     // Public
69
70     wireToDomAgent()
71     {
72         this._elementsTreeUpdater = new WI.DOMTreeUpdater(this);
73     }
74
75     close()
76     {
77         WI.settings.showShadowDOM.removeEventListener(null, null, this);
78
79         if (this._elementsTreeUpdater) {
80             this._elementsTreeUpdater.close();
81             this._elementsTreeUpdater = null;
82         }
83     }
84
85     setVisible(visible, omitFocus)
86     {
87         this._visible = visible;
88         if (!this._visible)
89             return;
90
91         this._updateModifiedNodes();
92
93         if (this._selectedDOMNode)
94             this._revealAndSelectNode(this._selectedDOMNode, omitFocus);
95
96         this.update();
97     }
98
99     get rootDOMNode()
100     {
101         return this._rootDOMNode;
102     }
103
104     set rootDOMNode(x)
105     {
106         if (this._rootDOMNode === x)
107             return;
108
109         this._rootDOMNode = x;
110
111         this._isXMLMimeType = x && x.isXMLNode();
112
113         this.update();
114     }
115
116     get isXMLMimeType()
117     {
118         return this._isXMLMimeType;
119     }
120
121     selectedDOMNode()
122     {
123         return this._selectedDOMNode;
124     }
125
126     selectDOMNode(node, focus)
127     {
128         if (this._selectedDOMNode === node) {
129             this._revealAndSelectNode(node, !focus);
130             return;
131         }
132
133         this._selectedDOMNode = node;
134         this._revealAndSelectNode(node, !focus);
135
136         // The _revealAndSelectNode() method might find a different element if there is inlined text,
137         // and the select() call would change the selectedDOMNode and reenter this setter. So to
138         // avoid calling _selectedNodeChanged() twice, first check if _selectedDOMNode is the same
139         // node as the one passed in.
140         // Note that _revealAndSelectNode will not do anything for a null node.
141         if (!node || this._selectedDOMNode === node)
142             this._selectedNodeChanged();
143     }
144
145     get editable()
146     {
147         return this._editable;
148     }
149
150     set editable(x)
151     {
152         this._editable = x;
153     }
154
155     get editing()
156     {
157         return this._editing;
158     }
159
160     update()
161     {
162         if (!this.rootDOMNode)
163             return;
164
165         let selectedTreeElements = this.selectedTreeElements;
166
167         this.removeChildren();
168
169         var treeElement;
170         if (this._includeRootDOMNode) {
171             treeElement = new WI.DOMTreeElement(this.rootDOMNode);
172             treeElement.selectable = this.selectable;
173             this.appendChild(treeElement);
174         } else {
175             // FIXME: this could use findTreeElement to reuse a tree element if it already exists
176             var node = this.rootDOMNode.firstChild;
177             while (node) {
178                 treeElement = new WI.DOMTreeElement(node);
179                 treeElement.selectable = this.selectable;
180                 this.appendChild(treeElement);
181                 node = node.nextSibling;
182
183                 if (treeElement.hasChildren && !treeElement.expanded)
184                     treeElement.expand();
185             }
186         }
187
188         if (!selectedTreeElements.length)
189             return;
190
191         // The selection cannot be restored from represented objects alone,
192         // since a closing tag DOMTreeElement has the same represented object
193         // as its parent.
194         selectedTreeElements = selectedTreeElements.map((oldTreeElement) => {
195             let treeElement = this.findTreeElement(oldTreeElement.representedObject);
196             if (treeElement && oldTreeElement.isCloseTag()) {
197                 console.assert(treeElement.closeTagTreeElement, "Missing close tag TreeElement.", treeElement);
198                 if (treeElement.closeTagTreeElement)
199                     treeElement = treeElement.closeTagTreeElement;
200             }
201             return treeElement;
202         });
203
204         // It's possible that a previously selected node will no longer exist (e.g. after navigation).
205         selectedTreeElements = selectedTreeElements.filter((x) => !!x);
206
207         if (!selectedTreeElements.length)
208             return;
209
210         this.selectTreeElements(selectedTreeElements);
211
212         if (this.selectedTreeElement)
213             this.selectedTreeElement.reveal();
214     }
215
216     updateSelectionArea()
217     {
218         // This will miss updating selection areas used for the hovered tree element and
219         // and those used to show forced pseudo class indicators, but this should be okay.
220         // The hovered element will update when user moves the mouse, and indicators don't need the
221         // selection area height to be accurate since they use ::before to place the indicator.
222         let selectedTreeElements = this.selectedTreeElements;
223         for (let treeElement of selectedTreeElements)
224             treeElement.updateSelectionArea();
225     }
226
227     toggleSelectedElementsVisibility(forceHidden)
228     {
229         for (let treeElement of this.selectedTreeElements)
230             treeElement.toggleElementVisibility(forceHidden);
231     }
232
233     _selectedNodeChanged()
234     {
235         this.dispatchEventToListeners(WI.DOMTreeOutline.Event.SelectedNodeChanged);
236     }
237
238     findTreeElement(node)
239     {
240         let isAncestorNode = (ancestor, node) => ancestor.isAncestor(node);
241         let parentNode = (node) => node.parentNode;
242         let treeElement = super.findTreeElement(node, isAncestorNode, parentNode);
243         if (!treeElement && node.nodeType() === Node.TEXT_NODE) {
244             // The text node might have been inlined if it was short, so try to find the parent element.
245             treeElement = super.findTreeElement(node.parentNode, isAncestorNode, parentNode);
246         }
247
248         return treeElement;
249     }
250
251     createTreeElementFor(node)
252     {
253         var treeElement = this.findTreeElement(node);
254         if (treeElement)
255             return treeElement;
256
257         if (!node.parentNode)
258             return null;
259
260         treeElement = this.createTreeElementFor(node.parentNode);
261         if (!treeElement)
262             return null;
263
264         return treeElement.showChildNode(node);
265     }
266
267     set suppressRevealAndSelect(x)
268     {
269         if (this._suppressRevealAndSelect === x)
270             return;
271         this._suppressRevealAndSelect = x;
272     }
273
274     populateContextMenu(contextMenu, event, treeElement)
275     {
276         let subMenus = {
277             add: new WI.ContextSubMenuItem(contextMenu, WI.UIString("Add")),
278             edit: new WI.ContextSubMenuItem(contextMenu, WI.UIString("Edit")),
279             copy: new WI.ContextSubMenuItem(contextMenu, WI.UIString("Copy")),
280             delete: new WI.ContextSubMenuItem(contextMenu, WI.UIString("Delete")),
281         };
282
283         if (treeElement.selected && this.selectedTreeElements.length > 1)
284             subMenus.delete.appendItem(WI.UIString("Nodes"), () => { this.ondelete(); }, !this._editable);
285
286         if (treeElement.populateDOMNodeContextMenu)
287             treeElement.populateDOMNodeContextMenu(contextMenu, subMenus, event, subMenus);
288
289         let options = {
290             excludeRevealElement: this._excludeRevealElementContextMenu,
291             copySubMenu: subMenus.copy,
292         };
293
294         if (treeElement.bindRevealDescendantBreakpointsMenuItemHandler)
295             options.revealDescendantBreakpointsMenuItemHandler = treeElement.bindRevealDescendantBreakpointsMenuItemHandler();
296
297         WI.appendContextMenuItemsForDOMNode(contextMenu, treeElement.representedObject, options);
298
299         super.populateContextMenu(contextMenu, event, treeElement);
300     }
301
302     adjustCollapsedRange()
303     {
304     }
305
306     ondelete()
307     {
308         if (!this._editable)
309             return false;
310
311         this._treeElementsToRemove = this.selectedTreeElements;
312
313         // Reveal all of the elements being deleted so that if the node is hidden (e.g. the parent
314         // is collapsed), we can select its siblings instead of the parent itself.
315         for (let treeElement of this._treeElementsToRemove)
316             treeElement.reveal();
317
318         this._selectionController.removeSelectedItems();
319
320         let levelMap = new Map;
321
322         function getLevel(treeElement) {
323             let level = levelMap.get(treeElement);
324             if (isNaN(level)) {
325                 level = 0;
326                 let current = treeElement;
327                 while (current = current.parent)
328                     level++;
329                 levelMap.set(treeElement, level);
330             }
331             return level;
332         }
333
334         // Sort in descending order by node level. This ensures that child nodes
335         // are removed before their ancestors.
336         this._treeElementsToRemove.sort((a, b) => getLevel(b) - getLevel(a));
337
338         // Track removed elements, since the opening and closing tags for the
339         // same WI.DOMNode can both be selected.
340         let removedDOMNodes = new Set;
341
342         for (let treeElement of this._treeElementsToRemove) {
343             if (removedDOMNodes.has(treeElement.representedObject))
344                 continue;
345             removedDOMNodes.add(treeElement.representedObject);
346             treeElement.remove();
347         }
348
349         this._treeElementsToRemove = null;
350
351         if (this.selectedTreeElement && !this.selectedTreeElement.isCloseTag()) {
352             console.assert(this.selectedTreeElements.length === 1);
353             this.selectedTreeElement.reveal();
354         }
355
356         return true;
357     }
358
359     // SelectionController delegate overrides
360
361     selectionControllerPreviousSelectableItem(controller, item)
362     {
363         let treeElement = this.getCachedTreeElement(item);
364         console.assert(treeElement, "Missing TreeElement for representedObject.", item);
365         if (!treeElement)
366             return null;
367
368         if (this._treeElementsToRemove) {
369             // When deleting, force the SelectionController to check siblings in
370             // the opposite direction before searching up the parent chain.
371             if (!treeElement.previousSelectableSibling && treeElement.nextSelectableSibling)
372                 return null;
373         }
374
375         return super.selectionControllerPreviousSelectableItem(controller, item);
376     }
377
378     // Protected
379
380     canSelectTreeElement(treeElement)
381     {
382         if (!super.canSelectTreeElement(treeElement))
383             return false;
384
385         let willRemoveAncestorOrSelf = false;
386         if (this._treeElementsToRemove) {
387             while (treeElement && !willRemoveAncestorOrSelf) {
388                 willRemoveAncestorOrSelf = this._treeElementsToRemove.includes(treeElement);
389                 treeElement = treeElement.parent;
390             }
391         }
392
393         return !willRemoveAncestorOrSelf;
394     }
395
396     objectForSelection(treeElement)
397     {
398         if (treeElement instanceof WI.DOMTreeElement && treeElement.isCloseTag()) {
399             // SelectionController requires every selectable item to be unique.
400             // The DOMTreeElement for a close tag has the same represented object
401             // as it's parent (the open tag). Return a proxy object associated
402             // with the tree element for the close tag so it can be selected.
403             if (!treeElement.__closeTagProxyObject)
404                 treeElement.__closeTagProxyObject = {__proxyObjectTreeElement: treeElement};
405             return treeElement.__closeTagProxyObject;
406         }
407
408         return super.objectForSelection(treeElement);
409     }
410
411     // Private
412
413     _revealAndSelectNode(node, omitFocus)
414     {
415         if (!node || this._suppressRevealAndSelect)
416             return;
417
418         if (!WI.settings.showShadowDOM.value) {
419             while (node && node.isInShadowTree())
420                 node = node.parentNode;
421             if (!node)
422                 return;
423         }
424
425         var treeElement = this.createTreeElementFor(node);
426         if (!treeElement)
427             return;
428
429         treeElement.revealAndSelect(omitFocus);
430     }
431
432     _onmousedown(event)
433     {
434         let element = this.treeElementFromEvent(event);
435         if (!element || element.isEventWithinDisclosureTriangle(event)) {
436             event.preventDefault();
437             return;
438         }
439     }
440
441     _onmousemove(event)
442     {
443         let element = this.treeElementFromEvent(event);
444         if (element && this._previousHoveredElement === element)
445             return;
446
447         if (this._previousHoveredElement) {
448             this._previousHoveredElement.hovered = false;
449             this._previousHoveredElement = null;
450         }
451
452         if (element) {
453             element.hovered = true;
454             this._previousHoveredElement = element;
455
456             // Lazily compute tag-specific tooltips.
457             if (element.representedObject && !element.tooltip && element._createTooltipForNode)
458                 element._createTooltipForNode();
459         }
460
461         WI.domManager.highlightDOMNode(element ? element.representedObject.id : 0);
462     }
463
464     _onmouseout(event)
465     {
466         var nodeUnderMouse = document.elementFromPoint(event.pageX, event.pageY);
467         if (nodeUnderMouse && this.element.contains(nodeUnderMouse))
468             return;
469
470         if (this._previousHoveredElement) {
471             this._previousHoveredElement.hovered = false;
472             this._previousHoveredElement = null;
473         }
474
475         WI.domManager.hideDOMNodeHighlight();
476     }
477
478     _ondragstart(event)
479     {
480         let treeElement = this.treeElementFromEvent(event);
481         if (!treeElement)
482             return false;
483
484         event.dataTransfer.effectAllowed = "copyMove";
485         event.dataTransfer.setData(DOMTreeOutline.DOMNodeIdDragType, treeElement.representedObject.id);
486
487         if (!this._isValidDragSourceOrTarget(treeElement))
488             return false;
489
490         if (treeElement.representedObject.nodeName() === "BODY" || treeElement.representedObject.nodeName() === "HEAD")
491             return false;
492
493         event.dataTransfer.setData("text/plain", treeElement.listItemElement.textContent);
494         this._nodeBeingDragged = treeElement.representedObject;
495
496         WI.domManager.hideDOMNodeHighlight();
497
498         return true;
499     }
500
501     _ondragover(event)
502     {
503         if (event.dataTransfer.types.includes(WI.GeneralStyleDetailsSidebarPanel.ToggledClassesDragType)) {
504             event.preventDefault();
505             event.dataTransfer.dropEffect = "copy";
506             return false;
507         }
508
509         if (!this._nodeBeingDragged)
510             return false;
511
512         let treeElement = this.treeElementFromEvent(event);
513         if (!this._isValidDragSourceOrTarget(treeElement))
514             return false;
515
516         let node = treeElement.representedObject;
517         while (node) {
518             if (node === this._nodeBeingDragged)
519                 return false;
520             node = node.parentNode;
521         }
522
523         this.dragOverTreeElement = treeElement;
524         treeElement.listItemElement.classList.add("elements-drag-over");
525         treeElement.updateSelectionArea();
526
527         event.preventDefault();
528         event.dataTransfer.dropEffect = "move";
529         return false;
530     }
531
532     _ondragleave(event)
533     {
534         this._clearDragOverTreeElementMarker();
535         event.preventDefault();
536         return false;
537     }
538
539     _isValidDragSourceOrTarget(treeElement)
540     {
541         if (!treeElement)
542             return false;
543
544         var node = treeElement.representedObject;
545         if (!(node instanceof WI.DOMNode))
546             return false;
547
548         if (!node.parentNode || node.parentNode.nodeType() !== Node.ELEMENT_NODE)
549             return false;
550
551         return true;
552     }
553
554     _ondrop(event)
555     {
556         event.preventDefault();
557
558         function callback(error, newNodeId)
559         {
560             if (error)
561                 return;
562
563             this._updateModifiedNodes();
564             var newNode = WI.domManager.nodeForId(newNodeId);
565             if (newNode)
566                 this.selectDOMNode(newNode, true);
567         }
568
569         let treeElement = this.treeElementFromEvent(event);
570         if (this._nodeBeingDragged && treeElement) {
571             let parentNode = null;
572             let anchorNode = null;
573
574             if (treeElement._elementCloseTag) {
575                 // Drop onto closing tag -> insert as last child.
576                 parentNode = treeElement.representedObject;
577             } else {
578                 let dragTargetNode = treeElement.representedObject;
579                 parentNode = dragTargetNode.parentNode;
580                 anchorNode = dragTargetNode;
581             }
582
583             this._nodeBeingDragged.moveTo(parentNode, anchorNode, callback.bind(this));
584         } else {
585             let className = event.dataTransfer.getData(WI.GeneralStyleDetailsSidebarPanel.ToggledClassesDragType);
586             if (className && treeElement)
587                 treeElement.representedObject.toggleClass(className, true);
588         }
589
590         delete this._nodeBeingDragged;
591     }
592
593     _ondragend(event)
594     {
595         event.preventDefault();
596         this._clearDragOverTreeElementMarker();
597         delete this._nodeBeingDragged;
598     }
599
600     _clearDragOverTreeElementMarker()
601     {
602         if (this.dragOverTreeElement) {
603             let element = this.dragOverTreeElement;
604             this.dragOverTreeElement = null;
605
606             element.listItemElement.classList.remove("elements-drag-over");
607             element.updateSelectionArea();
608         }
609     }
610
611     _updateModifiedNodes()
612     {
613         if (this._elementsTreeUpdater)
614             this._elementsTreeUpdater._updateModifiedNodes();
615     }
616
617     _showShadowDOMSettingChanged(event)
618     {
619         var nodeToSelect = this.selectedTreeElement ? this.selectedTreeElement.representedObject : null;
620         while (nodeToSelect) {
621             if (!nodeToSelect.isInShadowTree())
622                 break;
623             nodeToSelect = nodeToSelect.parentNode;
624         }
625
626         this.children.forEach(function(child) {
627             child.updateChildren(true);
628         });
629
630         if (nodeToSelect)
631             this.selectDOMNode(nodeToSelect);
632     }
633
634     _hideElements(event, keyboardShortcut)
635     {
636         if (!this.selectedTreeElement || WI.isEditingAnyField())
637             return;
638
639         event.preventDefault();
640
641         let forceHidden = !this.selectedTreeElements.every((treeElement) => treeElement.isNodeHidden);
642         this.toggleSelectedElementsVisibility(forceHidden);
643     }
644 };
645
646 WI.DOMTreeOutline.Event = {
647     SelectedNodeChanged: "dom-tree-outline-selected-node-changed"
648 };
649
650 WI.DOMTreeOutline.DOMNodeIdDragType = "web-inspector/dom-node-id";