@marianmeres/stuic 3.86.0 → 3.87.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/dist/actions/validate.svelte.d.ts +24 -4
- package/dist/actions/validate.svelte.js +18 -5
- package/dist/components/EmailVerifyForm/EmailVerifyForm.svelte +21 -0
- package/dist/components/EmailVerifyForm/EmailVerifyForm.svelte.d.ts +4 -1
- package/dist/components/Input/FieldAssets.svelte +48 -3
- package/dist/components/Input/FieldAssets.svelte.d.ts +8 -2
- package/dist/components/Input/FieldCheckbox.svelte +34 -3
- package/dist/components/Input/FieldCheckbox.svelte.d.ts +8 -1
- package/dist/components/Input/FieldCountry.svelte +64 -7
- package/dist/components/Input/FieldCountry.svelte.d.ts +8 -1
- package/dist/components/Input/FieldFile.svelte +34 -3
- package/dist/components/Input/FieldFile.svelte.d.ts +8 -1
- package/dist/components/Input/FieldInput.svelte +43 -3
- package/dist/components/Input/FieldInput.svelte.d.ts +8 -1
- package/dist/components/Input/FieldInputLocalized.svelte +41 -2
- package/dist/components/Input/FieldInputLocalized.svelte.d.ts +8 -2
- package/dist/components/Input/FieldKeyValues.svelte +37 -2
- package/dist/components/Input/FieldKeyValues.svelte.d.ts +8 -2
- package/dist/components/Input/FieldLikeButton.svelte +41 -4
- package/dist/components/Input/FieldLikeButton.svelte.d.ts +8 -1
- package/dist/components/Input/FieldObject.svelte +64 -6
- package/dist/components/Input/FieldObject.svelte.d.ts +8 -2
- package/dist/components/Input/FieldOptions.svelte +36 -3
- package/dist/components/Input/FieldOptions.svelte.d.ts +8 -2
- package/dist/components/Input/FieldPhoneNumber.svelte +51 -6
- package/dist/components/Input/FieldPhoneNumber.svelte.d.ts +8 -1
- package/dist/components/Input/FieldRadios.svelte +36 -2
- package/dist/components/Input/FieldRadios.svelte.d.ts +8 -1
- package/dist/components/Input/FieldSelect.svelte +34 -3
- package/dist/components/Input/FieldSelect.svelte.d.ts +8 -1
- package/dist/components/Input/FieldSwitch.svelte +41 -2
- package/dist/components/Input/FieldSwitch.svelte.d.ts +8 -1
- package/dist/components/Input/FieldTextarea.svelte +34 -3
- package/dist/components/Input/FieldTextarea.svelte.d.ts +8 -1
- package/dist/components/Input/_internal/FieldRadioInternal.svelte +34 -3
- package/dist/components/Input/_internal/FieldRadioInternal.svelte.d.ts +7 -1
- package/dist/components/LoginForm/LoginForm.svelte +35 -0
- package/dist/components/LoginForm/LoginForm.svelte.d.ts +5 -1
- package/dist/components/LoginOrRegisterForm/LoginOrRegisterForm.svelte +40 -0
- package/dist/components/LoginOrRegisterForm/LoginOrRegisterForm.svelte.d.ts +5 -1
- package/dist/components/RegisterForm/RegisterForm.svelte +46 -2
- package/dist/components/RegisterForm/RegisterForm.svelte.d.ts +5 -1
- package/dist/components/Switch/Switch.svelte +42 -4
- package/dist/components/Switch/Switch.svelte.d.ts +7 -1
- package/dist/utils/index.d.ts +1 -0
- package/dist/utils/index.js +1 -0
- package/dist/utils/validate-fields.d.ts +72 -0
- package/dist/utils/validate-fields.js +73 -0
- package/docs/domains/actions.md +74 -0
- package/docs/domains/components.md +131 -0
- package/docs/domains/utils.md +38 -0
- package/package.json +1 -1
|
@@ -81,6 +81,18 @@ export interface ValidateOptions {
|
|
|
81
81
|
customValidator?: (value: unknown, context: Record<string, unknown> | undefined, el: HTMLInputElement | HTMLTextAreaElement | HTMLSelectElement) => string | undefined;
|
|
82
82
|
on?: "input" | "change";
|
|
83
83
|
setValidationResult?: (res: ValidationResult) => void;
|
|
84
|
+
/**
|
|
85
|
+
* Receives a reference to the action's internal validator function so the
|
|
86
|
+
* host component can trigger validation imperatively (e.g., on submit,
|
|
87
|
+
* without waiting for the user to focus/blur or change a value).
|
|
88
|
+
*
|
|
89
|
+
* Called whenever the action's `$effect` re-runs — the host should store
|
|
90
|
+
* the latest reference. Invoking the function runs the exact same code
|
|
91
|
+
* path the change/blur listeners do (no synthetic DOM events).
|
|
92
|
+
*
|
|
93
|
+
* Pair with `setValidationResult` to read the new result after invoking.
|
|
94
|
+
*/
|
|
95
|
+
setDoValidate?: (doValidate: () => void) => void;
|
|
84
96
|
t?: false | ReasonTranslate;
|
|
85
97
|
}
|
|
86
98
|
/**
|
|
@@ -146,10 +158,18 @@ export interface ValidateOptions {
|
|
|
146
158
|
* validate.t = (reason, value, fallback) => translations[reason] ?? fallback;
|
|
147
159
|
* ```
|
|
148
160
|
*
|
|
149
|
-
* **Hidden Input
|
|
150
|
-
*
|
|
151
|
-
*
|
|
152
|
-
*
|
|
161
|
+
* **Hidden Input Limitations** (two distinct issues, both relevant when wrapping
|
|
162
|
+
* `<input type="hidden">` for form-data participation):
|
|
163
|
+
*
|
|
164
|
+
* 1. Browsers don't populate `el.validationMessage` for hidden inputs even when
|
|
165
|
+
* `setCustomValidity()` is called. This action works around it by preserving
|
|
166
|
+
* the `customValidator` return value and using it directly as the error
|
|
167
|
+
* message fallback.
|
|
168
|
+
* 2. Per the HTML spec, hidden inputs are **barred from constraint validation**
|
|
169
|
+
* entirely. `validity.valueMissing` stays `false` regardless of the
|
|
170
|
+
* `required` attribute, so `required` is a silent no-op. STUIC's hidden-input
|
|
171
|
+
* Field components (`FieldPhoneNumber`, `FieldCountry`, `FieldObject`, etc.)
|
|
172
|
+
* enforce `required` themselves inside their `customValidator`.
|
|
153
173
|
*/
|
|
154
174
|
export declare function validate(el: HTMLInputElement | HTMLTextAreaElement | HTMLSelectElement, fn?: () => boolean | ValidateOptions): void;
|
|
155
175
|
export declare namespace validate {
|
|
@@ -128,16 +128,24 @@ const KNOWN_REASONS = [
|
|
|
128
128
|
* validate.t = (reason, value, fallback) => translations[reason] ?? fallback;
|
|
129
129
|
* ```
|
|
130
130
|
*
|
|
131
|
-
* **Hidden Input
|
|
132
|
-
*
|
|
133
|
-
*
|
|
134
|
-
*
|
|
131
|
+
* **Hidden Input Limitations** (two distinct issues, both relevant when wrapping
|
|
132
|
+
* `<input type="hidden">` for form-data participation):
|
|
133
|
+
*
|
|
134
|
+
* 1. Browsers don't populate `el.validationMessage` for hidden inputs even when
|
|
135
|
+
* `setCustomValidity()` is called. This action works around it by preserving
|
|
136
|
+
* the `customValidator` return value and using it directly as the error
|
|
137
|
+
* message fallback.
|
|
138
|
+
* 2. Per the HTML spec, hidden inputs are **barred from constraint validation**
|
|
139
|
+
* entirely. `validity.valueMissing` stays `false` regardless of the
|
|
140
|
+
* `required` attribute, so `required` is a silent no-op. STUIC's hidden-input
|
|
141
|
+
* Field components (`FieldPhoneNumber`, `FieldCountry`, `FieldObject`, etc.)
|
|
142
|
+
* enforce `required` themselves inside their `customValidator`.
|
|
135
143
|
*/
|
|
136
144
|
export function validate(el, fn) {
|
|
137
145
|
$effect(() => {
|
|
138
146
|
//
|
|
139
147
|
const fnResult = fn?.() ?? {};
|
|
140
|
-
const { enabled, context, customValidator, on = "change", setValidationResult, t, } = typeof fnResult === "boolean" ? { enabled: !!fnResult } : fnResult;
|
|
148
|
+
const { enabled, context, customValidator, on = "change", setValidationResult, setDoValidate, t, } = typeof fnResult === "boolean" ? { enabled: !!fnResult } : fnResult;
|
|
141
149
|
//
|
|
142
150
|
const _t = (reason, value, fallback) => {
|
|
143
151
|
// explicit false
|
|
@@ -218,6 +226,11 @@ export function validate(el, fn) {
|
|
|
218
226
|
});
|
|
219
227
|
// });
|
|
220
228
|
};
|
|
229
|
+
// Expose the current validator to the host so it can trigger validation
|
|
230
|
+
// imperatively (e.g., on submit). The closure captures the current
|
|
231
|
+
// `enabled` / `customValidator` / `context` / `t`, so re-running the
|
|
232
|
+
// effect with new options updates the exposed function in lockstep.
|
|
233
|
+
setDoValidate?.(_doValidate);
|
|
221
234
|
el.addEventListener(on, _doValidate);
|
|
222
235
|
//
|
|
223
236
|
let _touchCount = 0;
|
|
@@ -201,6 +201,27 @@
|
|
|
201
201
|
);
|
|
202
202
|
|
|
203
203
|
let submitDisabled = $derived(code.length !== codeLength || isSubmitting);
|
|
204
|
+
|
|
205
|
+
// Imperative API ----------------------------------------------------------
|
|
206
|
+
// EmailVerifyForm uses OtpInput rather than validateAction-based fields, so
|
|
207
|
+
// "validation" here is just "is the code complete?". Exposed for parity
|
|
208
|
+
// with LoginForm / RegisterForm so consumers (and LoginOrRegisterForm) can
|
|
209
|
+
// call `.validate()` regardless of which sub-form is active.
|
|
210
|
+
|
|
211
|
+
/** Returns true when the OTP code is the expected length. */
|
|
212
|
+
export function validate(): boolean {
|
|
213
|
+
return code.length === codeLength;
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
/**
|
|
217
|
+
* Scroll the form into view if the code is incomplete. Returns true if
|
|
218
|
+
* a scroll was performed.
|
|
219
|
+
*/
|
|
220
|
+
export function scrollToFirstError(opts?: ScrollIntoViewOptions): boolean {
|
|
221
|
+
if (validate()) return false;
|
|
222
|
+
formEl?.scrollIntoView?.({ behavior: "smooth", block: "center", ...opts });
|
|
223
|
+
return true;
|
|
224
|
+
}
|
|
204
225
|
</script>
|
|
205
226
|
|
|
206
227
|
<form bind:this={formEl} class={_class} onsubmit={handleFormSubmit} {...rest}>
|
|
@@ -49,6 +49,9 @@ export interface Props extends Omit<HTMLAttributes<HTMLFormElement>, "children">
|
|
|
49
49
|
class?: string;
|
|
50
50
|
el?: HTMLFormElement;
|
|
51
51
|
}
|
|
52
|
-
declare const EmailVerifyForm: import("svelte").Component<Props, {
|
|
52
|
+
declare const EmailVerifyForm: import("svelte").Component<Props, {
|
|
53
|
+
validate: () => boolean;
|
|
54
|
+
scrollToFirstError: (opts?: ScrollIntoViewOptions) => boolean;
|
|
55
|
+
}, "el">;
|
|
53
56
|
type EmailVerifyForm = ReturnType<typeof EmailVerifyForm>;
|
|
54
57
|
export default EmailVerifyForm;
|
|
@@ -187,7 +187,8 @@
|
|
|
187
187
|
required = false,
|
|
188
188
|
disabled = false,
|
|
189
189
|
//
|
|
190
|
-
validate
|
|
190
|
+
// Renamed local binding to avoid collision with `export function validate()` below.
|
|
191
|
+
validate: validateProp,
|
|
191
192
|
//
|
|
192
193
|
labelAfter,
|
|
193
194
|
below,
|
|
@@ -251,6 +252,9 @@
|
|
|
251
252
|
let parentHiddenInputEl: HTMLInputElement | undefined = $state();
|
|
252
253
|
let hasLabel = $derived(isTHCNotEmpty(label) || typeof label === "function");
|
|
253
254
|
let inputEl = $state<HTMLInputElement>()!;
|
|
255
|
+
// Outer wrapper for scrollIntoView and focus targeting.
|
|
256
|
+
let wrapEl: HTMLDivElement | undefined = $state();
|
|
257
|
+
let hiddenInputEl: HTMLInputElement | undefined = $state();
|
|
254
258
|
let assetsPreview: AssetsPreview = $state()!;
|
|
255
259
|
|
|
256
260
|
let assets: FieldAsset[] = $derived(parseValue(value));
|
|
@@ -270,6 +274,38 @@
|
|
|
270
274
|
let validation: ValidationResult | undefined = $state();
|
|
271
275
|
const setValidationResult = (res: ValidationResult) => (validation = res);
|
|
272
276
|
|
|
277
|
+
let _doValidate: (() => void) | undefined = $state();
|
|
278
|
+
|
|
279
|
+
/** Trigger validation now. Renders the inline message if invalid. */
|
|
280
|
+
export function validate(): ValidationResult | undefined {
|
|
281
|
+
_doValidate?.();
|
|
282
|
+
return validation;
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
/** Clear the inline validation message and reset `setCustomValidity`. */
|
|
286
|
+
export function clearValidation(): void {
|
|
287
|
+
validation = undefined;
|
|
288
|
+
hiddenInputEl?.setCustomValidity?.("");
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
/** Current validation state, or undefined if validator has never run. */
|
|
292
|
+
export function getValidation(): ValidationResult | undefined {
|
|
293
|
+
return validation;
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
/**
|
|
297
|
+
* Focus the visible dropzone wrapper. The hidden file/validation inputs
|
|
298
|
+
* cannot be focused directly.
|
|
299
|
+
*/
|
|
300
|
+
export function focus(): void {
|
|
301
|
+
wrapEl?.focus?.();
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
/** Scroll the field into view. Defaults to smooth + center. */
|
|
305
|
+
export function scrollIntoView(opts?: ScrollIntoViewOptions): void {
|
|
306
|
+
wrapEl?.scrollIntoView?.({ behavior: "smooth", block: "center", ...opts });
|
|
307
|
+
}
|
|
308
|
+
|
|
273
309
|
//
|
|
274
310
|
let wrappedValidate: Omit<ValidateOptions, "setValidationResult"> = $derived({
|
|
275
311
|
enabled: true,
|
|
@@ -285,12 +321,13 @@
|
|
|
285
321
|
// normally, with other fieldtypes, we would continue with provided validator:
|
|
286
322
|
// return (validate as any)?.customValidator?.(value, context, el) || "";
|
|
287
323
|
// but not here, we just warn
|
|
288
|
-
if ((
|
|
324
|
+
if ((validateProp as any)?.customValidator) {
|
|
289
325
|
console.warn("Custom validator was provided, but is ignored in <FieldAssets />");
|
|
290
326
|
}
|
|
291
327
|
return "";
|
|
292
328
|
},
|
|
293
329
|
setValidationResult,
|
|
330
|
+
setDoValidate: (fn: () => void) => (_doValidate = fn),
|
|
294
331
|
});
|
|
295
332
|
|
|
296
333
|
//
|
|
@@ -404,6 +441,8 @@
|
|
|
404
441
|
|
|
405
442
|
<div
|
|
406
443
|
class={twMerge("w-full stuic-field-assets mb-8", classWrap)}
|
|
444
|
+
bind:this={wrapEl}
|
|
445
|
+
tabindex="-1"
|
|
407
446
|
use:highlightDragover={() => ({
|
|
408
447
|
enabled: typeof processAssets === "function",
|
|
409
448
|
classes: ["outline-dashed outline-2 outline-(--stuic-color-border)"],
|
|
@@ -516,7 +555,13 @@
|
|
|
516
555
|
|
|
517
556
|
<input type="file" bind:this={inputEl} multiple style="display: none" {accept} />
|
|
518
557
|
<!-- hack to be able to validate the conventional way -->
|
|
519
|
-
<input
|
|
558
|
+
<input
|
|
559
|
+
type="hidden"
|
|
560
|
+
{name}
|
|
561
|
+
{value}
|
|
562
|
+
bind:this={hiddenInputEl}
|
|
563
|
+
use:validateAction={() => wrappedValidate}
|
|
564
|
+
/>
|
|
520
565
|
|
|
521
566
|
<AssetsPreview
|
|
522
567
|
bind:this={assetsPreview}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { type Snippet } from "svelte";
|
|
2
|
-
import { type ValidateOptions } from "../../actions/validate.svelte.js";
|
|
2
|
+
import { type ValidateOptions, type ValidationResult } from "../../actions/validate.svelte.js";
|
|
3
3
|
import type { TranslateFn } from "../../types.js";
|
|
4
4
|
import { NotificationsStack } from "../Notifications/notifications-stack.svelte.js";
|
|
5
5
|
import { type THC } from "../Thc/Thc.svelte";
|
|
@@ -63,6 +63,12 @@ export interface Props extends InputWrapClassProps, Record<string, any> {
|
|
|
63
63
|
isLoading?: boolean;
|
|
64
64
|
classWrap?: string;
|
|
65
65
|
}
|
|
66
|
-
declare const FieldAssets: import("svelte").Component<Props, {
|
|
66
|
+
declare const FieldAssets: import("svelte").Component<Props, {
|
|
67
|
+
validate: () => ValidationResult | undefined;
|
|
68
|
+
clearValidation: () => void;
|
|
69
|
+
getValidation: () => ValidationResult | undefined;
|
|
70
|
+
focus: () => void;
|
|
71
|
+
scrollIntoView: (opts?: ScrollIntoViewOptions) => void;
|
|
72
|
+
}, "value">;
|
|
67
73
|
type FieldAssets = ReturnType<typeof FieldAssets>;
|
|
68
74
|
export default FieldAssets;
|
|
@@ -48,7 +48,8 @@
|
|
|
48
48
|
disabled,
|
|
49
49
|
renderSize = "md",
|
|
50
50
|
description,
|
|
51
|
-
validate
|
|
51
|
+
// Renamed local binding to avoid collision with `export function validate()` below.
|
|
52
|
+
validate: validateProp,
|
|
52
53
|
class: classProp,
|
|
53
54
|
classInputBox,
|
|
54
55
|
classInput,
|
|
@@ -64,6 +65,35 @@
|
|
|
64
65
|
let validation: ValidationResult | undefined = $state();
|
|
65
66
|
const setValidationResult = (res: ValidationResult) => (validation = res);
|
|
66
67
|
|
|
68
|
+
let _doValidate: (() => void) | undefined = $state();
|
|
69
|
+
|
|
70
|
+
/** Trigger validation now. Renders the inline message if invalid. */
|
|
71
|
+
export function validate(): ValidationResult | undefined {
|
|
72
|
+
_doValidate?.();
|
|
73
|
+
return validation;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/** Clear the inline validation message and reset `setCustomValidity`. */
|
|
77
|
+
export function clearValidation(): void {
|
|
78
|
+
validation = undefined;
|
|
79
|
+
input?.setCustomValidity?.("");
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/** Current validation state, or undefined if validator has never run. */
|
|
83
|
+
export function getValidation(): ValidationResult | undefined {
|
|
84
|
+
return validation;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/** Focus the checkbox element. */
|
|
88
|
+
export function focus(): void {
|
|
89
|
+
input?.focus?.();
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/** Scroll the field into view. Defaults to smooth + center. */
|
|
93
|
+
export function scrollIntoView(opts?: ScrollIntoViewOptions): void {
|
|
94
|
+
input?.scrollIntoView?.({ behavior: "smooth", block: "center", ...opts });
|
|
95
|
+
}
|
|
96
|
+
|
|
67
97
|
//
|
|
68
98
|
let invalid = $derived(validation && !validation?.valid);
|
|
69
99
|
let idDesc = getId();
|
|
@@ -99,9 +129,10 @@
|
|
|
99
129
|
aria-checked={checked}
|
|
100
130
|
aria-describedby={description ? idDesc : undefined}
|
|
101
131
|
use:validateAction={() => ({
|
|
102
|
-
enabled:
|
|
103
|
-
...(typeof
|
|
132
|
+
enabled: validateProp !== false,
|
|
133
|
+
...(typeof validateProp === "boolean" ? {} : validateProp),
|
|
104
134
|
setValidationResult,
|
|
135
|
+
setDoValidate: (fn) => (_doValidate = fn),
|
|
105
136
|
})}
|
|
106
137
|
class={twMerge(classInput)}
|
|
107
138
|
{required}
|
|
@@ -24,6 +24,13 @@ export interface Props extends HTMLInputAttributes {
|
|
|
24
24
|
classValidationBox?: string;
|
|
25
25
|
style?: string;
|
|
26
26
|
}
|
|
27
|
-
|
|
27
|
+
import { type ValidationResult } from "../../actions/validate.svelte.js";
|
|
28
|
+
declare const FieldCheckbox: import("svelte").Component<Props, {
|
|
29
|
+
validate: () => ValidationResult | undefined;
|
|
30
|
+
clearValidation: () => void;
|
|
31
|
+
getValidation: () => ValidationResult | undefined;
|
|
32
|
+
focus: () => void;
|
|
33
|
+
scrollIntoView: (opts?: ScrollIntoViewOptions) => void;
|
|
34
|
+
}, "input" | "checked">;
|
|
28
35
|
type FieldCheckbox = ReturnType<typeof FieldCheckbox>;
|
|
29
36
|
export default FieldCheckbox;
|
|
@@ -68,6 +68,7 @@
|
|
|
68
68
|
</script>
|
|
69
69
|
|
|
70
70
|
<script lang="ts">
|
|
71
|
+
import { tick } from "svelte";
|
|
71
72
|
import {
|
|
72
73
|
validate as validateAction,
|
|
73
74
|
type ValidationResult,
|
|
@@ -102,7 +103,8 @@
|
|
|
102
103
|
description,
|
|
103
104
|
class: classProp,
|
|
104
105
|
renderSize = "md",
|
|
105
|
-
validate
|
|
106
|
+
// Renamed local binding to avoid collision with `export function validate()` below.
|
|
107
|
+
validate: validateProp,
|
|
106
108
|
//
|
|
107
109
|
labelAfter,
|
|
108
110
|
inputBefore,
|
|
@@ -136,6 +138,39 @@
|
|
|
136
138
|
let validation: ValidationResult | undefined = $state();
|
|
137
139
|
const setValidationResult = (res: ValidationResult) => (validation = res);
|
|
138
140
|
|
|
141
|
+
let _doValidate: (() => void) | undefined = $state();
|
|
142
|
+
|
|
143
|
+
/** Trigger validation now. Renders the inline message if invalid. */
|
|
144
|
+
export function validate(): ValidationResult | undefined {
|
|
145
|
+
_doValidate?.();
|
|
146
|
+
return validation;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
/** Clear the inline validation message and reset `setCustomValidity`. */
|
|
150
|
+
export function clearValidation(): void {
|
|
151
|
+
validation = undefined;
|
|
152
|
+
hiddenInputEl?.setCustomValidity?.("");
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
/** Current validation state, or undefined if validator has never run. */
|
|
156
|
+
export function getValidation(): ValidationResult | undefined {
|
|
157
|
+
return validation;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
/** Focus the visible dropdown trigger button. */
|
|
161
|
+
export function focus(): void {
|
|
162
|
+
triggerEl?.focus?.();
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
/** Scroll the field into view. Defaults to smooth + center. */
|
|
166
|
+
export function scrollIntoView(opts?: ScrollIntoViewOptions): void {
|
|
167
|
+
triggerEl?.scrollIntoView?.({
|
|
168
|
+
behavior: "smooth",
|
|
169
|
+
block: "center",
|
|
170
|
+
...opts,
|
|
171
|
+
});
|
|
172
|
+
}
|
|
173
|
+
|
|
139
174
|
function localizedName(c: Country): string {
|
|
140
175
|
return countryNames?.[c.iso] ?? c.name;
|
|
141
176
|
}
|
|
@@ -174,8 +209,14 @@
|
|
|
174
209
|
onSelect: () => {
|
|
175
210
|
value = c.iso;
|
|
176
211
|
onChange?.(c.iso);
|
|
177
|
-
//
|
|
178
|
-
|
|
212
|
+
// Wait one tick so Svelte flushes the `value` binding onto the
|
|
213
|
+
// hidden input's DOM attribute BEFORE we fire the change event.
|
|
214
|
+
// Without this, the validate action reads the stale `el.value`
|
|
215
|
+
// (still empty) on the first selection — the required-check then
|
|
216
|
+
// keeps the inline error visible until the second selection.
|
|
217
|
+
tick().then(() => {
|
|
218
|
+
hiddenInputEl?.dispatchEvent(new Event("change", { bubbles: true }));
|
|
219
|
+
});
|
|
179
220
|
},
|
|
180
221
|
};
|
|
181
222
|
}
|
|
@@ -308,19 +349,35 @@
|
|
|
308
349
|
</DropdownMenu>
|
|
309
350
|
</InputWrap>
|
|
310
351
|
|
|
311
|
-
<!-- Hidden input for form submission + validation
|
|
312
|
-
|
|
352
|
+
<!-- Hidden input for form submission + validation.
|
|
353
|
+
Rendered whenever validation is enabled (default) OR `name` is set, so
|
|
354
|
+
imperative `validate()` works even when the field is used outside a
|
|
355
|
+
<form> / without a name. A hidden input without `name` is skipped by
|
|
356
|
+
FormData per the HTML spec, so this is invisible to form submission. -->
|
|
357
|
+
{#if name || validateProp !== false}
|
|
313
358
|
<input
|
|
314
359
|
type="hidden"
|
|
315
360
|
{name}
|
|
316
361
|
value={value ?? ""}
|
|
317
362
|
bind:this={hiddenInputEl}
|
|
318
363
|
use:validateAction={() => {
|
|
319
|
-
const customOpts =
|
|
364
|
+
const customOpts =
|
|
365
|
+
typeof validateProp === "object" && validateProp ? validateProp : {};
|
|
366
|
+
const userValidator = customOpts.customValidator;
|
|
320
367
|
return {
|
|
321
|
-
enabled:
|
|
368
|
+
enabled: validateProp !== false,
|
|
322
369
|
...customOpts,
|
|
370
|
+
// Hidden inputs are barred from native constraint validation, so
|
|
371
|
+
// `required` on the element itself is a no-op. We enforce it here
|
|
372
|
+
// instead, then fall through to the consumer's customValidator.
|
|
373
|
+
customValidator(val, ctx, el) {
|
|
374
|
+
if (required && (val == null || val === "")) {
|
|
375
|
+
return "This field requires attention. Please review and try again.";
|
|
376
|
+
}
|
|
377
|
+
return userValidator?.(val, ctx, el) || "";
|
|
378
|
+
},
|
|
323
379
|
setValidationResult,
|
|
380
|
+
setDoValidate: (fn) => (_doValidate = fn),
|
|
324
381
|
};
|
|
325
382
|
}}
|
|
326
383
|
{required}
|
|
@@ -54,6 +54,13 @@ export interface Props extends InputWrapClassProps, Record<string, any> {
|
|
|
54
54
|
style?: string;
|
|
55
55
|
t?: TranslateFn;
|
|
56
56
|
}
|
|
57
|
-
|
|
57
|
+
import { type ValidationResult } from "../../actions/validate.svelte.js";
|
|
58
|
+
declare const FieldCountry: import("svelte").Component<Props, {
|
|
59
|
+
validate: () => ValidationResult | undefined;
|
|
60
|
+
clearValidation: () => void;
|
|
61
|
+
getValidation: () => ValidationResult | undefined;
|
|
62
|
+
focus: () => void;
|
|
63
|
+
scrollIntoView: (opts?: ScrollIntoViewOptions) => void;
|
|
64
|
+
}, "value">;
|
|
58
65
|
type FieldCountry = ReturnType<typeof FieldCountry>;
|
|
59
66
|
export default FieldCountry;
|
|
@@ -64,7 +64,8 @@
|
|
|
64
64
|
required = false,
|
|
65
65
|
disabled = false,
|
|
66
66
|
//
|
|
67
|
-
validate
|
|
67
|
+
// Renamed local binding to avoid collision with `export function validate()` below.
|
|
68
|
+
validate: validateProp,
|
|
68
69
|
//
|
|
69
70
|
labelAfter,
|
|
70
71
|
inputBefore,
|
|
@@ -95,6 +96,35 @@
|
|
|
95
96
|
let validation: ValidationResult | undefined = $state();
|
|
96
97
|
const setValidationResult = (res: ValidationResult) => (validation = res);
|
|
97
98
|
|
|
99
|
+
let _doValidate: (() => void) | undefined = $state();
|
|
100
|
+
|
|
101
|
+
/** Trigger validation now. Renders the inline message if invalid. */
|
|
102
|
+
export function validate(): ValidationResult | undefined {
|
|
103
|
+
_doValidate?.();
|
|
104
|
+
return validation;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/** Clear the inline validation message and reset `setCustomValidity`. */
|
|
108
|
+
export function clearValidation(): void {
|
|
109
|
+
validation = undefined;
|
|
110
|
+
input?.setCustomValidity?.("");
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/** Current validation state, or undefined if validator has never run. */
|
|
114
|
+
export function getValidation(): ValidationResult | undefined {
|
|
115
|
+
return validation;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
/** Focus the underlying `<input type="file">` element. */
|
|
119
|
+
export function focus(): void {
|
|
120
|
+
input?.focus?.();
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
/** Scroll the field into view. Defaults to smooth + center. */
|
|
124
|
+
export function scrollIntoView(opts?: ScrollIntoViewOptions): void {
|
|
125
|
+
input?.scrollIntoView?.({ behavior: "smooth", block: "center", ...opts });
|
|
126
|
+
}
|
|
127
|
+
|
|
98
128
|
// $inspect(files);
|
|
99
129
|
</script>
|
|
100
130
|
|
|
@@ -134,9 +164,10 @@
|
|
|
134
164
|
class={twMerge("block w-full", classInput)}
|
|
135
165
|
use:highlightDragover={() => ({ classes: ["outline-dashed"] })}
|
|
136
166
|
use:validateAction={() => ({
|
|
137
|
-
enabled:
|
|
138
|
-
...(typeof
|
|
167
|
+
enabled: validateProp !== false,
|
|
168
|
+
...(typeof validateProp === "boolean" ? {} : validateProp),
|
|
139
169
|
setValidationResult,
|
|
170
|
+
setDoValidate: (fn) => (_doValidate = fn),
|
|
140
171
|
})}
|
|
141
172
|
{multiple}
|
|
142
173
|
{tabindex}
|
|
@@ -33,6 +33,13 @@ export interface Props extends HTMLInputAttributes, InputWrapClassProps, Record<
|
|
|
33
33
|
classFileList?: string;
|
|
34
34
|
style?: string;
|
|
35
35
|
}
|
|
36
|
-
|
|
36
|
+
import { type ValidationResult } from "../../actions/validate.svelte.js";
|
|
37
|
+
declare const FieldFile: import("svelte").Component<Props, {
|
|
38
|
+
validate: () => ValidationResult | undefined;
|
|
39
|
+
clearValidation: () => void;
|
|
40
|
+
getValidation: () => ValidationResult | undefined;
|
|
41
|
+
focus: () => void;
|
|
42
|
+
scrollIntoView: (opts?: ScrollIntoViewOptions) => void;
|
|
43
|
+
}, "input" | "files">;
|
|
37
44
|
type FieldFile = ReturnType<typeof FieldFile>;
|
|
38
45
|
export default FieldFile;
|
|
@@ -67,7 +67,10 @@
|
|
|
67
67
|
required = false,
|
|
68
68
|
disabled = false,
|
|
69
69
|
//
|
|
70
|
-
validate
|
|
70
|
+
// Renamed binding because `validate` is also the name of the exported
|
|
71
|
+
// imperative method below — destructuring under the same name would
|
|
72
|
+
// collide with `export function validate()` in the script scope.
|
|
73
|
+
validate: validateProp,
|
|
71
74
|
//
|
|
72
75
|
labelAfter,
|
|
73
76
|
inputBefore,
|
|
@@ -97,6 +100,42 @@
|
|
|
97
100
|
//
|
|
98
101
|
let validation: ValidationResult | undefined = $state();
|
|
99
102
|
const setValidationResult = (res: ValidationResult) => (validation = res);
|
|
103
|
+
|
|
104
|
+
// Imperative API. The validate action re-assigns `_doValidate` on every
|
|
105
|
+
// $effect run (whenever options change), so the exported `validate()`
|
|
106
|
+
// always invokes the current closure.
|
|
107
|
+
let _doValidate: (() => void) | undefined = $state();
|
|
108
|
+
|
|
109
|
+
/**
|
|
110
|
+
* Trigger validation now. Same code path as the change/blur listeners.
|
|
111
|
+
* Renders the inline validation message if invalid. Useful from submit
|
|
112
|
+
* handlers where the user may not have touched every field.
|
|
113
|
+
*/
|
|
114
|
+
export function validate(): ValidationResult | undefined {
|
|
115
|
+
_doValidate?.();
|
|
116
|
+
return validation;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
/** Clear the inline validation message and reset `setCustomValidity`. */
|
|
120
|
+
export function clearValidation(): void {
|
|
121
|
+
validation = undefined;
|
|
122
|
+
input?.setCustomValidity?.("");
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
/** Current validation state, or undefined if validator has never run. */
|
|
126
|
+
export function getValidation(): ValidationResult | undefined {
|
|
127
|
+
return validation;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
/** Focus the underlying `<input>` element. */
|
|
131
|
+
export function focus(): void {
|
|
132
|
+
input?.focus?.();
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
/** Scroll the field into view. Defaults to smooth + center. */
|
|
136
|
+
export function scrollIntoView(opts?: ScrollIntoViewOptions): void {
|
|
137
|
+
input?.scrollIntoView?.({ behavior: "smooth", block: "center", ...opts });
|
|
138
|
+
}
|
|
100
139
|
</script>
|
|
101
140
|
|
|
102
141
|
<InputWrap
|
|
@@ -142,9 +181,10 @@
|
|
|
142
181
|
...(typeof useTypeahead === "boolean" ? {} : useTypeahead),
|
|
143
182
|
})}
|
|
144
183
|
use:validateAction={() => ({
|
|
145
|
-
enabled:
|
|
146
|
-
...(typeof
|
|
184
|
+
enabled: validateProp !== false,
|
|
185
|
+
...(typeof validateProp === "boolean" ? {} : validateProp),
|
|
147
186
|
setValidationResult,
|
|
187
|
+
setDoValidate: (fn) => (_doValidate = fn),
|
|
148
188
|
})}
|
|
149
189
|
{tabindex}
|
|
150
190
|
{required}
|
|
@@ -34,6 +34,13 @@ export interface Props extends HTMLInputAttributes, InputWrapClassProps, Record<
|
|
|
34
34
|
classInput?: string;
|
|
35
35
|
style?: string;
|
|
36
36
|
}
|
|
37
|
-
|
|
37
|
+
import { type ValidationResult } from "../../actions/validate.svelte.js";
|
|
38
|
+
declare const FieldInput: import("svelte").Component<Props, {
|
|
39
|
+
validate: () => ValidationResult | undefined;
|
|
40
|
+
clearValidation: () => void;
|
|
41
|
+
getValidation: () => ValidationResult | undefined;
|
|
42
|
+
focus: () => void;
|
|
43
|
+
scrollIntoView: (opts?: ScrollIntoViewOptions) => void;
|
|
44
|
+
}, "value" | "input">;
|
|
38
45
|
type FieldInput = ReturnType<typeof FieldInput>;
|
|
39
46
|
export default FieldInput;
|