|
12 | 12 | conclusion: "Finish" |
13 | 13 | }; |
14 | 14 |
|
| 15 | + const config = { |
| 16 | + mainRunnerX: "-25px", // runner position next to the vertical line |
| 17 | + firstSideRunnerX: 55, // runner position on first side slide |
| 18 | + sideStep: 28 // distance between side slide dots |
| 19 | + }; |
| 20 | + |
15 | 21 | const state = { |
16 | 22 | sections: [], |
17 | 23 | currentSectionIndex: 0, |
|
27 | 33 | .map((section, index, array) => { |
28 | 34 | const anchor = section.dataset.anchor || `section-${index}`; |
29 | 35 | const slides = Array.from(section.querySelectorAll(".slide")); |
| 36 | + const slideCount = Math.max(1, slides.length); |
30 | 37 |
|
31 | 38 | return { |
32 | 39 | anchor, |
33 | 40 | label: sectionLabels[anchor] || anchor, |
34 | | - hasSlides: slides.length > 1, |
35 | | - slideCount: slides.length, |
| 41 | + slideCount, |
| 42 | + extraSlides: Math.max(0, slideCount - 1), |
| 43 | + hasSlides: slideCount > 1, |
36 | 44 | isStart: index === 0, |
37 | 45 | isFinish: index === array.length - 1 |
38 | 46 | }; |
|
56 | 64 | </div> |
57 | 65 |
|
58 | 66 | <div class="race-finish-wrap" aria-hidden="true"> |
59 | | - <div class="race-finish-label">FINISH</div> |
| 67 | + <div class="race-finish-label">FINISH</div> |
60 | 68 | </div> |
61 | 69 | </div> |
62 | 70 | `; |
|
75 | 83 |
|
76 | 84 | state.sections.forEach((section, index) => { |
77 | 85 | const checkpoint = document.createElement("button"); |
78 | | - |
79 | 86 | checkpoint.className = "race-checkpoint"; |
80 | 87 | checkpoint.type = "button"; |
81 | | - checkpoint.dataset.index = index; |
| 88 | + checkpoint.dataset.index = String(index); |
82 | 89 | checkpoint.style.top = `${getProgressPercent(index)}%`; |
83 | 90 |
|
| 91 | + const sideDots = Array.from({ length: section.extraSlides }, (_, i) => { |
| 92 | + const slideNumber = i + 1; |
| 93 | + const left = 66 + i * config.sideStep; |
| 94 | + |
| 95 | + return ` |
| 96 | + <span |
| 97 | + class="race-side-dot" |
| 98 | + data-slide="${slideNumber}" |
| 99 | + style="left: ${left}px;" |
| 100 | + ></span> |
| 101 | + `; |
| 102 | + }).join(""); |
| 103 | + |
| 104 | + const sidePathWidth = |
| 105 | + section.extraSlides > 0 |
| 106 | + ? 18 + (section.extraSlides - 1) * config.sideStep |
| 107 | + : 0; |
| 108 | + |
| 109 | + checkpoint.style.setProperty("--side-path-width", `${sidePathWidth}px`); |
| 110 | + |
84 | 111 | checkpoint.innerHTML = ` |
85 | 112 | <span class="race-dot"></span> |
86 | | - ${section.hasSlides ? `<span class="race-side-dot"></span>` : ""} |
| 113 | + ${sideDots} |
87 | 114 | <span class="race-label">${section.label}</span> |
88 | 115 | `; |
89 | 116 |
|
90 | | - checkpoint.addEventListener("click", () => { |
91 | | - if (window.fullpage_api) { |
92 | | - window.fullpage_api.moveTo(index + 1); |
93 | | - } |
| 117 | + checkpoint.addEventListener("click", (event) => { |
| 118 | + if (!window.fullpage_api) return; |
| 119 | + |
| 120 | + const sideDot = event.target.closest(".race-side-dot"); |
| 121 | + |
| 122 | + if (sideDot) { |
| 123 | + const slideNumber = Number(sideDot.dataset.slide); |
| 124 | + window.fullpage_api.moveTo(index + 1, slideNumber); |
| 125 | + return; |
| 126 | + } |
| 127 | + window.fullpage_api.moveTo(index + 1, 0); |
94 | 128 | }); |
95 | 129 |
|
96 | 130 | container.appendChild(checkpoint); |
|
104 | 138 | const safeSectionIndex = clamp(sectionIndex, 0, state.sections.length - 1); |
105 | 139 | const currentSection = state.sections[safeSectionIndex]; |
106 | 140 |
|
| 141 | + const safeSlideIndex = clamp( |
| 142 | + Number(slideIndex) || 0, |
| 143 | + 0, |
| 144 | + currentSection.slideCount - 1 |
| 145 | + ); |
| 146 | + |
107 | 147 | state.currentSectionIndex = safeSectionIndex; |
108 | | - state.currentSlideIndex = slideIndex; |
| 148 | + state.currentSlideIndex = safeSlideIndex; |
109 | 149 |
|
110 | 150 | const progress = getProgressPercent(safeSectionIndex); |
111 | 151 |
|
112 | 152 | nav.style.setProperty("--race-progress", `${progress}%`); |
113 | 153 |
|
| 154 | + updateRunner(nav, currentSection, progress, safeSlideIndex); |
| 155 | + updateNavClasses(nav, currentSection, safeSectionIndex, direction); |
| 156 | + updateCheckpoints(safeSectionIndex, safeSlideIndex); |
| 157 | + } |
| 158 | + |
| 159 | + function updateRunner(nav, currentSection, progress, slideIndex) { |
114 | 160 | const runner = nav.querySelector(".race-runner"); |
115 | | - if (runner) { |
| 161 | + if (!runner) return; |
| 162 | + |
116 | 163 | runner.style.top = `${progress}%`; |
117 | 164 |
|
118 | | - const shouldMoveRight = |
119 | | - currentSection && |
120 | | - currentSection.hasSlides && |
121 | | - slideIndex > 0; |
| 165 | + let runnerX = config.mainRunnerX; |
122 | 166 |
|
123 | | - // left of vertical line by default, move right when on horizontal slide |
124 | | - runner.style.setProperty( |
125 | | - "--runner-x", |
126 | | - shouldMoveRight ? "60px" : "-20px" |
127 | | - ); |
| 167 | + if (currentSection.hasSlides && slideIndex > 0) { |
| 168 | + runnerX = `${config.firstSideRunnerX + (slideIndex - 1) * config.sideStep}px`; |
128 | 169 | } |
129 | 170 |
|
130 | | - nav.classList.toggle("race-is-start", safeSectionIndex === 0); |
| 171 | + runner.style.setProperty("--runner-x", runnerX); |
| 172 | + } |
| 173 | + |
| 174 | + function updateNavClasses(nav, currentSection, sectionIndex, direction) { |
| 175 | + nav.classList.toggle("race-is-start", sectionIndex === 0); |
131 | 176 | nav.classList.toggle("race-is-finish", Boolean(currentSection?.isFinish)); |
132 | 177 |
|
133 | 178 | nav.classList.toggle("race-going-up", direction === "up"); |
|
141 | 186 | nav._raceTimer = window.setTimeout(() => { |
142 | 187 | nav.classList.remove("race-is-moving"); |
143 | 188 | }, 700); |
144 | | - |
145 | | - updateCheckpoints(safeSectionIndex, slideIndex); |
146 | 189 | } |
147 | 190 |
|
148 | 191 | function updateCheckpoints(activeSectionIndex, activeSlideIndex) { |
149 | 192 | document.querySelectorAll(".race-checkpoint").forEach((checkpoint, index) => { |
150 | | - const section = state.sections[index]; |
151 | | - |
152 | 193 | checkpoint.classList.toggle("is-active", index === activeSectionIndex); |
153 | 194 | checkpoint.classList.toggle("is-passed", index < activeSectionIndex); |
154 | 195 |
|
155 | | - const sideDot = checkpoint.querySelector(".race-side-dot"); |
| 196 | + const sideDots = checkpoint.querySelectorAll(".race-side-dot"); |
| 197 | + |
| 198 | + sideDots.forEach((sideDot) => { |
| 199 | + const slideNumber = Number(sideDot.dataset.slide); |
156 | 200 |
|
157 | | - if (sideDot && section) { |
158 | 201 | const sidePassed = |
159 | 202 | index < activeSectionIndex || |
160 | | - (index === activeSectionIndex && activeSlideIndex > 0); |
| 203 | + (index === activeSectionIndex && activeSlideIndex > slideNumber); |
161 | 204 |
|
162 | 205 | const sideActive = |
163 | | - index === activeSectionIndex && activeSlideIndex > 0; |
| 206 | + index === activeSectionIndex && activeSlideIndex === slideNumber; |
164 | 207 |
|
165 | 208 | sideDot.classList.toggle("is-passed-side", sidePassed); |
166 | 209 | sideDot.classList.toggle("is-active-side", sideActive); |
167 | | - } |
| 210 | + }); |
168 | 211 | }); |
169 | 212 | } |
170 | 213 |
|
|
0 commit comments