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 this.$form.on('dialogclose', (event) => { 43 if (this.#oncloseCB) this.#oncloseCB(); 44 this.destroy(); 45 }); 46 47 this.$form.on('change', 'input,select', this.updateInternalState.bind(this)); 48 49 this.#getButtonsMediaManager(this.#attributes); 50 this.#getButtonsEditor(this.#attributes); 51 52 this.updateFormState(); 53 } 54 55 #getButtonsEditor(attributes) { 56 if (attributes.type === 'embed' || attributes.id) { 57 const editButton = document.createElement('button'); 58 editButton.className = 'diagrams-btn-edit'; 59 editButton.id = 'diagrams__btn-edit'; 60 editButton.innerText = LANG.plugins.diagrams.editButton; 61 editButton.type = 'button'; 62 this.$form.find('fieldset').prepend(editButton); 63 64 editButton.addEventListener('click', event => { 65 event.preventDefault(); // prevent form submission 66 67 if (attributes.type === 'mediafile') { 68 const diagramsEditor = new DiagramsEditor(this.onSavedMediaFile.bind(this, attributes.id)); 69 diagramsEditor.editMediaFile(attributes.id); 70 } else { 71 const diagramsEditor = new DiagramsEditor(); 72 diagramsEditor.editMemory(attributes.svg, this.onSaveEmbed.bind(this)); 73 } 74 }); 75 } 76 } 77 78 #getButtonsMediaManager(attributes) { 79 // media manager button 80 if (attributes.type === 'mediafile') { 81 const selectButton = document.createElement('button'); 82 selectButton.innerText = LANG.plugins.diagrams.selectSource; 83 selectButton.className = 'diagrams-btn-select'; 84 selectButton.type = 'button'; 85 selectButton.addEventListener('click', () => 86 window.open( 87 `${DOKU_BASE}lib/exe/mediamanager.php?ns=${encodeURIComponent(JSINFO.namespace)}&onselect=dMediaSelect`, 88 'mediaselect', 89 'width=750,height=500,left=20,top=20,scrollbars=yes,resizable=yes', 90 ) 91 ); 92 this.$form.find('fieldset').prepend(selectButton); 93 window.dMediaSelect = this.mediaSelect.bind(this); // register as global function 94 } 95 } 96 97 /** 98 * Define form fields depending on type 99 * @returns {object} 100 */ 101 static #getFields(attributes) { 102 const fields = [ 103 { 104 type: 'select', 'label': LANG.plugins.diagrams.alignment, 'name': 'align', 'options': 105 [ 106 {value: '', label: ''}, 107 {value: 'left', label: LANG.plugins.diagrams.left}, 108 {value: 'right', label: LANG.plugins.diagrams.right}, 109 {value: 'center', label: LANG.plugins.diagrams.center} 110 ] 111 }, 112 { 113 label: LANG.plugins.diagrams.title, name: 'title' 114 } 115 ]; 116 117 if (attributes.type === 'mediafile') { 118 fields.unshift( 119 { 120 label: LANG.plugins.diagrams.mediaSource, 121 name: 'id' 122 } 123 ); 124 } 125 return fields; 126 } 127 128 /** 129 * Updates the form to reflect the current internal attributes 130 */ 131 updateFormState() { 132 for (const [key, value] of Object.entries(this.#attributes)) { 133 this.$form.find('[name="' + key + '"]').val(value); 134 } 135 this.updateInternalUrl(); 136 } 137 138 /** 139 * Update the internal attributes to reflect the current form state 140 */ 141 updateInternalState() { 142 for (const [key, value] of Object.entries(this.#attributes)) { 143 const $elem = this.$form.find('[name="' + key + '"]'); 144 if ($elem.length) { 145 this.#attributes[key] = $elem.val(); 146 } 147 } 148 this.updateInternalUrl(); 149 } 150 151 /** 152 * Calculate the Display URL for the current mediafile or SVG embed 153 */ 154 updateInternalUrl() { 155 if (this.#attributes.type === 'embed') { 156 this.#attributes.url = 'data:image/svg+xml;base64,' + btoa(this.#attributes.svg); 157 } else { 158 this.#attributes.url = `${DOKU_BASE}lib/exe/fetch.php?media=${this.#attributes.id}`; 159 } 160 } 161 162 /** 163 * After svaing a media file reload the src for all images using it 164 * 165 * @see https://stackoverflow.com/a/66312176 166 * @param {string} mediaid 167 * @returns {Promise<void>} 168 */ 169 async onSavedMediaFile(mediaid) { 170 const url = `${DOKU_BASE}lib/exe/fetch.php?cache=nocache&media=${mediaid}`; 171 await fetch(url, {cache: 'reload', mode: 'no-cors'}); 172 document.body.querySelectorAll(`img[src='${url}']`) 173 .forEach(img => img.src = url) 174 } 175 176 /** 177 * Save an embedded diagram back to the editor 178 */ 179 onSaveEmbed(svg) { 180 this.#attributes.svg = svg; 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 * @todo if the given mediaid is not a diagram we need to show an error and ignore it 191 * @param {string} edid ignored 192 * @param {string} mediaid the picked media ID 193 */ 194 mediaSelect(edid, mediaid) { 195 this.#attributes.id = mediaid; 196 this.updateFormState(); 197 } 198} 199