]> BookStack Code Mirror - bookstack/commitdiff
Merge pull request #5731 from BookStackApp/lexical_jul25
authorDan Brown <redacted>
Sat, 26 Jul 2025 09:08:44 +0000 (10:08 +0100)
committerGitHub <redacted>
Sat, 26 Jul 2025 09:08:44 +0000 (10:08 +0100)
New WYSIWYG editor changes for July 2025

18 files changed:
lang/en/editor.php
resources/js/wysiwyg/index.ts
resources/js/wysiwyg/lexical/core/__tests__/utils/index.ts
resources/js/wysiwyg/lexical/html/__tests__/unit/LexicalHtml.test.ts
resources/js/wysiwyg/lexical/html/index.ts
resources/js/wysiwyg/lexical/link/__tests__/unit/LexicalAutoLinkNode.test.ts
resources/js/wysiwyg/lexical/link/__tests__/unit/LexicalLinkNode.test.ts
resources/js/wysiwyg/lexical/link/index.ts
resources/js/wysiwyg/lexical/table/LexicalTableCellNode.ts
resources/js/wysiwyg/lexical/utils/__tests__/unit/LexicalUtilsSplitNode.test.ts
resources/js/wysiwyg/lexical/utils/__tests__/unit/LexlcaiUtilsInsertNodeToNearestRoot.test.ts
resources/js/wysiwyg/services/__tests__/mouse-handling.test.ts [new file with mode: 0644]
resources/js/wysiwyg/services/mouse-handling.ts [new file with mode: 0644]
resources/js/wysiwyg/services/shortcuts.ts
resources/js/wysiwyg/ui/defaults/buttons/inline-formats.ts
resources/js/wysiwyg/ui/defaults/forms/tables.ts
resources/js/wysiwyg/utils/tables.ts
resources/sass/_editor.scss

index 752c6f3f702b61603c4f76536eaeff659d11681b..0d250e9a7bd6e4382fa6b1e55199b9eacff5d5a6 100644 (file)
@@ -48,6 +48,7 @@ return [
     'superscript' => 'Superscript',
     'subscript' => 'Subscript',
     'text_color' => 'Text color',
+    'highlight_color' => 'Highlight color',
     'custom_color' => 'Custom color',
     'remove_color' => 'Remove color',
     'background_color' => 'Background color',
index f572f9de5ec9da58fc890b93b8efced42de8ab7d..e01b4e8f499feac530cd8f2d882edf66feb07838 100644 (file)
@@ -19,6 +19,7 @@ import {contextToolbars, getBasicEditorToolbar, getMainEditorFullToolbar} from "
 import {modals} from "./ui/defaults/modals";
 import {CodeBlockDecorator} from "./ui/decorators/code-block";
 import {DiagramDecorator} from "./ui/decorators/diagram";
+import {registerMouseHandling} from "./services/mouse-handling";
 
 const theme = {
     text: {
@@ -51,6 +52,7 @@ export function createPageEditorInstance(container: HTMLElement, htmlContent: st
         registerHistory(editor, createEmptyHistoryState(), 300),
         registerShortcuts(context),
         registerKeyboardHandling(context),
+        registerMouseHandling(context),
         registerTableResizer(editor, context.scrollDOM),
         registerTableSelectionHandler(editor),
         registerTaskListHandler(editor, context.editorDOM),
index e18ef97560b797d154ae10c4882c1996d2253045..fd87877eefbdb6b4bb5fef661eabb82e0c26890e 100644 (file)
@@ -848,4 +848,20 @@ export function dispatchKeydownEventForSelectedNode(editor: LexicalEditor, key:
       dispatchKeydownEventForNode(node, editor, key);
     }
   });
+}
+
+export function dispatchEditorMouseClick(editor: LexicalEditor, clientX: number, clientY: number) {
+  const dom = editor.getRootElement();
+  if (!dom) {
+    return;
+  }
+
+  const event = new MouseEvent('click', {
+    clientX: clientX,
+    clientY: clientY,
+    bubbles: true,
+    cancelable: true,
+  });
+  dom?.dispatchEvent(event);
+  editor.commitUpdates();
 }
\ No newline at end of file
index e5064121ab5432b0f8b29c0bc3bfad1dbe5831a9..b466ee34a2c0687da3e278d4b35ce64042215adb 100644 (file)
@@ -146,7 +146,7 @@ describe('HTML', () => {
     });
 
     expect(html).toBe(
-      '<p>Hello</p><p>World</p>',
+      '<p>Hello</p>\n<p>World</p>',
     );
   });
 
