Statistics
| Branch: | Tag: | Revision:

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);