// Copyright (c) Jupyter Development Team. // Distributed under the terms of the Modified BSD License. /*----------------------------------------------------------------------------- | Copyright (c) 2014-2019, PhosphorJS Contributors | | Distributed under the terms of the BSD 3-Clause License. | | The full license is in the file LICENSE, distributed with this software. |----------------------------------------------------------------------------*/ import { Platform } from '@lumino/domutils'; import { getKeyboardLayout } from '@lumino/keyboard'; import { DataGrid } from './datagrid'; import { SelectionModel } from './selectionmodel'; import { CellEditor } from './celleditor'; import { MutableDataModel } from './datamodel'; /** * A basic implementation of a data grid key handler. * * #### Notes * This class may be subclassed and customized as needed. */ export class BasicKeyHandler implements DataGrid.IKeyHandler { /** * Whether the key handler is disposed. */ get isDisposed(): boolean { return this._disposed; } /** * Dispose of the resources held by the key handler. */ dispose(): void { this._disposed = true; } /** * Handle the key down event for the data grid. * * @param grid - The data grid of interest. * * @param event - The keydown event of interest. * * #### Notes * This will not be called if the mouse button is pressed. */ onKeyDown(grid: DataGrid, event: KeyboardEvent): void { // if grid is editable and cell selection available, start cell editing // on key press (letters, numbers and space only) if ( grid.editable && grid.selectionModel!.cursorRow !== -1 && grid.selectionModel!.cursorColumn !== -1 ) { const input = String.fromCharCode(event.keyCode); if (/[a-zA-Z0-9-_ ]/.test(input)) { const row = grid.selectionModel!.cursorRow; const column = grid.selectionModel!.cursorColumn; const cell: CellEditor.CellConfig = { grid: grid, row: row, column: column }; grid.editorController!.edit(cell); if (getKeyboardLayout().keyForKeydownEvent(event) === 'Space') { event.stopPropagation(); event.preventDefault(); } return; } } switch (getKeyboardLayout().keyForKeydownEvent(event)) { case 'ArrowLeft': this.onArrowLeft(grid, event); break; case 'ArrowRight': this.onArrowRight(grid, event); break; case 'ArrowUp': this.onArrowUp(grid, event); break; case 'ArrowDown': this.onArrowDown(grid, event); break; case 'PageUp': this.onPageUp(grid, event); break; case 'PageDown': this.onPageDown(grid, event); break; case 'Escape': this.onEscape(grid, event); break; case 'Delete': this.onDelete(grid, event); break; case 'C': this.onKeyC(grid, event); break; case 'Enter': if (grid.selectionModel) { grid.moveCursor(event.shiftKey ? 'up' : 'down'); grid.scrollToCursor(); } break; case 'Tab': if (grid.selectionModel) { grid.moveCursor(event.shiftKey ? 'left' : 'right'); grid.scrollToCursor(); event.stopPropagation(); event.preventDefault(); } break; } } /** * Handle the `'ArrowLeft'` key press for the data grid. * * @param grid - The data grid of interest. * * @param event - The keyboard event of interest. */ protected onArrowLeft(grid: DataGrid, event: KeyboardEvent): void { // Stop the event propagation. event.preventDefault(); event.stopPropagation(); // Fetch the selection model. let model = grid.selectionModel; // Fetch the modifier flags. let shift = event.shiftKey; let accel = Platform.accelKey(event); // Handle no model with the accel modifier. if (!model && accel) { grid.scrollTo(0, grid.scrollY); return; } // Handle no model and no modifier. (ignore shift) if (!model) { grid.scrollByStep('left'); return; } // Fetch the selection mode. let mode = model.selectionMode; // Handle the row selection mode with accel key. if (mode === 'row' && accel) { grid.scrollTo(0, grid.scrollY); return; } // Handle the row selection mode with no modifier. (ignore shift) if (mode === 'row') { grid.scrollByStep('left'); return; } // Fetch the cursor and selection. let r = model.cursorRow; let c = model.cursorColumn; let cs = model.currentSelection(); // Set up the selection variables. let r1: number; let r2: number; let c1: number; let c2: number; let cr: number; let cc: number; let clear: SelectionModel.ClearMode; // Dispatch based on the modifier keys. if (accel && shift) { r1 = cs ? cs.r1 : 0; r2 = cs ? cs.r2 : 0; c1 = cs ? cs.c1 : 0; c2 = 0; cr = r; cc = c; clear = 'current'; } else if (shift) { r1 = cs ? cs.r1 : 0; r2 = cs ? cs.r2 : 0; c1 = cs ? cs.c1 : 0; c2 = cs ? cs.c2 - 1 : 0; cr = r; cc = c; clear = 'current'; } else if (accel) { r1 = r; r2 = r; c1 = 0; c2 = 0; cr = r1; cc = c1; clear = 'all'; } else { r1 = r; r2 = r; c1 = c - 1; c2 = c - 1; cr = r1; cc = c1; clear = 'all'; } // Create the new selection. model.select({ r1, c1, r2, c2, cursorRow: cr, cursorColumn: cc, clear }); // Re-fetch the current selection. cs = model.currentSelection(); // Bail if there is no selection. if (!cs) { return; } // Scroll the grid appropriately. if (shift || mode === 'column') { grid.scrollToColumn(cs.c2); } else { grid.scrollToCursor(); } } /** * Handle the `'ArrowRight'` key press for the data grid. * * @param grid - The data grid of interest. * * @param event - The keyboard event of interest. */ protected onArrowRight(grid: DataGrid, event: KeyboardEvent): void { // Stop the event propagation. event.preventDefault(); event.stopPropagation(); // Fetch the selection model. let model = grid.selectionModel; // Fetch the modifier flags. let shift = event.shiftKey; let accel = Platform.accelKey(event); // Handle no model with the accel modifier. if (!model && accel) { grid.scrollTo(grid.maxScrollX, grid.scrollY); return; } // Handle no model and no modifier. (ignore shift) if (!model) { grid.scrollByStep('right'); return; } // Fetch the selection mode. let mode = model.selectionMode; // Handle the row selection model with accel key. if (mode === 'row' && accel) { grid.scrollTo(grid.maxScrollX, grid.scrollY); return; } // Handle the row selection mode with no modifier. (ignore shift) if (mode === 'row') { grid.scrollByStep('right'); return; } // Fetch the cursor and selection. let r = model.cursorRow; let c = model.cursorColumn; let cs = model.currentSelection(); // Set up the selection variables. let r1: number; let r2: number; let c1: number; let c2: number; let cr: number; let cc: number; let clear: SelectionModel.ClearMode; // Dispatch based on the modifier keys. if (accel && shift) { r1 = cs ? cs.r1 : 0; r2 = cs ? cs.r2 : 0; c1 = cs ? cs.c1 : 0; c2 = Infinity; cr = r; cc = c; clear = 'current'; } else if (shift) { r1 = cs ? cs.r1 : 0; r2 = cs ? cs.r2 : 0; c1 = cs ? cs.c1 : 0; c2 = cs ? cs.c2 + 1 : 0; cr = r; cc = c; clear = 'current'; } else if (accel) { r1 = r; r2 = r; c1 = Infinity; c2 = Infinity; cr = r1; cc = c1; clear = 'all'; } else { r1 = r; r2 = r; c1 = c + 1; c2 = c + 1; cr = r1; cc = c1; clear = 'all'; } // Create the new selection. model.select({ r1, c1, r2, c2, cursorRow: cr, cursorColumn: cc, clear }); // Re-fetch the current selection. cs = model.currentSelection(); // Bail if there is no selection. if (!cs) { return; } // Scroll the grid appropriately. if (shift || mode === 'column') { grid.scrollToColumn(cs.c2); } else { grid.scrollToCursor(); } } /** * Handle the `'ArrowUp'` key press for the data grid. * * @param grid - The data grid of interest. * * @param event - The keyboard event of interest. */ protected onArrowUp(grid: DataGrid, event: KeyboardEvent): void { // Stop the event propagation. event.preventDefault(); event.stopPropagation(); // Fetch the selection model. let model = grid.selectionModel; // Fetch the modifier flags. let shift = event.shiftKey; let accel = Platform.accelKey(event); // Handle no model with the accel modifier. if (!model && accel) { grid.scrollTo(grid.scrollX, 0); return; } // Handle no model and no modifier. (ignore shift) if (!model) { grid.scrollByStep('up'); return; } // Fetch the selection mode. let mode = model.selectionMode; // Handle the column selection mode with accel key. if (mode === 'column' && accel) { grid.scrollTo(grid.scrollX, 0); return; } // Handle the column selection mode with no modifier. (ignore shift) if (mode === 'column') { grid.scrollByStep('up'); return; } // Fetch the cursor and selection. let r = model.cursorRow; let c = model.cursorColumn; let cs = model.currentSelection(); // Set up the selection variables. let r1: number; let r2: number; let c1: number; let c2: number; let cr: number; let cc: number; let clear: SelectionModel.ClearMode; // Dispatch based on the modifier keys. if (accel && shift) { r1 = cs ? cs.r1 : 0; r2 = 0; c1 = cs ? cs.c1 : 0; c2 = cs ? cs.c2 : 0; cr = r; cc = c; clear = 'current'; } else if (shift) { r1 = cs ? cs.r1 : 0; r2 = cs ? cs.r2 - 1 : 0; c1 = cs ? cs.c1 : 0; c2 = cs ? cs.c2 : 0; cr = r; cc = c; clear = 'current'; } else if (accel) { r1 = 0; r2 = 0; c1 = c; c2 = c; cr = r1; cc = c1; clear = 'all'; } else { r1 = r - 1; r2 = r - 1; c1 = c; c2 = c; cr = r1; cc = c1; clear = 'all'; } // Create the new selection. model.select({ r1, c1, r2, c2, cursorRow: cr, cursorColumn: cc, clear }); // Re-fetch the current selection. cs = model.currentSelection(); // Bail if there is no selection. if (!cs) { return; } // Scroll the grid appropriately. if (shift || mode === 'row') { grid.scrollToRow(cs.r2); } else { grid.scrollToCursor(); } } /** * Handle the `'ArrowDown'` key press for the data grid. * * @param grid - The data grid of interest. * * @param event - The keyboard event of interest. */ protected onArrowDown(grid: DataGrid, event: KeyboardEvent): void { // Stop the event propagation. event.preventDefault(); event.stopPropagation(); // Fetch the selection model. let model = grid.selectionModel; // Fetch the modifier flags. let shift = event.shiftKey; let accel = Platform.accelKey(event); // Handle no model with the accel modifier. if (!model && accel) { grid.scrollTo(grid.scrollX, grid.maxScrollY); return; } // Handle no model and no modifier. (ignore shift) if (!model) { grid.scrollByStep('down'); return; } // Fetch the selection mode. let mode = model.selectionMode; // Handle the column selection mode with accel key. if (mode === 'column' && accel) { grid.scrollTo(grid.scrollX, grid.maxScrollY); return; } // Handle the column selection mode with no modifier. (ignore shift) if (mode === 'column') { grid.scrollByStep('down'); return; } // Fetch the cursor and selection. let r = model.cursorRow; let c = model.cursorColumn; let cs = model.currentSelection(); // Set up the selection variables. let r1: number; let r2: number; let c1: number; let c2: number; let cr: number; let cc: number; let clear: SelectionModel.ClearMode; // Dispatch based on the modifier keys. if (accel && shift) { r1 = cs ? cs.r1 : 0; r2 = Infinity; c1 = cs ? cs.c1 : 0; c2 = cs ? cs.c2 : 0; cr = r; cc = c; clear = 'current'; } else if (shift) { r1 = cs ? cs.r1 : 0; r2 = cs ? cs.r2 + 1 : 0; c1 = cs ? cs.c1 : 0; c2 = cs ? cs.c2 : 0; cr = r; cc = c; clear = 'current'; } else if (accel) { r1 = Infinity; r2 = Infinity; c1 = c; c2 = c; cr = r1; cc = c1; clear = 'all'; } else { r1 = r + 1; r2 = r + 1; c1 = c; c2 = c; cr = r1; cc = c1; clear = 'all'; } // Create the new selection. model.select({ r1, c1, r2, c2, cursorRow: cr, cursorColumn: cc, clear }); // Re-fetch the current selection. cs = model.currentSelection(); // Bail if there is no selection. if (!cs) { return; } // Scroll the grid appropriately. if (shift || mode === 'row') { grid.scrollToRow(cs.r2); } else { grid.scrollToCursor(); } } /** * Handle the `'PageUp'` key press for the data grid. * * @param grid - The data grid of interest. * * @param event - The keyboard event of interest. */ protected onPageUp(grid: DataGrid, event: KeyboardEvent): void { // Ignore the event if the accel key is pressed. if (Platform.accelKey(event)) { return; } // Stop the event propagation. event.preventDefault(); event.stopPropagation(); // Fetch the selection model. let model = grid.selectionModel; // Scroll by page if there is no selection model. if (!model || model.selectionMode === 'column') { grid.scrollByPage('up'); return; } // Get the normal number of cells in the page height. let n = Math.floor(grid.pageHeight / grid.defaultSizes.rowHeight); // Fetch the cursor and selection. let r = model.cursorRow; let c = model.cursorColumn; let cs = model.currentSelection(); // Set up the selection variables. let r1: number; let r2: number; let c1: number; let c2: number; let cr: number; let cc: number; let clear: SelectionModel.ClearMode; // Select or resize as needed. if (event.shiftKey) { r1 = cs ? cs.r1 : 0; r2 = cs ? cs.r2 - n : 0; c1 = cs ? cs.c1 : 0; c2 = cs ? cs.c2 : 0; cr = r; cc = c; clear = 'current'; } else { r1 = cs ? cs.r1 - n : 0; r2 = r1; c1 = c; c2 = c; cr = r1; cc = c; clear = 'all'; } // Create the new selection. model.select({ r1, c1, r2, c2, cursorRow: cr, cursorColumn: cc, clear }); // Re-fetch the current selection. cs = model.currentSelection(); // Bail if there is no selection. if (!cs) { return; } // Scroll the grid appropriately. grid.scrollToRow(cs.r2); } /** * Handle the `'PageDown'` key press for the data grid. * * @param grid - The data grid of interest. * * @param event - The keyboard event of interest. */ protected onPageDown(grid: DataGrid, event: KeyboardEvent): void { // Ignore the event if the accel key is pressed. if (Platform.accelKey(event)) { return; } // Stop the event propagation. event.preventDefault(); event.stopPropagation(); // Fetch the selection model. let model = grid.selectionModel; // Scroll by page if there is no selection model. if (!model || model.selectionMode === 'column') { grid.scrollByPage('down'); return; } // Get the normal number of cells in the page height. let n = Math.floor(grid.pageHeight / grid.defaultSizes.rowHeight); // Fetch the cursor and selection. let r = model.cursorRow; let c = model.cursorColumn; let cs = model.currentSelection(); // Set up the selection variables. let r1: number; let r2: number; let c1: number; let c2: number; let cr: number; let cc: number; let clear: SelectionModel.ClearMode; // Select or resize as needed. if (event.shiftKey) { r1 = cs ? cs.r1 : 0; r2 = cs ? cs.r2 + n : 0; c1 = cs ? cs.c1 : 0; c2 = cs ? cs.c2 : 0; cr = r; cc = c; clear = 'current'; } else { r1 = cs ? cs.r1 + n : 0; r2 = r1; c1 = c; c2 = c; cr = r1; cc = c; clear = 'all'; } // Create the new selection. model.select({ r1, c1, r2, c2, cursorRow: cr, cursorColumn: cc, clear }); // Re-fetch the current selection. cs = model.currentSelection(); // Bail if there is no selection. if (!cs) { return; } // Scroll the grid appropriately. grid.scrollToRow(cs.r2); } /** * Handle the `'Escape'` key press for the data grid. * * @param grid - The data grid of interest. * * @param event - The keyboard event of interest. */ protected onEscape(grid: DataGrid, event: KeyboardEvent): void { if (grid.selectionModel) { grid.selectionModel.clear(); } } /** * Handle the `'Delete'` key press for the data grid. * * @param grid - The data grid of interest. * * @param event - The keyboard event of interest. */ protected onDelete(grid: DataGrid, event: KeyboardEvent): void { if (grid.editable && !grid.selectionModel!.isEmpty) { const dataModel = grid.dataModel as MutableDataModel; // Fetch the max row and column. let maxRow = dataModel.rowCount('body') - 1; let maxColumn = dataModel.columnCount('body') - 1; for (let s of grid.selectionModel!.selections()) { // Clamp the cell to the model bounds. let sr1 = Math.max(0, Math.min(s.r1, maxRow)); let sc1 = Math.max(0, Math.min(s.c1, maxColumn)); let sr2 = Math.max(0, Math.min(s.r2, maxRow)); let sc2 = Math.max(0, Math.min(s.c2, maxColumn)); for (let r = sr1; r <= sr2; ++r) { for (let c = sc1; c <= sc2; ++c) { dataModel.setData('body', r, c, null); } } } } } /** * Handle the `'C'` key press for the data grid. * * @param grid - The data grid of interest. * * @param event - The keyboard event of interest. */ protected onKeyC(grid: DataGrid, event: KeyboardEvent): void { // Bail early if the modifiers aren't correct for copy. if (event.shiftKey || !Platform.accelKey(event)) { return; } // Stop the event propagation. event.preventDefault(); event.stopPropagation(); // Copy the current selection to the clipboard. grid.copyToClipboard(); } private _disposed = false; }