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