marionette-rails 2.3.2 → 2.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: e6785ca4329e92cfb675a34ca251fd978c57e1e3
4
- data.tar.gz: 0627a47da1f8b06cd8bb2316026ffcaaafcee58a
3
+ metadata.gz: 784f3ed30d496806b0e0eef14a7cf643152dbd24
4
+ data.tar.gz: 90e2455a564f630445b624cc51081fb35b143e07
5
5
  SHA512:
6
- metadata.gz: 1a63dbf909602bd5a283f5a4d622a9bfac2afa98aa4a78061216bfd14db9bafb4bbd267c75f8b2d29143ec6c83f8cedcdf5ca0431f235862487f0e55fae20628
7
- data.tar.gz: 9a0560029ac2986b4a181ddc08cdca6043dbfed7864b518f5b53aca3d75d506058895cd5e723a4a37319ef688bf40e0b6954dd80b43fa2712479328e8ac6f069
6
+ metadata.gz: f6104cdf3edb687da3d167ea067fabe02a0bcdb67c9faff9ca917b8bc32b62311047d66db12e6b992dfa3cfa4bbaa2a3e7a40f4181c316c1fdb334bd957866db
7
+ data.tar.gz: 229f8eead03011880e83ccacd692c0be10da7df8265b92c78582af0aab09d819d0e31382bc53da8d0b38339f53333a4d401343958e12c51789bec7001c51b354
@@ -1,5 +1,5 @@
1
1
  module Marionette
2
2
  module Rails
3
- VERSION = '2.3.2'
3
+ VERSION = '2.4.0'
4
4
  end
5
5
  end
@@ -1,6 +1,6 @@
1
1
  // MarionetteJS (Backbone.Marionette)
2
2
  // ----------------------------------
3
- // v2.3.2
3
+ // v2.4.0
4
4
  //
5
5
  // Copyright (c)2015 Derick Bailey, Muted Solutions, LLC.
6
6
  // Distributed under MIT license
@@ -38,9 +38,9 @@
38
38
  /* istanbul ignore next */
39
39
  // Backbone.BabySitter
40
40
  // -------------------
41
- // v0.1.5
41
+ // v0.1.6
42
42
  //
43
- // Copyright (c)2014 Derick Bailey, Muted Solutions, LLC.
43
+ // Copyright (c)2015 Derick Bailey, Muted Solutions, LLC.
44
44
  // Distributed under MIT license
45
45
  //
46
46
  // http://github.com/marionettejs/backbone.babysitter
@@ -156,7 +156,7 @@
156
156
  //
157
157
  // Mix in methods from Underscore, for iteration, and other
158
158
  // collection related features.
159
- var methods = [ "forEach", "each", "map", "find", "detect", "filter", "select", "reject", "every", "all", "some", "any", "include", "contains", "invoke", "toArray", "first", "initial", "rest", "last", "without", "isEmpty", "pluck" ];
159
+ var methods = [ "forEach", "each", "map", "find", "detect", "filter", "select", "reject", "every", "all", "some", "any", "include", "contains", "invoke", "toArray", "first", "initial", "rest", "last", "without", "isEmpty", "pluck", "reduce" ];
160
160
  _.each(methods, function(method) {
161
161
  Container.prototype[method] = function() {
162
162
  var views = _.values(this._views);
@@ -167,7 +167,7 @@
167
167
  // return the public API
168
168
  return Container;
169
169
  }(Backbone, _);
170
- Backbone.ChildViewContainer.VERSION = "0.1.5";
170
+ Backbone.ChildViewContainer.VERSION = "0.1.6";
171
171
  Backbone.ChildViewContainer.noConflict = function() {
172
172
  Backbone.ChildViewContainer = previousChildViewContainer;
173
173
  return this;
@@ -490,13 +490,15 @@
490
490
  })(Backbone, _);
491
491
 
492
492
  var previousMarionette = root.Marionette;
493
+ var previousMn = root.Mn;
493
494
 
494
495
  var Marionette = Backbone.Marionette = {};
495
496
 
496
- Marionette.VERSION = '2.3.2';
497
+ Marionette.VERSION = '2.4.0';
497
498
 
498
499
  Marionette.noConflict = function() {
499
500
  root.Marionette = previousMarionette;
501
+ root.Mn = previousMn;
500
502
  return this;
501
503
  };
502
504
 
@@ -524,6 +526,11 @@
524
526
  return Backbone.$.contains(document.documentElement, el);
525
527
  };
526
528
 
529
+ // Merge `keys` from `options` onto `this`
530
+ Marionette.mergeOptions = function(options, keys) {
531
+ if (!options) { return; }
532
+ _.extend(this, _.pick(options, keys));
533
+ };
527
534
 
528
535
  // Marionette.getOption
529
536
  // --------------------
@@ -550,11 +557,7 @@
550
557
  // undefined return a default value
551
558
  Marionette._getValue = function(value, context, params) {
552
559
  if (_.isFunction(value)) {
553
- // We need to ensure that params is not undefined
554
- // to prevent `apply` from failing in ie8
555
- params = params || [];
556
-
557
- value = value.apply(context, params);
560
+ value = params ? value.apply(context, params) : value.call(context);
558
561
  }
559
562
  return value;
560
563
  };
