xref: /plugin/imgpaste/script.js (revision 3f394551e5a223e256cdd0d0c5518ca0a6fd68c9)
1(function () {
2
3    /**
4     * Handle pasting of files
5     *
6     * @param {ClipboardEvent} e
7     */
8    function handlePaste(e) {
9        if (!document.getElementById('wiki__text')) return; // only when editing
10
11        const items = (e.clipboardData || e.originalEvent.clipboardData).items;
12
13        // When running prosemirror, check for HTML paste first
14        if (typeof window.proseMirrorIsActive !== 'undefined' && window.proseMirrorIsActive === true) {
15            for (let index in items) {
16                const item = items[index];
17                if (item.kind === 'string' && item.type === 'text/html') {
18                    e.preventDefault();
19                    e.stopPropagation();
20
21                    item.getAsString(async html => {
22                            html = await processHTML(html);
23                            const pm = window.Prosemirror.view;
24                            const parser = window.Prosemirror.classes.DOMParser.fromSchema(pm.state.schema);
25                            const nodes = parser.parse(html);
26                            pm.dispatch(pm.state.tr.replaceSelectionWith(nodes));
27                        }
28                    );
29
30                    return; // we found an HTML item, no need to continue
31                }
32            }
33        }
34
35        // if we're still here, handle files
36        for (let index in items) {
37            const item = items[index];
38
39            if (item.kind === 'file') {
40                const reader = new FileReader();
41                reader.onload = event => {
42                    uploadData(event.target.result);
43                };
44                reader.readAsDataURL(item.getAsFile());
45
46                // we had at least one file, prevent default
47                e.preventDefault();
48                e.stopPropagation();
49            }
50        }
51    }
52
53    /**
54     * Creates and shows the progress dialog
55     *
56     * @returns {HTMLDivElement}
57     */
58    function progressDialog() {
59        // create dialog
60        const offset = document.querySelectorAll('.plugin_imagepaste').length * 3;
61        const box = document.createElement('div');
62        box.className = 'plugin_imagepaste';
63        box.innerText = LANG.plugins.imgpaste.inprogress;
64        box.style.position = 'fixed';
65        box.style.top = offset + 'em';
66        box.style.left = '1em';
67        document.querySelector('.dokuwiki').append(box);
68        return box;
69    }
70
71    /**
72     * Processes the given HTML and downloads all images
73     *
74     * @param html
75     * @returns {Promise<HTMLDivElement>}
76     */
77    async function processHTML(html) {
78        const box = progressDialog();
79
80        const div = document.createElement('div');
81        div.innerHTML = html;
82        const imgs = Array.from(div.querySelectorAll('img'));
83        await Promise.all(imgs.map(async img => {
84            if (img.src.startsWith(DOKU_BASE)) return; // skip local images
85            if (!img.src.match(/^(https?:\/\/|data:)/i)) return; // we only handle http(s) and data URLs
86
87            try {
88                let result;
89                if (img.src.startsWith('data:')) {
90                    result = await uploadDataURL(img.src);
91                } else {
92                    result = await downloadData(img.src);
93                }
94
95                img.src = result.url;
96                img.className = 'media';
97                img.dataset.relid = getRelativeID(result.id);
98            } catch (e) {
99                console.error(e);
100            }
101        }));
102
103        box.remove();
104        return div;
105    }
106
107    /**
108     * Tell the backend to download the given URL and return the new ID
109     *
110     * @param {string} imgUrl
111     * @returns {Promise<object>} The JSON response
112     */
113    async function downloadData(imgUrl) {
114        const formData = new FormData();
115        formData.append('call', 'plugin_imgpaste');
116        formData.append('url', imgUrl);
117        formData.append('id', JSINFO.id);
118
119        const response = await fetch(
120            DOKU_BASE + 'lib/exe/ajax.php',
121            {
122                method: 'POST',
123                body: formData
124            }
125        );
126
127        if (!response.ok) {
128            throw new Error(response.statusText);
129        }
130
131        return await response.json();
132    }
133
134    /**
135     * Tell the backend to create a file from the given dataURL and return the new ID
136     *
137     * @param {string} dataURL
138     * @returns {Promise<object>} The JSON response
139     */
140    async function uploadDataURL(dataURL) {
141        const formData = new FormData();
142        formData.append('call', 'plugin_imgpaste');
143        formData.append('data', dataURL);
144        formData.append('id', JSINFO.id);
145
146        const response = await fetch(
147            DOKU_BASE + 'lib/exe/ajax.php',
148            {
149                method: 'POST',
150                body: formData
151            }
152        );
153
154        if (!response.ok) {
155            throw new Error(response.statusText);
156        }
157
158        return await response.json();
159    }
160
161    /**
162     * Uploads the given dataURL to the server and displays a progress dialog, inserting the syntax on success
163     *
164     * @param {string} dataURL
165     */
166    async function uploadData(dataURL) {
167        const box = progressDialog();
168
169        try {
170            const data = await uploadDataURL(dataURL);
171            box.classList.remove('info');
172            box.classList.add('success');
173            box.innerText = data.message;
174            setTimeout(() => {
175                box.remove();
176            }, 1000);
177            insertSyntax(data.id);
178        } catch (e) {
179            box.classList.remove('info');
180            box.classList.add('error');
181            box.innerText = e.message;
182            setTimeout(() => {
183                box.remove();
184            }, 1000);
185        }
186    }
187
188    /**
189     * Create a link ID for the given ID, preferrably relative to the current page
190     *
191     * @param {string} id
192     * @returns {string}
193     */
194    function getRelativeID(id) {
195        // TODO remove the "if" check after LinkWizard.createRelativeID() is available in stable (after Kaos)
196        if (typeof LinkWizard !== 'undefined' && typeof LinkWizard.createRelativeID === 'function') {
197            id = LinkWizard.createRelativeID(JSINFO.id, id);
198        } else {
199            id = ':' + id;
200        }
201        return id;
202    }
203
204    /**
205     * Inserts the given ID into the current editor
206     *
207     * @todo add support for other editors like CKEditor
208     * @param {string} id The newly uploaded file ID
209     */
210    function insertSyntax(id) {
211        id = getRelativeID(id);
212
213        if (typeof window.proseMirrorIsActive !== 'undefined' && window.proseMirrorIsActive === true) {
214            const pm = window.Prosemirror.view;
215            const imageNode = pm.state.schema.nodes.image.create({id: id});
216            pm.dispatch(pm.state.tr.replaceSelectionWith(imageNode));
217        } else {
218            insertAtCarret('wiki__text', '{{' + id + '}}');
219        }
220    }
221
222    // main
223    window.addEventListener('paste', handlePaste, true);
224
225})();
226