diff --git a/resources/icons/editor/table-header.svg b/resources/icons/editor/table-header.svg new file mode 100644 index 000000000..e3cfcbb63 --- /dev/null +++ b/resources/icons/editor/table-header.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/resources/js/wysiwyg/lexical/table/LexicalTableCellNode.ts b/resources/js/wysiwyg/lexical/table/LexicalTableCellNode.ts index 1c9d7ecf6..0e714e70f 100644 --- a/resources/js/wysiwyg/lexical/table/LexicalTableCellNode.ts +++ b/resources/js/wysiwyg/lexical/table/LexicalTableCellNode.ts @@ -334,10 +334,13 @@ export function $convertTableCellNodeElement( width = parseFloat(domNode_.style.width); } + let isHeader = nodeName === 'th'; + if (domNode instanceof HTMLElement && domNode.closest('thead')) { + isHeader = true; + } + const tableCellNode = $createTableCellNode( - nodeName === 'th' - ? TableCellHeaderStates.ROW - : TableCellHeaderStates.NO_STATUS, + isHeader ? TableCellHeaderStates.ROW : TableCellHeaderStates.NO_STATUS, domNode_.colSpan, width, ); diff --git a/resources/js/wysiwyg/ui/defaults/buttons/tables.ts b/resources/js/wysiwyg/ui/defaults/buttons/tables.ts index 2e4883d88..0e01b301c 100644 --- a/resources/js/wysiwyg/ui/defaults/buttons/tables.ts +++ b/resources/js/wysiwyg/ui/defaults/buttons/tables.ts @@ -7,6 +7,7 @@ import insertColumnAfterIcon from "@icons/editor/table-insert-column-after.svg"; import insertColumnBeforeIcon from "@icons/editor/table-insert-column-before.svg"; import insertRowAboveIcon from "@icons/editor/table-insert-row-above.svg"; import insertRowBelowIcon from "@icons/editor/table-insert-row-below.svg"; +import tableHeaderIcon from "@icons/editor/table-header.svg"; import {EditorUiContext} from "../../framework/core"; import {$getSelection, BaseSelection} from "lexical"; import { @@ -14,7 +15,7 @@ import { $deleteTableRow__EXPERIMENTAL, $insertTableColumn__EXPERIMENTAL, $insertTableRow__EXPERIMENTAL, $isTableCellNode, - $isTableNode, $isTableRowNode, $isTableSelection, $unmergeCell, TableCellNode, + $isTableNode, $isTableRowNode, $isTableSelection, $unmergeCell, TableCellHeaderStates, TableCellNode, } from "@lexical/table"; import {$getNodeFromSelection, $selectionContainsNodeType} from "../../../utils/selection"; import {$getParentOfType} from "../../../utils/nodes"; @@ -23,7 +24,7 @@ import { $clearTableFormatting, $clearTableSizes, $getTableFromSelection, $getTableRowsFromSelection, - $mergeTableCellsInSelection + $mergeTableCellsInSelection, $toggleRowCellHeaderState } from "../../../utils/tables"; import { $copySelectedColumnsToClipboard, @@ -239,6 +240,28 @@ export const pasteRowAfter: EditorButtonDefinition = { isDisabled: (selection) => cellNotSelected(selection) || isRowClipboardEmpty(), }; +export const toggleRowHeaders: EditorButtonDefinition = { + label: 'Row header', + format: 'small', + icon: tableHeaderIcon, + action(context: EditorUiContext, button) { + context.editor.update(() => { + const row = $getNodeFromSelection($getSelection(), $isTableCellNode)?.getParent(); + if (!$isTableRowNode(row)) { + return; + } + + const isNowHeader = $toggleRowCellHeaderState(row); + button.setActiveState(isNowHeader); + }); + }, + isActive: (selection) => { + return $selectionContainsNodeType(selection, (node) => { + return $isTableCellNode(node) && node.getHeaderStyles() !== TableCellHeaderStates.NO_STATUS; + }); + } +}; + export const cutColumn: EditorButtonDefinition = { label: 'Cut column', format: 'long', diff --git a/resources/js/wysiwyg/ui/defaults/toolbars.ts b/resources/js/wysiwyg/ui/defaults/toolbars.ts index a3ada5c89..99d3b96d4 100644 --- a/resources/js/wysiwyg/ui/defaults/toolbars.ts +++ b/resources/js/wysiwyg/ui/defaults/toolbars.ts @@ -28,7 +28,7 @@ import { pasteRowBefore, resizeTableToContents, rowProperties, splitCell, - table, tableProperties + table, tableProperties, toggleRowHeaders } from "./buttons/tables"; import {about, fullscreen, redo, source, undo} from "./buttons/controls"; import { @@ -284,6 +284,19 @@ export const contextToolbars: Record = { return originalTarget.closest('table') as HTMLTableElement; } }, + table_header: { + selector: 'table tr:first-of-type td, table tr:first-of-type th', + content() { + return [ + new EditorOverflowContainer('table_headers', 1, [ + new EditorButton(toggleRowHeaders), + ]), + ]; + }, + displayTargetLocator(originalTarget: HTMLElement): HTMLElement { + return originalTarget.closest('table') as HTMLTableElement; + } + }, details: { selector: 'details', content() { diff --git a/resources/js/wysiwyg/utils/tables.ts b/resources/js/wysiwyg/utils/tables.ts index 15cc3cbbe..b835614ab 100644 --- a/resources/js/wysiwyg/utils/tables.ts +++ b/resources/js/wysiwyg/utils/tables.ts @@ -3,7 +3,7 @@ import { $isTableCellNode, $isTableNode, $isTableRowNode, - $isTableSelection, TableCellNode, TableNode, + $isTableSelection, TableCellHeaderStates, TableCellNode, TableNode, TableRowNode, TableSelection, } from "@lexical/table"; @@ -328,9 +328,21 @@ export function $getCellPaddingForTable(table: TableNode): string { return padding || ''; } - - - +/** + * Toggle the header state of the cells in the provided row. + * Returns a boolean to indicate if the new state of the cells is as headers. + */ +export function $toggleRowCellHeaderState(row: TableRowNode): boolean { + const firstCell = row.getFirstChild(); + const isHeader = $isTableCellNode(firstCell) ? firstCell.getHeaderStyles() !== TableCellHeaderStates.NO_STATUS : false; + const cells = row.getChildren(); + for (const cell of cells) { + if ($isTableCellNode(cell)) { + cell.setHeaderStyles(isHeader ? TableCellHeaderStates.NO_STATUS : TableCellHeaderStates.ROW); + } + } + return !isHeader; +} diff --git a/resources/sass/_tables.scss b/resources/sass/_tables.scss index 16be32fb3..5ea520d73 100644 --- a/resources/sass/_tables.scss +++ b/resources/sass/_tables.scss @@ -4,10 +4,6 @@ table { min-width: 100px; max-width: 100%; - thead { - @include mixins.lightDark(background-color, #f8f8f8, #333); - font-weight: 500; - } td, th { min-width: 10px; padding: 6px 8px; @@ -22,6 +18,15 @@ table { } } +// Table Header styles. +// Initial selector for in-body th cells, intended to (somewhat) not conflict with +// previous approach (second selector) to table headers where +// we would target the thead specifically. +table:not(:has(thead)) th, table thead { + @include mixins.lightDark(background-color, #f8f8f8, #333); + font-weight: 500; +} + table.table { width: 100%; tr td, tr th {