Added copy&paste support

Added copy event
This commit is contained in:
Eugene 2023-07-26 21:42:27 +03:00
parent 3a1367a901
commit 022435103b
9 changed files with 223 additions and 29 deletions

View File

@ -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

View File

@ -1,7 +1,7 @@
{
"name": "modern_spreadsheet",
"private": false,
"version": "0.0.29",
"version": "0.0.31",
"exports": {
".": {
"import": "./dist/main.js",

View File

@ -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();

View File

@ -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 };

View File

@ -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);

View File

@ -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<Omit<CellConstructorProps, "position">>,
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);
}

106
src/modules/clipboard.ts Normal file
View File

@ -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);
}
}
}
}

View File

@ -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
}
}

View File

@ -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<Omit<CellConstructorProps, "position">>;
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<Omit<CellConstructorProps, "position">>,
) {
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);
}
}