blob: 93a9a6b2c91746612ec289146f211e080149afbe [file] [log] [blame]
/*
* Copyright (C) 2013, 2015 Apple Inc. All rights reserved.
*
* 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.
*
* THIS SOFTWARE IS PROVIDED BY APPLE INC. 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 INC. 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.DOMTreeContentView = class DOMTreeContentView extends WI.ContentView
{
constructor(representedObject)
{
console.assert(representedObject);
super(representedObject);
this._compositingBordersButtonNavigationItem = new WI.ActivateButtonNavigationItem("layer-borders", WI.UIString("Show compositing borders"), WI.UIString("Hide compositing borders"), "Images/LayerBorders.svg", 13, 13);
this._compositingBordersButtonNavigationItem.addEventListener(WI.ButtonNavigationItem.Event.Clicked, this._toggleCompositingBorders, this);
this._compositingBordersButtonNavigationItem.enabled = !!PageAgent.getCompositingBordersVisible;
this._compositingBordersButtonNavigationItem.visibilityPriority = WI.NavigationItem.VisibilityPriority.Low;
WI.showPaintRectsSetting.addEventListener(WI.Setting.Event.Changed, this._showPaintRectsSettingChanged, this);
this._paintFlashingButtonNavigationItem = new WI.ActivateButtonNavigationItem("paint-flashing", WI.UIString("Enable paint flashing"), WI.UIString("Disable paint flashing"), "Images/Paint.svg", 16, 16);
this._paintFlashingButtonNavigationItem.addEventListener(WI.ButtonNavigationItem.Event.Clicked, this._togglePaintFlashing, this);
this._paintFlashingButtonNavigationItem.enabled = !!PageAgent.setShowPaintRects;
this._paintFlashingButtonNavigationItem.activated = PageAgent.setShowPaintRects && WI.showPaintRectsSetting.value;
this._paintFlashingButtonNavigationItem.visibilityPriority = WI.NavigationItem.VisibilityPriority.Low;
WI.showShadowDOMSetting.addEventListener(WI.Setting.Event.Changed, this._showShadowDOMSettingChanged, this);
this._showsShadowDOMButtonNavigationItem = new WI.ActivateButtonNavigationItem("shows-shadow-DOM", WI.UIString("Show shadow DOM nodes"), WI.UIString("Hide shadow DOM nodes"), "Images/ShadowDOM.svg", 13, 13);
this._showsShadowDOMButtonNavigationItem.addEventListener(WI.ButtonNavigationItem.Event.Clicked, this._toggleShowsShadowDOMSetting, this);
this._showsShadowDOMButtonNavigationItem.visibilityPriority = WI.NavigationItem.VisibilityPriority.Low;
this._showShadowDOMSettingChanged();
WI.showPrintStylesSetting.addEventListener(WI.Setting.Event.Changed, this._showPrintStylesSettingChanged, this);
this._showPrintStylesButtonNavigationItem = new WI.ActivateButtonNavigationItem("print-styles", WI.UIString("Force Print Media Styles"), WI.UIString("Use Default Media Styles"), "Images/Printer.svg", 16, 16);
this._showPrintStylesButtonNavigationItem.addEventListener(WI.ButtonNavigationItem.Event.Clicked, this._togglePrintStylesSetting, this);
this._showPrintStylesButtonNavigationItem.visibilityPriority = WI.NavigationItem.VisibilityPriority.Low;
this._showPrintStylesSettingChanged();
this.element.classList.add("dom-tree");
this.element.addEventListener("click", this._mouseWasClicked.bind(this), false);
this._domTreeOutline = new WI.DOMTreeOutline(true, true, true);
this._domTreeOutline.addEventListener(WI.TreeOutline.Event.ElementAdded, this._domTreeElementAdded, this);
this._domTreeOutline.addEventListener(WI.DOMTreeOutline.Event.SelectedNodeChanged, this._selectedNodeDidChange, this);
this._domTreeOutline.wireToDomAgent();
this._domTreeOutline.editable = true;
this.element.appendChild(this._domTreeOutline.element);
WI.domTreeManager.addEventListener(WI.DOMTreeManager.Event.AttributeModified, this._domNodeChanged, this);
WI.domTreeManager.addEventListener(WI.DOMTreeManager.Event.AttributeRemoved, this._domNodeChanged, this);
WI.domTreeManager.addEventListener(WI.DOMTreeManager.Event.CharacterDataModified, this._domNodeChanged, this);
this._lastSelectedNodePathSetting = new WI.Setting("last-selected-node-path", null);
this._numberOfSearchResults = null;
this._breakpointGutterEnabled = false;
this._pendingBreakpointNodeIdentifiers = new Set;
if (WI.domDebuggerManager.supported) {
WI.debuggerManager.addEventListener(WI.DebuggerManager.Event.BreakpointsEnabledDidChange, this._breakpointsEnabledDidChange, this);
WI.domDebuggerManager.addEventListener(WI.DOMDebuggerManager.Event.DOMBreakpointAdded, this._domBreakpointAddedOrRemoved, this);
WI.domDebuggerManager.addEventListener(WI.DOMDebuggerManager.Event.DOMBreakpointRemoved, this._domBreakpointAddedOrRemoved, this);
WI.DOMBreakpoint.addEventListener(WI.DOMBreakpoint.Event.DisabledStateDidChange, this._domBreakpointDisabledStateDidChange, this);
WI.DOMBreakpoint.addEventListener(WI.DOMBreakpoint.Event.ResolvedStateDidChange, this._domBreakpointResolvedStateDidChange, this);
this._breakpointsEnabledDidChange();
}
}
// Public
get navigationItems()
{
return [this._showPrintStylesButtonNavigationItem, this._showsShadowDOMButtonNavigationItem, this._compositingBordersButtonNavigationItem, this._paintFlashingButtonNavigationItem];
}
get domTreeOutline()
{
return this._domTreeOutline;
}
get scrollableElements()
{
return [this.element];
}
get breakpointGutterEnabled()
{
return this._breakpointGutterEnabled;
}
set breakpointGutterEnabled(flag)
{
if (this._breakpointGutterEnabled === flag)
return;
this._breakpointGutterEnabled = flag;
this.element.classList.toggle("show-gutter", this._breakpointGutterEnabled);
}
shown()
{
super.shown();
this._domTreeOutline.setVisible(true, WI.isConsoleFocused());
this._updateCompositingBordersButtonToMatchPageSettings();
if (!this._domTreeOutline.rootDOMNode)
return;
this._restoreBreakpointsAfterUpdate();
}
hidden()
{
super.hidden();
WI.domTreeManager.hideDOMNodeHighlight();
this._domTreeOutline.setVisible(false);
}
closed()
{
super.closed();
WI.showPaintRectsSetting.removeEventListener(null, null, this);
WI.showShadowDOMSetting.removeEventListener(null, null, this);
WI.debuggerManager.removeEventListener(null, null, this);
WI.domTreeManager.removeEventListener(null, null, this);
WI.domDebuggerManager.removeEventListener(null, null, this);
WI.DOMBreakpoint.removeEventListener(null, null, this);
this._domTreeOutline.close();
this._pendingBreakpointNodeIdentifiers.clear();
}
get selectionPathComponents()
{
var treeElement = this._domTreeOutline.selectedTreeElement;
var pathComponents = [];
while (treeElement && !treeElement.root) {
// The close tag is contained within the element it closes. So skip it since we don't want to
// show the same node twice in the hierarchy.
if (treeElement.isCloseTag()) {
treeElement = treeElement.parent;
continue;
}
var pathComponent = new WI.DOMTreeElementPathComponent(treeElement, treeElement.representedObject);
pathComponent.addEventListener(WI.HierarchicalPathComponent.Event.Clicked, this._pathComponentSelected, this);
pathComponents.unshift(pathComponent);
treeElement = treeElement.parent;
}
return pathComponents;
}
restoreFromCookie(cookie)
{
if (!cookie || !cookie.nodeToSelect)
return;
this.selectAndRevealDOMNode(cookie.nodeToSelect);
// Because nodeToSelect is ephemeral, we don't want to keep
// it around in the back-forward history entries.
cookie.nodeToSelect = undefined;
}
selectAndRevealDOMNode(domNode, preventFocusChange)
{
this._domTreeOutline.selectDOMNode(domNode, !preventFocusChange);
}
handleCopyEvent(event)
{
var selectedDOMNode = this._domTreeOutline.selectedDOMNode();
if (!selectedDOMNode)
return;
event.clipboardData.clearData();
event.preventDefault();
selectedDOMNode.copyNode();
}
get supportsSave()
{
return WI.canArchiveMainFrame();
}
get saveData()
{
function saveHandler(forceSaveAs)
{
WI.archiveMainFrame();
}
return {customSaveHandler: saveHandler};
}
get supportsSearch()
{
return true;
}
get numberOfSearchResults()
{
return this._numberOfSearchResults;
}
get hasPerformedSearch()
{
return this._numberOfSearchResults !== null;
}
set automaticallyRevealFirstSearchResult(reveal)
{
this._automaticallyRevealFirstSearchResult = reveal;
// If we haven't shown a search result yet, reveal one now.
if (this._automaticallyRevealFirstSearchResult && this._numberOfSearchResults > 0) {
if (this._currentSearchResultIndex === -1)
this.revealNextSearchResult();
}
}
performSearch(query)
{
if (this._searchQuery === query)
return;
if (this._searchIdentifier) {
DOMAgent.discardSearchResults(this._searchIdentifier);
this._hideSearchHighlights();
}
this._searchQuery = query;
this._searchIdentifier = null;
this._numberOfSearchResults = null;
this._currentSearchResultIndex = -1;
function searchResultsReady(error, searchIdentifier, resultsCount)
{
if (error)
return;
this._searchIdentifier = searchIdentifier;
this._numberOfSearchResults = resultsCount;
this.dispatchEventToListeners(WI.ContentView.Event.NumberOfSearchResultsDidChange);
this._showSearchHighlights();
if (this._automaticallyRevealFirstSearchResult)
this.revealNextSearchResult();
}
function contextNodesReady(nodeIds)
{
DOMAgent.performSearch(query, nodeIds, searchResultsReady.bind(this));
}
this.getSearchContextNodes(contextNodesReady.bind(this));
}
getSearchContextNodes(callback)
{
// Overwrite this to limit the search to just a subtree.
// Passing undefined will make DOMAgent.performSearch search through all the documents.
callback(undefined);
}
searchCleared()
{
if (this._searchIdentifier) {
DOMAgent.discardSearchResults(this._searchIdentifier);
this._hideSearchHighlights();
}
this._searchQuery = null;
this._searchIdentifier = null;
this._numberOfSearchResults = null;
this._currentSearchResultIndex = -1;
}
revealPreviousSearchResult(changeFocus)
{
if (!this._numberOfSearchResults)
return;
if (this._currentSearchResultIndex > 0)
--this._currentSearchResultIndex;
else
this._currentSearchResultIndex = this._numberOfSearchResults - 1;
this._revealSearchResult(this._currentSearchResultIndex, changeFocus);
}
revealNextSearchResult(changeFocus)
{
if (!this._numberOfSearchResults)
return;
if (this._currentSearchResultIndex + 1 < this._numberOfSearchResults)
++this._currentSearchResultIndex;
else
this._currentSearchResultIndex = 0;
this._revealSearchResult(this._currentSearchResultIndex, changeFocus);
}
// Protected
layout()
{
this._domTreeOutline.updateSelection();
}
// Private
_revealSearchResult(index, changeFocus)
{
console.assert(this._searchIdentifier);
var searchIdentifier = this._searchIdentifier;
function revealResult(error, nodeIdentifiers)
{
if (error)
return;
// Bail if the searchIdentifier changed since we started.
if (this._searchIdentifier !== searchIdentifier)
return;
console.assert(nodeIdentifiers.length === 1);
var domNode = WI.domTreeManager.nodeForId(nodeIdentifiers[0]);
console.assert(domNode);
if (!domNode)
return;
this._domTreeOutline.selectDOMNode(domNode, changeFocus);
var selectedTreeElement = this._domTreeOutline.selectedTreeElement;
if (selectedTreeElement)
selectedTreeElement.emphasizeSearchHighlight();
}
DOMAgent.getSearchResults(this._searchIdentifier, index, index + 1, revealResult.bind(this));
}
_restoreSelectedNodeAfterUpdate(documentURL, defaultNode)
{
if (!WI.domTreeManager.restoreSelectedNodeIsAllowed)
return;
function selectNode(lastSelectedNode)
{
var nodeToFocus = lastSelectedNode;
if (!nodeToFocus)
nodeToFocus = defaultNode;
if (!nodeToFocus)
return;
this._dontSetLastSelectedNodePath = true;
this.selectAndRevealDOMNode(nodeToFocus, WI.isConsoleFocused());
this._dontSetLastSelectedNodePath = false;
// If this wasn't the last selected node, then expand it.
if (!lastSelectedNode && this._domTreeOutline.selectedTreeElement)
this._domTreeOutline.selectedTreeElement.expand();
}
function selectLastSelectedNode(nodeId)
{
if (!WI.domTreeManager.restoreSelectedNodeIsAllowed)
return;
selectNode.call(this, WI.domTreeManager.nodeForId(nodeId));
}
if (documentURL && this._lastSelectedNodePathSetting.value && this._lastSelectedNodePathSetting.value.path && this._lastSelectedNodePathSetting.value.url === documentURL.hash)
WI.domTreeManager.pushNodeByPathToFrontend(this._lastSelectedNodePathSetting.value.path, selectLastSelectedNode.bind(this));
else
selectNode.call(this);
}
_domTreeElementAdded(event)
{
if (!this._pendingBreakpointNodeIdentifiers.size)
return;
let treeElement = event.data.element;
let node = treeElement.representedObject;
console.assert(node instanceof WI.DOMNode);
if (!(node instanceof WI.DOMNode))
return;
if (!this._pendingBreakpointNodeIdentifiers.delete(node.id))
return;
this._updateBreakpointStatus(node.id);
}
_selectedNodeDidChange(event)
{
var selectedDOMNode = this._domTreeOutline.selectedDOMNode();
if (selectedDOMNode && !this._dontSetLastSelectedNodePath)
this._lastSelectedNodePathSetting.value = {url: WI.frameResourceManager.mainFrame.url.hash, path: selectedDOMNode.path()};
if (selectedDOMNode)
WI.domTreeManager.setInspectedNode(selectedDOMNode);
this.dispatchEventToListeners(WI.ContentView.Event.SelectionPathComponentsDidChange);
}
_pathComponentSelected(event)
{
if (!event.data.pathComponent)
return;
console.assert(event.data.pathComponent instanceof WI.DOMTreeElementPathComponent);
console.assert(event.data.pathComponent.domTreeElement instanceof WI.DOMTreeElement);
this._domTreeOutline.selectDOMNode(event.data.pathComponent.domTreeElement.representedObject, true);
}
_domNodeChanged(event)
{
var selectedDOMNode = this._domTreeOutline.selectedDOMNode();
if (selectedDOMNode !== event.data.node)
return;
this.dispatchEventToListeners(WI.ContentView.Event.SelectionPathComponentsDidChange);
}
_mouseWasClicked(event)
{
var anchorElement = event.target.enclosingNodeOrSelfWithNodeName("a");
if (!anchorElement || !anchorElement.href)
return;
// Prevent the link from navigating, since we don't do any navigation by following links normally.
event.preventDefault();
event.stopPropagation();
if (WI.isBeingEdited(anchorElement)) {
// Don't follow the link when it is being edited.
return;
}
// Cancel any pending link navigation.
if (this._followLinkTimeoutIdentifier) {
clearTimeout(this._followLinkTimeoutIdentifier);
delete this._followLinkTimeoutIdentifier;
}
// If this is a double-click (or multiple-click), return early.
if (event.detail > 1)
return;
function followLink()
{
// Since followLink is delayed, the call to WI.openURL can't look at window.event
// to see if the command key is down like it normally would. So we need to do that check
// before calling WI.openURL.
const options = {
alwaysOpenExternally: event ? event.metaKey : false,
lineNumber: anchorElement.lineNumber,
ignoreNetworkTab: true,
ignoreSearchTab: true,
};
WI.openURL(anchorElement.href, this._frame, options);
}
// Start a timeout since this is a single click, if the timeout is canceled before it fires,
// then a double-click happened or another link was clicked.
// FIXME: The duration might be longer or shorter than the user's configured double click speed.
this._followLinkTimeoutIdentifier = setTimeout(followLink.bind(this), 333);
}
_toggleCompositingBorders(event)
{
console.assert(PageAgent.setCompositingBordersVisible);
var activated = !this._compositingBordersButtonNavigationItem.activated;
this._compositingBordersButtonNavigationItem.activated = activated;
PageAgent.setCompositingBordersVisible(activated);
}
_togglePaintFlashing(event)
{
WI.showPaintRectsSetting.value = !WI.showPaintRectsSetting.value;
}
_updateCompositingBordersButtonToMatchPageSettings()
{
var button = this._compositingBordersButtonNavigationItem;
// We need to sync with the page settings since these can be controlled
// in a different way than just using the navigation bar button.
PageAgent.getCompositingBordersVisible(function(error, compositingBordersVisible) {
button.activated = error ? false : compositingBordersVisible;
button.enabled = error !== "unsupported";
});
}
_showPaintRectsSettingChanged(event)
{
console.assert(PageAgent.setShowPaintRects);
this._paintFlashingButtonNavigationItem.activated = WI.showPaintRectsSetting.value;
PageAgent.setShowPaintRects(this._paintFlashingButtonNavigationItem.activated);
}
_showShadowDOMSettingChanged(event)
{
this._showsShadowDOMButtonNavigationItem.activated = WI.showShadowDOMSetting.value;
}
_toggleShowsShadowDOMSetting(event)
{
WI.showShadowDOMSetting.value = !WI.showShadowDOMSetting.value;
}
_showPrintStylesSettingChanged(event)
{
this._showPrintStylesButtonNavigationItem.activated = WI.showPrintStylesSetting.value;
}
_togglePrintStylesSetting(event)
{
WI.showPrintStylesSetting.value = !WI.showPrintStylesSetting.value;
let mediaType = WI.showPrintStylesSetting.value ? "print" : "";
PageAgent.setEmulatedMedia(mediaType);
WI.cssStyleManager.mediaTypeChanged();
}
_showSearchHighlights()
{
console.assert(this._searchIdentifier);
this._searchResultNodes = [];
var searchIdentifier = this._searchIdentifier;
DOMAgent.getSearchResults(this._searchIdentifier, 0, this._numberOfSearchResults, function(error, nodeIdentifiers) {
if (error)
return;
if (this._searchIdentifier !== searchIdentifier)
return;
console.assert(nodeIdentifiers.length === this._numberOfSearchResults);
for (var i = 0; i < nodeIdentifiers.length; ++i) {
var domNode = WI.domTreeManager.nodeForId(nodeIdentifiers[i]);
console.assert(domNode);
if (!domNode)
continue;
this._searchResultNodes.push(domNode);
var treeElement = this._domTreeOutline.findTreeElement(domNode);
console.assert(treeElement);
if (treeElement)
treeElement.highlightSearchResults(this._searchQuery);
}
}.bind(this));
}
_hideSearchHighlights()
{
if (!this._searchResultNodes)
return;
for (var domNode of this._searchResultNodes) {
var treeElement = this._domTreeOutline.findTreeElement(domNode);
if (treeElement)
treeElement.hideSearchHighlights();
}
delete this._searchResultNodes;
}
_domBreakpointAddedOrRemoved(event)
{
let breakpoint = event.data.breakpoint;
this._updateBreakpointStatus(breakpoint.domNodeIdentifier);
}
_domBreakpointDisabledStateDidChange(event)
{
let breakpoint = event.target;
this._updateBreakpointStatus(breakpoint.domNodeIdentifier);
}
_domBreakpointResolvedStateDidChange(event)
{
let breakpoint = event.target;
let nodeIdentifier = breakpoint.domNodeIdentifier || event.data.oldNodeIdentifier;
this._updateBreakpointStatus(nodeIdentifier);
}
_updateBreakpointStatus(nodeIdentifier)
{
let domNode = WI.domTreeManager.nodeForId(nodeIdentifier);
if (!domNode)
return;
let treeElement = this._domTreeOutline.findTreeElement(domNode);
if (!treeElement) {
this._pendingBreakpointNodeIdentifiers.add(nodeIdentifier);
return;
}
let breakpoints = WI.domDebuggerManager.domBreakpointsForNode(domNode);
if (!breakpoints.length) {
treeElement.breakpointStatus = WI.DOMTreeElement.BreakpointStatus.None;
return;
}
this.breakpointGutterEnabled = true;
let disabled = breakpoints.some((item) => item.disabled);
treeElement.breakpointStatus = disabled ? WI.DOMTreeElement.BreakpointStatus.DisabledBreakpoint : WI.DOMTreeElement.BreakpointStatus.Breakpoint;
}
_restoreBreakpointsAfterUpdate()
{
this._pendingBreakpointNodeIdentifiers.clear();
this.breakpointGutterEnabled = false;
let updatedNodes = new Set;
for (let breakpoint of WI.domDebuggerManager.domBreakpoints) {
if (updatedNodes.has(breakpoint.domNodeIdentifier))
continue;
this._updateBreakpointStatus(breakpoint.domNodeIdentifier);
}
}
_breakpointsEnabledDidChange(event)
{
this._domTreeOutline.element.classList.toggle("breakpoints-disabled", !WI.debuggerManager.breakpointsEnabled);
}
};