diff --git a/resources/js/wysiwyg/nodes/image.ts b/resources/js/wysiwyg/nodes/image.ts new file mode 100644 index 000000000..1e2cbd83c --- /dev/null +++ b/resources/js/wysiwyg/nodes/image.ts @@ -0,0 +1,174 @@ +import { + DecoratorNode, + DOMConversion, + DOMConversionMap, + DOMConversionOutput, + LexicalEditor, LexicalNode, + SerializedLexicalNode, + Spread +} from "lexical"; +import type {EditorConfig} from "lexical/LexicalEditor"; +import {el} from "../helpers"; + +export interface ImageNodeOptions { + alt?: string; + width?: number; + height?: number; +} + +export type SerializedImageNode = Spread<{ + src: string; + alt: string; + width: number; + height: number; +}, SerializedLexicalNode> + +export class ImageNode extends DecoratorNode { + __src: string = ''; + __alt: string = ''; + __width: number = 0; + __height: number = 0; + // TODO - Alignment + + static getType(): string { + return 'image'; + } + + static clone(node: ImageNode): ImageNode { + return new ImageNode(node.__src, { + alt: node.__alt, + width: node.__width, + height: node.__height, + }); + } + + constructor(src: string, options: ImageNodeOptions, key?: string) { + super(key); + this.__src = src; + if (options.alt) { + this.__alt = options.alt; + } + if (options.width) { + this.__width = options.width; + } + if (options.height) { + this.__height = options.height; + } + } + + setAltText(altText: string): void { + const self = this.getWritable(); + self.__alt = altText; + } + + getAltText(): string { + const self = this.getLatest(); + return self.__alt; + } + + setHeight(height: number): void { + const self = this.getWritable(); + self.__height = height; + } + + getHeight(): number { + const self = this.getLatest(); + return self.__height; + } + + setWidth(width: number): void { + const self = this.getWritable(); + self.__width = width; + } + + getWidth(): number { + const self = this.getLatest(); + return self.__width; + } + + isInline(): boolean { + return true; + } + + decorate(editor: LexicalEditor, config: EditorConfig): HTMLElement { + console.log('decorate!'); + return el('div', { + class: 'editor-image-decorator', + }, ['decoration!!!']); + } + + createDOM(_config: EditorConfig, _editor: LexicalEditor) { + const element = document.createElement('img'); + element.setAttribute('src', this.__src); + element.textContent + + if (this.__width) { + element.setAttribute('width', String(this.__width)); + } + if (this.__height) { + element.setAttribute('height', String(this.__height)); + } + if (this.__alt) { + element.setAttribute('alt', this.__alt); + } + return el('span', {class: 'editor-image-wrap'}, [ + element, + ]); + } + + updateDOM(prevNode: unknown, dom: HTMLElement) { + // Returning false tells Lexical that this node does not need its + // DOM element replacing with a new copy from createDOM. + return false; + } + + static importDOM(): DOMConversionMap|null { + return { + img(node: HTMLElement): DOMConversion|null { + return { + conversion: (element: HTMLElement): DOMConversionOutput|null => { + + const src = element.getAttribute('src') || ''; + const options: ImageNodeOptions = { + alt: element.getAttribute('alt') || '', + height: Number.parseInt(element.getAttribute('height') || '0'), + width: Number.parseInt(element.getAttribute('width') || '0'), + } + + return { + node: new ImageNode(src, options), + }; + }, + priority: 3, + }; + }, + }; + } + + exportJSON(): SerializedImageNode { + return { + type: 'image', + version: 1, + src: this.__src, + alt: this.__alt, + height: this.__height, + width: this.__width + }; + } + + static importJSON(serializedNode: SerializedImageNode): ImageNode { + return $createImageNode(serializedNode.src, { + alt: serializedNode.alt, + width: serializedNode.width, + height: serializedNode.height, + }); + } +} + +export function $createImageNode(src: string, options: ImageNodeOptions = {}): ImageNode { + return new ImageNode(src, options); +} + +export function $isImageNode(node: LexicalNode | null | undefined) { + return node instanceof ImageNode; +} \ No newline at end of file diff --git a/resources/js/wysiwyg/nodes/index.ts b/resources/js/wysiwyg/nodes/index.ts index 9f772df1e..1d492a87a 100644 --- a/resources/js/wysiwyg/nodes/index.ts +++ b/resources/js/wysiwyg/nodes/index.ts @@ -3,6 +3,7 @@ import {CalloutNode} from './callout'; import {ElementNode, KlassConstructor, LexicalNode, LexicalNodeReplacement, ParagraphNode} from "lexical"; import {CustomParagraphNode} from "./custom-paragraph"; import {LinkNode} from "@lexical/link"; +import {ImageNode} from "./image"; /** * Load the nodes for lexical. @@ -12,6 +13,7 @@ export function getNodesForPageEditor(): (KlassConstructor | CalloutNode, // Todo - Create custom HeadingNode, // Todo - Create custom QuoteNode, // Todo - Create custom + ImageNode, CustomParagraphNode, { replace: ParagraphNode, diff --git a/resources/js/wysiwyg/ui/framework/buttons.ts b/resources/js/wysiwyg/ui/framework/buttons.ts index 48046e9de..367a39330 100644 --- a/resources/js/wysiwyg/ui/framework/buttons.ts +++ b/resources/js/wysiwyg/ui/framework/buttons.ts @@ -67,7 +67,6 @@ export class FormatPreviewButton extends EditorButton { }, [this.getLabel()]); const stylesToApply = this.getStylesFromPreview(); - console.log(stylesToApply); for (const style of Object.keys(stylesToApply)) { preview.style.setProperty(style, stylesToApply[style]); } diff --git a/resources/js/wysiwyg/ui/index.ts b/resources/js/wysiwyg/ui/index.ts index 7e1f8d981..9206f8b40 100644 --- a/resources/js/wysiwyg/ui/index.ts +++ b/resources/js/wysiwyg/ui/index.ts @@ -7,6 +7,9 @@ import { import {getMainEditorFullToolbar} from "./toolbars"; import {EditorUIManager} from "./framework/manager"; import {link as linkFormDefinition} from "./defaults/form-definitions"; +import {DecoratorListener} from "lexical/LexicalEditor"; +import type {NodeKey} from "lexical/LexicalNode"; +import {el} from "../helpers"; export function buildEditorUI(element: HTMLElement, editor: LexicalEditor) { const manager = new EditorUIManager(); @@ -28,6 +31,20 @@ export function buildEditorUI(element: HTMLElement, editor: LexicalEditor) { form: linkFormDefinition, }); + // Register decorator listener + // Maybe move to manager? + const domDecorateListener: DecoratorListener = (decorator: Record) => { + const keys = Object.keys(decorator); + for (const key of keys) { + const decoratedEl = editor.getElementByKey(key); + const decoratorEl = decorator[key]; + if (decoratedEl) { + decoratedEl.append(decoratorEl); + } + } + } + editor.registerDecoratorListener(domDecorateListener); + // Update button states on editor selection change editor.registerCommand(SELECTION_CHANGE_COMMAND, () => { const selection = $getSelection(); diff --git a/resources/views/pages/parts/wysiwyg-editor.blade.php b/resources/views/pages/parts/wysiwyg-editor.blade.php index 940c005b5..c0ceddc45 100644 --- a/resources/views/pages/parts/wysiwyg-editor.blade.php +++ b/resources/views/pages/parts/wysiwyg-editor.blade.php @@ -9,13 +9,14 @@

Some content here

+

Content with image in, before text. Sleepy meow After text.

This has a link in it

List below this h2 header

  • Hello
-

+

Hello there, this is an info callout