PageSourceSearch

https://www.carwow.co.uk/assets/controllers/selling/approvals/checking_ticker_controller-a0d527bc.js

js carwow.co.uk collected 2026-09-24 09:12:13 UTC 3,021 bytes, 99 lines download raw bytes

1import { Controller } from '@hotwired/stimulus'
2
3const INTERVAL_MS = 2000
4
5export default class extends Controller {
6  static targets = ['track', 'ring', 'badge']
7
8  connect () {
9    this.index = 0
10    this.itemCount = this.trackTarget.children.length
11    this.stepHeight = this.itemCount > 1
12      ? this.trackTarget.children[1].offsetTop - this.trackTarget.children[0].offsetTop
13      : this.trackTarget.firstElementChild?.offsetHeight || 0
14
15    if (window.matchMedia('(prefers-reduced-motion: reduce)').matches) {
16      this.index = this.itemCount - 1
17      this.renderTicker()
18      this.setProgress(1)
19      this.complete()
20      this.ringTarget.classList.add('is-filled')
21      return
22    }
23
24    this.intervalId = setInterval(() => this.advance(), INTERVAL_MS)
25
26    this.ringDurationMs = INTERVAL_MS * this.itemCount
27    this.startedAt = performance.now()
28    this.frameId = requestAnimationFrame(() => this.animateRing())
29    // Drives completion off a timer, not the rAF loop below: requestAnimationFrame is paused
30    // entirely in backgrounded/hidden tabs, which would otherwise leave the ring stuck
31    // mid-fill (and the poller never started) for as long as the seller is tabbed away.
32    this.completionTimeoutId = setTimeout(() => this.finishRing(), this.ringDurationMs)
33  }
34
35  disconnect () {
36    if (this.intervalId) {
37      clearInterval(this.intervalId)
38    }
39
40    if (this.frameId) {
41      cancelAnimationFrame(this.frameId)
42    }
43
44    if (this.completionTimeoutId) {
45      clearTimeout(this.completionTimeoutId)
46    }
47  }
48
49  advance () {
50    this.index += 1
51    this.renderTicker()
52
53    if (this.index >= this.itemCount - 1) {
54      clearInterval(this.intervalId)
55    }
56  }
57
58  renderTicker () {
59    this.trackTarget.style.transform = `translateY(-${this.index * this.stepHeight}px)`
60  }
61
62  animateRing () {
63    const ratio = Math.min((performance.now() - this.startedAt) / this.ringDurationMs, 1)
64    this.setProgress(ratio)
65
66    if (ratio < 1) {
67      this.frameId = requestAnimationFrame(() => this.animateRing())
68    }
69  }
70
71  finishRing () {
72    this.setProgress(1)
73    this.complete()
74    this.ringTarget.classList.add('is-filled')
75
76    if (this.frameId) {
77      cancelAnimationFrame(this.frameId)
78    }
79  }
80
81  setProgress (ratio) {
82    this.ringTarget.style.setProperty('--progress', `${ratio * 100}%`)
83  }
84
85  complete () {
86    this.badgeTarget.classList.add('is-complete')
87
88    if (window.matchMedia('(prefers-reduced-motion: reduce)').matches) {
89      // Deferred: this runs from connect(), synchronously with every other controller's own
90      // connect() on the same element — dispatching immediately can fire before
91      // completion-poller has connected and attached its listener, losing the event for good.
92      setTimeout(() => this.dispatch('complete'), 0)
93      return
94    }
95
96    const rightStroke = this.badgeTarget.querySelector('.approval-loading__tick-stroke--right')
97    rightStroke.addEventListener('transitionend', () => this.dispatch('complete'), { once: true })
98  }
99}

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.