From b6182875858d637f19f49a2a98c61887a0c5c5d3 Mon Sep 17 00:00:00 2001
From: Dan Brown <ssddanbrown@googlemail.com>
Date: Sun, 21 Jul 2024 15:11:24 +0100
Subject: [PATCH] Lexical: Added table toolbar, organised button code

---
 .../icons/editor/table-delete-column.svg      |   1 +
 resources/icons/editor/table-delete-row.svg   |   1 +
 resources/icons/editor/table-delete.svg       |   1 +
 .../editor/table-insert-column-after.svg      |   1 +
 .../editor/table-insert-column-before.svg     |   1 +
 .../icons/editor/table-insert-row-above.svg   |   1 +
 .../icons/editor/table-insert-row-below.svg   |   1 +
 resources/js/wysiwyg/helpers.ts               |  17 +-
 resources/js/wysiwyg/todo.md                  |  12 +-
 .../wysiwyg/ui/defaults/button-definitions.ts | 528 ------------------
 .../wysiwyg/ui/defaults/buttons/alignments.ts |  61 ++
 .../ui/defaults/buttons/block-formats.ts      |  85 +++
 .../wysiwyg/ui/defaults/buttons/controls.ts   |  81 +++
 .../ui/defaults/buttons/inline-formats.ts     |  56 ++
 .../js/wysiwyg/ui/defaults/buttons/lists.ts   |  35 ++
 .../js/wysiwyg/ui/defaults/buttons/objects.ts | 215 +++++++
 .../js/wysiwyg/ui/defaults/buttons/tables.ts  | 122 ++++
 resources/js/wysiwyg/ui/index.ts              |  10 +-
 resources/js/wysiwyg/ui/toolbars.ts           |  74 ++-
 19 files changed, 756 insertions(+), 547 deletions(-)
 create mode 100644 resources/icons/editor/table-delete-column.svg
 create mode 100644 resources/icons/editor/table-delete-row.svg
 create mode 100644 resources/icons/editor/table-delete.svg
 create mode 100644 resources/icons/editor/table-insert-column-after.svg
 create mode 100644 resources/icons/editor/table-insert-column-before.svg
 create mode 100644 resources/icons/editor/table-insert-row-above.svg
 create mode 100644 resources/icons/editor/table-insert-row-below.svg
 delete mode 100644 resources/js/wysiwyg/ui/defaults/button-definitions.ts
 create mode 100644 resources/js/wysiwyg/ui/defaults/buttons/alignments.ts
 create mode 100644 resources/js/wysiwyg/ui/defaults/buttons/block-formats.ts
 create mode 100644 resources/js/wysiwyg/ui/defaults/buttons/controls.ts
 create mode 100644 resources/js/wysiwyg/ui/defaults/buttons/inline-formats.ts
 create mode 100644 resources/js/wysiwyg/ui/defaults/buttons/lists.ts
 create mode 100644 resources/js/wysiwyg/ui/defaults/buttons/objects.ts
 create mode 100644 resources/js/wysiwyg/ui/defaults/buttons/tables.ts

diff --git a/resources/icons/editor/table-delete-column.svg b/resources/icons/editor/table-delete-column.svg
new file mode 100644
index 000000000..428fc29a6
--- /dev/null
+++ b/resources/icons/editor/table-delete-column.svg
@@ -0,0 +1 @@
+<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path d="M21 19a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h14c1.1 0 2 .9 2 2zm-2 0V5h-4v2.2h-2V5h-2v2.2H9V5H5v14h4v-2.1h2V19h2v-2.1h2V19Z"/><path d="M14.829 10.585 13.415 12l1.414 1.414c.943.943-.472 2.357-1.414 1.414L12 13.414l-1.414 1.414c-.944.944-2.358-.47-1.414-1.414L10.586 12l-1.414-1.415c-.943-.942.471-2.357 1.414-1.414L12 10.585l1.344-1.343c1.111-1.112 2.2.627 1.485 1.343z" style="fill-rule:nonzero"/></svg>
\ No newline at end of file
diff --git a/resources/icons/editor/table-delete-row.svg b/resources/icons/editor/table-delete-row.svg
new file mode 100644
index 000000000..ee2f8a00d
--- /dev/null
+++ b/resources/icons/editor/table-delete-row.svg
@@ -0,0 +1 @@
+<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path d="M5 21a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h14a2 2 0 0 1 2 2v14c0 1.1-.9 2-2 2zm0-2h14v-4h-2.2v-2H19v-2h-2.2V9H19V5H5v4h2.1v2H5v2h2.1v2H5Z"/><path d="M13.415 14.829 12 13.415l-1.414 1.414c-.943.943-2.357-.472-1.414-1.414L10.586 12l-1.414-1.414c-.944-.944.47-2.358 1.414-1.414L12 10.586l1.415-1.414c.942-.943 2.357.471 1.414 1.414L13.415 12l1.343 1.344c1.112 1.111-.627 2.2-1.343 1.485z" style="fill-rule:nonzero"/></svg>
\ No newline at end of file
diff --git a/resources/icons/editor/table-delete.svg b/resources/icons/editor/table-delete.svg
new file mode 100644
index 000000000..412cf0732
--- /dev/null
+++ b/resources/icons/editor/table-delete.svg
@@ -0,0 +1 @@
+<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path d="M5 21a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h14a2 2 0 0 1 2 2v14c0 1.1-.9 2-2 2zm0-2h14V5H5v14z"/><path d="m13.711 15.423-1.71-1.712-1.712 1.712c-1.14 1.14-2.852-.57-1.71-1.712l1.71-1.71-1.71-1.712c-1.143-1.142.568-2.853 1.71-1.71L12 10.288l1.711-1.71c1.141-1.142 2.852.57 1.712 1.71L13.71 12l1.626 1.626c1.345 1.345-.76 2.663-1.626 1.797z" style="fill-rule:nonzero;stroke-width:1.20992"/></svg>
\ No newline at end of file
diff --git a/resources/icons/editor/table-insert-column-after.svg b/resources/icons/editor/table-insert-column-after.svg
new file mode 100644
index 000000000..75abd9a85
--- /dev/null
+++ b/resources/icons/editor/table-insert-column-after.svg
@@ -0,0 +1 @@
+<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path d="M16 5h-5v14h5c1.235 0 1.234 2 0 2H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h11c1.229 0 1.236 2 0 2zm-7 6V5H5v6zm0 8v-6H5v6zm11.076-6h-2v2c0 1.333-2 1.333-2 0v-2h-2c-1.335 0-1.335-2 0-2h2V9c0-1.333 2-1.333 2 0v2h1.9c1.572 0 1.113 2 .1 2z"/></svg>
\ No newline at end of file
diff --git a/resources/icons/editor/table-insert-column-before.svg b/resources/icons/editor/table-insert-column-before.svg
new file mode 100644
index 000000000..5bb38cd29
--- /dev/null
+++ b/resources/icons/editor/table-insert-column-before.svg
@@ -0,0 +1 @@
+<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path d="M8 19h5V5H8C6.764 5 6.766 3 8 3h11a2 2 0 0 1 2 2v14a2 2 0 0 1-2 2H8c-1.229 0-1.236-2 0-2zm7-6v6h4v-6zm0-8v6h4V5ZM3.924 11h2V9c0-1.333 2-1.333 2 0v2h2c1.335 0 1.335 2 0 2h-2v2c0 1.333-2 1.333-2 0v-2h-1.9c-1.572 0-1.113-2-.1-2z"/></svg>
\ No newline at end of file
diff --git a/resources/icons/editor/table-insert-row-above.svg b/resources/icons/editor/table-insert-row-above.svg
new file mode 100644
index 000000000..df951485a
--- /dev/null
+++ b/resources/icons/editor/table-insert-row-above.svg
@@ -0,0 +1 @@
+<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path d="M5 8v5h14V8c0-1.235 2-1.234 2 0v11a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V8C3 6.77 5 6.764 5 8zm6 7H5v4h6zm8 0h-6v4h6zM13 3.924v2h2c1.333 0 1.333 2 0 2h-2v2c0 1.335-2 1.335-2 0v-2H9c-1.333 0-1.333-2 0-2h2v-1.9c0-1.572 2-1.113 2-.1z"/></svg>
\ No newline at end of file
diff --git a/resources/icons/editor/table-insert-row-below.svg b/resources/icons/editor/table-insert-row-below.svg
new file mode 100644
index 000000000..b2af77592
--- /dev/null
+++ b/resources/icons/editor/table-insert-row-below.svg
@@ -0,0 +1 @@
+<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path d="M19 16v-5H5v5c0 1.235-2 1.234-2 0V5a2 2 0 0 1 2-2h14a2 2 0 0 1 2 2v11c0 1.229-2 1.236-2 0zm-6-7h6V5h-6zM5 9h6V5H5Zm6 11.076v-2H9c-1.333 0-1.333-2 0-2h2v-2c0-1.335 2-1.335 2 0v2h2c1.333 0 1.333 2 0 2h-2v1.9c0 1.572-2 1.113-2 .1z"/></svg>
\ No newline at end of file
diff --git a/resources/js/wysiwyg/helpers.ts b/resources/js/wysiwyg/helpers.ts
index a7c3e4453..6a55c429c 100644
--- a/resources/js/wysiwyg/helpers.ts
+++ b/resources/js/wysiwyg/helpers.ts
@@ -44,10 +44,19 @@ export function $getNodeFromSelection(selection: BaseSelection|null, matcher: Le
             return node;
         }
 
