OLD | NEW |
(Empty) | |
| 1 'use strict'; |
| 2 |
| 3 /** |
| 4 * Provides a declarative structure around interactive D3 |
| 5 * applications. |
| 6 * |
| 7 * @module d3-components |
| 8 **/ |
| 9 |
| 10 YUI.add('d3-components', function(Y) { |
| 11 var ns = Y.namespace('d3'), |
| 12 L = Y.Lang; |
| 13 |
| 14 var Module = Y.Base.create('Module', Y.Base, [], { |
| 15 /** |
| 16 * @property events |
| 17 * @type {object} |
| 18 **/ |
| 19 events: { |
| 20 scene: {}, |
| 21 d3: {}, |
| 22 yui: {} |
| 23 }, |
| 24 |
| 25 initializer: function() { |
| 26 this.events = Y.merge(this.events); |
| 27 } |
| 28 }, { |
| 29 ATTRS: { |
| 30 component: {}, |
| 31 options: {}, |
| 32 container: {getter: function() { |
| 33 return this.get('component').get('container');}} |
| 34 } |
| 35 }); |
| 36 ns.Module = Module; |
| 37 |
| 38 |
| 39 var Component = Y.Base.create('Component', Y.Base, [], { |
| 40 /** |
| 41 * @class Component |
| 42 * |
| 43 * Component collects modules implementing various portions of an |
| 44 * applications functionality in a declarative way. It is designed to allow |
| 45 * both a cleaner separation of concerns and the ability to reuse the |
| 46 * component in different ways. |
| 47 * |
| 48 * Component accomplishes these goals by: |
| 49 * - Control how events are bound and unbound. |
| 50 * - Providing patterns for update data cleanly. |
| 51 * - Providing suggestions around updating the interactive portions |
| 52 * of the application. |
| 53 * |
| 54 * @constructor |
| 55 **/ |
| 56 initializer: function() { |
| 57 this.modules = {}; |
| 58 this.events = {}; |
| 59 }, |
| 60 |
| 61 /** |
| 62 * @method addModule |
| 63 * @chainable |
| 64 * @param {Module} ModClassOrInstance bound will be to this. |
| 65 * @param {Object} options dict of options set as options attribute on |
| 66 * module. |
| 67 * |
| 68 * Add a Module to this Component. This will bind its events and set up all |
| 69 * needed event subscriptions. Modules can return three sets of events |
| 70 * that will be bound in different ways |
| 71 * |
| 72 * - scene: {selector: event-type: handlerName} -> YUI styled event |
| 73 * delegation |
| 74 * - d3 {selector: event-type: handlerName} -> Bound using |
| 75 * specialized d3 event handling |
| 76 * - yui {event-type: handlerName} -> collection of global and custom |
| 77 * events the module reacts to. |
| 78 **/ |
| 79 |
| 80 addModule: function(ModClassOrInstance, options) { |
| 81 var config = options || {}, |
| 82 module = ModClassOrInstance, |
| 83 modEvents; |
| 84 |
| 85 if (!(ModClassOrInstance instanceof Module)) { |
| 86 module = new ModClassOrInstance(); |
| 87 } |
| 88 module.setAttrs({ |
| 89 component: this, |
| 90 options: config}); |
| 91 |
| 92 this.modules[module.name] = module; |
| 93 |
| 94 modEvents = module.events; |
| 95 this.events[module.name] = modEvents; |
| 96 this.bind(module.name); |
| 97 return this; |
| 98 }, |
| 99 |
| 100 /** |
| 101 * @method removeModule |
| 102 * @param {String} moduleName Module name to remove. |
| 103 * @chainable |
| 104 **/ |
| 105 removeModule: function(moduleName) { |
| 106 this.unbind(moduleName); |
| 107 delete this.events[moduleName]; |
| 108 delete this.modules[moduleName]; |
| 109 return this; |
| 110 }, |
| 111 |
| 112 /** |
| 113 * Internal implementation of |
| 114 * binding both |
| 115 * Module.events.scene and |
| 116 * Module.events.yui. |
| 117 **/ |
| 118 _bindEvents: function(modName) { |
| 119 var self = this, |
| 120 modEvents = this.events[modName], |
| 121 module = this.modules[modName], |
| 122 owns = Y.Object.owns, |
| 123 container = this.get('container'), |
| 124 subscriptions = [], |
| 125 handlers, |
| 126 handler; |
| 127 |
| 128 function _bindEvent(name, handler, container, selector, context) { |
| 129 // Adapt between d3 events and YUI delegates. |
| 130 var d3Adaptor = function(evt) { |
| 131 var selection = d3.select(evt.currentTarget.getDOMNode()), |
| 132 d = selection.data()[0]; |
| 133 // This is a minor violation (extension) |
| 134 // of the interface, but suits us well. |
| 135 d3.event = evt; |
| 136 return handler.call( |
| 137 evt.currentTarget.getDOMNode(), d, context); |
| 138 }; |
| 139 |
| 140 subscriptions.push( |
| 141 Y.delegate(name, d3Adaptor, container, selector, context)); |
| 142 } |
| 143 |
| 144 // Return a resolved handler object in the form |
| 145 // {phase: str, callback: function} |
| 146 function _normalizeHandler(handler, module, selector) { |
| 147 var result = {}; |
| 148 |
| 149 if (L.isString(handler)) { |
| 150 result.callback = module[handler]; |
| 151 result.phase = 'on'; |
| 152 } |
| 153 |
| 154 if (L.isObject(handler)) { |
| 155 result.phase = handler.phase || 'on'; |
| 156 result.callback = handler.callback; |
| 157 } |
| 158 |
| 159 if (L.isString(result.callback)) { |
| 160 result.callback = module[result.callback]; |
| 161 } |
| 162 |
| 163 if (!result.callback) { |
| 164 console.error('No Event handler for', selector, modName); |
| 165 return; |
| 166 } |
| 167 if (!L.isFunction(result.callback)) { |
| 168 console.error('Unable to resolve a proper callback for', |
| 169 selector, handler, modName, result); |
| 170 return; |
| 171 } |
| 172 return result; |
| 173 } |
| 174 |
| 175 this.unbind(modName); |
| 176 |
| 177 // Bind 'scene' events |
| 178 Y.each(modEvents.scene, function(handlers, selector, sceneEvents) { |
| 179 Y.each(handlers, function(handler, trigger) { |
| 180 handler = _normalizeHandler(handler, module, selector); |
| 181 if (L.isValue(handler)) { |
| 182 _bindEvent(trigger, handler.callback, container, selector, self); |
| 183 } |
| 184 }); |
| 185 }); |
| 186 |
| 187 // Bind 'yui' custom/global subscriptions |
| 188 // yui: {str: str_or_function} |
| 189 // TODO {str: str/func/obj} |
| 190 // where object includes phase (before, on, after) |
| 191 if (modEvents.yui) { |
| 192 // Resolve any 'string' handlers to methods on module. |
| 193 Y.each(['after', 'before', 'on'], function(eventPhase) { |
| 194 var resolvedHandler = {}; |
| 195 Y.each(modEvents.yui, function(handler, name) { |
| 196 handler = _normalizeHandler(handler, module); |
| 197 if (!handler || handler.phase !== eventPhase) { |
| 198 return; |
| 199 } |
| 200 resolvedHandler[name] = handler.callback; |
| 201 }, this); |
| 202 // Bind resolved event handlers as a group. |
| 203 if (Y.Object.keys(resolvedHandler).length) { |
| 204 subscriptions.push(Y[eventPhase](resolvedHandler)); |
| 205 } |
| 206 }); |
| 207 } |
| 208 return subscriptions; |
| 209 }, |
| 210 |
| 211 /** |
| 212 * @method bind |
| 213 * |
| 214 * Internal. Called automatically by addModule. |
| 215 **/ |
| 216 bind: function(moduleName) { |
| 217 var eventSet = this.events, |
| 218 filtered = {}; |
| 219 |
| 220 if (moduleName) { |
| 221 filtered[moduleName] = eventSet[moduleName]; |
| 222 eventSet = filtered; |
| 223 } |
| 224 |
| 225 Y.each(Y.Object.keys(eventSet), function(name) { |
| 226 this.events[name].subscriptions = this._bindEvents(name); |
| 227 }, this); |
| 228 return this; |
| 229 }, |
| 230 |
| 231 /** |
| 232 * Specialized handling of events only found in d3. |
| 233 * This is again an internal implementation detail. |
| 234 * |
| 235 * Its worth noting that d3 events don't use a delegate pattern |
| 236 * and thus must be bound to nodes present in a selection. |
| 237 * For this reason binding d3 events happens after render cycles. |
| 238 * |
| 239 * @method _bindD3Events |
| 240 * @param {String} modName Module name. |
| 241 **/ |
| 242 _bindD3Events: function(modName) { |
| 243 // Walk each selector for a given module 'name', doing a |
| 244 // d3 selection and an 'on' binding. |
| 245 var modEvents = this.events[modName], |
| 246 owns = Y.Object.owns, |
| 247 module; |
| 248 if (!modEvents || !modEvents.d3) { |
| 249 return; |
| 250 } |
| 251 modEvents = modEvents.d3; |
| 252 module = this.modules[modName]; |
| 253 |
| 254 function _normalizeHandler(handler, module) { |
| 255 if (handler && !L.isFunction(handler)) { |
| 256 handler = module[handler]; |
| 257 } |
| 258 return handler; |
| 259 } |
| 260 |
| 261 Y.each(modEvents, function(handlers, selector) { |
| 262 Y.each(handlers, function(handler, trigger) { |
| 263 handler = _normalizeHandler(handler, module); |
| 264 d3.selectAll(selector).on(trigger, handler); |
| 265 }); |
| 266 }); |
| 267 }, |
| 268 |
| 269 /** |
| 270 * @method _unbindD3Events |
| 271 * |
| 272 * Internal Detail. Called by unbind automatically. |
| 273 * D3 events follow a 'slot' like system. Setting the |
| 274 * event to null unbinds existing handlers. |
| 275 **/ |
| 276 _unbindD3Events: function(modName) { |
| 277 var modEvents = this.events[modName], |
| 278 owns = Y.Object.owns, |
| 279 module; |
| 280 |
| 281 if (!modEvents || !modEvents.d3) { |
| 282 return; |
| 283 } |
| 284 modEvents = modEvents.d3; |
| 285 module = this.modules[modName]; |
| 286 |
| 287 Y.each(modEvents, function(handlers, selector) { |
| 288 Y.each(handlers, function(handler, trigger) { |
| 289 d3.selectAll(selector).on(trigger, null); |
| 290 }); |
| 291 }); |
| 292 }, |
| 293 |
| 294 /** |
| 295 * @method unbind |
| 296 * Internal. Called automatically by removeModule. |
| 297 **/ |
| 298 unbind: function(moduleName) { |
| 299 var eventSet = this.events, |
| 300 filtered = {}; |
| 301 |
| 302 function _unbind(modEvents) { |
| 303 Y.each(modEvents.subscriptions, function(handler) { |
| 304 if (handler) { |
| 305 handler.detach(); |
| 306 } |
| 307 }); |
| 308 delete modEvents.subscriptions; |
| 309 } |
| 310 |
| 311 if (moduleName) { |
| 312 filtered[moduleName] = eventSet[moduleName]; |
| 313 eventSet = filtered; |
| 314 } |
| 315 Y.each(Y.Object.values(eventSet), _unbind, this); |
| 316 // Remove any d3 subscriptions as well. |
| 317 this._unbindD3Events(); |
| 318 |
| 319 return this; |
| 320 }, |
| 321 |
| 322 /** |
| 323 * @method render |
| 324 * @chainable |
| 325 * |
| 326 * Render each module bound to the canvas |
| 327 */ |
| 328 render: function() { |
| 329 var self = this; |
| 330 function renderAndBind(module, name) { |
| 331 if (module && module.render) { |
| 332 module.render(); |
| 333 } |
| 334 self._bindD3Events(name); |
| 335 } |
| 336 |
| 337 // If the container isn't bound to the DOM |
| 338 // do so now. |
| 339 this.attachContainer(); |
| 340 // Render modules. |
| 341 Y.each(this.modules, renderAndBind, this); |
| 342 return this; |
| 343 }, |
| 344 |
| 345 /** |
| 346 * @method attachContainer |
| 347 * @chainable |
| 348 * |
| 349 * Called by render, conditionally attach container to the DOM if |
| 350 * it isn't already. The framework calls this before module |
| 351 * rendering so that d3 Events will have attached DOM elements. If |
| 352 * your application doesn't need this behavior feel free to override. |
| 353 **/ |
| 354 attachContainer: function() { |
| 355 var container = this.get('container'); |
| 356 if (container && !container.inDoc()) { |
| 357 Y.one('body').append(container); |
| 358 } |
| 359 return this; |
| 360 }, |
| 361 |
| 362 /** |
| 363 * @method detachContainer |
| 364 * |
| 365 * Remove container from DOM returning container. This |
| 366 * is explicitly not chainable. |
| 367 **/ |
| 368 detachContainer: function() { |
| 369 var container = this.get('container'); |
| 370 if (container.inDoc()) { |
| 371 container.remove(); |
| 372 } |
| 373 return container; |
| 374 }, |
| 375 |
| 376 /** |
| 377 * |
| 378 * @method update |
| 379 * @chainable |
| 380 * |
| 381 * Update the data for each module |
| 382 * see also the dataBinding event hookup |
| 383 */ |
| 384 update: function() { |
| 385 Y.each(Y.Object.values(this.modules), function(mod) { |
| 386 mod.update(); |
| 387 }); |
| 388 return this; |
| 389 } |
| 390 }, { |
| 391 ATTRS: { |
| 392 container: {} |
| 393 } |
| 394 |
| 395 }); |
| 396 ns.Component = Component; |
| 397 }, '0.1', { |
| 398 'requires': ['d3', |
| 399 'base', |
| 400 'array-extras', |
| 401 'event']}); |
OLD | NEW |