Compare commits

..

No commits in common. "main" and "events" have entirely different histories.
main ... events

34 changed files with 1496 additions and 1981 deletions

View File

@ -1,15 +1,9 @@
# Modern Spreadsheet # Modern Spreadsheet
<img src="https://raw.githubusercontent.com/yazmeyaa/modern_spreadsheet/6dc20f92e769210c076600c7fcfacd4ed528f085/repo_assets/spreadsheet_preview.png?raw=true" alt="spreadsheet_preview">
## Features:
- High performance spreadsheet based on CanvasAPI. - High performance spreadsheet based on CanvasAPI.
- TypeScript supported - TypeScript supported
- Native scrolling
- Customizable
- Copy & Paste support
### Basic usage ## Basic usage
```ts ```ts
import Spreadsheet from "modern_spreadsheet"; import Spreadsheet from "modern_spreadsheet";
@ -20,7 +14,7 @@ const sheet = new Spreadsheet(target);
//... //...
``` ```
### Save and load data ## Save and load data
```ts ```ts
function saveData() { function saveData() {
@ -36,11 +30,10 @@ function loadData() {
} }
``` ```
#### Supported events ## Supported events
- onCellClick - onCellClick
- onSelectionChange - onSelectionChange
- onCellChange - onCellChange
- onCopy
### Using events examples ### Using events examples
```ts ```ts
@ -48,27 +41,23 @@ import Spreadsheet, { SpreadsheetConstructorProperties } from "./main";
const options: SpreadsheetConstructorProperties = { const options: SpreadsheetConstructorProperties = {
onCellClick: (event, cell) => { onCellClick: (event, cell) => {
console.log("Cell click", event, cell); console.log('Cell click', event, cell)
}, },
onSelectionChange: (selection) => { onSelectionChange: (selection) => {
console.log("Changed selection: ", selection); console.log("Changed selection: ", selection)
}, },
onCellChange = (cell) => { onCellChange(cell) {
console.log("Cell changed: ", cell); console.log("Cell changed: ", cell)
}, },
onCopy: (range, data, dataAsString) => { }
console.log("Copy event: ", range, data, dataAsString)
}
};
const sheet = new Spreadsheet("#spreadsheet", options); const sheet = new Spreadsheet("#spreadsheet", options);
``` ```
### Roadmap ## Roadmap
- ~~Rows number and columns heading render~~ - ~~Rows number and columns heading render~~
- ~~Custom event functions (ex.: onSelectionChange, onCellEdit...). Full list of supported events will available on this page~~ - ~~Custom event functions (ex.: onSelectionChange, onCellEdit...). Full list of supported events will available on this page~~
- ~~Copy & Paste support~~
- Rows and columns resizing - Rows and columns resizing
- Toolbar - Toolbar
- Context menu - Context menu
@ -76,3 +65,4 @@ const sheet = new Spreadsheet("#spreadsheet", options);
- Selected cell depends cells highlight - Selected cell depends cells highlight
- Async formulas support - Async formulas support
- Mutlisheets (?) - Mutlisheets (?)
- Copy & Paste support

View File

@ -12,7 +12,6 @@ export declare class Scroller {
private root; private root;
private isSelecting; private isSelecting;
constructor(root: Spreadsheet); constructor(root: Spreadsheet);
setSelectingMode(mode: boolean): void;
private handleMouseMove; private handleMouseMove;
private handleMouseUp; private handleMouseUp;
private handleDoubleClick; private handleDoubleClick;

View File

@ -10,8 +10,5 @@ export declare class Sheet {
constructor(root: Spreadsheet); constructor(root: Spreadsheet);
getCellByCoords(x: number, y: number): Position; getCellByCoords(x: number, y: number): Position;
renderCell(position: Position): void; renderCell(position: Position): void;
private getSelectionRange;
private renderSelectionRange;
renderSelection(): void;
renderSheet(): void; renderSheet(): void;
} }

8
dist/main.cjs vendored

File diff suppressed because one or more lines are too long

2
dist/main.cjs.map vendored

File diff suppressed because one or more lines are too long

16
dist/main.d.ts vendored
View File

@ -1,18 +1,13 @@
import { Cell, CellConstructorProps, CellStyles, Position, SerializableCell } from "./modules/cell"; import { Cell, CellConstructorProps, CellStyles, Position, SerializableCell } from "./modules/cell";
import { CellChangeEvent, CellClickEvent, Config, CopyEvent, SelectionChangeEvent, ViewProperties } from "./modules/config"; import { Config, ViewProperties } from "./modules/config";
import { RangeSelectionType, Selection } from "./modules/selection"; import { RangeSelectionType, Selection } from "./modules/selection";
import { Styles } from "./modules/styles"; import { Styles } from "./modules/styles";
import { Viewport } from "./modules/viewport"; import { Viewport } from "./modules/viewport";
import "./scss/main.scss"; import "./scss/main.scss";
import { Cache } from "./modules/cache"; import { Cache } from "./modules/cache";
import { Events } from "./modules/events"; interface SpreadsheetConstructorProperties {
import { Clipboard } from "./modules/clipboard"; config?: Omit<Config, "view">;
export interface SpreadsheetConstructorProperties {
view?: ViewProperties; view?: ViewProperties;
onCellClick?: CellClickEvent | null;
onSelectionChange?: SelectionChangeEvent | null;
onCellChange?: CellChangeEvent | null;
onCopy?: CopyEvent | null;
} }
export declare const CSS_PREFIX = "modern_sc_"; export declare const CSS_PREFIX = "modern_sc_";
export default class Spreadsheet { export default class Spreadsheet {
@ -29,8 +24,6 @@ export default class Spreadsheet {
viewport: Viewport; viewport: Viewport;
selection: Selection; selection: Selection;
cache: Cache; cache: Cache;
events: Events;
clipboard: Clipboard;
constructor(target: string | HTMLElement, props?: SpreadsheetConstructorProperties); constructor(target: string | HTMLElement, props?: SpreadsheetConstructorProperties);
private setRowsBarPosition; private setRowsBarPosition;
private setColumnsBarPosition; private setColumnsBarPosition;
@ -56,13 +49,12 @@ export default class Spreadsheet {
focusTable(): void; focusTable(): void;
getCellByCoords(x: number, y: number): Position; getCellByCoords(x: number, y: number): Position;
getCell(position: Position): Cell; getCell(position: Position): Cell;
changeCellValues(position: Position, values: Partial<Omit<CellConstructorProps, "position">>, enableCallback?: boolean): void; changeCellValues(position: Position, values: Partial<Omit<CellConstructorProps, "position">>): void;
changeCellStyles(position: Position, styles: CellStyles): void; changeCellStyles(position: Position, styles: CellStyles): void;
applyActionToRange(range: RangeSelectionType, callback: (cell: Cell) => void): void; applyActionToRange(range: RangeSelectionType, callback: (cell: Cell) => void): void;
deleteSelectedCellsValues(): void; deleteSelectedCellsValues(): void;
showEditor(position: Position, initialString?: string): void; showEditor(position: Position, initialString?: string): void;
renderSheet(): void; renderSheet(): void;
renderSelection(): void;
renderColumnsBar(): void; renderColumnsBar(): void;
renderRowsBar(): void; renderRowsBar(): void;
renderCell(row: number, col: number): void; renderCell(row: number, col: number): void;

637
dist/main.js vendored

File diff suppressed because it is too large Load Diff

2
dist/main.js.map vendored

File diff suppressed because one or more lines are too long

View File

@ -49,6 +49,7 @@ export declare class Cell {
getSerializableCell(): SerializableCell; getSerializableCell(): SerializableCell;
changeStyles(styles: CellStyles): void; changeStyles(styles: CellStyles): void;
changeValues(values: Partial<Omit<CellConstructorProps, "position">>): void; changeValues(values: Partial<Omit<CellConstructorProps, "position">>): void;
private isCellInRange;
render(root: Spreadsheet): void; render(root: Spreadsheet): void;
} }
export {}; export {};

View File

@ -1,9 +0,0 @@
import Spreadsheet, { RangeSelectionType } from "../main";
import { Cell, Position } from "./cell";
export declare class Clipboard {
saved: Cell[][] | null;
root: Spreadsheet;
constructor(root: Spreadsheet);
copy(data: Cell[][], range: RangeSelectionType): void;
paste(root: Spreadsheet, { column, row }: Position, event: ClipboardEvent): void;
}

View File

@ -1,15 +1,9 @@
import { Cell } from "./cell";
import { Column } from "./column"; import { Column } from "./column";
import { Row } from "./row"; import { Row } from "./row";
import { RangeSelectionType, Selection } from "./selection";
export interface ViewProperties { export interface ViewProperties {
width: number; width: number;
height: number; height: number;
} }
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 = { export type ConfigProperties = {
/** Please, end it with '_' symbol. /** Please, end it with '_' symbol.
* *
@ -20,10 +14,6 @@ export type ConfigProperties = {
rows: Row[]; rows: Row[];
columns: Column[]; columns: Column[];
view: ViewProperties; view: ViewProperties;
onCellClick?: CellClickEvent | null;
onSelectionChange?: SelectionChangeEvent | null;
onCellChange?: CellChangeEvent | null;
onCopy?: CopyEvent | null;
}; };
export type SheetConfigConstructorProps = { export type SheetConfigConstructorProps = {
rows: Row[]; rows: Row[];
@ -33,9 +23,5 @@ export declare class Config {
rows: Row[]; rows: Row[];
columns: Column[]; columns: Column[];
view: ViewProperties; view: ViewProperties;
onCellClick: CellClickEvent | null;
onSelectonChange: SelectionChangeEvent | null;
onCellChange: CellChangeEvent | null;
onCopy: CopyEvent | null;
constructor(props: ConfigProperties); constructor(props: ConfigProperties);
} }

View File

@ -1,39 +0,0 @@
import { Scroller } from "../components/scroller";
import Spreadsheet, { Cell, RangeSelectionType, Selection } from "../main";
export declare enum EventTypes {
CELL_CLICK = "CELL_CLICK",
SELECTION_CHANGE = "CHANGE_SELECTION",
CELL_CHANGE = "CELL_CHANGE",
COPY_CELLS = "COPY_CELLS"
}
export type CellClickEvent = {
type: EventTypes.CELL_CLICK;
event: MouseEvent;
scroller: Scroller;
};
export type ChangeSelectionEvent = {
type: EventTypes.SELECTION_CHANGE;
selection: Selection;
enableCallback?: boolean;
};
export type ChangeCellEvent = {
type: EventTypes.CELL_CHANGE;
cell: Cell;
enableCallback?: boolean;
};
export type CopyAction = {
type: EventTypes.COPY_CELLS;
range: RangeSelectionType;
data: Cell[][];
dataAsString: string;
};
export type ActionTypes = CellClickEvent | ChangeSelectionEvent | ChangeCellEvent | CopyAction;
export declare class Events {
root: Spreadsheet;
constructor(root: Spreadsheet);
dispatch(action: ActionTypes): void;
private cellClick;
private changeSelection;
private changeCellValues;
private copy;
}

2
dist/style.css vendored
View File

@ -1 +1 @@
body{padding:0;margin:0}.modern_sc_spreadsheet_container{position:relative;isolation:isolate;border:2px solid black}.modern_sc_content{position:absolute}.modern_sc_sheet{display:block;contain:strict}.modern_sc_scroller{position:absolute;overflow:scroll;box-sizing:border-box;transform:translateZ(0)}.modern_sc_scroller:focus{outline:none}.modern_sc_editor{position:absolute;box-sizing:border-box;font-size:16px;font-family:Arial,Helvetica,sans-serif}.modern_sc_hide{visibility:hidden} body{padding:0;margin:0}.modern_sc_content{position:absolute}.modern_sc_spreadsheet_container{position:relative;isolation:isolate;border:2px solid black}.modern_sc_sheet{display:block;contain:strict}.modern_sc_scroller{position:absolute;overflow:scroll;box-sizing:border-box;transform:translateZ(0)}.modern_sc_scroller:focus{outline:none}.modern_sc_editor{position:absolute;box-sizing:border-box;font-size:16px;font-family:Arial,Helvetica,sans-serif}.modern_sc_hide{visibility:hidden}

View File

@ -1,3 +0,0 @@
import { BaseSelectionType, RangeSelectionType } from "../main";
export declare function checkEqualRanges(range1: RangeSelectionType, range2: RangeSelectionType): boolean;
export declare function checkEqualCellSelections(selection1: BaseSelectionType, selection2: BaseSelectionType): boolean;

View File

@ -1,7 +1,7 @@
{ {
"name": "modern_spreadsheet", "name": "modern_spreadsheet",
"private": false, "private": false,
"version": "0.0.33", "version": "0.0.29",
"exports": { "exports": {
".": { ".": {
"import": "./dist/main.js", "import": "./dist/main.js",
@ -33,21 +33,18 @@
"scripts": { "scripts": {
"dev": "vite", "dev": "vite",
"build": "tsc && vite build", "build": "tsc && vite build",
"build:HTML": "tsc && cross-env BUILD_BROWSER=true vite build",
"build:watch": "tsc && vite build --watch", "build:watch": "tsc && vite build --watch",
"preview": "vite preview", "preview": "vite preview",
"predeploy": "npm run dist", "predeploy": "npm run dist",
"deploy": "gh-pages -d dist", "deploy": "gh-pages -d dist",
"lint": "eslint src --ext .ts", "lint": "eslint src --ext .ts",
"lint:fix": "eslint src --ext .ts --fix", "lint:fix": "eslint src --ext .ts --fix"
"format": "prettier --write ./src/**/*.ts"
}, },
"devDependencies": { "devDependencies": {
"@rollup/plugin-typescript": "^11.1.2", "@rollup/plugin-typescript": "^11.1.2",
"@types/node": "^20.4.4", "@types/node": "^20.4.4",
"@typescript-eslint/eslint-plugin": "^6.2.0", "@typescript-eslint/eslint-plugin": "^6.2.0",
"@typescript-eslint/parser": "^6.2.0", "@typescript-eslint/parser": "^6.2.0",
"cross-env": "^7.0.3",
"eslint": "^8.45.0", "eslint": "^8.45.0",
"eslint-config-prettier": "^8.8.0", "eslint-config-prettier": "^8.8.0",
"gh-pages": "^5.0.0", "gh-pages": "^5.0.0",
@ -56,10 +53,6 @@
"sass": "^1.63.6", "sass": "^1.63.6",
"tslib": "^2.6.0", "tslib": "^2.6.0",
"typescript": "^5.0.2", "typescript": "^5.0.2",
"vite": "^4.4.0", "vite": "^4.4.0"
"vite-plugin-html": "^3.2.0"
},
"dependencies": {
"fast-formula-parser": "^1.0.19"
} }
} }

File diff suppressed because it is too large Load Diff

Binary file not shown.

Before

Width:  |  Height:  |  Size: 36 KiB

View File

@ -82,7 +82,9 @@ export class ColumnsBar {
const isColSelected = this.isColumnSelected(column); const isColSelected = this.isColumnSelected(column);
this.ctx.fillStyle = isColSelected ? "#c7ebff" : "white"; this.ctx.fillStyle = isColSelected
? this.root.styles.cells.selectedBackground
: "white";
this.ctx.strokeStyle = "black"; this.ctx.strokeStyle = "black";
this.ctx.lineWidth = 1; this.ctx.lineWidth = 1;

View File

@ -55,18 +55,16 @@ export class Editor {
case "Enter": { case "Enter": {
if (!this.root.selection.selectedCell) return; 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({ this.root.events.dispatch({
type: EventTypes.CELL_CHANGE, type: EventTypes.CELL_CHANGE,
cell: this.root.getCell(this.root.selection.selectedCell), cell: this.root.getCell(this.root.selection.selectedCell),
}); values: {
value: this.element.value,
displayValue: this.element.value,
}
})
this.hide(); this.hide();
this.root.renderSelection();
} }
} }
}; };

