5 DOMConversionOutput, DOMExportOutput,
6 LexicalEditor, LexicalNode,
10 import type {EditorConfig} from "lexical/LexicalEditor";
11 import {EditorDecoratorAdapter} from "../../ui/framework/decorator";
12 import {CodeEditor} from "../../../components";
13 import {el} from "../../utils/dom";
15 export type SerializedCodeBlockNode = Spread<{
19 }, SerializedLexicalNode>
21 const getLanguageFromClassList = (classes: string) => {
22 const langClasses = classes.split(' ').filter(cssClass => cssClass.startsWith('language-'));
23 return (langClasses[0] || '').replace('language-', '');
26 export class CodeBlockNode extends DecoratorNode<EditorDecoratorAdapter> {
28 __language: string = '';
31 static getType(): string {
35 static clone(node: CodeBlockNode): CodeBlockNode {
36 const newNode = new CodeBlockNode(node.__language, node.__code, node.__key);
37 newNode.__id = node.__id;
41 constructor(language: string = '', code: string = '', key?: string) {
43 this.__language = language;
47 setLanguage(language: string): void {
48 const self = this.getWritable();
49 self.__language = language;
52 getLanguage(): string {
53 const self = this.getLatest();
54 return self.__language;
57 setCode(code: string): void {
58 const self = this.getWritable();
63 const self = this.getLatest();
68 const self = this.getWritable();
73 const self = this.getLatest();
77 decorate(editor: LexicalEditor, config: EditorConfig): EditorDecoratorAdapter {
92 createDOM(_config: EditorConfig, _editor: LexicalEditor) {
93 const codeBlock = el('pre', {
94 id: this.__id || null,
97 class: this.__language ? `language-${this.__language}` : null,
101 return el('div', {class: 'editor-code-block-wrap'}, [codeBlock]);
104 updateDOM(prevNode: CodeBlockNode, dom: HTMLElement) {
105 const code = dom.querySelector('code');
106 if (!code) return false;
108 if (prevNode.__language !== this.__language) {
109 code.className = this.__language ? `language-${this.__language}` : '';
112 if (prevNode.__id !== this.__id) {
113 dom.setAttribute('id', this.__id);
116 if (prevNode.__code !== this.__code) {
117 code.textContent = this.__code;
123 exportDOM(editor: LexicalEditor): DOMExportOutput {
124 const dom = this.createDOM(editor._config, editor);
126 element: dom.querySelector('pre') as HTMLElement,
130 static importDOM(): DOMConversionMap|null {
132 pre(node: HTMLElement): DOMConversion|null {
134 conversion: (element: HTMLElement): DOMConversionOutput|null => {
136 const codeEl = element.querySelector('code');
137 const language = getLanguageFromClassList(element.className)
138 || (codeEl && getLanguageFromClassList(codeEl.className))
141 const code = codeEl ? (codeEl.textContent || '').trim() : (element.textContent || '').trim();
142 const node = $createCodeBlockNode(language, code);
145 node.setId(element.id);
150 after(childNodes): LexicalNode[] {
151 // Remove any child nodes that may get parsed since we're manually
152 // controlling the code contents.
163 exportJSON(): SerializedCodeBlockNode {
168 language: this.__language,
173 static importJSON(serializedNode: SerializedCodeBlockNode): CodeBlockNode {
174 const node = $createCodeBlockNode(serializedNode.language, serializedNode.code);
175 node.setId(serializedNode.id || '');
180 export function $createCodeBlockNode(language: string = '', code: string = ''): CodeBlockNode {
181 return new CodeBlockNode(language, code);
184 export function $isCodeBlockNode(node: LexicalNode | null | undefined) {
185 return node instanceof CodeBlockNode;
188 export function $openCodeEditorForNode(editor: LexicalEditor, node: CodeBlockNode): void {
189 const code = node.getCode();
190 const language = node.getLanguage();
193 const codeEditor = window.$components.first('code-editor') as CodeEditor;
194 // TODO - Handle direction
195 codeEditor.open(code, language, 'ltr', (newCode: string, newLang: string) => {
196 editor.update(() => {
197 node.setCode(newCode);
198 node.setLanguage(newLang);