PageSourceSearch

https://railsblocks.com/assets/controllers/two_factor_controller-b…3efaf880279af94ce63fe4334f160007f398975b.js

js railsblocks.com collected 2026-09-25 20:24:35 UTC 5,046 bytes, 191 lines download raw bytes

1import { Controller } from "@hotwired/stimulus";
2
3export default class extends Controller {
4  static targets = ["digit", "form", "submitButton"];
5  static values = {
6    autoSubmit: { type: Boolean, default: false }, // Whether to automatically submit the form
7    autofocus: { type: Boolean, default: true }, // Whether to autofocus the first input
8    numericOnly: { type: Boolean, default: true }, // Whether to allow only digits (0-9)
9  };
10
11  connect() {
12    const inputs = this._getInputTargets();
13
14    // Set autofocus on the first input if autofocus is enabled
15    if (this.autofocusValue && inputs[0]) {
16      inputs[0].focus();
17    }
18
19    // Keep a stable function reference for proper listener cleanup.
20    this.boundHandleFocus = this.handleFocus.bind(this);
21
22    // Add focus event listeners to all input targets.
23    inputs.forEach((input) => {
24      input.addEventListener("focus", this.boundHandleFocus);
25    });
26  }
27
28  disconnect() {
29    // Clean up event listeners
30    this._getInputTargets().forEach((input) => {
31      input.removeEventListener("focus", this.boundHandleFocus);
32    });
33  }
34
35  handleFocus(event) {
36    const input = event.target;
37    // Move cursor to end of input value
38    setTimeout(() => {
39      input.setSelectionRange(input.value.length, input.value.length);
40    }, 0);
41  }
42
43  isNumber(value) {
44    return /^[0-9]$/.test(value);
45  }
46
47  handleInput(event) {
48    const currentInput = event.target;
49    const nextInput = this._getNextInput(currentInput);
50    const normalizedValue = this._normalizeSingleValue(currentInput.value);
51    currentInput.value = normalizedValue;
52
53    if (this._isValidCharacter(normalizedValue)) {
54      if (nextInput) {
55        nextInput.focus();
56      } else {
57        // Last input filled
58        this.handleSubmit();
59      }
60    }
61  }
62
63  handleKeydown(event) {
64    const currentInput = event.target;
65
66    // Handle backspace navigation
67    if (event.key === "Backspace" && currentInput.value === "") {
68      const prevInput = this._getPreviousInput(currentInput);
69      if (prevInput) {
70        prevInput.focus();
71      }
72    }
73
74    // Handle arrow key navigation
75    if (event.key === "ArrowLeft") {
76      event.preventDefault();
77      const prevInput = this._getPreviousInput(currentInput);
78      if (prevInput) {
79        prevInput.focus();
80      }
81    }
82
83    if (event.key === "ArrowRight") {
84      event.preventDefault();
85      const nextInput = this._getNextInput(currentInput);
86      if (nextInput) {
87        nextInput.focus();
88      }
89    }
90  }
91
92  handlePaste(event) {
93    event.preventDefault();
94    const paste = this._normalizePasteValue((event.clipboardData || window.clipboardData).getData("text"));
95    const inputs = this._getInputTargets();
96
97    if (!paste.length || !inputs.length) {
98      return;
99    }
100
101    const characters = paste.slice(0, inputs.length).split("");
102    characters.forEach((character, index) => {
103      inputs[index].value = character;
104    });
105
106    if (characters.length === inputs.length) {
107      this.handleSubmit();
108      return;
109    }
110
111    const nextInput = inputs[characters.length];
112    if (nextInput) {
113      nextInput.focus();
114    }
115  }
116
117  handleSubmit(event) {
118    if (event) {
119      // If triggered by form submit event
120      event.preventDefault();
121    }
122    if (this.hasSubmitButtonTarget && this._isComplete()) {
123      this.submitButtonTarget.focus();
124    }
125
126    // Submit form if autoSubmit is true
127    if (this.autoSubmitValue) {
128      if (this.hasFormTarget) {
129        this.formTarget.submit();
130      }
131    }
132  }
133
134  _getInputTargets() {
135    if (this.hasDigitTarget) {
136      return this.digitTargets;
137    }
138
139    // Backward-compatible fallback for older markup using num1..numN targets.
140    return Array.from(this.element.querySelectorAll('[data-two-factor-target^="num"]'));
141  }
142
143  _getNextInput(currentInput) {
144    const inputs = this._getInputTargets();
145    const currentIndex = inputs.indexOf(currentInput);
146    if (currentIndex !== -1 && currentIndex < inputs.length - 1) {
147      return inputs[currentIndex + 1];
148    }
149    return null;
150  }
151
152  _getPreviousInput(currentInput) {
153    const inputs = this._getInputTargets();
154    const currentIndex = inputs.indexOf(currentInput);
155    if (currentIndex > 0) {
156      return inputs[currentIndex - 1];
157    }
158    return null;
159  }
160
161  _isComplete() {
162    const inputs = this._getInputTargets();
163    return inputs.length > 0 && inputs.every((input) => this._isValidCharacter(input.value));
164  }
165
166  _normalizeSingleValue(value) {
167    const withoutWhitespace = value.replace(/\s/g, "");
168    if (this.numericOnlyValue) {
169      return withoutWhitespace.replace(/\D/g, "").slice(-1);
170    }
171
172    return withoutWhitespace.slice(-1);
173  }
174
175  _normalizePasteValue(value) {
176    const withoutWhitespace = value.replace(/\s/g, "");
177    if (this.numericOnlyValue) {
178      return withoutWhitespace.replace(/\D/g, "");
179    }
180
181    return withoutWhitespace;
182  }
183
184  _isValidCharacter(value) {
185    if (this.numericOnlyValue) {
186      return this.isNumber(value);
187    }
188
189    return value.length === 1;
190  }
191};

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.