~ubuntu-branches/ubuntu/utopic/moodle/utopic

« back to all changes in this revision

Viewing changes to lib/yuilib/3.13.0/model-list/model-list-debug.js

  • Committer: Package Import Robot
  • Author(s): Thijs Kinkhorst
  • Date: 2014-05-12 16:10:38 UTC
  • mfrom: (36.1.3 sid)
  • Revision ID: package-import@ubuntu.com-20140512161038-puyqf65k4e0s8ytz
Tags: 2.6.3-1
New upstream release.

Show diffs side-by-side

added added

removed removed

Lines of Context:
 
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
*/
 
7
 
 
8
YUI.add('model-list', function (Y, NAME) {
 
9
 
 
10
/**
 
11
Provides an API for managing an ordered list of Model instances.
 
12
 
 
13
@module app
 
14
@submodule model-list
 
15
@since 3.4.0
 
16
**/
 
17
 
 
18
/**
 
19
Provides an API for managing an ordered list of Model instances.
 
20
 
 
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.
 
26
 
 
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).
 
30
 
 
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
**/
 
41
 
 
42
var AttrProto = Y.Attribute.prototype,
 
43
    Lang      = Y.Lang,
 
44
    YArray    = Y.Array,
 
45
 
 
46
    /**
 
47
    Fired when a model is added to the list.
 
48
 
 
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.
 
52
 
 
53
    Listen to the `after` phase of this event to be notified after a model has
 
54
    been added to the list.
 
55
 
 
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',
 
62
 
 
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.
 
67
 
 
68
    @event create
 
69
    @param {Model} model The model being created/updated.
 
70
    @since 3.5.0
 
71
    **/
 
72
    EVT_CREATE = 'create',
 
73
 
 
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.
 
77
 
 
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):
 
84
 
 
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',
 
95
 
 
96
    /**
 
97
    Fired after models are loaded from a sync layer.
 
98
 
 
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',
 
107
 
 
108
    /**
 
109
    Fired when a model is removed from the list.
 
110
 
 
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.
 
114
 
 
115
    Listen to the `after` phase of this event to be notified after a model has
 
116
    been removed from the list.
 
117
 
 
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',
 
124
 
 
125
    /**
 
126
    Fired when the list is completely reset via the `reset()` method or sorted
 
127
    via the `sort()` method.
 
128
 
 
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.
 
132
 
 
133
    Listen to the `after` phase of this event to be notified after the list has
 
134
    been reset.
 
135
 
 
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';
 
143
 
 
144
function ModelList() {
 
145
    ModelList.superclass.constructor.apply(this, arguments);
 
146
}
 
147
 
 
148
Y.ModelList = Y.extend(ModelList, Y.Base, {
 
149
    // -- Public Properties ----------------------------------------------------
 
150
 
 
151
    /**
 
152
    The `Model` class or subclass of the models in this list.
 
153
 
 
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.
 
157
 
 
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).
 
161
 
 
162
    @property model
 
163
    @type Model|String
 
164
    @default Y.Model
 
165
    **/
 
166
    model: Y.Model,
 
167
 
 
168
    // -- Protected Properties -------------------------------------------------
 
169
 
 
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.
 
174
 
 
175
    @property _isYUIModelList
 
176
    @type Boolean
 
177
    @default true
 
178
    @protected
 
179
    @since 3.5.0
 
180
    **/
 
181
    _isYUIModelList: true,
 
182
 
 
183
    // -- Lifecycle Methods ----------------------------------------------------
 
184
    initializer: function (config) {
 
185
        config || (config = {});
 
186
 
 
187
        var model = this.model = config.model || this.model;
 
188
 
 
189
        if (typeof model === 'string') {
 
190
            // Look for a namespaced Model class on `Y`.
 
191
            this.model = Y.Object.getValue(Y, model.split('.'));
 
192
 
 
193
            if (!this.model) {
 
194
                Y.error('ModelList: Model class not found: ' + model);
 
195
            }
 
196
        }
 
197
 
 
198
        this.publish(EVT_ADD,    {defaultFn: this._defAddFn});
 
199
        this.publish(EVT_RESET,  {defaultFn: this._defResetFn});
 
200
        this.publish(EVT_REMOVE, {defaultFn: this._defRemoveFn});
 
201
 
 
202
        this.after('*:idChange', this._afterIdChange);
 
203
 
 
204
        this._clear();
 
205
 
 
206
        if (config.items) {
 
207
            this.add(config.items, {silent: true});
 
208
        }
 
209
    },
 
210
 
 
211
    destructor: function () {
 
212
        this._clear();
 
213
    },
 
214
 
 
215
    // -- Public Methods -------------------------------------------------------
 
216
 
 
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.
 
221
 
 
222
    @example
 
223
 
 
224
        // Add a single model instance.
 
225
        list.add(new Model({foo: 'bar'}));
 
226
 
 
227
        // Add a single model, creating a new instance automatically.
 
228
        list.add({foo: 'bar'});
 
229
 
 
230
        // Add multiple models, creating new instances automatically.
 
231
        list.add([
 
232
            {foo: 'bar'},
 
233
            {baz: 'quux'}
 
234
        ]);
 
235
 
 
236
        // Add all the models in another ModelList instance.
 
237
        list.add(otherList);
 
238
 
 
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.
 
247
 
 
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.
 
254
 
 
255
    @return {Model|Model[]} Added model or array of added models.
 
256
    **/
 
257
    add: function (models, options) {
 
258
        var isList = models._isYUIModelList;
 
259
 
 
260
        if (isList || Lang.isArray(models)) {
 
261
            return YArray.map(isList ? models.toArray() : models, function (model, index) {
 
262
                var modelOptions = options || {};
 
263
 
 
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
                }
 
272
 
 
273
                return this._add(model, modelOptions);
 
274
            }, this);
 
275
        } else {
 
276
            return this._add(models, options);
 
277
        }
 
278
    },
 
