BookStackApp/BookStack

View on GitHub
resources/js/wysiwyg/utils/table-map.ts

Summary

Maintainability
C
7 hrs
Test Coverage
import {CustomTableNode} from "../nodes/custom-table";
import {$isCustomTableCellNode, CustomTableCellNode} from "../nodes/custom-table-cell";
import {$isTableRowNode} from "@lexical/table";

export type CellRange = {
    fromX: number;
    fromY: number;
    toX: number;
    toY: number;
}

export class TableMap {

    rowCount: number = 0;
    columnCount: number = 0;

    // Represents an array (rows*columns in length) of cell nodes from top-left to
    // bottom right. Cells may repeat where merged and covering multiple spaces.
    cells: CustomTableCellNode[] = [];

    constructor(table: CustomTableNode) {
        this.buildCellMap(table);
    }

    protected buildCellMap(table: CustomTableNode) {
        const rowsAndCells: CustomTableCellNode[][] = [];
        const setCell = (x: number, y: number, cell: CustomTableCellNode) => {
            if (typeof rowsAndCells[y] === 'undefined') {
                rowsAndCells[y] = [];
            }

            rowsAndCells[y][x] = cell;
        };
        const cellFilled = (x: number, y: number): boolean => !!(rowsAndCells[y] && rowsAndCells[y][x]);

        const rowNodes = table.getChildren().filter(r => $isTableRowNode(r));
        for (let rowIndex = 0; rowIndex < rowNodes.length; rowIndex++) {
            const rowNode = rowNodes[rowIndex];
            const cellNodes = rowNode.getChildren().filter(c => $isCustomTableCellNode(c));
            let targetColIndex: number = 0;
            for (let cellIndex = 0; cellIndex < cellNodes.length; cellIndex++) {
                const cellNode = cellNodes[cellIndex];
                const colspan = cellNode.getColSpan() || 1;
                const rowSpan = cellNode.getRowSpan() || 1;
                for (let x = targetColIndex; x < targetColIndex + colspan; x++) {
                    for (let y = rowIndex; y < rowIndex + rowSpan; y++) {
                        while (cellFilled(x, y)) {
                            targetColIndex += 1;
                            x += 1;
                        }

                        setCell(x, y, cellNode);
                    }
                }
                targetColIndex += colspan;
            }
        }

        this.rowCount = rowsAndCells.length;
        this.columnCount = Math.max(...rowsAndCells.map(r => r.length));

        const cells = [];
        let lastCell: CustomTableCellNode = rowsAndCells[0][0];
        for (let y = 0; y < this.rowCount; y++) {
            for (let x = 0; x < this.columnCount; x++) {
                if (!rowsAndCells[y] || !rowsAndCells[y][x]) {
                    cells.push(lastCell);
                } else {
                    cells.push(rowsAndCells[y][x]);
                    lastCell = rowsAndCells[y][x];
                }
            }
        }

        this.cells = cells;
    }

    public getCellAtPosition(x: number, y: number): CustomTableCellNode {
        const position = (y * this.columnCount) + x;
        if (position >= this.cells.length) {
            throw new Error(`TableMap Error: Attempted to get cell ${position+1} of ${this.cells.length}`);
        }

        return this.cells[position];
    }

    public getCellsInRange(range: CellRange): CustomTableCellNode[] {
        const minX = Math.max(Math.min(range.fromX, range.toX), 0);
        const maxX = Math.min(Math.max(range.fromX, range.toX), this.columnCount - 1);
        const minY = Math.max(Math.min(range.fromY, range.toY), 0);
        const maxY = Math.min(Math.max(range.fromY, range.toY), this.rowCount - 1);

        const cells = new Set<CustomTableCellNode>();

        for (let y = minY; y <= maxY; y++) {
            for (let x = minX; x <= maxX; x++) {
                cells.add(this.getCellAtPosition(x, y));
            }
        }

        return [...cells.values()];
    }

    public getCellsInColumn(columnIndex: number): CustomTableCellNode[] {
        return this.getCellsInRange({
            fromX: columnIndex,
            toX: columnIndex,
            fromY: 0,
            toY: this.rowCount - 1,
        });
    }

    public getRangeForCell(cell: CustomTableCellNode): CellRange|null {
        let range: CellRange|null = null;
        const cellKey = cell.getKey();

        for (let y = 0; y < this.rowCount; y++) {
            for (let x = 0; x < this.columnCount; x++) {
                const index = (y * this.columnCount) + x;
                const lCell = this.cells[index];
                if (lCell.getKey() === cellKey) {
                    if (range === null) {
                        range = {fromX: x, toX: x, fromY: y, toY: y};
                    } else {
                        range.fromX = Math.min(range.fromX, x);
                        range.toX = Math.max(range.toX, x);
                        range.fromY = Math.min(range.fromY, y);
                        range.toY = Math.max(range.toY, y);
                    }
                }
            }
        }

        return range;
    }
}