PageRenderTime 78ms CodeModel.GetById 38ms RepoModel.GetById 1ms app.codeStats 0ms

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

https://gitlab.com/Blueprint-Marketing/cdnjs
JavaScript | 1358 lines | 752 code | 247 blank | 359 comment | 105 complexity | ee56d48a0e4f0b24c2a7c4529a349660 MD5 | raw file
  1. // Backbone.Marionette, v0.9.12
  2. // Copyright (c)2012 Derick Bailey, Muted Solutions, LLC.
  3. // Distributed under MIT license
  4. // http://github.com/derickbailey/backbone.marionette
  5. Backbone.Marionette = (function(Backbone, _, $){
  6. var Marionette = {};
  7. // EventBinder
  8. // -----------
  9. // The event binder facilitates the binding and unbinding of events
  10. // from objects that extend `Backbone.Events`. It makes
  11. // unbinding events, even with anonymous callback functions,
  12. // easy.
  13. //
  14. // Inspired by [Johnny Oshika](http://stackoverflow.com/questions/7567404/backbone-js-repopulate-or-recreate-the-view/7607853#7607853)
  15. Marionette.EventBinder = function(){
  16. this._eventBindings = [];
  17. };
  18. _.extend(Marionette.EventBinder.prototype, {
  19. // Store the event binding in array so it can be unbound
  20. // easily, at a later point in time.
  21. bindTo: function (obj, eventName, callback, context) {
  22. context = context || this;
  23. obj.on(eventName, callback, context);
  24. var binding = {
  25. obj: obj,
  26. eventName: eventName,
  27. callback: callback,
  28. context: context
  29. };
  30. this._eventBindings.push(binding);
  31. return binding;
  32. },
  33. // Unbind from a single binding object. Binding objects are
  34. // returned from the `bindTo` method call.
  35. unbindFrom: function(binding){
  36. binding.obj.off(binding.eventName, binding.callback, binding.context);
  37. this._eventBindings = _.reject(this._eventBindings, function(bind){return bind === binding;});
  38. },
  39. // Unbind all of the events that we have stored.
  40. unbindAll: function () {
  41. var that = this;
  42. // The `unbindFrom` call removes elements from the array
  43. // while it is being iterated, so clone it first.
  44. var bindings = _.map(this._eventBindings, _.identity);
  45. _.each(bindings, function (binding, index) {
  46. that.unbindFrom(binding);
  47. });
  48. }
  49. });
  50. // Copy the `extend` function used by Backbone's classes
  51. Marionette.EventBinder.extend = Backbone.View.extend;
  52. // Marionette.View
  53. // ---------------
  54. // The core view type that other Marionette views extend from.
  55. Marionette.View = Backbone.View.extend({
  56. constructor: function(){
  57. var eventBinder = new Marionette.EventBinder();
  58. _.extend(this, eventBinder);
  59. Backbone.View.prototype.constructor.apply(this, arguments);
  60. this.bindTo(this, "show", this.onShowCalled, this);
  61. },
  62. // Get the template for this view
  63. // instance. You can set a `template` attribute in the view
  64. // definition or pass a `template: "whatever"` parameter in
  65. // to the constructor options.
  66. getTemplate: function(){
  67. var template;
  68. // Get the template from `this.options.template` or
  69. // `this.template`. The `options` takes precedence.
  70. if (this.options && this.options.template){
  71. template = this.options.template;
  72. } else {
  73. template = this.template;
  74. }
  75. return template;
  76. },
  77. // Serialize the model or collection for the view. If a model is
  78. // found, `.toJSON()` is called. If a collection is found, `.toJSON()`
  79. // is also called, but is used to populate an `items` array in the
  80. // resulting data. If both are found, defaults to the model.
  81. // You can override the `serializeData` method in your own view
  82. // definition, to provide custom serialization for your view's data.
  83. serializeData: function(){
  84. var data;
  85. if (this.model) {
  86. data = this.model.toJSON();
  87. }
  88. else if (this.collection) {
  89. data = { items: this.collection.toJSON() };
  90. }
  91. data = this.mixinTemplateHelpers(data);
  92. return data;
  93. },
  94. // Mix in template helper methods. Looks for a
  95. // `templateHelpers` attribute, which can either be an
  96. // object literal, or a function that returns an object
  97. // literal. All methods and attributes from this object
  98. // are copies to the object passed in.
  99. mixinTemplateHelpers: function(target){
  100. target = target || {};
  101. var templateHelpers = this.templateHelpers;
  102. if (_.isFunction(templateHelpers)){
  103. templateHelpers = templateHelpers.call(this);
  104. }
  105. return _.extend(target, templateHelpers);
  106. },
  107. // Configure `triggers` to forward DOM events to view
  108. // events. `triggers: {"click .foo": "do:foo"}`
  109. configureTriggers: function(){
  110. if (!this.triggers) { return; }
  111. var triggers = this.triggers;
  112. var that = this;
  113. var triggerEvents = {};
  114. // Allow `triggers` to be configured as a function
  115. if (_.isFunction(triggers)){ triggers = triggers.call(this); }
  116. // Configure the triggers, prevent default
  117. // action and stop propagation of DOM events
  118. _.each(triggers, function(value, key){
  119. triggerEvents[key] = function(e){
  120. if (e && e.preventDefault){ e.preventDefault(); }
  121. if (e && e.stopPropagation){ e.stopPropagation(); }
  122. that.trigger(value);
  123. };
  124. });
  125. return triggerEvents;
  126. },
  127. // Overriding Backbone.View's delegateEvents specifically
  128. // to handle the `triggers` configuration
  129. delegateEvents: function(events){
  130. events = events || this.events;
  131. if (_.isFunction(events)){ events = events.call(this); }
  132. var combinedEvents = {};
  133. var triggers = this.configureTriggers();
  134. _.extend(combinedEvents, events, triggers);
  135. Backbone.View.prototype.delegateEvents.call(this, combinedEvents);
  136. },
  137. // Internal method, handles the `show` event.
  138. onShowCalled: function(){},
  139. // Default `close` implementation, for removing a view from the
  140. // DOM and unbinding it. Regions will call this method
  141. // for you. You can specify an `onClose` method in your view to
  142. // add custom code that is called after the view is closed.
  143. close: function(){
  144. if (this.beforeClose) { this.beforeClose(); }
  145. this.remove();
  146. if (this.onClose) { this.onClose(); }
  147. this.trigger('close');
  148. this.unbindAll();
  149. this.unbind();
  150. },
  151. // This method binds the elements specified in the "ui" hash inside the view's code with
  152. // the associated jQuery selectors.
  153. bindUIElements: function(){
  154. if (!this.ui) { return; }
  155. var that = this;
  156. if (!this.uiBindings) {
  157. // We want to store the ui hash in uiBindings, since afterwards the values in the ui hash
  158. // will be overridden with jQuery selectors.
  159. this.uiBindings = this.ui;
  160. }
  161. // refreshing the associated selectors since they should point to the newly rendered elements.
  162. this.ui = {};
  163. _.each(_.keys(this.uiBindings), function(key) {
  164. var selector = that.uiBindings[key];
  165. that.ui[key] = that.$(selector);
  166. });
  167. }
  168. });
  169. // Item View
  170. // ---------
  171. // A single item view implementation that contains code for rendering
  172. // with underscore.js templates, serializing the view's model or collection,
  173. // and calling several methods on extended views, such as `onRender`.
  174. Marionette.ItemView = Marionette.View.extend({
  175. constructor: function(){
  176. Marionette.View.prototype.constructor.apply(this, arguments);
  177. if (this.initialEvents){
  178. this.initialEvents();
  179. }
  180. },
  181. // Render the view, defaulting to underscore.js templates.
  182. // You can override this in your view definition to provide
  183. // a very specific rendering for your view. In general, though,
  184. // you should override the `Marionette.Renderer` object to
  185. // change how Marionette renders views.
  186. render: function(){
  187. if (this.beforeRender){ this.beforeRender(); }
  188. this.trigger("before:render", this);
  189. this.trigger("item:before:render", this);
  190. var data = this.serializeData();
  191. var template = this.getTemplate();
  192. var html = Marionette.Renderer.render(template, data);
  193. this.$el.html(html);
  194. this.bindUIElements();
  195. if (this.onRender){ this.onRender(); }
  196. this.trigger("render", this);
  197. this.trigger("item:rendered", this);
  198. return this;
  199. },
  200. // Override the default close event to add a few
  201. // more events that are triggered.
  202. close: function(){
  203. this.trigger('item:before:close');
  204. Marionette.View.prototype.close.apply(this, arguments);
  205. this.trigger('item:closed');
  206. }
  207. });
  208. // Collection View
  209. // ---------------
  210. // A view that iterates over a Backbone.Collection
  211. // and renders an individual ItemView for each model.
  212. Marionette.CollectionView = Marionette.View.extend({
  213. constructor: function(){
  214. Marionette.View.prototype.constructor.apply(this, arguments);
  215. this.initChildViewStorage();
  216. this.initialEvents();
  217. this.onShowCallbacks = new Marionette.Callbacks();
  218. },
  219. // Configured the initial events that the collection view
  220. // binds to. Override this method to prevent the initial
  221. // events, or to add your own initial events.
  222. initialEvents: function(){
  223. if (this.collection){
  224. this.bindTo(this.collection, "add", this.addChildView, this);
  225. this.bindTo(this.collection, "remove", this.removeItemView, this);
  226. this.bindTo(this.collection, "reset", this.render, this);
  227. }
  228. },
  229. // Handle a child item added to the collection
  230. addChildView: function(item, collection, options){
  231. this.closeEmptyView();
  232. var ItemView = this.getItemView();
  233. return this.addItemView(item, ItemView, options.index);
  234. },
  235. // Override from `Marionette.View` to guarantee the `onShow` method
  236. // of child views is called.
  237. onShowCalled: function(){
  238. this.onShowCallbacks.run();
  239. },
  240. // Internal method to trigger the before render callbacks
  241. // and events
  242. triggerBeforeRender: function(){
  243. if (this.beforeRender) { this.beforeRender(); }
  244. this.trigger("before:render", this);
  245. this.trigger("collection:before:render", this);
  246. },
  247. // Internal method to trigger the rendered callbacks and
  248. // events
  249. triggerRendered: function(){
  250. if (this.onRender) { this.onRender(); }
  251. this.trigger("render", this);
  252. this.trigger("collection:rendered", this);
  253. },
  254. // Render the collection of items. Override this method to
  255. // provide your own implementation of a render function for
  256. // the collection view.
  257. render: function(){
  258. this.triggerBeforeRender();
  259. this.closeEmptyView();
  260. this.closeChildren();
  261. if (this.collection && this.collection.length > 0) {
  262. this.showCollection();
  263. } else {
  264. this.showEmptyView();
  265. }
  266. this.triggerRendered();
  267. return this;
  268. },
  269. // Internal method to loop through each item in the
  270. // collection view and show it
  271. showCollection: function(){
  272. var that = this;
  273. var ItemView = this.getItemView();
  274. this.collection.each(function(item, index){
  275. that.addItemView(item, ItemView, index);
  276. });
  277. },
  278. // Internal method to show an empty view in place of
  279. // a collection of item views, when the collection is
  280. // empty
  281. showEmptyView: function(){
  282. var EmptyView = this.options.emptyView || this.emptyView;
  283. if (EmptyView && !this._showingEmptyView){
  284. this._showingEmptyView = true;
  285. var model = new Backbone.Model();
  286. this.addItemView(model, EmptyView, 0);
  287. }
  288. },
  289. // Internal method to close an existing emptyView instance
  290. // if one exists. Called when a collection view has been
  291. // rendered empty, and then an item is added to the collection.
  292. closeEmptyView: function(){
  293. if (this._showingEmptyView){
  294. this.closeChildren();
  295. delete this._showingEmptyView;
  296. }
  297. },
  298. // Retrieve the itemView type, either from `this.options.itemView`
  299. // or from the `itemView` in the object definition. The "options"
  300. // takes precedence.
  301. getItemView: function(){
  302. var itemView = this.options.itemView || this.itemView;
  303. if (!itemView){
  304. var err = new Error("An `itemView` must be specified");
  305. err.name = "NoItemViewError";
  306. throw err;
  307. }
  308. return itemView;
  309. },
  310. // Render the child item's view and add it to the
  311. // HTML for the collection view.
  312. addItemView: function(item, ItemView, index){
  313. var that = this;
  314. var view = this.buildItemView(item, ItemView);
  315. // Store the child view itself so we can properly
  316. // remove and/or close it later
  317. this.storeChild(view);
  318. if (this.onItemAdded){ this.onItemAdded(view); }
  319. this.trigger("item:added", view);
  320. // Render it and show it
  321. var renderResult = this.renderItemView(view, index);
  322. // call onShow for child item views
  323. if (view.onShow){
  324. this.onShowCallbacks.add(view.onShow, view);
  325. }
  326. // Forward all child item view events through the parent,
  327. // prepending "itemview:" to the event name
  328. var childBinding = this.bindTo(view, "all", function(){
  329. var args = slice.call(arguments);
  330. args[0] = "itemview:" + args[0];
  331. args.splice(1, 0, view);
  332. that.trigger.apply(that, args);
  333. });
  334. // Store all child event bindings so we can unbind
  335. // them when removing / closing the child view
  336. this.childBindings = this.childBindings || {};
  337. this.childBindings[view.cid] = childBinding;
  338. return renderResult;
  339. },
  340. // render the item view
  341. renderItemView: function(view, index) {
  342. view.render();
  343. this.appendHtml(this, view, index);
  344. },
  345. // Build an `itemView` for every model in the collection.
  346. buildItemView: function(item, ItemView){
  347. var itemViewOptions = _.result(this, "itemViewOptions");
  348. var options = _.extend({model: item}, itemViewOptions);
  349. var view = new ItemView(options);
  350. return view;
  351. },
  352. // Remove the child view and close it
  353. removeItemView: function(item){
  354. var view = this.children[item.cid];
  355. if (view){
  356. var childBinding = this.childBindings[view.cid];
  357. if (childBinding) {
  358. this.unbindFrom(childBinding);
  359. delete this.childBindings[view.cid];
  360. }
  361. view.close();
  362. delete this.children[item.cid];
  363. }
  364. if (!this.collection || this.collection.length === 0){
  365. this.showEmptyView();
  366. }
  367. this.trigger("item:removed", view);
  368. },
  369. // Append the HTML to the collection's `el`.
  370. // Override this method to do something other
  371. // then `.append`.
  372. appendHtml: function(collectionView, itemView, index){
  373. collectionView.$el.append(itemView.el);
  374. },
  375. // Store references to all of the child `itemView`
  376. // instances so they can be managed and cleaned up, later.
  377. storeChild: function(view){
  378. this.children[view.model.cid] = view;
  379. },
  380. // Internal method to set up the `children` object for
  381. // storing all of the child views
  382. initChildViewStorage: function(){
  383. this.children = {};
  384. },
  385. // Handle cleanup and other closing needs for
  386. // the collection of views.
  387. close: function(){
  388. this.trigger("collection:before:close");
  389. this.closeChildren();
  390. Marionette.View.prototype.close.apply(this, arguments);
  391. this.trigger("collection:closed");
  392. },
  393. // Close the child views that this collection view
  394. // is holding on to, if any
  395. closeChildren: function(){
  396. var that = this;
  397. if (this.children){
  398. _.each(_.clone(this.children), function(childView){
  399. that.removeItemView(childView.model);
  400. });
  401. }
  402. }
  403. });
  404. // Composite View
  405. // --------------
  406. // Used for rendering a branch-leaf, hierarchical structure.
  407. // Extends directly from CollectionView and also renders an
  408. // an item view as `modelView`, for the top leaf
  409. Marionette.CompositeView = Marionette.CollectionView.extend({
  410. constructor: function(options){
  411. Marionette.CollectionView.apply(this, arguments);
  412. this.itemView = this.getItemView();
  413. },
  414. // Configured the initial events that the composite view
  415. // binds to. Override this method to prevent the initial
  416. // events, or to add your own initial events.
  417. initialEvents: function(){
  418. if (this.collection){
  419. this.bindTo(this.collection, "add", this.addChildView, this);
  420. this.bindTo(this.collection, "remove", this.removeItemView, this);
  421. this.bindTo(this.collection, "reset", this.renderCollection, this);
  422. }
  423. },
  424. // Retrieve the `itemView` to be used when rendering each of
  425. // the items in the collection. The default is to return
  426. // `this.itemView` or Marionette.CompositeView if no `itemView`
  427. // has been defined
  428. getItemView: function(){
  429. return this.itemView || this.constructor;
  430. },
  431. // Renders the model once, and the collection once. Calling
  432. // this again will tell the model's view to re-render itself
  433. // but the collection will not re-render.
  434. render: function(){
  435. var that = this;
  436. this.resetItemViewContainer();
  437. var html = this.renderModel();
  438. this.$el.html(html);
  439. // the ui bindings is done here and not at the end of render since they should be
  440. // available before the collection is rendered.
  441. this.bindUIElements();
  442. this.trigger("composite:model:rendered");
  443. this.trigger("render");
  444. this.renderCollection();
  445. this.trigger("composite:rendered");
  446. return this;
  447. },
  448. // Render the collection for the composite view
  449. renderCollection: function(){
  450. Marionette.CollectionView.prototype.render.apply(this, arguments);
  451. this.trigger("composite:collection:rendered");
  452. },
  453. // Render an individual model, if we have one, as
  454. // part of a composite view (branch / leaf). For example:
  455. // a treeview.
  456. renderModel: function(){
  457. var data = {};
  458. data = this.serializeData();
  459. var template = this.getTemplate();
  460. return Marionette.Renderer.render(template, data);
  461. },
  462. // Appends the `el` of itemView instances to the specified
  463. // `itemViewContainer` (a jQuery selector). Override this method to
  464. // provide custom logic of how the child item view instances have their
  465. // HTML appended to the composite view instance.
  466. appendHtml: function(cv, iv){
  467. var $container = this.getItemViewContainer(cv);
  468. $container.append(iv.el);
  469. },
  470. // Internal method to ensure an `$itemViewContainer` exists, for the
  471. // `appendHtml` method to use.
  472. getItemViewContainer: function(containerView){
  473. var container;
  474. if ("$itemViewContainer" in containerView){
  475. container = containerView.$itemViewContainer;
  476. } else {
  477. if (containerView.itemViewContainer){
  478. container = containerView.$(_.result(containerView, "itemViewContainer"));
  479. if (container.length <= 0) {
  480. var err = new Error("Missing `itemViewContainer`");
  481. err.name = "ItemViewContainerMissingError";
  482. throw err;
  483. }
  484. } else {
  485. container = containerView.$el;
  486. }
  487. containerView.$itemViewContainer = container;
  488. }
  489. return container;
  490. },
  491. // Internal method to reset the `$itemViewContainer` on render
  492. resetItemViewContainer: function(){
  493. if (this.$itemViewContainer){
  494. delete this.$itemViewContainer;
  495. }
  496. }
  497. });
  498. // Region
  499. // ------
  500. // Manage the visual regions of your composite application. See
  501. // http://lostechies.com/derickbailey/2011/12/12/composite-js-apps-regions-and-region-managers/
  502. Marionette.Region = function(options){
  503. this.options = options || {};
  504. var eventBinder = new Marionette.EventBinder();
  505. _.extend(this, eventBinder, options);
  506. if (!this.el){
  507. var err = new Error("An 'el' must be specified");
  508. err.name = "NoElError";
  509. throw err;
  510. }
  511. if (this.initialize){
  512. this.initialize.apply(this, arguments);
  513. }
  514. };
  515. _.extend(Marionette.Region.prototype, Backbone.Events, {
  516. // Displays a backbone view instance inside of the region.
  517. // Handles calling the `render` method for you. Reads content
  518. // directly from the `el` attribute. Also calls an optional
  519. // `onShow` and `close` method on your view, just after showing
  520. // or just before closing the view, respectively.
  521. show: function(view){
  522. this.ensureEl();
  523. this.close();
  524. view.render();
  525. this.open(view);
  526. if (view.onShow) { view.onShow(); }
  527. view.trigger("show");
  528. if (this.onShow) { this.onShow(view); }
  529. this.trigger("view:show", view);
  530. this.currentView = view;
  531. },
  532. ensureEl: function(){
  533. if (!this.$el || this.$el.length === 0){
  534. this.$el = this.getEl(this.el);
  535. }
  536. },
  537. // Override this method to change how the region finds the
  538. // DOM element that it manages. Return a jQuery selector object.
  539. getEl: function(selector){
  540. return $(selector);
  541. },
  542. // Override this method to change how the new view is
  543. // appended to the `$el` that the region is managing
  544. open: function(view){
  545. this.$el.html(view.el);
  546. },
  547. // Close the current view, if there is one. If there is no
  548. // current view, it does nothing and returns immediately.
  549. close: function(){
  550. var view = this.currentView;
  551. if (!view){ return; }
  552. if (view.close) { view.close(); }
  553. this.trigger("view:closed", view);
  554. delete this.currentView;
  555. },
  556. // Attach an existing view to the region. This
  557. // will not call `render` or `onShow` for the new view,
  558. // and will not replace the current HTML for the `el`
  559. // of the region.
  560. attachView: function(view){
  561. this.currentView = view;
  562. },
  563. // Reset the region by closing any existing view and
  564. // clearing out the cached `$el`. The next time a view
  565. // is shown via this region, the region will re-query the
  566. // DOM for the region's `el`.
  567. reset: function(){
  568. this.close();
  569. delete this.$el;
  570. }
  571. });
  572. // Copy the `extend` function used by Backbone's classes
  573. Marionette.Region.extend = Backbone.View.extend;
  574. // Layout
  575. // ------
  576. // Used for managing application layouts, nested layouts and
  577. // multiple regions within an application or sub-application.
  578. //
  579. // A specialized view type that renders an area of HTML and then
  580. // attaches `Region` instances to the specified `regions`.
  581. // Used for composite view management and sub-application areas.
  582. Marionette.Layout = Marionette.ItemView.extend({
  583. regionType: Marionette.Region,
  584. constructor: function () {
  585. Backbone.Marionette.ItemView.apply(this, arguments);
  586. this.initializeRegions();
  587. },
  588. // Layout's render will use the existing region objects the
  589. // first time it is called. Subsequent calls will close the
  590. // views that the regions are showing and then reset the `el`
  591. // for the regions to the newly rendered DOM elements.
  592. render: function(){
  593. var result = Marionette.ItemView.prototype.render.apply(this, arguments);
  594. // Rewrite this function to handle re-rendering and
  595. // re-initializing the `el` for each region
  596. this.render = function(){
  597. this.closeRegions();
  598. this.reInitializeRegions();
  599. var result = Marionette.ItemView.prototype.render.apply(this, arguments);
  600. return result;
  601. };
  602. return result;
  603. },
  604. // Handle closing regions, and then close the view itself.
  605. close: function () {
  606. this.closeRegions();
  607. this.destroyRegions();
  608. Backbone.Marionette.ItemView.prototype.close.call(this, arguments);
  609. },
  610. // Initialize the regions that have been defined in a
  611. // `regions` attribute on this layout. The key of the
  612. // hash becomes an attribute on the layout object directly.
  613. // For example: `regions: { menu: ".menu-container" }`
  614. // will product a `layout.menu` object which is a region
  615. // that controls the `.menu-container` DOM element.
  616. initializeRegions: function () {
  617. if (!this.regionManagers){
  618. this.regionManagers = {};
  619. }
  620. var that = this;
  621. _.each(this.regions, function (region, name) {
  622. var regionIsString = (typeof region === "string");
  623. var regionSelectorIsString = (typeof region.selector === "string");
  624. var regionTypeIsUndefined = (typeof region.regionType === "undefined");
  625. if (!regionIsString && !regionSelectorIsString) {
  626. throw new Error("Region must be specified as a selector string or an object with selector property");
  627. }
  628. var selector, RegionType;
  629. if (regionIsString) {
  630. selector = region;
  631. } else {
  632. selector = region.selector;
  633. }
  634. if (regionTypeIsUndefined){
  635. RegionType = that.regionType;
  636. } else {
  637. RegionType = region.regionType;
  638. }
  639. var regionManager = new RegionType({
  640. el: selector,
  641. getEl: function(selector){
  642. return that.$(selector);
  643. }
  644. });
  645. that.regionManagers[name] = regionManager;
  646. that[name] = regionManager;
  647. });
  648. },
  649. // Re-initialize all of the regions by updating the `el` that
  650. // they point to
  651. reInitializeRegions: function(){
  652. if (this.regionManagers && _.size(this.regionManagers)===0){
  653. this.initializeRegions();
  654. } else {
  655. _.each(this.regionManagers, function(region){
  656. region.reset();
  657. });
  658. }
  659. },
  660. // Close all of the regions that have been opened by
  661. // this layout. This method is called when the layout
  662. // itself is closed.
  663. closeRegions: function () {
  664. var that = this;
  665. _.each(this.regionManagers, function (manager, name) {
  666. manager.close();
  667. });
  668. },
  669. // Destroys all of the regions by removing references
  670. // from the Layout
  671. destroyRegions: function(){
  672. var that = this;
  673. _.each(this.regionManagers, function (manager, name) {
  674. delete that[name];
  675. });
  676. this.regionManagers = {};
  677. }
  678. });
  679. // Application
  680. // -----------
  681. // Contain and manage the composite application as a whole.
  682. // Stores and starts up `Region` objects, includes an
  683. // event aggregator as `app.vent`
  684. Marionette.Application = function(options){
  685. this.initCallbacks = new Marionette.Callbacks();
  686. this.vent = new Marionette.EventAggregator();
  687. this.submodules = {};
  688. var eventBinder = new Marionette.EventBinder();
  689. _.extend(this, eventBinder, options);
  690. };
  691. _.extend(Marionette.Application.prototype, Backbone.Events, {
  692. // Add an initializer that is either run at when the `start`
  693. // method is called, or run immediately if added after `start`
  694. // has already been called.
  695. addInitializer: function(initializer){
  696. this.initCallbacks.add(initializer);
  697. },
  698. // kick off all of the application's processes.
  699. // initializes all of the regions that have been added
  700. // to the app, and runs all of the initializer functions
  701. start: function(options){
  702. this.trigger("initialize:before", options);
  703. this.initCallbacks.run(options, this);
  704. this.trigger("initialize:after", options);
  705. this.trigger("start", options);
  706. },
  707. // Add regions to your app.
  708. // Accepts a hash of named strings or Region objects
  709. // addRegions({something: "#someRegion"})
  710. // addRegions{{something: Region.extend({el: "#someRegion"}) });
  711. addRegions: function(regions){
  712. var RegionValue, regionObj, region;
  713. for(region in regions){
  714. if (regions.hasOwnProperty(region)){
  715. RegionValue = regions[region];
  716. if (typeof RegionValue === "string"){
  717. regionObj = new Marionette.Region({
  718. el: RegionValue
  719. });
  720. } else {
  721. regionObj = new RegionValue();
  722. }
  723. this[region] = regionObj;
  724. }
  725. }
  726. },
  727. // Removes a region from your app.
  728. // Accepts the regions name
  729. // removeRegion('myRegion')
  730. removeRegion: function(region) {
  731. this[region].close();
  732. delete this[region];
  733. },
  734. // Create a module, attached to the application
  735. module: function(moduleNames, moduleDefinition){
  736. // slice the args, and add this application object as the
  737. // first argument of the array
  738. var args = slice.call(arguments);
  739. args.unshift(this);
  740. // see the Marionette.Module object for more information
  741. return Marionette.Module.create.apply(Marionette.Module, args);
  742. }
  743. });
  744. // Copy the `extend` function used by Backbone's classes
  745. Marionette.Application.extend = Backbone.View.extend;
  746. // AppRouter
  747. // ---------
  748. // Reduce the boilerplate code of handling route events
  749. // and then calling a single method on another object.
  750. // Have your routers configured to call the method on
  751. // your object, directly.
  752. //
  753. // Configure an AppRouter with `appRoutes`.
  754. //
  755. // App routers can only take one `controller` object.
  756. // It is recommended that you divide your controller
  757. // objects in to smaller peices of related functionality
  758. // and have multiple routers / controllers, instead of
  759. // just one giant router and controller.
  760. //
  761. // You can also add standard routes to an AppRouter.
  762. Marionette.AppRouter = Backbone.Router.extend({
  763. constructor: function(options){
  764. Backbone.Router.prototype.constructor.call(this, options);
  765. if (this.appRoutes){
  766. var controller = this.controller;
  767. if (options && options.controller) {
  768. controller = options.controller;
  769. }
  770. this.processAppRoutes(controller, this.appRoutes);
  771. }
  772. },
  773. // Internal method to process the `appRoutes` for the
  774. // router, and turn them in to routes that trigger the
  775. // specified method on the specified `controller`.
  776. processAppRoutes: function(controller, appRoutes){
  777. var method, methodName;
  778. var route, routesLength, i;
  779. var routes = [];
  780. var router = this;
  781. for(route in appRoutes){
  782. if (appRoutes.hasOwnProperty(route)){
  783. routes.unshift([route, appRoutes[route]]);
  784. }
  785. }
  786. routesLength = routes.length;
  787. for (i = 0; i < routesLength; i++){
  788. route = routes[i][0];
  789. methodName = routes[i][1];
  790. method = controller[methodName];
  791. if (!method){
  792. var msg = "Method '" + methodName + "' was not found on the controller";
  793. var err = new Error(msg);
  794. err.name = "NoMethodError";
  795. throw err;
  796. }
  797. method = _.bind(method, controller);
  798. router.route(route, methodName, method);
  799. }
  800. }
  801. });
  802. // Module
  803. // ------
  804. // A simple module system, used to create privacy and encapsulation in
  805. // Marionette applications
  806. Marionette.Module = function(moduleName, app, customArgs){
  807. this.moduleName = moduleName;
  808. // store sub-modules
  809. this.submodules = {};
  810. this._setupInitializersAndFinalizers();
  811. // store the configuration for this module
  812. this._config = {};
  813. this._config.app = app;
  814. this._config.customArgs = customArgs;
  815. this._config.definitions = [];
  816. // extend this module with an event binder
  817. var eventBinder = new Marionette.EventBinder();
  818. _.extend(this, eventBinder);
  819. };
  820. // Extend the Module prototype with events / bindTo, so that the module
  821. // can be used as an event aggregator or pub/sub.
  822. _.extend(Marionette.Module.prototype, Backbone.Events, {
  823. // Initializer for a specific module. Initializers are run when the
  824. // module's `start` method is called.
  825. addInitializer: function(callback){
  826. this._initializerCallbacks.add(callback);
  827. },
  828. // Finalizers are run when a module is stopped. They are used to teardown
  829. // and finalize any variables, references, events and other code that the
  830. // module had set up.
  831. addFinalizer: function(callback){
  832. this._finalizerCallbacks.add(callback);
  833. },
  834. // Start the module, and run all of it's initializers
  835. start: function(options){
  836. // Prevent re-start the module
  837. if (this._isInitialized){ return; }
  838. this._runModuleDefinition();
  839. this._initializerCallbacks.run(options, this);
  840. this._isInitialized = true;
  841. // start the sub-modules
  842. if (this.submodules){
  843. _.each(this.submodules, function(mod){
  844. mod.start(options);
  845. });
  846. }
  847. },
  848. // Stop this module by running its finalizers and then stop all of
  849. // the sub-modules for this module
  850. stop: function(){
  851. // if we are not initialized, don't bother finalizing
  852. if (!this._isInitialized){ return; }
  853. this._isInitialized = false;
  854. // run the finalizers
  855. this._finalizerCallbacks.run();
  856. // then reset the initializers and finalizers
  857. this._setupInitializersAndFinalizers();
  858. // stop the sub-modules
  859. _.each(this.submodules, function(mod){ mod.stop(); });
  860. },
  861. // Configure the module with a definition function and any custom args
  862. // that are to be passed in to the definition function
  863. addDefinition: function(moduleDefinition){
  864. this._config.definitions.push(moduleDefinition);
  865. },
  866. // Internal method: run the module definition function with the correct
  867. // arguments
  868. _runModuleDefinition: function(){
  869. if (this._config.definitions.length === 0) { return; }
  870. // build the correct list of arguments for the module definition
  871. var args = _.flatten([
  872. this,
  873. this._config.app,
  874. Backbone,
  875. Marionette,
  876. $, _,
  877. this._config.customArgs
  878. ]);
  879. // run the module definition function with the correct args
  880. var definitionCount = this._config.definitions.length-1;
  881. for(var i=0; i <= definitionCount; i++){
  882. var definition = this._config.definitions[i];
  883. definition.apply(this, args);
  884. }
  885. },
  886. // Internal method: set up new copies of initializers and finalizers.
  887. // Calling this method will wipe out all existing initializers and
  888. // finalizers.
  889. _setupInitializersAndFinalizers: function(){
  890. this._initializerCallbacks = new Marionette.Callbacks();
  891. this._finalizerCallbacks = new Marionette.Callbacks();
  892. }
  893. });
  894. // Function level methods to create modules
  895. _.extend(Marionette.Module, {
  896. // Create a module, hanging off the app parameter as the parent object.
  897. create: function(app, moduleNames, moduleDefinition){
  898. var that = this;
  899. var parentModule = app;
  900. moduleNames = moduleNames.split(".");
  901. // get the custom args passed in after the module definition and
  902. // get rid of the module name and definition function
  903. var customArgs = slice.apply(arguments);
  904. customArgs.splice(0, 3);
  905. // Loop through all the parts of the module definition
  906. var length = moduleNames.length;
  907. _.each(moduleNames, function(moduleName, i){
  908. var isLastModuleInChain = (i === length-1);
  909. // Get an existing module of this name if we have one
  910. var module = parentModule[moduleName];
  911. if (!module){
  912. // Create a new module if we don't have one
  913. module = new Marionette.Module(moduleName, app, customArgs);
  914. parentModule[moduleName] = module;
  915. // store the module on the parent
  916. parentModule.submodules[moduleName] = module;
  917. }
  918. // Only add a module definition and initializer when this is
  919. // the last module in a "parent.child.grandchild" hierarchy of
  920. // module names
  921. if (isLastModuleInChain ){
  922. that._createModuleDefinition(module, moduleDefinition, app);
  923. }
  924. // Reset the parent module so that the next child
  925. // in the list will be added to the correct parent
  926. parentModule = module;
  927. });
  928. // Return the last module in the definition chain
  929. return parentModule;
  930. },
  931. _createModuleDefinition: function(module, moduleDefinition, app){
  932. var moduleOptions = this._getModuleDefinitionOptions(moduleDefinition);
  933. // add the module definition
  934. if (moduleOptions.definition){
  935. module.addDefinition(moduleOptions.definition);
  936. }
  937. if (moduleOptions.startWithApp){
  938. // start the module when the app starts
  939. app.addInitializer(function(options){
  940. module.start(options);
  941. });
  942. }
  943. },
  944. _getModuleDefinitionOptions: function(moduleDefinition){
  945. // default to starting the module with the app
  946. var options = { startWithApp: true };
  947. // short circuit if we don't have a module definition
  948. if (!moduleDefinition){ return options; }
  949. if (_.isFunction(moduleDefinition)){
  950. // if the definition is a function, assign it directly
  951. // and use the defaults
  952. options.definition = moduleDefinition;
  953. } else {
  954. // the definition is an object. grab the "define" attribute
  955. // and the "startWithApp" attribute, as set the options
  956. // appropriately
  957. options.definition = moduleDefinition.define;
  958. if (moduleDefinition.hasOwnProperty("startWithApp")){
  959. options.startWithApp = moduleDefinition.startWithApp;
  960. }
  961. }
  962. return options;
  963. }
  964. });
  965. // Template Cache
  966. // --------------
  967. // Manage templates stored in `<script>` blocks,
  968. // caching them for faster access.
  969. Marionette.TemplateCache = function(templateId){
  970. this.templateId = templateId;
  971. };
  972. // TemplateCache object-level methods. Manage the template
  973. // caches from these method calls instead of creating
  974. // your own TemplateCache instances
  975. _.extend(Marionette.TemplateCache, {
  976. templateCaches: {},
  977. // Get the specified template by id. Either
  978. // retrieves the cached version, or loads it
  979. // from the DOM.
  980. get: function(templateId){
  981. var that = this;
  982. var cachedTemplate = this.templateCaches[templateId];
  983. if (!cachedTemplate){
  984. cachedTemplate = new Marionette.TemplateCache(templateId);
  985. this.templateCaches[templateId] = cachedTemplate;
  986. }
  987. return cachedTemplate.load();
  988. },
  989. // Clear templates from the cache. If no arguments
  990. // are specified, clears all templates:
  991. // `clear()`
  992. //
  993. // If arguments are specified, clears each of the
  994. // specified templates from the cache:
  995. // `clear("#t1", "#t2", "...")`
  996. clear: function(){
  997. var i;
  998. var length = arguments.length;
  999. if (length > 0){
  1000. for(i=0; i<length; i++){
  1001. delete this.templateCaches[arguments[i]];
  1002. }
  1003. } else {
  1004. this.templateCaches = {};
  1005. }
  1006. }
  1007. });
  1008. // TemplateCache instance methods, allowing each
  1009. // template cache object to manage it's own state
  1010. // and know whether or not it has been loaded
  1011. _.extend(Marionette.TemplateCache.prototype, {
  1012. // Internal method to load the template asynchronously.
  1013. load: function(){
  1014. var that = this;
  1015. // Guard clause to prevent loading this template more than once
  1016. if (this.compiledTemplate){
  1017. return this.compiledTemplate;
  1018. }
  1019. // Load the template and compile it
  1020. var template = this.loadTemplate(this.templateId);
  1021. this.compiledTemplate = this.compileTemplate(template);
  1022. return this.compiledTemplate;
  1023. },
  1024. // Load a template from the DOM, by default. Override
  1025. // this method to provide your own template retrieval,
  1026. // such as asynchronous loading from a server.
  1027. loadTemplate: function(templateId){
  1028. var template = $(templateId).html();
  1029. if (!template || template.length === 0){
  1030. var msg = "Could not find template: '" + templateId + "'";
  1031. var err = new Error(msg);
  1032. err.name = "NoTemplateError";
  1033. throw err;
  1034. }
  1035. return template;
  1036. },
  1037. // Pre-compile the template before caching it. Override
  1038. // this method if you do not need to pre-compile a template
  1039. // (JST / RequireJS for example) or if you want to change
  1040. // the template engine used (Handebars, etc).
  1041. compileTemplate: function(rawTemplate){
  1042. return _.template(rawTemplate);
  1043. }
  1044. });
  1045. // Renderer
  1046. // --------
  1047. // Render a template with data by passing in the template
  1048. // selector and the data to render.
  1049. Marionette.Renderer = {
  1050. // Render a template with data. The `template` parameter is
  1051. // passed to the `TemplateCache` object to retrieve the
  1052. // template function. Override this method to provide your own
  1053. // custom rendering and template handling for all of Marionette.
  1054. render: function(template, data){
  1055. var templateFunc = typeof template === 'function' ? template : Marionette.TemplateCache.get(template);
  1056. var html = templateFunc(data);
  1057. return html;
  1058. }
  1059. };
  1060. // Callbacks
  1061. // ---------
  1062. // A simple way of managing a collection of callbacks
  1063. // and executing them at a later point in time, using jQuery's
  1064. // `Deferred` object.
  1065. Marionette.Callbacks = function(){
  1066. this.deferred = $.Deferred();
  1067. this.promise = this.deferred.promise();
  1068. };
  1069. _.extend(Marionette.Callbacks.prototype, {
  1070. // Add a callback to be executed. Callbacks added here are
  1071. // guaranteed to execute, even if they are added after the
  1072. // `run` method is called.
  1073. add: function(callback, contextOverride){
  1074. this.promise.done(function(context, options){
  1075. if (contextOverride){ context = contextOverride; }
  1076. callback.call(context, options);
  1077. });
  1078. },
  1079. // Run all registered callbacks with the context specified.
  1080. // Additional callbacks can be added after this has been run
  1081. // and they will still be executed.
  1082. run: function(options, context){
  1083. this.deferred.resolve(context, options);
  1084. }
  1085. });
  1086. // Event Aggregator
  1087. // ----------------
  1088. // A pub-sub object that can be used to decouple various parts
  1089. // of an application through event-driven architecture.
  1090. Marionette.EventAggregator = Marionette.EventBinder.extend({
  1091. // Extend any provided options directly on to the event binder
  1092. constructor: function(options){
  1093. Marionette.EventBinder.apply(this, arguments);
  1094. _.extend(this, options);
  1095. },
  1096. // Override the `bindTo` method to ensure that the event aggregator
  1097. // is used as the event binding storage
  1098. bindTo: function(eventName, callback, context){
  1099. return Marionette.EventBinder.prototype.bindTo.call(this, this, eventName, callback, context);
  1100. }
  1101. });
  1102. // Copy the basic Backbone.Events on to the event aggregator
  1103. _.extend(Marionette.EventAggregator.prototype, Backbone.Events);
  1104. // Copy the `extend` function used by Backbone's classes
  1105. Marionette.EventAggregator.extend = Backbone.View.extend;
  1106. // Helpers
  1107. // -------
  1108. // For slicing `arguments` in functions
  1109. var slice = Array.prototype.slice;
  1110. return Marionette;
  1111. })(Backbone, _, window.jQuery || window.Zepto || window.ender);