From 5fc19b0edf3cf4bf88e54ff11c63a82d8664218f Mon Sep 17 00:00:00 2001 From: Dan Brown Date: Thu, 24 Jul 2025 13:48:00 +0100 Subject: [PATCH 1/5] Lexical: Fixed highlight format action, changed label --- lang/en/editor.php | 1 + resources/js/wysiwyg/ui/defaults/buttons/inline-formats.ts | 4 ++-- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/lang/en/editor.php b/lang/en/editor.php index 752c6f3f7..0d250e9a7 100644 --- a/lang/en/editor.php +++ b/lang/en/editor.php @@ -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', diff --git a/resources/js/wysiwyg/ui/defaults/buttons/inline-formats.ts b/resources/js/wysiwyg/ui/defaults/buttons/inline-formats.ts index c5b7ad29a..11411e140 100644 --- a/resources/js/wysiwyg/ui/defaults/buttons/inline-formats.ts +++ b/resources/js/wysiwyg/ui/defaults/buttons/inline-formats.ts @@ -32,7 +32,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 +44,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); From ae4d1d804a8dbe3e95511afb2470e36a0d4a38fa Mon Sep 17 00:00:00 2001 From: Dan Brown Date: Thu, 24 Jul 2025 16:51:11 +0100 Subject: [PATCH 2/5] Lexical: Table cell bg and format setting fixes - Updated table cell background color setting to be stable by specifically using the background property over the general styles. - Updated format shorcuts to be correct header levels as per old editor and format menu. - Updated format changes to properly update UI afterwards. --- .../lexical/table/LexicalTableCellNode.ts | 9 ++++++++- resources/js/wysiwyg/services/shortcuts.ts | 16 +++++++++------- .../ui/defaults/buttons/inline-formats.ts | 1 - resources/js/wysiwyg/ui/defaults/forms/tables.ts | 4 ++-- resources/js/wysiwyg/utils/tables.ts | 1 + 5 files changed, 20 insertions(+), 11 deletions(-) diff --git a/resources/js/wysiwyg/lexical/table/LexicalTableCellNode.ts b/resources/js/wysiwyg/lexical/table/LexicalTableCellNode.ts index 1fc6b42bb..1c9d7ecf6 100644 --- a/resources/js/wysiwyg/lexical/table/LexicalTableCellNode.ts +++ b/resources/js/wysiwyg/lexical/table/LexicalTableCellNode.ts @@ -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) { diff --git a/resources/js/wysiwyg/services/shortcuts.ts b/resources/js/wysiwyg/services/shortcuts.ts index 0384a3bf1..ead4c38d4 100644 --- a/resources/js/wysiwyg/services/shortcuts.ts +++ b/resources/js/wysiwyg/services/shortcuts.ts @@ -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 = { 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), diff --git a/resources/js/wysiwyg/ui/defaults/buttons/inline-formats.ts b/resources/js/wysiwyg/ui/defaults/buttons/inline-formats.ts index 11411e140..dea78d24f 100644 --- a/resources/js/wysiwyg/ui/defaults/buttons/inline-formats.ts +++ b/resources/js/wysiwyg/ui/defaults/buttons/inline-formats.ts @@ -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 { diff --git a/resources/js/wysiwyg/ui/defaults/forms/tables.ts b/resources/js/wysiwyg/ui/defaults/forms/tables.ts index 5b484310d..031e00983 100644 --- a/resources/js/wysiwyg/ui/defaults/forms/tables.ts +++ b/resources/js/wysiwyg/ui/defaults/forms/tables.ts @@ -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); } diff --git a/resources/js/wysiwyg/utils/tables.ts b/resources/js/wysiwyg/utils/tables.ts index 8f4a6599f..15cc3cbbe 100644 --- a/resources/js/wysiwyg/utils/tables.ts +++ b/resources/js/wysiwyg/utils/tables.ts @@ -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(); } } From 865e5aecc99ebb8051f7b1c99b11710bd9709383 Mon Sep 17 00:00:00 2001 From: Dan Brown Date: Thu, 24 Jul 2025 17:24:59 +0100 Subject: [PATCH 3/5] Lexical: Source code input changes - Increased default source code view size. - Updated HTML generation to output each top-level block on its own line. --- .../html/__tests__/unit/LexicalHtml.test.ts | 2 +- resources/js/wysiwyg/lexical/html/index.ts | 13 ++++++++- .../unit/LexicalUtilsSplitNode.test.ts | 12 ++++---- ...exlcaiUtilsInsertNodeToNearestRoot.test.ts | 28 +++++++++---------- resources/sass/_editor.scss | 8 ++++++ 5 files changed, 41 insertions(+), 22 deletions(-) diff --git a/resources/js/wysiwyg/lexical/html/__tests__/unit/LexicalHtml.test.ts b/resources/js/wysiwyg/lexical/html/__tests__/unit/LexicalHtml.test.ts index e5064121a..b466ee34a 100644 --- a/resources/js/wysiwyg/lexical/html/__tests__/unit/LexicalHtml.test.ts +++ b/resources/js/wysiwyg/lexical/html/__tests__/unit/LexicalHtml.test.ts @@ -146,7 +146,7 @@ describe('HTML', () => { }); expect(html).toBe( - '

