@proyecto-viviana/solid-stately 0.2.3 → 0.2.7
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/LICENSE +21 -0
- package/dist/autocomplete/createAutocompleteState.d.ts +2 -1
- package/dist/checkbox/createCheckboxGroupState.d.ts +10 -1
- package/dist/collections/types.d.ts +11 -0
- package/dist/color/getColorChannels.d.ts +20 -0
- package/dist/data/createAsyncList.d.ts +111 -0
- package/dist/data/createListData.d.ts +65 -0
- package/dist/data/createTreeData.d.ts +61 -0
- package/dist/data/index.d.ts +3 -0
- package/dist/datepicker/index.d.ts +10 -0
- package/dist/grid/types.d.ts +5 -1
- package/dist/index.d.ts +6 -1
- package/dist/index.js +3737 -2697
- package/dist/index.js.map +1 -7
- package/dist/menu/index.d.ts +8 -0
- package/dist/radio/createRadioGroupState.d.ts +10 -1
- package/dist/select/createSelectState.d.ts +17 -0
- package/dist/selection/index.d.ts +11 -0
- package/dist/toast/createToastState.d.ts +7 -1
- package/dist/toggle/createToggleGroupState.d.ts +45 -0
- package/dist/toggle/index.d.ts +1 -0
- package/dist/tree/TreeCollection.d.ts +3 -2
- package/package.json +6 -5
- package/src/autocomplete/createAutocompleteState.ts +10 -11
- package/src/calendar/createDateFieldState.ts +24 -1
- package/src/checkbox/createCheckboxGroupState.ts +42 -6
- package/src/collections/ListCollection.ts +152 -146
- package/src/collections/createListState.ts +266 -264
- package/src/collections/createMenuState.ts +106 -106
- package/src/collections/createSelectionState.ts +336 -336
- package/src/collections/index.ts +46 -46
- package/src/collections/types.ts +181 -169
- package/src/color/Color.ts +951 -951
- package/src/color/createColorAreaState.ts +293 -293
- package/src/color/createColorFieldState.ts +292 -292
- package/src/color/createColorSliderState.ts +241 -241
- package/src/color/createColorWheelState.ts +211 -211
- package/src/color/getColorChannels.ts +34 -0
- package/src/color/index.ts +47 -47
- package/src/color/types.ts +127 -127
- package/src/combobox/createComboBoxState.ts +703 -703
- package/src/combobox/index.ts +13 -13
- package/src/data/createAsyncList.ts +377 -0
- package/src/data/createListData.ts +298 -0
- package/src/data/createTreeData.ts +433 -0
- package/src/data/index.ts +25 -0
- package/src/datepicker/index.ts +36 -0
- package/src/disclosure/createDisclosureState.ts +4 -4
- package/src/dnd/createDragState.ts +153 -153
- package/src/dnd/createDraggableCollectionState.ts +165 -165
- package/src/dnd/createDropState.ts +212 -212
- package/src/dnd/createDroppableCollectionState.ts +357 -357
- package/src/dnd/index.ts +76 -76
- package/src/dnd/types.ts +317 -317
- package/src/form/createFormValidationState.ts +389 -389
- package/src/form/index.ts +15 -15
- package/src/grid/types.ts +5 -0
- package/src/index.ts +49 -0
- package/src/menu/index.ts +19 -0
- package/src/numberfield/createNumberFieldState.ts +427 -383
- package/src/numberfield/index.ts +5 -5
- package/src/overlays/createOverlayTriggerState.ts +67 -67
- package/src/overlays/index.ts +5 -5
- package/src/radio/createRadioGroupState.ts +44 -6
- package/src/searchfield/createSearchFieldState.ts +62 -62
- package/src/searchfield/index.ts +5 -5
- package/src/select/createSelectState.ts +290 -181
- package/src/select/index.ts +5 -5
- package/src/selection/index.ts +28 -0
- package/src/slider/createSliderState.ts +211 -211
- package/src/slider/index.ts +6 -6
- package/src/tabs/createTabListState.ts +37 -11
- package/src/toast/createToastState.d.ts +6 -1
- package/src/toast/createToastState.ts +8 -1
- package/src/toggle/createToggleGroupState.ts +127 -0
- package/src/toggle/index.ts +6 -0
- package/src/tooltip/createTooltipTriggerState.ts +183 -183
- package/src/tooltip/index.ts +6 -6
- package/src/tree/TreeCollection.ts +208 -175
- package/src/tree/createTreeState.ts +392 -392
- package/src/tree/index.ts +13 -13
- package/src/tree/types.ts +174 -174
|
@@ -1,383 +1,427 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* State management for NumberField.
|
|
3
|
-
* Based on @react-stately/numberfield useNumberFieldState.
|
|
4
|
-
*/
|
|
5
|
-
|
|
6
|
-
import { createSignal, createMemo, type Accessor } from 'solid-js';
|
|
7
|
-
import { access, type MaybeAccessor } from '../utils';
|
|
8
|
-
|
|
9
|
-
export interface NumberFieldStateProps {
|
|
10
|
-
/** The current value (controlled). */
|
|
11
|
-
value?: number;
|
|
12
|
-
/** The default value (uncontrolled). */
|
|
13
|
-
defaultValue?: number;
|
|
14
|
-
/** Handler called when the value changes. */
|
|
15
|
-
onChange?: (value: number) => void;
|
|
16
|
-
/** The minimum value. */
|
|
17
|
-
minValue?: number;
|
|
18
|
-
/** The maximum value. */
|
|
19
|
-
maxValue?: number;
|
|
20
|
-
/** The step value for increment/decrement. */
|
|
21
|
-
step?: number;
|
|
22
|
-
/** Whether the field is disabled. */
|
|
23
|
-
isDisabled?: boolean;
|
|
24
|
-
/** Whether the field is read-only. */
|
|
25
|
-
isReadOnly?: boolean;
|
|
26
|
-
/** The locale for number formatting. */
|
|
27
|
-
locale?: string;
|
|
28
|
-
/** Number format options. */
|
|
29
|
-
formatOptions?: Intl.NumberFormatOptions;
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
export interface NumberFieldState {
|
|
33
|
-
/** The current input value as a string. */
|
|
34
|
-
inputValue: Accessor<string>;
|
|
35
|
-
/** The current numeric value. */
|
|
36
|
-
numberValue: Accessor<number>;
|
|
37
|
-
/** Whether the value can be incremented. */
|
|
38
|
-
canIncrement: Accessor<boolean>;
|
|
39
|
-
/** Whether the value can be decremented. */
|
|
40
|
-
canDecrement: Accessor<boolean>;
|
|
41
|
-
/** Whether the field is disabled. */
|
|
42
|
-
isDisabled: Accessor<boolean>;
|
|
43
|
-
/** Whether the field is read-only. */
|
|
44
|
-
isReadOnly: Accessor<boolean>;
|
|
45
|
-
/** The minimum value. */
|
|
46
|
-
minValue: Accessor<number | undefined>;
|
|
47
|
-
/** The maximum value. */
|
|
48
|
-
maxValue: Accessor<number | undefined>;
|
|
49
|
-
/** Set the input value. */
|
|
50
|
-
setInputValue: (value: string) => void;
|
|
51
|
-
/** Validate a partial input value. */
|
|
52
|
-
validate: (value: string) => boolean;
|
|
53
|
-
/** Commit the current input value. */
|
|
54
|
-
commit: () => void;
|
|
55
|
-
/** Increment the value by step. */
|
|
56
|
-
increment: () => void;
|
|
57
|
-
/** Decrement the value by step. */
|
|
58
|
-
decrement: () => void;
|
|
59
|
-
/** Set to maximum value. */
|
|
60
|
-
incrementToMax: () => void;
|
|
61
|
-
/** Set to minimum value. */
|
|
62
|
-
decrementToMin: () => void;
|
|
63
|
-
}
|
|
64
|
-
|
|
65
|
-
/**
|
|
66
|
-
* Handles decimal operations to avoid floating point errors.
|
|
67
|
-
*/
|
|
68
|
-
function handleDecimalOperation(
|
|
69
|
-
operator: '+' | '-',
|
|
70
|
-
value1: number,
|
|
71
|
-
value2: number
|
|
72
|
-
): number {
|
|
73
|
-
// Find the number of decimal places
|
|
74
|
-
const getDecimals = (n: number) => {
|
|
75
|
-
const str = String(n);
|
|
76
|
-
const idx = str.indexOf('.');
|
|
77
|
-
return idx === -1 ? 0 : str.length - idx - 1;
|
|
78
|
-
};
|
|
79
|
-
|
|
80
|
-
const decimals = Math.max(getDecimals(value1), getDecimals(value2));
|
|
81
|
-
const multiplier = Math.pow(10, decimals);
|
|
82
|
-
|
|
83
|
-
const int1 = Math.round(value1 * multiplier);
|
|
84
|
-
const int2 = Math.round(value2 * multiplier);
|
|
85
|
-
|
|
86
|
-
const result = operator === '+' ? int1 + int2 : int1 - int2;
|
|
87
|
-
return result / multiplier;
|
|
88
|
-
}
|
|
89
|
-
|
|
90
|
-
/**
|
|
91
|
-
* Clamps a value between min and max.
|
|
92
|
-
*/
|
|
93
|
-
function clamp(value: number, min?: number, max?: number): number {
|
|
94
|
-
let result = value;
|
|
95
|
-
if (min != null && result < min) result = min;
|
|
96
|
-
if (max != null && result > max) result = max;
|
|
97
|
-
return result;
|
|
98
|
-
}
|
|
99
|
-
|
|
100
|
-
/**
|
|
101
|
-
* Snaps a value to the nearest step.
|
|
102
|
-
*/
|
|
103
|
-
function snapToStep(value: number, step: number, min?: number): number {
|
|
104
|
-
const base = min ?? 0;
|
|
105
|
-
const diff = value - base;
|
|
106
|
-
const steps = Math.round(diff / step);
|
|
107
|
-
return handleDecimalOperation('+', base, steps * step);
|
|
108
|
-
}
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
const
|
|
121
|
-
|
|
122
|
-
//
|
|
123
|
-
const
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
return
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
const
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
if (
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
const
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
const
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
if (
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
if (p.
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
1
|
+
/**
|
|
2
|
+
* State management for NumberField.
|
|
3
|
+
* Based on @react-stately/numberfield useNumberFieldState.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { createSignal, createMemo, type Accessor } from 'solid-js';
|
|
7
|
+
import { access, type MaybeAccessor } from '../utils';
|
|
8
|
+
|
|
9
|
+
export interface NumberFieldStateProps {
|
|
10
|
+
/** The current value (controlled). */
|
|
11
|
+
value?: number;
|
|
12
|
+
/** The default value (uncontrolled). */
|
|
13
|
+
defaultValue?: number;
|
|
14
|
+
/** Handler called when the value changes. */
|
|
15
|
+
onChange?: (value: number) => void;
|
|
16
|
+
/** The minimum value. */
|
|
17
|
+
minValue?: number;
|
|
18
|
+
/** The maximum value. */
|
|
19
|
+
maxValue?: number;
|
|
20
|
+
/** The step value for increment/decrement. */
|
|
21
|
+
step?: number;
|
|
22
|
+
/** Whether the field is disabled. */
|
|
23
|
+
isDisabled?: boolean;
|
|
24
|
+
/** Whether the field is read-only. */
|
|
25
|
+
isReadOnly?: boolean;
|
|
26
|
+
/** The locale for number formatting. */
|
|
27
|
+
locale?: string;
|
|
28
|
+
/** Number format options. */
|
|
29
|
+
formatOptions?: Intl.NumberFormatOptions;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export interface NumberFieldState {
|
|
33
|
+
/** The current input value as a string. */
|
|
34
|
+
inputValue: Accessor<string>;
|
|
35
|
+
/** The current numeric value. */
|
|
36
|
+
numberValue: Accessor<number>;
|
|
37
|
+
/** Whether the value can be incremented. */
|
|
38
|
+
canIncrement: Accessor<boolean>;
|
|
39
|
+
/** Whether the value can be decremented. */
|
|
40
|
+
canDecrement: Accessor<boolean>;
|
|
41
|
+
/** Whether the field is disabled. */
|
|
42
|
+
isDisabled: Accessor<boolean>;
|
|
43
|
+
/** Whether the field is read-only. */
|
|
44
|
+
isReadOnly: Accessor<boolean>;
|
|
45
|
+
/** The minimum value. */
|
|
46
|
+
minValue: Accessor<number | undefined>;
|
|
47
|
+
/** The maximum value. */
|
|
48
|
+
maxValue: Accessor<number | undefined>;
|
|
49
|
+
/** Set the input value. */
|
|
50
|
+
setInputValue: (value: string) => void;
|
|
51
|
+
/** Validate a partial input value. */
|
|
52
|
+
validate: (value: string) => boolean;
|
|
53
|
+
/** Commit the current input value. */
|
|
54
|
+
commit: () => void;
|
|
55
|
+
/** Increment the value by step. */
|
|
56
|
+
increment: () => void;
|
|
57
|
+
/** Decrement the value by step. */
|
|
58
|
+
decrement: () => void;
|
|
59
|
+
/** Set to maximum value. */
|
|
60
|
+
incrementToMax: () => void;
|
|
61
|
+
/** Set to minimum value. */
|
|
62
|
+
decrementToMin: () => void;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* Handles decimal operations to avoid floating point errors.
|
|
67
|
+
*/
|
|
68
|
+
function handleDecimalOperation(
|
|
69
|
+
operator: '+' | '-',
|
|
70
|
+
value1: number,
|
|
71
|
+
value2: number
|
|
72
|
+
): number {
|
|
73
|
+
// Find the number of decimal places
|
|
74
|
+
const getDecimals = (n: number) => {
|
|
75
|
+
const str = String(n);
|
|
76
|
+
const idx = str.indexOf('.');
|
|
77
|
+
return idx === -1 ? 0 : str.length - idx - 1;
|
|
78
|
+
};
|
|
79
|
+
|
|
80
|
+
const decimals = Math.max(getDecimals(value1), getDecimals(value2));
|
|
81
|
+
const multiplier = Math.pow(10, decimals);
|
|
82
|
+
|
|
83
|
+
const int1 = Math.round(value1 * multiplier);
|
|
84
|
+
const int2 = Math.round(value2 * multiplier);
|
|
85
|
+
|
|
86
|
+
const result = operator === '+' ? int1 + int2 : int1 - int2;
|
|
87
|
+
return result / multiplier;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* Clamps a value between min and max.
|
|
92
|
+
*/
|
|
93
|
+
function clamp(value: number, min?: number, max?: number): number {
|
|
94
|
+
let result = value;
|
|
95
|
+
if (min != null && result < min) result = min;
|
|
96
|
+
if (max != null && result > max) result = max;
|
|
97
|
+
return result;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/**
|
|
101
|
+
* Snaps a value to the nearest step.
|
|
102
|
+
*/
|
|
103
|
+
function snapToStep(value: number, step: number, min?: number): number {
|
|
104
|
+
const base = min ?? 0;
|
|
105
|
+
const diff = value - base;
|
|
106
|
+
const steps = Math.round(diff / step);
|
|
107
|
+
return handleDecimalOperation('+', base, steps * step);
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
function isValidStep(step: number | undefined): step is number {
|
|
111
|
+
return step != null && !isNaN(step) && step > 0;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/**
|
|
115
|
+
* Creates state for a number field.
|
|
116
|
+
*/
|
|
117
|
+
export function createNumberFieldState(
|
|
118
|
+
props: MaybeAccessor<NumberFieldStateProps>
|
|
119
|
+
): NumberFieldState {
|
|
120
|
+
const getProps = () => access(props);
|
|
121
|
+
|
|
122
|
+
// Get locale and formatter
|
|
123
|
+
const locale = () => getProps().locale ?? 'en-US';
|
|
124
|
+
const formatOptions = () => getProps().formatOptions ?? {};
|
|
125
|
+
|
|
126
|
+
// Create number formatter
|
|
127
|
+
const formatter = createMemo(() => {
|
|
128
|
+
return new Intl.NumberFormat(locale(), formatOptions());
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
// Create number parser (simplified - real implementation would be more robust)
|
|
132
|
+
const parseNumber = (value: string): number => {
|
|
133
|
+
if (!value || value === '' || value === '-') return NaN;
|
|
134
|
+
|
|
135
|
+
// Handle locale-specific decimal separators
|
|
136
|
+
const opts = formatOptions();
|
|
137
|
+
const testNumber = formatter().format(1.1);
|
|
138
|
+
const decimalSeparator = testNumber.charAt(1);
|
|
139
|
+
|
|
140
|
+
// Normalize the input
|
|
141
|
+
let normalized = value;
|
|
142
|
+
if (decimalSeparator !== '.') {
|
|
143
|
+
normalized = normalized.replace(decimalSeparator, '.');
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
// Remove grouping separators and currency symbols
|
|
147
|
+
normalized = normalized.replace(/[^\d.\-]/g, '');
|
|
148
|
+
|
|
149
|
+
const parsed = parseFloat(normalized);
|
|
150
|
+
if (isNaN(parsed)) return parsed;
|
|
151
|
+
|
|
152
|
+
if (opts.style === 'percent') {
|
|
153
|
+
return parsed / 100;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
return parsed;
|
|
157
|
+
};
|
|
158
|
+
|
|
159
|
+
// Format a number to string
|
|
160
|
+
const formatNumber = (value: number): string => {
|
|
161
|
+
if (isNaN(value)) return '';
|
|
162
|
+
return formatter().format(value);
|
|
163
|
+
};
|
|
164
|
+
|
|
165
|
+
// Determine step value
|
|
166
|
+
const hasCustomStep = createMemo(() => isValidStep(getProps().step));
|
|
167
|
+
|
|
168
|
+
const step = createMemo(() => {
|
|
169
|
+
const p = getProps();
|
|
170
|
+
if (hasCustomStep()) return p.step as number;
|
|
171
|
+
// Default step for percent is 0.01
|
|
172
|
+
if (p.formatOptions?.style === 'percent') return 0.01;
|
|
173
|
+
return 1;
|
|
174
|
+
});
|
|
175
|
+
|
|
176
|
+
// Internal signals
|
|
177
|
+
const [inputValue, setInputValueInternal] = createSignal<string>('');
|
|
178
|
+
const [numberValue, setNumberValue] = createSignal<number>(NaN);
|
|
179
|
+
|
|
180
|
+
const applyConstraints = (value: number): number => {
|
|
181
|
+
const p = getProps();
|
|
182
|
+
if (isNaN(value)) return NaN;
|
|
183
|
+
|
|
184
|
+
if (hasCustomStep()) {
|
|
185
|
+
return clamp(snapToStep(value, step(), p.minValue), p.minValue, p.maxValue);
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
return clamp(value, p.minValue, p.maxValue);
|
|
189
|
+
};
|
|
190
|
+
|
|
191
|
+
// Initialize from props
|
|
192
|
+
const initValue = () => {
|
|
193
|
+
const p = getProps();
|
|
194
|
+
const initial = p.value ?? p.defaultValue;
|
|
195
|
+
if (initial != null) {
|
|
196
|
+
const constrained = applyConstraints(initial);
|
|
197
|
+
setNumberValue(constrained);
|
|
198
|
+
setInputValueInternal(formatNumber(constrained));
|
|
199
|
+
}
|
|
200
|
+
};
|
|
201
|
+
|
|
202
|
+
// Call init on first access
|
|
203
|
+
let initialized = false;
|
|
204
|
+
const ensureInitialized = () => {
|
|
205
|
+
if (!initialized) {
|
|
206
|
+
initialized = true;
|
|
207
|
+
initValue();
|
|
208
|
+
}
|
|
209
|
+
};
|
|
210
|
+
|
|
211
|
+
// Controlled mode: sync with props.value
|
|
212
|
+
const actualNumberValue = createMemo(() => {
|
|
213
|
+
ensureInitialized();
|
|
214
|
+
const p = getProps();
|
|
215
|
+
if (p.value !== undefined) {
|
|
216
|
+
return applyConstraints(p.value);
|
|
217
|
+
}
|
|
218
|
+
return numberValue();
|
|
219
|
+
});
|
|
220
|
+
|
|
221
|
+
let lastControlledValue: number | undefined = undefined;
|
|
222
|
+
const syncControlledValue = () => {
|
|
223
|
+
const p = getProps();
|
|
224
|
+
if (p.value === undefined) {
|
|
225
|
+
lastControlledValue = undefined;
|
|
226
|
+
return;
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
const constrained = applyConstraints(p.value);
|
|
230
|
+
if (lastControlledValue === undefined || !Object.is(lastControlledValue, constrained)) {
|
|
231
|
+
lastControlledValue = constrained;
|
|
232
|
+
setNumberValue(constrained);
|
|
233
|
+
setInputValueInternal(formatNumber(constrained));
|
|
234
|
+
}
|
|
235
|
+
};
|
|
236
|
+
|
|
237
|
+
const parsedInputValue = () => {
|
|
238
|
+
ensureInitialized();
|
|
239
|
+
syncControlledValue();
|
|
240
|
+
return parseNumber(inputValue());
|
|
241
|
+
};
|
|
242
|
+
|
|
243
|
+
// Validate partial input
|
|
244
|
+
const validate = (value: string): boolean => {
|
|
245
|
+
if (value === '' || value === '-') return true;
|
|
246
|
+
|
|
247
|
+
// Allow partial decimal input like "1."
|
|
248
|
+
const opts = formatOptions();
|
|
249
|
+
const testNumber = formatter().format(1.1);
|
|
250
|
+
const decimalSeparator = testNumber.charAt(1);
|
|
251
|
+
|
|
252
|
+
// Check if it's a valid partial number
|
|
253
|
+
const pattern = new RegExp(
|
|
254
|
+
`^-?\\d*${decimalSeparator === '.' ? '\\.' : decimalSeparator}?\\d*$`
|
|
255
|
+
);
|
|
256
|
+
return pattern.test(value);
|
|
257
|
+
};
|
|
258
|
+
|
|
259
|
+
// Set input value with validation
|
|
260
|
+
const setInputValue = (value: string) => {
|
|
261
|
+
ensureInitialized();
|
|
262
|
+
syncControlledValue();
|
|
263
|
+
setInputValueInternal(value);
|
|
264
|
+
};
|
|
265
|
+
|
|
266
|
+
// Commit the current input value
|
|
267
|
+
const commit = () => {
|
|
268
|
+
ensureInitialized();
|
|
269
|
+
syncControlledValue();
|
|
270
|
+
const p = getProps();
|
|
271
|
+
const input = inputValue();
|
|
272
|
+
|
|
273
|
+
if (input === '' || input === '-') {
|
|
274
|
+
// Clear value
|
|
275
|
+
setNumberValue(NaN);
|
|
276
|
+
setInputValueInternal(p.value === undefined ? '' : formatNumber(actualNumberValue()));
|
|
277
|
+
p.onChange?.(NaN);
|
|
278
|
+
return;
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
let parsed = parseNumber(input);
|
|
282
|
+
|
|
283
|
+
if (isNaN(parsed)) {
|
|
284
|
+
// Invalid input - revert to current value
|
|
285
|
+
setInputValueInternal(formatNumber(actualNumberValue()));
|
|
286
|
+
return;
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
// Clamp and optionally snap to custom step.
|
|
290
|
+
parsed = applyConstraints(parsed);
|
|
291
|
+
|
|
292
|
+
// Update state
|
|
293
|
+
setNumberValue(parsed);
|
|
294
|
+
setInputValueInternal(formatNumber(parsed));
|
|
295
|
+
|
|
296
|
+
p.onChange?.(parsed);
|
|
297
|
+
};
|
|
298
|
+
|
|
299
|
+
// Check if can increment
|
|
300
|
+
const canIncrement = createMemo(() => {
|
|
301
|
+
ensureInitialized();
|
|
302
|
+
const p = getProps();
|
|
303
|
+
if (p.isDisabled || p.isReadOnly) return false;
|
|
304
|
+
|
|
305
|
+
const current = parsedInputValue();
|
|
306
|
+
if (isNaN(current)) return true; // Can start from min
|
|
307
|
+
|
|
308
|
+
if (p.maxValue == null) return true;
|
|
309
|
+
return (
|
|
310
|
+
snapToStep(current, step(), p.minValue) > current ||
|
|
311
|
+
handleDecimalOperation('+', current, step()) <= p.maxValue
|
|
312
|
+
);
|
|
313
|
+
});
|
|
314
|
+
|
|
315
|
+
// Check if can decrement
|
|
316
|
+
const canDecrement = createMemo(() => {
|
|
317
|
+
ensureInitialized();
|
|
318
|
+
const p = getProps();
|
|
319
|
+
if (p.isDisabled || p.isReadOnly) return false;
|
|
320
|
+
|
|
321
|
+
const current = parsedInputValue();
|
|
322
|
+
if (isNaN(current)) return true; // Can start from max
|
|
323
|
+
|
|
324
|
+
if (p.minValue == null) return true;
|
|
325
|
+
return (
|
|
326
|
+
snapToStep(current, step(), p.minValue) < current ||
|
|
327
|
+
handleDecimalOperation('-', current, step()) >= p.minValue
|
|
328
|
+
);
|
|
329
|
+
});
|
|
330
|
+
|
|
331
|
+
const safeNextStep = (operation: '+' | '-', minOrMaxValue: number = 0): number => {
|
|
332
|
+
const p = getProps();
|
|
333
|
+
const parsed = parsedInputValue();
|
|
334
|
+
|
|
335
|
+
if (isNaN(parsed)) {
|
|
336
|
+
const base = isNaN(minOrMaxValue) ? 0 : minOrMaxValue;
|
|
337
|
+
return clamp(snapToStep(base, step(), p.minValue), p.minValue, p.maxValue);
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
const snapped = snapToStep(parsed, step(), p.minValue);
|
|
341
|
+
if ((operation === '+' && snapped > parsed) || (operation === '-' && snapped < parsed)) {
|
|
342
|
+
return clamp(snapped, p.minValue, p.maxValue);
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
const next = handleDecimalOperation(operation, parsed, step());
|
|
346
|
+
return clamp(snapToStep(next, step(), p.minValue), p.minValue, p.maxValue);
|
|
347
|
+
};
|
|
348
|
+
|
|
349
|
+
// Increment by step
|
|
350
|
+
const increment = () => {
|
|
351
|
+
ensureInitialized();
|
|
352
|
+
syncControlledValue();
|
|
353
|
+
const p = getProps();
|
|
354
|
+
if (p.isDisabled || p.isReadOnly) return;
|
|
355
|
+
|
|
356
|
+
const current = safeNextStep('+', p.minValue);
|
|
357
|
+
setNumberValue(current);
|
|
358
|
+
setInputValueInternal(formatNumber(current));
|
|
359
|
+
p.onChange?.(current);
|
|
360
|
+
};
|
|
361
|
+
|
|
362
|
+
// Decrement by step
|
|
363
|
+
const decrement = () => {
|
|
364
|
+
ensureInitialized();
|
|
365
|
+
syncControlledValue();
|
|
366
|
+
const p = getProps();
|
|
367
|
+
if (p.isDisabled || p.isReadOnly) return;
|
|
368
|
+
|
|
369
|
+
const current = safeNextStep('-', p.maxValue);
|
|
370
|
+
setNumberValue(current);
|
|
371
|
+
setInputValueInternal(formatNumber(current));
|
|
372
|
+
p.onChange?.(current);
|
|
373
|
+
};
|
|
374
|
+
|
|
375
|
+
// Set to max
|
|
376
|
+
const incrementToMax = () => {
|
|
377
|
+
ensureInitialized();
|
|
378
|
+
syncControlledValue();
|
|
379
|
+
const p = getProps();
|
|
380
|
+
if (p.isDisabled || p.isReadOnly) return;
|
|
381
|
+
|
|
382
|
+
if (p.maxValue == null) return;
|
|
383
|
+
|
|
384
|
+
const snapped = snapToStep(p.maxValue, step(), p.minValue);
|
|
385
|
+
setNumberValue(snapped);
|
|
386
|
+
setInputValueInternal(formatNumber(snapped));
|
|
387
|
+
p.onChange?.(snapped);
|
|
388
|
+
};
|
|
389
|
+
|
|
390
|
+
// Set to min
|
|
391
|
+
const decrementToMin = () => {
|
|
392
|
+
ensureInitialized();
|
|
393
|
+
syncControlledValue();
|
|
394
|
+
const p = getProps();
|
|
395
|
+
if (p.isDisabled || p.isReadOnly) return;
|
|
396
|
+
|
|
397
|
+
if (p.minValue == null) return;
|
|
398
|
+
|
|
399
|
+
setNumberValue(p.minValue);
|
|
400
|
+
setInputValueInternal(formatNumber(p.minValue));
|
|
401
|
+
p.onChange?.(p.minValue);
|
|
402
|
+
};
|
|
403
|
+
|
|
404
|
+
return {
|
|
405
|
+
get inputValue() {
|
|
406
|
+
ensureInitialized();
|
|
407
|
+
syncControlledValue();
|
|
408
|
+
return inputValue;
|
|
409
|
+
},
|
|
410
|
+
get numberValue() {
|
|
411
|
+
return actualNumberValue;
|
|
412
|
+
},
|
|
413
|
+
canIncrement,
|
|
414
|
+
canDecrement,
|
|
415
|
+
isDisabled: () => getProps().isDisabled ?? false,
|
|
416
|
+
isReadOnly: () => getProps().isReadOnly ?? false,
|
|
417
|
+
minValue: () => getProps().minValue,
|
|
418
|
+
maxValue: () => getProps().maxValue,
|
|
419
|
+
setInputValue,
|
|
420
|
+
validate,
|
|
421
|
+
commit,
|
|
422
|
+
increment,
|
|
423
|
+
decrement,
|
|
424
|
+
incrementToMax,
|
|
425
|
+
decrementToMin,
|
|
426
|
+
};
|
|
427
|
+
}
|