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