Compare commits
No commits in common. "gh-pages" and "main" have entirely different histories.
|
|
@ -0,0 +1,30 @@
|
||||||
|
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: {},
|
||||||
|
};
|
||||||
|
|
@ -0,0 +1,23 @@
|
||||||
|
# Logs
|
||||||
|
logs
|
||||||
|
*.log
|
||||||
|
npm-debug.log*
|
||||||
|
yarn-debug.log*
|
||||||
|
yarn-error.log*
|
||||||
|
pnpm-debug.log*
|
||||||
|
lerna-debug.log*
|
||||||
|
|
||||||
|
node_modules
|
||||||
|
dist-ssr
|
||||||
|
*.local
|
||||||
|
|
||||||
|
# Editor directories and files
|
||||||
|
.vscode/*
|
||||||
|
!.vscode/extensions.json
|
||||||
|
.idea
|
||||||
|
.DS_Store
|
||||||
|
*.suo
|
||||||
|
*.ntvs*
|
||||||
|
*.njsproj
|
||||||
|
*.sln
|
||||||
|
*.sw?
|
||||||
|
|
@ -0,0 +1,2 @@
|
||||||
|
# Ignore artifacts:
|
||||||
|
dist
|
||||||
|
|
@ -0,0 +1 @@
|
||||||
|
{}
|
||||||
|
|
@ -0,0 +1,21 @@
|
||||||
|
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.
|
||||||
|
|
@ -0,0 +1,78 @@
|
||||||
|
# Modern Spreadsheet
|
||||||
|
|
||||||
|
<img src="https://raw.githubusercontent.com/yazmeyaa/modern_spreadsheet/6dc20f92e769210c076600c7fcfacd4ed528f085/repo_assets/spreadsheet_preview.png?raw=true" alt="spreadsheet_preview">
|
||||||
|
|
||||||
|
## Features:
|
||||||
|
- High performance spreadsheet based on CanvasAPI.
|
||||||
|
- TypeScript supported
|
||||||
|
- Native scrolling
|
||||||
|
- Customizable
|
||||||
|
- Copy & Paste support
|
||||||
|
|
||||||
|
### Basic usage
|
||||||
|
|
||||||
|
```ts
|
||||||
|
import Spreadsheet from "modern_spreadsheet";
|
||||||
|
import "modern_spreadsheet/style.css"; // <= this is required
|
||||||
|
|
||||||
|
const target = document.getElementById("spreadsheet");
|
||||||
|
const sheet = new Spreadsheet(target);
|
||||||
|
//...
|
||||||
|
```
|
||||||
|
|
||||||
|
### Save and load data
|
||||||
|
|
||||||
|
```ts
|
||||||
|
function saveData() {
|
||||||
|
const serialized = sheet.serializeData();
|
||||||
|
localStorage.setItem("sheet_data", JSON.stringify(serialized));
|
||||||
|
}
|
||||||
|
|
||||||
|
function loadData() {
|
||||||
|
const data = localStorage.getItem("sheet_data");
|
||||||
|
const json = JSON.parse(data);
|
||||||
|
if (!json) return;
|
||||||
|
sheet.loadData(json);
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Supported events
|
||||||
|
- onCellClick
|
||||||
|
- onSelectionChange
|
||||||
|
- onCellChange
|
||||||
|
- onCopy
|
||||||
|
|
||||||
|
### Using events examples
|
||||||
|
```ts
|
||||||
|
import Spreadsheet, { SpreadsheetConstructorProperties } from "./main";
|
||||||
|
|
||||||
|
const options: SpreadsheetConstructorProperties = {
|
||||||
|
onCellClick: (event, cell) => {
|
||||||
|
console.log("Cell click", event, cell);
|
||||||
|
},
|
||||||
|
onSelectionChange: (selection) => {
|
||||||
|
console.log("Changed selection: ", selection);
|
||||||
|
},
|
||||||
|
onCellChange = (cell) => {
|
||||||
|
console.log("Cell changed: ", cell);
|
||||||
|
},
|
||||||
|
onCopy: (range, data, dataAsString) => {
|
||||||
|
console.log("Copy event: ", range, data, dataAsString)
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const sheet = new Spreadsheet("#spreadsheet", options);
|
||||||
|
```
|
||||||
|
|
||||||
|
### Roadmap
|
||||||
|
|
||||||
|
- ~~Rows number and columns heading render~~
|
||||||
|
- ~~Custom event functions (ex.: onSelectionChange, onCellEdit...). Full list of supported events will available on this page~~
|
||||||
|
- ~~Copy & Paste support~~
|
||||||
|
- Rows and columns resizing
|
||||||
|
- Toolbar
|
||||||
|
- Context menu
|
||||||
|
- Formulas support
|
||||||
|
- Selected cell depends cells highlight
|
||||||
|
- Async formulas support
|
||||||
|
- Mutlisheets (?)
|
||||||
File diff suppressed because one or more lines are too long
|
|
@ -0,0 +1,16 @@
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,11 @@
|
||||||
|
import Spreadsheet from "../main";
|
||||||
|
import { Position } from "../modules/cell";
|
||||||
|
export declare class Editor {
|
||||||
|
element: HTMLInputElement;
|
||||||
|
root: Spreadsheet;
|
||||||
|
constructor(root: Spreadsheet);
|
||||||
|
hide(): void;
|
||||||
|
show(position: Position, initialString?: string): void;
|
||||||
|
handleKeydown: (event: KeyboardEvent) => void;
|
||||||
|
handleClickOutside: (event: MouseEvent) => void;
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,17 @@
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,29 @@
|
||||||
|
import Spreadsheet from "../main";
|
||||||
|
export interface ViewportRect {
|
||||||
|
top: number;
|
||||||
|
left: number;
|
||||||
|
right: number;
|
||||||
|
bottom: number;
|
||||||
|
}
|
||||||
|
export declare class Scroller {
|
||||||
|
element: HTMLDivElement;
|
||||||
|
private verticalScroller;
|
||||||
|
private horizontalScroller;
|
||||||
|
private root;
|
||||||
|
private isSelecting;
|
||||||
|
constructor(root: Spreadsheet);
|
||||||
|
setSelectingMode(mode: boolean): void;
|
||||||
|
private handleMouseMove;
|
||||||
|
private handleMouseUp;
|
||||||
|
private handleDoubleClick;
|
||||||
|
private handleKeydown;
|
||||||
|
private handleClick;
|
||||||
|
private handleScroll;
|
||||||
|
getViewportBoundlingRect(): ViewportRect;
|
||||||
|
private buildComponent;
|
||||||
|
private getActualHeight;
|
||||||
|
private getActualWidth;
|
||||||
|
updateScrollerSize(): void;
|
||||||
|
private setScrollerHeight;
|
||||||
|
private setScrollerWidth;
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,17 @@
|
||||||
|
import Spreadsheet from "../main";
|
||||||
|
import { Position } from "../modules/cell";
|
||||||
|
/**
|
||||||
|
* Display (CANVAS) element where cells render
|
||||||
|
*/
|
||||||
|
export declare class Sheet {
|
||||||
|
element: HTMLCanvasElement;
|
||||||
|
ctx: CanvasRenderingContext2D;
|
||||||
|
root: Spreadsheet;
|
||||||
|
constructor(root: Spreadsheet);
|
||||||
|
getCellByCoords(x: number, y: number): Position;
|
||||||
|
renderCell(position: Position): void;
|
||||||
|
private getSelectionRange;
|
||||||
|
private renderSelectionRange;
|
||||||
|
renderSelection(): void;
|
||||||
|
renderSheet(): void;
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,9 @@
|
||||||
|
import Spreadsheet from "../main";
|
||||||
|
import { ViewProperties } from "../modules/config";
|
||||||
|
/** Base (root) component */
|
||||||
|
export declare class Table {
|
||||||
|
element: HTMLDivElement;
|
||||||
|
root: Spreadsheet;
|
||||||
|
constructor(root: Spreadsheet);
|
||||||
|
changeElementSizes(sizes: ViewProperties): void;
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,7 @@
|
||||||
|
import Spreadsheet from "../main";
|
||||||
|
export declare class Toolbar {
|
||||||
|
element: HTMLDivElement;
|
||||||
|
root: Spreadsheet;
|
||||||
|
height: number;
|
||||||
|
constructor(root: Spreadsheet);
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1 @@
|
||||||
|
export {};
|
||||||
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
|
|
@ -0,0 +1,82 @@
|
||||||
|
import { Cell, CellConstructorProps, CellStyles, Position, SerializableCell } from "./modules/cell";
|
||||||
|
import { CellChangeEvent, CellClickEvent, Config, CopyEvent, SelectionChangeEvent, ViewProperties } from "./modules/config";
|
||||||
|
import { RangeSelectionType, Selection } from "./modules/selection";
|
||||||
|
import { Styles } from "./modules/styles";
|
||||||
|
import { Viewport } from "./modules/viewport";
|
||||||
|
import "./scss/main.scss";
|
||||||
|
import { Cache } from "./modules/cache";
|
||||||
|
import { Events } from "./modules/events";
|
||||||
|
import { Clipboard } from "./modules/clipboard";
|
||||||
|
export interface SpreadsheetConstructorProperties {
|
||||||
|
view?: ViewProperties;
|
||||||
|
onCellClick?: CellClickEvent | null;
|
||||||
|
onSelectionChange?: SelectionChangeEvent | null;
|
||||||
|
onCellChange?: CellChangeEvent | null;
|
||||||
|
onCopy?: CopyEvent | null;
|
||||||
|
}
|
||||||
|
export declare const CSS_PREFIX = "modern_sc_";
|
||||||
|
export default class Spreadsheet {
|
||||||
|
private table;
|
||||||
|
private scroller;
|
||||||
|
private toolbar;
|
||||||
|
private rowsBar;
|
||||||
|
private columnsBar;
|
||||||
|
private sheet;
|
||||||
|
private editor;
|
||||||
|
styles: Styles;
|
||||||
|
config: Config;
|
||||||
|
data: Cell[][];
|
||||||
|
viewport: Viewport;
|
||||||
|
selection: Selection;
|
||||||
|
cache: Cache;
|
||||||
|
events: Events;
|
||||||
|
clipboard: Clipboard;
|
||||||
|
constructor(target: string | HTMLElement, props?: SpreadsheetConstructorProperties);
|
||||||
|
private setRowsBarPosition;
|
||||||
|
private setColumnsBarPosition;
|
||||||
|
private setElementsPositions;
|
||||||
|
private getInitialCache;
|
||||||
|
private buildComponent;
|
||||||
|
/**Destroy spreadsheet DOM element.
|
||||||
|
*
|
||||||
|
* May be usefull when need to rerender component.
|
||||||
|
*/
|
||||||
|
destroy(): void;
|
||||||
|
private appendTableToTarget;
|
||||||
|
/** Canvas rendering context 2D.
|
||||||
|
*
|
||||||
|
* Abble to draw on canvas with default CanvasAPI methods
|
||||||
|
*/
|
||||||
|
get ctx(): CanvasRenderingContext2D;
|
||||||
|
get viewProps(): ViewProperties;
|
||||||
|
get columnsBarHeight(): number;
|
||||||
|
get rowsBarWidth(): number;
|
||||||
|
get toolbarHeight(): number;
|
||||||
|
/** Focusing on interactive part of spreadsheet */
|
||||||
|
focusTable(): void;
|
||||||
|
getCellByCoords(x: number, y: number): Position;
|
||||||
|
getCell(position: Position): Cell;
|
||||||
|
changeCellValues(position: Position, values: Partial<Omit<CellConstructorProps, "position">>, enableCallback?: boolean): void;
|
||||||
|
changeCellStyles(position: Position, styles: CellStyles): void;
|
||||||
|
applyActionToRange(range: RangeSelectionType, callback: (cell: Cell) => void): void;
|
||||||
|
deleteSelectedCellsValues(): void;
|
||||||
|
showEditor(position: Position, initialString?: string): void;
|
||||||
|
renderSheet(): void;
|
||||||
|
renderSelection(): void;
|
||||||
|
renderColumnsBar(): void;
|
||||||
|
renderRowsBar(): void;
|
||||||
|
renderCell(row: number, col: number): void;
|
||||||
|
loadData(data: Cell[][] | SerializableCell[][]): Spreadsheet;
|
||||||
|
private makeConfigFromData;
|
||||||
|
serializeData(): SerializableCell[][];
|
||||||
|
}
|
||||||
|
export * from "./modules/cache";
|
||||||
|
export * from "./modules/cell";
|
||||||
|
export * from "./modules/column";
|
||||||
|
export * from "./modules/config";
|
||||||
|
export * from "./modules/renderBox";
|
||||||
|
export * from "./modules/row";
|
||||||
|
export * from "./modules/selection";
|
||||||
|
export * from "./modules/styles";
|
||||||
|
export * from "./modules/viewport";
|
||||||
|
export * from "./utils/createData";
|
||||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because one or more lines are too long
|
|
@ -1,8 +1,8 @@
|
||||||
{
|
{
|
||||||
"index.html": {
|
"src/main.ts": {
|
||||||
"file": "demo.js",
|
"file": "main.cjs",
|
||||||
"isEntry": true,
|
"isEntry": true,
|
||||||
"src": "index.html"
|
"src": "src/main.ts"
|
||||||
},
|
},
|
||||||
"style.css": {
|
"style.css": {
|
||||||
"file": "style.css",
|
"file": "style.css",
|
||||||
|
|
@ -0,0 +1,29 @@
|
||||||
|
export interface CachedColumnProperties {
|
||||||
|
xPos: number;
|
||||||
|
colIdx: number;
|
||||||
|
}
|
||||||
|
export declare class CachedColumn {
|
||||||
|
xPos: number;
|
||||||
|
colIdx: number;
|
||||||
|
constructor(props: CachedColumnProperties);
|
||||||
|
}
|
||||||
|
export interface CachedRowProperties {
|
||||||
|
yPos: number;
|
||||||
|
rowIdx: number;
|
||||||
|
}
|
||||||
|
export declare class CachedRow {
|
||||||
|
yPos: number;
|
||||||
|
rowIdx: number;
|
||||||
|
constructor(props: CachedRowProperties);
|
||||||
|
}
|
||||||
|
export interface CacheConstructorProps {
|
||||||
|
columns: CachedColumn[];
|
||||||
|
rows: CachedRow[];
|
||||||
|
}
|
||||||
|
export declare class Cache {
|
||||||
|
columns: CachedColumn[];
|
||||||
|
rows: CachedRow[];
|
||||||
|
constructor(initial: CacheConstructorProps);
|
||||||
|
getRowByYCoord(y: number): number;
|
||||||
|
getColumnByXCoord(x: number): number;
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,54 @@
|
||||||
|
import Spreadsheet from "../main";
|
||||||
|
export type CellConstructorProps = {
|
||||||
|
value: string;
|
||||||
|
displayValue: string;
|
||||||
|
resultValue: string;
|
||||||
|
position: Position;
|
||||||
|
style: CellStyles | null;
|
||||||
|
};
|
||||||
|
interface CellStylesConstructorProps {
|
||||||
|
fontSize: number;
|
||||||
|
fontColor: string;
|
||||||
|
background: string;
|
||||||
|
borderColor: string;
|
||||||
|
selectedBackground: string;
|
||||||
|
selectedFontColor: string;
|
||||||
|
}
|
||||||
|
export declare class CellStyles {
|
||||||
|
fontSize: number;
|
||||||
|
fontColor: string;
|
||||||
|
background: string;
|
||||||
|
borderColor: string;
|
||||||
|
selectedBackground: string;
|
||||||
|
selectedFontColor: string;
|
||||||
|
constructor(props?: CellStylesConstructorProps);
|
||||||
|
}
|
||||||
|
export declare class Position {
|
||||||
|
row: number;
|
||||||
|
column: number;
|
||||||
|
constructor(row: number, column: number);
|
||||||
|
}
|
||||||
|
export declare class SerializableCell {
|
||||||
|
value: string;
|
||||||
|
displayValue: string;
|
||||||
|
resultValue: string;
|
||||||
|
position: Position;
|
||||||
|
style: CellStyles | null;
|
||||||
|
constructor(props: SerializableCell | SerializableCell);
|
||||||
|
}
|
||||||
|
export declare class Cell {
|
||||||
|
/** True value (data) */
|
||||||
|
value: string;
|
||||||
|
/** Value to render */
|
||||||
|
displayValue: string;
|
||||||
|
/** This refers to the values that were obtained by calculations, for example, after calculating the formula */
|
||||||
|
resultValue: string;
|
||||||
|
position: Position;
|
||||||
|
style: CellStyles | null;
|
||||||
|
constructor(props: CellConstructorProps);
|
||||||
|
getSerializableCell(): SerializableCell;
|
||||||
|
changeStyles(styles: CellStyles): void;
|
||||||
|
changeValues(values: Partial<Omit<CellConstructorProps, "position">>): void;
|
||||||
|
render(root: Spreadsheet): void;
|
||||||
|
}
|
||||||
|
export {};
|
||||||
|
|
@ -0,0 +1,9 @@
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,9 @@
|
||||||
|
export type ColumnConstructorProperties = {
|
||||||
|
width: number;
|
||||||
|
title: string;
|
||||||
|
};
|
||||||
|
export declare class Column {
|
||||||
|
width: number;
|
||||||
|
title: string;
|
||||||
|
constructor(props: ColumnConstructorProperties);
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,41 @@
|
||||||
|
import { Cell } from "./cell";
|
||||||
|
import { Column } from "./column";
|
||||||
|
import { Row } from "./row";
|
||||||
|
import { RangeSelectionType, Selection } from "./selection";
|
||||||
|
export interface ViewProperties {
|
||||||
|
width: number;
|
||||||
|
height: number;
|
||||||
|
}
|
||||||
|
export type CellClickEvent = (event: MouseEvent, cell: Cell) => void;
|
||||||
|
export type SelectionChangeEvent = (selection: Selection) => void;
|
||||||
|
export type CellChangeEvent = (cell: Cell) => void;
|
||||||
|
export type CopyEvent = (range: RangeSelectionType, data: Cell[][], dataAsString: string) => void;
|
||||||
|
export type ConfigProperties = {
|
||||||
|
/** Please, end it with '_' symbol.
|
||||||
|
*
|
||||||
|
* *Example:*
|
||||||
|
*
|
||||||
|
* 'test_'
|
||||||
|
* 'google_' */
|
||||||
|
rows: Row[];
|
||||||
|
columns: Column[];
|
||||||
|
view: ViewProperties;
|
||||||
|
onCellClick?: CellClickEvent | null;
|
||||||
|
onSelectionChange?: SelectionChangeEvent | null;
|
||||||
|
onCellChange?: CellChangeEvent | null;
|
||||||
|
onCopy?: CopyEvent | null;
|
||||||
|
};
|
||||||
|
export type SheetConfigConstructorProps = {
|
||||||
|
rows: Row[];
|
||||||
|
columns: Column[];
|
||||||
|
};
|
||||||
|
export declare class Config {
|
||||||
|
rows: Row[];
|
||||||
|
columns: Column[];
|
||||||
|
view: ViewProperties;
|
||||||
|
onCellClick: CellClickEvent | null;
|
||||||
|
onSelectonChange: SelectionChangeEvent | null;
|
||||||
|
onCellChange: CellChangeEvent | null;
|
||||||
|
onCopy: CopyEvent | null;
|
||||||
|
constructor(props: ConfigProperties);
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,39 @@
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,11 @@
|
||||||
|
import { Position } from "./cell";
|
||||||
|
import { Config } from "./config";
|
||||||
|
export declare class RenderBox {
|
||||||
|
x: number;
|
||||||
|
y: number;
|
||||||
|
width: number;
|
||||||
|
height: number;
|
||||||
|
constructor(config: Config, cellPosition: Position);
|
||||||
|
private getXCoord;
|
||||||
|
private getYCoord;
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,9 @@
|
||||||
|
export type RowConstructorProps = {
|
||||||
|
height: number;
|
||||||
|
title: string;
|
||||||
|
};
|
||||||
|
export declare class Row {
|
||||||
|
height: number;
|
||||||
|
title: string;
|
||||||
|
constructor(props: RowConstructorProps);
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,12 @@
|
||||||
|
export type BaseSelectionType = {
|
||||||
|
row: number;
|
||||||
|
column: number;
|
||||||
|
};
|
||||||
|
export type RangeSelectionType = {
|
||||||
|
from: BaseSelectionType;
|
||||||
|
to: BaseSelectionType;
|
||||||
|
};
|
||||||
|
export declare class Selection {
|
||||||
|
selectedCell: BaseSelectionType | null;
|
||||||
|
selectedRange: RangeSelectionType | null;
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,5 @@
|
||||||
|
import { CellStyles } from "./cell";
|
||||||
|
export declare class Styles {
|
||||||
|
cells: CellStyles;
|
||||||
|
constructor();
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,25 @@
|
||||||
|
import Spreadsheet from "../main";
|
||||||
|
export type ViewportConstructorProps = {
|
||||||
|
top: number;
|
||||||
|
left: number;
|
||||||
|
right: number;
|
||||||
|
bottom: number;
|
||||||
|
};
|
||||||
|
export declare class Viewport {
|
||||||
|
root: Spreadsheet;
|
||||||
|
top: number;
|
||||||
|
left: number;
|
||||||
|
right: number;
|
||||||
|
bottom: number;
|
||||||
|
firstRow: number;
|
||||||
|
lastRow: number;
|
||||||
|
firstCol: number;
|
||||||
|
lastCol: number;
|
||||||
|
constructor(root: Spreadsheet, props: ViewportConstructorProps);
|
||||||
|
updateValues(props: ViewportConstructorProps): void;
|
||||||
|
/** Get index of first row in viewport */
|
||||||
|
private getFirstRow;
|
||||||
|
private getLastRow;
|
||||||
|
private getFirstCol;
|
||||||
|
private getLastCol;
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,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 = {
|
||||||
|
config: Config;
|
||||||
|
data: Cell[][];
|
||||||
|
};
|
||||||
|
export declare function makeSpreadsheetConfigAndData(rows: number, columns: number): SpreadsheetConfigAndDataReturnType;
|
||||||
|
|
@ -0,0 +1,3 @@
|
||||||
|
import { BaseSelectionType, RangeSelectionType } from "../main";
|
||||||
|
export declare function checkEqualRanges(range1: RangeSelectionType, range2: RangeSelectionType): boolean;
|
||||||
|
export declare function checkEqualCellSelections(selection1: BaseSelectionType, selection2: BaseSelectionType): boolean;
|
||||||
|
Before Width: | Height: | Size: 1.5 KiB After Width: | Height: | Size: 1.5 KiB |
|
|
@ -2,16 +2,14 @@
|
||||||
<html lang="en">
|
<html lang="en">
|
||||||
<head>
|
<head>
|
||||||
<meta charset="UTF-8" />
|
<meta charset="UTF-8" />
|
||||||
<link rel="icon" type="image/svg+xml" href="/modern_spreadsheet/vite.svg" />
|
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
<title>Spreadsheet example</title>
|
<title>Spreadsheet example</title>
|
||||||
<script type="module" crossorigin src="/modern_spreadsheet/demo.js"></script>
|
|
||||||
<link rel="stylesheet" href="/modern_spreadsheet/style.css">
|
|
||||||
</head>
|
</head>
|
||||||
<body style="padding: 2rem;">
|
<body style="padding: 2rem;">
|
||||||
<div id="spreadsheet"></div>
|
<div id="spreadsheet"></div>
|
||||||
<button id="save_button">Save sheet</button>
|
<button id="save_button">Save sheet</button>
|
||||||
<button id="load_button">Load sheet</button>
|
<button id="load_button">Load sheet</button>
|
||||||
|
<script type="module" src="/src/index.ts"></script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,65 @@
|
||||||
|
{
|
||||||
|
"name": "modern_spreadsheet",
|
||||||
|
"private": false,
|
||||||
|
"version": "0.0.33",
|
||||||
|
"exports": {
|
||||||
|
".": {
|
||||||
|
"import": "./dist/main.js",
|
||||||
|
"require": "./dist/main.cjs.js"
|
||||||
|
},
|
||||||
|
"./style.css": {
|
||||||
|
"import": "./dist/style.css",
|
||||||
|
"require": "./dist/style.css"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"main": "./dist/main.cjs.js",
|
||||||
|
"module": "./dist/main.es.js",
|
||||||
|
"typings": "./dist/main.d.ts",
|
||||||
|
"files": [
|
||||||
|
"dist",
|
||||||
|
"dist/*.css"
|
||||||
|
],
|
||||||
|
"type": "module",
|
||||||
|
"homepage": "https://github.com/yazmeyaa/modern_spreadsheet",
|
||||||
|
"license": "MIT",
|
||||||
|
"repository": {
|
||||||
|
"type": "git",
|
||||||
|
"url": "https://github.com/yazmeyaa/modern_spreadsheet"
|
||||||
|
},
|
||||||
|
"keywords": [
|
||||||
|
"spreadsheet",
|
||||||
|
"table"
|
||||||
|
],
|
||||||
|
"scripts": {
|
||||||
|
"dev": "vite",
|
||||||
|
"build": "tsc && vite build",
|
||||||
|
"build:HTML": "tsc && cross-env BUILD_BROWSER=true vite build",
|
||||||
|
"build:watch": "tsc && vite build --watch",
|
||||||
|
"preview": "vite preview",
|
||||||
|
"predeploy": "npm run dist",
|
||||||
|
"deploy": "gh-pages -d dist",
|
||||||
|
"lint": "eslint src --ext .ts",
|
||||||
|
"lint:fix": "eslint src --ext .ts --fix",
|
||||||
|
"format": "prettier --write ./src/**/*.ts"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@rollup/plugin-typescript": "^11.1.2",
|
||||||
|
"@types/node": "^20.4.4",
|
||||||
|
"@typescript-eslint/eslint-plugin": "^6.2.0",
|
||||||
|
"@typescript-eslint/parser": "^6.2.0",
|
||||||
|
"cross-env": "^7.0.3",
|
||||||
|
"eslint": "^8.45.0",
|
||||||
|
"eslint-config-prettier": "^8.8.0",
|
||||||
|
"gh-pages": "^5.0.0",
|
||||||
|
"prettier": "3.0.0",
|
||||||
|
"rollup-plugin-typescript-paths": "^1.4.0",
|
||||||
|
"sass": "^1.63.6",
|
||||||
|
"tslib": "^2.6.0",
|
||||||
|
"typescript": "^5.0.2",
|
||||||
|
"vite": "^4.4.0",
|
||||||
|
"vite-plugin-html": "^3.2.0"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"fast-formula-parser": "^1.0.19"
|
||||||
|
}
|
||||||
|
}
|
||||||
File diff suppressed because it is too large
Load Diff
|
|
@ -0,0 +1 @@
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" class="iconify iconify--logos" width="31.88" height="32" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 257"><defs><linearGradient id="IconifyId1813088fe1fbc01fb466" x1="-.828%" x2="57.636%" y1="7.652%" y2="78.411%"><stop offset="0%" stop-color="#41D1FF"></stop><stop offset="100%" stop-color="#BD34FE"></stop></linearGradient><linearGradient id="IconifyId1813088fe1fbc01fb467" x1="43.376%" x2="50.316%" y1="2.242%" y2="89.03%"><stop offset="0%" stop-color="#FFEA83"></stop><stop offset="8.333%" stop-color="#FFDD35"></stop><stop offset="100%" stop-color="#FFA800"></stop></linearGradient></defs><path fill="url(#IconifyId1813088fe1fbc01fb466)" d="M255.153 37.938L134.897 252.976c-2.483 4.44-8.862 4.466-11.382.048L.875 37.958c-2.746-4.814 1.371-10.646 6.827-9.67l120.385 21.517a6.537 6.537 0 0 0 2.322-.004l117.867-21.483c5.438-.991 9.574 4.796 6.877 9.62Z"></path><path fill="url(#IconifyId1813088fe1fbc01fb467)" d="M185.432.063L96.44 17.501a3.268 3.268 0 0 0-2.634 3.014l-5.474 92.456a3.268 3.268 0 0 0 3.997 3.378l24.777-5.718c2.318-.535 4.413 1.507 3.936 3.838l-7.361 36.047c-.495 2.426 1.782 4.5 4.151 3.78l15.304-4.649c2.372-.72 4.652 1.36 4.15 3.788l-11.698 56.621c-.732 3.542 3.979 5.473 5.943 2.437l1.313-2.028l72.516-144.72c1.215-2.423-.88-5.186-3.54-4.672l-25.505 4.922c-2.396.462-4.435-1.77-3.759-4.114l16.646-57.705c.677-2.35-1.37-4.583-3.769-4.113Z"></path></svg>
|
||||||
|
After Width: | Height: | Size: 1.5 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 36 KiB |
|
|
@ -0,0 +1,122 @@
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,80 @@
|
||||||
|
import Spreadsheet, { CSS_PREFIX } from "../main";
|
||||||
|
import { Position } from "../modules/cell";
|
||||||
|
import { EventTypes } from "../modules/events";
|
||||||
|
import { RenderBox } from "../modules/renderBox";
|
||||||
|
|
||||||
|
export class Editor {
|
||||||
|
element: HTMLInputElement;
|
||||||
|
root: Spreadsheet;
|
||||||
|
constructor(root: Spreadsheet) {
|
||||||
|
this.root = root;
|
||||||
|
const element = document.createElement("input");
|
||||||
|
element.classList.add(CSS_PREFIX + "editor");
|
||||||
|
this.element = element;
|
||||||
|
this.hide();
|
||||||
|
}
|
||||||
|
|
||||||
|
hide() {
|
||||||
|
this.element.style.display = "none";
|
||||||
|
this.element.classList.add("hide");
|
||||||
|
this.element.blur();
|
||||||
|
window.removeEventListener("click", this.handleClickOutside);
|
||||||
|
this.element.removeEventListener("keydown", this.handleKeydown);
|
||||||
|
|
||||||
|
this.root.focusTable();
|
||||||
|
}
|
||||||
|
|
||||||
|
show(position: Position, initialString?: string) {
|
||||||
|
const { height, width, x, y } = new RenderBox(this.root.config, position);
|
||||||
|
const cell = this.root.getCell(position);
|
||||||
|
this.element.classList.remove("hide");
|
||||||
|
|
||||||
|
this.element.style.top =
|
||||||
|
y - this.root.viewport.top + this.root.columnsBarHeight + "px";
|
||||||
|
this.element.style.left =
|
||||||
|
x - this.root.viewport.left + this.root.rowsBarWidth + "px";
|
||||||
|
this.element.style.width = width + "px";
|
||||||
|
this.element.style.height = height + "px";
|
||||||
|
this.element.style.display = "block";
|
||||||
|
|
||||||
|
window.addEventListener("click", this.handleClickOutside);
|
||||||
|
this.element.addEventListener("keydown", this.handleKeydown);
|
||||||
|
this.element.value = initialString ? initialString : cell.value;
|
||||||
|
this.element.focus();
|
||||||
|
if (!initialString) this.element.select();
|
||||||
|
}
|
||||||
|
|
||||||
|
handleKeydown = (event: KeyboardEvent) => {
|
||||||
|
const { key } = event;
|
||||||
|
|
||||||
|
switch (key) {
|
||||||
|
case "Escape": {
|
||||||
|
this.hide();
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case "Enter": {
|
||||||
|
if (!this.root.selection.selectedCell) return;
|
||||||
|
|
||||||
|
this.root.changeCellValues(this.root.selection.selectedCell, {
|
||||||
|
value: this.element.value,
|
||||||
|
displayValue: this.element.value,
|
||||||
|
});
|
||||||
|
|
||||||
|
this.root.events.dispatch({
|
||||||
|
type: EventTypes.CELL_CHANGE,
|
||||||
|
cell: this.root.getCell(this.root.selection.selectedCell),
|
||||||
|
});
|
||||||
|
|
||||||
|
this.hide();
|
||||||
|
this.root.renderSelection();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
handleClickOutside = (event: MouseEvent) => {
|
||||||
|
const target = event.target as HTMLElement;
|
||||||
|
if (!this.element.contains(target)) {
|
||||||
|
this.hide();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,109 @@
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,320 @@
|
||||||
|
import Spreadsheet, { CSS_PREFIX, Cell, Selection } from "../main";
|
||||||
|
import { EventTypes } from "../modules/events";
|
||||||
|
import { checkEqualCellSelections } from "../utils/position";
|
||||||
|
|
||||||
|
export interface ViewportRect {
|
||||||
|
top: number;
|
||||||
|
left: number;
|
||||||
|
right: number;
|
||||||
|
bottom: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export class Scroller {
|
||||||
|
element: HTMLDivElement;
|
||||||
|
private verticalScroller: HTMLDivElement;
|
||||||
|
private horizontalScroller: HTMLDivElement;
|
||||||
|
private root: Spreadsheet;
|
||||||
|
private isSelecting = false;
|
||||||
|
|
||||||
|
constructor(root: Spreadsheet) {
|
||||||
|
this.root = root;
|
||||||
|
const { horizontalScroller, scroller, verticalScroller } =
|
||||||
|
this.buildComponent();
|
||||||
|
this.element = scroller;
|
||||||
|
this.verticalScroller = verticalScroller;
|
||||||
|
this.horizontalScroller = horizontalScroller;
|
||||||
|
|
||||||
|
this.element.style.height = this.root.config.view.height + "px";
|
||||||
|
this.element.style.width = this.root.config.view.width + "px";
|
||||||
|
this.element.style.top = this.root.columnsBarHeight + "px";
|
||||||
|
this.element.style.left = this.root.rowsBarWidth + "px";
|
||||||
|
this.element.tabIndex = -1;
|
||||||
|
|
||||||
|
this.updateScrollerSize(); //* Init size set
|
||||||
|
|
||||||
|
this.element.addEventListener("scroll", this.handleScroll);
|
||||||
|
|
||||||
|
this.element.addEventListener("mousedown", this.handleClick);
|
||||||
|
this.element.addEventListener("mousemove", this.handleMouseMove);
|
||||||
|
this.element.addEventListener("mouseup", this.handleMouseUp);
|
||||||
|
this.element.addEventListener("dblclick", this.handleDoubleClick);
|
||||||
|
|
||||||
|
this.element.addEventListener("keydown", this.handleKeydown);
|
||||||
|
this.element.addEventListener("paste", (event) => {
|
||||||
|
if (!this.root.selection.selectedCell) return;
|
||||||
|
this.root.clipboard.paste(
|
||||||
|
this.root,
|
||||||
|
this.root.selection.selectedCell,
|
||||||
|
event,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
public setSelectingMode(mode: boolean) {
|
||||||
|
this.isSelecting = mode;
|
||||||
|
}
|
||||||
|
|
||||||
|
private handleMouseMove = (event: MouseEvent) => {
|
||||||
|
if (!this.isSelecting) return;
|
||||||
|
const { offsetX, offsetY } = event;
|
||||||
|
const lastSelectedCell = this.root.getCellByCoords(offsetX, offsetY);
|
||||||
|
|
||||||
|
let isRangeChanged = false;
|
||||||
|
|
||||||
|
if (this.root.selection.selectedRange) {
|
||||||
|
isRangeChanged = !checkEqualCellSelections(
|
||||||
|
this.root.selection.selectedRange.to,
|
||||||
|
lastSelectedCell,
|
||||||
|
);
|
||||||
|
|
||||||
|
if (isRangeChanged) {
|
||||||
|
this.root.selection.selectedRange.to = lastSelectedCell;
|
||||||
|
this.root.events.dispatch({
|
||||||
|
type: EventTypes.SELECTION_CHANGE,
|
||||||
|
selection: this.root.selection,
|
||||||
|
enableCallback: true,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
private handleMouseUp = () => {
|
||||||
|
this.isSelecting = false;
|
||||||
|
const newSelection = { ...this.root.selection };
|
||||||
|
|
||||||
|
if (this.root.selection.selectedRange) {
|
||||||
|
if (
|
||||||
|
checkEqualCellSelections(
|
||||||
|
this.root.selection.selectedRange.from,
|
||||||
|
this.root.selection.selectedRange.to,
|
||||||
|
)
|
||||||
|
) {
|
||||||
|
newSelection.selectedRange = null;
|
||||||
|
this.root.events.dispatch({
|
||||||
|
type: EventTypes.SELECTION_CHANGE,
|
||||||
|
selection: newSelection,
|
||||||
|
enableCallback: false,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
this.root.renderSheet();
|
||||||
|
this.root.renderColumnsBar();
|
||||||
|
this.root.renderRowsBar();
|
||||||
|
};
|
||||||
|
|
||||||
|
private handleDoubleClick = (event: MouseEvent) => {
|
||||||
|
event.preventDefault();
|
||||||
|
const position = this.root.getCellByCoords(event.offsetX, event.offsetY);
|
||||||
|
this.root.showEditor(position);
|
||||||
|
};
|
||||||
|
|
||||||
|
private handleKeydown = (event: KeyboardEvent) => {
|
||||||
|
//* Navigation
|
||||||
|
|
||||||
|
if (
|
||||||
|
["ArrowLeft", "ArrowRight", "ArrowUp", "ArrowDown"].includes(event.key)
|
||||||
|
) {
|
||||||
|
event.preventDefault();
|
||||||
|
this.root.selection.selectedRange = null;
|
||||||
|
switch (event.key) {
|
||||||
|
case "ArrowLeft": {
|
||||||
|
if (
|
||||||
|
this.root.selection.selectedCell &&
|
||||||
|
this.root.selection.selectedCell.column > 0
|
||||||
|
) {
|
||||||
|
this.root.selection.selectedCell.column -= 1;
|
||||||
|
// this.root.renderSheet();
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case "ArrowRight": {
|
||||||
|
if (
|
||||||
|
this.root.selection.selectedCell &&
|
||||||
|
this.root.selection.selectedCell.column <
|
||||||
|
this.root.config.columns.length - 1
|
||||||
|
) {
|
||||||
|
this.root.selection.selectedCell.column += 1;
|
||||||
|
// this.root.renderSheet();
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case "ArrowUp": {
|
||||||
|
if (
|
||||||
|
this.root.selection.selectedCell &&
|
||||||
|
this.root.selection.selectedCell.row > 0
|
||||||
|
) {
|
||||||
|
this.root.selection.selectedCell.row -= 1;
|
||||||
|
// this.root.renderSheet();
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case "ArrowDown": {
|
||||||
|
if (
|
||||||
|
this.root.selection.selectedCell &&
|
||||||
|
this.root.selection.selectedCell.row <
|
||||||
|
this.root.config.rows.length - 1
|
||||||
|
) {
|
||||||
|
this.root.selection.selectedCell.row += 1;
|
||||||
|
// this.root.renderSheet();
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
this.root.events.dispatch({
|
||||||
|
type: EventTypes.SELECTION_CHANGE,
|
||||||
|
selection: this.root.selection,
|
||||||
|
enableCallback: true,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
//* Start typings
|
||||||
|
const keysRegex = /^([a-z]|[а-я]|[0-9]|=)$/;
|
||||||
|
if (!event.metaKey && !event.ctrlKey) {
|
||||||
|
//* Prevent handle shortcutrs
|
||||||
|
const isPressedLetterKey = keysRegex.test(event.key.toLowerCase());
|
||||||
|
|
||||||
|
if (event.key === "F2" || isPressedLetterKey) {
|
||||||
|
//* English and Russian keyboard. Or F2 button
|
||||||
|
event.preventDefault();
|
||||||
|
if (!this.root.selection.selectedCell) return;
|
||||||
|
|
||||||
|
this.root.showEditor(
|
||||||
|
this.root.selection.selectedCell,
|
||||||
|
isPressedLetterKey ? event.key : undefined,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (event.key === "Delete") {
|
||||||
|
event.preventDefault();
|
||||||
|
this.root.deleteSelectedCellsValues();
|
||||||
|
this.root.renderSheet();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (event.metaKey || event.ctrlKey) {
|
||||||
|
if (event.code === "KeyC") {
|
||||||
|
let cells: Cell[][] = undefined!;
|
||||||
|
const selection = new Selection();
|
||||||
|
|
||||||
|
if (this.root.selection.selectedRange) {
|
||||||
|
const { from, to } = this.root.selection.selectedRange;
|
||||||
|
|
||||||
|
selection.selectedRange = this.root.selection.selectedRange;
|
||||||
|
|
||||||
|
const subArrByRows = this.root.data.slice(from.row, to.row + 1);
|
||||||
|
|
||||||
|
const subArrByCols = subArrByRows.map((row) => {
|
||||||
|
return row.slice(from.column, to.column + 1);
|
||||||
|
});
|
||||||
|
|
||||||
|
cells = [...subArrByCols];
|
||||||
|
} else if (this.root.selection.selectedCell) {
|
||||||
|
const { column, row } = this.root.selection.selectedCell;
|
||||||
|
cells = [[this.root.data[row][column]]];
|
||||||
|
selection.selectedRange = {
|
||||||
|
from: this.root.selection.selectedCell,
|
||||||
|
to: this.root.selection.selectedCell,
|
||||||
|
};
|
||||||
|
} else {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.root.clipboard.copy(cells, selection.selectedRange);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (event.code === "KeyV") {
|
||||||
|
// if (!this.root.selection.selectedCell) return;
|
||||||
|
// this.root.clipboard.paste(this.root, this.root.selection.selectedCell);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
private handleClick = (event: MouseEvent) => {
|
||||||
|
this.root.events.dispatch({
|
||||||
|
type: EventTypes.CELL_CLICK,
|
||||||
|
event,
|
||||||
|
scroller: this,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
private handleScroll = () => {
|
||||||
|
const rect = this.getViewportBoundlingRect();
|
||||||
|
this.root.viewport.updateValues(rect);
|
||||||
|
|
||||||
|
this.root.renderSheet();
|
||||||
|
this.root.renderColumnsBar();
|
||||||
|
|
||||||
|
this.root.renderRowsBar();
|
||||||
|
};
|
||||||
|
|
||||||
|
public getViewportBoundlingRect(): ViewportRect {
|
||||||
|
const { scrollTop, scrollLeft } = this.element;
|
||||||
|
const { height, width } = this.element.getBoundingClientRect();
|
||||||
|
const bottom = scrollTop + height;
|
||||||
|
const right = scrollLeft + width;
|
||||||
|
|
||||||
|
return {
|
||||||
|
top: scrollTop,
|
||||||
|
left: scrollLeft,
|
||||||
|
bottom,
|
||||||
|
right,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
private buildComponent() {
|
||||||
|
const scroller = document.createElement("div");
|
||||||
|
const verticalScroller = document.createElement("div");
|
||||||
|
const horizontalScroller = document.createElement("div");
|
||||||
|
const groupScrollers = document.createElement("div");
|
||||||
|
const stack = document.createElement("div");
|
||||||
|
|
||||||
|
verticalScroller.style.width = "0px";
|
||||||
|
verticalScroller.style.pointerEvents = "none";
|
||||||
|
|
||||||
|
horizontalScroller.style.pointerEvents = "none";
|
||||||
|
|
||||||
|
groupScrollers.style.display = "flex";
|
||||||
|
|
||||||
|
stack.appendChild(verticalScroller);
|
||||||
|
stack.appendChild(horizontalScroller);
|
||||||
|
groupScrollers.appendChild(stack);
|
||||||
|
this.verticalScroller = verticalScroller;
|
||||||
|
this.horizontalScroller = horizontalScroller;
|
||||||
|
scroller.appendChild(groupScrollers);
|
||||||
|
scroller.contentEditable = "false";
|
||||||
|
scroller.classList.add(CSS_PREFIX + "scroller");
|
||||||
|
|
||||||
|
return { scroller, verticalScroller, horizontalScroller };
|
||||||
|
}
|
||||||
|
|
||||||
|
private getActualHeight() {
|
||||||
|
return this.root.config.rows.reduce((acc, curr) => {
|
||||||
|
acc += curr.height;
|
||||||
|
return acc;
|
||||||
|
}, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
private getActualWidth() {
|
||||||
|
return this.root.config.columns.reduce((acc, curr) => {
|
||||||
|
acc += curr.width;
|
||||||
|
return acc;
|
||||||
|
}, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
updateScrollerSize() {
|
||||||
|
const totalHeight = this.getActualHeight();
|
||||||
|
const totalWidth = this.getActualWidth();
|
||||||
|
|
||||||
|
this.setScrollerHeight(totalHeight);
|
||||||
|
this.setScrollerWidth(totalWidth);
|
||||||
|
}
|
||||||
|
|
||||||
|
private setScrollerHeight(height: number) {
|
||||||
|
this.verticalScroller.style.height = height + "px";
|
||||||
|
}
|
||||||
|
|
||||||
|
private setScrollerWidth(width: number) {
|
||||||
|
this.horizontalScroller.style.width = width + "px";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,138 @@
|
||||||
|
import Spreadsheet, { CSS_PREFIX, RenderBox } from "../main";
|
||||||
|
import { Position } from "../modules/cell";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Display (CANVAS) element where cells render
|
||||||
|
*/
|
||||||
|
export class Sheet {
|
||||||
|
element: HTMLCanvasElement;
|
||||||
|
ctx: CanvasRenderingContext2D;
|
||||||
|
root: Spreadsheet;
|
||||||
|
constructor(root: Spreadsheet) {
|
||||||
|
this.root = root;
|
||||||
|
const canvas = document.createElement("canvas");
|
||||||
|
canvas.classList.add(CSS_PREFIX + "sheet");
|
||||||
|
|
||||||
|
//* Set up canvas sizes based on provided root config
|
||||||
|
canvas.height = this.root.config.view.height;
|
||||||
|
canvas.width = this.root.config.view.width;
|
||||||
|
canvas.style.width = this.root.config.view.width + "px";
|
||||||
|
canvas.style.height = this.root.config.view.height + "px";
|
||||||
|
canvas.style.left = "0px";
|
||||||
|
|
||||||
|
this.element = canvas;
|
||||||
|
|
||||||
|
const ctx = this.element.getContext("2d");
|
||||||
|
if (!ctx) throw new Error("Enable hardware acceleration");
|
||||||
|
this.ctx = ctx;
|
||||||
|
}
|
||||||
|
|
||||||
|
getCellByCoords(x: number, y: number): Position {
|
||||||
|
let row = 0;
|
||||||
|
let height = 0;
|
||||||
|
while (height <= y) {
|
||||||
|
height += this.root.config.rows[row].height;
|
||||||
|
if (height >= y) break;
|
||||||
|
row++;
|
||||||
|
}
|
||||||
|
|
||||||
|
let col = 0;
|
||||||
|
let width = 0;
|
||||||
|
while (width <= x) {
|
||||||
|
width += this.root.config.columns[col].width;
|
||||||
|
if (width >= x) break;
|
||||||
|
col++;
|
||||||
|
}
|
||||||
|
|
||||||
|
return new Position(row, col);
|
||||||
|
}
|
||||||
|
|
||||||
|
renderCell(position: Position) {
|
||||||
|
const { column, row } = position;
|
||||||
|
this.root.data[row][column].render(this.root);
|
||||||
|
}
|
||||||
|
|
||||||
|
private getSelectionRange() {
|
||||||
|
const { selectedCell, selectedRange } = this.root.selection;
|
||||||
|
|
||||||
|
if (!selectedCell && !selectedRange) return;
|
||||||
|
if (selectedRange) {
|
||||||
|
const startRow = Math.min(selectedRange.from.row, selectedRange.to.row);
|
||||||
|
const startCol = Math.min(
|
||||||
|
selectedRange.from.column,
|
||||||
|
selectedRange.to.column,
|
||||||
|
);
|
||||||
|
const lastRow = Math.max(selectedRange.from.row, selectedRange.to.row);
|
||||||
|
const lastCol = Math.max(
|
||||||
|
selectedRange.from.column,
|
||||||
|
selectedRange.to.column,
|
||||||
|
);
|
||||||
|
|
||||||
|
const startCellBox = new RenderBox(this.root.config, {
|
||||||
|
row: startRow,
|
||||||
|
column: startCol,
|
||||||
|
});
|
||||||
|
|
||||||
|
let width = 0;
|
||||||
|
for (let col = startCol; col <= lastCol; col++) {
|
||||||
|
width += this.root.config.columns[col].width;
|
||||||
|
}
|
||||||
|
|
||||||
|
let height = 0;
|
||||||
|
for (let row = startRow; row <= lastRow; row++) {
|
||||||
|
height += this.root.config.rows[row].height;
|
||||||
|
}
|
||||||
|
|
||||||
|
const x = startCellBox.x - this.root.viewport.left;
|
||||||
|
const y = startCellBox.y - this.root.viewport.top;
|
||||||
|
|
||||||
|
return { x, y, height, width };
|
||||||
|
}
|
||||||
|
if (!selectedRange && selectedCell) {
|
||||||
|
const box = new RenderBox(this.root.config, selectedCell);
|
||||||
|
box.x -= this.root.viewport.left;
|
||||||
|
box.y -= this.root.viewport.top;
|
||||||
|
return box;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private renderSelectionRange(
|
||||||
|
x: number,
|
||||||
|
y: number,
|
||||||
|
width: number,
|
||||||
|
height: number,
|
||||||
|
) {
|
||||||
|
this.ctx.save();
|
||||||
|
this.ctx.strokeStyle = "#7da8ff";
|
||||||
|
this.ctx.lineWidth = 3;
|
||||||
|
this.ctx.strokeRect(x, y, width, height);
|
||||||
|
this.ctx.fillStyle = "#7da8ff35";
|
||||||
|
this.ctx.fillRect(x, y, width, height);
|
||||||
|
this.ctx.restore();
|
||||||
|
}
|
||||||
|
|
||||||
|
renderSelection() {
|
||||||
|
const box = this.getSelectionRange();
|
||||||
|
if (!box) return;
|
||||||
|
const { height, width, x, y } = box;
|
||||||
|
this.renderSelectionRange(x, y, width, height);
|
||||||
|
}
|
||||||
|
|
||||||
|
renderSheet() {
|
||||||
|
const firstRowIdx = this.root.viewport.firstRow;
|
||||||
|
const lastColIdx = this.root.viewport.lastCol + 3;
|
||||||
|
const lastRowIdx = this.root.viewport.lastRow + 3;
|
||||||
|
const firstColIdx = this.root.viewport.firstCol;
|
||||||
|
|
||||||
|
|
||||||
|
for (let row = firstRowIdx; row <= lastRowIdx; row++) {
|
||||||
|
for (let col = firstColIdx; col <= lastColIdx; col++) {
|
||||||
|
if (!this.root.config.columns[col] || !this.root.config.rows[row])
|
||||||
|
break; //* Prevent read undefined
|
||||||
|
|
||||||
|
this.renderCell({ column: col, row });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
this.renderSelection();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,22 @@
|
||||||
|
import Spreadsheet, { CSS_PREFIX } from "../main";
|
||||||
|
import { ViewProperties } from "../modules/config";
|
||||||
|
|
||||||
|
/** Base (root) component */
|
||||||
|
export class Table {
|
||||||
|
element: HTMLDivElement;
|
||||||
|
root: Spreadsheet;
|
||||||
|
constructor(root: Spreadsheet) {
|
||||||
|
this.root = root;
|
||||||
|
const container = document.createElement("div");
|
||||||
|
container.classList.add(CSS_PREFIX + "spreadsheet_container");
|
||||||
|
this.element = container;
|
||||||
|
|
||||||
|
this.changeElementSizes(this.root.viewProps);
|
||||||
|
}
|
||||||
|
|
||||||
|
changeElementSizes(sizes: ViewProperties) {
|
||||||
|
const { height, width } = sizes;
|
||||||
|
this.element.style.width = width + this.root.rowsBarWidth + "px";
|
||||||
|
this.element.style.height = height + this.root.columnsBarHeight + "px";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,13 @@
|
||||||
|
import Spreadsheet, { CSS_PREFIX } from "../main";
|
||||||
|
|
||||||
|
export class Toolbar {
|
||||||
|
element: HTMLDivElement;
|
||||||
|
root: Spreadsheet;
|
||||||
|
height: number = 0;
|
||||||
|
constructor(root: Spreadsheet) {
|
||||||
|
this.root = root;
|
||||||
|
const toolbarElement = document.createElement("div");
|
||||||
|
toolbarElement.classList.add(CSS_PREFIX + "toolbar");
|
||||||
|
this.element = toolbarElement;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,38 @@
|
||||||
|
import Spreadsheet, { SpreadsheetConstructorProperties } from "./main";
|
||||||
|
|
||||||
|
const options: SpreadsheetConstructorProperties = {
|
||||||
|
onCellClick: (event, cell) => {
|
||||||
|
console.log("Cell click", event, cell);
|
||||||
|
},
|
||||||
|
onSelectionChange: (selection) => {
|
||||||
|
console.log("Changed selection: ", selection);
|
||||||
|
},
|
||||||
|
onCellChange(cell) {
|
||||||
|
console.log("Cell changed: ", cell);
|
||||||
|
},
|
||||||
|
onCopy: (range, data, dataAsString) => {
|
||||||
|
console.log("Copy event: ", range, data, dataAsString)
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const sheet = new Spreadsheet("#spreadsheet", options);
|
||||||
|
|
||||||
|
function saveDataToLS() {
|
||||||
|
const serializableData = sheet.serializeData();
|
||||||
|
localStorage.setItem("sheet", JSON.stringify(serializableData));
|
||||||
|
}
|
||||||
|
|
||||||
|
function loadDataFromLS() {
|
||||||
|
const data = localStorage.getItem("sheet");
|
||||||
|
if (!data) return;
|
||||||
|
const json = JSON.parse(data);
|
||||||
|
sheet.loadData(json);
|
||||||
|
}
|
||||||
|
|
||||||
|
const saveButton = document.querySelector("#save_button");
|
||||||
|
const loadButton = document.querySelector("#load_button");
|
||||||
|
|
||||||
|
if (!saveButton || !loadButton) throw new Error("LOST");
|
||||||
|
|
||||||
|
saveButton.addEventListener("click", saveDataToLS);
|
||||||
|
loadButton.addEventListener("click", loadDataFromLS);
|
||||||
|
|
@ -0,0 +1,443 @@
|
||||||
|
import { Editor } from "./components/editor";
|
||||||
|
import { Scroller } from "./components/scroller";
|
||||||
|
import { Sheet } from "./components/sheet";
|
||||||
|
import { Table } from "./components/table";
|
||||||
|
import { Toolbar } from "./components/toolbar";
|
||||||
|
import {
|
||||||
|
Cell,
|
||||||
|
CellConstructorProps,
|
||||||
|
CellStyles,
|
||||||
|
Position,
|
||||||
|
SerializableCell,
|
||||||
|
} from "./modules/cell";
|
||||||
|
import {
|
||||||
|
CellChangeEvent,
|
||||||
|
CellClickEvent,
|
||||||
|
Config,
|
||||||
|
CopyEvent,
|
||||||
|
SelectionChangeEvent,
|
||||||
|
ViewProperties,
|
||||||
|
} from "./modules/config";
|
||||||
|
import { RangeSelectionType, Selection } from "./modules/selection";
|
||||||
|
import { Styles } from "./modules/styles";
|
||||||
|
import { Viewport } from "./modules/viewport";
|
||||||
|
import "./scss/main.scss";
|
||||||
|
import { createSampleData } from "./utils/createData";
|
||||||
|
import { Cache, CachedColumn, CachedRow } from "./modules/cache";
|
||||||
|
import { Row } from "./modules/row";
|
||||||
|
import { Column } from "./modules/column";
|
||||||
|
import { ColumnsBar } from "./components/columnsBar";
|
||||||
|
import { RowsBar } from "./components/rowsBar";
|
||||||
|
import { EventTypes, Events } from "./modules/events";
|
||||||
|
import { Clipboard } from "./modules/clipboard";
|
||||||
|
import { FormulaParser } from "./modules/formulaParser";
|
||||||
|
|
||||||
|
/*
|
||||||
|
! Component structure
|
||||||
|
<Table>
|
||||||
|
<Toolbar />
|
||||||
|
<Content> //* Abstract
|
||||||
|
<Header />
|
||||||
|
<Sheet />
|
||||||
|
</Content>
|
||||||
|
<Scroller />
|
||||||
|
</Table>
|
||||||
|
*/
|
||||||
|
|
||||||
|
export interface SpreadsheetConstructorProperties {
|
||||||
|
view?: ViewProperties;
|
||||||
|
onCellClick?: CellClickEvent | null;
|
||||||
|
onSelectionChange?: SelectionChangeEvent | null;
|
||||||
|
onCellChange?: CellChangeEvent | null;
|
||||||
|
onCopy?: CopyEvent | null
|
||||||
|
}
|
||||||
|
|
||||||
|
export const CSS_PREFIX = "modern_sc_";
|
||||||
|
|
||||||
|
export default class Spreadsheet {
|
||||||
|
private table: Table;
|
||||||
|
private scroller: Scroller;
|
||||||
|
private toolbar: Toolbar;
|
||||||
|
private rowsBar: RowsBar;
|
||||||
|
private columnsBar: ColumnsBar;
|
||||||
|
private sheet: Sheet;
|
||||||
|
private editor: Editor;
|
||||||
|
public styles: Styles;
|
||||||
|
public config: Config;
|
||||||
|
public data: Cell[][];
|
||||||
|
public viewport: Viewport;
|
||||||
|
public selection: Selection;
|
||||||
|
public cache: Cache;
|
||||||
|
public events: Events;
|
||||||
|
public clipboard: Clipboard;
|
||||||
|
public formulaParser: FormulaParser
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
target: string | HTMLElement,
|
||||||
|
props?: SpreadsheetConstructorProperties,
|
||||||
|
) {
|
||||||
|
const data = createSampleData(40, 40);
|
||||||
|
const config = this.makeConfigFromData(
|
||||||
|
data,
|
||||||
|
props?.view ?? { height: 600, width: 800 },
|
||||||
|
);
|
||||||
|
if (props?.view) {
|
||||||
|
config.view = props.view;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.config = new Config(config);
|
||||||
|
|
||||||
|
this.config.onCellClick = props?.onCellClick ?? null;
|
||||||
|
this.config.onSelectonChange = props?.onSelectionChange ?? null;
|
||||||
|
this.config.onCellChange = props?.onCellChange ?? null;
|
||||||
|
this.config.onCopy = props?.onCopy ?? null
|
||||||
|
|
||||||
|
this.rowsBar = new RowsBar(this);
|
||||||
|
this.columnsBar = new ColumnsBar(this);
|
||||||
|
this.sheet = new Sheet(this);
|
||||||
|
this.table = new Table(this);
|
||||||
|
this.scroller = new Scroller(this);
|
||||||
|
this.toolbar = new Toolbar(this);
|
||||||
|
this.editor = new Editor(this);
|
||||||
|
this.cache = this.getInitialCache();
|
||||||
|
this.viewport = new Viewport(
|
||||||
|
this,
|
||||||
|
this.scroller.getViewportBoundlingRect(),
|
||||||
|
);
|
||||||
|
this.selection = new Selection();
|
||||||
|
this.events = new Events(this);
|
||||||
|
this.clipboard = new Clipboard(this);
|
||||||
|
this.formulaParser = new FormulaParser(this)
|
||||||
|
|
||||||
|
this.data = data;
|
||||||
|
this.styles = new Styles();
|
||||||
|
this.buildComponent();
|
||||||
|
this.setElementsPositions();
|
||||||
|
this.appendTableToTarget(target);
|
||||||
|
this.renderSheet();
|
||||||
|
this.renderColumnsBar();
|
||||||
|
this.renderRowsBar();
|
||||||
|
}
|
||||||
|
|
||||||
|
private setRowsBarPosition() {
|
||||||
|
const top = this.columnsBar.height + this.toolbar.height;
|
||||||
|
const left = 0;
|
||||||
|
this.rowsBar.setElementPosition(top, left);
|
||||||
|
}
|
||||||
|
private setColumnsBarPosition() {
|
||||||
|
const top = this.toolbar.height;
|
||||||
|
const left = this.rowsBar.width;
|
||||||
|
this.columnsBar.setElementPosition(top, left);
|
||||||
|
}
|
||||||
|
|
||||||
|
private setElementsPositions() {
|
||||||
|
this.setRowsBarPosition();
|
||||||
|
this.setColumnsBarPosition();
|
||||||
|
}
|
||||||
|
|
||||||
|
private getInitialCache(): Cache {
|
||||||
|
const cachedCols: CachedColumn[] = [];
|
||||||
|
let currentWidth = 0;
|
||||||
|
for (let i = 0; i <= this.config.columns.length - 1; i++) {
|
||||||
|
const col = this.config.columns[i];
|
||||||
|
currentWidth += col.width;
|
||||||
|
const cacheCol = new CachedColumn({
|
||||||
|
xPos: currentWidth,
|
||||||
|
colIdx: i,
|
||||||
|
});
|
||||||
|
cachedCols.push(cacheCol);
|
||||||
|
}
|
||||||
|
|
||||||
|
const cachedRows: CachedRow[] = [];
|
||||||
|
let currentHeight = 0;
|
||||||
|
for (let i = 0; i <= this.config.rows.length - 1; i++) {
|
||||||
|
const row = this.config.rows[i];
|
||||||
|
currentHeight += row.height;
|
||||||
|
const cacheRow = new CachedRow({
|
||||||
|
yPos: currentHeight,
|
||||||
|
rowIdx: i,
|
||||||
|
});
|
||||||
|
cachedRows.push(cacheRow);
|
||||||
|
}
|
||||||
|
|
||||||
|
const cache = new Cache({
|
||||||
|
columns: cachedCols,
|
||||||
|
rows: cachedRows,
|
||||||
|
});
|
||||||
|
|
||||||
|
return cache;
|
||||||
|
}
|
||||||
|
|
||||||
|
private buildComponent(): void {
|
||||||
|
const content = document.createElement("div"); //* Abstract
|
||||||
|
content.style.top = this.columnsBarHeight + "px";
|
||||||
|
content.style.left = this.rowsBarWidth + "px";
|
||||||
|
|
||||||
|
content.appendChild(this.sheet.element);
|
||||||
|
|
||||||
|
content.classList.add(CSS_PREFIX + "content");
|
||||||
|
|
||||||
|
this.table.element.appendChild(this.toolbar.element);
|
||||||
|
this.table.element.appendChild(this.rowsBar.element);
|
||||||
|
this.table.element.appendChild(this.columnsBar.element);
|
||||||
|
this.table.element.appendChild(content);
|
||||||
|
this.table.element.appendChild(this.scroller.element);
|
||||||
|
this.table.element.append(this.editor.element);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**Destroy spreadsheet DOM element.
|
||||||
|
*
|
||||||
|
* May be usefull when need to rerender component.
|
||||||
|
*/
|
||||||
|
public destroy() {
|
||||||
|
this.table.element.remove();
|
||||||
|
}
|
||||||
|
|
||||||
|
private appendTableToTarget(target: string | HTMLElement) {
|
||||||
|
if (typeof target === "string") {
|
||||||
|
const element = document.querySelector(target);
|
||||||
|
if (!element)
|
||||||
|
throw new Error(
|
||||||
|
`Element with selector ${target} is not finded in DOM.\n Make sure it exists.`,
|
||||||
|
);
|
||||||
|
element?.appendChild(this.table.element);
|
||||||
|
}
|
||||||
|
if (target instanceof HTMLElement) {
|
||||||
|
target.append(this.table.element);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Canvas rendering context 2D.
|
||||||
|
*
|
||||||
|
* Abble to draw on canvas with default CanvasAPI methods
|
||||||
|
*/
|
||||||
|
get ctx() {
|
||||||
|
return this.sheet.ctx;
|
||||||
|
}
|
||||||
|
|
||||||
|
get viewProps() {
|
||||||
|
return this.config.view;
|
||||||
|
}
|
||||||
|
|
||||||
|
get columnsBarHeight() {
|
||||||
|
return this.columnsBar.height;
|
||||||
|
}
|
||||||
|
|
||||||
|
get rowsBarWidth() {
|
||||||
|
return this.rowsBar.width;
|
||||||
|
}
|
||||||
|
|
||||||
|
get toolbarHeight() {
|
||||||
|
return this.toolbar.height;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Focusing on interactive part of spreadsheet */
|
||||||
|
focusTable() {
|
||||||
|
this.scroller.element.focus();
|
||||||
|
}
|
||||||
|
|
||||||
|
getCellByCoords(x: number, y: number) {
|
||||||
|
return this.sheet.getCellByCoords(x, y);
|
||||||
|
}
|
||||||
|
|
||||||
|
getCell(position: Position): Cell {
|
||||||
|
const { column, row } = position;
|
||||||
|
return this.data[row][column];
|
||||||
|
}
|
||||||
|
|
||||||
|
changeCellValues(
|
||||||
|
position: Position,
|
||||||
|
values: Partial<Omit<CellConstructorProps, "position">>,
|
||||||
|
enableCallback: boolean = true
|
||||||
|
) {
|
||||||
|
const { column, row } = position;
|
||||||
|
|
||||||
|
|
||||||
|
this.data[row][column].changeValues(values);
|
||||||
|
|
||||||
|
this.events.dispatch({
|
||||||
|
type: EventTypes.CELL_CHANGE,
|
||||||
|
cell: this.data[row][column],
|
||||||
|
enableCallback: enableCallback
|
||||||
|
})
|
||||||
|
|
||||||
|
this.renderCell(row, column);
|
||||||
|
}
|
||||||
|
|
||||||
|
changeCellStyles(position: Position, styles: CellStyles) {
|
||||||
|
const { column, row } = position;
|
||||||
|
this.data[row][column].changeStyles(styles);
|
||||||
|
this.renderCell(row, column);
|
||||||
|
}
|
||||||
|
|
||||||
|
applyActionToRange(
|
||||||
|
range: RangeSelectionType,
|
||||||
|
callback: (cell: Cell) => void,
|
||||||
|
): void {
|
||||||
|
const fromRow = Math.min(range.from.row, range.to.row);
|
||||||
|
const toRow = Math.max(range.from.row, range.to.row);
|
||||||
|
|
||||||
|
const fromCol = Math.min(range.from.column, range.to.column);
|
||||||
|
const toCol = Math.max(range.from.column, range.to.column);
|
||||||
|
|
||||||
|
for (let row = fromRow; row <= toRow; row++) {
|
||||||
|
for (let col = fromCol; col <= toCol; col++) {
|
||||||
|
const cell = this.data[row][col];
|
||||||
|
callback(cell);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
deleteSelectedCellsValues() {
|
||||||
|
if (this.selection.selectedRange !== null) {
|
||||||
|
this.applyActionToRange(this.selection.selectedRange, (cell) => {
|
||||||
|
this.changeCellValues(cell.position, {
|
||||||
|
displayValue: "",
|
||||||
|
resultValue: "",
|
||||||
|
value: "",
|
||||||
|
});
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
if (!this.selection.selectedCell) return;
|
||||||
|
this.changeCellValues(this.selection.selectedCell, {
|
||||||
|
displayValue: "",
|
||||||
|
resultValue: "",
|
||||||
|
value: "",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
showEditor(position: Position, initialString?: string) {
|
||||||
|
this.editor.show(position, initialString);
|
||||||
|
}
|
||||||
|
|
||||||
|
renderSheet() {
|
||||||
|
this.sheet.renderSheet();
|
||||||
|
}
|
||||||
|
|
||||||
|
renderSelection() {
|
||||||
|
this.sheet.renderSelection();
|
||||||
|
}
|
||||||
|
|
||||||
|
renderColumnsBar() {
|
||||||
|
this.columnsBar.renderBar();
|
||||||
|
}
|
||||||
|
|
||||||
|
renderRowsBar() {
|
||||||
|
this.rowsBar.renderBar();
|
||||||
|
}
|
||||||
|
|
||||||
|
renderCell(row: number, col: number) {
|
||||||
|
this.data[row][col].render(this);
|
||||||
|
}
|
||||||
|
|
||||||
|
public loadData(data: Cell[][] | SerializableCell[][]): Spreadsheet {
|
||||||
|
const rowsLength = data.length;
|
||||||
|
const colsLength = data[0] ? data[0].length : 0;
|
||||||
|
this.data = [];
|
||||||
|
|
||||||
|
const formattedData: Cell[][] = [];
|
||||||
|
// Transform serialized objects to Cells
|
||||||
|
for (let row = 0; row < rowsLength; row++) {
|
||||||
|
const innerRow: Cell[] = [];
|
||||||
|
for (let col = 0; col < colsLength; col++) {
|
||||||
|
const cell = data[row][col];
|
||||||
|
innerRow.push(
|
||||||
|
new Cell({
|
||||||
|
displayValue: cell.displayValue,
|
||||||
|
position: cell.position,
|
||||||
|
resultValue: cell.resultValue,
|
||||||
|
value: cell.value,
|
||||||
|
style: cell.style,
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
formattedData.push(innerRow);
|
||||||
|
}
|
||||||
|
|
||||||
|
const config = this.makeConfigFromData(formattedData, this.config.view);
|
||||||
|
config.onCellChange = this.config.onCellChange
|
||||||
|
config.onCellClick = this.config.onCellClick
|
||||||
|
config.onCopy = this.config.onCopy
|
||||||
|
config.onSelectonChange = this.config.onSelectonChange
|
||||||
|
|
||||||
|
|
||||||
|
this.data = formattedData;
|
||||||
|
this.selection.selectedCell = null;
|
||||||
|
this.selection.selectedRange = null;
|
||||||
|
this.config = config
|
||||||
|
this.cache = this.getInitialCache();
|
||||||
|
this.scroller.updateScrollerSize();
|
||||||
|
this.viewport = new Viewport(
|
||||||
|
this,
|
||||||
|
this.scroller.getViewportBoundlingRect(),
|
||||||
|
);
|
||||||
|
this.renderSheet();
|
||||||
|
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
private makeConfigFromData(data: Cell[][], view: ViewProperties): Config {
|
||||||
|
const lastRowIdx = data.length - 1;
|
||||||
|
const lastColIdx = data[0] ? data[0].length : 0;
|
||||||
|
|
||||||
|
const rows: Row[] = [];
|
||||||
|
for (let row = 0; row < lastRowIdx; row++) {
|
||||||
|
rows.push(
|
||||||
|
new Row({
|
||||||
|
height: 40,
|
||||||
|
title: String(row),
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const columns: Column[] = [];
|
||||||
|
|
||||||
|
for (let col = 0; col < lastColIdx; col++) {
|
||||||
|
columns.push(
|
||||||
|
new Column({
|
||||||
|
width: 150,
|
||||||
|
title: String(col),
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const config = new Config({
|
||||||
|
view,
|
||||||
|
rows,
|
||||||
|
columns,
|
||||||
|
onCellClick: null,
|
||||||
|
});
|
||||||
|
|
||||||
|
return config;
|
||||||
|
}
|
||||||
|
|
||||||
|
public serializeData(): SerializableCell[][] {
|
||||||
|
const rowsLength = this.data.length;
|
||||||
|
const colsLength = this.data[0] ? this.data[0].length : 0;
|
||||||
|
|
||||||
|
const cellsArray: SerializableCell[][] = [];
|
||||||
|
|
||||||
|
for (let row = 0; row < rowsLength; row++) {
|
||||||
|
const innerRow: SerializableCell[] = [];
|
||||||
|
for (let col = 0; col < colsLength; col++) {
|
||||||
|
innerRow.push(this.data[row][col].getSerializableCell());
|
||||||
|
}
|
||||||
|
cellsArray.push(innerRow);
|
||||||
|
}
|
||||||
|
|
||||||
|
return cellsArray;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export * from "./modules/cache";
|
||||||
|
export * from "./modules/cell";
|
||||||
|
export * from "./modules/column";
|
||||||
|
export * from "./modules/config";
|
||||||
|
export * from "./modules/renderBox";
|
||||||
|
export * from "./modules/row";
|
||||||
|
export * from "./modules/selection";
|
||||||
|
export * from "./modules/styles";
|
||||||
|
export * from "./modules/viewport";
|
||||||
|
|
||||||
|
export * from "./utils/createData";
|
||||||
|
|
@ -0,0 +1,67 @@
|
||||||
|
export interface CachedColumnProperties {
|
||||||
|
xPos: number;
|
||||||
|
colIdx: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export class CachedColumn {
|
||||||
|
xPos: number;
|
||||||
|
colIdx: number;
|
||||||
|
|
||||||
|
constructor(props: CachedColumnProperties) {
|
||||||
|
this.xPos = props.xPos;
|
||||||
|
this.colIdx = props.colIdx;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface CachedRowProperties {
|
||||||
|
yPos: number;
|
||||||
|
rowIdx: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export class CachedRow {
|
||||||
|
yPos: number;
|
||||||
|
rowIdx: number;
|
||||||
|
|
||||||
|
constructor(props: CachedRowProperties) {
|
||||||
|
this.yPos = props.yPos;
|
||||||
|
this.rowIdx = props.rowIdx;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface CacheConstructorProps {
|
||||||
|
columns: CachedColumn[];
|
||||||
|
rows: CachedRow[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export class Cache {
|
||||||
|
public columns: CachedColumn[];
|
||||||
|
public rows: CachedRow[];
|
||||||
|
constructor(initial: CacheConstructorProps) {
|
||||||
|
this.columns = initial.columns;
|
||||||
|
this.rows = initial.rows;
|
||||||
|
}
|
||||||
|
|
||||||
|
public getRowByYCoord(y: number): number {
|
||||||
|
let rowIdx = 0;
|
||||||
|
for (let i = 0; i < this.rows.length; i++) {
|
||||||
|
rowIdx = i
|
||||||
|
if (y <= this.rows[i].yPos) { //* Intersection detect
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
return rowIdx;
|
||||||
|
}
|
||||||
|
|
||||||
|
public getColumnByXCoord(x: number): number {
|
||||||
|
let colIdx = 0;
|
||||||
|
for (let i = 0; i < this.columns.length; i++) {
|
||||||
|
colIdx = i
|
||||||
|
if (x <= this.columns[i].xPos) { //* Intersection detect
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return colIdx;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,155 @@
|
||||||
|
import Spreadsheet from "../main";
|
||||||
|
import { FormulaParser } from "./formulaParser";
|
||||||
|
import { RenderBox } from "./renderBox";
|
||||||
|
|
||||||
|
export type CellConstructorProps = {
|
||||||
|
value: string;
|
||||||
|
displayValue: string;
|
||||||
|
resultValue: string;
|
||||||
|
position: Position;
|
||||||
|
style: CellStyles | null;
|
||||||
|
};
|
||||||
|
|
||||||
|
interface CellStylesConstructorProps {
|
||||||
|
fontSize: number;
|
||||||
|
fontColor: string;
|
||||||
|
background: string;
|
||||||
|
borderColor: string;
|
||||||
|
|
||||||
|
selectedBackground: string;
|
||||||
|
selectedFontColor: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export class CellStyles {
|
||||||
|
fontSize: number = 16;
|
||||||
|
fontColor: string = "black";
|
||||||
|
background: string = "white";
|
||||||
|
borderColor: string = "black";
|
||||||
|
|
||||||
|
selectedBackground = "#4287f5";
|
||||||
|
selectedFontColor = "#ffffff";
|
||||||
|
|
||||||
|
constructor(props?: CellStylesConstructorProps) {
|
||||||
|
if (props) {
|
||||||
|
Object.assign(this, props); // Override default styles
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export class Position {
|
||||||
|
row: number;
|
||||||
|
column: number;
|
||||||
|
constructor(row: number, column: number) {
|
||||||
|
this.row = row;
|
||||||
|
this.column = column;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export class SerializableCell {
|
||||||
|
value: string;
|
||||||
|
displayValue: string;
|
||||||
|
resultValue: string;
|
||||||
|
position: Position;
|
||||||
|
style: CellStyles | null;
|
||||||
|
constructor(props: SerializableCell | SerializableCell) {
|
||||||
|
this.value = props.value;
|
||||||
|
this.displayValue = props.displayValue;
|
||||||
|
this.resultValue = props.resultValue;
|
||||||
|
this.position = props.position;
|
||||||
|
this.style = props.style;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export class Cell {
|
||||||
|
/** True value (data) */
|
||||||
|
value: string;
|
||||||
|
/** Value to render */
|
||||||
|
displayValue: string;
|
||||||
|
/** This refers to the values that were obtained by calculations, for example, after calculating the formula */
|
||||||
|
resultValue: string;
|
||||||
|
position: Position;
|
||||||
|
style: CellStyles | null = null;
|
||||||
|
|
||||||
|
cellsDependsOnThisCell: Position[] = []
|
||||||
|
dependedFromCells: Position[] = []
|
||||||
|
|
||||||
|
constructor(props: CellConstructorProps) {
|
||||||
|
this.value = props.value;
|
||||||
|
this.displayValue = props.displayValue;
|
||||||
|
this.resultValue = props.resultValue;
|
||||||
|
this.position = props.position;
|
||||||
|
this.style = props.style;
|
||||||
|
}
|
||||||
|
|
||||||
|
public getSerializableCell(): SerializableCell {
|
||||||
|
const cell: SerializableCell = new SerializableCell({
|
||||||
|
displayValue: this.displayValue,
|
||||||
|
position: this.position,
|
||||||
|
resultValue: this.resultValue,
|
||||||
|
style: this.style,
|
||||||
|
value: this.value,
|
||||||
|
});
|
||||||
|
return cell;
|
||||||
|
}
|
||||||
|
|
||||||
|
changeStyles(styles: CellStyles) {
|
||||||
|
this.style = styles;
|
||||||
|
}
|
||||||
|
|
||||||
|
changeValues(values: Partial<Omit<CellConstructorProps, "position">>) {
|
||||||
|
Object.assign(this, values);
|
||||||
|
}
|
||||||
|
|
||||||
|
evalFormula(parser: FormulaParser) {
|
||||||
|
if (this.value.substring(0, 1) !== '=') return;
|
||||||
|
|
||||||
|
this.resultValue = parser.parser.parse(this.value.slice(1), {
|
||||||
|
col: this.position.column,
|
||||||
|
row: this.position.row
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// private isCellInRange(root: Spreadsheet): boolean {
|
||||||
|
// const { column, row } = this.position;
|
||||||
|
// const { selectedRange } = root.selection;
|
||||||
|
|
||||||
|
// if (!selectedRange) return false;
|
||||||
|
|
||||||
|
// const isCellInRow =
|
||||||
|
// row >= Math.min(selectedRange.from.row, selectedRange.to.row) &&
|
||||||
|
// row <= Math.max(selectedRange.to.row, selectedRange.from.row);
|
||||||
|
// const isCellInCol =
|
||||||
|
// column >= Math.min(selectedRange.from.column, selectedRange.to.column) &&
|
||||||
|
// column <= Math.max(selectedRange.to.column, selectedRange.from.column);
|
||||||
|
|
||||||
|
// return isCellInCol && isCellInRow;
|
||||||
|
// }
|
||||||
|
|
||||||
|
render(root: Spreadsheet) {
|
||||||
|
const renderBox = new RenderBox(root.config, this.position);
|
||||||
|
let { x, y } = renderBox;
|
||||||
|
const { height, width } = renderBox;
|
||||||
|
const { ctx } = root;
|
||||||
|
|
||||||
|
// const isCellSelected =
|
||||||
|
// root.selection.selectedCell?.row === this.position.row &&
|
||||||
|
// root.selection.selectedCell.column === this.position.column;
|
||||||
|
// const isCellInRange = this.isCellInRange(root);
|
||||||
|
y -= root.viewport.top;
|
||||||
|
x -= root.viewport.left;
|
||||||
|
|
||||||
|
const styles = this.style ?? root.styles.cells;
|
||||||
|
|
||||||
|
ctx.clearRect(x, y, width, height);
|
||||||
|
ctx.fillStyle = styles.background;
|
||||||
|
ctx.strokeStyle = "black";
|
||||||
|
ctx.fillRect(x, y, width - 1, height - 1);
|
||||||
|
ctx.strokeRect(x, y, width, height);
|
||||||
|
|
||||||
|
ctx.fillStyle = styles.fontColor;
|
||||||
|
ctx.textAlign = "left";
|
||||||
|
ctx.font = `${styles.fontSize}px Arial`;
|
||||||
|
ctx.textBaseline = "middle";
|
||||||
|
ctx.fillText(this.displayValue, x + 2, y + height / 2);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,106 @@
|
||||||
|
import Spreadsheet, { RangeSelectionType } from "../main";
|
||||||
|
import { Cell, CellConstructorProps, CellStyles, Position } from "./cell";
|
||||||
|
import { EventTypes } from "./events";
|
||||||
|
|
||||||
|
export class Clipboard {
|
||||||
|
saved: Cell[][] | null = null;
|
||||||
|
root: Spreadsheet;
|
||||||
|
constructor(root: Spreadsheet) {
|
||||||
|
this.root = root;
|
||||||
|
}
|
||||||
|
|
||||||
|
copy(data: Cell[][], range: RangeSelectionType) {
|
||||||
|
const mapedData = data
|
||||||
|
.map((row) => {
|
||||||
|
return row
|
||||||
|
.map((item) => {
|
||||||
|
return item.displayValue;
|
||||||
|
})
|
||||||
|
.join("\t");
|
||||||
|
})
|
||||||
|
.join("\n");
|
||||||
|
|
||||||
|
this.saved = data;
|
||||||
|
navigator.clipboard.writeText(mapedData);
|
||||||
|
|
||||||
|
this.root.events.dispatch({
|
||||||
|
type: EventTypes.COPY_CELLS,
|
||||||
|
data,
|
||||||
|
dataAsString: mapedData,
|
||||||
|
range,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
paste(root: Spreadsheet, { column, row }: Position, event: ClipboardEvent) {
|
||||||
|
if (!this.saved) {
|
||||||
|
if (!event.clipboardData) return;
|
||||||
|
const data = event.clipboardData.getData("text");
|
||||||
|
try {
|
||||||
|
const arr = data.split("\n").map((item) => item.split("\t"));
|
||||||
|
const arrayOfCells = arr.map((innerRow) => {
|
||||||
|
return innerRow.map((item) => {
|
||||||
|
const cellProps: CellConstructorProps = {
|
||||||
|
displayValue: item,
|
||||||
|
position: {
|
||||||
|
column,
|
||||||
|
row,
|
||||||
|
},
|
||||||
|
resultValue: item,
|
||||||
|
style: new CellStyles(),
|
||||||
|
value: item,
|
||||||
|
};
|
||||||
|
return new Cell(cellProps);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
const rowsLength = arrayOfCells.length;
|
||||||
|
const colsLength = arrayOfCells[0] ? arrayOfCells[0].length : 0;
|
||||||
|
|
||||||
|
for (let i = 0; i < rowsLength; i++) {
|
||||||
|
for (let j = 0; j < colsLength; j++) {
|
||||||
|
const savedCell = arrayOfCells[i][j];
|
||||||
|
|
||||||
|
const position = {
|
||||||
|
column: column + j,
|
||||||
|
row: row + i,
|
||||||
|
};
|
||||||
|
|
||||||
|
const values = {
|
||||||
|
displayValue: savedCell.displayValue,
|
||||||
|
value: savedCell.value,
|
||||||
|
style: savedCell.style,
|
||||||
|
};
|
||||||
|
|
||||||
|
root.changeCellValues(position, values, false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
console.error("Cannot read clipboard. ", err);
|
||||||
|
}
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,14 @@
|
||||||
|
export type ColumnConstructorProperties = {
|
||||||
|
width: number;
|
||||||
|
title: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export class Column {
|
||||||
|
width: number;
|
||||||
|
title: string;
|
||||||
|
|
||||||
|
constructor(props: ColumnConstructorProperties) {
|
||||||
|
this.width = props.width;
|
||||||
|
this.title = props.title;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,63 @@
|
||||||
|
import { Cell } from "./cell";
|
||||||
|
import { Column } from "./column";
|
||||||
|
import { Row } from "./row";
|
||||||
|
import { RangeSelectionType, Selection } from "./selection";
|
||||||
|
|
||||||
|
export interface ViewProperties {
|
||||||
|
width: number;
|
||||||
|
height: number;
|
||||||
|
}
|
||||||
|
export type CellClickEvent = (event: MouseEvent, cell: Cell) => void;
|
||||||
|
export type SelectionChangeEvent = (selection: Selection) => void;
|
||||||
|
export type CellChangeEvent = (cell: Cell) => void;
|
||||||
|
export type CopyEvent = (
|
||||||
|
range: RangeSelectionType,
|
||||||
|
data: Cell[][],
|
||||||
|
dataAsString: string,
|
||||||
|
) => void;
|
||||||
|
|
||||||
|
export type ConfigProperties = {
|
||||||
|
/** Please, end it with '_' symbol.
|
||||||
|
*
|
||||||
|
* *Example:*
|
||||||
|
*
|
||||||
|
* 'test_'
|
||||||
|
* 'google_' */
|
||||||
|
rows: Row[];
|
||||||
|
columns: Column[];
|
||||||
|
view: ViewProperties;
|
||||||
|
onCellClick?: CellClickEvent | null;
|
||||||
|
onSelectionChange?: SelectionChangeEvent | null;
|
||||||
|
onCellChange?: CellChangeEvent | null;
|
||||||
|
onCopy?: CopyEvent | null;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type SheetConfigConstructorProps = {
|
||||||
|
rows: Row[];
|
||||||
|
columns: Column[];
|
||||||
|
};
|
||||||
|
|
||||||
|
export class Config {
|
||||||
|
rows: Row[];
|
||||||
|
columns: Column[];
|
||||||
|
view: ViewProperties = {
|
||||||
|
width: 800,
|
||||||
|
height: 600,
|
||||||
|
};
|
||||||
|
|
||||||
|
onCellClick: CellClickEvent | null = null;
|
||||||
|
onSelectonChange: SelectionChangeEvent | null = null;
|
||||||
|
onCellChange: CellChangeEvent | null = null;
|
||||||
|
onCopy: CopyEvent | null;
|
||||||
|
|
||||||
|
constructor(props: ConfigProperties) {
|
||||||
|
this.columns = props.columns;
|
||||||
|
this.rows = props.rows;
|
||||||
|
this.view = props.view;
|
||||||
|
|
||||||
|
this.onCellClick = props.onCellClick ?? null;
|
||||||
|
this.onSelectonChange = props.onSelectionChange ?? null;
|
||||||
|
this.onCellChange = props.onCellChange ?? null;
|
||||||
|
this.onCopy = props.onCopy ?? null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,142 @@
|
||||||
|
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);
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,23 @@
|
||||||
|
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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,34 @@
|
||||||
|
import { Position } from "./cell";
|
||||||
|
import { Config } from "./config";
|
||||||
|
|
||||||
|
export class RenderBox {
|
||||||
|
x: number;
|
||||||
|
y: number;
|
||||||
|
width: number;
|
||||||
|
height: number;
|
||||||
|
|
||||||
|
constructor(config: Config, cellPosition: Position) {
|
||||||
|
this.x = this.getXCoord(cellPosition.column, config);
|
||||||
|
this.y = this.getYCoord(cellPosition.row, config);
|
||||||
|
this.width = config.columns[cellPosition.column].width;
|
||||||
|
this.height = config.rows[cellPosition.row].height;
|
||||||
|
}
|
||||||
|
|
||||||
|
private getXCoord(column: number, config: Config): number {
|
||||||
|
let x = 0;
|
||||||
|
|
||||||
|
for (let i = 0; i < column; i++) {
|
||||||
|
x += config.columns[i].width;
|
||||||
|
}
|
||||||
|
|
||||||
|
return x;
|
||||||
|
}
|
||||||
|
|
||||||
|
private getYCoord(row: number, config: Config): number {
|
||||||
|
let y = 0;
|
||||||
|
for (let i = 0; i < row; i++) {
|
||||||
|
y += config.rows[i].height;
|
||||||
|
}
|
||||||
|
return y;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,13 @@
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,14 @@
|
||||||
|
export type BaseSelectionType = {
|
||||||
|
row: number;
|
||||||
|
column: number;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type RangeSelectionType = {
|
||||||
|
from: BaseSelectionType;
|
||||||
|
to: BaseSelectionType;
|
||||||
|
};
|
||||||
|
|
||||||
|
export class Selection {
|
||||||
|
selectedCell: BaseSelectionType | null = null;
|
||||||
|
selectedRange: RangeSelectionType | null = null;
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,8 @@
|
||||||
|
import { CellStyles } from "./cell";
|
||||||
|
|
||||||
|
export class Styles {
|
||||||
|
cells: CellStyles;
|
||||||
|
constructor() {
|
||||||
|
this.cells = new CellStyles();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,78 @@
|
||||||
|
import Spreadsheet from "../main";
|
||||||
|
|
||||||
|
export type ViewportConstructorProps = {
|
||||||
|
top: number;
|
||||||
|
left: number;
|
||||||
|
right: number;
|
||||||
|
bottom: number;
|
||||||
|
};
|
||||||
|
|
||||||
|
export class Viewport {
|
||||||
|
root: Spreadsheet;
|
||||||
|
|
||||||
|
top: number;
|
||||||
|
left: number;
|
||||||
|
right: number;
|
||||||
|
bottom: number;
|
||||||
|
|
||||||
|
firstRow: number;
|
||||||
|
lastRow: number;
|
||||||
|
firstCol: number;
|
||||||
|
lastCol: number;
|
||||||
|
|
||||||
|
constructor(root: Spreadsheet, props: ViewportConstructorProps) {
|
||||||
|
this.root = root;
|
||||||
|
|
||||||
|
this.top = props.top;
|
||||||
|
this.left = props.left;
|
||||||
|
this.right = props.right;
|
||||||
|
this.bottom = props.bottom;
|
||||||
|
|
||||||
|
this.firstRow = this.getFirstRow();
|
||||||
|
this.lastCol = this.getFirstRow(); //!Temp
|
||||||
|
this.firstCol = this.getFirstRow(); //!Temp
|
||||||
|
this.lastRow = this.getLastRow();
|
||||||
|
|
||||||
|
this.updateValues({
|
||||||
|
top: 0,
|
||||||
|
left: 0,
|
||||||
|
right: this.root.viewProps.width,
|
||||||
|
bottom: this.root.viewProps.height,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
updateValues(props: ViewportConstructorProps) {
|
||||||
|
this.top = props.top;
|
||||||
|
this.left = props.left;
|
||||||
|
this.right = props.right;
|
||||||
|
this.bottom = props.bottom;
|
||||||
|
|
||||||
|
this.firstRow = this.getFirstRow();
|
||||||
|
this.lastRow = this.getLastRow();
|
||||||
|
this.firstCol = this.getFirstCol();
|
||||||
|
this.lastCol = this.getLastCol();
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Get index of first row in viewport */
|
||||||
|
private getFirstRow(): number {
|
||||||
|
const rowIdx = this.root.cache.getRowByYCoord(this.top);
|
||||||
|
return rowIdx;
|
||||||
|
}
|
||||||
|
|
||||||
|
private getLastRow(): number {
|
||||||
|
const rowIdx = this.root.cache.getRowByYCoord(this.bottom);
|
||||||
|
return rowIdx;
|
||||||
|
}
|
||||||
|
|
||||||
|
private getFirstCol(): number {
|
||||||
|
const colIdx = this.root.cache.getColumnByXCoord(this.left);
|
||||||
|
|
||||||
|
return colIdx;
|
||||||
|
}
|
||||||
|
|
||||||
|
private getLastCol(): number {
|
||||||
|
const colIdx = this.root.cache.getColumnByXCoord(this.right);
|
||||||
|
|
||||||
|
return colIdx;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,4 @@
|
||||||
|
body {
|
||||||
|
padding: 0px;
|
||||||
|
margin: 0px;
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,38 @@
|
||||||
|
$css_prefix: "modern_sc_";
|
||||||
|
|
||||||
|
.#{$css_prefix}spreadsheet_container {
|
||||||
|
position: relative;
|
||||||
|
isolation: isolate;
|
||||||
|
border: 2px solid black;
|
||||||
|
}
|
||||||
|
|
||||||
|
.#{$css_prefix}content {
|
||||||
|
position: absolute;
|
||||||
|
}
|
||||||
|
|
||||||
|
.#{$css_prefix}sheet {
|
||||||
|
display: block;
|
||||||
|
contain: strict;
|
||||||
|
}
|
||||||
|
|
||||||
|
.#{$css_prefix}scroller {
|
||||||
|
position: absolute;
|
||||||
|
overflow: scroll;
|
||||||
|
box-sizing: border-box;
|
||||||
|
transform: translateZ(0);
|
||||||
|
|
||||||
|
&:focus {
|
||||||
|
outline: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.#{$css_prefix}editor {
|
||||||
|
position: absolute;
|
||||||
|
box-sizing: border-box;
|
||||||
|
font-size: 16px;
|
||||||
|
font-family: Arial, Helvetica, sans-serif;
|
||||||
|
}
|
||||||
|
|
||||||
|
.#{$css_prefix}hide {
|
||||||
|
visibility: hidden;
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,2 @@
|
||||||
|
@import "global.scss";
|
||||||
|
@import "spreadsheet.scss";
|
||||||
|
|
@ -0,0 +1,68 @@
|
||||||
|
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
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,80 @@
|
||||||
|
import { Cell } from "../modules/cell";
|
||||||
|
import { Column } from "../modules/column";
|
||||||
|
import { Config } from "../modules/config";
|
||||||
|
import { Row } from "../modules/row";
|
||||||
|
|
||||||
|
export function createSampleData(
|
||||||
|
rows: number,
|
||||||
|
columns: number,
|
||||||
|
fillCellsByCoords: boolean = false,
|
||||||
|
): Cell[][] {
|
||||||
|
const data: Cell[][] = [];
|
||||||
|
|
||||||
|
for (let row = 0; row <= rows; row++) {
|
||||||
|
const innerRow: Cell[] = [];
|
||||||
|
for (let col = 0; col <= columns; col++) {
|
||||||
|
const value = fillCellsByCoords ? `${row}:${col}` : "";
|
||||||
|
|
||||||
|
const cell = new Cell({
|
||||||
|
displayValue: value,
|
||||||
|
resultValue: value,
|
||||||
|
value,
|
||||||
|
position: {
|
||||||
|
column: col,
|
||||||
|
row: row,
|
||||||
|
},
|
||||||
|
style: null,
|
||||||
|
});
|
||||||
|
|
||||||
|
innerRow.push(cell);
|
||||||
|
}
|
||||||
|
data.push(innerRow);
|
||||||
|
}
|
||||||
|
return data;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function createSampleConfig(rows: number, columns: number): Config {
|
||||||
|
const rowsArr: Row[] = [];
|
||||||
|
for (let i = 0; i <= rows; i++) {
|
||||||
|
const rowItem = new Row({
|
||||||
|
height: 40,
|
||||||
|
title: String(i),
|
||||||
|
});
|
||||||
|
rowsArr.push(rowItem);
|
||||||
|
}
|
||||||
|
|
||||||
|
const colsArr: Column[] = [];
|
||||||
|
for (let i = 0; i <= columns; i++) {
|
||||||
|
const colItem = new Column({
|
||||||
|
title: String(i),
|
||||||
|
width: 150,
|
||||||
|
});
|
||||||
|
colsArr.push(colItem);
|
||||||
|
}
|
||||||
|
|
||||||
|
const config = new Config({
|
||||||
|
columns: colsArr,
|
||||||
|
rows: rowsArr,
|
||||||
|
view: {
|
||||||
|
height: 600,
|
||||||
|
width: 800,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
return config;
|
||||||
|
}
|
||||||
|
|
||||||
|
export type SpreadsheetConfigAndDataReturnType = {
|
||||||
|
config: Config;
|
||||||
|
data: Cell[][];
|
||||||
|
};
|
||||||
|
|
||||||
|
export function makeSpreadsheetConfigAndData(
|
||||||
|
rows: number,
|
||||||
|
columns: number,
|
||||||
|
): SpreadsheetConfigAndDataReturnType {
|
||||||
|
const data = createSampleData(rows, columns);
|
||||||
|
const config = createSampleConfig(rows, columns);
|
||||||
|
|
||||||
|
return { data, config };
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,20 @@
|
||||||
|
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
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1 @@
|
||||||
|
/// <reference types="vite/client" />
|
||||||
|
|
@ -0,0 +1,30 @@
|
||||||
|
{
|
||||||
|
"compilerOptions": {
|
||||||
|
"target": "ESNext",
|
||||||
|
"lib": ["ESNext", "DOM", "DOM.Iterable"],
|
||||||
|
"types": ["vite/client", "node"],
|
||||||
|
"allowJs": false,
|
||||||
|
"skipLibCheck": true,
|
||||||
|
"esModuleInterop": true,
|
||||||
|
"allowSyntheticDefaultImports": true,
|
||||||
|
"strict": true,
|
||||||
|
"forceConsistentCasingInFileNames": true,
|
||||||
|
"module": "ESNext",
|
||||||
|
"moduleResolution": "bundler",
|
||||||
|
"resolveJsonModule": true,
|
||||||
|
"isolatedModules": true,
|
||||||
|
"noEmit": true,
|
||||||
|
|
||||||
|
"useDefineForClassFields": true,
|
||||||
|
|
||||||
|
/* Bundler mode */
|
||||||
|
"allowImportingTsExtensions": true,
|
||||||
|
|
||||||
|
/* Linting */
|
||||||
|
"noUnusedLocals": true,
|
||||||
|
"noUnusedParameters": true,
|
||||||
|
"noFallthroughCasesInSwitch": true
|
||||||
|
},
|
||||||
|
"include": ["src"],
|
||||||
|
"exclude": ["node_modules"]
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,61 @@
|
||||||
|
import { defineConfig } from "vite";
|
||||||
|
import path from "path";
|
||||||
|
import typescript from "@rollup/plugin-typescript";
|
||||||
|
import { typescriptPaths } from "rollup-plugin-typescript-paths";
|
||||||
|
|
||||||
|
const BROWSER_MODE = process.env.BUILD_BROWSER === 'true';
|
||||||
|
console.log({ BROWSER_MODE });
|
||||||
|
|
||||||
|
const libConfig = defineConfig({
|
||||||
|
base: "/modern_spreadsheet/",
|
||||||
|
plugins: [],
|
||||||
|
resolve: {},
|
||||||
|
server: {
|
||||||
|
port: 5179,
|
||||||
|
open: true,
|
||||||
|
},
|
||||||
|
build: {
|
||||||
|
manifest: true,
|
||||||
|
minify: true,
|
||||||
|
reportCompressedSize: true,
|
||||||
|
sourcemap: true,
|
||||||
|
lib: {
|
||||||
|
entry: path.resolve(__dirname, "src/main.ts"),
|
||||||
|
fileName: "main",
|
||||||
|
formats: ["es", "cjs"],
|
||||||
|
},
|
||||||
|
rollupOptions: {
|
||||||
|
external: ["./src/index.ts"],
|
||||||
|
plugins: [
|
||||||
|
typescriptPaths({
|
||||||
|
preserveExtensions: true,
|
||||||
|
}),
|
||||||
|
typescript({
|
||||||
|
sourceMap: false,
|
||||||
|
declaration: true,
|
||||||
|
outDir: "dist",
|
||||||
|
}),
|
||||||
|
],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const browserConfig = defineConfig({
|
||||||
|
base: "/modern_spreadsheet/",
|
||||||
|
resolve: {},
|
||||||
|
build: {
|
||||||
|
manifest: true,
|
||||||
|
minify: true,
|
||||||
|
reportCompressedSize: true,
|
||||||
|
sourcemap: true,
|
||||||
|
lib: {
|
||||||
|
entry: path.resolve(__dirname, 'index.html'),
|
||||||
|
fileName: 'demo',
|
||||||
|
formats: ['es']
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
const config = BROWSER_MODE ? browserConfig : libConfig;
|
||||||
|
|
||||||
|
export default config;
|
||||||
Loading…
Reference in New Issue