1 |
|
2 | var includes = require('lodash.includes');
|
3 | var difference = require('lodash.difference');
|
4 | var forEach = require('lodash.foreach');
|
5 | var every = require('lodash.every');
|
6 | var assign = require('lodash.assign');
|
7 | var isArray = require('lodash.isarray');
|
8 | var isEqual = require('lodash.isequal');
|
9 | var keys = require('lodash.keys');
|
10 | var reduce = require('lodash.reduce');
|
11 | var sortBy = require('lodash.sortby');
|
12 | var sortedIndex = require('lodash.sortedindex');
|
13 | var union = require('lodash.union');
|
14 | var classExtend = require('ampersand-class-extend');
|
15 | var Events = require('ampersand-events');
|
16 |
|
17 | var slice = Array.prototype.slice;
|
18 |
|
19 |
|
20 | function FilteredCollection(collection, spec) {
|
21 | this.collection = collection;
|
22 | this.indexes = collection.indexes || [];
|
23 | this._indexes = {};
|
24 | this._resetIndexes(this._indexes);
|
25 | this.mainIndex = collection.mainIndex;
|
26 | this.models = [];
|
27 | this.configure(spec || {}, true);
|
28 | this.listenTo(this.collection, 'all', this._onCollectionEvent);
|
29 | }
|
30 |
|
31 | assign(FilteredCollection.prototype, Events, {
|
32 |
|
33 |
|
34 |
|
35 | addFilter: function (filter) {
|
36 | this.swapFilters([filter], []);
|
37 | },
|
38 |
|
39 |
|
40 | removeFilter: function (filter) {
|
41 | this.swapFilters([], [filter]);
|
42 | },
|
43 |
|
44 |
|
45 | clearFilters: function () {
|
46 | this._resetFilters();
|
47 | this._runFilters();
|
48 | },
|
49 |
|
50 |
|
51 |
|
52 | swapFilters: function (newFilters, oldFilters) {
|
53 | var self = this;
|
54 |
|
55 | if (!oldFilters) {
|
56 | oldFilters = this._filters;
|
57 | } else if (!isArray(oldFilters)) {
|
58 | oldFilters = [oldFilters];
|
59 | }
|
60 |
|
61 | if (!newFilters) {
|
62 | newFilters = [];
|
63 | } else if (!isArray(newFilters)) {
|
64 | newFilters = [newFilters];
|
65 | }
|
66 |
|
67 | oldFilters.forEach(function (filter) {
|
68 | self._removeFilter(filter);
|
69 | });
|
70 |
|
71 | newFilters.forEach(function (filter) {
|
72 | self._addFilter(filter);
|
73 | });
|
74 |
|
75 | this._runFilters();
|
76 | },
|
77 |
|
78 |
|
79 | configure: function (opts, clear) {
|
80 | if (clear) this._resetFilters(clear);
|
81 | this._parseSpec(opts);
|
82 | if (clear) this._runFilters();
|
83 | },
|
84 |
|
85 |
|
86 | at: function (index) {
|
87 | return this.models[index];
|
88 | },
|
89 |
|
90 |
|
91 | get: function (query, indexName) {
|
92 | var model = this.collection.get(query, indexName);
|
93 | if (model && includes(this.models, model)) return model;
|
94 | },
|
95 |
|
96 |
|
97 | reset: function () {
|
98 | this.configure({}, true);
|
99 | },
|
100 |
|
101 |
|
102 |
|
103 |
|
104 | _indexedGet: function (query, indexName) {
|
105 | if (!query) return;
|
106 | var index = this._indexes[indexName || this.mainIndex];
|
107 | return index[query] || index[query[this.mainIndex]] || this._indexes.cid[query] || this._indexes.cid[query.cid];
|
108 | },
|
109 |
|
110 | _parseSpec: function (spec) {
|
111 | if (spec.watched) this._watch(spec.watched);
|
112 |
|
113 | if (spec.comparator) this.comparator = spec.comparator;
|
114 | if (spec.where) {
|
115 | forEach(spec.where, function (value, item) {
|
116 | this._addFilter(function (model) {
|
117 | return (model.get ? model.get(item) : model[item]) === value;
|
118 | });
|
119 | }, this);
|
120 |
|
121 | this._watch(keys(spec.where));
|
122 | }
|
123 | if (spec.filter) {
|
124 | this._addFilter(spec.filter);
|
125 | }
|
126 | if (spec.filters) {
|
127 | spec.filters.forEach(this._addFilter, this);
|
128 | }
|
129 | },
|
130 |
|
131 | _addFilter: function (filter) {
|
132 | this._filters.push(filter);
|
133 | },
|
134 |
|
135 |
|
136 | _removeFilter: function (filter) {
|
137 | var index = this._filters.indexOf(filter);
|
138 | if (index !== -1) {
|
139 | this._filters.splice(index, 1);
|
140 | }
|
141 | },
|
142 |
|
143 |
|
144 | _resetFilters: function (resetComparator) {
|
145 | this._filters = [];
|
146 | this._watched = [];
|
147 | if (resetComparator) this.comparator = undefined;
|
148 | },
|
149 |
|
150 |
|
151 | _watch: function (items) {
|
152 | this._watched = union(this._watched, items);
|
153 | },
|
154 |
|
155 |
|
156 | _unwatch: function (item) {
|
157 | this._watched = difference(this._watched, isArray(item) ? item : [item]);
|
158 | },
|
159 |
|
160 | _sortModels: function (newModels, comparator) {
|
161 | comparator = comparator || this.comparator || this.collection.comparator;
|
162 | if (comparator) {
|
163 | if (typeof comparator === 'string' || comparator.length === 1) {
|
164 | newModels = sortBy(newModels, comparator);
|
165 | } else {
|
166 |
|
167 | newModels = newModels.sort(comparator);
|
168 | }
|
169 | } else {
|
170 |
|
171 | this._runFilters();
|
172 | }
|
173 | return newModels;
|
174 | },
|
175 |
|
176 |
|
177 | _addModel: function (model, options, eventName) {
|
178 | var newModels = slice.call(this.models);
|
179 | var comparator = this.comparator || this.collection.comparator;
|
180 |
|
181 | var sortable = eventName === 'add' && this.collection.comparator && (options.at == null) && options.sort !== false;
|
182 | if (!sortable) {
|
183 | var index = sortedIndex(newModels, model, comparator);
|
184 | newModels.splice(index, 0, model);
|
185 | } else {
|
186 | newModels.push(model);
|
187 | if (options.at) newModels = this._sortModels(newModels);
|
188 | }
|
189 |
|
190 | this.models = newModels;
|
191 | this._addIndex(this._indexes, model);
|
192 | if (this.comparator && !sortable) {
|
193 | this.trigger('sort', this);
|
194 | }
|
195 | },
|
196 |
|
197 |
|
198 | _removeModel: function (model) {
|
199 | var newModels = slice.call(this.models);
|
200 | var modelIndex = newModels.indexOf(model);
|
201 | if (modelIndex > -1) {
|
202 | newModels.splice(modelIndex, 1);
|
203 | this.models = newModels;
|
204 | this._removeIndex(this._indexes, model);
|
205 | return true;
|
206 | }
|
207 | return false;
|
208 | },
|
209 |
|
210 |
|
211 |
|
212 | _testModel: function (model) {
|
213 | if (this._filters.length === 0) {
|
214 | return true;
|
215 | }
|
216 | return every(this._filters, function (filter) {
|
217 | return filter(model);
|
218 | });
|
219 | },
|
220 |
|
221 | _addIndex: function (newIndexes, model) {
|
222 | for (var name in this._indexes) {
|
223 | var indexVal = model[name] || (model.get && model.get(name));
|
224 | if (indexVal) newIndexes[name][indexVal] = model;
|
225 | }
|
226 | },
|
227 |
|
228 | _removeIndex: function (newIndexes, model) {
|
229 | for (var name in this._indexes) {
|
230 | delete this._indexes[name][model[name] || (model.get && model.get(name))];
|
231 | }
|
232 | },
|
233 |
|
234 | _resetIndexes: function (newIndexes) {
|
235 | var list = slice.call(this.indexes);
|
236 | list.push(this.mainIndex);
|
237 | list.push('cid');
|
238 | for (var i = 0; i < list.length; i++) {
|
239 | newIndexes[list[i]] = {};
|
240 | }
|
241 | },
|
242 |
|
243 |
|
244 | _runFilters: function () {
|
245 |
|
246 | var existingModels = slice.call(this.models);
|
247 | var rootModels = slice.call(this.collection.models);
|
248 | var newIndexes = {};
|
249 | var newModels, toAdd, toRemove;
|
250 |
|
251 | this._resetIndexes(newIndexes);
|
252 |
|
253 |
|
254 | if (this._filters.length) {
|
255 | newModels = reduce(this._filters, function (startingArray, filterFunc) {
|
256 | return startingArray.filter(filterFunc);
|
257 | }, rootModels);
|
258 | } else {
|
259 | newModels = slice.call(rootModels);
|
260 | }
|
261 |
|
262 |
|
263 | if (this.comparator) newModels = this._sortModels(newModels, this.comparator);
|
264 |
|
265 | newModels.forEach(function (model) {
|
266 | this._addIndex(newIndexes, model);
|
267 | }, this);
|
268 |
|
269 |
|
270 | if (rootModels.length) {
|
271 | this._filtered = newModels;
|
272 | this._indexes = newIndexes;
|
273 | } else {
|
274 | this._filtered = [];
|
275 | this._resetIndexes(this._indexes);
|
276 | }
|
277 |
|
278 |
|
279 | toAdd = difference(newModels, existingModels);
|
280 | toRemove = difference(existingModels, newModels);
|
281 |
|
282 |
|
283 | this.models = newModels;
|
284 |
|
285 | forEach(toRemove, function (model) {
|
286 | this.trigger('remove', model, this);
|
287 | }, this);
|
288 |
|
289 | forEach(toAdd, function (model) {
|
290 | this.trigger('add', model, this);
|
291 | }, this);
|
292 |
|
293 |
|
294 | if (!isEqual(existingModels, newModels) && this.comparator) {
|
295 | this.trigger('sort', this);
|
296 | }
|
297 | },
|
298 |
|
299 | _onCollectionEvent: function (event, model, that, options) {
|
300 |
|
301 | options || (options = {});
|
302 | var accepted;
|
303 | var eventName = event.split(':')[0];
|
304 | var propName = event.split(':')[1];
|
305 | var action = event;
|
306 | var alreadyHave = this._indexedGet(model);
|
307 |
|
308 | var sortable = this.collection.comparator && (options.at == null) && (options.sort !== false);
|
309 | var add = options.add;
|
310 | var remove = options.remove;
|
311 | var ordered = !sortable && add && remove;
|
312 |
|
313 | if (
|
314 | (propName !== undefined && propName === this.comparator) ||
|
315 | includes(this._watched, propName)
|
316 | ) {
|
317 | accepted = this._testModel(model);
|
318 |
|
319 | if (!alreadyHave && accepted) {
|
320 | action = 'add';
|
321 | } else if (alreadyHave && !accepted) {
|
322 | action = 'remove';
|
323 | } else {
|
324 | action = 'ignore';
|
325 | }
|
326 | } else if (action === 'add') {
|
327 | if (!this._testModel(model) || alreadyHave) {
|
328 | action = 'ignore';
|
329 | }
|
330 | } else if (eventName === 'change' && !alreadyHave) {
|
331 |
|
332 | action = 'ignore';
|
333 | }
|
334 |
|
335 |
|
336 |
|
337 | if (action === 'reset') return this._runFilters();
|
338 |
|
339 | if (action === 'add') {
|
340 | if (this.models.length === 0) {
|
341 | this._runFilters();
|
342 | } else {
|
343 | this._addModel(model, options, event);
|
344 | this.trigger('add', model, this);
|
345 | }
|
346 | return;
|
347 | }
|
348 |
|
349 | if (action === 'remove') {
|
350 | if (this._removeModel(model)) {
|
351 | this.trigger('remove', model, this);
|
352 | }
|
353 | return;
|
354 | }
|
355 |
|
356 | if (action !== 'ignore') {
|
357 | this.trigger.apply(this, arguments);
|
358 | }
|
359 |
|
360 |
|
361 | if (
|
362 | action === 'sort' ||
|
363 | (propName && !sortable && includes([this.comparator, this.collection.comparator], propName))
|
364 | ) {
|
365 | if (ordered && model.isNew) return;
|
366 | this.models = this._sortModels(this.models);
|
367 | if (this.comparator && action !== 'sort') {
|
368 | this.trigger('sort', this);
|
369 | }
|
370 | }
|
371 |
|
372 | }
|
373 |
|
374 | });
|
375 |
|
376 | Object.defineProperty(FilteredCollection.prototype, 'length', {
|
377 | get: function () {
|
378 | return this.models.length;
|
379 | }
|
380 | });
|
381 |
|
382 | Object.defineProperty(FilteredCollection.prototype, 'isCollection', {
|
383 | get: function () {
|
384 | return true;
|
385 | }
|
386 | });
|
387 |
|
388 | var arrayMethods = [
|
389 | 'indexOf',
|
390 | 'lastIndexOf',
|
391 | 'every',
|
392 | 'some',
|
393 | 'forEach',
|
394 | 'map',
|
395 | 'filter',
|
396 | 'reduce',
|
397 | 'reduceRight'
|
398 | ];
|
399 |
|
400 | arrayMethods.forEach(function (method) {
|
401 | FilteredCollection.prototype[method] = function () {
|
402 | return this.models[method].apply(this.models, arguments);
|
403 | };
|
404 | });
|
405 |
|
406 |
|
407 | FilteredCollection.prototype.each = FilteredCollection.prototype.forEach;
|
408 |
|
409 |
|
410 | var collectionMethods = [
|
411 | 'serialize',
|
412 | 'toJSON'
|
413 | ];
|
414 |
|
415 | collectionMethods.forEach(function (method) {
|
416 | FilteredCollection.prototype[method] = function () {
|
417 | return this.collection[method].apply(this, arguments);
|
418 | };
|
419 | });
|
420 |
|
421 | FilteredCollection.extend = classExtend;
|
422 |
|
423 | module.exports = FilteredCollection;
|