1 /**
  2  * @fileOverview collection of models
  3  * @author yiminghe@gmail.com
  4  */
  5 KISSY.add("mvc/collection", function (S, Event, Model, Base) {
  6 
  7     function findModelIndex(mods, mod, comparator) {
  8         var i = mods.length;
  9         if (comparator) {
 10             var k = comparator(mod);
 11             for (i = 0; i < mods.length; i++) {
 12                 var k2 = comparator(mods[i]);
 13                 if (k < k2) {
 14                     break;
 15                 }
 16             }
 17         }
 18         return i;
 19     }
 20 
 21     /**
 22      * @name Collection
 23      * @class
 24      * Collection. A list of model.
 25      * @memberOf MVC
 26      * @extends Base
 27      */
 28     function Collection() {
 29         Collection.superclass.constructor.apply(this, arguments);
 30     }
 31 
 32     Collection.ATTRS =
 33     /**
 34      * @lends MVC.Collection#
 35      */
 36     {
 37         /**
 38          * Model constructor with in current collection.
 39          * @type MVC.Model
 40          */
 41         model:{
 42             value:Model
 43         },
 44         /**
 45          * Model list.
 46          * @type MVC.Model[]
 47          */
 48         models:{
 49             /*
 50              normalize model list
 51              @param models
 52              */
 53             setter:function (models) {
 54                 var prev = this.get("models");
 55                 this.remove(prev, {silent:1});
 56                 this.add(models, {silent:1});
 57                 return this.get("models");
 58             },
 59             value:[]
 60         },
 61         /**
 62          * Get url for sending data to server.
 63          * @type String|Function
 64          */
 65         url:{
 66             value:""
 67         },
 68         /**
 69          * Comparator function for index getter when adding model.
 70          * default to append to last of current model list.
 71          * @type Function
 72          */
 73         comparator:{},
 74         /**
 75          * Sync function to sync data with server.
 76          * Default to call {@link MVC.sync}
 77          * @type Function
 78          */
 79         sync:{
 80             value:function () {
 81                 S.require("mvc").sync.apply(this, arguments);
 82             }
 83         },
 84         /**
 85          * Get structured data from raw data returned from server.
 86          * default to return raw data from server.
 87          * @type Function
 88          */
 89         parse:{
 90             value:function (resp) {
 91                 return resp;
 92             }
 93         }
 94     };
 95 
 96     S.extend(Collection, Base,
 97         /**
 98          * @lends MVC.Collection#
 99          */
100         {
101             /**
102              * Sort model list according {@link MVC.Collection#comparator}.
103              */
104             sort:function () {
105                 var comparator = this.get("comparator");
106                 if (comparator) {
107                     this.get("models").sort(function (a, b) {
108                         return comparator(a) - comparator(b);
109                     });
110                 }
111             },
112 
113             /**
114              * Get json representation of this collection.
115              * @return Object[]
116              */
117             toJSON:function () {
118                 return S.map(this.get("models"), function (m) {
119                     return m.toJSON();
120                 });
121             },
122 
123             /**
124              * Add a model to current collection.
125              * @param {Object|MVC.Model} model Model or json data to be added.
126              * @param {object} [opts] Add config
127              * @param {Function} opts.silent Whether to fire add event.
128              */
129             add:function (model, opts) {
130                 var self = this,
131                     ret = true;
132                 if (S.isArray(model)) {
133                     var orig = [].concat(model);
134                     S.each(orig, function (m) {
135                         var t = self._add(m, opts);
136                         ret = ret && t;
137                     });
138                 } else {
139                     ret = self._add(model, opts);
140                 }
141                 return ret;
142             },
143 
144             /**
145              * Remove an existing model from current collection.
146              * @param {MVC.Model} model Model to be removed.
147              * @param {object} [opts] Remove config.
148              * @param {Function} opts.silent Whether to fire remove event.
149              */
150             remove:function (model, opts) {
151                 var self = this;
152                 if (S.isArray(model)) {
153                     var orig = [].concat(model);
154                     S.each(orig, function (m) {
155                         self._remove(m, opts);
156                     });
157                 } else if (model) {
158                     self._remove(model, opts);
159                 }
160             },
161 
162             /**
163              * Get model at specified index.
164              * @param {Number} i Specified index.
165              */
166             at:function (i) {
167                 return this.get("models")[i];
168             },
169 
170             _normModel:function (model) {
171                 var ret = true;
172                 if (!(model instanceof Model)) {
173                     var data = model,
174                         modelConstructor = this.get("model");
175                     model = new modelConstructor();
176                     ret = model.set(data, {
177                         silent:1
178                     });
179                 }
180                 return ret && model;
181             },
182 
183             /**
184              * Initialize model list by loading data using sync mechanism.
185              * @param {object} opts Load config.
186              * @param {Function} opts.success Callback when load is successful.
187              * @param {Function} opts.error Callback when error occurs on loading.
188              * @param {Function} opts.complete Callback when load is complete.
189              */
190             load:function (opts) {
191                 var self = this;
192                 opts = opts || {};
193                 var success = opts.success;
194                 /**
195                  * @ignore
196                  */
197                 opts.success = function (resp) {
198                     if (resp) {
199                         var v = self.get("parse").call(self, resp);
200                         if (v) {
201                             self.set("models", v, opts);
202                         }
203                     }
204                     // https://github.com/kissyteam/kissy/issues/138
205                     S.each(self.get("models"), function (m) {
206                         m.__isModified = 0;
207                     });
208                     success && success.apply(this, arguments);
209                 };
210                 self.get("sync").call(self, self, 'read', opts);
211                 return self;
212             },
213 
214             /**
215              * Add a model to current collection by provide json data.
216              * @param {Object} model Json data represent model data.
217              * @param {object} opts Create config.
218              * @param {Function} opts.success Callback when create is successful.
219              * @param {Function} opts.error Callback when error occurs on creating.
220              * @param {Function} opts.complete Callback when create is complete.
221              * @param {Function} opts.silent Whether to fire add event.
222              */
223             create:function (model, opts) {
224                 var self = this;
225                 opts = opts || {};
226                 model = this._normModel(model);
227                 if (model) {
228                     model.addToCollection(self);
229                     var success = opts.success;
230                     opts.success = function () {
231                         self.add(model, opts);
232                         success && success();
233                     };
234                     model.save(opts);
235                 }
236                 return model;
237             },
238 
239             _add:function (model, opts) {
240                 model = this._normModel(model);
241                 if (model) {
242                     opts = opts || {};
243                     var index = findModelIndex(this.get("models"), model, this.get("comparator"));
244                     this.get("models").splice(index, 0, model);
245                     model.addToCollection(this);
246                     if (!opts['silent']) {
247                         this.fire("add", {
248                             model:model
249                         });
250                     }
251                 }
252                 return model;
253             },
254 
255             /**
256              * not call model.destroy ,maybe model belongs to multiple collections
257              * @private
258              */
259             _remove:function (model, opts) {
260                 opts = opts || {};
261                 var index = S.indexOf(model, this.get("models"));
262                 if (index != -1) {
263                     this.get("models").splice(index, 1);
264                     model.removeFromCollection(this);
265                 }
266                 if (!opts['silent']) {
267                     this.fire("remove", {
268                         model:model
269                     });
270                 }
271             },
272 
273             /**
274              * Get model instance by id.
275              * @param {String} id
276              */
277             getById:function (id) {
278                 var models = this.get("models");
279                 for (var i = 0; i < models.length; i++) {
280                     var model = models[i];
281                     if (model.getId() === id) {
282                         return model;
283                     }
284                 }
285                 return null;
286             },
287 
288             /**
289              * Get model instance by client id.
290              * @param {String} cid Client id auto generated by model.
291              */
292             getByCid:function (cid) {
293                 var models = this.get("models");
294                 for (var i = 0; i < models.length; i++) {
295                     var model = models[i];
296                     if (model.get("clientId") === cid) {
297                         return model;
298                     }
299                 }
300                 return null;
301             }
302 
303         });
304 
305     return Collection;
306 
307 }, {
308     requires:['event', './model', 'base']
309 });