@sortsys/ui 0.1.7 → 0.1.9
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/dev.js +117 -58
- package/dist/index.d.ts +21 -1
- package/dist/index.js +117 -58
- package/dist/styles/default.css +1 -1
- package/package.json +1 -5
- package/dist/dev.jsx +0 -1607
- package/dist/index.jsx +0 -1607
package/dist/index.jsx
DELETED
|
@@ -1,1607 +0,0 @@
|
|
|
1
|
-
// src/components/SSButton.tsx
|
|
2
|
-
function SSButton(props) {
|
|
3
|
-
const classes = () => [
|
|
4
|
-
"ss_button",
|
|
5
|
-
props.isIconOnly ? "ss_button--icon" : "",
|
|
6
|
-
props.class ?? ""
|
|
7
|
-
].filter(Boolean).join(" ");
|
|
8
|
-
return <button
|
|
9
|
-
type={props.type ?? "button"}
|
|
10
|
-
class={classes()}
|
|
11
|
-
disabled={props.disabled}
|
|
12
|
-
aria-label={props.ariaLabel}
|
|
13
|
-
form={props.form}
|
|
14
|
-
onclick={props.onclick}
|
|
15
|
-
>
|
|
16
|
-
{props.children}
|
|
17
|
-
</button>;
|
|
18
|
-
}
|
|
19
|
-
|
|
20
|
-
// src/components/SSCallout.tsx
|
|
21
|
-
function SSCallout(props) {
|
|
22
|
-
const { icon, color, class: className, style, children: children2, ...rest } = props;
|
|
23
|
-
return <div
|
|
24
|
-
{...rest}
|
|
25
|
-
class={`ss_callout ss_callout--${color} ${className ?? ""}`}
|
|
26
|
-
style={style}
|
|
27
|
-
>
|
|
28
|
-
<span class="ss_callout__icon">
|
|
29
|
-
{icon}
|
|
30
|
-
</span>
|
|
31
|
-
<div class="ss_callout__content">
|
|
32
|
-
<span>{children2}</span>
|
|
33
|
-
</div>
|
|
34
|
-
</div>;
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
// src/components/SSChip.tsx
|
|
38
|
-
function SSChip(props) {
|
|
39
|
-
const commonClass = `ss_chip ss_chip--${props.color ?? "blue"} ${props.class ?? ""}`;
|
|
40
|
-
if ("onclick" in props && props.onclick) {
|
|
41
|
-
return <button
|
|
42
|
-
type="button"
|
|
43
|
-
class={`${commonClass} ss_chip--clickable`}
|
|
44
|
-
style={props.style}
|
|
45
|
-
onclick={props.onclick}
|
|
46
|
-
>
|
|
47
|
-
<span class="ss_chip__label">{props.children}</span>
|
|
48
|
-
</button>;
|
|
49
|
-
}
|
|
50
|
-
return <div class={commonClass} style={props.style}>
|
|
51
|
-
<span class="ss_chip__label">{props.children}</span>
|
|
52
|
-
{"ondismiss" in props && props.ondismiss && <button
|
|
53
|
-
type="button"
|
|
54
|
-
class="ss_chip__dismiss"
|
|
55
|
-
aria-label="Entfernen"
|
|
56
|
-
onclick={props.ondismiss}
|
|
57
|
-
>
|
|
58
|
-
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="icon icon-tabler icons-tabler-outline icon-tabler-x"><path stroke="none" d="M0 0h24v24H0z" fill="none" /><path d="M18 6l-12 12" /><path d="M6 6l12 12" /></svg>
|
|
59
|
-
</button>}
|
|
60
|
-
</div>;
|
|
61
|
-
}
|
|
62
|
-
|
|
63
|
-
// src/components/SSDataTable.tsx
|
|
64
|
-
import { createEffect, createMemo, createResource, createSignal, For } from "solid-js";
|
|
65
|
-
function SSDataCell(props) {
|
|
66
|
-
const rendered = createMemo(() => props.render(props.row));
|
|
67
|
-
const [asyncContent] = createResource(async () => {
|
|
68
|
-
const value = rendered();
|
|
69
|
-
if (value && typeof value.then === "function") {
|
|
70
|
-
return await value;
|
|
71
|
-
}
|
|
72
|
-
return null;
|
|
73
|
-
});
|
|
74
|
-
return <div>
|
|
75
|
-
{(() => {
|
|
76
|
-
const value = rendered();
|
|
77
|
-
if (value && typeof value.then === "function") {
|
|
78
|
-
return asyncContent() ?? "";
|
|
79
|
-
}
|
|
80
|
-
return value;
|
|
81
|
-
})()}
|
|
82
|
-
</div>;
|
|
83
|
-
}
|
|
84
|
-
function SSDataTable(props) {
|
|
85
|
-
const [sortIndex, setSortIndex] = createSignal(-1);
|
|
86
|
-
const [sortDir, setSortDir] = createSignal(null);
|
|
87
|
-
const [page, setPage] = createSignal(1);
|
|
88
|
-
const pageSize = () => Math.max(1, props.pageSize ?? 25);
|
|
89
|
-
const sortedRows = createMemo(() => {
|
|
90
|
-
const index = sortIndex();
|
|
91
|
-
const dir = sortDir();
|
|
92
|
-
if (index < 0 || !dir) return props.rows;
|
|
93
|
-
const column = props.columns[index];
|
|
94
|
-
if (!column?.sortKey) return props.rows;
|
|
95
|
-
const entries = props.rows.map((row, idx) => ({
|
|
96
|
-
row,
|
|
97
|
-
idx,
|
|
98
|
-
key: column.sortKey(row)
|
|
99
|
-
}));
|
|
100
|
-
entries.sort((a, b) => {
|
|
101
|
-
if (a.key === b.key) return a.idx - b.idx;
|
|
102
|
-
if (a.key < b.key) return dir === "asc" ? -1 : 1;
|
|
103
|
-
return dir === "asc" ? 1 : -1;
|
|
104
|
-
});
|
|
105
|
-
return entries.map((entry) => entry.row);
|
|
106
|
-
});
|
|
107
|
-
const totalPages = createMemo(() => {
|
|
108
|
-
return Math.max(1, Math.ceil(sortedRows().length / pageSize()));
|
|
109
|
-
});
|
|
110
|
-
const pagedRows = createMemo(() => {
|
|
111
|
-
const current = Math.min(page(), totalPages());
|
|
112
|
-
const start = (current - 1) * pageSize();
|
|
113
|
-
return sortedRows().slice(start, start + pageSize());
|
|
114
|
-
});
|
|
115
|
-
createEffect(() => {
|
|
116
|
-
if (page() > totalPages()) setPage(totalPages());
|
|
117
|
-
});
|
|
118
|
-
const toggleSort = (index) => {
|
|
119
|
-
if (sortIndex() !== index) {
|
|
120
|
-
setSortIndex(index);
|
|
121
|
-
setSortDir("asc");
|
|
122
|
-
setPage(1);
|
|
123
|
-
return;
|
|
124
|
-
}
|
|
125
|
-
if (sortDir() === "asc") {
|
|
126
|
-
setSortDir("desc");
|
|
127
|
-
return;
|
|
128
|
-
}
|
|
129
|
-
if (sortDir() === "desc") {
|
|
130
|
-
setSortIndex(-1);
|
|
131
|
-
setSortDir(null);
|
|
132
|
-
setPage(1);
|
|
133
|
-
return;
|
|
134
|
-
}
|
|
135
|
-
setSortDir("asc");
|
|
136
|
-
setPage(1);
|
|
137
|
-
};
|
|
138
|
-
const goToPage = (next) => {
|
|
139
|
-
const safePage = Math.min(Math.max(1, next), totalPages());
|
|
140
|
-
setPage(safePage);
|
|
141
|
-
};
|
|
142
|
-
const paginationPosition = () => props.paginationPosition ?? "bottom";
|
|
143
|
-
const containerClass = () => `ss_table ${paginationPosition() === "top" ? "ss_table--pagination-top" : ""} ${props.class ?? ""}`;
|
|
144
|
-
const paginationBar = () => <div class="ss_table__pagination">
|
|
145
|
-
<button
|
|
146
|
-
type="button"
|
|
147
|
-
class="ss_table__page_button"
|
|
148
|
-
disabled={page() === 1}
|
|
149
|
-
aria-label="Erste Seite"
|
|
150
|
-
onclick={() => goToPage(1)}
|
|
151
|
-
>
|
|
152
|
-
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="icon icon-tabler icons-tabler-outline icon-tabler-chevron-left-pipe"><path stroke="none" d="M0 0h24v24H0z" fill="none" /><path d="M7 6v12" /><path d="M18 6l-6 6l6 6" /></svg>
|
|
153
|
-
</button>
|
|
154
|
-
<button
|
|
155
|
-
type="button"
|
|
156
|
-
class="ss_table__page_button"
|
|
157
|
-
disabled={page() === 1}
|
|
158
|
-
aria-label="Vorherige Seite"
|
|
159
|
-
onclick={() => goToPage(page() - 1)}
|
|
160
|
-
>
|
|
161
|
-
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="icon icon-tabler icons-tabler-outline icon-tabler-chevron-left"><path stroke="none" d="M0 0h24v24H0z" fill="none" /><path d="M15 6l-6 6l6 6" /></svg>
|
|
162
|
-
</button>
|
|
163
|
-
<span class="ss_table__page_info">
|
|
164
|
-
Seite {page()} von {totalPages()}
|
|
165
|
-
</span>
|
|
166
|
-
<button
|
|
167
|
-
type="button"
|
|
168
|
-
class="ss_table__page_button"
|
|
169
|
-
disabled={page() === totalPages()}
|
|
170
|
-
aria-label="Nächste Seite"
|
|
171
|
-
onclick={() => goToPage(page() + 1)}
|
|
172
|
-
>
|
|
173
|
-
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="icon icon-tabler icons-tabler-outline icon-tabler-chevron-right"><path stroke="none" d="M0 0h24v24H0z" fill="none" /><path d="M9 6l6 6l-6 6" /></svg>
|
|
174
|
-
</button>
|
|
175
|
-
<button
|
|
176
|
-
type="button"
|
|
177
|
-
class="ss_table__page_button"
|
|
178
|
-
disabled={page() === totalPages()}
|
|
179
|
-
aria-label="Letzte Seite"
|
|
180
|
-
onclick={() => goToPage(totalPages())}
|
|
181
|
-
>
|
|
182
|
-
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="icon icon-tabler icons-tabler-outline icon-tabler-chevron-right-pipe"><path stroke="none" d="M0 0h24v24H0z" fill="none" /><path d="M6 6l6 6l-6 6" /><path d="M17 5v13" /></svg>
|
|
183
|
-
</button>
|
|
184
|
-
</div>;
|
|
185
|
-
return <div class={containerClass()} style={props.style}>
|
|
186
|
-
{paginationPosition() === "top" && paginationBar()}
|
|
187
|
-
<div class="ss_table__scroll">
|
|
188
|
-
<table>
|
|
189
|
-
<thead>
|
|
190
|
-
<tr>
|
|
191
|
-
<For each={props.columns}>
|
|
192
|
-
{(column, index) => {
|
|
193
|
-
const sortable = !!column.sortKey;
|
|
194
|
-
const isActive = () => sortIndex() === index();
|
|
195
|
-
const currentDir = () => isActive() ? sortDir() : null;
|
|
196
|
-
return <th>
|
|
197
|
-
{sortable ? <button
|
|
198
|
-
type="button"
|
|
199
|
-
class="ss_table__sort_button"
|
|
200
|
-
aria-sort={currentDir() === "asc" ? "ascending" : currentDir() === "desc" ? "descending" : "none"}
|
|
201
|
-
data-sort={currentDir() ?? "none"}
|
|
202
|
-
onclick={() => toggleSort(index())}
|
|
203
|
-
>
|
|
204
|
-
{column.label}
|
|
205
|
-
<span class="ss_table__sort_icon" aria-hidden="true">
|
|
206
|
-
{currentDir() === "asc" ? <svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="icon icon-tabler icons-tabler-outline icon-tabler-chevron-down"><path stroke="none" d="M0 0h24v24H0z" fill="none" /><path d="M6 9l6 6l6 -6" /></svg> : currentDir() === "desc" ? <svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="icon icon-tabler icons-tabler-outline icon-tabler-chevron-up"><path stroke="none" d="M0 0h24v24H0z" fill="none" /><path d="M6 15l6 -6l6 6" /></svg> : <svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="icon icon-tabler icons-tabler-outline icon-tabler-selector"><path stroke="none" d="M0 0h24v24H0z" fill="none" /><path d="M8 9l4 -4l4 4" /><path d="M16 15l-4 4l-4 -4" /></svg>}
|
|
207
|
-
</span>
|
|
208
|
-
</button> : <span class="ss_table__header_label">{column.label}</span>}
|
|
209
|
-
</th>;
|
|
210
|
-
}}
|
|
211
|
-
</For>
|
|
212
|
-
</tr>
|
|
213
|
-
</thead>
|
|
214
|
-
<tbody>
|
|
215
|
-
<For each={pagedRows()}>
|
|
216
|
-
{(row) => <tr
|
|
217
|
-
data-clickable={props.onRowClick ? "true" : void 0}
|
|
218
|
-
onclick={() => props.onRowClick?.(row)}
|
|
219
|
-
>
|
|
220
|
-
<For each={props.columns}>
|
|
221
|
-
{(column) => <td>
|
|
222
|
-
<SSDataCell row={row} render={column.render} />
|
|
223
|
-
</td>}
|
|
224
|
-
</For>
|
|
225
|
-
</tr>}
|
|
226
|
-
</For>
|
|
227
|
-
</tbody>
|
|
228
|
-
</table>
|
|
229
|
-
</div>
|
|
230
|
-
{paginationPosition() === "bottom" && paginationBar()}
|
|
231
|
-
</div>;
|
|
232
|
-
}
|
|
233
|
-
|
|
234
|
-
// src/components/SSDropdown.tsx
|
|
235
|
-
import { createSignal as createSignal2, For as For2, onCleanup, onMount, Show, createEffect as createEffect2 } from "solid-js";
|
|
236
|
-
function SSDropdown(props) {
|
|
237
|
-
const [open, setOpen] = createSignal2(false);
|
|
238
|
-
const [renderMenu, setRenderMenu] = createSignal2(false);
|
|
239
|
-
const [menuState, setMenuState] = createSignal2("closed");
|
|
240
|
-
let rootRef;
|
|
241
|
-
const close = () => setOpen(false);
|
|
242
|
-
createEffect2(() => {
|
|
243
|
-
if (open()) {
|
|
244
|
-
setRenderMenu(true);
|
|
245
|
-
requestAnimationFrame(() => setMenuState("open"));
|
|
246
|
-
return;
|
|
247
|
-
}
|
|
248
|
-
if (!renderMenu()) return;
|
|
249
|
-
setMenuState("closed");
|
|
250
|
-
const timeout = window.setTimeout(() => setRenderMenu(false), 160);
|
|
251
|
-
onCleanup(() => window.clearTimeout(timeout));
|
|
252
|
-
});
|
|
253
|
-
onMount(() => {
|
|
254
|
-
const handlePointerDown = (event) => {
|
|
255
|
-
const target = event.target;
|
|
256
|
-
if (!rootRef || !target) return;
|
|
257
|
-
if (!rootRef.contains(target)) close();
|
|
258
|
-
};
|
|
259
|
-
const handleKeyDown = (event) => {
|
|
260
|
-
if (event.key === "Escape") close();
|
|
261
|
-
};
|
|
262
|
-
document.addEventListener("mousedown", handlePointerDown);
|
|
263
|
-
window.addEventListener("keydown", handleKeyDown);
|
|
264
|
-
onCleanup(() => {
|
|
265
|
-
document.removeEventListener("mousedown", handlePointerDown);
|
|
266
|
-
window.removeEventListener("keydown", handleKeyDown);
|
|
267
|
-
});
|
|
268
|
-
});
|
|
269
|
-
return <div class={`ss_dropdown ${props.class ?? ""}`} style={props.style} ref={(el) => rootRef = el}>
|
|
270
|
-
<button
|
|
271
|
-
type="button"
|
|
272
|
-
class="ss_dropdown__trigger ss_button ss_button--icon"
|
|
273
|
-
aria-haspopup="menu"
|
|
274
|
-
aria-expanded={open()}
|
|
275
|
-
aria-label={props.ariaLabel ?? "Aktionen \xF6ffnen"}
|
|
276
|
-
onclick={() => setOpen((value) => !value)}
|
|
277
|
-
>
|
|
278
|
-
{props.icon ?? <svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="icon icon-tabler icons-tabler-outline icon-tabler-dots-vertical"><path stroke="none" d="M0 0h24v24H0z" fill="none" /><path d="M11 12a1 1 0 1 0 2 0a1 1 0 1 0 -2 0" /><path d="M11 19a1 1 0 1 0 2 0a1 1 0 1 0 -2 0" /><path d="M11 5a1 1 0 1 0 2 0a1 1 0 1 0 -2 0" /></svg>}
|
|
279
|
-
</button>
|
|
280
|
-
|
|
281
|
-
<Show when={renderMenu()}>
|
|
282
|
-
<div
|
|
283
|
-
class="ss_dropdown__menu"
|
|
284
|
-
role="menu"
|
|
285
|
-
data-state={menuState()}
|
|
286
|
-
>
|
|
287
|
-
<For2 each={props.items}>
|
|
288
|
-
{(item) => <button
|
|
289
|
-
type="button"
|
|
290
|
-
class="ss_dropdown__item"
|
|
291
|
-
role={item.checked ? "menuitemcheckbox" : "menuitem"}
|
|
292
|
-
aria-checked={item.checked ? "true" : void 0}
|
|
293
|
-
onclick={async () => {
|
|
294
|
-
close();
|
|
295
|
-
await item.onclick?.();
|
|
296
|
-
}}
|
|
297
|
-
>
|
|
298
|
-
<span class="ss_dropdown__item_icon">{item.icon}</span>
|
|
299
|
-
<span class="ss_dropdown__item_label">{item.label}</span>
|
|
300
|
-
{item.checked && <span class="ss_dropdown__item_check" aria-hidden="true">
|
|
301
|
-
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="icon icon-tabler icons-tabler-outline icon-tabler-check"><path stroke="none" d="M0 0h24v24H0z" fill="none" /><path d="M5 12l5 5l10 -10" /></svg>
|
|
302
|
-
</span>}
|
|
303
|
-
</button>}
|
|
304
|
-
</For2>
|
|
305
|
-
</div>
|
|
306
|
-
</Show>
|
|
307
|
-
</div>;
|
|
308
|
-
}
|
|
309
|
-
|
|
310
|
-
// src/components/SSExpandable.tsx
|
|
311
|
-
import { createMemo as createMemo2, createSignal as createSignal3, onCleanup as onCleanup2 } from "solid-js";
|
|
312
|
-
var TRANSITION_MS = 200;
|
|
313
|
-
function SSExpandable(props) {
|
|
314
|
-
const [height, setHeight] = createSignal3(props.initiallyExpanded ? "auto" : 0);
|
|
315
|
-
const isExpanded = createMemo2(() => height() !== 0);
|
|
316
|
-
let contentRef;
|
|
317
|
-
let timeoutId;
|
|
318
|
-
const toggle = () => {
|
|
319
|
-
if (timeoutId) clearTimeout(timeoutId);
|
|
320
|
-
const targetHeight = contentRef?.scrollHeight ?? 0;
|
|
321
|
-
if (isExpanded()) {
|
|
322
|
-
setHeight(targetHeight);
|
|
323
|
-
timeoutId = window.setTimeout(() => setHeight(0), 1);
|
|
324
|
-
return;
|
|
325
|
-
}
|
|
326
|
-
setHeight(targetHeight);
|
|
327
|
-
timeoutId = window.setTimeout(() => setHeight("auto"), TRANSITION_MS);
|
|
328
|
-
};
|
|
329
|
-
onCleanup2(() => {
|
|
330
|
-
if (timeoutId) clearTimeout(timeoutId);
|
|
331
|
-
});
|
|
332
|
-
return <div
|
|
333
|
-
class={`ss_expandable ${props.class ?? ""}`}
|
|
334
|
-
style={props.style}
|
|
335
|
-
data-state={isExpanded() ? "open" : "closed"}
|
|
336
|
-
>
|
|
337
|
-
<div
|
|
338
|
-
class="ss_expandable__header"
|
|
339
|
-
role="button"
|
|
340
|
-
tabindex="0"
|
|
341
|
-
aria-expanded={isExpanded()}
|
|
342
|
-
onclick={toggle}
|
|
343
|
-
onkeydown={(event) => {
|
|
344
|
-
if (event.key === "Enter" || event.key === " ") {
|
|
345
|
-
event.preventDefault();
|
|
346
|
-
toggle();
|
|
347
|
-
}
|
|
348
|
-
}}
|
|
349
|
-
>
|
|
350
|
-
<span class="ss_expandable__icon" aria-hidden="true">
|
|
351
|
-
{isExpanded() ? <svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="icon icon-tabler icons-tabler-outline icon-tabler-chevron-down"><path stroke="none" d="M0 0h24v24H0z" fill="none" /><path d="M6 9l6 6l6 -6" /></svg> : <svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="icon icon-tabler icons-tabler-outline icon-tabler-chevron-right"><path stroke="none" d="M0 0h24v24H0z" fill="none" /><path d="M9 6l6 6l-6 6" /></svg>}
|
|
352
|
-
</span>
|
|
353
|
-
<span class="ss_expandable__title">{props.title}</span>
|
|
354
|
-
</div>
|
|
355
|
-
|
|
356
|
-
<div
|
|
357
|
-
ref={(el) => contentRef = el}
|
|
358
|
-
class="ss_expandable__content"
|
|
359
|
-
style={{
|
|
360
|
-
height: typeof height() === "number" ? `${height()}px` : height(),
|
|
361
|
-
"transition-duration": `${TRANSITION_MS}ms`
|
|
362
|
-
}}
|
|
363
|
-
>
|
|
364
|
-
<div class='ss_expandable__divider_wrapper'>
|
|
365
|
-
<div class="ss_expandable__divider" />
|
|
366
|
-
{props.children}
|
|
367
|
-
</div>
|
|
368
|
-
</div>
|
|
369
|
-
</div>;
|
|
370
|
-
}
|
|
371
|
-
|
|
372
|
-
// src/components/SSForm.tsx
|
|
373
|
-
import { createContext, createEffect as createEffect3, createMemo as createMemo3, createSignal as createSignal5, createUniqueId, For as For3, onCleanup as onCleanup3, onMount as onMount2, untrack, useContext } from "solid-js";
|
|
374
|
-
|
|
375
|
-
// src/hooks/createLoading.ts
|
|
376
|
-
import { createSignal as createSignal4 } from "solid-js";
|
|
377
|
-
function createLoading() {
|
|
378
|
-
const [loading, setLoading] = createSignal4(false);
|
|
379
|
-
return [loading, async (callback) => {
|
|
380
|
-
if (loading()) return;
|
|
381
|
-
try {
|
|
382
|
-
setLoading(true);
|
|
383
|
-
await callback();
|
|
384
|
-
} finally {
|
|
385
|
-
setLoading(false);
|
|
386
|
-
}
|
|
387
|
-
}];
|
|
388
|
-
}
|
|
389
|
-
|
|
390
|
-
// src/components/SSForm.tsx
|
|
391
|
-
var SSFormContext = createContext();
|
|
392
|
-
var SSForm = function(props) {
|
|
393
|
-
const [loading, process] = createLoading();
|
|
394
|
-
const fields = {};
|
|
395
|
-
const context = {
|
|
396
|
-
loading,
|
|
397
|
-
process: (action) => process(() => action(context)),
|
|
398
|
-
field: (name) => fields[name] ?? null,
|
|
399
|
-
setField: (name, field) => {
|
|
400
|
-
fields[name] = field;
|
|
401
|
-
},
|
|
402
|
-
delField: (name) => {
|
|
403
|
-
delete fields[name];
|
|
404
|
-
},
|
|
405
|
-
setValues: (values) => Object.entries(values).forEach(([name, value]) => fields[name]?.setValue(value)),
|
|
406
|
-
getValues: async () => {
|
|
407
|
-
const entries = Object.entries(fields);
|
|
408
|
-
const values = await Promise.all(entries.map(([, field]) => field.getValue()));
|
|
409
|
-
return Object.fromEntries(entries.map(([name], i) => [name, values[i]]));
|
|
410
|
-
},
|
|
411
|
-
validate: () => Promise.all(Object.values(fields).map((field) => field.validate())).then(),
|
|
412
|
-
hasError: () => Object.values(fields).some((field) => field.hasError()),
|
|
413
|
-
submit: () => process(async () => {
|
|
414
|
-
await context.validate();
|
|
415
|
-
if (context.hasError()) return;
|
|
416
|
-
await props.onsubmit?.(context);
|
|
417
|
-
})
|
|
418
|
-
};
|
|
419
|
-
return <form class="ss_form" onsubmit={(e) => {
|
|
420
|
-
e.preventDefault();
|
|
421
|
-
context.submit();
|
|
422
|
-
}}>
|
|
423
|
-
<SSFormContext.Provider value={context}>
|
|
424
|
-
{props.children}
|
|
425
|
-
</SSFormContext.Provider>
|
|
426
|
-
</form>;
|
|
427
|
-
};
|
|
428
|
-
SSForm.Input = function(props) {
|
|
429
|
-
const context = useContext(SSFormContext);
|
|
430
|
-
const [value, setValue] = createSignal5("");
|
|
431
|
-
const [error, setError] = createSignal5(null);
|
|
432
|
-
const field = {
|
|
433
|
-
getValue: () => {
|
|
434
|
-
const trimmed = value().trim();
|
|
435
|
-
if (!trimmed) return null;
|
|
436
|
-
return trimmed;
|
|
437
|
-
},
|
|
438
|
-
setValue: (value2) => {
|
|
439
|
-
if (typeof value2 === "number") {
|
|
440
|
-
setValue(value2.toLocaleString());
|
|
441
|
-
return;
|
|
442
|
-
}
|
|
443
|
-
setValue(`${value2}`);
|
|
444
|
-
},
|
|
445
|
-
hasError: () => !!error(),
|
|
446
|
-
validate: async () => {
|
|
447
|
-
const rules = [...props.rules ?? []];
|
|
448
|
-
if (props.required) rules.unshift(SSForm.rules.required);
|
|
449
|
-
const value2 = await field.getValue();
|
|
450
|
-
for (const rule of rules) {
|
|
451
|
-
const newError = await rule(value2);
|
|
452
|
-
if (newError) {
|
|
453
|
-
setError(newError);
|
|
454
|
-
return;
|
|
455
|
-
}
|
|
456
|
-
}
|
|
457
|
-
setError(null);
|
|
458
|
-
}
|
|
459
|
-
};
|
|
460
|
-
if (props.name && context) {
|
|
461
|
-
const name = props.name;
|
|
462
|
-
onMount2(() => context.setField(name, field));
|
|
463
|
-
onCleanup3(() => context.delField(name));
|
|
464
|
-
}
|
|
465
|
-
let suggInitData = null;
|
|
466
|
-
const [suggItems, setSuggItems] = createSignal5([]);
|
|
467
|
-
const [suggIndex, setSuggIndex] = createSignal5(-1);
|
|
468
|
-
let suggListRef;
|
|
469
|
-
if (props.suggestions) {
|
|
470
|
-
const sugg = props.suggestions;
|
|
471
|
-
const [initialized, setInitialized] = createSignal5(!sugg.prepare);
|
|
472
|
-
if (sugg.prepare) onMount2(async () => {
|
|
473
|
-
if (!sugg.prepare) return;
|
|
474
|
-
suggInitData = await sugg.prepare();
|
|
475
|
-
setInitialized(true);
|
|
476
|
-
});
|
|
477
|
-
let timeout;
|
|
478
|
-
createEffect3(() => {
|
|
479
|
-
clearTimeout(timeout ?? void 0);
|
|
480
|
-
if (!initialized()) return;
|
|
481
|
-
setSuggItems([]);
|
|
482
|
-
const query = value().trim().toLowerCase();
|
|
483
|
-
if (!query) return;
|
|
484
|
-
timeout = setTimeout(async () => {
|
|
485
|
-
const items = await sugg.getItems({ query, init: suggInitData });
|
|
486
|
-
setSuggItems(items);
|
|
487
|
-
}, 250);
|
|
488
|
-
});
|
|
489
|
-
onCleanup3(() => clearTimeout(timeout ?? void 0));
|
|
490
|
-
}
|
|
491
|
-
createEffect3(() => {
|
|
492
|
-
const items = suggItems();
|
|
493
|
-
if (!items.length) {
|
|
494
|
-
setSuggIndex(-1);
|
|
495
|
-
return;
|
|
496
|
-
}
|
|
497
|
-
if (suggIndex() >= items.length) setSuggIndex(items.length - 1);
|
|
498
|
-
});
|
|
499
|
-
const scrollToIndex = (idx) => {
|
|
500
|
-
const list = suggListRef;
|
|
501
|
-
if (!list || idx < 0) return;
|
|
502
|
-
const item = list.querySelectorAll("li")[idx];
|
|
503
|
-
if (!item) return;
|
|
504
|
-
const itemTop = item.offsetTop;
|
|
505
|
-
const itemBottom = itemTop + item.offsetHeight;
|
|
506
|
-
const viewTop = list.scrollTop;
|
|
507
|
-
const viewBottom = viewTop + list.clientHeight;
|
|
508
|
-
if (itemTop < viewTop) list.scrollTop = itemTop;
|
|
509
|
-
else if (itemBottom > viewBottom) list.scrollTop = itemBottom - list.clientHeight;
|
|
510
|
-
};
|
|
511
|
-
const moveIndex = (delta) => {
|
|
512
|
-
const items = suggItems();
|
|
513
|
-
if (!items.length) return;
|
|
514
|
-
const next = Math.max(-1, Math.min(suggIndex() + delta, items.length - 1));
|
|
515
|
-
setSuggIndex(next);
|
|
516
|
-
scrollToIndex(next);
|
|
517
|
-
};
|
|
518
|
-
const selectSuggestion = (item) => {
|
|
519
|
-
setValue(props.suggestions.stringify({ item, init: suggInitData }));
|
|
520
|
-
setSuggIndex(-1);
|
|
521
|
-
};
|
|
522
|
-
const handleKeyDown = (e) => {
|
|
523
|
-
if (!suggItems().length) return;
|
|
524
|
-
if (e.key === "ArrowDown") {
|
|
525
|
-
e.preventDefault();
|
|
526
|
-
moveIndex(1);
|
|
527
|
-
} else if (e.key === "ArrowUp") {
|
|
528
|
-
e.preventDefault();
|
|
529
|
-
moveIndex(-1);
|
|
530
|
-
} else if (e.key === "Enter" && suggIndex() >= 0) {
|
|
531
|
-
e.preventDefault();
|
|
532
|
-
const item = suggItems()[suggIndex()];
|
|
533
|
-
if (item) selectSuggestion(item);
|
|
534
|
-
} else if (e.key === "Escape") {
|
|
535
|
-
setSuggIndex(-1);
|
|
536
|
-
}
|
|
537
|
-
};
|
|
538
|
-
const handleInputChange = (next) => {
|
|
539
|
-
setValue(next);
|
|
540
|
-
setSuggIndex(-1);
|
|
541
|
-
};
|
|
542
|
-
const _id = createUniqueId();
|
|
543
|
-
return <div class="ss_form_input">
|
|
544
|
-
<label for={props.id || _id}>{props.label}</label>
|
|
545
|
-
|
|
546
|
-
<div class="ss__wrapper">
|
|
547
|
-
{props.textArea ? <textarea
|
|
548
|
-
id={props.id || _id}
|
|
549
|
-
value={value()}
|
|
550
|
-
oninput={(e) => handleInputChange(e.target.value)}
|
|
551
|
-
onkeydown={handleKeyDown}
|
|
552
|
-
disabled={props.disabled || context?.loading()}
|
|
553
|
-
/> : <input
|
|
554
|
-
id={props.id || _id}
|
|
555
|
-
value={value()}
|
|
556
|
-
oninput={(e) => handleInputChange(e.target.value)}
|
|
557
|
-
onkeydown={handleKeyDown}
|
|
558
|
-
disabled={props.disabled || context?.loading()}
|
|
559
|
-
type={props.type}
|
|
560
|
-
/>}
|
|
561
|
-
|
|
562
|
-
{!!suggItems().length && <ul class="ss__suggestions" ref={(el) => suggListRef = el}>
|
|
563
|
-
<For3 each={suggItems()}>
|
|
564
|
-
{(item, idx) => {
|
|
565
|
-
const handleSelect = () => {
|
|
566
|
-
const active = document.activeElement;
|
|
567
|
-
if (active instanceof HTMLElement) active.blur();
|
|
568
|
-
selectSuggestion(item);
|
|
569
|
-
};
|
|
570
|
-
return <li
|
|
571
|
-
onpointerdown={(e) => e.preventDefault()}
|
|
572
|
-
onclick={handleSelect}
|
|
573
|
-
classList={{ "ss__hovered": idx() === suggIndex() }}
|
|
574
|
-
>
|
|
575
|
-
{props.suggestions.stringify({ item, init: suggInitData })}
|
|
576
|
-
</li>;
|
|
577
|
-
}}
|
|
578
|
-
</For3>
|
|
579
|
-
</ul>}
|
|
580
|
-
</div>
|
|
581
|
-
|
|
582
|
-
{!!error() && <span role="alert">{error()}</span>}
|
|
583
|
-
</div>;
|
|
584
|
-
};
|
|
585
|
-
SSForm.Date = function(props) {
|
|
586
|
-
const context = useContext(SSFormContext);
|
|
587
|
-
const [value, setValue] = createSignal5("");
|
|
588
|
-
const [error, setError] = createSignal5(null);
|
|
589
|
-
const [open, setOpen] = createSignal5(false);
|
|
590
|
-
const [viewDate, setViewDate] = createSignal5(/* @__PURE__ */ new Date());
|
|
591
|
-
let containerRef;
|
|
592
|
-
const pad = (num) => `${num}`.padStart(2, "0");
|
|
593
|
-
const formatDate = (date) => {
|
|
594
|
-
const day = pad(date.getDate());
|
|
595
|
-
const month = pad(date.getMonth() + 1);
|
|
596
|
-
const year = date.getFullYear();
|
|
597
|
-
return `${day}.${month}.${year}`;
|
|
598
|
-
};
|
|
599
|
-
const parseDate = (input) => {
|
|
600
|
-
const trimmed = input.trim();
|
|
601
|
-
if (!trimmed) return null;
|
|
602
|
-
const match = /^(\d{1,2})\.(\d{1,2})\.(\d{4})$/.exec(trimmed);
|
|
603
|
-
if (!match) return null;
|
|
604
|
-
const day = Number(match[1]);
|
|
605
|
-
const month = Number(match[2]);
|
|
606
|
-
const year = Number(match[3]);
|
|
607
|
-
if (!day || !month) return null;
|
|
608
|
-
const date = new Date(year, month - 1, day);
|
|
609
|
-
if (date.getFullYear() !== year || date.getMonth() !== month - 1 || date.getDate() !== day) {
|
|
610
|
-
return null;
|
|
611
|
-
}
|
|
612
|
-
return date;
|
|
613
|
-
};
|
|
614
|
-
createEffect3(() => {
|
|
615
|
-
if (!open()) return;
|
|
616
|
-
const parsed = parseDate(value());
|
|
617
|
-
setViewDate(parsed ?? /* @__PURE__ */ new Date());
|
|
618
|
-
});
|
|
619
|
-
const field = {
|
|
620
|
-
getValue: () => {
|
|
621
|
-
const trimmed = value().trim();
|
|
622
|
-
if (!trimmed) return null;
|
|
623
|
-
return trimmed;
|
|
624
|
-
},
|
|
625
|
-
setValue: (value2) => {
|
|
626
|
-
if (value2 instanceof Date) {
|
|
627
|
-
setValue(formatDate(value2));
|
|
628
|
-
return;
|
|
629
|
-
}
|
|
630
|
-
setValue(`${value2 ?? ""}`);
|
|
631
|
-
},
|
|
632
|
-
hasError: () => !!error(),
|
|
633
|
-
validate: async () => {
|
|
634
|
-
const rules = [...props.rules ?? []];
|
|
635
|
-
if (props.required) rules.unshift(SSForm.rules.required);
|
|
636
|
-
const value2 = await field.getValue();
|
|
637
|
-
for (const rule of rules) {
|
|
638
|
-
const newError = await rule(value2);
|
|
639
|
-
if (newError) {
|
|
640
|
-
setError(newError);
|
|
641
|
-
return;
|
|
642
|
-
}
|
|
643
|
-
}
|
|
644
|
-
if (value2 && !parseDate(value2)) {
|
|
645
|
-
setError("Ung\xFCltiges Datum");
|
|
646
|
-
return;
|
|
647
|
-
}
|
|
648
|
-
setError(null);
|
|
649
|
-
}
|
|
650
|
-
};
|
|
651
|
-
if (props.name && context) {
|
|
652
|
-
const name = props.name;
|
|
653
|
-
onMount2(() => context.setField(name, field));
|
|
654
|
-
onCleanup3(() => context.delField(name));
|
|
655
|
-
}
|
|
656
|
-
const _id = createUniqueId();
|
|
657
|
-
const isSelected = (day) => {
|
|
658
|
-
const parsed = parseDate(value());
|
|
659
|
-
if (!parsed) return false;
|
|
660
|
-
return parsed.getFullYear() === viewDate().getFullYear() && parsed.getMonth() === viewDate().getMonth() && parsed.getDate() === day;
|
|
661
|
-
};
|
|
662
|
-
const isToday = (day) => {
|
|
663
|
-
const today = /* @__PURE__ */ new Date();
|
|
664
|
-
return today.getFullYear() === viewDate().getFullYear() && today.getMonth() === viewDate().getMonth() && today.getDate() === day;
|
|
665
|
-
};
|
|
666
|
-
const buildCalendar = () => {
|
|
667
|
-
const current = viewDate();
|
|
668
|
-
const year = current.getFullYear();
|
|
669
|
-
const month = current.getMonth();
|
|
670
|
-
const firstDay = new Date(year, month, 1);
|
|
671
|
-
const daysInMonth = new Date(year, month + 1, 0).getDate();
|
|
672
|
-
const startOffset = (firstDay.getDay() + 6) % 7;
|
|
673
|
-
const cells = [];
|
|
674
|
-
for (let i = 0; i < startOffset; i += 1) cells.push(null);
|
|
675
|
-
for (let day = 1; day <= daysInMonth; day += 1) cells.push(day);
|
|
676
|
-
while (cells.length % 7 !== 0) cells.push(null);
|
|
677
|
-
return cells;
|
|
678
|
-
};
|
|
679
|
-
const monthLabel = () => {
|
|
680
|
-
return viewDate().toLocaleDateString("de-DE", { month: "long", year: "numeric" });
|
|
681
|
-
};
|
|
682
|
-
const handleSelect = (day) => {
|
|
683
|
-
const next = new Date(viewDate().getFullYear(), viewDate().getMonth(), day);
|
|
684
|
-
setValue(formatDate(next));
|
|
685
|
-
setOpen(false);
|
|
686
|
-
};
|
|
687
|
-
onMount2(() => {
|
|
688
|
-
const handleOutside = (event) => {
|
|
689
|
-
if (!open()) return;
|
|
690
|
-
const target = event.target;
|
|
691
|
-
if (containerRef && target && !containerRef.contains(target)) {
|
|
692
|
-
setOpen(false);
|
|
693
|
-
}
|
|
694
|
-
};
|
|
695
|
-
const handleKeyDown = (event) => {
|
|
696
|
-
if (event.key === "Escape") setOpen(false);
|
|
697
|
-
};
|
|
698
|
-
document.addEventListener("mousedown", handleOutside);
|
|
699
|
-
window.addEventListener("keydown", handleKeyDown);
|
|
700
|
-
onCleanup3(() => {
|
|
701
|
-
document.removeEventListener("mousedown", handleOutside);
|
|
702
|
-
window.removeEventListener("keydown", handleKeyDown);
|
|
703
|
-
});
|
|
704
|
-
});
|
|
705
|
-
return <div class="ss_form_date" ref={(el) => containerRef = el}>
|
|
706
|
-
<label for={props.id || _id}>{props.label}</label>
|
|
707
|
-
|
|
708
|
-
<div class="ss__wrapper">
|
|
709
|
-
<input
|
|
710
|
-
id={props.id || _id}
|
|
711
|
-
value={value()}
|
|
712
|
-
oninput={(e) => setValue(e.target.value)}
|
|
713
|
-
onfocus={() => setOpen(true)}
|
|
714
|
-
disabled={props.disabled || context?.loading()}
|
|
715
|
-
readonly={!props.editable}
|
|
716
|
-
type="text"
|
|
717
|
-
inputmode="numeric"
|
|
718
|
-
autocomplete="off"
|
|
719
|
-
placeholder="TT.MM.JJJJ"
|
|
720
|
-
/>
|
|
721
|
-
<button
|
|
722
|
-
type="button"
|
|
723
|
-
class="ss_form_date__icon"
|
|
724
|
-
aria-label="Kalender öffnen"
|
|
725
|
-
aria-haspopup="dialog"
|
|
726
|
-
aria-expanded={open()}
|
|
727
|
-
onclick={() => setOpen((value2) => !value2)}
|
|
728
|
-
disabled={props.disabled || context?.loading()}
|
|
729
|
-
>
|
|
730
|
-
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="currentColor" class="icon icon-tabler icons-tabler-filled icon-tabler-calendar-week"><path stroke="none" d="M0 0h24v24H0z" fill="none" /><path d="M16 2c.183 0 .355 .05 .502 .135l.033 .02c.28 .177 .465 .49 .465 .845v1h1a3 3 0 0 1 2.995 2.824l.005 .176v12a3 3 0 0 1 -2.824 2.995l-.176 .005h-12a3 3 0 0 1 -2.995 -2.824l-.005 -.176v-12a3 3 0 0 1 2.824 -2.995l.176 -.005h1v-1a1 1 0 0 1 .514 -.874l.093 -.046l.066 -.025l.1 -.029l.107 -.019l.12 -.007q .083 0 .161 .013l.122 .029l.04 .012l.06 .023c.328 .135 .568 .44 .61 .806l.007 .117v1h6v-1a1 1 0 0 1 1 -1m3 7h-14v9.625c0 .705 .386 1.286 .883 1.366l.117 .009h12c.513 0 .936 -.53 .993 -1.215l.007 -.16z" /><path d="M9.015 13a1 1 0 0 1 -1 1a1.001 1.001 0 1 1 -.005 -2c.557 0 1.005 .448 1.005 1" /><path d="M13.015 13a1 1 0 0 1 -1 1a1.001 1.001 0 1 1 -.005 -2c.557 0 1.005 .448 1.005 1" /><path d="M17.02 13a1 1 0 0 1 -1 1a1.001 1.001 0 1 1 -.005 -2c.557 0 1.005 .448 1.005 1" /><path d="M12.02 15a1 1 0 0 1 0 2a1.001 1.001 0 1 1 -.005 -2z" /><path d="M9.015 16a1 1 0 0 1 -1 1a1.001 1.001 0 1 1 -.005 -2c.557 0 1.005 .448 1.005 1" /></svg>
|
|
731
|
-
</button>
|
|
732
|
-
</div>
|
|
733
|
-
|
|
734
|
-
{open() && <div class="ss_form_date__picker" role="dialog" aria-label="Datum auswählen">
|
|
735
|
-
<div class="ss_form_date__header">
|
|
736
|
-
<div class="ss_form_date__nav_group">
|
|
737
|
-
<button type="button" class="ss_form_date__nav" onclick={() => {
|
|
738
|
-
const current = viewDate();
|
|
739
|
-
setViewDate(new Date(current.getFullYear() - 1, current.getMonth(), 1));
|
|
740
|
-
}}>
|
|
741
|
-
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="icon icon-tabler icons-tabler-outline icon-tabler-chevron-left-pipe"><path stroke="none" d="M0 0h24v24H0z" fill="none" /><path d="M7 6v12" /><path d="M18 6l-6 6l6 6" /></svg>
|
|
742
|
-
</button>
|
|
743
|
-
<button type="button" class="ss_form_date__nav" onclick={() => {
|
|
744
|
-
const current = viewDate();
|
|
745
|
-
setViewDate(new Date(current.getFullYear(), current.getMonth() - 1, 1));
|
|
746
|
-
}}>
|
|
747
|
-
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="icon icon-tabler icons-tabler-outline icon-tabler-chevron-left"><path stroke="none" d="M0 0h24v24H0z" fill="none" /><path d="M15 6l-6 6l6 6" /></svg>
|
|
748
|
-
</button>
|
|
749
|
-
</div>
|
|
750
|
-
<div class="ss_form_date__title">{monthLabel()}</div>
|
|
751
|
-
<div class="ss_form_date__nav_group">
|
|
752
|
-
<button type="button" class="ss_form_date__nav" onclick={() => {
|
|
753
|
-
const current = viewDate();
|
|
754
|
-
setViewDate(new Date(current.getFullYear(), current.getMonth() + 1, 1));
|
|
755
|
-
}}>
|
|
756
|
-
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="icon icon-tabler icons-tabler-outline icon-tabler-chevron-right"><path stroke="none" d="M0 0h24v24H0z" fill="none" /><path d="M9 6l6 6l-6 6" /></svg>
|
|
757
|
-
</button>
|
|
758
|
-
<button type="button" class="ss_form_date__nav" onclick={() => {
|
|
759
|
-
const current = viewDate();
|
|
760
|
-
setViewDate(new Date(current.getFullYear() + 1, current.getMonth(), 1));
|
|
761
|
-
}}>
|
|
762
|
-
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="icon icon-tabler icons-tabler-outline icon-tabler-chevron-right-pipe"><path stroke="none" d="M0 0h24v24H0z" fill="none" /><path d="M6 6l6 6l-6 6" /><path d="M17 5v13" /></svg>
|
|
763
|
-
</button>
|
|
764
|
-
</div>
|
|
765
|
-
</div>
|
|
766
|
-
<div class="ss_form_date__weekdays">
|
|
767
|
-
{["Mo", "Di", "Mi", "Do", "Fr", "Sa", "So"].map((day) => <span>{day}</span>)}
|
|
768
|
-
</div>
|
|
769
|
-
<div class="ss_form_date__grid">
|
|
770
|
-
{buildCalendar().map((day) => day ? <button
|
|
771
|
-
type="button"
|
|
772
|
-
class="ss_form_date__day"
|
|
773
|
-
classList={{ "is-selected": isSelected(day), "is-today": isToday(day) }}
|
|
774
|
-
onclick={() => handleSelect(day)}
|
|
775
|
-
>
|
|
776
|
-
{day}
|
|
777
|
-
</button> : <span class="ss_form_date__day is-empty" />)}
|
|
778
|
-
</div>
|
|
779
|
-
</div>}
|
|
780
|
-
|
|
781
|
-
{!!error() && <span role="alert">{error()}</span>}
|
|
782
|
-
</div>;
|
|
783
|
-
};
|
|
784
|
-
SSForm.Checkbox = function(props) {
|
|
785
|
-
const context = useContext(SSFormContext);
|
|
786
|
-
const [value, setValue] = createSignal5(false);
|
|
787
|
-
const [error, setError] = createSignal5(null);
|
|
788
|
-
const field = {
|
|
789
|
-
getValue: () => value(),
|
|
790
|
-
setValue: (value2) => setValue(!!value2),
|
|
791
|
-
hasError: () => !!error(),
|
|
792
|
-
validate: async () => {
|
|
793
|
-
const rules = [...props.rules ?? []];
|
|
794
|
-
if (props.required) rules.unshift(SSForm.rules.required);
|
|
795
|
-
const value2 = await field.getValue();
|
|
796
|
-
for (const rule of rules) {
|
|
797
|
-
const newError = await rule(value2);
|
|
798
|
-
if (newError) {
|
|
799
|
-
setError(newError);
|
|
800
|
-
return;
|
|
801
|
-
}
|
|
802
|
-
}
|
|
803
|
-
setError(null);
|
|
804
|
-
}
|
|
805
|
-
};
|
|
806
|
-
if (props.name && context) {
|
|
807
|
-
const name = props.name;
|
|
808
|
-
onMount2(() => context.setField(name, field));
|
|
809
|
-
onCleanup3(() => context.delField(name));
|
|
810
|
-
}
|
|
811
|
-
const _id = createUniqueId();
|
|
812
|
-
return <div class="ss_form_checkbox">
|
|
813
|
-
<div class="ss__wrapper">
|
|
814
|
-
<input
|
|
815
|
-
id={props.id || _id}
|
|
816
|
-
type="checkbox"
|
|
817
|
-
checked={value()}
|
|
818
|
-
onchange={(e) => setValue(e.target.checked)}
|
|
819
|
-
disabled={props.disabled || context?.loading()}
|
|
820
|
-
/>
|
|
821
|
-
<label for={props.id || _id}>{props.label}</label>
|
|
822
|
-
</div>
|
|
823
|
-
|
|
824
|
-
{!!error() && <span role="alert">{error()}</span>}
|
|
825
|
-
</div>;
|
|
826
|
-
};
|
|
827
|
-
SSForm.useContext = function() {
|
|
828
|
-
const context = useContext(SSFormContext);
|
|
829
|
-
if (!context) return null;
|
|
830
|
-
const { setField, delField, ...publicContext } = context;
|
|
831
|
-
return publicContext;
|
|
832
|
-
};
|
|
833
|
-
SSForm.Select = function(props) {
|
|
834
|
-
const context = useContext(SSFormContext);
|
|
835
|
-
const [options, setOptions] = createSignal5([]);
|
|
836
|
-
const [value, setValue] = createSignal5(null);
|
|
837
|
-
const [error, setError] = createSignal5(null);
|
|
838
|
-
const handleSetOptions = (result) => {
|
|
839
|
-
const built = result.map((opt) => ({ id: opt.id, label: props.buildOption(opt) }));
|
|
840
|
-
setOptions(built);
|
|
841
|
-
if (!result.length) {
|
|
842
|
-
setValue(null);
|
|
843
|
-
return;
|
|
844
|
-
}
|
|
845
|
-
if (value() && built.some((e) => e.id === value())) return;
|
|
846
|
-
setValue(built[0].id);
|
|
847
|
-
};
|
|
848
|
-
createEffect3(() => {
|
|
849
|
-
const newOptions = props.getOptions();
|
|
850
|
-
if ("then" in newOptions && typeof newOptions.then === "function" || newOptions instanceof Promise) {
|
|
851
|
-
newOptions.then(handleSetOptions);
|
|
852
|
-
} else {
|
|
853
|
-
handleSetOptions(newOptions);
|
|
854
|
-
}
|
|
855
|
-
});
|
|
856
|
-
const field = {
|
|
857
|
-
getValue: () => {
|
|
858
|
-
const _value = value();
|
|
859
|
-
const availableOptions = options();
|
|
860
|
-
if (!availableOptions.some((e) => e.id === _value)) {
|
|
861
|
-
return null;
|
|
862
|
-
}
|
|
863
|
-
return _value;
|
|
864
|
-
},
|
|
865
|
-
setValue: (newValue) => {
|
|
866
|
-
const availableOptions = options();
|
|
867
|
-
if (!availableOptions.length) {
|
|
868
|
-
setValue(null);
|
|
869
|
-
return;
|
|
870
|
-
}
|
|
871
|
-
const defaultOption = availableOptions[0].id;
|
|
872
|
-
if (typeof newValue !== "string") {
|
|
873
|
-
setValue(defaultOption);
|
|
874
|
-
return;
|
|
875
|
-
}
|
|
876
|
-
const _newValue = newValue.trim();
|
|
877
|
-
if (!availableOptions.some((e) => e.id === _newValue)) {
|
|
878
|
-
setValue(defaultOption);
|
|
879
|
-
return;
|
|
880
|
-
}
|
|
881
|
-
setValue(_newValue);
|
|
882
|
-
},
|
|
883
|
-
hasError: () => !!error(),
|
|
884
|
-
validate: async () => {
|
|
885
|
-
const availableOptions = options();
|
|
886
|
-
const value2 = await field.getValue();
|
|
887
|
-
if (!value2) {
|
|
888
|
-
setError("Pflichtfeld");
|
|
889
|
-
return;
|
|
890
|
-
}
|
|
891
|
-
if (!availableOptions.some((e) => e.id === value2)) {
|
|
892
|
-
setError("Option nicht verf\xFCgbar");
|
|
893
|
-
return;
|
|
894
|
-
}
|
|
895
|
-
for (const rule of props.rules ?? []) {
|
|
896
|
-
const err = await rule(value2);
|
|
897
|
-
if (err) {
|
|
898
|
-
setError(err);
|
|
899
|
-
return;
|
|
900
|
-
}
|
|
901
|
-
}
|
|
902
|
-
setError(null);
|
|
903
|
-
}
|
|
904
|
-
};
|
|
905
|
-
field.setOptions = handleSetOptions;
|
|
906
|
-
if (props.name && context) {
|
|
907
|
-
const name = props.name;
|
|
908
|
-
onMount2(() => context.setField(name, field));
|
|
909
|
-
onCleanup3(() => context.delField(name));
|
|
910
|
-
}
|
|
911
|
-
const _id = createUniqueId();
|
|
912
|
-
return <div class="ss_form_select">
|
|
913
|
-
<label for={props.id || _id}>{props.label}</label>
|
|
914
|
-
|
|
915
|
-
<div class="ss_form_select__field">
|
|
916
|
-
<select
|
|
917
|
-
id={props.id || _id}
|
|
918
|
-
onchange={(e) => setValue(e.target.value)}
|
|
919
|
-
disabled={props.disabled || context?.loading()}
|
|
920
|
-
>
|
|
921
|
-
<For3 each={options()}>
|
|
922
|
-
{(opt) => <option value={opt.id} selected={value() === opt.id}>{opt.label}</option>}
|
|
923
|
-
</For3>
|
|
924
|
-
</select>
|
|
925
|
-
<span class="ss_form_select__icon" aria-hidden="true">
|
|
926
|
-
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="icon icon-tabler icons-tabler-outline icon-tabler-chevron-down"><path stroke="none" d="M0 0h24v24H0z" fill="none" /><path d="M6 9l6 6l6 -6" /></svg>
|
|
927
|
-
</span>
|
|
928
|
-
</div>
|
|
929
|
-
|
|
930
|
-
{!!error() && <span role="alert">{error()}</span>}
|
|
931
|
-
</div>;
|
|
932
|
-
};
|
|
933
|
-
SSForm.ACSelect = function(props) {
|
|
934
|
-
const context = useContext(SSFormContext);
|
|
935
|
-
const [query, setQuery] = createSignal5("");
|
|
936
|
-
const [value, setValue] = createSignal5([]);
|
|
937
|
-
const [error, setError] = createSignal5(null);
|
|
938
|
-
const field = {
|
|
939
|
-
getValue: () => value(),
|
|
940
|
-
setValue: (newValue) => setValue(newValue),
|
|
941
|
-
hasError: () => !!error(),
|
|
942
|
-
validate: async () => {
|
|
943
|
-
const value2 = await field.getValue();
|
|
944
|
-
const min = props.minSelectedItems;
|
|
945
|
-
const max = props.maxSelectedItems;
|
|
946
|
-
if (min) {
|
|
947
|
-
if (value2.length < min) {
|
|
948
|
-
setError(`Mindestens ${min} ${min > 1 ? "Elemente m\xFCssen" : "Element muss"} ausgew\xE4hlt werden`);
|
|
949
|
-
return;
|
|
950
|
-
}
|
|
951
|
-
}
|
|
952
|
-
if (max) {
|
|
953
|
-
if (value2.length > max) {
|
|
954
|
-
setError(`Maximal ${max} ${max > 1 ? "Elemente d\xFCrfen" : "Element darf"} ausgew\xE4hlt werden`);
|
|
955
|
-
return;
|
|
956
|
-
}
|
|
957
|
-
}
|
|
958
|
-
for (const rule of props.rules ?? []) {
|
|
959
|
-
const err = await rule(value2);
|
|
960
|
-
if (err) {
|
|
961
|
-
setError(err);
|
|
962
|
-
return;
|
|
963
|
-
}
|
|
964
|
-
}
|
|
965
|
-
setError(null);
|
|
966
|
-
}
|
|
967
|
-
};
|
|
968
|
-
if (props.name && context) {
|
|
969
|
-
const name = props.name;
|
|
970
|
-
onMount2(() => context.setField(name, field));
|
|
971
|
-
onCleanup3(() => context.delField(name));
|
|
972
|
-
}
|
|
973
|
-
let initData = null;
|
|
974
|
-
const [initialized, setInitialized] = createSignal5(!props.prepare);
|
|
975
|
-
if (props.prepare) onMount2(async () => {
|
|
976
|
-
if (!props.prepare) return;
|
|
977
|
-
initData = await props.prepare();
|
|
978
|
-
setInitialized(true);
|
|
979
|
-
});
|
|
980
|
-
const [suggItems, setSuggItems] = createSignal5([]);
|
|
981
|
-
const [suggIndex, setSuggIndex] = createSignal5(-1);
|
|
982
|
-
let suggListRef;
|
|
983
|
-
let timeout;
|
|
984
|
-
createEffect3(() => {
|
|
985
|
-
if (!initialized()) return;
|
|
986
|
-
clearTimeout(timeout);
|
|
987
|
-
const _query = query().trim().toLowerCase();
|
|
988
|
-
if (!_query) {
|
|
989
|
-
setSuggItems(untrack(() => value()));
|
|
990
|
-
return;
|
|
991
|
-
}
|
|
992
|
-
setSuggItems([]);
|
|
993
|
-
timeout = setTimeout(async () => {
|
|
994
|
-
const items = await props.getOptions({ query: _query, init: initData });
|
|
995
|
-
setSuggItems(items);
|
|
996
|
-
}, 250);
|
|
997
|
-
});
|
|
998
|
-
createEffect3(() => {
|
|
999
|
-
const _query = query().trim().toLowerCase();
|
|
1000
|
-
if (_query) return;
|
|
1001
|
-
if (suggItems().length !== value().length) setSuggItems(value());
|
|
1002
|
-
});
|
|
1003
|
-
onCleanup3(() => clearTimeout(timeout));
|
|
1004
|
-
createEffect3(() => {
|
|
1005
|
-
const items = suggItems();
|
|
1006
|
-
if (!items.length) {
|
|
1007
|
-
setSuggIndex(-1);
|
|
1008
|
-
return;
|
|
1009
|
-
}
|
|
1010
|
-
if (suggIndex() >= items.length) setSuggIndex(items.length - 1);
|
|
1011
|
-
});
|
|
1012
|
-
const scrollToIndex = (idx) => {
|
|
1013
|
-
const list = suggListRef;
|
|
1014
|
-
if (!list || idx < 0) return;
|
|
1015
|
-
const item = list.querySelectorAll("li")[idx];
|
|
1016
|
-
if (!item) return;
|
|
1017
|
-
const itemTop = item.offsetTop;
|
|
1018
|
-
const itemBottom = itemTop + item.offsetHeight;
|
|
1019
|
-
const viewTop = list.scrollTop;
|
|
1020
|
-
const viewBottom = viewTop + list.clientHeight;
|
|
1021
|
-
if (itemTop < viewTop) list.scrollTop = itemTop;
|
|
1022
|
-
else if (itemBottom > viewBottom) list.scrollTop = itemBottom - list.clientHeight;
|
|
1023
|
-
};
|
|
1024
|
-
const moveIndex = (delta) => {
|
|
1025
|
-
const items = suggItems();
|
|
1026
|
-
if (!items.length) return;
|
|
1027
|
-
const next = Math.max(-1, Math.min(suggIndex() + delta, items.length - 1));
|
|
1028
|
-
setSuggIndex(next);
|
|
1029
|
-
scrollToIndex(next);
|
|
1030
|
-
};
|
|
1031
|
-
const _id = createUniqueId();
|
|
1032
|
-
const selectedIds = createMemo3(() => {
|
|
1033
|
-
return new Set(value().map(({ id }) => id));
|
|
1034
|
-
});
|
|
1035
|
-
const selectItem = (item, resetIndex = true) => {
|
|
1036
|
-
const isSelected = selectedIds().has(item.id);
|
|
1037
|
-
if (props.maxSelectedItems === 1) {
|
|
1038
|
-
setValue(isSelected ? [] : [item]);
|
|
1039
|
-
setQuery("");
|
|
1040
|
-
if (resetIndex) setSuggIndex(-1);
|
|
1041
|
-
return;
|
|
1042
|
-
}
|
|
1043
|
-
if (isSelected) {
|
|
1044
|
-
setValue((value2) => value2.filter(({ id }) => id !== item.id));
|
|
1045
|
-
} else {
|
|
1046
|
-
setValue((value2) => [...value2.filter(({ id }) => id !== item.id), item]);
|
|
1047
|
-
}
|
|
1048
|
-
if (resetIndex) setSuggIndex(-1);
|
|
1049
|
-
};
|
|
1050
|
-
const handleKeyDown = (e) => {
|
|
1051
|
-
if (!suggItems().length) return;
|
|
1052
|
-
if (e.key === "ArrowDown") {
|
|
1053
|
-
e.preventDefault();
|
|
1054
|
-
moveIndex(1);
|
|
1055
|
-
} else if (e.key === "ArrowUp") {
|
|
1056
|
-
e.preventDefault();
|
|
1057
|
-
moveIndex(-1);
|
|
1058
|
-
} else if (e.key === "Enter" && suggIndex() >= 0) {
|
|
1059
|
-
e.preventDefault();
|
|
1060
|
-
const item = suggItems()[suggIndex()];
|
|
1061
|
-
if (item) selectItem(item, false);
|
|
1062
|
-
} else if (e.key === "Escape") {
|
|
1063
|
-
setSuggIndex(-1);
|
|
1064
|
-
}
|
|
1065
|
-
};
|
|
1066
|
-
const isDisabled = () => props.disabled || context?.loading();
|
|
1067
|
-
return <div class="ss_form_ac_select">
|
|
1068
|
-
<label for={props.id || _id}>{props.label}</label>
|
|
1069
|
-
|
|
1070
|
-
{props.maxSelectedItems === 1 && !!props.renderSelection && value().length === 1 ? <div class="ss__selection">
|
|
1071
|
-
<div class="ss__content">
|
|
1072
|
-
{props.renderSelection({ item: value()[0], init: initData })}
|
|
1073
|
-
</div>
|
|
1074
|
-
<button
|
|
1075
|
-
type="button"
|
|
1076
|
-
class="ss__clear"
|
|
1077
|
-
disabled={isDisabled()}
|
|
1078
|
-
aria-label="Auswahl entfernen"
|
|
1079
|
-
onclick={() => {
|
|
1080
|
-
setValue([]);
|
|
1081
|
-
setQuery("");
|
|
1082
|
-
setTimeout(() => document.getElementById(props.id || _id)?.focus(), 0);
|
|
1083
|
-
}}
|
|
1084
|
-
>
|
|
1085
|
-
×
|
|
1086
|
-
</button>
|
|
1087
|
-
</div> : <div class="ss__wrapper">
|
|
1088
|
-
|
|
1089
|
-
<div class="ss__input_row">
|
|
1090
|
-
{!!value().length && <span
|
|
1091
|
-
class="ss__prefix"
|
|
1092
|
-
role="button"
|
|
1093
|
-
tabindex={isDisabled() ? -1 : 0}
|
|
1094
|
-
aria-disabled={isDisabled() ? "true" : void 0}
|
|
1095
|
-
onkeydown={(event) => {
|
|
1096
|
-
if (isDisabled()) return;
|
|
1097
|
-
if (event.key === "Enter" || event.key === " ") {
|
|
1098
|
-
event.preventDefault();
|
|
1099
|
-
setValue([]);
|
|
1100
|
-
}
|
|
1101
|
-
}}
|
|
1102
|
-
onclick={() => {
|
|
1103
|
-
if (isDisabled()) return;
|
|
1104
|
-
setValue([]);
|
|
1105
|
-
}}
|
|
1106
|
-
>
|
|
1107
|
-
{value().length}
|
|
1108
|
-
</span>}
|
|
1109
|
-
|
|
1110
|
-
<input
|
|
1111
|
-
id={props.id || _id}
|
|
1112
|
-
value={query()}
|
|
1113
|
-
oninput={(e) => {
|
|
1114
|
-
if (isDisabled()) return;
|
|
1115
|
-
setQuery(e.target.value);
|
|
1116
|
-
setSuggIndex(-1);
|
|
1117
|
-
}}
|
|
1118
|
-
onkeydown={handleKeyDown}
|
|
1119
|
-
disabled={isDisabled()}
|
|
1120
|
-
/>
|
|
1121
|
-
</div>
|
|
1122
|
-
|
|
1123
|
-
{!!suggItems().length && <ul class="ss__suggestions" ref={(el) => suggListRef = el}>
|
|
1124
|
-
<For3 each={suggItems()}>
|
|
1125
|
-
{(item, idx) => {
|
|
1126
|
-
const handleSelect = () => {
|
|
1127
|
-
if (isDisabled()) return;
|
|
1128
|
-
selectItem(item);
|
|
1129
|
-
document.getElementById(props.id || _id)?.focus();
|
|
1130
|
-
};
|
|
1131
|
-
return <li
|
|
1132
|
-
onpointerdown={(e) => e.preventDefault()}
|
|
1133
|
-
onclick={handleSelect}
|
|
1134
|
-
classList={{
|
|
1135
|
-
"ss__selected": selectedIds().has(item.id),
|
|
1136
|
-
"ss__hovered": idx() === suggIndex()
|
|
1137
|
-
}}
|
|
1138
|
-
>
|
|
1139
|
-
{props.renderItem({ item, init: initData })}
|
|
1140
|
-
</li>;
|
|
1141
|
-
}}
|
|
1142
|
-
</For3>
|
|
1143
|
-
</ul>}
|
|
1144
|
-
</div>}
|
|
1145
|
-
|
|
1146
|
-
{!!error() && <span role="alert">{error()}</span>}
|
|
1147
|
-
</div>;
|
|
1148
|
-
};
|
|
1149
|
-
SSForm.SubmitButton = function(props) {
|
|
1150
|
-
const context = useContext(SSFormContext);
|
|
1151
|
-
return <div>
|
|
1152
|
-
<SSButton
|
|
1153
|
-
type="submit"
|
|
1154
|
-
class="ss_form_submit"
|
|
1155
|
-
disabled={props.disabled || context?.loading()}
|
|
1156
|
-
>
|
|
1157
|
-
{props.children}
|
|
1158
|
-
</SSButton>
|
|
1159
|
-
</div>;
|
|
1160
|
-
};
|
|
1161
|
-
SSForm.rules = {
|
|
1162
|
-
required: (value) => !value ? "Pflichtfeld" : null,
|
|
1163
|
-
minLength: (length) => (value) => {
|
|
1164
|
-
if (value === null) value = "";
|
|
1165
|
-
if (typeof value !== "string") {
|
|
1166
|
-
throw new Error(`invalid rule minLength(..) for non-string value ${value}`);
|
|
1167
|
-
}
|
|
1168
|
-
if (value.length < length) return `Mindestens ${length} Zeichen`;
|
|
1169
|
-
return null;
|
|
1170
|
-
},
|
|
1171
|
-
maxLength: (length) => (value) => {
|
|
1172
|
-
if (value === null) value = "";
|
|
1173
|
-
if (typeof value !== "string") {
|
|
1174
|
-
throw new Error(`invalid rule maxLength(..) for non-string value ${value}`);
|
|
1175
|
-
}
|
|
1176
|
-
if (value.length < length) return `Maximal ${length} Zeichen`;
|
|
1177
|
-
return null;
|
|
1178
|
-
},
|
|
1179
|
-
pattern: (pattern) => (value) => {
|
|
1180
|
-
if (value === null) value = "";
|
|
1181
|
-
if (typeof value !== "string") {
|
|
1182
|
-
throw new Error(`invalid rule pattern(..) for non-string value ${value}`);
|
|
1183
|
-
}
|
|
1184
|
-
if (!pattern.test(value)) return "Eingabe widerspricht erwartetem Muster";
|
|
1185
|
-
return null;
|
|
1186
|
-
}
|
|
1187
|
-
};
|
|
1188
|
-
|
|
1189
|
-
// src/components/SSHeader.tsx
|
|
1190
|
-
function SSHeader(props) {
|
|
1191
|
-
return <div class={`ss_header ${props.class ?? ""}`} style={props.style}>
|
|
1192
|
-
<div class="ss_header__text">
|
|
1193
|
-
<h3 class="ss_header__title">{props.title}</h3>
|
|
1194
|
-
{props.subtitle && <h5 class="ss_header__subtitle">{props.subtitle}</h5>}
|
|
1195
|
-
</div>
|
|
1196
|
-
<div class="ss_header__actions">{props.actions}</div>
|
|
1197
|
-
</div>;
|
|
1198
|
-
}
|
|
1199
|
-
|
|
1200
|
-
// src/components/SSModal.tsx
|
|
1201
|
-
import { Show as Show3, createEffect as createEffect4, createSignal as createSignal6, createUniqueId as createUniqueId2, onCleanup as onCleanup4, onMount as onMount3 } from "solid-js";
|
|
1202
|
-
import { Portal } from "solid-js/web";
|
|
1203
|
-
var CLOSE_ANIMATION_MS = 180;
|
|
1204
|
-
function SSModal(props) {
|
|
1205
|
-
const [isMounted, setIsMounted] = createSignal6(props.open);
|
|
1206
|
-
const [state, setState] = createSignal6("closed");
|
|
1207
|
-
const titleId = createUniqueId2();
|
|
1208
|
-
let closeTimeout;
|
|
1209
|
-
let rafId;
|
|
1210
|
-
let panelRef;
|
|
1211
|
-
createEffect4(() => {
|
|
1212
|
-
if (closeTimeout) clearTimeout(closeTimeout);
|
|
1213
|
-
if (rafId) cancelAnimationFrame(rafId);
|
|
1214
|
-
if (props.open) {
|
|
1215
|
-
if (!isMounted()) setIsMounted(true);
|
|
1216
|
-
setState("closed");
|
|
1217
|
-
rafId = requestAnimationFrame(() => setState("open"));
|
|
1218
|
-
} else {
|
|
1219
|
-
setState("closed");
|
|
1220
|
-
if (isMounted()) {
|
|
1221
|
-
closeTimeout = window.setTimeout(() => setIsMounted(false), CLOSE_ANIMATION_MS);
|
|
1222
|
-
}
|
|
1223
|
-
}
|
|
1224
|
-
});
|
|
1225
|
-
onMount3(() => {
|
|
1226
|
-
if (!props.open) return;
|
|
1227
|
-
if (props.lockScroll !== false) {
|
|
1228
|
-
const prev = document.body.style.overflowY;
|
|
1229
|
-
document.body.style.overflowY = "hidden";
|
|
1230
|
-
onCleanup4(() => {
|
|
1231
|
-
document.body.style.overflowY = prev;
|
|
1232
|
-
});
|
|
1233
|
-
}
|
|
1234
|
-
rafId = requestAnimationFrame(() => panelRef?.focus());
|
|
1235
|
-
const handleKeyDown = (event) => {
|
|
1236
|
-
if (event.key === "Escape" && props.dismissible !== false) {
|
|
1237
|
-
props.onClose?.();
|
|
1238
|
-
}
|
|
1239
|
-
};
|
|
1240
|
-
window.addEventListener("keydown", handleKeyDown);
|
|
1241
|
-
onCleanup4(() => window.removeEventListener("keydown", handleKeyDown));
|
|
1242
|
-
});
|
|
1243
|
-
onCleanup4(() => {
|
|
1244
|
-
if (closeTimeout) clearTimeout(closeTimeout);
|
|
1245
|
-
if (rafId) cancelAnimationFrame(rafId);
|
|
1246
|
-
});
|
|
1247
|
-
const handleBackdropClick = () => {
|
|
1248
|
-
if (props.dismissible === false) return;
|
|
1249
|
-
props.onClose?.();
|
|
1250
|
-
};
|
|
1251
|
-
return <Show3 when={isMounted()}>
|
|
1252
|
-
<Portal>
|
|
1253
|
-
<div class="ss_modal" data-state={state()} aria-hidden={state() === "closed"}>
|
|
1254
|
-
<div class="ss_modal__backdrop" onclick={handleBackdropClick} />
|
|
1255
|
-
<div
|
|
1256
|
-
class="ss_modal__panel"
|
|
1257
|
-
classList={{
|
|
1258
|
-
"ss_modal__panel--sm": props.size === "sm",
|
|
1259
|
-
"ss_modal__panel--lg": props.size === "lg",
|
|
1260
|
-
"ss_modal__panel--fullscreen": props.fullscreen,
|
|
1261
|
-
"ss_modal__panel--no-fullscreen": props.disableResponsiveFullscreen
|
|
1262
|
-
}}
|
|
1263
|
-
ref={(el) => panelRef = el}
|
|
1264
|
-
role="dialog"
|
|
1265
|
-
aria-modal="true"
|
|
1266
|
-
aria-labelledby={props.title ? titleId : void 0}
|
|
1267
|
-
tabindex="-1"
|
|
1268
|
-
>
|
|
1269
|
-
{(props.title || props.onClose) && <div class="ss_modal__header">
|
|
1270
|
-
{props.title && <h2 id={titleId} class="ss_modal__title">
|
|
1271
|
-
{props.title}
|
|
1272
|
-
</h2>}
|
|
1273
|
-
<Show3 when={props.onClose}>
|
|
1274
|
-
<SSButton
|
|
1275
|
-
type="button"
|
|
1276
|
-
class="ss_modal__close"
|
|
1277
|
-
isIconOnly
|
|
1278
|
-
ariaLabel="Dialog schließen"
|
|
1279
|
-
onclick={() => {
|
|
1280
|
-
if (props.dismissible === false) return;
|
|
1281
|
-
props.onClose?.();
|
|
1282
|
-
}}
|
|
1283
|
-
>
|
|
1284
|
-
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="icon icon-tabler icons-tabler-outline icon-tabler-x"><path stroke="none" d="M0 0h24v24H0z" fill="none" /><path d="M18 6l-12 12" /><path d="M6 6l12 12" /></svg>
|
|
1285
|
-
</SSButton>
|
|
1286
|
-
</Show3>
|
|
1287
|
-
</div>}
|
|
1288
|
-
|
|
1289
|
-
<div class="ss_modal__body">
|
|
1290
|
-
<div class="ss_modal__body_inner">{props.children}</div>
|
|
1291
|
-
</div>
|
|
1292
|
-
|
|
1293
|
-
<Show3 when={props.footer}>
|
|
1294
|
-
<div class="ss_modal__footer">{props.footer}</div>
|
|
1295
|
-
</Show3>
|
|
1296
|
-
</div>
|
|
1297
|
-
</div>
|
|
1298
|
-
</Portal>
|
|
1299
|
-
</Show3>;
|
|
1300
|
-
}
|
|
1301
|
-
|
|
1302
|
-
// src/components/SSModals.tsx
|
|
1303
|
-
import { createContext as createContext2, createSignal as createSignal7, For as For4, onCleanup as onCleanup5, useContext as useContext2 } from "solid-js";
|
|
1304
|
-
import { useLocation, useNavigate } from "@solidjs/router";
|
|
1305
|
-
var SSModalsContext = createContext2();
|
|
1306
|
-
var modalCounter = 0;
|
|
1307
|
-
var nextModalId = () => `ss-modal-${modalCounter++}`;
|
|
1308
|
-
function useSSModals() {
|
|
1309
|
-
const context = useContext2(SSModalsContext);
|
|
1310
|
-
if (!context) {
|
|
1311
|
-
throw new Error("useSSModals must be used within SSModalsProvider");
|
|
1312
|
-
}
|
|
1313
|
-
return context;
|
|
1314
|
-
}
|
|
1315
|
-
function DefaultModal(props) {
|
|
1316
|
-
const navigate = useNavigate();
|
|
1317
|
-
const location = useLocation();
|
|
1318
|
-
const [loading, process] = createLoading();
|
|
1319
|
-
const modalProps = () => props.config.modalProps?.({ hide: props.hide }) ?? {};
|
|
1320
|
-
const {
|
|
1321
|
-
primaryButtonText,
|
|
1322
|
-
secondaryButtonText,
|
|
1323
|
-
hideSecondaryButton,
|
|
1324
|
-
danger,
|
|
1325
|
-
...rest
|
|
1326
|
-
} = modalProps();
|
|
1327
|
-
return <SSModal
|
|
1328
|
-
open={props.visible()}
|
|
1329
|
-
onClose={props.hide}
|
|
1330
|
-
{...rest}
|
|
1331
|
-
footer={<>
|
|
1332
|
-
{!hideSecondaryButton && <SSButton class="secondary" onclick={props.hide}>
|
|
1333
|
-
{secondaryButtonText ?? "Abbrechen"}
|
|
1334
|
-
</SSButton>}
|
|
1335
|
-
<SSButton
|
|
1336
|
-
class={danger ? "danger" : void 0}
|
|
1337
|
-
onclick={() => process(
|
|
1338
|
-
() => props.config.onPrimaryAction?.({
|
|
1339
|
-
hide: props.hide,
|
|
1340
|
-
navigate,
|
|
1341
|
-
pathname: location.pathname
|
|
1342
|
-
}) ?? props.hide()
|
|
1343
|
-
)}
|
|
1344
|
-
disabled={loading()}
|
|
1345
|
-
>
|
|
1346
|
-
{primaryButtonText ?? "Weiter"}
|
|
1347
|
-
</SSButton>
|
|
1348
|
-
</>}
|
|
1349
|
-
>
|
|
1350
|
-
{props.config.content({ hide: props.hide })}
|
|
1351
|
-
</SSModal>;
|
|
1352
|
-
}
|
|
1353
|
-
function FormModal(props) {
|
|
1354
|
-
const navigate = useNavigate();
|
|
1355
|
-
const location = useLocation();
|
|
1356
|
-
return <SSForm
|
|
1357
|
-
onsubmit={(context) => props.config.onSubmit({
|
|
1358
|
-
hide: props.hide,
|
|
1359
|
-
context,
|
|
1360
|
-
navigate,
|
|
1361
|
-
pathname: location.pathname
|
|
1362
|
-
})}
|
|
1363
|
-
>
|
|
1364
|
-
<FormModalInner visible={props.visible} hide={props.hide} config={props.config} />
|
|
1365
|
-
</SSForm>;
|
|
1366
|
-
}
|
|
1367
|
-
function FormModalInner(props) {
|
|
1368
|
-
const context = SSForm.useContext();
|
|
1369
|
-
if (!context) return null;
|
|
1370
|
-
const modalProps = () => props.config.modalProps?.({ hide: props.hide, context }) ?? {};
|
|
1371
|
-
const {
|
|
1372
|
-
primaryButtonText,
|
|
1373
|
-
secondaryButtonText,
|
|
1374
|
-
hideSecondaryButton,
|
|
1375
|
-
danger,
|
|
1376
|
-
...rest
|
|
1377
|
-
} = modalProps();
|
|
1378
|
-
return <SSModal
|
|
1379
|
-
open={props.visible()}
|
|
1380
|
-
onClose={props.hide}
|
|
1381
|
-
{...rest}
|
|
1382
|
-
footer={<>
|
|
1383
|
-
{!hideSecondaryButton && <SSButton class="secondary" onclick={props.hide} disabled={context.loading()}>
|
|
1384
|
-
{secondaryButtonText ?? "Abbrechen"}
|
|
1385
|
-
</SSButton>}
|
|
1386
|
-
<SSButton
|
|
1387
|
-
class={danger ? "danger" : void 0}
|
|
1388
|
-
onclick={() => context.submit()}
|
|
1389
|
-
disabled={context.loading()}
|
|
1390
|
-
>
|
|
1391
|
-
{primaryButtonText ?? "Speichern"}
|
|
1392
|
-
</SSButton>
|
|
1393
|
-
</>}
|
|
1394
|
-
>
|
|
1395
|
-
{props.config.content({ hide: props.hide, context })}
|
|
1396
|
-
</SSModal>;
|
|
1397
|
-
}
|
|
1398
|
-
function SSModalsProvider(props) {
|
|
1399
|
-
const [modals, setModals] = createSignal7([]);
|
|
1400
|
-
const modalsById = /* @__PURE__ */ new Map();
|
|
1401
|
-
const closeTimeouts = /* @__PURE__ */ new Map();
|
|
1402
|
-
const removeDelayMs = 220;
|
|
1403
|
-
const hide = (id) => {
|
|
1404
|
-
const modal = modalsById.get(id);
|
|
1405
|
-
if (!modal) return;
|
|
1406
|
-
modal.setVisible(false);
|
|
1407
|
-
const existing = closeTimeouts.get(id);
|
|
1408
|
-
if (existing) window.clearTimeout(existing);
|
|
1409
|
-
const timeout = window.setTimeout(() => {
|
|
1410
|
-
setModals((list) => list.filter((modal2) => modal2.id !== id));
|
|
1411
|
-
modalsById.delete(id);
|
|
1412
|
-
closeTimeouts.delete(id);
|
|
1413
|
-
}, removeDelayMs);
|
|
1414
|
-
closeTimeouts.set(id, timeout);
|
|
1415
|
-
};
|
|
1416
|
-
const show = (render) => {
|
|
1417
|
-
const id = nextModalId();
|
|
1418
|
-
const [visible, setVisible] = createSignal7(true);
|
|
1419
|
-
const entry = { id, visible, setVisible, render };
|
|
1420
|
-
modalsById.set(id, entry);
|
|
1421
|
-
setModals((list) => [...list, entry]);
|
|
1422
|
-
return id;
|
|
1423
|
-
};
|
|
1424
|
-
const showDefault = (config) => {
|
|
1425
|
-
return show(({ hide: hide2, visible }) => <DefaultModal visible={visible} hide={hide2} config={config} />);
|
|
1426
|
-
};
|
|
1427
|
-
const showForm = (config) => {
|
|
1428
|
-
return show(({ hide: hide2, visible }) => <FormModal visible={visible} hide={hide2} config={config} />);
|
|
1429
|
-
};
|
|
1430
|
-
onCleanup5(() => {
|
|
1431
|
-
closeTimeouts.forEach((timeout) => window.clearTimeout(timeout));
|
|
1432
|
-
closeTimeouts.clear();
|
|
1433
|
-
});
|
|
1434
|
-
return <SSModalsContext.Provider value={{ show, showDefault, showForm, hide }}>
|
|
1435
|
-
{props.children}
|
|
1436
|
-
|
|
1437
|
-
<For4 each={modals()}>
|
|
1438
|
-
{(modal) => {
|
|
1439
|
-
const hideModal = () => hide(modal.id);
|
|
1440
|
-
return modal.render({ id: modal.id, hide: hideModal, visible: modal.visible });
|
|
1441
|
-
}}
|
|
1442
|
-
</For4>
|
|
1443
|
-
</SSModalsContext.Provider>;
|
|
1444
|
-
}
|
|
1445
|
-
|
|
1446
|
-
// src/components/SSShell.tsx
|
|
1447
|
-
import { createContext as createContext3, createMemo as createMemo4, createSignal as createSignal8, createUniqueId as createUniqueId3, onCleanup as onCleanup6, onMount as onMount4, useContext as useContext3 } from "solid-js";
|
|
1448
|
-
import { useLocation as useLocation2 } from "@solidjs/router";
|
|
1449
|
-
var SSShellContext = createContext3();
|
|
1450
|
-
function SSShell(props) {
|
|
1451
|
-
const drawerId = createUniqueId3();
|
|
1452
|
-
const location = useLocation2();
|
|
1453
|
-
const [hrefs, setHrefs] = createSignal8([]);
|
|
1454
|
-
const closeDrawer = () => {
|
|
1455
|
-
const input = document.getElementById(drawerId);
|
|
1456
|
-
if (input) input.checked = false;
|
|
1457
|
-
};
|
|
1458
|
-
const registerHref = (href) => {
|
|
1459
|
-
setHrefs((prev) => prev.includes(href) ? prev : [...prev, href]);
|
|
1460
|
-
};
|
|
1461
|
-
const unregisterHref = (href) => {
|
|
1462
|
-
setHrefs((prev) => prev.filter((item) => item !== href));
|
|
1463
|
-
};
|
|
1464
|
-
const activeHref = createMemo4(() => {
|
|
1465
|
-
const path = location.pathname;
|
|
1466
|
-
let best = null;
|
|
1467
|
-
for (const href of hrefs()) {
|
|
1468
|
-
if (!path.startsWith(href)) continue;
|
|
1469
|
-
if (!best || href.length > best.length) {
|
|
1470
|
-
best = href;
|
|
1471
|
-
}
|
|
1472
|
-
}
|
|
1473
|
-
return best;
|
|
1474
|
-
});
|
|
1475
|
-
return <SSShellContext.Provider value={{ closeDrawer, activeHref, registerHref, unregisterHref }}>
|
|
1476
|
-
<div class={`ss_shell ${props.class ?? ""}`} style={props.style}>
|
|
1477
|
-
<input id={drawerId} type="checkbox" class="ss_shell__drawer_toggle_input" />
|
|
1478
|
-
|
|
1479
|
-
<header class="ss_shell__header">
|
|
1480
|
-
<div class="ss_shell__header_left">
|
|
1481
|
-
<label
|
|
1482
|
-
for={drawerId}
|
|
1483
|
-
class="ss_shell__drawer_toggle ss_button ss_button--icon"
|
|
1484
|
-
aria-label="Navigation öffnen"
|
|
1485
|
-
role="button"
|
|
1486
|
-
tabindex="0"
|
|
1487
|
-
>
|
|
1488
|
-
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="icon icon-tabler icons-tabler-outline icon-tabler-menu-2"><path stroke="none" d="M0 0h24v24H0z" fill="none" /><path d="M4 6l16 0" /><path d="M4 12l16 0" /><path d="M4 18l16 0" /></svg>
|
|
1489
|
-
</label>
|
|
1490
|
-
<div class="ss_shell__title">{props.title}</div>
|
|
1491
|
-
</div>
|
|
1492
|
-
<div class="ss_shell__actions">{props.actions}</div>
|
|
1493
|
-
</header>
|
|
1494
|
-
|
|
1495
|
-
<div class="ss_shell__body">
|
|
1496
|
-
<nav class="ss_shell__nav" aria-label="Hauptnavigation">
|
|
1497
|
-
<div class="ss_shell__nav_inner">{props.nav}</div>
|
|
1498
|
-
</nav>
|
|
1499
|
-
<div class="ss_shell__main">{props.children}</div>
|
|
1500
|
-
<label for={drawerId} class="ss_shell__scrim" aria-label="Navigation schließen" />
|
|
1501
|
-
</div>
|
|
1502
|
-
</div>
|
|
1503
|
-
</SSShellContext.Provider>;
|
|
1504
|
-
}
|
|
1505
|
-
SSShell.Nav = function(props) {
|
|
1506
|
-
return <div class="ss_shell__nav_list">{props.children}</div>;
|
|
1507
|
-
};
|
|
1508
|
-
SSShell.NavLink = function(props) {
|
|
1509
|
-
const context = useContext3(SSShellContext);
|
|
1510
|
-
onMount4(() => context?.registerHref(props.href));
|
|
1511
|
-
onCleanup6(() => context?.unregisterHref(props.href));
|
|
1512
|
-
const isActive = () => context?.activeHref() === props.href;
|
|
1513
|
-
return <a
|
|
1514
|
-
class="ss_shell__nav_item"
|
|
1515
|
-
classList={{ "ss_shell__nav_item--active": isActive() }}
|
|
1516
|
-
href={props.href}
|
|
1517
|
-
onclick={() => {
|
|
1518
|
-
props.onclick?.();
|
|
1519
|
-
context?.closeDrawer();
|
|
1520
|
-
}}
|
|
1521
|
-
>
|
|
1522
|
-
{props.icon && <span class="ss_shell__nav_icon">{props.icon}</span>}
|
|
1523
|
-
<span class="ss_shell__nav_label">{props.children}</span>
|
|
1524
|
-
</a>;
|
|
1525
|
-
};
|
|
1526
|
-
SSShell.NavAction = function(props) {
|
|
1527
|
-
const context = useContext3(SSShellContext);
|
|
1528
|
-
return <button
|
|
1529
|
-
type="button"
|
|
1530
|
-
class="ss_shell__nav_item"
|
|
1531
|
-
onclick={() => {
|
|
1532
|
-
props.onclick();
|
|
1533
|
-
context?.closeDrawer();
|
|
1534
|
-
}}
|
|
1535
|
-
>
|
|
1536
|
-
{props.icon && <span class="ss_shell__nav_icon">{props.icon}</span>}
|
|
1537
|
-
<span class="ss_shell__nav_label">{props.children}</span>
|
|
1538
|
-
</button>;
|
|
1539
|
-
};
|
|
1540
|
-
SSShell.NavGroup = function(props) {
|
|
1541
|
-
return <details class="ss_shell__nav_group" open={props.initiallyExpanded}>
|
|
1542
|
-
<summary class="ss_shell__nav_group_header">
|
|
1543
|
-
{props.icon && <span class="ss_shell__nav_icon">{props.icon}</span>}
|
|
1544
|
-
<span class="ss_shell__nav_label">{props.title}</span>
|
|
1545
|
-
<span class="ss_shell__nav_group_chevron" aria-hidden="true">
|
|
1546
|
-
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="ss_shell__nav_group_chevron_svg"><path stroke="none" d="M0 0h24v24H0z" fill="none" /><path d="M9 6l6 6l-6 6" /></svg>
|
|
1547
|
-
</span>
|
|
1548
|
-
</summary>
|
|
1549
|
-
<div class="ss_shell__nav_group_items">{props.children}</div>
|
|
1550
|
-
</details>;
|
|
1551
|
-
};
|
|
1552
|
-
|
|
1553
|
-
// src/components/SSSurface.tsx
|
|
1554
|
-
function SSSurface(props) {
|
|
1555
|
-
return <div class={`ss_surface ${props.class ?? ""}`} style={props.style}>
|
|
1556
|
-
{props.children}
|
|
1557
|
-
</div>;
|
|
1558
|
-
}
|
|
1559
|
-
|
|
1560
|
-
// src/components/SSTile.tsx
|
|
1561
|
-
import { A } from "@solidjs/router";
|
|
1562
|
-
function SSTile(props) {
|
|
1563
|
-
return <div class={`ss_tile ${props.class ?? ""}`} style={props.style}>
|
|
1564
|
-
<div class="ss_tile__row">
|
|
1565
|
-
{props.icon && <span class="ss_tile__icon">{props.icon}</span>}
|
|
1566
|
-
<div class="ss_tile__content">
|
|
1567
|
-
{props.href ? <h5 class="ss_tile__title">
|
|
1568
|
-
<A class="ss_tile__link" href={props.href} onclick={props.onLinkClick}>
|
|
1569
|
-
{props.title}
|
|
1570
|
-
</A>
|
|
1571
|
-
</h5> : <h5 class="ss_tile__title">
|
|
1572
|
-
<span class="ss_tile__text">{props.title}</span>
|
|
1573
|
-
</h5>}
|
|
1574
|
-
{props.subtitle && <div class="ss_tile__subtitle">{props.subtitle}</div>}
|
|
1575
|
-
</div>
|
|
1576
|
-
{props.trailing && <div class="ss_tile__trailing">{props.trailing}</div>}
|
|
1577
|
-
</div>
|
|
1578
|
-
</div>;
|
|
1579
|
-
}
|
|
1580
|
-
function createSSTile(build) {
|
|
1581
|
-
return function(props) {
|
|
1582
|
-
const built = build(props.data);
|
|
1583
|
-
return <SSTile
|
|
1584
|
-
{...built}
|
|
1585
|
-
onLinkClick={props.onLinkClick ?? built.onLinkClick}
|
|
1586
|
-
href={props.noLink ? void 0 : built.href}
|
|
1587
|
-
icon={props.noIcon ? void 0 : built.icon}
|
|
1588
|
-
/>;
|
|
1589
|
-
};
|
|
1590
|
-
}
|
|
1591
|
-
export {
|
|
1592
|
-
SSButton,
|
|
1593
|
-
SSCallout,
|
|
1594
|
-
SSChip,
|
|
1595
|
-
SSDataTable,
|
|
1596
|
-
SSDropdown,
|
|
1597
|
-
SSExpandable,
|
|
1598
|
-
SSForm,
|
|
1599
|
-
SSHeader,
|
|
1600
|
-
SSModal,
|
|
1601
|
-
SSModalsProvider,
|
|
1602
|
-
SSShell,
|
|
1603
|
-
SSSurface,
|
|
1604
|
-
SSTile,
|
|
1605
|
-
createSSTile,
|
|
1606
|
-
useSSModals
|
|
1607
|
-
};
|