279
 
 
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.
 
284
 
 
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).
 
287
 
 
288
    @example
 
289
        var list = new Y.ModelList({model: Y.Model});
 
290
 
 
291
        list.comparator = function (model) {
 
292
            return model.get('id'); // Sort models by id.
 
293
        };
 
294
 
 
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
    **/
 
300
 
 
301
    // comparator is not defined by default
 
302
 
 
303
    /**
 
304
    Creates or updates the specified model on the server, then adds it to this
 
305
    list if the server indicates success.
 
306
 
 
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;
 
324
 
 
325
        // Allow callback as second arg.
 
326
        if (typeof options === 'function') {
 
327
            callback = options;
 
328
            options  = {};
 
329
        }
 
330
 
 
331
        options || (options = {});
 
332
 
 
333
        if (!model._isYUIModel) {
 
334
            model = new this.model(model);
 
335
        }
 
336
 
 
337
        self.fire(EVT_CREATE, Y.merge(options, {
 
338
            model: model
 
339
        }));
 
340
 
 
341
        return model.save(options, function (err) {
 
342
            if (!err) {
 
343
                self.add(model, options);
 
344
            }
 
345
 
 
346
            if (callback) {
 
347
                callback.apply(null, arguments);
 
348
            }
 
349
        });
 
350
    },
 
351
 
 
352
    /**
 
353
    Executes the supplied function on each model in this list.
 
354
 
 
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.
 
358
 
 
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.
 
361
 
 
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;
 
375
 
 
376
        for (i = 0, len = items.length; i < len; i++) {
 
377
            item = items[i];
 
378
            callback.call(thisObj || item, item, i, this);
 
379
        }
 
380
 
 
381
        return this;
 
382
    },
 
383
 
 
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.
 
388
 
 
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.
 
391
 
 
392
    @example
 
393
 
 
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
        });
 
399
 
 
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
        });
 
405
 
 
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.
 
410
 
 
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.
 
415
 
 
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;
 
426
 
 
427
        // Allow options as first arg.
 
428
        if (typeof options === 'function') {
 
429
            callback = options;
 
430
            options  = {};
 
431
        }
 
432
 
 
433
        for (i = 0, len = items.length; i < len; ++i) {
 
434
            item = items[i];
 
435
 
 
436
            if (callback.call(this, item, i, this)) {
 
437
                filtered.push(item);
 
438
            }
 
439
        }
 
440
 
 
441
        if (options.asList) {
 
442
            list = new this.constructor({model: this.model});
 
443
 
 
444
            if (filtered.length) {
 
445
                list.add(filtered, {silent: true});
 
446
            }
 
447
 
 
448
            return list;
 
449
        } else {
 
450
            return filtered;
 
451
        }
 
452
    },
 
453
 
 
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.
 
458
 
 
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
        }
 
468
 
 
469
        return this.invoke('get', name);
 
470
    },
 
471
 
 
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.
 
477
 
 
478
    The values are escaped using `Escape.html()`.
 
479
 
 
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
        }
 
490
 
 
491
        return this.invoke('getAsHTML', name);
 
492
    },
 
493
 
 
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.
 
499
 
 
500
    The values are encoded using the native `encodeURIComponent()` function.
 
501
 
 
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
        }
 
511
 
 
512
        return this.invoke('getAsURL', name);
 
513
    },
 
514
 
 
515
    /**
 
516
    Returns the model with the specified _clientId_, or `null` if not found.
 
517
 
 
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
    },
 
525
 
 
526
    /**
 
527
    Returns the model with the specified _id_, or `null` if not found.
 
528
 
 
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()`.
 
532
 
 
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
    },
 
540
 
 
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.
 
544
 
 
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
    },
 
555
 
 
556
    /**
 
557
    Returns the model at the specified _index_.
 
558
 
 
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
    **/
 
