TableCellNode
} from "@lexical/table";
import {TableCellHeaderState} from "@lexical/table/LexicalTableCellNode";
-import {createStyleMapFromDomStyles, StyleMap} from "../utils/styles";
+import {extractStyleMapFromElement, StyleMap} from "../utils/dom";
export type SerializedCustomTableCellNode = Spread<{
styles: Record<string, string>,
return cellNode;
}
+ clearWidth(): void {
+ const self = this.getWritable();
+ self.__width = undefined;
+ }
+
getStyles(): StyleMap {
const self = this.getLatest();
return new Map(self.__styles);
const output = $convertTableCellNodeElement(domNode);
if (domNode instanceof HTMLElement && output.node instanceof CustomTableCellNode) {
- output.node.setStyles(createStyleMapFromDomStyles(domNode.style));
+ output.node.setStyles(extractStyleMapFromElement(domNode));
}
return output;
import {
- $createParagraphNode,
- $isElementNode,
- $isLineBreakNode,
- $isTextNode,
DOMConversionMap,
DOMConversionOutput,
EditorConfig,
} from "lexical";
import {
- $createTableCellNode,
- $isTableCellNode,
SerializedTableRowNode,
- TableCellHeaderStates,
TableRowNode
} from "@lexical/table";
-import {createStyleMapFromDomStyles, StyleMap} from "../utils/styles";
import {NodeKey} from "lexical/LexicalNode";
+import {extractStyleMapFromElement, StyleMap} from "../utils/dom";
export type SerializedCustomTableRowNode = Spread<{
styles: Record<string, string>,
const rowNode = $createCustomTableRowNode();
if (domNode instanceof HTMLElement) {
- rowNode.setStyles(createStyleMapFromDomStyles(domNode.style));
+ rowNode.setStyles(extractStyleMapFromElement(domNode));
}
return {node: rowNode};
import {DOMConversion, DOMConversionMap, DOMConversionOutput, LexicalNode, Spread} from "lexical";
import {EditorConfig} from "lexical/LexicalEditor";
-import {el} from "../utils/dom";
+import {el, extractStyleMapFromElement, StyleMap} from "../utils/dom";
import {getTableColumnWidths} from "../utils/tables";
export type SerializedCustomTableNode = Spread<{
id: string;
colWidths: string[];
+ styles: Record<string, string>,
}, SerializedTableNode>
export class CustomTableNode extends TableNode {
__id: string = '';
__colWidths: string[] = [];
+ __styles: StyleMap = new Map;
static getType() {
return 'custom-table';
return self.__colWidths;
}
+ getStyles(): StyleMap {
+ const self = this.getLatest();
+ return new Map(self.__styles);
+ }
+
+ setStyles(styles: StyleMap): void {
+ const self = this.getWritable();
+ self.__styles = new Map(styles);
+ }
+
static clone(node: CustomTableNode) {
const newNode = new CustomTableNode(node.__key);
newNode.__id = node.__id;
newNode.__colWidths = node.__colWidths;
+ newNode.__styles = new Map(node.__styles);
return newNode;
}
dom.append(colgroup);
}
+ for (const [name, value] of this.__styles.entries()) {
+ dom.style.setProperty(name, value);
+ }
+
return dom;
}
version: 1,
id: this.__id,
colWidths: this.__colWidths,
+ styles: Object.fromEntries(this.__styles),
};
}
const node = $createCustomTableNode();
node.setId(serializedNode.id);
node.setColWidths(serializedNode.colWidths);
+ node.setStyles(new Map(Object.entries(serializedNode.styles)));
return node;
}
const colWidths = getTableColumnWidths(element as HTMLTableElement);
node.setColWidths(colWidths);
+ node.setStyles(extractStyleMapFromElement(element));
return {node};
},
## In progress
-- Table features
- - Table properties form logic
- - Caption text support
- - Resize to contents button
- - Remove formatting button
- - Cut/Copy/Paste column
-
## Main Todo
- Alignments: Use existing classes for blocks (including table cells)
- Drawing gallery integration
- Support media src conversions (https://github.com/tinymce/tinymce/blob/release/6.6/modules/tinymce/src/plugins/media/main/ts/core/UrlPatterns.ts)
- Media resize support (like images)
+- Table caption text support
+- Table Cut/Copy/Paste column
## Secondary Todo
import insertRowAboveIcon from "@icons/editor/table-insert-row-above.svg";
import insertRowBelowIcon from "@icons/editor/table-insert-row-below.svg";
import {EditorUiContext} from "../../framework/core";
-import {$createNodeSelection, $createRangeSelection, $getSelection, BaseSelection} from "lexical";
+import {$getSelection, BaseSelection} from "lexical";
import {$isCustomTableNode} from "../../../nodes/custom-table";
import {
$deleteTableColumn__EXPERIMENTAL,
$deleteTableRow__EXPERIMENTAL,
$insertTableColumn__EXPERIMENTAL,
$insertTableRow__EXPERIMENTAL,
- $isTableNode, $isTableRowNode, $isTableSelection, $unmergeCell, TableCellNode,
+ $isTableNode, $isTableSelection, $unmergeCell, TableCellNode,
} from "@lexical/table";
import {$getNodeFromSelection, $selectionContainsNodeType} from "../../../utils/selection";
import {$getParentOfType} from "../../../utils/nodes";
import {$isCustomTableCellNode} from "../../../nodes/custom-table-cell";
-import {$showCellPropertiesForm, $showRowPropertiesForm} from "../forms/tables";
-import {$getTableRowsFromSelection, $mergeTableCellsInSelection} from "../../../utils/tables";
+import {$showCellPropertiesForm, $showRowPropertiesForm, $showTablePropertiesForm} from "../forms/tables";
+import {
+ $clearTableFormatting,
+ $clearTableSizes, $getTableFromSelection,
+ $getTableRowsFromSelection,
+ $mergeTableCellsInSelection
+} from "../../../utils/tables";
import {$isCustomTableRowNode, CustomTableRowNode} from "../../../nodes/custom-table-row";
import {NodeClipboard} from "../../../services/node-clipboard";
-import {r} from "@codemirror/legacy-modes/mode/r";
-import {$generateHtmlFromNodes} from "@lexical/html";
const neverActive = (): boolean => false;
const cellNotSelected = (selection: BaseSelection|null) => !$selectionContainsNodeType(selection, $isCustomTableCellNode);
icon: tableIcon,
action(context: EditorUiContext) {
context.editor.getEditorState().read(() => {
- const cell = $getNodeFromSelection($getSelection(), $isCustomTableCellNode);
- if (!$isCustomTableCellNode(cell)) {
- return;
+ const table = $getTableFromSelection($getSelection());
+ if ($isCustomTableNode(table)) {
+ $showTablePropertiesForm(table, context);
}
-
- const table = $getParentOfType(cell, $isTableNode);
- const modalForm = context.manager.createModal('table_properties');
- modalForm.show({});
- // TODO
});
},
isActive: neverActive,
label: 'Clear table formatting',
format: 'long',
action(context: EditorUiContext) {
- context.editor.getEditorState().read(() => {
+ context.editor.update(() => {
const cell = $getNodeFromSelection($getSelection(), $isCustomTableCellNode);
if (!$isCustomTableCellNode(cell)) {
return;
}
const table = $getParentOfType(cell, $isTableNode);
- // TODO
+ if ($isCustomTableNode(table)) {
+ $clearTableFormatting(table);
+ }
});
},
isActive: neverActive,
label: 'Resize to contents',
format: 'long',
action(context: EditorUiContext) {
- context.editor.getEditorState().read(() => {
+ context.editor.update(() => {
const cell = $getNodeFromSelection($getSelection(), $isCustomTableCellNode);
if (!$isCustomTableCellNode(cell)) {
return;
}
const table = $getParentOfType(cell, $isCustomTableNode);
- if (!$isCustomTableNode(table)) {
- return;
- }
-
- for (const row of table.getChildren()) {
- if ($isTableRowNode(row)) {
- // TODO - Come back later as this may depend on if we
- // are using a custom table row
- }
+ if ($isCustomTableNode(table)) {
+ $clearTableSizes(table);
}
});
},
format: 'long',
action(context: EditorUiContext) {
context.editor.getEditorState().read(() => {
- const cell = $getNodeFromSelection($getSelection(), $isCustomTableCellNode);
- if (!$isCustomTableCellNode(cell)) {
- return;
- }
-
- const row = $getParentOfType(cell, $isCustomTableRowNode);
- if ($isCustomTableRowNode(row)) {
- $showRowPropertiesForm(row, context);
+ const rows = $getTableRowsFromSelection($getSelection());
+ if ($isCustomTableRowNode(rows[0])) {
+ $showRowPropertiesForm(rows[0], context);
}
});
},
import {EditorFormModal} from "../../framework/modals";
import {$getSelection, ElementFormatType} from "lexical";
import {
+ $forEachTableCell, $getCellPaddingForTable,
$getTableCellColumnWidth,
- $getTableCellsFromSelection,
+ $getTableCellsFromSelection, $getTableFromSelection,
$getTableRowsFromSelection,
$setTableCellColumnWidth
} from "../../../utils/tables";
import {formatSizeValue} from "../../../utils/dom";
import {CustomTableRowNode} from "../../../nodes/custom-table-row";
+import {CustomTableNode} from "../../../nodes/custom-table";
const borderStyleInput: EditorSelectFormFieldDefinition = {
label: 'Border style',
backgroundColorInput, // style on tr: height
],
};
+
+export function $showTablePropertiesForm(table: CustomTableNode, context: EditorUiContext): EditorFormModal {
+ const styles = table.getStyles();
+ const modalForm = context.manager.createModal('table_properties');
+ modalForm.show({
+ width: styles.get('width') || '',
+ height: styles.get('height') || '',
+ cell_spacing: styles.get('cell-spacing') || '',
+ cell_padding: $getCellPaddingForTable(table),
+ border_width: styles.get('border-width') || '',
+ border_style: styles.get('border-style') || '',
+ border_color: styles.get('border-color') || '',
+ background_color: styles.get('background-color') || '',
+ // caption: '', TODO
+ align: table.getFormatType(),
+ });
+ return modalForm;
+}
+
export const tableProperties: EditorFormDefinition = {
submitText: 'Save',
async action(formData, context: EditorUiContext) {
- // TODO
+ context.editor.update(() => {
+ const table = $getTableFromSelection($getSelection());
+ if (!table) {
+ return;
+ }
+
+ const styles = table.getStyles();
+ styles.set('width', formatSizeValue(formData.get('width')?.toString() || ''));
+ styles.set('height', formatSizeValue(formData.get('height')?.toString() || ''));
+ styles.set('cell-spacing', formatSizeValue(formData.get('cell_spacing')?.toString() || ''));
+ styles.set('border-width', formatSizeValue(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() || '');
+ table.setStyles(styles);
+
+ table.setFormat(formData.get('align') as ElementFormatType);
+
+ const cellPadding = (formData.get('cell_padding')?.toString() || '');
+ if (cellPadding) {
+ const cellPaddingFormatted = formatSizeValue(cellPadding);
+ $forEachTableCell(table, (cell: CustomTableCellNode) => {
+ const styles = cell.getStyles();
+ styles.set('padding', cellPaddingFormatted);
+ cell.setStyles(styles);
+ });
+ }
+
+ // TODO - cell caption
+ });
return true;
},
fields: [
build() {
const generalFields: EditorFormFieldDefinition[] = [
{
- label: 'Width',
+ label: 'Width', // Style - width
name: 'width',
type: 'text',
},
{
- label: 'Height',
+ label: 'Height', // Style - height
name: 'height',
type: 'text',
},
{
- label: 'Cell spacing',
+ label: 'Cell spacing', // Style - border-spacing
name: 'cell_spacing',
type: 'text',
},
{
- label: 'Cell padding',
+ label: 'Cell padding', // Style - padding on child cells?
name: 'cell_padding',
type: 'text',
},
{
- label: 'Border width',
+ label: 'Border width', // Style - border-width
name: 'border_width',
type: 'text',
},
{
- label: 'caption',
- name: 'height',
+ label: 'caption', // Caption element
+ name: 'caption',
type: 'text', // TODO -
},
- alignmentInput,
+ alignmentInput, // alignment class
];
const advancedFields: EditorFormFieldDefinition[] = [
- borderStyleInput,
- borderColorInput,
- backgroundColorInput,
+ borderStyleInput, // Style - border-style
+ borderColorInput, // Style - border-color
+ backgroundColorInput, // Style - background-color
];
return new EditorFormTabs([
}
return size;
+}
+
+export type StyleMap = Map<string, string>;
+
+/**
+ * Creates a map from an element's styles.
+ * Uses direct attribute value string handling since attempting to iterate
+ * over .style will expand out any shorthand properties (like 'padding') making
+ * rather than being representative of the actual properties set.
+ */
+export function extractStyleMapFromElement(element: HTMLElement): StyleMap {
+ const map: StyleMap = new Map();
+ const styleText= element.getAttribute('style') || '';
+
+ const rules = styleText.split(';');
+ for (const rule of rules) {
+ const [name, value] = rule.split(':');
+ if (!name || !value) {
+ continue;
+ }
+
+ map.set(name.trim().toLowerCase(), value.trim());
+ }
+
+ return map;
}
\ No newline at end of file
+++ /dev/null
-
-export type StyleMap = Map<string, string>;
-
-export function createStyleMapFromDomStyles(domStyles: CSSStyleDeclaration): StyleMap {
- const styleMap: StyleMap = new Map();
- const styleNames: string[] = Array.from(domStyles);
- for (const style of styleNames) {
- styleMap.set(style, domStyles.getPropertyValue(style));
- }
- return styleMap;
-}
\ No newline at end of file
return Object.values(rowsByKey);
}
+export function $getTableFromSelection(selection: BaseSelection|null): CustomTableNode|null {
+ const cells = $getTableCellsFromSelection(selection);
+ if (cells.length === 0) {
+ return null;
+ }
+
+ const table = $getParentOfType(cells[0], $isCustomTableNode);
+ if ($isCustomTableNode(table)) {
+ return table;
+ }
+
+ return null;
+}
+
+export function $clearTableSizes(table: CustomTableNode): void {
+ table.setColWidths([]);
+
+ // TODO - Extra form things once table properties and extra things
+ // are supported
+ for (const row of table.getChildren()) {
+ if (!$isCustomTableRowNode(row)) {
+ continue;
+ }
+
+ const rowStyles = row.getStyles();
+ rowStyles.delete('height');
+ rowStyles.delete('width');
+ row.setStyles(rowStyles);
+
+ const cells = row.getChildren().filter(c => $isCustomTableCellNode(c));
+ for (const cell of cells) {
+ const cellStyles = cell.getStyles();
+ cellStyles.delete('height');
+ cellStyles.delete('width');
+ cell.setStyles(cellStyles);
+ cell.clearWidth();
+ }
+ }
+}
+
+export function $clearTableFormatting(table: CustomTableNode): void {
+ table.setColWidths([]);
+ table.setStyles(new Map);
+
+ for (const row of table.getChildren()) {
+ if (!$isCustomTableRowNode(row)) {
+ continue;
+ }
+ row.setStyles(new Map);
+ row.setFormat('');
+
+ const cells = row.getChildren().filter(c => $isCustomTableCellNode(c));
+ for (const cell of cells) {
+ cell.setStyles(new Map);
+ cell.clearWidth();
+ cell.setFormat('');
+ }
+ }
+}
+
+/**
+ * Perform the given callback for each cell in the given table.
+ * Returning false from the callback stops the function early.
+ */
+export function $forEachTableCell(table: CustomTableNode, callback: (c: CustomTableCellNode) => void|false): void {
+ outer: for (const row of table.getChildren()) {
+ if (!$isCustomTableRowNode(row)) {
+ continue;
+ }
+ const cells = row.getChildren();
+ for (const cell of cells) {
+ if (!$isCustomTableCellNode(cell)) {
+ return;
+ }
+ const result = callback(cell);
+ if (result === false) {
+ break outer;
+ }
+ }
+ }
+}
+
+export function $getCellPaddingForTable(table: CustomTableNode): string {
+ let padding: string|null = null;
+
+ $forEachTableCell(table, (cell: CustomTableCellNode) => {
+ const cellPadding = cell.getStyles().get('padding') || ''
+ if (padding === null) {
+ padding = cellPadding;
+ }
+
+ if (cellPadding !== padding) {
+ padding = null;
+ return false;
+ }
+ });
+
+ return padding || '';
+}