From 5f8de423f190bbb79a62f804151bc24824fa32d8 Mon Sep 17 00:00:00 2001 From: "Matt A. Tobin" Date: Fri, 2 Feb 2018 04:16:08 -0500 Subject: Add m-esr52 at 52.6.0 --- devtools/server/actors/inspector.js | 3186 +++++++++++++++++++++++++++++++++++ 1 file changed, 3186 insertions(+) create mode 100644 devtools/server/actors/inspector.js (limited to 'devtools/server/actors/inspector.js') diff --git a/devtools/server/actors/inspector.js b/devtools/server/actors/inspector.js new file mode 100644 index 000000000..20a227a40 --- /dev/null +++ b/devtools/server/actors/inspector.js @@ -0,0 +1,3186 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +"use strict"; + +/** + * Here's the server side of the remote inspector. + * + * The WalkerActor is the client's view of the debuggee's DOM. It's gives + * the client a tree of NodeActor objects. + * + * The walker presents the DOM tree mostly unmodified from the source DOM + * tree, but with a few key differences: + * + * - Empty text nodes are ignored. This is pretty typical of developer + * tools, but maybe we should reconsider that on the server side. + * - iframes with documents loaded have the loaded document as the child, + * the walker provides one big tree for the whole document tree. + * + * There are a few ways to get references to NodeActors: + * + * - When you first get a WalkerActor reference, it comes with a free + * reference to the root document's node. + * - Given a node, you can ask for children, siblings, and parents. + * - You can issue querySelector and querySelectorAll requests to find + * other elements. + * - Requests that return arbitrary nodes from the tree (like querySelector + * and querySelectorAll) will also return any nodes the client hasn't + * seen in order to have a complete set of parents. + * + * Once you have a NodeFront, you should be able to answer a few questions + * without further round trips, like the node's name, namespace/tagName, + * attributes, etc. Other questions (like a text node's full nodeValue) + * might require another round trip. + * + * The protocol guarantees that the client will always know the parent of + * any node that is returned by the server. This means that some requests + * (like querySelector) will include the extra nodes needed to satisfy this + * requirement. The client keeps track of this parent relationship, so the + * node fronts form a tree that is a subset of the actual DOM tree. + * + * + * We maintain this guarantee to support the ability to release subtrees on + * the client - when a node is disconnected from the DOM tree we want to be + * able to free the client objects for all the children nodes. + * + * So to be able to answer "all the children of a given node that we have + * seen on the client side", we guarantee that every time we've seen a node, + * we connect it up through its parents. + */ + +const {Cc, Ci, Cu} = require("chrome"); +const Services = require("Services"); +const protocol = require("devtools/shared/protocol"); +const {LayoutActor} = require("devtools/server/actors/layout"); +const {LongStringActor} = require("devtools/server/actors/string"); +const promise = require("promise"); +const {Task} = require("devtools/shared/task"); +const events = require("sdk/event/core"); +const {WalkerSearch} = require("devtools/server/actors/utils/walker-search"); +const {PageStyleActor, getFontPreviewData} = require("devtools/server/actors/styles"); +const { + HighlighterActor, + CustomHighlighterActor, + isTypeRegistered, + HighlighterEnvironment +} = require("devtools/server/actors/highlighters"); +const {EyeDropper} = require("devtools/server/actors/highlighters/eye-dropper"); +const { + isAnonymous, + isNativeAnonymous, + isXBLAnonymous, + isShadowAnonymous, + getFrameElement +} = require("devtools/shared/layout/utils"); +const {getLayoutChangesObserver, releaseLayoutChangesObserver} = require("devtools/server/actors/reflow"); +const nodeFilterConstants = require("devtools/shared/dom-node-filter-constants"); + +const {EventParsers} = require("devtools/server/event-parsers"); +const {nodeSpec, nodeListSpec, walkerSpec, inspectorSpec} = require("devtools/shared/specs/inspector"); + +const FONT_FAMILY_PREVIEW_TEXT = "The quick brown fox jumps over the lazy dog"; +const FONT_FAMILY_PREVIEW_TEXT_SIZE = 20; +const PSEUDO_CLASSES = [":hover", ":active", ":focus"]; +const HIDDEN_CLASS = "__fx-devtools-hide-shortcut__"; +const XHTML_NS = "http://www.w3.org/1999/xhtml"; +const XUL_NS = "http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"; +const IMAGE_FETCHING_TIMEOUT = 500; +const RX_FUNC_NAME = + /((var|const|let)\s+)?([\w$.]+\s*[:=]\s*)*(function)?\s*\*?\s*([\w$]+)?\s*$/; + +// The possible completions to a ':' with added score to give certain values +// some preference. +const PSEUDO_SELECTORS = [ + [":active", 1], + [":hover", 1], + [":focus", 1], + [":visited", 0], + [":link", 0], + [":first-letter", 0], + [":first-child", 2], + [":before", 2], + [":after", 2], + [":lang(", 0], + [":not(", 3], + [":first-of-type", 0], + [":last-of-type", 0], + [":only-of-type", 0], + [":only-child", 2], + [":nth-child(", 3], + [":nth-last-child(", 0], + [":nth-of-type(", 0], + [":nth-last-of-type(", 0], + [":last-child", 2], + [":root", 0], + [":empty", 0], + [":target", 0], + [":enabled", 0], + [":disabled", 0], + [":checked", 1], + ["::selection", 0] +]; + +var HELPER_SHEET = ` + .__fx-devtools-hide-shortcut__ { + visibility: hidden !important; + } + + :-moz-devtools-highlighted { + outline: 2px dashed #F06!important; + outline-offset: -2px !important; + } +`; + +const flags = require("devtools/shared/flags"); + +loader.lazyRequireGetter(this, "DevToolsUtils", + "devtools/shared/DevToolsUtils"); + +loader.lazyRequireGetter(this, "AsyncUtils", "devtools/shared/async-utils"); + +loader.lazyGetter(this, "DOMParser", function () { + return Cc["@mozilla.org/xmlextras/domparser;1"] + .createInstance(Ci.nsIDOMParser); +}); + +loader.lazyGetter(this, "eventListenerService", function () { + return Cc["@mozilla.org/eventlistenerservice;1"] + .getService(Ci.nsIEventListenerService); +}); + +loader.lazyGetter(this, "CssLogic", () => require("devtools/server/css-logic").CssLogic); + +/** + * We only send nodeValue up to a certain size by default. This stuff + * controls that size. + */ +exports.DEFAULT_VALUE_SUMMARY_LENGTH = 50; +var gValueSummaryLength = exports.DEFAULT_VALUE_SUMMARY_LENGTH; + +exports.getValueSummaryLength = function () { + return gValueSummaryLength; +}; + +exports.setValueSummaryLength = function (val) { + gValueSummaryLength = val; +}; + +// When the user selects a node to inspect in e10s, the parent process +// has a CPOW that wraps the node being inspected. It uses the +// message manager to send this node to the child, which stores the +// node in gInspectingNode. Then a findInspectingNode request is sent +// over the remote debugging protocol, and gInspectingNode is returned +// to the parent as a NodeFront. +var gInspectingNode = null; + +// We expect this function to be called from the child.js frame script +// when it receives the node to be inspected over the message manager. +exports.setInspectingNode = function (val) { + gInspectingNode = val; +}; + +/** + * Returns the properly cased version of the node's tag name, which can be + * used when displaying said name in the UI. + * + * @param {Node} rawNode + * Node for which we want the display name + * @return {String} + * Properly cased version of the node tag name + */ +const getNodeDisplayName = function (rawNode) { + if (rawNode.nodeName && !rawNode.localName) { + // The localName & prefix APIs have been moved from the Node interface to the Element + // interface. Use Node.nodeName as a fallback. + return rawNode.nodeName; + } + return (rawNode.prefix ? rawNode.prefix + ":" : "") + rawNode.localName; +}; +exports.getNodeDisplayName = getNodeDisplayName; + +/** + * Server side of the node actor. + */ +var NodeActor = exports.NodeActor = protocol.ActorClassWithSpec(nodeSpec, { + initialize: function (walker, node) { + protocol.Actor.prototype.initialize.call(this, null); + this.walker = walker; + this.rawNode = node; + this._eventParsers = new EventParsers().parsers; + + // Storing the original display of the node, to track changes when reflows + // occur + this.wasDisplayed = this.isDisplayed; + }, + + toString: function () { + return "[NodeActor " + this.actorID + " for " + + this.rawNode.toString() + "]"; + }, + + /** + * Instead of storing a connection object, the NodeActor gets its connection + * from its associated walker. + */ + get conn() { + return this.walker.conn; + }, + + isDocumentElement: function () { + return this.rawNode.ownerDocument && + this.rawNode.ownerDocument.documentElement === this.rawNode; + }, + + destroy: function () { + protocol.Actor.prototype.destroy.call(this); + + if (this.mutationObserver) { + if (!Cu.isDeadWrapper(this.mutationObserver)) { + this.mutationObserver.disconnect(); + } + this.mutationObserver = null; + } + this.rawNode = null; + this.walker = null; + }, + + // Returns the JSON representation of this object over the wire. + form: function (detail) { + if (detail === "actorid") { + return this.actorID; + } + + let parentNode = this.walker.parentNode(this); + let inlineTextChild = this.walker.inlineTextChild(this); + + let form = { + actor: this.actorID, + baseURI: this.rawNode.baseURI, + parent: parentNode ? parentNode.actorID : undefined, + nodeType: this.rawNode.nodeType, + namespaceURI: this.rawNode.namespaceURI, + nodeName: this.rawNode.nodeName, + nodeValue: this.rawNode.nodeValue, + displayName: getNodeDisplayName(this.rawNode), + numChildren: this.numChildren, + inlineTextChild: inlineTextChild ? inlineTextChild.form() : undefined, + + // doctype attributes + name: this.rawNode.name, + publicId: this.rawNode.publicId, + systemId: this.rawNode.systemId, + + attrs: this.writeAttrs(), + isBeforePseudoElement: this.isBeforePseudoElement, + isAfterPseudoElement: this.isAfterPseudoElement, + isAnonymous: isAnonymous(this.rawNode), + isNativeAnonymous: isNativeAnonymous(this.rawNode), + isXBLAnonymous: isXBLAnonymous(this.rawNode), + isShadowAnonymous: isShadowAnonymous(this.rawNode), + pseudoClassLocks: this.writePseudoClassLocks(), + + isDisplayed: this.isDisplayed, + isInHTMLDocument: this.rawNode.ownerDocument && + this.rawNode.ownerDocument.contentType === "text/html", + hasEventListeners: this._hasEventListeners, + }; + + if (this.isDocumentElement()) { + form.isDocumentElement = true; + } + + // Add an extra API for custom properties added by other + // modules/extensions. + form.setFormProperty = (name, value) => { + if (!form.props) { + form.props = {}; + } + form.props[name] = value; + }; + + // Fire an event so, other modules can create its own properties + // that should be passed to the client (within the form.props field). + events.emit(NodeActor, "form", { + target: this, + data: form + }); + + return form; + }, + + /** + * Watch the given document node for mutations using the DOM observer + * API. + */ + watchDocument: function (callback) { + let node = this.rawNode; + // Create the observer on the node's actor. The node will make sure + // the observer is cleaned up when the actor is released. + let observer = new node.defaultView.MutationObserver(callback); + observer.mergeAttributeRecords = true; + observer.observe(node, { + nativeAnonymousChildList: true, + attributes: true, + characterData: true, + characterDataOldValue: true, + childList: true, + subtree: true + }); + this.mutationObserver = observer; + }, + + get isBeforePseudoElement() { + return this.rawNode.nodeName === "_moz_generated_content_before"; + }, + + get isAfterPseudoElement() { + return this.rawNode.nodeName === "_moz_generated_content_after"; + }, + + // Estimate the number of children that the walker will return without making + // a call to children() if possible. + get numChildren() { + // For pseudo elements, childNodes.length returns 1, but the walker + // will return 0. + if (this.isBeforePseudoElement || this.isAfterPseudoElement) { + return 0; + } + + let rawNode = this.rawNode; + let numChildren = rawNode.childNodes.length; + let hasAnonChildren = rawNode.nodeType === Ci.nsIDOMNode.ELEMENT_NODE && + rawNode.ownerDocument.getAnonymousNodes(rawNode); + + let hasContentDocument = rawNode.contentDocument; + let hasSVGDocument = rawNode.getSVGDocument && rawNode.getSVGDocument(); + if (numChildren === 0 && (hasContentDocument || hasSVGDocument)) { + // This might be an iframe with virtual children. + numChildren = 1; + } + + // Normal counting misses ::before/::after. Also, some anonymous children + // may ultimately be skipped, so we have to consult with the walker. + if (numChildren === 0 || hasAnonChildren) { + numChildren = this.walker.children(this).nodes.length; + } + + return numChildren; + }, + + get computedStyle() { + return CssLogic.getComputedStyle(this.rawNode); + }, + + /** + * Is the node's display computed style value other than "none" + */ + get isDisplayed() { + // Consider all non-element nodes as displayed. + if (isNodeDead(this) || + this.rawNode.nodeType !== Ci.nsIDOMNode.ELEMENT_NODE || + this.isAfterPseudoElement || + this.isBeforePseudoElement) { + return true; + } + + let style = this.computedStyle; + if (!style) { + return true; + } + + return style.display !== "none"; + }, + + /** + * Are there event listeners that are listening on this node? This method + * uses all parsers registered via event-parsers.js.registerEventParser() to + * check if there are any event listeners. + */ + get _hasEventListeners() { + let parsers = this._eventParsers; + for (let [, {hasListeners}] of parsers) { + try { + if (hasListeners && hasListeners(this.rawNode)) { + return true; + } + } catch (e) { + // An object attached to the node looked like a listener but wasn't... + // do nothing. + } + } + return false; + }, + + writeAttrs: function () { + if (!this.rawNode.attributes) { + return undefined; + } + + return [...this.rawNode.attributes].map(attr => { + return {namespace: attr.namespace, name: attr.name, value: attr.value }; + }); + }, + + writePseudoClassLocks: function () { + if (this.rawNode.nodeType !== Ci.nsIDOMNode.ELEMENT_NODE) { + return undefined; + } + let ret = undefined; + for (let pseudo of PSEUDO_CLASSES) { + if (DOMUtils.hasPseudoClassLock(this.rawNode, pseudo)) { + ret = ret || []; + ret.push(pseudo); + } + } + return ret; + }, + + /** + * Gets event listeners and adds their information to the events array. + * + * @param {Node} node + * Node for which we are to get listeners. + */ + getEventListeners: function (node) { + let parsers = this._eventParsers; + let dbg = this.parent().tabActor.makeDebugger(); + let listeners = []; + + for (let [, {getListeners, normalizeHandler}] of parsers) { + try { + let eventInfos = getListeners(node); + + if (!eventInfos) { + continue; + } + + for (let eventInfo of eventInfos) { + if (normalizeHandler) { + eventInfo.normalizeHandler = normalizeHandler; + } + + this.processHandlerForEvent(node, listeners, dbg, eventInfo); + } + } catch (e) { + // An object attached to the node looked like a listener but wasn't... + // do nothing. + } + } + + listeners.sort((a, b) => { + return a.type.localeCompare(b.type); + }); + + return listeners; + }, + + /** + * Process a handler + * + * @param {Node} node + * The node for which we want information. + * @param {Array} events + * The events array contains all event objects that we have gathered + * so far. + * @param {Debugger} dbg + * JSDebugger instance. + * @param {Object} eventInfo + * See event-parsers.js.registerEventParser() for a description of the + * eventInfo object. + * + * @return {Array} + * An array of objects where a typical object looks like this: + * { + * type: "click", + * handler: function() { doSomething() }, + * origin: "http://www.mozilla.com", + * searchString: 'onclick="doSomething()"', + * tags: tags, + * DOM0: true, + * capturing: true, + * hide: { + * dom0: true + * } + * } + */ + processHandlerForEvent: function (node, listeners, dbg, eventInfo) { + let type = eventInfo.type || ""; + let handler = eventInfo.handler; + let tags = eventInfo.tags || ""; + let hide = eventInfo.hide || {}; + let override = eventInfo.override || {}; + let global = Cu.getGlobalForObject(handler); + let globalDO = dbg.addDebuggee(global); + let listenerDO = globalDO.makeDebuggeeValue(handler); + + if (eventInfo.normalizeHandler) { + listenerDO = eventInfo.normalizeHandler(listenerDO); + } + + // If the listener is an object with a 'handleEvent' method, use that. + if (listenerDO.class === "Object" || listenerDO.class === "XULElement") { + let desc; + + while (!desc && listenerDO) { + desc = listenerDO.getOwnPropertyDescriptor("handleEvent"); + listenerDO = listenerDO.proto; + } + + if (desc && desc.value) { + listenerDO = desc.value; + } + } + + if (listenerDO.isBoundFunction) { + listenerDO = listenerDO.boundTargetFunction; + } + + let script = listenerDO.script; + let scriptSource = script.source.text; + let functionSource = + scriptSource.substr(script.sourceStart, script.sourceLength); + + /* + The script returned is the whole script and + scriptSource.substr(script.sourceStart, script.sourceLength) returns + something like this: + () { doSomething(); } + + So we need to use some regex magic to get the appropriate function info + e.g.: + () => { ... } + function doit() { ... } + doit: function() { ... } + es6func() { ... } + var|let|const foo = function () { ... } + function generator*() { ... } + */ + let scriptBeforeFunc = scriptSource.substr(0, script.sourceStart); + let matches = scriptBeforeFunc.match(RX_FUNC_NAME); + if (matches && matches.length > 0) { + functionSource = matches[0].trim() + functionSource; + } + + let dom0 = false; + + if (typeof node.hasAttribute !== "undefined") { + dom0 = !!node.hasAttribute("on" + type); + } else { + dom0 = !!node["on" + type]; + } + + let line = script.startLine; + let url = script.url; + let origin = url + (dom0 ? "" : ":" + line); + let searchString; + + if (dom0) { + searchString = "on" + type + "=\"" + script.source.text + "\""; + } else { + scriptSource = " " + scriptSource; + } + + let eventObj = { + type: typeof override.type !== "undefined" ? override.type : type, + handler: functionSource.trim(), + origin: typeof override.origin !== "undefined" ? + override.origin : origin, + searchString: typeof override.searchString !== "undefined" ? + override.searchString : searchString, + tags: tags, + DOM0: typeof override.dom0 !== "undefined" ? override.dom0 : dom0, + capturing: typeof override.capturing !== "undefined" ? + override.capturing : eventInfo.capturing, + hide: hide + }; + + listeners.push(eventObj); + + dbg.removeDebuggee(globalDO); + }, + + /** + * Returns a LongStringActor with the node's value. + */ + getNodeValue: function () { + return new LongStringActor(this.conn, this.rawNode.nodeValue || ""); + }, + + /** + * Set the node's value to a given string. + */ + setNodeValue: function (value) { + this.rawNode.nodeValue = value; + }, + + /** + * Get a unique selector string for this node. + */ + getUniqueSelector: function () { + if (Cu.isDeadWrapper(this.rawNode)) { + return ""; + } + return CssLogic.findCssSelector(this.rawNode); + }, + + /** + * Scroll the selected node into view. + */ + scrollIntoView: function () { + this.rawNode.scrollIntoView(true); + }, + + /** + * Get the node's image data if any (for canvas and img nodes). + * Returns an imageData object with the actual data being a LongStringActor + * and a size json object. + * The image data is transmitted as a base64 encoded png data-uri. + * The method rejects if the node isn't an image or if the image is missing + * + * Accepts a maxDim request parameter to resize images that are larger. This + * is important as the resizing occurs server-side so that image-data being + * transfered in the longstring back to the client will be that much smaller + */ + getImageData: function (maxDim) { + return imageToImageData(this.rawNode, maxDim).then(imageData => { + return { + data: LongStringActor(this.conn, imageData.data), + size: imageData.size + }; + }); + }, + + /** + * Get all event listeners that are listening on this node. + */ + getEventListenerInfo: function () { + if (this.rawNode.nodeName.toLowerCase() === "html") { + return this.getEventListeners(this.rawNode.ownerGlobal); + } + return this.getEventListeners(this.rawNode); + }, + + /** + * Modify a node's attributes. Passed an array of modifications + * similar in format to "attributes" mutations. + * { + * attributeName: + * attributeNamespace: + * newValue: - If null or undefined, the attribute + * will be removed. + * } + * + * Returns when the modifications have been made. Mutations will + * be queued for any changes made. + */ + modifyAttributes: function (modifications) { + let rawNode = this.rawNode; + for (let change of modifications) { + if (change.newValue == null) { + if (change.attributeNamespace) { + rawNode.removeAttributeNS(change.attributeNamespace, + change.attributeName); + } else { + rawNode.removeAttribute(change.attributeName); + } + } else if (change.attributeNamespace) { + rawNode.setAttributeNS(change.attributeNamespace, change.attributeName, + change.newValue); + } else { + rawNode.setAttribute(change.attributeName, change.newValue); + } + } + }, + + /** + * Given the font and fill style, get the image data of a canvas with the + * preview text and font. + * Returns an imageData object with the actual data being a LongStringActor + * and the width of the text as a string. + * The image data is transmitted as a base64 encoded png data-uri. + */ + getFontFamilyDataURL: function (font, fillStyle = "black") { + let doc = this.rawNode.ownerDocument; + let options = { + previewText: FONT_FAMILY_PREVIEW_TEXT, + previewFontSize: FONT_FAMILY_PREVIEW_TEXT_SIZE, + fillStyle: fillStyle + }; + let { dataURL, size } = getFontPreviewData(font, doc, options); + + return { data: LongStringActor(this.conn, dataURL), size: size }; + } +}); + +/** + * Server side of a node list as returned by querySelectorAll() + */ +var NodeListActor = exports.NodeListActor = protocol.ActorClassWithSpec(nodeListSpec, { + typeName: "domnodelist", + + initialize: function (walker, nodeList) { + protocol.Actor.prototype.initialize.call(this); + this.walker = walker; + this.nodeList = nodeList || []; + }, + + destroy: function () { + protocol.Actor.prototype.destroy.call(this); + }, + + /** + * Instead of storing a connection object, the NodeActor gets its connection + * from its associated walker. + */ + get conn() { + return this.walker.conn; + }, + + /** + * Items returned by this actor should belong to the parent walker. + */ + marshallPool: function () { + return this.walker; + }, + + // Returns the JSON representation of this object over the wire. + form: function () { + return { + actor: this.actorID, + length: this.nodeList ? this.nodeList.length : 0 + }; + }, + + /** + * Get a single node from the node list. + */ + item: function (index) { + return this.walker.attachElement(this.nodeList[index]); + }, + + /** + * Get a range of the items from the node list. + */ + items: function (start = 0, end = this.nodeList.length) { + let items = Array.prototype.slice.call(this.nodeList, start, end) + .map(item => this.walker._ref(item)); + return this.walker.attachElements(items); + }, + + release: function () {} +}); + +/** + * Server side of the DOM walker. + */ +var WalkerActor = protocol.ActorClassWithSpec(walkerSpec, { + /** + * Create the WalkerActor + * @param DebuggerServerConnection conn + * The server connection. + */ + initialize: function (conn, tabActor, options) { + protocol.Actor.prototype.initialize.call(this, conn); + this.tabActor = tabActor; + this.rootWin = tabActor.window; + this.rootDoc = this.rootWin.document; + this._refMap = new Map(); + this._pendingMutations = []; + this._activePseudoClassLocks = new Set(); + this.showAllAnonymousContent = options.showAllAnonymousContent; + + this.walkerSearch = new WalkerSearch(this); + + // Nodes which have been removed from the client's known + // ownership tree are considered "orphaned", and stored in + // this set. + this._orphaned = new Set(); + + // The client can tell the walker that it is interested in a node + // even when it is orphaned with the `retainNode` method. This + // list contains orphaned nodes that were so retained. + this._retainedOrphans = new Set(); + + this.onMutations = this.onMutations.bind(this); + this.onFrameLoad = this.onFrameLoad.bind(this); + this.onFrameUnload = this.onFrameUnload.bind(this); + + events.on(tabActor, "will-navigate", this.onFrameUnload); + events.on(tabActor, "navigate", this.onFrameLoad); + + // Ensure that the root document node actor is ready and + // managed. + this.rootNode = this.document(); + + this.layoutChangeObserver = getLayoutChangesObserver(this.tabActor); + this._onReflows = this._onReflows.bind(this); + this.layoutChangeObserver.on("reflows", this._onReflows); + this._onResize = this._onResize.bind(this); + this.layoutChangeObserver.on("resize", this._onResize); + + this._onEventListenerChange = this._onEventListenerChange.bind(this); + eventListenerService.addListenerChangeListener(this._onEventListenerChange); + }, + + /** + * Callback for eventListenerService.addListenerChangeListener + * @param nsISimpleEnumerator changesEnum + * enumerator of nsIEventListenerChange + */ + _onEventListenerChange: function (changesEnum) { + let changes = changesEnum.enumerate(); + while (changes.hasMoreElements()) { + let current = changes.getNext().QueryInterface(Ci.nsIEventListenerChange); + let target = current.target; + + if (this._refMap.has(target)) { + let actor = this.getNode(target); + let mutation = { + type: "events", + target: actor.actorID, + hasEventListeners: actor._hasEventListeners + }; + this.queueMutation(mutation); + } + } + }, + + // Returns the JSON representation of this object over the wire. + form: function () { + return { + actor: this.actorID, + root: this.rootNode.form(), + traits: { + // FF42+ Inspector starts managing the Walker, while the inspector also + // starts cleaning itself up automatically on client disconnection. + // So that there is no need to manually release the walker anymore. + autoReleased: true, + // XXX: It seems silly that we need to tell the front which capabilities + // its actor has in this way when the target can use actorHasMethod. If + // this was ported to the protocol (Bug 1157048) we could call that + // inside of custom front methods and not need to do traits for this. + multiFrameQuerySelectorAll: true, + textSearch: true, + } + }; + }, + + toString: function () { + return "[WalkerActor " + this.actorID + "]"; + }, + + getDocumentWalker: function (node, whatToShow) { + // Allow native anon content (like