Compare commits

..

No commits in common. "main" and "build-config-export-css" have entirely different histories.

61 changed files with 1280 additions and 4278 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: {},
};

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 +1,17 @@
# Modern Spreadsheet # Modern Spreadsheet
<img src="https://raw.githubusercontent.com/yazmeyaa/modern_spreadsheet/6dc20f92e769210c076600c7fcfacd4ed528f085/repo_assets/spreadsheet_preview.png?raw=true" alt="spreadsheet_preview"> - <span>High performance spreadsheet based on CanvasAPI.</span>
- <span>TypeScript supported</span>
## Features: <div>
- High performance spreadsheet based on CanvasAPI. <span>Basic usage</span>
- TypeScript supported
- Native scrolling
- Customizable
- Copy & Paste support
### Basic usage ```js
import { Spreadsheet } from 'modern_spreadsheet'
import 'modern_spreadsheet/style.css' // <= this is required
```ts const target = document.getElementById('spreadsheet')
import Spreadsheet from "modern_spreadsheet";
import "modern_spreadsheet/style.css"; // <= this is required
const target = document.getElementById("spreadsheet"); const sheet = new Spreadsheet(target)
const sheet = new Spreadsheet(target);
//...
``` ```
</div>
### 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 (?)

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

6
dist/components/header.d.ts vendored Normal file
View File

@ -0,0 +1,6 @@
import { Spreadsheet } from "../main";
export declare class Header {
element: HTMLHeadElement;
root: Spreadsheet;
constructor(root: Spreadsheet);
}

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,4 +1,4 @@
import Spreadsheet from "../main"; import { Spreadsheet } from "../main";
export interface ViewportRect { export interface ViewportRect {
top: number; top: number;
left: number; left: number;
@ -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

@ -1,4 +1,4 @@
import Spreadsheet from "../main"; import { Spreadsheet } from "../main";
import { Position } from "../modules/cell"; import { Position } from "../modules/cell";
/** /**
* Display (CANVAS) element where cells render * Display (CANVAS) element where cells render
@ -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;
} }

View File

@ -1,4 +1,4 @@
import Spreadsheet from "../main"; import { Spreadsheet } from "../main";
import { ViewProperties } from "../modules/config"; import { ViewProperties } from "../modules/config";
/** Base (root) component */ /** Base (root) component */
export declare class Table { export declare class Table {

View File

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

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

63
dist/main.d.ts vendored
View File

@ -1,26 +1,19 @@
import { Cell, CellConstructorProps, CellStyles, Position, SerializableCell } from "./modules/cell"; import { Cell, CellConstructorProps, Position } 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 class Spreadsheet {
export default class Spreadsheet {
private table; private table;
private scroller; private scroller;
private toolbar; private toolbar;
private rowsBar; private header;
private columnsBar;
private sheet; private sheet;
private editor; private editor;
styles: Styles; styles: Styles;
@ -29,54 +22,22 @@ 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 setColumnsBarPosition;
private setElementsPositions;
private getInitialCache; private getInitialCache;
private buildComponent; private buildComponent;
/**Destroy spreadsheet DOM element.
*
* May be usefull when need to rerender component.
*/
destroy(): void;
private appendTableToTarget; private appendTableToTarget;
/** Canvas rendering context 2D.
*
* Abble to draw on canvas with default CanvasAPI methods
*/
get ctx(): CanvasRenderingContext2D; get ctx(): CanvasRenderingContext2D;
get viewProps(): ViewProperties; get viewProps(): ViewProperties;
get columnsBarHeight(): number;
get rowsBarWidth(): number;
get toolbarHeight(): number;
/** Focusing on interactive part of 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; applyActionToRange(range: RangeSelectionType, callback: (cell: Cell) => any): void;
applyActionToRange(range: RangeSelectionType, callback: (cell: Cell) => void): void;
deleteSelectedCellsValues(): void; deleteSelectedCellsValues(): void;
showEditor(position: Position, initialString?: string): void; showEditor(position: Position): void;
renderSheet(): void; renderSheet(): void;
renderSelection(): void;
renderColumnsBar(): void;
renderRowsBar(): void;
renderCell(row: number, col: number): void; renderCell(row: number, col: number): void;
loadData(data: Cell[][] | SerializableCell[][]): Spreadsheet; loadData(data: Cell[][]): void;
private makeConfigFromData; private makeConfigFromData;
serializeData(): SerializableCell[][];
} }
export * from "./modules/cache"; export {};
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";

1050
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

@ -1,10 +1,9 @@
import Spreadsheet from "../main"; import { Spreadsheet } from "../main";
export type CellConstructorProps = { export type CellConstructorProps = {
value: string; value: string;
displayValue: string; displayValue: string;
resultValue: string; resultValue: string;
position: Position; position: Position;
style: CellStyles | null;
}; };
interface CellStylesConstructorProps { interface CellStylesConstructorProps {
fontSize: number; fontSize: number;
@ -28,27 +27,16 @@ export declare class Position {
column: number; column: number;
constructor(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 { export declare class Cell {
/** True value (data) */
value: string; value: string;
/** Value to render */
displayValue: string; displayValue: string;
/** This refers to the values that were obtained by calculations, for example, after calculating the formula */ /** This refers to the values that were obtained by calculations, for example, after calculating the formula */
resultValue: string; resultValue: string;
position: Position; position: Position;
style: CellStyles | null; style: CellStyles;
constructor(props: CellConstructorProps); constructor(props: CellConstructorProps);
getSerializableCell(): SerializableCell; changeValues(values: Partial<Omit<CellConstructorProps, 'position'>>): void;
changeStyles(styles: CellStyles): void; private isCellInRange;
changeValues(values: Partial<Omit<CellConstructorProps, "position">>): void;
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;
}

View File

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

View File

@ -1,4 +1,4 @@
import Spreadsheet from "../main"; import { Spreadsheet } from "../main";
export type ViewportConstructorProps = { export type ViewportConstructorProps = {
top: number; top: number;
left: number; left: number;

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}.content{position:absolute;top:0;left:0}.spreadsheet_container{position:relative;isolation:isolate;border:2px solid black}.sheet{display:block;contain:strict}.scroller{overflow:scroll;box-sizing:border-box;transform:translateZ(0)}.scroller:focus{outline:none}.editor{position:absolute;box-sizing:border-box;font-size:16px;font-family:Arial,Helvetica,sans-serif}.hide{visibility:hidden}

View File

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

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,4 +1,4 @@
<!doctype html> <!DOCTYPE html>
<html lang="en"> <html lang="en">
<head> <head>
<meta charset="UTF-8" /> <meta charset="UTF-8" />
@ -6,10 +6,8 @@
<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>
</head> </head>
<body style="padding: 2rem;"> <body>
<div id="spreadsheet"></div> <div id="spreadsheet"></div>
<button id="save_button">Save sheet</button>
<button id="load_button">Load sheet</button>
<script type="module" src="/src/index.ts"></script> <script type="module" src="/src/index.ts"></script>
</body> </body>
</html> </html>

View File

@ -1,7 +1,7 @@
{ {
"name": "modern_spreadsheet", "name": "modern_spreadsheet",
"private": false, "private": false,
"version": "0.0.33", "version": "0.0.19",
"exports": { "exports": {
".": { ".": {
"import": "./dist/main.js", "import": "./dist/main.js",
@ -21,7 +21,6 @@
], ],
"type": "module", "type": "module",
"homepage": "https://github.com/yazmeyaa/modern_spreadsheet", "homepage": "https://github.com/yazmeyaa/modern_spreadsheet",
"license": "MIT",
"repository": { "repository": {
"type": "git", "type": "git",
"url": "https://github.com/yazmeyaa/modern_spreadsheet" "url": "https://github.com/yazmeyaa/modern_spreadsheet"
@ -33,33 +32,19 @@
"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: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/parser": "^6.2.0",
"cross-env": "^7.0.3",
"eslint": "^8.45.0",
"eslint-config-prettier": "^8.8.0",
"gh-pages": "^5.0.0", "gh-pages": "^5.0.0",
"prettier": "3.0.0",
"rollup-plugin-typescript-paths": "^1.4.0", "rollup-plugin-typescript-paths": "^1.4.0",
"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

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

12
src/components/header.ts Normal file
View File

@ -0,0 +1,12 @@
import { Spreadsheet } from "../main"
export class Header {
element: HTMLHeadElement
root: Spreadsheet
constructor(root: Spreadsheet) {
this.root = root
const headerElement = document.createElement('header')
headerElement.classList.add()
this.element = headerElement
}
}

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

View File

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

View File

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

View File

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

View File

@ -1,38 +1,5 @@
import Spreadsheet, { SpreadsheetConstructorProperties } from "./main"; import Spreadsheet, { createSampleData, } from './main'
const options: SpreadsheetConstructorProperties = { const sheet = new Spreadsheet('#spreadsheet').loadData(createSampleData(20, 20, true))
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); console.log(sheet)
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,36 +1,19 @@
import { Editor } from "./components/editor"; import { Editor } from "./components/editor";
import { Header } from "./components/header";
import { Scroller } from "./components/scroller"; import { Scroller } from "./components/scroller";
import { Sheet } from "./components/sheet"; import { Sheet } from "./components/sheet";
import { Table } from "./components/table"; import { Table } from "./components/table";
import { Toolbar } from "./components/toolbar"; import { Toolbar } from "./components/toolbar";
import { import { Cell, CellConstructorProps, Position } from "./modules/cell";
Cell, import { Config, ViewProperties } from "./modules/config";
CellConstructorProps,
CellStyles,
Position,
SerializableCell,
} from "./modules/cell";
import {
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";
import "./scss/main.scss"; import './scss/main.scss'
import { createSampleData } from "./utils/createData"; import { createSampleConfig, createSampleData } from "./utils/createData";
import { Cache, CachedColumn, CachedRow } from "./modules/cache"; import { Cache, CachedColumn, CachedRow } from "./modules/cache";
import { Row } from "./modules/row"; import { Row } from "./modules/row";
import { Column } from "./modules/column"; 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 ! Component structure
@ -44,400 +27,238 @@ import { FormulaParser } from "./modules/formulaParser";
</Table> </Table>
*/ */
export interface SpreadsheetConstructorProperties { interface SpreadsheetConstructorProperties {
view?: ViewProperties; config?: Omit<Config, 'view'> // Not optional.
onCellClick?: CellClickEvent | null; view?: ViewProperties
onSelectionChange?: SelectionChangeEvent | null;
onCellChange?: CellChangeEvent | null;
onCopy?: CopyEvent | null
} }
export const CSS_PREFIX = "modern_sc_";
export default class Spreadsheet { export default class Spreadsheet {
private table: Table; private table: Table
private scroller: Scroller; private scroller: Scroller
private toolbar: Toolbar; private toolbar: Toolbar
private rowsBar: RowsBar; private header: Header
private columnsBar: ColumnsBar; private sheet: Sheet
private sheet: Sheet; private editor: Editor
private editor: Editor; public styles: Styles
public styles: Styles; public config: Config
public config: Config; public data: Cell[][]
public data: Cell[][]; public viewport: Viewport
public viewport: Viewport; public selection: Selection
public selection: Selection; public cache: Cache
public cache: Cache;
public events: Events;
public clipboard: Clipboard;
public formulaParser: FormulaParser
constructor( constructor(target: string | HTMLElement, props?: SpreadsheetConstructorProperties) {
target: string | HTMLElement, const config = createSampleConfig(500, 500)
props?: SpreadsheetConstructorProperties,
) {
const data = createSampleData(40, 40);
const config = this.makeConfigFromData(
data,
props?.view ?? { height: 600, width: 800 },
);
if (props?.view) { if (props?.view) {
config.view = props.view; config.view = props.view
} }
this.config = new Config(config); this.config = new Config(config)
this.sheet = new Sheet(this)
const data = createSampleData(500, 500)
this.table = new Table(this)
this.scroller = new Scroller(this)
this.toolbar = new Toolbar(this)
this.header = new Header(this)
this.editor = new Editor(this)
this.cache = this.getInitialCache()
this.viewport = new Viewport(this, this.scroller.getViewportBoundlingRect())
this.selection = new Selection()
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.data = data
this.columnsBar = new ColumnsBar(this); this.styles = new Styles()
this.sheet = new Sheet(this); this.buildComponent()
this.table = new Table(this); this.appendTableToTarget(target)
this.scroller = new Scroller(this); this.renderSheet()
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 { private getInitialCache(): Cache {
const cachedCols: CachedColumn[] = []; const cachedCols: CachedColumn[] = []
let currentWidth = 0; let currentWidth = 0
for (let i = 0; i <= this.config.columns.length - 1; i++) { for (let i = 0; i <= this.config.columns.length - 1; i++) {
const col = this.config.columns[i]; const col = this.config.columns[i]
currentWidth += col.width; currentWidth += col.width
const cacheCol = new CachedColumn({ const cacheCol = new CachedColumn({
xPos: currentWidth, xPos: currentWidth,
colIdx: i, colIdx: i
}); })
cachedCols.push(cacheCol); cachedCols.push(cacheCol)
} }
const cachedRows: CachedRow[] = []; const cachedRows: CachedRow[] = []
let currentHeight = 0; let currentHeight = 0
for (let i = 0; i <= this.config.rows.length - 1; i++) { for (let i = 0; i <= this.config.rows.length - 1; i++) {
const row = this.config.rows[i]; const row = this.config.rows[i]
currentHeight += row.height; currentHeight += row.height
const cacheRow = new CachedRow({ const cacheRow = new CachedRow({
yPos: currentHeight, yPos: currentHeight,
rowIdx: i, rowIdx: i
}); })
cachedRows.push(cacheRow); cachedRows.push(cacheRow)
} }
const cache = new Cache({ const cache = new Cache({
columns: cachedCols, columns: cachedCols,
rows: cachedRows, rows: cachedRows
}); })
return cache; console.log("CACHE: ", cache)
console.log("CONFIG: ", this.config)
return cache
} }
private buildComponent(): void { 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); const content = document.createElement('div') //* Abstract
content.appendChild(this.header.element)
content.appendChild(this.sheet.element)
content.classList.add(CSS_PREFIX + "content"); content.classList.add('content')
this.table.element.appendChild(this.toolbar.element); this.table.element.appendChild(this.toolbar.element)
this.table.element.appendChild(this.rowsBar.element); this.table.element.appendChild(content)
this.table.element.appendChild(this.columnsBar.element); this.table.element.appendChild(this.scroller.element)
this.table.element.appendChild(content); this.table.element.append(this.editor.element)
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) { private appendTableToTarget(target: string | HTMLElement) {
if (typeof target === "string") { if (typeof target === 'string') {
const element = document.querySelector(target); const element = document.querySelector(target)
if (!element) if (!element) throw new Error(`Element with selector ${target} is not finded in DOM.\n Make sure it exists.`)
throw new Error( element?.appendChild(this.table.element)
`Element with selector ${target} is not finded in DOM.\n Make sure it exists.`,
);
element?.appendChild(this.table.element);
} }
if (target instanceof HTMLElement) { if (target instanceof HTMLElement) {
target.append(this.table.element); target.append(this.table.element)
} }
} }
/** Canvas rendering context 2D.
*
* Abble to draw on canvas with default CanvasAPI methods
*/
get ctx() { get ctx() {
return this.sheet.ctx; return this.sheet.ctx
} }
get viewProps() { get viewProps() {
return this.config.view; 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() { focusTable() {
this.scroller.element.focus(); this.scroller.element.focus()
} }
getCellByCoords(x: number, y: number) { getCellByCoords(x: number, y: number) {
return this.sheet.getCellByCoords(x, y); return this.sheet.getCellByCoords(x, y)
} }
getCell(position: Position): Cell { getCell(position: Position): Cell {
const { column, row } = position; const { column, row } = position
return this.data[row][column]; return this.data[row][column]
} }
changeCellValues( changeCellValues(position: Position, values: Partial<Omit<CellConstructorProps, 'position'>>) {
position: Position, const { column, row } = position
values: Partial<Omit<CellConstructorProps, "position">>,
enableCallback: boolean = true
) {
const { column, row } = position;
this.data[row][column].changeValues(values)
this.data[row][column].changeValues(values); this.renderCell(row, column)
this.events.dispatch({
type: EventTypes.CELL_CHANGE,
cell: this.data[row][column],
enableCallback: enableCallback
})
this.renderCell(row, column);
} }
changeCellStyles(position: Position, styles: CellStyles) { applyActionToRange(range: RangeSelectionType, callback: (cell: Cell) => any): void {
const { column, row } = position; const fromRow = Math.min(range.from.row, range.to.row)
this.data[row][column].changeStyles(styles); const toRow = Math.max(range.from.row, range.to.row)
this.renderCell(row, column);
}
applyActionToRange( const fromCol = Math.min(range.from.column, range.to.column)
range: RangeSelectionType, const toCol = Math.max(range.from.column, range.to.column)
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 row = fromRow; row <= toRow; row++) {
for (let col = fromCol; col <= toCol; col++) { for (let col = fromCol; col <= toCol; col++) {
const cell = this.data[row][col]; const cell = this.data[row][col]
callback(cell); callback(cell)
} }
} }
} }
deleteSelectedCellsValues() { deleteSelectedCellsValues() {
if (this.selection.selectedRange !== null) { if (this.selection.selectedRange !== null) {
this.applyActionToRange(this.selection.selectedRange, (cell) => {
this.applyActionToRange(this.selection.selectedRange, cell => {
this.changeCellValues(cell.position, { this.changeCellValues(cell.position, {
displayValue: "", displayValue: '',
resultValue: "", resultValue: '',
value: "", value: ''
}); })
}); })
} else { } else {
if (!this.selection.selectedCell) return; if (!this.selection.selectedCell) return;
this.changeCellValues(this.selection.selectedCell, { this.changeCellValues(this.selection.selectedCell, {
displayValue: "", displayValue: '',
resultValue: "", resultValue: '',
value: "", value: ''
}); })
} }
} }
showEditor(position: Position, initialString?: string) { showEditor(position: Position) {
this.editor.show(position, initialString); this.editor.show(position)
} }
renderSheet() { renderSheet() {
this.sheet.renderSheet(); this.sheet.renderSheet()
}
renderSelection() {
this.sheet.renderSelection();
}
renderColumnsBar() {
this.columnsBar.renderBar();
}
renderRowsBar() {
this.rowsBar.renderBar();
} }
renderCell(row: number, col: number) { renderCell(row: number, col: number) {
this.data[row][col].render(this); this.data[row][col].render(this)
} }
public loadData(data: Cell[][] | SerializableCell[][]): Spreadsheet { public loadData(data: Cell[][]): Spreadsheet {
const rowsLength = data.length; this.data = data
const colsLength = data[0] ? data[0].length : 0; this.config = this.makeConfigFromData(data, this.config.view)
this.data = []; this.cache = this.getInitialCache()
this.scroller.updateScrollerSize()
this.viewport = new Viewport(this, this.scroller.getViewportBoundlingRect())
this.renderSheet()
const formattedData: Cell[][] = []; return this
// 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 { private makeConfigFromData(data: Cell[][], view: ViewProperties): Config {
const lastRowIdx = data.length - 1; const lastRowIdx = data.length - 1
const lastColIdx = data[0] ? data[0].length : 0; const lastColIdx = data[0] ? data[0].length : 0
const rows: Row[] = []; const rows: Row[] = []
for (let row = 0; row < lastRowIdx; row++) { for (let row = 0; row < lastRowIdx; row++) {
rows.push( rows.push(new Row({
new Row({
height: 40, height: 40,
title: String(row), title: String(row)
}), }))
);
} }
const columns: Column[] = []; const columns: Column[] = []
for (let col = 0; col < lastColIdx; col++) { for (let col = 0; col < lastColIdx; col++) {
columns.push( columns.push(new Column({
new Column({
width: 150, width: 150,
title: String(col), title: String(col)
}), }))
);
} }
const config = new Config({ const config = new Config({
view, view,
rows, rows,
columns, columns
onCellClick: null, })
});
return config; 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/cache'
export * from "./modules/cell"; export * from './modules/cell'
export * from "./modules/column"; export * from './modules/column'
export * from "./modules/config"; export * from './modules/config'
export * from "./modules/renderBox"; export * from './modules/renderBox'
export * from "./modules/row"; export * from './modules/row'
export * from "./modules/selection"; export * from './modules/selection'
export * from "./modules/styles"; export * from './modules/styles'
export * from "./modules/viewport"; export * from './modules/viewport'
export * from "./utils/createData"; export * from './utils/createData'

View File

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

View File

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

View File

@ -1,20 +1,10 @@
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.
@ -23,41 +13,27 @@ export type ConfigProperties = {
* *
* 'test_' * 'test_'
* 'google_' */ * 'google_' */
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[]
columns: Column[]; columns: Column[]
}; }
export class Config { export class Config {
rows: Row[]; rows: Row[]
columns: Column[]; columns: Column[]
view: ViewProperties = { view: ViewProperties = {
width: 800, width: 800,
height: 600, height: 600,
}; }
onCellClick: CellClickEvent | null = null;
onSelectonChange: SelectionChangeEvent | 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.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

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -1,2 +1,2 @@
@import "global.scss"; @import 'global.scss';
@import "spreadsheet.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

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

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

@ -1,17 +1,15 @@
import { defineConfig } from "vite"; 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 }); base: '/modern_spreadsheet/',
const libConfig = defineConfig({
base: "/modern_spreadsheet/",
plugins: [], plugins: [],
resolve: {}, resolve: {},
server: { server: {
port: 5179, port: 3000,
open: true, open: true,
}, },
build: { build: {
@ -28,34 +26,14 @@ const libConfig = defineConfig({
external: ["./src/index.ts"], external: ["./src/index.ts"],
plugins: [ plugins: [
typescriptPaths({ typescriptPaths({
preserveExtensions: true, preserveExtensions: true
}), }),
typescript({ typescript({
sourceMap: false, sourceMap: false,
declaration: true, declaration: true,
outDir: "dist", 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;