blob: 65bef031f5efd1e9c599f13b11911f725daf5535 [file] [log] [blame]
// Copyright 2016 The Cobalt Authors. All Rights Reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
(function(debugBackend) {
// Attach methods to handle commands in the 'DOM' devtools domain.
// https://chromedevtools.github.io/devtools-protocol/tot/DOM
let commands = debugBackend.DOM = {};
// Creates and returns a new devtools.Node object corresponding to the document
// DOM node, including its children up to a default depth.
// https://chromedevtools.github.io/devtools-protocol/tot/DOM#method-getDocument
commands.getDocument = function(params) {
let result = {};
result.root = _getNodeWithChildren(document, 2);
result.root.documentURL = document.URL;
return JSON.stringify(result);
}
// Creates an array of devtools.Node objects corresponding to the children of
// the DOM node specified in the command params, and returns them via an event.
// A depth may be specified, where a negative depth means to return all
// descendants. If no depth is specified, the default is 1, a single level.
// https://chromedevtools.github.io/devtools-protocol/tot/DOM#method-requestChildNodes
commands.requestChildNodes = function(params) {
let node = commands._findNode(params);
_reportChildren(node, params.depth)
return '{}';
}
// Finds the node corresponding to a remote objectId. Also sends all unreported
// nodes from the root to the requested node as a series of DOM.setChildNodes
// events.
// https://chromedevtools.github.io/devtools-protocol/tot/DOM#method-requestNode
commands.requestNode = function(params) {
let node = commands._findNode(params);
if (!_getNodeId(node)) {
_reportPathFromRoot(node.parentNode);
}
return JSON.stringify({nodeId: _getNodeId(node)});
}
// Returns a Runtime.RemoteObject corresponding to a node.
// https://chromedevtools.github.io/devtools-protocol/tot/DOM#method-resolveNode
commands.resolveNode = function(params) {
let node = commands._findNode(params);
let result = {};
result.object =
JSON.parse(debugBackend.createRemoteObject(node, params.objectGroup));
return JSON.stringify(result);
}
// Creates and returns a devtools.Node object that represents the specified DOM
// node. Adds the node's children up to the specified depth. A negative depth
// will cause all descendants to be added. All returned children are added to
// the node store, and should be reported to the client to maintain integrity of
// the node store holding only nodes the client knows about.
function _getNodeWithChildren(node, depth) {
let result = new devtools.Node(node);
let children = _getChildNodes(node, depth);
if (children.length) {
result.children = children;
}
return result;
}
// Creates and returns an array of devtools.Node objects corresponding to the
// children of the specified DOM node, recursing on each on down to the
// specified depth. All returned children are added to the node store, and
// should be reported to the client to maintain integrity of the node store
// holding only nodes the client knows about.
function _getChildNodes(node, depth) {
let children;
// Special-case the only text child - pretend the children were requested.
if (node.firstChild && !node.firstChild.nextSibling &&
node.firstChild.nodeType === Node.TEXT_NODE) {
children = [node.firstChild];
} else if (depth != 0) { // Negative depth recurses the whole tree.
let child_nodes = Array.from(node.childNodes);
children = child_nodes.filter((child) => !_isWhitespace(child));
// Since we don't report whitespace text nodes they won't be in the node
// store and won't otherwise be observed. However, we still need to observe
// them to report if they get set to non-whitespace.
child_nodes.filter(_isWhitespace)
.forEach((child) => _nodeObserver.observe(child, _observerConfig));
} else {
// Children not requested, so don't set |childrenReported|.
return [];
}
let nodeId = _getNodeId(node);
_nodeStore.get(nodeId).childrenReported = true;
return children.map((child) => _getNodeWithChildren(child, depth - 1));
}
// Whether the children of a node have been reported to the frontend.
function _areChildrenReported(nodeId) {
let nodeInfo = _nodeStore.get(nodeId);
return nodeInfo && nodeInfo.childrenReported;
}
// Sends DOM.setChildNode events to report all nodes not yet known to the client
// from the root down to the specified DOM node.
function _reportPathFromRoot(node) {
// Report nothing if we get to a disconnected root.
if (!node) {
return false;
}
// Stop recursing when we get to a node that has already been reported, and
// report its children first before unwinding the recursion down the tree.
if (_getNodeId(node)) {
_reportChildren(node);
return true;
}
// Recurse up first to report in top-down order, and report nothing if we
// reached a disconnected root.
if (!_reportPathFromRoot(node.parentNode)) {
return false;
}
// All ancestors are now reported, so report the node's children.
_reportChildren(node);
return true;
}
// Sends a DOM.setChildNodes event reporting the children of a DOM node, and
// their children recursively to the requested depth.
function _reportChildren(node, depth) {
let nodeId = _addNode(node);
let children = _getChildNodes(node, depth || 1);
let params = {
parentId: nodeId,
nodes: children,
};
debugBackend.sendEvent('DOM.setChildNodes', JSON.stringify(params));
}
// Finds a DOM node specified by either nodeId or objectId (to get a node from
// its corresponding remote object). This is "exported" as a pseudo-command in
// the DOM domain for other agents to use.
commands._findNode = function(params) {
if (params.nodeId) {
return _nodeStore.get(params.nodeId).node;
}
if (params.objectId) {
return debugBackend.lookupRemoteObjectId(params.objectId);
}
// Either nodeId or objectId must be specified.
return null;
}
// Adds a DOM node to the internal node store and returns a unique id that can
// be used to access it again. If the node is already in the node store, its
// existing id is returned.
function _addNode(node) {
let nodeId = _getNodeId(node);
if (!nodeId) {
nodeId = _nextNodeId++;
// The map goes both ways: DOM node <=> node ID.
_nodeStore.set(nodeId, {node: node});
_nodeStore.set(node, nodeId);
_nodeObserver.observe(node, _observerConfig);
}
return nodeId;
}
// Removes a DOM node and its children from the internal node store.
function _removeNode(node) {
let nodeId = _getNodeId(node);
if (!nodeId) return;
Array.from(node.childNodes).forEach(_removeNode);
_nodeStore.delete(node);
_nodeStore.delete(nodeId);
}
// Returns the node id of a DOM node if it's already known, else undefined.
function _getNodeId(node) {
return _nodeStore.get(node);
}
// MutationObserver callback to send events when nodes are changed.
function _onNodeMutated(mutationsList) {
for (let mutation of mutationsList) {
if (mutation.type === 'childList') {
_onChildListMutated(mutation);
} else if (mutation.type === 'attributes') {
_onAttributesMutated(mutation);
} else if (mutation.type === 'characterData') {
_onCharacterDataMutated(mutation);
}
}
}
function _onChildListMutated(mutation) {
let parentNodeId = _getNodeId(mutation.target);
if (!_areChildrenReported(parentNodeId)) {
// The mutated node hasn't been expanded in the Element tree so we haven't
// yet reported any of its children to the frontend. Just report that the
// number of children changed, without reporting the actual child nodes.
let params = {
nodeId: parentNodeId,
childNodeCount: _countChildNodes(mutation.target),
};
debugBackend.sendEvent('DOM.childNodeCountUpdated', JSON.stringify(params));
} else {
// The mutated node has already been expanded in the Element tree so the
// frontend already knows about its children. Report the removed/inserted
// nodes. Report removed nodes first so that replacements (e.g. setting
// textContent) are coherent.
Array.from(mutation.removedNodes)
.forEach((n) => _onNodeRemoved(parentNodeId, n));
Array.from(mutation.addedNodes)
.forEach((n) => _onNodeInserted(parentNodeId, n));
}
}
// Report to the frontend when a DOM node is inserted.
function _onNodeInserted(parentNodeId, node) {
if (_isWhitespace(node)) return;
// Forget anything we knew about an existing subtree that gets re-attached.
_removeNode(node);
let params = {
parentNodeId: parentNodeId,
previousNodeId: _getNodeId(_getPreviousSibling(node)) || 0,
node: new devtools.Node(node),
};
debugBackend.sendEvent('DOM.childNodeInserted', JSON.stringify(params));
}
// Report to the frontend when a DOM node is removed.
function _onNodeRemoved(parentNodeId, node) {
let nodeId = _getNodeId(node);
if (!parentNodeId || !nodeId) return;
let params = {
parentNodeId: parentNodeId,
nodeId: _getNodeId(node),
};
debugBackend.sendEvent('DOM.childNodeRemoved', JSON.stringify(params));
_removeNode(node);
}
function _onAttributesMutated(mutation) {
let params = {
nodeId: _getNodeId(mutation.target),
name: mutation.attributeName,
};
if (mutation.target.hasAttribute(mutation.attributeName)) {
params.value = mutation.target.getAttribute(mutation.attributeName);
debugBackend.sendEvent('DOM.attributeModified', JSON.stringify(params));
} else {
debugBackend.sendEvent('DOM.attributeRemoved', JSON.stringify(params));
}
}
function _onCharacterDataMutated(mutation) {
let nodeId = _getNodeId(mutation.target);
let parentNodeId = _getNodeId(mutation.target.parentNode);
// If a node changes to/from whitespace, treat it as inserted/removed.
if (!nodeId) {
_onNodeInserted(parentNodeId, mutation.target);
return;
} else if (_isWhitespace(mutation.target)) {
_onNodeRemoved(parentNodeId, mutation.target);
return;
}
let params = {
nodeId: nodeId,
characterData: mutation.target.textContent,
};
debugBackend.sendEvent('DOM.characterDataModified', JSON.stringify(params));
}
// Whether a DOM node is a whitespace-only text node.
// (These are not reported to the frontend.)
function _isWhitespace(node) {
return node.nodeType === Node.TEXT_NODE &&
!(/[^\t\n\r ]/.test(node.nodeValue));
}
// Returns the count of non-whitespace children of a DOM node.
function _countChildNodes(node) {
let countCallback = (count, child) => count + (_isWhitespace(child) ? 0 : 1);
return Array.from(node.childNodes || []).reduce(countCallback, 0);
}
// Returns the non-whitespace previous sibling to the DOM node, if any.
function _getPreviousSibling(node) {
do {
node = node.previousSibling;
} while(node && _isWhitespace(node));
return node;
}
const _nodeObserver = new MutationObserver(_onNodeMutated);
const _observerConfig = {
attributes: true,
childList: true,
characterData: true,
};
let _nodeStore = new Map();
let _nextNodeId = 1;
// Clear the Elements tree in DevTools. It will call DOM.getDocument in response
// to re-load the document.
// https://chromedevtools.github.io/devtools-protocol/tot/DOM#event-documentUpdated
function _documentUpdated() {
debugBackend.sendEvent('DOM.documentUpdated', '{}');
}
// This script is injected when the 'DOM' domain is enabled. When navigating
// with DevTools connected, that happens when restoring debugger state for the
// new WebModule and its associated DebugModule along with its agents. That all
// happens before the new document is loaded, so immediately clear the old
// document from the Elements tree in DevTools while loading the new document.
_documentUpdated();
// Once the document is loaded, make DevTools reload its Elements tree again to
// pick up the new document.
document.addEventListener('load', _documentUpdated);
// Namespace for constructors of types defined in the Devtools protocol.
let devtools = {};
// Constructor for devtools.Node object, which is the type used to return
// information about nodes to the frontend. The associated DOM node is added to
// |nodeStore| since all devtools.Node objects are expected to be reported to
// the frontend, which can reference them later via |nodeId|.
// https://chromedevtools.github.io/devtools-protocol/tot/DOM#type-Node
devtools.Node = function(node) {
this.nodeId = _addNode(node);
this.localName = node.nodeName;
this.nodeName = node.nodeName;
this.nodeType = node.nodeType;
this.nodeValue = node.nodeValue || '';
this.childNodeCount = _countChildNodes(node);
if (node.attributes) {
this.attributes = [];
for (let i = 0; i < node.attributes.length; i++) {
this.attributes.push(node.attributes[i].name);
this.attributes.push(node.attributes[i].value);
}
}
}
// TODO: Pass debugBackend from C++ instead of getting it from the window.
})(window.debugBackend);