diff --git a/package.json b/package.json index 32efef17a..c509522a9 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "fin-hypergrid", - "version": "3.0.1", + "version": "3.0.2", "description": "Canvas-based high-performance grid", "main": "src/Hypergrid", "repository": { diff --git a/src/Hypergrid/scrolling.js b/src/Hypergrid/scrolling.js index bb064eb83..acd6368eb 100644 --- a/src/Hypergrid/scrolling.js +++ b/src/Hypergrid/scrolling.js @@ -230,6 +230,7 @@ exports.mixin = { orientation: 'vertical', deltaYFactor: this.constructor.defaults.wheelVFactor, onchange: self.setVScrollValue.bind(self), + cssStylesheetReferenceElement: this.div, paging: { up: self.pageUp.bind(self), down: self.pageDown.bind(self) diff --git a/src/Hypergrid/selection.js b/src/Hypergrid/selection.js index 1d29554b4..ca2682fa0 100644 --- a/src/Hypergrid/selection.js +++ b/src/Hypergrid/selection.js @@ -537,17 +537,18 @@ exports.mixin = { * @memberOf Hypergrid# * @desc Synthesize and dispatch a `fin-selection-changed` event. */ - selectionChanged: function() { + selectionChanged: function(silent) { // Project the cell selection into the rows this.selectRowsFromCells(); // Project the cell selection into the columns this.selectColumnsFromCells(); - var selectionEvent = new CustomEvent('fin-selection-changed', { - detail: this.selectionDetailGetters - }); - this.canvas.dispatchEvent(selectionEvent); + if (!silent) { + this.canvas.dispatchEvent(new CustomEvent('fin-selection-changed', { + detail: this.selectionDetailGetters + })); + } }, isColumnOrRowSelected: function() { diff --git a/src/behaviors/Behavior.js b/src/behaviors/Behavior.js index 71c0a22bc..a01c6addc 100644 --- a/src/behaviors/Behavior.js +++ b/src/behaviors/Behavior.js @@ -349,7 +349,8 @@ var Behavior = Base.extend('Behavior', { * @memberOf Hypergrid# */ setColumnWidth: function(columnOrIndex, width) { - var column = columnOrIndex >= -2 ? this.getActiveColumn(columnOrIndex) : columnOrIndex; + var column = columnOrIndex >= -2 ? // relation operator tests for number (index) vs column (object) + this.getActiveColumn(columnOrIndex) : columnOrIndex; column.setWidth(width); this.stateChanged(); }, diff --git a/src/behaviors/Local/columnEnum.js b/src/behaviors/Local/columnEnum.js index 11ab12771..b26dc11b3 100644 --- a/src/behaviors/Local/columnEnum.js +++ b/src/behaviors/Local/columnEnum.js @@ -9,8 +9,6 @@ var transformers = require('synonomous/transformers'); var warned = {}; -var columnEnumKey = function() {}; - function warnColumnEnumDeprecation(method, msg) { if (!warned[method]) { console.warn('.' + method + ' has been deprecated as of v3.0.0. (Will be removed in a future release.) ' + (msg || '')); @@ -62,14 +60,14 @@ exports.mixin = { if (transformer === 'passThrough') { transformer = 'verbatim'; } else if (!(transformer in transformers)) { - throw new this.HypergridError('Expected registered transformer for .columnEnumKey value from: ' + keys) + throw new this.HypergridError('Expected registered transformer for .columnEnumKey value from: ' + keys); } this._columnEnumKey = transformer; break; case 'function': this._columnEnumKey = keys.find(function(key) { return transformer === transformers[key]; }); if (!this._columnEnumKey) { - throw new this.HypergridError('.columnEnumKey has been deprecated as of v3.0.0 and now accepts a function reference (or string key) from require("synonmous/transformers"): ' + keys) + throw new this.HypergridError('.columnEnumKey has been deprecated as of v3.0.0 and now accepts a function reference (or string key) from require("synonmous/transformers"): ' + keys); } break; default: diff --git a/src/behaviors/Local/index.js b/src/behaviors/Local/index.js index 10dfd42a4..ad773aa4e 100644 --- a/src/behaviors/Local/index.js +++ b/src/behaviors/Local/index.js @@ -4,6 +4,7 @@ var Behavior = require('../Behavior'); /** @memberOf Local~ * @default require('datasaur-local') + * @type {function|function[]} * @summary Default data model. * @desc The default data model for newly instantiated `Hypergrid` objects without `DataModel` or `dataModel` options specified. Scheduled for eventual deprecation at which point one of the options will be required. */ @@ -91,24 +92,24 @@ var Local = Behavior.extend('Local', { * Create a new data model * @param {object} [options] * @param {DataModel} [options.dataModel] - A fully instantiated data model object. - * @param {function} [options.DataModel=require('datasaur-local')] - Data model will be instantiated from this constructor unless `options.dataModel` was given. + * @param {function|function[]} [options.DataModel=DefaultDataModel] - Data model constructor, or array of data model constructors for a multi-stage data model, to be used to instantiate the data model unless a fully instantiated `options.dataModel` was given. * @returns {boolean} `true` if the data model has changed. * @memberOf Local# */ getNewDataModel: function(options) { - var newDataModel; + var dataModel; options = options || {}; if (options.dataModel) { - newDataModel = options.dataModel; - } else if (options.DataModel) { - newDataModel = new options.DataModel; + dataModel = options.dataModel; } else { - newDataModel = new DefaultDataModel; + [].concat(options.DataModel || DefaultDataModel).forEach(function(DataModel) { + dataModel = new DataModel(dataModel); + }); } - return newDataModel; + return dataModel; }, /** diff --git a/src/defaults.js b/src/defaults.js index 328ab2fa6..c799f9992 100644 --- a/src/defaults.js +++ b/src/defaults.js @@ -761,6 +761,26 @@ var defaults = { */ minimumColumnWidth: 5, + /** + * 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. + * + * Set this new property to truthy to inversely resize the next column. + * 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`). + * + * 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. + * @default + * @type {boolean} + * @memberOf module:defaults + */ + resizeColumnInPlace: false, + //for immediate painting, set these values to 0, true respectively /** diff --git a/src/features/CellSelection.js b/src/features/CellSelection.js index c436ff3f9..7dc3ad1a5 100644 --- a/src/features/CellSelection.js +++ b/src/features/CellSelection.js @@ -108,9 +108,10 @@ var CellSelection = Feature.extend('CellSelection', { if (handler) { handler.call(this, grid, detail); - // STEP 2: Open the cell editor at the new position if it has `editOnNextCell` and is `editable` - cellEvent = grid.getGridCellFromLastSelection(true); // new cell + // STEP 2: Open the cell editor at the new position if `editable` AND edited cell had `editOnNextCell` if (cellEvent.properties.editOnNextCell) { + grid.renderer.computeCellsBounds(true); // moving selection may have auto-scrolled + cellEvent = grid.getGridCellFromLastSelection(); // new cell grid.editAt(cellEvent); // succeeds only if `editable` } diff --git a/src/features/ColumnMoving.js b/src/features/ColumnMoving.js index acfb569c4..e271409c2 100644 --- a/src/features/ColumnMoving.js +++ b/src/features/ColumnMoving.js @@ -160,15 +160,14 @@ var ColumnMoving = Feature.extend('ColumnMoving', { handleMouseDown: function(grid, event) { if ( grid.properties.columnsReorderable && - !event.isColumnFixed + !event.primitiveEvent.detail.isRightClick && + !event.isColumnFixed && + event.isHeaderCell ) { - if (event.isHeaderCell) { - this.dragArmed = true; - this.cursor = GRABBING; - grid.clearSelections(); - } - } - if (this.next) { + this.dragArmed = true; + this.cursor = GRABBING; + grid.clearSelections(); + } else if (this.next) { this.next.handleMouseDown(grid, event); } }, diff --git a/src/features/ColumnResizing.js b/src/features/ColumnResizing.js index 5ac33277e..74a8e96c7 100644 --- a/src/features/ColumnResizing.js +++ b/src/features/ColumnResizing.js @@ -64,7 +64,17 @@ var ColumnResizing = Feature.extend('ColumnResizing', { handleMouseDrag: function(grid, event) { if (this.dragColumn) { var delta = this.getMouseValue(event) - this.dragStart; - grid.behavior.setColumnWidth(this.dragColumn, this.dragStartWidth + delta); + var dragWidth = this.dragStartWidth + delta; + if (!this.nextColumn) { + 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 if (this.next) { this.next.handleMouseDrag(grid, event); } @@ -77,26 +87,38 @@ var ColumnResizing = Feature.extend('ColumnResizing', { */ handleMouseDown: function(grid, event) { if (event.isHeaderRow && this.overAreaDivider(grid, event)) { + var gridColumnIndex = event.gridCell.x; + if (event.mousePoint.x <= 3) { - var columnIndex = event.gridCell.x - 1; - this.dragColumn = grid.behavior.getActiveColumn(columnIndex); - //this.dragStartWidth = grid.renderer.visibleColumns[columnIndex].width; - var visibleColIndex = grid.behavior.rowColumnIndex; - var dragColumn = this.dragColumn; - grid.renderer.visibleColumns.forEachWithNeg(function(vCol, vIndex){ - var col = vCol.column; - if (col.index === dragColumn.index){ - visibleColIndex = vIndex; - } - }); - this.dragStartWidth = grid.renderer.visibleColumns[visibleColIndex].width; + gridColumnIndex -= 1; + var vc = grid.renderer.visibleColumns[gridColumnIndex] || + grid.renderer.visibleColumns[gridColumnIndex - 1]; // get row number column if tree column undefined + if (vc) { + this.dragColumn = vc.column; + this.dragStartWidth = vc.width; + } else { + return; // can't drag left-most column boundary + } } else { this.dragColumn = event.column; this.dragStartWidth = event.bounds.width; } this.dragStart = this.getMouseValue(event); - //this.detachChain(); + + if (this.dragColumn.properties.resizeColumnInPlace) { + gridColumnIndex += 1; + vc = grid.renderer.visibleColumns[gridColumnIndex] || + grid.renderer.visibleColumns[gridColumnIndex + 1]; // get first data column if tree column undefined; + if (vc) { + this.nextColumn = vc.column; + this.nextStartWidth = this.nextColumn.getWidth(); + } else { + this.nextColumn = undefined; + } + } else { + this.nextColumn = undefined; // in case resizeColumnInPlace was previously on but is now off + } } else if (this.next) { this.next.handleMouseDown(grid, event); } diff --git a/src/features/ColumnSelection.js b/src/features/ColumnSelection.js index d6bdbc754..eb6ddc70a 100644 --- a/src/features/ColumnSelection.js +++ b/src/features/ColumnSelection.js @@ -421,7 +421,7 @@ var ColumnSelection = Feature.extend('ColumnSelection', { isColumnDragging: function(grid) { var dragger = grid.lookupFeature('ColumnMoving'); - return dragger && dragger.dragging && !this.dragging; + return dragger && (dragger.dragging || dragger.dragArmed); //&& !this.dragging; } }); diff --git a/src/lib/SelectionModel.js b/src/lib/SelectionModel.js index d92e38d9a..4e62f79a8 100644 --- a/src/lib/SelectionModel.js +++ b/src/lib/SelectionModel.js @@ -154,9 +154,7 @@ SelectionModel.prototype = { } this.setLastSelectionType('cell'); - if (!silent) { - this.grid.selectionChanged(); - } + this.grid.selectionChanged(silent); }, /** @@ -168,17 +166,14 @@ SelectionModel.prototype = { */ toggleSelect: function(ox, oy, ex, ey) { - var selected, index; - - selected = this.selections.find(function(selection, idx) { - index = idx; + var index = this.selections.findIndex(function(selection) { return ( selection.origin.x === ox && selection.origin.y === oy && selection.extent.x === ex && selection.extent.y === ey ); }); - if (selected) { + if (index >= 0) { this.selections.splice(index, 1); this.flattenedX.splice(index, 1); this.flattenedY.splice(index, 1); @@ -335,6 +330,8 @@ SelectionModel.prototype = { this.rowSelectionModel.clear(); } else if (this.lastSelectionType.indexOf('row') >= 0) { this.lastSelectionType = ['row']; + } else { + this.lastSelectionType.length = 0; } //this.getGrid().selectionChanged(); }, diff --git a/src/renderer/index.js b/src/renderer/index.js index 32a96af4d..25d5f5124 100644 --- a/src/renderer/index.js +++ b/src/renderer/index.js @@ -174,8 +174,13 @@ var Renderer = Base.extend('Renderer', { this.lastKnowRowCount = undefined; }, - computeCellsBounds: function() { - this.needsComputeCellsBounds = true; + computeCellsBounds: function(immediate) { + if (immediate) { + computeCellsBounds.call(this); + this.needsComputeCellsBounds = false; + } else { + this.needsComputeCellsBounds = true; + } }, /** @@ -285,7 +290,7 @@ var Renderer = Base.extend('Renderer', { vrs = this.visibleRows, vcs = this.visibleColumns, firstColumn = vcs[this.grid.behavior.leftMostColIndex], - inFirstColumn = x < firstColumn.right, + inFirstColumn = firstColumn && x < firstColumn.right, vc = inFirstColumn ? firstColumn : vcs.findWithNeg(function(vc) { return x < vc.right; }), vr = vrs.find(function(vr) { return y < vr.bottom; }), result = {fake: false}; @@ -833,31 +838,46 @@ var Renderer = Base.extend('Renderer', { gridLinesHColor = gridProps.gridLinesHColor, borderBox = gridProps.boxSizing === 'border-box'; - if (gridProps.gridLinesV && (gridProps.gridLinesColumnHeader || gridProps.gridLinesUserDataArea)) { + if ( + gridProps.gridLinesV && ( // drawing vertical grid lines? + gridProps.gridLinesUserDataArea || // drawing vertical grid lines between data columns? + gridProps.gridLinesColumnHeader // drawing vertical grid lines between header columns? + ) + ) { var gridLinesVWidth = gridProps.gridLinesVWidth, headerRowCount = this.grid.getHeaderRowCount(), - userDataAreaTop = visibleRows[headerRowCount].top, + lastHeaderRow = visibleRows[headerRowCount - 1], // any header rows? + firstDataRow = visibleRows[headerRowCount], // any data rows? + userDataAreaTop = firstDataRow && firstDataRow.top, top = gridProps.gridLinesColumnHeader ? 0 : userDataAreaTop, - bottom = gridProps.gridLinesUserDataArea ? viewHeight : visibleRows[headerRowCount - 1].bottom; - - gc.cache.fillStyle = gridLinesVColor; - - visibleColumns.forEachWithNeg(function(vc, c) { - if ( - vc && // tree column may not be defined - c < C1 // don't draw rule after last column - ) { - var x = vc.right, - lineTop = Math.max(top, vc.top || 0), // vc.top may be set by grouped headers plug-in - height = Math.min(bottom, vc.bottom || Infinity) - lineTop; - if (borderBox) { x -= gridLinesVWidth; } - gc.fillRect(x, lineTop, gridLinesVWidth, height); - - if (gridProps.gridLinesUserDataArea && vc.bottom < userDataAreaTop) { - gc.fillRect(x, userDataAreaTop, gridLinesVWidth, bottom - userDataAreaTop); + bottom = gridProps.gridLinesUserDataArea ? viewHeight : lastHeaderRow && lastHeaderRow.bottom; + + if (top !== undefined && bottom !== undefined) { // either undefined means nothing to draw + gc.cache.fillStyle = gridLinesVColor; + + visibleColumns.forEachWithNeg(function(vc, c) { + if ( + vc && // tree column may not be defined + c < C1 // don't draw rule after last column + ) { + var x = vc.right, + lineTop = Math.max(top, vc.top || 0), // vc.top may be set by grouped headers plug-in + height = Math.min(bottom, vc.bottom || Infinity) - lineTop; // vc.bottom may be set by grouped headers plug-in + + if (borderBox) { + x -= gridLinesVWidth; + } + + // draw a single vertical grid line between both header and data cells OR a line segment in header only + gc.fillRect(x, lineTop, gridLinesVWidth, height); + + // when above drew a line segment in header (vc.bottom defined AND higher up), draw a second vertical grid line between data cells + if (gridProps.gridLinesUserDataArea && vc.bottom < userDataAreaTop) { + gc.fillRect(x, userDataAreaTop, gridLinesVWidth, bottom - userDataAreaTop); + } } - } - }); + }); + } } if ( @@ -875,7 +895,9 @@ var Renderer = Base.extend('Renderer', { visibleRows.forEach(function(vr, r) { if (r < R1) { // don't draw rule below last row var y = vr.bottom; - if (borderBox) { y -= gridLinesHWidth; } + if (borderBox) { + y -= gridLinesHWidth; + } gc.fillRect(left, y, right - left, gridLinesHWidth); } });