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} index The index of the diagram on the page (0-based) 73 */ 74 async editEmbed(pageid, index) { 75 this.#saveCallback = (svg) => this.#saveEmbed(pageid, index, svg); 76 77 const url = 'FIXME'; // FIXME we need a renderer endpoint that parses SVG out of a page 78 79 const response = await fetch(url, { 80 method: 'GET', 81 cache: 'no-cache', 82 }); 83 84 if (response.ok) { 85 // if not 404, load the SVG data 86 this.#svg = await response.text(); 87 } else { 88 // a 404 for an embedded diagram should not happen 89 alert(LANG.plugins.diagrams.errorLoading); 90 return; 91 } 92 93 this.#createEditor(); 94 } 95 96 /** 97 * Initialize the editor for editing a diagram in memory 98 * 99 * @param {string} svg The SVG raw data to edit, empty for new file 100 * @param {saveCallback} callback The callback to call when the editor is closed 101 */ 102 editMemory(svg, callback) { 103 this.#svg = svg; 104 this.#saveCallback = callback.bind(this); 105 this.#createEditor(); 106 } 107 108 /** 109 * Saves a diagram as a media file 110 * 111 * @param {string} mediaid The media ID to save 112 * @param {string} svg The SVG raw data to save 113 * @returns {Promise<boolean>} 114 */ 115 async #saveMediaFile(mediaid, svg) { 116 const uploadUrl = this.#mediaUploadUrl(mediaid); 117 118 const response = await fetch(uploadUrl, { 119 method: 'POST', 120 cache: 'no-cache', 121 }); 122 123 if (!response.ok) { 124 alert(LANG.plugins.diagrams.errorSaving); 125 } 126 return response.ok; 127 } 128 129 /** 130 * 131 * @param {string} pageid The page ID on which the diagram is embedded 132 * @param {string} index The index of the diagram on the page (0-based) 133 * @param {string} svg The SVG raw data to save 134 * @returns {Promise<boolean>} 135 */ 136 async #saveEmbed(pageid, index, svg) { 137 const uploadUrl = 'FIXME'; // FIXME we need an endpoint that saves an embedded diagram 138 139 const response = await fetch(uploadUrl, { 140 method: 'POST', 141 cache: 'no-cache', 142 }); 143 144 if (!response.ok) { 145 alert(LANG.plugins.diagrams.errorSaving); 146 } 147 148 return response.ok; 149 } 150 151 /** 152 * Create the editor iframe and attach the message listener 153 */ 154 #createEditor() { 155 this.#diagramsEditor = document.createElement('iframe'); 156 this.#diagramsEditor.id = 'diagrams-frame'; 157 this.#diagramsEditor.style = { 158 border: 0, 159 position: 'fixed', 160 top: 0, 161 left: 0, 162 right: 0, 163 bottom: 0, 164 width: '100%', 165 height: '100%', 166 zIndex: 9999, 167 }; // FIXME assign these via CSS 168 this.#diagramsEditor.src = JSINFO['plugins']['diagrams']['service_url']; 169 document.body.appendChild(this.#diagramsEditor); 170 window.addEventListener('message', this.#handleMessage.bind(this)); // FIXME might need to be public 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.removeDiagramsEditor(this.#handleMessage.bind(this)); 181 } 182 183 /** 184 * Handle messages from diagramming service 185 * 186 * @param {Event} event 187 */ 188 async #handleMessage(event) { 189 // FIXME do we need this originalEvent here? or is that jQuery specific? 190 const msg = JSON.parse(event.originalEvent.data); 191 192 switch (msg.event) { 193 case 'init': 194 // load the SVG data into the editor 195 this.#diagramsEditor.postMessage(JSON.stringify({action: 'load', xml: this.#svg}), '*'); 196 break; 197 case 'save': 198 // Save triggers an export to SVG action 199 this.#diagramsEditor.postMessage( 200 JSON.stringify({ 201 action: 'export', 202 format: 'xmlsvg', 203 spin: LANG.plugins.diagrams.saving 204 }), 205 '*' 206 ); 207 break; 208 case 'export': 209 if (msg.format !== 'svg') { 210 alert(LANG.plugins.diagrams.errorUnsupportedFormat); 211 return; 212 } 213 const ok = await this.#saveCallback( 214 // FIXME we used to prepend a doctype, but doctypes are no longer recommended for SVG 215 // FIXME we may need to add a XML header though? 216 decodeURIComponent(atob( 217 msg.data.split(',')[1]) 218 .split('') 219 .map(c => '%' + ('00' + c.charCodeAt(0).toString(16)).slice(-2)) 220 .join('') 221 ) 222 ); 223 this.#removeEditor(); // FIXME do we need this or wouldn't we get an exit message? 224 if (this.#postSaveCallback !== null) { 225 this.#postSaveCallback(ok); 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