-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";
## 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
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 {
+ $getNodeFromSelection,
+ $selectionContainsNodeType
+} from "../../../helpers";
import {$getSelection} from "lexical";
-import {$isCustomTableNode, CustomTableNode} from "../../../nodes/custom-table";
+import {$isCustomTableNode} from "../../../nodes/custom-table";
import {
- $deleteTableColumn, $deleteTableColumn__EXPERIMENTAL,
+ $deleteTableColumn__EXPERIMENTAL,
$deleteTableRow__EXPERIMENTAL,
- $getTableRowIndexFromTableCellNode, $insertTableColumn, $insertTableColumn__EXPERIMENTAL,
- $insertTableRow, $insertTableRow__EXPERIMENTAL,
- $isTableCellNode,
- $isTableRowNode,
- TableCellNode
+ $insertTableColumn__EXPERIMENTAL,
+ $insertTableRow__EXPERIMENTAL,
+ $isTableNode,
} from "@lexical/table";
}
};
+export const deleteTableMenuAction: EditorButtonDefinition = {
+ ...deleteTable,
+ format: 'long',
+ isDisabled(selection) {
+ return !$selectionContainsNodeType(selection, $isTableNode);
+ },
+};
+
export const insertRowAbove: EditorButtonDefinition = {
label: 'Insert row above',
icon: insertRowAboveIcon,
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;
},
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);
}, [button, menu]);
handleDropdown({toggle : button, menu : menu,
- showOnHover: this.showOnHover,
+ showOnHover: this.options.showOnHover,
onOpen : () => {
this.open = true;
this.getContext().manager.triggerStateUpdateForElement(this.button);
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);
this.size = size;
this.content = children;
this.overflowButton = new EditorDropdownButton({
- label: 'More',
- icon: moreHorizontal,
- }, false, []);
+ button: {
+ label: 'More',
+ icon: moreHorizontal,
+ },
+ }, []);
this.addChildren(this.overflowButton);
}
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;
}
}
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));
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);
updateState(state: EditorUiStateUpdate): void {
this.updateActiveState(state.selection);
+ this.updateDisabledState(state.selection);
}
isActive(): boolean {
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";
import {
deleteColumn,
deleteRow,
- deleteTable, insertColumnAfter,
+ deleteTable, deleteTableMenuAction, insertColumnAfter,
insertColumnBefore,
insertRowAbove,
insertRowBelow,
link, media,
unlink
} from "./defaults/buttons/objects";
+import {$isTableNode} from "@lexical/table";
export function getMainEditorFullToolbar(): EditorContainerUiElement {
return new EditorSimpleClassContainer('editor-toolbar-main', [
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),
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),
// Insert types
new EditorOverflowContainer(8, [
new EditorButton(link),
- new EditorDropdownButton(table, false, [
- new EditorTableCreator(),
+
+ 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),
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;
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 {