blob: 7185558ad3e0d979bb001db8e1103db82aa9e051 [file] [log] [blame]
/*
* Copyright (C) 2009, 2010 Google Inc. All rights reserved.
* Copyright (C) 2009 Joseph Pecoraro
* Copyright (C) 2013 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:
*
* * Redistributions of source code must retain the above copyright
* notice, this list of conditions and the following disclaimer.
* * 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.
* * Neither the name of Google Inc. 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 THE COPYRIGHT HOLDERS AND 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 THE COPYRIGHT
* OWNER OR 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.
*/
// FIXME: DOMManager lacks advanced multi-target support. (DOMNodes per-target)
WI.DOMManager = class DOMManager extends WI.Object
{
constructor()
{
super();
this._idToDOMNode = {};
this._document = null;
this._attributeLoadNodeIds = {};
this._restoreSelectedNodeIsAllowed = true;
this._loadNodeAttributesTimeout = 0;
this._inspectedNode = null;
this._breakpointsForEventListeners = new Map;
this._hasRequestedDocument = false;
this._pendingDocumentRequestCallbacks = null;
WI.EventBreakpoint.addEventListener(WI.EventBreakpoint.Event.DisabledStateChanged, this._handleEventBreakpointDisabledStateChanged, this);
WI.Frame.addEventListener(WI.Frame.Event.MainResourceDidChange, this._mainResourceDidChange, this);
}
// Target
initializeTarget(target)
{
// FIXME: This should be improved when adding better DOM multi-target support since it is really per-target.
// This currently uses a setTimeout since it doesn't need to happen immediately, and DOMManager uses the
// global DOMAgent to request the document, so we want to make sure we've transitioned the global agents
// to this target if necessary.
if (target.hasDomain("DOM")) {
setTimeout(() => {
this.ensureDocument();
});
if (WI.isEngineeringBuild) {
if (DOMManager.supportsEditingUserAgentShadowTrees({target}))
target.DOMAgent.setAllowEditingUserAgentShadowTrees(WI.settings.engineeringAllowEditingUserAgentShadowTrees.value);
}
}
}
transitionPageTarget()
{
this._documentUpdated();
}
// Static
static buildHighlightConfig(mode)
{
mode = mode || "all";
let highlightConfig = {showInfo: mode === "all"};
if (mode === "all" || mode === "content")
highlightConfig.contentColor = {r: 111, g: 168, b: 220, a: 0.66};
if (mode === "all" || mode === "padding")
highlightConfig.paddingColor = {r: 147, g: 196, b: 125, a: 0.66};
if (mode === "all" || mode === "border")
highlightConfig.borderColor = {r: 255, g: 229, b: 153, a: 0.66};
if (mode === "all" || mode === "margin")
highlightConfig.marginColor = {r: 246, g: 178, b: 107, a: 0.66};
return highlightConfig;
}
static wrapClientCallback(callback)
{
if (!callback)
return null;
return function(error, result) {
if (error)
console.error("Error during DOMAgent operation: " + error);
callback(error ? null : result);
};
}
static supportsDisablingEventListeners()
{
return InspectorBackend.hasCommand("DOM.setEventListenerDisabled");
}
static supportsEventListenerBreakpoints()
{
return InspectorBackend.hasCommand("DOM.setBreakpointForEventListener")
&& InspectorBackend.hasCommand("DOM.removeBreakpointForEventListener");
}
static supportsEditingUserAgentShadowTrees({frontendOnly, target} = {})
{
target = target || InspectorBackend;
return WI.settings.engineeringAllowEditingUserAgentShadowTrees.value
&& (frontendOnly || target.hasCommand("DOM.setAllowEditingUserAgentShadowTrees"));
}
// Public
get inspectedNode() { return this._inspectedNode; }
get eventListenerBreakpoints()
{
return Array.from(this._breakpointsForEventListeners.values());
}
requestDocument(callback)
{
if (this._document) {
callback(this._document);
return;
}
if (this._pendingDocumentRequestCallbacks)
this._pendingDocumentRequestCallbacks.push(callback);
else
this._pendingDocumentRequestCallbacks = [callback];
if (this._hasRequestedDocument)
return;
if (!WI.pageTarget)
return;
if (!WI.pageTarget.hasDomain("DOM"))
return;
this._hasRequestedDocument = true;
WI.pageTarget.DOMAgent.getDocument((error, root) => {
if (!error)
this._setDocument(root);
for (let callback of this._pendingDocumentRequestCallbacks)
callback(this._document);
this._pendingDocumentRequestCallbacks = null;
});
}
ensureDocument()
{
this.requestDocument(function(){});
}
pushNodeToFrontend(objectId, callback)
{
let target = WI.assumingMainTarget();
this._dispatchWhenDocumentAvailable((callbackWrapper) => {
target.DOMAgent.requestNode(objectId, callbackWrapper);
}, callback);
}
pushNodeByPathToFrontend(path, callback)
{
let target = WI.assumingMainTarget();
this._dispatchWhenDocumentAvailable((callbackWrapper) => {
target.DOMAgent.pushNodeByPathToFrontend(path, callbackWrapper);
}, callback);
}
// DOMObserver
didAddEventListener(nodeId)
{
let node = this._idToDOMNode[nodeId];
if (!node)
return;
node.dispatchEventToListeners(WI.DOMNode.Event.EventListenersChanged);
}
willRemoveEventListener(nodeId)
{
let node = this._idToDOMNode[nodeId];
if (!node)
return;
node.dispatchEventToListeners(WI.DOMNode.Event.EventListenersChanged);
}
didFireEvent(nodeId, eventName, timestamp, data)
{
let node = this._idToDOMNode[nodeId];
if (!node)
return;
node.didFireEvent(eventName, timestamp, data);
}
powerEfficientPlaybackStateChanged(nodeId, timestamp, isPowerEfficient)
{
let node = this._idToDOMNode[nodeId];
if (!node)
return;
node.powerEfficientPlaybackStateChanged(timestamp, isPowerEfficient);
}
// Private
_dispatchWhenDocumentAvailable(func, callback)
{
var callbackWrapper = DOMManager.wrapClientCallback(callback);
function onDocumentAvailable()
{
if (this._document)
func(callbackWrapper);
else {
if (callbackWrapper)
callbackWrapper("No document");
}
}
this.requestDocument(onDocumentAvailable.bind(this));
}
_attributeModified(nodeId, name, value)
{
var node = this._idToDOMNode[nodeId];
if (!node)
return;
node._setAttribute(name, value);
this.dispatchEventToListeners(WI.DOMManager.Event.AttributeModified, {node, name});
node.dispatchEventToListeners(WI.DOMNode.Event.AttributeModified, {name});
}
_attributeRemoved(nodeId, name)
{
var node = this._idToDOMNode[nodeId];
if (!node)
return;
node._removeAttribute(name);
this.dispatchEventToListeners(WI.DOMManager.Event.AttributeRemoved, {node, name});
node.dispatchEventToListeners(WI.DOMNode.Event.AttributeRemoved, {name});
}
_inlineStyleInvalidated(nodeIds)
{
for (var nodeId of nodeIds)
this._attributeLoadNodeIds[nodeId] = true;
if (this._loadNodeAttributesTimeout)
return;
this._loadNodeAttributesTimeout = setTimeout(this._loadNodeAttributes.bind(this), 0);
}
_loadNodeAttributes()
{
function callback(nodeId, error, attributes)
{
if (error) {
console.error("Error during DOMAgent operation: " + error);
return;
}
var node = this._idToDOMNode[nodeId];
if (node) {
node._setAttributesPayload(attributes);
this.dispatchEventToListeners(WI.DOMManager.Event.AttributeModified, {node, name: "style"});
node.dispatchEventToListeners(WI.DOMNode.Event.AttributeModified, {name: "style"});
}
}
this._loadNodeAttributesTimeout = 0;
let target = WI.assumingMainTarget();
for (var nodeId in this._attributeLoadNodeIds) {
if (!(nodeId in this._idToDOMNode))
continue;
var nodeIdAsNumber = parseInt(nodeId);
target.DOMAgent.getAttributes(nodeIdAsNumber, callback.bind(this, nodeIdAsNumber));
}
this._attributeLoadNodeIds = {};
}
_characterDataModified(nodeId, newValue)
{
var node = this._idToDOMNode[nodeId];
node._nodeValue = newValue;
this.dispatchEventToListeners(WI.DOMManager.Event.CharacterDataModified, {node});
}
nodeForId(nodeId)
{
return this._idToDOMNode[nodeId] || null;
}
_documentUpdated()
{
this._setDocument(null);
}
_setDocument(payload)
{
for (let node of Object.values(this._idToDOMNode))
node.markDestroyed();
this._idToDOMNode = {};
for (let breakpoint of this._breakpointsForEventListeners.values())
WI.domDebuggerManager.dispatchEventToListeners(WI.DOMDebuggerManager.Event.EventBreakpointRemoved, {breakpoint});
this._breakpointsForEventListeners.clear();
let newDocument = null;
if (payload && "nodeId" in payload)
newDocument = new WI.DOMNode(this, null, false, payload);
if (this._document === newDocument)
return;
this._document = newDocument;
if (!this._document)
this._hasRequestedDocument = false;
this.dispatchEventToListeners(WI.DOMManager.Event.DocumentUpdated, {document: this._document});
}
_setDetachedRoot(payload)
{
new WI.DOMNode(this, null, false, payload);
}
_setChildNodes(parentId, payloads)
{
if (!parentId && payloads.length) {
this._setDetachedRoot(payloads[0]);
return;
}
var parent = this._idToDOMNode[parentId];
parent._setChildrenPayload(payloads);
}
_childNodeCountUpdated(nodeId, newValue)
{
var node = this._idToDOMNode[nodeId];
node.childNodeCount = newValue;
this.dispatchEventToListeners(WI.DOMManager.Event.ChildNodeCountUpdated, node);
}
_childNodeInserted(parentId, prevId, payload)
{
var parent = this._idToDOMNode[parentId];
var prev = this._idToDOMNode[prevId];
var node = parent._insertChild(prev, payload);
this._idToDOMNode[node.id] = node;
this.dispatchEventToListeners(WI.DOMManager.Event.NodeInserted, {node, parent});
}
_childNodeRemoved(parentId, nodeId)
{
var parent = this._idToDOMNode[parentId];
var node = this._idToDOMNode[nodeId];
parent._removeChild(node);
this._unbind(node);
this.dispatchEventToListeners(WI.DOMManager.Event.NodeRemoved, {node, parent});
}
_customElementStateChanged(elementId, newState)
{
const node = this._idToDOMNode[elementId];
node._customElementState = newState;
this.dispatchEventToListeners(WI.DOMManager.Event.CustomElementStateChanged, {node});
}
_pseudoElementAdded(parentId, pseudoElement)
{
var parent = this._idToDOMNode[parentId];
if (!parent)
return;
var node = new WI.DOMNode(this, parent.ownerDocument, false, pseudoElement);
node.parentNode = parent;
this._idToDOMNode[node.id] = node;
console.assert(!parent.pseudoElements().get(node.pseudoType()));
parent.pseudoElements().set(node.pseudoType(), node);
this.dispatchEventToListeners(WI.DOMManager.Event.NodeInserted, {node, parent});
}
_pseudoElementRemoved(parentId, pseudoElementId)
{
var pseudoElement = this._idToDOMNode[pseudoElementId];
if (!pseudoElement)
return;
var parent = pseudoElement.parentNode;
console.assert(parent);
console.assert(parent.id === parentId);
if (!parent)
return;
parent._removeChild(pseudoElement);
this._unbind(pseudoElement);
this.dispatchEventToListeners(WI.DOMManager.Event.NodeRemoved, {node: pseudoElement, parent});
}
_unbind(node)
{
node.markDestroyed();
delete this._idToDOMNode[node.id];
for (let i = 0; node.children && i < node.children.length; ++i)
this._unbind(node.children[i]);
let templateContent = node.templateContent();
if (templateContent)
this._unbind(templateContent);
for (let pseudoElement of node.pseudoElements().values())
this._unbind(pseudoElement);
// FIXME: Handle shadow roots.
}
get restoreSelectedNodeIsAllowed()
{
return this._restoreSelectedNodeIsAllowed;
}
inspectElement(nodeId, options = {})
{
var node = this._idToDOMNode[nodeId];
if (!node || !node.ownerDocument)
return;
// This code path is hit by "Reveal in DOM Tree" and clicking element links/console widgets.
// Unless overridden by callers, assume that this is navigation is initiated by a Inspect mode.
let initiatorHint = options.initiatorHint || WI.TabBrowser.TabNavigationInitiator.Inspect;
this.dispatchEventToListeners(WI.DOMManager.Event.DOMNodeWasInspected, {node, initiatorHint});
this._inspectModeEnabled = false;
this.dispatchEventToListeners(WI.DOMManager.Event.InspectModeStateChanged);
}
inspectNodeObject(remoteObject)
{
this._restoreSelectedNodeIsAllowed = false;
function nodeAvailable(nodeId)
{
remoteObject.release();
console.assert(nodeId);
if (!nodeId)
return;
this.inspectElement(nodeId);
// Re-resolve the node in the console's object group when adding to the console.
let domNode = this.nodeForId(nodeId);
WI.RemoteObject.resolveNode(domNode, WI.RuntimeManager.ConsoleObjectGroup).then((remoteObject) => {
const specialLogStyles = true;
const shouldRevealConsole = false;
WI.consoleLogViewController.appendImmediateExecutionWithResult(WI.UIString("Selected Element"), remoteObject, specialLogStyles, shouldRevealConsole);
});
}
remoteObject.pushNodeToFrontend(nodeAvailable.bind(this));
}
highlightDOMNodeList(nodes, mode)
{
let target = WI.assumingMainTarget();
// COMPATIBILITY (iOS 11): DOM.highlightNodeList did not exist.
if (!target.hasCommand("DOM.highlightNodeList"))
return;
if (this._hideDOMNodeHighlightTimeout) {
clearTimeout(this._hideDOMNodeHighlightTimeout);
this._hideDOMNodeHighlightTimeout = undefined;
}
let nodeIds = [];
for (let node of nodes) {
console.assert(node instanceof WI.DOMNode, node);
console.assert(!node.destroyed, node);
if (node.destroyed)
continue;
nodeIds.push(node.id);
}
target.DOMAgent.highlightNodeList(nodeIds, DOMManager.buildHighlightConfig(mode));
}
highlightSelector(selectorText, frameId, mode)
{
let target = WI.assumingMainTarget();
// COMPATIBILITY (iOS 8): DOM.highlightSelector did not exist.
if (!target.hasCommand("DOM.highlightSelector"))
return;
if (this._hideDOMNodeHighlightTimeout) {
clearTimeout(this._hideDOMNodeHighlightTimeout);
this._hideDOMNodeHighlightTimeout = undefined;
}
target.DOMAgent.highlightSelector(DOMManager.buildHighlightConfig(mode), selectorText, frameId);
}
highlightRect(rect, usePageCoordinates)
{
let target = WI.assumingMainTarget();
target.DOMAgent.highlightRect.invoke({
x: rect.x,
y: rect.y,
width: rect.width,
height: rect.height,
color: {r: 111, g: 168, b: 220, a: 0.66},
outlineColor: {r: 255, g: 229, b: 153, a: 0.66},
usePageCoordinates
});
}
hideDOMNodeHighlight()
{
let target = WI.assumingMainTarget();
target.DOMAgent.hideHighlight();
}
highlightDOMNodeForTwoSeconds(nodeId)
{
let node = this._idToDOMNode[nodeId];
if (!node)
return;
node.highlight();
this._hideDOMNodeHighlightTimeout = setTimeout(this.hideDOMNodeHighlight.bind(this), 2000);
}
get inspectModeEnabled()
{
return this._inspectModeEnabled;
}
set inspectModeEnabled(enabled)
{
if (enabled === this._inspectModeEnabled)
return;
let target = WI.assumingMainTarget();
let commandArguments = {
enabled,
highlightConfig: DOMManager.buildHighlightConfig(),
showRulers: WI.settings.showRulersDuringElementSelection.value,
};
target.DOMAgent.setInspectModeEnabled.invoke(commandArguments, (error) => {
if (error) {
WI.reportInternalError(error);
return;
}
this._inspectModeEnabled = enabled;
this.dispatchEventToListeners(WI.DOMManager.Event.InspectModeStateChanged);
});
}
setInspectedNode(node)
{
console.assert(node instanceof WI.DOMNode);
if (node === this._inspectedNode)
return;
console.assert(!node.destroyed, node);
if (node.destroyed)
return;
let callback = (error) => {
console.assert(!error, error);
if (error)
return;
let lastInspectedNode = this._inspectedNode;
this._inspectedNode = node;
this.dispatchEventToListeners(WI.DOMManager.Event.InspectedNodeChanged, {lastInspectedNode});
};
let target = WI.assumingMainTarget();
// COMPATIBILITY (iOS 11): DOM.setInspectedNode did not exist.
if (!target.hasCommand("DOM.setInspectedNode")) {
target.ConsoleAgent.addInspectedNode(node.id, callback);
return;
}
target.DOMAgent.setInspectedNode(node.id, callback);
}
getSupportedEventNames(callback)
{
let target = WI.assumingMainTarget();
if (!target.hasCommand("DOM.getSupportedEventNames"))
return Promise.resolve(new Set);
if (!this._getSupportedEventNamesPromise) {
this._getSupportedEventNamesPromise = target.DOMAgent.getSupportedEventNames()
.then(({eventNames}) => new Set(eventNames));
}
return this._getSupportedEventNamesPromise;
}
setEventListenerDisabled(eventListener, disabled)
{
let target = WI.assumingMainTarget();
target.DOMAgent.setEventListenerDisabled(eventListener.eventListenerId, disabled);
}
setBreakpointForEventListener(eventListener)
{
let breakpoint = this._breakpointsForEventListeners.get(eventListener.eventListenerId);
if (breakpoint) {
console.assert(breakpoint.disabled);
breakpoint.disabled = false;
return;
}
breakpoint = new WI.EventBreakpoint(WI.EventBreakpoint.Type.Listener, {eventName: eventListener.type, eventListener});
console.assert(!breakpoint.disabled);
this._breakpointsForEventListeners.set(eventListener.eventListenerId, breakpoint);
for (let target of WI.targets) {
if (target.hasDomain("DOM"))
this._updateEventBreakpoint(breakpoint, target);
}
WI.domDebuggerManager.dispatchEventToListeners(WI.DOMDebuggerManager.Event.EventBreakpointAdded, {breakpoint});
}
removeBreakpointForEventListener(eventListener)
{
let breakpoint = this._breakpointsForEventListeners.take(eventListener.eventListenerId);
console.assert(breakpoint);
for (let target of WI.targets) {
if (target.hasDomain("DOM"))
target.DOMAgent.removeBreakpointForEventListener(eventListener.eventListenerId);
}
WI.domDebuggerManager.dispatchEventToListeners(WI.DOMDebuggerManager.Event.EventBreakpointRemoved, {breakpoint});
}
removeEventListenerBreakpointsForNode(domNode)
{
for (let breakpoint of Array.from(this._breakpointsForEventListeners.values())) {
let eventListener = breakpoint.eventListener;
if (eventListener.nodeId === domNode.id)
this.removeBreakpointForEventListener(eventListener);
}
}
breakpointForEventListenerId(eventListenerId)
{
return this._breakpointsForEventListeners.get(eventListenerId) || null;
}
// Private
_updateEventBreakpoint(breakpoint, target)
{
let eventListener = breakpoint.eventListener;
console.assert(eventListener);
if (breakpoint.disabled)
target.DOMAgent.removeBreakpointForEventListener(eventListener.eventListenerId);
else {
if (!WI.debuggerManager.breakpointsDisabledTemporarily)
WI.debuggerManager.breakpointsEnabled = true;
target.DOMAgent.setBreakpointForEventListener(eventListener.eventListenerId);
}
}
_handleEventBreakpointDisabledStateChanged(event)
{
let breakpoint = event.target;
// Non-specific event listener breakpoints are handled by `DOMDebuggerManager`.
if (!breakpoint.eventListener)
return;
for (let target of WI.targets) {
if (target.hasDomain("DOM"))
this._updateEventBreakpoint(breakpoint, target);
}
}
_mainResourceDidChange(event)
{
if (!event.target.isMainFrame())
return;
this._restoreSelectedNodeIsAllowed = true;
this.ensureDocument();
}
};
WI.DOMManager.Event = {
AttributeModified: "dom-manager-attribute-modified",
AttributeRemoved: "dom-manager-attribute-removed",
CharacterDataModified: "dom-manager-character-data-modified",
NodeInserted: "dom-manager-node-inserted",
NodeRemoved: "dom-manager-node-removed",
CustomElementStateChanged: "dom-manager-custom-element-state-changed",
DocumentUpdated: "dom-manager-document-updated",
ChildNodeCountUpdated: "dom-manager-child-node-count-updated",
DOMNodeWasInspected: "dom-manager-dom-node-was-inspected",
InspectModeStateChanged: "dom-manager-inspect-mode-state-changed",
InspectedNodeChanged: "dom-manager-inspected-node-changed",
};