@@ -0,0 +1,285 @@
|
||||
/**
|
||||
* Toast-уведомления зоотех-страниц (нижний pill-toast, стиль Яндекс Музыки).
|
||||
*/
|
||||
(function (global) {
|
||||
const HOST_ID = "wespZtToastHost";
|
||||
const DEFAULT_DURATION_MS = 4000;
|
||||
const EXIT_MS = 220;
|
||||
|
||||
const UM = () => global.WespUserMessages || {};
|
||||
const userFacingMessage = (message, fallback) => {
|
||||
const api = UM();
|
||||
if (typeof api.userFacingMessage === "function") {
|
||||
return api.userFacingMessage(message, fallback);
|
||||
}
|
||||
const text = message == null ? "" : String(message).trim();
|
||||
return text || fallback || "Не удалось выполнить операцию. Попробуйте ещё раз.";
|
||||
};
|
||||
|
||||
let hostEl = null;
|
||||
let activeToast = null;
|
||||
let dismissTimer = null;
|
||||
|
||||
function ensureHost() {
|
||||
if (hostEl && hostEl.isConnected) return hostEl;
|
||||
hostEl = document.getElementById(HOST_ID);
|
||||
if (!hostEl) {
|
||||
hostEl = document.createElement("div");
|
||||
hostEl.id = HOST_ID;
|
||||
hostEl.className = "wesp-zt-toast-host";
|
||||
hostEl.setAttribute("aria-live", "polite");
|
||||
hostEl.setAttribute("aria-atomic", "true");
|
||||
document.body.appendChild(hostEl);
|
||||
}
|
||||
return hostEl;
|
||||
}
|
||||
|
||||
function clearDismissTimer() {
|
||||
if (dismissTimer) {
|
||||
clearTimeout(dismissTimer);
|
||||
dismissTimer = null;
|
||||
}
|
||||
}
|
||||
|
||||
function normalizeType(type) {
|
||||
if (type === "error" || type === "warning") return type;
|
||||
return "success";
|
||||
}
|
||||
|
||||
function escapeHtml(value) {
|
||||
return String(value)
|
||||
.replace(/&/g, "&")
|
||||
.replace(/</g, "<")
|
||||
.replace(/>/g, ">")
|
||||
.replace(/"/g, """);
|
||||
}
|
||||
|
||||
function normalizeDisplayMessage(message) {
|
||||
if (message && typeof message === "object") {
|
||||
if (Array.isArray(message.parts)) return message;
|
||||
if (message.text != null) message = message.text;
|
||||
}
|
||||
const text = message == null ? "" : String(message).trim();
|
||||
if (/^Сохранено\b/i.test(text)) {
|
||||
return "Сохранено";
|
||||
}
|
||||
return text;
|
||||
}
|
||||
|
||||
function renderMessageContent(message, opts) {
|
||||
if (message && typeof message === "object") {
|
||||
if (Array.isArray(message.parts)) {
|
||||
return message.parts
|
||||
.map((part) => {
|
||||
const text = escapeHtml(part && part.text != null ? part.text : "");
|
||||
return part && part.bold ? `<strong>${text}</strong>` : text;
|
||||
})
|
||||
.join("");
|
||||
}
|
||||
if (message.text != null) {
|
||||
return escapeHtml(message.text);
|
||||
}
|
||||
}
|
||||
|
||||
const text = message == null ? "" : String(message);
|
||||
if (opts && Array.isArray(opts.parts)) {
|
||||
return opts.parts
|
||||
.map((part) => {
|
||||
const chunk = escapeHtml(part && part.text != null ? part.text : "");
|
||||
return part && part.bold ? `<strong>${chunk}</strong>` : chunk;
|
||||
})
|
||||
.join("");
|
||||
}
|
||||
return escapeHtml(text);
|
||||
}
|
||||
|
||||
function removeToast(toastEl, immediate) {
|
||||
if (!toastEl || !toastEl.isConnected) return Promise.resolve();
|
||||
clearDismissTimer();
|
||||
if (immediate) {
|
||||
toastEl.remove();
|
||||
if (activeToast === toastEl) activeToast = null;
|
||||
return Promise.resolve();
|
||||
}
|
||||
|
||||
return new Promise((resolve) => {
|
||||
toastEl.classList.add("is-exiting");
|
||||
const onEnd = () => {
|
||||
toastEl.removeEventListener("animationend", onEnd);
|
||||
toastEl.remove();
|
||||
if (activeToast === toastEl) activeToast = null;
|
||||
resolve();
|
||||
};
|
||||
toastEl.addEventListener("animationend", onEnd);
|
||||
setTimeout(onEnd, EXIT_MS + 40);
|
||||
});
|
||||
}
|
||||
|
||||
function dismissAll() {
|
||||
clearDismissTimer();
|
||||
if (!activeToast) return;
|
||||
const toast = activeToast;
|
||||
activeToast = null;
|
||||
return removeToast(toast, false);
|
||||
}
|
||||
|
||||
function plainTextFromMessage(message) {
|
||||
const api = UM();
|
||||
if (typeof api.plainText === "function") return api.plainText(message);
|
||||
if (message && typeof message === "object") {
|
||||
if (message instanceof Error) return "";
|
||||
if (Array.isArray(message.parts)) {
|
||||
return message.parts.map((p) => (p && p.text != null ? String(p.text) : "")).join("");
|
||||
}
|
||||
if (message.text != null) return String(message.text);
|
||||
}
|
||||
return message == null ? "" : String(message);
|
||||
}
|
||||
|
||||
function persistNotification(type, message, opts) {
|
||||
if (opts && opts.skipRecord) return;
|
||||
const plain = plainTextFromMessage(
|
||||
message && typeof message === "object" && Array.isArray(message.parts)
|
||||
? message
|
||||
: normalizeDisplayMessage(message)
|
||||
);
|
||||
if (!plain.trim() && !(opts && opts.record)) return;
|
||||
|
||||
const builder = global.WespZootechNotificationMessages;
|
||||
const payload = builder
|
||||
? builder.buildNotificationRecord(type, plain, opts)
|
||||
: {
|
||||
title: plain.slice(0, 200) || "Уведомление",
|
||||
detail: plain,
|
||||
kind: type,
|
||||
category: "general",
|
||||
page: "general",
|
||||
};
|
||||
|
||||
fetch("/api/notifications", {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify(payload),
|
||||
})
|
||||
.then(() => {
|
||||
global.WespZootechNotificationCenter?.refreshBadge?.();
|
||||
})
|
||||
.catch(() => {
|
||||
/* fire-and-forget */
|
||||
});
|
||||
}
|
||||
|
||||
function show(type, message, opts) {
|
||||
const normalizedType = normalizeType(type);
|
||||
const duration =
|
||||
opts && Number.isFinite(opts.duration) ? opts.duration : DEFAULT_DURATION_MS;
|
||||
const displayMessage =
|
||||
message && typeof message === "object" && Array.isArray(message.parts)
|
||||
? message
|
||||
: normalizeDisplayMessage(message);
|
||||
const html = renderMessageContent(displayMessage, opts);
|
||||
const plainText =
|
||||
displayMessage && typeof displayMessage === "object"
|
||||
? ""
|
||||
: String(displayMessage || "");
|
||||
const isMultiline = plainText.length > 42 || /\n/.test(plainText);
|
||||
|
||||
const run = async () => {
|
||||
if (activeToast) {
|
||||
await removeToast(activeToast, false);
|
||||
}
|
||||
|
||||
const root = ensureHost();
|
||||
const toast = document.createElement("div");
|
||||
toast.className = `wesp-zt-toast wesp-zt-toast--${normalizedType}${
|
||||
isMultiline ? " wesp-zt-toast--multiline" : ""
|
||||
}`;
|
||||
toast.setAttribute("role", "status");
|
||||
|
||||
const accent = document.createElement("span");
|
||||
accent.className = "wesp-zt-toast__accent";
|
||||
accent.setAttribute("aria-hidden", "true");
|
||||
|
||||
const messageEl = document.createElement("p");
|
||||
messageEl.className = "wesp-zt-toast__message";
|
||||
messageEl.innerHTML = html;
|
||||
|
||||
const closeBtn = document.createElement("button");
|
||||
closeBtn.type = "button";
|
||||
closeBtn.className = "wesp-zt-toast__close";
|
||||
closeBtn.setAttribute("aria-label", "Закрыть уведомление");
|
||||
closeBtn.textContent = "×";
|
||||
closeBtn.addEventListener("click", () => {
|
||||
dismissAll();
|
||||
});
|
||||
|
||||
toast.append(accent, messageEl, closeBtn);
|
||||
root.appendChild(toast);
|
||||
activeToast = toast;
|
||||
|
||||
if (duration > 0) {
|
||||
dismissTimer = setTimeout(() => {
|
||||
dismissAll();
|
||||
}, duration);
|
||||
}
|
||||
};
|
||||
|
||||
persistNotification(normalizedType, displayMessage, opts);
|
||||
run();
|
||||
}
|
||||
|
||||
function success(message, opts) {
|
||||
show("success", message, opts);
|
||||
}
|
||||
|
||||
function error(message, opts) {
|
||||
const safe = userFacingMessage(message, opts && opts.fallback);
|
||||
show("error", safe, opts);
|
||||
}
|
||||
|
||||
function warning(message, opts) {
|
||||
const safe = userFacingMessage(message, opts && opts.fallback);
|
||||
show("warning", safe, opts);
|
||||
}
|
||||
|
||||
function open(payload) {
|
||||
const data = payload && typeof payload === "object" ? payload : {};
|
||||
show(data.type || "success", data.message, data);
|
||||
}
|
||||
|
||||
function createAdapter() {
|
||||
return {
|
||||
success,
|
||||
error,
|
||||
warning,
|
||||
open,
|
||||
dismissAll,
|
||||
};
|
||||
}
|
||||
|
||||
global.WespZootechNotify = {
|
||||
success,
|
||||
error,
|
||||
warning,
|
||||
open,
|
||||
dismissAll,
|
||||
createAdapter,
|
||||
userFacingMessage,
|
||||
messageFromResponseBody: (body, fallback) => {
|
||||
const api = UM();
|
||||
if (typeof api.messageFromResponseBody === "function") {
|
||||
return api.messageFromResponseBody(body, fallback);
|
||||
}
|
||||
return userFacingMessage(null, fallback);
|
||||
},
|
||||
isTechnicalMessage: (message) => {
|
||||
const api = UM();
|
||||
if (typeof api.isTechnicalMessage === "function") return api.isTechnicalMessage(message);
|
||||
return false;
|
||||
},
|
||||
DEFAULT_ERROR_MESSAGE: UM().DEFAULT_ERROR_MESSAGE || "Не удалось выполнить операцию. Попробуйте ещё раз.",
|
||||
get instance() {
|
||||
return createAdapter();
|
||||
},
|
||||
};
|
||||
})(typeof window !== "undefined" ? window : globalThis);
|
||||
Reference in New Issue
Block a user