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