Files
2026-07-17 12:57:18 +03:00

210 lines
7.3 KiB
JavaScript

const KIOSK_SKELETON_MIN_MS = 300;
const KIOSK_SKELETON_EXIT_MS = 160;
const GRID_CARD_INNER = `
<div class="dashboard-skeleton-bar dashboard-skeleton-bar--kiosk-title"></div>
<div class="kiosk-skeleton-info">
<div class="dashboard-skeleton-bar dashboard-skeleton-bar--kiosk-meta"></div>
<div class="dashboard-skeleton-bar dashboard-skeleton-bar--kiosk-meta"></div>
</div>`;
const GROUP_ITEM_INNER = `
<div class="dashboard-skeleton-bar dashboard-skeleton-bar--title"></div>
<div class="dashboard-skeleton-bar dashboard-skeleton-bar--meta"></div>`;
export function buildGridSkeletonCards(rowCount = 4) {
return Array.from({ length: rowCount }, () =>
`<div class="dispenser-card kiosk-skeleton-card" aria-hidden="true">${GRID_CARD_INNER}</div>`
).join("");
}
export function buildDispenserGridSkeletonHtml(rowCount = 4) {
return buildGridSkeletonCards(rowCount);
}
export function buildPeriodRecipeGridSkeletonHtml(rowCount = 4) {
return buildGridSkeletonCards(rowCount);
}
export function buildUnloadingGroupsSkeletonHtml(rowCount = 3) {
const items = Array.from({ length: rowCount }, () =>
`<div class="kiosk-skeleton-group-item">${GROUP_ITEM_INNER}</div>`
).join("");
return `<div class="kiosk-skeleton-groups" data-skeleton-variant="groups" aria-busy="true" aria-label="Загрузка групп выгрузки">${items}</div>`;
}
export function buildDuplicatePanelSkeletonHtml() {
return `<div class="kiosk-skeleton-panel" data-skeleton-variant="duplicate" aria-busy="true" aria-label="Загрузка данных">
<div class="kiosk-skeleton-recipe-header">
<div class="dashboard-skeleton-bar dashboard-skeleton-bar--title"></div>
<div class="dashboard-skeleton-bar dashboard-skeleton-bar--subtitle"></div>
</div>
<div class="dashboard-skeleton-bar kiosk-skeleton-component-name"></div>
<div class="dashboard-skeleton-bar kiosk-skeleton-weight-hero"></div>
</div>`;
}
export function buildRecipeIngredientsSkeletonHtml(rowCount = 6) {
const rows = Array.from({ length: rowCount }, () =>
`<div class="kiosk-skeleton-table-row">
<div class="dashboard-skeleton-bar"></div>
<div class="dashboard-skeleton-bar"></div>
</div>`
).join("");
return `<div class="kiosk-skeleton-table" data-skeleton-variant="ingredients" aria-busy="true" aria-label="Загрузка ингредиентов">${rows}</div>`;
}
function prefersReducedMotion() {
return globalThis.matchMedia?.("(prefers-reduced-motion: reduce)")?.matches;
}
function isGridSkeletonHost(container) {
if (!container) return false;
return (
container.getAttribute("aria-busy") === "true" &&
container.querySelector(":scope > .kiosk-skeleton-card") != null
);
}
function findSkeletonRoot(container) {
if (!container) return null;
if (isGridSkeletonHost(container)) return container;
return container.querySelector(
":scope > .kiosk-skeleton-groups, :scope > .kiosk-skeleton-table, :scope > .kiosk-skeleton-panel"
);
}
export function mountKioskSkeleton(container, html, label) {
if (!container) return performance.now();
container.innerHTML = html;
container.classList.remove("kiosk-skeleton-host--exit");
if (container.querySelector(":scope > .kiosk-skeleton-card")) {
container.setAttribute("aria-busy", "true");
if (label) container.setAttribute("aria-label", label);
}
return performance.now();
}
export async function awaitKioskSkeletonMin(startedAt, minMs = KIOSK_SKELETON_MIN_MS) {
const elapsed = performance.now() - startedAt;
const rest = minMs - elapsed;
if (rest > 0) {
await new Promise((resolve) => setTimeout(resolve, rest));
}
}
async function fadeOutSkeleton(skel) {
if (!skel || prefersReducedMotion()) return;
const exitClass = isGridSkeletonHost(skel)
? "kiosk-skeleton-host--exit"
: skel.classList.contains("kiosk-skeleton-groups")
? "kiosk-skeleton-groups--exit"
: skel.classList.contains("kiosk-skeleton-table")
? "kiosk-skeleton-table--exit"
: "kiosk-skeleton-panel--exit";
skel.classList.add(exitClass);
await new Promise((resolve) => {
let settled = false;
const finish = () => {
if (settled) return;
settled = true;
skel.removeEventListener("transitionend", onEnd);
resolve();
};
const onEnd = (event) => {
if (event.target === skel && event.propertyName === "opacity") finish();
};
skel.addEventListener("transitionend", onEnd);
globalThis.setTimeout(finish, KIOSK_SKELETON_EXIT_MS + 40);
});
}
function clearSkeletonHostAttrs(container) {
if (!container) return;
container.classList.remove("kiosk-skeleton-host--exit");
container.removeAttribute("aria-busy");
container.removeAttribute("aria-label");
}
export async function replaceKioskContent(container, html, options = {}) {
if (!container) return;
const { minMs = KIOSK_SKELETON_MIN_MS, startedAt = performance.now() } = options;
const skel = findSkeletonRoot(container);
if (skel) {
await awaitKioskSkeletonMin(startedAt, minMs);
await fadeOutSkeleton(skel);
} else {
await awaitKioskSkeletonMin(startedAt, minMs);
}
container.innerHTML = html;
clearSkeletonHostAttrs(container);
}
export function clearStaleKioskSkeleton(container) {
if (!container) return;
if (findSkeletonRoot(container)) {
container.innerHTML = "";
clearSkeletonHostAttrs(container);
}
}
export function mountDuplicatePanelSkeleton(recipeInfoEl, weightSectionEl) {
const html = buildDuplicatePanelSkeletonHtml();
const startedAt = performance.now();
if (recipeInfoEl) {
recipeInfoEl.style.display = "block";
recipeInfoEl.innerHTML = html;
}
if (weightSectionEl) {
weightSectionEl.classList.add("kiosk-skeleton-host");
const host = weightSectionEl.querySelector(".weight-content");
if (host) {
host.dataset.kioskSkeletonBackup = host.innerHTML;
host.innerHTML = `<div class="kiosk-skeleton-panel" aria-busy="true" aria-hidden="true">
<div class="dashboard-skeleton-bar kiosk-skeleton-component-name"></div>
<div class="dashboard-skeleton-bar kiosk-skeleton-weight-hero"></div>
</div>`;
}
}
return startedAt;
}
export function clearDuplicatePanelSkeleton(recipeInfoEl, weightSectionEl) {
if (recipeInfoEl) {
const skel = recipeInfoEl.querySelector(".kiosk-skeleton-panel");
if (skel) {
recipeInfoEl.style.display = "none";
recipeInfoEl.innerHTML = `
<h3>Рецепт: <span id="recipeName"></span></h3>
<p>Компонент <span id="currentIndex">0</span> из <span id="totalComponents">0</span></p>`;
}
}
if (weightSectionEl) {
weightSectionEl.classList.remove("kiosk-skeleton-host");
const host = weightSectionEl.querySelector(".weight-content");
if (host?.dataset.kioskSkeletonBackup) {
host.innerHTML = host.dataset.kioskSkeletonBackup;
delete host.dataset.kioskSkeletonBackup;
}
}
}
const api = {
buildGridSkeletonCards,
buildDispenserGridSkeletonHtml,
buildPeriodRecipeGridSkeletonHtml,
buildUnloadingGroupsSkeletonHtml,
buildDuplicatePanelSkeletonHtml,
buildRecipeIngredientsSkeletonHtml,
mountKioskSkeleton,
replaceKioskContent,
awaitKioskSkeletonMin,
clearStaleKioskSkeleton,
mountDuplicatePanelSkeleton,
clearDuplicatePanelSkeleton,
};
if (typeof globalThis !== "undefined") {
globalThis.WespKioskSkeleton = api;
}