-        for (const parent of node.getParents()) {
-            if (matcher(parent)) {
-                return parent;
-            }
+        const matchedParent = $getParentOfType(node, matcher);
+        if (matchedParent) {
+            return matchedParent;
+        }
+    }
+
+    return null;
+}
+
+export function $getParentOfType(node: LexicalNode, matcher: LexicalNodeMatcher): LexicalNode|null {
+    for (const parent of node.getParents()) {
+        if (matcher(parent)) {
+            return parent;
         }
     }
 
diff --git a/resources/js/wysiwyg/todo.md b/resources/js/wysiwyg/todo.md
index 9950254df..7b9588194 100644
--- a/resources/js/wysiwyg/todo.md
+++ b/resources/js/wysiwyg/todo.md
@@ -2,12 +2,22 @@
 
 ## In progress
 
+- Add Type: Video/media/embed
+    - TinyMce media embed supported:
+      - iframe
+      - embed
+      - object
+      - video - Can take sources
+      - audio  - Can take sources
+    - Pretty much all attributes look like they were supported.
+    - Core old logic seen here: https://github.com/tinymce/tinymce/blob/main/modules/tinymce/src/plugins/media/main/ts/core/DataToHtml.ts
+    - Copy/store attributes on node based on allow list?
+      - width, height, src, controls, etc... Take valid values from MDN
 
 ## Main Todo
 
 - Alignments: Use existing classes for blocks
 - Alignments: Handle inline block content (image, video)
-- Add Type: Video/media/embed
 - Table features
 - Image paste upload
 - Keyboard shortcuts support
diff --git a/resources/js/wysiwyg/ui/defaults/button-definitions.ts b/resources/js/wysiwyg/ui/defaults/button-definitions.ts
deleted file mode 100644
index 5316dacf7..000000000
--- a/resources/js/wysiwyg/ui/defaults/button-definitions.ts
+++ /dev/null
@@ -1,528 +0,0 @@
-import {EditorBasicButtonDefinition, EditorButton, EditorButtonDefinition} from "../framework/buttons";
-import {
-    $createNodeSelection,
-    $createParagraphNode,
-    $createTextNode,
-    $getRoot,
-    $getSelection,
-    $isParagraphNode,
-    $isTextNode,
-    $setSelection,
-    BaseSelection,
-    CAN_REDO_COMMAND,
-    CAN_UNDO_COMMAND,
-    COMMAND_PRIORITY_LOW,
-    ElementFormatType,
-    ElementNode,
-    FORMAT_TEXT_COMMAND,
-    LexicalNode,
-    REDO_COMMAND,
-    TextFormatType,
-    UNDO_COMMAND
-} from "lexical";
-import {
-    $getBlockElementNodesInSelection,
-    $getNodeFromSelection, $insertNewBlockNodeAtSelection, $selectionContainsElementFormat,
-    $selectionContainsNodeType,
-    $selectionContainsTextFormat,
-    $toggleSelectionBlockNodeType
-} from "../../helpers";
-import {$createCalloutNode, $isCalloutNodeOfCategory, CalloutCategory} from "../../nodes/callout";
-import {
-    $createHeadingNode,
-    $createQuoteNode,
-    $isHeadingNode,
-    $isQuoteNode,
-    HeadingNode,
-    HeadingTagType
-} from "@lexical/rich-text";
-import {$isLinkNode, LinkNode} from "@lexical/link";
-import {EditorUiContext} from "../framework/core";
-import {$isImageNode, ImageNode} from "../../nodes/image";
-import {$createDetailsNode, $isDetailsNode} from "../../nodes/details";
-import {getEditorContentAsHtml} from "../../actions";
-import {$isListNode, insertList, ListNode, ListType, removeList} from "@lexical/list";
-import undoIcon from "@icons/editor/undo.svg";
-import redoIcon from "@icons/editor/redo.svg";
-import boldIcon from "@icons/editor/bold.svg";
-import italicIcon from "@icons/editor/italic.svg";
-import underlinedIcon from "@icons/editor/underlined.svg";
-import textColorIcon from "@icons/editor/text-color.svg";
-import highlightIcon from "@icons/editor/highlighter.svg";
-import strikethroughIcon from "@icons/editor/strikethrough.svg";
-import superscriptIcon from "@icons/editor/superscript.svg";
-import subscriptIcon from "@icons/editor/subscript.svg";
-import codeIcon from "@icons/editor/code.svg";
-import formatClearIcon from "@icons/editor/format-clear.svg";
-import alignLeftIcon from "@icons/editor/align-left.svg";
-import alignCenterIcon from "@icons/editor/align-center.svg";
-import alignRightIcon from "@icons/editor/align-right.svg";
-import alignJustifyIcon from "@icons/editor/align-justify.svg";
-import listBulletIcon from "@icons/editor/list-bullet.svg";
-import listNumberedIcon from "@icons/editor/list-numbered.svg";
-import listCheckIcon from "@icons/editor/list-check.svg";
-import linkIcon from "@icons/editor/link.svg";
-import unlinkIcon from "@icons/editor/unlink.svg";
-import tableIcon from "@icons/editor/table.svg";
-import imageIcon from "@icons/editor/image.svg";
-import horizontalRuleIcon from "@icons/editor/horizontal-rule.svg";
-import codeBlockIcon from "@icons/editor/code-block.svg";
-import diagramIcon from "@icons/editor/diagram.svg";
-import detailsIcon from "@icons/editor/details.svg";
-import sourceIcon from "@icons/editor/source-view.svg";
-import fullscreenIcon from "@icons/editor/fullscreen.svg";
-import editIcon from "@icons/edit.svg";
-import {$createHorizontalRuleNode, $isHorizontalRuleNode} from "../../nodes/horizontal-rule";
-import {$createCodeBlockNode, $isCodeBlockNode, $openCodeEditorForNode, CodeBlockNode} from "../../nodes/code-block";
-import {$createDiagramNode, $isDiagramNode, $openDrawingEditorForNode, DiagramNode} from "../../nodes/diagram";
-
-export const undo: EditorButtonDefinition = {
-    label: 'Undo',
-    icon: undoIcon,
-    action(context: EditorUiContext) {
-        context.editor.dispatchCommand(UNDO_COMMAND, undefined);
-    },
-    isActive(selection: BaseSelection|null): boolean {
-        return false;
-    },
-    setup(context: EditorUiContext, button: EditorButton) {
-        button.toggleDisabled(true);
-
-        context.editor.registerCommand(CAN_UNDO_COMMAND, (payload: boolean): boolean => {
-            button.toggleDisabled(!payload)
-            return false;
-        }, COMMAND_PRIORITY_LOW);
-    }
-}
-
-export const redo: EditorButtonDefinition = {
-    label: 'Redo',
-    icon: redoIcon,
-    action(context: EditorUiContext) {
-        context.editor.dispatchCommand(REDO_COMMAND, undefined);
-    },
-    isActive(selection: BaseSelection|null): boolean {
-        return false;
-    },
-    setup(context: EditorUiContext, button: EditorButton) {
-        button.toggleDisabled(true);
-
-        context.editor.registerCommand(CAN_REDO_COMMAND, (payload: boolean): boolean => {
-            button.toggleDisabled(!payload)
-            return false;
-        }, COMMAND_PRIORITY_LOW);
-    }
-}
-
-function buildCalloutButton(category: CalloutCategory, name: string): EditorButtonDefinition {
-    return {
-        label: `${name} Callout`,
-        action(context: EditorUiContext) {
-            context.editor.update(() => {
-                $toggleSelectionBlockNodeType(
-                    (node) => $isCalloutNodeOfCategory(node, category),
-                    () => $createCalloutNode(category),
-                )
-            });
-        },
-        isActive(selection: BaseSelection|null): boolean {
-            return $selectionContainsNodeType(selection, (node) => $isCalloutNodeOfCategory(node, category));
-        }
-    };
-}
-
-export const infoCallout: EditorButtonDefinition = buildCalloutButton('info', 'Info');
-export const dangerCallout: EditorButtonDefinition = buildCalloutButton('danger', 'Danger');
-export const warningCallout: EditorButtonDefinition = buildCalloutButton('warning', 'Warning');
-export const successCallout: EditorButtonDefinition = buildCalloutButton('success', 'Success');
-
-const isHeaderNodeOfTag = (node: LexicalNode | null | undefined, tag: HeadingTagType) => {
-      return $isHeadingNode(node) && (node as HeadingNode).getTag() === tag;
-};
-
-function buildHeaderButton(tag: HeadingTagType, name: string): EditorButtonDefinition {
-    return {
-        label: name,
-        action(context: EditorUiContext) {
-            context.editor.update(() => {
-                $toggleSelectionBlockNodeType(
-                (node) => isHeaderNodeOfTag(node, tag),
-                () => $createHeadingNode(tag),
-                )
-            });
-        },
-        isActive(selection: BaseSelection|null): boolean {
-            return $selectionContainsNodeType(selection, (node) => isHeaderNodeOfTag(node, tag));
-        }
-    };
-}
-
-export const h2: EditorButtonDefinition = buildHeaderButton('h2', 'Large Header');
-export const h3: EditorButtonDefinition = buildHeaderButton('h3', 'Medium Header');
-export const h4: EditorButtonDefinition = buildHeaderButton('h4', 'Small Header');
-export const h5: EditorButtonDefinition = buildHeaderButton('h5', 'Tiny Header');
-
-export const blockquote: EditorButtonDefinition = {
-    label: 'Blockquote',
-    action(context: EditorUiContext) {
-        context.editor.update(() => {
-            $toggleSelectionBlockNodeType($isQuoteNode, $createQuoteNode);
-        });
-    },
-    isActive(selection: BaseSelection|null): boolean {
-        return $selectionContainsNodeType(selection, $isQuoteNode);
-    }
-};
-
-export const paragraph: EditorButtonDefinition = {
-    label: 'Paragraph',
-    action(context: EditorUiContext) {
-        context.editor.update(() => {
-            $toggleSelectionBlockNodeType($isParagraphNode, $createParagraphNode);
-        });
-    },
-    isActive(selection: BaseSelection|null): boolean {
-        return $selectionContainsNodeType(selection, $isParagraphNode);
-    }
-}
-
-function buildFormatButton(label: string, format: TextFormatType, icon: string): EditorButtonDefinition {
-    return {
-        label: label,
-        icon,
-        action(context: EditorUiContext) {
-            context.editor.dispatchCommand(FORMAT_TEXT_COMMAND, format);
-        },
-        isActive(selection: BaseSelection|null): boolean {
-            return $selectionContainsTextFormat(selection, format);
-        }
-    };
-}
-
-export const bold: EditorButtonDefinition = buildFormatButton('Bold', 'bold', boldIcon);
-export const italic: EditorButtonDefinition = buildFormatButton('Italic', 'italic', italicIcon);
-export const underline: EditorButtonDefinition = buildFormatButton('Underline', 'underline', underlinedIcon);
-export const textColor: EditorBasicButtonDefinition = {label: 'Text color', icon: textColorIcon};
-export const highlightColor: EditorBasicButtonDefinition = {label: 'Highlight color', icon: highlightIcon};
-
-export const strikethrough: EditorButtonDefinition = buildFormatButton('Strikethrough', 'strikethrough', strikethroughIcon);
-export const superscript: EditorButtonDefinition = buildFormatButton('Superscript', 'superscript', superscriptIcon);
-export const subscript: EditorButtonDefinition = buildFormatButton('Subscript', 'subscript', subscriptIcon);
-export const code: EditorButtonDefinition = buildFormatButton('Inline Code', 'code', codeIcon);
-export const clearFormating: EditorButtonDefinition = {
-    label: 'Clear formatting',
-    icon: formatClearIcon,
-    action(context: EditorUiContext) {
-        context.editor.update(() => {
-            const selection = $getSelection();
-            for (const node of selection?.getNodes() || []) {
-                if ($isTextNode(node)) {
-                    node.setFormat(0);
-                    node.setStyle('');
-                }
-            }
-        });
-    },
-    isActive() {
-        return false;
-    }
-};
-
-function setAlignmentForSection(alignment: ElementFormatType): void {
-    const selection = $getSelection();
-    const elements = $getBlockElementNodesInSelection(selection);
-    for (const node of elements) {
-        node.setFormat(alignment);
-    }
-}
-
-export const alignLeft: EditorButtonDefinition = {
-    label: 'Align left',
-    icon: alignLeftIcon,
-    action(context: EditorUiContext) {
-        context.editor.update(() => setAlignmentForSection('left'));
-    },
-    isActive(selection: BaseSelection|null) {
-        return $selectionContainsElementFormat(selection, 'left');
-    }
-};
-
-export const alignCenter: EditorButtonDefinition = {
-    label: 'Align center',
-    icon: alignCenterIcon,
-    action(context: EditorUiContext) {
-        context.editor.update(() => setAlignmentForSection('center'));
-    },
-    isActive(selection: BaseSelection|null) {
-        return $selectionContainsElementFormat(selection, 'center');
-    }
-};
-
-export const alignRight: EditorButtonDefinition = {
-    label: 'Align right',
-    icon: alignRightIcon,
-    action(context: EditorUiContext) {
-        context.editor.update(() => setAlignmentForSection('right'));
-    },
-    isActive(selection: BaseSelection|null) {
-        return $selectionContainsElementFormat(selection, 'right');
-    }
-};
-
-export const alignJustify: EditorButtonDefinition = {
-    label: 'Align justify',
-    icon: alignJustifyIcon,
-    action(context: EditorUiContext) {
-        context.editor.update(() => setAlignmentForSection('justify'));
-    },
-    isActive(selection: BaseSelection|null) {
-        return $selectionContainsElementFormat(selection, 'justify');
-    }
-};
-
-
-function buildListButton(label: string, type: ListType, icon: string): EditorButtonDefinition {
-    return {
-        label,
-        icon,
-        action(context: EditorUiContext) {
-            context.editor.getEditorState().read(() => {
-                const selection = $getSelection();
-                if (this.isActive(selection, context)) {
-                    removeList(context.editor);
-                } else {
-                    insertList(context.editor, type);
-                }
-            });
-        },
-        isActive(selection: BaseSelection|null): boolean {
-            return $selectionContainsNodeType(selection, (node: LexicalNode | null | undefined): boolean => {
-                return $isListNode(node) && (node as ListNode).getListType() === type;
-            });
-        }
-    };
-}
-
-export const bulletList: EditorButtonDefinition = buildListButton('Bullet list', 'bullet', listBulletIcon);
-export const numberList: EditorButtonDefinition = buildListButton('Numbered list', 'number', listNumberedIcon);
-export const taskList: EditorButtonDefinition = buildListButton('Task list', 'check', listCheckIcon);
-
-
-export const link: EditorButtonDefinition = {
-    label: 'Insert/edit link',
-    icon: linkIcon,
-    action(context: EditorUiContext) {
-        const linkModal = context.manager.createModal('link');
-        context.editor.getEditorState().read(() => {
-            const selection = $getSelection();
-            const selectedLink = $getNodeFromSelection(selection, $isLinkNode) as LinkNode|null;
-
-            let formDefaults = {};
-            if (selectedLink) {
-                formDefaults = {
-                    url: selectedLink.getURL(),
-                    text: selectedLink.getTextContent(),
-                    title: selectedLink.getTitle(),
-                    target: selectedLink.getTarget(),
-                }
-
-                context.editor.update(() => {
-                    const selection = $createNodeSelection();
-                    selection.add(selectedLink.getKey());
-                    $setSelection(selection);
-                });
-            }
-
-            linkModal.show(formDefaults);
-        });
-    },
-    isActive(selection: BaseSelection|null): boolean {
-        return $selectionContainsNodeType(selection, $isLinkNode);
-    }
-};
-
-export const unlink: EditorButtonDefinition = {
-    label: 'Remove link',
-    icon: unlinkIcon,
-    action(context: EditorUiContext) {
-        context.editor.update(() => {
-            const selection = context.lastSelection;
-            const selectedLink = $getNodeFromSelection(selection, $isLinkNode) as LinkNode|null;
-            const selectionPoints = selection?.getStartEndPoints();
-
-            if (selectedLink) {
-                const newNode = $createTextNode(selectedLink.getTextContent());
-                selectedLink.replace(newNode);
-                if (selectionPoints?.length === 2) {
-                    newNode.select(selectionPoints[0].offset, selectionPoints[1].offset);
-                } else {
-                    newNode.select();
-                }
-            }
-        });
-    },
-    isActive(selection: BaseSelection|null): boolean {
-        return false;
-    }
-};
-
-export const table: EditorBasicButtonDefinition = {
-    label: 'Table',
-    icon: tableIcon,
-};
-
-export const image: EditorButtonDefinition = {
-    label: 'Insert/Edit Image',
-    icon: imageIcon,
-    action(context: EditorUiContext) {
-        const imageModal = context.manager.createModal('image');
-        const selection = context.lastSelection;
-        const selectedImage = $getNodeFromSelection(selection, $isImageNode) as ImageNode|null;
-
-        context.editor.getEditorState().read(() => {
-            let formDefaults = {};
-            if (selectedImage) {
-                formDefaults = {
-                    src: selectedImage.getSrc(),
-                    alt: selectedImage.getAltText(),
-                    height: selectedImage.getHeight(),
-                    width: selectedImage.getWidth(),
-                }
-
-                context.editor.update(() => {
-                    const selection = $createNodeSelection();
-                    selection.add(selectedImage.getKey());
-                    $setSelection(selection);
-                });
-            }
-
-            imageModal.show(formDefaults);
-        });
-    },
-    isActive(selection: BaseSelection|null): boolean {
-        return $selectionContainsNodeType(selection, $isImageNode);
-    }
-};
-
-export const horizontalRule: EditorButtonDefinition = {
-    label: 'Insert horizontal line',
-    icon: horizontalRuleIcon,
-    action(context: EditorUiContext) {
-        context.editor.update(() => {
-            $insertNewBlockNodeAtSelection($createHorizontalRuleNode(), false);
-        });
-    },
-    isActive(selection: BaseSelection|null): boolean {
-        return $selectionContainsNodeType(selection, $isHorizontalRuleNode);
-    }
-};
-
-export const codeBlock: EditorButtonDefinition = {
-    label: 'Insert code block',
-    icon: codeBlockIcon,
-    action(context: EditorUiContext) {
-        context.editor.getEditorState().read(() => {
-            const selection = $getSelection();
-            const codeBlock = $getNodeFromSelection(context.lastSelection, $isCodeBlockNode) as (CodeBlockNode|null);
-            if (codeBlock === null) {
-                context.editor.update(() => {
-                    const codeBlock = $createCodeBlockNode();
-                    codeBlock.setCode(selection?.getTextContent() || '');
-                    $insertNewBlockNodeAtSelection(codeBlock, true);
-                    $openCodeEditorForNode(context.editor, codeBlock);
-                    codeBlock.selectStart();
-                });
-            } else {
-                $openCodeEditorForNode(context.editor, codeBlock);
-            }
-        });
-    },
-    isActive(selection: BaseSelection|null): boolean {
-        return $selectionContainsNodeType(selection, $isCodeBlockNode);
-    }
-};
-
-export const editCodeBlock: EditorButtonDefinition = Object.assign({}, codeBlock, {
-    label: 'Edit code block',
-    icon: editIcon,
-});
-
-export const diagram: EditorButtonDefinition = {
-    label: 'Insert/edit drawing',
-    icon: diagramIcon,
-    action(context: EditorUiContext) {
-        context.editor.getEditorState().read(() => {
-            const selection = $getSelection();
-            const diagramNode = $getNodeFromSelection(context.lastSelection, $isDiagramNode) as (DiagramNode|null);
-            if (diagramNode === null) {
-                context.editor.update(() => {
-                    const diagram = $createDiagramNode();
-                    $insertNewBlockNodeAtSelection(diagram, true);
-                    $openDrawingEditorForNode(context, diagram);
-                    diagram.selectStart();
-                });
-            } else {
-                $openDrawingEditorForNode(context, diagramNode);
-            }
-        });
-    },
-    isActive(selection: BaseSelection|null): boolean {
-        return $selectionContainsNodeType(selection, $isDiagramNode);
-    }
-};
-
-
-export const details: EditorButtonDefinition = {
-    label: 'Insert collapsible block',
-    icon: detailsIcon,
-    action(context: EditorUiContext) {
-        context.editor.update(() => {
-            const selection = $getSelection();
-            const detailsNode = $createDetailsNode();
-            const selectionNodes = selection?.getNodes() || [];
-            const topLevels = selectionNodes.map(n => n.getTopLevelElement())
-                .filter(n => n !== null) as ElementNode[];
-            const uniqueTopLevels = [...new Set(topLevels)];
-
-            if (uniqueTopLevels.length > 0) {
-                uniqueTopLevels[0].insertAfter(detailsNode);
-            } else {
-                $getRoot().append(detailsNode);
-            }
-
-            for (const node of uniqueTopLevels) {
-                detailsNode.append(node);
-            }
-        });
-    },
-    isActive(selection: BaseSelection|null): boolean {
-        return $selectionContainsNodeType(selection, $isDetailsNode);
-    }
-}
-
-export const source: EditorButtonDefinition = {
-    label: 'Source code',
-    icon: sourceIcon,
-    async action(context: EditorUiContext) {
-        const modal = context.manager.createModal('source');
-        const source = await getEditorContentAsHtml(context.editor);
-        modal.show({source});
-    },
-    isActive() {
-        return false;
-    }
-};
-
-export const fullscreen: EditorButtonDefinition = {
-    label: 'Fullscreen',
-    icon: fullscreenIcon,
-    async action(context: EditorUiContext, button: EditorButton) {
-        const isFullScreen = context.containerDOM.classList.contains('fullscreen');
-        context.containerDOM.classList.toggle('fullscreen', !isFullScreen);
-        (context.containerDOM.closest('body') as HTMLElement).classList.toggle('editor-is-fullscreen', !isFullScreen);
-        button.setActiveState(!isFullScreen);
-    },
-    isActive(selection, context: EditorUiContext) {
-        return context.containerDOM.classList.contains('fullscreen');
-    }
-};
\ No newline at end of file
diff --git a/resources/js/wysiwyg/ui/defaults/buttons/alignments.ts b/resources/js/wysiwyg/ui/defaults/buttons/alignments.ts
new file mode 100644
index 000000000..2b441e5da
--- /dev/null
+++ b/resources/js/wysiwyg/ui/defaults/buttons/alignments.ts
@@ -0,0 +1,61 @@
+import {$getSelection, BaseSelection, ElementFormatType} from "lexical";
+import {$getBlockElementNodesInSelection, $selectionContainsElementFormat} from "../../../helpers";
+import {EditorButtonDefinition} from "../../framework/buttons";
+import alignLeftIcon from "@icons/editor/align-left.svg";
+import {EditorUiContext} from "../../framework/core";
+import alignCenterIcon from "@icons/editor/align-center.svg";
+import alignRightIcon from "@icons/editor/align-right.svg";
+import alignJustifyIcon from "@icons/editor/align-justify.svg";
+
+
+function setAlignmentForSection(alignment: ElementFormatType): void {
+    const selection = $getSelection();
+    const elements = $getBlockElementNodesInSelection(selection);
+    for (const node of elements) {
+        node.setFormat(alignment);
+    }
+}
+
+export const alignLeft: EditorButtonDefinition = {
+    label: 'Align left',
+    icon: alignLeftIcon,
+    action(context: EditorUiContext) {
+        context.editor.update(() => setAlignmentForSection('left'));
+    },
+    isActive(selection: BaseSelection|null) {
+        return $selectionContainsElementFormat(selection, 'left');
+    }
+};
+
+export const alignCenter: EditorButtonDefinition = {
+    label: 'Align center',
+    icon: alignCenterIcon,
+    action(context: EditorUiContext) {
+        context.editor.update(() => setAlignmentForSection('center'));
+    },
+    isActive(selection: BaseSelection|null) {
+        return $selectionContainsElementFormat(selection, 'center');
+    }
+};
+
+export const alignRight: EditorButtonDefinition = {
+    label: 'Align right',
+    icon: alignRightIcon,
+    action(context: EditorUiContext) {
+        context.editor.update(() => setAlignmentForSection('right'));
+    },
+    isActive(selection: BaseSelection|null) {
+        return $selectionContainsElementFormat(selection, 'right');
+    }
+};
+
+export const alignJustify: EditorButtonDefinition = {
+    label: 'Align justify',
+    icon: alignJustifyIcon,
+    action(context: EditorUiContext) {
+        context.editor.update(() => setAlignmentForSection('justify'));
+    },
+    isActive(selection: BaseSelection|null) {
+        return $selectionContainsElementFormat(selection, 'justify');
+    }
+};
diff --git a/resources/js/wysiwyg/ui/defaults/buttons/block-formats.ts b/resources/js/wysiwyg/ui/defaults/buttons/block-formats.ts
new file mode 100644
index 000000000..0eb07ecf1
--- /dev/null
+++ b/resources/js/wysiwyg/ui/defaults/buttons/block-formats.ts
@@ -0,0 +1,85 @@
+import {$createCalloutNode, $isCalloutNodeOfCategory, CalloutCategory} from "../../../nodes/callout";
+import {EditorButtonDefinition} from "../../framework/buttons";
+import {EditorUiContext} from "../../framework/core";
+import {$selectionContainsNodeType, $toggleSelectionBlockNodeType} from "../../../helpers";
+import {$createParagraphNode, $isParagraphNode, BaseSelection, LexicalNode} from "lexical";
+import {
+    $createHeadingNode,
+    $createQuoteNode,
+    $isHeadingNode,
+    $isQuoteNode,
+    HeadingNode,
+    HeadingTagType
+} from "@lexical/rich-text";
+
+function buildCalloutButton(category: CalloutCategory, name: string): EditorButtonDefinition {
+    return {
+        label: `${name} Callout`,
+        action(context: EditorUiContext) {
+            context.editor.update(() => {
+                $toggleSelectionBlockNodeType(
+                    (node) => $isCalloutNodeOfCategory(node, category),
+                    () => $createCalloutNode(category),
+                )
+            });
+        },
+        isActive(selection: BaseSelection|null): boolean {
+            return $selectionContainsNodeType(selection, (node) => $isCalloutNodeOfCategory(node, category));
+        }
+    };
+}
+
+export const infoCallout: EditorButtonDefinition = buildCalloutButton('info', 'Info');
+export const dangerCallout: EditorButtonDefinition = buildCalloutButton('danger', 'Danger');
+export const warningCallout: EditorButtonDefinition = buildCalloutButton('warning', 'Warning');
+export const successCallout: EditorButtonDefinition = buildCalloutButton('success', 'Success');
+
+const isHeaderNodeOfTag = (node: LexicalNode | null | undefined, tag: HeadingTagType) => {
+    return $isHeadingNode(node) && (node as HeadingNode).getTag() === tag;
+};
+
+function buildHeaderButton(tag: HeadingTagType, name: string): EditorButtonDefinition {
+    return {
+        label: name,
+        action(context: EditorUiContext) {
+            context.editor.update(() => {
+                $toggleSelectionBlockNodeType(
+                    (node) => isHeaderNodeOfTag(node, tag),
+                    () => $createHeadingNode(tag),
+                )
+            });
+        },
+        isActive(selection: BaseSelection|null): boolean {
+            return $selectionContainsNodeType(selection, (node) => isHeaderNodeOfTag(node, tag));
+        }
+    };
+}
+
+export const h2: EditorButtonDefinition = buildHeaderButton('h2', 'Large Header');
+export const h3: EditorButtonDefinition = buildHeaderButton('h3', 'Medium Header');
+export const h4: EditorButtonDefinition = buildHeaderButton('h4', 'Small Header');
+export const h5: EditorButtonDefinition = buildHeaderButton('h5', 'Tiny Header');
+
+export const blockquote: EditorButtonDefinition = {
+    label: 'Blockquote',
+    action(context: EditorUiContext) {
+        context.editor.update(() => {
+            $toggleSelectionBlockNodeType($isQuoteNode, $createQuoteNode);
+        });
+    },
+    isActive(selection: BaseSelection|null): boolean {
+        return $selectionContainsNodeType(selection, $isQuoteNode);
+    }
+};
+
+export const paragraph: EditorButtonDefinition = {
+    label: 'Paragraph',
+    action(context: EditorUiContext) {
+        context.editor.update(() => {
+            $toggleSelectionBlockNodeType($isParagraphNode, $createParagraphNode);
+        });
+    },
+    isActive(selection: BaseSelection|null): boolean {
+        return $selectionContainsNodeType(selection, $isParagraphNode);
+    }
+}
\ No newline at end of file
diff --git a/resources/js/wysiwyg/ui/defaults/buttons/controls.ts b/resources/js/wysiwyg/ui/defaults/buttons/controls.ts
new file mode 100644
index 000000000..ad69d69d1
--- /dev/null
+++ b/resources/js/wysiwyg/ui/defaults/buttons/controls.ts
@@ -0,0 +1,81 @@
+import {EditorButton, EditorButtonDefinition} from "../../framework/buttons";
+import undoIcon from "@icons/editor/undo.svg";
+import {EditorUiContext} from "../../framework/core";
+import {
+    BaseSelection,
+    CAN_REDO_COMMAND,
+    CAN_UNDO_COMMAND,
+    COMMAND_PRIORITY_LOW,
+    REDO_COMMAND,
+    UNDO_COMMAND
+} from "lexical";
+import redoIcon from "@icons/editor/redo.svg";
+import sourceIcon from "@icons/editor/source-view.svg";
+import {getEditorContentAsHtml} from "../../../actions";
+import fullscreenIcon from "@icons/editor/fullscreen.svg";
+
+export const undo: EditorButtonDefinition = {
+    label: 'Undo',
+    icon: undoIcon,
+    action(context: EditorUiContext) {
+        context.editor.dispatchCommand(UNDO_COMMAND, undefined);
+    },
+    isActive(selection: BaseSelection|null): boolean {
+        return false;
+    },
+    setup(context: EditorUiContext, button: EditorButton) {
+        button.toggleDisabled(true);
+
+        context.editor.registerCommand(CAN_UNDO_COMMAND, (payload: boolean): boolean => {
+            button.toggleDisabled(!payload)
+            return false;
+        }, COMMAND_PRIORITY_LOW);
+    }
+}
+
+export const redo: EditorButtonDefinition = {
+    label: 'Redo',
+    icon: redoIcon,
+    action(context: EditorUiContext) {
+        context.editor.dispatchCommand(REDO_COMMAND, undefined);
+    },
+    isActive(selection: BaseSelection|null): boolean {
+        return false;
+    },
+    setup(context: EditorUiContext, button: EditorButton) {
+        button.toggleDisabled(true);
+
+        context.editor.registerCommand(CAN_REDO_COMMAND, (payload: boolean): boolean => {
+            button.toggleDisabled(!payload)
+            return false;
+        }, COMMAND_PRIORITY_LOW);
+    }
+}
+
+
+export const source: EditorButtonDefinition = {
+    label: 'Source code',
+    icon: sourceIcon,
+    async action(context: EditorUiContext) {
+        const modal = context.manager.createModal('source');
+        const source = await getEditorContentAsHtml(context.editor);
+        modal.show({source});
+    },
+    isActive() {
+        return false;
+    }
+};
+
+export const fullscreen: EditorButtonDefinition = {
+    label: 'Fullscreen',
+    icon: fullscreenIcon,
+    async action(context: EditorUiContext, button: EditorButton) {
+        const isFullScreen = context.containerDOM.classList.contains('fullscreen');
+        context.containerDOM.classList.toggle('fullscreen', !isFullScreen);
+        (context.containerDOM.closest('body') as HTMLElement).classList.toggle('editor-is-fullscreen', !isFullScreen);
+        button.setActiveState(!isFullScreen);
+    },
+    isActive(selection, context: EditorUiContext) {
+        return context.containerDOM.classList.contains('fullscreen');
+    }
+};
\ No newline at end of file
diff --git a/resources/js/wysiwyg/ui/defaults/buttons/inline-formats.ts b/resources/js/wysiwyg/ui/defaults/buttons/inline-formats.ts
new file mode 100644
index 000000000..d04f72a2e
--- /dev/null
+++ b/resources/js/wysiwyg/ui/defaults/buttons/inline-formats.ts
@@ -0,0 +1,56 @@
+import {$getSelection, $isTextNode, BaseSelection, FORMAT_TEXT_COMMAND, TextFormatType} from "lexical";
+import {EditorBasicButtonDefinition, EditorButtonDefinition} from "../../framework/buttons";
+import {EditorUiContext} from "../../framework/core";
+import {$selectionContainsTextFormat} from "../../../helpers";
+import boldIcon from "@icons/editor/bold.svg";
+import italicIcon from "@icons/editor/italic.svg";
+import underlinedIcon from "@icons/editor/underlined.svg";
+import textColorIcon from "@icons/editor/text-color.svg";
+import highlightIcon from "@icons/editor/highlighter.svg";
+import strikethroughIcon from "@icons/editor/strikethrough.svg";
+import superscriptIcon from "@icons/editor/superscript.svg";
+import subscriptIcon from "@icons/editor/subscript.svg";
+import codeIcon from "@icons/editor/code.svg";
+import formatClearIcon from "@icons/editor/format-clear.svg";
+
+function buildFormatButton(label: string, format: TextFormatType, icon: string): EditorButtonDefinition {
+    return {
+        label: label,
+        icon,
+        action(context: EditorUiContext) {
+            context.editor.dispatchCommand(FORMAT_TEXT_COMMAND, format);
+        },
+        isActive(selection: BaseSelection|null): boolean {
+            return $selectionContainsTextFormat(selection, format);
+        }
+    };
+}
+
+export const bold: EditorButtonDefinition = buildFormatButton('Bold', 'bold', boldIcon);
+export const italic: EditorButtonDefinition = buildFormatButton('Italic', 'italic', italicIcon);
+export const underline: EditorButtonDefinition = buildFormatButton('Underline', 'underline', underlinedIcon);
+export const textColor: EditorBasicButtonDefinition = {label: 'Text color', icon: textColorIcon};
+export const highlightColor: EditorBasicButtonDefinition = {label: 'Highlight color', icon: highlightIcon};
+
+export const strikethrough: EditorButtonDefinition = buildFormatButton('Strikethrough', 'strikethrough', strikethroughIcon);
+export const superscript: EditorButtonDefinition = buildFormatButton('Superscript', 'superscript', superscriptIcon);
+export const subscript: EditorButtonDefinition = buildFormatButton('Subscript', 'subscript', subscriptIcon);
+export const code: EditorButtonDefinition = buildFormatButton('Inline Code', 'code', codeIcon);
+export const clearFormating: EditorButtonDefinition = {
+    label: 'Clear formatting',
+    icon: formatClearIcon,
+    action(context: EditorUiContext) {
+        context.editor.update(() => {
+            const selection = $getSelection();
+            for (const node of selection?.getNodes() || []) {
+                if ($isTextNode(node)) {
+                    node.setFormat(0);
+                    node.setStyle('');
+                }
+            }
+        });
+    },
+    isActive() {
+        return false;
+    }
+};
\ No newline at end of file
diff --git a/resources/js/wysiwyg/ui/defaults/buttons/lists.ts b/resources/js/wysiwyg/ui/defaults/buttons/lists.ts
new file mode 100644
index 000000000..ecda290a1
--- /dev/null
+++ b/resources/js/wysiwyg/ui/defaults/buttons/lists.ts
@@ -0,0 +1,35 @@
+import {$isListNode, insertList, ListNode, ListType, removeList} from "@lexical/list";
+import {EditorButtonDefinition} from "../../framework/buttons";
+import {EditorUiContext} from "../../framework/core";
+import {$getSelection, BaseSelection, LexicalNode} from "lexical";
+import {$selectionContainsNodeType} from "../../../helpers";
+import listBulletIcon from "@icons/editor/list-bullet.svg";
+import listNumberedIcon from "@icons/editor/list-numbered.svg";
+import listCheckIcon from "@icons/editor/list-check.svg";
+
+
+function buildListButton(label: string, type: ListType, icon: string): EditorButtonDefinition {
+    return {
+        label,
+        icon,
+        action(context: EditorUiContext) {
+            context.editor.getEditorState().read(() => {
+                const selection = $getSelection();
+                if (this.isActive(selection, context)) {
+                    removeList(context.editor);
+                } else {
+                    insertList(context.editor, type);
+                }
+            });
+        },
+        isActive(selection: BaseSelection|null): boolean {
+            return $selectionContainsNodeType(selection, (node: LexicalNode | null | undefined): boolean => {
+                return $isListNode(node) && (node as ListNode).getListType() === type;
+            });
+        }
+    };
+}
+
+export const bulletList: EditorButtonDefinition = buildListButton('Bullet list', 'bullet', listBulletIcon);
+export const numberList: EditorButtonDefinition = buildListButton('Numbered list', 'number', listNumberedIcon);
+export const taskList: EditorButtonDefinition = buildListButton('Task list', 'check', listCheckIcon);
diff --git a/resources/js/wysiwyg/ui/defaults/buttons/objects.ts b/resources/js/wysiwyg/ui/defaults/buttons/objects.ts
new file mode 100644
index 000000000..88241e926
--- /dev/null
+++ b/resources/js/wysiwyg/ui/defaults/buttons/objects.ts
@@ -0,0 +1,215 @@
+import {EditorButtonDefinition} from "../../framework/buttons";
+import linkIcon from "@icons/editor/link.svg";
+import {EditorUiContext} from "../../framework/core";
+import {
+    $createNodeSelection,
+    $createTextNode,
+    $getRoot,
+    $getSelection,
+    $setSelection,
+    BaseSelection,
+    ElementNode
+} from "lexical";
+import {$getNodeFromSelection, $insertNewBlockNodeAtSelection, $selectionContainsNodeType} from "../../../helpers";
+import {$isLinkNode, LinkNode} from "@lexical/link";
+import unlinkIcon from "@icons/editor/unlink.svg";
+import imageIcon from "@icons/editor/image.svg";
+import {$isImageNode, ImageNode} from "../../../nodes/image";
+import horizontalRuleIcon from "@icons/editor/horizontal-rule.svg";
+import {$createHorizontalRuleNode, $isHorizontalRuleNode} from "../../../nodes/horizontal-rule";
+import codeBlockIcon from "@icons/editor/code-block.svg";
+import {$createCodeBlockNode, $isCodeBlockNode, $openCodeEditorForNode, CodeBlockNode} from "../../../nodes/code-block";
+import editIcon from "@icons/edit.svg";
+import diagramIcon from "@icons/editor/diagram.svg";
+import {$createDiagramNode, $isDiagramNode, $openDrawingEditorForNode, DiagramNode} from "../../../nodes/diagram";
+import detailsIcon from "@icons/editor/details.svg";
+import {$createDetailsNode, $isDetailsNode} from "../../../nodes/details";
+
+export const link: EditorButtonDefinition = {
+    label: 'Insert/edit link',
+    icon: linkIcon,
+    action(context: EditorUiContext) {
+        const linkModal = context.manager.createModal('link');
+        context.editor.getEditorState().read(() => {
+            const selection = $getSelection();
+            const selectedLink = $getNodeFromSelection(selection, $isLinkNode) as LinkNode|null;
+
+            let formDefaults = {};
+            if (selectedLink) {
+                formDefaults = {
+                    url: selectedLink.getURL(),
+                    text: selectedLink.getTextContent(),
+                    title: selectedLink.getTitle(),
+                    target: selectedLink.getTarget(),
+                }
+
+                context.editor.update(() => {
+                    const selection = $createNodeSelection();
+                    selection.add(selectedLink.getKey());
+                    $setSelection(selection);
+                });
+            }
+
+            linkModal.show(formDefaults);
+        });
+    },
+    isActive(selection: BaseSelection|null): boolean {
+        return $selectionContainsNodeType(selection, $isLinkNode);
+    }
+};
+
+export const unlink: EditorButtonDefinition = {
+    label: 'Remove link',
+    icon: unlinkIcon,
+    action(context: EditorUiContext) {
+        context.editor.update(() => {
+            const selection = context.lastSelection;
+            const selectedLink = $getNodeFromSelection(selection, $isLinkNode) as LinkNode|null;
+            const selectionPoints = selection?.getStartEndPoints();
+
+            if (selectedLink) {
+                const newNode = $createTextNode(selectedLink.getTextContent());
+                selectedLink.replace(newNode);
+                if (selectionPoints?.length === 2) {
+                    newNode.select(selectionPoints[0].offset, selectionPoints[1].offset);
+                } else {
+                    newNode.select();
+                }
+            }
+        });
+    },
+    isActive(selection: BaseSelection|null): boolean {
+        return false;
+    }
+};
+
+
+
+export const image: EditorButtonDefinition = {
+    label: 'Insert/Edit Image',
+    icon: imageIcon,
+    action(context: EditorUiContext) {
+        const imageModal = context.manager.createModal('image');
+        const selection = context.lastSelection;
+        const selectedImage = $getNodeFromSelection(selection, $isImageNode) as ImageNode|null;
+
+        context.editor.getEditorState().read(() => {
+            let formDefaults = {};
+            if (selectedImage) {
+                formDefaults = {
+                    src: selectedImage.getSrc(),
+                    alt: selectedImage.getAltText(),
+                    height: selectedImage.getHeight(),
+                    width: selectedImage.getWidth(),
+                }
+
+                context.editor.update(() => {
+                    const selection = $createNodeSelection();
+                    selection.add(selectedImage.getKey());
+                    $setSelection(selection);
+                });
+            }
+
+            imageModal.show(formDefaults);
+        });
+    },
+    isActive(selection: BaseSelection|null): boolean {
+        return $selectionContainsNodeType(selection, $isImageNode);
+    }
+};
+
+export const horizontalRule: EditorButtonDefinition = {
+    label: 'Insert horizontal line',
+    icon: horizontalRuleIcon,
+    action(context: EditorUiContext) {
+        context.editor.update(() => {
+            $insertNewBlockNodeAtSelection($createHorizontalRuleNode(), false);
+        });
+    },
+    isActive(selection: BaseSelection|null): boolean {
+        return $selectionContainsNodeType(selection, $isHorizontalRuleNode);
+    }
+};
+
+export const codeBlock: EditorButtonDefinition = {
+    label: 'Insert code block',
+    icon: codeBlockIcon,
+    action(context: EditorUiContext) {
+        context.editor.getEditorState().read(() => {
+            const selection = $getSelection();
+            const codeBlock = $getNodeFromSelection(context.lastSelection, $isCodeBlockNode) as (CodeBlockNode|null);
+            if (codeBlock === null) {
+                context.editor.update(() => {
+                    const codeBlock = $createCodeBlockNode();
+                    codeBlock.setCode(selection?.getTextContent() || '');
+                    $insertNewBlockNodeAtSelection(codeBlock, true);
+                    $openCodeEditorForNode(context.editor, codeBlock);
+                    codeBlock.selectStart();
+                });
+            } else {
+                $openCodeEditorForNode(context.editor, codeBlock);
+            }
+        });
+    },
+    isActive(selection: BaseSelection|null): boolean {
+        return $selectionContainsNodeType(selection, $isCodeBlockNode);
+    }
+};
+
+export const editCodeBlock: EditorButtonDefinition = Object.assign({}, codeBlock, {
+    label: 'Edit code block',
+    icon: editIcon,
+});
+
+export const diagram: EditorButtonDefinition = {
+    label: 'Insert/edit drawing',
+    icon: diagramIcon,
+    action(context: EditorUiContext) {
+        context.editor.getEditorState().read(() => {
+            const selection = $getSelection();
+            const diagramNode = $getNodeFromSelection(context.lastSelection, $isDiagramNode) as (DiagramNode|null);
+            if (diagramNode === null) {
+                context.editor.update(() => {
+                    const diagram = $createDiagramNode();
+                    $insertNewBlockNodeAtSelection(diagram, true);
+                    $openDrawingEditorForNode(context, diagram);
+                    diagram.selectStart();
+                });
+            } else {
+                $openDrawingEditorForNode(context, diagramNode);
+            }
+        });
+    },
+    isActive(selection: BaseSelection|null): boolean {
+        return $selectionContainsNodeType(selection, $isDiagramNode);
+    }
+};
+
+
+export const details: EditorButtonDefinition = {
+    label: 'Insert collapsible block',
+    icon: detailsIcon,
+    action(context: EditorUiContext) {
+        context.editor.update(() => {
+            const selection = $getSelection();
+            const detailsNode = $createDetailsNode();
+            const selectionNodes = selection?.getNodes() || [];
+            const topLevels = selectionNodes.map(n => n.getTopLevelElement())
+                .filter(n => n !== null) as ElementNode[];
+            const uniqueTopLevels = [...new Set(topLevels)];
+
+            if (uniqueTopLevels.length > 0) {
+                uniqueTopLevels[0].insertAfter(detailsNode);
+            } else {
+                $getRoot().append(detailsNode);
+            }
+
+            for (const node of uniqueTopLevels) {
+                detailsNode.append(node);
+            }
+        });
+    },
+    isActive(selection: BaseSelection|null): boolean {
+        return $selectionContainsNodeType(selection, $isDetailsNode);
+    }
+}
\ No newline at end of file
diff --git a/resources/js/wysiwyg/ui/defaults/buttons/tables.ts b/resources/js/wysiwyg/ui/defaults/buttons/tables.ts
new file mode 100644
index 000000000..32fa49f88
--- /dev/null
+++ b/resources/js/wysiwyg/ui/defaults/buttons/tables.ts
@@ -0,0 +1,122 @@
+import {EditorBasicButtonDefinition, EditorButtonDefinition} from "../../framework/buttons";
+import tableIcon from "@icons/editor/table.svg";
+import deleteIcon from "@icons/editor/table-delete.svg";
+import deleteColumnIcon from "@icons/editor/table-delete-column.svg";
+import deleteRowIcon from "@icons/editor/table-delete-row.svg";
+import insertColumnAfterIcon from "@icons/editor/table-insert-column-after.svg";
+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,
+    $deleteTableRow__EXPERIMENTAL,
+    $getTableRowIndexFromTableCellNode, $insertTableColumn, $insertTableColumn__EXPERIMENTAL,
+    $insertTableRow, $insertTableRow__EXPERIMENTAL,
+    $isTableCellNode,
+    $isTableRowNode,
+    TableCellNode
+} from "@lexical/table";
+
+
+export const table: EditorBasicButtonDefinition = {
+    label: 'Table',
+    icon: tableIcon,
+};
+
+export const deleteTable: EditorButtonDefinition = {
+    label: 'Delete table',
+    icon: deleteIcon,
+    action(context: EditorUiContext) {
+        context.editor.update(() => {
+            const table = $getNodeFromSelection($getSelection(), $isCustomTableNode);
+            if (table) {
+                table.remove();
+            }
+        });
+    },
+    isActive() {
+        return false;
+    }
+};
+
+export const insertRowAbove: EditorButtonDefinition = {
+    label: 'Insert row above',
+    icon: insertRowAboveIcon,
+    action(context: EditorUiContext) {
+        context.editor.update(() => {
+            $insertTableRow__EXPERIMENTAL(false);
+        });
+    },
+    isActive() {
+        return false;
+    }
+};
+
+export const insertRowBelow: EditorButtonDefinition = {
+    label: 'Insert row below',
+    icon: insertRowBelowIcon,
+    action(context: EditorUiContext) {
+        context.editor.update(() => {
+            $insertTableRow__EXPERIMENTAL(true);
+        });
+    },
+    isActive() {
+        return false;
+    }
+};
+
+export const deleteRow: EditorButtonDefinition = {
+    label: 'Delete row',
+    icon: deleteRowIcon,
+    action(context: EditorUiContext) {
+        context.editor.update(() => {
+            $deleteTableRow__EXPERIMENTAL();
+        });
+    },
+    isActive() {
+        return false;
+    }
+};
+
+export const insertColumnBefore: EditorButtonDefinition = {
+    label: 'Insert column before',
+    icon: insertColumnBeforeIcon,
+    action(context: EditorUiContext) {
+        context.editor.update(() => {
+            $insertTableColumn__EXPERIMENTAL(false);
+        });
+    },
+    isActive() {
+        return false;
+    }
+};
+
+export const insertColumnAfter: EditorButtonDefinition = {
+    label: 'Insert column after',
+    icon: insertColumnAfterIcon,
+    action(context: EditorUiContext) {
+        context.editor.update(() => {
+            $insertTableColumn__EXPERIMENTAL(true);
+        });
+    },
+    isActive() {
+        return false;
+    }
+};
+
+export const deleteColumn: EditorButtonDefinition = {
+    label: 'Delete column',
+    icon: deleteColumnIcon,
+    action(context: EditorUiContext) {
+        context.editor.update(() => {
+            $deleteTableColumn__EXPERIMENTAL();
+        });
+    },
+    isActive() {
+        return false;
+    }
+};
\ No newline at end of file
diff --git a/resources/js/wysiwyg/ui/index.ts b/resources/js/wysiwyg/ui/index.ts
index f728ae48f..5dee6b62b 100644
--- a/resources/js/wysiwyg/ui/index.ts
+++ b/resources/js/wysiwyg/ui/index.ts
@@ -3,7 +3,7 @@ import {
     getCodeToolbarContent,
     getImageToolbarContent,
     getLinkToolbarContent,
-    getMainEditorFullToolbar
+    getMainEditorFullToolbar, getTableToolbarContent
 } from "./toolbars";
 import {EditorUIManager} from "./framework/manager";
 import {image as imageFormDefinition, link as linkFormDefinition, source as sourceFormDefinition} from "./defaults/form-definitions";
