@isoftdata/svelte-table 2.9.5 → 2.10.0-beta.0

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.
@@ -1,285 +1,285 @@
1
- <script
2
- lang="ts"
3
- generics="Row extends UuidRowProps"
4
- >
5
- import type { Snippet } from 'svelte'
6
- import type { ClassValue } from 'svelte/elements'
7
- import type { StringKeyOfType } from './tree-utility'
8
-
9
- import type Table from './Table.svelte'
10
- import type { Column, UuidRowProps } from './'
11
- import Td from './Td.svelte'
12
- import { flip } from 'svelte/animate'
13
- import { getContext, tick } from 'svelte'
14
- import Button from '@isoftdata/svelte-button'
15
-
16
- type RankProperty = StringKeyOfType<Row, number | null>
17
-
18
- interface Props {
19
- /** Columns definition */
20
- columns: Array<Column>
21
- /** This should be the same value passed to the table's `rows` prop. You should `bind:` this so changes to the rank column propagate back to the table component.*/
22
- rows: Array<Row>
23
- /** This should be the same value passed to the table's `currentPageRows` prop. */
24
- currentPageRows: Array<Row & { originalIndex: number; uuid: string }>
25
- /** The property of the column containing the "rank" or "order" of the row */
26
- rankProperty: RankProperty
27
- /** The table component this component is in. */
28
- table: Table<Row> | undefined
29
- /** Used to discriminate what is being dragged/dropped. If you have two draggable tables on the same page, consider changing this.*/
30
- dataType?: string
31
- /** Whether to show the rank next to the drag target*/
32
- showRank?: boolean
33
- /** Any classes to apply to every `tr`*/
34
- class?: ClassValue
35
- /** A function that computes any classes you want to add conditionally based on the state of the current row*/
36
- computeClass?: (row: Row) => ClassValue
37
- /** A function that computes if a row should be enabled for reordering(drag or stepper buttons) */
38
- computeReorderability?: ((row: Row) => boolean) | undefined
39
- /** The rank the list starts at. Defaults to 1. */
40
- firstRank?: number
41
- // Snippets
42
- prepend?: Snippet<[{ row: Row & { originalIndex: number; uuid: string } }]>
43
- children?: Snippet<[{ row: Row & { originalIndex: number; uuid: string } }]>
44
- append?: Snippet<[{ row: Row & { originalIndex: number; uuid: string } }]>
45
- // Callbacks
46
- rowClick?: (context: { row: Row & { originalIndex: number; uuid: string } }) => void
47
- drop?: (context: { rows: [Row, Row] }) => void
48
- }
49
-
50
- let {
51
- columns,
52
- rows = $bindable(),
53
- currentPageRows,
54
- rankProperty,
55
- table = $bindable(),
56
- dataType = 'application/table-row',
57
- showRank = false,
58
- class: classNames = '',
59
- computeClass = () => '',
60
- computeReorderability = undefined,
61
- firstRank = 1,
62
- prepend,
63
- children,
64
- append,
65
- rowClick,
66
- drop,
67
- }: Props = $props()
68
-
69
- let drugRowIndex: number | null = $state(null)
70
- let hoveredRowIndex: number | null = $state(null)
71
- let animate = $state(true)
72
-
73
- let maxRank = $derived(rows.length + firstRank - 1)
74
-
75
- const idProp = getContext<'uuid' | keyof Row>('idProp')
76
-
77
- function getRank(row: Row): number {
78
- return row[rankProperty] as number
79
- }
80
-
81
- function setRank(row: Row, rank: number) {
82
- row[rankProperty] = rank as Row[RankProperty]
83
- }
84
-
85
- function relatedTargetIsInTarget(relatedTarget: EventTarget | null, target: EventTarget | null): boolean {
86
- return (
87
- !!relatedTarget &&
88
- !!target &&
89
- relatedTarget instanceof Node &&
90
- target instanceof Node &&
91
- (relatedTarget === target || target.contains(relatedTarget))
92
- )
93
- }
94
-
95
- function normalizeRanks() {
96
- rows.sort((a, b) => getRank(a) - getRank(b))
97
- rows.forEach((row, index) => {
98
- setRank(row, index + firstRank)
99
- })
100
- rows = rows
101
- }
102
-
103
- function dragStart(event: DragEvent, row: Row & { originalIndex: number }) {
104
- if (!event.dataTransfer) {
105
- return
106
- }
107
- event.dataTransfer.setData(dataType, JSON.stringify(row))
108
- // Show the whole row while dragging
109
- if (event.target instanceof Element) {
110
- const tr = event.target.closest('tr')
111
- if (tr) {
112
- event.dataTransfer.setDragImage(tr, 15, 15)
113
- drugRowIndex = row.originalIndex
114
- }
115
- }
116
- event.dataTransfer.effectAllowed = 'move'
117
- event.dataTransfer.dropEffect = 'move'
118
- }
119
-
120
- function dragEnd() {
121
- drugRowIndex = null
122
- hoveredRowIndex = null
123
- }
124
-
125
- function dragEnter(event: DragEvent, row: Row & { originalIndex: number }) {
126
- if (event.dataTransfer?.types.includes(dataType)) {
127
- event.preventDefault()
128
- if (event.target instanceof Element) {
129
- const tr = event.target.closest('tr')
130
- if (tr && !relatedTargetIsInTarget(event.relatedTarget, tr)) {
131
- hoveredRowIndex = row.originalIndex
132
- tr.scrollIntoView({ behavior: 'instant', block: 'nearest' })
133
- }
134
- }
135
- }
136
- }
137
-
138
- function dragLeave(event: DragEvent) {
139
- if (event.dataTransfer?.types.includes(dataType)) {
140
- event.preventDefault()
141
- }
142
- }
143
-
144
- async function onDrop(event: DragEvent) {
145
- if (table && event.dataTransfer?.types.includes(dataType)) {
146
- event.preventDefault()
147
-
148
- const hoveredRow = typeof hoveredRowIndex === 'number' ? rows[hoveredRowIndex] : null
149
- const drugRow = typeof drugRowIndex === 'number' ? rows[drugRowIndex] : null
150
- if (drugRow && hoveredRow) {
151
- // This doesn't allow us to drop something at the end of the list, maybe fix that later if it causes enough grief.
152
- const newRank = getRank(hoveredRow)
153
-
154
- setRank(drugRow, newRank)
155
- setRank(hoveredRow, newRank + 1)
156
-
157
- normalizeRanks()
158
- table.sortColumn = columns.find(column => column.property === rankProperty)
159
- table.sortDirection ??= 'ASC'
160
-
161
- drop?.({ rows: [drugRow, hoveredRow] })
162
- }
163
- }
164
- await tick()
165
- drugRowIndex = null
166
- hoveredRowIndex = null
167
- }
168
-
169
- async function rankButtonClick(rowIndex: number, direction: 1 | -1) {
170
- const row = rows[rowIndex]
171
- const rank = getRank(row)
172
- const newRank = rank + direction
173
-
174
- if (newRank < firstRank || newRank > maxRank) {
175
- return
176
- }
177
-
178
- const otherRowIndex = rows.findIndex(row => row[rankProperty] === newRank)
179
- if (otherRowIndex === -1) {
180
- return
181
- }
182
- const otherRowRank = getRank(rows[otherRowIndex])
183
- setRank(rows[otherRowIndex], rank)
184
- setRank(rows[rowIndex], otherRowRank)
185
- normalizeRanks()
186
- drop?.({ rows: [rows[otherRowIndex], rows[rowIndex]] })
187
- }
188
-
189
- export async function withoutAnimation(callback: () => void | Promise<void>) {
190
- animate = false
191
- await callback()
192
- animate = true
193
- }
194
- </script>
195
-
196
- {#each currentPageRows as row (row[idProp])}
197
- {@const rank = getRank(row)}
198
- {@const reorderable = computeReorderability ? computeReorderability(row) : typeof row[rankProperty] === 'number'}
199
- <tr
200
- id="row-{row.id}"
201
- data-original-index={row.originalIndex}
202
- class={[classNames, computeClass(row)]}
203
- class:hovered-row-asc={table?.sortDirection === 'ASC' && hoveredRowIndex === row.originalIndex}
204
- class:hovered-row-desc={table?.sortDirection === 'DESC' && hoveredRowIndex === row.originalIndex}
205
- class:text-muted={drugRowIndex === row.originalIndex}
206
- animate:flip={{ duration: distance => (animate && distance > 5 ? 200 : 0) }}
207
- ondrop={onDrop}
208
- ondragover={event => event.preventDefault()}
209
- ondragenter={event => dragEnter(event, row)}
210
- ondragleave={dragLeave}
211
- onclick={() => rowClick?.({ row })}
212
- >
213
- {@render prepend?.({ row })}
214
- <Td property={rankProperty}>
215
- <!-- svelte-ignore a11y_no_static_element_interactions -->
216
- <div
217
- draggable={reorderable ? 'true' : 'false'}
218
- style:cursor={reorderable ? 'grab' : 'not-allowed'}
219
- class="d-none d-md-block"
220
- ondragstart={event => (reorderable ? dragStart(event, row) : undefined)}
221
- ondragend={dragEnd}
222
- >
223
- <i
224
- class="fas fa-bars"
225
- class:text-muted={!reorderable}
226
- ></i>
227
- {#if showRank}
228
- {rank}
229
- {/if}
230
- </div>
231
- <div class="btn-group d-md-none">
232
- <Button
233
- outline
234
- size="sm"
235
- iconClass="arrow-up"
236
- colorGreyDisabled={false}
237
- disabled={rank <= firstRank || !reorderable}
238
- onclick={() => rankButtonClick(row.originalIndex, -1)}
239
- />
240
- {#if showRank}
241
- <Button
242
- outline
243
- size="sm"
244
- colorGreyDisabled={false}
245
- disabled
246
- >
247
- {rank}
248
- </Button>
249
- {/if}
250
-
251
- <Button
252
- outline
253
- size="sm"
254
- iconClass="arrow-down"
255
- colorGreyDisabled={false}
256
- disabled={rank >= maxRank || !reorderable}
257
- onclick={() => rankButtonClick(row.originalIndex, 1)}
258
- />
259
- </div>
260
- {#if showRank}
261
- <span class="sr-only">{row[rankProperty]}</span>
262
- {/if}
263
- </Td>
264
- {@render children?.({ row })}
265
- {@render append?.({ row })}
266
- </tr>
267
- {/each}
268
-
269
- <style>
270
- .hovered-row-asc {
271
- border-top: 5px solid var(--dark, var(--bs-dark, black));
272
- }
273
- .hovered-row-desc {
274
- border-bottom: 5px solid var(--dark, var(--bs-dark, black));
275
- }
276
-
277
- /* Dark Mode! (BS5 Only) */
278
- :global([data-bs-theme='dark']) .hovered-row-asc {
279
- border-top: 5px solid var(--bs-light, white);
280
- }
281
-
282
- :global([data-bs-theme='dark']) .hovered-row-desc {
283
- border-bottom: 5px solid var(--bs-light, white);
284
- }
285
- </style>
1
+ <script
2
+ lang="ts"
3
+ generics="Row extends UuidRowProps"
4
+ >
5
+ import type { Snippet } from 'svelte'
6
+ import type { ClassValue } from 'svelte/elements'
7
+ import type { StringKeyOfType } from './tree-utility'
8
+
9
+ import type Table from './Table.svelte'
10
+ import type { Column, UuidRowProps } from './'
11
+ import Td from './Td.svelte'
12
+ import { flip } from 'svelte/animate'
13
+ import { getContext, tick } from 'svelte'
14
+ import Button from '@isoftdata/svelte-button'
15
+
16
+ type RankProperty = StringKeyOfType<Row, number | null>
17
+
18
+ interface Props {
19
+ /** Columns definition */
20
+ columns: Array<Column>
21
+ /** This should be the same value passed to the table's `rows` prop. You should `bind:` this so changes to the rank column propagate back to the table component.*/
22
+ rows: Array<Row>
23
+ /** This should be the same value passed to the table's `currentPageRows` prop. */
24
+ currentPageRows: Array<Row & { originalIndex: number; uuid: string }>
25
+ /** The property of the column containing the "rank" or "order" of the row */
26
+ rankProperty: RankProperty
27
+ /** The table component this component is in. */
28
+ table: Table<Row> | undefined
29
+ /** Used to discriminate what is being dragged/dropped. If you have two draggable tables on the same page, consider changing this.*/
30
+ dataType?: string
31
+ /** Whether to show the rank next to the drag target*/
32
+ showRank?: boolean
33
+ /** Any classes to apply to every `tr`*/
34
+ class?: ClassValue
35
+ /** A function that computes any classes you want to add conditionally based on the state of the current row*/
36
+ computeClass?: (row: Row) => ClassValue
37
+ /** A function that computes if a row should be enabled for reordering(drag or stepper buttons) */
38
+ computeReorderability?: ((row: Row) => boolean) | undefined
39
+ /** The rank the list starts at. Defaults to 1. */
40
+ firstRank?: number
41
+ // Snippets
42
+ prepend?: Snippet<[{ row: Row & { originalIndex: number; uuid: string } }]>
43
+ children?: Snippet<[{ row: Row & { originalIndex: number; uuid: string } }]>
44
+ append?: Snippet<[{ row: Row & { originalIndex: number; uuid: string } }]>
45
+ // Callbacks
46
+ rowClick?: (context: { row: Row & { originalIndex: number; uuid: string } }) => void
47
+ drop?: (context: { rows: [Row, Row] }) => void
48
+ }
49
+
50
+ let {
51
+ columns,
52
+ rows = $bindable(),
53
+ currentPageRows,
54
+ rankProperty,
55
+ table = $bindable(),
56
+ dataType = 'application/table-row',
57
+ showRank = false,
58
+ class: classNames = '',
59
+ computeClass = () => '',
60
+ computeReorderability = undefined,
61
+ firstRank = 1,
62
+ prepend,
63
+ children,
64
+ append,
65
+ rowClick,
66
+ drop,
67
+ }: Props = $props()
68
+
69
+ let drugRowIndex: number | null = $state(null)
70
+ let hoveredRowIndex: number | null = $state(null)
71
+ let animate = $state(true)
72
+
73
+ let maxRank = $derived(rows.length + firstRank - 1)
74
+
75
+ const idProp = getContext<'uuid' | keyof Row>('idProp')
76
+
77
+ function getRank(row: Row): number {
78
+ return row[rankProperty] as number
79
+ }
80
+
81
+ function setRank(row: Row, rank: number) {
82
+ row[rankProperty] = rank as Row[RankProperty]
83
+ }
84
+
85
+ function relatedTargetIsInTarget(relatedTarget: EventTarget | null, target: EventTarget | null): boolean {
86
+ return (
87
+ !!relatedTarget &&
88
+ !!target &&
89
+ relatedTarget instanceof Node &&
90
+ target instanceof Node &&
91
+ (relatedTarget === target || target.contains(relatedTarget))
92
+ )
93
+ }
94
+
95
+ function normalizeRanks() {
96
+ rows.sort((a, b) => getRank(a) - getRank(b))
97
+ rows.forEach((row, index) => {
98
+ setRank(row, index + firstRank)
99
+ })
100
+ rows = rows
101
+ }
102
+
103
+ function dragStart(event: DragEvent, row: Row & { originalIndex: number }) {
104
+ if (!event.dataTransfer) {
105
+ return
106
+ }
107
+ event.dataTransfer.setData(dataType, JSON.stringify(row))
108
+ // Show the whole row while dragging
109
+ if (event.target instanceof Element) {
110
+ const tr = event.target.closest('tr')
111
+ if (tr) {
112
+ event.dataTransfer.setDragImage(tr, 15, 15)
113
+ drugRowIndex = row.originalIndex
114
+ }
115
+ }
116
+ event.dataTransfer.effectAllowed = 'move'
117
+ event.dataTransfer.dropEffect = 'move'
118
+ }
119
+
120
+ function dragEnd() {
121
+ drugRowIndex = null
122
+ hoveredRowIndex = null
123
+ }
124
+
125
+ function dragEnter(event: DragEvent, row: Row & { originalIndex: number }) {
126
+ if (event.dataTransfer?.types.includes(dataType)) {
127
+ event.preventDefault()
128
+ if (event.target instanceof Element) {
129
+ const tr = event.target.closest('tr')
130
+ if (tr && !relatedTargetIsInTarget(event.relatedTarget, tr)) {
131
+ hoveredRowIndex = row.originalIndex
132
+ tr.scrollIntoView({ behavior: 'instant', block: 'nearest' })
133
+ }
134
+ }
135
+ }
136
+ }
137
+
138
+ function dragLeave(event: DragEvent) {
139
+ if (event.dataTransfer?.types.includes(dataType)) {
140
+ event.preventDefault()
141
+ }
142
+ }
143
+
144
+ async function onDrop(event: DragEvent) {
145
+ if (table && event.dataTransfer?.types.includes(dataType)) {
146
+ event.preventDefault()
147
+
148
+ const hoveredRow = typeof hoveredRowIndex === 'number' ? rows[hoveredRowIndex] : null
149
+ const drugRow = typeof drugRowIndex === 'number' ? rows[drugRowIndex] : null
150
+ if (drugRow && hoveredRow) {
151
+ // This doesn't allow us to drop something at the end of the list, maybe fix that later if it causes enough grief.
152
+ const newRank = getRank(hoveredRow)
153
+
154
+ setRank(drugRow, newRank)
155
+ setRank(hoveredRow, newRank + 1)
156
+
157
+ normalizeRanks()
158
+ table.sortColumn = columns.find(column => column.property === rankProperty)
159
+ table.sortDirection ??= 'ASC'
160
+
161
+ drop?.({ rows: [drugRow, hoveredRow] })
162
+ }
163
+ }
164
+ await tick()
165
+ drugRowIndex = null
166
+ hoveredRowIndex = null
167
+ }
168
+
169
+ async function rankButtonClick(rowIndex: number, direction: 1 | -1) {
170
+ const row = rows[rowIndex]
171
+ const rank = getRank(row)
172
+ const newRank = rank + direction
173
+
174
+ if (newRank < firstRank || newRank > maxRank) {
175
+ return
176
+ }
177
+
178
+ const otherRowIndex = rows.findIndex(row => row[rankProperty] === newRank)
179
+ if (otherRowIndex === -1) {
180
+ return
181
+ }
182
+ const otherRowRank = getRank(rows[otherRowIndex])
183
+ setRank(rows[otherRowIndex], rank)
184
+ setRank(rows[rowIndex], otherRowRank)
185
+ normalizeRanks()
186
+ drop?.({ rows: [rows[otherRowIndex], rows[rowIndex]] })
187
+ }
188
+
189
+ export async function withoutAnimation(callback: () => void | Promise<void>) {
190
+ animate = false
191
+ await callback()
192
+ animate = true
193
+ }
194
+ </script>
195
+
196
+ {#each currentPageRows as row (row[idProp])}
197
+ {@const rank = getRank(row)}
198
+ {@const reorderable = computeReorderability ? computeReorderability(row) : typeof row[rankProperty] === 'number'}
199
+ <tr
200
+ id="row-{row.id}"
201
+ data-original-index={row.originalIndex}
202
+ class={[classNames, computeClass(row)]}
203
+ class:hovered-row-asc={table?.sortDirection === 'ASC' && hoveredRowIndex === row.originalIndex}
204
+ class:hovered-row-desc={table?.sortDirection === 'DESC' && hoveredRowIndex === row.originalIndex}
205
+ class:text-muted={drugRowIndex === row.originalIndex}
206
+ animate:flip={{ duration: distance => (animate && distance > 5 ? 200 : 0) }}
207
+ ondrop={onDrop}
208
+ ondragover={event => event.preventDefault()}
209
+ ondragenter={event => dragEnter(event, row)}
210
+ ondragleave={dragLeave}
211
+ onclick={() => rowClick?.({ row })}
212
+ >
213
+ {@render prepend?.({ row })}
214
+ <Td property={rankProperty}>
215
+ <!-- svelte-ignore a11y_no_static_element_interactions -->
216
+ <div
217
+ draggable={reorderable ? 'true' : 'false'}
218
+ style:cursor={reorderable ? 'grab' : 'not-allowed'}
219
+ class="d-none d-md-block"
220
+ ondragstart={event => (reorderable ? dragStart(event, row) : undefined)}
221
+ ondragend={dragEnd}
222
+ >
223
+ <i
224
+ class="fas fa-bars"
225
+ class:text-muted={!reorderable}
226
+ ></i>
227
+ {#if showRank}
228
+ {rank}
229
+ {/if}
230
+ </div>
231
+ <div class="btn-group d-md-none">
232
+ <Button
233
+ outline
234
+ size="sm"
235
+ iconClass="arrow-up"
236
+ colorGreyDisabled={false}
237
+ disabled={rank <= firstRank || !reorderable}
238
+ onclick={() => rankButtonClick(row.originalIndex, -1)}
239
+ />
240
+ {#if showRank}
241
+ <Button
242
+ outline
243
+ size="sm"
244
+ colorGreyDisabled={false}
245
+ disabled
246
+ >
247
+ {rank}
248
+ </Button>
249
+ {/if}
250
+
251
+ <Button
252
+ outline
253
+ size="sm"
254
+ iconClass="arrow-down"
255
+ colorGreyDisabled={false}
256
+ disabled={rank >= maxRank || !reorderable}
257
+ onclick={() => rankButtonClick(row.originalIndex, 1)}
258
+ />
259
+ </div>
260
+ {#if showRank}
261
+ <span class="sr-only">{row[rankProperty]}</span>
262
+ {/if}
263
+ </Td>
264
+ {@render children?.({ row })}
265
+ {@render append?.({ row })}
266
+ </tr>
267
+ {/each}
268
+
269
+ <style>
270
+ .hovered-row-asc {
271
+ border-top: 5px solid var(--dark, var(--bs-dark, black));
272
+ }
273
+ .hovered-row-desc {
274
+ border-bottom: 5px solid var(--dark, var(--bs-dark, black));
275
+ }
276
+
277
+ /* Dark Mode! (BS5 Only) */
278
+ :global([data-bs-theme='dark']) .hovered-row-asc {
279
+ border-top: 5px solid var(--bs-light, white);
280
+ }
281
+
282
+ :global([data-bs-theme='dark']) .hovered-row-desc {
283
+ border-bottom: 5px solid var(--bs-light, white);
284
+ }
285
+ </style>