--- /dev/null
+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;
+}
\ No newline at end of file
--- /dev/null
+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);
+ });
+ }
+}
\ No newline at end of file