root / snf-cyclades-app / synnefo / ui / static / snf / js / lib / backbone-filtered-collection.js @ aedcb7f3
History | View | Annotate | Download (8.3 kB)
1 |
/*
|
---|---|
2 |
The MIT License (MIT)
|
3 |
|
4 |
Copyright (c) 2013 Dmitriy Likhten
|
5 |
|
6 |
Permission is hereby granted, free of charge, to any person obtaining a copy
|
7 |
of this software and associated documentation files (the "Software"), to deal
|
8 |
in the Software without restriction, including without limitation the rights
|
9 |
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
10 |
copies of the Software, and to permit persons to whom the Software is
|
11 |
furnished to do so, subject to the following conditions:
|
12 |
|
13 |
The above copyright notice and this permission notice shall be included in
|
14 |
all copies or substantial portions of the Software.
|
15 |
|
16 |
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
17 |
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
18 |
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
19 |
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
20 |
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
21 |
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
22 |
THE SOFTWARE.
|
23 |
*/
|
24 |
/* version 1.1.0 */
|
25 |
(function(_, Backbone) {
|
26 |
var defaultFilter = function() {return true;}; |
27 |
/**
|
28 |
* This represents a filtered collection. You can either pass a filter or
|
29 |
* invoke setFilter(filter) to give a filter. The filter is identical to
|
30 |
* that which is used for _.select(array, filter)
|
31 |
*
|
32 |
* false filter indicates no filtering.
|
33 |
*
|
34 |
* do not modify this collection directly via #add/#remove, modify the
|
35 |
* underlying origModel.
|
36 |
*
|
37 |
* Events:
|
38 |
* add - something was added (via filter or trickling from underlying collection)
|
39 |
* remove - something was removed (via filter or trickling from underlying collection)
|
40 |
* reset - the whole thing was reset
|
41 |
* sort - same as reset, but via sort
|
42 |
* filter-complete - filtering is complete -- very useful if you want to trigger a re-draw
|
43 |
* of the whole collection
|
44 |
*/
|
45 |
Backbone.FilteredCollection = Backbone.Collection.extend({ |
46 |
collectionFilter: null |
47 |
,defaultFilter: defaultFilter
|
48 |
|
49 |
,initialize: function(models, data) { |
50 |
if (models) throw "models cannot be set directly, unfortunately first argument is the models."; |
51 |
this.collection = data.collection;
|
52 |
this.setFilter(data.collectionFilter);
|
53 |
|
54 |
this.collection.bind("add", this.addModel, this); |
55 |
this.collection.bind("remove", this.removeModel, this); |
56 |
this.collection.bind("reset", this.resetCollection, this); |
57 |
this.collection.bind("sort", this.resortCollection, this); |
58 |
this.collection.bind("change", this._modelChanged, this); |
59 |
this.collection.bind("filter-complete", this._filterComplete, this); |
60 |
} |
61 |
|
62 |
,_reset: function(options) { |
63 |
Backbone.Collection.prototype._reset.call(this, options);
|
64 |
this._mapping = [];
|
65 |
} |
66 |
|
67 |
,add: function() { |
68 |
throw "Do not invoke directly"; |
69 |
} |
70 |
|
71 |
,remove: function() { |
72 |
throw "Do not invoke directly"; |
73 |
} |
74 |
|
75 |
,reset: function() { |
76 |
throw "Do not invoke directly"; |
77 |
} |
78 |
|
79 |
,_modelChanged: function(model, collection, options){ |
80 |
options || (options = {}); |
81 |
|
82 |
var ownIndexOfModel = this.indexOf(model); |
83 |
if (this.collectionFilter(model)){ |
84 |
// Model passed filter
|
85 |
if (ownIndexOfModel < 0){ |
86 |
// Model not found, add it
|
87 |
var index = this.collection.indexOf(model); |
88 |
this._forceAddModel(model, {index: index}); |
89 |
} |
90 |
// the model passes the filter and is already in the collection
|
91 |
// therefore we want to indicate that the model has changed
|
92 |
else {
|
93 |
this.trigger("change", model, this); |
94 |
} |
95 |
} else {
|
96 |
// Model did not pass filter
|
97 |
if (ownIndexOfModel > -1){ |
98 |
this._forceRemoveModel(model, {index: ownIndexOfModel}); |
99 |
} |
100 |
} |
101 |
if (! options.silent) {
|
102 |
this._filterComplete();
|
103 |
} |
104 |
} |
105 |
|
106 |
,resortCollection: function() { |
107 |
// note: we don't need to do any filter work since sort
|
108 |
// implies nothing changed, only order
|
109 |
var newModels = [];
|
110 |
var newMapping = [];
|
111 |
var models = this.models; |
112 |
_.each(this.collection.models, function(model, index) { |
113 |
if (models.indexOf(model) >= 0) { |
114 |
newModels.push(model); |
115 |
newMapping.push(index); |
116 |
} |
117 |
}); |
118 |
this.models = newModels;
|
119 |
this._mapping = newMapping;
|
120 |
this.trigger("sort", this); |
121 |
} |
122 |
|
123 |
,resetCollection: function() { |
124 |
this._mapping = [];
|
125 |
this._reset();
|
126 |
this.setFilter(undefined, {silent: true}); |
127 |
this.trigger("reset", this); |
128 |
} |
129 |
|
130 |
// this is to synchronize where the element exists in the original model
|
131 |
// to our _mappings array
|
132 |
,renumberMappings: function() { |
133 |
this._mapping = []
|
134 |
var collection = this.collection; |
135 |
var mapping = this._mapping; |
136 |
|
137 |
_(this.models).each(function(model) { |
138 |
mapping.push(collection.indexOf(model)); |
139 |
}); |
140 |
} |
141 |
|
142 |
,removeModel: function(model, colleciton, options) { |
143 |
var at = this._mapping.indexOf(options.index); |
144 |
if (at > -1) { |
145 |
this._forceRemoveModel(model, _.extend({index: at}, options)); |
146 |
} |
147 |
this.renumberMappings();
|
148 |
} |
149 |
|
150 |
// the options.index here is the index of the current model which we are removing
|
151 |
,_forceRemoveModel: function(model, options) { |
152 |
this._mapping.splice(options.index, 1); |
153 |
Backbone.Collection.prototype.remove.call(this, model, {silent: options.silent}); |
154 |
if (! options.silent) {
|
155 |
this.trigger("remove", model, this, {index: options.index}) |
156 |
} |
157 |
} |
158 |
|
159 |
,addModel: function(model, collection, options) { |
160 |
if (this.collectionFilter(model)) { |
161 |
this._forceAddModel(model, _.extend(options || {}, {index: (options && options.at) || collection.indexOf(model)})); |
162 |
} |
163 |
this.renumberMappings();
|
164 |
} |
165 |
|
166 |
// the options.index here is the index of the original model which we are inserting
|
167 |
,_forceAddModel: function(model, options) { |
168 |
var desiredIndex = options.index;
|
169 |
// determine where to add, look at mapping and find first object with the index
|
170 |
// great than the one that we are given
|
171 |
var addToIndex = _.sortedIndex(this._mapping, desiredIndex, function(origIndex) { return origIndex; }); |
172 |
|
173 |
// add it there
|
174 |
if (this.get(model.id)) { return } |
175 |
Backbone.Collection.prototype.add.call(this, model, {at: addToIndex, silent: options.silent}); |
176 |
this._mapping.splice(addToIndex, 0, desiredIndex); |
177 |
if (! options.silent) {
|
178 |
this.trigger("add", model, this, {index: addToIndex}) |
179 |
} |
180 |
} |
181 |
|
182 |
,setFilter: function(newFilter, options) { |
183 |
options || (options = {}); |
184 |
if (newFilter === false) { newFilter = this.defaultFilter } // false = clear out filter |
185 |
this.collectionFilter = newFilter || this.collectionFilter || this.defaultFilter; |
186 |
|
187 |
// this assumes that the original collection was unmodified
|
188 |
// without the use of add/remove/reset events. If it was, a
|
189 |
// reset event must be thrown, or this object's .resetCollection
|
190 |
// method must be invoked, or this will most likely fall out-of-sync
|
191 |
|
192 |
// why HashMap lookup when you can get it off the stack
|
193 |
var filter = this.collectionFilter; |
194 |
var mapping = this._mapping; |
195 |
|
196 |
// this is the option object to pass, it will be mutated on each
|
197 |
// iteration
|
198 |
var passthroughOption = _.extend({}, options);
|
199 |
this.collection.each(function(model, index) { |
200 |
var foundIndex = mapping.indexOf(index);
|
201 |
|
202 |
if (filter(model, index)) {
|
203 |
// if already added, no touchy
|
204 |
if (foundIndex == -1) { |
205 |
passthroughOption.index = index |
206 |
this._forceAddModel(model, passthroughOption);
|
207 |
} |
208 |
} |
209 |
else {
|
210 |
if (foundIndex > -1) { |
211 |
passthroughOption.index = foundIndex == -1 ? this.length : foundIndex; |
212 |
this._forceRemoveModel(model, passthroughOption);
|
213 |
} |
214 |
} |
215 |
}, this);
|
216 |
if (! options.silent) {
|
217 |
this._filterComplete();
|
218 |
} |
219 |
} |
220 |
|
221 |
,_onModelEvent: function(event, model, collection, options) { |
222 |
// noop, this collection has no business dealing with events of the original model
|
223 |
// they will be handled by the original normal collection and bubble up to here
|
224 |
} |
225 |
|
226 |
,_filterComplete: function() { |
227 |
this.trigger("filter-complete", this); |
228 |
} |
229 |
}); |
230 |
})(_, Backbone); |