/* * Copyright (C) 2007, 2008, 2013, 2015 Apple Inc. All rights reserved. * Copyright (C) 2008 Matt Lilek * Copyright (C) 2009 Joseph Pecoraro * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions * are met: * * 1. Redistributions of source code must retain the above copyright * notice, this list of conditions and the following disclaimer. * 2. Redistributions in binary form must reproduce the above copyright * notice, this list of conditions and the following disclaimer in the * documentation and/or other materials provided with the distribution. * 3. Neither the name of Apple Inc. ("Apple") nor the names of * its contributors may be used to endorse or promote products derived * from this software without specific prior written permission. * * THIS SOFTWARE IS PROVIDED BY APPLE AND ITS CONTRIBUTORS "AS IS" AND ANY * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE * DISCLAIMED. IN NO EVENT SHALL APPLE OR ITS CONTRIBUTORS BE LIABLE FOR ANY * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF * THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. */ WI.DOMTreeOutline = class DOMTreeOutline extends WI.TreeOutline { constructor({selectable, omitRootDOMNode, excludeRevealElementContextMenu, showInspectedNode, showBadges} = {}) { super(selectable); this.element.addEventListener("mousedown", this._onmousedown.bind(this), false); this.element.addEventListener("mousemove", this._onmousemove.bind(this), false); this.element.addEventListener("mouseout", this._onmouseout.bind(this), false); this.element.addEventListener("dragstart", this._ondragstart.bind(this), false); this.element.addEventListener("dragover", this._ondragover.bind(this), false); this.element.addEventListener("dragleave", this._ondragleave.bind(this), false); this.element.addEventListener("drop", this._ondrop.bind(this), false); this.element.addEventListener("dragend", this._ondragend.bind(this), false); this.element.classList.add("dom", WI.SyntaxHighlightedStyleClassName); this.element.dir = "ltr"; this.element.role = "tree"; this._includeRootDOMNode = !omitRootDOMNode; this._excludeRevealElementContextMenu = excludeRevealElementContextMenu; this._rootDOMNode = null; this._selectedDOMNode = null; this._treeElementsToRemove = null; this._editable = false; this._editing = false; this._visible = false; this._usingLocalDOMNode = false; this._hideElementsKeyboardShortcut = new WI.KeyboardShortcut(null, "H", this._hideElements.bind(this), this.element); this._hideElementsKeyboardShortcut.implicitlyPreventsDefault = false; this._showInspectedNode = !!showInspectedNode; if (this._showInspectedNode) WI.domManager.addEventListener(WI.DOMManager.Event.InspectedNodeChanged, this._handleInspectedNodeChanged, this); this._showBadges = !!showBadges; } // Public wireToDomAgent() { this._elementsTreeUpdater = new WI.DOMTreeUpdater(this); } close() { if (this._elementsTreeUpdater) { this._elementsTreeUpdater.close(); this._elementsTreeUpdater = null; } } setVisible(visible, omitFocus) { this._visible = visible; if (!this._visible) return; this._updateModifiedNodes(); if (this._selectedDOMNode) this._revealAndSelectNode(this._selectedDOMNode, omitFocus); this.update(); } get rootDOMNode() { return this._rootDOMNode; } set rootDOMNode(x) { if (this._rootDOMNode === x) return; this._rootDOMNode = x; this._isXMLMimeType = x && x.isXMLNode(); this.update(); } get isXMLMimeType() { return this._isXMLMimeType; } markAsUsingLocalDOMNode() { this._editable = false; this._usingLocalDOMNode = true; } selectedDOMNode() { return this._selectedDOMNode; } selectDOMNode(node, focus) { if (this._selectedDOMNode === node) { this._revealAndSelectNode(node, !focus); return; } this._selectedDOMNode = node; this._revealAndSelectNode(node, !focus); // The _revealAndSelectNode() method might find a different element if there is inlined text, // and the select() call would change the selectedDOMNode and reenter this setter. So to // avoid calling _selectedNodeChanged() twice, first check if _selectedDOMNode is the same // node as the one passed in. // Note that _revealAndSelectNode will not do anything for a null node. if (!node || this._selectedDOMNode === node) this._selectedNodeChanged(); } get editable() { return this._editable && this.rootDOMNode && !this.rootDOMNode.destroyed; } set editable(x) { this._editable = x; } get editing() { return this._editing; } update() { if (!this.rootDOMNode) return; let selectedTreeElements = this.selectedTreeElements; this.removeChildren(); const elementCloseTag = false; var treeElement; if (this._includeRootDOMNode) { treeElement = new WI.DOMTreeElement(this.rootDOMNode, elementCloseTag, {showBadges: this._showBadges}); treeElement.selectable = this.selectable; this.appendChild(treeElement); } else { // FIXME: this could use findTreeElement to reuse a tree element if it already exists var node = this.rootDOMNode.firstChild; while (node) { treeElement = new WI.DOMTreeElement(node, elementCloseTag, {showBadges: this._showBadges}); treeElement.selectable = this.selectable; this.appendChild(treeElement); node = node.nextSibling; if (treeElement.expandable && !treeElement.expanded) treeElement.expand(); } } if (this._showInspectedNode && WI.domManager.inspectedNode) { let inspectedNodeTreeElement = this.findTreeElement(WI.domManager.inspectedNode); if (inspectedNodeTreeElement) { inspectedNodeTreeElement.reveal(); inspectedNodeTreeElement.listItemElement.classList.add("inspected-node"); } } if (!selectedTreeElements.length) return; // The selection cannot be restored from represented objects alone, // since a closing tag DOMTreeElement has the same represented object // as its parent. selectedTreeElements = selectedTreeElements.map((oldTreeElement) => { let treeElement = this.findTreeElement(oldTreeElement.representedObject); if (treeElement && oldTreeElement.isCloseTag()) { console.assert(treeElement.closeTagTreeElement, "Missing close tag TreeElement.", treeElement); if (treeElement.closeTagTreeElement) treeElement = treeElement.closeTagTreeElement; } return treeElement; }); // It's possible that a previously selected node will no longer exist (e.g. after navigation). selectedTreeElements = selectedTreeElements.filter((x) => !!x); if (!selectedTreeElements.length) return; this.selectTreeElements(selectedTreeElements); if (this.selectedTreeElement) this.selectedTreeElement.reveal(); } updateSelectionArea() { // This will miss updating selection areas used for the hovered tree element and // and those used to show forced pseudo class indicators, but this should be okay. // The hovered element will update when user moves the mouse, and indicators don't need the // selection area height to be accurate since they use ::before to place the indicator. let selectedTreeElements = this.selectedTreeElements; for (let treeElement of selectedTreeElements) treeElement.updateSelectionArea(); } toggleSelectedElementsVisibility(forceHidden) { for (let treeElement of this.selectedTreeElements) treeElement.toggleElementVisibility(forceHidden); } _selectedNodeChanged() { this.dispatchEventToListeners(WI.DOMTreeOutline.Event.SelectedNodeChanged); } findTreeElement(node) { let isAncestorNode = (ancestor, node) => ancestor.isAncestor(node); let parentNode = (node) => node.parentNode; let treeElement = super.findTreeElement(node, isAncestorNode, parentNode); if (!treeElement && node.nodeType() === Node.TEXT_NODE) { // The text node might have been inlined if it was short, so try to find the parent element. treeElement = super.findTreeElement(node.parentNode, isAncestorNode, parentNode); } return treeElement; } createTreeElementFor(node) { var treeElement = this.findTreeElement(node); if (treeElement) return treeElement; if (!node.parentNode) return null; treeElement = this.createTreeElementFor(node.parentNode); if (!treeElement) return null; return treeElement.showChildNode(node); } set suppressRevealAndSelect(x) { if (this._suppressRevealAndSelect === x) return; this._suppressRevealAndSelect = x; } populateContextMenu(contextMenu, event, treeElement) { let subMenus = { add: new WI.ContextSubMenuItem(contextMenu, WI.UIString("Add")), edit: new WI.ContextSubMenuItem(contextMenu, WI.UIString("Edit")), copy: new WI.ContextSubMenuItem(contextMenu, WI.UIString("Copy")), delete: new WI.ContextSubMenuItem(contextMenu, WI.UIString("Delete")), }; if (this.editable && treeElement.selected && this.selectedTreeElements.length > 1) { subMenus.delete.appendItem(WI.UIString("Nodes"), () => { this.ondelete(); }); } if (treeElement.populateDOMNodeContextMenu) treeElement.populateDOMNodeContextMenu(contextMenu, subMenus, event, subMenus); let options = { disallowEditing: !this.editable, usingLocalDOMNode: this._usingLocalDOMNode, excludeRevealElement: this._excludeRevealElementContextMenu, copySubMenu: subMenus.copy, popoverTargetElement: treeElement.statusImageElement, }; if (treeElement.bindRevealDescendantBreakpointsMenuItemHandler) options.revealDescendantBreakpointsMenuItemHandler = treeElement.bindRevealDescendantBreakpointsMenuItemHandler(); WI.appendContextMenuItemsForDOMNode(contextMenu, treeElement.representedObject, options); super.populateContextMenu(contextMenu, event, treeElement); } adjustCollapsedRange() { } ondelete() { if (!this.editable) return false; this._treeElementsToRemove = this.selectedTreeElements; // Reveal all of the elements being deleted so that if the node is hidden (e.g. the parent // is collapsed), we can select its siblings instead of the parent itself. for (let treeElement of this._treeElementsToRemove) treeElement.reveal(); this._selectionController.removeSelectedItems(); let levelMap = new Map; function getLevel(treeElement) { let level = levelMap.get(treeElement); if (isNaN(level)) { level = 0; let current = treeElement; while (current = current.parent) level++; levelMap.set(treeElement, level); } return level; } // Sort in descending order by node level. This ensures that child nodes // are removed before their ancestors. this._treeElementsToRemove.sort((a, b) => getLevel(b) - getLevel(a)); // Track removed elements, since the opening and closing tags for the // same WI.DOMNode can both be selected. let removedDOMNodes = new Set; for (let treeElement of this._treeElementsToRemove) { if (removedDOMNodes.has(treeElement.representedObject)) continue; removedDOMNodes.add(treeElement.representedObject); treeElement.remove(); } this._treeElementsToRemove = null; if (this.selectedTreeElement && !this.selectedTreeElement.isCloseTag()) { console.assert(this.selectedTreeElements.length === 1); this.selectedTreeElement.reveal(); } return true; } // SelectionController delegate overrides selectionControllerPreviousSelectableItem(controller, item) { let treeElement = this.getCachedTreeElement(item); console.assert(treeElement, "Missing TreeElement for representedObject.", item); if (!treeElement) return null; if (this._treeElementsToRemove) { // When deleting, force the SelectionController to check siblings in // the opposite direction before searching up the parent chain. if (!treeElement.previousSelectableSibling && treeElement.nextSelectableSibling) return null; } return super.selectionControllerPreviousSelectableItem(controller, item); } // Protected canSelectTreeElement(treeElement) { if (!super.canSelectTreeElement(treeElement)) return false; let willRemoveAncestorOrSelf = false; if (this._treeElementsToRemove) { while (treeElement && !willRemoveAncestorOrSelf) { willRemoveAncestorOrSelf = this._treeElementsToRemove.includes(treeElement); treeElement = treeElement.parent; } } return !willRemoveAncestorOrSelf; } objectForSelection(treeElement) { if (treeElement instanceof WI.DOMTreeElement && treeElement.isCloseTag()) { // SelectionController requires every selectable item to be unique. // The DOMTreeElement for a close tag has the same represented object // as it's parent (the open tag). Return a proxy object associated // with the tree element for the close tag so it can be selected. if (!treeElement.__closeTagProxyObject) treeElement.__closeTagProxyObject = {__proxyObjectTreeElement: treeElement}; return treeElement.__closeTagProxyObject; } return super.objectForSelection(treeElement); } // Private _revealAndSelectNode(node, omitFocus) { if (!node || this._suppressRevealAndSelect) return; var treeElement = this.createTreeElementFor(node); if (!treeElement) return; treeElement.revealAndSelect(omitFocus); } _onmousedown(event) { let element = this.treeElementFromEvent(event); if (!element || element.isEventWithinDisclosureTriangle(event)) { event.preventDefault(); return; } } _onmousemove(event) { if (this._usingLocalDOMNode) return; let element = this.treeElementFromEvent(event); if (element && this._previousHoveredElement === element) return; if (this._previousHoveredElement) { this._previousHoveredElement.hovered = false; this._previousHoveredElement = null; } if (element instanceof WI.DOMTreeElement) { element.hovered = true; this._previousHoveredElement = element; if (element.representedObject) { // Lazily compute tag-specific tooltips. if (!element.tooltip && element._createTooltipForNode) element._createTooltipForNode(); element.representedObject.highlight(); } else WI.domManager.hideDOMNodeHighlight(); } else WI.domManager.hideDOMNodeHighlight(); } _onmouseout(event) { if (this._usingLocalDOMNode) return; var nodeUnderMouse = document.elementFromPoint(event.pageX, event.pageY); if (nodeUnderMouse && this.element.contains(nodeUnderMouse)) return; if (this._previousHoveredElement) { this._previousHoveredElement.hovered = false; this._previousHoveredElement = null; } WI.domManager.hideDOMNodeHighlight(); } _ondragstart(event) { if (!this.editable) return false; let treeElement = this.treeElementFromEvent(event); if (!treeElement) return false; event.dataTransfer.effectAllowed = "copyMove"; event.dataTransfer.setData(DOMTreeOutline.DOMNodeIdDragType, treeElement.representedObject.id); if (!this._isValidDragSourceOrTarget(treeElement)) return false; if (treeElement.representedObject.nodeName() === "BODY" || treeElement.representedObject.nodeName() === "HEAD") return false; event.dataTransfer.setData("text/plain", treeElement.listItemElement.textContent); this._nodeBeingDragged = treeElement.representedObject; WI.domManager.hideDOMNodeHighlight(); return true; } _ondragover(event) { if (!this.editable) return false; if (event.dataTransfer.types.includes(WI.GeneralStyleDetailsSidebarPanel.ToggledClassesDragType)) { event.preventDefault(); event.dataTransfer.dropEffect = "copy"; return false; } if (!this._nodeBeingDragged) return false; let treeElement = this.treeElementFromEvent(event); if (!this._isValidDragSourceOrTarget(treeElement)) return false; let node = treeElement.representedObject; while (node) { if (node === this._nodeBeingDragged) return false; node = node.parentNode; } this.dragOverTreeElement = treeElement; treeElement.listItemElement.classList.add("elements-drag-over"); treeElement.updateSelectionArea(); event.preventDefault(); event.dataTransfer.dropEffect = "move"; return false; } _ondragleave(event) { if (!this.editable) return false; this._clearDragOverTreeElementMarker(); event.preventDefault(); return false; } _isValidDragSourceOrTarget(treeElement) { if (!treeElement) return false; var node = treeElement.representedObject; if (!(node instanceof WI.DOMNode)) return false; if (!node.parentNode || node.parentNode.nodeType() !== Node.ELEMENT_NODE) return false; return true; } _ondrop(event) { if (!this.editable) return; event.preventDefault(); function callback(error, newNodeId) { if (error) return; this._updateModifiedNodes(); var newNode = WI.domManager.nodeForId(newNodeId); if (newNode) this.selectDOMNode(newNode, true); } let treeElement = this.treeElementFromEvent(event); if (this._nodeBeingDragged && treeElement) { let parentNode = null; let anchorNode = null; if (treeElement._elementCloseTag) { // Drop onto closing tag -> insert as last child. parentNode = treeElement.representedObject; } else { let dragTargetNode = treeElement.representedObject; parentNode = dragTargetNode.parentNode; anchorNode = dragTargetNode; } this._nodeBeingDragged.moveTo(parentNode, anchorNode, callback.bind(this)); } else { let className = event.dataTransfer.getData(WI.GeneralStyleDetailsSidebarPanel.ToggledClassesDragType); if (className && treeElement) treeElement.representedObject.toggleClass(className, true); } delete this._nodeBeingDragged; } _ondragend(event) { if (!this.editable) return; event.preventDefault(); this._clearDragOverTreeElementMarker(); delete this._nodeBeingDragged; } _clearDragOverTreeElementMarker() { if (this.dragOverTreeElement) { let element = this.dragOverTreeElement; this.dragOverTreeElement = null; element.listItemElement.classList.remove("elements-drag-over"); element.updateSelectionArea(); } } _updateModifiedNodes() { if (this._elementsTreeUpdater) this._elementsTreeUpdater._updateModifiedNodes(); } _handleInspectedNodeChanged(event) { let {lastInspectedNode} = event.data; if (lastInspectedNode) { let lastInspectedNodeTreeElement = this.findTreeElement(lastInspectedNode); if (lastInspectedNodeTreeElement) lastInspectedNodeTreeElement.listItemElement.classList.remove("inspected-node"); } let inspectedNodeTreeElement = this.findTreeElement(WI.domManager.inspectedNode); if (inspectedNodeTreeElement) inspectedNodeTreeElement.listItemElement.classList.add("inspected-node"); } _hideElements(event, keyboardShortcut) { if (!this.editable) return; if (!this.selectedTreeElement || WI.isEditingAnyField()) return; event.preventDefault(); let forceHidden = !this.selectedTreeElements.every((treeElement) => treeElement.isNodeHidden); this.toggleSelectedElementsVisibility(forceHidden); } }; WI.DOMTreeOutline.Event = { SelectedNodeChanged: "dom-tree-outline-selected-node-changed" }; WI.DOMTreeOutline.DOMNodeIdDragType = "web-inspector/dom-node-id";