diff --git a/.jsdoc.conf.json b/.jsdoc.conf.json index 90a02900e..33cf0bd24 100644 --- a/.jsdoc.conf.json +++ b/.jsdoc.conf.json @@ -4,8 +4,7 @@ "dictionaries": ["jsdoc","closure"] }, "source": { - "include": ["src", "add-ons"], - "exclude": ["src/behaviors/old"] + "include": ["src", "add-ons"] }, "plugins": ["plugins/markdown"], "markdown": { diff --git a/.travis.yml b/.travis.yml new file mode 100644 index 000000000..980bbcb2a --- /dev/null +++ b/.travis.yml @@ -0,0 +1,7 @@ +language: node_js +node_js: +- '6' + +before_script: + - npm install -g gulp +script: gulp build \ No newline at end of file diff --git a/README.md b/README.md index 1a321cc7c..7e9647534 100644 --- a/README.md +++ b/README.md @@ -1,10 +1,12 @@ +[![Build Status](https://travis-ci.org/openfin/fin-hypergrid.svg?branch=develop)](https://travis-ci.org/openfin/fin-hypergrid) + **fin-hypergrid** is an ultra-fast HTML5 grid presentation layer, achieving its speed by rendering (in a canvas tag) only the currently visible portion of your (virtual) grid, thus avoiding the latency and life-cycle issues of building, walking, and maintaining a complex DOM structure. -### Current Release (1.0.8 - 8 August 2016) +### Current Release (1.0.9 - 29 August 2016) -The current version replaces last year's [prototype version](https://github.com/openfin/fin-hypergrid/tree/polymer-prototype), which was built around Polymer. It is now completely "de-polymerized" and is being made available as: +The current version 1.0 replaces last year's [prototype version](https://github.com/openfin/fin-hypergrid/tree/polymer-prototype), which was built around Polymer. It is now completely "de-polymerized" and is being made available as: * An [npm module](https://www.npmjs.com/package/fin-hypergrid) for use with browserify. * A single JavaScript file [fin-hypergrid.js](https://openfin.github.io/fin-hypergrid/build/fin-hypergrid.js) you can reference in a ` + * + * ``` + * ```javascript + * var groupedHeader = fin.hypergrid.groupedHeader; + * ``` + * + * ### Usage + * + * This example specifies that the two named columns are under a group labeled "Name": + * ```javascript + * var grid = new fin.Hypergrid(...); + * groupedHeader.mixInTo(grid, options); + * grid.behavior.setHeaders({ + * firstName: 'Name|First', + * lastName: 'Name|Last' + * }); + * ``` + * + * You can nest headers to any level. The example above is only one level deep. The live {@link http://openfin.github.io/fin-hypergrid/grouped-header.html|demo} demonstrates a nested header (one additional level). + * + * This API uses its own extension of the {@link SimpleCell} cell renderer which does the stretched partial cell renders explained above. It also calls a background paint function to fill the area behind the group labels. This API comes with two such background paint functions, described below. These are both exposed so you can specify one over the other. You can also specify a reference to a custom function. + * + * ### Background paint functions + * + * #### {@link groupedHeader.drawProportionalBottomBorder|drawProportionalBottomBorder} + * This is the default background paint function. It paints a bottom border under the group header whose thickness is in proportion to the order (level) of the group: + * + * ![Bottom Border Background](https://github.com/openfin/fin-hypergrid/raw/master/images/jsdoc/grouped-header/bottom-border.png) + * + * The lowest-order group has a 1-pixel thick border; borders grow progressively thicker with each superior group; the highest-order group has the thickest border. + * + * #### {@link groupedHeader.drawLinearGradient|drawLinearGradient} + * This paints a background that transitions color from top to bottom: + * + * ![Linear Gradient Background](https://github.com/openfin/fin-hypergrid/raw/master/images/jsdoc/grouped-header/gradient.png) + * + * ### Options + * + * Options are supplied to the {@link groupedHeader.mixInTo|mixInTo} call and apply to the entire grid so that all column groups in a grid share the same appearance. Most options (with the exception of `delimiter`) are for the use of the {@link groupedHeader.mixInTo~GroupedHeader|GroupedHeader} cell renderer. + * + * #### `paintBackground` option + * + * To override the default background paint function, pass a reference in the `backgroundPaint` option: + * ```javascript + * fin.Hypergrid.groupedHeader.mixInTo(grid, { + * paintBackground: groupedHeader.drawLinearGradient + * }); + * ``` + * + * #### `gradientStops` option + * + * Gradients blend between a series of colors filling the area behind the group label from top to bottom. Each color may include an opacity level. + * + * The gradient illustrated above is the default. It uses the default header label color to fill the background, transitioning from top alpha=0% to bottom alpha=35%. + * + * You can however specify your own {@link https://developer.mozilla.org/en-US/docs/Web/API/CanvasGradient/addColorStop|gradient stops}: + * + * ```javascript + * fin.Hypergrid.groupedHeader.mixInTo(grid, { + * paintBackground: fin.Hypergrid.groupedHeader.drawLinearGradient, + * gradientStops: [ + * [0, 'rgba(255, 255, 0, 0)'], + * [1, 'rgba(255, 255, 0, .5)'] + * ], + * groupColor: 'red' + *}); + * ``` + * + * The gradient shown above fills the background, transitioning from top (0) in yellow (`255, 255, 0`) with alpha=0% (`0`) to bottom (`1`) with alpha=50% (`.5`): + * + * ![Linear Gradient Background (Yellow)](https://github.com/openfin/fin-hypergrid/raw/master/images/jsdoc/grouped-header/gradient-yellow.png) + * + * #### `groupColor` option + * + * The example above also illustrates setting the `groupColor` option which specifies the font color for the group labels, in this case red (yuck! but it's just an example). + * + * #### `delimiter` option + * + * Allows you to change the delimiter in the header strings from the default vertical bar character (`'|'`) to something else. + * + * @mixin + */ +var groupedHeader = { + mixInTo: mixInTo, + setHeaders: setHeaders, + drawProportionalBottomBorder: drawProportionalBottomBorder, + drawLinearGradient: drawLinearGradient }; module.exports = groupedHeader; diff --git a/add-ons/row-by-id.js b/add-ons/row-by-id.js new file mode 100644 index 000000000..3d6a514c6 --- /dev/null +++ b/add-ons/row-by-id.js @@ -0,0 +1,346 @@ +'use strict'; + +/** + * Mix into your data model for get/modify/delete access to rows by single- or multi-column ID. + * + * These functions are all basically wrappers for the heavily overloaded {@link http://openfin.github.io/hyper-analytics/DataSource.html#findRow|DataSource.prototype.findRow} method. + * + * ### Usage + * + * ##### Client-side install with ` + * + * ``` + * 2. In a `` element: + * ```javascript + * rowById.mixInTo(MyDataModel.prototype); + * ``` + * + * ##### Browserify integration with `require()` + * ```javascript + * var rowById = require('./add-ons/rowById'); + * ... + * robById.mixInTo(myGrid.behavior.dataModel); + * ``` + * + * ##### Note + * + * The above code access methodologies reference two different files: + * * The built version comes from ./build/add-ons and the API is assigned to `window.fin.Hypergrid.rowById` + * * The repo version comes from ./add-ons and the API is assigned to `module.exports` + * + * ### Examples + * + * _Instructions:_ You can try the examples below by going to [rowById.html](http://openfin.github.io/fin-hypergrid/rowById.html). This page is identical to [rowById.html](http://openfin.github.io/fin-hypergrid/example.html) _except_ with the addition of the above client-side install and mix-in lines. Then copy & paste each of the following code blocks into Chrome's developer console and hit the return key, observing the changes to the grid as you do so. + * + * 1. Set up some variables: + * ```javascript + * var behavior = grid.behavior; + * var dataModel = behavior.dataModel; + * var findKey = "symbol"; + * var findVal = 'FB'; + * ``` + * 2. Add a new row: + * ```javascript + * var newDataRow = {}; + * newDataRow[findKey] = findVal; + * newDataRow.name = 'Facebook'; + * newDataRow.prevclose = 125.08; + * dataModel.addRow(newDataRow); + * // To see the new row you must (eventually) call: + * behavior.applyAnalytics(); + * grid.behaviorChanged(); + * ``` + * 3. Modify an existing row: + * ```javascript + * var modKey = 'name'; + * var modVal = 'Facebook, Inc.'; + * var dataRow = dataModel.modifyRowById(findKey, findVal, modKey, modVal); + * // To see the modified cells you must (eventually) call: + * behavior.applyAnalytics(); + * grid.repaint(); + * ``` + * 4. Delete (remove) a row: + * ```javascript + * var oldRow = dataModel.deleteRowById(findKey, findVal); + * // To see the row disappear you must (eventually) call: + * behavior.applyAnalytics(); + * grid.behaviorChanged(); + * ``` + * 5. Replace an existing row: + * ```javascript + * findVal = 'MSFT'; + * var newRow = {symbol: "ABC", name: "Google", prevclose: 666}; + * var oldRow = dataModel.replaceRowById(findKey, findVal, newRow); + * // To see the row change you must (eventually) call: + * behavior.applyAnalytics(); + * grid.repaint(); + * ``` + * This replaces the row with the new row object, returning but otherwise discarding the old row object. That is, the new row object takes on the ordinal of the old row object. By contrast, modifyDataRow keeps the existing row object, updating it in place. + * + * 6. Fetch a row (find the row and return the row object): + * ```javascript + * findVal = 'ABC'; + * var dataRow = dataModel.getRowById(findKey, findVal); + * ``` + * 7. Get a row's index (find the row and return its ordinal) (for use with Hypergrid's various grid coordinate methods): + * ```javascript + * var rowIndex = dataModel.getRowIndexById(findKey, findVal); + * ``` + * 8. Erase (blank) a row: + * ```javascript + * var oldRow = dataModel.eraseRowById(findKey, findVal); + * // To see the row blank you must (eventually) call: + * grid.behavior.applyAnalytics(); + * grid.repaint(); + * ``` + * + * ### Notes + * + * ##### Updating the rendered grid + * + * The following calls should be made sparingly as they can be expensive. The good news is that they only need to be called at the very end of a batch grid data changes. + * 1. Call `grid.behavior.applyAnalytics()`. This call does nothing when the data source pipeline is empty. Otherwise, applies each data source transformations (filter, sort) in the pipeline. Needed when adding, deleting, or modifying rows. + * 2. Call `grid.behaviorChanged()` when the number of rows (or columns) changes as a result of the data alteration. + * 3. Call `grid.repaint()` when cells are updated in place. Note that `behaviorChanged` calls `repaint` for you so you only need to call one or the other. + * + * ##### Search key hash option + * + * Search key(s) may be provided in a single hash parameter instead of in two distinct parameters (_à la_ underscore.js) + * + * For any of the methods above that take a search key, the first two arguments (`findkey` and `findVal`) may be replaced with a single argument `findHash`, a hash of key:value pairs, optionally followed by a 2nd argument `findList`, a _whitelist_ (an array) of which keys in `findHash` to use. These overloads allow for searching based on multiple columns: + * + * ###### Example 1 + * Single-column primary key: + * ```javascript + * behavior.getRow({ symbol: 'FB' }); + * ``` + * ###### Example 2 + * Multi-column primary key (target row must match all keys): + * ```javascript + * behavior.getRow({ symbol: 'FB', name: 'Facebook' }); + * ``` + * ###### Example 3 + * Limit the column(s) that comprise the primary key with `findList`, an array of allowable search keys: + * ```javascript + * var findWhiteList = ['symbol']; + * behavior.getRow({ symbol: 'FB', name: 'the facebook' }, findKeyList); + * ``` + * In the above example, name is ignored because it is absent from the key list. This example is therefore functionally equivalent to example 2.a. + * + * ##### Modifications hash option + * + * Field(s) to modify may be provided in a hash instead (_à la_ jQuery.js) + * + * This overload allows for updating multiple columns of a row with a single method call: + * ```javascript + * var modHash = { name: 'Facebook, Inc.', prevclose: 125}; + * dataModel.modifyRowById('symbol', 'FB', modHash); + * ``` + * The above is equivalent to the following separate calls which each update a specific field in the same row: + * ```javascript + * dataModel.modifyRowById('symbol', 'FB', 'name', 'Facebook, Inc.'); + * dataModel.modifyRowById('symbol', 'FB', 'prevclose', 125); + * ``` + * Normally all included fields will be modified. As in `findList` (described above), you can limit which fields to actually modify by providing an additional parameter `modList`, an array of fields to modify. The following example modifies the "prevClose" field but not the "name" field: + * ```javascript + * var modWhiteList = ['prevclose']; + * dataModel.modifyRowById('symbol', 'FB', modHash, modWhiteList); + * ``` + * + * ##### Summary + * + * The overloads discussed above in _Search key hash option_ and _Modifier hash option_ may be combined. For example, the full list of overloads for {@link rowById.modifyRowById} is: + * + * ```javascript + * behavior.modifyRowById(findKey, findVal, modKey, modVal); + * behavior.modifyRowById(findKey, findVal, modHash); + * behavior.modifyRowById(findKey, findVal, modHash, modWhiteList); + * behavior.modifyRowById(findHash, modKey, modVal); + * behavior.modifyRowById(findHash, modHash); + * behavior.modifyRowById(findHash, modHash, modWhiteList); + * behavior.modifyRowById(findHash, findWhiteList, modKey, modVal); + * behavior.modifyRowById(findHash, findWhiteList, modHash); + * behavior.modifyRowById(findHash, findWhiteList, modHash, modWhiteList); + * ``` + * + * @mixin + */ +var rowById = { + /** + * @summary Remove the ID'd data row object from the data store. + * @desc If data source pipeline in use, to see the deletion in the grid, you must eventually call: + * ```javascript + * this.grid.behavior.applyAnalytics(); + * this.grid.behaviorChanged(); + * ``` + * Caveat: The row indexes of all rows following the deleted row will now be one less than they were! + * @param {object|string} keyOrHash - One of: + * * _string_ - Column name. + * * _object_ - Hash of 0 or more key-value pairs to search for. + * @param {*|string[]} [valOrList] - One of: + * _omitted_ - When `keyOrHash` is a hash and you want to search all its keys. + * _string[]_ - When `keyOrHash` is a hash but you only want to search certain keys. + * _otherwise_ - When `keyOrHash` is a string. Value to search for. + * @returns {object} The deleted row object. + */ + deleteRowById: function(keyOrHash, valOrList) { + return this.source.findRow.apply(this.source, getByIdArgs(keyOrHash, valOrList).concat([null])); + }, + + /** + * @summary Undefine the ID'd row object in place. + * @desc Similar to {@link rowById.deleteRowById|deleteRowById} except leave an `undefined` in place of data row object. This renders as a blank row in the grid. + * + * If data source pipeline in use, to see the deletion in the grid, you must eventually call: + * ```javascript + * this.grid.behavior.applyAnalytics(); + * this.grid.behaviorChanged(); + * ``` + * @param {object|string} keyOrHash - One of: + * * _string_ - Column name. + * * _object_ - Hash of 0 or more key-value pairs to search for. + * @param {*|string[]} [valOrList] - One of: + * _omitted_ - When `keyOrHash` is a hash and you want to search all its keys. + * _string[]_ - When `keyOrHash` is a hash but you only want to search certain keys. + * _otherwise_ - When `keyOrHash` is a string. Value to search for. + * @returns {object} The deleted row object. + */ + eraseRowById: function(keyOrHash, valOrList) { + return this.source.findRow.apply(this.source, getByIdArgs(keyOrHash, valOrList).concat([undefined])); + }, + + /** + * @param {object|string} keyOrHash - One of: + * * _string_ - Column name. + * * _object_ - Hash of 0 or more key-value pairs to search for. + * @param {*|string[]} [valOrList] - One of: + * _omitted_ - When `keyOrHash` is a hash and you want to search all its keys. + * _string[]_ - When `keyOrHash` is a hash but you only want to search certain keys. + * _otherwise_ - When `keyOrHash` is a string. Value to search for. + * @returns {number} + */ + getRowIndexById: function(keyOrHash, valOrList) { + this.source.findRow.apply(this.source, arguments); + return this.source.getProperty('foundRowIndex'); + }, + + /** + * @param {object|string} keyOrHash - One of: + * * _string_ - Column name. + * * _object_ - Hash of 0 or more key-value pairs to search for. + * @param {*|string[]} [valOrList] - One of: + * _omitted_ - When `keyOrHash` is a hash and you want to search all its keys. + * _string[]_ - When `keyOrHash` is a hash but you only want to search certain keys. + * _otherwise_ - When `keyOrHash` is a string. Value to search for. + * @returns {object} + */ + getRowById: function(keyOrHash, valOrList) { + return this.source.findRow.apply(this.source, arguments); + }, + + /** + * @summary Update selected columns in existing data row. + * @desc If data source pipeline in use, to see the deletion in the grid, you must eventually call: + * ```javascript + * this.grid.behavior.applyAnalytics(); + * this.grid.repaint(); + * ``` + * @param {object|string} findKeyOrHash - One of: + * * _string_ - Column name. + * * _object_ - Hash of zero or more key-value pairs to search for. + * @param {*|string[]} [findValOrList] - One of: + * _omitted_ - When `findKeyOrHash` is a hash and you want to search all its keys. + * _string[]_ - When `findKeyOrHash` is a hash but you only want to search certain keys. + * _otherwise_ - When `findKeyOrHash` is a string. Value to search for. + * @param {object|string} modKeyOrHash - One of: + * * _string_ - Column name. + * * _object_ - Hash of zero or more key-value pairs to modify. + * @param {*|string[]} [modValOrList] - One of: + * _omitted_ - When `modKeyOrHash` is a hash and you want to modify all its keys. + * _string[]_ - When `modKeyOrHash` is a hash but you only want to modify certain keys. + * _otherwise_ - When `modKeyOrHash` is a string. The modified value. + * @returns {object} The modified row object. + */ + modifyRowById: function(findKeyOrHash, findValOrList, modKeyOrHash, modValOrList) { + var dataRow, keys, columnName; + + if (typeof findKeyOrHash !== 'object' || findValOrList instanceof Array) { + dataRow = this.source.findRow(findKeyOrHash, findValOrList); + } else { + dataRow = this.source.findRow(findKeyOrHash); + modValOrList = modKeyOrHash; // promote + modKeyOrHash = findValOrList; // promote + } + + if (dataRow) { + if (typeof modKeyOrHash !== 'object') { + dataRow[modKeyOrHash] = modValOrList; + } else { + keys = modValOrList instanceof Array ? modValOrList : Object.keys(modKeyOrHash); + for (var key in keys) { + columnName = keys[key]; + dataRow[columnName] = modKeyOrHash[columnName]; + } + } + } + + return dataRow; + }, + + /** + * @summary Replace entire ID'd row object with another. + * @desc The replacement may have (but does not have to have) the same ID as the row object being replaced. + * + * If data source pipeline in use, to see the replaced row in the grid, you must eventually call: + * ```javascript + * this.grid.behavior.applyAnalytics(); + * this.grid.behaviorChanged(); + * ``` + * @param {object|string} keyOrHash - One of: + * * _string_ - Column name. + * * _object_ - Hash of zero or more key-value pairs to search for. + * @param {*|string[]} [valOrList] - One of: + * _omitted_ - When `keyOrHash` is a hash and you want to search all its keys. + * _string[]_ - When `keyOrHash` is a hash but you only want to search certain keys. + * _otherwise_ - When `keyOrHash` is a string. Value to search for. + * @param {object} replacement + * @returns {object} The replaced row object. + */ + replaceRowById: function(keyOrHash, valOrList, replacement) { + if (typeof keyOrHash === 'object' && !(valOrList instanceof Array)) { + replacement = valOrList; // promote + } + if (typeof replacement !== 'object') { + throw 'Expected an object for replacement but found ' + typeof replacement + '.'; + } + return this.source.findRow.apply(this.source, arguments); + } +}; + +function getByIdArgs(keyOrHash, valOrList) { + var length = typeof keyOrHash !== 'object' || valOrList instanceof Array ? 2 : 1; + return Array.prototype.slice.call(arguments, 0, length); +} + +/** + * @name mixInTo + * @summary Mix all the other members into the given target object. + * @desc The target object is intended to be Hypergrid's in-memory data model object ({@link dataModels.JSON}). + * + * **NOTE:** This `mixInTo` method is defined here rather than above just so that it will be non-enumerable and therefore not itself mixed into the `target` object. + * @function + * @param {object} target - Your data model instance or its prototype. + * @memberOf rowById + */ +Object.defineProperty(rowById, 'mixInTo', { // defined here just to make it non-enumerable + value: function(target) { + Object.keys(this).forEach(function(key) { + target[key] = this[key]; + }.bind(this)); + } +}); + +module.exports = rowById; diff --git a/add-ons/tree-view.js b/add-ons/tree-view.js index 42f8a620a..5c54f52c9 100644 --- a/add-ons/tree-view.js +++ b/add-ons/tree-view.js @@ -1,39 +1,74 @@ 'use strict'; +// NOTE: gulpfile.js's 'add-ons' task makes a copy of this file, altering the final line. The copy is placed in demo/build/add-ons/ along with a minified version. Both files are eventually deployed to http://openfin.github.io/fin-hypergrid/add-ons/. Neither file is saved to the repo. + /** * @classdesc This is a simple helper class to set up the tree-view data source in the context of a hypergrid. * * It includes methods to: - * * Insert `DataSourceTreeview` into the data model's pipeline (`addPipe`, `addPipeTo`). - * * Perform the self-join and rebuild the index to turn the tree-view on or off, optionally hiding the ID columns (`setRelation`). + * * Insert the tree-view data source (`DataSourceTreeview`) into the data model's pipeline (see {#@link TreeView#setPipeline|setPipeline} method) along with the optional filter and sort data sources. + * * Perform the self-join and rebuild the index to turn the tree-view on or off, optionally hiding the ID columns ({#@link TreeView#setRelation|setRelation} method). * - * @param {object} [options] - * @param {boolean} [options.shared=false] + * @param {object} options - Passed to data source's {@link DataSourceTreeView#setRelation|setRelation} method ({@link http://openfin.github.io/hyper-analytics/DataSourceTreeview.html#setRelation|see}) when called here by local API's {@link TreeView#setRelation|this.setRelation} method. * @constructor */ function TreeView(grid, options) { this.grid = grid; - this.options = options; + this.options = options || {}; } TreeView.prototype = { + constructor: TreeView.prototype.constructor, /** - * @summary Reconfigure the dataModel's pipeline for tree view. - * @desc The pipeline is reset starting with either the given `options.dataSource` _or_ the existing pipeline's first data source. + * @summary Reconfigure the data model's data pipeline for tree view. + * @desc The _data transformation pipeline_ is an ordered list of data transformations, always beginning with an actual data source. Each _transformation_ in the pipeline operates on the data source immediately ahead of it. While transformations are free to completely rewrite the data in any way they want, most transformations merely apply an index to the data. + * + * The _shared pipeline_ is defined on the data model's prototype and is used for all grid instances (using the same data model). A grid can however define a _local pipeline_ on the data model's instance. + * + * In any case, the actual data pipeline is (re)constructed from a _pipeline configuration_ each time data is set on the grid via {@link dataModel/JSON#setData|setData}. + * + * This method reconfigures the data pipeline suitable for tree view. It is designed to operate on either of: + * * the "shared" pipeline configuration (on the grid's data model's prototype) + * * the grid's "local" pipeline configuration (on the grid's data model's instance) * - * Then the tree view filter and sorter data sources are added as requested. + * This method operates as follows: + * 1. Reset the pipeline: + * * In the case of the shared pipeline, the array is truncated in place. + * * In the case of an instance pipeline, a new array is created. + * 2. Add the first data source: + * * The data source provided in `options.firstPipe` is used if given; otherwise... + * * The existing pipeline configuration's first data source will be reused. (First time in, this will always come from the prototype's version.) + * 3. Add the filter data source (if requested). + * 4. Add the tree sorter data source (if requested). + * 5. Finally, add the tree view data source. * - * Finally the tree view data source is added. + * Step 1 above operates on the shared pipeline when you supply the data model's prototype in `options.dataModelPrototype` (see below). In this case, you have the option of calling this method _before_ instantiating your grid(s): * - * This method can operate on either: - * * A data model prototype, which will affect all data models subsequently created therefrom. The prototype must be given in `options.dataModelPrototype`. - * * The current data model instance. In this case, the instance is given its own new pipeline. + * ```javascript + * var JSON = Hypergrid.dataModels.JSON.prototype; + * var pipelineOptions = { dataModelPrototype: JSON.prototype } + * TreeView.prototype.setPipeline(pipelineOptions); + * ``` + * + * This approach avoids the need to reset the data after reconfiguring the pipeline (in which case, do _not_ call this method again after instantiation). * * @param {object} [options] - * @param {object} [options.dataModelPrototype] - Adds the pipes to the given object. If omitted, this must be an instance; adds the pipes to a new "own" pipeline created from the first data source of the instance's old pipeline. - * @param {dataSourcePipelineObject} [options.firstPipe] - Use as first data source in the new pipeline. If omitted, re-uses the existing pipeline's first data source. + * + * @param {boolean} [options.includeFilter=false] - Enables filtering. Includes the filter data source. The filter row is hidden if falsy. + * + * @param {boolean} [options.includeSorter=false] - Enables sorting. Includes the specialized tree sorter data source. + * + * @param {object} [options.dataModelPrototype] - Adds requested pipes to the "shared" pipeline array object instead of to a new custom (instance) pipeline array object. + * + * Supply this option when you want to set up the "shared" pipeline on the data model prototype, which would then be available to all grid instances subsequently created thereafter. In this case, you can call this method before or after grid instantiation. To call it before, call it directly on `TreeView.prototype`; to call it after, call it normally (on the `TreeView` instance). + * + * If omitted, a new "own" (instance) pipeline is created, overriding the prototype's (shared) pipeline. (In this case this method must be called normally, on the `Treeview` instance.) + * + * In either case, if called "normally" (on the instance), the data is reset via `setData`. (If called on the prototype it is not reset here. Currently the `Hypergrid` constructor calls it.) + * + * @param {dataSourcePipelineObject} [options.firstPipe] - Use as first data source in the new pipeline. If undefined, the existing pipeline's first data source will be reused. */ setPipeline: function(options) { options = options || {}; @@ -78,7 +113,7 @@ TreeView.prototype = { /** * @summary Build/unbuild the tree view. - * @param {boolean} join - Turn tree-view **ON**. If falsy (or omitted), turn it **OFF**. + * @param {boolean} join - If truthy, turn tree-view **ON**. If falsy (or omitted), turn it **OFF**. * @param {boolean} [hideIdColumns=false] - Once hidden, cannot be unhidden from here. * @returns {boolean} Joined state. */ @@ -92,11 +127,11 @@ TreeView.prototype = { columnProps = behavior.getColumn(dataSource.treeColumn.index).getProperties(); if (joined) { - // save the current value of column's editable property and set it to false + // Make the tree column uneditable: Save the current value of the tree column's editable property and set it to false. this.editableWas = !!columnProps.editable; columnProps.editable = false; - // save value of grid's checkboxOnlyRowSelections property and set it to true so drill-down clicks don't select the row they are in + // Save value of grid's checkboxOnlyRowSelections property and set it to true so drill-down clicks don't select the row they are in this.checkboxOnlyRowSelectionsWas = state.checkboxOnlyRowSelections; state.checkboxOnlyRowSelections = true; @@ -110,16 +145,7 @@ TreeView.prototype = { } }); } - - dataSource.defaultSortColumn = dataSource.getColumnInfo(options.defaultSortColumn, dataSource.treeColumn.name); - - // If unsorted, sort by tree column - if (behavior.getSortedColumnIndexes().length === 0) { - var gridIndex = behavior.getActiveColumnIndex(dataSource.defaultSortColumn.index); - this.grid.toggleSort(gridIndex, []); - } } else { - dataSource.defaultSortColumn = undefined; columnProps.editable = this.editableWas; state.checkboxOnlyRowSelections = this.checkboxOnlyRowSelectionsWas; } @@ -132,12 +158,14 @@ TreeView.prototype = { return joined; } + }; /** * This is the required test function called by the data model's `isDrilldown` method in context. _Do not call directly._ * @param {number} [event.dataCell.x] If available, also checks that the column clicked is the tree column. * @returns {boolean} If the data source is a tree view. + * @private */ function isTreeview(event) { var treeview = this.sources.treeview, diff --git a/demo/data/tree-data.js b/demo/data/tree-data.js index 086227584..cb1d6bc05 100644 --- a/demo/data/tree-data.js +++ b/demo/data/tree-data.js @@ -2,18 +2,18 @@ (function() { var data = [ - { ID: 0, parentID: null, State: 'USA', Latitude: 36.2161472, Longitude: -113.6866279 }, - { ID: 10, parentID: null, State: 'France', Latitude: 46.1274793, Longitude: -2.288454 }, - { ID: 1, parentID: 0, State: 'New York', Latitude: 40.7055651, Longitude: -74.118086 }, - { ID: 2, parentID: 1, State: 'Albany', Latitude: 42.6681345, Longitude: -73.846419 }, - { ID: 3, parentID: 1, State: 'Syracuse', Latitude: 43.0352286, Longitude: -76.1742994 }, - { ID: 4, parentID: 0, State: 'California', Latitude: 37.1870791, Longitude: -123.762638 }, - { ID: 5, parentID: 4, State: 'Monterey', Latitude: 36.5943628, Longitude: -121.9025183 }, - { ID: 6, parentID: 4, State: 'Berkeley', Latitude: 37.8759458, Longitude: -122.2981316 }, - { ID: 7, parentID: 4, State: 'Laguna', Latitude: 33.5482634, Longitude: -117.8447927 }, - { ID: 8, parentID: 0, State: 'Massachusetts', Latitude: 42.6369691, Longitude: -71.3618803 }, - { ID: 9, parentID: 8, State: 'Lowell', Latitude: 42.6369691, Longitude: -71.3618803 }, - { ID: 11, parentID: 10, State: 'Paris', Latitude: 48.8588376, Longitude: 2.2773459 }, + { ID: 1, parentID: 20, State: 'New York', Latitude: 40.7055651, Longitude: -74.118086 }, + { ID: 5, parentID: 4, State: 'Monterey', Latitude: 36.5943628, Longitude: -121.9025183 }, + { ID: 2, parentID: 1, State: 'Albany', Latitude: 42.6681345, Longitude: -73.846419 }, + { ID: 20, parentID: null, State: 'USA', Latitude: 36.2161472, Longitude: -113.6866279 }, + { ID: 6, parentID: 4, State: 'Berkeley', Latitude: 37.8759458, Longitude: -122.2981316 }, + { ID: 3, parentID: 1, State: 'Syracuse', Latitude: 43.0352286, Longitude: -76.1742994 }, + { ID: 4, parentID: 20, State: 'California', Latitude: 37.1870791, Longitude: -123.762638 }, + { ID: 10, parentID: null, State: 'France', Latitude: 46.1274793, Longitude: -2.288454 }, + { ID: 7, parentID: 4, State: 'Laguna', Latitude: 33.5482634, Longitude: -117.8447927 }, + { ID: 8, parentID: 20, State: 'Massachusetts', Latitude: 42.6369691, Longitude: -71.3618803 }, + { ID: 9, parentID: 8, State: 'Lowell', Latitude: 42.6369691, Longitude: -71.3618803 }, + { ID: 11, parentID: 10, State: 'Paris', Latitude: 48.8588376, Longitude: 2.2773459 }, ]; window.treeData = data; })(); diff --git a/demo/example.html b/demo/example.html index 90f952e5a..c0739f193 100644 --- a/demo/example.html +++ b/demo/example.html @@ -7,7 +7,7 @@
- + - + Treeview -
+
diff --git a/demo/tree-view.html b/demo/tree-view.html index fb4a4cb32..c8d39b3ea 100644 --- a/demo/tree-view.html +++ b/demo/tree-view.html @@ -10,6 +10,6 @@ Treeview -
+
diff --git a/gulpfile.js b/gulpfile.js index 4888a9478..dd85bf1dd 100644 --- a/gulpfile.js +++ b/gulpfile.js @@ -39,10 +39,10 @@ gulp.task('build', function(callback) { clearBashScreen(); runSequence( 'lint', - 'test', 'images', 'html-templates', 'css-templates', + 'test', 'add-ons', //'doc', //'beautify', @@ -84,6 +84,7 @@ function lint() { srcDir + jsFiles, '!' + srcDir + '**/old/**/', demoDir + 'js/demo.js', + testDir + '**/*.js', //'../../filter-tree/src/' + jsFiles // comment off this line and the one above when filter tree on npm ]) .pipe($$.excludeGitignore()) @@ -93,12 +94,12 @@ function lint() { } function test(cb) { - return gulp.src(testDir + 'index.js') + return gulp.src(testDir + '**/*.js') .pipe($$.mocha({reporter: 'spec'})); } function beautify() { - return gulp.src(srcDir + jsFiles) + return gulp.src([srcDir + jsFiles, testDir + '**/*.js']) .pipe($$.beautify()) //apparent bug: presence of a .jsbeautifyrc file seems to force all options to their defaults (except space_after_anon_function which is forced to true) so I deleted the file. Any needed options can be included here. .pipe(gulp.dest(srcDir)); } diff --git a/package.json b/package.json index 071efb671..6acc700c2 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "fin-hypergrid", - "version": "1.0.8", + "version": "1.0.9", "description": "Canvas-based high-performance spreadsheet", "repository": { "type": "git", @@ -17,11 +17,12 @@ ], "dependencies": { "automat": "1.2.0", + "chai": "^3.5.0", "extend-me": "2.2.4", - "filter-tree": "0.3.34", + "filter-tree": "0.3.38", "finbars": "1.5.1", - "fincanvas": "1.3.0", - "hyper-analytics": "0.11.7", + "hyper-analytics": "0.11.13", + "fincanvas": "1.4.0", "list-dragon": "1.3.3", "lru-cache": "2.7.0", "mustache": "2.2.0", diff --git a/src/Hypergrid.js b/src/Hypergrid.js index 3a5a6fd41..b2ee44c93 100644 --- a/src/Hypergrid.js +++ b/src/Hypergrid.js @@ -22,6 +22,7 @@ var CellEditors = require('./cellEditors'); var themeInitialized = false, polymerTheme = Object.create(defaults), + defaultContainerHeight = 300, globalProperties = Object.create(polymerTheme); /**s @@ -43,7 +44,7 @@ var themeInitialized = false, * @param {string} [options.localization.dateOptions=Hypergrid.localization.dateOptions] - Options passed to `Intl.DateFomrat` for creating the basic "date" localizer. * @param {object} [options.margin] - optional canvas margins * @param {string} [options.margin.top=0] - * @param {string} [options.margin.right='-200px'] + * @param {string} [options.margin.right=0] * @param {string} [options.margin.bottom=0] * @param {string} [options.margin.left=0] */ @@ -52,6 +53,15 @@ function Hypergrid(div, options) { this.div = (typeof div === 'string') ? document.querySelector(div) : div; + //Default Position and height to ensure DnD works + if (!this.div.style.position){ + this.div.style.position = 'relative'; + } + + if (this.div.clientHeight < 1){ + this.div.style.height = defaultContainerHeight + 'px'; + } + stylesheet.inject('grid'); this.lastEdgeSelection = [0, 0]; @@ -75,6 +85,7 @@ function Hypergrid(div, options) { //prevent the default context menu for appearing this.div.oncontextmenu = function(event) { + event.stopPropagation(); event.preventDefault(); return false; }; @@ -93,7 +104,7 @@ function Hypergrid(div, options) { var margin = options.margin || {}; margin.top = margin.top || 0; - margin.right = margin.right === undefined ? '-200px' : 0; + margin.right = margin.right || 0; margin.bottom = margin.bottom || 0; margin.left = margin.left || 0; @@ -125,6 +136,8 @@ function Hypergrid(div, options) { self.checkClipboardCopy(evt); }); this.getCanvas().resize(); + + this.refreshProperties(); } Hypergrid.prototype = { @@ -284,6 +297,8 @@ Hypergrid.prototype = { this.getRenderer().reset(); this.getCanvas().resize(); this.behaviorChanged(); + + this.refreshProperties(); }, //resetTextWidthCache: function() { @@ -414,10 +429,13 @@ Hypergrid.prototype = { * @desc Utility function to push out properties if we change them. * @param {object} properties - An object of various key value pairs. */ - refreshProperties: function() { + var state = this.getProperties(); + this.selectionModel.multipleSelections = state.multipleSelections; + // this.canvas = this.shadowRoot.querySelector('fin-canvas'); //this.canvas = new Canvas(this.divCanvas, this.renderer); //TODO: Do we really need to be recreating it here? + this.renderer.computeCellsBounds(); this.checkScrollbarVisibility(); this.behavior.defaultRowHeight = null; @@ -428,7 +446,7 @@ Hypergrid.prototype = { /** * @memberOf Hypergrid.prototype - * @desc Ammend properties for this hypergrid only. + * @desc Amend properties for this hypergrid only. * @param {object} moreProperties - A simple properties hash. */ addProperties: function(moreProperties) { @@ -455,6 +473,7 @@ Hypergrid.prototype = { setState: function(state) { var self = this; this.behavior.setState(state); + this.refreshProperties(); setTimeout(function() { self.behaviorChanged(); self.synchronizeScrollingBoundries(); @@ -566,6 +585,7 @@ Hypergrid.prototype = { behavior.autoSizeRowNumberColumn(); if (this.isColumnAutosizing()) { behavior.checkColumnAutosizing(false); + setTimeout(function() { behavior.grid.synchronizeScrollingBoundries();}); } }, /** @@ -768,9 +788,10 @@ Hypergrid.prototype = { this.numColumns = this.getColumnCount(); this.numRows = this.getRowCount(); this.behaviorShapeChanged(); + } else { + this.computeCellsBounds(); + this.repaint(); } - this.computeCellsBounds(); - this.repaint(); }, /** @@ -807,7 +828,7 @@ Hypergrid.prototype = { * @desc The dimensions of the grid data have changed. You've been notified. */ behaviorStateChanged: function() { - this.getRenderer().computeCellsBounds(); + this.computeCellsBounds(); this.repaint(); }, @@ -828,8 +849,7 @@ Hypergrid.prototype = { * @desc Paint immediately in this microtask. */ paintNow: function() { - var canvas = this.getCanvas(); - canvas.paintNow(); + this.getCanvas().paintNow(); }, /** @@ -1215,14 +1235,8 @@ Hypergrid.prototype = { * @returns {Rectangle} The pixel coordinates of just the center 'main" data area. */ getDataBounds: function() { - var colDNDHackWidth = 200; //this was a hack to help with column dnd, need to factor this into a shared variable var b = this.canvas.bounds; - - //var x = this.getRowNumbersWidth(); - // var y = behavior.getFixedRowsHeight() + 2; - - var result = new Rectangle(0, 0, b.origin.x + b.extent.x - colDNDHackWidth, b.origin.y + b.extent.y); - return result; + return new Rectangle(0, 0, b.origin.x + b.extent.x, b.origin.y + b.extent.y); }, getRowNumbersWidth: function() { @@ -2184,8 +2198,10 @@ Hypergrid.prototype = { if (!bounds) { return; } - var scrollableHeight = bounds.height - this.behavior.getFixedRowsMaxHeight() - 15; //5px padding at bottom and right side - var scrollableWidth = (bounds.width - 200) - this.behavior.getFixedColumnsMaxWidth() - 15; + + // 15px padding at bottom and right side + var scrollableHeight = bounds.height - this.behavior.getFixedRowsMaxHeight() - 15; + var scrollableWidth = bounds.width - this.behavior.getFixedColumnsMaxWidth() - 15; var lastPageColumnCount = 0; var columnsWidth = 0; @@ -3467,20 +3483,23 @@ function clearObjectProperties(obj) { } function valOrFunc(dataRow, column) { - var result = dataRow[column.name], + var result, calculator; + if (dataRow) { + result = dataRow[column.name]; calculator = (typeof result)[0] === 'f' && result || column.calculator; if (calculator) { result = calculator(dataRow, column.name); } + } return result || result === 0 || result === false ? result : ''; } /** * @summary Shared localization defaults for all grid instances. * @desc These property values are overridden by those supplied in the `Hypergrid` constructor's `options.localization`. - * @property {string|string[]} [options.localization.defaultLocale] - The default locale to use when an explicit `locale` is omitted from localizer constructor calls. Passed to Intl.NumberFormat` and `Intl.DateFormat`. See {@ https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl#Locale_identification_and_negotiation|Locale identification and negotiation} for more information. Omitting will use the runtime's local language and region. - * @property {object} [options.localization.numberOptions] - Options passed to `Intl.NumberFormat` for creating the basic "number" localizer. - * @property {object} [options.localization.dateOptions] - Options passed to `Intl.DateFormat` for creating the basic "date" localizer. + * @property {string|string[]} [locale] - The default locale to use when an explicit `locale` is omitted from localizer constructor calls. Passed to Intl.NumberFormat` and `Intl.DateFormat`. See {@ https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl#Locale_identification_and_negotiation|Locale identification and negotiation} for more information. Omitting will use the runtime's local language and region. + * @property {object} [numberOptions] - Options passed to `Intl.NumberFormat` for creating the basic "number" localizer. + * @property {object} [dateOptions] - Options passed to `Intl.DateFormat` for creating the basic "date" localizer. */ Hypergrid.localization = { diff --git a/src/behaviors/JSON.js b/src/behaviors/JSON.js index 3b787c1e9..c511feaa0 100644 --- a/src/behaviors/JSON.js +++ b/src/behaviors/JSON.js @@ -77,16 +77,12 @@ var JSON = Local.extend('behaviors.JSON', { return new DataModelJSON(this.grid); }, - applyAnalytics: function() { - this.dataModel.applyAnalytics(); - }, - /** * @memberOf behaviors.JSON.prototype * @description Set the header labels. * @param {string[]|object} headers - The header labels. One of: * * _If an array:_ Must contain all headers in column order. - * * _If a hash:_ May contain any headers, keyed by field name, in any order (of course). + * * _If a hash:_ May contain any headers, keyed by field name, in any order. */ setHeaders: function(headers) { if (headers instanceof Array) { @@ -230,11 +226,16 @@ var JSON = Local.extend('behaviors.JSON', { }, getColumnAlignment: function(x) { - if (x === 0 && this.hasHierarchyColumn()) { - return 'left'; + var align; + if (x === -1) { + align = 'right'; + } else if (x === 0 && this.hasHierarchyColumn()) { + align = 'left'; } else { - return 'center'; + align = this.getColumnProperties(x).halign; } + + return align; }, getHiddenColumns: function() { return this.dataModel.getHiddenColumns(); diff --git a/src/cellEditors/CellEditor.js b/src/cellEditors/CellEditor.js index c3306be25..f0862a1b2 100644 --- a/src/cellEditors/CellEditor.js +++ b/src/cellEditors/CellEditor.js @@ -241,7 +241,7 @@ var CellEditor = Base.extend('CellEditor', { this.grid.selectViewportCell(point.x, point.y - this.grid.getHeaderRowCount()); this.errorEffectBegin(++this.errors % feedback === 0 && error); } else { // invalid but no feedback - return this.cancelEditing(); + this.cancelEditing(); } return !error; diff --git a/src/cellRenderers/SimpleCell.js b/src/cellRenderers/SimpleCell.js index 9496744a1..47f70812b 100644 --- a/src/cellRenderers/SimpleCell.js +++ b/src/cellRenderers/SimpleCell.js @@ -63,6 +63,7 @@ var SimpleCell = CellRenderer.extend('SimpleCell', { if (gc.font !== font) { gc.font = font; } + if (gc.textAlign !== 'left') { gc.textAlign = 'left'; } @@ -363,7 +364,7 @@ function layerColors(gc, colors, x, y, width, height) { function valOrFunc(vf, config, calculator) { var result = vf; - if (config.isGridColumn && config.isGridRow) { + if (config.isGridColumn && config.isGridRow && config.dataRow) { calculator = (typeof vf)[0] === 'f' && vf || calculator; if (calculator) { result = calculator(config.dataRow, config.columnName); diff --git a/src/dataModels/JSON.js b/src/dataModels/JSON.js index 55cecbcd1..86954d604 100644 --- a/src/dataModels/JSON.js +++ b/src/dataModels/JSON.js @@ -94,7 +94,7 @@ var JSON = DataModel.extend('dataModels.JSON', { }, getData: function() { - return this.sources.source.data; + return this.source.data; }, getFilteredData: function() { @@ -221,7 +221,7 @@ var JSON = DataModel.extend('dataModels.JSON', { } else if (isHeaderRow && y === 0) { return this._setHeader(x, value); } else if (isFilterRow) { - this.setFilter(x, value, { alert: true }); + this.setFilter(x, value); } else { return this._setHeader(x, value); } @@ -299,11 +299,11 @@ var JSON = DataModel.extend('dataModels.JSON', { * @returns {string[]} */ getCalculators: function() { - return this.dataSource.getCalculators(); + return this.dataSource.getProperty('calculators'); }, /** @typedef {object} dataSourcePipelineObject - * @property {function} DataSource - A `hyper-analytics`-style "data source" constructor. + * @property {string} type - A `hyper-analytics`-style "data source" constructor name. * @property {*} [options] - When defined, passed as 2nd argument to constructor. * @property {string} [parent] - Defines a branch off the main sequence. */ @@ -515,36 +515,18 @@ var JSON = DataModel.extend('dataModels.JSON', { /** * @memberOf dataModels.JSON.prototype - * @param {number} colIndex + * @param {number} columnIndex * @param {boolean} deferred */ unSortColumn: function(columnIndex, deferred) { var sorts = this.getSortedColumnIndexes(), - sortPosition, found; + sortPosition; if (sorts.find(function(sortSpec, index) { sortPosition = index; return sortSpec.columnIndex === columnIndex; })) { - if (sorts.length === 1) { - for (var dataSource = this.dataSource; dataSource; dataSource = dataSource.dataSource) { - if (dataSource.defaultSortColumn) { - found = true; - break; - } - } - } - - if (found) { - // Make the sole remaining sorted column the tree column of the "joined" data source - sorts[0] = { - columnIndex: dataSource.defaultSortColumn.index, - direction: 1 - }; - } else { - sorts.splice(sortPosition, 1); - } - + sorts.splice(sortPosition, 1); if (!deferred) { this.applyAnalytics({columnSort: true}); } @@ -832,12 +814,28 @@ var JSON = DataModel.extend('dataModels.JSON', { }, getUnfilteredValue: function(x, y) { - return this.sources.source.getValue(x, y); + return this.source.getValue(x, y); }, getUnfilteredRowCount: function() { - return this.sources.source.getRowCount(); - } + return this.source.getRowCount(); + }, + + /** + * @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.applyAnalytics(); + * 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; + }, }); // LOCAL METHODS -- to be called with `.call(this` diff --git a/src/defaults.js b/src/defaults.js index 905d89126..bc69f8d1a 100644 --- a/src/defaults.js +++ b/src/defaults.js @@ -16,7 +16,7 @@ module.exports = { /** * The font for data cells. * @default - * @type {cssFont} + * @type {string} * @instance */ noDataMessage: 'no data to display', @@ -364,6 +364,12 @@ module.exports = { * @instance */ fixedColAlign: 'center', + /** + * @default + * @type {string} + * @instance + */ + halign: 'center', /** * @default @@ -543,14 +549,14 @@ module.exports = { */ cellSelection: true, - /** Clicking in a row header (leftmost column) "selects" the row; the entire row is added to the select region and repainted with "row selection" colors. + /** Clicking in a column header (top row) "selects" the column; the entire column is added to the select region and repainted with "column selection" colors. * @default * @type {boolean} * @instance */ columnSelection: true, - /** Clicking in a column header (top row) "selects" the column; the entire column is added to the select region and repainted with "column selection" colors. + /** Clicking in a row header (leftmost column) "selects" the row; the entire row is added to the select region and repainted with "row selection" colors. * @default * @type {boolean} * @instance @@ -729,6 +735,11 @@ module.exports = { */ unsortable: false, + /** Allow multiple cell region selections. + * @type {boolean} + * @default + */ + multipleSelections: false, }; /** @typedef {string} cssColor diff --git a/src/dialogs/ColumnPicker.js b/src/dialogs/ColumnPicker.js index d61508252..fe2a19076 100644 --- a/src/dialogs/ColumnPicker.js +++ b/src/dialogs/ColumnPicker.js @@ -22,8 +22,11 @@ var ColumnPicker = Dialog.extend('ColumnPicker', { this.grid = grid; if (behavior.isColumnReorderable()) { - // grab the lists from the behavior - if (behavior.setGroups){ + // parse & add the drag-and-drop stylesheet addendum + var stylesheetAddendum = stylesheet.inject('list-dragon-addendum'); + + // grab the group lists from the behavior + if (behavior.setGroups) { this.selectedGroups = { title: 'Groups', models: behavior.getGroups() @@ -33,8 +36,18 @@ var ColumnPicker = Dialog.extend('ColumnPicker', { title: 'Available Groups', models: behavior.getAvailableGroups() }; + + var groupPicker = new ListDragon([ + this.selectedGroups, + this.availableGroups + ]); + + // add the drag-and-drop sets to the dialog + this.append(groupPicker.modelLists[0].container); + this.append(groupPicker.modelLists[1].container); } + // grab the column lists from the behavior this.inactiveColumns = { title: 'Inactive Columns', models: behavior.getHiddenColumns().sort(compareByName) @@ -47,36 +60,22 @@ var ColumnPicker = Dialog.extend('ColumnPicker', { this.sortOnHiddenColumns = this.wasSortOnHiddenColumns = grid.resolveProperty('sortOnHiddenColumns'); - // parse & add the drag-and-drop stylesheet addendum - var stylesheetAddendum = stylesheet.inject('list-dragon-addendum'); - - // create drag-and-drop sets from the lists - var listSets = [ - new ListDragon([ - this.selectedGroups, - this.availableGroups - ], { - // add the list-dragon-base stylesheet right before the addendum - cssStylesheetReferenceElement: stylesheetAddendum - }), - new ListDragon([ - this.inactiveColumns, - this.activeColumns - ], { - // these models have a header property as their labels - label: '{header}' - }) - ]; + var columnPicker = new ListDragon([ + this.inactiveColumns, + this.activeColumns + ], { + // add the list-dragon-base stylesheet right before the addendum + cssStylesheetReferenceElement: stylesheetAddendum, + // these models have a header property as their labels + label: '{header}' + }); // add the drag-and-drop sets to the dialog - var self = this; - listSets.forEach(function(listSet) { - listSet.modelLists.forEach(function(list) { - self.append(list.container); - }); - }); + this.append(columnPicker.modelLists[0].container); + this.append(columnPicker.modelLists[1].container); + //Listen to the visible column changes - listSets[1].modelLists[1].element.addEventListener('listchanged', function(e){ + columnPicker.modelLists[1].element.addEventListener('listchanged', function(e){ grid.fireSyntheticOnColumnsChangedEvent(); }); diff --git a/src/features/CellEditing.js b/src/features/CellEditing.js index 48e84b31a..355f61efb 100644 --- a/src/features/CellEditing.js +++ b/src/features/CellEditing.js @@ -68,7 +68,7 @@ var CellEditing = Feature.extend('CellEditing', { editor = grid.onEditorActivate(pseudoEvent); if (editor instanceof CellEditor) { if (isVisibleChar) { - editor.setEditorValue(char); + editor.input.value = char; } else if (isDeleteChar) { editor.setEditorValue(''); } diff --git a/src/features/CellSelection.js b/src/features/CellSelection.js index efa2a38ee..0145ad938 100644 --- a/src/features/CellSelection.js +++ b/src/features/CellSelection.js @@ -72,11 +72,7 @@ var CellSelection = Feature.extend('CellSelection', { var isHeader = dy < headerRowCount || dx < headerColumnCount; - if (!grid.isCellSelection() || isRightClick || isHeader || isOutside) { - if (this.next) { - this.next.handleMouseDown(grid, event); - } - } else { + if (grid.isCellSelection() && !(isRightClick || isHeader || isOutside)) { var numFixedColumns = grid.getFixedColumnCount(); var numFixedRows = grid.getFixedRowCount(); @@ -96,6 +92,8 @@ var CellSelection = Feature.extend('CellSelection', { var keys = primEvent.detail.keys; this.dragging = true; this.extendSelection(grid, dCell, keys); + } else if (this.next) { + this.next.handleMouseDown(grid, event); } }, @@ -108,12 +106,7 @@ var CellSelection = Feature.extend('CellSelection', { handleMouseDrag: function(grid, event) { var isRightClick = event.primitiveEvent.detail.isRightClick; - if (!grid.isCellSelection() || isRightClick || !this.dragging) { - if (this.next) { - this.next.handleMouseDrag(grid, event); - } - } else { - + if (this.dragging && grid.isCellSelection() && !isRightClick) { var numFixedColumns = grid.getFixedColumnCount(); var numFixedRows = grid.getFixedRowCount(); @@ -140,6 +133,8 @@ var CellSelection = Feature.extend('CellSelection', { this.checkDragScroll(grid, this.currentDrag); this.handleMouseDragCellSelection(grid, dCell, primEvent.detail.keys); + } else if (this.next) { + this.next.handleMouseDrag(grid, event); } }, @@ -315,8 +310,8 @@ var CellSelection = Feature.extend('CellSelection', { if (hasSHIFT) { grid.clearMostRecentSelection(); - grid.select(mousePoint.x, mousePoint.y, x - mousePoint.x + 1, y - mousePoint.y + 1); - grid.setDragExtent(grid.newPoint(x - mousePoint.x + 1, y - mousePoint.y)); + grid.select(mousePoint.x, mousePoint.y, x - mousePoint.x, y - mousePoint.y); + grid.setDragExtent(grid.newPoint(x - mousePoint.x, y - mousePoint.y)); } else { grid.select(x, y, 0, 0); grid.setMouseDown(grid.newPoint(x, y)); diff --git a/src/filter/DefaultFilter.js b/src/filter/DefaultFilter.js index eb8342c37..44cc0ae86 100644 --- a/src/filter/DefaultFilter.js +++ b/src/filter/DefaultFilter.js @@ -335,7 +335,7 @@ var DefaultFilter = FilterTree.extend('DefaultFilter', { state = this.parseStateString(state, options); // because .add() only takes object syntax subexpression = this.columnFilters.add(state); } - options.throw = true; + error = subexpression.invalid(options); } } diff --git a/src/filter/parser-CQL.js b/src/filter/parser-CQL.js index 0114a9854..8edd3dd30 100644 --- a/src/filter/parser-CQL.js +++ b/src/filter/parser-CQL.js @@ -44,7 +44,12 @@ function ParserCQL(operatorsHash, options) { operators = operators.sort(descendingByLength); // Escape all symbolic (non alpha) operators. - operators = operators.map(function(op) { return /[^\w]/.test(op) ? '\\' + op.split('').join('\\') : op; }); + operators = operators.map(function(op) { + if (/^[^A-Z]/.test(op)) { + op = '\\' + op.split('').join('\\'); + } + return op; + }); var symbolicOperators = operators.filter(function(op) { return op[0] === '\\'; }), alphaOperators = operators.filter(function(op) { return op[0] !== '\\'; }).join('|'); diff --git a/src/lib/Localization.js b/src/lib/Localization.js index 1ec1cd0f3..b85178482 100644 --- a/src/lib/Localization.js +++ b/src/lib/Localization.js @@ -37,6 +37,39 @@ var Formatter = Base.extend({ } }); + +// Safari has no Intl implementation +window.Intl = window.Intl || { + NumberFormat: function(locale, options) { + var digits = '0123456789'; + this.format = function(n) { + var s = n.toString(); + if (!options || options.useGrouping === undefined || options.useGrouping) { + var dp = s.indexOf('.'); + if (dp < 0) { dp = s.length; } + while ((dp -= 3) > 0 && digits.indexOf(s[dp - 1]) >= 0) { + s = s.substr(0, dp) + ',' + s.substr(dp); + } + } + return s; + }; + }, + DateTimeFormat: function(locale, options) { + this.format = function(date) { + if (date != null) { + if (typeof date !== 'object') { + date = new Date(date); + } + date = date.getMonth() + 1 + '-' + date.getDate() + '-' + date.getFullYear(); + } else { + date = null; + } + return date; + }; + } +}; + + /** * @summary Create a number localizer. * @implements localizerInterface @@ -72,7 +105,7 @@ var NumberFormatter = Formatter.extend('NumberFormatter', { * @private * @desc Localized digits and decimal point. Will also include standardized digits and decimal point if `options.acceptStandardDigits` is truthy. * - * For internal use by the {@link NumberFormatter#standardize|standardize} method. + * For internal use by the {@link NumberFormatter#parse|parse} method. * @memberOf NumberFormatter.prototype */ this.map = mapper(10123456789.5).substr(1, 11); // localized '0123456789.' @@ -116,7 +149,7 @@ var NumberFormatter = Formatter.extend('NumberFormatter', { * * Use this method to: * 1. Filter out invalid characters on a `onkeydown` event; or - * 2. Test an edited string prior to calling the {@link module:localization~NumberFormatter#standardize|standardize}. + * 2. Test an edited string prior to calling the {@link module:localization~NumberFormatter#parse|parse}. * * NOTE: This method does not check grammatical syntax; it only checks for invalid characters. * @@ -232,7 +265,7 @@ var DateFormatter = Formatter.extend('DateFormatter', { */ this.invalids = new RegExp( '[^' + - localizedDate + + localizedDate.replace(/-/g, '\\-') + missingDigits + ']' ); @@ -247,7 +280,7 @@ var DateFormatter = Formatter.extend('DateFormatter', { * * Use this method to: * 1. Filter out invalid characters on a `onkeydown` event; or - * 2. Test an edited string prior to calling the {@link module:localization~DateFormatter#standardize|standardize}. + * 2. Test an edited string prior to calling the {@link module:localization~DateFormatter#parse|parse}. * * NOTE: The current implementation only supports date formats using all numerics (which is the default for `Intl.DateFormat`). * diff --git a/src/lib/Mappy.js b/src/lib/Mappy.js deleted file mode 100644 index d8ba9b003..000000000 --- a/src/lib/Mappy.js +++ /dev/null @@ -1,131 +0,0 @@ -'use strict'; - -module.exports = (function() { - - var oidPrefix = '.~.#%_'; //this should be something we never will see at the begining of a string - var counter = 0; - - var hash = function(key) { - var typeOf = typeof key; - switch (typeOf) { - case 'number': - return oidPrefix + typeOf + '_' + key; - case 'string': - return oidPrefix + typeOf + '_' + key; - case 'boolean': - return oidPrefix + typeOf + '_' + key; - case 'symbol': - return oidPrefix + typeOf + '_' + key; - case 'undefined': - return oidPrefix + 'undefined'; - case 'object': - case 'function': - /*eslint-disable */ - if (!key.___finhash) { - key.___finhash = oidPrefix + counter++; - } - return key.___finhash; - /*eslint-enable */ - } - }; - - // Object.is polyfill, courtesy of @WebReflection - var is = Object.is || - function(a, b) { - return a === b ? a !== 0 || 1 / a == 1 / b : a != a && b != b; // eslint-disable-line - }; - - // More reliable indexOf, courtesy of @WebReflection - var betterIndexOf = function(arr, value) { - if (value != value || value === 0) { // eslint-disable-line - for (var i = arr.length; i-- && !is(arr[i], value);) { // eslint-disable-line - } - } else { - i = [].indexOf.call(arr, value); - } - return i; - }; - - function Mappy() { - this.keys = []; - this.data = {}; - this.values = []; - } - - Mappy.prototype.set = function(key, value) { - var hashCode = hash(key); - if (this.data[hashCode] === undefined) { - this.keys.push(key); - this.values.push(value); - } - this.data[hashCode] = value; - }; - - Mappy.prototype.get = function(key) { - var hashCode = hash(key); - return this.data[hashCode]; - }; - - Mappy.prototype.getIfAbsent = function(key, ifAbsentFunc) { - var value = this.get(key); - if (value === undefined) { - value = ifAbsentFunc(key, this); - } - return value; - }; - - Mappy.prototype.size = function() { - return this.keys.length; - }; - - Mappy.prototype.clear = function() { - this.keys.length = 0; - this.data = {}; - }; - - Mappy.prototype.delete = function(key) { - var hashCode = hash(key); - if (this.data[hashCode] === undefined) { - return; - } - var index = betterIndexOf(this.keys, key); - this.keys.splice(index, 1); - this.values.splice(index, 1); - delete this.data[hashCode]; - }; - - Mappy.prototype.forEach = function(func) { - var keys = this.keys; - for (var i = 0; i < keys.length; i++) { - var key = keys[i]; - var value = this.get(key); - func(value, key, this); - } - }; - - Mappy.prototype.map = function(func) { - var keys = this.keys; - var newMap = new Mappy(); - for (var i = 0; i < keys.length; i++) { - var key = keys[i]; - var value = this.get(key); - var transformed = func(value, key, this); - newMap.set(key, transformed); - } - return newMap; - }; - - Mappy.prototype.copy = function() { - var keys = this.keys; - var newMap = new Mappy(); - for (var i = 0; i < keys.length; i++) { - var key = keys[i]; - var value = this.get(key); - newMap.set(key, value); - } - return newMap; - }; - - return Mappy; - -})(); diff --git a/src/lib/Renderer.js b/src/lib/Renderer.js index bd05c8d6f..100f0c12d 100644 --- a/src/lib/Renderer.js +++ b/src/lib/Renderer.js @@ -460,7 +460,7 @@ var Renderer = Base.extend('Renderer', { var isMaxX = this.isLastColumnVisible(); var chop = isMaxX ? 2 : 1; var colWall = this.getColumnEdges()[this.getColumnEdges().length - chop]; - var result = Math.min(colWall, this.getBounds().width - 200); + var result = Math.min(colWall, this.getBounds().width); return result; }, @@ -1019,7 +1019,6 @@ var Renderer = Base.extend('Renderer', { } else { cellProperties.value = [images.checkbox(isRowSelected), rowNum, null]; } - cellProperties.halign = 'right'; } else { // set dataRow and columnName used by valOrFunc (needed when func) var column = behavior.getActiveColumn(c); @@ -1028,9 +1027,9 @@ var Renderer = Base.extend('Renderer', { cellProperties.calculator = column.calculator; cellProperties.value = grid.getValue(c, r); - cellProperties.halign = grid.getColumnAlignment(c); } + cellProperties.halign = grid.getColumnAlignment(c); cellProperties.isGridColumn = isGridColumn; cellProperties.isGridRow = isGridRow; cellProperties.isColumnHovered = grid.isColumnHovered(c) && isGridColumn; diff --git a/src/lib/SelectionModel.js b/src/lib/SelectionModel.js index c04fe257c..4baef113f 100644 --- a/src/lib/SelectionModel.js +++ b/src/lib/SelectionModel.js @@ -12,6 +12,14 @@ function SelectionModel(grid) { this.grid = grid; + /** + * @name multipleSelections + * @type {boolean} + * @summary Can select multiple cell regions. + * @memberOf SelectionModel.prototype + */ + this.multipleSelections = grid[grid.behavior ? 'getProperties' : '_getProperties']().multipleSelections; + /** * @name selections * @type {Rectangle[]} @@ -108,19 +116,31 @@ SelectionModel.prototype = { */ select: function(ox, oy, ex, ey, silent) { var newSelection = this.grid.newRectangle(ox, oy, ex, ey); - newSelection.firstSelectedCell = this.grid.newPoint(ox, oy); //Cache the first selected cell before it gets normalized to top-left origin + + //Cache the first selected cell before it gets normalized to top-left origin + newSelection.firstSelectedCell = this.grid.newPoint(ox, oy); + newSelection.lastSelectedCell = ( - (newSelection.firstSelectedCell.x === newSelection.origin.x && newSelection.firstSelectedCell.y === newSelection.origin.y) - ? - newSelection.corner - : - newSelection.origin - ); - this.selections.push(newSelection); - this.flattenedX.push(newSelection.flattenXAt(0)); - this.flattenedY.push(newSelection.flattenYAt(0)); + newSelection.firstSelectedCell.x === newSelection.origin.x && + newSelection.firstSelectedCell.y === newSelection.origin.y + ) + ? newSelection.corner + : newSelection.origin; + + if (this.multipleSelections) { + this.selections.push(newSelection); + this.flattenedX.push(newSelection.flattenXAt(0)); + this.flattenedY.push(newSelection.flattenYAt(0)); + } else { + this.selections[0] = newSelection; + this.flattenedX[0] = newSelection.flattenXAt(0); + this.flattenedY[0] = newSelection.flattenYAt(0); + } this.setLastSelectionType('cell'); - if (!silent) {this.grid.selectionChanged();} + + if (!silent) { + this.grid.selectionChanged(); + } }, /** @@ -392,6 +412,12 @@ SelectionModel.prototype = { * @param y2 */ deselectRow: function(y1, y2) { + if (this.areAllRowsSelected()) { + // To deselect a row, we must first remove the all rows flag... + this.setAllRowsSelected(false); + // ...and create a single range representing all rows + this.rowSelectionModel.select(this.grid.getHeaderRowCount(), this.grid.getRowCount() - 1); + } this.rowSelectionModel.deselect(y1, y2); this.setLastSelectionType('row'); }, diff --git a/src/lib/snippits.js b/src/lib/snippits.js deleted file mode 100644 index ac0f7e8d5..000000000 --- a/src/lib/snippits.js +++ /dev/null @@ -1,23 +0,0 @@ -'use strict'; - -// `deepClone` is not in use and has not been tested - -exports.deepClone = function deepClone(p) { - var result; - - if (typeof p !== 'object') { - result = p; - } else if (p instanceof Array) { - result = p.reduce(function(memo, value) { - memo.push(deepClone(value)); - return memo; - }, []); - } else { - result = Object.getOwnPropertyNames(p).reduce(function(memo, key) { - memo[key] = deepClone(p[key]); - return memo; - }, {}); - } - - return result; -}; diff --git a/test/Base-test.js b/test/Base-test.js new file mode 100644 index 000000000..107539034 --- /dev/null +++ b/test/Base-test.js @@ -0,0 +1,60 @@ +'use strict'; + +var expect = require('chai').expect; + +var instance = [{ + name:'extend', + type: 'function' +}]; + +describe('Base', function(){ + describe('Module expected shape', function(){ + + it('Should have the instance shape', function(){ + var base = require('../src/lib/Base'); + + instance.forEach(function(key){ + expect(typeof(base[key.name])).to.equal(key.type); + }); + }); + }); + + describe('extend', function(){ + it('Should create a new constructor', function(){ + var base = require('../src/lib/Base'); + var myConstructor = base.extend('myConstructor', {a:'a'}); + + expect(typeof(myConstructor)).to.equal('function'); + }); + + it('Should extend with HypergridError', function() { + var base = require('../src/lib/Base'); + var MyConstructor = base.extend('MyConstructor', {a:'a'}); + var myObject = new MyConstructor(); + + expect(typeof(myObject.HypergridError)).to.equal('function'); + }); + + it('Should extend with deprecated', function(){ + var base = require('../src/lib/Base'); + var MyConstructor = base.extend('MyConstructor', {a:'a'}); + var myObject = new MyConstructor(); + + expect(typeof(myObject.deprecated)).to.not.be.an('undefined'); + }); + }); + + describe('HypergridError', function(){ + it('should assign the message', function(){ + var base = require('../src/lib/Base'); + var message = 'this is the message'; + var MyConstructor = base.extend('MyConstructor', {a:'a'}); + var myObject = new MyConstructor(); + var myError = new myObject.HypergridError(message); + + expect(myError.message).to.equal(message); + + }); + }); + +}); diff --git a/test/SelectionModel-tests.js b/test/SelectionModel-tests.js new file mode 100644 index 000000000..b37f55e3e --- /dev/null +++ b/test/SelectionModel-tests.js @@ -0,0 +1,94 @@ +'use strict'; + +var expect = require('chai').expect; + +var instance = [ + { + name:'grid', + type: 'object' + }, + { + name:'selections', + type: 'object' + }, + { + name:'flattenedX', + type: 'object' + }, + { + name:'flattenedY', + type: 'object' + }, + { + name:'rowSelectionModel', + type: 'object' + }, + { + name:'columnSelectionModel', + type: 'object' + }, + { + name:'lastSelectionType', + type: 'string' + }, + {name:'allRowsSelected', type:'boolean'}, + {name:'areAllRowsSelected', type:'function'}, + {name:'clear', type:'function'}, + {name:'clearMostRecentColumnSelection', type:'function'}, + {name:'clearMostRecentRowSelection', type:'function'}, + {name:'clearMostRecentSelection', type:'function'}, + {name:'clearRowSelection', type:'function'}, + {name:'deselectColumn', type:'function'}, + {name:'deselectRow', type:'function'}, + {name:'getFlattenedYs', type:'function'}, + {name:'getLastSelection', type:'function'}, + {name:'getLastSelectionType', type:'function'}, + {name:'getSelectedColumns', type:'function'}, + {name:'getSelectedRows', type:'function'}, + {name:'getSelections', type:'function'}, + {name:'hasColumnSelections', type:'function'}, + {name:'hasRowSelections', type:'function'}, + {name:'hasSelections', type:'function'}, + {name:'isCellSelected', type:'function'}, + {name:'isCellSelectedInColumn', type:'function'}, + {name:'isCellSelectedInRow', type:'function'}, + {name:'isColumnOrRowSelected', type:'function'}, + {name:'isColumnSelected', type:'function'}, + {name:'isInCurrentSelectionRectangle', type:'function'}, + {name:'isRectangleSelected', type:'function'}, + {name:'isRowSelected', type:'function'}, + {name:'isSelected', type:'function'}, + {name:'rectangleContains', type:'function'}, + {name:'select', type:'function'}, + {name:'selectAllRows', type:'function'}, + {name:'selectColumn', type:'function'}, + {name:'selectColumnsFromCells', type:'function'}, + {name:'selectRow', type:'function'}, + {name:'selectRowsFromCells', type:'function'}, + {name:'setAllRowsSelected', type:'function'}, + {name:'setLastSelectionType', type:'function'}, + {name:'toggleSelect', type:'function'} +]; + +//TODO: sinon mock object perhaps... +function mockGrid(){ + return { + _getProperties: function() { return {}; } + }; +} + +describe('SelectionModel', function(){ + + describe('Module expected shape', function(){ + it('Should have the instance shape', function(){ + var SelectionModel = require('../src/lib/SelectionModel'); + var myMockGrid = mockGrid(); + var selectionModel = new SelectionModel(myMockGrid); + + instance.forEach(function(key){ + expect(typeof(selectionModel[key.name])).to.equal(key.type); + }); + }); + }); + +}); diff --git a/test/css-test.js b/test/css-test.js new file mode 100644 index 000000000..08b5a890a --- /dev/null +++ b/test/css-test.js @@ -0,0 +1,36 @@ +'use strict'; + +var expect = require('chai').expect; + +var instance = [ + { + name:'grid', + type:'string' + }, + { + name:'list-dragon-addendum', + type:'string' + }, +]; + +describe('CSS', function(){ + describe('Module expected shape', function() { + + it('Should have theinstance shape', function() { + var css = require('../css'); + + instance.forEach(function(key){ + expect(typeof(css[key.name])).to.equal(key.type); + }); + }); + + it('Should not be empty', function() { + var css = require('../css'); + + instance.forEach(function(key){ + expect(css[key.name].trim()).to.not.equal(''); + }); + }); + + }); +}); diff --git a/test/html-test.js b/test/html-test.js new file mode 100644 index 000000000..1c95f9dd7 --- /dev/null +++ b/test/html-test.js @@ -0,0 +1,44 @@ +'use strict'; + +var expect = require('chai').expect; + +var instance = [ + { + name:'CQL', + type:'string' + }, + { + name:'SQL', + type:'string' + }, + { + name:'dialog', + type:'string' + }, + { + name:'filterTrees', + type:'string' + } +]; +describe('HTML', function() { + + describe('Module expected shape', function(){ + it('Should have strings keys', function(){ + var html = require('../html'); + + instance.forEach(function(key){ + expect(typeof(html[key.name])).to.equal(key.type); + }); + }); + + it('Should not be empty', function() { + var html = require('../html'); + + instance.forEach(function(key){ + expect(html[key.name].trim()).to.not.equal(''); + }); + }); + }); + + +}); diff --git a/test/image-test.js b/test/image-test.js new file mode 100644 index 000000000..0db06d11e --- /dev/null +++ b/test/image-test.js @@ -0,0 +1,110 @@ +'use strict'; + +var expect = require('chai').expect; + +var instance = [ + { + name:'calendar', + type:'object' + }, + { + name:'checked', + type:'object' + }, + { + name:'dialog', + type:'object' + }, + { + name:'down-rectangle', + type:'object' + }, + { + name:'filter-off', + type:'object' + }, + { + name:'filter-on', + type:'object' + }, + { + name:'unchecked', + type:'object' + }, + { + name:'up-down-spin', + type:'object' + }, + { + name:'up-down', + type:'object' + }, + { + name:'checkbox', + type:'function' + }, + { + name:'filter', + type:'function' + } +]; + +describe('Image', function() { + + beforeEach(function() { + global.Image = function(){ + return {}; +}; + }); + + afterEach(function() { + delete global.image; + }); + + describe('Module expected shape', function() { + + it('Should have the instance shape', function() { + var images = require('../images'); + instance.forEach(function(key) { + expect(typeof(images[key.name])).to.equal(key.type); + }); +}); + + it('Image Objects have src properties', function(){ + var images = require('../images'); + instance.forEach(function(key) { + if (typeof(images[key.name]) === 'object') { + expect(typeof(images[key.name].src)).to.equal('string'); +} + }); +}); + }); + + describe('checkbox', function() { + it('Should return the checked image', function() { + var images = require('../images'); + var image = images.checkbox(true); + expect(image).to.equal(images.checked); +}); + + it('Should return the unchecked image', function() { + var images = require('../images'); + var image = images.checkbox(false); + expect(image).to.equal(images.unchecked); +}); + }); + + describe('filter', function() { + it('Should return the filter-on image', function() { + var images = require('../images'); + var image = images.filter(true); + expect(image).to.equal(images['filter-on']); + }); + + it('Should return the unchecked image', function() { + var images = require('../images'); + var image = images.filter(false); + expect(image).to.equal(images['filter-off']); + }); + }); +}); diff --git a/version-history.md b/version-history.md index 55986e313..9fbb321f8 100644 --- a/version-history.md +++ b/version-history.md @@ -1,9 +1,42 @@ +### 1.0.9 - 29 August 2016 + +* Restored Safari support +* Context Menu events no longer propagates +* Added `[halign](http://openfin.github.io/fin-hypergrid/doc/module-defaults.html#halign)` render property. +* Fixed: Vertical scrollbar is no longer misplaced 200 pixels to the left when grid overflows canvas's container width. +* Grid's container will default to a height of 300px and css relative positioning unless those attributes are set +* Selection model + * Added `[multipleSelections](http://openfin.github.io/fin-hypergrid/doc/module-defaults.html#multipleSelections)`, a new grid property that defaults to `false`. Set it to `true` to "opt in" to get the old behavior wherein CTRL-click(-drag) selects additional cell regions. These multiple regions are nearly useless. (The application developer can programmatically inspect the selection model to see all such selections, the user can only COPY the most recently selected region.) + * Sample code: Added a new dashboard checkbox _Selection: one cell region at a time_ which (when _one row at a time_ is also checked) causes the cell selection to travel with the row selection. See the code in `fin-row-selection-changed` event listener in demo.js. + * Fixed demo: The _Selection: one row at a time_ dashboard checkbox now initializes properly. + * Fixed: User can now _un_check individual row selection checkboxes after clicking on the _select all_ checkbox at the top of the row handle column. +* Tree view + * Fixed: Error was previously being thrown when column count was greater than row count. + * Improved sorting: Now only sorts on _expandable rows_ (rows with drill-down controls), leaving non-expandable rows (leaf node rows) _stable sorted_ (_i.e.,_ they retain their sort positions from the previous sorter). + * Improved default sort: When no column has an explicit sort, the group sorter is automatically applied, _e.g.,_ to the ID column which is usually hidden. UI no longer insists on a visible column sort; this functionality is now completely transparent to the user. +* Filtering: Filter cell syntax a.k.a. Column Query Language (CQL) + * Fixed: Error reported upon encountering operators consisting of consecutive non alpha chars, _e.g.,_ `<=`, `>=`, and `<>` (all of which incidentally have Unicode equivalents in CQL, `≤`, `≥`, and `≠`.) + * Added `[opMustBeInMenu](http://openfin.github.io/fin-hypergrid/doc/FilterNode.html#opMustBeInMenu)` column schema option. When `true`, rejects manually entered valid operators not specifically in column's operator menu. Added usage example to demo.js on `last_name` column, which had a custom operator menu. + * Fixed CQL syntax support: Certain error conditions were causing premature alerts. Now fails "gracefully." + * Fixed SQL syntax support: + * SQL parser + * Now accepts unquoted numeric operands + * Now accepts column name or alias as operand + * SQL output + * Now outputs column operands (restoring broken functionality) +* [Find Row API](http://openfin.github.io/fin-hypergrid/doc/rowById.html) plug-in: Find/modify/replace/delete a matching data row object. +* Added `[dataModel.addRow()](http://openfin.github.io/fin-hypergrid/doc/dataModels.JSON.html#addRow)` method to add a new data row to the grid. +* Internal change: Replaced use of `KeyboardEvent.keyIdentifier` in favor of `KeyboardEvent.key` because the former will be dropped in _Chromium M53_ due out in September. `KeyboardEvent.key` is also supported in IE 9 and FF 23. It is not however a perfect replacement. See comment under [1.4.0](https://github.com/openfin/fincanvas/blob/master/README.md) for more information. +* Added grid property 'enableContinuousRepaint' (boolean). This is a _dynamic_ property and can be set or cleared at any time. When set: + * Repaint occurs continuously (without have to call `grid.repaint()`). + * `grid.getCanvas().currentFPS` is a measure of the number times the grid is being re-rendered each second. + ### 1.0.8 - 8 August 2016 * Wrapped column headers no longer overflow bottom of cell. Overflow is clipped. * Clicking to right of last column no longer throws an error. * Zooming out (e.g., 80%) now properly clears the grid before repainting. -* Tree-view add-on improvements: +* Tree-view plug-in improvements: * *Now sorts properly* and maintains a sorted state: * On any column sort, applies a _group sorter_ which sorts the column as usual but then automatically stable-sorts each level of group starting with deepest and working up to the top level. (_Stable sorting_ from lowest to highest level grouping is an efficient means of sorting nested data, equivalent to starting with the highest groups and recursing down the tree to each lowest group.) * Because raw data order is assumed to be undefined _and_ grouping structure requires that groups be sorted, automatically applies an initial _default sort_ to the "tree" column (name or index specified in `treeColumn` option passed to `TreeView` constructor; defaults to `'name'`). If a default sort column is defined (name or index specified in `defaultSortColumn` option; defaults to the tree column), the initial sort is applied to that column instead. @@ -17,7 +50,7 @@ * Make the blank column (now the left-most column) a fixed column (via the grid's `fixedColumnCount` property). * Specify some other column for the initial sort (in `option.defaultSortColumn`). This will typically be the column that identifies the group. * Demo: [`tree-view-separate-drill-down.html`](http://openfin.github.io/fin-hypergrid/tree-view-separate-drill-down.html) -* *Grouped column headers* add-on (`add-ons/grouped-columns.js`): +* *Grouped column headers* plug-in (`add-ons/grouped-columns.js`): * Include: `` * Install: `fin.Hypergrid.groupedHeader.mixInTo(grid)` * Usage, for example: `grid.behavior.setHeaders({ lat: 'Coords|Lat.', long: 'Coords|Long.' })` @@ -29,8 +62,8 @@ * If the cell value is a function, however, legacy behavior is maintained: This function takes priority over the column function. * Upon selecting a new operator from a column filter cell's dropdown, rather than inserting the new operator at the cursor position, the old operator is now _replaced_ by the new one the operator. If the column filter cell contains several expressions (_i.e.,_ concatenated with `and`, `or`, or `nor`), the operator in the expression under the cursor is replaced. * Group view - * Aggregations and Group View have been added as add-ons and removed from HyperGrid core. - * The aggregations add-on has the same behavior as before while the Group View is a view of the original columns, with drill downs in the tree cell for expanding the groups provided. + * Aggregations and Group View have been added as plug-ins and removed from HyperGrid core. + * The aggregations plug-in has the same behavior as before while the Group View is a view of the original columns, with drill downs in the tree cell for expanding the groups provided. * Hypergrid now only loads with the original data source, the filter datasource as defaults, and the sorter data source. * Demo 1: [`aggregations.html`](http://openfin.github.io/fin-hypergrid/aggregations-view.html) * See Group Demo for example usage: [`group.html`](http://openfin.github.io/fin-hypergrid/group-view.html).