@daformat/react-number-flow-input 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (42) hide show
  1. package/LICENSE +14 -0
  2. package/README.md +298 -0
  3. package/dist/NumberFlowInput.d.ts +60 -0
  4. package/dist/NumberFlowInput.js +3099 -0
  5. package/dist/NumberFlowInput.js.map +1 -0
  6. package/dist/index.d.ts +2 -0
  7. package/dist/index.js +2 -0
  8. package/dist/index.js.map +1 -0
  9. package/dist/styles.d.ts +5 -0
  10. package/dist/styles.js +140 -0
  11. package/dist/styles.js.map +1 -0
  12. package/dist/utils/barrelWheel.d.ts +12 -0
  13. package/dist/utils/barrelWheel.js +83 -0
  14. package/dist/utils/barrelWheel.js.map +1 -0
  15. package/dist/utils/changes.d.ts +87 -0
  16. package/dist/utils/changes.js +794 -0
  17. package/dist/utils/changes.js.map +1 -0
  18. package/dist/utils/combineRefs.d.ts +5 -0
  19. package/dist/utils/combineRefs.js +16 -0
  20. package/dist/utils/combineRefs.js.map +1 -0
  21. package/dist/utils/cssEasing.d.ts +24 -0
  22. package/dist/utils/cssEasing.js +25 -0
  23. package/dist/utils/cssEasing.js.map +1 -0
  24. package/dist/utils/formatting.d.ts +33 -0
  25. package/dist/utils/formatting.js +99 -0
  26. package/dist/utils/formatting.js.map +1 -0
  27. package/dist/utils/maybe.d.ts +3 -0
  28. package/dist/utils/maybe.js +2 -0
  29. package/dist/utils/maybe.js.map +1 -0
  30. package/dist/utils/moveElementPreservingAnimation.d.ts +6 -0
  31. package/dist/utils/moveElementPreservingAnimation.js +61 -0
  32. package/dist/utils/moveElementPreservingAnimation.js.map +1 -0
  33. package/dist/utils/nullable.d.ts +12 -0
  34. package/dist/utils/nullable.js +19 -0
  35. package/dist/utils/nullable.js.map +1 -0
  36. package/dist/utils/textCleaning.d.ts +6 -0
  37. package/dist/utils/textCleaning.js +60 -0
  38. package/dist/utils/textCleaning.js.map +1 -0
  39. package/dist/utils/utils.d.ts +33 -0
  40. package/dist/utils/utils.js +162 -0
  41. package/dist/utils/utils.js.map +1 -0
  42. package/package.json +68 -0
