PageSourceSearch

https://www.davidrumsey.com/assets/controllers/carousel_controller…b4786f9da4e5705046546b245d2f55ca16bdbc0f.js

js davidrumsey.com collected 2026-09-24 08:45:37 UTC 5,323 bytes, 219 lines download raw bytes

1import { Controller } from "@hotwired/stimulus"
2import useActions from "stimulus-use-actions"
3import Mustache from "mustache"
4import "swiped-events"
5
6export default class extends Controller {
7  static targets = [
8    "slides",
9    "template",
10  ]
11
12  async connect() {
13    this.template = this.templateTarget.innerHTML
14    this.templateTarget.remove()
15
16    useActions(this, {
17      element: [
18        "mouseover->stop",
19        "mouseout->start",
20        "swiped-left->next",
21        "swiped-right->prev",
22      ]
23    })
24
25    this.margin = 5
26    this.interval = 7000
27
28    this.images = new SlideStream(await window.carousel.firstPage, page => this.fetchPage(page))
29    this.slidesTarget.classList.add("-moving")
30    this.slides = [this.buildSlide({ center: this.width / 2 })]
31    this.currentSlideIndex = 0
32    this.render()
33  }
34
35  fetchPage(page) {
36    const params = new URLSearchParams({ seed: window.carousel.seed, page })
37    return fetch(`/cells/carousel.js?${params}`, { headers: { Accept: "application/json" } }).then(response => response.json())
38  }
39
40  render() {
41    this.preloadSlides()
42    this.cullSlides()
43    this.setCurrentSlide()
44    this.start()
45  }
46
47  preloadSlides() {
48    while(this.right < this.rightBound) {
49      this.slides.push(this.buildSlide({
50        after: this.slides.at(-1).nextX,
51      }))
52    }
53    while(this.left > this.leftBound) {
54      this.slides.unshift(this.buildSlide({
55        before: this.slides.at(0).prevX,
56      }))
57      this.currentSlideIndex += 1
58    }
59  }
60
61  cullSlides() {
62    this.slides.forEach(slide => {
63      if(slide.prevX > this.rightBound) {
64        this.removeSlide(slide)
65      }
66
67      if(slide.nextX < this.leftBound) {
68        this.removeSlide(slide)
69        this.currentSlideIndex -= 1
70      }
71    })
72  }
73
74  setCurrentSlide() {
75    this.slides.forEach((slide, index) => {
76      slide.element.classList.toggle("current", this.currentSlideIndex === index)
77    })
78  }
79
80  buildSlide(opts={}) {
81    return new Slide({
82      margin: this.margin,
83      image: this.images.pop(),
84      template: this.template,
85      parent: this.slidesTarget,
86      height: this.slideHeight,
87      ...opts,
88    })
89  }
90
91  get slideHeight() {
92    return parseInt(getComputedStyle(this.element).getPropertyValue("--slide-height"))
93  }
94
95  removeSlide(slide) {
96    slide.element.remove()
97    const index = this.slides.indexOf(slide)
98    this.slides.splice(index, 1)
99  }
100
101  get left() {
102    return Math.min(...this.slides.map(s => s.x - s.margin))
103  }
104
105  get right() {
106    return Math.max(...this.slides.map(s => s.x + s.width + s.margin))
107  }
108
109  get leftBound() {
110    return this.width * -1
111  }
112
113  get rightBound() {
114    return this.width * 2
115  }
116
117  get width() {
118    return this.element.clientWidth
119  }
120
121  get currentSlide() {
122    return this.slides[this.currentSlideIndex]
123  }
124
125  get prevSlide() {
126    return this.slides[this.currentSlideIndex - 1]
127  }
128
129  get nextSlide() {
130    return this.slides[this.currentSlideIndex + 1]
131  }
132
133  // actions
134
135  prev() {
136    Array.from(this.slidesTarget.children).forEach(e => e.style.removeProperty("transition"))
137    const delta = (this.currentSlide.width + this.prevSlide.width) * 0.5 + this.margin * 2
138    this.slides.forEach(slide => slide.move(delta))
139    this.currentSlideIndex -= 1
140    this.render()
141  }
142
143  next() {
144    Array.from(this.slidesTarget.children).forEach(e => e.style.removeProperty("transition"))
145    const delta = (this.currentSlide.width + this.nextSlide.width) * -0.5 - this.margin * 2
146    this.slides.forEach(slide => slide.move(delta))
147    this.currentSlideIndex += 1
148    this.render()
149  }
150
151  start() {
152    this.stop()
153    this.timeout = setTimeout(() => this.next(), this.interval)
154  }
155
156  stop() {
157    if(this.timeout != null) clearInterval(this.timeout)
158  }
159}
160
161class Slide {
162  constructor({ margin, image, template, parent, before, after, center, height }) {
163    this.margin = margin
164    this.width = Math.round(image.aspectRatio * height)
165    this.x = after || before && (before - this.width) || center - (this.width + this.margin + 2) / 2
166    this.element = this.buildElement(template, image)
167    parent.insertAdjacentElement(before ? "afterBegin" : "beforeEnd", this.element)
168    this.render()
169  }
170
171  buildElement(template, image) {
172    const container = document.createElement("template");
173    container.innerHTML = Mustache.render(template, image)
174    return container.content.firstElementChild
175  }
176
177  get nextX() {
178    return this.x + this.width + this.margin * 2
179  }
180
181  get prevX() {
182    return this.x - this.margin * 2
183  }
184
185  render() {
186    this.element.style.transform = `translateX(${this.x}px)`
187  }
188
189  move(x) {
190    this.x += x
191    this.render()
192  }
193}
194
195// Pages through one seeded ordering; once every image has been shown it cycles the ones already seen.
196class SlideStream {
197  constructor(images, fetchPage) {
198    this.fresh = images
199    this.seen = []
200    this.fetchPage = fetchPage
201    this.page = 0
202  }
203
204  pop() {
205    const image = this.fresh.shift() ?? this.seen.shift()
206    this.seen.push(image)
207    if(this.fresh.length < 10) this.load()
208    return image
209  }
210
211  async load() {
212    if(this.loading || this.exhausted) return
213    this.loading = true
214    const images = await this.fetchPage(++this.page)
215    this.exhausted = images.length === 0
216    this.fresh.push(...images)
217    this.loading = false
218  }
219};

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.