react-konva-grid
Version:
Declarative React Canvas Grid primitive for Data table, Pivot table, Excel Worksheets
296 lines • 11.1 kB
JavaScript
;
Object.defineProperty(exports, "__esModule", { value: true });
const react_1 = require("react");
const helpers_1 = require("./../helpers");
const types_1 = require("./../types");
const EMPTY_SELECTION = [];
/**
* useSelection hook to enable selection in datagrid
* @param initialSelection
*/
const useSelection = (options) => {
const { gridRef, initialActiveCell = null, initialSelections = [], columnCount = 0, rowCount = 0, } = options || {};
const [activeCell, setActiveCell] = react_1.useState(initialActiveCell);
const [selections, setSelections] = react_1.useState(initialSelections);
const selectionStart = react_1.useRef();
const selectionEnd = react_1.useRef();
const isSelectionMode = react_1.useRef();
/* New selection */
const newSelection = (start, end = start) => {
selectionStart.current = start;
selectionEnd.current = end;
const bounds = selectionFromStartEnd(start, end);
if (!bounds)
return;
setActiveCell({ rowIndex: bounds.top, columnIndex: bounds.left });
setSelections(EMPTY_SELECTION);
};
/* selection object from start, end */
const selectionFromStartEnd = (start, end) => {
if (!gridRef)
return null;
const boundsStart = gridRef.current.getCellBounds(start);
const boundsEnd = gridRef.current.getCellBounds(end);
return {
top: Math.min(boundsStart.top, boundsEnd.top),
bottom: Math.max(boundsStart.bottom, boundsEnd.bottom),
left: Math.min(boundsStart.left, boundsEnd.left),
right: Math.max(boundsStart.right, boundsEnd.right),
};
};
/* Modify current selection */
const modifySelection = (coords, setInProgress) => {
if (!selectionStart.current)
return;
selectionEnd.current = coords;
const bounds = selectionFromStartEnd(selectionStart.current, coords);
if (!bounds)
return;
/**
* 1. Multiple selections on mousedown/mousemove
* 2. Move the activeCell to newly selection. Done by appendSelection
*/
setSelections((prevSelection) => {
const len = prevSelection.length;
if (!len) {
return [{ bounds, inProgress: setInProgress ? true : false }];
}
return prevSelection.map((sel, i) => {
if (len - 1 === i) {
return Object.assign(Object.assign({}, sel), { bounds, inProgress: setInProgress ? true : false });
}
return sel;
});
});
};
/* Adds a new selection, CMD key */
const appendSelection = (coords) => {
selectionStart.current = coords;
selectionEnd.current = coords;
const bounds = selectionFromStartEnd(coords, coords);
if (!bounds)
return;
setActiveCell({ rowIndex: bounds.top, columnIndex: bounds.left });
setSelections((prev) => [...prev, { bounds }]);
};
/**
* Triggers a new selection start
*/
const handleMouseDown = react_1.useCallback((e) => {
/* Exit early if grid is not initialized */
if (!gridRef || !gridRef.current)
return;
/* Activate selection mode */
isSelectionMode.current = true;
const { rowIndex, columnIndex } = gridRef.current.getCellCoordsFromOffset(e.clientX, e.clientY);
/**
* Save the initial Selection in ref
* so we can adjust the bounds in mousemove
*/
const coords = { rowIndex, columnIndex };
/* Shift key */
if (e.nativeEvent.shiftKey) {
modifySelection(coords);
return;
}
/* Command or Control key */
if (e.nativeEvent.metaKey || e.nativeEvent.ctrlKey) {
appendSelection(coords);
return;
}
/* Trigger new selection */
newSelection(coords);
}, []);
/**
* Mousemove handler
*/
const handleMouseMove = react_1.useCallback((e) => {
/* Exit if user is not in selection mode */
if (!isSelectionMode.current || !gridRef || !selectionEnd.current)
return;
const { rowIndex, columnIndex } = gridRef.current.getCellCoordsFromOffset(e.clientX, e.clientY);
/**
* If the user is moving across the Active Cell, lets not add it to selection
*/
if ((activeCell === null || activeCell === void 0 ? void 0 : activeCell.rowIndex) === rowIndex &&
(activeCell === null || activeCell === void 0 ? void 0 : activeCell.columnIndex) === columnIndex)
return;
modifySelection({ rowIndex, columnIndex }, true);
}, [activeCell]);
/**
* Mouse up handler
*/
const handleMouseUp = react_1.useCallback(() => {
/* Reset selection mode */
isSelectionMode.current = false;
if (!selections.length)
return;
/* Update last selection */
setSelections((prevSelection) => {
const len = prevSelection.length;
return prevSelection.map((sel, i) => {
if (len - 1 === i) {
return Object.assign(Object.assign({}, sel), { inProgress: false });
}
return sel;
});
});
}, [selections]);
/**
* Navigate selection using keyboard
* @param direction
* @param modify
*/
const keyNavigate = react_1.useCallback((direction, modify) => {
if (!selectionStart.current ||
!selectionEnd.current ||
!gridRef ||
!activeCell)
return;
var { rowIndex, columnIndex } = modify
? selectionEnd.current
: activeCell;
const isMergedCell = gridRef === null || gridRef === void 0 ? void 0 : gridRef.current.isMergedCell({
rowIndex,
columnIndex,
});
const bounds = gridRef.current.getCellBounds({ rowIndex, columnIndex });
switch (direction) {
case types_1.Direction.Up:
if (isMergedCell)
rowIndex = bounds.top;
rowIndex = Math.max(rowIndex - 1, 0);
break;
case types_1.Direction.Down:
if (isMergedCell)
rowIndex = bounds.bottom;
rowIndex = Math.min(rowIndex + 1, rowCount - 1);
break;
case types_1.Direction.Left:
if (isMergedCell)
columnIndex = bounds.left;
columnIndex = Math.max(columnIndex - 1, 0);
break;
case types_1.Direction.Right:
if (isMergedCell)
columnIndex = bounds.right;
columnIndex = Math.min(columnIndex + 1, columnCount - 1);
break;
}
const scrollToCell = modify
? selectionEnd.current.rowIndex === rowIndex
? { columnIndex }
: { rowIndex }
: { rowIndex, columnIndex };
if (modify) {
modifySelection({ rowIndex, columnIndex });
}
else {
newSelection({ rowIndex, columnIndex });
}
/* Keep the item in view */
gridRef.current.scrollToItem(scrollToCell);
}, [activeCell]);
// ⌘A or ⌘+Shift+Space
const selectAll = () => {
selectionStart.current = { rowIndex: 0, columnIndex: 0 };
modifySelection({ rowIndex: rowCount - 1, columnIndex: columnCount - 1 });
};
// Ctrl+Space
const selectColumn = () => {
if (!selectionEnd.current || !selectionStart.current)
return;
selectionStart.current = {
rowIndex: 0,
columnIndex: selectionStart.current.columnIndex,
};
modifySelection({
rowIndex: rowCount - 1,
columnIndex: selectionEnd.current.columnIndex,
});
};
// Shift+Space
const selectRow = () => {
if (!selectionEnd.current || !selectionStart.current)
return;
selectionStart.current = {
rowIndex: selectionStart.current.rowIndex,
columnIndex: 0,
};
modifySelection({
rowIndex: selectionEnd.current.rowIndex,
columnIndex: columnCount - 1,
});
};
const handleKeyDown = react_1.useCallback((e) => {
const isShiftKey = e.nativeEvent.shiftKey;
const isMetaKey = e.nativeEvent.ctrlKey || e.nativeEvent.metaKey;
switch (e.nativeEvent.which) {
case types_1.KeyCodes.Right:
keyNavigate(types_1.Direction.Right, isShiftKey);
break;
case types_1.KeyCodes.Left:
keyNavigate(types_1.Direction.Left, isShiftKey);
break;
// Up
case types_1.KeyCodes.Up:
keyNavigate(types_1.Direction.Up, isShiftKey);
break;
case types_1.KeyCodes.Down:
keyNavigate(types_1.Direction.Down, isShiftKey);
break;
case types_1.KeyCodes.A:
// Select All
if (isMetaKey) {
selectAll();
}
break;
case types_1.KeyCodes.SPACE:
if (isMetaKey && isShiftKey) {
selectAll();
}
else if (isMetaKey) {
selectColumn();
}
else if (isShiftKey) {
selectRow();
}
break;
case types_1.KeyCodes.Tab:
/* Cycle through the selections if selections.length > 0 */
if (selections.length && activeCell && gridRef) {
const { bounds } = selections[0];
const activeCellBounds = gridRef.current.getCellBounds(activeCell);
const direction = isShiftKey
? types_1.Movement.backwards
: types_1.Movement.forwards;
const nextCell = helpers_1.findNextCellWithinBounds(activeCellBounds, bounds, direction);
if (nextCell)
setActiveCell(nextCell);
}
else {
if (isShiftKey) {
keyNavigate(types_1.Direction.Left);
}
else {
keyNavigate(types_1.Direction.Right);
}
}
e.preventDefault();
break;
}
}, [rowCount, columnCount, activeCell, selections]);
return {
activeCell,
selections,
onMouseDown: handleMouseDown,
onMouseMove: handleMouseMove,
onMouseUp: handleMouseUp,
onKeyDown: handleKeyDown,
newSelection,
setSelections,
setActiveCell,
};
};
exports.default = useSelection;
//# sourceMappingURL=useSelection.js.map