index 5018e10b4f2c3c4ef75a4af7f8efcfd0abbd1378..de5e53bb8c489f8abd34e6dbea6e3c80314f05ce 100644 (file)
@@ -85,7 +85,18 @@ export function $generateHtmlFromNodes(
     $appendNodesToHTML(editor, topLevelNode, container, selection);
   }
 
-  return container.innerHTML;
+  const nodeCode = [];
+  for (const node of container.childNodes) {
+    if ("outerHTML" in node) {
+      nodeCode.push(node.outerHTML)
+    } else {
+      const wrap = document.createElement('div');
+      wrap.appendChild(node.cloneNode(true));
+      nodeCode.push(wrap.innerHTML);
+    }
+  }
+
+  return nodeCode.join('\n');
 }
 
 function $appendNodesToHTML(
index 0f35136821be7d88c57dfb891b8934c1c286e5f9..1103f73d3dc726f4d45030bd4ed3c402ae0639c4 100644 (file)
@@ -273,18 +273,6 @@ describe('LexicalAutoAutoLinkNode tests', () => {
       });
     });
 
-    test('AutoLinkNode.createDOM() sanitizes javascript: URLs', async () => {
-      const {editor} = testEnv;
-
-      await editor.update(() => {
-        // eslint-disable-next-line no-script-url
-        const autoLinkNode = new AutoLinkNode('javascript:alert(0)');
-        expect(autoLinkNode.createDOM(editorConfig).outerHTML).toBe(
-          '<a href="about:blank" class="my-autolink-class"></a>',
-        );
-      });
-    });
-
     test('AutoLinkNode.updateDOM()', async () => {
       const {editor} = testEnv;
 
index 1aff9186306a25d008000bb19f4744e1728c49ea..c50450302f8dfd11ff8272b0477813ef6206d114 100644 (file)
@@ -218,18 +218,6 @@ describe('LexicalLinkNode tests', () => {
       });
     });
 
