Files
site/apps/web/public/wesp/js/wesp-zootech-notify.js
T

286 lines
8.5 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/**
* 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, "&lt;")
.replace(/>/g, "&gt;")
.replace(/"/g, "&quot;");
}
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);