mirror of
https://github.com/gristlabs/grist-core.git
synced 2024-10-27 20:44:07 +00:00
(core) Scroll shift selection into view
Summary: Using the selection shortcuts will now scroll the selection into view. Test Plan: Manual. Reviewers: jarek Reviewed By: jarek Differential Revision: https://phab.getgrist.com/D4055
This commit is contained in:
parent
f0ef93e9de
commit
988ab47376
@ -134,23 +134,7 @@ function GridView(gristDoc, viewSectionModel, isPreview = false) {
|
|||||||
}));
|
}));
|
||||||
|
|
||||||
this.autoDispose(this.cursor.fieldIndex.subscribe(idx => {
|
this.autoDispose(this.cursor.fieldIndex.subscribe(idx => {
|
||||||
// If there are some frozen columns.
|
this._scrollColumnIntoView(idx);
|
||||||
if (this.numFrozen.peek() && idx < this.numFrozen.peek()) { return; }
|
|
||||||
|
|
||||||
const offset = this.colRightOffsets.peek().getSumTo(idx);
|
|
||||||
|
|
||||||
const rowNumsWidth = this._cornerDom.clientWidth;
|
|
||||||
const viewWidth = this.scrollPane.clientWidth - rowNumsWidth;
|
|
||||||
const fieldWidth = this.colRightOffsets.peek().getValue(idx) + 1; // +1px border
|
|
||||||
|
|
||||||
// Left and right pixel edge of 'viewport', starting from edge of row nums.
|
|
||||||
const frozenWidth = this.frozenWidth.peek();
|
|
||||||
const leftEdge = this.scrollPane.scrollLeft + frozenWidth;
|
|
||||||
const rightEdge = leftEdge + (viewWidth - frozenWidth);
|
|
||||||
|
|
||||||
//If cell doesn't fit onscreen, scroll to fit
|
|
||||||
const scrollShift = offset - gutil.clamp(offset, leftEdge, rightEdge - fieldWidth);
|
|
||||||
this.scrollPane.scrollLeft = this.scrollPane.scrollLeft + scrollShift;
|
|
||||||
}));
|
}));
|
||||||
|
|
||||||
this.isPreview = isPreview;
|
this.isPreview = isPreview;
|
||||||
@ -302,34 +286,14 @@ GridView.gridCommands = {
|
|||||||
}
|
}
|
||||||
this.cursor.rowIndex(this.cursor.rowIndex() - 1);
|
this.cursor.rowIndex(this.cursor.rowIndex() - 1);
|
||||||
},
|
},
|
||||||
shiftDown: function() {
|
shiftDown: function() { this._shiftSelect({step: 1, direction: 'down'}); },
|
||||||
this._shiftSelect(1, this.cellSelector.row.end, selector.COL, this.getLastDataRowIndex());
|
shiftUp: function() { this._shiftSelect({step: 1, direction: 'up'}); },
|
||||||
},
|
shiftRight: function() { this._shiftSelect({step: 1, direction: 'right'}); },
|
||||||
shiftUp: function() {
|
shiftLeft: function() { this._shiftSelect({step: 1, direction: 'left'}); },
|
||||||
this._shiftSelect(-1, this.cellSelector.row.end, selector.COL, this.getLastDataRowIndex());
|
ctrlShiftDown: function () { this._shiftSelectUntilFirstOrLastNonEmptyCell({direction: 'down'}); },
|
||||||
},
|
ctrlShiftUp: function () { this._shiftSelectUntilFirstOrLastNonEmptyCell({direction: 'up'}); },
|
||||||
shiftRight: function() {
|
ctrlShiftRight: function () { this._shiftSelectUntilFirstOrLastNonEmptyCell({direction: 'right'}); },
|
||||||
this._shiftSelect(1, this.cellSelector.col.end, selector.ROW,
|
ctrlShiftLeft: function () { this._shiftSelectUntilFirstOrLastNonEmptyCell({direction: 'left'}); },
|
||||||
this.viewSection.viewFields().peekLength - 1);
|
|
||||||
},
|
|
||||||
shiftLeft: function() {
|
|
||||||
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(); },
|
fillSelectionDown: function() { this.fillSelectionDown(); },
|
||||||
selectAll: function() { this.selectAll(); },
|
selectAll: function() { this.selectAll(); },
|
||||||
|
|
||||||
@ -401,81 +365,117 @@ GridView.prototype.onTableLoaded = function() {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Update the bounds of the cell selector's selected range for Shift+Direction keyboard shortcuts.
|
* Update the bounds of the cell selector's selected range for Shift+Direction keyboard shortcuts.
|
||||||
* @param {integer} step - amount to increase/decrease the select bound
|
*/
|
||||||
* @param {Observable} selectObs - observable to change
|
GridView.prototype._shiftSelect = function({step, direction}) {
|
||||||
* @exemptType {Selector type string} - selector type to noop on
|
const type = ['up', 'down'].includes(direction) ? selector.ROW : selector.COL;
|
||||||
IE: Shift + Up/Down should noop if columns are selected. And vice versa for rows.
|
const exemptType = type === selector.ROW ? selector.COL : selector.ROW;
|
||||||
* @param {integer} maxVal - maximum value allowed for the selectObs
|
if (this.cellSelector.isCurrentSelectType(exemptType)) { return; }
|
||||||
**/
|
|
||||||
GridView.prototype._shiftSelect = function(step, selectObs, exemptType, maxVal) {
|
|
||||||
console.assert(exemptType === selector.ROW || exemptType === selector.COL);
|
|
||||||
if (this.cellSelector.isCurrentSelectType(exemptType)) return;
|
|
||||||
if (this.cellSelector.isCurrentSelectType(selector.NONE)) {
|
if (this.cellSelector.isCurrentSelectType(selector.NONE)) {
|
||||||
this.cellSelector.currentSelectType(selector.CELL);
|
this.cellSelector.currentSelectType(selector.CELL);
|
||||||
}
|
}
|
||||||
var newVal = gutil.clamp(selectObs() + step, 0, maxVal);
|
let selectObs;
|
||||||
|
let maxVal;
|
||||||
|
if (type === 'row') {
|
||||||
|
selectObs = this.cellSelector.row.end;
|
||||||
|
maxVal = this.getLastDataRowIndex();
|
||||||
|
} else {
|
||||||
|
selectObs = this.cellSelector.col.end;
|
||||||
|
maxVal = this.viewSection.viewFields().peekLength - 1;
|
||||||
|
}
|
||||||
|
step = ['up', 'left'].includes(direction) ? -step : step;
|
||||||
|
const newVal = gutil.clamp(selectObs() + step, 0, maxVal);
|
||||||
selectObs(newVal);
|
selectObs(newVal);
|
||||||
|
if (type === 'row') {
|
||||||
|
this.scrolly.scrollRowIntoView(newVal);
|
||||||
|
} else {
|
||||||
|
this._scrollColumnIntoView(newVal);
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
GridView.prototype._shiftSelectUntilContent = function(type, direction, selectObs, maxVal) {
|
/**
|
||||||
const selection = {
|
* Shifts the current selection in the specified `direction` until the first or last
|
||||||
colStart: this.cellSelector.col.start(),
|
* non-empty cell.
|
||||||
colEnd: this.cellSelector.col.end(),
|
*
|
||||||
rowStart: this.cellSelector.row.start(),
|
* If the current selection ends on an empty cell, the selection will be shifted to
|
||||||
rowEnd: this.cellSelector.row.end(),
|
* the first non-empty cell in the specified direction. Otherwise, the selection
|
||||||
};
|
* will be shifted to the last non-empty cell.
|
||||||
|
*/
|
||||||
const steps = this._stepsToContent(type, direction, selection, maxVal);
|
GridView.prototype._shiftSelectUntilFirstOrLastNonEmptyCell = function({direction}) {
|
||||||
if (steps > 0) { this._shiftSelect(direction * steps, selectObs, type, maxVal); }
|
const steps = this._stepsToContent({direction});
|
||||||
|
if (steps > 0) { this._shiftSelect({step: steps, direction}); }
|
||||||
}
|
}
|
||||||
|
|
||||||
GridView.prototype._stepsToContent = function (type, direction, selection, maxVal) {
|
/**
|
||||||
const {colEnd: colEnd, rowEnd: rowEnd} = selection;
|
* Gets the number of rows/columns until the first or last non-empty cell in the specified
|
||||||
let selectionData;
|
* `direction`.
|
||||||
|
*/
|
||||||
|
GridView.prototype._stepsToContent = function ({direction}) {
|
||||||
|
const colEnd = this.cellSelector.col.end();
|
||||||
|
const rowEnd = this.cellSelector.row.end();
|
||||||
const cursorCol = this.cursor.fieldIndex();
|
const cursorCol = this.cursor.fieldIndex();
|
||||||
const cursorRow = this.cursor.rowIndex();
|
const cursorRow = this.cursor.rowIndex();
|
||||||
|
const type = ['up', 'down'].includes(direction) ? selector.ROW : selector.COL;
|
||||||
|
const maxVal = type === selector.ROW
|
||||||
|
? this.getLastDataRowIndex()
|
||||||
|
: this.viewSection.viewFields().peekLength - 1;
|
||||||
|
|
||||||
if (type === selector.ROW && direction > 0) {
|
// Get table data for the current selection plus additional data in the specified `direction`.
|
||||||
|
let selectionData;
|
||||||
|
switch (direction) {
|
||||||
|
case 'right': {
|
||||||
if (colEnd + 1 > maxVal) { return 0; }
|
if (colEnd + 1 > maxVal) { return 0; }
|
||||||
|
|
||||||
selectionData = this._selectionData({colStart: colEnd, colEnd: maxVal, rowStart: cursorRow, rowEnd: cursorRow});
|
selectionData = this._selectionData({colStart: colEnd, colEnd: maxVal, rowStart: cursorRow, rowEnd: cursorRow});
|
||||||
} else if (type === selector.ROW && direction < 0) {
|
break;
|
||||||
|
}
|
||||||
|
case 'left': {
|
||||||
if (colEnd - 1 < 0) { return 0; }
|
if (colEnd - 1 < 0) { return 0; }
|
||||||
|
|
||||||
selectionData = this._selectionData({colStart: 0, colEnd, rowStart: cursorRow, rowEnd: cursorRow});
|
selectionData = this._selectionData({colStart: 0, colEnd, rowStart: cursorRow, rowEnd: cursorRow});
|
||||||
} else if (type === selector.COL && direction > 0) {
|
break;
|
||||||
if (rowEnd + 1 > maxVal) { return 0; }
|
}
|
||||||
|
case 'up': {
|
||||||
selectionData = this._selectionData({colStart: cursorCol, colEnd: cursorCol, rowStart: rowEnd, rowEnd: maxVal});
|
|
||||||
} else if (type === selector.COL && direction < 0) {
|
|
||||||
if (rowEnd - 1 > maxVal) { return 0; }
|
if (rowEnd - 1 > maxVal) { return 0; }
|
||||||
|
|
||||||
selectionData = this._selectionData({colStart: cursorCol, colEnd: cursorCol, rowStart: 0, rowEnd});
|
selectionData = this._selectionData({colStart: cursorCol, colEnd: cursorCol, rowStart: 0, rowEnd});
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case 'down': {
|
||||||
|
if (rowEnd + 1 > maxVal) { return 0; }
|
||||||
|
|
||||||
|
selectionData = this._selectionData({colStart: cursorCol, colEnd: cursorCol, rowStart: rowEnd, rowEnd: maxVal});
|
||||||
|
break;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const {fields, rowIndices} = selectionData;
|
const {fields, rowIndices} = selectionData;
|
||||||
if (type === selector.ROW && direction < 0) {
|
if (direction === 'left') {
|
||||||
// When moving selection left, we step through fields in reverse order.
|
// When moving selection left, we step through fields in reverse order.
|
||||||
fields.reverse();
|
fields.reverse();
|
||||||
}
|
}
|
||||||
if (type === selector.COL && direction < 0) {
|
if (direction === 'up') {
|
||||||
// When moving selection up, we step through rows in reverse order.
|
// When moving selection up, we step through rows in reverse order.
|
||||||
rowIndices.reverse();
|
rowIndices.reverse();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Prepare a map of field indexes to their respective column values. We'll consult these
|
||||||
|
// values below when looking for the first (or last) non-empty cell value in the direction
|
||||||
|
// of the new selection.
|
||||||
const colValuesByIndex = {};
|
const colValuesByIndex = {};
|
||||||
for (const field of fields) {
|
for (const field of fields) {
|
||||||
const displayColId = field.displayColModel.peek().colId.peek();
|
const displayColId = field.displayColModel.peek().colId.peek();
|
||||||
colValuesByIndex[field._index()] = this.tableModel.tableData.getColValues(displayColId);
|
colValuesByIndex[field._index()] = this.tableModel.tableData.getColValues(displayColId);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Count the number of steps until the first or last non-empty cell.
|
||||||
let steps = 0;
|
let steps = 0;
|
||||||
|
if (type === selector.COL) {
|
||||||
if (type === selector.ROW) {
|
// The selection is changing on the x-axis (i.e. the selected columns changed).
|
||||||
const rowIndex = rowIndices[0];
|
const rowIndex = rowIndices[0];
|
||||||
const isLastColEmpty = this._isCellValueEmpty(colValuesByIndex[colEnd][rowIndex]);
|
const isLastColEmpty = this._isCellValueEmpty(colValuesByIndex[colEnd][rowIndex]);
|
||||||
const isNextColEmpty = this._isCellValueEmpty(colValuesByIndex[colEnd + direction][rowIndex]);
|
const isNextColEmpty = this._isCellValueEmpty(
|
||||||
|
colValuesByIndex[colEnd + (direction === 'right' ? 1 : -1)][rowIndex]);
|
||||||
const shouldStopOnEmptyValue = !isLastColEmpty && !isNextColEmpty;
|
const shouldStopOnEmptyValue = !isLastColEmpty && !isNextColEmpty;
|
||||||
for (let i = 1; i < fields.length; i++) {
|
for (let i = 1; i < fields.length; i++) {
|
||||||
const hasEmptyValues = this._isCellValueEmpty(colValuesByIndex[fields[i]._index()][rowIndex]);
|
const hasEmptyValues = this._isCellValueEmpty(colValuesByIndex[fields[i]._index()][rowIndex]);
|
||||||
@ -488,6 +488,7 @@ GridView.prototype._stepsToContent = function (type, direction, selection, maxVa
|
|||||||
steps += 1;
|
steps += 1;
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
|
// The selection is changing on the y-axis (i.e. the selected rows changed).
|
||||||
const colValues = colValuesByIndex[fields[0]._index()];
|
const colValues = colValuesByIndex[fields[0]._index()];
|
||||||
const isLastRowEmpty = this._isCellValueEmpty(colValues[rowIndices[0]]);
|
const isLastRowEmpty = this._isCellValueEmpty(colValues[rowIndices[0]]);
|
||||||
const isNextRowEmpty = this._isCellValueEmpty(colValues[rowIndices[1]]);
|
const isNextRowEmpty = this._isCellValueEmpty(colValues[rowIndices[1]]);
|
||||||
@ -1952,6 +1953,26 @@ GridView.prototype._showTooltipOnHover = function(field, isShowingTooltip) {
|
|||||||
];
|
];
|
||||||
};
|
};
|
||||||
|
|
||||||
|
GridView.prototype._scrollColumnIntoView = function(colIndex) {
|
||||||
|
// If there are some frozen columns.
|
||||||
|
if (this.numFrozen.peek() && colIndex < this.numFrozen.peek()) { return; }
|
||||||
|
|
||||||
|
const offset = this.colRightOffsets.peek().getSumTo(colIndex);
|
||||||
|
|
||||||
|
const rowNumsWidth = this._cornerDom.clientWidth;
|
||||||
|
const viewWidth = this.scrollPane.clientWidth - rowNumsWidth;
|
||||||
|
const fieldWidth = this.colRightOffsets.peek().getValue(colIndex) + 1; // +1px border
|
||||||
|
|
||||||
|
// Left and right pixel edge of 'viewport', starting from edge of row nums.
|
||||||
|
const frozenWidth = this.frozenWidth.peek();
|
||||||
|
const leftEdge = this.scrollPane.scrollLeft + frozenWidth;
|
||||||
|
const rightEdge = leftEdge + (viewWidth - frozenWidth);
|
||||||
|
|
||||||
|
// If cell doesn't fit onscreen, scroll to fit.
|
||||||
|
const scrollShift = offset - gutil.clamp(offset, leftEdge, rightEdge - fieldWidth);
|
||||||
|
this.scrollPane.scrollLeft = this.scrollPane.scrollLeft + scrollShift;
|
||||||
|
}
|
||||||
|
|
||||||
function buildStyleOption(owner, computedRule, optionName) {
|
function buildStyleOption(owner, computedRule, optionName) {
|
||||||
return ko.computed(() => {
|
return ko.computed(() => {
|
||||||
if (owner.isDisposed()) { return null; }
|
if (owner.isDisposed()) { return null; }
|
||||||
|
Loading…
Reference in New Issue
Block a user