runeforge 0.0.53 → 0.0.55
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 +1342 -1222
- package/dist/components/Avatar.svelte +31 -31
- package/dist/components/IconRenderer.svelte +22 -22
- package/dist/components/Modal.svelte +75 -75
- package/dist/components/common/Header.svelte +40 -40
- package/dist/components/crud/EmbeddedField.svelte +175 -175
- package/dist/components/crud/Field.svelte +376 -376
- package/dist/components/crud/GenericCRUD.svelte +426 -435
- package/dist/components/crud/GenericCRUD.svelte.d.ts +6 -9
- 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/utils/reorder.d.ts +4 -0
- package/dist/components/crud/utils/reorder.js +13 -0
- package/dist/components/crud/views/Create.svelte +232 -232
- package/dist/components/crud/views/Read.svelte +124 -124
- package/dist/components/crud/views/Update.svelte +208 -208
- package/dist/components/crud/views/list/List.svelte +291 -0
- package/dist/components/crud/views/{List.svelte.d.ts → list/List.svelte.d.ts} +4 -14
- package/dist/components/crud/views/list/Modals.svelte +56 -0
- package/dist/components/crud/views/list/Modals.svelte.d.ts +36 -0
- package/dist/components/crud/views/list/Table.svelte +176 -0
- package/dist/components/crud/views/list/Table.svelte.d.ts +59 -0
- package/dist/components/crud/views/list/Toolbar.svelte +341 -0
- package/dist/components/crud/views/list/Toolbar.svelte.d.ts +46 -0
- 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 +62 -62
- package/dist/components/form/TreeNode.svelte +66 -66
- package/dist/components/navigation/Breadcrumbs.svelte +111 -111
- package/dist/components/table/ColumnFilter.svelte +168 -168
- package/dist/components/table/PaginatedTable.svelte +536 -215
- package/dist/components/table/PaginatedTable.svelte.d.ts +11 -2
- package/dist/components/table/Paginator.svelte +113 -113
- package/dist/components/table/SortHeader.svelte +43 -43
- package/dist/components/table/TableBody.svelte +150 -70
- package/dist/components/table/TableBody.svelte.d.ts +14 -1
- package/dist/components/table/TableHeader.svelte +88 -83
- package/dist/components/table/TableHeader.svelte.d.ts +1 -0
- package/dist/components/table/sortable.d.ts +27 -0
- package/dist/components/table/sortable.js +55 -0
- package/dist/components/table/utils.d.ts +28 -1
- package/dist/components/table/utils.js +75 -0
- package/dist/i18n/en.js +2 -0
- package/dist/i18n/es.js +2 -0
- package/dist/i18n/types.d.ts +2 -0
- 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 -0
- package/dist/icons/defaults/Grip.svelte.d.ts +7 -0
- 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/icons/sets/bootstrap.js +2 -1
- package/dist/icons/sets/default.js +2 -0
- package/dist/icons/types.d.ts +3 -0
- package/dist/index.d.ts +5 -4
- package/dist/index.js +3 -2
- package/dist/types/attribute.d.ts +9 -0
- package/dist/types/crud.d.ts +72 -1
- package/dist/types/table.d.ts +56 -0
- package/package.json +3 -1
- package/dist/components/crud/views/List.svelte +0 -584
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { SvelteSet } from 'svelte/reactivity';
|
|
2
|
-
import type { FilterSnapshot, ServerPagination, SortDirection, TableQuery } from '../../types/table.js';
|
|
3
|
-
import type
|
|
2
|
+
import type { FilterSnapshot, ReorderOptions, ServerPagination, SortDirection, TableQuery } from '../../types/table.js';
|
|
3
|
+
import { type Snippet } from 'svelte';
|
|
4
4
|
import type { ColumnDefinition } from '../../types/crud.js';
|
|
5
5
|
declare function $$render<T extends object = Record<string, unknown>>(): {
|
|
6
6
|
props: {
|
|
@@ -27,6 +27,15 @@ declare function $$render<T extends object = Record<string, unknown>>(): {
|
|
|
27
27
|
/** Current ordering + filters snapshot, kept in sync for callers that
|
|
28
28
|
* need to replicate the active query (e.g. exporting server-side). */
|
|
29
29
|
query?: TableQuery;
|
|
30
|
+
/** Turns on drag-to-reorder. Per-column filters are suppressed and row
|
|
31
|
+
* order is fully owned by `reorder.compare`/`reorder.attribute` while
|
|
32
|
+
* it's active — ignored entirely in server-pagination mode
|
|
33
|
+
* (`pagination` set), since the full row set needs to be reachable
|
|
34
|
+
* client-side for drag positions to be meaningful. */
|
|
35
|
+
reorder?: ReorderOptions<T>;
|
|
36
|
+
/** Fires after a drag settles with the complete reordered row list
|
|
37
|
+
* (client mode only). The caller decides how to persist it. */
|
|
38
|
+
onReorder?: (rows: T[]) => void;
|
|
30
39
|
};
|
|
31
40
|
exports: {};
|
|
32
41
|
bindings: "selected" | "visibleRows" | "query";
|
|
@@ -1,113 +1,113 @@
|
|
|
1
|
-
<script lang="ts">
|
|
2
|
-
import Button from '../form/Button.svelte';
|
|
3
|
-
import { getStrings } from '../../i18n/context.js';
|
|
4
|
-
|
|
5
|
-
const strings = getStrings();
|
|
6
|
-
|
|
7
|
-
let {
|
|
8
|
-
page = $bindable(1),
|
|
9
|
-
totalPages,
|
|
10
|
-
pageStart,
|
|
11
|
-
pageSize,
|
|
12
|
-
total
|
|
13
|
-
}: {
|
|
14
|
-
page?: number;
|
|
15
|
-
totalPages: number;
|
|
16
|
-
pageStart: number;
|
|
17
|
-
pageSize: number;
|
|
18
|
-
total: number;
|
|
19
|
-
} = $props();
|
|
20
|
-
|
|
21
|
-
function prev() {
|
|
22
|
-
if (page > 1) page--;
|
|
23
|
-
}
|
|
24
|
-
function next() {
|
|
25
|
-
if (page < totalPages) page++;
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
function buildPages(current: number, total: number): number[] {
|
|
29
|
-
if (total <= 7) return Array.from({ length: total }, (_, i) => i + 1);
|
|
30
|
-
|
|
31
|
-
// Local, immediately-discarded dedup set for this one calculation, not
|
|
32
|
-
// reactive state — a plain Set is correct here.
|
|
33
|
-
// eslint-disable-next-line svelte/prefer-svelte-reactivity
|
|
34
|
-
const visible = new Set<number>();
|
|
35
|
-
visible.add(1);
|
|
36
|
-
visible.add(total);
|
|
37
|
-
for (let i = Math.max(1, current - 2); i <= Math.min(total, current + 2); i++) {
|
|
38
|
-
visible.add(i);
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
const sorted = Array.from(visible).sort((a, b) => a - b);
|
|
42
|
-
const result: number[] = [];
|
|
43
|
-
for (let i = 0; i < sorted.length; i++) {
|
|
44
|
-
if (i > 0 && sorted[i] - sorted[i - 1] > 1) result.push(0);
|
|
45
|
-
result.push(sorted[i]);
|
|
46
|
-
}
|
|
47
|
-
return result;
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
const pages = $derived(buildPages(page, totalPages));
|
|
51
|
-
const inputWidth = $derived(`${String(totalPages).length + 4}ch`);
|
|
52
|
-
|
|
53
|
-
function handlePageInput(e: KeyboardEvent) {
|
|
54
|
-
if (e.key !== 'Enter') return;
|
|
55
|
-
const val = parseInt((e.currentTarget as HTMLInputElement).value);
|
|
56
|
-
if (!isNaN(val) && val >= 1 && val <= totalPages) {
|
|
57
|
-
page = val;
|
|
58
|
-
} else {
|
|
59
|
-
(e.currentTarget as HTMLInputElement).value = String(page);
|
|
60
|
-
}
|
|
61
|
-
}
|
|
62
|
-
|
|
63
|
-
function resetInput(e: FocusEvent) {
|
|
64
|
-
(e.currentTarget as HTMLInputElement).value = String(page);
|
|
65
|
-
}
|
|
66
|
-
</script>
|
|
67
|
-
|
|
68
|
-
{#if totalPages > 1}
|
|
69
|
-
<div class="flex items-center justify-between">
|
|
70
|
-
<span class="text-sm text-base-content/60">
|
|
71
|
-
{strings.showing(pageStart + 1, Math.min(pageStart + pageSize, total), total)}
|
|
72
|
-
</span>
|
|
73
|
-
|
|
74
|
-
<div class="join">
|
|
75
|
-
<Button class="join-item btn-sm" disabled={page === 1} onclick={prev}>«</Button>
|
|
76
|
-
|
|
77
|
-
{#each pages as p, i (i)}
|
|
78
|
-
{#if p === 0}
|
|
79
|
-
<Button class="join-item btn-sm" disabled>…</Button>
|
|
80
|
-
{:else if p === page}
|
|
81
|
-
<input
|
|
82
|
-
type="number"
|
|
83
|
-
class="join-item btn btn-sm no-spinner text-center"
|
|
84
|
-
style="background-color: var(--color-base-100); width: {inputWidth};"
|
|
85
|
-
min="1"
|
|
86
|
-
max={totalPages}
|
|
87
|
-
value={page}
|
|
88
|
-
onkeydown={handlePageInput}
|
|
89
|
-
onblur={resetInput}
|
|
90
|
-
/>
|
|
91
|
-
{:else}
|
|
92
|
-
<Button class="join-item btn-sm" onclick={() => (page = p)}>{p}</Button>
|
|
93
|
-
{/if}
|
|
94
|
-
{/each}
|
|
95
|
-
|
|
96
|
-
<Button class="join-item btn-sm" disabled={page === totalPages} onclick={next}>»</Button>
|
|
97
|
-
</div>
|
|
98
|
-
</div>
|
|
99
|
-
{/if}
|
|
100
|
-
|
|
101
|
-
<style>
|
|
102
|
-
.no-spinner {
|
|
103
|
-
appearance: none;
|
|
104
|
-
-moz-appearance: textfield;
|
|
105
|
-
}
|
|
106
|
-
|
|
107
|
-
.no-spinner::-webkit-outer-spin-button,
|
|
108
|
-
.no-spinner::-webkit-inner-spin-button {
|
|
109
|
-
-webkit-appearance: none;
|
|
110
|
-
margin: 0;
|
|
111
|
-
}
|
|
112
|
-
</style>
|
|
113
|
-
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import Button from '../form/Button.svelte';
|
|
3
|
+
import { getStrings } from '../../i18n/context.js';
|
|
4
|
+
|
|
5
|
+
const strings = getStrings();
|
|
6
|
+
|
|
7
|
+
let {
|
|
8
|
+
page = $bindable(1),
|
|
9
|
+
totalPages,
|
|
10
|
+
pageStart,
|
|
11
|
+
pageSize,
|
|
12
|
+
total
|
|
13
|
+
}: {
|
|
14
|
+
page?: number;
|
|
15
|
+
totalPages: number;
|
|
16
|
+
pageStart: number;
|
|
17
|
+
pageSize: number;
|
|
18
|
+
total: number;
|
|
19
|
+
} = $props();
|
|
20
|
+
|
|
21
|
+
function prev() {
|
|
22
|
+
if (page > 1) page--;
|
|
23
|
+
}
|
|
24
|
+
function next() {
|
|
25
|
+
if (page < totalPages) page++;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function buildPages(current: number, total: number): number[] {
|
|
29
|
+
if (total <= 7) return Array.from({ length: total }, (_, i) => i + 1);
|
|
30
|
+
|
|
31
|
+
// Local, immediately-discarded dedup set for this one calculation, not
|
|
32
|
+
// reactive state — a plain Set is correct here.
|
|
33
|
+
// eslint-disable-next-line svelte/prefer-svelte-reactivity
|
|
34
|
+
const visible = new Set<number>();
|
|
35
|
+
visible.add(1);
|
|
36
|
+
visible.add(total);
|
|
37
|
+
for (let i = Math.max(1, current - 2); i <= Math.min(total, current + 2); i++) {
|
|
38
|
+
visible.add(i);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
const sorted = Array.from(visible).sort((a, b) => a - b);
|
|
42
|
+
const result: number[] = [];
|
|
43
|
+
for (let i = 0; i < sorted.length; i++) {
|
|
44
|
+
if (i > 0 && sorted[i] - sorted[i - 1] > 1) result.push(0);
|
|
45
|
+
result.push(sorted[i]);
|
|
46
|
+
}
|
|
47
|
+
return result;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
const pages = $derived(buildPages(page, totalPages));
|
|
51
|
+
const inputWidth = $derived(`${String(totalPages).length + 4}ch`);
|
|
52
|
+
|
|
53
|
+
function handlePageInput(e: KeyboardEvent) {
|
|
54
|
+
if (e.key !== 'Enter') return;
|
|
55
|
+
const val = parseInt((e.currentTarget as HTMLInputElement).value);
|
|
56
|
+
if (!isNaN(val) && val >= 1 && val <= totalPages) {
|
|
57
|
+
page = val;
|
|
58
|
+
} else {
|
|
59
|
+
(e.currentTarget as HTMLInputElement).value = String(page);
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
function resetInput(e: FocusEvent) {
|
|
64
|
+
(e.currentTarget as HTMLInputElement).value = String(page);
|
|
65
|
+
}
|
|
66
|
+
</script>
|
|
67
|
+
|
|
68
|
+
{#if totalPages > 1}
|
|
69
|
+
<div class="flex items-center justify-between">
|
|
70
|
+
<span class="text-sm text-base-content/60">
|
|
71
|
+
{strings.showing(pageStart + 1, Math.min(pageStart + pageSize, total), total)}
|
|
72
|
+
</span>
|
|
73
|
+
|
|
74
|
+
<div class="join">
|
|
75
|
+
<Button class="join-item btn-sm" disabled={page === 1} onclick={prev}>«</Button>
|
|
76
|
+
|
|
77
|
+
{#each pages as p, i (i)}
|
|
78
|
+
{#if p === 0}
|
|
79
|
+
<Button class="join-item btn-sm" disabled>…</Button>
|
|
80
|
+
{:else if p === page}
|
|
81
|
+
<input
|
|
82
|
+
type="number"
|
|
83
|
+
class="join-item btn btn-sm no-spinner text-center"
|
|
84
|
+
style="background-color: var(--color-base-100); width: {inputWidth};"
|
|
85
|
+
min="1"
|
|
86
|
+
max={totalPages}
|
|
87
|
+
value={page}
|
|
88
|
+
onkeydown={handlePageInput}
|
|
89
|
+
onblur={resetInput}
|
|
90
|
+
/>
|
|
91
|
+
{:else}
|
|
92
|
+
<Button class="join-item btn-sm" onclick={() => (page = p)}>{p}</Button>
|
|
93
|
+
{/if}
|
|
94
|
+
{/each}
|
|
95
|
+
|
|
96
|
+
<Button class="join-item btn-sm" disabled={page === totalPages} onclick={next}>»</Button>
|
|
97
|
+
</div>
|
|
98
|
+
</div>
|
|
99
|
+
{/if}
|
|
100
|
+
|
|
101
|
+
<style>
|
|
102
|
+
.no-spinner {
|
|
103
|
+
appearance: none;
|
|
104
|
+
-moz-appearance: textfield;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
.no-spinner::-webkit-outer-spin-button,
|
|
108
|
+
.no-spinner::-webkit-inner-spin-button {
|
|
109
|
+
-webkit-appearance: none;
|
|
110
|
+
margin: 0;
|
|
111
|
+
}
|
|
112
|
+
</style>
|
|
113
|
+
|
|
@@ -1,43 +1,43 @@
|
|
|
1
|
-
<script lang="ts">
|
|
2
|
-
import Button from '../form/Button.svelte';
|
|
3
|
-
import { getIconSet } from '../../icons/context.js';
|
|
4
|
-
import { defaultIconSet } from '../../icons/sets/default.js';
|
|
5
|
-
import type { SortDirection } from '../../types/table.js';
|
|
6
|
-
|
|
7
|
-
let {
|
|
8
|
-
title,
|
|
9
|
-
sortable = true,
|
|
10
|
-
direction = null,
|
|
11
|
-
onsort,
|
|
12
|
-
}: {
|
|
13
|
-
title: string;
|
|
14
|
-
sortable?: boolean;
|
|
15
|
-
direction?: SortDirection | null;
|
|
16
|
-
onsort?: () => void;
|
|
17
|
-
} = $props();
|
|
18
|
-
|
|
19
|
-
const icons = $derived(getIconSet() ?? defaultIconSet);
|
|
20
|
-
</script>
|
|
21
|
-
|
|
22
|
-
{#if sortable}
|
|
23
|
-
<Button
|
|
24
|
-
btn={false}
|
|
25
|
-
class="flex cursor-pointer items-center gap-1 capitalize hover:text-primary"
|
|
26
|
-
onclick={onsort}
|
|
27
|
-
title="Ordenar"
|
|
28
|
-
>
|
|
29
|
-
<span>{title}</span>
|
|
30
|
-
{#if direction === 'desc'}
|
|
31
|
-
{@const Icon = icons.sortDesc}
|
|
32
|
-
<Icon class="size-3" />
|
|
33
|
-
{:else if direction === 'asc'}
|
|
34
|
-
{@const Icon = icons.sortAsc}
|
|
35
|
-
<Icon class="size-3" />
|
|
36
|
-
{:else}
|
|
37
|
-
{@const Icon = icons.sortNone}
|
|
38
|
-
<Icon class="size-3 opacity-30" />
|
|
39
|
-
{/if}
|
|
40
|
-
</Button>
|
|
41
|
-
{:else}
|
|
42
|
-
<span class="capitalize">{title}</span>
|
|
43
|
-
{/if}
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import Button from '../form/Button.svelte';
|
|
3
|
+
import { getIconSet } from '../../icons/context.js';
|
|
4
|
+
import { defaultIconSet } from '../../icons/sets/default.js';
|
|
5
|
+
import type { SortDirection } from '../../types/table.js';
|
|
6
|
+
|
|
7
|
+
let {
|
|
8
|
+
title,
|
|
9
|
+
sortable = true,
|
|
10
|
+
direction = null,
|
|
11
|
+
onsort,
|
|
12
|
+
}: {
|
|
13
|
+
title: string;
|
|
14
|
+
sortable?: boolean;
|
|
15
|
+
direction?: SortDirection | null;
|
|
16
|
+
onsort?: () => void;
|
|
17
|
+
} = $props();
|
|
18
|
+
|
|
19
|
+
const icons = $derived(getIconSet() ?? defaultIconSet);
|
|
20
|
+
</script>
|
|
21
|
+
|
|
22
|
+
{#if sortable}
|
|
23
|
+
<Button
|
|
24
|
+
btn={false}
|
|
25
|
+
class="flex cursor-pointer items-center gap-1 capitalize hover:text-primary"
|
|
26
|
+
onclick={onsort}
|
|
27
|
+
title="Ordenar"
|
|
28
|
+
>
|
|
29
|
+
<span>{title}</span>
|
|
30
|
+
{#if direction === 'desc'}
|
|
31
|
+
{@const Icon = icons.sortDesc}
|
|
32
|
+
<Icon class="size-3" />
|
|
33
|
+
{:else if direction === 'asc'}
|
|
34
|
+
{@const Icon = icons.sortAsc}
|
|
35
|
+
<Icon class="size-3" />
|
|
36
|
+
{:else}
|
|
37
|
+
{@const Icon = icons.sortNone}
|
|
38
|
+
<Icon class="size-3 opacity-30" />
|
|
39
|
+
{/if}
|
|
40
|
+
</Button>
|
|
41
|
+
{:else}
|
|
42
|
+
<span class="capitalize">{title}</span>
|
|
43
|
+
{/if}
|
|
@@ -1,70 +1,150 @@
|
|
|
1
|
-
<script lang="ts" generics="T extends object">
|
|
2
|
-
import type { Snippet } from 'svelte';
|
|
3
|
-
import type { SvelteSet } from 'svelte/reactivity';
|
|
4
|
-
import type { ColumnDefinition } from '../../types/crud.js';
|
|
5
|
-
import type { IndexedRow } from '../../types/table.js';
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
columns
|
|
17
|
-
rows
|
|
18
|
-
selectable
|
|
19
|
-
selected
|
|
20
|
-
onToggle
|
|
21
|
-
colCount
|
|
22
|
-
rowActions
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
{
|
|
28
|
-
<
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
1
|
+
<script lang="ts" generics="T extends object">
|
|
2
|
+
import type { Snippet } from 'svelte';
|
|
3
|
+
import type { SvelteSet } from 'svelte/reactivity';
|
|
4
|
+
import type { ColumnDefinition } from '../../types/crud.js';
|
|
5
|
+
import type { IndexedRow, ReorderOptions } from '../../types/table.js';
|
|
6
|
+
import { sortableRows, type SortEndIndices } from './sortable.js';
|
|
7
|
+
import { moveIndexedRows } from './utils.js';
|
|
8
|
+
import Button from '../form/Button.svelte';
|
|
9
|
+
import { getIconSet } from '../../icons/context.js';
|
|
10
|
+
import { defaultIconSet } from '../../icons/sets/default.js';
|
|
11
|
+
import { getStrings } from '../../i18n/context.js';
|
|
12
|
+
|
|
13
|
+
const strings = getStrings();
|
|
14
|
+
|
|
15
|
+
let {
|
|
16
|
+
columns,
|
|
17
|
+
rows,
|
|
18
|
+
selectable,
|
|
19
|
+
selected,
|
|
20
|
+
onToggle,
|
|
21
|
+
colCount,
|
|
22
|
+
rowActions,
|
|
23
|
+
reorder,
|
|
24
|
+
visibleRange,
|
|
25
|
+
onDragStart,
|
|
26
|
+
onReorder,
|
|
27
|
+
}: {
|
|
28
|
+
columns: ColumnDefinition<T>[];
|
|
29
|
+
rows: IndexedRow<T>[];
|
|
30
|
+
selectable: boolean;
|
|
31
|
+
selected: SvelteSet<number>;
|
|
32
|
+
onToggle: (index: number) => void;
|
|
33
|
+
colCount: number;
|
|
34
|
+
rowActions?: Snippet<[T]>;
|
|
35
|
+
reorder?: ReorderOptions<T>;
|
|
36
|
+
/** Reorder mode only: positions within `rows` (not the `index` field)
|
|
37
|
+
* that are actually on screen — the rest render `hidden` so they stay in
|
|
38
|
+
* the DOM (and reachable by SortableJS) across a page flip mid-drag. */
|
|
39
|
+
visibleRange?: { start: number; end: number };
|
|
40
|
+
onDragStart?: () => void;
|
|
41
|
+
/** Fires once a drag settles, with the complete row list in its new
|
|
42
|
+
* order — computed from SortableJS's own before/after indices, correct
|
|
43
|
+
* uniformly for a single-row drag and a `multiDrag` group move alike. */
|
|
44
|
+
onReorder?: (rows: IndexedRow<T>[]) => void;
|
|
45
|
+
} = $props();
|
|
46
|
+
|
|
47
|
+
const icons = $derived(getIconSet() ?? defaultIconSet);
|
|
48
|
+
|
|
49
|
+
let tbodyEl: HTMLElement | undefined = $state();
|
|
50
|
+
|
|
51
|
+
function handleDragEnd(indices: SortEndIndices) {
|
|
52
|
+
const fromIndices =
|
|
53
|
+
indices.oldIndicies.length > 0 ? indices.oldIndicies : [indices.oldIndex ?? -1];
|
|
54
|
+
const toIndex =
|
|
55
|
+
indices.newIndicies.length > 0 ? Math.min(...indices.newIndicies) : (indices.newIndex ?? -1);
|
|
56
|
+
onReorder?.(moveIndexedRows(rows, fromIndices, toIndex));
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
// Mirrors the checkbox selection into SortableJS's own MultiDrag selection
|
|
60
|
+
// registry, so a `multiDrag` drag moves whatever's currently checked
|
|
61
|
+
// instead of relying on the plugin's own click/modifier-key selection UX.
|
|
62
|
+
$effect(() => {
|
|
63
|
+
const utils = reorder?.multiDrag ? reorder.sortable.utils : undefined;
|
|
64
|
+
if (!utils || !tbodyEl) return;
|
|
65
|
+
for (const { index } of rows) {
|
|
66
|
+
const el = tbodyEl.querySelector(`[data-row-key="${index}"]`);
|
|
67
|
+
if (!(el instanceof HTMLElement)) continue;
|
|
68
|
+
if (selected.has(index)) utils.select(el);
|
|
69
|
+
else utils.deselect(el);
|
|
70
|
+
}
|
|
71
|
+
});
|
|
72
|
+
</script>
|
|
73
|
+
|
|
74
|
+
<tbody
|
|
75
|
+
data-testid="paginated-table-body"
|
|
76
|
+
bind:this={tbodyEl}
|
|
77
|
+
use:sortableRows={{
|
|
78
|
+
enabled: !!reorder,
|
|
79
|
+
sortable: reorder?.sortable,
|
|
80
|
+
multiDrag: reorder?.multiDrag,
|
|
81
|
+
onStart: () => onDragStart?.(),
|
|
82
|
+
onEnd: handleDragEnd,
|
|
83
|
+
}}
|
|
84
|
+
>
|
|
85
|
+
{#if rows.length === 0}
|
|
86
|
+
<tr>
|
|
87
|
+
<td colspan={colCount} class="py-10 text-center text-base-content/50">
|
|
88
|
+
Sin registros
|
|
89
|
+
</td>
|
|
90
|
+
</tr>
|
|
91
|
+
{:else}
|
|
92
|
+
{#each rows as { row, index }, position (index)}
|
|
93
|
+
{@const isHidden = !!visibleRange && (position < visibleRange.start || position >= visibleRange.end)}
|
|
94
|
+
<tr
|
|
95
|
+
class="hover"
|
|
96
|
+
class:group={!!reorder}
|
|
97
|
+
class:cursor-pointer={selectable}
|
|
98
|
+
data-row-key={index}
|
|
99
|
+
style:display={isHidden ? 'none' : null}
|
|
100
|
+
onclick={selectable ? () => onToggle(index) : undefined}
|
|
101
|
+
>
|
|
102
|
+
{#if reorder}
|
|
103
|
+
{@const GripIcon = reorder.icon ?? icons.grip}
|
|
104
|
+
<td class="w-10 border-l-4 border-base-content/10 p-0 group-hover:border-primary/50">
|
|
105
|
+
<Button
|
|
106
|
+
type="button"
|
|
107
|
+
btn={false}
|
|
108
|
+
data-reorder-handle
|
|
109
|
+
class="flex h-full w-full cursor-grab items-center justify-center py-2 text-base-content/40 active:cursor-grabbing"
|
|
110
|
+
title={strings.reorder}
|
|
111
|
+
aria-label={strings.reorder}
|
|
112
|
+
onclick={(e) => e.stopPropagation()}
|
|
113
|
+
>
|
|
114
|
+
{#if GripIcon}
|
|
115
|
+
<GripIcon class="size-4" />
|
|
116
|
+
{/if}
|
|
117
|
+
</Button>
|
|
118
|
+
</td>
|
|
119
|
+
{/if}
|
|
120
|
+
{#if selectable}
|
|
121
|
+
<td>
|
|
122
|
+
<input
|
|
123
|
+
type="checkbox"
|
|
124
|
+
class="checkbox checkbox-sm"
|
|
125
|
+
checked={selected.has(index)}
|
|
126
|
+
onchange={() => onToggle(index)}
|
|
127
|
+
onclick={(e) => e.stopPropagation()}
|
|
128
|
+
/>
|
|
129
|
+
</td>
|
|
130
|
+
{/if}
|
|
131
|
+
{#each columns as col (col.attribute)}
|
|
132
|
+
<td>
|
|
133
|
+
{#if col.component}
|
|
134
|
+
{@const Cell = col.component}
|
|
135
|
+
<Cell value={row[col.attribute]} row={row} />
|
|
136
|
+
{:else if col.formatter}
|
|
137
|
+
<!-- eslint-disable-next-line svelte/no-at-html-tags -->
|
|
138
|
+
{@html col.formatter(row[col.attribute], row)}
|
|
139
|
+
{:else}
|
|
140
|
+
{String(row[col.attribute] ?? '')}
|
|
141
|
+
{/if}
|
|
142
|
+
</td>
|
|
143
|
+
{/each}
|
|
144
|
+
{#if rowActions}
|
|
145
|
+
<td class="text-right">{@render rowActions(row)}</td>
|
|
146
|
+
{/if}
|
|
147
|
+
</tr>
|
|
148
|
+
{/each}
|
|
149
|
+
{/if}
|
|
150
|
+
</tbody>
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import type { Snippet } from 'svelte';
|
|
2
2
|
import type { SvelteSet } from 'svelte/reactivity';
|
|
3
3
|
import type { ColumnDefinition } from '../../types/crud.js';
|
|
4
|
-
import type { IndexedRow } from '../../types/table.js';
|
|
4
|
+
import type { IndexedRow, ReorderOptions } from '../../types/table.js';
|
|
5
5
|
declare function $$render<T extends object>(): {
|
|
6
6
|
props: {
|
|
7
7
|
columns: ColumnDefinition<T>[];
|
|
@@ -11,6 +11,19 @@ declare function $$render<T extends object>(): {
|
|
|
11
11
|
onToggle: (index: number) => void;
|
|
12
12
|
colCount: number;
|
|
13
13
|
rowActions?: Snippet<[T]>;
|
|
14
|
+
reorder?: ReorderOptions<T>;
|
|
15
|
+
/** Reorder mode only: positions within `rows` (not the `index` field)
|
|
16
|
+
* that are actually on screen — the rest render `hidden` so they stay in
|
|
17
|
+
* the DOM (and reachable by SortableJS) across a page flip mid-drag. */
|
|
18
|
+
visibleRange?: {
|
|
19
|
+
start: number;
|
|
20
|
+
end: number;
|
|
21
|
+
};
|
|
22
|
+
onDragStart?: () => void;
|
|
23
|
+
/** Fires once a drag settles, with the complete row list in its new
|
|
24
|
+
* order — computed from SortableJS's own before/after indices, correct
|
|
25
|
+
* uniformly for a single-row drag and a `multiDrag` group move alike. */
|
|
26
|
+
onReorder?: (rows: IndexedRow<T>[]) => void;
|
|
14
27
|
};
|
|
15
28
|
exports: {};
|
|
16
29
|
bindings: "";
|