table cell selection improvement (#20854)

* Support SHIFT key for table cell selection

* fix for mac

* PR comment
This commit is contained in:
Alan Ren
2022-10-14 19:44:14 -07:00
committed by GitHub
parent a773be1bba
commit 5d731fe0ad
6 changed files with 79 additions and 50 deletions

View File

@@ -3,16 +3,20 @@
import { mixin } from 'vs/base/common/objects'; import { mixin } from 'vs/base/common/objects';
import { isUndefinedOrNull } from 'vs/base/common/types'; import { isUndefinedOrNull } from 'vs/base/common/types';
import * as platform from 'vs/base/common/platform';
import { CellRangeSelector, ICellRangeSelector } from 'sql/base/browser/ui/table/plugins/cellRangeSelector'; import { CellRangeSelector, ICellRangeSelector } from 'sql/base/browser/ui/table/plugins/cellRangeSelector';
export interface ICellSelectionModelOptions { export interface ICellSelectionModelOptions {
cellRangeSelector?: any; cellRangeSelector?: any;
selectActiveCell?: boolean; /**
* Whether the grid has a row selection column. Needs to take this into account to decide the cell click's selection range.
*/
hasRowSelector?: boolean,
} }
const defaults: ICellSelectionModelOptions = { const defaults: ICellSelectionModelOptions = {
selectActiveCell: true hasRowSelector: false
}; };
interface EventTargetWithClassName extends EventTarget { interface EventTargetWithClassName extends EventTarget {
@@ -40,9 +44,8 @@ export class CellSelectionModel<T> implements Slick.SelectionModel<T, Array<Slic
public init(grid: Slick.Grid<T>) { public init(grid: Slick.Grid<T>) {
this.grid = grid; this.grid = grid;
this._handler.subscribe(this.grid.onClick, (e: DOMEvent, args: Slick.OnActiveCellChangedEventArgs<T>) => this.handleActiveCellChange(e as MouseEvent, args));
this._handler.subscribe(this.grid.onKeyDown, (e: DOMEvent) => this.handleKeyDown(e as KeyboardEvent)); this._handler.subscribe(this.grid.onKeyDown, (e: DOMEvent) => this.handleKeyDown(e as KeyboardEvent));
this._handler.subscribe(this.grid.onClick, (e: DOMEvent, args: Slick.OnClickEventArgs<T>) => this.handleIndividualCellSelection(e as MouseEvent, args)); this._handler.subscribe(this.grid.onClick, (e: DOMEvent, args: Slick.OnClickEventArgs<T>) => this.handleCellClick(e as MouseEvent, args));
this._handler.subscribe(this.grid.onHeaderClick, (e: DOMEvent, args: Slick.OnHeaderClickEventArgs<T>) => this.handleHeaderClick(e as MouseEvent, args)); this._handler.subscribe(this.grid.onHeaderClick, (e: DOMEvent, args: Slick.OnHeaderClickEventArgs<T>) => this.handleHeaderClick(e as MouseEvent, args));
this.grid.registerPlugin(this.selector); this.grid.registerPlugin(this.selector);
this._handler.subscribe(this.selector.onCellRangeSelected, (e: Event, range: Slick.Range) => this.handleCellRangeSelected(e, range, false)); this._handler.subscribe(this.selector.onCellRangeSelected, (e: Event, range: Slick.Range) => this.handleCellRangeSelected(e, range, false));
@@ -104,13 +107,8 @@ export class CellSelectionModel<T> implements Slick.SelectionModel<T, Array<Slic
} }
} }
private handleActiveCellChange(e: MouseEvent, args: Slick.OnActiveCellChangedEventArgs<T>) { private isMultiSelection(e: MouseEvent): boolean {
if (this.options.selectActiveCell && !isUndefinedOrNull(args.row) && !isUndefinedOrNull(args.cell) && !e.ctrlKey) { return platform.isMacintosh ? e.metaKey : e.ctrlKey;
this.setSelectedRanges([new Slick.Range(args.row, args.cell)]);
} else if (!this.options.selectActiveCell) {
// clear the previous selection once the cell changes
this.setSelectedRanges([]);
}
} }
private handleHeaderClick(e: MouseEvent, args: Slick.OnHeaderClickEventArgs<T>) { private handleHeaderClick(e: MouseEvent, args: Slick.OnHeaderClickEventArgs<T>) {
@@ -118,17 +116,32 @@ export class CellSelectionModel<T> implements Slick.SelectionModel<T, Array<Slic
return; return;
} }
if (!isUndefinedOrNull(args.column)) { if (!isUndefinedOrNull(args.column)) {
let columnIndex = this.grid.getColumnIndex(args.column.id!); const columnIndex = this.grid.getColumnIndex(args.column.id!);
if (this.grid.canCellBeSelected(0, columnIndex)) { const rowCount = this.grid.getDataLength();
let ranges: Array<Slick.Range>; const columnCount = this.grid.getColumns().length;
if (e.ctrlKey) { const currentActiveCell = this.grid.getActiveCell();
ranges = this.getSelectedRanges(); let newActiveCell: Slick.Cell;
ranges.push(new Slick.Range(0, columnIndex, this.grid.getDataLength() - 1, columnIndex)); if (this.options.hasRowSelector && columnIndex === 0) {
} else { // When the row selector's header is clicked, all cells should be selected
ranges = [new Slick.Range(0, columnIndex, this.grid.getDataLength() - 1, columnIndex)]; this.setSelectedRanges([new Slick.Range(0, 1, rowCount - 1, columnCount - 1)]);
// The first data cell in the view should be selected.
newActiveCell = { row: this.grid.getViewport()?.top ?? 0, cell: 1 };
} }
this.grid.setActiveCell(this.grid.getViewport()?.top ?? 0, columnIndex); else if (this.grid.canCellBeSelected(0, columnIndex)) {
this.setSelectedRanges(ranges); // When SHIFT is pressed, all the columns between active cell's column and target column should be selected
const newlySelectedRange = (e.shiftKey && currentActiveCell) ? new Slick.Range(0, currentActiveCell.cell, rowCount - 1, columnIndex)
: new Slick.Range(0, columnIndex, rowCount - 1, columnIndex);
// When CTRL is pressed, we need to merge the new selection with existing selections
const rangesToBeMerged: Slick.Range[] = this.isMultiSelection(e) ? this.getSelectedRanges() : [];
const result = this.insertIntoSelections(rangesToBeMerged, newlySelectedRange);
this.setSelectedRanges(result);
// The first data cell of the target column in the view should be selected.
newActiveCell = { row: this.grid.getViewport()?.top ?? 0, cell: columnIndex };
}
if (newActiveCell) {
this.grid.setActiveCell(newActiveCell.row, newActiveCell.cell);
} }
} }
} }
@@ -228,29 +241,35 @@ export class CellSelectionModel<T> implements Slick.SelectionModel<T, Array<Slic
return newRanges; return newRanges;
} }
private handleIndividualCellSelection(e: MouseEvent, args: Slick.OnClickEventArgs<T>) { private handleCellClick(e: MouseEvent, args: Slick.OnClickEventArgs<T>) {
if (!e.ctrlKey) { const activeCell = this.grid.getActiveCell();
return; const columns = this.grid.getColumns();
} const isRowSelectorClicked: boolean = this.options.hasRowSelector && args.cell === 0;
let ranges: Array<Slick.Range>; let newlySelectedRange: Slick.Range;
// The selection is a range when there is an active cell and the SHIFT key is pressed.
ranges = this.getSelectedRanges(); if (activeCell !== undefined && e.shiftKey) {
// When the row selector cell is clicked, the new selection is all rows from current active row to target row.
let selectedRange: Slick.Range; // Otherwise, the new selection is the cells in the rectangle between current active cell and target cell.
if (args.cell === 0) { newlySelectedRange = isRowSelectorClicked ? new Slick.Range(activeCell.row, columns.length - 1, args.row, 1)
selectedRange = new Slick.Range(args.row, 1, args.row, args.grid.getColumns().length - 1); : new Slick.Range(activeCell.row, activeCell.cell, args.row, args.cell);
} else { } else {
selectedRange = new Slick.Range(args.row, args.cell); // If the row selector cell is clicked, the new selection is all the cells in the target row.
// Otherwise, the new selection is the target cell
newlySelectedRange = isRowSelectorClicked ? new Slick.Range(args.row, 1, args.row, columns.length - 1)
: new Slick.Range(args.row, args.cell, args.row, args.cell);
} }
ranges = this.insertIntoSelections(ranges, selectedRange);
this.grid.setActiveCell(selectedRange.toRow, selectedRange.toCell); // When the CTRL key is pressed, we need to merge the new selection with the existing selections.
this.setSelectedRanges(ranges); const rangesToBeMerged: Slick.Range[] = this.isMultiSelection(e) ? this.getSelectedRanges() : [];
const result = this.insertIntoSelections(rangesToBeMerged, newlySelectedRange);
this.setSelectedRanges(result);
e.preventDefault(); // Find out the new active cell
e.stopImmediatePropagation(); // If the row selector is clicked, the first data cell in the row should be the new active cell,
// otherwise, the target cell should be the new active cell.
const newActiveCell: Slick.Cell = isRowSelectorClicked ? { cell: 1, row: args.row } : { cell: args.cell, row: args.row };
this.grid.setActiveCell(newActiveCell.row, newActiveCell.cell);
} }
private handleKeyDown(e: KeyboardEvent) { private handleKeyDown(e: KeyboardEvent) {

View File

@@ -4,17 +4,27 @@
*--------------------------------------------------------------------------------------------*/ *--------------------------------------------------------------------------------------------*/
import { FilterableColumn } from 'sql/base/browser/ui/table/interfaces'; import { FilterableColumn } from 'sql/base/browser/ui/table/interfaces';
import { mixin } from 'vs/base/common/objects';
export interface IRowNumberColumnOptions { export interface IRowNumberColumnOptions {
numberOfRows: number;
cssClass?: string; cssClass?: string;
/**
* Controls the cell selection behavior. If the value is true or not specified, the entire row will be selected when a cell is clicked,
* and when the header is clicked, the entire table will be selected. If the value is false, the auto selection will not happen.
*/
autoCellSelection?: boolean;
} }
const defaultOptions: IRowNumberColumnOptions = {
autoCellSelection: true
};
export class RowNumberColumn<T> implements Slick.Plugin<T> { export class RowNumberColumn<T> implements Slick.Plugin<T> {
private handler = new Slick.EventHandler(); private handler = new Slick.EventHandler();
private grid!: Slick.Grid<T>; private grid!: Slick.Grid<T>;
constructor(private options: IRowNumberColumnOptions) { constructor(private options?: IRowNumberColumnOptions) {
this.options = mixin(this.options, defaultOptions, false);
} }
public init(grid: Slick.Grid<T>) { public init(grid: Slick.Grid<T>) {
@@ -29,7 +39,7 @@ export class RowNumberColumn<T> implements Slick.Plugin<T> {
} }
private handleClick(e: MouseEvent, args: Slick.OnClickEventArgs<T>): void { private handleClick(e: MouseEvent, args: Slick.OnClickEventArgs<T>): void {
if (this.grid.getColumns()[args.cell].id === 'rowNumber') { if (this.grid.getColumns()[args.cell].id === 'rowNumber' && this.options.autoCellSelection) {
this.grid.setActiveCell(args.row, 1); this.grid.setActiveCell(args.row, 1);
if (this.grid.getSelectionModel()) { if (this.grid.getSelectionModel()) {
this.grid.setSelectedRows([args.row]); this.grid.setSelectedRows([args.row]);
@@ -38,7 +48,7 @@ export class RowNumberColumn<T> implements Slick.Plugin<T> {
} }
private handleHeaderClick(e: MouseEvent, args: Slick.OnHeaderClickEventArgs<T>): void { private handleHeaderClick(e: MouseEvent, args: Slick.OnHeaderClickEventArgs<T>): void {
if (args.column.id === 'rowNumber') { if (args.column.id === 'rowNumber' && this.options.autoCellSelection) {
this.grid.setActiveCell(this.grid.getViewport()?.top ?? 0, 1); this.grid.setActiveCell(this.grid.getViewport()?.top ?? 0, 1);
let selectionModel = this.grid.getSelectionModel(); let selectionModel = this.grid.getSelectionModel();
if (selectionModel) { if (selectionModel) {

View File

@@ -392,7 +392,7 @@ export class EditDataGridPanel extends GridParentComponent {
let maxHeight = this.getMaxHeight(resultSet.rowCount); let maxHeight = this.getMaxHeight(resultSet.rowCount);
let minHeight = this.getMinHeight(resultSet.rowCount); let minHeight = this.getMinHeight(resultSet.rowCount);
let rowNumberColumn = new RowNumberColumn({ numberOfRows: resultSet.rowCount }); let rowNumberColumn = new RowNumberColumn();
// Store the result set from the event // Store the result set from the event
let dataSet: IGridDataSet = { let dataSet: IGridDataSet = {

View File

@@ -191,7 +191,7 @@ export class TopOperationsTabView extends Disposable implements IPanelView {
topOperationContainer.appendChild(tableContainer); topOperationContainer.appendChild(tableContainer);
this._topOperationsContainers.push(topOperationContainer); this._topOperationsContainers.push(topOperationContainer);
const rowNumberColumn = new RowNumberColumn({ numberOfRows: dataMap.length }); const rowNumberColumn = new RowNumberColumn({ autoCellSelection: false });
columns.unshift(rowNumberColumn.getColumnDefinition()); columns.unshift(rowNumberColumn.getColumnDefinition());
let copyHandler = new CopyKeybind<any>(); let copyHandler = new CopyKeybind<any>();
@@ -234,7 +234,7 @@ export class TopOperationsTabView extends Disposable implements IPanelView {
}); });
})); }));
const selectionModel = new CellSelectionModel<Slick.SlickData>(); const selectionModel = new CellSelectionModel<Slick.SlickData>({ hasRowSelector: true });
const table = this._register(new Table<Slick.SlickData>(tableContainer, { const table = this._register(new Table<Slick.SlickData>(tableContainer, {
columns: columns, columns: columns,

View File

@@ -353,7 +353,7 @@ export abstract class GridTableBase<T> extends Disposable implements IView {
private table: Table<T>; private table: Table<T>;
private actionBar: ActionBar; private actionBar: ActionBar;
private container = document.createElement('div'); private container = document.createElement('div');
private selectionModel = new CellSelectionModel<T>(); private selectionModel = new CellSelectionModel<T>({ hasRowSelector: true });
private styles: ITableStyles; private styles: ITableStyles;
private currentHeight: number; private currentHeight: number;
private dataProvider: HybridDataProvider<T>; private dataProvider: HybridDataProvider<T>;
@@ -498,7 +498,7 @@ export abstract class GridTableBase<T> extends Disposable implements IView {
collection.setCollectionChangedCallback((startIndex, count) => { collection.setCollectionChangedCallback((startIndex, count) => {
this.renderGridDataRowsRange(startIndex, count); this.renderGridDataRowsRange(startIndex, count);
}); });
this.rowNumberColumn = new RowNumberColumn({ numberOfRows: this.resultSet.rowCount }); this.rowNumberColumn = new RowNumberColumn({ autoCellSelection: false });
this.columns.unshift(this.rowNumberColumn.getColumnDefinition()); this.columns.unshift(this.rowNumberColumn.getColumnDefinition());
let tableOptions: Slick.GridOptions<T> = { let tableOptions: Slick.GridOptions<T> = {
rowHeight: this.rowHeight, rowHeight: this.rowHeight,

View File

@@ -49,7 +49,7 @@ export function renderDataResource(
// In order to show row numbers, we need to put the row number column // In order to show row numbers, we need to put the row number column
// ahead of all of the other columns, and register the plugin below // ahead of all of the other columns, and register the plugin below
let rowNumberColumn = new RowNumberColumn({ numberOfRows: source.length }); let rowNumberColumn = new RowNumberColumn();
columnsTransformed.unshift(rowNumberColumn.getColumnDefinition()); columnsTransformed.unshift(rowNumberColumn.getColumnDefinition());
let transformedData = transformData(sourceObject.data, columnsTransformed); let transformedData = transformData(sourceObject.data, columnsTransformed);