runeforge 0.0.61 → 0.0.63
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/LICENSE +21 -21
- package/README.md +1410 -1407
- package/dist/components/Avatar.svelte +31 -31
- package/dist/components/IconRenderer.svelte +73 -73
- package/dist/components/Modal.svelte +75 -75
- package/dist/components/common/Header.svelte +40 -40
- package/dist/components/crud/EmbeddedField.svelte +203 -203
- package/dist/components/crud/Field.svelte +122 -122
- package/dist/components/crud/GenericCRUD.svelte +426 -426
- package/dist/components/crud/SearchInput.svelte +53 -53
- package/dist/components/crud/columns/Avatar.svelte +15 -15
- package/dist/components/crud/columns/Icon.svelte +8 -8
- package/dist/components/crud/fields/BooleanField.svelte +38 -38
- package/dist/components/crud/fields/DatetimeField.svelte +250 -250
- package/dist/components/crud/fields/DefaultField.svelte +67 -67
- package/dist/components/crud/fields/FileField.svelte +70 -70
- package/dist/components/crud/fields/MultiSelectField.svelte +70 -70
- package/dist/components/crud/fields/SelectField.svelte +67 -67
- package/dist/components/crud/fields/TextareaField.svelte +55 -55
- package/dist/components/crud/fields/TreeField.svelte +1 -0
- package/dist/components/crud/utils/resolution.js +1 -0
- package/dist/components/crud/utils/tree.d.ts +5 -0
- package/dist/components/crud/utils/tree.js +29 -0
- package/dist/components/crud/views/Create.svelte +239 -239
- package/dist/components/crud/views/Read.svelte +131 -131
- package/dist/components/crud/views/Update.svelte +215 -215
- package/dist/components/crud/views/list/List.svelte +291 -291
- package/dist/components/crud/views/list/Modals.svelte +56 -56
- package/dist/components/crud/views/list/Table.svelte +173 -173
- package/dist/components/crud/views/list/Toolbar.svelte +340 -340
- package/dist/components/form/Button.svelte +27 -27
- package/dist/components/form/Label.svelte +37 -37
- package/dist/components/form/MultiSelect.svelte +248 -248
- package/dist/components/form/PasswordInput.svelte +68 -68
- package/dist/components/form/Required.svelte +1 -1
- package/dist/components/form/Select.svelte +209 -209
- package/dist/components/form/Tree.svelte +43 -4
- package/dist/components/form/Tree.svelte.d.ts +1 -0
- package/dist/components/form/TreeNode.svelte +31 -2
- package/dist/components/form/TreeNode.svelte.d.ts +2 -0
- package/dist/components/navigation/Breadcrumbs.svelte +111 -111
- package/dist/components/table/ColumnFilter.svelte +168 -168
- package/dist/components/table/PaginatedTable.svelte +542 -542
- package/dist/components/table/Paginator.svelte +113 -113
- package/dist/components/table/SortHeader.svelte +43 -43
- package/dist/components/table/TableBody.svelte +150 -150
- package/dist/components/table/TableHeader.svelte +88 -88
- package/dist/components/table/state.svelte.js +7 -2
- package/dist/icons/defaults/Clear.svelte +6 -6
- package/dist/icons/defaults/Create.svelte +6 -6
- package/dist/icons/defaults/Delete.svelte +6 -6
- package/dist/icons/defaults/Download.svelte +7 -7
- package/dist/icons/defaults/Edit.svelte +7 -7
- package/dist/icons/defaults/Filter.svelte +6 -6
- package/dist/icons/defaults/FilterActive.svelte +6 -6
- package/dist/icons/defaults/Folder.svelte +6 -6
- package/dist/icons/defaults/Grip.svelte +7 -7
- package/dist/icons/defaults/Home.svelte +6 -6
- package/dist/icons/defaults/PasswordHide.svelte +9 -9
- package/dist/icons/defaults/PasswordShow.svelte +7 -7
- package/dist/icons/defaults/SortAsc.svelte +6 -6
- package/dist/icons/defaults/SortDesc.svelte +6 -6
- package/dist/icons/defaults/SortNone.svelte +6 -6
- package/dist/icons/defaults/View.svelte +7 -7
- package/dist/types/attribute.d.ts +3 -0
- package/dist/types/crud/fields.d.ts +3 -0
- package/package.json +1 -1
|
@@ -1,250 +1,250 @@
|
|
|
1
|
-
<script lang="ts" generics="T extends object = Record<string, unknown>">
|
|
2
|
-
import { onMount } from 'svelte';
|
|
3
|
-
import type { Snippet } from 'svelte';
|
|
4
|
-
import { getStrings } from '../../../i18n/context.js';
|
|
5
|
-
import type { FieldDefinition } from '../../../types/crud/fields.js';
|
|
6
|
-
|
|
7
|
-
const strings = getStrings();
|
|
8
|
-
|
|
9
|
-
onMount(() => {
|
|
10
|
-
import('cally');
|
|
11
|
-
});
|
|
12
|
-
|
|
13
|
-
let {
|
|
14
|
-
field,
|
|
15
|
-
record = $bindable({} as Record<string, unknown>),
|
|
16
|
-
parentRecord = undefined,
|
|
17
|
-
error = '',
|
|
18
|
-
readonly = false,
|
|
19
|
-
labelRow,
|
|
20
|
-
errorRow
|
|
21
|
-
}: {
|
|
22
|
-
field: FieldDefinition<T>;
|
|
23
|
-
record?: Record<string, unknown>;
|
|
24
|
-
parentRecord?: Record<string, unknown>;
|
|
25
|
-
error?: string;
|
|
26
|
-
readonly?: boolean;
|
|
27
|
-
labelRow: Snippet;
|
|
28
|
-
errorRow: Snippet;
|
|
29
|
-
} = $props();
|
|
30
|
-
|
|
31
|
-
const datePopId = $props.id();
|
|
32
|
-
const dateAnchorName = `--date-anchor-${datePopId}`;
|
|
33
|
-
let datePopoverEl: HTMLElement | undefined = $state();
|
|
34
|
-
let calendarDateEl: (HTMLElement & { value: string }) | undefined = $state();
|
|
35
|
-
|
|
36
|
-
const name = $derived(readonly ? undefined : field.attribute);
|
|
37
|
-
const saved = $derived(record[field.attribute]);
|
|
38
|
-
const displayValue = $derived(saved == null ? '' : String(saved));
|
|
39
|
-
const formattedValue = $derived(
|
|
40
|
-
readonly && field.formatter ? field.formatter(saved, record) : displayValue
|
|
41
|
-
);
|
|
42
|
-
const fieldDisabled = $derived(field.disabled ? field.disabled(record, parentRecord) : false);
|
|
43
|
-
|
|
44
|
-
function pad(n: number): string {
|
|
45
|
-
return String(n).padStart(2, '0');
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
// ISO (yyyy-mm-dd) <-> display (dd/mm/yyyy) conversion and typing mask.
|
|
49
|
-
function isoDateToDisplay(iso: string): string {
|
|
50
|
-
const [year, month, day] = iso.split('-');
|
|
51
|
-
return `${day}/${month}/${year}`;
|
|
52
|
-
}
|
|
53
|
-
|
|
54
|
-
function displayToIsoDate(display: string): string | null {
|
|
55
|
-
const match = display.trim().match(/^(\d{1,2})\/(\d{1,2})\/(\d{4})$/);
|
|
56
|
-
if (!match) return null;
|
|
57
|
-
const [, dd, mm, yyyy] = match;
|
|
58
|
-
const day = Number(dd);
|
|
59
|
-
const month = Number(mm);
|
|
60
|
-
const year = Number(yyyy);
|
|
61
|
-
// Rejects invalid dates (e.g. 31/02).
|
|
62
|
-
const date = new Date(Date.UTC(year, month - 1, day));
|
|
63
|
-
if (
|
|
64
|
-
date.getUTCFullYear() !== year ||
|
|
65
|
-
date.getUTCMonth() !== month - 1 ||
|
|
66
|
-
date.getUTCDate() !== day
|
|
67
|
-
) {
|
|
68
|
-
return null;
|
|
69
|
-
}
|
|
70
|
-
return `${yyyy}-${mm.padStart(2, '0')}-${dd.padStart(2, '0')}`;
|
|
71
|
-
}
|
|
72
|
-
|
|
73
|
-
// Inserts "/" as digits are typed: "21" -> "21/", "211" -> "21/1".
|
|
74
|
-
function maskDateDigits(digits: string): string {
|
|
75
|
-
const day = digits.slice(0, 2);
|
|
76
|
-
const month = digits.slice(2, 4);
|
|
77
|
-
const year = digits.slice(4, 8);
|
|
78
|
-
let out = day;
|
|
79
|
-
if (digits.length >= 2) out += '/';
|
|
80
|
-
out += month;
|
|
81
|
-
if (digits.length >= 4) out += '/';
|
|
82
|
-
return out + year;
|
|
83
|
-
}
|
|
84
|
-
|
|
85
|
-
// `record[field.attribute]` may be empty, a bare "YYYY-MM-DD" (freshly
|
|
86
|
-
// picked, before any time is set) or a full ISO datetime (loaded from an
|
|
87
|
-
// existing instance) — parsed once here so the date button, the time
|
|
88
|
-
// input and cally's own `value` all derive from the same local-time
|
|
89
|
-
// breakdown instead of each re-parsing the raw string their own way.
|
|
90
|
-
const parsedDateTime = $derived.by(() => {
|
|
91
|
-
const raw = record[field.attribute];
|
|
92
|
-
if (raw == null || raw === '') return null;
|
|
93
|
-
const d = new Date(String(raw));
|
|
94
|
-
return isNaN(d.getTime()) ? null : d;
|
|
95
|
-
});
|
|
96
|
-
const datePart = $derived(
|
|
97
|
-
parsedDateTime
|
|
98
|
-
? `${parsedDateTime.getFullYear()}-${pad(parsedDateTime.getMonth() + 1)}-${pad(parsedDateTime.getDate())}`
|
|
99
|
-
: ''
|
|
100
|
-
);
|
|
101
|
-
const timePart = $derived(
|
|
102
|
-
parsedDateTime ? `${pad(parsedDateTime.getHours())}:${pad(parsedDateTime.getMinutes())}` : ''
|
|
103
|
-
);
|
|
104
|
-
|
|
105
|
-
function setDateTime(nextDatePart: string, nextTimePart: string) {
|
|
106
|
-
record[field.attribute] = nextDatePart ? `${nextDatePart}T${nextTimePart || '00:00'}` : '';
|
|
107
|
-
}
|
|
108
|
-
|
|
109
|
-
// Typed display text for the date part, synced from `datePart`.
|
|
110
|
-
// eslint-disable-next-line svelte/prefer-writable-derived
|
|
111
|
-
let dateTextValue = $state('');
|
|
112
|
-
$effect(() => {
|
|
113
|
-
dateTextValue = datePart ? isoDateToDisplay(datePart) : '';
|
|
114
|
-
});
|
|
115
|
-
|
|
116
|
-
function onDateTextInput(event: Event) {
|
|
117
|
-
const raw = (event.currentTarget as HTMLInputElement).value;
|
|
118
|
-
const prevDigits = dateTextValue.replace(/\D/g, '');
|
|
119
|
-
let digits = raw.replace(/\D/g, '');
|
|
120
|
-
// Drops one extra digit when backspacing over an inserted "/".
|
|
121
|
-
if (raw.length < dateTextValue.length && digits.length === prevDigits.length) {
|
|
122
|
-
digits = digits.slice(0, -1);
|
|
123
|
-
}
|
|
124
|
-
digits = digits.slice(0, 8);
|
|
125
|
-
dateTextValue = maskDateDigits(digits);
|
|
126
|
-
if (!digits) {
|
|
127
|
-
setDateTime('', timePart);
|
|
128
|
-
return;
|
|
129
|
-
}
|
|
130
|
-
const parsed = displayToIsoDate(dateTextValue);
|
|
131
|
-
if (parsed) setDateTime(parsed, timePart);
|
|
132
|
-
}
|
|
133
|
-
|
|
134
|
-
function onDateTextBlur() {
|
|
135
|
-
if (!dateTextValue.trim()) {
|
|
136
|
-
setDateTime('', timePart);
|
|
137
|
-
return;
|
|
138
|
-
}
|
|
139
|
-
if (!displayToIsoDate(dateTextValue)) {
|
|
140
|
-
// Discards invalid input.
|
|
141
|
-
dateTextValue = datePart ? isoDateToDisplay(datePart) : '';
|
|
142
|
-
}
|
|
143
|
-
}
|
|
144
|
-
|
|
145
|
-
// cally's `change` event doesn't bubble, so the usual `onchange={...}` prop
|
|
146
|
-
// never fires — Svelte 5 delegates events like `change` to a listener on
|
|
147
|
-
// a shared ancestor, which only ever sees events that bubble up to it.
|
|
148
|
-
// Attaching directly on the element (same fix ColumnFilter's date-range
|
|
149
|
-
// picker already uses) sidesteps delegation entirely.
|
|
150
|
-
$effect(() => {
|
|
151
|
-
if (!calendarDateEl) return;
|
|
152
|
-
function handler() {
|
|
153
|
-
if (fieldDisabled) return;
|
|
154
|
-
setDateTime(calendarDateEl!.value, timePart);
|
|
155
|
-
datePopoverEl?.hidePopover();
|
|
156
|
-
}
|
|
157
|
-
calendarDateEl.addEventListener('change', handler);
|
|
158
|
-
return () => calendarDateEl?.removeEventListener('change', handler);
|
|
159
|
-
});
|
|
160
|
-
</script>
|
|
161
|
-
|
|
162
|
-
{@render labelRow()}
|
|
163
|
-
|
|
164
|
-
{#if readonly}
|
|
165
|
-
<input
|
|
166
|
-
type="text"
|
|
167
|
-
id={field.attribute}
|
|
168
|
-
class="input input-bordered w-full"
|
|
169
|
-
value={formattedValue}
|
|
170
|
-
disabled
|
|
171
|
-
/>
|
|
172
|
-
{:else}
|
|
173
|
-
<input
|
|
174
|
-
type="hidden"
|
|
175
|
-
{name}
|
|
176
|
-
value={String(record[field.attribute] ?? '')}
|
|
177
|
-
disabled={fieldDisabled}
|
|
178
|
-
/>
|
|
179
|
-
<div class="flex gap-2">
|
|
180
|
-
<div class="relative flex-1" style="anchor-name:{dateAnchorName}">
|
|
181
|
-
<input
|
|
182
|
-
type="text"
|
|
183
|
-
id={field.attribute}
|
|
184
|
-
placeholder={field.placeholder ?? 'dd/mm/aaaa'}
|
|
185
|
-
value={dateTextValue}
|
|
186
|
-
oninput={onDateTextInput}
|
|
187
|
-
onblur={onDateTextBlur}
|
|
188
|
-
autocomplete="off"
|
|
189
|
-
disabled={fieldDisabled}
|
|
190
|
-
class="input input-bordered w-full pr-9"
|
|
191
|
-
class:input-error={!!error}
|
|
192
|
-
/>
|
|
193
|
-
<button
|
|
194
|
-
type="button"
|
|
195
|
-
popovertarget={datePopId}
|
|
196
|
-
aria-label={strings.chooseDate}
|
|
197
|
-
disabled={fieldDisabled}
|
|
198
|
-
class="absolute inset-y-0 right-0 flex items-center px-2.5 text-base-content/50 hover:text-base-content disabled:pointer-events-none disabled:opacity-40"
|
|
199
|
-
>
|
|
200
|
-
<svg
|
|
201
|
-
class="size-4"
|
|
202
|
-
xmlns="http://www.w3.org/2000/svg"
|
|
203
|
-
viewBox="0 0 24 24"
|
|
204
|
-
fill="none"
|
|
205
|
-
stroke="currentColor"
|
|
206
|
-
stroke-width="2"
|
|
207
|
-
stroke-linecap="round"
|
|
208
|
-
stroke-linejoin="round"
|
|
209
|
-
>
|
|
210
|
-
<rect x="3" y="4" width="18" height="18" rx="2" />
|
|
211
|
-
<path d="M16 2v4M8 2v4M3 10h18" />
|
|
212
|
-
</svg>
|
|
213
|
-
</button>
|
|
214
|
-
</div>
|
|
215
|
-
<input
|
|
216
|
-
type="time"
|
|
217
|
-
class="input input-bordered w-32 shrink-0"
|
|
218
|
-
value={timePart}
|
|
219
|
-
disabled={fieldDisabled || !record[field.attribute]}
|
|
220
|
-
onchange={(e) => setDateTime(datePart, (e.currentTarget as HTMLInputElement).value)}
|
|
221
|
-
/>
|
|
222
|
-
</div>
|
|
223
|
-
<div
|
|
224
|
-
popover="auto"
|
|
225
|
-
id={datePopId}
|
|
226
|
-
bind:this={datePopoverEl}
|
|
227
|
-
style="position-anchor:{dateAnchorName}; position-try-fallbacks:flip-block;"
|
|
228
|
-
class="dropdown mt-1 rounded-box border border-base-content/10 bg-base-100 p-2 shadow-lg"
|
|
229
|
-
>
|
|
230
|
-
<calendar-date class="cally" bind:this={calendarDateEl} value={datePart}>
|
|
231
|
-
<svg
|
|
232
|
-
aria-label={strings.previous}
|
|
233
|
-
class="fill-current size-4"
|
|
234
|
-
{...{ slot: 'previous' }}
|
|
235
|
-
xmlns="http://www.w3.org/2000/svg"
|
|
236
|
-
viewBox="0 0 24 24"><path fill="currentColor" d="M15.75 19.5 8.25 12l7.5-7.5" /></svg
|
|
237
|
-
>
|
|
238
|
-
<svg
|
|
239
|
-
aria-label={strings.next}
|
|
240
|
-
class="fill-current size-4"
|
|
241
|
-
{...{ slot: 'next' }}
|
|
242
|
-
xmlns="http://www.w3.org/2000/svg"
|
|
243
|
-
viewBox="0 0 24 24"><path fill="currentColor" d="m8.25 4.5 7.5 7.5-7.5 7.5" /></svg
|
|
244
|
-
>
|
|
245
|
-
<calendar-month></calendar-month>
|
|
246
|
-
</calendar-date>
|
|
247
|
-
</div>
|
|
248
|
-
{/if}
|
|
249
|
-
|
|
250
|
-
{@render errorRow()}
|
|
1
|
+
<script lang="ts" generics="T extends object = Record<string, unknown>">
|
|
2
|
+
import { onMount } from 'svelte';
|
|
3
|
+
import type { Snippet } from 'svelte';
|
|
4
|
+
import { getStrings } from '../../../i18n/context.js';
|
|
5
|
+
import type { FieldDefinition } from '../../../types/crud/fields.js';
|
|
6
|
+
|
|
7
|
+
const strings = getStrings();
|
|
8
|
+
|
|
9
|
+
onMount(() => {
|
|
10
|
+
import('cally');
|
|
11
|
+
});
|
|
12
|
+
|
|
13
|
+
let {
|
|
14
|
+
field,
|
|
15
|
+
record = $bindable({} as Record<string, unknown>),
|
|
16
|
+
parentRecord = undefined,
|
|
17
|
+
error = '',
|
|
18
|
+
readonly = false,
|
|
19
|
+
labelRow,
|
|
20
|
+
errorRow
|
|
21
|
+
}: {
|
|
22
|
+
field: FieldDefinition<T>;
|
|
23
|
+
record?: Record<string, unknown>;
|
|
24
|
+
parentRecord?: Record<string, unknown>;
|
|
25
|
+
error?: string;
|
|
26
|
+
readonly?: boolean;
|
|
27
|
+
labelRow: Snippet;
|
|
28
|
+
errorRow: Snippet;
|
|
29
|
+
} = $props();
|
|
30
|
+
|
|
31
|
+
const datePopId = $props.id();
|
|
32
|
+
const dateAnchorName = `--date-anchor-${datePopId}`;
|
|
33
|
+
let datePopoverEl: HTMLElement | undefined = $state();
|
|
34
|
+
let calendarDateEl: (HTMLElement & { value: string }) | undefined = $state();
|
|
35
|
+
|
|
36
|
+
const name = $derived(readonly ? undefined : field.attribute);
|
|
37
|
+
const saved = $derived(record[field.attribute]);
|
|
38
|
+
const displayValue = $derived(saved == null ? '' : String(saved));
|
|
39
|
+
const formattedValue = $derived(
|
|
40
|
+
readonly && field.formatter ? field.formatter(saved, record) : displayValue
|
|
41
|
+
);
|
|
42
|
+
const fieldDisabled = $derived(field.disabled ? field.disabled(record, parentRecord) : false);
|
|
43
|
+
|
|
44
|
+
function pad(n: number): string {
|
|
45
|
+
return String(n).padStart(2, '0');
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
// ISO (yyyy-mm-dd) <-> display (dd/mm/yyyy) conversion and typing mask.
|
|
49
|
+
function isoDateToDisplay(iso: string): string {
|
|
50
|
+
const [year, month, day] = iso.split('-');
|
|
51
|
+
return `${day}/${month}/${year}`;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function displayToIsoDate(display: string): string | null {
|
|
55
|
+
const match = display.trim().match(/^(\d{1,2})\/(\d{1,2})\/(\d{4})$/);
|
|
56
|
+
if (!match) return null;
|
|
57
|
+
const [, dd, mm, yyyy] = match;
|
|
58
|
+
const day = Number(dd);
|
|
59
|
+
const month = Number(mm);
|
|
60
|
+
const year = Number(yyyy);
|
|
61
|
+
// Rejects invalid dates (e.g. 31/02).
|
|
62
|
+
const date = new Date(Date.UTC(year, month - 1, day));
|
|
63
|
+
if (
|
|
64
|
+
date.getUTCFullYear() !== year ||
|
|
65
|
+
date.getUTCMonth() !== month - 1 ||
|
|
66
|
+
date.getUTCDate() !== day
|
|
67
|
+
) {
|
|
68
|
+
return null;
|
|
69
|
+
}
|
|
70
|
+
return `${yyyy}-${mm.padStart(2, '0')}-${dd.padStart(2, '0')}`;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
// Inserts "/" as digits are typed: "21" -> "21/", "211" -> "21/1".
|
|
74
|
+
function maskDateDigits(digits: string): string {
|
|
75
|
+
const day = digits.slice(0, 2);
|
|
76
|
+
const month = digits.slice(2, 4);
|
|
77
|
+
const year = digits.slice(4, 8);
|
|
78
|
+
let out = day;
|
|
79
|
+
if (digits.length >= 2) out += '/';
|
|
80
|
+
out += month;
|
|
81
|
+
if (digits.length >= 4) out += '/';
|
|
82
|
+
return out + year;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
// `record[field.attribute]` may be empty, a bare "YYYY-MM-DD" (freshly
|
|
86
|
+
// picked, before any time is set) or a full ISO datetime (loaded from an
|
|
87
|
+
// existing instance) — parsed once here so the date button, the time
|
|
88
|
+
// input and cally's own `value` all derive from the same local-time
|
|
89
|
+
// breakdown instead of each re-parsing the raw string their own way.
|
|
90
|
+
const parsedDateTime = $derived.by(() => {
|
|
91
|
+
const raw = record[field.attribute];
|
|
92
|
+
if (raw == null || raw === '') return null;
|
|
93
|
+
const d = new Date(String(raw));
|
|
94
|
+
return isNaN(d.getTime()) ? null : d;
|
|
95
|
+
});
|
|
96
|
+
const datePart = $derived(
|
|
97
|
+
parsedDateTime
|
|
98
|
+
? `${parsedDateTime.getFullYear()}-${pad(parsedDateTime.getMonth() + 1)}-${pad(parsedDateTime.getDate())}`
|
|
99
|
+
: ''
|
|
100
|
+
);
|
|
101
|
+
const timePart = $derived(
|
|
102
|
+
parsedDateTime ? `${pad(parsedDateTime.getHours())}:${pad(parsedDateTime.getMinutes())}` : ''
|
|
103
|
+
);
|
|
104
|
+
|
|
105
|
+
function setDateTime(nextDatePart: string, nextTimePart: string) {
|
|
106
|
+
record[field.attribute] = nextDatePart ? `${nextDatePart}T${nextTimePart || '00:00'}` : '';
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
// Typed display text for the date part, synced from `datePart`.
|
|
110
|
+
// eslint-disable-next-line svelte/prefer-writable-derived
|
|
111
|
+
let dateTextValue = $state('');
|
|
112
|
+
$effect(() => {
|
|
113
|
+
dateTextValue = datePart ? isoDateToDisplay(datePart) : '';
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
function onDateTextInput(event: Event) {
|
|
117
|
+
const raw = (event.currentTarget as HTMLInputElement).value;
|
|
118
|
+
const prevDigits = dateTextValue.replace(/\D/g, '');
|
|
119
|
+
let digits = raw.replace(/\D/g, '');
|
|
120
|
+
// Drops one extra digit when backspacing over an inserted "/".
|
|
121
|
+
if (raw.length < dateTextValue.length && digits.length === prevDigits.length) {
|
|
122
|
+
digits = digits.slice(0, -1);
|
|
123
|
+
}
|
|
124
|
+
digits = digits.slice(0, 8);
|
|
125
|
+
dateTextValue = maskDateDigits(digits);
|
|
126
|
+
if (!digits) {
|
|
127
|
+
setDateTime('', timePart);
|
|
128
|
+
return;
|
|
129
|
+
}
|
|
130
|
+
const parsed = displayToIsoDate(dateTextValue);
|
|
131
|
+
if (parsed) setDateTime(parsed, timePart);
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
function onDateTextBlur() {
|
|
135
|
+
if (!dateTextValue.trim()) {
|
|
136
|
+
setDateTime('', timePart);
|
|
137
|
+
return;
|
|
138
|
+
}
|
|
139
|
+
if (!displayToIsoDate(dateTextValue)) {
|
|
140
|
+
// Discards invalid input.
|
|
141
|
+
dateTextValue = datePart ? isoDateToDisplay(datePart) : '';
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
// cally's `change` event doesn't bubble, so the usual `onchange={...}` prop
|
|
146
|
+
// never fires — Svelte 5 delegates events like `change` to a listener on
|
|
147
|
+
// a shared ancestor, which only ever sees events that bubble up to it.
|
|
148
|
+
// Attaching directly on the element (same fix ColumnFilter's date-range
|
|
149
|
+
// picker already uses) sidesteps delegation entirely.
|
|
150
|
+
$effect(() => {
|
|
151
|
+
if (!calendarDateEl) return;
|
|
152
|
+
function handler() {
|
|
153
|
+
if (fieldDisabled) return;
|
|
154
|
+
setDateTime(calendarDateEl!.value, timePart);
|
|
155
|
+
datePopoverEl?.hidePopover();
|
|
156
|
+
}
|
|
157
|
+
calendarDateEl.addEventListener('change', handler);
|
|
158
|
+
return () => calendarDateEl?.removeEventListener('change', handler);
|
|
159
|
+
});
|
|
160
|
+
</script>
|
|
161
|
+
|
|
162
|
+
{@render labelRow()}
|
|
163
|
+
|
|
164
|
+
{#if readonly}
|
|
165
|
+
<input
|
|
166
|
+
type="text"
|
|
167
|
+
id={field.attribute}
|
|
168
|
+
class="input input-bordered w-full"
|
|
169
|
+
value={formattedValue}
|
|
170
|
+
disabled
|
|
171
|
+
/>
|
|
172
|
+
{:else}
|
|
173
|
+
<input
|
|
174
|
+
type="hidden"
|
|
175
|
+
{name}
|
|
176
|
+
value={String(record[field.attribute] ?? '')}
|
|
177
|
+
disabled={fieldDisabled}
|
|
178
|
+
/>
|
|
179
|
+
<div class="flex gap-2">
|
|
180
|
+
<div class="relative flex-1" style="anchor-name:{dateAnchorName}">
|
|
181
|
+
<input
|
|
182
|
+
type="text"
|
|
183
|
+
id={field.attribute}
|
|
184
|
+
placeholder={field.placeholder ?? 'dd/mm/aaaa'}
|
|
185
|
+
value={dateTextValue}
|
|
186
|
+
oninput={onDateTextInput}
|
|
187
|
+
onblur={onDateTextBlur}
|
|
188
|
+
autocomplete="off"
|
|
189
|
+
disabled={fieldDisabled}
|
|
190
|
+
class="input input-bordered w-full pr-9"
|
|
191
|
+
class:input-error={!!error}
|
|
192
|
+
/>
|
|
193
|
+
<button
|
|
194
|
+
type="button"
|
|
195
|
+
popovertarget={datePopId}
|
|
196
|
+
aria-label={strings.chooseDate}
|
|
197
|
+
disabled={fieldDisabled}
|
|
198
|
+
class="absolute inset-y-0 right-0 flex items-center px-2.5 text-base-content/50 hover:text-base-content disabled:pointer-events-none disabled:opacity-40"
|
|
199
|
+
>
|
|
200
|
+
<svg
|
|
201
|
+
class="size-4"
|
|
202
|
+
xmlns="http://www.w3.org/2000/svg"
|
|
203
|
+
viewBox="0 0 24 24"
|
|
204
|
+
fill="none"
|
|
205
|
+
stroke="currentColor"
|
|
206
|
+
stroke-width="2"
|
|
207
|
+
stroke-linecap="round"
|
|
208
|
+
stroke-linejoin="round"
|
|
209
|
+
>
|
|
210
|
+
<rect x="3" y="4" width="18" height="18" rx="2" />
|
|
211
|
+
<path d="M16 2v4M8 2v4M3 10h18" />
|
|
212
|
+
</svg>
|
|
213
|
+
</button>
|
|
214
|
+
</div>
|
|
215
|
+
<input
|
|
216
|
+
type="time"
|
|
217
|
+
class="input input-bordered w-32 shrink-0"
|
|
218
|
+
value={timePart}
|
|
219
|
+
disabled={fieldDisabled || !record[field.attribute]}
|
|
220
|
+
onchange={(e) => setDateTime(datePart, (e.currentTarget as HTMLInputElement).value)}
|
|
221
|
+
/>
|
|
222
|
+
</div>
|
|
223
|
+
<div
|
|
224
|
+
popover="auto"
|
|
225
|
+
id={datePopId}
|
|
226
|
+
bind:this={datePopoverEl}
|
|
227
|
+
style="position-anchor:{dateAnchorName}; position-try-fallbacks:flip-block;"
|
|
228
|
+
class="dropdown mt-1 rounded-box border border-base-content/10 bg-base-100 p-2 shadow-lg"
|
|
229
|
+
>
|
|
230
|
+
<calendar-date class="cally" bind:this={calendarDateEl} value={datePart}>
|
|
231
|
+
<svg
|
|
232
|
+
aria-label={strings.previous}
|
|
233
|
+
class="fill-current size-4"
|
|
234
|
+
{...{ slot: 'previous' }}
|
|
235
|
+
xmlns="http://www.w3.org/2000/svg"
|
|
236
|
+
viewBox="0 0 24 24"><path fill="currentColor" d="M15.75 19.5 8.25 12l7.5-7.5" /></svg
|
|
237
|
+
>
|
|
238
|
+
<svg
|
|
239
|
+
aria-label={strings.next}
|
|
240
|
+
class="fill-current size-4"
|
|
241
|
+
{...{ slot: 'next' }}
|
|
242
|
+
xmlns="http://www.w3.org/2000/svg"
|
|
243
|
+
viewBox="0 0 24 24"><path fill="currentColor" d="m8.25 4.5 7.5 7.5-7.5 7.5" /></svg
|
|
244
|
+
>
|
|
245
|
+
<calendar-month></calendar-month>
|
|
246
|
+
</calendar-date>
|
|
247
|
+
</div>
|
|
248
|
+
{/if}
|
|
249
|
+
|
|
250
|
+
{@render errorRow()}
|
|
@@ -1,67 +1,67 @@
|
|
|
1
|
-
<script lang="ts" generics="T extends object = Record<string, unknown>">
|
|
2
|
-
import type { Snippet } from 'svelte';
|
|
3
|
-
import type { FieldDefinition } from '../../../types/crud/fields.js';
|
|
4
|
-
|
|
5
|
-
let {
|
|
6
|
-
field,
|
|
7
|
-
record = $bindable({} as Record<string, unknown>),
|
|
8
|
-
parentRecord = undefined,
|
|
9
|
-
error = '',
|
|
10
|
-
readonly = false,
|
|
11
|
-
labelRow,
|
|
12
|
-
errorRow
|
|
13
|
-
}: {
|
|
14
|
-
field: FieldDefinition<T>;
|
|
15
|
-
record?: Record<string, unknown>;
|
|
16
|
-
parentRecord?: Record<string, unknown>;
|
|
17
|
-
error?: string;
|
|
18
|
-
readonly?: boolean;
|
|
19
|
-
labelRow: Snippet;
|
|
20
|
-
errorRow: Snippet;
|
|
21
|
-
} = $props();
|
|
22
|
-
|
|
23
|
-
const name = $derived(readonly ? undefined : field.attribute);
|
|
24
|
-
const saved = $derived(record[field.attribute]);
|
|
25
|
-
const displayValue = $derived(saved == null ? '' : String(saved));
|
|
26
|
-
const formattedValue = $derived(
|
|
27
|
-
readonly && field.formatter ? field.formatter(saved, record) : displayValue
|
|
28
|
-
);
|
|
29
|
-
const fieldDisabled = $derived(field.disabled ? field.disabled(record, parentRecord) : false);
|
|
30
|
-
</script>
|
|
31
|
-
|
|
32
|
-
{@render labelRow()}
|
|
33
|
-
|
|
34
|
-
{#if readonly}
|
|
35
|
-
{#if field.formatter}
|
|
36
|
-
<div
|
|
37
|
-
id={field.attribute}
|
|
38
|
-
class="input input-bordered w-full bg-base-200 border-base-200 text-base-content/40 shadow-none cursor-not-allowed"
|
|
39
|
-
>
|
|
40
|
-
<!-- eslint-disable-next-line svelte/no-at-html-tags -->
|
|
41
|
-
{@html formattedValue}
|
|
42
|
-
</div>
|
|
43
|
-
{:else}
|
|
44
|
-
<input
|
|
45
|
-
type={field.type ?? 'text'}
|
|
46
|
-
id={field.attribute}
|
|
47
|
-
class="input input-bordered w-full"
|
|
48
|
-
value={formattedValue}
|
|
49
|
-
disabled
|
|
50
|
-
/>
|
|
51
|
-
{/if}
|
|
52
|
-
{:else}
|
|
53
|
-
<input
|
|
54
|
-
type={field.type ?? 'text'}
|
|
55
|
-
id={field.attribute}
|
|
56
|
-
{name}
|
|
57
|
-
placeholder={field.placeholder ?? ''}
|
|
58
|
-
bind:value={record[field.attribute]}
|
|
59
|
-
autocomplete={field.autocomplete}
|
|
60
|
-
step={field.type === 'number' ? 'any' : undefined}
|
|
61
|
-
class="input input-bordered w-full"
|
|
62
|
-
class:input-error={!!error}
|
|
63
|
-
disabled={fieldDisabled}
|
|
64
|
-
/>
|
|
65
|
-
{/if}
|
|
66
|
-
|
|
67
|
-
{@render errorRow()}
|
|
1
|
+
<script lang="ts" generics="T extends object = Record<string, unknown>">
|
|
2
|
+
import type { Snippet } from 'svelte';
|
|
3
|
+
import type { FieldDefinition } from '../../../types/crud/fields.js';
|
|
4
|
+
|
|
5
|
+
let {
|
|
6
|
+
field,
|
|
7
|
+
record = $bindable({} as Record<string, unknown>),
|
|
8
|
+
parentRecord = undefined,
|
|
9
|
+
error = '',
|
|
10
|
+
readonly = false,
|
|
11
|
+
labelRow,
|
|
12
|
+
errorRow
|
|
13
|
+
}: {
|
|
14
|
+
field: FieldDefinition<T>;
|
|
15
|
+
record?: Record<string, unknown>;
|
|
16
|
+
parentRecord?: Record<string, unknown>;
|
|
17
|
+
error?: string;
|
|
18
|
+
readonly?: boolean;
|
|
19
|
+
labelRow: Snippet;
|
|
20
|
+
errorRow: Snippet;
|
|
21
|
+
} = $props();
|
|
22
|
+
|
|
23
|
+
const name = $derived(readonly ? undefined : field.attribute);
|
|
24
|
+
const saved = $derived(record[field.attribute]);
|
|
25
|
+
const displayValue = $derived(saved == null ? '' : String(saved));
|
|
26
|
+
const formattedValue = $derived(
|
|
27
|
+
readonly && field.formatter ? field.formatter(saved, record) : displayValue
|
|
28
|
+
);
|
|
29
|
+
const fieldDisabled = $derived(field.disabled ? field.disabled(record, parentRecord) : false);
|
|
30
|
+
</script>
|
|
31
|
+
|
|
32
|
+
{@render labelRow()}
|
|
33
|
+
|
|
34
|
+
{#if readonly}
|
|
35
|
+
{#if field.formatter}
|
|
36
|
+
<div
|
|
37
|
+
id={field.attribute}
|
|
38
|
+
class="input input-bordered w-full bg-base-200 border-base-200 text-base-content/40 shadow-none cursor-not-allowed"
|
|
39
|
+
>
|
|
40
|
+
<!-- eslint-disable-next-line svelte/no-at-html-tags -->
|
|
41
|
+
{@html formattedValue}
|
|
42
|
+
</div>
|
|
43
|
+
{:else}
|
|
44
|
+
<input
|
|
45
|
+
type={field.type ?? 'text'}
|
|
46
|
+
id={field.attribute}
|
|
47
|
+
class="input input-bordered w-full"
|
|
48
|
+
value={formattedValue}
|
|
49
|
+
disabled
|
|
50
|
+
/>
|
|
51
|
+
{/if}
|
|
52
|
+
{:else}
|
|
53
|
+
<input
|
|
54
|
+
type={field.type ?? 'text'}
|
|
55
|
+
id={field.attribute}
|
|
56
|
+
{name}
|
|
57
|
+
placeholder={field.placeholder ?? ''}
|
|
58
|
+
bind:value={record[field.attribute]}
|
|
59
|
+
autocomplete={field.autocomplete}
|
|
60
|
+
step={field.type === 'number' ? 'any' : undefined}
|
|
61
|
+
class="input input-bordered w-full"
|
|
62
|
+
class:input-error={!!error}
|
|
63
|
+
disabled={fieldDisabled}
|
|
64
|
+
/>
|
|
65
|
+
{/if}
|
|
66
|
+
|
|
67
|
+
{@render errorRow()}
|