@qooxdoo/framework
Version:
The JS Framework for Coders
705 lines (613 loc) • 19.9 kB
JavaScript
/* ************************************************************************
qooxdoo - the new era of web development
http://qooxdoo.org
Copyright:
2004-2008 1&1 Internet AG, Germany, http://www.1und1.de
License:
MIT: https://opensource.org/licenses/MIT
See the LICENSE file in the project's top-level directory for details.
Authors:
* Sebastian Werner (wpbasti)
* Andreas Ecker (ecker)
* Fabian Jakobs (fjakobs)
************************************************************************ */
/**
* This class provides unified key event handler for Internet Explorer,
* Firefox, Opera and Safari.
*
* NOTE: Instances of this class must be disposed of after use
*
* @require(qx.event.handler.UserAction)
*/
qx.Class.define("qx.event.handler.Keyboard", {
extend: qx.core.Object,
implement: [qx.event.IEventHandler, qx.core.IDisposable],
/*
*****************************************************************************
CONSTRUCTOR
*****************************************************************************
*/
/**
* Create a new instance
*
* @param manager {qx.event.Manager} Event manager for the window to use
*/
construct(manager) {
super();
// Define shorthands
this.__manager = manager;
this.__window = manager.getWindow();
// Gecko ignores key events when not explicitly clicked in the document.
if (qx.core.Environment.get("engine.name") == "gecko") {
this.__root = this.__window;
} else {
this.__root = this.__window.document.documentElement;
}
// Internal sequence cache
this.__lastUpDownType = {};
// Initialize observer
this._initKeyObserver();
},
/*
*****************************************************************************
STATICS
*****************************************************************************
*/
statics: {
/** @type {Integer} Priority of this handler */
PRIORITY: qx.event.Registration.PRIORITY_NORMAL,
/** @type {Map} Supported event types */
SUPPORTED_TYPES: {
keyup: 1,
keydown: 1,
keypress: 1,
keyinput: 1
},
/** @type {Integer} Which target check to use */
TARGET_CHECK: qx.event.IEventHandler.TARGET_DOMNODE,
/** @type {Integer} Whether the method "canHandleEvent" must be called */
IGNORE_CAN_HANDLE: true
},
/*
*****************************************************************************
MEMBERS
*****************************************************************************
*/
members: {
__onKeyUpDownWrapper: null,
__manager: null,
__window: null,
__root: null,
__lastUpDownType: null,
__lastKeyCode: null,
__inputListeners: null,
__onKeyPressWrapper: null,
/*
---------------------------------------------------------------------------
EVENT HANDLER INTERFACE
---------------------------------------------------------------------------
*/
// interface implementation
canHandleEvent(target, type) {},
// interface implementation
registerEvent(target, type, capture) {
// Nothing needs to be done here
},
// interface implementation
unregisterEvent(target, type, capture) {
// Nothing needs to be done here
},
/*
---------------------------------------------------------------------------
HELPER
---------------------------------------------------------------------------
*/
/**
* Fire a key input event with the given parameters
*
* @param domEvent {Event} DOM event
* @param charCode {Integer} character code
* @return {qx.Promise?} a promise if the event handlers created one
*/
_fireInputEvent(domEvent, charCode) {
var target = this.__getEventTarget();
var tracker = {};
var self = this;
// Only fire when target is defined and visible
if (target && target.offsetWidth != 0) {
var event = qx.event.Registration.createEvent(
"keyinput",
qx.event.type.KeyInput,
[domEvent, target, charCode]
);
qx.event.Utils.then(tracker, function () {
self.__manager.dispatchEvent(target, event);
});
}
// Fire user action event
// Needs to check if still alive first
if (this.__window) {
var self = this;
qx.event.Utils.then(tracker, function () {
return qx.event.Registration.fireEvent(
self.__window,
"useraction",
qx.event.type.Data,
["keyinput"]
);
});
}
return tracker.promise;
},
/**
* Fire a key up/down/press event with the given parameters
*
* @param domEvent {Event} DOM event
* @param type {String} type og the event
* @param keyIdentifier {String} key identifier
* @return {qx.Promise?} a promise, if any of the event handlers returned a promise
*/
_fireSequenceEvent(domEvent, type, keyIdentifier) {
var target = this.__getEventTarget();
var keyCode = domEvent.keyCode;
var tracker = {};
var self = this;
// Fire key event
var event = qx.event.Registration.createEvent(
type,
qx.event.type.KeySequence,
[domEvent, target, keyIdentifier]
);
qx.event.Utils.then(tracker, function () {
return self.__manager.dispatchEvent(target, event);
});
// IE and Safari suppress a "keypress" event if the "keydown" event's
// default action was prevented. In this case we emulate the "keypress"
//
// FireFox suppresses "keypress" when "keydown" default action is prevented.
// from version 29: https://bugzilla.mozilla.org/show_bug.cgi?id=935876.
if (event.getDefaultPrevented() && type == "keydown") {
if (
qx.core.Environment.get("engine.name") == "mshtml" ||
qx.core.Environment.get("engine.name") == "webkit" ||
(qx.core.Environment.get("engine.name") == "gecko" &&
qx.core.Environment.get("browser.version") >= 29)
) {
// some key press events are already emulated. Ignore these events.
if (
!qx.event.util.Keyboard.isNonPrintableKeyCode(keyCode) &&
!this._emulateKeyPress[keyCode]
) {
qx.event.Utils.then(tracker, function () {
return self._fireSequenceEvent(
domEvent,
"keypress",
keyIdentifier
);
});
}
}
}
// Fire user action event
// Needs to check if still alive first
if (this.__window) {
qx.event.Utils.then(tracker, function () {
return qx.event.Registration.fireEvent(
self.__window,
"useraction",
qx.event.type.Data,
[type]
);
});
}
return tracker.promise;
},
/**
* Get the target element for key events
*
* @return {Element} the event target element
*/
__getEventTarget() {
var focusHandler = this.__manager.getHandler(qx.event.handler.Focus);
var target = focusHandler.getActive();
// Fallback to focused element when active is null or invisible
if (!target || target.offsetWidth == 0) {
target = focusHandler.getFocus();
}
// Fallback to body when focused is null or invisible
if (!target || target.offsetWidth == 0) {
target = this.__manager.getWindow().document.body;
}
return target;
},
/*
---------------------------------------------------------------------------
OBSERVER INIT/STOP
---------------------------------------------------------------------------
*/
/**
* Initializes the native key event listeners.
*
* @signature function()
*/
_initKeyObserver() {
this.__onKeyUpDownWrapper = qx.lang.Function.listener(
this.__onKeyUpDown,
this
);
this.__onKeyPressWrapper = qx.lang.Function.listener(
this.__onKeyPress,
this
);
var Event = qx.bom.Event;
Event.addNativeListener(this.__root, "keyup", this.__onKeyUpDownWrapper);
Event.addNativeListener(
this.__root,
"keydown",
this.__onKeyUpDownWrapper
);
Event.addNativeListener(
this.__root,
"keypress",
this.__onKeyPressWrapper
);
},
/**
* Stops the native key event listeners.
*
* @signature function()
*/
_stopKeyObserver() {
var Event = qx.bom.Event;
Event.removeNativeListener(
this.__root,
"keyup",
this.__onKeyUpDownWrapper
);
Event.removeNativeListener(
this.__root,
"keydown",
this.__onKeyUpDownWrapper
);
Event.removeNativeListener(
this.__root,
"keypress",
this.__onKeyPressWrapper
);
for (var key in this.__inputListeners || {}) {
var listener = this.__inputListeners[key];
Event.removeNativeListener(
listener.target,
"keypress",
listener.callback
);
}
delete this.__inputListeners;
},
/*
---------------------------------------------------------------------------
NATIVE EVENT OBSERVERS
---------------------------------------------------------------------------
*/
/**
* Low level handler for "keyup" and "keydown" events
*
* @internal
* @signature function(domEvent)
* @param domEvent {Event} DOM event object
*/
__onKeyUpDown: qx.event.GlobalError.observeMethod(
qx.core.Environment.select("engine.name", {
"gecko|webkit|mshtml"(domEvent) {
var keyCode = 0;
var charCode = 0;
var type = domEvent.type;
keyCode = domEvent.keyCode;
var tracker = {};
var self = this;
qx.event.Utils.track(
tracker,
this._idealKeyHandler(keyCode, charCode, type, domEvent)
);
// On non print-able character be sure to add a keypress event
if (type == "keydown") {
/*
* We need an artificial keypress event for every keydown event.
* Newer browsers do not fire keypress for a regular charachter key (e.g when typing 'a')
* if it was typed with the CTRL, ALT or META Key pressed during typing, like
* doing it when typing the combination CTRL+A
*/
var isModifierDown =
domEvent.ctrlKey || domEvent.altKey || domEvent.metaKey;
// non-printable, backspace, tab or the modfier keys are down
if (
qx.event.util.Keyboard.isNonPrintableKeyCode(keyCode) ||
this._emulateKeyPress[keyCode] ||
isModifierDown
) {
qx.event.Utils.then(tracker, function () {
return self._idealKeyHandler(
keyCode,
charCode,
"keypress",
domEvent
);
});
}
}
// Store last type
this.__lastUpDownType[keyCode] = type;
return tracker.promise;
},
opera(domEvent) {
this.__lastKeyCode = domEvent.keyCode;
return this._idealKeyHandler(
domEvent.keyCode,
0,
domEvent.type,
domEvent
);
}
})
),
/**
* some keys like "up", "down", "pageup", "pagedown" do not bubble a
* "keypress" event in Firefox. To work around this bug we attach keypress
* listeners directly to the input events.
*
* https://bugzilla.mozilla.org/show_bug.cgi?id=467513
*
* @signature function(target, type, keyCode)
* @param target {Element} The event target
* @param type {String} The event type
* @param keyCode {Integer} the key code
*/
__firefoxInputFix: qx.core.Environment.select("engine.name", {
gecko(target, type, keyCode) {
if (
type === "keydown" &&
(keyCode == 33 || keyCode == 34 || keyCode == 38 || keyCode == 40) &&
target.type == "text" &&
target.tagName.toLowerCase() === "input" &&
target.getAttribute("autoComplete") !== "off"
) {
if (!this.__inputListeners) {
this.__inputListeners = {};
}
var hash = qx.core.ObjectRegistry.toHashCode(target);
if (this.__inputListeners[hash]) {
return;
}
var self = this;
this.__inputListeners[hash] = {
target: target,
callback(domEvent) {
qx.bom.Event.stopPropagation(domEvent);
self.__onKeyPress(domEvent);
}
};
var listener = qx.event.GlobalError.observeMethod(
this.__inputListeners[hash].callback
);
qx.bom.Event.addNativeListener(target, "keypress", listener);
}
},
default: null
}),
/**
* Low level key press handler
*
* @signature function(domEvent)
* @param domEvent {Event} DOM event object
*/
__onKeyPress: qx.event.GlobalError.observeMethod(
qx.core.Environment.select("engine.name", {
mshtml(domEvent) {
domEvent = window.event || domEvent;
if (this._charCode2KeyCode[domEvent.keyCode]) {
return this._idealKeyHandler(
this._charCode2KeyCode[domEvent.keyCode],
0,
domEvent.type,
domEvent
);
} else {
return this._idealKeyHandler(
0,
domEvent.keyCode,
domEvent.type,
domEvent
);
}
},
gecko(domEvent) {
if (qx.core.Environment.get("engine.version") < 66) {
var charCode = domEvent.charCode;
var type = domEvent.type;
return this._idealKeyHandler(
domEvent.keyCode,
charCode,
type,
domEvent
);
} else {
if (this._charCode2KeyCode[domEvent.keyCode]) {
return this._idealKeyHandler(
this._charCode2KeyCode[domEvent.keyCode],
0,
domEvent.type,
domEvent
);
} else {
return this._idealKeyHandler(
0,
domEvent.keyCode,
domEvent.type,
domEvent
);
}
}
},
webkit(domEvent) {
if (this._charCode2KeyCode[domEvent.keyCode]) {
return this._idealKeyHandler(
this._charCode2KeyCode[domEvent.keyCode],
0,
domEvent.type,
domEvent
);
} else {
return this._idealKeyHandler(
0,
domEvent.keyCode,
domEvent.type,
domEvent
);
}
},
opera(domEvent) {
var keyCode = domEvent.keyCode;
var type = domEvent.type;
// Some keys are identified differently for key up/down and keypress
// (e.g. "v" gets identified as "F7").
// So we store the last key up/down keycode and compare it to the
// current keycode.
// See http://bugzilla.qooxdoo.org/show_bug.cgi?id=603
if (keyCode != this.__lastKeyCode) {
return this._idealKeyHandler(0, this.__lastKeyCode, type, domEvent);
} else {
if (
qx.event.util.Keyboard.keyCodeToIdentifierMap[domEvent.keyCode]
) {
return this._idealKeyHandler(
domEvent.keyCode,
0,
domEvent.type,
domEvent
);
} else {
return this._idealKeyHandler(
0,
domEvent.keyCode,
domEvent.type,
domEvent
);
}
}
}
})
),
/*
---------------------------------------------------------------------------
IDEAL KEY HANDLER
---------------------------------------------------------------------------
*/
/**
* Key handler for an idealized browser.
* Runs after the browser specific key handlers have normalized the key events.
*
* @param keyCode {String} keyboard code
* @param charCode {String} character code
* @param eventType {String} type of the event (keydown, keypress, keyup)
* @param domEvent {Element} DomEvent
* @return {qx.Promise?} a promise, if an event handler created one
*/
_idealKeyHandler(keyCode, charCode, eventType, domEvent) {
var keyIdentifier;
// Use: keyCode
if (keyCode || (!keyCode && !charCode)) {
keyIdentifier = qx.event.util.Keyboard.keyCodeToIdentifier(keyCode);
return this._fireSequenceEvent(domEvent, eventType, keyIdentifier);
}
// Use: charCode
else {
keyIdentifier = qx.event.util.Keyboard.charCodeToIdentifier(charCode);
var tracker = {};
var self = this;
qx.event.Utils.track(
tracker,
this._fireSequenceEvent(domEvent, "keypress", keyIdentifier)
);
return qx.event.Utils.then(tracker, function () {
return self._fireInputEvent(domEvent, charCode);
});
}
},
/*
---------------------------------------------------------------------------
KEY MAPS
---------------------------------------------------------------------------
*/
/**
* @type {Map} maps the charcodes of special keys for key press emulation
*
* @lint ignoreReferenceField(_emulateKeyPress)
*/
_emulateKeyPress: qx.core.Environment.select("engine.name", {
mshtml: {
8: true,
9: true
},
webkit: {
8: true,
9: true,
27: true
},
gecko:
qx.core.Environment.get("browser.version") >= 65
? {
8: true,
9: true,
27: true
}
: {},
default: {}
}),
/*
---------------------------------------------------------------------------
HELPER METHODS
---------------------------------------------------------------------------
*/
/**
* converts a key identifier back to a keycode
*
* @param keyIdentifier {String} The key identifier to convert
* @return {Integer} keyboard code
*/
_identifierToKeyCode(keyIdentifier) {
return (
qx.event.util.Keyboard.identifierToKeyCodeMap[keyIdentifier] ||
keyIdentifier.charCodeAt(0)
);
}
},
/*
*****************************************************************************
DESTRUCTOR
*****************************************************************************
*/
destruct() {
this._stopKeyObserver();
this.__lastKeyCode =
this.__manager =
this.__window =
this.__root =
this.__lastUpDownType =
null;
},
/*
*****************************************************************************
DEFER
*****************************************************************************
*/
defer(statics, members) {
// register at the event handler
qx.event.Registration.addHandler(statics);
if (qx.core.Environment.get("engine.name") !== "opera") {
members._charCode2KeyCode = {
13: 13,
27: 27
};
}
}
});