@qooxdoo/framework
Version:
The JS Framework for Coders
729 lines (633 loc) • 22.7 kB
JavaScript
/* ************************************************************************
qooxdoo - the new era of web development
http://qooxdoo.org
Copyright:
2011-2012 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:
* Martin Wittemann (wittemann)
* Daniel Wagner (danielwagner)
************************************************************************ */
/**
* Support for native and custom events.
*
* @require(qx.module.Polyfill)
* @require(qx.module.Environment)
* @use(qx.module.event.PointerHandler)
* @group (Core)
*/
qx.Bootstrap.define("qx.module.Event", {
statics :
{
/**
* Event normalization registry
*
* @internal
*/
__normalizations : {},
/**
* Registry of event hooks
* @internal
*/
__hooks : {
on : {},
off : {}
},
__isReady : false,
/**
* Executes the given function once the document is ready.
*
* @attachStatic {qxWeb}
* @param callback {Function} callback function
*/
ready : function(callback) {
// DOM is already ready
if (document.readyState === "complete") {
window.setTimeout(callback, 1);
return;
}
// listen for the load event so the callback is executed no matter what
var onWindowLoad = function()
{
qx.module.Event.__isReady = true;
callback();
};
qxWeb(window).on("load", onWindowLoad);
var wrappedCallback = function() {
qxWeb(window).off("load", onWindowLoad);
callback();
};
// Listen for DOMContentLoaded event if available (no way to reliably detect
// support)
if (qxWeb.env.get("engine.name") !== "mshtml" || qxWeb.env.get("browser.documentmode") > 8) {
qx.bom.Event.addNativeListener(document, "DOMContentLoaded", wrappedCallback);
}
else {
// Continually check to see if the document is ready
var timer = function() {
// onWindowLoad already executed
if (qx.module.Event.__isReady) {
return;
}
try {
// If DOMContentLoaded is unavailable, use the trick by Diego Perini
// http://javascript.nwbox.com/IEContentLoaded/
document.documentElement.doScroll("left");
if (document.body) {
wrappedCallback();
}
}
catch(error) {
window.setTimeout(timer, 100);
}
};
timer();
}
},
/**
* Registers a normalization function for the given event types. Listener
* callbacks for these types will be called with the return value of the
* normalization function instead of the regular event object.
*
* The normalizer will be called with two arguments: The original event
* object and the element on which the event was triggered
*
* @attachStatic {qxWeb, $registerEventNormalization}
* @param types {String[]} List of event types to be normalized. Use an
* asterisk (<code>*</code>) to normalize all event types
* @param normalizer {Function} Normalizer function
*/
$registerEventNormalization : function(types, normalizer)
{
if (!qx.lang.Type.isArray(types)) {
types = [types];
}
var registry = qx.module.Event.__normalizations;
for (var i=0,l=types.length; i<l; i++) {
var type = types[i];
if (qx.lang.Type.isFunction(normalizer)) {
if (!registry[type]) {
registry[type] = [];
}
registry[type].push(normalizer);
}
}
},
/**
* Unregisters a normalization function from the given event types.
*
* @attachStatic {qxWeb, $unregisterEventNormalization}
* @param types {String[]} List of event types
* @param normalizer {Function} Normalizer function
*/
$unregisterEventNormalization : function(types, normalizer)
{
if (!qx.lang.Type.isArray(types)) {
types = [types];
}
var registry = qx.module.Event.__normalizations;
for (var i=0,l=types.length; i<l; i++) {
var type = types[i];
if (registry[type]) {
qx.lang.Array.remove(registry[type], normalizer);
}
}
},
/**
* Returns all registered event normalizers
*
* @attachStatic {qxWeb, $getEventNormalizationRegistry}
* @return {Map} Map of event types/normalizer functions
*/
$getEventNormalizationRegistry : function()
{
return qx.module.Event.__normalizations;
},
/**
* Registers an event hook for the given event types.
*
* @attachStatic {qxWeb, $registerEventHook}
* @param types {String[]} List of event types
* @param registerHook {Function} Hook function to be called on event registration
* @param unregisterHook {Function?} Hook function to be called on event deregistration
* @internal
*/
$registerEventHook : function(types, registerHook, unregisterHook)
{
if (!qx.lang.Type.isArray(types)) {
types = [types];
}
var onHooks = qx.module.Event.__hooks.on;
for (var i=0,l=types.length; i<l; i++) {
var type = types[i];
if (qx.lang.Type.isFunction(registerHook)) {
if (!onHooks[type]) {
onHooks[type] = [];
}
onHooks[type].push(registerHook);
}
}
if (!unregisterHook) {
return;
}
var offHooks = qx.module.Event.__hooks.off;
for (var i=0,l=types.length; i<l; i++) {
var type = types[i];
if (qx.lang.Type.isFunction(unregisterHook)) {
if (!offHooks[type]) {
offHooks[type] = [];
}
offHooks[type].push(unregisterHook);
}
}
},
/**
* Unregisters a hook from the given event types.
*
* @attachStatic {qxWeb, $unregisterEventHooks}
* @param types {String[]} List of event types
* @param registerHook {Function} Hook function to be called on event registration
* @param unregisterHook {Function?} Hook function to be called on event deregistration
* @internal
*/
$unregisterEventHook : function(types, registerHook, unregisterHook)
{
if (!qx.lang.Type.isArray(types)) {
types = [types];
}
var onHooks = qx.module.Event.__hooks.on;
for (var i=0,l=types.length; i<l; i++) {
var type = types[i];
if (onHooks[type]) {
qx.lang.Array.remove(onHooks[type], registerHook);
}
}
if (!unregisterHook) {
return;
}
var offHooks = qx.module.Event.__hooks.off;
for (var i=0,l=types.length; i<l; i++) {
var type = types[i];
if (offHooks[type]) {
qx.lang.Array.remove(offHooks[type], unregisterHook);
}
}
},
/**
* Returns all registered event hooks
*
* @attachStatic {qxWeb, $getEventHookRegistry}
* @return {Map} Map of event types/registration hook functions
* @internal
*/
$getEventHookRegistry : function()
{
return qx.module.Event.__hooks;
}
},
members :
{
/**
* Registers a listener for the given event type on each item in the
* collection. This can be either native or custom events.
*
* @attach {qxWeb}
* @param type {String} Type of the event to listen for
* @param listener {Function} Listener callback
* @param context {Object?} Context the callback function will be executed in.
* Default: The element on which the listener was registered
* @param useCapture {Boolean?} Attach the listener to the capturing
* phase if true
* @return {qxWeb} The collection for chaining
*/
on : function(type, listener, context, useCapture) {
for (var i=0; i < this.length; i++) {
var el = this[i];
var ctx = context || qxWeb(el);
// call hooks
var hooks = qx.module.Event.__hooks.on;
// generic
var typeHooks = hooks["*"] || [];
// type specific
if (hooks[type]) {
typeHooks = typeHooks.concat(hooks[type]);
}
for (var j=0, m=typeHooks.length; j<m; j++) {
typeHooks[j](el, type, listener, context);
}
var bound = function(el, event) {
// apply normalizations
var registry = qx.module.Event.__normalizations;
// generic
var normalizations = registry["*"] || [];
// type specific
if (registry[type]) {
normalizations = normalizations.concat(registry[type]);
}
for (var x=0, y=normalizations.length; x<y; x++) {
event = normalizations[x](event, el, type);
}
// call original listener with normalized event
listener.apply(this, [event]);
}.bind(ctx, el);
bound.original = listener;
// add native listener
qx.bom.Event.addNativeListener(el, type, bound, useCapture);
// create an emitter if necessary
if (!el.$$emitter) {
el.$$emitter = new qx.event.Emitter();
}
el.$$lastlistenerId = el.$$emitter.on(type, bound, ctx);
// save the useCapture for removing
el.$$emitter.getEntryById(el.$$lastlistenerId).useCapture = !!useCapture;
if (!el.__listener) {
el.__listener = {};
}
if (!el.__listener[type]) {
el.__listener[type] = {};
}
el.__listener[type][el.$$lastlistenerId] = bound;
if (!context) {
// store a reference to the dynamically created context so we know
// what to check for when removing the listener
if (!el.__ctx) {
el.__ctx = {};
}
el.__ctx[el.$$lastlistenerId] = ctx;
}
}
return this;
},
/**
* Unregisters event listeners for the given type from each element in the
* collection.
*
* @attach {qxWeb}
* @param type {String} Type of the event
* @param listener {Function} Listener callback
* @param context {Object?} Listener callback context
* @param useCapture {Boolean?} Attach the listener to the capturing
* phase if true
* @return {qxWeb} The collection for chaining
*/
off : function(type, listener, context, useCapture) {
var removeAll = (listener === null && context === null);
for (var j=0; j < this.length; j++) {
var el = this[j];
// continue if no listeners are available
if (!el.__listener) {
continue;
}
var types = [];
if (type !== null) {
types.push(type);
} else {
// no type specified, remove all listeners
for (var listenerType in el.__listener) {
types.push(listenerType);
}
}
for (var i=0, l=types.length; i<l; i++) {
for (var id in el.__listener[types[i]]) {
var storedListener = el.__listener[types[i]][id];
if (removeAll || storedListener == listener || storedListener.original == listener) {
// get the stored context
var hasStoredContext = typeof el.__ctx !== "undefined" && el.__ctx[id];
var storedContext;
if (!context && hasStoredContext) {
storedContext = el.__ctx[id];
}
// remove the listener from the emitter
var result = el.$$emitter.off(types[i], storedListener, storedContext || context);
// check if it's a bound listener which means it was a native event
if (removeAll || storedListener.original == listener) {
// remove the native listener
qx.bom.Event.removeNativeListener(el, types[i], storedListener, useCapture);
}
// BUG #9184
// only if the emitter was successfully removed also delete the key in the data structure
if (result !== null) {
delete el.__listener[types[i]][id];
}
if (hasStoredContext) {
delete el.__ctx[id];
}
}
}
// call hooks
var hooks = qx.module.Event.__hooks.off;
// generic
var typeHooks = hooks["*"] || [];
// type specific
if (hooks[type]) {
typeHooks = typeHooks.concat(hooks[type]);
}
for (var k=0, m=typeHooks.length; k<m; k++) {
typeHooks[k](el, type, listener, context);
}
}
}
return this;
},
/**
* Removes all event listeners (or all listeners for a given type) from the
* collection.
*
* @attach {qxWeb}
* @param type {String?} Event type. All listeners will be removed if this is undefined.
* @return {qxWeb} The collection for chaining
*/
allOff : function(type) {
return this.off(type || null, null, null);
},
/**
* Removes the listener with the given id.
* @param id {Number} The id of the listener to remove
* @return {qxWeb} The collection for chaining.
*/
offById : function(id) {
var entry = this[0].$$emitter.getEntryById(id);
return this.off(entry.name, entry.listener.original, entry.ctx, entry.useCapture);
},
/**
* Fire an event of the given type.
*
* @attach {qxWeb}
* @param type {String} Event type
* @param data {var?} Optional data that will be passed to the listener
* callback function.
* @return {qxWeb} The collection for chaining
*/
emit : function(type, data) {
for (var j=0; j < this.length; j++) {
var el = this[j];
if (el.$$emitter) {
el.$$emitter.emit(type, data);
}
}
return this;
},
/**
* Attaches a listener for the given event that will be executed only once.
*
* @attach {qxWeb}
* @param type {String} Type of the event to listen for
* @param listener {Function} Listener callback
* @param context {Object?} Context the callback function will be executed in.
* Default: The element on which the listener was registered
* @return {qxWeb} The collection for chaining
*/
once : function(type, listener, context) {
var self = this;
var wrappedListener = function(data) {
self.off(type, wrappedListener, context);
listener.call(this, data);
};
this.on(type, wrappedListener, context);
return this;
},
/**
* Checks if one or more listeners for the given event type are attached to
* the first element in the collection.
*
* *Important:* Make sure you are handing in the *identical* context object to get
* the correct result. Especially when using a collection instance this is a common pitfall.
*
* @attach {qxWeb}
* @param type {String} Event type, e.g. <code>mousedown</code>
* @param listener {Function?} Event listener to check for.
* @param context {Object?} Context object listener to check for.
* @return {Boolean} <code>true</code> if one or more listeners are attached
*/
hasListener : function(type, listener, context) {
if (!this[0] || !this[0].$$emitter ||
!this[0].$$emitter.getListeners()[type])
{
return false;
}
if (listener) {
var attachedListeners = this[0].$$emitter.getListeners()[type];
for (var i = 0; i < attachedListeners.length; i++) {
var hasListener = false;
if (attachedListeners[i].listener == listener) {
hasListener = true;
}
if (attachedListeners[i].listener.original &&
attachedListeners[i].listener.original == listener) {
hasListener = true;
}
if (hasListener) {
if (context !== undefined) {
if (attachedListeners[i].ctx === context) {
return true;
}
} else {
return true;
}
}
}
return false;
}
return this[0].$$emitter.getListeners()[type].length > 0;
},
/**
* Copies any event listeners that are attached to the elements in the
* collection to the provided target element
*
* @internal
* @param target {Element} Element to attach the copied listeners to
*/
copyEventsTo : function(target) {
// Copy both arrays to make sure the original collections are not manipulated.
// If e.g. the 'target' array contains a DOM node with child nodes we run into
// problems because the 'target' array is flattened within this method.
var source = this.concat();
var targetCopy = target.concat();
// get all children of source and target
for (var i = source.length - 1; i >= 0; i--) {
var descendants = source[i].getElementsByTagName("*");
for (var j=0; j < descendants.length; j++) {
source.push(descendants[j]);
}
}
for (var i = targetCopy.length -1; i >= 0; i--) {
var descendants = targetCopy[i].getElementsByTagName("*");
for (var j=0; j < descendants.length; j++) {
targetCopy.push(descendants[j]);
}
}
// make sure no emitter object has been copied
targetCopy.forEach(function(el) {
el.$$emitter = null;
});
for (var i=0; i < source.length; i++) {
var el = source[i];
if (!el.$$emitter) {
continue;
}
var storage = el.$$emitter.getListeners();
for (var name in storage) {
for (var j = storage[name].length - 1; j >= 0; j--) {
var listener = storage[name][j].listener;
if (listener.original) {
listener = listener.original;
}
qxWeb(targetCopy[i]).on(name, listener, storage[name][j].ctx);
}
}
}
},
/**
* Bind one or two callbacks to the collection.
* If only the first callback is defined the collection
* does react on 'pointerover' only.
*
* @attach {qxWeb}
*
* @param callbackIn {Function} callback when hovering over
* @param callbackOut {Function?} callback when hovering out
* @return {qxWeb} The collection for chaining
*/
hover : function(callbackIn, callbackOut) {
this.on("pointerover", callbackIn, this);
if (qx.lang.Type.isFunction(callbackOut)) {
this.on("pointerout", callbackOut, this);
}
return this;
},
/**
* Adds a listener for the given type and checks if the target fulfills the selector check.
* If the check is successful the callback is executed with the target and event as arguments.
*
* @attach{qxWeb}
*
* @param eventType {String} name of the event to watch out for (attached to the document object)
* @param target {String|Element|Element[]|qxWeb} Selector expression, DOM element,
* Array of DOM elements or collection
* @param callback {Function} function to call if the selector matches.
* The callback will get the target as qxWeb collection and the event as arguments
* @param context {Object?} optional context object to call the callback
* @return {qxWeb} The collection for chaining
*/
onMatchTarget : function(eventType, target, callback, context) {
context = context !== undefined ? context : this;
var listener = function(e){
var eventTarget = qxWeb(e.getTarget());
if (eventTarget.is(target)) {
callback.call(context, eventTarget, qxWeb.object.clone(e));
} else {
var targetToMatch = typeof target == "string" ? this.find(target) : qxWeb(target);
for(var i = 0, l = targetToMatch.length; i < l; i++) {
if(eventTarget.isChildOf(qxWeb(targetToMatch[i]))) {
callback.call(context, eventTarget, qxWeb.object.clone(e));
break;
}
}
}
};
// make sure to store the infos for 'offMatchTarget' at each element of the collection
// to be able to remove the listener separately
this.forEach(function(el) {
var matchTarget = {
type : eventType,
listener : listener,
callback : callback,
context : context
};
if (!el.$$matchTargetInfo) {
el.$$matchTargetInfo = [];
}
el.$$matchTargetInfo.push(matchTarget);
});
this.on(eventType, listener);
return this;
},
/**
* Removes a listener for the given type and selector check.
*
* @attach{qxWeb}
*
* @param eventType {String} name of the event to remove for
* @param target {String|Element|Element[]|qxWeb} Selector expression, DOM element,
* Array of DOM elements or collection
* @param callback {Function} function to remove
* @param context {Object?} optional context object to remove
* @return {qxWeb} The collection for chaining
*/
offMatchTarget : function(eventType, target, callback, context) {
context = context !== undefined ? context : this;
this.forEach(function(el) {
if (el.$$matchTargetInfo && qxWeb.type.get(el.$$matchTargetInfo) == "Array") {
var infos = el.$$matchTargetInfo;
for (var i=infos.length - 1; i>=0; i--) {
var entry = infos[i];
if (entry.type == eventType &&
entry.callback == callback &&
entry.context == context) {
this.off(eventType, entry.listener);
infos.splice(i, 1);
}
}
if (infos.length === 0) {
el.$$matchTargetInfo = null;
}
}
}, this);
return this;
}
},
defer : function(statics) {
qxWeb.$attachAll(this);
// manually attach internal $-methods as they are ignored by the previous method-call
qxWeb.$attachStatic({
"$registerEventNormalization" : statics.$registerEventNormalization,
"$unregisterEventNormalization" : statics.$unregisterEventNormalization,
"$getEventNormalizationRegistry" : statics.$getEventNormalizationRegistry,
"$registerEventHook" : statics.$registerEventHook,
"$unregisterEventHook" : statics.$unregisterEventHook,
"$getEventHookRegistry" : statics.$getEventHookRegistry
});
}
});