return new CustomParagraphNode();
}
-export function $isCustomParagraphNode(node: LexicalNode | null | undefined) {
+export function $isCustomParagraphNode(node: LexicalNode | null | undefined): node is CustomParagraphNode {
return node instanceof CustomParagraphNode;
}
\ No newline at end of file
--- /dev/null
+import {EditorConfig} from "lexical/LexicalEditor";
+import {DOMExportOutput, LexicalEditor, LexicalNode, Spread} from "lexical";
+
+import {SerializedTableCellNode, TableCellHeaderStates, TableCellNode} from "@lexical/table";
+import {TableCellHeaderState} from "@lexical/table/LexicalTableCellNode";
+
+export type SerializedCustomTableCellNode = Spread<{
+ styles: Record<string, string>,
+}, SerializedTableCellNode>
+
+export class CustomTableCellNode extends TableCellNode {
+ __styles: Map<string, string> = new Map;
+
+ static getType(): string {
+ return 'custom-table-cell';
+ }
+
+ static clone(node: CustomTableCellNode): CustomTableCellNode {
+ const cellNode = new CustomTableCellNode(
+ node.__headerState,
+ node.__colSpan,
+ node.__width,
+ node.__key,
+ );
+ cellNode.__rowSpan = node.__rowSpan;
+ cellNode.__styles = new Map(node.__styles);
+ return cellNode;
+ }
+
+ getStyles(): Map<string, string> {
+ const self = this.getLatest();
+ return new Map(self.__styles);
+ }
+
+ setStyles(styles: Map<string, string>): void {
+ const self = this.getWritable();
+ self.__styles = new Map(styles);
+ }
+
+ updateTag(tag: string): void {
+ const isHeader = tag.toLowerCase() === 'th';
+ const state = isHeader ? TableCellHeaderStates.ROW : TableCellHeaderStates.NO_STATUS;
+ const self = this.getWritable();
+ self.__headerState = state;
+ }
+
+ createDOM(config: EditorConfig): HTMLElement {
+ const element = super.createDOM(config);
+
+ for (const [name, value] of this.__styles.entries()) {
+ element.style.setProperty(name, value);
+ }
+
+ return element;
+ }
+
+ // TODO - Import DOM
+
+ updateDOM(prevNode: CustomTableCellNode): boolean {
+ return super.updateDOM(prevNode)
+ || this.__styles !== prevNode.__styles;
+ }
+
+ exportDOM(editor: LexicalEditor): DOMExportOutput {
+ const element = this.createDOM(editor._config);
+ return {
+ element
+ };
+ }
+
+ exportJSON(): SerializedCustomTableCellNode {
+ return {
+ ...super.exportJSON(),
+ type: 'custom-table-cell',
+ styles: Object.fromEntries(this.__styles),
+ };
+ }
+}
+
+export function $createCustomTableCellNode(
+ headerState: TableCellHeaderState,
+ colSpan = 1,
+ width?: number,
+): CustomTableCellNode {
+ return new CustomTableCellNode(headerState, colSpan, width);
+}
+
+export function $isCustomTableCellNode(node: LexicalNode | null | undefined): node is CustomTableCellNode {
+ return node instanceof CustomTableCellNode;
+}
\ No newline at end of file
import {EditorUiContext} from "../ui/framework/core";
import {MediaNode} from "./media";
import {CustomListItemNode} from "./custom-list-item";
+import {CustomTableCellNode} from "./custom-table-cell-node";
/**
* Load the nodes for lexical.
CustomListItemNode,
CustomTableNode,
TableRowNode,
- TableCellNode,
+ CustomTableCellNode,
ImageNode,
HorizontalRuleNode,
DetailsNode, SummaryNode,
with: (node: ListItemNode) => {
return new CustomListItemNode(node.__value, node.__checked);
}
- }
+ },
+ {
+ replace: TableCellNode,
+ with: (node: TableCellNode) => {
+ const cell = new CustomTableCellNode(
+ node.__headerState,
+ node.__colSpan,
+ node.__width,
+ );
+ cell.__rowSpan = node.__rowSpan;
+ return cell;
+ }
+ },
];
}
## Main Todo
-- Alignments: Use existing classes for blocks
+- Alignments: Use existing classes for blocks (including table cells)
- Alignments: Handle inline block content (image, video)
- Image paste upload
- Keyboard shortcuts support
import {$getSelection, BaseSelection} from "lexical";
import {$isCustomTableNode} from "../../../nodes/custom-table";
import {
- $createTableRowNode,
$deleteTableColumn__EXPERIMENTAL,
$deleteTableRow__EXPERIMENTAL,
$insertTableColumn__EXPERIMENTAL,
- $insertTableRow__EXPERIMENTAL, $isTableCellNode,
- $isTableNode, $isTableRowNode, $isTableSelection, $unmergeCell, TableCellNode, TableNode,
+ $insertTableRow__EXPERIMENTAL,
+ $isTableNode, $isTableRowNode, $isTableSelection, $unmergeCell, TableCellNode,
} from "@lexical/table";
import {$getNodeFromSelection, $selectionContainsNodeType} from "../../../utils/selection";
import {$getParentOfType} from "../../../utils/nodes";
+import {$isCustomTableCellNode} from "../../../nodes/custom-table-cell-node";
+import {showCellPropertiesForm} from "../forms/tables";
const neverActive = (): boolean => false;
-const cellNotSelected = (selection: BaseSelection|null) => !$selectionContainsNodeType(selection, $isTableCellNode);
+const cellNotSelected = (selection: BaseSelection|null) => !$selectionContainsNodeType(selection, $isCustomTableCellNode);
export const table: EditorBasicButtonDefinition = {
label: 'Table',
icon: tableIcon,
action(context: EditorUiContext) {
context.editor.getEditorState().read(() => {
- const cell = $getNodeFromSelection($getSelection(), $isTableCellNode);
- if (!$isTableCellNode(cell)) {
+ const cell = $getNodeFromSelection($getSelection(), $isCustomTableCellNode);
+ if (!$isCustomTableCellNode(cell)) {
return;
}
format: 'long',
action(context: EditorUiContext) {
context.editor.getEditorState().read(() => {
- const cell = $getNodeFromSelection($getSelection(), $isTableCellNode);
- if (!$isTableCellNode(cell)) {
+ const cell = $getNodeFromSelection($getSelection(), $isCustomTableCellNode);
+ if (!$isCustomTableCellNode(cell)) {
return;
}
format: 'long',
action(context: EditorUiContext) {
context.editor.getEditorState().read(() => {
- const cell = $getNodeFromSelection($getSelection(), $isTableCellNode);
- if (!$isTableCellNode(cell)) {
+ const cell = $getNodeFromSelection($getSelection(), $isCustomTableCellNode);
+ if (!$isCustomTableCellNode(cell)) {
return;
}
format: 'long',
action(context: EditorUiContext) {
context.editor.getEditorState().read(() => {
- const cell = $getNodeFromSelection($getSelection(), $isTableCellNode);
- if (!$isTableCellNode(cell)) {
+ const cell = $getNodeFromSelection($getSelection(), $isCustomTableCellNode);
+ if (!$isCustomTableCellNode(cell)) {
return;
}
label: 'Cell properties',
action(context: EditorUiContext) {
context.editor.getEditorState().read(() => {
- const cell = $getNodeFromSelection($getSelection(), $isTableCellNode);
- if ($isTableCellNode(cell)) {
-
- const modalForm = context.manager.createModal('cell_properties');
- modalForm.show({});
+ const cell = $getNodeFromSelection($getSelection(), $isCustomTableCellNode);
+ if ($isCustomTableCellNode(cell)) {
+ showCellPropertiesForm(cell, context);
}
});
},
},
isActive: neverActive,
isDisabled(selection) {
- const cell = $getNodeFromSelection(selection, $isTableCellNode) as TableCellNode|null;
+ const cell = $getNodeFromSelection(selection, $isCustomTableCellNode) as TableCellNode|null;
if (cell) {
const merged = cell.getRowSpan() > 1 || cell.getColSpan() > 1;
return !merged;
EditorSelectFormFieldDefinition
} from "../../framework/forms";
import {EditorUiContext} from "../../framework/core";
+import {$isCustomTableCellNode, CustomTableCellNode} from "../../../nodes/custom-table-cell-node";
+import {EditorFormModal} from "../../framework/modals";
+import {$getNodeFromSelection} from "../../../utils/selection";
+import {$getSelection, ElementFormatType} from "lexical";
+import {TableCellHeaderStates} from "@lexical/table";
const borderStyleInput: EditorSelectFormFieldDefinition = {
label: 'Border style',
}
};
+export function showCellPropertiesForm(cell: CustomTableCellNode, context: EditorUiContext): EditorFormModal {
+ const styles = cell.getStyles();
+ const modalForm = context.manager.createModal('cell_properties');
+ modalForm.show({
+ width: '', // TODO
+ height: styles.get('height') || '',
+ type: cell.getTag(),
+ h_align: '', // TODO
+ v_align: styles.get('vertical-align') || '',
+ border_width: styles.get('border-width') || '',
+ border_style: styles.get('border-style') || '',
+ border_color: styles.get('border-color') || '',
+ background_color: styles.get('background-color') || '',
+ });
+ return modalForm;
+}
+
export const cellProperties: EditorFormDefinition = {
submitText: 'Save',
async action(formData, context: EditorUiContext) {
- // TODO
+ // TODO - Set for cell selection range
+ context.editor.update(() => {
+ const cell = $getNodeFromSelection($getSelection(), $isCustomTableCellNode);
+ if ($isCustomTableCellNode(cell)) {
+ // TODO - Set width
+ cell.setFormat((formData.get('h_align')?.toString() || '') as ElementFormatType);
+ cell.updateTag(formData.get('type')?.toString() || '');
+
+ const styles = cell.getStyles();
+ styles.set('height', formData.get('height')?.toString() || '');
+ styles.set('vertical-align', formData.get('v_align')?.toString() || '');
+ styles.set('border-width', formData.get('border_width')?.toString() || '');
+ styles.set('border-style', formData.get('border_style')?.toString() || '');
+ styles.set('border-color', formData.get('border_color')?.toString() || '');
+ styles.set('background-color', formData.get('background_color')?.toString() || '');
+
+ cell.setStyles(styles);
+ }
+ });
+
return true;
},
fields: [
build() {
const generalFields: EditorFormFieldDefinition[] = [
{
- label: 'Width',
+ label: 'Width', // Colgroup width
name: 'width',
type: 'text',
},
{
- label: 'Height',
+ label: 'Height', // inline-style: height
name: 'height',
type: 'text',
},
{
- label: 'Cell type',
+ label: 'Cell type', // element
name: 'type',
type: 'select',
valuesByLabel: {
- 'Cell': 'cell',
- 'Header cell': 'header',
+ 'Cell': 'td',
+ 'Header cell': 'th',
}
} as EditorSelectFormFieldDefinition,
{
- ...alignmentInput,
+ ...alignmentInput, // class: 'align-right/left/center'
label: 'Horizontal align',
name: 'h_align',
},
{
- label: 'Vertical align',
+ label: 'Vertical align', // inline-style: vertical-align
name: 'v_align',
type: 'select',
valuesByLabel: {
const advancedFields: EditorFormFieldDefinition[] = [
{
- label: 'Border width',
+ label: 'Border width', // inline-style: border-width
name: 'border_width',
type: 'text',
},
- borderStyleInput,
- borderColorInput,
- backgroundColorInput,
+ borderStyleInput, // inline-style: border-style
+ borderColorInput, // inline-style: border-color
+ backgroundColorInput, // inline-style: background-color
];
return new EditorFormTabs([
},
],
};
-
export const tableProperties: EditorFormDefinition = {
submitText: 'Save',
async action(formData, context: EditorUiContext) {