diff --git a/README.md b/README.md index 0b158e3f5..477e955c3 100644 --- a/README.md +++ b/README.md @@ -1,7 +1,7 @@ **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. Please be sure to checkout our [design overview](OVERVIEW.md) Below is an example custom application built on top of the Hypergrid API tooling. -It also highlights a DOM-based custom external editor triggered via hypergrid events as well as interaction with Hypergrid's column ordering API +It also highlights a DOM-based custom external editor triggered via hypergrid events as well as interaction with Hypergrid’s column ordering API. @@ -15,9 +15,9 @@ It also highlights a DOM-based custom external editor triggered via hypergrid ev * [Roadmap](#roadmap) * [Contributing](#contributors) -### Current Release (3.0.3 - 25 September 2018) +### Current Release (3.1.0 - 29 September 2018) -**Hypergrid 3.0 includes a revised data model with some breaking changes.** +**Hypergrid 3.1 includes 3.0’s revised data model with some breaking changes.** _For a complete list of changes, see the [release notes](https://github.com/fin-hypergrid/core/releases)._ @@ -25,7 +25,7 @@ _For a complete list of changes, see the [release notes](https://github.com/fin- #### npm module _(recommended)_ Published as a CommonJS module to npmjs.org. -Specify a SEMVER of `"fin-hypergrid": "3.0.3"` (or `"^3.0.3"`) in your package.json file, +Specify a SEMVER of `"fin-hypergrid": "3.1.0"` (or `"^3.1.0"`) in your package.json file, issue the `npm install` command, and let your bundler (wepback, Browserify) create a single file containing both Hypergrid and your application. @@ -69,7 +69,7 @@ The [Perspective](https://github.com/jpmorganchase/perspective) open source proj ##### AdaptableBlotter.JS -[Openfin](http://openfin.co)'s AdaptableBlotter.JS ([installer](https://install.openfin.co/download/?fileName=adaptable_blotter_openfin&config=http://beta.adaptableblotter.com/app-beta.json)) is a demo app that shows the capabilities of both Openfin and Hypergrid. +[Openfin](http://openfin.co)’s AdaptableBlotter.JS ([installer](https://install.openfin.co/download/?fileName=adaptable_blotter_openfin&config=http://beta.adaptableblotter.com/app-beta.json)) is a demo app that shows the capabilities of both Openfin and Hypergrid. ![](images/README/partner-adaptableblotter_image-01@2x-667x375@2x.png) diff --git a/package.json b/package.json index 74b5037f2..0d8f059cf 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "fin-hypergrid", - "version": "3.0.3", + "version": "3.1.0", "description": "Canvas-based high-performance grid", "main": "src/Hypergrid", "repository": { @@ -25,7 +25,7 @@ "datasaur-base": "^3.0.0", "datasaur-local": "^3.0.0", "extend-me": "^2.7.0", - "finbars": "^1.6.0", + "finbars": "^2.0.0", "inject-stylesheet-template": "^1.0.1", "mustache": "^2.3.0", "object-iterators": "1.3.0", diff --git a/src/Hypergrid/index.js b/src/Hypergrid/index.js index 5e19c3869..9b126b54e 100644 --- a/src/Hypergrid/index.js +++ b/src/Hypergrid/index.js @@ -34,31 +34,6 @@ var EDGE_STYLES = ['top', 'bottom', 'left', 'right'], * @constructor * @classdesc An object representing a Hypergrid. * @desc The first parameter, `container`, is optional. If omitted, the `options` parameter is promoted to first position. (Note that the container can also be given in `options.container.`) - * #### `options.canvasContextAttributes` object (see below) - * The only currently meaningful property of this object is `alpha`: - * - * ```js - * var gridOptions = { - * canvasContextAttributes: { alpha: false } - * }; - * var myGrid = new Hypergrid(gridOptions); - * ``` - * - * `alpha` is a boolean that indicates if the canvas contains an alpha channel. If set to `false`, the browser now knows that the backdrop is always opaque, which can speed up drawing of transparent content and images. - * - * This option was added by request although testing failed to show any measurable performance benefit. - * - * Use with caution. In particular, if the canvas is set to "opaque" (`{alpha: false}`), do _not_ also specify a transparent or translucent color for `grid.properties.backGround` because content may then be drawn with corrupt anti-aliasing (at lest in Chrome v67). - * - * Note that such an "opaque" canvas can still be made to appear translucent using the CSS `opacity` property — a different effect entirely. - * - * Although this option has no apparent performance gains (in Chrome v63), it does permit the graphics context to use [sub-pixel rendering](https://en.wikipedia.org/wiki/Subpixel_rendering) for sharper text as viewed on LCD or LED screens, especially black text on white backgrounds, and especially when viewed on a high-pixel-density display such as an [Apple retina display](https://en.wikipedia.org/wiki/Retina_Display). - * - * value | Canvas | Text | Sample - * ----- | :----: | :--: | ------ - * `{ alpha: true } ` | transparent | regular
anti-aliasing | ![regular.png](https://cdn-pro.dprcdn.net/files/acc_645730/ZqurK3) - * `{ alpha: false }` | opaque | sub-pixel
rendering | ![sub-pixel.png](https://cdn-std.dprcdn.net/files/acc_645730/bf3VXh) - * * @param {string|Element} [container] - CSS selector or Element. If omitted (and `options.container` also omitted), Hypergrid first looks for an _empty_ element with an ID of `hypergrid`. If not found, it will create a new element. In either case, the container element has the class name `hypergrid-container` added to its class name list. Finally, if the there is more than one such element with that class name, the element's ID attribute is set to `hypergrid` + _n_ where n is an ordinal one less than the number of such elements. * @param {object} [options] - If `options.data` provided, passed to {@link Hypergrid#setData setData}; else if `options.Behavior` provided, passed to {@link Hypergrid#setBehavior setBehavior}. * @param {function} [options.Behavior=Local] - _Per {@link Behavior#setData}._ @@ -73,8 +48,22 @@ var EDGE_STYLES = ['top', 'bottom', 'left', 'right'], * * @param {string|Element} [options.container] - Alternative to providing `container` (first) parameter above. * - * @param {object} [options.canvasContextAttributes] - Passed to [`HTMLCanvasElement.getContext`](https://developer.mozilla.org/docs/Web/API/HTMLCanvasElement/getContext). _Please see discussion above._ + * @param {object} [options.contextAttributes={ alpha: true }] - Passed to [`HTMLCanvasElement.getContext`](https://developer.mozilla.org/docs/Web/API/HTMLCanvasElement/getContext). Although the MDN docs say setting this to `{alpha: false}` (opaque canvas) can "can speed up drawing of transparent content and images," our testing (with Chrome v63) failed to show any measurable performance gain. + * + * _An opaque canvas does have an important advantage, however!_ It permits the graphics context to use [sub-pixel rendering](https://en.wikipedia.org/wiki/Subpixel_rendering) for sharper text as viewed on LCD or LED screens, especially black text on white backgrounds, and especially when viewed on a high-pixel-density display such as an [Apple retina display](https://en.wikipedia.org/wiki/Retina_Display). + * + * Zoom in on the following samples images to see the difference in rendering. + * + * Value | Sample + * :---: | :----: + * `{ alpha: true }`
Transparent canvas,
renders text using
_regular anti-aliasing_ | ![regular.png](https://cdn-pro.dprcdn.net/files/acc_645730/ZqurK3) + * `{ alpha: false }`
Opaque canvas,
renders text using
_sub-pixel rendering_ | ![sub-pixel.png](https://cdn-std.dprcdn.net/files/acc_645730/bf3VXh). + * + * Use with caution, however. In particular, if the canvas is set to "opaque" (`{alpha: false}`), do _not_ also specify a transparent or translucent color for `grid.properties.backGround` because content may then be drawn with corrupt anti-aliasing (at lest as of Chrome v67). + * + * To clarify, the default setting (`{ alpha: true }`) is a transparent canvas, meaning that elements rendered underneath the `` element can be seen through any non-opaque pixels (pixels with alpha channel < 1.0). Hypergrids that set their background color to non-opaque can see this effect. * + * Note: An opaque canvas can still be made _to appear_ translucent using the CSS `opacity` property. But that is a different effect entirely, setting the entire rendered canvas to translucent, not just so all pixels become translucent. * @param {string} [options.localization=Hypergrid.localization] * @param {string|string[]} [options.localization.locale=Hypergrid.localization.locale] - The default locale to use when an explicit `locale` is omitted from localizer constructor calls. Passed to `Intl.NumberFomrat` and `Intl.DateFomrat`. See {@link https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/Intl#Locale_identification_and_negotiation|Locale identification and negotiation} for more information. * @param {string} [options.localization.numberOptions=Hypergrid.localization.numberOptions] - Options passed to [`Intl.NumberFormat`](https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/NumberFormat) for creating the basic "number" localizer. @@ -1048,7 +1037,11 @@ var Hypergrid = Base.extend('Hypergrid', { this.div.appendChild(divCanvas); - var canvas = new Canvas(divCanvas, this.renderer, options && options.contextAttributes); + var contextAttributes = options && ( + options.contextAttributes || + options.canvasContextAttributes + ); + var canvas = new Canvas(divCanvas, this.renderer, contextAttributes); canvas.canvas.classList.add('hypergrid'); this.divCanvas = divCanvas; diff --git a/src/Hypergrid/modules.js b/src/Hypergrid/modules.js index a9ca1a35d..348afa40e 100644 --- a/src/Hypergrid/modules.js +++ b/src/Hypergrid/modules.js @@ -26,6 +26,7 @@ Object.defineProperties(module.exports, { 'datasaur-base': { value: require('datasaur-base') }, // may be removed in a future release 'datasaur-local': { value: require('datasaur-local') }, // may be removed in a future release 'extend-me': {value: require('extend-me') }, + finbars: { value: require('finbars') }, 'object-iterators': { value: require('object-iterators') }, overrider: { value: require('overrider') }, rectangular: { value: require('rectangular') }, diff --git a/src/behaviors/Column.js b/src/behaviors/Column.js index 039ea4d97..76fbe3267 100644 --- a/src/behaviors/Column.js +++ b/src/behaviors/Column.js @@ -188,7 +188,7 @@ Column.prototype = { }, setWidth: function(width) { - width = Math.max(this.properties.minimumColumnWidth, width); + width = Math.min(Math.max(this.properties.minimumColumnWidth, width), this.properties.maximumColumnWidth || Infinity); if (width !== this.properties.width) { this.properties.width = width; this.properties.columnAutosizing = false; diff --git a/src/defaults.js b/src/defaults.js index 647a68eb4..d8da405fa 100644 --- a/src/defaults.js +++ b/src/defaults.js @@ -748,6 +748,8 @@ var defaults = { defaultRowHeight: version > 2 ? 14 : 15, /** + * This default column width is used when `width` property is undefined. + * (`width` is defined on column creation unless {@link module:defaults.columnAutosizing columnAutosizing} has been set to `false`.) * @default * @type {number} * @memberOf module:defaults @@ -755,12 +757,28 @@ var defaults = { defaultColumnWidth: 100, /** + * Minimum column width. + * Adjust this value for different fonts/sizes or exotic cell renderers. + * _Must be defined._ + * The default (`5`) is enough room for an ellipsis with default font size. * @default * @type {number} * @memberOf module:defaults */ minimumColumnWidth: 5, + /** + * Maximum column width. + * _When defined,_ column width is clamped to this value by {@link Column#setWidth setWidth}). + * Ignored when falsy. + * Respects {@link module:defaults.resizeColumnInPlace resizeColumnInPlace} but may cause user confusion when + * user can't make column narrower due to next column having reached its maximum. + * @default + * @type {number} + * @memberOf module:defaults + */ + maximumColumnWidth: undefined, + /** * Resizing a column through the UI (by clicking and dragging on the column's * right border in the column header row) normally affects the width of the whole grid. @@ -769,12 +787,12 @@ var defaults = { * In other words, if user expands (say) the third column, then the fourth column will contract — * and _vice versa_ — without therefore affecting the width of the grid. * - * This is a _column propert_ and may be set for selected columns (`myColumn.properties.resizeColumnInPlace`) - * or for all columns by setting it at the grid level. (`myGrid.properties.resizeColumnInPlace`). + * This is a _column property_ and may be set for selected columns (`myColumn.properties.resizeColumnInPlace`) + * or for all columns by setting it at the grid level (`myGrid.properties.resizeColumnInPlace`). * * Note that the implementation of this property does not allow expanding a * column beyond the width it can borrow from the next column. - * The last column, however, is unconstrained and resizing it will affect the total grid width. + * The last column, however, is unconstrained, and resizing of course affects the total grid width. * @default * @type {boolean} * @memberOf module:defaults diff --git a/src/features/ColumnResizing.js b/src/features/ColumnResizing.js index 74a8e96c7..e336407cb 100644 --- a/src/features/ColumnResizing.js +++ b/src/features/ColumnResizing.js @@ -63,17 +63,23 @@ var ColumnResizing = Feature.extend('ColumnResizing', { */ handleMouseDrag: function(grid, event) { if (this.dragColumn) { - var delta = this.getMouseValue(event) - this.dragStart; - var dragWidth = this.dragStartWidth + delta; - if (!this.nextColumn) { + var delta = this.getMouseValue(event) - this.dragStart, + dragWidth = this.dragStartWidth + delta, + nextWidth = this.nextStartWidth - delta; + if (!this.nextColumn) { // nextColumn et al instance vars defined when resizeColumnInPlace (by handleMouseDown) grid.behavior.setColumnWidth(this.dragColumn, dragWidth); - } else if ( - 0 < delta && delta <= (this.nextStartWidth - this.nextColumn.properties.minimumColumnWidth) || - 0 > delta && delta >= -(this.dragStartWidth - this.dragColumn.properties.minimumColumnWidth) - ) { - var nextWidth = this.nextStartWidth - delta; - grid.behavior.setColumnWidth(this.dragColumn, dragWidth); - grid.behavior.setColumnWidth(this.nextColumn, nextWidth); + } else { + var np = this.nextColumn.properties, dp = this.dragColumn.properties; + if ( + 0 < delta && delta <= (this.nextStartWidth - np.minimumColumnWidth) && + (!dp.maximumColumnWidth || dragWidth <= dp.maximumColumnWidth) + || + 0 > delta && delta >= -(this.dragStartWidth - dp.minimumColumnWidth) && + (!np.maximumColumnWidth || nextWidth < np.maximumColumnWidth) + ) { + grid.behavior.setColumnWidth(this.dragColumn, dragWidth); + grid.behavior.setColumnWidth(this.nextColumn, nextWidth); + } } } else if (this.next) { this.next.handleMouseDrag(grid, event); diff --git a/src/lib/Canvas.js b/src/lib/Canvas.js index ac42752ea..830172307 100644 --- a/src/lib/Canvas.js +++ b/src/lib/Canvas.js @@ -335,7 +335,7 @@ Canvas.prototype = { dispatchNewMouseKeysEvent: function(event, name, detail) { detail = detail || {}; detail.mouse = this.mouseLocation; - detail.keys = this.currentKeys; + defKeysProp.call(this, event, 'keys', detail); return this.dispatchNewEvent(event, name, detail); }, @@ -452,12 +452,8 @@ Canvas.prototype = { return; } - // prevent TAB from moving focus off the canvas element - if (e.keyCode === 9) { - e.preventDefault(); - } + var keyChar = updateCurrentKeys.call(this, e, true); - var keyChar = this.getKeyChar(e); if (e.repeat) { if (this.repeatKey === keyChar) { this.repeatKeyCount++; @@ -470,11 +466,8 @@ Canvas.prototype = { this.repeatKeyCount = 0; this.repeatKeyStartTime = 0; } - if (this.currentKeys.indexOf(keyChar) === -1) { - this.currentKeys.push(keyChar); - } - this.dispatchNewEvent(e, 'fin-canvas-keydown', { + this.dispatchNewEvent(e, 'fin-canvas-keydown', defKeysProp.call(this, e, 'currentKeys', { alt: e.altKey, ctrl: e.ctrlKey, char: keyChar, @@ -485,9 +478,8 @@ Canvas.prototype = { repeatCount: this.repeatKeyCount, repeatStartTime: this.repeatKeyStartTime, shift: e.shiftKey, - identifier: e.key, - currentKeys: this.currentKeys.slice(0) - }); + identifier: e.key + })); }, finkeyup: function(e) { @@ -495,17 +487,12 @@ Canvas.prototype = { return; } - // prevent TAB from moving focus off the canvas element - if (e.keyCode === 9) { - e.preventDefault(); - } + var keyChar = updateCurrentKeys.call(this, e, false); - var keyChar = this.getKeyChar(e); - this.currentKeys.splice(this.currentKeys.indexOf(keyChar), 1); this.repeatKeyCount = 0; this.repeatKey = null; this.repeatKeyStartTime = 0; - this.dispatchNewEvent(e, 'fin-canvas-keyup', { + this.dispatchNewEvent(e, 'fin-canvas-keyup', defKeysProp.call(this, e, 'currentKeys', { alt: e.altKey, ctrl: e.ctrlKey, char: keyChar, @@ -517,7 +504,7 @@ Canvas.prototype = { shift: e.shiftKey, identifier: e.key, currentKeys: this.currentKeys.slice(0) - }); + })); }, finfocusgained: function(e) { @@ -791,6 +778,72 @@ function makeCharMap() { return map; } +function updateCurrentKeys(e, keydown) { + var keyChar = this.getKeyChar(e); + + // prevent TAB from moving focus off the canvas element + switch (keyChar) { + case 'TAB': + case 'TABSHIFT': + case 'Tab': + e.preventDefault(); + } + + fixCurrentKeys.call(this, keyChar, keydown); + + return keyChar; +} + +function fixCurrentKeys(keyChar, keydown) { + var index = this.currentKeys.indexOf(keyChar); + + if (!keydown && index >= 0) { + this.currentKeys.splice(index, 1); + } + + if (keyChar === 'SHIFT') { + // on keydown, replace unshifted keys with shifted keys + // on keyup, vice-versa + this.currentKeys.forEach(function(key, index, currentKeys) { + var pair = charMap.find(function(pair) { + return pair[keydown ? 0 : 1] === key; + }); + if (pair) { + currentKeys[index] = pair[keydown ? 1 : 0]; + } + }); + } + + if (keydown && index < 0) { + this.currentKeys.push(keyChar); + } +} + +function defKeysProp(event, propName, object) { + var canvas = this; + Object.defineProperty(object, propName, { + configurable: true, + ennumerable: true, + get: function() { + var shiftKey; + if ('shiftKey' in event) { + fixCurrentKeys.call(canvas, 'SHIFT', shiftKey = event.shiftKey); + } else { + shiftKey = canvas.currentKeys.indexOf('SHIFT') >= 0; + } + var SHIFT = shiftKey ? 'SHIFT' : ''; + if ('ctrlKey' in event) { + fixCurrentKeys.call(canvas, 'CTRL' + SHIFT, event.ctrlKey); + } + if ('altKey' in event) { + fixCurrentKeys.call(canvas, 'ALT' + SHIFT, event.altKey); + } + return canvas.currentKeys.slice(); + } + }); + return object; +} + function getCachedContext(canvasElement, contextAttributes) { var gc = canvasElement.getContext('2d', contextAttributes), props = {}, diff --git a/src/lib/SelectionModel.js b/src/lib/SelectionModel.js index 4e62f79a8..7d24860b0 100644 --- a/src/lib/SelectionModel.js +++ b/src/lib/SelectionModel.js @@ -444,11 +444,10 @@ SelectionModel.prototype = { */ getSelectedRows: function() { if (this.areAllRowsSelected()) { - var headerRows = this.grid.getHeaderRowCount(); - var rowCount = this.grid.getRowCount() - headerRows; + var rowCount = this.grid.getRowCount(); var result = new Array(rowCount); for (var i = 0; i < rowCount; i++) { - result[i] = i + headerRows; + result[i] = i; } return result; } diff --git a/src/renderer/index.js b/src/renderer/index.js index 25d5f5124..fccbfea5a 100644 --- a/src/renderer/index.js +++ b/src/renderer/index.js @@ -613,8 +613,8 @@ var Renderer = Base.extend('Renderer', { return; } - var vci = this.visibleColumnsByIndex, - vri = this.visibleRowsByDataRowIndex, + var vc, vci = this.visibleColumnsByIndex, + vr, vri = this.visibleRowsByDataRowIndex, lastScrollableColumn = this.visibleColumns[this.visibleColumns.length - 1], // last column in scrollable section lastScrollableRow = this.visibleRows[this.visibleRows.length - 1], // last row in scrollable data section firstScrollableColumn = vci[this.dataWindow.origin.x], @@ -625,25 +625,33 @@ var Renderer = Base.extend('Renderer', { if ( // entire selection scrolled out of view to left of visible columns; or - selection.corner.x < this.visibleColumns[0].columnIndex || + (vc = this.visibleColumns[0]) && + selection.corner.x < vc.columnIndex || // entire selection scrolled out of view between fixed columns and scrollable columns; or fixedColumnCount && - selection.origin.x > this.visibleColumns[fixedColumnCount - 1].columnIndex && + firstScrollableColumn && + (vc = this.visibleColumns[fixedColumnCount - 1]) && + selection.origin.x > vc.columnIndex && selection.corner.x < firstScrollableColumn.columnIndex || // entire selection scrolled out of view to right of visible columns; or + lastScrollableColumn && selection.origin.x > lastScrollableColumn.columnIndex || // entire selection scrolled out of view above visible rows; or - selection.corner.y < this.visibleRows[headerRowCount].rowIndex || + (vr = this.visibleRows[headerRowCount]) && + selection.corner.y < vr.rowIndex || // entire selection scrolled out of view between fixed rows and scrollable rows; or fixedRowCount && - selection.origin.y > this.visibleRows[headerRowCount + fixedRowCount - 1].rowIndex && + firstScrollableRow && + (vr = this.visibleRows[headerRowCount + fixedRowCount - 1]) && + selection.origin.y > vr.rowIndex && selection.corner.y < firstScrollableRow.rowIndex || // entire selection scrolled out of view below visible rows + lastScrollableRow && selection.origin.y > lastScrollableRow.rowIndex ) { return; @@ -654,6 +662,10 @@ var Renderer = Base.extend('Renderer', { vcCorner = vci[selection.corner.x] || (selection.corner.x > lastScrollableColumn.columnIndex ? lastScrollableColumn : vci[fixedColumnCount - 1]), vrCorner = vri[selection.corner.y] || (selection.corner.y > lastScrollableRow.rowIndex ? lastScrollableRow : vri[fixedRowCount - 1]); + if (!(vcOrigin && vrOrigin && vcCorner && vrCorner)) { + return; + } + // Render the selection model around the bounds var config = { bounds: {