UNPKG

5.52 kBJavaScriptView Raw
1var _ = require('underscore');
2var Events = require('backbone-events-standalone');
3var classExtend = require('ampersand-class-extend');
4var underscoreMixins = require('ampersand-collection-underscore-mixin');
5var slice = Array.prototype.slice;
6
7
8function SubCollection(collection, spec) {
9 spec || (spec = {});
10 this.collection = collection;
11 this._reset();
12 this._watched = spec.watched || [];
13 this._parseFilters(spec);
14 this._runFilters();
15 this.listenTo(this.collection, 'all', this._onCollectionEvent);
16}
17
18
19_.extend(SubCollection.prototype, Events, underscoreMixins, {
20 // add a filter function directly
21 addFilter: function (filter) {
22 this._addFilter();
23 this._runFilters();
24 },
25
26 // remove filter function directly
27 removeFilter: function (filter) {
28 this._removeFilter(filter);
29 this._runFilters();
30 },
31
32 // clears filters fires events for changes
33 clearFilters: function () {
34 this._reset();
35 this._runFilters();
36 },
37
38 // Update sub collection config, if `clear`
39 // then clear existing filters before start.
40 // This takes all the same filter arguments
41 // as the init function. So you can pass:
42 // {
43 // where: {
44 // name: 'something'
45 // },
46 // limit: 20
47 // }
48 configure: function (opts, clear) {
49 if (clear) this._reset();
50 this._parseFilters(opts);
51 this._runFilters();
52 },
53
54 // gets a model at a given index
55 at: function (index) {
56 return this.models[index];
57 },
58
59 // proxy `get` method to the underlying collection
60 get: function (query, indexName) {
61 var model = this.collection.get(query, indexName);
62 if (model && this.contains(model)) return model;
63 },
64
65 // remove filter if found
66 _removeFilter: function () {
67 var index = this._filters.indexOf(filter);
68 if (index !== -1) {
69 this._filters.splice(index, 1);
70 }
71 },
72
73 // clear all filters, reset everything
74 _reset: function () {
75 this._filters = [];
76 this._watched = [];
77 this.models = [];
78 this.limit = undefined;
79 this.offset = undefined;
80 },
81
82 // internal method registering new filter function
83 _addFilter: function (filter) {
84 this._filters.push(filter);
85 },
86
87 // adds a property or array of properties to watch, ensures uniquness.
88 _watch: function (item) {
89 this._watched = _.union(this._watched, _.isArray(item) ? item : [item]);
90 },
91
92 // removes a watched property
93 _unwatch: function (item) {
94 this._watched = _.without(this._watched, item);
95 },
96
97 _parseFilters: function (spec) {
98 if (spec.where) {
99 _.each(spec.where, function (value, item) {
100 this._addFilter(function (model) {
101 return (model.get ? model.get(item) : model[item]) === value;
102 });
103 }, this);
104 // also make sure we watch all `where` keys
105 this._watch(_.keys(spec.where));
106 }
107 if (spec.hasOwnProperty('limit')) this.limit = spec.limit;
108 if (spec.hasOwnProperty('offset')) this.offset = spec.offset;
109 if (spec.filter) {
110 this._addFilter(spec.filter, false);
111 }
112 if (spec.filters) {
113 spec.filters.forEach(this._addFilter, this);
114 }
115 if (spec.comparator) {
116 this.comparator = spec.comparator;
117 }
118 },
119
120 _runFilters: function () {
121 // make a copy of the array for comparisons
122 var existingModels = slice.call(this.models);
123 var rootModels = slice.call(this.collection.models);
124 var offset = (this.offset || 0);
125 var newModels, toAdd, toRemove;
126
127 // reduce base model set by applying filters
128 if (this._filters.length) {
129 newModels = _.reduce(this._filters, function (startingArray, filterFunc) {
130 return startingArray.filter(filterFunc);
131 }, rootModels);
132 } else {
133 newModels = slice.call(rootModels);
134 }
135
136 // sort it
137 if (this.comparator) newModels = _.sortBy(newModels, this.comparator);
138
139 // trim it to length
140 if (this.limit || this.offset) newModels = newModels.slice(offset, this.limit + offset);
141
142 // now we've got our new models time to compare
143 toAdd = _.difference(newModels, existingModels);
144 toRemove = _.difference(existingModels, newModels);
145
146 _.each(toRemove, function (model) {
147 this.trigger('remove', model, this);
148 }, this);
149
150 _.each(toAdd, function (model) {
151 this.trigger('add', model, this);
152 }, this);
153
154 // if they contain the same models, but in new order, trigger sort
155 if (toAdd.length === 0 && toRemove.length === 0 && !_.isEqual(existingModels, newModels)) {
156 this.trigger('sort', this);
157 }
158
159 // save 'em
160 this.models = newModels;
161 },
162
163 _onCollectionEvent: function (eventName, event) {
164 if (_.contains(this._watched, eventName.split(':')[1]) || _.contains(['add', 'remove'], eventName)) {
165 this._runFilters();
166 }
167 }
168});
169
170Object.defineProperty(SubCollection.prototype, 'length', {
171 get: function () {
172 return this.models.length;
173 },
174 // we add a `set` to keep Safari 5.1 from freaking out
175 set: function () {
176 return;
177 }
178});
179
180SubCollection.extend = classExtend;
181
182module.exports = SubCollection;