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