564
 
 
565
    // item() is inherited from ArrayList.
 
566
 
 
567
    /**
 
568
    Loads this list of models from the server.
 
569
 
 
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.
 
573
 
 
574
    If the load operation succeeds, a `reset` event will be fired.
 
575
 
 
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;
 
591
 
 
592
        // Allow callback as only arg.
 
593
        if (typeof options === 'function') {
 
594
            callback = options;
 
595
            options  = {};
 
596
        }
 
597
 
 
598
        options || (options = {});
 
599
 
 
600
        this.sync('read', options, function (err, response) {
 
601
            var facade = {
 
602
                    options : options,
 
603
                    response: response
 
604
                },
 
605
 
 
606
                parsed;
 
607
 
 
608
            if (err) {
 
609
                facade.error = err;
 
610
                facade.src   = 'load';
 
611
 
 
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
                }
 
620
 
 
621
                parsed = facade.parsed = self._parse(response);
 
622
 
 
623
                self.reset(parsed, options);
 
624
                self.fire(EVT_LOAD, facade);
 
625
            }
 
626
 
 
627
            if (callback) {
 
628
                callback.apply(null, arguments);
 
629
            }
 
630
        });
 
631
 
 
632
        return this;
 
633
    },
 
634
 
 
635
    /**
 
636
    Executes the specified function on each model in this list and returns an
 
637
    array of the function's collected return values.
 
638
 
 
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
    },
 
650
 
 
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.
 
655
 
 
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.
 
662
 
 
663
    You may override this method to implement custom parsing logic if necessary.
 
664
 
 
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
                });
 
679
 
 
680
                return null;
 
681
            }
 
682
        }
 
683
 
 
684
        return response || [];
 
685
    },
 
686
 
 
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.
 
692
 
 
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.
 
698
 
 
699
        @param {Boolean} [options.silent=false] If `true`, no `remove` event(s)
 
700
            will be fired.
 
701
 
 
702
    @return {Model|Model[]} Removed model or array of removed models.
 
703
    **/
 
704
    remove: function (models, options) {
 
705
        var isList = models._isYUIModelList;
 
706
 
 
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
                }
 
715
 
 
716
                return model;
 
717
            }, this);
 
718
 
 
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
    },
 
726
 
 
727
    /**
 
728
    Completely replaces all models in the list with those specified, and fires a
 
729
    single `reset` event.
 
730
 
 
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.
 
734
 
 
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.
 
743
 
 
744
        @param {Boolean} [options.silent=false] If `true`, no `reset` event will
 
745
            be fired.
 
746
 
 
747
    @chainable
 
748
    **/
 
749
    reset: function (models, options) {
 
750
        models  || (models  = []);
 
751
        options || (options = {});
 
752
 
 
753
        var facade = Y.merge({src: 'reset'}, options);
 
754
 
 
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
        }
 
762
 
 
763
        facade.models = models;
 
764
 
 
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
            }
 
772
 
 
773
            this.fire(EVT_RESET, facade);
 
774
        }
 
775
 
 
776
        return this;
 
777
    },
 
