Lexical: Completed out table menu elements, logic pending

This commit is contained in:
Dan Brown 2024-08-03 18:01:54 +01:00
parent a27a325af7
commit e94ad78ea7
No known key found for this signature in database
GPG Key ID: 46D9F943C24A2EF9
7 changed files with 409 additions and 66 deletions

View File

@ -3,15 +3,18 @@
## In progress ## In progress
- Table features - Table features
- Continued table dropdown menu - Cell properties form logic
- Connect up cell properties form
- Merge cell action - Merge cell action
- Row properties form logic
- Table properties form logic
- Caption text support
- Resize to contents button
- Remove formatting button
## Main Todo ## Main Todo
- Alignments: Use existing classes for blocks - Alignments: Use existing classes for blocks
- Alignments: Handle inline block content (image, video) - Alignments: Handle inline block content (image, video)
- Image paste upload - Image paste upload
- Keyboard shortcuts support - Keyboard shortcuts support
- Add ID support to all block types - Add ID support to all block types

View File

@ -9,25 +9,93 @@ import insertRowAboveIcon from "@icons/editor/table-insert-row-above.svg";
import insertRowBelowIcon from "@icons/editor/table-insert-row-below.svg"; import insertRowBelowIcon from "@icons/editor/table-insert-row-below.svg";
import {EditorUiContext} from "../../framework/core"; import {EditorUiContext} from "../../framework/core";
import { import {
$getNodeFromSelection, $getNodeFromSelection, $getParentOfType,
$selectionContainsNodeType $selectionContainsNodeType
} from "../../../helpers"; } from "../../../helpers";
import {$getSelection} from "lexical"; import {$getSelection, BaseSelection} from "lexical";
import {$isCustomTableNode} from "../../../nodes/custom-table"; import {$isCustomTableNode} from "../../../nodes/custom-table";
import { import {
$createTableRowNode,
$deleteTableColumn__EXPERIMENTAL, $deleteTableColumn__EXPERIMENTAL,
$deleteTableRow__EXPERIMENTAL, $deleteTableRow__EXPERIMENTAL,
$insertTableColumn__EXPERIMENTAL, $insertTableColumn__EXPERIMENTAL,
$insertTableRow__EXPERIMENTAL, $isTableCellNode, $insertTableRow__EXPERIMENTAL, $isTableCellNode,
$isTableNode, $isTableSelection, $unmergeCell, TableCellNode, $isTableNode, $isTableRowNode, $isTableSelection, $unmergeCell, TableCellNode, TableNode,
} from "@lexical/table"; } from "@lexical/table";
const neverActive = (): boolean => false;
const cellNotSelected = (selection: BaseSelection|null) => !$selectionContainsNodeType(selection, $isTableCellNode);
export const table: EditorBasicButtonDefinition = { export const table: EditorBasicButtonDefinition = {
label: 'Table', label: 'Table',
icon: tableIcon, 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 = { export const deleteTable: EditorButtonDefinition = {
label: 'Delete table', label: 'Delete table',
icon: deleteIcon, icon: deleteIcon,
@ -53,29 +121,27 @@ export const deleteTableMenuAction: EditorButtonDefinition = {
}; };
export const insertRowAbove: EditorButtonDefinition = { export const insertRowAbove: EditorButtonDefinition = {
label: 'Insert row above', label: 'Insert row before',
icon: insertRowAboveIcon, icon: insertRowAboveIcon,
action(context: EditorUiContext) { action(context: EditorUiContext) {
context.editor.update(() => { context.editor.update(() => {
$insertTableRow__EXPERIMENTAL(false); $insertTableRow__EXPERIMENTAL(false);
}); });
}, },
isActive() { isActive: neverActive,
return false; isDisabled: cellNotSelected,
}
}; };
export const insertRowBelow: EditorButtonDefinition = { export const insertRowBelow: EditorButtonDefinition = {
label: 'Insert row below', label: 'Insert row after',
icon: insertRowBelowIcon, icon: insertRowBelowIcon,
action(context: EditorUiContext) { action(context: EditorUiContext) {
context.editor.update(() => { context.editor.update(() => {
$insertTableRow__EXPERIMENTAL(true); $insertTableRow__EXPERIMENTAL(true);
}); });
}, },
isActive() { isActive: neverActive,
return false; isDisabled: cellNotSelected,
}
}; };
export const deleteRow: EditorButtonDefinition = { export const deleteRow: EditorButtonDefinition = {
@ -86,9 +152,124 @@ export const deleteRow: EditorButtonDefinition = {
$deleteTableRow__EXPERIMENTAL(); $deleteTableRow__EXPERIMENTAL();
}); });
}, },
isActive() { isActive: neverActive,
return false; 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 = { export const insertColumnBefore: EditorButtonDefinition = {
@ -142,12 +323,8 @@ export const cellProperties: EditorButtonDefinition = {
} }
}); });
}, },
isActive() { isActive: neverActive,
return false; isDisabled: cellNotSelected,
},
isDisabled(selection) {
return !$selectionContainsNodeType(selection, $isTableCellNode);
}
}; };
export const mergeCells: EditorButtonDefinition = { export const mergeCells: EditorButtonDefinition = {
@ -159,9 +336,7 @@ export const mergeCells: EditorButtonDefinition = {
// https://github.com/facebook/lexical/blob/f373759a7849f473d34960a6bf4e34b2a011e762/packages/lexical-playground/src/plugins/TableActionMenuPlugin/index.tsx#L299 // https://github.com/facebook/lexical/blob/f373759a7849f473d34960a6bf4e34b2a011e762/packages/lexical-playground/src/plugins/TableActionMenuPlugin/index.tsx#L299
}); });
}, },
isActive() { isActive: neverActive,
return false;
},
isDisabled(selection) { isDisabled(selection) {
return !$isTableSelection(selection); return !$isTableSelection(selection);
} }
@ -174,9 +349,7 @@ export const splitCell: EditorButtonDefinition = {
$unmergeCell(); $unmergeCell();
}); });
}, },
isActive() { isActive: neverActive,
return false;
},
isDisabled(selection) { isDisabled(selection) {
const cell = $getNodeFromSelection(selection, $isTableCellNode) as TableCellNode|null; const cell = $getNodeFromSelection(selection, $isTableCellNode) as TableCellNode|null;
if (cell) { if (cell) {

View File

@ -5,12 +5,54 @@ import {
EditorSelectFormFieldDefinition EditorSelectFormFieldDefinition
} from "../../framework/forms"; } from "../../framework/forms";
import {EditorUiContext} from "../../framework/core"; 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 = { export const cellProperties: EditorFormDefinition = {
submitText: 'Save', submitText: 'Save',
async action(formData, context: EditorUiContext) { async action(formData, context: EditorUiContext) {
setEditorContentFromHtml(context.editor, formData.get('source')?.toString() || ''); // TODO
return true; return true;
}, },
fields: [ fields: [
@ -37,16 +79,10 @@ export const cellProperties: EditorFormDefinition = {
} }
} as EditorSelectFormFieldDefinition, } as EditorSelectFormFieldDefinition,
{ {
...alignmentInput,
label: 'Horizontal align', label: 'Horizontal align',
name: 'h_align', name: 'h_align',
type: 'select', },
valuesByLabel: {
'None': '',
'Left': 'left',
'Center': 'center',
'Right': 'right',
}
} as EditorSelectFormFieldDefinition,
{ {
label: 'Vertical align', label: 'Vertical align',
name: 'v_align', name: 'v_align',
@ -66,34 +102,122 @@ export const cellProperties: EditorFormDefinition = {
name: 'border_width', name: 'border_width',
type: 'text', type: 'text',
}, },
{ borderStyleInput,
label: 'Border style', borderColorInput,
name: 'border_style', backgroundColorInput,
type: 'select', ];
valuesByLabel: {
'Select...': '', return new EditorFormTabs([
"Solid": 'solid', {
"Dotted": 'dotted', label: 'General',
"Dashed": 'dashed', contents: generalFields,
"Double": 'double', },
"Groove": 'groove', {
"Ridge": 'ridge', label: 'Advanced',
"Inset": 'inset', contents: advancedFields,
"Outset": 'outset', }
"None": 'none', ])
"Hidden": 'hidden', }
} },
} as EditorSelectFormFieldDefinition, ],
{ };
label: 'Border color',
name: 'border_color', export const rowProperties: EditorFormDefinition = {
type: 'text', submitText: 'Save',
}, async action(formData, context: EditorUiContext) {
{ // TODO
label: 'Background color', return true;
name: 'background_color', },
type: 'text', fields: [
}, {
build() {
const generalFields: EditorFormFieldDefinition[] = [
{
label: 'Row type',
name: 'type',
type: 'select',
valuesByLabel: {
'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: 'Cell spacing',
name: 'cell_spacing',
type: 'text',
},
{
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([ return new EditorFormTabs([

View File

@ -1,7 +1,7 @@
import {EditorFormModalDefinition} from "../framework/modals"; import {EditorFormModalDefinition} from "../framework/modals";
import {image, link, media} from "./forms/objects"; import {image, link, media} from "./forms/objects";
import {source} from "./forms/controls"; import {source} from "./forms/controls";
import {cellProperties} from "./forms/tables"; import {cellProperties, rowProperties, tableProperties} from "./forms/tables";
export const modals: Record<string, EditorFormModalDefinition> = { export const modals: Record<string, EditorFormModalDefinition> = {
link: { link: {
@ -24,4 +24,12 @@ export const modals: Record<string, EditorFormModalDefinition> = {
title: 'Cell Properties', title: 'Cell Properties',
form: cellProperties, form: cellProperties,
}, },
row_properties: {
title: 'Row Properties',
form: rowProperties,
},
table_properties: {
title: 'Table Properties',
form: tableProperties,
},
}; };

View File

@ -24,7 +24,7 @@ export class EditorDropdownButton extends EditorContainerUiElement {
constructor(options: EditorDropdownButtonOptions, children: EditorUiElement[]) { constructor(options: EditorDropdownButtonOptions, children: EditorUiElement[]) {
super(children); super(children);
this.childItems = children; this.childItems = children;
this.options = Object.assign(defaultOptions, options); this.options = Object.assign({}, defaultOptions, options);
if (options.button instanceof EditorButton) { if (options.button instanceof EditorButton) {
this.button = options.button; this.button = options.button;

View File

@ -44,5 +44,5 @@ export function handleDropdown(options: HandleDropdownParams) {
toggle.addEventListener('mouseenter', toggleShowing); toggle.addEventListener('mouseenter', toggleShowing);
} }
menu.addEventListener('mouseleave', hide); menu.parentElement?.addEventListener('mouseleave', hide);
} }

View File

@ -9,14 +9,27 @@ import {EditorTableCreator} from "./framework/blocks/table-creator";
import {EditorColorButton} from "./framework/blocks/color-button"; import {EditorColorButton} from "./framework/blocks/color-button";
import {EditorOverflowContainer} from "./framework/blocks/overflow-container"; import {EditorOverflowContainer} from "./framework/blocks/overflow-container";
import { import {
cellProperties, cellProperties, clearTableFormatting,
copyColumn,
copyRow,
cutColumn,
cutRow,
deleteColumn, deleteColumn,
deleteRow, deleteRow,
deleteTable, deleteTableMenuAction, insertColumnAfter, deleteTable,
deleteTableMenuAction,
insertColumnAfter,
insertColumnBefore, insertColumnBefore,
insertRowAbove, insertRowAbove,
insertRowBelow, mergeCells, splitCell, insertRowBelow,
table mergeCells,
pasteColumnAfter,
pasteColumnBefore,
pasteRowAfter,
pasteRowBefore, resizeTableToContents,
rowProperties,
splitCell,
table, tableProperties
} from "./defaults/buttons/tables"; } from "./defaults/buttons/tables";
import {fullscreen, redo, source, undo} from "./defaults/buttons/controls"; import {fullscreen, redo, source, undo} from "./defaults/buttons/controls";
import { import {
@ -119,11 +132,33 @@ export function getMainEditorFullToolbar(): EditorContainerUiElement {
new EditorDropdownButton({button: {...table, format: 'long'}, showOnHover: true}, [ new EditorDropdownButton({button: {...table, format: 'long'}, showOnHover: true}, [
new EditorTableCreator(), new EditorTableCreator(),
]), ]),
new EditorDropdownButton({button: {label: 'Cell'}}, [ new EditorDropdownButton({button: {label: 'Cell'}, direction: 'vertical', showOnHover: true}, [
new EditorButton(cellProperties), new EditorButton(cellProperties),
new EditorButton(mergeCells), new EditorButton(mergeCells),
new EditorButton(splitCell), 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), new EditorButton(deleteTableMenuAction),
]), ]),
@ -176,7 +211,7 @@ export function getCodeToolbarContent(): EditorUiElement[] {
export function getTableToolbarContent(): EditorUiElement[] { export function getTableToolbarContent(): EditorUiElement[] {
return [ return [
new EditorOverflowContainer(2, [ new EditorOverflowContainer(2, [
// Todo - Table properties new EditorButton(tableProperties),
new EditorButton(deleteTable), new EditorButton(deleteTable),
]), ]),
new EditorOverflowContainer(3, [ new EditorOverflowContainer(3, [