OLD | NEW |
---|---|
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 Loading... | |
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 // Handles adding/removing/dispatching listeners for unfiltered events. | |
82 var UnfilteredAttachmentStrategy = function(event) { | |
83 this.event_ = event; | |
84 }; | |
85 | |
86 UnfilteredAttachmentStrategy.prototype.onAddedListener = | |
87 function(listener) { | |
88 // Only attach / detach on the first / last listener removed. | |
89 if (this.event_.listeners_.length == 0) | |
90 AttachEvent(this.event_.eventName_); | |
91 }; | |
92 | |
93 UnfilteredAttachmentStrategy.prototype.onRemovedListener = | |
94 function(listener) { | |
95 if (this.event_.listeners_.length == 0) | |
96 this.detach(true); | |
97 }; | |
98 | |
99 UnfilteredAttachmentStrategy.prototype.detach = function(manual) { | |
100 var i = allAttachedEvents.indexOf(this.event_); | |
battre
2012/06/20 09:50:46
where is this declared?
koz (OOO until 15th September)
2012/06/21 02:23:11
It was declared lower down, but I've moved above t
| |
101 if (i >= 0) | |
102 delete allAttachedEvents[i]; | |
103 DetachEvent(this.event_.eventName_, manual); | |
104 }; | |
105 | |
106 UnfilteredAttachmentStrategy.prototype.getListenersByIDs = function(ids) { | |
107 return this.event_.listeners_; | |
108 }; | |
109 | |
110 var FilteredAttachmentStrategy = function(event) { | |
111 this.event_ = event; | |
112 this.listenerMap_ = {}; | |
113 }; | |
114 | |
115 FilteredAttachmentStrategy.idToEventMap = {}; | |
116 | |
117 FilteredAttachmentStrategy.prototype.onAddedListener = function(listener) { | |
118 var id = AttachFilteredEvent(this.event_.eventName_, | |
119 listener.filters || {}); | |
120 if (id == -1) | |
121 throw new Error("Can't add listener"); | |
122 listener.id = id; | |
123 this.listenerMap_[id] = listener; | |
124 FilteredAttachmentStrategy.idToEventMap[id] = this.event_; | |
125 }; | |
126 | |
127 FilteredAttachmentStrategy.prototype.onRemovedListener = function(listener) { | |
128 this.detachListener(listener, true); | |
129 }; | |
130 | |
131 FilteredAttachmentStrategy.prototype.detachListener = | |
132 function(listener, manual) { | |
133 if (listener.id == undefined) | |
134 throw new Error("listener.id undefined - '" + listener + "'"); | |
135 var id = listener.id; | |
136 delete this.listenerMap_[id]; | |
137 delete FilteredAttachmentStrategy.idToEventMap[id]; | |
138 DetachFilteredEvent(id, manual); | |
139 }; | |
140 | |
141 FilteredAttachmentStrategy.prototype.detach = function(manual) { | |
142 for (var i in this.listenerMap_) | |
143 this.detachListener(this.listenerMap_[i], manual); | |
144 }; | |
145 | |
146 FilteredAttachmentStrategy.prototype.getListenersByIDs = function(ids) { | |
147 var result = []; | |
148 for (var i = 0; i < ids.length; i++) | |
149 result.push(this.listenerMap_[ids[i]]); | |
150 return result; | |
151 }; | |
152 | |
78 // Event object. If opt_eventName is provided, this object represents | 153 // Event object. If opt_eventName is provided, this object represents |
79 // the unique instance of that named event, and dispatching an event | 154 // the unique instance of that named event, and dispatching an event |
80 // with that name will route through this object's listeners. Note that | 155 // with that name will route through this object's listeners. Note that |
81 // opt_eventName is required for events that support rules. | 156 // opt_eventName is required for events that support rules. |
82 // | 157 // |
83 // Example: | 158 // Example: |
84 // chrome.tabs.onChanged = new chrome.Event("tab-changed"); | 159 // chrome.tabs.onChanged = new chrome.Event("tab-changed"); |
85 // chrome.tabs.onChanged.addListener(function(data) { alert(data); }); | 160 // chrome.tabs.onChanged.addListener(function(data) { alert(data); }); |
86 // chromeHidden.Event.dispatch("tab-changed", "hi"); | 161 // chromeHidden.Event.dispatch("tab-changed", "hi"); |
87 // will result in an alert dialog that says 'hi'. | 162 // will result in an alert dialog that says 'hi'. |
88 // | 163 // |
89 // If opt_eventOptions exists, it is a dictionary that contains the boolean | 164 // If opt_eventOptions exists, it is a dictionary that contains the boolean |
90 // entries "supportsListeners" and "supportsRules". | 165 // entries "supportsListeners" and "supportsRules". |
91 chrome.Event = function(opt_eventName, opt_argSchemas, opt_eventOptions) { | 166 chrome.Event = function(opt_eventName, opt_argSchemas, opt_eventOptions) { |
92 this.eventName_ = opt_eventName; | 167 this.eventName_ = opt_eventName; |
93 this.listeners_ = []; | 168 this.listeners_ = []; |
94 this.eventOptions_ = opt_eventOptions || | 169 this.eventOptions_ = opt_eventOptions || |
95 {"supportsListeners": true, "supportsRules": false}; | 170 {supportsFilters: false, |
171 supportsListeners: true, | |
172 supportsRules: false, | |
173 }; | |
96 | 174 |
97 if (this.eventOptions_.supportsRules && !opt_eventName) | 175 if (this.eventOptions_.supportsRules && !opt_eventName) |
98 throw new Error("Events that support rules require an event name."); | 176 throw new Error("Events that support rules require an event name."); |
99 | 177 |
178 if (this.eventOptions_.supportsFilters) { | |
179 this.attachmentStrategy_ = new FilteredAttachmentStrategy(this); | |
180 } else { | |
181 this.attachmentStrategy_ = new UnfilteredAttachmentStrategy(this); | |
182 } | |
183 | |
100 // Validate event arguments (the data that is passed to the callbacks) | 184 // Validate event arguments (the data that is passed to the callbacks) |
101 // if we are in debug. | 185 // if we are in debug. |
102 if (opt_argSchemas && | 186 if (opt_argSchemas && |
103 chromeHidden.validateCallbacks) { | 187 chromeHidden.validateCallbacks) { |
104 | 188 |
105 this.validateEventArgs_ = function(args) { | 189 this.validateEventArgs_ = function(args) { |
106 try { | 190 try { |
107 validate(args, opt_argSchemas); | 191 validate(args, opt_argSchemas); |
108 } catch (exception) { | 192 } catch (exception) { |
109 return "Event validation error during " + opt_eventName + " -- " + | 193 return "Event validation error during " + opt_eventName + " -- " + |
(...skipping 19 matching lines...) Expand all Loading... | |
129 | 213 |
130 chromeHidden.Event.registerArgumentMassager = function(name, fn) { | 214 chromeHidden.Event.registerArgumentMassager = function(name, fn) { |
131 if (eventArgumentMassagers[name]) | 215 if (eventArgumentMassagers[name]) |
132 throw new Error("Massager already registered for event: " + name); | 216 throw new Error("Massager already registered for event: " + name); |
133 eventArgumentMassagers[name] = fn; | 217 eventArgumentMassagers[name] = fn; |
134 }; | 218 }; |
135 | 219 |
136 // Dispatches a named event with the given JSON array, which is deserialized | 220 // 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 | 221 // before dispatch. The JSON array is the list of arguments that will be |
138 // sent with the event callback. | 222 // sent with the event callback. |
139 chromeHidden.Event.dispatchJSON = function(name, args) { | 223 chromeHidden.Event.dispatchJSON = function(name, args, filteringInfo) { |
224 var listenerIDs; | |
225 | |
226 if (filteringInfo) { | |
227 listenerIDs = MatchAgainstEventFilter(name, filteringInfo); | |
battre
2012/06/20 09:50:46
would it make sense to change this logic such that
koz (OOO until 15th September)
2012/06/21 02:23:11
Yes, that would work, but it wouldn't save a round
battre
2012/06/21 05:35:16
SGTM
| |
228 } | |
140 if (attachedNamedEvents[name]) { | 229 if (attachedNamedEvents[name]) { |
141 if (args) { | 230 if (args) { |
142 // TODO(asargent): This is an antiquity. Until all callers of | 231 // TODO(asargent): This is an antiquity. Until all callers of |
143 // dispatchJSON use actual values, this must remain here to catch the | 232 // 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. | 233 // cases where a caller has hard-coded a JSON string to pass in. |
145 if (typeof(args) == "string") { | 234 if (typeof(args) == "string") { |
146 args = chromeHidden.JSON.parse(args); | 235 args = chromeHidden.JSON.parse(args); |
147 } | 236 } |
148 if (eventArgumentMassagers[name]) | 237 if (eventArgumentMassagers[name]) |
149 eventArgumentMassagers[name](args); | 238 eventArgumentMassagers[name](args); |
150 } | 239 } |
151 var result = attachedNamedEvents[name].dispatch.apply( | 240 var result = attachedNamedEvents[name].dispatch_(args, listenerIDs); |
152 attachedNamedEvents[name], args); | |
153 if (result && result.validationErrors) | 241 if (result && result.validationErrors) |
154 return result.validationErrors; | 242 return result.validationErrors; |
155 } | 243 } |
156 }; | 244 }; |
157 | 245 |
158 // Dispatches a named event with the given arguments, supplied as an array. | 246 // Dispatches a named event with the given arguments, supplied as an array. |
159 chromeHidden.Event.dispatch = function(name, args) { | 247 chromeHidden.Event.dispatch = function(name, args) { |
160 if (attachedNamedEvents[name]) { | 248 if (attachedNamedEvents[name]) { |
161 attachedNamedEvents[name].dispatch.apply( | 249 attachedNamedEvents[name].dispatch.apply( |
162 attachedNamedEvents[name], args); | 250 attachedNamedEvents[name], args); |
163 } | 251 } |
164 }; | 252 }; |
165 | 253 |
166 // Test if a named event has any listeners. | 254 // Test if a named event has any listeners. |
167 chromeHidden.Event.hasListener = function(name) { | 255 chromeHidden.Event.hasListener = function(name) { |
168 return (attachedNamedEvents[name] && | 256 return (attachedNamedEvents[name] && |
169 attachedNamedEvents[name].listeners_.length > 0); | 257 attachedNamedEvents[name].listeners_.length > 0); |
170 }; | 258 }; |
171 | 259 |
172 // Registers a callback to be called when this event is dispatched. | 260 // Registers a callback to be called when this event is dispatched. |
173 chrome.Event.prototype.addListener = function(cb) { | 261 chrome.Event.prototype.addListener = function(cb, filters) { |
174 if (!this.eventOptions_.supportsListeners) | 262 if (!this.eventOptions_.supportsListeners) |
175 throw new Error("This event does not support listeners."); | 263 throw new Error("This event does not support listeners."); |
264 if (filters) { | |
265 if (!this.eventOptions_.supportsFilters) | |
266 throw new Error("This event does not support filters."); | |
267 if (filters.url && !(filters.url instanceof Array)) | |
268 throw new Error("filters.url should be an array"); | |
269 } | |
270 var listener = {callback: cb, filters: filters}; | |
271 this.attach_(listener); | |
272 this.listeners_.push(listener); | |
273 }; | |
274 | |
275 chrome.Event.prototype.attach_ = function(listener) { | |
276 this.attachmentStrategy_.onAddedListener(listener); | |
176 if (this.listeners_.length == 0) { | 277 if (this.listeners_.length == 0) { |
177 this.attach_(); | 278 allAttachedEvents[allAttachedEvents.length] = this; |
battre
2012/06/20 09:50:46
Is my understanding correct, that this adds the Ev
koz (OOO until 15th September)
2012/06/21 02:23:11
Nice spot, this code is really hairy. Done.
| |
279 if (!this.eventName_) | |
280 return; | |
281 | |
282 if (attachedNamedEvents[this.eventName_]) { | |
283 throw new Error("chrome.Event '" + this.eventName_ + | |
284 "' is already attached."); | |
285 } | |
286 | |
287 attachedNamedEvents[this.eventName_] = this; | |
178 } | 288 } |
179 this.listeners_.push(cb); | |
180 }; | 289 }; |
181 | 290 |
182 // Unregisters a callback. | 291 // Unregisters a callback. |
183 chrome.Event.prototype.removeListener = function(cb) { | 292 chrome.Event.prototype.removeListener = function(cb) { |
184 if (!this.eventOptions_.supportsListeners) | 293 if (!this.eventOptions_.supportsListeners) |
185 throw new Error("This event does not support listeners."); | 294 throw new Error("This event does not support listeners."); |
186 var idx = this.findListener_(cb); | 295 var idx = this.findListener_(cb); |
187 if (idx == -1) { | 296 if (idx == -1) { |
188 return; | 297 return; |
189 } | 298 } |
190 | 299 |
191 this.listeners_.splice(idx, 1); | 300 var removedListener = this.listeners_.splice(idx, 1)[0]; |
301 this.attachmentStrategy_.onRemovedListener(removedListener); | |
302 | |
192 if (this.listeners_.length == 0) { | 303 if (this.listeners_.length == 0) { |
193 this.detach_(true); | 304 if (!this.eventName_) |
305 return; | |
306 | |
307 if (!attachedNamedEvents[this.eventName_]) { | |
308 throw new Error("chrome.Event '" + this.eventName_ + | |
309 "' is not attached."); | |
310 } | |
311 | |
312 delete attachedNamedEvents[this.eventName_]; | |
194 } | 313 } |
195 }; | 314 }; |
196 | 315 |
197 // Test if the given callback is registered for this event. | 316 // Test if the given callback is registered for this event. |
198 chrome.Event.prototype.hasListener = function(cb) { | 317 chrome.Event.prototype.hasListener = function(cb) { |
199 if (!this.eventOptions_.supportsListeners) | 318 if (!this.eventOptions_.supportsListeners) |
200 throw new Error("This event does not support listeners."); | 319 throw new Error("This event does not support listeners."); |
201 return this.findListener_(cb) > -1; | 320 return this.findListener_(cb) > -1; |
202 }; | 321 }; |
203 | 322 |
204 // Test if any callbacks are registered for this event. | 323 // Test if any callbacks are registered for this event. |
205 chrome.Event.prototype.hasListeners = function() { | 324 chrome.Event.prototype.hasListeners = function() { |
206 if (!this.eventOptions_.supportsListeners) | 325 if (!this.eventOptions_.supportsListeners) |
207 throw new Error("This event does not support listeners."); | 326 throw new Error("This event does not support listeners."); |
208 return this.listeners_.length > 0; | 327 return this.listeners_.length > 0; |
209 }; | 328 }; |
210 | 329 |
211 // Returns the index of the given callback if registered, or -1 if not | 330 // Returns the index of the given callback if registered, or -1 if not |
212 // found. | 331 // found. |
213 chrome.Event.prototype.findListener_ = function(cb) { | 332 chrome.Event.prototype.findListener_ = function(cb) { |
214 for (var i = 0; i < this.listeners_.length; i++) { | 333 for (var i = 0; i < this.listeners_.length; i++) { |
215 if (this.listeners_[i] == cb) { | 334 if (this.listeners_[i].callback == cb) { |
216 return i; | 335 return i; |
217 } | 336 } |
218 } | 337 } |
219 | 338 |
220 return -1; | 339 return -1; |
221 }; | 340 }; |
222 | 341 |
223 // Dispatches this event object to all listeners, passing all supplied | 342 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) | 343 if (!this.eventOptions_.supportsListeners) |
227 throw new Error("This event does not support listeners."); | 344 throw new Error("This event does not support listeners."); |
228 var args = Array.prototype.slice.call(arguments); | |
229 var validationErrors = this.validateEventArgs_(args); | 345 var validationErrors = this.validateEventArgs_(args); |
230 if (validationErrors) { | 346 if (validationErrors) { |
231 console.error(validationErrors); | 347 console.error(validationErrors); |
232 return {validationErrors: validationErrors}; | 348 return {validationErrors: validationErrors}; |
233 } | 349 } |
350 | |
351 var listeners = this.attachmentStrategy_.getListenersByIDs(listenerIDs); | |
352 | |
234 var results = []; | 353 var results = []; |
235 for (var i = 0; i < this.listeners_.length; i++) { | 354 for (var i = 0; i < listeners.length; i++) { |
236 try { | 355 try { |
237 var result = this.listeners_[i].apply(null, args); | 356 var result = listeners[i].callback.apply(null, args); |
238 if (result !== undefined) | 357 if (result !== undefined) |
239 results.push(result); | 358 results.push(result); |
240 } catch (e) { | 359 } catch (e) { |
241 console.error("Error in event handler for '" + this.eventName_ + | 360 console.error("Error in event handler for '" + this.eventName_ + |
242 "': " + e.message + ' ' + e.stack); | 361 "': " + e.message + ' ' + e.stack); |
243 } | 362 } |
244 } | 363 } |
245 if (results.length) | 364 if (results.length) |
246 return {results: results}; | 365 return {results: results}; |
247 }; | 366 } |
248 | 367 |
249 // Attaches this event object to its name. Only one object can have a given | 368 // Dispatches this event object to all listeners, passing all supplied |
250 // name. | 369 // arguments to this function each listener. |
251 chrome.Event.prototype.attach_ = function() { | 370 chrome.Event.prototype.dispatch = function(varargs) { |
252 AttachEvent(this.eventName_); | 371 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 }; | 372 }; |
264 | 373 |
265 // Detaches this event object from its name. | 374 // Detaches this event object from its name. |
266 chrome.Event.prototype.detach_ = function(manual) { | 375 chrome.Event.prototype.detach_ = function() { |
267 var i = allAttachedEvents.indexOf(this); | 376 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 }; | 377 }; |
281 | 378 |
282 chrome.Event.prototype.destroy_ = function() { | 379 chrome.Event.prototype.destroy_ = function() { |
283 this.listeners_ = []; | 380 this.listeners_ = []; |
284 this.validateEventArgs_ = []; | 381 this.validateEventArgs_ = []; |
285 this.detach_(false); | 382 this.detach_(false); |
286 }; | 383 }; |
287 | 384 |
288 chrome.Event.prototype.addRules = function(rules, opt_cb) { | 385 chrome.Event.prototype.addRules = function(rules, opt_cb) { |
289 if (!this.eventOptions_.supportsRules) | 386 if (!this.eventOptions_.supportsRules) |
(...skipping 66 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... | |
356 chromeHidden.onUnload = new chrome.Event(); | 453 chromeHidden.onUnload = new chrome.Event(); |
357 | 454 |
358 chromeHidden.dispatchOnLoad = | 455 chromeHidden.dispatchOnLoad = |
359 chromeHidden.onLoad.dispatch.bind(chromeHidden.onLoad); | 456 chromeHidden.onLoad.dispatch.bind(chromeHidden.onLoad); |
360 | 457 |
361 chromeHidden.dispatchOnUnload = function() { | 458 chromeHidden.dispatchOnUnload = function() { |
362 chromeHidden.onUnload.dispatch(); | 459 chromeHidden.onUnload.dispatch(); |
363 for (var i = 0; i < allAttachedEvents.length; ++i) { | 460 for (var i = 0; i < allAttachedEvents.length; ++i) { |
364 var event = allAttachedEvents[i]; | 461 var event = allAttachedEvents[i]; |
365 if (event) | 462 if (event) |
366 event.detach_(false); | 463 event.detach_(); |
367 } | 464 } |
368 }; | 465 }; |
369 | 466 |
370 chromeHidden.dispatchError = function(msg) { | 467 chromeHidden.dispatchError = function(msg) { |
371 console.error(msg); | 468 console.error(msg); |
372 }; | 469 }; |
373 | 470 |
374 exports.Event = chrome.Event; | 471 exports.Event = chrome.Event; |
OLD | NEW |