import { RevAssertError, RevEnsureFullyInViewEnum, RevModifierKey, RevSchemaField, RevSelectionAreaTypeId, RevStartLength, RevUnreachableCaseError } from '../../../common'; import { RevFocus } from '../../components/focus/focus'; import { RevMouse } from '../../components/mouse/mouse'; import { RevLastSelectionArea } from '../../components/selection/last-selection-area'; import { RevLinedHoverCell } from '../../interfaces/lined-hover-cell'; import { RevSubgrid } from '../../interfaces/subgrid'; import { RevViewCell } from '../../interfaces/view-cell'; import { RevBehavioredColumnSettings, RevBehavioredGridSettings, RevGridSettings } from '../../settings'; import { RevUiController } from './ui-controller'; /** @internal */ export class RevSelectionUiController extends RevUiController { readonly typeName = RevSelectionUiController.typeName; /** @internal */ private _dragCaptured = false; private _lastSelectionAreaDraggable = false; /** * a millisecond value representing the previous time an autoscroll started * Probably not used */ private _sbLastAuto = 0; /** * a millisecond value representing the time the current autoscroll started * Probably not used */ private _sbAutoStart = 0; private _stepScrollDragTickTimeoutHandle: ReturnType | undefined; private _firstStepScrollDragTime: number | undefined; private _lastColumnStepScrollDragTime: number | undefined; private _lastRowStepScrollDragTime: number | undefined; override handlePointerDown(event: PointerEvent, hoverCell: RevLinedHoverCell | null | undefined) { if (hoverCell === null) { hoverCell = this.tryGetHoverCellFromMouseEvent(event); } if (hoverCell === undefined || RevLinedHoverCell.isMouseOverLine(hoverCell)) { return super.handlePointerDown(event, hoverCell); } else { const viewCell = hoverCell.viewCell; const subgrid = viewCell.subgrid; const isSelectable = subgrid.selectable; // && this.cellPropertiesBehavior.getCellProperty(cell.viewLayout.column, cell.viewLayoutRow.subgridRowIndex, 'cellSelection', subgrid); if (!isSelectable) { return super.handlePointerDown(event, hoverCell); } else { if (!viewCell.isScrollable) { return super.handlePointerDown(event, hoverCell); } else { const selectSucceeded = this.trySelectInScrollableMain(event, viewCell, false); if (!selectSucceeded) { return super.handlePointerDown(event, hoverCell); } else { return hoverCell; } } } } } override handleClick(event: MouseEvent, hoverCell: RevLinedHoverCell | null | undefined): RevLinedHoverCell | null | undefined { if (hoverCell === null) { hoverCell = this.tryGetHoverCellFromMouseEvent(event); } if (hoverCell === undefined || RevLinedHoverCell.isMouseOverLine(hoverCell)) { return super.handleClick(event, hoverCell); } else { // Only check for Fixed Column or Fixed Row select here. Rectangle select is checked for in Pointer Down let selectSucceeded: boolean; const viewCell = hoverCell.viewCell; if (viewCell.isColumnFixed) { if (!viewCell.subgrid.selectable) { selectSucceeded = false; } else { selectSucceeded = this.trySelectRowsFromCell(event, viewCell, false); } } else { if (!viewCell.isHeaderOrRowFixed || !this._mainSubgrid.selectable) { selectSucceeded = false; } else { selectSucceeded = this.trySelectColumnsFromCell(event, viewCell, false); } } if (!selectSucceeded) { return super.handleClick(event, hoverCell); } else { return hoverCell; } } } override handlePointerDragStart(event: DragEvent, hoverCell: RevLinedHoverCell | null | undefined) { let dragAllowed: boolean; if (!this._focusSelectBehavior.isMouseAddToggleExtendSelectionAreaAllowed(event)) { dragAllowed = false; } else { const gridSettings = this._gridSettings; dragAllowed = ( gridSettings.mouseAddToggleExtendSelectionAreaDragModifierKey === undefined || RevModifierKey.isDownInEvent(this._gridSettings.mouseAddToggleExtendSelectionAreaDragModifierKey, event) ); } if (!dragAllowed) { return super.handlePointerDragStart(event, hoverCell); } else { if (hoverCell === null) { hoverCell = this.tryGetHoverCellFromMouseEvent(event); } if (hoverCell === undefined || RevLinedHoverCell.isMouseOverLine(hoverCell)) { return super.handlePointerDragStart(event, hoverCell); } else { const viewCell = hoverCell.viewCell; const subgrid = viewCell.subgrid; if (!subgrid.selectable) { return super.handlePointerDragStart(event, hoverCell); } else { let selectSucceeded: boolean; if (viewCell.isHeaderOrRowFixed) { selectSucceeded = this.trySelectColumnsFromCell(event, viewCell, true); } else { if (viewCell.isColumnFixed) { selectSucceeded = this.trySelectRowsFromCell(event, viewCell, true); } else { if (viewCell.isMain) { selectSucceeded = this.trySelectInScrollableMain(event, viewCell, true); } else { selectSucceeded = false; } } } if (!selectSucceeded) { return super.handlePointerDragStart(event, hoverCell); } else { const dragType = this.getDragTypeFromSelectionLastArea(); if (dragType === undefined) { return super.handlePointerDragStart(event, hoverCell); } else { this._dragCaptured = true; this._lastSelectionAreaDraggable = true; this._mouse.setActiveDragType(dragType); return { started: true, hoverCell, }; } } } } } } override handlePointerDrag(event: PointerEvent, cell: RevLinedHoverCell | null | undefined) { if (!this._dragCaptured) { return super.handlePointerDrag(event, cell); } else { if (!this._lastSelectionAreaDraggable) { return cell; } else { const lastArea = this._selection.lastArea; if (lastArea === undefined) { this.cancelDraggable(); return cell; } else { this.cancelScheduledStepScrollDrag(); const stepScrolled = this.checkStepScrollDrag(lastArea, event.offsetX, event.offsetY); if (stepScrolled) { return cell; } else { if (cell === null) { cell = this.tryGetHoverCellFromMouseEvent(event); } if (cell !== undefined) { this.tryUpdateLastSelectionArea(lastArea, cell.viewCell); } return cell; } } } } } override handlePointerDragEnd(event: PointerEvent, cell: RevLinedHoverCell | null | undefined): RevLinedHoverCell | null | undefined { if (!this._dragCaptured) { return super.handlePointerDragEnd(event, cell); } else { this.cancelDraggable(); this._dragCaptured = false; return cell; } } override handleKeyDown(event: KeyboardEvent, fromEditor: boolean) { if (RevFocus.isNavActionKeyboardKey(event.key as RevFocus.ActionKeyboardKey)) { if (RevGridSettings.isExtendLastSelectionAreaModifierKeyDownInEvent(this._gridSettings, event)) { if (this._focusSelectBehavior.tryExtendLastSelectionAreaAsCloseAsPossibleToFocus()) { this.pingAutoScroll(); } } else { this._focusSelectBehavior.tryOnlySelectFocusedCell(); } } super.handleKeyDown(event, fromEditor); } private trySelectInScrollableMain( event: MouseEvent, viewCell: RevViewCell, forceAddToggleToBeAdd: boolean, ) { const allowedAreaTypeId = this._selection.calculateMouseSelectAllowedAreaTypeId(); switch (allowedAreaTypeId) { case undefined: return false; case RevSelectionAreaTypeId.rectangle: return this.trySelectRectangleFromCell(event, viewCell, forceAddToggleToBeAdd); case RevSelectionAreaTypeId.row: return this.trySelectRowsFromCell(event, viewCell, forceAddToggleToBeAdd); case RevSelectionAreaTypeId.column: return this.trySelectColumnsFromCell(event, viewCell, forceAddToggleToBeAdd); default: throw new RevUnreachableCaseError('SUCTSISM', allowedAreaTypeId); } } private trySelectRectangleFromCell(event: MouseEvent, cell: RevViewCell, forceAddToggleToBeAdd: boolean) { const gridSettings = this._gridSettings; const subgrid = cell.subgrid; const activeColumnIndex = cell.viewLayoutColumn.activeColumnIndex; const subgridRowIndex = cell.viewLayoutRow.subgridRowIndex; const selection = this._selection; const mouseAddToggleExtendSelectionAreaAllowed = this._focusSelectBehavior.isMouseAddToggleExtendSelectionAreaAllowed(event); const addToggleModifier = mouseAddToggleExtendSelectionAreaAllowed && RevGridSettings.isAddToggleSelectionAreaModifierKeyDownInEvent(gridSettings, event); const extendModifier = mouseAddToggleExtendSelectionAreaAllowed && RevGridSettings.isExtendLastSelectionAreaModifierKeyDownInEvent(gridSettings, event); const lastArea = this._selection.lastArea; if (extendModifier && !addToggleModifier) { if (lastArea !== undefined && lastArea.areaTypeId === RevSelectionAreaTypeId.rectangle) { const inclusiveFirstCorner = lastArea.inclusiveFirst; const startLengthX = RevStartLength.createFromInclusiveFirstLast(inclusiveFirstCorner.x, activeColumnIndex); const startLengthY = RevStartLength.createFromInclusiveFirstLast(inclusiveFirstCorner.y, subgridRowIndex); selection.replaceLastAreaWithRectangle( startLengthX.start, startLengthY.start, startLengthX.length, startLengthY.length, subgrid, ); } else { selection.selectCell(activeColumnIndex, subgridRowIndex, subgrid); } } else { if (addToggleModifier && !extendModifier) { if (gridSettings.addToggleSelectionAreaModifierKeyDoesToggle && !forceAddToggleToBeAdd) { selection.toggleSelectCell(activeColumnIndex, subgridRowIndex, subgrid); } else { selection.selectCell(activeColumnIndex, subgridRowIndex, subgrid); } } else { this.onlySelectCell(activeColumnIndex, subgridRowIndex, subgrid); } } return true; } private trySelectColumnsFromCell(event: MouseEvent, cell: RevViewCell, forceAddToggleToBeAdd: boolean) { const gridSettings = this._gridSettings; const allowed = gridSettings.mouseColumnSelectionEnabled && ( gridSettings.mouseColumnSelectionModifierKey === undefined || RevModifierKey.isDownInEvent(gridSettings.mouseColumnSelectionModifierKey, event) ); if (!allowed) { return false; } else { const activeColumnIndex = cell.viewLayoutColumn.activeColumnIndex; const mouseAddToggleExtendSelectionAreaAllowed = this._focusSelectBehavior.isMouseAddToggleExtendSelectionAreaAllowed(event); const addToggleModifier = mouseAddToggleExtendSelectionAreaAllowed && RevGridSettings.isAddToggleSelectionAreaModifierKeyDownInEvent(gridSettings, event); const extendModifier = mouseAddToggleExtendSelectionAreaAllowed && RevGridSettings.isExtendLastSelectionAreaModifierKeyDownInEvent(gridSettings, event); const lastArea = this._selection.lastArea; const focusSelectionBehavior = this._focusSelectBehavior; if (extendModifier && !addToggleModifier) { if (lastArea !== undefined && lastArea.areaTypeId === RevSelectionAreaTypeId.column) { const inclusiveFirstCorner = lastArea.inclusiveFirst; const startLengthX = RevStartLength.createFromInclusiveFirstLast(inclusiveFirstCorner.x, activeColumnIndex); this._selection.replaceLastAreaWithColumns(startLengthX.start, startLengthX.length); } else { focusSelectionBehavior.selectColumn(activeColumnIndex); } } else { if (addToggleModifier && !extendModifier) { if (gridSettings.addToggleSelectionAreaModifierKeyDoesToggle && !forceAddToggleToBeAdd) { focusSelectionBehavior.toggleSelectColumn(activeColumnIndex); } else { focusSelectionBehavior.selectColumn(activeColumnIndex); } } else { focusSelectionBehavior.focusOnlySelectColumn(activeColumnIndex, RevEnsureFullyInViewEnum.Always); } } return true; } } private trySelectRowsFromCell(event: MouseEvent, cell: RevViewCell, forceAddToggleToBeAdd: boolean) { const gridSettings = this._gridSettings; const allowed = gridSettings.mouseRowSelectionEnabled && ( gridSettings.mouseRowSelectionModifierKey === undefined || RevModifierKey.isDownInEvent(gridSettings.mouseRowSelectionModifierKey, event) ); if (!allowed) { return false; } else { const subgridRowIndex = cell.viewLayoutRow.subgridRowIndex; const subgrid = cell.viewLayoutRow.subgrid; const mouseAddToggleExtendSelectionAreaAllowed = this._focusSelectBehavior.isMouseAddToggleExtendSelectionAreaAllowed(event); const addToggleModifier = mouseAddToggleExtendSelectionAreaAllowed && RevGridSettings.isAddToggleSelectionAreaModifierKeyDownInEvent(gridSettings, event); const extendModifier = mouseAddToggleExtendSelectionAreaAllowed && RevGridSettings.isExtendLastSelectionAreaModifierKeyDownInEvent(gridSettings, event); const lastArea = this._selection.lastArea; const focusSelectionBehavior = this._focusSelectBehavior; if (extendModifier && !addToggleModifier) { if (lastArea !== undefined && lastArea.areaTypeId === RevSelectionAreaTypeId.row && lastArea.subgrid === subgrid) { const inclusiveFirstCorner = lastArea.inclusiveFirst; const startLengthY = RevStartLength.createFromInclusiveFirstLast(inclusiveFirstCorner.y, subgridRowIndex); this._selection.replaceLastAreaWithRows( 0, startLengthY.start, this._columnsManager.activeColumnCount, startLengthY.length, subgrid ); } else { focusSelectionBehavior.selectRow(subgridRowIndex, subgrid); } } else { if (addToggleModifier && !extendModifier) { if (gridSettings.addToggleSelectionAreaModifierKeyDoesToggle && !forceAddToggleToBeAdd) { focusSelectionBehavior.toggleSelectRow(subgridRowIndex, subgrid); } else { focusSelectionBehavior.selectRow(subgridRowIndex, subgrid); } } else { focusSelectionBehavior.focusOnlySelectRow(subgridRowIndex, subgrid, RevEnsureFullyInViewEnum.Always); } } return true; } } /** * Handle a mousedrag selection. * @param keys - array of the keys that are currently pressed down */ private tryUpdateLastSelectionArea(lastArea: RevLastSelectionArea, cell: RevViewCell) { const selection = this._selection; const subgrid = cell.subgrid; // let updatePossible: boolean; // switch (lastArea.areaType) { // case RevSelectionAreaType.Rectangle: { // updatePossible = // this.isCellAndLastAreaColumnFixedSame(cell, lastArea) && // this.isCellAndLastAreaRowFixedSame(cell, lastArea) && // subgrid === selection.subgrid // break; // } // case RevSelectionAreaType.Column: { // updatePossible = this.isCellAndLastAreaColumnFixedSame(cell, lastArea); // break; // } // case RevSelectionAreaType.Row: { // updatePossible = // this.isCellAndLastAreaColumnFixedSame(cell, lastArea) && // this.isCellAndLastAreaRowFixedSame(cell, lastArea) && // subgrid === selection.subgrid // break; // } // } const lastAreaFirstX = lastArea.inclusiveFirst.x; const lastAreaFirstXColumnFixed = lastAreaFirstX < this._gridSettings.fixedColumnCount; if (cell.isColumnFixed === lastAreaFirstXColumnFixed) { const lastAreaFirstY = lastArea.inclusiveFirst.y; const lastAreaFirstYRowFixed = lastAreaFirstY < this._gridSettings.fixedRowCount; if (cell.isRowFixed === lastAreaFirstYRowFixed) { if (lastArea.areaTypeId === RevSelectionAreaTypeId.column || lastArea.subgrid === subgrid) { const inclusiveFirstCorner = lastArea.inclusiveFirst; const xStartLength = RevStartLength.createFromInclusiveFirstLast(inclusiveFirstCorner.x, cell.viewLayoutColumn.activeColumnIndex); const yStartLength = RevStartLength.createFromInclusiveFirstLast(inclusiveFirstCorner.y, cell.viewLayoutRow.subgridRowIndex); selection.replaceLastArea( lastArea.areaTypeId, xStartLength.start, yStartLength.start, xStartLength.length, yStartLength.length, subgrid, ); } } } } // private isCellAndLastAreaColumnFixedSame(cell: RevViewCell, lastArea: LastSelectionArea) { // const lastAreaFirstX = lastArea.inclusiveFirst.x; // const lastAreaFirstXColumnFixed = lastAreaFirstX < this.gridSettings.fixedColumnCount; // return cell.isColumnFixed === lastAreaFirstXColumnFixed; // } // private isCellAndLastAreaRowFixedSame(cell: RevViewCell, lastArea: LastSelectionArea) { // const lastAreaFirstY = lastArea.inclusiveFirst.y; // const lastAreaFirstYRowFixed = lastAreaFirstY < this.gridSettings.fixedRowCount; // return cell.isRowFixed === lastAreaFirstYRowFixed; // } /** * this checks while were dragging if we go outside the visible bounds, if so, kick off the external autoscroll check function (above) */ private checkStepScrollDrag(lastArea: RevLastSelectionArea, canvasOffsetX: number, canvasOffsetY: number): boolean { const scrollableBounds = this._viewLayout.scrollableCanvasBounds; if (scrollableBounds === undefined || !this._gridSettings.scrollingEnabled) { this.cancelStepScroll(); return false; } else { const xInScrollableBounds = scrollableBounds.containsX(canvasOffsetX); const yInScrollableBounds = scrollableBounds.containsY(canvasOffsetY); if (xInScrollableBounds && yInScrollableBounds) { this.cancelStepScroll(); return false; } else { if (this._firstStepScrollDragTime === undefined) { this._firstStepScrollDragTime = performance.now(); } const firstStepScrollDragTime = this._firstStepScrollDragTime; let stepScrolled = false; if (!xInScrollableBounds) { stepScrolled = this.checkStepScrollColumn(canvasOffsetX, firstStepScrollDragTime); } if (!yInScrollableBounds) { if (this.checkStepScrollRow(canvasOffsetY, firstStepScrollDragTime)) { stepScrolled = true; } } if (!stepScrolled) { this.cancelStepScroll(); return false; } else { this.scheduleStepScrollDragTick(canvasOffsetX, canvasOffsetY); const cell = this._viewLayout.findScrollableCellClosestToCanvasOffset(canvasOffsetX, canvasOffsetY); if (cell !== undefined) { this.tryUpdateLastSelectionArea(lastArea, cell); // update the selection } return true; } } } } /** * If we are holding down the same navigation key, accelerate the increment we scroll */ private getAutoScrollAcceleration() { const elapsed = this.getAutoScrollDuration() / 2000; const count = Math.max(1, Math.floor(elapsed * elapsed * elapsed * elapsed)); return count; } /** * set the start time to right now when we initiate an auto scroll */ private setAutoScrollStartTime() { this._sbAutoStart = Date.now(); } /** * update the autoscroll start time if we haven't autoscrolled within the last 500ms otherwise update the current autoscroll time */ private pingAutoScroll() { const now = Date.now(); if (now - this._sbLastAuto > 500) { this.setAutoScrollStartTime(); } this._sbLastAuto = Date.now(); } /** * answer how long we have been auto scrolling */ private getAutoScrollDuration() { if (Date.now() - this._sbLastAuto > 500) { return 0; } return Date.now() - this._sbAutoStart; } private scheduleStepScrollDragTick(canvasOffsetX: number, canvasOffsetY: number) { this._stepScrollDragTickTimeoutHandle = setTimeout( () => { this._stepScrollDragTickTimeoutHandle = undefined; const lastArea = this._selection.lastArea; if (lastArea === undefined) { this.cancelDraggable(); } else { this.checkStepScrollDrag(lastArea, canvasOffsetX, canvasOffsetY); } }, RevSelectionUiController.scheduleStepScrollDragTickInterval ); } private cancelScheduledStepScrollDrag() { if (this._stepScrollDragTickTimeoutHandle !== undefined) { clearTimeout(this._stepScrollDragTickTimeoutHandle); this._stepScrollDragTickTimeoutHandle = undefined; } } private cancelStepScroll() { this.cancelScheduledStepScrollDrag(); this._firstStepScrollDragTime = undefined; this._lastColumnStepScrollDragTime = undefined; this._lastRowStepScrollDragTime = undefined; } private checkStepScrollColumn(directionCanvasOffsetX: number, firstStepScrollDragTime: number) { const nowTime = performance.now(); let actualStep: boolean; if (this._lastColumnStepScrollDragTime === undefined) { actualStep = true; } else { const steppingTime = nowTime - firstStepScrollDragTime; let stepInterval: number; if (steppingTime >= 4000 ) { stepInterval = 150; } else { if (steppingTime >= 3000 ) { stepInterval = 250; } else { if (steppingTime >= 2200) { stepInterval = 400; } else { if (steppingTime >= 1200) { stepInterval = 500; } else { stepInterval = 600; } } } } const nextStepTime = this._lastColumnStepScrollDragTime + stepInterval; actualStep = nowTime >= nextStepTime; } let stepped: boolean; if (actualStep) { this._lastColumnStepScrollDragTime = nowTime; stepped = this._focusScrollBehavior.tryStepScrollColumn(directionCanvasOffsetX); } else { stepped = true; // dummy step } return stepped; } private checkStepScrollRow(directionCanvasOffsetY: number, firstStepScrollDragTime: number) { const nowTime = performance.now(); let actualStep: boolean; if (this._lastRowStepScrollDragTime === undefined) { actualStep = true; } else { const steppingTime = nowTime - firstStepScrollDragTime; let stepInterval: number; if (steppingTime >= 3500 ) { stepInterval = 35; } else { if (steppingTime >= 2800 ) { stepInterval = 50; } else { if (steppingTime >= 2000 ) { stepInterval = 90; } else { if (steppingTime >= 1100) { stepInterval = 130; } else { if (steppingTime >= 500) { stepInterval = 200; } else { stepInterval = 250; } } } } } const nextStepTime = this._lastRowStepScrollDragTime + stepInterval; actualStep = nowTime >= nextStepTime; } let stepped: boolean; if (actualStep) { this._lastRowStepScrollDragTime = nowTime; stepped = this._focusScrollBehavior.tryStepScrollRow(directionCanvasOffsetY); } else { stepped = true; // dummy step } return stepped; } private onlySelectCell(activeColumnIndex: number, subgridRowIndex: number, subgrid: RevSubgrid) { let lastActiveColumnIndex = this._columnsManager.activeColumnCount - 1; let lastSubgridRowIndex = subgrid.getRowCount() - 1; if (subgrid.scrollable && !this._gridSettings.scrollingEnabled) { const lastVisibleScrollableActiveColumnIndex = this._viewLayout.lastScrollableActiveColumnIndex; const lastVisibleScrollableSubgridRowIndex = this._viewLayout.lastScrollableRowSubgridRowIndex; if (lastVisibleScrollableActiveColumnIndex !== undefined) { lastActiveColumnIndex = Math.min(lastActiveColumnIndex, lastVisibleScrollableActiveColumnIndex); } if (lastVisibleScrollableSubgridRowIndex !== undefined) { lastSubgridRowIndex = Math.min(lastSubgridRowIndex, lastVisibleScrollableSubgridRowIndex); } } let focusLinked: boolean; const focusPoint = this._focus.current; if (focusPoint === undefined) { focusLinked = false; } else { focusLinked = focusPoint.x === activeColumnIndex && focusPoint.y === subgridRowIndex && focusPoint.subgrid === subgrid; } activeColumnIndex = Math.min(lastActiveColumnIndex, Math.max(0, activeColumnIndex)); subgridRowIndex = Math.min(lastSubgridRowIndex, Math.max(0, subgridRowIndex)); this._selection.beginChange(); this._focusSelectBehavior.focusOnlySelectCell(activeColumnIndex, subgridRowIndex, subgrid, RevEnsureFullyInViewEnum.Always); if (focusLinked) { this._selection.clearOnNextFocusChange = true; } this._selection.endChange(); } private getDragTypeFromSelectionLastArea() { const lastArea = this._selection.lastArea; if (lastArea === undefined) { return undefined; } else { switch (lastArea.areaTypeId) { case RevSelectionAreaTypeId.dynamicAll: throw new RevAssertError('SUCGDTFSLA44377'); case RevSelectionAreaTypeId.rectangle: return RevMouse.DragType.lastRectangleSelectionAreaExtending; case RevSelectionAreaTypeId.column: return RevMouse.DragType.lastColumnSelectionAreaExtending; case RevSelectionAreaTypeId.row: return RevMouse.DragType.lastRowSelectionAreaExtending; default: throw new RevUnreachableCaseError('SUBGDTFSLA59598', lastArea.areaTypeId); } } } private cancelDraggable() { this.cancelStepScroll(); this._lastSelectionAreaDraggable = false; this._mouse.setActiveDragType(undefined); } } /** @internal */ export namespace RevSelectionUiController { export const typeName = 'selection'; export const scheduleStepScrollDragTickInterval = 20; }