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