mirror of
				https://github.com/gristlabs/grist-core.git
				synced 2025-06-13 20:53:59 +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 => {
 | 
			
		||||
    // If there are some frozen columns.
 | 
			
		||||
    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._scrollColumnIntoView(idx);
 | 
			
		||||
  }));
 | 
			
		||||
 | 
			
		||||
  this.isPreview = isPreview;
 | 
			
		||||
@ -302,34 +286,14 @@ GridView.gridCommands = {
 | 
			
		||||
    }
 | 
			
		||||
    this.cursor.rowIndex(this.cursor.rowIndex() - 1);
 | 
			
		||||
  },
 | 
			
		||||
  shiftDown: function() {
 | 
			
		||||
    this._shiftSelect(1, this.cellSelector.row.end, selector.COL, this.getLastDataRowIndex());
 | 
			
		||||
  },
 | 
			
		||||
  shiftUp: function() {
 | 
			
		||||
    this._shiftSelect(-1, this.cellSelector.row.end, selector.COL, this.getLastDataRowIndex());
 | 
			
		||||
  },
 | 
			
		||||
  shiftRight: function() {
 | 
			
		||||
    this._shiftSelect(1, this.cellSelector.col.end, selector.ROW,
 | 
			
		||||
                      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);
 | 
			
		||||
  },
 | 
			
		||||
  shiftDown: function() { this._shiftSelect({step: 1, direction: 'down'}); },
 | 
			
		||||
  shiftUp: function() { this._shiftSelect({step: 1, direction: 'up'}); },
 | 
			
		||||
  shiftRight: function() { this._shiftSelect({step: 1, direction: 'right'}); },
 | 
			
		||||
  shiftLeft: function() { this._shiftSelect({step: 1, direction: 'left'}); },
 | 
			
		||||
  ctrlShiftDown: function () { this._shiftSelectUntilFirstOrLastNonEmptyCell({direction: 'down'}); },
 | 
			
		||||
  ctrlShiftUp: function () { this._shiftSelectUntilFirstOrLastNonEmptyCell({direction: 'up'}); },
 | 
			
		||||
  ctrlShiftRight: function () { this._shiftSelectUntilFirstOrLastNonEmptyCell({direction: 'right'}); },
 | 
			
		||||
  ctrlShiftLeft: function () { this._shiftSelectUntilFirstOrLastNonEmptyCell({direction: 'left'}); },
 | 
			
		||||
  fillSelectionDown: function() { this.fillSelectionDown(); },
 | 
			
		||||
  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.
 | 
			
		||||
 * @param {integer} step - amount to increase/decrease the select bound
 | 
			
		||||
 * @param {Observable} selectObs - observable to change
 | 
			
		||||
 * @exemptType {Selector type string} - selector type to noop on
 | 
			
		||||
     IE: Shift + Up/Down should noop if columns are selected. And vice versa for rows.
 | 
			
		||||
 * @param {integer} maxVal - maximum value allowed for the selectObs
 | 
			
		||||
 **/
 | 
			
		||||
