import { Editor } from "./components/editor"; import { Scroller } from "./components/scroller"; import { Sheet } from "./components/sheet"; import { Table } from "./components/table"; import { Toolbar } from "./components/toolbar"; import { Cell, CellConstructorProps, CellStyles, Position, SerializableCell, } from "./modules/cell"; import { CellChangeEvent, CellClickEvent, Config, CopyEvent, SelectionChangeEvent, ViewProperties, } from "./modules/config"; import { RangeSelectionType, Selection } from "./modules/selection"; import { Styles } from "./modules/styles"; import { Viewport } from "./modules/viewport"; import "./scss/main.scss"; import { createSampleData } from "./utils/createData"; import { Cache, CachedColumn, CachedRow } from "./modules/cache"; import { Row } from "./modules/row"; import { Column } from "./modules/column"; import { ColumnsBar } from "./components/columnsBar"; import { RowsBar } from "./components/rowsBar"; import { EventTypes, Events } from "./modules/events"; import { Clipboard } from "./modules/clipboard"; /* ! Component structure //* Abstract
*/ export interface SpreadsheetConstructorProperties { view?: ViewProperties; onCellClick?: CellClickEvent | null; onSelectionChange?: SelectionChangeEvent | null; onCellChange?: CellChangeEvent | null; onCopy?: CopyEvent | null } export const CSS_PREFIX = "modern_sc_"; export default class Spreadsheet { private table: Table; private scroller: Scroller; private toolbar: Toolbar; private rowsBar: RowsBar; private columnsBar: ColumnsBar; private sheet: Sheet; private editor: Editor; public styles: Styles; public config: Config; public data: Cell[][]; public viewport: Viewport; public selection: Selection; public cache: Cache; public events: Events; public clipboard: Clipboard; constructor( target: string | HTMLElement, props?: SpreadsheetConstructorProperties, ) { const data = createSampleData(40, 40); const config = this.makeConfigFromData( data, props?.view ?? { height: 600, width: 800 }, ); if (props?.view) { config.view = props.view; } this.config = new Config(config); this.config.onCellClick = props?.onCellClick ?? null; this.config.onSelectonChange = props?.onSelectionChange ?? null; this.config.onCellChange = props?.onCellChange ?? null; this.config.onCopy = props?.onCopy ?? null this.rowsBar = new RowsBar(this); this.columnsBar = new ColumnsBar(this); this.sheet = new Sheet(this); this.table = new Table(this); this.scroller = new Scroller(this); this.toolbar = new Toolbar(this); this.editor = new Editor(this); this.cache = this.getInitialCache(); this.viewport = new Viewport( this, this.scroller.getViewportBoundlingRect(), ); this.selection = new Selection(); this.events = new Events(this); this.clipboard = new Clipboard(this); this.data = data; this.styles = new Styles(); this.buildComponent(); this.setElementsPositions(); this.appendTableToTarget(target); this.renderSheet(); this.renderColumnsBar(); this.renderRowsBar(); } private setRowsBarPosition() { const top = this.columnsBar.height + this.toolbar.height; const left = 0; this.rowsBar.setElementPosition(top, left); } private setColumnsBarPosition() { const top = this.toolbar.height; const left = this.rowsBar.width; console.log(top, left); this.columnsBar.setElementPosition(top, left); } private setElementsPositions() { this.setRowsBarPosition(); this.setColumnsBarPosition(); } private getInitialCache(): Cache { const cachedCols: CachedColumn[] = []; let currentWidth = 0; for (let i = 0; i <= this.config.columns.length - 1; i++) { const col = this.config.columns[i]; currentWidth += col.width; const cacheCol = new CachedColumn({ xPos: currentWidth, colIdx: i, }); cachedCols.push(cacheCol); } const cachedRows: CachedRow[] = []; let currentHeight = 0; for (let i = 0; i <= this.config.rows.length - 1; i++) { const row = this.config.rows[i]; currentHeight += row.height; const cacheRow = new CachedRow({ yPos: currentHeight, rowIdx: i, }); cachedRows.push(cacheRow); } const cache = new Cache({ columns: cachedCols, rows: cachedRows, }); console.log("CACHE: ", cache); console.log("CONFIG: ", this.config); return cache; } private buildComponent(): void { const content = document.createElement("div"); //* Abstract content.style.top = this.columnsBarHeight + "px"; content.style.left = this.rowsBarWidth + "px"; content.appendChild(this.sheet.element); content.classList.add(CSS_PREFIX + "content"); this.table.element.appendChild(this.toolbar.element); this.table.element.appendChild(this.rowsBar.element); this.table.element.appendChild(this.columnsBar.element); this.table.element.appendChild(content); this.table.element.appendChild(this.scroller.element); this.table.element.append(this.editor.element); } /**Destroy spreadsheet DOM element. * * May be usefull when need to rerender component. */ public destroy() { this.table.element.remove(); } private appendTableToTarget(target: string | HTMLElement) { if (typeof target === "string") { const element = document.querySelector(target); if (!element) throw new Error( `Element with selector ${target} is not finded in DOM.\n Make sure it exists.`, ); element?.appendChild(this.table.element); } if (target instanceof HTMLElement) { target.append(this.table.element); } } /** Canvas rendering context 2D. * * Abble to draw on canvas with default CanvasAPI methods */ get ctx() { return this.sheet.ctx; } get viewProps() { return this.config.view; } get columnsBarHeight() { return this.columnsBar.height; } get rowsBarWidth() { return this.rowsBar.width; } get toolbarHeight() { return this.toolbar.height; } /** Focusing on interactive part of spreadsheet */ focusTable() { this.scroller.element.focus(); } getCellByCoords(x: number, y: number) { return this.sheet.getCellByCoords(x, y); } getCell(position: Position): Cell { const { column, row } = position; return this.data[row][column]; } changeCellValues( position: Position, values: Partial>, enableCallback: boolean = true ) { const { column, row } = position; this.data[row][column].changeValues(values); this.events.dispatch({ type: EventTypes.CELL_CHANGE, cell: this.data[row][column], enableCallback: enableCallback }) this.renderCell(row, column); } changeCellStyles(position: Position, styles: CellStyles) { const { column, row } = position; this.data[row][column].changeStyles(styles); this.renderCell(row, column); } applyActionToRange( range: RangeSelectionType, callback: (cell: Cell) => void, ): void { const fromRow = Math.min(range.from.row, range.to.row); const toRow = Math.max(range.from.row, range.to.row); const fromCol = Math.min(range.from.column, range.to.column); const toCol = Math.max(range.from.column, range.to.column); for (let row = fromRow; row <= toRow; row++) { for (let col = fromCol; col <= toCol; col++) { const cell = this.data[row][col]; callback(cell); } } } deleteSelectedCellsValues() { if (this.selection.selectedRange !== null) { this.applyActionToRange(this.selection.selectedRange, (cell) => { this.changeCellValues(cell.position, { displayValue: "", resultValue: "", value: "", }); }); } else { if (!this.selection.selectedCell) return; this.changeCellValues(this.selection.selectedCell, { displayValue: "", resultValue: "", value: "", }); } } showEditor(position: Position, initialString?: string) { this.editor.show(position, initialString); } renderSheet() { this.sheet.renderSheet(); } renderColumnsBar() { this.columnsBar.renderBar(); } renderRowsBar() { this.rowsBar.renderBar(); } renderCell(row: number, col: number) { this.data[row][col].render(this); } public loadData(data: Cell[][] | SerializableCell[][]): Spreadsheet { const rowsLength = data.length; const colsLength = data[0] ? this.data[0].length : 0; this.data = []; const formattedData: Cell[][] = []; for (let row = 0; row < rowsLength; row++) { const innerRow: Cell[] = []; for (let col = 0; col < colsLength; col++) { const cell = data[row][col]; innerRow.push( new Cell({ displayValue: cell.displayValue, position: cell.position, resultValue: cell.resultValue, value: cell.value, style: cell.style, }), ); } formattedData.push(innerRow); } this.data = formattedData; this.selection.selectedCell = null; this.selection.selectedRange = null; this.config = this.makeConfigFromData(formattedData, this.config.view); this.cache = this.getInitialCache(); this.scroller.updateScrollerSize(); this.viewport = new Viewport( this, this.scroller.getViewportBoundlingRect(), ); this.renderSheet(); return this; } private makeConfigFromData(data: Cell[][], view: ViewProperties): Config { const lastRowIdx = data.length - 1; const lastColIdx = data[0] ? data[0].length : 0; const rows: Row[] = []; for (let row = 0; row < lastRowIdx; row++) { rows.push( new Row({ height: 40, title: String(row), }), ); } const columns: Column[] = []; for (let col = 0; col < lastColIdx; col++) { columns.push( new Column({ width: 150, title: String(col), }), ); } const config = new Config({ view, rows, columns, onCellClick: null, }); return config; } public serializeData(): SerializableCell[][] { const rowsLength = this.data.length; const colsLength = this.data[0] ? this.data[0].length : 0; const cellsArray: SerializableCell[][] = []; for (let row = 0; row < rowsLength; row++) { const innerRow: SerializableCell[] = []; for (let col = 0; col < colsLength; col++) { innerRow.push(this.data[row][col].getSerializableCell()); } cellsArray.push(innerRow); } return cellsArray; } } export * from "./modules/cache"; export * from "./modules/cell"; export * from "./modules/column"; export * from "./modules/config"; export * from "./modules/renderBox"; export * from "./modules/row"; export * from "./modules/selection"; export * from "./modules/styles"; export * from "./modules/viewport"; export * from "./utils/createData";