/* Requires base64.js Usage: setupMarkdownUpload(eSubmit, eFileInput, eUploadBar, eText, doMarkdown, maxFileSize, uploadUrl) eSubmit is the element of the button to submit/save the markdown changes. It will be disabled and tell users files are uploading while uploading is happening. eFileInput is the eUploadBar usually looks like
Upload files by dragging & dropping, pasting, or them.
eText is the text field that can be dropped into and is editing the markdown. doMarkdown is the function returned by initLiveMarkdown. maxFileSize uploadUrl */ function setupMarkdownUpload(eSubmit, eFileInput, eUploadBar, eText, doMarkdown, maxFileSize, uploadUrl) { const submitText = eSubmit.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%"; eSubmit.disabled = true; eSubmit.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 { eSubmit.disabled = false; eSubmit.value = submitText; eUploadBar.classList.remove("uploading"); currentBatchSize = 0; currentBatchDone = 0; } } } }