runeforge 0.0.55 → 0.0.56
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 -1342
- 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 +469 -376
- package/dist/components/crud/GenericCRUD.svelte +426 -426
- package/dist/components/crud/SearchInput.svelte +53 -53
- package/dist/components/crud/columns/Avatar.svelte +15 -15
- package/dist/components/crud/columns/Icon.svelte +8 -8
- package/dist/components/crud/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 -291
- package/dist/components/crud/views/list/Modals.svelte +56 -56
- package/dist/components/crud/views/list/Table.svelte +176 -176
- package/dist/components/crud/views/list/Toolbar.svelte +341 -341
- 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 -536
- package/dist/components/table/Paginator.svelte +113 -113
- package/dist/components/table/SortHeader.svelte +43 -43
- package/dist/components/table/TableBody.svelte +150 -150
- package/dist/components/table/TableHeader.svelte +88 -88
- package/dist/i18n/en.js +1 -0
- package/dist/i18n/es.js +1 -0
- package/dist/i18n/types.d.ts +1 -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 -7
- package/dist/icons/defaults/Home.svelte +6 -6
- package/dist/icons/defaults/PasswordHide.svelte +9 -9
- package/dist/icons/defaults/PasswordShow.svelte +7 -7
- package/dist/icons/defaults/SortAsc.svelte +6 -6
- package/dist/icons/defaults/SortDesc.svelte +6 -6
- package/dist/icons/defaults/SortNone.svelte +6 -6
- package/dist/icons/defaults/View.svelte +7 -7
- package/package.json +1 -1
|
@@ -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,150 +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, 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
|
+
<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>
|