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