/** * Client runtime for `
` 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 `
` fallback if you wrap it): *
* * 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(); })(); `;