MDL-32843 import YUI 3.5.1
[moodle.git] / lib / yui / 3.5.1 / build / model-list / model-list-debug.js
blobcb45d164f9c30749d9dbd63964894d2a38e99c48
1 /*
2 YUI 3.5.1 (build 22)
3 Copyright 2012 Yahoo! Inc. All rights reserved.
4 Licensed under the BSD License.
5 http://yuilibrary.com/license/
6 */
7 YUI.add('model-list', function(Y) {
9 /**
10 Provides an API for managing an ordered list of Model instances.
12 @module app
13 @submodule model-list
14 @since 3.4.0
15 **/
17 /**
18 Provides an API for managing an ordered list of Model instances.
20 In addition to providing convenient `add`, `create`, `reset`, and `remove`
21 methods for managing the models in the list, ModelLists are also bubble targets
22 for events on the model instances they contain. This means, for example, that
23 you can add several models to a list, and then subscribe to the `*:change` event
24 on the list to be notified whenever any model in the list changes.
26 ModelLists also maintain sort order efficiently as models are added and removed,
27 based on a custom `comparator` function you may define (if no comparator is
28 defined, models are sorted in insertion order).
30 @class ModelList
31 @extends Base
32 @uses ArrayList
33 @constructor
34 @since 3.4.0
35 **/
37 var AttrProto = Y.Attribute.prototype,
38     Lang      = Y.Lang,
39     YArray    = Y.Array,
41     /**
42     Fired when a model is added to the list.
44     Listen to the `on` phase of this event to be notified before a model is
45     added to the list. Calling `e.preventDefault()` during the `on` phase will
46     prevent the model from being added.
48     Listen to the `after` phase of this event to be notified after a model has
49     been added to the list.
51     @event add
52     @param {Model} model The model being added.
53     @param {Number} index The index at which the model will be added.
54     @preventable _defAddFn
55     **/
56     EVT_ADD = 'add',
58     /**
59     Fired when a model is created or updated via the `create()` method, but
60     before the model is actually saved or added to the list. The `add` event
61     will be fired after the model has been saved and added to the list.
63     @event create
64     @param {Model} model The model being created/updated.
65     @since 3.5.0
66     **/
67     EVT_CREATE = 'create',
69     /**
70     Fired when an error occurs, such as when an attempt is made to add a
71     duplicate model to the list, or when a sync layer response can't be parsed.
73     @event error
74     @param {Any} error Error message, object, or exception generated by the
75       error. Calling `toString()` on this should result in a meaningful error
76       message.
77     @param {String} src Source of the error. May be one of the following (or any
78       custom error source defined by a ModelList subclass):
80       * `add`: Error while adding a model (probably because it's already in the
81          list and can't be added again). The model in question will be provided
82          as the `model` property on the event facade.
83       * `parse`: An error parsing a JSON response. The response in question will
84          be provided as the `response` property on the event facade.
85       * `remove`: Error while removing a model (probably because it isn't in the
86         list and can't be removed). The model in question will be provided as
87         the `model` property on the event facade.
88     **/
89     EVT_ERROR = 'error',
91     /**
92     Fired after models are loaded from a sync layer.
94     @event load
95     @param {Object} parsed The parsed version of the sync layer's response to
96         the load request.
97     @param {Mixed} response The sync layer's raw, unparsed response to the load
98         request.
99     @since 3.5.0
100     **/
101     EVT_LOAD = 'load',
103     /**
104     Fired when a model is removed from the list.
106     Listen to the `on` phase of this event to be notified before a model is
107     removed from the list. Calling `e.preventDefault()` during the `on` phase
108     will prevent the model from being removed.
110     Listen to the `after` phase of this event to be notified after a model has
111     been removed from the list.
113     @event remove
114     @param {Model} model The model being removed.
115     @param {Number} index The index of the model being removed.
116     @preventable _defRemoveFn
117     **/
118     EVT_REMOVE = 'remove',
120     /**
121     Fired when the list is completely reset via the `reset()` method or sorted
122     via the `sort()` method.
124     Listen to the `on` phase of this event to be notified before the list is
125     reset. Calling `e.preventDefault()` during the `on` phase will prevent
126     the list from being reset.
128     Listen to the `after` phase of this event to be notified after the list has
129     been reset.
131     @event reset
132     @param {Model[]} models Array of the list's new models after the reset.
133     @param {String} src Source of the event. May be either `'reset'` or
134       `'sort'`.
135     @preventable _defResetFn
136     **/
137     EVT_RESET = 'reset';
139 function ModelList() {
140     ModelList.superclass.constructor.apply(this, arguments);
143 Y.ModelList = Y.extend(ModelList, Y.Base, {
144     // -- Public Properties ----------------------------------------------------
146     /**
147     The `Model` class or subclass of the models in this list.
149     The class specified here will be used to create model instances
150     automatically based on attribute hashes passed to the `add()`, `create()`,
151     and `reset()` methods.
153     You may specify the class as an actual class reference or as a string that
154     resolves to a class reference at runtime (the latter can be useful if the
155     specified class will be loaded lazily).
157     @property model
158     @type Model|String
159     @default Y.Model
160     **/
161     model: Y.Model,
163     // -- Protected Properties -------------------------------------------------
165     /**
166     Total hack to allow us to identify ModelList instances without using
167     `instanceof`, which won't work when the instance was created in another
168     window or YUI sandbox.
170     @property _isYUIModelList
171     @type Boolean
172     @default true
173     @protected
174     @since 3.5.0
175     **/
176     _isYUIModelList: true,
178     // -- Lifecycle Methods ----------------------------------------------------
179     initializer: function (config) {
180         config || (config = {});
182         var model = this.model = config.model || this.model;
184         if (typeof model === 'string') {
185             // Look for a namespaced Model class on `Y`.
186             this.model = Y.Object.getValue(Y, model.split('.'));
188             if (!this.model) {
189                 Y.error('ModelList: Model class not found: ' + model);
190             }
191         }
193         this.publish(EVT_ADD,    {defaultFn: this._defAddFn});
194         this.publish(EVT_RESET,  {defaultFn: this._defResetFn});
195         this.publish(EVT_REMOVE, {defaultFn: this._defRemoveFn});
197         this.after('*:idChange', this._afterIdChange);
199         this._clear();
200     },
202     destructor: function () {
203         YArray.each(this._items, this._detachList, this);
204     },
206     // -- Public Methods -------------------------------------------------------
208     /**
209     Adds the specified model or array of models to this list. You may also pass
210     another ModelList instance, in which case all the models in that list will
211     be added to this one as well.
213     @example
215         // Add a single model instance.
216         list.add(new Model({foo: 'bar'}));
218         // Add a single model, creating a new instance automatically.
219         list.add({foo: 'bar'});
221         // Add multiple models, creating new instances automatically.
222         list.add([
223             {foo: 'bar'},
224             {baz: 'quux'}
225         ]);
227         // Add all the models in another ModelList instance.
228         list.add(otherList);
230     @method add
231     @param {Model|Model[]|ModelList|Object|Object[]} models Model or array of
232         models to add. May be existing model instances or hashes of model
233         attributes, in which case new model instances will be created from the
234         hashes. You may also pass a ModelList instance to add all the models it
235         contains.
236     @param {Object} [options] Data to be mixed into the event facade of the
237         `add` event(s) for the added models.
239         @param {Boolean} [options.silent=false] If `true`, no `add` event(s)
240             will be fired.
242     @return {Model|Model[]} Added model or array of added models.
243     **/
244     add: function (models, options) {
245         var isList = models._isYUIModelList;
247         if (isList || Lang.isArray(models)) {
248             return YArray.map(isList ? models.toArray() : models, function (model) {
249                 return this._add(model, options);
250             }, this);
251         } else {
252             return this._add(models, options);
253         }
254     },
256     /**
257     Define this method to provide a function that takes a model as a parameter
258     and returns a value by which that model should be sorted relative to other
259     models in this list.
261     By default, no comparator is defined, meaning that models will not be sorted
262     (they'll be stored in the order they're added).
264     @example
265         var list = new Y.ModelList({model: Y.Model});
267         list.comparator = function (model) {
268             return model.get('id'); // Sort models by id.
269         };
271     @method comparator
272     @param {Model} model Model being sorted.
273     @return {Number|String} Value by which the model should be sorted relative
274       to other models in this list.
275     **/
277     // comparator is not defined by default
279     /**
280     Creates or updates the specified model on the server, then adds it to this
281     list if the server indicates success.
283     @method create
284     @param {Model|Object} model Model to create. May be an existing model
285       instance or a hash of model attributes, in which case a new model instance
286       will be created from the hash.
287     @param {Object} [options] Options to be passed to the model's `sync()` and
288         `set()` methods and mixed into the `create` and `add` event facades.
289       @param {Boolean} [options.silent=false] If `true`, no `add` event(s) will
290           be fired.
291     @param {Function} [callback] Called when the sync operation finishes.
292       @param {Error} callback.err If an error occurred, this parameter will
293         contain the error. If the sync operation succeeded, _err_ will be
294         falsy.
295       @param {Any} callback.response The server's response.
296     @return {Model} Created model.
297     **/
298     create: function (model, options, callback) {
299         var self = this;
301         // Allow callback as second arg.
302         if (typeof options === 'function') {
303             callback = options;
304             options  = {};
305         }
307         options || (options = {});
309         if (!model._isYUIModel) {
310             model = new this.model(model);
311         }
313         self.fire(EVT_CREATE, Y.merge(options, {
314             model: model
315         }));
317         return model.save(options, function (err) {
318             if (!err) {
319                 self.add(model, options);
320             }
322             callback && callback.apply(null, arguments);
323         });
324     },
326     /**
327     Executes the supplied function on each model in this list. Returns an array
328     containing the models for which the supplied function returned a truthy
329     value.
331     The callback function's `this` object will refer to this ModelList. Use
332     `Y.bind()` to bind the `this` object to another object if desired.
334     @example
336         // Get an array containing only the models whose "enabled" attribute is
337         // truthy.
338         var filtered = list.filter(function (model) {
339             return model.get('enabled');
340         });
342         // Get a new ModelList containing only the models whose "enabled"
343         // attribute is truthy.
344         var filteredList = list.filter({asList: true}, function (model) {
345             return model.get('enabled');
346         });
348     @method filter
349     @param {Object} [options] Filter options.
350         @param {Boolean} [options.asList=false] If truthy, results will be
351             returned as a new ModelList instance rather than as an array.
353     @param {Function} callback Function to execute on each model.
354         @param {Model} callback.model Model instance.
355         @param {Number} callback.index Index of the current model.
356         @param {ModelList} callback.list The ModelList being filtered.
358     @return {Array|ModelList} Array of models for which the callback function
359         returned a truthy value (empty if it never returned a truthy value). If
360         the `options.asList` option is truthy, a new ModelList instance will be
361         returned instead of an array.
362     @since 3.5.0
363     */
364     filter: function (options, callback) {
365         var filtered = [],
366             items    = this._items,
367             i, item, len, list;
369         // Allow options as first arg.
370         if (typeof options === 'function') {
371             callback = options;
372             options  = {};
373         }
375         for (i = 0, len = items.length; i < len; ++i) {
376             item = items[i];
378             if (callback.call(this, item, i, this)) {
379                 filtered.push(item);
380             }
381         }
383         if (options.asList) {
384             list = new Y.ModelList({model: this.model});
385             filtered.length && list.add(filtered, {silent: true});
386             return list;
387         } else {
388             return filtered;
389         }
390     },
392     /**
393     If _name_ refers to an attribute on this ModelList instance, returns the
394     value of that attribute. Otherwise, returns an array containing the values
395     of the specified attribute from each model in this list.
397     @method get
398     @param {String} name Attribute name or object property path.
399     @return {Any|Array} Attribute value or array of attribute values.
400     @see Model.get()
401     **/
402     get: function (name) {
403         if (this.attrAdded(name)) {
404             return AttrProto.get.apply(this, arguments);
405         }
407         return this.invoke('get', name);
408     },
410     /**
411     If _name_ refers to an attribute on this ModelList instance, returns the
412     HTML-escaped value of that attribute. Otherwise, returns an array containing
413     the HTML-escaped values of the specified attribute from each model in this
414     list.
416     The values are escaped using `Escape.html()`.
418     @method getAsHTML
419     @param {String} name Attribute name or object property path.
420     @return {String|String[]} HTML-escaped value or array of HTML-escaped
421       values.
422     @see Model.getAsHTML()
423     **/
424     getAsHTML: function (name) {
425         if (this.attrAdded(name)) {
426             return Y.Escape.html(AttrProto.get.apply(this, arguments));
427         }
429         return this.invoke('getAsHTML', name);
430     },
432     /**
433     If _name_ refers to an attribute on this ModelList instance, returns the
434     URL-encoded value of that attribute. Otherwise, returns an array containing
435     the URL-encoded values of the specified attribute from each model in this
436     list.
438     The values are encoded using the native `encodeURIComponent()` function.
440     @method getAsURL
441     @param {String} name Attribute name or object property path.
442     @return {String|String[]} URL-encoded value or array of URL-encoded values.
443     @see Model.getAsURL()
444     **/
445     getAsURL: function (name) {
446         if (this.attrAdded(name)) {
447             return encodeURIComponent(AttrProto.get.apply(this, arguments));
448         }
450         return this.invoke('getAsURL', name);
451     },
453     /**
454     Returns the model with the specified _clientId_, or `null` if not found.
456     @method getByClientId
457     @param {String} clientId Client id.
458     @return {Model} Model, or `null` if not found.
459     **/
460     getByClientId: function (clientId) {
461         return this._clientIdMap[clientId] || null;
462     },
464     /**
465     Returns the model with the specified _id_, or `null` if not found.
467     Note that models aren't expected to have an id until they're saved, so if
468     you're working with unsaved models, it may be safer to call
469     `getByClientId()`.
471     @method getById
472     @param {String|Number} id Model id.
473     @return {Model} Model, or `null` if not found.
474     **/
475     getById: function (id) {
476         return this._idMap[id] || null;
477     },
479     /**
480     Calls the named method on every model in the list. Any arguments provided
481     after _name_ will be passed on to the invoked method.
483     @method invoke
484     @param {String} name Name of the method to call on each model.
485     @param {Any} [args*] Zero or more arguments to pass to the invoked method.
486     @return {Array} Array of return values, indexed according to the index of
487       the model on which the method was called.
488     **/
489     invoke: function (name /*, args* */) {
490         var args = [this._items, name].concat(YArray(arguments, 1, true));
491         return YArray.invoke.apply(YArray, args);
492     },
494     /**
495     Returns the model at the specified _index_.
497     @method item
498     @param {Number} index Index of the model to fetch.
499     @return {Model} The model at the specified index, or `undefined` if there
500       isn't a model there.
501     **/
503     // item() is inherited from ArrayList.
505     /**
506     Loads this list of models from the server.
508     This method delegates to the `sync()` method to perform the actual load
509     operation, which is an asynchronous action. Specify a _callback_ function to
510     be notified of success or failure.
512     If the load operation succeeds, a `reset` event will be fired.
514     @method load
515     @param {Object} [options] Options to be passed to `sync()` and to
516       `reset()` when adding the loaded models. It's up to the custom sync
517       implementation to determine what options it supports or requires, if any.
518     @param {Function} [callback] Called when the sync operation finishes.
519       @param {Error} callback.err If an error occurred, this parameter will
520         contain the error. If the sync operation succeeded, _err_ will be
521         falsy.
522       @param {Any} callback.response The server's response. This value will
523         be passed to the `parse()` method, which is expected to parse it and
524         return an array of model attribute hashes.
525     @chainable
526     **/
527     load: function (options, callback) {
528         var self = this;
530         // Allow callback as only arg.
531         if (typeof options === 'function') {
532             callback = options;
533             options  = {};
534         }
536         options || (options = {});
538         this.sync('read', options, function (err, response) {
539             var facade = {
540                     options : options,
541                     response: response
542                 },
544                 parsed;
546             if (err) {
547                 facade.error = err;
548                 facade.src   = 'load';
550                 self.fire(EVT_ERROR, facade);
551             } else {
552                 // Lazy publish.
553                 if (!self._loadEvent) {
554                     self._loadEvent = self.publish(EVT_LOAD, {
555                         preventable: false
556                     });
557                 }
559                 parsed = facade.parsed = self.parse(response);
561                 self.reset(parsed, options);
562                 self.fire(EVT_LOAD, facade);
563             }
565             callback && callback.apply(null, arguments);
566         });
568         return this;
569     },
571     /**
572     Executes the specified function on each model in this list and returns an
573     array of the function's collected return values.
575     @method map
576     @param {Function} fn Function to execute on each model.
577       @param {Model} fn.model Current model being iterated.
578       @param {Number} fn.index Index of the current model in the list.
579       @param {Model[]} fn.models Array of models being iterated.
580     @param {Object} [thisObj] `this` object to use when calling _fn_.
581     @return {Array} Array of return values from _fn_.
582     **/
583     map: function (fn, thisObj) {
584         return YArray.map(this._items, fn, thisObj);
585     },
587     /**
588     Called to parse the _response_ when the list is loaded from the server.
589     This method receives a server _response_ and is expected to return an array
590     of model attribute hashes.
592     The default implementation assumes that _response_ is either an array of
593     attribute hashes or a JSON string that can be parsed into an array of
594     attribute hashes. If _response_ is a JSON string and either `Y.JSON` or the
595     native `JSON` object are available, it will be parsed automatically. If a
596     parse error occurs, an `error` event will be fired and the model will not be
597     updated.
599     You may override this method to implement custom parsing logic if necessary.
601     @method parse
602     @param {Any} response Server response.
603     @return {Object[]} Array of model attribute hashes.
604     **/
605     parse: function (response) {
606         if (typeof response === 'string') {
607             try {
608                 return Y.JSON.parse(response) || [];
609             } catch (ex) {
610                 this.fire(EVT_ERROR, {
611                     error   : ex,
612                     response: response,
613                     src     : 'parse'
614                 });
616                 return null;
617             }
618         }
620         return response || [];
621     },
623     /**
624     Removes the specified model or array of models from this list. You may also
625     pass another ModelList instance to remove all the models that are in both
626     that instance and this instance.
628     @method remove
629     @param {Model|Model[]|ModelList} models Models to remove.
630     @param {Object} [options] Data to be mixed into the event facade of the
631         `remove` event(s) for the removed models.
633         @param {Boolean} [options.silent=false] If `true`, no `remove` event(s)
634             will be fired.
636     @return {Model|Model[]} Removed model or array of removed models.
637     **/
638     remove: function (models, options) {
639         var isList = models._isYUIModelList;
641         if (isList || Lang.isArray(models)) {
642             return YArray.map(isList ? models.toArray() : models, function (model) {
643                 return this._remove(model, options);
644             }, this);
645         } else {
646             return this._remove(models, options);
647         }
648     },
650     /**
651     Completely replaces all models in the list with those specified, and fires a
652     single `reset` event.
654     Use `reset` when you want to add or remove a large number of items at once
655     with less overhead, and without firing `add` or `remove` events for each
656     one.
658     @method reset
659     @param {Model[]|ModelList|Object[]} [models] Models to add. May be existing
660         model instances or hashes of model attributes, in which case new model
661         instances will be created from the hashes. If a ModelList is passed, all
662         the models in that list will be added to this list. Calling `reset()`
663         without passing in any models will clear the list.
664     @param {Object} [options] Data to be mixed into the event facade of the
665         `reset` event.
667         @param {Boolean} [options.silent=false] If `true`, no `reset` event will
668             be fired.
670     @chainable
671     **/
672     reset: function (models, options) {
673         models  || (models  = []);
674         options || (options = {});
676         var facade = Y.merge({src: 'reset'}, options);
678         if (models._isYUIModelList) {
679             models = models.toArray();
680         } else {
681             models = YArray.map(models, function (model) {
682                 return model._isYUIModel ? model : new this.model(model);
683             }, this);
684         }
686         facade.models = models;
688         if (options.silent) {
689             this._defResetFn(facade);
690         } else {
691             // Sort the models before firing the reset event.
692             if (this.comparator) {
693                 models.sort(Y.bind(this._sort, this));
694             }
696             this.fire(EVT_RESET, facade);
697         }
699         return this;
700     },
702     /**
703     Forcibly re-sorts the list.
705     Usually it shouldn't be necessary to call this method since the list
706     maintains its sort order when items are added and removed, but if you change
707     the `comparator` function after items are already in the list, you'll need
708     to re-sort.
710     @method sort
711     @param {Object} [options] Data to be mixed into the event facade of the
712         `reset` event.
713       @param {Boolean} [options.silent=false] If `true`, no `reset` event will
714           be fired.
715     @chainable
716     **/
717     sort: function (options) {
718         if (!this.comparator) {
719             return this;
720         }
722         var models = this._items.concat(),
723             facade;
725         options || (options = {});
727         models.sort(Y.bind(this._sort, this));
729         facade = Y.merge(options, {
730             models: models,
731             src   : 'sort'
732         });
734         options.silent ? this._defResetFn(facade) :
735                 this.fire(EVT_RESET, facade);
737         return this;
738     },
740     /**
741     Override this method to provide a custom persistence implementation for this
742     list. The default method just calls the callback without actually doing
743     anything.
745     This method is called internally by `load()`.
747     @method sync
748     @param {String} action Sync action to perform. May be one of the following:
750       * `create`: Store a list of newly-created models for the first time.
751       * `delete`: Delete a list of existing models.
752       * `read`  : Load a list of existing models.
753       * `update`: Update a list of existing models.
755       Currently, model lists only make use of the `read` action, but other
756       actions may be used in future versions.
758     @param {Object} [options] Sync options. It's up to the custom sync
759       implementation to determine what options it supports or requires, if any.
760     @param {Function} [callback] Called when the sync operation finishes.
761       @param {Error} callback.err If an error occurred, this parameter will
762         contain the error. If the sync operation succeeded, _err_ will be
763         falsy.
764       @param {Any} [callback.response] The server's response. This value will
765         be passed to the `parse()` method, which is expected to parse it and
766         return an array of model attribute hashes.
767     **/
768     sync: function (/* action, options, callback */) {
769         var callback = YArray(arguments, 0, true).pop();
771         if (typeof callback === 'function') {
772             callback();
773         }
774     },
776     /**
777     Returns an array containing the models in this list.
779     @method toArray
780     @return {Array} Array containing the models in this list.
781     **/
782     toArray: function () {
783         return this._items.concat();
784     },
786     /**
787     Returns an array containing attribute hashes for each model in this list,
788     suitable for being passed to `Y.JSON.stringify()`.
790     Under the hood, this method calls `toJSON()` on each model in the list and
791     pushes the results into an array.
793     @method toJSON
794     @return {Object[]} Array of model attribute hashes.
795     @see Model.toJSON()
796     **/
797     toJSON: function () {
798         return this.map(function (model) {
799             return model.toJSON();
800         });
801     },
803     // -- Protected Methods ----------------------------------------------------
805     /**
806     Adds the specified _model_ if it isn't already in this list.
808     If the model's `clientId` or `id` matches that of a model that's already in
809     the list, an `error` event will be fired and the model will not be added.
811     @method _add
812     @param {Model|Object} model Model or object to add.
813     @param {Object} [options] Data to be mixed into the event facade of the
814         `add` event for the added model.
815       @param {Boolean} [options.silent=false] If `true`, no `add` event will be
816           fired.
817     @return {Model} The added model.
818     @protected
819     **/
820     _add: function (model, options) {
821         var facade, id;
823         options || (options = {});
825         if (!model._isYUIModel) {
826             model = new this.model(model);
827         }
829         id = model.get('id');
831         if (this._clientIdMap[model.get('clientId')]
832                 || (Lang.isValue(id) && this._idMap[id])) {
834             this.fire(EVT_ERROR, {
835                 error: 'Model is already in the list.',
836                 model: model,
837                 src  : 'add'
838             });
840             return;
841         }
843         facade = Y.merge(options, {
844             index: this._findIndex(model),
845             model: model
846         });
848         options.silent ? this._defAddFn(facade) : this.fire(EVT_ADD, facade);
850         return model;
851     },
853     /**
854     Adds this list as a bubble target for the specified model's events.
856     @method _attachList
857     @param {Model} model Model to attach to this list.
858     @protected
859     **/
860     _attachList: function (model) {
861         // Attach this list and make it a bubble target for the model.
862         model.lists.push(this);
863         model.addTarget(this);
864     },
866     /**
867     Clears all internal state and the internal list of models, returning this
868     list to an empty state. Automatically detaches all models in the list.
870     @method _clear
871     @protected
872     **/
873     _clear: function () {
874         YArray.each(this._items, this._detachList, this);
876         this._clientIdMap = {};
877         this._idMap       = {};
878         this._items       = [];
879     },
881     /**
882     Compares the value _a_ to the value _b_ for sorting purposes. Values are
883     assumed to be the result of calling a model's `comparator()` method. You can
884     override this method to implement custom sorting logic, such as a descending
885     sort or multi-field sorting.
887     @method _compare
888     @param {Mixed} a First value to compare.
889     @param {Mixed} b Second value to compare.
890     @return {Number} `-1` if _a_ should come before _b_, `0` if they're
891         equivalent, `1` if _a_ should come after _b_.
892     @protected
893     @since 3.5.0
894     **/
895     _compare: function (a, b) {
896         return a < b ? -1 : (a > b ? 1 : 0);
897     },
899     /**
900     Removes this list as a bubble target for the specified model's events.
902     @method _detachList
903     @param {Model} model Model to detach.
904     @protected
905     **/
906     _detachList: function (model) {
907         var index = YArray.indexOf(model.lists, this);
909         if (index > -1) {
910             model.lists.splice(index, 1);
911             model.removeTarget(this);
912         }
913     },
915     /**
916     Returns the index at which the given _model_ should be inserted to maintain
917     the sort order of the list.
919     @method _findIndex
920     @param {Model} model The model being inserted.
921     @return {Number} Index at which the model should be inserted.
922     @protected
923     **/
924     _findIndex: function (model) {
925         var items = this._items,
926             max   = items.length,
927             min   = 0,
928             item, middle, needle;
930         if (!this.comparator || !max) {
931             return max;
932         }
934         needle = this.comparator(model);
936         // Perform an iterative binary search to determine the correct position
937         // based on the return value of the `comparator` function.
938         while (min < max) {
939             middle = (min + max) >> 1; // Divide by two and discard remainder.
940             item   = items[middle];
942             if (this._compare(this.comparator(item), needle) < 0) {
943                 min = middle + 1;
944             } else {
945                 max = middle;
946             }
947         }
949         return min;
950     },
952     /**
953     Removes the specified _model_ if it's in this list.
955     @method _remove
956     @param {Model} model Model to remove.
957     @param {Object} [options] Data to be mixed into the event facade of the
958         `remove` event for the removed model.
959       @param {Boolean} [options.silent=false] If `true`, no `remove` event will
960           be fired.
961     @return {Model} Removed model.
962     @protected
963     **/
964     _remove: function (model, options) {
965         var index = this.indexOf(model),
966             facade;
968         options || (options = {});
970         if (index === -1) {
971             this.fire(EVT_ERROR, {
972                 error: 'Model is not in the list.',
973                 model: model,
974                 src  : 'remove'
975             });
977             return;
978         }
980         facade = Y.merge(options, {
981             index: index,
982             model: model
983         });
985         options.silent ? this._defRemoveFn(facade) :
986                 this.fire(EVT_REMOVE, facade);
988         return model;
989     },
991     /**
992     Array sort function used by `sort()` to re-sort the models in the list.
994     @method _sort
995     @param {Model} a First model to compare.
996     @param {Model} b Second model to compare.
997     @return {Number} `-1` if _a_ is less than _b_, `0` if equal, `1` if greater.
998     @protected
999     **/
1000     _sort: function (a, b) {
1001         return this._compare(this.comparator(a), this.comparator(b));
1002     },
1004     // -- Event Handlers -------------------------------------------------------
1006     /**
1007     Updates the model maps when a model's `id` attribute changes.
1009     @method _afterIdChange
1010     @param {EventFacade} e
1011     @protected
1012     **/
1013     _afterIdChange: function (e) {
1014         Lang.isValue(e.prevVal) && delete this._idMap[e.prevVal];
1015         Lang.isValue(e.newVal) && (this._idMap[e.newVal] = e.target);
1016     },
1018     // -- Default Event Handlers -----------------------------------------------
1020     /**
1021     Default event handler for `add` events.
1023     @method _defAddFn
1024     @param {EventFacade} e
1025     @protected
1026     **/
1027     _defAddFn: function (e) {
1028         var model = e.model,
1029             id    = model.get('id');
1031         this._clientIdMap[model.get('clientId')] = model;
1033         if (Lang.isValue(id)) {
1034             this._idMap[id] = model;
1035         }
1037         this._attachList(model);
1038         this._items.splice(e.index, 0, model);
1039     },
1041     /**
1042     Default event handler for `remove` events.
1044     @method _defRemoveFn
1045     @param {EventFacade} e
1046     @protected
1047     **/
1048     _defRemoveFn: function (e) {
1049         var model = e.model,
1050             id    = model.get('id');
1052         this._detachList(model);
1053         delete this._clientIdMap[model.get('clientId')];
1055         if (Lang.isValue(id)) {
1056             delete this._idMap[id];
1057         }
1059         this._items.splice(e.index, 1);
1060     },
1062     /**
1063     Default event handler for `reset` events.
1065     @method _defResetFn
1066     @param {EventFacade} e
1067     @protected
1068     **/
1069     _defResetFn: function (e) {
1070         // When fired from the `sort` method, we don't need to clear the list or
1071         // add any models, since the existing models are sorted in place.
1072         if (e.src === 'sort') {
1073             this._items = e.models.concat();
1074             return;
1075         }
1077         this._clear();
1079         if (e.models.length) {
1080             this.add(e.models, {silent: true});
1081         }
1082     }
1083 }, {
1084     NAME: 'modelList'
1087 Y.augment(ModelList, Y.ArrayList);
1090 }, '3.5.1' ,{requires:['array-extras', 'array-invoke', 'arraylist', 'base-build', 'escape', 'json-parse', 'model']});