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 '§ok=' + 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 '§ok=' + 255 encodeURIComponent(JSINFO['sectok']); 256 } 257} 258