Proyectos de Subversion Moodle

Rev

| Ultima modificación | Ver Log |

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