Lexical: Started code block node implementation

This commit is contained in:
Dan Brown 2024-07-02 14:46:30 +01:00
parent 9ebbf7ce94
commit 97f570a4ee
No known key found for this signature in database
GPG Key ID: 46D9F943C24A2EF9
6 changed files with 226 additions and 3 deletions

View File

@ -0,0 +1,168 @@
import {
DecoratorNode,
DOMConversion,
DOMConversionMap,
DOMConversionOutput,
LexicalEditor, LexicalNode,
SerializedLexicalNode,
Spread
} from "lexical";
import type {EditorConfig} from "lexical/LexicalEditor";
import {el} from "../helpers";
import {EditorDecoratorAdapter} from "../ui/framework/decorator";
import {code} from "../ui/defaults/button-definitions";
export type SerializedCodeBlockNode = Spread<{
language: string;
id: string;
code: string;
}, SerializedLexicalNode>
const getLanguageFromClassList = (classes: string) => {
const langClasses = classes.split(' ').filter(cssClass => cssClass.startsWith('language-'));
return (langClasses[0] || '').replace('language-', '');
};
export class CodeBlockNode extends DecoratorNode<EditorDecoratorAdapter> {
__id: string = '';
__language: string = '';
__code: string = '';
static getType(): string {
return 'code-block';
}
static clone(node: CodeBlockNode): CodeBlockNode {
return new CodeBlockNode(node.__language, node.__code);
}
constructor(language: string = '', code: string = '', key?: string) {
super(key);
this.__language = language;
this.__code = code;
}
setLanguage(language: string): void {
const self = this.getWritable();
self.__language = language;
}
getLanguage(): string {
const self = this.getLatest();
return self.__language;
}
setCode(code: string): void {
const self = this.getWritable();
self.__code = code;
}
getCode(): string {
const self = this.getLatest();
return self.__code;
}
setId(id: string) {
const self = this.getWritable();
self.__id = id;
}
getId(): string {
const self = this.getLatest();
return self.__id;
}
decorate(editor: LexicalEditor, config: EditorConfig): EditorDecoratorAdapter {
// TODO
return {
type: 'code',
getNode: () => this,
};
}
isInline(): boolean {
return false;
}
isIsolated() {
return true;
}
createDOM(_config: EditorConfig, _editor: LexicalEditor) {
const codeBlock = el('pre', {
id: this.__id || null,
}, [
el('code', {
class: this.__language ? `language-${this.__language}` : null,
}, [this.__code]),
]);
return el('div', {class: 'editor-code-block-wrap'}, [codeBlock]);
}
updateDOM(prevNode: CodeBlockNode, dom: HTMLElement) {
const code = dom.querySelector('code');
if (!code) return false;
if (prevNode.__language !== this.__language) {
code.className = this.__language ? `language-${this.__language}` : '';
}
if (prevNode.__id !== this.__id) {
dom.setAttribute('id', this.__id);
}
if (prevNode.__code !== this.__code) {
code.textContent = this.__code;
}
return false;
}
static importDOM(): DOMConversionMap|null {
return {
pre(node: HTMLElement): DOMConversion|null {
return {
conversion: (element: HTMLElement): DOMConversionOutput|null => {
const codeEl = element.querySelector('code');
const language = getLanguageFromClassList(element.className)
|| (codeEl && getLanguageFromClassList(codeEl.className))
|| '';
const code = codeEl ? (codeEl.textContent || '').trim() : (element.textContent || '').trim();
return {
node: $createCodeBlockNode(language, code),
};
},
priority: 3,
};
},
};
}
exportJSON(): SerializedCodeBlockNode {
return {
type: 'code-block',
version: 1,
id: this.__id,
language: this.__language,
code: this.__code,
};
}
static importJSON(serializedNode: SerializedCodeBlockNode): CodeBlockNode {
const node = $createCodeBlockNode(serializedNode.language, serializedNode.code);
node.setId(serializedNode.id || '');
return node;
}
}
export function $createCodeBlockNode(language: string = '', code: string = ''): CodeBlockNode {
return new CodeBlockNode(language, code);
}
export function $isCodeBlockNode(node: LexicalNode | null | undefined) {
return node instanceof CodeBlockNode;
}

