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