stimeo-ui 0.7.0 → 0.9.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/CHANGELOG.md +151 -0
- package/dist/controllers/auto_submit_controller.d.ts +14 -3
- package/dist/controllers/auto_submit_controller.js +94 -16
- package/dist/controllers/auto_submit_controller.js.map +1 -1
- package/dist/controllers/bulk_select_controller.d.ts +62 -15
- package/dist/controllers/bulk_select_controller.js +139 -28
- package/dist/controllers/bulk_select_controller.js.map +1 -1
- package/dist/controllers/carousel_controller.d.ts +81 -47
- package/dist/controllers/carousel_controller.js +451 -100
- package/dist/controllers/carousel_controller.js.map +1 -1
- package/dist/controllers/clipboard_controller.d.ts +48 -15
- package/dist/controllers/clipboard_controller.js +102 -20
- package/dist/controllers/clipboard_controller.js.map +1 -1
- package/dist/controllers/color_picker_controller.d.ts +37 -6
- package/dist/controllers/color_picker_controller.js +180 -43
- package/dist/controllers/color_picker_controller.js.map +1 -1
- package/dist/controllers/currency_input_controller.d.ts +39 -5
- package/dist/controllers/currency_input_controller.js +305 -74
- package/dist/controllers/currency_input_controller.js.map +1 -1
- package/dist/controllers/data_grid_controller.d.ts +25 -9
- package/dist/controllers/data_grid_controller.js +150 -24
- package/dist/controllers/data_grid_controller.js.map +1 -1
- package/dist/controllers/direct_upload_controller.d.ts +3 -1
- package/dist/controllers/direct_upload_controller.js +12 -2
- package/dist/controllers/direct_upload_controller.js.map +1 -1
- package/dist/controllers/editable_controller.d.ts +34 -9
- package/dist/controllers/editable_controller.js +83 -30
- package/dist/controllers/editable_controller.js.map +1 -1
- package/dist/controllers/file_dropzone_controller.d.ts +123 -29
- package/dist/controllers/file_dropzone_controller.js +386 -63
- package/dist/controllers/file_dropzone_controller.js.map +1 -1
- package/dist/controllers/filter_controller.d.ts +15 -3
- package/dist/controllers/filter_controller.js +32 -1
- package/dist/controllers/filter_controller.js.map +1 -1
- package/dist/controllers/flash_controller.d.ts +3 -1
- package/dist/controllers/flash_controller.js +3 -1
- package/dist/controllers/flash_controller.js.map +1 -1
- package/dist/controllers/frame_loading_controller.d.ts +4 -2
- package/dist/controllers/frame_loading_controller.js +2 -1
- package/dist/controllers/frame_loading_controller.js.map +1 -1
- package/dist/controllers/input_mask_controller.d.ts +42 -16
- package/dist/controllers/input_mask_controller.js +251 -76
- package/dist/controllers/input_mask_controller.js.map +1 -1
- package/dist/controllers/masonry_controller.d.ts +34 -5
- package/dist/controllers/masonry_controller.js +129 -17
- package/dist/controllers/masonry_controller.js.map +1 -1
- package/dist/controllers/nested_form_controller.d.ts +51 -14
- package/dist/controllers/nested_form_controller.js +450 -42
- package/dist/controllers/nested_form_controller.js.map +1 -1
- package/dist/controllers/otp_controller.d.ts +64 -25
- package/dist/controllers/otp_controller.js +485 -114
- package/dist/controllers/otp_controller.js.map +1 -1
- package/dist/controllers/reset_before_cache_controller.d.ts +25 -2
- package/dist/controllers/reset_before_cache_controller.js +51 -5
- package/dist/controllers/reset_before_cache_controller.js.map +1 -1
- package/dist/controllers/resizable_controller.d.ts +23 -7
- package/dist/controllers/resizable_controller.js +128 -55
- package/dist/controllers/resizable_controller.js.map +1 -1
- package/dist/controllers/spinner_controller.d.ts +3 -2
- package/dist/controllers/spinner_controller.js +7 -5
- package/dist/controllers/spinner_controller.js.map +1 -1
- package/dist/controllers/submit_once_controller.d.ts +4 -1
- package/dist/controllers/submit_once_controller.js +3 -1
- package/dist/controllers/submit_once_controller.js.map +1 -1
- package/dist/controllers/textarea_autosize_controller.d.ts +30 -10
- package/dist/controllers/textarea_autosize_controller.js +131 -3
- package/dist/controllers/textarea_autosize_controller.js.map +1 -1
- package/dist/index.js +2764 -923
- package/dist/index.js.map +1 -1
- package/dist/inspector/cli.d.ts +8 -1
- package/dist/inspector/cli.js +5 -1
- package/dist/inspector/cli.js.map +1 -1
- package/dist/inspector/cli_bin.js +5 -1
- package/dist/inspector/cli_bin.js.map +1 -1
- package/dist/inspector/examples.json +24 -24
- package/dist/inspector/manifest.json +193 -30
- package/package.json +2 -2
|
@@ -1,5 +1,63 @@
|
|
|
1
1
|
import { Controller } from '@hotwired/stimulus';
|
|
2
2
|
|
|
3
|
+
// src/controllers/currency_input_controller.ts
|
|
4
|
+
|
|
5
|
+
// src/utils/composition_tracker.ts
|
|
6
|
+
var CompositionTracker = class {
|
|
7
|
+
#observedTargets = /* @__PURE__ */ new Set();
|
|
8
|
+
#activeTargets = /* @__PURE__ */ new Set();
|
|
9
|
+
#onStart;
|
|
10
|
+
#onEnd;
|
|
11
|
+
constructor(options = {}) {
|
|
12
|
+
this.#onStart = options.onStart;
|
|
13
|
+
this.#onEnd = options.onEnd;
|
|
14
|
+
}
|
|
15
|
+
/** Starts lifecycle tracking for `target`; repeated calls are idempotent. */
|
|
16
|
+
observe(target) {
|
|
17
|
+
if (this.#observedTargets.has(target)) return;
|
|
18
|
+
target.addEventListener("compositionstart", this.#handleStart);
|
|
19
|
+
target.addEventListener("compositionend", this.#handleEnd);
|
|
20
|
+
this.#observedTargets.add(target);
|
|
21
|
+
}
|
|
22
|
+
/** Stops tracking one target and clears any active composition it owned. */
|
|
23
|
+
unobserve(target) {
|
|
24
|
+
if (!this.#observedTargets.delete(target)) return;
|
|
25
|
+
target.removeEventListener("compositionstart", this.#handleStart);
|
|
26
|
+
target.removeEventListener("compositionend", this.#handleEnd);
|
|
27
|
+
this.#activeTargets.delete(target);
|
|
28
|
+
}
|
|
29
|
+
/** Releases every listener and clears state so reconnect starts cleanly. */
|
|
30
|
+
disconnect() {
|
|
31
|
+
for (const target of this.#observedTargets) {
|
|
32
|
+
target.removeEventListener("compositionstart", this.#handleStart);
|
|
33
|
+
target.removeEventListener("compositionend", this.#handleEnd);
|
|
34
|
+
}
|
|
35
|
+
this.#observedTargets.clear();
|
|
36
|
+
this.#activeTargets.clear();
|
|
37
|
+
}
|
|
38
|
+
/** True when lifecycle tracking or the current event reports composition. */
|
|
39
|
+
isComposing(event) {
|
|
40
|
+
return this.#activeTargets.size > 0 || event?.isComposing === true;
|
|
41
|
+
}
|
|
42
|
+
#handleStart = (event) => {
|
|
43
|
+
if (event.currentTarget) this.#activeTargets.add(event.currentTarget);
|
|
44
|
+
this.#onStart?.(event);
|
|
45
|
+
};
|
|
46
|
+
#handleEnd = (event) => {
|
|
47
|
+
if (event.currentTarget) this.#activeTargets.delete(event.currentTarget);
|
|
48
|
+
this.#onEnd?.(event);
|
|
49
|
+
};
|
|
50
|
+
};
|
|
51
|
+
|
|
52
|
+
// src/utils/half_width.ts
|
|
53
|
+
var FULL_WIDTH_SHIFT = 65248;
|
|
54
|
+
function halfWidthChar(char) {
|
|
55
|
+
if (char >= "\uFF01" && char <= "\uFF5E") {
|
|
56
|
+
return String.fromCharCode(char.charCodeAt(0) - FULL_WIDTH_SHIFT);
|
|
57
|
+
}
|
|
58
|
+
return char === "\u3000" ? " " : char;
|
|
59
|
+
}
|
|
60
|
+
|
|
3
61
|
// src/controllers/currency_input_controller.ts
|
|
4
62
|
var CurrencyInputController = class extends Controller {
|
|
5
63
|
static targets = ["display", "field", "srValue"];
|
|
@@ -12,19 +70,92 @@ var CurrencyInputController = class extends Controller {
|
|
|
12
70
|
static events = ["change"];
|
|
13
71
|
/** Last committed numeric value, to suppress duplicate `change` dispatches. */
|
|
14
72
|
#lastValue = null;
|
|
73
|
+
#started = false;
|
|
74
|
+
/** Validated mirrors of the Values; the hot path never reads a raw Value. */
|
|
75
|
+
#locale = "en-US";
|
|
76
|
+
#currency = "";
|
|
77
|
+
#precision = 2;
|
|
78
|
+
/** Formatters rebuilt only when a Value changes, never per keystroke. */
|
|
79
|
+
#grouping;
|
|
80
|
+
#fixed;
|
|
81
|
+
#accessible;
|
|
82
|
+
#group = ",";
|
|
83
|
+
#decimal = ".";
|
|
84
|
+
/** The locale's non-Latin digits mapped back to ASCII (empty for Latin locales). */
|
|
85
|
+
#digits = /* @__PURE__ */ new Map();
|
|
86
|
+
/** Holds mid-composition input so the IME's uncommitted text is never rewritten. */
|
|
87
|
+
#composition = new CompositionTracker({
|
|
88
|
+
onEnd: () => this.#reformat(false)
|
|
89
|
+
});
|
|
90
|
+
/** Re-validates on declaration changes and re-renders the committed display. */
|
|
91
|
+
localeValueChanged() {
|
|
92
|
+
this.#applyValueChange();
|
|
93
|
+
}
|
|
94
|
+
currencyValueChanged() {
|
|
95
|
+
this.#applyValueChange();
|
|
96
|
+
}
|
|
97
|
+
precisionValueChanged() {
|
|
98
|
+
this.#applyValueChange();
|
|
99
|
+
}
|
|
100
|
+
/**
|
|
101
|
+
* Scans the display under the *outgoing* configuration (its separators wrote
|
|
102
|
+
* that text), then revalidates and re-renders under the new one — so a locale
|
|
103
|
+
* switch re-interprets the value, never the old text with new separators.
|
|
104
|
+
*/
|
|
105
|
+
#applyValueChange() {
|
|
106
|
+
const scan = this.#started && this.hasDisplayTarget ? this.#scan(this.displayTarget.value) : null;
|
|
107
|
+
this.#revalidate();
|
|
108
|
+
if (!scan || !this.hasDisplayTarget) return;
|
|
109
|
+
if (document.activeElement === this.displayTarget) {
|
|
110
|
+
const formatted = this.#render(scan.parts);
|
|
111
|
+
this.displayTarget.value = formatted;
|
|
112
|
+
this.#reflect(scan.value, formatted);
|
|
113
|
+
} else if (scan.value === null) {
|
|
114
|
+
this.displayTarget.value = "";
|
|
115
|
+
this.#reflect(null, "");
|
|
116
|
+
} else {
|
|
117
|
+
const rounded = round(scan.value, this.#precision);
|
|
118
|
+
const formatted = this.#fixed.format(rounded);
|
|
119
|
+
this.displayTarget.value = formatted;
|
|
120
|
+
this.#reflect(rounded, formatted);
|
|
121
|
+
}
|
|
122
|
+
}
|
|
15
123
|
/** Normalizes any pre-filled display value to its fixed-precision form. */
|
|
16
124
|
connect() {
|
|
125
|
+
this.#started = true;
|
|
17
126
|
if (!this.hasDisplayTarget) return;
|
|
18
|
-
const
|
|
19
|
-
this.#lastValue =
|
|
20
|
-
if (
|
|
21
|
-
this
|
|
22
|
-
} else {
|
|
127
|
+
const { value } = this.#scan(this.displayTarget.value);
|
|
128
|
+
this.#lastValue = value === null ? null : round(value, this.#precision);
|
|
129
|
+
if (value === null) {
|
|
130
|
+
this.displayTarget.value = "";
|
|
23
131
|
this.#reflect(null, "");
|
|
132
|
+
} else {
|
|
133
|
+
this.#reformat(true);
|
|
24
134
|
}
|
|
25
135
|
}
|
|
136
|
+
disconnect() {
|
|
137
|
+
this.#started = false;
|
|
138
|
+
this.#composition.disconnect();
|
|
139
|
+
}
|
|
140
|
+
/** Tracks composition on an arriving (or swapped-in) display and normalizes it. */
|
|
141
|
+
displayTargetConnected(target) {
|
|
142
|
+
this.#composition.observe(target);
|
|
143
|
+
if (this.#started) this.#reformat(true);
|
|
144
|
+
}
|
|
145
|
+
displayTargetDisconnected(target) {
|
|
146
|
+
this.#composition.unobserve(target);
|
|
147
|
+
}
|
|
148
|
+
/** Syncs a late-arriving hidden field without touching the display or events. */
|
|
149
|
+
fieldTargetConnected() {
|
|
150
|
+
if (this.#started) this.#resync();
|
|
151
|
+
}
|
|
152
|
+
/** Syncs a late-arriving screen-reader span the same way. */
|
|
153
|
+
srValueTargetConnected() {
|
|
154
|
+
if (this.#started) this.#resync();
|
|
155
|
+
}
|
|
26
156
|
/** Re-groups digits as the user types, preserving the caret position. */
|
|
27
|
-
onInput() {
|
|
157
|
+
onInput(event) {
|
|
158
|
+
if (this.#composition.isComposing(event)) return;
|
|
28
159
|
this.#reformat(false);
|
|
29
160
|
}
|
|
30
161
|
/** Applies the fixed-precision rounding on blur. */
|
|
@@ -33,112 +164,212 @@ var CurrencyInputController = class extends Controller {
|
|
|
33
164
|
}
|
|
34
165
|
/**
|
|
35
166
|
* Parses the display value, rewrites it grouped (optionally at fixed
|
|
36
|
-
* precision), keeps the caret stable by
|
|
37
|
-
* the screen-reader span, and the `change` event.
|
|
167
|
+
* precision), keeps the caret stable by significant characters, and syncs the
|
|
168
|
+
* field, the screen-reader span, and the `change` event.
|
|
169
|
+
*
|
|
170
|
+
* @stimeoRenderRoot
|
|
38
171
|
*/
|
|
39
172
|
#reformat(fixedPrecision) {
|
|
40
173
|
if (!this.hasDisplayTarget) return;
|
|
41
174
|
const raw = this.displayTarget.value;
|
|
42
|
-
const
|
|
43
|
-
if (
|
|
44
|
-
|
|
45
|
-
|
|
175
|
+
const { parts, value } = this.#scan(raw);
|
|
176
|
+
if (fixedPrecision) {
|
|
177
|
+
if (value === null) {
|
|
178
|
+
this.displayTarget.value = "";
|
|
179
|
+
this.#reflect(null, "");
|
|
180
|
+
return;
|
|
181
|
+
}
|
|
182
|
+
const rounded = round(value, this.#precision);
|
|
183
|
+
const formatted2 = this.#fixed.format(rounded);
|
|
184
|
+
this.displayTarget.value = formatted2;
|
|
185
|
+
this.#reflect(rounded, formatted2);
|
|
46
186
|
return;
|
|
47
187
|
}
|
|
48
|
-
const
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
188
|
+
const formatted = this.#render(parts);
|
|
189
|
+
if (formatted !== raw) {
|
|
190
|
+
const caret = this.displayTarget.selectionStart;
|
|
191
|
+
const anchor = caret === null ? null : this.#significantBefore(raw, caret);
|
|
192
|
+
this.displayTarget.value = formatted;
|
|
193
|
+
if (anchor !== null) this.#restoreCaret(formatted, anchor);
|
|
194
|
+
}
|
|
195
|
+
this.#reflect(value, value === null ? "" : formatted);
|
|
196
|
+
}
|
|
197
|
+
/**
|
|
198
|
+
* The in-progress rendering: grouped integer, sign and fraction as typed.
|
|
199
|
+
*
|
|
200
|
+
* @stimeoRenderRoot
|
|
201
|
+
*/
|
|
202
|
+
#render(parts) {
|
|
203
|
+
const int = parts.int === "" ? "" : this.#grouping.format(BigInt(parts.int));
|
|
204
|
+
const frac = parts.hasDot ? this.#decimal + parts.frac : "";
|
|
205
|
+
return parts.sign + int + frac;
|
|
206
|
+
}
|
|
207
|
+
/** Restores the caret to sit just after the n-th significant character. */
|
|
208
|
+
#restoreCaret(formatted, significantBefore) {
|
|
58
209
|
let seen = 0;
|
|
59
210
|
let position = formatted.length;
|
|
60
211
|
for (let i = 0; i < formatted.length; i++) {
|
|
61
|
-
if (seen >=
|
|
212
|
+
if (seen >= significantBefore) {
|
|
62
213
|
position = i;
|
|
63
214
|
break;
|
|
64
215
|
}
|
|
65
|
-
if (
|
|
216
|
+
if (this.#isSignificant(formatted[i])) seen += 1;
|
|
66
217
|
}
|
|
67
218
|
try {
|
|
68
219
|
this.displayTarget.setSelectionRange(position, position);
|
|
69
220
|
} catch {
|
|
70
221
|
}
|
|
71
222
|
}
|
|
72
|
-
/**
|
|
73
|
-
|
|
223
|
+
/**
|
|
224
|
+
* Counts the characters before `caret` that survive into the rendering,
|
|
225
|
+
* applying the same acceptance rules as {@link #scan} — a rejected keystroke
|
|
226
|
+
* (a mid-string sign, a second decimal mark) must not shift the anchor.
|
|
227
|
+
*/
|
|
228
|
+
#significantBefore(text, caret) {
|
|
229
|
+
let count = 0;
|
|
230
|
+
let sawSign = false;
|
|
231
|
+
let sawDigit = false;
|
|
232
|
+
let sawDot = false;
|
|
233
|
+
for (const ch of this.#normalize(text.slice(0, caret))) {
|
|
234
|
+
if (ch >= "0" && ch <= "9") {
|
|
235
|
+
count += 1;
|
|
236
|
+
sawDigit = true;
|
|
237
|
+
} else if ((ch === "-" || ch === "+") && !sawSign && !sawDigit && !sawDot) {
|
|
238
|
+
count += 1;
|
|
239
|
+
sawSign = true;
|
|
240
|
+
} else if (this.#isDecimalMark(ch) && !sawDot) {
|
|
241
|
+
count += 1;
|
|
242
|
+
sawDot = true;
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
return count;
|
|
246
|
+
}
|
|
247
|
+
/** Digits, signs, and the decimal mark anchor the caret; grouping does not. */
|
|
248
|
+
#isSignificant(ch) {
|
|
249
|
+
if (ch >= "0" && ch <= "9") return true;
|
|
250
|
+
if (ch === "-" || ch === "+") return true;
|
|
251
|
+
return ch === this.#decimal || ch === "." && this.#group !== ".";
|
|
252
|
+
}
|
|
253
|
+
/** Writes the normalized value to the field, the SR span, and the empty hook. */
|
|
254
|
+
#write(value) {
|
|
74
255
|
const isEmpty = value === null;
|
|
75
256
|
if (this.hasFieldTarget) this.fieldTarget.value = isEmpty ? "" : String(value);
|
|
76
257
|
if (this.hasSrValueTarget) {
|
|
77
|
-
this.srValueTarget.textContent = isEmpty ? "" : this.#
|
|
258
|
+
this.srValueTarget.textContent = isEmpty ? "" : this.#accessible.format(value);
|
|
78
259
|
}
|
|
79
260
|
this.element.toggleAttribute("data-stimeo--currency-input-empty", isEmpty);
|
|
261
|
+
}
|
|
262
|
+
/** {@link #write}, then reports a moved value as `change` (`""` rides with `null`). */
|
|
263
|
+
#reflect(value, formatted) {
|
|
264
|
+
this.#write(value);
|
|
80
265
|
if (value !== this.#lastValue) {
|
|
81
266
|
this.#lastValue = value;
|
|
82
|
-
|
|
267
|
+
this.dispatch("change", { detail: { value, formatted } });
|
|
83
268
|
}
|
|
84
269
|
}
|
|
85
|
-
/**
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
270
|
+
/**
|
|
271
|
+
* Scans arbitrary input text into its number-shaped parts and value. Keeps
|
|
272
|
+
* digits, one leading sign, one decimal mark, and the fraction verbatim;
|
|
273
|
+
* everything else (grouping, symbols, words) is dropped. An ASCII "." is only
|
|
274
|
+
* a decimal mark where it is not the locale's grouping separator. Precision
|
|
275
|
+
* plays no part here — rounding belongs to the blur-time format.
|
|
276
|
+
*/
|
|
277
|
+
#scan(text) {
|
|
278
|
+
let sign = "";
|
|
279
|
+
let int = "";
|
|
280
|
+
let hasDot = false;
|
|
281
|
+
let frac = "";
|
|
282
|
+
for (const ch of this.#normalize(text)) {
|
|
283
|
+
if (ch >= "0" && ch <= "9") {
|
|
284
|
+
if (hasDot) frac += ch;
|
|
285
|
+
else int += ch;
|
|
286
|
+
} else if ((ch === "-" || ch === "+") && sign === "" && int === "" && !hasDot) {
|
|
287
|
+
sign = ch;
|
|
288
|
+
} else if (this.#isDecimalMark(ch) && !hasDot) {
|
|
289
|
+
hasDot = true;
|
|
98
290
|
}
|
|
99
291
|
}
|
|
100
|
-
|
|
101
|
-
const value = Number(
|
|
102
|
-
return Number.isFinite(value) ? value : null;
|
|
292
|
+
const parts = { sign, int, hasDot, frac };
|
|
293
|
+
const value = Number(`${sign}${int}.${frac}`);
|
|
294
|
+
return { parts, value: Number.isFinite(value) ? value : null };
|
|
295
|
+
}
|
|
296
|
+
/** Whether `ch` reads as this locale's decimal mark. */
|
|
297
|
+
#isDecimalMark(ch) {
|
|
298
|
+
return ch === this.#decimal || ch === "." && this.#group !== ".";
|
|
103
299
|
}
|
|
104
|
-
/**
|
|
105
|
-
#
|
|
106
|
-
|
|
300
|
+
/** Re-syncs field / srValue / hook from the current display without dispatching. */
|
|
301
|
+
#resync() {
|
|
302
|
+
if (!this.hasDisplayTarget) return;
|
|
303
|
+
this.#write(this.#scan(this.displayTarget.value).value);
|
|
304
|
+
}
|
|
305
|
+
/**
|
|
306
|
+
* Validates the declared Values, falling back to each Value's default when a
|
|
307
|
+
* declaration cannot be interpreted (a malformed locale or currency tag, a
|
|
308
|
+
* precision outside `Intl`'s 0–100 integer range), and rebuilds the cached
|
|
309
|
+
* formatters from the validated set.
|
|
310
|
+
*/
|
|
311
|
+
#revalidate() {
|
|
312
|
+
this.#locale = "en-US";
|
|
313
|
+
try {
|
|
314
|
+
new Intl.NumberFormat(this.localeValue);
|
|
315
|
+
this.#locale = this.localeValue;
|
|
316
|
+
} catch {
|
|
317
|
+
}
|
|
318
|
+
const precision = this.precisionValue;
|
|
319
|
+
this.#precision = Number.isInteger(precision) && precision >= 0 && precision <= 100 ? precision : 2;
|
|
320
|
+
this.#currency = "";
|
|
321
|
+
if (this.currencyValue !== "") {
|
|
322
|
+
try {
|
|
323
|
+
new Intl.NumberFormat(this.#locale, { style: "currency", currency: this.currencyValue });
|
|
324
|
+
this.#currency = this.currencyValue;
|
|
325
|
+
} catch {
|
|
326
|
+
}
|
|
327
|
+
}
|
|
328
|
+
this.#grouping = new Intl.NumberFormat(this.#locale, {
|
|
329
|
+
useGrouping: true,
|
|
330
|
+
maximumFractionDigits: 0
|
|
331
|
+
});
|
|
332
|
+
this.#fixed = new Intl.NumberFormat(this.#locale, {
|
|
107
333
|
useGrouping: true,
|
|
108
|
-
minimumFractionDigits:
|
|
109
|
-
maximumFractionDigits: this
|
|
334
|
+
minimumFractionDigits: this.#precision,
|
|
335
|
+
maximumFractionDigits: this.#precision
|
|
110
336
|
});
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
337
|
+
this.#accessible = this.#currency ? new Intl.NumberFormat(this.#locale, { style: "currency", currency: this.#currency }) : this.#fixed;
|
|
338
|
+
const parts = new Intl.NumberFormat(this.#locale).formatToParts(11111.1);
|
|
339
|
+
this.#group = parts.find((p) => p.type === "group")?.value ?? ",";
|
|
340
|
+
this.#decimal = parts.find((p) => p.type === "decimal")?.value ?? ".";
|
|
341
|
+
this.#digits.clear();
|
|
342
|
+
const digitFormatter = new Intl.NumberFormat(this.#locale, { useGrouping: false });
|
|
343
|
+
for (let i = 0; i <= 9; i++) {
|
|
344
|
+
const digit = digitFormatter.format(i);
|
|
345
|
+
if (digit !== String(i)) this.#digits.set(digit, String(i));
|
|
120
346
|
}
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
347
|
+
}
|
|
348
|
+
/**
|
|
349
|
+
* Maps the locale's own digits to ASCII, folds full-width forms (the digits,
|
|
350
|
+
* signs, and marks an IME confirms as 0-9+-.,) through the shared
|
|
351
|
+
* half-width mapping, and reads U+2212 MINUS SIGN as "-", which some locales'
|
|
352
|
+
* formatted output uses for negatives.
|
|
353
|
+
*/
|
|
354
|
+
#normalize(text) {
|
|
355
|
+
let out = "";
|
|
356
|
+
for (const ch of text) {
|
|
357
|
+
const mapped = this.#digits.get(ch);
|
|
358
|
+
if (mapped !== void 0) {
|
|
359
|
+
out += mapped;
|
|
360
|
+
} else if (ch === "\u2212") {
|
|
361
|
+
out += "-";
|
|
362
|
+
} else {
|
|
363
|
+
out += halfWidthChar(ch);
|
|
364
|
+
}
|
|
365
|
+
}
|
|
366
|
+
return out;
|
|
132
367
|
}
|
|
133
368
|
};
|
|
134
|
-
function countDigits(text) {
|
|
135
|
-
let count = 0;
|
|
136
|
-
for (const ch of text) if (ch >= "0" && ch <= "9") count += 1;
|
|
137
|
-
return count;
|
|
138
|
-
}
|
|
139
369
|
function round(value, precision) {
|
|
140
370
|
const factor = 10 ** Math.max(0, precision);
|
|
141
371
|
const rounded = Math.round(value * factor) / factor;
|
|
372
|
+
if (!Number.isFinite(rounded)) return value;
|
|
142
373
|
return rounded === 0 ? 0 : rounded;
|
|
143
374
|
}
|
|
144
375
|
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../src/controllers/currency_input_controller.ts"],"names":[],"mappings":"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","file":"currency_input_controller.js","sourcesContent":["import { Controller } from \"@hotwired/stimulus\";\n\n/**\n * Headless currency / amount input behavior: groups digits for display while\n * keeping a machine-readable number in a hidden field, and mirrors the\n * normalized value to a visually-hidden span so assistive tech reads the real\n * amount rather than the grouped display string.\n *\n * Markup contract (identifier: `stimeo--currency-input`):\n * <div data-controller=\"stimeo--currency-input\"\n * data-stimeo--currency-input-locale-value=\"en-US\"\n * data-stimeo--currency-input-currency-value=\"USD\"\n * data-stimeo--currency-input-precision-value=\"2\">\n * <input type=\"text\" inputmode=\"decimal\"\n * aria-describedby=\"amt-sr\"\n * data-stimeo--currency-input-target=\"display\"\n * data-action=\"input->stimeo--currency-input#onInput\n * blur->stimeo--currency-input#format\" />\n * <span id=\"amt-sr\" class=\"visually-hidden\"\n * data-stimeo--currency-input-target=\"srValue\"></span>\n * <input type=\"hidden\" data-stimeo--currency-input-target=\"field\" />\n * </div>\n *\n * `change` dispatches `{ value: number, formatted: string }`.\n *\n * @remarks\n * Behavior only — no styling, no validation (range/required belong to the\n * consumer or Form Field). The display field is the sole Tab stop and keeps its\n * native text-editing behavior; this controller never steals focus. Grouping is\n * applied on every input while preserving the caret (counted by digits to its\n * left), and the fixed-precision rounding is applied on `blur`.\n *\n * Honest a11y note: a hidden `<input>` is not exposed to assistive tech, so the\n * normalized value is *also* published as text in the `srValue` span referenced\n * by the display's `aria-describedby` — that, not the hidden field, is what a\n * screen reader announces.\n */\nexport class CurrencyInputController extends Controller<HTMLElement> {\n static override targets = [\"display\", \"field\", \"srValue\"];\n static override values = {\n locale: { type: String, default: \"en-US\" },\n currency: { type: String, default: \"\" },\n precision: { type: Number, default: 2 },\n };\n static actions = [\"format\", \"onInput\"] as const;\n static events = [\"change\"] as const;\n\n declare readonly displayTarget: HTMLInputElement;\n declare readonly fieldTarget: HTMLInputElement;\n declare readonly srValueTarget: HTMLElement;\n declare readonly hasDisplayTarget: boolean;\n declare readonly hasFieldTarget: boolean;\n declare readonly hasSrValueTarget: boolean;\n declare localeValue: string;\n declare currencyValue: string;\n declare precisionValue: number;\n\n /** Last committed numeric value, to suppress duplicate `change` dispatches. */\n #lastValue: number | null = null;\n\n /** Normalizes any pre-filled display value to its fixed-precision form. */\n override connect(): void {\n if (!this.hasDisplayTarget) return;\n // Seed lastValue with the *rounded* initial value so the idempotent\n // connect-time reformat does not dispatch a spurious `change`.\n const parsed = this.#parse(this.displayTarget.value);\n this.#lastValue = parsed === null ? null : round(parsed, this.precisionValue);\n if (this.displayTarget.value.trim() !== \"\") {\n this.#reformat(true);\n } else {\n this.#reflect(null, \"\");\n }\n }\n\n /** Re-groups digits as the user types, preserving the caret position. */\n onInput(): void {\n this.#reformat(false);\n }\n\n /** Applies the fixed-precision rounding on blur. */\n format(): void {\n this.#reformat(true);\n }\n\n /**\n * Parses the display value, rewrites it grouped (optionally at fixed\n * precision), keeps the caret stable by digit count, and syncs the field,\n * the screen-reader span, and the `change` event.\n */\n #reformat(fixedPrecision: boolean): void {\n if (!this.hasDisplayTarget) return;\n const raw = this.displayTarget.value;\n const number = this.#parse(raw);\n\n if (number === null) {\n this.displayTarget.value = \"\";\n this.#reflect(null, \"\");\n return;\n }\n\n const value = fixedPrecision ? round(number, this.precisionValue) : number;\n const caret = this.displayTarget.selectionStart;\n const digitsBeforeCaret = typeof caret === \"number\" ? countDigits(raw.slice(0, caret)) : null;\n\n const formatted = this.#formatNumber(value, fixedPrecision);\n this.displayTarget.value = formatted;\n if (digitsBeforeCaret !== null) this.#restoreCaret(formatted, digitsBeforeCaret);\n\n this.#reflect(value, formatted);\n }\n\n /** Restores the caret to sit just after the n-th digit of the new string. */\n #restoreCaret(formatted: string, digitsBefore: number): void {\n let seen = 0;\n let position = formatted.length;\n for (let i = 0; i < formatted.length; i++) {\n if (seen >= digitsBefore) {\n position = i;\n break;\n }\n if (/\\d/.test(formatted[i] as string)) seen += 1;\n }\n try {\n this.displayTarget.setSelectionRange(position, position);\n } catch {\n // Some hosts disallow selection on certain input states; the value is\n // already correct, so a failed caret restore is non-fatal.\n }\n }\n\n /** Writes the normalized value to the field, the SR span, and `change`. */\n #reflect(value: number | null, formatted: string): void {\n const isEmpty = value === null;\n if (this.hasFieldTarget) this.fieldTarget.value = isEmpty ? \"\" : String(value);\n if (this.hasSrValueTarget) {\n this.srValueTarget.textContent = isEmpty ? \"\" : this.#accessibleText(value);\n }\n this.element.toggleAttribute(\"data-stimeo--currency-input-empty\", isEmpty);\n\n if (value !== this.#lastValue) {\n this.#lastValue = value;\n if (!isEmpty) this.dispatch(\"change\", { detail: { value, formatted } });\n }\n }\n\n /** Parses arbitrary input text into a finite number, or `null` when blank. */\n #parse(text: string): number | null {\n if (text.trim() === \"\") return null;\n const { decimal } = this.#separators();\n // Keep digits, a leading sign, and decimal marks; normalize the locale's\n // decimal separator to \".\" and drop everything else (grouping, symbols).\n let cleaned = \"\";\n let sawDot = false;\n for (let i = 0; i < text.length; i++) {\n const ch = text[i] as string;\n if (ch >= \"0\" && ch <= \"9\") cleaned += ch;\n else if ((ch === \"-\" || ch === \"+\") && cleaned === \"\") cleaned += ch;\n else if ((ch === decimal || ch === \".\") && !sawDot) {\n cleaned += \".\";\n sawDot = true;\n }\n }\n if (cleaned === \"\" || cleaned === \"-\" || cleaned === \"+\" || cleaned === \".\") return null;\n const value = Number(cleaned);\n return Number.isFinite(value) ? value : null;\n }\n\n /** Formats a number with grouping for the display field. */\n #formatNumber(value: number, fixedPrecision: boolean): string {\n const formatter = new Intl.NumberFormat(this.localeValue, {\n useGrouping: true,\n minimumFractionDigits: fixedPrecision ? this.precisionValue : 0,\n maximumFractionDigits: this.precisionValue,\n });\n return formatter.format(value);\n }\n\n /** The text announced to assistive tech (currency-aware when configured). */\n #accessibleText(value: number): string {\n if (this.currencyValue) {\n return new Intl.NumberFormat(this.localeValue, {\n style: \"currency\",\n currency: this.currencyValue,\n }).format(value);\n }\n return new Intl.NumberFormat(this.localeValue, {\n minimumFractionDigits: this.precisionValue,\n maximumFractionDigits: this.precisionValue,\n }).format(value);\n }\n\n /** Resolves the locale's grouping and decimal separator characters. */\n #separators(): { group: string; decimal: string } {\n const parts = new Intl.NumberFormat(this.localeValue).formatToParts(11111.1);\n const group = parts.find((p) => p.type === \"group\")?.value ?? \",\";\n const decimal = parts.find((p) => p.type === \"decimal\")?.value ?? \".\";\n return { group, decimal };\n }\n}\n\n/** Counts the digit characters in a string. */\nfunction countDigits(text: string): number {\n let count = 0;\n for (const ch of text) if (ch >= \"0\" && ch <= \"9\") count += 1;\n return count;\n}\n\n/** Rounds `value` to `precision` decimal places, avoiding `-0`. */\nfunction round(value: number, precision: number): number {\n const factor = 10 ** Math.max(0, precision);\n const rounded = Math.round(value * factor) / factor;\n return rounded === 0 ? 0 : rounded;\n}\n"]}
|
|
1
|
+
{"version":3,"sources":["../../src/utils/composition_tracker.ts","../../src/utils/half_width.ts","../../src/controllers/currency_input_controller.ts"],"names":["formatted"],"mappings":"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","file":"currency_input_controller.js","sourcesContent":["/** Minimal event shape carrying the platform's per-event composition signal. */\nexport interface CompositionSignal {\n readonly isComposing?: boolean;\n}\n\n/** Hooks that keep component-specific work outside {@link CompositionTracker}. */\nexport interface CompositionTrackerOptions {\n /** Runs after lifecycle state is set for a `compositionstart` event. */\n readonly onStart?: (event: Event) => void;\n /** Runs after lifecycle state is cleared for a `compositionend` event. */\n readonly onEnd?: (event: Event) => void;\n}\n\n/**\n * Owns IME composition listeners and transient state for one or more event targets.\n *\n * Some browsers omit `KeyboardEvent.isComposing` on the keydown that confirms a\n * conversion. Tracking `compositionstart` through `compositionend` supplies the\n * missing lifecycle signal without relying on deprecated numeric key codes.\n * Component policy stays with the consumer: filtering, validation, and submission\n * belong in the optional {@link CompositionTrackerOptions.onEnd} callback.\n *\n * @example\n * ```ts\n * #composition = new CompositionTracker({ onEnd: () => this.filter() });\n *\n * connect(): void {\n * this.#composition.observe(this.inputTarget);\n * }\n *\n * disconnect(): void {\n * this.#composition.disconnect();\n * }\n *\n * onKeydown(event: KeyboardEvent): void {\n * if (this.#composition.isComposing(event)) return;\n * }\n * ```\n */\nexport class CompositionTracker {\n readonly #observedTargets = new Set<EventTarget>();\n readonly #activeTargets = new Set<EventTarget>();\n readonly #onStart: ((event: Event) => void) | undefined;\n readonly #onEnd: ((event: Event) => void) | undefined;\n\n constructor(options: CompositionTrackerOptions = {}) {\n this.#onStart = options.onStart;\n this.#onEnd = options.onEnd;\n }\n\n /** Starts lifecycle tracking for `target`; repeated calls are idempotent. */\n observe(target: EventTarget): void {\n if (this.#observedTargets.has(target)) return;\n target.addEventListener(\"compositionstart\", this.#handleStart);\n target.addEventListener(\"compositionend\", this.#handleEnd);\n this.#observedTargets.add(target);\n }\n\n /** Stops tracking one target and clears any active composition it owned. */\n unobserve(target: EventTarget): void {\n if (!this.#observedTargets.delete(target)) return;\n target.removeEventListener(\"compositionstart\", this.#handleStart);\n target.removeEventListener(\"compositionend\", this.#handleEnd);\n this.#activeTargets.delete(target);\n }\n\n /** Releases every listener and clears state so reconnect starts cleanly. */\n disconnect(): void {\n for (const target of this.#observedTargets) {\n target.removeEventListener(\"compositionstart\", this.#handleStart);\n target.removeEventListener(\"compositionend\", this.#handleEnd);\n }\n this.#observedTargets.clear();\n this.#activeTargets.clear();\n }\n\n /** True when lifecycle tracking or the current event reports composition. */\n isComposing(event?: CompositionSignal): boolean {\n return this.#activeTargets.size > 0 || event?.isComposing === true;\n }\n\n readonly #handleStart = (event: Event): void => {\n if (event.currentTarget) this.#activeTargets.add(event.currentTarget);\n this.#onStart?.(event);\n };\n\n readonly #handleEnd = (event: Event): void => {\n if (event.currentTarget) this.#activeTargets.delete(event.currentTarget);\n this.#onEnd?.(event);\n };\n}\n","/** The distance between a full-width ASCII code point and its half-width twin. */\nconst FULL_WIDTH_SHIFT = 0xfee0;\n\n/**\n * The half-width form of `char`, or `char` itself when it has none.\n *\n * An IME confirming in full-width mode produces U+FF01–U+FF5E (and U+3000 for\n * the space) where the page means ASCII — the digits of a phone number, the\n * letters of a code, the separators between them. Reading those as their ASCII\n * equivalents keeps confirmed text usable instead of discarding it. Characters\n * outside that block, including kana and astral code points, are returned\n * unchanged, so this never rewrites text a consumer meant to keep as typed.\n */\nexport function halfWidthChar(char: string): string {\n if (char >= \"!\" && char <= \"~\") {\n return String.fromCharCode(char.charCodeAt(0) - FULL_WIDTH_SHIFT);\n }\n return char === \" \" ? \" \" : char;\n}\n\n/** Every full-width ASCII character in `text` rewritten by {@link halfWidthChar}. */\nexport function toHalfWidth(text: string): string {\n let out = \"\";\n for (const char of text) out += halfWidthChar(char);\n return out;\n}\n","import { Controller } from \"@hotwired/stimulus\";\nimport { CompositionTracker } from \"../utils/composition_tracker\";\nimport { halfWidthChar } from \"../utils/half_width\";\n\n/** The number-shaped pieces of an in-progress entry, in typing order. */\ninterface EntryParts {\n /** The leading sign exactly as typed (`\"\"` when none). */\n sign: \"\" | \"-\" | \"+\";\n /** Integer digits with grouping stripped (may be empty mid-entry). */\n int: string;\n /** Whether a decimal mark was typed. */\n hasDot: boolean;\n /** Fraction digits exactly as typed; rounding happens only on blur. */\n frac: string;\n}\n\n/** One scan of the display text: its structure and the numeric value it holds. */\ninterface Scan {\n parts: EntryParts;\n /** The finite value, or `null` while no digit has been typed yet. */\n value: number | null;\n}\n\n/**\n * Headless currency / amount input behavior: groups digits for display while\n * keeping a machine-readable number in a hidden field, and mirrors the\n * normalized value to a visually-hidden span so assistive tech reads the real\n * amount rather than the grouped display string.\n *\n * Markup contract (identifier: `stimeo--currency-input`):\n * <div data-controller=\"stimeo--currency-input\"\n * data-stimeo--currency-input-locale-value=\"en-US\"\n * data-stimeo--currency-input-currency-value=\"USD\"\n * data-stimeo--currency-input-precision-value=\"2\">\n * <input type=\"text\" inputmode=\"decimal\"\n * aria-describedby=\"amt-sr\"\n * data-stimeo--currency-input-target=\"display\"\n * data-action=\"input->stimeo--currency-input#onInput\n * blur->stimeo--currency-input#format\" />\n * <span id=\"amt-sr\" class=\"visually-hidden\"\n * data-stimeo--currency-input-target=\"srValue\"></span>\n * <input type=\"hidden\" data-stimeo--currency-input-target=\"field\" />\n * </div>\n *\n * `change` dispatches `{ value: number | null, formatted: string }` — `null`\n * (with an empty `formatted`) when the amount is cleared, so consumers hear\n * every transition of the numeric value, including back to empty.\n *\n * @remarks\n * Behavior only — no styling, no validation (range/required belong to the\n * consumer or Form Field). The display field is the sole Tab stop and keeps its\n * native text-editing behavior; this controller never steals focus.\n *\n * While typing, the entry is preserved as typed: grouping is applied to the\n * integer digits per locale, a leading sign and a trailing decimal mark stay in\n * place, fraction digits are kept verbatim, and the caret is restored after the\n * same significant character it followed. Negative amounts are in scope. The\n * fixed-precision rounding is applied on `blur`, never mid-entry.\n * Full-width digits, signs, and marks (an IME confirming 0-9/./-) are\n * normalized to their ASCII forms rather than stripped; the locale's own\n * digits (a non-Latin default numbering system such as ar-EG's) are mapped\n * back the same way; and U+2212 MINUS SIGN is read as a sign. An ASCII \".\"\n * only counts as a decimal mark in locales where it is not the grouping\n * separator. Together these keep the controller's own output re-parseable to\n * the same value in every locale. Events fired mid-IME-composition are\n * ignored; the confirmed text is formatted once on `compositionend`.\n *\n * A malformed `locale`, `currency`, or `precision` declaration falls back to\n * that Value's default instead of throwing: each is validated once in its\n * `<name>ValueChanged`, and the hot path only ever sees validated values\n * through cached `Intl.NumberFormat` instances (rebuilt on Value changes, never\n * per keystroke). Late-arriving or swapped `field` / `srValue` / `display`\n * targets are re-synced on connection. The composition listeners are released\n * on `disconnect()`.\n *\n * Honest a11y note: a hidden `<input>` is not exposed to assistive tech, so the\n * normalized value is *also* published as text in the `srValue` span referenced\n * by the display's `aria-describedby` — that, not the hidden field, is what a\n * screen reader announces.\n */\nexport class CurrencyInputController extends Controller<HTMLElement> {\n static override targets = [\"display\", \"field\", \"srValue\"];\n static override values = {\n locale: { type: String, default: \"en-US\" },\n currency: { type: String, default: \"\" },\n precision: { type: Number, default: 2 },\n };\n static actions = [\"format\", \"onInput\"] as const;\n static events = [\"change\"] as const;\n\n declare readonly displayTarget: HTMLInputElement;\n declare readonly fieldTarget: HTMLInputElement;\n declare readonly srValueTarget: HTMLElement;\n declare readonly hasDisplayTarget: boolean;\n declare readonly hasFieldTarget: boolean;\n declare readonly hasSrValueTarget: boolean;\n declare localeValue: string;\n declare currencyValue: string;\n declare precisionValue: number;\n\n /** Last committed numeric value, to suppress duplicate `change` dispatches. */\n #lastValue: number | null = null;\n #started = false;\n\n /** Validated mirrors of the Values; the hot path never reads a raw Value. */\n #locale = \"en-US\";\n #currency = \"\";\n #precision = 2;\n\n /** Formatters rebuilt only when a Value changes, never per keystroke. */\n #grouping!: Intl.NumberFormat;\n #fixed!: Intl.NumberFormat;\n #accessible!: Intl.NumberFormat;\n #group = \",\";\n #decimal = \".\";\n /** The locale's non-Latin digits mapped back to ASCII (empty for Latin locales). */\n readonly #digits = new Map<string, string>();\n\n /** Holds mid-composition input so the IME's uncommitted text is never rewritten. */\n readonly #composition = new CompositionTracker({\n onEnd: () => this.#reformat(false),\n });\n\n /** Re-validates on declaration changes and re-renders the committed display. */\n localeValueChanged(): void {\n this.#applyValueChange();\n }\n\n currencyValueChanged(): void {\n this.#applyValueChange();\n }\n\n precisionValueChanged(): void {\n this.#applyValueChange();\n }\n\n /**\n * Scans the display under the *outgoing* configuration (its separators wrote\n * that text), then revalidates and re-renders under the new one — so a locale\n * switch re-interprets the value, never the old text with new separators.\n */\n #applyValueChange(): void {\n const scan =\n this.#started && this.hasDisplayTarget ? this.#scan(this.displayTarget.value) : null;\n this.#revalidate();\n if (!scan || !this.hasDisplayTarget) return;\n if (document.activeElement === this.displayTarget) {\n const formatted = this.#render(scan.parts);\n this.displayTarget.value = formatted;\n this.#reflect(scan.value, formatted);\n } else if (scan.value === null) {\n this.displayTarget.value = \"\";\n this.#reflect(null, \"\");\n } else {\n const rounded = round(scan.value, this.#precision);\n const formatted = this.#fixed.format(rounded);\n this.displayTarget.value = formatted;\n this.#reflect(rounded, formatted);\n }\n }\n\n /** Normalizes any pre-filled display value to its fixed-precision form. */\n override connect(): void {\n this.#started = true;\n if (!this.hasDisplayTarget) return;\n // Seed lastValue with the *rounded* initial value so the idempotent\n // connect-time reformat does not dispatch a spurious `change`.\n const { value } = this.#scan(this.displayTarget.value);\n this.#lastValue = value === null ? null : round(value, this.#precision);\n if (value === null) {\n this.displayTarget.value = \"\";\n this.#reflect(null, \"\");\n } else {\n this.#reformat(true);\n }\n }\n\n override disconnect(): void {\n this.#started = false;\n this.#composition.disconnect();\n }\n\n /** Tracks composition on an arriving (or swapped-in) display and normalizes it. */\n displayTargetConnected(target: HTMLInputElement): void {\n this.#composition.observe(target);\n if (this.#started) this.#reformat(true);\n }\n\n displayTargetDisconnected(target: HTMLInputElement): void {\n this.#composition.unobserve(target);\n }\n\n /** Syncs a late-arriving hidden field without touching the display or events. */\n fieldTargetConnected(): void {\n if (this.#started) this.#resync();\n }\n\n /** Syncs a late-arriving screen-reader span the same way. */\n srValueTargetConnected(): void {\n if (this.#started) this.#resync();\n }\n\n /** Re-groups digits as the user types, preserving the caret position. */\n onInput(event: Event): void {\n if (this.#composition.isComposing(event as InputEvent)) return;\n this.#reformat(false);\n }\n\n /** Applies the fixed-precision rounding on blur. */\n format(): void {\n this.#reformat(true);\n }\n\n /**\n * Parses the display value, rewrites it grouped (optionally at fixed\n * precision), keeps the caret stable by significant characters, and syncs the\n * field, the screen-reader span, and the `change` event.\n *\n * @stimeoRenderRoot\n */\n #reformat(fixedPrecision: boolean): void {\n if (!this.hasDisplayTarget) return;\n const raw = this.displayTarget.value;\n const { parts, value } = this.#scan(raw);\n\n if (fixedPrecision) {\n if (value === null) {\n // Blur with no digits (an abandoned sign or dot) clears the entry.\n this.displayTarget.value = \"\";\n this.#reflect(null, \"\");\n return;\n }\n const rounded = round(value, this.#precision);\n const formatted = this.#fixed.format(rounded);\n this.displayTarget.value = formatted;\n this.#reflect(rounded, formatted);\n return;\n }\n\n const formatted = this.#render(parts);\n if (formatted !== raw) {\n const caret = this.displayTarget.selectionStart;\n const anchor = caret === null ? null : this.#significantBefore(raw, caret);\n this.displayTarget.value = formatted;\n if (anchor !== null) this.#restoreCaret(formatted, anchor);\n }\n // The display may hold an in-progress \"-\", but a null value always rides\n // with an empty `formatted` so consumers can treat the pair as \"cleared\".\n this.#reflect(value, value === null ? \"\" : formatted);\n }\n\n /**\n * The in-progress rendering: grouped integer, sign and fraction as typed.\n *\n * @stimeoRenderRoot\n */\n #render(parts: EntryParts): string {\n const int = parts.int === \"\" ? \"\" : this.#grouping.format(BigInt(parts.int));\n const frac = parts.hasDot ? this.#decimal + parts.frac : \"\";\n return parts.sign + int + frac;\n }\n\n /** Restores the caret to sit just after the n-th significant character. */\n #restoreCaret(formatted: string, significantBefore: number): void {\n let seen = 0;\n let position = formatted.length;\n for (let i = 0; i < formatted.length; i++) {\n if (seen >= significantBefore) {\n position = i;\n break;\n }\n if (this.#isSignificant(formatted[i] as string)) seen += 1;\n }\n try {\n this.displayTarget.setSelectionRange(position, position);\n } catch {\n // Some hosts disallow selection on certain input states; the value is\n // already correct, so a failed caret restore is non-fatal.\n }\n }\n\n /**\n * Counts the characters before `caret` that survive into the rendering,\n * applying the same acceptance rules as {@link #scan} — a rejected keystroke\n * (a mid-string sign, a second decimal mark) must not shift the anchor.\n */\n #significantBefore(text: string, caret: number): number {\n let count = 0;\n let sawSign = false;\n let sawDigit = false;\n let sawDot = false;\n for (const ch of this.#normalize(text.slice(0, caret))) {\n if (ch >= \"0\" && ch <= \"9\") {\n count += 1;\n sawDigit = true;\n } else if ((ch === \"-\" || ch === \"+\") && !sawSign && !sawDigit && !sawDot) {\n count += 1;\n sawSign = true;\n } else if (this.#isDecimalMark(ch) && !sawDot) {\n count += 1;\n sawDot = true;\n }\n }\n return count;\n }\n\n /** Digits, signs, and the decimal mark anchor the caret; grouping does not. */\n #isSignificant(ch: string): boolean {\n if (ch >= \"0\" && ch <= \"9\") return true;\n if (ch === \"-\" || ch === \"+\") return true;\n return ch === this.#decimal || (ch === \".\" && this.#group !== \".\");\n }\n\n /** Writes the normalized value to the field, the SR span, and the empty hook. */\n #write(value: number | null): void {\n const isEmpty = value === null;\n if (this.hasFieldTarget) this.fieldTarget.value = isEmpty ? \"\" : String(value);\n if (this.hasSrValueTarget) {\n this.srValueTarget.textContent = isEmpty ? \"\" : this.#accessible.format(value);\n }\n this.element.toggleAttribute(\"data-stimeo--currency-input-empty\", isEmpty);\n }\n\n /** {@link #write}, then reports a moved value as `change` (`\"\"` rides with `null`). */\n #reflect(value: number | null, formatted: string): void {\n this.#write(value);\n if (value !== this.#lastValue) {\n this.#lastValue = value;\n this.dispatch(\"change\", { detail: { value, formatted } });\n }\n }\n\n /**\n * Scans arbitrary input text into its number-shaped parts and value. Keeps\n * digits, one leading sign, one decimal mark, and the fraction verbatim;\n * everything else (grouping, symbols, words) is dropped. An ASCII \".\" is only\n * a decimal mark where it is not the locale's grouping separator. Precision\n * plays no part here — rounding belongs to the blur-time format.\n */\n #scan(text: string): Scan {\n let sign: EntryParts[\"sign\"] = \"\";\n let int = \"\";\n let hasDot = false;\n let frac = \"\";\n for (const ch of this.#normalize(text)) {\n if (ch >= \"0\" && ch <= \"9\") {\n if (hasDot) frac += ch;\n else int += ch;\n } else if ((ch === \"-\" || ch === \"+\") && sign === \"\" && int === \"\" && !hasDot) {\n sign = ch;\n } else if (this.#isDecimalMark(ch) && !hasDot) {\n hasDot = true;\n }\n }\n const parts: EntryParts = { sign, int, hasDot, frac };\n // `Number` accepts \"12.\" and \".5\" alike, so one dotted form covers every\n // shape — and a digit-less entry (\"\", \"-\", \".\") reads as NaN, hence null.\n const value = Number(`${sign}${int}.${frac}`);\n return { parts, value: Number.isFinite(value) ? value : null };\n }\n\n /** Whether `ch` reads as this locale's decimal mark. */\n #isDecimalMark(ch: string): boolean {\n return ch === this.#decimal || (ch === \".\" && this.#group !== \".\");\n }\n\n /** Re-syncs field / srValue / hook from the current display without dispatching. */\n #resync(): void {\n if (!this.hasDisplayTarget) return;\n this.#write(this.#scan(this.displayTarget.value).value);\n }\n\n /**\n * Validates the declared Values, falling back to each Value's default when a\n * declaration cannot be interpreted (a malformed locale or currency tag, a\n * precision outside `Intl`'s 0–100 integer range), and rebuilds the cached\n * formatters from the validated set.\n */\n #revalidate(): void {\n this.#locale = \"en-US\";\n try {\n new Intl.NumberFormat(this.localeValue);\n this.#locale = this.localeValue;\n } catch {\n // Fall through to the default locale.\n }\n\n const precision = this.precisionValue;\n this.#precision =\n Number.isInteger(precision) && precision >= 0 && precision <= 100 ? precision : 2;\n\n this.#currency = \"\";\n if (this.currencyValue !== \"\") {\n try {\n new Intl.NumberFormat(this.#locale, { style: \"currency\", currency: this.currencyValue });\n this.#currency = this.currencyValue;\n } catch {\n // Fall through to the plain-number accessible text.\n }\n }\n\n this.#grouping = new Intl.NumberFormat(this.#locale, {\n useGrouping: true,\n maximumFractionDigits: 0,\n });\n this.#fixed = new Intl.NumberFormat(this.#locale, {\n useGrouping: true,\n minimumFractionDigits: this.#precision,\n maximumFractionDigits: this.#precision,\n });\n this.#accessible = this.#currency\n ? new Intl.NumberFormat(this.#locale, { style: \"currency\", currency: this.#currency })\n : this.#fixed;\n\n const parts = new Intl.NumberFormat(this.#locale).formatToParts(11111.1);\n this.#group = parts.find((p) => p.type === \"group\")?.value ?? \",\";\n this.#decimal = parts.find((p) => p.type === \"decimal\")?.value ?? \".\";\n\n // Locales whose default numbering system is not Latin (ar-EG, fa-IR, …)\n // format with their own digits; mapping them back keeps the controller's\n // own output re-parseable to the same value.\n this.#digits.clear();\n const digitFormatter = new Intl.NumberFormat(this.#locale, { useGrouping: false });\n for (let i = 0; i <= 9; i++) {\n const digit = digitFormatter.format(i);\n if (digit !== String(i)) this.#digits.set(digit, String(i));\n }\n }\n\n /**\n * Maps the locale's own digits to ASCII, folds full-width forms (the digits,\n * signs, and marks an IME confirms as 0-9+-.,) through the shared\n * half-width mapping, and reads U+2212 MINUS SIGN as \"-\", which some locales'\n * formatted output uses for negatives.\n */\n #normalize(text: string): string {\n let out = \"\";\n for (const ch of text) {\n const mapped = this.#digits.get(ch);\n if (mapped !== undefined) {\n out += mapped;\n } else if (ch === \"−\") {\n out += \"-\";\n } else {\n out += halfWidthChar(ch);\n }\n }\n return out;\n }\n}\n\n/** Rounds `value` to `precision` decimal places, avoiding `-0`; overflow keeps `value`. */\nfunction round(value: number, precision: number): number {\n const factor = 10 ** Math.max(0, precision);\n const rounded = Math.round(value * factor) / factor;\n if (!Number.isFinite(rounded)) return value;\n return rounded === 0 ? 0 : rounded;\n}\n"]}
|
|
@@ -32,13 +32,27 @@ import { Controller } from '@hotwired/stimulus';
|
|
|
32
32
|
* Behavior only — the consumer performs the actual data sort/render in response to
|
|
33
33
|
* the `sort` event and owns all styling. No timers or observers are held, so there
|
|
34
34
|
* is nothing to leak across Turbo navigations; `connect()` rebuilds the single tab
|
|
35
|
-
* stop idempotently from the DOM
|
|
35
|
+
* stop idempotently from the DOM, and the target callbacks rebuild it again after
|
|
36
|
+
* rows or cells are added or removed at runtime.
|
|
36
37
|
*
|
|
37
38
|
* Behavior provided:
|
|
38
39
|
* - `Arrow*` move between cells (clamped at edges); `Home`/`End` to the row's
|
|
39
40
|
* first/last cell; `Ctrl+Home`/`Ctrl+End` to the grid's first/last cell.
|
|
40
41
|
* - `Enter`/`Space` cycles a header's sort (`none→ascending→descending`) or toggles
|
|
41
42
|
* the focused row's selection when selection is enabled.
|
|
43
|
+
*
|
|
44
|
+
* Consumer contract — controls nested inside a cell or header:
|
|
45
|
+
* - APG's grid hosts working controls in its cells, so a keystroke or click that
|
|
46
|
+
* reached one — {@link INTERACTIVE_HOST_SELECTOR}, an editable host, or any
|
|
47
|
+
* widget that already called `preventDefault()` — is left to it entirely: no
|
|
48
|
+
* move, no sort, no selection. The one control that hands the event on is a
|
|
49
|
+
* sortable header's own `<button>`, whose activation is what the click carries.
|
|
50
|
+
* - A control keeps its own Tab behavior, so give it `tabindex="-1"` to preserve
|
|
51
|
+
* the grid's single Tab stop. A sortable `columnheader` hosting a `<button>` is
|
|
52
|
+
* the common case: the roving position stays on the header, and the button's
|
|
53
|
+
* own activation reaches `sort` through the click that bubbles to it.
|
|
54
|
+
* - Host `role="gridcell"` / `role="columnheader"` on a non-interactive element
|
|
55
|
+
* (`td`, `th`); an interactive host makes the grid stand down on that cell.
|
|
42
56
|
*/
|
|
43
57
|
declare class DataGridController extends Controller<HTMLElement> {
|
|
44
58
|
#private;
|
|
@@ -65,7 +79,7 @@ declare class DataGridController extends Controller<HTMLElement> {
|
|
|
65
79
|
* attribute, so the Value callback does not fire a second time.
|
|
66
80
|
*/
|
|
67
81
|
connect(): void;
|
|
68
|
-
/**
|
|
82
|
+
/** Closes the reconcile window so a queued pass cannot run against a detached tree. */
|
|
69
83
|
disconnect(): void;
|
|
70
84
|
/**
|
|
71
85
|
* Keeps `aria-multiselectable` in step with the `selection` Value. Fires on connect
|
|
@@ -73,14 +87,16 @@ declare class DataGridController extends Controller<HTMLElement> {
|
|
|
73
87
|
* never drifts from the selection logic, which reads `selectionValue` live.
|
|
74
88
|
*/
|
|
75
89
|
selectionValueChanged(): void;
|
|
76
|
-
/**
|
|
77
|
-
* Re-establishes the row baseline for a row added after connect.
|
|
78
|
-
*
|
|
79
|
-
* Each pass walks every row, and Stimulus reports the authored rows one by one
|
|
80
|
-
* before `connect()`, so the mount is gated to keep it linear in the row count
|
|
81
|
-
* rather than quadratic; `connect()` runs the single baseline pass instead.
|
|
82
|
-
*/
|
|
90
|
+
/** Re-establishes the baselines for a row added after connect. */
|
|
83
91
|
rowTargetConnected(): void;
|
|
92
|
+
/** Re-establishes the tab stop when a cell joins the grid after connect. */
|
|
93
|
+
cellTargetConnected(): void;
|
|
94
|
+
/** Re-establishes the tab stop when a cell leaves the grid. */
|
|
95
|
+
cellTargetDisconnected(): void;
|
|
96
|
+
/** Re-establishes the tab stop when a header joins the grid after connect. */
|
|
97
|
+
columnHeaderTargetConnected(): void;
|
|
98
|
+
/** Re-establishes the tab stop when a header leaves the grid. */
|
|
99
|
+
columnHeaderTargetDisconnected(): void;
|
|
84
100
|
/** Cycles the activated column header's sort and emits `sort`. */
|
|
85
101
|
sort(event: Event): void;
|
|
86
102
|
/** Toggles selection of the row owning the event target. Bound optionally. */
|