var util = require('./util'); var DataSet = require('./DataSet'); /** * DataView * * a dataview offers a filtered view on a dataset or an other dataview. * * @param {DataSet | DataView} data * @param {Object} [options] Available options: see method get * * @constructor DataView */ function DataView (data, options) { this._data = null; this._ids = {}; // ids of the items currently in memory (just contains a boolean true) this.length = 0; // number of items in the DataView this._options = options || {}; this._fieldId = 'id'; // name of the field containing id this._subscribers = {}; // event subscribers var me = this; this.listener = function () { me._onEvent.apply(me, arguments); }; this.setData(data); } // TODO: implement a function .config() to dynamically update things like configured filter // and trigger changes accordingly /** * Set a data source for the view * @param {DataSet | DataView} data */ DataView.prototype.setData = function (data) { var ids, id, i, len, items; if (this._data) { // unsubscribe from current dataset if (this._data.off) { this._data.off('*', this.listener); } // trigger a remove of all items in memory ids = this._data.getIds({filter: this._options && this._options.filter}); items = []; for (i = 0, len = ids.length; i < len; i++) { items.push(this._data._data[ids[i]]); } this._ids = {}; this.length = 0; this._trigger('remove', {items: ids, oldData: items}); } this._data = data; if (this._data) { // update fieldId this._fieldId = this._options.fieldId || (this._data && this._data.options && this._data.options.fieldId) || 'id'; // trigger an add of all added items ids = this._data.getIds({filter: this._options && this._options.filter}); for (i = 0, len = ids.length; i < len; i++) { id = ids[i]; this._ids[id] = true; } this.length = ids.length; this._trigger('add', {items: ids}); // subscribe to new dataset if (this._data.on) { this._data.on('*', this.listener); } } }; /** * Refresh the DataView. Useful when the DataView has a filter function * containing a variable parameter. */ DataView.prototype.refresh = function () { var id, i, len; var ids = this._data.getIds({filter: this._options && this._options.filter}), oldIds = Object.keys(this._ids), newIds = {}, addedIds = [], removedIds = [], removedItems = []; // check for additions for (i = 0, len = ids.length; i < len; i++) { id = ids[i]; newIds[id] = true; if (!this._ids[id]) { addedIds.push(id); this._ids[id] = true; } } // check for removals for (i = 0, len = oldIds.length; i < len; i++) { id = oldIds[i]; if (!newIds[id]) { removedIds.push(id); removedItems.push(this._data[id]); delete this._ids[id]; } } this.length += addedIds.length - removedIds.length; // trigger events if (addedIds.length) { this._trigger('add', {items: added}); } if (removedIds.length) { this._trigger('remove', {items: removedIds, oldData: removedItems}); } }; /** * Get data from the data view * * Usage: * * get() * get(options: Object) * get(options: Object, data: Array | DataTable) * * get(id: Number) * get(id: Number, options: Object) * get(id: Number, options: Object, data: Array | DataTable) * * get(ids: Number[]) * get(ids: Number[], options: Object) * get(ids: Number[], options: Object, data: Array | DataTable) * * Where: * * {Number | String} id The id of an item * {Number[] | String{}} ids An array with ids of items * {Object} options An Object with options. Available options: * {String} [type] Type of data to be returned. Can * be 'DataTable' or 'Array' (default) * {Object.} [convert] * {String[]} [fields] field names to be returned * {function} [filter] filter items * {String | function} [order] Order the items by * a field name or custom sort function. * {Array | DataTable} [data] If provided, items will be appended to this * array or table. Required in case of Google * DataTable. * @param args */ DataView.prototype.get = function (args) { var me = this; // parse the arguments var ids, options, data; var firstType = util.getType(arguments[0]); if (firstType == 'String' || firstType == 'Number' || firstType == 'Array') { // get(id(s) [, options] [, data]) ids = arguments[0]; // can be a single id or an array with ids options = arguments[1]; data = arguments[2]; } else { // get([, options] [, data]) options = arguments[0]; data = arguments[1]; } // extend the options with the default options and provided options var viewOptions = util.extend({}, this._options, options); // create a combined filter method when needed if (this._options.filter && options && options.filter) { viewOptions.filter = function (item) { return me._options.filter(item) && options.filter(item); } } // build up the call to the linked data set var getArguments = []; if (ids != undefined) { getArguments.push(ids); } getArguments.push(viewOptions); getArguments.push(data); return this._data && this._data.get.apply(this._data, getArguments); }; /** * Get ids of all items or from a filtered set of items. * @param {Object} [options] An Object with options. Available options: * {function} [filter] filter items * {String | function} [order] Order the items by * a field name or custom sort function. * @return {Array} ids */ DataView.prototype.getIds = function (options) { var ids; if (this._data) { var defaultFilter = this._options.filter; var filter; if (options && options.filter) { if (defaultFilter) { filter = function (item) { return defaultFilter(item) && options.filter(item); } } else { filter = options.filter; } } else { filter = defaultFilter; } ids = this._data.getIds({ filter: filter, order: options && options.order }); } else { ids = []; } return ids; }; /** * Map every item in the dataset. * @param {function} callback * @param {Object} [options] Available options: * {Object.} [type] * {String[]} [fields] filter fields * {function} [filter] filter items * {String | function} [order] Order the items by * a field name or custom sort function. * @return {Object[]} mappedItems */ DataView.prototype.map = function (callback,options) { var mappedItems = []; if (this._data) { var defaultFilter = this._options.filter; var filter; if (options && options.filter) { if (defaultFilter) { filter = function (item) { return defaultFilter(item) && options.filter(item); } } else { filter = options.filter; } } else { filter = defaultFilter; } mappedItems = this._data.map(callback,{ filter: filter, order: options && options.order }); } else { mappedItems = []; } return mappedItems; }; /** * Get the DataSet to which this DataView is connected. In case there is a chain * of multiple DataViews, the root DataSet of this chain is returned. * @return {DataSet} dataSet */ DataView.prototype.getDataSet = function () { var dataSet = this; while (dataSet instanceof DataView) { dataSet = dataSet._data; } return dataSet || null; }; /** * Event listener. Will propagate all events from the connected data set to * the subscribers of the DataView, but will filter the items and only trigger * when there are changes in the filtered data set. * @param {String} event * @param {Object | null} params * @param {String} senderId * @private */ DataView.prototype._onEvent = function (event, params, senderId) { var i, len, id, item; var ids = params && params.items; var addedIds = [], updatedIds = [], removedIds = [], oldItems = [], updatedItems = [], removedItems = []; if (ids && this._data) { switch (event) { case 'add': // filter the ids of the added items for (i = 0, len = ids.length; i < len; i++) { id = ids[i]; item = this.get(id); if (item) { this._ids[id] = true; addedIds.push(id); } } break; case 'update': // determine the event from the views viewpoint: an updated // item can be added, updated, or removed from this view. for (i = 0, len = ids.length; i < len; i++) { id = ids[i]; item = this.get(id); if (item) { if (this._ids[id]) { updatedIds.push(id); updatedItems.push(params.data[i]); oldItems.push(params.oldData[i]); } else { this._ids[id] = true; addedIds.push(id); } } else { if (this._ids[id]) { delete this._ids[id]; removedIds.push(id); removedItems.push(params.oldData[i]); } else { // nothing interesting for me :-( } } } break; case 'remove': // filter the ids of the removed items for (i = 0, len = ids.length; i < len; i++) { id = ids[i]; if (this._ids[id]) { delete this._ids[id]; removedIds.push(id); removedItems.push(params.oldData[i]); } } break; } this.length += addedIds.length - removedIds.length; if (addedIds.length) { this._trigger('add', {items: addedIds}, senderId); } if (updatedIds.length) { this._trigger('update', {items: updatedIds, oldData: oldItems, data: updatedItems}, senderId); } if (removedIds.length) { this._trigger('remove', {items: removedIds, oldData: removedItems}, senderId); } } }; // copy subscription functionality from DataSet DataView.prototype.on = DataSet.prototype.on; DataView.prototype.off = DataSet.prototype.off; DataView.prototype._trigger = DataSet.prototype._trigger; // TODO: make these functions deprecated (replaced with `on` and `off` since version 0.5) DataView.prototype.subscribe = DataView.prototype.on; DataView.prototype.unsubscribe = DataView.prototype.off; module.exports = DataView;