From e7f2e3ebda39e77b2d9fd1e9eeb99eabb7d39a00 Mon Sep 17 00:00:00 2001 From: Ivaylo Pavlov Date: Sun, 12 Jan 2025 19:51:14 +0000 Subject: [PATCH 1/9] Support table alignment --- .../lexical-table/src/LexicalTableNode.ts | 22 +++++++++++-- .../src/LexicalTableSelectionHelpers.ts | 33 ++++++++++++++++++- 2 files changed, 52 insertions(+), 3 deletions(-) diff --git a/packages/lexical-table/src/LexicalTableNode.ts b/packages/lexical-table/src/LexicalTableNode.ts index d2960ab40ef..0d47339979f 100644 --- a/packages/lexical-table/src/LexicalTableNode.ts +++ b/packages/lexical-table/src/LexicalTableNode.ts @@ -22,6 +22,7 @@ import { DOMExportOutput, EditorConfig, ElementDOMSlot, + type ElementFormatType, ElementNode, LexicalEditor, LexicalNode, @@ -77,7 +78,7 @@ function setRowStriping( dom: HTMLElement, config: EditorConfig, rowStriping: boolean, -) { +): void { if (rowStriping) { addClassNamesToElement(dom, config.theme.tableRowStriping); dom.setAttribute('data-lexical-row-striping', 'true'); @@ -87,6 +88,20 @@ function setRowStriping( } } +function alignTableElement( + dom: HTMLElement, + formatType: ElementFormatType, +): void { + if (formatType === 'center') { + dom.style.marginLeft = 'auto'; + dom.style.marginRight = 'auto'; + } else if (formatType === 'right') { + dom.style.marginLeft = 'auto'; + } else { + dom.style.marginLeft = ''; + } +} + const scrollableEditors = new WeakSet(); export function $isScrollableTablesActive( @@ -211,6 +226,9 @@ export class TableNode extends ElementNode { setDOMUnmanaged(colGroup); addClassNamesToElement(tableElement, config.theme.table); + if (this.__format) { + alignTableElement(tableElement, this.getFormatType()); + } if (this.__rowStriping) { setRowStriping(tableElement, config, true); } @@ -234,7 +252,7 @@ export class TableNode extends ElementNode { setRowStriping(dom, config, this.__rowStriping); } updateColgroup(dom, config, this.getColumnCount(), this.getColWidths()); - return false; + return prevNode.__format !== this.__format; } exportDOM(editor: LexicalEditor): DOMExportOutput { diff --git a/packages/lexical-table/src/LexicalTableSelectionHelpers.ts b/packages/lexical-table/src/LexicalTableSelectionHelpers.ts index 970ddd17132..cbed096bc47 100644 --- a/packages/lexical-table/src/LexicalTableSelectionHelpers.ts +++ b/packages/lexical-table/src/LexicalTableSelectionHelpers.ts @@ -82,7 +82,7 @@ import { TableNode, } from './LexicalTableNode'; import {TableDOMTable, TableObserver} from './LexicalTableObserver'; -import {$isTableRowNode} from './LexicalTableRowNode'; +import {$isTableRowNode, type TableRowNode} from './LexicalTableRowNode'; import {$isTableSelection} from './LexicalTableSelection'; import { $computeTableCellRectBoundary, @@ -563,6 +563,12 @@ export function applyTableHandlers( return false; } + // Align table if exact full table selection + if ($isFullTableSelection(selection, tableNode)) { + tableNode.setFormat(formatType); + return true; + } + const [tableMap, anchorCell, focusCell] = $computeTableMap( tableNode, anchorNode, @@ -1580,6 +1586,31 @@ function $isSelectionInTable( return false; } +function $isFullTableSelection( + selection: null | BaseSelection, + tableNode: TableNode, +): boolean { + if ($isTableSelection(selection)) { + const anchorNode = selection.anchor.getNode(); + const focusNode = selection.focus.getNode(); + if (tableNode && anchorNode && focusNode) { + const firstRow = tableNode.getFirstChild(); + const lastRow = tableNode.getLastChild(); + if (firstRow && lastRow) { + const firstCell = firstRow.getFirstChild(); + const lastCell = lastRow.getLastChild(); + if (firstCell && lastCell) { + return ( + anchorNode.getKey() === firstCell.getKey() && + focusNode.getKey() === lastCell.getKey() + ); + } + } + } + } + return false; +} + function selectTableCellNode(tableCell: TableCellNode, fromStart: boolean) { if (fromStart) { tableCell.selectStart(); From 1f07b1b6ba8b05be77f00f04af0b9ca2256cbac4 Mon Sep 17 00:00:00 2001 From: Ivaylo Pavlov Date: Sun, 12 Jan 2025 20:06:25 +0000 Subject: [PATCH 2/9] ExportDOM and test --- .../lexical-table/src/LexicalTableNode.ts | 8 ++- .../src/LexicalTableSelectionHelpers.ts | 2 +- .../__tests__/unit/LexicalTableNode.test.tsx | 64 +++++++++++++++++++ 3 files changed, 72 insertions(+), 2 deletions(-) diff --git a/packages/lexical-table/src/LexicalTableNode.ts b/packages/lexical-table/src/LexicalTableNode.ts index 0d47339979f..c273a49f2fd 100644 --- a/packages/lexical-table/src/LexicalTableNode.ts +++ b/packages/lexical-table/src/LexicalTableNode.ts @@ -89,9 +89,12 @@ function setRowStriping( } function alignTableElement( - dom: HTMLElement, + dom: HTMLElement | Text | null | undefined, formatType: ElementFormatType, ): void { + if (!dom) { + return; + } if (formatType === 'center') { dom.style.marginLeft = 'auto'; dom.style.marginRight = 'auto'; @@ -262,6 +265,9 @@ export class TableNode extends ElementNode { after: (tableElement) => { if (superExport.after) { tableElement = superExport.after(tableElement); + if (this.__format) { + alignTableElement(tableElement, this.getFormatType()); + } } if (isHTMLElement(tableElement) && tableElement.nodeName !== 'TABLE') { tableElement = tableElement.querySelector('table'); diff --git a/packages/lexical-table/src/LexicalTableSelectionHelpers.ts b/packages/lexical-table/src/LexicalTableSelectionHelpers.ts index cbed096bc47..a3df143f6ab 100644 --- a/packages/lexical-table/src/LexicalTableSelectionHelpers.ts +++ b/packages/lexical-table/src/LexicalTableSelectionHelpers.ts @@ -563,7 +563,7 @@ export function applyTableHandlers( return false; } - // Align table if exact full table selection + // Align the table if the entire table is selected if ($isFullTableSelection(selection, tableNode)) { tableNode.setFormat(formatType); return true; diff --git a/packages/lexical-table/src/__tests__/unit/LexicalTableNode.test.tsx b/packages/lexical-table/src/__tests__/unit/LexicalTableNode.test.tsx index c5f208c51f8..3f8e29ab38a 100644 --- a/packages/lexical-table/src/__tests__/unit/LexicalTableNode.test.tsx +++ b/packages/lexical-table/src/__tests__/unit/LexicalTableNode.test.tsx @@ -1040,6 +1040,70 @@ describe('LexicalTableNode tests', () => { }); }); + test('Change Table-level alignment', async () => { + const {editor} = testEnv; + + await editor.update(() => { + const root = $getRoot(); + const table = $createTableNodeWithDimensions(4, 4, true); + root.append(table); + }); + await editor.update(() => { + const root = $getRoot(); + const table = root.getLastChild(); + if (table) { + table.setFormat('center'); + } + }); + + await editor.update(() => { + const root = $getRoot(); + const table = root.getLastChild(); + expectTableHtmlToBeEqual( + table!.createDOM(editorConfig).outerHTML, + html` + + + + + + + +
+ `, + ); + }); + + await editor.update(() => { + const root = $getRoot(); + const table = root.getLastChild(); + if (table) { + table.setFormat('left'); + } + }); + + await editor.update(() => { + const root = $getRoot(); + const table = root.getLastChild(); + expectTableHtmlToBeEqual( + table!.createDOM(editorConfig).outerHTML, + html` + + + + + + + +
+ `, + ); + }); + }); + test('Update column widths', async () => { const {editor} = testEnv; From f628d54b3e42764c27513cf79aed7ea94fd19a7a Mon Sep 17 00:00:00 2001 From: Ivaylo Pavlov Date: Sun, 12 Jan 2025 20:11:49 +0000 Subject: [PATCH 3/9] type --- packages/lexical-table/src/LexicalTableNode.ts | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/packages/lexical-table/src/LexicalTableNode.ts b/packages/lexical-table/src/LexicalTableNode.ts index c273a49f2fd..8ede2ab14da 100644 --- a/packages/lexical-table/src/LexicalTableNode.ts +++ b/packages/lexical-table/src/LexicalTableNode.ts @@ -89,7 +89,7 @@ function setRowStriping( } function alignTableElement( - dom: HTMLElement | Text | null | undefined, + dom: HTMLElement, formatType: ElementFormatType, ): void { if (!dom) { @@ -266,7 +266,10 @@ export class TableNode extends ElementNode { if (superExport.after) { tableElement = superExport.after(tableElement); if (this.__format) { - alignTableElement(tableElement, this.getFormatType()); + alignTableElement( + tableElement as HTMLElement, + this.getFormatType(), + ); } } if (isHTMLElement(tableElement) && tableElement.nodeName !== 'TABLE') { From fc0baf136f473b2f25dbeba46ba7f4f71050c8ae Mon Sep 17 00:00:00 2001 From: Ivaylo Pavlov Date: Sun, 12 Jan 2025 20:17:22 +0000 Subject: [PATCH 4/9] fix test --- .../lexical-table/src/__tests__/unit/LexicalTableNode.test.tsx | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/packages/lexical-table/src/__tests__/unit/LexicalTableNode.test.tsx b/packages/lexical-table/src/__tests__/unit/LexicalTableNode.test.tsx index 3f8e29ab38a..6375b5b1237 100644 --- a/packages/lexical-table/src/__tests__/unit/LexicalTableNode.test.tsx +++ b/packages/lexical-table/src/__tests__/unit/LexicalTableNode.test.tsx @@ -1064,8 +1064,7 @@ describe('LexicalTableNode tests', () => { html` + style="margin-left: auto; margin-right: auto"> From 5bb76a66b82dc9516ff5319b9235961c81564715 Mon Sep 17 00:00:00 2001 From: Ivaylo Pavlov Date: Sun, 12 Jan 2025 23:34:47 +0000 Subject: [PATCH 5/9] Do via theme classes vs style override --- .../src/themes/PlaygroundEditorTheme.css | 7 +++++++ .../src/themes/PlaygroundEditorTheme.ts | 4 ++++ packages/lexical-table/src/LexicalTableNode.ts | 17 ++++++++++++----- .../__tests__/unit/LexicalTableNode.test.tsx | 8 ++++++-- 4 files changed, 29 insertions(+), 7 deletions(-) diff --git a/packages/lexical-playground/src/themes/PlaygroundEditorTheme.css b/packages/lexical-playground/src/themes/PlaygroundEditorTheme.css index b5306f0b691..c0fa7d1fc80 100644 --- a/packages/lexical-playground/src/themes/PlaygroundEditorTheme.css +++ b/packages/lexical-playground/src/themes/PlaygroundEditorTheme.css @@ -196,6 +196,13 @@ /* Remove the table's margin and put it on the wrapper */ margin: 0; } +.PlaygroundEditorTheme__tableAlignmentCenter { + margin-left: auto !important; + margin-right: auto !important; +} +.PlaygroundEditorTheme__tableAlignmentRight { + margin-left: auto !important; +} .PlaygroundEditorTheme__table { border-collapse: collapse; border-spacing: 0; diff --git a/packages/lexical-playground/src/themes/PlaygroundEditorTheme.ts b/packages/lexical-playground/src/themes/PlaygroundEditorTheme.ts index e7c6a4aab7e..06fc67b44d3 100644 --- a/packages/lexical-playground/src/themes/PlaygroundEditorTheme.ts +++ b/packages/lexical-playground/src/themes/PlaygroundEditorTheme.ts @@ -93,6 +93,10 @@ const theme: EditorThemeClasses = { specialText: 'PlaygroundEditorTheme__specialText', tab: 'PlaygroundEditorTheme__tabNode', table: 'PlaygroundEditorTheme__table', + tableAlignment: { + center: 'PlaygroundEditorTheme__tableAlignmentCenter', + right: 'PlaygroundEditorTheme__tableAlignmentRight', + }, tableCell: 'PlaygroundEditorTheme__tableCell', tableCellActionButton: 'PlaygroundEditorTheme__tableCellActionButton', tableCellActionButtonContainer: diff --git a/packages/lexical-table/src/LexicalTableNode.ts b/packages/lexical-table/src/LexicalTableNode.ts index 8ede2ab14da..940737a098c 100644 --- a/packages/lexical-table/src/LexicalTableNode.ts +++ b/packages/lexical-table/src/LexicalTableNode.ts @@ -90,18 +90,24 @@ function setRowStriping( function alignTableElement( dom: HTMLElement, + config: EditorConfig, formatType: ElementFormatType, ): void { if (!dom) { return; } if (formatType === 'center') { - dom.style.marginLeft = 'auto'; - dom.style.marginRight = 'auto'; + addClassNamesToElement(dom, config.theme.tableAlignment.center); } else if (formatType === 'right') { - dom.style.marginLeft = 'auto'; + addClassNamesToElement(dom, config.theme.tableAlignment.right); } else { - dom.style.marginLeft = ''; + removeClassNamesFromElement( + dom, + ...[ + config.theme.tableAlignment.center, + config.theme.tableAlignment.right, + ], + ); } } @@ -230,7 +236,7 @@ export class TableNode extends ElementNode { addClassNamesToElement(tableElement, config.theme.table); if (this.__format) { - alignTableElement(tableElement, this.getFormatType()); + alignTableElement(tableElement, config, this.getFormatType()); } if (this.__rowStriping) { setRowStriping(tableElement, config, true); @@ -268,6 +274,7 @@ export class TableNode extends ElementNode { if (this.__format) { alignTableElement( tableElement as HTMLElement, + editor._config, this.getFormatType(), ); } diff --git a/packages/lexical-table/src/__tests__/unit/LexicalTableNode.test.tsx b/packages/lexical-table/src/__tests__/unit/LexicalTableNode.test.tsx index 6375b5b1237..136516733f2 100644 --- a/packages/lexical-table/src/__tests__/unit/LexicalTableNode.test.tsx +++ b/packages/lexical-table/src/__tests__/unit/LexicalTableNode.test.tsx @@ -73,6 +73,10 @@ const editorConfig = Object.freeze({ namespace: '', theme: { table: 'test-table-class', + tableAlignment: { + center: 'test-table-alignment-center', + right: 'test-table-alignment-right', + }, tableRowStriping: 'test-table-row-striping-class', tableScrollableWrapper: 'table-scrollable-wrapper', }, @@ -1063,8 +1067,8 @@ describe('LexicalTableNode tests', () => { table!.createDOM(editorConfig).outerHTML, html`
+ class="${editorConfig.theme.table} ${editorConfig.theme + .tableAlignment.center}"> From 3fed32da42531e0408d2d9cfa9bee2b2ac8520ac Mon Sep 17 00:00:00 2001 From: Ivaylo Pavlov Date: Mon, 13 Jan 2025 23:35:26 +0000 Subject: [PATCH 6/9] Implement PR comments --- .../src/themes/PlaygroundEditorTheme.css | 5 +-- .../lexical-table/src/LexicalTableNode.ts | 36 +++++++++---------- .../src/LexicalTableSelectionHelpers.ts | 23 +++++------- 3 files changed, 28 insertions(+), 36 deletions(-) diff --git a/packages/lexical-playground/src/themes/PlaygroundEditorTheme.css b/packages/lexical-playground/src/themes/PlaygroundEditorTheme.css index c0fa7d1fc80..33a750d3c38 100644 --- a/packages/lexical-playground/src/themes/PlaygroundEditorTheme.css +++ b/packages/lexical-playground/src/themes/PlaygroundEditorTheme.css @@ -193,8 +193,9 @@ margin: 0px 25px 30px 0px; } .PlaygroundEditorTheme__tableScrollableWrapper > .PlaygroundEditorTheme__table { - /* Remove the table's margin and put it on the wrapper */ - margin: 0; + /* Remove the table's vertical margin and put it on the wrapper */ + margin-top: 0; + margin-bottom: 0; } .PlaygroundEditorTheme__tableAlignmentCenter { margin-left: auto !important; diff --git a/packages/lexical-table/src/LexicalTableNode.ts b/packages/lexical-table/src/LexicalTableNode.ts index 940737a098c..cc070c3ccb2 100644 --- a/packages/lexical-table/src/LexicalTableNode.ts +++ b/packages/lexical-table/src/LexicalTableNode.ts @@ -93,22 +93,17 @@ function alignTableElement( config: EditorConfig, formatType: ElementFormatType, ): void { - if (!dom) { - return; - } - if (formatType === 'center') { - addClassNamesToElement(dom, config.theme.tableAlignment.center); - } else if (formatType === 'right') { - addClassNamesToElement(dom, config.theme.tableAlignment.right); - } else { - removeClassNamesFromElement( - dom, - ...[ - config.theme.tableAlignment.center, - config.theme.tableAlignment.right, - ], - ); + const removeClasses: string[] = []; + const addClasses: string[] = []; + for (const format of ['center', 'right'] as const) { + const classes = config.theme.tableAlignment[format]; + if (!classes) { + continue; + } + (format === formatType ? addClasses : removeClasses).push(classes); } + removeClassNamesFromElement(dom, ...removeClasses); + addClassNamesToElement(dom, ...addClasses); } const scrollableEditors = new WeakSet(); @@ -235,9 +230,7 @@ export class TableNode extends ElementNode { setDOMUnmanaged(colGroup); addClassNamesToElement(tableElement, config.theme.table); - if (this.__format) { - alignTableElement(tableElement, config, this.getFormatType()); - } + alignTableElement(tableElement, config, this.getFormatType()); if (this.__rowStriping) { setRowStriping(tableElement, config, true); } @@ -261,7 +254,12 @@ export class TableNode extends ElementNode { setRowStriping(dom, config, this.__rowStriping); } updateColgroup(dom, config, this.getColumnCount(), this.getColWidths()); - return prevNode.__format !== this.__format; + alignTableElement( + this.getDOMSlot(dom).element, + config, + this.getFormatType(), + ); + return false; } exportDOM(editor: LexicalEditor): DOMExportOutput { diff --git a/packages/lexical-table/src/LexicalTableSelectionHelpers.ts b/packages/lexical-table/src/LexicalTableSelectionHelpers.ts index a3df143f6ab..2aadd81723c 100644 --- a/packages/lexical-table/src/LexicalTableSelectionHelpers.ts +++ b/packages/lexical-table/src/LexicalTableSelectionHelpers.ts @@ -82,7 +82,7 @@ import { TableNode, } from './LexicalTableNode'; import {TableDOMTable, TableObserver} from './LexicalTableObserver'; -import {$isTableRowNode, type TableRowNode} from './LexicalTableRowNode'; +import {$isTableRowNode} from './LexicalTableRowNode'; import {$isTableSelection} from './LexicalTableSelection'; import { $computeTableCellRectBoundary, @@ -1591,21 +1591,14 @@ function $isFullTableSelection( tableNode: TableNode, ): boolean { if ($isTableSelection(selection)) { - const anchorNode = selection.anchor.getNode(); - const focusNode = selection.focus.getNode(); + const anchorNode = selection.anchor.getNode() as TableCellNode; + const focusNode = selection.focus.getNode() as TableCellNode; if (tableNode && anchorNode && focusNode) { - const firstRow = tableNode.getFirstChild(); - const lastRow = tableNode.getLastChild(); - if (firstRow && lastRow) { - const firstCell = firstRow.getFirstChild(); - const lastCell = lastRow.getLastChild(); - if (firstCell && lastCell) { - return ( - anchorNode.getKey() === firstCell.getKey() && - focusNode.getKey() === lastCell.getKey() - ); - } - } + const [map] = $computeTableMap(tableNode, anchorNode, focusNode); + return ( + anchorNode.getKey() === map[0][0].cell.getKey() && + focusNode.getKey() === map[map.length - 1].at(-1)!.cell.getKey() + ); } } return false; From 29617ec4202a4051ed37d09e40a244fcb78615f5 Mon Sep 17 00:00:00 2001 From: Ivaylo Pavlov Date: Mon, 13 Jan 2025 23:48:33 +0000 Subject: [PATCH 7/9] Remove important --- .../src/themes/PlaygroundEditorTheme.css | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/packages/lexical-playground/src/themes/PlaygroundEditorTheme.css b/packages/lexical-playground/src/themes/PlaygroundEditorTheme.css index 33a750d3c38..ffb38afdc6d 100644 --- a/packages/lexical-playground/src/themes/PlaygroundEditorTheme.css +++ b/packages/lexical-playground/src/themes/PlaygroundEditorTheme.css @@ -198,11 +198,11 @@ margin-bottom: 0; } .PlaygroundEditorTheme__tableAlignmentCenter { - margin-left: auto !important; - margin-right: auto !important; + margin-left: auto; + margin-right: auto; } .PlaygroundEditorTheme__tableAlignmentRight { - margin-left: auto !important; + margin-left: auto; } .PlaygroundEditorTheme__table { border-collapse: collapse; @@ -211,7 +211,8 @@ overflow-x: scroll; table-layout: fixed; width: fit-content; - margin: 0px 25px 30px 0px; + margin-top: 25px; + margin-bottom: 30px; } .PlaygroundEditorTheme__tableRowStriping tr:nth-child(even) { background-color: #f2f5fb; From c73d69aeb5a8b00a5aa52909f781db93409eca38 Mon Sep 17 00:00:00 2001 From: Ivaylo Pavlov Date: Tue, 14 Jan 2025 21:48:46 +0000 Subject: [PATCH 8/9] Start tests fix --- packages/lexical/src/__tests__/unit/LexicalEditor.test.tsx | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/packages/lexical/src/__tests__/unit/LexicalEditor.test.tsx b/packages/lexical/src/__tests__/unit/LexicalEditor.test.tsx index a9a3eed4a60..6c7108c43ea 100644 --- a/packages/lexical/src/__tests__/unit/LexicalEditor.test.tsx +++ b/packages/lexical/src/__tests__/unit/LexicalEditor.test.tsx @@ -103,6 +103,10 @@ describe('LexicalEditor tests', () => { nodes: nodes ?? [], onError: onError || jest.fn(), theme: { + tableAlignment: { + center: 'editor-table-alignment-center', + right: 'editor-table-alignment-right', + }, text: { bold: 'editor-text-bold', italic: 'editor-text-italic', From 4584f6560713ca70ee7b6690acaad5120a6b440d Mon Sep 17 00:00:00 2001 From: Ivaylo Pavlov Date: Tue, 14 Jan 2025 22:02:58 +0000 Subject: [PATCH 9/9] Tests fix 2 --- packages/lexical-table/src/LexicalTableNode.ts | 3 +++ 1 file changed, 3 insertions(+) diff --git a/packages/lexical-table/src/LexicalTableNode.ts b/packages/lexical-table/src/LexicalTableNode.ts index cc070c3ccb2..1069c072e7b 100644 --- a/packages/lexical-table/src/LexicalTableNode.ts +++ b/packages/lexical-table/src/LexicalTableNode.ts @@ -93,6 +93,9 @@ function alignTableElement( config: EditorConfig, formatType: ElementFormatType, ): void { + if (!config.theme.tableAlignment) { + return; + } const removeClasses: string[] = []; const addClasses: string[] = []; for (const format of ['center', 'right'] as const) {