michael@0: /** michael@0: * EventUtils provides some utility methods for creating and sending DOM events. michael@0: * Current methods: michael@0: * sendMouseEvent michael@0: * sendChar michael@0: * sendString michael@0: * sendKey michael@0: * synthesizeMouse michael@0: * synthesizeMouseAtCenter michael@0: * synthesizePointer michael@0: * synthesizeWheel michael@0: * synthesizeKey michael@0: * synthesizeNativeKey michael@0: * synthesizeMouseExpectEvent michael@0: * synthesizeKeyExpectEvent michael@0: * michael@0: * When adding methods to this file, please add a performance test for it. michael@0: */ michael@0: michael@0: // This file is used both in privileged and unprivileged contexts, so we have to michael@0: // be careful about our access to Components.interfaces. We also want to avoid michael@0: // naming collisions with anything that might be defined in the scope that imports michael@0: // this script. michael@0: window.__defineGetter__('_EU_Ci', function() { michael@0: // Even if the real |Components| doesn't exist, we might shim in a simple JS michael@0: // placebo for compat. An easy way to differentiate this from the real thing michael@0: // is whether the property is read-only or not. michael@0: var c = Object.getOwnPropertyDescriptor(window, 'Components'); michael@0: return c && c.value && !c.writable ? Components.interfaces : SpecialPowers.Ci; michael@0: }); michael@0: michael@0: /** michael@0: * Send a mouse event to the node aTarget (aTarget can be an id, or an michael@0: * actual node) . The "event" passed in to aEvent is just a JavaScript michael@0: * object with the properties set that the real mouse event object should michael@0: * have. This includes the type of the mouse event. michael@0: * E.g. to send an click event to the node with id 'node' you might do this: michael@0: * michael@0: * sendMouseEvent({type:'click'}, 'node'); michael@0: */ michael@0: function getElement(id) { michael@0: return ((typeof(id) == "string") ? michael@0: document.getElementById(id) : id); michael@0: }; michael@0: michael@0: this.$ = this.getElement; michael@0: michael@0: function sendMouseEvent(aEvent, aTarget, aWindow) { michael@0: if (['click', 'contextmenu', 'dblclick', 'mousedown', 'mouseup', 'mouseover', 'mouseout'].indexOf(aEvent.type) == -1) { michael@0: throw new Error("sendMouseEvent doesn't know about event type '" + aEvent.type + "'"); michael@0: } michael@0: michael@0: if (!aWindow) { michael@0: aWindow = window; michael@0: } michael@0: michael@0: if (!(aTarget instanceof aWindow.Element)) { michael@0: aTarget = aWindow.document.getElementById(aTarget); michael@0: } michael@0: michael@0: var event = aWindow.document.createEvent('MouseEvent'); michael@0: michael@0: var typeArg = aEvent.type; michael@0: var canBubbleArg = true; michael@0: var cancelableArg = true; michael@0: var viewArg = aWindow; michael@0: var detailArg = aEvent.detail || (aEvent.type == 'click' || michael@0: aEvent.type == 'mousedown' || michael@0: aEvent.type == 'mouseup' ? 1 : michael@0: aEvent.type == 'dblclick'? 2 : 0); michael@0: var screenXArg = aEvent.screenX || 0; michael@0: var screenYArg = aEvent.screenY || 0; michael@0: var clientXArg = aEvent.clientX || 0; michael@0: var clientYArg = aEvent.clientY || 0; michael@0: var ctrlKeyArg = aEvent.ctrlKey || false; michael@0: var altKeyArg = aEvent.altKey || false; michael@0: var shiftKeyArg = aEvent.shiftKey || false; michael@0: var metaKeyArg = aEvent.metaKey || false; michael@0: var buttonArg = aEvent.button || (aEvent.type == 'contextmenu' ? 2 : 0); michael@0: var relatedTargetArg = aEvent.relatedTarget || null; michael@0: michael@0: event.initMouseEvent(typeArg, canBubbleArg, cancelableArg, viewArg, detailArg, michael@0: screenXArg, screenYArg, clientXArg, clientYArg, michael@0: ctrlKeyArg, altKeyArg, shiftKeyArg, metaKeyArg, michael@0: buttonArg, relatedTargetArg); michael@0: michael@0: return SpecialPowers.dispatchEvent(aWindow, aTarget, event); michael@0: } michael@0: michael@0: /** michael@0: * Send the char aChar to the focused element. This method handles casing of michael@0: * chars (sends the right charcode, and sends a shift key for uppercase chars). michael@0: * No other modifiers are handled at this point. michael@0: * michael@0: * For now this method only works for ASCII characters and emulates the shift michael@0: * key state on US keyboard layout. michael@0: */ michael@0: function sendChar(aChar, aWindow) { michael@0: var hasShift; michael@0: // Emulate US keyboard layout for the shiftKey state. michael@0: switch (aChar) { michael@0: case "!": michael@0: case "@": michael@0: case "#": michael@0: case "$": michael@0: case "%": michael@0: case "^": michael@0: case "&": michael@0: case "*": michael@0: case "(": michael@0: case ")": michael@0: case "_": michael@0: case "+": michael@0: case "{": michael@0: case "}": michael@0: case ":": michael@0: case "\"": michael@0: case "|": michael@0: case "<": michael@0: case ">": michael@0: case "?": michael@0: hasShift = true; michael@0: break; michael@0: default: michael@0: hasShift = (aChar == aChar.toUpperCase()); michael@0: break; michael@0: } michael@0: synthesizeKey(aChar, { shiftKey: hasShift }, aWindow); michael@0: } michael@0: michael@0: /** michael@0: * Send the string aStr to the focused element. michael@0: * michael@0: * For now this method only works for ASCII characters and emulates the shift michael@0: * key state on US keyboard layout. michael@0: */ michael@0: function sendString(aStr, aWindow) { michael@0: for (var i = 0; i < aStr.length; ++i) { michael@0: sendChar(aStr.charAt(i), aWindow); michael@0: } michael@0: } michael@0: michael@0: /** michael@0: * Send the non-character key aKey to the focused node. michael@0: * The name of the key should be the part that comes after "DOM_VK_" in the michael@0: * KeyEvent constant name for this key. michael@0: * No modifiers are handled at this point. michael@0: */ michael@0: function sendKey(aKey, aWindow) { michael@0: var keyName = "VK_" + aKey.toUpperCase(); michael@0: synthesizeKey(keyName, { shiftKey: false }, aWindow); michael@0: } michael@0: michael@0: /** michael@0: * Parse the key modifier flags from aEvent. Used to share code between michael@0: * synthesizeMouse and synthesizeKey. michael@0: */ michael@0: function _parseModifiers(aEvent) michael@0: { michael@0: const nsIDOMWindowUtils = _EU_Ci.nsIDOMWindowUtils; michael@0: var mval = 0; michael@0: if (aEvent.shiftKey) { michael@0: mval |= nsIDOMWindowUtils.MODIFIER_SHIFT; michael@0: } michael@0: if (aEvent.ctrlKey) { michael@0: mval |= nsIDOMWindowUtils.MODIFIER_CONTROL; michael@0: } michael@0: if (aEvent.altKey) { michael@0: mval |= nsIDOMWindowUtils.MODIFIER_ALT; michael@0: } michael@0: if (aEvent.metaKey) { michael@0: mval |= nsIDOMWindowUtils.MODIFIER_META; michael@0: } michael@0: if (aEvent.accelKey) { michael@0: mval |= (navigator.platform.indexOf("Mac") >= 0) ? michael@0: nsIDOMWindowUtils.MODIFIER_META : nsIDOMWindowUtils.MODIFIER_CONTROL; michael@0: } michael@0: if (aEvent.altGrKey) { michael@0: mval |= nsIDOMWindowUtils.MODIFIER_ALTGRAPH; michael@0: } michael@0: if (aEvent.capsLockKey) { michael@0: mval |= nsIDOMWindowUtils.MODIFIER_CAPSLOCK; michael@0: } michael@0: if (aEvent.fnKey) { michael@0: mval |= nsIDOMWindowUtils.MODIFIER_FN; michael@0: } michael@0: if (aEvent.numLockKey) { michael@0: mval |= nsIDOMWindowUtils.MODIFIER_NUMLOCK; michael@0: } michael@0: if (aEvent.scrollLockKey) { michael@0: mval |= nsIDOMWindowUtils.MODIFIER_SCROLLLOCK; michael@0: } michael@0: if (aEvent.symbolLockKey) { michael@0: mval |= nsIDOMWindowUtils.MODIFIER_SYMBOLLOCK; michael@0: } michael@0: if (aEvent.osKey) { michael@0: mval |= nsIDOMWindowUtils.MODIFIER_OS; michael@0: } michael@0: michael@0: return mval; michael@0: } michael@0: michael@0: /** michael@0: * Synthesize a mouse event on a target. The actual client point is determined michael@0: * by taking the aTarget's client box and offseting it by aOffsetX and michael@0: * aOffsetY. This allows mouse clicks to be simulated by calling this method. michael@0: * michael@0: * aEvent is an object which may contain the properties: michael@0: * shiftKey, ctrlKey, altKey, metaKey, accessKey, clickCount, button, type michael@0: * michael@0: * If the type is specified, an mouse event of that type is fired. Otherwise, michael@0: * a mousedown followed by a mouse up is performed. michael@0: * michael@0: * aWindow is optional, and defaults to the current window object. michael@0: * michael@0: * Returns whether the event had preventDefault() called on it. michael@0: */ michael@0: function synthesizeMouse(aTarget, aOffsetX, aOffsetY, aEvent, aWindow) michael@0: { michael@0: var rect = aTarget.getBoundingClientRect(); michael@0: return synthesizeMouseAtPoint(rect.left + aOffsetX, rect.top + aOffsetY, michael@0: aEvent, aWindow); michael@0: } michael@0: function synthesizeTouch(aTarget, aOffsetX, aOffsetY, aEvent, aWindow) michael@0: { michael@0: var rect = aTarget.getBoundingClientRect(); michael@0: synthesizeTouchAtPoint(rect.left + aOffsetX, rect.top + aOffsetY, michael@0: aEvent, aWindow); michael@0: } michael@0: function synthesizePointer(aTarget, aOffsetX, aOffsetY, aEvent, aWindow) michael@0: { michael@0: var rect = aTarget.getBoundingClientRect(); michael@0: return synthesizePointerAtPoint(rect.left + aOffsetX, rect.top + aOffsetY, michael@0: aEvent, aWindow); michael@0: } michael@0: michael@0: /* michael@0: * Synthesize a mouse event at a particular point in aWindow. michael@0: * michael@0: * aEvent is an object which may contain the properties: michael@0: * shiftKey, ctrlKey, altKey, metaKey, accessKey, clickCount, button, type michael@0: * michael@0: * If the type is specified, an mouse event of that type is fired. Otherwise, michael@0: * a mousedown followed by a mouse up is performed. michael@0: * michael@0: * aWindow is optional, and defaults to the current window object. michael@0: */ michael@0: function synthesizeMouseAtPoint(left, top, aEvent, aWindow) michael@0: { michael@0: var utils = _getDOMWindowUtils(aWindow); michael@0: var defaultPrevented = false; michael@0: michael@0: if (utils) { michael@0: var button = aEvent.button || 0; michael@0: var clickCount = aEvent.clickCount || 1; michael@0: var modifiers = _parseModifiers(aEvent); michael@0: var pressure = ("pressure" in aEvent) ? aEvent.pressure : 0; michael@0: var inputSource = ("inputSource" in aEvent) ? aEvent.inputSource : 0; michael@0: var synthesized = ("isSynthesized" in aEvent) ? aEvent.isSynthesized : true; michael@0: michael@0: if (("type" in aEvent) && aEvent.type) { michael@0: defaultPrevented = utils.sendMouseEvent(aEvent.type, left, top, button, michael@0: clickCount, modifiers, false, michael@0: pressure, inputSource, michael@0: synthesized); michael@0: } michael@0: else { michael@0: utils.sendMouseEvent("mousedown", left, top, button, clickCount, modifiers, false, pressure, inputSource); michael@0: utils.sendMouseEvent("mouseup", left, top, button, clickCount, modifiers, false, pressure, inputSource); michael@0: } michael@0: } michael@0: michael@0: return defaultPrevented; michael@0: } michael@0: function synthesizeTouchAtPoint(left, top, aEvent, aWindow) michael@0: { michael@0: var utils = _getDOMWindowUtils(aWindow); michael@0: michael@0: if (utils) { michael@0: var id = aEvent.id || 0; michael@0: var rx = aEvent.rx || 1; michael@0: var ry = aEvent.rx || 1; michael@0: var angle = aEvent.angle || 0; michael@0: var force = aEvent.force || 1; michael@0: var modifiers = _parseModifiers(aEvent); michael@0: michael@0: if (("type" in aEvent) && aEvent.type) { michael@0: utils.sendTouchEvent(aEvent.type, [id], [left], [top], [rx], [ry], [angle], [force], 1, modifiers); michael@0: } michael@0: else { michael@0: utils.sendTouchEvent("touchstart", [id], [left], [top], [rx], [ry], [angle], [force], 1, modifiers); michael@0: utils.sendTouchEvent("touchend", [id], [left], [top], [rx], [ry], [angle], [force], 1, modifiers); michael@0: } michael@0: } michael@0: } michael@0: function synthesizePointerAtPoint(left, top, aEvent, aWindow) michael@0: { michael@0: var utils = _getDOMWindowUtils(aWindow); michael@0: var defaultPrevented = false; michael@0: michael@0: if (utils) { michael@0: var button = aEvent.button || 0; michael@0: var clickCount = aEvent.clickCount || 1; michael@0: var modifiers = _parseModifiers(aEvent); michael@0: var pressure = ("pressure" in aEvent) ? aEvent.pressure : 0; michael@0: var inputSource = ("inputSource" in aEvent) ? aEvent.inputSource : 0; michael@0: var synthesized = ("isSynthesized" in aEvent) ? aEvent.isSynthesized : true; michael@0: michael@0: if (("type" in aEvent) && aEvent.type) { michael@0: defaultPrevented = utils.sendPointerEvent(aEvent.type, left, top, button, michael@0: clickCount, modifiers, false, michael@0: pressure, inputSource, michael@0: synthesized); michael@0: } michael@0: else { michael@0: utils.sendPointerEvent("pointerdown", left, top, button, clickCount, modifiers, false, pressure, inputSource); michael@0: utils.sendPointerEvent("pointerup", left, top, button, clickCount, modifiers, false, pressure, inputSource); michael@0: } michael@0: } michael@0: michael@0: return defaultPrevented; michael@0: } michael@0: michael@0: // Call synthesizeMouse with coordinates at the center of aTarget. michael@0: function synthesizeMouseAtCenter(aTarget, aEvent, aWindow) michael@0: { michael@0: var rect = aTarget.getBoundingClientRect(); michael@0: synthesizeMouse(aTarget, rect.width / 2, rect.height / 2, aEvent, michael@0: aWindow); michael@0: } michael@0: function synthesizeTouchAtCenter(aTarget, aEvent, aWindow) michael@0: { michael@0: var rect = aTarget.getBoundingClientRect(); michael@0: synthesizeTouch(aTarget, rect.width / 2, rect.height / 2, aEvent, michael@0: aWindow); michael@0: } michael@0: michael@0: /** michael@0: * Synthesize a wheel event on a target. The actual client point is determined michael@0: * by taking the aTarget's client box and offseting it by aOffsetX and michael@0: * aOffsetY. michael@0: * michael@0: * aEvent is an object which may contain the properties: michael@0: * shiftKey, ctrlKey, altKey, metaKey, accessKey, deltaX, deltaY, deltaZ, michael@0: * deltaMode, lineOrPageDeltaX, lineOrPageDeltaY, isMomentum, isPixelOnlyDevice, michael@0: * isCustomizedByPrefs, expectedOverflowDeltaX, expectedOverflowDeltaY michael@0: * michael@0: * deltaMode must be defined, others are ok even if undefined. michael@0: * michael@0: * expectedOverflowDeltaX and expectedOverflowDeltaY take integer value. The michael@0: * value is just checked as 0 or positive or negative. michael@0: * michael@0: * aWindow is optional, and defaults to the current window object. michael@0: */ michael@0: function synthesizeWheel(aTarget, aOffsetX, aOffsetY, aEvent, aWindow) michael@0: { michael@0: var utils = _getDOMWindowUtils(aWindow); michael@0: if (!utils) { michael@0: return; michael@0: } michael@0: michael@0: var modifiers = _parseModifiers(aEvent); michael@0: var options = 0; michael@0: if (aEvent.isPixelOnlyDevice && michael@0: (aEvent.deltaMode == WheelEvent.DOM_DELTA_PIXEL)) { michael@0: options |= utils.WHEEL_EVENT_CAUSED_BY_PIXEL_ONLY_DEVICE; michael@0: } michael@0: if (aEvent.isMomentum) { michael@0: options |= utils.WHEEL_EVENT_CAUSED_BY_MOMENTUM; michael@0: } michael@0: if (aEvent.isCustomizedByPrefs) { michael@0: options |= utils.WHEEL_EVENT_CUSTOMIZED_BY_USER_PREFS; michael@0: } michael@0: if (typeof aEvent.expectedOverflowDeltaX !== "undefined") { michael@0: if (aEvent.expectedOverflowDeltaX === 0) { michael@0: options |= utils.WHEEL_EVENT_EXPECTED_OVERFLOW_DELTA_X_ZERO; michael@0: } else if (aEvent.expectedOverflowDeltaX > 0) { michael@0: options |= utils.WHEEL_EVENT_EXPECTED_OVERFLOW_DELTA_X_POSITIVE; michael@0: } else { michael@0: options |= utils.WHEEL_EVENT_EXPECTED_OVERFLOW_DELTA_X_NEGATIVE; michael@0: } michael@0: } michael@0: if (typeof aEvent.expectedOverflowDeltaY !== "undefined") { michael@0: if (aEvent.expectedOverflowDeltaY === 0) { michael@0: options |= utils.WHEEL_EVENT_EXPECTED_OVERFLOW_DELTA_Y_ZERO; michael@0: } else if (aEvent.expectedOverflowDeltaY > 0) { michael@0: options |= utils.WHEEL_EVENT_EXPECTED_OVERFLOW_DELTA_Y_POSITIVE; michael@0: } else { michael@0: options |= utils.WHEEL_EVENT_EXPECTED_OVERFLOW_DELTA_Y_NEGATIVE; michael@0: } michael@0: } michael@0: var isPixelOnlyDevice = michael@0: aEvent.isPixelOnlyDevice && aEvent.deltaMode == WheelEvent.DOM_DELTA_PIXEL; michael@0: michael@0: // Avoid the JS warnings "reference to undefined property" michael@0: if (!aEvent.deltaX) { michael@0: aEvent.deltaX = 0; michael@0: } michael@0: if (!aEvent.deltaY) { michael@0: aEvent.deltaY = 0; michael@0: } michael@0: if (!aEvent.deltaZ) { michael@0: aEvent.deltaZ = 0; michael@0: } michael@0: michael@0: var lineOrPageDeltaX = michael@0: aEvent.lineOrPageDeltaX != null ? aEvent.lineOrPageDeltaX : michael@0: aEvent.deltaX > 0 ? Math.floor(aEvent.deltaX) : michael@0: Math.ceil(aEvent.deltaX); michael@0: var lineOrPageDeltaY = michael@0: aEvent.lineOrPageDeltaY != null ? aEvent.lineOrPageDeltaY : michael@0: aEvent.deltaY > 0 ? Math.floor(aEvent.deltaY) : michael@0: Math.ceil(aEvent.deltaY); michael@0: michael@0: var rect = aTarget.getBoundingClientRect(); michael@0: utils.sendWheelEvent(rect.left + aOffsetX, rect.top + aOffsetY, michael@0: aEvent.deltaX, aEvent.deltaY, aEvent.deltaZ, michael@0: aEvent.deltaMode, modifiers, michael@0: lineOrPageDeltaX, lineOrPageDeltaY, options); michael@0: } michael@0: michael@0: function _computeKeyCodeFromChar(aChar) michael@0: { michael@0: if (aChar.length != 1) { michael@0: return 0; michael@0: } michael@0: const nsIDOMKeyEvent = _EU_Ci.nsIDOMKeyEvent; michael@0: if (aChar >= 'a' && aChar <= 'z') { michael@0: return nsIDOMKeyEvent.DOM_VK_A + aChar.charCodeAt(0) - 'a'.charCodeAt(0); michael@0: } michael@0: if (aChar >= 'A' && aChar <= 'Z') { michael@0: return nsIDOMKeyEvent.DOM_VK_A + aChar.charCodeAt(0) - 'A'.charCodeAt(0); michael@0: } michael@0: if (aChar >= '0' && aChar <= '9') { michael@0: return nsIDOMKeyEvent.DOM_VK_0 + aChar.charCodeAt(0) - '0'.charCodeAt(0); michael@0: } michael@0: // returns US keyboard layout's keycode michael@0: switch (aChar) { michael@0: case '~': michael@0: case '`': michael@0: return nsIDOMKeyEvent.DOM_VK_BACK_QUOTE; michael@0: case '!': michael@0: return nsIDOMKeyEvent.DOM_VK_1; michael@0: case '@': michael@0: return nsIDOMKeyEvent.DOM_VK_2; michael@0: case '#': michael@0: return nsIDOMKeyEvent.DOM_VK_3; michael@0: case '$': michael@0: return nsIDOMKeyEvent.DOM_VK_4; michael@0: case '%': michael@0: return nsIDOMKeyEvent.DOM_VK_5; michael@0: case '^': michael@0: return nsIDOMKeyEvent.DOM_VK_6; michael@0: case '&': michael@0: return nsIDOMKeyEvent.DOM_VK_7; michael@0: case '*': michael@0: return nsIDOMKeyEvent.DOM_VK_8; michael@0: case '(': michael@0: return nsIDOMKeyEvent.DOM_VK_9; michael@0: case ')': michael@0: return nsIDOMKeyEvent.DOM_VK_0; michael@0: case '-': michael@0: case '_': michael@0: return nsIDOMKeyEvent.DOM_VK_SUBTRACT; michael@0: case '+': michael@0: case '=': michael@0: return nsIDOMKeyEvent.DOM_VK_EQUALS; michael@0: case '{': michael@0: case '[': michael@0: return nsIDOMKeyEvent.DOM_VK_OPEN_BRACKET; michael@0: case '}': michael@0: case ']': michael@0: return nsIDOMKeyEvent.DOM_VK_CLOSE_BRACKET; michael@0: case '|': michael@0: case '\\': michael@0: return nsIDOMKeyEvent.DOM_VK_BACK_SLASH; michael@0: case ':': michael@0: case ';': michael@0: return nsIDOMKeyEvent.DOM_VK_SEMICOLON; michael@0: case '\'': michael@0: case '"': michael@0: return nsIDOMKeyEvent.DOM_VK_QUOTE; michael@0: case '<': michael@0: case ',': michael@0: return nsIDOMKeyEvent.DOM_VK_COMMA; michael@0: case '>': michael@0: case '.': michael@0: return nsIDOMKeyEvent.DOM_VK_PERIOD; michael@0: case '?': michael@0: case '/': michael@0: return nsIDOMKeyEvent.DOM_VK_SLASH; michael@0: case '\n': michael@0: return nsIDOMKeyEvent.DOM_VK_RETURN; michael@0: case ' ': michael@0: return nsIDOMKeyEvent.DOM_VK_SPACE; michael@0: default: michael@0: return 0; michael@0: } michael@0: } michael@0: michael@0: /** michael@0: * isKeypressFiredKey() returns TRUE if the given key should cause keypress michael@0: * event when widget handles the native key event. Otherwise, FALSE. michael@0: * michael@0: * aDOMKeyCode should be one of consts of nsIDOMKeyEvent::DOM_VK_*, or a key michael@0: * name begins with "VK_", or a character. michael@0: */ michael@0: function isKeypressFiredKey(aDOMKeyCode) michael@0: { michael@0: if (typeof(aDOMKeyCode) == "string") { michael@0: if (aDOMKeyCode.indexOf("VK_") == 0) { michael@0: aDOMKeyCode = KeyEvent["DOM_" + aDOMKeyCode]; michael@0: if (!aDOMKeyCode) { michael@0: throw "Unknown key: " + aDOMKeyCode; michael@0: } michael@0: } else { michael@0: // If the key generates a character, it must cause a keypress event. michael@0: return true; michael@0: } michael@0: } michael@0: switch (aDOMKeyCode) { michael@0: case KeyEvent.DOM_VK_SHIFT: michael@0: case KeyEvent.DOM_VK_CONTROL: michael@0: case KeyEvent.DOM_VK_ALT: michael@0: case KeyEvent.DOM_VK_CAPS_LOCK: michael@0: case KeyEvent.DOM_VK_NUM_LOCK: michael@0: case KeyEvent.DOM_VK_SCROLL_LOCK: michael@0: case KeyEvent.DOM_VK_META: michael@0: return false; michael@0: default: michael@0: return true; michael@0: } michael@0: } michael@0: michael@0: /** michael@0: * Synthesize a key event. It is targeted at whatever would be targeted by an michael@0: * actual keypress by the user, typically the focused element. michael@0: * michael@0: * aKey should be either a character or a keycode starting with VK_ such as michael@0: * VK_RETURN. michael@0: * michael@0: * aEvent is an object which may contain the properties: michael@0: * shiftKey, ctrlKey, altKey, metaKey, accessKey, type, location michael@0: * michael@0: * Sets one of KeyboardEvent.DOM_KEY_LOCATION_* to location. Otherwise, michael@0: * DOMWindowUtils will choose good location from the keycode. michael@0: * michael@0: * If the type is specified, a key event of that type is fired. Otherwise, michael@0: * a keydown, a keypress and then a keyup event are fired in sequence. michael@0: * michael@0: * aWindow is optional, and defaults to the current window object. michael@0: */ michael@0: function synthesizeKey(aKey, aEvent, aWindow) michael@0: { michael@0: var utils = _getDOMWindowUtils(aWindow); michael@0: if (utils) { michael@0: var keyCode = 0, charCode = 0; michael@0: if (aKey.indexOf("VK_") == 0) { michael@0: keyCode = KeyEvent["DOM_" + aKey]; michael@0: if (!keyCode) { michael@0: throw "Unknown key: " + aKey; michael@0: } michael@0: } else { michael@0: charCode = aKey.charCodeAt(0); michael@0: keyCode = _computeKeyCodeFromChar(aKey.charAt(0)); michael@0: } michael@0: michael@0: var modifiers = _parseModifiers(aEvent); michael@0: var flags = 0; michael@0: if (aEvent.location != undefined) { michael@0: switch (aEvent.location) { michael@0: case KeyboardEvent.DOM_KEY_LOCATION_STANDARD: michael@0: flags |= utils.KEY_FLAG_LOCATION_STANDARD; michael@0: break; michael@0: case KeyboardEvent.DOM_KEY_LOCATION_LEFT: michael@0: flags |= utils.KEY_FLAG_LOCATION_LEFT; michael@0: break; michael@0: case KeyboardEvent.DOM_KEY_LOCATION_RIGHT: michael@0: flags |= utils.KEY_FLAG_LOCATION_RIGHT; michael@0: break; michael@0: case KeyboardEvent.DOM_KEY_LOCATION_NUMPAD: michael@0: flags |= utils.KEY_FLAG_LOCATION_NUMPAD; michael@0: break; michael@0: case KeyboardEvent.DOM_KEY_LOCATION_MOBILE: michael@0: flags |= utils.KEY_FLAG_LOCATION_MOBILE; michael@0: break; michael@0: case KeyboardEvent.DOM_KEY_LOCATION_JOYSTICK: michael@0: flags |= utils.KEY_FLAG_LOCATION_JOYSTICK; michael@0: break; michael@0: } michael@0: } michael@0: michael@0: if (!("type" in aEvent) || !aEvent.type) { michael@0: // Send keydown + (optional) keypress + keyup events. michael@0: var keyDownDefaultHappened = michael@0: utils.sendKeyEvent("keydown", keyCode, 0, modifiers, flags); michael@0: if (isKeypressFiredKey(keyCode) && keyDownDefaultHappened) { michael@0: utils.sendKeyEvent("keypress", keyCode, charCode, modifiers, flags); michael@0: } michael@0: utils.sendKeyEvent("keyup", keyCode, 0, modifiers, flags); michael@0: } else if (aEvent.type == "keypress") { michael@0: // Send standalone keypress event. michael@0: utils.sendKeyEvent(aEvent.type, keyCode, charCode, modifiers, flags); michael@0: } else { michael@0: // Send other standalone event than keypress. michael@0: utils.sendKeyEvent(aEvent.type, keyCode, 0, modifiers, flags); michael@0: } michael@0: } michael@0: } michael@0: michael@0: function _parseNativeModifiers(aModifiers) michael@0: { michael@0: var modifiers; michael@0: if (aModifiers.capsLockKey) { michael@0: modifiers |= 0x00000001; michael@0: } michael@0: if (aModifiers.numLockKey) { michael@0: modifiers |= 0x00000002; michael@0: } michael@0: if (aModifiers.shiftKey) { michael@0: modifiers |= 0x00000100; michael@0: } michael@0: if (aModifiers.shiftRightKey) { michael@0: modifiers |= 0x00000200; michael@0: } michael@0: if (aModifiers.ctrlKey) { michael@0: modifiers |= 0x00000400; michael@0: } michael@0: if (aModifiers.ctrlRightKey) { michael@0: modifiers |= 0x00000800; michael@0: } michael@0: if (aModifiers.altKey) { michael@0: modifiers |= 0x00001000; michael@0: } michael@0: if (aModifiers.altRightKey) { michael@0: modifiers |= 0x00002000; michael@0: } michael@0: if (aModifiers.metaKey) { michael@0: modifiers |= 0x00004000; michael@0: } michael@0: if (aModifiers.metaRightKey) { michael@0: modifiers |= 0x00008000; michael@0: } michael@0: if (aModifiers.helpKey) { michael@0: modifiers |= 0x00010000; michael@0: } michael@0: if (aModifiers.fnKey) { michael@0: modifiers |= 0x00100000; michael@0: } michael@0: if (aModifiers.numericKeyPadKey) { michael@0: modifiers |= 0x01000000; michael@0: } michael@0: michael@0: if (aModifiers.accelKey) { michael@0: modifiers |= michael@0: (navigator.platform.indexOf("Mac") == 0) ? 0x00004000 : 0x00000400; michael@0: } michael@0: if (aModifiers.accelRightKey) { michael@0: modifiers |= michael@0: (navigator.platform.indexOf("Mac") == 0) ? 0x00008000 : 0x00000800; michael@0: } michael@0: if (aModifiers.altGrKey) { michael@0: modifiers |= michael@0: (navigator.platform.indexOf("Win") == 0) ? 0x00002800 : 0x00001000; michael@0: } michael@0: return modifiers; michael@0: } michael@0: michael@0: // Mac: Any unused number is okay for adding new keyboard layout. michael@0: // When you add new keyboard layout here, you need to modify michael@0: // TISInputSourceWrapper::InitByLayoutID(). michael@0: // Win: These constants can be found by inspecting registry keys under michael@0: // HKEY_LOCAL_MACHINE\System\CurrentControlSet\Control\Keyboard Layouts michael@0: michael@0: const KEYBOARD_LAYOUT_ARABIC = michael@0: { name: "Arabic", Mac: 6, Win: 0x00000401 }; michael@0: const KEYBOARD_LAYOUT_BRAZILIAN_ABNT = michael@0: { name: "Brazilian ABNT", Mac: null, Win: 0x00000416 }; michael@0: const KEYBOARD_LAYOUT_DVORAK_QWERTY = michael@0: { name: "Dvorak-QWERTY", Mac: 4, Win: null }; michael@0: const KEYBOARD_LAYOUT_EN_US = michael@0: { name: "US", Mac: 0, Win: 0x00000409 }; michael@0: const KEYBOARD_LAYOUT_FRENCH = michael@0: { name: "French", Mac: 7, Win: 0x0000040C }; michael@0: const KEYBOARD_LAYOUT_GREEK = michael@0: { name: "Greek", Mac: 1, Win: 0x00000408 }; michael@0: const KEYBOARD_LAYOUT_GERMAN = michael@0: { name: "German", Mac: 2, Win: 0x00000407 }; michael@0: const KEYBOARD_LAYOUT_HEBREW = michael@0: { name: "Hebrew", Mac: 8, Win: 0x0000040D }; michael@0: const KEYBOARD_LAYOUT_JAPANESE = michael@0: { name: "Japanese", Mac: null, Win: 0x00000411 }; michael@0: const KEYBOARD_LAYOUT_LITHUANIAN = michael@0: { name: "Lithuanian", Mac: 9, Win: 0x00010427 }; michael@0: const KEYBOARD_LAYOUT_NORWEGIAN = michael@0: { name: "Norwegian", Mac: 10, Win: 0x00000414 }; michael@0: const KEYBOARD_LAYOUT_SPANISH = michael@0: { name: "Spanish", Mac: 11, Win: 0x0000040A }; michael@0: const KEYBOARD_LAYOUT_SWEDISH = michael@0: { name: "Swedish", Mac: 3, Win: 0x0000041D }; michael@0: const KEYBOARD_LAYOUT_THAI = michael@0: { name: "Thai", Mac: 5, Win: 0x0002041E }; michael@0: michael@0: /** michael@0: * synthesizeNativeKey() dispatches native key event on active window. michael@0: * This is implemented only on Windows and Mac. michael@0: * michael@0: * @param aKeyboardLayout One of KEYBOARD_LAYOUT_* defined above. michael@0: * @param aNativeKeyCode A native keycode value defined in michael@0: * NativeKeyCodes.js. michael@0: * @param aModifiers Modifier keys. If no modifire key is pressed, michael@0: * this must be {}. Otherwise, one or more items michael@0: * referred in _parseNativeModifiers() must be michael@0: * true. michael@0: * @param aChars Specify characters which should be generated michael@0: * by the key event. michael@0: * @param aUnmodifiedChars Specify characters of unmodified (except Shift) michael@0: * aChar value. michael@0: * @return True if this function succeed dispatching michael@0: * native key event. Otherwise, false. michael@0: */ michael@0: michael@0: function synthesizeNativeKey(aKeyboardLayout, aNativeKeyCode, aModifiers, michael@0: aChars, aUnmodifiedChars) michael@0: { michael@0: var utils = _getDOMWindowUtils(window); michael@0: if (!utils) { michael@0: return false; michael@0: } michael@0: var nativeKeyboardLayout = null; michael@0: if (navigator.platform.indexOf("Mac") == 0) { michael@0: nativeKeyboardLayout = aKeyboardLayout.Mac; michael@0: } else if (navigator.platform.indexOf("Win") == 0) { michael@0: nativeKeyboardLayout = aKeyboardLayout.Win; michael@0: } michael@0: if (nativeKeyboardLayout === null) { michael@0: return false; michael@0: } michael@0: utils.sendNativeKeyEvent(nativeKeyboardLayout, aNativeKeyCode, michael@0: _parseNativeModifiers(aModifiers), michael@0: aChars, aUnmodifiedChars); michael@0: return true; michael@0: } michael@0: michael@0: var _gSeenEvent = false; michael@0: michael@0: /** michael@0: * Indicate that an event with an original target of aExpectedTarget and michael@0: * a type of aExpectedEvent is expected to be fired, or not expected to michael@0: * be fired. michael@0: */ michael@0: function _expectEvent(aExpectedTarget, aExpectedEvent, aTestName) michael@0: { michael@0: if (!aExpectedTarget || !aExpectedEvent) michael@0: return null; michael@0: michael@0: _gSeenEvent = false; michael@0: michael@0: var type = (aExpectedEvent.charAt(0) == "!") ? michael@0: aExpectedEvent.substring(1) : aExpectedEvent; michael@0: var eventHandler = function(event) { michael@0: var epassed = (!_gSeenEvent && event.originalTarget == aExpectedTarget && michael@0: event.type == type); michael@0: is(epassed, true, aTestName + " " + type + " event target " + (_gSeenEvent ? "twice" : "")); michael@0: _gSeenEvent = true; michael@0: }; michael@0: michael@0: aExpectedTarget.addEventListener(type, eventHandler, false); michael@0: return eventHandler; michael@0: } michael@0: michael@0: /** michael@0: * Check if the event was fired or not. The event handler aEventHandler michael@0: * will be removed. michael@0: */ michael@0: function _checkExpectedEvent(aExpectedTarget, aExpectedEvent, aEventHandler, aTestName) michael@0: { michael@0: if (aEventHandler) { michael@0: var expectEvent = (aExpectedEvent.charAt(0) != "!"); michael@0: var type = expectEvent ? aExpectedEvent : aExpectedEvent.substring(1); michael@0: aExpectedTarget.removeEventListener(type, aEventHandler, false); michael@0: var desc = type + " event"; michael@0: if (!expectEvent) michael@0: desc += " not"; michael@0: is(_gSeenEvent, expectEvent, aTestName + " " + desc + " fired"); michael@0: } michael@0: michael@0: _gSeenEvent = false; michael@0: } michael@0: michael@0: /** michael@0: * Similar to synthesizeMouse except that a test is performed to see if an michael@0: * event is fired at the right target as a result. michael@0: * michael@0: * aExpectedTarget - the expected originalTarget of the event. michael@0: * aExpectedEvent - the expected type of the event, such as 'select'. michael@0: * aTestName - the test name when outputing results michael@0: * michael@0: * To test that an event is not fired, use an expected type preceded by an michael@0: * exclamation mark, such as '!select'. This might be used to test that a michael@0: * click on a disabled element doesn't fire certain events for instance. michael@0: * michael@0: * aWindow is optional, and defaults to the current window object. michael@0: */ michael@0: function synthesizeMouseExpectEvent(aTarget, aOffsetX, aOffsetY, aEvent, michael@0: aExpectedTarget, aExpectedEvent, aTestName, michael@0: aWindow) michael@0: { michael@0: var eventHandler = _expectEvent(aExpectedTarget, aExpectedEvent, aTestName); michael@0: synthesizeMouse(aTarget, aOffsetX, aOffsetY, aEvent, aWindow); michael@0: _checkExpectedEvent(aExpectedTarget, aExpectedEvent, eventHandler, aTestName); michael@0: } michael@0: michael@0: /** michael@0: * Similar to synthesizeKey except that a test is performed to see if an michael@0: * event is fired at the right target as a result. michael@0: * michael@0: * aExpectedTarget - the expected originalTarget of the event. michael@0: * aExpectedEvent - the expected type of the event, such as 'select'. michael@0: * aTestName - the test name when outputing results michael@0: * michael@0: * To test that an event is not fired, use an expected type preceded by an michael@0: * exclamation mark, such as '!select'. michael@0: * michael@0: * aWindow is optional, and defaults to the current window object. michael@0: */ michael@0: function synthesizeKeyExpectEvent(key, aEvent, aExpectedTarget, aExpectedEvent, michael@0: aTestName, aWindow) michael@0: { michael@0: var eventHandler = _expectEvent(aExpectedTarget, aExpectedEvent, aTestName); michael@0: synthesizeKey(key, aEvent, aWindow); michael@0: _checkExpectedEvent(aExpectedTarget, aExpectedEvent, eventHandler, aTestName); michael@0: } michael@0: michael@0: function disableNonTestMouseEvents(aDisable) michael@0: { michael@0: var domutils = _getDOMWindowUtils(); michael@0: domutils.disableNonTestMouseEvents(aDisable); michael@0: } michael@0: michael@0: function _getDOMWindowUtils(aWindow) michael@0: { michael@0: if (!aWindow) { michael@0: aWindow = window; michael@0: } michael@0: michael@0: // we need parent.SpecialPowers for: michael@0: // layout/base/tests/test_reftests_with_caret.html michael@0: // chrome: toolkit/content/tests/chrome/test_findbar.xul michael@0: // chrome: toolkit/content/tests/chrome/test_popup_anchor.xul michael@0: if ("SpecialPowers" in window && window.SpecialPowers != undefined) { michael@0: return SpecialPowers.getDOMWindowUtils(aWindow); michael@0: } michael@0: if ("SpecialPowers" in parent && parent.SpecialPowers != undefined) { michael@0: return parent.SpecialPowers.getDOMWindowUtils(aWindow); michael@0: } michael@0: michael@0: //TODO: this is assuming we are in chrome space michael@0: return aWindow.QueryInterface(_EU_Ci.nsIInterfaceRequestor). michael@0: getInterface(_EU_Ci.nsIDOMWindowUtils); michael@0: } michael@0: michael@0: // Must be synchronized with nsICompositionStringSynthesizer. michael@0: const COMPOSITION_ATTR_RAWINPUT = 0x02; michael@0: const COMPOSITION_ATTR_SELECTEDRAWTEXT = 0x03; michael@0: const COMPOSITION_ATTR_CONVERTEDTEXT = 0x04; michael@0: const COMPOSITION_ATTR_SELECTEDCONVERTEDTEXT = 0x05; michael@0: michael@0: /** michael@0: * Synthesize a composition event. michael@0: * michael@0: * @param aEvent The composition event information. This must michael@0: * have |type| member. The value must be michael@0: * "compositionstart", "compositionend" or michael@0: * "compositionupdate". michael@0: * And also this may have |data| and |locale| which michael@0: * would be used for the value of each property of michael@0: * the composition event. Note that the data would michael@0: * be ignored if the event type were michael@0: * "compositionstart". michael@0: * @param aWindow Optional (If null, current |window| will be used) michael@0: */ michael@0: function synthesizeComposition(aEvent, aWindow) michael@0: { michael@0: var utils = _getDOMWindowUtils(aWindow); michael@0: if (!utils) { michael@0: return; michael@0: } michael@0: michael@0: utils.sendCompositionEvent(aEvent.type, aEvent.data ? aEvent.data : "", michael@0: aEvent.locale ? aEvent.locale : ""); michael@0: } michael@0: /** michael@0: * Synthesize a text event. michael@0: * michael@0: * @param aEvent The text event's information, this has |composition| michael@0: * and |caret| members. |composition| has |string| and michael@0: * |clauses| members. |clauses| must be array object. Each michael@0: * object has |length| and |attr|. And |caret| has |start| and michael@0: * |length|. See the following tree image. michael@0: * michael@0: * aEvent michael@0: * +-- composition michael@0: * | +-- string michael@0: * | +-- clauses[] michael@0: * | +-- length michael@0: * | +-- attr michael@0: * +-- caret michael@0: * +-- start michael@0: * +-- length michael@0: * michael@0: * Set the composition string to |composition.string|. Set its michael@0: * clauses information to the |clauses| array. michael@0: * michael@0: * When it's composing, set the each clauses' length to the michael@0: * |composition.clauses[n].length|. The sum of the all length michael@0: * values must be same as the length of |composition.string|. michael@0: * Set nsICompositionStringSynthesizer.ATTR_* to the michael@0: * |composition.clauses[n].attr|. michael@0: * michael@0: * When it's not composing, set 0 to the michael@0: * |composition.clauses[0].length| and michael@0: * |composition.clauses[0].attr|. michael@0: * michael@0: * Set caret position to the |caret.start|. It's offset from michael@0: * the start of the composition string. Set caret length to michael@0: * |caret.length|. If it's larger than 0, it should be wide michael@0: * caret. However, current nsEditor doesn't support wide michael@0: * caret, therefore, you should always set 0 now. michael@0: * michael@0: * @param aWindow Optional (If null, current |window| will be used) michael@0: */ michael@0: function synthesizeText(aEvent, aWindow) michael@0: { michael@0: var utils = _getDOMWindowUtils(aWindow); michael@0: if (!utils) { michael@0: return; michael@0: } michael@0: michael@0: if (!aEvent.composition || !aEvent.composition.clauses || michael@0: !aEvent.composition.clauses[0]) { michael@0: return; michael@0: } michael@0: michael@0: var compositionString = utils.createCompositionStringSynthesizer(); michael@0: compositionString.setString(aEvent.composition.string); michael@0: if (aEvent.composition.clauses[0].length) { michael@0: for (var i = 0; i < aEvent.composition.clauses.length; i++) { michael@0: switch (aEvent.composition.clauses[i].attr) { michael@0: case compositionString.ATTR_RAWINPUT: michael@0: case compositionString.ATTR_SELECTEDRAWTEXT: michael@0: case compositionString.ATTR_CONVERTEDTEXT: michael@0: case compositionString.ATTR_SELECTEDCONVERTEDTEXT: michael@0: compositionString.appendClause(aEvent.composition.clauses[i].length, michael@0: aEvent.composition.clauses[i].attr); michael@0: break; michael@0: case 0: michael@0: // Ignore dummy clause for the argument. michael@0: break; michael@0: default: michael@0: throw new Error("invalid clause attribute specified"); michael@0: break; michael@0: } michael@0: } michael@0: } michael@0: michael@0: if (aEvent.caret) { michael@0: compositionString.setCaret(aEvent.caret.start, aEvent.caret.length); michael@0: } michael@0: michael@0: compositionString.dispatchEvent(); michael@0: } michael@0: michael@0: // Must be synchronized with nsIDOMWindowUtils. michael@0: const QUERY_CONTENT_FLAG_USE_NATIVE_LINE_BREAK = 0x0000; michael@0: const QUERY_CONTENT_FLAG_USE_XP_LINE_BREAK = 0x0001; michael@0: michael@0: const SELECTION_SET_FLAG_USE_NATIVE_LINE_BREAK = 0x0000; michael@0: const SELECTION_SET_FLAG_USE_XP_LINE_BREAK = 0x0001; michael@0: const SELECTION_SET_FLAG_REVERSE = 0x0002; michael@0: michael@0: /** michael@0: * Synthesize a query selected text event. michael@0: * michael@0: * @param aWindow Optional (If null, current |window| will be used) michael@0: * @return An nsIQueryContentEventResult object. If this failed, michael@0: * the result might be null. michael@0: */ michael@0: function synthesizeQuerySelectedText(aWindow) michael@0: { michael@0: var utils = _getDOMWindowUtils(aWindow); michael@0: if (!utils) { michael@0: return null; michael@0: } michael@0: michael@0: return utils.sendQueryContentEvent(utils.QUERY_SELECTED_TEXT, 0, 0, 0, 0, michael@0: QUERY_CONTENT_FLAG_USE_NATIVE_LINE_BREAK); michael@0: } michael@0: michael@0: /** michael@0: * Synthesize a query caret rect event. michael@0: * michael@0: * @param aOffset The caret offset. 0 means left side of the first character michael@0: * in the selection root. michael@0: * @param aWindow Optional (If null, current |window| will be used) michael@0: * @return An nsIQueryContentEventResult object. If this failed, michael@0: * the result might be null. michael@0: */ michael@0: function synthesizeQueryCaretRect(aOffset, aWindow) michael@0: { michael@0: var utils = _getDOMWindowUtils(aWindow); michael@0: if (!utils) { michael@0: return null; michael@0: } michael@0: return utils.sendQueryContentEvent(utils.QUERY_CARET_RECT, michael@0: aOffset, 0, 0, 0, michael@0: QUERY_CONTENT_FLAG_USE_NATIVE_LINE_BREAK); michael@0: } michael@0: michael@0: /** michael@0: * Synthesize a selection set event. michael@0: * michael@0: * @param aOffset The character offset. 0 means the first character in the michael@0: * selection root. michael@0: * @param aLength The length of the text. If the length is too long, michael@0: * the extra length is ignored. michael@0: * @param aReverse If true, the selection is from |aOffset + aLength| to michael@0: * |aOffset|. Otherwise, from |aOffset| to |aOffset + aLength|. michael@0: * @param aWindow Optional (If null, current |window| will be used) michael@0: * @return True, if succeeded. Otherwise false. michael@0: */ michael@0: function synthesizeSelectionSet(aOffset, aLength, aReverse, aWindow) michael@0: { michael@0: var utils = _getDOMWindowUtils(aWindow); michael@0: if (!utils) { michael@0: return false; michael@0: } michael@0: var flags = aReverse ? SELECTION_SET_FLAG_REVERSE : 0; michael@0: return utils.sendSelectionSetEvent(aOffset, aLength, flags); michael@0: }