Added eslint & prettier config

Formatted files in project
This commit is contained in:
Eugene 2023-07-25 16:59:49 +03:00
parent 9e25b2869c
commit 55e4eb0f70
29 changed files with 1978 additions and 1058 deletions

26
.eslintrc.cjs Normal file
View File

@ -0,0 +1,26 @@
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: {},
};

2
.prettierignore Normal file
View File

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

1
.prettierrc.json Normal file
View File

@ -0,0 +1 @@
{}

View File

@ -1,33 +1,37 @@
# Modern Spreadsheet # Modern Spreadsheet
- High performance spreadsheet based on CanvasAPI. - High performance spreadsheet based on CanvasAPI.
- TypeScript supported - TypeScript supported
## Basic usage ## Basic usage
```ts
import Spreadsheet from 'modern_spreadsheet'
import 'modern_spreadsheet/style.css' // <= this is required
const target = document.getElementById('spreadsheet') ```ts
const sheet = new Spreadsheet(target) 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 ## Save and load data
```ts ```ts
function saveData() { function saveData() {
const serialized = sheet.serializeData() const serialized = sheet.serializeData();
localStorage.setItem('sheet_data', JSON.stringify(serialized)) localStorage.setItem("sheet_data", JSON.stringify(serialized));
} }
function loadData() { function loadData() {
const data = localStorage.getItem('sheet_data') const data = localStorage.getItem("sheet_data");
const json = JSON.parse(data) const json = JSON.parse(data);
if(!json) return; if (!json) return;
sheet.loadData(json) sheet.loadData(json);
} }
``` ```
## Roadmap ## Roadmap
- Custom event functions (ex.: onSelectionChange, onCellEdit...). Full list of supported events will available on this page - Custom event functions (ex.: onSelectionChange, onCellEdit...). Full list of supported events will available on this page
- Rows number and columns heading render - Rows number and columns heading render
- Rows and columns resizing - Rows and columns resizing
@ -37,4 +41,4 @@ function loadData() {
- Selected cell depends cells highlight - Selected cell depends cells highlight
- Async formulas support - Async formulas support
- Mutlisheets (?) - Mutlisheets (?)
- -

View File

@ -1,4 +1,4 @@
<!DOCTYPE html> <!doctype html>
<html lang="en"> <html lang="en">
<head> <head>
<meta charset="UTF-8" /> <meta charset="UTF-8" />

View File

@ -36,12 +36,19 @@
"build:watch": "tsc && vite build --watch", "build:watch": "tsc && vite build --watch",
"preview": "vite preview", "preview": "vite preview",
"predeploy": "npm run dist", "predeploy": "npm run dist",
"deploy": "gh-pages -d dist" "deploy": "gh-pages -d dist",
"lint": "eslint src --ext .ts",
"lint:fix": "eslint src --ext .ts --fix"
}, },
"devDependencies": { "devDependencies": {
"@rollup/plugin-typescript": "^11.1.2", "@rollup/plugin-typescript": "^11.1.2",
"@types/node": "^20.4.4", "@types/node": "^20.4.4",
"@typescript-eslint/eslint-plugin": "^6.2.0",
"@typescript-eslint/parser": "^6.2.0",
"eslint": "^8.45.0",
"eslint-config-prettier": "^8.8.0",
"gh-pages": "^5.0.0", "gh-pages": "^5.0.0",
"prettier": "3.0.0",
"rollup-plugin-typescript-paths": "^1.4.0", "rollup-plugin-typescript-paths": "^1.4.0",
"sass": "^1.63.6", "sass": "^1.63.6",
"tslib": "^2.6.0", "tslib": "^2.6.0",

File diff suppressed because it is too large Load Diff

View File

@ -3,68 +3,66 @@ import { Position } from "../modules/cell";
import { RenderBox } from "../modules/renderBox"; import { RenderBox } from "../modules/renderBox";
export class Editor { export class Editor {
element: HTMLInputElement element: HTMLInputElement;
root: Spreadsheet root: Spreadsheet;
constructor(root: Spreadsheet) { constructor(root: Spreadsheet) {
this.root = root this.root = root;
const element = document.createElement('input') const element = document.createElement("input");
element.classList.add(CSS_PREFIX + 'editor') element.classList.add(CSS_PREFIX + "editor");
this.element = element this.element = element;
this.hide() 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 + "px";
this.element.style.left = x - this.root.viewport.left + "px";
this.element.style.width = width + "px";
this.element.style.height = height + "px";
this.element.style.display = "block";
window.addEventListener("click", this.handleClickOutside);
this.element.addEventListener("keydown", this.handleKeydown);
this.element.value = 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": {
this.root.changeCellValues(this.root.selection.selectedCell!, {
value: this.element.value,
displayValue: this.element.value,
});
this.hide();
}
} }
};
hide() { handleClickOutside = (event: MouseEvent) => {
this.element.style.display = 'none' const target = event.target as HTMLElement;
this.element.classList.add('hide') if (!this.element.contains(target)) {
this.element.blur() this.hide();
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) + 'px'
this.element.style.left = (x - this.root.viewport.left) + 'px'
this.element.style.width = width + 'px'
this.element.style.height = height + 'px'
this.element.style.display = 'block'
window.addEventListener('click', this.handleClickOutside)
this.element.addEventListener('keydown', this.handleKeydown)
this.element.value = 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': {
this.root.changeCellValues(this.root.selection.selectedCell!, {
value: this.element.value,
displayValue: this.element.value
})
this.hide();
}
}
}
handleClickOutside = (event: MouseEvent) => {
const target = event.target as HTMLElement
if (!this.element.contains(target)) {
this.hide()
}
}
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -1,34 +1,37 @@
import Spreadsheet from './main' import Spreadsheet from "./main";
const saveButton = document.querySelector('#save_button') const saveButton = document.querySelector("#save_button");
const loadButton = document.querySelector('#load_button') const loadButton = document.querySelector("#load_button");
if(!saveButton || !loadButton) throw new Error("LOST") if (!saveButton || !loadButton) throw new Error("LOST");
const sheet = new Spreadsheet('#spreadsheet') const sheet = new Spreadsheet("#spreadsheet");
const sheet2 = new Spreadsheet('#spreadsheet_2') const sheet2 = new Spreadsheet("#spreadsheet_2");
console.log(sheet2) console.log(sheet2);
function saveDataToLS() { function saveDataToLS() {
const serializableData = sheet.serializeData() const serializableData = sheet.serializeData();
localStorage.setItem('sheet', JSON.stringify(serializableData)) localStorage.setItem("sheet", JSON.stringify(serializableData));
} }
function loadDataFromLS() { function loadDataFromLS() {
const data = localStorage.getItem('sheet') const data = localStorage.getItem("sheet");
if(!data) return if (!data) return;
const json = JSON.parse(data) const json = JSON.parse(data);
sheet.loadData(json) sheet.loadData(json);
} }
saveButton.addEventListener('click', saveDataToLS) saveButton.addEventListener("click", saveDataToLS);
loadButton.addEventListener('click', loadDataFromLS) loadButton.addEventListener("click", loadDataFromLS);
sheet.changeCellStyles({column: 1, row: 1}, { sheet.changeCellStyles(
background: 'black', { column: 1, row: 1 },
borderColor: 'white', {
fontColor: 'white', background: "black",
borderColor: "white",
fontColor: "white",
fontSize: 20, fontSize: 20,
selectedBackground: 'green', selectedBackground: "green",
selectedFontColor: 'black' selectedFontColor: "black",
}) },
);

View File

@ -4,12 +4,18 @@ import { Scroller } from "./components/scroller";
import { Sheet } from "./components/sheet"; import { Sheet } from "./components/sheet";
import { Table } from "./components/table"; import { Table } from "./components/table";
import { Toolbar } from "./components/toolbar"; import { Toolbar } from "./components/toolbar";
import { Cell, CellConstructorProps, CellStyles, Position, SerializableCell } from "./modules/cell"; import {
Cell,
CellConstructorProps,
CellStyles,
Position,
SerializableCell,
} from "./modules/cell";
import { Config, ViewProperties } from "./modules/config"; import { Config, ViewProperties } from "./modules/config";
import { RangeSelectionType, Selection } from "./modules/selection"; import { RangeSelectionType, Selection } from "./modules/selection";
import { Styles } from "./modules/styles"; import { Styles } from "./modules/styles";
import { Viewport } from "./modules/viewport"; import { Viewport } from "./modules/viewport";
import './scss/main.scss' import "./scss/main.scss";
import { createSampleData } from "./utils/createData"; import { createSampleData } from "./utils/createData";
import { Cache, CachedColumn, CachedRow } from "./modules/cache"; import { Cache, CachedColumn, CachedRow } from "./modules/cache";
import { Row } from "./modules/row"; import { Row } from "./modules/row";
@ -28,300 +34,322 @@ import { Column } from "./modules/column";
*/ */
interface SpreadsheetConstructorProperties { interface SpreadsheetConstructorProperties {
config?: Omit<Config, 'view'> // Not optional. config?: Omit<Config, "view">; // Not optional.
view?: ViewProperties view?: ViewProperties;
} }
export const CSS_PREFIX = "modern_sc_" export const CSS_PREFIX = "modern_sc_";
export default class Spreadsheet { export default class Spreadsheet {
private table: Table private table: Table;
private scroller: Scroller private scroller: Scroller;
private toolbar: Toolbar private toolbar: Toolbar;
private header: Header private header: Header;
private sheet: Sheet private sheet: Sheet;
private editor: Editor private editor: Editor;
public styles: Styles public styles: Styles;
public config: Config public config: Config;
public data: Cell[][] public data: Cell[][];
public viewport: Viewport public viewport: Viewport;
public selection: Selection public selection: Selection;
public cache: Cache public cache: Cache;
constructor(target: string | HTMLElement, props?: SpreadsheetConstructorProperties) { constructor(
const data = createSampleData(40, 40) target: string | HTMLElement,
const config = this.makeConfigFromData(data, props?.view ?? { height: 600, width: 800 }) props?: SpreadsheetConstructorProperties,
if (props?.view) { ) {
config.view = props.view const data = createSampleData(40, 40);
} const config = this.makeConfigFromData(
data,
this.config = new Config(config) props?.view ?? { height: 600, width: 800 },
this.sheet = new Sheet(this) );
if (props?.view) {
this.table = new Table(this) config.view = props.view;
this.scroller = new Scroller(this)
this.toolbar = new Toolbar(this)
this.header = new Header(this)
this.editor = new Editor(this)
this.cache = this.getInitialCache()
this.viewport = new Viewport(this, this.scroller.getViewportBoundlingRect())
this.selection = new Selection()
this.data = data
this.styles = new Styles()
this.buildComponent()
this.appendTableToTarget(target)
this.renderSheet()
} }
private getInitialCache(): Cache { this.config = new Config(config);
const cachedCols: CachedColumn[] = [] this.sheet = new Sheet(this);
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[] = [] this.table = new Table(this);
let currentHeight = 0 this.scroller = new Scroller(this);
for (let i = 0; i <= this.config.rows.length - 1; i++) { this.toolbar = new Toolbar(this);
const row = this.config.rows[i] this.header = new Header(this);
currentHeight += row.height this.editor = new Editor(this);
const cacheRow = new CachedRow({ this.cache = this.getInitialCache();
yPos: currentHeight, this.viewport = new Viewport(
rowIdx: i this,
}) this.scroller.getViewportBoundlingRect(),
cachedRows.push(cacheRow) );
} this.selection = new Selection();
this.data = data;
this.styles = new Styles();
this.buildComponent();
this.appendTableToTarget(target);
this.renderSheet();
}
const cache = new Cache({ private getInitialCache(): Cache {
columns: cachedCols, const cachedCols: CachedColumn[] = [];
rows: cachedRows let currentWidth = 0;
}) for (let i = 0; i <= this.config.columns.length - 1; i++) {
const col = this.config.columns[i];
console.log("CACHE: ", cache) currentWidth += col.width;
console.log("CONFIG: ", this.config) const cacheCol = new CachedColumn({
return cache xPos: currentWidth,
colIdx: i,
});
cachedCols.push(cacheCol);
} }
private buildComponent(): void { const cachedRows: CachedRow[] = [];
let currentHeight = 0;
const content = document.createElement('div') //* Abstract for (let i = 0; i <= this.config.rows.length - 1; i++) {
content.appendChild(this.header.element) const row = this.config.rows[i];
content.appendChild(this.sheet.element) currentHeight += row.height;
const cacheRow = new CachedRow({
content.classList.add(CSS_PREFIX + 'content') yPos: currentHeight,
rowIdx: i,
this.table.element.appendChild(this.toolbar.element) });
this.table.element.appendChild(content) cachedRows.push(cacheRow);
this.table.element.appendChild(this.scroller.element)
this.table.element.append(this.editor.element)
} }
/**Destroy spreadsheet DOM element. const cache = new Cache({
* columns: cachedCols,
* May be usefull when need to rerender component. rows: cachedRows,
*/ });
public destroy() {
this.table.element.remove() console.log("CACHE: ", cache);
console.log("CONFIG: ", this.config);
return cache;
}
private buildComponent(): void {
const content = document.createElement("div"); //* Abstract
content.appendChild(this.header.element);
content.appendChild(this.sheet.element);
content.classList.add(CSS_PREFIX + "content");
this.table.element.appendChild(this.toolbar.element);
this.table.element.appendChild(content);
this.table.element.appendChild(this.scroller.element);
this.table.element.append(this.editor.element);
}
/**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;
}
/** 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">>,
) {
const { column, row } = position;
this.data[row][column].changeValues(values);
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();
}
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] ? this.data[0].length : 0;
this.data = [];
const formattedData: Cell[][] = [];
for (let row = 0; row < rowsLength; row++) {
const innerRow: Cell[] = [];
for (let col = 0; col < colsLength; col++) {
const cell = data[row][col];
innerRow.push(
new Cell({
displayValue: cell.displayValue,
position: cell.position,
resultValue: cell.resultValue,
value: cell.value,
style: cell.style,
}),
);
}
formattedData.push(innerRow);
} }
private appendTableToTarget(target: string | HTMLElement) { this.data = formattedData;
if (typeof target === 'string') {
const element = document.querySelector(target) this.selection.selectedCell = null;
if (!element) throw new Error(`Element with selector ${target} is not finded in DOM.\n Make sure it exists.`) this.selection.selectedRange = null;
element?.appendChild(this.table.element) this.config = this.makeConfigFromData(formattedData, this.config.view);
} this.cache = this.getInitialCache();
if (target instanceof HTMLElement) { this.scroller.updateScrollerSize();
target.append(this.table.element) 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),
}),
);
} }
/** Canvas rendering context 2D. const columns: Column[] = [];
*
* Abble to draw on canvas with default CanvasAPI methods for (let col = 0; col < lastColIdx; col++) {
*/ columns.push(
get ctx() { new Column({
return this.sheet.ctx width: 150,
title: String(col),
}),
);
} }
get viewProps() { const config = new Config({
return this.config.view view,
rows,
columns,
});
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);
} }
/** Focusing on interactive part of spreadsheet */ return cellsArray;
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'>>) {
const { column, row } = position
this.data[row][column].changeValues(values)
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) => any): void {
const fromRow = Math.min(range.from.row, range.to.row)
const toRow = Math.max(range.from.row, range.to.row)
const fromCol = Math.min(range.from.column, range.to.column)
const toCol = Math.max(range.from.column, range.to.column)
for (let row = fromRow; row <= toRow; row++) {
for (let col = fromCol; col <= toCol; col++) {
const cell = this.data[row][col]
callback(cell)
}
}
}
deleteSelectedCellsValues() {
if (this.selection.selectedRange !== null) {
this.applyActionToRange(this.selection.selectedRange, cell => {
this.changeCellValues(cell.position, {
displayValue: '',
resultValue: '',
value: ''
})
})
} else {
if (!this.selection.selectedCell) return;
this.changeCellValues(this.selection.selectedCell, {
displayValue: '',
resultValue: '',
value: ''
})
}
}
showEditor(position: Position, initialString?: string) {
this.editor.show(position, initialString)
}
renderSheet() {
this.sheet.renderSheet()
}
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] ? this.data[0].length : 0
this.data = []
const formattedData: Cell[][] = []
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)
}
this.data = formattedData
this.selection.selectedCell = null
this.selection.selectedRange = null
this.config = this.makeConfigFromData(formattedData, this.config.view)
this.cache = this.getInitialCache()
this.scroller.updateScrollerSize()
this.viewport = new Viewport(this, this.scroller.getViewportBoundlingRect())
this.renderSheet()
return this
}
private makeConfigFromData(data: Cell[][], view: ViewProperties): Config {
const lastRowIdx = data.length - 1
const lastColIdx = data[0] ? data[0].length : 0
const rows: Row[] = []
for (let row = 0; row < lastRowIdx; row++) {
rows.push(new Row({
height: 40,
title: String(row)
}))
}
const columns: Column[] = []
for (let col = 0; col < lastColIdx; col++) {
columns.push(new Column({
width: 150,
title: String(col)
}))
}
const config = new Config({
view,
rows,
columns
})
return config
}
public serializeData(): SerializableCell[][] {
const rowsLength = this.data.length
const colsLength = this.data[0] ? this.data[0].length : 0
const cellsArray: SerializableCell[][] = []
for (let row = 0; row < rowsLength; row++) {
const innerRow: SerializableCell[] = []
for (let col = 0; col < colsLength; col++) {
innerRow.push(this.data[row][col].getSerializableCell())
}
cellsArray.push(innerRow)
}
return cellsArray
}
} }
export * from './modules/cache' export * from "./modules/cache";
export * from './modules/cell' export * from "./modules/cell";
export * from './modules/column' export * from "./modules/column";
export * from './modules/config' export * from "./modules/config";
export * from './modules/renderBox' export * from "./modules/renderBox";
export * from './modules/row' export * from "./modules/row";
export * from './modules/selection' export * from "./modules/selection";
export * from './modules/styles' export * from "./modules/styles";
export * from './modules/viewport' export * from "./modules/viewport";
export * from './utils/createData' export * from "./utils/createData";

