diff --git a/resources/js/wysiwyg/ui/defaults/forms/controls.ts b/resources/js/wysiwyg/ui/defaults/forms/controls.ts index 8e7219d67..c92a16834 100644 --- a/resources/js/wysiwyg/ui/defaults/forms/controls.ts +++ b/resources/js/wysiwyg/ui/defaults/forms/controls.ts @@ -1,12 +1,16 @@ import {EditorFormDefinition} from "../../framework/forms"; import {EditorUiContext, EditorUiElement} from "../../framework/core"; -import {setEditorContentFromHtml} from "../../../utils/actions"; +import {focusEditor, setEditorContentFromHtml} from "../../../utils/actions"; import {ExternalContent} from "../../framework/blocks/external-content"; export const source: EditorFormDefinition = { submitText: 'Save', async action(formData, context: EditorUiContext) { setEditorContentFromHtml(context.editor, formData.get('source')?.toString() || ''); + context.editor.commitUpdates(); + window.requestAnimationFrame(() => { + focusEditor(context.editor); + }); return true; }, fields: [ diff --git a/resources/js/wysiwyg/utils/__tests__/actions.test.ts b/resources/js/wysiwyg/utils/__tests__/actions.test.ts new file mode 100644 index 000000000..9450638f5 --- /dev/null +++ b/resources/js/wysiwyg/utils/__tests__/actions.test.ts @@ -0,0 +1,107 @@ +import { + createTestContext, + destroyFromContext, + expectNodeShapeToMatch, +} from "lexical/__tests__/utils"; +import { $createParagraphNode, $createTextNode, $getRoot, LexicalEditor } from "lexical"; +import { EditorUiContext } from "../../ui/framework/core"; +import { setEditorContentFromHtml } from "../actions"; + +describe('Actions', () => { + + let context!: EditorUiContext; + let editor!: LexicalEditor; + + beforeEach(() => { + context = createTestContext(); + editor = context.editor; + }); + + afterEach(() => { + destroyFromContext(context); + }); + + describe('setEditorContentFromHtml', () => { + it('parses HTML and sets content in editor', async () => { + setEditorContentFromHtml(editor, '

Hello World

'); + await Promise.resolve().then(); + + expectNodeShapeToMatch(editor, [ + { + type: 'paragraph', + children: [{ text: 'Hello World' }], + }, + ]); + }); + + it('parses multiple block elements', async () => { + setEditorContentFromHtml(editor, '

First

Second

'); + await Promise.resolve().then(); + + expectNodeShapeToMatch(editor, [ + { type: 'paragraph', children: [{ text: 'First' }] }, + { type: 'paragraph', children: [{ text: 'Second' }] }, + ]); + }); + + it('wraps plain text in a paragraph', async () => { + setEditorContentFromHtml(editor, 'Plain text'); + await Promise.resolve().then(); + + expectNodeShapeToMatch(editor, [ + { + type: 'paragraph', + children: [{ text: 'Plain text' }], + }, + ]); + }); + + it('ensures at least a paragraph when HTML is empty', async () => { + setEditorContentFromHtml(editor, ''); + await Promise.resolve().then(); + + expectNodeShapeToMatch(editor, [ + { type: 'paragraph' }, + ]); + }); + + it('ensures at least a paragraph when HTML contains only whitespace', async () => { + setEditorContentFromHtml(editor, ' '); + await Promise.resolve().then(); + + expectNodeShapeToMatch(editor, [ + { type: 'paragraph' }, + ]); + }); + + it('clears existing content before setting new content', async () => { + editor.updateAndCommit(() => { + const p = $createParagraphNode(); + p.append($createTextNode('Existing')); + $getRoot().append(p); + }); + + setEditorContentFromHtml(editor, '

New

'); + await Promise.resolve().then(); + + expectNodeShapeToMatch(editor, [ + { type: 'paragraph', children: [{ text: 'New' }] }, + ]); + }); + + it('handles nested HTML structures', async () => { + setEditorContentFromHtml(editor, ''); + await Promise.resolve().then(); + + expectNodeShapeToMatch(editor, [ + { + type: 'list', + children: [ + { type: 'listitem', children: [{ text: 'Item A' }] }, + { type: 'listitem', children: [{ text: 'Item B' }] }, + ], + }, + ]); + }); + }); +}); diff --git a/resources/js/wysiwyg/utils/actions.ts b/resources/js/wysiwyg/utils/actions.ts index e18ac515f..465d44d31 100644 --- a/resources/js/wysiwyg/utils/actions.ts +++ b/resources/js/wysiwyg/utils/actions.ts @@ -1,4 +1,4 @@ -import {$getRoot, $getSelection, $insertNodes, $isBlockElementNode, LexicalEditor} from "lexical"; +import {$createParagraphNode, $getRoot, $getSelection, $insertNodes, $isBlockElementNode, LexicalEditor} from "lexical"; import {$generateHtmlFromNodes} from "@lexical/html"; import {$getNearestNodeBlockParent, $htmlToBlockNodes, $htmlToNodes} from "./nodes"; @@ -12,6 +12,12 @@ export function setEditorContentFromHtml(editor: LexicalEditor, html: string) { const nodes = $htmlToBlockNodes(editor, html); root.append(...nodes); + + // Always ensure we at least have a paragraph in the root + // as a target for the cursor/focus/actions. + if (root.isEmpty()) { + root.append($createParagraphNode()); + } }); } @@ -85,5 +91,14 @@ export function getEditorContentAsHtml(editor: LexicalEditor): Promise { } export function focusEditor(editor: LexicalEditor): void { + editor.update(() => { + const root = $getRoot(); + const selection = $getSelection(); + const firstChild = root.getFirstChild(); + if (firstChild && !selection) { + firstChild.selectStart(); + } + }); + editor.commitUpdates(); editor.focus(() => {}, {defaultSelection: "rootStart"}); } \ No newline at end of file diff --git a/resources/views/pages/parts/wysiwyg-editor.blade.php b/resources/views/pages/parts/wysiwyg-editor.blade.php index 73e655752..4344b234d 100644 --- a/resources/views/pages/parts/wysiwyg-editor.blade.php +++ b/resources/views/pages/parts/wysiwyg-editor.blade.php @@ -11,7 +11,7 @@ {{--
--}} - + @if($errors->has('html'))