/* This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ /** * EventUtils provides some utility methods for creating and sending DOM events. * Current methods: * sendMouseEvent * sendChar * sendString * sendKey * synthesizeMouse * synthesizeMouseAtCenter * synthesizeMouseScroll * synthesizeKey * synthesizeMouseExpectEvent * synthesizeKeyExpectEvent * * When adding methods to this file, please add a performance test for it. */ /** * 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. * E.g. to send an click event to the node with id 'node' you might do this: * * sendMouseEvent({type:'click'}, 'node'); */ function getElement(id) { return ((typeof(id) == "string") ? document.getElementById(id) : id); }; this.$ = this.getElement; const KeyEvent = Components.interfaces.nsIDOMKeyEvent; function sendMouseEvent(aEvent, aTarget, aWindow) { if (['click', 'dblclick', 'mousedown', 'mouseup', 'mouseover', 'mouseout'].indexOf(aEvent.type) == -1) { throw new Error("sendMouseEvent doesn't know about event type '" + aEvent.type + "'"); } if (!aWindow) { aWindow = window; } if (!(aTarget instanceof Element)) { aTarget = aWindow.document.getElementById(aTarget); } var event = aWindow.document.createEvent('MouseEvent'); var typeArg = aEvent.type; var canBubbleArg = true; var cancelableArg = true; var viewArg = aWindow; var detailArg = aEvent.detail || (aEvent.type == 'click' || aEvent.type == 'mousedown' || aEvent.type == 'mouseup' ? 1 : aEvent.type == 'dblclick'? 2 : 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 = aEvent.button || 0; var relatedTargetArg = aEvent.relatedTarget || null; event.initMouseEvent(typeArg, canBubbleArg, cancelableArg, viewArg, detailArg, screenXArg, screenYArg, clientXArg, clientYArg, ctrlKeyArg, altKeyArg, shiftKeyArg, metaKeyArg, buttonArg, relatedTargetArg); //removed: SpecialPowers.dispatchEvent(aWindow, 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 English letters (lower and upper case) * and the digits 0-9. */ function sendChar(aChar, aWindow) { // DOM event charcodes match ASCII (JS charcodes) for a-zA-Z0-9. var hasShift = (aChar == aChar.toUpperCase()); synthesizeKey(aChar, { shiftKey: hasShift }, aWindow); } /** * Send the string aStr to the focused element. * * For now this method only works for English letters (lower and upper case) * and the digits 0-9. */ function sendString(aStr, aWindow) { for (var i = 0; i < aStr.length; ++i) { 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) { const masks = Components.interfaces.nsIDOMNSEvent; var mval = 0; if (aEvent.shiftKey) mval |= masks.SHIFT_MASK; if (aEvent.ctrlKey) mval |= masks.CONTROL_MASK; if (aEvent.altKey) mval |= masks.ALT_MASK; if (aEvent.metaKey) mval |= masks.META_MASK; if (aEvent.accelKey) mval |= (navigator.platform.indexOf("Mac") >= 0) ? masks.META_MASK : masks.CONTROL_MASK; return mval; } /** * Synthesize a mouse event on a target. The actual client point is determined * by taking the aTarget's client box and offseting it by aOffsetX and * aOffsetY. This allows mouse clicks to be simulated by calling this method. * * aEvent is an object which may contain the properties: * shiftKey, ctrlKey, altKey, metaKey, accessKey, clickCount, button, type * * If the type is specified, an mouse event of that type is fired. Otherwise, * a mousedown followed by a mouse up is performed. * * aWindow is optional, and defaults to the current window object. */ function synthesizeMouse(aTarget, aOffsetX, aOffsetY, aEvent, aWindow) { var rect = aTarget.getBoundingClientRect(); synthesizeMouseAtPoint(rect.left + aOffsetX, rect.top + aOffsetY, aEvent, aWindow); } /* * Synthesize a mouse event at a particular point in aWindow. * * aEvent is an object which may contain the properties: * shiftKey, ctrlKey, altKey, metaKey, accessKey, clickCount, button, type * * If the type is specified, an mouse event of that type is fired. Otherwise, * a mousedown followed by a mouse up is performed. * * aWindow is optional, and defaults to the current window object. */ function synthesizeMouseAtPoint(left, top, aEvent, aWindow) { var utils = _getDOMWindowUtils(aWindow); if (utils) { var button = aEvent.button || 0; var clickCount = aEvent.clickCount || 1; var modifiers = _parseModifiers(aEvent); if (("type" in aEvent) && aEvent.type) { utils.sendMouseEvent(aEvent.type, left, top, button, clickCount, modifiers); } else { utils.sendMouseEvent("mousedown", left, top, button, clickCount, modifiers); utils.sendMouseEvent("mouseup", left, top, button, clickCount, modifiers); } } } // Call synthesizeMouse with coordinates at the center of aTarget. function synthesizeMouseAtCenter(aTarget, aEvent, aWindow) { var rect = aTarget.getBoundingClientRect(); synthesizeMouse(aTarget, rect.width / 2, rect.height / 2, aEvent, aWindow); } /** * Synthesize a mouse scroll event on a target. The actual client point is determined * by taking the aTarget's client box and offseting it by aOffsetX and * aOffsetY. * * aEvent is an object which may contain the properties: * shiftKey, ctrlKey, altKey, metaKey, accessKey, button, type, axis, delta, hasPixels * * If the type is specified, a mouse scroll event of that type is fired. Otherwise, * "DOMMouseScroll" is used. * * If the axis is specified, it must be one of "horizontal" or "vertical". If not specified, * "vertical" is used. * * 'delta' is the amount to scroll by (can be positive or negative). It must * be specified. * * 'hasPixels' specifies whether kHasPixels should be set in the scrollFlags. * * 'isMomentum' specifies whether kIsMomentum should be set in the scrollFlags. * * aWindow is optional, and defaults to the current window object. */ function synthesizeMouseScroll(aTarget, aOffsetX, aOffsetY, aEvent, aWindow) { var utils = _getDOMWindowUtils(aWindow); if (utils) { // See nsMouseScrollFlags in nsGUIEvent.h const kIsVertical = 0x02; const kIsHorizontal = 0x04; const kHasPixels = 0x08; const kIsMomentum = 0x40; var button = aEvent.button || 0; var modifiers = _parseModifiers(aEvent); var rect = aTarget.getBoundingClientRect(); var left = rect.left; var top = rect.top; var type = (("type" in aEvent) && aEvent.type) || "DOMMouseScroll"; var axis = aEvent.axis || "vertical"; var scrollFlags = (axis == "horizontal") ? kIsHorizontal : kIsVertical; if (aEvent.hasPixels) { scrollFlags |= kHasPixels; } if (aEvent.isMomentum) { scrollFlags |= kIsMomentum; } utils.sendMouseScrollEvent(type, left + aOffsetX, top + aOffsetY, button, scrollFlags, aEvent.delta, modifiers); } } function _computeKeyCodeFromChar(aChar) { if (aChar.length != 1) { return 0; } const nsIDOMKeyEvent = Components.interfaces.nsIDOMKeyEvent; if (aChar >= 'a' && aChar <= 'z') { return nsIDOMKeyEvent.DOM_VK_A + aChar.charCodeAt(0) - 'a'.charCodeAt(0); } if (aChar >= 'A' && aChar <= 'Z') { return nsIDOMKeyEvent.DOM_VK_A + aChar.charCodeAt(0) - 'A'.charCodeAt(0); } if (aChar >= '0' && aChar <= '9') { return nsIDOMKeyEvent.DOM_VK_0 + aChar.charCodeAt(0) - '0'.charCodeAt(0); } // returns US keyboard layout's keycode switch (aChar) { case '~': case '`': return nsIDOMKeyEvent.DOM_VK_BACK_QUOTE; case '!': return nsIDOMKeyEvent.DOM_VK_1; case '@': return nsIDOMKeyEvent.DOM_VK_2; case '#': return nsIDOMKeyEvent.DOM_VK_3; case '$': return nsIDOMKeyEvent.DOM_VK_4; case '%': return nsIDOMKeyEvent.DOM_VK_5; case '^': return nsIDOMKeyEvent.DOM_VK_6; case '&': return nsIDOMKeyEvent.DOM_VK_7; case '*': return nsIDOMKeyEvent.DOM_VK_8; case '(': return nsIDOMKeyEvent.DOM_VK_9; case ')': return nsIDOMKeyEvent.DOM_VK_0; case '-': case '_': return nsIDOMKeyEvent.DOM_VK_SUBTRACT; case '+': case '=': return nsIDOMKeyEvent.DOM_VK_EQUALS; case '{': case '[': return nsIDOMKeyEvent.DOM_VK_OPEN_BRACKET; case '}': case ']': return nsIDOMKeyEvent.DOM_VK_CLOSE_BRACKET; case '|': case '\\': return nsIDOMKeyEvent.DOM_VK_BACK_SLASH; case ':': case ';': return nsIDOMKeyEvent.DOM_VK_SEMICOLON; case '\'': case '"': return nsIDOMKeyEvent.DOM_VK_QUOTE; case '<': case ',': return nsIDOMKeyEvent.DOM_VK_COMMA; case '>': case '.': return nsIDOMKeyEvent.DOM_VK_PERIOD; case '?': case '/': return nsIDOMKeyEvent.DOM_VK_SLASH; case '\n': return nsIDOMKeyEvent.DOM_VK_RETURN; default: return 0; } } /** * isKeypressFiredKey() returns TRUE if the given key should cause keypress * event when widget handles the native key event. Otherwise, FALSE. * * aDOMKeyCode should be one of consts of nsIDOMKeyEvent::DOM_VK_*, or a key * name begins with "VK_", or a character. */ function isKeypressFiredKey(aDOMKeyCode) { const KeyEvent = Components.interfaces.nsIDOMKeyEvent; if (typeof(aDOMKeyCode) == "string") { if (aDOMKeyCode.indexOf("VK_") == 0) { aDOMKeyCode = KeyEvent["DOM_" + aDOMKeyCode]; if (!aDOMKeyCode) { throw "Unknown key: " + aDOMKeyCode; } } else { // If the key generates a character, it must cause a keypress event. return true; } } switch (aDOMKeyCode) { case KeyEvent.DOM_VK_SHIFT: case KeyEvent.DOM_VK_CONTROL: case KeyEvent.DOM_VK_ALT: case KeyEvent.DOM_VK_CAPS_LOCK: case KeyEvent.DOM_VK_NUM_LOCK: case KeyEvent.DOM_VK_SCROLL_LOCK: case KeyEvent.DOM_VK_META: return false; default: return true; } } /** * Synthesize a key event. It is targeted at whatever would be targeted by an * actual keypress by the user, typically the focused element. * * aKey should be either a character or a keycode starting with VK_ such as * VK_RETURN. * * aEvent is an object which may contain the properties: * shiftKey, ctrlKey, altKey, metaKey, accessKey, type * * If the type is specified, a key event of that type is fired. Otherwise, * a keydown, a keypress and then a keyup event are fired in sequence. * * aWindow is optional, and defaults to the current window object. */ function synthesizeKey(aKey, aEvent, aWindow) { var utils = _getDOMWindowUtils(aWindow); if (utils) { var keyCode = 0, charCode = 0; if (aKey.indexOf("VK_") == 0) { keyCode = KeyEvent["DOM_" + aKey]; if (!keyCode) { throw "Unknown key: " + aKey; } } else { charCode = aKey.charCodeAt(0); keyCode = _computeKeyCodeFromChar(aKey.charAt(0)); } var modifiers = _parseModifiers(aEvent); if (!("type" in aEvent) || !aEvent.type) { // Send keydown + (optional) keypress + keyup events. var keyDownDefaultHappened = utils.sendKeyEvent("keydown", keyCode, 0, modifiers); if (isKeypressFiredKey(keyCode)) { utils.sendKeyEvent("keypress", charCode ? 0 : keyCode, charCode, modifiers, !keyDownDefaultHappened); } utils.sendKeyEvent("keyup", keyCode, 0, modifiers); } else if (aEvent.type == "keypress") { // Send standalone keypress event. utils.sendKeyEvent(aEvent.type, charCode ? 0 : keyCode, charCode, modifiers); } else { // Send other standalone event than keypress. utils.sendKeyEvent(aEvent.type, keyCode, 0, modifiers); } } } 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, false); 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, false); 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) { if (!aWindow) { aWindow = window; } //TODO: this is assuming we are in chrome space return aWindow.QueryInterface(Components.interfaces.nsIInterfaceRequestor). getInterface(Components.interfaces.nsIDOMWindowUtils); } // Must be synchronized with nsIDOMWindowUtils. const COMPOSITION_ATTR_RAWINPUT = 0x02; const COMPOSITION_ATTR_SELECTEDRAWTEXT = 0x03; const COMPOSITION_ATTR_CONVERTEDTEXT = 0x04; const COMPOSITION_ATTR_SELECTEDCONVERTEDTEXT = 0x05; /** * Synthesize a composition event. * * @param aEvent The composition event information. This must * have |type| member. The value must be * "compositionstart", "compositionend" or * "compositionupdate". * 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 would * be ignored if the event type were * "compositionstart". * @param aWindow Optional (If null, current |window| will be used) */ function synthesizeComposition(aEvent, aWindow) { var utils = _getDOMWindowUtils(aWindow); if (!utils) { return; } utils.sendCompositionEvent(aEvent.type, aEvent.data ? aEvent.data : "", aEvent.locale ? aEvent.locale : ""); } /** * Synthesize a text event. * * @param aEvent The text 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 * * 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 nsIDOMWindowUtils.COMPOSITION_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. * * @param aWindow Optional (If null, current |window| will be used) */ function synthesizeText(aEvent, aWindow) { var utils = _getDOMWindowUtils(aWindow); if (!utils) { return; } if (!aEvent.composition || !aEvent.composition.clauses || !aEvent.composition.clauses[0]) { return; } var firstClauseLength = aEvent.composition.clauses[0].length; var firstClauseAttr = aEvent.composition.clauses[0].attr; var secondClauseLength = 0; var secondClauseAttr = 0; var thirdClauseLength = 0; var thirdClauseAttr = 0; if (aEvent.composition.clauses[1]) { secondClauseLength = aEvent.composition.clauses[1].length; secondClauseAttr = aEvent.composition.clauses[1].attr; if (aEvent.composition.clauses[2]) { thirdClauseLength = aEvent.composition.clauses[2].length; thirdClauseAttr = aEvent.composition.clauses[2].attr; } } var caretStart = -1; var caretLength = 0; if (aEvent.caret) { caretStart = aEvent.caret.start; caretLength = aEvent.caret.length; } utils.sendTextEvent(aEvent.composition.string, firstClauseLength, firstClauseAttr, secondClauseLength, secondClauseAttr, thirdClauseLength, thirdClauseAttr, caretStart, caretLength); } /** * Synthesize a query selected text event. * * @param aWindow Optional (If null, current |window| will be used) * @return An nsIQueryContentEventResult object. If this failed, * the result might be null. */ function synthesizeQuerySelectedText(aWindow) { var utils = _getDOMWindowUtils(aWindow); if (!utils) { return null; } return utils.sendQueryContentEvent(utils.QUERY_SELECTED_TEXT, 0, 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. */ function synthesizeSelectionSet(aOffset, aLength, aReverse, aWindow) { var utils = _getDOMWindowUtils(aWindow); if (!utils) { return false; } return utils.sendSelectionSetEvent(aOffset, aLength, aReverse); }