## In progress
- Table features
- - Continued table dropdown menu
- - Connect up cell properties form
+ - Cell properties form logic
- Merge cell action
+ - Row properties form logic
+ - Table properties form logic
+ - Caption text support
+ - Resize to contents button
+ - Remove formatting button
## Main Todo
- Alignments: Use existing classes for blocks
- Alignments: Handle inline block content (image, video)
-
- Image paste upload
- Keyboard shortcuts support
- Add ID support to all block types
import insertRowBelowIcon from "@icons/editor/table-insert-row-below.svg";
import {EditorUiContext} from "../../framework/core";
import {
- $getNodeFromSelection,
+ $getNodeFromSelection, $getParentOfType,
$selectionContainsNodeType
} from "../../../helpers";
-import {$getSelection} from "lexical";
+import {$getSelection, BaseSelection} from "lexical";
import {$isCustomTableNode} from "../../../nodes/custom-table";
import {
+ $createTableRowNode,
$deleteTableColumn__EXPERIMENTAL,
$deleteTableRow__EXPERIMENTAL,
$insertTableColumn__EXPERIMENTAL,
$insertTableRow__EXPERIMENTAL, $isTableCellNode,
- $isTableNode, $isTableSelection, $unmergeCell, TableCellNode,
+ $isTableNode, $isTableRowNode, $isTableSelection, $unmergeCell, TableCellNode, TableNode,
} from "@lexical/table";
+const neverActive = (): boolean => false;
+const cellNotSelected = (selection: BaseSelection|null) => !$selectionContainsNodeType(selection, $isTableCellNode);
export const table: EditorBasicButtonDefinition = {
label: 'Table',
icon: tableIcon,
};
+export const tableProperties: EditorButtonDefinition = {
+ label: 'Table properties',
+ icon: tableIcon,
+ action(context: EditorUiContext) {
+ context.editor.getEditorState().read(() => {
+ const cell = $getNodeFromSelection($getSelection(), $isTableCellNode);
+ if (!$isTableCellNode(cell)) {
+ return;
+ }
+
+ const table = $getParentOfType(cell, $isTableNode);
+ const modalForm = context.manager.createModal('table_properties');
+ modalForm.show({});
+ // TODO
+ });
+ },
+ isActive: neverActive,
+ isDisabled: cellNotSelected,
+};
+
+export const clearTableFormatting: EditorButtonDefinition = {
+ label: 'Clear table formatting',
+ format: 'long',
+ action(context: EditorUiContext) {
+ context.editor.getEditorState().read(() => {
+ const cell = $getNodeFromSelection($getSelection(), $isTableCellNode);
+ if (!$isTableCellNode(cell)) {
+ return;
+ }
+
+ const table = $getParentOfType(cell, $isTableNode);
+ // TODO
+ });
+ },
+ isActive: neverActive,
+ isDisabled: cellNotSelected,
+};
+
+export const resizeTableToContents: EditorButtonDefinition = {
+ label: 'Resize to contents',
+ format: 'long',
+ action(context: EditorUiContext) {
+ context.editor.getEditorState().read(() => {
+ const cell = $getNodeFromSelection($getSelection(), $isTableCellNode);
+ if (!$isTableCellNode(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
+ }
+ }
+ });
+ },
+ isActive: neverActive,
+ isDisabled: cellNotSelected,
+};
+
export const deleteTable: EditorButtonDefinition = {
label: 'Delete table',
icon: deleteIcon,
};
export const insertRowAbove: EditorButtonDefinition = {
- label: 'Insert row above',
+ label: 'Insert row before',
icon: insertRowAboveIcon,
action(context: EditorUiContext) {
context.editor.update(() => {
$insertTableRow__EXPERIMENTAL(false);
});
},
- isActive() {
- return false;
- }
+ isActive: neverActive,
+ isDisabled: cellNotSelected,
};
export const insertRowBelow: EditorButtonDefinition = {
- label: 'Insert row below',
+ label: 'Insert row after',
icon: insertRowBelowIcon,
action(context: EditorUiContext) {
context.editor.update(() => {
$insertTableRow__EXPERIMENTAL(true);
});
},
- isActive() {
- return false;
- }
+ isActive: neverActive,
+ isDisabled: cellNotSelected,
};
export const deleteRow: EditorButtonDefinition = {
$deleteTableRow__EXPERIMENTAL();
});
},
- isActive() {
- return false;
- }
+ isActive: neverActive,
+ isDisabled: cellNotSelected,
+};
+
+export const rowProperties: EditorButtonDefinition = {
+ label: 'Row properties',
+ format: 'long',
+ action(context: EditorUiContext) {
+ context.editor.getEditorState().read(() => {
+ const cell = $getNodeFromSelection($getSelection(), $isTableCellNode);
+ if (!$isTableCellNode(cell)) {
+ return;
+ }
+
+ const row = $getParentOfType(cell, $isTableRowNode);
+ const modalForm = context.manager.createModal('row_properties');
+ modalForm.show({});
+ // TODO
+ });
+ },
+ isActive: neverActive,
+ isDisabled: cellNotSelected,
+};
+
+export const cutRow: EditorButtonDefinition = {
+ label: 'Cut row',
+ format: 'long',
+ action(context: EditorUiContext) {
+ context.editor.getEditorState().read(() => {
+ // TODO
+ });
+ },
+ isActive: neverActive,
+ isDisabled: cellNotSelected,
+};
+
+export const copyRow: EditorButtonDefinition = {
+ label: 'Copy row',
+ format: 'long',
+ action(context: EditorUiContext) {
+ context.editor.getEditorState().read(() => {
+ // TODO
+ });
+ },
+ isActive: neverActive,
+ isDisabled: cellNotSelected,
+};
+
+export const pasteRowBefore: EditorButtonDefinition = {
+ label: 'Paste row before',
+ format: 'long',
+ action(context: EditorUiContext) {
+ context.editor.getEditorState().read(() => {
+ // TODO
+ });
+ },
+ isActive: neverActive,
+ isDisabled: cellNotSelected,
+};
+
+export const pasteRowAfter: EditorButtonDefinition = {
+ label: 'Paste row after',
+ format: 'long',
+ action(context: EditorUiContext) {
+ context.editor.getEditorState().read(() => {
+ // TODO
+ });
+ },
+ isActive: neverActive,
+ isDisabled: cellNotSelected,
+};
+
+export const cutColumn: EditorButtonDefinition = {
+ label: 'Cut column',
+ format: 'long',
+ action(context: EditorUiContext) {
+ context.editor.getEditorState().read(() => {
+ // TODO
+ });
+ },
+ isActive: neverActive,
+ isDisabled: cellNotSelected,
+};
+
+export const copyColumn: EditorButtonDefinition = {
+ label: 'Copy column',
+ format: 'long',
+ action(context: EditorUiContext) {
+ context.editor.getEditorState().read(() => {
+ // TODO
+ });
+ },
+ isActive: neverActive,
+ isDisabled: cellNotSelected,
+};
+
+export const pasteColumnBefore: EditorButtonDefinition = {
+ label: 'Paste column before',
+ format: 'long',
+ action(context: EditorUiContext) {
+ context.editor.getEditorState().read(() => {
+ // TODO
+ });
+ },
+ isActive: neverActive,
+ isDisabled: cellNotSelected,
+};
+
+export const pasteColumnAfter: EditorButtonDefinition = {
+ label: 'Paste column after',
+ format: 'long',
+ action(context: EditorUiContext) {
+ context.editor.getEditorState().read(() => {
+ // TODO
+ });
+ },
+ isActive: neverActive,
+ isDisabled: cellNotSelected,
};
export const insertColumnBefore: EditorButtonDefinition = {
}
});
},
- isActive() {
- return false;
- },
- isDisabled(selection) {
- return !$selectionContainsNodeType(selection, $isTableCellNode);
- }
+ isActive: neverActive,
+ isDisabled: cellNotSelected,
};
export const mergeCells: EditorButtonDefinition = {
// https://github.com/facebook/lexical/blob/f373759a7849f473d34960a6bf4e34b2a011e762/packages/lexical-playground/src/plugins/TableActionMenuPlugin/index.tsx#L299
});
},
- isActive() {
- return false;
- },
+ isActive: neverActive,
isDisabled(selection) {
return !$isTableSelection(selection);
}
$unmergeCell();
});
},
- isActive() {
- return false;
- },
+ isActive: neverActive,
isDisabled(selection) {
const cell = $getNodeFromSelection(selection, $isTableCellNode) as TableCellNode|null;
if (cell) {
EditorSelectFormFieldDefinition
} from "../../framework/forms";
import {EditorUiContext} from "../../framework/core";
-import {setEditorContentFromHtml} from "../../../actions";
+
+const borderStyleInput: EditorSelectFormFieldDefinition = {
+ label: 'Border style',
+ name: 'border_style',
+ type: 'select',
+ valuesByLabel: {
+ 'Select...': '',
+ "Solid": 'solid',
+ "Dotted": 'dotted',
+ "Dashed": 'dashed',
+ "Double": 'double',
+ "Groove": 'groove',
+ "Ridge": 'ridge',
+ "Inset": 'inset',
+ "Outset": 'outset',
+ "None": 'none',
+ "Hidden": 'hidden',
+ }
+};
+
+const borderColorInput: EditorFormFieldDefinition = {
+ label: 'Border color',
+ name: 'border_color',
+ type: 'text',
+};
+
+const backgroundColorInput: EditorFormFieldDefinition = {
+ label: 'Background color',
+ name: 'background_color',
+ type: 'text',
+};
+
+const alignmentInput: EditorSelectFormFieldDefinition = {
+ label: 'Alignment',
+ name: 'align',
+ type: 'select',
+ valuesByLabel: {
+ 'None': '',
+ 'Left': 'left',
+ 'Center': 'center',
+ 'Right': 'right',
+ }
+};
export const cellProperties: EditorFormDefinition = {
submitText: 'Save',
async action(formData, context: EditorUiContext) {
- setEditorContentFromHtml(context.editor, formData.get('source')?.toString() || '');
+ // TODO
return true;
},
fields: [
}
} as EditorSelectFormFieldDefinition,
{
+ ...alignmentInput,
label: 'Horizontal align',
name: 'h_align',
- type: 'select',
- valuesByLabel: {
- 'None': '',
- 'Left': 'left',
- 'Center': 'center',
- 'Right': 'right',
- }
- } as EditorSelectFormFieldDefinition,
+ },
{
label: 'Vertical align',
name: 'v_align',
name: 'border_width',
type: 'text',
},
+ borderStyleInput,
+ borderColorInput,
+ backgroundColorInput,
+ ];
+
+ return new EditorFormTabs([
+ {
+ label: 'General',
+ contents: generalFields,
+ },
+ {
+ label: 'Advanced',
+ contents: advancedFields,
+ }
+ ])
+ }
+ },
+ ],
+};
+
+export const rowProperties: EditorFormDefinition = {
+ submitText: 'Save',
+ async action(formData, context: EditorUiContext) {
+ // TODO
+ return true;
+ },
+ fields: [
+ {
+ build() {
+ const generalFields: EditorFormFieldDefinition[] = [
{
- label: 'Border style',
- name: 'border_style',
+ label: 'Row type',
+ name: 'type',
type: 'select',
valuesByLabel: {
- 'Select...': '',
- "Solid": 'solid',
- "Dotted": 'dotted',
- "Dashed": 'dashed',
- "Double": 'double',
- "Groove": 'groove',
- "Ridge": 'ridge',
- "Inset": 'inset',
- "Outset": 'outset',
- "None": 'none',
- "Hidden": 'hidden',
+ 'Body': 'body',
+ 'Header': 'header',
+ 'Footer': 'footer',
}
} as EditorSelectFormFieldDefinition,
+ alignmentInput,
+ {
+ label: 'Height',
+ name: 'height',
+ type: 'text',
+ },
+ ];
+
+ const advancedFields: EditorFormFieldDefinition[] = [
+ borderStyleInput,
+ borderColorInput,
+ backgroundColorInput,
+ ];
+
+ return new EditorFormTabs([
+ {
+ label: 'General',
+ contents: generalFields,
+ },
+ {
+ label: 'Advanced',
+ contents: advancedFields,
+ }
+ ])
+ }
+ },
+ ],
+};
+
+export const tableProperties: EditorFormDefinition = {
+ submitText: 'Save',
+ async action(formData, context: EditorUiContext) {
+ // TODO
+ return true;
+ },
+ fields: [
+ {
+ build() {
+ const generalFields: EditorFormFieldDefinition[] = [
+ {
+ label: 'Width',
+ name: 'width',
+ type: 'text',
+ },
+ {
+ label: 'Height',
+ name: 'height',
+ type: 'text',
+ },
{
- label: 'Border color',
- name: 'border_color',
+ label: 'Cell spacing',
+ name: 'cell_spacing',
type: 'text',
},
{
- label: 'Background color',
- name: 'background_color',
+ label: 'Cell padding',
+ name: 'cell_padding',
type: 'text',
},
+ {
+ label: 'Border width',
+ name: 'border_width',
+ type: 'text',
+ },
+ {
+ label: 'caption',
+ name: 'height',
+ type: 'text', // TODO -
+ },
+ alignmentInput,
+ ];
+
+ const advancedFields: EditorFormFieldDefinition[] = [
+ borderStyleInput,
+ borderColorInput,
+ backgroundColorInput,
];
return new EditorFormTabs([
import {EditorFormModalDefinition} from "../framework/modals";
import {image, link, media} from "./forms/objects";
import {source} from "./forms/controls";
-import {cellProperties} from "./forms/tables";
+import {cellProperties, rowProperties, tableProperties} from "./forms/tables";
export const modals: Record<string, EditorFormModalDefinition> = {
link: {
title: 'Cell Properties',
form: cellProperties,
},
+ row_properties: {
+ title: 'Row Properties',
+ form: rowProperties,
+ },
+ table_properties: {
+ title: 'Table Properties',
+ form: tableProperties,
+ },
};
\ No newline at end of file
constructor(options: EditorDropdownButtonOptions, children: EditorUiElement[]) {
super(children);
this.childItems = children;
- this.options = Object.assign(defaultOptions, options);
+ this.options = Object.assign({}, defaultOptions, options);
if (options.button instanceof EditorButton) {
this.button = options.button;
class: 'editor-dropdown-menu-container',
}, [button, menu]);
- handleDropdown({toggle : button, menu : menu,
+ handleDropdown({toggle: button, menu : menu,
showOnHover: this.options.showOnHover,
onOpen : () => {
this.open = true;
toggle.addEventListener('mouseenter', toggleShowing);
}
- menu.addEventListener('mouseleave', hide);
+ menu.parentElement?.addEventListener('mouseleave', hide);
}
\ No newline at end of file
import {EditorColorButton} from "./framework/blocks/color-button";
import {EditorOverflowContainer} from "./framework/blocks/overflow-container";
import {
- cellProperties,
+ cellProperties, clearTableFormatting,
+ copyColumn,
+ copyRow,
+ cutColumn,
+ cutRow,
deleteColumn,
deleteRow,
- deleteTable, deleteTableMenuAction, insertColumnAfter,
+ deleteTable,
+ deleteTableMenuAction,
+ insertColumnAfter,
insertColumnBefore,
insertRowAbove,
- insertRowBelow, mergeCells, splitCell,
- table
+ insertRowBelow,
+ mergeCells,
+ pasteColumnAfter,
+ pasteColumnBefore,
+ pasteRowAfter,
+ pasteRowBefore, resizeTableToContents,
+ rowProperties,
+ splitCell,
+ table, tableProperties
} from "./defaults/buttons/tables";
import {fullscreen, redo, source, undo} from "./defaults/buttons/controls";
import {
new EditorDropdownButton({button: {...table, format: 'long'}, showOnHover: true}, [
new EditorTableCreator(),
]),
- new EditorDropdownButton({button: {label: 'Cell'}}, [
+ new EditorDropdownButton({button: {label: 'Cell'}, direction: 'vertical', showOnHover: true}, [
new EditorButton(cellProperties),
new EditorButton(mergeCells),
new EditorButton(splitCell),
]),
+ new EditorDropdownButton({button: {label: 'Row'}, direction: 'vertical', showOnHover: true}, [
+ new EditorButton({...insertRowAbove, format: 'long'}),
+ new EditorButton({...insertRowBelow, format: 'long'}),
+ new EditorButton({...deleteRow, format: 'long'}),
+ new EditorButton(rowProperties),
+ new EditorButton(cutRow),
+ new EditorButton(copyRow),
+ new EditorButton(pasteRowBefore),
+ new EditorButton(pasteRowAfter),
+ ]),
+ new EditorDropdownButton({button: {label: 'Column'}, direction: 'vertical', showOnHover: true}, [
+ new EditorButton({...insertColumnBefore, format: 'long'}),
+ new EditorButton({...insertColumnAfter, format: 'long'}),
+ new EditorButton({...deleteColumn, format: 'long'}),
+ new EditorButton(cutColumn),
+ new EditorButton(copyColumn),
+ new EditorButton(pasteColumnBefore),
+ new EditorButton(pasteColumnAfter),
+ ]),
+ new EditorButton({...tableProperties, format: 'long'}),
+ new EditorButton(clearTableFormatting),
+ new EditorButton(resizeTableToContents),
new EditorButton(deleteTableMenuAction),
]),
export function getTableToolbarContent(): EditorUiElement[] {
return [
new EditorOverflowContainer(2, [
- // Todo - Table properties
+ new EditorButton(tableProperties),
new EditorButton(deleteTable),
]),
new EditorOverflowContainer(3, [