210 lines
7.3 KiB
JavaScript
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;
|
|
}
|