@@ -61,6 +61,14 @@ export function buildEditorUI(container: HTMLElement, element: HTMLElement, scro
         content: getCodeToolbarContent(),
     });
 
+    manager.registerContextToolbar('table', {
+        selector: 'td,th',
+        content: getTableToolbarContent(),
+        displayTargetLocator(originalTarget: HTMLElement): HTMLElement {
+            return originalTarget.closest('table') as HTMLTableElement;
+        }
+    });
+
     // Register image decorator listener
     manager.registerDecoratorType('image', ImageDecorator);
     manager.registerDecoratorType('code', CodeBlockDecorator);
diff --git a/resources/js/wysiwyg/ui/toolbars.ts b/resources/js/wysiwyg/ui/toolbars.ts
index f5eae6b21..5d40578a5 100644
--- a/resources/js/wysiwyg/ui/toolbars.ts
+++ b/resources/js/wysiwyg/ui/toolbars.ts
@@ -1,17 +1,4 @@
 import {EditorButton} from "./framework/buttons";
-import {
-    alignCenter, alignJustify,
-    alignLeft,
-    alignRight,
-    blockquote, bold, bulletList, clearFormating, code, codeBlock,
-    dangerCallout, details, diagram, editCodeBlock, fullscreen,
-    h2, h3, h4, h5, highlightColor, horizontalRule, image,
-    infoCallout, italic, link, numberList, paragraph,
-    redo, source, strikethrough, subscript,
-    successCallout, superscript, table, taskList, textColor, underline,
-    undo, unlink,
-    warningCallout
-} from "./defaults/button-definitions";
 import {EditorContainerUiElement, EditorSimpleClassContainer, EditorUiElement} from "./framework/core";
 import {el} from "../helpers";
 import {EditorFormatMenu} from "./framework/blocks/format-menu";
