diff --git a/demo/js/demo/events.js b/demo/js/demo/events.js index fab06816f..ab34c6f0e 100644 --- a/demo/js/demo/events.js +++ b/demo/js/demo/events.js @@ -1,17 +1,15 @@ -/* globals vent */ - 'use strict'; module.exports = function(demo, grid) { grid.addEventListener('fin-click', function(e) { var cell = e.detail.gridCell; - if (vent) { console.log('fin-click cell:', cell); } + if (demo.vent) { console.log('fin-click cell:', cell); } }); grid.addEventListener('fin-double-click', function(e) { var rowContext = e.detail.dataRow; - if (vent) { console.log('fin-double-click row-context:', rowContext); } + if (demo.vent) { console.log('fin-double-click row-context:', rowContext); } }); grid.addEventListener('fin-button-pressed', function(e) { @@ -20,17 +18,17 @@ module.exports = function(demo, grid) { }); grid.addEventListener('fin-scroll-x', function(e) { - if (vent) { console.log('fin-scroll-x ', e.detail.value); } + if (demo.vent) { console.log('fin-scroll-x ', e.detail.value); } }); grid.addEventListener('fin-scroll-y', function(e) { - if (vent) { console.log('fin-scroll-y', e.detail.value); } + if (demo.vent) { console.log('fin-scroll-y', e.detail.value); } }); grid.addEventListener('fin-cell-enter', function(e) { var cellEvent = e.detail; - //if (vent) { console.log('fin-cell-enter', cell.x, cell.y); } + //if (demo.vent) { console.log('fin-cell-enter', cell.x, cell.y); } //how to set the tooltip.... grid.setAttribute('title', 'event name: "fin-cell-enter"\n' + @@ -56,7 +54,7 @@ module.exports = function(demo, grid) { }); grid.addEventListener('fin-filter-applied', function(e) { - if (vent) { console.log('fin-filter-applied', e); } + if (demo.vent) { console.log('fin-filter-applied', e); } }); /** @@ -109,7 +107,7 @@ module.exports = function(demo, grid) { grid.addEventListener('fin-selection-changed', function(e) { - if (vent) { + if (demo.vent) { console.log('fin-selection-changed', grid.getSelectedRows(), grid.getSelectedColumns(), grid.getSelections()); } @@ -127,7 +125,7 @@ module.exports = function(demo, grid) { grid.addEventListener('fin-row-selection-changed', function(e) { var detail = e.detail; - if (vent) { console.log('fin-row-selection-changed', detail); } + if (demo.vent) { console.log('fin-row-selection-changed', detail); } // Move cell selection with row selection var rows = detail.rows, @@ -161,7 +159,7 @@ module.exports = function(demo, grid) { }); grid.addEventListener('fin-column-selection-changed', function(e) { - if (vent) { console.log('fin-column-selection-changed', e.detail); } + if (demo.vent) { console.log('fin-column-selection-changed', e.detail); } if (e.detail.columns.length === 0) { console.log('no rows selected'); @@ -175,43 +173,43 @@ module.exports = function(demo, grid) { }); grid.addEventListener('fin-editor-data-change', function(e) { - if (vent) { console.log('fin-editor-data-change', e.detail); } + if (demo.vent) { console.log('fin-editor-data-change', e.detail); } }); grid.addEventListener('fin-request-cell-edit', function(e) { - if (vent) { console.log('fin-request-cell-edit', e); } + if (demo.vent) { console.log('fin-request-cell-edit', e); } //e.preventDefault(); //uncomment to cancel editor popping up }); grid.addEventListener('fin-before-cell-edit', function(e) { - if (vent) { console.log('fin-before-cell-edit', e); } + if (demo.vent) { console.log('fin-before-cell-edit', e); } //e.preventDefault(); //uncomment to cancel updating the model with the new data }); grid.addEventListener('fin-after-cell-edit', function(e) { - if (vent) { console.log('fin-after-cell-edit', e); } + if (demo.vent) { console.log('fin-after-cell-edit', e); } }); grid.addEventListener('fin-editor-keyup', function(e) { - if (vent) { console.log('fin-editor-keyup', e.detail); } + if (demo.vent) { console.log('fin-editor-keyup', e.detail); } }); grid.addEventListener('fin-editor-keypress', function(e) { - if (vent) { console.log('fin-editor-keypress', e.detail); } + if (demo.vent) { console.log('fin-editor-keypress', e.detail); } }); grid.addEventListener('fin-editor-keydown', function(e) { - if (vent) { console.log('fin-editor-keydown', e.detail); } + if (demo.vent) { console.log('fin-editor-keydown', e.detail); } }); grid.addEventListener('fin-groups-changed', function(e) { - if (vent) { console.log('fin-groups-changed', e.detail); } + if (demo.vent) { console.log('fin-groups-changed', e.detail); } }); grid.addEventListener('fin-context-menu', function(e) { var modelPoint = e.detail.gridCell; - if (vent) { console.log('fin-context-menu(' + modelPoint.x + ', ' + modelPoint.y + ')'); } + if (demo.vent) { console.log('fin-context-menu(' + modelPoint.x + ', ' + modelPoint.y + ')'); } }); }; diff --git a/src/Hypergrid.js b/src/Hypergrid.js index 5a8e9e8e7..7c2f8553e 100644 --- a/src/Hypergrid.js +++ b/src/Hypergrid.js @@ -614,6 +614,83 @@ var Hypergrid = Base.extend('Hypergrid', { getState: function() { return this.behavior.getState(); }, + + loadState: function(state) { + this.behavior.setState(state); + }, + + /** + * @param {object} [options] + * @param {object} [options.include] - Hash of special objects to exclude. To exclude one or more of `rows`, `columns`, `cells`, `subgrids`, `calculators`, `columnIndexes`, or `columnNames`, provide an object with those key(s) paired with value `false`. + * @param {boolean} [options.compact] - Run garbage collection first. The only property this current affects is `properties.calculators` (removes unused calculators from). + * @param {number|string} [options.space='\t'] - For no space, give `0`. (See {@link https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/JSON/stringify|JSON.stringify}'s `space` param other options.) + * @param {function} [options.headerify] - If your headers were generated by a function (taking column name as a parameter), give a reference to that function here to avoid persisting headers that match the generated string. + * @memberOf Hypergrid# + */ + saveState: function(options) { + options = options || {}; + + var space = options.space === undefined ? '\t' : options.space, + include = options.include || {}, + properties = this.properties, + calculators = properties.calculators; + + if (calculators) { + if (options.compact) { + var columns = this.behavior.getColumns(); + Object.keys(calculators).forEach(function(key) { + if (!columns.find(function(column) { return column.properties.calculator === calculators[key]; })) { + delete calculators[key]; + } + }); + } + calculators.toJSON = stringifyFunctions; + } + + // Temporarily copy the given headerify function for access by columns getter + this.headerify = options.headerify; + + // Temporarily copy values from dynamic properties layer as own members (stringify ignores prototype chain) + var dynaPropDescriptors = {}; + ['rows', 'columns', 'cells', 'subgrids', 'columnIndexes', 'columnNames'].forEach(function(key) { + if (!definedFalsy(include[key])) { + dynaPropDescriptors[key] = { + configurable: true, + enumerable: true, + value: properties[key] + }; + } + }); + Object.defineProperties(properties, dynaPropDescriptors); //must use descriptors because can't override a prop when it is a setter + + var json = JSON.stringify(properties, function(key, value) { + switch (key) { + case 'calculator': + if (calculators) { + value = Object.keys(calculators).find(function(key) { + return calculators[key] === value; + }); + } else { + value = value.toString(); + } + break; + case 'calculators': + if (definedFalsy(include.calculators)) { + value = undefined; + } + } + return value; + }, space); //todo: still needs the cells array + + // Remove the temporary copies + Object.keys(dynaPropDescriptors).forEach(function(key) { + delete properties[key]; + }); + delete this.headerify; + + return json; + }, + /** * @memberOf Hypergrid# * @returns {object} The initial mouse position on a mouse down event for cell editing or a drag operation. @@ -2190,6 +2267,20 @@ function setStyles(el, style, keys) { } } +function stringifyFunctions() { + var self = this; + return Object.keys(this).reduce(function(obj, key) { + if (key !== 'toJSON') { + obj[key] = /^function /.test(key) ? null : self[key].toString(); + } + return obj; + }, {}); +} + +// is it falsy (excluding undefined)? +function definedFalsy(b) { + return !b && b !== undefined; +} /** * @name plugins diff --git a/src/behaviors/Behavior.js b/src/behaviors/Behavior.js index a8a832d9b..1277245a1 100644 --- a/src/behaviors/Behavior.js +++ b/src/behaviors/Behavior.js @@ -4,7 +4,7 @@ var Point = require('rectangular').Point; var Base = require('../Base'); var Column = require('./Column'); -var cellEventFactory = require('./../lib/cellEventFactory'); +var cellEventFactory = require('../lib/cellEventFactory'); var noExportProperties = [ 'columnHeader', 'columnHeaderColumnSelection', @@ -160,13 +160,11 @@ var Behavior = Base.extend('Behavior', { this.allColumns[tc] = this.columns[tc] = this.newColumn({ index: tc, - header: this.dataModel.schema[tc].header, - calculator: this.dataModel.schema[tc].calculator + header: this.dataModel.schema[tc].header }); this.allColumns[rc] = this.columns[rc] = this.newColumn({ index: rc, - header: this.dataModel.schema[rc].header, - calculator: this.dataModel.schema[rc].calculator + header: this.dataModel.schema[rc].header }); }, @@ -319,24 +317,9 @@ var Behavior = Base.extend('Behavior', { this.createColumns(); var state = this.grid.properties; - for (var key in memento) { - if (memento.hasOwnProperty(key)) { - var value = memento[key]; - switch (key) { - case 'rows': - this.setRowHeights(value); - break; - case 'columns': - this.setAllColumnPropertiesByName(value); - break; - case 'cells': - this.setAllCellProperties(value); - break; - default: - state[key] = value; - } - } - } + Object.keys(memento).forEach(function(key) { + state[key] = memento[key]; + }, this); this.setAllColumnProperties(memento.columnProperties); @@ -351,54 +334,6 @@ var Behavior = Base.extend('Behavior', { } }, - setAllColumnPropertiesByName: function(columns) { - var columnName; - - if (columns) { - for (columnName in columns) { - if (columns.hasOwnProperty(columnName)) { - var column = this.columns.find(nameMatches); - if (column) { - column.properties = columns[columnName]; - } - } - } - } - - function nameMatches(column) { - return column.name === columnName; - } - }, - - setAllCellProperties: function(cells) { - for (var subgridName in cells) { - if (cells.hasOwnProperty(subgridName)) { - var subgrid = this.subgrids.lookup[subgridName]; - if (subgrid) { - var subgridHash = cells[subgridName]; - for (var rowIndex in subgridHash) { - if (subgridHash.hasOwnProperty(rowIndex)) { - var columnProps = subgridHash[rowIndex]; - for (var columnName in columnProps) { - if (columnProps.hasOwnProperty(columnName)) { - var column = this.columns.find(nameMatches); - if (column) { - var properties = columnProps[columnName]; - column.addCellProperties(rowIndex, properties, subgrid); - } - } - } - } - } - } - } - } - - function nameMatches(column) { - return column.name === columnName; - } - }, - setColumnOrder: function(columnIndexes) { if (Array.isArray(columnIndexes)){ this.columns.length = columnIndexes.length; @@ -571,15 +506,19 @@ var Behavior = Base.extend('Behavior', { /** * @param {CellEvent|number} xOrCellEvent - Grid column coordinate. * @param {number} [y] - Grid row coordinate. Omit if `xOrCellEvent` is a CellEvent. + * @param {dataModelAPI} [dataModel] - For use only when `xOrCellEvent` is _not_ a `CellEvent`: Provide a subgrid. If given, x and y are interpreted as data cell coordinates (unadjusted for scrolling). Does not default to the data subgrid, although you can provide it explicitly (`this.subgrids.lookup.data`). * @memberOf Behavior# */ - getValue: function(xOrCellEvent, y) { - switch (arguments.length) { - case 1: - return xOrCellEvent.value; - case 2: - return new this.CellEvent(xOrCellEvent, y).value; + getValue: function(xOrCellEvent, y, dataModel) { + if (typeof xOrCellEvent !== 'object') { + xOrCellEvent = new this.CellEvent; + if (dataModel) { + xOrCellEvent.resetDataXY(xOrCellEvent, y, dataModel); + } else { + xOrCellEvent.resetGridCY(xOrCellEvent, y); + } } + return xOrCellEvent.value; }, /** @@ -601,14 +540,20 @@ var Behavior = Base.extend('Behavior', { * @param {CellEvent|number} xOrCellEvent - Grid column coordinate. * @param {number} [y] - Grid row coordinate. Omit if `xOrCellEvent` is a CellEvent. * @param {Object} value - The value to use. _When `y` omitted, promoted to 2nd arg._ + * @param {dataModelAPI} [dataModel] - For use only when `xOrCellEvent` is _not_ a `CellEvent`: Provide a subgrid. If given, x and y are interpreted as data cell coordinates (unadjusted for scrolling). Does not default to the data subgrid, although you can provide it explicitly (`this.subgrids.lookup.data`). * @return {boolean} Consumed. */ - setValue: function(xOrCellEvent, y, value) { - switch (arguments.length) { - case 3: xOrCellEvent = new this.CellEvent(xOrCellEvent, y); break; - case 2: value = y; break; + setValue: function(xOrCellEvent, y, value, dataModel) { + if (typeof xOrCellEvent === 'object') { + value = y; + } else { + xOrCellEvent = new this.CellEvent; + if (dataModel) { + xOrCellEvent.resetDataXY(xOrCellEvent, y, dataModel); + } else { + xOrCellEvent.resetGridCY(xOrCellEvent, y); + } } - xOrCellEvent.value = value; }, @@ -625,17 +570,16 @@ var Behavior = Base.extend('Behavior', { * @desc May be undefined because cells only have their own properties object when at lest one own property has been set. * @param {CellEvent|number} xOrCellEvent - Data x coordinate. * @param {number} [y] - Grid row coordinate. _Omit when `xOrCellEvent` is a `CellEvent`._ + * @param {dataModelAPI} [dataModel=this.subgrids.lookup.data] - For use only when `xOrCellEvent` is _not_ a `CellEvent`: Provide a subgrid. * @returns {undefined|object} The "own" properties of the cell at x,y in the grid. If the cell does not own a properties object, returns `undefined`. * @memberOf Behavior# */ - getCellOwnProperties: function(xOrCellEvent, y) { + getCellOwnProperties: function(xOrCellEvent, y, dataModel) { switch (arguments.length) { - case 1: - return xOrCellEvent.column // xOrCellEvent is cellEvent - .getCellOwnProperties(xOrCellEvent.dataCell.y, xOrCellEvent.visibleRow.subgrid); - case 2: - return this.getColumn(xOrCellEvent) // xOrCellEvent is x - .getCellOwnProperties(y); + case 1: // xOrCellEvent is cellEvent + return xOrCellEvent.column.getCellOwnProperties(xOrCellEvent.dataCell.y, xOrCellEvent.visibleRow.subgrid); + case 2: case 3: // xOrCellEvent is x + return this.getColumn(xOrCellEvent).getCellOwnProperties(y, dataModel); } }, @@ -646,16 +590,16 @@ var Behavior = Base.extend('Behavior', { * If you are seeking a single specific property, consider calling {@link Behavior#getCellProperty} instead. * @param {CellEvent|number} xOrCellEvent - Data x coordinate. * @param {number} [y] - Grid row coordinate. _Omit when `xOrCellEvent` is a `CellEvent`._ + * @param {dataModelAPI} [dataModel=this.subgrids.lookup.data] - For use only when `xOrCellEvent` is _not_ a `CellEvent`: Provide a subgrid. * @return {object} The properties of the cell at x,y in the grid. * @memberOf Behavior# */ - getCellProperties: function(xOrCellEvent, y) { + getCellProperties: function(xOrCellEvent, y, dataModel) { switch (arguments.length) { - case 1: - return xOrCellEvent.properties; // xOrCellEvent is cellEvent - case 2: - return this.getColumn(xOrCellEvent) // xOrCellEvent is x - .getCellProperties(y); + case 1: // xOrCellEvent is cellEvent + return xOrCellEvent.properties; + case 2: case 3: // xOrCellEvent is x + return this.getColumn(xOrCellEvent).getCellProperties(y, dataModel); } }, @@ -665,16 +609,16 @@ var Behavior = Base.extend('Behavior', { * @param {CellEvent|number} xOrCellEvent - Data x coordinate. * @param {number} [y] - Grid row coordinate._ Omit when `xOrCellEvent` is a `CellEvent`._ * @param {string} key - Name of property to get. _When `y` omitted, this param promoted to 2nd arg._ + * @param {dataModelAPI} [dataModel=this.subgrids.lookup.data] - For use only when `xOrCellEvent` is _not_ a `CellEvent`: Provide a subgrid. * @return {object} The specified property for the cell at x,y in the grid. * @memberOf Behavior# */ - getCellProperty: function(xOrCellEvent, y, key) { - switch (arguments.length) { - case 2: - return xOrCellEvent.properties[y]; // xOrCellEvent is cellEvent and y omitted so y here is actually key - case 3: - return this.getColumn(xOrCellEvent) // xOrCellEvent is x - .getCellProperty(y, key); + getCellProperty: function(xOrCellEvent, y, key, dataModel) { + if (typeof xOrCellEvent === 'object') { + key = y; + return xOrCellEvent.properties[key]; + } else { + return this.getColumn(xOrCellEvent).getCellProperty(y, key, dataModel); } }, @@ -684,14 +628,14 @@ var Behavior = Base.extend('Behavior', { * @param {CellEvent|number} xOrCellEvent - Data x coordinate. * @param {number} [y] - Grid row coordinate. _Omit when `xOrCellEvent` is a `CellEvent`._ * @param {Object} properties - Hash of cell properties. _When `y` omitted, this param promoted to 2nd arg._ + * @param {dataModelAPI} [dataModel=this.subgrids.lookup.data] - For use only when `xOrCellEvent` is _not_ a `CellEvent`: Provide a subgrid. */ - setCellProperties: function(xOrCellEvent, y, properties) { - if (typeof y === 'object') { - xOrCellEvent.column // xOrCellEvent is cellEvent - .setCellProperties(xOrCellEvent.dataCell.y, y, xOrCellEvent.visibleRow.subgrid); // y omitted so y here is actually properties + setCellProperties: function(xOrCellEvent, y, properties, dataModel) { + if (typeof xOrCellEvent === 'object') { + properties = y; + xOrCellEvent.column.setCellProperties(xOrCellEvent.dataCell.y, properties, xOrCellEvent.visibleRow.subgrid); } else { - this.getColumn(xOrCellEvent) // xOrCellEvent is x - .setCellProperties(y, properties); + this.getColumn(xOrCellEvent).setCellProperties(y, properties, dataModel); } }, @@ -701,14 +645,14 @@ var Behavior = Base.extend('Behavior', { * @param {CellEvent|number} xOrCellEvent - Data x coordinate. * @param {number} [y] - Grid row coordinate. _Omit when `xOrCellEvent` is a `CellEvent`._ * @param {Object} properties - Hash of cell properties. _When `y` omitted, this param promoted to 2nd arg._ + * @param {dataModelAPI} [dataModel=this.subgrids.lookup.data] - For use only when `xOrCellEvent` is _not_ a `CellEvent`: Provide a subgrid. */ - addCellProperties: function(xOrCellEvent, y, properties) { - if (typeof y === 'object') { - xOrCellEvent.column // xOrCellEvent is cellEvent - .addCellProperties(xOrCellEvent.dataCell.y, y, xOrCellEvent.visibleRow.subgrid); // y omitted so y here is actually properties + addCellProperties: function(xOrCellEvent, y, properties, dataModel) { + if (typeof xOrCellEvent === 'object') { + properties = y; + xOrCellEvent.column.addCellProperties(xOrCellEvent.dataCell.y, properties, xOrCellEvent.visibleRow.subgrid); // y omitted so y here is actually properties } else { - this.getColumn(xOrCellEvent) // xOrCellEvent is x - .addCellProperties(y, properties); + this.getColumn(xOrCellEvent).addCellProperties(y, properties, dataModel); } }, @@ -725,21 +669,18 @@ var Behavior = Base.extend('Behavior', { * @param {number} [y] - Grid row coordinate. _Omit when `xOrCellEvent` is a `CellEvent`._ * @param {string} key - Name of property to get. _When `y` omitted, this param promoted to 2nd arg._ * @param value - * @parsam {dataModelAPI} [dataModel=this.subgrids.lookup.data] - For use only when `xOrCellEvent` is _not_ a `CellEvent`: Provide a subgrid. + * @param {dataModelAPI} [dataModel=this.subgrids.lookup.data] - For use only when `xOrCellEvent` is _not_ a `CellEvent`: Provide a subgrid. * @memberOf Behavior# */ setCellProperty: function(xOrCellEvent, y, key, value, dataModel) { var cellOwnProperties; - switch (arguments.length) { - case 3: - // xOrCellEvent is cellEvent, y is key, key is value - cellOwnProperties = xOrCellEvent.setCellProperty(y, key); - break; - case 4: - case 5: - cellOwnProperties = this.getColumn(xOrCellEvent) // xOrCellEvent is x - .setCellProperty(y, key, value, dataModel); - this.grid.renderer.resetCellPropertiesCache(xOrCellEvent, y, dataModel); + if (typeof xOrCellEvent === 'object') { + value = key; + key = y; + cellOwnProperties = xOrCellEvent.setCellProperty(key, value); + } else { + cellOwnProperties = this.getColumn(xOrCellEvent).setCellProperty(y, key, value, dataModel); + this.grid.renderer.resetCellPropertiesCache(xOrCellEvent, y, dataModel); } return cellOwnProperties; }, @@ -807,34 +748,6 @@ var Behavior = Base.extend('Behavior', { } }, - setRowHeights: function(rowHeights) { - for (var subgridName in rowHeights) { - if (rowHeights.hasOwnProperty(subgridName)) { - var subgrid = this.subgrids.lookup[subgridName]; - if (subgrid) { - var subgridHash = rowHeights[subgridName]; - for (var rowIndex in subgridHash) { - if (subgridHash.hasOwnProperty(rowIndex)) { - var properties = subgridHash[rowIndex]; - for (var propName in properties) { - if (properties.hasOwnProperty(propName)) { - var propValue = properties[propName]; - switch (propName) { - case 'height': - this.setRowHeight(rowIndex, Number(propValue), subgrid); - break; - default: - console.warn('Unexpected row property "' + propName + '" ignored. (The only row property currently implemented is "height").'); - } - } - } - } - } - } - } - } - }, - /** * @memberOf Behavior# * @return {number} The width of the fixed column area in the hypergrid. diff --git a/src/behaviors/cellProperties.js b/src/behaviors/cellProperties.js index e15315b1a..701af05ac 100644 --- a/src/behaviors/cellProperties.js +++ b/src/behaviors/cellProperties.js @@ -51,7 +51,7 @@ var cell = { * * has the column properties object as its prototype * * is returned * - * If the cell does not have its own properties object, this method simply returns `undefined`. + * If the cell does not have its own properties object, this method returns `null`. * * Call this method only when you need to know if the the cell has its own properties object; otherwise call {@link Column#getCellProperties|getCellProperties}. * @param {number} rowIndex - Data row coordinate. diff --git a/src/behaviors/columnProperties.js b/src/behaviors/columnProperties.js index bdfd48c5a..812747993 100644 --- a/src/behaviors/columnProperties.js +++ b/src/behaviors/columnProperties.js @@ -19,21 +19,18 @@ function createColumnProperties() { properties = Object.create(tableState, { index: { // read-only (no setter) - enumerable: true, get: function() { return column.index; } }, name: { // read-only (no setter) - enumerable: true, - get: function() { + get: function() { return column.name; } }, field: { // read-only (no setter) - enumerable: true, get: function() { if (FIELD) { console.warn(FIELD); FIELD = undefined; } return column.name; @@ -41,7 +38,6 @@ function createColumnProperties() { }, columnName: { // read-only (no setter) - enumerable: true, get: function() { if (COLUMN_NAME) { console.warn(COLUMN_NAME); COLUMN_NAME = undefined; } return column.name; @@ -83,7 +79,33 @@ function createColumnProperties() { if (this !== column.properties) { throw new column.HypergridError(COLUMN_ONLY_PROPERTY); } - column.calculator = toFunction(calculator); + + if (!calculator) { + column.calculator = undefined; + return; + } + + if (typeof calculator === 'function') { + calculator = calculator.toString(); + } else if (typeof calculator !== 'string') { + throw new this.grid.HypergridError('Expected function or string containing function or function name.'); + } + + var matches, key = calculator, + calculators = this.grid.properties.calculators = this.grid.properties.calculators || {}; + + if (/^\w+$/.test(calculator)) { // just a function name? + calculator = calculators[calculator]; + } else { + matches = calculator.match(/^function\s*(\w+)\(/); + if (matches) { + key = matches[1]; + } + } + + column.calculator = calculators[key] = typeof calculators[key] === 'function' + ? calculators[key] || key //null calculators use the key itself (anonymous functions) + : toFunction(calculator); } } diff --git a/src/lib/cellEventFactory.js b/src/lib/cellEventFactory.js index 492b45864..7dc4dc2cb 100644 --- a/src/lib/cellEventFactory.js +++ b/src/lib/cellEventFactory.js @@ -270,8 +270,8 @@ function factory(grid) { * * Excludes `this.gridCell`, `this.dataCell`, `this.visibleRow.subgrid` defined by constructor (as non-enumerable). * * Any additional (enumerable) members mixed in by application's `getCellEditorAt` override. * - * Omit params to defer the convenience call to {CellEvent#resetGridCY}. - * (See also the alternative {@link CellEvent#resetGridXY}; and {@link CellEvent#resetDataXY} which accepts `dataX`, `dataY`.) + * Including params calls {CellEvent#resetGridCY}. + * (See also the alternatives {@link CellEvent#resetGridXY}, {@link CellEvent#resetDataXY}, and {@link CellEvent#resetGridXDataY}.) * * @param {number} [gridX] - grid cell coordinate (adjusted for horizontal scrolling after fixed columns). * @param {number} [gridY] - grid cell coordinate, adjusted (adjusted for vertical scrolling if data subgrid) diff --git a/src/lib/dynamicProperties.js b/src/lib/dynamicProperties.js index ff7ce4f3d..a1607510d 100644 --- a/src/lib/dynamicProperties.js +++ b/src/lib/dynamicProperties.js @@ -3,8 +3,8 @@ /** * @summary Dynamic property getter/setters. * @desc ### Backing store - * Dynamic properties can make use of a backing store. - * This backing store is created in the "own" layer by {@link Hypergrid#clearState|clearState}. + * Dynamic grid properties can make use of a backing store. + * This backing store is created in the "own" layer by {@link Hypergrid#clearState|clearState} and backs grid-only properties. We currently do not create one for derived (column and cell) properties objects. * The members of the backing store have the same names as the dynamic properties that utilize them. * They are initialized by {@link Hypergrid#clearState|clearState} to the default values from {@link module:defaults|defaults} object members (also) of the same name. * @@ -71,7 +71,192 @@ var dynamicPropertyDescriptors = { this.grid.behavior.setColumnOrderByName(columnNames); this.grid.behavior.changed(); } + }, + + /** + * @memberOf module:dynamicPropertyDescriptors + */ + rows: { + get: getRowPropertiesBySubgridAndRowIndex, + set: function(rowsHash) { + if (rowsHash) { + setRowPropertiesBySubgridAndRowIndex.call(this, rowsHash); + this.grid.behavior.changed(); + } + } + }, + + /** + * @memberOf module:dynamicPropertyDescriptors + */ + columns: { + get: getColumnPropertiesByColumnName, + set: function(columnsHash) { + if (columnsHash) { + setColumnPropertiesByColumnName.call(this, columnsHash); + this.grid.behavior.changed(); + } + } + }, + + /** + * @memberOf module:dynamicPropertyDescriptors + */ + cells: { + get: getCellPropertiesByColumnNameAndRowIndex, + set: function(cellsHash) { + if (cellsHash) { + setCellPropertiesByColumnNameAndRowIndex.call(this, cellsHash); + this.grid.behavior.changed(); + } + } } }; +function getRowPropertiesBySubgridAndRowIndex() { // to be called with grid.properties as context + var subgrids = {}; + this.grid.behavior.subgrids.forEach(function(dataModel) { + var key = dataModel.name || dataModel.type; + for (var rowIndex = 0, rowCount = dataModel.getRowCount(); rowIndex < rowCount; ++rowIndex) { + var height = dataModel.getRow(rowIndex).__ROW_HEIGHT; + if (height !== undefined) { + var subgrid = subgrids[key] = subgrids[key] || {}; + subgrid[rowIndex] = { height: height }; + } + } + }); + return subgrids; +} + +function setRowPropertiesBySubgridAndRowIndex(rowsHash) { // to be called with grid.properties as context + var behavior = this.grid.behavior; + for (var subgridName in rowsHash) { + if (rowsHash.hasOwnProperty(subgridName)) { + var subgrid = behavior.subgrids.lookup[subgridName]; + if (subgrid) { + var subgridHash = rowsHash[subgridName]; + for (var rowIndex in subgridHash) { + if (subgridHash.hasOwnProperty(rowIndex)) { + var properties = subgridHash[rowIndex]; + for (var propName in properties) { + if (properties.hasOwnProperty(propName)) { + var propValue = properties[propName]; + switch (propName) { + case 'height': + behavior.setRowHeight(rowIndex, Number(propValue), subgrid); + break; + default: + console.warn('Unexpected row property "' + propName + '" ignored. (The only row property currently implemented is "height").'); + } + } + } + } + } + } + } + } +} + +function getColumnPropertiesByColumnName() { // to be called with grid.properties as context + var columns = this.grid.behavior.getColumns(), + headerify = this.grid.headerify; + return columns.reduce(function(obj, column) { + var properties = Object.keys(column.properties).reduce(function(properties, key) { + switch (key) { + case 'preferredWidth': // not a public property + break; + case 'header': + if (headerify && column.properties.header === headerify(column.properties.name)) { + break; + } + // eslint-disable-line no-fallthrough + default: + var value = column.properties[key]; + if (value !== undefined) { + properties[key] = value; + } + } + return properties; + }, {}); + if (Object.keys(properties).length) { + obj[column.name] = properties; + } + return obj; + }, {}); +} + +function setColumnPropertiesByColumnName(columnsHash) { // to be called with grid.properties as context + var columns = this.grid.behavior.getColumns(); + + for (var columnName in columnsHash) { + if (columnsHash.hasOwnProperty(columnName)) { + var column = columns.find(nameMatches); + if (column) { + column.properties = columnsHash[columnName]; + } + } + } + + function nameMatches(column) { + return column.name === columnName; + } +} + +function getCellPropertiesByColumnNameAndRowIndex() { + var behavior = this.grid.behavior, + columns = behavior.getColumns(), + subgrids = {}; + + behavior.subgrids.forEach(function(dataModel) { + var key = dataModel.name || dataModel.type; + + for (var rowIndex = 0, rowCount = dataModel.getRowCount(); rowIndex < rowCount; ++rowIndex) { + columns.forEach(copyCellOwnProperties); + } + + function copyCellOwnProperties(column) { + var properties = behavior.getCellOwnProperties(column.index, rowIndex, dataModel); + if (properties) { + var subgrid = subgrids[key] = subgrids[key] || {}, + row = subgrid[rowIndex] = subgrid[rowIndex] = {}; + row[column.name] = Object.assign({}, properties); + } + } + }); + + return subgrids; +} + +function setCellPropertiesByColumnNameAndRowIndex(cellsHash) { // to be called with grid.properties as context + var subgrids = this.grid.behavior.subgrids, + columns = this.grid.behavior.getColumns(); + + for (var subgridName in cellsHash) { + if (cellsHash.hasOwnProperty(subgridName)) { + var subgrid = subgrids.lookup[subgridName]; + if (subgrid) { + var subgridHash = cellsHash[subgridName]; + for (var rowIndex in subgridHash) { + if (subgridHash.hasOwnProperty(rowIndex)) { + var columnProps = subgridHash[rowIndex]; + for (var columnName in columnProps) { + if (columnProps.hasOwnProperty(columnName)) { + var column = columns.find(nameMatches); + if (column) { + var properties = columnProps[columnName]; + column.addCellProperties(rowIndex, properties, subgrid); + } + } + } + } + } + } + } + } + + function nameMatches(column) { + return column.name === columnName; + } +} + module.exports = dynamicPropertyDescriptors; diff --git a/src/lib/toFunction.js b/src/lib/toFunction.js index 3d30c66ef..0d331b5a2 100644 --- a/src/lib/toFunction.js +++ b/src/lib/toFunction.js @@ -16,7 +16,7 @@ module.exports = function(string) { throw 'Expected string, function, or undefined.'; } - var args = string.match(/function\s*\(([^]*?)\)/); + var args = string.match(/^function\s*\w*\s*\(([^]*?)\)/); if (!args) { throw 'Expected function keyword with formal parameter list.'; }