@zag-js/number-input 1.24.1 → 1.25.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.
package/dist/index.js CHANGED
@@ -106,6 +106,83 @@ var createVirtualCursor = (ctx, point) => {
106
106
  doc.body.appendChild(el);
107
107
  };
108
108
 
109
+ // src/cursor.ts
110
+ function recordCursor(inputEl, scope) {
111
+ if (!inputEl || !scope.isActiveElement(inputEl)) return;
112
+ try {
113
+ const { selectionStart: start, selectionEnd: end, value } = inputEl;
114
+ if (start == null || end == null) return void 0;
115
+ return { start, end, value };
116
+ } catch {
117
+ return void 0;
118
+ }
119
+ }
120
+ function restoreCursor(inputEl, selection, scope) {
121
+ if (!inputEl || !scope.isActiveElement(inputEl)) return;
122
+ if (!selection) {
123
+ const len = inputEl.value.length;
124
+ inputEl.setSelectionRange(len, len);
125
+ return;
126
+ }
127
+ try {
128
+ const newValue = inputEl.value;
129
+ const { start, end, value: oldValue } = selection;
130
+ if (newValue === oldValue) {
131
+ inputEl.setSelectionRange(start, end);
132
+ return;
133
+ }
134
+ const newStart = getNewCursorPosition(oldValue, newValue, start);
135
+ const newEnd = start === end ? newStart : getNewCursorPosition(oldValue, newValue, end);
136
+ const clampedStart = Math.max(0, Math.min(newStart, newValue.length));
137
+ const clampedEnd = Math.max(clampedStart, Math.min(newEnd, newValue.length));
138
+ inputEl.setSelectionRange(clampedStart, clampedEnd);
139
+ } catch {
140
+ const len = inputEl.value.length;
141
+ inputEl.setSelectionRange(len, len);
142
+ }
143
+ }
144
+ function getNewCursorPosition(oldValue, newValue, oldPosition) {
145
+ const beforeCursor = oldValue.slice(0, oldPosition);
146
+ const afterCursor = oldValue.slice(oldPosition);
147
+ let prefixLength = 0;
148
+ const maxPrefixLength = Math.min(beforeCursor.length, newValue.length);
149
+ for (let i = 0; i < maxPrefixLength; i++) {
150
+ if (beforeCursor[i] === newValue[i]) {
151
+ prefixLength = i + 1;
152
+ } else {
153
+ break;
154
+ }
155
+ }
156
+ let suffixLength = 0;
157
+ const maxSuffixLength = Math.min(afterCursor.length, newValue.length - prefixLength);
158
+ for (let i = 0; i < maxSuffixLength; i++) {
159
+ const oldIndex = afterCursor.length - 1 - i;
160
+ const newIndex = newValue.length - 1 - i;
161
+ if (afterCursor[oldIndex] === newValue[newIndex]) {
162
+ suffixLength = i + 1;
163
+ } else {
164
+ break;
165
+ }
166
+ }
167
+ if (prefixLength >= beforeCursor.length) {
168
+ return prefixLength;
169
+ }
170
+ if (suffixLength >= afterCursor.length) {
171
+ return newValue.length - suffixLength;
172
+ }
173
+ if (prefixLength > 0) {
174
+ return prefixLength;
175
+ }
176
+ if (suffixLength > 0) {
177
+ return newValue.length - suffixLength;
178
+ }
179
+ if (oldValue.length > 0) {
180
+ const ratio = oldPosition / oldValue.length;
181
+ return Math.round(ratio * newValue.length);
182
+ }
183
+ return newValue.length;
184
+ }
185
+
109
186
  // src/number-input.connect.ts
