diff --git a/README.md b/README.md index b61383d..d89c694 100644 --- a/README.md +++ b/README.md @@ -34,6 +34,7 @@ function loadData() { - onCellClick - onSelectionChange - onCellChange +- onCopy ### Using events examples ```ts @@ -41,15 +42,18 @@ import Spreadsheet, { SpreadsheetConstructorProperties } from "./main"; const options: SpreadsheetConstructorProperties = { onCellClick: (event, cell) => { - console.log('Cell click', event, cell) + console.log("Cell click", event, cell); }, onSelectionChange: (selection) => { - console.log("Changed selection: ", selection) + console.log("Changed selection: ", selection); }, - onCellChange(cell) { - console.log("Cell changed: ", cell) + onCellChange = (cell) => { + console.log("Cell changed: ", cell); }, -} + onCopy: (range, data, dataAsString) => { + console.log("Copy event: ", range, data, dataAsString) + } +}; const sheet = new Spreadsheet("#spreadsheet", options); ``` @@ -58,6 +62,7 @@ const sheet = new Spreadsheet("#spreadsheet", options); - ~~Rows number and columns heading render~~ - ~~Custom event functions (ex.: onSelectionChange, onCellEdit...). Full list of supported events will available on this page~~ +- ~~Copy & Paste support~~ - Rows and columns resizing - Toolbar - Context menu @@ -65,4 +70,3 @@ const sheet = new Spreadsheet("#spreadsheet", options); - Selected cell depends cells highlight - Async formulas support - Mutlisheets (?) -- Copy & Paste support diff --git a/package.json b/package.json index 7c634c1..70bfa2a 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "modern_spreadsheet", "private": false, - "version": "0.0.29", + "version": "0.0.31", "exports": { ".": { "import": "./dist/main.js", diff --git a/src/components/editor.ts b/src/components/editor.ts index 36a1606..b707083 100644 --- a/src/components/editor.ts +++ b/src/components/editor.ts @@ -55,13 +55,14 @@ export class Editor { case "Enter": { if (!this.root.selection.selectedCell) return; + this.root.changeCellValues(this.root.selection.selectedCell, { + value: this.element.value, + displayValue: this.element.value, + }); + this.root.events.dispatch({ type: EventTypes.CELL_CHANGE, cell: this.root.getCell(this.root.selection.selectedCell), - values: { - value: this.element.value, - displayValue: this.element.value, - }, }); this.hide(); diff --git a/src/components/scroller.ts b/src/components/scroller.ts index 0788416..17c2e3b 100644 --- a/src/components/scroller.ts +++ b/src/components/scroller.ts @@ -1,4 +1,4 @@ -import Spreadsheet, { CSS_PREFIX } from "../main"; +import Spreadsheet, { CSS_PREFIX, Cell, Selection } from "../main"; import { EventTypes } from "../modules/events"; import { checkEqualCellSelections } from "../utils/position"; @@ -40,6 +40,10 @@ export class Scroller { 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) { @@ -103,6 +107,7 @@ export class Scroller { private handleKeydown = (event: KeyboardEvent) => { //* Navigation + if ( ["ArrowLeft", "ArrowRight", "ArrowUp", "ArrowDown"].includes(event.key) ) { @@ -123,7 +128,7 @@ export class Scroller { if ( this.root.selection.selectedCell && this.root.selection.selectedCell.column < - this.root.config.columns.length - 1 + this.root.config.columns.length - 1 ) { this.root.selection.selectedCell.column += 1; // this.root.renderSheet(); @@ -144,7 +149,7 @@ export class Scroller { if ( this.root.selection.selectedCell && this.root.selection.selectedCell.row < - this.root.config.rows.length - 1 + this.root.config.rows.length - 1 ) { this.root.selection.selectedCell.row += 1; // this.root.renderSheet(); @@ -160,7 +165,7 @@ export class Scroller { } //* Start typings - const keysRegex = /^([a-z]|[а-я])$/; + const keysRegex = /^([a-z]|[а-я]|[0-9])$/; if (!event.metaKey && !event.ctrlKey) { //* Prevent handle shortcutrs const isPressedLetterKey = keysRegex.test(event.key.toLowerCase()); @@ -182,6 +187,46 @@ export class Scroller { 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) => { @@ -236,6 +281,7 @@ export class Scroller { this.verticalScroller = verticalScroller; this.horizontalScroller = horizontalScroller; scroller.appendChild(groupScrollers); + scroller.contentEditable = "false" scroller.classList.add(CSS_PREFIX + "scroller"); return { scroller, verticalScroller, horizontalScroller }; diff --git a/src/index.ts b/src/index.ts index e8c2d67..8d7f10a 100644 --- a/src/index.ts +++ b/src/index.ts @@ -10,6 +10,9 @@ const options: SpreadsheetConstructorProperties = { onCellChange(cell) { console.log("Cell changed: ", cell); }, + onCopy: (range, data, dataAsString) => { + console.log("Copy event: ", range, data, dataAsString) + } }; const sheet = new Spreadsheet("#spreadsheet", options); diff --git a/src/main.ts b/src/main.ts index 46b90e9..bdc1983 100644 --- a/src/main.ts +++ b/src/main.ts @@ -14,6 +14,7 @@ import { CellChangeEvent, CellClickEvent, Config, + CopyEvent, SelectionChangeEvent, ViewProperties, } from "./modules/config"; @@ -27,7 +28,8 @@ import { Row } from "./modules/row"; import { Column } from "./modules/column"; import { ColumnsBar } from "./components/columnsBar"; import { RowsBar } from "./components/rowsBar"; -import { Events } from "./modules/events"; +import { EventTypes, Events } from "./modules/events"; +import { Clipboard } from "./modules/clipboard"; /* ! Component structure @@ -46,6 +48,7 @@ export interface SpreadsheetConstructorProperties { onCellClick?: CellClickEvent | null; onSelectionChange?: SelectionChangeEvent | null; onCellChange?: CellChangeEvent | null; + onCopy?: CopyEvent | null } export const CSS_PREFIX = "modern_sc_"; @@ -65,6 +68,7 @@ export default class Spreadsheet { public selection: Selection; public cache: Cache; public events: Events; + public clipboard: Clipboard; constructor( target: string | HTMLElement, @@ -84,6 +88,7 @@ export default class Spreadsheet { 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); @@ -99,6 +104,7 @@ export default class Spreadsheet { ); this.selection = new Selection(); this.events = new Events(this); + this.clipboard = new Clipboard(this); this.data = data; this.styles = new Styles(); @@ -242,10 +248,19 @@ export default class Spreadsheet { 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); } diff --git a/src/modules/clipboard.ts b/src/modules/clipboard.ts new file mode 100644 index 0000000..1a4cd35 --- /dev/null +++ b/src/modules/clipboard.ts @@ -0,0 +1,106 @@ +import Spreadsheet, { RangeSelectionType } from "../main"; +import { Cell, CellConstructorProps, CellStyles, Position } from "./cell"; +import { EventTypes } from "./events"; + +export class Clipboard { + saved: Cell[][] | null = null; + root: Spreadsheet + constructor(root: Spreadsheet) { + this.root = root + } + + copy(data: Cell[][], range: RangeSelectionType) { + const mapedData = data.map((row) => { + return row.map((item) => { + return item.displayValue; + }).join("\t"); + }).join("\n"); + + this.saved = data; + navigator.clipboard.writeText(mapedData); + + this.root.events.dispatch({ + type: EventTypes.COPY_CELLS, + data, + dataAsString: mapedData, + range + }) + } + + paste(root: Spreadsheet, { column, row }: Position, event: ClipboardEvent) { + + if (!this.saved) { + + if(!event.clipboardData) return; + const data = event.clipboardData.getData("text") + try{ + const arr = data.split('\n').map(item => item.split('\t')) + const arrayOfCells = arr.map((innerRow) => { + return innerRow.map(item => { + const cellProps: CellConstructorProps = { + displayValue: item, + position: { + column, + row, + }, + resultValue: item, + style: new CellStyles(), + value: item + } + return new Cell(cellProps) + }) + }) + + const rowsLength = arrayOfCells.length; + const colsLength = arrayOfCells[0] ? arrayOfCells[0].length : 0; + + for (let i = 0; i < rowsLength; i++) { + for (let j = 0; j < colsLength; j++) { + const savedCell = arrayOfCells[i][j]; + + const position = { + column: column + j, + row: row + i, + }; + + const values = { + displayValue: savedCell.displayValue, + value: savedCell.value, + style: savedCell.style, + }; + + root.changeCellValues(position, values, false); + } + } + + } + catch(err) { + console.error('Cannot read clipboard. ', err) + } + + return; + } + + const rowsLength = this.saved.length; + const colsLength = this.saved[0] ? this.saved[0].length : 0; + + for (let i = 0; i < rowsLength; i++) { + for (let j = 0; j < colsLength; j++) { + const savedCell = this.saved[i][j]; + + const position = { + column: column + j, + row: row + i, + }; + + const values = { + displayValue: savedCell.displayValue, + value: savedCell.value, + style: savedCell.style, + }; + + root.changeCellValues(position, values, false); + } + } + } +} diff --git a/src/modules/config.ts b/src/modules/config.ts index 22b0257..b47cfc8 100644 --- a/src/modules/config.ts +++ b/src/modules/config.ts @@ -1,7 +1,7 @@ import { Cell } from "./cell"; import { Column } from "./column"; import { Row } from "./row"; -import { Selection } from "./selection"; +import { RangeSelectionType, Selection } from "./selection"; export interface ViewProperties { width: number; @@ -10,6 +10,7 @@ export interface ViewProperties { export type CellClickEvent = (event: MouseEvent, cell: Cell) => void; export type SelectionChangeEvent = (selection: Selection) => void; export type CellChangeEvent = (cell: Cell) => void; +export type CopyEvent = (range: RangeSelectionType, data: Cell[][], dataAsString: string) => void export type ConfigProperties = { /** Please, end it with '_' symbol. @@ -24,6 +25,7 @@ export type ConfigProperties = { onCellClick?: CellClickEvent | null; onSelectionChange?: SelectionChangeEvent | null; onCellChange?: CellChangeEvent | null; + onCopy?: CopyEvent | null }; export type SheetConfigConstructorProps = { @@ -39,9 +41,10 @@ export class Config { height: 600, }; - onCellClick: ((event: MouseEvent, cell: Cell) => void) | null = null; + onCellClick: CellClickEvent | null = null; onSelectonChange: SelectionChangeEvent | null = null; onCellChange: CellChangeEvent | null = null; + onCopy: CopyEvent | null constructor(props: ConfigProperties) { this.columns = props.columns; @@ -51,5 +54,6 @@ export class Config { this.onCellClick = props.onCellClick ?? null; this.onSelectonChange = props.onSelectionChange ?? null; this.onCellChange = props.onCellChange ?? null; + this.onCopy = props.onCopy ?? null } } diff --git a/src/modules/events.ts b/src/modules/events.ts index ab59955..3e90963 100644 --- a/src/modules/events.ts +++ b/src/modules/events.ts @@ -1,10 +1,11 @@ import { Scroller } from "../components/scroller"; -import Spreadsheet, { Cell, CellConstructorProps, Selection } from "../main"; +import Spreadsheet, { Cell, RangeSelectionType, Selection } from "../main"; export enum EventTypes { CELL_CLICK = "CELL_CLICK", SELECTION_CHANGE = "CHANGE_SELECTION", CELL_CHANGE = "CELL_CHANGE", + COPY_CELLS = "COPY_CELLS" } export type CellClickEvent = { @@ -22,13 +23,22 @@ export type ChangeSelectionEvent = { export type ChangeCellEvent = { type: EventTypes.CELL_CHANGE; cell: Cell; - values: Partial>; + enableCallback?: boolean; }; +export type CopyAction = { + type: EventTypes.COPY_CELLS; + range: RangeSelectionType; + data: Cell[][] + dataAsString: string +} + export type ActionTypes = | CellClickEvent | ChangeSelectionEvent - | ChangeCellEvent; + | ChangeCellEvent + | CopyAction; + export class Events { root: Spreadsheet; @@ -58,14 +68,20 @@ export class Events { } case EventTypes.CELL_CHANGE: { - const { cell, values } = action; + const { cell, enableCallback } = action; // //* Here may be side effects // - this.changeCellValues(cell, values); + this.changeCellValues(cell, enableCallback); break; } + case EventTypes.COPY_CELLS: { + const {data, dataAsString, range} = action + this.copy(range, data, dataAsString) + break + } + default: { break; } @@ -101,12 +117,11 @@ export class Events { this.root.renderRowsBar(); }; - private changeCellValues( - cell: Cell, - values: Partial>, - ) { - this.root.changeCellValues(cell.position, values); + private changeCellValues(cell: Cell, enableCallback: boolean = true) { + if (enableCallback) this.root.config.onCellChange?.(cell); + } - this.root.config.onCellChange?.(cell); + private copy = (range: RangeSelectionType, data: Cell[][], dataAsString: string) => { + this.root.config.onCopy?.(range, data, dataAsString); } }