From 634b0aaa07097f4a413a85e7c172176dda8e42e1 Mon Sep 17 00:00:00 2001 From: Dan Brown Date: Thu, 18 Jul 2024 11:19:11 +0100 Subject: [PATCH] Lexical: Started converting drawio to TS Converted events service to TS as part of this. --- resources/js/app.js | 4 +- resources/js/global.d.ts | 2 + .../js/services/{drawio.js => drawio.ts} | 60 ++++++++------ resources/js/services/events.js | 81 ------------------- resources/js/services/events.ts | 71 ++++++++++++++++ resources/js/wysiwyg/todo.md | 8 +- .../js/wysiwyg/ui/decorators/code-block.ts | 1 - resources/js/wysiwyg/ui/decorators/diagram.ts | 25 +++++- tsconfig.json | 2 +- 9 files changed, 142 insertions(+), 112 deletions(-) rename resources/js/services/{drawio.js => drawio.ts} (69%) delete mode 100644 resources/js/services/events.js create mode 100644 resources/js/services/events.ts diff --git a/resources/js/app.js b/resources/js/app.js index 123d6c8f5..812a451f2 100644 --- a/resources/js/app.js +++ b/resources/js/app.js @@ -1,4 +1,4 @@ -import * as events from './services/events'; +import {EventManager} from './services/events.ts'; import * as httpInstance from './services/http'; import Translations from './services/translations'; import * as componentMap from './components'; @@ -21,7 +21,7 @@ window.importVersioned = function importVersioned(moduleName) { // Set events and http services on window window.$http = httpInstance; -window.$events = events; +window.$events = new EventManager(); // Translation setup // Creates a global function with name 'trans' to be used in the same way as the Laravel translation system diff --git a/resources/js/global.d.ts b/resources/js/global.d.ts index da19545d1..a9b9275e9 100644 --- a/resources/js/global.d.ts +++ b/resources/js/global.d.ts @@ -1,7 +1,9 @@ import {ComponentStore} from "./services/components"; +import {EventManager} from "./services/events"; declare global { interface Window { $components: ComponentStore, + $events: EventManager, } } \ No newline at end of file diff --git a/resources/js/services/drawio.js b/resources/js/services/drawio.ts similarity index 69% rename from resources/js/services/drawio.js rename to resources/js/services/drawio.ts index 46e10327a..75b161f75 100644 --- a/resources/js/services/drawio.js +++ b/resources/js/services/drawio.ts @@ -1,17 +1,31 @@ // Docs: https://www.diagrams.net/doc/faq/embed-mode import * as store from './store'; +import {ConfirmDialog} from "../components"; -let iFrame = null; -let lastApprovedOrigin; -let onInit; -let onSave; +type DrawioExportEventResponse = { + action: 'export', + format: string, + message: string, + data: string, + xml: string, +}; + +type DrawioSaveEventResponse = { + action: 'save', + xml: string, +}; + +let iFrame: HTMLIFrameElement|null = null; +let lastApprovedOrigin: string; +let onInit: () => Promise; +let onSave: (data: string) => Promise; const saveBackupKey = 'last-drawing-save'; -function drawPostMessage(data) { - iFrame.contentWindow.postMessage(JSON.stringify(data), lastApprovedOrigin); +function drawPostMessage(data: Record): void { + iFrame?.contentWindow?.postMessage(JSON.stringify(data), lastApprovedOrigin); } -function drawEventExport(message) { +function drawEventExport(message: DrawioExportEventResponse) { store.set(saveBackupKey, message.data); if (onSave) { onSave(message.data).then(() => { @@ -20,7 +34,7 @@ function drawEventExport(message) { } } -function drawEventSave(message) { +function drawEventSave(message: DrawioSaveEventResponse) { drawPostMessage({ action: 'export', format: 'xmlpng', xml: message.xml, spin: 'Updating drawing', }); @@ -35,8 +49,10 @@ function drawEventInit() { function drawEventConfigure() { const config = {}; - window.$events.emitPublic(iFrame, 'editor-drawio::configure', {config}); - drawPostMessage({action: 'configure', config}); + if (iFrame) { + window.$events.emitPublic(iFrame, 'editor-drawio::configure', {config}); + drawPostMessage({action: 'configure', config}); + } } function drawEventClose() { @@ -47,9 +63,8 @@ function drawEventClose() { /** * Receive and handle a message event from the draw.io window. - * @param {MessageEvent} event */ -function drawReceive(event) { +function drawReceive(event: MessageEvent) { if (!event.data || event.data.length < 1) return; if (event.origin !== lastApprovedOrigin) return; @@ -59,9 +74,9 @@ function drawReceive(event) { } else if (message.event === 'exit') { drawEventClose(); } else if (message.event === 'save') { - drawEventSave(message); + drawEventSave(message as DrawioSaveEventResponse); } else if (message.event === 'export') { - drawEventExport(message); + drawEventExport(message as DrawioExportEventResponse); } else if (message.event === 'configure') { drawEventConfigure(); } @@ -79,9 +94,8 @@ async function attemptRestoreIfExists() { console.error('Missing expected unsaved-drawing dialog'); } - if (backupVal) { - /** @var {ConfirmDialog} */ - const dialog = window.$components.firstOnElement(dialogEl, 'confirm-dialog'); + if (backupVal && dialogEl) { + const dialog = window.$components.firstOnElement(dialogEl, 'confirm-dialog') as ConfirmDialog; const restore = await dialog.show(); if (restore) { onInit = async () => backupVal; @@ -94,11 +108,9 @@ async function attemptRestoreIfExists() { * onSaveCallback must return a promise that resolves on successful save and errors on failure. * onInitCallback must return a promise with the xml to load for the editor. * Will attempt to provide an option to restore unsaved changes if found to exist. - * @param {String} drawioUrl - * @param {Function>} onInitCallback - * @param {Function} onSaveCallback - Is called with the drawing data on save. + * onSaveCallback Is called with the drawing data on save. */ -export async function show(drawioUrl, onInitCallback, onSaveCallback) { +export async function show(drawioUrl: string, onInitCallback: () => Promise, onSaveCallback: (data: string) => Promise): Promise { onInit = onInitCallback; onSave = onSaveCallback; @@ -114,7 +126,7 @@ export async function show(drawioUrl, onInitCallback, onSaveCallback) { lastApprovedOrigin = (new URL(drawioUrl)).origin; } -export async function upload(imageData, pageUploadedToId) { +export async function upload(imageData: string, pageUploadedToId: string): Promise<{}|string> { const data = { image: imageData, uploaded_to: pageUploadedToId, @@ -129,10 +141,8 @@ export function close() { /** * Load an existing image, by fetching it as Base64 from the system. - * @param drawingId - * @returns {Promise} */ -export async function load(drawingId) { +export async function load(drawingId: string): Promise { try { const resp = await window.$http.get(window.baseUrl(`/images/drawio/base64/${drawingId}`)); return `data:image/png;base64,${resp.data.content}`; diff --git a/resources/js/services/events.js b/resources/js/services/events.js deleted file mode 100644 index 761305793..000000000 --- a/resources/js/services/events.js +++ /dev/null @@ -1,81 +0,0 @@ -const listeners = {}; -const stack = []; - -/** - * Emit a custom event for any handlers to pick-up. - * @param {String} eventName - * @param {*} eventData - */ -export function emit(eventName, eventData) { - stack.push({name: eventName, data: eventData}); - - const listenersToRun = listeners[eventName] || []; - for (const listener of listenersToRun) { - listener(eventData); - } -} - -/** - * Listen to a custom event and run the given callback when that event occurs. - * @param {String} eventName - * @param {Function} callback - * @returns {Events} - */ -export function listen(eventName, callback) { - if (typeof listeners[eventName] === 'undefined') listeners[eventName] = []; - listeners[eventName].push(callback); -} - -/** - * Emit an event for public use. - * Sends the event via the native DOM event handling system. - * @param {Element} targetElement - * @param {String} eventName - * @param {Object} eventData - */ -export function emitPublic(targetElement, eventName, eventData) { - const event = new CustomEvent(eventName, { - detail: eventData, - bubbles: true, - }); - targetElement.dispatchEvent(event); -} - -/** - * Emit a success event with the provided message. - * @param {String} message - */ -export function success(message) { - emit('success', message); -} - -/** - * Emit an error event with the provided message. - * @param {String} message - */ -export function error(message) { - emit('error', message); -} - -/** - * Notify of standard server-provided validation errors. - * @param {Object} responseErr - */ -export function showValidationErrors(responseErr) { - if (!responseErr.status) return; - if (responseErr.status === 422 && responseErr.data) { - const message = Object.values(responseErr.data).flat().join('\n'); - error(message); - } -} - -/** - * Notify standard server-provided error messages. - * @param {Object} responseErr - */ -export function showResponseError(responseErr) { - if (!responseErr.status) return; - if (responseErr.status >= 400 && responseErr.data && responseErr.data.message) { - error(responseErr.data.message); - } -} diff --git a/resources/js/services/events.ts b/resources/js/services/events.ts new file mode 100644 index 000000000..c251ee21b --- /dev/null +++ b/resources/js/services/events.ts @@ -0,0 +1,71 @@ +export class EventManager { + protected listeners: Record void)[]> = {}; + protected stack: {name: string, data: {}}[] = []; + + /** + * Emit a custom event for any handlers to pick-up. + */ + emit(eventName: string, eventData: {}): void { + this.stack.push({name: eventName, data: eventData}); + + const listenersToRun = this.listeners[eventName] || []; + for (const listener of listenersToRun) { + listener(eventData); + } + } + + /** + * Listen to a custom event and run the given callback when that event occurs. + */ + listen(eventName: string, callback: (data: {}) => void): void { + if (typeof this.listeners[eventName] === 'undefined') this.listeners[eventName] = []; + this.listeners[eventName].push(callback); + } + + /** + * Emit an event for public use. + * Sends the event via the native DOM event handling system. + */ + emitPublic(targetElement: Element, eventName: string, eventData: {}): void { + const event = new CustomEvent(eventName, { + detail: eventData, + bubbles: true, + }); + targetElement.dispatchEvent(event); + } + + /** + * Emit a success event with the provided message. + */ + success(message: string): void { + this.emit('success', message); + } + + /** + * Emit an error event with the provided message. + */ + error(message: string): void { + this.emit('error', message); + } + + /** + * Notify of standard server-provided validation errors. + */ + showValidationErrors(responseErr: {status?: number, data?: object}): void { + if (!responseErr.status) return; + if (responseErr.status === 422 && responseErr.data) { + const message = Object.values(responseErr.data).flat().join('\n'); + this.error(message); + } + } + + /** + * Notify standard server-provided error messages. + */ + showResponseError(responseErr: {status?: number, data?: {message?: string}}): void { + if (!responseErr.status) return; + if (responseErr.status >= 400 && responseErr.data && responseErr.data.message) { + this.error(responseErr.data.message); + } + } +} diff --git a/resources/js/wysiwyg/todo.md b/resources/js/wysiwyg/todo.md index 67b5fb780..61b592ca0 100644 --- a/resources/js/wysiwyg/todo.md +++ b/resources/js/wysiwyg/todo.md @@ -1,11 +1,16 @@ # Lexical based editor todo +## In progress + +- Add Type: Drawings + - Continue converting drawio to typescript + - Next step to convert http service to ts. + ## Main Todo - Alignments: Use existing classes for blocks - Alignments: Handle inline block content (image, video) - Add Type: Video/media/embed -- Add Type: Drawings - Handle toolbars on scroll - Table features - Image paste upload @@ -20,6 +25,7 @@ - Link heading-based ID reference menu - Image gallery integration for insert - Image gallery integration for form +- Drawing gallery integration ## Bugs diff --git a/resources/js/wysiwyg/ui/decorators/code-block.ts b/resources/js/wysiwyg/ui/decorators/code-block.ts index cfb2c6aef..d6947ea75 100644 --- a/resources/js/wysiwyg/ui/decorators/code-block.ts +++ b/resources/js/wysiwyg/ui/decorators/code-block.ts @@ -2,7 +2,6 @@ import {EditorDecorator} from "../framework/decorator"; import {EditorUiContext} from "../framework/core"; import {$openCodeEditorForNode, CodeBlockNode} from "../../nodes/code-block"; import {$selectionContainsNode, $selectSingleNode} from "../../helpers"; -import {context} from "esbuild"; import {BaseSelection} from "lexical"; diff --git a/resources/js/wysiwyg/ui/decorators/diagram.ts b/resources/js/wysiwyg/ui/decorators/diagram.ts index 2f092bd20..9c48f8c24 100644 --- a/resources/js/wysiwyg/ui/decorators/diagram.ts +++ b/resources/js/wysiwyg/ui/decorators/diagram.ts @@ -1,12 +1,35 @@ import {EditorDecorator} from "../framework/decorator"; import {EditorUiContext} from "../framework/core"; +import {$selectionContainsNode, $selectSingleNode} from "../../helpers"; +import {$openCodeEditorForNode, CodeBlockNode} from "../../nodes/code-block"; +import {BaseSelection} from "lexical"; +import {$openDrawingEditorForNode, DiagramNode} from "../../nodes/diagram"; export class DiagramDecorator extends EditorDecorator { protected completedSetup: boolean = false; setup(context: EditorUiContext, element: HTMLElement) { - // + const diagramNode = this.getNode(); + element.addEventListener('click', event => { + context.editor.update(() => { + $selectSingleNode(this.getNode()); + }) + }); + + element.addEventListener('dblclick', event => { + context.editor.getEditorState().read(() => { + $openDrawingEditorForNode(context.editor, (this.getNode() as DiagramNode)); + }); + }); + + const selectionChange = (selection: BaseSelection|null): void => { + element.classList.toggle('selected', $selectionContainsNode(selection, diagramNode)); + }; + context.manager.onSelectionChange(selectionChange); + this.onDestroy(() => { + context.manager.offSelectionChange(selectionChange); + }); this.completedSetup = true; } diff --git a/tsconfig.json b/tsconfig.json index 3ca03da30..0be5421c7 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -12,7 +12,7 @@ // "disableReferencedProjectLoad": true, /* Reduce the number of projects loaded automatically by TypeScript. */ /* Language and Environment */ - "target": "es2016", /* Set the JavaScript language version for emitted JavaScript and include compatible library declarations. */ + "target": "es2019", /* Set the JavaScript language version for emitted JavaScript and include compatible library declarations. */ // "lib": [], /* Specify a set of bundled library declaration files that describe the target runtime environment. */ // "jsx": "preserve", /* Specify what JSX code is generated. */ // "experimentalDecorators": true, /* Enable experimental support for legacy experimental decorators. */