diff --git a/index.html b/index.html index 0d15dc6e..7393928a 100644 --- a/index.html +++ b/index.html @@ -3,7 +3,7 @@ - + diff --git a/index.js b/index.js index 13870b71..5517329d 100644 --- a/index.js +++ b/index.js @@ -1,23 +1,40 @@ import Lightsheet from "./src/main.ts"; var data = [ - ["", "=1+2/3*6+A1+test(1,2)", "img/nophoto.jpg", "Marketing"], - ["2", "Jorge", "img/nophoto.jpg", "Marketing", "3120"], - ["3", "Jorge", "img/nophoto.jpg", "Marketing", "3120"], + ["1", "=1+2/3*6+A1+test(1,2)", "img/nophoto.jpg", "Marketing"], + ["2.44445", "400000.000000", "img/nophoto.jpg", "Marketing", "3120"], + ["3.555555", "312", "43", "64", "3120"], ]; const toolbar = ["undo", "redo", "save"]; new Lightsheet( { - sheetName: "Sheet1", data, onCellChange: (colIndex, rowIndex, newValue) => { console.log(colIndex, rowIndex, newValue); }, toolbarOptions: { - showToolbar: true, + showToolbar: false, items: toolbar, + element: document.getElementById("toolbar-dom-id"), }, + style: [ + { + position: "A", + css: "font-weight: bold;", + format: { type: "number", options: { decimal: 2 } }, + }, + { + position: "B2", + css: "background-color: yellow;", + format: { type: "number", options: { decimal: 2 } }, + }, + { + position: "3", + css: "background-color: gray;", + format: { type: "number", options: { decimal: 0 } }, + }, + ], }, document.getElementById("lightsheet"), ); diff --git a/src/core/evaluation/expressionHandler.ts b/src/core/evaluation/expressionHandler.ts index 3ece47e9..88c6acb8 100644 --- a/src/core/evaluation/expressionHandler.ts +++ b/src/core/evaluation/expressionHandler.ts @@ -18,8 +18,8 @@ import { } from "./expressionHandler.types.ts"; import { CellState } from "../structure/cell/cellState.ts"; -import LightsheetHelper from "../../utils/helpers.ts"; -import { Coordinate } from "../../utils/common.types.ts"; +import { GenerateColumnLabel } from "../../utils/helpers.ts"; +import { IndexPosition } from "../../utils/common.types.ts"; import { CellReference } from "../structure/cell/types.cell.ts"; import SheetHolder from "../structure/sheetHolder.ts"; @@ -86,16 +86,16 @@ export default class ExpressionHandler { } } - updatePositionalReferences(from: Coordinate, to: Coordinate) { + updatePositionalReferences(from: IndexPosition, to: IndexPosition) { if (!this.rawValue.startsWith("=")) return this.rawValue; const expression = this.rawValue.substring(1); const parseResult = math.parse(expression); const fromSymbol = - LightsheetHelper.generateColumnLabel(from.column + 1) + (from.row + 1); + GenerateColumnLabel(from.columnIndex! + 1) + (from.rowIndex! + 1); const toSymbol = - LightsheetHelper.generateColumnLabel(to.column + 1) + (to.row + 1); + GenerateColumnLabel(to.columnIndex! + 1) + (to.rowIndex! + 1); // Update each symbol in the expression. const transform = parseResult.transform((node) => @@ -212,7 +212,7 @@ export default class ExpressionHandler { this.cellRefHolder.push({ sheetKey: targetSheet.key, - position: { column: j, row: i }, + position: { columnIndex: j, rowIndex: i }, }); values.push(cellInfo?.resolvedValue ?? ""); } @@ -230,8 +230,8 @@ export default class ExpressionHandler { this.cellRefHolder.push({ sheetKey: targetSheet.key, position: { - column: colIndex, - row: rowIndex, + columnIndex: colIndex, + rowIndex: rowIndex, }, }); return cellInfo?.resolvedValue ?? ""; diff --git a/src/core/evaluation/expressionHandler.types.ts b/src/core/evaluation/expressionHandler.types.ts index 4b8b56a7..e88bc6ef 100644 --- a/src/core/evaluation/expressionHandler.types.ts +++ b/src/core/evaluation/expressionHandler.types.ts @@ -1,9 +1,9 @@ +import { IndexPosition } from "../../utils/common.types.ts"; import { SheetKey } from "../structure/key/keyTypes.ts"; -import { Coordinate } from "../../utils/common.types.ts"; export type CellSheetPosition = { sheetKey: SheetKey; - position: Coordinate; + position: IndexPosition; }; export type EvaluationResult = { diff --git a/src/core/event/event.ts b/src/core/event/event.ts index eda0cfc6..78d78dd9 100644 --- a/src/core/event/event.ts +++ b/src/core/event/event.ts @@ -1,5 +1,5 @@ -import EventType from "./eventType"; import EventState from "./eventState"; +import { EventType } from "./events.types"; export default class Event { eventType: EventType; diff --git a/src/core/event/eventType.ts b/src/core/event/eventType.ts deleted file mode 100644 index 90abeaf2..00000000 --- a/src/core/event/eventType.ts +++ /dev/null @@ -1,6 +0,0 @@ -enum EventType { - UI_SET_CELL = 0, - CORE_SET_CELL = 1, -} - -export default EventType; diff --git a/src/core/event/events.ts b/src/core/event/events.ts index 6852ad69..876687a8 100644 --- a/src/core/event/events.ts +++ b/src/core/event/events.ts @@ -1,6 +1,6 @@ -import EventType from "./eventType"; import Event from "./event"; import EventState from "./eventState"; +import { EventType } from "./events.types"; export type ListenerFunction = (event: Event) => void; diff --git a/src/core/event/events.types.ts b/src/core/event/events.types.ts index ec442865..c80724f3 100644 --- a/src/core/event/events.types.ts +++ b/src/core/event/events.types.ts @@ -1,18 +1,29 @@ -import { PositionInfo } from "../structure/sheet.types.ts"; -import { Coordinate } from "../../utils/common.types.ts"; +import { IndexPosition } from "../../utils/common.types.ts"; +import { KeyPosition } from "../structure/sheet.types.ts"; export type UISetCellPayload = { - keyPosition?: PositionInfo; - indexPosition?: Coordinate; + keyPosition?: KeyPosition; + indexPosition?: IndexPosition; rawValue: string; }; export type CoreSetCellPayload = { - keyPosition: PositionInfo; - indexPosition: Coordinate; + keyPosition?: KeyPosition; + indexPosition: IndexPosition; rawValue: string; formattedValue: string; - clearCell: boolean; - clearRow: boolean; + clearCell?: boolean; + clearRow?: boolean; }; + +export type CoreSetStylePayload = { + indexPosition: IndexPosition; + value: string; +}; + +export enum EventType { + VIEW_SET_CELL = 0, + CORE_SET_CELL = 1, + VIEW_SET_STYLE = 2, +} diff --git a/src/core/structure/cellStyle.ts b/src/core/structure/cellStyle.ts index 92babfae..5a714a2d 100644 --- a/src/core/structure/cellStyle.ts +++ b/src/core/structure/cellStyle.ts @@ -3,24 +3,24 @@ import Cloneable from "../cloneable.ts"; export default class CellStyle extends Cloneable { formatter: Formatter | null; - styling: Map; + css: Map; constructor( - styling: Map | null = null, + css: Map | null = null, formatter: Formatter | null = null, ) { super(); this.formatter = formatter; - this.styling = new Map(styling); + this.css = new Map(css); } applyStylesOf(other: CellStyle | null): CellStyle { if (!other) return this; // If a style is set in other but not in this, apply it to this. - for (const [key, value] of other.styling) { - if (!this.styling.has(key)) { - this.styling.set(key, value); + for (const [key, value] of other.css) { + if (!this.css.has(key)) { + this.css.set(key, value); } } @@ -31,19 +31,30 @@ export default class CellStyle extends Cloneable { return this; } + applyCss(css: Map): CellStyle { + // If a style is set in other but not in this, apply it to this. + for (const [key, value] of css) { + if (!this.css.has(key)) { + this.css.set(key, value); + } + } + + return this; + } + clearStylingSetBy(other: CellStyle | null) { if (!other) return false; let isEmpty = true; // If a property is set in other, clear it from this. - for (const key in other.styling) { - if (this.styling.has(key)) { - this.styling.delete(key); + for (const key in other.css) { + if (this.css.has(key)) { + this.css.delete(key); } if (other.formatter) this.formatter = null; - if (isEmpty && (this.styling.has(key) || this.formatter)) isEmpty = false; + if (isEmpty && (this.css.has(key) || this.formatter)) isEmpty = false; } return isEmpty; diff --git a/src/core/structure/sheet.ts b/src/core/structure/sheet.ts index 113b054d..21dc88d8 100644 --- a/src/core/structure/sheet.ts +++ b/src/core/structure/sheet.ts @@ -8,19 +8,31 @@ import { import Cell from "./cell/cell.ts"; import Column from "./group/column.ts"; import Row from "./group/row.ts"; -import { CellInfo, PositionInfo, ShiftDirection } from "./sheet.types.ts"; +import { + CellInfo, + GroupType, + GroupTypes, + KeyPosition, + ShiftDirection, +} from "./sheet.types.ts"; import ExpressionHandler from "../evaluation/expressionHandler.ts"; import CellStyle from "./cellStyle.ts"; import CellGroup from "./group/cellGroup.ts"; import Events from "../event/events.ts"; import LightsheetEvent from "../event/event.ts"; -import { CoreSetCellPayload, UISetCellPayload } from "../event/events.types.ts"; -import EventType from "../event/eventType.ts"; +import { + CoreSetCellPayload, + CoreSetStylePayload, + EventType, + UISetCellPayload, +} from "../event/events.types.ts"; import { CellState } from "./cell/cellState.ts"; import { EvaluationResult } from "../evaluation/expressionHandler.types.ts"; +import Formatter from "../evaluation/formatter.ts"; import SheetHolder from "./sheetHolder.ts"; import { CellReference } from "./cell/types.cell.ts"; -import { Coordinate } from "../../utils/common.types.ts"; +import { GenerateStyleStringFromMap } from "../../utils/helpers.ts"; +import { IndexPosition } from "../../utils/common.types.ts"; export default class Sheet { readonly key: SheetKey; @@ -98,7 +110,7 @@ export default class Sheet { this.resolveCell(cell!, colKey, rowKey); } - this.emitSetCellEvent(colKey, rowKey, colIndex, rowIndex, cell); + this.emitSetCellEvent(colIndex, rowIndex, cell); return { rawValue: cell ? cell.rawValue : undefined, @@ -113,17 +125,23 @@ export default class Sheet { } public moveCell( - from: Coordinate, - to: Coordinate, + from: IndexPosition, + to: IndexPosition, moveStyling: boolean = true, ) { - const fromPosition = this.getCellInfoAt(from.column, from.row)?.position; - let toPosition = this.getCellInfoAt(to.column, to.row)?.position; + const fromPosition = this.getCellInfoAt( + from.columnIndex!, + from.rowIndex!, + )?.position; + let toPosition = this.getCellInfoAt( + to.columnIndex!, + to.rowIndex!, + )?.position; if (!fromPosition) return false; if (!toPosition) { - toPosition = this.initializePosition(to.column, to.row); + toPosition = this.initializePosition(to.columnIndex!, to.rowIndex!); } else { this.deleteCell(toPosition.columnKey!, toPosition.rowKey!); } @@ -331,14 +349,14 @@ export default class Sheet { ? this.rows.get(oppositeKey as RowKey)!.position : this.columns.get(oppositeKey as ColumnKey)!.position; - const fromCoord: Coordinate = { - column: group instanceof Column ? from : oppositeGroupPos!, - row: group instanceof Row ? from : oppositeGroupPos!, + const fromCoord: IndexPosition = { + columnIndex: group instanceof Column ? from : oppositeGroupPos!, + rowIndex: group instanceof Row ? from : oppositeGroupPos!, }; - const toCoord: Coordinate = { - column: group instanceof Column ? to : oppositeGroupPos!, - row: group instanceof Row ? to : oppositeGroupPos!, + const toCoord: IndexPosition = { + columnIndex: group instanceof Column ? to : oppositeGroupPos!, + rowIndex: group instanceof Row ? to : oppositeGroupPos!, }; this.updateCellReferenceSymbols(cell, fromCoord, toCoord); @@ -347,8 +365,8 @@ export default class Sheet { private updateCellReferenceSymbols( cell: Cell, - from: Coordinate, - to: Coordinate, + from: IndexPosition, + to: IndexPosition, ) { // Update reference symbols for all cell formulas that refer to the cell being moved. for (const [refCellKey, refInfo] of cell.referencesIn) { @@ -364,8 +382,6 @@ export default class Sheet { // Emit event for the rawValue change. refSheet.emitSetCellEvent( - refInfo.column, - refInfo.row, refSheet.getColumnIndex(refInfo.column)!, refSheet.getRowIndex(refInfo.row)!, refCell, @@ -416,7 +432,10 @@ export default class Sheet { return true; } - getCellStyle(colKey?: ColumnKey, rowKey?: RowKey): CellStyle { + getMergedCellStyle( + colKey: ColumnKey | null = null, + rowKey: RowKey | null = null, + ): CellStyle { const col = colKey ? this.columns.get(colKey) : null; const row = rowKey ? this.rows.get(rowKey) : null; if (!col && !row) return this.defaultStyle; @@ -433,46 +452,96 @@ export default class Sheet { return cellStyle; } - setCellStyle( - colKey: ColumnKey, - rowKey: RowKey, - style: CellStyle | null, - ): boolean { - const col = this.columns.get(colKey); - const row = this.rows.get(rowKey); - if (!col || !row) return false; - - if (style == null) { - return this.clearCellStyle(colKey, rowKey); - } - - // TODO Style could be non-null but empty; should we allow this? - style = new CellStyle().clone(style); + setCellFormatter( + columnIndex: number, + rowIndex: number, + formatter: Formatter | null = null, + ): void { + const { columnKey, rowKey } = this.initializePosition( + columnIndex, + rowIndex, + ); + const column = this.columns.get(columnKey!); + const row = this.rows.get(rowKey!); - col.cellFormatting.set(row.key, style); - row.cellFormatting.set(col.key, style); + if (!column || !row) return; - if (style.formatter) { - this.applyCellFormatter(this.getCell(colKey, rowKey)!, colKey, rowKey); - } + const newStyle = new CellStyle().clone(column.cellFormatting.get(row.key)); + newStyle.formatter = formatter; + column.cellFormatting.set(row.key, newStyle); + row.cellFormatting.set(column.key, newStyle); - return true; - } + const cell = this.getCell(columnKey!, rowKey!); - setColumnStyle(colKey: ColumnKey, style: CellStyle | null): boolean { - const col = this.columns.get(colKey); - if (!col) return false; + this.applyCellFormatter(cell!, columnKey!, rowKey!); - this.setCellGroupStyle(col, style); - return true; + this.deleteCellIfUnused(columnKey!, rowKey!); + this.emitSetCellEvent(columnIndex, rowIndex, cell); } - setRowStyle(rowKey: RowKey, style: CellStyle | null): boolean { - const row = this.rows.get(rowKey); - if (!row) return false; - - this.setCellGroupStyle(row, style); - return true; + setCellCss( + columnIndex: number, + rowIndex: number, + css: Map = new Map(), + ): void { + const { columnKey, rowKey } = this.initializePosition( + columnIndex, + rowIndex, + ); + const column = this.columns.get(columnKey!); + const row = this.rows.get(rowKey!); + + if (!column || !row) return; + + const newStyle = new CellStyle().clone(column.cellFormatting.get(row.key)); + newStyle.css = css; + column.cellFormatting.set(row.key, newStyle); + row.cellFormatting.set(column.key, newStyle); + + this.deleteCellIfUnused(columnKey!, rowKey!); + this.emitSetStyleEvent(columnIndex, rowIndex, columnKey, rowKey); + } + + setGroupCss( + groupIndex: number, + groupType: GroupType, + css: Map = new Map(), + ): void { + const isColumnGroup = groupType == GroupTypes.Column; + const groupKey = isColumnGroup + ? this.columnPositions.get(groupIndex) + : this.rowPositions.get(groupIndex); + if (!groupKey) return; + + const group = isColumnGroup + ? this.columns.get(groupKey as ColumnKey) + : this.rows.get(groupKey as RowKey); + if (!group) return; + + group.defaultStyle = new CellStyle(css, group.defaultStyle?.formatter); + isColumnGroup + ? this.emitSetStyleEvent(groupIndex, null, groupKey as ColumnKey, null) + : this.emitSetStyleEvent(null, groupIndex, null, groupKey as RowKey); + } + + setGroupFormatter( + groupIndex: number, + groupType: GroupType, + formatter: Formatter | null = null, + ): void { + const isColumnGroup: boolean = groupType == GroupTypes.Column; + const groupKey = isColumnGroup + ? this.columnPositions.get(groupIndex) + : this.rowPositions.get(groupIndex); + if (!groupKey) return; + const group = isColumnGroup + ? this.columns.get(groupKey as ColumnKey) + : this.rows.get(groupKey as RowKey); + if (!group) return; + + const cellStyle = new CellStyle(group.defaultStyle?.css, formatter); + + this.setCellGroupStyle(group, cellStyle); } private setCellGroupStyle( @@ -482,7 +551,6 @@ export default class Sheet { style = style ? new CellStyle().clone(style) : null; const formatterChanged = style?.formatter != group.defaultStyle?.formatter; group.defaultStyle = style; - // Iterate through formatted cells in this group and clear any styling properties set by the new style. for (const [opposingKey, cellStyle] of group.cellFormatting) { const shouldClear = cellStyle.clearStylingSetBy(style); @@ -497,18 +565,18 @@ export default class Sheet { } if (!formatterChanged) return; - // Apply new formatter to all cells in this group. for (const [opposingKey] of group.cellIndex) { const cell = this.cellData.get(group.cellIndex.get(opposingKey)!)!; - if (group instanceof Column) { - this.applyCellFormatter(cell, group.key, opposingKey as RowKey); - continue; - } - this.applyCellFormatter( + const colKey = ( + group instanceof Column ? group.key : opposingKey + ) as ColumnKey; + const rowKey = (group instanceof Row ? group.key : opposingKey) as RowKey; + this.applyCellFormatter(cell, colKey, rowKey); + this.emitSetCellEvent( + this.getColumnIndex(colKey)!, + this.getRowIndex(rowKey)!, cell, - opposingKey as ColumnKey, - group.key as RowKey, ); } } @@ -637,8 +705,6 @@ export default class Sheet { // Emit event if the referred cell's value has changed (for the referring sheet's events). if (refUpdated) { referringSheet.emitSetCellEvent( - refInfo.column, - refInfo.row, referringSheet.getColumnIndex(refInfo.column)!, referringSheet.getRowIndex(refInfo.row)!, referringCell, @@ -649,12 +715,9 @@ export default class Sheet { return valueChanged; } - private applyCellFormatter( - cell: Cell, - colKey: ColumnKey, - rowKey: RowKey, - ): boolean { - const style = this.getCellStyle(colKey, rowKey); + private applyCellFormatter(cell: Cell, colKey: ColumnKey, rowKey: RowKey) { + if (!cell) return; + const style = this.getMergedCellStyle(colKey, rowKey); let formattedValue: string | null = cell.resolvedValue; if (style?.formatter) { formattedValue = style.formatter.format(formattedValue); @@ -665,14 +728,15 @@ export default class Sheet { } cell.formattedValue = formattedValue; - return true; + return; } /** * Delete a cell if it's empty, has no formatting and is not referenced by any other cell. */ private deleteCellIfUnused(colKey: ColumnKey, rowKey: RowKey): boolean { - const cell = this.getCell(colKey, rowKey)!; + const cell = this.getCell(colKey, rowKey); + if (!cell) return false; if (cell.rawValue != "") return false; // Check if this cell is referenced by anything. @@ -702,10 +766,15 @@ export default class Sheet { // Initialize the referred cell if it doesn't exist yet. const position = refSheet.initializePosition( - ref.position.column, - ref.position.row, + ref.position.columnIndex!, + ref.position.rowIndex!, ); - if (!refSheet.getCellInfoAt(ref.position.column, ref.position.row)) { + if ( + !refSheet.getCellInfoAt( + ref.position.columnIndex!, + ref.position.rowIndex!, + ) + ) { refSheet.createCell(position.columnKey!, position.rowKey!, ""); } @@ -771,9 +840,9 @@ export default class Sheet { return false; } - private initializePosition(colPos: number, rowPos: number): PositionInfo { + private initializePosition(columnPos: number, rowPos: number): KeyPosition { let rowKey; - let colKey; + let columnKey; // Create row and column if they don't exist yet. if (!this.rowPositions.has(rowPos)) { @@ -786,47 +855,58 @@ export default class Sheet { rowKey = this.rowPositions.get(rowPos)!; } - if (!this.columnPositions.has(colPos)) { + if (!this.columnPositions.has(columnPos)) { // Create a new column - const col = new Column(this.defaultWidth, colPos); + const col = new Column(this.defaultWidth, columnPos); this.columns.set(col.key, col); - this.columnPositions.set(colPos, col.key); + this.columnPositions.set(columnPos, col.key); - colKey = col.key; + columnKey = col.key; } else { - colKey = this.columnPositions.get(colPos)!; + columnKey = this.columnPositions.get(columnPos)!; } - return { rowKey: rowKey, columnKey: colKey }; + return { columnKey, rowKey }; } private emitSetCellEvent( - colKey: ColumnKey, - rowKey: RowKey, - colPos: number, - rowPos: number, + columnIndex: number, + rowIndex: number, cell: Cell | null, ) { const payload: CoreSetCellPayload = { - keyPosition: { - rowKey: rowKey, - columnKey: colKey, - }, indexPosition: { - column: colPos, - row: rowPos, + columnIndex, + rowIndex, }, rawValue: cell ? cell.rawValue : "", formattedValue: cell ? cell.formattedValue : "", - clearCell: cell == null, - clearRow: this.rows.get(rowKey) == null, }; this.events.emit(new LightsheetEvent(EventType.CORE_SET_CELL, payload)); } + private emitSetStyleEvent( + columnIndex: number | null, + rowIndex: number | null, + columnKey: ColumnKey | null = null, + rowKey: RowKey | null = null, + ) { + const payload: CoreSetStylePayload = { + indexPosition: { + rowIndex, + columnIndex, + }, + value: GenerateStyleStringFromMap( + this.getMergedCellStyle(columnKey, rowKey).css, + ), + }; + + this.events.emit(new LightsheetEvent(EventType.VIEW_SET_STYLE, payload)); + } + private registerEvents() { - this.events.on(EventType.UI_SET_CELL, (event) => + this.events.on(EventType.VIEW_SET_CELL, (event) => this.handleUISetCell(event), ); } @@ -842,8 +922,8 @@ export default class Sheet { ); } else if (payload.indexPosition) { this.setCellAt( - payload.indexPosition.column, - payload.indexPosition.row, + payload.indexPosition.columnIndex!, + payload.indexPosition.rowIndex!, payload.rawValue, ); } else { diff --git a/src/core/structure/sheet.types.ts b/src/core/structure/sheet.types.ts index b8a5f715..64ed5da6 100644 --- a/src/core/structure/sheet.types.ts +++ b/src/core/structure/sheet.types.ts @@ -1,20 +1,34 @@ import { ColumnKey, RowKey } from "./key/keyTypes.ts"; import { CellState } from "./cell/cellState.ts"; -export type PositionInfo = { +export type KeyPosition = { columnKey?: ColumnKey; rowKey?: RowKey; }; export type CellInfo = { - position: PositionInfo; + position: KeyPosition; rawValue?: string; resolvedValue?: string; formattedValue?: string; state?: CellState; }; +export enum GroupTypes { + Column = 1, + Row, +} +export type GroupType = GroupTypes; + export enum ShiftDirection { forward = "forward", backward = "backward", } + +export type Format = { type: string; options?: any }; + +export type StyleInfo = { + position: string; + css?: string; + format?: Format; +}; diff --git a/src/main.ts b/src/main.ts index 5ea38eb8..b2c1844e 100644 --- a/src/main.ts +++ b/src/main.ts @@ -1,18 +1,26 @@ -import UI from "./ui/render.ts"; -import { LightsheetOptions } from "./main.types.ts"; +import UI from "./view/view.ts"; import Sheet from "./core/structure/sheet.ts"; -import { CellInfo } from "./core/structure/sheet.types.ts"; +import { + CellInfo, + Format, + GroupTypes, + StyleInfo, +} from "./core/structure/sheet.types.ts"; import Events from "./core/event/events.ts"; import { ListenerFunction } from "./core/event/events.ts"; import EventState from "./core/event/eventState.ts"; -import EventType from "./core/event/eventType.ts"; import SheetHolder from "./core/structure/sheetHolder.ts"; import { DefaultColCount, DefaultRowCount } from "./utils/constants.ts"; import ExpressionHandler from "./core/evaluation/expressionHandler.ts"; import { CellReference } from "./core/structure/cell/types.cell.ts"; -import { RowKey, ColumnKey } from "./core/structure/key/keyTypes.ts"; -import CellStyle from "./core/structure/cellStyle.ts"; -import { Coordinate } from "./utils/common.types.ts"; +import NumberFormatter from "./core/evaluation/numberFormatter.ts"; +import { + GenerateStyleMapFromString, + GetRowColFromCellRef, +} from "./utils/helpers.ts"; +import { LightsheetOptions } from "./main.types.ts"; +import { EventType } from "./core/event/events.types.ts"; +import { IndexPosition } from "./utils/common.types.ts"; export default class Lightsheet { private ui: UI | undefined; @@ -20,6 +28,7 @@ export default class Lightsheet { private sheet: Sheet; sheetHolder: SheetHolder; private events: Events; + style?: any = null; onCellChange?; isReady: boolean = false; @@ -37,7 +46,7 @@ export default class Lightsheet { this.events = new Events(); this.sheetHolder = SheetHolder.getInstance(); this.sheet = new Sheet(options.sheetName, this.events); - + this.style = options.style; if (targetElement) { this.ui = new UI(targetElement, this.options, this.events); @@ -61,7 +70,7 @@ export default class Lightsheet { if (options.onCellChange) { this.onCellChange = options.onCellChange; } - + this.initializeStyle(); if (options.onReady) options.onReady = this.options.onReady; this.onTableReady(); } @@ -100,56 +109,76 @@ export default class Lightsheet { this.options.isReadOnly = isReadOnly; } - showToolbar(isShown: boolean) { - this.ui?.showToolbar(isShown); - } - - getKey() { - return this.sheet.key; + getFormatter(type: string, options?: any) { + if (type == "number") { + return new NumberFormatter(options.decimal); + } + return; + } + + setCss(position: string, css: string) { + const { rowIndex, columnIndex } = GetRowColFromCellRef(position); + const mappedCss = css ? GenerateStyleMapFromString(css) : null; + if (rowIndex == null && columnIndex == null) { + return; + } else if (rowIndex != null && columnIndex != null) { + this.sheet.setCellCss(columnIndex, rowIndex, mappedCss!); + } else if (rowIndex != null) { + this.sheet.setGroupCss(rowIndex, GroupTypes.Row, mappedCss!); + } else if (columnIndex != null) { + this.sheet.setGroupCss(columnIndex, GroupTypes.Column, mappedCss!); + } } - getName() { - return this.options.sheetName; + clearCss(position: string) { + this.setCss(position, ""); } - setCellAt(columnIndex: number, rowIndex: number, value: any): CellInfo { - return this.sheet.setCellAt(columnIndex, rowIndex, value.toString()); + setFormatting(position: string, format: Format) { + this.processFormatting(position, format); } - setCell(colKey: ColumnKey, rowKey: RowKey, formula: string): CellInfo | null { - return this.sheet.setCell(colKey, rowKey, formula); + clearFormatter(position: string) { + this.processFormatting(position, null); } - getCellInfoAt(colPos: number, rowPos: number): CellInfo | null { - return this.sheet.getCellInfoAt(colPos, rowPos); - } - - getRowIndex(rowKey: RowKey): number | undefined { - return this.sheet.getRowIndex(rowKey); + private processFormatting(position: string, format: Format | null) { + const { rowIndex, columnIndex } = GetRowColFromCellRef(position); + const formatter = format + ? this.getFormatter(format.type, format.options) + : null; + if (!formatter) return; + if (rowIndex == null && columnIndex == null) { + return; + } else if (rowIndex != null && columnIndex != null) { + this.sheet.setCellFormatter(columnIndex, rowIndex, formatter); + } else if (rowIndex != null) { + this.sheet.setGroupFormatter(rowIndex, GroupTypes.Row, formatter); + } else if (columnIndex != null) { + this.sheet.setGroupFormatter(columnIndex, GroupTypes.Column, formatter); + } } - getColumnIndex(colKey: ColumnKey): number | undefined { - return this.sheet.getColumnIndex(colKey); + private initializeStyle() { + this.style?.forEach((item: StyleInfo) => { + if (item.css) this.setCss(item.position, item.css!); + if (item.format) this.setFormatting(item.position, item.format); + }); } - - getCellStyle(colKey?: ColumnKey, rowKey?: RowKey): CellStyle { - return this.sheet.getCellStyle(colKey, rowKey); + showToolbar(isShown: boolean) { + this.ui?.showToolbar(isShown); } - setCellStyle( - colKey: ColumnKey, - rowKey: RowKey, - style: CellStyle | null, - ): boolean { - return this.sheet.setCellStyle(colKey, rowKey, style); + getName() { + return this.options.sheetName; } - setRowStyle(rowkey: RowKey, cellStyle: CellStyle): boolean { - return this.sheet.setRowStyle(rowkey, cellStyle); + setCell(columnIndex: number, rowIndex: number, value: any): CellInfo { + return this.sheet.setCellAt(columnIndex, rowIndex, value.toString()); } - setColumnStyle(columnKey: ColumnKey, cellStyle: CellStyle): boolean { - return this.sheet.setColumnStyle(columnKey, cellStyle); + getCellInfoAt(colPos: number, rowPos: number): CellInfo | null { + return this.sheet.getCellInfoAt(colPos, rowPos); } moveColumn(from: number, to: number): boolean { @@ -160,7 +189,11 @@ export default class Lightsheet { return this.sheet.moveRow(from, to); } - moveCell(from: Coordinate, to: Coordinate, moveStyling: boolean = true) { + moveCell( + from: IndexPosition, + to: IndexPosition, + moveStyling: boolean = true, + ) { this.sheet.moveCell(from, to, moveStyling); } diff --git a/src/main.types.ts b/src/main.types.ts index ddc0665a..36795595 100644 --- a/src/main.types.ts +++ b/src/main.types.ts @@ -2,6 +2,8 @@ export type LightsheetOptions = { sheetName: string; data?: any[]; + + style?: any; onCellChange?: (colIndex: number, rowIndex: number, value: any) => void; onCellClick?: (colIndex: number, rowIndex: number) => void; onReady?: () => void; diff --git a/src/ui/render.types.ts b/src/ui/render.types.ts deleted file mode 100644 index 1a3f6fee..00000000 --- a/src/ui/render.types.ts +++ /dev/null @@ -1,11 +0,0 @@ -import { Coordinate } from "../utils/common.types.ts"; - -export type CellIdInfo = { - keyParts: string[]; - isIndex: boolean; -}; - -export type SelectionContainer = { - selectionStart: Coordinate | null; - selectionEnd: Coordinate | null; -}; diff --git a/src/utils/common.types.ts b/src/utils/common.types.ts index e3f6075b..8c2d54b5 100644 --- a/src/utils/common.types.ts +++ b/src/utils/common.types.ts @@ -1,4 +1,4 @@ -export type Coordinate = { - row: number; - column: number; +export type IndexPosition = { + columnIndex?: number | null; + rowIndex?: number | null; }; diff --git a/src/utils/helpers.ts b/src/utils/helpers.ts index 34d6c7cc..614991c8 100644 --- a/src/utils/helpers.ts +++ b/src/utils/helpers.ts @@ -1,12 +1,69 @@ -export default class LightsheetHelper { - static generateColumnLabel = (rowIndex: number) => { - let label = ""; - const alphabet = "ABCDEFGHIJKLMNOPQRSTUVWXYZ"; - while (rowIndex > 0) { - rowIndex--; // Adjust index to start from 0 - label = alphabet[rowIndex % 26] + label; - rowIndex = Math.floor(rowIndex / 26); +import { IndexPosition } from "./common.types"; + +export function GenerateRowLabel(rowIndex: number): string { + let label = ""; + const alphabet = "ABCDEFGHIJKLMNOPQRSTUVWXYZ"; + while (rowIndex > 0) { + rowIndex--; // Adjust index to start from 0 + label = alphabet[rowIndex % 26] + label; + rowIndex = Math.floor(rowIndex / 26); + } + return label || "A"; // Return "A" if index is 0 +} + +export function GetRowColFromCellRef(cellRef: string): IndexPosition { + // Regular expression to extract the column and row indexes + const matches = cellRef.match(/^([A-Z]+)?(\d+)?$/); + if (matches) { + const colStr = matches[1] || ""; // If column letter is not provided, default to empty string + const rowStr = matches[2] || ""; // If row number is not provided, default to empty string + + // Convert column string to index + let colIndex = 0; + if (colStr !== "") { + for (let i = 0; i < colStr.length; i++) { + colIndex = colIndex * 26 + (colStr.charCodeAt(i) - 64); + } } - return label || "A"; // Return "A" if index is 0 - }; + + // Convert row string to index + const rowIndex = rowStr ? parseInt(rowStr, 10) : null; + + return { + rowIndex: rowIndex ? rowIndex - 1 : null, + columnIndex: colIndex ? colIndex - 1 : null, + }; + } else { + // Invalid cell reference + return { rowIndex: null, columnIndex: null }; + } +} + +export function GenerateColumnLabel(rowIndex: number) { + let label = ""; + const alphabet = "ABCDEFGHIJKLMNOPQRSTUVWXYZ"; + while (rowIndex > 0) { + rowIndex--; // Adjust index to start from 0 + label = alphabet[rowIndex % 26] + label; + rowIndex = Math.floor(rowIndex / 26); + } + return label || "A"; // Return "A" if index is 0 +} + +export function GenerateStyleMapFromString(style: string): Map { + const mappedStyle = new Map(); + style.split(";").forEach((item: string) => { + const [key, value] = item.split(":"); + if (!key || !value) return; + mappedStyle.set(key.trim(), value.trim()); + }); + return mappedStyle; +} + +export function GenerateStyleStringFromMap(style: Map) { + let result = ""; + for (const [key, value] of style) { + result += `${key}:${value};`; + } + return result; } diff --git a/src/ui/ui.css b/src/view/view.css similarity index 96% rename from src/ui/ui.css rename to src/view/view.css index e017d73b..d504a577 100644 --- a/src/ui/ui.css +++ b/src/view/view.css @@ -61,8 +61,8 @@ .lightsheet_table_td { border-top: 1px solid var(--lightsheet-silver-400); border-left: 1px solid var(--lightsheet-silver-400); - border-right: 0px; - border-bottom: 0px; + border-right: 1px solid transparent; + border-bottom: 1px solid transparent; background-color: white; } @@ -100,6 +100,7 @@ td:first-child { margin: 0px; padding: 2px; background-color: transparent; + font-weight: inherit; } .lightsheet_table_row_number { diff --git a/src/ui/render.ts b/src/view/view.ts similarity index 78% rename from src/ui/render.ts rename to src/view/view.ts index 9d932301..c9b7788d 100644 --- a/src/ui/render.ts +++ b/src/view/view.ts @@ -1,15 +1,16 @@ -import { SelectionContainer } from "./render.types.ts"; -import LightsheetEvent from "../core/event/event.ts"; +import Event from "../core/event/event"; +import Events from "../core/event/events"; import { - CoreSetCellPayload, UISetCellPayload, -} from "../core/event/events.types.ts"; -import EventType from "../core/event/eventType.ts"; -import { LightsheetOptions, ToolbarOptions } from "../main.types"; -import LightsheetHelper from "../utils/helpers.ts"; -import { ToolbarItems } from "../utils/constants.ts"; -import { Coordinate } from "../utils/common.types.ts"; -import Events from "../core/event/events.ts"; + EventType, + CoreSetStylePayload, + CoreSetCellPayload, +} from "../core/event/events.types"; +import { ToolbarOptions, LightsheetOptions } from "../main.types"; +import { IndexPosition } from "../utils/common.types"; +import { ToolbarItems } from "../utils/constants"; +import { GenerateColumnLabel } from "../utils/helpers"; +import { SelectionContainer } from "./view.types"; export default class UI { tableEl!: Element; @@ -25,7 +26,7 @@ export default class UI { selectedCellsContainer: SelectionContainer; toolbarOptions: ToolbarOptions; isReadOnly: boolean; - singleSelectedCell: Coordinate | undefined; + singleSelectedCell: IndexPosition | undefined; tableContainerDom: Element; private events: Events; @@ -166,8 +167,8 @@ export default class UI { const newValue = this.formulaInput.value; if (event.key === "Enter") { if (this.singleSelectedCell) { - const colIndex = this.singleSelectedCell.column; - const rowIndex = this.singleSelectedCell.row; + const colIndex = this.singleSelectedCell.columnIndex!; + const rowIndex = this.singleSelectedCell.rowIndex!; this.onUICellValueChange(newValue, colIndex, rowIndex); } this.formulaInput.blur(); @@ -184,8 +185,8 @@ export default class UI { this.formulaInput.onblur = () => { const newValue = this.formulaInput.value; if (this.singleSelectedCell) { - const colIndex = this.singleSelectedCell.column; - const rowIndex = this.singleSelectedCell.row; + const colIndex = this.singleSelectedCell.columnIndex!; + const rowIndex = this.singleSelectedCell.rowIndex!; this.onUICellValueChange(newValue, colIndex, rowIndex); } }; @@ -206,8 +207,7 @@ export default class UI { ); const newColumnNumber = this.getColumnCount() + 1; - const newHeaderValue = - LightsheetHelper.generateColumnLabel(newColumnNumber); + const newHeaderValue = GenerateColumnLabel(newColumnNumber); headerCellDom.textContent = newHeaderValue; headerCellDom.onclick = (e: MouseEvent) => @@ -215,8 +215,7 @@ export default class UI { const rowCount = this.getRowCount(); for (let rowIndex = 0; rowIndex < rowCount; rowIndex++) { - const rowDom = this.tableBodyDom.children[rowIndex]; - this.addCell(rowDom, newColumnNumber - 1, rowIndex, ""); + this.addCell(newColumnNumber - 1, rowIndex, ""); } this.tableHeadDom.children[0].appendChild(headerCellDom); @@ -250,7 +249,7 @@ export default class UI { const columnCount = this.getColumnCount(); for (let columnIndex = 0; columnIndex < columnCount; columnIndex++) { - this.addCell(rowDom, columnIndex, rowCount, ""); + this.addCell(columnIndex, rowCount, ""); } return rowDom; } @@ -295,23 +294,20 @@ export default class UI { return rowDom; } - getRow(rowKey: string): HTMLElement | null { - return document.getElementById(rowKey); + getRowDom(rowIndex: number) { + return this.tableBodyDom.children.length < rowIndex + 1 + ? null + : this.tableBodyDom.children[rowIndex]; } - addCell( - rowDom: Element, - colIndex: number, - rowIndex: number, - value: any, - columnKey?: string, - ): HTMLElement { + addCell(colIndex: number, rowIndex: number, value: any): HTMLElement { const cellDom = document.createElement("td"); cellDom.classList.add( "lightsheet_table_cell", "lightsheet_table_row_cell", "lightsheet_table_td", ); + const rowDom = this.tableBodyDom.children[rowIndex]; rowDom.appendChild(cellDom); cellDom.id = `${colIndex}_${rowIndex}`; cellDom.setAttribute("column-index", `${colIndex}` || ""); @@ -324,7 +320,6 @@ export default class UI { cellDom.appendChild(inputDom); if (value) { - cellDom.id = `${columnKey}_${rowDom.id}`; inputDom.value = value; } @@ -359,8 +354,8 @@ export default class UI { } this.singleSelectedCell = { - column: Number(colIndex), - row: Number(rowIndex), + columnIndex: Number(colIndex), + rowIndex: Number(rowIndex), }; if (this.formulaBarDom) { @@ -405,83 +400,100 @@ export default class UI { } } - onUICellValueChange(rawValue: string, colIndex: number, rowIndex: number) { + onUICellValueChange(rawValue: string, columnIndex: number, rowIndex: number) { const payload: UISetCellPayload = { - indexPosition: { column: colIndex, row: rowIndex }, + indexPosition: { columnIndex, rowIndex }, rawValue, }; - this.events.emit(new LightsheetEvent(EventType.UI_SET_CELL, payload)); + this.events.emit(new Event(EventType.VIEW_SET_CELL, payload)); } private registerEvents() { this.events.on(EventType.CORE_SET_CELL, (event) => { this.onCoreSetCell(event); }); + this.events.on(EventType.VIEW_SET_STYLE, (event) => { + this.onCoreSetStyle(event.payload); + }); + } + + private onCoreSetStyle(event: CoreSetStylePayload) { + const { indexPosition, value } = event; + if ( + indexPosition.columnIndex != undefined && + indexPosition.rowIndex != undefined + ) { + const cellDom = + this.tableBodyDom.children[indexPosition.rowIndex].children[ + indexPosition.columnIndex + 1 + ]; + const inputElement = cellDom! as HTMLElement; + inputElement.setAttribute("style", value); + } else if (indexPosition.columnIndex || indexPosition.columnIndex === 0) { + for (let i = 0; i < this.tableBodyDom.children.length; i++) { + this.tableBodyDom.children[i].children[ + indexPosition.columnIndex + 1 + ].setAttribute("style", value); + } + } else { + for ( + let i = 1; + i < this.tableBodyDom.children[indexPosition.rowIndex!].children.length; + i++ + ) { + this.tableBodyDom.children[indexPosition.rowIndex!].children[ + i + ].setAttribute("style", value); + } + } } private onCoreSetCell(event: LightsheetEvent) { const payload = event.payload as CoreSetCellPayload; // Create new columns if the column index is greater than the current column count. - const newColumns = payload.indexPosition.column - this.getColumnCount() + 1; + const newColumns = + payload.indexPosition.columnIndex! - this.getColumnCount() + 1; for (let i = 0; i < newColumns; i++) { this.addColumn(); } - const newRows = payload.indexPosition.row - this.getRowCount() + 1; + const newRows = payload.indexPosition.rowIndex! - this.getRowCount() + 1; for (let i = 0; i < newRows; i++) { this.addRow(); } // Get HTML elements and (new) IDs for the payload's cell and row. - const elInfo = this.getElementInfoForSetCell(payload); - - elInfo.cellDom!.id = elInfo.cellDomId; - elInfo.rowDom!.id = elInfo.rowDomId; + const cellInputDom = this.getElementInfoForSetCell( + payload.indexPosition.columnIndex!, + payload.indexPosition.rowIndex!, + payload.formattedValue, + ); - // Update input element with values from the core. - const inputEl = elInfo.cellDom!.firstChild! as HTMLInputElement; - inputEl.setAttribute("rawValue", payload.rawValue); - inputEl.setAttribute("resolvedValue", payload.formattedValue); - inputEl.value = payload.formattedValue; + cellInputDom.setAttribute("rawValue", payload.rawValue); + cellInputDom.setAttribute("resolvedValue", payload.formattedValue); + cellInputDom.value = payload.formattedValue; } - private getElementInfoForSetCell = (payload: CoreSetCellPayload) => { - const colKey = payload.keyPosition.columnKey?.toString(); - const rowKey = payload.keyPosition.rowKey?.toString(); - - const columnIndex = payload.indexPosition.column; - const rowIndex = payload.indexPosition.row; - - const cellDomKey = - colKey && rowKey ? `${colKey!.toString()}_${rowKey!.toString()}` : null; - - // Get the cell by either column and row key or position. - // TODO Index-based ID may not be unique if there are multiple sheets. - const cellDom = - (cellDomKey && document.getElementById(cellDomKey)) || - document.getElementById(`${columnIndex}_${rowIndex}`); - - const newCellDomId = payload.clearCell - ? `${columnIndex}_${rowIndex}` - : `${colKey}_${rowKey}`; - - const newRowDomId = payload.clearRow ? `row_${rowIndex}` : rowKey!; - - let rowDom: HTMLElement | null = null; - if (rowKey) { - rowDom = document.getElementById(rowKey); - } - if (!rowDom) { - const rowId = `row_${rowIndex}`; - rowDom = document.getElementById(rowId); + private getElementInfoForSetCell = ( + columnIndex: number, + rowIndex: number, + formattedValue: string, + ) => { + let cellDom; + if (this.tableBodyDom.children.length < rowIndex + 1) { + this.addRow(); + cellDom = this.addCell(columnIndex, rowIndex, formattedValue); + } else { + if ( + this.tableBodyDom.children[rowIndex].children.length < + columnIndex + 2 + ) { + cellDom = this.addCell(columnIndex, rowIndex, formattedValue); + } else + cellDom = + this.tableBodyDom.children[rowIndex].children[columnIndex + 1]; } - - return { - cellDom: cellDom, - cellDomId: newCellDomId, - rowDom: rowDom, - rowDomId: newRowDomId, - }; + return cellDom.firstChild! as HTMLInputElement; }; getColumnCount() { @@ -551,14 +563,15 @@ export default class UI { return false; const withinX = - (cellColumnIndex >= selectionStart.column && - cellColumnIndex <= selectionEnd.column) || - (cellColumnIndex <= selectionStart.column && - cellColumnIndex >= selectionEnd.column); + (cellColumnIndex >= selectionStart.columnIndex! && + cellColumnIndex <= selectionEnd.columnIndex!) || + (cellColumnIndex <= selectionStart.columnIndex! && + cellColumnIndex >= selectionEnd.columnIndex!); const withinY = - (cellRowIndex >= selectionStart.row && - cellRowIndex <= selectionEnd.row) || - (cellRowIndex <= selectionStart.row && cellRowIndex >= selectionEnd.row); + (cellRowIndex >= selectionStart.rowIndex! && + cellRowIndex <= selectionEnd.rowIndex!) || + (cellRowIndex <= selectionStart.rowIndex! && + cellRowIndex >= selectionEnd.rowIndex!); return withinX && withinY; } @@ -584,8 +597,8 @@ export default class UI { this.selectedCellsContainer.selectionStart = (colIndex != null || undefined) && (rowIndex != null || undefined) ? { - row: rowIndex, - column: colIndex, + rowIndex: rowIndex, + columnIndex: colIndex, } : null; } @@ -597,8 +610,8 @@ export default class UI { this.selectedCellsContainer.selectionEnd = (colIndex != null || undefined) && (rowIndex != null || undefined) ? { - row: rowIndex, - column: colIndex, + rowIndex: rowIndex, + columnIndex: colIndex, } : null; if ( diff --git a/src/view/view.types.ts b/src/view/view.types.ts new file mode 100644 index 00000000..ea92b0ba --- /dev/null +++ b/src/view/view.types.ts @@ -0,0 +1,11 @@ +import { IndexPosition } from "../utils/common.types"; + +export type CellIdInfo = { + keyParts: string[]; + isIndex: boolean; +}; + +export type SelectionContainer = { + selectionStart: IndexPosition | null; + selectionEnd: IndexPosition | null; +}; diff --git a/tests/core/event/event.test.ts b/tests/core/event/event.test.ts index f68f8d31..6450245c 100644 --- a/tests/core/event/event.test.ts +++ b/tests/core/event/event.test.ts @@ -1,7 +1,7 @@ import Event from "../../../src/core/event/event"; -import EventType from "../../../src/core/event/eventType"; import EventState from "../../../src/core/event/eventState"; import Events from "../../../src/core/event/events"; +import { EventType } from "../../../src/core/event/events.types"; describe("Events", () => { let events: Events; @@ -12,9 +12,9 @@ describe("Events", () => { test("should register and trigger an event", () => { const mockCallback = jest.fn(); - events.on(EventType.UI_SET_CELL, mockCallback); + events.on(EventType.VIEW_SET_CELL, mockCallback); - const event = new Event(EventType.UI_SET_CELL, "test payload", false); + const event = new Event(EventType.VIEW_SET_CELL, "test payload", false); events.emit(event); expect(mockCallback).toHaveBeenCalledWith(event); @@ -96,10 +96,10 @@ describe("Events", () => { test("should remove an event listener", () => { const mockCallback = jest.fn(); - events.on(EventType.UI_SET_CELL, mockCallback); - events.removeEventListener(EventType.UI_SET_CELL, mockCallback); + events.on(EventType.VIEW_SET_CELL, mockCallback); + events.removeEventListener(EventType.VIEW_SET_CELL, mockCallback); - const event = new Event(EventType.UI_SET_CELL, "test payload"); + const event = new Event(EventType.VIEW_SET_CELL, "test payload"); events.emit(event); expect(mockCallback).not.toHaveBeenCalled(); @@ -107,17 +107,17 @@ describe("Events", () => { test("should remove an event listener only with correct state", () => { const mockCallback = jest.fn(); - events.on(EventType.UI_SET_CELL, mockCallback); - events.on(EventType.UI_SET_CELL, mockCallback, EventState.PRE_EVENT); + events.on(EventType.VIEW_SET_CELL, mockCallback); + events.on(EventType.VIEW_SET_CELL, mockCallback, EventState.PRE_EVENT); events.removeEventListener( - EventType.UI_SET_CELL, + EventType.VIEW_SET_CELL, mockCallback, EventState.PRE_EVENT, ); const event = new Event( - EventType.UI_SET_CELL, + EventType.VIEW_SET_CELL, "test payload", false, EventState.PRE_EVENT, @@ -130,10 +130,10 @@ describe("Events", () => { test("should handle multiple listeners for the same event", () => { const firstCallback = jest.fn(); const secondCallback = jest.fn(); - events.on(EventType.UI_SET_CELL, firstCallback); - events.on(EventType.UI_SET_CELL, secondCallback); + events.on(EventType.VIEW_SET_CELL, firstCallback); + events.on(EventType.VIEW_SET_CELL, secondCallback); - const event = new Event(EventType.UI_SET_CELL, "test payload"); + const event = new Event(EventType.VIEW_SET_CELL, "test payload"); events.emit(event); expect(firstCallback).toHaveBeenCalledWith(event); @@ -142,7 +142,7 @@ describe("Events", () => { test("should not trigger listeners of a different event type", () => { const mockCallback = jest.fn(); - events.on(EventType.UI_SET_CELL, mockCallback); + events.on(EventType.VIEW_SET_CELL, mockCallback); const event = new Event(EventType.CORE_SET_CELL, "test payload"); events.emit(event); @@ -152,10 +152,10 @@ describe("Events", () => { test("should not trigger listeners after they are removed", () => { const mockCallback = jest.fn(); - events.on(EventType.UI_SET_CELL, mockCallback); - events.removeEventListener(EventType.UI_SET_CELL, mockCallback); + events.on(EventType.VIEW_SET_CELL, mockCallback); + events.removeEventListener(EventType.VIEW_SET_CELL, mockCallback); - const event = new Event(EventType.UI_SET_CELL, "test payload"); + const event = new Event(EventType.VIEW_SET_CELL, "test payload"); events.emit(event); expect(mockCallback).not.toHaveBeenCalled(); @@ -167,11 +167,11 @@ describe("Events", () => { }); const anotherCallback = jest.fn(); - events.on(EventType.UI_SET_CELL, mockCallback, EventState.PRE_EVENT); - events.addEventListener(EventType.UI_SET_CELL, anotherCallback); + events.on(EventType.VIEW_SET_CELL, mockCallback, EventState.PRE_EVENT); + events.addEventListener(EventType.VIEW_SET_CELL, anotherCallback); const event = new Event( - EventType.UI_SET_CELL, + EventType.VIEW_SET_CELL, "test payload", false, EventState.PRE_EVENT, @@ -188,20 +188,24 @@ describe("Events", () => { const postEventCallback = jest.fn(); events.addEventListener( - EventType.UI_SET_CELL, + EventType.VIEW_SET_CELL, preEventCallback, EventState.PRE_EVENT, ); - events.on(EventType.UI_SET_CELL, postEventCallback, EventState.POST_EVENT); + events.on( + EventType.VIEW_SET_CELL, + postEventCallback, + EventState.POST_EVENT, + ); const preEvent = new Event( - EventType.UI_SET_CELL, + EventType.VIEW_SET_CELL, "pre payload", false, EventState.PRE_EVENT, ); const postEvent = new Event( - EventType.UI_SET_CELL, + EventType.VIEW_SET_CELL, "post payload", false, EventState.POST_EVENT, diff --git a/tests/core/structure/cellReferences.test.ts b/tests/core/structure/cellReferences.test.ts index bb3668e5..c1c4e72c 100644 --- a/tests/core/structure/cellReferences.test.ts +++ b/tests/core/structure/cellReferences.test.ts @@ -1,7 +1,6 @@ import Sheet from "../../../src/core/structure/sheet"; import { CellState } from "../../../src/core/structure/cell/cellState.ts"; import { CellInfo } from "../../../src/core/structure/sheet.types.ts"; -import CellStyle from "../../../src/core/structure/cellStyle.ts"; describe("Cell references", () => { let sheet: Sheet; @@ -107,17 +106,12 @@ describe("Cell references", () => { }); it("should create an empty cell with styling", () => { - const b2 = sheet.getCellInfoAt(1, 1)!; - sheet.setCellStyle( - b2.position!.columnKey!, - b2.position!.rowKey!, - new CellStyle(new Map([["width", "50px"]])), - ); + sheet.setCellCss(1, 1, new Map([["width", "50px;"]])); sheet.setCellAt(1, 1, ""); // Clearing the style should result in the cell being deleted. - sheet.setCellStyle(b2!.position.columnKey!, b2!.position.rowKey!, null); + sheet.setCellCss(1, 1, new Map()); expect(sheet.getCellInfoAt(1, 1)).toBeNull(); }); diff --git a/tests/core/structure/cellStyle.test.ts b/tests/core/structure/cellStyle.test.ts index 176c08b0..c4755e3c 100644 --- a/tests/core/structure/cellStyle.test.ts +++ b/tests/core/structure/cellStyle.test.ts @@ -1,5 +1,6 @@ import Sheet from "../../../src/core/structure/sheet.ts"; import CellStyle from "../../../src/core/structure/cellStyle.ts"; +import { GroupTypes } from "../../../src/core/structure/sheet.types.ts"; describe("CellStyle", () => { let sheet: Sheet; @@ -24,43 +25,46 @@ describe("CellStyle", () => { new CellStyle(new Map([["border", "1px solid black"]])), ]; - sheet.setCellStyle(pos.columnKey!, pos.rowKey!, styles[0]); - expect(sheet.getCellStyle(pos.columnKey!, pos.rowKey!)!).toEqual(styles[0]); - - sheet.setCellStyle(pos.columnKey!, pos.rowKey!, null); - expect(sheet.getCellStyle(pos.columnKey!, pos.rowKey!)).toEqual( - sheet["defaultStyle"], + sheet.setCellCss(1, 1, styles[0].css); + expect(sheet.getMergedCellStyle(pos.columnKey, pos.rowKey).css!).toEqual( + styles[0].css, ); - sheet.setRowStyle(pos.rowKey!, styles[1]); - expect(sheet.getCellStyle(pos.columnKey!, pos.rowKey!)!).toEqual(styles[1]); + sheet.setCellCss(1, 1, new Map()); + expect(sheet.getMergedCellStyle(pos.columnKey, pos.rowKey).css).toEqual( + sheet.defaultStyle.css, + ); + sheet.setGroupCss(1, GroupTypes.Row, styles[1].css); + expect(sheet.getMergedCellStyle(pos.columnKey, pos.rowKey).css).toEqual( + styles[1].css, + ); - sheet.setColumnStyle(pos.columnKey!, styles[2]); - expect(sheet.getCellStyle(pos.columnKey!, pos.rowKey!)!).toEqual( + sheet.setGroupCss(1!, GroupTypes.Column, styles[2].css); + expect(sheet.getMergedCellStyle(pos.columnKey, pos.rowKey).css).toEqual( new CellStyle( new Map([ ["width", "50px"], ["color", "0xff0000"], ]), - ), + ).css, ); - sheet.setCellStyle(pos.columnKey!, pos.rowKey!, styles[3]); - expect(sheet.getCellStyle(pos.columnKey!, pos.rowKey!)!).toEqual( + sheet.setCellCss(1, 1, styles[3].css); + expect(sheet.getMergedCellStyle(pos.columnKey, pos.rowKey).css).toEqual( new CellStyle( new Map([ ["width", "50px"], ["color", "0xff0000"], ["border", "1px solid black"], ]), - ), + ).css, ); - sheet.setCellStyle(pos.columnKey!, pos.rowKey!, null); - sheet.setRowStyle(pos.rowKey!, null); - sheet.setColumnStyle(pos.columnKey!, null); - expect(sheet.getCellStyle(pos.columnKey!, pos.rowKey!)).toEqual( - sheet["defaultStyle"], + sheet.setCellCss(1, 1, new Map()); + sheet.setGroupCss(1, GroupTypes.Row, new Map()); + sheet.setGroupCss(1, GroupTypes.Column, new Map()); + expect(sheet.getMergedCellStyle(pos.columnKey, pos.rowKey).css).toEqual( + sheet.defaultStyle.css, ); }); }); diff --git a/tests/core/structure/formatter.test.ts b/tests/core/structure/formatter.test.ts index ce0fbd2f..a5590b79 100644 --- a/tests/core/structure/formatter.test.ts +++ b/tests/core/structure/formatter.test.ts @@ -2,6 +2,7 @@ import Sheet from "../../../src/core/structure/sheet.ts"; import CellStyle from "../../../src/core/structure/cellStyle.ts"; import NumberFormatter from "../../../src/core/evaluation/numberFormatter.ts"; import { CellState } from "../../../src/core/structure/cell/cellState.ts"; +import { GroupTypes } from "../../../src/core/structure/sheet.types.ts"; describe("Formatter test", () => { let sheet: Sheet; @@ -19,24 +20,23 @@ describe("Formatter test", () => { it("Should round a fraction correctly", () => { const oneDigit = new CellStyle(null, new NumberFormatter(1)); - sheet.setColumnStyle(sheet["columnPositions"].get(1)!, oneDigit); + sheet.setGroupFormatter(1, GroupTypes.Column, oneDigit.formatter); expect(sheet.getCellInfoAt(1, 1)!.formattedValue).toBe("0.8"); }); it("Should apply two different formatting rules to the same cell value", () => { const noDigits = new CellStyle(null, new NumberFormatter(0)); const twoDigits = new CellStyle(null, new NumberFormatter(2)); + sheet.setGroupFormatter(1, GroupTypes.Column, noDigits.formatter); + sheet.setGroupFormatter(2, GroupTypes.Column, twoDigits.formatter); - sheet.setColumnStyle(sheet["columnPositions"].get(0)!, noDigits); - sheet.setColumnStyle(sheet["columnPositions"].get(2)!, twoDigits); - - expect(sheet.getCellInfoAt(0, 1)!.formattedValue).toBe("12"); + expect(sheet.getCellInfoAt(1, 1)!.formattedValue).toBe("1"); expect(sheet.getCellInfoAt(2, 1)!.formattedValue).toBe("12.30"); }); it("Should format a string value as a number and result in an invalid cell state", () => { const style = new CellStyle(null, new NumberFormatter(0)); - sheet.setColumnStyle(sheet["columnPositions"].get(0)!, style); + sheet.setGroupFormatter(0, GroupTypes.Column, style.formatter); expect(sheet.getCellInfoAt(0, 0)!.state).toBe(CellState.INVALID_FORMAT); }); diff --git a/tests/core/structure/moveCell.test.ts b/tests/core/structure/moveCell.test.ts index 6ac2716e..2a64358b 100644 --- a/tests/core/structure/moveCell.test.ts +++ b/tests/core/structure/moveCell.test.ts @@ -21,7 +21,10 @@ describe("Cell moving tests", () => { }); it("should move a single cell and not invalidate incoming references", () => { - sheet.moveCell({ column: 0, row: 0 }, { column: 3, row: 3 }); + sheet.moveCell( + { columnIndex: 0, rowIndex: 0 }, + { columnIndex: 3, rowIndex: 3 }, + ); const referringCell = sheet.getCellInfoAt(1, 0); expect(sheet.getCellInfoAt(0, 0)).toBe(null); expect(referringCell!.resolvedValue).toBe("1"); @@ -29,9 +32,18 @@ describe("Cell moving tests", () => { }); it("should move multiple cells and not invalidate references", () => { - sheet.moveCell({ column: 0, row: 0 }, { column: 3, row: 0 }); - sheet.moveCell({ column: 1, row: 1 }, { column: 4, row: 1 }); - sheet.moveCell({ column: 2, row: 2 }, { column: 5, row: 2 }); + sheet.moveCell( + { columnIndex: 0, rowIndex: 0 }, + { columnIndex: 3, rowIndex: 0 }, + ); + sheet.moveCell( + { columnIndex: 1, rowIndex: 1 }, + { columnIndex: 4, rowIndex: 1 }, + ); + sheet.moveCell( + { columnIndex: 2, rowIndex: 2 }, + { columnIndex: 5, rowIndex: 2 }, + ); expect(sheet.getCellInfoAt(0, 0)).toBe(null); expect(sheet.getCellInfoAt(1, 0)?.rawValue).toBe("=D1"); @@ -47,15 +59,15 @@ describe("Cell moving tests", () => { it("should move a single cell with its styling", () => { const style = new CellStyle(new Map([["color", "red"]])); const fromCell = sheet.getCellInfoAt(0, 0)!; - sheet.setCellStyle( - fromCell.position.columnKey!, - fromCell.position.rowKey!, - style, - ); - sheet.moveCell({ column: 0, row: 0 }, { column: 3, row: 3 }); + sheet.setCellCss(0, 0, style.css); + + sheet.moveCell( + { columnIndex: 0, rowIndex: 0 }, + { columnIndex: 3, rowIndex: 3 }, + ); expect( - sheet.getCellStyle( + sheet.getMergedCellStyle( fromCell.position.columnKey!, fromCell.position.rowKey!, ), @@ -63,7 +75,10 @@ describe("Cell moving tests", () => { const toCell = sheet.getCellInfoAt(3, 3)!; expect( - sheet.getCellStyle(toCell.position.columnKey!, toCell.position.rowKey!), + sheet.getMergedCellStyle( + toCell.position.columnKey!, + toCell.position.rowKey!, + ), ).toEqual(style); }); }); diff --git a/tests/ui/setCellAt.test.ts b/tests/ui/setCellAt.test.ts index 49d55ad3..2348d0d8 100644 --- a/tests/ui/setCellAt.test.ts +++ b/tests/ui/setCellAt.test.ts @@ -22,20 +22,8 @@ describe("Lightsheet setCellAt", () => { document.body.removeChild(targetElementMock); }); - it("Should set the cell and use col and row keys", () => { - const cellInfo = lightSheet.setCellAt(1, 1, "test"); - - //Query the HTML via document else it will not be able to find the element - const cellId = cellInfo.position.columnKey + "_" + cellInfo.position.rowKey; - const cellElement = document.getElementById(cellId); - - const cellInput = cellElement?.children[0] as HTMLInputElement; - expect(cellInput.value).toBe("test"); - }); - it("Should set the cell at the correct position in the DOM", () => { lightSheet.setCellAt(2, 3, "test"); - //Query the HTML via document else it will not be able to find the element const tableElement = document.querySelector("table"); //This Table API accept position as 1 based index diff --git a/tests/ui/setCss.test.ts b/tests/ui/setCss.test.ts new file mode 100644 index 00000000..c6e125ff --- /dev/null +++ b/tests/ui/setCss.test.ts @@ -0,0 +1,80 @@ +import LightSheet from "../../src/main"; + +describe("LightSheet", () => { + let targetElementMock: HTMLElement; + + beforeEach(() => { + window.sheetHolder?.clear(); + targetElementMock = document.createElement("div"); + document.body.appendChild(targetElementMock); + }); + + afterEach(() => { + document.body.removeChild(targetElementMock); + }); + + test("Should be able to render table based on provided styles", () => { + const styleString = "font-weight: bold;"; + + new LightSheet( + { + data: [ + ["1", "=1+2/3*6+A1+test(1,2)", "img/nophoto.jpg", "Marketing"], + ["2.44445", "400000.000000", "img/nophoto.jpg", "Marketing", "3120"], + ["3.555555", "Jorge", "img/nophoto.jpg", "Marketing", "3120"], + ], + sheetName: "Sheet", + style: [ + { + position: "A1", + css: styleString, + }, + ], + }, + targetElementMock, + ); + + const tableBody = targetElementMock.querySelector("tbody"); + if (!tableBody) { + // If tbody is not found, fail the test or log an error + fail("tbody element not found in the table."); + } + + expect( + (tableBody.rows[0].children[1] as HTMLElement).style.cssText, + ).toEqual(styleString); + }); + + test("Should be able to clear existing table style", () => { + const styleString = "font-weight: bold;"; + + const ls = new LightSheet( + { + data: [ + ["1", "=1+2/3*6+A1+test(1,2)", "img/nophoto.jpg", "Marketing"], + ["2.44445", "400000.000000", "img/nophoto.jpg", "Marketing", "3120"], + ["3.555555", "Jorge", "img/nophoto.jpg", "Marketing", "3120"], + ], + sheetName: "Sheet", + style: [ + { + position: "A1", + css: styleString, + }, + ], + }, + targetElementMock, + ); + ls.clearCss("A"); + + const tableBody = targetElementMock.querySelector("tbody"); + if (!tableBody) { + // If tbody is not found, fail the test or log an error + fail("tbody element not found in the table."); + } + + expect( + (tableBody.rows[0].children[1] as HTMLElement).style.cssText, + ).toEqual(""); + }); +}); diff --git a/tests/ui/setFromatter.test.ts b/tests/ui/setFromatter.test.ts new file mode 100644 index 00000000..d34d03a1 --- /dev/null +++ b/tests/ui/setFromatter.test.ts @@ -0,0 +1,101 @@ +import LightSheet from "../../src/main"; + +describe("LightSheet", () => { + let targetElementMock: HTMLElement; + + beforeEach(() => { + window.sheetHolder?.clear(); + targetElementMock = document.createElement("div"); + document.body.appendChild(targetElementMock); + }); + + afterEach(() => { + document.body.removeChild(targetElementMock); + }); + + test("Should be able to render table based on provided formatters", () => { + new LightSheet( + { + data: [ + ["1", "=1+2/3*6+A1+test(1,2)", "img/nophoto.jpg", "Marketing"], + ["2.44445", "400000.000000", "img/nophoto.jpg", "Marketing", "3120"], + ["3.555555", "Jorge", "img/nophoto.jpg", "Marketing", "3120"], + ], + sheetName: "Sheet", + style: [ + { + position: "A", + css: "font-weight: bold;", + format: { type: "number", options: { decimal: 2 } }, + }, + ], + }, + targetElementMock, + ); + + const tableBody = targetElementMock.querySelector("tbody"); + if (!tableBody) { + // If tbody is not found, fail the test or log an error + fail("tbody element not found in the table."); + } + expect( + (tableBody.rows[1].children[1].children[0] as HTMLInputElement).value, + ).toEqual("2.44"); + }); + + test("Should be able to set formatter to existing table", () => { + const ls = new LightSheet( + { + data: [ + ["1", "=1+2/3*6+A1+test(1,2)", "img/nophoto.jpg", "Marketing"], + ["2.44445", "400000.000000", "img/nophoto.jpg", "Marketing", "3120"], + ["3.555555", "Jorge", "img/nophoto.jpg", "Marketing", "3120"], + ], + sheetName: "Sheet", + }, + targetElementMock, + ); + ls.setFormatting("A2", { type: "number", options: { decimal: 2 } }); + + const tableBody = targetElementMock.querySelector("tbody"); + if (!tableBody) { + // If tbody is not found, fail the test or log an error + fail("tbody element not found in the table."); + } + expect( + (tableBody.rows[1].children[1].children[0] as HTMLInputElement).value, + ).toEqual("2.44"); + }); + + test("Should be able to clear formatter to from table", () => { + const ls = new LightSheet( + { + data: [ + ["1", "=1+2/3*6+A1+test(1,2)", "img/nophoto.jpg", "Marketing"], + ["2.44445", "400000.000000", "img/nophoto.jpg", "Marketing", "3120"], + ["3.555555", "Jorge", "img/nophoto.jpg", "Marketing", "3120"], + ], + sheetName: "Sheet", + style: [ + { + position: "A", + css: "font-weight: bold;", + format: { type: "number", options: { decimal: 2 } }, + }, + ], + }, + targetElementMock, + ); + ls.clearFormatter("A"); + + const tableBody = targetElementMock.querySelector("tbody"); + if (!tableBody) { + // If tbody is not found, fail the test or log an error + fail("tbody element not found in the table."); + } + + expect( + (tableBody.rows[1].children[1].children[0] as HTMLInputElement).value, + ).toEqual("2.44445"); + }); +});