@@ -599,11 +602,21 @@
599
602
  // allows for the use of the @ui. syntax within
600
603
  // a given value for regions
601
604
  // swaps the @ui with the associated selector
602
- Marionette.normalizeUIValues = function(hash, ui) {
605
+ Marionette.normalizeUIValues = function(hash, ui, properties) {
603
606
  _.each(hash, function(val, key) {
604
607
  if (_.isString(val)) {
605
608
  hash[key] = Marionette.normalizeUIString(val, ui);
606
609
  }
610
+ else if (_.isObject(val) && _.isArray(properties)) {
611
+ _.extend(val, Marionette.normalizeUIValues(_.pick(val, properties), ui));
612
+ /* Value is an object, and we got an array of embedded property names to normalize. */
613
+ _.each(properties, function(property) {
614
+ var propertyVal = val[property];
615
+ if (_.isString(propertyVal)) {
616
+ val[property] = Marionette.normalizeUIString(propertyVal, ui);
617
+ }
618
+ });
619
+ }
607
620
  });
608
621
  return hash;
609
622
  };
@@ -682,7 +695,7 @@
682
695
  // trigger the event, if a trigger method exists
683
696
  if (_.isFunction(context.trigger)) {
684
697
  if (noEventArg + args.length > 1) {
685
- context.trigger.apply(context, noEventArg ? args : [event].concat(_.rest(args, 0)));
698
+ context.trigger.apply(context, noEventArg ? args : [event].concat(_.drop(args, 0)));
686
699
  } else {
687
700
  context.trigger(event);
688
701
  }
@@ -984,6 +997,9 @@
984
997
  // methods if the method exists
985
998
  triggerMethod: Marionette.triggerMethod,
986
999
 
1000
+ // A handy way to merge options onto the instance
1001
+ mergeOptions: Marionette.mergeOptions,
1002
+
987
1003
  // Proxy `getOption` to enable getting options from this or this.options by name.
988
1004
  getOption: Marionette.proxyGetOption
989
1005
 
@@ -1015,12 +1031,17 @@
1015
1031
  this.triggerMethod('before:destroy');
1016
1032
  this.triggerMethod('destroy');
1017
1033
  this.stopListening();
1034
+
1035
+ return this;
1018
1036
  },
1019
1037
 
1020
1038
  // Import the `triggerMethod` to trigger events with corresponding
1021
1039
  // methods if the method exists
1022
1040
  triggerMethod: Marionette.triggerMethod,
1023
1041
 
1042
+ // A handy way to merge options onto the instance
1043
+ mergeOptions: Marionette.mergeOptions,
1044
+
1024
1045
  // Proxy `getOption` to enable getting options from this or this.options by name.
1025
1046
  getOption: Marionette.proxyGetOption,
1026
1047
 
@@ -1235,20 +1256,28 @@
1235
1256
 
1236
1257
  // Destroy the current view, if there is one. If there is no
1237
1258
  // current view, it does nothing and returns immediately.
1238
- empty: function() {
1259
+ empty: function(options) {
1239
1260
  var view = this.currentView;
1240
1261
 
1262
+ var preventDestroy = Marionette._getValue(options, 'preventDestroy', this);
1241
1263
  // If there is no view in the region
1242
1264
  // we should not remove anything
1243
1265
  if (!view) { return; }
1244
1266
 
1245
1267
  view.off('destroy', this.empty, this);
1246
1268
  this.triggerMethod('before:empty', view);
1247
- this._destroyView();
1269
+ if (!preventDestroy) {
1270
+ this._destroyView();
1271
+ }
1248
1272
  this.triggerMethod('empty', view);
1249
1273
 
1250
1274
  // Remove region pointer to the currentView
1251
1275
  delete this.currentView;
1276
+
1277
+ if (preventDestroy) {
1278
+ this.$el.contents().detach();
1279
+ }
1280
+
1252
1281
  return this;
1253
1282
  },
1254
1283
 
@@ -1371,6 +1400,7 @@
1371
1400
  Marionette.RegionManager = Marionette.Controller.extend({
1372
1401
  constructor: function(options) {
1373
1402
  this._regions = {};
1403
+ this.length = 0;
1374
1404
 
1375
1405
  Marionette.Controller.call(this, options);
1376
1406
 
@@ -1464,8 +1494,11 @@
1464
1494
 
1465
1495
  // internal method to store regions
1466
1496
  _store: function(name, region) {
1497
+ if (!this._regions[name]) {
1498
+ this.length++;
1499
+ }
1500
+
1467
1501
  this._regions[name] = region;
1468
- this._setLength();
1469
1502
  },
1470
1503
 
1471
1504
  // internal method to remove a region
@@ -1476,13 +1509,8 @@
1476
1509
 
1477
1510
  delete region._parent;
1478
1511
  delete this._regions[name];
1479
- this._setLength();
1512
+ this.length--;
1480
1513
  this.triggerMethod('remove:region', name, region);
1481
- },
1482
-
1483
- // set the number of regions current held
1484
- _setLength: function() {
1485
- this.length = _.size(this._regions);
1486
1514
  }
1487
1515
  });
1488
1516
 
@@ -1507,7 +1535,7 @@
1507
1535
  // Get the specified template by id. Either
1508
1536
  // retrieves the cached version, or loads it
1509
1537
  // from the DOM.
1510
- get: function(templateId) {
1538
+ get: function(templateId, options) {
1511
1539
  var cachedTemplate = this.templateCaches[templateId];
1512
1540
 
1513
1541
  if (!cachedTemplate) {
@@ -1515,7 +1543,7 @@
1515
1543
  this.templateCaches[templateId] = cachedTemplate;
1516
1544
  }
1517
1545
 
1518
- return cachedTemplate.load();
1546
+ return cachedTemplate.load(options);
1519
1547
  },
1520
1548
 
1521
1549
  // Clear templates from the cache. If no arguments
@@ -1546,15 +1574,15 @@
1546
1574
  _.extend(Marionette.TemplateCache.prototype, {
1547
1575
 
1548
1576
  // Internal method to load the template
1549
- load: function() {
1577
+ load: function(options) {
1550
1578
  // Guard clause to prevent loading this template more than once
1551
1579
  if (this.compiledTemplate) {
1552
1580
  return this.compiledTemplate;
1553
1581
  }
1554
1582
 
1555
1583
  // Load the template and compile it
1556
- var template = this.loadTemplate(this.templateId);
1557
- this.compiledTemplate = this.compileTemplate(template);
1584
+ var template = this.loadTemplate(this.templateId, options);
1585
+ this.compiledTemplate = this.compileTemplate(template, options);
1558
1586
 
1559
1587
  return this.compiledTemplate;
1560
1588
  },
@@ -1564,7 +1592,7 @@
1564
1592
  // For asynchronous loading with AMD/RequireJS, consider
1565
1593
  // using a template-loader plugin as described here:
1566
1594
  // https://github.com/marionettejs/backbone.marionette/wiki/Using-marionette-with-requirejs
1567
- loadTemplate: function(templateId) {
1595
+ loadTemplate: function(templateId, options) {
1568
1596
  var template = Backbone.$(templateId).html();
1569
1597
 
1570
1598
  if (!template || template.length === 0) {
@@ -1581,8 +1609,8 @@
1581
1609
  // this method if you do not need to pre-compile a template
1582
1610
  // (JST / RequireJS for example) or if you want to change
1583
1611
  // the template engine used (Handebars, etc).
1584
- compileTemplate: function(rawTemplate) {
1585
- return _.template(rawTemplate);
1612
+ compileTemplate: function(rawTemplate, options) {
1613
+ return _.template(rawTemplate, options);
1586
1614
  }
1587
1615
  });
1588
1616
 
@@ -1633,10 +1661,9 @@
1633
1661
 
1634
1662
  this._behaviors = Marionette.Behaviors(this);
1635
1663
 
1636
- Backbone.View.apply(this, arguments);
1664
+ Backbone.View.call(this, this.options);
1637
1665
 
1638
1666
  Marionette.MonitorDOMRefresh(this);
1639
- this.on('show', this.onShowCalled);
1640
1667
  },
1641
1668
 
1642
1669
  // Get the template for this view
@@ -1674,10 +1701,10 @@
1674
1701
 
1675
1702
  // normalize the values of passed hash with the views `ui` selectors.
1676
1703
  // `{foo: "@ui.bar"}`
1677
- normalizeUIValues: function(hash) {
1704
+ normalizeUIValues: function(hash, properties) {
1678
1705
  var ui = _.result(this, 'ui');
1679
1706
  var uiBindings = _.result(this, '_uiBindings');
1680
- return Marionette.normalizeUIValues(hash, uiBindings || ui);
1707
+ return Marionette.normalizeUIValues(hash, uiBindings || ui, properties);
1681
1708
  },
1682
1709
 
1683
1710
  // Configure `triggers` to forward DOM events to view
@@ -1748,9 +1775,6 @@
1748
1775
  return this;
1749
1776
  },
1750
1777
 
1751
- // Internal method, handles the `show` event.
1752
- onShowCalled: function() {},
1753
-
1754
1778
  // Internal helper method to verify whether the view hasn't been destroyed
1755
1779
  _ensureViewIsIntact: function() {
1756
1780
  if (this.isDestroyed) {
@@ -1766,7 +1790,7 @@
1766
1790
  // for you. You can specify an `onDestroy` method in your view to
1767
1791
  // add custom code that is called after the view is destroyed.
1768
1792
  destroy: function() {
1769
- if (this.isDestroyed) { return; }
1793
+ if (this.isDestroyed) { return this; }
1770
1794
 
1771
1795
  var args = _.toArray(arguments);
1772
1796
 
@@ -1781,6 +1805,8 @@
1781
1805
  // unbind UI elements
1782
1806
  this.unbindUIElements();
1783
1807
 
1808
+ this.isRendered = false;
1809
+
1784
1810
  // remove the view from the DOM
1785
1811
  this.remove();
1786
1812
 
@@ -1887,15 +1913,42 @@
1887
1913
  // import the `triggerMethod` to trigger events with corresponding
1888
1914
  // methods if the method exists
1889
1915
  triggerMethod: function() {
1916
+ var ret = Marionette._triggerMethod(this, arguments);
1917
+
1918
+ this._triggerEventOnBehaviors(arguments);
1919
+ this._triggerEventOnParentLayout(arguments[0], _.rest(arguments));
1920
+
1921
+ return ret;
1922
+ },
1923
+
1924
+ _triggerEventOnBehaviors: function(args) {
1890
1925
  var triggerMethod = Marionette._triggerMethod;
1891
- var ret = triggerMethod(this, arguments);
1892
1926
  var behaviors = this._behaviors;
1893
1927
  // Use good ol' for as this is a very hot function
1894
1928
  for (var i = 0, length = behaviors && behaviors.length; i < length; i++) {
1895
- triggerMethod(behaviors[i], arguments);
1929
+ triggerMethod(behaviors[i], args);
1896
1930
  }
1931
+ },
1897
1932
 
1898
- return ret;
1933
+ _triggerEventOnParentLayout: function(eventName, args) {
1934
+ var layoutView = this._parentLayoutView();
1935
+ if (!layoutView) {
1936
+ return;
1937
+ }
1938
+
1939
+ // invoke triggerMethod on parent view
1940
+ var eventPrefix = Marionette.getOption(layoutView, 'childViewEventPrefix');
1941
+ var prefixedEventName = eventPrefix + ':' + eventName;
1942
+
1943
+ Marionette._triggerMethod(layoutView, [prefixedEventName, this].concat(args));
1944
+
1945
+ // call the parent view's childEvents handler
1946
+ var childEvents = Marionette.getOption(layoutView, 'childEvents');
1947
+ var normalizedChildEvents = layoutView.normalizeMethods(childEvents);
1948
+
1949
+ if (!!normalizedChildEvents && _.isFunction(normalizedChildEvents[eventName])) {
1950
+ normalizedChildEvents[eventName].apply(layoutView, [this].concat(args));
1951
+ }
1899
1952
  },
1900
1953
 
1901
1954
  // This method returns any views that are immediate
@@ -1916,10 +1969,35 @@
1916
1969
  }, children);
1917
1970
  },
1918
1971
 
1972
+ // Internal utility for building an ancestor
1973
+ // view tree list.
1974
+ _getAncestors: function() {
1975
+ var ancestors = [];
1976
+ var parent = this._parent;
1977
+
1978
+ while (parent) {
1979
+ ancestors.push(parent);
1980
+ parent = parent._parent;
1981
+ }
1982
+
1983
+ return ancestors;
1984
+ },
1985
+
1986
+ // Returns the containing parent view.
1987
+ _parentLayoutView: function() {
1988
+ var ancestors = this._getAncestors();
1989
+ return _.find(ancestors, function(parent) {
1990
+ return parent instanceof Marionette.LayoutView;
1991
+ });
1992
+ },
1993
+
1919
1994
  // Imports the "normalizeMethods" to transform hashes of
1920
1995
  // events=>function references/names to a hash of events=>function references
1921
1996
  normalizeMethods: Marionette.normalizeMethods,
1922
1997
 
1998
+ // A handy way to merge passed-in options onto the instance
1999
+ mergeOptions: Marionette.mergeOptions,
2000
+
1923
2001
  // Proxy `getOption` to enable getting options from this or this.options by name.
1924
2002
  getOption: Marionette.proxyGetOption,
1925
2003
 
@@ -1986,6 +2064,7 @@
1986
2064
  this.triggerMethod('before:render', this);
1987
2065
 
1988
2066
  this._renderTemplate();
2067
+ this.isRendered = true;
1989
2068
  this.bindUIElements();
1990
2069
 
1991
2070
  this.triggerMethod('render', this);
@@ -2013,8 +2092,7 @@
2013
2092
  }
2014
2093
 
2015
2094
  // Add in entity data and template helpers
2016
- var data = this.serializeData();
2017
- data = this.mixinTemplateHelpers(data);
2095
+ var data = this.mixinTemplateHelpers(this.serializeData());
2018
2096
 
2019
2097
  // Render and add to el
2020
2098
  var html = Marionette.Renderer.render(template, data, this);
@@ -2055,21 +2133,25 @@
2055
2133
  // that are forwarded through the collectionview
2056
2134
  childViewEventPrefix: 'childview',
2057
2135
 
2136
+ // flag for maintaining the sorted order of the collection
2137
+ sort: true,
2138
+
2058
2139
  // constructor
2059
2140
  // option to pass `{sort: false}` to prevent the `CollectionView` from
2060
2141
  // maintaining the sorted order of the collection.
2061
2142
  // This will fallback onto appending childView's to the end.
2143
+ //
2144
+ // option to pass `{comparator: compFunction()}` to allow the `CollectionView`
2145
+ // to use a custom sort order for the collection.
2062
2146
  constructor: function(options){
2063
- var initOptions = options || {};
2064
- if (_.isUndefined(this.sort)){
2065
- this.sort = _.isUndefined(initOptions.sort) ? true : initOptions.sort;
2066
- }
2067
2147
 
2068
2148
  this.once('render', this._initialEvents);
2069
2149
  this._initChildViewStorage();
2070
2150
 
2071
2151
  Marionette.View.apply(this, arguments);
2072
2152
 
2153
+ this.on('show', this._onShowCalled);
2154
+
2073
2155
  this.initRenderBuffer();
2074
2156
  },
2075
2157
 
@@ -2077,7 +2159,6 @@
2077
2159
  // it's much more performant to insert elements into a document
2078
2160
  // fragment and then insert that document fragment into the page
2079
2161
  initRenderBuffer: function() {
2080
- this.elBuffer = document.createDocumentFragment();
2081
2162
  this._bufferedChildren = [];
2082
2163
  },
2083
2164
 
@@ -2089,7 +2170,9 @@
2089
2170
  endBuffering: function() {
2090
2171
  this.isBuffering = false;
2091
2172
  this._triggerBeforeShowBufferedChildren();
2092
- this.attachBuffer(this, this.elBuffer);
2173
+
2174
+ this.attachBuffer(this);
2175
+
2093
2176
  this._triggerShowBufferedChildren();
2094
2177
  this.initRenderBuffer();
2095
2178
  },
@@ -2122,18 +2205,26 @@
2122
2205
  this.listenTo(this.collection, 'remove', this._onCollectionRemove);
2123
2206
  this.listenTo(this.collection, 'reset', this.render);
2124
2207
 
2125
- if (this.sort) {
2208
+ if (this.getOption('sort')) {
2126
2209
  this.listenTo(this.collection, 'sort', this._sortViews);
2127
2210
  }
2128
2211
  }
2129
2212
  },
2130
2213
 
2131
2214
  // Handle a child added to the collection
2132
- _onCollectionAdd: function(child) {
2133
- this.destroyEmptyView();
2134
- var ChildView = this.getChildView(child);
2135
- var index = this.collection.indexOf(child);
2136
- this.addChild(child, ChildView, index);
2215
+ _onCollectionAdd: function(child, collection, opts) {
2216
+ var index;
2217
+ if (opts.at !== undefined) {
2218
+ index = opts.at;
2219
+ } else {
2220
+ index = _.indexOf(this._filteredSortedModels(), child);
2221
+ }
2222
+
2223
+ if (this._shouldAddChild(child, index)) {
2224
+ this.destroyEmptyView();
2225
+ var ChildView = this.getChildView(child);
2226
+ this.addChild(child, ChildView, index);
2227
+ }
2137
2228
  },
2138
2229
 
2139
2230
  // get the child view by model it holds, and remove it
@@ -2143,8 +2234,7 @@
2143
2234
  this.checkEmpty();
2144
2235
  },
2145
2236
 
2146
- // Override from `Marionette.View` to trigger show on child views
2147
- onShowCalled: function() {
2237
+ _onShowCalled: function() {
2148
2238
  this.children.each(_.partial(this._triggerMethodOnChild, 'show'));
2149
2239
  },
2150
2240
 
@@ -2155,23 +2245,60 @@
2155
2245
  this._ensureViewIsIntact();
2156
2246
  this.triggerMethod('before:render', this);
2157
2247
  this._renderChildren();
2248
+ this.isRendered = true;
2158
2249
  this.triggerMethod('render', this);
2159
2250
  return this;
2160
2251
  },
2161
2252
 
2253
+ // Reorder DOM after sorting. When your element's rendering
2254
+ // do not use their index, you can pass reorderOnSort: true
2255
+ // to only reorder the DOM after a sort instead of rendering
2256
+ // all the collectionView
2257
+ reorder: function () {
2258
+ var children = this.children;
2259
+ var models = this._filteredSortedModels();
2260
+ var modelsChanged = _.find(models, function (model) {
2261
+ return !children.findByModel(model);
2262
+ });
2263
+
2264
+ // If the models we're displaying have changed due to filtering
2265
+ // We need to add and/or remove child views
2266
+ // So render as normal
2267
+ if (modelsChanged) {
2268
+ this.render();
2269
+ } else {
2270
+ // get the DOM nodes in the same order as the models
2271
+ var els = _.map(models, function (model) {
2272
+ return children.findByModel(model).el;
2273
+ });
2274
+
2275
+ // since append moves elements that are already in the DOM,
2276
+ // appending the elements will effectively reorder them
2277
+ this.triggerMethod('before:reorder');
2278
+ this.$el.append(els);
2279
+ this.triggerMethod('reorder');
2280
+ }
2281
+ },
2282
+
2162
2283
  // Render view after sorting. Override this method to
2163
2284
  // change how the view renders after a `sort` on the collection.
2164
2285
  // An example of this would be to only `renderChildren` in a `CompositeView`
2165
2286
  // rather than the full view.
2166
2287
  resortView: function() {
2167
- this.render();
2288
+ if (Marionette.getOption(this, 'reorderOnSort')) {
2289
+ this.reorder();
2290
+ } else {
2291
+ this.render();
2292
+ }
2168
2293
  },
2169
2294
 
2170
2295
  // Internal method. This checks for any changes in the order of the collection.
2171
2296
  // If the index of any view doesn't match, it will render.
2172
2297
  _sortViews: function() {
2298
+ var models = this._filteredSortedModels();
2299
+
2173
2300
  // check for any changes in sort order of views
2174
- var orderChanged = this.collection.find(function(item, index){
2301
+ var orderChanged = _.find(models, function(item, index){
2175
2302
  var view = this.children.findByModel(item);
2176
2303
  return !view || view._index !== index;
2177
2304
  }, this);
@@ -2199,18 +2326,51 @@
2199
2326
  this.showCollection();
2200
2327
  this.endBuffering();
2201
2328
  this.triggerMethod('render:collection', this);
2329
+
2330
+ // If we have shown children and none have passed the filter, show the empty view
2331
+ if (this.children.isEmpty()) {
2332
+ this.showEmptyView();
2333
+ }
2202
2334
  }
2203
2335
  },
2204
2336
 
2205
2337
  // Internal method to loop through collection and show each child view.
2206
2338
  showCollection: function() {
2207
2339
  var ChildView;
2208
- this.collection.each(function(child, index) {
2340
+
2341
+ var models = this._filteredSortedModels();
2342
+
2343
+ _.each(models, function(child, index) {
2209
2344
  ChildView = this.getChildView(child);
2210
2345
  this.addChild(child, ChildView, index);
2211
2346
  }, this);
2212
2347
  },
2213
2348
 
2349
+ // Allow the collection to be sorted by a custom view comparator
2350
+ _filteredSortedModels: function() {
2351
+ var models;
2352
+ var viewComparator = this.getViewComparator();
2353
+
2354
+ if (viewComparator) {
2355
+ if (_.isString(viewComparator) || viewComparator.length === 1) {
2356
+ models = this.collection.sortBy(viewComparator, this);
2357
+ } else {
2358
+ models = _.clone(this.collection.models).sort(_.bind(viewComparator, this));
2359
+ }
2360
+ } else {
2361
+ models = this.collection.models;
2362
+ }
2363
+
2364
+ // Filter after sorting in case the filter uses the index
2365
+ if (this.getOption('filter')) {
2366
+ models = _.filter(models, function (model, index) {
2367
+ return this._shouldAddChild(model, index);
2368
+ }, this);
2369
+ }
2370
+
2371
+ return models;
2372
+ },
2373
+
2214
2374
  // Internal method to show an empty view in place of
2215
2375
  // a collection of child views, when the collection is empty
2216
2376
  showEmptyView: function() {
@@ -2329,7 +2489,7 @@
2329
2489
  // Internal method. This decrements or increments the indices of views after the
2330
2490
  // added/removed view to keep in sync with the collection.
2331
2491
  _updateIndices: function(view, increment, index) {
2332
- if (!this.sort) {
2492
+ if (!this.getOption('sort')) {
2333
2493
  return;
2334
2494
  }
2335
2495
 
@@ -2355,6 +2515,12 @@
2355
2515
 
2356
2516
  this.triggerMethod('before:add:child', view);
2357
2517
 
2518
+ // trigger the 'before:show' event on `view` if the collection view
2519
+ // has already been shown
2520
+ if (this._isShown && !this.isBuffering) {
2521
+ Marionette.triggerMethodOn(view, 'before:show');
2522
+ }
2523
+
2358
2524
  // Store the child view itself so we can properly
2359
2525
  // remove and/or destroy it later
2360
2526
  this.children.add(view);
@@ -2417,8 +2583,17 @@
2417
2583
  },
2418
2584
 
2419
2585
  // You might need to override this if you've overridden attachHtml
2420
- attachBuffer: function(collectionView, buffer) {
2421
- collectionView.$el.append(buffer);
2586
+ attachBuffer: function(collectionView) {
2587
+ collectionView.$el.append(this._createBuffer(collectionView));
2588
+ },
2589
+
2590
+ // Create a fragment buffer from the currently buffered children
2591
+ _createBuffer: function(collectionView) {
2592
+ var elBuffer = document.createDocumentFragment();
2593
+ _.each(collectionView._bufferedChildren, function(b) {
2594
+ elBuffer.appendChild(b.el);
2595
+ });
2596
+ return elBuffer;
2422
2597
  },
2423
2598
 
2424
2599
  // Append the HTML to the collection's `el`.
@@ -2429,8 +2604,7 @@
2429
2604
  // buffering happens on reset events and initial renders
2430
2605
  // in order to reduce the number of inserts into the
2431
2606
  // document, which are expensive.
2432
- collectionView.elBuffer.appendChild(childView.el);
2433
- collectionView._bufferedChildren.push(childView);
2607
+ collectionView._bufferedChildren.splice(index, 0, childView);
2434
2608
  }
2435
2609
  else {
2436
2610
  // If we've already rendered the main collection, append
@@ -2446,7 +2620,7 @@
2446
2620
  // the correct position.
2447
2621
  _insertBefore: function(childView, index) {
2448
2622
  var currentView;
2449
- var findPosition = this.sort && (index < this.children.length - 1);
2623
+ var findPosition = this.getOption('sort') && (index < this.children.length - 1);
2450
2624
  if (findPosition) {
2451
2625
  // Find the view after this one
2452
2626
  currentView = this.children.find(function (view) {
@@ -2475,7 +2649,7 @@
2475
2649
 
2476
2650
  // Handle cleanup and other destroying needs for the collection of views
2477
2651
  destroy: function() {
2478
- if (this.isDestroyed) { return; }
2652
+ if (this.isDestroyed) { return this; }
2479
2653
 
2480
2654
  this.triggerMethod('before:destroy:collection');
2481
2655
  this.destroyChildren();
@@ -2493,6 +2667,18 @@
2493
2667
  return childViews;
2494
2668
  },
2495
2669
 
2670
+ // Return true if the given child should be shown
2671
+ // Return false otherwise
2672
+ // The filter will be passed (child, index, collection)
2673
+ // Where
2674
+ // 'child' is the given model
2675
+ // 'index' is the index of that model in the collection
2676
+ // 'collection' is the collection referenced by this CollectionView
2677
+ _shouldAddChild: function (child, index) {
2678
+ var filter = this.getOption('filter');
2679
+ return !_.isFunction(filter) || filter.call(this, child, index, this.collection);
2680
+ },
2681
+
2496
2682
  // Set up the child view event forwarding. Uses a "childview:"
2497
2683
  // prefix in front of all forwarded events.
2498
2684
  proxyChildEvents: function(view) {
@@ -2514,11 +2700,15 @@
2514
2700
  }
2515
2701
 
2516
2702
  this.triggerMethod.apply(this, args);
2517
- }, this);
2703
+ });
2518
2704
  },
2519
2705
 
2520
2706
  _getImmediateChildren: function() {
2521
2707
  return _.values(this.children._views);
2708
+ },
2709
+
2710
+ getViewComparator: function() {
2711
+ return this.getOption('viewComparator');
2522
2712
  }
2523
2713
  });
2524
2714
 
@@ -2554,7 +2744,7 @@
2554
2744
  this.listenTo(this.collection, 'remove', this._onCollectionRemove);
2555
2745
  this.listenTo(this.collection, 'reset', this._renderChildren);
2556
2746
 
2557
- if (this.sort) {
2747
+ if (this.getOption('sort')) {
2558
2748
  this.listenTo(this.collection, 'sort', this._sortViews);
2559
2749
  }
2560
2750
  }
@@ -2586,7 +2776,7 @@
2586
2776
  // Renders the model and the collection.
2587
2777
  render: function() {
2588
2778
  this._ensureViewIsIntact();
2589
- this.isRendered = true;
2779
+ this._isRendering = true;
2590
2780
  this.resetChildViewContainer();
2591
2781
 
2592
2782
  this.triggerMethod('before:render', this);
@@ -2594,12 +2784,14 @@
2594
2784
  this._renderTemplate();
2595
2785
  this._renderChildren();
2596
2786
 
2787
+ this._isRendering = false;
2788
+ this.isRendered = true;
2597
2789
  this.triggerMethod('render', this);
2598
2790
  return this;
2599
2791
  },
2600
2792
 
2601
2793
  _renderChildren: function() {
2602
- if (this.isRendered) {
2794
+ if (this.isRendered || this._isRendering) {
2603
2795
  Marionette.CollectionView.prototype._renderChildren.call(this);
2604
2796
  }
2605
2797
  },
@@ -2643,9 +2835,9 @@
2643
2835
  },
2644
2836
 
2645
2837
  // You might need to override this if you've overridden attachHtml
2646
- attachBuffer: function(compositeView, buffer) {
2838
+ attachBuffer: function(compositeView) {
2647
2839
  var $container = this.getChildViewContainer(compositeView);
2648
- $container.append(buffer);
2840
+ $container.append(this._createBuffer(compositeView));
2649
2841
  },
2650
2842
 
2651
2843
  // Internal method. Append a view to the end of the $el.
@@ -2710,6 +2902,14 @@
2710
2902
  Marionette.LayoutView = Marionette.ItemView.extend({
2711
2903
  regionClass: Marionette.Region,
2712
2904
 
2905
+ options: {
2906
+ destroyImmediate: false
2907
+ },
2908
+
2909
+ // used as the prefix for child view events
2910
+ // that are forwarded through the layoutview
2911
+ childViewEventPrefix: 'childview',
2912
+
2713
2913
  // Ensure the regions are available when the `initialize` method
2714
2914
  // is called.
2715
2915
  constructor: function(options) {
@@ -2744,11 +2944,23 @@
2744
2944
  // Handle destroying regions, and then destroy the view itself.
2745
2945
  destroy: function() {
2746
2946
  if (this.isDestroyed) { return this; }
2747
-
2947
+ // #2134: remove parent element before destroying the child views, so
2948
+ // removing the child views doesn't retrigger repaints
2949
+ if(this.getOption('destroyImmediate') === true) {
2950
+ this.$el.remove();
2951
+ }
2748
2952
  this.regionManager.destroy();
2749
2953
  return Marionette.ItemView.prototype.destroy.apply(this, arguments);
2750
2954
  },
2751
2955
 
2956
+ showChildView: function(regionName, view) {
2957
+ return this.getRegion(regionName).show(view);
2958
+ },
2959
+
2960
+ getChildView: function(regionName) {
2961
+ return this.getRegion(regionName).currentView;
2962
+ },
2963
+
2752
2964
  // Add a single region, by name, to the layoutView
2753
2965
  addRegion: function(name, definition) {
2754
2966
  var regions = {};
@@ -2808,7 +3020,7 @@
2808
3020
 
2809
3021
  // Normalize region selectors hash to allow
2810
3022
  // a user to use the @ui. syntax.
2811
- regions = this.normalizeUIValues(regions);
3023
+ regions = this.normalizeUIValues(regions, ['selector', 'el']);
2812
3024
 
2813
3025
  this.addRegions(regions);
2814
3026
  },
@@ -2877,6 +3089,12 @@
2877
3089
  this.view = view;
2878
3090
  this.defaults = _.result(this, 'defaults') || {};
2879
3091
  this.options = _.extend({}, this.defaults, options);
3092
+ // Construct an internal UI hash using
3093
+ // the views UI hash and then the behaviors UI hash.
3094
+ // This allows the user to use UI hash elements
3095
+ // defined in the parent view as well as those
3096
+ // defined in the given behavior.
3097
+ this.ui = _.extend({}, _.result(view, 'ui'), _.result(this, 'ui'));
2880
3098
 
2881
3099
  Marionette.Object.apply(this, arguments);
2882
3100
  },
@@ -2892,6 +3110,8 @@
2892
3110
  // Overrides Object#destroy to prevent additional events from being triggered.
2893
3111
  destroy: function() {
2894
3112
  this.stopListening();
3113
+
3114
+ return this;
2895
3115
  },
2896
3116
 
2897
3117
  proxyViewProperties: function (view) {
@@ -2939,23 +3159,15 @@
2939
3159
 
2940
3160
  behaviorEvents: function(behaviorEvents, behaviors) {
2941
3161
  var _behaviorsEvents = {};
2942
- var viewUI = this._uiBindings || _.result(this, 'ui');
2943
3162
 
2944
3163
  _.each(behaviors, function(b, i) {
2945
3164
  var _events = {};
2946
3165
  var behaviorEvents = _.clone(_.result(b, 'events')) || {};
2947
- var behaviorUI = b._uiBindings || _.result(b, 'ui');
2948
3166
 
2949
- // Construct an internal UI hash first using
2950
- // the views UI hash and then the behaviors UI hash.
2951
- // This allows the user to use UI hash elements
2952
- // defined in the parent view as well as those
2953
- // defined in the given behavior.
2954
- var ui = _.extend({}, viewUI, behaviorUI);
2955
3167
 
2956
3168
  // Normalize behavior events hash to allow
2957
3169
  // a user to use the @ui. syntax.
2958
- behaviorEvents = Marionette.normalizeUIKeys(behaviorEvents, ui);
3170
+ behaviorEvents = Marionette.normalizeUIKeys(behaviorEvents, getBehaviorsUI(b));
2959
3171
 
2960
3172
  var j = 0;
2961
3173
  _.each(behaviorEvents, function(behaviour, key) {
@@ -3042,7 +3254,6 @@
3042
3254
  // for views
3043
3255
  function BehaviorTriggersBuilder(view, behaviors) {
3044
3256
  this._view = view;
3045
- this._viewUI = _.result(view, 'ui');
3046
3257
  this._behaviors = behaviors;
3047
3258
  this._triggers = {};
3048
3259
  }
@@ -3056,10 +3267,9 @@
3056
3267
 
3057
3268
  // Internal method to build all trigger handlers for a given behavior
3058
3269
  _buildTriggerHandlersForBehavior: function(behavior, i) {
3059
- var ui = _.extend({}, this._viewUI, _.result(behavior, 'ui'));
3060
3270
  var triggersHash = _.clone(_.result(behavior, 'triggers')) || {};
3061
3271
 
3062
- triggersHash = Marionette.normalizeUIKeys(triggersHash, ui);
3272
+ triggersHash = Marionette.normalizeUIKeys(triggersHash, getBehaviorsUI(behavior));
3063
3273
 
3064
3274
  _.each(triggersHash, _.bind(this._setHandlerForBehavior, this, behavior, i));
3065
3275
  },
@@ -3076,6 +3286,10 @@
3076
3286
  }
3077
3287
  });
3078
3288
 
3289
+ function getBehaviorsUI(behavior) {
3290
+ return behavior._uiBindings || behavior.ui;
3291
+ }
3292
+
3079
3293
  return Behaviors;
3080
3294
 
3081
3295
  })(Marionette, _);
@@ -3157,6 +3371,8 @@
3157
3371
  this.route(route, methodName, _.bind(method, controller));
3158
3372
  },
3159
3373
 
3374
+ mergeOptions: Marionette.mergeOptions,
3375
+
3160
3376
  // Proxy `getOption` to enable getting options from this or this.options by name.
3161
3377
  getOption: Marionette.proxyGetOption,
3162
3378
 
@@ -3464,7 +3680,7 @@
3464
3680
 
3465
3681
  // get the custom args passed in after the module definition and
3466
3682
  // get rid of the module name and definition function
3467
- var customArgs = _.rest(arguments, 3);
3683
+ var customArgs = _.drop(arguments, 3);
3468
3684
 
3469
3685
  // Split the module names and get the number of submodules.
3470
3686
  // i.e. an example module name of `Doge.Wow.Amaze` would