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
<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:
- High performance spreadsheet based on CanvasAPI.
- TypeScript supported
- Native scrolling
- Customizable
- Copy & Paste support
<div>
<span>Basic usage</span>
### Basic usage
```js
import { Spreadsheet } from 'modern_spreadsheet'
import 'modern_spreadsheet/style.css' // <= this is required
```ts
import Spreadsheet from "modern_spreadsheet";
import "modern_spreadsheet/style.css"; // <= this is required
const target = document.getElementById('spreadsheet')
const target = document.getElementById("spreadsheet");
const sheet = new Spreadsheet(target);
//...
const sheet = new Spreadsheet(target)
```
### Save and load data
```ts
function saveData() {
const serialized = sheet.serializeData();
localStorage.setItem("sheet_data", JSON.stringify(serialized));
}
function loadData() {
const data = localStorage.getItem("sheet_data");
const json = JSON.parse(data);
if (!json) return;
sheet.loadData(json);
}
```
#### Supported events
- onCellClick
- onSelectionChange
- onCellChange
- onCopy
### Using events examples
```ts
import Spreadsheet, { SpreadsheetConstructorProperties } from "./main";
const options: SpreadsheetConstructorProperties = {
onCellClick: (event, cell) => {
console.log("Cell click", event, cell);
},
onSelectionChange: (selection) => {
console.log("Changed selection: ", selection);
},
onCellChange = (cell) => {
console.log("Cell changed: ", cell);
},
onCopy: (range, data, dataAsString) => {
console.log("Copy event: ", range, data, dataAsString)
}
};
const sheet = new Spreadsheet("#spreadsheet", options);
```
### Roadmap
- ~~Rows number and columns heading render~~
- ~~Custom event functions (ex.: onSelectionChange, onCellEdit...). Full list of supported events will available on this page~~
- ~~Copy & Paste support~~
- Rows and columns resizing
- Toolbar
- Context menu
- Formulas support
- Selected cell depends cells highlight
- Async formulas support
- Mutlisheets (?)
</div>

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";
export declare class Editor {
element: HTMLInputElement;
root: Spreadsheet;
constructor(root: Spreadsheet);
hide(): void;
show(position: Position, initialString?: string): void;
show(position: Position): void;
handleKeydown: (event: KeyboardEvent) => 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 {
top: number;
left: number;
@ -12,7 +12,6 @@ export declare class Scroller {
private root;
private isSelecting;
constructor(root: Spreadsheet);
setSelectingMode(mode: boolean): void;
private handleMouseMove;
private handleMouseUp;
private handleDoubleClick;

View File

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

View File

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

View File

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

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

View File

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

View File

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

View File

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

View File

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

View File

@ -1,4 +1,4 @@
import Spreadsheet from "../main";
import { Spreadsheet } from "../main";
export type ViewportConstructorProps = {
top: 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";
export declare function createSampleData(rows: number, columns: number, fillCellsByCoords?: boolean): Cell[][];
export declare function createSampleConfig(rows: number, columns: number): Config;
export type SpreadsheetConfigAndDataReturnType = {
type SpreadsheetConfigAndDataReturnType = {
config: Config;
data: Cell[][];
};
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">
<head>
<meta charset="UTF-8" />
@ -6,10 +6,8 @@
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Spreadsheet example</title>
</head>
<body style="padding: 2rem;">
<body>
<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>
</body>
</html>

View File

@ -1,7 +1,7 @@
{
"name": "modern_spreadsheet",
"private": false,
"version": "0.0.33",
"version": "0.0.19",
"exports": {
".": {
"import": "./dist/main.js",
@ -21,7 +21,6 @@
],
"type": "module",
"homepage": "https://github.com/yazmeyaa/modern_spreadsheet",
"license": "MIT",
"repository": {
"type": "git",
"url": "https://github.com/yazmeyaa/modern_spreadsheet"
@ -33,33 +32,19 @@
"scripts": {
"dev": "vite",
"build": "tsc && vite build",
"build:HTML": "tsc && cross-env BUILD_BROWSER=true vite build",
"build:watch": "tsc && vite build --watch",
"preview": "vite preview",
"predeploy": "npm run dist",
"deploy": "gh-pages -d dist",
"lint": "eslint src --ext .ts",
"lint:fix": "eslint src --ext .ts --fix",
"format": "prettier --write ./src/**/*.ts"
"deploy": "gh-pages -d dist"
},
"devDependencies": {
"@rollup/plugin-typescript": "^11.1.2",
"@types/node": "^20.4.4",
"@typescript-eslint/eslint-plugin": "^6.2.0",
"@typescript-eslint/parser": "^6.2.0",
"cross-env": "^7.0.3",
"eslint": "^8.45.0",
"eslint-config-prettier": "^8.8.0",
"gh-pages": "^5.0.0",
"prettier": "3.0.0",
"rollup-plugin-typescript-paths": "^1.4.0",
"sass": "^1.63.6",
"tslib": "^2.6.0",
"typescript": "^5.0.2",
"vite": "^4.4.0",
"vite-plugin-html": "^3.2.0"
},
"dependencies": {
"fast-formula-parser": "^1.0.19"
"vite": "^4.4.0"
}
}

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

View File

@ -1,138 +1,71 @@
import Spreadsheet, { CSS_PREFIX, RenderBox } from "../main";
import { Position } from "../modules/cell";
import { Spreadsheet } from "../main"
import { Position } from "../modules/cell"
/**
* Display (CANVAS) element where cells render
*/
export class Sheet {
element: HTMLCanvasElement;
ctx: CanvasRenderingContext2D;
root: Spreadsheet;
constructor(root: Spreadsheet) {
this.root = root;
const canvas = document.createElement("canvas");
canvas.classList.add(CSS_PREFIX + "sheet");
element: HTMLCanvasElement
ctx: CanvasRenderingContext2D
root: Spreadsheet
constructor(root: Spreadsheet) {
this.root = root
const canvas = document.createElement('canvas')
canvas.classList.add('sheet')
//* Set up canvas sizes based on provided root config
canvas.height = this.root.config.view.height;
canvas.width = this.root.config.view.width;
canvas.style.width = this.root.config.view.width + "px";
canvas.style.height = this.root.config.view.height + "px";
canvas.style.left = "0px";
//* Set up canvas sizes based on provided root config
canvas.height = this.root.config.view.height
canvas.width = this.root.config.view.width
canvas.style.width = this.root.config.view.width + 'px'
canvas.style.height = this.root.config.view.height + 'px'
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 {
let row = 0;
let height = 0;
while (height <= y) {
height += this.root.config.rows[row].height;
if (height >= y) break;
row++;
}
let col = 0;
let width = 0;
while (width <= x) {
width += this.root.config.columns[col].width;
if (width >= x) break;
col++;
getCellByCoords(x: number, y: number): Position {
let row = 0;
let height = 0
while (height <= y) {
height += this.root.config.rows[row].height
if (height >= y) break;
row++;
}
let col = 0;
let width = 0;
while (width <= x) {
width += this.root.config.columns[col].width
if (width >= x) break;
col++;
}
return new Position(row, col)
}
return new Position(row, col);
}
renderCell(position: Position) {
const { column, row } = position;
this.root.data[row][column].render(this.root);
}
private getSelectionRange() {
const { selectedCell, selectedRange } = this.root.selection;
if (!selectedCell && !selectedRange) return;
if (selectedRange) {
const startRow = Math.min(selectedRange.from.row, selectedRange.to.row);
const startCol = Math.min(
selectedRange.from.column,
selectedRange.to.column,
);
const lastRow = Math.max(selectedRange.from.row, selectedRange.to.row);
const lastCol = Math.max(
selectedRange.from.column,
selectedRange.to.column,
);
const startCellBox = new RenderBox(this.root.config, {
row: startRow,
column: startCol,
});
let width = 0;
for (let col = startCol; col <= lastCol; col++) {
width += this.root.config.columns[col].width;
}
let height = 0;
for (let row = startRow; row <= lastRow; row++) {
height += this.root.config.rows[row].height;
}
const x = startCellBox.x - this.root.viewport.left;
const y = startCellBox.y - this.root.viewport.top;
return { x, y, height, width };
renderCell(position: Position) {
const { column, row } = position
this.root.data[row][column].render(this.root)
}
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;
renderSheet() {
const firstRowIdx = this.root.viewport.firstRow
const lastColIdx = this.root.viewport.lastCol + 3
const lastRowIdx = this.root.viewport.lastRow + 3
const firstColIdx = this.root.viewport.firstCol
for (let row = firstRowIdx; row <= lastRowIdx; row++) {
for (let col = firstColIdx; col <= lastColIdx; col++) {
if (!this.root.config.columns[col] || !this.root.config.rows[row]) break; //* Prevent read undefined
this.renderCell({ column: col, row })
}
}
}
}
private renderSelectionRange(
x: number,
y: number,
width: number,
height: number,
) {
this.ctx.save();
this.ctx.strokeStyle = "#7da8ff";
this.ctx.lineWidth = 3;
this.ctx.strokeRect(x, y, width, height);
this.ctx.fillStyle = "#7da8ff35";
this.ctx.fillRect(x, y, width, height);
this.ctx.restore();
}
renderSelection() {
const box = this.getSelectionRange();
if (!box) return;
const { height, width, x, y } = box;
this.renderSelectionRange(x, y, width, height);
}
renderSheet() {
const firstRowIdx = this.root.viewport.firstRow;
const lastColIdx = this.root.viewport.lastCol + 3;
const lastRowIdx = this.root.viewport.lastRow + 3;
const firstColIdx = this.root.viewport.firstCol;
for (let row = firstRowIdx; row <= lastRowIdx; row++) {
for (let col = firstColIdx; col <= lastColIdx; col++) {
if (!this.root.config.columns[col] || !this.root.config.rows[row])
break; //* Prevent read undefined
this.renderCell({ column: col, row });
}
}
this.renderSelection();
}
}
}

View File

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

View File

@ -1,38 +1,5 @@
import Spreadsheet, { SpreadsheetConstructorProperties } from "./main";
import Spreadsheet, { createSampleData, } 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').loadData(createSampleData(20, 20, true))
const sheet = new Spreadsheet("#spreadsheet", options);
function saveDataToLS() {
const serializableData = sheet.serializeData();
localStorage.setItem("sheet", JSON.stringify(serializableData));
}
function loadDataFromLS() {
const data = localStorage.getItem("sheet");
if (!data) return;
const json = JSON.parse(data);
sheet.loadData(json);
}
const saveButton = document.querySelector("#save_button");
const loadButton = document.querySelector("#load_button");
if (!saveButton || !loadButton) throw new Error("LOST");
saveButton.addEventListener("click", saveDataToLS);
loadButton.addEventListener("click", loadDataFromLS);
console.log(sheet)

View File

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

View File

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

View File

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

View File

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

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -1,61 +1,39 @@
import { defineConfig } from "vite";
import path from "path";
import { defineConfig } from 'vite'
import path from 'path'
import typescript from "@rollup/plugin-typescript";
import { typescriptPaths } from "rollup-plugin-typescript-paths";
import { fileURLToPath } from 'node:url';
const BROWSER_MODE = process.env.BUILD_BROWSER === 'true';
console.log({ BROWSER_MODE });
const libConfig = defineConfig({
base: "/modern_spreadsheet/",
plugins: [],
resolve: {},
server: {
port: 5179,
open: true,
},
build: {
manifest: true,
minify: true,
reportCompressedSize: true,
sourcemap: true,
lib: {
entry: path.resolve(__dirname, "src/main.ts"),
fileName: "main",
formats: ["es", "cjs"],
export default defineConfig({
base: '/modern_spreadsheet/',
plugins: [],
resolve: {},
server: {
port: 3000,
open: true,
},
rollupOptions: {
external: ["./src/index.ts"],
plugins: [
typescriptPaths({
preserveExtensions: true,
}),
typescript({
sourceMap: false,
declaration: true,
outDir: "dist",
}),
],
},
},
});
const browserConfig = defineConfig({
base: "/modern_spreadsheet/",
resolve: {},
build: {
manifest: true,
minify: true,
reportCompressedSize: true,
sourcemap: true,
lib: {
entry: path.resolve(__dirname, 'index.html'),
fileName: 'demo',
formats: ['es']
build: {
manifest: true,
minify: true,
reportCompressedSize: true,
sourcemap: true,
lib: {
entry: path.resolve(__dirname, "src/main.ts"),
fileName: "main",
formats: ["es", "cjs"],
},
rollupOptions: {
external: ["./src/index.ts"],
plugins: [
typescriptPaths({
preserveExtensions: true
}),
typescript({
sourceMap: false,
declaration: true,
outDir: 'dist'
})
]
},
}
}
})
const config = BROWSER_MODE ? browserConfig : libConfig;
export default config;
})