xref: /plugin/diagrams/script/DiagramsEditor.js (revision 317bdfc2bd4bf051cf5d349bd5d8d27dc2a0b6c5)
1/**
2 * Callback for saving a diagram
3 * @callback saveCallback
4 * @param {string} svg The SVG data to save
5 */
6
7/**
8 * Callback for when saving has finished suscessfully
9 * @callback postSaveCallback
10 */
11
12
13/**
14 * This class encapsulates all interaction with the diagrams editor
15 *
16 * It manages displaying and communicating with the editor, most importantly in manages loading
17 * and saving diagrams.
18 *
19 * FIXME we're not catching any fetch exceptions currently. Should we?
20 * FIXME should we somehow ensure that there is only ever one instance of this class?
21 * @class
22 */
23class DiagramsEditor {
24    /** @type {HTMLIFrameElement} the editor iframe */
25    #diagramsEditor = null;
26
27    /** @type {saveCallback} the method to call for saving the diagram */
28    #saveCallback = null;
29
30    /** @type {postSaveCallback} called when saving has finished*/
31    #postSaveCallback = null;
32
33    /** @type {string} the initial save data to load, set by one of the edit* methods */
34    #svg = '';
35
36    /**
37     * Create a new diagrams editor
38     *
39     * @param {postSaveCallback} postSaveCallback Called when saving has finished
40     */
41    constructor(postSaveCallback = null) {
42        this.#postSaveCallback = postSaveCallback;
43    }
44
45    /**
46     * Initialize the editor for editing a media file
47     *
48     * @param {string} mediaid The media ID to edit, if 404 a new file will be created
49     */
50    async editMediaFile(mediaid) {
51        this.#saveCallback = (svg) => this.#saveMediaFile(mediaid, svg);
52
53        const response = await fetch(DOKU_BASE + 'lib/exe/fetch.php?media=' + mediaid, {
54            method: 'GET',
55            cache: 'no-cache',
56        });
57
58        if (response.ok) {
59            // if not 404, load the SVG data
60            this.#svg = await response.text();
61        }
62
63        this.#createEditor();
64    }
65
66    /**
67     * Initialize the editor for editing an embedded diagram
68     *
69     * @param {string} pageid The page ID to on which the diagram is embedded
70     * @param {int} position The position of the diagram in the page
71     * @param {int} length The length of the diagram in the page
72     */
73    async editEmbed(pageid, position, length) {
74        this.#saveCallback = (svg) => this.#saveEmbed(pageid, position, length, svg);
75
76        const url = DOKU_BASE + 'lib/exe/ajax.php?call=plugin_diagrams_embed_load' +
77            '&id=' + encodeURIComponent(pageid) +
78            '&pos=' + encodeURIComponent(position) +
79            '&len=' + encodeURIComponent(length);
80
81        const response = await fetch(url, {
82            method: 'GET',
83            cache: 'no-cache',
84        });
85
86        if (response.ok) {
87            // if not 404, load the SVG data
88            this.#svg = await response.text();
89        } else {
90            // a 404 for an embedded diagram should not happen
91            alert(LANG.plugins.diagrams.errorLoading);
92            return;
93        }
94
95        this.#createEditor();
96    }
97
98    /**
99     * Initialize the editor for editing a diagram in memory
100     *
101     * @param {string} svg The SVG raw data to edit, empty for new file
102     * @param {saveCallback} callback The callback to call when the editor is closed
103     */
104    editMemory(svg, callback) {
105        this.#svg = svg;
106        this.#saveCallback = callback.bind(this);
107        this.#createEditor();
108    }
109
110    /**
111     * Saves a diagram as a media file
112     *
113     * @param {string} mediaid The media ID to save
114     * @param {string} svg The SVG raw data to save
115     * @returns {Promise<boolean>}
116     */
117    async #saveMediaFile(mediaid, svg) {
118        const uploadUrl = this.#mediaUploadUrl(mediaid);
119
120        const response = await fetch(uploadUrl, {
121            method: 'POST',
122            cache: 'no-cache',
123            body: svg,
124        });
125
126        return response.ok;
127    }
128
129    /**
130     * Saves a diagram as an embedded diagram
131     *
132     * This replaces the previous diagram at the given postion
133     *
134     * @param {string} pageid The page ID on which the diagram is embedded
135     * @param {int} position The position of the diagram in the page
136     * @param {int} length The length of the diagram as it was before
137     * @param {string} svg The SVG raw data to save
138     * @returns {Promise<boolean>}
139     */
140    async #saveEmbed(pageid, position, length, svg) {
141        const uploadUrl = DOKU_BASE + 'lib/exe/ajax.php?call=plugin_diagrams_embed_save' +
142            '&id=' + encodeURIComponent(pageid) +
143            '&pos=' + encodeURIComponent(position) +
144            '&len=' + encodeURIComponent(length) +
145            '&sectok=' + JSINFO['sectok'];
146
147        const body = new FormData();
148        body.set('svg', svg);
149
150        const response = await fetch(uploadUrl, {
151            method: 'POST',
152            cache: 'no-cache',
153            body: body,
154        });
155
156        return response.ok;
157    }
158
159    /**
160     * Create the editor iframe and attach the message listener
161     */
162    #createEditor() {
163        this.#diagramsEditor = document.createElement('iframe');
164        this.#diagramsEditor.id = 'plugin__diagrams-editor';
165        this.#diagramsEditor.src = JSINFO['plugins']['diagrams']['service_url'];
166        document.body.appendChild(this.#diagramsEditor);
167        window.addEventListener('message', this.#handleMessage.bind(this));
168    }
169
170    /**
171     * Remove the editor iframe and detach the message listener
172     */
173    #removeEditor() {
174        if (this.#diagramsEditor === null) return;
175        this.#diagramsEditor.remove();
176        this.#diagramsEditor = null;
177        window.removeEventListener('message', this.#handleMessage.bind(this));
178    }
179
180    /**
181     * Handle messages from diagramming service
182     *
183     * @param {Event} event
184     */
185    async #handleMessage(event) {
186        const msg = JSON.parse(event.data);
187        console.log(msg);
188
189        switch (msg.event) {
190            case 'init':
191                // load the SVG data into the editor
192                this.#diagramsEditor.contentWindow.postMessage(JSON.stringify({action: 'load', xml: this.#svg}), '*');
193                break;
194            case 'save':
195                // Save triggers an export to SVG action
196                this.#diagramsEditor.contentWindow.postMessage(
197                    JSON.stringify({
198                        action: 'export',
199                        format: 'xmlsvg',
200                        spin: LANG.plugins.diagrams.saving
201                    }),
202                    '*'
203                );
204                break;
205            case 'export':
206                if (msg.format !== 'svg') {
207                    alert(LANG.plugins.diagrams.errorUnsupportedFormat);
208                    return;
209                }
210                const ok = await this.#saveCallback(
211                    // msg.data contains the SVG as a Base64 encoded data URI
212                    decodeURIComponent(atob(
213                        msg.data.split(',')[1])
214                        .split('')
215                        .map(c => '%' + ('00' + c.charCodeAt(0).toString(16)).slice(-2))
216                        .join('')
217                    )
218                );
219                if (ok) {
220                    this.#removeEditor();
221                    if (this.#postSaveCallback !== null) {
222                        this.#postSaveCallback();
223                    }
224                } else {
225                    alert(LANG.plugins.diagrams.errorSaving);
226                }
227                break;
228            case 'exit':
229                this.#removeEditor();
230                break;
231        }
232    }
233
234    /**
235     * Get the URL to upload a media file
236     * @param {string} mediaid
237     * @returns {string}
238     */
239    #mediaUploadUrl(mediaid) {
240        // split mediaid into namespace and id
241        let id = mediaid;
242        let ns = '';
243        const idParts = id.split(':');
244        if (idParts.length > 1) {
245            id = idParts.pop(idParts);
246            ns = idParts.join(':');
247        }
248
249        return DOKU_BASE +
250            'lib/exe/ajax.php?call=mediaupload&ow=true&ns=' +
251            encodeURIComponent(ns) +
252            '&qqfile=' +
253            encodeURIComponent(id) +
254            '&sectok=' +
255            encodeURIComponent(JSINFO['sectok']);
256    }
257}
258