778
 
 
779
    /**
 
780
    Executes the supplied function on each model in this list, and stops
 
781
    iterating if the callback returns `true`.
 
782
 
 
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.
 
786
 
 
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.
 
789
 
 
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;
 
804
 
 
805
        for (i = 0, len = items.length; i < len; i++) {
 
806
            item = items[i];
 
807
 
 
808
            if (callback.call(thisObj || item, item, i, this)) {
 
809
                return true;
 
810
            }
 
811
        }
 
812
 
 
813
        return false;
 
814
    },
 
815
 
 
816
    /**
 
817
    Forcibly re-sorts the list.
 
818
 
 
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.
 
823
 
 
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
        }
 
837
 
 
838
        var models = this._items.concat(),
 
839
            facade;
 
840
 
 
841
        options || (options = {});
 
842
 
 
843
        models.sort(Y.rbind(this._sort, this, options));
 
844
 
 
845
        facade = Y.merge(options, {
 
846
            models: models,
 
847
            src   : 'sort'
 
848
        });
 
849
 
 
850
        if (options.silent) {
 
851
            this._defResetFn(facade);
 
852
        } else {
 
853
            this.fire(EVT_RESET, facade);
 
854
        }
 
855
 
 
856
        return this;
 
857
    },
 
858
 
 
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.
 
863
 
 
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.
 
867
 
 
868
    @method sync
 
869
    @param {String} action Sync action to perform. May be one of the following:
 
870
 
 
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.
 
875
 
 
876
      Currently, model lists only make use of the `read` action, but other
 
877
      actions may be used in future versions.
 
878
 
 
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();
 
891
 
 
892
        if (typeof callback === 'function') {
 
893
            callback();
 
894
        }
 
895
    },
 
896
 
 
897
    /**
 
898
    Returns an array containing the models in this list.
 
899
 
 
900
    @method toArray
 
901
    @return {Array} Array containing the models in this list.
 
902
    **/
 
903
    toArray: function () {
 
904
        return this._items.concat();
 
905
    },
 
906
 
 
907
    /**
 
908
    Returns an array containing attribute hashes for each model in this list,
 
909
    suitable for being passed to `Y.JSON.stringify()`.
 
910
 
 
911
    Under the hood, this method calls `toJSON()` on each model in the list and
 
912
    pushes the results into an array.
 
913
 
 
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
    },
 
923
 
 
924
    // -- Protected Methods ----------------------------------------------------
 
925
 
 
926
    /**
 
927
    Adds the specified _model_ if it isn't already in this list.
 
928
 
 
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.
 
931
 
 
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;
 
943
 
 
944
        options || (options = {});
 
945
 
 
946
        if (!model._isYUIModel) {
 
947
            model = new this.model(model);
 
948
        }
 
949
 
 
950
        id = model.get('id');
 
951
 
 
952
        if (this._clientIdMap[model.get('clientId')]
 
953
                || (Lang.isValue(id) && this._idMap[id])) {
 
954
 
 
955
            this.fire(EVT_ERROR, {
 
956
                error: 'Model is already in the list.',
 
957
                model: model,
 
958
                src  : 'add'
 
959
            });
 
960
 
 
961
            return;
 
962
        }
 
963
 
 
964
        facade = Y.merge(options, {
 
965
            index: 'index' in options ? options.index : this._findIndex(model),
 
966
            model: model
 
967
        });
 
968
 
 
969
        if (options.silent) {
 
970
            this._defAddFn(facade);
 
971
        } else {
 
972
            this.fire(EVT_ADD, facade);
 
973
        }
 
974
 
 
975
        return model;
 
976
    },
 
977
 
 
978
    /**
 
979
    Adds this list as a bubble target for the specified model's events.
 
980
 
 
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
    },
 
990
 
 
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.
 
994
 
 
995
    @method _clear
 
996
    @protected
 
997
    **/
 
998
    _clear: function () {
 
999
        YArray.each(this._items, this._detachList, this);
 
1000
 
 
1001
        this._clientIdMap = {};
 
1002
        this._idMap       = {};
 
1003
        this._items       = [];
 
1004
    },
 
1005
 
 
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.
 
1011
 
 
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
    },
 
