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

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

Issue 10514013: Filtered events. (Closed) Base URL: svn://svn.chromium.org/chrome/trunk/src
Patch Set: rebase, reland Created 8 years, 5 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
1 // Copyright (c) 2012 The Chromium Authors. All rights reserved. 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 2 // Use of this source code is governed by a BSD-style license that can be
3 // found in the LICENSE file. 3 // found in the LICENSE file.
4 4
5 var eventBindingsNatives = requireNative('event_bindings'); 5 var eventBindingsNatives = requireNative('event_bindings');
6 var AttachEvent = eventBindingsNatives.AttachEvent; 6 var AttachEvent = eventBindingsNatives.AttachEvent;
7 var DetachEvent = eventBindingsNatives.DetachEvent; 7 var DetachEvent = eventBindingsNatives.DetachEvent;
8 var AttachFilteredEvent = eventBindingsNatives.AttachFilteredEvent;
9 var DetachFilteredEvent = eventBindingsNatives.DetachFilteredEvent;
10 var MatchAgainstEventFilter = eventBindingsNatives.MatchAgainstEventFilter;
8 var sendRequest = require('sendRequest').sendRequest; 11 var sendRequest = require('sendRequest').sendRequest;
9 var utils = require('utils'); 12 var utils = require('utils');
10 var validate = require('schemaUtils').validate; 13 var validate = require('schemaUtils').validate;
11 14
12 var chromeHidden = requireNative('chrome_hidden').GetChromeHidden(); 15 var chromeHidden = requireNative('chrome_hidden').GetChromeHidden();
13 var GetExtensionAPIDefinition = 16 var GetExtensionAPIDefinition =
14 requireNative('apiDefinitions').GetExtensionAPIDefinition; 17 requireNative('apiDefinitions').GetExtensionAPIDefinition;
15 18
16 // Schemas for the rule-style functions on the events API that 19 // Schemas for the rule-style functions on the events API that
17 // only need to be generated occasionally, so populate them lazily. 20 // only need to be generated occasionally, so populate them lazily.
(...skipping 50 matching lines...) Expand 10 before | Expand all | Expand 10 after
68 $Array.prototype.toJSON = customizedArrayToJSON; 71 $Array.prototype.toJSON = customizedArrayToJSON;
69 } 72 }
70 } 73 }
71 }; 74 };
72 75
73 this.parse = function(thing) { 76 this.parse = function(thing) {
74 return $jsonParse(thing); 77 return $jsonParse(thing);
75 }; 78 };
76 })(); 79 })();
77 80
81 // A map of event names to the event object that is registered to that name.
82 var attachedNamedEvents = {};
83
84 // An array of all attached event objects, used for detaching on unload.
85 var allAttachedEvents = [];
86
87 // A map of functions that massage event arguments before they are dispatched.
88 // Key is event name, value is function.
89 var eventArgumentMassagers = {};
90
91 // Handles adding/removing/dispatching listeners for unfiltered events.
92 var UnfilteredAttachmentStrategy = function(event) {
93 this.event_ = event;
94 };
95
96 UnfilteredAttachmentStrategy.prototype.onAddedListener =
97 function(listener) {
98 // Only attach / detach on the first / last listener removed.
99 if (this.event_.listeners_.length == 0)
100 AttachEvent(this.event_.eventName_);
101 };
102
103 UnfilteredAttachmentStrategy.prototype.onRemovedListener =
104 function(listener) {
105 if (this.event_.listeners_.length == 0)
106 this.detach(true);
107 };
108
109 UnfilteredAttachmentStrategy.prototype.detach = function(manual) {
110 DetachEvent(this.event_.eventName_, manual);
111 };
112
113 UnfilteredAttachmentStrategy.prototype.getListenersByIDs = function(ids) {
114 return this.event_.listeners_;
115 };
116
117 var FilteredAttachmentStrategy = function(event) {
118 this.event_ = event;
119 this.listenerMap_ = {};
120 };
121
122 FilteredAttachmentStrategy.idToEventMap = {};
123
124 FilteredAttachmentStrategy.prototype.onAddedListener = function(listener) {
125 var id = AttachFilteredEvent(this.event_.eventName_,
126 listener.filters || {});
127 if (id == -1)
128 throw new Error("Can't add listener");
129 listener.id = id;
130 this.listenerMap_[id] = listener;
131 FilteredAttachmentStrategy.idToEventMap[id] = this.event_;
132 };
133
134 FilteredAttachmentStrategy.prototype.onRemovedListener = function(listener) {
135 this.detachListener(listener, true);
136 };
137
138 FilteredAttachmentStrategy.prototype.detachListener =
139 function(listener, manual) {
140 if (listener.id == undefined)
141 throw new Error("listener.id undefined - '" + listener + "'");
142 var id = listener.id;
143 delete this.listenerMap_[id];
144 delete FilteredAttachmentStrategy.idToEventMap[id];
145 DetachFilteredEvent(id, manual);
146 };
147
148 FilteredAttachmentStrategy.prototype.detach = function(manual) {
149 for (var i in this.listenerMap_)
150 this.detachListener(this.listenerMap_[i], manual);
151 };
152
153 FilteredAttachmentStrategy.prototype.getListenersByIDs = function(ids) {
154 var result = [];
155 for (var i = 0; i < ids.length; i++)
156 result.push(this.listenerMap_[ids[i]]);
157 return result;
158 };
159
78 // Event object. If opt_eventName is provided, this object represents 160 // Event object. If opt_eventName is provided, this object represents
79 // the unique instance of that named event, and dispatching an event 161 // the unique instance of that named event, and dispatching an event
80 // with that name will route through this object's listeners. Note that 162 // with that name will route through this object's listeners. Note that
81 // opt_eventName is required for events that support rules. 163 // opt_eventName is required for events that support rules.
82 // 164 //
83 // Example: 165 // Example:
84 // chrome.tabs.onChanged = new chrome.Event("tab-changed"); 166 // chrome.tabs.onChanged = new chrome.Event("tab-changed");
85 // chrome.tabs.onChanged.addListener(function(data) { alert(data); }); 167 // chrome.tabs.onChanged.addListener(function(data) { alert(data); });
86 // chromeHidden.Event.dispatch("tab-changed", "hi"); 168 // chromeHidden.Event.dispatch("tab-changed", "hi");
87 // will result in an alert dialog that says 'hi'. 169 // will result in an alert dialog that says 'hi'.
88 // 170 //
89 // If opt_eventOptions exists, it is a dictionary that contains the boolean 171 // If opt_eventOptions exists, it is a dictionary that contains the boolean
90 // entries "supportsListeners" and "supportsRules". 172 // entries "supportsListeners" and "supportsRules".
91 chrome.Event = function(opt_eventName, opt_argSchemas, opt_eventOptions) { 173 chrome.Event = function(opt_eventName, opt_argSchemas, opt_eventOptions) {
92 this.eventName_ = opt_eventName; 174 this.eventName_ = opt_eventName;
93 this.listeners_ = []; 175 this.listeners_ = [];
94 this.eventOptions_ = opt_eventOptions || 176 this.eventOptions_ = opt_eventOptions ||
95 {"supportsListeners": true, "supportsRules": false}; 177 {supportsFilters: false,
178 supportsListeners: true,
179 supportsRules: false,
180 };
96 181
97 if (this.eventOptions_.supportsRules && !opt_eventName) 182 if (this.eventOptions_.supportsRules && !opt_eventName)
98 throw new Error("Events that support rules require an event name."); 183 throw new Error("Events that support rules require an event name.");
99 184
185 if (this.eventOptions_.supportsFilters) {
186 this.attachmentStrategy_ = new FilteredAttachmentStrategy(this);
187 } else {
188 this.attachmentStrategy_ = new UnfilteredAttachmentStrategy(this);
189 }
190
100 // Validate event arguments (the data that is passed to the callbacks) 191 // Validate event arguments (the data that is passed to the callbacks)
101 // if we are in debug. 192 // if we are in debug.
102 if (opt_argSchemas && 193 if (opt_argSchemas &&
103 chromeHidden.validateCallbacks) { 194 chromeHidden.validateCallbacks) {
104 195
105 this.validateEventArgs_ = function(args) { 196 this.validateEventArgs_ = function(args) {
106 try { 197 try {
107 validate(args, opt_argSchemas); 198 validate(args, opt_argSchemas);
108 } catch (exception) { 199 } catch (exception) {
109 return "Event validation error during " + opt_eventName + " -- " + 200 return "Event validation error during " + opt_eventName + " -- " +
110 exception; 201 exception;
111 } 202 }
112 }; 203 };
113 } else { 204 } else {
114 this.validateEventArgs_ = function() {} 205 this.validateEventArgs_ = function() {}
115 } 206 }
116 }; 207 };
117 208
118 // A map of event names to the event object that is registered to that name.
119 var attachedNamedEvents = {};
120
121 // An array of all attached event objects, used for detaching on unload.
122 var allAttachedEvents = [];
123
124 // A map of functions that massage event arguments before they are dispatched.
125 // Key is event name, value is function.
126 var eventArgumentMassagers = {};
127
128 chromeHidden.Event = {}; 209 chromeHidden.Event = {};
129 210
130 chromeHidden.Event.registerArgumentMassager = function(name, fn) { 211 chromeHidden.Event.registerArgumentMassager = function(name, fn) {
131 if (eventArgumentMassagers[name]) 212 if (eventArgumentMassagers[name])
132 throw new Error("Massager already registered for event: " + name); 213 throw new Error("Massager already registered for event: " + name);
133 eventArgumentMassagers[name] = fn; 214 eventArgumentMassagers[name] = fn;
134 }; 215 };
135 216
136 // Dispatches a named event with the given JSON array, which is deserialized 217 // Dispatches a named event with the given JSON array, which is deserialized
137 // before dispatch. The JSON array is the list of arguments that will be 218 // before dispatch. The JSON array is the list of arguments that will be
138 // sent with the event callback. 219 // sent with the event callback.
139 chromeHidden.Event.dispatchJSON = function(name, args) { 220 chromeHidden.Event.dispatchJSON = function(name, args, filteringInfo) {
221 var listenerIDs = null;
222
223 if (filteringInfo) {
224 listenerIDs = MatchAgainstEventFilter(name, filteringInfo);
225 }
140 if (attachedNamedEvents[name]) { 226 if (attachedNamedEvents[name]) {
141 if (args) { 227 if (args) {
142 // TODO(asargent): This is an antiquity. Until all callers of 228 // TODO(asargent): This is an antiquity. Until all callers of
143 // dispatchJSON use actual values, this must remain here to catch the 229 // dispatchJSON use actual values, this must remain here to catch the
144 // cases where a caller has hard-coded a JSON string to pass in. 230 // cases where a caller has hard-coded a JSON string to pass in.
145 if (typeof(args) == "string") { 231 if (typeof(args) == "string") {
146 args = chromeHidden.JSON.parse(args); 232 args = chromeHidden.JSON.parse(args);
147 } 233 }
148 if (eventArgumentMassagers[name]) 234 if (eventArgumentMassagers[name])
149 eventArgumentMassagers[name](args); 235 eventArgumentMassagers[name](args);
150 } 236 }
151 var result = attachedNamedEvents[name].dispatch.apply( 237
152 attachedNamedEvents[name], args); 238 var event = attachedNamedEvents[name];
239 var result;
240 // TODO(koz): We have to do this differently for unfiltered events (which
241 // have listenerIDs = null) because some bindings write over
242 // event.dispatch (eg: experimental.app.custom_bindings.js) and so expect
243 // events to go through it. These places need to be fixed so that they
244 // expect a listenerIDs parameter.
245 if (listenerIDs)
246 result = event.dispatch_(args, listenerIDs);
247 else
248 result = event.dispatch.apply(event, args);
153 if (result && result.validationErrors) 249 if (result && result.validationErrors)
154 return result.validationErrors; 250 return result.validationErrors;
155 } 251 }
156 }; 252 };
157 253
158 // Dispatches a named event with the given arguments, supplied as an array. 254 // Dispatches a named event with the given arguments, supplied as an array.
159 chromeHidden.Event.dispatch = function(name, args) { 255 chromeHidden.Event.dispatch = function(name, args) {
160 if (attachedNamedEvents[name]) { 256 if (attachedNamedEvents[name]) {
161 attachedNamedEvents[name].dispatch.apply( 257 attachedNamedEvents[name].dispatch.apply(
162 attachedNamedEvents[name], args); 258 attachedNamedEvents[name], args);
163 } 259 }
164 }; 260 };
165 261
166 // Test if a named event has any listeners. 262 // Test if a named event has any listeners.
167 chromeHidden.Event.hasListener = function(name) { 263 chromeHidden.Event.hasListener = function(name) {
168 return (attachedNamedEvents[name] && 264 return (attachedNamedEvents[name] &&
169 attachedNamedEvents[name].listeners_.length > 0); 265 attachedNamedEvents[name].listeners_.length > 0);
170 }; 266 };
171 267
172 // Registers a callback to be called when this event is dispatched. 268 // Registers a callback to be called when this event is dispatched.
173 chrome.Event.prototype.addListener = function(cb) { 269 chrome.Event.prototype.addListener = function(cb, filters) {
174 if (!this.eventOptions_.supportsListeners) 270 if (!this.eventOptions_.supportsListeners)
175 throw new Error("This event does not support listeners."); 271 throw new Error("This event does not support listeners.");
272 if (filters) {
273 if (!this.eventOptions_.supportsFilters)
274 throw new Error("This event does not support filters.");
275 if (filters.url && !(filters.url instanceof Array))
276 throw new Error("filters.url should be an array");
277 }
278 var listener = {callback: cb, filters: filters};
279 this.attach_(listener);
280 this.listeners_.push(listener);
281 };
282
283 chrome.Event.prototype.attach_ = function(listener) {
284 this.attachmentStrategy_.onAddedListener(listener);
176 if (this.listeners_.length == 0) { 285 if (this.listeners_.length == 0) {
177 this.attach_(); 286 allAttachedEvents[allAttachedEvents.length] = this;
287 if (!this.eventName_)
288 return;
289
290 if (attachedNamedEvents[this.eventName_]) {
291 throw new Error("chrome.Event '" + this.eventName_ +
292 "' is already attached.");
293 }
294
295 attachedNamedEvents[this.eventName_] = this;
178 } 296 }
179 this.listeners_.push(cb);
180 }; 297 };
181 298
182 // Unregisters a callback. 299 // Unregisters a callback.
183 chrome.Event.prototype.removeListener = function(cb) { 300 chrome.Event.prototype.removeListener = function(cb) {
184 if (!this.eventOptions_.supportsListeners) 301 if (!this.eventOptions_.supportsListeners)
185 throw new Error("This event does not support listeners."); 302 throw new Error("This event does not support listeners.");
186 var idx = this.findListener_(cb); 303 var idx = this.findListener_(cb);
187 if (idx == -1) { 304 if (idx == -1) {
188 return; 305 return;
189 } 306 }
190 307
191 this.listeners_.splice(idx, 1); 308 var removedListener = this.listeners_.splice(idx, 1)[0];
309 this.attachmentStrategy_.onRemovedListener(removedListener);
310
192 if (this.listeners_.length == 0) { 311 if (this.listeners_.length == 0) {
193 this.detach_(true); 312 var i = allAttachedEvents.indexOf(this);
313 if (i >= 0)
314 delete allAttachedEvents[i];
315 if (!this.eventName_)
316 return;
317
318 if (!attachedNamedEvents[this.eventName_]) {
319 throw new Error("chrome.Event '" + this.eventName_ +
320 "' is not attached.");
321 }
322
323 delete attachedNamedEvents[this.eventName_];
194 } 324 }
195 }; 325 };
196 326
197 // Test if the given callback is registered for this event. 327 // Test if the given callback is registered for this event.
198 chrome.Event.prototype.hasListener = function(cb) { 328 chrome.Event.prototype.hasListener = function(cb) {
199 if (!this.eventOptions_.supportsListeners) 329 if (!this.eventOptions_.supportsListeners)
200 throw new Error("This event does not support listeners."); 330 throw new Error("This event does not support listeners.");
201 return this.findListener_(cb) > -1; 331 return this.findListener_(cb) > -1;
202 }; 332 };
203 333
204 // Test if any callbacks are registered for this event. 334 // Test if any callbacks are registered for this event.
205 chrome.Event.prototype.hasListeners = function() { 335 chrome.Event.prototype.hasListeners = function() {
206 if (!this.eventOptions_.supportsListeners) 336 if (!this.eventOptions_.supportsListeners)
207 throw new Error("This event does not support listeners."); 337 throw new Error("This event does not support listeners.");
208 return this.listeners_.length > 0; 338 return this.listeners_.length > 0;
209 }; 339 };
210 340
211 // Returns the index of the given callback if registered, or -1 if not 341 // Returns the index of the given callback if registered, or -1 if not
212 // found. 342 // found.
213 chrome.Event.prototype.findListener_ = function(cb) { 343 chrome.Event.prototype.findListener_ = function(cb) {
214 for (var i = 0; i < this.listeners_.length; i++) { 344 for (var i = 0; i < this.listeners_.length; i++) {
215 if (this.listeners_[i] == cb) { 345 if (this.listeners_[i].callback == cb) {
216 return i; 346 return i;
217 } 347 }
218 } 348 }
219 349
220 return -1; 350 return -1;
221 }; 351 };
222 352
223 // Dispatches this event object to all listeners, passing all supplied 353 chrome.Event.prototype.dispatch_ = function(args, listenerIDs) {
224 // arguments to this function each listener.
225 chrome.Event.prototype.dispatch = function(varargs) {
226 if (!this.eventOptions_.supportsListeners) 354 if (!this.eventOptions_.supportsListeners)
227 throw new Error("This event does not support listeners."); 355 throw new Error("This event does not support listeners.");
228 var args = Array.prototype.slice.call(arguments);
229 var validationErrors = this.validateEventArgs_(args); 356 var validationErrors = this.validateEventArgs_(args);
230 if (validationErrors) { 357 if (validationErrors) {
231 console.error(validationErrors); 358 console.error(validationErrors);
232 return {validationErrors: validationErrors}; 359 return {validationErrors: validationErrors};
233 } 360 }
361
362 var listeners = this.attachmentStrategy_.getListenersByIDs(listenerIDs);
363
234 var results = []; 364 var results = [];
235 for (var i = 0; i < this.listeners_.length; i++) { 365 for (var i = 0; i < listeners.length; i++) {
236 try { 366 try {
237 var result = this.listeners_[i].apply(null, args); 367 var result = listeners[i].callback.apply(null, args);
238 if (result !== undefined) 368 if (result !== undefined)
239 results.push(result); 369 results.push(result);
240 } catch (e) { 370 } catch (e) {
241 console.error("Error in event handler for '" + this.eventName_ + 371 console.error("Error in event handler for '" + this.eventName_ +
242 "': " + e.message + ' ' + e.stack); 372 "': " + e.message + ' ' + e.stack);
243 } 373 }
244 } 374 }
245 if (results.length) 375 if (results.length)
246 return {results: results}; 376 return {results: results};
247 }; 377 }
248 378
249 // Attaches this event object to its name. Only one object can have a given 379 // Dispatches this event object to all listeners, passing all supplied
250 // name. 380 // arguments to this function each listener.
251 chrome.Event.prototype.attach_ = function() { 381 chrome.Event.prototype.dispatch = function(varargs) {
252 AttachEvent(this.eventName_); 382 return this.dispatch_(Array.prototype.slice.call(arguments), undefined);
253 allAttachedEvents[allAttachedEvents.length] = this;
254 if (!this.eventName_)
255 return;
256
257 if (attachedNamedEvents[this.eventName_]) {
258 throw new Error("chrome.Event '" + this.eventName_ +
259 "' is already attached.");
260 }
261
262 attachedNamedEvents[this.eventName_] = this;
263 }; 383 };
264 384
265 // Detaches this event object from its name. 385 // Detaches this event object from its name.
266 chrome.Event.prototype.detach_ = function(manual) { 386 chrome.Event.prototype.detach_ = function() {
267 var i = allAttachedEvents.indexOf(this); 387 this.attachmentStrategy_.detach(false);
268 if (i >= 0)
269 delete allAttachedEvents[i];
270 DetachEvent(this.eventName_, manual);
271 if (!this.eventName_)
272 return;
273
274 if (!attachedNamedEvents[this.eventName_]) {
275 throw new Error("chrome.Event '" + this.eventName_ +
276 "' is not attached.");
277 }
278
279 delete attachedNamedEvents[this.eventName_];
280 }; 388 };
281 389
282 chrome.Event.prototype.destroy_ = function() { 390 chrome.Event.prototype.destroy_ = function() {
283 this.listeners_ = []; 391 this.listeners_ = [];
284 this.validateEventArgs_ = []; 392 this.validateEventArgs_ = [];
285 this.detach_(false); 393 this.detach_(false);
286 }; 394 };
287 395
288 chrome.Event.prototype.addRules = function(rules, opt_cb) { 396 chrome.Event.prototype.addRules = function(rules, opt_cb) {
289 if (!this.eventOptions_.supportsRules) 397 if (!this.eventOptions_.supportsRules)
(...skipping 66 matching lines...) Expand 10 before | Expand all | Expand 10 after
356 chromeHidden.onUnload = new chrome.Event(); 464 chromeHidden.onUnload = new chrome.Event();
357 465
358 chromeHidden.dispatchOnLoad = 466 chromeHidden.dispatchOnLoad =
359 chromeHidden.onLoad.dispatch.bind(chromeHidden.onLoad); 467 chromeHidden.onLoad.dispatch.bind(chromeHidden.onLoad);
360 468
361 chromeHidden.dispatchOnUnload = function() { 469 chromeHidden.dispatchOnUnload = function() {
362 chromeHidden.onUnload.dispatch(); 470 chromeHidden.onUnload.dispatch();
363 for (var i = 0; i < allAttachedEvents.length; ++i) { 471 for (var i = 0; i < allAttachedEvents.length; ++i) {
364 var event = allAttachedEvents[i]; 472 var event = allAttachedEvents[i];
365 if (event) 473 if (event)
366 event.detach_(false); 474 event.detach_();
367 } 475 }
368 }; 476 };
369 477
370 chromeHidden.dispatchError = function(msg) { 478 chromeHidden.dispatchError = function(msg) {
371 console.error(msg); 479 console.error(msg);
372 }; 480 };
373 481
374 exports.Event = chrome.Event; 482 exports.Event = chrome.Event;
OLDNEW
« no previous file with comments | « chrome/renderer/extensions/extension_dispatcher.cc ('k') | chrome/test/data/extensions/api_test/filtered_events/manifest.json » ('j') | no next file with comments »

Powered by Google App Engine
This is Rietveld 408576698