} from "lexical";
import type {EditorConfig} from "lexical/LexicalEditor";
import {el} from "../helpers";
+import {EditorDecoratorAdapter} from "../ui/framework/decorator";
export interface ImageNodeOptions {
alt?: string;
height: number;
}, SerializedLexicalNode>
-export class ImageNode extends DecoratorNode<HTMLElement> {
+export class ImageNode extends DecoratorNode<EditorDecoratorAdapter> {
__src: string = '';
__alt: string = '';
__width: number = 0;
setWidth(width: number): void {
const self = this.getWritable();
self.__width = width;
+ console.log('widrg', width)
}
getWidth(): number {
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));
]);
}
- 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;
}
--- /dev/null
+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
--- /dev/null
+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
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) {
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);
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
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();
// 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);
}
+// Common variables
+:root {
+ --editor-color-primary: #206ea7;
+}
+
// Main UI elements
.editor-toolbar-main {
display: flex;
}
.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;
+ }
+}