3
Copyright 2011 Yahoo! Inc. All rights reserved.
4
Licensed under the BSD License.
5
http://yuilibrary.com/license/
7
YUI.add('event-synthetic', function(Y) {
9
/* Define new DOM events that can be subscribed to from Nodes.
12
* @submodule event-synthetic
14
var DOMMap = Y.Env.evt.dom_map,
17
isObject = YLang.isObject,
18
isString = YLang.isString,
19
isArray = YLang.isArray,
20
query = Y.Selector.query,
21
noop = function () {};
24
* <p>The triggering mechanism used by SyntheticEvents.</p>
26
* <p>Implementers should not instantiate these directly. Use the Notifier
27
* provided to the event's implemented <code>on(node, sub, notifier)</code> or
28
* <code>delegate(node, sub, notifier, filter)</code> methods.</p>
30
* @class SyntheticEvent.Notifier
32
* @param handle {EventHandle} the detach handle for the subscription to an
33
* internal custom event used to execute the callback passed to
34
* on(..) or delegate(..)
35
* @param emitFacade {Boolean} take steps to ensure the first arg received by
36
* the subscription callback is an event facade
40
function Notifier(handle, emitFacade) {
42
this.emitFacade = emitFacade;
46
* <p>Executes the subscription callback, passing the firing arguments as the
47
* first parameters to that callback. For events that are configured with
48
* emitFacade=true, it is common practice to pass the triggering DOMEventFacade
49
* as the first parameter. Barring a proper DOMEventFacade or EventFacade
50
* (from a CustomEvent), a new EventFacade will be generated. In that case, if
51
* fire() is called with a simple object, it will be mixed into the facade.
52
* Otherwise, the facade will be prepended to the callback parameters.</p>
54
* <p>For notifiers provided to delegate logic, the first argument should be an
55
* object with a "currentTarget" property to identify what object to
56
* default as 'this' in the callback. Typically this is gleaned from the
57
* DOMEventFacade or EventFacade, but if configured with emitFacade=false, an
58
* object must be provided. In that case, the object will be removed from the
59
* callback parameters.</p>
61
* <p>Additional arguments passed during event subscription will be
62
* automatically added after those passed to fire().</p>
65
* @param e {EventFacade|DOMEventFacade|Object|any} (see description)
66
* @param arg* {any} additional arguments received by all subscriptions
69
Notifier.prototype.fire = function (e) {
70
// first arg to delegate notifier should be an object with currentTarget
71
var args = toArray(arguments, 0, true),
75
thisObj = sub.context,
76
delegate = sub.filter,
80
if (this.emitFacade) {
81
if (!e || !e.preventDefault) {
82
event = ce._getFacade();
84
if (isObject(e) && !e.preventDefault) {
85
Y.mix(event, e, true);
93
event.details = args.slice();
96
event.container = ce.host;
98
} else if (delegate && isObject(e) && e.currentTarget) {
102
sub.context = thisObj || event.currentTarget || ce.host;
103
ret = ce.fire.apply(ce, args);
104
sub.context = thisObj; // reset for future firing
106
// to capture callbacks that return false to stopPropagation.
107
// Useful for delegate implementations
112
* Manager object for synthetic event subscriptions to aggregate multiple synths on the same node without colliding with actual DOM subscription entries in the global map of DOM subscriptions. Also facilitates proper cleanup on page unload.
114
* @class SynthRegistry
116
* @param el {HTMLElement} the DOM element
117
* @param yuid {String} the yuid stamp for the element
118
* @param key {String} the generated id token used to identify an event type +
119
* element in the global DOM subscription map.
122
function SynthRegistry(el, yuid, key) {
129
SynthRegistry.prototype = {
130
constructor: SynthRegistry,
132
// A few object properties to fake the CustomEvent interface for page
133
// unload cleanup. DON'T TOUCH!
139
* Adds a subscription from the Notifier registry.
142
* @param handle {EventHandle} the subscription
145
register: function (handle) {
146
handle.evt.registry = this;
147
this.handles.push(handle);
151
* Removes the subscription from the Notifier registry.
153
* @method _unregisterSub
154
* @param sub {Subscription} the subscription
157
unregister: function (sub) {
158
var handles = this.handles,
159
events = DOMMap[this.domkey],
162
for (i = handles.length - 1; i >= 0; --i) {
163
if (handles[i].sub === sub) {
164
handles.splice(i, 1);
169
// Clean up left over objects when there are no more subscribers.
170
if (!handles.length) {
171
delete events[this.key];
172
if (!Y.Object.size(events)) {
173
delete DOMMap[this.domkey];
179
* Used by the event system's unload cleanup process. When navigating
180
* away from the page, the event system iterates the global map of element
181
* subscriptions and detaches everything using detachAll(). Normally,
182
* the map is populated with custom events, so this object needs to
183
* at least support the detachAll method to duck type its way to
190
detachAll : function () {
191
var handles = this.handles,
201
* <p>Wrapper class for the integration of new events into the YUI event
202
* infrastructure. Don't instantiate this object directly, use
203
* <code>Y.Event.define(type, config)</code>. See that method for details.</p>
205
* <p>Properties that MAY or SHOULD be specified in the configuration are noted
206
* below and in the description of <code>Y.Event.define</code>.</p>
208
* @class SyntheticEvent
210
* @param cfg {Object} Implementation pieces and configuration
212
* @in event-synthetic
214
function SyntheticEvent() {
215
this._init.apply(this, arguments);
218
Y.mix(SyntheticEvent, {
220
SynthRegistry: SynthRegistry,
223
* Returns the array of subscription handles for a node for the given event
224
* type. Passing true as the third argument will create a registry entry
225
* in the event system's DOM map to host the array if one doesn't yet exist.
227
* @method getRegistry
228
* @param node {Node} the node
229
* @param type {String} the event
230
* @param create {Boolean} create a registration entry to host a new array
231
* if one doesn't exist.
237
getRegistry: function (node, type, create) {
240
key = 'event:' + yuid + type + '_synth',
241
events = DOMMap[yuid];
245
events = DOMMap[yuid] = {};
248
events[key] = new SynthRegistry(el, yuid, key);
252
return (events && events[key]) || null;
256
* Alternate <code>_delete()</code> method for the CustomEvent object
257
* created to manage SyntheticEvent subscriptions.
260
* @param sub {Subscription} the subscription to clean up
264
_deleteSub: function (sub) {
266
var synth = this.eventDef,
267
method = (sub.filter) ? 'detachDelegate' : 'detach';
269
this.subscribers = {};
272
synth[method](sub.node, sub, this.notifier, sub.filter);
273
this.registry.unregister(sub);
282
constructor: SyntheticEvent,
285
* Construction logic for the event.
291
var config = this.publishConfig || (this.publishConfig = {});
293
// The notification mechanism handles facade creation
294
this.emitFacade = ('emitFacade' in config) ?
297
config.emitFacade = false;
301
* <p>Implementers MAY provide this method definition.</p>
303
* <p>Implement this function if the event supports a different
304
* subscription signature. This function is used by both
305
* <code>on()</code> and <code>delegate()</code>. The second parameter
306
* indicates that the event is being subscribed via
307
* <code>delegate()</code>.</p>
309
* <p>Implementations must remove extra arguments from the args list
310
* before returning. The required args for <code>on()</code>
311
* subscriptions are</p>
312
* <pre><code>[type, callback, target, context, argN...]</code></pre>
314
* <p>The required args for <code>delegate()</code>
315
* subscriptions are</p>
317
* <pre><code>[type, callback, target, filter, context, argN...]</code></pre>
319
* <p>The return value from this function will be stored on the
320
* subscription in the '_extra' property for reference elsewhere.</p>
322
* @method processArgs
323
* @param args {Array} parmeters passed to Y.on(..) or Y.delegate(..)
324
* @param delegate {Boolean} true if the subscription is from Y.delegate
330
* <p>Implementers MAY override this property.</p>
332
* <p>Whether to prevent multiple subscriptions to this event that are
333
* classified as being the same. By default, this means the subscribed
334
* callback is the same function. See the <code>subMatch</code>
335
* method. Setting this to true will impact performance for high volume
338
* @property preventDups
342
//preventDups : false,
345
* <p>Implementers SHOULD provide this method definition.</p>
347
* Implementation logic for subscriptions done via <code>node.on(type,
348
* fn)</code> or <code>Y.on(type, fn, target)</code>. This
349
* function should set up the monitor(s) that will eventually fire the
350
* event. Typically this involves subscribing to at least one DOM
351
* event. It is recommended to store detach handles from any DOM
352
* subscriptions to make for easy cleanup in the <code>detach</code>
353
* method. Typically these handles are added to the <code>sub</code>
354
* object. Also for SyntheticEvents that leverage a single DOM
355
* subscription under the hood, it is recommended to pass the DOM event
356
* object to <code>notifier.fire(e)</code>. (The event name on the
357
* object will be updated).
360
* @param node {Node} the node the subscription is being applied to
361
* @param sub {Subscription} the object to track this subscription
362
* @param notifier {SyntheticEvent.Notifier} call notifier.fire(..) to
363
* trigger the execution of the subscribers
368
* <p>Implementers SHOULD provide this method definition.</p>
370
* <p>Implementation logic for detaching subscriptions done via
371
* <code>node.on(type, fn)</code>. This function should clean up any
372
* subscriptions made in the <code>on()</code> phase.</p>
375
* @param node {Node} the node the subscription was applied to
376
* @param sub {Subscription} the object tracking this subscription
377
* @param notifier {SyntheticEvent.Notifier} the Notifier used to
378
* trigger the execution of the subscribers
383
* <p>Implementers SHOULD provide this method definition.</p>
385
* <p>Implementation logic for subscriptions done via
386
* <code>node.delegate(type, fn, filter)</code> or
387
* <code>Y.delegate(type, fn, container, filter)</code>. Like with
388
* <code>on()</code> above, this function should monitor the environment
389
* for the event being fired, and trigger subscription execution by
390
* calling <code>notifier.fire(e)</code>.</p>
392
* <p>This function receives a fourth argument, which is the filter
393
* used to identify which Node's are of interest to the subscription.
394
* The filter will be either a boolean function that accepts a target
395
* Node for each hierarchy level as the event bubbles, or a selector
396
* string. To translate selector strings into filter functions, use
397
* <code>Y.delegate.compileFilter(filter)</code>.</p>
400
* @param node {Node} the node the subscription is being applied to
401
* @param sub {Subscription} the object to track this subscription
402
* @param notifier {SyntheticEvent.Notifier} call notifier.fire(..) to
403
* trigger the execution of the subscribers
404
* @param filter {String|Function} Selector string or function that
405
* accepts an event object and returns null, a Node, or an
406
* array of Nodes matching the criteria for processing.
412
* <p>Implementers SHOULD provide this method definition.</p>
414
* <p>Implementation logic for detaching subscriptions done via
415
* <code>node.delegate(type, fn, filter)</code> or
416
* <code>Y.delegate(type, fn, container, filter)</code>. This function
417
* should clean up any subscriptions made in the
418
* <code>delegate()</code> phase.</p>
420
* @method detachDelegate
421
* @param node {Node} the node the subscription was applied to
422
* @param sub {Subscription} the object tracking this subscription
423
* @param notifier {SyntheticEvent.Notifier} the Notifier used to
424
* trigger the execution of the subscribers
425
* @param filter {String|Function} Selector string or function that
426
* accepts an event object and returns null, a Node, or an
427
* array of Nodes matching the criteria for processing.
430
detachDelegate : noop,
433
* Sets up the boilerplate for detaching the event and facilitating the
434
* execution of subscriber callbacks.
437
* @param args {Array} array of arguments passed to
438
* <code>Y.on(...)</code> or <code>Y.delegate(...)</code>
439
* @param delegate {Boolean} true if called from
440
* <code>Y.delegate(...)</code>
441
* @return {EventHandle} the detach handle for this subscription
445
_on: function (args, delegate) {
447
originalArgs = args.slice(),
448
extra = this.processArgs(args, delegate),
450
method = delegate ? 'delegate' : 'on',
453
// Can't just use Y.all because it doesn't support window (yet?)
454
nodes = (isString(selector)) ?
456
toArray(selector || Y.one(Y.config.win));
458
if (!nodes.length && isString(selector)) {
459
handle = Y.on('available', function () {
460
Y.mix(handle, Y[method].apply(Y, originalArgs), true);
466
Y.Array.each(nodes, function (node) {
467
var subArgs = args.slice(),
474
filter = subArgs.splice(3, 1)[0];
477
// (type, fn, el, thisObj, ...) => (fn, thisObj, ...)
478
subArgs.splice(0, 4, subArgs[1], subArgs[3]);
480
if (!this.preventDups ||
481
!this.getSubs(node, args, null, true))
483
handles.push(this._subscribe(node, method, subArgs, extra, filter));
488
return (handles.length === 1) ?
490
new Y.EventHandle(handles);
494
* Creates a new Notifier object for use by this event's
495
* <code>on(...)</code> or <code>delegate(...)</code> implementation
496
* and register the custom event proxy in the DOM system for cleanup.
499
* @param node {Node} the Node hosting the event
500
* @param method {String} "on" or "delegate"
501
* @param args {Array} the subscription arguments passed to either
502
* <code>Y.on(...)</code> or <code>Y.delegate(...)</code>
503
* after running through <code>processArgs(args)</code> to
504
* normalize the argument signature
505
* @param extra {any} Extra data parsed from
506
* <code>processArgs(args)</code>
507
* @param filter {String|Function} the selector string or function
508
* filter passed to <code>Y.delegate(...)</code> (not
509
* present when called from <code>Y.on(...)</code>)
510
* @return {EventHandle}
514
_subscribe: function (node, method, args, extra, filter) {
515
var dispatcher = new Y.CustomEvent(this.type, this.publishConfig),
516
handle = dispatcher.on.apply(dispatcher, args),
517
notifier = new Notifier(handle, this.emitFacade),
518
registry = SyntheticEvent.getRegistry(node, this.type, true),
524
this.applyArgExtras(extra, sub);
530
host : node, // I forget what this is for
531
currentTarget: node, // for generating facades
532
target : node, // for generating facades
533
el : node._node, // For category detach
535
_delete : SyntheticEvent._deleteSub
538
handle.notifier = notifier;
540
registry.register(handle);
542
// Call the implementation's "on" or "delegate" method
543
this[method](node, sub, notifier, filter);
549
* <p>Implementers MAY provide this method definition.</p>
551
* <p>Implement this function if you want extra data extracted during
552
* processArgs to be propagated to subscriptions on a per-node basis.
553
* That is to say, if you call <code>Y.on('xyz', fn, xtra, 'div')</code>
554
* the data returned from processArgs will be shared
555
* across the subscription objects for all the divs. If you want each
556
* subscription to receive unique information, do that processing
559
* <p>The default implementation adds the data extracted by processArgs
560
* to the subscription object as <code>sub._extra</code>.</p>
562
* @method applyArgExtras
563
* @param extra {any} Any extra data extracted from processArgs
564
* @param sub {Subscription} the individual subscription
566
applyArgExtras: function (extra, sub) {
571
* Removes the subscription(s) from the internal subscription dispatch
572
* mechanism. See <code>SyntheticEvent._deleteSub</code>.
575
* @param args {Array} The arguments passed to
576
* <code>node.detach(...)</code>
580
_detach: function (args) {
581
// Can't use Y.all because it doesn't support window (yet?)
582
// TODO: Does Y.all support window now?
583
var target = args[2],
584
els = (isString(target)) ?
585
query(target) : toArray(target),
586
node, i, len, handles, j;
588
// (type, fn, el, context, filter?) => (type, fn, context, filter?)
591
for (i = 0, len = els.length; i < len; ++i) {
592
node = Y.one(els[i]);
595
handles = this.getSubs(node, args);
598
for (j = handles.length - 1; j >= 0; --j) {
607
* Returns the detach handles of subscriptions on a node that satisfy a
608
* search/filter function. By default, the filter used is the
609
* <code>subMatch</code> method.
612
* @param node {Node} the node hosting the event
613
* @param args {Array} the array of original subscription args passed
614
* to <code>Y.on(...)</code> (before
615
* <code>processArgs</code>
616
* @param filter {Function} function used to identify a subscription
617
* for inclusion in the returned array
618
* @param first {Boolean} stop after the first match (used to check for
619
* duplicate subscriptions)
620
* @return {EventHandle[]} detach handles for the matching subscriptions
622
getSubs: function (node, args, filter, first) {
623
var registry = SyntheticEvent.getRegistry(node, this.type),
625
allHandles, i, len, handle;
628
allHandles = registry.handles;
631
filter = this.subMatch;
634
for (i = 0, len = allHandles.length; i < len; ++i) {
635
handle = allHandles[i];
636
if (filter.call(this, handle.sub, args)) {
640
handles.push(allHandles[i]);
646
return handles.length && handles;
650
* <p>Implementers MAY override this to define what constitutes a
651
* "same" subscription. Override implementations should
652
* consider the lack of a comparator as a match, so calling
653
* <code>getSubs()</code> with no arguments will return all subs.</p>
655
* <p>Compares a set of subscription arguments against a Subscription
656
* object to determine if they match. The default implementation
657
* compares the callback function against the second argument passed to
658
* <code>Y.on(...)</code> or <code>node.detach(...)</code> etc.</p>
661
* @param sub {Subscription} the existing subscription
662
* @param args {Array} the calling arguments passed to
663
* <code>Y.on(...)</code> etc.
664
* @return {Boolean} true if the sub can be described by the args
668
subMatch: function (sub, args) {
669
// Default detach cares only about the callback matching
670
return !args[1] || sub.fn === args[1];
675
Y.SyntheticEvent = SyntheticEvent;
678
* <p>Defines a new event in the DOM event system. Implementers are
679
* responsible for monitoring for a scenario whereby the event is fired. A
680
* notifier object is provided to the functions identified below. When the
681
* criteria defining the event are met, call notifier.fire( [args] ); to
682
* execute event subscribers.</p>
684
* <p>The first parameter is the name of the event. The second parameter is a
685
* configuration object which define the behavior of the event system when the
686
* new event is subscribed to or detached from. The methods that should be
687
* defined in this configuration object are <code>on</code>,
688
* <code>detach</code>, <code>delegate</code>, and <code>detachDelegate</code>.
689
* You are free to define any other methods or properties needed to define your
690
* event. Be aware, however, that since the object is used to subclass
691
* SyntheticEvent, you should avoid method names used by SyntheticEvent unless
692
* your intention is to override the default behavior.</p>
694
* <p>This is a list of properties and methods that you can or should specify
695
* in the configuration object:</p>
698
* <dt><code>on</code></dt>
699
* <dd><code>function (node, subscription, notifier)</code> The
700
* implementation logic for subscription. Any special setup you need to
701
* do to create the environment for the event being fired--E.g. native
702
* DOM event subscriptions. Store subscription related objects and
703
* state on the <code>subscription</code> object. When the
704
* criteria have been met to fire the synthetic event, call
705
* <code>notifier.fire(e)</code>. See Notifier's <code>fire()</code>
706
* method for details about what to pass as parameters.</dd>
708
* <dt><code>detach</code></dt>
709
* <dd><code>function (node, subscription, notifier)</code> The
710
* implementation logic for cleaning up a detached subscription. E.g.
711
* detach any DOM subscriptions added in <code>on</code>.</dd>
713
* <dt><code>delegate</code></dt>
714
* <dd><code>function (node, subscription, notifier, filter)</code> The
715
* implementation logic for subscription via <code>Y.delegate</code> or
716
* <code>node.delegate</code>. The filter is typically either a selector
717
* string or a function. You can use
718
* <code>Y.delegate.compileFilter(selectorString)</code> to create a
719
* filter function from a selector string if needed. The filter function
720
* expects an event object as input and should output either null, a
721
* matching Node, or an array of matching Nodes. Otherwise, this acts
722
* like <code>on</code> DOM event subscriptions. Store subscription
723
* related objects and information on the <code>subscription</code>
724
* object. When the criteria have been met to fire the synthetic event,
725
* call <code>notifier.fire(e)</code> as noted above.</dd>
727
* <dt><code>detachDelegate</code></dt>
728
* <dd><code>function (node, subscription, notifier)</code> The
729
* implementation logic for cleaning up a detached delegate subscription.
730
* E.g. detach any DOM delegate subscriptions added in
731
* <code>delegate</code>.</dd>
733
* <dt><code>publishConfig</code></dt>
734
* <dd>(Object) The configuration object that will be used to instantiate
735
* the underlying CustomEvent. See Notifier's <code>fire</code> method
738
* <dt><code>processArgs</code></dt
740
* <p><code>function (argArray, fromDelegate)</code> Optional method
741
* to extract any additional arguments from the subscription
742
* signature. Using this allows <code>on</code> or
743
* <code>delegate</code> signatures like
744
* <code>node.on("hover", overCallback,
745
* outCallback)</code>.</p>
746
* <p>When processing an atypical argument signature, make sure the
747
* args array is returned to the normal signature before returning
748
* from the function. For example, in the "hover" example
749
* above, the <code>outCallback</code> needs to be <code>splice</code>d
750
* out of the array. The expected signature of the args array for
751
* <code>on()</code> subscriptions is:</p>
753
* <code>[type, callback, target, contextOverride, argN...]</code>
755
* <p>And for <code>delegate()</code>:</p>
757
* <code>[type, callback, target, filter, contextOverride, argN...]</code>
759
* <p>where <code>target</code> is the node the event is being
760
* subscribed for. You can see these signatures documented for
761
* <code>Y.on()</code> and <code>Y.delegate()</code> respectively.</p>
762
* <p>Whatever gets returned from the function will be stored on the
763
* <code>subscription</code> object under
764
* <code>subscription._extra</code>.</p></dd>
765
* <dt><code>subMatch</code></dt>
767
* <p><code>function (sub, args)</code> Compares a set of
768
* subscription arguments against a Subscription object to determine
769
* if they match. The default implementation compares the callback
770
* function against the second argument passed to
771
* <code>Y.on(...)</code> or <code>node.detach(...)</code> etc.</p>
776
* @param type {String} the name of the event
777
* @param config {Object} the prototype definition for the new event (see above)
778
* @param force {Boolean} override an existing event (use with caution)
779
* @return {SyntheticEvent} the subclass implementation instance created to
780
* handle event subscriptions of this type
784
* @in event-synthetic
786
Y.Event.define = function (type, config, force) {
787
var eventDef, Impl, synth;
789
if (type && type.type) {
793
eventDef = Y.merge({ type: type }, config);
797
if (force || !Y.Node.DOM_EVENTS[eventDef.type]) {
799
SyntheticEvent.apply(this, arguments);
801
Y.extend(Impl, SyntheticEvent, eventDef);
806
Y.Node.DOM_EVENTS[type] = Y.Env.evt.plugins[type] = {
810
return synth._on(toArray(arguments));
813
delegate: function () {
814
return synth._on(toArray(arguments), true);
817
detach: function () {
818
return synth._detach(toArray(arguments));
823
} else if (isString(type) || isArray(type)) {
824
Y.Array.each(toArray(type), function (t) {
825
Y.Node.DOM_EVENTS[t] = 1;
833
}, '3.4.1' ,{requires:['node-base', 'event-custom-complex']});