xref: /plugin/diagrams/script/DiagramsForm.js (revision ab4202fa8a6983f7bc07d26fd7b4cb517c5eca80)
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
137        if (this.#view.node.attrs.type === 'embed') {
138            attrs.url = this.#view.node.attrs.url; // keep the data uri
139        } else {
140            attrs.url = `${DOKU_BASE}lib/exe/fetch.php?cache=nocache&media=` + attrs.id;
141        }
142        return attrs;
143    }
144
145    /**
146     * After svaing a media file reload the src for all images using it
147     *
148     * @see https://stackoverflow.com/a/66312176
149     * @param {string} url
150     * @returns {Promise<void>}
151     */
152    async onSavedMediaFile(url) {
153        await fetch(url, {cache: 'reload', mode: 'no-cors'});
154        document.body.querySelectorAll(`img[src='${url}']`)
155            .forEach(img => img.src = url)
156    }
157
158    /**
159     * Save an embedded diagram back to the editor
160     *
161     * @todo if the nodeView is not set, we need to create a new one
162     */
163    onSaveEmbed(svg) {
164        this.#view.node.attrs.url = 'data:image/svg+xml;base64,' + btoa(svg);
165        this.updateViewFromForm();
166        return true;
167    }
168
169    /**
170     * Callback called from the media popup on selecting a file
171     *
172     * This is globally registered as window.dMediaSelect
173     *
174     * @todo if the given mediaid is not a diagram we need to show an error and ignore it
175     * @param {string} edid ignored
176     * @param {string} mediaid the picked media ID
177     */
178    mediaSelect(edid, mediaid) {
179        this.$form.find('[name="src"]').val(mediaid);
180    }
181}
182