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 {