diff --git a/resources/js/wysiwyg/nodes/code-block.ts b/resources/js/wysiwyg/nodes/code-block.ts new file mode 100644 index 000000000..7184334a0 --- /dev/null +++ b/resources/js/wysiwyg/nodes/code-block.ts @@ -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 { + __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 diff --git a/resources/js/wysiwyg/nodes/index.ts b/resources/js/wysiwyg/nodes/index.ts index befc2ab2e..4cc6bd08b 100644 --- a/resources/js/wysiwyg/nodes/index.ts +++ b/resources/js/wysiwyg/nodes/index.ts @@ -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 | ImageNode, HorizontalRuleNode, DetailsNode, SummaryNode, + CodeBlockNode, CustomParagraphNode, LinkNode, { diff --git a/resources/js/wysiwyg/ui/decorators/code-block.ts b/resources/js/wysiwyg/ui/decorators/code-block.ts new file mode 100644 index 000000000..f1fd8c199 --- /dev/null +++ b/resources/js/wysiwyg/ui/decorators/code-block.ts @@ -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); + }); + } +} \ No newline at end of file diff --git a/resources/js/wysiwyg/ui/framework/decorator.ts b/resources/js/wysiwyg/ui/framework/decorator.ts index 890774126..b0d2392fd 100644 --- a/resources/js/wysiwyg/ui/framework/decorator.ts +++ b/resources/js/wysiwyg/ui/framework/decorator.ts @@ -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; } \ No newline at end of file diff --git a/resources/js/wysiwyg/ui/framework/manager.ts b/resources/js/wysiwyg/ui/framework/manager.ts index 3c2ad8926..a75d24786 100644 --- a/resources/js/wysiwyg/ui/framework/manager.ts +++ b/resources/js/wysiwyg/ui/framework/manager.ts @@ -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); } } diff --git a/resources/js/wysiwyg/ui/index.ts b/resources/js/wysiwyg/ui/index.ts index 3501ed557..1ad1395dc 100644 --- a/resources/js/wysiwyg/ui/index.ts +++ b/resources/js/wysiwyg/ui/index.ts @@ -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); } \ No newline at end of file