marionette-rails 2.3.2 → 2.4.0

Sign up to get free protection for your applications and to get access to all the features.
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