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 {DOMExportOutput, LexicalEditor, LexicalNode} from "lexical";
import {el} from "../helpers";

View File

@ -2,13 +2,14 @@
## In progress
//
- Table features
- Continued table dropdown menu
## Main Todo
- Alignments: Use existing classes for blocks
- Alignments: Handle inline block content (image, video)
- Table features
- Image paste upload
- Keyboard shortcuts support
- 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 insertRowBelowIcon from "@icons/editor/table-insert-row-below.svg";
import {EditorUiContext} from "../../framework/core";
import {$getBlockElementNodesInSelection, $getNodeFromSelection, $getParentOfType} from "../../../helpers";
import {$getSelection} from "lexical";
import {$isCustomTableNode, CustomTableNode} from "../../../nodes/custom-table";
import {
$deleteTableColumn, $deleteTableColumn__EXPERIMENTAL,
$getNodeFromSelection,
$selectionContainsNodeType
} from "../../../helpers";
import {$getSelection} from "lexical";
import {$isCustomTableNode} from "../../../nodes/custom-table";
import {
$deleteTableColumn__EXPERIMENTAL,
$deleteTableRow__EXPERIMENTAL,
$getTableRowIndexFromTableCellNode, $insertTableColumn, $insertTableColumn__EXPERIMENTAL,
$insertTableRow, $insertTableRow__EXPERIMENTAL,
$isTableCellNode,
$isTableRowNode,
TableCellNode
$insertTableColumn__EXPERIMENTAL,
$insertTableRow__EXPERIMENTAL,
$isTableNode,
} 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 = {
label: 'Insert row above',
icon: insertRowAboveIcon,

View File

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

View File

@ -7,7 +7,7 @@ export class EditorFormatMenu extends EditorContainerUiElement {
buildDOM(): HTMLElement {
const childElements: HTMLElement[] = this.getChildren().map(child => child.getDOMElement());
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',
}, childElements);

View File

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

View File

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

View File

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

View File

@ -59,6 +59,18 @@ body.editor-is-fullscreen {
background-color: #ceebff;
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 {
padding: 4px 6px;
display: block;
@ -84,21 +96,20 @@ body.editor-is-fullscreen {
display: flex;
flex-direction: row;
}
.editor-menu-list {
.editor-dropdown-menu-vertical {
display: flex;
flex-direction: column;
align-items: stretch;
}
.editor-menu-list .editor-button {
.editor-dropdown-menu-vertical .editor-button {
border-bottom: 0;
text-align: start;
display: block;
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%;
top: 0;
flex-direction: column;
}
.editor-format-menu-toggle {