sveltacular 1.0.18 → 1.0.21
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/README.md +1 -2
- package/dist/forms/bool-box/bool-box.svelte +392 -13
- package/dist/forms/bool-box/bool-box.svelte.d.ts +2 -0
- package/dist/forms/bool-box/index.d.ts +2 -0
- package/dist/forms/bool-box/index.js +2 -0
- package/dist/forms/date-box/date-box.svelte +104 -53
- package/dist/forms/date-box/date-box.svelte.d.ts +8 -3
- package/dist/forms/dimension-box/dimension-box.svelte +15 -50
- package/dist/forms/file-box/file-box.svelte +7 -25
- package/dist/forms/form-input-wrapper/form-input-wrapper.svelte +203 -0
- package/dist/forms/form-input-wrapper/form-input-wrapper.svelte.d.ts +16 -0
- package/dist/forms/form-input-wrapper/index.d.ts +2 -0
- package/dist/forms/form-input-wrapper/index.js +2 -0
- package/dist/forms/index.d.ts +0 -1
- package/dist/forms/index.js +0 -1
- package/dist/forms/list-box/list-box.svelte +26 -9
- package/dist/forms/list-box/list-box.svelte.d.ts +3 -0
- package/dist/forms/money-box/money-box.svelte +104 -65
- package/dist/forms/money-box/money-box.svelte.d.ts +6 -0
- package/dist/forms/number-box/number-box.svelte +93 -49
- package/dist/forms/number-box/number-box.svelte.d.ts +6 -0
- package/dist/forms/number-range-box/number-range-box.svelte +22 -58
- package/dist/forms/phone-box/phone-box.svelte +101 -38
- package/dist/forms/phone-box/phone-box.svelte.d.ts +6 -1
- package/dist/forms/slider/slider.svelte +13 -6
- package/dist/forms/slider/slider.svelte.d.ts +6 -2
- package/dist/forms/tag-box/tag-box.svelte +5 -3
- package/dist/forms/text-area/text-area.svelte +22 -2
- package/dist/forms/text-area/text-area.svelte.d.ts +4 -0
- package/dist/forms/text-box/text-box.svelte +97 -131
- package/dist/forms/text-box/text-box.svelte.d.ts +7 -2
- package/dist/forms/time-box/time-box.svelte +106 -37
- package/dist/forms/time-box/time-box.svelte.d.ts +10 -3
- package/dist/forms/url-box/url-box.svelte +26 -5
- package/dist/forms/url-box/url-box.svelte.d.ts +7 -1
- package/dist/generic/theme-provider/theme-provider-demo.svelte +7 -2
- package/dist/navigation/dropdown-button/dropdown-button.svelte +102 -3
- package/dist/navigation/dropdown-button/dropdown-manager.svelte.d.ts +47 -0
- package/dist/navigation/dropdown-button/dropdown-manager.svelte.js +47 -0
- package/dist/tables/table-cell.svelte +2 -0
- package/dist/tables/table-row.svelte +1 -0
- package/package.json +1 -1
|
@@ -1,9 +1,8 @@
|
|
|
1
1
|
<script lang="ts">
|
|
2
2
|
import { untrack } from 'svelte';
|
|
3
3
|
import { uniqueId } from '../../helpers/unique-id.js';
|
|
4
|
-
import { animateShake, animateScaleIn } from '../../helpers/animations.js';
|
|
5
4
|
import FormField, { type FormFieldFeedback } from '../form-field/form-field.svelte';
|
|
6
|
-
import
|
|
5
|
+
import FormInputWrapper from '../form-input-wrapper';
|
|
7
6
|
import type { AllowedTextInputTypes, FormFieldSizeOptions } from '../../types/form.js';
|
|
8
7
|
|
|
9
8
|
const id = uniqueId();
|
|
@@ -20,6 +19,7 @@
|
|
|
20
19
|
disabled = false,
|
|
21
20
|
required = false,
|
|
22
21
|
readonly = false,
|
|
22
|
+
nullable = false,
|
|
23
23
|
maxlength = undefined,
|
|
24
24
|
minlength = undefined,
|
|
25
25
|
pattern = undefined,
|
|
@@ -30,8 +30,12 @@
|
|
|
30
30
|
allowLetters = true,
|
|
31
31
|
textCase = undefined,
|
|
32
32
|
onChange = undefined,
|
|
33
|
+
onCheckChanged = undefined,
|
|
33
34
|
onInput = undefined,
|
|
34
|
-
|
|
35
|
+
onFocus = undefined,
|
|
36
|
+
onBlur = undefined,
|
|
37
|
+
label = undefined,
|
|
38
|
+
nullText = ''
|
|
35
39
|
}: {
|
|
36
40
|
value?: string | null;
|
|
37
41
|
placeholder?: string;
|
|
@@ -44,6 +48,7 @@
|
|
|
44
48
|
disabled?: boolean;
|
|
45
49
|
required?: boolean;
|
|
46
50
|
readonly?: boolean;
|
|
51
|
+
nullable?: boolean;
|
|
47
52
|
maxlength?: number | undefined;
|
|
48
53
|
minlength?: number | undefined;
|
|
49
54
|
pattern?: string | undefined;
|
|
@@ -53,16 +58,27 @@
|
|
|
53
58
|
allowNumbers?: boolean;
|
|
54
59
|
allowLetters?: boolean;
|
|
55
60
|
textCase?: 'lower' | 'upper' | undefined;
|
|
56
|
-
onChange?: ((value: string) => void) | undefined;
|
|
57
|
-
|
|
61
|
+
onChange?: ((value: string | null) => void) | undefined;
|
|
62
|
+
onCheckChanged?: ((isChecked: boolean) => void) | undefined;
|
|
63
|
+
onInput?: ((value: string | null) => void) | undefined;
|
|
64
|
+
onFocus?: ((value: string | null) => void) | undefined;
|
|
65
|
+
onBlur?: ((value: string | null) => void) | undefined;
|
|
58
66
|
label?: string;
|
|
67
|
+
nullText?: string;
|
|
59
68
|
} = $props();
|
|
60
69
|
|
|
70
|
+
// Track whether the nullable checkbox is checked (i.e., whether field has a value)
|
|
71
|
+
let isChecked = $state(untrack(() => !!value));
|
|
72
|
+
|
|
73
|
+
// Remember the last non-null value so we can restore it when re-checking
|
|
74
|
+
let lastValue = $state<string | undefined>(undefined);
|
|
75
|
+
|
|
76
|
+
// Derive the actual disabled state: disabled prop OR (nullable and unchecked)
|
|
77
|
+
let inputDisabled = $derived(disabled || (nullable && !isChecked));
|
|
78
|
+
|
|
61
79
|
let hasError = $derived(!!feedback?.isError);
|
|
62
80
|
let hasSuccess = $derived(!!feedback && !feedback.isError);
|
|
63
81
|
let describedByIds = $state<string[]>([]);
|
|
64
|
-
let inputElement: HTMLDivElement | null = $state(null);
|
|
65
|
-
let successIconElement: HTMLDivElement | null = $state(null);
|
|
66
82
|
let characterCount = $derived((value || '').length);
|
|
67
83
|
let characterLimitClass = $derived(
|
|
68
84
|
maxlength && characterCount > maxlength * 0.9
|
|
@@ -93,32 +109,8 @@
|
|
|
93
109
|
});
|
|
94
110
|
});
|
|
95
111
|
|
|
96
|
-
// Trigger shake animation when error appears (track previous error state)
|
|
97
|
-
let prevHasError = $state(false);
|
|
98
|
-
$effect(() => {
|
|
99
|
-
if (hasError && !prevHasError && inputElement) {
|
|
100
|
-
// Use untrack to prevent animation from triggering effect again
|
|
101
|
-
untrack(() => {
|
|
102
|
-
animateShake(inputElement!); // Already checked above
|
|
103
|
-
});
|
|
104
|
-
}
|
|
105
|
-
prevHasError = hasError;
|
|
106
|
-
});
|
|
107
|
-
|
|
108
|
-
// Trigger scale-in animation when success appears (track previous success state)
|
|
109
|
-
let prevHasSuccess = $state(false);
|
|
110
|
-
$effect(() => {
|
|
111
|
-
if (hasSuccess && !prevHasSuccess && successIconElement) {
|
|
112
|
-
// Use untrack to prevent animation from triggering effect again
|
|
113
|
-
untrack(() => {
|
|
114
|
-
animateScaleIn(successIconElement!); // Already checked above
|
|
115
|
-
});
|
|
116
|
-
}
|
|
117
|
-
prevHasSuccess = hasSuccess;
|
|
118
|
-
});
|
|
119
|
-
|
|
120
112
|
// Don't allow certain characters to be typed into the input
|
|
121
|
-
const
|
|
113
|
+
const handleKeyPress = (e: KeyboardEvent) => {
|
|
122
114
|
if (!allowSpaces && e.key === ' ') {
|
|
123
115
|
e.preventDefault();
|
|
124
116
|
}
|
|
@@ -130,6 +122,33 @@
|
|
|
130
122
|
}
|
|
131
123
|
};
|
|
132
124
|
|
|
125
|
+
const checkChanged = () => {
|
|
126
|
+
if (nullable) {
|
|
127
|
+
if (isChecked) {
|
|
128
|
+
// Restore last value if available, otherwise use empty string
|
|
129
|
+
value = lastValue || '';
|
|
130
|
+
} else {
|
|
131
|
+
// Store current value before clearing
|
|
132
|
+
if (value) {
|
|
133
|
+
lastValue = value;
|
|
134
|
+
}
|
|
135
|
+
value = '';
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
onCheckChanged?.(isChecked);
|
|
139
|
+
handleInputChange();
|
|
140
|
+
};
|
|
141
|
+
|
|
142
|
+
const handleInputChange = () => {
|
|
143
|
+
const currentValue = !nullable || isChecked ? value : null;
|
|
144
|
+
// Remember the value if it's not empty
|
|
145
|
+
if (isChecked && value) {
|
|
146
|
+
lastValue = value;
|
|
147
|
+
}
|
|
148
|
+
onChange?.(currentValue);
|
|
149
|
+
onInput?.(currentValue);
|
|
150
|
+
};
|
|
151
|
+
|
|
133
152
|
// When the value changes, make sure it is in the correct case
|
|
134
153
|
const handleInput = (e: Event) => {
|
|
135
154
|
const cleanValue = String(value);
|
|
@@ -147,27 +166,52 @@
|
|
|
147
166
|
if (type === 'url') {
|
|
148
167
|
value = cleanValue.replace(/\s/g, '');
|
|
149
168
|
}
|
|
150
|
-
|
|
151
|
-
|
|
169
|
+
handleInputChange();
|
|
170
|
+
};
|
|
171
|
+
|
|
172
|
+
const handleFocus = (e: FocusEvent) => {
|
|
173
|
+
onFocus?.(value);
|
|
174
|
+
};
|
|
175
|
+
|
|
176
|
+
const handleBlur = (e: FocusEvent) => {
|
|
177
|
+
onBlur?.(value);
|
|
152
178
|
};
|
|
179
|
+
|
|
180
|
+
$effect(() => {
|
|
181
|
+
if (!value) {
|
|
182
|
+
// Use untrack to prevent writes to isChecked/value from triggering this effect again
|
|
183
|
+
untrack(() => {
|
|
184
|
+
if (nullable) isChecked = false;
|
|
185
|
+
});
|
|
186
|
+
} else {
|
|
187
|
+
// Initialize lastValue if we have an initial value
|
|
188
|
+
if (!lastValue) {
|
|
189
|
+
lastValue = value;
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
});
|
|
193
|
+
|
|
194
|
+
let effectiveNullText = $derived(nullText || placeholder || '--');
|
|
153
195
|
</script>
|
|
154
196
|
|
|
155
197
|
<FormField {size} {label} {id} {required} {disabled} {helperText} {feedback}>
|
|
156
|
-
<
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
198
|
+
<FormInputWrapper
|
|
199
|
+
{disabled}
|
|
200
|
+
error={hasError}
|
|
201
|
+
success={hasSuccess}
|
|
202
|
+
{prefix}
|
|
203
|
+
{suffix}
|
|
204
|
+
{nullable}
|
|
205
|
+
nullText={effectiveNullText}
|
|
206
|
+
{isLoading}
|
|
207
|
+
onCheckChanged={checkChanged}
|
|
161
208
|
>
|
|
162
|
-
{#if prefix}
|
|
163
|
-
<div class="prefix">{prefix}</div>
|
|
164
|
-
{/if}
|
|
165
209
|
<input
|
|
166
210
|
{id}
|
|
167
211
|
{placeholder}
|
|
168
212
|
bind:value
|
|
169
213
|
{...{ type }}
|
|
170
|
-
{
|
|
214
|
+
disabled={inputDisabled}
|
|
171
215
|
{readonly}
|
|
172
216
|
{required}
|
|
173
217
|
{maxlength}
|
|
@@ -177,22 +221,12 @@
|
|
|
177
221
|
aria-required={required}
|
|
178
222
|
aria-invalid={hasError}
|
|
179
223
|
aria-busy={isLoading}
|
|
180
|
-
onkeypress={
|
|
224
|
+
onkeypress={handleKeyPress}
|
|
181
225
|
oninput={handleInput}
|
|
226
|
+
onfocus={handleFocus}
|
|
227
|
+
onblur={handleBlur}
|
|
182
228
|
/>
|
|
183
|
-
|
|
184
|
-
<div class="loading-indicator" aria-label="Loading">
|
|
185
|
-
<div class="spinner"></div>
|
|
186
|
-
</div>
|
|
187
|
-
{:else if hasSuccess}
|
|
188
|
-
<div class="success-indicator" bind:this={successIconElement}>
|
|
189
|
-
<Icon type="check" size="sm" />
|
|
190
|
-
</div>
|
|
191
|
-
{/if}
|
|
192
|
-
{#if suffix}
|
|
193
|
-
<div class="suffix">{suffix}</div>
|
|
194
|
-
{/if}
|
|
195
|
-
</div>
|
|
229
|
+
</FormInputWrapper>
|
|
196
230
|
{#if showCharacterCount && maxlength}
|
|
197
231
|
<div class="character-count {characterLimitClass}">
|
|
198
232
|
{characterCount} / {maxlength}
|
|
@@ -200,92 +234,24 @@
|
|
|
200
234
|
{/if}
|
|
201
235
|
</FormField>
|
|
202
236
|
|
|
203
|
-
<style
|
|
204
|
-
display: flex;
|
|
205
|
-
align-items: center;
|
|
206
|
-
justify-content: flex-start;
|
|
207
|
-
position: relative;
|
|
208
|
-
width: 100%;
|
|
209
|
-
height: 100%;
|
|
210
|
-
border-radius: var(--radius-md);
|
|
211
|
-
border: var(--border-thin) solid var(--form-input-border);
|
|
212
|
-
background-color: var(--form-input-bg);
|
|
213
|
-
color: var(--form-input-fg);
|
|
214
|
-
font-size: var(--font-md);
|
|
215
|
-
font-weight: 500;
|
|
216
|
-
line-height: 2rem;
|
|
217
|
-
transition: background-color var(--transition-base) var(--ease-in-out), border-color var(--transition-base) var(--ease-in-out), color var(--transition-base) var(--ease-in-out), fill var(--transition-base) var(--ease-in-out), stroke var(--transition-base) var(--ease-in-out);
|
|
218
|
-
user-select: none;
|
|
219
|
-
white-space: nowrap;
|
|
220
|
-
}
|
|
221
|
-
.input.disabled {
|
|
222
|
-
opacity: 0.5;
|
|
223
|
-
}
|
|
224
|
-
.input.error {
|
|
225
|
-
border-color: var(--danger, #dc3545);
|
|
226
|
-
}
|
|
227
|
-
.input.success {
|
|
228
|
-
border-color: var(--success, #28a745);
|
|
229
|
-
}
|
|
230
|
-
.input .loading-indicator,
|
|
231
|
-
.input .success-indicator {
|
|
232
|
-
display: flex;
|
|
233
|
-
align-items: center;
|
|
234
|
-
justify-content: center;
|
|
235
|
-
padding: 0 var(--spacing-base);
|
|
236
|
-
}
|
|
237
|
-
.input .loading-indicator .spinner {
|
|
238
|
-
width: 1rem;
|
|
239
|
-
height: 1rem;
|
|
240
|
-
border: 2px solid var(--form-input-border);
|
|
241
|
-
border-top-color: var(--primary-500, #3b82f6);
|
|
242
|
-
border-radius: 50%;
|
|
243
|
-
animation: spin 0.6s linear infinite;
|
|
244
|
-
}
|
|
245
|
-
@keyframes spin {
|
|
246
|
-
to {
|
|
247
|
-
transform: rotate(360deg);
|
|
248
|
-
}
|
|
249
|
-
}
|
|
250
|
-
.input .success-indicator {
|
|
251
|
-
color: var(--success, #28a745);
|
|
252
|
-
width: 1.5rem;
|
|
253
|
-
height: 1.5rem;
|
|
254
|
-
}
|
|
255
|
-
.input .success-indicator :global(svg) {
|
|
256
|
-
width: 100%;
|
|
257
|
-
height: 100%;
|
|
258
|
-
}
|
|
259
|
-
.input input {
|
|
237
|
+
<style>input {
|
|
260
238
|
background-color: transparent;
|
|
261
239
|
border: none;
|
|
262
240
|
line-height: 2rem;
|
|
263
241
|
font-size: var(--font-md);
|
|
264
242
|
width: 100%;
|
|
265
243
|
flex-grow: 1;
|
|
266
|
-
padding
|
|
244
|
+
padding: 0 0 0 var(--spacing-base);
|
|
267
245
|
}
|
|
268
|
-
|
|
246
|
+
input:focus {
|
|
269
247
|
outline: none;
|
|
270
248
|
}
|
|
271
|
-
|
|
249
|
+
input:focus-visible {
|
|
272
250
|
outline: 2px solid var(--focus-ring, #007bff);
|
|
273
251
|
outline-offset: 2px;
|
|
274
252
|
}
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
font-size: var(--font-md);
|
|
278
|
-
line-height: 2rem;
|
|
279
|
-
padding-left: var(--spacing-base);
|
|
280
|
-
padding-right: var(--spacing-base);
|
|
281
|
-
background-color: var(--form-input-accent-bg);
|
|
282
|
-
color: var(--form-input-accent-fg);
|
|
283
|
-
}
|
|
284
|
-
.input .prefix {
|
|
285
|
-
border-right: var(--border-thin) solid var(--form-input-border);
|
|
286
|
-
}
|
|
287
|
-
.input .suffix {
|
|
288
|
-
border-left: var(--border-thin) solid var(--form-input-border);
|
|
253
|
+
input:disabled {
|
|
254
|
+
cursor: not-allowed;
|
|
289
255
|
}
|
|
290
256
|
|
|
291
257
|
.character-count {
|
|
@@ -12,6 +12,7 @@ type $$ComponentProps = {
|
|
|
12
12
|
disabled?: boolean;
|
|
13
13
|
required?: boolean;
|
|
14
14
|
readonly?: boolean;
|
|
15
|
+
nullable?: boolean;
|
|
15
16
|
maxlength?: number | undefined;
|
|
16
17
|
minlength?: number | undefined;
|
|
17
18
|
pattern?: string | undefined;
|
|
@@ -21,9 +22,13 @@ type $$ComponentProps = {
|
|
|
21
22
|
allowNumbers?: boolean;
|
|
22
23
|
allowLetters?: boolean;
|
|
23
24
|
textCase?: 'lower' | 'upper' | undefined;
|
|
24
|
-
onChange?: ((value: string) => void) | undefined;
|
|
25
|
-
|
|
25
|
+
onChange?: ((value: string | null) => void) | undefined;
|
|
26
|
+
onCheckChanged?: ((isChecked: boolean) => void) | undefined;
|
|
27
|
+
onInput?: ((value: string | null) => void) | undefined;
|
|
28
|
+
onFocus?: ((value: string | null) => void) | undefined;
|
|
29
|
+
onBlur?: ((value: string | null) => void) | undefined;
|
|
26
30
|
label?: string;
|
|
31
|
+
nullText?: string;
|
|
27
32
|
};
|
|
28
33
|
declare const TextBox: import("svelte").Component<$$ComponentProps, {}, "value">;
|
|
29
34
|
type TextBox = ReturnType<typeof TextBox>;
|
|
@@ -1,92 +1,161 @@
|
|
|
1
1
|
<script lang="ts">
|
|
2
|
+
import { untrack } from 'svelte';
|
|
2
3
|
import { uniqueId } from '../../helpers/unique-id.js';
|
|
3
4
|
import FormField, { type FormFieldFeedback } from '../form-field/form-field.svelte';
|
|
4
|
-
import
|
|
5
|
+
import FormInputWrapper from '../form-input-wrapper';
|
|
6
|
+
import type { FormFieldSizeOptions } from '../../types/form.js';
|
|
5
7
|
|
|
6
8
|
const id = uniqueId();
|
|
7
9
|
|
|
8
10
|
let {
|
|
9
11
|
value = $bindable('' as string | null),
|
|
10
|
-
size = 'full' as
|
|
12
|
+
size = 'full' as FormFieldSizeOptions,
|
|
13
|
+
nullable = false,
|
|
11
14
|
disabled = false,
|
|
12
15
|
required = false,
|
|
16
|
+
readonly = false,
|
|
13
17
|
onChange = undefined,
|
|
18
|
+
onCheckChanged = undefined,
|
|
19
|
+
onInput = undefined,
|
|
20
|
+
onFocus = undefined,
|
|
21
|
+
onBlur = undefined,
|
|
14
22
|
label = undefined,
|
|
15
23
|
helperText = undefined,
|
|
24
|
+
nullText = '-- : -- : --',
|
|
16
25
|
feedback = undefined
|
|
17
26
|
}: {
|
|
18
27
|
value?: string | null;
|
|
19
|
-
size?:
|
|
28
|
+
size?: FormFieldSizeOptions;
|
|
29
|
+
nullable?: boolean;
|
|
20
30
|
disabled?: boolean;
|
|
21
31
|
required?: boolean;
|
|
22
|
-
|
|
32
|
+
readonly?: boolean;
|
|
33
|
+
onChange?: ((value: string | null) => void) | undefined;
|
|
34
|
+
onCheckChanged?: ((isChecked: boolean) => void) | undefined;
|
|
35
|
+
onInput?: ((value: string | null) => void) | undefined;
|
|
36
|
+
onFocus?: ((e: FocusEvent) => void) | undefined;
|
|
37
|
+
onBlur?: ((e: FocusEvent) => void) | undefined;
|
|
23
38
|
label?: string;
|
|
24
39
|
helperText?: string;
|
|
40
|
+
nullText?: string;
|
|
25
41
|
feedback?: FormFieldFeedback;
|
|
26
42
|
} = $props();
|
|
27
43
|
|
|
44
|
+
// Track whether the nullable checkbox is checked (i.e., whether field has a value)
|
|
45
|
+
let isChecked = $state(untrack(() => !!value));
|
|
46
|
+
|
|
47
|
+
// Remember the last non-null value so we can restore it when re-checking
|
|
48
|
+
let lastValue = $state<string | undefined>(undefined);
|
|
49
|
+
|
|
50
|
+
// Derive the actual disabled state: disabled prop OR (nullable and unchecked)
|
|
51
|
+
let inputDisabled = $derived(disabled || (nullable && !isChecked));
|
|
52
|
+
|
|
53
|
+
const getCurrentTime = () => {
|
|
54
|
+
const now = new Date();
|
|
55
|
+
return `${String(now.getHours()).padStart(2, '0')}:${String(now.getMinutes()).padStart(2, '0')}`;
|
|
56
|
+
};
|
|
57
|
+
|
|
58
|
+
const checkChanged = () => {
|
|
59
|
+
if (nullable) {
|
|
60
|
+
if (isChecked) {
|
|
61
|
+
// Restore last value if available, otherwise use current time
|
|
62
|
+
value = lastValue || getCurrentTime();
|
|
63
|
+
} else {
|
|
64
|
+
// Store current value before clearing
|
|
65
|
+
if (value) {
|
|
66
|
+
lastValue = value;
|
|
67
|
+
}
|
|
68
|
+
value = '';
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
onCheckChanged?.(isChecked);
|
|
72
|
+
handleInputChange();
|
|
73
|
+
};
|
|
74
|
+
|
|
75
|
+
const handleInputChange = () => {
|
|
76
|
+
const currentValue = !nullable || isChecked ? value : null;
|
|
77
|
+
// Remember the value if it's not empty
|
|
78
|
+
if (isChecked && value) {
|
|
79
|
+
lastValue = value;
|
|
80
|
+
}
|
|
81
|
+
onChange?.(currentValue);
|
|
82
|
+
onInput?.(currentValue);
|
|
83
|
+
};
|
|
84
|
+
|
|
28
85
|
const handleInput = (e: Event) => {
|
|
29
86
|
const target = e.target as HTMLInputElement;
|
|
30
87
|
value = target.value;
|
|
31
|
-
|
|
88
|
+
handleInputChange();
|
|
89
|
+
};
|
|
90
|
+
|
|
91
|
+
const handleChange = (e: Event) => {
|
|
92
|
+
const target = e.target as HTMLInputElement;
|
|
93
|
+
value = target.value;
|
|
94
|
+
handleInputChange();
|
|
32
95
|
};
|
|
96
|
+
|
|
97
|
+
$effect(() => {
|
|
98
|
+
if (!value) {
|
|
99
|
+
// Use untrack to prevent writes to isChecked/value from triggering this effect again
|
|
100
|
+
untrack(() => {
|
|
101
|
+
if (nullable) isChecked = false;
|
|
102
|
+
});
|
|
103
|
+
} else {
|
|
104
|
+
// Initialize lastValue if we have an initial value
|
|
105
|
+
if (!lastValue) {
|
|
106
|
+
lastValue = value;
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
let showInput = $derived(!nullable || isChecked);
|
|
33
112
|
</script>
|
|
34
113
|
|
|
35
114
|
<FormField {size} {label} {id} {required} {disabled} {helperText} {feedback}>
|
|
36
|
-
<
|
|
115
|
+
<FormInputWrapper
|
|
116
|
+
{disabled}
|
|
117
|
+
{nullable}
|
|
118
|
+
nullText={nullText || '-- : -- : --'}
|
|
119
|
+
onCheckChanged={checkChanged}
|
|
120
|
+
>
|
|
37
121
|
<input
|
|
38
122
|
{id}
|
|
39
123
|
type="time"
|
|
40
124
|
bind:value
|
|
41
|
-
{
|
|
125
|
+
disabled={inputDisabled}
|
|
42
126
|
{required}
|
|
127
|
+
{readonly}
|
|
43
128
|
oninput={handleInput}
|
|
129
|
+
onchange={handleChange}
|
|
130
|
+
onfocus={onFocus}
|
|
131
|
+
onblur={onBlur}
|
|
44
132
|
aria-required={required}
|
|
45
133
|
/>
|
|
46
|
-
</
|
|
134
|
+
</FormInputWrapper>
|
|
47
135
|
</FormField>
|
|
48
136
|
|
|
49
|
-
<style
|
|
50
|
-
display: flex;
|
|
51
|
-
align-items: center;
|
|
52
|
-
justify-content: flex-start;
|
|
53
|
-
position: relative;
|
|
54
|
-
width: 100%;
|
|
55
|
-
height: 100%;
|
|
56
|
-
border-radius: var(--radius-md);
|
|
57
|
-
border: var(--border-thin) solid var(--form-input-border);
|
|
58
|
-
background-color: var(--form-input-bg);
|
|
59
|
-
color: var(--form-input-fg);
|
|
60
|
-
font-size: var(--font-md);
|
|
61
|
-
font-weight: 500;
|
|
62
|
-
line-height: 2rem;
|
|
63
|
-
transition: background-color var(--transition-base) var(--ease-in-out), border-color var(--transition-base) var(--ease-in-out), color var(--transition-base) var(--ease-in-out);
|
|
64
|
-
user-select: none;
|
|
65
|
-
white-space: nowrap;
|
|
66
|
-
}
|
|
67
|
-
.input.disabled {
|
|
68
|
-
opacity: 0.5;
|
|
69
|
-
}
|
|
70
|
-
.input input {
|
|
137
|
+
<style>input[type=time] {
|
|
71
138
|
background-color: transparent;
|
|
72
139
|
border: none;
|
|
73
140
|
line-height: 2rem;
|
|
141
|
+
height: 2rem;
|
|
74
142
|
font-size: var(--font-md);
|
|
75
143
|
width: 100%;
|
|
76
144
|
flex-grow: 1;
|
|
77
|
-
padding
|
|
78
|
-
|
|
145
|
+
padding: 0 var(--spacing-base);
|
|
146
|
+
margin: 0;
|
|
147
|
+
box-sizing: border-box;
|
|
79
148
|
}
|
|
80
|
-
|
|
149
|
+
input[type=time]:focus {
|
|
81
150
|
outline: none;
|
|
82
151
|
}
|
|
83
|
-
|
|
152
|
+
input[type=time]:focus-visible {
|
|
84
153
|
outline: 2px solid var(--focus-ring, #007bff);
|
|
85
154
|
outline-offset: 2px;
|
|
86
155
|
}
|
|
87
|
-
|
|
156
|
+
input[type=time]:disabled {
|
|
88
157
|
cursor: not-allowed;
|
|
89
158
|
}
|
|
90
|
-
|
|
91
|
-
|
|
159
|
+
input[type=time]:read-only {
|
|
160
|
+
cursor: default;
|
|
92
161
|
}</style>
|
|
@@ -1,13 +1,20 @@
|
|
|
1
1
|
import { type FormFieldFeedback } from '../form-field/form-field.svelte';
|
|
2
|
-
import type {
|
|
2
|
+
import type { FormFieldSizeOptions } from '../../types/form.js';
|
|
3
3
|
type $$ComponentProps = {
|
|
4
4
|
value?: string | null;
|
|
5
|
-
size?:
|
|
5
|
+
size?: FormFieldSizeOptions;
|
|
6
|
+
nullable?: boolean;
|
|
6
7
|
disabled?: boolean;
|
|
7
8
|
required?: boolean;
|
|
8
|
-
|
|
9
|
+
readonly?: boolean;
|
|
10
|
+
onChange?: ((value: string | null) => void) | undefined;
|
|
11
|
+
onCheckChanged?: ((isChecked: boolean) => void) | undefined;
|
|
12
|
+
onInput?: ((value: string | null) => void) | undefined;
|
|
13
|
+
onFocus?: ((e: FocusEvent) => void) | undefined;
|
|
14
|
+
onBlur?: ((e: FocusEvent) => void) | undefined;
|
|
9
15
|
label?: string;
|
|
10
16
|
helperText?: string;
|
|
17
|
+
nullText?: string;
|
|
11
18
|
feedback?: FormFieldFeedback;
|
|
12
19
|
};
|
|
13
20
|
declare const TimeBox: import("svelte").Component<$$ComponentProps, {}, "value">;
|
|
@@ -14,11 +14,17 @@
|
|
|
14
14
|
disabled = false,
|
|
15
15
|
required = false,
|
|
16
16
|
readonly = false,
|
|
17
|
+
nullable = false,
|
|
17
18
|
maxlength = undefined,
|
|
18
19
|
minlength = undefined,
|
|
19
20
|
pattern = undefined,
|
|
20
21
|
isLoading = false,
|
|
21
|
-
onChange = undefined
|
|
22
|
+
onChange = undefined,
|
|
23
|
+
onCheckChanged = undefined,
|
|
24
|
+
onInput = undefined,
|
|
25
|
+
onFocus = undefined,
|
|
26
|
+
onBlur = undefined,
|
|
27
|
+
nullText = ''
|
|
22
28
|
}: {
|
|
23
29
|
protocol?: HttpProtocol;
|
|
24
30
|
value?: string | null;
|
|
@@ -30,22 +36,32 @@
|
|
|
30
36
|
disabled?: boolean;
|
|
31
37
|
required?: boolean;
|
|
32
38
|
readonly?: boolean;
|
|
39
|
+
nullable?: boolean;
|
|
33
40
|
maxlength?: number | undefined;
|
|
34
41
|
minlength?: number | undefined;
|
|
35
42
|
pattern?: string | undefined;
|
|
36
43
|
isLoading?: boolean;
|
|
37
|
-
onChange?: ((value: string) => void) | undefined;
|
|
44
|
+
onChange?: ((value: string | null) => void) | undefined;
|
|
45
|
+
onCheckChanged?: ((isChecked: boolean) => void) | undefined;
|
|
46
|
+
onInput?: ((value: string | null) => void) | undefined;
|
|
47
|
+
onFocus?: ((e: FocusEvent) => void) | undefined;
|
|
48
|
+
onBlur?: ((e: FocusEvent) => void) | undefined;
|
|
49
|
+
nullText?: string;
|
|
38
50
|
} = $props();
|
|
39
51
|
|
|
40
52
|
// On input, parse the value and set the protocol
|
|
41
|
-
const handleInput = (inputValue: string) => {
|
|
53
|
+
const handleInput = (inputValue: string | null) => {
|
|
42
54
|
const cleanValue = inputValue ?? '';
|
|
43
55
|
const urlParts = cleanValue.split('://');
|
|
44
56
|
if (['http', 'https'].includes(urlParts[0])) {
|
|
45
57
|
protocol = urlParts[0] as HttpProtocol;
|
|
46
58
|
value = urlParts[1];
|
|
47
59
|
}
|
|
48
|
-
|
|
60
|
+
onInput?.(inputValue);
|
|
61
|
+
};
|
|
62
|
+
|
|
63
|
+
const handleChange = (inputValue: string | null) => {
|
|
64
|
+
onChange?.(inputValue);
|
|
49
65
|
};
|
|
50
66
|
</script>
|
|
51
67
|
|
|
@@ -56,7 +72,10 @@
|
|
|
56
72
|
prefix={protocol + '://'}
|
|
57
73
|
{size}
|
|
58
74
|
onInput={handleInput}
|
|
59
|
-
onChange={
|
|
75
|
+
onChange={handleChange}
|
|
76
|
+
{onCheckChanged}
|
|
77
|
+
{onFocus}
|
|
78
|
+
{onBlur}
|
|
60
79
|
allowSpaces={false}
|
|
61
80
|
{label}
|
|
62
81
|
{helperText}
|
|
@@ -64,8 +83,10 @@
|
|
|
64
83
|
{disabled}
|
|
65
84
|
{required}
|
|
66
85
|
{readonly}
|
|
86
|
+
{nullable}
|
|
67
87
|
{maxlength}
|
|
68
88
|
{minlength}
|
|
69
89
|
{pattern}
|
|
70
90
|
{isLoading}
|
|
91
|
+
{nullText}
|
|
71
92
|
/>
|
|
@@ -11,11 +11,17 @@ type $$ComponentProps = {
|
|
|
11
11
|
disabled?: boolean;
|
|
12
12
|
required?: boolean;
|
|
13
13
|
readonly?: boolean;
|
|
14
|
+
nullable?: boolean;
|
|
14
15
|
maxlength?: number | undefined;
|
|
15
16
|
minlength?: number | undefined;
|
|
16
17
|
pattern?: string | undefined;
|
|
17
18
|
isLoading?: boolean;
|
|
18
|
-
onChange?: ((value: string) => void) | undefined;
|
|
19
|
+
onChange?: ((value: string | null) => void) | undefined;
|
|
20
|
+
onCheckChanged?: ((isChecked: boolean) => void) | undefined;
|
|
21
|
+
onInput?: ((value: string | null) => void) | undefined;
|
|
22
|
+
onFocus?: ((e: FocusEvent) => void) | undefined;
|
|
23
|
+
onBlur?: ((e: FocusEvent) => void) | undefined;
|
|
24
|
+
nullText?: string;
|
|
19
25
|
};
|
|
20
26
|
declare const UrlBox: import("svelte").Component<$$ComponentProps, {}, "value" | "protocol">;
|
|
21
27
|
type UrlBox = ReturnType<typeof UrlBox>;
|
|
@@ -13,7 +13,7 @@
|
|
|
13
13
|
import TextBox from '../../forms/text-box/text-box.svelte';
|
|
14
14
|
import TextArea from '../../forms/text-area/text-area.svelte';
|
|
15
15
|
import CheckBox from '../../forms/check-box/check-box.svelte';
|
|
16
|
-
import
|
|
16
|
+
import BoolBox from '../../forms/bool-box/bool-box.svelte';
|
|
17
17
|
|
|
18
18
|
const theme = useTheme();
|
|
19
19
|
|
|
@@ -125,7 +125,12 @@
|
|
|
125
125
|
|
|
126
126
|
<CheckBox bind:isChecked={checked} label="Check Box Option" />
|
|
127
127
|
|
|
128
|
-
<
|
|
128
|
+
<BoolBox
|
|
129
|
+
variant="switch"
|
|
130
|
+
bind:value={switchOn}
|
|
131
|
+
options={['On', 'Off']}
|
|
132
|
+
label="Switch Option"
|
|
133
|
+
/>
|
|
129
134
|
</div>
|
|
130
135
|
</div>
|
|
131
136
|
</Card>
|