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 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