1//minimum pixel threshhold before considering the gesture dragging 2const DX = 5; 3let isDragging = false; 4let isMouseDown = false; 5let currentDraggedElement = null; 6let scrollMultiplier = 1; 7let startX, scrollLeft; 8let lastX, lastTime, velocity; 9let isCarousel = false; 10let carouselInitialPage = 0; 11let target = null; 12let pointerId = null; 13 14const onPointerDown = (e) => { 15 currentDraggedElement = e.currentTarget; 16 target = e.target; 17 18 isCarousel = currentDraggedElement.getAttribute("carousel") !== null; 19 if (isCarousel) { 20 carouselInitialPage = Math.round( 21 currentDraggedElement.scrollLeft / getCarouselStride(currentDraggedElement) 22 ); 23 } 24 25 const scrollMultiplierValue = Number.parseFloat( 26 currentDraggedElement.getAttribute("scroll-multiplier") 27 ); 28 scrollMultiplier = scrollMultiplierValue || 1; 29 30 startX = e.pageX - currentDraggedElement.offsetLeft; 31 scrollLeft = currentDraggedElement.scrollLeft; 32 33 currentDraggedElement.onpointermove = move; 34 currentDraggedElement.setPointerCapture(e.pointerId); 35 pointerId = e.pointerId; 36 37 lastX = e.clientX; 38 lastTime = Date.now(); 39 isMouseDown = true; 40}; 41 42const onMouseUp = (e) => { 43 if (!currentDraggedElement) { 44 return; 45 } 46 47 if (!isDragging) { 48 target?.dispatchEvent(new PointerEvent("click", e)); 49 reset(); 50 return; 51 } 52 53 currentDraggedElement.classList.remove("isDragging"); 54 55 if (!isCarousel) { 56 applyMomentumScroll(currentDraggedElement, velocity); 57 reset(); 58 return; 59 } 60 const page = Math.round( 61 currentDraggedElement.scrollLeft / getCarouselStride(currentDraggedElement) 62 ); 63 64 const reachedXThreshhold = page !== carouselInitialPage; 65 const reachedVThreshhold = Math.abs(velocity) > 0.12; 66 if (reachedXThreshhold) { 67 scrollToCarouselPage(currentDraggedElement, () => page); 68 } else if (reachedVThreshhold) { 69 const delta = velocity < 0 ? 1 : -1; 70 scrollToCarouselPage(currentDraggedElement, () => page + delta); 71 } else { 72 scrollToCarouselPage(currentDraggedElement, () => carouselInitialPage); 73 } 74 75 reset(); 76}; 77 78const friction = 0.95; 79const minVelocity = 2; 80function applyMomentumScroll(container, initialVelocity) { 81 let velocity = initialVelocity * 20; 82 function step(cont) { 83 if (Math.abs(velocity) > minVelocity) { 84 requestAnimationFrame(() => { 85 step(cont); 86 container.scrollTo({ 87 left: cont.scrollLeft - velocity, 88 behavior: "instant", 89 }); 90 velocity *= friction; 91 }); 92 } 93 } 94 95 requestAnimationFrame(() => step(container)); 96} 97 98function reset() { 99 currentDraggedElement.onpointermove = null; 100 currentDraggedElement.releasePointerCapture(pointerId); 101 isMouseDown = false; 102 currentDragId = null; 103 currentDraggedElement = null; 104 carouselInitialPage = 0; 105 target = null; 106 pointerId = null; 107} 108 109function onClick(e) { 110 if (isDragging) { 111 e.preventDefault(); 112 e.stopImmediatePropagation(); 113 } 114 isDragging = false; 115} 116 117const move = (e) => { 118 if (!isMouseDown) { 119 return; 120 } 121 122 if (!isDragging) { 123 isDragging = Math.abs(startX - e.clientX) > DX; 124 } 125 126 currentDraggedElement.classList.add("isDragging"); 127 128 const x = e.pageX - currentDraggedElement.offsetLeft; 129 const scroll = x - startX; 130 131 const now = Date.now(); 132 velocity = (e.clientX - lastX) / (now - lastTime); 133 134 lastX = e.clientX; 135 lastTime = now; 136 137 scrollable = currentDraggedElement; 138 requestAnimationFrame(() => { 139 scrollable.scroll({ 140 left: scrollLeft - scroll * scrollMultiplier, 141 behavior: "instant", 142 }); 143 }); 144}; 145 146document.addEventListener("DOMContentLoaded", () => { 147 const isPC = window.matchMedia("(pointer: fine)").matches; 148 document.querySelectorAll("[scroll-container]").forEach((container) => { 149 if (container.hasAttribute("init") && container.scrollLeft !== 0) { 150 container.scrollTo({ 151 left: 0, 152 behavior: "instant", 153 }); 154 } 155 156 if (container.hasAttribute("carousel")) { 157 initialiazeCarousel(container); 158 } 159 160 if (isPC) { 161 container.addEventListener("pointerdown", onPointerDown); 162 container.addEventListener("click", onClick); 163 } else { 164 container.addEventListener("scroll", updateIndicatorOnPointerScroll); 165 } 166 }); 167 168 if (isPC) { 169 document.addEventListener("mouseup", onMouseUp); 170 } 171}); 172 173//distance between the left edges of two neighbouring cards (card
173width + gap) 174function getCarouselStride(container) { 175 const items = container.querySelectorAll("[scrollable]"); 176 if (items.length > 1) { 177 return ( 178 items[1].getBoundingClientRect().left - 179 items[0].getBoundingClientRect().left 180 ); 181 } 182 return container.getBoundingClientRect().width; 183} 184 185function scrollToCarouselPage(scrollContainer, getPage = () => 0) { 186 const indicatorContainer = scrollContainer.parentElement.querySelector( 187 "[carousel-indicator-container]" 188 ); 189 const stride = getCarouselStride(scrollContainer); 190 191 const currentPage = Math.round(scrollContainer.scrollLeft / stride); 192 const pageCount = Math.round(scrollContainer.scrollWidth / stride); 193 const nextPage = getPage(currentPage); 194 if (nextPage < 0 || nextPage >= pageCount) { 195 return; 196 } 197 198 requestAnimationFrame(() => { 199 scrollContainer.scroll({ 200 left: nextPage * stride, 201 behavior: "smooth", 202 }); 203 }); 204 setIndicatorState(indicatorContainer, nextPage); 205} 206 207function updateIndicatorOnPointerScroll(e) { 208 const container = e.currentTarget; 209 const indicatorContainer = container.parentElement.querySelector( 210 "[carousel-indicator-container]" 211 ); 212 const stride = getCarouselStride(container); 213 const page = container.scrollLeft / stride; 214 const pageToReach = Math.round(container.scrollLeft / stride); 215 if (Math.abs(page - pageToReach) < 0.05) { 216 setIndicatorState(indicatorContainer, pageToReach); 217 } 218} 219 220function setIndicatorState(indicatorContainer, activePage) { 221 [...indicatorContainer.children].forEach((indicator, i) => { 222 if (activePage === i) { 223 indicator.classList.add("active"); 224 } else { 225 indicator.classList.remove("active"); 226 } 227 }); 228} 229 230function initialiazeCarousel(container) { 231 const indicatorContainer = container.parentElement.querySelector( 232 "[carousel-indicator-container]" 233 ); 234 const currentPage = Math.round( 235 container.scrollLeft / getCarouselStride(container) 236 ); 237 indicatorContainer.children.item(currentPage).classList.add("active"); 238 239 const prev = container.parentElement.querySelector("[carousel-prev]"); 240 prev.onclick = () => { 241 scrollToCarouselPage( 242 container, 243 (getPage = (currentPage) => currentPage - 1) 244 ); 245 }; 246 247 const next = container.parentElement.querySelector("[carousel-next]"); 248 next.onclick = () => { 249 scrollToCarouselPage( 250 container, 251 (getPage = (currentPage) => currentPage + 1) 252 ); 253 }; 254 255 [...indicatorContainer.children].forEach((indicator, i) => { 256 indicator.onclick = () => { 257 scrollToCarouselPage(container, (getPage = (currentPage) => i)); 258 }; 259 }); 260}
Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.