View File

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

View File

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

View File

@ -1,14 +1,14 @@
export type ColumnConstructorProperties = { export type ColumnConstructorProperties = {
width: number width: number;
title: string title: string;
} };
export class Column { export class Column {
width: number width: number;
title: string title: string;
constructor(props: ColumnConstructorProperties) { constructor(props: ColumnConstructorProperties) {
this.width = props.width this.width = props.width;
this.title = props.title this.title = props.title;
} }
} }

View File

@ -1,39 +1,38 @@
import { Column } from "./column" import { Column } from "./column";
import { Row } from "./row" import { Row } from "./row";
export interface ViewProperties { export interface ViewProperties {
width: number width: number;
height: number height: number;
} }
export type ConfigProperties = { export type ConfigProperties = {
/** Please, end it with '_' symbol. /** Please, end it with '_' symbol.
* *
* *Example:* * *Example:*
* *
* 'test_' * 'test_'
* 'google_' */ * 'google_' */
rows: Row[] rows: Row[];
columns: Column[] columns: Column[];
view: ViewProperties view: ViewProperties;
} };
export type SheetConfigConstructorProps = { export type SheetConfigConstructorProps = {
rows: Row[] rows: Row[];
columns: Column[] columns: Column[];
} };
export class Config { export class Config {
rows: Row[] rows: Row[];
columns: Column[] columns: Column[];
view: ViewProperties = { view: ViewProperties = {
width: 800, width: 800,
height: 600, height: 600,
} };
constructor(props: ConfigProperties) { constructor(props: ConfigProperties) {
this.columns = props.columns this.columns = props.columns;
this.rows = props.rows this.rows = props.rows;
this.view = props.view this.view = props.view;
} }
} }

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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