Lexical: Added view/edit source code button/form/action

This commit is contained in:
Dan Brown 2024-06-12 14:01:36 +01:00
parent 5c343638b6
commit e889bc680b
No known key found for this signature in database
GPG Key ID: 46D9F943C24A2EF9
7 changed files with 70 additions and 14 deletions

View File

@ -0,0 +1,26 @@
import {$getRoot, LexicalEditor} from "lexical";
import {$generateHtmlFromNodes, $generateNodesFromDOM} from "@lexical/html";
export function setEditorContentFromHtml(editor: LexicalEditor, html: string) {
const parser = new DOMParser();
const dom = parser.parseFromString(html, 'text/html');
editor.update(() => {
const nodes = $generateNodesFromDOM(editor, dom);
const root = $getRoot();
for (const child of root.getChildren()) {
child.remove(true);
}
root.append(...nodes);
});
}
export function getEditorContentAsHtml(editor: LexicalEditor): Promise<string> {
return new Promise((resolve, reject) => {
editor.getEditorState().read(() => {
const html = $generateHtmlFromNodes(editor);
resolve(html);
});
});
}

View File

@ -1,10 +1,10 @@
import {$getRoot, createEditor, CreateEditorArgs} from 'lexical'; import {createEditor, CreateEditorArgs} from 'lexical';
import {createEmptyHistoryState, registerHistory} from '@lexical/history'; import {createEmptyHistoryState, registerHistory} from '@lexical/history';
import {registerRichText} from '@lexical/rich-text'; import {registerRichText} from '@lexical/rich-text';
import {mergeRegister} from '@lexical/utils'; import {mergeRegister} from '@lexical/utils';
import {$generateNodesFromDOM} from '@lexical/html';
import {getNodesForPageEditor} from './nodes'; import {getNodesForPageEditor} from './nodes';
import {buildEditorUI} from "./ui"; import {buildEditorUI} from "./ui";
import {setEditorContentFromHtml} from "./actions";
export function createPageEditorInstance(editArea: HTMLElement) { export function createPageEditorInstance(editArea: HTMLElement) {
const config: CreateEditorArgs = { const config: CreateEditorArgs = {
@ -14,8 +14,6 @@ export function createPageEditorInstance(editArea: HTMLElement) {
}; };
const startingHtml = editArea.innerHTML; const startingHtml = editArea.innerHTML;
const parser = new DOMParser();
const dom = parser.parseFromString(startingHtml, 'text/html');
const editor = createEditor(config); const editor = createEditor(config);
editor.setRootElement(editArea); editor.setRootElement(editArea);
@ -25,11 +23,7 @@ export function createPageEditorInstance(editArea: HTMLElement) {
registerHistory(editor, createEmptyHistoryState(), 300), registerHistory(editor, createEmptyHistoryState(), 300),
); );
editor.update(() => { setEditorContentFromHtml(editor, startingHtml);
const startingNodes = $generateNodesFromDOM(editor, dom);
const root = $getRoot();
root.append(...startingNodes);
});
const debugView = document.getElementById('lexical-debug'); const debugView = document.getElementById('lexical-debug');
editor.registerUpdateListener(({editorState}) => { editor.registerUpdateListener(({editorState}) => {

View File

@ -28,6 +28,7 @@ import {EditorUiContext} from "../framework/core";
import {$isImageNode, ImageNode} from "../../nodes/image"; import {$isImageNode, ImageNode} from "../../nodes/image";
import {$createDetailsNode, $isDetailsNode} from "../../nodes/details"; import {$createDetailsNode, $isDetailsNode} from "../../nodes/details";
import {$insertNodeToNearestRoot} from "@lexical/utils"; import {$insertNodeToNearestRoot} from "@lexical/utils";
import {getEditorContentAsHtml} from "../../actions";
export const undo: EditorButtonDefinition = { export const undo: EditorButtonDefinition = {
label: 'Undo', label: 'Undo',
@ -230,3 +231,14 @@ export const details: EditorButtonDefinition = {
} }
} }
export const source: EditorButtonDefinition = {
label: 'Source code',
async action(context: EditorUiContext) {
const modal = context.manager.createModal('source');
const source = await getEditorContentAsHtml(context.editor);
modal.show({source});
},
isActive() {
return false;
}
};

View File

@ -3,6 +3,7 @@ import {EditorUiContext} from "../framework/core";
import {$createLinkNode} from "@lexical/link"; import {$createLinkNode} from "@lexical/link";
import {$createTextNode, $getSelection} from "lexical"; import {$createTextNode, $getSelection} from "lexical";
import {$createImageNode} from "../../nodes/image"; import {$createImageNode} from "../../nodes/image";
import {setEditorContentFromHtml} from "../../actions";
export const link: EditorFormDefinition = { export const link: EditorFormDefinition = {
@ -86,4 +87,19 @@ export const image: EditorFormDefinition = {
type: 'text', type: 'text',
}, },
], ],
};
export const source: EditorFormDefinition = {
submitText: 'Save',
action(formData, context: EditorUiContext) {
setEditorContentFromHtml(context.editor, formData.get('source')?.toString() || '');
return true;
},
fields: [
{
label: 'Source',
name: 'source',
type: 'textarea',
},
],
}; };

View File

@ -5,7 +5,7 @@ import {el} from "../../helpers";
export interface EditorFormFieldDefinition { export interface EditorFormFieldDefinition {
label: string; label: string;
name: string; name: string;
type: 'text' | 'select'; type: 'text' | 'select' | 'textarea';
} }
export interface EditorSelectFormFieldDefinition extends EditorFormFieldDefinition { export interface EditorSelectFormFieldDefinition extends EditorFormFieldDefinition {
@ -28,7 +28,7 @@ export class EditorFormField extends EditorUiElement {
} }
setValue(value: string) { setValue(value: string) {
const input = this.getDOMElement().querySelector('input,select') as HTMLInputElement; const input = this.getDOMElement().querySelector('input,select,textarea') as HTMLInputElement;
input.value = value; input.value = value;
} }
@ -45,6 +45,8 @@ export class EditorFormField extends EditorUiElement {
const labels = Object.keys(options); const labels = Object.keys(options);
const optionElems = labels.map(label => el('option', {value: options[label]}, [label])); const optionElems = labels.map(label => el('option', {value: options[label]}, [label]));
input = el('select', {id, name: this.definition.name, class: 'editor-form-field-input'}, optionElems); input = el('select', {id, name: this.definition.name, class: 'editor-form-field-input'}, optionElems);
} else if (this.definition.type === 'textarea') {
input = el('textarea', {id, name: this.definition.name, class: 'editor-form-field-input'});
} else { } else {
input = el('input', {id, name: this.definition.name, class: 'editor-form-field-input'}); input = el('input', {id, name: this.definition.name, class: 'editor-form-field-input'});
} }

View File

@ -6,7 +6,7 @@ import {
} from "lexical"; } from "lexical";
import {getMainEditorFullToolbar} from "./toolbars"; import {getMainEditorFullToolbar} from "./toolbars";
import {EditorUIManager} from "./framework/manager"; import {EditorUIManager} from "./framework/manager";
import {image as imageFormDefinition, link as linkFormDefinition} from "./defaults/form-definitions"; import {image as imageFormDefinition, link as linkFormDefinition, source as sourceFormDefinition} from "./defaults/form-definitions";
import {DecoratorListener} from "lexical/LexicalEditor"; import {DecoratorListener} from "lexical/LexicalEditor";
import type {NodeKey} from "lexical/LexicalNode"; import type {NodeKey} from "lexical/LexicalNode";
import {EditorDecoratorAdapter} from "./framework/decorator"; import {EditorDecoratorAdapter} from "./framework/decorator";
@ -36,7 +36,11 @@ export function buildEditorUI(element: HTMLElement, editor: LexicalEditor) {
manager.registerModal('image', { manager.registerModal('image', {
title: 'Insert/Edit Image', title: 'Insert/Edit Image',
form: imageFormDefinition form: imageFormDefinition
}) });
manager.registerModal('source', {
title: 'Source code',
form: sourceFormDefinition,
});
// Register decorator listener // Register decorator listener
// Maybe move to manager? // Maybe move to manager?

View File

@ -4,7 +4,7 @@ import {
dangerCallout, details, dangerCallout, details,
h2, h3, h4, h5, image, h2, h3, h4, h5, image,
infoCallout, italic, link, paragraph, infoCallout, italic, link, paragraph,
redo, strikethrough, subscript, redo, source, strikethrough, subscript,
successCallout, superscript, underline, successCallout, superscript, underline,
undo, undo,
warningCallout warningCallout
@ -42,5 +42,7 @@ export function getMainEditorFullToolbar(): EditorContainerUiElement {
new EditorButton(link), new EditorButton(link),
new EditorButton(image), new EditorButton(image),
new EditorButton(details), new EditorButton(details),
new EditorButton(source),
]); ]);
} }