jsaction
Version:
Google's event delegation library
502 lines (442 loc) • 16.5 kB
JavaScript
// Copyright 2005 Google Inc. All Rights Reserved.
goog.provide('jsaction.Dispatcher');
goog.provide('jsaction.Loader');
goog.require('goog.array');
goog.require('goog.async.run');
goog.require('goog.functions');
goog.require('goog.object');
goog.require('jsaction.ActionFlow');
goog.require('jsaction.Branch');
goog.require('jsaction.Char');
goog.require('jsaction.event');
/**
* A loader is a function that will do whatever is necessary to register
* handlers for a given namespace. A loader takes a dispatcher and a namespace
* as parameters.
* @typedef {function(!jsaction.Dispatcher,string,jsaction.EventInfo):void}
*/
jsaction.Loader;
/**
* An action for a namespace. It consists of two members:
* accept -- whether the handler can accept the given
* EventInfo immediately. If it returns false, the
* dispatcher will queue the events for later replaying, which
* can be triggered by calling replay().
* handle -- the actual handler for the namespace.
* @typedef {{accept: function(jsaction.EventInfo): boolean,
* handle: function(jsaction.ActionFlow)}}
*/
jsaction.NamespaceAction;
/**
* Receives a DOM event, determines the jsaction associated with the source
* element of the DOM event, and invokes the handler associated with the
* jsaction.
*
* @param {function(jsaction.EventInfo):jsaction.ActionFlow=} opt_flowFactory
* A function that knows how to instantiate an ActionFlow for a particular
* browser event. If not provided, a built-in one is used.
* @param {function(jsaction.EventInfo):Function=} opt_getHandler A function
* that knows how to get the handler for a given event info.
* @constructor
*/
jsaction.Dispatcher = function(opt_flowFactory, opt_getHandler) {
/**
* The actions that are registered for this jsaction.Dispatcher instance.
*
* @type {Object}
* @private
*/
this.actions_ = {};
/**
* A map from namespace to associated actions.
* @type {!Object.<!jsaction.NamespaceAction>}
* @private
*/
this.namespaceActions_ = {};
/**
* A mapping between namespaces and loader functions. We also keep a flag
* indicating whether the loader was called to prevent it being called
* multiple times.
* @type {!Object.<string,{loader: jsaction.Loader, called: boolean}>}
* @private
*/
this.loaders_ = {};
/**
* The default loader to be invoked if no loader is found for a particular
* namespace.
* @type {?jsaction.Loader}
* @private
*/
this.defaultLoader_ = null;
/**
* A list of namespaces already loaded by the default loader. This avoids
* loading them once again. Using Object (with namespaces as keys) instead of
* Array for O(1) search.
* @type {!Object.<boolean>}
* @private
*/
this.defaultLoaderNamespaces_ = {};
/**
* The queue of events.
* @type {!Array.<jsaction.EventInfo>}
* @private
*/
this.queue_ = [];
/**
* The ActionFlow factory.
* @type {function(jsaction.EventInfo):jsaction.ActionFlow}
* @private
*/
this.flowFactory_ = opt_flowFactory || jsaction.Dispatcher.createActionFlow_;
/**
* A function to retrieve the handler function for a given event info.
* @type {function(jsaction.EventInfo):Function|undefined}
* @private
*/
this.getHandler_ = opt_getHandler;
/**
* A map of global event handlers, where each key is an event type.
* @private {!Object.<string, !Array.<function(!Event):(boolean|undefined)>>}
*/
this.globalHandlers_ = {};
/**
* @private {?function(
* !Array.<jsaction.EventInfo>, !jsaction.Dispatcher):void}
*/
this.eventReplayer_ = null;
};
/**
* Receives an event or the event queue from the EventContract. The event
* queue is copied and it attempts to replay.
* If event info is passed in it looks for an action handler that can handle
* the given event. If there is no handler registered queues the event and
* checks if a loader is registered for the given namespace. If so, calls it.
*
* Alternatively, if in global dispatch mode, calls all registered global
* handlers for the appropriate event type.
*
* The three functionalities of this call are deliberately not split into three
* methods (and then declared as an abstract interface), because the interface
* is used by EventContract, which lives in a different jsbinary. Therefore the
* interface between the three is defined entirely in terms that are invariant
* under jscompiler processing (Function and Array, as opposed to a custom type
* with method names).
*
* @param {(jsaction.EventInfo|!Array.<jsaction.EventInfo>)} eventInfo
* The info for the event that triggered this call or the queue of events
* from EventContract.
* @param {boolean=} opt_globalDispatch If true, dispatches a global event
* instead of a regular jsaction handler.
*/
jsaction.Dispatcher.prototype.dispatch = function(
eventInfo, opt_globalDispatch) {
if (goog.isArray(eventInfo)) {
// We received the queued events from EventContract. Copy them and try to
// replay.
this.queue_ = goog.array.clone(eventInfo);
this.replayQueuedEvents_();
return;
}
if (opt_globalDispatch) {
// Skip everything related to jsaction handlers, and execute the global
// handlers.
var ev = eventInfo['event'];
var eventTypeHandlers = this.globalHandlers_[eventInfo['eventType']];
if (eventTypeHandlers) {
var shouldPreventDefault = false;
for (var i = 0, handler; handler = eventTypeHandlers[i++];) {
if (handler(ev) === false) {
shouldPreventDefault = true;
}
}
}
if (shouldPreventDefault) {
jsaction.event.preventDefault(ev);
}
return;
}
var action = eventInfo['action'];
var namespace = jsaction.Dispatcher.getNamespace_(action);
var namespaceAction = this.namespaceActions_[namespace];
var handler;
if (this.getHandler_) {
handler = this.getHandler_(eventInfo);
} else if (!namespaceAction) {
handler = this.actions_[action];
} else if (namespaceAction.accept(eventInfo)) {
handler = namespaceAction.handle;
}
if (handler) {
var stats = this.flowFactory_(
/** @type {jsaction.EventInfo} */ (eventInfo));
handler(stats);
stats.done(jsaction.Branch.MAIN);
return;
}
// No handler was found. Potentially make a copy of the event to extend its
// life and queue it.
var eventCopy = jsaction.event.maybeCopyEvent(eventInfo['event']);
eventInfo['event'] = eventCopy;
this.queue_.push(eventInfo);
if (!namespaceAction) {
// If there is no handler, check if there is a loader available.
// If there already is a handler for the namespace, but it is not
// yet ready to accept the event, then the namespace handler
// might load handlers on its own, and will call replay() later.
this.maybeInvokeLoader_(namespace, eventInfo);
}
};
/**
* Registers a loader function to be called in case a jsaction is encountered
* for which there is no handler registered.
* The loader is expected to register the jsaction handlers for the given
* namespace.
*
* @param {string} actionNamespace The action namespace.
* @param {jsaction.Loader} loaderFn The loader that will install the action
* handlers for this namespace. It takes the dispatcher and the namespace
* as parameters.
*/
jsaction.Dispatcher.prototype.registerLoader = function(
actionNamespace, loaderFn) {
this.loaders_[actionNamespace] = {loader: loaderFn, called: false};
};
/**
* Registers the default loader function to be called if no specific loader
* exists for a given namespace.
*
* @param {jsaction.Loader} loaderFn The loader that will install the action
* handlers for this namespace. It takes the dispatcher and the namespace
* as parameters.
*/
jsaction.Dispatcher.prototype.registerDefaultLoader = function(loaderFn) {
this.defaultLoader_ = loaderFn;
};
/**
* Registers a handler for a whole namespace. The dispatcher will
* dispatch all jsaction for the given namespace to the handler.
*
* Namespace handlers has higher precedence than other handlers/loader.
*
* @param {string} namespace The namespace to register handler on.
* @param {function(jsaction.ActionFlow)} handler The handler function.
* @param {(function(jsaction.EventInfo):boolean)=} opt_accept
* A function that, given the EventInfo, can determine whether
* the event should be immediately handled or be queued. Defaults
* to always returning true.
*/
jsaction.Dispatcher.prototype.registerNamespaceHandler = function(
namespace, handler, opt_accept) {
this.namespaceActions_[namespace] = {
accept: opt_accept || goog.functions.TRUE,
handle: handler
};
};
/**
* Invokes the loader for the namespace if there is one and it wasn't called
* already. The dispatcher is passed as a parameter to the loader. If no
* loader is found for the namespace, invoke the default loader.
*
* @param {string} namespace The namespace.
* @param {jsaction.EventInfo} eventInfo The event info.
* @private
*/
jsaction.Dispatcher.prototype.maybeInvokeLoader_ = function(
namespace, eventInfo) {
var loaderInfo = this.loaders_[namespace];
if (!loaderInfo) {
if (this.defaultLoader_ && !(namespace in this.defaultLoaderNamespaces_)) {
this.defaultLoaderNamespaces_[namespace] = true;
this.defaultLoader_(this, namespace, eventInfo);
}
} else if (!loaderInfo.called) {
loaderInfo.loader(this, namespace, eventInfo);
loaderInfo.called = true;
}
};
/**
* Extracts and returns the namespace from a fully qualified jsaction
* of the form "namespace.actionname".
* @param {string} action The action.
* @return {string} The namespace.
* @private
*/
jsaction.Dispatcher.getNamespace_ = function(action) {
return action.split('.')[0];
};
/**
* Creates a jsaction.ActionFlow to be passed to an action handler.
* @param {jsaction.EventInfo} eventInfo The event info.
* @return {jsaction.ActionFlow} The newly created ActionFlow.
* @private
*/
jsaction.Dispatcher.createActionFlow_ = function(eventInfo) {
return new jsaction.ActionFlow(
eventInfo['action'], eventInfo['actionElement'], eventInfo['event'],
eventInfo['timeStamp'], eventInfo['eventType']);
};
/**
* Registers multiple methods all bound to the same object
* instance. This is a common case: an application module binds
* multiple of its methods under public names to the event contract of
* the application. So we provide a shortcut for it.
* Attempts to replay the queued events after registering the handlers.
*
* @param {string} namespace The namespace of the jsaction name.
* NOTE(user): This is not optional in order to encourage uniform
* naming for all methods registered by a module.
*
* @param {Object} instance The object to bind the methods to. If this
* is null, then the functions are not bound, but directly added
* under the public names.
*
* @param {!Object.<string, function(jsaction.ActionFlow):void>} methods
* A map from public name to functions that will be bound
* to instance and registered as action under the public
* name. I.e. the property names are the public names. The
* property values are the methods of instance.
*/
jsaction.Dispatcher.prototype.registerHandlers = function(
namespace, instance, methods) {
goog.object.forEach(methods, goog.bind(function(method, name) {
var handler = instance ? goog.bind(method, instance) : method;
// Include a '.' separator between namespace name and action name.
// In the case that no namespace name is provided, the jsaction name
// consists of the action name only (no period).
if (namespace) {
var fullName = namespace + jsaction.Char.NAMESPACE_ACTION_SEPARATOR +
name;
this.actions_[fullName] = handler;
} else {
this.actions_[name] = handler;
}
}, this));
this.replayQueuedEvents_();
};
/**
* Unregisters an action. Provided as an easy way to reverse the effects of
* registerHandlers.
* @param {string} namespace The namespace of the jsaction name.
* @param {string} name The action name to unbind.
*/
jsaction.Dispatcher.prototype.unregisterHandler = function(namespace, name) {
var fullName = null;
if (namespace) {
fullName = namespace + jsaction.Char.NAMESPACE_ACTION_SEPARATOR + name;
} else {
fullName = name;
}
delete this.actions_[fullName];
};
/**
* Registers a global event handler.
* @param {string} eventType
* @param {function(!Event):(boolean|undefined)} handler
*/
jsaction.Dispatcher.prototype.registerGlobalHandler = function(
eventType, handler) {
this.globalHandlers_[eventType] = this.globalHandlers_[eventType] || [];
this.globalHandlers_[eventType].push(handler);
};
/**
* Unregisters a global event handler.
* @param {string} eventType
* @param {function(!Event):(boolean|undefined)} handler
*/
jsaction.Dispatcher.prototype.unregisterGlobalHandler = function(
eventType, handler) {
if (this.globalHandlers_[eventType]) {
goog.array.remove(this.globalHandlers_[eventType], handler);
}
};
/**
* Checks whether there is an action registered under the given
* name. This returns true if there is a namespace handler, even
* if it can not yet handle the event.
*
* TODO(chrishenry): Remove this when canDispatch is used everywhere.
*
* @param {string} name Action name.
* @return {boolean} Whether the name is registered.
* @see #canDispatch
*/
jsaction.Dispatcher.prototype.hasAction = function(name) {
return this.actions_.hasOwnProperty(name) ||
this.namespaceActions_.hasOwnProperty(
jsaction.Dispatcher.getNamespace_(name));
};
/**
* Whether this dispatcher can dispatch the event. This can be used by
* event replayer to check whether the dispatcher can replay an event.
* @param {jsaction.EventInfo} eventInfo
* @return {boolean}
*/
jsaction.Dispatcher.prototype.canDispatch = function(eventInfo) {
var name = eventInfo['action'];
if (this.actions_.hasOwnProperty(name)) {
return true;
}
var ns = jsaction.Dispatcher.getNamespace_(name);
if (this.namespaceActions_.hasOwnProperty(ns)) {
return this.namespaceActions_[ns].accept(eventInfo);
}
return false;
};
/**
* Replays queued events, if any. The replaying will happen in its own
* stack once the current flow cedes control. This is done to mimic
* browser event handling.
*/
jsaction.Dispatcher.prototype.replay = function() {
this.replayQueuedEvents_();
};
/**
* Replays queued events, if any. The replaying will happen in its own
* stack once the current flow cedes control. As opposed to the replay()
* method, the replay happens immediately.
*/
jsaction.Dispatcher.prototype.replayNow = function() {
if (!this.eventReplayer_ || goog.array.isEmpty(this.queue_)) {
return;
}
this.eventReplayer_(this.queue_, this);
};
/**
* Replays queued events. The replaying will happen in its own stack once the
* current flow cedes control. This is done to mimic browser event handling.
* @private
*/
jsaction.Dispatcher.prototype.replayQueuedEvents_ = function() {
if (!this.eventReplayer_ || goog.array.isEmpty(this.queue_)) {
return;
}
goog.async.run(function() {
this.eventReplayer_(this.queue_, this);
}, this);
};
/**
* Sets the event replayer, enabling queued events to be replayed when actions
* are bound. After setting the event replayer, tries to replay queued events.
* The event replayer takes as parameters the queue of events and the dispatcher
* (used to check whether actions have handlers registered and can be replayed).
* The event replayer is also responsible for dequeuing events.
*
* Example: An event replayer that replays only the last event.
*
* var dispatcher = new Dispatcher;
* // ...
* dispatcher.setEventReplayer(function(queue, dispatcher) {
* var lastEventInfo = goog.array.peek(queue);
* if (dispatcher.canDispatch(lastEventInfo.action) {
* jsaction.replay.replayEvent(lastEventInfo);
* goog.array.clear(queue);
* }
* });
*
* @param {function(!Array.<jsaction.EventInfo>, !jsaction.Dispatcher):void}
* eventReplayer It allows elements to be replayed and dequeuing.
*/
jsaction.Dispatcher.prototype.setEventReplayer = function(eventReplayer) {
this.eventReplayer_ = eventReplayer;
this.replayQueuedEvents_();
};