-    test('LinkNode.createDOM() sanitizes javascript: URLs', async () => {
-      const {editor} = testEnv;
-
-      await editor.update(() => {
-        // eslint-disable-next-line no-script-url
-        const linkNode = new LinkNode('javascript:alert(0)');
-        expect(linkNode.createDOM(editorConfig).outerHTML).toBe(
-          '<a href="about:blank" class="my-link-class"></a>',
-        );
-      });
-    });
-
     test('LinkNode.updateDOM()', async () => {
       const {editor} = testEnv;
 
index 884fe9153a09aa48c7204391aee59a42e6098c00..336bb15467a878bcffda790eadb4af18c7c9df87 100644 (file)
@@ -48,14 +48,6 @@ export type SerializedLinkNode = Spread<
 
 type LinkHTMLElementType = HTMLAnchorElement | HTMLSpanElement;
 
-const SUPPORTED_URL_PROTOCOLS = new Set([
-  'http:',
-  'https:',
-  'mailto:',
-  'sms:',
-  'tel:',
-]);
-
 /** @noInheritDoc */
 export class LinkNode extends ElementNode {
   /** @internal */
@@ -90,7 +82,7 @@ export class LinkNode extends ElementNode {
 
   createDOM(config: EditorConfig): LinkHTMLElementType {
     const element = document.createElement('a');
-    element.href = this.sanitizeUrl(this.__url);
+    element.href = this.__url;
     if (this.__target !== null) {
       element.target = this.__target;
     }
@@ -166,19 +158,6 @@ export class LinkNode extends ElementNode {
     return node;
   }
 
-  sanitizeUrl(url: string): string {
-    try {
-      const parsedUrl = new URL(url);
-      // eslint-disable-next-line no-script-url
-      if (!SUPPORTED_URL_PROTOCOLS.has(parsedUrl.protocol)) {
-        return 'about:blank';
-      }
-    } catch {
-      return url;
-    }
-    return url;
-  }
-
   exportJSON(): SerializedLinkNode | SerializedAutoLinkNode {
     return {
       ...super.exportJSON(),
index 1fc6b42bbeba271f22c6ca1d1e40e67d9b6c601c..1c9d7ecf69221f853d49d49cc8e0ba4f30b5219b 100644 (file)
@@ -353,10 +353,17 @@ export function $convertTableCellNodeElement(
   const hasUnderlineTextDecoration = textDecoration.includes('underline');
 
   if (domNode instanceof HTMLElement) {
-    tableCellNode.setStyles(extractStyleMapFromElement(domNode));
+    const styleMap = extractStyleMapFromElement(domNode);
+    styleMap.delete('background-color');
+    tableCellNode.setStyles(styleMap);
     tableCellNode.setAlignment(extractAlignmentFromElement(domNode));
   }
 
+  const background = style.backgroundColor || null;
+  if (background) {
+    tableCellNode.setBackgroundColor(background);
+  }
+
   return {
     after: (childLexicalNodes) => {
       if (childLexicalNodes.length === 0) {
index 54cd8b54f1ef1b9095c2693b8fd660df85f679f9..6a415d00810db3c9804c62d05987024062c3cfb0 100644 (file)
@@ -38,7 +38,7 @@ describe('LexicalUtils#splitNode', () => {
     {
       _: 'split paragraph in between two text nodes',
       expectedHtml:
-        '<p>Hello</p><p>world</p>',
+        '<p>Hello</p>\n<p>world</p>',
       initialHtml: '<p><span>Hello</span><span>world</span></p>',
       splitOffset: 1,
       splitPath: [0],
@@ -46,7 +46,7 @@ describe('LexicalUtils#splitNode', () => {
     {
       _: 'split paragraph before the first text node',
       expectedHtml:
-        '<p><br></p><p>Helloworld</p>',
+        '<p><br></p>\n<p>Helloworld</p>',
       initialHtml: '<p><span>Hello</span><span>world</span></p>',
       splitOffset: 0,
       splitPath: [0],
@@ -54,7 +54,7 @@ describe('LexicalUtils#splitNode', () => {
     {
       _: 'split paragraph after the last text node',
       expectedHtml:
-        '<p>Helloworld</p><p><br></p>',
+        '<p>Helloworld</p>\n<p><br></p>',
       initialHtml: '<p><span>Hello</span><span>world</span></p>',
       splitOffset: 2, // Any offset that is higher than children size
       splitPath: [0],
@@ -62,7 +62,7 @@ describe('LexicalUtils#splitNode', () => {
     {
       _: 'split list items between two text nodes',
       expectedHtml:
-        '<ul><li>Hello</li></ul>' +
+        '<ul><li>Hello</li></ul>\n' +
         '<ul><li>world</li></ul>',
       initialHtml: '<ul><li><span>Hello</span><span>world</span></li></ul>',
       splitOffset: 1, // Any offset that is higher than children size
@@ -71,7 +71,7 @@ describe('LexicalUtils#splitNode', () => {
     {
       _: 'split list items before the first text node',
       expectedHtml:
-        '<ul><li></li></ul>' +
+        '<ul><li></li></ul>\n' +
         '<ul><li>Helloworld</li></ul>',
       initialHtml: '<ul><li><span>Hello</span><span>world</span></li></ul>',
       splitOffset: 0, // Any offset that is higher than children size
@@ -83,7 +83,7 @@ describe('LexicalUtils#splitNode', () => {
         '<ul>' +
         '<li>Before</li>' +
         '<li style="list-style: none;"><ul><li>Hello</li></ul></li>' +
-        '</ul>' +
+        '</ul>\n' +
         '<ul>' +
         '<li style="list-style: none;"><ul><li>world</li></ul></li>' +
         '<li>After</li>' +
index 8c31496de5acd5f2d314c36cf8ead05a70be9ba1..f13aed4086e02f8b95601e450c3267b733976b06 100644 (file)
@@ -46,7 +46,7 @@ describe('LexicalUtils#insertNodeToNearestRoot', () => {
     {
       _: 'insert into paragraph in between two text nodes',
       expectedHtml:
-        '<p>Hello</p><test-decorator></test-decorator><p>world</p>',
+        '<p>Hello</p>\n<test-decorator></test-decorator>\n<p>world</p>',
       initialHtml: '<p><span>Helloworld</span></p>',
       selectionOffset: 5, // Selection on text node after "Hello" world
       selectionPath: [0, 0],
@@ -57,8 +57,8 @@ describe('LexicalUtils#insertNodeToNearestRoot', () => {
         '<ul>' +
         '<li>Before</li>' +
         '<li style="list-style: none;"><ul><li>Hello</li></ul></li>' +
-        '</ul>' +
-        '<test-decorator></test-decorator>' +
+        '</ul>\n' +
+        '<test-decorator></test-decorator>\n' +
         '<ul>' +
         '<li style="list-style: none;"><ul><li>world</li></ul></li>' +
         '<li>After</li>' +
@@ -74,7 +74,7 @@ describe('LexicalUtils#insertNodeToNearestRoot', () => {
     },
     {
       _: 'insert into empty paragraph',
-      expectedHtml: '<p><br></p><test-decorator></test-decorator><p><br></p>',
+      expectedHtml: '<p><br></p>\n<test-decorator></test-decorator>\n<p><br></p>',
       initialHtml: '<p></p>',
       selectionOffset: 0, // Selection on text node after "Hello" world
       selectionPath: [0],
@@ -82,8 +82,8 @@ describe('LexicalUtils#insertNodeToNearestRoot', () => {
     {
       _: 'insert in the end of paragraph',
       expectedHtml:
-        '<p>Hello world</p>' +
-        '<test-decorator></test-decorator>' +
+        '<p>Hello world</p>\n' +
+        '<test-decorator></test-decorator>\n' +
         '<p><br></p>',
       initialHtml: '<p>Hello world</p>',
       selectionOffset: 12, // Selection on text node after "Hello" world
@@ -92,8 +92,8 @@ describe('LexicalUtils#insertNodeToNearestRoot', () => {
     {
       _: 'insert in the beginning of paragraph',
       expectedHtml:
-        '<p><br></p>' +
-        '<test-decorator></test-decorator>' +
+        '<p><br></p>\n' +
+        '<test-decorator></test-decorator>\n' +
         '<p>Hello world</p>',
       initialHtml: '<p>Hello world</p>',
       selectionOffset: 0, // Selection on text node after "Hello" world
@@ -102,9 +102,9 @@ describe('LexicalUtils#insertNodeToNearestRoot', () => {
     {
       _: 'insert with selection on root start',
       expectedHtml:
-        '<test-decorator></test-decorator>' +
-        '<test-decorator></test-decorator>' +
-        '<p>Before</p>' +
+        '<test-decorator></test-decorator>\n' +
+        '<test-decorator></test-decorator>\n' +
+        '<p>Before</p>\n' +
         '<p>After</p>',
       initialHtml:
         '<test-decorator></test-decorator>' +
@@ -116,8 +116,8 @@ describe('LexicalUtils#insertNodeToNearestRoot', () => {
     {
       _: 'insert with selection on root child',
       expectedHtml:
-        '<p>Before</p>' +
-        '<test-decorator></test-decorator>' +
+        '<p>Before</p>\n' +
+        '<test-decorator></test-decorator>\n' +
         '<p>After</p>',
       initialHtml: '<p>Before</p><p>After</p>',
       selectionOffset: 1,
@@ -126,7 +126,7 @@ describe('LexicalUtils#insertNodeToNearestRoot', () => {
     {
       _: 'insert with selection on root end',
       expectedHtml:
-        '<p>Before</p>' +
+        '<p>Before</p>\n' +
         '<test-decorator></test-decorator>',
       initialHtml: '<p>Before</p>',
       selectionOffset: 1,
diff --git a/resources/js/wysiwyg/services/__tests__/mouse-handling.test.ts b/resources/js/wysiwyg/services/__tests__/mouse-handling.test.ts
new file mode 100644 (file)
index 0000000..a3da352
--- /dev/null
@@ -0,0 +1,51 @@
+import {
+    createTestContext, destroyFromContext, dispatchEditorMouseClick,
+} from "lexical/__tests__/utils";
+import {
+    $getRoot, LexicalEditor, LexicalNode,
+    ParagraphNode,
+} from "lexical";
+import {registerRichText} from "@lexical/rich-text";
+import {EditorUiContext} from "../../ui/framework/core";
+import {registerMouseHandling} from "../mouse-handling";
+import {$createTableNode, TableNode} from "@lexical/table";
+
+describe('Mouse-handling service tests', () => {
+
+    let context!: EditorUiContext;
+    let editor!: LexicalEditor;
+
+    beforeEach(() => {
+        context = createTestContext();
+        editor = context.editor;
+        registerRichText(editor);
+        registerMouseHandling(context);
+    });
+
+    afterEach(() => {
+        destroyFromContext(context);
+    });
+
+    test('Click below last table inserts new empty paragraph', () => {
+        let tableNode!: TableNode;
+        let lastRootChild!: LexicalNode|null;
+
+        editor.updateAndCommit(() => {
+            tableNode = $createTableNode();
+            $getRoot().append(tableNode);
+            lastRootChild = $getRoot().getLastChild();
+        });
+
+        expect(lastRootChild).toBeInstanceOf(TableNode);
+
+        const tableDOM = editor.getElementByKey(tableNode.getKey());
+        const rect = tableDOM?.getBoundingClientRect();
+        dispatchEditorMouseClick(editor, 0, (rect?.bottom || 0) + 1)
+
+        editor.getEditorState().read(() => {
+            lastRootChild = $getRoot().getLastChild();
+        });
+
+        expect(lastRootChild).toBeInstanceOf(ParagraphNode);
+    });
+});
\ No newline at end of file
diff --git a/resources/js/wysiwyg/services/mouse-handling.ts b/resources/js/wysiwyg/services/mouse-handling.ts
new file mode 100644 (file)
index 0000000..058efc8
--- /dev/null
@@ -0,0 +1,63 @@
+import {EditorUiContext} from "../ui/framework/core";
+import {
+    $createParagraphNode, $getRoot,
+    $getSelection,
+    $isDecoratorNode, CLICK_COMMAND,
+    COMMAND_PRIORITY_LOW, KEY_ARROW_DOWN_COMMAND, KEY_ARROW_UP_COMMAND,
+    KEY_BACKSPACE_COMMAND,
+    KEY_DELETE_COMMAND,
+    KEY_ENTER_COMMAND, KEY_TAB_COMMAND,
+    LexicalEditor,
+    LexicalNode
+} from "lexical";
+import {$isImageNode} from "@lexical/rich-text/LexicalImageNode";
+import {$isMediaNode} from "@lexical/rich-text/LexicalMediaNode";
+import {getLastSelection} from "../utils/selection";
+import {$getNearestNodeBlockParent, $getParentOfType, $selectOrCreateAdjacent} from "../utils/nodes";
+import {$setInsetForSelection} from "../utils/lists";
+import {$isListItemNode} from "@lexical/list";
+import {$isDetailsNode, DetailsNode} from "@lexical/rich-text/LexicalDetailsNode";
+import {$isDiagramNode} from "../utils/diagrams";
+import {$isTableNode} from "@lexical/table";
+
+function isHardToEscapeNode(node: LexicalNode): boolean {
+    return $isDecoratorNode(node) || $isImageNode(node) || $isMediaNode(node) || $isDiagramNode(node) || $isTableNode(node);
+}
+
+function insertBelowLastNode(context: EditorUiContext, event: MouseEvent): boolean {
+    const lastNode = $getRoot().getLastChild();
+    if (!lastNode || !isHardToEscapeNode(lastNode)) {
+        return false;
+    }
+
+    const lastNodeDom = context.editor.getElementByKey(lastNode.getKey());
+    if (!lastNodeDom) {
+        return false;
+    }
+
+    const nodeBounds = lastNodeDom.getBoundingClientRect();
+    const isClickBelow = event.clientY > nodeBounds.bottom;
+    if (isClickBelow) {
+        context.editor.update(() => {
+            const newNode = $createParagraphNode();
+            $getRoot().append(newNode);
+            newNode.select();
+        });
+        return true;
+    }
+
+    return false;
+}
+
+
+export function registerMouseHandling(context: EditorUiContext): () => void {
+    const unregisterClick = context.editor.registerCommand(CLICK_COMMAND, (event): boolean => {
+        insertBelowLastNode(context, event);
+        return false;
+    }, COMMAND_PRIORITY_LOW);
+
+
+    return () => {
+        unregisterClick();
+    };
+}
\ No newline at end of file
index 0384a3bf13356f9cab2bb5e3a3e4a8d6579e35f5..ead4c38d432e7ff3f705a693cef0dcc6575aa9e3 100644 (file)
@@ -13,14 +13,16 @@ import {$showLinkForm} from "../ui/defaults/forms/objects";
 import {showLinkSelector} from "../utils/links";
 import {HeadingTagType} from "@lexical/rich-text/LexicalHeadingNode";
 
-function headerHandler(editor: LexicalEditor, tag: HeadingTagType): boolean {
-    toggleSelectionAsHeading(editor, tag);
+function headerHandler(context: EditorUiContext, tag: HeadingTagType): boolean {
+    toggleSelectionAsHeading(context.editor, tag);
+    context.manager.triggerFutureStateRefresh();
     return true;
 }
 
 function wrapFormatAction(formatAction: (editor: LexicalEditor) => any): ShortcutAction {
-    return (editor: LexicalEditor) => {
+    return (editor: LexicalEditor, context: EditorUiContext) => {
         formatAction(editor);
+        context.manager.triggerFutureStateRefresh();
         return true;
     };
 }
@@ -45,10 +47,10 @@ const actionsByKeys: Record<string, ShortcutAction> = {
         window.$events.emit('editor-save-page');
         return true;
     },
-    'meta+1': (editor) => headerHandler(editor, 'h1'),
-    'meta+2': (editor) => headerHandler(editor, 'h2'),
-    'meta+3': (editor) => headerHandler(editor, 'h3'),
-    'meta+4': (editor) => headerHandler(editor, 'h4'),
+    'meta+1': (editor, context) => headerHandler(context, 'h2'),
+    'meta+2': (editor, context) => headerHandler(context, 'h3'),
+    'meta+3': (editor, context) => headerHandler(context, 'h4'),
+    'meta+4': (editor, context) => headerHandler(context, 'h5'),
     'meta+5': wrapFormatAction(toggleSelectionAsParagraph),
     'meta+d': wrapFormatAction(toggleSelectionAsParagraph),
     'meta+6': wrapFormatAction(toggleSelectionAsBlockquote),
index c5b7ad29ad9c6be63548854d506f1e8944ef231f..dea78d24f375bc09fab28844eab55ebaa43c0198 100644 (file)
@@ -13,7 +13,6 @@ import codeIcon from "@icons/editor/code.svg";
 import formatClearIcon from "@icons/editor/format-clear.svg";
 import {$selectionContainsTextFormat} from "../../../utils/selection";
 import {$patchStyleText} from "@lexical/selection";
-import {context} from "esbuild";
 
 function buildFormatButton(label: string, format: TextFormatType, icon: string): EditorButtonDefinition {
     return {
@@ -32,7 +31,7 @@ export const bold: EditorButtonDefinition = buildFormatButton('Bold', 'bold', bo
 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: 'Background color', icon: highlightIcon};
+export const highlightColor: EditorBasicButtonDefinition = {label: 'Highlight color', icon: highlightIcon};
 
 function colorAction(context: EditorUiContext, property: string, color: string): void {
     context.editor.update(() => {
@@ -44,7 +43,7 @@ function colorAction(context: EditorUiContext, property: string, color: string):
 }
 
 export const textColorAction = (color: string, context: EditorUiContext) => colorAction(context, 'color', color);
-export const highlightColorAction = (color: string, context: EditorUiContext) => colorAction(context, 'color', color);
+export const highlightColorAction = (color: string, context: EditorUiContext) => colorAction(context, 'background-color', color);
 
 export const strikethrough: EditorButtonDefinition = buildFormatButton('Strikethrough', 'strikethrough', strikethroughIcon);
 export const superscript: EditorButtonDefinition = buildFormatButton('Superscript', 'superscript', superscriptIcon);
index 5b484310d9a52bb11add68aa26335a0d6944e446..031e0098368a39f3cebe51b383c26dc6d46d1b76 100644 (file)
@@ -75,7 +75,7 @@ export function $showCellPropertiesForm(cell: TableCellNode, context: EditorUiCo
         border_width: styles.get('border-width') || '',
         border_style: styles.get('border-style') || '',
         border_color: styles.get('border-color') || '',
-        background_color: styles.get('background-color') || '',
+        background_color: cell.getBackgroundColor() || styles.get('background-color') || '',
     });
     return modalForm;
 }
@@ -91,6 +91,7 @@ export const cellProperties: EditorFormDefinition = {
                 $setTableCellColumnWidth(cell, width);
                 cell.updateTag(formData.get('type')?.toString() || '');
                 cell.setAlignment((formData.get('h_align')?.toString() || '') as CommonBlockAlignment);
+                cell.setBackgroundColor(formData.get('background_color')?.toString() || '');
 
                 const styles = cell.getStyles();
                 styles.set('height', formatSizeValue(formData.get('height')?.toString() || ''));
@@ -98,7 +99,6 @@ export const cellProperties: EditorFormDefinition = {
                 styles.set('border-width', formatSizeValue(formData.get('border_width')?.toString() || ''));
                 styles.set('border-style', formData.get('border_style')?.toString() || '');
                 styles.set('border-color', formData.get('border_color')?.toString() || '');
-                styles.set('background-color', formData.get('background_color')?.toString() || '');
 
                 cell.setStyles(styles);
             }
index 8f4a6599f9a17d760fceb36d624b5a97d95603c6..15cc3cbbeb8e6455c671b0545af22d243de012d7 100644 (file)
@@ -282,6 +282,7 @@ export function $clearTableFormatting(table: TableNode): void {
         const cells = row.getChildren().filter(c => $isTableCellNode(c));
         for (const cell of cells) {
             cell.setStyles(new Map);
+            cell.setBackgroundColor(null);
             cell.clearWidth();
         }
     }
index de43540a31d80b5a9d5cc3ee2ba1f99db270c227..a7f5ab387d5ee1781ca3f867ac241bdda8dba667 100644 (file)
@@ -681,6 +681,14 @@ textarea.editor-form-field-input {
   }
 }
 
+// Specific field styles
+textarea.editor-form-field-input[name="source"] {
+  width: 1000px;
+  height: 600px;
+  max-height: 60vh;
+  max-width: 80vw;
+}
+
 // Editor theme styles
 .editor-theme-bold {
   font-weight: bold;