Skip to content

Commit

Permalink
grid selection with Ctrl+Shift+Arrow
Browse files Browse the repository at this point in the history
  • Loading branch information
Ocarthon committed Aug 11, 2023
1 parent 8848035 commit 76b3844
Show file tree
Hide file tree
Showing 4 changed files with 422 additions and 39 deletions.
233 changes: 194 additions & 39 deletions app/client/components/GridView.js
Original file line number Diff line number Diff line change
Expand Up @@ -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(); },

Expand Down Expand Up @@ -403,6 +417,181 @@ 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 {colStart, colEnd, rowStart, rowEnd} = selection;
let selectionData;

if (type === selector.ROW && direction > 0) {
if (colEnd + 1 > maxVal) { return 0; }

selectionData = this._selectionData({colStart: colEnd, colEnd: maxVal, rowStart, rowEnd});
} else if (type === selector.ROW && direction < 0) {
if (colEnd - 1 < 0) { return 0; }

selectionData = this._selectionData({colStart: 0, colEnd, rowStart, rowEnd});
} else if (type === selector.COL && direction > 0) {
if (rowEnd + 1 > maxVal) { return 0; }

selectionData = this._selectionData({colStart, colEnd, rowStart: rowEnd, rowEnd: maxVal});
} else if (type === selector.COL && direction < 0) {
if (rowEnd - 1 > maxVal) { return 0; }

selectionData = this._selectionData({colStart, colEnd, 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 hasEmptyValuesInLastCol = this._hasEmptyValuesInCol(colEnd, rowIndices, colValuesByIndex);
const hasEmptyValuesInNextCol = this._hasEmptyValuesInCol(colEnd + direction, rowIndices, colValuesByIndex);
const shouldStopOnEmptyValue = !hasEmptyValuesInLastCol && !hasEmptyValuesInNextCol;
for (let i = 1; i < fields.length; i++) {
const hasEmptyValues = this._hasEmptyValuesInCol(fields[i]._index(), rowIndices, colValuesByIndex);
if (hasEmptyValues && shouldStopOnEmptyValue) {
return steps;
} else if (!hasEmptyValues && !shouldStopOnEmptyValue) {
return steps + 1;
}

steps += 1;
}
} else {
const hasEmptyValuesInLastRow = this._hasEmptyValuesInRow(rowIndices[0], colValuesByIndex);
const hasEmptyValuesInNextRow = this._hasEmptyValuesInRow(rowIndices[1], colValuesByIndex);
const shouldStopOnEmptyValue = !hasEmptyValuesInLastRow && !hasEmptyValuesInNextRow;
for (let i = 1; i < rowIndices.length; i++) {
const hasEmptyValues = this._hasEmptyValuesInRow(rowIndices[i], colValuesByIndex);
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._hasEmptyValuesInCol = function(colIndex, rowIndices, colValuesByIndex) {
return rowIndices.some(rowIndex => {
const value = colValuesByIndex[colIndex][rowIndex];
return value === null || value === undefined || value === '' || value === 'false';
});
}

GridView.prototype._hasEmptyValuesInRow = function(rowIndex, colValuesByIndex) {
return Object.values(colValuesByIndex).some((colValues) => {
const value = colValues[rowIndex];
return value === null || value === undefined || value === '' || value === 'false';
});
}

/**
* Returns a GridSelection of the given cell selection
*
* @param {integer} colStart
* @param {integer} colEnd
* @param {integer} rowStart
* @param {integer} rowEnd
* @returns {Object} CopySelection
*/
GridView.prototype.getCells = function(colStart, colEnd, rowStart, rowEnd) {
let rowIds = [], fields = [], rowStyle = {}, colStyle = {};

// If there is no selection, just copy/paste the cursor cell
if (this.cellSelector.isCurrentSelectType(selector.NONE)) {
rowStart = rowEnd = this.cursor.rowIndex();
colStart = colEnd = this.cursor.fieldIndex();
}

// Get all the cols if rows are selected, and viceversa
if (this.cellSelector.isCurrentSelectType(selector.ROW)) {
colStart = 0;
colEnd = this.viewSection.viewFields().peekLength - 1;
} else if(this.cellSelector.isCurrentSelectType(selector.COL)) {
rowStart = 0;
rowEnd = this.getLastDataRowIndex();
}

// Start or end will be null if no fields are visible.
if (colStart !== null && colEnd !== null) {
for(let i = colStart; i <= colEnd; i++) {
let field = this.viewSection.viewFields().at(i);
if (!field) {
continue;
}

fields.push(field);
colStyle[field.colId()] = this._getColStyle(i);
}
}

let rowId;
for(let j = rowStart; j <= rowEnd; j++) {
rowId = this.viewData.getRowId(j);
if (!rowId) {
continue;
}

rowIds.push(rowId);
rowStyle[rowId] = this._getRowStyle(j);
}
return new CopySelection(this.tableModel.tableData, rowIds, fields, {
rowStyle: rowStyle,
colStyle: colStyle
});
}

/**
* Pastes the provided data at the current cursor.
*
Expand Down Expand Up @@ -536,46 +725,12 @@ GridView.prototype.fillSelectionDown = function() {
* @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();
const colStart = this.cellSelector.colLower();
const colEnd = this.cellSelector.colUpper();
const rowStart = this.cellSelector.rowLower();
const rowEnd = this.cellSelector.rowUpper();

// If there is no selection, just copy/paste the cursor cell
if (this.cellSelector.isCurrentSelectType(selector.NONE)) {
rowStart = rowEnd = this.cursor.rowIndex();
colStart = colEnd = this.cursor.fieldIndex();
}

// Get all the cols if rows are selected, and viceversa
if (this.cellSelector.isCurrentSelectType(selector.ROW)) {
colStart = 0;
colEnd = this.viewSection.viewFields().peekLength - 1;
} else if(this.cellSelector.isCurrentSelectType(selector.COL)) {
rowStart = 0;
rowEnd = this.getLastDataRowIndex();
}

// Start or end will be null if no fields are visible.
if (colStart !== null && colEnd !== null) {
for(var i = colStart; i <= colEnd; i++) {
let field = this.viewSection.viewFields().at(i);
fields.push(field);
colStyle[field.colId()] = this._getColStyle(i);
}
}

var rowId;
for(var j = rowStart; j <= rowEnd; j++) {
rowId = this.viewData.getRowId(j);
rowIds.push(rowId);
rowStyle[rowId] = this._getRowStyle(j);
}
return new CopySelection(this.tableModel.tableData, rowIds, fields, {
rowStyle: rowStyle,
colStyle: colStyle
});
return this.getCells(colStart, colEnd, rowStart, rowEnd);
};

/**
Expand Down
20 changes: 20 additions & 0 deletions app/client/components/commandList.ts
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,10 @@ export type CommandName =
| 'shiftUp'
| 'shiftRight'
| 'shiftLeft'
| 'ctrlShiftDown'
| 'ctrlShiftUp'
| 'ctrlShiftRight'
| 'ctrlShiftLeft'
| 'selectAll'
| 'copyLink'
| 'editField'
Expand Down Expand Up @@ -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 the 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'],
Expand Down
Binary file added test/fixtures/docs/ShiftSelection.grist
Binary file not shown.
Loading

0 comments on commit 76b3844

Please sign in to comment.