| 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  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, '');
|
| 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 |
}
|