@@ -0,0 +1,794 @@
1
+ /**
2
+ * Check if a character is a separator (not a digit, decimal point, or minus)
3
+ * @param char - The character to check
4
+ * @param localeDecimal - Optional locale-specific decimal separator (e.g., "," for fr-FR)
5
+ */
6
+ const isSeparator = (char, localeDecimal) => {
7
+ if (!char) {
8
+ return false;
9
+ }
10
+ // Check against standard raw characters
11
+ if (/[\d.\-]/.test(char)) {
12
+ return false;
13
+ }
14
+ // Also check against locale decimal separator
15
+ if (localeDecimal && char === localeDecimal) {
16
+ return false;
17
+ }
18
+ return true;
19
+ };
20
+ /**
21
+ * Count separators in a string, returning a map of char -> count
22
+ * @param str - The string to count separators in
23
+ * @param localeDecimal - Optional locale-specific decimal separator
24
+ */
25
+ const countSeparators = (str, localeDecimal) => {
26
+ const counts = new Map();
27
+ for (const char of str) {
28
+ if (isSeparator(char, localeDecimal)) {
29
+ counts.set(char, (counts.get(char) ?? 0) + 1);
30
+ }
31
+ }
32
+ return counts;
33
+ };
34
+ /**
35
+ * Maps a raw (unformatted) cursor position to a formatted cursor position.
36
+ * Counts non-separator characters up to the raw position.
37
+ * @param rawPos - Position in raw (unformatted) string
38
+ * @param formattedStr - The formatted string
39
+ * @param localeDecimal - Optional locale-specific decimal separator
40
+ */
41
+ const mapRawPosToFormattedPos = (rawPos, formattedStr, localeDecimal) => {
42
+ let rawCount = 0;
43
+ for (let i = 0; i < formattedStr.length; i++) {
44
+ if (rawCount === rawPos) {
45
+ return i;
46
+ }
47
+ if (!isSeparator(formattedStr[i], localeDecimal)) {
48
+ rawCount++;
49
+ }
50
+ }
51
+ return formattedStr.length;
52
+ };
53
+ /**
54
+ * Detects which characters in the formatted string are new vs unchanged.
55
+ * Uses cursor position to correctly identify which specific characters are new,
56
+ * especially important when there are repeated identical characters.
57
+ * Special handling for separators: they only animate if they're truly new,
58
+ * not just shifted in position.
59
+ *
60
+ * @param oldFormatted - The old formatted string
61
+ * @param newFormatted - The new formatted string
62
+ * @param rawCursorPos - Cursor position in raw (unformatted) text after the change
63
+ * @param rawSelectionStart - Selection start in raw text before the change
64
+ * @param rawOldLength - Length of old raw text
65
+ * @param localeDecimal - Optional locale-specific decimal separator (e.g., "," for fr-FR)
66
+ */
67
+ export const getFormattedChanges = (oldFormatted, newFormatted, rawCursorPos, rawSelectionStart, rawOldLength, localeDecimal) => {
68
+ const addedIndices = new Set();
69
+ const unchangedIndices = new Set();
70
+ if (!oldFormatted) {
71
+ for (let i = 0; i < newFormatted.length; i++) {
72
+ addedIndices.add(i);
73
+ }
74
+ return { addedIndices, unchangedIndices };
75
+ }
76
+ // Count separators to determine which are truly new
77
+ const oldSeparatorCounts = countSeparators(oldFormatted, localeDecimal);
78
+ const newSeparatorCounts = countSeparators(newFormatted, localeDecimal);
79
+ const newSeparatorAmounts = new Map();
80
+ for (const [char, newCount] of newSeparatorCounts) {
81
+ const oldCount = oldSeparatorCounts.get(char) ?? 0;
82
+ const trulyNew = Math.max(0, newCount - oldCount);
83
+ newSeparatorAmounts.set(char, trulyNew);
84
+ }
85
+ // Extract non-separator characters (keeping locale decimal as a raw char)
86
+ const extractNonSep = (str) => {
87
+ let result = "";
88
+ for (const char of str) {
89
+ if (!isSeparator(char, localeDecimal)) {
90
+ result += char;
91
+ }
92
+ }
93
+ return result;
94
+ };
95
+ const oldNonSep = extractNonSep(oldFormatted);
96
+ const newNonSep = extractNonSep(newFormatted);
97
+ // If cursor info is provided, use position-based diff
98
+ // This correctly handles repeated identical characters
99
+ if (rawCursorPos !== undefined &&
100
+ rawSelectionStart !== undefined &&
101
+ rawOldLength !== undefined) {
102
+ const numInserted = newNonSep.length - oldNonSep.length;
103
+ // Characters inserted at rawSelectionStart, cursor moved to rawCursorPos
104
+ // So inserted characters are from rawSelectionStart to rawCursorPos (exclusive)
105
+ const insertStartRaw = rawSelectionStart;
106
+ const insertEndRaw = rawCursorPos;
107
+ // Map raw positions to formatted positions
108
+ const insertStartFormatted = mapRawPosToFormattedPos(insertStartRaw, newFormatted, localeDecimal);
109
+ const insertEndFormatted = mapRawPosToFormattedPos(insertEndRaw, newFormatted, localeDecimal);
110
+ // Track separators that should animate (truly new ones)
111
+ // We want to animate new separators that appear in the inserted region
112
+ const remainingNewSeparators = new Map(newSeparatorAmounts);
113
+ for (let idx = 0; idx < newFormatted.length; idx++) {
114
+ const char = newFormatted[idx];
115
+ if (isSeparator(char, localeDecimal)) {
116
+ // For separators: animate if truly new AND in/near the insertion region
117
+ const remaining = remainingNewSeparators.get(char ?? "") ?? 0;
118
+ if (remaining > 0 &&
119
+ idx >= insertStartFormatted &&
120
+ idx < insertEndFormatted) {
121
+ addedIndices.add(idx);
122
+ remainingNewSeparators.set(char ?? "", remaining - 1);
123
+ }
124
+ else if (remaining > 0 && numInserted > 0) {
125
+ // New separator but outside insertion region - still mark as new
126
+ // This handles cases where separator appears due to digit insertion
127
+ addedIndices.add(idx);
128
+ remainingNewSeparators.set(char ?? "", remaining - 1);
129
+ }
130
+ else {
131
+ unchangedIndices.add(idx);
132
+ }
133
+ }
134
+ else {
135
+ // For non-separator characters: use position-based logic
136
+ if (idx >= insertStartFormatted && idx < insertEndFormatted) {
137
+ // This character is in the inserted region
138
+ addedIndices.add(idx);
139
+ }
140
+ else {
141
+ unchangedIndices.add(idx);
142
+ }
143
+ }
144
+ }
145
+ return { addedIndices, unchangedIndices };
146
+ }
147
+ // Fallback: use LCS-based detection when cursor info not available
148
+ // (This is the old behavior, kept for backwards compatibility)
149
+ const m = oldNonSep.length;
150
+ const n = newNonSep.length;
151
+ const dp = Array.from({ length: m + 1 }, () => Array(n + 1).fill(0));
152
+ for (let i = 1; i <= m; i++) {
153
+ const prevRow = dp[i - 1];
154
+ const currentRow = dp[i];
155
+ if (!prevRow || !currentRow) {
156
+ continue;
157
+ }
158
+ for (let j = 1; j <= n; j++) {
159
+ if (oldNonSep[i - 1] === newNonSep[j - 1]) {
160
+ currentRow[j] = (prevRow[j - 1] ?? 0) + 1;
161
+ }
162
+ else {
163
+ currentRow[j] = Math.max(prevRow[j] ?? 0, currentRow[j - 1] ?? 0);
164
+ }
165
+ }
166
+ }
167
+ let i = m;
168
+ let j = n;
169
+ const lcsNewNonSepIndices = new Set();
170
+ while (i > 0 && j > 0) {
171
+ const prevRow = dp[i - 1];
172
+ const currentRow = dp[i];
173
+ if (!prevRow || !currentRow) {
174
+ break;
175
+ }
176
+ if (oldNonSep[i - 1] === newNonSep[j - 1]) {
177
+ lcsNewNonSepIndices.add(j - 1);
178
+ i--;
179
+ j--;
180
+ }
181
+ else if ((prevRow[j] ?? 0) > (currentRow[j - 1] ?? 0)) {
182
+ i--;
183
+ }
184
+ else {
185
+ j--;
186
+ }
187
+ }
188
+ const nonSepIndexToFormattedIndex = new Map();
189
+ let nonSepIdx = 0;
190
+ for (let idx = 0; idx < newFormatted.length; idx++) {
191
+ if (!isSeparator(newFormatted[idx], localeDecimal)) {
192
+ nonSepIndexToFormattedIndex.set(nonSepIdx, idx);
193
+ nonSepIdx++;
194
+ }
195
+ }
196
+ const remainingNewSeparators = new Map(newSeparatorAmounts);
197
+ for (let idx = 0; idx < newFormatted.length; idx++) {
198
+ const char = newFormatted[idx];
199
+ if (isSeparator(char, localeDecimal)) {
200
+ const remaining = remainingNewSeparators.get(char ?? "") ?? 0;
201
+ if (remaining > 0) {
202
+ addedIndices.add(idx);
203
+ remainingNewSeparators.set(char ?? "", remaining - 1);
204
+ }
205
+ else {
206
+ unchangedIndices.add(idx);
207
+ }
208
+ }
209
+ else {
210
+ let nonSepIdxForThis = -1;
211
+ for (const [nsi, fi] of nonSepIndexToFormattedIndex) {
212
+ if (fi === idx) {
213
+ nonSepIdxForThis = nsi;
214
+ break;
215
+ }
216
+ }
217
+ if (nonSepIdxForThis >= 0 && lcsNewNonSepIndices.has(nonSepIdxForThis)) {
218
+ unchangedIndices.add(idx);
219
+ }
220
+ else {
221
+ addedIndices.add(idx);
222
+ }
223
+ }
224
+ }
225
+ return { addedIndices, unchangedIndices };
226
+ };
227
+ /**
228
+ * Build a barrel-wheel sequence + direction for a digit-to-digit transition.
229
+ * @internal
230
+ */
231
+ const buildBarrelWheel = (oldChar, newChar) => {
232
+ const oldDigit = parseInt(oldChar, 10);
233
+ const newDigit = parseInt(newChar, 10);
234
+ const direction = newDigit > oldDigit ? "up" : "down";
235
+ const sequence = [];
236
+ if (direction === "up") {
237
+ for (let i = oldDigit; i <= newDigit; i++) {
238
+ sequence.push(i.toString());
239
+ }
240
+ }
241
+ else {
242
+ for (let i = newDigit; i <= oldDigit; i++) {
243
+ sequence.push(i.toString());
244
+ }
245
+ }
246
+ return { sequence, direction };
247
+ };
248
+ /**
249
+ * Split a raw value into [sign?][integer][.decimal?] with their start positions.
250
+ * @internal
251
+ */
252
+ const splitRawParts = (value) => {
253
+ let start = 0;
254
+ let sign = "";
255
+ if (value[0] === "-") {
256
+ sign = "-";
257
+ start = 1;
258
+ }
259
+ const dotIdx = value.indexOf(".", start);
260
+ const int = dotIdx === -1 ? value.slice(start) : value.slice(start, dotIdx);
261
+ const dec = dotIdx === -1 ? null : value.slice(dotIdx + 1);
262
+ return {
263
+ sign,
264
+ int,
265
+ dec,
266
+ intStart: start,
267
+ dotIdx,
268
+ decStart: dotIdx === -1 ? -1 : dotIdx + 1,
269
+ };
270
+ };
271
+ /**
272
+ * Diff two values as a wholesale replacement (used when the `value` prop
273
+ * changes externally rather than via the user typing). Aligns integer digits
274
+ * from the right and decimal digits from the left, so digits in the same
275
+ * "column" produce barrel-wheel animations and any extra digits are added/
276
+ * removed at the edges.
277
+ */
278
+ export const getReplacementChanges = (oldValue, newValue) => {
279
+ const changes = {
280
+ addedIndices: new Set(),
281
+ unchangedIndices: new Set(),
282
+ barrelWheelIndices: new Map(),
283
+ removedDigitWheels: new Map(),
284
+ };
285
+ if (!oldValue) {
286
+ for (let i = 0; i < newValue.length; i++) {
287
+ const ch = newValue[i];
288
+ if (ch && /^\d$/.test(ch)) {
289
+ const wheel = buildBarrelWheel("0", ch);
290
+ wheel.fromZero = true;
291
+ changes.barrelWheelIndices.set(i, wheel);
292
+ }
293
+ else {
294
+ changes.addedIndices.add(i);
295
+ }
296
+ }
297
+ return changes;
298
+ }
299
+ if (!newValue) {
300
+ for (let i = 0; i < oldValue.length; i++) {
301
+ const ch = oldValue[i];
302
+ if (ch && /^\d$/.test(ch)) {
303
+ changes.removedDigitWheels.set(i, {
304
+ oldChar: ch,
305
+ oldFormattedIndex: -1,
306
+ });
307
+ }
308
+ }
309
+ return changes;
310
+ }
311
+ const oldParts = splitRawParts(oldValue);
312
+ const newParts = splitRawParts(newValue);
313
+ // Sign (always at index 0 in raw)
314
+ if (newParts.sign) {
315
+ if (oldParts.sign) {
316
+ changes.unchangedIndices.add(0);
317
+ }
318
+ else {
319
+ changes.addedIndices.add(0);
320
+ }
321
+ }
322
+ // Integer part: right-align
323
+ const oldIntLen = oldParts.int.length;
324
+ const newIntLen = newParts.int.length;
325
+ // Track which old integer positions get consumed by the alignment so we
326
+ // can flag the leftover ones (digits dropped off the left edge) as
327
+ // removed-digit wheels.
328
+ const consumedOldIntPositions = new Set();
329
+ for (let pos = 0; pos < newIntLen; pos++) {
330
+ const newIdx = newParts.intStart + pos;
331
+ const newChar = newParts.int[pos];
332
+ if (!newChar) {
333
+ continue;
334
+ }
335
+ const distFromEnd = newIntLen - 1 - pos;
336
+ const oldPos = oldIntLen - 1 - distFromEnd;
337
+ const oldChar = oldPos >= 0 ? oldParts.int[oldPos] : undefined;
338
+ if (oldChar !== undefined) {
339
+ consumedOldIntPositions.add(oldPos);
340
+ }
341
+ if (oldChar === undefined) {
342
+ // No aligned old digit at this slot. If the new char is a digit it
343
+ // should animate in as a wheel from "0"; otherwise (separators)
344
+ // keep the existing flow-animation behavior.
345
+ if (/^\d$/.test(newChar)) {
346
+ const wheel = buildBarrelWheel("0", newChar);
347
+ wheel.fromZero = true;
348
+ changes.barrelWheelIndices.set(newIdx, wheel);
349
+ }
350
+ else {
351
+ changes.addedIndices.add(newIdx);
352
+ }
353
+ }
354
+ else if (oldChar === newChar) {
355
+ changes.unchangedIndices.add(newIdx);
356
+ }
357
+ else if (/^\d$/.test(oldChar) && /^\d$/.test(newChar)) {
358
+ changes.barrelWheelIndices.set(newIdx, buildBarrelWheel(oldChar, newChar));
359
+ }
360
+ else {
361
+ changes.addedIndices.add(newIdx);
362
+ }
363
+ }
364
+ // Old integer positions left over (because newIntLen < oldIntLen) drop
365
+ // off the left edge. Each old digit position becomes a removed-digit
366
+ // wheel; non-digit chars (separators) are not tracked here so they keep
367
+ // their existing "fade out" path.
368
+ for (let oldPos = 0; oldPos < oldIntLen; oldPos++) {
369
+ if (consumedOldIntPositions.has(oldPos)) {
370
+ continue;
371
+ }
372
+ const oldChar = oldParts.int[oldPos];
373
+ if (oldChar && /^\d$/.test(oldChar)) {
374
+ const oldRawIdx = oldParts.intStart + oldPos;
375
+ changes.removedDigitWheels.set(oldRawIdx, {
376
+ oldChar,
377
+ oldFormattedIndex: -1,
378
+ });
379
+ }
380
+ }
381
+ // Decimal point
382
+ if (newParts.dotIdx !== -1) {
383
+ if (oldParts.dotIdx !== -1) {
384
+ changes.unchangedIndices.add(newParts.dotIdx);
385
+ }
386
+ else {
387
+ changes.addedIndices.add(newParts.dotIdx);
388
+ }
389
+ }
390
+ // Decimal part: left-align
391
+ if (newParts.dec !== null && newParts.decStart !== -1) {
392
+ const oldDec = oldParts.dec ?? "";
393
+ for (let pos = 0; pos < newParts.dec.length; pos++) {
394
+ const newIdx = newParts.decStart + pos;
395
+ const newChar = newParts.dec[pos];
396
+ if (!newChar) {
397
+ continue;
398
+ }
399
+ const oldChar = pos < oldDec.length ? oldDec[pos] : undefined;
400
+ if (oldChar === undefined) {
401
+ if (/^\d$/.test(newChar)) {
402
+ const wheel = buildBarrelWheel("0", newChar);
403
+ wheel.fromZero = true;
404
+ changes.barrelWheelIndices.set(newIdx, wheel);
405
+ }
406
+ else {
407
+ changes.addedIndices.add(newIdx);
408
+ }
409
+ }
410
+ else if (oldChar === newChar) {
411
+ changes.unchangedIndices.add(newIdx);
412
+ }
413
+ else if (/^\d$/.test(oldChar) && /^\d$/.test(newChar)) {
414
+ changes.barrelWheelIndices.set(newIdx, buildBarrelWheel(oldChar, newChar));
415
+ }
416
+ else {
417
+ changes.addedIndices.add(newIdx);
418
+ }
419
+ }
420
+ // Old decimal positions left over (because newDec.length < oldDec.length)
421
+ // drop off the right edge. Each old digit position becomes a
422
+ // removed-digit wheel.
423
+ if (oldParts.decStart !== -1) {
424
+ for (let pos = newParts.dec.length; pos < oldDec.length; pos++) {
425
+ const oldChar = oldDec[pos];
426
+ if (oldChar && /^\d$/.test(oldChar)) {
427
+ const oldRawIdx = oldParts.decStart + pos;
428
+ changes.removedDigitWheels.set(oldRawIdx, {
429
+ oldChar,
430
+ oldFormattedIndex: -1,
431
+ });
432
+ }
433
+ }
434
+ }
435
+ }
436
+ else if (oldParts.dec !== null && oldParts.decStart !== -1) {
437
+ // Old value had a decimal part, new value has none: every old decimal
438
+ // digit is a removed digit.
439
+ for (let pos = 0; pos < oldParts.dec.length; pos++) {
440
+ const oldChar = oldParts.dec[pos];
441
+ if (oldChar && /^\d$/.test(oldChar)) {
442
+ const oldRawIdx = oldParts.decStart + pos;
443
+ changes.removedDigitWheels.set(oldRawIdx, {
444
+ oldChar,
445
+ oldFormattedIndex: -1,
446
+ });
447
+ }
448
+ }
449
+ }
450
+ return changes;
451
+ };
452
+ /**
453
+ * Formatted-space variant of {@link getReplacementChanges}. Returns
454
+ * added/unchanged indices in the *formatted* string, aligned the same way
455
+ * (integers right-aligned, decimals left-aligned).
456
+ */
457
+ export const getReplacementFormattedChanges = (oldFormatted, newFormatted, localeDecimal) => {
458
+ const addedIndices = new Set();
459
+ const unchangedIndices = new Set();
460
+ if (!oldFormatted) {
461
+ for (let i = 0; i < newFormatted.length; i++) {
462
+ addedIndices.add(i);
463
+ }
464
+ return { addedIndices, unchangedIndices };
465
+ }
466
+ if (!newFormatted) {
467
+ return { addedIndices, unchangedIndices };
468
+ }
469
+ const oldDecIdx = oldFormatted.indexOf(localeDecimal);
470
+ const newDecIdx = newFormatted.indexOf(localeDecimal);
471
+ const oldIntStr = oldDecIdx === -1 ? oldFormatted : oldFormatted.slice(0, oldDecIdx);
472
+ const newIntStr = newDecIdx === -1 ? newFormatted : newFormatted.slice(0, newDecIdx);
473
+ const oldIntLen = oldIntStr.length;
474
+ const newIntLen = newIntStr.length;
475
+ for (let pos = 0; pos < newIntLen; pos++) {
476
+ const distFromEnd = newIntLen - 1 - pos;
477
+ const oldPos = oldIntLen - 1 - distFromEnd;
478
+ const newChar = newIntStr[pos];
479
+ const oldChar = oldPos >= 0 ? oldIntStr[oldPos] : undefined;
480
+ if (oldChar !== undefined && oldChar === newChar) {
481
+ unchangedIndices.add(pos);
482
+ }
483
+ else {
484
+ addedIndices.add(pos);
485
+ }
486
+ }
487
+ if (newDecIdx !== -1) {
488
+ if (oldDecIdx !== -1) {
489
+ unchangedIndices.add(newDecIdx);
490
+ }
491
+ else {
492
+ addedIndices.add(newDecIdx);
493
+ }
494
+ const oldDecStr = oldDecIdx === -1 ? "" : oldFormatted.slice(oldDecIdx + 1);
495
+ const newDecStr = newFormatted.slice(newDecIdx + 1);
496
+ for (let pos = 0; pos < newDecStr.length; pos++) {
497
+ const fullIdx = newDecIdx + 1 + pos;
498
+ const newChar = newDecStr[pos];
499
+ const oldChar = pos < oldDecStr.length ? oldDecStr[pos] : undefined;
500
+ if (oldChar !== undefined && oldChar === newChar) {
501
+ unchangedIndices.add(fullIdx);
502
+ }
503
+ else {
504
+ addedIndices.add(fullIdx);
505
+ }
506
+ }
507
+ }
508
+ return { addedIndices, unchangedIndices };
509
+ };
510
+ export const getChanges = (oldValue, newValue, selectionStart, selectionEnd, newCursorPos) => {
511
+ const changes = {
512
+ addedIndices: new Set(),
513
+ unchangedIndices: new Set(),
514
+ barrelWheelIndices: new Map(),
515
+ };
516
+ if (!oldValue) {
517
+ for (let i = 0; i < newValue.length; i++) {
518
+ changes.addedIndices.add(i);
519
+ }
520
+ return changes;
521
+ }
522
+ const hadSelection = selectionStart !== selectionEnd;
523
+ const lengthDiff = newValue.length - oldValue.length;
524
+ if (hadSelection) {
525
+ const numReplaced = selectionEnd - selectionStart;
526
+ const numInserted = newCursorPos - selectionStart;
527
+ const insertStart = selectionStart;
528
+ if (numReplaced === 1 &&
529
+ numInserted === 1 &&
530
+ insertStart < oldValue.length &&
531
+ insertStart < newValue.length) {
532
+ const oldChar = oldValue[insertStart];
533
+ const newChar = newValue[insertStart];
534
+ if (oldChar && newChar && /^\d$/.test(oldChar) && /^\d$/.test(newChar)) {
535
+ if (oldChar !== newChar) {
536
+ changes.barrelWheelIndices.set(insertStart, buildBarrelWheel(oldChar, newChar));
537
+ }
538
+ }
539
+ }
540
+ for (let i = 0; i < insertStart; i++) {
541
+ if (i < oldValue.length &&
542
+ i < newValue.length &&
543
+ oldValue[i] === newValue[i]) {
544
+ changes.unchangedIndices.add(i);
545
+ }
546
+ }
547
+ for (let i = insertStart; i < newCursorPos; i++) {
548
+ if (!changes.barrelWheelIndices.has(i)) {
549
+ changes.addedIndices.add(i);
550
+ }
551
+ }
552
+ const oldAfterEnd = selectionEnd;
553
+ const newAfterEnd = newCursorPos;
554
+ const minLength = Math.min(oldValue.length - oldAfterEnd, newValue.length - newAfterEnd);
555
+ for (let i = 0; i < minLength; i++) {
556
+ const oldIdx = oldAfterEnd + i;
557
+ const newIdx = newAfterEnd + i;
558
+ if (oldIdx < oldValue.length &&
559
+ newIdx < newValue.length &&
560
+ oldValue[oldIdx] === newValue[newIdx]) {
561
+ changes.unchangedIndices.add(newIdx);
562
+ }
563
+ }
564
+ }
565
+ else if (lengthDiff > 0) {
566
+ const insertPos = newCursorPos - lengthDiff;
567
+ for (let i = 0; i < insertPos; i++) {
568
+ if (i < oldValue.length &&
569
+ i < newValue.length &&
570
+ oldValue[i] === newValue[i]) {
571
+ changes.unchangedIndices.add(i);
572
+ }
573
+ }
574
+ for (let i = insertPos; i < newCursorPos; i++) {
575
+ changes.addedIndices.add(i);
576
+ }
577
+ for (let i = newCursorPos; i < newValue.length; i++) {
578
+ const oldIdx = i - lengthDiff;
579
+ if (oldIdx >= 0 &&
580
+ oldIdx < oldValue.length &&
581
+ oldValue[oldIdx] === newValue[i]) {
582
+ changes.unchangedIndices.add(i);
583
+ }
584
+ }
585
+ }
586
+ else if (lengthDiff < 0) {
587
+ const deletePos = selectionStart;
588
+ const numDeleted = -lengthDiff;
589
+ for (let i = 0; i < deletePos; i++) {
590
+ if (i < oldValue.length &&
591
+ i < newValue.length &&
592
+ oldValue[i] === newValue[i]) {
593
+ changes.unchangedIndices.add(i);
594
+ }
595
+ }
596
+ for (let i = deletePos; i < newValue.length; i++) {
597
+ const oldIdx = i + numDeleted;
598
+ if (oldIdx < oldValue.length && oldValue[oldIdx] === newValue[i]) {
599
+ changes.unchangedIndices.add(i);
600
+ }
601
+ }
602
+ }
603
+ else {
604
+ for (let i = 0; i < newValue.length; i++) {
605
+ if (i < oldValue.length && oldValue[i] === newValue[i]) {
606
+ changes.unchangedIndices.add(i);
607
+ }
608
+ }
609
+ }
610
+ return changes;
611
+ };
612
+ /**
613
+ * Get the group number for a character at a given index.
614
+ * Groups are separated by separator characters (commas, spaces, etc.).
615
+ * Group 0 is before the first separator, group 1 is after, etc.
616
+ * @param localeDecimal - Optional locale-specific decimal separator
617
+ */
618
+ const getGroupNumber = (str, index, localeDecimal) => {
619
+ let group = 0;
620
+ for (let i = 0; i < index && i < str.length; i++) {
621
+ if (isSeparator(str[i], localeDecimal)) {
622
+ group++;
623
+ }
624
+ }
625
+ return group;
626
+ };
627
+ /**
628
+ * Detects which characters should animate their x-position when the formatted
629
+ * string changes. This includes:
630
+ * - Separators that moved positions
631
+ * - Digits that crossed group boundaries (moved past a separator)
632
+ *
633
+ * @param localeDecimal - Optional locale-specific decimal separator
634
+ * @returns Array of indices in the new formatted string that should animate
635
+ */
636
+ export const getPositionChanges = (oldFormatted, newFormatted, localeDecimal) => {
637
+ const changes = [];
638
+ if (!oldFormatted || !newFormatted) {
639
+ return changes;
640
+ }
641
+ // Helper to extract non-separator characters
642
+ const extractNonSep = (str) => {
643
+ let result = "";
644
+ for (const char of str) {
645
+ if (!isSeparator(char, localeDecimal)) {
646
+ result += char;
647
+ }
648
+ }
649
+ return result;
650
+ };
651
+ // Extract non-separator characters from both strings
652
+ const oldNonSep = extractNonSep(oldFormatted);
653
+ const newNonSep = extractNonSep(newFormatted);
654
+ // Map each non-separator character in the new string to its position in old string
655
+ // We use a simple matching algorithm: match from left to right for unchanged chars
656
+ // This handles insertions and deletions correctly
657
+ // First, find the LCS (longest common subsequence) to match characters
658
+ const m = oldNonSep.length;
659
+ const n = newNonSep.length;
660
+ if (m === 0 || n === 0) {
661
+ return changes;
662
+ }
663
+ // Build LCS table
664
+ const dp = Array.from({ length: m + 1 }, () => Array(n + 1).fill(0));
665
+ for (let i = 1; i <= m; i++) {
666
+ const prevRow = dp[i - 1];
667
+ const currentRow = dp[i];
668
+ if (!prevRow || !currentRow) {
669
+ continue;
670
+ }
671
+ for (let j = 1; j <= n; j++) {
672
+ if (oldNonSep[i - 1] === newNonSep[j - 1]) {
673
+ currentRow[j] = (prevRow[j - 1] ?? 0) + 1;
674
+ }
675
+ else {
676
+ currentRow[j] = Math.max(prevRow[j] ?? 0, currentRow[j - 1] ?? 0);
677
+ }
678
+ }
679
+ }
680
+ // Backtrack to find matching pairs
681
+ // Maps new non-sep index to old non-sep index
682
+ const newToOldNonSepIndex = new Map();
683
+ let i = m;
684
+ let j = n;
685
+ while (i > 0 && j > 0) {
686
+ if (oldNonSep[i - 1] === newNonSep[j - 1]) {
687
+ newToOldNonSepIndex.set(j - 1, i - 1);
688
+ i--;
689
+ j--;
690
+ }
691
+ else if ((dp[i - 1]?.[j] ?? 0) > (dp[i]?.[j - 1] ?? 0)) {
692
+ i--;
693
+ }
694
+ else {
695
+ j--;
696
+ }
697
+ }
698
+ // Build maps from non-sep index to formatted index
699
+ const oldNonSepToFormatted = new Map();
700
+ let nonSepIdx = 0;
701
+ for (let idx = 0; idx < oldFormatted.length; idx++) {
702
+ if (!isSeparator(oldFormatted[idx], localeDecimal)) {
703
+ oldNonSepToFormatted.set(nonSepIdx, idx);
704
+ nonSepIdx++;
705
+ }
706
+ }
707
+ const newNonSepToFormatted = new Map();
708
+ nonSepIdx = 0;
709
+ for (let idx = 0; idx < newFormatted.length; idx++) {
710
+ if (!isSeparator(newFormatted[idx], localeDecimal)) {
711
+ newNonSepToFormatted.set(nonSepIdx, idx);
712
+ nonSepIdx++;
713
+ }
714
+ }
715
+ // Check each character in the new formatted string
716
+ for (let newIdx = 0; newIdx < newFormatted.length; newIdx++) {
717
+ const char = newFormatted[newIdx];
718
+ if (isSeparator(char, localeDecimal)) {
719
+ // For separators, check if there was a separator at a different position
720
+ // We need to find if this separator "moved" from somewhere
721
+ // Count separators of this type before this position in both strings
722
+ let newSepCountBefore = 0;
723
+ for (let k = 0; k < newIdx; k++) {
724
+ if (newFormatted[k] === char) {
725
+ newSepCountBefore++;
726
+ }
727
+ }
728
+ // Find the matching separator in old string (same type, same occurrence number)
729
+ let oldSepCount = 0;
730
+ let oldIdx = -1;
731
+ for (let k = 0; k < oldFormatted.length; k++) {
732
+ if (oldFormatted[k] === char) {
733
+ if (oldSepCount === newSepCountBefore) {
734
+ oldIdx = k;
735
+ break;
736
+ }
737
+ oldSepCount++;
738
+ }
739
+ }
740
+ // If found and positions differ, this separator moved
741
+ if (oldIdx >= 0 && oldIdx !== newIdx && char !== undefined) {
742
+ changes.push({
743
+ newIndex: newIdx,
744
+ oldIndex: oldIdx,
745
+ char,
746
+ isSeparator: true,
747
+ crossedGroup: false,
748
+ });
749
+ }
750
+ }
751
+ else {
752
+ // For non-separator characters, check if they crossed a group boundary
753
+ // Find the non-sep index for this formatted index
754
+ let newNonSepIdx = -1;
755
+ for (const [nsi, fi] of newNonSepToFormatted) {
756
+ if (fi === newIdx) {
757
+ newNonSepIdx = nsi;
758
+ break;
759
+ }
760
+ }
761
+ if (newNonSepIdx >= 0) {
762
+ const oldNonSepIdx = newToOldNonSepIndex.get(newNonSepIdx);
763
+ if (oldNonSepIdx !== undefined) {
764
+ const oldFormattedIdx = oldNonSepToFormatted.get(oldNonSepIdx);
765
+ if (oldFormattedIdx !== undefined && char !== undefined) {
766
+ // LCS can be ambiguous when the same digit appears multiple times
767
+ // (e.g. "6,650,431" → "6,850,431": the leading "6" can be matched
768
+ // to either of the two old "6"s of equal LCS length). When the
769
+ // SAME character is at the SAME formatted position in old, the
770
+ // digit obviously didn't move and we must skip the animation to
771
+ // avoid translating it from a phantom location.
772
+ if (oldFormatted[newIdx] === char) {
773
+ continue;
774
+ }
775
+ // Character existed before - check if it crossed a group boundary
776
+ const oldGroup = getGroupNumber(oldFormatted, oldFormattedIdx, localeDecimal);
777
+ const newGroup = getGroupNumber(newFormatted, newIdx, localeDecimal);
778
+ if (oldGroup !== newGroup) {
779
+ changes.push({
780
+ newIndex: newIdx,
781
+ oldIndex: oldFormattedIdx,
782
+ char,
783
+ isSeparator: false,
784
+ crossedGroup: true,
785
+ });
786
+ }
787
+ }
788
+ }
789
+ }
790
+ }
791
+ }
792
+ return changes;
793
+ };
794
+ //# sourceMappingURL=changes.js.map