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