| /* |
| * Copyright (C) 2007, 2008, 2013, 2015 Apple Inc. All rights reserved. |
| * Copyright (C) 2008 Matt Lilek <webkit@mattlilek.com> |
| * 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} = {}) |
| { |
| 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._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; |
| |
| if (showInspectedNode) |
| WI.domManager.addEventListener(WI.DOMManager.Event.InspectedNodeChanged, this._handleInspectedNodeChanged, this); |
| } |
| |
| // 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(); |
| |
| var treeElement; |
| if (this._includeRootDOMNode) { |
| treeElement = new WI.DOMTreeElement(this.rootDOMNode); |
| 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); |
| treeElement.selectable = this.selectable; |
| this.appendChild(treeElement); |
| node = node.nextSibling; |
| |
| if (treeElement.hasChildren && !treeElement.expanded) |
| treeElement.expand(); |
| } |
| } |
| |
| 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, |
| }; |
| |
| 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) { |
| 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"; |