Proyectos de Subversion Moodle

Rev

| Ultima modificación | Ver Log |

Rev Autor Línea Nro. Línea
1 efrain 1
YUI.add('lazy-model-list', function (Y, NAME) {
2
 
3
/**
4
Provides the LazyModelList class, which is a ModelList subclass that manages
5
plain objects instead of fully instantiated model instances.
6
 
7
@module app
8
@submodule lazy-model-list
9
@since 3.6.0
10
**/
11
 
12
/**
13
LazyModelList is a subclass of ModelList that maintains a list of plain
14
JavaScript objects rather than a list of Model instances. This makes it
15
well-suited for managing large amounts of data (on the order of thousands of
16
items) that would tend to bog down a vanilla ModelList.
17
 
18
The API presented by LazyModelList is the same as that of ModelList, except that
19
in every case where ModelList would provide a Model instance, LazyModelList
20
provides a plain JavaScript object. LazyModelList also provides a `revive()`
21
method that can convert the plain object at a given index into a full Model
22
instance.
23
 
24
Since the items stored in a LazyModelList are plain objects and not full Model
25
instances, there are a few caveats to be aware of:
26
 
27
  * Since items are plain objects and not Model instances, they contain
28
    properties rather than Model attributes. To retrieve a property, use
29
    `item.foo` rather than `item.get('foo')`. To set a property, use
30
    `item.foo = 'bar'` rather than `item.set('foo', 'bar')`.
31
 
32
  * Model attribute getters and setters aren't supported, since items in the
33
    LazyModelList are stored and manipulated as plain objects with simple
34
    properties rather than YUI attributes.
35
 
36
  * Changes made to the plain object version of an item will not trigger or
37
    bubble up Model `change` events. However, once an item is revived into a
38
    full Model using the `revive()` method, changes to that Model instance
39
    will trigger and bubble change events as expected.
40
 
41
  * Custom `idAttribute` fields are not supported.
42
 
43
  * `id` and `clientId` properties _are_ supported. If an item doesn't have a
44
    `clientId` property, one will be generated automatically when the item is
45
    added to a LazyModelList.
46
 
47
LazyModelList is generally much more memory efficient than ModelList when
48
managing large numbers of items, and adding/removing items is significantly
49
faster. However, the tradeoff is that LazyModelList is only well-suited for
50
storing very simple items without complex attributes, and consumers must
51
explicitly revive items into full Model instances as needed (this is not done
52
transparently for performance reasons).
53
 
54
@class LazyModelList
55
@extends ModelList
56
@constructor
57
@since 3.6.0
58
**/
59
 
60
var AttrProto = Y.Attribute.prototype,
61
    GlobalEnv = YUI.namespace('Env.Model'),
62
    Lang      = Y.Lang,
63
    YArray    = Y.Array,
64
 
65
    EVT_ADD   = 'add',
66
    EVT_ERROR = 'error',
67
    EVT_RESET = 'reset';
68
 
69
Y.LazyModelList = Y.Base.create('lazyModelList', Y.ModelList, [], {
70
    // -- Lifecycle ------------------------------------------------------------
71
    initializer: function () {
72
        this.after('*:change', this._afterModelChange);
73
    },
74
 
75
    // -- Public Methods -------------------------------------------------------
76
 
77
    /**
78
    Deletes the specified model from the model cache to release memory. The
79
    model won't be destroyed or removed from the list, just freed from the
80
    cache; it can still be instantiated again using `revive()`.
81
 
82
    If no model or model index is specified, all cached models in this list will
83
    be freed.
84
 
85
    Note: Specifying an index is faster than specifying a model instance, since
86
    the latter requires an `indexOf()` call.
87
 
88
    @method free
89
    @param {Model|Number} [model] Model or index of the model to free. If not
90
        specified, all instantiated models in this list will be freed.
91
    @chainable
92
    @see revive()
93
    **/
94
    free: function (model) {
95
        var index;
96
 
97
        if (model) {
98
            index = Lang.isNumber(model) ? model : this.indexOf(model);
99
 
100
            if (index >= 0) {
101
                // We don't detach the model because it's not being removed from
102
                // the list, just being freed from memory. If something else
103
                // still holds a reference to it, it may still bubble events to
104
                // the list, but that's okay.
105
                //
106
                // `this._models` is a sparse array, which ensures that the
107
                // indices of models and items match even if we don't have model
108
                // instances for all items.
109
                delete this._models[index];
110
            }
111
        } else {
112
            this._models = [];
113
        }
114
 
115
        return this;
116
    },
117
 
118
    /**
119
    Overrides ModelList#get() to return a map of property values rather than
120
    performing attribute lookups.
121
 
122
    @method get
123
    @param {String} name Property name.
124
    @return {String[]} Array of property values.
125
    @see ModelList.get()
126
    **/
127
    get: function (name) {
128
        if (this.attrAdded(name)) {
129
            return AttrProto.get.apply(this, arguments);
130
        }
131
 
132
        return YArray.map(this._items, function (item) {
133
            return item[name];
134
        });
135
    },
136
 
137
    /**
138
    Overrides ModelList#getAsHTML() to return a map of HTML-escaped property
139
    values rather than performing attribute lookups.
140
 
141
    @method getAsHTML
142
    @param {String} name Property name.
143
    @return {String[]} Array of HTML-escaped property values.
144
    @see ModelList.getAsHTML()
145
    **/
146
    getAsHTML: function (name) {
147
        if (this.attrAdded(name)) {
148
            return Y.Escape.html(AttrProto.get.apply(this, arguments));
149
        }
150
 
151
        return YArray.map(this._items, function (item) {
152
            return Y.Escape.html(item[name]);
153
        });
154
    },
155
 
156
    /**
157
    Overrides ModelList#getAsURL() to return a map of URL-encoded property
158
    values rather than performing attribute lookups.
159
 
160
    @method getAsURL
161
    @param {String} name Property name.
162
    @return {String[]} Array of URL-encoded property values.
163
    @see ModelList.getAsURL()
164
    **/
165
    getAsURL: function (name) {
166
        if (this.attrAdded(name)) {
167
            return encodeURIComponent(AttrProto.get.apply(this, arguments));
168
        }
169
 
170
        return YArray.map(this._items, function (item) {
171
            return encodeURIComponent(item[name]);
172
        });
173
    },
174
 
175
    /**
176
    Returns the index of the given object or Model instance in this
177
    LazyModelList.
178
 
179
    @method indexOf
180
    @param {Model|Object} needle The object or Model instance to search for.
181
    @return {Number} Item index, or `-1` if not found.
182
    @see ModelList.indexOf()
183
    **/
184
    indexOf: function (model) {
185
        return YArray.indexOf(model && model._isYUIModel ?
186
            this._models : this._items, model);
187
    },
188
 
189
    /**
190
    Overrides ModelList#reset() to work with plain objects.
191
 
192
    @method reset
193
    @param {Object[]|Model[]|ModelList} [models] Models to add.
194
    @param {Object} [options] Options.
195
    @chainable
196
    @see ModelList.reset()
197
    **/
198
    reset: function (items, options) {
199
        items || (items  = []);
200
        options || (options = {});
201
 
202
        var facade = Y.merge({src: 'reset'}, options);
203
 
204
        // Convert `items` into an array of plain objects, since we don't want
205
        // model instances.
206
        items = items._isYUIModelList ? items.map(this._modelToObject) :
207
            YArray.map(items, this._modelToObject);
208
 
209
        facade.models = items;
210
 
211
        if (options.silent) {
212
            this._defResetFn(facade);
213
        } else {
214
            // Sort the items before firing the reset event.
215
            if (this.comparator) {
216
                items.sort(Y.bind(this._sort, this));
217
            }
218
 
219
            this.fire(EVT_RESET, facade);
220
        }
221
 
222
        return this;
223
    },
224
 
225
    /**
226
    Revives an item (or all items) into a full Model instance. The _item_
227
    argument may be the index of an object in this list, an actual object (which
228
    must exist in the list), or may be omitted to revive all items in the list.
229
 
230
    Once revived, Model instances are attached to this list and cached so that
231
    reviving them in the future doesn't require another Model instantiation. Use
232
    the `free()` method to explicitly uncache and detach a previously revived
233
    Model instance.
234
 
235
    Note: Specifying an index rather than an object will be faster, since
236
    objects require an `indexOf()` lookup in order to retrieve the index.
237
 
238
    @method revive
239
    @param {Number|Object} [item] Index of the object to revive, or the object
240
        itself. If an object, that object must exist in this list. If not
241
        specified, all items in the list will be revived and an array of models
242
        will be returned.
243
    @return {Model|Model[]|null} Revived Model instance, array of revived Model
244
        instances, or `null` if the given index or object was not found in this
245
        list.
246
    @see free()
247
    **/
248
    revive: function (item) {
249
        var i, len, models;
250
 
251
        if (item || item === 0) {
252
            return this._revive(Lang.isNumber(item) ? item :
253
                this.indexOf(item));
254
        } else {
255
            models = [];
256
 
257
            for (i = 0, len = this._items.length; i < len; i++) {
258
                models.push(this._revive(i));
259
            }
260
 
261
            return models;
262
        }
263
    },
264
 
265
    /**
266
    Overrides ModelList#toJSON() to use toArray() instead, since it's more
267
    efficient for LazyModelList.
268
 
269
    @method toJSON
270
    @return {Object[]} Array of objects.
271
    @see ModelList.toJSON()
272
    **/
273
    toJSON: function () {
274
        return this.toArray();
275
    },
276
 
277
    // -- Protected Methods ----------------------------------------------------
278
 
279
    /**
280
    Overrides ModelList#add() to work with plain objects.
281
 
282
    @method _add
283
    @param {Object|Model} item Object or model to add.
284
    @param {Object} [options] Options.
285
    @return {Object} Added item.
286
    @protected
287
    @see ModelList._add()
288
    **/
289
    _add: function (item, options) {
290
        var facade;
291
 
292
        options || (options = {});
293
 
294
        // If the item is a model instance, convert it to a plain object.
295
        item = this._modelToObject(item);
296
 
297
        // Ensure that the item has a clientId.
298
        if (!('clientId' in item)) {
299
            item.clientId = this._generateClientId();
300
        }
301
 
302
        if (this._isInList(item)) {
303
            this.fire(EVT_ERROR, {
304
                error: 'Model is already in the list.',
305
                model: item,
306
                src  : 'add'
307
            });
308
 
309
            return;
310
        }
311
 
312
        facade = Y.merge(options, {
313
            index: 'index' in options ? options.index : this._findIndex(item),
314
            model: item
315
        });
316
 
317
        options.silent ? this._defAddFn(facade) : this.fire(EVT_ADD, facade);
318
 
319
        return item;
320
    },
321
 
322
    /**
323
    Overrides ModelList#clear() to support `this._models`.
324
 
325
    @method _clear
326
    @protected
327
    @see ModelList.clear()
328
    **/
329
    _clear: function () {
330
        YArray.each(this._models, this._detachList, this);
331
 
332
        this._clientIdMap = {};
333
        this._idMap       = {};
334
        this._items       = [];
335
        this._models      = [];
336
    },
337
 
338
    /**
339
    Generates an ad-hoc clientId for a non-instantiated Model.
340
 
341
    @method _generateClientId
342
    @return {String} Unique clientId.
343
    @protected
344
    **/
345
    _generateClientId: function () {
346
        GlobalEnv.lastId || (GlobalEnv.lastId = 0);
347
        return this.model.NAME + '_' + (GlobalEnv.lastId += 1);
348
    },
349
 
350
    /**
351
    Returns `true` if the given item is in this list, `false` otherwise.
352
 
353
    @method _isInList
354
    @param {Object} item Plain object item.
355
    @return {Boolean} `true` if the item is in this list, `false` otherwise.
356
    @protected
357
    **/
358
    _isInList: function (item) {
359
        return !!(('clientId' in item && this._clientIdMap[item.clientId]) ||
360
                ('id' in item && this._idMap[item.id]));
361
    },
362
 
363
    /**
364
    Converts a Model instance into a plain object. If _model_ is not a Model
365
    instance, it will be returned as is.
366
 
367
    This method differs from Model#toJSON() in that it doesn't delete the
368
    `clientId` property.
369
 
370
    @method _modelToObject
371
    @param {Model|Object} model Model instance to convert.
372
    @return {Object} Plain object.
373
    @protected
374
    **/
375
    _modelToObject: function (model) {
376
        if (model._isYUIModel) {
377
            model = model.getAttrs();
378
            delete model.destroyed;
379
            delete model.initialized;
380
        }
381
 
382
        return model;
383
    },
384
 
385
    /**
386
    Overrides ModelList#_remove() to convert Model instances to indices
387
    before removing to ensure consistency in the `remove` event facade.
388
 
389
    @method _remove
390
    @param {Object|Model} item Object or model to remove.
391
    @param {Object} [options] Options.
392
    @return {Object} Removed object.
393
    @protected
394
    **/
395
    _remove: function (item, options) {
396
        // If the given item is a model instance, turn it into an index before
397
        // calling the parent _remove method, since we only want to deal with
398
        // the plain object version.
399
        if (item._isYUIModel) {
400
            item = this.indexOf(item);
401
        }
402
 
403
        return Y.ModelList.prototype._remove.call(this, item, options);
404
    },
405
 
406
    /**
407
    Revives a single model at the specified index and returns it. This is the
408
    underlying implementation for `revive()`.
409
 
410
    @method _revive
411
    @param {Number} index Index of the item to revive.
412
    @return {Model} Revived model.
413
    @protected
414
    **/
415
    _revive: function (index) {
416
        var item, model;
417
 
418
        if (index < 0) {
419
            return null;
420
        }
421
 
422
        item = this._items[index];
423
 
424
        if (!item) {
425
            return null;
426
        }
427
 
428
        model = this._models[index];
429
 
430
        if (!model) {
431
            model = new this.model(item);
432
 
433
            // The clientId attribute is read-only, but revived models should
434
            // have the same clientId as the original object, so we need to set
435
            // it manually.
436
            model._set('clientId', item.clientId);
437
 
438
            this._attachList(model);
439
            this._models[index] = model;
440
        }
441
 
442
        return model;
443
    },
444
 
445
    // -- Event Handlers -------------------------------------------------------
446
 
447
    /**
448
    Handles `change` events on revived models and updates the original objects
449
    with the changes.
450
 
451
    @method _afterModelChange
452
    @param {EventFacade} e
453
    @protected
454
    **/
455
    _afterModelChange: function (e) {
456
        var changed = e.changed,
457
            item    = this._clientIdMap[e.target.get('clientId')],
458
            name;
459
 
460
        if (item) {
461
            for (name in changed) {
462
                if (changed.hasOwnProperty(name)) {
463
                    item[name] = changed[name].newVal;
464
                }
465
            }
466
        }
467
    },
468
 
469
    // -- Default Event Handlers -----------------------------------------------
470
 
471
    /**
472
    Overrides ModelList#_defAddFn() to support plain objects.
473
 
474
    @method _defAddFn
475
    @param {EventFacade} e
476
    @protected
477
    **/
478
    _defAddFn: function (e) {
479
        var item = e.model;
480
 
481
        this._clientIdMap[item.clientId] = item;
482
 
483
        if (Lang.isValue(item.id)) {
484
            this._idMap[item.id] = item;
485
        }
486
 
487
        this._items.splice(e.index, 0, item);
488
    },
489
 
490
    /**
491
    Overrides ModelList#_defRemoveFn() to support plain objects.
492
 
493
    @method _defRemoveFn
494
    @param {EventFacade} e
495
    @protected
496
    **/
497
    _defRemoveFn: function (e) {
498
        var index = e.index,
499
            item  = e.model,
500
            model = this._models[index];
501
 
502
        delete this._clientIdMap[item.clientId];
503
 
504
        if ('id' in item) {
505
            delete this._idMap[item.id];
506
        }
507
 
508
        if (model) {
509
            this._detachList(model);
510
        }
511
 
512
        this._items.splice(index, 1);
513
        this._models.splice(index, 1);
514
    }
515
});
516
 
517
 
518
}, '3.18.1', {"requires": ["model-list"]});