1023
 
 
1024
    /**
 
1025
    Removes this list as a bubble target for the specified model's events.
 
1026
 
 
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);
 
1033
 
 
1034
        if (index > -1) {
 
1035
            model.lists.splice(index, 1);
 
1036
            model.removeTarget(this);
 
1037
        }
 
1038
    },
 
1039
 
 
1040
    /**
 
1041
    Returns the index at which the given _model_ should be inserted to maintain
 
1042
    the sort order of the list.
 
1043
 
 
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;
 
1054
 
 
1055
        if (!this.comparator || !max) {
 
1056
            return max;
 
1057
        }
 
1058
 
 
1059
        needle = this.comparator(model);
 
1060
 
 
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];
 
1066
 
 
1067
            if (this._compare(this.comparator(item), needle) < 0) {
 
1068
                min = middle + 1;
 
1069
            } else {
 
1070
                max = middle;
 
1071
            }
 
1072
        }
 
1073
 
 
1074
        return min;
 
1075
    },
 
1076
 
 
1077
    /**
 
1078
    Calls the public, overrideable `parse()` method and returns the result.
 
1079
 
 
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.
 
1083
 
 
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
    },
 
1094
 
 
1095
    /**
 
1096
    Removes the specified _model_ if it's in this list.
 
1097
 
 
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;
 
1109
 
 
1110
        options || (options = {});
 
1111
 
 
1112
        if (Lang.isNumber(model)) {
 
1113
            index = model;
 
1114
            model = this.item(index);
 
1115
        } else {
 
1116
            index = this.indexOf(model);
 
1117
        }
 
1118
 
 
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
            });
 
1126
 
 
1127
            return;
 
1128
        }
 
1129
 
 
1130
        facade = Y.merge(options, {
 
1131
            index: index,
 
1132
            model: model
 
1133
        });
 
1134
 
 
1135
        if (options.silent) {
 
1136
            this._defRemoveFn(facade);
 
1137
        } else {
 
1138
            this.fire(EVT_REMOVE, facade);
 
1139
        }
 
1140
 
 
1141
        return model;
 
1142
    },
 
1143
 
 
1144
    /**
 
1145
    Array sort function used by `sort()` to re-sort the models in the list.
 
1146
 
 
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));
 
1159
 
 
1160
        // Early return when items are equal in their sort comparison.
 
1161
        if (result === 0) {
 
1162
            return result;
 
1163
        }
 
1164
 
 
1165
        // Flips sign when the sort is to be peformed in descending order.
 
1166
        return options && options.descending ? -result : result;
 
1167
    },
 
1168
 
 
1169
    // -- Event Handlers -------------------------------------------------------
 
1170
 
 
1171
    /**
 
1172
    Updates the model maps when a model's `id` attribute changes.
 
1173
 
 
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;
 
1182
 
 
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
        }
 
1198
 
 
1199
        if (Lang.isValue(newVal)) {
 
1200
            this._idMap[newVal] = target;
 
1201
        }
 
1202
    },
 
1203
 
 
1204
    // -- Default Event Handlers -----------------------------------------------
 
1205
 
 
1206
    /**
 
1207
    Default event handler for `add` events.
 
1208
 
 
1209
    @method _defAddFn
 
1210
    @param {EventFacade} e
 
1211
    @protected
 
1212
    **/
 
1213
    _defAddFn: function (e) {
 
1214
        var model = e.model,
 
1215
            id    = model.get('id');
 
1216
 
 
1217
        this._clientIdMap[model.get('clientId')] = model;
 
1218
 
 
1219
        if (Lang.isValue(id)) {
 
1220
            this._idMap[id] = model;
 
1221
        }
 
1222
 
 
1223
        this._attachList(model);
 
1224
        this._items.splice(e.index, 0, model);
 
1225
    },
 
1226
 
 
1227
    /**
 
1228
    Default event handler for `remove` events.
 
1229
 
 
1230
    @method _defRemoveFn
 
1231
    @param {EventFacade} e
 
1232
    @protected
 
1233
    **/
 
1234
    _defRemoveFn: function (e) {
 
1235
        var model = e.model,
 
1236
            id    = model.get('id');
 
1237
 
 
1238
        this._detachList(model);
 
1239
        delete this._clientIdMap[model.get('clientId')];
 
1240
 
 
1241
        if (Lang.isValue(id)) {
 
1242
            delete this._idMap[id];
 
1243
        }
 
1244
 
 
1245
        this._items.splice(e.index, 1);
 
1246
    },
 
1247
 
 
1248
    /**
 
1249
    Default event handler for `reset` events.
 
1250
 
 
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
        }
 
1262
 
 
1263
        this._clear();
 
1264
 
 
1265
        if (e.models.length) {
 
1266
            this.add(e.models, {silent: true});
 
1267
        }
 
1268
    }
 
1269
}, {
 
1270
    NAME: 'modelList'
 
1271
});
 
1272
 
 
1273
Y.augment(ModelList, Y.ArrayList);
 
1274
 
 
1275
 
 
1276
}, '3.13.0', {"requires": ["array-extras", "array-invoke", "arraylist", "base-build", "escape", "json-parse", "model"]});