diff --git a/resources/js/wysiwyg/nodes/image.ts b/resources/js/wysiwyg/nodes/image.ts index 1e2cbd83c..289e36c4b 100644 --- a/resources/js/wysiwyg/nodes/image.ts +++ b/resources/js/wysiwyg/nodes/image.ts @@ -9,6 +9,7 @@ import { } from "lexical"; import type {EditorConfig} from "lexical/LexicalEditor"; import {el} from "../helpers"; +import {EditorDecoratorAdapter} from "../ui/framework/decorator"; export interface ImageNodeOptions { alt?: string; @@ -23,7 +24,7 @@ export type SerializedImageNode = Spread<{ height: number; }, SerializedLexicalNode> -export class ImageNode extends DecoratorNode<HTMLElement> { +export class ImageNode extends DecoratorNode<EditorDecoratorAdapter> { __src: string = ''; __alt: string = ''; __width: number = 0; @@ -79,6 +80,7 @@ export class ImageNode extends DecoratorNode<HTMLElement> { setWidth(width: number): void { const self = this.getWritable(); self.__width = width; + console.log('widrg', width) } getWidth(): number { @@ -90,17 +92,16 @@ export class ImageNode extends DecoratorNode<HTMLElement> { return true; } - decorate(editor: LexicalEditor, config: EditorConfig): HTMLElement { - console.log('decorate!'); - return el('div', { - class: 'editor-image-decorator', - }, ['decoration!!!']); + decorate(editor: LexicalEditor, config: EditorConfig): EditorDecoratorAdapter { + return { + type: 'image', + getNode: () => this, + }; } 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)); @@ -116,9 +117,38 @@ export class ImageNode extends DecoratorNode<HTMLElement> { ]); } - 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. + updateDOM(prevNode: ImageNode, dom: HTMLElement) { + const image = dom.querySelector('img'); + if (!image) return false; + + if (prevNode.__src !== this.__src) { + image.setAttribute('src', this.__src); + } + + if (prevNode.__width !== this.__width) { + if (this.__width) { + image.setAttribute('width', String(this.__width)); + } else { + image.removeAttribute('width'); + } + } + + if (prevNode.__height !== this.__height) { + if (this.__height) { + image.setAttribute('height', String(this.__height)); + } else { + image.removeAttribute('height'); + } + } + + if (prevNode.__alt !== this.__alt) { + if (this.__alt) { + image.setAttribute('alt', String(this.__alt)); + } else { + image.removeAttribute('alt'); + } + } + return false; } diff --git a/resources/js/wysiwyg/ui/decorators/image.ts b/resources/js/wysiwyg/ui/decorators/image.ts new file mode 100644 index 000000000..fd333fa54 --- /dev/null +++ b/resources/js/wysiwyg/ui/decorators/image.ts @@ -0,0 +1,91 @@ +import {EditorDecorator} from "../framework/decorator"; +import {el} from "../../helpers"; +import {$createNodeSelection, $setSelection} from "lexical"; +import {EditorUiContext} from "../framework/core"; +import {ImageNode} from "../../nodes/image"; + + +export class ImageDecorator extends EditorDecorator { + protected dom: HTMLElement|null = null; + + buildDOM(context: EditorUiContext) { + const handleClasses = ['nw', 'ne', 'se', 'sw']; + const handleEls = handleClasses.map(c => { + return el('div', {class: `editor-image-decorator-handle ${c}`}); + }); + + const decorateEl = el('div', { + class: 'editor-image-decorator', + }, handleEls); + + const windowClick = (event: MouseEvent) => { + if (!decorateEl.contains(event.target as Node)) { + unselect(); + } + }; + + const select = () => { + decorateEl.classList.add('selected'); + window.addEventListener('click', windowClick); + }; + + const unselect = () => { + decorateEl.classList.remove('selected'); + window.removeEventListener('click', windowClick); + }; + + decorateEl.addEventListener('click', (event) => { + context.editor.update(() => { + const nodeSelection = $createNodeSelection(); + nodeSelection.add(this.getNode().getKey()); + $setSelection(nodeSelection); + }); + + select(); + }); + + decorateEl.addEventListener('mousedown', (event: MouseEvent) => { + const handle = (event.target as Element).closest('.editor-image-decorator-handle'); + if (handle) { + this.startHandlingResize(handle, event, context); + } + }); + + return decorateEl; + } + + render(context: EditorUiContext): HTMLElement { + if (this.dom) { + return this.dom; + } + + this.dom = this.buildDOM(context); + return this.dom; + } + + startHandlingResize(element: Node, event: MouseEvent, context: EditorUiContext) { + const startingX = event.screenX; + const startingY = event.screenY; + + const mouseMoveListener = (event: MouseEvent) => { + const xChange = event.screenX - startingX; + const yChange = event.screenY - startingY; + console.log({ xChange, yChange }); + + context.editor.update(() => { + const node = this.getNode() as ImageNode; + node.setWidth(node.getWidth() + xChange); + node.setHeight(node.getHeight() + yChange); + }); + }; + + const mouseUpListener = (event: MouseEvent) => { + window.removeEventListener('mousemove', mouseMoveListener); + window.removeEventListener('mouseup', mouseUpListener); + } + + window.addEventListener('mousemove', mouseMoveListener); + window.addEventListener('mouseup', mouseUpListener); + } + +} \ No newline at end of file diff --git a/resources/js/wysiwyg/ui/framework/decorator.ts b/resources/js/wysiwyg/ui/framework/decorator.ts new file mode 100644 index 000000000..890774126 --- /dev/null +++ b/resources/js/wysiwyg/ui/framework/decorator.ts @@ -0,0 +1,32 @@ +import {EditorUiContext} from "./core"; +import {LexicalNode} from "lexical"; + +export interface EditorDecoratorAdapter { + type: string; + getNode(): LexicalNode; +} + +export abstract class EditorDecorator { + + protected node: LexicalNode | null = null; + protected context: EditorUiContext; + + constructor(context: EditorUiContext) { + this.context = context; + } + + protected getNode(): LexicalNode { + if (!this.node) { + throw new Error('Attempted to get use node without it being set'); + } + + return this.node; + } + + setNode(node: LexicalNode) { + this.node = node; + } + + abstract render(context: EditorUiContext): HTMLElement; + +} \ 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 c3fe9ecd8..1684b6628 100644 --- a/resources/js/wysiwyg/ui/framework/manager.ts +++ b/resources/js/wysiwyg/ui/framework/manager.ts @@ -1,10 +1,13 @@ import {EditorFormModal, EditorFormModalDefinition} from "./modals"; import {EditorUiContext} from "./core"; +import {EditorDecorator} from "./decorator"; export class EditorUIManager { protected modalDefinitionsByKey: Record<string, EditorFormModalDefinition> = {}; + protected decoratorConstructorsByType: Record<string, typeof EditorDecorator> = {}; + protected decoratorInstancesByNodeKey: Record<string, EditorDecorator> = {}; protected context: EditorUiContext|null = null; setContext(context: EditorUiContext) { @@ -26,7 +29,7 @@ export class EditorUIManager { createModal(key: string): EditorFormModal { const modalDefinition = this.modalDefinitionsByKey[key]; if (!modalDefinition) { - console.error(`Attempted to show modal of key [${key}] but no modal registered for that key`); + throw new Error(`Attempted to show modal of key [${key}] but no modal registered for that key`); } const modal = new EditorFormModal(modalDefinition); @@ -35,4 +38,23 @@ export class EditorUIManager { return modal; } + registerDecoratorType(type: string, decorator: typeof EditorDecorator) { + this.decoratorConstructorsByType[type] = decorator; + } + + getDecorator(decoratorType: string, nodeKey: string): EditorDecorator { + if (this.decoratorInstancesByNodeKey[nodeKey]) { + return this.decoratorInstancesByNodeKey[nodeKey]; + } + + const decoratorClass = this.decoratorConstructorsByType[decoratorType]; + if (!decoratorClass) { + throw new Error(`Attempted to use decorator of type [${decoratorType}] but not decorator registered for that type`); + } + + // @ts-ignore + const decorator = new decoratorClass(nodeKey); + this.decoratorInstancesByNodeKey[nodeKey] = decorator; + return decorator; + } } \ No newline at end of file diff --git a/resources/js/wysiwyg/ui/index.ts b/resources/js/wysiwyg/ui/index.ts index 9206f8b40..19320b262 100644 --- a/resources/js/wysiwyg/ui/index.ts +++ b/resources/js/wysiwyg/ui/index.ts @@ -9,7 +9,8 @@ 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"; +import {EditorDecoratorAdapter} from "./framework/decorator"; +import {ImageDecorator} from "./decorators/image"; export function buildEditorUI(element: HTMLElement, editor: LexicalEditor) { const manager = new EditorUIManager(); @@ -33,11 +34,15 @@ export function buildEditorUI(element: HTMLElement, editor: LexicalEditor) { // Register decorator listener // Maybe move to manager? - const domDecorateListener: DecoratorListener<HTMLElement> = (decorator: Record<NodeKey, HTMLElement>) => { - const keys = Object.keys(decorator); + manager.registerDecoratorType('image', ImageDecorator); + const domDecorateListener: DecoratorListener<EditorDecoratorAdapter> = (decorators: Record<NodeKey, EditorDecoratorAdapter>) => { + const keys = Object.keys(decorators); for (const key of keys) { const decoratedEl = editor.getElementByKey(key); - const decoratorEl = decorator[key]; + const adapter = decorators[key]; + const decorator = manager.getDecorator(adapter.type, key); + decorator.setNode(adapter.getNode()); + const decoratorEl = decorator.render(context); if (decoratedEl) { decoratedEl.append(decoratorEl); } diff --git a/resources/sass/_editor.scss b/resources/sass/_editor.scss index 2633e8539..94fe2c756 100644 --- a/resources/sass/_editor.scss +++ b/resources/sass/_editor.scss @@ -1,3 +1,8 @@ +// Common variables +:root { + --editor-color-primary: #206ea7; +} + // Main UI elements .editor-toolbar-main { display: flex; @@ -72,4 +77,47 @@ } .editor-modal-title { font-weight: 700; -} \ No newline at end of file +} + +// In-editor elements +.editor-image-wrap { + position: relative; + display: inline-flex; +} +.editor-image-decorator { + display: inline-block; + position: absolute; + border: 1px solid var(--editor-color-primary); + left: 0; + right: 0; + width: 100%; + height: 100%; +} +.editor-image-decorator-handle { + position: absolute; + display: block; + width: 10px; + height: 10px; + background-color: var(--editor-color-primary); + user-select: none; + &.nw { + inset-inline-start: -5px; + inset-block-start: -5px; + cursor: nw-resize; + } + &.ne { + inset-inline-end: -5px; + inset-block-start: -5px; + cursor: ne-resize; + } + &.se { + inset-inline-end: -5px; + inset-block-end: -5px; + cursor: se-resize; + } + &.sw { + inset-inline-start: -5px; + inset-block-end: -5px; + cursor: sw-resize; + } +}