stimeo-ui 0.14.0 → 0.16.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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +212 -0
- data/README.md +120 -0
- data/dist/cable/index.js +123 -29
- data/dist/controllers/accordion_controller.js +98 -9
- data/dist/controllers/announcer_controller.js +96 -62
- data/dist/controllers/auto_submit_controller.js +83 -7
- data/dist/controllers/avatar_controller.js +1 -1
- data/dist/controllers/breadcrumb_controller.js +38 -11
- data/dist/controllers/bulk_select_controller.js +7 -6
- data/dist/controllers/calendar_controller.js +340 -123
- data/dist/controllers/carousel_controller.js +263 -38
- data/dist/controllers/character_counter_controller.js +40 -2
- data/dist/controllers/checkbox_controller.js +81 -12
- data/dist/controllers/clipboard_controller.js +63 -5
- data/dist/controllers/collapsible_controller.js +99 -14
- data/dist/controllers/color_picker_controller.js +80 -34
- data/dist/controllers/combobox_controller.js +106 -16
- data/dist/controllers/command_palette_controller.js +35 -3
- data/dist/controllers/conditional_fields_controller.js +85 -17
- data/dist/controllers/confirm_controller.js +3 -0
- data/dist/controllers/context_menu_controller.js +32 -12
- data/dist/controllers/countdown_controller.js +129 -26
- data/dist/controllers/currency_input_controller.js +221 -67
- data/dist/controllers/data_grid_controller.js +195 -29
- data/dist/controllers/date_range_picker_controller.js +151 -30
- data/dist/controllers/dialog_controller.js +35 -8
- data/dist/controllers/direct_upload_controller.js +22 -4
- data/dist/controllers/dirty_form_controller.js +46 -13
- data/dist/controllers/dismissible_controller.js +1 -0
- data/dist/controllers/drawer_controller.js +54 -19
- data/dist/controllers/dropdown_controller.js +36 -9
- data/dist/controllers/editable_controller.js +34 -0
- data/dist/controllers/file_dropzone_controller.js +144 -51
- data/dist/controllers/filter_controller.js +20 -6
- data/dist/controllers/flash_controller.js +432 -71
- data/dist/controllers/focus_controller.js +1 -0
- data/dist/controllers/form_field_controller.js +7 -5
- data/dist/controllers/form_validation_controller.js +19 -13
- data/dist/controllers/frame_loading_controller.js +45 -8
- data/dist/controllers/highlight_controller.js +82 -25
- data/dist/controllers/hover_card_controller.js +40 -14
- data/dist/controllers/idle_controller.js +90 -5
- data/dist/controllers/input_mask_controller.js +65 -9
- data/dist/controllers/intersection_controller.js +3 -0
- data/dist/controllers/lazy_frame_controller.js +11 -2
- data/dist/controllers/listbox_controller.js +203 -45
- data/dist/controllers/local_time_controller.js +10 -5
- data/dist/controllers/masonry_controller.js +31 -15
- data/dist/controllers/menu_controller.js +45 -16
- data/dist/controllers/menubar_controller.js +58 -24
- data/dist/controllers/meter_controller.js +9 -5
- data/dist/controllers/multi_select_controller.js +278 -104
- data/dist/controllers/navigation_menu_controller.js +48 -15
- data/dist/controllers/nested_form_controller.js +37 -8
- data/dist/controllers/network_status_controller.js +9 -1
- data/dist/controllers/number_input_controller.js +124 -21
- data/dist/controllers/optimistic_controller.js +42 -5
- data/dist/controllers/otp_controller.js +198 -55
- data/dist/controllers/overflow_indicator_controller.js +115 -21
- data/dist/controllers/overflow_menu_controller.js +141 -44
- data/dist/controllers/pagination_controller.js +74 -28
- data/dist/controllers/password_reveal_controller.js +59 -2
- data/dist/controllers/persist_controller.js +30 -8
- data/dist/controllers/pointer_drag_controller.js +131 -52
- data/dist/controllers/popover_controller.js +45 -11
- data/dist/controllers/portal_controller.js +6 -2
- data/dist/controllers/preview_guard_controller.js +16 -1
- data/dist/controllers/progress_controller.js +8 -4
- data/dist/controllers/radio_group_controller.js +42 -17
- data/dist/controllers/range_slider_controller.js +88 -42
- data/dist/controllers/rating_controller.js +39 -15
- data/dist/controllers/read_more_controller.js +100 -7
- data/dist/controllers/reading_progress_controller.js +65 -19
- data/dist/controllers/relative_time_controller.js +10 -5
- data/dist/controllers/resizable_controller.js +82 -22
- data/dist/controllers/scroll_area_controller.js +75 -27
- data/dist/controllers/scroll_restore_controller.js +37 -16
- data/dist/controllers/scroll_visibility_controller.js +49 -30
- data/dist/controllers/scrollspy_controller.js +71 -26
- data/dist/controllers/separator_controller.js +66 -37
- data/dist/controllers/sidebar_controller.js +77 -18
- data/dist/controllers/skeleton_controller.js +6 -1
- data/dist/controllers/slider_controller.js +82 -47
- data/dist/controllers/smart_sticky_header_controller.js +60 -26
- data/dist/controllers/sortable_controller.js +17 -2
- data/dist/controllers/spinner_controller.js +10 -2
- data/dist/controllers/step_indicator_controller.js +18 -17
- data/dist/controllers/stepper_controller.js +101 -19
- data/dist/controllers/stick_to_bottom_controller.js +104 -8
- data/dist/controllers/submit_once_controller.js +45 -9
- data/dist/controllers/switch_controller.js +101 -10
- data/dist/controllers/tabs_controller.js +21 -2
- data/dist/controllers/tags_input_controller.js +209 -59
- data/dist/controllers/textarea_autosize_controller.js +29 -3
- data/dist/controllers/theme_controller.js +64 -14
- data/dist/controllers/time_picker_controller.js +23 -8
- data/dist/controllers/toast_controller.js +451 -105
- data/dist/controllers/toggle_group_controller.js +159 -23
- data/dist/controllers/toolbar_controller.js +32 -0
- data/dist/controllers/tooltip_controller.js +39 -13
- data/dist/controllers/transition_controller.js +4 -0
- data/dist/controllers/tree_view_controller.js +169 -16
- data/dist/index.js +5002 -1911
- data/dist/positioning/index.js +2 -0
- data/lib/stimeo/ui/version.rb +1 -1
- metadata +2 -2
|
@@ -3,11 +3,19 @@ import { Controller } from '@hotwired/stimulus';
|
|
|
3
3
|
// src/controllers/currency_input_controller.ts
|
|
4
4
|
|
|
5
5
|
// src/utils/composition_tracker.ts
|
|
6
|
+
var COMPOSITION_INPUT_TYPES = /* @__PURE__ */ new Set([
|
|
7
|
+
"insertCompositionText",
|
|
8
|
+
"insertFromComposition",
|
|
9
|
+
"deleteCompositionText",
|
|
10
|
+
"deleteByComposition"
|
|
11
|
+
]);
|
|
6
12
|
var CompositionTracker = class {
|
|
7
13
|
#observedTargets = /* @__PURE__ */ new Set();
|
|
8
14
|
#activeTargets = /* @__PURE__ */ new Set();
|
|
9
15
|
#onStart;
|
|
10
16
|
#onEnd;
|
|
17
|
+
/** The field whose confirming `input` is still owed, while the window is open. */
|
|
18
|
+
#confirmedTarget = null;
|
|
11
19
|
constructor(options = {}) {
|
|
12
20
|
this.#onStart = options.onStart;
|
|
13
21
|
this.#onEnd = options.onEnd;
|
|
@@ -17,6 +25,7 @@ var CompositionTracker = class {
|
|
|
17
25
|
if (this.#observedTargets.has(target)) return;
|
|
18
26
|
target.addEventListener("compositionstart", this.#handleStart);
|
|
19
27
|
target.addEventListener("compositionend", this.#handleEnd);
|
|
28
|
+
target.addEventListener("keydown", this.#handleKeydown);
|
|
20
29
|
this.#observedTargets.add(target);
|
|
21
30
|
}
|
|
22
31
|
/** Stops tracking one target and clears any active composition it owned. */
|
|
@@ -24,29 +33,52 @@ var CompositionTracker = class {
|
|
|
24
33
|
if (!this.#observedTargets.delete(target)) return;
|
|
25
34
|
target.removeEventListener("compositionstart", this.#handleStart);
|
|
26
35
|
target.removeEventListener("compositionend", this.#handleEnd);
|
|
36
|
+
target.removeEventListener("keydown", this.#handleKeydown);
|
|
27
37
|
this.#activeTargets.delete(target);
|
|
38
|
+
if (this.#confirmedTarget === target) this.#confirmedTarget = null;
|
|
28
39
|
}
|
|
29
40
|
/** Releases every listener and clears state so reconnect starts cleanly. */
|
|
30
41
|
disconnect() {
|
|
31
42
|
for (const target of this.#observedTargets) {
|
|
32
43
|
target.removeEventListener("compositionstart", this.#handleStart);
|
|
33
44
|
target.removeEventListener("compositionend", this.#handleEnd);
|
|
45
|
+
target.removeEventListener("keydown", this.#handleKeydown);
|
|
34
46
|
}
|
|
35
47
|
this.#observedTargets.clear();
|
|
36
48
|
this.#activeTargets.clear();
|
|
49
|
+
this.#confirmedTarget = null;
|
|
37
50
|
}
|
|
38
51
|
/** True when lifecycle tracking or the current event reports composition. */
|
|
39
52
|
isComposing(event) {
|
|
40
53
|
return this.#activeTargets.size > 0 || event?.isComposing === true;
|
|
41
54
|
}
|
|
55
|
+
/**
|
|
56
|
+
* Whether `event` is the `input` echoing the composition just confirmed.
|
|
57
|
+
*
|
|
58
|
+
* Asking closes the window either way, so one confirmation is folded at most
|
|
59
|
+
* once and a consumer asks once per `input`.
|
|
60
|
+
*/
|
|
61
|
+
consumesConfirmedInput(event) {
|
|
62
|
+
const confirmed = this.#confirmedTarget;
|
|
63
|
+
this.#confirmedTarget = null;
|
|
64
|
+
if (confirmed === null || confirmed !== event.target) return false;
|
|
65
|
+
const inputType = event.inputType;
|
|
66
|
+
return !inputType || COMPOSITION_INPUT_TYPES.has(inputType);
|
|
67
|
+
}
|
|
42
68
|
#handleStart = (event) => {
|
|
69
|
+
this.#confirmedTarget = null;
|
|
43
70
|
if (event.currentTarget) this.#activeTargets.add(event.currentTarget);
|
|
44
71
|
this.#onStart?.(event);
|
|
45
72
|
};
|
|
46
73
|
#handleEnd = (event) => {
|
|
47
74
|
if (event.currentTarget) this.#activeTargets.delete(event.currentTarget);
|
|
75
|
+
this.#confirmedTarget = event.target;
|
|
48
76
|
this.#onEnd?.(event);
|
|
49
77
|
};
|
|
78
|
+
/** A key on an observed field opens an edit of its own, so no echo is owed. */
|
|
79
|
+
#handleKeydown = () => {
|
|
80
|
+
this.#confirmedTarget = null;
|
|
81
|
+
};
|
|
50
82
|
};
|
|
51
83
|
|
|
52
84
|
// src/utils/half_width.ts
|
|
@@ -88,24 +120,39 @@ function construct(factory, locale, options, fallbackLocale) {
|
|
|
88
120
|
}
|
|
89
121
|
}
|
|
90
122
|
|
|
123
|
+
// src/utils/locale.ts
|
|
124
|
+
function resolveLocale(element, declared = "") {
|
|
125
|
+
return declared || element.closest("[lang]")?.getAttribute("lang") || void 0;
|
|
126
|
+
}
|
|
127
|
+
|
|
91
128
|
// src/controllers/currency_input_controller.ts
|
|
129
|
+
var FALLBACK_LOCALE = "en-US";
|
|
92
130
|
var CurrencyInputController = class extends Controller {
|
|
93
131
|
static targets = ["display", "field", "srValue"];
|
|
94
132
|
static values = {
|
|
95
|
-
locale: { type: String, default: "
|
|
133
|
+
locale: { type: String, default: "" },
|
|
96
134
|
currency: { type: String, default: "" },
|
|
97
135
|
precision: { type: Number, default: 2 }
|
|
98
136
|
};
|
|
99
137
|
static actions = ["format", "onInput"];
|
|
100
|
-
static events = ["change"];
|
|
101
|
-
/**
|
|
138
|
+
static events = ["change", "reconcile"];
|
|
139
|
+
/** The numeric value last committed; both events report only a move away from it. */
|
|
102
140
|
#lastValue = null;
|
|
103
141
|
#started = false;
|
|
142
|
+
/** A declaration change that arrived mid-composition, applied once nothing composes. */
|
|
143
|
+
#changeHeld = false;
|
|
144
|
+
/** The display a composition is running on, until that composition ends. */
|
|
145
|
+
#composing = null;
|
|
146
|
+
/**
|
|
147
|
+
* The display text this controller last rendered. It tells this controller's own
|
|
148
|
+
* text, written with the separators in force, from text the page wrote.
|
|
149
|
+
*/
|
|
150
|
+
#rendered = null;
|
|
104
151
|
/** Validated mirrors of the Values; the hot path never reads a raw Value. */
|
|
105
|
-
#locale
|
|
152
|
+
#locale;
|
|
106
153
|
#currency = "";
|
|
107
154
|
#precision = 2;
|
|
108
|
-
/** Formatters rebuilt
|
|
155
|
+
/** Formatters rebuilt when the declarations are taken in, never per keystroke. */
|
|
109
156
|
#grouping;
|
|
110
157
|
#fixed;
|
|
111
158
|
#accessible;
|
|
@@ -113,11 +160,25 @@ var CurrencyInputController = class extends Controller {
|
|
|
113
160
|
#decimal = ".";
|
|
114
161
|
/** The locale's non-Latin digits mapped back to ASCII (empty for Latin locales). */
|
|
115
162
|
#digits = /* @__PURE__ */ new Map();
|
|
116
|
-
/**
|
|
163
|
+
/**
|
|
164
|
+
* Holds mid-composition input so the IME's uncommitted text is never rewritten.
|
|
165
|
+
* The confirmed text is formatted once, and a declaration change the composition
|
|
166
|
+
* held back is applied after it.
|
|
167
|
+
*/
|
|
117
168
|
#composition = new CompositionTracker({
|
|
118
|
-
|
|
169
|
+
onStart: (event) => {
|
|
170
|
+
this.#composing = event.currentTarget;
|
|
171
|
+
},
|
|
172
|
+
onEnd: () => {
|
|
173
|
+
this.#composing = null;
|
|
174
|
+
this.#reformat(false, "edit");
|
|
175
|
+
this.#applyHeldChange();
|
|
176
|
+
}
|
|
119
177
|
});
|
|
120
|
-
/**
|
|
178
|
+
/**
|
|
179
|
+
* Re-validates on declaration changes and re-renders the committed display,
|
|
180
|
+
* reporting a value the re-render moved as `reconcile`.
|
|
181
|
+
*/
|
|
121
182
|
localeValueChanged() {
|
|
122
183
|
this.#applyValueChange();
|
|
123
184
|
}
|
|
@@ -128,52 +189,123 @@ var CurrencyInputController = class extends Controller {
|
|
|
128
189
|
this.#applyValueChange();
|
|
129
190
|
}
|
|
130
191
|
/**
|
|
131
|
-
*
|
|
132
|
-
*
|
|
133
|
-
*
|
|
192
|
+
* Reads the display with the declarations its text was written for, then
|
|
193
|
+
* re-renders it under the new ones — so a locale switch re-interprets the
|
|
194
|
+
* value, never the old text with new separators, and text the page wrote for
|
|
195
|
+
* the declarations it changes is never read with the old ones.
|
|
196
|
+
*
|
|
197
|
+
* While the display is composing, its text is the IME's: the change is held,
|
|
198
|
+
* configuration and all, until the composition ends, so the confirmed text is
|
|
199
|
+
* read with the separators it was typed against before the change applies.
|
|
200
|
+
*
|
|
201
|
+
* @stimeoRenderRoot
|
|
134
202
|
*/
|
|
135
203
|
#applyValueChange() {
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
204
|
+
if (this.#composition.isComposing()) {
|
|
205
|
+
this.#changeHeld = true;
|
|
206
|
+
return;
|
|
207
|
+
}
|
|
208
|
+
if (!this.#started || !this.hasDisplayTarget) return;
|
|
209
|
+
const scan = this.#readDisplay();
|
|
139
210
|
if (document.activeElement === this.displayTarget) {
|
|
140
211
|
const formatted = this.#render(scan.parts);
|
|
141
|
-
this
|
|
142
|
-
this.#reflect(scan.value, formatted);
|
|
143
|
-
} else if (scan.value === null) {
|
|
144
|
-
this.displayTarget.value = "";
|
|
145
|
-
this.#reflect(null, "");
|
|
212
|
+
this.#show(formatted);
|
|
213
|
+
this.#reflect(scan.value, formatted, "reconcile");
|
|
146
214
|
} else {
|
|
147
|
-
|
|
148
|
-
const formatted = this.#fixed.format(rounded);
|
|
149
|
-
this.displayTarget.value = formatted;
|
|
150
|
-
this.#reflect(rounded, formatted);
|
|
215
|
+
this.#renderFixed(scan.value, "reconcile");
|
|
151
216
|
}
|
|
152
217
|
}
|
|
153
|
-
/**
|
|
218
|
+
/** Applies a declaration change a composition held back. */
|
|
219
|
+
#applyHeldChange() {
|
|
220
|
+
if (!this.#changeHeld) return;
|
|
221
|
+
this.#changeHeld = false;
|
|
222
|
+
this.#applyValueChange();
|
|
223
|
+
}
|
|
224
|
+
/**
|
|
225
|
+
* Reads the display and takes in the declarations the page carries now. This
|
|
226
|
+
* controller's own rendering was written with the separators in force, so it
|
|
227
|
+
* is read before they change; any other text is the page's, written for the
|
|
228
|
+
* declarations it carries, so it is read after.
|
|
229
|
+
*/
|
|
230
|
+
#readDisplay() {
|
|
231
|
+
const text = this.displayTarget.value;
|
|
232
|
+
const own = text === this.#rendered ? this.#scan(text) : null;
|
|
233
|
+
this.#takeInDeclarations();
|
|
234
|
+
return own ?? this.#scan(text);
|
|
235
|
+
}
|
|
236
|
+
/** Takes in the declarations the page carries now, a change a composition held among them. */
|
|
237
|
+
#takeInDeclarations() {
|
|
238
|
+
this.#changeHeld = false;
|
|
239
|
+
this.#revalidate();
|
|
240
|
+
}
|
|
241
|
+
/**
|
|
242
|
+
* Takes in the declarations the page carries now and normalizes the display
|
|
243
|
+
* to its fixed-precision form.
|
|
244
|
+
*
|
|
245
|
+
* A display still showing this controller's own last rendering is not read
|
|
246
|
+
* back: its separators may belong to declarations that changed while the
|
|
247
|
+
* controller was away, and the committed value is known anyway. That value is
|
|
248
|
+
* re-rendered at the current fixed precision, which rounds it whether
|
|
249
|
+
* `precision` changed meanwhile or the entry was left unrounded mid-typing, and
|
|
250
|
+
* a move the rounding causes is reported as `reconcile`. Any other text is the
|
|
251
|
+
* page's, written for the declarations it carries, so it is read with them.
|
|
252
|
+
*/
|
|
154
253
|
connect() {
|
|
155
254
|
this.#started = true;
|
|
255
|
+
this.#takeInDeclarations();
|
|
156
256
|
if (!this.hasDisplayTarget) return;
|
|
257
|
+
if (this.displayTarget.value === this.#rendered) {
|
|
258
|
+
this.#renderFixed(this.#lastValue, "reconcile");
|
|
259
|
+
return;
|
|
260
|
+
}
|
|
157
261
|
const { value } = this.#scan(this.displayTarget.value);
|
|
158
262
|
this.#lastValue = value === null ? null : round(value, this.#precision);
|
|
159
|
-
|
|
160
|
-
this.displayTarget.value = "";
|
|
161
|
-
this.#reflect(null, "");
|
|
162
|
-
} else {
|
|
163
|
-
this.#reformat(true);
|
|
164
|
-
}
|
|
263
|
+
this.#renderFixed(value, "reconcile");
|
|
165
264
|
}
|
|
265
|
+
/**
|
|
266
|
+
* Releases the composition listeners. A composition still running hears no
|
|
267
|
+
* `compositionend` once they are gone, so it ends here; a change it held is
|
|
268
|
+
* taken in on the next `connect()`.
|
|
269
|
+
*/
|
|
166
270
|
disconnect() {
|
|
167
|
-
this.#
|
|
271
|
+
this.#endUntrackedComposition();
|
|
168
272
|
this.#composition.disconnect();
|
|
273
|
+
this.#started = false;
|
|
169
274
|
}
|
|
170
|
-
/**
|
|
275
|
+
/**
|
|
276
|
+
* Tracks composition on an arriving (or swapped-in) display and normalizes it
|
|
277
|
+
* under the declarations the page carries now, reading this controller's own
|
|
278
|
+
* rendering with the separators it was written under; an amount that differs
|
|
279
|
+
* from the committed one is the page's, so it is reported as `reconcile`.
|
|
280
|
+
*/
|
|
171
281
|
displayTargetConnected(target) {
|
|
172
282
|
this.#composition.observe(target);
|
|
173
|
-
if (this.#started)
|
|
283
|
+
if (!this.#started) return;
|
|
284
|
+
this.#renderFixed(this.#readDisplay().value, "reconcile");
|
|
174
285
|
}
|
|
286
|
+
/**
|
|
287
|
+
* Stops tracking a display that leaves. A composition running on it hears no
|
|
288
|
+
* `compositionend` any more, so it ends here; a change it held waits for the
|
|
289
|
+
* next display to be read.
|
|
290
|
+
*/
|
|
175
291
|
displayTargetDisconnected(target) {
|
|
176
292
|
this.#composition.unobserve(target);
|
|
293
|
+
this.#endUntrackedComposition();
|
|
294
|
+
}
|
|
295
|
+
/**
|
|
296
|
+
* Ends a composition no `compositionend` will reach. A display still in place —
|
|
297
|
+
* one a move re-inserted, or the display of a controller that moves — holds the
|
|
298
|
+
* text the composition left, typed against the declarations in force, so it is
|
|
299
|
+
* read as `compositionend` would read it and becomes this controller's own
|
|
300
|
+
* rendering. The page's move ended the composition, so a value it moves is
|
|
301
|
+
* reported as `reconcile`. A display that left takes its composition along.
|
|
302
|
+
*/
|
|
303
|
+
#endUntrackedComposition() {
|
|
304
|
+
const display = this.#composing;
|
|
305
|
+
this.#composing = null;
|
|
306
|
+
if (this.hasDisplayTarget && this.displayTarget === display) {
|
|
307
|
+
this.#reformat(false, "reconcile");
|
|
308
|
+
}
|
|
177
309
|
}
|
|
178
310
|
/** Syncs a late-arriving hidden field without touching the display or events. */
|
|
179
311
|
fieldTargetConnected() {
|
|
@@ -186,33 +318,23 @@ var CurrencyInputController = class extends Controller {
|
|
|
186
318
|
/** Re-groups digits as the user types, preserving the caret position. */
|
|
187
319
|
onInput(event) {
|
|
188
320
|
if (this.#composition.isComposing(event)) return;
|
|
189
|
-
this.#reformat(false);
|
|
321
|
+
this.#reformat(false, "edit");
|
|
190
322
|
}
|
|
191
323
|
/** Applies the fixed-precision rounding on blur. */
|
|
192
324
|
format() {
|
|
193
|
-
this.#reformat(true);
|
|
325
|
+
this.#reformat(true, "edit");
|
|
194
326
|
}
|
|
195
327
|
/**
|
|
196
328
|
* Parses the display value, rewrites it grouped (optionally at fixed
|
|
197
329
|
* precision), keeps the caret stable by significant characters, and syncs the
|
|
198
|
-
* field, the screen-reader span, and the `
|
|
199
|
-
*
|
|
200
|
-
* @stimeoRenderRoot
|
|
330
|
+
* field, the screen-reader span, and the event `cause` selects.
|
|
201
331
|
*/
|
|
202
|
-
#reformat(fixedPrecision) {
|
|
332
|
+
#reformat(fixedPrecision, cause) {
|
|
203
333
|
if (!this.hasDisplayTarget) return;
|
|
204
334
|
const raw = this.displayTarget.value;
|
|
205
335
|
const { parts, value } = this.#scan(raw);
|
|
206
336
|
if (fixedPrecision) {
|
|
207
|
-
|
|
208
|
-
this.displayTarget.value = "";
|
|
209
|
-
this.#reflect(null, "");
|
|
210
|
-
return;
|
|
211
|
-
}
|
|
212
|
-
const rounded = round(value, this.#precision);
|
|
213
|
-
const formatted2 = this.#fixed.format(rounded);
|
|
214
|
-
this.displayTarget.value = formatted2;
|
|
215
|
-
this.#reflect(rounded, formatted2);
|
|
337
|
+
this.#renderFixed(value, cause);
|
|
216
338
|
return;
|
|
217
339
|
}
|
|
218
340
|
const formatted = this.#render(parts);
|
|
@@ -222,12 +344,31 @@ var CurrencyInputController = class extends Controller {
|
|
|
222
344
|
this.displayTarget.value = formatted;
|
|
223
345
|
if (anchor !== null) this.#restoreCaret(formatted, anchor);
|
|
224
346
|
}
|
|
225
|
-
this.#
|
|
347
|
+
this.#rendered = formatted;
|
|
348
|
+
this.#reflect(value, formatted, cause);
|
|
349
|
+
}
|
|
350
|
+
/**
|
|
351
|
+
* Shows `value` at the fixed precision — an empty display for no value — and
|
|
352
|
+
* reports a move under the event `cause` selects.
|
|
353
|
+
*/
|
|
354
|
+
#renderFixed(value, cause) {
|
|
355
|
+
if (value === null) {
|
|
356
|
+
this.#show("");
|
|
357
|
+
this.#reflect(null, "", cause);
|
|
358
|
+
return;
|
|
359
|
+
}
|
|
360
|
+
const rounded = round(value, this.#precision);
|
|
361
|
+
const formatted = this.#fixed.format(rounded);
|
|
362
|
+
this.#show(formatted);
|
|
363
|
+
this.#reflect(rounded, formatted, cause);
|
|
364
|
+
}
|
|
365
|
+
/** Writes `text` to the display as this controller's own rendering. */
|
|
366
|
+
#show(text) {
|
|
367
|
+
this.displayTarget.value = text;
|
|
368
|
+
this.#rendered = text;
|
|
226
369
|
}
|
|
227
370
|
/**
|
|
228
371
|
* The in-progress rendering: grouped integer, sign and fraction as typed.
|
|
229
|
-
*
|
|
230
|
-
* @stimeoRenderRoot
|
|
231
372
|
*/
|
|
232
373
|
#render(parts) {
|
|
233
374
|
const int = parts.int === "" ? "" : this.#grouping.format(BigInt(parts.int));
|
|
@@ -287,15 +428,22 @@ var CurrencyInputController = class extends Controller {
|
|
|
287
428
|
if (this.hasSrValueTarget) {
|
|
288
429
|
this.srValueTarget.textContent = isEmpty ? "" : this.#accessible.format(value);
|
|
289
430
|
}
|
|
290
|
-
this.element.toggleAttribute(
|
|
431
|
+
this.element.toggleAttribute(`data-${this.identifier}-empty`, isEmpty);
|
|
291
432
|
}
|
|
292
|
-
/**
|
|
293
|
-
|
|
433
|
+
/**
|
|
434
|
+
* {@link #write}, then reports a moved value under the event `cause` selects:
|
|
435
|
+
* `change` for the user's edit, `reconcile` for a move the page caused. The
|
|
436
|
+
* display may hold an in-progress `-`, but a `null` value always rides with an
|
|
437
|
+
* empty `formatted`, whichever event reports it, so consumers can treat the
|
|
438
|
+
* pair as cleared.
|
|
439
|
+
*/
|
|
440
|
+
#reflect(value, formatted, cause) {
|
|
294
441
|
this.#write(value);
|
|
295
|
-
if (value
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
}
|
|
442
|
+
if (value === this.#lastValue) return;
|
|
443
|
+
this.#lastValue = value;
|
|
444
|
+
const detail = { value, formatted: value === null ? "" : formatted };
|
|
445
|
+
if (cause === "edit") this.dispatch("change", { detail });
|
|
446
|
+
else this.dispatch("reconcile", { detail });
|
|
299
447
|
}
|
|
300
448
|
/**
|
|
301
449
|
* Scans arbitrary input text into its number-shaped parts and value. Keeps
|
|
@@ -327,10 +475,15 @@ var CurrencyInputController = class extends Controller {
|
|
|
327
475
|
#isDecimalMark(ch) {
|
|
328
476
|
return ch === this.#decimal || ch === "." && this.#group !== ".";
|
|
329
477
|
}
|
|
330
|
-
/**
|
|
478
|
+
/**
|
|
479
|
+
* Re-syncs field / srValue / hook without dispatching: from the display, or —
|
|
480
|
+
* while it is composing, when its text is not committed yet — from the value
|
|
481
|
+
* last committed.
|
|
482
|
+
*/
|
|
331
483
|
#resync() {
|
|
332
484
|
if (!this.hasDisplayTarget) return;
|
|
333
|
-
this.#
|
|
485
|
+
const value = this.#composition.isComposing() ? this.#lastValue : this.#scan(this.displayTarget.value).value;
|
|
486
|
+
this.#write(value);
|
|
334
487
|
}
|
|
335
488
|
/**
|
|
336
489
|
* Validates the declared Values, falling back to each Value's default when a
|
|
@@ -339,8 +492,9 @@ var CurrencyInputController = class extends Controller {
|
|
|
339
492
|
* formatters from the validated set.
|
|
340
493
|
*/
|
|
341
494
|
#revalidate() {
|
|
342
|
-
const
|
|
343
|
-
|
|
495
|
+
const resolved = resolveLocale(this.element, this.localeValue);
|
|
496
|
+
const usable = intlFormatter(Intl.NumberFormat, resolved, {});
|
|
497
|
+
this.#locale = usable === null ? FALLBACK_LOCALE : resolved;
|
|
344
498
|
const precision = this.precisionValue;
|
|
345
499
|
this.#precision = Number.isInteger(precision) && precision >= 0 && precision <= 100 ? precision : 2;
|
|
346
500
|
this.#currency = "";
|
|
@@ -355,7 +509,7 @@ var CurrencyInputController = class extends Controller {
|
|
|
355
509
|
Intl.NumberFormat,
|
|
356
510
|
this.#locale,
|
|
357
511
|
{ useGrouping: true, maximumFractionDigits: 0 },
|
|
358
|
-
|
|
512
|
+
FALLBACK_LOCALE
|
|
359
513
|
);
|
|
360
514
|
this.#fixed = intlFormatter(
|
|
361
515
|
Intl.NumberFormat,
|
|
@@ -365,15 +519,15 @@ var CurrencyInputController = class extends Controller {
|
|
|
365
519
|
minimumFractionDigits: this.#precision,
|
|
366
520
|
maximumFractionDigits: this.#precision
|
|
367
521
|
},
|
|
368
|
-
|
|
522
|
+
FALLBACK_LOCALE
|
|
369
523
|
);
|
|
370
524
|
this.#accessible = this.#currency ? intlFormatter(
|
|
371
525
|
Intl.NumberFormat,
|
|
372
526
|
this.#locale,
|
|
373
527
|
{ style: "currency", currency: this.#currency },
|
|
374
|
-
|
|
528
|
+
FALLBACK_LOCALE
|
|
375
529
|
) : this.#fixed;
|
|
376
|
-
const parts = intlFormatter(Intl.NumberFormat, this.#locale, {},
|
|
530
|
+
const parts = intlFormatter(Intl.NumberFormat, this.#locale, {}, FALLBACK_LOCALE).formatToParts(
|
|
377
531
|
11111.1
|
|
378
532
|
);
|
|
379
533
|
this.#group = parts.find((p) => p.type === "group")?.value ?? ",";
|
|
@@ -383,7 +537,7 @@ var CurrencyInputController = class extends Controller {
|
|
|
383
537
|
Intl.NumberFormat,
|
|
384
538
|
this.#locale,
|
|
385
539
|
{ useGrouping: false },
|
|
386
|
-
|
|
540
|
+
FALLBACK_LOCALE
|
|
387
541
|
);
|
|
388
542
|
for (let i = 0; i <= 9; i++) {
|
|
389
543
|
const digit = digitFormatter.format(i);
|