1/** 2 * ProseMirror Form for editing diagram attribute 3 */ 4class DiagramsForm extends KeyValueForm { 5 6 /** {DiagramsForm} The singleton instance */ 7 static #instance = null; 8 9 /** {DiagramsView} The view of the currently selected node */ 10 #view = null; 11 12 /** 13 * Get the singleton instance of this class 14 * 15 * @returns {DiagramsForm} 16 */ 17 static getInstance() { 18 if (!this.#instance) { 19 this.#instance = new DiagramsForm(); 20 } 21 22 return this.#instance; 23 } 24 25 /** 26 * Initialize the KeyValue form with fields and event handlers 27 * 28 * @internal Use DiagramsForm.getInstance() instead 29 */ 30 constructor() { 31 const name = 'diagrams-form'; // FIXME localize 32 33 const fields = [ 34 { 35 label: LANG.plugins.diagrams.mediaSource, name: 'src' 36 }, 37 { 38 type: 'select', 'label': LANG.plugins.diagrams.alignment, 'options': 39 [ 40 {name: 'alignment', value: '', label: ''}, 41 {name: 'alignment', value: 'left', label: LANG.plugins.diagrams.left}, 42 {name: 'alignment', value: 'right', label: LANG.plugins.diagrams.right}, 43 {name: 'alignment', value: 'center', label: LANG.plugins.diagrams.center} 44 ] 45 }, 46 { 47 label: LANG.plugins.diagrams.title, name: 'title' 48 } 49 ]; 50 51 super(name, fields); 52 53 this.$form.on('submit', (event) => { 54 event.preventDefault(); // prevent form submission 55 this.updateViewFromForm(); 56 this.#view.deselectNode(); 57 }); 58 59 60 // media manager button 61 const selectButton = document.createElement('button'); 62 selectButton.innerText = LANG.plugins.diagrams.selectSource; 63 selectButton.className = 'diagrams-btn-select'; 64 selectButton.addEventListener('click', () => 65 window.open( 66 `${DOKU_BASE}lib/exe/mediamanager.php?ns=${encodeURIComponent(JSINFO.namespace)}&onselect=dMediaSelect`, 67 'mediaselect', 68 'width=750,height=500,left=20,top=20,scrollbars=yes,resizable=yes', 69 ) 70 ); 71 this.$form.find('fieldset').prepend(selectButton); 72 window.dMediaSelect = this.mediaSelect.bind(this); // register as global function 73 74 // editor button 75 const editButton = document.createElement('button'); 76 editButton.className = 'diagrams-btn-edit'; 77 editButton.id = 'diagrams__btn-edit'; 78 editButton.innerText = LANG.plugins.diagrams.editButton; 79 this.$form.find('fieldset').prepend(editButton); 80 81 editButton.addEventListener('click', event => { 82 event.preventDefault(); // prevent form submission 83 84 const url = this.#view.node.attrs.url; 85 const mediaid = this.#view.node.attrs.id; 86 87 if (this.#view.node.attrs.type === 'mediafile') { 88 const diagramsEditor = new DiagramsEditor(this.onSavedMediaFile.bind(this, url)); 89 diagramsEditor.editMediaFile(mediaid); 90 } else { 91 const diagramsEditor = new DiagramsEditor(); 92 diagramsEditor.editMemory(url, this.onSaveEmbed.bind(this)); 93 } 94 95 }); 96 } 97 98 /** 99 * Update the form to reflect the new selected nodeView 100 * 101 * @param {DiagramsView} view 102 */ 103 updateFormFromView(view) { 104 this.#view = view; 105 106 this.$form.find('[name="src"]').val(view.node.attrs.id); 107 this.$form.find('[name="title"]').val(view.node.attrs.title); 108 109 const align = view.node.attrs.align; 110 this.$form.find('[name="alignment"]').prop('selected', ''); 111 this.$form.find(`[name="alignment"][value="${align}"]`).prop('selected', 'selected'); 112 } 113 114 /** 115 * Update the nodeView to reflect the new form values 116 * 117 * @todo the nodeView might not be set, in that case we probably need to create a new one 118 */ 119 updateViewFromForm() { 120 const newAttrs = this.getAttributes(); 121 this.#view.dispatchNodeUpdate(newAttrs); 122 } 123 124 /** 125 * Construct a new attributes object from the current form values 126 * 127 * @returns {object} 128 */ 129 getAttributes() { 130 const attrs = {}; 131 attrs.id = this.$form.find('[name="src"]').val(); 132 attrs.align = this.$form.find('[name="alignment"]:selected').val(); 133 attrs.title = this.$form.find('[name="title"]').val(); 134 attrs.width = this.#view.node.attrs.width; 135 attrs.height = this.#view.node.attrs.height; 136 attrs.type = this.#view.node.attrs.type; 137 138 if (this.#view.node.attrs.type === 'embed') { 139 attrs.url = this.#view.node.attrs.url; // keep the data uri 140 } else { 141 attrs.url = `${DOKU_BASE}lib/exe/fetch.php?cache=nocache&media=` + attrs.id; 142 } 143 return attrs; 144 } 145 146 /** 147 * After svaing a media file reload the src for all images using it 148 * 149 * @see https://stackoverflow.com/a/66312176 150 * @param {string} url 151 * @returns {Promise<void>} 152 */ 153 async onSavedMediaFile(url) { 154 await fetch(url, {cache: 'reload', mode: 'no-cors'}); 155 document.body.querySelectorAll(`img[src='${url}']`) 156 .forEach(img => img.src = url) 157 } 158 159 /** 160 * Save an embedded diagram back to the editor 161 * 162 * @todo if the nodeView is not set, we need to create a new one 163 */ 164 onSaveEmbed(svg) { 165 this.#view.node.attrs.url = 'data:image/svg+xml;base64,' + btoa(svg); 166 this.updateViewFromForm(); 167 return true; 168 } 169 170 /** 171 * Callback called from the media popup on selecting a file 172 * 173 * This is globally registered as window.dMediaSelect 174 * 175 * @todo if the given mediaid is not a diagram we need to show an error and ignore it 176 * @param {string} edid ignored 177 * @param {string} mediaid the picked media ID 178 */ 179 mediaSelect(edid, mediaid) { 180 this.$form.find('[name="src"]').val(mediaid); 181 } 182} 183