Hello

World

', + '

Hello

\n

World

', ); }); diff --git a/resources/js/wysiwyg/lexical/html/index.ts b/resources/js/wysiwyg/lexical/html/index.ts index 5018e10b4..de5e53bb8 100644 --- a/resources/js/wysiwyg/lexical/html/index.ts +++ b/resources/js/wysiwyg/lexical/html/index.ts @@ -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( diff --git a/resources/js/wysiwyg/lexical/utils/__tests__/unit/LexicalUtilsSplitNode.test.ts b/resources/js/wysiwyg/lexical/utils/__tests__/unit/LexicalUtilsSplitNode.test.ts index 54cd8b54f..6a415d008 100644 --- a/resources/js/wysiwyg/lexical/utils/__tests__/unit/LexicalUtilsSplitNode.test.ts +++ b/resources/js/wysiwyg/lexical/utils/__tests__/unit/LexicalUtilsSplitNode.test.ts @@ -38,7 +38,7 @@ describe('LexicalUtils#splitNode', () => { { _: 'split paragraph in between two text nodes', expectedHtml: - '

Hello

world

', + '

Hello

\n

world

', initialHtml: '

Helloworld

', splitOffset: 1, splitPath: [0], @@ -46,7 +46,7 @@ describe('LexicalUtils#splitNode', () => { { _: 'split paragraph before the first text node', expectedHtml: - '


Helloworld

', + '


\n

Helloworld

', initialHtml: '

Helloworld

', splitOffset: 0, splitPath: [0], @@ -54,7 +54,7 @@ describe('LexicalUtils#splitNode', () => { { _: 'split paragraph after the last text node', expectedHtml: - '

Helloworld


', + '

Helloworld

\n


', initialHtml: '

Helloworld

', 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: - '
  • Hello
' + + '
  • Hello
\n' + '
  • world
', initialHtml: '
  • Helloworld
', 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: - '
' + + '
\n' + '
  • Helloworld
', initialHtml: '
  • Helloworld
', splitOffset: 0, // Any offset that is higher than children size @@ -83,7 +83,7 @@ describe('LexicalUtils#splitNode', () => { '
    ' + '
  • Before
  • ' + '
    • Hello
  • ' + - '
' + + '\n' + '
    ' + '
    • world
  • ' + '
  • After
  • ' + diff --git a/resources/js/wysiwyg/lexical/utils/__tests__/unit/LexlcaiUtilsInsertNodeToNearestRoot.test.ts b/resources/js/wysiwyg/lexical/utils/__tests__/unit/LexlcaiUtilsInsertNodeToNearestRoot.test.ts index 8c31496de..f13aed408 100644 --- a/resources/js/wysiwyg/lexical/utils/__tests__/unit/LexlcaiUtilsInsertNodeToNearestRoot.test.ts +++ b/resources/js/wysiwyg/lexical/utils/__tests__/unit/LexlcaiUtilsInsertNodeToNearestRoot.test.ts @@ -46,7 +46,7 @@ describe('LexicalUtils#insertNodeToNearestRoot', () => { { _: 'insert into paragraph in between two text nodes', expectedHtml: - '

    Hello

    world

    ', + '

    Hello

    \n\n

    world

    ', initialHtml: '

    Helloworld

    ', selectionOffset: 5, // Selection on text node after "Hello" world selectionPath: [0, 0], @@ -57,8 +57,8 @@ describe('LexicalUtils#insertNodeToNearestRoot', () => { '
      ' + '
    • Before
    • ' + '
      • Hello
    • ' + - '
    ' + - '' + + '