110
187
  function connect(service, normalize) {
111
188
  const { state, send, prop, scope, computed } = service;
@@ -227,7 +304,8 @@ function connect(service, normalize) {
227
304
  send({ type: "INPUT.BLUR" });
228
305
  },
229
306
  onInput(event) {
230
- send({ type: "INPUT.CHANGE", target: event.currentTarget, hint: "set" });
307
+ const selection = recordCursor(event.currentTarget, scope);
308
+ send({ type: "INPUT.CHANGE", target: event.currentTarget, hint: "set", selection });
231
309
  },
232
310
  onBeforeInput(event) {
233
311
  try {
@@ -361,49 +439,6 @@ function connect(service, normalize) {
361
439
  }
362
440
  };
363
441
  }
364
-
365
- // src/cursor.ts
366
- function recordCursor(inputEl, scope) {
367
- if (!inputEl || !scope.isActiveElement(inputEl)) return;
368
- try {
369
- const { selectionStart: start, selectionEnd: end, value } = inputEl;
370
- const beforeTxt = value.substring(0, start);
371
- const afterTxt = value.substring(end);
372
- return {
373
- start,
374
- end,
375
- value,
376
- beforeTxt,
377
- afterTxt
378
- };
379
- } catch {
380
- }
381
- }
382
- function restoreCursor(inputEl, selection, scope) {
383
- if (!inputEl || !scope.isActiveElement(inputEl)) return;
384
- if (!selection) {
385
- inputEl.setSelectionRange(inputEl.value.length, inputEl.value.length);
386
- return;
387
- }
388
- try {
389
- const { value } = inputEl;
390
- const { beforeTxt = "", afterTxt = "", start } = selection;
391
- let startPos = value.length;
392
- if (value.endsWith(afterTxt)) {
393
- startPos = value.length - afterTxt.length;
394
- } else if (value.startsWith(beforeTxt)) {
395
- startPos = beforeTxt.length;
396
- } else if (start != null) {
397
- const beforeLastChar = beforeTxt[start - 1];
398
- const newIndex = value.indexOf(beforeLastChar, start - 1);
399
- if (newIndex !== -1) {
400
- startPos = newIndex + 1;
401
- }
402
- }
403
- inputEl.setSelectionRange(startPos, startPos);
404
- } catch {
405
- }
406
- }
407
442
  var createFormatter = (locale, options = {}) => {
408
443
  return new Intl.NumberFormat(locale, options);
409
444
  };
@@ -815,7 +850,7 @@ var machine = createMachine({
815
850
  syncInputElement({ context, event, computed, scope }) {
816
851
  const value = event.type.endsWith("CHANGE") ? context.get("value") : computed("formattedValue");
817
852
  const inputEl = getInputEl(scope);
818
- const sel = recordCursor(inputEl, scope);
853
+ const sel = event.selection;
819
854
  domQuery.raf(() => {
820
855
  domQuery.setElementValue(inputEl, value);
821
856
  restoreCursor(inputEl, sel, scope);
package/dist/index.mjs CHANGED
@@ -104,6 +104,83 @@ var createVirtualCursor = (ctx, point) => {
104
104
  doc.body.appendChild(el);
105
105
  };
106
106
 
107
+ // src/cursor.ts
108
+ function recordCursor(inputEl, scope) {
109
+ if (!inputEl || !scope.isActiveElement(inputEl)) return;
110
+ try {
111
+ const { selectionStart: start, selectionEnd: end, value } = inputEl;
112
+ if (start == null || end == null) return void 0;
113
+ return { start, end, value };
114
+ } catch {
115
+ return void 0;
116
+ }
117
+ }
118
+ function restoreCursor(inputEl, selection, scope) {
119
+ if (!inputEl || !scope.isActiveElement(inputEl)) return;
120
+ if (!selection) {
121
+ const len = inputEl.value.length;
122
+ inputEl.setSelectionRange(len, len);
123
+ return;
124
+ }
125
+ try {
126
+ const newValue = inputEl.value;
127
+ const { start, end, value: oldValue } = selection;
128
+ if (newValue === oldValue) {
129
+ inputEl.setSelectionRange(start, end);
130
+ return;
131
+ }
132
+ const newStart = getNewCursorPosition(oldValue, newValue, start);
133
+ const newEnd = start === end ? newStart : getNewCursorPosition(oldValue, newValue, end);
134
+ const clampedStart = Math.max(0, Math.min(newStart, newValue.length));
135
+ const clampedEnd = Math.max(clampedStart, Math.min(newEnd, newValue.length));
136
+ inputEl.setSelectionRange(clampedStart, clampedEnd);
137
+ } catch {
138
+ const len = inputEl.value.length;
139
+ inputEl.setSelectionRange(len, len);
140
+ }
141
+ }
142
+ function getNewCursorPosition(oldValue, newValue, oldPosition) {
143
+ const beforeCursor = oldValue.slice(0, oldPosition);
144
+ const afterCursor = oldValue.slice(oldPosition);
145
+ let prefixLength = 0;
146
+ const maxPrefixLength = Math.min(beforeCursor.length, newValue.length);
147
+ for (let i = 0; i < maxPrefixLength; i++) {
148
+ if (beforeCursor[i] === newValue[i]) {
149
+ prefixLength = i + 1;
150
+ } else {
151
+ break;
152
+ }
153
+ }
154
+ let suffixLength = 0;
155
+ const maxSuffixLength = Math.min(afterCursor.length, newValue.length - prefixLength);
156
+ for (let i = 0; i < maxSuffixLength; i++) {
157
+ const oldIndex = afterCursor.length - 1 - i;
158
+ const newIndex = newValue.length - 1 - i;
159
+ if (afterCursor[oldIndex] === newValue[newIndex]) {
160
+ suffixLength = i + 1;
161
+ } else {
162
+ break;
163
+ }
164
+ }
165
+ if (prefixLength >= beforeCursor.length) {
166
+ return prefixLength;
167
+ }
168
+ if (suffixLength >= afterCursor.length) {
169
+ return newValue.length - suffixLength;
170
+ }
171
+ if (prefixLength > 0) {
172
+ return prefixLength;
173
+ }
174
+ if (suffixLength > 0) {
175
+ return newValue.length - suffixLength;
176
+ }
177
+ if (oldValue.length > 0) {
178
+ const ratio = oldPosition / oldValue.length;
179
+ return Math.round(ratio * newValue.length);
180
+ }
181
+ return newValue.length;
182
+ }
183
+
107
184
  // src/number-input.connect.ts
108
185
  function connect(service, normalize) {
109
186
  const { state, send, prop, scope, computed } = service;
@@ -225,7 +302,8 @@ function connect(service, normalize) {
225
302
  send({ type: "INPUT.BLUR" });
226
303
  },
227
304
  onInput(event) {
228
- send({ type: "INPUT.CHANGE", target: event.currentTarget, hint: "set" });
305
+ const selection = recordCursor(event.currentTarget, scope);
306
+ send({ type: "INPUT.CHANGE", target: event.currentTarget, hint: "set", selection });
229
307
  },
230
308
  onBeforeInput(event) {
231
309
  try {
@@ -359,49 +437,6 @@ function connect(service, normalize) {
359
437
  }
360
438
  };
361
439
  }
362
-
363
- // src/cursor.ts
364
- function recordCursor(inputEl, scope) {
365
- if (!inputEl || !scope.isActiveElement(inputEl)) return;
366
- try {
367
- const { selectionStart: start, selectionEnd: end, value } = inputEl;
368
- const beforeTxt = value.substring(0, start);
369
- const afterTxt = value.substring(end);
370
- return {
371
- start,
372
- end,
373
- value,
374
- beforeTxt,
375
- afterTxt
376
- };
377
- } catch {
378
- }
379
- }
380
- function restoreCursor(inputEl, selection, scope) {
381
- if (!inputEl || !scope.isActiveElement(inputEl)) return;
382
- if (!selection) {
383
- inputEl.setSelectionRange(inputEl.value.length, inputEl.value.length);
384
- return;
385
- }
386
- try {
387
- const { value } = inputEl;
388
- const { beforeTxt = "", afterTxt = "", start } = selection;
389
- let startPos = value.length;
390
- if (value.endsWith(afterTxt)) {
391
- startPos = value.length - afterTxt.length;
392
- } else if (value.startsWith(beforeTxt)) {
393
- startPos = beforeTxt.length;
394
- } else if (start != null) {
395
- const beforeLastChar = beforeTxt[start - 1];
396
- const newIndex = value.indexOf(beforeLastChar, start - 1);
397
- if (newIndex !== -1) {
398
- startPos = newIndex + 1;
399
- }
400
- }
401
- inputEl.setSelectionRange(startPos, startPos);
402
- } catch {
403
- }
404
- }
405
440
  var createFormatter = (locale, options = {}) => {
406
441
  return new Intl.NumberFormat(locale, options);
407
442
  };
@@ -813,7 +848,7 @@ var machine = createMachine({
813
848
  syncInputElement({ context, event, computed, scope }) {
814
849
  const value = event.type.endsWith("CHANGE") ? context.get("value") : computed("formattedValue");
815
850
  const inputEl = getInputEl(scope);
816
- const sel = recordCursor(inputEl, scope);
851
+ const sel = event.selection;
817
852
  raf(() => {
818
853
  setElementValue(inputEl, value);
819
854
  restoreCursor(inputEl, sel, scope);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@zag-js/number-input",
3
- "version": "1.24.1",
3
+ "version": "1.25.0",
4
4
  "description": "Core logic for the number-input widget implemented as a state machine",
5
5
  "keywords": [
6
6
  "js",
@@ -27,11 +27,11 @@
27
27
  },
28
28
  "dependencies": {
29
29
  "@internationalized/number": "3.6.5",
30
- "@zag-js/anatomy": "1.24.1",
31
- "@zag-js/core": "1.24.1",
32
- "@zag-js/dom-query": "1.24.1",
33
- "@zag-js/types": "1.24.1",
34
- "@zag-js/utils": "1.24.1"
30
+ "@zag-js/anatomy": "1.25.0",
31
+ "@zag-js/core": "1.25.0",
32
+ "@zag-js/dom-query": "1.25.0",
33
+ "@zag-js/types": "1.25.0",
34
+ "@zag-js/utils": "1.25.0"
35
35
  },
36
36
  "devDependencies": {
37
37
  "clean-package": "2.2.0"