Chromium Code Reviews
chromiumcodereview-hr@appspot.gserviceaccount.com (chromiumcodereview-hr) | Please choose your nickname with Settings | Help | Chromium Project | Gerrit Changes | Sign out
(440)

Side by Side Diff: chrome/renderer/resources/extensions/event.js

Issue 307833002: Move some extensions renderer resources to extensions_renderer_resources.grd. (Closed) Base URL: svn://svn.chromium.org/chrome/trunk/src
Patch Set: rebase Created 6 years, 6 months ago
Use n/p to move between diff chunks; N/P to move between comments. Draft comments are only viewable by you.
Jump to:
View unified diff | Download patch | Annotate | Revision Log
OLDNEW
(Empty)
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.
4
5 var eventNatives = requireNative('event_natives');
6 var handleUncaughtException = require('uncaught_exception_handler').handle;
7 var logging = requireNative('logging');
8 var schemaRegistry = requireNative('schema_registry');
9 var sendRequest = require('sendRequest').sendRequest;
10 var utils = require('utils');
11 var validate = require('schemaUtils').validate;
12 var unloadEvent = require('unload_event');
13
14 // Schemas for the rule-style functions on the events API that
15 // only need to be generated occasionally, so populate them lazily.
16 var ruleFunctionSchemas = {
17 // These values are set lazily:
18 // addRules: {},
19 // getRules: {},
20 // removeRules: {}
21 };
22
23 // This function ensures that |ruleFunctionSchemas| is populated.
24 function ensureRuleSchemasLoaded() {
25 if (ruleFunctionSchemas.addRules)
26 return;
27 var eventsSchema = schemaRegistry.GetSchema("events");
28 var eventType = utils.lookup(eventsSchema.types, 'id', 'events.Event');
29
30 ruleFunctionSchemas.addRules =
31 utils.lookup(eventType.functions, 'name', 'addRules');
32 ruleFunctionSchemas.getRules =
33 utils.lookup(eventType.functions, 'name', 'getRules');
34 ruleFunctionSchemas.removeRules =
35 utils.lookup(eventType.functions, 'name', 'removeRules');
36 }
37
38 // A map of event names to the event object that is registered to that name.
39 var attachedNamedEvents = {};
40
41 // An array of all attached event objects, used for detaching on unload.
42 var allAttachedEvents = [];
43
44 // A map of functions that massage event arguments before they are dispatched.
45 // Key is event name, value is function.
46 var eventArgumentMassagers = {};
47
48 // An attachment strategy for events that aren't attached to the browser.
49 // This applies to events with the "unmanaged" option and events without
50 // names.
51 var NullAttachmentStrategy = function(event) {
52 this.event_ = event;
53 };
54 NullAttachmentStrategy.prototype.onAddedListener =
55 function(listener) {
56 };
57 NullAttachmentStrategy.prototype.onRemovedListener =
58 function(listener) {
59 };
60 NullAttachmentStrategy.prototype.detach = function(manual) {
61 };
62 NullAttachmentStrategy.prototype.getListenersByIDs = function(ids) {
63 // |ids| is for filtered events only.
64 return this.event_.listeners;
65 };
66
67 // Handles adding/removing/dispatching listeners for unfiltered events.
68 var UnfilteredAttachmentStrategy = function(event) {
69 this.event_ = event;
70 };
71
72 UnfilteredAttachmentStrategy.prototype.onAddedListener =
73 function(listener) {
74 // Only attach / detach on the first / last listener removed.
75 if (this.event_.listeners.length == 0)
76 eventNatives.AttachEvent(this.event_.eventName);
77 };
78
79 UnfilteredAttachmentStrategy.prototype.onRemovedListener =
80 function(listener) {
81 if (this.event_.listeners.length == 0)
82 this.detach(true);
83 };
84
85 UnfilteredAttachmentStrategy.prototype.detach = function(manual) {
86 eventNatives.DetachEvent(this.event_.eventName, manual);
87 };
88
89 UnfilteredAttachmentStrategy.prototype.getListenersByIDs = function(ids) {
90 // |ids| is for filtered events only.
91 return this.event_.listeners;
92 };
93
94 var FilteredAttachmentStrategy = function(event) {
95 this.event_ = event;
96 this.listenerMap_ = {};
97 };
98
99 FilteredAttachmentStrategy.idToEventMap = {};
100
101 FilteredAttachmentStrategy.prototype.onAddedListener = function(listener) {
102 var id = eventNatives.AttachFilteredEvent(this.event_.eventName,
103 listener.filters || {});
104 if (id == -1)
105 throw new Error("Can't add listener");
106 listener.id = id;
107 this.listenerMap_[id] = listener;
108 FilteredAttachmentStrategy.idToEventMap[id] = this.event_;
109 };
110
111 FilteredAttachmentStrategy.prototype.onRemovedListener = function(listener) {
112 this.detachListener(listener, true);
113 };
114
115 FilteredAttachmentStrategy.prototype.detachListener =
116 function(listener, manual) {
117 if (listener.id == undefined)
118 throw new Error("listener.id undefined - '" + listener + "'");
119 var id = listener.id;
120 delete this.listenerMap_[id];
121 delete FilteredAttachmentStrategy.idToEventMap[id];
122 eventNatives.DetachFilteredEvent(id, manual);
123 };
124
125 FilteredAttachmentStrategy.prototype.detach = function(manual) {
126 for (var i in this.listenerMap_)
127 this.detachListener(this.listenerMap_[i], manual);
128 };
129
130 FilteredAttachmentStrategy.prototype.getListenersByIDs = function(ids) {
131 var result = [];
132 for (var i = 0; i < ids.length; i++)
133 $Array.push(result, this.listenerMap_[ids[i]]);
134 return result;
135 };
136
137 function parseEventOptions(opt_eventOptions) {
138 function merge(dest, src) {
139 for (var k in src) {
140 if (!$Object.hasOwnProperty(dest, k)) {
141 dest[k] = src[k];
142 }
143 }
144 }
145
146 var options = opt_eventOptions || {};
147 merge(options, {
148 // Event supports adding listeners with filters ("filtered events"), for
149 // example as used in the webNavigation API.
150 //
151 // event.addListener(listener, [filter1, filter2]);
152 supportsFilters: false,
153
154 // Events supports vanilla events. Most APIs use these.
155 //
156 // event.addListener(listener);
157 supportsListeners: true,
158
159 // Event supports adding rules ("declarative events") rather than
160 // listeners, for example as used in the declarativeWebRequest API.
161 //
162 // event.addRules([rule1, rule2]);
163 supportsRules: false,
164
165 // Event is unmanaged in that the browser has no knowledge of its
166 // existence; it's never invoked, doesn't keep the renderer alive, and
167 // the bindings system has no knowledge of it.
168 //
169 // Both events created by user code (new chrome.Event()) and messaging
170 // events are unmanaged, though in the latter case the browser *does*
171 // interact indirectly with them via IPCs written by hand.
172 unmanaged: false,
173 });
174 return options;
175 };
176
177 // Event object. If opt_eventName is provided, this object represents
178 // the unique instance of that named event, and dispatching an event
179 // with that name will route through this object's listeners. Note that
180 // opt_eventName is required for events that support rules.
181 //
182 // Example:
183 // var Event = require('event_bindings').Event;
184 // chrome.tabs.onChanged = new Event("tab-changed");
185 // chrome.tabs.onChanged.addListener(function(data) { alert(data); });
186 // Event.dispatch("tab-changed", "hi");
187 // will result in an alert dialog that says 'hi'.
188 //
189 // If opt_eventOptions exists, it is a dictionary that contains the boolean
190 // entries "supportsListeners" and "supportsRules".
191 // If opt_webViewInstanceId exists, it is an integer uniquely identifying a
192 // <webview> tag within the embedder. If it does not exist, then this is an
193 // extension event rather than a <webview> event.
194 var EventImpl = function(opt_eventName, opt_argSchemas, opt_eventOptions,
195 opt_webViewInstanceId) {
196 this.eventName = opt_eventName;
197 this.argSchemas = opt_argSchemas;
198 this.listeners = [];
199 this.eventOptions = parseEventOptions(opt_eventOptions);
200 this.webViewInstanceId = opt_webViewInstanceId || 0;
201
202 if (!this.eventName) {
203 if (this.eventOptions.supportsRules)
204 throw new Error("Events that support rules require an event name.");
205 // Events without names cannot be managed by the browser by definition
206 // (the browser has no way of identifying them).
207 this.eventOptions.unmanaged = true;
208 }
209
210 // Track whether the event has been destroyed to help track down the cause
211 // of http://crbug.com/258526.
212 // This variable will eventually hold the stack trace of the destroy call.
213 // TODO(kalman): Delete this and replace with more sound logic that catches
214 // when events are used without being *attached*.
215 this.destroyed = null;
216
217 if (this.eventOptions.unmanaged)
218 this.attachmentStrategy = new NullAttachmentStrategy(this);
219 else if (this.eventOptions.supportsFilters)
220 this.attachmentStrategy = new FilteredAttachmentStrategy(this);
221 else
222 this.attachmentStrategy = new UnfilteredAttachmentStrategy(this);
223 };
224
225 // callback is a function(args, dispatch). args are the args we receive from
226 // dispatchEvent(), and dispatch is a function(args) that dispatches args to
227 // its listeners.
228 function registerArgumentMassager(name, callback) {
229 if (eventArgumentMassagers[name])
230 throw new Error("Massager already registered for event: " + name);
231 eventArgumentMassagers[name] = callback;
232 }
233
234 // Dispatches a named event with the given argument array. The args array is
235 // the list of arguments that will be sent to the event callback.
236 function dispatchEvent(name, args, filteringInfo) {
237 var listenerIDs = [];
238
239 if (filteringInfo)
240 listenerIDs = eventNatives.MatchAgainstEventFilter(name, filteringInfo);
241
242 var event = attachedNamedEvents[name];
243 if (!event)
244 return;
245
246 var dispatchArgs = function(args) {
247 var result = event.dispatch_(args, listenerIDs);
248 if (result)
249 logging.DCHECK(!result.validationErrors, result.validationErrors);
250 return result;
251 };
252
253 if (eventArgumentMassagers[name])
254 eventArgumentMassagers[name](args, dispatchArgs);
255 else
256 dispatchArgs(args);
257 }
258
259 // Registers a callback to be called when this event is dispatched.
260 EventImpl.prototype.addListener = function(cb, filters) {
261 if (!this.eventOptions.supportsListeners)
262 throw new Error("This event does not support listeners.");
263 if (this.eventOptions.maxListeners &&
264 this.getListenerCount_() >= this.eventOptions.maxListeners) {
265 throw new Error("Too many listeners for " + this.eventName);
266 }
267 if (filters) {
268 if (!this.eventOptions.supportsFilters)
269 throw new Error("This event does not support filters.");
270 if (filters.url && !(filters.url instanceof Array))
271 throw new Error("filters.url should be an array.");
272 if (filters.serviceType &&
273 !(typeof filters.serviceType === 'string')) {
274 throw new Error("filters.serviceType should be a string.")
275 }
276 }
277 var listener = {callback: cb, filters: filters};
278 this.attach_(listener);
279 $Array.push(this.listeners, listener);
280 };
281
282 EventImpl.prototype.attach_ = function(listener) {
283 this.attachmentStrategy.onAddedListener(listener);
284
285 if (this.listeners.length == 0) {
286 allAttachedEvents[allAttachedEvents.length] = this;
287 if (this.eventName) {
288 if (attachedNamedEvents[this.eventName]) {
289 throw new Error("Event '" + this.eventName +
290 "' is already attached.");
291 }
292 attachedNamedEvents[this.eventName] = this;
293 }
294 }
295 };
296
297 // Unregisters a callback.
298 EventImpl.prototype.removeListener = function(cb) {
299 if (!this.eventOptions.supportsListeners)
300 throw new Error("This event does not support listeners.");
301
302 var idx = this.findListener_(cb);
303 if (idx == -1)
304 return;
305
306 var removedListener = $Array.splice(this.listeners, idx, 1)[0];
307 this.attachmentStrategy.onRemovedListener(removedListener);
308
309 if (this.listeners.length == 0) {
310 var i = $Array.indexOf(allAttachedEvents, this);
311 if (i >= 0)
312 delete allAttachedEvents[i];
313 if (this.eventName) {
314 if (!attachedNamedEvents[this.eventName]) {
315 throw new Error(
316 "Event '" + this.eventName + "' is not attached.");
317 }
318 delete attachedNamedEvents[this.eventName];
319 }
320 }
321 };
322
323 // Test if the given callback is registered for this event.
324 EventImpl.prototype.hasListener = function(cb) {
325 if (!this.eventOptions.supportsListeners)
326 throw new Error("This event does not support listeners.");
327 return this.findListener_(cb) > -1;
328 };
329
330 // Test if any callbacks are registered for this event.
331 EventImpl.prototype.hasListeners = function() {
332 return this.getListenerCount_() > 0;
333 };
334
335 // Returns the number of listeners on this event.
336 EventImpl.prototype.getListenerCount_ = function() {
337 if (!this.eventOptions.supportsListeners)
338 throw new Error("This event does not support listeners.");
339 return this.listeners.length;
340 };
341
342 // Returns the index of the given callback if registered, or -1 if not
343 // found.
344 EventImpl.prototype.findListener_ = function(cb) {
345 for (var i = 0; i < this.listeners.length; i++) {
346 if (this.listeners[i].callback == cb) {
347 return i;
348 }
349 }
350
351 return -1;
352 };
353
354 EventImpl.prototype.dispatch_ = function(args, listenerIDs) {
355 if (this.destroyed) {
356 throw new Error(this.eventName + ' was already destroyed at: ' +
357 this.destroyed);
358 }
359 if (!this.eventOptions.supportsListeners)
360 throw new Error("This event does not support listeners.");
361
362 if (this.argSchemas && logging.DCHECK_IS_ON()) {
363 try {
364 validate(args, this.argSchemas);
365 } catch (e) {
366 e.message += ' in ' + this.eventName;
367 throw e;
368 }
369 }
370
371 // Make a copy of the listeners in case the listener list is modified
372 // while dispatching the event.
373 var listeners = $Array.slice(
374 this.attachmentStrategy.getListenersByIDs(listenerIDs));
375
376 var results = [];
377 for (var i = 0; i < listeners.length; i++) {
378 try {
379 var result = this.wrapper.dispatchToListener(listeners[i].callback,
380 args);
381 if (result !== undefined)
382 $Array.push(results, result);
383 } catch (e) {
384 handleUncaughtException(
385 'Error in event handler for ' +
386 (this.eventName ? this.eventName : '(unknown)') +
387 ': ' + e.message + '\nStack trace: ' + e.stack,
388 e);
389 }
390 }
391 if (results.length)
392 return {results: results};
393 }
394
395 // Can be overridden to support custom dispatching.
396 EventImpl.prototype.dispatchToListener = function(callback, args) {
397 return $Function.apply(callback, null, args);
398 }
399
400 // Dispatches this event object to all listeners, passing all supplied
401 // arguments to this function each listener.
402 EventImpl.prototype.dispatch = function(varargs) {
403 return this.dispatch_($Array.slice(arguments), undefined);
404 };
405
406 // Detaches this event object from its name.
407 EventImpl.prototype.detach_ = function() {
408 this.attachmentStrategy.detach(false);
409 };
410
411 EventImpl.prototype.destroy_ = function() {
412 this.listeners.length = 0;
413 this.detach_();
414 this.destroyed = new Error().stack;
415 };
416
417 EventImpl.prototype.addRules = function(rules, opt_cb) {
418 if (!this.eventOptions.supportsRules)
419 throw new Error("This event does not support rules.");
420
421 // Takes a list of JSON datatype identifiers and returns a schema fragment
422 // that verifies that a JSON object corresponds to an array of only these
423 // data types.
424 function buildArrayOfChoicesSchema(typesList) {
425 return {
426 'type': 'array',
427 'items': {
428 'choices': typesList.map(function(el) {return {'$ref': el};})
429 }
430 };
431 };
432
433 // Validate conditions and actions against specific schemas of this
434 // event object type.
435 // |rules| is an array of JSON objects that follow the Rule type of the
436 // declarative extension APIs. |conditions| is an array of JSON type
437 // identifiers that are allowed to occur in the conditions attribute of each
438 // rule. Likewise, |actions| is an array of JSON type identifiers that are
439 // allowed to occur in the actions attribute of each rule.
440 function validateRules(rules, conditions, actions) {
441 var conditionsSchema = buildArrayOfChoicesSchema(conditions);
442 var actionsSchema = buildArrayOfChoicesSchema(actions);
443 $Array.forEach(rules, function(rule) {
444 validate([rule.conditions], [conditionsSchema]);
445 validate([rule.actions], [actionsSchema]);
446 });
447 };
448
449 if (!this.eventOptions.conditions || !this.eventOptions.actions) {
450 throw new Error('Event ' + this.eventName + ' misses ' +
451 'conditions or actions in the API specification.');
452 }
453
454 validateRules(rules,
455 this.eventOptions.conditions,
456 this.eventOptions.actions);
457
458 ensureRuleSchemasLoaded();
459 // We remove the first parameter from the validation to give the user more
460 // meaningful error messages.
461 validate([this.webViewInstanceId, rules, opt_cb],
462 $Array.splice(
463 $Array.slice(ruleFunctionSchemas.addRules.parameters), 1));
464 sendRequest(
465 "events.addRules",
466 [this.eventName, this.webViewInstanceId, rules, opt_cb],
467 ruleFunctionSchemas.addRules.parameters);
468 }
469
470 EventImpl.prototype.removeRules = function(ruleIdentifiers, opt_cb) {
471 if (!this.eventOptions.supportsRules)
472 throw new Error("This event does not support rules.");
473 ensureRuleSchemasLoaded();
474 // We remove the first parameter from the validation to give the user more
475 // meaningful error messages.
476 validate([this.webViewInstanceId, ruleIdentifiers, opt_cb],
477 $Array.splice(
478 $Array.slice(ruleFunctionSchemas.removeRules.parameters), 1));
479 sendRequest("events.removeRules",
480 [this.eventName,
481 this.webViewInstanceId,
482 ruleIdentifiers,
483 opt_cb],
484 ruleFunctionSchemas.removeRules.parameters);
485 }
486
487 EventImpl.prototype.getRules = function(ruleIdentifiers, cb) {
488 if (!this.eventOptions.supportsRules)
489 throw new Error("This event does not support rules.");
490 ensureRuleSchemasLoaded();
491 // We remove the first parameter from the validation to give the user more
492 // meaningful error messages.
493 validate([this.webViewInstanceId, ruleIdentifiers, cb],
494 $Array.splice(
495 $Array.slice(ruleFunctionSchemas.getRules.parameters), 1));
496
497 sendRequest(
498 "events.getRules",
499 [this.eventName, this.webViewInstanceId, ruleIdentifiers, cb],
500 ruleFunctionSchemas.getRules.parameters);
501 }
502
503 unloadEvent.addListener(function() {
504 for (var i = 0; i < allAttachedEvents.length; ++i) {
505 var event = allAttachedEvents[i];
506 if (event)
507 event.detach_();
508 }
509 });
510
511 var Event = utils.expose('Event', EventImpl, { functions: [
512 'addListener',
513 'removeListener',
514 'hasListener',
515 'hasListeners',
516 'dispatchToListener',
517 'dispatch',
518 'addRules',
519 'removeRules',
520 'getRules'
521 ] });
522
523 // NOTE: Event is (lazily) exposed as chrome.Event from dispatcher.cc.
524 exports.Event = Event;
525
526 exports.dispatchEvent = dispatchEvent;
527 exports.parseEventOptions = parseEventOptions;
528 exports.registerArgumentMassager = registerArgumentMassager;
OLDNEW

Powered by Google App Engine
This is Rietveld 408576698