webcoreui 0.4.1 → 0.5.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.
Files changed (47) hide show
  1. package/README.md +230 -227
  2. package/astro.d.ts +6 -0
  3. package/astro.js +6 -0
  4. package/components/Avatar/Avatar.astro +9 -2
  5. package/components/Avatar/Avatar.svelte +3 -1
  6. package/components/Avatar/Avatar.tsx +4 -2
  7. package/components/Avatar/avatar.ts +1 -0
  8. package/components/Button/button.module.scss +6 -1
  9. package/components/Button/button.ts +2 -2
  10. package/components/Carousel/Carousel.astro +198 -0
  11. package/components/Carousel/Carousel.svelte +161 -0
  12. package/components/Carousel/Carousel.tsx +172 -0
  13. package/components/Carousel/carousel.module.scss +58 -0
  14. package/components/Carousel/carousel.ts +26 -0
  15. package/components/DataTable/DataTable.astro +332 -0
  16. package/components/DataTable/DataTable.svelte +272 -0
  17. package/components/DataTable/DataTable.tsx +287 -0
  18. package/components/DataTable/datatable.module.scss +102 -0
  19. package/components/DataTable/datatable.ts +41 -0
  20. package/components/Icon/map.ts +6 -0
  21. package/components/Input/input.module.scss +6 -0
  22. package/components/List/List.astro +1 -1
  23. package/components/List/List.svelte +1 -1
  24. package/components/List/List.tsx +1 -2
  25. package/components/Pagination/Pagination.astro +189 -0
  26. package/components/Pagination/Pagination.svelte +144 -0
  27. package/components/Pagination/Pagination.tsx +162 -0
  28. package/components/Pagination/pagination.module.scss +49 -0
  29. package/components/Pagination/pagination.ts +35 -0
  30. package/components/Select/Select.astro +8 -4
  31. package/components/Select/Select.svelte +15 -6
  32. package/components/Select/Select.tsx +15 -8
  33. package/components/Select/select.ts +7 -2
  34. package/components/Table/Table.svelte +1 -1
  35. package/components/Table/table.ts +1 -1
  36. package/icons/arrow-left.svg +3 -0
  37. package/icons/arrow-right.svg +3 -0
  38. package/icons/order.svg +3 -0
  39. package/icons.d.ts +3 -0
  40. package/icons.js +3 -0
  41. package/index.d.ts +6 -6
  42. package/package.json +1 -1
  43. package/react.d.ts +6 -0
  44. package/react.js +6 -0
  45. package/scss/resets.scss +27 -1
  46. package/svelte.d.ts +6 -0
  47. package/svelte.js +6 -0
