/* eslint-disable no-nested-ternary */
/**
* EventUtils provides some utility methods for creating and sending DOM events .
*
* When adding methods to this file , please add a performance test for it .
*/
// Certain functions assume this is loaded into browser window scope.
// This is modifiable because certain chrome tests create their own gBrowser.
/* global gBrowser:true */
// This file is used both in privileged and unprivileged contexts, so we have to
// be careful about our access to Components.interfaces. We also want to avoid
// naming collisions with anything that might be defined in the scope that imports
// this script.
//
// Even if the real |Components| doesn't exist, we might shim in a simple JS
// placebo for compat. An easy way to differentiate this from the real thing
// is whether the property is read-only or not. The real |Components| property
// is read-only.
/* global _EU_Ci, _EU_Cc, _EU_Cu, _EU_ChromeUtils, _EU_OS */
window.__defineGetter__("_EU_Ci" , function () {
var c = Object.getOwnPropertyDescriptor(window, "Components" );
return c && c.value && !c.writable ? Ci : SpecialPowers.Ci;
});
window.__defineGetter__("_EU_Cc" , function () {
var c = Object.getOwnPropertyDescriptor(window, "Components" );
return c && c.value && !c.writable ? Cc : SpecialPowers.Cc;
});
window.__defineGetter__("_EU_Cu" , function () {
var c = Object.getOwnPropertyDescriptor(window, "Components" );
return c && c.value && !c.writable ? Cu : SpecialPowers.Cu;
});
window.__defineGetter__("_EU_ChromeUtils" , function () {
var c = Object.getOwnPropertyDescriptor(window, "ChromeUtils" );
return c && c.value && !c.writable ? ChromeUtils : SpecialPowers.ChromeUtils;
});
window.__defineGetter__("_EU_OS" , function () {
delete this ._EU_OS;
try {
this ._EU_OS = _EU_ChromeUtils.importESModule(
"resource://gre/modules/AppConstants.sys.mjs"
).platform;
} catch (ex) {
this ._EU_OS = null ;
}
return this ._EU_OS;
});
function _EU_isMac(aWindow = window) {
if (window._EU_OS) {
return window._EU_OS == "macosx" ;
}
if (aWindow) {
try {
return aWindow.navigator.platform.indexOf("Mac" ) > -1 ;
} catch (ex) {}
}
return navigator.platform.indexOf("Mac" ) > -1 ;
}
function _EU_isWin(aWindow = window) {
if (window._EU_OS) {
return window._EU_OS == "win" ;
}
if (aWindow) {
try {
return aWindow.navigator.platform.indexOf("Win" ) > -1 ;
} catch (ex) {}
}
return navigator.platform.indexOf("Win" ) > -1 ;
}
function _EU_isLinux(aWindow = window) {
if (window._EU_OS) {
return window._EU_OS == "linux" ;
}
if (aWindow) {
try {
return aWindow.navigator.platform.startsWith("Linux" );
} catch (ex) {}
}
return navigator.platform.startsWith("Linux" );
}
function _EU_isAndroid(aWindow = window) {
if (window._EU_OS) {
return window._EU_OS == "android" ;
}
if (aWindow) {
try {
return aWindow.navigator.userAgent.includes("Android" );
} catch (ex) {}
}
return navigator.userAgent.includes("Android" );
}
function _EU_maybeWrap(o) {
// We're used in some contexts where there is no SpecialPowers and also in
// some where it exists but has no wrap() method. And this is somewhat
// independent of whether window.Components is a thing...
var haveWrap = false ;
try {
haveWrap = SpecialPowers.wrap != undefined;
} catch (e) {
// Just leave it false.
}
if (!haveWrap) {
// Not much we can do here.
return o;
}
var c = Object.getOwnPropertyDescriptor(window, "Components" );
return c && c.value && !c.writable ? o : SpecialPowers.wrap(o);
}
function _EU_maybeUnwrap(o) {
var haveWrap = false ;
try {
haveWrap = SpecialPowers.unwrap != undefined;
} catch (e) {
// Just leave it false.
}
if (!haveWrap) {
// Not much we can do here.
return o;
}
var c = Object.getOwnPropertyDescriptor(window, "Components" );
return c && c.value && !c.writable ? o : SpecialPowers.unwrap(o);
}
function _EU_getPlatform() {
if (_EU_isWin()) {
return "windows" ;
}
if (_EU_isMac()) {
return "mac" ;
}
if (_EU_isAndroid()) {
return "android" ;
}
if (_EU_isLinux()) {
return "linux" ;
}
return "unknown" ;
}
function _EU_roundDevicePixels(aMaybeFractionalPixels) {
return Math.floor(aMaybeFractionalPixels + 0 .5 );
}
/**
* Return the additional version details of Windows , e . g . , " 7309 " of build
* number " 6100 . 7309 " .
*/
function _EU_getWindowsUBR() {
try {
const { WindowsRegistry } = _EU_ChromeUtils.importESModule(
"resource://gre/modules/WindowsRegistry.sys.mjs"
);
const ubr = WindowsRegistry.readRegKey(
_EU_Ci.nsIWindowsRegKey.ROOT_KEY_LOCAL_MACHINE,
"SOFTWARE\\Microsoft\\Windows NT\\CurrentVersion" ,
"UBR" ,
_EU_Ci.nsIWindowsRegKey.WOW64_64
);
if (Number.isInteger(ubr)) {
return ubr;
}
} catch (ex) {
if (typeof info == "function" ) {
info(`Error: ${ex}`);
}
}
return NaN;
}
/**
* promiseElementReadyForUserInput ( ) dispatches mousemove events to aElement
* and waits one of them for a while . Then , returns " resolved " state when it ' s
* successfully received . Otherwise , if it couldn ' t receive mousemove event on
* it , this throws an exception . So , aElement must be an element which is
* assumed non - collapsed visible element in the window .
*
* This is useful if you need to synthesize mouse events via the main process
* but your test cannot check whether the element is now in APZ to deliver
* a user input event .
*/
async function promiseElementReadyForUserInput(
aElement,
aWindow = window,
aLogFunc = null
) {
if (typeof aElement == "string" ) {
aElement = aWindow.document.getElementById(aElement);
}
function waitForMouseMoveForHittest() {
return new Promise(resolve => {
let timeout;
const onHit = () => {
if (aLogFunc) {
aLogFunc("mousemove received" );
}
aWindow.clearInterval(timeout);
resolve(true );
};
aElement.addEventListener("mousemove" , onHit, {
capture: true ,
once: true ,
});
timeout = aWindow.setInterval(() => {
if (aLogFunc) {
aLogFunc("mousemove not received in this 300ms" );
}
aElement.removeEventListener("mousemove" , onHit, {
capture: true ,
});
resolve(false );
}, 300 );
synthesizeMouseAtCenter(aElement, { type: "mousemove" }, aWindow);
});
}
for (let i = 0 ; i < 20 ; i++) {
if (await waitForMouseMoveForHittest()) {
return Promise.resolve();
}
}
throw new Error("The element or the window did not become interactive" );
}
function getElement(id) {
return typeof id == "string" ? document.getElementById(id) : id;
}
this .$ = this .getElement;
function computeButton(aEvent) {
if (typeof aEvent.button != "undefined" ) {
return aEvent.button;
}
return aEvent.type == "contextmenu" ? 2 : 0 ;
}
/**
* Send a mouse event to the node aTarget ( aTarget can be an id , or an
* actual node ) . The " event " passed in to aEvent is just a JavaScript
* object with the properties set that the real mouse event object should
* have . This includes the type of the mouse event . Pretty much all those
* properties are optional .
* E . g . to send an click event to the node with id ' node ' you might do this :
*
* ` ` sendMouseEvent ( { type : ' click ' } , ' node ' ) ; ` `
*/
function sendMouseEvent(aEvent, aTarget, aWindow) {
if (
![
"click" ,
"contextmenu" ,
"dblclick" ,
"mousedown" ,
"mouseup" ,
"mouseover" ,
"mouseout" ,
].includes(aEvent.type)
) {
throw new Error(
"sendMouseEvent doesn't know about event type '" + aEvent.type + "'"
);
}
if (!aWindow) {
aWindow = window;
}
if (typeof aTarget == "string" ) {
aTarget = aWindow.document.getElementById(aTarget);
}
let dict = {
bubbles: true ,
cancelable: true ,
view: aWindow,
detail:
aEvent.detail ||
// eslint-disable-next-line no-nested-ternary
(aEvent.type == "click" ||
aEvent.type == "mousedown" ||
aEvent.type == "mouseup"
? 1
: aEvent.type == "dblclick"
? 2
: 0 ),
screenX: aEvent.screenX || 0 ,
screenY: aEvent.screenY || 0 ,
clientX: aEvent.clientX || 0 ,
clientY: aEvent.clientY || 0 ,
ctrlKey: aEvent.ctrlKey || false ,
altKey: aEvent.altKey || false ,
shiftKey: aEvent.shiftKey || false ,
metaKey: aEvent.metaKey || false ,
button: computeButton(aEvent),
// FIXME: Set buttons
relatedTarget: aEvent.relatedTarget || null ,
};
let event =
aEvent.type == "click" || aEvent.type == "contextmenu"
? new aWindow.PointerEvent(aEvent.type, dict)
: new aWindow.MouseEvent(aEvent.type, dict);
// If documentURIObject exists or `window` is a stub object, we're in
// a chrome scope, so don't bother trying to go through SpecialPowers.
if (!window.document || window.document.documentURIObject) {
return aTarget.dispatchEvent(event);
}
return SpecialPowers.dispatchEvent(aWindow, aTarget, event);
}
function isHidden(aElement) {
var box = aElement.getBoundingClientRect();
return box.width == 0 && box.height == 0 ;
}
/**
* Send a drag event to the node aTarget ( aTarget can be an id , or an
* actual node ) . The " event " passed in to aEvent is just a JavaScript
* object with the properties set that the real drag event object should
* have . This includes the type of the drag event .
*
* @ returns { boolean }
*/
function sendDragEvent(aEvent, aTarget, aWindow = window) {
if (
![
"drag" ,
"dragstart" ,
"dragend" ,
"dragover" ,
"dragenter" ,
"dragleave" ,
"drop" ,
].includes(aEvent.type)
) {
throw new Error(
"sendDragEvent doesn't know about event type '" + aEvent.type + "'"
);
}
if (typeof aTarget == "string" ) {
aTarget = aWindow.document.getElementById(aTarget);
}
/*
* Drag event cannot be performed if the element is hidden , except ' dragend '
* event where the element can becomes hidden after start dragging .
*/
if (aEvent.type != "dragend" && isHidden(aTarget)) {
var targetName = aTarget.nodeName;
if ("id" in aTarget && aTarget.id) {
targetName += "#" + aTarget.id;
}
throw new Error(`${aEvent.type} event target ${targetName} is hidden`);
}
var event = aWindow.document.createEvent("DragEvent" );
var typeArg = aEvent.type;
var canBubbleArg = true ;
var cancelableArg = true ;
var viewArg = aWindow;
var detailArg = aEvent.detail || 0 ;
var screenXArg = aEvent.screenX || 0 ;
var screenYArg = aEvent.screenY || 0 ;
var clientXArg = aEvent.clientX || 0 ;
var clientYArg = aEvent.clientY || 0 ;
var ctrlKeyArg = aEvent.ctrlKey || false ;
var altKeyArg = aEvent.altKey || false ;
var shiftKeyArg = aEvent.shiftKey || false ;
var metaKeyArg = aEvent.metaKey || false ;
var buttonArg = computeButton(aEvent);
var relatedTargetArg = aEvent.relatedTarget || null ;
var dataTransfer = aEvent.dataTransfer || null ;
event.initDragEvent(
typeArg,
canBubbleArg,
cancelableArg,
viewArg,
detailArg,
Math.round(screenXArg),
Math.round(screenYArg),
Math.round(clientXArg),
Math.round(clientYArg),
ctrlKeyArg,
altKeyArg,
shiftKeyArg,
metaKeyArg,
buttonArg,
relatedTargetArg,
dataTransfer
);
if (aEvent._domDispatchOnly) {
return aTarget.dispatchEvent(event);
}
var utils = _getDOMWindowUtils(aWindow);
return utils.dispatchDOMEventViaPresShellForTesting(aTarget, event);
}
/**
* Send the char aChar to the focused element . This method handles casing of
* chars ( sends the right charcode , and sends a shift key for uppercase chars ) .
* No other modifiers are handled at this point .
*
* For now this method only works for ASCII characters and emulates the shift
* key state on US keyboard layout .
*/
function sendChar(aChar, aWindow) {
var hasShift;
// Emulate US keyboard layout for the shiftKey state.
switch (aChar) {
case "!" :
case "@" :
case "#" :
case "$" :
case "%" :
case "^" :
case "&" :
case "*" :
case "(" :
case ")" :
case "_" :
case "+" :
case "{" :
case "}" :
case ":" :
case '"' :
case "|" :
case "<" :
case ">" :
case "?" :
hasShift = true ;
break ;
default :
hasShift =
aChar.toLowerCase() != aChar.toUpperCase() &&
aChar == aChar.toUpperCase();
break ;
}
synthesizeKey(aChar, { shiftKey: hasShift }, aWindow);
}
/**
* Send the string aStr to the focused element .
*
* For now this method only works for ASCII characters and emulates the shift
* key state on US keyboard layout .
*/
function sendString(aStr, aWindow) {
for (let i = 0 ; i < aStr.length; ++i) {
// Do not split a surrogate pair to call synthesizeKey. Dispatching two
// sets of keydown and keyup caused by two calls of synthesizeKey is not
// good behavior. It could happen due to a bug, but a surrogate pair should
// be introduced with one key press operation. Therefore, calling it with
// a surrogate pair is the right thing.
// Note that TextEventDispatcher will consider whether a surrogate pair
// should cause one or two keypress events automatically. Therefore, we
// don't need to check the related prefs here.
if (
(aStr.charCodeAt(i) & 0 xfc00) == 0 xd800 &&
i + 1 < aStr.length &&
(aStr.charCodeAt(i + 1 ) & 0 xfc00) == 0 xdc00
) {
sendChar(aStr.substring(i, i + 2 ), aWindow);
i++;
} else {
sendChar(aStr.charAt(i), aWindow);
}
}
}
/**
* Send the non - character key aKey to the focused node .
* The name of the key should be the part that comes after ` ` DOM_VK_ ` ` in the
* KeyEvent constant name for this key .
* No modifiers are handled at this point .
*/
function sendKey(aKey, aWindow) {
var keyName = "VK_" + aKey.toUpperCase();
synthesizeKey(keyName, { shiftKey: false }, aWindow);
}
/**
* Parse the key modifier flags from aEvent . Used to share code between
* synthesizeMouse and synthesizeKey .
*/
function _parseModifiers(aEvent, aWindow = window) {
var nsIDOMWindowUtils = _EU_Ci.nsIDOMWindowUtils;
var mval = 0 ;
if (aEvent.shiftKey) {
mval |= nsIDOMWindowUtils.MODIFIER_SHIFT;
}
if (aEvent.ctrlKey) {
mval |= nsIDOMWindowUtils.MODIFIER_CONTROL;
}
if (aEvent.altKey) {
mval |= nsIDOMWindowUtils.MODIFIER_ALT;
}
if (aEvent.metaKey) {
mval |= nsIDOMWindowUtils.MODIFIER_META;
}
if (aEvent.accelKey) {
mval |= _EU_isMac(aWindow)
? nsIDOMWindowUtils.MODIFIER_META
: nsIDOMWindowUtils.MODIFIER_CONTROL;
}
if (aEvent.altGrKey) {
mval |= nsIDOMWindowUtils.MODIFIER_ALTGRAPH;
}
if (aEvent.capsLockKey) {
mval |= nsIDOMWindowUtils.MODIFIER_CAPSLOCK;
}
if (aEvent.fnKey) {
mval |= nsIDOMWindowUtils.MODIFIER_FN;
}
if (aEvent.fnLockKey) {
mval |= nsIDOMWindowUtils.MODIFIER_FNLOCK;
}
if (aEvent.numLockKey) {
mval |= nsIDOMWindowUtils.MODIFIER_NUMLOCK;
}
if (aEvent.scrollLockKey) {
mval |= nsIDOMWindowUtils.MODIFIER_SCROLLLOCK;
}
if (aEvent.symbolKey) {
mval |= nsIDOMWindowUtils.MODIFIER_SYMBOL;
}
if (aEvent.symbolLockKey) {
mval |= nsIDOMWindowUtils.MODIFIER_SYMBOLLOCK;
}
return mval;
}
/**
* Return the drag service . Note that if we ' re in the headless mode , this
* may return null because the service may be never instantiated ( e . g . , on
* Linux ) .
*/
function getDragService() {
try {
return _EU_Cc["@mozilla.org/widget/dragservice;1" ].getService(
_EU_Ci.nsIDragService
);
} catch (e) {
// If we're in the headless mode, the drag service may be never
// instantiated. In this case, an exception is thrown. Let's ignore
// any exceptions since without the drag service, nobody can create a
// drag session.
return null ;
}
}
/**
* End drag session if there is .
*
* TODO : This should synthesize " drop " if necessary .
*
* @ param left X offset in the viewport
* @ param top Y offset in the viewport
* @ param aEvent The event data , the modifiers are applied to the
* " dragend " event .
* @ param aWindow The window .
* @ return true if handled . In this case , the caller should not
* synthesize DOM events basically .
*/
function _maybeEndDragSession(left, top, aEvent, aWindow) {
let utils = _getDOMWindowUtils(aWindow);
const dragSession = utils.dragSession;
if (!dragSession) {
return false ;
}
// FIXME: If dragSession.dragAction is not
// nsIDragService.DRAGDROP_ACTION_NONE nor aEvent.type is not `keydown`, we
// need to synthesize a "drop" event or call setDragEndPointForTests here to
// set proper left/top to `dragend` event.
try {
dragSession.endDragSession(false , _parseModifiers(aEvent, aWindow));
} catch (e) {}
return true ;
}
function _maybeSynthesizeDragOver(left, top, aEvent, aWindow) {
let utils = _getDOMWindowUtils(aWindow);
const dragSession = utils.dragSession;
if (!dragSession) {
return false ;
}
const target = aWindow.document.elementFromPoint(left, top);
if (target) {
sendDragEvent(
createDragEventObject(
"dragover" ,
target,
aWindow,
dragSession.dataTransfer,
{
accelKey: aEvent.accelKey,
altKey: aEvent.altKey,
altGrKey: aEvent.altGrKey,
ctrlKey: aEvent.ctrlKey,
metaKey: aEvent.metaKey,
shiftKey: aEvent.shiftKey,
capsLockKey: aEvent.capsLockKey,
fnKey: aEvent.fnKey,
fnLockKey: aEvent.fnLockKey,
numLockKey: aEvent.numLockKey,
scrollLockKey: aEvent.scrollLockKey,
symbolKey: aEvent.symbolKey,
symbolLockKey: aEvent.symbolLockKey,
}
),
target,
aWindow
);
}
return true ;
}
/**
* @ typedef { object } MouseEventData
*
* @ property { string } [ accessKey ] - The character or key associated with
* the access key event . Typically a single character used to activate a UI
* element via keyboard shortcuts ( e . g . , Alt + accessKey ) .
* @ property { boolean } [ altKey ] - If set to ` true ` , the Alt key will be
* considered pressed .
* @ property { boolean } [ asyncEnabled ] - If ` true ` , the event is
* dispatched to the parent process through APZ , without being injected
* into the OS event queue .
* @ property { number } [ button = 0 ] - Button to synthesize .
* @ property { number } [ buttons ] - Indicates which mouse buttons are pressed
* when a mouse event is triggered .
* @ property { number } [ clickCount = 1 ] - Number of clicks that have to be performed .
* @ property { boolean } [ ctrlKey ] - If set to ` true ` , the Ctrl key will
* be considered pressed .
* @ property { number } [ id ] - A unique identifier for the pointer causing the event .
* @ property { number } [ inputSource ] - Input source , see MouseEvent for values .
* Defaults to MouseEvent . MOZ_SOURCE_MOUSE .
* @ property { boolean } [ isSynthesized ] - Controls Event . isSynthesized value that
* helps identifying test related events
* @ property { boolean } [ isWidgetEventSynthesized ] - Controls WidgetMouseEvent . mReason value .
* @ property { boolean } [ metaKey ] - If set to ` true ` , the Meta key will
* be considered pressed .
* @ property { number } [ pressure = 0 ] - Touch input pressure ( 0 . 0 - > 1 . 0 ) .
* @ property { boolean } [ shiftKey ] - If set to ` true ` , the Shift key will
* be considered pressed .
* @ property { string } [ type ] - Event type to synthesize . If not specified
* a ` mousedown ` followed by a ` mouseup ` are performed .
*
* @ see nsIDOMWindowUtils . sendMouseEvent
*/
/**
* Synthesize a mouse event on a target .
*
* The actual client point is determined by taking the aTarget ' s client box
* and offsetting it by aOffsetX and aOffsetY .
*
* Note that additional events may be fired as a result of this call . For
* instance , typically a click event will be fired as a result of a
* mousedown and mouseup in sequence .
*
* @ param { Element } aTarget - DOM element to dispatch the event on .
* @ param { number } aOffsetX - X offset in CSS pixels from the element ’ s left edge .
* @ param { number } aOffsetY - Y offset in CSS pixels from the element ’ s top edge .
* @ param { MouseEventData } aEvent - Details of the mouse event to dispatch .
* @ param { DOMWindow } [ aWindow = window ] - DOM window used to dispatch the event .
* @ param { Function } [ aCallback ] - A callback function that is invoked when the
* mouse event is dispatched .
*
* @ returns { boolean } Whether the event had preventDefault ( ) called on it .
*/
function synthesizeMouse(
aTarget,
aOffsetX,
aOffsetY,
aEvent,
aWindow,
aCallback
) {
var rect = aTarget.getBoundingClientRect();
return synthesizeMouseAtPoint(
rect.left + aOffsetX,
rect.top + aOffsetY,
aEvent,
aWindow,
aCallback
);
}
/**
* Synthesize a mouse event in ` aWindow ` at a point .
*
* ` nsIDOMWindowUtils . sendMouseEvent ` takes floats for the coordinates .
* Therefore , don ' t round or truncate the values .
*
* Note that additional events may be fired as a result of this call . For
* instance , typically a click event will be fired as a result of a
* mousedown and mouseup in sequence .
*
* @ param { number } aLeft - Floating - point value for the X offset in CSS pixels .
* @ param { number } aTop - Floating - point value for the Y offset in CSS pixels .
* @ param { MouseEventData } aEvent - Details of the mouse event to dispatch .
* @ param { DOMWindow } [ aWindow = window ] - DOM window used to dispatch the event .
* @ param { Function } [ aCallback ] - A callback function that is invoked when the
* mouse event is dispatched .
*
* @ returns { boolean } Whether the event had preventDefault ( ) called on it .
*/
function synthesizeMouseAtPoint(
aLeft,
aTop,
aEvent,
aWindow = window,
aCallback
) {
if (aEvent.allowToHandleDragDrop) {
if (aEvent.type == "mouseup" || !aEvent.type) {
if (_maybeEndDragSession(aLeft, aTop, aEvent, aWindow)) {
return false ;
}
} else if (aEvent.type == "mousemove" ) {
if (_maybeSynthesizeDragOver(aLeft, aTop, aEvent, aWindow)) {
return false ;
}
}
}
var utils = _getDOMWindowUtils(aWindow);
var defaultPrevented = false ;
if (utils) {
var button = computeButton(aEvent);
var clickCount = aEvent.clickCount || 1 ;
var modifiers = _parseModifiers(aEvent, aWindow);
// aWindow might be cross-origin from us.
var MouseEvent = _EU_maybeWrap(aWindow).MouseEvent;
// Default source to mouse.
var inputSource =
"inputSource" in aEvent
? aEvent.inputSource
: MouseEvent.MOZ_SOURCE_MOUSE;
// Compute a pointerId if needed.
var id;
if ("id" in aEvent) {
id = aEvent.id;
} else {
var isFromPen = inputSource === MouseEvent.MOZ_SOURCE_PEN;
id = isFromPen
? utils.DEFAULT_PEN_POINTER_ID
: utils.DEFAULT_MOUSE_POINTER_ID;
}
// FYI: Window.synthesizeMouseEvent takes floats for the coordinates.
// Therefore, don't round/truncate the fractional values.
const isDOMEventSynthesized =
"isSynthesized" in aEvent ? aEvent.isSynthesized : true ;
const isWidgetEventSynthesized =
"isWidgetEventSynthesized" in aEvent
? aEvent.isWidgetEventSynthesized
: false ;
const isAsyncEnabled =
"asyncEnabled" in aEvent ? aEvent.asyncEnabled : false ;
if ("type" in aEvent && aEvent.type) {
defaultPrevented = _EU_maybeWrap(aWindow).synthesizeMouseEvent(
aEvent.type,
aLeft,
aTop,
{
identifier: id,
button,
buttons: aEvent.buttons,
clickCount,
modifiers,
pressure: aEvent.pressure,
inputSource,
},
{
isDOMEventSynthesized,
isWidgetEventSynthesized,
isAsyncEnabled,
},
aCallback
);
} else {
_EU_maybeWrap(aWindow).synthesizeMouseEvent(
"mousedown" ,
aLeft,
aTop,
{
identifier: id,
button,
buttons: aEvent.buttons,
clickCount,
modifiers,
pressure: aEvent.pressure,
inputSource,
},
{
isDOMEventSynthesized,
isWidgetEventSynthesized,
isAsyncEnabled,
},
aCallback
);
_EU_maybeWrap(aWindow).synthesizeMouseEvent(
"mouseup" ,
aLeft,
aTop,
{
identifier: id,
button,
buttons: aEvent.buttons,
clickCount,
modifiers,
pressure: aEvent.pressure,
inputSource,
},
{
isDOMEventSynthesized,
isWidgetEventSynthesized,
isAsyncEnabled,
},
aCallback
);
}
}
return defaultPrevented;
}
/**
* Synthesize a mouse event at the center of ` aTarget ` .
*
* Note that additional events may be fired as a result of this call . For
* instance , typically a click event will be fired as a result of a
* mousedown and mouseup in sequence .
*
* @ param { Element } aTarget - DOM element to dispatch the event on .
* @ param { MouseEventData } aEvent - Details of the mouse event to dispatch .
* @ param { DOMWindow } [ aWindow = window ] - DOM window used to dispatch the event .
* @ param { Function } [ aCallback ] - A callback function that is invoked when the
* mouse event is dispatched .
*
* @ returns { boolean } Whether the event had preventDefault ( ) called on it .
*/
function synthesizeMouseAtCenter(aTarget, aEvent, aWindow, aCallback) {
var rect = aTarget.getBoundingClientRect();
return synthesizeMouse(
aTarget,
rect.width / 2 ,
rect.height / 2 ,
aEvent,
aWindow,
aCallback
);
}
/**
* @ typedef { object } TouchEventData
* @ property { boolean } [ asyncEnabled ] - If ` true ` , the event is
* dispatched to the parent process through APZ , without being injected
* into the OS event queue .
* @ property { string } [ type ] - The touch event type . If undefined ,
* " touchstart " and " touchend " will be synthesized at same point .
* @ property { number | number [ ] } [ id ] - The touch id . If you don ' t specify this ,
* default touch id will be used for first touch and further touch ids
* are the values incremented from the first id .
* @ property { number | number [ ] } [ rx ] - The X radius in CSS pixels of the touch
* @ property { number | number [ ] } [ ry ] - The Y radius in CSS pixels of the touch
* @ property { number | number [ ] } [ angle ] - The angle in degrees
* @ property { number | number [ ] } [ force ] - The force of the touch
* @ property { number | number [ ] } [ tiltX ] - The X tilt of the touch
* @ property { number | number [ ] } [ tiltY ] - The Y tilt of the touch
* @ property { number | number [ ] } [ twist ] - The twist of the touch
* @ property { number | number [ ] } [ altitudeAngle ] - The altitude angle of the touch
* @ property { number | number [ ] } [ azimuthAngle ] - The azimuth angle of the touch
*/
/**
* Synthesize one or more touches on aTarget . aTarget can be either Element
* or Array of Elements . aOffsetX , aOffsetY , aEvent . id , aEvent . rx , aEvent . ry ,
* aEvent . angle , aEvent . force , aEvent . tiltX , aEvent . tiltY and aEvent . twist can
* be either number or array of numbers ( can be mixed ) . If you specify array
* to synthesize a multi - touch , you need to specify same length arrays . If
* you don ' t specify array to them , same values ( or computed default values for
* aEvent . id ) are used for all touches .
*
* @ param { Element | Element [ ] } aTarget - The target element which you specify
* relative offset from its top - left .
* @ param { number | number [ ] } aOffsetX - The relative offset from left of aTarget .
* @ param { number | number [ ] } aOffsetY - The relative offset from top of aTarget .
* @ param { TouchEventData } aEvent - Details of the touch event to dispatch
* @ param { DOMWindow } [ aWindow ] - DOM window used to dispatch the event .
* @ param { Function } [ aCallback ] - A callback function that is invoked when the
* touch event is dispatched .
*
* @ returns true if and only if aEvent . type is specified and default of the
* event is prevented .
*/
function synthesizeTouch(
aTarget,
aOffsetX,
aOffsetY,
aEvent = {},
aWindow = window,
aCallback
) {
let rectX, rectY;
if (Array.isArray(aTarget)) {
let lastTarget, lastTargetRect;
aTarget.forEach(target => {
const rect =
target == lastTarget ? lastTargetRect : target.getBoundingClientRect();
rectX.push(rect.left);
rectY.push(rect.top);
lastTarget = target;
lastTargetRect = rect;
});
} else {
const rect = aTarget.getBoundingClientRect();
rectX = [rect.left];
rectY = [rect.top];
}
const offsetX = (() => {
if (Array.isArray(aOffsetX)) {
let ret = [];
aOffsetX.forEach((value, index) => {
ret.push(value + rectX[Math.min(index, rectX.length - 1 )]);
});
return ret;
}
return aOffsetX + rectX[0 ];
})();
const offsetY = (() => {
if (Array.isArray(aOffsetY)) {
let ret = [];
aOffsetY.forEach((value, index) => {
ret.push(value + rectY[Math.min(index, rectY.length - 1 )]);
});
return ret;
}
return aOffsetY + rectY[0 ];
})();
return synthesizeTouchAtPoint(offsetX, offsetY, aEvent, aWindow, aCallback);
}
/**
* Synthesize one or more touches at the points . aLeft , aTop , aEvent . id ,
* aEvent . rx , aEvent . ry , aEvent . angle , aEvent . force , aEvent . tiltX , aEvent . tiltY
* and aEvent . twist can be either number or array of numbers ( can be mixed ) .
* If you specify array to synthesize a multi - touch , you need to specify same
* length arrays . If you don ' t specify array to them , same values are used for
* all touches .
*
* @ param { number | number [ ] } aLeft - The relative offset from left of aTarget .
* @ param { number | number [ ] } aTop - The relative offset from top of aTarget .
* @ param { TouchEventData } aEvent - Details of the touch event to dispatch
* @ param { DOMWindow } [ aWindow = window ] - DOM window used to dispatch the event .
* @ param { Function } [ aCallback ] - A callback function that is invoked when the
* touch event is dispatched .
*
* @ returns true if and only if aEvent . type is specified and default of the
* event is prevented .
*/
function synthesizeTouchAtPoint(
aLeft,
aTop,
aEvent = {},
aWindow = window,
aCallback
) {
let utils = _getDOMWindowUtils(aWindow);
if (!utils) {
return false ;
}
if (
Array.isArray(aLeft) &&
Array.isArray(aTop) &&
aLeft.length != aTop.length
) {
throw new Error(`aLeft and aTop should be same length array`);
}
const arrayLength = Array.isArray(aLeft)
? aLeft.length
: Array.isArray(aTop)
? aTop.length
: 1 ;
function throwExceptionIfDifferentLengthArray(aArray, aName) {
if (Array.isArray(aArray) && arrayLength !== aArray.length) {
throw new Error(`${aName} is different length array`);
}
}
const leftArray = (() => {
if (Array.isArray(aLeft)) {
for (let i = 0 ; i < aLeft.length; i++) {
aLeft[i] = _EU_roundDevicePixels(aLeft[i]);
}
return aLeft;
}
return new Array(arrayLength).fill(_EU_roundDevicePixels(aLeft));
})();
const topArray = (() => {
if (Array.isArray(aTop)) {
throwExceptionIfDifferentLengthArray(aTop, "aTop" );
for (let i = 0 ; i < aTop.length; i++) {
aTop[i] = _EU_roundDevicePixels(aTop[i]);
}
return aTop;
}
return new Array(arrayLength).fill(_EU_roundDevicePixels(aTop));
})();
const idArray = (() => {
if ("id" in aEvent && Array.isArray(aEvent.id)) {
throwExceptionIfDifferentLengthArray(aEvent.id, "aEvent.id" );
return aEvent.id;
}
let id = aEvent.id || utils.DEFAULT_TOUCH_POINTER_ID;
let ret = [];
for (let i = 0 ; i < arrayLength; i++) {
ret.push(id++);
}
return ret;
})();
function getSameLengthArrayOfEventProperty(aProperty, aDefaultValue) {
if (aProperty in aEvent && Array.isArray(aEvent[aProperty])) {
throwExceptionIfDifferentLengthArray(
aEvent.rx,
arrayLength,
`aEvent.${aProperty}`
);
return aEvent[aProperty];
}
return new Array(arrayLength).fill(aEvent[aProperty] || aDefaultValue);
}
const rxArray = getSameLengthArrayOfEventProperty("rx" , 1 );
const ryArray = getSameLengthArrayOfEventProperty("ry" , 1 );
const angleArray = getSameLengthArrayOfEventProperty("angle" , 0 );
const forceArray = getSameLengthArrayOfEventProperty("force" );
const tiltXArray = getSameLengthArrayOfEventProperty("tiltX" , 0 );
const tiltYArray = getSameLengthArrayOfEventProperty("tiltY" , 0 );
const twistArray = getSameLengthArrayOfEventProperty("twist" , 0 );
const altitudeAngleArray = getSameLengthArrayOfEventProperty(
"altitudeAngle" ,
undefined
);
const azimuthAngleArray = getSameLengthArrayOfEventProperty(
"azimuthAngle" ,
undefined
);
const touches = [];
for (let i = 0 ; i < arrayLength; i++) {
touches.push({
identifier: idArray[i],
offsetX: leftArray[i],
offsetY: topArray[i],
radiiX: rxArray[i],
radiiY: ryArray[i],
rotationAngle: angleArray[i],
pressure: forceArray[i],
tiltX: tiltXArray[i],
tiltY: tiltYArray[i],
twist: twistArray[i],
altitudeAngle: altitudeAngleArray[i],
azimuthAngle: azimuthAngleArray[i],
});
}
const args = [
touches,
_parseModifiers(aEvent, aWindow),
{
isAsyncEnabled: aEvent.asyncEnabled || false ,
isPen: aEvent.mozInputSource === "pen" ,
isDOMEventSynthesized: aEvent.isSynthesized,
},
];
if ("type" in aEvent && aEvent.type) {
return _EU_maybeWrap(aWindow).synthesizeTouchEvent(
aEvent.type,
...args,
aCallback
);
}
_EU_maybeWrap(aWindow).synthesizeTouchEvent("touchstart" , ...args);
_EU_maybeWrap(aWindow).synthesizeTouchEvent("touchend" , ...args, aCallback);
return false ;
}
/**
* Synthesize one or more touches at the center of your target
*
* @ param { Element | Element [ ] } aTarget - The target element
* @ param { TouchEventData } aEvent - Details of the touch event to dispatch
* @ param { DOMWindow } [ aWindow = window ] - DOM window used to dispatch the event .
* @ param { Function } [ aCallback ] - A callback function that is invoked when the
* touch event is dispatched .
*
* @ returns { boolean } Whether the event had preventDefault ( ) called on it .
*/
function synthesizeTouchAtCenter(aTarget, aEvent, aWindow, aCallback) {
var rect = aTarget.getBoundingClientRect();
return synthesizeTouchAtPoint(
rect.left + rect.width / 2 ,
rect.top + rect.height / 2 ,
aEvent,
aWindow,
aCallback
);
}
/**
* @ typedef { object } WheelEventData
* @ property { string } [ accessKey ] - The character or key associated with
* the access key event . Typically a single character used to activate a UI
* element via keyboard shortcuts ( e . g . , Alt + accessKey ) .
* @ property { boolean } [ altKey ] - If set to ` true ` , the Alt key will be
* considered pressed .
* @ property { boolean } [ asyncEnabled ] - If ` true ` , the event is
* dispatched to the parent process through APZ , without being injected
* into the OS event queue .
* @ property { boolean } [ ctrlKey ] - If set to ` true ` , the Ctrl key will
* be considered pressed .
* @ property { number } [ deltaMode = WheelEvent . DOM_DELTA_PIXEL ] - Delta Mode
* for scrolling ( pixel , line , or page ) , which must be one of the
* ` WheelEvent . DOM_DELTA_ * ` constants .
* @ property { number } [ deltaX = 0 ] - Floating - point value in CSS pixels to
* scroll in the x direction .
* @ property { number } [ deltaY = 0 ] - Floating - point value in CSS pixels to
* scroll in the y direction .
* @ property { number } [ deltaZ = 0 ] - Floating - point value in CSS pixels to
* scroll in the z direction .
* @ property { number } [ expectedOverflowDeltaX ] - Decimal value
* indicating horizontal scroll overflow . Only the sign is checked : ` 0 ` ,
* positive , or negative .
* @ property { number } [ expectedOverflowDeltaY ] - Decimal value
* indicating vertical scroll overflow . Only the sign is checked : ` 0 ` ,
* positive , or negative .
* @ property { boolean } [ isCustomizedByPrefs ] - If set to ` true ` the
* delta values are computed from preferences .
* @ property { boolean } [ isMomentum ] - If set to ` true ` the event will be
* caused by momentum .
* @ property { boolean } [ isNoLineOrPageDelta ] - If ` true ` , the creator
* does not set ` lineOrPageDeltaX / Y ` . When a widget wheel event is
* generated from this object , those fields will be automatically
* calculated during dispatch by the ` EventStateManager ` .
* @ property { number } [ lineOrPageDeltaX ] - If set to a non - zero value
* for a ` DOM_DELTA_PIXEL ` event , the EventStateManager will dispatch a
* ` NS_MOUSE_SCROLL ` event for a horizontal scroll .
* @ property { number } [ lineOrPageDeltaY ] - If set to a non - zero value
* for a ` DOM_DELTA_PIXEL ` event , the EventStateManager will dispatch a
* ` NS_MOUSE_SCROLL ` event for a vertical scroll .
* @ property { boolean } [ metaKey ] - If set to ` true ` , the Meta key will
* be considered pressed .
* @ property { boolean } [ shiftKey ] - If set to ` true ` , the Shift key will
* be considered pressed .
*/
/**
* Synthesize a wheel event in ` aWindow ` at a point , without flushing layout .
*
* ` nsIDOMWindowUtils . sendWheelEvent ` takes floats for the coordinates .
* Therefore , don ' t round or truncate the values .
*
* @ param { number } aLeft - Floating - point value for the X offset in CSS pixels .
* @ param { number } aTop - Floating - point value for the Y offset in CSS pixels .
* @ param { WheelEventData } aEvent - Details of the wheel event to dispatch .
* @ param { DOMWindow } [ aWindow = window ] - DOM window used to dispatch the event .
* @ param { Function } [ aCallback = null ] - A callback function that is invoked when
* the wheel event is dispatched .
*/
function synthesizeWheelAtPoint(
aLeft,
aTop,
aEvent,
aWindow = window,
aCallback = null
) {
var utils = _getDOMWindowUtils(aWindow);
if (!utils) {
return ;
}
var modifiers = _parseModifiers(aEvent, aWindow);
var options = 0 ;
if (aEvent.isNoLineOrPageDelta) {
options |= utils.WHEEL_EVENT_CAUSED_BY_NO_LINE_OR_PAGE_DELTA_DEVICE;
}
if (aEvent.isMomentum) {
options |= utils.WHEEL_EVENT_CAUSED_BY_MOMENTUM;
}
if (aEvent.isCustomizedByPrefs) {
options |= utils.WHEEL_EVENT_CUSTOMIZED_BY_USER_PREFS;
}
if (typeof aEvent.expectedOverflowDeltaX !== "undefined" ) {
if (aEvent.expectedOverflowDeltaX === 0 ) {
options |= utils.WHEEL_EVENT_EXPECTED_OVERFLOW_DELTA_X_ZERO;
} else if (aEvent.expectedOverflowDeltaX > 0 ) {
options |= utils.WHEEL_EVENT_EXPECTED_OVERFLOW_DELTA_X_POSITIVE;
} else {
options |= utils.WHEEL_EVENT_EXPECTED_OVERFLOW_DELTA_X_NEGATIVE;
}
}
if (typeof aEvent.expectedOverflowDeltaY !== "undefined" ) {
if (aEvent.expectedOverflowDeltaY === 0 ) {
options |= utils.WHEEL_EVENT_EXPECTED_OVERFLOW_DELTA_Y_ZERO;
} else if (aEvent.expectedOverflowDeltaY > 0 ) {
options |= utils.WHEEL_EVENT_EXPECTED_OVERFLOW_DELTA_Y_POSITIVE;
} else {
options |= utils.WHEEL_EVENT_EXPECTED_OVERFLOW_DELTA_Y_NEGATIVE;
}
}
if (aEvent.asyncEnabled) {
options |= utils.WHEEL_EVENT_ASYNC_ENABLED;
}
// Avoid the JS warnings "reference to undefined property"
if (!aEvent.deltaMode) {
aEvent.deltaMode = WheelEvent.DOM_DELTA_PIXEL;
}
if (!aEvent.deltaX) {
aEvent.deltaX = 0 ;
}
if (!aEvent.deltaY) {
aEvent.deltaY = 0 ;
}
if (!aEvent.deltaZ) {
aEvent.deltaZ = 0 ;
}
var lineOrPageDeltaX =
// eslint-disable-next-line no-nested-ternary
aEvent.lineOrPageDeltaX != null
? aEvent.lineOrPageDeltaX
: aEvent.deltaX > 0
? Math.floor(aEvent.deltaX)
: Math.ceil(aEvent.deltaX);
var lineOrPageDeltaY =
// eslint-disable-next-line no-nested-ternary
aEvent.lineOrPageDeltaY != null
? aEvent.lineOrPageDeltaY
: aEvent.deltaY > 0
? Math.floor(aEvent.deltaY)
: Math.ceil(aEvent.deltaY);
utils.sendWheelEvent(
aLeft,
aTop,
aEvent.deltaX,
aEvent.deltaY,
aEvent.deltaZ,
aEvent.deltaMode,
modifiers,
lineOrPageDeltaX,
lineOrPageDeltaY,
options,
aCallback
);
}
/**
* Synthesize a wheel event on a target .
*
* The actual client point is determined by taking the aTarget ' s client box
* and offsetting it by aOffsetX and aOffsetY .
*
* @ param { Element } aTarget - DOM element to dispatch the event on .
* @ param { number } aOffsetX - X offset in CSS pixels from the element ’ s left edge .
* @ param { number } aOffsetY - Y offset in CSS pixels from the element ’ s top edge .
* @ param { WheelEventData } aEvent - Details of the wheel event to dispatch .
* @ param { DOMWindow } [ aWindow = window ] - DOM window used to dispatch the event .
* @ param { Function } [ aCallback = null ] - A callback function that is invoked when
* the wheel event is dispatched .
* /
function synthesizeWheel (
aTarget ,
aOffsetX ,
aOffsetY ,
aEvent ,
aWindow = window ,
aCallback = null
) {
var rect = aTarget . getBoundingClientRect ( ) ;
synthesizeWheelAtPoint (
rect . left + aOffsetX ,
rect . top + aOffsetY ,
aEvent ,
aWindow ,
aCallback
) ;
}
const _ FlushModes = {
FLUSH : 0 ,
NOFLUSH : 1 ,
} ;
function _ sendWheelAndPaint (
aTarget ,
aOffsetX ,
aOffsetY ,
aEvent ,
aCallback ,
aFlushMode = _ FlushModes . FLUSH ,
aWindow = window
) {
var utils = _ getDOMWindowUtils ( aWindow ) ;
if ( ! utils ) {
return ;
}
if ( utils . isMozAfterPaintPending ) {
/ / If a paint is pending , then APZ may be waiting for a scroll acknowledgement
/ / from the content thread . If we send a wheel event now , it could be ignored
/ / by APZ ( or its scroll offset could be overridden ) . To avoid problems we
/ / just wait for the paint to complete .
aWindow . waitForAllPaintsFlushed ( function ( ) {
_ sendWheelAndPaint (
aTarget ,
aOffsetX ,
aOffsetY ,
aEvent ,
aCallback ,
aFlushMode ,
aWindow
) ;
} ) ;
return ;
}
var onwheel = function ( ) {
SpecialPowers . wrap ( window ) . removeEventListener ( " wheel " , onwheel , {
mozSystemGroup : true ,
} ) ;
/ / Wait one frame since the wheel event has not caused a refresh observer
/ / to be added yet .
setTimeout ( function ( ) {
utils . advanceTimeAndRefresh ( 1000 ) ;
if ( ! aCallback ) {
utils . advanceTimeAndRefresh ( 0 ) ;
return ;
}
var waitForPaints = function ( ) {
SpecialPowers . Services . obs . removeObserver (
waitForPaints ,
" apz - repaints - flushed "
) ;
aWindow . waitForAllPaintsFlushed ( function ( ) {
utils . restoreNormalRefresh ( ) ;
aCallback ( ) ;
} ) ;
} ;
SpecialPowers . Services . obs . addObserver (
waitForPaints ,
" apz - repaints - flushed "
) ;
if ( ! utils . flushApzRepaints ( ) ) {
waitForPaints ( ) ;
}
} , 0 ) ;
} ;
/ / Listen for the system wheel event , because it happens after all of
/ / the other wheel events , including legacy events .
SpecialPowers . wrap ( aWindow ) . addEventListener ( " wheel " , onwheel , {
mozSystemGroup : true ,
} ) ;
if ( aFlushMode = = = _ FlushModes . FLUSH ) {
synthesizeWheel ( aTarget , aOffsetX , aOffsetY , aEvent , aWindow ) ;
} else {
synthesizeWheelAtPoint ( aOffsetX , aOffsetY , aEvent , aWindow ) ;
}
}
/ * *
* Wrapper around synthesizeWheel that waits for the wheel event to be
* dispatched and for any resulting layout and paint operations to flush .
*
* Requires including ` paint_listener . js ` . Tests using this function must call
* ` DOMWindowUtils . restoreNormalRefresh ( ) ` before finishing .
*
* @ param { Element } aTarget - DOM element to dispatch the event on .
* @ param { number } aOffsetX - X offset in CSS pixels from the element ’ s left edge .
* @ param { number } aOffsetY - Y offset in CSS pixels from the element ’ s top edge .
* @ param { WheelEventData } aEvent - Details of the wheel event to dispatch .
* @ param { Function } [ aCallback ] - Called after paint flush , if provided . If not ,
* the caller is expected to handle scroll completion manually . In this case ,
* the refresh driver will not be restored automatically .
* @ param { DOMWindow } [ aWindow = window ] - DOM window used to dispatch the event .
* /
function sendWheelAndPaint (
aTarget ,
aOffsetX ,
aOffsetY ,
aEvent ,
aCallback ,
aWindow = window
) {
_ sendWheelAndPaint (
aTarget ,
aOffsetX ,
aOffsetY ,
aEvent ,
aCallback ,
_ FlushModes . FLUSH ,
aWindow
) ;
}
/ * *
* Similar to ` sendWheelAndPaint ( ) ` , but skips layout flush when resolving
* ` aTarget ` ' s position in ` aWindow ` before dispatching the wheel event .
*
* @ param { Element } aTarget - DOM element to dispatch the event on .
* @ param { number } aOffsetX - X offset in CSS pixels from the ` aWindow ` ’ s left edge .
* @ param { number } aOffsetY - Y offset in CSS pixels from the ` aWindow ` ’ s top edge .
* @ param { WheelEventData } aEvent - Details of the wheel event to dispatch .
* @ param { Function } [ aCallback ] - Called after paint , if provided . If not ,
* the caller is expected to handle scroll completion manually . In this case ,
* the refresh driver will not be restored automatically .
* @ param { DOMWindow } [ aWindow = window ] - DOM window used to dispatch the event .
* /
function sendWheelAndPaintNoFlush (
aTarget ,
aOffsetX ,
aOffsetY ,
aEvent ,
aCallback ,
aWindow = window
) {
_ sendWheelAndPaint (
aTarget ,
aOffsetX ,
aOffsetY ,
aEvent ,
aCallback ,
_ FlushModes . NOFLUSH ,
aWindow
) ;
}
function synthesizeNativeTapAtCenter (
aTarget ,
aLongTap = false ,
aCallback = null ,
aWindow = window
) {
let rect = aTarget . getBoundingClientRect ( ) ;
return synthesizeNativeTap (
aTarget ,
rect . width / 2 ,
rect . height / 2 ,
aLongTap ,
aCallback ,
aWindow
) ;
}
function synthesizeNativeTap (
aTarget ,
aOffsetX ,
aOffsetY ,
aLongTap = false ,
aCallback = null ,
aWindow = window
) {
let utils = _ getDOMWindowUtils ( aWindow ) ;
if ( ! utils ) {
return ;
}
let scale = aWindow . devicePixelRatio ;
let rect = aTarget . getBoundingClientRect ( ) ;
let x = _ EU_roundDevicePixels (
( aWindow . mozInnerScreenX + rect . left + aOffsetX ) * scale
) ;
let y = _ EU_roundDevicePixels (
( aWindow . mozInnerScreenY + rect . top + aOffsetY ) * scale
) ;
utils . sendNativeTouchTap ( x , y , aLongTap , aCallback ) ;
}
/ * *
* Similar to synthesizeMouse but generates a native widget level event
* ( so will actually move the " real " mouse cursor etc . Be careful because
* this can impact later code as well ! ( e . g . with hover states etc . )
*
* @ description There are 3 mutually exclusive ways of indicating the location of the
* mouse event : set ` ` atCenter ` ` , or pass ` ` offsetX ` ` and ` ` offsetY ` ` ,
* or pass ` ` screenX ` ` and ` ` screenY ` ` . Do not attempt to mix these .
*
* @ param { object } aParams
* @ param { string } aParams . type " click " , " mousedown " , " mouseup " or " mousemove "
* @ param { Element } aParams . target Origin of offsetX and offsetY , must be an element
* @ param { boolean } [ aParams . atCenter ]
* Instead of offsetX / Y , synthesize the event at center of ` target ` .
* @ param { number } [ aParams . offsetX ]
* X offset in ` target ` ( in CSS pixels if ` scale ` is " screenPixelsPerCSSPixel " )
* @ param { number } [ aParams . offsetY ]
* Y offset in ` target ` ( in CSS pixels if ` scale ` is " screenPixelsPerCSSPixel " )
* @ param { number } [ aParams . screenX ]
* X offset in screen ( in CSS pixels if ` scale ` is " screenPixelsPerCSSPixel " ) ,
* Neither offsetX / Y nor atCenter must be set if this is set .
* @ param { number } [ aParams . screenY ]
* Y offset in screen ( in CSS pixels if ` scale ` is " screenPixelsPerCSSPixel " ) ,
* Neither offsetX / Y nor atCenter must be set if this is set .
* @ param { string } [ aParams . scale = " screenPixelsPerCSSPixel " ]
* If scale is " screenPixelsPerCSSPixel " , devicePixelRatio will be used .
* If scale is " inScreenPixels " , clientX / Y nor scaleX / Y are not adjusted with screenPixelsPerCSSPixel .
* @ param { number } [ aParams . button = 0 ]
* Defaults to 0 , if " click " , " mousedown " , " mouseup " , set same value as DOM MouseEvent . button
* @ param { object } [ aParams . modifiers = { } ]
* Active modifiers , see ` _ parseNativeModifiers `
* @ param { DOMWindow } [ aParams . win = window ]
* The window to use its utils . Defaults to the window in which EventUtils . js is running .
* @ param { Element } [ aParams . elementOnWidget = target ]
* Defaults to target . If element under the point is in another widget from target ' s widget ,
* e . g . , when it ' s in a XUL < panel > , specify this .
* /
function synthesizeNativeMouseEvent ( aParams , aCallback = null ) {
const {
type ,
target ,
offsetX ,
offsetY ,
atCenter ,
screenX ,
screenY ,
scale = " screenPixelsPerCSSPixel " ,
button = 0 ,
modifiers = { } ,
win = window ,
elementOnWidget = target ,
} = aParams ;
if ( atCenter ) {
if ( offsetX ! = undefined | | offsetY ! = undefined ) {
throw Error (
` atCenter is specified , but offsetX ( $ { offsetX } ) and / or offsetY ( $ { offsetY } ) are also specified `
) ;
}
if ( screenX ! = undefined | | screenY ! = undefined ) {
throw Error (
` atCenter is specified , but screenX ( $ { screenX } ) and / or screenY ( $ { screenY } ) are also specified `
) ;
}
if ( ! target ) {
throw Error ( " atCenter is specified , but target is not specified " ) ;
}
} else if ( offsetX ! = undefined & & offsetY ! = undefined ) {
if ( screenX ! = undefined | | screenY ! = undefined ) {
throw Error (
` offsetX / Y are specified , but screenX ( $ { screenX } ) and / or screenY ( $ { screenY } ) are also specified `
) ;
}
if ( ! target ) {
throw Error (
" offsetX and offsetY are specified , but target is not specified "
) ;
}
} else if ( screenX ! = undefined & & screenY ! = undefined ) {
if ( offsetX ! = undefined | | offsetY ! = undefined ) {
throw Error (
` screenX / Y are specified , but offsetX ( $ { offsetX } ) and / or offsetY ( $ { offsetY } ) are also specified `
) ;
}
}
const utils = _ getDOMWindowUtils ( win ) ;
if ( ! utils ) {
return ;
}
const rect = target ? . getBoundingClientRect ( ) ;
const resolution = _ getTopWindowResolution ( win ) ;
const scaleValue = ( ( ) = > {
if ( scale = = = " inScreenPixels " ) {
return 1 . 0 ;
}
if ( scale = = = " screenPixelsPerCSSPixel " ) {
return win . devicePixelRatio ;
}
throw Error ( ` invalid scale value ( $ { scale } ) is specified ` ) ;
} ) ( ) ;
/ / XXX mozInnerScreen might be invalid value on mobile viewport ( Bug 1701546 ) ,
/ / so use window . top ' s mozInnerScreen . But this won ' t work fission + xorigin
/ / with mobile viewport until mozInnerScreen returns valid value with
/ / scale .
const x = _ EU_roundDevicePixels (
( ( ) = > {
if ( screenX ! = undefined ) {
return screenX * scaleValue ;
}
const winInnerOffsetX = _ getScreenXInUnscaledCSSPixels ( win ) ;
return (
( ( ( atCenter ? rect . width / 2 : offsetX ) + rect . left ) * resolution +
winInnerOffsetX ) *
scaleValue
) ;
} ) ( )
) ;
const y = _ EU_roundDevicePixels (
( ( ) = > {
if ( screenY ! = undefined ) {
return screenY * scaleValue ;
}
const winInnerOffsetY = _ getScreenYInUnscaledCSSPixels ( win ) ;
return (
( ( ( atCenter ? rect . height / 2 : offsetY ) + rect . top ) * resolution +
winInnerOffsetY ) *
scaleValue
) ;
} ) ( )
) ;
const modifierFlags = _ parseNativeModifiers ( modifiers ) ;
if ( type = = = " click " ) {
utils . sendNativeMouseEvent (
x ,
y ,
utils . NATIVE_MOUSE_MESSAGE_BUTTON_DOWN ,
button ,
modifierFlags ,
elementOnWidget ,
function ( ) {
utils . sendNativeMouseEvent (
x ,
y ,
utils . NATIVE_MOUSE_MESSAGE_BUTTON_UP ,
button ,
modifierFlags ,
elementOnWidget ,
aCallback
) ;
}
) ;
return ;
}
utils . sendNativeMouseEvent (
x ,
y ,
( ( ) = > {
switch ( type ) {
case " mousedown " :
return utils . NATIVE_MOUSE_MESSAGE_BUTTON_DOWN ;
case " mouseup " :
return utils . NATIVE_MOUSE_MESSAGE_BUTTON_UP ;
case " mousemove " :
return utils . NATIVE_MOUSE_MESSAGE_MOVE ;
default :
throw Error ( ` Invalid type is specified : $ { type } ` ) ;
}
} ) ( ) ,
button ,
modifierFlags ,
elementOnWidget ,
aCallback
) ;
}
function promiseNativeMouseEvent ( aParams ) {
return new Promise ( resolve = > synthesizeNativeMouseEvent ( aParams , resolve ) ) ;
}
function synthesizeNativeMouseEventAndWaitForEvent ( aParams , aCallback ) {
const listener = aParams . eventTargetToListen | | aParams . target ;
const eventType = aParams . eventTypeToWait | | aParams . type ;
listener . addEventListener ( eventType , aCallback , {
capture : true ,
once : true ,
} ) ;
synthesizeNativeMouseEvent ( aParams ) ;
}
function promiseNativeMouseEventAndWaitForEvent ( aParams ) {
return new Promise ( resolve = >
synthesizeNativeMouseEventAndWaitForEvent ( aParams , resolve )
) ;
}
/ * *
* This is a wrapper around synthesizeNativeMouseEvent that waits for the mouse
* event to be dispatched to the target content .
*
* This API is supposed to be used in those test cases that synthesize some
* input events to chrome process and have some checks in content .
* /
function synthesizeAndWaitNativeMouseMove (
aTarget ,
aOffsetX ,
aOffsetY ,
aCallback ,
aWindow = window
) {
let browser = gBrowser . selectedTab . linkedBrowser ;
let mm = browser . messageManager ;
let { ContentTask } = _ EU_ChromeUtils . importESModule (
" resource : / / testing - common / ContentTask . sys . mjs "
) ;
let eventRegisteredPromise = new Promise ( resolve = > {
mm . addMessageListener ( " Test : MouseMoveRegistered " , function processed ( ) {
mm . removeMessageListener ( " Test : MouseMoveRegistered " , processed ) ;
resolve ( ) ;
} ) ;
} ) ;
/ / TODO : Switch to SpecialPowers . spawn
/ / eslint - disable - next - line mozilla / reject - contenttask - spawn
let eventReceivedPromise = ContentTask . spawn (
browser ,
[ aOffsetX , aOffsetY ] ,
( [ clientX , clientY ] ) = > {
return new Promise ( resolve = > {
addEventListener ( " mousemove " , function onMouseMoveEvent ( e ) {
if ( e . clientX = = clientX & & e . clientY = = clientY ) {
removeEventListener ( " mousemove " , onMouseMoveEvent ) ;
resolve ( ) ;
}
} ) ;
sendAsyncMessage ( " Test : MouseMoveRegistered " ) ;
} ) ;
}
) ;
eventRegisteredPromise . then ( ( ) = > {
synthesizeNativeMouseEvent ( {
type : " mousemove " ,
target : aTarget ,
offsetX : aOffsetX ,
offsetY : aOffsetY ,
win : aWindow ,
} ) ;
} ) ;
return eventReceivedPromise ;
}
/ * *
* Synthesize a key event . It is targeted at whatever would be targeted by an
* actual keypress by the user , typically the focused element .
*
* @ param { string } aKey
* Should be either :
*
* - key value ( recommended ) . If you specify a non - printable key name ,
* prepend the ` ` KEY_ ` ` prefix . Otherwise , specifying a printable key , the
* key value should be specified .
*
* - keyCode name starting with ` ` VK_ ` ` ( e . g . , ` ` VK_RETURN ` ` ) . This is available
* only for compatibility with legacy API . Don ' t use this with new tests .
*
* @ param { object } [ aEvent ]
* Optional event object with more specifics about the key event to
* synthesize .
* @ param { string } [ aEvent . code ]
* If you don ' t specify this explicitly , it ' ll be guessed from aKey
* of US keyboard layout . Note that this value may be different
* between browsers . For example , " Insert " is never set only on
* macOS since actual key operation won ' t cause this code value .
* In such case , the value becomes empty string .
* If you need to emulate non - US keyboard layout or virtual keyboard
* which doesn ' t emulate hardware key input , you should set this value
* to empty string explicitly .
* @ param { number } [ aEvent . repeat ]
* If you emulate auto - repeat , you should set the count of repeat .
* This method will automatically synthesize keydown ( and keypress ) .
* @ param { * } aEvent . location
* If you want to specify this , you can specify this explicitly .
* However , if you don ' t specify this value , it will be computed
* from code value .
* @ param { string } aEvent . type
* Basically , you shouldn ' t specify this . Then , this function will
* synthesize keydown ( , keypress ) and keyup .
* If keydown is specified , this only fires keydown ( and keypress if
* it should be fired ) .
* If keyup is specified , this only fires keyup .
* @ param { number } aEvent . keyCode
* Must be 0 - 255 ( 0 xFF ) . If this is specified explicitly ,
* . keyCode value is initialized with this value .
* @ param { DOMWindow } [ aWindow = window ]
* DOM window used to dispatch the event .
* @ param { Function } aCallback
* Is optional and can be used to receive notifications from TIP .
*
* @ description
* ` ` accelKey ` ` , ` ` altKey ` ` , ` ` altGraphKey ` ` , ` ` ctrlKey ` ` , ` ` capsLockKey ` ` ,
* ` ` fnKey ` ` , ` ` fnLockKey ` ` , ` ` numLockKey ` ` , ` ` metaKey ` ` , ` ` scrollLockKey ` ` ,
* ` ` shiftKey ` ` , ` ` symbolKey ` ` , ` ` symbolLockKey ` `
* Basically , you shouldn ' t use these attributes . nsITextInputProcessor
* manages modifier key state when you synthesize modifier key events .
* However , if some of these attributes are true , this function activates
* the modifiers only during dispatching the key events .
* Note that if some of these values are false , they are ignored ( i . e . ,
* not inactivated with this function ) .
* /
function synthesizeKey ( aKey , aEvent = undefined , aWindow = window , aCallback ) {
const event = aEvent = = = undefined | | aEvent = = = null ? { } : aEvent ;
let dispatchKeydown =
! ( " type " in event ) | | event . type = = = " keydown " | | ! event . type ;
const dispatchKeyup =
! ( " type " in event ) | | event . type = = = " keyup " | | ! event . type ;
if ( dispatchKeydown & & aKey = = " KEY_Escape " ) {
let eventForKeydown = Object . assign ( { } , JSON . parse ( JSON . stringify ( event ) ) ) ;
eventForKeydown . type = " keydown " ;
if (
_ maybeEndDragSession (
/ / TODO : We should set the last dragover point instead
0 ,
0 ,
eventForKeydown ,
aWindow
)
) {
if ( ! dispatchKeyup ) {
return ;
}
/ / We don ' t need to dispatch only keydown event because it ' s consumed by
/ / the drag session .
dispatchKeydown = false ;
}
}
var TIP = _ getTIP ( aWindow , aCallback ) ;
if ( ! TIP ) {
return ;
}
var KeyboardEvent = _ getKeyboardEvent ( aWindow ) ;
var modifiers = _ emulateToActivateModifiers ( TIP , event , aWindow ) ;
var keyEventDict = _ createKeyboardEventDictionary ( aKey , event , TIP , aWindow ) ;
var keyEvent = new KeyboardEvent ( " " , keyEventDict . dictionary ) ;
try {
if ( dispatchKeydown ) {
TIP . keydown ( keyEvent , keyEventDict . flags ) ;
if ( " repeat " in event & & event . repeat > 1 ) {
keyEventDict . dictionary . repeat = true ;
var repeatedKeyEvent = new KeyboardEvent ( " " , keyEventDict . dictionary ) ;
for ( var i = 1 ; i < event . repeat ; i + + ) {
TIP . keydown ( repeatedKeyEvent , keyEventDict . flags ) ;
}
}
}
if ( dispatchKeyup ) {
TIP . keyup ( keyEvent , keyEventDict . flags ) ;
}
} finally {
_ emulateToInactivateModifiers ( TIP , modifiers , aWindow ) ;
}
}
/ * *
* This is a wrapper around synthesizeKey that waits for the key event to be
* dispatched to the target content . It returns a promise which is resolved
* when the content receives the key event .
*
* This API is supposed to be used in those test cases that synthesize some
* input events to chrome process and have some checks in content .
* /
function synthesizeAndWaitKey (
aKey ,
aEvent ,
aWindow = window ,
checkBeforeSynthesize ,
checkAfterSynthesize
) {
let browser = gBrowser . selectedTab . linkedBrowser ;
let mm = browser . messageManager ;
let keyCode = _ createKeyboardEventDictionary ( aKey , aEvent , null , aWindow )
. dictionary . keyCode ;
let { ContentTask } = _ EU_ChromeUtils . importESModule (
" resource : / / testing - common / ContentTask . sys . mjs "
) ;
let keyRegisteredPromise = new Promise ( resolve = > {
mm . addMessageListener ( " Test : KeyRegistered " , function processed ( ) {
mm . removeMessageListener ( " Test : KeyRegistered " , processed ) ;
resolve ( ) ;
} ) ;
} ) ;
/ / eslint - disable - next - line no - shadow
/ / TODO : Switch to SpecialPowers . spawn
/ / eslint - disable - next - line mozilla / reject - contenttask - spawn
let keyReceivedPromise = ContentTask . spawn ( browser , keyCode , keyCode = > {
return new Promise ( resolve = > {
addEventListener ( " keyup " , function onKeyEvent ( e ) {
if ( e . keyCode = = keyCode ) {
removeEventListener ( " keyup " , onKeyEvent ) ;
resolve ( ) ;
}
} ) ;
sendAsyncMessage ( " Test : KeyRegistered " ) ;
} ) ;
} ) ;
keyRegisteredPromise . then ( ( ) = > {
if ( checkBeforeSynthesize ) {
checkBeforeSynthesize ( ) ;
}
synthesizeKey ( aKey , aEvent , aWindow ) ;
if ( checkAfterSynthesize ) {
checkAfterSynthesize ( ) ;
}
} ) ;
return keyReceivedPromise ;
}
function _ parseNativeModifiers ( aModifiers , aWindow = window ) {
let modifiers = 0 ;
if ( aModifiers . capsLockKey ) {
modifiers | = SpecialPowers . Ci . nsIDOMWindowUtils . NATIVE_MODIFIER_CAPS_LOCK ;
}
if ( aModifiers . numLockKey ) {
modifiers | = SpecialPowers . Ci . nsIDOMWindowUtils . NATIVE_MODIFIER_NUM_LOCK ;
}
if ( aModifiers . shiftKey ) {
modifiers | = SpecialPowers . Ci . nsIDOMWindowUtils . NATIVE_MODIFIER_SHIFT_LEFT ;
}
if ( aModifiers . shiftRightKey ) {
modifiers | = SpecialPowers . Ci . nsIDOMWindowUtils . NATIVE_MODIFIER_SHIFT_RIGHT ;
}
if ( aModifiers . ctrlKey ) {
modifiers | =
SpecialPowers . Ci . nsIDOMWindowUtils . NATIVE_MODIFIER_CONTROL_LEFT ;
}
if ( aModifiers . ctrlRightKey ) {
modifiers | =
SpecialPowers . Ci . nsIDOMWindowUtils . NATIVE_MODIFIER_CONTROL_RIGHT ;
}
if ( aModifiers . altKey ) {
modifiers | = SpecialPowers . Ci . nsIDOMWindowUtils . NATIVE_MODIFIER_ALT_LEFT ;
}
if ( aModifiers . altRightKey ) {
modifiers | = SpecialPowers . Ci . nsIDOMWindowUtils . NATIVE_MODIFIER_ALT_RIGHT ;
}
if ( aModifiers . metaKey ) {
modifiers | =
SpecialPowers . Ci . nsIDOMWindowUtils . NATIVE_MODIFIER_COMMAND_LEFT ;
}
if ( aModifiers . metaRightKey ) {
modifiers | =
SpecialPowers . Ci . nsIDOMWindowUtils . NATIVE_MODIFIER_COMMAND_RIGHT ;
}
if ( aModifiers . helpKey ) {
modifiers | = SpecialPowers . Ci . nsIDOMWindowUtils . NATIVE_MODIFIER_HELP ;
}
if ( aModifiers . fnKey ) {
modifiers | = SpecialPowers . Ci . nsIDOMWindowUtils . NATIVE_MODIFIER_FUNCTION ;
}
if ( aModifiers . numericKeyPadKey ) {
modifiers | =
SpecialPowers . Ci . nsIDOMWindowUtils . NATIVE_MODIFIER_NUMERIC_KEY_PAD ;
}
if ( aModifiers . accelKey ) {
modifiers | = _ EU_isMac ( aWindow )
? SpecialPowers . Ci . nsIDOMWindowUtils . NATIVE_MODIFIER_COMMAND_LEFT
: SpecialPowers . Ci . nsIDOMWindowUtils . NATIVE_MODIFIER_CONTROL_LEFT ;
}
if ( aModifiers . accelRightKey ) {
modifiers | = _ EU_isMac ( aWindow )
? SpecialPowers . Ci . nsIDOMWindowUtils . NATIVE_MODIFIER_COMMAND_RIGHT
: SpecialPowers . Ci . nsIDOMWindowUtils . NATIVE_MODIFIER_CONTROL_RIGHT ;
}
if ( aModifiers . altGrKey ) {
modifiers | = _ EU_isMac ( aWindow )
? SpecialPowers . Ci . nsIDOMWindowUtils . NATIVE_MODIFIER_ALT_LEFT
: SpecialPowers . Ci . nsIDOMWindowUtils . NATIVE_MODIFIER_ALT_GRAPH ;
}
return modifiers ;
}
/ / Mac : Any unused number is okay for adding new keyboard layout .
/ / When you add new keyboard layout here , you need to modify
/ / TISInputSourceWrapper : : InitByLayoutID ( ) .
/ / Win : These constants can be found by inspecting registry keys under
/ / HKEY_LOCAL_MACHINE \ System \ CurrentControlSet \ Control \ Keyboard Layouts
const KEYBOARD_LAYOUT_ARABIC = {
name : " Arabic " ,
Mac : 6 ,
Win : 0 x00000401 ,
get hasAltGrOnWin ( ) {
/ / KB5070311 added AltGr to Arabic keyboard layouts on
/ / Windows 11 24 H2 and 25 H2 ( build 26100 and build 26200 ) 7309 .
return (
parseInt ( Services . sysinfo . getProperty ( " build " ) , 10 ) > = 26100 & &
_ EU_getWindowsUBR ( ) > = 7309
) ;
} ,
} ;
_ defineConstant ( " KEYBOARD_LAYOUT_ARABIC " , KEYBOARD_LAYOUT_ARABIC ) ;
const KEYBOARD_LAYOUT_ARABIC_PC = {
name : " Arabic - PC " ,
Mac : 7 ,
Win : null ,
hasAltGrOnWin : false ,
} ;
_ defineConstant ( " KEYBOARD_LAYOUT_ARABIC_PC " , KEYBOARD_LAYOUT_ARABIC_PC ) ;
const KEYBOARD_LAYOUT_BRAZILIAN_ABNT = {
name : " Brazilian ABNT " ,
Mac : null ,
Win : 0 x00000416 ,
hasAltGrOnWin : true ,
} ;
_ defineConstant (
" KEYBOARD_LAYOUT_BRAZILIAN_ABNT " ,
KEYBOARD_LAYOUT_BRAZILIAN_ABNT
) ;
const KEYBOARD_LAYOUT_DVORAK_QWERTY = {
name : " Dvorak - QWERTY " ,
Mac : 4 ,
Win : null ,
hasAltGrOnWin : false ,
} ;
_ defineConstant ( " KEYBOARD_LAYOUT_DVORAK_QWERTY " , KEYBOARD_LAYOUT_DVORAK_QWERTY ) ;
const KEYBOARD_LAYOUT_EN_US = {
name : " US " ,
Mac : 0 ,
Win : 0 x00000409 ,
hasAltGrOnWin : false ,
} ;
_ defineConstant ( " KEYBOARD_LAYOUT_EN_US " , KEYBOARD_LAYOUT_EN_US ) ;
const KEYBOARD_LAYOUT_FRENCH = {
name : " French " ,
Mac : 8 , / / Some keys mapped different from PC , e . g . , Digit6 , Digit8 , Equal , Slash and Backslash
Win : 0 x0000040c ,
hasAltGrOnWin : true ,
} ;
_ defineConstant ( " KEYBOARD_LAYOUT_FRENCH " , KEYBOARD_LAYOUT_FRENCH ) ;
const KEYBOARD_LAYOUT_FRENCH_PC = {
name : " French - PC " ,
Mac : 13 , / / Compatible with Windows
Win : 0 x0000040c ,
hasAltGrOnWin : true ,
} ;
_ defineConstant ( " KEYBOARD_LAYOUT_FRENCH_PC " , KEYBOARD_LAYOUT_FRENCH_PC ) ;
const KEYBOARD_LAYOUT_GREEK = {
name : " Greek " ,
Mac : 1 ,
Win : 0 x00000408 ,
hasAltGrOnWin : true ,
} ;
_ defineConstant ( " KEYBOARD_LAYOUT_GREEK " , KEYBOARD_LAYOUT_GREEK ) ;
const KEYBOARD_LAYOUT_GERMAN = {
name : " German " ,
Mac : 2 ,
Win : 0 x00000407 ,
hasAltGrOnWin : true ,
} ;
_ defineConstant ( " KEYBOARD_LAYOUT_GERMAN " , KEYBOARD_LAYOUT_GERMAN ) ;
const KEYBOARD_LAYOUT_HEBREW = {
name : " Hebrew " ,
Mac : 9 ,
Win : 0 x0000040d ,
hasAltGrOnWin : true ,
} ;
_ defineConstant ( " KEYBOARD_LAYOUT_HEBREW " , KEYBOARD_LAYOUT_HEBREW ) ;
const KEYBOARD_LAYOUT_JAPANESE = {
name : " Japanese " ,
Mac : null ,
Win : 0 x00000411 ,
hasAltGrOnWin : false ,
} ;
_ defineConstant ( " KEYBOARD_LAYOUT_JAPANESE " , KEYBOARD_LAYOUT_JAPANESE ) ;
const KEYBOARD_LAYOUT_KHMER = {
name : " Khmer " ,
Mac : null ,
Win : 0 x00000453 ,
hasAltGrOnWin : true ,
} ; / / available on Win7 or later .
_ defineConstant ( " KEYBOARD_LAYOUT_KHMER " , KEYBOARD_LAYOUT_KHMER ) ;
const KEYBOARD_LAYOUT_LITHUANIAN = {
name : " Lithuanian " ,
Mac : 10 ,
Win : 0 x00010427 ,
hasAltGrOnWin : true ,
} ;
_ defineConstant ( " KEYBOARD_LAYOUT_LITHUANIAN " , KEYBOARD_LAYOUT_LITHUANIAN ) ;
const KEYBOARD_LAYOUT_NORWEGIAN = {
name : " Norwegian " ,
Mac : 11 ,
Win : 0 x00000414 ,
hasAltGrOnWin : true ,
} ;
_ defineConstant ( " KEYBOARD_LAYOUT_NORWEGIAN " , KEYBOARD_LAYOUT_NORWEGIAN ) ;
const KEYBOARD_LAYOUT_RUSSIAN = {
name : " Russian " ,
Mac : null ,
Win : 0 x00000419 ,
hasAltGrOnWin : true , / / No AltGr , but Ctrl + Alt + Digit8 introduces a char
} ;
_ defineConstant ( " KEYBOARD_LAYOUT_RUSSIAN " , KEYBOARD_LAYOUT_RUSSIAN ) ;
const KEYBOARD_LAYOUT_RUSSIAN_MNEMONIC = {
name : " Russian - Mnemonic " ,
Mac : null ,
Win : 0 x00020419 ,
hasAltGrOnWin : true ,
} ; / / available on Win8 or later .
_ defineConstant (
" KEYBOARD_LAYOUT_RUSSIAN_MNEMONIC " ,
KEYBOARD_LAYOUT_RUSSIAN_MNEMONIC
) ;
const KEYBOARD_LAYOUT_SPANISH = {
name : " Spanish " ,
Mac : 12 ,
Win : 0 x0000040a ,
hasAltGrOnWin : true ,
} ;
_ defineConstant ( " KEYBOARD_LAYOUT_SPANISH " , KEYBOARD_LAYOUT_SPANISH ) ;
const KEYBOARD_LAYOUT_SWEDISH = {
name : " Swedish " ,
Mac : 3 ,
Win : 0 x0000041d ,
hasAltGrOnWin : true ,
} ;
_ defineConstant ( " KEYBOARD_LAYOUT_SWEDISH " , KEYBOARD_LAYOUT_SWEDISH ) ;
const KEYBOARD_LAYOUT_THAI = {
name : " Thai " ,
Mac : 5 ,
Win : 0 x0002041e ,
hasAltGrOnWin : false ,
} ;
_ defineConstant ( " KEYBOARD_LAYOUT_THAI " , KEYBOARD_LAYOUT_THAI ) ;
/ * *
* synthesizeNativeKey ( ) dispatches native key event on active window .
* This is implemented only on Windows and Mac . Note that this function
* dispatches the key event asynchronously and returns immediately . If a
* callback function is provided , the callback will be called upon
* completion of the key dispatch .
*
* @ param aKeyboardLayout One of KEYBOARD_LAYOUT_ * defined above .
* @ param aNativeKeyCode A native keycode value defined in
* NativeKeyCodes . js .
* @ param aModifiers Modifier keys . If no modifire key is pressed ,
* this must be { } . Otherwise , one or more items
* referred in _ parseNativeModifiers ( ) must be
* true .
* @ param aChars Specify characters which should be generated
* by the key event .
* @ param aUnmodifiedChars Specify characters of unmodified ( except Shift )
* aChar value .
* @ param aCallback If provided , this callback will be invoked
* once the native keys have been processed
* by Gecko . Will never be called if this
* function returns false .
* @ return True if this function succeed dispatching
* native key event . Otherwise , false .
* /
function synthesizeNativeKey (
aKeyboardLayout ,
aNativeKeyCode ,
aModifiers ,
aChars ,
aUnmodifiedChars ,
aCallback ,
aWindow = window
) {
var utils = _ getDOMWindowUtils ( aWindow ) ;
if ( ! utils ) {
return false ;
}
var nativeKeyboardLayout = null ;
if ( _ EU_isMac ( aWindow ) ) {
nativeKeyboardLayout = aKeyboardLayout . Mac ;
} else if ( _ EU_isWin ( aWindow ) ) {
nativeKeyboardLayout = aKeyboardLayout . Win ;
}
if ( nativeKeyboardLayout = = = null ) {
return false ;
}
utils . sendNativeKeyEvent (
nativeKeyboardLayout ,
aNativeKeyCode ,
_ parseNativeModifiers ( aModifiers , aWindow ) ,
aChars ,
aUnmodifiedChars ,
aCallback
) ;
return true ;
}
var _ gSeenEvent = false ;
/ * *
* Indicate that an event with an original target of aExpectedTarget and
* a type of aExpectedEvent is expected to be fired , or not expected to
* be fired .
* /
function _ expectEvent ( aExpectedTarget , aExpectedEvent , aTestName ) {
if ( ! aExpectedTarget | | ! aExpectedEvent ) {
return null ;
}
_ gSeenEvent = false ;
var type =
aExpectedEvent . charAt ( 0 ) = = " ! "
? aExpectedEvent . substring ( 1 )
: aExpectedEvent ;
var eventHandler = function ( event ) {
var epassed =
! _ gSeenEvent & &
event . originalTarget = = aExpectedTarget & &
event . type = = type ;
is (
epassed ,
true ,
aTestName + " " + type + " event target " + ( _ gSeenEvent ? " twice " : " " )
) ;
_ gSeenEvent = true ;
} ;
aExpectedTarget . addEventListener ( type , eventHandler ) ;
return eventHandler ;
}
/ * *
* Check if the event was fired or not . The event handler aEventHandler
* will be removed .
* /
function _ checkExpectedEvent (
aExpectedTarget ,
aExpectedEvent ,
aEventHandler ,
aTestName
) {
if ( aEventHandler ) {
var expectEvent = aExpectedEvent . charAt ( 0 ) ! = " ! " ;
var type = expectEvent ? aExpectedEvent : aExpectedEvent . substring ( 1 ) ;
aExpectedTarget . removeEventListener ( type , aEventHandler ) ;
var desc = type + " event " ;
if ( ! expectEvent ) {
desc + = " not " ;
}
is ( _ gSeenEvent , expectEvent , aTestName + " " + desc + " fired " ) ;
}
_ gSeenEvent = false ;
}
/ * *
* Similar to synthesizeMouse except that a test is performed to see if an
* event is fired at the right target as a result .
*
* aExpectedTarget - the expected originalTarget of the event .
* aExpectedEvent - the expected type of the event , such as ' select ' .
* aTestName - the test name when outputing results
*
* To test that an event is not fired , use an expected type preceded by an
* exclamation mark , such as ' ! select ' . This might be used to test that a
* click on a disabled element doesn ' t fire certain events for instance .
*
* aWindow is optional , and defaults to the current window object .
* /
function synthesizeMouseExpectEvent (
aTarget ,
aOffsetX ,
aOffsetY ,
aEvent ,
aExpectedTarget ,
aExpectedEvent ,
aTestName ,
aWindow
) {
var eventHandler = _ expectEvent ( aExpectedTarget , aExpectedEvent , aTestName ) ;
synthesizeMouse ( aTarget , aOffsetX , aOffsetY , aEvent , aWindow ) ;
_ checkExpectedEvent ( aExpectedTarget , aExpectedEvent , eventHandler , aTestName ) ;
}
/ * *
* Similar to synthesizeKey except that a test is performed to see if an
* event is fired at the right target as a result .
*
* aExpectedTarget - the expected originalTarget of the event .
* aExpectedEvent - the expected type of the event , such as ' select ' .
* aTestName - the test name when outputing results
*
* To test that an event is not fired , use an expected type preceded by an
* exclamation mark , such as ' ! select ' .
*
* aWindow is optional , and defaults to the current window object .
* /
function synthesizeKeyExpectEvent (
key ,
aEvent ,
aExpectedTarget ,
aExpectedEvent ,
aTestName ,
aWindow
) {
var eventHandler = _ expectEvent ( aExpectedTarget , aExpectedEvent , aTestName ) ;
synthesizeKey ( key , aEvent , aWindow ) ;
_ checkExpectedEvent ( aExpectedTarget , aExpectedEvent , eventHandler , aTestName ) ;
}
function disableNonTestMouseEvents ( aDisable ) {
var domutils = _ getDOMWindowUtils ( ) ;
domutils . disableNonTestMouseEvents ( aDisable ) ;
}
function _ getDOMWindowUtils ( aWindow = window ) {
/ / Leave this here as something , somewhere , passes a falsy argument
/ / to this , causing the | window | default argument not to get picked up .
if ( ! aWindow ) {
aWindow = window ;
}
/ / If documentURIObject exists or ` window ` is a stub object , we ' re in
/ / a chrome scope , so don ' t bother trying to go through SpecialPowers .
if ( ! aWindow . document | | aWindow . document . documentURIObject ) {
return aWindow . windowUtils ;
}
/ / we need parent . SpecialPowers for :
/ / layout / base / tests / test_reftests_with_caret . html
/ / chrome : toolkit / content / tests / chrome / test_findbar . xul
/ / chrome : toolkit / content / tests / chrome / test_popup_anchor . xul
if ( " SpecialPowers " in aWindow & & aWindow . SpecialPowers ! = undefined ) {
return aWindow . SpecialPowers . getDOMWindowUtils ( aWindow ) ;
}
if (
" SpecialPowers " in aWindow . parent & &
aWindow . parent . SpecialPowers ! = undefined
) {
return aWindow . parent . SpecialPowers . getDOMWindowUtils ( aWindow ) ;
}
/ / TODO : this is assuming we are in chrome space
return aWindow . windowUtils ;
}
/ * *
* @ param { DOMWindow } [ aWindow ] - DOM window
* @ returns The scaling value applied to the top window .
* /
function _ getTopWindowResolution ( aWindow ) {
let resolution = 1 . 0 ;
try {
resolution = _ getDOMWindowUtils ( aWindow . top ) . getResolution ( ) ;
} catch ( e ) {
/ / XXX How to get mobile viewport scale on Fission + xorigin since
/ / window . top access isn ' t allowed due to cross - origin ?
}
return resolution ;
}
/ * *
* @ param { DOMWindow } [ aWindow ] - The DOM window which you want
* to get its x - offset in the screen .
* @ returns The screenX of aWindow in the unscaled CSS pixels .
* /
function _ getScreenXInUnscaledCSSPixels ( aWindow ) {
/ / XXX mozInnerScreen might be invalid value on mobile viewport ( Bug 1701546 ) ,
/ / so use window . top ' s mozInnerScreen . But this won ' t work fission + xorigin
/ / with mobile viewport until mozInnerScreen returns valid value with
/ / scale .
let winInnerOffsetX = aWindow . mozInnerScreenX ;
try {
winInnerOffsetX =
aWindow . top . mozInnerScreenX +
( aWindow . mozInnerScreenX - aWindow . top . mozInnerScreenX ) *
_ getTopWindowResolution ( aWindow ) ;
} catch ( e ) {
/ / XXX fission + xorigin test throws permission denied since win . top is
/ / cross - origin .
}
return winInnerOffsetX ;
}
/ * *
* @ param { DOMWindow } [ aWindow ] - The DOM window which you want
* to get its y - offset in the screen .
* @ returns The screenY of aWindow in the unscaled CSS pixels .
* /
function _ getScreenYInUnscaledCSSPixels ( aWindow ) {
/ / XXX mozInnerScreen might be invalid value on mobile viewport ( Bug 1701546 ) ,
/ / so use window . top ' s mozInnerScreen . But this won ' t work fission + xorigin
/ / with mobile viewport until mozInnerScreen returns valid value with
/ / scale .
let winInnerOffsetY = aWindow . mozInnerScreenY ;
try {
winInnerOffsetY =
aWindow . top . mozInnerScreenY +
( aWindow . mozInnerScreenY - aWindow . top . mozInnerScreenY ) *
_ getTopWindowResolution ( aWindow ) ;
} catch ( e ) {
/ / XXX fission + xorigin test throws permission denied since win . top is
/ / cross - origin .
}
return winInnerOffsetY ;
}
function _ defineConstant ( name , value ) {
Object . defineProperty ( this , name , {
value ,
enumerable : true ,
writable : false ,
} ) ;
}
const COMPOSITION_ATTR_RAW_CLAUSE =
_ EU_Ci . nsITextInputProcessor . ATTR_RAW_CLAUSE ;
_ defineConstant ( " COMPOSITION_ATTR_RAW_CLAUSE " , COMPOSITION_ATTR_RAW_CLAUSE ) ;
const COMPOSITION_ATTR_SELECTED_RAW_CLAUSE =
_ EU_Ci . nsITextInputProcessor . ATTR_SELECTED_RAW_CLAUSE ;
_ defineConstant (
" COMPOSITION_ATTR_SELECTED_RAW_CLAUSE " ,
COMPOSITION_ATTR_SELECTED_RAW_CLAUSE
) ;
const COMPOSITION_ATTR_CONVERTED_CLAUSE =
_ EU_Ci . nsITextInputProcessor . ATTR_CONVERTED_CLAUSE ;
_ defineConstant (
" COMPOSITION_ATTR_CONVERTED_CLAUSE " ,
COMPOSITION_ATTR_CONVERTED_CLAUSE
) ;
const COMPOSITION_ATTR_SELECTED_CLAUSE =
_ EU_Ci . nsITextInputProcessor . ATTR_SELECTED_CLAUSE ;
_ defineConstant (
" COMPOSITION_ATTR_SELECTED_CLAUSE " ,
COMPOSITION_ATTR_SELECTED_CLAUSE
) ;
var TIPMap = new WeakMap ( ) ;
function _ getTIP ( aWindow , aCallback ) {
if ( ! aWindow ) {
aWindow = window ;
}
var tip ;
if ( TIPMap . has ( aWindow ) ) {
tip = TIPMap . get ( aWindow ) ;
} else {
tip = _ EU_Cc [ " @ mozilla . org / text - input - processor ; 1 " ] . createInstance (
_ EU_Ci . nsITextInputProcessor
) ;
TIPMap . set ( aWindow , tip ) ;
}
if ( ! tip . beginInputTransactionForTests ( aWindow , aCallback ) ) {
tip = null ;
TIPMap . delete ( aWindow ) ;
}
return tip ;
}
function _ getKeyboardEvent ( aWindow = window ) {
if ( typeof KeyboardEvent ! = " undefined " ) {
try {
/ / See if the object can be instantiated ; sometimes this yields
/ / ' TypeError : can ' t access dead object ' or ' KeyboardEvent is not a constructor ' .
new KeyboardEvent ( " " , { } ) ;
return KeyboardEvent ;
} catch ( ex ) { }
}
if ( typeof content ! = " undefined " & & " KeyboardEvent " in content ) {
return content . KeyboardEvent ;
}
return aWindow . KeyboardEvent ;
}
/ / eslint - disable - next - line complexity
function _ guessKeyNameFromKeyCode ( aKeyCode , aWindow = window ) {
var KeyboardEvent = _ getKeyboardEvent ( aWindow ) ;
switch ( aKeyCode ) {
case KeyboardEvent . DOM_VK_CANCEL :
return " Cancel " ;
case KeyboardEvent . DOM_VK_HELP :
return " Help " ;
case KeyboardEvent . DOM_VK_BACK_SPACE :
return " Backspace " ;
case KeyboardEvent . DOM_VK_TAB :
return " Tab " ;
case KeyboardEvent . DOM_VK_CLEAR :
return " Clear " ;
case KeyboardEvent . DOM_VK_RETURN :
return " Enter " ;
case KeyboardEvent . DOM_VK_SHIFT :
return " Shift " ;
case KeyboardEvent . DOM_VK_CONTROL :
return " Control " ;
case KeyboardEvent . DOM_VK_ALT :
return " Alt " ;
case KeyboardEvent . DOM_VK_PAUSE :
return " Pause " ;
case KeyboardEvent . DOM_VK_EISU :
return " Eisu " ;
case KeyboardEvent . DOM_VK_ESCAPE :
return " Escape " ;
case KeyboardEvent . DOM_VK_CONVERT :
return " Convert " ;
case KeyboardEvent . DOM_VK_NONCONVERT :
return " NonConvert " ;
case KeyboardEvent . DOM_VK_ACCEPT :
return " Accept " ;
case KeyboardEvent . DOM_VK_MODECHANGE :
return " ModeChange " ;
case KeyboardEvent . DOM_VK_PAGE_UP :
return " PageUp " ;
case KeyboardEvent . DOM_VK_PAGE_DOWN :
return " PageDown " ;
case KeyboardEvent . DOM_VK_END :
return " End " ;
case KeyboardEvent . DOM_VK_HOME :
return " Home " ;
case KeyboardEvent . DOM_VK_LEFT :
return " ArrowLeft " ;
case KeyboardEvent . DOM_VK_UP :
return " ArrowUp " ;
case KeyboardEvent . DOM_VK_RIGHT :
return " ArrowRight " ;
case KeyboardEvent . DOM_VK_DOWN :
return " ArrowDown " ;
case KeyboardEvent . DOM_VK_SELECT :
return " Select " ;
case KeyboardEvent . DOM_VK_PRINT :
return " Print " ;
case KeyboardEvent . DOM_VK_EXECUTE :
return " Execute " ;
case KeyboardEvent . DOM_VK_PRINTSCREEN :
return " PrintScreen " ;
case KeyboardEvent . DOM_VK_INSERT :
return " Insert " ;
case KeyboardEvent . DOM_VK_DELETE :
return " Delete " ;
case KeyboardEvent . DOM_VK_WIN :
return " OS " ;
case KeyboardEvent . DOM_VK_CONTEXT_MENU :
return " ContextMenu " ;
case KeyboardEvent . DOM_VK_SLEEP :
return " Standby " ;
case KeyboardEvent . DOM_VK_F1 :
return " F1 " ;
case KeyboardEvent . DOM_VK_F2 :
return " F2 " ;
case KeyboardEvent . DOM_VK_F3 :
return " F3 " ;
case KeyboardEvent . DOM_VK_F4 :
return " F4 " ;
case KeyboardEvent . DOM_VK_F5 :
return " F5 " ;
case KeyboardEvent . DOM_VK_F6 :
return " F6 " ;
case KeyboardEvent . DOM_VK_F7 :
return " F7 " ;
case KeyboardEvent . DOM_VK_F8 :
return " F8 " ;
case KeyboardEvent . DOM_VK_F9 :
return " F9 " ;
case KeyboardEvent . DOM_VK_F10 :
return " F10 " ;
case KeyboardEvent . DOM_VK_F11 :
return " F11 " ;
case KeyboardEvent . DOM_VK_F12 :
return " F12 " ;
case KeyboardEvent . DOM_VK_F13 :
return " F13 " ;
case KeyboardEvent . DOM_VK_F14 :
return " F14 " ;
case KeyboardEvent . DOM_VK_F15 :
return " F15 " ;
case KeyboardEvent . DOM_VK_F16 :
return " F16 " ;
case KeyboardEvent . DOM_VK_F17 :
return " F17 " ;
case KeyboardEvent . DOM_VK_F18 :
return " F18 " ;
case KeyboardEvent . DOM_VK_F19 :
return " F19 " ;
case KeyboardEvent . DOM_VK_F20 :
return " F20 " ;
case KeyboardEvent . DOM_VK_F21 :
return " F21 " ;
case KeyboardEvent . DOM_VK_F22 :
return " F22 " ;
case KeyboardEvent . DOM_VK_F23 :
return " F23 " ;
case KeyboardEvent . DOM_VK_F24 :
return " F24 " ;
case KeyboardEvent . DOM_VK_NUM_LOCK :
return " NumLock " ;
case KeyboardEvent . DOM_VK_SCROLL_LOCK :
return " ScrollLock " ;
case KeyboardEvent . DOM_VK_VOLUME_MUTE :
return " AudioVolumeMute " ;
case KeyboardEvent . DOM_VK_VOLUME_DOWN :
return " AudioVolumeDown " ;
case KeyboardEvent . DOM_VK_VOLUME_UP :
return " AudioVolumeUp " ;
case KeyboardEvent . DOM_VK_META :
return " Meta " ;
case KeyboardEvent . DOM_VK_ALTGR :
return " AltGraph " ;
case KeyboardEvent . DOM_VK_PROCESSKEY :
return " Process " ;
case KeyboardEvent . DOM_VK_ATTN :
return " Attn " ;
case KeyboardEvent . DOM_VK_CRSEL :
return " CrSel " ;
case KeyboardEvent . DOM_VK_EXSEL :
return " ExSel " ;
case KeyboardEvent . DOM_VK_EREOF :
return " EraseEof " ;
case KeyboardEvent . DOM_VK_PLAY :
return " Play " ;
default :
return " Unidentified " ;
}
}
function _ createKeyboardEventDictionary (
aKey ,
aKeyEvent ,
aTIP = null ,
aWindow = window
) {
var result = { dictionary : null , flags : 0 } ;
var keyCodeIsDefined = " keyCode " in aKeyEvent ;
var keyCode =
keyCodeIsDefined & & aKeyEvent . keyCode > = 0 & & aKeyEvent . keyCode < = 255
? aKeyEvent . keyCode
: 0 ;
var keyName = " Unidentified " ;
var code = aKeyEvent . code ;
if ( ! aTIP ) {
aTIP = _ getTIP ( aWindow ) ;
}
if ( aKey . indexOf ( " KEY_ " ) = = 0 ) {
keyName = aKey . substr ( " KEY_ " . length ) ;
result . flags | = _ EU_Ci . nsITextInputProcessor . KEY_NON_PRINTABLE_KEY ;
if ( code = = = undefined ) {
code = aTIP . computeCodeValueOfNonPrintableKey (
keyName ,
aKeyEvent . location
) ;
}
} else if ( aKey . indexOf ( " VK_ " ) = = 0 ) {
keyCode = _ getKeyboardEvent ( aWindow ) [ " DOM_ " + aKey ] ;
if ( ! keyCode ) {
throw new Error ( " Unknown key : " + aKey ) ;
}
keyName = _ guessKeyNameFromKeyCode ( keyCode , aWindow ) ;
result . flags | = _ EU_Ci . nsITextInputProcessor . KEY_NON_PRINTABLE_KEY ;
if ( code = = = undefined ) {
code = aTIP . computeCodeValueOfNonPrintableKey (
keyName ,
aKeyEvent . location
) ;
}
} else if ( aKey ! = " " ) {
keyName = aKey ;
if ( ! keyCodeIsDefined ) {
keyCode = aTIP . guessKeyCodeValueOfPrintableKeyInUSEnglishKeyboardLayout (
aKey ,
aKeyEvent . location
) ;
}
if ( ! keyCode ) {
result . flags | = _ EU_Ci . nsITextInputProcessor . KEY_KEEP_KEYCODE_ZERO ;
}
result . flags | = _ EU_Ci . nsITextInputProcessor . KEY_FORCE_PRINTABLE_KEY ;
if ( code = = = undefined ) {
code = aTIP . guessCodeValueOfPrintableKeyInUSEnglishKeyboardLayout (
keyName ,
aKeyEvent . location
) ;
}
}
var locationIsDefined = " location " in aKeyEvent ;
if ( locationIsDefined & & aKeyEvent . location = = = 0 ) {
result . flags | = _ EU_Ci . nsITextInputProcessor . KEY_KEEP_KEY_LOCATION_STANDARD ;
}
if ( aKeyEvent . doNotMarkKeydownAsProcessed ) {
result . flags | =
_ EU_Ci . nsITextInputProcessor . KEY_DONT_MARK_KEYDOWN_AS_PROCESSED ;
}
if ( aKeyEvent . markKeyupAsProcessed ) {
result . flags | = _ EU_Ci . nsITextInputProcessor . KEY_MARK_KEYUP_AS_PROCESSED ;
}
result . dictionary = {
key : keyName ,
code ,
location : locationIsDefined ? aKeyEvent . location : 0 ,
repeat : " repeat " in aKeyEvent ? aKeyEvent . repeat = = = true : false ,
keyCode ,
} ;
return result ;
}
function _ emulateToActivateModifiers ( aTIP , aKeyEvent , aWindow = window ) {
if ( ! aKeyEvent ) {
return null ;
}
var KeyboardEvent = _ getKeyboardEvent ( aWindow ) ;
var modifiers = {
normal : [
{ key : " Alt " , attr : " altKey " } ,
{ key : " AltGraph " , attr : " altGraphKey " } ,
{ key : " Control " , attr : " ctrlKey " } ,
{ key : " Fn " , attr : " fnKey " } ,
{ key : " Meta " , attr : " metaKey " } ,
{ key : " Shift " , attr : " shiftKey " } ,
{ key : " Symbol " , attr : " symbolKey " } ,
{ key : _ EU_isMac ( aWindow ) ? " Meta " : " Control " , attr : " accelKey " } ,
] ,
lockable : [
{ key : " CapsLock " , attr : " capsLockKey " } ,
{ key : " FnLock " , attr : " fnLockKey " } ,
{ key : " NumLock " , attr : " numLockKey " } ,
{ key : " ScrollLock " , attr : " scrollLockKey " } ,
{ key : " SymbolLock " , attr : " symbolLockKey " } ,
] ,
} ;
for ( let i = 0 ; i < modifiers . normal . length ; i + + ) {
if ( ! aKeyEvent [ modifiers . normal [ i ] . attr ] ) {
continue ;
}
if ( aTIP . getModifierState ( modifiers . normal [ i ] . key ) ) {
continue ; / / already activated .
}
let event = new KeyboardEvent ( " " , { key : modifiers . normal [ i ] . key } ) ;
aTIP . keydown (
event ,
aTIP . KEY_NON_PRINTABLE_KEY | aTIP . KEY_DONT_DISPATCH_MODIFIER_KEY_EVENT
) ;
modifiers . normal [ i ] . activated = true ;
}
for ( let i = 0 ; i < modifiers . lockable . length ; i + + ) {
if ( ! aKeyEvent [ modifiers . lockable [ i ] . attr ] ) {
continue ;
}
if ( aTIP . getModifierState ( modifiers . lockable [ i ] . key ) ) {
continue ; / / already activated .
}
let event = new KeyboardEvent ( " " , { key : modifiers . lockable [ i ] . key } ) ;
aTIP . keydown (
event ,
aTIP . KEY_NON_PRINTABLE_KEY | aTIP . KEY_DONT_DISPATCH_MODIFIER_KEY_EVENT
) ;
aTIP . keyup (
event ,
aTIP . KEY_NON_PRINTABLE_KEY | aTIP . KEY_DONT_DISPATCH_MODIFIER_KEY_EVENT
) ;
modifiers . lockable [ i ] . activated = true ;
}
return modifiers ;
}
function _ emulateToInactivateModifiers ( aTIP , aModifiers , aWindow = window ) {
if ( ! aModifiers ) {
return ;
}
var KeyboardEvent = _ getKeyboardEvent ( aWindow ) ;
for ( let i = 0 ; i < aModifiers . normal . length ; i + + ) {
if ( ! aModifiers . normal [ i ] . activated ) {
continue ;
}
let event = new KeyboardEvent ( " " , { key : aModifiers . normal [ i ] . key } ) ;
aTIP . keyup (
event ,
aTIP . KEY_NON_PRINTABLE_KEY | aTIP . KEY_DONT_DISPATCH_MODIFIER_KEY_EVENT
) ;
}
for ( let i = 0 ; i < aModifiers . lockable . length ; i + + ) {
if ( ! aModifiers . lockable [ i ] . activated ) {
continue ;
}
if ( ! aTIP . getModifierState ( aModifiers . lockable [ i ] . key ) ) {
continue ; / / who already inactivated this ?
}
let event = new KeyboardEvent ( " " , { key : aModifiers . lockable [ i ] . key } ) ;
aTIP . keydown (
event ,
aTIP . KEY_NON_PRINTABLE_KEY | aTIP . KEY_DONT_DISPATCH_MODIFIER_KEY_EVENT
) ;
aTIP . keyup (
event ,
aTIP . KEY_NON_PRINTABLE_KEY | aTIP . KEY_DONT_DISPATCH_MODIFIER_KEY_EVENT
) ;
}
}
/ * *
* Synthesize a composition event and keydown event and keyup events unless
* you prevent to dispatch them explicitly ( see aEvent . key ' s explanation ) .
*
* Note that you shouldn ' t call this with " compositionstart " unless you need to
* test compositionstart event which is NOT followed by compositionupdate
* event immediately . Typically , native IME starts composition with
* a pair of keydown and keyup event and dispatch compositionstart and
* compositionupdate ( and non - standard text event ) between them . So , in most
* cases , you should call synthesizeCompositionChange ( ) directly .
* If you call this with compositionstart , keyup event will be fired
* immediately after compositionstart . In other words , you should use
* " compositionstart " only when you need to emulate IME which just starts
* composition with compositionstart event but does not send composing text to
* us until committing the composition . This is behavior of some Chinese IMEs .
*
* @ param aEvent The composition event information . This must
* have | type | member . The value must be
* " compositionstart " , " compositionend " ,
* " compositioncommitasis " or " compositioncommit " .
*
* And also this may have | data | and | locale | which
* would be used for the value of each property of
* the composition event . Note that the | data | is
* ignored if the event type is " compositionstart "
* or " compositioncommitasis " .
*
* If | key | is undefined , " keydown " and " keyup "
* events which are marked as " processed by IME "
* are dispatched . If | key | is not null , " keydown "
* and / or " keyup " events are dispatched ( if the
* | key . type | is specified as " keydown " , only
* " keydown " event is dispatched ) . Otherwise ,
* i . e . , if | key | is null , neither " keydown " nor
* " keyup " event is dispatched .
*
* If | key . doNotMarkKeydownAsProcessed | is not true ,
* key value and keyCode value of " keydown " event
* will be set to " Process " and DOM_VK_PROCESSKEY .
* If | key . markKeyupAsProcessed | is true ,
* key value and keyCode value of " keyup " event
* will be set to " Process " and DOM_VK_PROCESSKEY .
* @ param aWindow Optional ( If null , current | window | will be used )
* @ param aCallback Optional ( If non - null , use the callback for
* receiving notifications to IME )
* /
function synthesizeComposition ( aEvent , aWindow = window , aCallback ) {
var TIP = _ getTIP ( aWindow , aCallback ) ;
if ( ! TIP ) {
return ;
}
var KeyboardEvent = _ getKeyboardEvent ( aWindow ) ;
var modifiers = _ emulateToActivateModifiers ( TIP , aEvent . key , aWindow ) ;
var keyEventDict = { dictionary : null , flags : 0 } ;
var keyEvent = null ;
if ( aEvent . key & & typeof aEvent . key . key = = = " string " ) {
keyEventDict = _ createKeyboardEventDictionary (
aEvent . key . key ,
aEvent . key ,
TIP ,
aWindow
) ;
keyEvent = new KeyboardEvent (
/ / eslint - disable - next - line no - nested - ternary
aEvent . key . type = = = " keydown "
? " keydown "
: aEvent . key . type = = = " keyup "
? " keyup "
: " " ,
keyEventDict . dictionary
) ;
} else if ( aEvent . key = = = undefined ) {
keyEventDict = _ createKeyboardEventDictionary (
" KEY_Process " ,
{ } ,
TIP ,
aWindow
) ;
keyEvent = new KeyboardEvent ( " " , keyEventDict . dictionary ) ;
}
try {
switch ( aEvent . type ) {
case " compositionstart " :
TIP . startComposition ( keyEvent , keyEventDict . flags ) ;
break ;
case " compositioncommitasis " :
TIP . commitComposition ( keyEvent , keyEventDict . flags ) ;
break ;
case " compositioncommit " :
TIP . commitCompositionWith ( aEvent . data , keyEvent , keyEventDict . flags ) ;
break ;
}
} finally {
_ emulateToInactivateModifiers ( TIP , modifiers , aWindow ) ;
}
}
/ * *
* Synthesize eCompositionChange event which causes a DOM text event , may
* cause compositionupdate event , and causes keydown event and keyup event
* unless you prevent to dispatch them explicitly ( see aEvent . key ' s
* explanation ) .
*
* Note that if you call this when there is no composition , compositionstart
* event will be fired automatically . This is better than you use
* synthesizeComposition ( " compositionstart " ) in most cases . See the
* explanation of synthesizeComposition ( ) .
*
* @ param aEvent The compositionchange event ' s information , this has
* | composition | and | caret | members . | composition | has
* | string | and | clauses | members . | clauses | must be array
* object . Each object has | length | and | attr | . And | caret |
* has | start | and | length | . See the following tree image .
*
* aEvent
* + - - composition
* | + - - string
* | + - - clauses [ ]
* | + - - length
* | + - - attr
* + - - caret
* | + - - start
* | + - - length
* + - - key
*
* Set the composition string to | composition . string | . Set its
* clauses information to the | clauses | array .
*
* When it ' s composing , set the each clauses ' length to the
* | composition . clauses [ n ] . length | . The sum of the all length
* values must be same as the length of | composition . string | .
* Set nsICompositionStringSynthesizer . ATTR_ * to the
* | composition . clauses [ n ] . attr | .
*
* When it ' s not composing , set 0 to the
* | composition . clauses [ 0 ] . length | and
* | composition . clauses [ 0 ] . attr | .
*
* Set caret position to the | caret . start | . It ' s offset from
* the start of the composition string . Set caret length to
* | caret . length | . If it ' s larger than 0 , it should be wide
* caret . However , current nsEditor doesn ' t support wide
* caret , therefore , you should always set 0 now .
*
* If | key | is undefined , " keydown " and " keyup " events which
* are marked as " processed by IME " are dispatched . If | key |
* is not null , " keydown " and / or " keyup " events are dispatched
* ( if the | key . type | is specified as " keydown " , only " keydown "
* event is dispatched ) . Otherwise , i . e . , if | key | is null ,
* neither " keydown " nor " keyup " event is dispatched .
* If | key . doNotMarkKeydownAsProcessed | is not true , key value
* and keyCode value of " keydown " event will be set to
* " Process " and DOM_VK_PROCESSKEY .
* If | key . markKeyupAsProcessed | is true key value and keyCode
* value of " keyup " event will be set to " Process " and
* DOM_VK_PROCESSKEY .
*
* @ param aWindow Optional ( If null , current | window | will be used )
* @ param aCallback Optional ( If non - null , use the callback for receiving
* notifications to IME )
* /
function synthesizeCompositionChange ( aEvent , aWindow = window , aCallback ) {
var TIP = _ getTIP ( aWindow , aCallback ) ;
if ( ! TIP ) {
return ;
}
var KeyboardEvent = _ getKeyboardEvent ( aWindow ) ;
if (
! aEvent . composition | |
! aEvent . composition . clauses | |
! aEvent . composition . clauses [ 0 ]
) {
return ;
}
TIP . setPendingCompositionString ( aEvent . composition . string ) ;
if ( aEvent . composition . clauses [ 0 ] . length ) {
for ( var i = 0 ; i < aEvent . composition . clauses . length ; i + + ) {
switch ( aEvent . composition . clauses [ i ] . attr ) {
case TIP . ATTR_RAW_CLAUSE :
case TIP . ATTR_SELECTED_RAW_CLAUSE :
case TIP . ATTR_CONVERTED_CLAUSE :
case TIP . ATTR_SELECTED_CLAUSE :
TIP . appendClauseToPendingComposition (
aEvent . composition . clauses [ i ] . length ,
aEvent . composition . clauses [ i ] . attr
) ;
break ;
case 0 :
/ / Ignore dummy clause for the argument .
break ;
default :
throw new Error ( " invalid clause attribute specified " ) ;
}
}
}
if ( aEvent . caret ) {
TIP . setCaretInPendingComposition ( aEvent . caret . start ) ;
}
var modifiers = _ emulateToActivateModifiers ( TIP , aEvent . key , aWindow ) ;
try {
var keyEventDict = { dictionary : null , flags : 0 } ;
var keyEvent = null ;
if ( aEvent . key & & typeof aEvent . key . key = = = " string " ) {
keyEventDict = _ createKeyboardEventDictionary (
aEvent . key . key ,
aEvent . key ,
TIP ,
aWindow
) ;
keyEvent = new KeyboardEvent (
/ / eslint - disable - next - line no - nested - ternary
aEvent . key . type = = = " keydown "
? " keydown "
: aEvent . key . type = = = " keyup "
? " keyup "
: " " ,
keyEventDict . dictionary
) ;
} else if ( aEvent . key = = = undefined ) {
keyEventDict = _ createKeyboardEventDictionary (
" KEY_Process " ,
{ } ,
TIP ,
aWindow
) ;
keyEvent = new KeyboardEvent ( " " , keyEventDict . dictionary ) ;
}
TIP . flushPendingComposition ( keyEvent , keyEventDict . flags ) ;
} finally {
_ emulateToInactivateModifiers ( TIP , modifiers , aWindow ) ;
}
}
/ / Must be synchronized with nsIDOMWindowUtils .
const QUERY_CONTENT_FLAG_SELECTION_NORMAL = 0 x0000 ;
const QUERY_CONTENT_FLAG_SELECTION_SPELLCHECK = 0 x0002 ;
const QUERY_CONTENT_FLAG_SELECTION_IME_RAWINPUT = 0 x0004 ;
const QUERY_CONTENT_FLAG_SELECTION_IME_SELECTEDRAWTEXT = 0 x0008 ;
const QUERY_CONTENT_FLAG_SELECTION_IME_CONVERTEDTEXT = 0 x0010 ;
const QUERY_CONTENT_FLAG_SELECTION_IME_SELECTEDCONVERTEDTEXT = 0 x0020 ;
const QUERY_CONTENT_FLAG_SELECTION_ACCESSIBILITY = 0 x0040 ;
const QUERY_CONTENT_FLAG_SELECTION_FIND = 0 x0080 ;
const QUERY_CONTENT_FLAG_SELECTION_URLSECONDARY = 0 x0100 ;
const QUERY_CONTENT_FLAG_SELECTION_URLSTRIKEOUT = 0 x0200 ;
const QUERY_CONTENT_FLAG_OFFSET_RELATIVE_TO_INSERTION_POINT = 0 x0400 ;
const SELECTION_SET_FLAG_USE_NATIVE_LINE_BREAK = 0 x0000 ;
const SELECTION_SET_FLAG_USE_XP_LINE_BREAK = 0 x0001 ;
const SELECTION_SET_FLAG_REVERSE = 0 x0002 ;
/ * *
* Synthesize a query text content event .
*
* @ param aOffset The character offset . 0 means the first character in the
* selection root .
* @ param aLength The length of getting text . If the length is too long ,
* the extra length is ignored .
* @ param aIsRelative Optional ( If true , aOffset is relative to start of
* composition if there is , or start of selection . )
* @ param aWindow Optional ( If null , current | window | will be used )
* @ return An nsIQueryContentEventResult object . If this failed ,
* the result might be null .
* /
function synthesizeQueryTextContent ( aOffset , aLength , aIsRelative , aWindow ) {
var utils = _ getDOMWindowUtils ( aWindow ) ;
if ( ! utils ) {
return null ;
}
let flags = 0 ;
if ( aIsRelative = = = true ) {
flags | = QUERY_CONTENT_FLAG_OFFSET_RELATIVE_TO_INSERTION_POINT ;
}
return utils . sendQueryContentEvent (
utils . QUERY_TEXT_CONTENT ,
aOffset ,
aLength ,
0 ,
0 ,
flags
) ;
}
/ * *
* Synthesize a query selected text event .
*
* @ param aSelectionType Optional , one of QUERY_CONTENT_FLAG_SELECTION_ * .
* If null , QUERY_CONTENT_FLAG_SELECTION_NORMAL will
* be used .
* @ param aWindow Optional ( If null , current | window | will be used )
* @ return An nsIQueryContentEventResult object . If this failed ,
* the result might be null .
* /
function synthesizeQuerySelectedText ( aSelectionType , aWindow ) {
var utils = _ getDOMWindowUtils ( aWindow ) ;
let flags = 0 ;
if ( aSelectionType ) {
flags | = aSelectionType ;
}
return utils . sendQueryContentEvent (
utils . QUERY_SELECTED_TEXT ,
0 ,
0 ,
0 ,
0 ,
flags
) ;
}
/ * *
* Synthesize a query caret rect event .
*
* @ param aOffset The caret offset . 0 means left side of the first character
* in the selection root .
* @ param aWindow Optional ( If null , current | window | will be used )
* @ return An nsIQueryContentEventResult object . If this failed ,
* the result might be null .
* /
function synthesizeQueryCaretRect ( aOffset , aWindow ) {
var utils = _ getDOMWindowUtils ( aWindow ) ;
if ( ! utils ) {
return null ;
}
return utils . sendQueryContentEvent ( utils . QUERY_CARET_RECT , aOffset , 0 , 0 , 0 ) ;
}
/ * *
* Synthesize a selection set event .
*
* @ param aOffset The character offset . 0 means the first character in the
* selection root .
* @ param aLength The length of the text . If the length is too long ,
* the extra length is ignored .
* @ param aReverse If true , the selection is from | aOffset + aLength | to
* | aOffset | . Otherwise , from | aOffset | to | aOffset + aLength | .
* @ param aWindow Optional ( If null , current | window | will be used )
* @ return True , if succeeded . Otherwise false .
* /
async function synthesizeSelectionSet (
aOffset ,
aLength ,
aReverse ,
aWindow = window
) {
const utils = _ getDOMWindowUtils ( aWindow ) ;
if ( ! utils ) {
return false ;
}
/ / eSetSelection event will be compared with selection cache in
/ / IMEContentObserver , but it may have not been updated yet . Therefore , we
/ / need to flush pending things of IMEContentObserver .
await new Promise ( resolve = >
aWindow . requestAnimationFrame ( ( ) = > aWindow . requestAnimationFrame ( resolve ) )
) ;
const flags = aReverse ? SELECTION_SET_FLAG_REVERSE : 0 ;
return utils . sendSelectionSetEvent ( aOffset , aLength , flags ) ;
}
/ * *
* Synthesize a query text rect event .
*
* @ param aOffset The character offset . 0 means the first character in the
* selection root .
* @ param aLength The length of the text . If the length is too long ,
* the extra length is ignored .
* @ param aIsRelative Optional ( If true , aOffset is relative to start of
* composition if there is , or start of selection . )
* @ param aWindow Optional ( If null , current | window | will be used )
* @ return An nsIQueryContentEventResult object . If this failed ,
* the result might be null .
* /
function synthesizeQueryTextRect ( aOffset , aLength , aIsRelative , aWindow ) {
if ( aIsRelative ! = = undefined & & typeof aIsRelative ! = = " boolean " ) {
throw new Error (
" Maybe , you set Window object to the 3 rd argument , but it should be a boolean value "
) ;
}
var utils = _ getDOMWindowUtils ( aWindow ) ;
let flags = 0 ;
if ( aIsRelative = = = true ) {
flags | = QUERY_CONTENT_FLAG_OFFSET_RELATIVE_TO_INSERTION_POINT ;
}
return utils . sendQueryContentEvent (
utils . QUERY_TEXT_RECT ,
aOffset ,
aLength ,
0 ,
0 ,
flags
) ;
}
/ * *
* Synthesize a query text rect array event .
*
* @ param aOffset The character offset . 0 means the first character in the
* selection root .
* @ param aLength The length of the text . If the length is too long ,
* the extra length is ignored .
* @ param aWindow Optional ( If null , current | window | will be used )
* @ return An nsIQueryContentEventResult object . If this failed ,
* the result might be null .
* /
function synthesizeQueryTextRectArray ( aOffset , aLength , aWindow ) {
var utils = _ getDOMWindowUtils ( aWindow ) ;
return utils . sendQueryContentEvent (
utils . QUERY_TEXT_RECT_ARRAY ,
aOffset ,
aLength ,
0 ,
0
) ;
}
/ * *
* Synthesize a query editor rect event .
*
* @ param aWindow Optional ( If null , current | window | will be used )
* @ return An nsIQueryContentEventResult object . If this failed ,
* the result might be null .
* /
function synthesizeQueryEditorRect ( aWindow ) {
var utils = _ getDOMWindowUtils ( aWindow ) ;
return utils . sendQueryContentEvent ( utils . QUERY_EDITOR_RECT , 0 , 0 , 0 , 0 ) ;
}
/ * *
* Synthesize a character at point event .
*
* @ param aX , aY The offset in the client area of the DOM window .
* @ param aWindow Optional ( If null , current | window | will be used )
* @ return An nsIQueryContentEventResult object . If this failed ,
* the result might be null .
* /
function synthesizeCharAtPoint ( aX , aY , aWindow ) {
var utils = _ getDOMWindowUtils ( aWindow ) ;
return utils . sendQueryContentEvent (
utils . QUERY_CHARACTER_AT_POINT ,
0 ,
0 ,
aX ,
aY
) ;
}
/ * *
* INTERNAL USE ONLY
* Create an event object to pass to sendDragEvent .
*
* @ param aType The string represents drag event type .
* @ param aDestElement The element to fire the drag event , used to calculate
* screenX / Y and clientX / Y .
* @ param aDestWindow Optional ; Defaults to the current window object .
* @ param aDataTransfer dataTransfer for current drag session .
* @ param aDragEvent The object contains properties to override the event
* object
* @ return An object to pass to sendDragEvent .
* /
function createDragEventObject (
aType ,
aDestElement ,
aDestWindow ,
aDataTransfer ,
aDragEvent
) {
const resolution = _ getTopWindowResolution ( aDestWindow . top ) ;
const destRect = aDestElement . getBoundingClientRect ( ) ;
/ / If clientX and / or clientY are specified , we should use them . Otherwise ,
/ / use the center of the dest element .
const destClientXInCSSPixels =
" clientX " in aDragEvent & & ! ( " screenX " in aDragEvent )
? aDragEvent . clientX
: destRect . left + destRect . width / 2 ;
const destClientYInCSSPixels =
" clientY " in aDragEvent & & ! ( " screenY " in aDragEvent )
? aDragEvent . clientY
: destRect . top + destRect . height / 2 ;
const devicePixelRatio = aDestWindow . devicePixelRatio ;
const destScreenXInDevicePixels =
( _ getScreenXInUnscaledCSSPixels ( aDestWindow ) +
destClientXInCSSPixels * resolution ) *
devicePixelRatio ;
const destScreenYInDevicePixels =
( _ getScreenYInUnscaledCSSPixels ( aDestWindow ) +
destClientYInCSSPixels * resolution ) *
devicePixelRatio ;
/ / Wrap only in plain mochitests
let dataTransfer ;
if ( aDataTransfer ) {
dataTransfer = _ EU_maybeUnwrap (
_ EU_maybeWrap ( aDataTransfer ) . mozCloneForEvent ( aType )
) ;
/ / Copy over the drop effect . This isn ' t copied over by Clone , as it uses
/ / more complex logic in the actual implementation ( see
/ / nsContentUtils : : SetDataTransferInEvent for actual impl ) .
dataTransfer . dropEffect = aDataTransfer . dropEffect ;
}
return Object . assign (
{
type : aType ,
screenX : _ EU_roundDevicePixels ( destScreenXInDevicePixels ) ,
screenY : _ EU_roundDevicePixels ( destScreenYInDevicePixels ) ,
clientX : _ EU_roundDevicePixels ( destClientXInCSSPixels ) ,
clientY : _ EU_roundDevicePixels ( destClientYInCSSPixels ) ,
dataTransfer ,
_ domDispatchOnly : aDragEvent . _ domDispatchOnly ,
} ,
aDragEvent
) ;
}
/ * *
* Emulate a event sequence of dragstart , dragenter , and dragover .
*
* @ param { Element } aSrcElement
* The element to use to start the drag .
* @ param { Element } aDestElement
* The element to fire the dragover , dragenter events
* @ param { Array } aDragData
* The data to supply for the data transfer .
* This data is in the format :
*
* [
* [
* { " type " : value , " data " : value } ,
* . . . ,
* ] ,
* . . .
* ]
*
* Pass null to avoid modifying dataTransfer .
* @ param { string } [ aDropEffect = " move " ]
* The drop effect to set during the dragstart event , or ' move ' if omitted .
* @ param { DOMWindow } [ aWindow = window ]
* The DOM window in which the drag happens . Defaults to the window in which
* EventUtils . js is loaded .
* @ param { DOMWindow } [ aDestWindow = aWindow ]
* Used when aDestElement is in a different DOM window than aSrcElement .
* Default is to match ` ` aWindow ` ` .
* @ param { object } [ aDragEvent = { } ]
* Defaults to empty object . Overwrites an object passed to sendDragEvent .
* @ return { [ boolean , DataTransfer ] }
* A two element array , where the first element is the value returned
* from sendDragEvent for dragover event , and the second element is the
* dataTransfer for the current drag session .
* /
function synthesizeDragOver (
aSrcElement ,
aDestElement ,
aDragData ,
aDropEffect ,
aWindow ,
aDestWindow ,
aDragEvent = { }
) {
if ( ! aWindow ) {
aWindow = window ;
}
if ( ! aDestWindow ) {
aDestWindow = aWindow ;
}
/ / eslint - disable - next - line mozilla / use - services
const obs = _ EU_Cc [ " @ mozilla . org / observer - service ; 1 " ] . getService (
_ EU_Ci . nsIObserverService
) ;
let utils = _ getDOMWindowUtils ( aWindow ) ;
var sess = utils . dragSession ;
/ / This method runs before other callbacks , and acts as a way to inject the
/ / initial drag data into the DataTransfer .
function fillDrag ( event ) {
if ( aDragData ) {
for ( var i = 0 ; i < aDragData . length ; i + + ) {
var item = aDragData [ i ] ;
for ( var j = 0 ; j < item . length ; j + + ) {
_ EU_maybeWrap ( event . dataTransfer ) . mozSetDataAt (
item [ j ] . type ,
item [ j ] . data ,
i
) ;
}
}
}
event . dataTransfer . dropEffect = aDropEffect | | " move " ;
event . preventDefault ( ) ;
}
function trapDrag ( subject , topic ) {
if ( topic = = " on - datatransfer - available " ) {
sess . dataTransfer = _ EU_maybeUnwrap (
_ EU_maybeWrap ( subject ) . mozCloneForEvent ( " drop " )
) ;
sess . dataTransfer . dropEffect = subject . dropEffect ;
}
}
/ / need to use real mouse action
aWindow . addEventListener ( " dragstart " , fillDrag , true ) ;
obs . addObserver ( trapDrag , " on - datatransfer - available " ) ;
synthesizeMouseAtCenter ( aSrcElement , { type : " mousedown " } , aWindow ) ;
var rect = aSrcElement . getBoundingClientRect ( ) ;
var x = rect . width / 2 ;
var y = rect . height / 2 ;
synthesizeMouse ( aSrcElement , x , y , { type : " mousemove " } , aWindow ) ;
synthesizeMouse ( aSrcElement , x + 10 , y + 10 , { type : " mousemove " } , aWindow ) ;
aWindow . removeEventListener ( " dragstart " , fillDrag , true ) ;
obs . removeObserver ( trapDrag , " on - datatransfer - available " ) ;
var dataTransfer = sess . dataTransfer ;
if ( ! dataTransfer ) {
throw new Error ( " No data transfer object after synthesizing the mouse ! " ) ;
}
/ / The EventStateManager will fire our dragenter event if it needs to .
var event = createDragEventObject (
" dragover " ,
aDestElement ,
aDestWindow ,
dataTransfer ,
aDragEvent
) ;
var result = sendDragEvent ( event , aDestElement , aDestWindow ) ;
return [ result , dataTransfer ] ;
}
/ * *
* Emulate the drop event and mouseup event .
* This should be called after synthesizeDragOver .
*
* @ param { * } aResult
* The first element of the array returned from ` ` synthesizeDragOver ` ` .
* @ param { DataTransfer } aDataTransfer
* The second element of the array returned from ` ` synthesizeDragOver ` ` .
* @ param { Element } aDestElement
* The element on which to fire the drop event .
* @ param { DOMWindow } [ aDestWindow = window ]
* The DOM window in which the drop happens . Defaults to the window in which
* EventUtils . js is loaded .
* @ param { object } [ aDragEvent = { } ]
* Defaults to empty object . Overwrites an object passed to sendDragEvent .
* @ return { string }
* " none " if aResult is true , ` ` aDataTransfer . dropEffect ` ` otherwise .
* /
function synthesizeDropAfterDragOver (
aResult ,
aDataTransfer ,
aDestElement ,
aDestWindow ,
aDragEvent = { }
) {
if ( ! aDestWindow ) {
aDestWindow = window ;
}
var effect = aDataTransfer . dropEffect ;
var event ;
if ( aResult ) {
effect = " none " ;
} else if ( effect ! = " none " ) {
event = createDragEventObject (
" drop " ,
aDestElement ,
aDestWindow ,
aDataTransfer ,
aDragEvent
) ;
sendDragEvent ( event , aDestElement , aDestWindow ) ;
}
/ / Don ' t run accessibility checks for this click , since we ' re not actually
/ / clicking . It ' s just generated as part of the drop .
/ / this . AccessibilityUtils might not be set if this isn ' t a browser test or
/ / if a browser test has loaded its own copy of EventUtils for some reason .
/ / In the latter case , the test probably shouldn ' t do that .
this . AccessibilityUtils ? . suppressClickHandling ( true ) ;
synthesizeMouse ( aDestElement , 2 , 2 , { type : " mouseup " } , aDestWindow ) ;
this . AccessibilityUtils ? . suppressClickHandling ( false ) ;
return effect ;
}
/ * *
* Calls ` nsIDragService . startDragSessionForTests ` , which is required before
* any other code can use ` nsIDOMWindowUtils . dragSession ` . Most notably ,
* a drag session is required before populating a drag - drop event ' s
* ` dataTransfer ` property .
*
* @ param { Window } aWindow
* @ param { typeof DataTransfer . prototype . dropEffect } aDropEffect
* /
function startDragSession ( aWindow , aDropEffect ) {
const ds = _ EU_Cc [ " @ mozilla . org / widget / dragservice ; 1 " ] . getService (
_ EU_Ci . nsIDragService
) ;
let dropAction ;
switch ( aDropEffect ) {
case null :
case undefined :
case " move " :
dropAction = _ EU_Ci . nsIDragService . DRAGDROP_ACTION_MOVE ;
break ;
case " copy " :
dropAction = _ EU_Ci . nsIDragService . DRAGDROP_ACTION_COPY ;
break ;
case " link " :
dropAction = _ EU_Ci . nsIDragService . DRAGDROP_ACTION_LINK ;
break ;
default :
throw new Error ( ` $ { aDropEffect } is an invalid drop effect value ` ) ;
}
ds . startDragSessionForTests ( aWindow , dropAction ) ;
}
/ * *
* Emulate a drag and drop by emulating a dragstart and firing events dragenter ,
* dragover , and drop .
*
* @ param { Element } aSrcElement
* The element to use to start the drag .
* @ param { Element } aDestElement
* The element to fire the dragover , dragenter events
* @ param { Array } aDragData
* The data to supply for the data transfer .
* This data is in the format :
*
* [
* [
* { " type " : value , " data " : value } ,
* . . . ,
* ] ,
* . . .
* ]
*
* Pass null to avoid modifying dataTransfer .
* @ param { string } [ aDropEffect = " move " ]
* The drop effect to set during the dragstart event , or ' move ' if omitted . .
* @ param { DOMWindow } [ aWindow = window ]
* The DOM window in which the drag happens . Defaults to the window in which
* EventUtils . js is loaded .
* @ param { DOMWindow } [ aDestWindow = aWindow ]
* Used when aDestElement is in a different DOM window than aSrcElement .
* Default is to match ` ` aWindow ` ` .
* @ param { object } [ aDragEvent = { } ]
* Defaults to empty object . Overwrites an object passed to sendDragEvent .
* @ return { string }
* The drop effect that was desired .
* /
function synthesizeDrop (
aSrcElement ,
aDestElement ,
aDragData ,
aDropEffect ,
aWindow ,
aDestWindow ,
aDragEvent = { }
) {
if ( ! aWindow ) {
aWindow = window ;
}
if ( ! aDestWindow ) {
aDestWindow = aWindow ;
}
startDragSession ( aWindow , aDropEffect ) ;
try {
var [ result , dataTransfer ] = synthesizeDragOver (
aSrcElement ,
aDestElement ,
aDragData ,
aDropEffect ,
aWindow ,
aDestWindow ,
aDragEvent
) ;
return synthesizeDropAfterDragOver (
result ,
dataTransfer ,
aDestElement ,
aDestWindow ,
aDragEvent
) ;
} finally {
let srcWindowUtils = _ getDOMWindowUtils ( aWindow ) ;
const srcDragSession = srcWindowUtils . dragSession ;
if ( srcDragSession ) {
/ / After each event handler , there is a microtask checkpoint .
/ / Event handlers or microtasks might ' ve already ended our drag session .
/ / E . g . in SubDialog . open during browser_toolbar_drop_bookmarklet . js
srcDragSession . endDragSession ( true , _ parseModifiers ( aDragEvent ) ) ;
}
}
}
function _ getFlattenedTreeParentNode ( aNode ) {
return _ EU_maybeUnwrap ( _ EU_maybeWrap ( aNode ) . flattenedTreeParentNode ) ;
}
function _ getInclusiveFlattenedTreeParentElement ( aNode ) {
for (
let inclusiveAncestor = aNode ;
inclusiveAncestor ;
inclusiveAncestor = _ getFlattenedTreeParentNode ( inclusiveAncestor )
) {
if ( inclusiveAncestor . nodeType = = Node . ELEMENT_NODE ) {
return inclusiveAncestor ;
}
}
return null ;
}
function _ nodeIsFlattenedTreeDescendantOf (
aPossibleDescendant ,
aPossibleAncestor
) {
do {
if ( aPossibleDescendant = = aPossibleAncestor ) {
return true ;
}
aPossibleDescendant = _ getFlattenedTreeParentNode ( aPossibleDescendant ) ;
} while ( aPossibleDescendant ) ;
return false ;
}
function _ computeSrcElementFromSrcSelection ( aSrcSelection ) {
let srcElement = _ EU_maybeUnwrap (
_ EU_maybeWrap ( aSrcSelection ) . mayCrossShadowBoundaryFocusNode
) ;
while ( _ EU_maybeWrap ( srcElement ) . isNativeAnonymous ) {
srcElement = _ getFlattenedTreeParentNode ( srcElement ) ;
}
if ( srcElement . nodeType ! = = Node . ELEMENT_NODE ) {
srcElement = _ getInclusiveFlattenedTreeParentElement ( srcElement ) ;
}
return srcElement ;
}
/ * *
* Emulate a drag and drop by emulating a dragstart by mousedown and mousemove ,
* and firing events dragenter , dragover , drop , and dragend .
* This does not modify dataTransfer and tries to emulate the plain drag and
* drop as much as possible , compared to synthesizeDrop .
* Note that if synthesized dragstart is canceled , this throws an exception
* because in such case , Gecko does not start drag session .
*
* @ param { object } aParams
* @ param { Event } aParams . dragEvent
* The DnD events will be generated with modifiers specified with this .
* @ param { Element } aParams . srcElement
* The element to start dragging . If srcSelection is
* set , this is computed for element at focus node .
* @ param { Selection | null } aParams . srcSelection
* The selection to start to drag , set null if srcElement is set .
* @ param { Element | null } aParams . destElement
* The element to drop on . Pass null to emulate a drop on an invalid target .
* @ param { number } aParams . srcX
* The initial x coordinate inside srcElement or ignored if srcSelection is set .
* @ param { number } aParams . srcY
* The initial y coordinate inside srcElement or ignored if srcSelection is set .
* @ param { number } aParams . stepX
* The x - axis step for mousemove inside srcElement
* @ param { number } aParams . stepY
* The y - axis step for mousemove inside srcElement
* @ param { number } aParams . finalX
* The final x coordinate inside srcElement
* @ param { number } aParams . finalY
* The final x coordinate inside srcElement
* @ param { Any } aParams . id
* The pointer event id
* @ param { DOMWindow } aParams . srcWindow
* The DOM window for dispatching event on srcElement , defaults to the current window object .
* @ param { DOMWindow } aParams . destWindow
* The DOM window for dispatching event on destElement , defaults to the current window object .
* @ param { boolean } aParams . expectCancelDragStart
* Set to true if the test cancels " dragstart "
* @ param { boolean } aParams . expectSrcElementDisconnected
* Set to true if srcElement will be disconnected and
* " dragend " event won ' t be fired .
* @ param { Function } aParams . logFunc
* Set function which takes one argument if you need to log rect of target . E . g . , ` console . log ` .
* /
/ / eslint - disable - next - line complexity
async function synthesizePlainDragAndDrop ( aParams ) {
let {
# You may obtain # https : / / www . java.lang.StringIndexOutOfBoundsException: Index 20 out of bounds for length 1
srcElement ,
srcSelection ,
destElement ,
srcX = 2 ,
srcY = 2 ,
stepX = 9 ,
stepY = 9 ,
finalX = srcX + stepX * 2 ,
finalY = srcY + stepY * 2 ,
id = _ getDOMWindowUtils ( window ) . DEFAULT_MOUSE_POINTER_ID ,
srcWindow = window ,
destWindow = window ,
expectCancelDragStart = false ,
expectSrcElementDisconnected = false ,
logFunc ,
} = aParams ;
/ / Don ' t modify given dragEvent object because we modify dragEvent below and
/ / callers may use the object multiple times so that callers must not assume
/ / that it ' ll be modified .
if ( aParams . dragEvent ! = = undefined ) {
dragEvent = Object . assign ( { } , aParams . dragEvent ) ;
}
function rectToString ( aRect ) {
return ` left : $ { aRect . left } , top : $ { aRect . top } , right : $ { aRect . right } , bottom : $ { aRect . bottom } ` ;
}
let srcWindowUtils = _ getDOMWindowUtils ( srcWindow ) ;
let destWindowUtils = _ getDOMWindowUtils ( destWindow ) ;
if ( logFunc ) {
logFunc ( " synthesizePlainDragAndDrop ( ) - - START " ) ;
}
if ( srcSelection ) {
srcElement = _ computeSrcElementFromSrcSelection ( srcSelection ) ;
let srcElementRect = srcElement . getBoundingClientRect ( ) ;
if ( logFunc ) {
logFunc (
` srcElement . getBoundingClientRect ( ) : $ { rectToString ( srcElementRect ) } `
) ;
}
/ / Use last selection client rect because nsIDragSession . sourceNode is
/ / initialized from focus node which is usually in last rect .
let selectionRectList = SpecialPowers . wrap (
srcSelection . getRangeAt ( 0 )
) . getAllowCrossShadowBoundaryClientRects ( ) ;
let lastSelectionRect = selectionRectList [ selectionRectList . length - 1 ] ;
if ( logFunc ) {
logFunc (
` srcSelection . getRangeAt ( 0 ) . getClientRects ( ) [ $ {
selectionRectList . length - 1
} ] : $ { rectToString ( lastSelectionRect ) } `
) ;
}
/ / Click at center of last selection rect .
srcX = Math . floor ( lastSelectionRect . left + lastSelectionRect . width / 2 ) ;
srcY = Math . floor ( lastSelectionRect . top + lastSelectionRect . height / 2 ) ;
/ / Then , adjust srcX and srcY for making them offset relative to
/ / srcElementRect because they will be used when we call synthesizeMouse ( )
/ / with srcElement .
srcX = Math . floor ( srcX - srcElementRect . left ) ;
srcY = Math . floor ( srcY - srcElementRect . top ) ;
/ / Finally , recalculate finalX and finalY with new srcX and srcY if they
/ / are not specified by the caller .
if ( aParams . finalX = = = undefined ) {
finalX = srcX + stepX * 2 ;
}
if ( aParams . finalY = = = undefined ) {
finalY = srcY + stepY * 2 ;
}
} else if ( logFunc ) {
logFunc (
` srcElement . getBoundingClientRect ( ) : $ { rectToString (
srcElement . getBoundingClientRect ( )
) } `
) ;
}
const editingHost = ( ( ) = > {
if ( ! srcElement . matches ( " : read - write " ) ) {
return null ;
}
let lastEditableElement = srcElement ;
for (
let inclusiveAncestor =
_ getInclusiveFlattenedTreeParentElement ( srcElement ) ;
inclusiveAncestor ;
inclusiveAncestor = _ getInclusiveFlattenedTreeParentElement (
_ getFlattenedTreeParentNode ( inclusiveAncestor )
)
) {
if ( inclusiveAncestor . matches ( " : read - write " ) ) {
lastEditableElement = inclusiveAncestor ;
if ( lastEditableElement = = srcElement . ownerDocument . body ) {
break ;
}
}
}
return lastEditableElement ;
} ) ( ) ;
try {
srcWindowUtils . disableNonTestMouseEvents ( true ) ;
await new Promise ( r = > setTimeout ( r , 0 ) ) ;
let mouseDownEvent ;
function onMouseDown ( aEvent ) {
mouseDownEvent = aEvent ;
if ( logFunc ) {
logFunc (
` " $ { aEvent . type } " event is fired on $ {
aEvent . target
} ( composedTarget : $ { _ EU_maybeUnwrap (
_ EU_maybeWrap ( aEvent ) . composedTarget
) } `
) ;
}
if (
! _ nodeIsFlattenedTreeDescendantOf (
_ EU_maybeUnwrap ( _ EU_maybeWrap ( aEvent ) . composedTarget ) ,
srcElement
)
) {
/ / If srcX and srcY does not point in one of rects in srcElement ,
/ / " mousedown " target is not in srcElement . Such case must not
/ / be expected by this API users so that we should throw an exception
/ / for making debugging easier .
throw new Error (
' event target of " mousedown " is not srcElement nor its descendant '
) ;
}
}
try {
srcWindow . addEventListener ( " mousedown " , onMouseDown , { capture : true } ) ;
synthesizeMouse (
srcElement ,
srcX ,
srcY ,
{ type : " mousedown " , id } ,
srcWindow
) ;
if ( logFunc ) {
logFunc ( ` mousedown at $ { srcX } , $ { srcY } ` ) ;
}
if ( ! mouseDownEvent ) {
throw new Error ( ' " mousedown " event is not fired ' ) ;
}
} finally {
srcWindow . removeEventListener ( " mousedown " , onMouseDown , {
capture : true ,
} ) ;
}
let dragStartEvent ;
function onDragStart ( aEvent ) {
dragStartEvent = aEvent ;
if ( logFunc ) {
logFunc ( ` " $ { aEvent . type } " event is fired ` ) ;
}
if (
! _ nodeIsFlattenedTreeDescendantOf (
_ EU_maybeUnwrap ( _ EU_maybeWrap ( aEvent ) . composedTarget ) ,
srcElement
)
) {
/ / If srcX and srcY does not point in one of rects in srcElement ,
/ / " dragstart " target is not in srcElement . Such case must not
/ / be expected by this API users so that we should throw an exception
/ / for making debugging easier .
throw new Error (
' event target of " dragstart " is not srcElement nor its descendant '
) ;
}
}
let dragEnterEvent ;
function onDragEnterGenerated ( aEvent ) {
dragEnterEvent = aEvent ;
}
srcWindow . addEventListener ( " dragstart " , onDragStart , { capture : true } ) ;
srcWindow . addEventListener ( " dragenter " , onDragEnterGenerated , {
capture : true ,
} ) ;
try {
/ / Wait for the next event tick after each event dispatch , so that UI
/ / elements ( e . g . menu ) work like the real user input .
await new Promise ( r = > setTimeout ( r , 0 ) ) ;
srcX + = stepX ;
srcY + = stepY ;
synthesizeMouse (
srcElement ,
srcX ,
srcY ,
{ type : " mousemove " , id } ,
srcWindow
) ;
if ( logFunc ) {
logFunc ( ` first mousemove at $ { srcX } , $ { srcY } ` ) ;
}
await new Promise ( r = > setTimeout ( r , 0 ) ) ;
srcX + = stepX ;
srcY + = stepY ;
synthesizeMouse (
srcElement ,
srcX ,
srcY ,
{ type : " mousemove " , id } ,
srcWindow
) ;
if ( logFunc ) {
logFunc ( ` second mousemove at $ { srcX } , $ { srcY } ` ) ;
}
await new Promise ( r = > setTimeout ( r , 0 ) ) ;
if ( ! dragStartEvent ) {
throw new Error ( ' " dragstart " event is not fired ' ) ;
}
} finally {
srcWindow . removeEventListener ( " dragstart " , onDragStart , {
capture : true ,
} ) ;
srcWindow . removeEventListener ( " dragenter " , onDragEnterGenerated , {
capture : true ,
} ) ;
}
let srcSession = srcWindowUtils . dragSession ;
if ( ! srcSession ) {
if ( expectCancelDragStart ) {
synthesizeMouse (
srcElement ,
finalX ,
finalY ,
{ type : " mouseup " , id } ,
srcWindow
) ;
return ;
}
throw new Error ( " drag hasn ' t been started by the operation " ) ;
} else if ( expectCancelDragStart ) {
throw new Error ( " drag has been started by the operation " ) ;
}
if ( destElement ) {
if (
( srcElement ! = destElement & & ! dragEnterEvent ) | |
destElement ! = dragEnterEvent . target
) {
if ( logFunc ) {
logFunc (
` destElement . getBoundingClientRect ( ) : $ { rectToString (
destElement . getBoundingClientRect ( )
) } `
) ;
}
function onDragEnter ( aEvent ) {
dragEnterEvent = aEvent ;
if ( logFunc ) {
logFunc ( ` " $ { aEvent . type } " event is fired ` ) ;
}
if ( aEvent . target ! = destElement ) {
throw new Error ( ' event target of " dragenter " is not destElement ' ) ;
}
}
destWindow . addEventListener ( " dragenter " , onDragEnter , {
capture : true ,
} ) ;
try {
let event = createDragEventObject (
" dragenter " ,
destElement ,
destWindow ,
null ,
dragEvent
) ;
sendDragEvent ( event , destElement , destWindow ) ;
if ( ! dragEnterEvent & & ! destElement . disabled ) {
throw new Error ( ' " dragenter " event is not fired ' ) ;
}
if ( dragEnterEvent & & destElement . disabled ) {
throw new Error (
' " dragenter " event should not be fired on disable element '
) ;
}
} finally {
destWindow . removeEventListener ( " dragenter " , onDragEnter , {
capture : true ,
} ) ;
}
}
let dragOverEvent ;
function onDragOver ( aEvent ) {
dragOverEvent = aEvent ;
if ( logFunc ) {
logFunc ( ` " $ { aEvent . type } " event is fired ` ) ;
}
if ( aEvent . target ! = destElement ) {
throw new Error ( ' event target of " dragover " is not destElement ' ) ;
}
}
destWindow . addEventListener ( " dragover " , onDragOver , { capture : true } ) ;
try {
/ / dragover and drop are only fired to a valid drop target . If the
/ / destElement parameter is null , this function is being used to
/ / simulate a drag ' n ' drop over an invalid drop target .
let event = createDragEventObject (
" dragover " ,
destElement ,
destWindow ,
null ,
dragEvent
) ;
sendDragEvent ( event , destElement , destWindow ) ;
if ( ! dragOverEvent & & ! destElement . disabled ) {
throw new Error ( ' " dragover " event is not fired ' ) ;
}
if ( dragEnterEvent & & destElement . disabled ) {
throw new Error (
' " dragover " event should not be fired on disable element '
) ;
}
} finally {
destWindow . removeEventListener ( " dragover " , onDragOver , {
capture : true ,
} ) ;
}
await new Promise ( r = > setTimeout ( r , 0 ) ) ;
/ / If there is not accept to drop the data , " drop " event shouldn ' t be
/ / fired .
/ / XXX nsIDragSession . canDrop is different only on Linux . It must be
/ / a bug of gtk / nsDragService since it manages ` mCanDrop ` by itself .
/ / Thus , we should use nsIDragSession . dragAction instead .
let destSession = destWindowUtils . dragSession ;
if (
destSession . dragAction ! = _ EU_Ci . nsIDragService . DRAGDROP_ACTION_NONE
) {
let dropEvent ;
function onDrop ( aEvent ) {
dropEvent = aEvent ;
if ( logFunc ) {
logFunc ( ` " $ { aEvent . type } " event is fired ` ) ;
}
if (
! _ nodeIsFlattenedTreeDescendantOf (
_ EU_maybeUnwrap ( _ EU_maybeWrap ( aEvent ) . composedTarget ) ,
destElement
)
) {
throw new Error (
' event target of " drop " is not destElement nor its descendant '
) ;
}
}
destWindow . addEventListener ( " drop " , onDrop , { capture : true } ) ;
try {
let event = createDragEventObject (
" drop " ,
destElement ,
destWindow ,
null ,
dragEvent
) ;
sendDragEvent ( event , destElement , destWindow ) ;
if ( ! dropEvent & & destSession . canDrop ) {
throw new Error ( ' " drop " event is not fired ' ) ;
}
} finally {
destWindow . removeEventListener ( " drop " , onDrop , { capture : true } ) ;
}
return ;
}
}
/ / Since we don ' t synthesize drop event , we need to set drag end point
/ / explicitly for " dragEnd " event which will be fired by
/ / endDragSession ( ) .
dragEvent . clientX = srcElement . getBoundingClientRect ( ) . x + finalX ;
dragEvent . clientY = srcElement . getBoundingClientRect ( ) . y + finalY ;
let event = createDragEventObject (
" dragend " ,
srcElement ,
srcWindow ,
null ,
dragEvent
) ;
srcSession . setDragEndPointForTests ( event . screenX , event . screenY ) ;
if ( logFunc ) {
logFunc (
` dragend event client ( X , Y ) = ( $ { event . clientX } , $ { event . clientY } ) `
) ;
logFunc (
` dragend event screen ( X , Y ) = ( $ { event . screenX } , $ { event . screenY } ) `
) ;
}
} finally {
await new Promise ( r = > setTimeout ( r , 0 ) ) ;
if ( srcWindowUtils . dragSession ) {
const sourceNode = srcWindowUtils . dragSession . sourceNode ;
let dragEndEvent ;
function onDragEnd ( aEvent ) {
dragEndEvent = aEvent ;
if ( logFunc ) {
logFunc ( ` " $ { aEvent . type } " event is fired ` ) ;
}
if (
! _ nodeIsFlattenedTreeDescendantOf (
_ EU_maybeUnwrap ( _ EU_maybeWrap ( aEvent ) . composedTarget ) ,
srcElement
) & &
_ EU_maybeUnwrap ( _ EU_maybeWrap ( aEvent ) . composedTarget ) ! = editingHost
) {
throw new Error (
' event target of " dragend " is not srcElement nor its descendant '
) ;
}
if ( expectSrcElementDisconnected ) {
throw new Error (
` " dragend " event shouldn ' t be fired when the source node is disconnected ( the source node is $ {
sourceNode ? . isConnected ? " connected " : " null or disconnected "
} ) `
) ;
}
}
srcWindow . addEventListener ( " dragend " , onDragEnd , { capture : true } ) ;
try {
srcWindowUtils . dragSession . endDragSession (
true ,
_ parseModifiers ( dragEvent )
) ;
if ( ! expectSrcElementDisconnected & & ! dragEndEvent ) {
/ / eslint - disable - next - line no - unsafe - finally
throw new Error (
` " dragend " event is not fired by nsIDragSession . endDragSession ( ) $ {
srcWindowUtils . dragSession . sourceNode & &
! srcWindowUtils . dragSession . sourceNode . isConnected
? " ( sourceNode was disconnected ) "
: " "
} `
) ;
}
} finally {
srcWindow . removeEventListener ( " dragend " , onDragEnd , { capture : true } ) ;
}
}
srcWindowUtils . disableNonTestMouseEvents ( false ) ;
if ( logFunc ) {
logFunc ( " synthesizePlainDragAndDrop ( ) - - END " ) ;
}
}
}
function _ checkDataTransferItems ( aDataTransfer , aExpectedDragData ) {
try {
/ / We must wrap only in plain mochitests , not chrome
let dataTransfer = _ EU_maybeWrap ( aDataTransfer ) ;
if ( ! dataTransfer ) {
return null ;
}
if (
aExpectedDragData = = null | |
dataTransfer . mozItemCount ! = aExpectedDragData . length
) {
return dataTransfer ;
}
for ( let i = 0 ; i < dataTransfer . mozItemCount ; i + + ) {
let dtTypes = dataTransfer . mozTypesAt ( i ) ;
if ( dtTypes . length ! = aExpectedDragData [ i ] . length ) {
return dataTransfer ;
}
for ( let j = 0 ; j < dtTypes . length ; j + + ) {
if ( dtTypes [ j ] ! = aExpectedDragData [ i ] [ j ] . type ) {
return dataTransfer ;
}
let dtData = dataTransfer . mozGetDataAt ( dtTypes [ j ] , i ) ;
if ( aExpectedDragData [ i ] [ j ] . eqTest ) {
if (
! aExpectedDragData [ i ] [ j ] . eqTest (
dtData ,
aExpectedDragData [ i ] [ j ] . data
)
) {
return dataTransfer ;
}
} else if ( aExpectedDragData [ i ] [ j ] . data ! = dtData ) {
return dataTransfer ;
}
}
}
} catch ( ex ) {
return ex ;
}
return true ;
}
/ * *
* @ typedef { ( actualData : any , expectedData : any ) - > boolean } eqTest
* This callback type is used with ` ` synthesizePlainDragAndCancel ( ) ` ` .
* It should compare ` ` actualData ` ` and ` ` expectedData ` ` and return
* true if the two should be considered equal , false otherwise .
* /
/ * *
* synthesizePlainDragAndCancel ( ) synthesizes drag start with
* synthesizePlainDragAndDrop ( ) , but always cancel it with preventing default
* of " dragstart " . Additionally , this checks whether the dataTransfer of
* " dragstart " event has only expected items .
*
* @ param { object } aParams
* The params which is set to the argument of ` ` synthesizePlainDragAndDrop ( ) ` ` .
* @ param { Array } aExpectedDataTransferItems
* All expected dataTransfer items .
* This data is in the format :
*
* [
* [
* { " type " : value , " data " : value , " eqTest " : eqTest }
* . . . ,
* ] ,
* . . .
* ]
*
* This can also be null .
* You can optionally provide ` ` eqTest ` ` if the
* comparison to the expected data transfer items can ' t be done
* with x = = y ;
* @ return { boolean }
* true if aExpectedDataTransferItems matches with
* DragEvent . dataTransfer of " dragstart " event .
* Otherwise , the dataTransfer object ( may be null ) or
* thrown exception , NOT false . Therefore , you shouldn ' t
* use .
* /
async function synthesizePlainDragAndCancel (
aParams ,
aExpectedDataTransferItems
) {
let srcElement = aParams . srcSelection
? _ computeSrcElementFromSrcSelection ( aParams . srcSelection )
: aParams . srcElement ;
let result ;
function onDragStart ( aEvent ) {
aEvent . preventDefault ( ) ;
result = _ checkDataTransferItems (
aEvent . dataTransfer ,
aExpectedDataTransferItems
) ;
}
SpecialPowers . wrap ( srcElement . ownerDocument ) . addEventListener (
" dragstart " ,
onDragStart ,
{ capture : true , mozSystemGroup : true }
) ;
try {
aParams . expectCancelDragStart = true ;
await synthesizePlainDragAndDrop ( aParams ) ;
} finally {
SpecialPowers . wrap ( srcElement . ownerDocument ) . removeEventListener (
" dragstart " ,
onDragStart ,
{ capture : true , mozSystemGroup : true }
) ;
}
return result ;
}
async function _ synthesizeMockDndFromChild ( aParams ) {
/ / Since we know that this is the ( only ) content process that will be involved
/ / in the drag , we can set this for the caller .
const ds = SpecialPowers . Cc [ " @ mozilla . org / widget / dragservice ; 1 " ] . getService (
SpecialPowers . Ci . nsIDragService
) ;
ds java.lang.StringIndexOutOfBoundsException: Range [6, 5) out of bounds for length 72
let sourceElt = document . getElementById ( aParams . srcElement ) ;
let targetElt = document . getElementById ( aParams . targetElement ) ;
/ / The spawnChrome call below may return before the DND is complete since
/ / the parent process will not synchronize with the child for child - initiated
/ / drags . So we need to wait for the dragend here . If the drag motion is
/ / expected to not result in a drag session then we wait for the mouseup
/ / instead .
let resolveEndPromise ;
let endPromise = new Promise ( res = > {
resolveEndPromise = res ;
} ) ;
let endEvent = aParams . expectNoDragEvents ? " mouseup " : " dragend " ;
sourceElt . addEventListener (
endEvent ,
( ) = > {
resolveEndPromise ( ) ;
} ,
{ once : true }
) ;
/ / The parent call will not get the element positions , so set ' sourceOffset ' to
/ / the screen coordinates for the drag start and ' targetOffset ' for the screen
/ / position to drag to .
const scale = window . devicePixelRatio ;
let sourceOffset = [
( window . mozInnerScreenX + sourceElt . offsetLeft ) * scale +
aParams . sourceOffset [ 0 ] ,
( window . mozInnerScreenY + sourceElt . offsetTop ) * scale +
aParams . sourceOffset [ 1 ] ,
] ;
let targetOffset = [
( window . mozInnerScreenX + targetElt . offsetLeft ) * scale +
aParams . targetOffset [ 0 ] ,
( window . mozInnerScreenY + targetElt . offsetTop ) * scale +
aParams . targetOffset [ 1 ] ,
] ;
let params = {
srcElement : aParams . srcElement ,
targetElement : aParams . targetElement ,
sourceOffset ,
targetOffset ,
step : aParams . step ,
expectCancelDragStart : aParams . expectCancelDragStart ,
cancel : aParams . cancel ,
expectSrcElementDisconnected : aParams . expectSrcElementDisconnected ,
expectDragLeave : aParams . expectDragLeave ,
expectNoDragEvents : aParams . expectNoDragEvents ,
expectNoDragTargetEvents : aParams . expectNoDragTargetEvents ,
contextLabel : aParams . contextLabel ,
throwOnExtraMessage : aParams . throwOnExtraMessage ,
} ;
let record =
aParams . record | |
( ( cond , msg , _ , stack ) = > {
if ( cond ) {
console . error ( msg + " \ n " + stack ) ;
}
} ) ;
let info = aParams . info | | console . log ;
try {
await SpecialPowers . spawnChrome ( [ params ] , async _ params = > {
let params = {
sourceBrowsingCxt : browsingContext ,
targetBrowsingCxt : browsingContext ,
record : ( ) = > { } ,
info : ( ) = > { } ,
. . . _ params ,
} ;
await EventUtils . synthesizeMockDragAndDrop ( params ) ;
} ) ;
await endPromise ;
} catch ( ex ) {
/ / Display any exceptions since EventUtils does not propagate them .
record (
true ,
` Parent synthesizeMockDragAndDrop threw exception : $ { ex } ` ,
null ,
ex . stack
) ;
} finally {
info ( " Remote synthesizeMockDragAndDrop has completed . " ) ;
}
}
/ * *
* Emulate a drag and drop by generating a dragstart from mousedown and mousemove ,
* then firing events dragover and drop ( or dragleave if expectDragLeave is set ) .
* This does not modify dataTransfer and tries to emulate the plain drag and
* drop as much as possible , compared to synthesizeDrop and
* synthesizePlainDragAndDrop . MockDragService is used in place of the native
* nsIDragService implementation . All coordinates are in client space .
*
* This method can be called from the parent process , in which case it will
* perform checks of DND internals ( if ' record ' is set ) . It can also be
* called from content processes , in which case the drag is over the window
* that is in context , and no checks of DND internals will occur .
*
* @ param { object } aParams
* @ param { Window } aParams . sourceBrowsingCxt
* The BrowsingContext ( possibly remote ) that contains
* srcElement . Only set in parent process .
* @ param { Window } aParams . targetBrowsingCxt
* The BrowsingContext ( possibly remote ) that contains
* targetElement . Only set in parent process .
* Default is sourceBrowsingCxt .
* @ param { Element } aParams . srcElement
* ID of the element to drag .
* @ param { Element | null } aParams . targetElement
* ID of the element to drop on .
* @ param { number } aParams . sourceOffset
* The 2 D offset from the source element at which the drag
* starts . Default is [ 0 , 0 ] .
* @ param { number } aParams . targetOffset
* The 2 D offset from the target element at which the drag ends .
* Default is [ 0 , 0 ] .
* @ param { number } aParams . step
* The 2 D step for intermediate dragging mousemoves .
* Default is [ 5 , 5 ] .
* @ param { boolean } aParams . expectCancelDragStart
* Set to true if srcElement is set up to cancel " dragstart "
* @ param { number } aParams . cancel
* The 2 D coord the mouse is moved to as the last step if
* expectCancelDragStart is set
* @ param { boolean } aParams . expectSrcElementDisconnected
* Set to true if srcElement will be disconnected and
* " dragend " event won ' t be fired .
* @ param { boolean } aParams . expectDragLeave
* Set to true if the drop event will be converted to a
* dragleave before it is sent ( e . g . it was rejected by a
* content analysis check ) .
* @ param { boolean } aParams . expectNoDragEvents
* Set to true if no mouse or drag events should be received
* on the source or target .
* @ param { boolean } aParams . expectNoDragTargetEvents
* Set to true if the drag should be blocked from sending
* events to the target .
* @ param { boolean } aParams . dropPromise
* A promise that the caller will resolve before we check
* that the drop has happened . Default is a pre - resolved
* promise .
* @ param { string } aParms . contextLabel
* Label that will appear in each output message . Useful to
* distinguish between concurrent calls . Default is none .
* @ param { boolean } aParams . throwOnExtraMessage
* Throw an exception in child process when an unexpected
* event is received . Used for debugging . Default is false .
* @ param { Function } aParams . record
* Four - parameter function that logs the results of a remote
* assertion . The parameters are ( condition , message , ignored ,
* stack ) . This is the type of the mochitest report function .
* Pass the empty function , or call this from content , to skip
* testing of DND internals .
* This parameter is required in the parent process and is
* optional in content processes .
* @ param { Function } aParams . info
* One - parameter info logging function . This is the type of
* the mochitest info function . Pass the empty function , or
* call this from content , to skip testing of DND internals .
* This parameter is required in the parent process and is
* optional in content processes .
* @ param { object } aParams . dragController
* MockDragController that the function should use . This
* function will automatically generate one if none is given .
* This can only be set in the parent process .
* /
/ / eslint - disable - next - line complexity
async function synthesizeMockDragAndDrop ( aParams ) {
/ / eslint - disable - next - line mozilla / use - services
let appinfo = _ EU_Cc [ " @ mozilla . org / xre / app - info ; 1 " ] . getService (
_ EU_Ci . nsIXULRuntime
) ;
if ( appinfo . processType ! = = appinfo . PROCESS_TYPE_DEFAULT ) {
await _ synthesizeMockDndFromChild ( aParams ) ;
return ;
}
const {
srcElement ,
targetElement ,
sourceOffset = [ 0 , 0 ] ,
targetOffset = [ 0 , 0 ] ,
step = [ 5 , 5 ] ,
cancel = [ 0 , 0 ] ,
sourceBrowsingCxt ,
targetBrowsingCxt = sourceBrowsingCxt ,
expectCancelDragStart = false ,
expectSrcElementDisconnected = false ,
expectDragLeave = false ,
expectNoDragEvents = false ,
dropPromise = Promise . resolve ( undefined ) ,
contextLabel = " " ,
throwOnExtraMessage = false ,
} = aParams ;
let { dragController = null , expectNoDragTargetEvents = false } = aParams ;
/ / Configure test reporting functions
const prefix = contextLabel ? ` [ $ { contextLabel } ] | ` : " " ;
const info = msg = > {
aParams . info ( ` $ { prefix } $ { msg } ` ) ;
} ;
const record = ( cond , msg , _ , stack ) = > {
aParams . record ( cond , ` $ { prefix } $ { msg } ` , null , stack ) ;
} ;
const ok = ( cond , msg ) = > {
record ( cond , msg , null , Components . stack . caller ) ;
} ;
info ( " synthesizeMockDragAndDrop ( ) - - START " ) ;
/ / Validate parameters
ok ( sourceBrowsingCxt , " sourceBrowsingCxt was given " ) ;
ok (
sourceBrowsingCxt ! = targetBrowsingCxt | | srcElement ! = targetElement ,
" sourceBrowsingCxt + Element cannot be the same as targetBrowsingCxt + Element "
) ;
/ / no drag implies no drag target
expectNoDragTargetEvents | = expectNoDragEvents ;
/ / Returns true if one browsing context is an ancestor of the other .
let browsingContextsAreRelated = function ( cxt1 , cxt2 ) {
return cxt1 . top = = cxt2 . top ;
} ;
/ / The rules for accessing the dataTransfer from internal drags in Gecko
/ / during drag event handlers are as follows :
/ /
/ / dragstart :
/ / Always grants read - write access
/ / dragenter / dragover / dragleave :
/ / If dom . events . dataTransfer . protected . enabled is set :
/ / Read - only permission is granted if any of these holds :
/ / * The drag target ' s browsing context is the same as the drag
/ / source ' s ( e . g . dragging inside of one frame on a web page ) .
/ / * The drag source and target are the same domain / principal and
/ / one has a browsing context that is an ancestor of the other
/ / ( e . g . one is an iframe nested inside of the other ) .
/ / * The principal of the drag target element is privileged ( not
/ / a content principal ) .
/ / Otherwise :
/ / Permission is never granted
/ / drop :
/ / Always grants read - only permission
/ / dragend :
/ / Read - only permission is granted if
/ / dom . events . dataTransfer . protected . enabled is set .
/ /
/ / dragstart and dragend are special because they target the drag - source ,
/ / not the drag - target .
/ / eslint - disable - next - line mozilla / use - services
let prefs = _ EU_Cc [ " @ mozilla . org / preferences - service ; 1 " ] . getService (
Ci . nsIPrefBranch
) ;
let expectProtectedDataTransferAccessSource = ! prefs . getBoolPref (
" dom . events . dataTransfer . protected . enabled "
) ;
let expectProtectedDataTransferAccessTarget =
expectProtectedDataTransferAccessSource & &
browsingContextsAreRelated ( targetBrowsingCxt , sourceBrowsingCxt ) ;
info (
` expectProtectedDataTransferAccessSource : $ { expectProtectedDataTransferAccessSource } `
) ;
info (
` expectProtectedDataTransferAccessTarget : $ { expectProtectedDataTransferAccessTarget } `
) ;
/ / Essentially the entire function is in a try block so that we can make sure
/ / that the mock drag service is removed and non - test mouse events are
/ / restored .
const { MockRegistrar } = ChromeUtils . importESModule (
" resource : / / testing - common / MockRegistrar . sys . mjs "
) ;
let dragServiceCid ;
let sourceCxt ;
let targetCxt ;
let srcWindowUtils = _ getDOMWindowUtils ( sourceBrowsingCxt . documentGlobal ) ;
let targetWindowUtils = _ getDOMWindowUtils ( targetBrowsingCxt . documentGlobal ) ;
try {
/ / Disable native mouse events to avoid external interference while the test
/ / runs . One call disables for all windows .
if ( srcWindowUtils ) {
srcWindowUtils . disableNonTestMouseEvents ( true ) ;
}
if ( targetWindowUtils ) {
targetWindowUtils . disableNonTestMouseEvents ( true ) ;
}
/ / Install mock drag service .
if ( ! dragController ) {
info ( " No dragController was given so creating mock drag service " ) ;
const oldDragService = _ EU_Cc [
" @ mozilla . org / widget / dragservice ; 1 "
] . getService ( _ EU_Ci . nsIDragService ) ;
dragController = oldDragService . getMockDragController ( ) ;
dragServiceCid = MockRegistrar . register (
" @ mozilla . org / widget / dragservice ; 1 " ,
dragController . mockDragService
) ;
ok ( dragServiceCid , " MockDragService was registered " ) ;
/ / If the mock failed then don ' t continue or else we will trigger native
/ / DND behavior .
if ( ! dragServiceCid ) {
throw new Error ( " MockDragService failed to register " ) ;
}
}
const mockDragService = _ EU_Cc [
" @ mozilla . org / widget / dragservice ; 1 "
] . getService ( _ EU_Ci . nsIDragService ) ;
let runChecks = globalThis . hasOwnProperty ( " SpecialPowers " ) ;
if ( runChecks ) {
/ / Variables that are added to the child actor objects .
const srcVars = {
expectCancelDragStart ,
expectSrcElementDisconnected ,
expectNoDragEvents ,
expectProtectedDataTransferAccess :
expectProtectedDataTransferAccessSource ,
dragElementId : srcElement ,
} ;
const targetVars = {
expectDragLeave ,
expectNoDragTargetEvents ,
expectProtectedDataTransferAccess :
expectProtectedDataTransferAccessTarget ,
dragElementId : targetElement ,
} ;
const bothVars = {
contextLabel ,
throwOnExtraMessage ,
relevantEvents : [
" mousedown " ,
" mouseup " ,
" dragstart " ,
" dragenter " ,
" dragover " ,
" drop " ,
" dragleave " ,
" dragend " ,
] ,
} ;
const makeDragSourceContext = async ( aBC , aRemoteVars ) = > {
let { DragSourceParentContext } = ChromeUtils . importESModule (
" chrome : / / mochikit / content / tests / SimpleTest / DragSourceParentContext . sys . mjs "
) ;
let ret = new DragSourceParentContext ( aBC , aRemoteVars , SpecialPowers ) ;
await ret . initialize ( ) ;
return ret ;
} ;
const makeDragTargetContext = async ( aBC , aRemoteVars ) = > {
let { DragTargetParentContext } = ChromeUtils . importESModule (
" chrome : / / mochikit / content / tests / SimpleTest / DragTargetParentContext . sys . mjs "
) ;
let ret = new DragTargetParentContext ( aBC , aRemoteVars , SpecialPowers ) ;
await ret . initialize ( ) ;
return ret ;
} ;
[ sourceCxt , targetCxt ] = await Promise . all ( [
makeDragSourceContext ( sourceBrowsingCxt , { . . . srcVars , . . . bothVars } ) ,
makeDragTargetContext ( targetBrowsingCxt , {
. . . targetVars ,
. . . bothVars ,
} ) ,
] ) ;
} else {
/ / We don ' t have SpecialPowers so we cannot perform DND checks . Make
/ / them empty functions .
info ( " synthesizeMockDragAndDrop will skip DND checks " ) ;
let dragParentBaseCxt = {
expect : ( ) = > { } ,
checkExpected : ( ) = > { } ,
checkHasDrag : ( ) = > { } ,
checkSessionHasAction : ( ) = > { } ,
synchronize : ( ) = > { } ,
cleanup : ( ) = > { } ,
} ;
sourceCxt = {
getElementPositions : ( ) = > {
return { screenPos : [ 0 , 0 ] } ;
} ,
checkMouseDown : ( ) = > { } ,
checkDragStart : ( ) = > { } ,
checkDragEnd : ( ) = > { } ,
. . . dragParentBaseCxt ,
} ;
targetCxt = {
getElementPositions : ( ) = > {
return { screenPos : [ 0 , 0 ] } ;
} ,
checkDropOrDragLeave : ( ) = > { } ,
. . . dragParentBaseCxt ,
} ;
}
/ / Get element positions in screen coords
let add2d = ( a , b ) = > {
return [ a [ 0 ] + b [ 0 ] , a [ 1 ] + b [ 1 ] ] ;
} ;
let srcPos = add2d (
( await sourceCxt . getElementPositions ( ) ) . screenPos ,
sourceOffset
) ;
let targetPos = add2d (
( await targetCxt . getElementPositions ( ) ) . screenPos ,
targetOffset
) ;
info ( ` screenSrcPos : $ { srcPos } | screenTargetPos : $ { targetPos } ` ) ;
/ / Send and verify the mousedown on src .
if ( ! expectNoDragEvents ) {
sourceCxt . expect ( " mousedown " ) ;
}
/ / Take ceiling of ccoordinates to make sure that the integer coordinates
/ / are over the element .
let currentSrcScreenPos = [ Math . ceil ( srcPos [ 0 ] ) , Math . ceil ( srcPos [ 1 ] ) ] ;
info (
` sending mousedown at $ { currentSrcScreenPos [ 0 ] } , $ { currentSrcScreenPos [ 1 ] } `
) ;
dragController . sendEvent (
sourceBrowsingCxt ,
Ci . nsIMockDragServiceController . eMouseDown ,
currentSrcScreenPos [ 0 ] ,
currentSrcScreenPos [ 1 ]
) ;
info ( ` mousedown sent ` ) ;
await sourceCxt . synchronize ( ) ;
await sourceCxt . checkMouseDown ( ) ;
let contentInvokedDragPromise ;
info ( " setting up content - invoked - drag observer and expecting dragstart " ) ;
if ( ! expectNoDragEvents ) {
sourceCxt . expect ( " dragstart " ) ;
/ / Set up observable for content - invoked - drag , which is sent when the
/ / parent learns that content has begun a drag session .
contentInvokedDragPromise = new Promise ( cb = > {
Services . obs . addObserver ( function observe ( ) {
info ( " content - invoked - drag observer received message " ) ;
Services . obs . removeObserver ( observe , " content - invoked - drag " ) ;
cb ( ) ;
} , " content - invoked - drag " ) ;
} ) ;
}
/ / It takes two mouse - moves to initiate a drag session .
currentSrcScreenPos = [
currentSrcScreenPos [ 0 ] + step [ 0 ] ,
currentSrcScreenPos [ 1 ] + step [ 1 ] ,
] ;
info (
` first mousemove at $ { currentSrcScreenPos [ 0 ] } , $ { currentSrcScreenPos [ 1 ] } `
) ;
dragController . sendEvent (
sourceBrowsingCxt ,
Ci . nsIMockDragServiceController . eMouseMove ,
currentSrcScreenPos [ 0 ] ,
currentSrcScreenPos [ 1 ]
) ;
info ( ` first mousemove sent ` ) ;
currentSrcScreenPos = [
currentSrcScreenPos [ 0 ] + step [ 0 ] ,
currentSrcScreenPos [ 1 ] + step [ 1 ] ,
] ;
info (
` second mousemove at $ { currentSrcScreenPos [ 0 ] } , $ { currentSrcScreenPos [ 1 ] } `
) ;
dragController . sendEvent (
sourceBrowsingCxt ,
Ci . nsIMockDragServiceController . eMouseMove ,
currentSrcScreenPos [ 0 ] ,
currentSrcScreenPos [ 1 ]
) ;
info ( ` second mousemove sent ` ) ;
if ( ! expectNoDragEvents ) {
info ( " waiting for content - invoked - drag observable " ) ;
await contentInvokedDragPromise ;
ok ( true , " content - invoked - drag was received " ) ;
}
info ( " checking dragstart " ) ;
await sourceCxt . checkDragStart ( ) ;
if ( expectNoDragEvents ) {
ok (
! mockDragService . getCurrentSession ( ) ,
" Drag was properly blocked from starting . "
) ;
dragController . sendEvent (
sourceBrowsingCxt ,
Ci . nsIMockDragServiceController . eMouseUp ,
cancel [ 0 ] ,
cancel [ 1 ]
) ;
return ;
}
/ / Another move creates the drag session in the parent process ( but we need
/ / to wait for the src process to get there ) .
currentSrcScreenPos = [
currentSrcScreenPos [ 0 ] + step [ 0 ] ,
currentSrcScreenPos [ 1 ] + step [ 1 ] ,
] ;
info (
` third mousemove at $ { currentSrcScreenPos [ 0 ] } , $ { currentSrcScreenPos [ 1 ] } `
) ;
dragController . sendEvent (
sourceBrowsingCxt ,
Ci . nsIMockDragServiceController . eMouseMove ,
currentSrcScreenPos [ 0 ] ,
currentSrcScreenPos [ 1 ]
) ;
info ( ` third mousemove sent ` ) ;
ok ( mockDragService . getCurrentSession ( ) , ` Parent process has drag session . ` ) ;
if ( expectCancelDragStart ) {
dragController . sendEvent (
sourceBrowsingCxt ,
Ci . nsIMockDragServiceController . eMouseUp ,
cancel [ 0 ] ,
cancel [ 1 ]
) ;
return ;
}
await sourceCxt . checkExpected ( ) ;
/ / Implementation detail : EventStateManager : : GenerateDragDropEnterExit
/ / expects the source to get at least one dragover before leaving the
/ / widget or else it fails to send dragenter / dragleave events to the
/ / browsers .
info ( " synthesizing dragover inside source " ) ;
sourceCxt . expect ( " dragenter " ) ;
sourceCxt . expect ( " dragover " ) ;
currentSrcScreenPos = [
currentSrcScreenPos [ 0 ] + step [ 0 ] ,
currentSrcScreenPos [ 1 ] + step [ 1 ] ,
] ;
info ( ` dragover at $ { currentSrcScreenPos [ 0 ] } , $ { currentSrcScreenPos [ 1 ] } ` ) ;
dragController . sendEvent (
sourceBrowsingCxt ,
Ci . nsIMockDragServiceController . eDragOver ,
currentSrcScreenPos [ 0 ] ,
currentSrcScreenPos [ 1 ]
) ;
info ( ` dragover sent ` ) ;
await sourceCxt . checkExpected ( ) ;
let currentTargetScreenPos = [
Math . ceil ( targetPos [ 0 ] ) ,
Math . ceil ( targetPos [ 1 ] ) ,
] ;
/ / The next step is to drag to the target element .
if ( ! expectNoDragTargetEvents ) {
sourceCxt . expect ( " dragleave " ) ;
}
if (
sourceBrowsingCxt . top . embedderElement ! = =
targetBrowsingCxt . top . embedderElement
) {
/ / Send dragexit and dragenter only if we are dragging to another widget .
/ / If we are dragging in the same widget then dragenter does not involve
/ / the parent process . This mirrors the native behavior . In the
/ / widget - to - widget case , the source gets the dragexit immediately but
/ / the target won ' t get a dragenter in content until we send a dragover - -
/ / this is because dragenters are generated by the EventStateManager and
/ / are not forwarded remotely .
/ / NB : dragleaves are synthesized by Gecko from dragexits .
info ( " synthesizing dragexit and dragenter to enter new widget " ) ;
if ( ! expectNoDragTargetEvents ) {
info ( " This will generate dragleave on the source " ) ;
}
dragController . sendEvent (
sourceBrowsingCxt ,
Ci . nsIMockDragServiceController . eDragExit ,
currentTargetScreenPos [ 0 ] ,
currentTargetScreenPos [ 1 ]
) ;
dragController . sendEvent (
targetBrowsingCxt ,
Ci . nsIMockDragServiceController . eDragEnter ,
currentTargetScreenPos [ 0 ] ,
currentTargetScreenPos [ 1 ]
) ;
await sourceCxt . synchronize ( ) ;
await sourceCxt . checkExpected ( ) ;
await targetCxt . checkExpected ( ) ;
}
info (
" Synthesizing dragover over target . This will first generate a dragenter . "
) ;
if ( ! expectNoDragTargetEvents ) {
targetCxt . expect ( " dragenter " ) ;
targetCxt . expect ( " dragover " ) ;
}
dragController . sendEvent (
targetBrowsingCxt ,
Ci . nsIMockDragServiceController . eDragOver ,
currentTargetScreenPos [ 0 ] ,
currentTargetScreenPos [ 1 ]
) ;
await targetCxt . checkExpected ( ) ;
let expectedMessage = expectDragLeave ? " dragleave " : " drop " ;
if ( expectNoDragTargetEvents ) {
await targetCxt . checkHasDrag ( false ) ;
} else {
await targetCxt . checkSessionHasAction ( ) ;
targetCxt . expect ( expectedMessage ) ;
}
if ( ! expectSrcElementDisconnected ) {
await sourceCxt . checkHasDrag ( true ) ;
sourceCxt . expect ( " dragend " ) ;
}
info (
` issuing drop event that should be ` +
` $ {
! expectNoDragTargetEvents
? ` received as a $ { expectedMessage } event `
: " ignored "
} , followed by a dragend event `
) ;
currentTargetScreenPos = [
currentTargetScreenPos [ 0 ] + step [ 0 ] ,
currentTargetScreenPos [ 1 ] + step [ 1 ] ,
] ;
dragController . sendEvent (
targetBrowsingCxt ,
Ci . nsIMockDragServiceController . eDrop ,
currentTargetScreenPos [ 0 ] ,
currentTargetScreenPos [ 1 ]
) ;
/ / Wait for any caller - supplied dropPromise before continuing .
await dropPromise ;
if ( ! expectNoDragTargetEvents ) {
await targetCxt . checkDropOrDragLeave ( ) ;
} else {
await targetCxt . checkExpected ( ) ;
}
if ( ! expectSrcElementDisconnected ) {
await sourceCxt . checkDragEnd ( ) ;
} else {
await sourceCxt . checkExpected ( ) ;
}
ok (
! mockDragService . getCurrentSession ( ) ,
` Parent process does not have a drag session . `
) ;
} catch ( e ) {
/ / Any exception is a test failure .
record ( false , e . toString ( ) , null , e . stack ) ;
throw e ;
} finally {
if ( sourceCxt ) {
await sourceCxt . cleanup ( ) ;
}
if ( targetCxt ) {
await targetCxt . cleanup ( ) ;
}
if ( dragServiceCid ) {
MockRegistrar . unregister ( dragServiceCid ) ;
}
if ( srcWindowUtils ) {
srcWindowUtils . disableNonTestMouseEvents ( false ) ;
}
if ( targetWindowUtils ) {
targetWindowUtils . disableNonTestMouseEvents ( false ) ;
}
info ( " synthesizeMockDragAndDrop ( ) - - END " ) ;
}
}
class EventCounter {
constructor ( aTarget , aType , aOptions = { } ) {
this . target = aTarget ;
this . type = aType ;
this . options = aOptions ;
this . eventCount = 0 ;
/ / Bug 1512817 :
/ / SpecialPowers is picky and needs to be passed an explicit reference to
/ / the function to be called . To avoid having to bind " this " , we therefore
/ / define the method this way , via a property .
this . handleEvent = ( ) = > {
this . eventCount + + ;
} ;
SpecialPowers . wrap ( aTarget ) . addEventListener (
aType ,
this . handleEvent ,
aOptions
) ;
}
unregister ( ) {
SpecialPowers . wrap ( this . target ) . removeEventListener (
this . type ,
this . handleEvent ,
this . options
) ;
}
get count ( ) {
return this . eventCount ;
}
}
Messung V0.5 in Prozent C=99 H=96 G=97
¤ Dauer der Verarbeitung: 0.481 Sekunden
¤
*© Formatika GbR, Deutschland