Bug 2033191 made sure we only call ownerGlobal on nodes. As per the discussion in dev-platform, the front-end ~always wants .documentGlobal. This removes ownerGlobal from the bindings. Following patch will rename internal GetOwnerGlobal in gecko to GetRelevantGlobal as per the HTML spec terminology. Differential Revision: https://phabricator.services.mozilla.com/D297183
257 lines
6.7 KiB
JavaScript
257 lines
6.7 KiB
JavaScript
/* 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";
|
|
|
|
loader.lazyRequireGetter(
|
|
this,
|
|
"PICKER_TYPES",
|
|
"resource://devtools/shared/picker-constants.js"
|
|
);
|
|
|
|
var isClickHoldEnabled = Services.prefs.getBoolPref(
|
|
"ui.click_hold_context_menus"
|
|
);
|
|
var clickHoldDelay = Services.prefs.getIntPref(
|
|
"ui.click_hold_context_menus.delay",
|
|
500
|
|
);
|
|
|
|
const EVENTS_TO_HANDLE = [
|
|
"mousedown",
|
|
"mousemove",
|
|
"mouseup",
|
|
"mouseenter",
|
|
"mouseover",
|
|
"mouseout",
|
|
"mouseleave",
|
|
];
|
|
|
|
const kStateHover = 0x00000004; // ElementState::HOVER
|
|
|
|
/**
|
|
* Simulate touch events for platforms where they aren't generally available.
|
|
*/
|
|
class TouchSimulator {
|
|
/**
|
|
* @param {WindowGlobalTargetActor} windowTarget: The window object we'll use
|
|
* to listen for click and touch events to handle.
|
|
*/
|
|
constructor(windowTarget) {
|
|
this.windowTarget = windowTarget;
|
|
this.simulatorTarget = windowTarget.chromeEventHandler;
|
|
this._currentPickerMap = new Map();
|
|
this.previousScreenY = 0;
|
|
}
|
|
|
|
enabled = false;
|
|
|
|
start() {
|
|
if (this.enabled) {
|
|
// Simulator is already started
|
|
return;
|
|
}
|
|
|
|
EVENTS_TO_HANDLE.forEach(evt => {
|
|
// Only listen trusted events to prevent messing with
|
|
// event dispatched manually within content documents
|
|
this.simulatorTarget.addEventListener(evt, this, true, false);
|
|
});
|
|
|
|
this.enabled = true;
|
|
}
|
|
|
|
stop() {
|
|
if (!this.enabled) {
|
|
// Simulator isn't running
|
|
return;
|
|
}
|
|
EVENTS_TO_HANDLE.forEach(evt => {
|
|
this.simulatorTarget.removeEventListener(evt, this, true);
|
|
});
|
|
this.enabled = false;
|
|
}
|
|
|
|
_isPicking() {
|
|
const types = Object.values(PICKER_TYPES);
|
|
return types.some(type => this._currentPickerMap.get(type));
|
|
}
|
|
|
|
/**
|
|
* Set the state value for one of DevTools pickers (either eyedropper or
|
|
* element picker).
|
|
* If any content picker is currently active, we should not be emulating
|
|
* touch events. Otherwise it is ok to emulate touch events.
|
|
* In theory only one picker can ever be active at a time, but tracking the
|
|
* different pickers independantly avoids race issues in the client code.
|
|
*
|
|
* @param {boolean} state
|
|
* True if the picker is currently active, false otherwise.
|
|
* @param {string} pickerType
|
|
* One of PICKER_TYPES.
|
|
*/
|
|
setElementPickerState(state, pickerType) {
|
|
if (!Object.values(PICKER_TYPES).includes(pickerType)) {
|
|
throw new Error(
|
|
"Unsupported type in setElementPickerState: " + pickerType
|
|
);
|
|
}
|
|
this._currentPickerMap.set(pickerType, state);
|
|
}
|
|
|
|
// eslint-disable-next-line complexity
|
|
handleEvent(evt) {
|
|
// Bail out if devtools is in pick mode in the same tab.
|
|
if (this._isPicking()) {
|
|
return;
|
|
}
|
|
|
|
const content = this.getContent(evt.target);
|
|
if (!content) {
|
|
return;
|
|
}
|
|
|
|
// Ignore all but real mouse event coming from physical mouse
|
|
// (especially ignore mouse event being dispatched from a touch event)
|
|
if (
|
|
evt.button ||
|
|
evt.inputSource != evt.MOZ_SOURCE_MOUSE ||
|
|
evt.isSynthesized
|
|
) {
|
|
return;
|
|
}
|
|
|
|
const eventTarget = this.target;
|
|
let type = "";
|
|
switch (evt.type) {
|
|
case "mouseenter":
|
|
case "mouseover":
|
|
case "mouseout":
|
|
case "mouseleave":
|
|
// Don't propagate events which are not related to touch events
|
|
evt.stopPropagation();
|
|
evt.preventDefault();
|
|
|
|
// We don't want to trigger any visual changes to elements whose content can
|
|
// be modified via hover states. We can avoid this by removing the element's
|
|
// content state.
|
|
InspectorUtils.removeContentState(evt.target, kStateHover);
|
|
break;
|
|
|
|
case "mousedown":
|
|
this.target = evt.target;
|
|
|
|
// If the click-hold feature is enabled, start a timeout to convert long clicks
|
|
// into contextmenu events.
|
|
// Just don't do it if the event occurred on a scrollbar.
|
|
if (isClickHoldEnabled && !evt.originalTarget.closest("scrollbar")) {
|
|
this._contextMenuTimeout = this.sendContextMenu(evt);
|
|
}
|
|
|
|
this.previousScreenY = evt.screenY;
|
|
|
|
type = "touchstart";
|
|
break;
|
|
|
|
case "mousemove": {
|
|
if (!eventTarget) {
|
|
// Don't propagate mousemove event when touchstart event isn't fired
|
|
evt.stopPropagation();
|
|
return;
|
|
}
|
|
|
|
type = "touchmove";
|
|
const deltaY = evt.screenY - this.previousScreenY;
|
|
this.previousScreenY = evt.screenY;
|
|
this.windowTarget.emit("contentScrolled", deltaY);
|
|
break;
|
|
}
|
|
|
|
case "mouseup":
|
|
if (!eventTarget) {
|
|
return;
|
|
}
|
|
this.target = null;
|
|
|
|
content.clearTimeout(this._contextMenuTimeout);
|
|
type = "touchend";
|
|
|
|
// Only register click listener after mouseup to ensure
|
|
// catching only real user click. (Especially ignore click
|
|
// being dispatched on form submit)
|
|
if (evt.detail == 1) {
|
|
this.simulatorTarget.addEventListener("click", this, {
|
|
capture: true,
|
|
once: true,
|
|
});
|
|
}
|
|
break;
|
|
}
|
|
|
|
const target = eventTarget || this.target;
|
|
if (target && type) {
|
|
this.sendTouchEvent(content, evt.clientX, evt.clientY, type);
|
|
}
|
|
|
|
evt.preventDefault();
|
|
evt.stopImmediatePropagation();
|
|
}
|
|
|
|
sendContextMenu({ target, clientX, clientY, screenX, screenY }) {
|
|
const view = target.documentGlobal;
|
|
const evt = new view.PointerEvent("contextmenu", {
|
|
bubbles: true,
|
|
cancelable: true,
|
|
view,
|
|
screenX,
|
|
screenY,
|
|
clientX,
|
|
clientY,
|
|
});
|
|
const content = this.getContent(target);
|
|
const timeout = content.setTimeout(() => {
|
|
target.dispatchEvent(evt);
|
|
}, clickHoldDelay);
|
|
|
|
return timeout;
|
|
}
|
|
|
|
/**
|
|
* Sends a touch action on a given target element.
|
|
*
|
|
* @param {Window} win
|
|
* The target window.
|
|
* @param {number} clientX
|
|
* The `x` screen coordinate relative to the viewport origin.
|
|
* @param {number} clientY
|
|
* The `y` screen coordinate relative to the viewport origin.
|
|
* @param {string} type
|
|
* The type of the touch event.
|
|
*/
|
|
sendTouchEvent(win, clientX, clientY, type) {
|
|
win.synthesizeTouchEvent(
|
|
type,
|
|
[
|
|
{
|
|
identifier: 0,
|
|
offsetX: clientX,
|
|
offsetY: clientY,
|
|
radiiX: 0,
|
|
radiiY: 0,
|
|
},
|
|
],
|
|
0,
|
|
{ isAsyncEnabled: true }
|
|
);
|
|
return true;
|
|
}
|
|
|
|
getContent(target) {
|
|
const win = target?.ownerDocument ? target.documentGlobal : null;
|
|
return win;
|
|
}
|
|
}
|
|
|
|
exports.TouchSimulator = TouchSimulator;
|