Compare commits
4 Commits
main
...
implement-
| Author | SHA1 | Date |
|---|---|---|
|
|
75e929f0d5 | |
|
|
c4c8ff7a8f | |
|
|
c68a88c907 | |
|
|
ad69c4e01c |
|
|
@ -41,4 +41,4 @@ function loadData() {
|
|||
- Selected cell depends cells highlight
|
||||
- Async formulas support
|
||||
- Mutlisheets (?)
|
||||
-
|
||||
- Copy & Paste support
|
||||
|
|
|
|||
|
|
@ -6,9 +6,8 @@
|
|||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>Spreadsheet example</title>
|
||||
</head>
|
||||
<body>
|
||||
<body style="padding: 2rem;">
|
||||
<div id="spreadsheet"></div>
|
||||
<div id="spreadsheet_2"></div>
|
||||
<button id="save_button">Save sheet</button>
|
||||
<button id="load_button">Load sheet</button>
|
||||
<script type="module" src="/src/index.ts"></script>
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
{
|
||||
"name": "modern_spreadsheet",
|
||||
"private": false,
|
||||
"version": "0.0.25",
|
||||
"version": "0.0.27",
|
||||
"exports": {
|
||||
".": {
|
||||
"import": "./dist/main.js",
|
||||
|
|
|
|||
|
|
@ -0,0 +1,118 @@
|
|||
import Spreadsheet, { RenderBox } from "../main"
|
||||
|
||||
export class ColumnsBar {
|
||||
public element: HTMLCanvasElement
|
||||
private root: Spreadsheet
|
||||
public height: number = 32
|
||||
public width: number
|
||||
private resizerWidth = 1
|
||||
ctx: CanvasRenderingContext2D
|
||||
|
||||
constructor(root: Spreadsheet) {
|
||||
this.root = root
|
||||
this.element = this.createElement()
|
||||
const ctx = this.element.getContext('2d')
|
||||
if (!ctx) throw new Error("Enable hardware acceleration");
|
||||
this.ctx = ctx
|
||||
|
||||
this.width = this.root.viewProps.width
|
||||
}
|
||||
|
||||
private createElement(): HTMLCanvasElement {
|
||||
const element = document.createElement('canvas')
|
||||
element.style.position = 'absolute'
|
||||
element.style.height = this.height + 'px'
|
||||
element.style.width = this.root.viewProps.width + 'px'
|
||||
element.style.display = 'block'
|
||||
element.style.borderLeft = '1px solid black'
|
||||
// element.style.boxSizing = 'border-box'
|
||||
|
||||
|
||||
element.width = this.root.viewProps.width
|
||||
element.height = this.height
|
||||
return element
|
||||
}
|
||||
|
||||
public setElementPosition(top: number, left: number) {
|
||||
this.element.style.top = top + 'px'
|
||||
this.element.style.left = left + 'px'
|
||||
}
|
||||
|
||||
private isColumnSelected(column: number): boolean {
|
||||
const { selectedCell, selectedRange } = this.root.selection
|
||||
if (selectedCell && selectedCell.column === column) return true
|
||||
if (selectedRange) {
|
||||
const inRange =
|
||||
column >= Math.min(selectedRange.from.column, selectedRange.to.column) &&
|
||||
column <= Math.max(selectedRange.from.column, selectedRange.to.column)
|
||||
|
||||
return inRange
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// private getYCoordWithOffset(renderBox: RenderBox): number {
|
||||
// const {y} = renderBox
|
||||
|
||||
// return y + this.root.toolbarHeight
|
||||
// }
|
||||
|
||||
// private getXCoordWithOffset(renderBox: RenderBox): number {
|
||||
// const {x} = renderBox
|
||||
|
||||
// return x
|
||||
// }
|
||||
|
||||
private renderText(column: number, renderBox: RenderBox) {
|
||||
const { width, x } = renderBox
|
||||
|
||||
this.ctx.fillStyle = 'black'
|
||||
this.ctx.textAlign = 'center'
|
||||
this.ctx.textBaseline = 'middle'
|
||||
this.ctx.font = '16px Arial'
|
||||
this.ctx.fillText(this.root.config.columns[column].title, x + (width / 2) - this.root.viewport.left, 0 + this.height / 2)
|
||||
}
|
||||
|
||||
private renderRect(column: number, renderBox: RenderBox) {
|
||||
const { width, x } = renderBox
|
||||
|
||||
const isColSelected = this.isColumnSelected(column)
|
||||
|
||||
this.ctx.fillStyle = isColSelected ? this.root.styles.cells.selectedBackground : 'white'
|
||||
this.ctx.strokeStyle = 'black'
|
||||
this.ctx.lineWidth = 1
|
||||
|
||||
const specialX = x - this.root.viewport.left
|
||||
|
||||
this.ctx.fillRect(specialX - 1 , 0, width, this.height)
|
||||
this.ctx.strokeRect(specialX - 1, 0, width, this.height)
|
||||
}
|
||||
|
||||
private renderSingleColumn(column: number) {
|
||||
const renderBox = new RenderBox(this.root.config, {
|
||||
row: 0,
|
||||
column: column
|
||||
})
|
||||
|
||||
this.renderRect(column, renderBox)
|
||||
this.renderText(column, renderBox)
|
||||
}
|
||||
|
||||
public renderBar() {
|
||||
const lastColIdx = this.root.viewport.lastCol + 3;
|
||||
const firstColIdx = this.root.viewport.firstCol;
|
||||
|
||||
this.ctx.beginPath();
|
||||
this.ctx.strokeStyle = 'black';
|
||||
this.ctx.lineWidth = 1;
|
||||
this.ctx.moveTo(0, 0);
|
||||
this.ctx.lineTo(0, this.height);
|
||||
this.ctx.closePath();
|
||||
this.ctx.stroke();
|
||||
|
||||
for (let col = firstColIdx; col <= lastColIdx; col++) {
|
||||
if(!this.root.config.columns[col]) break;
|
||||
this.renderSingleColumn(col)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -28,8 +28,8 @@ export class Editor {
|
|||
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.top = y - this.root.viewport.top + this.root.columnsBarHeight + "px";
|
||||
this.element.style.left = x - this.root.viewport.left + this.root.rowsBarWidth + "px";
|
||||
this.element.style.width = width + "px";
|
||||
this.element.style.height = height + "px";
|
||||
this.element.style.display = "block";
|
||||
|
|
|
|||
|
|
@ -1,12 +0,0 @@
|
|||
import Spreadsheet from "../main";
|
||||
|
||||
export class Header {
|
||||
element: HTMLHeadElement;
|
||||
root: Spreadsheet;
|
||||
constructor(root: Spreadsheet) {
|
||||
this.root = root;
|
||||
const headerElement = document.createElement("header");
|
||||
headerElement.classList.add();
|
||||
this.element = headerElement;
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,109 @@
|
|||
import Spreadsheet, { RenderBox } from "../main";
|
||||
|
||||
export class RowsBar {
|
||||
element: HTMLCanvasElement
|
||||
ctx: CanvasRenderingContext2D
|
||||
root: Spreadsheet
|
||||
width: number = 30
|
||||
height: number
|
||||
resizerHeight = 1
|
||||
constructor(root: Spreadsheet) {
|
||||
this.root = root
|
||||
this.element = this.createElement()
|
||||
const ctx = this.element.getContext('2d')
|
||||
if (!ctx) throw new Error("Enable hardware acceleration");
|
||||
this.ctx = ctx
|
||||
|
||||
this.height = this.root.viewProps.height
|
||||
}
|
||||
|
||||
private createElement() {
|
||||
const element = document.createElement('canvas')
|
||||
|
||||
element.style.position = 'absolute'
|
||||
element.style.height = this.root.viewProps.height + 'px'
|
||||
element.style.width = this.width + 'px'
|
||||
element.style.display = 'block'
|
||||
element.style.borderTop = '1px solid black'
|
||||
// element.style.boxSizing = 'border-box'
|
||||
|
||||
|
||||
element.width = this.width
|
||||
element.height = this.root.viewProps.height
|
||||
return element
|
||||
}
|
||||
|
||||
public setElementPosition(top: number, left: number) {
|
||||
this.element.style.top = top + 'px'
|
||||
this.element.style.left = left + 'px'
|
||||
}
|
||||
|
||||
private isRowSelected(row: number): boolean {
|
||||
const { selectedCell, selectedRange } = this.root.selection
|
||||
if (selectedCell && selectedCell.row === row) return true
|
||||
if (selectedRange) {
|
||||
const inRange =
|
||||
row >= Math.min(selectedRange.from.row, selectedRange.to.row) &&
|
||||
row <= Math.max(selectedRange.from.row, selectedRange.to.row)
|
||||
|
||||
return inRange
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
|
||||
private renderText(row: number, renderBox: RenderBox) {
|
||||
const { y, height } = renderBox
|
||||
|
||||
this.ctx.fillStyle = 'black'
|
||||
this.ctx.textAlign = 'center'
|
||||
this.ctx.textBaseline = 'middle'
|
||||
this.ctx.font = '16px Arial'
|
||||
this.ctx.fillText(this.root.config.rows[row].title, this.width / 2, y - this.root.viewport.top + height / 2)
|
||||
}
|
||||
|
||||
private renderRect(column: number, renderBox: RenderBox) {
|
||||
const { y, height } = renderBox
|
||||
|
||||
|
||||
const isRowSeleted = this.isRowSelected(column)
|
||||
|
||||
this.ctx.fillStyle = isRowSeleted ? this.root.styles.cells.selectedBackground : 'white'
|
||||
this.ctx.strokeStyle = 'black'
|
||||
this.ctx.lineWidth = this.resizerHeight
|
||||
|
||||
const specialY = y - this.root.viewport.top
|
||||
|
||||
this.ctx.fillRect(0, specialY - 1, this.width, height )
|
||||
this.ctx.strokeRect(0, specialY - 1, this.width, height )
|
||||
}
|
||||
|
||||
private renderSingleRow(row: number) {
|
||||
const renderBox = new RenderBox(this.root.config, {
|
||||
column: 0,
|
||||
row: row
|
||||
})
|
||||
|
||||
this.renderRect(row, renderBox)
|
||||
this.renderText(row, renderBox)
|
||||
}
|
||||
|
||||
public renderBar() {
|
||||
const lastRowIdx = this.root.viewport.lastRow + 3;
|
||||
const firstRowIdx = this.root.viewport.firstRow;
|
||||
|
||||
this.ctx.beginPath();
|
||||
this.ctx.moveTo(0, 0);
|
||||
this.ctx.strokeStyle = 'black';
|
||||
this.ctx.lineWidth = 16;
|
||||
this.ctx.lineTo(35, 0);
|
||||
this.ctx.closePath();
|
||||
this.ctx.stroke();
|
||||
|
||||
for (let row = firstRowIdx; row <= lastRowIdx; row++) {
|
||||
if(!this.root.config.rows[row]) break;
|
||||
this.renderSingleRow(row)
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -25,6 +25,8 @@ export class Scroller {
|
|||
|
||||
this.element.style.height = this.root.config.view.height + "px";
|
||||
this.element.style.width = this.root.config.view.width + "px";
|
||||
this.element.style.top = this.root.columnsBarHeight + 'px'
|
||||
this.element.style.left = this.root.rowsBarWidth + 'px'
|
||||
this.element.tabIndex = -1;
|
||||
|
||||
this.updateScrollerSize(); //* Init size set
|
||||
|
|
@ -47,6 +49,8 @@ export class Scroller {
|
|||
this.root.selection.selectedRange.to = lastSelectedCell;
|
||||
}
|
||||
this.root.renderSheet();
|
||||
this.root.renderColumnsBar();
|
||||
this.root.renderRowsBar();
|
||||
};
|
||||
|
||||
private handleMouseUp = () => {
|
||||
|
|
@ -55,15 +59,17 @@ export class Scroller {
|
|||
if (this.root.selection.selectedRange) {
|
||||
if (
|
||||
this.root.selection.selectedRange.from.row ===
|
||||
this.root.selection.selectedRange.to.row &&
|
||||
this.root.selection.selectedRange.to.row &&
|
||||
this.root.selection.selectedRange.from.column ===
|
||||
this.root.selection.selectedRange.to.column
|
||||
this.root.selection.selectedRange.to.column
|
||||
) {
|
||||
this.root.selection.selectedRange = null;
|
||||
}
|
||||
}
|
||||
|
||||
this.root.renderSheet();
|
||||
this.root.renderColumnsBar();
|
||||
this.root.renderRowsBar();
|
||||
};
|
||||
|
||||
private handleDoubleClick = (event: MouseEvent) => {
|
||||
|
|
@ -96,7 +102,7 @@ export class Scroller {
|
|||
if (
|
||||
this.root.selection.selectedCell &&
|
||||
this.root.selection.selectedCell.column <
|
||||
this.root.config.columns.length - 1
|
||||
this.root.config.columns.length - 1
|
||||
) {
|
||||
this.root.selection.selectedCell.column += 1;
|
||||
this.root.renderSheet();
|
||||
|
|
@ -117,7 +123,7 @@ export class Scroller {
|
|||
if (
|
||||
this.root.selection.selectedCell &&
|
||||
this.root.selection.selectedCell.row <
|
||||
this.root.config.rows.length - 1
|
||||
this.root.config.rows.length - 1
|
||||
) {
|
||||
this.root.selection.selectedCell.row += 1;
|
||||
this.root.renderSheet();
|
||||
|
|
@ -162,14 +168,18 @@ export class Scroller {
|
|||
this.root.selection.selectedCell = clickedCell;
|
||||
|
||||
this.root.renderSheet();
|
||||
};
|
||||
this.root.renderColumnsBar()
|
||||
|
||||
this.root.renderRowsBar(); };
|
||||
|
||||
private handleScroll = () => {
|
||||
const rect = this.getViewportBoundlingRect();
|
||||
this.root.viewport.updateValues(rect);
|
||||
|
||||
this.root.renderSheet();
|
||||
};
|
||||
this.root.renderColumnsBar()
|
||||
|
||||
this.root.renderRowsBar(); };
|
||||
|
||||
public getViewportBoundlingRect(): ViewportRect {
|
||||
const { scrollTop, scrollLeft } = this.element;
|
||||
|
|
|
|||
|
|
@ -18,6 +18,7 @@ export class Sheet {
|
|||
canvas.width = this.root.config.view.width;
|
||||
canvas.style.width = this.root.config.view.width + "px";
|
||||
canvas.style.height = this.root.config.view.height + "px";
|
||||
canvas.style.left = '0px'
|
||||
|
||||
this.element = canvas;
|
||||
|
||||
|
|
|
|||
|
|
@ -16,7 +16,7 @@ export class Table {
|
|||
|
||||
changeElementSizes(sizes: ViewProperties) {
|
||||
const { height, width } = sizes;
|
||||
this.element.style.width = width + "px";
|
||||
this.element.style.height = height + "px";
|
||||
this.element.style.width = width + this.root.rowsBarWidth + "px";
|
||||
this.element.style.height = height + this.root.columnsBarHeight + "px";
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@ import Spreadsheet, { CSS_PREFIX } from "../main";
|
|||
export class Toolbar {
|
||||
element: HTMLDivElement;
|
||||
root: Spreadsheet;
|
||||
height: number = 0
|
||||
constructor(root: Spreadsheet) {
|
||||
this.root = root;
|
||||
const toolbarElement = document.createElement("div");
|
||||
|
|
|
|||
14
src/index.ts
14
src/index.ts
|
|
@ -6,9 +6,6 @@ const loadButton = document.querySelector("#load_button");
|
|||
if (!saveButton || !loadButton) throw new Error("LOST");
|
||||
|
||||
const sheet = new Spreadsheet("#spreadsheet");
|
||||
const sheet2 = new Spreadsheet("#spreadsheet_2");
|
||||
|
||||
console.log(sheet2);
|
||||
|
||||
function saveDataToLS() {
|
||||
const serializableData = sheet.serializeData();
|
||||
|
|
@ -24,14 +21,3 @@ function loadDataFromLS() {
|
|||
|
||||
saveButton.addEventListener("click", saveDataToLS);
|
||||
loadButton.addEventListener("click", loadDataFromLS);
|
||||
sheet.changeCellStyles(
|
||||
{ column: 1, row: 1 },
|
||||
{
|
||||
background: "black",
|
||||
borderColor: "white",
|
||||
fontColor: "white",
|
||||
fontSize: 20,
|
||||
selectedBackground: "green",
|
||||
selectedFontColor: "black",
|
||||
},
|
||||
);
|
||||
|
|
|
|||
57
src/main.ts
57
src/main.ts
|
|
@ -1,5 +1,4 @@
|
|||
import { Editor } from "./components/editor";
|
||||
import { Header } from "./components/header";
|
||||
import { Scroller } from "./components/scroller";
|
||||
import { Sheet } from "./components/sheet";
|
||||
import { Table } from "./components/table";
|
||||
|
|
@ -20,6 +19,8 @@ import { createSampleData } from "./utils/createData";
|
|||
import { Cache, CachedColumn, CachedRow } from "./modules/cache";
|
||||
import { Row } from "./modules/row";
|
||||
import { Column } from "./modules/column";
|
||||
import { ColumnsBar } from "./components/columnsBar";
|
||||
import { RowsBar } from "./components/rowsBar";
|
||||
|
||||
/*
|
||||
! Component structure
|
||||
|
|
@ -44,7 +45,8 @@ export default class Spreadsheet {
|
|||
private table: Table;
|
||||
private scroller: Scroller;
|
||||
private toolbar: Toolbar;
|
||||
private header: Header;
|
||||
private rowsBar: RowsBar
|
||||
private columnsBar: ColumnsBar
|
||||
private sheet: Sheet;
|
||||
private editor: Editor;
|
||||
public styles: Styles;
|
||||
|
|
@ -68,12 +70,13 @@ export default class Spreadsheet {
|
|||
}
|
||||
|
||||
this.config = new Config(config);
|
||||
this.sheet = new Sheet(this);
|
||||
|
||||
this.rowsBar = new RowsBar(this)
|
||||
this.columnsBar = new ColumnsBar(this)
|
||||
this.sheet = new Sheet(this);
|
||||
this.table = new Table(this);
|
||||
this.scroller = new Scroller(this);
|
||||
this.toolbar = new Toolbar(this);
|
||||
this.header = new Header(this);
|
||||
this.editor = new Editor(this);
|
||||
this.cache = this.getInitialCache();
|
||||
this.viewport = new Viewport(
|
||||
|
|
@ -85,8 +88,28 @@ export default class Spreadsheet {
|
|||
this.data = data;
|
||||
this.styles = new Styles();
|
||||
this.buildComponent();
|
||||
this.setElementsPositions()
|
||||
this.appendTableToTarget(target);
|
||||
this.renderSheet();
|
||||
this.renderColumnsBar()
|
||||
this.renderRowsBar()
|
||||
}
|
||||
|
||||
private setRowsBarPosition() {
|
||||
const top = this.columnsBar.height + this.toolbar.height
|
||||
const left = 0
|
||||
this.rowsBar.setElementPosition(top, left)
|
||||
}
|
||||
private setColumnsBarPosition() {
|
||||
const top = this.toolbar.height
|
||||
const left = this.rowsBar.width
|
||||
console.log(top,left)
|
||||
this.columnsBar.setElementPosition(top, left)
|
||||
}
|
||||
|
||||
private setElementsPositions() {
|
||||
this.setRowsBarPosition()
|
||||
this.setColumnsBarPosition()
|
||||
}
|
||||
|
||||
private getInitialCache(): Cache {
|
||||
|
|
@ -126,12 +149,16 @@ export default class Spreadsheet {
|
|||
|
||||
private buildComponent(): void {
|
||||
const content = document.createElement("div"); //* Abstract
|
||||
content.appendChild(this.header.element);
|
||||
content.style.top = this.columnsBarHeight + 'px'
|
||||
content.style.left = this.rowsBarWidth + 'px'
|
||||
|
||||
content.appendChild(this.sheet.element);
|
||||
|
||||
content.classList.add(CSS_PREFIX + "content");
|
||||
|
||||
this.table.element.appendChild(this.toolbar.element);
|
||||
this.table.element.appendChild(this.rowsBar.element)
|
||||
this.table.element.appendChild(this.columnsBar.element)
|
||||
this.table.element.appendChild(content);
|
||||
this.table.element.appendChild(this.scroller.element);
|
||||
this.table.element.append(this.editor.element);
|
||||
|
|
@ -171,6 +198,18 @@ export default class Spreadsheet {
|
|||
return this.config.view;
|
||||
}
|
||||
|
||||
get columnsBarHeight() {
|
||||
return this.columnsBar.height
|
||||
}
|
||||
|
||||
get rowsBarWidth() {
|
||||
return this.rowsBar.width
|
||||
}
|
||||
|
||||
get toolbarHeight() {
|
||||
return this.toolbar.height
|
||||
}
|
||||
|
||||
/** Focusing on interactive part of spreadsheet */
|
||||
focusTable() {
|
||||
this.scroller.element.focus();
|
||||
|
|
@ -246,6 +285,14 @@ export default class Spreadsheet {
|
|||
this.sheet.renderSheet();
|
||||
}
|
||||
|
||||
renderColumnsBar() {
|
||||
this.columnsBar.renderBar()
|
||||
}
|
||||
|
||||
renderRowsBar() {
|
||||
this.rowsBar.renderBar()
|
||||
}
|
||||
|
||||
renderCell(row: number, col: number) {
|
||||
this.data[row][col].render(this);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -6,6 +6,7 @@ export class RenderBox {
|
|||
y: number;
|
||||
width: number;
|
||||
height: number;
|
||||
|
||||
constructor(config: Config, cellPosition: Position) {
|
||||
this.x = this.getXCoord(cellPosition.column, config);
|
||||
this.y = this.getYCoord(cellPosition.row, config);
|
||||
|
|
|
|||
|
|
@ -2,8 +2,6 @@ $css_prefix: "modern_sc_";
|
|||
|
||||
.#{$css_prefix}content {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
}
|
||||
|
||||
.#{$css_prefix}spreadsheet_container {
|
||||
|
|
@ -18,6 +16,7 @@ $css_prefix: "modern_sc_";
|
|||
}
|
||||
|
||||
.#{$css_prefix}scroller {
|
||||
position: absolute;
|
||||
overflow: scroll;
|
||||
box-sizing: border-box;
|
||||
transform: translateZ(0);
|
||||
|
|
|
|||
Loading…
Reference in New Issue