(() => {
const isTypingTarget = (el) => {
if (!el) return false;
const tag = el.tagName;
if (tag === "TEXTAREA") return true;
if (tag === "INPUT") {
const t = el.type;
return t !== "file" && t !== "checkbox" && t !== "radio" && t !== "button" && t !== "submit";
}
return !!el.isContentEditable;
};
const extFor = (type) => {
if (!type) return "bin";
const slash = type.indexOf("/");
return slash >= 0 ? type.slice(slash + 1).split("+")[0] : "bin";
};
const pastedName = (blob) => {
if (blob.name && blob.name !== "image.png") return blob;
return new File([blob], `pasted-${Date.now()}.${extFor(blob.type)}`, { type: blob.type });
};
// Toast — created on demand, auto-removes after a few seconds.
const showToast = (msg, opts = {}) => {
let host = document.getElementById("dms-toast-host");
if (!host) {
host = document.createElement("div");
host.id = "dms-toast-host";
host.className = "toast toast-end z-50";
document.body.appendChild(host);
}
const alert = document.createElement("div");
const kind = opts.kind || "info";
alert.className = `alert alert-${kind} shadow-lg`;
if (opts.href) {
const a = document.createElement("a");
a.href = opts.href;
a.className = "underline";
a.textContent = msg;
alert.appendChild(a);
} else {
const span = document.createElement("span");
span.textContent = msg;
alert.appendChild(span);
}
host.appendChild(alert);
const ttl = opts.ttl ?? 4000;
if (ttl > 0) setTimeout(() => alert.remove(), ttl);
return alert;
};
// Path 1: a quick-upload form is on this page — attach the pasted file to it.
const attachToQuickForm = (file) => {
const fileInput = document.getElementById("quick-upload-file");
const preview = document.getElementById("quick-upload-preview");
if (!fileInput) return false;
const dt = new DataTransfer();
dt.items.add(file);
fileInput.files = dt.files;
if (preview) {
const img = preview.querySelector("img");
const name = preview.querySelector("[data-name]");
if (img) {
if (img.src) URL.revokeObjectURL(img.src);
img.src = URL.createObjectURL(file);
}
if (name) name.textContent = `${file.name} · ${(file.size / 1024).toFixed(1)} KB`;
preview.classList.remove("hidden");
preview.classList.add("flex");
}
fileInput.scrollIntoView({ behavior: "smooth", block: "center" });
return true;
};
// Path 2: anywhere else on the site — POST it directly and surface the result.
const uploadDirect = async (file) => {
const fd = new FormData();
fd.append("file", file, file.name);
const status = showToast(`Uploading ${file.name}…`, { kind: "info", ttl: 0 });
try {
const res = await fetch("/uploads", { method: "POST", body: fd, credentials: "same-origin" });
status.remove();
if (res.redirected && res.url.includes("/login")) {
showToast("Sign in to upload.", { kind: "warning" });
return;
}
if (!res.ok) {
showToast(`Upload failed (${res.status}).`, { kind: "error" });
return;
}
const href = res.redirected ? res.url : "/uploads";
showToast(`Uploaded ${file.name} — view`, { kind: "success", href });
} catch (e) {
status.remove();
showToast(`Upload error: ${e.message || e}`, { kind: "error" });
}
};
// Fetch a pasted link server-side. Going through the server keeps the
// original bytes (animated GIFs, videos) that a clipboard image-paste would
// otherwise flatten into a single-frame PNG.
const uploadUrl = async (url) => {
const status = showToast(`Fetching ${url}…`, { kind: "info", ttl: 0 });
try {
const res = await fetch("/uploads/url", {
method: "POST",
headers: { "Content-Type": "application/x-www-form-urlencoded" },
body: new URLSearchParams({ url }),
credentials: "same-origin",
});
status.remove();
if (res.redirected && res.url.includes("/login")) {
showToast("Sign in to upload.", { kind: "warning" });
return;
}
if (!res.ok) {
showToast(`Fetch failed (${res.status}).`, { kind: "error" });
return;
}
const href = res.redirected ? res.url : "/uploads";
showToast("Fetched link — view", { kind: "success", href });
} catch (e) {
status.remove();
showToast(`Fetch error: ${e.message || e}`, { kind: "error" });
}
};
const looksLikeUrl = (text) => /^https?:\/\/\S+$/i.test((text || "").trim());
document.addEventListener("paste", (e) => {
if (!document.body.dataset.authed) return;
if (isTypingTarget(e.target)) return;
const cd = e.clipboardData;
const items = cd && cd.items;
if (!items) return;
for (const it of items) {
if (it.kind !== "file") continue;
const blob = it.getAsFile();
if (!blob || !blob.type.startsWith("image/")) continue;
const file = pastedName(blob);
e.preventDefault();
if (!attachToQuickForm(file)) uploadDirect(file);
return;
}
// No file on the clipboard — fall back to a pasted link.
const text = cd.getData && cd.getData("text/plain");
if (looksLikeUrl(text)) {
e.preventDefault();
uploadUrl(text.trim());
}
});
// When the quick-upload form is on the page, also preview manual file picks.
const quickFile = document.getElementById("quick-upload-file");
const quickPreview = document.getElementById("quick-upload-preview");
if (quickFile && quickPreview) {
const img = quickPreview.querySelector("img");
const name = quickPreview.querySelector("[data-name]");
quickFile.addEventListener("change", () => {
const f = quickFile.files && quickFile.files[0];
if (!f || !f.type.startsWith("image/")) {
quickPreview.classList.add("hidden");
quickPreview.classList.remove("flex");
return;
}
if (img) {
if (img.src) URL.revokeObjectURL(img.src);
img.src = URL.createObjectURL(f);
}
if (name) name.textContent = `${f.name} · ${(f.size / 1024).toFixed(1)} KB`;
quickPreview.classList.remove("hidden");
quickPreview.classList.add("flex");
});
}
})();
⭐
Log in to mark this repository.
6.2 KiB · text
History
2e5cc52