From feca1f0502177dc3d9911101000244ed6a25396d Mon Sep 17 00:00:00 2001 From: Dan Brown Date: Wed, 3 Jul 2024 10:28:04 +0100 Subject: [PATCH] Lexical: Started diagram support --- resources/js/wysiwyg/nodes/code-block.ts | 1 - resources/js/wysiwyg/nodes/diagram.ts | 158 ++++++++++++++++++ resources/js/wysiwyg/nodes/index.ts | 2 + resources/js/wysiwyg/ui/decorators/diagram.ts | 25 +++ resources/js/wysiwyg/ui/index.ts | 2 + tsconfig.json | 2 +- 6 files changed, 188 insertions(+), 2 deletions(-) create mode 100644 resources/js/wysiwyg/nodes/diagram.ts create mode 100644 resources/js/wysiwyg/ui/decorators/diagram.ts diff --git a/resources/js/wysiwyg/nodes/code-block.ts b/resources/js/wysiwyg/nodes/code-block.ts index 934fe7edd..f839501db 100644 --- a/resources/js/wysiwyg/nodes/code-block.ts +++ b/resources/js/wysiwyg/nodes/code-block.ts @@ -10,7 +10,6 @@ import { 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; diff --git a/resources/js/wysiwyg/nodes/diagram.ts b/resources/js/wysiwyg/nodes/diagram.ts new file mode 100644 index 000000000..15726813c --- /dev/null +++ b/resources/js/wysiwyg/nodes/diagram.ts @@ -0,0 +1,158 @@ +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"; + +export type SerializedDiagramNode = Spread<{ + id: string; + drawingId: string; + drawingUrl: string; +}, SerializedLexicalNode> + +export class DiagramNode extends DecoratorNode { + __id: string = ''; + __drawingId: string = ''; + __drawingUrl: string = ''; + + static getType(): string { + return 'diagram'; + } + + static clone(node: DiagramNode): DiagramNode { + return new DiagramNode(node.__drawingId, node.__drawingUrl); + } + + constructor(drawingId: string, drawingUrl: string, key?: string) { + super(key); + this.__drawingId = drawingId; + this.__drawingUrl = drawingUrl; + } + + setDrawingIdAndUrl(drawingId: string, drawingUrl: string): void { + const self = this.getWritable(); + self.__drawingUrl = drawingUrl; + self.__drawingId = drawingId; + } + + getDrawingIdAndUrl(): {id: string, url: string} { + const self = this.getLatest(); + return { + id: self.__drawingUrl, + url: self.__drawingUrl, + }; + } + + 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 { + return { + type: 'diagram', + getNode: () => this, + }; + } + + isInline(): boolean { + return false; + } + + isIsolated() { + return true; + } + + createDOM(_config: EditorConfig, _editor: LexicalEditor) { + return el('div', { + id: this.__id || null, + 'drawio-diagram': this.__drawingId, + }, [ + el('img', {src: this.__drawingUrl}), + ]); + } + + updateDOM(prevNode: DiagramNode, dom: HTMLElement) { + const img = dom.querySelector('img'); + if (!img) return false; + + if (prevNode.__id !== this.__id) { + dom.setAttribute('id', this.__id); + } + + if (prevNode.__drawingUrl !== this.__drawingUrl) { + img.setAttribute('src', this.__drawingUrl); + } + + if (prevNode.__drawingId !== this.__drawingId) { + dom.setAttribute('drawio-diagram', this.__drawingId); + } + + return false; + } + + static importDOM(): DOMConversionMap|null { + return { + div(node: HTMLElement): DOMConversion|null { + + if (!node.hasAttribute('drawio-diagram')) { + return null; + } + + return { + conversion: (element: HTMLElement): DOMConversionOutput|null => { + + const img = element.querySelector('img'); + const drawingUrl = img?.getAttribute('src') || ''; + const drawingId = element.getAttribute('drawio-diagram') || ''; + + return { + node: $createDiagramNode(drawingId, drawingUrl), + }; + }, + priority: 3, + }; + }, + }; + } + + exportJSON(): SerializedDiagramNode { + return { + type: 'diagram', + version: 1, + id: this.__id, + drawingId: this.__drawingId, + drawingUrl: this.__drawingUrl, + }; + } + + static importJSON(serializedNode: SerializedDiagramNode): DiagramNode { + const node = $createDiagramNode(serializedNode.drawingId, serializedNode.drawingUrl); + node.setId(serializedNode.id || ''); + return node; + } +} + +export function $createDiagramNode(drawingId: string = '', drawingUrl: string = ''): DiagramNode { + return new DiagramNode(drawingId, drawingUrl); +} + +export function $isDiagramNode(node: LexicalNode | null | undefined) { + return node instanceof DiagramNode; +} + +export function $openDrawingEditorForNode(editor: LexicalEditor, node: DiagramNode): void { + // Todo +} \ No newline at end of file diff --git a/resources/js/wysiwyg/nodes/index.ts b/resources/js/wysiwyg/nodes/index.ts index 4cc6bd08b..e2c6902d3 100644 --- a/resources/js/wysiwyg/nodes/index.ts +++ b/resources/js/wysiwyg/nodes/index.ts @@ -10,6 +10,7 @@ import {TableCellNode, TableNode, TableRowNode} from "@lexical/table"; import {CustomTableNode} from "./custom-table"; import {HorizontalRuleNode} from "./horizontal-rule"; import {CodeBlockNode} from "./code-block"; +import {DiagramNode} from "./diagram"; /** * Load the nodes for lexical. @@ -28,6 +29,7 @@ export function getNodesForPageEditor(): (KlassConstructor | HorizontalRuleNode, DetailsNode, SummaryNode, CodeBlockNode, + DiagramNode, CustomParagraphNode, LinkNode, { diff --git a/resources/js/wysiwyg/ui/decorators/diagram.ts b/resources/js/wysiwyg/ui/decorators/diagram.ts new file mode 100644 index 000000000..2f092bd20 --- /dev/null +++ b/resources/js/wysiwyg/ui/decorators/diagram.ts @@ -0,0 +1,25 @@ +import {EditorDecorator} from "../framework/decorator"; +import {EditorUiContext} from "../framework/core"; + + +export class DiagramDecorator extends EditorDecorator { + protected completedSetup: boolean = false; + + setup(context: EditorUiContext, element: HTMLElement) { + // + + this.completedSetup = true; + } + + update() { + // + } + + 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/index.ts b/resources/js/wysiwyg/ui/index.ts index 1ad1395dc..50307fa61 100644 --- a/resources/js/wysiwyg/ui/index.ts +++ b/resources/js/wysiwyg/ui/index.ts @@ -5,6 +5,7 @@ import {image as imageFormDefinition, link as linkFormDefinition, source as sour import {ImageDecorator} from "./decorators/image"; import {EditorUiContext} from "./framework/core"; import {CodeBlockDecorator} from "./decorators/code-block"; +import {DiagramDecorator} from "./decorators/diagram"; export function buildEditorUI(container: HTMLElement, element: HTMLElement, editor: LexicalEditor) { const manager = new EditorUIManager(); @@ -51,4 +52,5 @@ export function buildEditorUI(container: HTMLElement, element: HTMLElement, edit // Register image decorator listener manager.registerDecoratorType('image', ImageDecorator); manager.registerDecoratorType('code', CodeBlockDecorator); + manager.registerDecoratorType('diagram', DiagramDecorator); } \ No newline at end of file diff --git a/tsconfig.json b/tsconfig.json index 40d930149..9913c1235 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -46,7 +46,7 @@ // "noResolve": true, /* Disallow 'import's, 'require's or ''s from expanding the number of files TypeScript should add to a project. */ /* JavaScript Support */ - // "allowJs": true, /* Allow JavaScript files to be a part of your program. Use the 'checkJS' option to get errors from these files. */ + "allowJs": true, /* Allow JavaScript files to be a part of your program. Use the 'checkJS' option to get errors from these files. */ // "checkJs": true, /* Enable error reporting in type-checked JavaScript files. */ // "maxNodeModuleJsDepth": 1, /* Specify the maximum folder depth used for checking JavaScript files from 'node_modules'. Only applicable with 'allowJs'. */