webcoreui 0.6.0 → 0.6.1
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/components/Button/button.ts +1 -1
- package/components/Carousel/Carousel.astro +16 -7
- package/components/Carousel/Carousel.svelte +15 -5
- package/components/Carousel/Carousel.tsx +15 -5
- package/components/Carousel/carousel.module.scss +4 -1
- package/components/Carousel/carousel.ts +1 -1
- package/components/DataTable/DataTable.astro +2 -2
- package/components/DataTable/DataTable.tsx +287 -287
- package/components/Pagination/Pagination.astro +2 -0
- package/components/Pagination/Pagination.svelte +6 -3
- package/components/Pagination/Pagination.tsx +5 -3
- package/components/Pagination/pagination.ts +1 -0
- package/index.d.ts +8 -0
- package/index.js +1 -0
- package/package.json +2 -1
- package/scss/config/typography.scss +3 -1
- package/scss/global/utility.scss +1 -1
- package/utils/DOMUtils.ts +28 -0
- package/scss/global/elements.scss +0 -31
|
@@ -13,7 +13,7 @@ interface Props extends CarouselProps {}
|
|
|
13
13
|
|
|
14
14
|
const {
|
|
15
15
|
items,
|
|
16
|
-
|
|
16
|
+
itemsPerSlide = 1,
|
|
17
17
|
subText,
|
|
18
18
|
scrollSnap = true,
|
|
19
19
|
progress,
|
|
@@ -38,6 +38,7 @@ const containerClasses = [
|
|
|
38
38
|
const wrapperClasses = [
|
|
39
39
|
styles.wrapper,
|
|
40
40
|
effect && styles[effect],
|
|
41
|
+
itemsPerSlide > 1 && styles['no-snap'],
|
|
41
42
|
wrapperClassName
|
|
42
43
|
]
|
|
43
44
|
|
|
@@ -51,10 +52,10 @@ const paginationClasses = classNames([
|
|
|
51
52
|
!subText && paginationClassName
|
|
52
53
|
])
|
|
53
54
|
|
|
54
|
-
const totalPages = Math.ceil(items /
|
|
55
|
+
const totalPages = Math.ceil(items / itemsPerSlide)
|
|
55
56
|
const subTextValue = subText?.match(/\{0\}|\{1\}/g) ? subText : undefined
|
|
56
|
-
const style =
|
|
57
|
-
? `--w-slide-width: ${100 /
|
|
57
|
+
const style = itemsPerSlide > 1
|
|
58
|
+
? `--w-slide-width: calc(${100 / itemsPerSlide}% - 5px);`
|
|
58
59
|
: null
|
|
59
60
|
---
|
|
60
61
|
|
|
@@ -67,7 +68,7 @@ const style = visibleItems > 1
|
|
|
67
68
|
<ul
|
|
68
69
|
class:list={wrapperClasses}
|
|
69
70
|
style={style}
|
|
70
|
-
data-visible-items={
|
|
71
|
+
data-visible-items={itemsPerSlide > 1 ? itemsPerSlide : null}
|
|
71
72
|
>
|
|
72
73
|
<slot />
|
|
73
74
|
</ul>
|
|
@@ -159,8 +160,16 @@ const style = visibleItems > 1
|
|
|
159
160
|
|
|
160
161
|
const progress = target.closest('section').querySelector('.w-carousel-progress')
|
|
161
162
|
const progressValue = (100 / (Number(target.dataset.totalPages) - 1))
|
|
162
|
-
const
|
|
163
|
-
const
|
|
163
|
+
const itemsPerSlide = Number(carousel.dataset.visibleItems) || 1
|
|
164
|
+
const totalItems = carousel.children.length
|
|
165
|
+
const indexes = Array.from({ length: Math.ceil(totalItems / itemsPerSlide) }, (_, i) => {
|
|
166
|
+
return Array.from({ length: itemsPerSlide }, (_, j) => (i * itemsPerSlide) + j)
|
|
167
|
+
.filter(index => index < totalItems)
|
|
168
|
+
})
|
|
169
|
+
|
|
170
|
+
const pageIndex = event.direction === 'prev'
|
|
171
|
+
? indexes[event.page - 1][0]
|
|
172
|
+
: indexes[event.page - 1][indexes[event.page - 1].length - 1]
|
|
164
173
|
|
|
165
174
|
const liElement = carousel.children[pageIndex]
|
|
166
175
|
const subText = event.target.nextElementSibling
|
|
@@ -14,7 +14,7 @@
|
|
|
14
14
|
import type { PaginationEventType } from '../Pagination/pagination'
|
|
15
15
|
|
|
16
16
|
export let items: SvelteCarouselProps['items'] = 0
|
|
17
|
-
export let
|
|
17
|
+
export let itemsPerSlide: SvelteCarouselProps['itemsPerSlide'] = 1
|
|
18
18
|
export let subText: SvelteCarouselProps['subText'] = ''
|
|
19
19
|
export let scrollSnap: SvelteCarouselProps['scrollSnap'] = true
|
|
20
20
|
export let progress: SvelteCarouselProps['progress'] = false
|
|
@@ -46,6 +46,7 @@
|
|
|
46
46
|
const wrapperClasses = classNames([
|
|
47
47
|
styles.wrapper,
|
|
48
48
|
effect && styles[effect],
|
|
49
|
+
itemsPerSlide! > 1 && styles['no-snap'],
|
|
49
50
|
wrapperClassName
|
|
50
51
|
])
|
|
51
52
|
|
|
@@ -59,10 +60,10 @@
|
|
|
59
60
|
!subText && paginationClassName
|
|
60
61
|
])
|
|
61
62
|
|
|
62
|
-
const totalPages = Math.ceil(items /
|
|
63
|
+
const totalPages = Math.ceil(items / itemsPerSlide!)
|
|
63
64
|
const subTextValue = subText?.match(/\{0\}|\{1\}/g) ? subText : undefined
|
|
64
|
-
const style =
|
|
65
|
-
? `--w-slide-width: ${100 /
|
|
65
|
+
const style = itemsPerSlide! > 1
|
|
66
|
+
? `--w-slide-width: calc(${100 / itemsPerSlide!}% - 5px);`
|
|
66
67
|
: null
|
|
67
68
|
|
|
68
69
|
const updateValues = () => {
|
|
@@ -100,7 +101,16 @@
|
|
|
100
101
|
}, debounce)
|
|
101
102
|
|
|
102
103
|
const paginate = (event: PaginationEventType) => {
|
|
103
|
-
const
|
|
104
|
+
const indexes = Array.from({ length: Math.ceil(items / itemsPerSlide!) }, (_, i) => {
|
|
105
|
+
return Array.from({ length: itemsPerSlide! }, (_, j) => (i * itemsPerSlide!) + j)
|
|
106
|
+
.filter(index => index < items)
|
|
107
|
+
})
|
|
108
|
+
|
|
109
|
+
const pageIndex = event.direction === 'prev'
|
|
110
|
+
? indexes[event.page - 1][0]
|
|
111
|
+
: indexes[event.page - 1][indexes[event.page - 1].length - 1]
|
|
112
|
+
|
|
113
|
+
const liElement = carouselItems[pageIndex] as HTMLLIElement
|
|
104
114
|
|
|
105
115
|
liElement.scrollIntoView({ behavior: 'smooth', block: 'nearest' })
|
|
106
116
|
|
|
@@ -14,7 +14,7 @@ import type { PaginationEventType } from '../Pagination/pagination'
|
|
|
14
14
|
|
|
15
15
|
const Carousel = ({
|
|
16
16
|
items,
|
|
17
|
-
|
|
17
|
+
itemsPerSlide = 1,
|
|
18
18
|
subText,
|
|
19
19
|
scrollSnap = true,
|
|
20
20
|
progress,
|
|
@@ -49,6 +49,7 @@ const Carousel = ({
|
|
|
49
49
|
const wrapperClasses = classNames([
|
|
50
50
|
styles.wrapper,
|
|
51
51
|
effect && styles[effect],
|
|
52
|
+
itemsPerSlide! > 1 && styles['no-snap'],
|
|
52
53
|
wrapperClassName
|
|
53
54
|
])
|
|
54
55
|
|
|
@@ -62,10 +63,10 @@ const Carousel = ({
|
|
|
62
63
|
!subText && paginationClassName
|
|
63
64
|
])
|
|
64
65
|
|
|
65
|
-
const totalPages = Math.ceil(items /
|
|
66
|
+
const totalPages = Math.ceil(items / itemsPerSlide!)
|
|
66
67
|
const subTextValue = subText?.match(/\{0\}|\{1\}/g) ? subText : undefined
|
|
67
|
-
const style =
|
|
68
|
-
? { '--w-slide-width':
|
|
68
|
+
const style = itemsPerSlide > 1
|
|
69
|
+
? { '--w-slide-width': `calc(${100 / itemsPerSlide!}% - 5px);` } as React.CSSProperties
|
|
69
70
|
: undefined
|
|
70
71
|
|
|
71
72
|
const updateValues = (page: number) => {
|
|
@@ -105,7 +106,16 @@ const Carousel = ({
|
|
|
105
106
|
}, debounce)
|
|
106
107
|
|
|
107
108
|
const paginate = (event: PaginationEventType) => {
|
|
108
|
-
const
|
|
109
|
+
const indexes = Array.from({ length: Math.ceil(items / itemsPerSlide!) }, (_, i) => {
|
|
110
|
+
return Array.from({ length: itemsPerSlide! }, (_, j) => (i * itemsPerSlide!) + j)
|
|
111
|
+
.filter(index => index < items)
|
|
112
|
+
})
|
|
113
|
+
|
|
114
|
+
const pageIndex = event.direction === 'prev'
|
|
115
|
+
? indexes[event.page - 1][0]
|
|
116
|
+
: indexes[event.page - 1][indexes[event.page - 1].length - 1]
|
|
117
|
+
|
|
118
|
+
const liElement = carouselItems.current[pageIndex]
|
|
109
119
|
|
|
110
120
|
liElement.scrollIntoView({ behavior: 'smooth', block: 'nearest' })
|
|
111
121
|
|
|
@@ -34,12 +34,15 @@ body {
|
|
|
34
34
|
filter: saturate(0);
|
|
35
35
|
}
|
|
36
36
|
|
|
37
|
+
&:not(.no-snap) li {
|
|
38
|
+
scroll-snap-align: center;
|
|
39
|
+
}
|
|
40
|
+
|
|
37
41
|
li {
|
|
38
42
|
@include transition();
|
|
39
43
|
@include spacing(m0);
|
|
40
44
|
@include layout(flex, h-center);
|
|
41
45
|
|
|
42
|
-
scroll-snap-align: center;
|
|
43
46
|
min-width: var(--w-slide-width);
|
|
44
47
|
}
|
|
45
48
|
}
|
|
@@ -72,8 +72,8 @@ const columnToggleItems = [{
|
|
|
72
72
|
}]
|
|
73
73
|
|
|
74
74
|
const columnFilterItems = headings?.filter(heading => typeof heading !== 'string')
|
|
75
|
-
.filter(heading => heading.filterable)
|
|
76
|
-
.map(heading => heading.name)
|
|
75
|
+
.filter(heading => (heading as HeadingObject).filterable)
|
|
76
|
+
.map(heading => (heading as HeadingObject).name)
|
|
77
77
|
.map(heading => getColumnName(heading))
|
|
78
78
|
|
|
79
79
|
const hasPagination = data?.length && itemsPerPage
|
|
@@ -1,287 +1,287 @@
|
|
|
1
|
-
import React, { useState } from 'react'
|
|
2
|
-
import type { HeadingObject, ReactDataTableProps } from './datatable'
|
|
3
|
-
|
|
4
|
-
import Button from '../Button/Button.tsx'
|
|
5
|
-
import ConditionalWrapper from '../ConditionalWrapper/ConditionalWrapper.tsx'
|
|
6
|
-
import Input from '../Input/Input.tsx'
|
|
7
|
-
import Pagination from '../Pagination/Pagination.tsx'
|
|
8
|
-
import Select from '../Select/Select.tsx'
|
|
9
|
-
|
|
10
|
-
import { classNames } from '../../utils/classNames'
|
|
11
|
-
import { debounce } from '../../utils/debounce'
|
|
12
|
-
|
|
13
|
-
import checkIcon from '../../icons/check.svg?raw'
|
|
14
|
-
import orderIcon from '../../icons/order.svg?raw'
|
|
15
|
-
import searchIcon from '../../icons/search.svg?raw'
|
|
16
|
-
|
|
17
|
-
import styles from './datatable.module.scss'
|
|
18
|
-
|
|
19
|
-
import type { ListEventType } from '../List/list'
|
|
20
|
-
|
|
21
|
-
// eslint-disable-next-line complexity
|
|
22
|
-
const DataTable = ({
|
|
23
|
-
headings,
|
|
24
|
-
filterPlaceholder = 'Filter entries',
|
|
25
|
-
showFilterIcon,
|
|
26
|
-
noResultsLabel = 'No results.',
|
|
27
|
-
itemsPerPage,
|
|
28
|
-
subText,
|
|
29
|
-
columnToggleLabel = 'Columns',
|
|
30
|
-
pagination,
|
|
31
|
-
data,
|
|
32
|
-
hover,
|
|
33
|
-
striped,
|
|
34
|
-
offsetStripe,
|
|
35
|
-
compact,
|
|
36
|
-
maxHeight,
|
|
37
|
-
className,
|
|
38
|
-
id,
|
|
39
|
-
onFilter,
|
|
40
|
-
children
|
|
41
|
-
}: ReactDataTableProps) => {
|
|
42
|
-
const [filteredData, setFilteredData] = useState<any>(data)
|
|
43
|
-
const [toggledData, setToggledData] = useState(filteredData)
|
|
44
|
-
const [filteredHeadings, setFilteredHeadings] = useState<any>(headings)
|
|
45
|
-
const [page, setPage] = useState(1)
|
|
46
|
-
const [hasActiveFilter, setHasActiveFilter] = useState(false)
|
|
47
|
-
const [sortOrder, setSortOrder] = useState(1)
|
|
48
|
-
|
|
49
|
-
const classes = classNames([
|
|
50
|
-
styles.table,
|
|
51
|
-
hover && styles.hover,
|
|
52
|
-
striped && styles[`striped-${striped}s`],
|
|
53
|
-
offsetStripe && styles.offset,
|
|
54
|
-
compact && styles.compact,
|
|
55
|
-
maxHeight && styles.scroll
|
|
56
|
-
])
|
|
57
|
-
|
|
58
|
-
const footerClasses = classNames([
|
|
59
|
-
styles.footer,
|
|
60
|
-
subText && styles.between
|
|
61
|
-
])
|
|
62
|
-
|
|
63
|
-
const styleVariables = {
|
|
64
|
-
...(maxHeight && {
|
|
65
|
-
} as React.CSSProperties
|
|
66
|
-
|
|
67
|
-
const showColumnToggle = headings?.some(heading => {
|
|
68
|
-
return typeof heading === 'string' ? false : heading.toggleable
|
|
69
|
-
})
|
|
70
|
-
|
|
71
|
-
const columnToggleItems = [{
|
|
72
|
-
items: headings?.length ? headings
|
|
73
|
-
.filter(heading => typeof heading !== 'string' && heading.toggleable)
|
|
74
|
-
.map(heading => ({
|
|
75
|
-
icon: checkIcon,
|
|
76
|
-
name: (heading as HeadingObject).name,
|
|
77
|
-
value: String(headings.findIndex(h => {
|
|
78
|
-
return (h as HeadingObject).name === (heading as HeadingObject).name
|
|
79
|
-
}))
|
|
80
|
-
})) : []
|
|
81
|
-
}]
|
|
82
|
-
|
|
83
|
-
const columnFilterIndexes = headings?.map(heading => (heading as HeadingObject).filterable)
|
|
84
|
-
.map((heading, index) => heading ? index : null)
|
|
85
|
-
.filter(heading => heading !== null) || []
|
|
86
|
-
|
|
87
|
-
const hasPagination = data?.length && itemsPerPage
|
|
88
|
-
? data.length > itemsPerPage
|
|
89
|
-
: false
|
|
90
|
-
|
|
91
|
-
const filter = debounce((event: Event) => {
|
|
92
|
-
const target = event.target as HTMLInputElement
|
|
93
|
-
|
|
94
|
-
setHasActiveFilter(!!target.value)
|
|
95
|
-
|
|
96
|
-
setFilteredData(toggledData?.filter((row: string[]) => {
|
|
97
|
-
const rowValue = row.filter((_, index) => columnFilterIndexes.includes(index))
|
|
98
|
-
.join('')
|
|
99
|
-
.toLowerCase()
|
|
100
|
-
|
|
101
|
-
return rowValue.includes(target.value.toLowerCase())
|
|
102
|
-
}))
|
|
103
|
-
|
|
104
|
-
onFilter?.({
|
|
105
|
-
results: filteredData,
|
|
106
|
-
numberOfResults: filteredData.length
|
|
107
|
-
})
|
|
108
|
-
}, 400)
|
|
109
|
-
|
|
110
|
-
const toggleColumns = (event: ListEventType) => {
|
|
111
|
-
const columnToggleListElement = Array.from(event.list.children)
|
|
112
|
-
.find(child => (child as HTMLLIElement).dataset.name === event.name) as HTMLLIElement
|
|
113
|
-
const svgIcon = columnToggleListElement.children[0] as HTMLElement
|
|
114
|
-
let mappedData
|
|
115
|
-
|
|
116
|
-
svgIcon.style.opacity = svgIcon.style.opacity === '0'
|
|
117
|
-
? '1'
|
|
118
|
-
: '0'
|
|
119
|
-
|
|
120
|
-
if (svgIcon.style.opacity === '0') {
|
|
121
|
-
mappedData = (hasActiveFilter ? data : filteredData)?.map((row: string[]) => {
|
|
122
|
-
return row.map((column, index) => index === Number(event.value) ? null : column)
|
|
123
|
-
})
|
|
124
|
-
|
|
125
|
-
setFilteredData(mappedData)
|
|
126
|
-
|
|
127
|
-
setFilteredHeadings(filteredHeadings.map((heading: HeadingObject | string) => {
|
|
128
|
-
return ((heading as HeadingObject)?.name || heading) === event.name ? null : heading
|
|
129
|
-
}))
|
|
130
|
-
} else {
|
|
131
|
-
mappedData = (hasActiveFilter ? data : filteredData)?.map((row: string[], x: number) => {
|
|
132
|
-
return row.map((column, y) => y === Number(event.value) ? data?.[x][y] : column)
|
|
133
|
-
})
|
|
134
|
-
|
|
135
|
-
setFilteredData(mappedData)
|
|
136
|
-
|
|
137
|
-
setFilteredHeadings(filteredHeadings.map((heading: HeadingObject | string, index: number) => {
|
|
138
|
-
return ((headings?.[index] as HeadingObject)?.name || headings?.[index]) === event.name
|
|
139
|
-
? headings?.[index]
|
|
140
|
-
: heading
|
|
141
|
-
}))
|
|
142
|
-
}
|
|
143
|
-
|
|
144
|
-
setToggledData(mappedData)
|
|
145
|
-
}
|
|
146
|
-
|
|
147
|
-
const sort = (index: number) => {
|
|
148
|
-
const sortedData = filteredData.sort((a: string[], b: string[]) => {
|
|
149
|
-
let aValue: string | number = a[index]
|
|
150
|
-
let bValue: string | number = b[index]
|
|
151
|
-
|
|
152
|
-
if (!isNaN(aValue as any)) {
|
|
153
|
-
aValue = Number(aValue)
|
|
154
|
-
}
|
|
155
|
-
|
|
156
|
-
if (!isNaN(bValue as any)) {
|
|
157
|
-
bValue = Number(bValue)
|
|
158
|
-
}
|
|
159
|
-
|
|
160
|
-
return aValue > bValue
|
|
161
|
-
? sortOrder * -1
|
|
162
|
-
: sortOrder
|
|
163
|
-
})
|
|
164
|
-
|
|
165
|
-
setFilteredData(sortedData)
|
|
166
|
-
setSortOrder(sortOrder === 1 ? -1 : 1)
|
|
167
|
-
}
|
|
168
|
-
|
|
169
|
-
const isNextPage = (index: number) => {
|
|
170
|
-
if (hasPagination && itemsPerPage && !hasActiveFilter) {
|
|
171
|
-
const currentPage = Math.ceil((index + 1) / itemsPerPage)
|
|
172
|
-
|
|
173
|
-
return currentPage !== page ? 'true' : undefined
|
|
174
|
-
}
|
|
175
|
-
|
|
176
|
-
if (hasActiveFilter && itemsPerPage) {
|
|
177
|
-
return index >= itemsPerPage ? 'true' : undefined
|
|
178
|
-
}
|
|
179
|
-
|
|
180
|
-
return undefined
|
|
181
|
-
}
|
|
182
|
-
|
|
183
|
-
return (
|
|
184
|
-
<section className={className} id={id}>
|
|
185
|
-
{(!!columnFilterIndexes?.length || showColumnToggle) && (
|
|
186
|
-
<div className={styles.filters}>
|
|
187
|
-
{!!columnFilterIndexes?.length && (
|
|
188
|
-
<Input
|
|
189
|
-
type="search"
|
|
190
|
-
placeholder={filterPlaceholder}
|
|
191
|
-
onInput={filter}
|
|
192
|
-
>
|
|
193
|
-
{showFilterIcon && (
|
|
194
|
-
<span dangerouslySetInnerHTML={{ __html: searchIcon }} />
|
|
195
|
-
)}
|
|
196
|
-
</Input>
|
|
197
|
-
)}
|
|
198
|
-
{showColumnToggle && (
|
|
199
|
-
<Select
|
|
200
|
-
name={`data-table-${id || crypto.randomUUID()}`}
|
|
201
|
-
itemGroups={columnToggleItems}
|
|
202
|
-
position="bottom-end"
|
|
203
|
-
value={columnToggleLabel}
|
|
204
|
-
onChange={toggleColumns}
|
|
205
|
-
updateValue={false}
|
|
206
|
-
/>
|
|
207
|
-
)}
|
|
208
|
-
</div>
|
|
209
|
-
)}
|
|
210
|
-
|
|
211
|
-
<div className={classes} style={styleVariables}>
|
|
212
|
-
<table>
|
|
213
|
-
{!!filteredHeadings?.length && (
|
|
214
|
-
<thead>
|
|
215
|
-
<tr>
|
|
216
|
-
{filteredHeadings?.map((heading: HeadingObject | string, index: number) => {
|
|
217
|
-
if (!heading) {
|
|
218
|
-
return null
|
|
219
|
-
}
|
|
220
|
-
|
|
221
|
-
return (
|
|
222
|
-
<th key={index}>
|
|
223
|
-
<ConditionalWrapper
|
|
224
|
-
condition={!!(heading as HeadingObject).sortable}
|
|
225
|
-
wrapper={children => (
|
|
226
|
-
<Button theme="flat" slot="wrapper" onClick={() => sort(index)}>
|
|
227
|
-
{children}
|
|
228
|
-
<span dangerouslySetInnerHTML={{ __html: orderIcon }} />
|
|
229
|
-
</Button>
|
|
230
|
-
)}
|
|
231
|
-
>
|
|
232
|
-
{(heading as HeadingObject).name || heading as string}
|
|
233
|
-
</ConditionalWrapper>
|
|
234
|
-
</th>
|
|
235
|
-
)
|
|
236
|
-
})}
|
|
237
|
-
</tr>
|
|
238
|
-
</thead>
|
|
239
|
-
)}
|
|
240
|
-
|
|
241
|
-
<tbody>
|
|
242
|
-
{filteredData?.map((row: string[], rowIndex: number) => (
|
|
243
|
-
<tr key={rowIndex} data-hidden={isNextPage(rowIndex)}>
|
|
244
|
-
{row.filter(Boolean).map((column, columnIndex) => (
|
|
245
|
-
<td
|
|
246
|
-
key={columnIndex}
|
|
247
|
-
dangerouslySetInnerHTML={{ __html: column }}
|
|
248
|
-
/>
|
|
249
|
-
))}
|
|
250
|
-
</tr>
|
|
251
|
-
))}
|
|
252
|
-
{children}
|
|
253
|
-
</tbody>
|
|
254
|
-
{!filteredData?.length && (
|
|
255
|
-
<tfoot>
|
|
256
|
-
<tr>
|
|
257
|
-
<td
|
|
258
|
-
colSpan={data?.[0].length}
|
|
259
|
-
className={styles['no-results']}
|
|
260
|
-
>
|
|
261
|
-
{noResultsLabel}
|
|
262
|
-
</td>
|
|
263
|
-
</tr>
|
|
264
|
-
</tfoot>
|
|
265
|
-
)}
|
|
266
|
-
</table>
|
|
267
|
-
</div>
|
|
268
|
-
{(subText || hasPagination) && (
|
|
269
|
-
<div className={footerClasses}>
|
|
270
|
-
{subText && (
|
|
271
|
-
<span className={styles.subtext}>{subText}</span>
|
|
272
|
-
)}
|
|
273
|
-
{(hasPagination && itemsPerPage && !hasActiveFilter) && (
|
|
274
|
-
<Pagination
|
|
275
|
-
{...pagination}
|
|
276
|
-
totalPages={Math.ceil((data?.length || 0) / itemsPerPage)}
|
|
277
|
-
currentPage={page}
|
|
278
|
-
onChange={event => setPage(event.page)}
|
|
279
|
-
/>
|
|
280
|
-
)}
|
|
281
|
-
</div>
|
|
282
|
-
)}
|
|
283
|
-
</section>
|
|
284
|
-
)
|
|
285
|
-
}
|
|
286
|
-
|
|
287
|
-
export default DataTable
|
|
1
|
+
import React, { useState } from 'react'
|
|
2
|
+
import type { HeadingObject, ReactDataTableProps } from './datatable'
|
|
3
|
+
|
|
4
|
+
import Button from '../Button/Button.tsx'
|
|
5
|
+
import ConditionalWrapper from '../ConditionalWrapper/ConditionalWrapper.tsx'
|
|
6
|
+
import Input from '../Input/Input.tsx'
|
|
7
|
+
import Pagination from '../Pagination/Pagination.tsx'
|
|
8
|
+
import Select from '../Select/Select.tsx'
|
|
9
|
+
|
|
10
|
+
import { classNames } from '../../utils/classNames'
|
|
11
|
+
import { debounce } from '../../utils/debounce'
|
|
12
|
+
|
|
13
|
+
import checkIcon from '../../icons/check.svg?raw'
|
|
14
|
+
import orderIcon from '../../icons/order.svg?raw'
|
|
15
|
+
import searchIcon from '../../icons/search.svg?raw'
|
|
16
|
+
|
|
17
|
+
import styles from './datatable.module.scss'
|
|
18
|
+
|
|
19
|
+
import type { ListEventType } from '../List/list'
|
|
20
|
+
|
|
21
|
+
// eslint-disable-next-line complexity
|
|
22
|
+
const DataTable = ({
|
|
23
|
+
headings,
|
|
24
|
+
filterPlaceholder = 'Filter entries',
|
|
25
|
+
showFilterIcon,
|
|
26
|
+
noResultsLabel = 'No results.',
|
|
27
|
+
itemsPerPage,
|
|
28
|
+
subText,
|
|
29
|
+
columnToggleLabel = 'Columns',
|
|
30
|
+
pagination,
|
|
31
|
+
data,
|
|
32
|
+
hover,
|
|
33
|
+
striped,
|
|
34
|
+
offsetStripe,
|
|
35
|
+
compact,
|
|
36
|
+
maxHeight,
|
|
37
|
+
className,
|
|
38
|
+
id,
|
|
39
|
+
onFilter,
|
|
40
|
+
children
|
|
41
|
+
}: ReactDataTableProps) => {
|
|
42
|
+
const [filteredData, setFilteredData] = useState<any>(data)
|
|
43
|
+
const [toggledData, setToggledData] = useState(filteredData)
|
|
44
|
+
const [filteredHeadings, setFilteredHeadings] = useState<any>(headings)
|
|
45
|
+
const [page, setPage] = useState(1)
|
|
46
|
+
const [hasActiveFilter, setHasActiveFilter] = useState(false)
|
|
47
|
+
const [sortOrder, setSortOrder] = useState(1)
|
|
48
|
+
|
|
49
|
+
const classes = classNames([
|
|
50
|
+
styles.table,
|
|
51
|
+
hover && styles.hover,
|
|
52
|
+
striped && styles[`striped-${striped}s`],
|
|
53
|
+
offsetStripe && styles.offset,
|
|
54
|
+
compact && styles.compact,
|
|
55
|
+
maxHeight && styles.scroll
|
|
56
|
+
])
|
|
57
|
+
|
|
58
|
+
const footerClasses = classNames([
|
|
59
|
+
styles.footer,
|
|
60
|
+
subText && styles.between
|
|
61
|
+
])
|
|
62
|
+
|
|
63
|
+
const styleVariables = {
|
|
64
|
+
...(maxHeight && { maxHeight })
|
|
65
|
+
} as React.CSSProperties
|
|
66
|
+
|
|
67
|
+
const showColumnToggle = headings?.some(heading => {
|
|
68
|
+
return typeof heading === 'string' ? false : heading.toggleable
|
|
69
|
+
})
|
|
70
|
+
|
|
71
|
+
const columnToggleItems = [{
|
|
72
|
+
items: headings?.length ? headings
|
|
73
|
+
.filter(heading => typeof heading !== 'string' && heading.toggleable)
|
|
74
|
+
.map(heading => ({
|
|
75
|
+
icon: checkIcon,
|
|
76
|
+
name: (heading as HeadingObject).name,
|
|
77
|
+
value: String(headings.findIndex(h => {
|
|
78
|
+
return (h as HeadingObject).name === (heading as HeadingObject).name
|
|
79
|
+
}))
|
|
80
|
+
})) : []
|
|
81
|
+
}]
|
|
82
|
+
|
|
83
|
+
const columnFilterIndexes = headings?.map(heading => (heading as HeadingObject).filterable)
|
|
84
|
+
.map((heading, index) => heading ? index : null)
|
|
85
|
+
.filter(heading => heading !== null) || []
|
|
86
|
+
|
|
87
|
+
const hasPagination = data?.length && itemsPerPage
|
|
88
|
+
? data.length > itemsPerPage
|
|
89
|
+
: false
|
|
90
|
+
|
|
91
|
+
const filter = debounce((event: Event) => {
|
|
92
|
+
const target = event.target as HTMLInputElement
|
|
93
|
+
|
|
94
|
+
setHasActiveFilter(!!target.value)
|
|
95
|
+
|
|
96
|
+
setFilteredData(toggledData?.filter((row: string[]) => {
|
|
97
|
+
const rowValue = row.filter((_, index) => columnFilterIndexes.includes(index))
|
|
98
|
+
.join('')
|
|
99
|
+
.toLowerCase()
|
|
100
|
+
|
|
101
|
+
return rowValue.includes(target.value.toLowerCase())
|
|
102
|
+
}))
|
|
103
|
+
|
|
104
|
+
onFilter?.({
|
|
105
|
+
results: filteredData,
|
|
106
|
+
numberOfResults: filteredData.length
|
|
107
|
+
})
|
|
108
|
+
}, 400)
|
|
109
|
+
|
|
110
|
+
const toggleColumns = (event: ListEventType) => {
|
|
111
|
+
const columnToggleListElement = Array.from(event.list.children)
|
|
112
|
+
.find(child => (child as HTMLLIElement).dataset.name === event.name) as HTMLLIElement
|
|
113
|
+
const svgIcon = columnToggleListElement.children[0] as HTMLElement
|
|
114
|
+
let mappedData
|
|
115
|
+
|
|
116
|
+
svgIcon.style.opacity = svgIcon.style.opacity === '0'
|
|
117
|
+
? '1'
|
|
118
|
+
: '0'
|
|
119
|
+
|
|
120
|
+
if (svgIcon.style.opacity === '0') {
|
|
121
|
+
mappedData = (hasActiveFilter ? data : filteredData)?.map((row: string[]) => {
|
|
122
|
+
return row.map((column, index) => index === Number(event.value) ? null : column)
|
|
123
|
+
})
|
|
124
|
+
|
|
125
|
+
setFilteredData(mappedData)
|
|
126
|
+
|
|
127
|
+
setFilteredHeadings(filteredHeadings.map((heading: HeadingObject | string) => {
|
|
128
|
+
return ((heading as HeadingObject)?.name || heading) === event.name ? null : heading
|
|
129
|
+
}))
|
|
130
|
+
} else {
|
|
131
|
+
mappedData = (hasActiveFilter ? data : filteredData)?.map((row: string[], x: number) => {
|
|
132
|
+
return row.map((column, y) => y === Number(event.value) ? data?.[x][y] : column)
|
|
133
|
+
})
|
|
134
|
+
|
|
135
|
+
setFilteredData(mappedData)
|
|
136
|
+
|
|
137
|
+
setFilteredHeadings(filteredHeadings.map((heading: HeadingObject | string, index: number) => {
|
|
138
|
+
return ((headings?.[index] as HeadingObject)?.name || headings?.[index]) === event.name
|
|
139
|
+
? headings?.[index]
|
|
140
|
+
: heading
|
|
141
|
+
}))
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
setToggledData(mappedData)
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
const sort = (index: number) => {
|
|
148
|
+
const sortedData = filteredData.sort((a: string[], b: string[]) => {
|
|
149
|
+
let aValue: string | number = a[index]
|
|
150
|
+
let bValue: string | number = b[index]
|
|
151
|
+
|
|
152
|
+
if (!isNaN(aValue as any)) {
|
|
153
|
+
aValue = Number(aValue)
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
if (!isNaN(bValue as any)) {
|
|
157
|
+
bValue = Number(bValue)
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
return aValue > bValue
|
|
161
|
+
? sortOrder * -1
|
|
162
|
+
: sortOrder
|
|
163
|
+
})
|
|
164
|
+
|
|
165
|
+
setFilteredData(sortedData)
|
|
166
|
+
setSortOrder(sortOrder === 1 ? -1 : 1)
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
const isNextPage = (index: number) => {
|
|
170
|
+
if (hasPagination && itemsPerPage && !hasActiveFilter) {
|
|
171
|
+
const currentPage = Math.ceil((index + 1) / itemsPerPage)
|
|
172
|
+
|
|
173
|
+
return currentPage !== page ? 'true' : undefined
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
if (hasActiveFilter && itemsPerPage) {
|
|
177
|
+
return index >= itemsPerPage ? 'true' : undefined
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
return undefined
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
return (
|
|
184
|
+
<section className={className} id={id}>
|
|
185
|
+
{(!!columnFilterIndexes?.length || showColumnToggle) && (
|
|
186
|
+
<div className={styles.filters}>
|
|
187
|
+
{!!columnFilterIndexes?.length && (
|
|
188
|
+
<Input
|
|
189
|
+
type="search"
|
|
190
|
+
placeholder={filterPlaceholder}
|
|
191
|
+
onInput={filter}
|
|
192
|
+
>
|
|
193
|
+
{showFilterIcon && (
|
|
194
|
+
<span dangerouslySetInnerHTML={{ __html: searchIcon }} />
|
|
195
|
+
)}
|
|
196
|
+
</Input>
|
|
197
|
+
)}
|
|
198
|
+
{showColumnToggle && (
|
|
199
|
+
<Select
|
|
200
|
+
name={`data-table-${id || crypto.randomUUID()}`}
|
|
201
|
+
itemGroups={columnToggleItems}
|
|
202
|
+
position="bottom-end"
|
|
203
|
+
value={columnToggleLabel}
|
|
204
|
+
onChange={toggleColumns}
|
|
205
|
+
updateValue={false}
|
|
206
|
+
/>
|
|
207
|
+
)}
|
|
208
|
+
</div>
|
|
209
|
+
)}
|
|
210
|
+
|
|
211
|
+
<div className={classes} style={styleVariables}>
|
|
212
|
+
<table>
|
|
213
|
+
{!!filteredHeadings?.length && (
|
|
214
|
+
<thead>
|
|
215
|
+
<tr>
|
|
216
|
+
{filteredHeadings?.map((heading: HeadingObject | string, index: number) => {
|
|
217
|
+
if (!heading) {
|
|
218
|
+
return null
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
return (
|
|
222
|
+
<th key={index}>
|
|
223
|
+
<ConditionalWrapper
|
|
224
|
+
condition={!!(heading as HeadingObject).sortable}
|
|
225
|
+
wrapper={children => (
|
|
226
|
+
<Button theme="flat" slot="wrapper" onClick={() => sort(index)}>
|
|
227
|
+
{children}
|
|
228
|
+
<span dangerouslySetInnerHTML={{ __html: orderIcon }} />
|
|
229
|
+
</Button>
|
|
230
|
+
)}
|
|
231
|
+
>
|
|
232
|
+
{(heading as HeadingObject).name || heading as string}
|
|
233
|
+
</ConditionalWrapper>
|
|
234
|
+
</th>
|
|
235
|
+
)
|
|
236
|
+
})}
|
|
237
|
+
</tr>
|
|
238
|
+
</thead>
|
|
239
|
+
)}
|
|
240
|
+
|
|
241
|
+
<tbody>
|
|
242
|
+
{filteredData?.map((row: string[], rowIndex: number) => (
|
|
243
|
+
<tr key={rowIndex} data-hidden={isNextPage(rowIndex)}>
|
|
244
|
+
{row.filter(Boolean).map((column, columnIndex) => (
|
|
245
|
+
<td
|
|
246
|
+
key={columnIndex}
|
|
247
|
+
dangerouslySetInnerHTML={{ __html: column }}
|
|
248
|
+
/>
|
|
249
|
+
))}
|
|
250
|
+
</tr>
|
|
251
|
+
))}
|
|
252
|
+
{children}
|
|
253
|
+
</tbody>
|
|
254
|
+
{!filteredData?.length && (
|
|
255
|
+
<tfoot>
|
|
256
|
+
<tr>
|
|
257
|
+
<td
|
|
258
|
+
colSpan={data?.[0].length}
|
|
259
|
+
className={styles['no-results']}
|
|
260
|
+
>
|
|
261
|
+
{noResultsLabel}
|
|
262
|
+
</td>
|
|
263
|
+
</tr>
|
|
264
|
+
</tfoot>
|
|
265
|
+
)}
|
|
266
|
+
</table>
|
|
267
|
+
</div>
|
|
268
|
+
{(subText || hasPagination) && (
|
|
269
|
+
<div className={footerClasses}>
|
|
270
|
+
{subText && (
|
|
271
|
+
<span className={styles.subtext}>{subText}</span>
|
|
272
|
+
)}
|
|
273
|
+
{(hasPagination && itemsPerPage && !hasActiveFilter) && (
|
|
274
|
+
<Pagination
|
|
275
|
+
{...pagination}
|
|
276
|
+
totalPages={Math.ceil((data?.length || 0) / itemsPerPage)}
|
|
277
|
+
currentPage={page}
|
|
278
|
+
onChange={event => setPage(event.page)}
|
|
279
|
+
/>
|
|
280
|
+
)}
|
|
281
|
+
</div>
|
|
282
|
+
)}
|
|
283
|
+
</section>
|
|
284
|
+
)
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
export default DataTable
|
|
@@ -150,6 +150,7 @@ const generatedPages = pages?.length
|
|
|
150
150
|
const prevPageButton = element.querySelector('[data-page="prev"]') as HTMLButtonElement
|
|
151
151
|
const nextPageButton = element.querySelector('[data-page="next"]') as HTMLButtonElement
|
|
152
152
|
const currentPageButton = element.querySelector('[data-active]') as HTMLButtonElement
|
|
153
|
+
const previousPage = currentPage
|
|
153
154
|
|
|
154
155
|
const pageElements = Array
|
|
155
156
|
.from(pagination.children)
|
|
@@ -179,6 +180,7 @@ const generatedPages = pages?.length
|
|
|
179
180
|
|
|
180
181
|
dispatch('paginate', {
|
|
181
182
|
page: currentPage,
|
|
183
|
+
direction: previousPage > currentPage ? 'prev' : 'next',
|
|
182
184
|
...(activeButton?.dataset.page && { label: activeButton?.dataset.page }),
|
|
183
185
|
target: element,
|
|
184
186
|
trusted: event.isTrusted
|
|
@@ -45,6 +45,8 @@
|
|
|
45
45
|
}))
|
|
46
46
|
|
|
47
47
|
const paginate = (to: string | number) => {
|
|
48
|
+
const previousPage = calculatedCurrentPage
|
|
49
|
+
|
|
48
50
|
if (to === 'prev') {
|
|
49
51
|
calculatedCurrentPage = calculatedCurrentPage - 1
|
|
50
52
|
} else if (to === 'next') {
|
|
@@ -57,6 +59,7 @@
|
|
|
57
59
|
|
|
58
60
|
onChange?.({
|
|
59
61
|
page: calculatedCurrentPage,
|
|
62
|
+
direction: previousPage > calculatedCurrentPage ? 'prev' : 'next',
|
|
60
63
|
...(label && { label })
|
|
61
64
|
})
|
|
62
65
|
}
|
|
@@ -87,7 +90,7 @@
|
|
|
87
90
|
theme={theme}
|
|
88
91
|
onClick={!(disablePrevious || (calculatedCurrentPage === 1 && !previousLink))
|
|
89
92
|
? () => paginate('prev')
|
|
90
|
-
:
|
|
93
|
+
: undefined
|
|
91
94
|
}
|
|
92
95
|
>
|
|
93
96
|
{#if showChevrons || type === 'arrows'}
|
|
@@ -107,7 +110,7 @@
|
|
|
107
110
|
theme={theme}
|
|
108
111
|
onClick={calculatedCurrentPage !== index + 1
|
|
109
112
|
? () => paginate(index + 1)
|
|
110
|
-
:
|
|
113
|
+
: undefined
|
|
111
114
|
}
|
|
112
115
|
>
|
|
113
116
|
{page.label}
|
|
@@ -129,7 +132,7 @@
|
|
|
129
132
|
theme={theme}
|
|
130
133
|
onClick={!disableNext
|
|
131
134
|
? () => paginate('next')
|
|
132
|
-
:
|
|
135
|
+
: undefined
|
|
133
136
|
}
|
|
134
137
|
>
|
|
135
138
|
{#if type !== 'arrows'}
|
|
@@ -53,6 +53,7 @@ const Pagination = ({
|
|
|
53
53
|
}))
|
|
54
54
|
|
|
55
55
|
const paginate = (to: string | number) => {
|
|
56
|
+
const previousPage = calculatedCurrentPage
|
|
56
57
|
let currentPage = calculatedCurrentPage
|
|
57
58
|
|
|
58
59
|
if (to === 'prev') {
|
|
@@ -69,6 +70,7 @@ const Pagination = ({
|
|
|
69
70
|
|
|
70
71
|
onChange?.({
|
|
71
72
|
page: currentPage,
|
|
73
|
+
direction: previousPage > currentPage ? 'prev' : 'next',
|
|
72
74
|
...(label && { label })
|
|
73
75
|
})
|
|
74
76
|
}
|
|
@@ -104,7 +106,7 @@ const Pagination = ({
|
|
|
104
106
|
theme={theme}
|
|
105
107
|
onClick={!(disablePrevious || (calculatedCurrentPage === 1 && !previousLink))
|
|
106
108
|
? () => paginate('prev')
|
|
107
|
-
:
|
|
109
|
+
: undefined
|
|
108
110
|
}
|
|
109
111
|
>
|
|
110
112
|
{(showChevrons || type === 'arrows') && (
|
|
@@ -122,7 +124,7 @@ const Pagination = ({
|
|
|
122
124
|
theme={theme}
|
|
123
125
|
onClick={calculatedCurrentPage !== index + 1
|
|
124
126
|
? () => paginate(index + 1)
|
|
125
|
-
:
|
|
127
|
+
: undefined
|
|
126
128
|
}
|
|
127
129
|
>
|
|
128
130
|
{page.label}
|
|
@@ -144,7 +146,7 @@ const Pagination = ({
|
|
|
144
146
|
theme={theme}
|
|
145
147
|
onClick={!disableNext
|
|
146
148
|
? () => paginate('next')
|
|
147
|
-
:
|
|
149
|
+
: undefined
|
|
148
150
|
}
|
|
149
151
|
>
|
|
150
152
|
{type !== 'arrows' && nextPageLabel}
|
package/index.d.ts
CHANGED
|
@@ -68,6 +68,14 @@ declare module 'webcoreui' {
|
|
|
68
68
|
cancel: () => void
|
|
69
69
|
}
|
|
70
70
|
|
|
71
|
+
export const get: (selector: string, all: boolean) => Element | NodeListOf<Element> | null
|
|
72
|
+
export const on: (
|
|
73
|
+
selector: string | HTMLElement,
|
|
74
|
+
event: string,
|
|
75
|
+
callback: any,
|
|
76
|
+
all?: boolean
|
|
77
|
+
) => void
|
|
78
|
+
|
|
71
79
|
export const dispatch: (event: string, detail: unknown) => void
|
|
72
80
|
export const listen: (event: string, callback: (e: any) => unknown) => {
|
|
73
81
|
remove: () => void
|
package/index.js
CHANGED
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "webcoreui",
|
|
3
3
|
"type": "module",
|
|
4
|
-
"version": "0.6.
|
|
4
|
+
"version": "0.6.1",
|
|
5
5
|
"scripts": {
|
|
6
6
|
"prepare": "husky",
|
|
7
7
|
"pre-commit": "lint-staged",
|
|
@@ -14,6 +14,7 @@
|
|
|
14
14
|
},
|
|
15
15
|
"devDependencies": {
|
|
16
16
|
"@astrojs/check": "0.7.0",
|
|
17
|
+
"@astrojs/node": "8.3.4",
|
|
17
18
|
"@astrojs/react": "3.5.0",
|
|
18
19
|
"@astrojs/svelte": "5.5.0",
|
|
19
20
|
"@eslint/js": "9.9.1",
|
package/scss/global/utility.scss
CHANGED
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
export const get = (selector: string, all: boolean = false) => all
|
|
2
|
+
? document?.querySelectorAll(selector)
|
|
3
|
+
: document?.querySelector(selector)
|
|
4
|
+
|
|
5
|
+
export const on = (
|
|
6
|
+
selector: string | HTMLElement,
|
|
7
|
+
event: string,
|
|
8
|
+
callback: any,
|
|
9
|
+
all?: boolean
|
|
10
|
+
) => {
|
|
11
|
+
if (all) {
|
|
12
|
+
const elements = document.querySelectorAll(selector as string)
|
|
13
|
+
|
|
14
|
+
elements?.forEach(element => {
|
|
15
|
+
element.addEventListener(event, callback)
|
|
16
|
+
})
|
|
17
|
+
|
|
18
|
+
return
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
if (typeof selector === 'string') {
|
|
22
|
+
(get(selector) as HTMLElement)?.addEventListener(event, callback)
|
|
23
|
+
|
|
24
|
+
return
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
selector?.addEventListener(event, callback)
|
|
28
|
+
}
|
|
@@ -1,31 +0,0 @@
|
|
|
1
|
-
@import '../config';
|
|
2
|
-
|
|
3
|
-
@mixin elements() {
|
|
4
|
-
code {
|
|
5
|
-
display: inline-block;
|
|
6
|
-
padding: 2px 10px;
|
|
7
|
-
border-radius: 5px;
|
|
8
|
-
border: 1px solid var(--w-color-primary-50);
|
|
9
|
-
font-size: 14px;
|
|
10
|
-
}
|
|
11
|
-
|
|
12
|
-
figure {
|
|
13
|
-
margin: 0;
|
|
14
|
-
}
|
|
15
|
-
|
|
16
|
-
p {
|
|
17
|
-
font-size: 16px;
|
|
18
|
-
line-height: 1.7;
|
|
19
|
-
margin: 20px 0;
|
|
20
|
-
}
|
|
21
|
-
|
|
22
|
-
ul, ol {
|
|
23
|
-
line-height: 1.7;
|
|
24
|
-
font-size: 16px;
|
|
25
|
-
margin: 20px 0;
|
|
26
|
-
|
|
27
|
-
li {
|
|
28
|
-
margin-bottom: 10px;
|
|
29
|
-
}
|
|
30
|
-
}
|
|
31
|
-
}
|