305 lines
8.1 KiB
JavaScript
305 lines
8.1 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
|
|
);
|
|
|
|
// Touch state constants are derived from values defined in: nsIDOMWindowUtils.idl
|
|
const TOUCH_CONTACT = 0x02;
|
|
const TOUCH_REMOVE = 0x04;
|
|
|
|
const TOUCH_STATES = {
|
|
touchstart: TOUCH_CONTACT,
|
|
touchmove: TOUCH_CONTACT,
|
|
touchend: TOUCH_REMOVE,
|
|
};
|
|
|
|
const EVENTS_TO_HANDLE = [
|
|
"mousedown",
|
|
"mousemove",
|
|
"mouseup",
|
|
"touchstart",
|
|
"touchend",
|
|
"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();
|
|
}
|
|
|
|
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;
|
|
}
|
|
|
|
// App touchstart & touchend should also be dispatched on the system app
|
|
// to match on-device behavior.
|
|
if (evt.type.startsWith("touch")) {
|
|
const sysFrame = content.realFrameElement;
|
|
if (!sysFrame) {
|
|
return;
|
|
}
|
|
const sysDocument = sysFrame.ownerDocument;
|
|
const sysWindow = sysDocument.defaultView;
|
|
|
|
const touchEvent = sysDocument.createEvent("touchevent");
|
|
const touch = evt.touches[0] || evt.changedTouches[0];
|
|
const point = sysDocument.createTouch(
|
|
sysWindow,
|
|
sysFrame,
|
|
0,
|
|
touch.pageX,
|
|
touch.pageY,
|
|
touch.screenX,
|
|
touch.screenY,
|
|
touch.clientX,
|
|
touch.clientY,
|
|
1,
|
|
1,
|
|
0,
|
|
0
|
|
);
|
|
|
|
const touches = sysDocument.createTouchList(point);
|
|
const targetTouches = touches;
|
|
const changedTouches = touches;
|
|
touchEvent.initTouchEvent(
|
|
evt.type,
|
|
true,
|
|
true,
|
|
sysWindow,
|
|
0,
|
|
false,
|
|
false,
|
|
false,
|
|
false,
|
|
touches,
|
|
targetTouches,
|
|
changedTouches
|
|
);
|
|
sysFrame.dispatchEvent(touchEvent);
|
|
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.startX = evt.pageX;
|
|
this.startY = evt.pageY;
|
|
|
|
type = "touchstart";
|
|
break;
|
|
|
|
case "mousemove":
|
|
if (!eventTarget) {
|
|
// Don't propagate mousemove event when touchstart event isn't fired
|
|
evt.stopPropagation();
|
|
return;
|
|
}
|
|
|
|
type = "touchmove";
|
|
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.synthesizeNativeTouch(content, evt.screenX, evt.screenY, type);
|
|
}
|
|
|
|
evt.preventDefault();
|
|
evt.stopImmediatePropagation();
|
|
}
|
|
|
|
sendContextMenu({ target, clientX, clientY, screenX, screenY }) {
|
|
const view = target.ownerGlobal;
|
|
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;
|
|
}
|
|
|
|
/**
|
|
* Synthesizes a native touch action on a given target element.
|
|
*
|
|
* @param {Window} win
|
|
* The target window.
|
|
* @param {Number} screenX
|
|
* The `x` screen coordinate relative to the screen origin.
|
|
* @param {Number} screenY
|
|
* The `y` screen coordinate relative to the screen origin.
|
|
* @param {String} type
|
|
* A key appearing in the TOUCH_STATES associative array.
|
|
*/
|
|
synthesizeNativeTouch(win, screenX, screenY, type) {
|
|
// Native events work in device pixels.
|
|
const utils = win.windowUtils;
|
|
const deviceScale = win.devicePixelRatio;
|
|
const pt = { x: screenX * deviceScale, y: screenY * deviceScale };
|
|
|
|
utils.sendNativeTouchPoint(0, TOUCH_STATES[type], pt.x, pt.y, 1, 90, null);
|
|
return true;
|
|
}
|
|
|
|
getContent(target) {
|
|
const win = target?.ownerDocument ? target.ownerGlobal : null;
|
|
return win;
|
|
}
|
|
}
|
|
|
|
exports.TouchSimulator = TouchSimulator;
|