2 Copyright (c) 2010, Yahoo! Inc. All rights reserved.
3 Code licensed under the BSD License:
4 http://developer.yahoo.com/yui/license.html
8 YUI.add('recordset-base', function(Y) {
11 * Provides a wrapper around a standard javascript object. Can be inserted into a Recordset instance.
15 var Record = Y.Base.create('record', Y.Base, [], {
20 initializer: function() {
23 destructor: function() {
27 * Retrieve a particular (or all) values from the object
29 * @param field {string} (optional) The key to retrieve the value from. If not supplied, the entire object is returned.
33 getValue: function(field) {
34 if (field === undefined) {
35 return this.get("data");
38 return this.get("data")[field];
47 * @description Unique ID of the record instance
56 * @description The object stored within the record instance
69 * The Recordset utility provides a standard way for dealing with
70 * a collection of similar objects.
72 * @submodule recordset-base
76 var ArrayList = Y.ArrayList,
80 * The Recordset utility provides a standard way for dealing with
81 * a collection of similar objects.
83 * Provides the base Recordset implementation, which can be extended to add
84 * additional functionality, such as custom indexing. sorting, and filtering.
89 * @param config {Object} Configuration object literal with initial attribute values
93 Recordset = Y.Base.create('recordset', Y.Base, [], {
97 * @description Publish default functions for events. Create the initial hash table.
102 initializer: function() {
105 If this._items does not exist, then create it and set it to an empty array.
106 The reason the conditional is needed is because of two scenarios:
107 Instantiating new Y.Recordset() will not go into the setter of "records", and so
108 it is necessary to create this._items in the initializer.
110 Instantiating new Y.Recordset({records: [{...}]}) will call the setter of "records" and create
111 this._items. In this case, we don't want that to be overwritten by [].
118 //set up event listener to fire events when recordset is modified in anyway
119 this.publish('add', {
120 defaultFn: this._defAddFn
122 this.publish('remove', {
123 defaultFn: this._defRemoveFn
125 this.publish('empty', {
126 defaultFn: this._defEmptyFn
128 this.publish('update', {
129 defaultFn: this._defUpdateFn
132 this._recordsetChanged();
133 //Fires recordset changed event when any updates are made to the recordset
134 this._syncHashTable();
135 //Fires appropriate hashTable methods on "add", "remove", "update" and "empty" events
138 destructor: function() {
142 * @description Helper method called upon by add() - it is used to create a new record(s) in the recordset
145 * @return {Y.Record} A Record instance.
148 _defAddFn: function(e) {
149 var len = this._items.length,
153 //index = (Lang.isNumber(index) && (index > -1)) ? index : len;
154 for (; i < recs.length; i++) {
155 //if records are to be added one at a time, push them in one at a time
157 this._items.push(recs[i]);
160 this._items.splice(index, 0, recs[i]);
169 * @description Helper method called upon by remove() - it is used to remove record(s) from the recordset
171 * @method _defRemoveFn
174 _defRemoveFn: function(e) {
176 //remove from beginning
182 else if (e.index === this._items.length - 1) {
188 this._items.splice(e.index, e.range);
193 * Helper method called upon by empty() - it is used to empty the recordset
195 * @method _defEmptyFn
198 _defEmptyFn: function(e) {
203 * @description Helper method called upon by update() - it is used to update the recordset
205 * @method _defUpdateFn
208 _defUpdateFn: function(e) {
210 for (var i = 0; i < e.updated.length; i++) {
211 this._items[e.index + i] = this._changeToRecord(e.updated[i]);
216 //---------------------------------------------
217 // Hash Table Methods
218 //---------------------------------------------
221 * @description Method called whenever "recordset:add" event is fired. It adds the new record(s) to the hashtable.
223 * @method _defAddHash
226 _defAddHash: function(e) {
227 var obj = this.get('table'),
228 key = this.get('key'),
230 for (; i < e.added.length; i++) {
231 obj[e.added[i].get(key)] = e.added[i];
233 this.set('table', obj);
237 * @description Method called whenever "recordset:remove" event is fired. It removes the record(s) from the recordset.
239 * @method _defRemoveHash
242 _defRemoveHash: function(e) {
243 var obj = this.get('table'),
244 key = this.get('key'),
246 for (; i < e.removed.length; i++) {
247 delete obj[e.removed[i].get(key)];
249 this.set('table', obj);
254 * @description Method called whenever "recordset:update" event is fired. It updates the record(s) by adding the new ones and removing the overwritten ones.
256 * @method _defUpdateHash
259 _defUpdateHash: function(e) {
260 var obj = this.get('table'),
261 key = this.get('key'),
264 //deletes the object key that held on to an overwritten record and
265 //creates an object key to hold on to the updated record
266 for (; i < e.updated.length; i++) {
267 if (e.overwritten[i]) {
268 delete obj[e.overwritten[i].get(key)];
270 obj[e.updated[i].get(key)] = e.updated[i];
272 this.set('table', obj);
276 * @description Method called whenever "recordset:empty" event is fired. It empties the hash table.
278 * @method _defEmptyHash
281 _defEmptyHash: function() {
282 this.set('table', {});
286 * @description Sets up the hashtable with all the records currently in the recordset
288 * @method _setHashTable
291 _setHashTable: function() {
293 key = this.get('key'),
296 //If it is not an empty recordset - go through and set up the hash table.
297 if (this._items && this._items.length > 0) {
298 var len = this._items.length;
299 for (; i < len; i++) {
300 obj[this._items[i].get(key)] = this._items[i];
308 * @description Helper method - it takes an object bag and converts it to a Y.Record
310 * @method _changeToRecord
311 * @param obj {Object || Y.Record} Any objet literal or Y.Record instance
312 * @return {Y.Record} A Record instance.
315 _changeToRecord: function(obj) {
317 if (obj instanceof Y.Record) {
321 oRec = new Y.Record({
329 //---------------------------------------------
331 //---------------------------------------------
333 * @description Event that is fired whenever the recordset is changed. Note that multiple simultaneous changes still fires this event once. (ie: Adding multiple records via an array will only fire this event once at the completion of all the additions)
335 * @method _recordSetUpdated
338 _recordsetChanged: function() {
340 this.on(['update', 'add', 'remove', 'empty'],
342 this.fire('change', {});
348 * @description Syncs up the private hash methods with their appropriate triggering events.
350 * @method _syncHashTable
353 _syncHashTable: function() {
361 this._defRemoveHash(e);
365 this._defUpdateHash(e);
369 this._defEmptyHash();
374 //---------------------------------------------
376 //---------------------------------------------
378 * @description Returns the record with particular ID or index
381 * @param i {String, Number} The ID of the record if a string, or the index if a number.
382 * @return {Y.Record} An Y.Record instance
385 getRecord: function(i) {
387 if (Lang.isString(i)) {
388 return this.get('table')[i];
390 else if (Lang.isNumber(i)) {
391 return this._items[i];
398 * @description Returns the record at a particular index
400 * @method getRecordByIndex
401 * @param i {Number} Index at which the required record resides
402 * @return {Y.Record} An Y.Record instance
405 getRecordByIndex: function(i) {
406 return this._items[i];
410 * @description Returns a range of records beginning at particular index
412 * @method getRecordsByIndex
413 * @param index {Number} Index at which the required record resides
414 * @param range {Number} (Optional) Number of records to retrieve. The default is 1
415 * @return {Array} An array of Y.Record instances
418 getRecordsByIndex: function(index, range) {
420 returnedRecords = [];
421 //Range cannot take on negative values
422 range = (Lang.isNumber(range) && (range > 0)) ? range: 1;
424 for (; i < range; i++) {
425 returnedRecords.push(this._items[index + i]);
427 return returnedRecords;
431 * @description Returns the length of the recordset
434 * @return {Number} Number of records in the recordset
437 getLength: function() {
442 * @description Returns an array of values for a specified key in the recordset
444 * @method getValuesByKey
445 * @param index {Number} (optional) Index at which the required record resides
446 * @return {Array} An array of values for the given key
449 getValuesByKey: function(key) {
451 len = this._items.length,
453 for (; i < len; i++) {
454 retVals.push(this._items[i].getValue(key));
461 * @description Adds one or more Records to the RecordSet at the given index. If index is null, then adds the Records to the end of the RecordSet.
464 * @param oData {Y.Record, Object Literal, Array} A Y.Record instance, An object literal of data or an array of object literals
465 * @param index {Number} (optional) Index at which to add the record(s)
466 * @return {Y.Recordset} The updated recordset instance
469 add: function(oData, index) {
475 idx = (Lang.isNumber(index) && (index > -1)) ? index: this._items.length;
479 //Passing in array of object literals for oData
480 if (Lang.isArray(oData)) {
481 for (; i < oData.length; i++) {
482 newRecords[i] = this._changeToRecord(oData[i]);
486 else if (Lang.isObject(oData)) {
487 newRecords[0] = this._changeToRecord(oData);
498 * @description Removes one or more Records to the RecordSet at the given index. If index is null, then removes a single Record from the end of the RecordSet.
501 * @param index {Number} (optional) Index at which to remove the record(s) from
502 * @param range {Number} (optional) Number of records to remove (including the one at the index)
503 * @return {Y.Recordset} The updated recordset instance
506 remove: function(index, range) {
509 //Default is to only remove the last record - the length is always 1 greater than the last index
510 index = (index > -1) ? index: (this._items.length - 1);
511 range = (range > 0) ? range: 1;
513 remRecords = this._items.slice(index, (index + range));
514 this.fire('remove', {
519 //this._recordRemoved(remRecords, index);
520 //return ({data: remRecords, index:index});
525 * @description Empties the recordset
528 * @return {Y.Recordset} The updated recordset instance
532 this.fire('empty', {});
537 * @description Updates the recordset with the new records passed in. Overwrites existing records when updating the index with the new records.
540 * @param data {Y.Record, Object Literal, Array} A Y.Record instance, An object literal of data or an array of object literals
541 * @param index {Number} (optional) The index to start updating from.
542 * @return {Y.Recordset} The updated recordset instance
545 update: function(data, index) {
550 //Whatever is passed in, we are changing it to an array so that it can be easily iterated in the _defUpdateFn method
551 arr = (!(Lang.isArray(data))) ? [data] : data;
552 rec = this._items.slice(index, index + arr.length);
554 for (; i < arr.length; i++) {
555 arr[i] = this._changeToRecord(arr[i]);
558 this.fire('update', {
573 * @description An array of records that the recordset is storing
580 validator: Lang.isArray,
582 // give them a copy, not the internal object
583 return new Y.Array(this._items);
585 setter: function(allData) {
586 //For allData passed in here, see if each instance is a Record.
587 //If its not, change it to a record.
588 //Then push it into the array.
590 function initRecord(oneData) {
593 if (oneData instanceof Y.Record) {
594 records.push(oneData);
604 //This conditional statement handles creating empty recordsets
606 Y.Array.each(allData, initRecord);
607 this._items = Y.Array(records);
612 //initialization of the attribute must be done before the first call to get('records') is made.
613 //if lazyAdd were set to true, then instantiating using new Y.Recordset({records:[..]}) would
614 //not call the setter.
615 //see http://developer.yahoo.com/yui/3/api/Attribute.html#method_addAttr for details on this
620 * @description A hash table where the ID of the record is the key, and the record instance is the value.
627 //Initially, create the hash table with all records currently in the recordset
628 valueFn: '_setHashTable'
632 * @description The ID to use as the key in the hash table.
640 //set to readonly true. If you want custom hash tables, you should use the recordset-indexer plugin.
646 Y.augment(Recordset, ArrayList);
647 Y.Recordset = Recordset;
652 }, '3.3.0' ,{requires:['base','arraylist']});
654 YUI.add('recordset-sort', function(Y) {
657 * Adds default and custom sorting functionality to the Recordset utility
659 * @submodule recordset-sort
662 var Compare = Y.ArraySort.compare,
663 isValue = Y.Lang.isValue;
666 * Plugin that adds default and custom sorting functionality to the Recordset utility
667 * @class RecordsetSort
670 function RecordsetSort(field, desc, sorter) {
671 RecordsetSort.superclass.constructor.apply(this, arguments);
674 Y.mix(RecordsetSort, {
677 NAME: "recordsetSort",
682 * @description The last properties used to sort. Consists of an object literal with the keys "field", "desc", and "sorter"
684 * @attribute lastSortProperties
688 lastSortProperties: {
694 validator: function(v) {
695 return (isValue(v.field) && isValue(v.desc) && isValue(v.sorter));
700 * @description Default sort function to use if none is specified by the user.
701 * Takes two records, the key to sort by, and whether sorting direction is descending or not (boolean).
702 * If two records have the same value for a given key, the ID is used as the tie-breaker.
704 * @attribute defaultSorter
709 value: function(recA, recB, field, desc) {
710 var sorted = Compare(recA.getValue(field), recB.getValue(field), desc);
712 return Compare(recA.get("id"), recB.get("id"), desc);
721 * @description A boolean telling if the recordset is in a sorted state.
723 * @attribute defaultSorter
733 Y.extend(RecordsetSort, Y.Plugin.Base, {
736 * @description Sets up the default function to use when the "sort" event is fired.
738 * @method initializer
741 initializer: function(config) {
744 host = this.get('host');
747 this.publish("sort", {
748 defaultFn: Y.bind("_defSortFn", this)
751 //Toggle the isSorted ATTR based on events.
752 //Remove events dont affect isSorted, as they are just popped/sliced out
755 self.set('isSorted', true);
758 this.onHostEvent('add',
760 self.set('isSorted', false);
763 this.onHostEvent('update',
765 self.set('isSorted', false);
771 destructor: function(config) {
775 * @description Method that all sort calls go through.
776 * Sets up the lastSortProperties object with the details of the sort, and passes in parameters
777 * to the "defaultSorter" or a custom specified sort function.
782 _defSortFn: function(e) {
783 //have to work directly with _items here - changing the recordset.
784 this.get("host")._items.sort(function(a, b) {
785 return (e.sorter)(a, b, e.field, e.desc);
788 this.set('lastSortProperties', e);
792 * @description Sorts the recordset.
795 * @param field {string} A key to sort by.
796 * @param desc {boolean} True if you want sort order to be descending, false if you want sort order to be ascending
799 sort: function(field, desc, sorter) {
803 sorter: sorter || this.get("defaultSorter")
808 * @description Resorts the recordset based on the last-used sort parameters (stored in 'lastSortProperties' ATTR)
814 var p = this.get('lastSortProperties');
818 sorter: p.sorter || this.get("defaultSorter")
823 * @description Reverses the recordset calling the standard array.reverse() method.
828 reverse: function() {
829 this.get('host')._items.reverse();
833 * @description Sorts the recordset based on the last-used sort parameters, but flips the order. (ie: Descending becomes ascending, and vice versa).
839 var p = this.get('lastSortProperties');
841 //If a predefined field is not provided by which to sort by, throw an error
842 if (isValue(p.field)) {
846 sorter: p.sorter || this.get("defaultSorter")
854 Y.namespace("Plugin").RecordsetSort = RecordsetSort;
859 }, '3.3.0' ,{requires:['arraysort','recordset-base','plugin']});
861 YUI.add('recordset-filter', function(Y) {
864 * Plugin that provides the ability to filter through a recordset.
865 * Uses the filter methods available on Y.Array (see arrayextras submodule) to filter the recordset.
867 * @submodule recordset-filter
871 var YArray = Y.Array,
876 * Plugin that provides the ability to filter through a recordset.
877 * Uses the filter methods available on Y.Array (see arrayextras submodule) to filter the recordset.
878 * @class RecordsetFilter
880 function RecordsetFilter(config) {
881 RecordsetFilter.superclass.constructor.apply(this, arguments);
884 Y.mix(RecordsetFilter, {
887 NAME: "recordsetFilter",
895 Y.extend(RecordsetFilter, Y.Plugin.Base, {
898 initializer: function(config) {
901 destructor: function(config) {
905 * @description Filter through the recordset with a custom filter function, or a key-value pair.
908 * @param f {Function, String} A custom filter function or a string representing the key to filter by.
909 * @param v {any} (optional) If a string is passed into f, this represents the value that key should take in order to be accepted by the filter. Do not pass in anything if 'f' is a custom function
910 * @return recordset {Y.Recordset} A new filtered recordset instance
913 filter: function(f, v) {
914 var recs = this.get('host').get('records'),
918 //If a key-value pair is passed in, generate a custom function
919 if (Lang.isString(f) && Lang.isValue(v)) {
921 func = function(item) {
922 if (item.getValue(f) === v) {
931 oRecs = YArray.filter(recs, func);
934 //TODO: PARENT CHILD RELATIONSHIP
935 return new Y.Recordset({
938 //return new host.constructor({records:arr});
942 * @description The inverse of filter. Executes the supplied function on each item. Returns a new Recordset containing the items that the supplied function returned *false* for.
944 * @param {Function} f is the function to execute on each item.
945 * @return {Y.Recordset} A new Recordset instance containing the items on which the supplied function returned false.
947 reject: function(f) {
948 return new Y.Recordset({
949 records: YArray.reject(this.get('host').get('records'), f)
954 * @description Iterates over the Recordset, returning a new Recordset of all the elements that match the supplied regular expression
956 * @param {pattern} pattern The regular expression to test against
958 * @return {Y.Recordset} A Recordset instance containing all the items in the collection that produce a match against the supplied regular expression. If no items match, an empty Recordset instance is returned.
960 grep: function(pattern) {
961 return new Y.Recordset({
962 records: YArray.grep(this.get('host').get('records'), pattern)
966 //TODO: Add more pass-through methods to arrayextras
969 Y.namespace("Plugin").RecordsetFilter = RecordsetFilter;
974 }, '3.3.0' ,{requires:['recordset-base','array-extras','plugin']});
976 YUI.add('recordset-indexer', function(Y) {
979 * Provides the ability to store multiple custom hash tables referencing records in the recordset.
981 * @submodule recordset-indexer
984 * Plugin that provides the ability to store multiple custom hash tables referencing records in the recordset.
985 * This utility does not support any collision handling. New hash table entries with a used key overwrite older ones.
986 * @class RecordsetIndexer
988 function RecordsetIndexer(config) {
989 RecordsetIndexer.superclass.constructor.apply(this, arguments);
992 Y.mix(RecordsetIndexer, {
995 NAME: "recordsetIndexer",
999 * @description Collection of all the hashTables created by the plugin.
1000 * The individual tables can be accessed by the key they are hashing against.
1002 * @attribute hashTables
1022 Y.extend(RecordsetIndexer, Y.Plugin.Base, {
1024 initializer: function(config) {
1025 var host = this.get('host');
1027 //setup listeners on recordset events
1028 this.onHostEvent('add', Y.bind("_defAddHash", this), host);
1029 this.onHostEvent('remove', Y.bind('_defRemoveHash', this), host);
1030 this.onHostEvent('update', Y.bind('_defUpdateHash', this), host);
1034 destructor: function(config) {
1040 * @description Setup the hash table for a given key with all existing records in the recordset
1042 * @method _setHashTable
1043 * @param key {string} A key to hash by.
1044 * @return obj {object} The created hash table
1047 _setHashTable: function(key) {
1048 var host = this.get('host'),
1051 len = host.getLength();
1053 for (; i < len; i++) {
1054 obj[host._items[i].getValue(key)] = host._items[i];
1059 //---------------------------------------------
1061 //---------------------------------------------
1064 * @description Updates all hash tables when a record is added to the recordset
1066 * @method _defAddHash
1069 _defAddHash: function(e) {
1070 var tbl = this.get('hashTables');
1073 //Go through every hashtable that is stored.
1074 //in each hashtable, look to see if the key is represented in the object being added.
1077 Y.each(e.added || e.updated,
1079 //if the object being added has a key which is being stored by hashtable v, add it into the table.
1080 if (o.getValue(key)) {
1081 v[o.getValue(key)] = o;
1089 * @description Updates all hash tables when a record is removed from the recordset
1091 * @method _defRemoveHash
1094 _defRemoveHash: function(e) {
1095 var tbl = this.get('hashTables'),
1098 //Go through every hashtable that is stored.
1099 //in each hashtable, look to see if the key is represented in the object being deleted.
1102 Y.each(e.removed || e.overwritten,
1104 reckey = o.getValue(key);
1106 //if the hashtable has a key storing a record, and the key and the record both match the record being deleted, delete that row from the hashtable
1107 if (reckey && v[reckey] === o) {
1115 * @description Updates all hash tables when the recordset is updated (a combination of add and remove)
1117 * @method _defUpdateHash
1120 _defUpdateHash: function(e) {
1122 //TODO: It will be more performant to create a new method rather than using _defAddHash, _defRemoveHash, due to the number of loops. See commented code.
1123 e.added = e.updated;
1124 e.removed = e.overwritten;
1125 this._defAddHash(e);
1126 this._defRemoveHash(e);
1129 var tbl = this.get('hashTables'), reckey;
1131 Y.each(tbl, function(v, key) {
1132 Y.each(e.updated, function(o, i) {
1134 //delete record from hashtable if it has been overwritten
1135 reckey = o.getValue(key);
1141 //the undefined case is if more records are updated than currently exist in the recordset.
1142 if (e.overwritten[i] && (v[e.overwritten[i].getValue(key)] === e.overwritten[i])) {
1143 delete v[e.overwritten[i].getValue(key)];
1146 // if (v[reckey] === o) {
1147 // delete v[reckey];
1150 // //add the new updated record if it has a key that corresponds to a hash table
1151 // if (o.getValue(key)) {
1152 // v[o.getValue(key)] = o;
1160 //---------------------------------------------
1162 //---------------------------------------------
1165 * @description Creates a new hash table.
1167 * @method createTable
1168 * @param key {string} A key to hash by.
1169 * @return tbls[key] {object} The created hash table
1172 createTable: function(key) {
1173 var tbls = this.get('hashTables');
1174 tbls[key] = this._setHashTable(key);
1175 this.set('hashTables', tbls);
1182 * @description Get a hash table that hashes records by a given key.
1185 * @param key {string} A key to hash by.
1186 * @return table {object} The created hash table
1189 getTable: function(key) {
1190 return this.get('hashTables')[key];
1198 Y.namespace("Plugin").RecordsetIndexer = RecordsetIndexer;
1203 }, '3.3.0' ,{requires:['recordset-base','plugin']});
1207 YUI.add('recordset', function(Y){}, '3.3.0' ,{use:['recordset-base','recordset-sort','recordset-filter','recordset-indexer']});