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

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

Issue 10381143: Add filtered event support to event.js. (Closed) Base URL: svn://svn.chromium.org/chrome/trunk/src
Patch Set: Created 8 years, 7 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;
Matt Perry 2012/05/15 19:28:27 I don't see these in this CL.
koz (OOO until 15th September) 2012/05/16 01:26:12 These are only exercised by the unit test where we
8 10
9 var chromeHidden = requireNative('chrome_hidden').GetChromeHidden(); 11 var chromeHidden = requireNative('chrome_hidden').GetChromeHidden();
10 12
11 // Local implementation of JSON.parse & JSON.stringify that protect us 13 // Local implementation of JSON.parse & JSON.stringify that protect us
12 // from being clobbered by an extension. 14 // from being clobbered by an extension.
13 // 15 //
14 // TODO(aa): This makes me so sad. We shouldn't need it, as we can just pass 16 // TODO(aa): This makes me so sad. We shouldn't need it, as we can just pass
15 // Values directly over IPC without serializing to strings and use 17 // Values directly over IPC without serializing to strings and use
16 // JSONValueConverter. 18 // JSONValueConverter.
17 chromeHidden.JSON = new (function() { 19 chromeHidden.JSON = new (function() {
(...skipping 21 matching lines...) Expand all
39 $Array.prototype.toJSON = customizedArrayToJSON; 41 $Array.prototype.toJSON = customizedArrayToJSON;
40 } 42 }
41 } 43 }
42 }; 44 };
43 45
44 this.parse = function(thing) { 46 this.parse = function(thing) {
45 return $jsonParse(thing); 47 return $jsonParse(thing);
46 }; 48 };
47 })(); 49 })();
48 50
51 // Handles adding/removing/dispatching listeners for unfiltered events.
52 var UnfilteredAttachmentStrategy = function(event) {
53 this.event_ = event;
54 this.eventName_ = event.eventName_;
55 };
56
57 UnfilteredAttachmentStrategy.prototype.onAddedListener =
58 function(listener) {
59 // Only attach / detach on the first / last listener removed.
Matt Perry 2012/05/15 19:28:27 Is this proper formatting? I thought indent should
koz (OOO until 15th September) 2012/05/16 01:26:12 Done.
60 if (this.event_.listeners_.length > 1)
61 return;
62 AttachEvent(this.eventName_);
63 allAttachedEvents[allAttachedEvents.length] = this.event_;
64 if (!this.eventName_)
65 return;
66
67 if (attachedNamedEvents[this.eventName_]) {
68 throw new Error("chrome.Event '" + this.eventName_ +
69 "' is already attached.");
70 }
71
72 attachedNamedEvents[this.eventName_] = this.event_;
73 };
74
75 UnfilteredAttachmentStrategy.prototype.onRemovedListener =
76 function(listener) {
77 if (this.event_.listeners_.length == 0)
78 this.detach(true);
79 };
80
81 UnfilteredAttachmentStrategy.prototype.detach =
82 function(manual) {
83 var i = allAttachedEvents.indexOf(this.event_);
84 if (i >= 0)
85 delete allAttachedEvents[i];
86 DetachEvent(this.eventName_, manual);
87 if (!this.eventName_)
88 return;
89
90 if (!attachedNamedEvents[this.eventName_]) {
91 throw new Error("chrome.Event '" + this.eventName_ +
92 "' is not attached.");
93 }
94
95 delete attachedNamedEvents[this.eventName_];
96 };
97
98 var FilteredAttachmentStrategy = function(event) {
99 this.event_ = event;
100 this.eventName_ = event.eventName_;
101 this.listenerMap_ = {};
102 };
103
104 FilteredAttachmentStrategy.prototype.onAddedListener = function(listener) {
105 var id = AttachFilteredEvent(this.eventName_, listener.filters || {});
106 listener.id = id;
107 this.listenerMap_[id] = listener;
108 };
109
110 FilteredAttachmentStrategy.prototype.onRemovedListener = function(listener) {
111 this.detachListener(listener, true);
112 }
113
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 DetachFilteredEvent(id, manual);
121 };
122
123 FilteredAttachmentStrategy.prototype.detach = function(manual) {
124 for (var i in this.listenerMap_)
125 this.detachListener(this.listenerMap_[i], manual);
126 };
127
49 // Event object. If opt_eventName is provided, this object represents 128 // Event object. If opt_eventName is provided, this object represents
50 // the unique instance of that named event, and dispatching an event 129 // the unique instance of that named event, and dispatching an event
51 // with that name will route through this object's listeners. Note that 130 // with that name will route through this object's listeners. Note that
52 // opt_eventName is required for events that support rules. 131 // opt_eventName is required for events that support rules.
53 // 132 //
54 // Example: 133 // Example:
55 // chrome.tabs.onChanged = new chrome.Event("tab-changed"); 134 // chrome.tabs.onChanged = new chrome.Event("tab-changed");
56 // chrome.tabs.onChanged.addListener(function(data) { alert(data); }); 135 // chrome.tabs.onChanged.addListener(function(data) { alert(data); });
57 // chromeHidden.Event.dispatch("tab-changed", "hi"); 136 // chromeHidden.Event.dispatch("tab-changed", "hi");
58 // will result in an alert dialog that says 'hi'. 137 // will result in an alert dialog that says 'hi'.
59 // 138 //
60 // If opt_eventOptions exists, it is a dictionary that contains the boolean 139 // If opt_eventOptions exists, it is a dictionary that contains the boolean
61 // entries "supportsListeners" and "supportsRules". 140 // entries "supportsListeners" and "supportsRules".
62 chrome.Event = function(opt_eventName, opt_argSchemas, opt_eventOptions) { 141 chrome.Event = function(opt_eventName, opt_argSchemas, opt_eventOptions) {
63 this.eventName_ = opt_eventName; 142 this.eventName_ = opt_eventName;
64 this.listeners_ = []; 143 this.listeners_ = [];
65 this.eventOptions_ = opt_eventOptions || 144 this.eventOptions_ = opt_eventOptions ||
66 {"supportsListeners": true, "supportsRules": false}; 145 {"supportsListeners": true,
146 "supportsRules": false,
147 "supportsFilters": false,
Matt Perry 2012/05/15 19:28:27 nit: quotes are unnecessary
koz (OOO until 15th September) 2012/05/16 01:26:12 Done.
148 };
67 149
68 if (this.eventOptions_.supportsRules && !opt_eventName) 150 if (this.eventOptions_.supportsRules && !opt_eventName)
69 throw new Error("Events that support rules require an event name."); 151 throw new Error("Events that support rules require an event name.");
70 152
153 if (this.eventOptions_.supportsFilters) {
154 this.attachmentStrategy_ = new FilteredAttachmentStrategy(this);
155 } else {
156 this.attachmentStrategy_ = new UnfilteredAttachmentStrategy(this);
157 }
158
71 // Validate event parameters if we are in debug. 159 // Validate event parameters if we are in debug.
72 if (opt_argSchemas && 160 if (opt_argSchemas &&
73 chromeHidden.validateCallbacks && 161 chromeHidden.validateCallbacks &&
74 chromeHidden.validate) { 162 chromeHidden.validate) {
75 163
76 this.validate_ = function(args) { 164 this.validate_ = function(args) {
77 try { 165 try {
78 chromeHidden.validate(args, opt_argSchemas); 166 chromeHidden.validate(args, opt_argSchemas);
79 } catch (exception) { 167 } catch (exception) {
80 return "Event validation error during " + opt_eventName + " -- " + 168 return "Event validation error during " + opt_eventName + " -- " +
(...skipping 48 matching lines...) Expand 10 before | Expand all | Expand 10 after
129 } 217 }
130 }; 218 };
131 219
132 // Test if a named event has any listeners. 220 // Test if a named event has any listeners.
133 chromeHidden.Event.hasListener = function(name) { 221 chromeHidden.Event.hasListener = function(name) {
134 return (attachedNamedEvents[name] && 222 return (attachedNamedEvents[name] &&
135 attachedNamedEvents[name].listeners_.length > 0); 223 attachedNamedEvents[name].listeners_.length > 0);
136 }; 224 };
137 225
138 // Registers a callback to be called when this event is dispatched. 226 // Registers a callback to be called when this event is dispatched.
139 chrome.Event.prototype.addListener = function(cb) { 227 chrome.Event.prototype.addListener = function(cb, filters) {
140 if (!this.eventOptions_.supportsListeners) 228 if (!this.eventOptions_.supportsListeners)
141 throw new Error("This event does not support listeners."); 229 throw new Error("This event does not support listeners.");
142 if (this.listeners_.length == 0) { 230 if (!this.eventOptions_.supportsFilters && filters)
143 this.attach_(); 231 throw new Error("This event does not support filters.");
144 } 232 var listener = {callback: cb, filters: filters};
145 this.listeners_.push(cb); 233 this.listeners_.push(listener);
234 this.attachmentStrategy_.onAddedListener(listener);
146 }; 235 };
147 236
148 // Unregisters a callback. 237 // Unregisters a callback.
149 chrome.Event.prototype.removeListener = function(cb) { 238 chrome.Event.prototype.removeListener = function(cb) {
150 if (!this.eventOptions_.supportsListeners) 239 if (!this.eventOptions_.supportsListeners)
151 throw new Error("This event does not support listeners."); 240 throw new Error("This event does not support listeners.");
152 var idx = this.findListener_(cb); 241 var idx = this.findListener_(cb);
153 if (idx == -1) { 242 if (idx == -1) {
154 return; 243 return;
155 } 244 }
156 245
246 var removedListener = this.listeners_[idx];
157 this.listeners_.splice(idx, 1); 247 this.listeners_.splice(idx, 1);
158 if (this.listeners_.length == 0) { 248 this.attachmentStrategy_.onRemovedListener(removedListener);
159 this.detach_(true);
160 }
161 }; 249 };
162 250
163 // Test if the given callback is registered for this event. 251 // Test if the given callback is registered for this event.
164 chrome.Event.prototype.hasListener = function(cb) { 252 chrome.Event.prototype.hasListener = function(cb) {
165 if (!this.eventOptions_.supportsListeners) 253 if (!this.eventOptions_.supportsListeners)
166 throw new Error("This event does not support listeners."); 254 throw new Error("This event does not support listeners.");
167 return this.findListener_(cb) > -1; 255 return this.findListener_(cb) > -1;
168 }; 256 };
169 257
170 // Test if any callbacks are registered for this event. 258 // Test if any callbacks are registered for this event.
171 chrome.Event.prototype.hasListeners = function() { 259 chrome.Event.prototype.hasListeners = function() {
172 if (!this.eventOptions_.supportsListeners) 260 if (!this.eventOptions_.supportsListeners)
173 throw new Error("This event does not support listeners."); 261 throw new Error("This event does not support listeners.");
174 return this.listeners_.length > 0; 262 return this.listeners_.length > 0;
175 }; 263 };
176 264
177 // Returns the index of the given callback if registered, or -1 if not 265 // Returns the index of the given callback if registered, or -1 if not
178 // found. 266 // found.
179 chrome.Event.prototype.findListener_ = function(cb) { 267 chrome.Event.prototype.findListener_ = function(cb) {
180 for (var i = 0; i < this.listeners_.length; i++) { 268 for (var i = 0; i < this.listeners_.length; i++) {
181 if (this.listeners_[i] == cb) { 269 if (this.listeners_[i].callback == cb) {
182 return i; 270 return i;
183 } 271 }
184 } 272 }
185 273
186 return -1; 274 return -1;
187 }; 275 };
188 276
189 // Dispatches this event object to all listeners, passing all supplied 277 // Dispatches this event object to all listeners, passing all supplied
190 // arguments to this function each listener. 278 // arguments to this function each listener.
191 chrome.Event.prototype.dispatch = function(varargs) { 279 chrome.Event.prototype.dispatch = function(varargs) {
192 if (!this.eventOptions_.supportsListeners) 280 if (!this.eventOptions_.supportsListeners)
193 throw new Error("This event does not support listeners."); 281 throw new Error("This event does not support listeners.");
194 var args = Array.prototype.slice.call(arguments); 282 var args = Array.prototype.slice.call(arguments);
195 var validationErrors = this.validate_(args); 283 var validationErrors = this.validate_(args);
196 if (validationErrors) { 284 if (validationErrors) {
197 return {validationErrors: validationErrors}; 285 return {validationErrors: validationErrors};
198 } 286 }
199 var results = []; 287 var results = [];
200 for (var i = 0; i < this.listeners_.length; i++) { 288 for (var i = 0; i < this.listeners_.length; i++) {
201 try { 289 try {
202 var result = this.listeners_[i].apply(null, args); 290 var result = this.listeners_[i].callback.apply(null, args);
203 if (result !== undefined) 291 if (result !== undefined)
204 results.push(result); 292 results.push(result);
205 } catch (e) { 293 } catch (e) {
206 console.error("Error in event handler for '" + this.eventName_ + 294 console.error("Error in event handler for '" + this.eventName_ +
207 "': " + e.message + ' ' + e.stack); 295 "': " + e.message + ' ' + e.stack);
208 } 296 }
209 } 297 }
210 if (results.length) 298 if (results.length)
211 return {results: results}; 299 return {results: results};
212 }; 300 };
213 301
214 // Attaches this event object to its name. Only one object can have a given
215 // name.
216 chrome.Event.prototype.attach_ = function() {
217 AttachEvent(this.eventName_);
218 allAttachedEvents[allAttachedEvents.length] = this;
219 if (!this.eventName_)
220 return;
221
222 if (attachedNamedEvents[this.eventName_]) {
223 throw new Error("chrome.Event '" + this.eventName_ +
224 "' is already attached.");
225 }
226
227 attachedNamedEvents[this.eventName_] = this;
228 };
229
230 // Detaches this event object from its name. 302 // Detaches this event object from its name.
231 chrome.Event.prototype.detach_ = function(manual) { 303 chrome.Event.prototype.detach = function() {
232 var i = allAttachedEvents.indexOf(this); 304 this.attachmentStrategy_.detach(false);
233 if (i >= 0)
234 delete allAttachedEvents[i];
235 DetachEvent(this.eventName_, manual);
236 if (!this.eventName_)
237 return;
238
239 if (!attachedNamedEvents[this.eventName_]) {
240 throw new Error("chrome.Event '" + this.eventName_ +
241 "' is not attached.");
242 }
243
244 delete attachedNamedEvents[this.eventName_];
245 }; 305 };
246 306
247 chrome.Event.prototype.destroy_ = function() { 307 chrome.Event.prototype.destroy_ = function() {
248 this.listeners_ = []; 308 var listenersToRemove = this.listeners_.slice();
309 for (var listener in listenersToRemove) {
310 this.removeListener(listener);
Matt Perry 2012/05/15 19:28:27 This will result in detach being called with manua
koz (OOO until 15th September) 2012/05/16 01:26:12 Indeed that is what happens and this is dead code.
311 }
249 this.validate_ = []; 312 this.validate_ = [];
250 this.detach_(false);
251 }; 313 };
252 314
253 // Gets the declarative API object, or undefined if this extension doesn't 315 // Gets the declarative API object, or undefined if this extension doesn't
254 // have access to it. 316 // have access to it.
255 // 317 //
256 // This is defined as a function (rather than a variable) because it isn't 318 // This is defined as a function (rather than a variable) because it isn't
257 // accessible until the schema bindings have been generated. 319 // accessible until the schema bindings have been generated.
258 function getDeclarativeAPI() { 320 function getDeclarativeAPI() {
259 return chromeHidden.internalAPIs.declarative; 321 return chromeHidden.internalAPIs.declarative;
260 } 322 }
(...skipping 37 matching lines...) Expand 10 before | Expand all | Expand 10 after
298 chromeHidden.onUnload = new chrome.Event(); 360 chromeHidden.onUnload = new chrome.Event();
299 361
300 chromeHidden.dispatchOnLoad = 362 chromeHidden.dispatchOnLoad =
301 chromeHidden.onLoad.dispatch.bind(chromeHidden.onLoad); 363 chromeHidden.onLoad.dispatch.bind(chromeHidden.onLoad);
302 364
303 chromeHidden.dispatchOnUnload = function() { 365 chromeHidden.dispatchOnUnload = function() {
304 chromeHidden.onUnload.dispatch(); 366 chromeHidden.onUnload.dispatch();
305 for (var i = 0; i < allAttachedEvents.length; ++i) { 367 for (var i = 0; i < allAttachedEvents.length; ++i) {
306 var event = allAttachedEvents[i]; 368 var event = allAttachedEvents[i];
307 if (event) 369 if (event)
308 event.detach_(false); 370 event.detach();
309 } 371 }
310 }; 372 };
311 373
312 chromeHidden.dispatchError = function(msg) { 374 chromeHidden.dispatchError = function(msg) {
313 console.error(msg); 375 console.error(msg);
314 }; 376 };
315 377
316 exports.Event = chrome.Event; 378 exports.Event = chrome.Event;
OLDNEW
« chrome/renderer/extensions/event_unittest.cc ('K') | « chrome/renderer/extensions/event_unittest.cc ('k') | no next file » | no next file with comments »

Powered by Google App Engine
This is Rietveld 408576698