All files / roosterjs-content-model-dom/lib/modelApi/editing normalizeTable.ts

100% Statements 65/65
94.83% Branches 55/58
100% Functions 12/12
100% Lines 57/57

Press n or j to go to the next uncovered block, b, p or k for the previous block.

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 1551x 1x 1x 1x 1x                     1x       1x                         1x       29x       29x   29x 28x 28x           29x 60x 139x   139x 65x           65x       65x     139x 58x 81x 1x     139x 59x     139x       60x 38x           29x   31x 31x 80x 80x 80x 5x       33x 4x   3x       1x           2x         32x 32x   32x 80x 80x 3x       33x 2x 2x                 8x 8x     8x 7x 7x         8x   8x    
import { addBlock } from '../common/addBlock';
import { addSegment } from '../common/addSegment';
import { createBr } from '../creators/createBr';
import { createParagraph } from '../creators/createParagraph';
import { mutateBlock } from '../common/mutate';
import type {
    ContentModelSegmentFormat,
    ReadonlyContentModelSegment,
    ReadonlyContentModelTable,
    ReadonlyContentModelTableCell,
} from 'roosterjs-content-model-types';
 
/**
 * Minimum width for a table cell
 */
export const MIN_ALLOWED_TABLE_CELL_WIDTH: number = 30;
/**
 * Minimum height for a table cell
 */
export const MIN_ALLOWED_TABLE_CELL_HEIGHT: number = 22;
 
/**
 * Normalize a Content Model table, make sure:
 * 1. Fist cells are not spanned
 * 2. Only first column and row can have headers
 * 3. All cells have content
 * 4. Table and table row have correct width/height
 * 5. Spanned cell has no child blocks
 * 6. default format is correctly applied
 * @param readonlyTable The table to normalize
 * @param defaultSegmentFormat @optional Default segment format to apply to cell
 */
export function normalizeTable(
    readonlyTable: ReadonlyContentModelTable,
    defaultSegmentFormat?: ContentModelSegmentFormat
) {
    const table = mutateBlock(readonlyTable);
 
    // Collapse border and use border box for table in roosterjs to make layout simpler
    // But if this is a legacy style table (table with deprecated border attributes), we should not change its border model
    const format = table.format;
 
    if (!format.cellSpacing && !format.cellPadding && !format.legacyTableBorder) {
        format.borderCollapse = true;
        format.useBorderBox = true;
    }
 
    // Make sure all first cells are not spanned
    // Make sure all inner cells are not header
    // Make sure all cells have content and width
    table.rows.forEach((row, rowIndex) => {
        row.cells.forEach((readonlyCell, colIndex) => {
            const cell = mutateBlock(readonlyCell);
 
            if (cell.blocks.length == 0) {
                const format = cell.format.textColor
                    ? {
                          ...defaultSegmentFormat,
                          textColor: cell.format.textColor,
                      }
                    : defaultSegmentFormat;
                addBlock(
                    cell,
                    createParagraph(undefined /*isImplicit*/, undefined /*blockFormat*/, format)
                );
                addSegment(cell, createBr(format));
            }
 
            if (rowIndex == 0) {
                cell.spanAbove = false;
            } else if (rowIndex > 0 && colIndex > 0 && cell.isHeader) {
                cell.isHeader = false;
            }
 
            if (colIndex == 0) {
                cell.spanLeft = false;
            }
 
            cell.format.useBorderBox = true;
        });
 
        // Make sure table has correct width and height array
        if (row.height < MIN_ALLOWED_TABLE_CELL_HEIGHT) {
            row.height = MIN_ALLOWED_TABLE_CELL_HEIGHT;
        }
    });
 
    // Move blocks from spanned cell to its main cell if any,
    // and remove rows/columns if all cells in it are spanned
    const colCount = table.rows[0]?.cells.length || 0;
 
    for (let colIndex = colCount - 1; colIndex > 0; colIndex--) {
        table.rows.forEach(row => {
            const cell = row.cells[colIndex];
            const leftCell = row.cells[colIndex - 1];
            if (cell && leftCell && cell.spanLeft) {
                tryMoveBlocks(leftCell, cell);
            }
        });
 
        if (table.rows.every(row => row.cells[colIndex]?.spanLeft)) {
            table.rows.forEach(row => row.cells.splice(colIndex, 1));
 
            if (
                typeof table.widths[colIndex] === 'number' &&
                typeof table.widths[colIndex - 1] === 'number'
            ) {
                table.widths.splice(
                    colIndex - 1,
                    2,
                    table.widths[colIndex - 1] + table.widths[colIndex]
                );
            } else {
                table.widths.splice(colIndex, 1);
            }
        }
    }
 
    for (let rowIndex = table.rows.length - 1; rowIndex > 0; rowIndex--) {
        const row = table.rows[rowIndex];
 
        row.cells.forEach((cell, colIndex) => {
            const aboveCell = table.rows[rowIndex - 1]?.cells[colIndex];
            if (aboveCell && cell.spanAbove) {
                tryMoveBlocks(aboveCell, cell);
            }
        });
 
        if (row.cells.every(cell => cell.spanAbove)) {
            table.rows[rowIndex - 1].height += row.height;
            table.rows.splice(rowIndex, 1);
        }
    }
}
 
function tryMoveBlocks(
    targetCell: ReadonlyContentModelTableCell,
    sourceCell: ReadonlyContentModelTableCell
) {
    const onlyHasEmptyOrBr = sourceCell.blocks.every(
        block => block.blockType == 'Paragraph' && hasOnlyBrSegment(block.segments)
    );
 
    if (!onlyHasEmptyOrBr) {
        mutateBlock(targetCell).blocks.push(...sourceCell.blocks);
        mutateBlock(sourceCell).blocks = [];
    }
}
 
function hasOnlyBrSegment(segments: ReadonlyArray<ReadonlyContentModelSegment>): boolean {
    segments = segments.filter(s => s.segmentType != 'SelectionMarker');
 
    return segments.length == 0 || (segments.length == 1 && segments[0].segmentType == 'Br');
}