mirror of
https://github.com/BookStackApp/BookStack.git
synced 2026-07-15 21:31:36 +03:00
Lexical: Fixed actions not applying on empty state
Updated editor to always attempt to start from at least a paragraph isntead of empty state. Improved focus on HTML change.
This commit is contained in:
@@ -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: [
|
||||
|
||||
107
resources/js/wysiwyg/utils/__tests__/actions.test.ts
Normal file
107
resources/js/wysiwyg/utils/__tests__/actions.test.ts
Normal file
@@ -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, '<p>Hello World</p>');
|
||||
await Promise.resolve().then();
|
||||
|
||||
expectNodeShapeToMatch(editor, [
|
||||
{
|
||||
type: 'paragraph',
|
||||
children: [{ text: 'Hello World' }],
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
||||
it('parses multiple block elements', async () => {
|
||||
setEditorContentFromHtml(editor, '<p>First</p><p>Second</p>');
|
||||
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, '<p>New</p>');
|
||||
await Promise.resolve().then();
|
||||
|
||||
expectNodeShapeToMatch(editor, [
|
||||
{ type: 'paragraph', children: [{ text: 'New' }] },
|
||||
]);
|
||||
});
|
||||
|
||||
it('handles nested HTML structures', async () => {
|
||||
setEditorContentFromHtml(editor, '<ul><li>Item A</li><li>Item B</li></ul>');
|
||||
await Promise.resolve().then();
|
||||
|
||||
expectNodeShapeToMatch(editor, [
|
||||
{
|
||||
type: 'list',
|
||||
children: [
|
||||
{ type: 'listitem', children: [{ text: 'Item A' }] },
|
||||
{ type: 'listitem', children: [{ text: 'Item B' }] },
|
||||
],
|
||||
},
|
||||
]);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -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<string> {
|
||||
}
|
||||
|
||||
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"});
|
||||
}
|
||||
@@ -11,7 +11,7 @@
|
||||
|
||||
{{-- <div id="lexical-debug" style="white-space: pre-wrap; font-size: 12px; height: 200px; overflow-y: scroll; background-color: #000; padding: 1rem; border-radius: 4px; color: #FFF;"></div>--}}
|
||||
|
||||
<textarea refs="wysiwyg-editor@input" id="html-editor" hidden="hidden" name="html" rows="5">{{ old('html') ?? $model->html ?? '' }}</textarea>
|
||||
<textarea refs="wysiwyg-editor@input" id="html-editor" hidden="hidden" name="html" rows="5">{{ (old('html') ?? $model->html ?? '') ?: '<p></p>' }}</textarea>
|
||||
</div>
|
||||
|
||||
@if($errors->has('html'))
|
||||
|
||||
Reference in New Issue
Block a user