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       21x       21x   21x 20x 20x           21x 43x 106x   106x 41x           41x       41x     106x 43x 63x 1x     106x 42x     106x       43x 21x           21x   24x 24x 64x 64x 64x 5x       26x 4x   3x       1x           2x         23x 23x   23x 62x 62x 3x       24x 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');
}