@humanspeak/svelte-headless-table
Version: 
A powerful, headless table library for Svelte that provides complete control over table UI while handling complex data operations like sorting, filtering, pagination, grouping, and row expansion. Build custom, accessible data tables with zero styling opin
186 lines (185 loc) • 6.85 kB
JavaScript
import { DataHeaderCell, FlatDisplayHeaderCell, GroupDisplayHeaderCell, GroupHeaderCell } from './headerCells.js';
import { TableComponent } from './tableComponent.js';
import { sum } from './utils/math.js';
import { getNullMatrix, getTransposed } from './utils/matrix.js';
import { derived } from 'svelte/store';
export class HeaderRow extends TableComponent {
    cells;
    constructor({ id, cells }) {
        super({ id });
        this.cells = cells;
    }
    attrs() {
        return derived(super.attrs(), ($baseAttrs) => {
            return {
                ...$baseAttrs,
                role: 'row'
            };
        });
    }
    clone() {
        return new HeaderRow({
            id: this.id,
            cells: this.cells
        });
    }
}
export const getHeaderRows = (columns, flatColumnIds = []) => {
    const rowMatrix = getHeaderRowMatrix(columns);
    // Perform all column operations on the transposed columnMatrix. This helps
    // to reduce the number of expensive transpose operations required.
    let columnMatrix = getTransposed(rowMatrix);
    columnMatrix = getOrderedColumnMatrix(columnMatrix, flatColumnIds);
    // eslint-disable-next-line @typescript-eslint/no-explicit-any
    populateGroupHeaderCellIds(columnMatrix);
    return headerRowsForRowMatrix(getTransposed(columnMatrix));
};
export const getHeaderRowMatrix = (columns) => {
    const maxColspan = sum(columns.map((c) => (c.isGroup() ? c.ids.length : 1)));
    const maxHeight = Math.max(...columns.map((c) => c.height));
    const rowMatrix = getNullMatrix(maxColspan, maxHeight);
    let cellOffset = 0;
    columns.forEach((c) => {
        const heightOffset = maxHeight - c.height;
        loadHeaderRowMatrix(rowMatrix, c, heightOffset, cellOffset);
        cellOffset += c.isGroup() ? c.ids.length : 1;
    });
    // Replace null cells with blank display cells.
    return rowMatrix.map((cells, rowIdx) => cells.map((cell, columnIdx) => {
        if (cell !== null)
            return cell;
        if (rowIdx === maxHeight - 1)
            return new FlatDisplayHeaderCell({ id: columnIdx.toString(), colstart: columnIdx });
        const flatId = rowMatrix[maxHeight - 1][columnIdx]?.id ?? columnIdx.toString();
        return new GroupDisplayHeaderCell({ ids: [], allIds: [flatId], colstart: columnIdx });
    }));
};
const loadHeaderRowMatrix = (rowMatrix, column, rowOffset, cellOffset) => {
    if (column.isData()) {
        // `DataHeaderCell` should always be in the last row.
        rowMatrix[rowMatrix.length - 1][cellOffset] = new DataHeaderCell({
            // eslint-disable-next-line @typescript-eslint/no-explicit-any
            label: column.header,
            accessorFn: column.accessorFn,
            accessorKey: column.accessorKey,
            id: column.id,
            colstart: cellOffset
        });
        return;
    }
    if (column.isDisplay()) {
        rowMatrix[rowMatrix.length - 1][cellOffset] = new FlatDisplayHeaderCell({
            id: column.id,
            // eslint-disable-next-line @typescript-eslint/no-explicit-any
            label: column.header,
            colstart: cellOffset
        });
        return;
    }
    if (column.isGroup()) {
        // Fill multi-colspan cells.
        for (let i = 0; i < column.ids.length; i++) {
            rowMatrix[rowOffset][cellOffset + i] = new GroupHeaderCell({
                // eslint-disable-next-line @typescript-eslint/no-explicit-any
                label: column.header,
                colspan: 1,
                allIds: column.ids,
                ids: [],
                colstart: cellOffset
            });
        }
        let childCellOffset = 0;
        column.columns.forEach((c) => {
            loadHeaderRowMatrix(rowMatrix, c, rowOffset + 1, cellOffset + childCellOffset);
            childCellOffset += c.isGroup() ? c.ids.length : 1;
        });
        return;
    }
};
export const getOrderedColumnMatrix = (columnMatrix, flatColumnIds) => {
    if (flatColumnIds.length === 0) {
        return columnMatrix;
    }
    const orderedColumnMatrix = [];
    // Each row of the transposed matrix represents a column.
    // The `FlatHeaderCell` should be the last cell of each column.
    flatColumnIds.forEach((key, columnIdx) => {
        const nextColumn = columnMatrix.find((columnCells) => {
            const flatCell = columnCells[columnCells.length - 1];
            if (!flatCell.isFlat()) {
                throw new Error('The last element of each column must be a `FlatHeaderCell`');
            }
            return flatCell.id === key;
        });
        if (nextColumn !== undefined) {
            orderedColumnMatrix.push(nextColumn.map((column) => {
                const clonedColumn = column.clone();
                clonedColumn.colstart = columnIdx;
                return clonedColumn;
            }));
        }
    });
    return orderedColumnMatrix;
};
const populateGroupHeaderCellIds = (columnMatrix) => {
    columnMatrix.forEach((columnCells) => {
        const lastCell = columnCells[columnCells.length - 1];
        if (!lastCell.isFlat()) {
            throw new Error('The last element of each column must be a `FlatHeaderCell`');
        }
        columnCells.forEach((c) => {
            if (c.isGroup()) {
                c.pushId(lastCell.id);
            }
        });
    });
};
export const headerRowsForRowMatrix = (rowMatrix) => {
    return rowMatrix.map((rowCells, rowIdx) => {
        return new HeaderRow({ id: rowIdx.toString(), cells: getMergedRow(rowCells) });
    });
};
/**
 * Multi-colspan cells will appear as multiple adjacent cells on the same row.
 * Join these adjacent multi-colspan cells and update the colspan property.
 *
 * Non-adjacent multi-colspan cells (due to column ordering) must be cloned
 * from the original .
 *
 * @param cells An array of cells.
 * @returns An array of cells with no duplicate consecutive cells.
 */
export const getMergedRow = (cells) => {
    if (cells.length === 0) {
        return cells;
    }
    const mergedCells = [];
    let startIdx = 0;
    let endIdx = 1;
    while (startIdx < cells.length) {
        const cell = cells[startIdx].clone();
        if (!cell.isGroup()) {
            mergedCells.push(cell);
            startIdx++;
            continue;
        }
        endIdx = startIdx + 1;
        const ids = [...cell.ids];
        while (endIdx < cells.length) {
            const nextCell = cells[endIdx];
            if (!nextCell.isGroup()) {
                break;
            }
            if (cell.allId !== nextCell.allId) {
                break;
            }
            ids.push(...nextCell.ids);
            endIdx++;
        }
        cell.setIds(ids);
        cell.colspan = endIdx - startIdx;
        mergedCells.push(cell);
        startIdx = endIdx;
    }
    return mergedCells;
};