PageRenderTime 62ms CodeModel.GetById 19ms RepoModel.GetById 1ms app.codeStats 0ms

/ajax/libs/backbone.marionette/1.0.1-bundled/backbone.marionette.js

https://bitbucket.org/kolbyjAFK/cdnjs
JavaScript | 2331 lines | 1230 code | 473 blank | 628 comment | 154 complexity | d3f2824022e492f6b503aeed13a012f2 MD5 | raw file
Possible License(s): MPL-2.0-no-copyleft-exception
  1. // MarionetteJS (Backbone.Marionette)
  2. // ----------------------------------
  3. // v1.0.1
  4. //
  5. // Copyright (c)2013 Derick Bailey, Muted Solutions, LLC.
  6. // Distributed under MIT license
  7. //
  8. // http://marionettejs.com
  9. /*!
  10. * Includes BabySitter
  11. * https://github.com/marionettejs/backbone.babysitter/
  12. *
  13. * Includes Wreqr
  14. * https://github.com/marionettejs/backbone.wreqr/
  15. */
  16. // Backbone.BabySitter, v0.0.4
  17. // Copyright (c)2012 Derick Bailey, Muted Solutions, LLC.
  18. // Distributed under MIT license
  19. // http://github.com/marionettejs/backbone.babysitter
  20. // Backbone.ChildViewContainer
  21. // ---------------------------
  22. //
  23. // Provide a container to store, retrieve and
  24. // shut down child views.
  25. Backbone.ChildViewContainer = (function(Backbone, _){
  26. // Container Constructor
  27. // ---------------------
  28. var Container = function(initialViews){
  29. this._views = {};
  30. this._indexByModel = {};
  31. this._indexByCollection = {};
  32. this._indexByCustom = {};
  33. this._updateLength();
  34. this._addInitialViews(initialViews);
  35. };
  36. // Container Methods
  37. // -----------------
  38. _.extend(Container.prototype, {
  39. // Add a view to this container. Stores the view
  40. // by `cid` and makes it searchable by the model
  41. // and/or collection of the view. Optionally specify
  42. // a custom key to store an retrieve the view.
  43. add: function(view, customIndex){
  44. var viewCid = view.cid;
  45. // store the view
  46. this._views[viewCid] = view;
  47. // index it by model
  48. if (view.model){
  49. this._indexByModel[view.model.cid] = viewCid;
  50. }
  51. // index it by collection
  52. if (view.collection){
  53. this._indexByCollection[view.collection.cid] = viewCid;
  54. }
  55. // index by custom
  56. if (customIndex){
  57. this._indexByCustom[customIndex] = viewCid;
  58. }
  59. this._updateLength();
  60. },
  61. // Find a view by the model that was attached to
  62. // it. Uses the model's `cid` to find it, and
  63. // retrieves the view by it's `cid` from the result
  64. findByModel: function(model){
  65. var viewCid = this._indexByModel[model.cid];
  66. return this.findByCid(viewCid);
  67. },
  68. // Find a view by the collection that was attached to
  69. // it. Uses the collection's `cid` to find it, and
  70. // retrieves the view by it's `cid` from the result
  71. findByCollection: function(col){
  72. var viewCid = this._indexByCollection[col.cid];
  73. return this.findByCid(viewCid);
  74. },
  75. // Find a view by a custom indexer.
  76. findByCustom: function(index){
  77. var viewCid = this._indexByCustom[index];
  78. return this.findByCid(viewCid);
  79. },
  80. // Find by index. This is not guaranteed to be a
  81. // stable index.
  82. findByIndex: function(index){
  83. return _.values(this._views)[index];
  84. },
  85. // retrieve a view by it's `cid` directly
  86. findByCid: function(cid){
  87. return this._views[cid];
  88. },
  89. // Remove a view
  90. remove: function(view){
  91. var viewCid = view.cid;
  92. // delete model index
  93. if (view.model){
  94. delete this._indexByModel[view.model.cid];
  95. }
  96. // delete collection index
  97. if (view.collection){
  98. delete this._indexByCollection[view.collection.cid];
  99. }
  100. // delete custom index
  101. var cust;
  102. for (var key in this._indexByCustom){
  103. if (this._indexByCustom.hasOwnProperty(key)){
  104. if (this._indexByCustom[key] === viewCid){
  105. cust = key;
  106. break;
  107. }
  108. }
  109. }
  110. if (cust){
  111. delete this._indexByCustom[cust];
  112. }
  113. // remove the view from the container
  114. delete this._views[viewCid];
  115. // update the length
  116. this._updateLength();
  117. },
  118. // Call a method on every view in the container,
  119. // passing parameters to the call method one at a
  120. // time, like `function.call`.
  121. call: function(method, args){
  122. args = Array.prototype.slice.call(arguments, 1);
  123. this.apply(method, args);
  124. },
  125. // Apply a method on every view in the container,
  126. // passing parameters to the call method one at a
  127. // time, like `function.apply`.
  128. apply: function(method, args){
  129. var view;
  130. // fix for IE < 9
  131. args = args || [];
  132. _.each(this._views, function(view, key){
  133. if (_.isFunction(view[method])){
  134. view[method].apply(view, args);
  135. }
  136. });
  137. },
  138. // Update the `.length` attribute on this container
  139. _updateLength: function(){
  140. this.length = _.size(this._views);
  141. },
  142. // set up an initial list of views
  143. _addInitialViews: function(views){
  144. if (!views){ return; }
  145. var view, i,
  146. length = views.length;
  147. for (i=0; i<length; i++){
  148. view = views[i];
  149. this.add(view);
  150. }
  151. }
  152. });
  153. // Borrowing this code from Backbone.Collection:
  154. // http://backbonejs.org/docs/backbone.html#section-106
  155. //
  156. // Mix in methods from Underscore, for iteration, and other
  157. // collection related features.
  158. var methods = ['forEach', 'each', 'map', 'find', 'detect', 'filter',
  159. 'select', 'reject', 'every', 'all', 'some', 'any', 'include',
  160. 'contains', 'invoke', 'toArray', 'first', 'initial', 'rest',
  161. 'last', 'without', 'isEmpty', 'pluck'];
  162. _.each(methods, function(method) {
  163. Container.prototype[method] = function() {
  164. var views = _.values(this._views);
  165. var args = [views].concat(_.toArray(arguments));
  166. return _[method].apply(_, args);
  167. };
  168. });
  169. // return the public API
  170. return Container;
  171. })(Backbone, _);
  172. // Backbone.Wreqr (Backbone.Marionette)
  173. // ----------------------------------
  174. // v0.2.0
  175. //
  176. // Copyright (c)2013 Derick Bailey, Muted Solutions, LLC.
  177. // Distributed under MIT license
  178. //
  179. // http://github.com/marionettejs/backbone.wreqr
  180. Backbone.Wreqr = (function(Backbone, Marionette, _){
  181. "use strict";
  182. var Wreqr = {};
  183. // Handlers
  184. // --------
  185. // A registry of functions to call, given a name
  186. Wreqr.Handlers = (function(Backbone, _){
  187. "use strict";
  188. // Constructor
  189. // -----------
  190. var Handlers = function(options){
  191. this.options = options;
  192. this._wreqrHandlers = {};
  193. if (_.isFunction(this.initialize)){
  194. this.initialize(options);
  195. }
  196. };
  197. Handlers.extend = Backbone.Model.extend;
  198. // Instance Members
  199. // ----------------
  200. _.extend(Handlers.prototype, Backbone.Events, {
  201. // Add multiple handlers using an object literal configuration
  202. setHandlers: function(handlers){
  203. _.each(handlers, function(handler, name){
  204. var context = null;
  205. if (_.isObject(handler) && !_.isFunction(handler)){
  206. context = handler.context;
  207. handler = handler.callback;
  208. }
  209. this.setHandler(name, handler, context);
  210. }, this);
  211. },
  212. // Add a handler for the given name, with an
  213. // optional context to run the handler within
  214. setHandler: function(name, handler, context){
  215. var config = {
  216. callback: handler,
  217. context: context
  218. };
  219. this._wreqrHandlers[name] = config;
  220. this.trigger("handler:add", name, handler, context);
  221. },
  222. // Determine whether or not a handler is registered
  223. hasHandler: function(name){
  224. return !! this._wreqrHandlers[name];
  225. },
  226. // Get the currently registered handler for
  227. // the specified name. Throws an exception if
  228. // no handler is found.
  229. getHandler: function(name){
  230. var config = this._wreqrHandlers[name];
  231. if (!config){
  232. throw new Error("Handler not found for '" + name + "'");
  233. }
  234. return function(){
  235. var args = Array.prototype.slice.apply(arguments);
  236. return config.callback.apply(config.context, args);
  237. };
  238. },
  239. // Remove a handler for the specified name
  240. removeHandler: function(name){
  241. delete this._wreqrHandlers[name];
  242. },
  243. // Remove all handlers from this registry
  244. removeAllHandlers: function(){
  245. this._wreqrHandlers = {};
  246. }
  247. });
  248. return Handlers;
  249. })(Backbone, _);
  250. // Wreqr.CommandStorage
  251. // --------------------
  252. //
  253. // Store and retrieve commands for execution.
  254. Wreqr.CommandStorage = (function(){
  255. "use strict";
  256. // Constructor function
  257. var CommandStorage = function(options){
  258. this.options = options;
  259. this._commands = {};
  260. if (_.isFunction(this.initialize)){
  261. this.initialize(options);
  262. }
  263. };
  264. // Instance methods
  265. _.extend(CommandStorage.prototype, Backbone.Events, {
  266. // Get an object literal by command name, that contains
  267. // the `commandName` and the `instances` of all commands
  268. // represented as an array of arguments to process
  269. getCommands: function(commandName){
  270. var commands = this._commands[commandName];
  271. // we don't have it, so add it
  272. if (!commands){
  273. // build the configuration
  274. commands = {
  275. command: commandName,
  276. instances: []
  277. };
  278. // store it
  279. this._commands[commandName] = commands;
  280. }
  281. return commands;
  282. },
  283. // Add a command by name, to the storage and store the
  284. // args for the command
  285. addCommand: function(commandName, args){
  286. var command = this.getCommands(commandName);
  287. command.instances.push(args);
  288. },
  289. // Clear all commands for the given `commandName`
  290. clearCommands: function(commandName){
  291. var command = this.getCommands(commandName);
  292. command.instances = [];
  293. }
  294. });
  295. return CommandStorage;
  296. })();
  297. // Wreqr.Commands
  298. // --------------
  299. //
  300. // A simple command pattern implementation. Register a command
  301. // handler and execute it.
  302. Wreqr.Commands = (function(Wreqr){
  303. "use strict";
  304. return Wreqr.Handlers.extend({
  305. // default storage type
  306. storageType: Wreqr.CommandStorage,
  307. constructor: function(options){
  308. this.options = options || {};
  309. this._initializeStorage(this.options);
  310. this.on("handler:add", this._executeCommands, this);
  311. var args = Array.prototype.slice.call(arguments);
  312. Wreqr.Handlers.prototype.constructor.apply(this, args);
  313. },
  314. // Execute a named command with the supplied args
  315. execute: function(name, args){
  316. name = arguments[0];
  317. args = Array.prototype.slice.call(arguments, 1);
  318. if (this.hasHandler(name)){
  319. this.getHandler(name).apply(this, args);
  320. } else {
  321. this.storage.addCommand(name, args);
  322. }
  323. },
  324. // Internal method to handle bulk execution of stored commands
  325. _executeCommands: function(name, handler, context){
  326. var command = this.storage.getCommands(name);
  327. // loop through and execute all the stored command instances
  328. _.each(command.instances, function(args){
  329. handler.apply(context, args);
  330. });
  331. this.storage.clearCommands(name);
  332. },
  333. // Internal method to initialize storage either from the type's
  334. // `storageType` or the instance `options.storageType`.
  335. _initializeStorage: function(options){
  336. var storage;
  337. var StorageType = options.storageType || this.storageType;
  338. if (_.isFunction(StorageType)){
  339. storage = new StorageType();
  340. } else {
  341. storage = StorageType;
  342. }
  343. this.storage = storage;
  344. }
  345. });
  346. })(Wreqr);
  347. // Wreqr.RequestResponse
  348. // ---------------------
  349. //
  350. // A simple request/response implementation. Register a
  351. // request handler, and return a response from it
  352. Wreqr.RequestResponse = (function(Wreqr){
  353. "use strict";
  354. return Wreqr.Handlers.extend({
  355. request: function(){
  356. var name = arguments[0];
  357. var args = Array.prototype.slice.call(arguments, 1);
  358. return this.getHandler(name).apply(this, args);
  359. }
  360. });
  361. })(Wreqr);
  362. // Event Aggregator
  363. // ----------------
  364. // A pub-sub object that can be used to decouple various parts
  365. // of an application through event-driven architecture.
  366. Wreqr.EventAggregator = (function(Backbone, _){
  367. "use strict";
  368. var EA = function(){};
  369. // Copy the `extend` function used by Backbone's classes
  370. EA.extend = Backbone.Model.extend;
  371. // Copy the basic Backbone.Events on to the event aggregator
  372. _.extend(EA.prototype, Backbone.Events);
  373. return EA;
  374. })(Backbone, _);
  375. return Wreqr;
  376. })(Backbone, Backbone.Marionette, _);
  377. var Marionette = (function(global, Backbone, _){
  378. "use strict";
  379. // Define and export the Marionette namespace
  380. var Marionette = {};
  381. Backbone.Marionette = Marionette;
  382. // Get the DOM manipulator for later use
  383. Marionette.$ = Backbone.$;
  384. // Helpers
  385. // -------
  386. // For slicing `arguments` in functions
  387. var protoSlice = Array.prototype.slice;
  388. function slice(args) {
  389. return protoSlice.call(args);
  390. }
  391. function throwError(message, name) {
  392. var error = new Error(message);
  393. error.name = name || 'Error';
  394. throw error;
  395. }
  396. // Marionette.extend
  397. // -----------------
  398. // Borrow the Backbone `extend` method so we can use it as needed
  399. Marionette.extend = Backbone.Model.extend;
  400. // Marionette.getOption
  401. // --------------------
  402. // Retrieve an object, function or other value from a target
  403. // object or it's `options`, with `options` taking precedence.
  404. Marionette.getOption = function(target, optionName){
  405. if (!target || !optionName){ return; }
  406. var value;
  407. if (target.options && (optionName in target.options) && (target.options[optionName] !== undefined)){
  408. value = target.options[optionName];
  409. } else {
  410. value = target[optionName];
  411. }
  412. return value;
  413. };
  414. // Trigger an event and a corresponding method name. Examples:
  415. //
  416. // `this.triggerMethod("foo")` will trigger the "foo" event and
  417. // call the "onFoo" method.
  418. //
  419. // `this.triggerMethod("foo:bar") will trigger the "foo:bar" event and
  420. // call the "onFooBar" method.
  421. Marionette.triggerMethod = (function(){
  422. // split the event name on the :
  423. var splitter = /(^|:)(\w)/gi;
  424. // take the event section ("section1:section2:section3")
  425. // and turn it in to uppercase name
  426. function getEventName(match, prefix, eventName) {
  427. return eventName.toUpperCase();
  428. }
  429. // actual triggerMethod name
  430. var triggerMethod = function(event) {
  431. // get the method name from the event name
  432. var methodName = 'on' + event.replace(splitter, getEventName);
  433. var method = this[methodName];
  434. // trigger the event
  435. this.trigger.apply(this, arguments);
  436. // call the onMethodName if it exists
  437. if (_.isFunction(method)) {
  438. // pass all arguments, except the event name
  439. return method.apply(this, _.tail(arguments));
  440. }
  441. };
  442. return triggerMethod;
  443. })();
  444. // DOMRefresh
  445. // ----------
  446. //
  447. // Monitor a view's state, and after it has been rendered and shown
  448. // in the DOM, trigger a "dom:refresh" event every time it is
  449. // re-rendered.
  450. Marionette.MonitorDOMRefresh = (function(){
  451. // track when the view has been rendered
  452. function handleShow(view){
  453. view._isShown = true;
  454. triggerDOMRefresh(view);
  455. }
  456. // track when the view has been shown in the DOM,
  457. // using a Marionette.Region (or by other means of triggering "show")
  458. function handleRender(view){
  459. view._isRendered = true;
  460. triggerDOMRefresh(view);
  461. }
  462. // Trigger the "dom:refresh" event and corresponding "onDomRefresh" method
  463. function triggerDOMRefresh(view){
  464. if (view._isShown && view._isRendered){
  465. if (_.isFunction(view.triggerMethod)){
  466. view.triggerMethod("dom:refresh");
  467. }
  468. }
  469. }
  470. // Export public API
  471. return function(view){
  472. view.listenTo(view, "show", function(){
  473. handleShow(view);
  474. });
  475. view.listenTo(view, "render", function(){
  476. handleRender(view);
  477. });
  478. };
  479. })();
  480. // Marionette.bindEntityEvents & unbindEntityEvents
  481. // ---------------------------
  482. //
  483. // These methods are used to bind/unbind a backbone "entity" (collection/model)
  484. // to methods on a target object.
  485. //
  486. // The first parameter, `target`, must have a `listenTo` method from the
  487. // EventBinder object.
  488. //
  489. // The second parameter is the entity (Backbone.Model or Backbone.Collection)
  490. // to bind the events from.
  491. //
  492. // The third parameter is a hash of { "event:name": "eventHandler" }
  493. // configuration. Multiple handlers can be separated by a space. A
  494. // function can be supplied instead of a string handler name.
  495. (function(Marionette){
  496. "use strict";
  497. // Bind the event to handlers specified as a string of
  498. // handler names on the target object
  499. function bindFromStrings(target, entity, evt, methods){
  500. var methodNames = methods.split(/\s+/);
  501. _.each(methodNames,function(methodName) {
  502. var method = target[methodName];
  503. if(!method) {
  504. throwError("Method '"+ methodName +"' was configured as an event handler, but does not exist.");
  505. }
  506. target.listenTo(entity, evt, method, target);
  507. });
  508. }
  509. // Bind the event to a supplied callback function
  510. function bindToFunction(target, entity, evt, method){
  511. target.listenTo(entity, evt, method, target);
  512. }
  513. // Bind the event to handlers specified as a string of
  514. // handler names on the target object
  515. function unbindFromStrings(target, entity, evt, methods){
  516. var methodNames = methods.split(/\s+/);
  517. _.each(methodNames,function(methodName) {
  518. var method = target[method];
  519. target.stopListening(entity, evt, method, target);
  520. });
  521. }
  522. // Bind the event to a supplied callback function
  523. function unbindToFunction(target, entity, evt, method){
  524. target.stopListening(entity, evt, method, target);
  525. }
  526. // generic looping function
  527. function iterateEvents(target, entity, bindings, functionCallback, stringCallback){
  528. if (!entity || !bindings) { return; }
  529. // allow the bindings to be a function
  530. if (_.isFunction(bindings)){
  531. bindings = bindings.call(target);
  532. }
  533. // iterate the bindings and bind them
  534. _.each(bindings, function(methods, evt){
  535. // allow for a function as the handler,
  536. // or a list of event names as a string
  537. if (_.isFunction(methods)){
  538. functionCallback(target, entity, evt, methods);
  539. } else {
  540. stringCallback(target, entity, evt, methods);
  541. }
  542. });
  543. }
  544. // Export Public API
  545. Marionette.bindEntityEvents = function(target, entity, bindings){
  546. iterateEvents(target, entity, bindings, bindToFunction, bindFromStrings);
  547. };
  548. Marionette.unbindEntityEvents = function(target, entity, bindings){
  549. iterateEvents(target, entity, bindings, unbindToFunction, unbindFromStrings);
  550. };
  551. })(Marionette);
  552. // Callbacks
  553. // ---------
  554. // A simple way of managing a collection of callbacks
  555. // and executing them at a later point in time, using jQuery's
  556. // `Deferred` object.
  557. Marionette.Callbacks = function(){
  558. this._deferred = Marionette.$.Deferred();
  559. this._callbacks = [];
  560. };
  561. _.extend(Marionette.Callbacks.prototype, {
  562. // Add a callback to be executed. Callbacks added here are
  563. // guaranteed to execute, even if they are added after the
  564. // `run` method is called.
  565. add: function(callback, contextOverride){
  566. this._callbacks.push({cb: callback, ctx: contextOverride});
  567. this._deferred.done(function(context, options){
  568. if (contextOverride){ context = contextOverride; }
  569. callback.call(context, options);
  570. });
  571. },
  572. // Run all registered callbacks with the context specified.
  573. // Additional callbacks can be added after this has been run
  574. // and they will still be executed.
  575. run: function(options, context){
  576. this._deferred.resolve(context, options);
  577. },
  578. // Resets the list of callbacks to be run, allowing the same list
  579. // to be run multiple times - whenever the `run` method is called.
  580. reset: function(){
  581. var callbacks = this._callbacks;
  582. this._deferred = Marionette.$.Deferred();
  583. this._callbacks = [];
  584. _.each(callbacks, function(cb){
  585. this.add(cb.cb, cb.ctx);
  586. }, this);
  587. }
  588. });
  589. // Marionette Controller
  590. // ---------------------
  591. //
  592. // A multi-purpose object to use as a controller for
  593. // modules and routers, and as a mediator for workflow
  594. // and coordination of other objects, views, and more.
  595. Marionette.Controller = function(options){
  596. this.triggerMethod = Marionette.triggerMethod;
  597. this.options = options || {};
  598. if (_.isFunction(this.initialize)){
  599. this.initialize(this.options);
  600. }
  601. };
  602. Marionette.Controller.extend = Marionette.extend;
  603. // Controller Methods
  604. // --------------
  605. // Ensure it can trigger events with Backbone.Events
  606. _.extend(Marionette.Controller.prototype, Backbone.Events, {
  607. close: function(){
  608. this.stopListening();
  609. this.triggerMethod("close");
  610. this.unbind();
  611. }
  612. });
  613. // Region
  614. // ------
  615. //
  616. // Manage the visual regions of your composite application. See
  617. // http://lostechies.com/derickbailey/2011/12/12/composite-js-apps-regions-and-region-managers/
  618. Marionette.Region = function(options){
  619. this.options = options || {};
  620. this.el = Marionette.getOption(this, "el");
  621. if (!this.el){
  622. var err = new Error("An 'el' must be specified for a region.");
  623. err.name = "NoElError";
  624. throw err;
  625. }
  626. if (this.initialize){
  627. var args = Array.prototype.slice.apply(arguments);
  628. this.initialize.apply(this, args);
  629. }
  630. };
  631. // Region Type methods
  632. // -------------------
  633. _.extend(Marionette.Region, {
  634. // Build an instance of a region by passing in a configuration object
  635. // and a default region type to use if none is specified in the config.
  636. //
  637. // The config object should either be a string as a jQuery DOM selector,
  638. // a Region type directly, or an object literal that specifies both
  639. // a selector and regionType:
  640. //
  641. // ```js
  642. // {
  643. // selector: "#foo",
  644. // regionType: MyCustomRegion
  645. // }
  646. // ```
  647. //
  648. buildRegion: function(regionConfig, defaultRegionType){
  649. var regionIsString = (typeof regionConfig === "string");
  650. var regionSelectorIsString = (typeof regionConfig.selector === "string");
  651. var regionTypeIsUndefined = (typeof regionConfig.regionType === "undefined");
  652. var regionIsType = (typeof regionConfig === "function");
  653. if (!regionIsType && !regionIsString && !regionSelectorIsString) {
  654. throw new Error("Region must be specified as a Region type, a selector string or an object with selector property");
  655. }
  656. var selector, RegionType;
  657. // get the selector for the region
  658. if (regionIsString) {
  659. selector = regionConfig;
  660. }
  661. if (regionConfig.selector) {
  662. selector = regionConfig.selector;
  663. }
  664. // get the type for the region
  665. if (regionIsType){
  666. RegionType = regionConfig;
  667. }
  668. if (!regionIsType && regionTypeIsUndefined) {
  669. RegionType = defaultRegionType;
  670. }
  671. if (regionConfig.regionType) {
  672. RegionType = regionConfig.regionType;
  673. }
  674. // build the region instance
  675. var region = new RegionType({
  676. el: selector
  677. });
  678. // override the `getEl` function if we have a parentEl
  679. // this must be overridden to ensure the selector is found
  680. // on the first use of the region. if we try to assign the
  681. // region's `el` to `parentEl.find(selector)` in the object
  682. // literal to build the region, the element will not be
  683. // guaranteed to be in the DOM already, and will cause problems
  684. if (regionConfig.parentEl){
  685. region.getEl = function(selector) {
  686. var parentEl = regionConfig.parentEl;
  687. if (_.isFunction(parentEl)){
  688. parentEl = parentEl();
  689. }
  690. return parentEl.find(selector);
  691. };
  692. }
  693. return region;
  694. }
  695. });
  696. // Region Instance Methods
  697. // -----------------------
  698. _.extend(Marionette.Region.prototype, Backbone.Events, {
  699. // Displays a backbone view instance inside of the region.
  700. // Handles calling the `render` method for you. Reads content
  701. // directly from the `el` attribute. Also calls an optional
  702. // `onShow` and `close` method on your view, just after showing
  703. // or just before closing the view, respectively.
  704. show: function(view){
  705. this.ensureEl();
  706. if (view !== this.currentView) {
  707. this.close();
  708. view.render();
  709. this.open(view);
  710. } else {
  711. view.render();
  712. }
  713. Marionette.triggerMethod.call(view, "show");
  714. Marionette.triggerMethod.call(this, "show", view);
  715. this.currentView = view;
  716. },
  717. ensureEl: function(){
  718. if (!this.$el || this.$el.length === 0){
  719. this.$el = this.getEl(this.el);
  720. }
  721. },
  722. // Override this method to change how the region finds the
  723. // DOM element that it manages. Return a jQuery selector object.
  724. getEl: function(selector){
  725. return Marionette.$(selector);
  726. },
  727. // Override this method to change how the new view is
  728. // appended to the `$el` that the region is managing
  729. open: function(view){
  730. this.$el.empty().append(view.el);
  731. },
  732. // Close the current view, if there is one. If there is no
  733. // current view, it does nothing and returns immediately.
  734. close: function(){
  735. var view = this.currentView;
  736. if (!view || view.isClosed){ return; }
  737. // call 'close' or 'remove', depending on which is found
  738. if (view.close) { view.close(); }
  739. else if (view.remove) { view.remove(); }
  740. Marionette.triggerMethod.call(this, "close");
  741. delete this.currentView;
  742. },
  743. // Attach an existing view to the region. This
  744. // will not call `render` or `onShow` for the new view,
  745. // and will not replace the current HTML for the `el`
  746. // of the region.
  747. attachView: function(view){
  748. this.currentView = view;
  749. },
  750. // Reset the region by closing any existing view and
  751. // clearing out the cached `$el`. The next time a view
  752. // is shown via this region, the region will re-query the
  753. // DOM for the region's `el`.
  754. reset: function(){
  755. this.close();
  756. delete this.$el;
  757. }
  758. });
  759. // Copy the `extend` function used by Backbone's classes
  760. Marionette.Region.extend = Marionette.extend;
  761. // Marionette.RegionManager
  762. // ------------------------
  763. //
  764. // Manage one or more related `Marionette.Region` objects.
  765. Marionette.RegionManager = (function(Marionette){
  766. var RegionManager = Marionette.Controller.extend({
  767. constructor: function(options){
  768. this._regions = {};
  769. Marionette.Controller.prototype.constructor.call(this, options);
  770. },
  771. // Add multiple regions using an object literal, where
  772. // each key becomes the region name, and each value is
  773. // the region definition.
  774. addRegions: function(regionDefinitions, defaults){
  775. var regions = {};
  776. _.each(regionDefinitions, function(definition, name){
  777. if (typeof definition === "string"){
  778. definition = { selector: definition };
  779. }
  780. if (definition.selector){
  781. definition = _.defaults({}, definition, defaults);
  782. }
  783. var region = this.addRegion(name, definition);
  784. regions[name] = region;
  785. }, this);
  786. return regions;
  787. },
  788. // Add an individual region to the region manager,
  789. // and return the region instance
  790. addRegion: function(name, definition){
  791. var region;
  792. var isObject = _.isObject(definition);
  793. var isString = _.isString(definition);
  794. var hasSelector = !!definition.selector;
  795. if (isString || (isObject && hasSelector)){
  796. region = Marionette.Region.buildRegion(definition, Marionette.Region);
  797. } else if (_.isFunction(definition)){
  798. region = Marionette.Region.buildRegion(definition, Marionette.Region);
  799. } else {
  800. region = definition;
  801. }
  802. this._store(name, region);
  803. this.triggerMethod("region:add", name, region);
  804. return region;
  805. },
  806. // Get a region by name
  807. get: function(name){
  808. return this._regions[name];
  809. },
  810. // Remove a region by name
  811. removeRegion: function(name){
  812. var region = this._regions[name];
  813. this._remove(name, region);
  814. },
  815. // Close all regions in the region manager, and
  816. // remove them
  817. removeRegions: function(){
  818. _.each(this._regions, function(region, name){
  819. this._remove(name, region);
  820. }, this);
  821. },
  822. // Close all regions in the region manager, but
  823. // leave them attached
  824. closeRegions: function(){
  825. _.each(this._regions, function(region, name){
  826. region.close();
  827. }, this);
  828. },
  829. // Close all regions and shut down the region
  830. // manager entirely
  831. close: function(){
  832. this.removeRegions();
  833. var args = Array.prototype.slice.call(arguments);
  834. Marionette.Controller.prototype.close.apply(this, args);
  835. },
  836. // internal method to store regions
  837. _store: function(name, region){
  838. this._regions[name] = region;
  839. this.length = _.size(this._regions);
  840. },
  841. // internal method to remove a region
  842. _remove: function(name, region){
  843. region.close();
  844. delete this._regions[name];
  845. this.triggerMethod("region:remove", name, region);
  846. }
  847. });
  848. // Borrowing this code from Backbone.Collection:
  849. // http://backbonejs.org/docs/backbone.html#section-106
  850. //
  851. // Mix in methods from Underscore, for iteration, and other
  852. // collection related features.
  853. var methods = ['forEach', 'each', 'map', 'find', 'detect', 'filter',
  854. 'select', 'reject', 'every', 'all', 'some', 'any', 'include',
  855. 'contains', 'invoke', 'toArray', 'first', 'initial', 'rest',
  856. 'last', 'without', 'isEmpty', 'pluck'];
  857. _.each(methods, function(method) {
  858. RegionManager.prototype[method] = function() {
  859. var regions = _.values(this._regions);
  860. var args = [regions].concat(_.toArray(arguments));
  861. return _[method].apply(_, args);
  862. };
  863. });
  864. return RegionManager;
  865. })(Marionette);
  866. // Template Cache
  867. // --------------
  868. // Manage templates stored in `<script>` blocks,
  869. // caching them for faster access.
  870. Marionette.TemplateCache = function(templateId){
  871. this.templateId = templateId;
  872. };
  873. // TemplateCache object-level methods. Manage the template
  874. // caches from these method calls instead of creating
  875. // your own TemplateCache instances
  876. _.extend(Marionette.TemplateCache, {
  877. templateCaches: {},
  878. // Get the specified template by id. Either
  879. // retrieves the cached version, or loads it
  880. // from the DOM.
  881. get: function(templateId){
  882. var cachedTemplate = this.templateCaches[templateId];
  883. if (!cachedTemplate){
  884. cachedTemplate = new Marionette.TemplateCache(templateId);
  885. this.templateCaches[templateId] = cachedTemplate;
  886. }
  887. return cachedTemplate.load();
  888. },
  889. // Clear templates from the cache. If no arguments
  890. // are specified, clears all templates:
  891. // `clear()`
  892. //
  893. // If arguments are specified, clears each of the
  894. // specified templates from the cache:
  895. // `clear("#t1", "#t2", "...")`
  896. clear: function(){
  897. var i;
  898. var args = slice(arguments);
  899. var length = args.length;
  900. if (length > 0){
  901. for(i=0; i<length; i++){
  902. delete this.templateCaches[args[i]];
  903. }
  904. } else {
  905. this.templateCaches = {};
  906. }
  907. }
  908. });
  909. // TemplateCache instance methods, allowing each
  910. // template cache object to manage it's own state
  911. // and know whether or not it has been loaded
  912. _.extend(Marionette.TemplateCache.prototype, {
  913. // Internal method to load the template
  914. load: function(){
  915. // Guard clause to prevent loading this template more than once
  916. if (this.compiledTemplate){
  917. return this.compiledTemplate;
  918. }
  919. // Load the template and compile it
  920. var template = this.loadTemplate(this.templateId);
  921. this.compiledTemplate = this.compileTemplate(template);
  922. return this.compiledTemplate;
  923. },
  924. // Load a template from the DOM, by default. Override
  925. // this method to provide your own template retrieval
  926. // For asynchronous loading with AMD/RequireJS, consider
  927. // using a template-loader plugin as described here:
  928. // https://github.com/marionettejs/backbone.marionette/wiki/Using-marionette-with-requirejs
  929. loadTemplate: function(templateId){
  930. var template = Marionette.$(templateId).html();
  931. if (!template || template.length === 0){
  932. throwError("Could not find template: '" + templateId + "'", "NoTemplateError");
  933. }
  934. return template;
  935. },
  936. // Pre-compile the template before caching it. Override
  937. // this method if you do not need to pre-compile a template
  938. // (JST / RequireJS for example) or if you want to change
  939. // the template engine used (Handebars, etc).
  940. compileTemplate: function(rawTemplate){
  941. return _.template(rawTemplate);
  942. }
  943. });
  944. // Renderer
  945. // --------
  946. // Render a template with data by passing in the template
  947. // selector and the data to render.
  948. Marionette.Renderer = {
  949. // Render a template with data. The `template` parameter is
  950. // passed to the `TemplateCache` object to retrieve the
  951. // template function. Override this method to provide your own
  952. // custom rendering and template handling for all of Marionette.
  953. render: function(template, data){
  954. var templateFunc = typeof template === 'function' ? template : Marionette.TemplateCache.get(template);
  955. return templateFunc(data);
  956. }
  957. };
  958. // Marionette.View
  959. // ---------------
  960. // The core view type that other Marionette views extend from.
  961. Marionette.View = Backbone.View.extend({
  962. constructor: function(){
  963. _.bindAll(this, "render");
  964. var args = Array.prototype.slice.apply(arguments);
  965. Backbone.View.prototype.constructor.apply(this, args);
  966. Marionette.MonitorDOMRefresh(this);
  967. this.listenTo(this, "show", this.onShowCalled, this);
  968. },
  969. // import the "triggerMethod" to trigger events with corresponding
  970. // methods if the method exists
  971. triggerMethod: Marionette.triggerMethod,
  972. // Get the template for this view
  973. // instance. You can set a `template` attribute in the view
  974. // definition or pass a `template: "whatever"` parameter in
  975. // to the constructor options.
  976. getTemplate: function(){
  977. return Marionette.getOption(this, "template");
  978. },
  979. // Mix in template helper methods. Looks for a
  980. // `templateHelpers` attribute, which can either be an
  981. // object literal, or a function that returns an object
  982. // literal. All methods and attributes from this object
  983. // are copies to the object passed in.
  984. mixinTemplateHelpers: function(target){
  985. target = target || {};
  986. var templateHelpers = this.templateHelpers;
  987. if (_.isFunction(templateHelpers)){
  988. templateHelpers = templateHelpers.call(this);
  989. }
  990. return _.extend(target, templateHelpers);
  991. },
  992. // Configure `triggers` to forward DOM events to view
  993. // events. `triggers: {"click .foo": "do:foo"}`
  994. configureTriggers: function(){
  995. if (!this.triggers) { return; }
  996. var triggerEvents = {};
  997. // Allow `triggers` to be configured as a function
  998. var triggers = _.result(this, "triggers");
  999. // Configure the triggers, prevent default
  1000. // action and stop propagation of DOM events
  1001. _.each(triggers, function(value, key){
  1002. // build the event handler function for the DOM event
  1003. triggerEvents[key] = function(e){
  1004. // stop the event in it's tracks
  1005. if (e && e.preventDefault){ e.preventDefault(); }
  1006. if (e && e.stopPropagation){ e.stopPropagation(); }
  1007. // build the args for the event
  1008. var args = {
  1009. view: this,
  1010. model: this.model,
  1011. collection: this.collection
  1012. };
  1013. // trigger the event
  1014. this.triggerMethod(value, args);
  1015. };
  1016. }, this);
  1017. return triggerEvents;
  1018. },
  1019. // Overriding Backbone.View's delegateEvents to handle
  1020. // the `triggers`, `modelEvents`, and `collectionEvents` configuration
  1021. delegateEvents: function(events){
  1022. this._delegateDOMEvents(events);
  1023. Marionette.bindEntityEvents(this, this.model, Marionette.getOption(this, "modelEvents"));
  1024. Marionette.bindEntityEvents(this, this.collection, Marionette.getOption(this, "collectionEvents"));
  1025. },
  1026. // internal method to delegate DOM events and triggers
  1027. _delegateDOMEvents: function(events){
  1028. events = events || this.events;
  1029. if (_.isFunction(events)){ events = events.call(this); }
  1030. var combinedEvents = {};
  1031. var triggers = this.configureTriggers();
  1032. _.extend(combinedEvents, events, triggers);
  1033. Backbone.View.prototype.delegateEvents.call(this, combinedEvents);
  1034. },
  1035. // Overriding Backbone.View's undelegateEvents to handle unbinding
  1036. // the `triggers`, `modelEvents`, and `collectionEvents` config
  1037. undelegateEvents: function(){
  1038. var args = Array.prototype.slice.call(arguments);
  1039. Backbone.View.prototype.undelegateEvents.apply(this, args);
  1040. Marionette.unbindEntityEvents(this, this.model, Marionette.getOption(this, "modelEvents"));
  1041. Marionette.unbindEntityEvents(this, this.collection, Marionette.getOption(this, "collectionEvents"));
  1042. },
  1043. // Internal method, handles the `show` event.
  1044. onShowCalled: function(){},
  1045. // Default `close` implementation, for removing a view from the
  1046. // DOM and unbinding it. Regions will call this method
  1047. // for you. You can specify an `onClose` method in your view to
  1048. // add custom code that is called after the view is closed.
  1049. close: function(){
  1050. if (this.isClosed) { return; }
  1051. // allow the close to be stopped by returning `false`
  1052. // from the `onBeforeClose` method
  1053. var shouldClose = this.triggerMethod("before:close");
  1054. if (shouldClose === false){
  1055. return;
  1056. }
  1057. // unbind UI elements
  1058. this.unbindUIElements();
  1059. // mark as closed before doing the actual close, to
  1060. // prevent infinite loops within "close" event handlers
  1061. // that are trying to close other views
  1062. this.isClosed = true;
  1063. this.triggerMethod("close");
  1064. this.remove();
  1065. },
  1066. // This method binds the elements specified in the "ui" hash inside the view's code with
  1067. // the associated jQuery selectors.
  1068. bindUIElements: function(){
  1069. if (!this.ui) { return; }
  1070. // store the ui hash in _uiBindings so they can be reset later
  1071. // and so re-rendering the view will be able to find the bindings
  1072. if (!this._uiBindings){
  1073. this._uiBindings = this.ui;
  1074. }
  1075. // get the bindings result, as a function or otherwise
  1076. var bindings = _.result(this, "_uiBindings");
  1077. // empty the ui so we don't have anything to start with
  1078. this.ui = {};
  1079. // bind each of the selectors
  1080. _.each(_.keys(bindings), function(key) {
  1081. var selector = bindings[key];
  1082. this.ui[key] = this.$(selector);
  1083. }, this);
  1084. },
  1085. // This method unbinds the elements specified in the "ui" hash
  1086. unbindUIElements: function(){
  1087. if (!this.ui){ return; }
  1088. // delete all of the existing ui bindings
  1089. _.each(this.ui, function($el, name){
  1090. delete this.ui[name];
  1091. }, this);
  1092. // reset the ui element to the original bindings configuration
  1093. this.ui = this._uiBindings;
  1094. delete this._uiBindings;
  1095. }
  1096. });
  1097. // Item View
  1098. // ---------
  1099. // A single item view implementation that contains code for rendering
  1100. // with underscore.js templates, serializing the view's model or collection,
  1101. // and calling several methods on extended views, such as `onRender`.
  1102. Marionette.ItemView = Marionette.View.extend({
  1103. constructor: function(){
  1104. Marionette.View.prototype.constructor.apply(this, slice(arguments));
  1105. },
  1106. // Serialize the model or collection for the view. If a model is
  1107. // found, `.toJSON()` is called. If a collection is found, `.toJSON()`
  1108. // is also called, but is used to populate an `items` array in the
  1109. // resulting data. If both are found, defaults to the model.
  1110. // You can override the `serializeData` method in your own view
  1111. // definition, to provide custom serialization for your view's data.
  1112. serializeData: function(){
  1113. var data = {};
  1114. if (this.model) {
  1115. data = this.model.toJSON();
  1116. }
  1117. else if (this.collection) {
  1118. data = { items: this.collection.toJSON() };
  1119. }
  1120. return data;
  1121. },
  1122. // Render the view, defaulting to underscore.js templates.
  1123. // You can override this in your view definition to provide
  1124. // a very specific rendering for your view. In general, though,
  1125. // you should override the `Marionette.Renderer` object to
  1126. // change how Marionette renders views.
  1127. render: function(){
  1128. this.isClosed = false;
  1129. this.triggerMethod("before:render", this);
  1130. this.triggerMethod("item:before:render", this);
  1131. var data = this.serializeData();
  1132. data = this.mixinTemplateHelpers(data);
  1133. var template = this.getTemplate();
  1134. var html = Marionette.Renderer.render(template, data);
  1135. this.$el.html(html);
  1136. this.bindUIElements();
  1137. this.triggerMethod("render", this);
  1138. this.triggerMethod("item:rendered", this);
  1139. return this;
  1140. },
  1141. // Override the default close event to add a few
  1142. // more events that are triggered.
  1143. close: function(){
  1144. if (this.isClosed){ return; }
  1145. this.triggerMethod('item:before:close');
  1146. Marionette.View.prototype.close.apply(this, slice(arguments));
  1147. this.triggerMethod('item:closed');
  1148. }
  1149. });
  1150. // Collection View
  1151. // ---------------
  1152. // A view that iterates over a Backbone.Collection
  1153. // and renders an individual ItemView for each model.
  1154. Marionette.CollectionView = Marionette.View.extend({
  1155. // used as the prefix for item view events
  1156. // that are forwarded through the collectionview
  1157. itemViewEventPrefix: "itemview",
  1158. // constructor
  1159. constructor: function(options){
  1160. this._initChildViewStorage();
  1161. Marionette.View.prototype.constructor.apply(this, slice(arguments));
  1162. this._initialEvents();
  1163. },
  1164. // Configured the initial events that the collection view
  1165. // binds to. Override this method to prevent the initial
  1166. // events, or to add your own initial events.
  1167. _initialEvents: function(){
  1168. if (this.collection){
  1169. this.listenTo(this.collection, "add", this.addChildView, this);
  1170. this.listenTo(this.collection, "remove", this.removeItemView, this);
  1171. this.listenTo(this.collection, "reset", this.render, this);
  1172. }
  1173. },
  1174. // Handle a child item added to the collection
  1175. addChildView: function(item, collection, options){
  1176. this.closeEmptyView();
  1177. var ItemView = this.getItemView(item);
  1178. var index = this.collection.indexOf(item);
  1179. this.addItemView(item, ItemView, index);
  1180. },
  1181. // Override from `Marionette.View` to guarantee the `onShow` method
  1182. // of child views is called.
  1183. onShowCalled: function(){
  1184. this.children.each(function(child){
  1185. Marionette.triggerMethod.call(child, "show");
  1186. });
  1187. },
  1188. // Internal method to trigger the before render callbacks
  1189. // and events
  1190. triggerBeforeRender: function(){
  1191. this.triggerMethod("before:render", this);
  1192. this.triggerMethod("collection:before:render", this);
  1193. },
  1194. // Internal method to trigger the rendered callbacks and
  1195. // events
  1196. triggerRendered: function(){
  1197. this.triggerMethod("render", this);
  1198. this.triggerMethod("collection:rendered", this);
  1199. },
  1200. // Render the collection of items. Override this method to
  1201. // provide your own implementation of a render function for
  1202. // the collection view.
  1203. render: function(){
  1204. this.isClosed = false;
  1205. this.triggerBeforeRender();
  1206. this._renderChildren();
  1207. this.triggerRendered();
  1208. return this;
  1209. },
  1210. // Internal method. Separated so that CompositeView can have
  1211. // more control over events being triggered, around the rendering
  1212. // process
  1213. _renderChildren: function(){
  1214. this.closeEmptyView();
  1215. this.closeChildren();
  1216. if (this.collection && this.collection.length > 0) {
  1217. this.showCollection();
  1218. } else {
  1219. this.showEmptyView();
  1220. }
  1221. },
  1222. // Internal method to loop through each item in the
  1223. // collection view and show it
  1224. showCollection: function(){
  1225. var ItemView;
  1226. this.collection.each(function(item, index){
  1227. ItemView = this.getItemView(item);
  1228. this.addItemView(item, ItemView, index);
  1229. }, this);
  1230. },
  1231. // Internal method to show an empty view in place of
  1232. // a collection of item views, when the collection is
  1233. // empty
  1234. showEmptyView: function(){
  1235. var EmptyView = Marionette.getOption(this, "emptyView");
  1236. if (EmptyView && !this._showingEmptyView){
  1237. this._showingEmptyView = true;
  1238. var model = new Backbone.Model();
  1239. this.addItemView(model, EmptyView, 0);
  1240. }
  1241. },
  1242. // Internal method to close an existing emptyView instance
  1243. // if one exists. Called when a collection view has been
  1244. // rendered empty, and then an item is added to the collection.
  1245. closeEmptyView: function(){
  1246. if (this._showingEmptyView){
  1247. this.closeChildren();
  1248. delete this._showingEmptyView;
  1249. }
  1250. },
  1251. // Retrieve the itemView type, either from `this.options.itemView`
  1252. // or from the `itemView` in the object definition. The "options"
  1253. // takes precedence.
  1254. getItemView: function(item){
  1255. var itemView = Marionette.getOption(this, "itemView");
  1256. if (!itemView){
  1257. throwError("An `itemView` must be specified", "NoItemViewError");
  1258. }
  1259. return itemView;
  1260. },
  1261. // Render the child item's view and add it to the
  1262. // HTML for the collection view.
  1263. addItemView: function(item, ItemView, index){
  1264. // get the itemViewOptions if any were specified
  1265. var itemViewOptions = Marionette.getOption(this, "itemViewOptions");
  1266. if (_.isFunction(itemViewOptions)){
  1267. itemViewOptions = itemViewOptions.call(this, item, index);
  1268. }
  1269. // build the view
  1270. var view = this.buildItemView(item, ItemView, itemViewOptions);
  1271. // set up the child view event forwarding
  1272. this.addChildViewEventForwarding(view);
  1273. // this view is about to be added
  1274. this.triggerMethod("before:item:added", view);
  1275. // Store the child view itself so we can properly
  1276. // remove and/or close it later
  1277. this.children.add(view);
  1278. // Render it and show it
  1279. this.renderItemView(view, index);
  1280. // call the "show" method if the collection view
  1281. // has already been shown
  1282. if (this._isShown){
  1283. Marionette.triggerMethod.call(view, "show");
  1284. }
  1285. // this view was added
  1286. this.triggerMethod("after:item:added", view);
  1287. },
  1288. // Set up the child view event forwarding. Uses an "itemview:"
  1289. // prefix in front of all forwarded events.
  1290. addChildViewEventForwarding: function(view){
  1291. var prefix = Marionette.getOption(this, "itemViewEventPrefix");
  1292. // Forward all child item view events through the parent,
  1293. // prepending "itemview:" to the event name
  1294. this.listenTo(view, "all", function(){
  1295. var args = slice(arguments);
  1296. args[0] = prefix + ":" + args[0];
  1297. args.splice(1, 0, view);
  1298. Marionette.triggerMethod.apply(this, args);
  1299. }, this);
  1300. },
  1301. // render the item view
  1302. renderItemView: function(view, index) {
  1303. view.render();
  1304. this.appendHtml(this, view, index);
  1305. },
  1306. // Build an `itemView` for every model in the collection.
  1307. buildItemView: function(item, ItemViewType, itemViewOptions){
  1308. var options = _.extend({model: item}, itemViewOptions);
  1309. return new ItemViewType(options);
  1310. },
  1311. // get the child view by item it holds, and remove it
  1312. removeItemView: function(item){
  1313. var view = this.children.findByModel(item);
  1314. this.removeChildView(view);
  1315. this.checkEmpty();
  1316. },
  1317. // Remove the child view and close it
  1318. removeChildView: function(view){
  1319. // shut down the child view properly,
  1320. // including events that the collection has from it
  1321. if (view){
  1322. this.stopListening(view);
  1323. // call 'close' or 'remove', depending on which is found
  1324. if (view.close) { view.close(); }
  1325. else if (view.remove) { view.remove(); }
  1326. this.children.remove(view);
  1327. }
  1328. this.triggerMethod("item:removed", view);
  1329. },
  1330. // helper to show the empty view if the collection is empty
  1331. checkEmpty: function() {
  1332. // check if we're empty now, and if we are, show the
  1333. // empty view
  1334. if (!this.collection || this.collection.length === 0){
  1335. this.showEmptyView();
  1336. }
  1337. },
  1338. // Append the HTML to the collection's `el`.
  1339. // Override this method to do something other
  1340. // then `.append`.
  1341. appendHtml: function(collectionView, itemView, index){
  1342. collectionView.$el.append(itemView.el);
  1343. },
  1344. // Internal method to set up the `children` object for
  1345. // storing all of the child views
  1346. _initChildViewStorage: function(){
  1347. this.children = new Backbone.ChildViewContainer();
  1348. },
  1349. // Handle cleanup and other closing needs for
  1350. // the collection of views.
  1351. close: function(){
  1352. if (this.isClosed){ return; }
  1353. this.triggerMethod("collection:before:close");
  1354. this.closeChildren();
  1355. this.triggerMethod("collection:closed");
  1356. Marionette.View.prototype.close.apply(this, slice(arguments));
  1357. },
  1358. // Close the child views that this collection view
  1359. // is holding on to, if any
  1360. closeChildren: function(){
  1361. this.children.each(function(child){
  1362. this.removeChildView(child);
  1363. }, this);
  1364. this.checkEmpty();
  1365. }
  1366. });
  1367. // Composite View
  1368. // --------------
  1369. // Used for rendering a branch-leaf, hierarchical structure.
  1370. // Extends directly from CollectionView and also renders an
  1371. // an item view as `modelView`, for the top leaf
  1372. Marionette.CompositeView = Marionette.CollectionView.extend({
  1373. constructor: function(options){
  1374. Marionette.CollectionView.apply(this, slice(arguments));
  1375. this.itemView = this.getItemView();
  1376. },
  1377. // Configured the initial events that the composite view
  1378. // binds to. Override this method to prevent the initial
  1379. // events, or to add your own initial events.
  1380. _initialEvents: function(){
  1381. if (this.collection){
  1382. this.listenTo(this.collection, "add", this.addChildView, this);
  1383. this.listenTo(this.collection, "remove", this.removeItemView, this);
  1384. this.listenTo(this.collection, "reset", this._renderChildren, this);
  1385. }
  1386. },
  1387. // Retrieve the `itemView` to be used when rendering each of
  1388. // the items in the collection. The default is to return
  1389. // `this.itemView` or Marionette.CompositeView if no `itemView`
  1390. // has been defined
  1391. getItemView: function(item){
  1392. var itemView = Marionette.getOption(this, "itemView") || this.constructor;
  1393. if (!itemView){
  1394. throwError("An `itemView` must be specified", "NoItemViewError");
  1395. }
  1396. return itemView;
  1397. },
  1398. // Serialize the collection for the view.
  1399. // You can override the `serializeData` method in your own view
  1400. // definition, to provide custom serialization for your view's data.
  1401. serializeData: function(){
  1402. var data = {};
  1403. if (this.model){
  1404. data = this.model.toJSON();
  1405. }
  1406. return data;
  1407. },
  1408. // Renders the model once, and the collection once. Calling
  1409. // this again will tell the model's view to re-render itself
  1410. // but the collection will not re-render.
  1411. render: function(){
  1412. this.isRendered = true;
  1413. this.isClosed = false;
  1414. this.resetItemViewContainer();
  1415. this.triggerBeforeRender();
  1416. var html = this.renderModel();
  1417. this.$el.html(html);
  1418. // the ui bindings is done here and not at the end of render since they
  1419. // will not be available until after the model is rendered, but should be
  1420. // available before the collection is rendered.
  1421. this.bindUIElements();
  1422. this.triggerMethod("composite:model:rendered");
  1423. this._renderChildren();
  1424. this.triggerMethod("composite:rendered");
  1425. this.triggerRendered();
  1426. return this;
  1427. },
  1428. _renderChildren: function(){
  1429. if (this.isRendered){
  1430. Marionette.CollectionView.prototype._renderChildren.call(this);
  1431. this.triggerMethod("composite:collection:rendered");
  1432. }
  1433. },
  1434. // Render an individual model, if we have one, as
  1435. // part of a composite view (branch / leaf). For example:
  1436. // a treeview.
  1437. renderModel: function(){
  1438. var data = {};
  1439. data = this.serializeData();
  1440. data = this.mixinTemplateHelpers(data);
  1441. var template = this.getTemplate();
  1442. return Marionette.Renderer.render(template, data);
  1443. },
  1444. // Appends the `el` of itemView instances to the specified
  1445. // `itemViewContainer` (a jQuery selector). Override this method to
  1446. // provide custom logic of how the child item view instances have their
  1447. // HTML appended to the composite view instance.
  1448. appendHtml: function(cv, iv){
  1449. var $container = this.getItemViewContainer(cv);
  1450. $container.append(iv.el);
  1451. },
  1452. // Internal method to ensure an `$itemViewContainer` exists, for the
  1453. // `appendHtml` method to use.
  1454. getItemViewContainer: function(containerView){
  1455. if ("$itemViewContainer" in containerView){
  1456. return containerView.$itemViewContainer;
  1457. }
  1458. var container;
  1459. if (containerView.itemViewContainer){
  1460. var selector = _.result(containerView, "itemViewContainer");
  1461. container = containerView.$(selector);
  1462. if (container.length <= 0) {
  1463. throwError("The specified `itemViewContainer` was not found: " + containerView.itemViewContainer, "ItemViewContainerMissingError");
  1464. }
  1465. } else {
  1466. container = containerView.$el;
  1467. }
  1468. containerView.$itemViewContainer = container;
  1469. return container;
  1470. },
  1471. // Internal method to reset the `$itemViewContainer` on render
  1472. resetItemViewContainer: function(){
  1473. if (this.$itemViewContainer){
  1474. delete this.$itemViewContainer;
  1475. }
  1476. }
  1477. });
  1478. // Layout
  1479. // ------
  1480. // Used for managing application layouts, nested layouts and
  1481. // multiple regions within an application or sub-application.
  1482. //
  1483. // A specialized view type that renders an area of HTML and then
  1484. // attaches `Region` instances to the specified `regions`.
  1485. // Used for composite view management and sub-application areas.
  1486. Marionette.Layout = Marionette.ItemView.extend({
  1487. regionType: Marionette.Region,
  1488. // Ensure the regions are available when the `initialize` method
  1489. // is called.
  1490. constructor: function (options) {
  1491. options = options || {};
  1492. this._firstRender = true;
  1493. this._initializeRegions(options);
  1494. Marionette.ItemView.call(this, options);
  1495. },
  1496. // Layout's render will use the existing region objects the
  1497. // first time it is called. Subsequent calls will close the
  1498. // views that the regions are showing and then reset the `el`
  1499. // for the regions to the newly rendered DOM elements.
  1500. render: function(){
  1501. if (this._firstRender){
  1502. // if this is the first render, don't do anything to
  1503. // reset the regions
  1504. this._firstRender = false;
  1505. } else if (this.isClosed){
  1506. // a previously closed layout means we need to
  1507. // completely re-initialize the regions
  1508. this._initializeRegions();
  1509. } else {
  1510. // If this is not the first render call, then we need to
  1511. // re-initializing the `el` for each region
  1512. this._reInitializeRegions();
  1513. }
  1514. var args = Array.prototype.slice.apply(arguments);
  1515. var result = Marionette.ItemView.prototype.render.apply(this, args);
  1516. return result;
  1517. },
  1518. // Handle closing regions, and then close the view itself.
  1519. close: function () {
  1520. if (this.isClosed){ return; }
  1521. this.regionManager.close();
  1522. var args = Array.prototype.slice.apply(arguments);
  1523. Marionette.ItemView.prototype.close.apply(this, args);
  1524. },
  1525. // Add a single region, by name, to the layout
  1526. addRegion: function(name, definition){
  1527. var regions = {};
  1528. regions[name] = definition;
  1529. return this.addRegions(regions)[name];
  1530. },
  1531. // Add multiple regions as a {name: definition, name2: def2} object literal
  1532. addRegions: function(regions){
  1533. this.regions = _.extend(this.regions || {}, regions);
  1534. return this._buildRegions(regions);
  1535. },
  1536. // Remove a single region from the Layout, by name
  1537. removeRegion: function(name){
  1538. return this.regionManager.removeRegion(name);
  1539. },
  1540. // internal method to build regions
  1541. _buildRegions: function(regions){
  1542. var that = this;
  1543. var defaults = {
  1544. parentEl: function(){ return that.$el; }
  1545. };
  1546. return this.regionManager.addRegions(regions, defaults);
  1547. },
  1548. // Internal method to initialize the regions that have been defined in a
  1549. // `regions` attribute on this layout.
  1550. _initializeRegions: function (options) {
  1551. var regions;
  1552. this._initRegionManager();
  1553. if (_.isFunction(this.regions)) {
  1554. regions = this.regions(options);
  1555. } else {
  1556. regions = this.regions || {};
  1557. }
  1558. this.addRegions(regions);
  1559. },
  1560. // Internal method to re-initialize all of the regions by updating the `el` that
  1561. // they point to
  1562. _reInitializeRegions: function(){
  1563. this.regionManager.closeRegions();
  1564. this.regionManager.each(function(region){
  1565. region.reset();
  1566. });
  1567. },
  1568. // Internal method to initialize the region manager
  1569. // and all regions in it
  1570. _initRegionManager: function(){
  1571. this.regionManager = new Marionette.RegionManager();
  1572. this.listenTo(this.regionManager, "region:add", function(name, region){
  1573. this[name] = region;
  1574. this.trigger("region:add", name, region);
  1575. });
  1576. this.listenTo(this.regionManager, "region:remove", function(name, region){
  1577. delete this[name];
  1578. this.trigger("region:remove", name, region);
  1579. });
  1580. }
  1581. });
  1582. // AppRouter
  1583. // ---------
  1584. // Reduce the boilerplate code of handling route events
  1585. // and then calling a single method on another object.
  1586. // Have your routers configured to call the method on
  1587. // your object, directly.
  1588. //
  1589. // Configure an AppRouter with `appRoutes`.
  1590. //
  1591. // App routers can only take one `controller` object.
  1592. // It is recommended that you divide your controller
  1593. // objects in to smaller pieces of related functionality
  1594. // and have multiple routers / controllers, instead of
  1595. // just one giant router and controller.
  1596. //
  1597. // You can also add standard routes to an AppRouter.
  1598. Marionette.AppRouter = Backbone.Router.extend({
  1599. constructor: function(options){
  1600. Backbone.Router.prototype.constructor.apply(this, slice(arguments));
  1601. this.options = options;
  1602. if (this.appRoutes){
  1603. var controller = Marionette.getOption(this, "controller");
  1604. this.processAppRoutes(controller, this.appRoutes);
  1605. }
  1606. },
  1607. // Internal method to process the `appRoutes` for the
  1608. // router, and turn them in to routes that trigger the
  1609. // specified method on the specified `controller`.
  1610. processAppRoutes: function(controller, appRoutes){
  1611. _.each(appRoutes, function(methodName, route) {
  1612. var method = controller[methodName];
  1613. if (!method) {
  1614. throw new Error("Method '" + methodName + "' was not found on the controller");
  1615. }
  1616. this.route(route, methodName, _.bind(method, controller));
  1617. }, this);
  1618. }
  1619. });
  1620. // Application
  1621. // -----------
  1622. // Contain and manage the composite application as a whole.
  1623. // Stores and starts up `Region` objects, includes an
  1624. // event aggregator as `app.vent`
  1625. Marionette.Application = function(options){
  1626. this._initRegionManager();
  1627. this._initCallbacks = new Marionette.Callbacks();
  1628. this.vent = new Backbone.Wreqr.EventAggregator();
  1629. this.commands = new Backbone.Wreqr.Commands();
  1630. this.reqres = new Backbone.Wreqr.RequestResponse();
  1631. this.submodules = {};
  1632. _.extend(this, options);
  1633. this.triggerMethod = Marionette.triggerMethod;
  1634. };
  1635. _.extend(Marionette.Application.prototype, Backbone.Events, {
  1636. // Command execution, facilitated by Backbone.Wreqr.Commands
  1637. execute: function(){
  1638. var args = Array.prototype.slice.apply(arguments);
  1639. this.commands.execute.apply(this.commands, args);
  1640. },
  1641. // Request/response, facilitated by Backbone.Wreqr.RequestResponse
  1642. request: function(){
  1643. var args = Array.prototype.slice.apply(arguments);
  1644. return this.reqres.request.apply(this.reqres, args);
  1645. },
  1646. // Add an initializer that is either run at when the `start`
  1647. // method is called, or run immediately if added after `start`
  1648. // has already been called.
  1649. addInitializer: function(initializer){
  1650. this._initCallbacks.add(initializer);
  1651. },
  1652. // kick off all of the application's processes.
  1653. // initializes all of the regions that have been added
  1654. // to the app, and runs all of the initializer functions
  1655. start: function(options){
  1656. this.triggerMethod("initialize:before", options);
  1657. this._initCallbacks.run(options, this);
  1658. this.triggerMethod("initialize:after", options);
  1659. this.triggerMethod("start", options);
  1660. },
  1661. // Add regions to your app.
  1662. // Accepts a hash of named strings or Region objects
  1663. // addRegions({something: "#someRegion"})
  1664. // addRegions({something: Region.extend({el: "#someRegion"}) });
  1665. addRegions: function(regions){
  1666. return this._regionManager.addRegions(regions);
  1667. },
  1668. // Removes a region from your app.
  1669. // Accepts the regions name
  1670. // removeRegion('myRegion')
  1671. removeRegion: function(region) {
  1672. this._regionManager.removeRegion(region);
  1673. },
  1674. // Create a module, attached to the application
  1675. module: function(moduleNames, moduleDefinition){
  1676. // slice the args, and add this application object as the
  1677. // first argument of the array
  1678. var args = slice(arguments);
  1679. args.unshift(this);
  1680. // see the Marionette.Module object for more information
  1681. return Marionette.Module.create.apply(Marionette.Module, args);
  1682. },
  1683. // Internal method to set up the region manager
  1684. _initRegionManager: function(){
  1685. this._regionManager = new Marionette.RegionManager();
  1686. this.listenTo(this._regionManager, "region:add", function(name, region){
  1687. this[name] = region;
  1688. });
  1689. this.listenTo(this._regionManager, "region:remove", function(name, region){
  1690. delete this[name];
  1691. });
  1692. }
  1693. });
  1694. // Copy the `extend` function used by Backbone's classes
  1695. Marionette.Application.extend = Marionette.extend;
  1696. // Module
  1697. // ------
  1698. // A simple module system, used to create privacy and encapsulation in
  1699. // Marionette applications
  1700. Marionette.Module = function(moduleName, app){
  1701. this.moduleName = moduleName;
  1702. // store sub-modules
  1703. this.submodules = {};
  1704. this._setupInitializersAndFinalizers();
  1705. // store the configuration for this module
  1706. this.app = app;
  1707. this.startWithParent = true;
  1708. this.triggerMethod = Marionette.triggerMethod;
  1709. };
  1710. // Extend the Module prototype with events / listenTo, so that the module
  1711. // can be used as an event aggregator or pub/sub.
  1712. _.extend(Marionette.Module.prototype, Backbone.Events, {
  1713. // Initializer for a specific module. Initializers are run when the
  1714. // module's `start` method is called.
  1715. addInitializer: function(callback){
  1716. this._initializerCallbacks.add(callback);
  1717. },
  1718. // Finalizers are run when a module is stopped. They are used to teardown
  1719. // and finalize any variables, references, events and other code that the
  1720. // module had set up.
  1721. addFinalizer: function(callback){
  1722. this._finalizerCallbacks.add(callback);
  1723. },
  1724. // Start the module, and run all of its initializers
  1725. start: function(options){
  1726. // Prevent re-starting a module that is already started
  1727. if (this._isInitialized){ return; }
  1728. // start the sub-modules (depth-first hierarchy)
  1729. _.each(this.submodules, function(mod){
  1730. // check to see if we should start the sub-module with this parent
  1731. if (mod.startWithParent){
  1732. mod.start(options);
  1733. }
  1734. });
  1735. // run the callbacks to "start" the current module
  1736. this.triggerMethod("before:start", options);
  1737. this._initializerCallbacks.run(options, this);
  1738. this._isInitialized = true;
  1739. this.triggerMethod("start", options);
  1740. },
  1741. // Stop this module by running its finalizers and then stop all of
  1742. // the sub-modules for this module
  1743. stop: function(){
  1744. // if we are not initialized, don't bother finalizing
  1745. if (!this._isInitialized){ return; }
  1746. this._isInitialized = false;
  1747. Marionette.triggerMethod.call(this, "before:stop");
  1748. // stop the sub-modules; depth-first, to make sure the
  1749. // sub-modules are stopped / finalized before parents
  1750. _.each(this.submodules, function(mod){ mod.stop(); });
  1751. // run the finalizers
  1752. this._finalizerCallbacks.run(undefined,this);
  1753. // reset the initializers and finalizers
  1754. this._initializerCallbacks.reset();
  1755. this._finalizerCallbacks.reset();
  1756. Marionette.triggerMethod.call(this, "stop");
  1757. },
  1758. // Configure the module with a definition function and any custom args
  1759. // that are to be passed in to the definition function
  1760. addDefinition: function(moduleDefinition, customArgs){
  1761. this._runModuleDefinition(moduleDefinition, customArgs);
  1762. },
  1763. // Internal method: run the module definition function with the correct
  1764. // arguments
  1765. _runModuleDefinition: function(definition, customArgs){
  1766. if (!definition){ return; }
  1767. // build the correct list of arguments for the module definition
  1768. var args = _.flatten([
  1769. this,
  1770. this.app,
  1771. Backbone,
  1772. Marionette,
  1773. Marionette.$, _,
  1774. customArgs
  1775. ]);
  1776. definition.apply(this, args);
  1777. },
  1778. // Internal method: set up new copies of initializers and finalizers.
  1779. // Calling this method will wipe out all existing initializers and
  1780. // finalizers.
  1781. _setupInitializersAndFinalizers: function(){
  1782. this._initializerCallbacks = new Marionette.Callbacks();
  1783. this._finalizerCallbacks = new Marionette.Callbacks();
  1784. }
  1785. });
  1786. // Type methods to create modules
  1787. _.extend(Marionette.Module, {
  1788. // Create a module, hanging off the app parameter as the parent object.
  1789. create: function(app, moduleNames, moduleDefinition){
  1790. var module = app;
  1791. // get the custom args passed in after the module definition and
  1792. // get rid of the module name and definition function
  1793. var customArgs = slice(arguments);
  1794. customArgs.splice(0, 3);
  1795. // split the module names and get the length
  1796. moduleNames = moduleNames.split(".");
  1797. var length = moduleNames.length;
  1798. // store the module definition for the last module in the chain
  1799. var moduleDefinitions = [];
  1800. moduleDefinitions[length-1] = moduleDefinition;
  1801. // Loop through all the parts of the module definition
  1802. _.each(moduleNames, function(moduleName, i){
  1803. var parentModule = module;
  1804. module = this._getModule(parentModule, moduleName, app);
  1805. this._addModuleDefinition(parentModule, module, moduleDefinitions[i], customArgs);
  1806. }, this);
  1807. // Return the last module in the definition chain
  1808. return module;
  1809. },
  1810. _getModule: function(parentModule, moduleName, app, def, args){
  1811. // Get an existing module of this name if we have one
  1812. var module = parentModule[moduleName];
  1813. if (!module){
  1814. // Create a new module if we don't have one
  1815. module = new Marionette.Module(moduleName, app);
  1816. parentModule[moduleName] = module;
  1817. // store the module on the parent
  1818. parentModule.submodules[moduleName] = module;
  1819. }
  1820. return module;
  1821. },
  1822. _addModuleDefinition: function(parentModule, module, def, args){
  1823. var fn;
  1824. var startWithParent;
  1825. if (_.isFunction(def)){
  1826. // if a function is supplied for the module definition
  1827. fn = def;
  1828. startWithParent = true;
  1829. } else if (_.isObject(def)){
  1830. // if an object is supplied
  1831. fn = def.define;
  1832. startWithParent = def.startWithParent;
  1833. } else {
  1834. // if nothing is supplied
  1835. startWithParent = true;
  1836. }
  1837. // add module definition if needed
  1838. if (fn){
  1839. module.addDefinition(fn, args);
  1840. }
  1841. // `and` the two together, ensuring a single `false` will prevent it
  1842. // from starting with the parent
  1843. module.startWithParent = module.startWithParent && startWithParent;
  1844. // setup auto-start if needed
  1845. if (module.startWithParent && !module.startWithParentIsConfigured){
  1846. // only configure this once
  1847. module.startWithParentIsConfigured = true;
  1848. // add the module initializer config
  1849. parentModule.addInitializer(function(options){
  1850. if (module.startWithParent){
  1851. module.start(options);
  1852. }
  1853. });
  1854. }
  1855. }
  1856. });
  1857. return Marionette;
  1858. })(this, Backbone, _);