'use strict'; let seq = 0; /** * this function digs the csrf token out of the form the textarea belongs to * * @param {HTMLFormElement} form * @return {string} */ export function csrf_for(form) { const input = form ? form.querySelector('input[name="csrf"]') : null; return input ? input.value : ''; } /** * this function inserts text at the textarea's caret (replacing any * selection) and fires an input event so vim mode / anything else resyncs * * @param {HTMLTextAreaElement} ta * @param {string} text * @return {void} */ export function insert_at_cursor(ta, text) { const start = ta.selectionStart; const end = ta.selectionEnd; if (typeof ta.setRangeText === 'function') { ta.setRangeText(text, start, end, 'end'); } else { ta.value = ta.value.slice(0, start) + text + ta.value.slice(end); ta.setSelectionRange(start + text.length, start + text.length); } ta.dispatchEvent(new Event('input', { bubbles: true })); } /** * this function swaps the first occurrence of a placeholder token in the * textarea for the final markdown (or for nothing if the upload failed) * * @param {HTMLTextAreaElement} ta * @param {string} token * @param {string} text * @return {void} */ export function replace_token(ta, token, text) { const i = ta.value.indexOf(token); if (i === -1) { return; } ta.value = ta.value.slice(0, i) + text + ta.value.slice(i + token.length); ta.dispatchEvent(new Event('input', { bubbles: true })); } /** * this function writes a message into the little status span next to the * post box * * @param {HTMLTextAreaElement} ta * @param {string} msg * @return {void} */ export function set_status(ta, msg) { const box = ta.parentNode.querySelector('.upload-status'); if (box) { box.textContent = msg; } } /** * this function uploads one image file. it drops an "uploading…" placeholder * in at the caret right away, posts the file to /upload, and then swaps the * placeholder for the real ![](url) markdown (or clears it and shows the * error on failure) * * @param {HTMLTextAreaElement} ta * @param {File} file * @return {void} */ export function upload_file(ta, file) { if (!file || file.type.indexOf('image/') !== 0) { return; } const token = '![uploading #' + (++seq) + '…]()'; insert_at_cursor(ta, token + '\n'); const data = new FormData(); data.append('csrf', csrf_for(ta.form)); data.append('image', file); window.fetch('/upload?json=1', { method: 'POST', body: data }) .then((r) => r.json().then((j) => ({ ok: r.ok, body: j }))) .then((res) => { if (res.ok && res.body.url) { replace_token(ta, token, '![](' + res.body.url + ')'); } else { replace_token(ta, token, ''); set_status(ta, res.body.error || 'Upload failed.'); } }) .catch(() => { replace_token(ta, token, ''); set_status(ta, 'Upload failed.'); }); } /** * this function hooks paste and drag-drop on a post box so dropping or * pasting an image uploads it * * @param {HTMLTextAreaElement} ta * @return {void} */ export function wire(ta) { ta.addEventListener('paste', (e) => { const items = e.clipboardData && e.clipboardData.items; if (!items) { return; } let handled = false; for (let i = 0; i < items.length; i++) { if (items[i].kind === 'file') { const file = items[i].getAsFile(); if (file && file.type.indexOf('image/') === 0) { upload_file(ta, file); handled = true; } } } if (handled) { e.preventDefault(); } }); ta.addEventListener('dragover', (e) => { e.preventDefault(); ta.classList.add('drag-over'); }); ta.addEventListener('dragleave', () => { ta.classList.remove('drag-over'); }); ta.addEventListener('drop', (e) => { e.preventDefault(); ta.classList.remove('drag-over'); const files = e.dataTransfer && e.dataTransfer.files; if (!files) { return; } for (let i = 0; i < files.length; i++) { upload_file(ta, files[i]); } }); } /** * this function wires up the post boxes for upload: paste/drop on the * textarea, plus the "attach image" link which falls back to the /upload * page when js is off and otherwise pops the file picker * * @return {void} */ function upload_init() { const areas = Array.prototype.slice.call(document.querySelectorAll('form .vim-area')); areas.forEach(wire); const buttons = Array.prototype.slice.call(document.querySelectorAll('[data-upload]')); buttons.forEach((btn) => { const form = btn.closest('form'); const ta = form ? form.querySelector('.vim-area') : null; const picker = form ? form.querySelector('.upload-input') : null; if (!ta || !picker) { return; } btn.addEventListener('click', (e) => { e.preventDefault(); picker.click(); }); picker.addEventListener('change', () => { const files = picker.files; for (let i = 0; i < files.length; i++) { upload_file(ta, files[i]); } picker.value = ''; }); }); } if (typeof document !== 'undefined' && document.querySelectorAll) { upload_init(); }