зеркало из https://github.com/mozilla/gecko-dev.git
Bug 1607756 - Extract node picker from HighlighterActor. r=ochameau,devtools-backward-compat-reviewers
Moved the node picker functionality from `HighlighterActor` to a standalone `NodePicker` class used by `WalkerActor`. `WalkerActor`/`WalkerFront` was already used to [emit node picker events](https://searchfox.org/mozilla-central/rev/027893497316897b8f292bde48dbb6da2391a331/devtools/shared/specs/walker.js#65-79) listened to by the client. This was kept intact. The alternative to introduce a new `NodePickerActor` had issues when emitting events because the `NodeFront` payload ended up being managed by the `NodePickerFront` and not the `WalkerFront` (see: https://searchfox.org/mozilla-central/source/devtools/shared/protocol/types.js#337,346). There isn't a strong reason to introduce a new `NodePickerActor` considering that the functionality is already dependent on `WalkerActor` to [attach the hovered / picked nodes](https://searchfox.org/mozilla-central/rev/027893497316897b8f292bde48dbb6da2391a331/devtools/server/actors/highlighters.js#435-441). As we progress with changes in D81526 and [Bug 1623667](https://bugzilla.mozilla.org/show_bug.cgi?id=1623667), the standalone [HighlighterActor](https://searchfox.org/mozilla-central/source/devtools/server/actors/highlighters.js#111-517) will go away completely. It is just a wrapper around `BoxModelHighlighter` and `SimpleOutlineHighlighter`, both of which are already supported by [CustomHighlighterActor](https://searchfox.org/mozilla-central/source/devtools/server/actors/highlighters.js#523-652) that provides all the other highlighters. NOTE: D81476 is removing `SimpleOutlineHighlighter` , thus adding to the reasons for dropping `HighlighterActor` once tests and call sites are updated to get highlighters via the highlighter manager logic changes introduced by D81526 Differential Revision: https://phabricator.services.mozilla.com/D81525
This commit is contained in:
Родитель
6e3170ab3d
Коммит
1ad6fbabbc
|
@ -19,7 +19,6 @@ class HighlighterFront extends FrontClassWithSpec(highlighterSpec) {
|
|||
super(client, targetFront, parentFront);
|
||||
|
||||
this.isNodeFrontHighlighted = false;
|
||||
this.isPicking = false;
|
||||
}
|
||||
|
||||
// Update the object given a form representation off the wire.
|
||||
|
@ -29,37 +28,6 @@ class HighlighterFront extends FrontClassWithSpec(highlighterSpec) {
|
|||
this.traits = json.traits || {};
|
||||
}
|
||||
|
||||
/**
|
||||
* Start the element picker on the debuggee target.
|
||||
* @param {Boolean} doFocus - Optionally focus the content area once the picker is
|
||||
* activated.
|
||||
* @return promise that resolves when the picker has started or immediately
|
||||
* if it is already started
|
||||
*/
|
||||
pick(doFocus) {
|
||||
if (this.isPicking) {
|
||||
return null;
|
||||
}
|
||||
this.isPicking = true;
|
||||
if (doFocus && super.pickAndFocus) {
|
||||
return super.pickAndFocus();
|
||||
}
|
||||
return super.pick();
|
||||
}
|
||||
|
||||
/**
|
||||
* Stop the element picker.
|
||||
* @return promise that resolves when the picker has stopped or immediately
|
||||
* if it is already stopped
|
||||
*/
|
||||
cancelPick() {
|
||||
if (!this.isPicking) {
|
||||
return Promise.resolve();
|
||||
}
|
||||
this.isPicking = false;
|
||||
return super.cancelPick();
|
||||
}
|
||||
|
||||
/**
|
||||
* Show the box model highlighter on a node in the content page.
|
||||
* The node needs to be a NodeFront, as defined by the inspector actor
|
||||
|
|
|
@ -23,6 +23,7 @@ loader.lazyRequireGetter(
|
|||
class WalkerFront extends FrontClassWithSpec(walkerSpec) {
|
||||
constructor(client, targetFront, parentFront) {
|
||||
super(client, targetFront, parentFront);
|
||||
this._isPicking = false;
|
||||
this._orphaned = new Set();
|
||||
this._retainedOrphans = new Set();
|
||||
|
||||
|
@ -588,6 +589,46 @@ class WalkerFront extends FrontClassWithSpec(walkerSpec) {
|
|||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Start the element picker on the debuggee target.
|
||||
* @param {Boolean} doFocus - Optionally focus the content area once the picker is
|
||||
* activated.
|
||||
*/
|
||||
pick(doFocus) {
|
||||
if (this._isPicking) {
|
||||
return Promise.resolve();
|
||||
}
|
||||
this._isPicking = true;
|
||||
|
||||
// Firefox 80 - backwards compatibility for servers without walker.pick()
|
||||
if (!this.traits.supportsNodePicker) {
|
||||
// parent is InspectorFront
|
||||
return doFocus
|
||||
? this.parentFront.highlighter.pickAndFocus()
|
||||
: this.parentFront.highlighter.pick();
|
||||
}
|
||||
|
||||
return super.pick(doFocus);
|
||||
}
|
||||
|
||||
/**
|
||||
* Stop the element picker.
|
||||
*/
|
||||
cancelPick() {
|
||||
if (!this._isPicking) {
|
||||
return Promise.resolve();
|
||||
}
|
||||
this._isPicking = false;
|
||||
|
||||
// Firefox 80 - backwards compatibility for servers without walker.cancelPick()
|
||||
if (!this.traits.supportsNodePicker) {
|
||||
// parent is InspectorFront
|
||||
return this.parentFront.highlighter.cancelPick();
|
||||
}
|
||||
|
||||
return super.cancelPick();
|
||||
}
|
||||
|
||||
unwatchRootNode(onRootNodeAvailable) {
|
||||
this.off("root-available", onRootNodeAvailable);
|
||||
|
||||
|
|
|
@ -9,13 +9,12 @@ loader.lazyRequireGetter(this, "EventEmitter", "devtools/shared/event-emitter");
|
|||
/**
|
||||
* Client-side NodePicker module.
|
||||
* To be used by inspector front when it needs to select DOM elements.
|
||||
*/
|
||||
|
||||
/**
|
||||
* Get the NodePicker instance for an inspector front.
|
||||
* The NodePicker wraps the highlighter so that it can interact with the
|
||||
* walkerFront and selection api. The nodeFront is stateless, with the
|
||||
* HighlighterFront managing it's own state.
|
||||
*
|
||||
* NodePicker is a proxy for the node picker functionality from WalkerFront instances
|
||||
* of all available InspectorFronts. It is a single point of entry for the client to:
|
||||
* - invoke actions to start and stop picking nodes on all walkers
|
||||
* - listen to node picker events from all walkers and relay them to subscribers
|
||||
*
|
||||
*
|
||||
* @param {TargetList} targetList
|
||||
* The TargetList component referencing all the targets to be debugged
|
||||
|
@ -85,13 +84,12 @@ class NodePicker extends EventEmitter {
|
|||
"inspector"
|
||||
);
|
||||
|
||||
for (const { walker, highlighter } of this._currentInspectorFronts) {
|
||||
for (const { walker } of this._currentInspectorFronts) {
|
||||
walker.on("picker-node-hovered", this._onHovered);
|
||||
walker.on("picker-node-picked", this._onPicked);
|
||||
walker.on("picker-node-previewed", this._onPreviewed);
|
||||
walker.on("picker-node-canceled", this._onCanceled);
|
||||
|
||||
await highlighter.pick(doFocus);
|
||||
await walker.pick(doFocus);
|
||||
}
|
||||
|
||||
this.emit("picker-started");
|
||||
|
@ -107,13 +105,12 @@ class NodePicker extends EventEmitter {
|
|||
}
|
||||
this.isPicking = false;
|
||||
|
||||
for (const { walker, highlighter } of this._currentInspectorFronts) {
|
||||
await highlighter.cancelPick();
|
||||
|
||||
for (const { walker } of this._currentInspectorFronts) {
|
||||
walker.off("picker-node-hovered", this._onHovered);
|
||||
walker.off("picker-node-picked", this._onPicked);
|
||||
walker.off("picker-node-previewed", this._onPreviewed);
|
||||
walker.off("picker-node-canceled", this._onCanceled);
|
||||
await walker.cancelPick();
|
||||
}
|
||||
|
||||
this._currentInspectorFronts = [];
|
||||
|
@ -125,6 +122,12 @@ class NodePicker extends EventEmitter {
|
|||
* Stop the picker, but also emit an event that the picker was canceled.
|
||||
*/
|
||||
async cancel() {
|
||||
// TODO: Remove once migrated to process-agnostic box model highlighter (Bug 1646028)
|
||||
Promise.all(
|
||||
this._currentInspectorFronts.map(({ highlighter }) =>
|
||||
highlighter.hideBoxModel()
|
||||
)
|
||||
).catch(e => console.error);
|
||||
await this.stop();
|
||||
this.emit("picker-node-canceled");
|
||||
}
|
||||
|
@ -135,9 +138,12 @@ class NodePicker extends EventEmitter {
|
|||
* @param {Object} data
|
||||
* Information about the node being hovered
|
||||
*/
|
||||
_onHovered(data) {
|
||||
async _onHovered(data) {
|
||||
this.emit("picker-node-hovered", data.node);
|
||||
|
||||
// TODO: Remove once migrated to process-agnostic box model highlighter (Bug 1646028)
|
||||
await data.node.highlighterFront.showBoxModel(data.node);
|
||||
|
||||
// One of the HighlighterActor instances, in one of the current targets, is hovering
|
||||
// over a node. Because we may be connected to several targets, we have several
|
||||
// HighlighterActor instances running at the same time. Tell the ones that don't match
|
||||
|
@ -170,8 +176,11 @@ class NodePicker extends EventEmitter {
|
|||
* @param {Object} data
|
||||
* Information about the picked node
|
||||
*/
|
||||
_onPreviewed(data) {
|
||||
async _onPreviewed(data) {
|
||||
this.emit("picker-node-previewed", data.node);
|
||||
|
||||
// TODO: Remove once migrated to process-agnostic box model highlighter (Bug 1646028)
|
||||
await data.node.highlighterFront.showBoxModel(data.node);
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
@ -9,24 +9,11 @@ const { Ci, Cu } = require("chrome");
|
|||
const ChromeUtils = require("ChromeUtils");
|
||||
const EventEmitter = require("devtools/shared/event-emitter");
|
||||
const protocol = require("devtools/shared/protocol");
|
||||
const Services = require("Services");
|
||||
const {
|
||||
highlighterSpec,
|
||||
customHighlighterSpec,
|
||||
} = require("devtools/shared/specs/highlighters");
|
||||
|
||||
loader.lazyRequireGetter(
|
||||
this,
|
||||
"isWindowIncluded",
|
||||
"devtools/shared/layout/utils",
|
||||
true
|
||||
);
|
||||
loader.lazyRequireGetter(
|
||||
this,
|
||||
"isRemoteFrame",
|
||||
"devtools/shared/layout/utils",
|
||||
true
|
||||
);
|
||||
loader.lazyRequireGetter(
|
||||
this,
|
||||
"isXUL",
|
||||
|
@ -46,9 +33,6 @@ loader.lazyRequireGetter(
|
|||
true
|
||||
);
|
||||
|
||||
const HIGHLIGHTER_PICKED_TIMER = 1000;
|
||||
const IS_OSX = Services.appinfo.OS === "Darwin";
|
||||
|
||||
/**
|
||||
* The registration mechanism for highlighters provide a quick way to
|
||||
* have modular highlighters, instead of a hard coded list.
|
||||
|
@ -112,9 +96,7 @@ exports.HighlighterActor = protocol.ActorClassWithSpec(highlighterSpec, {
|
|||
initialize: function(inspector, autohide) {
|
||||
protocol.Actor.prototype.initialize.call(this, null);
|
||||
|
||||
this._autohide = autohide;
|
||||
this._inspector = inspector;
|
||||
this._walker = this._inspector.walker;
|
||||
this._targetActor = this._inspector.targetActor;
|
||||
this._highlighterEnv = new HighlighterEnvironment();
|
||||
this._highlighterEnv.initFromTargetActor(this._targetActor);
|
||||
|
@ -181,16 +163,13 @@ exports.HighlighterActor = protocol.ActorClassWithSpec(highlighterSpec, {
|
|||
protocol.Actor.prototype.destroy.call(this);
|
||||
|
||||
this.hideBoxModel();
|
||||
this.cancelPick();
|
||||
this._destroyHighlighter();
|
||||
this._targetActor.off("navigate", this._onNavigate);
|
||||
|
||||
this._highlighterEnv.destroy();
|
||||
this._highlighterEnv = null;
|
||||
|
||||
this._autohide = null;
|
||||
this._inspector = null;
|
||||
this._walker = null;
|
||||
this._targetActor = null;
|
||||
},
|
||||
|
||||
|
@ -217,302 +196,6 @@ exports.HighlighterActor = protocol.ActorClassWithSpec(highlighterSpec, {
|
|||
if (this._highlighter) {
|
||||
this._highlighter.hide();
|
||||
}
|
||||
|
||||
// Since the node-picker works independently in each remote frame, the inspector
|
||||
// front-end decides which highlighter to show and hide while picking.
|
||||
// If we're being asked to hide here, we should also reset the current hovered node so
|
||||
// we can start highlighting correctly again later.
|
||||
this._hoveredNode = null;
|
||||
},
|
||||
|
||||
/**
|
||||
* Returns `true` if the event was dispatched from a window included in
|
||||
* the current highlighter environment; or if the highlighter environment has
|
||||
* chrome privileges
|
||||
*
|
||||
* The method is specifically useful on B2G, where we do not want that events
|
||||
* from app or main process are processed if we're inspecting the content.
|
||||
*
|
||||
* @param {Event} event
|
||||
* The event to allow
|
||||
* @return {Boolean}
|
||||
*/
|
||||
_isEventAllowed: function({ view }) {
|
||||
const { window } = this._highlighterEnv;
|
||||
|
||||
return (
|
||||
window instanceof Ci.nsIDOMChromeWindow || isWindowIncluded(window, view)
|
||||
);
|
||||
},
|
||||
|
||||
/**
|
||||
* Pick a node on click, and highlight hovered nodes in the process.
|
||||
*
|
||||
* This method doesn't respond anything interesting, however, it starts
|
||||
* mousemove, and click listeners on the content document to fire
|
||||
* events and let connected clients know when nodes are hovered over or
|
||||
* clicked.
|
||||
*
|
||||
* Once a node is picked, events will cease, and listeners will be removed.
|
||||
*/
|
||||
_isPicking: false,
|
||||
_hoveredNode: null,
|
||||
_currentNode: null,
|
||||
|
||||
pick: function() {
|
||||
if (this._targetActor.threadActor) {
|
||||
this._targetActor.threadActor.hideOverlay();
|
||||
}
|
||||
|
||||
if (this._isPicking) {
|
||||
return null;
|
||||
}
|
||||
this._isPicking = true;
|
||||
|
||||
// In most cases, we need to prevent content events from reaching the content. This is
|
||||
// needed to avoid triggering actions such as submitting forms or following links.
|
||||
// In the case where the event happens on a remote frame however, we do want to let it
|
||||
// through. That is because otherwise the pickers started in nested remote frames will
|
||||
// never have a chance of picking their own elements.
|
||||
this._preventContentEvent = event => {
|
||||
if (isRemoteFrame(event.target)) {
|
||||
return;
|
||||
}
|
||||
event.stopPropagation();
|
||||
event.preventDefault();
|
||||
};
|
||||
|
||||
this._onPick = event => {
|
||||
// If the picked node is a remote frame, then we need to let the event through
|
||||
// since there's a highlighter actor in that sub-frame also picking.
|
||||
if (isRemoteFrame(event.target)) {
|
||||
return;
|
||||
}
|
||||
|
||||
this._preventContentEvent(event);
|
||||
|
||||
if (!this._isEventAllowed(event)) {
|
||||
return;
|
||||
}
|
||||
|
||||
// If shift is pressed, this is only a preview click, send the event to
|
||||
// the client, but don't stop picking.
|
||||
if (event.shiftKey) {
|
||||
this._walker.emit(
|
||||
"picker-node-previewed",
|
||||
this._findAndAttachElement(event)
|
||||
);
|
||||
return;
|
||||
}
|
||||
this._stopPickerListeners();
|
||||
this._isPicking = false;
|
||||
if (this._autohide) {
|
||||
this._targetActor.window.setTimeout(() => {
|
||||
this._highlighter.hide();
|
||||
}, HIGHLIGHTER_PICKED_TIMER);
|
||||
}
|
||||
if (!this._currentNode) {
|
||||
this._currentNode = this._findAndAttachElement(event);
|
||||
}
|
||||
this._walker.emit("picker-node-picked", this._currentNode);
|
||||
};
|
||||
|
||||
this._onHovered = event => {
|
||||
// If the hovered node is a remote frame, then we need to let the event through
|
||||
// since there's a highlighter actor in that sub-frame also picking.
|
||||
if (isRemoteFrame(event.target)) {
|
||||
return;
|
||||
}
|
||||
|
||||
this._preventContentEvent(event);
|
||||
if (!this._isEventAllowed(event)) {
|
||||
return;
|
||||
}
|
||||
|
||||
this._currentNode = this._findAndAttachElement(event);
|
||||
if (this._hoveredNode !== this._currentNode.node) {
|
||||
this._highlighter.show(this._currentNode.node.rawNode);
|
||||
this._walker.emit("picker-node-hovered", this._currentNode);
|
||||
this._hoveredNode = this._currentNode.node;
|
||||
}
|
||||
};
|
||||
|
||||
this._onKey = event => {
|
||||
if (!this._currentNode || !this._isPicking) {
|
||||
return;
|
||||
}
|
||||
|
||||
this._preventContentEvent(event);
|
||||
|
||||
if (!this._isEventAllowed(event)) {
|
||||
return;
|
||||
}
|
||||
|
||||
let currentNode = this._currentNode.node.rawNode;
|
||||
|
||||
/**
|
||||
* KEY: Action/scope
|
||||
* LEFT_KEY: wider or parent
|
||||
* RIGHT_KEY: narrower or child
|
||||
* ENTER/CARRIAGE_RETURN: Picks currentNode
|
||||
* ESC/CTRL+SHIFT+C: Cancels picker, picks currentNode
|
||||
*/
|
||||
switch (event.keyCode) {
|
||||
// Wider.
|
||||
case event.DOM_VK_LEFT:
|
||||
if (!currentNode.parentElement) {
|
||||
return;
|
||||
}
|
||||
currentNode = currentNode.parentElement;
|
||||
break;
|
||||
|
||||
// Narrower.
|
||||
case event.DOM_VK_RIGHT:
|
||||
if (!currentNode.children.length) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Set firstElementChild by default
|
||||
let child = currentNode.firstElementChild;
|
||||
// If currentNode is parent of hoveredNode, then
|
||||
// previously selected childNode is set
|
||||
const hoveredNode = this._hoveredNode.rawNode;
|
||||
for (const sibling of currentNode.children) {
|
||||
if (sibling.contains(hoveredNode) || sibling === hoveredNode) {
|
||||
child = sibling;
|
||||
}
|
||||
}
|
||||
|
||||
currentNode = child;
|
||||
break;
|
||||
|
||||
// Select the element.
|
||||
case event.DOM_VK_RETURN:
|
||||
this._onPick(event);
|
||||
return;
|
||||
|
||||
// Cancel pick mode.
|
||||
case event.DOM_VK_ESCAPE:
|
||||
this.cancelPick();
|
||||
this._walker.emit("picker-node-canceled");
|
||||
return;
|
||||
case event.DOM_VK_C:
|
||||
const { altKey, ctrlKey, metaKey, shiftKey } = event;
|
||||
|
||||
if (
|
||||
(IS_OSX && metaKey && altKey | shiftKey) ||
|
||||
(!IS_OSX && ctrlKey && shiftKey)
|
||||
) {
|
||||
this.cancelPick();
|
||||
this._walker.emit("picker-node-canceled");
|
||||
}
|
||||
return;
|
||||
default:
|
||||
return;
|
||||
}
|
||||
|
||||
// Store currently attached element
|
||||
this._currentNode = this._walker.attachElement(currentNode);
|
||||
this._highlighter.show(this._currentNode.node.rawNode);
|
||||
this._walker.emit("picker-node-hovered", this._currentNode);
|
||||
};
|
||||
|
||||
this._startPickerListeners();
|
||||
|
||||
return null;
|
||||
},
|
||||
|
||||
/**
|
||||
* This pick method also focuses the highlighter's target window.
|
||||
*/
|
||||
pickAndFocus: function() {
|
||||
// Go ahead and pass on the results to help future-proof this method.
|
||||
const pickResults = this.pick();
|
||||
this._highlighterEnv.window.focus();
|
||||
return pickResults;
|
||||
},
|
||||
|
||||
_findAndAttachElement: function(event) {
|
||||
// originalTarget allows access to the "real" element before any retargeting
|
||||
// is applied, such as in the case of XBL anonymous elements. See also
|
||||
// https://developer.mozilla.org/docs/XBL/XBL_1.0_Reference/Anonymous_Content#Event_Flow_and_Targeting
|
||||
const node = event.originalTarget || event.target;
|
||||
return this._walker.attachElement(node);
|
||||
},
|
||||
|
||||
_onSuppressedEvent(event) {
|
||||
if (event.type == "mousemove") {
|
||||
this._onHovered(event);
|
||||
} else if (event.type == "mouseup") {
|
||||
// Suppressed mousedown/mouseup events will be sent to us before they have
|
||||
// been converted into click events. Just treat any mouseup as a click.
|
||||
this._onPick(event);
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* When the debugger pauses execution in a page, events will not be delivered
|
||||
* to any handlers added to elements on that page. This method uses the
|
||||
* document's setSuppressedEventListener interface to bypass this restriction:
|
||||
* events will be delivered to the callback at times when they would
|
||||
* otherwise be suppressed. The set of events delivered this way is currently
|
||||
* limited to mouse events.
|
||||
*
|
||||
* @param callback The function to call with suppressed events, or null.
|
||||
*/
|
||||
_setSuppressedEventListener(callback) {
|
||||
const document = this._targetActor.window.document;
|
||||
|
||||
// Pass the callback to setSuppressedEventListener as an EventListener.
|
||||
document.setSuppressedEventListener(
|
||||
callback ? { handleEvent: callback } : null
|
||||
);
|
||||
},
|
||||
|
||||
_startPickerListeners: function() {
|
||||
const target = this._highlighterEnv.pageListenerTarget;
|
||||
target.addEventListener("mousemove", this._onHovered, true);
|
||||
target.addEventListener("click", this._onPick, true);
|
||||
target.addEventListener("mousedown", this._preventContentEvent, true);
|
||||
target.addEventListener("mouseup", this._preventContentEvent, true);
|
||||
target.addEventListener("dblclick", this._preventContentEvent, true);
|
||||
target.addEventListener("keydown", this._onKey, true);
|
||||
target.addEventListener("keyup", this._preventContentEvent, true);
|
||||
|
||||
this._setSuppressedEventListener(this._onSuppressedEvent.bind(this));
|
||||
},
|
||||
|
||||
_stopPickerListeners: function() {
|
||||
const target = this._highlighterEnv.pageListenerTarget;
|
||||
|
||||
if (!target) {
|
||||
return;
|
||||
}
|
||||
|
||||
target.removeEventListener("mousemove", this._onHovered, true);
|
||||
target.removeEventListener("click", this._onPick, true);
|
||||
target.removeEventListener("mousedown", this._preventContentEvent, true);
|
||||
target.removeEventListener("mouseup", this._preventContentEvent, true);
|
||||
target.removeEventListener("dblclick", this._preventContentEvent, true);
|
||||
target.removeEventListener("keydown", this._onKey, true);
|
||||
target.removeEventListener("keyup", this._preventContentEvent, true);
|
||||
|
||||
this._setSuppressedEventListener(null);
|
||||
},
|
||||
|
||||
cancelPick: function() {
|
||||
if (this._targetActor.threadActor) {
|
||||
this._targetActor.threadActor.showOverlay();
|
||||
}
|
||||
|
||||
if (this._isPicking) {
|
||||
if (this._highlighter) {
|
||||
this._highlighter.hide();
|
||||
}
|
||||
this._stopPickerListeners();
|
||||
this._isPicking = false;
|
||||
this._hoveredNode = null;
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
|
|
|
@ -11,6 +11,7 @@ DevToolsModules(
|
|||
'document-walker.js',
|
||||
'event-collector.js',
|
||||
'inspector.js',
|
||||
'node-picker.js',
|
||||
'node.js',
|
||||
'utils.js',
|
||||
'walker.js',
|
||||
|
|
|
@ -0,0 +1,311 @@
|
|||
/* 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";
|
||||
const { Ci } = require("chrome");
|
||||
const Services = require("Services");
|
||||
|
||||
loader.lazyRequireGetter(
|
||||
this,
|
||||
"isWindowIncluded",
|
||||
"devtools/shared/layout/utils",
|
||||
true
|
||||
);
|
||||
loader.lazyRequireGetter(
|
||||
this,
|
||||
"isRemoteFrame",
|
||||
"devtools/shared/layout/utils",
|
||||
true
|
||||
);
|
||||
|
||||
const IS_OSX = Services.appinfo.OS === "Darwin";
|
||||
|
||||
class NodePicker {
|
||||
constructor(walker, targetActor) {
|
||||
this._walker = walker;
|
||||
this._targetActor = targetActor;
|
||||
|
||||
this._isPicking = false;
|
||||
this._hoveredNode = null;
|
||||
this._currentNode = null;
|
||||
|
||||
this._onHovered = this._onHovered.bind(this);
|
||||
this._onKey = this._onKey.bind(this);
|
||||
this._onPick = this._onPick.bind(this);
|
||||
this._onSuppressedEvent = this._onSuppressedEvent.bind(this);
|
||||
}
|
||||
|
||||
_findAndAttachElement(event) {
|
||||
// originalTarget allows access to the "real" element before any retargeting
|
||||
// is applied, such as in the case of XBL anonymous elements. See also
|
||||
// https://developer.mozilla.org/docs/XBL/XBL_1.0_Reference/Anonymous_Content#Event_Flow_and_Targeting
|
||||
const node = event.originalTarget || event.target;
|
||||
return this._walker.attachElement(node);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns `true` if the event was dispatched from a window included in
|
||||
* the current highlighter environment; or if the highlighter environment has
|
||||
* chrome privileges
|
||||
*
|
||||
* @param {Event} event
|
||||
* The event to allow
|
||||
* @return {Boolean}
|
||||
*/
|
||||
_isEventAllowed({ view }) {
|
||||
const { window } = this._targetActor;
|
||||
|
||||
return (
|
||||
window instanceof Ci.nsIDOMChromeWindow || isWindowIncluded(window, view)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Pick a node on click.
|
||||
*
|
||||
* This method doesn't respond anything interesting, however, it starts
|
||||
* mousemove, and click listeners on the content document to fire
|
||||
* events and let connected clients know when nodes are hovered over or
|
||||
* clicked.
|
||||
*
|
||||
* Once a node is picked, events will cease, and listeners will be removed.
|
||||
*/
|
||||
_onPick(event) {
|
||||
// If the picked node is a remote frame, then we need to let the event through
|
||||
// since there's a highlighter actor in that sub-frame also picking.
|
||||
if (isRemoteFrame(event.target)) {
|
||||
return;
|
||||
}
|
||||
|
||||
this._preventContentEvent(event);
|
||||
if (!this._isEventAllowed(event)) {
|
||||
return;
|
||||
}
|
||||
|
||||
// If Shift is pressed, this is only a preview click.
|
||||
// Send the event to the client, but don't stop picking.
|
||||
if (event.shiftKey) {
|
||||
this._walker.emit(
|
||||
"picker-node-previewed",
|
||||
this._findAndAttachElement(event)
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
this._stopPickerListeners();
|
||||
this._isPicking = false;
|
||||
if (!this._currentNode) {
|
||||
this._currentNode = this._findAndAttachElement(event);
|
||||
}
|
||||
|
||||
this._walker.emit("picker-node-picked", this._currentNode);
|
||||
}
|
||||
|
||||
_onHovered(event) {
|
||||
// If the hovered node is a remote frame, then we need to let the event through
|
||||
// since there's a highlighter actor in that sub-frame also picking.
|
||||
if (isRemoteFrame(event.target)) {
|
||||
return;
|
||||
}
|
||||
|
||||
this._preventContentEvent(event);
|
||||
if (!this._isEventAllowed(event)) {
|
||||
return;
|
||||
}
|
||||
|
||||
this._currentNode = this._findAndAttachElement(event);
|
||||
if (this._hoveredNode !== this._currentNode.node) {
|
||||
this._walker.emit("picker-node-hovered", this._currentNode);
|
||||
this._hoveredNode = this._currentNode.node;
|
||||
}
|
||||
}
|
||||
|
||||
_onKey(event) {
|
||||
if (!this._currentNode || !this._isPicking) {
|
||||
return;
|
||||
}
|
||||
|
||||
this._preventContentEvent(event);
|
||||
if (!this._isEventAllowed(event)) {
|
||||
return;
|
||||
}
|
||||
|
||||
let currentNode = this._currentNode.node.rawNode;
|
||||
|
||||
/**
|
||||
* KEY: Action/scope
|
||||
* LEFT_KEY: wider or parent
|
||||
* RIGHT_KEY: narrower or child
|
||||
* ENTER/CARRIAGE_RETURN: Picks currentNode
|
||||
* ESC/CTRL+SHIFT+C: Cancels picker, picks currentNode
|
||||
*/
|
||||
switch (event.keyCode) {
|
||||
// Wider.
|
||||
case event.DOM_VK_LEFT:
|
||||
if (!currentNode.parentElement) {
|
||||
return;
|
||||
}
|
||||
currentNode = currentNode.parentElement;
|
||||
break;
|
||||
|
||||
// Narrower.
|
||||
case event.DOM_VK_RIGHT:
|
||||
if (!currentNode.children.length) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Set firstElementChild by default
|
||||
let child = currentNode.firstElementChild;
|
||||
// If currentNode is parent of hoveredNode, then
|
||||
// previously selected childNode is set
|
||||
const hoveredNode = this._hoveredNode.rawNode;
|
||||
for (const sibling of currentNode.children) {
|
||||
if (sibling.contains(hoveredNode) || sibling === hoveredNode) {
|
||||
child = sibling;
|
||||
}
|
||||
}
|
||||
|
||||
currentNode = child;
|
||||
break;
|
||||
|
||||
// Select the element.
|
||||
case event.DOM_VK_RETURN:
|
||||
this._onPick(event);
|
||||
return;
|
||||
|
||||
// Cancel pick mode.
|
||||
case event.DOM_VK_ESCAPE:
|
||||
this.cancelPick();
|
||||
this._walker.emit("picker-node-canceled");
|
||||
return;
|
||||
case event.DOM_VK_C:
|
||||
const { altKey, ctrlKey, metaKey, shiftKey } = event;
|
||||
|
||||
if (
|
||||
(IS_OSX && metaKey && altKey | shiftKey) ||
|
||||
(!IS_OSX && ctrlKey && shiftKey)
|
||||
) {
|
||||
this.cancelPick();
|
||||
this._walker.emit("picker-node-canceled");
|
||||
}
|
||||
return;
|
||||
default:
|
||||
return;
|
||||
}
|
||||
|
||||
// Store currently attached element
|
||||
this._currentNode = this._walker.attachElement(currentNode);
|
||||
this._walker.emit("picker-node-hovered", this._currentNode);
|
||||
}
|
||||
|
||||
_onSuppressedEvent(event) {
|
||||
if (event.type == "mousemove") {
|
||||
this._onHovered(event);
|
||||
} else if (event.type == "mouseup") {
|
||||
// Suppressed mousedown/mouseup events will be sent to us before they have
|
||||
// been converted into click events. Just treat any mouseup as a click.
|
||||
this._onPick(event);
|
||||
}
|
||||
}
|
||||
|
||||
// In most cases, we need to prevent content events from reaching the content. This is
|
||||
// needed to avoid triggering actions such as submitting forms or following links.
|
||||
// In the case where the event happens on a remote frame however, we do want to let it
|
||||
// through. That is because otherwise the pickers started in nested remote frames will
|
||||
// never have a chance of picking their own elements.
|
||||
_preventContentEvent(event) {
|
||||
if (isRemoteFrame(event.target)) {
|
||||
return;
|
||||
}
|
||||
event.stopPropagation();
|
||||
event.preventDefault();
|
||||
}
|
||||
|
||||
/**
|
||||
* When the debugger pauses execution in a page, events will not be delivered
|
||||
* to any handlers added to elements on that page. This method uses the
|
||||
* document's setSuppressedEventListener interface to bypass this restriction:
|
||||
* events will be delivered to the callback at times when they would
|
||||
* otherwise be suppressed. The set of events delivered this way is currently
|
||||
* limited to mouse events.
|
||||
*
|
||||
* @param callback The function to call with suppressed events, or null.
|
||||
*/
|
||||
_setSuppressedEventListener(callback) {
|
||||
const { document } = this._targetActor.window;
|
||||
|
||||
// Pass the callback to setSuppressedEventListener as an EventListener.
|
||||
document.setSuppressedEventListener(
|
||||
callback ? { handleEvent: callback } : null
|
||||
);
|
||||
}
|
||||
|
||||
_startPickerListeners() {
|
||||
const target = this._targetActor.chromeEventHandler;
|
||||
target.addEventListener("mousemove", this._onHovered, true);
|
||||
target.addEventListener("click", this._onPick, true);
|
||||
target.addEventListener("mousedown", this._preventContentEvent, true);
|
||||
target.addEventListener("mouseup", this._preventContentEvent, true);
|
||||
target.addEventListener("dblclick", this._preventContentEvent, true);
|
||||
target.addEventListener("keydown", this._onKey, true);
|
||||
target.addEventListener("keyup", this._preventContentEvent, true);
|
||||
|
||||
this._setSuppressedEventListener(this._onSuppressedEvent);
|
||||
}
|
||||
|
||||
_stopPickerListeners() {
|
||||
const target = this._targetActor.chromeEventHandler;
|
||||
if (!target) {
|
||||
return;
|
||||
}
|
||||
|
||||
target.removeEventListener("mousemove", this._onHovered, true);
|
||||
target.removeEventListener("click", this._onPick, true);
|
||||
target.removeEventListener("mousedown", this._preventContentEvent, true);
|
||||
target.removeEventListener("mouseup", this._preventContentEvent, true);
|
||||
target.removeEventListener("dblclick", this._preventContentEvent, true);
|
||||
target.removeEventListener("keydown", this._onKey, true);
|
||||
target.removeEventListener("keyup", this._preventContentEvent, true);
|
||||
|
||||
this._setSuppressedEventListener(null);
|
||||
}
|
||||
|
||||
cancelPick() {
|
||||
if (this._targetActor.threadActor) {
|
||||
this._targetActor.threadActor.showOverlay();
|
||||
}
|
||||
|
||||
if (this._isPicking) {
|
||||
this._stopPickerListeners();
|
||||
this._isPicking = false;
|
||||
this._hoveredNode = null;
|
||||
}
|
||||
}
|
||||
|
||||
pick(doFocus = false) {
|
||||
if (this._targetActor.threadActor) {
|
||||
this._targetActor.threadActor.hideOverlay();
|
||||
}
|
||||
|
||||
if (this._isPicking) {
|
||||
return;
|
||||
}
|
||||
|
||||
this._startPickerListeners();
|
||||
this._isPicking = true;
|
||||
|
||||
if (doFocus) {
|
||||
this._targetActor.window.focus();
|
||||
}
|
||||
}
|
||||
|
||||
destroy() {
|
||||
this.cancelPick();
|
||||
|
||||
this._targetActor = null;
|
||||
this._walker = null;
|
||||
}
|
||||
}
|
||||
|
||||
exports.NodePicker = NodePicker;
|
|
@ -152,6 +152,12 @@ loader.lazyRequireGetter(
|
|||
"devtools/server/actors/inspector/node",
|
||||
true
|
||||
);
|
||||
loader.lazyRequireGetter(
|
||||
this,
|
||||
"NodePicker",
|
||||
"devtools/server/actors/inspector/node-picker",
|
||||
true
|
||||
);
|
||||
loader.lazyRequireGetter(
|
||||
this,
|
||||
"LayoutActor",
|
||||
|
@ -369,6 +375,14 @@ var WalkerActor = protocol.ActorClassWithSpec(walkerSpec, {
|
|||
eventListenerService.addListenerChangeListener(this._onEventListenerChange);
|
||||
},
|
||||
|
||||
get nodePicker() {
|
||||
if (!this._nodePicker) {
|
||||
this._nodePicker = new NodePicker(this, this.targetActor);
|
||||
}
|
||||
|
||||
return this._nodePicker;
|
||||
},
|
||||
|
||||
watchRootNode() {
|
||||
if (this._isWatchingRootNode) {
|
||||
throw new Error("WalkerActor::watchRootNode should only be called once");
|
||||
|
@ -438,6 +452,8 @@ var WalkerActor = protocol.ActorClassWithSpec(walkerSpec, {
|
|||
actor: this.actorID,
|
||||
root: this.rootNode.form(),
|
||||
traits: {
|
||||
// Walker implements node picker starting with Firefox 80
|
||||
supportsNodePicker: true,
|
||||
// watch/unwatchRootNode are available starting with Fx77
|
||||
watchRootNode: true,
|
||||
},
|
||||
|
@ -524,6 +540,11 @@ var WalkerActor = protocol.ActorClassWithSpec(walkerSpec, {
|
|||
|
||||
this.walkerSearch.destroy();
|
||||
|
||||
if (this._nodePicker) {
|
||||
this._nodePicker.destroy();
|
||||
this._nodePicker = null;
|
||||
}
|
||||
|
||||
this.layoutChangeObserver.off("reflows", this._onReflows);
|
||||
this.layoutChangeObserver.off("resize", this._onResize);
|
||||
this.layoutChangeObserver = null;
|
||||
|
@ -2907,6 +2928,14 @@ var WalkerActor = protocol.ActorClassWithSpec(walkerSpec, {
|
|||
|
||||
return this.attachElement(rawNode);
|
||||
},
|
||||
|
||||
pick(doFocus) {
|
||||
this.nodePicker.pick(doFocus);
|
||||
},
|
||||
|
||||
cancelPick() {
|
||||
this.nodePicker.cancelPick();
|
||||
},
|
||||
});
|
||||
|
||||
exports.WalkerActor = WalkerActor;
|
||||
|
|
|
@ -27,6 +27,8 @@ const highlighterSpec = generateActorSpec({
|
|||
hideBoxModel: {
|
||||
request: {},
|
||||
},
|
||||
// WalkerFront implements pick(doFocus) and cancelPick() since Firefox 80
|
||||
// Keep these on HighlighterSpec for backwards compat until Firefox 80 reaches Release
|
||||
pick: {},
|
||||
pickAndFocus: {},
|
||||
cancelPick: {},
|
||||
|
|
|
@ -363,6 +363,15 @@ const walkerSpec = generateActorSpec({
|
|||
nodeFront: RetVal("disconnectedNode"),
|
||||
},
|
||||
},
|
||||
pick: {
|
||||
request: {
|
||||
doFocus: Arg(0, "nullable:boolean"),
|
||||
},
|
||||
},
|
||||
cancelPick: {
|
||||
request: {},
|
||||
response: {},
|
||||
},
|
||||
watchRootNode: {
|
||||
request: {},
|
||||
response: {},
|
||||
|
|
Загрузка…
Ссылка в новой задаче