@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 (typeof el.setCustomValidity === "function")
84
- el.setCustomValidity("");
85
- el.dispatchEvent(new Event("input", { bubbles: true }));
86
- el.dispatchEvent(new Event("change", { bubbles: true }));
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
- if (accept && [...(files ?? [])].some((f) => !is_accepted_type(accept, f.type))) {
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
- if (assets.length > cardinality) {
465
- // if (assets.length + (files?.length ?? 0) > cardinality) {
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 files ?? []) {
474
- if (assets.length > cardinality) {
496
+ for (const file of incoming) {
497
+ if (assets.length >= cardinality) {
475
498
  notifications ? notifications.error(cardErrMsg) : alert(cardErrMsg);
476
499
  break;
477
500
  }
@@ -8,23 +8,23 @@
8
8
 
9
9
  ## Available Actions
10
10
 
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/` |
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 *barred from constraint
120
- validation* — `validity.valueMissing` stays `false` regardless of the
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
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@marianmeres/stuic",
3
- "version": "3.110.0",
3
+ "version": "3.111.0",
4
4
  "scripts": {
5
5
  "dev": "vite dev",
6
6
  "build": "vite build && pnpm run prepack",