View File

@ -9,6 +9,7 @@ import {ListItemNode, ListNode} from "@lexical/list";
import {TableCellNode, TableNode, TableRowNode} from "@lexical/table";
import {CustomTableNode} from "./custom-table";
import {HorizontalRuleNode} from "./horizontal-rule";
import {CodeBlockNode} from "./code-block";
/**
* Load the nodes for lexical.
@ -26,6 +27,7 @@ export function getNodesForPageEditor(): (KlassConstructor<typeof LexicalNode> |
ImageNode,
HorizontalRuleNode,
DetailsNode, SummaryNode,
CodeBlockNode,
CustomParagraphNode,
LinkNode,
{

View File

@ -0,0 +1,42 @@
import {EditorDecorator} from "../framework/decorator";
import {el} from "../../helpers";
import {EditorUiContext} from "../framework/core";
import {CodeBlockNode} from "../../nodes/code-block";
export class CodeBlockDecorator extends EditorDecorator {
render(context: EditorUiContext, element: HTMLElement): void {
const codeNode = this.getNode() as CodeBlockNode;
const preEl = element.querySelector('pre');
if (preEl) {
preEl.hidden = true;
}
const code = codeNode.__code;
const language = codeNode.__language;
const lines = code.split('\n').length;
const height = (lines * 19.2) + 18 + 24;
element.style.height = `${height}px`;
let editor = null;
const startTime = Date.now();
// Todo - Handling click/edit control
// Todo - Add toolbar button for code
// @ts-ignore
const renderEditor = (Code) => {
editor = Code.wysiwygView(element, document, code, language);
setTimeout(() => {
element.style.height = '';
}, 12);
};
// @ts-ignore
window.importVersioned('code').then((Code) => {
const timeout = (Date.now() - startTime < 20) ? 20 : 0;
setTimeout(() => renderEditor(Code), timeout);
});
}
}

View File

@ -27,6 +27,11 @@ export abstract class EditorDecorator {
this.node = node;
}
abstract render(context: EditorUiContext): HTMLElement;
/**
* Render the decorator.
* If an element is returned, this will be appended to the element
* that is being decorated.
*/
abstract render(context: EditorUiContext, decorated: HTMLElement): HTMLElement|void;
}

View File

@ -160,11 +160,15 @@ export class EditorUIManager {
const keys = Object.keys(decorators);
for (const key of keys) {
const decoratedEl = editor.getElementByKey(key);
if (!decoratedEl) {
continue;
}
const adapter = decorators[key];
const decorator = this.getDecorator(adapter.type, key);
decorator.setNode(adapter.getNode());
const decoratorEl = decorator.render(this.getContext());
if (decoratedEl) {
const decoratorEl = decorator.render(this.getContext(), decoratedEl);
if (decoratorEl) {
decoratedEl.append(decoratorEl);
}
}

View File

@ -4,6 +4,7 @@ import {EditorUIManager} from "./framework/manager";
import {image as imageFormDefinition, link as linkFormDefinition, source as sourceFormDefinition} from "./defaults/form-definitions";
import {ImageDecorator} from "./decorators/image";
import {EditorUiContext} from "./framework/core";
import {CodeBlockDecorator} from "./decorators/code-block";
export function buildEditorUI(container: HTMLElement, element: HTMLElement, editor: LexicalEditor) {
const manager = new EditorUIManager();
@ -49,4 +50,5 @@ export function buildEditorUI(container: HTMLElement, element: HTMLElement, edit
// Register image decorator listener
manager.registerDecoratorType('image', ImageDecorator);
manager.registerDecoratorType('code', CodeBlockDecorator);
}