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
- 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

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 {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,
@ -53,29 +121,27 @@ export const deleteTableMenuAction: EditorButtonDefinition = {
};
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 = {
@ -86,9 +152,124 @@ 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 = {
@ -142,12 +323,8 @@ export const cellProperties: EditorButtonDefinition = {
}
});
},
isActive() {
return false;
},
isDisabled(selection) {
return !$selectionContainsNodeType(selection, $isTableCellNode);
}
isActive: neverActive,
isDisabled: cellNotSelected,
};
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
});
},
isActive() {
return false;
},
isActive: neverActive,
isDisabled(selection) {
return !$isTableSelection(selection);
}
@ -174,9 +349,7 @@ export const splitCell: EditorButtonDefinition = {
$unmergeCell();
});
},
isActive() {
return false;
},
isActive: neverActive,
isDisabled(selection) {
const cell = $getNodeFromSelection(selection, $isTableCellNode) as TableCellNode|null;
if (cell) {

View File

@ -5,12 +5,54 @@ import {
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: [
@ -37,16 +79,10 @@ export const cellProperties: EditorFormDefinition = {
}
} 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',
@ -66,34 +102,122 @@ export const cellProperties: EditorFormDefinition = {
name: 'border_width',
type: 'text',
},
borderStyleInput,
borderColorInput,
backgroundColorInput,
];
return new EditorFormTabs([
{
label: 'Border style',
name: 'border_style',
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: '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: 'Border color',
name: 'border_color',
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: 'Background color',
name: 'background_color',
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([

View File

@ -1,7 +1,7 @@
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: {
@ -24,4 +24,12 @@ export const modals: Record<string, EditorFormModalDefinition> = {
title: 'Cell Properties',
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[]) {
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;
@ -61,7 +61,7 @@ export class EditorDropdownButton extends EditorContainerUiElement {
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;

View File

@ -44,5 +44,5 @@ export function handleDropdown(options: HandleDropdownParams) {
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 {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 {
@ -119,11 +132,33 @@ export function getMainEditorFullToolbar(): EditorContainerUiElement {
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),
]),
@ -176,7 +211,7 @@ export function getCodeToolbarContent(): EditorUiElement[] {
export function getTableToolbarContent(): EditorUiElement[] {
return [
new EditorOverflowContainer(2, [
// Todo - Table properties
new EditorButton(tableProperties),
new EditorButton(deleteTable),
]),
new EditorOverflowContainer(3, [