UNPKG

fin-hypergrid

Version:
833 lines (726 loc) 29.4 kB
'use strict'; var DataModel = require('./DataModel'); var DataSourceOrigin = require('../lib/DataSourceOrigin'); /** @typedef {object} dataSourcePipelineObject * @property {string} type - A "DataSourceOrigin" style constructor name. * @property {*} [options] - When defined, passed as 2nd argument to constructor. * @property {string} [parent] - Defines a branch off the main sequence. */ /** * @name dataModels.JSON * @constructor * @extends DataModel */ var JSON = DataModel.extend('dataModels.JSON', { initialize: function(grid, options) { /** * @summary Hash of controllers. * @desc Keyed by data source type. * Data controller are only accepted by data sources that have a defined `type` property. * @see {@link dataControlInterface} * @type {object} * @memberOf dataModels.JSON.prototype */ this.controllers = {}; this.charMap = new CharMap(this); this.reset(options); }, /** * Override to use a different origin. * @type(DataSourceBase} * @memberOf dataModels.JSON.prototype */ DataSourceOrigin: DataSourceOrigin, /** * @type {dataSourcePipelineObject[][]} * @summary Pipeline stash push-down list. * @desc The pipeline stash may be shared or instanced. This is the shared stash. An instance may override this with an instance stash variable (of the same name). See {@link dataModels.JSON#getPipelineSchemaStash}. * @memberOf dataModels.JSON.prototype */ pipelineSchemaStash: [], /** * @param {object} [options] * @param {object} [options.pipeline] - Consumed by {@link dataModels.JSON#setPipeline}. * If omitted, previously established pipeline is reused. * @param {object} [options.controllers] - Consumed by {@link dataModels.JSON#setPipeline}. * If omitted, previously established controllers. * * @memberOf dataModels.JSON.prototype */ reset: function(options) { delete this.pipelineSchemaStash; // remove existing "own" version if any options = options || {}; this.source = new this.DataSourceOrigin( options.data, options.schema, this.grid.behavior.treeColumnIndex, this.grid.behavior.rowColumnIndex ); this.setPipeline({ pipeline: options.pipeline, controllers: options.controllers }); }, /** * Application developer should override to set up a default pipeline. * @type {pipelineSchema} * @memberOf dataModels.JSON.prototype */ defaultPipelineSchema: [], clearSelectedData: function() { var key = 'clearSelectedData()', warned = this.$$DEPRECATION_WARNED = this.$$DEPRECATION_WARNED || {}; if (!(key in warned)) { warned[key] = 0; console.warn(key + ' has been deprecated as of v1.2.23. This function no longer has any meaning; calls should be removed.'); } }, /** * @deprecated As of v1.0.7, reference the `dataSource` property instead. * @returns {*} * @memberOf dataModels.JSON.prototype */ getDataSource: function() { return this.deprecated('getDataSource()', 'dataSource', '1.0.7'); }, getData: function() { return this.source.data; }, /** * @deprecated As of v1.1.0, use `getIndexedData()` instead. * @memberOf dataModels.JSON.prototype */ getFilteredData: function() { return this.deprecated('getFilteredData()', 'getIndexedData()', '1.2.0', arguments); }, getIndexedData: function() { var ds = this.dataSource; var count = ds.getRowCount(); var result = new Array(count); for (var y = 0; y < count; y++) { result[y] = ds.getRow(y); } return result; }, /** * @param {number} x - Data column coordinate. * @param {number} y - Data row coordinate. * @memberOf dataModels.JSON.prototype */ getValue: function(x, y) { return this.dataSource.getValue(x, y); }, /** * @param {number} y - Data row coordinate. * @returns {nunber} Row index in raw data array after dereferencing all data source indexing. * @memberOf dataModels.JSON.prototype */ getDataIndex: function(y) { return this.dataSource.getDataIndex(y); }, /** * @memberOf dataModels.JSON.prototype * @param {number} x - Data column coordinate. * @param {number} r - Grid row coordinate. * @param value */ setValue: function(x, r, value) { this.dataSource.setValue(x, r, value); }, /** * @deprecated As of v1.1.0, use `this.grid.behavior.getColumnProperties(x)` instead. * @memberOf dataModels.JSON.prototype * @param {number} x - Data column coordinate. * @returns {*} */ getColumnProperties: function(x) { //access directly because we want it ordered return this.deprecated('getColumnProperties(x)', 'grid.behavior.getColumnProperties(x)', '1.2.0', arguments); }, /** * @memberOf dataModels.JSON.prototype * @returns {number} */ getColumnCount: function() { var offset = this.grid.behavior.hasTreeColumn() ? -1 : 0; return this.dataSource.getColumnCount() + offset; }, /** * @memberOf dataModels.JSON.prototype * @returns {number} */ getRowCount: function() { return this.dataSource.getRowCount(); }, /** * @memberOf dataModels.JSON.prototype * @returns {string[]} */ getHeaders: function() { return getSchemaPropArr.call(this, 'header', 'getHeaders'); }, /** * @memberOf dataModels.JSON.prototype * @param {string[]} headers */ setHeaders: function(headers) { this.dataSource.setHeaders(headers); }, /** * @memberOf dataModels.JSON.prototype * @param {string[]} fields */ setFields: function(fields) { this.dataSource.setFields(fields); }, /** * @memberOf dataModels.JSON.prototype * @returns {string[]} */ getFields: function() { return getSchemaPropArr.call(this, 'name', 'getFields'); }, /** * @memberOf dataModels.JSON.prototype * @returns {string[]} */ getCalculators: function() { return getSchemaPropArr.call(this, 'calculator', 'getCalculators'); }, /** * @memberOf dataModels.JSON.prototype */ reindex: function(options) { var selectedRowSourceIndexes = getSelectedDataRowIndexes.call(this), selectedColumnNames = getSelectedColumNames.call(this); this.pipeline.forEach(function(dataSource) { if (dataSource) { if (dataSource.apply) { dataSource.apply(options); } } }); this.grid.selectionModel.reset(); reselectRowsByUnderlyingIndexes.call(this, selectedRowSourceIndexes); reselectColumnsByNames.call(this, selectedColumnNames); }, /** * @summary Set or reset grid data. * See {@link DataSourceOrigin#setData} for details. * @memberOf dataModels.JSON.prototype */ setData: function(dataSource, schema) { this.source.setData(dataSource, schema); }, /** @typedef {DataSourceBase[]} pipelineSchema * @summary Describes a new pipeline. * @desc Consists of an ordered list of data source constructors, descendants of `DataSourceBase`. * May contain `undefined` elements, which are ignored. */ /** * @summary Instantiates the data source pipeline. * @desc Each new pipe is created from the list of supplied constructors, each taking a reference to the previous data source in the pipeline. * * A reference to each new pipe is added to `this.sources` dataModel using the pipe's derived name. * * Will clear out any filtering and sorting state. * * The last pipe is assigned the synonym `this.dataSource`. * @param {pipelineSchema} [DataSources] - New pipeline description. If not given, uses the default {@link dataModels.JSON#DataSources|this.defaultPipelineSchema}. * @param {object} [options] - Takes first argument position when `DataSources` omitted. * @param {string} [options.stash] - See {@link dataModels.JSON#getPipelineSchemaStash}. If given, saves the currently defined pipeline onto the indicated stash stack and then resets it with the given `DataSources`. * @memberOf dataModels.JSON.prototype */ setPipeline: function(DataSources, options) { if (!Array.isArray(DataSources)) { options = DataSources; DataSources = options.pipeline; } options = options || {}; if (DataSources) { DataSources = DataSources.slice(); } else if (this.DataSources) { DataSources = this.DataSources; } else { DataSources = this.defaultPipelineSchema.slice(); } this.DataSources = DataSources; var pipeline = [], dataSource = this.source; if (DataSources.length) { if (options.stash) { this.getPipelineSchemaStash(options.stash).push(DataSources); } DataSources.forEach(function(DataSource) { if (DataSource) { dataSource = new DataSource(dataSource); pipeline.push(dataSource); } }, this); } this.dataSource = dataSource; /** * @summary Currently defined pipeline. * @desc Each instance has its own pipeline. * (Pipelines cannot be shared because they contain indexes specific to the data in the grid.) * @name pipeline * @type {dataSourcePipelineObject[]} * @memberOf dataModels.JSON.prototype */ this.pipeline = pipeline; this.setController(options.controllers || this.controllers); // set the new or previously set data controller(s) on the new pipeline }, /** * Find the last data source in the pipeline of specified type. * @param {string} type * @returns {DataSourceBase} * @memberOf dataModels.JSON.prototype */ findDataSourceByType: function(type) { var dataSource; for (var i = this.pipeline.length - 1; i >= 0; i--) { dataSource = this.pipeline[i]; if (dataSource.type === type) { return dataSource; } } }, /** * @summary The pipeline stash currently in use (either shared or instance). * @desc Instance stash is created here when requested and instance doesn't yet have its "own" version. * @param {string} [whichStash='default'] - One of: * * `'shared'` - Use shared stash. * * `'own'' or `'instance'` - Use instance stash, creating it if it does not exist. * * `'default'` - Use instance stash if previously created; otherwise use shared stash. * @returns The pipeline stash push-down list. * @memberOf dataModels.JSON.prototype */ getPipelineSchemaStash: function(whichStash) { var stash; switch (whichStash) { case 'shared': stash = DataModel.prototype.stash; break; case 'own': case 'instance': if (!this.hasOwnProperty('pipelineSchemaStash')) { this.pipelineSchemaStash = []; } // fallthrough case 'default': case undefined: stash = this.pipelineSchemaStash; break; } return stash; }, /** * Pops the last stashed pipeline off the stash stack, making it the currently defined pipeline. * @param {string} [whichStash] - See {@link dataModels.JSON#getPipelineSchemaStash}. * @memberOf dataModels.JSON.prototype */ unstashPipeline: function(whichStash) { var pipelineSchemaStash = this.getPipelineSchemaStash(whichStash); if (pipelineSchemaStash.length) { this.setPipeline(pipelineSchemaStash.pop()); } }, /** * @deprecated * @param {number} [newLength=0] * @memberOf dataModels.JSON.prototype */ truncatePipeline: function(newLength) { return this.deprecated('truncatePipeline(newLength)', 'setPipeline()', '1.2.0', arguments, 'Build a local pipeline (array of data source constructors) and pass it to setPipeline.'); }, isTree: function() { return this.dataSource.isDrillDown(); }, isTreeCol: function(event) { return this.dataSource.isDrillDownCol(event); }, /** * @deprecated As pf v1.1.0, use `this.grid.behavior.setTopTotals()` instead. * @summary Set the top total row(s). * @param {dataRowObject[]} totalRows - Array of 0 or more rows containing summary data. Omit to set to empty array. * @memberOf dataModels.JSON.prototype */ setTopTotals: function(totalRows) { return this.deprecated('setTopTotals(rows)', 'grid.behavior.setTopTotals(rows)', '1.1.0', arguments); }, /** * @deprecated As pf v1.1.0, use `this.grid.behavior.getTopTotals()` instead. * @summary Get the top total row(s). * @returns {dataRowObject[]} * @memberOf dataModels.JSON.prototype */ getTopTotals: function() { return this.deprecated('getTopTotals(rows)', 'grid.behavior.getTopTotals(rows)', '1.1.0', arguments); }, /** * @deprecated * @summary Set the bottom total row(s). * @param {dataRowObject[]} totalRows - Array of 0 or more rows containing summary data. Omit to set to empty array. * @memberOf dataModels.JSON.prototype */ setBottomTotals: function(totalRows) { return this.deprecated('setBottomTotals(rows)', 'grid.behavior.setBottomTotals(rows)', '1.1.0', arguments); }, /** * @summary Get the bottom total row(s). * @deprecated As pf v1.1.0, use `this.grid.behavior.getBottomTotals()` instead. * @returns {dataRowObject[]} * @memberOf dataModels.JSON.prototype */ getBottomTotals: function() { return this.deprecated('getBottomTotals()', 'grid.behavior.getBottomTotals()', '1.1.0'); }, /** * @memberOf dataModels.JSON.prototype * @returns {Column[]} */ getActiveColumns: function() { return this.deprecated('getActiveColumns()', 'grid.behavior.getActiveColumns()', '1.2.14', arguments); }, /** * @memberOf dataModels.JSON.prototype * @deprecated As of v1.0.6, use `this.getActiveColumns` instead. * @returns {Column[]} */ getVisibleColumns: function() { return this.deprecated('getVisibleColumns()', 'getActiveColumns()', '1.0.6', arguments); }, /** * @memberOf dataModels.JSON.prototype * @returns {object[]} */ getHiddenColumns: function() { return this.deprecated('getHiddenColumns()', 'grid.behavior.getHiddenColumns()', '1.2.14', arguments); }, hasHierarchyColumn: function() { return this.deprecated('hasHierarchyColumn()', '', 'v1.3.3'); }, /** * @memberOf dataModels.JSON.prototype * @param index * @param returnAsString * @desc Provides the unicode character used to denote visually if a column is a sorted state * @returns {*} */ getSortImageForColumn: function(columnIndex) { //Not implemented }, /** * @param cell * @param event * @return {boolean} Clicked in a drill-down column. * @memberOf dataModels.JSON.prototype */ cellClicked: function(event) { if (arguments.length === 2) { return this.deprecated('cellClicked(cell, event)', 'cellClicked(event)', '1.2.0', arguments); } return this.toggleRow(event.dataCell.y, undefined, event); }, /** * @summary Toggle the drill-down control of a the specified row. * @desc Operates only on the following rows: * * Expandable rows - Rows with a drill-down control. * * Revealed rows - Rows not hidden inside of collapsed drill-downs. * @param y - Revealed row number. (This is not the row ID.) * @param {boolean} [expand] - One of: * * `true` - Expand row. * * `false` - Collapse row. * * `undefined` (or omitted) - Toggle state of row. * @param event * @returns {boolean|undefined} Changed. Specifically, one of: * * `undefined` row had no drill-down control * * `true` drill-down changed * * `false` drill-down unchanged (was already in requested state) * @memberOf dataModels.JSON.prototype */ toggleRow: function(y, expand, event) { //TODO: fire a row toggle event var changed; if (this.isTreeCol(event)) { changed = this.dataSource.click(y, expand); if (changed) { this.reindex({rowClick: true}); this.grid.behavior.changed(); } } return changed; }, /** * @param {number} r - Data row coordinate. * @returns {object|undefined} Returns data row object or `undefined` if a header row. * @memberOf dataModels.JSON.prototype */ getRow: function(r) { return this.dataSource.getRow(r); }, /** * @summary Get the given data controller. * @param {string} type * @returns {undefined|*} The data controller; or `undefined` if data controller unknown to data model. * @memberOf dataModels.JSON# */ getController: function(type) { return this.controllers[type]; // alternatively: this.dataSource.getController(type) }, /** * @summary Set the given data controller(s). * @param {string} typeOrHashOfTypes - One of: * * **object** - Hash of multiple data controllers, by type. * * **string** - Type of the single data controller given in `controller`. * @param {dataControlInterface} [controller] - Only required when 'hash' is a string; omit when `hash` is an object. * @returns {object} - Hash of all results, by type. Each member will be: * * The given data controller for that type when defined. * * A new "null" data controller, generated by the data model when the given data controller for that type was `undefined`. * * `undefined` - The data controller was unknown to the data model. * @memberOf dataModels.JSON# */ setController: function(typeOrHashOfTypes, controller) { var dataSource = this.dataSource, controllers = this.controllers, result, results = {}, atLeastOneAccepted, hash; if (typeof typeOrHashOfTypes === 'string') { var type = typeOrHashOfTypes; hash = {}; hash[type] = controller; } else { hash = typeOrHashOfTypes; } Object.keys(hash).forEach(function(type) { result = dataSource.setController(type, hash[type]); atLeastOneAccepted = atLeastOneAccepted || result; results[type] = result; }); // add in the results to the active list including rejections Object.assign(controllers, results); // prune rejections from the active list Object.keys(controllers).forEach(function(type) { if (!controllers[type]) { delete controllers[type]; } }); if (atLeastOneAccepted) { this.reindex(); } return results; }, /** * @summary Digests `(columnIndex, propName, value)` and calls specified data controllers `properties()` method. * @desc Digests the three parameters `(columnIndex, propName, value)` detailed below, creating a single {@link dataControlInterface} object with which it then calls the `properties` method of the data controller specified by `type`. * * This method is overloaded in the jQuery style: You can both set a data controller prop (when value give) and a get a data controller prop (when value omitted); or you can give a hash in place of the property name to set several properties at once. Whichever way you use it, you can in addition specify a column index for column-specific properties. * * @param {null|string} type - The controller type from which to get or to which to set the given property value(s). `null` in a setter operation applies the value(s) to all data controllers; `null` has questionable usefulness in a getter operation. * * @param {number} [columnIndex] - If given, this is a property on a specific column. If omitted, this is a property on the whole data controller properties object. * * @param {string|object} propNameOrPropHash - _If `columnIndex` is omitted, this arg takes its place._ * * One of these types: * * **string** - Property name. The name of the explicit property to either get or (if `value` also given) set on the properties object. * * **object** - Hash of properties to set on the properties object. * * @param [value] - _If `columnIndex` is omitted, this arg takes its place._ * * One of: * * If omitted when `propNameOrPropHash` is a string, this is the "getter" action: * Return the value from the properties object of the key in `property`. * * Provided when `propNameOrPropHash` is a string, this is the "setter" action: * Copy this value to properties object using the key in `property`. * * When `propNameOrPropHash` is a hash and `value` is given: Unexpected; throws an error. * * @returns {propObject} * * @memberOf dataModels.JSON# */ prop: function(type, columnIndex, propNameOrPropHash, value) { var result, invalid, properties = {}, argCount = arguments.length, controllers = this.controllers, types = (type !== null) ? [type] : Object.keys(controllers); controllers = types .map(function(type) { return controllers[type]; }) .filter(function(controller) { return controller; }); if (controllers.length) { if (typeof columnIndex === 'number') { argCount--; } else { value = propNameOrPropHash; propNameOrPropHash = columnIndex; columnIndex = undefined; } switch (argCount) { case 2: // getter propName name or setter hash if (typeof propNameOrPropHash === 'object') { properties = propNameOrPropHash; // prop is object } else { properties.GETTER = propNameOrPropHash; // prop is name } break; case 3: // setter for value if (typeof propNameOrPropHash === 'string') { properties[propNameOrPropHash] = value; // prop is name } else { invalid = true; } break; default: // too few or too many args invalid = true; } if (invalid) { throw 'Invalid overload.'; } if (columnIndex !== undefined) { // non-enumerable propName: Object.defineProperty(properties, 'COLUMN', { value: { index: columnIndex, name: this.schema[columnIndex].name } }); } // Use the prepared propObject to get or set the properties on the controller controllers.forEach(function(controller) { result = controller.properties(properties); }); } return result; }, /** * @deprecated As of v1.1.0, use `this.reindex` instead. * @memberOf dataModels.JSON.prototype */ applyState: function() { return this.deprecated('applyState()', 'reindex()', '1.2.0', arguments); }, getUnfilteredValue: function(x, y) { return this.deprecated('getUnfilteredValue(x, y)', null, '1.2.0', arguments, 'No longer supported'); }, getUnfilteredRowCount: function() { return this.deprecated('getUnfilteredValue(x, y)', null, '1.2.0', arguments, 'No longer supported'); }, /** * @summary Add a new data row to the grid. * @desc If data source pipeline in use, to see the new row in the grid, you must eventually call: * ```javascript * this.grid.behavior.reindex(); * this.grid.behaviorChanged(); * ``` * @param {object} newDataRow * @returns {object} The new row object. * @memberOf dataModels.JSON.prototype */ addRow: function(newDataRow) { this.getData().push(newDataRow); return newDataRow; }, get schema() { return this.dataSource.schema; }, set schema(schema) { this.dataSource.setSchema(schema); } }); // LOCAL METHODS -- to be called with `.call(this` /** * Save underlying data row indexes backing current grid row selections. * This call should be paired with a subsequent call to `reselectGridRowsBackedBySelectedDataRows`. * @private * @this {dataModels.JSON} * @memberOf dataModels.JSON~ */ function getSelectedDataRowIndexes() { if (this.grid.properties.restoreRowSelections) { var dataSource = this.dataSource; return this.grid.getSelectedRows().map(function(selectedRowIndex) { return dataSource.getDataIndex(selectedRowIndex); }); } } function getSelectedColumNames() { if (this.grid.properties.restoreColumnSelections) { var behavior = this.grid.behavior; return this.grid.getSelectedColumns().map(function(selectedColumnIndex) { return behavior.getActiveColumn(selectedColumnIndex).name; }); } } /** * Re-establish grid row selections based on underlying data row indexes saved by `getSelectedDataRowsBackingSelectedGridRows` which should be called first. * @private * @this {dataModels.JSON} * @param {number[]} [dataRowIndexes] - List of underlying data row indexes. If omitted, this function is a no-op. * @returns {number} Number of rows reselected or `undefined` if `dataRowIndexes` was undefined. * @memberOf dataModels.JSON~ */ function reselectRowsByUnderlyingIndexes(dataRowIndexes) { if (dataRowIndexes) { var i, r, rowCount = this.getRowCount(), selectedRowCount = dataRowIndexes.length, gridRowIndexes = [], selectionModel = this.grid.selectionModel; for (r = 0; selectedRowCount && r < rowCount; ++r) { i = dataRowIndexes.indexOf(this.dataSource.getDataIndex(r)); if (i >= 0) { gridRowIndexes.push(r); delete dataRowIndexes[i]; // might make indexOf increasingly faster as deleted elements are not enumerable selectedRowCount--; // count down so we can bail early if all found } } gridRowIndexes.forEach(function(rowIndex) { selectionModel.selectRow(rowIndex); }); return gridRowIndexes.length; } } function reselectColumnsByNames(sourceColumnNames) { if (sourceColumnNames) { var behavior = this.grid.behavior, selectionModel = this.grid.selectionModel; return sourceColumnNames.reduce(function(reselectedCount, columnName) { var activeColumnIndex = behavior.getActiveColumnIndex(columnName); if (activeColumnIndex) { selectionModel.selectColumn(activeColumnIndex); reselectedCount++; } return reselectedCount; }, 0); } } /** * @private * @param {string} propName * @this DataSourceOrigin# * @returns {Array} * @memberOf dataModels.JSON~ */ function getSchemaPropArr(propName, deprecatedMethodName) { this.deprecated(deprecatedMethodName, deprecatedMethodName + '() has been deprecated as of v1.2.0 and will be removed in a future release. Constructs like ' + deprecatedMethodName + '()[i] should be changed to schema[i]. (This deprecated method now returns a new array derived from schema.)'); return this.schema.map(function(columnSchema) { return columnSchema[propName]; }, this); } function CharMap(dataModel) { this.dataModel = dataModel; } CharMap.prototype = { mixIn: require('overrider').mixIn, get OPEN() { return this.dataModel.dataSource.drillDownCharMap.OPEN; }, set OPEN(s) { this.dataModel.dataSource.drillDownCharMap.OPEN = s; }, get CLOSE() { return this.dataModel.dataSource.drillDownCharMap.CLOSE; }, set CLOSE(s) { this.dataModel.dataSource.drillDownCharMap.CLOSE = s; }, }; /** * Synonym of {@link JSON.prototype.reindex}. * @name applyAnalytics * @deprecated * @memberOf dataModels.JSON.prototype */ JSON.prototype.applyAnalytics = JSON.prototype.reindex; // eslint-disable-line no-extend-native module.exports = JSON;