1/** 2 * ProseMirror Form for editing diagram attribute 3 */ 4class DiagramsForm extends KeyValueForm { 5 6 #attributes = { 7 id: '', 8 svg: '', 9 type: '', 10 title: '', 11 url: '', 12 width: null, 13 height: null, 14 align: '' 15 }; 16 17 #onsubmitCB = null; 18 #oncloseCB = null; 19 20 /** 21 * Initialize the KeyValue form with fields and event handlers 22 */ 23 constructor(attributes, onsubmit, onclose = null) { 24 const name = LANG.plugins.diagrams.formtitle; 25 const fields = DiagramsForm.#getFields(attributes); 26 27 super(name, fields); 28 this.#attributes = { 29 ...this.#attributes, 30 ...attributes 31 }; 32 this.#onsubmitCB = onsubmit; 33 this.#oncloseCB = onclose; 34 35 // attach handlers 36 this.$form.on('submit', (event) => { 37 event.preventDefault(); // prevent form submission 38 this.#onsubmitCB(this.#attributes); 39 this.hide(); // close dialog 40 }); 41 42 43 this.$form.on('dialogclose', (event) => { 44 if (this.#oncloseCB) this.#oncloseCB(); 45 this.destroy(); 46 }); 47 48 this.$form.on('change', 'input,select', this.updateInternalState.bind(this)); 49 50 this.#getButtonsMediaManager(this.#attributes); 51 this.#getButtonsEditor(this.#attributes); 52 53 this.updateFormState(); 54 } 55 56 #getButtonsEditor(attributes) { 57 if (attributes.type === 'embed' || attributes.id) { 58 const editButton = document.createElement('button'); 59 editButton.className = 'diagrams-btn-edit'; 60 editButton.id = 'diagrams__btn-edit'; 61 editButton.innerText = LANG.plugins.diagrams.editButton; 62 editButton.type = 'button'; 63 this.$form.find('fieldset').append(editButton); 64 65 editButton.addEventListener('click', event => { 66 event.preventDefault(); // prevent form submission 67 68 if (attributes.type === 'mediafile') { 69 const diagramsEditor = new DiagramsEditor(this.onSavedMediaFile.bind(this, attributes.id)); 70 diagramsEditor.editMediaFile(attributes.id); 71 } else { 72 const diagramsEditor = new DiagramsEditor(); 73 diagramsEditor.editMemory(attributes.url, this.onSaveEmbed.bind(this)); 74 } 75 }); 76 } 77 } 78 79 #getButtonsMediaManager(attributes) { 80 // media manager button 81 if (attributes.type === 'mediafile') { 82 const selectButton = document.createElement('button'); 83 selectButton.innerText = LANG.plugins.diagrams.selectSource; 84 selectButton.className = 'diagrams-btn-select'; 85 selectButton.type = 'button'; 86 selectButton.addEventListener('click', () => 87 window.open( 88 `${DOKU_BASE}lib/exe/mediamanager.php?ns=${encodeURIComponent(JSINFO.namespace)}&onselect=dMediaSelect`, 89 'mediaselect', 90 'width=750,height=500,left=20,top=20,scrollbars=yes,resizable=yes', 91 ) 92 ); 93 this.$form.find('fieldset').prepend(selectButton); 94 window.dMediaSelect = this.mediaSelect.bind(this); // register as global function 95 } 96 } 97 98 /** 99 * Define form fields depending on type 100 * @returns {object} 101 */ 102 static #getFields(attributes) { 103 const fields = [ 104 { 105 type: 'select', 'label': LANG.plugins.diagrams.alignment, 'name': 'align', 'options': 106 [ 107 {value: '', label: ''}, 108 {value: 'left', label: LANG.plugins.diagrams.left}, 109 {value: 'right', label: LANG.plugins.diagrams.right}, 110 {value: 'center', label: LANG.plugins.diagrams.center} 111 ] 112 }, 113 { 114 label: LANG.plugins.diagrams.title, name: 'title' 115 } 116 ]; 117 118 if (attributes.type === 'mediafile') { 119 fields.unshift( 120 { 121 label: LANG.plugins.diagrams.mediaSource, 122 name: 'id' 123 } 124 ); 125 } 126 return fields; 127 } 128 129 /** 130 * Updates the form to reflect the current internal attributes 131 */ 132 updateFormState() { 133 for (const [key, value] of Object.entries(this.#attributes)) { 134 this.$form.find('[name="' + key + '"]').val(value); 135 } 136 this.updateInternalUrl(); 137 } 138 139 /** 140 * Update the internal attributes to reflect the current form state 141 */ 142 updateInternalState() { 143 for (const [key, value] of Object.entries(this.#attributes)) { 144 const $elem = this.$form.find('[name="' + key + '"]'); 145 if ($elem.length) { 146 this.#attributes[key] = $elem.val(); 147 } 148 } 149 this.updateInternalUrl(); 150 } 151 152 /** 153 * Calculate the Display URL for the current mediafile 154 */ 155 updateInternalUrl() { 156 if (this.#attributes.type === 'mediafile') { 157 this.#attributes.url = `${DOKU_BASE}lib/exe/fetch.php?media=${this.#attributes.id}`; 158 } 159 } 160 161 /** 162 * After svaing a media file reload the src for all images using it 163 * 164 * @see https://stackoverflow.com/a/66312176 165 * @param {string} mediaid 166 * @returns {Promise<void>} 167 */ 168 async onSavedMediaFile(mediaid) { 169 const url = `${DOKU_BASE}lib/exe/fetch.php?cache=nocache&media=${mediaid}`; 170 await fetch(url, {cache: 'reload', mode: 'no-cors'}); 171 document.body.querySelectorAll(`img[src='${url}']`) 172 .forEach(img => img.src = url) 173 } 174 175 /** 176 * Save an embedded diagram back to the editor 177 */ 178 onSaveEmbed(svg) { 179 const encSvg = this.bytesToBase64(new TextEncoder().encode(svg)); 180 this.#attributes.url = 'data:image/svg+xml;base64,' + encSvg; 181 this.updateFormState(); 182 return true; 183 } 184 185 /** 186 * Callback called from the media popup on selecting a file 187 * 188 * This is globally registered as window.dMediaSelect 189 * 190 * @param {string} edid ignored 191 * @param {string} mediaid the picked media ID 192 */ 193 async mediaSelect(edid, mediaid) { 194 const response = await fetch( 195 `${DOKU_BASE}lib/exe/ajax.php?call=plugin_diagrams_mediafile_isdiagramcheck&diagram=` + 196 encodeURIComponent(mediaid), 197 { 198 method: 'POST', 199 cache: 'no-cache', 200 } 201 ); 202 203 if (!response.ok) { 204 alert(LANG.plugins.diagrams.mediafileIsNotDiagram); 205 return; 206 } 207 208 this.#attributes.id = mediaid; 209 this.updateFormState(); 210 } 211 212 /** 213 * UTF-8 safe Base64 encoder 214 * 215 * @see https://developer.mozilla.org/en-US/docs/Glossary/Base64#the_unicode_problem 216 * @param bytes 217 * @returns {string} 218 */ 219 bytesToBase64(bytes) { 220 const binString = String.fromCodePoint(...bytes); 221 return btoa(binString); 222 } 223} 224