forked from google/jsaction
-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy pathdispatcher.js
501 lines (443 loc) · 16.2 KB
/
dispatcher.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
// Copyright 2005 Google Inc. All Rights Reserved.
goog.provide('jsaction.Dispatcher');
goog.provide('jsaction.Loader');
goog.require('goog.array');
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):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_ = {};
};
/**
* 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'];
// We allow dispatching directly to controller methods defined in IDL. We
// support this here because the jsaction event type, which we need to
// dispatch to the right handler, is not available in ActionFlow.
// The alternative would be to extend the ActionFlow interface and to
// register a corresponding handler.
if (action == '__idl_action') {
var idlMaps = eventInfo['actionElement']['__idl_map'];
if (idlMaps) {
var functionKey = 'action:' + eventInfo['eventType'];
for (var templateKey in idlMaps) {
var idlMap = idlMaps[templateKey];
var idlActionFunction = idlMap[functionKey];
if (idlActionFunction) {
idlActionFunction.call(
eventInfo['actionElement'], eventInfo['event']);
}
}
}
return;
}
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);
}
};
/**
* 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.
* @private
*/
jsaction.Dispatcher.prototype.maybeInvokeLoader_ = function(namespace) {
var loaderInfo = this.loaders_[namespace];
if (!loaderInfo) {
if (this.defaultLoader_ && !(namespace in this.defaultLoaderNamespaces_)) {
this.defaultLoaderNamespaces_[namespace] = true;
this.defaultLoader_(this, namespace);
}
} else if (!loaderInfo.called) {
loaderInfo.loader(this, namespace);
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']);
};
/**
* 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(user): 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. 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.global.setTimeout(goog.bind(function() {
this.eventReplayer_(this.queue_, this);
}, this), 0);
};
/**
* 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_();
};