mirror of
https://github.com/gristlabs/grist-core.git
synced 2024-10-27 20:44:07 +00:00
Support grid selection with Ctrl+Shift+Arrow (#615)
This commit is contained in:
parent
f4d2c866d2
commit
cbdffdfff8
@ -316,6 +316,20 @@ GridView.gridCommands = {
|
||||
this._shiftSelect(-1, this.cellSelector.col.end, selector.ROW,
|
||||
this.viewSection.viewFields().peekLength - 1);
|
||||
},
|
||||
ctrlShiftDown: function () {
|
||||
this._shiftSelectUntilContent(selector.COL, 1, this.cellSelector.row.end, this.getLastDataRowIndex());
|
||||
},
|
||||
ctrlShiftUp: function () {
|
||||
this._shiftSelectUntilContent(selector.COL, -1, this.cellSelector.row.end, this.getLastDataRowIndex());
|
||||
},
|
||||
ctrlShiftRight: function () {
|
||||
this._shiftSelectUntilContent(selector.ROW, 1, this.cellSelector.col.end,
|
||||
this.viewSection.viewFields().peekLength - 1);
|
||||
},
|
||||
ctrlShiftLeft: function () {
|
||||
this._shiftSelectUntilContent(selector.ROW, -1, this.cellSelector.col.end,
|
||||
this.viewSection.viewFields().peekLength - 1);
|
||||
},
|
||||
fillSelectionDown: function() { this.fillSelectionDown(); },
|
||||
selectAll: function() { this.selectAll(); },
|
||||
|
||||
@ -403,6 +417,120 @@ GridView.prototype._shiftSelect = function(step, selectObs, exemptType, maxVal)
|
||||
selectObs(newVal);
|
||||
};
|
||||
|
||||
GridView.prototype._shiftSelectUntilContent = function(type, direction, selectObs, maxVal) {
|
||||
const selection = {
|
||||
colStart: this.cellSelector.col.start(),
|
||||
colEnd: this.cellSelector.col.end(),
|
||||
rowStart: this.cellSelector.row.start(),
|
||||
rowEnd: this.cellSelector.row.end(),
|
||||
};
|
||||
|
||||
const steps = this._stepsToContent(type, direction, selection, maxVal);
|
||||
if (steps > 0) { this._shiftSelect(direction * steps, selectObs, type, maxVal); }
|
||||
}
|
||||
|
||||
GridView.prototype._stepsToContent = function (type, direction, selection, maxVal) {
|
||||
const {colEnd: colEnd, rowEnd: rowEnd} = selection;
|
||||
let selectionData;
|
||||
|
||||
const cursorCol = this.cursor.fieldIndex();
|
||||
const cursorRow = this.cursor.rowIndex();
|
||||
|
||||
if (type === selector.ROW && direction > 0) {
|
||||
if (colEnd + 1 > maxVal) { return 0; }
|
||||
|
||||
selectionData = this._selectionData({colStart: colEnd, colEnd: maxVal, rowStart: cursorRow, rowEnd: cursorRow});
|
||||
} else if (type === selector.ROW && direction < 0) {
|
||||
if (colEnd - 1 < 0) { return 0; }
|
||||
|
||||
selectionData = this._selectionData({colStart: 0, colEnd, rowStart: cursorRow, rowEnd: cursorRow});
|
||||
} else if (type === selector.COL && direction > 0) {
|
||||
if (rowEnd + 1 > maxVal) { return 0; }
|
||||
|
||||
selectionData = this._selectionData({colStart: cursorCol, colEnd: cursorCol, rowStart: rowEnd, rowEnd: maxVal});
|
||||
} else if (type === selector.COL && direction < 0) {
|
||||
if (rowEnd - 1 > maxVal) { return 0; }
|
||||
|
||||
selectionData = this._selectionData({colStart: cursorCol, colEnd: cursorCol, rowStart: 0, rowEnd});
|
||||
}
|
||||
|
||||
const {fields, rowIndices} = selectionData;
|
||||
if (type === selector.ROW && direction < 0) {
|
||||
// When moving selection left, we step through fields in reverse order.
|
||||
fields.reverse();
|
||||
}
|
||||
if (type === selector.COL && direction < 0) {
|
||||
// When moving selection up, we step through rows in reverse order.
|
||||
rowIndices.reverse();
|
||||
}
|
||||
|
||||
const colValuesByIndex = {};
|
||||
for (const field of fields) {
|
||||
const displayColId = field.displayColModel.peek().colId.peek();
|
||||
colValuesByIndex[field._index()] = this.tableModel.tableData.getColValues(displayColId);
|
||||
}
|
||||
|
||||
let steps = 0;
|
||||
|
||||
if (type === selector.ROW) {
|
||||
const rowIndex = rowIndices[0];
|
||||
const isLastColEmpty = this._isCellValueEmpty(colValuesByIndex[colEnd][rowIndex]);
|
||||
const isNextColEmpty = this._isCellValueEmpty(colValuesByIndex[colEnd + direction][rowIndex]);
|
||||
const shouldStopOnEmptyValue = !isLastColEmpty && !isNextColEmpty;
|
||||
for (let i = 1; i < fields.length; i++) {
|
||||
const hasEmptyValues = this._isCellValueEmpty(colValuesByIndex[fields[i]._index()][rowIndex]);
|
||||
if (hasEmptyValues && shouldStopOnEmptyValue) {
|
||||
return steps;
|
||||
} else if (!hasEmptyValues && !shouldStopOnEmptyValue) {
|
||||
return steps + 1;
|
||||
}
|
||||
|
||||
steps += 1;
|
||||
}
|
||||
} else {
|
||||
const colValues = colValuesByIndex[fields[0]._index()];
|
||||
const isLastRowEmpty = this._isCellValueEmpty(colValues[rowIndices[0]]);
|
||||
const isNextRowEmpty = this._isCellValueEmpty(colValues[rowIndices[1]]);
|
||||
const shouldStopOnEmptyValue = !isLastRowEmpty && !isNextRowEmpty;
|
||||
for (let i = 1; i < rowIndices.length; i++) {
|
||||
const hasEmptyValues = this._isCellValueEmpty(colValues[rowIndices[i]]);
|
||||
if (hasEmptyValues && shouldStopOnEmptyValue) {
|
||||
return steps;
|
||||
} else if (!hasEmptyValues && !shouldStopOnEmptyValue) {
|
||||
return steps + 1;
|
||||
}
|
||||
|
||||
steps += 1;
|
||||
}
|
||||
}
|
||||
|
||||
return steps;
|
||||
}
|
||||
|
||||
GridView.prototype._selectionData = function({colStart, colEnd, rowStart, rowEnd}) {
|
||||
const fields = [];
|
||||
for (let i = colStart; i <= colEnd; i++) {
|
||||
const field = this.viewSection.viewFields().at(i);
|
||||
if (!field) { continue; }
|
||||
|
||||
fields.push(field);
|
||||
}
|
||||
|
||||
const rowIndices = [];
|
||||
for (let i = rowStart; i <= rowEnd; i++) {
|
||||
const rowId = this.viewData.getRowId(i);
|
||||
if (!rowId) { continue; }
|
||||
|
||||
rowIndices.push(this.tableModel.tableData.getRowIdIndex(rowId));
|
||||
}
|
||||
|
||||
return {fields, rowIndices};
|
||||
}
|
||||
|
||||
GridView.prototype._isCellValueEmpty = function(value) {
|
||||
return value === null || value === undefined || value === '' || value === 'false';
|
||||
}
|
||||
|
||||
/**
|
||||
* Pastes the provided data at the current cursor.
|
||||
*
|
||||
@ -532,15 +660,15 @@ GridView.prototype.fillSelectionDown = function() {
|
||||
|
||||
|
||||
/**
|
||||
* Returns a GridSelection of the selected rows and cols
|
||||
* Returns a CopySelection of the selected rows and cols
|
||||
* @returns {Object} CopySelection
|
||||
*/
|
||||
GridView.prototype.getSelection = function() {
|
||||
var rowIds = [], fields = [], rowStyle = {}, colStyle = {};
|
||||
var colStart = this.cellSelector.colLower();
|
||||
var colEnd = this.cellSelector.colUpper();
|
||||
var rowStart = this.cellSelector.rowLower();
|
||||
var rowEnd = this.cellSelector.rowUpper();
|
||||
let rowIds = [], fields = [], rowStyle = {}, colStyle = {};
|
||||
let colStart = this.cellSelector.colLower();
|
||||
let colEnd = this.cellSelector.colUpper();
|
||||
let rowStart = this.cellSelector.rowLower();
|
||||
let rowEnd = this.cellSelector.rowUpper();
|
||||
|
||||
// If there is no selection, just copy/paste the cursor cell
|
||||
if (this.cellSelector.isCurrentSelectType(selector.NONE)) {
|
||||
|
@ -50,6 +50,10 @@ export type CommandName =
|
||||
| 'shiftUp'
|
||||
| 'shiftRight'
|
||||
| 'shiftLeft'
|
||||
| 'ctrlShiftDown'
|
||||
| 'ctrlShiftUp'
|
||||
| 'ctrlShiftRight'
|
||||
| 'ctrlShiftLeft'
|
||||
| 'selectAll'
|
||||
| 'copyLink'
|
||||
| 'editField'
|
||||
@ -373,6 +377,22 @@ export const groups: CommendGroupDef[] = [{
|
||||
name: 'shiftLeft',
|
||||
keys: ['Shift+Left'],
|
||||
desc: 'Adds the element to the left of the cursor to the selected range'
|
||||
}, {
|
||||
name: 'ctrlShiftDown',
|
||||
keys: ['Mod+Shift+Down'],
|
||||
desc: 'Adds all elements below the cursor to the selected range'
|
||||
}, {
|
||||
name: 'ctrlShiftUp',
|
||||
keys: ['Mod+Shift+Up'],
|
||||
desc: 'Adds all elements above the cursor to the selected range'
|
||||
}, {
|
||||
name: 'ctrlShiftRight',
|
||||
keys: ['Mod+Shift+Right'],
|
||||
desc: 'Adds all elements to the right of the cursor to the selected range'
|
||||
}, {
|
||||
name: 'ctrlShiftLeft',
|
||||
keys: ['Mod+Shift+Left'],
|
||||
desc: 'Adds all elements to the left of the cursor to the selected range'
|
||||
}, {
|
||||
name: 'selectAll',
|
||||
keys: ['Mod+A'],
|
||||
|
BIN
test/fixtures/docs/ShiftSelection.grist
vendored
Normal file
BIN
test/fixtures/docs/ShiftSelection.grist
vendored
Normal file
Binary file not shown.
213
test/nbrowser/ShiftSelection.ts
Normal file
213
test/nbrowser/ShiftSelection.ts
Normal file
@ -0,0 +1,213 @@
|
||||
import {assert, driver, Key, WebElement} from 'mocha-webdriver';
|
||||
import * as gu from 'test/nbrowser/gristUtils';
|
||||
import {setupTestSuite} from 'test/nbrowser/testUtils';
|
||||
|
||||
interface CellSelection {
|
||||
/** 0-based column index. */
|
||||
colStart: number;
|
||||
|
||||
/** 0-based column index. */
|
||||
colEnd: number;
|
||||
|
||||
/** 0-based row index. */
|
||||
rowStart: number;
|
||||
|
||||
/** 0-based row index. */
|
||||
rowEnd: number;
|
||||
}
|
||||
|
||||
interface SelectionRange {
|
||||
/** 0-based index. */
|
||||
start: number;
|
||||
|
||||
/** 0-based index. */
|
||||
end: number;
|
||||
}
|
||||
|
||||
describe('ShiftSelection', function () {
|
||||
this.timeout(20000);
|
||||
const cleanup = setupTestSuite();
|
||||
gu.bigScreen();
|
||||
|
||||
before(async function() {
|
||||
const session = await gu.session().personalSite.login();
|
||||
await session.tempDoc(cleanup, 'ShiftSelection.grist');
|
||||
});
|
||||
|
||||
async function getSelectionRange(parent: WebElement, selector: string): Promise<SelectionRange|undefined> {
|
||||
let start, end;
|
||||
|
||||
let selectionActive = false;
|
||||
const elements = await parent.findAll(selector);
|
||||
for (let i = 0; i < elements.length; i++) {
|
||||
const element = elements[i];
|
||||
const isSelected = await element.matches('.selected');
|
||||
|
||||
if (isSelected && !selectionActive) {
|
||||
start = i;
|
||||
selectionActive = true;
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!isSelected && selectionActive) {
|
||||
end = i - 1;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (start === undefined) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
if (end === undefined) {
|
||||
end = elements.length - 1;
|
||||
}
|
||||
|
||||
return {
|
||||
start: start,
|
||||
end: end,
|
||||
};
|
||||
}
|
||||
|
||||
async function getSelectedCells(): Promise<CellSelection|undefined> {
|
||||
const activeSection = await driver.find('.active_section');
|
||||
|
||||
const colSelection = await getSelectionRange(activeSection, '.column_names .column_name');
|
||||
if (!colSelection) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const rowSelection = await getSelectionRange(activeSection, '.gridview_row .gridview_data_row_num');
|
||||
if (!rowSelection) {
|
||||
// Edge case if only a cell in the "new" row is selected
|
||||
// Not relevant for our tests
|
||||
return undefined;
|
||||
}
|
||||
|
||||
return {
|
||||
colStart: colSelection.start,
|
||||
colEnd: colSelection.end,
|
||||
rowStart: rowSelection.start,
|
||||
rowEnd: rowSelection.end,
|
||||
};
|
||||
}
|
||||
|
||||
async function assertCellSelection(expected: CellSelection|undefined) {
|
||||
const currentSelection = await getSelectedCells();
|
||||
assert.deepEqual(currentSelection, expected);
|
||||
}
|
||||
|
||||
|
||||
it('Shift+Up extends the selection up', async function () {
|
||||
await gu.getCell(1, 2).click();
|
||||
await gu.sendKeys(Key.chord(Key.SHIFT, Key.UP));
|
||||
await assertCellSelection({colStart: 1, colEnd: 1, rowStart: 0, rowEnd: 1});
|
||||
});
|
||||
|
||||
it('Shift+Down extends the selection down', async function () {
|
||||
await gu.getCell(1, 2).click();
|
||||
await gu.sendKeys(Key.chord(Key.SHIFT, Key.DOWN));
|
||||
await assertCellSelection({colStart: 1, colEnd: 1, rowStart: 1, rowEnd: 2});
|
||||
});
|
||||
|
||||
it('Shift+Left extends the selection left', async function () {
|
||||
await gu.getCell(1, 2).click();
|
||||
await gu.sendKeys(Key.chord(Key.SHIFT, Key.LEFT));
|
||||
await assertCellSelection({colStart: 0, colEnd: 1, rowStart: 1, rowEnd: 1});
|
||||
});
|
||||
|
||||
it('Shift+Right extends the selection right', async function () {
|
||||
await gu.getCell(1, 2).click();
|
||||
await gu.sendKeys(Key.chord(Key.SHIFT, Key.RIGHT));
|
||||
await assertCellSelection({colStart: 1, colEnd: 2, rowStart: 1, rowEnd: 1});
|
||||
});
|
||||
|
||||
it('Shift+Right + Shift+Left leads to the initial selection', async function () {
|
||||
await gu.getCell(1, 2).click();
|
||||
await gu.sendKeys(Key.chord(Key.SHIFT, Key.RIGHT));
|
||||
await gu.sendKeys(Key.chord(Key.SHIFT, Key.LEFT));
|
||||
await assertCellSelection({colStart: 1, colEnd: 1, rowStart: 1, rowEnd: 1});
|
||||
});
|
||||
|
||||
it('Shift+Up + Shift+Down leads to the initial selection', async function () {
|
||||
await gu.getCell(1, 2).click();
|
||||
await gu.sendKeys(Key.chord(Key.SHIFT, Key.UP));
|
||||
await gu.sendKeys(Key.chord(Key.SHIFT, Key.DOWN));
|
||||
await assertCellSelection({colStart: 1, colEnd: 1, rowStart: 1, rowEnd: 1});
|
||||
});
|
||||
|
||||
it('Ctrl+Shift+Up extends the selection blockwise up', async function () {
|
||||
await gu.getCell(5, 7).click();
|
||||
|
||||
const ctrlKey = await gu.modKey();
|
||||
|
||||
await gu.sendKeys(Key.chord(ctrlKey, Key.SHIFT, Key.UP));
|
||||
await assertCellSelection({colStart: 5, colEnd: 5, rowStart: 4, rowEnd: 6});
|
||||
|
||||
await gu.sendKeys(Key.chord(ctrlKey, Key.SHIFT, Key.UP));
|
||||
await assertCellSelection({colStart: 5, colEnd: 5, rowStart: 2, rowEnd: 6});
|
||||
|
||||
await gu.sendKeys(Key.chord(ctrlKey, Key.SHIFT, Key.UP));
|
||||
await assertCellSelection({colStart: 5, colEnd: 5, rowStart: 0, rowEnd: 6});
|
||||
});
|
||||
|
||||
it('Ctrl+Shift+Down extends the selection blockwise down', async function () {
|
||||
await gu.getCell(5, 5).click();
|
||||
|
||||
const ctrlKey = await gu.modKey();
|
||||
|
||||
await gu.sendKeys(Key.chord(ctrlKey, Key.SHIFT, Key.DOWN));
|
||||
await assertCellSelection({colStart: 5, colEnd: 5, rowStart: 4, rowEnd: 6});
|
||||
|
||||
await gu.sendKeys(Key.chord(ctrlKey, Key.SHIFT, Key.DOWN));
|
||||
await assertCellSelection({colStart: 5, colEnd: 5, rowStart: 4, rowEnd: 8});
|
||||
|
||||
await gu.sendKeys(Key.chord(ctrlKey, Key.SHIFT, Key.DOWN));
|
||||
await assertCellSelection({colStart: 5, colEnd: 5, rowStart: 4, rowEnd: 10});
|
||||
});
|
||||
|
||||
it('Ctrl+Shift+Left extends the selection blockwise left', async function () {
|
||||
await gu.getCell(6, 5).click();
|
||||
|
||||
const ctrlKey = await gu.modKey();
|
||||
|
||||
await gu.sendKeys(Key.chord(ctrlKey, Key.SHIFT, Key.LEFT));
|
||||
await assertCellSelection({colStart: 4, colEnd: 6, rowStart: 4, rowEnd: 4});
|
||||
|
||||
await gu.sendKeys(Key.chord(ctrlKey, Key.SHIFT, Key.LEFT));
|
||||
await assertCellSelection({colStart: 2, colEnd: 6, rowStart: 4, rowEnd: 4});
|
||||
|
||||
await gu.sendKeys(Key.chord(ctrlKey, Key.SHIFT, Key.LEFT));
|
||||
await assertCellSelection({colStart: 0, colEnd: 6, rowStart: 4, rowEnd: 4});
|
||||
});
|
||||
|
||||
it('Ctrl+Shift+Right extends the selection blockwise right', async function () {
|
||||
await gu.getCell(4, 5).click();
|
||||
|
||||
const ctrlKey = await gu.modKey();
|
||||
|
||||
await gu.sendKeys(Key.chord(ctrlKey, Key.SHIFT, Key.RIGHT));
|
||||
await assertCellSelection({colStart: 4, colEnd: 6, rowStart: 4, rowEnd: 4});
|
||||
|
||||
await gu.sendKeys(Key.chord(ctrlKey, Key.SHIFT, Key.RIGHT));
|
||||
await assertCellSelection({colStart: 4, colEnd: 8, rowStart: 4, rowEnd: 4});
|
||||
|
||||
await gu.sendKeys(Key.chord(ctrlKey, Key.SHIFT, Key.RIGHT));
|
||||
await assertCellSelection({colStart: 4, colEnd: 10, rowStart: 4, rowEnd: 4});
|
||||
});
|
||||
|
||||
it('Ctrl+Shift+* extends the selection until all the next cells are empty', async function () {
|
||||
await gu.getCell(3, 7).click();
|
||||
|
||||
const ctrlKey = await gu.modKey();
|
||||
|
||||
await gu.sendKeys(Key.chord(Key.SHIFT, Key.RIGHT));
|
||||
await gu.sendKeys(Key.chord(ctrlKey, Key.SHIFT, Key.UP));
|
||||
await assertCellSelection({colStart: 3, colEnd: 4, rowStart: 2, rowEnd: 6});
|
||||
|
||||
await gu.getCell(4, 7).click();
|
||||
await gu.sendKeys(Key.chord(Key.SHIFT, Key.LEFT));
|
||||
await gu.sendKeys(Key.chord(ctrlKey, Key.SHIFT, Key.UP));
|
||||
await assertCellSelection({colStart: 3, colEnd: 4, rowStart: 4, rowEnd: 6});
|
||||
});
|
||||
});
|
Loading…
Reference in New Issue
Block a user