Lexical: Started table menu options

Updated UI elements to handle new scenarios needed in more complex table
menu
This commit is contained in:
Dan Brown 2024-08-02 11:16:54 +01:00
parent 13f8f39dd5
commit 6b06d490c5
No known key found for this signature in database
GPG Key ID: 46D9F943C24A2EF9
9 changed files with 101 additions and 43 deletions

View File

@ -1,4 +1,4 @@
import {$isListNode, ListItemNode, ListNode, SerializedListItemNode} from "@lexical/list"; import {$isListNode, ListItemNode, SerializedListItemNode} from "@lexical/list";
import {EditorConfig} from "lexical/LexicalEditor"; import {EditorConfig} from "lexical/LexicalEditor";
import {DOMExportOutput, LexicalEditor, LexicalNode} from "lexical"; import {DOMExportOutput, LexicalEditor, LexicalNode} from "lexical";
import {el} from "../helpers"; import {el} from "../helpers";

View File

@ -2,13 +2,14 @@
## In progress ## In progress
// - Table features
- Continued table dropdown menu
## 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)
- Table features
- 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

@ -8,17 +8,18 @@ import insertColumnBeforeIcon from "@icons/editor/table-insert-column-before.svg
import insertRowAboveIcon from "@icons/editor/table-insert-row-above.svg"; 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 {$getBlockElementNodesInSelection, $getNodeFromSelection, $getParentOfType} from "../../../helpers";
import {$getSelection} from "lexical";
import {$isCustomTableNode, CustomTableNode} from "../../../nodes/custom-table";
import { import {
$deleteTableColumn, $deleteTableColumn__EXPERIMENTAL, $getNodeFromSelection,
$selectionContainsNodeType
} from "../../../helpers";
import {$getSelection} from "lexical";
import {$isCustomTableNode} from "../../../nodes/custom-table";
import {
$deleteTableColumn__EXPERIMENTAL,
$deleteTableRow__EXPERIMENTAL, $deleteTableRow__EXPERIMENTAL,
$getTableRowIndexFromTableCellNode, $insertTableColumn, $insertTableColumn__EXPERIMENTAL, $insertTableColumn__EXPERIMENTAL,
$insertTableRow, $insertTableRow__EXPERIMENTAL, $insertTableRow__EXPERIMENTAL,
$isTableCellNode, $isTableNode,
$isTableRowNode,
TableCellNode
} from "@lexical/table"; } from "@lexical/table";
@ -43,6 +44,14 @@ export const deleteTable: EditorButtonDefinition = {
} }
}; };
export const deleteTableMenuAction: EditorButtonDefinition = {
...deleteTable,
format: 'long',
isDisabled(selection) {
return !$selectionContainsNodeType(selection, $isTableNode);
},
};
export const insertRowAbove: EditorButtonDefinition = { export const insertRowAbove: EditorButtonDefinition = {
label: 'Insert row above', label: 'Insert row above',
icon: insertRowAboveIcon, icon: insertRowAboveIcon,

View File

@ -3,22 +3,34 @@ import {handleDropdown} from "../helpers/dropdowns";
import {EditorContainerUiElement, EditorUiElement} from "../core"; import {EditorContainerUiElement, EditorUiElement} from "../core";
import {EditorBasicButtonDefinition, EditorButton} from "../buttons"; import {EditorBasicButtonDefinition, EditorButton} from "../buttons";
export type EditorDropdownButtonOptions = {
showOnHover?: boolean;
direction?: 'vertical'|'horizontal';
button: EditorBasicButtonDefinition|EditorButton;
};
const defaultOptions: EditorDropdownButtonOptions = {
showOnHover: false,
direction: 'horizontal',
button: {label: 'Menu'},
}
export class EditorDropdownButton extends EditorContainerUiElement { export class EditorDropdownButton extends EditorContainerUiElement {
protected button: EditorButton; protected button: EditorButton;
protected childItems: EditorUiElement[]; protected childItems: EditorUiElement[];
protected open: boolean = false; protected open: boolean = false;
protected showOnHover: boolean = false; protected options: EditorDropdownButtonOptions;
constructor(button: EditorBasicButtonDefinition|EditorButton, showOnHover: boolean, children: EditorUiElement[]) { constructor(options: EditorDropdownButtonOptions, children: EditorUiElement[]) {
super(children); super(children);
this.childItems = children; this.childItems = children;
this.showOnHover = showOnHover; this.options = Object.assign(defaultOptions, options);
if (button instanceof EditorButton) { if (options.button instanceof EditorButton) {
this.button = button; this.button = options.button;
} else { } else {
this.button = new EditorButton({ this.button = new EditorButton({
...button, ...options.button,
action() { action() {
return false; return false;
}, },
@ -41,7 +53,7 @@ export class EditorDropdownButton extends EditorContainerUiElement {
const childElements: HTMLElement[] = this.childItems.map(child => child.getDOMElement()); const childElements: HTMLElement[] = this.childItems.map(child => child.getDOMElement());
const menu = el('div', { const menu = el('div', {
class: 'editor-dropdown-menu', class: `editor-dropdown-menu editor-dropdown-menu-${this.options.direction}`,
hidden: 'true', hidden: 'true',
}, childElements); }, childElements);
@ -50,7 +62,7 @@ export class EditorDropdownButton extends EditorContainerUiElement {
}, [button, menu]); }, [button, menu]);
handleDropdown({toggle : button, menu : menu, handleDropdown({toggle : button, menu : menu,
showOnHover: this.showOnHover, showOnHover: this.options.showOnHover,
onOpen : () => { onOpen : () => {
this.open = true; this.open = true;
this.getContext().manager.triggerStateUpdateForElement(this.button); this.getContext().manager.triggerStateUpdateForElement(this.button);

View File

@ -7,7 +7,7 @@ export class EditorFormatMenu extends EditorContainerUiElement {
buildDOM(): HTMLElement { buildDOM(): HTMLElement {
const childElements: HTMLElement[] = this.getChildren().map(child => child.getDOMElement()); const childElements: HTMLElement[] = this.getChildren().map(child => child.getDOMElement());
const menu = el('div', { const menu = el('div', {
class: 'editor-format-menu-dropdown editor-dropdown-menu editor-menu-list', class: 'editor-format-menu-dropdown editor-dropdown-menu editor-dropdown-menu-vertical',
hidden: 'true', hidden: 'true',
}, childElements); }, childElements);

View File

@ -15,9 +15,11 @@ export class EditorOverflowContainer extends EditorContainerUiElement {
this.size = size; this.size = size;
this.content = children; this.content = children;
this.overflowButton = new EditorDropdownButton({ this.overflowButton = new EditorDropdownButton({
button: {
label: 'More', label: 'More',
icon: moreHorizontal, icon: moreHorizontal,
}, false, []); },
}, []);
this.addChildren(this.overflowButton); this.addChildren(this.overflowButton);
} }

View File

@ -5,11 +5,13 @@ import {el} from "../../helpers";
export interface EditorBasicButtonDefinition { export interface EditorBasicButtonDefinition {
label: string; label: string;
icon?: string|undefined; icon?: string|undefined;
format?: 'small' | 'long';
} }
export interface EditorButtonDefinition extends EditorBasicButtonDefinition { export interface EditorButtonDefinition extends EditorBasicButtonDefinition {
action: (context: EditorUiContext, button: EditorButton) => void; action: (context: EditorUiContext, button: EditorButton) => void;
isActive: (selection: BaseSelection|null, context: EditorUiContext) => boolean; isActive: (selection: BaseSelection|null, context: EditorUiContext) => boolean;
isDisabled?: (selection: BaseSelection|null, context: EditorUiContext) => boolean;
setup?: (context: EditorUiContext, button: EditorButton) => void; setup?: (context: EditorUiContext, button: EditorButton) => void;
} }
@ -47,20 +49,27 @@ export class EditorButton extends EditorUiElement {
} }
protected buildDOM(): HTMLButtonElement { protected buildDOM(): HTMLButtonElement {
const label = this.getLabel(); const label = this.getLabel();
let child: string|HTMLElement = label; const format = this.definition.format || 'small';
if (this.definition.icon) { const children: (string|HTMLElement)[] = [];
child = el('div', {class: 'editor-button-icon'});
child.innerHTML = this.definition.icon; if (this.definition.icon || format === 'long') {
const icon = el('div', {class: 'editor-button-icon'});
icon.innerHTML = this.definition.icon || '';
children.push(icon);
}
if (!this.definition.icon ||format === 'long') {
const text = el('div', {class: 'editor-button-text'}, [label]);
children.push(text);
} }
const button = el('button', { const button = el('button', {
type: 'button', type: 'button',
class: 'editor-button', class: `editor-button editor-button-${format}`,
title: this.definition.icon ? label : null, title: this.definition.icon ? label : null,
disabled: this.disabled ? 'true' : null, disabled: this.disabled ? 'true' : null,
}, [child]) as HTMLButtonElement; }, children) as HTMLButtonElement;
button.addEventListener('click', this.onClick.bind(this)); button.addEventListener('click', this.onClick.bind(this));
@ -71,11 +80,18 @@ export class EditorButton extends EditorUiElement {
this.definition.action(this.getContext(), this); this.definition.action(this.getContext(), this);
} }
updateActiveState(selection: BaseSelection|null) { protected updateActiveState(selection: BaseSelection|null) {
const isActive = this.definition.isActive(selection, this.getContext()); const isActive = this.definition.isActive(selection, this.getContext());
this.setActiveState(isActive); this.setActiveState(isActive);
} }
protected updateDisabledState(selection: BaseSelection|null) {
if (this.definition.isDisabled) {
const isDisabled = this.definition.isDisabled(selection, this.getContext());
this.toggleDisabled(isDisabled);
}
}
setActiveState(active: boolean) { setActiveState(active: boolean) {
this.active = active; this.active = active;
this.dom?.classList.toggle('editor-button-active', this.active); this.dom?.classList.toggle('editor-button-active', this.active);
@ -83,6 +99,7 @@ export class EditorButton extends EditorUiElement {
updateState(state: EditorUiStateUpdate): void { updateState(state: EditorUiStateUpdate): void {
this.updateActiveState(state.selection); this.updateActiveState(state.selection);
this.updateDisabledState(state.selection);
} }
isActive(): boolean { isActive(): boolean {

View File

@ -1,6 +1,6 @@
import {EditorButton} from "./framework/buttons"; import {EditorButton} from "./framework/buttons";
import {EditorContainerUiElement, EditorSimpleClassContainer, EditorUiElement} from "./framework/core"; import {EditorContainerUiElement, EditorSimpleClassContainer, EditorUiElement} from "./framework/core";
import {el} from "../helpers"; import {$selectionContainsNodeType, el} from "../helpers";
import {EditorFormatMenu} from "./framework/blocks/format-menu"; import {EditorFormatMenu} from "./framework/blocks/format-menu";
import {FormatPreviewButton} from "./framework/blocks/format-preview-button"; import {FormatPreviewButton} from "./framework/blocks/format-preview-button";
import {EditorDropdownButton} from "./framework/blocks/dropdown-button"; import {EditorDropdownButton} from "./framework/blocks/dropdown-button";
@ -11,7 +11,7 @@ import {EditorOverflowContainer} from "./framework/blocks/overflow-container";
import { import {
deleteColumn, deleteColumn,
deleteRow, deleteRow,
deleteTable, insertColumnAfter, deleteTable, deleteTableMenuAction, insertColumnAfter,
insertColumnBefore, insertColumnBefore,
insertRowAbove, insertRowAbove,
insertRowBelow, insertRowBelow,
@ -50,6 +50,7 @@ import {
link, media, link, media,
unlink unlink
} from "./defaults/buttons/objects"; } from "./defaults/buttons/objects";
import {$isTableNode} from "@lexical/table";
export function getMainEditorFullToolbar(): EditorContainerUiElement { export function getMainEditorFullToolbar(): EditorContainerUiElement {
return new EditorSimpleClassContainer('editor-toolbar-main', [ return new EditorSimpleClassContainer('editor-toolbar-main', [
@ -68,7 +69,7 @@ export function getMainEditorFullToolbar(): EditorContainerUiElement {
new FormatPreviewButton(el('h5'), h5), new FormatPreviewButton(el('h5'), h5),
new FormatPreviewButton(el('blockquote'), blockquote), new FormatPreviewButton(el('blockquote'), blockquote),
new FormatPreviewButton(el('p'), paragraph), new FormatPreviewButton(el('p'), paragraph),
new EditorDropdownButton({label: 'Callouts'}, true, [ new EditorDropdownButton({button: {label: 'Callouts'}, showOnHover: true, direction: 'vertical'}, [
new FormatPreviewButton(el('p', {class: 'callout info'}), infoCallout), new FormatPreviewButton(el('p', {class: 'callout info'}), infoCallout),
new FormatPreviewButton(el('p', {class: 'callout success'}), successCallout), new FormatPreviewButton(el('p', {class: 'callout success'}), successCallout),
new FormatPreviewButton(el('p', {class: 'callout warning'}), warningCallout), new FormatPreviewButton(el('p', {class: 'callout warning'}), warningCallout),
@ -81,10 +82,10 @@ export function getMainEditorFullToolbar(): EditorContainerUiElement {
new EditorButton(bold), new EditorButton(bold),
new EditorButton(italic), new EditorButton(italic),
new EditorButton(underline), new EditorButton(underline),
new EditorDropdownButton(new EditorColorButton(textColor, 'color'), false, [ new EditorDropdownButton({ button: new EditorColorButton(textColor, 'color') }, [
new EditorColorPicker('color'), new EditorColorPicker('color'),
]), ]),
new EditorDropdownButton(new EditorColorButton(highlightColor, 'background-color'), false, [ new EditorDropdownButton({button: new EditorColorButton(highlightColor, 'background-color')}, [
new EditorColorPicker('background-color'), new EditorColorPicker('background-color'),
]), ]),
new EditorButton(strikethrough), new EditorButton(strikethrough),
@ -112,9 +113,14 @@ export function getMainEditorFullToolbar(): EditorContainerUiElement {
// Insert types // Insert types
new EditorOverflowContainer(8, [ new EditorOverflowContainer(8, [
new EditorButton(link), new EditorButton(link),
new EditorDropdownButton(table, false, [
new EditorDropdownButton({button: table, direction: 'vertical'}, [
new EditorDropdownButton({button: {...table, format: 'long'}, showOnHover: true}, [
new EditorTableCreator(), new EditorTableCreator(),
]), ]),
new EditorButton(deleteTableMenuAction),
]),
new EditorButton(image), new EditorButton(image),
new EditorButton(horizontalRule), new EditorButton(horizontalRule),
new EditorButton(codeBlock), new EditorButton(codeBlock),

View File

@ -59,6 +59,18 @@ body.editor-is-fullscreen {
background-color: #ceebff; background-color: #ceebff;
color: #000; color: #000;
} }
.editor-button-long {
display: flex !important;
flex-direction: row;
align-items: center;
justify-content: start;
gap: .5rem;
}
.editor-button-text {
font-weight: 400;
color: #000;
font-size: 12.2px;
}
.editor-button-format-preview { .editor-button-format-preview {
padding: 4px 6px; padding: 4px 6px;
display: block; display: block;
@ -84,21 +96,20 @@ body.editor-is-fullscreen {
display: flex; display: flex;
flex-direction: row; flex-direction: row;
} }
.editor-menu-list { .editor-dropdown-menu-vertical {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
align-items: stretch; align-items: stretch;
} }
.editor-menu-list .editor-button { .editor-dropdown-menu-vertical .editor-button {
border-bottom: 0; border-bottom: 0;
text-align: start; text-align: start;
display: block; display: block;
width: 100%; width: 100%;
} }
.editor-menu-list > .editor-dropdown-menu-container .editor-dropdown-menu { .editor-dropdown-menu-vertical > .editor-dropdown-menu-container .editor-dropdown-menu {
inset-inline-start: 100%; inset-inline-start: 100%;
top: 0; top: 0;
flex-direction: column;
} }
.editor-format-menu-toggle { .editor-format-menu-toggle {