Compare commits

...

No commits in common. "main" and "gh-pages" have entirely different histories.

72 changed files with 13985 additions and 5984 deletions

View File

@ -1,30 +0,0 @@
module.exports = {
env: {
browser: true,
es2021: true,
node: true,
},
extends: [
"eslint:recommended",
"plugin:@typescript-eslint/recommended",
"prettier",
],
overrides: [
{
env: {
node: true,
},
files: [".eslintrc.{js,cjs}"],
parserOptions: {
sourceType: "script",
},
},
],
parser: "@typescript-eslint/parser",
parserOptions: {
ecmaVersion: "latest",
sourceType: "module",
},
plugins: ["@typescript-eslint"],
rules: {},
};

23
.gitignore vendored
View File

@ -1,23 +0,0 @@
# Logs
logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
pnpm-debug.log*
lerna-debug.log*
node_modules
dist-ssr
*.local
# Editor directories and files
.vscode/*
!.vscode/extensions.json
.idea
.DS_Store
*.suo
*.ntvs*
*.njsproj
*.sln
*.sw?

View File

@ -1,2 +0,0 @@
# Ignore artifacts:
dist

View File

@ -1 +0,0 @@
{}

21
LICENCE
View File

@ -1,21 +0,0 @@
MIT License
Copyright (c) 2021 typeguard, Inc.
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

View File

@ -1,78 +0,0 @@
# 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.
- TypeScript supported
- Native scrolling
- Customizable
- Copy & Paste support
### Basic usage
```ts
import Spreadsheet from "modern_spreadsheet";
import "modern_spreadsheet/style.css"; // <= this is required
const target = document.getElementById("spreadsheet");
const sheet = new Spreadsheet(target);
//...
```
### Save and load data
```ts
function saveData() {
const serialized = sheet.serializeData();
localStorage.setItem("sheet_data", JSON.stringify(serialized));
}
function loadData() {
const data = localStorage.getItem("sheet_data");
const json = JSON.parse(data);
if (!json) return;
sheet.loadData(json);
}
```
#### Supported events
- onCellClick
- onSelectionChange
- onCellChange
- onCopy
### Using events examples
```ts
import Spreadsheet, { SpreadsheetConstructorProperties } from "./main";
const options: SpreadsheetConstructorProperties = {
onCellClick: (event, cell) => {
console.log("Cell click", event, cell);
},
onSelectionChange: (selection) => {
console.log("Changed selection: ", selection);
},
onCellChange = (cell) => {
console.log("Cell changed: ", cell);
},
onCopy: (range, data, dataAsString) => {
console.log("Copy event: ", range, data, dataAsString)
}
};
const sheet = new Spreadsheet("#spreadsheet", options);
```
### Roadmap
- ~~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
- Formulas support
- Selected cell depends cells highlight
- Async formulas support
- Mutlisheets (?)

13977
demo.js Normal file

File diff suppressed because it is too large Load Diff

1
demo.js.map Normal file

File diff suppressed because one or more lines are too long

View File

@ -1,16 +0,0 @@
import Spreadsheet from "../main";
export declare class ColumnsBar {
element: HTMLCanvasElement;
private root;
height: number;
width: number;
ctx: CanvasRenderingContext2D;
constructor(root: Spreadsheet);
private createElement;
setElementPosition(top: number, left: number): void;
private isColumnSelected;
private renderText;
private renderRect;
private renderSingleColumn;
renderBar(): void;
}

View File

@ -1,11 +0,0 @@
import Spreadsheet from "../main";
import { Position } from "../modules/cell";
export declare class Editor {
element: HTMLInputElement;
root: Spreadsheet;
constructor(root: Spreadsheet);
hide(): void;
show(position: Position, initialString?: string): void;
handleKeydown: (event: KeyboardEvent) => void;
handleClickOutside: (event: MouseEvent) => void;
}

View File

@ -1,17 +0,0 @@
import Spreadsheet from "../main";
export declare class RowsBar {
element: HTMLCanvasElement;
ctx: CanvasRenderingContext2D;
root: Spreadsheet;
width: number;
height: number;
resizerHeight: number;
constructor(root: Spreadsheet);
private createElement;
setElementPosition(top: number, left: number): void;
private isRowSelected;
private renderText;
private renderRect;
private renderSingleRow;
renderBar(): void;
}

View File

@ -1,29 +0,0 @@
import Spreadsheet from "../main";
export interface ViewportRect {
top: number;
left: number;
right: number;
bottom: number;
}
export declare class Scroller {
element: HTMLDivElement;
private verticalScroller;
private horizontalScroller;
private root;
private isSelecting;
constructor(root: Spreadsheet);
setSelectingMode(mode: boolean): void;
private handleMouseMove;
private handleMouseUp;
private handleDoubleClick;
private handleKeydown;
private handleClick;
private handleScroll;
getViewportBoundlingRect(): ViewportRect;
private buildComponent;
private getActualHeight;
private getActualWidth;
updateScrollerSize(): void;
private setScrollerHeight;
private setScrollerWidth;
}

View File

@ -1,17 +0,0 @@
import Spreadsheet from "../main";
import { Position } from "../modules/cell";
/**
* Display (CANVAS) element where cells render
*/
export declare class Sheet {
element: HTMLCanvasElement;
ctx: CanvasRenderingContext2D;
root: Spreadsheet;
constructor(root: Spreadsheet);
getCellByCoords(x: number, y: number): Position;
renderCell(position: Position): void;
private getSelectionRange;
private renderSelectionRange;
renderSelection(): void;
renderSheet(): void;
}

View File

@ -1,9 +0,0 @@
import Spreadsheet from "../main";
import { ViewProperties } from "../modules/config";
/** Base (root) component */
export declare class Table {
element: HTMLDivElement;
root: Spreadsheet;
constructor(root: Spreadsheet);
changeElementSizes(sizes: ViewProperties): void;
}

View File

@ -1,7 +0,0 @@
import Spreadsheet from "../main";
export declare class Toolbar {
element: HTMLDivElement;
root: Spreadsheet;
height: number;
constructor(root: Spreadsheet);
}

1
dist/index.d.ts vendored
View File

@ -1 +0,0 @@
export {};

7
dist/main.cjs vendored

File diff suppressed because one or more lines are too long

1
dist/main.cjs.map vendored

File diff suppressed because one or more lines are too long

82
dist/main.d.ts vendored
View File

