import { animate } from "motion"; import { animateStepIn, animateStepOut } from "../../Libs/UiHelper"; import { updateServerStep } from "./WizardApi"; import { teardownPreviewsSW } from "../Shared/Preview"; import { qs } from "../../Libs/Utils"; import ExtraNavigationInterface from "../../Interfaces/ExtraNavigationInterface"; /** * Progress bar animations */ function animateProgressBar(slots: NodeListOf, currentStep: number) { slots.forEach((slot, index) => { const step = index + 1; if (step < currentStep) { // Passed steps animate(slot, { scale: [1, 1.05, 1] }, { duration: 0.3, delay: index * 0.1 }); } else if (step === currentStep) { // Current step animate(slot, { scale: [1, 1.1, 1] }, { duration: 0.4, delay: index * 0.1 }); } }); } /** * Updates the progress bar slots with animation. */ function updateSlots(step: number, skipped: boolean = false): void { const slots = document.querySelectorAll('.slot'); if (!slots.length) return; const logicalStep = Math.floor(step); const slotsArray = Array.from(slots); let currentVisualIndex = slotsArray.findIndex(slot => { const slotStep = parseInt((slot as HTMLElement).dataset.step || "0"); return slotStep === logicalStep; }); if (currentVisualIndex === -1) { if (logicalStep >= 5) { currentVisualIndex = slotsArray.length - 1; } else { currentVisualIndex = 0; } } slots.forEach((slot, index) => { const isCompleted = slot.classList.contains('completed'); const isSkipped = slot.classList.contains('skipped'); const isCurrent = slot.classList.contains('current'); if (index === currentVisualIndex) { slot.classList.remove('completed', 'skipped'); slot.classList.add('current'); if (currentVisualIndex === slotsArray.length - 1) { slot.classList.add('visually-completed'); } else { slot.classList.remove('visually-completed'); } } else { if (isCurrent) { slot.classList.remove('current'); slot.classList.remove('visually-completed'); if (skipped) { slot.classList.add('skipped'); } else { slot.classList.add('completed'); } } else if (index > currentVisualIndex) { // When navigating backwards, reset future slots to pending. slot.classList.remove('completed', 'skipped', 'current', 'visually-completed'); } else if (index < currentVisualIndex) { if (!isCompleted && !isSkipped) { slot.classList.add('completed'); } } } }); syncSlotClickability(slotsArray); animateProgressBar(slots as NodeListOf, currentVisualIndex + 1); } function syncSlotClickability(slotsArray: Element[]): void { slotsArray.forEach((slot) => { const slotElement = slot as HTMLElement; const isCompleted = slotElement.classList.contains('completed'); const isSkipped = slotElement.classList.contains('skipped'); const isCurrent = slotElement.classList.contains('current'); const isVisuallyCompleted = slotElement.classList.contains('visually-completed'); const shouldBeClickable = isCompleted || isSkipped || (isCurrent && isVisuallyCompleted); const stepUrl = slotElement.dataset.stepUrl; const currentLabel = slotElement.querySelector('.slot-label') as HTMLElement | null; if (!currentLabel || !stepUrl) { return; } if (shouldBeClickable) { slotElement.classList.add('is-clickable'); if (currentLabel.tagName !== 'A') { const link = document.createElement('a'); link.className = currentLabel.className; link.href = stepUrl; link.innerHTML = currentLabel.innerHTML; currentLabel.replaceWith(link); } else { (currentLabel as HTMLAnchorElement).href = stepUrl; } } else { slotElement.classList.remove('is-clickable'); if (currentLabel.tagName === 'A') { const span = document.createElement('span'); span.className = currentLabel.className; span.setAttribute('aria-disabled', 'true'); span.innerHTML = currentLabel.innerHTML; currentLabel.replaceWith(span); } } }); } /** * Consolidated navigation logic for the setup wizard. */ export async function navigateToWizardStep(step: number, extra: ExtraNavigationInterface = {}): Promise { const currentStepRadio = document.querySelector('input[type="radio"].wizard-step:checked') as HTMLInputElement; const allStepRadios = Array.from(document.querySelectorAll('input[type="radio"].wizard-step')); const currentStepContent = currentStepRadio ? document.querySelector(`.wizard-content:nth-of-type(${allStepRadios.indexOf(currentStepRadio) + 1})`) as HTMLElement : null; let nextStepRadio: HTMLInputElement | null = null; const stepIdMap: { [key: number]: string } = { 1: 'step-1', 2: 'step-2', 2.1: 'step-2-1', 3: 'step-3', 3.1: 'step-3-1', 4: 'step-4', 4.1: 'step-4-1', 5: 'step-5' }; const targetId = stepIdMap[step] || (step === -1 ? 'step-1' : null); if (targetId) { nextStepRadio = qs(targetId); } if (!nextStepRadio) return; const nextStepContent = document.querySelector(`.wizard-content:nth-of-type(${allStepRadios.indexOf(nextStepRadio) + 1})`) as HTMLElement; // Update URL and history const url = new URL(window.location.href); url.searchParams.set('step', step.toString()); window.history.pushState({ step: step }, '', url.toString()); // Sync with server updateServerStep(step, extra).catch(error => { console.error('Error updating server step:', error); }); // Teardown previews: if not teared down, the video still playing hidden if (step !== 3.1 && step !== 4.1) { await teardownPreviewsSW('all'); } const skipped = extra.skip_step ? true : false; if (currentStepContent) { await animateStepOut(currentStepContent); nextStepRadio.checked = true; updateSlots(step, skipped); animateStepIn(nextStepContent); } else { nextStepRadio.checked = true; updateSlots(step, skipped); animateStepIn(nextStepContent); } }