\n' + + '\n' + '
    ' + '
    • world
  • ' + '
  • After
  • ' + @@ -74,7 +74,7 @@ describe('LexicalUtils#insertNodeToNearestRoot', () => { }, { _: 'insert into empty paragraph', - expectedHtml: '



    ', + expectedHtml: '


    \n\n


    ', initialHtml: '

    ', 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: - '

    Hello world

    ' + - '' + + '

    Hello world

    \n' + + '\n' + '


    ', initialHtml: '

    Hello world

    ', selectionOffset: 12, // Selection on text node after "Hello" world @@ -92,8 +92,8 @@ describe('LexicalUtils#insertNodeToNearestRoot', () => { { _: 'insert in the beginning of paragraph', expectedHtml: - '


    ' + - '' + + '


    \n' + + '\n' + '

    Hello world

    ', initialHtml: '

    Hello world

    ', selectionOffset: 0, // Selection on text node after "Hello" world @@ -102,9 +102,9 @@ describe('LexicalUtils#insertNodeToNearestRoot', () => { { _: 'insert with selection on root start', expectedHtml: - '' + - '' + - '

    Before

    ' + + '\n' + + '\n' + + '

    Before

    \n' + '

    After

    ', initialHtml: '' + @@ -116,8 +116,8 @@ describe('LexicalUtils#insertNodeToNearestRoot', () => { { _: 'insert with selection on root child', expectedHtml: - '

    Before

    ' + - '' + + '

    Before

    \n' + + '\n' + '

    After

    ', initialHtml: '

    Before

    After

    ', selectionOffset: 1, @@ -126,7 +126,7 @@ describe('LexicalUtils#insertNodeToNearestRoot', () => { { _: 'insert with selection on root end', expectedHtml: - '

    Before

    ' + + '

    Before

    \n' + '', initialHtml: '

    Before

    ', selectionOffset: 1, diff --git a/resources/sass/_editor.scss b/resources/sass/_editor.scss index de43540a3..a7f5ab387 100644 --- a/resources/sass/_editor.scss +++ b/resources/sass/_editor.scss @@ -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; From c54101c603a3e8e06c372de673a811d41a1f1f9d Mon Sep 17 00:00:00 2001 From: Dan Brown Date: Fri, 25 Jul 2025 13:58:48 +0100 Subject: [PATCH 4/5] Lexical: Updated URL handling, added mouse handling - Removed URL protocol allow-list to allow any as per old editor. - Added mouse handling, so that clicks below many last hard-to-escape block types will add an empty new paragraph for easy escaping & editing. --- resources/js/wysiwyg/index.ts | 2 + .../lexical/core/__tests__/utils/index.ts | 16 +++++ resources/js/wysiwyg/lexical/link/index.ts | 23 +------ .../services/__tests__/mouse-handling.test.ts | 51 +++++++++++++++ .../js/wysiwyg/services/mouse-handling.ts | 63 +++++++++++++++++++ 5 files changed, 133 insertions(+), 22 deletions(-) create mode 100644 resources/js/wysiwyg/services/__tests__/mouse-handling.test.ts create mode 100644 resources/js/wysiwyg/services/mouse-handling.ts diff --git a/resources/js/wysiwyg/index.ts b/resources/js/wysiwyg/index.ts index f572f9de5..e01b4e8f4 100644 --- a/resources/js/wysiwyg/index.ts +++ b/resources/js/wysiwyg/index.ts @@ -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), diff --git a/resources/js/wysiwyg/lexical/core/__tests__/utils/index.ts b/resources/js/wysiwyg/lexical/core/__tests__/utils/index.ts index e18ef9756..fd87877ee 100644 --- a/resources/js/wysiwyg/lexical/core/__tests__/utils/index.ts +++ b/resources/js/wysiwyg/lexical/core/__tests__/utils/index.ts @@ -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 diff --git a/resources/js/wysiwyg/lexical/link/index.ts b/resources/js/wysiwyg/lexical/link/index.ts index 884fe9153..336bb1546 100644 --- a/resources/js/wysiwyg/lexical/link/index.ts +++ b/resources/js/wysiwyg/lexical/link/index.ts @@ -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(), 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 index 000000000..a3da35206 --- /dev/null +++ b/resources/js/wysiwyg/services/__tests__/mouse-handling.test.ts @@ -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 index 000000000..058efc8d2 --- /dev/null +++ b/resources/js/wysiwyg/services/mouse-handling.ts @@ -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 From d145efb6f6eac73947c7f3c53173f42b4dd9a615 Mon Sep 17 00:00:00 2001 From: Dan Brown Date: Fri, 25 Jul 2025 14:25:02 +0100 Subject: [PATCH 5/5] Lexical: Updated tests after link changes --- .../link/__tests__/unit/LexicalAutoLinkNode.test.ts | 12 ------------ .../link/__tests__/unit/LexicalLinkNode.test.ts | 12 ------------ 2 files changed, 24 deletions(-) diff --git a/resources/js/wysiwyg/lexical/link/__tests__/unit/LexicalAutoLinkNode.test.ts b/resources/js/wysiwyg/lexical/link/__tests__/unit/LexicalAutoLinkNode.test.ts index 0f3513682..1103f73d3 100644 --- a/resources/js/wysiwyg/lexical/link/__tests__/unit/LexicalAutoLinkNode.test.ts +++ b/resources/js/wysiwyg/lexical/link/__tests__/unit/LexicalAutoLinkNode.test.ts @@ -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( - '', - ); - }); - }); - test('AutoLinkNode.updateDOM()', async () => { const {editor} = testEnv; diff --git a/resources/js/wysiwyg/lexical/link/__tests__/unit/LexicalLinkNode.test.ts b/resources/js/wysiwyg/lexical/link/__tests__/unit/LexicalLinkNode.test.ts index 1aff91863..c50450302 100644 --- a/resources/js/wysiwyg/lexical/link/__tests__/unit/LexicalLinkNode.test.ts +++ b/resources/js/wysiwyg/lexical/link/__tests__/unit/LexicalLinkNode.test.ts @@ -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( - '', - ); - }); - }); - test('LinkNode.updateDOM()', async () => { const {editor} = testEnv;