@@ -21,6 +8,48 @@ import {EditorColorPicker} from "./framework/blocks/color-picker";
 import {EditorTableCreator} from "./framework/blocks/table-creator";
 import {EditorColorButton} from "./framework/blocks/color-button";
 import {EditorOverflowContainer} from "./framework/blocks/overflow-container";
+import {
+    deleteColumn,
+    deleteRow,
+    deleteTable, insertColumnAfter,
+    insertColumnBefore,
+    insertRowAbove,
+    insertRowBelow,
+    table
+} from "./defaults/buttons/tables";
+import {fullscreen, redo, source, undo} from "./defaults/buttons/controls";
+import {
+    blockquote, dangerCallout,
+    h2,
+    h3,
+    h4,
+    h5,
+    infoCallout,
+    paragraph,
+    successCallout,
+    warningCallout
+} from "./defaults/buttons/block-formats";
+import {
+    bold, clearFormating, code,
+    highlightColor,
+    italic,
+    strikethrough, subscript,
+    superscript,
+    textColor,
+    underline
+} from "./defaults/buttons/inline-formats";
+import {alignCenter, alignJustify, alignLeft, alignRight} from "./defaults/buttons/alignments";
+import {bulletList, numberList, taskList} from "./defaults/buttons/lists";
+import {
+    codeBlock,
+    details,
+    diagram,
+    editCodeBlock,
+    horizontalRule,
+    image,
+    link,
+    unlink
+} from "./defaults/buttons/objects";
 
 export function getMainEditorFullToolbar(): EditorContainerUiElement {
     return new EditorSimpleClassContainer('editor-toolbar-main', [
@@ -129,4 +158,23 @@ export function getCodeToolbarContent(): EditorUiElement[] {
     return [
         new EditorButton(editCodeBlock),
     ];
+}
+
+export function getTableToolbarContent(): EditorUiElement[] {
+    return [
+        new EditorOverflowContainer(2, [
+            // Todo - Table properties
+            new EditorButton(deleteTable),
+        ]),
+        new EditorOverflowContainer(3, [
+            new EditorButton(insertRowAbove),
+            new EditorButton(insertRowBelow),
+            new EditorButton(deleteRow),
+        ]),
+        new EditorOverflowContainer(3, [
+            new EditorButton(insertColumnBefore),
+            new EditorButton(insertColumnAfter),
+            new EditorButton(deleteColumn),
+        ]),
+    ];
 }
\ No newline at end of file