Files
WRNexusJS/packages/uploader/src/runtime.ts
T
Clintchiz 4cebacadfe
Quality / quality (ubuntu-latest) (push) Failing after 12m9s
Quality / quality (windows-latest) (push) Canceled after 0s
release: WRNexusJS 0.8.3
2026-08-03 19:47:30 +05:30

212 lines
9.6 KiB
TypeScript

/**
* Client runtime for `<div data-uploader>` elements — drag-and-drop + file
* input, per-file progress bars, and success/failed states. Injected by
* `collectScripts` only on pages that contain `data-uploader` (same mechanism as
* `validate.js`). Self-contained: it injects its own themed stylesheet (using
* `--wire-*` tokens) and posts each file via XHR so upload progress is live.
*
* Markup it enhances (also a valid no-JS `<form>` fallback if you wrap it):
* <div data-uploader="public" data-endpoint="/api/upload"
* data-accept="image/*" data-max="10000000" data-multiple></div>
*
* Events dispatched on the element (bubble):
* wrnexus:upload detail: { file, result: { key, url, name, size, type } }
* wrnexus:upload-error detail: { file, error }
*
* NOTE: written with single/double quotes + string concatenation only — no
* backticks and no ${...}, so it embeds safely in the exported template string.
*/
export const UPLOAD_JS_HREF = "/__wrnexus/uploader.js";
export const UPLOAD_RUNTIME = `
(function () {
if (typeof document === "undefined") return;
var CSRF_COOKIE = "wire-csrf";
var CSS =
".wire-uploader{display:block}" +
".wire-uploader-zone{display:flex;align-items:center;justify-content:center;text-align:center;" +
"min-height:8rem;padding:1.25rem;border:2px dashed var(--wire-border,#cbd5e1);border-radius:12px;" +
"background:var(--wire-surface,transparent);color:var(--wire-muted,#64748b);cursor:pointer;" +
"transition:border-color .15s ease,background-color .15s ease;position:relative}" +
".wire-uploader-zone:hover,.wire-uploader-zone:focus-visible{border-color:var(--wire-brand,#3f7dff);outline:none}" +
".wire-uploader-zone.is-drag{border-color:var(--wire-brand,#3f7dff);background:color-mix(in oklab,var(--wire-brand,#3f7dff) 8%,transparent)}" +
".wire-uploader-prompt{font-size:.9rem;pointer-events:none}" +
".wire-uploader-input{position:absolute;inset:0;width:100%;height:100%;opacity:0;cursor:pointer}" +
".wire-uploader-list{list-style:none;margin:.75rem 0 0;padding:0;display:flex;flex-direction:column;gap:.5rem}" +
".wire-uploader-item{display:grid;grid-template-columns:1fr auto;gap:.15rem .75rem;align-items:center;" +
"font-size:.82rem;padding:.5rem .7rem;border:1px solid var(--wire-border,#e2e8f0);border-radius:8px}" +
".wire-uploader-name{font-weight:500;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;color:var(--wire-text,#0f172a)}" +
".wire-uploader-meta{color:var(--wire-muted,#94a3b8);font-variant-numeric:tabular-nums}" +
".wire-uploader-bar{grid-column:1/-1;height:5px;border-radius:999px;background:var(--wire-border,#e2e8f0);overflow:hidden}" +
".wire-uploader-fill{height:100%;width:0;border-radius:999px;background:var(--wire-brand,#3f7dff);transition:width .15s ease}" +
".wire-uploader-status{grid-column:1/-1;font-size:.75rem;color:var(--wire-muted,#94a3b8);font-variant-numeric:tabular-nums}" +
".wire-uploader-item.is-done .wire-uploader-fill{background:var(--wire-success,#16a34a)}" +
".wire-uploader-item.is-done .wire-uploader-status{color:var(--wire-success,#16a34a)}" +
".wire-uploader-item.is-error .wire-uploader-fill{background:var(--wire-danger,#dc2626)}" +
".wire-uploader-item.is-error .wire-uploader-status{color:var(--wire-danger,#dc2626)}";
function injectCss() {
if (document.getElementById("wire-uploader-css")) return;
var s = document.createElement("style");
s.id = "wire-uploader-css";
s.textContent = CSS;
document.head.appendChild(s);
}
function cookie(name) {
var m = document.cookie.match(new RegExp("(?:^|; )" + name + "=([^;]*)"));
return m ? decodeURIComponent(m[1]) : "";
}
function el(tag, cls, text) {
var e = document.createElement(tag);
if (cls) e.className = cls;
if (text != null) e.textContent = text;
return e;
}
function fmt(n) {
if (n < 1024) return n + " B";
if (n < 1048576) return (n / 1024).toFixed(1) + " KB";
return (n / 1048576).toFixed(1) + " MB";
}
function accepts(accept, file) {
var list = (accept || "").split(",").map(function (s) { return s.trim().toLowerCase(); }).filter(Boolean);
if (!list.length) return true;
var type = (file.type || "").toLowerCase();
var name = (file.name || "").toLowerCase();
var ext = name.indexOf(".") >= 0 ? name.slice(name.lastIndexOf(".")) : "";
return list.some(function (rule) {
if (rule.charAt(0) === ".") return rule === ext;
if (rule.slice(-2) === "/*") return type.indexOf(rule.slice(0, -1)) === 0;
return rule === type;
});
}
function setup(root) {
if (root.__wrnexusUploader) return;
root.__wrnexusUploader = true;
var endpoint = root.getAttribute("data-endpoint") || "/api/upload";
var multipleAttr = root.getAttribute("data-multiple");
var multiple = root.hasAttribute("data-multiple") && multipleAttr !== "false";
var accept = root.getAttribute("data-accept") || "";
var maxBytes = parseInt(root.getAttribute("data-max") || "0", 10) || 0;
var field = root.getAttribute("data-field") || (multiple ? "files" : "file");
var invalidate = String(root.getAttribute("data-invalidate") || "")
.split(",")
.map(function (tag) { return tag.trim(); })
.filter(Boolean);
var promptText = root.getAttribute("data-label") || "Drag files here or click to browse";
root.classList.add("wire-uploader");
var zone = el("div", "wire-uploader-zone");
zone.setAttribute("role", "button");
zone.setAttribute("tabindex", "0");
zone.appendChild(el("div", "wire-uploader-prompt", promptText));
var input = document.createElement("input");
input.type = "file";
input.className = "wire-uploader-input";
if (multiple) input.multiple = true;
if (accept) input.accept = accept;
zone.appendChild(input);
var listEl = el("ul", "wire-uploader-list");
root.appendChild(zone);
root.appendChild(listEl);
zone.addEventListener("keydown", function (e) {
if (e.key === "Enter" || e.key === " ") { e.preventDefault(); input.click(); }
});
["dragenter", "dragover"].forEach(function (ev) {
zone.addEventListener(ev, function (e) { e.preventDefault(); zone.classList.add("is-drag"); });
});
["dragleave", "drop"].forEach(function (ev) {
zone.addEventListener(ev, function (e) { e.preventDefault(); zone.classList.remove("is-drag"); });
});
zone.addEventListener("drop", function (e) {
if (e.dataTransfer && e.dataTransfer.files) handle(e.dataTransfer.files);
});
input.addEventListener("change", function () {
if (input.files) handle(input.files);
input.value = "";
});
function handle(files) {
var arr = Array.prototype.slice.call(files);
if (!multiple) arr = arr.slice(0, 1);
arr.forEach(uploadOne);
}
function row(file) {
var li = el("li", "wire-uploader-item");
li.appendChild(el("span", "wire-uploader-name", file.name));
li.appendChild(el("span", "wire-uploader-meta", fmt(file.size)));
var bar = el("div", "wire-uploader-bar");
var fill = el("div", "wire-uploader-fill");
bar.appendChild(fill);
li.appendChild(bar);
var status = el("span", "wire-uploader-status", "");
li.appendChild(status);
listEl.appendChild(li);
return { li: li, fill: fill, status: status };
}
function uploadOne(file) {
var ui = row(file);
if (maxBytes && file.size > maxBytes) return fail(ui, "Too large (max " + fmt(maxBytes) + ")", file);
if (!accepts(accept, file)) return fail(ui, "Type not allowed", file);
var fd = new FormData();
fd.append(field, file, file.name);
var xhr = new XMLHttpRequest();
xhr.open("POST", endpoint, true);
var token = cookie(CSRF_COOKIE);
if (token) xhr.setRequestHeader("x-csrf-token", token);
xhr.upload.addEventListener("progress", function (e) {
if (e.lengthComputable) {
var pct = Math.round((e.loaded / e.total) * 100);
ui.fill.style.width = pct + "%";
ui.status.textContent = pct + "%";
}
});
xhr.addEventListener("load", function () {
var data = null;
try { data = JSON.parse(xhr.responseText); } catch (e2) {}
if (xhr.status >= 200 && xhr.status < 300 && data && data.ok) {
done(ui, (data.files && data.files[0]) || null, file);
} else {
fail(ui, (data && data.error) || ("Upload failed (" + xhr.status + ")"), file);
}
});
xhr.addEventListener("error", function () { fail(ui, "Network error", file); });
xhr.send(fd);
}
function done(ui, info, file) {
ui.li.classList.remove("is-error");
ui.li.classList.add("is-done");
ui.fill.style.width = "100%";
ui.status.textContent = "\\u2713 Uploaded";
root.dispatchEvent(new CustomEvent("wrnexus:upload", { bubbles: true, detail: { file: file, result: info } }));
if (invalidate.length) {
window.dispatchEvent(new CustomEvent("wrnexus:cache:invalidate", {
detail: { tags: invalidate, source: "uploader", file: file, result: info }
}));
}
}
function fail(ui, msg, file) {
ui.li.classList.add("is-error");
ui.status.textContent = "\\u2717 " + msg;
root.dispatchEvent(new CustomEvent("wrnexus:upload-error", { bubbles: true, detail: { file: file, error: msg } }));
}
}
function init() {
injectCss();
var nodes = document.querySelectorAll("[data-uploader]");
for (var i = 0; i < nodes.length; i++) setup(nodes[i]);
}
if (document.readyState === "loading") document.addEventListener("DOMContentLoaded", init);
else init();
})();
`;