1 // Copyright (c) 2012 The Chromium Authors. All rights reserved.
2 // Use of this source code is governed by a BSD-style license that can be
3 // found in the LICENSE file.
5 var eventNatives
= requireNative('event_natives');
6 var logging
= requireNative('logging');
7 var schemaRegistry
= requireNative('schema_registry');
8 var sendRequest
= require('sendRequest').sendRequest
;
9 var utils
= require('utils');
10 var validate
= require('schemaUtils').validate
;
11 var unloadEvent
= require('unload_event');
13 // Schemas for the rule-style functions on the events API that
14 // only need to be generated occasionally, so populate them lazily.
15 var ruleFunctionSchemas
= {
16 // These values are set lazily:
22 // This function ensures that |ruleFunctionSchemas| is populated.
23 function ensureRuleSchemasLoaded() {
24 if (ruleFunctionSchemas
.addRules
)
26 var eventsSchema
= schemaRegistry
.GetSchema("events");
27 var eventType
= utils
.lookup(eventsSchema
.types
, 'id', 'events.Event');
29 ruleFunctionSchemas
.addRules
=
30 utils
.lookup(eventType
.functions
, 'name', 'addRules');
31 ruleFunctionSchemas
.getRules
=
32 utils
.lookup(eventType
.functions
, 'name', 'getRules');
33 ruleFunctionSchemas
.removeRules
=
34 utils
.lookup(eventType
.functions
, 'name', 'removeRules');
37 // A map of event names to the event object that is registered to that name.
38 var attachedNamedEvents
= {};
40 // An array of all attached event objects, used for detaching on unload.
41 var allAttachedEvents
= [];
43 // A map of functions that massage event arguments before they are dispatched.
44 // Key is event name, value is function.
45 var eventArgumentMassagers
= {};
47 // An attachment strategy for events that aren't attached to the browser.
48 // This applies to events with the "unmanaged" option and events without
50 var NullAttachmentStrategy = function(event
) {
53 NullAttachmentStrategy
.prototype.onAddedListener
=
56 NullAttachmentStrategy
.prototype.onRemovedListener
=
59 NullAttachmentStrategy
.prototype.detach = function(manual
) {
61 NullAttachmentStrategy
.prototype.getListenersByIDs = function(ids
) {
62 // |ids| is for filtered events only.
63 return this.event_
.listeners_
;
66 // Handles adding/removing/dispatching listeners for unfiltered events.
67 var UnfilteredAttachmentStrategy = function(event
) {
71 UnfilteredAttachmentStrategy
.prototype.onAddedListener
=
73 // Only attach / detach on the first / last listener removed.
74 if (this.event_
.listeners_
.length
== 0)
75 eventNatives
.AttachEvent(this.event_
.eventName_
);
78 UnfilteredAttachmentStrategy
.prototype.onRemovedListener
=
80 if (this.event_
.listeners_
.length
== 0)
84 UnfilteredAttachmentStrategy
.prototype.detach = function(manual
) {
85 eventNatives
.DetachEvent(this.event_
.eventName_
, manual
);
88 UnfilteredAttachmentStrategy
.prototype.getListenersByIDs = function(ids
) {
89 // |ids| is for filtered events only.
90 return this.event_
.listeners_
;
93 var FilteredAttachmentStrategy = function(event
) {
95 this.listenerMap_
= {};
98 FilteredAttachmentStrategy
.idToEventMap
= {};
100 FilteredAttachmentStrategy
.prototype.onAddedListener = function(listener
) {
101 var id
= eventNatives
.AttachFilteredEvent(this.event_
.eventName_
,
102 listener
.filters
|| {});
104 throw new Error("Can't add listener");
106 this.listenerMap_
[id
] = listener
;
107 FilteredAttachmentStrategy
.idToEventMap
[id
] = this.event_
;
110 FilteredAttachmentStrategy
.prototype.onRemovedListener = function(listener
) {
111 this.detachListener(listener
, true);
114 FilteredAttachmentStrategy
.prototype.detachListener
=
115 function(listener
, manual
) {
116 if (listener
.id
== undefined)
117 throw new Error("listener.id undefined - '" + listener
+ "'");
118 var id
= listener
.id
;
119 delete this.listenerMap_
[id
];
120 delete FilteredAttachmentStrategy
.idToEventMap
[id
];
121 eventNatives
.DetachFilteredEvent(id
, manual
);
124 FilteredAttachmentStrategy
.prototype.detach = function(manual
) {
125 for (var i
in this.listenerMap_
)
126 this.detachListener(this.listenerMap_
[i
], manual
);
129 FilteredAttachmentStrategy
.prototype.getListenersByIDs = function(ids
) {
131 for (var i
= 0; i
< ids
.length
; i
++)
132 $Array
.push(result
, this.listenerMap_
[ids
[i
]]);
136 function parseEventOptions(opt_eventOptions
) {
137 function merge(dest
, src
) {
139 if (!$Object
.hasOwnProperty(dest
, k
)) {
145 var options
= opt_eventOptions
|| {};
147 // Event supports adding listeners with filters ("filtered events"), for
148 // example as used in the webNavigation API.
150 // event.addListener(listener, [filter1, filter2]);
151 supportsFilters
: false,
153 // Events supports vanilla events. Most APIs use these.
155 // event.addListener(listener);
156 supportsListeners
: true,
158 // Event supports adding rules ("declarative events") rather than
159 // listeners, for example as used in the declarativeWebRequest API.
161 // event.addRules([rule1, rule2]);
162 supportsRules
: false,
164 // Event is unmanaged in that the browser has no knowledge of its
165 // existence; it's never invoked, doesn't keep the renderer alive, and
166 // the bindings system has no knowledge of it.
168 // Both events created by user code (new chrome.Event()) and messaging
169 // events are unmanaged, though in the latter case the browser *does*
170 // interact indirectly with them via IPCs written by hand.
176 // Event object. If opt_eventName is provided, this object represents
177 // the unique instance of that named event, and dispatching an event
178 // with that name will route through this object's listeners. Note that
179 // opt_eventName is required for events that support rules.
182 // var Event = require('event_bindings').Event;
183 // chrome.tabs.onChanged = new Event("tab-changed");
184 // chrome.tabs.onChanged.addListener(function(data) { alert(data); });
185 // Event.dispatch("tab-changed", "hi");
186 // will result in an alert dialog that says 'hi'.
188 // If opt_eventOptions exists, it is a dictionary that contains the boolean
189 // entries "supportsListeners" and "supportsRules".
190 var Event = function(opt_eventName
, opt_argSchemas
, opt_eventOptions
) {
191 this.eventName_
= opt_eventName
;
192 this.argSchemas_
= opt_argSchemas
;
193 this.listeners_
= [];
194 this.eventOptions_
= parseEventOptions(opt_eventOptions
);
196 if (!this.eventName_
) {
197 if (this.eventOptions_
.supportsRules
)
198 throw new Error("Events that support rules require an event name.");
199 // Events without names cannot be managed by the browser by definition
200 // (the browser has no way of identifying them).
201 this.eventOptions_
.unmanaged
= true;
204 // Track whether the event has been destroyed to help track down the cause
205 // of http://crbug.com/258526.
206 // This variable will eventually hold the stack trace of the destroy call.
207 // TODO(kalman): Delete this and replace with more sound logic that catches
208 // when events are used without being *attached*.
209 this.destroyed_
= null;
211 if (this.eventOptions_
.unmanaged
)
212 this.attachmentStrategy_
= new NullAttachmentStrategy(this);
213 else if (this.eventOptions_
.supportsFilters
)
214 this.attachmentStrategy_
= new FilteredAttachmentStrategy(this);
216 this.attachmentStrategy_
= new UnfilteredAttachmentStrategy(this);
219 // callback is a function(args, dispatch). args are the args we receive from
220 // dispatchEvent(), and dispatch is a function(args) that dispatches args to
222 function registerArgumentMassager(name
, callback
) {
223 if (eventArgumentMassagers
[name
])
224 throw new Error("Massager already registered for event: " + name
);
225 eventArgumentMassagers
[name
] = callback
;
228 // Dispatches a named event with the given argument array. The args array is
229 // the list of arguments that will be sent to the event callback.
230 function dispatchEvent(name
, args
, filteringInfo
) {
231 var listenerIDs
= null;
234 listenerIDs
= eventNatives
.MatchAgainstEventFilter(name
, filteringInfo
);
236 var event
= attachedNamedEvents
[name
];
240 var dispatchArgs = function(args
) {
241 var result
= event
.dispatch_(args
, listenerIDs
);
243 logging
.DCHECK(!result
.validationErrors
, result
.validationErrors
);
247 if (eventArgumentMassagers
[name
])
248 eventArgumentMassagers
[name
](args
, dispatchArgs
);
253 // Registers a callback to be called when this event is dispatched.
254 Event
.prototype.addListener = function(cb
, filters
) {
255 if (!this.eventOptions_
.supportsListeners
)
256 throw new Error("This event does not support listeners.");
257 if (this.eventOptions_
.maxListeners
&&
258 this.getListenerCount() >= this.eventOptions_
.maxListeners
) {
259 throw new Error("Too many listeners for " + this.eventName_
);
262 if (!this.eventOptions_
.supportsFilters
)
263 throw new Error("This event does not support filters.");
264 if (filters
.url
&& !(filters
.url
instanceof Array
))
265 throw new Error("filters.url should be an array");
267 var listener
= {callback
: cb
, filters
: filters
};
268 this.attach_(listener
);
269 $Array
.push(this.listeners_
, listener
);
272 Event
.prototype.attach_ = function(listener
) {
273 this.attachmentStrategy_
.onAddedListener(listener
);
275 if (this.listeners_
.length
== 0) {
276 allAttachedEvents
[allAttachedEvents
.length
] = this;
277 if (this.eventName_
) {
278 if (attachedNamedEvents
[this.eventName_
]) {
279 throw new Error("Event '" + this.eventName_
+
280 "' is already attached.");
282 attachedNamedEvents
[this.eventName_
] = this;
287 // Unregisters a callback.
288 Event
.prototype.removeListener = function(cb
) {
289 if (!this.eventOptions_
.supportsListeners
)
290 throw new Error("This event does not support listeners.");
292 var idx
= this.findListener_(cb
);
296 var removedListener
= $Array
.splice(this.listeners_
, idx
, 1)[0];
297 this.attachmentStrategy_
.onRemovedListener(removedListener
);
299 if (this.listeners_
.length
== 0) {
300 var i
= allAttachedEvents
.indexOf(this);
302 delete allAttachedEvents
[i
];
303 if (this.eventName_
) {
304 if (!attachedNamedEvents
[this.eventName_
])
305 throw new Error("Event '" + this.eventName_
+ "' is not attached.");
306 delete attachedNamedEvents
[this.eventName_
];
311 // Test if the given callback is registered for this event.
312 Event
.prototype.hasListener = function(cb
) {
313 if (!this.eventOptions_
.supportsListeners
)
314 throw new Error("This event does not support listeners.");
315 return this.findListener_(cb
) > -1;
318 // Test if any callbacks are registered for this event.
319 Event
.prototype.hasListeners = function() {
320 return this.getListenerCount() > 0;
323 // Return the number of listeners on this event.
324 Event
.prototype.getListenerCount = function() {
325 if (!this.eventOptions_
.supportsListeners
)
326 throw new Error("This event does not support listeners.");
327 return this.listeners_
.length
;
330 // Returns the index of the given callback if registered, or -1 if not
332 Event
.prototype.findListener_ = function(cb
) {
333 for (var i
= 0; i
< this.listeners_
.length
; i
++) {
334 if (this.listeners_
[i
].callback
== cb
) {
342 Event
.prototype.dispatch_ = function(args
, listenerIDs
) {
343 if (this.destroyed_
) {
344 throw new Error(this.eventName_
+ ' was already destroyed at: ' +
347 if (!this.eventOptions_
.supportsListeners
)
348 throw new Error("This event does not support listeners.");
350 if (this.argSchemas_
&& logging
.DCHECK_IS_ON()) {
352 validate(args
, this.argSchemas_
);
354 e
.message
+= ' in ' + this.eventName_
;
359 // Make a copy of the listeners in case the listener list is modified
360 // while dispatching the event.
361 var listeners
= $Array
.slice(
362 this.attachmentStrategy_
.getListenersByIDs(listenerIDs
));
365 for (var i
= 0; i
< listeners
.length
; i
++) {
367 var result
= this.dispatchToListener(listeners
[i
].callback
, args
);
368 if (result
!== undefined)
369 $Array
.push(results
, result
);
371 console
.error('Error in event handler for ' +
372 (this.eventName_
? this.eventName_
: '(unknown)') +
377 return {results
: results
};
380 // Can be overridden to support custom dispatching.
381 Event
.prototype.dispatchToListener = function(callback
, args
) {
382 return $Function
.apply(callback
, null, args
);
385 // Dispatches this event object to all listeners, passing all supplied
386 // arguments to this function each listener.
387 Event
.prototype.dispatch = function(varargs
) {
388 return this.dispatch_($Array
.slice(arguments
), undefined);
391 // Detaches this event object from its name.
392 Event
.prototype.detach_ = function() {
393 this.attachmentStrategy_
.detach(false);
396 Event
.prototype.destroy_ = function() {
397 this.listeners_
.length
= 0;
399 this.destroyed_
= new Error().stack
;
402 Event
.prototype.addRules = function(rules
, opt_cb
) {
403 if (!this.eventOptions_
.supportsRules
)
404 throw new Error("This event does not support rules.");
406 // Takes a list of JSON datatype identifiers and returns a schema fragment
407 // that verifies that a JSON object corresponds to an array of only these
409 function buildArrayOfChoicesSchema(typesList
) {
413 'choices': typesList
.map(function(el
) {return {'$ref': el
};})
418 // Validate conditions and actions against specific schemas of this
419 // event object type.
420 // |rules| is an array of JSON objects that follow the Rule type of the
421 // declarative extension APIs. |conditions| is an array of JSON type
422 // identifiers that are allowed to occur in the conditions attribute of each
423 // rule. Likewise, |actions| is an array of JSON type identifiers that are
424 // allowed to occur in the actions attribute of each rule.
425 function validateRules(rules
, conditions
, actions
) {
426 var conditionsSchema
= buildArrayOfChoicesSchema(conditions
);
427 var actionsSchema
= buildArrayOfChoicesSchema(actions
);
428 $Array
.forEach(rules
, function(rule
) {
429 validate([rule
.conditions
], [conditionsSchema
]);
430 validate([rule
.actions
], [actionsSchema
]);
434 if (!this.eventOptions_
.conditions
|| !this.eventOptions_
.actions
) {
435 throw new Error('Event ' + this.eventName_
+ ' misses conditions or ' +
436 'actions in the API specification.');
440 this.eventOptions_
.conditions
,
441 this.eventOptions_
.actions
);
443 ensureRuleSchemasLoaded();
444 // We remove the first parameter from the validation to give the user more
445 // meaningful error messages.
446 validate([rules
, opt_cb
],
448 $Array
.slice(ruleFunctionSchemas
.addRules
.parameters
), 1));
449 sendRequest("events.addRules", [this.eventName_
, rules
, opt_cb
],
450 ruleFunctionSchemas
.addRules
.parameters
);
453 Event
.prototype.removeRules = function(ruleIdentifiers
, opt_cb
) {
454 if (!this.eventOptions_
.supportsRules
)
455 throw new Error("This event does not support rules.");
456 ensureRuleSchemasLoaded();
457 // We remove the first parameter from the validation to give the user more
458 // meaningful error messages.
459 validate([ruleIdentifiers
, opt_cb
],
461 $Array
.slice(ruleFunctionSchemas
.removeRules
.parameters
), 1));
462 sendRequest("events.removeRules",
463 [this.eventName_
, ruleIdentifiers
, opt_cb
],
464 ruleFunctionSchemas
.removeRules
.parameters
);
467 Event
.prototype.getRules = function(ruleIdentifiers
, cb
) {
468 if (!this.eventOptions_
.supportsRules
)
469 throw new Error("This event does not support rules.");
470 ensureRuleSchemasLoaded();
471 // We remove the first parameter from the validation to give the user more
472 // meaningful error messages.
473 validate([ruleIdentifiers
, cb
],
475 $Array
.slice(ruleFunctionSchemas
.getRules
.parameters
), 1));
477 sendRequest("events.getRules",
478 [this.eventName_
, ruleIdentifiers
, cb
],
479 ruleFunctionSchemas
.getRules
.parameters
);
482 unloadEvent
.addListener(function() {
483 for (var i
= 0; i
< allAttachedEvents
.length
; ++i
) {
484 var event
= allAttachedEvents
[i
];
490 // NOTE: Event is (lazily) exposed as chrome.Event from dispatcher.cc.
491 exports
.Event
= Event
;
493 exports
.dispatchEvent
= dispatchEvent
;
494 exports
.parseEventOptions
= parseEventOptions
;
495 exports
.registerArgumentMassager
= registerArgumentMassager
;