// Requires base64.js /** * Sets up file / image uploading for Markdown content. * * @param eSubmits A list of elements of buttons to submit/save the page you're on. They * will be disabled and tell users files are uploading while uploading is * happening. * @param eFileInput The `` * @param eUploadBar Usually looks like * ``` *
*
* Upload files by dragging & dropping, pasting, or them. *
*
*
*
*
*
* ``` * @param eText The text field that can be dropped into and is editing the markdown. * @param doMarkdown The function returned by `initLiveMarkdown`. * @param maxFileSize The max allowed file size in bytes. * @param uploadUrl The URL to POST assets to (unique per project to avoid CORS issues). */ function setupMarkdownUpload(eSubmits, eFileInput, eUploadBar, eText, doMarkdown, maxFileSize, uploadUrl) { const submitTexts = Array.from(eSubmits).map(e => e.value); const uploadProgress = eUploadBar.querySelector('.progress'); const uploadProgressText = eUploadBar.querySelector('.progress_text'); const uploadProgressBar = eUploadBar.querySelector('.progress_bar'); const uploadProgressBarFill = eUploadBar.querySelector('.progress_bar > div'); let fileCounter = 0; let enterCounter = 0; let uploadQueue = []; let currentUpload = null; let currentXhr = null; let currentBatchSize = 0; let currentBatchDone = 0; eFileInput.addEventListener("change", function(ev) { if (eFileInput.files.length > 0) { importUserFiles(eFileInput.files); } }); eText.addEventListener("dragover", function(ev) { let effect = "none"; for (let i = 0; i < ev.dataTransfer.items.length; ++i) { if (ev.dataTransfer.items[i].kind.toLowerCase() == "file") { effect = "copy"; break; } } ev.dataTransfer.dropEffect = effect; ev.preventDefault(); }); eText.addEventListener("dragenter", function(ev) { enterCounter++; let droppable = false; for (let i = 0; i < ev.dataTransfer.items.length; ++i) { if (ev.dataTransfer.items[i].kind.toLowerCase() == "file") { droppable = true; break; } } if (droppable) { eText.classList.add("drop"); } }); eText.addEventListener("dragleave", function(ev) { enterCounter--; if (enterCounter == 0) { eText.classList.remove("drop"); } }); function makeUploadString(uploadNumber, filename) { return `Uploading file #${uploadNumber}: \`${filename}\`...`; } eText.addEventListener("drop", function(ev) { enterCounter = 0; eText.classList.remove("drop"); if (ev.dataTransfer && ev.dataTransfer.files) { importUserFiles(ev.dataTransfer.files) } ev.preventDefault(); }); eText.addEventListener("paste", function(ev) { const files = ev.clipboardData?.files ?? []; if (files.length > 0) { importUserFiles(files) } }); function importUserFiles(files) { let items = []; for (let i = 0; i < files.length; ++i) { let f = files[i]; if (f.size < maxFileSize) { items.push({ file: f, error: null }); } else { items.push({ file: null, error: `\`${f.name}\` is too big! Max size is ${maxFileSize} but the file is ${f.size}.` }); } } let cursorStart = eText.selectionStart; let cursorEnd = eText.selectionEnd; let toInsert = ""; let linesToCursor = eText.value.substr(0, cursorStart).split("\n"); let cursorLine = linesToCursor[linesToCursor.length-1].trim(); if (cursorLine.length > 0) { toInsert = "\n\n"; } for (let i = 0; i < items.length; ++i) { if (items[i].file) { fileCounter++; toInsert += makeUploadString(fileCounter, items[i].file.name) + "\n\n"; queueUpload(fileCounter, items[i].file); } else { toInsert += `${items[i].error}\n\n`; } } eText.value = eText.value.substring(0, cursorStart) + toInsert + eText.value.substring(cursorEnd, eText.value.length); doMarkdown(); uploadNext(); } function replaceUploadString(upload, newString) { let cursorStart = eText.selectionStart; let cursorEnd = eText.selectionEnd; let uploadString = makeUploadString(upload.uploadNumber, upload.file.name); let insertIndex = eText.value.indexOf(uploadString) eText.value = eText.value.replace(uploadString, newString); if (cursorStart <= insertIndex + uploadString.length) { eText.selectionStart = cursorStart; } else { eText.selectionStart = cursorStart - uploadString.length + newString.length; } if (cursorEnd <= insertIndex + uploadString.length) { eText.selectionEnd = cursorEnd; } else { eText.selectionEnd = cursorEnd - uploadString.length + newString.length; } doMarkdown(); } function replaceUploadStringError(upload) { replaceUploadString(upload, `There was a problem uploading your file \`${upload.file.name}\`.`); } function queueUpload(uploadNumber, file) { uploadQueue.push({ uploadNumber: uploadNumber, file: file }); currentBatchSize++; uploadProgressText.textContent = `Uploading files ${currentBatchDone+1}/${currentBatchSize}`; } function uploadDone(ev) { try { if (currentXhr.status == 200 && currentXhr.response) { if (currentXhr.response.url) { let url = currentXhr.response.url; let newString = `[${currentUpload.file.name}](${url})`; if (currentXhr.response.mime.startsWith("image")) { newString = "!" + newString; } replaceUploadString(currentUpload, newString); } else if (currentXhr.response.error) { replaceUploadString(currentUpload, `Upload failed for \`${currentUpload.file.name}\`: ${currentXhr.response.error}.`); } else { replaceUploadStringError(currentUpload); } } else { replaceUploadStringError(currentUpload); } } catch (err) { console.error(err); replaceUploadStringError(currentUpload); } currentUpload = null; currentXhr = null; currentBatchDone++; uploadNext(); } function updateUploadProgress(ev) { if (ev.lengthComputable) { let progress = ev.loaded / ev.total; uploadProgressBarFill.style.width = Math.floor(progress * 100) + "%"; } } function uploadNext() { if (currentUpload == null) { next = uploadQueue.shift(); if (next) { uploadProgressText.textContent = `Uploading files ${currentBatchDone+1}/${currentBatchSize}`; eUploadBar.classList.add("uploading"); uploadProgressBarFill.style.width = "0%"; for (const e of eSubmits) { e.disabled = true; e.value = "Uploading files..."; } try { let utf8Filename = strToUTF8Arr(next.file.name); let base64Filename = base64EncArr(utf8Filename); // NOTE(asaf): We use XHR because fetch can't do upload progress reports. Womp womp. https://youtu.be/Pubd-spHN-0?t=2 currentXhr = new XMLHttpRequest(); currentXhr.upload.addEventListener("progress", updateUploadProgress); currentXhr.open("POST", uploadUrl, true); currentXhr.setRequestHeader("Hmn-Upload-Filename", base64Filename); currentXhr.responseType = "json"; currentXhr.addEventListener("loadend", uploadDone); currentXhr.send(next.file); currentUpload = next; } catch (err) { replaceUploadStringError(next); console.error(err); uploadNext(); } } else { for (const [i, e] of eSubmits.entries()) { e.disabled = false; e.value = submitTexts[i]; } eUploadBar.classList.remove("uploading"); currentBatchSize = 0; currentBatchDone = 0; } } } }