]> BookStack Code Mirror - bookstack/blob - resources/js/services/drawio.ts
Lexical: Started converting drawio to TS
[bookstack] / resources / js / services / drawio.ts
1 // Docs: https://www.diagrams.net/doc/faq/embed-mode
2 import * as store from './store';
3 import {ConfirmDialog} from "../components";
4
5 type DrawioExportEventResponse = {
6     action: 'export',
7     format: string,
8     message: string,
9     data: string,
10     xml: string,
11 };
12
13 type DrawioSaveEventResponse = {
14     action: 'save',
15     xml: string,
16 };
17
18 let iFrame: HTMLIFrameElement|null = null;
19 let lastApprovedOrigin: string;
20 let onInit: () => Promise<string>;
21 let onSave: (data: string) => Promise<any>;
22 const saveBackupKey = 'last-drawing-save';
23
24 function drawPostMessage(data: Record<any, any>): void {
25     iFrame?.contentWindow?.postMessage(JSON.stringify(data), lastApprovedOrigin);
26 }
27
28 function drawEventExport(message: DrawioExportEventResponse) {
29     store.set(saveBackupKey, message.data);
30     if (onSave) {
31         onSave(message.data).then(() => {
32             store.del(saveBackupKey);
33         });
34     }
35 }
36
37 function drawEventSave(message: DrawioSaveEventResponse) {
38     drawPostMessage({
39         action: 'export', format: 'xmlpng', xml: message.xml, spin: 'Updating drawing',
40     });
41 }
42
43 function drawEventInit() {
44     if (!onInit) return;
45     onInit().then(xml => {
46         drawPostMessage({action: 'load', autosave: 1, xml});
47     });
48 }
49
50 function drawEventConfigure() {
51     const config = {};
52     if (iFrame) {
53         window.$events.emitPublic(iFrame, 'editor-drawio::configure', {config});
54         drawPostMessage({action: 'configure', config});
55     }
56 }
57
58 function drawEventClose() {
59     // eslint-disable-next-line no-use-before-define
60     window.removeEventListener('message', drawReceive);
61     if (iFrame) document.body.removeChild(iFrame);
62 }
63
64 /**
65  * Receive and handle a message event from the draw.io window.
66  */
67 function drawReceive(event: MessageEvent) {
68     if (!event.data || event.data.length < 1) return;
69     if (event.origin !== lastApprovedOrigin) return;
70
71     const message = JSON.parse(event.data);
72     if (message.event === 'init') {
73         drawEventInit();
74     } else if (message.event === 'exit') {
75         drawEventClose();
76     } else if (message.event === 'save') {
77         drawEventSave(message as DrawioSaveEventResponse);
78     } else if (message.event === 'export') {
79         drawEventExport(message as DrawioExportEventResponse);
80     } else if (message.event === 'configure') {
81         drawEventConfigure();
82     }
83 }
84
85 /**
86  * Attempt to prompt and restore unsaved drawing content if existing.
87  * @returns {Promise<void>}
88  */
89 async function attemptRestoreIfExists() {
90     const backupVal = await store.get(saveBackupKey);
91     const dialogEl = document.getElementById('unsaved-drawing-dialog');
92
93     if (!dialogEl) {
94         console.error('Missing expected unsaved-drawing dialog');
95     }
96
97     if (backupVal && dialogEl) {
98         const dialog = window.$components.firstOnElement(dialogEl, 'confirm-dialog') as ConfirmDialog;
99         const restore = await dialog.show();
100         if (restore) {
101             onInit = async () => backupVal;
102         }
103     }
104 }
105
106 /**
107  * Show the draw.io editor.
108  * onSaveCallback must return a promise that resolves on successful save and errors on failure.
109  * onInitCallback must return a promise with the xml to load for the editor.
110  * Will attempt to provide an option to restore unsaved changes if found to exist.
111  * onSaveCallback Is called with the drawing data on save.
112  */
113 export async function show(drawioUrl: string, onInitCallback: () => Promise<string>, onSaveCallback: (data: string) => Promise<void>): Promise<void> {
114     onInit = onInitCallback;
115     onSave = onSaveCallback;
116
117     await attemptRestoreIfExists();
118
119     iFrame = document.createElement('iframe');
120     iFrame.setAttribute('frameborder', '0');
121     window.addEventListener('message', drawReceive);
122     iFrame.setAttribute('src', drawioUrl);
123     iFrame.setAttribute('class', 'fullscreen');
124     iFrame.style.backgroundColor = '#FFFFFF';
125     document.body.appendChild(iFrame);
126     lastApprovedOrigin = (new URL(drawioUrl)).origin;
127 }
128
129 export async function upload(imageData: string, pageUploadedToId: string): Promise<{}|string> {
130     const data = {
131         image: imageData,
132         uploaded_to: pageUploadedToId,
133     };
134     const resp = await window.$http.post(window.baseUrl('/images/drawio'), data);
135     return resp.data;
136 }
137
138 export function close() {
139     drawEventClose();
140 }
141
142 /**
143  * Load an existing image, by fetching it as Base64 from the system.
144  */
145 export async function load(drawingId: string): Promise<string> {
146     try {
147         const resp = await window.$http.get(window.baseUrl(`/images/drawio/base64/${drawingId}`));
148         return `data:image/png;base64,${resp.data.content}`;
149     } catch (error) {
150         if (error instanceof window.$http.HttpError) {
151             window.$events.showResponseError(error);
152         }
153         close();
154         throw error;
155     }
156 }