@marianmeres/stuic 3.86.0 → 3.88.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/API.md +41 -0
- 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/components/UserAvatarMenu/README.md +188 -0
- package/dist/components/UserAvatarMenu/UserAvatarMenu.svelte +416 -0
- package/dist/components/UserAvatarMenu/UserAvatarMenu.svelte.d.ts +143 -0
- package/dist/components/UserAvatarMenu/index.css +95 -0
- package/dist/components/UserAvatarMenu/index.d.ts +1 -0
- package/dist/components/UserAvatarMenu/index.js +1 -0
- package/dist/icons/index.d.ts +3 -0
- package/dist/icons/index.js +3 -0
- package/dist/index.css +1 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.js +1 -0
- 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 +190 -0
- package/docs/domains/utils.md +38 -0
- package/package.json +1 -1
|
@@ -19,6 +19,13 @@ export interface Props {
|
|
|
19
19
|
classValidationBox?: string;
|
|
20
20
|
style?: string;
|
|
21
21
|
}
|
|
22
|
-
|
|
22
|
+
import type { ValidationResult } from "../../actions/validate.svelte.js";
|
|
23
|
+
declare const FieldRadios: import("svelte").Component<Props, {
|
|
24
|
+
validate: () => ValidationResult | undefined;
|
|
25
|
+
clearValidation: () => void;
|
|
26
|
+
getValidation: () => ValidationResult | undefined;
|
|
27
|
+
focus: () => void;
|
|
28
|
+
scrollIntoView: (opts?: ScrollIntoViewOptions) => void;
|
|
29
|
+
}, "value">;
|
|
23
30
|
type FieldRadios = ReturnType<typeof FieldRadios>;
|
|
24
31
|
export default FieldRadios;
|
|
@@ -60,7 +60,8 @@
|
|
|
60
60
|
required = false,
|
|
61
61
|
disabled = false,
|
|
62
62
|
//
|
|
63
|
-
validate
|
|
63
|
+
// Renamed local binding to avoid collision with `export function validate()` below.
|
|
64
|
+
validate: validateProp,
|
|
64
65
|
//
|
|
65
66
|
labelAfter,
|
|
66
67
|
inputBefore,
|
|
@@ -91,6 +92,35 @@
|
|
|
91
92
|
let validation: ValidationResult | undefined = $state();
|
|
92
93
|
const setValidationResult = (res: ValidationResult) => (validation = res);
|
|
93
94
|
|
|
95
|
+
let _doValidate: (() => void) | undefined = $state();
|
|
96
|
+
|
|
97
|
+
/** Trigger validation now. Renders the inline message if invalid. */
|
|
98
|
+
export function validate(): ValidationResult | undefined {
|
|
99
|
+
_doValidate?.();
|
|
100
|
+
return validation;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/** Clear the inline validation message and reset `setCustomValidity`. */
|
|
104
|
+
export function clearValidation(): void {
|
|
105
|
+
validation = undefined;
|
|
106
|
+
input?.setCustomValidity?.("");
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/** Current validation state, or undefined if validator has never run. */
|
|
110
|
+
export function getValidation(): ValidationResult | undefined {
|
|
111
|
+
return validation;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/** Focus the underlying `<select>` element. */
|
|
115
|
+
export function focus(): void {
|
|
116
|
+
input?.focus?.();
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
/** Scroll the field into view. Defaults to smooth + center. */
|
|
120
|
+
export function scrollIntoView(opts?: ScrollIntoViewOptions): void {
|
|
121
|
+
input?.scrollIntoView?.({ behavior: "smooth", block: "center", ...opts });
|
|
122
|
+
}
|
|
123
|
+
|
|
94
124
|
const _normalizeAndGroupOptions = (opts: (string | FieldSelectOption)[]) => {
|
|
95
125
|
const groupped = new Map<string, FieldSelectOption[]>();
|
|
96
126
|
opts.forEach((v) => {
|
|
@@ -140,9 +170,10 @@
|
|
|
140
170
|
{id}
|
|
141
171
|
class={twMerge(classInput)}
|
|
142
172
|
use:validateAction={() => ({
|
|
143
|
-
enabled:
|
|
144
|
-
...(typeof
|
|
173
|
+
enabled: validateProp !== false,
|
|
174
|
+
...(typeof validateProp === "boolean" ? {} : validateProp),
|
|
145
175
|
setValidationResult,
|
|
176
|
+
setDoValidate: (fn) => (_doValidate = fn),
|
|
146
177
|
})}
|
|
147
178
|
{tabindex}
|
|
148
179
|
{required}
|
|
@@ -31,6 +31,13 @@ export interface Props extends HTMLSelectAttributes, InputWrapClassProps {
|
|
|
31
31
|
classInput?: string;
|
|
32
32
|
style?: string;
|
|
33
33
|
}
|
|
34
|
-
|
|
34
|
+
import { type ValidationResult } from "../../actions/validate.svelte.js";
|
|
35
|
+
declare const FieldSelect: import("svelte").Component<Props, {
|
|
36
|
+
validate: () => ValidationResult | undefined;
|
|
37
|
+
clearValidation: () => void;
|
|
38
|
+
getValidation: () => ValidationResult | undefined;
|
|
39
|
+
focus: () => void;
|
|
40
|
+
scrollIntoView: (opts?: ScrollIntoViewOptions) => void;
|
|
41
|
+
}, "value" | "input">;
|
|
35
42
|
type FieldSelect = ReturnType<typeof FieldSelect>;
|
|
36
43
|
export default FieldSelect;
|
|
@@ -58,7 +58,8 @@
|
|
|
58
58
|
required = false,
|
|
59
59
|
disabled = false,
|
|
60
60
|
//
|
|
61
|
-
validate
|
|
61
|
+
// Renamed local binding to avoid collision with `export function validate()` below.
|
|
62
|
+
validate: validateProp,
|
|
62
63
|
//
|
|
63
64
|
labelAfter,
|
|
64
65
|
inputBefore,
|
|
@@ -90,6 +91,36 @@
|
|
|
90
91
|
//
|
|
91
92
|
let validation: ValidationResult | undefined = $state();
|
|
92
93
|
const setValidationResult = (res: ValidationResult) => (validation = res);
|
|
94
|
+
|
|
95
|
+
// Delegate the imperative API to the inner Switch.
|
|
96
|
+
let switchRef: Switch | undefined = $state();
|
|
97
|
+
|
|
98
|
+
/** Trigger validation now. Renders the inline message if invalid. */
|
|
99
|
+
export function validate(): ValidationResult | undefined {
|
|
100
|
+
switchRef?.validate();
|
|
101
|
+
return validation;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/** Clear the inline validation message. */
|
|
105
|
+
export function clearValidation(): void {
|
|
106
|
+
switchRef?.clearValidation?.();
|
|
107
|
+
validation = undefined;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/** Current validation state. */
|
|
111
|
+
export function getValidation(): ValidationResult | undefined {
|
|
112
|
+
return validation;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/** Focus the visual switch. */
|
|
116
|
+
export function focus(): void {
|
|
117
|
+
switchRef?.focus?.();
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
/** Scroll the field into view. */
|
|
121
|
+
export function scrollIntoView(opts?: ScrollIntoViewOptions): void {
|
|
122
|
+
switchRef?.scrollIntoView?.(opts);
|
|
123
|
+
}
|
|
93
124
|
</script>
|
|
94
125
|
|
|
95
126
|
<InputWrap
|
|
@@ -119,5 +150,13 @@
|
|
|
119
150
|
classInputBoxWrap={twMerge("input-wrap-transparent", classInputBoxWrap)}
|
|
120
151
|
{style}
|
|
121
152
|
>
|
|
122
|
-
<Switch
|
|
153
|
+
<Switch
|
|
154
|
+
bind:this={switchRef}
|
|
155
|
+
bind:checked
|
|
156
|
+
{name}
|
|
157
|
+
{required}
|
|
158
|
+
{disabled}
|
|
159
|
+
validate={validateProp}
|
|
160
|
+
{setValidationResult}
|
|
161
|
+
/>
|
|
123
162
|
</InputWrap>
|
|
@@ -32,6 +32,13 @@ export interface Props extends InputWrapClassProps, Record<string, any> {
|
|
|
32
32
|
style?: string;
|
|
33
33
|
renderValue?: (rawValue: any) => string;
|
|
34
34
|
}
|
|
35
|
-
|
|
35
|
+
import type { ValidationResult } from "../../actions/validate.svelte.js";
|
|
36
|
+
declare const FieldSwitch: import("svelte").Component<Props, {
|
|
37
|
+
validate: () => ValidationResult | undefined;
|
|
38
|
+
clearValidation: () => void;
|
|
39
|
+
getValidation: () => ValidationResult | undefined;
|
|
40
|
+
focus: () => void;
|
|
41
|
+
scrollIntoView: (opts?: ScrollIntoViewOptions) => void;
|
|
42
|
+
}, "input" | "checked">;
|
|
36
43
|
type FieldSwitch = ReturnType<typeof FieldSwitch>;
|
|
37
44
|
export default FieldSwitch;
|
|
@@ -61,7 +61,8 @@
|
|
|
61
61
|
required = false,
|
|
62
62
|
disabled = false,
|
|
63
63
|
//
|
|
64
|
-
validate
|
|
64
|
+
// Renamed local binding to avoid collision with `export function validate()` below.
|
|
65
|
+
validate: validateProp,
|
|
65
66
|
//
|
|
66
67
|
labelAfter,
|
|
67
68
|
inputBefore,
|
|
@@ -91,6 +92,35 @@
|
|
|
91
92
|
//
|
|
92
93
|
let validation: ValidationResult | undefined = $state();
|
|
93
94
|
const setValidationResult = (res: ValidationResult) => (validation = res);
|
|
95
|
+
|
|
96
|
+
let _doValidate: (() => void) | undefined = $state();
|
|
97
|
+
|
|
98
|
+
/** Trigger validation now. Renders the inline message if invalid. */
|
|
99
|
+
export function validate(): ValidationResult | undefined {
|
|
100
|
+
_doValidate?.();
|
|
101
|
+
return validation;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/** Clear the inline validation message and reset `setCustomValidity`. */
|
|
105
|
+
export function clearValidation(): void {
|
|
106
|
+
validation = undefined;
|
|
107
|
+
input?.setCustomValidity?.("");
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/** Current validation state, or undefined if validator has never run. */
|
|
111
|
+
export function getValidation(): ValidationResult | undefined {
|
|
112
|
+
return validation;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/** Focus the underlying `<textarea>` element. */
|
|
116
|
+
export function focus(): void {
|
|
117
|
+
input?.focus?.();
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
/** Scroll the field into view. Defaults to smooth + center. */
|
|
121
|
+
export function scrollIntoView(opts?: ScrollIntoViewOptions): void {
|
|
122
|
+
input?.scrollIntoView?.({ behavior: "smooth", block: "center", ...opts });
|
|
123
|
+
}
|
|
94
124
|
</script>
|
|
95
125
|
|
|
96
126
|
<InputWrap
|
|
@@ -130,9 +160,10 @@
|
|
|
130
160
|
setValue: (v: string) => (value = v),
|
|
131
161
|
})}
|
|
132
162
|
use:validateAction={() => ({
|
|
133
|
-
enabled:
|
|
134
|
-
...(typeof
|
|
163
|
+
enabled: validateProp !== false,
|
|
164
|
+
...(typeof validateProp === "boolean" ? {} : validateProp),
|
|
135
165
|
setValidationResult,
|
|
166
|
+
setDoValidate: (fn) => (_doValidate = fn),
|
|
136
167
|
})}
|
|
137
168
|
use:autogrow={() => ({
|
|
138
169
|
enabled: !!useAutogrow,
|
|
@@ -35,6 +35,13 @@ export interface Props extends HTMLTextareaAttributes, InputWrapClassProps {
|
|
|
35
35
|
classInput?: string;
|
|
36
36
|
style?: string;
|
|
37
37
|
}
|
|
38
|
-
|
|
38
|
+
import { type ValidationResult } from "../../actions/validate.svelte.js";
|
|
39
|
+
declare const FieldTextarea: import("svelte").Component<Props, {
|
|
40
|
+
validate: () => ValidationResult | undefined;
|
|
41
|
+
clearValidation: () => void;
|
|
42
|
+
getValidation: () => ValidationResult | undefined;
|
|
43
|
+
focus: () => void;
|
|
44
|
+
scrollIntoView: (opts?: ScrollIntoViewOptions) => void;
|
|
45
|
+
}, "value" | "input">;
|
|
39
46
|
type FieldTextarea = ReturnType<typeof FieldTextarea>;
|
|
40
47
|
export default FieldTextarea;
|
|
@@ -48,7 +48,8 @@
|
|
|
48
48
|
description,
|
|
49
49
|
renderSize,
|
|
50
50
|
tabindex,
|
|
51
|
-
validate
|
|
51
|
+
// Renamed local binding to avoid collision with `export function validate()` below.
|
|
52
|
+
validate: validateProp,
|
|
52
53
|
classRadioBox,
|
|
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 (radio-group level). */
|
|
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. */
|
|
83
|
+
export function getValidation(): ValidationResult | undefined {
|
|
84
|
+
return validation;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/** Focus the radio input. */
|
|
88
|
+
export function focus(): void {
|
|
89
|
+
input?.focus?.();
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/** Scroll the radio into view. */
|
|
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 id = getId();
|
|
@@ -97,9 +127,10 @@
|
|
|
97
127
|
class={twMerge(classInput)}
|
|
98
128
|
aria-describedby={description ? idDesc : undefined}
|
|
99
129
|
use:validateAction={() => ({
|
|
100
|
-
enabled:
|
|
101
|
-
...(typeof
|
|
130
|
+
enabled: validateProp !== false,
|
|
131
|
+
...(typeof validateProp === "boolean" ? {} : validateProp),
|
|
102
132
|
setValidationResult,
|
|
133
|
+
setDoValidate: (fn) => (_doValidate = fn),
|
|
103
134
|
})}
|
|
104
135
|
{required}
|
|
105
136
|
{disabled}
|
|
@@ -25,6 +25,12 @@ interface Props extends HTMLInputAttributes {
|
|
|
25
25
|
classValidationBox?: string;
|
|
26
26
|
validation: ValidationResult | undefined;
|
|
27
27
|
}
|
|
28
|
-
declare const FieldRadioInternal: import("svelte").Component<Props, {
|
|
28
|
+
declare const FieldRadioInternal: 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" | "group" | "validation">;
|
|
29
35
|
type FieldRadioInternal = ReturnType<typeof FieldRadioInternal>;
|
|
30
36
|
export default FieldRadioInternal;
|
|
@@ -81,6 +81,10 @@
|
|
|
81
81
|
import { onSubmitValidityCheck } from "../../actions/on-submit-validity-check.svelte.js";
|
|
82
82
|
import { tooltip } from "../../actions/tooltip/tooltip.svelte.js";
|
|
83
83
|
import { twMerge } from "../../utils/tw-merge.js";
|
|
84
|
+
import {
|
|
85
|
+
scrollToFirstInvalidField,
|
|
86
|
+
validateAllFields,
|
|
87
|
+
} from "../../utils/validate-fields.js";
|
|
84
88
|
import Button from "../Button/Button.svelte";
|
|
85
89
|
import DismissibleMessage from "../DismissibleMessage/DismissibleMessage.svelte";
|
|
86
90
|
import FieldCheckbox from "../Input/FieldCheckbox.svelte";
|
|
@@ -212,6 +216,35 @@
|
|
|
212
216
|
});
|
|
213
217
|
|
|
214
218
|
let _class = $derived(unstyled ? classProp : twMerge("stuic-login-form", classProp));
|
|
219
|
+
|
|
220
|
+
// Imperative API ----------------------------------------------------------
|
|
221
|
+
// Field refs collected during render so consumers can trigger validation
|
|
222
|
+
// without waiting for native form submission (e.g., from a custom button
|
|
223
|
+
// outside this component, or to pre-render server-error messages).
|
|
224
|
+
let emailField = $state<FieldInput>();
|
|
225
|
+
let passwordField = $state<FieldInput>();
|
|
226
|
+
|
|
227
|
+
function _fields() {
|
|
228
|
+
return [emailField, passwordField];
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
/**
|
|
232
|
+
* Run every field's validator and render any inline errors. Returns true
|
|
233
|
+
* if all fields are valid. Useful from custom submit handlers.
|
|
234
|
+
*/
|
|
235
|
+
export function validate(): boolean {
|
|
236
|
+
return validateAllFields(_fields());
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
/**
|
|
240
|
+
* Scroll the first invalid field into view and focus it. Returns true
|
|
241
|
+
* if a field was scrolled. Call after `validate()`.
|
|
242
|
+
*/
|
|
243
|
+
export function scrollToFirstError(
|
|
244
|
+
opts?: Parameters<typeof scrollToFirstInvalidField>[1]
|
|
245
|
+
): boolean {
|
|
246
|
+
return scrollToFirstInvalidField(_fields(), opts);
|
|
247
|
+
}
|
|
215
248
|
</script>
|
|
216
249
|
|
|
217
250
|
<form bind:this={formEl} class={_class} use:onSubmitValidityCheck {...rest}>
|
|
@@ -226,6 +259,7 @@
|
|
|
226
259
|
<!-- Email -->
|
|
227
260
|
<!-- svelte-ignore binding_property_non_reactive -->
|
|
228
261
|
<FieldInput
|
|
262
|
+
bind:this={emailField}
|
|
229
263
|
bind:value={formData.email}
|
|
230
264
|
label={t("login_form.email_label")}
|
|
231
265
|
type="email"
|
|
@@ -244,6 +278,7 @@
|
|
|
244
278
|
<!-- Password -->
|
|
245
279
|
<!-- svelte-ignore binding_property_non_reactive -->
|
|
246
280
|
<FieldInput
|
|
281
|
+
bind:this={passwordField}
|
|
247
282
|
bind:value={formData.password}
|
|
248
283
|
label={t("login_form.password_label")}
|
|
249
284
|
autocomplete="current-password"
|
|
@@ -58,6 +58,10 @@ export interface Props extends Omit<HTMLAttributes<HTMLFormElement>, "children">
|
|
|
58
58
|
class?: string;
|
|
59
59
|
el?: HTMLFormElement;
|
|
60
60
|
}
|
|
61
|
-
|
|
61
|
+
import { scrollToFirstInvalidField } from "../../utils/validate-fields.js";
|
|
62
|
+
declare const LoginForm: import("svelte").Component<Props, {
|
|
63
|
+
validate: () => boolean;
|
|
64
|
+
scrollToFirstError: (opts?: Parameters<typeof scrollToFirstInvalidField>[1]) => boolean;
|
|
65
|
+
}, "el" | "formData">;
|
|
62
66
|
type LoginForm = ReturnType<typeof LoginForm>;
|
|
63
67
|
export default LoginForm;
|
|
@@ -141,6 +141,7 @@
|
|
|
141
141
|
import { createEmptyRegisterFormData } from "../RegisterForm/_internal/register-form-utils.js";
|
|
142
142
|
import EmailVerifyForm from "../EmailVerifyForm/EmailVerifyForm.svelte";
|
|
143
143
|
import ButtonGroupRadio from "../ButtonGroupRadio/ButtonGroupRadio.svelte";
|
|
144
|
+
import type { scrollToFirstInvalidField } from "../../utils/validate-fields.js";
|
|
144
145
|
|
|
145
146
|
let {
|
|
146
147
|
mode = $bindable("login"),
|
|
@@ -212,6 +213,42 @@
|
|
|
212
213
|
let _class = $derived(
|
|
213
214
|
unstyled ? classProp : twMerge("stuic-login-or-register-form", classProp)
|
|
214
215
|
);
|
|
216
|
+
|
|
217
|
+
// Imperative API ----------------------------------------------------------
|
|
218
|
+
// Refs to the currently rendered inner form. Only one is mounted at a time
|
|
219
|
+
// (Svelte tears down the others), so `validate()` just asks the active one.
|
|
220
|
+
let loginFormRef = $state<LoginForm>();
|
|
221
|
+
let registerFormRef = $state<RegisterForm>();
|
|
222
|
+
let verifyFormRef = $state<EmailVerifyForm>();
|
|
223
|
+
|
|
224
|
+
function _activeForm(): {
|
|
225
|
+
validate?(): boolean;
|
|
226
|
+
scrollToFirstError?(opts?: Parameters<typeof scrollToFirstInvalidField>[1]): boolean;
|
|
227
|
+
} | undefined {
|
|
228
|
+
if (mode === "login") return loginFormRef;
|
|
229
|
+
if (mode === "register") return registerFormRef;
|
|
230
|
+
if (mode === "verify") return verifyFormRef;
|
|
231
|
+
return undefined;
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
/**
|
|
235
|
+
* Run validation on the active inner form (login / register / verify).
|
|
236
|
+
* Returns true if valid, false if any field is invalid. No-op if no form
|
|
237
|
+
* is mounted yet.
|
|
238
|
+
*/
|
|
239
|
+
export function validate(): boolean {
|
|
240
|
+
return _activeForm()?.validate?.() ?? true;
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
/**
|
|
244
|
+
* Scroll the first invalid field of the active inner form into view.
|
|
245
|
+
* Returns true if a field was scrolled.
|
|
246
|
+
*/
|
|
247
|
+
export function scrollToFirstError(
|
|
248
|
+
opts?: Parameters<typeof scrollToFirstInvalidField>[1]
|
|
249
|
+
): boolean {
|
|
250
|
+
return _activeForm()?.scrollToFirstError?.(opts) ?? false;
|
|
251
|
+
}
|
|
215
252
|
</script>
|
|
216
253
|
|
|
217
254
|
<div class={_class} {...rest}>
|
|
@@ -238,6 +275,7 @@
|
|
|
238
275
|
{#if mode === "login"}
|
|
239
276
|
<!-- svelte-ignore binding_property_non_reactive -->
|
|
240
277
|
<LoginForm
|
|
278
|
+
bind:this={loginFormRef}
|
|
241
279
|
bind:formData={loginData}
|
|
242
280
|
onSubmit={onLogin}
|
|
243
281
|
{isSubmitting}
|
|
@@ -249,6 +287,7 @@
|
|
|
249
287
|
{:else if mode === "register"}
|
|
250
288
|
<!-- svelte-ignore binding_property_non_reactive -->
|
|
251
289
|
<RegisterForm
|
|
290
|
+
bind:this={registerFormRef}
|
|
252
291
|
bind:formData={registerData}
|
|
253
292
|
onSubmit={onRegister}
|
|
254
293
|
{isSubmitting}
|
|
@@ -258,6 +297,7 @@
|
|
|
258
297
|
/>
|
|
259
298
|
{:else}
|
|
260
299
|
<EmailVerifyForm
|
|
300
|
+
bind:this={verifyFormRef}
|
|
261
301
|
email={verifyEmail || registerData.email || loginData.email}
|
|
262
302
|
onSubmit={(code) => onVerify?.(code)}
|
|
263
303
|
onResend={onResendCode}
|
|
@@ -89,6 +89,10 @@ export interface Props extends Omit<HTMLAttributes<HTMLDivElement>, "children">
|
|
|
89
89
|
unstyled?: boolean;
|
|
90
90
|
class?: string;
|
|
91
91
|
}
|
|
92
|
-
|
|
92
|
+
import type { scrollToFirstInvalidField } from "../../utils/validate-fields.js";
|
|
93
|
+
declare const LoginOrRegisterForm: import("svelte").Component<Props, {
|
|
94
|
+
validate: () => boolean;
|
|
95
|
+
scrollToFirstError: (opts?: Parameters<typeof scrollToFirstInvalidField>[1]) => boolean;
|
|
96
|
+
}, "mode" | "loginData" | "registerData" | "verifyEmail">;
|
|
93
97
|
type LoginOrRegisterForm = ReturnType<typeof LoginOrRegisterForm>;
|
|
94
98
|
export default LoginOrRegisterForm;
|
|
@@ -106,6 +106,10 @@
|
|
|
106
106
|
import DismissibleMessage from "../DismissibleMessage/DismissibleMessage.svelte";
|
|
107
107
|
import FieldInput from "../Input/FieldInput.svelte";
|
|
108
108
|
import { onSubmitValidityCheck } from "../../actions/on-submit-validity-check.svelte.js";
|
|
109
|
+
import {
|
|
110
|
+
scrollToFirstInvalidField,
|
|
111
|
+
validateAllFields,
|
|
112
|
+
} from "../../utils/validate-fields.js";
|
|
109
113
|
|
|
110
114
|
let {
|
|
111
115
|
formData = $bindable(createEmptyRegisterFormData()),
|
|
@@ -216,6 +220,41 @@
|
|
|
216
220
|
});
|
|
217
221
|
|
|
218
222
|
let _class = $derived(unstyled ? classProp : twMerge("stuic-register-form", classProp));
|
|
223
|
+
|
|
224
|
+
// Imperative API ----------------------------------------------------------
|
|
225
|
+
let topFieldRefs: (FieldInput | undefined)[] = $state([]);
|
|
226
|
+
let bottomFieldRefs: (FieldInput | undefined)[] = $state([]);
|
|
227
|
+
let emailField = $state<FieldInput>();
|
|
228
|
+
let passwordField = $state<FieldInput>();
|
|
229
|
+
let passwordConfirmField = $state<FieldInput>();
|
|
230
|
+
|
|
231
|
+
function _fields() {
|
|
232
|
+
return [
|
|
233
|
+
...topFieldRefs,
|
|
234
|
+
emailField,
|
|
235
|
+
passwordField,
|
|
236
|
+
...(showPasswordConfirm ? [passwordConfirmField] : []),
|
|
237
|
+
...bottomFieldRefs,
|
|
238
|
+
];
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
/**
|
|
242
|
+
* Run every field's validator and render any inline errors. Returns true
|
|
243
|
+
* if all fields are valid. Useful from custom submit handlers.
|
|
244
|
+
*/
|
|
245
|
+
export function validate(): boolean {
|
|
246
|
+
return validateAllFields(_fields());
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
/**
|
|
250
|
+
* Scroll the first invalid field into view and focus it. Returns true
|
|
251
|
+
* if a field was scrolled. Call after `validate()`.
|
|
252
|
+
*/
|
|
253
|
+
export function scrollToFirstError(
|
|
254
|
+
opts?: Parameters<typeof scrollToFirstInvalidField>[1]
|
|
255
|
+
): boolean {
|
|
256
|
+
return scrollToFirstInvalidField(_fields(), opts);
|
|
257
|
+
}
|
|
219
258
|
</script>
|
|
220
259
|
|
|
221
260
|
<form bind:this={formEl} class={_class} use:onSubmitValidityCheck {...rest}>
|
|
@@ -223,8 +262,9 @@
|
|
|
223
262
|
<DismissibleMessage message={error} intent="destructive" />
|
|
224
263
|
|
|
225
264
|
<!-- Top-position extra fields -->
|
|
226
|
-
{#each topFields as cfg (cfg.name)}
|
|
265
|
+
{#each topFields as cfg, i (cfg.name)}
|
|
227
266
|
<FieldInput
|
|
267
|
+
bind:this={topFieldRefs[i]}
|
|
228
268
|
value={extraValue(cfg)}
|
|
229
269
|
oninput={(e: Event) =>
|
|
230
270
|
setExtraValue(cfg, (e.currentTarget as HTMLInputElement).value)}
|
|
@@ -252,6 +292,7 @@
|
|
|
252
292
|
<!-- Email -->
|
|
253
293
|
<!-- svelte-ignore binding_property_non_reactive -->
|
|
254
294
|
<FieldInput
|
|
295
|
+
bind:this={emailField}
|
|
255
296
|
bind:value={formData.email}
|
|
256
297
|
label={t("register_form.email_label")}
|
|
257
298
|
type="email"
|
|
@@ -270,6 +311,7 @@
|
|
|
270
311
|
<!-- Password -->
|
|
271
312
|
<!-- svelte-ignore binding_property_non_reactive -->
|
|
272
313
|
<FieldInput
|
|
314
|
+
bind:this={passwordField}
|
|
273
315
|
bind:value={formData.password}
|
|
274
316
|
label={t("register_form.password_label")}
|
|
275
317
|
autocomplete="new-password"
|
|
@@ -290,6 +332,7 @@
|
|
|
290
332
|
{#if showPasswordConfirm}
|
|
291
333
|
<!-- svelte-ignore binding_property_non_reactive -->
|
|
292
334
|
<FieldInput
|
|
335
|
+
bind:this={passwordConfirmField}
|
|
293
336
|
bind:value={formData.passwordConfirm}
|
|
294
337
|
label={t("register_form.password_confirm_label")}
|
|
295
338
|
autocomplete="new-password"
|
|
@@ -308,8 +351,9 @@
|
|
|
308
351
|
{/if}
|
|
309
352
|
|
|
310
353
|
<!-- Bottom-position extra fields (default) -->
|
|
311
|
-
{#each bottomFields as cfg (cfg.name)}
|
|
354
|
+
{#each bottomFields as cfg, i (cfg.name)}
|
|
312
355
|
<FieldInput
|
|
356
|
+
bind:this={bottomFieldRefs[i]}
|
|
313
357
|
value={extraValue(cfg)}
|
|
314
358
|
oninput={(e: Event) =>
|
|
315
359
|
setExtraValue(cfg, (e.currentTarget as HTMLInputElement).value)}
|
|
@@ -71,6 +71,10 @@ export interface Props extends Omit<HTMLAttributes<HTMLFormElement>, "children">
|
|
|
71
71
|
class?: string;
|
|
72
72
|
el?: HTMLFormElement;
|
|
73
73
|
}
|
|
74
|
-
|
|
74
|
+
import { scrollToFirstInvalidField } from "../../utils/validate-fields.js";
|
|
75
|
+
declare const RegisterForm: import("svelte").Component<Props, {
|
|
76
|
+
validate: () => boolean;
|
|
77
|
+
scrollToFirstError: (opts?: Parameters<typeof scrollToFirstInvalidField>[1]) => boolean;
|
|
78
|
+
}, "el" | "formData">;
|
|
75
79
|
type RegisterForm = ReturnType<typeof RegisterForm>;
|
|
76
80
|
export default RegisterForm;
|
|
@@ -58,7 +58,8 @@
|
|
|
58
58
|
off,
|
|
59
59
|
onclick,
|
|
60
60
|
preHook,
|
|
61
|
-
validate
|
|
61
|
+
// Renamed local binding to avoid collision with `export function validate()` below.
|
|
62
|
+
validate: validateProp,
|
|
62
63
|
setValidationResult,
|
|
63
64
|
...rest
|
|
64
65
|
}: Props = $props();
|
|
@@ -89,6 +90,39 @@
|
|
|
89
90
|
checkbox.dispatchEvent(new Event("change", { bubbles: true, cancelable: true }));
|
|
90
91
|
wrap.focus();
|
|
91
92
|
}
|
|
93
|
+
|
|
94
|
+
//
|
|
95
|
+
let _doValidate: (() => void) | undefined = $state();
|
|
96
|
+
// Local copy of the last validation result so getValidation() works even
|
|
97
|
+
// when no external setValidationResult was provided.
|
|
98
|
+
let _validation: ValidationResult | undefined = $state();
|
|
99
|
+
|
|
100
|
+
/** Trigger validation now. Reaches the parent via `setValidationResult`. */
|
|
101
|
+
export function validate(): ValidationResult | undefined {
|
|
102
|
+
_doValidate?.();
|
|
103
|
+
return _validation;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/** Clear the inline validation message and reset `setCustomValidity`. */
|
|
107
|
+
export function clearValidation(): void {
|
|
108
|
+
_validation = undefined;
|
|
109
|
+
checkbox?.setCustomValidity?.("");
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
/** Current validation state. */
|
|
113
|
+
export function getValidation(): ValidationResult | undefined {
|
|
114
|
+
return _validation;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
/** Focus the visual switch wrapper. */
|
|
118
|
+
export function focus(): void {
|
|
119
|
+
wrap?.focus?.();
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
/** Scroll the switch into view. */
|
|
123
|
+
export function scrollIntoView(opts?: ScrollIntoViewOptions): void {
|
|
124
|
+
wrap?.scrollIntoView?.({ behavior: "smooth", block: "center", ...opts });
|
|
125
|
+
}
|
|
92
126
|
</script>
|
|
93
127
|
|
|
94
128
|
<!-- svelte-ignore a11y_no_noninteractive_tabindex -->
|
|
@@ -140,9 +174,13 @@
|
|
|
140
174
|
{required}
|
|
141
175
|
{name}
|
|
142
176
|
use:validateAction={() => ({
|
|
143
|
-
enabled:
|
|
144
|
-
...(typeof
|
|
145
|
-
setValidationResult
|
|
177
|
+
enabled: validateProp !== false,
|
|
178
|
+
...(typeof validateProp === "boolean" ? {} : validateProp),
|
|
179
|
+
setValidationResult: (res) => {
|
|
180
|
+
_validation = res;
|
|
181
|
+
setValidationResult?.(res);
|
|
182
|
+
},
|
|
183
|
+
setDoValidate: (fn) => (_doValidate = fn),
|
|
146
184
|
})}
|
|
147
185
|
tabindex="-1"
|
|
148
186
|
/>
|
|
@@ -29,6 +29,12 @@ export interface Props extends Omit<HTMLLabelAttributes, "children" | "onchange"
|
|
|
29
29
|
validate?: boolean | Omit<ValidateOptions, "setValidationResult">;
|
|
30
30
|
setValidationResult?: (res: ValidationResult) => void;
|
|
31
31
|
}
|
|
32
|
-
declare const Switch: import("svelte").Component<Props, {
|
|
32
|
+
declare const Switch: import("svelte").Component<Props, {
|
|
33
|
+
validate: () => ValidationResult | undefined;
|
|
34
|
+
clearValidation: () => void;
|
|
35
|
+
getValidation: () => ValidationResult | undefined;
|
|
36
|
+
focus: () => void;
|
|
37
|
+
scrollIntoView: (opts?: ScrollIntoViewOptions) => void;
|
|
38
|
+
}, "button" | "checked">;
|
|
33
39
|
type Switch = ReturnType<typeof Switch>;
|
|
34
40
|
export default Switch;
|