Skip to content

Commit

Permalink
Improve selection and paste (#500)
Browse files Browse the repository at this point in the history
* Support filling data over/down on paste

If the number of selected rows or columns is
larger than the number of content available to
paste, repeat the content to fill the remaining
selected rows and columns.

It is possible to extend the behavior of this
functionality by providing a callback function
using the 'fillCellCallback' grid argument.

* Add tests for the 'fill data' feature

* Fill data on selection handle move

This adds a handle to the desktop version that
users can drag and drop to quickly fill a region.

When the handle is dragged, an overlay will
appear covering the previous selection area and
later expanding upon it when moved. When the user
releases the handle, this fills the region that
the overlay now covers with the data from the
previous selection area.

Similarly to paste events, this can benefit from
'fillCellCallback' to transform the data before
placing it into the cell.

* Add test for the 'fill by dragging handle' feature
  • Loading branch information
velitasali committed Mar 16, 2022
1 parent e61d80d commit 5fc1e77
Show file tree
Hide file tree
Showing 8 changed files with 783 additions and 82 deletions.
4 changes: 4 additions & 0 deletions lib/defaults.js
Original file line number Diff line number Diff line change
Expand Up @@ -25,9 +25,11 @@ export default function (self) {
['autoResizeRows', false],
['autoScrollOnMousemove', false],
['autoScrollMargin', 5],
['allowShrinkingSelection', false],
['blanksText', '(Blanks)'],
['borderDragBehavior', 'none'],
['borderResizeZone', 10],
['clearCellsWhenShrinkingSelection', false],
['clearSettingsOptionText', 'Clear saved settings'],
['columnHeaderClickBehavior', 'sort'],
['columnSelectorHiddenText', '   '],
Expand Down Expand Up @@ -371,6 +373,8 @@ export default function (self) {
['selectionHandleColor', 'rgba(66, 133, 244, 1)'],
['selectionHandleSize', 8],
['selectionHandleType', 'square'],
['fillOverlayBorderColor', 'rgba(127, 127, 127, 1)'],
['fillOverlayBorderWidth', 2],
['selectionOverlayBorderColor', 'rgba(66, 133, 244, 1)'],
['selectionOverlayBorderWidth', 1],
['treeArrowBorderColor', 'rgba(195, 199, 202, 1)'],
Expand Down
33 changes: 32 additions & 1 deletion lib/docs.js
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,8 @@
* @param {number} [args.autoScrollMargin=5] - Number of pixels of mouse movement to trigger auto scroll.
* @param {boolean} [args.allowFreezingRows=false] - When true, the UI provides a drag-able cutter to freeze rows.
* @param {boolean} [args.allowFreezingColumns=false] - When true, the UI provides a drag-able cutter to freeze columns.
* @param {boolean} [args.allowShrinkingSelection=false] - When true, this allows users to shrink the selection by dragging and dropping the handle inwards, similar to Excel. (Not supported yet.)
* @param {boolean} [args.clearCellsWhenShrinkingSelection=false] - When true, this allows the clearing of the previously selected cells after the handle is moved inwards. (Not supported yet.)
* @param {boolean} [args.filterFrozenRows=true] - When false, rows on and above {@link canvasDatagrid#property:frozenRow}` will be ignored by filters when {@link canvasDatagrid#param:allowFreezingRows} is true.
* @param {boolean} [args.sortFrozenRows=true] - When false, rows on and above {@link canvasDatagrid#property:frozenRow}` will be ignored by sorters when {@link canvasDatagrid#param:allowSorting} is true.
* @param {boolean} [args.allowColumnReordering=true] - When true columns can be reordered.
Expand Down Expand Up @@ -91,7 +93,7 @@
* @param {string} [args.copyText=Copy] - The text that appears on the context menu when copy is available.
* @param {string} [args.columnHeaderClickBehavior=sort] - Can be any of sort, select, none. When sort is selected, left clicking a column header will result in sorting ascending, then sorting descending if already sorted by this column. If select is selected then clicking a column header will result in the column becoming selected. Holding control/command or shift will select multiple columns in this mode.
* @param {boolean} [args.scrollPointerLock=false] - When true, clicking on the scroll box to scroll will cause the mouse cursor to disappear to prevent it from exiting the area observable to the grid.
* @param {string} [args.selectionHandleBehavior='none'] - When set to a value other than none a handle will appear in the lower right corner of the desktop version of the grid. It does nothing but will be used in a future version of the grid.
* @param {string} [args.selectionHandleBehavior='none'] - When set to a value other than none, a handle will appear in the lower right corner of the desktop version of the grid.
* @param {string} [args.columnSelectorVisibleText='  &nbsp '] - When a column is hidden, this is the value to the left of the title in the column selector content menu.
* @param {string} [args.columnSelectorHiddenText='\u2713'] - When a column is visible, this is the value to the left of the title in the column selector content menu.
* @param {string} [args.columnSelectorText='Add/remove columns'] - The text of the column selector context menu.
Expand All @@ -106,6 +108,7 @@
* @param {number} [args.touchZoomMin=0.5] - The minimum zoom scale.
* @param {number} [args.touchZoomMax=1.75] - The maximum zoom scale.
* @param {number} [args.maxPixelRatio=1.5] - The maximum pixel ratio for high DPI displays. High DPI displays can cause sluggish performance, this caps resolution the grid is rendered at. Standard resolution (e.g.: 1920x1080) have a pixel ratio of 1:1 while higher resolution displays can be higher (e.g.: Retina display 2:1). Higher numbers are sharper (higher resolution) up to the max of your display (usually 2), lower numbers are lower resolution, down to 1. It might be fun to set a value lower than 1, I've never done it.
* @param {object} [args.fillCellCallback] - The callback function that is called when a cell is filled.
* @param {canvasDatagrid.style} [args.style={}] - Sets selected values in style.
* @param {canvasDatagrid.filter} [args.filters={}] - Sets selected values in filters. See {@link canvasDatagrid.filters}.
* @param {canvasDatagrid.sorter} [args.sorters={}] - Sets selected values in sorters. See {@link canvasDatagrid.sorters}.
Expand Down Expand Up @@ -256,6 +259,32 @@
* @property {object} text.width - The width of the text, including truncation and ellipsis.
* @property {object} text.value - The value of the text, including truncation and ellipsis.
*/
/**
* The additional function to invoke when a cell is filled. The return value of this
* function sets the new data of the cell.
* @function
* @name canvasDataGrid.fillCellCallback
* @param {object} args - Arguments that are passed to the function.
* @param {array} args.rows - Data that is being filled into all the cells, including this one.
* @param {string} args.direction - Direction of the fill operation, which can be 'horizontal' for x axis, 'vertical' for y axis, or 'both' when it goes both ways, e.g., when pasting unevenly.
* @param {object} args.newRowData - New row data that is going to replace 'existingRowData'. The result of this function will be kept in this object.
* @param {object} args.existingRowData - Existing data of the row. This will be replaced with 'newRowData'.
* @param {number} args.rowIndex - Index of the row on the grid.
* @param {number} args.rowOffset - Position/offset of 'cells' in 'rows'. Starts with '0'.
* @param {array} args.cells - New data for all the cells, including this one, for the current row. This is the same as accessing 'rows' using 'rowOffset'.
* @param {boolean} args.reversed - Whether the fill operation is applied in the reverse order, meaning if horizontal, we are going from right to left, and if vertical, we are going from bottom to top.
* @param {boolean} args.isFillingRow - Whether the current row is being filled. If the 'direction' is 'vertical', this will always be true, or sometimes true if the 'direction' is 'both'.
* @param {number} args.fillingRowPosition - Increase in position since the first filling of the rows. This will be '-1' if 'isFillingRow' is false. Starts with '0'.
* @param {number} args.fillingRowLength - Total length that 'fillingRowPosition' will go. This will be '-1' if 'isFillingRow' is false.
* @param {object} args.column - Column that this cell is on.
* @param {number} args.columnIndex - The index of the column on the grid.
* @param {string} args.newCellData - Data from 'cells' to be set for this cell. You can modify this and return the result to set the new data for this cell.
* @param {string} args.existingCellData - Existing data on this cell that is about to be replaced with the data you will return.
* @param {number} args.columnOffset - Position/offset of the 'newCellData' in 'cells'. Starts with '0'.
* @param {boolean} args.isFillingColumn - Whether the current column is being filled. If the 'direction' is 'horizontal', this will always be true, or sometimes true if the 'direction' is 'both'.
* @param {number} args.fillingColumnPosition - Increase in position since the first filling of the columns. This will be '-1' if 'isFillingColumn' is false. Starts with '0'.
* @param {number} args.fillingColumnLength - Total length that 'fillingColumnPosition' will go. This will be '-1' if 'isFillingColumn' is false.
*/
/**
* Styles for the canvas data grid. Standard CSS styles still apply but are not listed here.
* @class
Expand Down Expand Up @@ -364,6 +393,8 @@
* @property {string} [editCellColor=black] - Style of editCellColor.
* @property {string} [editCellBackgroundColor=white] - Style of editCellBackgroundColor.
* @property {number} [editCellZIndex=10000] - Style of editCellZIndex.
* @property {string} [fillOverlayBorderColor=rgba(127, 127, 127, 1)] - Style of fillOverlayBorderColor.
* @property {number} [fillOverlayBorderWidth=2] - Style of fillOverlayBorderWidth.
* @property {string} [gridBackgroundColor=rgba(240, 240, 240, 1)] - Style of gridBackgroundColor.
* @property {string} [gridBorderColor=rgba(202, 202, 202, 1)] - Style of gridBorderColor.
* @property {number} [gridBorderWidth=1] - Style of gridBorderWidth.
Expand Down
105 changes: 94 additions & 11 deletions lib/draw.js
Original file line number Diff line number Diff line change
Expand Up @@ -943,13 +943,17 @@ export default function (self) {
selectionHandles.push([cell, 'tr']);
cell.selectionHandle = 'tr';
}
if (
cell.selectionBorderTop &&
cell.selectionBorderLeft &&
self.mobile
) {
selectionHandles.push([cell, 'tl']);
cell.selectionHandle = 'tl';
if (cell.selectionBorderTop && cell.selectionBorderLeft) {
if (self.mobile) {
selectionHandles.push([cell, 'tl']);
cell.selectionHandle = 'tl';
}
if (self.fillOverlay.handle) {
self.fillOverlay.snapTo = {
x: cell.x,
y: cell.y,
};
}
}
if (
cell.selectionBorderBottom &&
Expand All @@ -966,6 +970,22 @@ export default function (self) {
) {
selectionHandles.push([cell, 'br']);
cell.selectionHandle = 'br';

if (self.fillOverlay.handle) {
self.fillOverlay.handle.x = cell.x + cell.width;
self.fillOverlay.handle.y = cell.y + cell.height;
}
}

if (self.fillOverlay.handle) {
// Some corners may not be displaying, so we get at least
// one correct axis in order to display a fill overlay.
if (self.fillOverlay.snapTo.x === -1 && cell.selectionBorderLeft) {
self.fillOverlay.snapTo.x = cell.x;
}
if (self.fillOverlay.snapTo.y === -1 && cell.selectionBorderTop) {
self.fillOverlay.snapTo.y = cell.y;
}
}
}
}
Expand Down Expand Up @@ -2496,10 +2516,12 @@ export default function (self) {
strokeRect(0, 0, self.width, self.height);
}
function drawSelectionBorders() {
self.ctx.lineWidth = self.style.selectionOverlayBorderWidth;
self.ctx.strokeStyle = self.style.selectionOverlayBorderColor;
function dsb(c) {
addBorderLine(c[0], c[1]);
if (!self.fillOverlay.handle) {
self.ctx.lineWidth = self.style.selectionOverlayBorderWidth;
self.ctx.strokeStyle = self.style.selectionOverlayBorderColor;
addBorderLine(c[0], c[1]);
}
}
selectionBorders
.filter(function (c) {
Expand Down Expand Up @@ -2542,9 +2564,14 @@ export default function (self) {
})
.forEach(dsb);
self.ctx.restore();

drawFillOverlay();
}
function drawSelectionHandles() {
if (self.mobile || self.attributes.allowMovingSelection) {
if (
(self.mobile || self.attributes.allowMovingSelection) &&
self.attributes.editable
) {
self.ctx.lineWidth = self.style.selectionHandleBorderWidth;
self.ctx.strokeStyle = self.style.selectionHandleBorderColor;
self.ctx.fillStyle = self.style.selectionHandleColor;
Expand All @@ -2565,6 +2592,62 @@ export default function (self) {
});
}
}
function drawFillOverlay() {
if (!self.fillOverlay.handle || !self.fillOverlay.snapTo) {
return;
}

self.ctx.save();

const overlay = self.fillOverlay;
const handle = overlay.handle;
const toX = overlay.snapTo.x;
const toY = overlay.snapTo.y;

// The cell that the cursor is moving over. This may be unavailable
// when the cursor is outside the grid or is pointing to something else.
//
// When unavailable, we use the actual position of the cursor to draw
// the overlay.
const snap = overlay.snap;

// Calculate the X, Y coordinates of the cursor with snap positions and
// the direction of the movement in mind.
//
// When the movement is horizontal, we get the Y coordinate from the handle,
// and vice-versa.
const cursorX =
overlay.direction === 'x'
? snap
? snap.x + (overlay.x < toX ? 0 : snap.width)
: overlay.x
: handle.x;
const cursorY =
overlay.direction === undefined || overlay.direction === 'y'
? snap
? snap.y + (overlay.y < toY ? 0 : snap.height)
: overlay.y
: handle.y;

// The final coordinates of the rect. We use 'minX' and 'minY' to avoid
// drawing on the frozen area.
const x = Math.max(Math.min(toX, cursorX), overlay.minX);
const y = Math.max(Math.min(toY, cursorY), overlay.minY);

// Width and height of the rect are the difference between the
// coordinates of both the rect and the cursor. The user may be pointing
// behind and/or upwards of the rect, so we need to take that into
// account.
const width = Math.max(toX, cursorX) - x;
const height = Math.max(toY, cursorY) - y;

self.ctx.strokeStyle = self.style.fillOverlayBorderColor;
self.ctx.lineWidth = self.style.fillOverlayBorderWidth;
self.ctx.setLineDash([3, 3]);
strokeRect(x, y, width, height);
self.ctx.setLineDash([]);
self.ctx.restore();
}
function drawActiveCell() {
if (!aCell) {
return;
Expand Down
Loading

0 comments on commit 5fc1e77

Please sign in to comment.