| 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; |
| 8 var sendRequest = require('sendRequest').sendRequest; | 10 var sendRequest = require('sendRequest').sendRequest; |
| 9 var utils = require('utils'); | 11 var utils = require('utils'); |
| 10 | 12 |
| 11 var chromeHidden = requireNative('chrome_hidden').GetChromeHidden(); | 13 var chromeHidden = requireNative('chrome_hidden').GetChromeHidden(); |
| 12 var GetExtensionAPIDefinition = | 14 var GetExtensionAPIDefinition = |
| 13 requireNative('apiDefinitions').GetExtensionAPIDefinition; | 15 requireNative('apiDefinitions').GetExtensionAPIDefinition; |
| 14 | 16 |
| 15 // Schemas for the rule-style functions on the events API that | 17 // Schemas for the rule-style functions on the events API that |
| 16 // only need to be generated occasionally, so populate them lazily. | 18 // only need to be generated occasionally, so populate them lazily. |
| 17 var ruleFunctionSchemas = { | 19 var ruleFunctionSchemas = { |
| (...skipping 49 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
| 67 $Array.prototype.toJSON = customizedArrayToJSON; | 69 $Array.prototype.toJSON = customizedArrayToJSON; |
| 68 } | 70 } |
| 69 } | 71 } |
| 70 }; | 72 }; |
| 71 | 73 |
| 72 this.parse = function(thing) { | 74 this.parse = function(thing) { |
| 73 return $jsonParse(thing); | 75 return $jsonParse(thing); |
| 74 }; | 76 }; |
| 75 })(); | 77 })(); |
| 76 | 78 |
| 79 // Handles adding/removing/dispatching listeners for unfiltered events. |
| 80 var UnfilteredAttachmentStrategy = function(event) { |
| 81 this.event_ = event; |
| 82 }; |
| 83 |
| 84 UnfilteredAttachmentStrategy.prototype.onAddedListener = |
| 85 function(listener) { |
| 86 // Only attach / detach on the first / last listener removed. |
| 87 if (this.event_.listeners_.length > 1) |
| 88 return; |
| 89 AttachEvent(this.event_.eventName_); |
| 90 allAttachedEvents[allAttachedEvents.length] = this.event_; |
| 91 if (!this.event_.eventName_) |
| 92 return; |
| 93 |
| 94 if (attachedNamedEvents[this.event_.eventName_]) { |
| 95 throw new Error("chrome.Event '" + this.event_.eventName_ + |
| 96 "' is already attached."); |
| 97 } |
| 98 |
| 99 attachedNamedEvents[this.event_.eventName_] = this.event_; |
| 100 }; |
| 101 |
| 102 UnfilteredAttachmentStrategy.prototype.onRemovedListener = |
| 103 function(listener) { |
| 104 if (this.event_.listeners_.length == 0) |
| 105 this.detach(true); |
| 106 }; |
| 107 |
| 108 UnfilteredAttachmentStrategy.prototype.detach = |
| 109 function(manual) { |
| 110 var i = allAttachedEvents.indexOf(this.event_); |
| 111 if (i >= 0) |
| 112 delete allAttachedEvents[i]; |
| 113 DetachEvent(this.event_.eventName_, manual); |
| 114 if (!this.event_.eventName_) |
| 115 return; |
| 116 |
| 117 if (!attachedNamedEvents[this.event_.eventName_]) { |
| 118 throw new Error("chrome.Event '" + this.event_.eventName_ + |
| 119 "' is not attached."); |
| 120 } |
| 121 |
| 122 delete attachedNamedEvents[this.event_.eventName_]; |
| 123 }; |
| 124 |
| 125 var FilteredAttachmentStrategy = function(event) { |
| 126 this.event_ = event; |
| 127 this.listenerMap_ = {}; |
| 128 }; |
| 129 |
| 130 FilteredAttachmentStrategy.prototype.onAddedListener = function(listener) { |
| 131 var id = AttachFilteredEvent(this.event_.eventName_, |
| 132 listener.filters || []); |
| 133 listener.id = id; |
| 134 this.listenerMap_[id] = listener; |
| 135 }; |
| 136 |
| 137 FilteredAttachmentStrategy.prototype.onRemovedListener = function(listener) { |
| 138 this.detachListener(listener, true); |
| 139 }; |
| 140 |
| 141 FilteredAttachmentStrategy.prototype.detachListener = |
| 142 function(listener, manual) { |
| 143 if (listener.id == undefined) |
| 144 throw new Error("listener.id undefined - '" + listener + "'"); |
| 145 var id = listener.id; |
| 146 delete this.listenerMap_[id]; |
| 147 DetachFilteredEvent(id, manual); |
| 148 }; |
| 149 |
| 150 FilteredAttachmentStrategy.prototype.detach = function(manual) { |
| 151 for (var i in this.listenerMap_) |
| 152 this.detachListener(this.listenerMap_[i], manual); |
| 153 }; |
| 154 |
| 77 // Event object. If opt_eventName is provided, this object represents | 155 // Event object. If opt_eventName is provided, this object represents |
| 78 // the unique instance of that named event, and dispatching an event | 156 // the unique instance of that named event, and dispatching an event |
| 79 // with that name will route through this object's listeners. Note that | 157 // with that name will route through this object's listeners. Note that |
| 80 // opt_eventName is required for events that support rules. | 158 // opt_eventName is required for events that support rules. |
| 81 // | 159 // |
| 82 // Example: | 160 // Example: |
| 83 // chrome.tabs.onChanged = new chrome.Event("tab-changed"); | 161 // chrome.tabs.onChanged = new chrome.Event("tab-changed"); |
| 84 // chrome.tabs.onChanged.addListener(function(data) { alert(data); }); | 162 // chrome.tabs.onChanged.addListener(function(data) { alert(data); }); |
| 85 // chromeHidden.Event.dispatch("tab-changed", "hi"); | 163 // chromeHidden.Event.dispatch("tab-changed", "hi"); |
| 86 // will result in an alert dialog that says 'hi'. | 164 // will result in an alert dialog that says 'hi'. |
| 87 // | 165 // |
| 88 // If opt_eventOptions exists, it is a dictionary that contains the boolean | 166 // If opt_eventOptions exists, it is a dictionary that contains the boolean |
| 89 // entries "supportsListeners" and "supportsRules". | 167 // entries "supportsListeners" and "supportsRules". |
| 90 chrome.Event = function(opt_eventName, opt_argSchemas, opt_eventOptions) { | 168 chrome.Event = function(opt_eventName, opt_argSchemas, opt_eventOptions) { |
| 91 this.eventName_ = opt_eventName; | 169 this.eventName_ = opt_eventName; |
| 92 this.listeners_ = []; | 170 this.listeners_ = []; |
| 93 this.eventOptions_ = opt_eventOptions || | 171 this.eventOptions_ = opt_eventOptions || |
| 94 {"supportsListeners": true, "supportsRules": false}; | 172 {supportsListeners: true, |
| 173 supportsRules: false, |
| 174 supportsFilters: false, |
| 175 }; |
| 95 | 176 |
| 96 if (this.eventOptions_.supportsRules && !opt_eventName) | 177 if (this.eventOptions_.supportsRules && !opt_eventName) |
| 97 throw new Error("Events that support rules require an event name."); | 178 throw new Error("Events that support rules require an event name."); |
| 98 | 179 |
| 180 if (this.eventOptions_.supportsFilters) { |
| 181 this.attachmentStrategy_ = new FilteredAttachmentStrategy(this); |
| 182 } else { |
| 183 this.attachmentStrategy_ = new UnfilteredAttachmentStrategy(this); |
| 184 } |
| 185 |
| 99 // Validate event arguments (the data that is passed to the callbacks) | 186 // Validate event arguments (the data that is passed to the callbacks) |
| 100 // if we are in debug. | 187 // if we are in debug. |
| 101 if (opt_argSchemas && | 188 if (opt_argSchemas && |
| 102 chromeHidden.validateCallbacks && | 189 chromeHidden.validateCallbacks && |
| 103 chromeHidden.validate) { | 190 chromeHidden.validate) { |
| 104 | 191 |
| 105 this.validateEventArgs_ = function(args) { | 192 this.validateEventArgs_ = function(args) { |
| 106 try { | 193 try { |
| 107 chromeHidden.validate(args, opt_argSchemas); | 194 chromeHidden.validate(args, opt_argSchemas); |
| 108 } catch (exception) { | 195 } catch (exception) { |
| (...skipping 49 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
| 158 } | 245 } |
| 159 }; | 246 }; |
| 160 | 247 |
| 161 // Test if a named event has any listeners. | 248 // Test if a named event has any listeners. |
| 162 chromeHidden.Event.hasListener = function(name) { | 249 chromeHidden.Event.hasListener = function(name) { |
| 163 return (attachedNamedEvents[name] && | 250 return (attachedNamedEvents[name] && |
| 164 attachedNamedEvents[name].listeners_.length > 0); | 251 attachedNamedEvents[name].listeners_.length > 0); |
| 165 }; | 252 }; |
| 166 | 253 |
| 167 // Registers a callback to be called when this event is dispatched. | 254 // Registers a callback to be called when this event is dispatched. |
| 168 chrome.Event.prototype.addListener = function(cb) { | 255 chrome.Event.prototype.addListener = function(cb, filters) { |
| 169 if (!this.eventOptions_.supportsListeners) | 256 if (!this.eventOptions_.supportsListeners) |
| 170 throw new Error("This event does not support listeners."); | 257 throw new Error("This event does not support listeners."); |
| 171 if (this.listeners_.length == 0) { | 258 if (filters) { |
| 172 this.attach_(); | 259 if (!this.eventOptions_.supportsFilters) |
| 260 throw new Error("This event does not support filters."); |
| 261 if (!(filters instanceof Array)) |
| 262 throw new Error("filters should be an array"); |
| 173 } | 263 } |
| 174 this.listeners_.push(cb); | 264 var listener = {callback: cb, filters: filters}; |
| 265 this.listeners_.push(listener); |
| 266 this.attachmentStrategy_.onAddedListener(listener); |
| 175 }; | 267 }; |
| 176 | 268 |
| 177 // Unregisters a callback. | 269 // Unregisters a callback. |
| 178 chrome.Event.prototype.removeListener = function(cb) { | 270 chrome.Event.prototype.removeListener = function(cb) { |
| 179 if (!this.eventOptions_.supportsListeners) | 271 if (!this.eventOptions_.supportsListeners) |
| 180 throw new Error("This event does not support listeners."); | 272 throw new Error("This event does not support listeners."); |
| 181 var idx = this.findListener_(cb); | 273 var idx = this.findListener_(cb); |
| 182 if (idx == -1) { | 274 if (idx == -1) { |
| 183 return; | 275 return; |
| 184 } | 276 } |
| 185 | 277 |
| 186 this.listeners_.splice(idx, 1); | 278 var removedListener = this.listeners_.splice(idx, 1)[0]; |
| 187 if (this.listeners_.length == 0) { | 279 this.attachmentStrategy_.onRemovedListener(removedListener); |
| 188 this.detach_(true); | |
| 189 } | |
| 190 }; | 280 }; |
| 191 | 281 |
| 192 // Test if the given callback is registered for this event. | 282 // Test if the given callback is registered for this event. |
| 193 chrome.Event.prototype.hasListener = function(cb) { | 283 chrome.Event.prototype.hasListener = function(cb) { |
| 194 if (!this.eventOptions_.supportsListeners) | 284 if (!this.eventOptions_.supportsListeners) |
| 195 throw new Error("This event does not support listeners."); | 285 throw new Error("This event does not support listeners."); |
| 196 return this.findListener_(cb) > -1; | 286 return this.findListener_(cb) > -1; |
| 197 }; | 287 }; |
| 198 | 288 |
| 199 // Test if any callbacks are registered for this event. | 289 // Test if any callbacks are registered for this event. |
| 200 chrome.Event.prototype.hasListeners = function() { | 290 chrome.Event.prototype.hasListeners = function() { |
| 201 if (!this.eventOptions_.supportsListeners) | 291 if (!this.eventOptions_.supportsListeners) |
| 202 throw new Error("This event does not support listeners."); | 292 throw new Error("This event does not support listeners."); |
| 203 return this.listeners_.length > 0; | 293 return this.listeners_.length > 0; |
| 204 }; | 294 }; |
| 205 | 295 |
| 206 // Returns the index of the given callback if registered, or -1 if not | 296 // Returns the index of the given callback if registered, or -1 if not |
| 207 // found. | 297 // found. |
| 208 chrome.Event.prototype.findListener_ = function(cb) { | 298 chrome.Event.prototype.findListener_ = function(cb) { |
| 209 for (var i = 0; i < this.listeners_.length; i++) { | 299 for (var i = 0; i < this.listeners_.length; i++) { |
| 210 if (this.listeners_[i] == cb) { | 300 if (this.listeners_[i].callback == cb) { |
| 211 return i; | 301 return i; |
| 212 } | 302 } |
| 213 } | 303 } |
| 214 | 304 |
| 215 return -1; | 305 return -1; |
| 216 }; | 306 }; |
| 217 | 307 |
| 218 // Dispatches this event object to all listeners, passing all supplied | 308 // Dispatches this event object to all listeners, passing all supplied |
| 219 // arguments to this function each listener. | 309 // arguments to this function each listener. |
| 220 chrome.Event.prototype.dispatch = function(varargs) { | 310 chrome.Event.prototype.dispatch = function(varargs) { |
| 221 if (!this.eventOptions_.supportsListeners) | 311 if (!this.eventOptions_.supportsListeners) |
| 222 throw new Error("This event does not support listeners."); | 312 throw new Error("This event does not support listeners."); |
| 223 var args = Array.prototype.slice.call(arguments); | 313 var args = Array.prototype.slice.call(arguments); |
| 224 var validationErrors = this.validateEventArgs_(args); | 314 var validationErrors = this.validateEventArgs_(args); |
| 225 if (validationErrors) { | 315 if (validationErrors) { |
| 226 return {validationErrors: validationErrors}; | 316 return {validationErrors: validationErrors}; |
| 227 } | 317 } |
| 228 var results = []; | 318 var results = []; |
| 229 for (var i = 0; i < this.listeners_.length; i++) { | 319 for (var i = 0; i < this.listeners_.length; i++) { |
| 230 try { | 320 try { |
| 231 var result = this.listeners_[i].apply(null, args); | 321 var result = this.listeners_[i].callback.apply(null, args); |
| 232 if (result !== undefined) | 322 if (result !== undefined) |
| 233 results.push(result); | 323 results.push(result); |
| 234 } catch (e) { | 324 } catch (e) { |
| 235 console.error("Error in event handler for '" + this.eventName_ + | 325 console.error("Error in event handler for '" + this.eventName_ + |
| 236 "': " + e.message + ' ' + e.stack); | 326 "': " + e.message + ' ' + e.stack); |
| 237 } | 327 } |
| 238 } | 328 } |
| 239 if (results.length) | 329 if (results.length) |
| 240 return {results: results}; | 330 return {results: results}; |
| 241 }; | 331 }; |
| 242 | 332 |
| 243 // Attaches this event object to its name. Only one object can have a given | |
| 244 // name. | |
| 245 chrome.Event.prototype.attach_ = function() { | |
| 246 AttachEvent(this.eventName_); | |
| 247 allAttachedEvents[allAttachedEvents.length] = this; | |
| 248 if (!this.eventName_) | |
| 249 return; | |
| 250 | |
| 251 if (attachedNamedEvents[this.eventName_]) { | |
| 252 throw new Error("chrome.Event '" + this.eventName_ + | |
| 253 "' is already attached."); | |
| 254 } | |
| 255 | |
| 256 attachedNamedEvents[this.eventName_] = this; | |
| 257 }; | |
| 258 | |
| 259 // Detaches this event object from its name. | 333 // Detaches this event object from its name. |
| 260 chrome.Event.prototype.detach_ = function(manual) { | 334 chrome.Event.prototype.detach_ = function() { |
| 261 var i = allAttachedEvents.indexOf(this); | 335 this.attachmentStrategy_.detach(false); |
| 262 if (i >= 0) | |
| 263 delete allAttachedEvents[i]; | |
| 264 DetachEvent(this.eventName_, manual); | |
| 265 if (!this.eventName_) | |
| 266 return; | |
| 267 | |
| 268 if (!attachedNamedEvents[this.eventName_]) { | |
| 269 throw new Error("chrome.Event '" + this.eventName_ + | |
| 270 "' is not attached."); | |
| 271 } | |
| 272 | |
| 273 delete attachedNamedEvents[this.eventName_]; | |
| 274 }; | 336 }; |
| 275 | 337 |
| 276 chrome.Event.prototype.destroy_ = function() { | 338 chrome.Event.prototype.destroy_ = function() { |
| 277 this.listeners_ = []; | 339 this.listeners_ = []; |
| 278 this.validateEventArgs_ = []; | 340 this.validateEventArgs_ = []; |
| 279 this.detach_(false); | 341 this.detach_(false); |
| 280 }; | 342 }; |
| 281 | 343 |
| 344 // Gets the declarative API object, or undefined if this extension doesn't |
| 345 // have access to it. |
| 346 // |
| 347 // This is defined as a function (rather than a variable) because it isn't |
| 348 // accessible until the schema bindings have been generated. |
| 349 function getDeclarativeAPI() { |
| 350 return chromeHidden.internalAPIs.declarative; |
| 351 } |
| 352 |
| 282 chrome.Event.prototype.addRules = function(rules, opt_cb) { | 353 chrome.Event.prototype.addRules = function(rules, opt_cb) { |
| 283 if (!this.eventOptions_.supportsRules) | 354 if (!this.eventOptions_.supportsRules) |
| 284 throw new Error("This event does not support rules."); | 355 throw new Error("This event does not support rules."); |
| 285 | 356 |
| 286 // Takes a list of JSON datatype identifiers and returns a schema fragment | 357 // Takes a list of JSON datatype identifiers and returns a schema fragment |
| 287 // that verifies that a JSON object corresponds to an array of only these | 358 // that verifies that a JSON object corresponds to an array of only these |
| 288 // data types. | 359 // data types. |
| 289 function buildArrayOfChoicesSchema(typesList) { | 360 function buildArrayOfChoicesSchema(typesList) { |
| 290 return { | 361 return { |
| 291 'type': 'array', | 362 'type': 'array', |
| (...skipping 58 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
| 350 chromeHidden.onUnload = new chrome.Event(); | 421 chromeHidden.onUnload = new chrome.Event(); |
| 351 | 422 |
| 352 chromeHidden.dispatchOnLoad = | 423 chromeHidden.dispatchOnLoad = |
| 353 chromeHidden.onLoad.dispatch.bind(chromeHidden.onLoad); | 424 chromeHidden.onLoad.dispatch.bind(chromeHidden.onLoad); |
| 354 | 425 |
| 355 chromeHidden.dispatchOnUnload = function() { | 426 chromeHidden.dispatchOnUnload = function() { |
| 356 chromeHidden.onUnload.dispatch(); | 427 chromeHidden.onUnload.dispatch(); |
| 357 for (var i = 0; i < allAttachedEvents.length; ++i) { | 428 for (var i = 0; i < allAttachedEvents.length; ++i) { |
| 358 var event = allAttachedEvents[i]; | 429 var event = allAttachedEvents[i]; |
| 359 if (event) | 430 if (event) |
| 360 event.detach_(false); | 431 event.detach_(); |
| 361 } | 432 } |
| 362 }; | 433 }; |
| 363 | 434 |
| 364 chromeHidden.dispatchError = function(msg) { | 435 chromeHidden.dispatchError = function(msg) { |
| 365 console.error(msg); | 436 console.error(msg); |
| 366 }; | 437 }; |
| 367 | 438 |
| 368 exports.Event = chrome.Event; | 439 exports.Event = chrome.Event; |
| OLD | NEW |