PageSourceSearch

https://www.georgetowncoc.com/assets/controllers/hero_carousel_controller-90127b06.js

js georgetowncoc.com collected 2026-09-25 04:04:52 UTC 11,673 bytes, 353 lines download raw bytes

1import { Controller } from "@hotwired/stimulus"
2
3const MOTION_PRESETS = [
4  {
5    panFromX: -4.2,
6    panFromY: -2.6,
7    panToX: 4.8,
8    panToY: 2.9,
9    softPanFromX: -2.3,
10    softPanFromY: -1.4,
11    softPanToX: 2.6,
12    softPanToY: 1.6,
13    zoomFrom: 1.06,
14    zoomTo: 1.21,
15    softZoomFromDelta: 0.03,
16    softZoomToDelta: 0.12
17  },
18  {
19    panFromX: 4.4,
20    panFromY: -2.8,
21    panToX: -4.6,
22    panToY: 2.8,
23    softPanFromX: 2.4,
24    softPanFromY: -1.5,
25    softPanToX: -2.5,
26    softPanToY: 1.5,
27    zoomFrom: 1.07,
28    zoomTo: 1.22,
29    softZoomFromDelta: 0.04,
30    softZoomToDelta: 0.13
31  },
32  {
33    panFromX: -2.6,
34    panFromY: 3.3,
35    panToX: 3.2,
36    panToY: -3.6,
37    softPanFromX: -1.6,
38    softPanFromY: 2.1,
39    softPanToX: 1.9,
40    softPanToY: -2.2,
41    zoomFrom: 1.05,
42    zoomTo: 1.2,
43    softZoomFromDelta: 0.03,
44    softZoomToDelta: 0.11
45  },
46  {
47    panFromX: 3.5,
48    panFromY: 2.9,
49    panToX: -3.4,
50    panToY: -3.2,
51    softPanFromX: 2,
52    softPanFromY: 1.8,
53    softPanToX: -2,
54    softPanToY: -1.9,
55    zoomFrom: 1.06,
56    zoomTo: 1.21,
57    softZoomFromDelta: 0.03,
58    softZoomToDelta: 0.12
59  }
60]
61
62export default class extends Controller {
63  static targets = ["slide", "dot"]
64  static values = {
65    interval: Number,
66    transition: Number
67  }
68
69  connect() {
70    this.index = 0
71    this.motionPresetIndex = 0
72    this.preloadedSlideImages = []
73    this.preloadedSlideSrcs = new Set()
74    this.isInViewport = true
75    this.visibilityObserver = null
76    this.restartAnimationFrame = null
77    this.tickTimer = null
78    this.aspectRefreshFrame = null
79    this.onVisibilityChange = () => {
80      if (document.hidden) {
81        this.stopAuto()
82      } else if (this.isInViewport) {
83        this.startAuto(this.cycleDelayMs())
84      }
85    }
86    this.onResize = () => this.scheduleAspectModeRefresh()
87
88    // We intentionally avoid hover pause; on coarse/touch devices and mixed-input
89    // browsers it can intermittently pause without a matching resume event.
90    document.addEventListener("visibilitychange", this.onVisibilityChange)
91    window.addEventListener("resize", this.onResize)
92
93    this.setupVisibilityObserver()
94    this.updateSlideAspectModes()
95    this.show(this.index)
96    if (this.isInViewport) this.startAuto(this.cycleDelayMs())
97  }
98
99  disconnect() {
100    document.removeEventListener("visibilitychange", this.onVisibilityChange)
101    window.removeEventListener("resize", this.onResize)
102    if (this.visibilityObserver) this.visibilityObserver.disconnect()
103    this.visibilityObserver = null
104    if (this.restartAnimationFrame) window.cancelAnimationFrame(this.restartAnimationFrame)
105    this.restartAnimationFrame = null
106    if (this.aspectRefreshFrame) window.cancelAnimationFrame(this.aspectRefreshFrame)
107    this.aspectRefreshFrame = null
108    this.preloadedSlideImages = []
109    this.preloadedSlideSrcs.clear()
110    this.stopAuto()
111  }
112
113  next(event) {
114    event?.preventDefault()
115    this.advance(1, true)
116  }
117
118  prev(event) {
119    event?.preventDefault()
120    this.advance(-1, true)
121  }
122
123  go(event) {
124    event?.preventDefault()
125    const nextIndex = Number.parseInt(event.currentTarget?.dataset?.heroCarouselIndex, 10)
126    if (!Number.isFinite(nextIndex)) return
127    this.show(nextIndex)
128    this.startAuto(this.cycleDelayMs())
129  }
130
131  stopAuto() {
132    if (!this.tickTimer) return
133    window.clearTimeout(this.tickTimer)
134    this.tickTimer = null
135  }
136
137  startAuto(delayMs) {
138    if (this.slideTargets.length <= 1) return
139    this.stopAuto()
140    const safeDelay = Number.isFinite(delayMs) ? delayMs : this.cycleDelayMs()
141    this.tickTimer = window.setTimeout(() => this.tick(), safeDelay)
142  }
143
144  tick() {
145    this.tickTimer = null
146    if (this.slideTargets.length <= 1) return
147    this.advance(1)
148    this.startAuto(this.cycleDelayMs())
149  }
150
151  advance(step, restartTimer = false) {
152    const count = this.slideTargets.length
153    if (count <= 1) return
154    this.show((this.index + step + count) % count)
155    if (restartTimer) this.startAuto(this.cycleDelayMs())
156  }
157
158  show(index) {
159    const count = this.slideTargets.length
160    if (count === 0) return
161
162    const nextIndex = ((index % count) + count) % count
163    this.index = nextIndex
164
165    this.slideTargets.forEach((slide, idx) => {
166      const isActive = idx === this.index
167      slide.classList.toggle("is-active", isActive)
168      slide.classList.toggle("opacity-100", isActive)
169      slide.classList.toggle("pointer-events-auto", isActive)
170      slide.classList.toggle("opacity-0", !isActive)
171      slide.classList.toggle("pointer-events-none", !isActive)
172      slide.setAttribute("aria-hidden", isActive ? "false" : "true")
173
174      if (isActive) this.restartSlideMotion(slide)
175    })
176
177    this.dotTargets.forEach((dot, idx) => {
178      const isActive = idx === this.index
179      dot.classList.toggle("bg-white", isActive)
180      dot.classList.toggle("bg-white/40", !isActive)
181      dot.setAttribute("aria-current", isActive ? "true" : "false")
182    })
183
184    this.applyAnimationPlayState()
185    this.preloadSlideAt(this.index + 1)
186  }
187
188  intervalMs() {
189    const parsed = Number.parseInt(this.intervalValue, 10)
190    return Number.isFinite(parsed) && parsed >= 1500 ? parsed : 6000
191  }
192
193  transitionMs() {
194    const parsed = Number.parseInt(this.transitionValue, 10)
195    return Number.isFinite(parsed) && parsed >= 0 ? parsed : 700
196  }
197
198  cycleDelayMs() {
199    const delay = this.intervalMs() - this.transitionMs()
200    return delay >= 500 ? delay : this.intervalMs()
201  }
202
203  restartSlideMotion(slide) {
204    if (!slide.classList.contains("hero-carousel-slide")) return
205
206    const image = slide.querySelector(".hero-carousel-slide-image")
207    if (!image) return
208
209    this.applyMotionPreset(slide)
210
211    if (this.restartAnimationFrame) window.cancelAnimationFrame(this.restartAnimationFrame)
212    image.style.animation = "none"
213    this.restartAnimationFrame = window.requestAnimationFrame(() => {
214      image.style.animation = ""
215      image.style.animationPlayState = this.isInViewport ? "running" : "paused"
216      this.restartAnimationFrame = null
217    })
218  }
219
220  applyMotionPreset(slide) {
221    const preset = MOTION_PRESETS[this.motionPresetIndex % MOTION_PRESETS.length]
222    this.motionPresetIndex += 1
223
224    slide.style.setProperty("--hero-pan-x-start", `${preset.panFromX}%`)
225    slide.style.setProperty("--hero-pan-y-start", `${preset.panFromY}%`)
226    slide.style.setProperty("--hero-pan-x-end", `${preset.panToX}%`)
227    slide.style.setProperty("--hero-pan-y-end", `${preset.panToY}%`)
228
229    slide.style.setProperty("--hero-pan-out-x-start", `${preset.panToX}%`)
230    slide.style.setProperty("--hero-pan-out-y-start", `${preset.panToY}%`)
231    slide.style.setProperty("--hero-pan-out-x-end", `${preset.panFromX}%`)
232    slide.style.setProperty("--hero-pan-out-y-end", `${preset.panFromY}%`)
233
234    slide.style.setProperty("--hero-pan-soft-x-start", `${preset.softPanFromX}%`)
235    slide.style.setProperty("--hero-pan-soft-y-start", `${preset.softPanFromY}%`)
236    slide.style.setProperty("--hero-pan-soft-x-end", `${preset.softPanToX}%`)
237    slide.style.setProperty("--hero-pan-soft-y-end", `${preset.softPanToY}%`)
238
239    slide.style.setProperty("--hero-pan-soft-out-x-start", `${preset.softPanToX}%`)
240    slide.style.setProperty("--hero-pan-soft-out-y-start", `${preset.softPanToY}%`)
241    slide.style.setProperty("--hero-pan-soft-out-x-end", `${preset.softPanFromX}%`)
242    slide.style.setProperty("--hero-pan-soft-out-y-end", `${preset.softPanFromY}%`)
243
244    slide.style.setProperty("--hero-zoom-start", String(preset.zoomFrom))
245    slide.style.setProperty("--hero-zoom-end", String(preset.zoomTo))
246    slide.style.setProperty("--hero-zoom-out-start", String(preset.zoomTo))
247    slide.style.setProperty("--hero-zoom-out-end", String(preset.zoomFrom))
248
249    slide.style.setProperty("--hero-soft-zoom-start-delta", String(preset.softZoomFromDelta))
250    slide.style.setProperty("--hero-soft-zoom-end-delta", String(preset.softZoomToDelta))
251    slide.style.setProperty("--hero-soft-zoom-out-start-delta", String(preset.softZoomToDelta))
252    slide.style.setProperty("--hero-soft-zoom-out-end-delta", String(preset.softZoomFromDelta))
253  }
254
255  preloadSlideAt(index) {
256    const count = this.slideTargets.length
257    if (count <= 1) return
258
259    const nextIndex = ((index % count) + count) % count
260    const slide = this.slideTargets[nextIndex]
261    if (!slide) return
262
263    const image = slide.querySelector(".hero-carousel-slide-image")
264    const src = image?.currentSrc || image?.src
265    if (!src || this.preloadedSlideSrcs.has(src)) return
266
267    const preloader = new Image()
268    preloader.decoding = "async"
269    preloader.src = src
270    this.preloadedSlideImages.push(preloader)
271    this.preloadedSlideSrcs.add(src)
272  }
273
274  setupVisibilityObserver() {
275    if (!("IntersectionObserver" in window)) return
276
277    this.visibilityObserver = new IntersectionObserver((entries) => {
278      const entry = entries[0]
279      if (!entry) return
280
281      const inViewport = entry.isIntersecting && entry.intersectionRatio > 0.12
282      if (inViewport === this.isInViewport) return
283
284      this.isInViewport = inViewport
285      this.applyAnimationPlayState()
286
287      if (this.isInViewport) {
288        this.restartSlideMotion(this.slideTargets[this.index])
289        this.startAuto(this.cycleDelayMs())
290      } else {
291        this.stopAuto()
292      }
293    }, { threshold: [0, 0.12, 0.25] })
294
295    this.visibilityObserver.observe(this.element)
296  }
297
298  applyAnimationPlayState() {
299    const state = this.isInViewport ? "running" : "paused"
300    this.slideTargets.forEach((slide) => {
301      const image = slide.querySelector(".hero-carousel-slide-image")
302      if (image) image.style.animationPlayState = state
303    })
304  }
305
306  scheduleAspectModeRefresh() {
307    if (this.aspectRefreshFrame) return
308    this.aspectRefreshFrame = window.requestAnimationFrame(() => {
309      this.aspectRefreshFrame = null
310      this.updateSlideAspectModes()
311    })
312  }
313
314  updateSlideAspectModes() {
315    this.slideTargets.forEach((slide) => {
316      const image = slide.querySelector(".hero-carousel-slide-image")
317      if (!image) return
318
319      if (!image.complete || image.naturalWidth <= 0 || image.naturalHeight <= 0) {
320        if (image.dataset.aspectListenerBound !== "true") {
321          image.addEventListener("load", () => this.scheduleAspectModeRefresh(), { once: true })
322          image.dataset.aspectListenerBound = "true"
323        }
324        return
325      }
326
327      const imageRatio = image.naturalWidth / image.naturalHeight
328      const slideWidth = slide.clientWidth
329      const slideHeight = slide.clientHeight
330      if (!(slideWidth > 0 && slideHeight > 0)) return
331
332      const slideRatio = slideWidth / slideHeight
333      const cropFraction =
334        imageRatio > slideRatio ? (1 - (slideRatio / imageRatio)) : (1 - (imageRatio / slideRatio))
335      const shouldContainEdges = cropFraction >= 0.2
336      const horizontalBars = imageRatio < slideRatio
337
338      if (shouldContainEdges) {
339        const containFillFraction = horizontalBars ? (imageRatio / slideRatio) : (slideRatio / imageRatio)
340        const targetFillFraction = 0.86
341        const rawScale = targetFillFraction / containFillFraction
342        const containScale = Math.min(1.42, Math.max(1.08, rawScale))
343        slide.style.setProperty("--hero-contain-scale", containScale.toFixed(3))
344      } else {
345        slide.style.removeProperty("--hero-contain-scale")
346      }
347
348      slide.classList.toggle("hero-carousel-slide--contain-edges", shouldContainEdges)
349      slide.classList.toggle("hero-carousel-slide--contain-edges-x", shouldContainEdges && horizontalBars)
350      slide.classList.toggle("hero-carousel-slide--contain-edges-y", shouldContainEdges && !horizontalBars)
351    })
352  }
353}

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.