View File

@ -69,7 +69,9 @@ export class RowsBar {
const isRowSeleted = this.isRowSelected(column); const isRowSeleted = this.isRowSelected(column);
this.ctx.fillStyle = isRowSeleted ? "#c7ebff" : "white"; this.ctx.fillStyle = isRowSeleted
? this.root.styles.cells.selectedBackground
: "white";
this.ctx.strokeStyle = "black"; this.ctx.strokeStyle = "black";
this.ctx.lineWidth = this.resizerHeight; this.ctx.lineWidth = this.resizerHeight;

View File

@ -1,4 +1,4 @@
import Spreadsheet, { CSS_PREFIX, Cell, Selection } from "../main"; import Spreadsheet, { CSS_PREFIX } from "../main";
import { EventTypes } from "../modules/events"; import { EventTypes } from "../modules/events";
import { checkEqualCellSelections } from "../utils/position"; import { checkEqualCellSelections } from "../utils/position";
@ -40,18 +40,10 @@ export class Scroller {
this.element.addEventListener("dblclick", this.handleDoubleClick); this.element.addEventListener("dblclick", this.handleDoubleClick);
this.element.addEventListener("keydown", this.handleKeydown); 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) { public setSelectingMode(mode: boolean) {
this.isSelecting = mode; this.isSelecting = mode
} }
private handleMouseMove = (event: MouseEvent) => { private handleMouseMove = (event: MouseEvent) => {
@ -59,42 +51,37 @@ export class Scroller {
const { offsetX, offsetY } = event; const { offsetX, offsetY } = event;
const lastSelectedCell = this.root.getCellByCoords(offsetX, offsetY); const lastSelectedCell = this.root.getCellByCoords(offsetX, offsetY);
let isRangeChanged = false; let isRangeChanged = false
if (this.root.selection.selectedRange) { if (this.root.selection.selectedRange) {
isRangeChanged = !checkEqualCellSelections( isRangeChanged = !checkEqualCellSelections(this.root.selection.selectedRange.to, lastSelectedCell)
this.root.selection.selectedRange.to,
lastSelectedCell,
);
if (isRangeChanged) { if (isRangeChanged) {
this.root.selection.selectedRange.to = lastSelectedCell; this.root.selection.selectedRange.to = lastSelectedCell;
this.root.events.dispatch({ this.root.events.dispatch({
type: EventTypes.SELECTION_CHANGE, type: EventTypes.SELECTION_CHANGE,
selection: this.root.selection, selection: this.root.selection,
enableCallback: true, enableCallback: true
}); })
} }
} }
}; };
private handleMouseUp = () => { private handleMouseUp = () => {
this.isSelecting = false; this.isSelecting = false;
const newSelection = { ...this.root.selection }; const newSelection = {...this.root.selection}
if (this.root.selection.selectedRange) { if (this.root.selection.selectedRange) {
if ( if (
checkEqualCellSelections( checkEqualCellSelections(this.root.selection.selectedRange.from, this.root.selection.selectedRange.to)
this.root.selection.selectedRange.from,
this.root.selection.selectedRange.to,
)
) { ) {
newSelection.selectedRange = null; newSelection.selectedRange = null;
this.root.events.dispatch({ this.root.events.dispatch({
type: EventTypes.SELECTION_CHANGE, type: EventTypes.SELECTION_CHANGE,
selection: newSelection, selection: newSelection,
enableCallback: false, enableCallback: false
}); })
} }
} }
@ -111,7 +98,6 @@ export class Scroller {
private handleKeydown = (event: KeyboardEvent) => { private handleKeydown = (event: KeyboardEvent) => {
//* Navigation //* Navigation
if ( if (
["ArrowLeft", "ArrowRight", "ArrowUp", "ArrowDown"].includes(event.key) ["ArrowLeft", "ArrowRight", "ArrowUp", "ArrowDown"].includes(event.key)
) { ) {
@ -132,7 +118,7 @@ export class Scroller {
if ( if (
this.root.selection.selectedCell && this.root.selection.selectedCell &&
this.root.selection.selectedCell.column < 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.selection.selectedCell.column += 1;
// this.root.renderSheet(); // this.root.renderSheet();
@ -153,7 +139,7 @@ export class Scroller {
if ( if (
this.root.selection.selectedCell && this.root.selection.selectedCell &&
this.root.selection.selectedCell.row < 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.selection.selectedCell.row += 1;
// this.root.renderSheet(); // this.root.renderSheet();
@ -164,12 +150,12 @@ export class Scroller {
this.root.events.dispatch({ this.root.events.dispatch({
type: EventTypes.SELECTION_CHANGE, type: EventTypes.SELECTION_CHANGE,
selection: this.root.selection, selection: this.root.selection,
enableCallback: true, enableCallback: true
}); })
} }
//* Start typings //* Start typings
const keysRegex = /^([a-z]|[а-я]|[0-9]|=)$/; const keysRegex = /^([a-z]|[а-я])$/;
if (!event.metaKey && !event.ctrlKey) { if (!event.metaKey && !event.ctrlKey) {
//* Prevent handle shortcutrs //* Prevent handle shortcutrs
const isPressedLetterKey = keysRegex.test(event.key.toLowerCase()); const isPressedLetterKey = keysRegex.test(event.key.toLowerCase());
@ -191,51 +177,14 @@ export class Scroller {
this.root.deleteSelectedCellsValues(); this.root.deleteSelectedCellsValues();
this.root.renderSheet(); this.root.renderSheet();
} }
if (event.metaKey || event.ctrlKey) {
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) => { private handleClick = (event: MouseEvent) => {
this.root.events.dispatch({ this.root.events.dispatch({
type: EventTypes.CELL_CLICK, type: EventTypes.CELL_CLICK,
event, event,
scroller: this, scroller: this
}); })
}; };
private handleScroll = () => { private handleScroll = () => {
@ -282,7 +231,6 @@ export class Scroller {
this.verticalScroller = verticalScroller; this.verticalScroller = verticalScroller;
this.horizontalScroller = horizontalScroller; this.horizontalScroller = horizontalScroller;
scroller.appendChild(groupScrollers); scroller.appendChild(groupScrollers);
scroller.contentEditable = "false";
scroller.classList.add(CSS_PREFIX + "scroller"); scroller.classList.add(CSS_PREFIX + "scroller");
return { scroller, verticalScroller, horizontalScroller }; return { scroller, verticalScroller, horizontalScroller };

View File

@ -1,4 +1,4 @@
import Spreadsheet, { CSS_PREFIX, RenderBox } from "../main"; import Spreadsheet, { CSS_PREFIX } from "../main";
import { Position } from "../modules/cell"; import { Position } from "../modules/cell";
/** /**
@ -52,79 +52,12 @@ export class Sheet {
this.root.data[row][column].render(this.root); this.root.data[row][column].render(this.root);
} }
private getSelectionRange() {
const { selectedCell, selectedRange } = this.root.selection;
if (!selectedCell && !selectedRange) return;
if (selectedRange) {
const startRow = Math.min(selectedRange.from.row, selectedRange.to.row);
const startCol = Math.min(
selectedRange.from.column,
selectedRange.to.column,
);
const lastRow = Math.max(selectedRange.from.row, selectedRange.to.row);
const lastCol = Math.max(
selectedRange.from.column,
selectedRange.to.column,
);
const startCellBox = new RenderBox(this.root.config, {
row: startRow,
column: startCol,
});
let width = 0;
for (let col = startCol; col <= lastCol; col++) {
width += this.root.config.columns[col].width;
}
let height = 0;
for (let row = startRow; row <= lastRow; row++) {
height += this.root.config.rows[row].height;
}
const x = startCellBox.x - this.root.viewport.left;
const y = startCellBox.y - this.root.viewport.top;
return { x, y, height, width };
}
if (!selectedRange && selectedCell) {
const box = new RenderBox(this.root.config, selectedCell);
box.x -= this.root.viewport.left;
box.y -= this.root.viewport.top;
return box;
}
}
private renderSelectionRange(
x: number,
y: number,
width: number,
height: number,
) {
this.ctx.save();
this.ctx.strokeStyle = "#7da8ff";
this.ctx.lineWidth = 3;
this.ctx.strokeRect(x, y, width, height);
this.ctx.fillStyle = "#7da8ff35";
this.ctx.fillRect(x, y, width, height);
this.ctx.restore();
}
renderSelection() {
const box = this.getSelectionRange();
if (!box) return;
const { height, width, x, y } = box;
this.renderSelectionRange(x, y, width, height);
}
renderSheet() { renderSheet() {
const firstRowIdx = this.root.viewport.firstRow; const firstRowIdx = this.root.viewport.firstRow;
const lastColIdx = this.root.viewport.lastCol + 3; const lastColIdx = this.root.viewport.lastCol + 3;
const lastRowIdx = this.root.viewport.lastRow + 3; const lastRowIdx = this.root.viewport.lastRow + 3;
const firstColIdx = this.root.viewport.firstCol; const firstColIdx = this.root.viewport.firstCol;
for (let row = firstRowIdx; row <= lastRowIdx; row++) { for (let row = firstRowIdx; row <= lastRowIdx; row++) {
for (let col = firstColIdx; col <= lastColIdx; col++) { for (let col = firstColIdx; col <= lastColIdx; col++) {
if (!this.root.config.columns[col] || !this.root.config.rows[row]) if (!this.root.config.columns[col] || !this.root.config.rows[row])
@ -133,6 +66,5 @@ export class Sheet {
this.renderCell({ column: col, row }); this.renderCell({ column: col, row });
} }
} }
this.renderSelection();
} }
} }

View File

@ -2,21 +2,19 @@ import Spreadsheet, { SpreadsheetConstructorProperties } from "./main";
const options: SpreadsheetConstructorProperties = { const options: SpreadsheetConstructorProperties = {
onCellClick: (event, cell) => { onCellClick: (event, cell) => {
console.log("Cell click", event, cell); console.log('Cell click', event, cell)
}, },
onSelectionChange: (selection) => { onSelectionChange: (selection) => {
console.log("Changed selection: ", selection); console.log("Changed selection: ", selection)
}, },
onCellChange(cell) { onCellChange(cell) {
console.log("Cell changed: ", cell); console.log("Cell changed: ", cell)
}, },
onCopy: (range, data, dataAsString) => { }
console.log("Copy event: ", range, data, dataAsString)
}
};
const sheet = new Spreadsheet("#spreadsheet", options); const sheet = new Spreadsheet("#spreadsheet", options);
function saveDataToLS() { function saveDataToLS() {
const serializableData = sheet.serializeData(); const serializableData = sheet.serializeData();
localStorage.setItem("sheet", JSON.stringify(serializableData)); localStorage.setItem("sheet", JSON.stringify(serializableData));

View File

@ -10,14 +10,7 @@ import {
Position, Position,
SerializableCell, SerializableCell,
} from "./modules/cell"; } from "./modules/cell";
import { import { CellChangeEvent, CellClickEvent, Config, SelectionChangeEvent, ViewProperties } from "./modules/config";
CellChangeEvent,
CellClickEvent,
Config,
CopyEvent,
SelectionChangeEvent,
ViewProperties,
} from "./modules/config";
import { RangeSelectionType, Selection } from "./modules/selection"; import { RangeSelectionType, Selection } from "./modules/selection";
import { Styles } from "./modules/styles"; import { Styles } from "./modules/styles";
import { Viewport } from "./modules/viewport"; import { Viewport } from "./modules/viewport";
@ -28,9 +21,7 @@ import { Row } from "./modules/row";
import { Column } from "./modules/column"; import { Column } from "./modules/column";
import { ColumnsBar } from "./components/columnsBar"; import { ColumnsBar } from "./components/columnsBar";
import { RowsBar } from "./components/rowsBar"; import { RowsBar } from "./components/rowsBar";
import { EventTypes, Events } from "./modules/events"; import { Events } from "./modules/events";
import { Clipboard } from "./modules/clipboard";
import { FormulaParser } from "./modules/formulaParser";
/* /*
! Component structure ! Component structure
@ -46,10 +37,9 @@ import { FormulaParser } from "./modules/formulaParser";
export interface SpreadsheetConstructorProperties { export interface SpreadsheetConstructorProperties {
view?: ViewProperties; view?: ViewProperties;
onCellClick?: CellClickEvent | null; onCellClick?: CellClickEvent | null
onSelectionChange?: SelectionChangeEvent | null; onSelectionChange?: SelectionChangeEvent | null
onCellChange?: CellChangeEvent | null; onCellChange?: CellChangeEvent | null
onCopy?: CopyEvent | null
} }
export const CSS_PREFIX = "modern_sc_"; export const CSS_PREFIX = "modern_sc_";
@ -68,9 +58,7 @@ export default class Spreadsheet {
public viewport: Viewport; public viewport: Viewport;
public selection: Selection; public selection: Selection;
public cache: Cache; public cache: Cache;
public events: Events; public events: Events
public clipboard: Clipboard;
public formulaParser: FormulaParser
constructor( constructor(
target: string | HTMLElement, target: string | HTMLElement,
@ -87,10 +75,9 @@ export default class Spreadsheet {
this.config = new Config(config); this.config = new Config(config);
this.config.onCellClick = props?.onCellClick ?? null; this.config.onCellClick = props?.onCellClick ?? null
this.config.onSelectonChange = props?.onSelectionChange ?? null; this.config.onSelectonChange = props?.onSelectionChange ?? null
this.config.onCellChange = props?.onCellChange ?? null; this.config.onCellChange = props?.onCellChange ?? null
this.config.onCopy = props?.onCopy ?? null
this.rowsBar = new RowsBar(this); this.rowsBar = new RowsBar(this);
this.columnsBar = new ColumnsBar(this); this.columnsBar = new ColumnsBar(this);
@ -105,9 +92,8 @@ export default class Spreadsheet {
this.scroller.getViewportBoundlingRect(), this.scroller.getViewportBoundlingRect(),
); );
this.selection = new Selection(); this.selection = new Selection();
this.events = new Events(this); this.events = new Events(this)
this.clipboard = new Clipboard(this);
this.formulaParser = new FormulaParser(this)
this.data = data; this.data = data;
this.styles = new Styles(); this.styles = new Styles();
@ -127,6 +113,7 @@ export default class Spreadsheet {
private setColumnsBarPosition() { private setColumnsBarPosition() {
const top = this.toolbar.height; const top = this.toolbar.height;
const left = this.rowsBar.width; const left = this.rowsBar.width;
console.log(top, left);
this.columnsBar.setElementPosition(top, left); this.columnsBar.setElementPosition(top, left);
} }
@ -165,6 +152,8 @@ export default class Spreadsheet {
rows: cachedRows, rows: cachedRows,
}); });
console.log("CACHE: ", cache);
console.log("CONFIG: ", this.config);
return cache; return cache;
} }
@ -248,19 +237,10 @@ export default class Spreadsheet {
changeCellValues( changeCellValues(
position: Position, position: Position,
values: Partial<Omit<CellConstructorProps, "position">>, values: Partial<Omit<CellConstructorProps, "position">>,
enableCallback: boolean = true
) { ) {
const { column, row } = position; const { column, row } = position;
this.data[row][column].changeValues(values); this.data[row][column].changeValues(values);
this.events.dispatch({
type: EventTypes.CELL_CHANGE,
cell: this.data[row][column],
enableCallback: enableCallback
})
this.renderCell(row, column); this.renderCell(row, column);
} }
@ -315,10 +295,6 @@ export default class Spreadsheet {
this.sheet.renderSheet(); this.sheet.renderSheet();
} }
renderSelection() {
this.sheet.renderSelection();
}
renderColumnsBar() { renderColumnsBar() {
this.columnsBar.renderBar(); this.columnsBar.renderBar();
} }
@ -333,11 +309,11 @@ export default class Spreadsheet {
public loadData(data: Cell[][] | SerializableCell[][]): Spreadsheet { public loadData(data: Cell[][] | SerializableCell[][]): Spreadsheet {
const rowsLength = data.length; const rowsLength = data.length;
const colsLength = data[0] ? data[0].length : 0; const colsLength = data[0] ? this.data[0].length : 0;
this.data = []; this.data = [];
const formattedData: Cell[][] = []; const formattedData: Cell[][] = [];
// Transform serialized objects to Cells
for (let row = 0; row < rowsLength; row++) { for (let row = 0; row < rowsLength; row++) {
const innerRow: Cell[] = []; const innerRow: Cell[] = [];
for (let col = 0; col < colsLength; col++) { for (let col = 0; col < colsLength; col++) {
@ -355,17 +331,11 @@ export default class Spreadsheet {
formattedData.push(innerRow); formattedData.push(innerRow);
} }
const config = this.makeConfigFromData(formattedData, this.config.view);
config.onCellChange = this.config.onCellChange
config.onCellClick = this.config.onCellClick
config.onCopy = this.config.onCopy
config.onSelectonChange = this.config.onSelectonChange
this.data = formattedData; this.data = formattedData;
this.selection.selectedCell = null; this.selection.selectedCell = null;
this.selection.selectedRange = null; this.selection.selectedRange = null;
this.config = config this.config = this.makeConfigFromData(formattedData, this.config.view);
this.cache = this.getInitialCache(); this.cache = this.getInitialCache();
this.scroller.updateScrollerSize(); this.scroller.updateScrollerSize();
this.viewport = new Viewport( this.viewport = new Viewport(
@ -406,7 +376,7 @@ export default class Spreadsheet {
view, view,
rows, rows,
columns, columns,
onCellClick: null, onCellClick: null
}); });
return config; return config;

View File

@ -44,21 +44,21 @@ export class Cache {
public getRowByYCoord(y: number): number { public getRowByYCoord(y: number): number {
let rowIdx = 0; let rowIdx = 0;
for (let i = 0; i < this.rows.length; i++) { for (let i = 0; i < this.rows.length; i++) {
rowIdx = i if (y <= this.rows[i].yPos) {
if (y <= this.rows[i].yPos) { //* Intersection detect //* Intersection detect
rowIdx = i;
break; break;
} }
} }
return rowIdx; return rowIdx;
} }
public getColumnByXCoord(x: number): number { public getColumnByXCoord(x: number): number {
let colIdx = 0; let colIdx = 0;
for (let i = 0; i < this.columns.length; i++) { for (let i = 0; i < this.columns.length; i++) {
colIdx = i if (x <= this.columns[i].xPos) {
if (x <= this.columns[i].xPos) { //* Intersection detect //* Intersection detect
colIdx = i;
break; break;
} }
} }

View File

@ -1,5 +1,4 @@
import Spreadsheet from "../main"; import Spreadsheet from "../main";
import { FormulaParser } from "./formulaParser";
import { RenderBox } from "./renderBox"; import { RenderBox } from "./renderBox";
export type CellConstructorProps = { export type CellConstructorProps = {
@ -70,9 +69,6 @@ export class Cell {
position: Position; position: Position;
style: CellStyles | null = null; style: CellStyles | null = null;
cellsDependsOnThisCell: Position[] = []
dependedFromCells: Position[] = []
constructor(props: CellConstructorProps) { constructor(props: CellConstructorProps) {
this.value = props.value; this.value = props.value;
this.displayValue = props.displayValue; this.displayValue = props.displayValue;
@ -100,53 +96,50 @@ export class Cell {
Object.assign(this, values); Object.assign(this, values);
} }
evalFormula(parser: FormulaParser) { private isCellInRange(root: Spreadsheet): boolean {
if (this.value.substring(0, 1) !== '=') return; const { column, row } = this.position;
const { selectedRange } = root.selection;
this.resultValue = parser.parser.parse(this.value.slice(1), { if (!selectedRange) return false;
col: this.position.column,
row: this.position.row const isCellInRow =
}) row >= Math.min(selectedRange.from.row, selectedRange.to.row) &&
row <= Math.max(selectedRange.to.row, selectedRange.from.row);
const isCellInCol =
column >= Math.min(selectedRange.from.column, selectedRange.to.column) &&
column <= Math.max(selectedRange.to.column, selectedRange.from.column);
return isCellInCol && isCellInRow;
} }
// private isCellInRange(root: Spreadsheet): boolean {
// const { column, row } = this.position;
// const { selectedRange } = root.selection;
// if (!selectedRange) return false;
// const isCellInRow =
// row >= Math.min(selectedRange.from.row, selectedRange.to.row) &&
// row <= Math.max(selectedRange.to.row, selectedRange.from.row);
// const isCellInCol =
// column >= Math.min(selectedRange.from.column, selectedRange.to.column) &&
// column <= Math.max(selectedRange.to.column, selectedRange.from.column);
// return isCellInCol && isCellInRow;
// }
render(root: Spreadsheet) { render(root: Spreadsheet) {
const renderBox = new RenderBox(root.config, this.position); const renderBox = new RenderBox(root.config, this.position);
let { x, y } = renderBox; let { x, y } = renderBox;
const { height, width } = renderBox; const { height, width } = renderBox;
const { ctx } = root; const { ctx } = root;
// const isCellSelected = const isCellSelected =
// root.selection.selectedCell?.row === this.position.row && root.selection.selectedCell?.row === this.position.row &&
// root.selection.selectedCell.column === this.position.column; root.selection.selectedCell.column === this.position.column;
// const isCellInRange = this.isCellInRange(root); const isCellInRange = this.isCellInRange(root);
y -= root.viewport.top; y -= root.viewport.top;
x -= root.viewport.left; x -= root.viewport.left;
const styles = this.style ?? root.styles.cells; const styles = this.style ?? root.styles.cells;
ctx.clearRect(x, y, width, height); ctx.clearRect(x, y, width, height);
ctx.fillStyle = styles.background; ctx.fillStyle =
isCellSelected || isCellInRange
? styles.selectedBackground
: styles.background;
ctx.strokeStyle = "black"; ctx.strokeStyle = "black";
ctx.fillRect(x, y, width - 1, height - 1); ctx.fillRect(x, y, width - 1, height - 1);
ctx.strokeRect(x, y, width, height); ctx.strokeRect(x, y, width, height);
ctx.fillStyle = styles.fontColor; ctx.fillStyle =
isCellSelected || isCellInRange
? styles.selectedFontColor
: styles.fontColor;
ctx.textAlign = "left"; ctx.textAlign = "left";
ctx.font = `${styles.fontSize}px Arial`; ctx.font = `${styles.fontSize}px Arial`;
ctx.textBaseline = "middle"; ctx.textBaseline = "middle";

View File

@ -1,106 +0,0 @@
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);
}
root.renderSheet();
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,20 +1,15 @@
import { Cell } from "./cell"; import { Cell } from "./cell";
import { Column } from "./column"; import { Column } from "./column";
import { Row } from "./row"; import { Row } from "./row";
import { RangeSelectionType, Selection } from "./selection"; import { Selection } from "./selection";
export interface ViewProperties { export interface ViewProperties {
width: number; width: number;
height: number; height: number;
} }
export type CellClickEvent = (event: MouseEvent, cell: Cell) => void; export type CellClickEvent = (event: MouseEvent, cell: Cell) => void
export type SelectionChangeEvent = (selection: Selection) => void; export type SelectionChangeEvent = (selection: Selection) => void
export type CellChangeEvent = (cell: Cell) => void; export type CellChangeEvent = (cell: Cell) => void
export type CopyEvent = (
range: RangeSelectionType,
data: Cell[][],
dataAsString: string,
) => void;
export type ConfigProperties = { export type ConfigProperties = {
/** Please, end it with '_' symbol. /** Please, end it with '_' symbol.
@ -26,10 +21,9 @@ export type ConfigProperties = {
rows: Row[]; rows: Row[];
columns: Column[]; columns: Column[];
view: ViewProperties; view: ViewProperties;
onCellClick?: CellClickEvent | null; onCellClick?: CellClickEvent | null
onSelectionChange?: SelectionChangeEvent | null; onSelectionChange?: SelectionChangeEvent | null
onCellChange?: CellChangeEvent | null; onCellChange?: CellChangeEvent | null
onCopy?: CopyEvent | null;
}; };
export type SheetConfigConstructorProps = { export type SheetConfigConstructorProps = {
@ -45,19 +39,17 @@ export class Config {
height: 600, height: 600,
}; };
onCellClick: CellClickEvent | null = null; onCellClick: ((event: MouseEvent, cell: Cell) => void) | null = null
onSelectonChange: SelectionChangeEvent | null = null; onSelectonChange: SelectionChangeEvent | null = null
onCellChange: CellChangeEvent | null = null; onCellChange: CellChangeEvent | null = null
onCopy: CopyEvent | null;
constructor(props: ConfigProperties) { constructor(props: ConfigProperties) {
this.columns = props.columns; this.columns = props.columns;
this.rows = props.rows; this.rows = props.rows;
this.view = props.view; this.view = props.view;
this.onCellClick = props.onCellClick ?? null; this.onCellClick = props.onCellClick ?? null
this.onSelectonChange = props.onSelectionChange ?? null; this.onSelectonChange = props.onSelectionChange ?? null
this.onCellChange = props.onCellChange ?? null; this.onCellChange = props.onCellChange ?? null
this.onCopy = props.onCopy ?? null;
} }
} }

View File

@ -1,142 +1,107 @@
import { Scroller } from "../components/scroller"; import { Scroller } from "../components/scroller";
import Spreadsheet, { Cell, RangeSelectionType, Selection } from "../main"; import Spreadsheet, { Cell, CellConstructorProps, Selection } from "../main";
export enum EventTypes { export enum EventTypes {
CELL_CLICK = "CELL_CLICK", CELL_CLICK = "CELL_CLICK",
SELECTION_CHANGE = "CHANGE_SELECTION", SELECTION_CHANGE = "CHANGE_SELECTION",
CELL_CHANGE = "CELL_CHANGE", CELL_CHANGE = "CELL_CHANGE"
COPY_CELLS = "COPY_CELLS",
} }
export type CellClickEvent = { export type CellClickEvent = {
type: EventTypes.CELL_CLICK; type: EventTypes.CELL_CLICK
event: MouseEvent; event: MouseEvent
scroller: Scroller; scroller: Scroller
}; }
export type ChangeSelectionEvent = { export type ChangeSelectionEvent = {
type: EventTypes.SELECTION_CHANGE; type: EventTypes.SELECTION_CHANGE,
selection: Selection; selection: Selection
enableCallback?: boolean; enableCallback?: boolean
}; }
export type ChangeCellEvent = { export type ChangeCellEvent = {
type: EventTypes.CELL_CHANGE; type: EventTypes.CELL_CHANGE,
cell: Cell; cell: Cell,
enableCallback?: boolean; values: Partial<Omit<CellConstructorProps, "position">>
}; }
export type CopyAction = {
type: EventTypes.COPY_CELLS;
range: RangeSelectionType;
data: Cell[][];
dataAsString: string;
};
export type ActionTypes =
| CellClickEvent
| ChangeSelectionEvent
| ChangeCellEvent
| CopyAction;
export type ActionTypes = CellClickEvent | ChangeSelectionEvent | ChangeCellEvent
export class Events { export class Events {
root: Spreadsheet;
constructor(root: Spreadsheet) { root: Spreadsheet
this.root = root;
}
async dispatch(action: ActionTypes) { constructor(root: Spreadsheet) {
switch (action.type) { this.root = root
case EventTypes.CELL_CLICK: {
const { event, scroller } = action;
//
//* Here may be side effects
//
this.cellClick(event, scroller);
break;
}
case EventTypes.SELECTION_CHANGE: {
const { selection, enableCallback } = action;
//
//* Here may be side effects
//
this.changeSelection(selection, enableCallback);
break;
}
case EventTypes.CELL_CHANGE: {
const { cell, enableCallback } = action;
if (cell.value.substring(0, 1).startsWith('=')) {
try {
await cell.evalFormula(this.root.formulaParser)
cell.displayValue = cell.resultValue
this.root.renderCell(cell.position.row, cell.position.column)
this.changeCellValues(cell, enableCallback);
return;
}
catch (err) {
console.error(err)
}
}
this.root.renderCell(cell.position.row, cell.position.column)
this.changeCellValues(cell, enableCallback);
break;
}
case EventTypes.COPY_CELLS: {
const { data, dataAsString, range } = action;
this.copy(range, data, dataAsString);
break;
}
default: {
break;
}
} }
}
private cellClick = (event: MouseEvent, scroller: Scroller) => { dispatch(action: ActionTypes) {
if (event.button !== 0) return; // Left mouse button switch (action.type) {
const { offsetX, offsetY } = event; case EventTypes.CELL_CLICK: {
const clickedCell = this.root.getCellByCoords(offsetX, offsetY); const { event, scroller } = action
const cell = this.root.getCell(clickedCell); //
//* Here may be side effects
//
this.cellClick(event, scroller)
break;
}
const selection = new Selection(); case EventTypes.SELECTION_CHANGE: {
selection.selectedCell = clickedCell; const { selection, enableCallback } = action
selection.selectedRange = { //
from: clickedCell, //* Here may be side effects
to: clickedCell, //
}; this.changeSelection(selection, enableCallback)
break;
}
scroller.setSelectingMode(true); case EventTypes.CELL_CHANGE: {
const { cell, values } = action
//
//* Here may be side effects
//
this.changeCellValues(cell, values)
break;
}
this.changeSelection(selection, true); default: {
break;
}
}
}
this.root.config.onCellClick?.(event, cell); private cellClick = (event: MouseEvent, scroller: Scroller) => {
}; if (event.button !== 0) return; // Left mouse button
const { offsetX, offsetY } = event;
const clickedCell = this.root.getCellByCoords(offsetX, offsetY);
const cell = this.root.getCell(clickedCell)
private changeSelection = (selection: Selection, enableCallback = false) => { const selection = new Selection()
this.root.selection = selection; selection.selectedCell = clickedCell
selection.selectedRange = {
from: clickedCell,
to: clickedCell,
};
if (enableCallback) this.root.config.onSelectonChange?.(selection); scroller.setSelectingMode(true);
this.root.renderSheet();
this.root.renderColumnsBar();
this.root.renderRowsBar();
};
private changeCellValues(cell: Cell, enableCallback: boolean = true) { this.changeSelection(selection, true)
if (enableCallback) this.root.config.onCellChange?.(cell);
}
private copy = ( this.root.config.onCellClick?.(event, cell)
range: RangeSelectionType, }
data: Cell[][],
dataAsString: string, private changeSelection = (selection: Selection, enableCallback = false) => {
) => { this.root.selection = selection
this.root.config.onCopy?.(range, data, dataAsString);
}; if (enableCallback) this.root.config.onSelectonChange?.(selection)
this.root.renderSheet();
this.root.renderColumnsBar();
this.root.renderRowsBar();
}
private changeCellValues(cell: Cell, values: Partial<Omit<CellConstructorProps, "position">>) {
this.root.changeCellValues(cell.position, values)
this.root.config.onCellChange?.(cell)
}
} }

View File

@ -1,23 +0,0 @@
import Parser, { DepParser } from 'fast-formula-parser'
import Spreadsheet from '../main'
export class FormulaParser {
parser: Parser
depParser: DepParser
root: Spreadsheet
constructor(root: Spreadsheet) {
this.root = root
this.parser = new Parser({
onCell: ({col, row}) => {
const cell = this.root.data[row - 1][col - 1]
const cellValue = cell.resultValue.length > 0 ? cell.resultValue : cell.value
if( cellValue && isNaN(Number(cellValue)) === false) return Number(cellValue)
return this.root.data[row - 1][col - 1].resultValue ?? ''
},
})
this.depParser = new DepParser({})
this.depParser
}
}

View File

@ -1,68 +0,0 @@
declare module 'fast-formula-parser' {
export type PositionWithSheet = {
sheet?: string
row: number
col: number
}
export type FunctionArgument = {
isArray: boolean
isCellRef: boolean
isRangeRef: boolean
value: string | number
}
export type Position = {
col: number
row: number
}
export type RangeReference = {
sheet?: string
from: Position,
to: Position
}
export type Config = {
functions?: Record<string, (...args: FunctionArgument[]) => string>
functionsNeedContext?: (context: Parser, ...args: FunctionArgument[]) => string
onCell?: (position: PositionWithSheet) => number | string
onRange?: (ref) => Array<string|number>[]
onVariable?: (name: string, sheetName: string) => RangeReference
}
export const Types = {
NUMBER: 0,
ARRAY: 1,
BOOLEAN: 2,
STRING: 3,
RANGE_REF: 4, // can be 'A:C' or '1:4', not only 'A1:C3'
CELL_REF: 5,
COLLECTIONS: 6, // Unions of references
NUMBER_NO_BOOLEAN: 10,
};
export const Factorials: number[]
export default class Parser {
constructor(config: Config)
parse: (expression: string, position: PositionWithSheet) => string
parseAsync: (expression: string, position: PositionWithSheet) => Promise<string>
}
type FormulaHelpersType = {
accept: (param: FunctionArgument, type?: number, defValue?: number | string, flat?: boolean, allowSingleValue?: boolean) => number | string
type: (variable) => number
isRangeRef: (param) => boolean
isCellRef: (param) => boolean
}
export class DepParser {
constructor(config?: {onVariable?: (name: string, sheetName: string) => RangeReference})
parse(expression: string, position: PositionWithSheet): PositionWithSheet[]
}
export const FormulaHelpers: FormulaHelpersType
}

View File

@ -1,20 +1,12 @@
import { BaseSelectionType, RangeSelectionType } from "../main"; import { BaseSelectionType, RangeSelectionType } from "../main";
export function checkEqualRanges( export function checkEqualRanges(range1: RangeSelectionType, range2: RangeSelectionType) {
range1: RangeSelectionType, const equalRows = range1.from.row === range2.to.row
range2: RangeSelectionType, const equalColumns = range1.from.column === range2.to.column
) {
const equalRows = range1.from.row === range2.to.row;
const equalColumns = range1.from.column === range2.to.column;
return equalRows && equalColumns; return equalRows && equalColumns
} }
export function checkEqualCellSelections( export function checkEqualCellSelections(selection1: BaseSelectionType, selection2: BaseSelectionType) {
selection1: BaseSelectionType, return selection1.column === selection2.column && selection1.row === selection2.row
selection2: BaseSelectionType,
) {
return (
selection1.column === selection2.column && selection1.row === selection2.row
);
} }

View File

@ -5,7 +5,7 @@
"types": ["vite/client", "node"], "types": ["vite/client", "node"],
"allowJs": false, "allowJs": false,
"skipLibCheck": true, "skipLibCheck": true,
"esModuleInterop": true, "esModuleInterop": false,
"allowSyntheticDefaultImports": true, "allowSyntheticDefaultImports": true,
"strict": true, "strict": true,
"forceConsistentCasingInFileNames": true, "forceConsistentCasingInFileNames": true,

View File

@ -2,16 +2,14 @@ import { defineConfig } from "vite";
import path from "path"; import path from "path";
import typescript from "@rollup/plugin-typescript"; import typescript from "@rollup/plugin-typescript";
import { typescriptPaths } from "rollup-plugin-typescript-paths"; import { typescriptPaths } from "rollup-plugin-typescript-paths";
import { fileURLToPath } from "node:url";
const BROWSER_MODE = process.env.BUILD_BROWSER === 'true'; export default defineConfig({
console.log({ BROWSER_MODE });
const libConfig = defineConfig({
base: "/modern_spreadsheet/", base: "/modern_spreadsheet/",
plugins: [], plugins: [],
resolve: {}, resolve: {},
server: { server: {
port: 5179, port: 3000,
open: true, open: true,
}, },
build: { build: {
@ -39,23 +37,3 @@ const libConfig = defineConfig({
}, },
}, },
}); });
const browserConfig = defineConfig({
base: "/modern_spreadsheet/",
resolve: {},
build: {
manifest: true,
minify: true,
reportCompressedSize: true,
sourcemap: true,
lib: {
entry: path.resolve(__dirname, 'index.html'),
fileName: 'demo',
formats: ['es']
}
}
})
const config = BROWSER_MODE ? browserConfig : libConfig;
export default config;