parent
3a1367a901
commit
022435103b
16
README.md
16
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
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
{
|
||||
"name": "modern_spreadsheet",
|
||||
"private": false,
|
||||
"version": "0.0.29",
|
||||
"version": "0.0.31",
|
||||
"exports": {
|
||||
".": {
|
||||
"import": "./dist/main.js",
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
|
|
|
|||
|
|
@ -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 };
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
17
src/main.ts
17
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<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);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in New Issue