NOBUG: Fixed file access permissions
[moodle.git] / lib / yuilib / 3.13.0 / model-list / model-list.js
blob34aaa55819a565cce9162a89ad1e1c6a27132dd6
1 /*
2 YUI 3.13.0 (build 508226d)
3 Copyright 2013 Yahoo! Inc. All rights reserved.
4 Licensed under the BSD License.
5 http://yuilibrary.com/license/
6 */
8 YUI.add('model-list', function (Y, NAME) {
10 /**
11 Provides an API for managing an ordered list of Model instances.
13 @module app
14 @submodule model-list
15 @since 3.4.0
16 **/
18 /**
19 Provides an API for managing an ordered list of Model instances.
21 In addition to providing convenient `add`, `create`, `reset`, and `remove`
22 methods for managing the models in the list, ModelLists are also bubble targets
23 for events on the model instances they contain. This means, for example, that
24 you can add several models to a list, and then subscribe to the `*:change` event
25 on the list to be notified whenever any model in the list changes.
27 ModelLists also maintain sort order efficiently as models are added and removed,
28 based on a custom `comparator` function you may define (if no comparator is
29 defined, models are sorted in insertion order).
31 @class ModelList
32 @extends Base
33 @uses ArrayList
34 @constructor
35 @param {Object} config Config options.
36     @param {Model|Model[]|ModelList|Object|Object[]} config.items Model
37         instance, array of model instances, or ModelList to add to this list on
38         init. The `add` event will not be fired for models added on init.
39 @since 3.4.0
40 **/
42 var AttrProto = Y.Attribute.prototype,
43     Lang      = Y.Lang,
44     YArray    = Y.Array,
46     /**
47     Fired when a model is added to the list.
49     Listen to the `on` phase of this event to be notified before a model is
50     added to the list. Calling `e.preventDefault()` during the `on` phase will
51     prevent the model from being added.
53     Listen to the `after` phase of this event to be notified after a model has
54     been added to the list.
56     @event add
57     @param {Model} model The model being added.
58     @param {Number} index The index at which the model will be added.
59     @preventable _defAddFn
60     **/
61     EVT_ADD = 'add',
63     /**
64     Fired when a model is created or updated via the `create()` method, but
65     before the model is actually saved or added to the list. The `add` event
66     will be fired after the model has been saved and added to the list.
68     @event create
69     @param {Model} model The model being created/updated.
70     @since 3.5.0
71     **/
72     EVT_CREATE = 'create',
74     /**
75     Fired when an error occurs, such as when an attempt is made to add a
76     duplicate model to the list, or when a sync layer response can't be parsed.
78     @event error
79     @param {Any} error Error message, object, or exception generated by the
80       error. Calling `toString()` on this should result in a meaningful error
81       message.
82     @param {String} src Source of the error. May be one of the following (or any
83       custom error source defined by a ModelList subclass):
85       * `add`: Error while adding a model (probably because it's already in the
86          list and can't be added again). The model in question will be provided
87          as the `model` property on the event facade.
88       * `parse`: An error parsing a JSON response. The response in question will
89          be provided as the `response` property on the event facade.
90       * `remove`: Error while removing a model (probably because it isn't in the
91         list and can't be removed). The model in question will be provided as
92         the `model` property on the event facade.
93     **/
94     EVT_ERROR = 'error',
96     /**
97     Fired after models are loaded from a sync layer.
99     @event load
100     @param {Object} parsed The parsed version of the sync layer's response to
101         the load request.
102     @param {Mixed} response The sync layer's raw, unparsed response to the load
103         request.
104     @since 3.5.0
105     **/
106     EVT_LOAD = 'load',
108     /**
109     Fired when a model is removed from the list.
111     Listen to the `on` phase of this event to be notified before a model is
112     removed from the list. Calling `e.preventDefault()` during the `on` phase
113     will prevent the model from being removed.
115     Listen to the `after` phase of this event to be notified after a model has
116     been removed from the list.
118     @event remove
119     @param {Model} model The model being removed.
120     @param {Number} index The index of the model being removed.
121     @preventable _defRemoveFn
122     **/
123     EVT_REMOVE = 'remove',
125     /**
126     Fired when the list is completely reset via the `reset()` method or sorted
127     via the `sort()` method.
129     Listen to the `on` phase of this event to be notified before the list is
130     reset. Calling `e.preventDefault()` during the `on` phase will prevent
131     the list from being reset.
133     Listen to the `after` phase of this event to be notified after the list has
134     been reset.
136     @event reset
137     @param {Model[]} models Array of the list's new models after the reset.
138     @param {String} src Source of the event. May be either `'reset'` or
139       `'sort'`.
140     @preventable _defResetFn
141     **/
142     EVT_RESET = 'reset';
144 function ModelList() {
145     ModelList.superclass.constructor.apply(this, arguments);
148 Y.ModelList = Y.extend(ModelList, Y.Base, {
149     // -- Public Properties ----------------------------------------------------
151     /**
152     The `Model` class or subclass of the models in this list.
154     The class specified here will be used to create model instances
155     automatically based on attribute hashes passed to the `add()`, `create()`,
156     and `reset()` methods.
158     You may specify the class as an actual class reference or as a string that
159     resolves to a class reference at runtime (the latter can be useful if the
160     specified class will be loaded lazily).
162     @property model
163     @type Model|String
164     @default Y.Model
165     **/
166     model: Y.Model,
168     // -- Protected Properties -------------------------------------------------
170     /**
171     Total hack to allow us to identify ModelList instances without using
172     `instanceof`, which won't work when the instance was created in another
173     window or YUI sandbox.
175     @property _isYUIModelList
176     @type Boolean
177     @default true
178     @protected
179     @since 3.5.0
180     **/
181     _isYUIModelList: true,
183     // -- Lifecycle Methods ----------------------------------------------------
184     initializer: function (config) {
185         config || (config = {});
187         var model = this.model = config.model || this.model;
189         if (typeof model === 'string') {
190             // Look for a namespaced Model class on `Y`.
191             this.model = Y.Object.getValue(Y, model.split('.'));
193             if (!this.model) {
194                 Y.error('ModelList: Model class not found: ' + model);
195             }
196         }
198         this.publish(EVT_ADD,    {defaultFn: this._defAddFn});
199         this.publish(EVT_RESET,  {defaultFn: this._defResetFn});
200         this.publish(EVT_REMOVE, {defaultFn: this._defRemoveFn});
202         this.after('*:idChange', this._afterIdChange);
204         this._clear();
206         if (config.items) {
207             this.add(config.items, {silent: true});
208         }
209     },
211     destructor: function () {
212         this._clear();
213     },
215     // -- Public Methods -------------------------------------------------------
217     /**
218     Adds the specified model or array of models to this list. You may also pass
219     another ModelList instance, in which case all the models in that list will
220     be added to this one as well.
222     @example
224         // Add a single model instance.
225         list.add(new Model({foo: 'bar'}));
227         // Add a single model, creating a new instance automatically.
228         list.add({foo: 'bar'});
230         // Add multiple models, creating new instances automatically.
231         list.add([
232             {foo: 'bar'},
233             {baz: 'quux'}
234         ]);
236         // Add all the models in another ModelList instance.
237         list.add(otherList);
239     @method add
240     @param {Model|Model[]|ModelList|Object|Object[]} models Model or array of
241         models to add. May be existing model instances or hashes of model
242         attributes, in which case new model instances will be created from the
243         hashes. You may also pass a ModelList instance to add all the models it
244         contains.
245     @param {Object} [options] Data to be mixed into the event facade of the
246         `add` event(s) for the added models.
248         @param {Number} [options.index] Index at which to insert the added
249             models. If not specified, the models will automatically be inserted
250             in the appropriate place according to the current sort order as
251             dictated by the `comparator()` method, if any.
252         @param {Boolean} [options.silent=false] If `true`, no `add` event(s)
253             will be fired.
255     @return {Model|Model[]} Added model or array of added models.
256     **/
257     add: function (models, options) {
258         var isList = models._isYUIModelList;
260         if (isList || Lang.isArray(models)) {
261             return YArray.map(isList ? models.toArray() : models, function (model, index) {
262                 var modelOptions = options || {};
264                 // When an explicit insertion index is specified, ensure that
265                 // the index is increased by one for each subsequent item in the
266                 // array.
267                 if ('index' in modelOptions) {
268                     modelOptions = Y.merge(modelOptions, {
269                         index: modelOptions.index + index
270                     });
271                 }
273                 return this._add(model, modelOptions);
274             }, this);
275         } else {
276             return this._add(models, options);
277         }
278     },
280     /**
281     Define this method to provide a function that takes a model as a parameter
282     and returns a value by which that model should be sorted relative to other
283     models in this list.
285     By default, no comparator is defined, meaning that models will not be sorted
286     (they'll be stored in the order they're added).
288     @example
289         var list = new Y.ModelList({model: Y.Model});
291         list.comparator = function (model) {
292             return model.get('id'); // Sort models by id.
293         };
295     @method comparator
296     @param {Model} model Model being sorted.
297     @return {Number|String} Value by which the model should be sorted relative
298       to other models in this list.
299     **/
301     // comparator is not defined by default
303     /**
304     Creates or updates the specified model on the server, then adds it to this
305     list if the server indicates success.
307     @method create
308     @param {Model|Object} model Model to create. May be an existing model
309       instance or a hash of model attributes, in which case a new model instance
310       will be created from the hash.
311     @param {Object} [options] Options to be passed to the model's `sync()` and
312         `set()` methods and mixed into the `create` and `add` event facades.
313       @param {Boolean} [options.silent=false] If `true`, no `add` event(s) will
314           be fired.
315     @param {Function} [callback] Called when the sync operation finishes.
316       @param {Error} callback.err If an error occurred, this parameter will
317         contain the error. If the sync operation succeeded, _err_ will be
318         falsy.
319       @param {Any} callback.response The server's response.
320     @return {Model} Created model.
321     **/
322     create: function (model, options, callback) {
323         var self = this;
325         // Allow callback as second arg.
326         if (typeof options === 'function') {
327             callback = options;
328             options  = {};
329         }
331         options || (options = {});
333         if (!model._isYUIModel) {
334             model = new this.model(model);
335         }
337         self.fire(EVT_CREATE, Y.merge(options, {
338             model: model
339         }));
341         return model.save(options, function (err) {
342             if (!err) {
343                 self.add(model, options);
344             }
346             if (callback) {
347                 callback.apply(null, arguments);
348             }
349         });
350     },
352     /**
353     Executes the supplied function on each model in this list.
355     By default, the callback function's `this` object will refer to the model
356     currently being iterated. Specify a `thisObj` to override the `this` object
357     if desired.
359     Note: Iteration is performed on a copy of the internal array of models, so
360     it's safe to delete a model from the list during iteration.
362     @method each
363     @param {Function} callback Function to execute on each model.
364         @param {Model} callback.model Model instance.
365         @param {Number} callback.index Index of the current model.
366         @param {ModelList} callback.list The ModelList being iterated.
367     @param {Object} [thisObj] Object to use as the `this` object when executing
368         the callback.
369     @chainable
370     @since 3.6.0
371     **/
372     each: function (callback, thisObj) {
373         var items = this._items.concat(),
374             i, item, len;
376         for (i = 0, len = items.length; i < len; i++) {
377             item = items[i];
378             callback.call(thisObj || item, item, i, this);
379         }
381         return this;
382     },
384     /**
385     Executes the supplied function on each model in this list. Returns an array
386     containing the models for which the supplied function returned a truthy
387     value.
389     The callback function's `this` object will refer to this ModelList. Use
390     `Y.bind()` to bind the `this` object to another object if desired.
392     @example
394         // Get an array containing only the models whose "enabled" attribute is
395         // truthy.
396         var filtered = list.filter(function (model) {
397             return model.get('enabled');
398         });
400         // Get a new ModelList containing only the models whose "enabled"
401         // attribute is truthy.
402         var filteredList = list.filter({asList: true}, function (model) {
403             return model.get('enabled');
404         });
406     @method filter
407     @param {Object} [options] Filter options.
408         @param {Boolean} [options.asList=false] If truthy, results will be
409             returned as a new ModelList instance rather than as an array.
411     @param {Function} callback Function to execute on each model.
412         @param {Model} callback.model Model instance.
413         @param {Number} callback.index Index of the current model.
414         @param {ModelList} callback.list The ModelList being filtered.
416     @return {Array|ModelList} Array of models for which the callback function
417         returned a truthy value (empty if it never returned a truthy value). If
418         the `options.asList` option is truthy, a new ModelList instance will be
419         returned instead of an array.
420     @since 3.5.0
421     */
422     filter: function (options, callback) {
423         var filtered = [],
424             items    = this._items,
425             i, item, len, list;
427         // Allow options as first arg.
428         if (typeof options === 'function') {
429             callback = options;
430             options  = {};
431         }
433         for (i = 0, len = items.length; i < len; ++i) {
434             item = items[i];
436             if (callback.call(this, item, i, this)) {
437                 filtered.push(item);
438             }
439         }
441         if (options.asList) {
442             list = new this.constructor({model: this.model});
444             if (filtered.length) {
445                 list.add(filtered, {silent: true});
446             }
448             return list;
449         } else {
450             return filtered;
451         }
452     },
454     /**
455     If _name_ refers to an attribute on this ModelList instance, returns the
456     value of that attribute. Otherwise, returns an array containing the values
457     of the specified attribute from each model in this list.
459     @method get
460     @param {String} name Attribute name or object property path.
461     @return {Any|Array} Attribute value or array of attribute values.
462     @see Model.get()
463     **/
464     get: function (name) {
465         if (this.attrAdded(name)) {
466             return AttrProto.get.apply(this, arguments);
467         }
469         return this.invoke('get', name);
470     },
472     /**
473     If _name_ refers to an attribute on this ModelList instance, returns the
474     HTML-escaped value of that attribute. Otherwise, returns an array containing
475     the HTML-escaped values of the specified attribute from each model in this
476     list.
478     The values are escaped using `Escape.html()`.
480     @method getAsHTML
481     @param {String} name Attribute name or object property path.
482     @return {String|String[]} HTML-escaped value or array of HTML-escaped
483       values.
484     @see Model.getAsHTML()
485     **/
486     getAsHTML: function (name) {
487         if (this.attrAdded(name)) {
488             return Y.Escape.html(AttrProto.get.apply(this, arguments));
489         }
491         return this.invoke('getAsHTML', name);
492     },
494     /**
495     If _name_ refers to an attribute on this ModelList instance, returns the
496     URL-encoded value of that attribute. Otherwise, returns an array containing
497     the URL-encoded values of the specified attribute from each model in this
498     list.
500     The values are encoded using the native `encodeURIComponent()` function.
502     @method getAsURL
503     @param {String} name Attribute name or object property path.
504     @return {String|String[]} URL-encoded value or array of URL-encoded values.
505     @see Model.getAsURL()
506     **/
507     getAsURL: function (name) {
508         if (this.attrAdded(name)) {
509             return encodeURIComponent(AttrProto.get.apply(this, arguments));
510         }
512         return this.invoke('getAsURL', name);
513     },
515     /**
516     Returns the model with the specified _clientId_, or `null` if not found.
518     @method getByClientId
519     @param {String} clientId Client id.
520     @return {Model} Model, or `null` if not found.
521     **/
522     getByClientId: function (clientId) {
523         return this._clientIdMap[clientId] || null;
524     },
526     /**
527     Returns the model with the specified _id_, or `null` if not found.
529     Note that models aren't expected to have an id until they're saved, so if
530     you're working with unsaved models, it may be safer to call
531     `getByClientId()`.
533     @method getById
534     @param {String|Number} id Model id.
535     @return {Model} Model, or `null` if not found.
536     **/
537     getById: function (id) {
538         return this._idMap[id] || null;
539     },
541     /**
542     Calls the named method on every model in the list. Any arguments provided
543     after _name_ will be passed on to the invoked method.
545     @method invoke
546     @param {String} name Name of the method to call on each model.
547     @param {Any} [args*] Zero or more arguments to pass to the invoked method.
548     @return {Array} Array of return values, indexed according to the index of
549       the model on which the method was called.
550     **/
551     invoke: function (name /*, args* */) {
552         var args = [this._items, name].concat(YArray(arguments, 1, true));
553         return YArray.invoke.apply(YArray, args);
554     },
556     /**
557     Returns the model at the specified _index_.
559     @method item
560     @param {Number} index Index of the model to fetch.
561     @return {Model} The model at the specified index, or `undefined` if there
562       isn't a model there.
563     **/
565     // item() is inherited from ArrayList.
567     /**
568     Loads this list of models from the server.
570     This method delegates to the `sync()` method to perform the actual load
571     operation, which is an asynchronous action. Specify a _callback_ function to
572     be notified of success or failure.
574     If the load operation succeeds, a `reset` event will be fired.
576     @method load
577     @param {Object} [options] Options to be passed to `sync()` and to
578       `reset()` when adding the loaded models. It's up to the custom sync
579       implementation to determine what options it supports or requires, if any.
580     @param {Function} [callback] Called when the sync operation finishes.
581       @param {Error} callback.err If an error occurred, this parameter will
582         contain the error. If the sync operation succeeded, _err_ will be
583         falsy.
584       @param {Any} callback.response The server's response. This value will
585         be passed to the `parse()` method, which is expected to parse it and
586         return an array of model attribute hashes.
587     @chainable
588     **/
589     load: function (options, callback) {
590         var self = this;
592         // Allow callback as only arg.
593         if (typeof options === 'function') {
594             callback = options;
595             options  = {};
596         }
598         options || (options = {});
600         this.sync('read', options, function (err, response) {
601             var facade = {
602                     options : options,
603                     response: response
604                 },
606                 parsed;
608             if (err) {
609                 facade.error = err;
610                 facade.src   = 'load';
612                 self.fire(EVT_ERROR, facade);
613             } else {
614                 // Lazy publish.
615                 if (!self._loadEvent) {
616                     self._loadEvent = self.publish(EVT_LOAD, {
617                         preventable: false
618                     });
619                 }
621                 parsed = facade.parsed = self._parse(response);
623                 self.reset(parsed, options);
624                 self.fire(EVT_LOAD, facade);
625             }
627             if (callback) {
628                 callback.apply(null, arguments);
629             }
630         });
632         return this;
633     },
635     /**
636     Executes the specified function on each model in this list and returns an
637     array of the function's collected return values.
639     @method map
640     @param {Function} fn Function to execute on each model.
641       @param {Model} fn.model Current model being iterated.
642       @param {Number} fn.index Index of the current model in the list.
643       @param {Model[]} fn.models Array of models being iterated.
644     @param {Object} [thisObj] `this` object to use when calling _fn_.
645     @return {Array} Array of return values from _fn_.
646     **/
647     map: function (fn, thisObj) {
648         return YArray.map(this._items, fn, thisObj);
649     },
651     /**
652     Called to parse the _response_ when the list is loaded from the server.
653     This method receives a server _response_ and is expected to return an array
654     of model attribute hashes.
656     The default implementation assumes that _response_ is either an array of
657     attribute hashes or a JSON string that can be parsed into an array of
658     attribute hashes. If _response_ is a JSON string and either `Y.JSON` or the
659     native `JSON` object are available, it will be parsed automatically. If a
660     parse error occurs, an `error` event will be fired and the model will not be
661     updated.
663     You may override this method to implement custom parsing logic if necessary.
665     @method parse
666     @param {Any} response Server response.
667     @return {Object[]} Array of model attribute hashes.
668     **/
669     parse: function (response) {
670         if (typeof response === 'string') {
671             try {
672                 return Y.JSON.parse(response) || [];
673             } catch (ex) {
674                 this.fire(EVT_ERROR, {
675                     error   : ex,
676                     response: response,
677                     src     : 'parse'
678                 });
680                 return null;
681             }
682         }
684         return response || [];
685     },
687     /**
688     Removes the specified model or array of models from this list. You may also
689     pass another ModelList instance to remove all the models that are in both
690     that instance and this instance, or pass numerical indices to remove the
691     models at those indices.
693     @method remove
694     @param {Model|Model[]|ModelList|Number|Number[]} models Models or indices of
695         models to remove.
696     @param {Object} [options] Data to be mixed into the event facade of the
697         `remove` event(s) for the removed models.
699         @param {Boolean} [options.silent=false] If `true`, no `remove` event(s)
700             will be fired.
702     @return {Model|Model[]} Removed model or array of removed models.
703     **/
704     remove: function (models, options) {
705         var isList = models._isYUIModelList;
707         if (isList || Lang.isArray(models)) {
708             // We can't remove multiple models by index because the indices will
709             // change as we remove them, so we need to get the actual models
710             // first.
711             models = YArray.map(isList ? models.toArray() : models, function (model) {
712                 if (Lang.isNumber(model)) {
713                     return this.item(model);
714                 }
716                 return model;
717             }, this);
719             return YArray.map(models, function (model) {
720                 return this._remove(model, options);
721             }, this);
722         } else {
723             return this._remove(models, options);
724         }
725     },
727     /**
728     Completely replaces all models in the list with those specified, and fires a
729     single `reset` event.
731     Use `reset` when you want to add or remove a large number of items at once
732     with less overhead, and without firing `add` or `remove` events for each
733     one.
735     @method reset
736     @param {Model[]|ModelList|Object[]} [models] Models to add. May be existing
737         model instances or hashes of model attributes, in which case new model
738         instances will be created from the hashes. If a ModelList is passed, all
739         the models in that list will be added to this list. Calling `reset()`
740         without passing in any models will clear the list.
741     @param {Object} [options] Data to be mixed into the event facade of the
742         `reset` event.
744         @param {Boolean} [options.silent=false] If `true`, no `reset` event will
745             be fired.
747     @chainable
748     **/
749     reset: function (models, options) {
750         models  || (models  = []);
751         options || (options = {});
753         var facade = Y.merge({src: 'reset'}, options);
755         if (models._isYUIModelList) {
756             models = models.toArray();
757         } else {
758             models = YArray.map(models, function (model) {
759                 return model._isYUIModel ? model : new this.model(model);
760             }, this);
761         }
763         facade.models = models;
765         if (options.silent) {
766             this._defResetFn(facade);
767         } else {
768             // Sort the models before firing the reset event.
769             if (this.comparator) {
770                 models.sort(Y.bind(this._sort, this));
771             }
773             this.fire(EVT_RESET, facade);
774         }
776         return this;
777     },
779     /**
780     Executes the supplied function on each model in this list, and stops
781     iterating if the callback returns `true`.
783     By default, the callback function's `this` object will refer to the model
784     currently being iterated. Specify a `thisObj` to override the `this` object
785     if desired.
787     Note: Iteration is performed on a copy of the internal array of models, so
788     it's safe to delete a model from the list during iteration.
790     @method some
791     @param {Function} callback Function to execute on each model.
792         @param {Model} callback.model Model instance.
793         @param {Number} callback.index Index of the current model.
794         @param {ModelList} callback.list The ModelList being iterated.
795     @param {Object} [thisObj] Object to use as the `this` object when executing
796         the callback.
797     @return {Boolean} `true` if the callback returned `true` for any item,
798         `false` otherwise.
799     @since 3.6.0
800     **/
801     some: function (callback, thisObj) {
802         var items = this._items.concat(),
803             i, item, len;
805         for (i = 0, len = items.length; i < len; i++) {
806             item = items[i];
808             if (callback.call(thisObj || item, item, i, this)) {
809                 return true;
810             }
811         }
813         return false;
814     },
816     /**
817     Forcibly re-sorts the list.
819     Usually it shouldn't be necessary to call this method since the list
820     maintains its sort order when items are added and removed, but if you change
821     the `comparator` function after items are already in the list, you'll need
822     to re-sort.
824     @method sort
825     @param {Object} [options] Data to be mixed into the event facade of the
826         `reset` event.
827       @param {Boolean} [options.silent=false] If `true`, no `reset` event will
828           be fired.
829       @param {Boolean} [options.descending=false] If `true`, the sort is
830           performed in descending order.
831     @chainable
832     **/
833     sort: function (options) {
834         if (!this.comparator) {
835             return this;
836         }
838         var models = this._items.concat(),
839             facade;
841         options || (options = {});
843         models.sort(Y.rbind(this._sort, this, options));
845         facade = Y.merge(options, {
846             models: models,
847             src   : 'sort'
848         });
850         if (options.silent) {
851             this._defResetFn(facade);
852         } else {
853             this.fire(EVT_RESET, facade);
854         }
856         return this;
857     },
859     /**
860     Override this method to provide a custom persistence implementation for this
861     list. The default method just calls the callback without actually doing
862     anything.
864     This method is called internally by `load()` and its implementations relies
865     on the callback being called. This effectively means that when a callback is
866     provided, it must be called at some point for the class to operate correctly.
868     @method sync
869     @param {String} action Sync action to perform. May be one of the following:
871       * `create`: Store a list of newly-created models for the first time.
872       * `delete`: Delete a list of existing models.
873       * `read`  : Load a list of existing models.
874       * `update`: Update a list of existing models.
876       Currently, model lists only make use of the `read` action, but other
877       actions may be used in future versions.
879     @param {Object} [options] Sync options. It's up to the custom sync
880       implementation to determine what options it supports or requires, if any.
881     @param {Function} [callback] Called when the sync operation finishes.
882       @param {Error} callback.err If an error occurred, this parameter will
883         contain the error. If the sync operation succeeded, _err_ will be
884         falsy.
885       @param {Any} [callback.response] The server's response. This value will
886         be passed to the `parse()` method, which is expected to parse it and
887         return an array of model attribute hashes.
888     **/
889     sync: function (/* action, options, callback */) {
890         var callback = YArray(arguments, 0, true).pop();
892         if (typeof callback === 'function') {
893             callback();
894         }
895     },
897     /**
898     Returns an array containing the models in this list.
900     @method toArray
901     @return {Array} Array containing the models in this list.
902     **/
903     toArray: function () {
904         return this._items.concat();
905     },
907     /**
908     Returns an array containing attribute hashes for each model in this list,
909     suitable for being passed to `Y.JSON.stringify()`.
911     Under the hood, this method calls `toJSON()` on each model in the list and
912     pushes the results into an array.
914     @method toJSON
915     @return {Object[]} Array of model attribute hashes.
916     @see Model.toJSON()
917     **/
918     toJSON: function () {
919         return this.map(function (model) {
920             return model.toJSON();
921         });
922     },
924     // -- Protected Methods ----------------------------------------------------
926     /**
927     Adds the specified _model_ if it isn't already in this list.
929     If the model's `clientId` or `id` matches that of a model that's already in
930     the list, an `error` event will be fired and the model will not be added.
932     @method _add
933     @param {Model|Object} model Model or object to add.
934     @param {Object} [options] Data to be mixed into the event facade of the
935         `add` event for the added model.
936       @param {Boolean} [options.silent=false] If `true`, no `add` event will be
937           fired.
938     @return {Model} The added model.
939     @protected
940     **/
941     _add: function (model, options) {
942         var facade, id;
944         options || (options = {});
946         if (!model._isYUIModel) {
947             model = new this.model(model);
948         }
950         id = model.get('id');
952         if (this._clientIdMap[model.get('clientId')]
953                 || (Lang.isValue(id) && this._idMap[id])) {
955             this.fire(EVT_ERROR, {
956                 error: 'Model is already in the list.',
957                 model: model,
958                 src  : 'add'
959             });
961             return;
962         }
964         facade = Y.merge(options, {
965             index: 'index' in options ? options.index : this._findIndex(model),
966             model: model
967         });
969         if (options.silent) {
970             this._defAddFn(facade);
971         } else {
972             this.fire(EVT_ADD, facade);
973         }
975         return model;
976     },
978     /**
979     Adds this list as a bubble target for the specified model's events.
981     @method _attachList
982     @param {Model} model Model to attach to this list.
983     @protected
984     **/
985     _attachList: function (model) {
986         // Attach this list and make it a bubble target for the model.
987         model.lists.push(this);
988         model.addTarget(this);
989     },
991     /**
992     Clears all internal state and the internal list of models, returning this
993     list to an empty state. Automatically detaches all models in the list.
995     @method _clear
996     @protected
997     **/
998     _clear: function () {
999         YArray.each(this._items, this._detachList, this);
1001         this._clientIdMap = {};
1002         this._idMap       = {};
1003         this._items       = [];
1004     },
1006     /**
1007     Compares the value _a_ to the value _b_ for sorting purposes. Values are
1008     assumed to be the result of calling a model's `comparator()` method. You can
1009     override this method to implement custom sorting logic, such as a descending
1010     sort or multi-field sorting.
1012     @method _compare
1013     @param {Mixed} a First value to compare.
1014     @param {Mixed} b Second value to compare.
1015     @return {Number} `-1` if _a_ should come before _b_, `0` if they're
1016         equivalent, `1` if _a_ should come after _b_.
1017     @protected
1018     @since 3.5.0
1019     **/
1020     _compare: function (a, b) {
1021         return a < b ? -1 : (a > b ? 1 : 0);
1022     },
1024     /**
1025     Removes this list as a bubble target for the specified model's events.
1027     @method _detachList
1028     @param {Model} model Model to detach.
1029     @protected
1030     **/
1031     _detachList: function (model) {
1032         var index = YArray.indexOf(model.lists, this);
1034         if (index > -1) {
1035             model.lists.splice(index, 1);
1036             model.removeTarget(this);
1037         }
1038     },
1040     /**
1041     Returns the index at which the given _model_ should be inserted to maintain
1042     the sort order of the list.
1044     @method _findIndex
1045     @param {Model} model The model being inserted.
1046     @return {Number} Index at which the model should be inserted.
1047     @protected
1048     **/
1049     _findIndex: function (model) {
1050         var items = this._items,
1051             max   = items.length,
1052             min   = 0,
1053             item, middle, needle;
1055         if (!this.comparator || !max) {
1056             return max;
1057         }
1059         needle = this.comparator(model);
1061         // Perform an iterative binary search to determine the correct position
1062         // based on the return value of the `comparator` function.
1063         while (min < max) {
1064             middle = (min + max) >> 1; // Divide by two and discard remainder.
1065             item   = items[middle];
1067             if (this._compare(this.comparator(item), needle) < 0) {
1068                 min = middle + 1;
1069             } else {
1070                 max = middle;
1071             }
1072         }
1074         return min;
1075     },
1077     /**
1078     Calls the public, overrideable `parse()` method and returns the result.
1080     Override this method to provide a custom pre-parsing implementation. This
1081     provides a hook for custom persistence implementations to "prep" a response
1082     before calling the `parse()` method.
1084     @method _parse
1085     @param {Any} response Server response.
1086     @return {Object[]} Array of model attribute hashes.
1087     @protected
1088     @see ModelList.parse()
1089     @since 3.7.0
1090     **/
1091     _parse: function (response) {
1092         return this.parse(response);
1093     },
1095     /**
1096     Removes the specified _model_ if it's in this list.
1098     @method _remove
1099     @param {Model|Number} model Model or index of the model to remove.
1100     @param {Object} [options] Data to be mixed into the event facade of the
1101         `remove` event for the removed model.
1102       @param {Boolean} [options.silent=false] If `true`, no `remove` event will
1103           be fired.
1104     @return {Model} Removed model.
1105     @protected
1106     **/
1107     _remove: function (model, options) {
1108         var index, facade;
1110         options || (options = {});
1112         if (Lang.isNumber(model)) {
1113             index = model;
1114             model = this.item(index);
1115         } else {
1116             index = this.indexOf(model);
1117         }
1119         if (index === -1 || !model) {
1120             this.fire(EVT_ERROR, {
1121                 error: 'Model is not in the list.',
1122                 index: index,
1123                 model: model,
1124                 src  : 'remove'
1125             });
1127             return;
1128         }
1130         facade = Y.merge(options, {
1131             index: index,
1132             model: model
1133         });
1135         if (options.silent) {
1136             this._defRemoveFn(facade);
1137         } else {
1138             this.fire(EVT_REMOVE, facade);
1139         }
1141         return model;
1142     },
1144     /**
1145     Array sort function used by `sort()` to re-sort the models in the list.
1147     @method _sort
1148     @param {Model} a First model to compare.
1149     @param {Model} b Second model to compare.
1150     @param {Object} [options] Options passed from `sort()` function.
1151         @param {Boolean} [options.descending=false] If `true`, the sort is
1152           performed in descending order.
1153     @return {Number} `-1` if _a_ is less than _b_, `0` if equal, `1` if greater
1154       (for ascending order, the reverse for descending order).
1155     @protected
1156     **/
1157     _sort: function (a, b, options) {
1158         var result = this._compare(this.comparator(a), this.comparator(b));
1160         // Early return when items are equal in their sort comparison.
1161         if (result === 0) {
1162             return result;
1163         }
1165         // Flips sign when the sort is to be peformed in descending order.
1166         return options && options.descending ? -result : result;
1167     },
1169     // -- Event Handlers -------------------------------------------------------
1171     /**
1172     Updates the model maps when a model's `id` attribute changes.
1174     @method _afterIdChange
1175     @param {EventFacade} e
1176     @protected
1177     **/
1178     _afterIdChange: function (e) {
1179         var newVal  = e.newVal,
1180             prevVal = e.prevVal,
1181             target  = e.target;
1183         if (Lang.isValue(prevVal)) {
1184             if (this._idMap[prevVal] === target) {
1185                 delete this._idMap[prevVal];
1186             } else {
1187                 // The model that changed isn't in this list. Probably just a
1188                 // bubbled change event from a nested Model List.
1189                 return;
1190             }
1191         } else {
1192             // The model had no previous id. Verify that it exists in this list
1193             // before continuing.
1194             if (this.indexOf(target) === -1) {
1195                 return;
1196             }
1197         }
1199         if (Lang.isValue(newVal)) {
1200             this._idMap[newVal] = target;
1201         }
1202     },
1204     // -- Default Event Handlers -----------------------------------------------
1206     /**
1207     Default event handler for `add` events.
1209     @method _defAddFn
1210     @param {EventFacade} e
1211     @protected
1212     **/
1213     _defAddFn: function (e) {
1214         var model = e.model,
1215             id    = model.get('id');
1217         this._clientIdMap[model.get('clientId')] = model;
1219         if (Lang.isValue(id)) {
1220             this._idMap[id] = model;
1221         }
1223         this._attachList(model);
1224         this._items.splice(e.index, 0, model);
1225     },
1227     /**
1228     Default event handler for `remove` events.
1230     @method _defRemoveFn
1231     @param {EventFacade} e
1232     @protected
1233     **/
1234     _defRemoveFn: function (e) {
1235         var model = e.model,
1236             id    = model.get('id');
1238         this._detachList(model);
1239         delete this._clientIdMap[model.get('clientId')];
1241         if (Lang.isValue(id)) {
1242             delete this._idMap[id];
1243         }
1245         this._items.splice(e.index, 1);
1246     },
1248     /**
1249     Default event handler for `reset` events.
1251     @method _defResetFn
1252     @param {EventFacade} e
1253     @protected
1254     **/
1255     _defResetFn: function (e) {
1256         // When fired from the `sort` method, we don't need to clear the list or
1257         // add any models, since the existing models are sorted in place.
1258         if (e.src === 'sort') {
1259             this._items = e.models.concat();
1260             return;
1261         }
1263         this._clear();
1265         if (e.models.length) {
1266             this.add(e.models, {silent: true});
1267         }
1268     }
1269 }, {
1270     NAME: 'modelList'
1273 Y.augment(ModelList, Y.ArrayList);
1276 }, '3.13.0', {"requires": ["array-extras", "array-invoke", "arraylist", "base-build", "escape", "json-parse", "model"]});