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