@ -1,82 +0,0 @@
import { Cell, CellConstructorProps, CellStyles, Position, SerializableCell } from "./modules/cell";
import { CellChangeEvent, CellClickEvent, Config, CopyEvent, SelectionChangeEvent, ViewProperties } from "./modules/config";
import { RangeSelectionType, Selection } from "./modules/selection";
import { Styles } from "./modules/styles";
import { Viewport } from "./modules/viewport";
import "./scss/main.scss";
import { Cache } from "./modules/cache";
import { Events } from "./modules/events";
import { Clipboard } from "./modules/clipboard";
export interface SpreadsheetConstructorProperties {
view?: ViewProperties;
onCellClick?: CellClickEvent | null;
onSelectionChange?: SelectionChangeEvent | null;
onCellChange?: CellChangeEvent | null;
onCopy?: CopyEvent | null;
}
export declare const CSS_PREFIX = "modern_sc_";
export default class Spreadsheet {
private table;
private scroller;
private toolbar;
private rowsBar;
private columnsBar;
private sheet;
private editor;
styles: Styles;
config: Config;
data: Cell[][];
viewport: Viewport;
selection: Selection;
cache: Cache;
events: Events;
clipboard: Clipboard;
constructor(target: string | HTMLElement, props?: SpreadsheetConstructorProperties);
private setRowsBarPosition;
private setColumnsBarPosition;
private setElementsPositions;
private getInitialCache;
private buildComponent;
/**Destroy spreadsheet DOM element.
*
* May be usefull when need to rerender component.
*/
destroy(): void;
private appendTableToTarget;
/** Canvas rendering context 2D.
*
* Abble to draw on canvas with default CanvasAPI methods
*/
get ctx(): CanvasRenderingContext2D;
get viewProps(): ViewProperties;
get columnsBarHeight(): number;
get rowsBarWidth(): number;
get toolbarHeight(): number;
/** Focusing on interactive part of spreadsheet */
focusTable(): void;
getCellByCoords(x: number, y: number): Position;
getCell(position: Position): Cell;
changeCellValues(position: Position, values: Partial<Omit<CellConstructorProps, "position">>, enableCallback?: boolean): void;
changeCellStyles(position: Position, styles: CellStyles): void;
applyActionToRange(range: RangeSelectionType, callback: (cell: Cell) => void): void;
deleteSelectedCellsValues(): void;
showEditor(position: Position, initialString?: string): void;
renderSheet(): void;
renderSelection(): void;
renderColumnsBar(): void;
renderRowsBar(): void;
renderCell(row: number, col: number): void;
loadData(data: Cell[][] | SerializableCell[][]): Spreadsheet;
private makeConfigFromData;
serializeData(): SerializableCell[][];
}
export * from "./modules/cache";
export * from "./modules/cell";
export * from "./modules/column";
export * from "./modules/config";
export * from "./modules/renderBox";
export * from "./modules/row";
export * from "./modules/selection";
export * from "./modules/styles";
export * from "./modules/viewport";
export * from "./utils/createData";

1008
dist/main.js vendored

File diff suppressed because it is too large Load Diff

1
dist/main.js.map vendored

File diff suppressed because one or more lines are too long

View File

@ -1,29 +0,0 @@
export interface CachedColumnProperties {
xPos: number;
colIdx: number;
}
export declare class CachedColumn {
xPos: number;
colIdx: number;
constructor(props: CachedColumnProperties);
}
export interface CachedRowProperties {
yPos: number;
rowIdx: number;
}
export declare class CachedRow {
yPos: number;
rowIdx: number;
constructor(props: CachedRowProperties);
}
export interface CacheConstructorProps {
columns: CachedColumn[];
rows: CachedRow[];
}
export declare class Cache {
columns: CachedColumn[];
rows: CachedRow[];
constructor(initial: CacheConstructorProps);
getRowByYCoord(y: number): number;
getColumnByXCoord(x: number): number;
}

View File

@ -1,54 +0,0 @@
import Spreadsheet from "../main";
export type CellConstructorProps = {
value: string;
displayValue: string;
resultValue: string;
position: Position;
style: CellStyles | null;
};
interface CellStylesConstructorProps {
fontSize: number;
fontColor: string;
background: string;
borderColor: string;
selectedBackground: string;
selectedFontColor: string;
}
export declare class CellStyles {
fontSize: number;
fontColor: string;
background: string;
borderColor: string;
selectedBackground: string;
selectedFontColor: string;
constructor(props?: CellStylesConstructorProps);
}
export declare class Position {
row: number;
column: number;
constructor(row: number, column: number);
}
export declare class SerializableCell {
value: string;
displayValue: string;
resultValue: string;
position: Position;
style: CellStyles | null;
constructor(props: SerializableCell | SerializableCell);
}
export declare class Cell {
/** True value (data) */
value: string;
/** Value to render */
displayValue: string;
/** This refers to the values that were obtained by calculations, for example, after calculating the formula */
resultValue: string;
position: Position;
style: CellStyles | null;
constructor(props: CellConstructorProps);
getSerializableCell(): SerializableCell;
changeStyles(styles: CellStyles): void;
changeValues(values: Partial<Omit<CellConstructorProps, "position">>): void;
render(root: Spreadsheet): void;
}
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,9 +0,0 @@
export type ColumnConstructorProperties = {
width: number;
title: string;
};
export declare class Column {
width: number;
title: string;
constructor(props: ColumnConstructorProperties);
}

View File

