diff --git a/resources/js/global.d.ts b/resources/js/global.d.ts index c5aba8ee2..537da6368 100644 --- a/resources/js/global.d.ts +++ b/resources/js/global.d.ts @@ -1,4 +1,12 @@ declare module '*.svg' { const content: string; export default content; +} + +declare global { + interface Window { + $components: { + first: (string) => Object, + } + } } \ No newline at end of file diff --git a/resources/js/wysiwyg/nodes/code-block.ts b/resources/js/wysiwyg/nodes/code-block.ts index 7184334a0..934fe7edd 100644 --- a/resources/js/wysiwyg/nodes/code-block.ts +++ b/resources/js/wysiwyg/nodes/code-block.ts @@ -73,7 +73,6 @@ export class CodeBlockNode extends DecoratorNode { } decorate(editor: LexicalEditor, config: EditorConfig): EditorDecoratorAdapter { - // TODO return { type: 'code', getNode: () => this, @@ -165,4 +164,22 @@ export function $createCodeBlockNode(language: string = '', code: string = ''): export function $isCodeBlockNode(node: LexicalNode | null | undefined) { return node instanceof CodeBlockNode; +} + +export function $openCodeEditorForNode(editor: LexicalEditor, node: CodeBlockNode): void { + const code = node.getCode(); + const language = node.getLanguage(); + + // @ts-ignore + const codeEditor = window.$components.first('code-editor'); + // TODO - Handle direction + codeEditor.open(code, language, 'ltr', (newCode: string, newLang: string) => { + editor.update(() => { + node.setCode(newCode); + node.setLanguage(newLang); + }); + // TODO - Re-focus + }, () => { + // TODO - Re-focus + }); } \ No newline at end of file diff --git a/resources/js/wysiwyg/ui/decorators/code-block.ts b/resources/js/wysiwyg/ui/decorators/code-block.ts index f1fd8c199..80dcef3bd 100644 --- a/resources/js/wysiwyg/ui/decorators/code-block.ts +++ b/resources/js/wysiwyg/ui/decorators/code-block.ts @@ -1,33 +1,46 @@ import {EditorDecorator} from "../framework/decorator"; -import {el} from "../../helpers"; import {EditorUiContext} from "../framework/core"; -import {CodeBlockNode} from "../../nodes/code-block"; +import {$openCodeEditorForNode, CodeBlockNode} from "../../nodes/code-block"; +import {ImageNode} from "../../nodes/image"; export class CodeBlockDecorator extends EditorDecorator { - render(context: EditorUiContext, element: HTMLElement): void { + protected completedSetup: boolean = false; + protected latestCode: string = ''; + protected latestLanguage: string = ''; + + // @ts-ignore + protected editor: any = null; + + setup(context: EditorUiContext, element: HTMLElement) { const codeNode = this.getNode() as CodeBlockNode; const preEl = element.querySelector('pre'); + if (!preEl) { + return; + } + if (preEl) { preEl.hidden = true; } - const code = codeNode.__code; - const language = codeNode.__language; - const lines = code.split('\n').length; + this.latestCode = codeNode.__code; + this.latestLanguage = codeNode.__language; + const lines = this.latestCode.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 + element.addEventListener('dblclick', event => { + context.editor.getEditorState().read(() => { + $openCodeEditorForNode(context.editor, (this.getNode() as CodeBlockNode)); + }); + }); // @ts-ignore const renderEditor = (Code) => { - editor = Code.wysiwygView(element, document, code, language); + this.editor = Code.wysiwygView(element, document, this.latestCode, this.latestLanguage); setTimeout(() => { element.style.height = ''; }, 12); @@ -38,5 +51,32 @@ export class CodeBlockDecorator extends EditorDecorator { const timeout = (Date.now() - startTime < 20) ? 20 : 0; setTimeout(() => renderEditor(Code), timeout); }); + + this.completedSetup = true; + } + + update() { + const codeNode = this.getNode() as CodeBlockNode; + const code = codeNode.getCode(); + const language = codeNode.getLanguage(); + + if (this.latestCode === code && this.latestLanguage === language) { + return; + } + this.latestLanguage = language; + this.latestCode = code; + + if (this.editor) { + this.editor.setContent(code); + this.editor.setMode(language, code); + } + } + + render(context: EditorUiContext, element: HTMLElement): void { + if (this.completedSetup) { + this.update(); + } else { + this.setup(context, element); + } } } \ No newline at end of file diff --git a/resources/js/wysiwyg/ui/defaults/button-definitions.ts b/resources/js/wysiwyg/ui/defaults/button-definitions.ts index 4a45ef75d..9f83fbea3 100644 --- a/resources/js/wysiwyg/ui/defaults/button-definitions.ts +++ b/resources/js/wysiwyg/ui/defaults/button-definitions.ts @@ -49,10 +49,12 @@ import unlinkIcon from "@icons/editor/unlink.svg" import tableIcon from "@icons/editor/table.svg" import imageIcon from "@icons/editor/image.svg" import horizontalRuleIcon from "@icons/editor/horizontal-rule.svg" +import codeBlockIcon from "@icons/editor/code-block.svg" import detailsIcon from "@icons/editor/details.svg" import sourceIcon from "@icons/editor/source-view.svg" import fullscreenIcon from "@icons/editor/fullscreen.svg" import {$createHorizontalRuleNode, $isHorizontalRuleNode} from "../../nodes/horizontal-rule"; +import {$createCodeBlockNode, $isCodeBlockNode, $openCodeEditorForNode, CodeBlockNode} from "../../nodes/code-block"; export const undo: EditorButtonDefinition = { label: 'Undo', @@ -336,6 +338,31 @@ export const horizontalRule: EditorButtonDefinition = { } }; +export const codeBlock: EditorButtonDefinition = { + label: 'Insert code block', + icon: codeBlockIcon, + action(context: EditorUiContext) { + context.editor.getEditorState().read(() => { + const selection = $getSelection(); + const codeBlock = getNodeFromSelection(selection, $isCodeBlockNode) as (CodeBlockNode|null); + if (codeBlock === null) { + context.editor.update(() => { + const codeBlock = $createCodeBlockNode(); + codeBlock.setCode(selection?.getTextContent() || ''); + insertNewBlockNodeAtSelection(codeBlock, true); + $openCodeEditorForNode(context.editor, codeBlock); + codeBlock.selectStart(); + }); + } else { + $openCodeEditorForNode(context.editor, codeBlock); + } + }); + }, + isActive(selection: BaseSelection|null): boolean { + return selectionContainsNodeType(selection, $isCodeBlockNode); + } +}; + export const details: EditorButtonDefinition = { label: 'Insert collapsible block', icon: detailsIcon, diff --git a/resources/js/wysiwyg/ui/framework/decorator.ts b/resources/js/wysiwyg/ui/framework/decorator.ts index b0d2392fd..a9917ab23 100644 --- a/resources/js/wysiwyg/ui/framework/decorator.ts +++ b/resources/js/wysiwyg/ui/framework/decorator.ts @@ -29,6 +29,7 @@ export abstract class EditorDecorator { /** * Render the decorator. + * Can run on both creation and update for a node decorator. * If an element is returned, this will be appended to the element * that is being decorated. */ diff --git a/resources/js/wysiwyg/ui/framework/manager.ts b/resources/js/wysiwyg/ui/framework/manager.ts index a75d24786..6477c4a1a 100644 --- a/resources/js/wysiwyg/ui/framework/manager.ts +++ b/resources/js/wysiwyg/ui/framework/manager.ts @@ -157,21 +157,23 @@ export class EditorUIManager { // Register our DOM decorate listener with the editor const domDecorateListener: DecoratorListener = (decorators: Record) => { - const keys = Object.keys(decorators); - for (const key of keys) { - const decoratedEl = editor.getElementByKey(key); - if (!decoratedEl) { - continue; - } + editor.getEditorState().read(() => { + 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(), decoratedEl); - if (decoratorEl) { - decoratedEl.append(decoratorEl); + const adapter = decorators[key]; + const decorator = this.getDecorator(adapter.type, key); + decorator.setNode(adapter.getNode()); + const decoratorEl = decorator.render(this.getContext(), decoratedEl); + if (decoratorEl) { + decoratedEl.append(decoratorEl); + } } - } + }); } editor.registerDecoratorListener(domDecorateListener); } diff --git a/resources/js/wysiwyg/ui/toolbars.ts b/resources/js/wysiwyg/ui/toolbars.ts index 550c798c2..18b811380 100644 --- a/resources/js/wysiwyg/ui/toolbars.ts +++ b/resources/js/wysiwyg/ui/toolbars.ts @@ -1,6 +1,6 @@ import {EditorButton} from "./framework/buttons"; import { - blockquote, bold, bulletList, clearFormating, code, + blockquote, bold, bulletList, clearFormating, code, codeBlock, dangerCallout, details, fullscreen, h2, h3, h4, h5, highlightColor, horizontalRule, image, infoCallout, italic, link, numberList, paragraph, @@ -68,6 +68,7 @@ export function getMainEditorFullToolbar(): EditorContainerUiElement { ]), new EditorButton(image), new EditorButton(horizontalRule), + new EditorButton(codeBlock), new EditorButton(details), ]), diff --git a/resources/sass/_editor.scss b/resources/sass/_editor.scss index 753038263..5305ada82 100644 --- a/resources/sass/_editor.scss +++ b/resources/sass/_editor.scss @@ -244,6 +244,13 @@ body.editor-is-fullscreen { cursor: row-resize; } +.editor-code-block-wrap { + user-select: none; + > * { + pointer-events: none; + } +} + // Editor theme styles .editor-theme-bold { font-weight: bold;