1import { Controller } from "@hotwired/stimulus"; 2 3// Continuously scrolls its track horizontally for a seamless logo marquee. 4// Items are duplicated once so the loop has no visible seam, and the track 5// bleeds to the viewport edges. Pauses on hover/focus. When all items already 6// fit within the viewport, the track is simply centered and does not move. 7// Honors prefers-reduced-motion by falling back to a manually scrollable strip. 8export default class extends Controller { 9 static targets = ["track"]; 10 static values = { speed: { type: Number, default: 40 } }; // px per second 11 12 connect() { 13 this.#setup(); 14 this.onResize = this.#debounce(() => this.#reset(), 150); 15 window.addEventListener("resize", this.onResize); 16 } 17 18 disconnect() { 19 window.removeEventListener("resize", this.onResize); 20 this.#stop(); 21 } 22 23 pause() { 24 this.paused = true; 25 } 26 27 resume() { 28 this.paused = false; 29 } 30 31 #setup() { 32 // Everything fits: center the row and leave it still. 33 if (this.trackTarget.scrollWidth <= this.element.clientWidth) { 34 this.element.classList.remove("overflow-hidden", "overflow-x-auto"); 35 this.trackTarget.classList.remove("w-max"); 36 this.trackTarget.classList.add("w-full", "justify-center"); 37 return; 38 } 39 40 // Reduced motion: let the user scroll the overflow manually. 41 if (window.matchMedia("(prefers-reduced-motion: reduce)").matches) { 42 this.element.classList.remove("overflow-hidden"); 43 this.element.classList.add("overflow-x-auto"); 44 return; 45 } 46 47 this.#duplicateItems(); 48 this.offset = 0; 49 this.paused = false; 50 this.lastTime = null; 51 this.frame = requestAnimationFrame(this.#step); 52 } 53 54 #reset() { 55 this.#stop(); 56 this.element.classList.add("overflow-hidden"); 57 this.element.classList.remove("overflow-x-auto"); 58 this.trackTarget.classList.add("w-max"); 59 this.trackTarget.classList.remove("w-full", "justify-center"); 60 this.trackTarget.style.transform = ""; 61 this.trackTarget.querySelectorAll("[data-marquee-clone]").forEach((clone) => clone.remove()); 62 this.#setup(); 63 } 64 65 #stop() { 66 if (this.frame) cancelAnimationFrame(this.frame); 67 this.frame = null; 68 } 69 70 #duplicateItems() { 71 Array.from(this.trackTarget.children).forEach((item) => { 72 const clone = item.cloneNode(true); 73 clone.setAttribute("aria-hidden", "true"); 74 clone.setAttribute("data-marquee-clone", ""); 75 this.trackTarget.appendChild(clone); 76 }); 77 } 78 79 #step = (time) => { 80 if (this.lastTime === null) this.lastTime = time; 81 const delta = (time - this.lastTime) / 1000; 82 this.lastTime = time; 83 84 if (!this.paused) { 85 this.offset += this.speedValue * delta; 86 const half = this.trackTarget.scrollWidth / 2; 87 if (half > 0 && this.offset >= half) this.offset -= half; 88 this.trackTarget.style.transform = `translateX(${-this.offset}px)`; 89 } 90 91 this.frame = requestAnimationFrame(this.#step); 92 }; 93 94 #debounce(fn, wait) { 95 let timer; 96 return () => { 97 clearTimeout(timer); 98 timer = setTimeout(fn, wait); 99 }; 100 } 101}
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.