@ -1,41 +0,0 @@
import { Cell } from "./cell";
import { Column } from "./column";
import { Row } from "./row";
import { RangeSelectionType, Selection } from "./selection";
export interface ViewProperties {
width: 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 = {
/** Please, end it with '_' symbol.
*
* *Example:*
*
* 'test_'
* 'google_' */
rows: Row[];
columns: Column[];
view: ViewProperties;
onCellClick?: CellClickEvent | null;
onSelectionChange?: SelectionChangeEvent | null;
onCellChange?: CellChangeEvent | null;
onCopy?: CopyEvent | null;
};
export type SheetConfigConstructorProps = {
rows: Row[];
columns: Column[];
};
export declare class Config {
rows: Row[];
columns: Column[];
view: ViewProperties;
onCellClick: CellClickEvent | null;
onSelectonChange: SelectionChangeEvent | null;
onCellChange: CellChangeEvent | null;
onCopy: CopyEvent | null;
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;
}

View File

@ -1,11 +0,0 @@
import { Position } from "./cell";
import { Config } from "./config";
export declare class RenderBox {
x: number;
y: number;
width: number;
height: number;
constructor(config: Config, cellPosition: Position);
private getXCoord;
private getYCoord;
}

View File

@ -1,9 +0,0 @@
export type RowConstructorProps = {
height: number;
title: string;
};
export declare class Row {
height: number;
title: string;
constructor(props: RowConstructorProps);
}

View File

@ -1,12 +0,0 @@
export type BaseSelectionType = {
row: number;
column: number;
};
export type RangeSelectionType = {
from: BaseSelectionType;
to: BaseSelectionType;
};
export declare class Selection {
selectedCell: BaseSelectionType | null;
selectedRange: RangeSelectionType | null;
}

View File

@ -1,5 +0,0 @@
import { CellStyles } from "./cell";
export declare class Styles {
cells: CellStyles;
constructor();
}

View File

@ -1,25 +0,0 @@
import Spreadsheet from "../main";
export type ViewportConstructorProps = {
top: number;
left: number;
right: number;
bottom: number;
};
export declare class Viewport {
root: Spreadsheet;
top: number;
left: number;
right: number;
bottom: number;
firstRow: number;
lastRow: number;
firstCol: number;
lastCol: number;
constructor(root: Spreadsheet, props: ViewportConstructorProps);
updateValues(props: ViewportConstructorProps): void;
/** Get index of first row in viewport */
private getFirstRow;
private getLastRow;
private getFirstCol;
private getLastCol;
}

View File

@ -1,9 +0,0 @@
import { Cell } from "../modules/cell";
import { Config } from "../modules/config";
export declare function createSampleData(rows: number, columns: number, fillCellsByCoords?: boolean): Cell[][];
export declare function createSampleConfig(rows: number, columns: number): Config;
export type SpreadsheetConfigAndDataReturnType = {
config: Config;
data: Cell[][];
};
export declare function makeSpreadsheetConfigAndData(rows: number, columns: number): SpreadsheetConfigAndDataReturnType;

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

@ -2,14 +2,16 @@
<html lang="en"> <html lang="en">
<head> <head>
<meta charset="UTF-8" /> <meta charset="UTF-8" />
<link rel="icon" type="image/svg+xml" href="/vite.svg" /> <link rel="icon" type="image/svg+xml" href="/modern_spreadsheet/vite.svg" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" /> <meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Spreadsheet example</title> <title>Spreadsheet example</title>
<script type="module" crossorigin src="/modern_spreadsheet/demo.js"></script>
<link rel="stylesheet" href="/modern_spreadsheet/style.css">
</head> </head>
<body style="padding: 2rem;"> <body style="padding: 2rem;">
<div id="spreadsheet"></div> <div id="spreadsheet"></div>
<button id="save_button">Save sheet</button> <button id="save_button">Save sheet</button>
<button id="load_button">Load sheet</button> <button id="load_button">Load sheet</button>
<script type="module" src="/src/index.ts"></script>
</body> </body>
</html> </html>

View File

@ -1,8 +1,8 @@
{ {
"src/main.ts": { "index.html": {
"file": "main.cjs", "file": "demo.js",
"isEntry": true, "isEntry": true,
"src": "src/main.ts" "src": "index.html"
}, },
"style.css": { "style.css": {
"file": "style.css", "file": "style.css",

View File

@ -1,65 +0,0 @@
{
"name": "modern_spreadsheet",
"private": false,
"version": "0.0.33",
"exports": {
".": {
"import": "./dist/main.js",
"require": "./dist/main.cjs.js"
},
"./style.css": {
"import": "./dist/style.css",
"require": "./dist/style.css"
}
},
"main": "./dist/main.cjs.js",
"module": "./dist/main.es.js",
"typings": "./dist/main.d.ts",
"files": [
"dist",
"dist/*.css"
],
"type": "module",
"homepage": "https://github.com/yazmeyaa/modern_spreadsheet",
"license": "MIT",
"repository": {
"type": "git",
"url": "https://github.com/yazmeyaa/modern_spreadsheet"
},
"keywords": [
"spreadsheet",
"table"
],
"scripts": {
"dev": "vite",
"build": "tsc && vite build",
"build:HTML": "tsc && cross-env BUILD_BROWSER=true vite build",
"build:watch": "tsc && vite build --watch",
"preview": "vite preview",
"predeploy": "npm run dist",
"deploy": "gh-pages -d dist",
"lint": "eslint src --ext .ts",
"lint:fix": "eslint src --ext .ts --fix",
"format": "prettier --write ./src/**/*.ts"
},
"devDependencies": {
"@rollup/plugin-typescript": "^11.1.2",
"@types/node": "^20.4.4",
"@typescript-eslint/eslint-plugin": "^6.2.0",
"@typescript-eslint/parser": "^6.2.0",
"cross-env": "^7.0.3",
"eslint": "^8.45.0",
"eslint-config-prettier": "^8.8.0",
"gh-pages": "^5.0.0",
"prettier": "3.0.0",
"rollup-plugin-typescript-paths": "^1.4.0",
"sass": "^1.63.6",
"tslib": "^2.6.0",
"typescript": "^5.0.2",
"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

View File

@ -1 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" class="iconify iconify--logos" width="31.88" height="32" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 257"><defs><linearGradient id="IconifyId1813088fe1fbc01fb466" x1="-.828%" x2="57.636%" y1="7.652%" y2="78.411%"><stop offset="0%" stop-color="#41D1FF"></stop><stop offset="100%" stop-color="#BD34FE"></stop></linearGradient><linearGradient id="IconifyId1813088fe1fbc01fb467" x1="43.376%" x2="50.316%" y1="2.242%" y2="89.03%"><stop offset="0%" stop-color="#FFEA83"></stop><stop offset="8.333%" stop-color="#FFDD35"></stop><stop offset="100%" stop-color="#FFA800"></stop></linearGradient></defs><path fill="url(#IconifyId1813088fe1fbc01fb466)" d="M255.153 37.938L134.897 252.976c-2.483 4.44-8.862 4.466-11.382.048L.875 37.958c-2.746-4.814 1.371-10.646 6.827-9.67l120.385 21.517a6.537 6.537 0 0 0 2.322-.004l117.867-21.483c5.438-.991 9.574 4.796 6.877 9.62Z"></path><path fill="url(#IconifyId1813088fe1fbc01fb467)" d="M185.432.063L96.44 17.501a3.268 3.268 0 0 0-2.634 3.014l-5.474 92.456a3.268 3.268 0 0 0 3.997 3.378l24.777-5.718c2.318-.535 4.413 1.507 3.936 3.838l-7.361 36.047c-.495 2.426 1.782 4.5 4.151 3.78l15.304-4.649c2.372-.72 4.652 1.36 4.15 3.788l-11.698 56.621c-.732 3.542 3.979 5.473 5.943 2.437l1.313-2.028l72.516-144.72c1.215-2.423-.88-5.186-3.54-4.672l-25.505 4.922c-2.396.462-4.435-1.77-3.759-4.114l16.646-57.705c.677-2.35-1.37-4.583-3.769-4.113Z"></path></svg>

Before

Width:  |  Height:  |  Size: 1.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 36 KiB

View File

@ -1,122 +0,0 @@
import Spreadsheet, { RenderBox } from "../main";
export class ColumnsBar {
public element: HTMLCanvasElement;
private root: Spreadsheet;
public height: number = 35;
public width: number;
// private resizerWidth = 1;
ctx: CanvasRenderingContext2D;
constructor(root: Spreadsheet) {
this.root = root;
this.element = this.createElement();
const ctx = this.element.getContext("2d");
if (!ctx) throw new Error("Enable hardware acceleration");
this.ctx = ctx;
this.width = this.root.viewProps.width;
}
private createElement(): HTMLCanvasElement {
const element = document.createElement("canvas");
element.style.position = "absolute";
element.style.height = this.height + "px";
element.style.width = this.root.viewProps.width + "px";
element.style.display = "block";
element.style.borderLeft = "1px solid black";
// element.style.boxSizing = 'border-box'
element.width = this.root.viewProps.width;
element.height = this.height;
return element;
}
public setElementPosition(top: number, left: number) {
this.element.style.top = top + "px";
this.element.style.left = left + "px";
}
private isColumnSelected(column: number): boolean {
const { selectedCell, selectedRange } = this.root.selection;
if (selectedCell && selectedCell.column === column) return true;
if (selectedRange) {
const inRange =
column >=
Math.min(selectedRange.from.column, selectedRange.to.column) &&
column <= Math.max(selectedRange.from.column, selectedRange.to.column);
return inRange;
}
return false;
}
// private getYCoordWithOffset(renderBox: RenderBox): number {
// const {y} = renderBox
// return y + this.root.toolbarHeight
// }
// private getXCoordWithOffset(renderBox: RenderBox): number {
// const {x} = renderBox
// return x
// }
private renderText(column: number, renderBox: RenderBox) {
const { width, x } = renderBox;
this.ctx.fillStyle = "black";
this.ctx.textAlign = "center";
this.ctx.textBaseline = "middle";
this.ctx.font = "12px Arial";
this.ctx.fillText(
this.root.config.columns[column].title,
x + width / 2 - this.root.viewport.left,
0 + this.height / 2,
);
}
private renderRect(column: number, renderBox: RenderBox) {
const { width, x } = renderBox;
const isColSelected = this.isColumnSelected(column);
this.ctx.fillStyle = isColSelected ? "#c7ebff" : "white";
this.ctx.strokeStyle = "black";
this.ctx.lineWidth = 1;
const specialX = x - this.root.viewport.left;
this.ctx.fillRect(specialX - 1, 0, width, this.height);
this.ctx.strokeRect(specialX - 1, 0, width, this.height);
}
private renderSingleColumn(column: number) {
const renderBox = new RenderBox(this.root.config, {
row: 0,
column: column,
});
this.renderRect(column, renderBox);
this.renderText(column, renderBox);
}
public renderBar() {
const lastColIdx = this.root.viewport.lastCol + 3;
const firstColIdx = this.root.viewport.firstCol;
this.ctx.beginPath();
this.ctx.strokeStyle = "black";
this.ctx.lineWidth = 1;
this.ctx.moveTo(0, 0);
this.ctx.lineTo(0, this.height);
this.ctx.closePath();
this.ctx.stroke();
for (let col = firstColIdx; col <= lastColIdx; col++) {
if (!this.root.config.columns[col]) break;
this.renderSingleColumn(col);
}
}
}

View File

@ -1,80 +0,0 @@
import Spreadsheet, { CSS_PREFIX } from "../main";
import { Position } from "../modules/cell";
import { EventTypes } from "../modules/events";
import { RenderBox } from "../modules/renderBox";
export class Editor {
element: HTMLInputElement;
root: Spreadsheet;
constructor(root: Spreadsheet) {
this.root = root;
const element = document.createElement("input");
element.classList.add(CSS_PREFIX + "editor");
this.element = element;
this.hide();
}
hide() {
this.element.style.display = "none";
this.element.classList.add("hide");
this.element.blur();
window.removeEventListener("click", this.handleClickOutside);
this.element.removeEventListener("keydown", this.handleKeydown);
this.root.focusTable();
}
show(position: Position, initialString?: string) {
const { height, width, x, y } = new RenderBox(this.root.config, position);
const cell = this.root.getCell(position);
this.element.classList.remove("hide");
this.element.style.top =
y - this.root.viewport.top + this.root.columnsBarHeight + "px";
this.element.style.left =
x - this.root.viewport.left + this.root.rowsBarWidth + "px";
this.element.style.width = width + "px";
this.element.style.height = height + "px";
this.element.style.display = "block";
window.addEventListener("click", this.handleClickOutside);
this.element.addEventListener("keydown", this.handleKeydown);
this.element.value = initialString ? initialString : cell.value;
this.element.focus();
if (!initialString) this.element.select();
}
handleKeydown = (event: KeyboardEvent) => {
const { key } = event;
switch (key) {
case "Escape": {
this.hide();
break;
}
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),
});
this.hide();
this.root.renderSelection();
}
}
};
handleClickOutside = (event: MouseEvent) => {
const target = event.target as HTMLElement;
if (!this.element.contains(target)) {
this.hide();
}
};
}

View File

@ -1,109 +0,0 @@
import Spreadsheet, { RenderBox } from "../main";
export class RowsBar {
element: HTMLCanvasElement;
ctx: CanvasRenderingContext2D;
root: Spreadsheet;
width: number = 35;
height: number;
resizerHeight = 1;
constructor(root: Spreadsheet) {
this.root = root;
this.element = this.createElement();
const ctx = this.element.getContext("2d");
if (!ctx) throw new Error("Enable hardware acceleration");
this.ctx = ctx;
this.height = this.root.viewProps.height;
}
private createElement() {
const element = document.createElement("canvas");
element.style.position = "absolute";
element.style.height = this.root.viewProps.height + "px";
element.style.width = this.width + "px";
element.style.display = "block";
element.style.borderTop = "1px solid black";
// element.style.boxSizing = 'border-box'
element.width = this.width;
element.height = this.root.viewProps.height;
return element;
}
public setElementPosition(top: number, left: number) {
this.element.style.top = top + "px";
this.element.style.left = left + "px";
}
private isRowSelected(row: number): boolean {
const { selectedCell, selectedRange } = this.root.selection;
if (selectedCell && selectedCell.row === row) return true;
if (selectedRange) {
const inRange =
row >= Math.min(selectedRange.from.row, selectedRange.to.row) &&
row <= Math.max(selectedRange.from.row, selectedRange.to.row);
return inRange;
}
return false;
}
private renderText(row: number, renderBox: RenderBox) {
const { y, height } = renderBox;
this.ctx.fillStyle = "black";
this.ctx.textAlign = "center";
this.ctx.textBaseline = "middle";
this.ctx.font = "12px Arial";
this.ctx.fillText(
this.root.config.rows[row].title,
this.width / 2,
y - this.root.viewport.top + height / 2,
);
}
private renderRect(column: number, renderBox: RenderBox) {
const { y, height } = renderBox;
const isRowSeleted = this.isRowSelected(column);
this.ctx.fillStyle = isRowSeleted ? "#c7ebff" : "white";
this.ctx.strokeStyle = "black";
this.ctx.lineWidth = this.resizerHeight;
const specialY = y - this.root.viewport.top;
this.ctx.fillRect(0, specialY - 1, this.width, height);
this.ctx.strokeRect(0, specialY - 1, this.width, height);
}
private renderSingleRow(row: number) {
const renderBox = new RenderBox(this.root.config, {
column: 0,
row: row,
});
this.renderRect(row, renderBox);
this.renderText(row, renderBox);
}
public renderBar() {
const lastRowIdx = this.root.viewport.lastRow + 3;
const firstRowIdx = this.root.viewport.firstRow;
this.ctx.beginPath();
this.ctx.moveTo(0, 0);
this.ctx.strokeStyle = "black";
this.ctx.lineWidth = 16;
this.ctx.lineTo(35, 0);
this.ctx.closePath();
this.ctx.stroke();
for (let row = firstRowIdx; row <= lastRowIdx; row++) {
if (!this.root.config.rows[row]) break;
this.renderSingleRow(row);
}
}
}

View File

@ -1,320 +0,0 @@
import Spreadsheet, { CSS_PREFIX, Cell, Selection } from "../main";
import { EventTypes } from "../modules/events";
import { checkEqualCellSelections } from "../utils/position";
export interface ViewportRect {
top: number;
left: number;
right: number;
bottom: number;
}
export class Scroller {
element: HTMLDivElement;
private verticalScroller: HTMLDivElement;
private horizontalScroller: HTMLDivElement;
private root: Spreadsheet;
private isSelecting = false;
constructor(root: Spreadsheet) {
this.root = root;
const { horizontalScroller, scroller, verticalScroller } =
this.buildComponent();
this.element = scroller;
this.verticalScroller = verticalScroller;
this.horizontalScroller = horizontalScroller;
this.element.style.height = this.root.config.view.height + "px";
this.element.style.width = this.root.config.view.width + "px";
this.element.style.top = this.root.columnsBarHeight + "px";
this.element.style.left = this.root.rowsBarWidth + "px";
this.element.tabIndex = -1;
this.updateScrollerSize(); //* Init size set
this.element.addEventListener("scroll", this.handleScroll);
this.element.addEventListener("mousedown", this.handleClick);
this.element.addEventListener("mousemove", this.handleMouseMove);
this.element.addEventListener("mouseup", this.handleMouseUp);
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) {
this.isSelecting = mode;
}
private handleMouseMove = (event: MouseEvent) => {
if (!this.isSelecting) return;
const { offsetX, offsetY } = event;
const lastSelectedCell = this.root.getCellByCoords(offsetX, offsetY);
let isRangeChanged = false;
if (this.root.selection.selectedRange) {
isRangeChanged = !checkEqualCellSelections(
this.root.selection.selectedRange.to,
lastSelectedCell,
);
if (isRangeChanged) {
this.root.selection.selectedRange.to = lastSelectedCell;
this.root.events.dispatch({
type: EventTypes.SELECTION_CHANGE,
selection: this.root.selection,
enableCallback: true,
});
}
}
};
private handleMouseUp = () => {
this.isSelecting = false;
const newSelection = { ...this.root.selection };
if (this.root.selection.selectedRange) {
if (
checkEqualCellSelections(
this.root.selection.selectedRange.from,
this.root.selection.selectedRange.to,
)
) {
newSelection.selectedRange = null;
this.root.events.dispatch({
type: EventTypes.SELECTION_CHANGE,
selection: newSelection,
enableCallback: false,
});
}
}
this.root.renderSheet();
this.root.renderColumnsBar();
this.root.renderRowsBar();
};
private handleDoubleClick = (event: MouseEvent) => {
event.preventDefault();
const position = this.root.getCellByCoords(event.offsetX, event.offsetY);
this.root.showEditor(position);
};
private handleKeydown = (event: KeyboardEvent) => {
//* Navigation
if (
["ArrowLeft", "ArrowRight", "ArrowUp", "ArrowDown"].includes(event.key)
) {
event.preventDefault();
this.root.selection.selectedRange = null;
switch (event.key) {
case "ArrowLeft": {
if (
this.root.selection.selectedCell &&
this.root.selection.selectedCell.column > 0
) {
this.root.selection.selectedCell.column -= 1;
// this.root.renderSheet();
}
break;
}
case "ArrowRight": {
if (
this.root.selection.selectedCell &&
this.root.selection.selectedCell.column <
this.root.config.columns.length - 1
) {
this.root.selection.selectedCell.column += 1;
// this.root.renderSheet();
}
break;
}
case "ArrowUp": {
if (
this.root.selection.selectedCell &&
this.root.selection.selectedCell.row > 0
) {
this.root.selection.selectedCell.row -= 1;
// this.root.renderSheet();
}
break;
}
case "ArrowDown": {
if (
this.root.selection.selectedCell &&
this.root.selection.selectedCell.row <
this.root.config.rows.length - 1
) {
this.root.selection.selectedCell.row += 1;
// this.root.renderSheet();
}
break;
}
}
this.root.events.dispatch({
type: EventTypes.SELECTION_CHANGE,
selection: this.root.selection,
enableCallback: true,
});
}
//* Start typings
const keysRegex = /^([a-z]|[а-я]|[0-9]|=)$/;
if (!event.metaKey && !event.ctrlKey) {
//* Prevent handle shortcutrs
const isPressedLetterKey = keysRegex.test(event.key.toLowerCase());
if (event.key === "F2" || isPressedLetterKey) {
//* English and Russian keyboard. Or F2 button
event.preventDefault();
if (!this.root.selection.selectedCell) return;
this.root.showEditor(
this.root.selection.selectedCell,
isPressedLetterKey ? event.key : undefined,
);
}
}
if (event.key === "Delete") {
event.preventDefault();
this.root.deleteSelectedCellsValues();
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) => {
this.root.events.dispatch({
type: EventTypes.CELL_CLICK,
event,
scroller: this,
});
};
private handleScroll = () => {
const rect = this.getViewportBoundlingRect();
this.root.viewport.updateValues(rect);
this.root.renderSheet();
this.root.renderColumnsBar();
this.root.renderRowsBar();
};
public getViewportBoundlingRect(): ViewportRect {
const { scrollTop, scrollLeft } = this.element;
const { height, width } = this.element.getBoundingClientRect();
const bottom = scrollTop + height;
const right = scrollLeft + width;
return {
top: scrollTop,
left: scrollLeft,
bottom,
right,
};
}
private buildComponent() {
const scroller = document.createElement("div");
const verticalScroller = document.createElement("div");
const horizontalScroller = document.createElement("div");
const groupScrollers = document.createElement("div");
const stack = document.createElement("div");
verticalScroller.style.width = "0px";
verticalScroller.style.pointerEvents = "none";
horizontalScroller.style.pointerEvents = "none";
groupScrollers.style.display = "flex";
stack.appendChild(verticalScroller);
stack.appendChild(horizontalScroller);
groupScrollers.appendChild(stack);
this.verticalScroller = verticalScroller;
this.horizontalScroller = horizontalScroller;
scroller.appendChild(groupScrollers);
scroller.contentEditable = "false";
scroller.classList.add(CSS_PREFIX + "scroller");
return { scroller, verticalScroller, horizontalScroller };
}
private getActualHeight() {
return this.root.config.rows.reduce((acc, curr) => {
acc += curr.height;
return acc;
}, 0);
}
private getActualWidth() {
return this.root.config.columns.reduce((acc, curr) => {
acc += curr.width;
return acc;
}, 0);
}
updateScrollerSize() {
const totalHeight = this.getActualHeight();
const totalWidth = this.getActualWidth();
this.setScrollerHeight(totalHeight);
this.setScrollerWidth(totalWidth);
}
private setScrollerHeight(height: number) {
this.verticalScroller.style.height = height + "px";
}
private setScrollerWidth(width: number) {
this.horizontalScroller.style.width = width + "px";
}
}

View File

@ -1,138 +0,0 @@
import Spreadsheet, { CSS_PREFIX, RenderBox } from "../main";
import { Position } from "../modules/cell";
/**
* Display (CANVAS) element where cells render
*/
export class Sheet {
element: HTMLCanvasElement;
ctx: CanvasRenderingContext2D;
root: Spreadsheet;
constructor(root: Spreadsheet) {
this.root = root;
const canvas = document.createElement("canvas");
canvas.classList.add(CSS_PREFIX + "sheet");
//* Set up canvas sizes based on provided root config
canvas.height = this.root.config.view.height;
canvas.width = this.root.config.view.width;
canvas.style.width = this.root.config.view.width + "px";
canvas.style.height = this.root.config.view.height + "px";
canvas.style.left = "0px";
this.element = canvas;
const ctx = this.element.getContext("2d");
if (!ctx) throw new Error("Enable hardware acceleration");
this.ctx = ctx;
}
getCellByCoords(x: number, y: number): Position {
let row = 0;
let height = 0;
while (height <= y) {
height += this.root.config.rows[row].height;
if (height >= y) break;
row++;
}
let col = 0;
let width = 0;
while (width <= x) {
width += this.root.config.columns[col].width;
if (width >= x) break;
col++;
}
return new Position(row, col);
}
renderCell(position: Position) {
const { column, row } = position;
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() {
const firstRowIdx = this.root.viewport.firstRow;
const lastColIdx = this.root.viewport.lastCol + 3;
const lastRowIdx = this.root.viewport.lastRow + 3;
const firstColIdx = this.root.viewport.firstCol;
for (let row = firstRowIdx; row <= lastRowIdx; row++) {
for (let col = firstColIdx; col <= lastColIdx; col++) {
if (!this.root.config.columns[col] || !this.root.config.rows[row])
break; //* Prevent read undefined
this.renderCell({ column: col, row });
}
}
this.renderSelection();
}
}

View File

@ -1,22 +0,0 @@
import Spreadsheet, { CSS_PREFIX } from "../main";
import { ViewProperties } from "../modules/config";
/** Base (root) component */
export class Table {
element: HTMLDivElement;
root: Spreadsheet;
constructor(root: Spreadsheet) {
this.root = root;
const container = document.createElement("div");
container.classList.add(CSS_PREFIX + "spreadsheet_container");
this.element = container;
this.changeElementSizes(this.root.viewProps);
}
changeElementSizes(sizes: ViewProperties) {
const { height, width } = sizes;
this.element.style.width = width + this.root.rowsBarWidth + "px";
this.element.style.height = height + this.root.columnsBarHeight + "px";
}
}

View File

@ -1,13 +0,0 @@
import Spreadsheet, { CSS_PREFIX } from "../main";
export class Toolbar {
element: HTMLDivElement;
root: Spreadsheet;
height: number = 0;
constructor(root: Spreadsheet) {
this.root = root;
const toolbarElement = document.createElement("div");
toolbarElement.classList.add(CSS_PREFIX + "toolbar");
this.element = toolbarElement;
}
}

View File

@ -1,38 +0,0 @@
import Spreadsheet, { SpreadsheetConstructorProperties } from "./main";
const options: SpreadsheetConstructorProperties = {
onCellClick: (event, cell) => {
console.log("Cell click", event, cell);
},
onSelectionChange: (selection) => {
console.log("Changed selection: ", selection);
},
onCellChange(cell) {
console.log("Cell changed: ", cell);
},
onCopy: (range, data, dataAsString) => {
console.log("Copy event: ", range, data, dataAsString)
}
};
const sheet = new Spreadsheet("#spreadsheet", options);
function saveDataToLS() {
const serializableData = sheet.serializeData();
localStorage.setItem("sheet", JSON.stringify(serializableData));
}
function loadDataFromLS() {
const data = localStorage.getItem("sheet");
if (!data) return;
const json = JSON.parse(data);
sheet.loadData(json);
}
const saveButton = document.querySelector("#save_button");
const loadButton = document.querySelector("#load_button");
if (!saveButton || !loadButton) throw new Error("LOST");
saveButton.addEventListener("click", saveDataToLS);
loadButton.addEventListener("click", loadDataFromLS);

View File

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

View File

@ -1,67 +0,0 @@
export interface CachedColumnProperties {
xPos: number;
colIdx: number;
}
export class CachedColumn {
xPos: number;
colIdx: number;
constructor(props: CachedColumnProperties) {
this.xPos = props.xPos;
this.colIdx = props.colIdx;
}
}
export interface CachedRowProperties {
yPos: number;
rowIdx: number;
}
export class CachedRow {
yPos: number;
rowIdx: number;
constructor(props: CachedRowProperties) {
this.yPos = props.yPos;
this.rowIdx = props.rowIdx;
}
}
export interface CacheConstructorProps {
columns: CachedColumn[];
rows: CachedRow[];
}
export class Cache {
public columns: CachedColumn[];
public rows: CachedRow[];
constructor(initial: CacheConstructorProps) {
this.columns = initial.columns;
this.rows = initial.rows;
}
public getRowByYCoord(y: number): number {
let rowIdx = 0;
for (let i = 0; i < this.rows.length; i++) {
rowIdx = i
if (y <= this.rows[i].yPos) { //* Intersection detect
break;
}
}
return rowIdx;
}
public getColumnByXCoord(x: number): number {
let colIdx = 0;
for (let i = 0; i < this.columns.length; i++) {
colIdx = i
if (x <= this.columns[i].xPos) { //* Intersection detect
break;
}
}
return colIdx;
}
}

View File

@ -1,155 +0,0 @@
import Spreadsheet from "../main";
import { FormulaParser } from "./formulaParser";
import { RenderBox } from "./renderBox";
export type CellConstructorProps = {
value: string;
displayValue: string;
resultValue: string;
position: Position;
style: CellStyles | null;
};
interface CellStylesConstructorProps {
fontSize: number;
fontColor: string;
background: string;
borderColor: string;
selectedBackground: string;
selectedFontColor: string;
}
export class CellStyles {
fontSize: number = 16;
fontColor: string = "black";
background: string = "white";
borderColor: string = "black";
selectedBackground = "#4287f5";
selectedFontColor = "#ffffff";
constructor(props?: CellStylesConstructorProps) {
if (props) {
Object.assign(this, props); // Override default styles
}
}
}
export class Position {
row: number;
column: number;
constructor(row: number, column: number) {
this.row = row;
this.column = column;
}
}
export class SerializableCell {
value: string;
displayValue: string;
resultValue: string;
position: Position;
style: CellStyles | null;
constructor(props: SerializableCell | SerializableCell) {
this.value = props.value;
this.displayValue = props.displayValue;
this.resultValue = props.resultValue;
this.position = props.position;
this.style = props.style;
}
}
export class Cell {
/** True value (data) */
value: string;
/** Value to render */
displayValue: string;
/** This refers to the values that were obtained by calculations, for example, after calculating the formula */
resultValue: string;
position: Position;
style: CellStyles | null = null;
cellsDependsOnThisCell: Position[] = []
dependedFromCells: Position[] = []
constructor(props: CellConstructorProps) {
this.value = props.value;
this.displayValue = props.displayValue;
this.resultValue = props.resultValue;
this.position = props.position;
this.style = props.style;
}
public getSerializableCell(): SerializableCell {
const cell: SerializableCell = new SerializableCell({
displayValue: this.displayValue,
position: this.position,
resultValue: this.resultValue,
style: this.style,
value: this.value,
});
return cell;
}
changeStyles(styles: CellStyles) {
this.style = styles;
}
changeValues(values: Partial<Omit<CellConstructorProps, "position">>) {
Object.assign(this, values);
}
evalFormula(parser: FormulaParser) {
if (this.value.substring(0, 1) !== '=') return;
this.resultValue = parser.parser.parse(this.value.slice(1), {
col: this.position.column,
row: this.position.row
})
}
// 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) {
const renderBox = new RenderBox(root.config, this.position);
let { x, y } = renderBox;
const { height, width } = renderBox;
const { ctx } = root;
// const isCellSelected =
// root.selection.selectedCell?.row === this.position.row &&
// root.selection.selectedCell.column === this.position.column;
// const isCellInRange = this.isCellInRange(root);
y -= root.viewport.top;
x -= root.viewport.left;
const styles = this.style ?? root.styles.cells;
ctx.clearRect(x, y, width, height);
ctx.fillStyle = styles.background;
ctx.strokeStyle = "black";
ctx.fillRect(x, y, width - 1, height - 1);
ctx.strokeRect(x, y, width, height);
ctx.fillStyle = styles.fontColor;
ctx.textAlign = "left";
ctx.font = `${styles.fontSize}px Arial`;
ctx.textBaseline = "middle";
ctx.fillText(this.displayValue, x + 2, y + height / 2);
}
}

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,14 +0,0 @@
export type ColumnConstructorProperties = {
width: number;
title: string;
};
export class Column {
width: number;
title: string;
constructor(props: ColumnConstructorProperties) {
this.width = props.width;
this.title = props.title;
}
}

View File

@ -1,63 +0,0 @@
import { Cell } from "./cell";
import { Column } from "./column";
import { Row } from "./row";
import { RangeSelectionType, Selection } from "./selection";
export interface ViewProperties {
width: 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 = {
/** Please, end it with '_' symbol.
*
* *Example:*
*
* 'test_'
* 'google_' */
rows: Row[];
columns: Column[];
view: ViewProperties;
onCellClick?: CellClickEvent | null;
onSelectionChange?: SelectionChangeEvent | null;
onCellChange?: CellChangeEvent | null;
onCopy?: CopyEvent | null;
};
export type SheetConfigConstructorProps = {
rows: Row[];
columns: Column[];
};
export class Config {
rows: Row[];
columns: Column[];
view: ViewProperties = {
width: 800,
height: 600,
};
onCellClick: CellClickEvent | null = null;
onSelectonChange: SelectionChangeEvent | null = null;
onCellChange: CellChangeEvent | null = null;
onCopy: CopyEvent | null;
constructor(props: ConfigProperties) {
this.columns = props.columns;
this.rows = props.rows;
this.view = props.view;
this.onCellClick = props.onCellClick ?? null;
this.onSelectonChange = props.onSelectionChange ?? null;
this.onCellChange = props.onCellChange ?? null;
this.onCopy = props.onCopy ?? null;
}
}

View File

@ -1,142 +0,0 @@
import { Scroller } from "../components/scroller";
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 = {
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 class Events {
root: Spreadsheet;
constructor(root: Spreadsheet) {
this.root = root;
}
async dispatch(action: ActionTypes) {
switch (action.type) {
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) => {
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);
const selection = new Selection();
selection.selectedCell = clickedCell;
selection.selectedRange = {
from: clickedCell,
to: clickedCell,
};
scroller.setSelectingMode(true);
this.changeSelection(selection, true);
this.root.config.onCellClick?.(event, cell);
};
private changeSelection = (selection: Selection, enableCallback = false) => {
this.root.selection = selection;
if (enableCallback) this.root.config.onSelectonChange?.(selection);
this.root.renderSheet();
this.root.renderColumnsBar();
this.root.renderRowsBar();
};
private changeCellValues(cell: Cell, enableCallback: boolean = true) {
if (enableCallback) this.root.config.onCellChange?.(cell);
}
private copy = (
range: RangeSelectionType,
data: Cell[][],
dataAsString: string,
) => {
this.root.config.onCopy?.(range, data, dataAsString);
};
}

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,34 +0,0 @@
import { Position } from "./cell";
import { Config } from "./config";
export class RenderBox {
x: number;
y: number;
width: number;
height: number;
constructor(config: Config, cellPosition: Position) {
this.x = this.getXCoord(cellPosition.column, config);
this.y = this.getYCoord(cellPosition.row, config);
this.width = config.columns[cellPosition.column].width;
this.height = config.rows[cellPosition.row].height;
}
private getXCoord(column: number, config: Config): number {
let x = 0;
for (let i = 0; i < column; i++) {
x += config.columns[i].width;
}
return x;
}
private getYCoord(row: number, config: Config): number {
let y = 0;
for (let i = 0; i < row; i++) {
y += config.rows[i].height;
}
return y;
}
}

View File

@ -1,13 +0,0 @@
export type RowConstructorProps = {
height: number;
title: string;
};
export class Row {
height: number;
title: string;
constructor(props: RowConstructorProps) {
this.height = props.height;
this.title = props.title;
}
}

View File

@ -1,14 +0,0 @@
export type BaseSelectionType = {
row: number;
column: number;
};
export type RangeSelectionType = {
from: BaseSelectionType;
to: BaseSelectionType;
};
export class Selection {
selectedCell: BaseSelectionType | null = null;
selectedRange: RangeSelectionType | null = null;
}

View File

@ -1,8 +0,0 @@
import { CellStyles } from "./cell";
export class Styles {
cells: CellStyles;
constructor() {
this.cells = new CellStyles();
}
}

View File

@ -1,78 +0,0 @@
import Spreadsheet from "../main";
export type ViewportConstructorProps = {
top: number;
left: number;
right: number;
bottom: number;
};
export class Viewport {
root: Spreadsheet;
top: number;
left: number;
right: number;
bottom: number;
firstRow: number;
lastRow: number;
firstCol: number;
lastCol: number;
constructor(root: Spreadsheet, props: ViewportConstructorProps) {
this.root = root;
this.top = props.top;
this.left = props.left;
this.right = props.right;
this.bottom = props.bottom;
this.firstRow = this.getFirstRow();
this.lastCol = this.getFirstRow(); //!Temp
this.firstCol = this.getFirstRow(); //!Temp
this.lastRow = this.getLastRow();
this.updateValues({
top: 0,
left: 0,
right: this.root.viewProps.width,
bottom: this.root.viewProps.height,
});
}
updateValues(props: ViewportConstructorProps) {
this.top = props.top;
this.left = props.left;
this.right = props.right;
this.bottom = props.bottom;
this.firstRow = this.getFirstRow();
this.lastRow = this.getLastRow();
this.firstCol = this.getFirstCol();
this.lastCol = this.getLastCol();
}
/** Get index of first row in viewport */
private getFirstRow(): number {
const rowIdx = this.root.cache.getRowByYCoord(this.top);
return rowIdx;
}
private getLastRow(): number {
const rowIdx = this.root.cache.getRowByYCoord(this.bottom);
return rowIdx;
}
private getFirstCol(): number {
const colIdx = this.root.cache.getColumnByXCoord(this.left);
return colIdx;
}
private getLastCol(): number {
const colIdx = this.root.cache.getColumnByXCoord(this.right);
return colIdx;
}
}

View File

@ -1,4 +0,0 @@
body {
padding: 0px;
margin: 0px;
}

View File

@ -1,38 +0,0 @@
$css_prefix: "modern_sc_";
.#{$css_prefix}spreadsheet_container {
position: relative;
isolation: isolate;
border: 2px solid black;
}
.#{$css_prefix}content {
position: absolute;
}
.#{$css_prefix}sheet {
display: block;
contain: strict;
}
.#{$css_prefix}scroller {
position: absolute;
overflow: scroll;
box-sizing: border-box;
transform: translateZ(0);
&:focus {
outline: none;
}
}
.#{$css_prefix}editor {
position: absolute;
box-sizing: border-box;
font-size: 16px;
font-family: Arial, Helvetica, sans-serif;
}
.#{$css_prefix}hide {
visibility: hidden;
}

