forums.tonybtw.com

https://git.tonybtw.com/forums.tonybtw.com.git git://git.tonybtw.com/forums.tonybtw.com.git
5,609 bytes raw
1
'use strict';
2
3
let seq = 0;
4
5
/**
6
 * this function digs the csrf token out of the form the textarea belongs to
7
 *
8
 * @param {HTMLFormElement} form
9
 * @return {string}
10
 */
11
export function csrf_for(form) {
12
    const input = form ? form.querySelector('input[name="csrf"]') : null;
13
    return input ? input.value : '';
14
}
15
16
/**
17
 * this function inserts text at the textarea's caret (replacing any
18
 * selection) and fires an input event so vim mode / anything else resyncs
19
 *
20
 * @param {HTMLTextAreaElement} ta
21
 * @param {string} text
22
 * @return {void}
23
 */
24
export function insert_at_cursor(ta, text) {
25
    const start = ta.selectionStart;
26
    const end = ta.selectionEnd;
27
    if (typeof ta.setRangeText === 'function') {
28
        ta.setRangeText(text, start, end, 'end');
29
    } else {
30
        ta.value = ta.value.slice(0, start) + text + ta.value.slice(end);
31
        ta.setSelectionRange(start + text.length, start + text.length);
32
    }
33
    ta.dispatchEvent(new Event('input', { bubbles: true }));
34
}
35
36
/**
37
 * this function swaps the first occurrence of a placeholder token in the
38
 * textarea for the final markdown (or for nothing if the upload failed)
39
 *
40
 * @param {HTMLTextAreaElement} ta
41
 * @param {string} token
42
 * @param {string} text
43
 * @return {void}
44
 */
45
export function replace_token(ta, token, text) {
46
    const i = ta.value.indexOf(token);
47
    if (i === -1) {
48
        return;
49
    }
50
    ta.value = ta.value.slice(0, i) + text + ta.value.slice(i + token.length);
51
    ta.dispatchEvent(new Event('input', { bubbles: true }));
52
}
53
54
/**
55
 * this function writes a message into the little status span next to the
56
 * post box
57
 *
58
 * @param {HTMLTextAreaElement} ta
59
 * @param {string} msg
60
 * @return {void}
61
 */
62
export function set_status(ta, msg) {
63
    const box = ta.parentNode.querySelector('.upload-status');
64
    if (box) {
65
        box.textContent = msg;
66
    }
67
}
68
69
/**
70
 * this function uploads one image file. it drops an "uploading…" placeholder
71
 * in at the caret right away, posts the file to /upload, and then swaps the
72
 * placeholder for the real ![](url) markdown (or clears it and shows the
73
 * error on failure)
74
 *
75
 * @param {HTMLTextAreaElement} ta
76
 * @param {File} file
77
 * @return {void}
78
 */
79
export function upload_file(ta, file) {
80
    if (!file || file.type.indexOf('image/') !== 0) {
81
        return;
82
    }
83
    const token = '![uploading #' + (++seq) + '…]()';
84
    insert_at_cursor(ta, token + '\n');
85
86
    const data = new FormData();
87
    data.append('csrf', csrf_for(ta.form));
88
    data.append('image', file);
89
90
    window.fetch('/upload?json=1', { method: 'POST', body: data })
91
        .then((r) => r.json().then((j) => ({ ok: r.ok, body: j })))
92
        .then((res) => {
93
            if (res.ok && res.body.url) {
94
                replace_token(ta, token, '![](' + res.body.url + ')');
95
            } else {
96
                replace_token(ta, token, '');
97
                set_status(ta, res.body.error || 'Upload failed.');
98
            }
99
        })
100
        .catch(() => {
101
            replace_token(ta, token, '');
102
            set_status(ta, 'Upload failed.');
103
        });
104
}
105
106
/**
107
 * this function hooks paste and drag-drop on a post box so dropping or
108
 * pasting an image uploads it
109
 *
110
 * @param {HTMLTextAreaElement} ta
111
 * @return {void}
112
 */
113
export function wire(ta) {
114
    ta.addEventListener('paste', (e) => {
115
        const items = e.clipboardData && e.clipboardData.items;
116
        if (!items) {
117
            return;
118
        }
119
        let handled = false;
120
        for (let i = 0; i < items.length; i++) {
121
            if (items[i].kind === 'file') {
122
                const file = items[i].getAsFile();
123
                if (file && file.type.indexOf('image/') === 0) {
124
                    upload_file(ta, file);
125
                    handled = true;
126
                }
127
            }
128
        }
129
        if (handled) {
130
            e.preventDefault();
131
        }
132
    });
133
134
    ta.addEventListener('dragover', (e) => {
135
        e.preventDefault();
136
        ta.classList.add('drag-over');
137
    });
138
    ta.addEventListener('dragleave', () => {
139
        ta.classList.remove('drag-over');
140
    });
141
    ta.addEventListener('drop', (e) => {
142
        e.preventDefault();
143
        ta.classList.remove('drag-over');
144
        const files = e.dataTransfer && e.dataTransfer.files;
145
        if (!files) {
146
            return;
147
        }
148
        for (let i = 0; i < files.length; i++) {
149
            upload_file(ta, files[i]);
150
        }
151
    });
152
}
153
154
/**
155
 * this function wires up the post boxes for upload: paste/drop on the
156
 * textarea, plus the "attach image" link which falls back to the /upload
157
 * page when js is off and otherwise pops the file picker
158
 *
159
 * @return {void}
160
 */
161
function upload_init() {
162
    const areas = Array.prototype.slice.call(document.querySelectorAll('form .vim-area'));
163
    areas.forEach(wire);
164
165
    const buttons = Array.prototype.slice.call(document.querySelectorAll('[data-upload]'));
166
    buttons.forEach((btn) => {
167
        const form = btn.closest('form');
168
        const ta = form ? form.querySelector('.vim-area') : null;
169
        const picker = form ? form.querySelector('.upload-input') : null;
170
        if (!ta || !picker) {
171
            return;
172
        }
173
        btn.addEventListener('click', (e) => {
174
            e.preventDefault();
175
            picker.click();
176
        });
177
        picker.addEventListener('change', () => {
178
            const files = picker.files;
179
            for (let i = 0; i < files.length; i++) {
180
                upload_file(ta, files[i]);
181
            }
182
            picker.value = '';
183
        });
184
    });
185
}
186
187
if (typeof document !== 'undefined' && document.querySelectorAll) {
188
    upload_init();
189
}