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