@marianmeres/stuic 3.110.0 → 3.111.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.
|
@@ -61,6 +61,17 @@ export function onSubmitValidityCheck(node) {
|
|
|
61
61
|
// input (last radio input), which is not desired
|
|
62
62
|
if (el.type === "radio")
|
|
63
63
|
continue;
|
|
64
|
+
// File inputs: validate WITHOUT re-dispatching events. A synthetic
|
|
65
|
+
// `change` can't change a file input's value (it's read-only to
|
|
66
|
+
// script), so it adds nothing for validation — but it DOES re-trigger
|
|
67
|
+
// any dropzone/upload listener bound to the input. `FieldAssets`'
|
|
68
|
+
// hidden `<input type="file">` is exactly such a listener (wired via
|
|
69
|
+
// the `fileDropzone` action): re-firing `change` re-runs its
|
|
70
|
+
// `processFiles` with the file still sitting in `inputEl.files`,
|
|
71
|
+
// pushing a duplicate optimistic asset and kicking off a real
|
|
72
|
+
// re-upload on every submit. We still read `el.validity` below, so
|
|
73
|
+
// native `required` file inputs are honored as before.
|
|
74
|
+
const isFile = el.type === "file";
|
|
64
75
|
// // [debug] kept commented for the next time Issue A regresses
|
|
65
76
|
// // eslint-disable-next-line no-console
|
|
66
77
|
// console.log(`[onSubmitValidityCheck] el#${i} ${el.name || el.type} BEFORE`, {
|
|
@@ -80,10 +91,12 @@ export function onSubmitValidityCheck(node) {
|
|
|
80
91
|
// error, silently routing the form to `submit_invalid` and never calling
|
|
81
92
|
// the consumer's onSubmit. The dispatched change event below re-runs the
|
|
82
93
|
// per-field validator which re-applies a real customValidity if needed.
|
|
83
|
-
if (
|
|
84
|
-
el.setCustomValidity
|
|
85
|
-
|
|
86
|
-
|
|
94
|
+
if (!isFile) {
|
|
95
|
+
if (typeof el.setCustomValidity === "function")
|
|
96
|
+
el.setCustomValidity("");
|
|
97
|
+
el.dispatchEvent(new Event("input", { bubbles: true }));
|
|
98
|
+
el.dispatchEvent(new Event("change", { bubbles: true }));
|
|
99
|
+
}
|
|
87
100
|
// // [debug] kept commented for the next time Issue A regresses
|
|
88
101
|
// // eslint-disable-next-line no-console
|
|
89
102
|
// console.log(`[onSubmitValidityCheck] el#${i} ${el.name || el.type} AFTER `, {
|
|
@@ -453,7 +453,27 @@
|
|
|
453
453
|
processFiles(files: FileList | null, wasDrop?: boolean) {
|
|
454
454
|
clog.debug(`processFiles`, wasDrop ? "[DROPPED]" : "", files);
|
|
455
455
|
|
|
456
|
-
|
|
456
|
+
// Copy the File objects into a stable array, then IMMEDIATELY release the
|
|
457
|
+
// hidden <input type="file">'s retained FileList. A native file input
|
|
458
|
+
// keeps its FileList after a selection until it is reset (form reset or
|
|
459
|
+
// `value = ""`), and its `change` event (wired by the `fileDropzone`
|
|
460
|
+
// action) calls back into this handler. Without this reset, any later
|
|
461
|
+
// stray `change` re-runs processFiles with the SAME file still in
|
|
462
|
+
// `inputEl.files`, pushing a duplicate optimistic asset and firing a real
|
|
463
|
+
// re-upload. `onSubmitValidityCheck` used to dispatch exactly such a
|
|
464
|
+
// synthetic `change` on submit (now fixed there too — belt and braces).
|
|
465
|
+
// Clearing also restores the ability to re-select the same file twice in
|
|
466
|
+
// a row (an unchanged value emits no `change`). The blob URLs we create
|
|
467
|
+
// below are independent of the input, so clearing here is safe.
|
|
468
|
+
const incoming = [...(files ?? [])];
|
|
469
|
+
if (inputEl) inputEl.value = "";
|
|
470
|
+
|
|
471
|
+
// Nothing to consume — a cancelled picker, or a stray/synthetic `change`
|
|
472
|
+
// on an already-cleared input. Bail before touching state or calling
|
|
473
|
+
// processAssets (which would otherwise run with an empty batch).
|
|
474
|
+
if (!incoming.length) return;
|
|
475
|
+
|
|
476
|
+
if (accept && incoming.some((f) => !is_accepted_type(accept, f.type))) {
|
|
457
477
|
const msg = t("invalid_type", { accept });
|
|
458
478
|
if (notifications) notifications.error(msg);
|
|
459
479
|
else alert(msg);
|
|
@@ -461,8 +481,11 @@
|
|
|
461
481
|
}
|
|
462
482
|
|
|
463
483
|
const cardErrMsg = t("cardinality_reached", { max: cardinality });
|
|
464
|
-
|
|
465
|
-
|
|
484
|
+
// `>=` (not `>`): refuse the moment the field already holds `cardinality`
|
|
485
|
+
// assets, instead of optimistically adding one past the limit and relying
|
|
486
|
+
// on the validator to reject it afterwards (the off-by-one that made the
|
|
487
|
+
// single-cardinality symptom loud).
|
|
488
|
+
if (assets.length >= cardinality) {
|
|
466
489
|
if (notifications) notifications.error(cardErrMsg);
|
|
467
490
|
else alert(cardErrMsg);
|
|
468
491
|
return;
|
|
@@ -470,8 +493,8 @@
|
|
|
470
493
|
|
|
471
494
|
const toBeProcessed: FieldAsset[] = [];
|
|
472
495
|
|
|
473
|
-
for (const file of
|
|
474
|
-
if (assets.length
|
|
496
|
+
for (const file of incoming) {
|
|
497
|
+
if (assets.length >= cardinality) {
|
|
475
498
|
notifications ? notifications.error(cardErrMsg) : alert(cardErrMsg);
|
|
476
499
|
break;
|
|
477
500
|
}
|
package/docs/domains/actions.md
CHANGED
|
@@ -8,23 +8,23 @@
|
|
|
8
8
|
|
|
9
9
|
## Available Actions
|
|
10
10
|
|
|
11
|
-
| Action
|
|
12
|
-
|
|
|
13
|
-
| `validate`
|
|
14
|
-
| `focusTrap`
|
|
15
|
-
| `autogrow`
|
|
16
|
-
| `autoscroll`
|
|
17
|
-
| `dimBehind`
|
|
18
|
-
| `fileDropzone`
|
|
19
|
-
| `highlightDragover`
|
|
20
|
-
| `resizableWidth`
|
|
21
|
-
| `trim`
|
|
22
|
-
| `typeahead`
|
|
23
|
-
| `onSubmitValidityCheck`
|
|
24
|
-
| `popover`
|
|
25
|
-
| `spotlight`
|
|
26
|
-
| `tooltip`
|
|
27
|
-
| `createTour` / `tourStep` | Multi-step onboarding tour (built on spotlight)
|
|
11
|
+
| Action | Purpose | File |
|
|
12
|
+
| ------------------------- | ------------------------------------------------------------- | ------------------------------------ |
|
|
13
|
+
| `validate` | Form field validation with i18n support | `validate.svelte.ts` |
|
|
14
|
+
| `focusTrap` | Keyboard focus containment (modals/dialogs) | `focus-trap.ts` |
|
|
15
|
+
| `autogrow` | Auto-resize textarea to content | `autogrow.svelte.ts` |
|
|
16
|
+
| `autoscroll` | Auto-scroll container to bottom | `autoscroll.ts` |
|
|
17
|
+
| `dimBehind` | Dim everything behind a target element (simplified spotlight) | `dim-behind/` |
|
|
18
|
+
| `fileDropzone` | Drag-and-drop file handling | `file-dropzone.svelte.ts` |
|
|
19
|
+
| `highlightDragover` | Visual feedback on drag-over | `highlight-dragover.svelte.ts` |
|
|
20
|
+
| `resizableWidth` | Draggable width resizing | `resizable-width.svelte.ts` |
|
|
21
|
+
| `trim` | Auto-trim whitespace from input | `trim.svelte.ts` |
|
|
22
|
+
| `typeahead` | Advanced autocomplete behavior | `typeahead.svelte.ts` |
|
|
23
|
+
| `onSubmitValidityCheck` | Form submit validation | `on-submit-validity-check.svelte.ts` |
|
|
24
|
+
| `popover` | Popover positioning | `popover/` |
|
|
25
|
+
| `spotlight` | Spotlight/coach mark overlay with cutout hole | `spotlight/` |
|
|
26
|
+
| `tooltip` | Tooltip positioning and display | `tooltip/` |
|
|
27
|
+
| `createTour` / `tourStep` | Multi-step onboarding tour (built on spotlight) | `onboarding/` |
|
|
28
28
|
|
|
29
29
|
---
|
|
30
30
|
|
|
@@ -116,8 +116,8 @@ inline messages to render. They won't — the `validate` action's
|
|
|
116
116
|
|
|
117
117
|
### Hidden inputs and `required`
|
|
118
118
|
|
|
119
|
-
Per the HTML spec, `<input type="hidden">` is
|
|
120
|
-
|
|
119
|
+
Per the HTML spec, `<input type="hidden">` is _barred from constraint
|
|
120
|
+
validation_ — `validity.valueMissing` stays `false` regardless of the
|
|
121
121
|
`required` attribute, and native browser submit blocking is skipped. Several
|
|
122
122
|
STUIC field components (`FieldPhoneNumber`, `FieldCountry`, `FieldObject`,
|
|
123
123
|
`FieldAssets`, `FieldInputLocalized`, `FieldKeyValues`, `FieldLikeButton`)
|
|
@@ -138,6 +138,29 @@ correctly fail validation when empty — both through the imperative
|
|
|
138
138
|
`validate()` path and via `use:onSubmitValidityCheck`. Without this wrap
|
|
139
139
|
they'd silently accept empty values.
|
|
140
140
|
|
|
141
|
+
### `onSubmitValidityCheck` and synthetic events
|
|
142
|
+
|
|
143
|
+
On submit the action walks `form.elements` and synthetically dispatches
|
|
144
|
+
`input` + `change` on each control, so custom `validate` listeners run even on
|
|
145
|
+
fields the user never touched. `form.elements` includes CSS-hidden controls
|
|
146
|
+
(`display:none` does **not** remove an element — only `disabled` or being
|
|
147
|
+
outside the form does), so this fan-out reaches every wired input.
|
|
148
|
+
|
|
149
|
+
Two element types are deliberately exempt from the synthetic dispatch:
|
|
150
|
+
|
|
151
|
+
- **`type="radio"`** — dispatching `change` auto-selects the last radio in the
|
|
152
|
+
group, which is wrong.
|
|
153
|
+
- **`type="file"`** — a file input's value is read-only to script, so a
|
|
154
|
+
synthetic `change` can't re-validate it; worse, it re-triggers any dropzone /
|
|
155
|
+
upload listener bound to the input. `FieldAssets` wires its hidden
|
|
156
|
+
`<input type="file">` through `fileDropzone`, so a re-fired `change` would
|
|
157
|
+
re-run `processFiles` with the previously-picked file still in `inputEl.files`
|
|
158
|
+
— duplicating the asset and firing a real re-upload on every save. File inputs
|
|
159
|
+
are still read for native constraint validation (`required`); only the event
|
|
160
|
+
dispatch is skipped. (`FieldAssets` additionally clears its file input after
|
|
161
|
+
consuming a selection, both as defense-in-depth and to allow re-selecting the
|
|
162
|
+
same file twice in a row.)
|
|
163
|
+
|
|
141
164
|
### File Dropzone
|
|
142
165
|
|
|
143
166
|
```svelte
|