import Spreadsheet, { CSS_PREFIX, Cell, Selection } from "../main"; import { EventTypes } from "../modules/events"; import { checkEqualCellSelections } from "../utils/position"; export interface ViewportRect { top: number; left: number; right: number; bottom: number; } export class Scroller { element: HTMLDivElement; private verticalScroller: HTMLDivElement; private horizontalScroller: HTMLDivElement; private root: Spreadsheet; private isSelecting = false; constructor(root: Spreadsheet) { this.root = root; const { horizontalScroller, scroller, verticalScroller } = this.buildComponent(); this.element = scroller; this.verticalScroller = verticalScroller; this.horizontalScroller = horizontalScroller; this.element.style.height = this.root.config.view.height + "px"; this.element.style.width = this.root.config.view.width + "px"; this.element.style.top = this.root.columnsBarHeight + "px"; this.element.style.left = this.root.rowsBarWidth + "px"; this.element.tabIndex = -1; this.updateScrollerSize(); //* Init size set this.element.addEventListener("scroll", this.handleScroll); this.element.addEventListener("mousedown", this.handleClick); this.element.addEventListener("mousemove", this.handleMouseMove); this.element.addEventListener("mouseup", this.handleMouseUp); this.element.addEventListener("dblclick", this.handleDoubleClick); this.element.addEventListener("keydown", this.handleKeydown); this.element.addEventListener("paste", (event) => { if (!this.root.selection.selectedCell) return; this.root.clipboard.paste( this.root, this.root.selection.selectedCell, event, ); }); } public setSelectingMode(mode: boolean) { this.isSelecting = mode; } private handleMouseMove = (event: MouseEvent) => { if (!this.isSelecting) return; const { offsetX, offsetY } = event; const lastSelectedCell = this.root.getCellByCoords(offsetX, offsetY); let isRangeChanged = false; if (this.root.selection.selectedRange) { isRangeChanged = !checkEqualCellSelections( this.root.selection.selectedRange.to, lastSelectedCell, ); if (isRangeChanged) { this.root.selection.selectedRange.to = lastSelectedCell; this.root.events.dispatch({ type: EventTypes.SELECTION_CHANGE, selection: this.root.selection, enableCallback: true, }); } } }; private handleMouseUp = () => { this.isSelecting = false; const newSelection = { ...this.root.selection }; if (this.root.selection.selectedRange) { if ( checkEqualCellSelections( this.root.selection.selectedRange.from, this.root.selection.selectedRange.to, ) ) { newSelection.selectedRange = null; this.root.events.dispatch({ type: EventTypes.SELECTION_CHANGE, selection: newSelection, enableCallback: false, }); } } this.root.renderSheet(); this.root.renderColumnsBar(); this.root.renderRowsBar(); }; private handleDoubleClick = (event: MouseEvent) => { event.preventDefault(); const position = this.root.getCellByCoords(event.offsetX, event.offsetY); this.root.showEditor(position); }; private handleKeydown = (event: KeyboardEvent) => { //* Navigation if ( ["ArrowLeft", "ArrowRight", "ArrowUp", "ArrowDown"].includes(event.key) ) { event.preventDefault(); this.root.selection.selectedRange = null; switch (event.key) { case "ArrowLeft": { if ( this.root.selection.selectedCell && this.root.selection.selectedCell.column > 0 ) { this.root.selection.selectedCell.column -= 1; // this.root.renderSheet(); } break; } case "ArrowRight": { if ( this.root.selection.selectedCell && this.root.selection.selectedCell.column < this.root.config.columns.length - 1 ) { this.root.selection.selectedCell.column += 1; // this.root.renderSheet(); } break; } case "ArrowUp": { if ( this.root.selection.selectedCell && this.root.selection.selectedCell.row > 0 ) { this.root.selection.selectedCell.row -= 1; // this.root.renderSheet(); } break; } case "ArrowDown": { if ( this.root.selection.selectedCell && this.root.selection.selectedCell.row < this.root.config.rows.length - 1 ) { this.root.selection.selectedCell.row += 1; // this.root.renderSheet(); } break; } } this.root.events.dispatch({ type: EventTypes.SELECTION_CHANGE, selection: this.root.selection, enableCallback: true, }); } //* Start typings const keysRegex = /^([a-z]|[а-я]|[0-9])$/; if (!event.metaKey && !event.ctrlKey) { //* Prevent handle shortcutrs const isPressedLetterKey = keysRegex.test(event.key.toLowerCase()); if (event.key === "F2" || isPressedLetterKey) { //* English and Russian keyboard. Or F2 button event.preventDefault(); if (!this.root.selection.selectedCell) return; this.root.showEditor( this.root.selection.selectedCell, isPressedLetterKey ? event.key : undefined, ); } } if (event.key === "Delete") { event.preventDefault(); this.root.deleteSelectedCellsValues(); this.root.renderSheet(); } if (event.metaKey || event.ctrlKey) { console.log(event.code); if (event.code === "KeyC") { let cells: Cell[][] = undefined!; const selection = new Selection(); if (this.root.selection.selectedRange) { const { from, to } = this.root.selection.selectedRange; selection.selectedRange = this.root.selection.selectedRange; const subArrByRows = this.root.data.slice(from.row, to.row + 1); const subArrByCols = subArrByRows.map((row) => { return row.slice(from.column, to.column + 1); }); cells = [...subArrByCols]; } else if (this.root.selection.selectedCell) { const { column, row } = this.root.selection.selectedCell; cells = [[this.root.data[row][column]]]; selection.selectedRange = { from: this.root.selection.selectedCell, to: this.root.selection.selectedCell, }; } else { return; } this.root.clipboard.copy(cells, selection.selectedRange); return; } if (event.code === "KeyV") { // if (!this.root.selection.selectedCell) return; // this.root.clipboard.paste(this.root, this.root.selection.selectedCell); } } }; private handleClick = (event: MouseEvent) => { this.root.events.dispatch({ type: EventTypes.CELL_CLICK, event, scroller: this, }); }; private handleScroll = () => { const rect = this.getViewportBoundlingRect(); this.root.viewport.updateValues(rect); this.root.renderSheet(); this.root.renderColumnsBar(); this.root.renderRowsBar(); }; public getViewportBoundlingRect(): ViewportRect { const { scrollTop, scrollLeft } = this.element; const { height, width } = this.element.getBoundingClientRect(); const bottom = scrollTop + height; const right = scrollLeft + width; return { top: scrollTop, left: scrollLeft, bottom, right, }; } private buildComponent() { const scroller = document.createElement("div"); const verticalScroller = document.createElement("div"); const horizontalScroller = document.createElement("div"); const groupScrollers = document.createElement("div"); const stack = document.createElement("div"); verticalScroller.style.width = "0px"; verticalScroller.style.pointerEvents = "none"; horizontalScroller.style.pointerEvents = "none"; groupScrollers.style.display = "flex"; stack.appendChild(verticalScroller); stack.appendChild(horizontalScroller); groupScrollers.appendChild(stack); this.verticalScroller = verticalScroller; this.horizontalScroller = horizontalScroller; scroller.appendChild(groupScrollers); scroller.contentEditable = "false"; scroller.classList.add(CSS_PREFIX + "scroller"); return { scroller, verticalScroller, horizontalScroller }; } private getActualHeight() { return this.root.config.rows.reduce((acc, curr) => { acc += curr.height; return acc; }, 0); } private getActualWidth() { return this.root.config.columns.reduce((acc, curr) => { acc += curr.width; return acc; }, 0); } updateScrollerSize() { const totalHeight = this.getActualHeight(); const totalWidth = this.getActualWidth(); this.setScrollerHeight(totalHeight); this.setScrollerWidth(totalWidth); } private setScrollerHeight(height: number) { this.verticalScroller.style.height = height + "px"; } private setScrollerWidth(width: number) { this.horizontalScroller.style.width = width + "px"; } }