LEFT | RIGHT |
| 1 /* |
| 2 This file is part of the Juju GUI, which lets users view and manage Juju |
| 3 environments within a graphical interface (https://launchpad.net/juju-gui). |
| 4 Copyright (C) 2012-2013 Canonical Ltd. |
| 5 |
| 6 This program is free software: you can redistribute it and/or modify it under |
| 7 the terms of the GNU Affero General Public License version 3, as published by |
| 8 the Free Software Foundation. |
| 9 |
| 10 This program is distributed in the hope that it will be useful, but WITHOUT |
| 11 ANY WARRANTY; without even the implied warranties of MERCHANTABILITY, |
| 12 SATISFACTORY QUALITY, or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero |
| 13 General Public License for more details. |
| 14 |
| 15 You should have received a copy of the GNU Affero General Public License along |
| 16 with this program. If not, see <http://www.gnu.org/licenses/>. |
| 17 */ |
| 18 |
| 19 'use strict'; |
| 20 |
| 21 |
| 22 /** |
| 23 Adds the ViewContainer constructor class and viewlet instantiable object |
| 24 |
| 25 @module juju-view-container |
| 26 */ |
| 27 YUI.add('juju-view-container', function(Y) { |
| 28 |
| 29 /** |
| 30 Viewlet object class. It's expected that these properties will be |
| 31 overwritten on instantiation and so only basic defaults are defined here. |
| 32 |
| 33 Instantiate with Object.create() |
| 34 |
| 35 @class ViewletBase |
| 36 @constructor |
| 37 */ |
| 38 var ViewletBase = { |
| 39 |
| 40 /** |
| 41 The user defined name for the viewlet. This will be inferred from the |
| 42 viewlets object on the ViewContainer when possible. |
| 43 |
| 44 @property name |
| 45 @type {String} |
| 46 @default null |
| 47 */ |
| 48 name: null, |
| 49 |
| 50 /** |
| 51 String template of the viewlet wrapper |
| 52 |
| 53 @property templateWrapper |
| 54 @type {string | compiled Handlebars template} |
| 55 @default '<div class="viewlet-wrapper" style="display: none"></div>' |
| 56 */ |
| 57 templateWrapper: '<div class="viewlet-wrapper" style="display:none"></div>', |
| 58 |
| 59 /** |
| 60 Template of the viewlet, provided during configuration |
| 61 |
| 62 @property template |
| 63 @type {string | compiled Handlebars template} |
| 64 @default '{{viewlet}}' |
| 65 */ |
| 66 template: '{{viewlet}}', // compiled handlebars template |
| 67 |
| 68 /** |
| 69 The rendered viewlet element |
| 70 |
| 71 @property container |
| 72 @type {Y.Node} |
| 73 @default null |
| 74 */ |
| 75 container: null, |
| 76 |
| 77 /** |
| 78 Optional logical slot name for this viewlet to fill. |
| 79 |
| 80 @property slot |
| 81 @type {String} |
| 82 @default null |
| 83 */ |
| 84 slot: null, |
| 85 |
| 86 /** |
| 87 When defined it allows the developer to specify another model to bind the |
| 88 Viewlet to, usually one nested in the model passed to the View Container. |
| 89 |
| 90 @property selectBindModel |
| 91 @type {Function} |
| 92 @default null |
| 93 */ |
| 94 selectBindModel: null, |
| 95 |
| 96 /** |
| 97 User defined update method which re-renders the contents of the viewlet. |
| 98 Called by the binding engine if a modellist is updated. This is |
| 99 accomplished by grabbing the viewlets container and setHTML() with the |
| 100 new contents. Passed a reference to the modellist in question. |
| 101 |
| 102 @method update |
| 103 @type {function} |
| 104 @param {Y.ModelList | Y.LazyModelList} modellist from the selectBindModel. |
| 105 @default {noop function} |
| 106 */ |
| 107 update: function(modellist) {}, |
| 108 |
| 109 /** |
| 110 Render method to generate the container and insert the compiled viewlet |
| 111 template into it. It's passed reference to the model passed to the view |
| 112 container. |
| 113 |
| 114 @method render |
| 115 @type {function} |
| 116 @param {Y.Model} model passed to the view container. |
| 117 @param {Object} viewContainerAttrs object of all of the view container |
| 118 attributes. |
| 119 @default {render function} |
| 120 */ |
| 121 render: function(model, viewContainerAttrs) { |
| 122 this.container = Y.Node.create(this.templateWrapper); |
| 123 |
| 124 if (typeof this.template === 'string') { |
| 125 this.template = Y.Handlebars.compile(this.template); |
| 126 } |
| 127 this.container.setHTML(this.template(model.getAttrs())); |
| 128 }, |
| 129 |
| 130 /** |
| 131 Called when there is a bind conflict in the viewlet. |
| 132 |
| 133 @method conflict |
| 134 @type {function} |
| 135 @default {noop function} |
| 136 */ |
| 137 conflict: function(node) {}, |
| 138 |
| 139 /** |
| 140 Called by the databinding engine when fields drop out of sync with |
| 141 the supplied model. |
| 142 |
| 143 @method unsyncedFields |
| 144 @param {Array} dirtyFields an array of keys representing changed fields. |
| 145 */ |
| 146 unsyncedFields: function(dirtyFields) {}, |
| 147 |
| 148 /** |
| 149 Called by the databinding engine when the viewlet drops out |
| 150 off a conflicted state |
| 151 |
| 152 @method syncedFields |
| 153 */ |
| 154 syncedFields: function() {} |
| 155 |
| 156 /** |
| 157 Used for conflict resolution. When the user changes a value on a bound |
| 158 viewlet we store a reference of the element key here so that we know to |
| 159 offer a conflict resolution. |
| 160 |
| 161 @property _changedValues |
| 162 @type {Array} |
| 163 @default empty array |
| 164 @private |
| 165 */ |
| 166 |
| 167 /** |
| 168 Model change events handles associated with this viewlet. |
| 169 |
| 170 @property _eventHandles |
| 171 @type {Array} |
1 @default empty array | 172 @default empty array |
2 @private | 173 @private |
3 */ | 174 */ |
| 175 |
| 176 /** |
| 177 Removes the databinding events. This method is added to the viewlet |
| 178 instance in the databinding class on binding. |
| 179 |
| 180 @method remove |
| 181 */ |
4 }; | 182 }; |
5 | 183 |
6 /** | 184 /** |
| 185 ViewContainer class for rendering a parent view container which manages the |
| 186 display of viewlets. |
| 187 |
| 188 @namespace juju |
| 189 @class ViewContainer |
| 190 @constructor |
| 191 */ |
| 192 var jujuViews = Y.namespace('juju.views'); |
| 193 jujuViews.ViewContainer = new Y.Base.create('view-container', Y.View, [], { |
| 194 |
| 195 /** |
| 196 DOM bound events for any view container related events |
| 197 |
| 198 @property events |
| 199 */ |
| 200 events: {}, |
| 201 |
| 202 /** |
| 203 Viewlet configuration object. Set by passing `viewlets` in during |
| 204 instantiation. |
| 205 ex) (see ViewletBase) |
| 206 |
| 207 @property viewletConfig |
| 208 @default undefined |
| 209 */ |
| 210 |
| 211 /** |
| 212 Template of the view container. Set by passing in during instantiation. |
| 213 ex) { template: Y.juju.templates['view-container'] } |
| 214 Must include {{ viewlets }} to allow rendering of the viewlets. |
| 215 |
| 216 @property template, |
| 217 @type {Handlebars Template} |
| 218 @default '<div class="view-container-wrapper">{{viewlets}}</div>' |
| 219 */ |
| 220 |
| 221 /** |
| 222 Handlebars config options for the view-container template. Set by passing |
| 223 in during instantiation ex) { templateConfig: {} } |
| 224 |
| 225 @property templateConfig |
| 226 @type {Object} |
| 227 */ |
| 228 |
| 229 /** |
| 230 A hash of the viewlet instances |
| 231 |
| 232 ex) {} |
| 233 |
| 234 @property viewlets |
| 235 @type {Object} |
| 236 @default undefined |
| 237 */ |
| 238 |
| 239 initializer: function(options) { |
| 240 // Passed in on instantiation |
| 241 this.viewletConfig = options.viewlets; |
| 242 this.template = options.template; |
| 243 this.templateConfig = options.templateConfig || {}; |
| 244 this.viewletContainer = options.viewletContainer; |
| 245 this.viewlets = this._generateViewlets(); // {String}: {Viewlet} |
| 246 this.events = options.events; |
| 247 // Map from logical slot name to the CSS selector within viewContainer's |
| 248 // DOM to be used to hold this slot when rendered. |
| 249 this.slots = {}; |
| 250 // Internal mapping from slot name to viewlet rendered into slot. |
| 251 this._slots = {}; |
| 252 this._events = []; |
| 253 |
| 254 this._setupEvents(); |
| 255 |
| 256 this.bindingEngine = new jujuViews.BindingEngine( |
| 257 options.databinding || {}); |
| 258 }, |
| 259 |
| 260 /** |
| 261 Return the name of the model as a key to index its |
| 262 inspector panel. |
| 263 |
| 264 @method getName |
| 265 @return {String} id of the model. |
| 266 */ |
| 267 getName: function() { |
| 268 return this.get('model').get('id'); |
| 269 }, |
| 270 |
| 271 /** |
| 272 Renders the viewlets into the view container. Viewlets with a logical |
| 273 slot name defined are not rendered by defaul and require that showViewlet |
| 274 be called for them to render. Slots are typically filled through event |
| 275 callback interactions (for example in a click handler). |
| 276 |
| 277 @method render |
| 278 @chainable |
| 279 */ |
| 280 render: function() { |
| 281 var attrs = this.getAttrs(), |
| 282 container = attrs.container, |
| 283 model = attrs.model, |
| 284 viewletTemplate; |
| 285 |
| 286 // To allow you to pass in a string instead of a precompiled template |
| 287 if (typeof this.template === 'string') { |
| 288 this.template = Y.Handlebars.compile(this.template); |
| 289 } |
| 290 container.setHTML(this.template(this.templateConfig)); |
| 291 |
| 292 var viewletContainer = container.one(this.viewletContainer); |
| 293 |
| 294 // render the viewlets into their containers |
| 295 Y.Object.each(this.viewlets, function(viewlet, name) { |
| 296 if (!viewlet.name) { |
| 297 viewlet.name = name; |
| 298 } |
| 299 if (viewlet.slot) { |
| 300 return; |
| 301 } |
| 302 var result = viewlet.render(model, attrs); |
| 303 if (result && typeof result === 'string') { |
| 304 viewlet.container = Y.Node.create(result); |
| 305 } |
| 306 viewletContainer.append(viewlet.container); |
| 307 this.bindingEngine.bind(model, viewlet); |
| 308 }, this); |
| 309 |
| 310 this.recalculateHeight(viewletContainer); |
| 311 |
| 312 // chainable |
| 313 return this; |
| 314 }, |
| 315 |
| 316 /** |
| 317 Switches the visible viewlet to the requested. |
| 318 |
| 319 @method showViewlet |
| 320 @param {String} viewletName is a string representing the viewlet name. |
| 321 @param {Model} model to associate with the viewlet in its slot. |
| 322 */ |
| 323 showViewlet: function(viewletName, model) { |
| 324 var container = this.get('container'); |
| 325 // This method can be called directly but it is also an event handler |
| 326 // for clicking on the view container tab handles |
| 327 if (typeof viewletName !== 'string') { |
| 328 viewletName = viewletName.currentTarget.getData('viewlet'); |
| 329 } |
| 330 var viewlet = this.viewlets[viewletName]; |
| 331 if (!model) { |
| 332 model = this.get('model'); |
| 333 } |
| 334 this.fillSlot(viewlet, model); |
| 335 viewlet.container.show(); |
| 336 this.recalculateHeight(); |
| 337 }, |
| 338 |
| 339 /** |
| 340 Render a viewlet/model pair into a logically named slot. |
| 341 Called automatically by showViewlet. When the viewlet has a |
| 342 slot defined this method registers the model bindings |
| 343 properly removing any existing bindings for the slot. |
| 344 |
| 345 @method fillSlot |
| 346 @param {Viewlet} viewlet to render. |
| 347 @param {Model} model to bind to the slot. |
| 348 */ |
| 349 fillSlot: function(viewlet, model) { |
| 350 var target; |
| 351 var slot = viewlet.slot; |
| 352 if (slot === null) { |
| 353 return; |
| 354 } |
| 355 var existing = this._slots[slot]; |
| 356 if (existing) { |
| 357 existing = this.bindingEngine.getViewlet(existing.name); |
| 358 if (existing) { |
| 359 // remove only removes the databinding but does not clear the DOM. |
| 360 existing.remove(); |
| 361 } |
| 362 } |
| 363 if (model === undefined) { |
| 364 model = this.get('model'); |
| 365 } |
| 366 if (this.slots[slot]) { |
| 367 // Look up the target selector for the slot. |
| 368 target = this.get('container').one(this.slots[slot]); |
| 369 var result = viewlet.render(model, this.getAttrs()); |
| 370 if (result) { |
| 371 if (typeof result === 'string') { |
| 372 result = Y.Node.create(result); |
| 373 } |
| 374 viewlet.container = result; |
| 375 } |
| 376 target.setHTML(viewlet.container); |
| 377 this._slots[slot] = viewlet; |
| 378 this.bindingEngine.bind(model, viewlet); |
| 379 } else { |
| 380 console.error('View Container Missing slot', slot); |
| 381 } |
| 382 }, |
| 383 |
| 384 /** |
| 385 Event callback which hides the viewlet slot which is related |
| 386 to the close button |
| 387 |
| 388 @method hideSlot |
| 389 @param {Y.EventFacade} e Click event. |
| 390 */ |
| 391 hideSlot: function(e) { |
| 392 var existing = this._slots[e.currentTarget.getData('slot')]; |
| 393 if (existing) { |
| 394 // unbind the databinding |
| 395 existing.remove(); |
| 396 // remove the element from the DOM |
| 397 existing.container.remove(true); |
| 398 } |
| 399 }, |
| 400 |
| 401 /** |
| 402 Recalculates and sets the height of the view-container when |
| 403 the browser is resized or by being called directly. |
| 404 |
| 405 @method recalculateHeight |
| 406 @param {Y.Node} container A reference to the container element. |
| 407 */ |
| 408 recalculateHeight: function(container) { |
| 409 // Because this is also a callback we need to check to see |
| 410 // if this is an event object or a real container element |
| 411 if (container && container.type) { container = null; } |
| 412 container = container || this.get('container'); |
| 413 var TB_SPACING = 20; |
| 414 var winHeight = container.get('winHeight'), |
| 415 header = Y.one('.navbar'), |
| 416 footer = Y.one('.bottom-navbar'), |
| 417 // Depending on the render cycle these may or may not be in the DOM |
| 418 // which is why we pull their heights separately |
| 419 vcHeader = container.one('.view-container-navigation'), |
| 420 vcFooter = container.one('.view-container-footer'), |
| 421 headerHeight = 0, |
| 422 footerHeight = 0, |
| 423 vcHeaderHeight = 0, |
| 424 vcFooterHeight = 0; |
| 425 |
| 426 if (header) { headerHeight = header.get('clientHeight'); } |
| 427 if (footer) { footerHeight = footer.get('clientHeight'); } |
| 428 if (vcHeader) { vcHeaderHeight = vcHeader.get('clientHeight'); } |
| 429 if (vcFooter) { vcFooterHeight = vcFooter.get('clientHeight'); } |
| 430 |
| 431 var height = winHeight - headerHeight - footerHeight - (TB_SPACING * 3); |
| 432 // subtract the height of the header and footer of the view container. |
| 433 height = height - vcHeaderHeight - vcFooterHeight; |
| 434 |
| 435 this.get('container').one(this.viewletContainer) |
| 436 .setStyle('maxHeight', height + 'px'); |
| 437 }, |
| 438 |
| 439 /** |
| 440 Generates the viewlet instances based on the passed in configuration |
| 441 |
| 442 @method _generateViewlets |
| 443 @private |
| 444 */ |
| 445 _generateViewlets: function() { |
| 446 var viewlets = {}, |
| 447 model = this.get('model'); |
| 448 |
| 449 // expand out the config to defineProperty syntax |
| 450 this._expandViewletConfig(); |
| 451 |
| 452 Y.Object.each(this.viewletConfig, function(viewlet, key) { |
| 453 // create viewlet instances using the base and supplied config |
| 454 viewlets[key] = Object.create(ViewletBase, viewlet); |
| 455 viewlets[key]._changedValues = []; |
| 456 viewlets[key]._eventHandles = []; |
| 457 }, this); |
| 458 |
| 459 return viewlets; |
| 460 }, |
| 461 |
| 462 /** |
| 463 Expands the basic objects provided in the viewlet config into the |
| 464 defineProperty format for Object.create() |
| 465 |
| 466 @method _expandViewletConfig |
| 467 @private |
| 468 */ |
| 469 _expandViewletConfig: function() { |
| 470 // uncomment below when we upgrade jshint |
| 471 // /*jshint -W089 */ |
| 472 for (var viewlet in this.viewletConfig) { |
| 473 // remove ifcheck when we upgrade jshint |
| 474 if (this.viewletConfig.hasOwnProperty(viewlet)) { |
| 475 for (var cfg in this.viewletConfig[viewlet]) { |
| 476 // remove ifcheck when we upgrade jshint |
| 477 if (this.viewletConfig[viewlet].hasOwnProperty(cfg)) { |
| 478 if (this.viewletConfig[viewlet][cfg].value === undefined) { |
| 479 this.viewletConfig[viewlet][cfg] = { |
| 480 value: this.viewletConfig[viewlet][cfg], |
| 481 writable: true |
| 482 }; |
| 483 } |
| 484 } |
| 485 } |
| 486 } |
| 487 } |
| 488 // uncomment below when we upgrade jshint |
| 489 // /*jshint +W089 */ |
| 490 }, |
| 491 |
| 492 /** |
| 493 Attaches events which cannot be attached using the container event object |
| 494 |
| 495 @method _setupEvents |
| 496 @private |
| 497 */ |
| 498 _setupEvents: function() { |
| 499 this._events.push( |
| 500 Y.one('window').after('resize', this.recalculateHeight, this)); |
| 501 }, |
| 502 |
| 503 /** |
| 504 Removes and destroys the container |
| 505 |
| 506 @method destructor |
| 507 */ |
| 508 destructor: function() { |
| 509 this._events.forEach(function(event) { |
| 510 event.detach(); |
| 511 }); |
| 512 this.bindingEngine.unbind(); |
| 513 this.get('container').remove().destroy(true); |
| 514 } |
| 515 |
| 516 }, { |
| 517 ATTRS: { |
| 518 /** |
| 519 Reference to the model |
| 520 |
| 521 @attribute model |
| 522 @default undefined |
| 523 */ |
| 524 model: {}, |
| 525 /** |
| 526 Reference to the database |
| 527 |
| 528 @attribute db |
| 529 @default undefined |
| 530 */ |
| 531 db: {}, |
| 532 /** |
| 533 Reference to the environment. |
| 534 |
| 535 @attribute env |
| 536 @default undefined |
| 537 */ |
| 538 env: {} |
| 539 } |
| 540 |
| 541 }); |
| 542 |
| 543 }, '', { |
| 544 requires: [ |
| 545 'juju-databinding', |
| 546 'view', |
| 547 'node', |
| 548 'base-build', |
| 549 'handlebars' |
| 550 ]}); |
LEFT | RIGHT |