feat: add profile language preferences and refine toast, dropdown and history UI
This commit is contained in:
+358
-71
@@ -1,109 +1,386 @@
|
||||
/*
|
||||
* Copyright (C) 2026 Mateusz Gruszczyński @linuxiarz.pl
|
||||
* Source-Available Code / Dual-Licensed.
|
||||
*
|
||||
*
|
||||
* Free for non-commercial and evaluation use under terms of BSL/GPLv3.
|
||||
* Commercial or production use requires a valid paid license.
|
||||
* See LICENSE file in repository root for details.
|
||||
*/
|
||||
|
||||
let hideTimer;
|
||||
import { formatNumber, t, translateSource } from "@rustpad/i18n";
|
||||
|
||||
function toastElement(selector = "#toast") {
|
||||
return document.querySelector(selector);
|
||||
const TOAST_TYPES = new Set(["info", "success", "warning", "danger"]);
|
||||
const DEFAULT_TITLE_KEYS = {
|
||||
info: "toast.title.info",
|
||||
success: "toast.title.success",
|
||||
warning: "toast.title.warning",
|
||||
danger: "toast.title.danger",
|
||||
};
|
||||
|
||||
function defaultTitle(type) {
|
||||
const normalized = normalizeType(type);
|
||||
return t(DEFAULT_TITLE_KEYS[normalized], {}, { info: "Information", success: "Success", warning: "Warning", danger: "Something went wrong" }[normalized]);
|
||||
}
|
||||
const DEFAULT_DURATIONS = {
|
||||
info: 4200,
|
||||
success: 3800,
|
||||
warning: 5600,
|
||||
danger: 6500,
|
||||
};
|
||||
const MAX_VISIBLE_TOASTS = 4;
|
||||
const FLASH_TOAST_STORAGE_KEY = "rustpad:toast:flash";
|
||||
const MODAL_REGION_CLASS = "toast-region--modal";
|
||||
const modalRegionBindings = new WeakSet();
|
||||
|
||||
function normalizeType(type) {
|
||||
return TOAST_TYPES.has(type) ? type : "info";
|
||||
}
|
||||
|
||||
function show(element, duration = null) {
|
||||
element.classList.add("visible");
|
||||
clearTimeout(hideTimer);
|
||||
if (duration != null) hideTimer = setTimeout(() => element.classList.remove("visible"), duration);
|
||||
function normalizeToastCopy(value) {
|
||||
return String(value ?? "")
|
||||
.replace(/\s+/gu, " ")
|
||||
.trim()
|
||||
.replace(/[.!?…]+$/gu, "")
|
||||
.trim()
|
||||
.toLocaleLowerCase();
|
||||
}
|
||||
|
||||
function reset(element) {
|
||||
element.className = "toast";
|
||||
element.removeAttribute("aria-busy");
|
||||
element.removeAttribute("aria-label");
|
||||
function prepareToastRegion(element) {
|
||||
if (!element) return null;
|
||||
element.classList.remove("toast");
|
||||
element.classList.add("toast-region");
|
||||
element.setAttribute("aria-live", "polite");
|
||||
element.setAttribute("aria-label", t("toast.region.label", {}, "Notifications"));
|
||||
element.setAttribute("aria-relevant", "additions removals");
|
||||
return element;
|
||||
}
|
||||
|
||||
function pageToastContainer(selector = "#toast") {
|
||||
return prepareToastRegion(document.querySelector(selector));
|
||||
}
|
||||
|
||||
function activeModalDialog() {
|
||||
const dialogs = [...document.querySelectorAll("dialog[open]")];
|
||||
for (let index = dialogs.length - 1; index >= 0; index -= 1) {
|
||||
const dialog = dialogs[index];
|
||||
try {
|
||||
if (dialog.matches(":modal")) return dialog;
|
||||
} catch {
|
||||
return dialog;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
function rehomeModalToasts(dialog) {
|
||||
const region = [...dialog.children].find(child => child.classList?.contains(MODAL_REGION_CLASS));
|
||||
if (!region) return;
|
||||
const pageRegion = pageToastContainer();
|
||||
if (pageRegion) {
|
||||
[...region.querySelectorAll(".toast-card")].forEach(card => pageRegion.append(card));
|
||||
}
|
||||
try {
|
||||
if (region.matches(":popover-open")) region.hidePopover();
|
||||
} catch { /* Popover API fallback. */ }
|
||||
dialog.classList.remove("has-modal-toast-region");
|
||||
region.remove();
|
||||
}
|
||||
|
||||
function modalToastContainer(dialog) {
|
||||
let region = [...dialog.children].find(child => child.classList?.contains(MODAL_REGION_CLASS));
|
||||
if (!region) {
|
||||
region = document.createElement("div");
|
||||
region.className = `toast-region ${MODAL_REGION_CLASS}`;
|
||||
region.setAttribute("popover", "manual");
|
||||
dialog.append(region);
|
||||
}
|
||||
prepareToastRegion(region);
|
||||
if (typeof region.showPopover === "function") {
|
||||
try {
|
||||
if (!region.matches(":popover-open")) region.showPopover();
|
||||
} catch {
|
||||
dialog.classList.add("has-modal-toast-region");
|
||||
}
|
||||
} else {
|
||||
dialog.classList.add("has-modal-toast-region");
|
||||
}
|
||||
if (!modalRegionBindings.has(dialog)) {
|
||||
dialog.addEventListener("close", () => rehomeModalToasts(dialog));
|
||||
modalRegionBindings.add(dialog);
|
||||
}
|
||||
return region;
|
||||
}
|
||||
|
||||
function toastContainer(selector = "#toast", { modalAware = true } = {}) {
|
||||
if (modalAware && selector === "#toast") {
|
||||
const dialog = activeModalDialog();
|
||||
if (dialog) return modalToastContainer(dialog);
|
||||
}
|
||||
return pageToastContainer(selector);
|
||||
}
|
||||
|
||||
function applyType(card, type) {
|
||||
const normalized = normalizeType(type);
|
||||
for (const candidate of TOAST_TYPES) card.classList.remove(`toast-card--${candidate}`);
|
||||
card.classList.add(`toast-card--${normalized}`);
|
||||
card.dataset.toastType = normalized;
|
||||
if (normalized === "danger" || normalized === "warning") card.setAttribute("role", "alert");
|
||||
else card.removeAttribute("role");
|
||||
return normalized;
|
||||
}
|
||||
|
||||
function clearAutoDismiss(card) {
|
||||
clearTimeout(card._toastHideTimer);
|
||||
card._toastHideTimer = null;
|
||||
card._toastRemaining = null;
|
||||
card._toastStartedAt = null;
|
||||
const timer = card.querySelector(".toast-card__timer");
|
||||
const fill = timer?.querySelector("span");
|
||||
if (timer) timer.hidden = true;
|
||||
if (fill) {
|
||||
fill.style.animation = "none";
|
||||
fill.style.animationPlayState = "running";
|
||||
}
|
||||
}
|
||||
|
||||
function pauseAutoDismiss(card) {
|
||||
if (!card._toastHideTimer || !Number.isFinite(card._toastRemaining)) return;
|
||||
const elapsed = Date.now() - card._toastStartedAt;
|
||||
card._toastRemaining = Math.max(0, card._toastRemaining - elapsed);
|
||||
clearTimeout(card._toastHideTimer);
|
||||
card._toastHideTimer = null;
|
||||
const fill = card.querySelector(".toast-card__timer>span");
|
||||
if (fill) fill.style.animationPlayState = "paused";
|
||||
}
|
||||
|
||||
function resumeAutoDismiss(card) {
|
||||
if (card.dataset.dismissed === "true" || !Number.isFinite(card._toastRemaining) || card._toastRemaining <= 0) return;
|
||||
const fill = card.querySelector(".toast-card__timer>span");
|
||||
if (fill) fill.style.animationPlayState = "running";
|
||||
card._toastStartedAt = Date.now();
|
||||
card._toastHideTimer = window.setTimeout(() => dismissCard(card), card._toastRemaining);
|
||||
}
|
||||
|
||||
function dismissCard(card) {
|
||||
if (!card || card.dataset.dismissed === "true") return;
|
||||
card.dataset.dismissed = "true";
|
||||
clearAutoDismiss(card);
|
||||
card.classList.remove("is-visible");
|
||||
card.classList.add("is-leaving");
|
||||
window.setTimeout(() => card.remove(), 180);
|
||||
}
|
||||
|
||||
function armAutoDismiss(card, duration) {
|
||||
clearAutoDismiss(card);
|
||||
const timeout = Number(duration);
|
||||
if (!Number.isFinite(timeout) || timeout <= 0) return;
|
||||
const timer = card.querySelector(".toast-card__timer");
|
||||
const fill = timer?.querySelector("span");
|
||||
if (timer && fill) {
|
||||
timer.hidden = false;
|
||||
fill.style.animation = "none";
|
||||
void fill.offsetWidth;
|
||||
fill.style.animation = `toast-countdown ${timeout}ms linear forwards`;
|
||||
}
|
||||
card._toastRemaining = timeout;
|
||||
card._toastStartedAt = Date.now();
|
||||
card._toastHideTimer = window.setTimeout(() => dismissCard(card), timeout);
|
||||
if (card._toastHovering || card._toastFocused) pauseAutoDismiss(card);
|
||||
}
|
||||
|
||||
function createCard(container, { type = "info", title, dismissible = true, persistent = false } = {}) {
|
||||
const card = document.createElement("section");
|
||||
card.className = "toast-card";
|
||||
card.dataset.persistent = String(Boolean(persistent));
|
||||
card.setAttribute("aria-atomic", "true");
|
||||
card.innerHTML = `
|
||||
<div class="toast-card__content">
|
||||
<strong class="toast-card__title"></strong>
|
||||
</div>
|
||||
<button class="toast-card__close" type="button" aria-label="${t("toast.close", {}, "Close notification")}">
|
||||
<svg viewBox="0 0 24 24" aria-hidden="true"><path d="m7 7 10 10"></path><path d="m17 7-10 10"></path></svg>
|
||||
</button>
|
||||
<div class="toast-card__timer" aria-hidden="true" hidden><span></span></div>`;
|
||||
|
||||
const normalized = applyType(card, type);
|
||||
card.querySelector(".toast-card__title").textContent = title ? translateSource(title) : defaultTitle(normalized);
|
||||
const closeButton = card.querySelector(".toast-card__close");
|
||||
closeButton.hidden = !dismissible;
|
||||
closeButton.addEventListener("click", () => dismissCard(card));
|
||||
|
||||
card.addEventListener("mouseenter", () => {
|
||||
card._toastHovering = true;
|
||||
pauseAutoDismiss(card);
|
||||
});
|
||||
card.addEventListener("mouseleave", () => {
|
||||
card._toastHovering = false;
|
||||
if (!card._toastFocused) resumeAutoDismiss(card);
|
||||
});
|
||||
card.addEventListener("focusin", () => {
|
||||
card._toastFocused = true;
|
||||
pauseAutoDismiss(card);
|
||||
});
|
||||
card.addEventListener("focusout", event => {
|
||||
if (card.contains(event.relatedTarget)) return;
|
||||
card._toastFocused = false;
|
||||
if (!card._toastHovering) resumeAutoDismiss(card);
|
||||
});
|
||||
|
||||
if (!persistent) {
|
||||
const transient = [...container.querySelectorAll('.toast-card[data-persistent="false"]')];
|
||||
while (transient.length >= MAX_VISIBLE_TOASTS) dismissCard(transient.shift());
|
||||
}
|
||||
|
||||
container.append(card);
|
||||
requestAnimationFrame(() => card.classList.add("is-visible"));
|
||||
return card;
|
||||
}
|
||||
|
||||
function setCardTitle(card, title, fallbackType = "info") {
|
||||
const titleElement = card.querySelector(".toast-card__title");
|
||||
if (titleElement) titleElement.textContent = title ? translateSource(title) : defaultTitle(fallbackType);
|
||||
}
|
||||
|
||||
function formatBytes(value) {
|
||||
const bytes = Math.max(0, Number(value) || 0);
|
||||
if (bytes < 1024) return `${Math.round(bytes)} B`;
|
||||
if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(bytes >= 10240 ? 0 : 1)} KB`;
|
||||
if (bytes < 1024 * 1024 * 1024) return `${(bytes / (1024 * 1024)).toFixed(bytes >= 10 * 1024 * 1024 ? 1 : 2)} MB`;
|
||||
return `${(bytes / (1024 * 1024 * 1024)).toFixed(2)} GB`;
|
||||
if (bytes < 1024) return `${formatNumber(Math.round(bytes))} B`;
|
||||
if (bytes < 1024 * 1024) return `${formatNumber(bytes / 1024, { maximumFractionDigits: bytes >= 10240 ? 0 : 1 })} KB`;
|
||||
if (bytes < 1024 * 1024 * 1024) return `${formatNumber(bytes / (1024 * 1024), { maximumFractionDigits: bytes >= 10 * 1024 * 1024 ? 1 : 2 })} MB`;
|
||||
return `${formatNumber(bytes / (1024 * 1024 * 1024), { maximumFractionDigits: 2 })} GB`;
|
||||
}
|
||||
|
||||
export function queueToast(message, options = {}) {
|
||||
const payload = {
|
||||
message: String(message ?? ""),
|
||||
type: normalizeType(options.type),
|
||||
title: options.title == null ? null : String(options.title),
|
||||
duration: Number.isFinite(Number(options.duration)) ? Number(options.duration) : null,
|
||||
};
|
||||
try {
|
||||
sessionStorage.setItem(FLASH_TOAST_STORAGE_KEY, JSON.stringify(payload));
|
||||
return true;
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
export function consumeQueuedToast() {
|
||||
let raw = null;
|
||||
try {
|
||||
raw = sessionStorage.getItem(FLASH_TOAST_STORAGE_KEY);
|
||||
sessionStorage.removeItem(FLASH_TOAST_STORAGE_KEY);
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
if (!raw) return false;
|
||||
try {
|
||||
const payload = JSON.parse(raw);
|
||||
if (!payload || typeof payload.message !== "string") return false;
|
||||
const options = { type: normalizeType(payload.type) };
|
||||
if (typeof payload.title === "string" && payload.title) options.title = payload.title;
|
||||
if (Number.isFinite(payload.duration) && payload.duration > 0) options.duration = payload.duration;
|
||||
toast(payload.message, options);
|
||||
return true;
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
export function toast(message, options = {}) {
|
||||
const element = toastElement(options.selector);
|
||||
if (!element) return;
|
||||
reset(element);
|
||||
element.textContent = String(message ?? "");
|
||||
show(element, options.duration ?? 1800);
|
||||
const container = toastContainer(options.selector, { modalAware: options.modalAware !== false });
|
||||
if (!container) return { dismiss() {} };
|
||||
const type = normalizeType(options.type);
|
||||
const card = createCard(container, {
|
||||
type,
|
||||
title: options.title,
|
||||
dismissible: options.dismissible !== false,
|
||||
});
|
||||
const translatedMessage = translateSource(String(message ?? ""));
|
||||
const renderedTitle = card.querySelector(".toast-card__title")?.textContent || "";
|
||||
const normalizedMessage = normalizeToastCopy(translatedMessage);
|
||||
if (normalizedMessage && normalizedMessage !== normalizeToastCopy(renderedTitle)) {
|
||||
const body = document.createElement("p");
|
||||
body.className = "toast-card__message";
|
||||
body.textContent = translatedMessage;
|
||||
card.querySelector(".toast-card__content").append(body);
|
||||
}
|
||||
armAutoDismiss(card, options.duration ?? DEFAULT_DURATIONS[type]);
|
||||
return { dismiss: () => dismissCard(card), element: card };
|
||||
}
|
||||
|
||||
toast.info = (message, options = {}) => toast(message, { ...options, type: "info" });
|
||||
toast.success = (message, options = {}) => toast(message, { ...options, type: "success" });
|
||||
toast.warning = (message, options = {}) => toast(message, { ...options, type: "warning" });
|
||||
toast.danger = (message, options = {}) => toast(message, { ...options, type: "danger" });
|
||||
|
||||
export function createUploadToast(filename, options = {}) {
|
||||
const element = toastElement(options.selector);
|
||||
if (!element) {
|
||||
return { start() { }, update() { }, fail() { }, success() { }, dismiss() { } };
|
||||
const container = toastContainer(options.selector, { modalAware: options.modalAware !== false });
|
||||
if (!container) {
|
||||
return { start() {}, update() {}, fail() {}, success() {}, dismiss() {} };
|
||||
}
|
||||
|
||||
reset(element);
|
||||
element.classList.add("toast--upload", "toast--interactive");
|
||||
element.setAttribute("aria-live", "polite");
|
||||
element.innerHTML = `
|
||||
<div class="upload-toast__header">
|
||||
<div class="upload-toast__heading">
|
||||
<strong class="upload-toast__title">Uploading file</strong>
|
||||
<span class="upload-toast__filename"></span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="upload-toast__progress" role="progressbar" aria-label="File upload progress" aria-valuemin="0" aria-valuemax="100" aria-valuenow="0"><span></span></div>
|
||||
<div class="upload-toast__meta"><span data-upload-amount>Preparing…</span><span data-upload-speed>—</span></div>
|
||||
const card = createCard(container, {
|
||||
type: "info",
|
||||
title: t("upload.title.uploading", {}, "Uploading file"),
|
||||
dismissible: true,
|
||||
persistent: true,
|
||||
});
|
||||
card.classList.add("toast-card--upload");
|
||||
card.setAttribute("aria-busy", "true");
|
||||
|
||||
const content = card.querySelector(".toast-card__content");
|
||||
content.insertAdjacentHTML("beforeend", `
|
||||
<span class="upload-toast__filename"></span>
|
||||
<div class="upload-toast__progress" role="progressbar" aria-label="${t("upload.progress.label", {}, "File upload progress")}" data-i18n-aria-label="upload.progress.label" aria-valuemin="0" aria-valuemax="100" aria-valuenow="0"><span></span></div>
|
||||
<div class="upload-toast__meta"><span data-upload-amount>${t("upload.status.preparing", {}, "Preparing...")}</span><span data-upload-speed>-</span></div>
|
||||
<p class="upload-toast__error" data-upload-error hidden></p>
|
||||
<div class="upload-toast__actions" data-upload-actions hidden>
|
||||
<button type="button" class="upload-toast__retry">Retry</button>
|
||||
<button type="button" class="upload-toast__dismiss">Dismiss</button>
|
||||
</div>`;
|
||||
<button type="button" class="upload-toast__retry" data-i18n="common.retry">${t("common.retry", {}, "Retry")}</button>
|
||||
<button type="button" class="upload-toast__dismiss" data-i18n="common.dismiss">${t("common.dismiss", {}, "Dismiss")}</button>
|
||||
</div>`);
|
||||
|
||||
const title = element.querySelector(".upload-toast__title");
|
||||
const filenameElement = element.querySelector(".upload-toast__filename");
|
||||
const progress = element.querySelector(".upload-toast__progress");
|
||||
const filenameElement = card.querySelector(".upload-toast__filename");
|
||||
const progress = card.querySelector(".upload-toast__progress");
|
||||
const progressFill = progress.querySelector("span");
|
||||
const amount = element.querySelector("[data-upload-amount]");
|
||||
const speedElement = element.querySelector("[data-upload-speed]");
|
||||
const errorElement = element.querySelector("[data-upload-error]");
|
||||
const actions = element.querySelector("[data-upload-actions]");
|
||||
const retryButton = element.querySelector(".upload-toast__retry");
|
||||
const dismissButton = element.querySelector(".upload-toast__dismiss");
|
||||
const amount = card.querySelector("[data-upload-amount]");
|
||||
const speedElement = card.querySelector("[data-upload-speed]");
|
||||
const errorElement = card.querySelector("[data-upload-error]");
|
||||
const actions = card.querySelector("[data-upload-actions]");
|
||||
const retryButton = card.querySelector(".upload-toast__retry");
|
||||
const dismissButton = card.querySelector(".upload-toast__dismiss");
|
||||
let retryHandler = null;
|
||||
|
||||
filenameElement.textContent = String(filename || "file");
|
||||
|
||||
function dismiss() {
|
||||
clearTimeout(hideTimer);
|
||||
element.classList.remove("visible");
|
||||
dismissCard(card);
|
||||
}
|
||||
|
||||
function start() {
|
||||
reset(element);
|
||||
element.classList.add("toast--upload", "toast--interactive", "visible");
|
||||
element.setAttribute("aria-live", "polite");
|
||||
element.setAttribute("aria-busy", "true");
|
||||
title.textContent = "Uploading file";
|
||||
clearAutoDismiss(card);
|
||||
card.dataset.dismissed = "false";
|
||||
card.dataset.persistent = "true";
|
||||
card.classList.remove("is-leaving");
|
||||
card.classList.add("is-visible");
|
||||
card.setAttribute("aria-busy", "true");
|
||||
applyType(card, "info");
|
||||
setCardTitle(card, t("upload.title.uploading", {}, "Uploading file"), "info");
|
||||
filenameElement.textContent = String(filename || "file");
|
||||
progress.classList.remove("is-error", "is-complete");
|
||||
progress.classList.remove("is-error", "is-complete", "is-indeterminate");
|
||||
progress.setAttribute("aria-valuenow", "0");
|
||||
progressFill.style.width = "0%";
|
||||
amount.textContent = "Preparing…";
|
||||
speedElement.textContent = "—";
|
||||
amount.textContent = t("upload.status.preparing", {}, "Preparing...");
|
||||
delete speedElement.dataset.i18n;
|
||||
speedElement.textContent = "-";
|
||||
errorElement.hidden = true;
|
||||
errorElement.textContent = "";
|
||||
actions.hidden = true;
|
||||
retryButton.hidden = false;
|
||||
retryButton.disabled = false;
|
||||
clearTimeout(hideTimer);
|
||||
}
|
||||
|
||||
function update({ loaded = 0, total = 0, speed = 0, percent = null, phase = "uploading" } = {}) {
|
||||
progress.classList.remove("is-indeterminate");
|
||||
const normalizedPercent = Number.isFinite(percent)
|
||||
? Math.max(0, Math.min(100, percent))
|
||||
: total > 0 ? Math.max(0, Math.min(100, (loaded / total) * 100)) : null;
|
||||
@@ -116,42 +393,52 @@ export function createUploadToast(filename, options = {}) {
|
||||
progress.classList.add("is-indeterminate");
|
||||
}
|
||||
amount.textContent = total > 0
|
||||
? `${Math.round(normalizedPercent || 0)}% · ${formatBytes(loaded)} / ${formatBytes(total)}`
|
||||
? `${Math.round(normalizedPercent || 0)}% - ${formatBytes(loaded)} / ${formatBytes(total)}`
|
||||
: formatBytes(loaded);
|
||||
speedElement.textContent = phase === "processing" ? "Processing…" : speed > 0 ? `${formatBytes(speed)}/s` : "Starting…";
|
||||
speedElement.textContent = phase === "processing" ? t("upload.status.processing", {}, "Processing...") : speed > 0 ? `${formatBytes(speed)}/s` : t("upload.status.starting", {}, "Starting...");
|
||||
}
|
||||
|
||||
function fail(message, { retryable = true, onRetry = null } = {}) {
|
||||
element.removeAttribute("aria-busy");
|
||||
title.textContent = "Upload failed";
|
||||
clearAutoDismiss(card);
|
||||
card.removeAttribute("aria-busy");
|
||||
card.dataset.persistent = "true";
|
||||
applyType(card, "danger");
|
||||
setCardTitle(card, t("upload.title.failed", {}, "Upload failed"), "danger");
|
||||
progress.classList.remove("is-indeterminate", "is-complete");
|
||||
progress.classList.add("is-error");
|
||||
errorElement.textContent = String(message || "Upload failed.");
|
||||
errorElement.textContent = translateSource(String(message || t("upload.error.generic", {}, "The file could not be uploaded.")));
|
||||
errorElement.hidden = false;
|
||||
actions.hidden = false;
|
||||
retryButton.hidden = !retryable;
|
||||
retryButton.disabled = false;
|
||||
retryHandler = typeof onRetry === "function" ? onRetry : null;
|
||||
show(element);
|
||||
}
|
||||
|
||||
function success(message = "File uploaded") {
|
||||
element.removeAttribute("aria-busy");
|
||||
title.textContent = message;
|
||||
function success(message = t("upload.title.complete", {}, "File uploaded")) {
|
||||
card.removeAttribute("aria-busy");
|
||||
card.dataset.persistent = "false";
|
||||
applyType(card, "success");
|
||||
setCardTitle(card, message, "success");
|
||||
progress.classList.remove("is-error", "is-indeterminate");
|
||||
progress.classList.add("is-complete");
|
||||
progress.setAttribute("aria-valuenow", "100");
|
||||
progressFill.style.width = "100%";
|
||||
amount.textContent = "100%";
|
||||
speedElement.textContent = "Complete";
|
||||
speedElement.dataset.i18n = "upload.status.complete";
|
||||
speedElement.textContent = t("upload.status.complete", {}, "Complete");
|
||||
errorElement.hidden = true;
|
||||
actions.hidden = true;
|
||||
show(element, 1800);
|
||||
armAutoDismiss(card, 3200);
|
||||
}
|
||||
|
||||
retryButton.addEventListener("click", async () => {
|
||||
if (!retryHandler) return;
|
||||
retryButton.disabled = true;
|
||||
await retryHandler();
|
||||
try {
|
||||
await retryHandler();
|
||||
} finally {
|
||||
if (card.isConnected && !card.hasAttribute("aria-busy")) retryButton.disabled = false;
|
||||
}
|
||||
});
|
||||
dismissButton.addEventListener("click", dismiss);
|
||||
|
||||
|
||||
Reference in New Issue
Block a user