239 lines
5.9 KiB
JavaScript
239 lines
5.9 KiB
JavaScript
"use strict";
|
|
|
|
const socket = require("./socket");
|
|
const updateCursor = require("undate").update;
|
|
|
|
class Uploader {
|
|
init() {
|
|
this.xhr = null;
|
|
this.fileQueue = [];
|
|
this.overlay = document.getElementById("upload-overlay");
|
|
this.uploadInput = document.getElementById("upload-input");
|
|
this.uploadProgressbar = document.getElementById("upload-progressbar");
|
|
|
|
this.uploadInput.addEventListener("change", (e) => this.filesChanged(e));
|
|
document.addEventListener("dragenter", (e) => this.dragEnter(e));
|
|
document.addEventListener("dragover", (e) => this.dragOver(e));
|
|
document.addEventListener("dragleave", (e) => this.dragLeave(e));
|
|
document.addEventListener("drop", (e) => this.drop(e));
|
|
document.addEventListener("paste", (e) => this.paste(e));
|
|
}
|
|
|
|
dragOver(event) {
|
|
// Prevent dragover event completely and do nothing with it
|
|
// This stops the browser from trying to guess which cursor to show
|
|
event.preventDefault();
|
|
}
|
|
|
|
dragEnter(event) {
|
|
event.preventDefault();
|
|
|
|
// relatedTarget is the target where we entered the drag from
|
|
// when dragging from another window, the target is null, otherwise its a DOM element
|
|
if (!event.relatedTarget && event.dataTransfer.types.includes("Files")) {
|
|
this.overlay.classList.add("is-dragover");
|
|
}
|
|
}
|
|
|
|
dragLeave(event) {
|
|
event.preventDefault();
|
|
|
|
// If relatedTarget is null, that means we are no longer dragging over the page
|
|
if (!event.relatedTarget) {
|
|
this.overlay.classList.remove("is-dragover");
|
|
}
|
|
}
|
|
|
|
drop(event) {
|
|
event.preventDefault();
|
|
this.overlay.classList.remove("is-dragover");
|
|
|
|
let files;
|
|
|
|
if (event.dataTransfer.items) {
|
|
files = Array.from(event.dataTransfer.items)
|
|
.filter((item) => item.kind === "file")
|
|
.map((item) => item.getAsFile());
|
|
} else {
|
|
files = Array.from(event.dataTransfer.files);
|
|
}
|
|
|
|
this.triggerUpload(files);
|
|
}
|
|
|
|
paste(event) {
|
|
const items = event.clipboardData.items;
|
|
const files = [];
|
|
|
|
for (const item of items) {
|
|
if (item.kind === "file") {
|
|
files.push(item.getAsFile());
|
|
}
|
|
}
|
|
|
|
if (files.length === 0) {
|
|
return;
|
|
}
|
|
|
|
event.preventDefault();
|
|
this.triggerUpload(files);
|
|
}
|
|
|
|
filesChanged() {
|
|
const files = Array.from(this.uploadInput.files);
|
|
this.triggerUpload(files);
|
|
}
|
|
|
|
triggerUpload(files) {
|
|
if (!files.length) {
|
|
return;
|
|
}
|
|
|
|
const wasQueueEmpty = this.fileQueue.length === 0;
|
|
|
|
for (const file of files) {
|
|
if (this.maxFileSize > 0 && file.size > this.maxFileSize) {
|
|
this.handleResponse({
|
|
error: `File ${file.name} is over the maximum allowed size`,
|
|
});
|
|
|
|
continue;
|
|
}
|
|
|
|
this.fileQueue.push(file);
|
|
}
|
|
|
|
// if the queue was empty and we added some files to it, start uploading them
|
|
if (wasQueueEmpty && this.fileQueue.length > 0) {
|
|
this.dequeueNextFile();
|
|
}
|
|
}
|
|
|
|
dequeueNextFile() {
|
|
const file = this.fileQueue.shift();
|
|
|
|
// request an upload authentication token and then upload a file using said token
|
|
socket.emit("upload:auth");
|
|
socket.once("upload:auth", (token) => {
|
|
this.uploadSingleFile(file, token);
|
|
});
|
|
}
|
|
|
|
setProgress(value) {
|
|
this.uploadProgressbar.classList.toggle("upload-progressbar-visible", value > 0);
|
|
this.uploadProgressbar.style.width = value + "%";
|
|
}
|
|
|
|
uploadSingleFile(file, token) {
|
|
const xhr = new XMLHttpRequest();
|
|
this.xhr = xhr;
|
|
|
|
xhr.upload.addEventListener(
|
|
"progress",
|
|
(e) => {
|
|
const percent = Math.floor((e.loaded / e.total) * 1000) / 10;
|
|
this.setProgress(percent);
|
|
},
|
|
false
|
|
);
|
|
|
|
xhr.onreadystatechange = () => {
|
|
if (xhr.readyState === XMLHttpRequest.DONE) {
|
|
this.xhr = null;
|
|
|
|
let response;
|
|
|
|
try {
|
|
response = JSON.parse(xhr.responseText);
|
|
} catch (err) {
|
|
// This is just a safe guard and should not happen if server doesn't throw any errors.
|
|
// Browsers break the HTTP spec by aborting the request without reading any response data,
|
|
// if there is still data to be uploaded. Servers will only error in extreme cases like bad
|
|
// authentication or server-side errors.
|
|
response = {
|
|
error: `Upload aborted: HTTP ${xhr.status}`,
|
|
};
|
|
}
|
|
|
|
this.handleResponse(response);
|
|
|
|
// this file was processed, if we still have files in the queue, upload the next one
|
|
if (this.fileQueue.length > 0) {
|
|
this.dequeueNextFile();
|
|
}
|
|
}
|
|
};
|
|
|
|
const formData = new FormData();
|
|
formData.append("file", file);
|
|
xhr.open("POST", `uploads/new/${token}`);
|
|
xhr.send(formData);
|
|
}
|
|
|
|
handleResponse(response) {
|
|
this.setProgress(0);
|
|
|
|
if (response.error) {
|
|
// require here due to circular dependency
|
|
const {vueApp} = require("./vue");
|
|
vueApp.currentUserVisibleError = response.error;
|
|
return;
|
|
}
|
|
|
|
if (response.url) {
|
|
this.insertUploadUrl(response.url);
|
|
}
|
|
}
|
|
|
|
insertUploadUrl(url) {
|
|
const fullURL = new URL(url, location).toString();
|
|
const textbox = document.getElementById("input");
|
|
const initStart = textbox.selectionStart;
|
|
|
|
// Get the text before the cursor, and add a space if it's not in the beginning
|
|
const headToCursor = initStart > 0 ? textbox.value.substr(0, initStart) + " " : "";
|
|
|
|
// Get the remaining text after the cursor
|
|
const cursorToTail = textbox.value.substr(initStart);
|
|
|
|
// Construct the value until the point where we want the cursor to be
|
|
const textBeforeTail = headToCursor + fullURL + " ";
|
|
|
|
updateCursor(textbox, textBeforeTail + cursorToTail);
|
|
|
|
// Set the cursor after the link and a space
|
|
textbox.selectionStart = textbox.selectionEnd = textBeforeTail.length;
|
|
}
|
|
|
|
// TODO: This is a temporary hack while Vue porting is finalized
|
|
abort() {
|
|
this.fileQueue = [];
|
|
|
|
if (this.xhr) {
|
|
this.xhr.abort();
|
|
this.xhr = null;
|
|
}
|
|
}
|
|
}
|
|
|
|
const instance = new Uploader();
|
|
|
|
function initialize() {
|
|
instance.init();
|
|
return instance;
|
|
}
|
|
|
|
/**
|
|
* Called in the `configuration` socket event.
|
|
* Makes it so the user can be notified if a file is too large without waiting for the upload to finish server-side.
|
|
**/
|
|
function setMaxFileSize(kb) {
|
|
instance.maxFileSize = kb;
|
|
}
|
|
|
|
module.exports = {
|
|
abort: () => instance.abort(),
|
|
initialize,
|
|
setMaxFileSize,
|
|
};
|