PageSourceSearch

https://railsblocks.com/assets/controllers/animated_number_control…4d8f99e89fed6834f5432fc3da637b3d23f1e8a8.js

js railsblocks.com collected 2026-09-25 20:22:36 UTC 6,048 bytes, 189 lines download raw bytes

1import { Controller } from "@hotwired/stimulus";
2import "number-flow";
3import { continuous } from "number-flow";
4
5export default class extends Controller {
6  static values = {
7    start: { type: Number, default: 0 }, // Start value of the animation
8    end: { type: Number, default: 0 }, // End value of the animation
9    duration: { type: Number, default: 700 }, // Duration of the animation for each number change
10    trigger: { type: String, default: "viewport" }, // Trigger for the animation (load, viewport, manual)
11    prefix: String, // Prefix for the number
12    suffix: String, // Suffix for the number
13    formatOptions: String, // Go here to learn more: https://number-flow.barvian.me/vanilla#properties
14    trend: Number, // Trend for the animation
15    realtime: { type: Boolean, default: false }, // If true, uses interval for timed updates
16    updateInterval: { type: Number, default: 1000 }, // Interval in ms for realtime updates
17    continuous: { type: Boolean, default: true }, // If true, uses continuous plugin for smooth transitions
18    spinEasing: { type: String, default: "ease-in-out" }, // Easing for digit spin animations
19    transformEasing: { type: String, default: "ease-in-out" }, // Easing for layout transforms
20    opacityEasing: { type: String, default: "ease-out" }, // Easing for fade in/out
21  };
22
23  connect() {
24    this.element.innerHTML = "<number-flow></number-flow>";
25    this.flow = this.element.querySelector("number-flow");
26    this.currentValue = this.startValue || 0;
27
28    // Set initial properties from data attributes
29    if (this.hasPrefixValue) this.flow.numberPrefix = this.prefixValue;
30    if (this.hasSuffixValue) this.flow.numberSuffix = this.suffixValue;
31
32    if (this.hasFormatOptionsValue) {
33      try {
34        this.flow.format = JSON.parse(this.formatOptionsValue);
35      } catch (e) {
36        console.error("Error parsing formatOptions JSON:", e);
37        // Apply default or no formatting if parsing fails
38      }
39    }
40
41    if (this.hasTrendValue) {
42      this.flow.trend = this.trendValue;
43    } else {
44      // Default trend for continuous plugin if not specified
45      this.flow.trend = Math.sign(this.endValue - this.currentValue) || 1;
46    }
47
48    // Initialize with start value without animation for non-realtime, or let realtime handle first update
49    if (!this.realtimeValue) {
50      this.flow.update(this.currentValue);
51    }
52
53    // Configure timing with customizable easing
54    this.configureTimings();
55
56    // Apply continuous plugin if enabled
57    if (this.continuousValue) {
58      this.flow.plugins = [continuous];
59    }
60
61    this.handleTrigger();
62  }
63
64  configureTimings() {
65    const animationDuration = this.durationValue || 700;
66
67    // Configure spin timing (for digit animations)
68    this.flow.spinTiming = {
69      duration: animationDuration,
70      easing: this.spinEasingValue,
71    };
72
73    // Configure transform timing (for layout changes)
74    this.flow.transformTiming = {
75      duration: animationDuration,
76      easing: this.transformEasingValue,
77    };
78
79    // Configure opacity timing (for fade effects)
80    this.flow.opacityTiming = {
81      duration: 350,
82      easing: this.opacityEasingValue,
83    };
84  }
85
86  handleTrigger() {
87    const trigger = this.triggerValue || "viewport";
88
89    switch (trigger) {
90      case "load":
91        this.startAnimation();
92        break;
93      case "viewport":
94        this.observeViewport();
95        break;
96      case "manual":
97        // Don't auto-start for manual trigger
98        break;
99      default:
100        this.startAnimation();
101        break;
102    }
103  }
104
105  startAnimation() {
106    if (this.realtimeValue) {
107      this.flow.update(this.currentValue); // Initial update for realtime
108      this.timerInterval = setInterval(() => {
109        this.tick();
110      }, this.updateIntervalValue);
111    } else {
112      this.animateToEnd();
113    }
114  }
115
116  tick() {
117    const step = Math.sign(this.endValue - this.startValue) || (this.startValue > this.endValue ? -1 : 1);
118    this.currentValue += step;
119    this.flow.update(this.currentValue);
120
121    if ((step > 0 && this.currentValue >= this.endValue) || (step < 0 && this.currentValue <= this.endValue)) {
122      clearInterval(this.timerInterval);
123    }
124  }
125
126  animateToEnd() {
127    if (!this.flow) return;
128    // For non-realtime, the duration value from HTML (or default) applies to the whole animation.
129    // Reconfigure timings if duration was specifically set for the full range
130    if (!this.realtimeValue && this.hasDurationValue) {
131      const overallDuration = this.durationValue || 2000;
132
133      this.flow.spinTiming = {
134        duration: overallDuration,
135        easing: this.spinEasingValue,
136      };
137
138      this.flow.transformTiming = {
139        duration: overallDuration,
140        easing: this.transformEasingValue,
141      };
142
143      this.flow.opacityTiming = {
144        duration: 350,
145        easing: this.opacityEasingValue,
146      };
147    }
148    this.flow.update(this.endValue);
149  }
150
151  observeViewport() {
152    this.observer = new IntersectionObserver((entries) => {
153      entries.forEach((entry) => {
154        if (entry.isIntersecting) {
155          this.startAnimation();
156          this.observer.unobserve(this.element);
157        }
158      });
159    });
160
161    this.observer.observe(this.element);
162  }
163
164  // Method for manual triggering (can be called externally)
165  triggerAnimation() {
166    // Reset to start value first
167    this.currentValue = this.startValue || 0;
168    this.flow.update(this.currentValue);
169
170    // Small delay to ensure reset is visible
171    setTimeout(() => {
172      this.startAnimation();
173    }, 50);
174  }
175
176  disconnect() {
177    if (this.timerInterval) {
178      clearInterval(this.timerInterval);
179    }
180    if (this.flow && typeof this.flow.destroy === "function") {
181      // Assuming number-flow might have a cleanup/destroy method
182      // If not, this line might need adjustment based on the library's API
183      // For now, we'll assume it doesn't to prevent errors if 'destroy' doesn't exist.
184    }
185    if (this.observer) {
186      this.observer.disconnect();
187    }
188  }
189};

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.