GridView.prototype._shiftSelect = function(step, selectObs, exemptType, maxVal) {
 | 
			
		||||
  console.assert(exemptType === selector.ROW || exemptType === selector.COL);
 | 
			
		||||
  if (this.cellSelector.isCurrentSelectType(exemptType)) return;
 | 
			
		||||
 */
 | 
			
		||||
GridView.prototype._shiftSelect = function({step, direction}) {
 | 
			
		||||
  const type = ['up', 'down'].includes(direction) ? selector.ROW : selector.COL;
 | 
			
		||||
  const exemptType = type === selector.ROW ? selector.COL : selector.ROW;
 | 
			
		||||
  if (this.cellSelector.isCurrentSelectType(exemptType)) { return; }
 | 
			
		||||
 | 
			
		||||
  if (this.cellSelector.isCurrentSelectType(selector.NONE)) {
 | 
			
		||||
    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);
 | 
			
		||||
  if (type === 'row') {
 | 
			
		||||
    this.scrolly.scrollRowIntoView(newVal);
 | 
			
		||||
  } else {
 | 
			
		||||
    this._scrollColumnIntoView(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); }
 | 
			
		||||
/**
 | 
			
		||||
 * Shifts the current selection in the specified `direction` until the first or last
 | 
			
		||||
 * non-empty cell.
 | 
			
		||||
 *
 | 
			
		||||
 * If the current selection ends on an empty cell, the selection will be shifted to
 | 
			
		||||
 * the first non-empty cell in the specified direction. Otherwise, the selection
 | 
			
		||||
 * will be shifted to the last non-empty cell.
 | 
			
		||||
 */
 | 
			
		||||
GridView.prototype._shiftSelectUntilFirstOrLastNonEmptyCell = function({direction}) {
 | 
			
		||||
  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;
 | 
			
		||||
  let selectionData;
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Gets the number of rows/columns until the first or last non-empty cell in the specified
 | 
			
		||||
 * `direction`.
 | 
			
		||||
 */
 | 
			
		||||
GridView.prototype._stepsToContent = function ({direction}) {
 | 
			
		||||
  const colEnd = this.cellSelector.col.end();
 | 
			
		||||
  const rowEnd = this.cellSelector.row.end();
 | 
			
		||||
  const cursorCol = this.cursor.fieldIndex();
 | 
			
		||||
  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) {
 | 
			
		||||
    if (colEnd + 1 > maxVal) { return 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; }
 | 
			
		||||
 | 
			
		||||
    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: colEnd, colEnd: maxVal, rowStart: cursorRow, rowEnd: cursorRow});
 | 
			
		||||
      break;
 | 
			
		||||
    }
 | 
			
		||||
    case 'left': {
 | 
			
		||||
      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: 0, colEnd, rowStart: cursorRow, rowEnd: cursorRow});
 | 
			
		||||
      break;
 | 
			
		||||
    }
 | 
			
		||||
    case 'up': {
 | 
			
		||||
      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});
 | 
			
		||||
      break;
 | 
			
		||||
    }
 | 
			
		||||
    case 'down': {
 | 
			
		||||
      if (rowEnd + 1 > maxVal) { return 0; }
 | 
			
		||||
 | 
			
		||||
    selectionData = this._selectionData({colStart: cursorCol, colEnd: cursorCol, rowStart: 0, rowEnd});
 | 
			
		||||
      selectionData = this._selectionData({colStart: cursorCol, colEnd: cursorCol, rowStart: rowEnd, rowEnd: maxVal});
 | 
			
		||||
      break;
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  const {fields, rowIndices} = selectionData;
 | 
			
		||||
  if (type === selector.ROW && direction < 0) {
 | 
			
		||||
  if (direction === 'left') {
 | 
			
		||||
    // When moving selection left, we step through fields in reverse order.
 | 
			
		||||
    fields.reverse();
 | 
			
		||||
  }
 | 
			
		||||
  if (type === selector.COL && direction < 0) {
 | 
			
		||||
  if (direction === 'up') {
 | 
			
		||||
    // When moving selection up, we step through rows in reverse order.
 | 
			
		||||
    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 = {};
 | 
			
		||||
  for (const field of fields) {
 | 
			
		||||
    const displayColId = field.displayColModel.peek().colId.peek();
 | 
			
		||||
    colValuesByIndex[field._index()] = this.tableModel.tableData.getColValues(displayColId);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  // Count the number of steps until the first or last non-empty cell.
 | 
			
		||||
  let steps = 0;
 | 
			
		||||
 | 
			
		||||
  if (type === selector.ROW) {
 | 
			
		||||
  if (type === selector.COL) {
 | 
			
		||||
    // The selection is changing on the x-axis (i.e. the selected columns changed).
 | 
			
		||||
    const rowIndex = rowIndices[0];
 | 
			
		||||
    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;
 | 
			
		||||
    for (let i = 1; i < fields.length; i++) {
 | 
			
		||||
      const hasEmptyValues = this._isCellValueEmpty(colValuesByIndex[fields[i]._index()][rowIndex]);
 | 
			
		||||
@ -488,6 +488,7 @@ GridView.prototype._stepsToContent = function (type, direction, selection, maxVa
 | 
			
		||||
      steps += 1;
 | 
			
		||||
    }
 | 
			
		||||
  } else {
 | 
			
		||||
    // The selection is changing on the y-axis (i.e. the selected rows changed).
 | 
			
		||||
    const colValues = colValuesByIndex[fields[0]._index()];
 | 
			
		||||
    const isLastRowEmpty = this._isCellValueEmpty(colValues[rowIndices[0]]);
 | 
			
		||||
    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) {
 | 
			
		||||
  return ko.computed(() => {
 | 
			
		||||
    if (owner.isDisposed()) { return null; }
 | 
			
		||||
 | 
			
		||||
		Loading…
	
		Reference in New Issue
	
	Block a user