Disable view source for Developer Tools.
[chromium-blink-merge.git] / chrome / renderer / resources / extensions / event.js
blob09a5b3b70c3500a083abe1d73270e7480b41ae15
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:
17 // addRules: {},
18 // getRules: {},
19 // removeRules: {}
22 // This function ensures that |ruleFunctionSchemas| is populated.
23 function ensureRuleSchemasLoaded() {
24 if (ruleFunctionSchemas.addRules)
25 return;
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
49 // names.
50 var NullAttachmentStrategy = function(event) {
51 this.event_ = event;
53 NullAttachmentStrategy.prototype.onAddedListener =
54 function(listener) {
56 NullAttachmentStrategy.prototype.onRemovedListener =
57 function(listener) {
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) {
68 this.event_ = event;
71 UnfilteredAttachmentStrategy.prototype.onAddedListener =
72 function(listener) {
73 // Only attach / detach on the first / last listener removed.
74 if (this.event_.listeners_.length == 0)
75 eventNatives.AttachEvent(privates(this.event_).eventName);
78 UnfilteredAttachmentStrategy.prototype.onRemovedListener =
79 function(listener) {
80 if (this.event_.listeners_.length == 0)
81 this.detach(true);
84 UnfilteredAttachmentStrategy.prototype.detach = function(manual) {
85 eventNatives.DetachEvent(privates(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) {
94 this.event_ = event;
95 this.listenerMap_ = {};
98 FilteredAttachmentStrategy.idToEventMap = {};
100 FilteredAttachmentStrategy.prototype.onAddedListener = function(listener) {
101 var id = eventNatives.AttachFilteredEvent(privates(this.event_).eventName,
102 listener.filters || {});
103 if (id == -1)
104 throw new Error("Can't add listener");
105 listener.id = id;
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) {
130 var result = [];
131 for (var i = 0; i < ids.length; i++)
132 $Array.push(result, this.listenerMap_[ids[i]]);
133 return result;
136 function parseEventOptions(opt_eventOptions) {
137 function merge(dest, src) {
138 for (var k in src) {
139 if (!$Object.hasOwnProperty(dest, k)) {
140 dest[k] = src[k];
145 var options = opt_eventOptions || {};
146 merge(options, {
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.
171 unmanaged: false,
173 return options;
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.
181 // Example:
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 // If opt_webViewInstanceId exists, it is an integer uniquely identifying a
191 // <webview> tag within the embedder. If it does not exist, then this is an
192 // extension event rather than a <webview> event.
193 var Event = function(opt_eventName, opt_argSchemas, opt_eventOptions,
194 opt_webViewInstanceId) {
195 privates(this).eventName = opt_eventName;
196 this.argSchemas_ = opt_argSchemas;
197 this.listeners_ = [];
198 this.eventOptions_ = parseEventOptions(opt_eventOptions);
199 this.webViewInstanceId_ = opt_webViewInstanceId || 0;
201 if (!privates(this).eventName) {
202 if (this.eventOptions_.supportsRules)
203 throw new Error("Events that support rules require an event name.");
204 // Events without names cannot be managed by the browser by definition
205 // (the browser has no way of identifying them).
206 this.eventOptions_.unmanaged = true;
209 // Track whether the event has been destroyed to help track down the cause
210 // of http://crbug.com/258526.
211 // This variable will eventually hold the stack trace of the destroy call.
212 // TODO(kalman): Delete this and replace with more sound logic that catches
213 // when events are used without being *attached*.
214 this.destroyed_ = null;
216 if (this.eventOptions_.unmanaged)
217 this.attachmentStrategy_ = new NullAttachmentStrategy(this);
218 else if (this.eventOptions_.supportsFilters)
219 this.attachmentStrategy_ = new FilteredAttachmentStrategy(this);
220 else
221 this.attachmentStrategy_ = new UnfilteredAttachmentStrategy(this);
224 // callback is a function(args, dispatch). args are the args we receive from
225 // dispatchEvent(), and dispatch is a function(args) that dispatches args to
226 // its listeners.
227 function registerArgumentMassager(name, callback) {
228 if (eventArgumentMassagers[name])
229 throw new Error("Massager already registered for event: " + name);
230 eventArgumentMassagers[name] = callback;
233 // Dispatches a named event with the given argument array. The args array is
234 // the list of arguments that will be sent to the event callback.
235 function dispatchEvent(name, args, filteringInfo) {
236 var listenerIDs = [];
238 if (filteringInfo)
239 listenerIDs = eventNatives.MatchAgainstEventFilter(name, filteringInfo);
241 var event = attachedNamedEvents[name];
242 if (!event)
243 return;
245 var dispatchArgs = function(args) {
246 var result = event.dispatch_(args, listenerIDs);
247 if (result)
248 logging.DCHECK(!result.validationErrors, result.validationErrors);
249 return result;
252 if (eventArgumentMassagers[name])
253 eventArgumentMassagers[name](args, dispatchArgs);
254 else
255 dispatchArgs(args);
258 // Registers a callback to be called when this event is dispatched.
259 Event.prototype.addListener = function(cb, filters) {
260 if (!this.eventOptions_.supportsListeners)
261 throw new Error("This event does not support listeners.");
262 if (this.eventOptions_.maxListeners &&
263 this.getListenerCount() >= this.eventOptions_.maxListeners) {
264 throw new Error("Too many listeners for " + privates(this).eventName);
266 if (filters) {
267 if (!this.eventOptions_.supportsFilters)
268 throw new Error("This event does not support filters.");
269 if (filters.url && !(filters.url instanceof Array))
270 throw new Error("filters.url should be an array.");
271 if (filters.serviceType &&
272 !(typeof filters.serviceType === 'string')) {
273 throw new Error("filters.serviceType should be a string.")
276 var listener = {callback: cb, filters: filters};
277 this.attach_(listener);
278 $Array.push(this.listeners_, listener);
281 Event.prototype.attach_ = function(listener) {
282 this.attachmentStrategy_.onAddedListener(listener);
284 if (this.listeners_.length == 0) {
285 allAttachedEvents[allAttachedEvents.length] = this;
286 if (privates(this).eventName) {
287 if (attachedNamedEvents[privates(this).eventName]) {
288 throw new Error("Event '" + privates(this).eventName +
289 "' is already attached.");
291 attachedNamedEvents[privates(this).eventName] = this;
296 // Unregisters a callback.
297 Event.prototype.removeListener = function(cb) {
298 if (!this.eventOptions_.supportsListeners)
299 throw new Error("This event does not support listeners.");
301 var idx = this.findListener_(cb);
302 if (idx == -1)
303 return;
305 var removedListener = $Array.splice(this.listeners_, idx, 1)[0];
306 this.attachmentStrategy_.onRemovedListener(removedListener);
308 if (this.listeners_.length == 0) {
309 var i = $Array.indexOf(allAttachedEvents, this);
310 if (i >= 0)
311 delete allAttachedEvents[i];
312 if (privates(this).eventName) {
313 if (!attachedNamedEvents[privates(this).eventName]) {
314 throw new Error(
315 "Event '" + privates(this).eventName + "' is not attached.");
317 delete attachedNamedEvents[privates(this).eventName];
322 // Test if the given callback is registered for this event.
323 Event.prototype.hasListener = function(cb) {
324 if (!this.eventOptions_.supportsListeners)
325 throw new Error("This event does not support listeners.");
326 return this.findListener_(cb) > -1;
329 // Test if any callbacks are registered for this event.
330 Event.prototype.hasListeners = function() {
331 return this.getListenerCount() > 0;
334 // Return the number of listeners on this event.
335 Event.prototype.getListenerCount = function() {
336 if (!this.eventOptions_.supportsListeners)
337 throw new Error("This event does not support listeners.");
338 return this.listeners_.length;
341 // Returns the index of the given callback if registered, or -1 if not
342 // found.
343 Event.prototype.findListener_ = function(cb) {
344 for (var i = 0; i < this.listeners_.length; i++) {
345 if (this.listeners_[i].callback == cb) {
346 return i;
350 return -1;
353 Event.prototype.dispatch_ = function(args, listenerIDs) {
354 if (this.destroyed_) {
355 throw new Error(privates(this).eventName + ' was already destroyed at: ' +
356 this.destroyed_);
358 if (!this.eventOptions_.supportsListeners)
359 throw new Error("This event does not support listeners.");
361 if (this.argSchemas_ && logging.DCHECK_IS_ON()) {
362 try {
363 validate(args, this.argSchemas_);
364 } catch (e) {
365 e.message += ' in ' + privates(this).eventName;
366 throw e;
370 // Make a copy of the listeners in case the listener list is modified
371 // while dispatching the event.
372 var listeners = $Array.slice(
373 this.attachmentStrategy_.getListenersByIDs(listenerIDs));
375 var results = [];
376 for (var i = 0; i < listeners.length; i++) {
377 try {
378 var result = this.dispatchToListener(listeners[i].callback, args);
379 if (result !== undefined)
380 $Array.push(results, result);
381 } catch (e) {
382 console.error(
383 'Error in event handler for ' +
384 (privates(this).eventName ? privates(this).eventName : '(unknown)') +
385 ': ' + e.message + '\nStack trace: ' + e.stack);
388 if (results.length)
389 return {results: results};
392 // Can be overridden to support custom dispatching.
393 Event.prototype.dispatchToListener = function(callback, args) {
394 return $Function.apply(callback, null, args);
397 // Dispatches this event object to all listeners, passing all supplied
398 // arguments to this function each listener.
399 Event.prototype.dispatch = function(varargs) {
400 return this.dispatch_($Array.slice(arguments), undefined);
403 // Detaches this event object from its name.
404 Event.prototype.detach_ = function() {
405 this.attachmentStrategy_.detach(false);
408 Event.prototype.destroy_ = function() {
409 this.listeners_.length = 0;
410 this.detach_();
411 this.destroyed_ = new Error().stack;
414 Event.prototype.addRules = function(rules, opt_cb) {
415 if (!this.eventOptions_.supportsRules)
416 throw new Error("This event does not support rules.");
418 // Takes a list of JSON datatype identifiers and returns a schema fragment
419 // that verifies that a JSON object corresponds to an array of only these
420 // data types.
421 function buildArrayOfChoicesSchema(typesList) {
422 return {
423 'type': 'array',
424 'items': {
425 'choices': typesList.map(function(el) {return {'$ref': el};})
430 // Validate conditions and actions against specific schemas of this
431 // event object type.
432 // |rules| is an array of JSON objects that follow the Rule type of the
433 // declarative extension APIs. |conditions| is an array of JSON type
434 // identifiers that are allowed to occur in the conditions attribute of each
435 // rule. Likewise, |actions| is an array of JSON type identifiers that are
436 // allowed to occur in the actions attribute of each rule.
437 function validateRules(rules, conditions, actions) {
438 var conditionsSchema = buildArrayOfChoicesSchema(conditions);
439 var actionsSchema = buildArrayOfChoicesSchema(actions);
440 $Array.forEach(rules, function(rule) {
441 validate([rule.conditions], [conditionsSchema]);
442 validate([rule.actions], [actionsSchema]);
446 if (!this.eventOptions_.conditions || !this.eventOptions_.actions) {
447 throw new Error('Event ' + privates(this).eventName + ' misses ' +
448 'conditions or actions in the API specification.');
451 validateRules(rules,
452 this.eventOptions_.conditions,
453 this.eventOptions_.actions);
455 ensureRuleSchemasLoaded();
456 // We remove the first parameter from the validation to give the user more
457 // meaningful error messages.
458 validate([this.webViewInstanceId_, rules, opt_cb],
459 $Array.splice(
460 $Array.slice(ruleFunctionSchemas.addRules.parameters), 1));
461 sendRequest(
462 "events.addRules",
463 [privates(this).eventName, this.webViewInstanceId_, rules, opt_cb],
464 ruleFunctionSchemas.addRules.parameters);
467 Event.prototype.removeRules = function(ruleIdentifiers, opt_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([this.webViewInstanceId_, ruleIdentifiers, opt_cb],
474 $Array.splice(
475 $Array.slice(ruleFunctionSchemas.removeRules.parameters), 1));
476 sendRequest("events.removeRules",
477 [privates(this).eventName,
478 this.webViewInstanceId_,
479 ruleIdentifiers,
480 opt_cb],
481 ruleFunctionSchemas.removeRules.parameters);
484 Event.prototype.getRules = function(ruleIdentifiers, cb) {
485 if (!this.eventOptions_.supportsRules)
486 throw new Error("This event does not support rules.");
487 ensureRuleSchemasLoaded();
488 // We remove the first parameter from the validation to give the user more
489 // meaningful error messages.
490 validate([this.webViewInstanceId_, ruleIdentifiers, cb],
491 $Array.splice(
492 $Array.slice(ruleFunctionSchemas.getRules.parameters), 1));
494 sendRequest(
495 "events.getRules",
496 [privates(this).eventName, this.webViewInstanceId_, ruleIdentifiers, cb],
497 ruleFunctionSchemas.getRules.parameters);
500 unloadEvent.addListener(function() {
501 for (var i = 0; i < allAttachedEvents.length; ++i) {
502 var event = allAttachedEvents[i];
503 if (event)
504 event.detach_();
508 // NOTE: Event is (lazily) exposed as chrome.Event from dispatcher.cc.
509 exports.Event = Event;
511 exports.dispatchEvent = dispatchEvent;
512 exports.parseEventOptions = parseEventOptions;
513 exports.registerArgumentMassager = registerArgumentMassager;