1// MarionetteJS (Backbone.Marionette) 2// ---------------------------------- 3// v2.4.1 4// 5// Copyright (c)2015 Derick Bailey, Muted Solutions, LLC. 6// Distributed under MIT license 7// 8// http://marionettejs.com 9 10 11/*! 12 * Includes BabySitter 13 * https://github.com/marionettejs/backbone.babysitter/ 14 * 15 * Includes Wreqr 16 * https://github.com/marionettejs/backbone.wreqr/ 17 */ 18 19 20(function(root, factory) { 21 22 /* istanbul ignore next */ 23 if (typeof define === 'function' && define.amd) { 24 define(['backbone', 'underscore'], function(Backbone, _) { 25 return (root.Marionette = root.Mn = factory(root, Backbone, _)); 26 }); 27 } else if (typeof exports !== 'undefined') { 28 var Backbone = require('backbone'); 29 var _ = require('underscore'); 30 module.exports = factory(root, Backbone, _); 31 } else { 32 root.Marionette = root.Mn = factory(root, root.Backbone, root._); 33 } 34 35}(this, function(root, Backbone, _) { 36 'use strict'; 37 38 /* istanbul ignore next */ 39 // Backbone.BabySitter 40 // ------------------- 41 // v0.1.6 42 // 43 // Copyright (c)2015 Derick Bailey, Muted Solutions, LLC. 44 // Distributed under MIT license 45 // 46 // http://github.com/marionettejs/backbone.babysitter 47 (function(Backbone, _) { 48 "use strict"; 49 var previousChildViewContainer = Backbone.ChildViewContainer; 50 // BabySitter.ChildViewContainer 51 // ----------------------------- 52 // 53 // Provide a container to store, retrieve and 54 // shut down child views. 55 Backbone.ChildViewContainer = function(Backbone, _) { 56 // Container Constructor 57 // --------------------- 58 var Container = function(views) { 59 this._views = {}; 60 this._indexByModel = {}; 61 this._indexByCustom = {}; 62 this._updateLength(); 63 _.each(views, this.add, this); 64 }; 65 // Container Methods 66 // ----------------- 67 _.extend(Container.prototype, { 68 // Add a view to this container. Stores the view 69 // by `cid` and makes it searchable by the model 70 // cid (and model itself). Optionally specify 71 // a custom key to store an retrieve the view. 72 add: function(view, customIndex) { 73 var viewCid = view.cid; 74 // store the view 75 this._views[viewCid] = view; 76 // index it by model 77 if (view.model) { 78 this._indexByModel[view.model.cid] = viewCid; 79 } 80 // index by custom 81 if (customIndex) { 82 this._indexByCustom[customIndex] = viewCid; 83 } 84 this._updateLength(); 85 return this; 86 }, 87 // Find a view by the model that was attached to 88 // it. Uses the model's `cid` to find it. 89 findByModel: function(model) { 90 return this.findByModelCid(model.cid); 91 }, 92 // Find a view by the `cid` of the model that was attached to 93 // it. Uses the model's `cid` to find the view `cid` and 94 // retrieve the view using it. 95 findByModelCid: function(modelCid) { 96 var viewCid = this._indexByModel[modelCid]; 97 return this.findByCid(viewCid); 98 }, 99 // Find a view by a custom indexer. 100 findByCustom: function(index) { 101 var viewCid = this._indexByCustom[index]; 102 return this.findByCid(viewCid); 103 }, 104 // Find by index. This is not guaranteed to be a 105 // stable index. 106 findByIndex: function(index) { 107 return _.values(this._views)[index]; 108 }, 109 // retrieve a view by its `cid` directly 110 findByCid: function(cid) { 111 return this._views[cid]; 112 }, 113 // Remove a view 114 remove: function(view) { 115 var viewCid = view.cid; 116 // delete model index 117 if (view.model) { 118 delete this._indexByModel[view.model.cid]; 119 } 120 // delete custom index 121 _.any(this._indexByCustom, function(cid, key) { 122 if (cid === viewCid) { 123 delete this._indexByCustom[key]; 124 return true; 125 } 126 }, this); 127 // remove the view from the container 128 delete this._views[viewCid]; 129 // update the length 130 this._updateLength(); 131 return this; 132 }, 133 // Call a method on every view in the container, 134 // passing parameters to the call method one at a 135 // time, like `function.call`. 136 call: function(method) { 137 this.apply(method, _.tail(arguments)); 138 }, 139 // Apply a method on every view in the container, 140 // passing parameters to the call method one at a 141 // time, like `function.apply`. 142 apply: function(method, args) { 143 _.each(this._views, function(view) { 144 if (_.isFunction(view[method])) { 145 view[method].apply(view, args || []); 146 } 147 }); 148 }, 149 // Update the `.length` attribute on this container 150 _updateLength: function() { 151 this.length = _.size(this._views); 152 } 153 }); 154 // Borrowing this code from Backbone.Collection: 155 // http://backbonejs.org/docs/backbone.html#section-106 156 // 157 // Mix in methods from Underscore, for iteration, and other 158 // collection related features. 159 var methods = [ "forEach", "each", "map", "find", "detect", "filter", "select", "reject", "every", "all", "some", "any", "include", "contains", "invoke", "toArray", "first", "initial", "rest", "last", "without", "isEmpty", "pluck", "reduce" ]; 160 _.each(methods, function(method) { 161 Container.prototype[method] = function() { 162 var views = _.values(this._views); 163 var args = [ views ].concat(_.toArray(arguments)); 164 return _[method].apply(_, args); 165 }; 166 }); 167 // return the public API 168 return Container; 169 }(Backbone, _); 170 Backbone.ChildViewContainer.VERSION = "0.1.6"; 171 Backbone.ChildViewContainer.noConflict = function() { 172 Backbone.ChildViewContainer = previousChildViewContainer; 173 return this; 174 }; 175 return Backbone.ChildViewContainer; 176 })(Backbone, _); 177 178 /* istanbul ignore next */ 179 // Backbone.Wreqr (Backbone.Marionette) 180 // ---------------------------------- 181 // v1.3.1 182 // 183 // Copyright (c)2014 Derick Bailey, Muted Solutions, LLC. 184 // Distributed under MIT license 185 // 186 // http://github.com/marionettejs/backbone.wreqr 187 (function(Backbone, _) { 188 "use strict"; 189 var previousWreqr = Backbone.Wreqr; 190 var Wreqr = Backbone.Wreqr = {}; 191 Backbone.Wreqr.VERSION = "1.3.1"; 192 Backbone.Wreqr.noConflict = function() { 193 Backbone.Wreqr = previousWreqr; 194 return this; 195 }; 196 // Handlers 197 // -------- 198 // A registry of functions to call, given a name 199 Wreqr.Handlers = function(Backbone, _) { 200 "use strict"; 201 // Constructor 202 // ----------- 203 var Handlers = function(options) { 204 this.options = options; 205 this._wreqrHandlers = {}; 206 if (_.isFunction(this.initialize)) { 207 this.initialize(options); 208 } 209 }; 210 Handlers.extend = Backbone.Model.extend; 211 // Instance Members 212 // ---------------- 213 _.extend(Handlers.prototype, Backbone.Events, { 214 // Add multiple handlers using an object literal configuration 215 setHandlers: function(handlers) { 216 _.each(handlers, function(handler, name) { 217 var context = null; 218 if (_.isObject(handler) && !_.isFunction(handler)) { 219 context = handler.context; 220 handler = handler.callback; 221 } 222 this.setHandler(name, handler, context); 223 }, this); 224 }, 225 // Add a handler for the given name, with an 226 // optional context to run the handler within 227 setHandler: function(name, handler, context) { 228 var config = { 229 callback: handler, 230 context: context 231 }; 232 this._wreqrHandlers[name] = config; 233 this.trigger("handler:add", name, handler, context); 234 }, 235 // Determine whether or not a handler is registered 236 hasHandler: function(name) { 237 return !!this._wreqrHandlers[name]; 238 }, 239 // Get the currently registered handler for 240 // the specified name. Throws an exception if 241 // no handler is found. 242 getHandler: function(name) { 243 var config = this._wreqrHandlers[name]; 244 if (!config) { 245 return; 246 } 247 return function() { 248 var args = Array.prototype.slice.apply(arguments);
249 return config.callback.apply(config.context, args); 250 }; 251 }, 252 // Remove a handler for the specified name 253 removeHandler: function(name) { 254 delete this._wreqrHandlers[name]; 255 }, 256 // Remove all handlers from this registry 257 removeAllHandlers: function() { 258 this._wreqrHandlers = {}; 259 } 260 }); 261 return Handlers; 262 }(Backbone, _); 263 // Wreqr.CommandStorage 264 // -------------------- 265 // 266 // Store and retrieve commands for execution. 267 Wreqr.CommandStorage = function() { 268 "use strict"; 269 // Constructor function 270 var CommandStorage = function(options) { 271 this.options = options; 272 this._commands = {}; 273 if (_.isFunction(this.initialize)) { 274 this.initialize(options); 275 } 276 }; 277 // Instance methods 278 _.extend(CommandStorage.prototype, Backbone.Events, { 279 // Get an object literal by command name, that contains 280 // the `commandName` and the `instances` of all commands 281 // represented as an array of arguments to process 282 getCommands: function(commandName) { 283 var commands = this._commands[commandName]; 284 // we don't have it, so add it 285 if (!commands) { 286 // build the configuration 287 commands = { 288 command: commandName, 289 instances: [] 290 }; 291 // store it 292 this._commands[commandName] = commands; 293 } 294 return commands; 295 }, 296 // Add a command by name, to the storage and store the 297 // args for the command 298 addCommand: function(commandName, args) { 299 var command = this.getCommands(commandName); 300 command.instances.push(args); 301 }, 302 // Clear all commands for the given `commandName` 303 clearCommands: function(commandName) { 304 var command = this.getCommands(commandName); 305 command.instances = []; 306 } 307 }); 308 return CommandStorage; 309 }(); 310 // Wreqr.Commands 311 // -------------- 312 // 313 // A simple command pattern implementation. Register a command 314 // handler and execute it. 315 Wreqr.Commands = function(Wreqr) { 316 "use strict"; 317 return Wreqr.Handlers.extend({ 318 // default storage type 319 storageType: Wreqr.CommandStorage, 320 constructor: function(options) { 321 this.options = options || {}; 322 this._initializeStorage(this.options); 323 this.on("handler:add", this._executeCommands, this); 324 var args = Array.prototype.slice.call(arguments); 325 Wreqr.Handlers.prototype.constructor.apply(this, args); 326 }, 327 // Execute a named command with the supplied args 328 execute: function(name, args) { 329 name = arguments[0]; 330 args = Array.prototype.slice.call(arguments, 1); 331 if (this.hasHandler(name)) { 332 this.getHandler(name).apply(this, args); 333 } else { 334 this.storage.addCommand(name, args); 335 } 336 }, 337 // Internal method to handle bulk execution of stored commands 338 _executeCommands: function(name, handler, context) { 339 var command = this.storage.getCommands(name); 340 // loop through and execute all the stored command instances 341 _.each(command.instances, function(args) { 342 handler.apply(context, args); 343 }); 344 this.storage.clearCommands(name); 345 }, 346 // Internal method to initialize storage either from the type's 347 // `storageType` or the instance `options.storageType`. 348 _initializeStorage: function(options) { 349 var storage; 350 var StorageType = options.storageType || this.storageType; 351 if (_.isFunction(StorageType)) { 352 storage = new StorageType(); 353 } else { 354 storage = StorageType; 355 } 356 this.storage = storage; 357 } 358 }); 359 }(Wreqr); 360 // Wreqr.RequestResponse 361 // --------------------- 362 // 363 // A simple request/response implementation. Register a 364 // request handler, and return a response from it 365 Wreqr.RequestResponse = function(Wreqr) { 366 "use strict"; 367 return Wreqr.Handlers.extend({ 368 request: function() { 369 var name = arguments[0]; 370 var args = Array.prototype.slice.call(arguments, 1); 371 if (this.hasHandler(name)) { 372 return this.getHandler(name).apply(this, args); 373 } 374 } 375 }); 376 }(Wreqr); 377 // Event Aggregator 378 // ---------------- 379 // A pub-sub object that can be used to decouple various parts 380 // of an application through event-driven architecture. 381 Wreqr.EventAggregator = function(Backbone, _) { 382 "use strict"; 383 var EA = function() {}; 384 // Copy the `extend` function used by Backbone's classes 385 EA.extend = Backbone.Model.extend; 386 // Copy the basic Backbone.Events on to the event aggregator 387 _.extend(EA.prototype, Backbone.Events); 388 return EA; 389 }(Backbone, _); 390 // Wreqr.Channel 391 // -------------- 392 // 393 // An object that wraps the three messaging systems: 394 // EventAggregator, RequestResponse, Commands 395 Wreqr.Channel = function(Wreqr) { 396 "use strict";
397 var Channel = function(channelName) { 398 this.vent = new Backbone.Wreqr.EventAggregator(); 399 this.reqres = new Backbone.Wreqr.RequestResponse(); 400 this.commands = new Backbone.Wreqr.Commands(); 401 this.channelName = channelName; 402 }; 403 _.extend(Channel.prototype, { 404 // Remove all handlers from the messaging systems of this channel 405 reset: function() { 406 this.vent.off(); 407 this.vent.stopListening(); 408 this.reqres.removeAllHandlers(); 409 this.commands.removeAllHandlers(); 410 return this; 411 }, 412 // Connect a hash of events; one for each messaging system 413 connectEvents: function(hash, context) { 414 this._connect("vent", hash, context); 415 return this; 416 }, 417 connectCommands: function(hash, context) { 418 this._connect("commands", hash, context); 419 return this; 420 }, 421 connectRequests: function(hash, context) { 422 this._connect("reqres", hash, context); 423 return this; 424 }, 425 // Attach the handlers to a given message system `type` 426 _connect: function(type, hash, context) { 427 if (!hash) { 428 return; 429 } 430 context = context || this; 431 var method = type === "vent" ? "on" : "setHandler"; 432 _.each(hash, function(fn, eventName) { 433 this[type][method](eventName, _.bind(fn, context)); 434 }, this); 435 } 436 }); 437 return Channel; 438 }(Wreqr); 439 // Wreqr.Radio 440 // -------------- 441 // 442 // An object that lets you communicate with many channels. 443 Wreqr.radio = function(Wreqr) { 444 "use strict"; 445 var Radio = function() { 446 this._channels = {}; 447 this.vent = {}; 448 this.commands = {}; 449 this.reqres = {}; 450 this._proxyMethods(); 451 }; 452 _.extend(Radio.prototype, { 453 channel: function(channelName) { 454 if (!channelName) { 455 throw new Error("Channel must receive a name"); 456 } 457 return this._getChannel(channelName); 458 }, 459 _getChannel: function(channelName) { 460 var channel = this._channels[channelName]; 461 if (!channel) { 462 channel = new Wreqr.Channel(channelName); 463 this._channels[channelName] = channel; 464 } 465 return channel; 466 }, 467 _proxyMethods: function() { 468 _.each([ "vent", "commands", "reqres" ], function(system) { 469 _.each(messageSystems[system], function(method) { 470 this[system][method] = proxyMethod(this, system, method); 471 }, this); 472 }, this); 473 } 474 }); 475 var messageSystems = { 476 vent: [ "on", "off", "trigger", "once", "stopListening", "listenTo", "listenToOnce" ], 477 commands: [ "execute", "setHandler", "setHandlers", "removeHandler", "removeAllHandlers" ], 478 reqres: [ "request", "setHandler", "setHandlers", "removeHandler", "removeAllHandlers" ] 479 }; 480 var proxyMethod = function(radio, system, method) { 481 return function(channelName) { 482 var messageSystem = radio._getChannel(channelName)[system]; 483 var args = Array.prototype.slice.call(arguments, 1); 484 return messageSystem[method].apply(messageSystem, args); 485 }; 486 }; 487 return new Radio(); 488 }(Wreqr); 489 return Backbone.Wreqr; 490 })(Backbone, _); 491 492 var previousMarionette = root.Marionette; 493 var previousMn = root.Mn; 494 495 var Marionette = Backbone.Marionette = {}; 496 497 Marionette.VERSION = '2.4.1'; 498 499 Marionette.noConflict = function() { 500 root.Marionette = previousMarionette; 501 root.Mn = previousMn; 502 return this; 503 }; 504 505 Backbone.Marionette = Marionette; 506 507 // Get the Deferred creator for later use 508 Marionette.Deferred = Backbone.$.Deferred; 509 510 /* jshint unused: false *//* global console */ 511 512 // Helpers 513 // ------- 514 515 // Marionette.extend 516 // ----------------- 517 518 // Borrow the Backbone `extend` method so we can use it as needed 519 Marionette.extend = Backbone.Model.extend; 520 521 // Marionette.isNodeAttached 522 // ------------------------- 523 524 // Determine if `el` is a child of the document 525 Marionette.isNodeAttached = function(el) { 526 return Backbone.$.contains(document.documentElement, el); 527 }; 528 529 // Merge `keys` from `options` onto `this` 530 Marionette.mergeOptions = function(options, keys) { 531 if (!options) { return; } 532 _.extend(this, _.pick(options, keys)); 533 }; 534 535 // Marionette.getOption 536 // -------------------- 537 538 // Retrieve an object, function or other value from a target 539 // object or its `options`, with `options` taking precedence. 540 Marionette.getOption = function(target, optionName) { 541 if (!target || !optionName) { return; } 542 if (target.options && (target.options[optionName] !== undefined)) { 543 return target.options[optionName]; 544 } else { 545 return target[optionName]; 546 } 547 }; 548 549 // Proxy `Marionette.getOption` 550 Marionette.proxyGetOption = function(optionName) { 551 return Marionette.getOption(this, optionName); 552 }; 553 554 // Similar to `_.result`, this is a simple helper 555 // If a function is provided we call it with context 556 // otherwise just return the value. If the value is 557 // undefined return a default value 558 Marionette._getValue = function(value, context, params) { 559 if (_.isFunction(value)) { 560 value = params ? value.apply(context, params) : value.call(context); 561 } 562 return value; 563 }; 564 565 // Marionette.normalizeMethods 566 // ---------------------- 567 568 // Pass in a mapping of events => functions or function names 569 // and return a mapping of events => functions 570 Marionette.normalizeMethods = function(hash) { 571 return _.reduce(hash, function(normalizedHash, method, name) { 572 if (!_.isFunction(method)) { 573 method = this[method]; 574 } 575 if (method) { 576 normalizedHash[name] = method; 577 } 578 return normalizedHash; 579 }, {}, this); 580 }; 581 582 // utility method for parsing @ui. syntax strings 583 // into associated selector 584 Marionette.normalizeUIString = function(uiString, ui) { 585 return uiString.replace(/@ui\.[a-zA-Z_$0-9]*/g, function(r) { 586 return ui[r.slice(4)]; 587 }); 588 }; 589 590 // allows for the use of the @ui. syntax within 591 // a given key for triggers and events 592 // swaps the @ui with the associated selector. 593 // Returns a new, non-mutated, parsed events hash. 594 Marionette.normalizeUIKeys = function(hash, ui) { 595 return _.reduce(hash, function(memo, val, key) { 596 var normalizedKey = Marionette.normalizeUIString(key, ui); 597 memo[normalizedKey] = val; 598 return memo; 599 }, {}); 600 }; 601 602 // allows for the use of the @ui. syntax within 603 // a given value for regions 604 // swaps the @ui with the associated selector 605 Marionette.normalizeUIValues = function(hash, ui, properties) { 606 _.each(hash, function(val, key) { 607 if (_.isString(val)) { 608 hash[key] = Marionette.normalizeUIString(val, ui); 609 } else if (_.isObject(val) && _.isArray(properties)) { 610 _.extend(val, Marionette.normalizeUIValues(_.pick(val, properties), ui)); 611 /* Value is an object, and we got an array of embedded property names to normalize. */ 612 _.each(properties, function(property) { 613 var propertyVal = val[property]; 614 if (_.isString(propertyVal)) { 615 val[property] = Marionette.normalizeUIString(propertyVal, ui); 616 } 617 }); 618 } 619 }); 620 return hash; 621 }; 622 623 // Mix in methods from Underscore, for iteration, and other 624 // collection related features. 625 // Borrowing this code from Backbone.Collection: 626 // http://backbonejs.org/docs/backbone.html#section-121 627 Marionette.actAsCollection = function(object, listProperty) { 628 var methods = ['forEach', 'each', 'map', 'find', 'detect', 'filter', 629 'select', 'reject', 'every', 'all', 'some', 'any', 'include', 630 'contains', 'invoke', 'toArray', 'first', 'initial', 'rest', 631 'last', 'without', 'isEmpty', 'pluck']; 632 633 _.each(methods, function(method) { 634 object[method] = function() { 635 var list = _.values(_.result(this, listProperty)); 636 var args = [list].concat(_.toArray(arguments)); 637 return _[method].apply(_, args); 638 }; 639 }); 640 }; 641 642 var deprecate = Marionette.deprecate = function(message, test) { 643 if (_.isObject(message)) { 644 message = ( 645 message.prev + ' is going to be removed in the future. ' + 646 'Please use ' + message.next + ' instead.' + 647 (message.url ? ' See: ' + message.url : '') 648 ); 649 } 650 651 if ((test === undefined || !test) && !deprecate._cache[message]) { 652 deprecate._warn('Deprecation warning: ' + message); 653 deprecate._cache[message] = true; 654 } 655 }; 656 657 deprecate._warn = typeof console !== 'undefined' && (console.warn || console.log) || function() {}; 658 deprecate._cache = {}; 659 660 /* jshint maxstatements: 14, maxcomplexity: 7 */ 661
662 // Trigger Method 663 // -------------- 664 665 Marionette._triggerMethod = (function() { 666 // split the event name on the ":" 667 var splitter = /(^|:)(\w)/gi; 668 669 // take the event section ("section1:section2:section3") 670 // and turn it in to uppercase name 671 function getEventName(match, prefix, eventName) { 672 return eventName.toUpperCase(); 673 } 674 675 return function(context, event, args) { 676 var noEventArg = arguments.length < 3; 677 if (noEventArg) { 678 args = event; 679 event = args[0]; 680 } 681 682 // get the method name from the event name 683 var methodName = 'on' + event.replace(splitter, getEventName); 684 var method = context[methodName]; 685 var result; 686 687 // call the onMethodName if it exists 688 if (_.isFunction(method)) { 689 // pass all args, except the event name 690 result = method.apply(context, noEventArg ? _.rest(args) : args); 691 } 692 693 // trigger the event, if a trigger method exists 694 if (_.isFunction(context.trigger)) { 695 if (noEventArg + args.length > 1) { 696 context.trigger.apply(context, noEventArg ? args : [event].concat(_.drop(args, 0))); 697 } else { 698 context.trigger(event); 699 } 700 } 701 702 return result; 703 }; 704 })(); 705 706 // Trigger an event and/or a corresponding method name. Examples: 707 // 708 // `this.triggerMethod("foo")` will trigger the "foo" event and 709 // call the "onFoo" method. 710 // 711 // `this.triggerMethod("foo:bar")` will trigger the "foo:bar" event and 712 // call the "onFooBar" method. 713 Marionette.triggerMethod = function(event) { 714 return Marionette._triggerMethod(this, arguments); 715 }; 716 717 // triggerMethodOn invokes triggerMethod on a specific context 718 // 719 // e.g. `Marionette.triggerMethodOn(view, 'show')` 720 // will trigger a "show" event or invoke onShow the view. 721 Marionette.triggerMethodOn = function(context) { 722 var fnc = _.isFunction(context.triggerMethod) ? 723 context.triggerMethod : 724 Marionette.triggerMethod; 725 726 return fnc.apply(context, _.rest(arguments)); 727 }; 728 729 // DOM Refresh 730 // ----------- 731 732 // Monitor a view's state, and after it has been rendered and shown 733 // in the DOM, trigger a "dom:refresh" event every time it is 734 // re-rendered. 735 736 Marionette.MonitorDOMRefresh = function(view) { 737 738 // track when the view has been shown in the DOM, 739 // using a Marionette.Region (or by other means of triggering "show") 740 function handleShow() { 741 view._isShown = true; 742 triggerDOMRefresh(); 743 } 744 745 // track when the view has been rendered 746 function handleRender() { 747 view._isRendered = true; 748 triggerDOMRefresh(); 749 } 750 751 // Trigger the "dom:refresh" event and corresponding "onDomRefresh" method 752 function triggerDOMRefresh() { 753 if (view._isShown && view._isRendered && Marionette.isNodeAttached(view.el)) { 754 if (_.isFunction(view.triggerMethod)) { 755 view.triggerMethod('dom:refresh'); 756 } 757 } 758 } 759 760 view.on({ 761 show: handleShow, 762 render: handleRender 763 }); 764 }; 765 766 /* jshint maxparams: 5 */ 767 768 // Bind Entity Events & Unbind Entity Events 769 // ----------------------------------------- 770 // 771 // These methods are used to bind/unbind a backbone "entity" (e.g. collection/model) 772 // to methods on a target object. 773 // 774 // The first parameter, `target`, must have the Backbone.Events module mixed in. 775 // 776 // The second parameter is the `entity` (Backbone.Model, Backbone.Collection or 777 // any object that has Backbone.Events mixed in) to bind the events from. 778 // 779 // The third parameter is a hash of { "event:name": "eventHandler" } 780 // configuration. Multiple handlers can be separated by a space. A 781 // function can be supplied instead of a string handler name. 782 783 (function(Marionette) { 784 'use strict'; 785 786 // Bind the event to handlers specified as a string of 787 // handler names on the target object 788 function bindFromStrings(target, entity, evt, methods) { 789 var methodNames = methods.split(/\s+/); 790 791 _.each(methodNames, function(methodName) { 792 793 var method = target[methodName];
794 if (!method) { 795 throw new Marionette.Error('Method "' + methodName + 796 '" was configured as an event handler, but does not exist.'); 797 } 798 799 target.listenTo(entity, evt, method); 800 }); 801 } 802 803 // Bind the event to a supplied callback function 804 function bindToFunction(target, entity, evt, method) { 805 target.listenTo(entity, evt, method); 806 } 807 808 // Bind the event to handlers specified as a string of 809 // handler names on the target object 810 function unbindFromStrings(target, entity, evt, methods) { 811 var methodNames = methods.split(/\s+/); 812 813 _.each(methodNames, function(methodName) { 814 var method = target[methodName]; 815 target.stopListening(entity, evt, method); 816 }); 817 } 818 819 // Bind the event to a supplied callback function 820 function unbindToFunction(target, entity, evt, method) { 821 target.stopListening(entity, evt, method); 822 } 823 824 // generic looping function 825 function iterateEvents(target, entity, bindings, functionCallback, stringCallback) { 826 if (!entity || !bindings) { return; } 827 828 // type-check bindings 829 if (!_.isObject(bindings)) { 830 throw new Marionette.Error({ 831 message: 'Bindings must be an object or function.', 832 url: 'marionette.functions.html#marionettebindentityevents' 833 }); 834 } 835 836 // allow the bindings to be a function 837 bindings = Marionette._getValue(bindings, target); 838 839 // iterate the bindings and bind them 840 _.each(bindings, function(methods, evt) { 841 842 // allow for a function as the handler, 843 // or a list of event names as a string 844 if (_.isFunction(methods)) { 845 functionCallback(target, entity, evt, methods); 846 } else { 847 stringCallback(target, entity, evt, methods); 848 } 849 850 }); 851 } 852 853 // Export Public API 854 Marionette.bindEntityEvents = function(target, entity, bindings) { 855 iterateEvents(target, entity, bindings, bindToFunction, bindFromStrings); 856 }; 857 858 Marionette.unbindEntityEvents = function(target, entity, bindings) { 859 iterateEvents(target, entity, bindings, unbindToFunction, unbindFromStrings); 860 }; 861 862 // Proxy `bindEntityEvents` 863 Marionette.proxyBindEntityEvents = function(entity, bindings) { 864 return Marionette.bindEntityEvents(this, entity, bindings); 865 }; 866 867 // Proxy `unbindEntityEvents` 868 Marionette.proxyUnbindEntityEvents = function(entity, bindings) { 869 return Marionette.unbindEntityEvents(this, entity, bindings); 870 }; 871 })(Marionette); 872 873 874 // Error 875 // ----- 876 877 var errorProps = ['description', 'fileName', 'lineNumber', 'name', 'message', 'number']; 878 879 Marionette.Error = Marionette.extend.call(Error, { 880 urlRoot: 'http://marionettejs.com/docs/v' + Marionette.VERSION + '/', 881 882 constructor: function(message, options) { 883 if (_.isObject(message)) { 884 options = message; 885 message = options.message; 886 } else if (!options) { 887 options = {}; 888 } 889 890 var error = Error.call(this, message); 891 _.extend(this, _.pick(error, errorProps), _.pick(options, errorProps)); 892 893 this.captureStackTrace(); 894 895 if (options.url) { 896 this.url = this.urlRoot + options.url; 897 } 898 }, 899 900 captureStackTrace: function() { 901 if (Error.captureStackTrace) { 902 Error.captureStackTrace(this, Marionette.Error); 903 } 904 }, 905 906 toString: function() { 907 return this.name + ': ' + this.message + (this.url ? ' See: ' + this.url : ''); 908 } 909 }); 910 911 Marionette.Error.extend = Marionette.extend; 912 913 // Callbacks 914 // --------- 915 916 // A simple way of managing a collection of callbacks 917 // and executing them at a later point in time, using jQuery's 918 // `Deferred` object. 919 Marionette.Callbacks = function() { 920 this._deferred = Marionette.Deferred(); 921 this._callbacks = []; 922 }; 923 924 _.extend(Marionette.Callbacks.prototype, { 925
926 // Add a callback to be executed. Callbacks added here are 927 // guaranteed to execute, even if they are added after the 928 // `run` method is called. 929 add: function(callback, contextOverride) { 930 var promise = _.result(this._deferred, 'promise'); 931 932 this._callbacks.push({cb: callback, ctx: contextOverride}); 933 934 promise.then(function(args) { 935 if (contextOverride) { args.context = contextOverride; } 936 callback.call(args.context, args.options); 937 }); 938 }, 939 940 // Run all registered callbacks with the context specified. 941 // Additional callbacks can be added after this has been run 942 // and they will still be executed. 943 run: function(options, context) { 944 this._deferred.resolve({ 945 options: options, 946 context: context 947 }); 948 }, 949 950 // Resets the list of callbacks to be run, allowing the same list 951 // to be run multiple times - whenever the `run` method is called. 952 reset: function() { 953 var callbacks = this._callbacks; 954 this._deferred = Marionette.Deferred(); 955 this._callbacks = []; 956 957 _.each(callbacks, function(cb) { 958 this.add(cb.cb, cb.ctx); 959 }, this); 960 } 961 }); 962 963 // Controller 964 // ---------- 965 966 // A multi-purpose object to use as a controller for 967 // modules and routers, and as a mediator for workflow 968 // and coordination of other objects, views, and more. 969 Marionette.Controller = function(options) { 970 this.options = options || {}; 971 972 if (_.isFunction(this.initialize)) { 973 this.initialize(this.options); 974 } 975 }; 976 977 Marionette.Controller.extend = Marionette.extend; 978 979 // Controller Methods 980 // -------------- 981 982 // Ensure it can trigger events with Backbone.Events 983 _.extend(Marionette.Controller.prototype, Backbone.Events, { 984 destroy: function() { 985 Marionette._triggerMethod(this, 'before:destroy', arguments); 986 Marionette._triggerMethod(this, 'destroy', arguments); 987 988 this.stopListening(); 989 this.off(); 990 return this; 991 }, 992 993 // import the `triggerMethod` to trigger events with corresponding 994 // methods if the method exists 995 triggerMethod: Marionette.triggerMethod, 996 997 // A handy way to merge options onto the instance 998 mergeOptions: Marionette.mergeOptions, 999 1000 // Proxy `getOption` to enable getting options from this or this.options by name. 1001 getOption: Marionette.proxyGetOption 1002 1003 }); 1004 1005 // Object 1006 // ------ 1007 1008 // A Base Class that other Classes should descend from. 1009 // Object borrows many conventions and utilities from Backbone. 1010 Marionette.Object = function(options) { 1011 this.options = _.extend({}, _.result(this, 'options'), options); 1012 1013 this.initialize.apply(this, arguments); 1014 }; 1015 1016 Marionette.Object.extend = Marionette.extend; 1017 1018 // Object Methods 1019 // -------------- 1020 1021 // Ensure it can trigger events with Backbone.Events 1022 _.extend(Marionette.Object.prototype, Backbone.Events, { 1023 1024 //this is a noop method intended to be overridden by classes that extend from this base 1025 initialize: function() {}, 1026 1027 destroy: function() { 1028 this.triggerMethod('before:destroy'); 1029 this.triggerMethod('destroy'); 1030 this.stopListening(); 1031 1032 return this; 1033 }, 1034 1035 // Import the `triggerMethod` to trigger events with corresponding 1036 // methods if the method exists 1037 triggerMethod: Marionette.triggerMethod, 1038 1039 // A handy way to merge options onto the instance 1040 mergeOptions: Marionette.mergeOptions, 1041 1042 // Proxy `getOption` to enable getting options from this or this.options by name. 1043 getOption: Marionette.proxyGetOption, 1044 1045 // Proxy `bindEntityEvents` to enable binding view's events from another entity. 1046 bindEntityEvents: Marionette.proxyBindEntityEvents, 1047 1048 // Proxy `unbindEntityEvents` to enable unbinding view's events from another entity. 1049 unbindEntityEvents: Marionette.proxyUnbindEntityEvents 1050 }); 1051 1052 /* jshint maxcomplexity: 16, maxstatements: 45, maxlen: 120 */ 1053 1054 // Region 1055 // ------ 1056 1057 // Manage the visual regions of your composite application. See 1058 // http://lostechies.com/derickbailey/2011/12/12/composite-js-apps-regions-and-region-managers/ 1059 1060 Marionette.Region = Marionette.Object.extend({ 1061 constructor: function(options) { 1062 1063 // set options temporarily so that we can get `el`. 1064 // options will be overriden by Object.constructor 1065 this.options = options || {}; 1066 this.el = this.getOption('el'); 1067 1068 // Handle when this.el is passed in as a $ wrapped element. 1069 this.el = this.el instanceof Backbone.$ ? this.el[0] : this.el; 1070 1071 if (!this.el) { 1072 throw new Marionette.Error({
1073 name: 'NoElError', 1074 message: 'An "el" must be specified for a region.' 1075 }); 1076 } 1077 1078 this.$el = this.getEl(this.el); 1079 Marionette.Object.call(this, options); 1080 }, 1081 1082 // Displays a backbone view instance inside of the region. 1083 // Handles calling the `render` method for you. Reads content 1084 // directly from the `el` attribute. Also calls an optional 1085 // `onShow` and `onDestroy` method on your view, just after showing 1086 // or just before destroying the view, respectively. 1087 // The `preventDestroy` option can be used to prevent a view from 1088 // the old view being destroyed on show. 1089 // The `forceShow` option can be used to force a view to be 1090 // re-rendered if it's already shown in the region. 1091 show: function(view, options) { 1092 if (!this._ensureElement()) { 1093 return; 1094 } 1095 1096 this._ensureViewIsIntact(view); 1097 1098 var showOptions = options || {}; 1099 var isDifferentView = view !== this.currentView; 1100 var preventDestroy = !!showOptions.preventDestroy; 1101 var forceShow = !!showOptions.forceShow; 1102 1103 // We are only changing the view if there is a current view to change to begin with 1104 var isChangingView = !!this.currentView; 1105 1106 // Only destroy the current view if we don't want to `preventDestroy` and if 1107 // the view given in the first argument is different than `currentView` 1108 var _shouldDestroyView = isDifferentView && !preventDestroy; 1109 1110 // Only show the view given in the first argument if it is different than 1111 // the current view or if we want to re-show the view. Note that if 1112 // `_shouldDestroyView` is true, then `_shouldShowView` is also necessarily true. 1113 var _shouldShowView = isDifferentView || forceShow; 1114 1115 if (isChangingView) { 1116 this.triggerMethod('before:swapOut', this.currentView, this, options); 1117 } 1118 1119 if (this.currentView) { 1120 delete this.currentView._parent; 1121 } 1122 1123 if (_shouldDestroyView) { 1124 this.empty(); 1125 1126 // A `destroy` event is attached to the clean up manually removed views. 1127 // We need to detach this event when a new view is going to be shown as it 1128 // is no longer relevant. 1129 } else if (isChangingView && _shouldShowView) { 1130 this.currentView.off('destroy', this.empty, this); 1131 } 1132 1133 if (_shouldShowView) { 1134 1135 // We need to listen for if a view is destroyed 1136 // in a way other than through the region. 1137 // If this happens we need to remove the reference 1138 // to the currentView since once a view has been destroyed 1139 // we can not reuse it. 1140 view.once('destroy', this.empty, this); 1141 view.render(); 1142 1143 view._parent = this; 1144 1145 if (isChangingView) { 1146 this.triggerMethod('before:swap', view, this, options); 1147 } 1148 1149 this.triggerMethod('before:show', view, this, options); 1150 Marionette.triggerMethodOn(view, 'before:show', view, this, options); 1151 1152 if (isChangingView) { 1153 this.triggerMethod('swapOut', this.currentView, this, options); 1154 } 1155 1156 // An array of views that we're about to display 1157 var attachedRegion = Marionette.isNodeAttached(this.el); 1158 1159 // The views that we're about to attach to the document 1160 // It's important that we prevent _getNestedViews from being executed unnecessarily 1161 // as it's a potentially-slow method 1162 var displayedViews = []; 1163 1164 var triggerBeforeAttach = showOptions.triggerBeforeAttach || this.triggerBeforeAttach; 1165 var triggerAttach = showOptions.triggerAttach || this.triggerAttach; 1166 1167 if (attachedRegion && triggerBeforeAttach) { 1168 displayedViews = this._displayedViews(view); 1169 this._triggerAttach(displayedViews, 'before:'); 1170 } 1171 1172 this.attachHtml(view); 1173 this.currentView = view; 1174 1175 if (attachedRegion && triggerAttach) { 1176 displayedViews = this._displayedViews(view); 1177 this._triggerAttach(displayedViews); 1178 } 1179 1180 if (isChangingView) { 1181 this.triggerMethod('swap', view, this, options); 1182 } 1183 1184 this.triggerMethod('show', view, this, options); 1185 Marionette.triggerMethodOn(view, 'show', view, this, options); 1186 1187 return this; 1188 } 1189 1190 return this; 1191 }, 1192 1193 triggerBeforeAttach: true, 1194 triggerAttach: true, 1195 1196 _triggerAttach: function(views, prefix) { 1197 var eventName = (prefix || '') + 'attach'; 1198 _.each(views, function(view) { 1199 Marionette.triggerMethodOn(view, eventName, view, this); 1200 }, this); 1201 }, 1202 1203 _displayedViews: function(view) { 1204 return _.union([view], _.result(view, '_getNestedViews') || []); 1205 }, 1206 1207 _ensureElement: function() { 1208 if (!_.isObject(this.el)) { 1209 this.$el = this.getEl(this.el); 1210 this.el = this.$el[0]; 1211 } 1212 1213 if (!this.$el || this.$el.length === 0) { 1214 if (this.getOption('allowMissingEl')) { 1215 return false;
1216 } else { 1217 throw new Marionette.Error('An "el" ' + this.$el.selector + ' must exist in DOM'); 1218 } 1219 } 1220 return true; 1221 }, 1222 1223 _ensureViewIsIntact: function(view) { 1224 if (!view) { 1225 throw new Marionette.Error({ 1226 name: 'ViewNotValid', 1227 message: 'The view passed is undefined and therefore invalid. You must pass a view instance to show.' 1228 }); 1229 } 1230 1231 if (view.isDestroyed) { 1232 throw new Marionette.Error({ 1233 name: 'ViewDestroyedError', 1234 message: 'View (cid: "' + view.cid + '") has already been destroyed and cannot be used.' 1235 }); 1236 } 1237 }, 1238 1239 // Override this method to change how the region finds the DOM 1240 // element that it manages. Return a jQuery selector object scoped 1241 // to a provided parent el or the document if none exists. 1242 getEl: function(el) { 1243 return Backbone.$(el, Marionette._getValue(this.options.parentEl, this)); 1244 }, 1245 1246 // Override this method to change how the new view is 1247 // appended to the `$el` that the region is managing 1248 attachHtml: function(view) { 1249 this.$el.contents().detach(); 1250 1251 this.el.appendChild(view.el); 1252 }, 1253 1254 // Destroy the current view, if there is one. If there is no 1255 // current view, it does nothing and returns immediately. 1256 empty: function(options) { 1257 var view = this.currentView; 1258 1259 var preventDestroy = Marionette._getValue(options, 'preventDestroy', this); 1260 // If there is no view in the region 1261 // we should not remove anything 1262 if (!view) { return; } 1263 1264 view.off('destroy', this.empty, this); 1265 this.triggerMethod('before:empty', view); 1266 if (!preventDestroy) { 1267 this._destroyView(); 1268 } 1269 this.triggerMethod('empty', view); 1270 1271 // Remove region pointer to the currentView 1272 delete this.currentView; 1273 1274 if (preventDestroy) { 1275 this.$el.contents().detach(); 1276 } 1277 1278 return this; 1279 }, 1280 1281 // call 'destroy' or 'remove', depending on which is found 1282 // on the view (if showing a raw Backbone view or a Marionette View) 1283 _destroyView: function() { 1284 var view = this.currentView; 1285 1286 if (view.destroy && !view.isDestroyed) { 1287 view.destroy(); 1288 } else if (view.remove) { 1289 view.remove(); 1290 1291 // appending isDestroyed to raw Backbone View allows regions 1292 // to throw a ViewDestroyedError for this view 1293 view.isDestroyed = true; 1294 } 1295 }, 1296 1297 // Attach an existing view to the region. This 1298 // will not call `render` or `onShow` for the new view, 1299 // and will not replace the current HTML for the `el` 1300 // of the region. 1301 attachView: function(view) { 1302 this.currentView = view; 1303 return this; 1304 }, 1305 1306 // Checks whether a view is currently present within 1307 // the region. Returns `true` if there is and `false` if 1308 // no view is present. 1309 hasView: function() { 1310 return !!this.currentView; 1311 }, 1312 1313 // Reset the region by destroying any existing view and 1314 // clearing out the cached `$el`. The next time a view 1315 // is shown via this region, the region will re-query the 1316 // DOM for the region's `el`. 1317 reset: function() { 1318 this.empty(); 1319 1320 if (this.$el) { 1321 this.el = this.$el.selector; 1322 } 1323 1324 delete this.$el; 1325 return this; 1326 } 1327 1328 }, 1329 1330 // Static Methods 1331 { 1332 1333 // Build an instance of a region by passing in a configuration object 1334 // and a default region class to use if none is specified in the config. 1335 // 1336 // The config object should either be a string as a jQuery DOM selector, 1337 // a Region class directly, or an object literal that specifies a selector, 1338 // a custom regionClass, and any options to be supplied to the region: 1339 // 1340 // ```js 1341 // { 1342 // selector: "#foo", 1343 // regionClass: MyCustomRegion, 1344 // allowMissingEl: false 1345 // } 1346 // ``` 1347 // 1348 buildRegion: function(regionConfig, DefaultRegionClass) { 1349 if (_.isString(regionConfig)) { 1350 return this._buildRegionFromSelector(regionConfig, DefaultRegionClass); 1351 } 1352 1353 if (regionConfig.selector || regionConfig.el || regionConfig.regionClass) { 1354 return this._buildRegionFromObject(regionConfig, DefaultRegionClass); 1355 } 1356 1357 if (_.isFunction(regionConfig)) { 1358 return this._buildRegionFromRegionClass(regionConfig); 1359 } 1360 1361 throw new Marionette.Error({ 1362 message: 'Improper region configuration type.', 1363 url: 'marionette.region.html#region-configuration-types' 1364 }); 1365 }, 1366 1367 // Build the region from a string selector like '#foo-region' 1368 _buildRegionFromSelector: function(selector, DefaultRegionClass) { 1369 return new DefaultRegionClass({el: selector}); 1370 }, 1371 1372 // Build the region from a configuration object 1373 // ```js 1374 // { selector: '#foo', regionClass: FooRegion, allowMissingEl: false } 1375 // ``` 1376 _buildRegionFromObject: function(regionConfig, DefaultRegionClass) { 1377 var RegionClass = regionConfig.regionClass || DefaultRegionClass; 1378 var options = _.omit(regionConfig, 'selector', 'regionClass'); 1379 1380 if (regionConfig.selector && !options.el) { 1381 options.el = regionConfig.selector; 1382 } 1383 1384 return new RegionClass(options); 1385 }, 1386 1387 // Build the region directly from a given `RegionClass` 1388 _buildRegionFromRegionClass: function(RegionClass) { 1389 return new RegionClass(); 1390 } 1391 }); 1392 1393 // Region Manager 1394 // -------------- 1395 1396 // Manage one or more related `Marionette.Region` objects. 1397 Marionette.RegionManager = Marionette.Controller.extend({ 1398 constructor: function(options) { 1399 this._regions = {}; 1400 this.length = 0; 1401 1402 Marionette.Controller.call(this, options); 1403 1404 this.addRegions(this.getOption('regions')); 1405 }, 1406 1407 // Add multiple regions using an object literal or a 1408 // function that returns an object literal, where 1409 // each key becomes the region name, and each value is 1410 // the region definition. 1411 addRegions: function(regionDefinitions, defaults) { 1412 regionDefinitions = Marionette._getValue(regionDefinitions, this, arguments); 1413 1414 return _.reduce(regionDefinitions, function(regions, definition, name) { 1415 if (_.isString(definition)) { 1416 definition = {selector: definition}; 1417 } 1418 if (definition.selector) { 1419 definition = _.defaults({}, definition, defaults); 1420 } 1421 1422 regions[name] = this.addRegion(name, definition); 1423 return regions; 1424 }, {}, this); 1425 }, 1426 1427 // Add an individual region to the region manager, 1428 // and return the region instance 1429 addRegion: function(name, definition) { 1430 var region; 1431 1432 if (definition instanceof Marionette.Region) { 1433 region = definition; 1434 } else { 1435 region = Marionette.Region.buildRegion(definition, Marionette.Region); 1436 } 1437 1438 this.triggerMethod('before:add:region', name, region); 1439 1440 region._parent = this; 1441 this._store(name, region); 1442 1443 this.triggerMethod('add:region', name, region); 1444 return region; 1445 }, 1446 1447 // Get a region by name 1448 get: function(name) { 1449 return this._regions[name]; 1450 }, 1451 1452 // Gets all the regions contained within 1453 // the `regionManager` instance. 1454 getRegions: function() { 1455 return _.clone(this._regions); 1456 }, 1457
1458 // Remove a region by name 1459 removeRegion: function(name) { 1460 var region = this._regions[name]; 1461 this._remove(name, region); 1462 1463 return region; 1464 }, 1465 1466 // Empty all regions in the region manager, and 1467 // remove them 1468 removeRegions: function() { 1469 var regions = this.getRegions(); 1470 _.each(this._regions, function(region, name) { 1471 this._remove(name, region); 1472 }, this); 1473 1474 return regions; 1475 }, 1476 1477 // Empty all regions in the region manager, but 1478 // leave them attached 1479 emptyRegions: function() { 1480 var regions = this.getRegions(); 1481 _.invoke(regions, 'empty'); 1482 return regions; 1483 }, 1484 1485 // Destroy all regions and shut down the region 1486 // manager entirely 1487 destroy: function() { 1488 this.removeRegions(); 1489 return Marionette.Controller.prototype.destroy.apply(this, arguments); 1490 }, 1491 1492 // internal method to store regions 1493 _store: function(name, region) { 1494 if (!this._regions[name]) { 1495 this.length++; 1496 } 1497 1498 this._regions[name] = region; 1499 }, 1500 1501 // internal method to remove a region 1502 _remove: function(name, region) { 1503 this.triggerMethod('before:remove:region', name, region); 1504 region.empty(); 1505 region.stopListening(); 1506 1507 delete region._parent; 1508 delete this._regions[name]; 1509 this.length--; 1510 this.triggerMethod('remove:region', name, region); 1511 } 1512 }); 1513 1514 Marionette.actAsCollection(Marionette.RegionManager.prototype, '_regions'); 1515 1516 1517 // Template Cache 1518 // -------------- 1519 1520 // Manage templates stored in `<script>` blocks, 1521 // caching them for faster access. 1522 Marionette.TemplateCache = function(templateId) { 1523 this.templateId = templateId; 1524 }; 1525 1526 // TemplateCache object-level methods. Manage the template 1527 // caches from these method calls instead of creating 1528 // your own TemplateCache instances 1529 _.extend(Marionette.TemplateCache, { 1530 templateCaches: {}, 1531 1532 // Get the specified template by id. Either 1533 // retrieves the cached version, or loads it 1534 // from the DOM. 1535 get: function(templateId, options) { 1536 var cachedTemplate = this.templateCaches[templateId]; 1537 1538 if (!cachedTemplate) { 1539 cachedTemplate = new Marionette.TemplateCache(templateId); 1540 this.templateCaches[templateId] = cachedTemplate; 1541 } 1542 1543 return cachedTemplate.load(options); 1544 }, 1545 1546 // Clear templates from the cache. If no arguments 1547 // are specified, clears all templates: 1548 // `clear()` 1549 // 1550 // If arguments are specified, clears each of the 1551 // specified templates from the cache: 1552 // `clear("#t1", "#t2", "...")` 1553 clear: function() { 1554 var i; 1555 var args = _.toArray(arguments); 1556 var length = args.length; 1557 1558 if (length > 0) { 1559 for (i = 0; i < length; i++) { 1560 delete this.templateCaches[args[i]]; 1561 } 1562 } else { 1563 this.templateCaches = {}; 1564 } 1565 } 1566 }); 1567 1568 // TemplateCache instance methods, allowing each 1569 // template cache object to manage its own state 1570 // and know whether or not it has been loaded 1571 _.extend(Marionette.TemplateCache.prototype, { 1572
1573 // Internal method to load the template 1574 load: function(options) { 1575 // Guard clause to prevent loading this template more than once 1576 if (this.compiledTemplate) { 1577 return this.compiledTemplate; 1578 } 1579 1580 // Load the template and compile it 1581 var template = this.loadTemplate(this.templateId, options); 1582 this.compiledTemplate = this.compileTemplate(template, options); 1583 1584 return this.compiledTemplate; 1585 }, 1586 1587 // Load a template from the DOM, by default. Override 1588 // this method to provide your own template retrieval 1589 // For asynchronous loading with AMD/RequireJS, consider 1590 // using a template-loader plugin as described here: 1591 // https://github.com/marionettejs/backbone.marionette/wiki/Using-marionette-with-requirejs 1592 loadTemplate: function(templateId, options) { 1593 var template = Backbone.$(templateId).html(); 1594 1595 if (!template || template.length === 0) { 1596 throw new Marionette.Error({ 1597 name: 'NoTemplateError', 1598 message: 'Could not find template: "' + templateId + '"' 1599 }); 1600 } 1601 1602 return template; 1603 }, 1604 1605 // Pre-compile the template before caching it. Override 1606 // this method if you do not need to pre-compile a template 1607 // (JST / RequireJS for example) or if you want to change 1608 // the template engine used (Handebars, etc). 1609 compileTemplate: function(rawTemplate, options) { 1610 return _.template(rawTemplate, options); 1611 } 1612 }); 1613 1614 // Renderer 1615 // -------- 1616 1617 // Render a template with data by passing in the template 1618 // selector and the data to render. 1619 Marionette.Renderer = { 1620 1621 // Render a template with data. The `template` parameter is 1622 // passed to the `TemplateCache` object to retrieve the 1623 // template function. Override this method to provide your own 1624 // custom rendering and template handling for all of Marionette. 1625 render: function(template, data) { 1626 if (!template) { 1627 throw new Marionette.Error({ 1628 name: 'TemplateNotFoundError', 1629 message: 'Cannot render the template since its false, null or undefined.' 1630 }); 1631 } 1632 1633 var templateFunc = _.isFunction(template) ? template : Marionette.TemplateCache.get(template); 1634 1635 return templateFunc(data); 1636 } 1637 }; 1638 1639 1640 /* jshint maxlen: 114, nonew: false */ 1641 // View 1642 // ---- 1643 1644 // The core view class that other Marionette views extend from. 1645 Marionette.View = Backbone.View.extend({ 1646 isDestroyed: false, 1647 1648 constructor: function(options) { 1649 _.bindAll(this, 'render'); 1650 1651 options = Marionette._getValue(options, this); 1652 1653 // this exposes view options to the view initializer 1654 // this is a backfill since backbone removed the assignment 1655 // of this.options 1656 // at some point however this may be removed 1657 this.options = _.extend({}, _.result(this, 'options'), options); 1658 1659 this._behaviors = Marionette.Behaviors(this); 1660 1661 Backbone.View.call(this, this.options); 1662 1663 Marionette.MonitorDOMRefresh(this); 1664 }, 1665 1666 // Get the template for this view 1667 // instance. You can set a `template` attribute in the view 1668 // definition or pass a `template: "whatever"` parameter in 1669 // to the constructor options. 1670 getTemplate: function() { 1671 return this.getOption('template'); 1672 }, 1673 1674 // Serialize a model by returning its attributes. Clones 1675 // the attributes to allow modification. 1676 serializeModel: function(model) { 1677 return model.toJSON.apply(model, _.rest(arguments)); 1678 }, 1679 1680 // Mix in template helper methods. Looks for a 1681 // `templateHelpers` attribute, which can either be an 1682 // object literal, or a function that returns an object 1683 // literal. All methods and attributes from this object 1684 // are copies to the object passed in. 1685 mixinTemplateHelpers: function(target) { 1686 target = target || {}; 1687 var templateHelpers = this.getOption('templateHelpers'); 1688 templateHelpers = Marionette._getValue(templateHelpers, this); 1689 return _.extend(target, templateHelpers); 1690 }, 1691 1692 // normalize the keys of passed hash with the views `ui` selectors. 1693 // `{"@ui.foo": "bar"}` 1694 normalizeUIKeys: function(hash) { 1695 var uiBindings = _.result(this, '_uiBindings'); 1696 return Marionette.normalizeUIKeys(hash, uiBindings || _.result(this, 'ui')); 1697 }, 1698 1699 // normalize the values of passed hash with the views `ui` selectors. 1700 // `{foo: "@ui.bar"}` 1701 normalizeUIValues: function(hash, properties) { 1702 var ui = _.result(this, 'ui'); 1703 var uiBindings = _.result(this, '_uiBindings'); 1704 return Marionette.normalizeUIValues(hash, uiBindings || ui, properties); 1705 }, 1706 1707 // Configure `triggers` to forward DOM events to view 1708 // events. `triggers: {"click .foo": "do:foo"}` 1709 configureTriggers: function() { 1710 if (!this.triggers) { return; } 1711 1712 // Allow `triggers` to be configured as a function 1713 var triggers = this.normalizeUIKeys(_.result(this, 'triggers')); 1714 1715 // Configure the triggers, prevent default 1716 // action and stop propagation of DOM events 1717 return _.reduce(triggers, function(events, value, key) { 1718 events[key] = this._buildViewTrigger(value); 1719 return events; 1720 }, {}, this); 1721 }, 1722 1723 // Overriding Backbone.View's delegateEvents to handle 1724 // the `triggers`, `modelEvents`, and `collectionEvents` configuration 1725 delegateEvents: function(events) { 1726 this._delegateDOMEvents(events); 1727 this.bindEntityEvents(this.model, this.getOption('modelEvents')); 1728 this.bindEntityEvents(this.collection, this.getOption('collectionEvents')); 1729 1730 _.each(this._behaviors, function(behavior) { 1731 behavior.bindEntityEvents(this.model, behavior.getOption('modelEvents')); 1732 behavior.bindEntityEvents(this.collection, behavior.getOption('collectionEvents')); 1733 }, this); 1734 1735 return this; 1736 }, 1737 1738 // internal method to delegate DOM events and triggers 1739 _delegateDOMEvents: function(eventsArg) { 1740 var events = Marionette._getValue(eventsArg || this.events, this); 1741 1742 // normalize ui keys 1743 events = this.normalizeUIKeys(events); 1744 if (_.isUndefined(eventsArg)) {this.events = events;} 1745 1746 var combinedEvents = {}; 1747 1748 // look up if this view has behavior events 1749 var behaviorEvents = _.result(this, 'behaviorEvents') || {}; 1750 var triggers = this.configureTriggers(); 1751 var behaviorTriggers = _.result(this, 'behaviorTriggers') || {}; 1752 1753 // behavior events will be overriden by view events and or triggers 1754 _.extend(combinedEvents, behaviorEvents, events, triggers, behaviorTriggers); 1755 1756 Backbone.View.prototype.delegateEvents.call(this, combinedEvents); 1757 }, 1758 1759 // Overriding Backbone.View's undelegateEvents to handle unbinding 1760 // the `triggers`, `modelEvents`, and `collectionEvents` config 1761 undelegateEvents: function() { 1762 Backbone.View.prototype.undelegateEvents.apply(this, arguments); 1763 1764 this.unbindEntityEvents(this.model, this.getOption('modelEvents')); 1765 this.unbindEntityEvents(this.collection, this.getOption('collectionEvents')); 1766 1767 _.each(this._behaviors, function(behavior) { 1768 behavior.unbindEntityEvents(this.model, behavior.getOption('modelEvents')); 1769 behavior.unbindEntityEvents(this.collection, behavior.getOption('collectionEvents')); 1770 }, this); 1771 1772 return this; 1773 }, 1774 1775 // Internal helper method to verify whether the view hasn't been destroyed 1776 _ensureViewIsIntact: function() { 1777 if (this.isDestroyed) { 1778 throw new Marionette.Error({ 1779 name: 'ViewDestroyedError', 1780 message: 'View (cid: "' + this.cid + '") has already been destroyed and cannot be used.' 1781 }); 1782 } 1783 }, 1784 1785 // Default `destroy` implementation, for removing a view from the 1786 // DOM and unbinding it. Regions will call this method 1787 // for you. You can specify an `onDestroy` method in your view to 1788 // add custom code that is called after the view is destroyed. 1789 destroy: function() { 1790 if (this.isDestroyed) { return this; } 1791 1792 var args = _.toArray(arguments); 1793 1794 this.triggerMethod.apply(this, ['before:destroy'].concat(args)); 1795 1796 // mark as destroyed before doing the actual destroy, to 1797 // prevent infinite loops within "destroy" event handlers
1798 // that are trying to destroy other views 1799 this.isDestroyed = true; 1800 this.triggerMethod.apply(this, ['destroy'].concat(args)); 1801 1802 // unbind UI elements 1803 this.unbindUIElements(); 1804 1805 this.isRendered = false; 1806 1807 // remove the view from the DOM 1808 this.remove(); 1809 1810 // Call destroy on each behavior after 1811 // destroying the view. 1812 // This unbinds event listeners 1813 // that behaviors have registered for. 1814 _.invoke(this._behaviors, 'destroy', args); 1815 1816 return this; 1817 }, 1818 1819 bindUIElements: function() { 1820 this._bindUIElements(); 1821 _.invoke(this._behaviors, this._bindUIElements); 1822 }, 1823 1824 // This method binds the elements specified in the "ui" hash inside the view's code with 1825 // the associated jQuery selectors. 1826 _bindUIElements: function() { 1827 if (!this.ui) { return; } 1828 1829 // store the ui hash in _uiBindings so they can be reset later 1830 // and so re-rendering the view will be able to find the bindings 1831 if (!this._uiBindings) { 1832 this._uiBindings = this.ui; 1833 } 1834 1835 // get the bindings result, as a function or otherwise 1836 var bindings = _.result(this, '_uiBindings'); 1837 1838 // empty the ui so we don't have anything to start with 1839 this.ui = {}; 1840 1841 // bind each of the selectors 1842 _.each(bindings, function(selector, key) { 1843 this.ui[key] = this.$(selector); 1844 }, this); 1845 }, 1846 1847 // This method unbinds the elements specified in the "ui" hash 1848 unbindUIElements: function() { 1849 this._unbindUIElements(); 1850 _.invoke(this._behaviors, this._unbindUIElements); 1851 }, 1852 1853 _unbindUIElements: function() { 1854 if (!this.ui || !this._uiBindings) { return; } 1855 1856 // delete all of the existing ui bindings 1857 _.each(this.ui, function($el, name) { 1858 delete this.ui[name]; 1859 }, this); 1860 1861 // reset the ui element to the original bindings configuration 1862 this.ui = this._uiBindings; 1863 delete this._uiBindings; 1864 }, 1865 1866 // Internal method to create an event handler for a given `triggerDef` like 1867 // 'click:foo' 1868 _buildViewTrigger: function(triggerDef) { 1869 var hasOptions = _.isObject(triggerDef); 1870 1871 var options = _.defaults({}, (hasOptions ? triggerDef : {}), { 1872 preventDefault: true, 1873 stopPropagation: true 1874 }); 1875 1876 var eventName = hasOptions ? options.event : triggerDef; 1877 1878 return function(e) { 1879 if (e) { 1880 if (e.preventDefault && options.preventDefault) { 1881 e.preventDefault(); 1882 } 1883 1884 if (e.stopPropagation && options.stopPropagation) { 1885 e.stopPropagation(); 1886 } 1887 } 1888 1889 var args = { 1890 view: this, 1891 model: this.model, 1892 collection: this.collection 1893 }; 1894 1895 this.triggerMethod(eventName, args); 1896 }; 1897 }, 1898 1899 setElement: function() { 1900 var ret = Backbone.View.prototype.setElement.apply(this, arguments); 1901 1902 // proxy behavior $el to the view's $el. 1903 // This is needed because a view's $el proxy 1904 // is not set until after setElement is called. 1905 _.invoke(this._behaviors, 'proxyViewProperties', this); 1906 1907 return ret; 1908 }, 1909 1910 // import the `triggerMethod` to trigger events with corresponding 1911 // methods if the method exists 1912 triggerMethod: function() { 1913 var ret = Marionette._triggerMethod(this, arguments); 1914 1915 this._triggerEventOnBehaviors(arguments); 1916 this._triggerEventOnParentLayout(arguments[0], _.rest(arguments)); 1917 1918 return ret; 1919 }, 1920 1921 _triggerEventOnBehaviors: function(args) { 1922 var triggerMethod = Marionette._triggerMethod; 1923 var behaviors = this._behaviors; 1924 // Use good ol' for as this is a very hot function 1925 for (var i = 0, length = behaviors && behaviors.length; i < length; i++) { 1926 triggerMethod(behaviors[i], args); 1927 } 1928 }, 1929 1930 _triggerEventOnParentLayout: function(eventName, args) { 1931 var layoutView = this._parentLayoutView(); 1932 if (!layoutView) { 1933 return; 1934 } 1935 1936 // invoke triggerMethod on parent view 1937 var eventPrefix = Marionette.getOption(layoutView, 'childViewEventPrefix'); 1938 var prefixedEventName = eventPrefix + ':' + eventName; 1939 1940 Marionette._triggerMethod(layoutView, [prefixedEventName, this].concat(args)); 1941 1942 // call the parent view's childEvents handler 1943 var childEvents = Marionette.getOption(layoutView, 'childEvents'); 1944 var normalizedChildEvents = layoutView.normalizeMethods(childEvents); 1945 1946 if (!!normalizedChildEvents && _.isFunction(normalizedChildEvents[eventName])) { 1947 normalizedChildEvents[eventName].apply(layoutView, [this].concat(args)); 1948 } 1949 }, 1950 1951 // This method returns any views that are immediate 1952 // children of this view 1953 _getImmediateChildren: function() { 1954 return []; 1955 }, 1956 1957 // Returns an array of every nested view within this view 1958 _getNestedViews: function() { 1959 var children = this._getImmediateChildren(); 1960 1961 if (!children.length) { return children; } 1962 1963 return _.reduce(children, function(memo, view) { 1964 if (!view._getNestedViews) { return memo; } 1965 return memo.concat(view._getNestedViews()); 1966 }, children); 1967 }, 1968 1969 // Internal utility for building an ancestor 1970 // view tree list. 1971 _getAncestors: function() { 1972 var ancestors = []; 1973 var parent = this._parent; 1974 1975 while (parent) { 1976 ancestors.push(parent); 1977 parent = parent._parent; 1978 } 1979 1980 return ancestors; 1981 }, 1982 1983 // Returns the containing parent view. 1984 _parentLayoutView: function() { 1985 var ancestors = this._getAncestors(); 1986 return _.find(ancestors, function(parent) { 1987 return parent instanceof Marionette.LayoutView; 1988 }); 1989 }, 1990 1991 // Imports the "normalizeMethods" to transform hashes of 1992 // events=>function references/names to a hash of events=>function references 1993 normalizeMethods: Marionette.normalizeMethods, 1994 1995 // A handy way to merge passed-in options onto the instance 1996 mergeOptions: Marionette.mergeOptions, 1997 1998 // Proxy `getOption` to enable getting options from this or this.options by name. 1999 getOption: Marionette.proxyGetOption, 2000 2001 // Proxy `bindEntityEvents` to enable binding view's events from another entity. 2002 bindEntityEvents: Marionette.proxyBindEntityEvents, 2003 2004 // Proxy `unbindEntityEvents` to enable unbinding view's events from another entity. 2005 unbindEntityEvents: Marionette.proxyUnbindEntityEvents 2006 }); 2007 2008 // Item View 2009 // --------- 2010 2011 // A single item view implementation that contains code for rendering 2012 // with underscore.js templates, serializing the view's model or collection, 2013 // and calling several methods on extended views, such as `onRender`.
2014 Marionette.ItemView = Marionette.View.extend({ 2015 2016 // Setting up the inheritance chain which allows changes to 2017 // Marionette.View.prototype.constructor which allows overriding 2018 constructor: function() { 2019 Marionette.View.apply(this, arguments); 2020 }, 2021 2022 // Serialize the model or collection for the view. If a model is 2023 // found, the view's `serializeModel` is called. If a collection is found, 2024 // each model in the collection is serialized by calling 2025 // the view's `serializeCollection` and put into an `items` array in 2026 // the resulting data. If both are found, defaults to the model. 2027 // You can override the `serializeData` method in your own view definition, 2028 // to provide custom serialization for your view's data. 2029 serializeData: function() { 2030 if (!this.model && !this.collection) { 2031 return {}; 2032 } 2033 2034 var args = [this.model || this.collection]; 2035 if (arguments.length) { 2036 args.push.apply(args, arguments); 2037 } 2038 2039 if (this.model) { 2040 return this.serializeModel.apply(this, args); 2041 } else { 2042 return { 2043 items: this.serializeCollection.apply(this, args) 2044 }; 2045 } 2046 }, 2047 2048 // Serialize a collection by serializing each of its models. 2049 serializeCollection: function(collection) { 2050 return collection.toJSON.apply(collection, _.rest(arguments)); 2051 }, 2052 2053 // Render the view, defaulting to underscore.js templates. 2054 // You can override this in your view definition to provide 2055 // a very specific rendering for your view. In general, though, 2056 // you should override the `Marionette.Renderer` object to 2057 // change how Marionette renders views. 2058 render: function() { 2059 this._ensureViewIsIntact(); 2060 2061 this.triggerMethod('before:render', this); 2062 2063 this._renderTemplate(); 2064 this.isRendered = true; 2065 this.bindUIElements(); 2066 2067 this.triggerMethod('render', this); 2068 2069 return this; 2070 }, 2071 2072 // Internal method to render the template with the serialized data 2073 // and template helpers via the `Marionette.Renderer` object. 2074 // Throws an `UndefinedTemplateError` error if the template is 2075 // any falsely value but literal `false`. 2076 _renderTemplate: function() { 2077 var template = this.getTemplate(); 2078 2079 // Allow template-less item views 2080 if (template === false) { 2081 return; 2082 } 2083 2084 if (!template) { 2085 throw new Marionette.Error({ 2086 name: 'UndefinedTemplateError', 2087 message: 'Cannot render the template since it is null or undefined.' 2088 }); 2089 } 2090 2091 // Add in entity data and template helpers 2092 var data = this.mixinTemplateHelpers(this.serializeData()); 2093 2094 // Render and add to el 2095 var html = Marionette.Renderer.render(template, data, this); 2096 this.attachElContent(html); 2097 2098 return this; 2099 }, 2100 2101 // Attaches the content of a given view. 2102 // This method can be overridden to optimize rendering, 2103 // or to render in a non standard way. 2104 // 2105 // For example, using `innerHTML` instead of `$el.html` 2106 // 2107 // ```js 2108 // attachElContent: function(html) { 2109 // this.el.innerHTML = html; 2110 // return this; 2111 // } 2112 // ``` 2113 attachElContent: function(html) { 2114 this.$el.html(html); 2115 2116 return this; 2117 } 2118 }); 2119 2120 /* jshint maxstatements: 14 */ 2121 2122 // Collection View 2123 // --------------- 2124 2125 // A view that iterates over a Backbone.Collection 2126 // and renders an individual child view for each model. 2127 Marionette.CollectionView = Marionette.View.extend({ 2128 2129 // used as the prefix for child view events 2130 // that are forwarded through the collectionview 2131 childViewEventPrefix: 'childview', 2132 2133 // flag for maintaining the sorted order of the collection 2134 sort: true, 2135 2136 // constructor 2137 // option to pass `{sort: false}` to prevent the `CollectionView` from 2138 // maintaining the sorted order of the collection. 2139 // This will fallback onto appending childView's to the end. 2140 // 2141 // option to pass `{comparator: compFunction()}` to allow the `CollectionView` 2142 // to use a custom sort order for the collection. 2143 constructor: function(options) { 2144 2145 this.once('render', this._initialEvents); 2146 this._initChildViewStorage(); 2147
2148 Marionette.View.apply(this, arguments); 2149 2150 this.on('show', this._onShowCalled); 2151 2152 this.initRenderBuffer(); 2153 }, 2154 2155 // Instead of inserting elements one by one into the page, 2156 // it's much more performant to insert elements into a document 2157 // fragment and then insert that document fragment into the page 2158 initRenderBuffer: function() { 2159 this._bufferedChildren = []; 2160 }, 2161 2162 startBuffering: function() { 2163 this.initRenderBuffer(); 2164 this.isBuffering = true; 2165 }, 2166 2167 endBuffering: function() { 2168 this.isBuffering = false; 2169 this._triggerBeforeShowBufferedChildren(); 2170 2171 this.attachBuffer(this); 2172 2173 this._triggerShowBufferedChildren(); 2174 this.initRenderBuffer(); 2175 }, 2176 2177 _triggerBeforeShowBufferedChildren: function() { 2178 if (this._isShown) { 2179 _.each(this._bufferedChildren, _.partial(this._triggerMethodOnChild, 'before:show')); 2180 } 2181 }, 2182 2183 _triggerShowBufferedChildren: function() { 2184 if (this._isShown) { 2185 _.each(this._bufferedChildren, _.partial(this._triggerMethodOnChild, 'show')); 2186 2187 this._bufferedChildren = []; 2188 } 2189 }, 2190 2191 // Internal method for _.each loops to call `Marionette.triggerMethodOn` on 2192 // a child view 2193 _triggerMethodOnChild: function(event, childView) { 2194 Marionette.triggerMethodOn(childView, event); 2195 }, 2196 2197 // Configured the initial events that the collection view 2198 // binds to. 2199 _initialEvents: function() { 2200 if (this.collection) { 2201 this.listenTo(this.collection, 'add', this._onCollectionAdd); 2202 this.listenTo(this.collection, 'remove', this._onCollectionRemove); 2203 this.listenTo(this.collection, 'reset', this.render); 2204 2205 if (this.getOption('sort')) { 2206 this.listenTo(this.collection, 'sort', this._sortViews); 2207 } 2208 } 2209 }, 2210 2211 // Handle a child added to the collection 2212 _onCollectionAdd: function(child, collection, opts) { 2213 var index; 2214 if (opts.at !== undefined) { 2215 index = opts.at; 2216 } else { 2217 index = _.indexOf(this._filteredSortedModels(), child); 2218 } 2219 2220 if (this._shouldAddChild(child, index)) { 2221 this.destroyEmptyView(); 2222 var ChildView = this.getChildView(child); 2223 this.addChild(child, ChildView, index); 2224 } 2225 }, 2226 2227 // get the child view by model it holds, and remove it 2228 _onCollectionRemove: function(model) { 2229 var view = this.children.findByModel(model); 2230 this.removeChildView(view); 2231 this.checkEmpty(); 2232 }, 2233 2234 _onShowCalled: function() { 2235 this.children.each(_.partial(this._triggerMethodOnChild, 'show')); 2236 }, 2237 2238 // Render children views. Override this method to 2239 // provide your own implementation of a render function for 2240 // the collection view. 2241 render: function() { 2242 this._ensureViewIsIntact(); 2243 this.triggerMethod('before:render', this); 2244 this._renderChildren(); 2245 this.isRendered = true; 2246 this.triggerMethod('render', this); 2247 return this; 2248 }, 2249 2250 // Reorder DOM after sorting. When your element's rendering 2251 // do not use their index, you can pass reorderOnSort: true 2252 // to only reorder the DOM after a sort instead of rendering 2253 // all the collectionView 2254 reorder: function() { 2255 var children = this.children; 2256 var models = this._filteredSortedModels(); 2257 var modelsChanged = _.find(models, function(model) { 2258 return !children.findByModel(model); 2259 }); 2260 2261 // If the models we're displaying have changed due to filtering 2262 // We need to add and/or remove child views 2263 // So render as normal 2264 if (modelsChanged) { 2265 this.render(); 2266 } else { 2267 // get the DOM nodes in the same order as the models 2268 var els = _.map(models, function(model) { 2269 return children.findByModel(model).el; 2270 }); 2271 2272 // since append moves elements that are already in the DOM, 2273 // appending the elements will effectively reorder them 2274 this.triggerMethod('before:reorder'); 2275 this._appendReorderedChildren(els); 2276 this.triggerMethod('reorder'); 2277 } 2278 }, 2279 2280 // Render view after sorting. Override this method to 2281 // change how the view renders after a `sort` on the collection. 2282 // An example of this would be to only `renderChildren` in a `CompositeView` 2283 // rather than the full view. 2284 resortView: function() { 2285 if (Marionette.getOption(this, 'reorderOnSort')) { 2286 this.reorder(); 2287 } else { 2288 this.render(); 2289 } 2290 }, 2291 2292 // Internal method. This checks for any changes in the order of the collection. 2293 // If the index of any view doesn't match, it will render. 2294 _sortViews: function() { 2295 var models = this._filteredSortedModels(); 2296 2297 // check for any changes in sort order of views 2298 var orderChanged = _.find(models, function(item, index) { 2299 var view = this.children.findByModel(item); 2300 return !view || view._index !== index; 2301 }, this); 2302 2303 if (orderChanged) { 2304 this.resortView(); 2305 } 2306 }, 2307 2308 // Internal reference to what index a `emptyView` is. 2309 _emptyViewIndex: -1, 2310
2311 // Internal method. Separated so that CompositeView can append to the childViewContainer 2312 // if necessary 2313 _appendReorderedChildren: function(children) { 2314 this.$el.append(children); 2315 }, 2316 2317 // Internal method. Separated so that CompositeView can have 2318 // more control over events being triggered, around the rendering 2319 // process 2320 _renderChildren: function() { 2321 this.destroyEmptyView(); 2322 this.destroyChildren(); 2323 2324 if (this.isEmpty(this.collection)) { 2325 this.showEmptyView(); 2326 } else { 2327 this.triggerMethod('before:render:collection', this); 2328 this.startBuffering(); 2329 this.showCollection(); 2330 this.endBuffering(); 2331 this.triggerMethod('render:collection', this); 2332 2333 // If we have shown children and none have passed the filter, show the empty view 2334 if (this.children.isEmpty()) { 2335 this.showEmptyView(); 2336 } 2337 } 2338 }, 2339 2340 // Internal method to loop through collection and show each child view. 2341 showCollection: function() { 2342 var ChildView; 2343 2344 var models = this._filteredSortedModels(); 2345 2346 _.each(models, function(child, index) { 2347 ChildView = this.getChildView(child); 2348 this.addChild(child, ChildView, index); 2349 }, this); 2350 }, 2351 2352 // Allow the collection to be sorted by a custom view comparator 2353 _filteredSortedModels: function() { 2354 var models; 2355 var viewComparator = this.getViewComparator(); 2356 2357 if (viewComparator) { 2358 if (_.isString(viewComparator) || viewComparator.length === 1) { 2359 models = this.collection.sortBy(viewComparator, this); 2360 } else { 2361 models = _.clone(this.collection.models).sort(_.bind(viewComparator, this)); 2362 } 2363 } else { 2364 models = this.collection.models; 2365 } 2366 2367 // Filter after sorting in case the filter uses the index 2368 if (this.getOption('filter')) { 2369 models = _.filter(models, function(model, index) { 2370 return this._shouldAddChild(model, index); 2371 }, this); 2372 } 2373 2374 return models; 2375 }, 2376 2377 // Internal method to show an empty view in place of 2378 // a collection of child views, when the collection is empty 2379 showEmptyView: function() { 2380 var EmptyView = this.getEmptyView(); 2381 2382 if (EmptyView && !this._showingEmptyView) { 2383 this.triggerMethod('before:render:empty'); 2384 2385 this._showingEmptyView = true; 2386 var model = new Backbone.Model(); 2387 this.addEmptyView(model, EmptyView); 2388 2389 this.triggerMethod('render:empty'); 2390 } 2391 }, 2392 2393 // Internal method to destroy an existing emptyView instance 2394 // if one exists. Called when a collection view has been 2395 // rendered empty, and then a child is added to the collection. 2396 destroyEmptyView: function() { 2397 if (this._showingEmptyView) { 2398 this.triggerMethod('before:remove:empty'); 2399 2400 this.destroyChildren(); 2401 delete this._showingEmptyView; 2402 2403 this.triggerMethod('remove:empty'); 2404 } 2405 }, 2406 2407 // Retrieve the empty view class 2408 getEmptyView: function() { 2409 return this.getOption('emptyView'); 2410 }, 2411 2412 // Render and show the emptyView. Similar to addChild method 2413 // but "add:child" events are not fired, and the event from 2414 // emptyView are not forwarded 2415 addEmptyView: function(child, EmptyView) { 2416 2417 // get the emptyViewOptions, falling back to childViewOptions 2418 var emptyViewOptions = this.getOption('emptyViewOptions') || 2419 this.getOption('childViewOptions'); 2420 2421 if (_.isFunction(emptyViewOptions)) { 2422 emptyViewOptions = emptyViewOptions.call(this, child, this._emptyViewIndex); 2423 } 2424 2425 // build the empty view 2426 var view = this.buildChildView(child, EmptyView, emptyViewOptions); 2427 2428 view._parent = this; 2429 2430 // Proxy emptyView events 2431 this.proxyChildEvents(view); 2432 2433 // trigger the 'before:show' event on `view` if the collection view 2434 // has already been shown 2435 if (this._isShown) { 2436 Marionette.triggerMethodOn(view, 'before:show'); 2437 } 2438 2439 // Store the `emptyView` like a `childView` so we can properly 2440 // remove and/or close it later 2441 this.children.add(view); 2442 2443 // Render it and show it 2444 this.renderChildView(view, this._emptyViewIndex); 2445 2446 // call the 'show' method if the collection view 2447 // has already been shown 2448 if (this._isShown) { 2449 Marionette.triggerMethodOn(view, 'show'); 2450 } 2451 }, 2452 2453 // Retrieve the `childView` class, either from `this.options.childView` 2454 // or from the `childView` in the object definition. The "options" 2455 // takes precedence. 2456 // This method receives the model that will be passed to the instance 2457 // created from this `childView`. Overriding methods may use the child 2458 // to determine what `childView` class to return. 2459 getChildView: function(child) { 2460 var childView = this.getOption('childView'); 2461 2462 if (!childView) { 2463 throw new Marionette.Error({ 2464 name: 'NoChildViewError', 2465 message: 'A "childView" must be specified' 2466 }); 2467 } 2468 2469 return childView; 2470 }, 2471 2472 // Render the child's view and add it to the 2473 // HTML for the collection view at a given index. 2474 // This will also update the indices of later views in the collection 2475 // in order to keep the children in sync with the collection. 2476 addChild: function(child, ChildView, index) { 2477 var childViewOptions = this.getOption('childViewOptions'); 2478 childViewOptions = Marionette._getValue(childViewOptions, this, [child, index]); 2479 2480 var view = this.buildChildView(child, ChildView, childViewOptions); 2481 2482 // increment indices of views after this one 2483 this._updateIndices(view, true, index); 2484 2485 this._addChildView(view, index); 2486 2487 view._parent = this; 2488 2489 return view; 2490 }, 2491 2492 // Internal method. This decrements or increments the indices of views after the 2493 // added/removed view to keep in sync with the collection. 2494 _updateIndices: function(view, increment, index) { 2495 if (!this.getOption('sort')) { 2496 return; 2497 } 2498 2499 if (increment) { 2500 // assign the index to the view 2501 view._index = index; 2502 } 2503 2504 // update the indexes of views after this one 2505 this.children.each(function(laterView) { 2506 if (laterView._index >= view._index) { 2507 laterView._index += increment ? 1 : -1; 2508 } 2509 }); 2510 }, 2511 2512 // Internal Method. Add the view to children and render it at 2513 // the given index. 2514 _addChildView: function(view, index) { 2515 // set up the child view event forwarding 2516 this.proxyChildEvents(view); 2517 2518 this.triggerMethod('before:add:child', view); 2519 2520 // trigger the 'before:show' event on `view` if the collection view 2521 // has already been shown 2522 if (this._isShown && !this.isBuffering) { 2523 Marionette.triggerMethodOn(view, 'before:show'); 2524 } 2525 2526 // Store the child view itself so we can properly 2527 // remove and/or destroy it later 2528 this.children.add(view); 2529 this.renderChildView(view, index); 2530 2531 if (this._isShown && !this.isBuffering) { 2532 Marionette.triggerMethodOn(view, 'show'); 2533 } 2534 2535 this.triggerMethod('add:child', view); 2536 }, 2537 2538 // render the child view 2539 renderChildView: function(view, index) { 2540 view.render(); 2541 this.attachHtml(this, view, index); 2542 return view; 2543 }, 2544 2545 // Build a `childView` for a model in the collection. 2546 buildChildView: function(child, ChildViewClass, childViewOptions) { 2547 var options = _.extend({model: child}, childViewOptions); 2548 return new ChildViewClass(options); 2549 }, 2550 2551 // Remove the child view and destroy it. 2552 // This function also updates the indices of 2553 // later views in the collection in order to keep 2554 // the children in sync with the collection. 2555 removeChildView: function(view) { 2556 2557 if (view) { 2558 this.triggerMethod('before:remove:child', view); 2559 2560 // call 'destroy' or 'remove', depending on which is found 2561 if (view.destroy) { 2562 view.destroy(); 2563 } else if (view.remove) { 2564 view.remove(); 2565 } 2566 2567 delete view._parent; 2568 this.stopListening(view); 2569 this.children.remove(view); 2570 this.triggerMethod('remove:child', view); 2571 2572 // decrement the index of views after this one 2573 this._updateIndices(view, false); 2574 } 2575 2576 return view; 2577 }, 2578 2579 // check if the collection is empty 2580 isEmpty: function() { 2581 return !this.collection || this.collection.length === 0; 2582 }, 2583 2584 // If empty, show the empty view 2585 checkEmpty: function() { 2586 if (this.isEmpty(this.collection)) { 2587 this.showEmptyView(); 2588 } 2589 }, 2590 2591 // You might need to override this if you've overridden attachHtml 2592 attachBuffer: function(collectionView) { 2593 collectionView.$el.append(this._createBuffer(collectionView)); 2594 }, 2595 2596 // Create a fragment buffer from the currently buffered children 2597 _createBuffer: function(collectionView) { 2598 var elBuffer = document.createDocumentFragment(); 2599 _.each(collectionView._bufferedChildren, function(b) { 2600 elBuffer.appendChild(b.el); 2601 }); 2602 return elBuffer; 2603 }, 2604 2605 // Append the HTML to the collection's `el`. 2606 // Override this method to do something other 2607 // than `.append`. 2608 attachHtml: function(collectionView, childView, index) { 2609 if (collectionView.isBuffering) { 2610 // buffering happens on reset events and initial renders 2611 // in order to reduce the number of inserts into the 2612 // document, which are expensive. 2613 collectionView._bufferedChildren.splice(index, 0, childView); 2614 } else { 2615 // If we've already rendered the main collection, append 2616 // the new child into the correct order if we need to. Otherwise 2617 // append to the end. 2618 if (!collectionView._insertBefore(childView, index)) { 2619 collectionView._insertAfter(childView); 2620 } 2621 } 2622 }, 2623 2624 // Internal method. Check whether we need to insert the view into 2625 // the correct position. 2626 _insertBefore: function(childView, index) { 2627 var currentView; 2628 var findPosition = this.getOption('sort') && (index < this.children.length - 1); 2629 if (findPosition) { 2630 // Find the view after this one 2631 currentView = this.children.find(function(view) { 2632 return view._index === index + 1; 2633 }); 2634 } 2635 2636 if (currentView) { 2637 currentView.$el.before(childView.el); 2638 return true; 2639 } 2640 2641 return false;
2642 }, 2643 2644 // Internal method. Append a view to the end of the $el 2645 _insertAfter: function(childView) { 2646 this.$el.append(childView.el); 2647 }, 2648 2649 // Internal method to set up the `children` object for 2650 // storing all of the child views 2651 _initChildViewStorage: function() { 2652 this.children = new Backbone.ChildViewContainer(); 2653 }, 2654 2655 // Handle cleanup and other destroying needs for the collection of views 2656 destroy: function() { 2657 if (this.isDestroyed) { return this; } 2658 2659 this.triggerMethod('before:destroy:collection'); 2660 this.destroyChildren(); 2661 this.triggerMethod('destroy:collection'); 2662 2663 return Marionette.View.prototype.destroy.apply(this, arguments); 2664 }, 2665 2666 // Destroy the child views that this collection view 2667 // is holding on to, if any 2668 destroyChildren: function() { 2669 var childViews = this.children.map(_.identity); 2670 this.children.each(this.removeChildView, this); 2671 this.checkEmpty(); 2672 return childViews; 2673 }, 2674 2675 // Return true if the given child should be shown 2676 // Return false otherwise 2677 // The filter will be passed (child, index, collection) 2678 // Where 2679 // 'child' is the given model 2680 // 'index' is the index of that model in the collection 2681 // 'collection' is the collection referenced by this CollectionView 2682 _shouldAddChild: function(child, index) { 2683 var filter = this.getOption('filter'); 2684 return !_.isFunction(filter) || filter.call(this, child, index, this.collection); 2685 }, 2686 2687 // Set up the child view event forwarding. Uses a "childview:" 2688 // prefix in front of all forwarded events. 2689 proxyChildEvents: function(view) { 2690 var prefix = this.getOption('childViewEventPrefix'); 2691 2692 // Forward all child view events through the parent, 2693 // prepending "childview:" to the event name 2694 this.listenTo(view, 'all', function() { 2695 var args = _.toArray(arguments); 2696 var rootEvent = args[0]; 2697 var childEvents = this.normalizeMethods(_.result(this, 'childEvents')); 2698 2699 args[0] = prefix + ':' + rootEvent; 2700 args.splice(1, 0, view); 2701 2702 // call collectionView childEvent if defined 2703 if (typeof childEvents !== 'undefined' && _.isFunction(childEvents[rootEvent])) { 2704 childEvents[rootEvent].apply(this, args.slice(1)); 2705 } 2706 2707 this.triggerMethod.apply(this, args); 2708 }); 2709 }, 2710 2711 _getImmediateChildren: function() { 2712 return _.values(this.children._views); 2713 }, 2714 2715 getViewComparator: function() { 2716 return this.getOption('viewComparator'); 2717 } 2718 }); 2719 2720 /* jshint maxstatements: 17, maxlen: 117 */ 2721 2722 // Composite View 2723 // -------------- 2724 2725 // Used for rendering a branch-leaf, hierarchical structure. 2726 // Extends directly from CollectionView and also renders an 2727 // a child view as `modelView`, for the top leaf 2728 Marionette.CompositeView = Marionette.CollectionView.extend({ 2729 2730 // Setting up the inheritance chain which allows changes to 2731 // Marionette.CollectionView.prototype.constructor which allows overriding 2732 // option to pass '{sort: false}' to prevent the CompositeView from 2733 // maintaining the sorted order of the collection. 2734 // This will fallback onto appending childView's to the end. 2735 constructor: function() { 2736 Marionette.CollectionView.apply(this, arguments); 2737 }, 2738 2739 // Configured the initial events that the composite view 2740 // binds to. Override this method to prevent the initial 2741 // events, or to add your own initial events. 2742 _initialEvents: function() { 2743 2744 // Bind only after composite view is rendered to avoid adding child views 2745 // to nonexistent childViewContainer 2746 2747 if (this.collection) { 2748 this.listenTo(this.collection, 'add', this._onCollectionAdd); 2749 this.listenTo(this.collection, 'remove', this._onCollectionRemove); 2750 this.listenTo(this.collection, 'reset', this._renderChildren); 2751 2752 if (this.getOption('sort')) { 2753 this.listenTo(this.collection, 'sort', this._sortViews); 2754 } 2755 } 2756 }, 2757 2758 // Retrieve the `childView` to be used when rendering each of 2759 // the items in the collection. The default is to return 2760 // `this.childView` or Marionette.CompositeView if no `childView` 2761 // has been defined 2762 getChildView: function(child) { 2763 var childView = this.getOption('childView') || this.constructor; 2764 2765 return childView; 2766 }, 2767 2768 // Serialize the model for the view. 2769 // You can override the `serializeData` method in your own view
2770 // definition, to provide custom serialization for your view's data. 2771 serializeData: function() { 2772 var data = {}; 2773 2774 if (this.model) { 2775 data = _.partial(this.serializeModel, this.model).apply(this, arguments); 2776 } 2777 2778 return data; 2779 }, 2780 2781 // Renders the model and the collection. 2782 render: function() { 2783 this._ensureViewIsIntact(); 2784 this._isRendering = true; 2785 this.resetChildViewContainer(); 2786 2787 this.triggerMethod('before:render', this); 2788 2789 this._renderTemplate(); 2790 this._renderChildren(); 2791 2792 this._isRendering = false; 2793 this.isRendered = true; 2794 this.triggerMethod('render', this); 2795 return this; 2796 }, 2797 2798 _renderChildren: function() { 2799 if (this.isRendered || this._isRendering) { 2800 Marionette.CollectionView.prototype._renderChildren.call(this); 2801 } 2802 }, 2803 2804 // Render the root template that the children 2805 // views are appended to 2806 _renderTemplate: function() { 2807 var data = {}; 2808 data = this.serializeData(); 2809 data = this.mixinTemplateHelpers(data); 2810 2811 this.triggerMethod('before:render:template'); 2812 2813 var template = this.getTemplate(); 2814 var html = Marionette.Renderer.render(template, data, this); 2815 this.attachElContent(html); 2816 2817 // the ui bindings is done here and not at the end of render since they 2818 // will not be available until after the model is rendered, but should be 2819 // available before the collection is rendered. 2820 this.bindUIElements(); 2821 this.triggerMethod('render:template'); 2822 }, 2823 2824 // Attaches the content of the root. 2825 // This method can be overridden to optimize rendering, 2826 // or to render in a non standard way. 2827 // 2828 // For example, using `innerHTML` instead of `$el.html` 2829 // 2830 // ```js 2831 // attachElContent: function(html) { 2832 // this.el.innerHTML = html; 2833 // return this; 2834 // } 2835 // ``` 2836 attachElContent: function(html) { 2837 this.$el.html(html); 2838 2839 return this; 2840 }, 2841 2842 // You might need to override this if you've overridden attachHtml 2843 attachBuffer: function(compositeView) { 2844 var $container = this.getChildViewContainer(compositeView); 2845 $container.append(this._createBuffer(compositeView)); 2846 }, 2847 2848 // Internal method. Append a view to the end of the $el. 2849 // Overidden from CollectionView to ensure view is appended to 2850 // childViewContainer 2851 _insertAfter: function(childView) { 2852 var $container = this.getChildViewContainer(this, childView); 2853 $container.append(childView.el); 2854 }, 2855 2856 // Internal method. Append reordered childView'. 2857 // Overidden from CollectionView to ensure reordered views 2858 // are appended to childViewContainer 2859 _appendReorderedChildren: function(children) { 2860 var $container = this.getChildViewContainer(this); 2861 $container.append(children); 2862 }, 2863 2864 // Internal method to ensure an `$childViewContainer` exists, for the 2865 // `attachHtml` method to use. 2866 getChildViewContainer: function(containerView, childView) { 2867 if ('$childViewContainer' in containerView) { 2868 return containerView.$childViewContainer; 2869 } 2870 2871 var container; 2872 var childViewContainer = Marionette.getOption(containerView, 'childViewContainer'); 2873 if (childViewContainer) { 2874 2875 var selector = Marionette._getValue(childViewContainer, containerView); 2876 2877 if (selector.charAt(0) === '@' && containerView.ui) { 2878 container = containerView.ui[selector.substr(4)]; 2879 } else { 2880 container = containerView.$(selector); 2881 } 2882 2883 if (container.length <= 0) { 2884 throw new Marionette.Error({ 2885 name: 'ChildViewContainerMissingError', 2886 message: 'The specified "childViewContainer" was not found: ' + containerView.childViewContainer 2887 }); 2888 } 2889 2890 } else { 2891 container = containerView.$el; 2892 } 2893 2894 containerView.$childViewContainer = container; 2895 return container; 2896 }, 2897 2898 // Internal method to reset the `$childViewContainer` on render 2899 resetChildViewContainer: function() { 2900 if (this.$childViewContainer) { 2901 delete this.$childViewContainer; 2902 } 2903 } 2904 }); 2905 2906 // Layout View 2907 // ----------- 2908 2909 // Used for managing application layoutViews, nested layoutViews and 2910 // multiple regions within an application or sub-application. 2911 // 2912 // A specialized view class that renders an area of HTML and then 2913 // attaches `Region` instances to the specified `regions`.
2914 // Used for composite view management and sub-application areas. 2915 Marionette.LayoutView = Marionette.ItemView.extend({ 2916 regionClass: Marionette.Region, 2917 2918 options: { 2919 destroyImmediate: false 2920 }, 2921 2922 // used as the prefix for child view events 2923 // that are forwarded through the layoutview 2924 childViewEventPrefix: 'childview', 2925 2926 // Ensure the regions are available when the `initialize` method 2927 // is called. 2928 constructor: function(options) { 2929 options = options || {}; 2930 2931 this._firstRender = true; 2932 this._initializeRegions(options); 2933 2934 Marionette.ItemView.call(this, options); 2935 }, 2936 2937 // LayoutView's render will use the existing region objects the 2938 // first time it is called. Subsequent calls will destroy the 2939 // views that the regions are showing and then reset the `el` 2940 // for the regions to the newly rendered DOM elements. 2941 render: function() { 2942 this._ensureViewIsIntact(); 2943 2944 if (this._firstRender) { 2945 // if this is the first render, don't do anything to 2946 // reset the regions 2947 this._firstRender = false;
2948 } else { 2949 // If this is not the first render call, then we need to 2950 // re-initialize the `el` for each region 2951 this._reInitializeRegions(); 2952 } 2953 2954 return Marionette.ItemView.prototype.render.apply(this, arguments); 2955 }, 2956 2957 // Handle destroying regions, and then destroy the view itself. 2958 destroy: function() { 2959 if (this.isDestroyed) { return this; } 2960 // #2134: remove parent element before destroying the child views, so 2961 // removing the child views doesn't retrigger repaints 2962 if (this.getOption('destroyImmediate') === true) { 2963 this.$el.remove(); 2964 } 2965 this.regionManager.destroy(); 2966 return Marionette.ItemView.prototype.destroy.apply(this, arguments); 2967 }, 2968 2969 showChildView: function(regionName, view) { 2970 return this.getRegion(regionName).show(view); 2971 }, 2972 2973 getChildView: function(regionName) { 2974 return this.getRegion(regionName).currentView; 2975 }, 2976 2977 // Add a single region, by name, to the layoutView 2978 addRegion: function(name, definition) { 2979 var regions = {}; 2980 regions[name] = definition; 2981 return this._buildRegions(regions)[name]; 2982 }, 2983 2984 // Add multiple regions as a {name: definition, name2: def2} object literal 2985 addRegions: function(regions) { 2986 this.regions = _.extend({}, this.regions, regions); 2987 return this._buildRegions(regions); 2988 }, 2989 2990 // Remove a single region from the LayoutView, by name 2991 removeRegion: function(name) { 2992 delete this.regions[name]; 2993 return this.regionManager.removeRegion(name); 2994 }, 2995 2996 // Provides alternative access to regions 2997 // Accepts the region name 2998 // getRegion('main') 2999 getRegion: function(region) { 3000 return this.regionManager.get(region); 3001 }, 3002 3003 // Get all regions 3004 getRegions: function() { 3005 return this.regionManager.getRegions(); 3006 }, 3007 3008 // internal method to build regions 3009 _buildRegions: function(regions) { 3010 var defaults = { 3011 regionClass: this.getOption('regionClass'), 3012 parentEl: _.partial(_.result, this, 'el') 3013 }; 3014 3015 return this.regionManager.addRegions(regions, defaults); 3016 }, 3017 3018 // Internal method to initialize the regions that have been defined in a 3019 // `regions` attribute on this layoutView. 3020 _initializeRegions: function(options) { 3021 var regions; 3022 this._initRegionManager(); 3023 3024 regions = Marionette._getValue(this.regions, this, [options]) || {}; 3025 3026 // Enable users to define `regions` as instance options. 3027 var regionOptions = this.getOption.call(options, 'regions'); 3028 3029 // enable region options to be a function 3030 regionOptions = Marionette._getValue(regionOptions, this, [options]); 3031 3032 _.extend(regions, regionOptions); 3033 3034 // Normalize region selectors hash to allow 3035 // a user to use the @ui. syntax. 3036 regions = this.normalizeUIValues(regions, ['selector', 'el']); 3037 3038 this.addRegions(regions); 3039 }, 3040 3041 // Internal method to re-initialize all of the regions by updating the `el` that 3042 // they point to 3043 _reInitializeRegions: function() { 3044 this.regionManager.invoke('reset'); 3045 }, 3046 3047 // Enable easy overriding of the default `RegionManager` 3048 // for customized region interactions and business specific 3049 // view logic for better control over single regions. 3050 getRegionManager: function() { 3051 return new Marionette.RegionManager(); 3052 }, 3053 3054 // Internal method to initialize the region manager 3055 // and all regions in it 3056 _initRegionManager: function() { 3057 this.regionManager = this.getRegionManager(); 3058 this.regionManager._parent = this; 3059 3060 this.listenTo(this.regionManager, 'before:add:region', function(name) { 3061 this.triggerMethod('before:add:region', name); 3062 }); 3063 3064 this.listenTo(this.regionManager, 'add:region', function(name, region) { 3065 this[name] = region; 3066 this.triggerMethod('add:region', name, region); 3067 }); 3068 3069 this.listenTo(this.regionManager, 'before:remove:region', function(name) { 3070 this.triggerMethod('before:remove:region', name); 3071 }); 3072 3073 this.listenTo(this.regionManager, 'remove:region', function(name, region) { 3074 delete this[name]; 3075 this.triggerMethod('remove:region', name, region); 3076 }); 3077 }, 3078 3079 _getImmediateChildren: function() { 3080 return _.chain(this.regionManager.getRegions()) 3081 .pluck('currentView') 3082 .compact() 3083 .value(); 3084 } 3085 }); 3086 3087 3088 // Behavior 3089 // -------- 3090 3091 // A Behavior is an isolated set of DOM / 3092 // user interactions that can be mixed into any View. 3093 // Behaviors allow you to blackbox View specific interactions 3094 // into portable logical chunks, keeping your views simple and your code DRY. 3095 3096 Marionette.Behavior = Marionette.Object.extend({ 3097 constructor: function(options, view) { 3098 // Setup reference to the view. 3099 // this comes in handle when a behavior 3100 // wants to directly talk up the chain 3101 // to the view. 3102 this.view = view; 3103 this.defaults = _.result(this, 'defaults') || {}; 3104 this.options = _.extend({}, this.defaults, options); 3105 // Construct an internal UI hash using 3106 // the views UI hash and then the behaviors UI hash. 3107 // This allows the user to use UI hash elements 3108 // defined in the parent view as well as those 3109 // defined in the given behavior. 3110 this.ui = _.extend({}, _.result(view, 'ui'), _.result(this, 'ui')); 3111 3112 Marionette.Object.apply(this, arguments); 3113 }, 3114 3115 // proxy behavior $ method to the view 3116 // this is useful for doing jquery DOM lookups 3117 // scoped to behaviors view. 3118 $: function() { 3119 return this.view.$.apply(this.view, arguments); 3120 }, 3121 3122 // Stops the behavior from listening to events. 3123 // Overrides Object#destroy to prevent additional events from being triggered. 3124 destroy: function() { 3125 this.stopListening(); 3126 3127 return this; 3128 }, 3129 3130 proxyViewProperties: function(view) { 3131 this.$el = view.$el; 3132 this.el = view.el; 3133 } 3134 }); 3135 3136 /* jshint maxlen: 143 */ 3137 // Behaviors 3138 // --------- 3139 3140 // Behaviors is a utility class that takes care of 3141 // gluing your behavior instances to their given View. 3142 // The most important part of this class is that you 3143 // **MUST** override the class level behaviorsLookup 3144 // method for things to work properly. 3145 3146 Marionette.Behaviors = (function(Marionette, _) { 3147 // Borrow event splitter from Backbone 3148 var delegateEventSplitter = /^(\S+)\s*(.*)$/; 3149 3150 function Behaviors(view, behaviors) { 3151 3152 if (!_.isObject(view.behaviors)) { 3153 return {}; 3154 } 3155 3156 // Behaviors defined on a view can be a flat object literal 3157 // or it can be a function that returns an object. 3158 behaviors = Behaviors.parseBehaviors(view, behaviors || _.result(view, 'behaviors')); 3159 3160 // Wraps several of the view's methods 3161 // calling the methods first on each behavior 3162 // and then eventually calling the method on the view. 3163 Behaviors.wrap(view, behaviors, _.keys(methods)); 3164 return behaviors; 3165 } 3166 3167 var methods = { 3168 behaviorTriggers: function(behaviorTriggers, behaviors) { 3169 var triggerBuilder = new BehaviorTriggersBuilder(this, behaviors); 3170 return triggerBuilder.buildBehaviorTriggers(); 3171 }, 3172 3173 behaviorEvents: function(behaviorEvents, behaviors) { 3174 var _behaviorsEvents = {}; 3175 3176 _.each(behaviors, function(b, i) { 3177 var _events = {}; 3178 var behaviorEvents = _.clone(_.result(b, 'events')) || {}; 3179 3180 // Normalize behavior events hash to allow 3181 // a user to use the @ui. syntax. 3182 behaviorEvents = Marionette.normalizeUIKeys(behaviorEvents, getBehaviorsUI(b)); 3183 3184 var j = 0; 3185 _.each(behaviorEvents, function(behaviour, key) {
3186 var match = key.match(delegateEventSplitter); 3187 3188 // Set event name to be namespaced using the view cid, 3189 // the behavior index, and the behavior event index 3190 // to generate a non colliding event namespace 3191 // http://api.jquery.com/event.namespace/ 3192 var eventName = match[1] + '.' + [this.cid, i, j++, ' '].join(''); 3193 var selector = match[2]; 3194 3195 var eventKey = eventName + selector; 3196 var handler = _.isFunction(behaviour) ? behaviour : b[behaviour]; 3197 3198 _events[eventKey] = _.bind(handler, b); 3199 }, this); 3200 3201 _behaviorsEvents = _.extend(_behaviorsEvents, _events); 3202 }, this); 3203 3204 return _behaviorsEvents; 3205 } 3206 }; 3207 3208 _.extend(Behaviors, { 3209 3210 // Placeholder method to be extended by the user. 3211 // The method should define the object that stores the behaviors. 3212 // i.e. 3213 // 3214 // ```js 3215 // Marionette.Behaviors.behaviorsLookup: function() { 3216 // return App.Behaviors 3217 // } 3218 // ``` 3219 behaviorsLookup: function() { 3220 throw new Marionette.Error({ 3221 message: 'You must define where your behaviors are stored.', 3222 url: 'marionette.behaviors.html#behaviorslookup' 3223 }); 3224 }, 3225 3226 // Takes care of getting the behavior class 3227 // given options and a key. 3228 // If a user passes in options.behaviorClass 3229 // default to using that. Otherwise delegate 3230 // the lookup to the users `behaviorsLookup` implementation. 3231 getBehaviorClass: function(options, key) { 3232 if (options.behaviorClass) { 3233 return options.behaviorClass; 3234 } 3235 3236 // Get behavior class can be either a flat object or a method 3237 return Marionette._getValue(Behaviors.behaviorsLookup, this, [options, key])[key]; 3238 }, 3239 3240 // Iterate over the behaviors object, for each behavior 3241 // instantiate it and get its grouped behaviors. 3242 parseBehaviors: function(view, behaviors) { 3243 return _.chain(behaviors).map(function(options, key) { 3244 var BehaviorClass = Behaviors.getBehaviorClass(options, key); 3245 3246 var behavior = new BehaviorClass(options, view); 3247 var nestedBehaviors = Behaviors.parseBehaviors(view, _.result(behavior, 'behaviors')); 3248 3249 return [behavior].concat(nestedBehaviors); 3250 }).flatten().value(); 3251 }, 3252 3253 // Wrap view internal methods so that they delegate to behaviors. For example, 3254 // `onDestroy` should trigger destroy on all of the behaviors and then destroy itself. 3255 // i.e. 3256 // 3257 // `view.delegateEvents = _.partial(methods.delegateEvents, view.delegateEvents, behaviors);` 3258 wrap: function(view, behaviors, methodNames) { 3259 _.each(methodNames, function(methodName) { 3260 view[methodName] = _.partial(methods[methodName], view[methodName], behaviors); 3261 }); 3262 } 3263 }); 3264 3265 // Class to build handlers for `triggers` on behaviors 3266 // for views 3267 function BehaviorTriggersBuilder(view, behaviors) { 3268 this._view = view; 3269 this._behaviors = behaviors; 3270 this._triggers = {}; 3271 } 3272 3273 _.extend(BehaviorTriggersBuilder.prototype, { 3274 // Main method to build the triggers hash with event keys and handlers 3275 buildBehaviorTriggers: function() { 3276 _.each(this._behaviors, this._buildTriggerHandlersForBehavior, this); 3277 return this._triggers; 3278 }, 3279 3280 // Internal method to build all trigger handlers for a given behavior 3281 _buildTriggerHandlersForBehavior: function(behavior, i) { 3282 var triggersHash = _.clone(_.result(behavior, 'triggers')) || {}; 3283 3284 triggersHash = Marionette.normalizeUIKeys(triggersHash, getBehaviorsUI(behavior)); 3285 3286 _.each(triggersHash, _.bind(this._setHandlerForBehavior, this, behavior, i)); 3287 }, 3288 3289 // Internal method to create and assign the trigger handler for a given 3290 // behavior 3291 _setHandlerForBehavior: function(behavior, i, eventName, trigger) { 3292 // Unique identifier for the `this._triggers` hash 3293 var triggerKey = trigger.replace(/^\S+/, function(triggerName) { 3294 return triggerName + '.' + 'behaviortriggers' + i; 3295 }); 3296 3297 this._triggers[triggerKey] = this._view._buildViewTrigger(eventName); 3298 } 3299 }); 3300 3301 function getBehaviorsUI(behavior) { 3302 return behavior._uiBindings || behavior.ui; 3303 } 3304 3305 return Behaviors; 3306 3307 })(Marionette, _); 3308 3309 3310 // App Router 3311 // ---------- 3312 3313 // Reduce the boilerplate code of handling route events 3314 // and then calling a single method on another object. 3315 // Have your routers configured to call the method on 3316 // your object, directly. 3317 // 3318 // Configure an AppRouter with `appRoutes`. 3319 // 3320 // App routers can only take one `controller` object. 3321 // It is recommended that you divide your controller 3322 // objects in to smaller pieces of related functionality 3323 // and have multiple routers / controllers, instead of 3324 // just one giant router and controller. 3325 // 3326 // You can also add standard routes to an AppRouter. 3327 3328 Marionette.AppRouter = Backbone.Router.extend({ 3329 3330 constructor: function(options) { 3331 this.options = options || {}; 3332 3333 Backbone.Router.apply(this, arguments); 3334 3335 var appRoutes = this.getOption('appRoutes'); 3336 var controller = this._getController(); 3337 this.processAppRoutes(controller, appRoutes); 3338 this.on('route', this._processOnRoute, this); 3339 }, 3340 3341 // Similar to route method on a Backbone Router but 3342 // method is called on the controller 3343 appRoute: function(route, methodName) { 3344 var controller = this._getController(); 3345 this._addAppRoute(controller, route, methodName); 3346 }, 3347 3348 // process the route event and trigger the onRoute 3349 // method call, if it exists 3350 _processOnRoute: function(routeName, routeArgs) { 3351 // make sure an onRoute before trying to call it 3352 if (_.isFunction(this.onRoute)) { 3353 // find the path that matches the current route 3354 var routePath = _.invert(this.getOption('appRoutes'))[routeName]; 3355 this.onRoute(routeName, routePath, routeArgs); 3356 } 3357 }, 3358 3359 // Internal method to process the `appRoutes` for the 3360 // router, and turn them in to routes that trigger the 3361 // specified method on the specified `controller`. 3362 processAppRoutes: function(controller, appRoutes) { 3363 if (!appRoutes) { return; } 3364 3365 var routeNames = _.keys(appRoutes).reverse(); // Backbone requires reverted order of routes 3366 3367 _.each(routeNames, function(route) { 3368 this._addAppRoute(controller, route, appRoutes[route]); 3369 }, this); 3370 }, 3371 3372 _getController: function() { 3373 return this.getOption('controller'); 3374 }, 3375 3376 _addAppRoute: function(controller, route, methodName) { 3377 var method = controller[methodName]; 3378
3379 if (!method) { 3380 throw new Marionette.Error('Method "' + methodName + '" was not found on the controller'); 3381 } 3382 3383 this.route(route, methodName, _.bind(method, controller)); 3384 }, 3385 3386 mergeOptions: Marionette.mergeOptions, 3387 3388 // Proxy `getOption` to enable getting options from this or this.options by name. 3389 getOption: Marionette.proxyGetOption, 3390 3391 triggerMethod: Marionette.triggerMethod, 3392 3393 bindEntityEvents: Marionette.proxyBindEntityEvents, 3394 3395 unbindEntityEvents: Marionette.proxyUnbindEntityEvents 3396 }); 3397 3398 // Application 3399 // ----------- 3400 3401 // Contain and manage the composite application as a whole. 3402 // Stores and starts up `Region` objects, includes an 3403 // event aggregator as `app.vent` 3404 Marionette.Application = Marionette.Object.extend({ 3405 constructor: function(options) { 3406 this._initializeRegions(options); 3407 this._initCallbacks = new Marionette.Callbacks(); 3408 this.submodules = {}; 3409 _.extend(this, options); 3410 this._initChannel(); 3411 Marionette.Object.call(this, options); 3412 }, 3413 3414 // Command execution, facilitated by Backbone.Wreqr.Commands 3415 execute: function() { 3416 this.commands.execute.apply(this.commands, arguments); 3417 }, 3418 3419 // Request/response, facilitated by Backbone.Wreqr.RequestResponse 3420 request: function() { 3421 return this.reqres.request.apply(this.reqres, arguments); 3422 }, 3423 3424 // Add an initializer that is either run at when the `start` 3425 // method is called, or run immediately if added after `start` 3426 // has already been called. 3427 addInitializer: function(initializer) { 3428 this._initCallbacks.add(initializer); 3429 }, 3430 3431 // kick off all of the application's processes. 3432 // initializes all of the regions that have been added 3433 // to the app, and runs all of the initializer functions 3434 start: function(options) { 3435 this.triggerMethod('before:start', options); 3436 this._initCallbacks.run(options, this); 3437 this.triggerMethod('start', options); 3438 }, 3439 3440 // Add regions to your app. 3441 // Accepts a hash of named strings or Region objects 3442 // addRegions({something: "#someRegion"}) 3443 // addRegions({something: Region.extend({el: "#someRegion"}) }); 3444 addRegions: function(regions) { 3445 return this._regionManager.addRegions(regions); 3446 }, 3447 3448 // Empty all regions in the app, without removing them 3449 emptyRegions: function() { 3450 return this._regionManager.emptyRegions(); 3451 }, 3452 3453 // Removes a region from your app, by name 3454 // Accepts the regions name 3455 // removeRegion('myRegion') 3456 removeRegion: function(region) { 3457 return this._regionManager.removeRegion(region); 3458 }, 3459 3460 // Provides alternative access to regions 3461 // Accepts the region name 3462 // getRegion('main') 3463 getRegion: function(region) { 3464 return this._regionManager.get(region); 3465 }, 3466 3467 // Get all the regions from the region manager 3468 getRegions: function() { 3469 return this._regionManager.getRegions(); 3470 }, 3471 3472 // Create a module, attached to the application 3473 module: function(moduleNames, moduleDefinition) { 3474 3475 // Overwrite the module class if the user specifies one 3476 var ModuleClass = Marionette.Module.getClass(moduleDefinition); 3477 3478 var args = _.toArray(arguments); 3479 args.unshift(this); 3480 3481 // see the Marionette.Module object for more information 3482 return ModuleClass.create.apply(ModuleClass, args); 3483 }, 3484 3485 // Enable easy overriding of the default `RegionManager` 3486 // for customized region interactions and business-specific 3487 // view logic for better control over single regions. 3488 getRegionManager: function() { 3489 return new Marionette.RegionManager(); 3490 }, 3491 3492 // Internal method to initialize the regions that have been defined in a 3493 // `regions` attribute on the application instance 3494 _initializeRegions: function(options) { 3495 var regions = _.isFunction(this.regions) ? this.regions(options) : this.regions || {}; 3496 3497 this._initRegionManager(); 3498 3499 // Enable users to define `regions` in instance options. 3500 var optionRegions = Marionette.getOption(options, 'regions'); 3501 3502 // Enable region options to be a function 3503 if (_.isFunction(optionRegions)) { 3504 optionRegions = optionRegions.call(this, options); 3505 } 3506 3507 // Overwrite current regions with those passed in options 3508 _.extend(regions, optionRegions); 3509 3510 this.addRegions(regions); 3511 3512 return this; 3513 }, 3514 3515 // Internal method to set up the region manager 3516 _initRegionManager: function() { 3517 this._regionManager = this.getRegionManager(); 3518 this._regionManager._parent = this; 3519 3520 this.listenTo(this._regionManager, 'before:add:region', function() { 3521 Marionette._triggerMethod(this, 'before:add:region', arguments); 3522 }); 3523 3524 this.listenTo(this._regionManager, 'add:region', function(name, region) { 3525 this[name] = region; 3526 Marionette._triggerMethod(this, 'add:region', arguments); 3527 }); 3528 3529 this.listenTo(this._regionManager, 'before:remove:region', function() { 3530 Marionette._triggerMethod(this, 'before:remove:region', arguments); 3531 }); 3532 3533 this.listenTo(this._regionManager, 'remove:region', function(name) { 3534 delete this[name]; 3535 Marionette._triggerMethod(this, 'remove:region', arguments); 3536 }); 3537 }, 3538 3539 // Internal method to setup the Wreqr.radio channel 3540 _initChannel: function() { 3541 this.channelName = _.result(this, 'channelName') || 'global'; 3542 this.channel = _.result(this, 'channel') || Backbone.Wreqr.radio.channel(this.channelName); 3543 this.vent = _.result(this, 'vent') || this.channel.vent; 3544 this.commands = _.result(this, 'commands') || this.channel.commands; 3545 this.reqres = _.result(this, 'reqres') || this.channel.reqres; 3546 } 3547 }); 3548 3549 /* jshint maxparams: 9 */ 3550 3551 // Module 3552 // ------ 3553 3554 // A simple module system, used to create privacy and encapsulation in 3555 // Marionette applications 3556 Marionette.Module = function(moduleName, app, options) { 3557 this.moduleName = moduleName; 3558 this.options = _.extend({}, this.options, options); 3559 // Allow for a user to overide the initialize 3560 // for a given module instance. 3561 this.initialize = options.initialize || this.initialize; 3562 3563 // Set up an internal store for sub-modules. 3564 this.submodules = {}; 3565
3566 this._setupInitializersAndFinalizers(); 3567 3568 // Set an internal reference to the app 3569 // within a module. 3570 this.app = app; 3571 3572 if (_.isFunction(this.initialize)) { 3573 this.initialize(moduleName, app, this.options); 3574 } 3575 }; 3576 3577 Marionette.Module.extend = Marionette.extend; 3578 3579 // Extend the Module prototype with events / listenTo, so that the module 3580 // can be used as an event aggregator or pub/sub. 3581 _.extend(Marionette.Module.prototype, Backbone.Events, { 3582 3583 // By default modules start with their parents. 3584 startWithParent: true, 3585 3586 // Initialize is an empty function by default. Override it with your own 3587 // initialization logic when extending Marionette.Module. 3588 initialize: function() {}, 3589 3590 // Initializer for a specific module. Initializers are run when the 3591 // module's `start` method is called. 3592 addInitializer: function(callback) { 3593 this._initializerCallbacks.add(callback); 3594 }, 3595 3596 // Finalizers are run when a module is stopped. They are used to teardown 3597 // and finalize any variables, references, events and other code that the 3598 // module had set up. 3599 addFinalizer: function(callback) { 3600 this._finalizerCallbacks.add(callback); 3601 }, 3602 3603 // Start the module, and run all of its initializers 3604 start: function(options) { 3605 // Prevent re-starting a module that is already started 3606 if (this._isInitialized) { return; } 3607 3608 // start the sub-modules (depth-first hierarchy) 3609 _.each(this.submodules, function(mod) { 3610 // check to see if we should start the sub-module with this parent 3611 if (mod.startWithParent) { 3612 mod.start(options); 3613 } 3614 }); 3615 3616 // run the callbacks to "start" the current module 3617 this.triggerMethod('before:start', options); 3618 3619 this._initializerCallbacks.run(options, this); 3620 this._isInitialized = true; 3621 3622 this.triggerMethod('start', options); 3623 }, 3624 3625 // Stop this module by running its finalizers and then stop all of 3626 // the sub-modules for this module 3627 stop: function() { 3628 // if we are not initialized, don't bother finalizing 3629 if (!this._isInitialized) { return; } 3630 this._isInitialized = false; 3631 3632 this.triggerMethod('before:stop'); 3633 3634 // stop the sub-modules; depth-first, to make sure the 3635 // sub-modules are stopped / finalized before parents 3636 _.invoke(this.submodules, 'stop'); 3637 3638 // run the finalizers 3639 this._finalizerCallbacks.run(undefined, this); 3640 3641 // reset the initializers and finalizers 3642 this._initializerCallbacks.reset(); 3643 this._finalizerCallbacks.reset(); 3644 3645 this.triggerMethod('stop'); 3646 }, 3647 3648 // Configure the module with a definition function and any custom args 3649 // that are to be passed in to the definition function 3650 addDefinition: function(moduleDefinition, customArgs) { 3651 this._runModuleDefinition(moduleDefinition, customArgs); 3652 }, 3653 3654 // Internal method: run the module definition function with the correct 3655 // arguments 3656 _runModuleDefinition: function(definition, customArgs) { 3657 // If there is no definition short circut the method. 3658 if (!definition) { return; } 3659 3660 // build the correct list of arguments for the module definition 3661 var args = _.flatten([ 3662 this, 3663 this.app, 3664 Backbone, 3665 Marionette, 3666 Backbone.$, _, 3667 customArgs 3668 ]); 3669 3670 definition.apply(this, args); 3671 }, 3672 3673 // Internal method: set up new copies of initializers and finalizers. 3674 // Calling this method will wipe out all existing initializers and 3675 // finalizers. 3676 _setupInitializersAndFinalizers: function() { 3677 this._initializerCallbacks = new Marionette.Callbacks(); 3678 this._finalizerCallbacks = new Marionette.Callbacks(); 3679 }, 3680 3681 // import the `triggerMethod` to trigger events with corresponding 3682 // methods if the method exists 3683 triggerMethod: Marionette.triggerMethod 3684 }); 3685 3686 // Class methods to create modules 3687 _.extend(Marionette.Module, { 3688 3689 // Create a module, hanging off the app parameter as the parent object. 3690 create: function(app, moduleNames, moduleDefinition) { 3691 var module = app; 3692 3693 // get the custom args passed in after the module definition and 3694 // get rid of the module name and definition function 3695 var customArgs = _.drop(arguments, 3); 3696 3697 // Split the module names and get the number of submodules. 3698 // i.e. an example module name of `Doge.Wow.Amaze` would 3699 // then have the potential for 3 module definitions. 3700 moduleNames = moduleNames.split('.'); 3701 var length = moduleNames.length; 3702 3703 // store the module definition for the last module in the chain 3704 var moduleDefinitions = []; 3705 moduleDefinitions[length - 1] = moduleDefinition; 3706 3707 // Loop through all the parts of the module definition 3708 _.each(moduleNames, function(moduleName, i) { 3709 var parentModule = module; 3710 module = this._getModule(parentModule, moduleName, app, moduleDefinition); 3711 this._addModuleDefinition(parentModule, module, moduleDefinitions[i], customArgs); 3712 }, this); 3713 3714 // Return the last module in the definition chain 3715 return module; 3716 }, 3717 3718 _getModule: function(parentModule, moduleName, app, def, args) { 3719 var options = _.extend({}, def); 3720 var ModuleClass = this.getClass(def); 3721 3722 // Get an existing module of this name if we have one 3723 var module = parentModule[moduleName]; 3724 3725 if (!module) { 3726 // Create a new module if we don't have one 3727 module = new ModuleClass(moduleName, app, options); 3728 parentModule[moduleName] = module; 3729 // store the module on the parent 3730 parentModule.submodules[moduleName] = module; 3731 } 3732 3733 return module; 3734 }, 3735 3736 // ## Module Classes 3737 // 3738 // Module classes can be used as an alternative to the define pattern. 3739 // The extend function of a Module is identical to the extend functions 3740 // on other Backbone and Marionette classes. 3741 // This allows module lifecyle events like `onStart` and `onStop` to be called directly. 3742 getClass: function(moduleDefinition) { 3743 var ModuleClass = Marionette.Module; 3744 3745 if (!moduleDefinition) { 3746 return ModuleClass; 3747 } 3748 3749 // If all of the module's functionality is defined inside its class, 3750 // then the class can be passed in directly. `MyApp.module("Foo", FooModule)`. 3751 if (moduleDefinition.prototype instanceof ModuleClass) { 3752 return moduleDefinition; 3753 } 3754 3755 return moduleDefinition.moduleClass || ModuleClass; 3756 }, 3757 3758 // Add the module definition and add a startWithParent initializer function. 3759 // This is complicated because module definitions are heavily overloaded 3760 // and support an anonymous function, module class, or options object 3761 _addModuleDefinition: function(parentModule, module, def, args) { 3762 var fn = this._getDefine(def); 3763 var startWithParent = this._getStartWithParent(def, module); 3764 3765 if (fn) { 3766 module.addDefinition(fn, args); 3767 } 3768 3769 this._addStartWithParent(parentModule, module, startWithParent); 3770 }, 3771 3772 _getStartWithParent: function(def, module) { 3773 var swp; 3774 3775 if (_.isFunction(def) && (def.prototype instanceof Marionette.Module)) { 3776 swp = module.constructor.prototype.startWithParent; 3777 return _.isUndefined(swp) ? true : swp; 3778 } 3779 3780 if (_.isObject(def)) { 3781 swp = def.startWithParent; 3782 return _.isUndefined(swp) ? true : swp; 3783 } 3784 3785 return true; 3786 }, 3787 3788 _getDefine: function(def) { 3789 if (_.isFunction(def) && !(def.prototype instanceof Marionette.Module)) { 3790 return def; 3791 } 3792 3793 if (_.isObject(def)) { 3794 return def.define; 3795 } 3796 3797 return null; 3798 }, 3799 3800 _addStartWithParent: function(parentModule, module, startWithParent) { 3801 module.startWithParent = module.startWithParent && startWithParent; 3802 3803 if (!module.startWithParent || !!module.startWithParentIsConfigured) { 3804 return; 3805 } 3806 3807 module.startWithParentIsConfigured = true; 3808 3809 parentModule.addInitializer(function(options) { 3810 if (module.startWithParent) { 3811 module.start(options); 3812 } 3813 }); 3814 } 3815 }); 3816 3817 3818 return Marionette; 3819}));
Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.