diff --git a/demo/scripts/controlsV2/demoButtons/setTableHeaderButton.ts b/demo/scripts/controlsV2/demoButtons/setTableHeaderButton.ts deleted file mode 100644 index c6ab1b058df..00000000000 --- a/demo/scripts/controlsV2/demoButtons/setTableHeaderButton.ts +++ /dev/null @@ -1,13 +0,0 @@ -import { formatTable, getFormatState } from 'roosterjs-content-model-api'; -import type { RibbonButton } from '../roosterjsReact/ribbon'; - -export const setTableHeaderButton: RibbonButton<'ribbonButtonSetTableHeader'> = { - key: 'ribbonButtonSetTableHeader', - unlocalizedText: 'Toggle table header', - iconName: 'Header', - isDisabled: formatState => !formatState.isInTable, - onClick: editor => { - const format = getFormatState(editor); - formatTable(editor, { hasHeaderRow: !format.tableHasHeader }, true /*keepCellShade*/); - }, -}; diff --git a/demo/scripts/controlsV2/demoButtons/tableOptionsButton.ts b/demo/scripts/controlsV2/demoButtons/tableOptionsButton.ts new file mode 100644 index 00000000000..d5be61d9517 --- /dev/null +++ b/demo/scripts/controlsV2/demoButtons/tableOptionsButton.ts @@ -0,0 +1,53 @@ +import { formatTable, getFormatState } from 'roosterjs-content-model-api'; +import { TableMetadataFormat } from 'roosterjs-content-model-types'; +import type { RibbonButton } from '../roosterjsReact/ribbon'; + +const TableEditOperationMap: Partial> = { + menuNameTableSetHeaderRow: 'hasHeaderRow', + menuNameTableSetFirstColumn: 'hasFirstColumn', + menuNameTableSetBandedColumns: 'hasBandedColumns', + menuNameTableSetBandedRows: 'hasBandedRows', +}; + +/** + * Key of localized strings of Table Options menu items + */ +type TableOptionsMenuItemStringKey = + | 'menuNameTableSetHeaderRow' + | 'menuNameTableSetFirstColumn' + | 'menuNameTableSetBandedColumns' + | 'menuNameTableSetBandedRows'; + +export const tableOptionsButton: RibbonButton< + 'ribbonButtonTableOptions' | TableOptionsMenuItemStringKey +> = { + key: 'ribbonButtonTableOptions', + iconName: '', + unlocalizedText: 'Options', + isDisabled: formatState => !formatState.isInTable, + dropDownMenu: { + items: { + menuNameTableSetHeaderRow: 'Header Row', + menuNameTableSetFirstColumn: 'First Column', + menuNameTableSetBandedColumns: 'Banded Columns', + menuNameTableSetBandedRows: 'Banded Rows', + }, + }, + onClick: (editor, key) => { + if (key != 'ribbonButtonTableOptions') { + const format = getFormatState(editor); + const tableFormatProperty = TableEditOperationMap[key]; + formatTable( + editor, + { [tableFormatProperty]: !format.tableFormat[tableFormatProperty] }, + true /*keepCellShade*/ + ); + } + }, + commandBarProperties: { + iconOnly: false, + }, +}; diff --git a/demo/scripts/controlsV2/tabs/ribbonButtons.ts b/demo/scripts/controlsV2/tabs/ribbonButtons.ts index 1a3a504156b..c2879edc086 100644 --- a/demo/scripts/controlsV2/tabs/ribbonButtons.ts +++ b/demo/scripts/controlsV2/tabs/ribbonButtons.ts @@ -37,7 +37,6 @@ import { setBulletedListStyleButton } from '../demoButtons/setBulletedListStyleB import { setHeadingLevelButton } from '../roosterjsReact/ribbon/buttons/setHeadingLevelButton'; import { setNumberedListStyleButton } from '../demoButtons/setNumberedListStyleButton'; import { setTableCellShadeButton } from '../demoButtons/setTableCellShadeButton'; -import { setTableHeaderButton } from '../demoButtons/setTableHeaderButton'; import { spaceAfterButton, spaceBeforeButton } from '../demoButtons/spaceBeforeAfterButtons'; import { spacingButton } from '../demoButtons/spacingButton'; import { strikethroughButton } from '../roosterjsReact/ribbon/buttons/strikethroughButton'; @@ -47,6 +46,7 @@ import { tableBorderApplyButton } from '../demoButtons/tableBorderApplyButton'; import { tableBorderColorButton } from '../demoButtons/tableBorderColorButton'; import { tableBorderStyleButton } from '../demoButtons/tableBorderStyleButton'; import { tableBorderWidthButton } from '../demoButtons/tableBorderWidthButton'; +import { tableOptionsButton } from '../demoButtons/tableOptionsButton'; import { tabNames } from './getTabs'; import { textColorButton } from '../roosterjsReact/ribbon/buttons/textColorButton'; import { underlineButton } from '../roosterjsReact/ribbon/buttons/underlineButton'; @@ -79,7 +79,7 @@ const tableButtons: RibbonButton[] = [ insertTableButton, formatTableButton, setTableCellShadeButton, - setTableHeaderButton, + tableOptionsButton, tableInsertButton, tableDeleteButton, tableBorderApplyButton, @@ -169,7 +169,7 @@ const allButtons: RibbonButton[] = [ listStartNumberButton, formatTableButton, setTableCellShadeButton, - setTableHeaderButton, + tableOptionsButton, tableInsertButton, tableDeleteButton, tableMergeButton, diff --git a/packages/roosterjs-content-model-api/lib/publicApi/table/applyTableBorderFormat.ts b/packages/roosterjs-content-model-api/lib/publicApi/table/applyTableBorderFormat.ts index 83a98fd84e7..55262c39c5e 100644 --- a/packages/roosterjs-content-model-api/lib/publicApi/table/applyTableBorderFormat.ts +++ b/packages/roosterjs-content-model-api/lib/publicApi/table/applyTableBorderFormat.ts @@ -2,7 +2,10 @@ import { extractBorderValues, getFirstSelectedTable, getSelectedCells, + getTableMetadata, + hasMetadata, parseValueWithUnit, + setFirstColumnFormatBorders, updateTableCellMetadata, } from 'roosterjs-content-model-dom'; import type { @@ -366,6 +369,12 @@ export function applyTableBorderFormat( modifyPerimeter(tableModel, sel, borderFormat, perimeter, isRtl); } + const tableMeta = hasMetadata(tableModel) ? getTableMetadata(tableModel) : {}; + if (tableMeta) { + // Enforce first column format if necessary + setFirstColumnFormatBorders(tableModel.rows, tableMeta); + } + return true; } else { return false; diff --git a/packages/roosterjs-content-model-api/test/publicApi/table/applyTableBorderFormatTest.ts b/packages/roosterjs-content-model-api/test/publicApi/table/applyTableBorderFormatTest.ts index a2e23423c36..20b9aba5603 100644 --- a/packages/roosterjs-content-model-api/test/publicApi/table/applyTableBorderFormatTest.ts +++ b/packages/roosterjs-content-model-api/test/publicApi/table/applyTableBorderFormatTest.ts @@ -26,7 +26,7 @@ describe('applyTableBorderFormat', () => { format?: ContentModelTableCell['format'] ) { // Create a table with all cells selected except the first and last row and column - const table = createTable(rows); + const table: ContentModelTable = createTable(rows); for (let i = 0; i < rows; i++) { const row = table.rows[i]; for (let j = 0; j < columns; j++) { diff --git a/packages/roosterjs-content-model-dom/lib/index.ts b/packages/roosterjs-content-model-dom/lib/index.ts index 202b32ddc95..00eaa77bd95 100644 --- a/packages/roosterjs-content-model-dom/lib/index.ts +++ b/packages/roosterjs-content-model-dom/lib/index.ts @@ -124,7 +124,7 @@ export { mergeModel } from './modelApi/editing/mergeModel'; export { deleteSelection } from './modelApi/editing/deleteSelection'; export { deleteSegment } from './modelApi/editing/deleteSegment'; export { deleteBlock } from './modelApi/editing/deleteBlock'; -export { applyTableFormat } from './modelApi/editing/applyTableFormat'; +export { applyTableFormat, setFirstColumnFormatBorders } from './modelApi/editing/applyTableFormat'; export { normalizeTable, MIN_ALLOWED_TABLE_CELL_WIDTH } from './modelApi/editing/normalizeTable'; export { setTableCellBackgroundColor } from './modelApi/editing/setTableCellBackgroundColor'; export { retrieveModelFormatState } from './modelApi/editing/retrieveModelFormatState'; diff --git a/packages/roosterjs-content-model-dom/lib/modelApi/editing/applyTableFormat.ts b/packages/roosterjs-content-model-dom/lib/modelApi/editing/applyTableFormat.ts index 7ef33b0ce18..28b51645fc9 100644 --- a/packages/roosterjs-content-model-dom/lib/modelApi/editing/applyTableFormat.ts +++ b/packages/roosterjs-content-model-dom/lib/modelApi/editing/applyTableFormat.ts @@ -58,7 +58,7 @@ export function applyTableFormat( clearCache(rows); formatCells(rows, effectiveMetadata, metaOverrides); - setFirstColumnFormat(rows, effectiveMetadata, metaOverrides); + setFirstColumnFormatBorders(rows, effectiveMetadata); setHeaderRowFormat(rows, effectiveMetadata, metaOverrides); return effectiveMetadata; }); @@ -176,7 +176,7 @@ function formatCells( format: TableMetadataFormat, metaOverrides: MetaOverrides ) { - const { hasBandedRows, hasBandedColumns, bgColorOdd, bgColorEven } = format; + const { hasBandedRows, hasBandedColumns, bgColorOdd, bgColorEven, hasFirstColumn } = format; rows.forEach((row, rowIndex) => { row.cells.forEach((cell, colIndex) => { @@ -212,14 +212,18 @@ function formatCells( // Format Background Color if (!metaOverrides.bgColorOverrides[rowIndex][colIndex]) { - const color = - hasBandedRows || hasBandedColumns - ? (hasBandedColumns && colIndex % 2 != 0) || - (hasBandedRows && rowIndex % 2 != 0) - ? bgColorOdd - : bgColorEven - : bgColorEven; /* bgColorEven is the default color */ - + let color: string | null | undefined; + if (hasFirstColumn && colIndex == 0) { + color = null; + } else { + color = + hasBandedRows || hasBandedColumns + ? (hasBandedColumns && colIndex % 2 != 0) || + (hasBandedRows && rowIndex % 2 != 0) + ? bgColorOdd + : bgColorEven + : bgColorEven; /* bgColorEven is the default color */ + } setTableCellBackgroundColor( cell, color, @@ -232,35 +236,46 @@ function formatCells( if (format.verticalAlign && !metaOverrides.vAlignOverrides[rowIndex][colIndex]) { cell.format.verticalAlign = format.verticalAlign; } + + // Format Header + cell.isHeader = false; }); }); } -function setFirstColumnFormat( +/** + * Set the first column format borders for the table + * @param rows The rows of the table + * @param format The table metadata format + */ +export function setFirstColumnFormatBorders( rows: ContentModelTableRow[], - format: Partial, - metaOverrides: MetaOverrides + format: Partial ) { + // Exit early hasFirstColumn is not set + if (!format.hasFirstColumn) { + return; + } + rows.forEach((row, rowIndex) => { row.cells.forEach((cell, cellIndex) => { - if (format.hasFirstColumn && cellIndex === 0) { + if (cellIndex == 0) { cell.isHeader = true; - if (rowIndex !== 0 && !metaOverrides.bgColorOverrides[rowIndex][cellIndex]) { - setBorderColor(cell.format, 'borderTop'); - setTableCellBackgroundColor( - cell, - null /*color*/, - false /*isColorOverride*/, - true /*applyToSegments*/ - ); - } - - if (rowIndex !== rows.length - 1 && rowIndex !== 0) { - setBorderColor(cell.format, 'borderBottom'); + switch (rowIndex) { + case 0: + break; + case 1: + setBorderColor(cell.format, 'borderBottom'); + break; + case rows.length - 1: + setBorderColor(cell.format, 'borderTop'); + break; + default: + setBorderColor(cell.format, 'borderTop'); + setBorderColor(cell.format, 'borderBottom'); + break; } - } else { - cell.isHeader = false; } }); }); @@ -271,12 +286,17 @@ function setHeaderRowFormat( format: TableMetadataFormat, metaOverrides: MetaOverrides ) { + // Exit early if hasHeaderRow is not set + if (!format.hasHeaderRow) { + return; + } + const rowIndex = 0; rows[rowIndex]?.cells.forEach((cell, cellIndex) => { - cell.isHeader = format.hasHeaderRow; + cell.isHeader = true; - if (format.hasHeaderRow && format.headerRowColor) { + if (format.headerRowColor) { if (!metaOverrides.bgColorOverrides[rowIndex][cellIndex]) { setTableCellBackgroundColor( cell, @@ -293,6 +313,11 @@ function setHeaderRowFormat( }); } +/** + * @param format The cell format to set the border color + * @param key The border key to set the color + * @param value The color to set. If not given, it removes the color and sets the style to transparent + */ function setBorderColor(format: BorderFormat, key: keyof BorderFormat, value?: string) { const border = extractBorderValues(format[key]); border.color = value || ''; diff --git a/packages/roosterjs-content-model-types/lib/contentModel/format/metadata/TableMetadataFormat.ts b/packages/roosterjs-content-model-types/lib/contentModel/format/metadata/TableMetadataFormat.ts index 394581cc8f7..9e6090779b0 100644 --- a/packages/roosterjs-content-model-types/lib/contentModel/format/metadata/TableMetadataFormat.ts +++ b/packages/roosterjs-content-model-types/lib/contentModel/format/metadata/TableMetadataFormat.ts @@ -56,6 +56,7 @@ export type TableMetadataFormat = { * Table Borders Type. Use value of constant TableBorderFormat as value */ tableBorderFormat?: number; + /** * Vertical alignment for each row */