View File

@ -1,2 +0,0 @@
@import "global.scss";
@import "spreadsheet.scss";

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,80 +0,0 @@
import { Cell } from "../modules/cell";
import { Column } from "../modules/column";
import { Config } from "../modules/config";
import { Row } from "../modules/row";
export function createSampleData(
rows: number,
columns: number,
fillCellsByCoords: boolean = false,
): Cell[][] {
const data: Cell[][] = [];
for (let row = 0; row <= rows; row++) {
const innerRow: Cell[] = [];
for (let col = 0; col <= columns; col++) {
const value = fillCellsByCoords ? `${row}:${col}` : "";
const cell = new Cell({
displayValue: value,
resultValue: value,
value,
position: {
column: col,
row: row,
},
style: null,
});
innerRow.push(cell);
}
data.push(innerRow);
}
return data;
}
export function createSampleConfig(rows: number, columns: number): Config {
const rowsArr: Row[] = [];
for (let i = 0; i <= rows; i++) {
const rowItem = new Row({
height: 40,
title: String(i),
});
rowsArr.push(rowItem);
}
const colsArr: Column[] = [];
for (let i = 0; i <= columns; i++) {
const colItem = new Column({
title: String(i),
width: 150,
});
colsArr.push(colItem);
}
const config = new Config({
columns: colsArr,
rows: rowsArr,
view: {
height: 600,
width: 800,
},
});
return config;
}
export type SpreadsheetConfigAndDataReturnType = {
config: Config;
data: Cell[][];
};
export function makeSpreadsheetConfigAndData(
rows: number,
columns: number,
): SpreadsheetConfigAndDataReturnType {
const data = createSampleData(rows, columns);
const config = createSampleConfig(rows, columns);
return { data, config };
}