@@ -0,0 +1,332 @@
1
+ ---
2
+ /* eslint-disable max-lines */
3
+ import type { DataTableProps, HeadingObject } from './datatable'
4
+
5
+ import Button from '../Button/Button.astro'
6
+ import ConditionalWrapper from '../ConditionalWrapper/ConditionalWrapper.astro'
7
+ import Input from '../Input/Input.astro'
8
+ import Pagination from '../Pagination/Pagination.astro'
9
+ import Select from '../Select/Select.astro'
10
+
11
+ import checkIcon from '../../icons/check.svg?raw'
12
+ import orderIcon from '../../icons/order.svg?raw'
13
+ import searchIcon from '../../icons/search.svg?raw'
14
+
15
+ import styles from './datatable.module.scss'
16
+
17
+ interface Props extends DataTableProps {}
18
+
19
+ const {
20
+ headings,
21
+ filterPlaceholder = 'Filter entries',
22
+ showFilterIcon,
23
+ noResultsLabel = 'No results.',
24
+ itemsPerPage,
25
+ subText,
26
+ columnToggleLabel = 'Columns',
27
+ pagination,
28
+ data,
29
+ hover,
30
+ striped,
31
+ offsetStripe,
32
+ compact,
33
+ maxHeight,
34
+ className,
35
+ id
36
+ } = Astro.props
37
+
38
+ const classes = [
39
+ styles.table,
40
+ hover && styles.hover,
41
+ striped && styles[`striped-${striped}s`],
42
+ offsetStripe && styles.offset,
43
+ compact && styles.compact,
44
+ maxHeight && styles.scroll
45
+ ]
46
+
47
+ const footerClasses = [
48
+ styles.footer,
49
+ subText && styles.between
50
+ ]
51
+
52
+ const getColumnName = (heading: HeadingObject | string | undefined) => {
53
+ if (!heading) {
54
+ return undefined
55
+ }
56
+
57
+ return ((heading as HeadingObject).name || heading as string).toLowerCase().replace(/\s/g, '')
58
+ }
59
+
60
+ const showColumnToggle = headings?.some(heading => {
61
+ return typeof heading === 'string' ? false : heading.toggleable
62
+ })
63
+
64
+ const columnToggleItems = [{
65
+ items: headings?.length ? headings
66
+ .filter(heading => typeof heading !== 'string' && heading.toggleable)
67
+ .map(heading => ({
68
+ name: (heading as HeadingObject).name,
69
+ value: (heading as HeadingObject).name.toLowerCase(),
70
+ icon: checkIcon
71
+ })) : []
72
+ }]
73
+
74
+ const columnFilterItems = headings?.filter(heading => typeof heading !== 'string')
75
+ .filter(heading => heading.filterable)
76
+ .map(heading => heading.name)
77
+ .map(heading => getColumnName(heading))
78
+
79
+ const hasPagination = data?.length && itemsPerPage
80
+ ? data.length > itemsPerPage
81
+ : false
82
+ ---
83
+
84
+ <section class={className} id={id}>
85
+ {(!!columnFilterItems?.length || showColumnToggle) && (
86
+ <div class={styles.filters}>
87
+ {!!columnFilterItems?.length && (
88
+ <Input
89
+ type="search"
90
+ placeholder={filterPlaceholder}
91
+ data-id="w-data-table-filter"
92
+ data-filter={columnFilterItems}
93
+ >
94
+ <Fragment
95
+ set:html={searchIcon}
96
+ slot={showFilterIcon ? 'default' : null}
97
+ />
98
+ </Input>
99
+ )}
100
+
101
+ {showColumnToggle && (
102
+ <Select
103
+ name={`data-table-${id || crypto.randomUUID()}`}
104
+ itemGroups={columnToggleItems}
105
+ position="bottom-end"
106
+ value={columnToggleLabel}
107
+ updateValue={false}
108
+ />
109
+ )}
110
+ </div>
111
+ )}
112
+ <div
113
+ class:list={classes}
114
+ style={maxHeight ? `max-height:${maxHeight}` : undefined}
115
+ >
116
+ <table data-items-per-page={itemsPerPage} data-page={hasPagination && 1}>
117
+ {headings?.length && (
118
+ <thead>
119
+ <tr>
120
+ {headings.map(heading => (
121
+ <th data-name={getColumnName(heading)}>
122
+ <ConditionalWrapper condition={!!(heading as HeadingObject).sortable}>
123
+ <Button theme="flat" slot="wrapper" data-id="w-data-table-sort">
124
+ children
125
+ <Fragment set:html={orderIcon} />
126
+ </Button>
127
+ {(heading as HeadingObject).name || heading}
128
+ </ConditionalWrapper>
129
+ </th>
130
+ ))}
131
+ </tr>
132
+ </thead>
133
+ )}
134
+ <tbody>
135
+ {data?.map((row, index) => (
136
+ <tr
137
+ data-page={hasPagination ? Math.ceil((index + 1) / itemsPerPage!) : undefined}
138
+ data-hidden={hasPagination && index >= itemsPerPage!}
139
+ >
140
+ {row.map((column, index) => (
141
+ <td data-name={getColumnName(headings?.[index])}>
142
+ <Fragment set:html={column} />
143
+ </td>
144
+ ))}
145
+ </tr>
146
+ ))}
147
+ <slot />
148
+ </tbody>
149
+ {!!columnFilterItems?.length && (
150
+ <tfoot data-hidden="true">
151
+ <tr>
152
+ <td colspan={data?.[0].length} class={styles['no-results']}>{noResultsLabel}</td>
153
+ </tr>
154
+ </tfoot>
155
+ )}
156
+ </table>
157
+ </div>
158
+ {(subText || hasPagination) && (
159
+ <div class:list={footerClasses}>
160
+ {subText && <span class={styles.subtext}>{subText}</span>}
161
+ {hasPagination && (
162
+ <Pagination
163
+ {...pagination}
164
+ totalPages={Math.ceil((data?.length || 0) / itemsPerPage!)}
165
+ className="w-data-table-pagination"
166
+ />
167
+ )}
168
+ </div>
169
+ )}
170
+ </section>
171
+
172
+ <script>
173
+ import { debounce } from '../../utils/debounce'
174
+ import { dispatch, listen } from '../../utils/event'
175
+
176
+ const filters = document.querySelectorAll('[data-id="w-data-table-filter"]')
177
+ const sorts = document.querySelectorAll('[data-id="w-data-table-sort"]')
178
+
179
+ Array.from(filters).forEach(filter => {
180
+ filter.addEventListener('input', debounce((event: Event) => {
181
+ const target = event.target as HTMLInputElement
182
+ const filterableColumns = target.dataset.filter
183
+ const table = target.closest('section')?.querySelector('table') as HTMLTableElement
184
+ const pagination = target.closest('section')?.querySelector('.w-data-table-pagination') as HTMLUListElement
185
+ const noResults = table.querySelector('tfoot') as HTMLElement
186
+ const tableRows = Array.from(table.querySelectorAll('tbody tr') as NodeListOf<HTMLTableRowElement>)
187
+
188
+ tableRows.forEach(row => {
189
+ const rowValue = Array.from(row.querySelectorAll('td'))
190
+ .filter(td => filterableColumns?.includes(td.dataset.name || ''))
191
+ .map(td => td.innerText)
192
+ .join('')
193
+ .toLowerCase()
194
+
195
+ if (rowValue.includes(target.value.toLowerCase())) {
196
+ row.removeAttribute('data-hidden')
197
+ } else {
198
+ row.dataset.hidden = 'true'
199
+ }
200
+ })
201
+
202
+ const filteredRows = Array.from(
203
+ table?.querySelectorAll('tbody tr:not([data-hidden])') as NodeListOf<HTMLTableRowElement>
204
+ )
205
+
206
+ if (!filteredRows.length) {
207
+ noResults.removeAttribute('data-hidden')
208
+ } else {
209
+ noResults.dataset.hidden = 'true'
210
+ }
211
+
212
+ if (table.dataset.itemsPerPage && filteredRows.length >= Number(table.dataset.itemsPerPage)) {
213
+ filteredRows.forEach((row, index) => {
214
+ if (index >= Number(table.dataset.itemsPerPage)) {
215
+ row.dataset.hidden = 'true'
216
+ }
217
+ })
218
+ }
219
+
220
+ if (target.value) {
221
+ pagination.style.display = 'none'
222
+ } else {
223
+ pagination.style.display = 'flex'
224
+
225
+ tableRows.forEach(row => {
226
+ if (row.dataset.page !== table.dataset.page) {
227
+ row.dataset.hidden = 'true'
228
+ } else {
229
+ row.removeAttribute('data-hidden')
230
+ }
231
+ })
232
+ }
233
+
234
+ dispatch('dataTableFilter', {
235
+ results: filteredRows,
236
+ numberOfResults: filteredRows.length
237
+ })
238
+ }, 400))
239
+ })
240
+
241
+ Array.from(sorts).forEach(sort => {
242
+ let sortOrder = 1
243
+
244
+ sort.addEventListener('click', event => {
245
+ const target = event.target as HTMLButtonElement
246
+ const sortBy = target.parentElement?.dataset.name
247
+ const table = target.closest('section')?.querySelector('table')
248
+ const tableBody = table?.querySelector('tbody')
249
+ const sortedTableRows = Array.from(
250
+ table?.querySelectorAll('tbody tr') as NodeListOf<HTMLTableRowElement>
251
+ ).sort((a, b) => {
252
+ let aValue: string | number = (a.querySelector(`[data-name=${sortBy}]`) as HTMLElement)
253
+ ?.innerText.replace(/\s/g, '')
254
+ let bValue: string | number = (b.querySelector(`[data-name=${sortBy}]`) as HTMLElement)
255
+ ?.innerText.replace(/\s/g, '')
256
+
257
+ if (!isNaN(aValue as any)) {
258
+ aValue = Number(aValue)
259
+ }
260
+
261
+ if (!isNaN(bValue as any)) {
262
+ bValue = Number(bValue)
263
+ }
264
+
265
+ return aValue > bValue
266
+ ? sortOrder * -1
267
+ : sortOrder
268
+ }).map((row, index) => {
269
+ if (table?.dataset.page) {
270
+ row.dataset.page = `${Math.ceil((index + 1) / Number(table.dataset.itemsPerPage))}`
271
+
272
+ if (row.dataset.page !== table.dataset.page) {
273
+ row.dataset.hidden = 'true'
274
+ } else {
275
+ row.removeAttribute('data-hidden')
276
+ }
277
+ }
278
+
279
+ return row
280
+ })
281
+
282
+ tableBody?.replaceChildren(...sortedTableRows)
283
+
284
+ sortOrder = sortOrder === 1 ? -1 : 1
285
+ })
286
+ })
287
+
288
+ listen('selectOnChange', event => {
289
+ const eventName = event.name.toLowerCase().replace(/\s/g, '')
290
+ const table = (document.querySelector(`[data-id="w-select-${event.select}"]`)
291
+ ?.closest('section') as HTMLElement)
292
+ .querySelector('table') as HTMLTableElement
293
+
294
+ const affectedTableCells = Array.from(table.querySelectorAll(`[data-name=${eventName}]`)) as HTMLElement[]
295
+
296
+ const columnToggleListElement = Array.from(event.list.children)
297
+ .find(child => (child as HTMLLIElement).dataset.name === event.name) as HTMLLIElement
298
+ const svgIcon = columnToggleListElement.children[0] as HTMLElement
299
+
300
+ svgIcon.style.opacity = svgIcon.style.opacity === '0'
301
+ ? '1'
302
+ : '0'
303
+
304
+ if (svgIcon.style.opacity === '0') {
305
+ affectedTableCells.forEach(cell => cell.dataset.hidden = 'true')
306
+ } else {
307
+ affectedTableCells.forEach(cell => cell.removeAttribute('data-hidden'))
308
+ }
309
+ })
310
+
311
+ listen('paginate', event => {
312
+ const table = event.target
313
+ .closest('section')
314
+ .querySelector('table')
315
+
316
+ if (!table) {
317
+ return
318
+ }
319
+
320
+ const tableRows = Array.from(table.querySelectorAll('tbody tr') as NodeListOf<HTMLTableRowElement>)
321
+
322
+ table.dataset.page = event.page
323
+
324
+ tableRows.forEach(row => {
325
+ if (Number(row.dataset.page) === event.page) {
326
+ row.removeAttribute('data-hidden')
327
+ } else {
328
+ row.dataset.hidden = 'true'
329
+ }
330
+ })
331
+ })
332
+ </script>
@@ -0,0 +1,272 @@
1
+ <script lang="ts">
2
+ import type { HeadingObject, SvelteDataTableProps } from './datatable'
3
+
4
+ import Button from '../Button/Button.svelte'
5
+ import Input from '../Input/Input.svelte'
6
+ import Pagination from '../Pagination/Pagination.svelte'
7
+ import Select from '../Select/Select.svelte'
8
+
9
+ import { classNames } from '../../utils/classNames'
10
+ import { debounce } from '../../utils/debounce'
11
+
12
+ import checkIcon from '../../icons/check.svg?raw'
13
+ import orderIcon from '../../icons/order.svg?raw'
14
+ import searchIcon from '../../icons/search.svg?raw'
15
+
16
+ import styles from './datatable.module.scss'
17
+
18
+ import type { ListEventType } from '../List/list'
19
+
20
+ export let headings: SvelteDataTableProps['headings'] = []
21
+ export let filterPlaceholder: SvelteDataTableProps['filterPlaceholder'] = 'Filter entries'
22
+ export let showFilterIcon: SvelteDataTableProps['showFilterIcon'] = false
23
+ export let noResultsLabel: SvelteDataTableProps['noResultsLabel'] = 'No results.'
24
+ export let itemsPerPage: SvelteDataTableProps['itemsPerPage'] = null
25
+ export let subText: SvelteDataTableProps['subText'] = ''
26
+ export let columnToggleLabel: SvelteDataTableProps['columnToggleLabel'] = 'Columns'
27
+ export let pagination: SvelteDataTableProps['pagination'] = {}
28
+ export let data: SvelteDataTableProps['data'] = []
29
+ export let hover: SvelteDataTableProps['hover'] = false
30
+ export let striped: SvelteDataTableProps['striped'] = null
31
+ export let offsetStripe: SvelteDataTableProps['offsetStripe'] = false
32
+ export let compact: SvelteDataTableProps['compact'] = false
33
+ export let maxHeight: SvelteDataTableProps['maxHeight'] = ''
34
+ export let className: SvelteDataTableProps['className'] = ''
35
+ export let id: SvelteDataTableProps['id'] = ''
36
+ export let onFilter: SvelteDataTableProps['onFilter'] = () => {}
37
+
38
+ let filteredData: any = data
39
+ let toggledData: any = filteredData
40
+ let filteredHeadings: any = headings
41
+ let page: number = 1
42
+ let hasActiveFilter: boolean = false
43
+ let sortOrder = 1
44
+
45
+ const classes = classNames([
46
+ styles.table,
47
+ hover && styles.hover,
48
+ striped && styles[`striped-${striped}s`],
49
+ offsetStripe && styles.offset,
50
+ compact && styles.compact,
51
+ maxHeight && styles.scroll
52
+ ])
53
+
54
+ const footerClasses = classNames([
55
+ styles.footer,
56
+ subText && styles.between
57
+ ])
58
+
59
+ const showColumnToggle = headings?.some(heading => {
60
+ return typeof heading === 'string' ? false : heading.toggleable
61
+ })
62
+
63
+ const columnToggleItems = [{
64
+ items: headings?.length ? headings
65
+ .filter(heading => typeof heading !== 'string' && heading.toggleable)
66
+ .map(heading => ({
67
+ icon: checkIcon,
68
+ name: (heading as HeadingObject).name,
69
+ value: String(headings.findIndex(h => {
70
+ return (h as HeadingObject).name === (heading as HeadingObject).name
71
+ }))
72
+ })) : []
73
+ }]
74
+
75
+ const columnFilterIndexes = headings?.map(heading => (heading as HeadingObject).filterable)
76
+ .map((heading, index) => heading ? index : null)
77
+ .filter(heading => heading !== null) || []
78
+
79
+ const hasPagination = data?.length && itemsPerPage
80
+ ? data.length > itemsPerPage
81
+ : false
82
+
83
+ const filter = debounce((event: Event) => {
84
+ const target = event.target as HTMLInputElement
85
+
86
+ hasActiveFilter = !!target.value
87
+
88
+ filteredData = toggledData?.filter((row: string[]) => {
89
+ const rowValue = row.filter((_, index) => columnFilterIndexes.includes(index))
90
+ .join('')
91
+ .toLowerCase()
92
+
93
+ return rowValue.includes(target.value.toLowerCase())
94
+ })
95
+
96
+ onFilter?.({
97
+ results: filteredData,
98
+ numberOfResults: filteredData.length
99
+ })
100
+ }, 400)
101
+
102
+ const toggleColumns = (event: ListEventType) => {
103
+ const columnToggleListElement = Array.from(event.list.children)
104
+ .find(child => (child as HTMLLIElement).dataset.name === event.name) as HTMLLIElement
105
+ const svgIcon = columnToggleListElement.children[0] as HTMLElement
106
+
107
+ svgIcon.style.opacity = svgIcon.style.opacity === '0'
108
+ ? '1'
109
+ : '0'
110
+
111
+ if (svgIcon.style.opacity === '0') {
112
+ filteredData = (hasActiveFilter ? data : filteredData)?.map((row: string[]) => {
113
+ return row.map((column, index) => index === Number(event.value) ? null : column)
114
+ })
115
+
116
+ filteredHeadings = filteredHeadings.map((heading: HeadingObject | string) => {
117
+ return ((heading as HeadingObject)?.name || heading) === event.name ? null : heading
118
+ })
119
+ } else {
120
+ filteredData = (hasActiveFilter ? data : filteredData)?.map((row: string[], x: number) => {
121
+ return row.map((column, y) => y === Number(event.value) ? data?.[x][y] : column)
122
+ })
123
+
124
+ filteredHeadings = filteredHeadings.map((heading: HeadingObject | string, index: number) => {
125
+ return ((headings?.[index] as HeadingObject)?.name || headings?.[index]) === event.name
126
+ ? headings?.[index]
127
+ : heading
128
+ })
129
+ }
130
+
131
+ hasActiveFilter = false
132
+ toggledData = filteredData
133
+ }
134
+
135
+ const sort = (index: number) => {
136
+ filteredData = filteredData.sort((a: string[], b: string[]) => {
137
+ let aValue: string | number = a[index]
138
+ let bValue: string | number = b[index]
139
+
140
+ if (!isNaN(aValue as any)) {
141
+ aValue = Number(aValue)
142
+ }
143
+
144
+ if (!isNaN(bValue as any)) {
145
+ bValue = Number(bValue)
146
+ }
147
+
148
+ return aValue > bValue
149
+ ? sortOrder * -1
150
+ : sortOrder
151
+ })
152
+
153
+ sortOrder = sortOrder === 1 ? -1 : 1
154
+ }
155
+
156
+ $: isNextPage = (index: number) => {
157
+ if (hasPagination && itemsPerPage && !hasActiveFilter) {
158
+ const currentPage = Math.ceil((index + 1) / itemsPerPage)
159
+
160
+ return currentPage !== page ? 'true' : undefined
161
+ }
162
+
163
+ if (hasActiveFilter && itemsPerPage) {
164
+ return index >= itemsPerPage ? 'true' : undefined
165
+ }
166
+
167
+ return undefined
168
+ }
169
+ </script>
170
+
171
+ <section class={className || null} id={id || null}>
172
+ {#if columnFilterIndexes?.length || showColumnToggle}
173
+ <div class={styles.filters}>
174
+ {#if columnFilterIndexes?.length}
175
+ {#if showFilterIcon}
176
+ <Input
177
+ type="search"
178
+ placeholder={filterPlaceholder}
179
+ onInput={filter}
180
+ >
181
+ {@html searchIcon}
182
+ </Input>
183
+ {:else}
184
+ <Input
185
+ type="search"
186
+ placeholder={filterPlaceholder}
187
+ onInput={filter}
188
+ />
189
+ {/if}
190
+ {/if}
191
+ {#if showColumnToggle}
192
+ <Select
193
+ name={`data-table-${id || crypto.randomUUID()}`}
194
+ itemGroups={columnToggleItems}
195
+ position="bottom-end"
196
+ value={columnToggleLabel}
197
+ onChange={toggleColumns}
198
+ updateValue={false}
199
+ />
200
+ {/if}
201
+ </div>
202
+ {/if}
203
+
204
+ <div
205
+ class={classes}
206
+ style={maxHeight ? `max-height:${maxHeight}` : undefined}
207
+ >
208
+ <table>
209
+ {#if filteredHeadings?.length}
210
+ <thead>
211
+ <tr>
212
+ {#each filteredHeadings as heading, index}
213
+ {#if heading}
214
+ <th>
215
+ {#if heading.sortable}
216
+ <Button theme="flat" slot="wrapper" onClick={() => sort(index)}>
217
+ {heading.name || heading}
218
+ {@html orderIcon}
219
+ </Button>
220
+ {:else}
221
+ {heading.name || heading}
222
+ {/if}
223
+ </th>
224
+ {/if}
225
+ {/each}
226
+ </tr>
227
+ </thead>
228
+ {/if}
229
+
230
+ <tbody>
231
+ {#if filteredData?.length}
232
+ {#each filteredData as row, index}
233
+ <tr data-hidden={isNextPage(index)}>
234
+ {#each row as column}
235
+ {#if column}
236
+ <td>
237
+ {@html column}
238
+ </td>
239
+ {/if}
240
+ {/each}
241
+ </tr>
242
+ {/each}
243
+ {/if}
244
+ <slot />
245
+ </tbody>
246
+ {#if columnFilterIndexes?.length && !filteredData.length}
247
+ <tfoot>
248
+ <tr>
249
+ <td colspan={data?.[0].length} class={styles['no-results']}>
250
+ {noResultsLabel}
251
+ </td>
252
+ </tr>
253
+ </tfoot>
254
+ {/if}
255
+ </table>
256
+ </div>
257
+ {#if subText || hasPagination}
258
+ <div class={footerClasses}>
259
+ {#if subText}
260
+ <span class={styles.subtext}>{subText}</span>
261
+ {/if}
262
+ {#if hasPagination && itemsPerPage && !hasActiveFilter}
263
+ <Pagination
264
+ {...pagination}
265
+ totalPages={Math.ceil((data?.length || 0) / itemsPerPage)}
266
+ currentPage={page}
267
+ onChange={event => page = event.page}
268
+ />
269
+ {/if}
270
+ </div>
271
+ {/if}
272
+ </section>