Lexical: Started converting drawio to TS

Converted events service to TS as part of this.
This commit is contained in:
Dan Brown 2024-07-18 11:19:11 +01:00
parent 5002a89754
commit 634b0aaa07
No known key found for this signature in database
GPG Key ID: 46D9F943C24A2EF9
9 changed files with 142 additions and 112 deletions

View File

@ -1,4 +1,4 @@
import * as events from './services/events'; import {EventManager} from './services/events.ts';
import * as httpInstance from './services/http'; import * as httpInstance from './services/http';
import Translations from './services/translations'; import Translations from './services/translations';
import * as componentMap from './components'; import * as componentMap from './components';
@ -21,7 +21,7 @@ window.importVersioned = function importVersioned(moduleName) {
// Set events and http services on window // Set events and http services on window
window.$http = httpInstance; window.$http = httpInstance;
window.$events = events; window.$events = new EventManager();
// Translation setup // Translation setup
// Creates a global function with name 'trans' to be used in the same way as the Laravel translation system // Creates a global function with name 'trans' to be used in the same way as the Laravel translation system

View File

@ -1,7 +1,9 @@
import {ComponentStore} from "./services/components"; import {ComponentStore} from "./services/components";
import {EventManager} from "./services/events";
declare global { declare global {
interface Window { interface Window {
$components: ComponentStore, $components: ComponentStore,
$events: EventManager,
} }
} }

View File

@ -1,17 +1,31 @@
// Docs: https://www.diagrams.net/doc/faq/embed-mode // Docs: https://www.diagrams.net/doc/faq/embed-mode
import * as store from './store'; import * as store from './store';
import {ConfirmDialog} from "../components";
let iFrame = null; type DrawioExportEventResponse = {
let lastApprovedOrigin; action: 'export',
let onInit; format: string,
let onSave; message: string,
data: string,
xml: string,
};
type DrawioSaveEventResponse = {
action: 'save',
xml: string,
};
let iFrame: HTMLIFrameElement|null = null;
let lastApprovedOrigin: string;
let onInit: () => Promise<string>;
let onSave: (data: string) => Promise<any>;
const saveBackupKey = 'last-drawing-save'; const saveBackupKey = 'last-drawing-save';
function drawPostMessage(data) { function drawPostMessage(data: Record<any, any>): void {
iFrame.contentWindow.postMessage(JSON.stringify(data), lastApprovedOrigin); iFrame?.contentWindow?.postMessage(JSON.stringify(data), lastApprovedOrigin);
} }
function drawEventExport(message) { function drawEventExport(message: DrawioExportEventResponse) {
store.set(saveBackupKey, message.data); store.set(saveBackupKey, message.data);
if (onSave) { if (onSave) {
onSave(message.data).then(() => { onSave(message.data).then(() => {
@ -20,7 +34,7 @@ function drawEventExport(message) {
} }
} }
function drawEventSave(message) { function drawEventSave(message: DrawioSaveEventResponse) {
drawPostMessage({ drawPostMessage({
action: 'export', format: 'xmlpng', xml: message.xml, spin: 'Updating drawing', action: 'export', format: 'xmlpng', xml: message.xml, spin: 'Updating drawing',
}); });
@ -35,9 +49,11 @@ function drawEventInit() {
function drawEventConfigure() { function drawEventConfigure() {
const config = {}; const config = {};
if (iFrame) {
window.$events.emitPublic(iFrame, 'editor-drawio::configure', {config}); window.$events.emitPublic(iFrame, 'editor-drawio::configure', {config});
drawPostMessage({action: 'configure', config}); drawPostMessage({action: 'configure', config});
} }
}
function drawEventClose() { function drawEventClose() {
// eslint-disable-next-line no-use-before-define // eslint-disable-next-line no-use-before-define
@ -47,9 +63,8 @@ function drawEventClose() {
/** /**
* Receive and handle a message event from the draw.io window. * 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.data || event.data.length < 1) return;
if (event.origin !== lastApprovedOrigin) return; if (event.origin !== lastApprovedOrigin) return;
@ -59,9 +74,9 @@ function drawReceive(event) {
} else if (message.event === 'exit') { } else if (message.event === 'exit') {
drawEventClose(); drawEventClose();
} else if (message.event === 'save') { } else if (message.event === 'save') {
drawEventSave(message); drawEventSave(message as DrawioSaveEventResponse);
} else if (message.event === 'export') { } else if (message.event === 'export') {
drawEventExport(message); drawEventExport(message as DrawioExportEventResponse);
} else if (message.event === 'configure') { } else if (message.event === 'configure') {
drawEventConfigure(); drawEventConfigure();
} }
@ -79,9 +94,8 @@ async function attemptRestoreIfExists() {
console.error('Missing expected unsaved-drawing dialog'); console.error('Missing expected unsaved-drawing dialog');
} }
if (backupVal) { if (backupVal && dialogEl) {
/** @var {ConfirmDialog} */ const dialog = window.$components.firstOnElement(dialogEl, 'confirm-dialog') as ConfirmDialog;
const dialog = window.$components.firstOnElement(dialogEl, 'confirm-dialog');
const restore = await dialog.show(); const restore = await dialog.show();
if (restore) { if (restore) {
onInit = async () => backupVal; onInit = async () => backupVal;
@ -94,11 +108,9 @@ async function attemptRestoreIfExists() {
* onSaveCallback must return a promise that resolves on successful save and errors on failure. * 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. * 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. * Will attempt to provide an option to restore unsaved changes if found to exist.
* @param {String} drawioUrl * onSaveCallback Is called with the drawing data on save.
* @param {Function<Promise<String>>} onInitCallback
* @param {Function<Promise>} onSaveCallback - Is called with the drawing data on save.
*/ */
export async function show(drawioUrl, onInitCallback, onSaveCallback) { export async function show(drawioUrl: string, onInitCallback: () => Promise<string>, onSaveCallback: (data: string) => Promise<void>): Promise<void> {
onInit = onInitCallback; onInit = onInitCallback;
onSave = onSaveCallback; onSave = onSaveCallback;
@ -114,7 +126,7 @@ export async function show(drawioUrl, onInitCallback, onSaveCallback) {
lastApprovedOrigin = (new URL(drawioUrl)).origin; lastApprovedOrigin = (new URL(drawioUrl)).origin;
} }
export async function upload(imageData, pageUploadedToId) { export async function upload(imageData: string, pageUploadedToId: string): Promise<{}|string> {
const data = { const data = {
image: imageData, image: imageData,
uploaded_to: pageUploadedToId, uploaded_to: pageUploadedToId,
@ -129,10 +141,8 @@ export function close() {
/** /**
* Load an existing image, by fetching it as Base64 from the system. * Load an existing image, by fetching it as Base64 from the system.
* @param drawingId
* @returns {Promise<string>}
*/ */
export async function load(drawingId) { export async function load(drawingId: string): Promise<string> {
try { try {
const resp = await window.$http.get(window.baseUrl(`/images/drawio/base64/${drawingId}`)); const resp = await window.$http.get(window.baseUrl(`/images/drawio/base64/${drawingId}`));
return `data:image/png;base64,${resp.data.content}`; return `data:image/png;base64,${resp.data.content}`;

View File

@ -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);
}
}

View File

@ -0,0 +1,71 @@
export class EventManager {
protected listeners: Record<string, ((data: {}) => 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);
}
}
}

View File

@ -1,11 +1,16 @@
# Lexical based editor todo # Lexical based editor todo
## In progress
- Add Type: Drawings
- Continue converting drawio to typescript
- Next step to convert http service to ts.
## Main Todo ## Main Todo
- Alignments: Use existing classes for blocks - Alignments: Use existing classes for blocks
- Alignments: Handle inline block content (image, video) - Alignments: Handle inline block content (image, video)
- Add Type: Video/media/embed - Add Type: Video/media/embed
- Add Type: Drawings
- Handle toolbars on scroll - Handle toolbars on scroll
- Table features - Table features
- Image paste upload - Image paste upload
@ -20,6 +25,7 @@
- Link heading-based ID reference menu - Link heading-based ID reference menu
- Image gallery integration for insert - Image gallery integration for insert
- Image gallery integration for form - Image gallery integration for form
- Drawing gallery integration
## Bugs ## Bugs

View File

@ -2,7 +2,6 @@ import {EditorDecorator} from "../framework/decorator";
import {EditorUiContext} from "../framework/core"; import {EditorUiContext} from "../framework/core";
import {$openCodeEditorForNode, CodeBlockNode} from "../../nodes/code-block"; import {$openCodeEditorForNode, CodeBlockNode} from "../../nodes/code-block";
import {$selectionContainsNode, $selectSingleNode} from "../../helpers"; import {$selectionContainsNode, $selectSingleNode} from "../../helpers";
import {context} from "esbuild";
import {BaseSelection} from "lexical"; import {BaseSelection} from "lexical";

View File

@ -1,12 +1,35 @@
import {EditorDecorator} from "../framework/decorator"; import {EditorDecorator} from "../framework/decorator";
import {EditorUiContext} from "../framework/core"; 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 { export class DiagramDecorator extends EditorDecorator {
protected completedSetup: boolean = false; protected completedSetup: boolean = false;
setup(context: EditorUiContext, element: HTMLElement) { 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; this.completedSetup = true;
} }

View File

@ -12,7 +12,7 @@
// "disableReferencedProjectLoad": true, /* Reduce the number of projects loaded automatically by TypeScript. */ // "disableReferencedProjectLoad": true, /* Reduce the number of projects loaded automatically by TypeScript. */
/* Language and Environment */ /* 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. */ // "lib": [], /* Specify a set of bundled library declaration files that describe the target runtime environment. */
// "jsx": "preserve", /* Specify what JSX code is generated. */ // "jsx": "preserve", /* Specify what JSX code is generated. */
// "experimentalDecorators": true, /* Enable experimental support for legacy experimental decorators. */ // "experimentalDecorators": true, /* Enable experimental support for legacy experimental decorators. */