1import { Controller } from "@hotwired/stimulus" 2 3// A running time-tracker total that actually moves. 4// 5// The server renders the completed total and, when a timer is running, the 6// instant it started. This adds the two together once a second, so "hours 7// logged" includes the timer you're currently running instead of jumping only 8// when you stop it. 9// 10// Values 11// startAt ISO8601 instant the running timer began. Absent â static. 12// baseSeconds Already-completed seconds this readout should include. 13// rate Hourly rate; when set, the amount target ticks alongside. 14// showSeconds HH:MM:SS (an entry) vs HH:MM (a rolled-up total). 15// 16// The interval is cleared on disconnect: Turbo restores cached pages, and a 17// timer still pointing at a detached element leaks on every visit. 18export default class extends Controller { 19 static targets = ["duration", "amount"] 20 static values = { 21 startAt: String, 22 baseSeconds: { type: Number, default: 0 }, 23 rate: { type: Number, default: 0 }, 24 showSeconds: { type: Boolean, default: true } 25 } 26 27 connect() { 28 this.start = this.startAtValue ? new Date(this.startAtValue) : null 29 if (this.start && isNaN(this.start.getTime())) this.start = null 30 31 this._tick() 32 // No running timer means nothing changes â render once and stop, rather 33 // than waking up every second to write the same string. 34 if (this.start) this.timer = setInterval(() => this._tick(), 1000) 35 } 36 37 disconnect() { 38 if (this.timer) clearInterval(this.timer) 39 this.timer = null 40 } 41 42 _tick() { 43 const running = this.start ? Math.max(0, Math.floor((Date.now() - this.start.getTime()) / 1000)) : 0 44 const total = this.baseSecondsValue + running 45 46 if (this.hasDurationTarget) { 47 this.durationTarget.textContent = this._format(total) 48 } 49 50 if (this.hasAmountTarget && this.rateValue > 0) { 51 const amount = (total / 3600) * this.rateValue 52 this.amountTarget.textContent = `$${amount.toFixed(2)}` 53 } 54 } 55 56 _format(totalSeconds) { 57 const hours = Math.floor(totalSeconds / 3600) 58 const minutes = Math.floor((totalSeconds % 3600) / 60) 59 const pad = n => String(n).padStart(2, "0") 60 61 if (!this.showSecondsValue) return `${pad(hours)}:${pad(minutes)}` 62 return `${pad(hours)}:${pad(minutes)}:${pad(totalSeconds % 60)}` 63 } 64};
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.