View File

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

1
src/vite-env.d.ts vendored
View File

@ -1 +0,0 @@
/// <reference types="vite/client" />

View File

@ -1,30 +0,0 @@
{
"compilerOptions": {
"target": "ESNext",
"lib": ["ESNext", "DOM", "DOM.Iterable"],
"types": ["vite/client", "node"],
"allowJs": false,
"skipLibCheck": true,
"esModuleInterop": true,
"allowSyntheticDefaultImports": true,
"strict": true,
"forceConsistentCasingInFileNames": true,
"module": "ESNext",
"moduleResolution": "bundler",
"resolveJsonModule": true,
"isolatedModules": true,
"noEmit": true,
"useDefineForClassFields": true,
/* Bundler mode */
"allowImportingTsExtensions": true,
/* Linting */
"noUnusedLocals": true,
"noUnusedParameters": true,
"noFallthroughCasesInSwitch": true
},
"include": ["src"],
"exclude": ["node_modules"]
}

View File

@ -1,61 +0,0 @@
import { defineConfig } from "vite";
import path from "path";
import typescript from "@rollup/plugin-typescript";
import { typescriptPaths } from "rollup-plugin-typescript-paths";
const BROWSER_MODE = process.env.BUILD_BROWSER === 'true';
console.log({ BROWSER_MODE });
const libConfig = defineConfig({
base: "/modern_spreadsheet/",
plugins: [],
resolve: {},
server: {
port: 5179,
open: true,
},
build: {
manifest: true,
minify: true,
reportCompressedSize: true,
sourcemap: true,
lib: {
entry: path.resolve(__dirname, "src/main.ts"),
fileName: "main",
formats: ["es", "cjs"],
},
rollupOptions: {
external: ["./src/index.ts"],
plugins: [
typescriptPaths({
preserveExtensions: true,
}),
typescript({
sourceMap: false,
declaration: true,
outDir: "dist",
}),
],
},
},
});
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;

View File

Before

Width:  |  Height:  |  Size: 1.5 KiB

After

Width:  |  Height:  |  Size: 1.5 KiB