/* Copyright (c) 2010, Yahoo! Inc. All rights reserved. Code licensed under the BSD License: http://developer.yahoo.com/yui/license.html version: 3.3.0 build: 3167 */ YUI.add('recordset-base', function(Y) { /** * Provides a wrapper around a standard javascript object. Can be inserted into a Recordset instance. * * @class Record */ var Record = Y.Base.create('record', Y.Base, [], { _setId: function() { return Y.guid(); }, initializer: function() { }, destructor: function() { }, /** * Retrieve a particular (or all) values from the object * * @param field {string} (optional) The key to retrieve the value from. If not supplied, the entire object is returned. * @method getValue * @public */ getValue: function(field) { if (field === undefined) { return this.get("data"); } else { return this.get("data")[field]; } return null; } }, { ATTRS: { /** * @description Unique ID of the record instance * @attribute id * @type string */ id: { valueFn: "_setId" }, /** * @description The object stored within the record instance * @attribute data * @type object */ data: { value: null } } }); Y.Record = Record; /** * The Recordset utility provides a standard way for dealing with * a collection of similar objects. * @module recordset * @submodule recordset-base */ var ArrayList = Y.ArrayList, Lang = Y.Lang, /** * The Recordset utility provides a standard way for dealing with * a collection of similar objects. * * Provides the base Recordset implementation, which can be extended to add * additional functionality, such as custom indexing. sorting, and filtering. * * @class Recordset * @extends Base * @augments ArrayList * @param config {Object} Configuration object literal with initial attribute values * @constructor */ Recordset = Y.Base.create('recordset', Y.Base, [], { /** * @description Publish default functions for events. Create the initial hash table. * * @method initializer * @protected */ initializer: function() { /* If this._items does not exist, then create it and set it to an empty array. The reason the conditional is needed is because of two scenarios: Instantiating new Y.Recordset() will not go into the setter of "records", and so it is necessary to create this._items in the initializer. Instantiating new Y.Recordset({records: [{...}]}) will call the setter of "records" and create this._items. In this case, we don't want that to be overwritten by []. */ if (!this._items) { this._items = []; } //set up event listener to fire events when recordset is modified in anyway this.publish('add', { defaultFn: this._defAddFn }); this.publish('remove', { defaultFn: this._defRemoveFn }); this.publish('empty', { defaultFn: this._defEmptyFn }); this.publish('update', { defaultFn: this._defUpdateFn }); this._recordsetChanged(); //Fires recordset changed event when any updates are made to the recordset this._syncHashTable(); //Fires appropriate hashTable methods on "add", "remove", "update" and "empty" events }, destructor: function() { }, /** * @description Helper method called upon by add() - it is used to create a new record(s) in the recordset * * @method _defAddFn * @return {Y.Record} A Record instance. * @private */ _defAddFn: function(e) { var len = this._items.length, recs = e.added, index = e.index, i = 0; //index = (Lang.isNumber(index) && (index > -1)) ? index : len; for (; i < recs.length; i++) { //if records are to be added one at a time, push them in one at a time if (index === len) { this._items.push(recs[i]); } else { this._items.splice(index, 0, recs[i]); index++; } } }, /** * @description Helper method called upon by remove() - it is used to remove record(s) from the recordset * * @method _defRemoveFn * @private */ _defRemoveFn: function(e) { //remove from beginning if (e.index === 0) { this._items.shift(); } //remove from end else if (e.index === this._items.length - 1) { this._items.pop(); } //remove from middle else { this._items.splice(e.index, e.range); } }, /** * Helper method called upon by empty() - it is used to empty the recordset * * @method _defEmptyFn * @private */ _defEmptyFn: function(e) { this._items = []; }, /** * @description Helper method called upon by update() - it is used to update the recordset * * @method _defUpdateFn * @private */ _defUpdateFn: function(e) { for (var i = 0; i < e.updated.length; i++) { this._items[e.index + i] = this._changeToRecord(e.updated[i]); } }, //--------------------------------------------- // Hash Table Methods //--------------------------------------------- /** * @description Method called whenever "recordset:add" event is fired. It adds the new record(s) to the hashtable. * * @method _defAddHash * @private */ _defAddHash: function(e) { var obj = this.get('table'), key = this.get('key'), i = 0; for (; i < e.added.length; i++) { obj[e.added[i].get(key)] = e.added[i]; } this.set('table', obj); }, /** * @description Method called whenever "recordset:remove" event is fired. It removes the record(s) from the recordset. * * @method _defRemoveHash * @private */ _defRemoveHash: function(e) { var obj = this.get('table'), key = this.get('key'), i = 0; for (; i < e.removed.length; i++) { delete obj[e.removed[i].get(key)]; } this.set('table', obj); }, /** * @description Method called whenever "recordset:update" event is fired. It updates the record(s) by adding the new ones and removing the overwritten ones. * * @method _defUpdateHash * @private */ _defUpdateHash: function(e) { var obj = this.get('table'), key = this.get('key'), i = 0; //deletes the object key that held on to an overwritten record and //creates an object key to hold on to the updated record for (; i < e.updated.length; i++) { if (e.overwritten[i]) { delete obj[e.overwritten[i].get(key)]; } obj[e.updated[i].get(key)] = e.updated[i]; } this.set('table', obj); }, /** * @description Method called whenever "recordset:empty" event is fired. It empties the hash table. * * @method _defEmptyHash * @private */ _defEmptyHash: function() { this.set('table', {}); }, /** * @description Sets up the hashtable with all the records currently in the recordset * * @method _setHashTable * @private */ _setHashTable: function() { var obj = {}, key = this.get('key'), i = 0; //If it is not an empty recordset - go through and set up the hash table. if (this._items && this._items.length > 0) { var len = this._items.length; for (; i < len; i++) { obj[this._items[i].get(key)] = this._items[i]; } } return obj; }, /** * @description Helper method - it takes an object bag and converts it to a Y.Record * * @method _changeToRecord * @param obj {Object || Y.Record} Any objet literal or Y.Record instance * @return {Y.Record} A Record instance. * @private */ _changeToRecord: function(obj) { var oRec; if (obj instanceof Y.Record) { oRec = obj; } else { oRec = new Y.Record({ data: obj }); } return oRec; }, //--------------------------------------------- // Events //--------------------------------------------- /** * @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) * * @method _recordSetUpdated * @private */ _recordsetChanged: function() { this.on(['update', 'add', 'remove', 'empty'], function() { this.fire('change', {}); }); }, /** * @description Syncs up the private hash methods with their appropriate triggering events. * * @method _syncHashTable * @private */ _syncHashTable: function() { this.after('add', function(e) { this._defAddHash(e); }); this.after('remove', function(e) { this._defRemoveHash(e); }); this.after('update', function(e) { this._defUpdateHash(e); }); this.after('update', function(e) { this._defEmptyHash(); }); }, //--------------------------------------------- // Public Methods //--------------------------------------------- /** * @description Returns the record with particular ID or index * * @method getRecord * @param i {String, Number} The ID of the record if a string, or the index if a number. * @return {Y.Record} An Y.Record instance * @public */ getRecord: function(i) { if (Lang.isString(i)) { return this.get('table')[i]; } else if (Lang.isNumber(i)) { return this._items[i]; } return null; }, /** * @description Returns the record at a particular index * * @method getRecordByIndex * @param i {Number} Index at which the required record resides * @return {Y.Record} An Y.Record instance * @public */ getRecordByIndex: function(i) { return this._items[i]; }, /** * @description Returns a range of records beginning at particular index * * @method getRecordsByIndex * @param index {Number} Index at which the required record resides * @param range {Number} (Optional) Number of records to retrieve. The default is 1 * @return {Array} An array of Y.Record instances * @public */ getRecordsByIndex: function(index, range) { var i = 0, returnedRecords = []; //Range cannot take on negative values range = (Lang.isNumber(range) && (range > 0)) ? range: 1; for (; i < range; i++) { returnedRecords.push(this._items[index + i]); } return returnedRecords; }, /** * @description Returns the length of the recordset * * @method getLength * @return {Number} Number of records in the recordset * @public */ getLength: function() { return this.size(); }, /** * @description Returns an array of values for a specified key in the recordset * * @method getValuesByKey * @param index {Number} (optional) Index at which the required record resides * @return {Array} An array of values for the given key * @public */ getValuesByKey: function(key) { var i = 0, len = this._items.length, retVals = []; for (; i < len; i++) { retVals.push(this._items[i].getValue(key)); } return retVals; }, /** * @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. * * @method add * @param oData {Y.Record, Object Literal, Array} A Y.Record instance, An object literal of data or an array of object literals * @param index {Number} (optional) Index at which to add the record(s) * @return {Y.Recordset} The updated recordset instance * @public */ add: function(oData, index) { var newRecords = [], idx, i = 0; idx = (Lang.isNumber(index) && (index > -1)) ? index: this._items.length; //Passing in array of object literals for oData if (Lang.isArray(oData)) { for (; i < oData.length; i++) { newRecords[i] = this._changeToRecord(oData[i]); } } else if (Lang.isObject(oData)) { newRecords[0] = this._changeToRecord(oData); } this.fire('add', { added: newRecords, index: idx }); return this; }, /** * @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. * * @method remove * @param index {Number} (optional) Index at which to remove the record(s) from * @param range {Number} (optional) Number of records to remove (including the one at the index) * @return {Y.Recordset} The updated recordset instance * @public */ remove: function(index, range) { var remRecords = []; //Default is to only remove the last record - the length is always 1 greater than the last index index = (index > -1) ? index: (this._items.length - 1); range = (range > 0) ? range: 1; remRecords = this._items.slice(index, (index + range)); this.fire('remove', { removed: remRecords, range: range, index: index }); //this._recordRemoved(remRecords, index); //return ({data: remRecords, index:index}); return this; }, /** * @description Empties the recordset * * @method empty * @return {Y.Recordset} The updated recordset instance * @public */ empty: function() { this.fire('empty', {}); return this; }, /** * @description Updates the recordset with the new records passed in. Overwrites existing records when updating the index with the new records. * * @method update * @param data {Y.Record, Object Literal, Array} A Y.Record instance, An object literal of data or an array of object literals * @param index {Number} (optional) The index to start updating from. * @return {Y.Recordset} The updated recordset instance * @public */ update: function(data, index) { var rec, arr, i = 0; //Whatever is passed in, we are changing it to an array so that it can be easily iterated in the _defUpdateFn method arr = (!(Lang.isArray(data))) ? [data] : data; rec = this._items.slice(index, index + arr.length); for (; i < arr.length; i++) { arr[i] = this._changeToRecord(arr[i]); } this.fire('update', { updated: arr, overwritten: rec, index: index }); return this; } }, { ATTRS: { /** * @description An array of records that the recordset is storing * * @attribute records * @public * @type array */ records: { validator: Lang.isArray, getter: function() { // give them a copy, not the internal object return new Y.Array(this._items); }, setter: function(allData) { //For allData passed in here, see if each instance is a Record. //If its not, change it to a record. //Then push it into the array. var records = []; function initRecord(oneData) { var o; if (oneData instanceof Y.Record) { records.push(oneData); } else { o = new Y.Record({ data: oneData }); records.push(o); } } //This conditional statement handles creating empty recordsets if (allData) { Y.Array.each(allData, initRecord); this._items = Y.Array(records); } }, //value: [], //initialization of the attribute must be done before the first call to get('records') is made. //if lazyAdd were set to true, then instantiating using new Y.Recordset({records:[..]}) would //not call the setter. //see http://developer.yahoo.com/yui/3/api/Attribute.html#method_addAttr for details on this lazyAdd: false }, /** * @description A hash table where the ID of the record is the key, and the record instance is the value. * * @attribute table * @public * @type object */ table: { //Initially, create the hash table with all records currently in the recordset valueFn: '_setHashTable' }, /** * @description The ID to use as the key in the hash table. * * @attribute key * @public * @type string */ key: { value: 'id', //set to readonly true. If you want custom hash tables, you should use the recordset-indexer plugin. readOnly: true } } }); Y.augment(Recordset, ArrayList); Y.Recordset = Recordset; }, '3.3.0' ,{requires:['base','arraylist']}); YUI.add('recordset-sort', function(Y) { /** * Adds default and custom sorting functionality to the Recordset utility * @module recordset * @submodule recordset-sort */ var Compare = Y.ArraySort.compare, isValue = Y.Lang.isValue; /** * Plugin that adds default and custom sorting functionality to the Recordset utility * @class RecordsetSort */ function RecordsetSort(field, desc, sorter) { RecordsetSort.superclass.constructor.apply(this, arguments); } Y.mix(RecordsetSort, { NS: "sort", NAME: "recordsetSort", ATTRS: { /** * @description The last properties used to sort. Consists of an object literal with the keys "field", "desc", and "sorter" * * @attribute lastSortProperties * @public * @type object */ lastSortProperties: { value: { field: undefined, desc: true, sorter: undefined }, validator: function(v) { return (isValue(v.field) && isValue(v.desc) && isValue(v.sorter)); } }, /** * @description Default sort function to use if none is specified by the user. * Takes two records, the key to sort by, and whether sorting direction is descending or not (boolean). * If two records have the same value for a given key, the ID is used as the tie-breaker. * * @attribute defaultSorter * @public * @type function */ defaultSorter: { value: function(recA, recB, field, desc) { var sorted = Compare(recA.getValue(field), recB.getValue(field), desc); if (sorted === 0) { return Compare(recA.get("id"), recB.get("id"), desc); } else { return sorted; } } }, /** * @description A boolean telling if the recordset is in a sorted state. * * @attribute defaultSorter * @public * @type function */ isSorted: { value: false } } }); Y.extend(RecordsetSort, Y.Plugin.Base, { /** * @description Sets up the default function to use when the "sort" event is fired. * * @method initializer * @protected */ initializer: function(config) { var self = this, host = this.get('host'); this.publish("sort", { defaultFn: Y.bind("_defSortFn", this) }); //Toggle the isSorted ATTR based on events. //Remove events dont affect isSorted, as they are just popped/sliced out this.on("sort", function() { self.set('isSorted', true); }); this.onHostEvent('add', function() { self.set('isSorted', false); }, host); this.onHostEvent('update', function() { self.set('isSorted', false); }, host); }, destructor: function(config) { }, /** * @description Method that all sort calls go through. * Sets up the lastSortProperties object with the details of the sort, and passes in parameters * to the "defaultSorter" or a custom specified sort function. * * @method _defSortFn * @private */ _defSortFn: function(e) { //have to work directly with _items here - changing the recordset. this.get("host")._items.sort(function(a, b) { return (e.sorter)(a, b, e.field, e.desc); }); this.set('lastSortProperties', e); }, /** * @description Sorts the recordset. * * @method sort * @param field {string} A key to sort by. * @param desc {boolean} True if you want sort order to be descending, false if you want sort order to be ascending * @public */ sort: function(field, desc, sorter) { this.fire("sort", { field: field, desc: desc, sorter: sorter || this.get("defaultSorter") }); }, /** * @description Resorts the recordset based on the last-used sort parameters (stored in 'lastSortProperties' ATTR) * * @method resort * @public */ resort: function() { var p = this.get('lastSortProperties'); this.fire("sort", { field: p.field, desc: p.desc, sorter: p.sorter || this.get("defaultSorter") }); }, /** * @description Reverses the recordset calling the standard array.reverse() method. * * @method reverse * @public */ reverse: function() { this.get('host')._items.reverse(); }, /** * @description Sorts the recordset based on the last-used sort parameters, but flips the order. (ie: Descending becomes ascending, and vice versa). * * @method flip * @public */ flip: function() { var p = this.get('lastSortProperties'); //If a predefined field is not provided by which to sort by, throw an error if (isValue(p.field)) { this.fire("sort", { field: p.field, desc: !p.desc, sorter: p.sorter || this.get("defaultSorter") }); } else { } } }); Y.namespace("Plugin").RecordsetSort = RecordsetSort; }, '3.3.0' ,{requires:['arraysort','recordset-base','plugin']}); YUI.add('recordset-filter', function(Y) { /** * Plugin that provides the ability to filter through a recordset. * Uses the filter methods available on Y.Array (see arrayextras submodule) to filter the recordset. * @module recordset * @submodule recordset-filter */ var YArray = Y.Array, Lang = Y.Lang; /** * Plugin that provides the ability to filter through a recordset. * Uses the filter methods available on Y.Array (see arrayextras submodule) to filter the recordset. * @class RecordsetFilter */ function RecordsetFilter(config) { RecordsetFilter.superclass.constructor.apply(this, arguments); } Y.mix(RecordsetFilter, { NS: "filter", NAME: "recordsetFilter", ATTRS: { } }); Y.extend(RecordsetFilter, Y.Plugin.Base, { initializer: function(config) { }, destructor: function(config) { }, /** * @description Filter through the recordset with a custom filter function, or a key-value pair. * * @method filter * @param f {Function, String} A custom filter function or a string representing the key to filter by. * @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 * @return recordset {Y.Recordset} A new filtered recordset instance * @public */ filter: function(f, v) { var recs = this.get('host').get('records'), oRecs = [], func = f; //If a key-value pair is passed in, generate a custom function if (Lang.isString(f) && Lang.isValue(v)) { func = function(item) { if (item.getValue(f) === v) { return true; } else { return false; } }; } oRecs = YArray.filter(recs, func); //TODO: PARENT CHILD RELATIONSHIP return new Y.Recordset({ records: oRecs }); //return new host.constructor({records:arr}); }, /** * @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. * @method reject * @param {Function} f is the function to execute on each item. * @return {Y.Recordset} A new Recordset instance containing the items on which the supplied function returned false. */ reject: function(f) { return new Y.Recordset({ records: YArray.reject(this.get('host').get('records'), f) }); }, /** * @description Iterates over the Recordset, returning a new Recordset of all the elements that match the supplied regular expression * @method grep * @param {pattern} pattern The regular expression to test against * each record. * @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. */ grep: function(pattern) { return new Y.Recordset({ records: YArray.grep(this.get('host').get('records'), pattern) }); } //TODO: Add more pass-through methods to arrayextras }); Y.namespace("Plugin").RecordsetFilter = RecordsetFilter; }, '3.3.0' ,{requires:['recordset-base','array-extras','plugin']}); YUI.add('recordset-indexer', function(Y) { /** * Provides the ability to store multiple custom hash tables referencing records in the recordset. * @module recordset * @submodule recordset-indexer */ /** * Plugin that provides the ability to store multiple custom hash tables referencing records in the recordset. * This utility does not support any collision handling. New hash table entries with a used key overwrite older ones. * @class RecordsetIndexer */ function RecordsetIndexer(config) { RecordsetIndexer.superclass.constructor.apply(this, arguments); } Y.mix(RecordsetIndexer, { NS: "indexer", NAME: "recordsetIndexer", ATTRS: { /** * @description Collection of all the hashTables created by the plugin. * The individual tables can be accessed by the key they are hashing against. * * @attribute hashTables * @public * @type object */ hashTables: { value: { } }, keys: { value: { } } } }); Y.extend(RecordsetIndexer, Y.Plugin.Base, { initializer: function(config) { var host = this.get('host'); //setup listeners on recordset events this.onHostEvent('add', Y.bind("_defAddHash", this), host); this.onHostEvent('remove', Y.bind('_defRemoveHash', this), host); this.onHostEvent('update', Y.bind('_defUpdateHash', this), host); }, destructor: function(config) { }, /** * @description Setup the hash table for a given key with all existing records in the recordset * * @method _setHashTable * @param key {string} A key to hash by. * @return obj {object} The created hash table * @private */ _setHashTable: function(key) { var host = this.get('host'), obj = {}, i = 0, len = host.getLength(); for (; i < len; i++) { obj[host._items[i].getValue(key)] = host._items[i]; } return obj; }, //--------------------------------------------- // Syncing Methods //--------------------------------------------- /** * @description Updates all hash tables when a record is added to the recordset * * @method _defAddHash * @private */ _defAddHash: function(e) { var tbl = this.get('hashTables'); //Go through every hashtable that is stored. //in each hashtable, look to see if the key is represented in the object being added. Y.each(tbl, function(v, key) { Y.each(e.added || e.updated, function(o) { //if the object being added has a key which is being stored by hashtable v, add it into the table. if (o.getValue(key)) { v[o.getValue(key)] = o; } }); }); }, /** * @description Updates all hash tables when a record is removed from the recordset * * @method _defRemoveHash * @private */ _defRemoveHash: function(e) { var tbl = this.get('hashTables'), reckey; //Go through every hashtable that is stored. //in each hashtable, look to see if the key is represented in the object being deleted. Y.each(tbl, function(v, key) { Y.each(e.removed || e.overwritten, function(o) { reckey = o.getValue(key); //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 if (reckey && v[reckey] === o) { delete v[reckey]; } }); }); }, /** * @description Updates all hash tables when the recordset is updated (a combination of add and remove) * * @method _defUpdateHash * @private */ _defUpdateHash: function(e) { //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. e.added = e.updated; e.removed = e.overwritten; this._defAddHash(e); this._defRemoveHash(e); /* var tbl = this.get('hashTables'), reckey; Y.each(tbl, function(v, key) { Y.each(e.updated, function(o, i) { //delete record from hashtable if it has been overwritten reckey = o.getValue(key); if (reckey) { v[reckey] = o; } //the undefined case is if more records are updated than currently exist in the recordset. if (e.overwritten[i] && (v[e.overwritten[i].getValue(key)] === e.overwritten[i])) { delete v[e.overwritten[i].getValue(key)]; } // if (v[reckey] === o) { // delete v[reckey]; // } // // //add the new updated record if it has a key that corresponds to a hash table // if (o.getValue(key)) { // v[o.getValue(key)] = o; // } }); }); */ }, //--------------------------------------------- // Public Methods //--------------------------------------------- /** * @description Creates a new hash table. * * @method createTable * @param key {string} A key to hash by. * @return tbls[key] {object} The created hash table * @public */ createTable: function(key) { var tbls = this.get('hashTables'); tbls[key] = this._setHashTable(key); this.set('hashTables', tbls); return tbls[key]; }, /** * @description Get a hash table that hashes records by a given key. * * @method getTable * @param key {string} A key to hash by. * @return table {object} The created hash table * @public */ getTable: function(key) { return this.get('hashTables')[key]; } }); Y.namespace("Plugin").RecordsetIndexer = RecordsetIndexer; }, '3.3.0' ,{requires:['recordset-base','plugin']}); YUI.add('recordset', function(Y){}, '3.3.0' ,{use:['recordset-base','recordset-sort','recordset-filter','recordset-indexer']});