Added eslint & prettier config
Formatted files in project
This commit is contained in:
parent
9e25b2869c
commit
55e4eb0f70
|
|
@ -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: {},
|
||||||
|
};
|
||||||
|
|
@ -0,0 +1,2 @@
|
||||||
|
# Ignore artifacts:
|
||||||
|
dist
|
||||||
|
|
@ -0,0 +1 @@
|
||||||
|
{}
|
||||||
24
README.md
24
README.md
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
<!DOCTYPE html>
|
<!doctype html>
|
||||||
<html lang="en">
|
<html lang="en">
|
||||||
<head>
|
<head>
|
||||||
<meta charset="UTF-8" />
|
<meta charset="UTF-8" />
|
||||||
|
|
|
||||||
|
|
@ -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",
|
||||||
|
|
|
||||||
813
pnpm-lock.yaml
813
pnpm-lock.yaml
File diff suppressed because it is too large
Load Diff
|
|
@ -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() {
|
hide() {
|
||||||
this.element.style.display = 'none'
|
this.element.style.display = "none";
|
||||||
this.element.classList.add('hide')
|
this.element.classList.add("hide");
|
||||||
this.element.blur()
|
this.element.blur();
|
||||||
window.removeEventListener('click', this.handleClickOutside)
|
window.removeEventListener("click", this.handleClickOutside);
|
||||||
this.element.removeEventListener('keydown', this.handleKeydown)
|
this.element.removeEventListener("keydown", this.handleKeydown);
|
||||||
|
|
||||||
this.root.focusTable()
|
this.root.focusTable();
|
||||||
}
|
}
|
||||||
|
|
||||||
show(position: Position, initialString?: string) {
|
show(position: Position, initialString?: string) {
|
||||||
const { height, width, x, y } = new RenderBox(this.root.config, position);
|
const { height, width, x, y } = new RenderBox(this.root.config, position);
|
||||||
const cell = this.root.getCell(position)
|
const cell = this.root.getCell(position);
|
||||||
this.element.classList.remove('hide')
|
this.element.classList.remove("hide");
|
||||||
|
|
||||||
this.element.style.top = (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()
|
|
||||||
|
|
||||||
|
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) => {
|
handleKeydown = (event: KeyboardEvent) => {
|
||||||
const { key } = event
|
const { key } = event;
|
||||||
|
|
||||||
switch (key) {
|
switch (key) {
|
||||||
case 'Escape': {
|
case "Escape": {
|
||||||
this.hide();
|
this.hide();
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
case 'Enter': {
|
case "Enter": {
|
||||||
this.root.changeCellValues(this.root.selection.selectedCell!, {
|
this.root.changeCellValues(this.root.selection.selectedCell!, {
|
||||||
value: this.element.value,
|
value: this.element.value,
|
||||||
displayValue: this.element.value
|
displayValue: this.element.value,
|
||||||
})
|
});
|
||||||
this.hide();
|
this.hide();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
};
|
||||||
|
|
||||||
handleClickOutside = (event: MouseEvent) => {
|
handleClickOutside = (event: MouseEvent) => {
|
||||||
const target = event.target as HTMLElement
|
const target = event.target as HTMLElement;
|
||||||
if (!this.element.contains(target)) {
|
if (!this.element.contains(target)) {
|
||||||
this.hide()
|
this.hide();
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
@ -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;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -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) => {
|
private handleMouseMove = (event: MouseEvent) => {
|
||||||
if (!this.isSelecting) return;
|
if (!this.isSelecting) return;
|
||||||
const { offsetX, offsetY } = event
|
const { offsetX, offsetY } = event;
|
||||||
const lastSelectedCell = this.root.getCellByCoords(offsetX, offsetY)
|
const lastSelectedCell = this.root.getCellByCoords(offsetX, offsetY);
|
||||||
if (this.root.selection.selectedRange) {
|
if (this.root.selection.selectedRange) {
|
||||||
this.root.selection.selectedRange.to = lastSelectedCell
|
this.root.selection.selectedRange.to = lastSelectedCell;
|
||||||
}
|
|
||||||
this.root.renderSheet()
|
|
||||||
}
|
}
|
||||||
|
this.root.renderSheet();
|
||||||
|
};
|
||||||
|
|
||||||
private handleMouseUp = () => {
|
private handleMouseUp = () => {
|
||||||
this.isSelecting = false
|
this.isSelecting = false;
|
||||||
|
|
||||||
if (this.root.selection.selectedRange) {
|
if (this.root.selection.selectedRange) {
|
||||||
if (
|
if (
|
||||||
(this.root.selection.selectedRange.from.row === this.root.selection.selectedRange.to.row) &&
|
this.root.selection.selectedRange.from.row ===
|
||||||
(this.root.selection.selectedRange.from.column === this.root.selection.selectedRange.to.column)
|
this.root.selection.selectedRange.to.row &&
|
||||||
|
this.root.selection.selectedRange.from.column ===
|
||||||
|
this.root.selection.selectedRange.to.column
|
||||||
) {
|
) {
|
||||||
this.root.selection.selectedRange = null
|
this.root.selection.selectedRange = null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
this.root.renderSheet()
|
this.root.renderSheet();
|
||||||
}
|
};
|
||||||
|
|
||||||
private handleDoubleClick = (event: MouseEvent) => {
|
private handleDoubleClick = (event: MouseEvent) => {
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
const position = this.root.getCellByCoords(event.offsetX, event.offsetY)
|
const position = this.root.getCellByCoords(event.offsetX, event.offsetY);
|
||||||
this.root.showEditor(position)
|
this.root.showEditor(position);
|
||||||
}
|
};
|
||||||
|
|
||||||
private handleKeydown = (event: KeyboardEvent) => {
|
private handleKeydown = (event: KeyboardEvent) => {
|
||||||
console.log(event)
|
console.log(event);
|
||||||
//* Navigation
|
//* Navigation
|
||||||
if (['ArrowLeft', 'ArrowRight', 'ArrowUp', 'ArrowDown'].includes(event.key)) {
|
if (
|
||||||
event.preventDefault()
|
["ArrowLeft", "ArrowRight", "ArrowUp", "ArrowDown"].includes(event.key)
|
||||||
this.root.selection.selectedRange = null
|
) {
|
||||||
|
event.preventDefault();
|
||||||
|
this.root.selection.selectedRange = null;
|
||||||
switch (event.key) {
|
switch (event.key) {
|
||||||
case 'ArrowLeft': {
|
case "ArrowLeft": {
|
||||||
if (this.root.selection.selectedCell && this.root.selection.selectedCell.column > 0) {
|
if (
|
||||||
console.log('tick')
|
this.root.selection.selectedCell &&
|
||||||
this.root.selection.selectedCell.column -= 1
|
this.root.selection.selectedCell.column > 0
|
||||||
this.root.renderSheet()
|
) {
|
||||||
|
console.log("tick");
|
||||||
|
this.root.selection.selectedCell.column -= 1;
|
||||||
|
this.root.renderSheet();
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
case 'ArrowRight': {
|
case "ArrowRight": {
|
||||||
if (this.root.selection.selectedCell && this.root.selection.selectedCell.column < this.root.config.columns.length - 1) {
|
if (
|
||||||
this.root.selection.selectedCell.column += 1
|
this.root.selection.selectedCell &&
|
||||||
this.root.renderSheet()
|
this.root.selection.selectedCell.column <
|
||||||
|
this.root.config.columns.length - 1
|
||||||
|
) {
|
||||||
|
this.root.selection.selectedCell.column += 1;
|
||||||
|
this.root.renderSheet();
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
case 'ArrowUp': {
|
case "ArrowUp": {
|
||||||
if (this.root.selection.selectedCell && this.root.selection.selectedCell.row > 0) {
|
if (
|
||||||
this.root.selection.selectedCell.row -= 1
|
this.root.selection.selectedCell &&
|
||||||
this.root.renderSheet()
|
this.root.selection.selectedCell.row > 0
|
||||||
|
) {
|
||||||
|
this.root.selection.selectedCell.row -= 1;
|
||||||
|
this.root.renderSheet();
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
case 'ArrowDown': {
|
case "ArrowDown": {
|
||||||
if (this.root.selection.selectedCell && this.root.selection.selectedCell.row < this.root.config.rows.length - 1) {
|
if (
|
||||||
this.root.selection.selectedCell.row += 1
|
this.root.selection.selectedCell &&
|
||||||
this.root.renderSheet()
|
this.root.selection.selectedCell.row <
|
||||||
|
this.root.config.rows.length - 1
|
||||||
|
) {
|
||||||
|
this.root.selection.selectedCell.row += 1;
|
||||||
|
this.root.renderSheet();
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
const keysRegex = /^([a-z]|[а-я])$/
|
const keysRegex = /^([a-z]|[а-я])$/;
|
||||||
if (!event.metaKey && !event.ctrlKey) { //* Prevent handle shortcutrs
|
if (!event.metaKey && !event.ctrlKey) {
|
||||||
const isPressedLetterKey = keysRegex.test(event.key.toLowerCase())
|
//* Prevent handle shortcutrs
|
||||||
|
const isPressedLetterKey = keysRegex.test(event.key.toLowerCase());
|
||||||
|
|
||||||
if (event.key === 'F2' || isPressedLetterKey) { //* English and Russian keyboard. Or F2 button
|
if (event.key === "F2" || isPressedLetterKey) {
|
||||||
event.preventDefault()
|
//* English and Russian keyboard. Or F2 button
|
||||||
|
event.preventDefault();
|
||||||
if (!this.root.selection.selectedCell) return;
|
if (!this.root.selection.selectedCell) return;
|
||||||
|
|
||||||
this.root.showEditor(this.root.selection.selectedCell, isPressedLetterKey ? event.key : undefined)
|
this.root.showEditor(
|
||||||
|
this.root.selection.selectedCell,
|
||||||
|
isPressedLetterKey ? event.key : undefined,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (event.key === 'Delete') {
|
if (event.key === "Delete") {
|
||||||
event.preventDefault()
|
event.preventDefault();
|
||||||
this.root.deleteSelectedCellsValues()
|
this.root.deleteSelectedCellsValues();
|
||||||
this.root.renderSheet()
|
this.root.renderSheet();
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
};
|
||||||
|
|
||||||
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";
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -1,37 +1,36 @@
|
||||||
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')
|
|
||||||
if (!ctx) throw new Error('Enable hardware acceleration')
|
|
||||||
this.ctx = ctx
|
|
||||||
|
|
||||||
|
const ctx = this.element.getContext("2d");
|
||||||
|
if (!ctx) throw new Error("Enable hardware acceleration");
|
||||||
|
this.ctx = ctx;
|
||||||
}
|
}
|
||||||
|
|
||||||
getCellByCoords(x: number, y: number): Position {
|
getCellByCoords(x: number, y: number): Position {
|
||||||
let row = 0;
|
let row = 0;
|
||||||
let height = 0
|
let height = 0;
|
||||||
while (height <= y) {
|
while (height <= y) {
|
||||||
height += this.root.config.rows[row].height
|
height += this.root.config.rows[row].height;
|
||||||
if (height >= y) break;
|
if (height >= y) break;
|
||||||
row++;
|
row++;
|
||||||
}
|
}
|
||||||
|
|
@ -39,33 +38,32 @@ export class Sheet {
|
||||||
let col = 0;
|
let col = 0;
|
||||||
let width = 0;
|
let width = 0;
|
||||||
while (width <= x) {
|
while (width <= x) {
|
||||||
width += this.root.config.columns[col].width
|
width += this.root.config.columns[col].width;
|
||||||
if (width >= x) break;
|
if (width >= x) break;
|
||||||
col++;
|
col++;
|
||||||
}
|
}
|
||||||
|
|
||||||
return new Position(row, col)
|
return new Position(row, col);
|
||||||
}
|
}
|
||||||
|
|
||||||
renderCell(position: Position) {
|
renderCell(position: Position) {
|
||||||
const { column, row } = position
|
const { column, row } = position;
|
||||||
this.root.data[row][column].render(this.root)
|
this.root.data[row][column].render(this.root);
|
||||||
}
|
}
|
||||||
|
|
||||||
renderSheet() {
|
renderSheet() {
|
||||||
const firstRowIdx = this.root.viewport.firstRow
|
const firstRowIdx = this.root.viewport.firstRow;
|
||||||
const lastColIdx = this.root.viewport.lastCol + 3
|
const lastColIdx = this.root.viewport.lastCol + 3;
|
||||||
const lastRowIdx = this.root.viewport.lastRow + 3
|
const lastRowIdx = this.root.viewport.lastRow + 3;
|
||||||
const firstColIdx = this.root.viewport.firstCol
|
const firstColIdx = this.root.viewport.firstCol;
|
||||||
|
|
||||||
for (let row = firstRowIdx; row <= lastRowIdx; row++) {
|
for (let row = firstRowIdx; row <= lastRowIdx; row++) {
|
||||||
for (let col = firstColIdx; col <= lastColIdx; col++) {
|
for (let col = firstColIdx; col <= lastColIdx; col++) {
|
||||||
if (!this.root.config.columns[col] || !this.root.config.rows[row]) break; //* Prevent read undefined
|
if (!this.root.config.columns[col] || !this.root.config.rows[row])
|
||||||
|
break; //* Prevent read undefined
|
||||||
|
|
||||||
this.renderCell({ column: col, row })
|
this.renderCell({ column: col, row });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
@ -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";
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -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;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
47
src/index.ts
47
src/index.ts
|
|
@ -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",
|
||||||
})
|
},
|
||||||
|
);
|
||||||
|
|
|
||||||
340
src/main.ts
340
src/main.ts
|
|
@ -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,101 +34,107 @@ 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,
|
||||||
|
) {
|
||||||
|
const data = createSampleData(40, 40);
|
||||||
|
const config = this.makeConfigFromData(
|
||||||
|
data,
|
||||||
|
props?.view ?? { height: 600, width: 800 },
|
||||||
|
);
|
||||||
if (props?.view) {
|
if (props?.view) {
|
||||||
config.view = props.view
|
config.view = props.view;
|
||||||
}
|
}
|
||||||
|
|
||||||
this.config = new Config(config)
|
this.config = new Config(config);
|
||||||
this.sheet = new Sheet(this)
|
this.sheet = new Sheet(this);
|
||||||
|
|
||||||
this.table = new Table(this)
|
this.table = new Table(this);
|
||||||
this.scroller = new Scroller(this)
|
this.scroller = new Scroller(this);
|
||||||
this.toolbar = new Toolbar(this)
|
this.toolbar = new Toolbar(this);
|
||||||
this.header = new Header(this)
|
this.header = new Header(this);
|
||||||
this.editor = new Editor(this)
|
this.editor = new Editor(this);
|
||||||
this.cache = this.getInitialCache()
|
this.cache = this.getInitialCache();
|
||||||
this.viewport = new Viewport(this, this.scroller.getViewportBoundlingRect())
|
this.viewport = new Viewport(
|
||||||
this.selection = new Selection()
|
this,
|
||||||
|
this.scroller.getViewportBoundlingRect(),
|
||||||
|
);
|
||||||
|
this.selection = new Selection();
|
||||||
|
|
||||||
|
this.data = data;
|
||||||
this.data = data
|
this.styles = new Styles();
|
||||||
this.styles = new Styles()
|
this.buildComponent();
|
||||||
this.buildComponent()
|
this.appendTableToTarget(target);
|
||||||
this.appendTableToTarget(target)
|
this.renderSheet();
|
||||||
this.renderSheet()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private getInitialCache(): Cache {
|
private getInitialCache(): Cache {
|
||||||
const cachedCols: CachedColumn[] = []
|
const cachedCols: CachedColumn[] = [];
|
||||||
let currentWidth = 0
|
let currentWidth = 0;
|
||||||
for (let i = 0; i <= this.config.columns.length - 1; i++) {
|
for (let i = 0; i <= this.config.columns.length - 1; i++) {
|
||||||
const col = this.config.columns[i]
|
const col = this.config.columns[i];
|
||||||
currentWidth += col.width
|
currentWidth += col.width;
|
||||||
const cacheCol = new CachedColumn({
|
const cacheCol = new CachedColumn({
|
||||||
xPos: currentWidth,
|
xPos: currentWidth,
|
||||||
colIdx: i
|
colIdx: i,
|
||||||
})
|
});
|
||||||
cachedCols.push(cacheCol)
|
cachedCols.push(cacheCol);
|
||||||
}
|
}
|
||||||
|
|
||||||
const cachedRows: CachedRow[] = []
|
const cachedRows: CachedRow[] = [];
|
||||||
let currentHeight = 0
|
let currentHeight = 0;
|
||||||
for (let i = 0; i <= this.config.rows.length - 1; i++) {
|
for (let i = 0; i <= this.config.rows.length - 1; i++) {
|
||||||
const row = this.config.rows[i]
|
const row = this.config.rows[i];
|
||||||
currentHeight += row.height
|
currentHeight += row.height;
|
||||||
const cacheRow = new CachedRow({
|
const cacheRow = new CachedRow({
|
||||||
yPos: currentHeight,
|
yPos: currentHeight,
|
||||||
rowIdx: i
|
rowIdx: i,
|
||||||
})
|
});
|
||||||
cachedRows.push(cacheRow)
|
cachedRows.push(cacheRow);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
const cache = new Cache({
|
const cache = new Cache({
|
||||||
columns: cachedCols,
|
columns: cachedCols,
|
||||||
rows: cachedRows
|
rows: cachedRows,
|
||||||
})
|
});
|
||||||
|
|
||||||
console.log("CACHE: ", cache)
|
console.log("CACHE: ", cache);
|
||||||
console.log("CONFIG: ", this.config)
|
console.log("CONFIG: ", this.config);
|
||||||
return cache
|
return cache;
|
||||||
}
|
}
|
||||||
|
|
||||||
private buildComponent(): void {
|
private buildComponent(): void {
|
||||||
|
const content = document.createElement("div"); //* Abstract
|
||||||
|
content.appendChild(this.header.element);
|
||||||
|
content.appendChild(this.sheet.element);
|
||||||
|
|
||||||
const content = document.createElement('div') //* Abstract
|
content.classList.add(CSS_PREFIX + "content");
|
||||||
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.toolbar.element)
|
this.table.element.appendChild(this.scroller.element);
|
||||||
this.table.element.appendChild(content)
|
this.table.element.append(this.editor.element);
|
||||||
this.table.element.appendChild(this.scroller.element)
|
|
||||||
this.table.element.append(this.editor.element)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**Destroy spreadsheet DOM element.
|
/**Destroy spreadsheet DOM element.
|
||||||
|
|
@ -130,17 +142,20 @@ export default class Spreadsheet {
|
||||||
* May be usefull when need to rerender component.
|
* May be usefull when need to rerender component.
|
||||||
*/
|
*/
|
||||||
public destroy() {
|
public destroy() {
|
||||||
this.table.element.remove()
|
this.table.element.remove();
|
||||||
}
|
}
|
||||||
|
|
||||||
private appendTableToTarget(target: string | HTMLElement) {
|
private appendTableToTarget(target: string | HTMLElement) {
|
||||||
if (typeof target === 'string') {
|
if (typeof target === "string") {
|
||||||
const element = document.querySelector(target)
|
const element = document.querySelector(target);
|
||||||
if (!element) throw new Error(`Element with selector ${target} is not finded in DOM.\n Make sure it exists.`)
|
if (!element)
|
||||||
element?.appendChild(this.table.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) {
|
if (target instanceof HTMLElement) {
|
||||||
target.append(this.table.element)
|
target.append(this.table.element);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -149,179 +164,192 @@ export default class Spreadsheet {
|
||||||
* Abble to draw on canvas with default CanvasAPI methods
|
* Abble to draw on canvas with default CanvasAPI methods
|
||||||
*/
|
*/
|
||||||
get ctx() {
|
get ctx() {
|
||||||
return this.sheet.ctx
|
return this.sheet.ctx;
|
||||||
}
|
}
|
||||||
|
|
||||||
get viewProps() {
|
get viewProps() {
|
||||||
return this.config.view
|
return this.config.view;
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Focusing on interactive part of spreadsheet */
|
/** Focusing on interactive part of spreadsheet */
|
||||||
focusTable() {
|
focusTable() {
|
||||||
this.scroller.element.focus()
|
this.scroller.element.focus();
|
||||||
}
|
}
|
||||||
|
|
||||||
getCellByCoords(x: number, y: number) {
|
getCellByCoords(x: number, y: number) {
|
||||||
return this.sheet.getCellByCoords(x, y)
|
return this.sheet.getCellByCoords(x, y);
|
||||||
}
|
}
|
||||||
|
|
||||||
getCell(position: Position): Cell {
|
getCell(position: Position): Cell {
|
||||||
const { column, row } = position
|
const { column, row } = position;
|
||||||
return this.data[row][column]
|
return this.data[row][column];
|
||||||
}
|
}
|
||||||
|
|
||||||
changeCellValues(position: Position, values: Partial<Omit<CellConstructorProps, 'position'>>) {
|
changeCellValues(
|
||||||
const { column, row } = position
|
position: Position,
|
||||||
|
values: Partial<Omit<CellConstructorProps, "position">>,
|
||||||
|
) {
|
||||||
|
const { column, row } = position;
|
||||||
|
|
||||||
this.data[row][column].changeValues(values)
|
this.data[row][column].changeValues(values);
|
||||||
this.renderCell(row, column)
|
this.renderCell(row, column);
|
||||||
}
|
}
|
||||||
|
|
||||||
changeCellStyles(position: Position, styles: CellStyles) {
|
changeCellStyles(position: Position, styles: CellStyles) {
|
||||||
const { column, row } = position
|
const { column, row } = position;
|
||||||
this.data[row][column].changeStyles(styles)
|
this.data[row][column].changeStyles(styles);
|
||||||
this.renderCell(row, column)
|
this.renderCell(row, column);
|
||||||
}
|
}
|
||||||
|
|
||||||
applyActionToRange(range: RangeSelectionType, callback: (cell: Cell) => any): void {
|
applyActionToRange(
|
||||||
const fromRow = Math.min(range.from.row, range.to.row)
|
range: RangeSelectionType,
|
||||||
const toRow = Math.max(range.from.row, range.to.row)
|
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 fromCol = Math.min(range.from.column, range.to.column);
|
||||||
const toCol = Math.max(range.from.column, range.to.column)
|
const toCol = Math.max(range.from.column, range.to.column);
|
||||||
|
|
||||||
for (let row = fromRow; row <= toRow; row++) {
|
for (let row = fromRow; row <= toRow; row++) {
|
||||||
for (let col = fromCol; col <= toCol; col++) {
|
for (let col = fromCol; col <= toCol; col++) {
|
||||||
const cell = this.data[row][col]
|
const cell = this.data[row][col];
|
||||||
callback(cell)
|
callback(cell);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
deleteSelectedCellsValues() {
|
deleteSelectedCellsValues() {
|
||||||
if (this.selection.selectedRange !== null) {
|
if (this.selection.selectedRange !== null) {
|
||||||
|
this.applyActionToRange(this.selection.selectedRange, (cell) => {
|
||||||
this.applyActionToRange(this.selection.selectedRange, cell => {
|
|
||||||
this.changeCellValues(cell.position, {
|
this.changeCellValues(cell.position, {
|
||||||
displayValue: '',
|
displayValue: "",
|
||||||
resultValue: '',
|
resultValue: "",
|
||||||
value: ''
|
value: "",
|
||||||
})
|
});
|
||||||
})
|
});
|
||||||
|
|
||||||
} else {
|
} else {
|
||||||
if (!this.selection.selectedCell) return;
|
if (!this.selection.selectedCell) return;
|
||||||
this.changeCellValues(this.selection.selectedCell, {
|
this.changeCellValues(this.selection.selectedCell, {
|
||||||
displayValue: '',
|
displayValue: "",
|
||||||
resultValue: '',
|
resultValue: "",
|
||||||
value: ''
|
value: "",
|
||||||
})
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
showEditor(position: Position, initialString?: string) {
|
showEditor(position: Position, initialString?: string) {
|
||||||
this.editor.show(position, initialString)
|
this.editor.show(position, initialString);
|
||||||
}
|
}
|
||||||
|
|
||||||
renderSheet() {
|
renderSheet() {
|
||||||
this.sheet.renderSheet()
|
this.sheet.renderSheet();
|
||||||
}
|
}
|
||||||
|
|
||||||
renderCell(row: number, col: number) {
|
renderCell(row: number, col: number) {
|
||||||
this.data[row][col].render(this)
|
this.data[row][col].render(this);
|
||||||
}
|
}
|
||||||
|
|
||||||
public loadData(data: Cell[][] | SerializableCell[][]): Spreadsheet {
|
public loadData(data: Cell[][] | SerializableCell[][]): Spreadsheet {
|
||||||
const rowsLength = data.length
|
const rowsLength = data.length;
|
||||||
const colsLength = data[0] ? this.data[0].length : 0
|
const colsLength = data[0] ? this.data[0].length : 0;
|
||||||
this.data = []
|
this.data = [];
|
||||||
|
|
||||||
const formattedData: Cell[][] = []
|
const formattedData: Cell[][] = [];
|
||||||
|
|
||||||
for (let row = 0; row < rowsLength; row++) {
|
for (let row = 0; row < rowsLength; row++) {
|
||||||
const innerRow: Cell[] = []
|
const innerRow: Cell[] = [];
|
||||||
for (let col = 0; col < colsLength; col++) {
|
for (let col = 0; col < colsLength; col++) {
|
||||||
const cell = data[row][col]
|
const cell = data[row][col];
|
||||||
innerRow.push(new Cell({
|
innerRow.push(
|
||||||
|
new Cell({
|
||||||
displayValue: cell.displayValue,
|
displayValue: cell.displayValue,
|
||||||
position: cell.position,
|
position: cell.position,
|
||||||
resultValue: cell.resultValue,
|
resultValue: cell.resultValue,
|
||||||
value: cell.value,
|
value: cell.value,
|
||||||
style: cell.style
|
style: cell.style,
|
||||||
}))
|
}),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
formattedData.push(innerRow)
|
formattedData.push(innerRow);
|
||||||
}
|
}
|
||||||
|
|
||||||
this.data = formattedData
|
this.data = formattedData;
|
||||||
|
|
||||||
this.selection.selectedCell = null
|
this.selection.selectedCell = null;
|
||||||
this.selection.selectedRange = null
|
this.selection.selectedRange = null;
|
||||||
this.config = this.makeConfigFromData(formattedData, this.config.view)
|
this.config = this.makeConfigFromData(formattedData, this.config.view);
|
||||||
this.cache = this.getInitialCache()
|
this.cache = this.getInitialCache();
|
||||||
this.scroller.updateScrollerSize()
|
this.scroller.updateScrollerSize();
|
||||||
this.viewport = new Viewport(this, this.scroller.getViewportBoundlingRect())
|
this.viewport = new Viewport(
|
||||||
this.renderSheet()
|
this,
|
||||||
|
this.scroller.getViewportBoundlingRect(),
|
||||||
|
);
|
||||||
|
this.renderSheet();
|
||||||
|
|
||||||
return this
|
return this;
|
||||||
}
|
}
|
||||||
|
|
||||||
private makeConfigFromData(data: Cell[][], view: ViewProperties): Config {
|
private makeConfigFromData(data: Cell[][], view: ViewProperties): Config {
|
||||||
const lastRowIdx = data.length - 1
|
const lastRowIdx = data.length - 1;
|
||||||
const lastColIdx = data[0] ? data[0].length : 0
|
const lastColIdx = data[0] ? data[0].length : 0;
|
||||||
|
|
||||||
const rows: Row[] = []
|
const rows: Row[] = [];
|
||||||
for (let row = 0; row < lastRowIdx; row++) {
|
for (let row = 0; row < lastRowIdx; row++) {
|
||||||
rows.push(new Row({
|
rows.push(
|
||||||
|
new Row({
|
||||||
height: 40,
|
height: 40,
|
||||||
title: String(row)
|
title: String(row),
|
||||||
}))
|
}),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
const columns: Column[] = []
|
const columns: Column[] = [];
|
||||||
|
|
||||||
for (let col = 0; col < lastColIdx; col++) {
|
for (let col = 0; col < lastColIdx; col++) {
|
||||||
columns.push(new Column({
|
columns.push(
|
||||||
|
new Column({
|
||||||
width: 150,
|
width: 150,
|
||||||
title: String(col)
|
title: String(col),
|
||||||
}))
|
}),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
const config = new Config({
|
const config = new Config({
|
||||||
view,
|
view,
|
||||||
rows,
|
rows,
|
||||||
columns
|
columns,
|
||||||
})
|
});
|
||||||
|
|
||||||
return config
|
return config;
|
||||||
}
|
}
|
||||||
|
|
||||||
public serializeData(): SerializableCell[][] {
|
public serializeData(): SerializableCell[][] {
|
||||||
const rowsLength = this.data.length
|
const rowsLength = this.data.length;
|
||||||
const colsLength = this.data[0] ? this.data[0].length : 0
|
const colsLength = this.data[0] ? this.data[0].length : 0;
|
||||||
|
|
||||||
const cellsArray: SerializableCell[][] = []
|
const cellsArray: SerializableCell[][] = [];
|
||||||
|
|
||||||
for (let row = 0; row < rowsLength; row++) {
|
for (let row = 0; row < rowsLength; row++) {
|
||||||
const innerRow: SerializableCell[] = []
|
const innerRow: SerializableCell[] = [];
|
||||||
for (let col = 0; col < colsLength; col++) {
|
for (let col = 0; col < colsLength; col++) {
|
||||||
innerRow.push(this.data[row][col].getSerializableCell())
|
innerRow.push(this.data[row][col].getSerializableCell());
|
||||||
}
|
}
|
||||||
cellsArray.push(innerRow)
|
cellsArray.push(innerRow);
|
||||||
}
|
}
|
||||||
|
|
||||||
return cellsArray
|
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";
|
||||||
|
|
|
||||||
|
|
@ -1,50 +1,51 @@
|
||||||
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) {
|
||||||
|
//* Intersection detect
|
||||||
rowIdx = i;
|
rowIdx = i;
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
@ -52,16 +53,15 @@ export class Cache {
|
||||||
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) {
|
||||||
|
//* Intersection detect
|
||||||
colIdx = i;
|
colIdx = i;
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return colIdx;
|
return colIdx;
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
@ -1,80 +1,80 @@
|
||||||
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 {
|
||||||
|
|
@ -83,52 +83,66 @@ export class Cell {
|
||||||
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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -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;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -1,9 +1,9 @@
|
||||||
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 = {
|
||||||
|
|
@ -13,27 +13,26 @@ export type ConfigProperties = {
|
||||||
*
|
*
|
||||||
* '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;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -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.x = this.getXCoord(cellPosition.column, config)
|
this.y = this.getYCoord(cellPosition.row, config);
|
||||||
this.y = this.getYCoord(cellPosition.row, config)
|
this.width = config.columns[cellPosition.column].width;
|
||||||
this.width = config.columns[cellPosition.column].width
|
this.height = config.rows[cellPosition.row].height;
|
||||||
this.height = config.rows[cellPosition.row].height
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private getXCoord(column: number, config: Config): number {
|
private getXCoord(column: number, config: Config): number {
|
||||||
let x = 0;
|
let x = 0;
|
||||||
|
|
||||||
for (let i = 0; i < column; i++) {
|
for (let i = 0; i < column; i++) {
|
||||||
x += config.columns[i].width
|
x += config.columns[i].width;
|
||||||
}
|
}
|
||||||
|
|
||||||
return x
|
return x;
|
||||||
}
|
}
|
||||||
|
|
||||||
private getYCoord(row: number, config: Config): number {
|
private getYCoord(row: number, config: Config): number {
|
||||||
let y = 0
|
let y = 0;
|
||||||
for (let i = 0; i < row; i++) {
|
for (let i = 0; i < row; i++) {
|
||||||
y += config.rows[i].height
|
y += config.rows[i].height;
|
||||||
}
|
}
|
||||||
return y
|
return y;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -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;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -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;
|
||||||
}
|
}
|
||||||
|
|
@ -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();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -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;
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
@ -1,2 +1,2 @@
|
||||||
@import 'global.scss';
|
@import "global.scss";
|
||||||
@import 'spreadsheet.scss';
|
@import "spreadsheet.scss";
|
||||||
|
|
|
||||||
|
|
@ -3,13 +3,17 @@ 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,
|
||||||
|
|
@ -17,36 +21,35 @@ export function createSampleData(rows: number, columns: number, fillCellsByCoord
|
||||||
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)
|
data.push(innerRow);
|
||||||
}
|
}
|
||||||
return data
|
return data;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function createSampleConfig(rows: number, columns: number): Config {
|
export function createSampleConfig(rows: number, columns: number): Config {
|
||||||
|
const rowsArr: Row[] = [];
|
||||||
const rowsArr: Row[] = []
|
|
||||||
for (let i = 0; i <= rows; i++) {
|
for (let i = 0; i <= rows; i++) {
|
||||||
const rowItem = new Row({
|
const rowItem = new Row({
|
||||||
height: 40,
|
height: 40,
|
||||||
title: String(i)
|
title: String(i),
|
||||||
})
|
});
|
||||||
rowsArr.push(rowItem)
|
rowsArr.push(rowItem);
|
||||||
}
|
}
|
||||||
|
|
||||||
const colsArr: Column[] = []
|
const colsArr: Column[] = [];
|
||||||
for (let i = 0; i <= columns; i++) {
|
for (let i = 0; i <= columns; i++) {
|
||||||
const colItem = new Column({
|
const colItem = new Column({
|
||||||
title: String(i),
|
title: String(i),
|
||||||
width: 150
|
width: 150,
|
||||||
})
|
});
|
||||||
colsArr.push(colItem)
|
colsArr.push(colItem);
|
||||||
}
|
}
|
||||||
|
|
||||||
const config = new Config({
|
const config = new Config({
|
||||||
|
|
@ -54,21 +57,24 @@ export function createSampleConfig(rows: number, columns: number): Config {
|
||||||
rows: rowsArr,
|
rows: rowsArr,
|
||||||
view: {
|
view: {
|
||||||
height: 600,
|
height: 600,
|
||||||
width: 800
|
width: 800,
|
||||||
}
|
},
|
||||||
})
|
});
|
||||||
|
|
||||||
return config
|
return config;
|
||||||
}
|
}
|
||||||
|
|
||||||
export type SpreadsheetConfigAndDataReturnType = {
|
export type SpreadsheetConfigAndDataReturnType = {
|
||||||
config: Config,
|
config: Config;
|
||||||
data: Cell[][]
|
data: Cell[][];
|
||||||
}
|
};
|
||||||
|
|
||||||
export function makeSpreadsheetConfigAndData(rows: number, columns: number): SpreadsheetConfigAndDataReturnType {
|
export function makeSpreadsheetConfigAndData(
|
||||||
const data = createSampleData(rows, columns)
|
rows: number,
|
||||||
const config = createSampleConfig(rows, columns)
|
columns: number,
|
||||||
|
): SpreadsheetConfigAndDataReturnType {
|
||||||
|
const data = createSampleData(rows, columns);
|
||||||
|
const config = createSampleConfig(rows, columns);
|
||||||
|
|
||||||
return { data, config }
|
return { data, config };
|
||||||
}
|
}
|
||||||
|
|
@ -1,11 +1,11 @@
|
||||||
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: {
|
||||||
|
|
@ -26,14 +26,14 @@ export default defineConfig({
|
||||||
external: ["./src/index.ts"],
|
external: ["./src/index.ts"],
|
||||||
plugins: [
|
plugins: [
|
||||||
typescriptPaths({
|
typescriptPaths({
|
||||||
preserveExtensions: true
|
preserveExtensions: true,
|
||||||
}),
|
}),
|
||||||
typescript({
|
typescript({
|
||||||
sourceMap: false,
|
sourceMap: false,
|
||||||
declaration: true,
|
declaration: true,
|
||||||
outDir: 'dist'
|
outDir: "dist",
|
||||||
})
|
}),
|
||||||
]
|
],
|
||||||
},
|
},
|
||||||
}
|
},
|
||||||
})
|
});
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue