PageSourceSearch

https://railsblocks.com/assets/controllers/increment_controller-9c…5447862f69efed2c875173fb3bcb9fbf6bd4aa9f.js

js railsblocks.com collected 2026-09-25 20:23:31 UTC 5,834 bytes, 160 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 targets = ["input", "numberFlowDisplay", "decrementButton", "incrementButton"];
7
8  static values = {
9    min: { type: Number, default: -Infinity }, // Minimum value
10    max: { type: Number, default: Infinity }, // Maximum value
11    step: { type: Number, default: 1 }, // Step size
12    shiftStep: { type: Number, default: 10 }, // Shift step size
13    initial: { type: Number, default: 0 },
14    animationDuration: { type: Number, default: 300 },
15  };
16
17  connect() {
18    this.currentValue = this.initialValue;
19
20    this.numberFlowDisplayTarget.innerHTML = "<number-flow></number-flow>";
21    this.flow = this.numberFlowDisplayTarget.querySelector("number-flow");
22
23    this.flow.format = { useGrouping: false }; // Match example
24    this.flow.locales = "en-US";
25    this.flow.plugins = [continuous];
26    this.flow.animated = true;
27    this.flow.willChange = true; // from example
28
29    this.flow.spinTiming = { duration: this.animationDurationValue, easing: "ease-out" };
30    this.flow.transformTiming = { duration: this.animationDurationValue, easing: "ease-out" };
31
32    // Event listeners for caret visibility during animation
33    this.flow.addEventListener("animationsstart", () => this.toggleCaret(false));
34    this.flow.addEventListener("animationsfinish", () => this.toggleCaret(true));
35
36    this.inputTarget.value = this.currentValue;
37    this.flow.update(this.currentValue); // Initialize NumberFlow
38
39    this.updateButtonStates();
40  }
41
42  toggleCaret(show) {
43    this.inputTarget.classList.toggle("caret-transparent", !show);
44    this.inputTarget.classList.toggle("caret-slate-700", show); // Or your preferred caret color
45  }
46
47  handleInput(event) {
48    this.flow.animated = false; // Disable animation during direct input
49    let nextValue = this.currentValue;
50    const inputValue = event.currentTarget.value;
51
52    if (inputValue === "") {
53      // If input is empty, we might revert to initial or do nothing,
54      // for now, let's stick to the current value if it becomes empty during typing.
55      // Or, as in example, could revert to a "defaultValue" if we stored one.
56      // For simplicity, we will ensure input always reflects a valid number.
57      nextValue = this.currentValue; // Keep it as is, or handle as per UX preference
58    } else {
59      const num = parseInt(inputValue, 10); // Using parseInt, as step is 1
60      if (!isNaN(num)) {
61        nextValue = Math.min(Math.max(num, this.minValue), this.maxValue);
62      } else {
63        nextValue = this.currentValue; // Revert if not a number
64      }
65    }
66
67    this.currentValue = nextValue;
68    // Manually update the input.value in case the number stays the same e.g. 09 == 9
69    // or if clamped.
70    event.currentTarget.value = this.currentValue;
71    this.flow.update(this.currentValue);
72    this.updateButtonStates();
73
74    // Re-enable animation shortly after input stops, or on blur.
75    // Using a timeout here for simplicity after typing.
76    clearTimeout(this.typingTimer);
77    this.typingTimer = setTimeout(() => {
78      this.flow.animated = true;
79    }, 500);
80  }
81
82  handleKeydown(event) {
83    if (event.key === "ArrowUp") {
84      event.preventDefault();
85      this.flow.animated = true; // Enable animation for arrow key interactions
86      this.increment(event);
87    } else if (event.key === "ArrowDown") {
88      event.preventDefault();
89      this.flow.animated = true; // Enable animation for arrow key interactions
90      this.decrement(event);
91    }
92  }
93
94  // Ensure animation is re-enabled when input loses focus
95  handleBlur() {
96    this.flow.animated = true;
97    // If input is empty on blur, reset to current valid value
98    if (this.inputTarget.value === "") {
99      this.inputTarget.value = this.currentValue;
100    }
101  }
102
103  decrement(event = null) {
104    const stepSize = event && event.shiftKey ? this.shiftStepValue : this.stepValue;
105    this.updateValue(this.currentValue - stepSize);
106  }
107
108  increment(event = null) {
109    const stepSize = event && event.shiftKey ? this.shiftStepValue : this.stepValue;
110    this.updateValue(this.currentValue + stepSize);
111  }
112
113  updateValue(newValue, animate = true) {
114    this.flow.animated = animate;
115    const clampedValue = Math.min(Math.max(newValue, this.minValue), this.maxValue);
116    if (clampedValue !== this.currentValue) {
117      this.currentValue = clampedValue;
118      this.inputTarget.value = this.currentValue;
119      this.flow.update(this.currentValue);
120      this.updateButtonStates();
121    } else if (!animate) {
122      // If value is same but we need to ensure flow is updated (e.g. after input)
123      this.flow.update(this.currentValue);
124    }
125  }
126
127  updateButtonStates() {
128    this.decrementButtonTarget.disabled = this.currentValue <= this.minValue;
129    this.incrementButtonTarget.disabled = this.currentValue >= this.maxValue;
130  }
131
132  // Allow focus on input when clicking buttons (like original example)
133  focusInput(event) {
134    if (event.pointerType === "mouse") {
135      event.preventDefault(); // Prevent button from taking focus if it's a mouse click
136      this.inputTarget.focus();
137    }
138  }
139
140  disconnect() {
141    clearTimeout(this.typingTimer);
142    // Remove event listeners if they were added directly to this.flow
143    // For now, they are auto-cleaned by Stimulus if on controller element or targets
144  }
145}
146
147// Add some basic CSS for the caret and input
148const style = document.createElement("style");
149style.textContent = `
150  .caret-transparent { caret-color: transparent !important; }
151  .spin-hide::-webkit-outer-spin-button,
152  .spin-hide::-webkit-inner-spin-button {
153    -webkit-appearance: none;
154    margin: 0;
155  }
156  .spin-hide {
157    -moz-appearance: textfield; /* Firefox */
158  }
159`;
160document.head.appendChild(style);

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.