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.