@dnax/ui 0.0.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/README.md +15 -0
- package/components/QAccordion.vue +68 -0
- package/components/QAccordionContent.vue +62 -0
- package/components/QAccordionItem.vue +24 -0
- package/components/QAccordionTrigger.vue +51 -0
- package/components/QActionSheet.vue +159 -0
- package/components/QApp.vue +8 -0
- package/components/QAutocomplete.vue +316 -0
- package/components/QAvatar.vue +92 -0
- package/components/QBackHeader.vue +69 -0
- package/components/QBackTop.vue +125 -0
- package/components/QBadge.vue +55 -0
- package/components/QBottomSheet.vue +177 -0
- package/components/QBottomSheetFooter.vue +9 -0
- package/components/QBottomSheetHeader.vue +35 -0
- package/components/QBottomSheetProvider.vue +62 -0
- package/components/QBottomSheetTrigger.vue +18 -0
- package/components/QBtn.vue +144 -0
- package/components/QBtnGroup.vue +31 -0
- package/components/QBubble.vue +36 -0
- package/components/QBubbleContent.vue +28 -0
- package/components/QBubbleGroup.vue +25 -0
- package/components/QBubbleReactions.vue +40 -0
- package/components/QCard.vue +63 -0
- package/components/QCardActions.vue +27 -0
- package/components/QCardContent.vue +29 -0
- package/components/QCarousel.vue +115 -0
- package/components/QCarouselContent.vue +23 -0
- package/components/QCarouselItem.vue +20 -0
- package/components/QCarouselNav.vue +51 -0
- package/components/QCheckbox.vue +146 -0
- package/components/QChip.vue +91 -0
- package/components/QCircularProgress.vue +122 -0
- package/components/QCol.vue +52 -0
- package/components/QCollapse.vue +141 -0
- package/components/QConfigProvider.vue +164 -0
- package/components/QContainer.vue +201 -0
- package/components/QCountDown.vue +164 -0
- package/components/QCountryPicker.vue +593 -0
- package/components/QDataGrid.vue +491 -0
- package/components/QDatePicker.vue +250 -0
- package/components/QDialog.vue +163 -0
- package/components/QDialogFooter.vue +9 -0
- package/components/QDialogHeader.vue +41 -0
- package/components/QDialogProvider.vue +65 -0
- package/components/QDialogTrigger.vue +18 -0
- package/components/QFab.vue +80 -0
- package/components/QFabAction.vue +48 -0
- package/components/QFilePicker.vue +294 -0
- package/components/QFooter.vue +72 -0
- package/components/QGallery.vue +280 -0
- package/components/QGrid.vue +54 -0
- package/components/QHeader.vue +38 -0
- package/components/QIcon.vue +53 -0
- package/components/QImagePicker.vue +248 -0
- package/components/QImagePreview.vue +225 -0
- package/components/QImagePreviewProvider.vue +35 -0
- package/components/QImg.vue +142 -0
- package/components/QInfiniteScroll.vue +109 -0
- package/components/QInnerLoading.vue +67 -0
- package/components/QInput.vue +289 -0
- package/components/QInputOtp.vue +238 -0
- package/components/QInputPassword.vue +67 -0
- package/components/QInputTag.vue +281 -0
- package/components/QIntersection.vue +119 -0
- package/components/QItem.vue +60 -0
- package/components/QItemSection.vue +47 -0
- package/components/QLinearProgress.vue +94 -0
- package/components/QList.vue +39 -0
- package/components/QLoading.vue +77 -0
- package/components/QLoadingProvider.vue +41 -0
- package/components/QMarquee.vue +56 -0
- package/components/QMessageScroller.vue +10 -0
- package/components/QMessageScrollerButton.vue +48 -0
- package/components/QMessageScrollerContent.vue +31 -0
- package/components/QMessageScrollerItem.vue +49 -0
- package/components/QMessageScrollerProvider.vue +198 -0
- package/components/QMessageScrollerViewport.vue +109 -0
- package/components/QNavMenu.vue +75 -0
- package/components/QNavMenuContent.vue +10 -0
- package/components/QNavMenuItem.vue +60 -0
- package/components/QNavMenuTrigger.vue +96 -0
- package/components/QNotifyProvider.vue +33 -0
- package/components/QNotifyToast.vue +60 -0
- package/components/QPage.vue +56 -0
- package/components/QPagination.vue +195 -0
- package/components/QParallax.vue +124 -0
- package/components/QPullToRefresh.vue +138 -0
- package/components/QRadio.vue +99 -0
- package/components/QRating.vue +105 -0
- package/components/QReorder.vue +191 -0
- package/components/QRollingText.vue +225 -0
- package/components/QRow.vue +23 -0
- package/components/QSafeArea.vue +39 -0
- package/components/QScrollArea.vue +118 -0
- package/components/QSelect.vue +698 -0
- package/components/QSeparator.vue +39 -0
- package/components/QSidebar.vue +153 -0
- package/components/QSidebarContent.vue +14 -0
- package/components/QSidebarFooter.vue +14 -0
- package/components/QSidebarHeader.vue +14 -0
- package/components/QSidebarMenu.vue +9 -0
- package/components/QSidebarMenuButton.vue +59 -0
- package/components/QSidebarMenuItem.vue +9 -0
- package/components/QSidebarTrigger.vue +32 -0
- package/components/QSkeleton.vue +95 -0
- package/components/QSlider.vue +205 -0
- package/components/QSpace.vue +10 -0
- package/components/QSpinner.vue +172 -0
- package/components/QSplitter.vue +145 -0
- package/components/QSticky.vue +181 -0
- package/components/QSwipeCell.vue +217 -0
- package/components/QSwiper.vue +102 -0
- package/components/QSwiperSlide.vue +19 -0
- package/components/QSyntax.vue +171 -0
- package/components/QTab.vue +175 -0
- package/components/QTabPanel.vue +50 -0
- package/components/QTabPanels.vue +106 -0
- package/components/QTable.vue +575 -0
- package/components/QTabs.vue +188 -0
- package/components/QText.vue +316 -0
- package/components/QTextCaption.vue +122 -0
- package/components/QToolbar.vue +28 -0
- package/components/QTooltip.vue +313 -0
- package/components/QUploader.vue +473 -0
- package/components/QVideo.vue +289 -0
- package/components/QVirtualScroll.vue +190 -0
- package/components/internal/QDateCalendar.vue +153 -0
- package/index.ts +151 -0
- package/lib/breakpoints.ts +44 -0
- package/lib/colors.ts +24 -0
- package/lib/config.ts +34 -0
- package/lib/countries.ts +261 -0
- package/lib/formula.ts +630 -0
- package/lib/icons.ts +38 -0
- package/lib/loading.ts +75 -0
- package/lib/overlayBack.ts +124 -0
- package/lib/platform.ts +146 -0
- package/lib/primitive.ts +25 -0
- package/lib/q.ts +287 -0
- package/lib/screen.ts +105 -0
- package/lib/search.ts +35 -0
- package/lib/themeVars.ts +22 -0
- package/lib/useComponentProps.ts +54 -0
- package/lib/utils.ts +7 -0
- package/module.ts +89 -0
- package/package.json +41 -0
- package/scripts/generate-exports.ts +56 -0
- package/styles/main.css +7627 -0
- package/tsconfig.json +33 -0
|
@@ -0,0 +1,575 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
// QTable — API Quasar : <q-table :rows :columns row-key="id" v-model:pagination v-model:selected selection="multiple" @request @row-click />
|
|
3
|
+
// Architecture type shadcn-vue data table : colonnes déclaratives, tri client, pagination, sélection.
|
|
4
|
+
export interface QTableColumn {
|
|
5
|
+
name?: string
|
|
6
|
+
label?: string
|
|
7
|
+
/** Chemin dans la ligne : string, tableau de chemins, ou fonction */
|
|
8
|
+
field?: string | string[] | ((row: any) => any)
|
|
9
|
+
align?: "left" | "center" | "right"
|
|
10
|
+
sortable?: boolean
|
|
11
|
+
/** Tri custom (a, b) => nombre */
|
|
12
|
+
sort?: (a: any, b: any) => number
|
|
13
|
+
/** Formatteur de la valeur affichée */
|
|
14
|
+
format?: (val: any, row: any) => any
|
|
15
|
+
/** Colonne épinglée (sticky) : left | right — ex. colonne Actions */
|
|
16
|
+
pinned?: "left" | "right"
|
|
17
|
+
classes?: string
|
|
18
|
+
style?: string
|
|
19
|
+
headerClasses?: string
|
|
20
|
+
headerStyle?: string
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export interface QTablePagination {
|
|
24
|
+
sortBy: string | null
|
|
25
|
+
descending: boolean
|
|
26
|
+
page: number
|
|
27
|
+
rowsPerPage: number
|
|
28
|
+
}
|
|
29
|
+
</script>
|
|
30
|
+
|
|
31
|
+
<script setup lang="ts">
|
|
32
|
+
import { computed, getCurrentInstance, onBeforeUnmount, onMounted, ref, watch } from "vue"
|
|
33
|
+
import { Icon } from "@iconify/vue"
|
|
34
|
+
import { icons } from "../lib/icons"
|
|
35
|
+
import { cn } from "../lib/utils"
|
|
36
|
+
import { radiusStyle, useRadius } from "../lib/useComponentProps"
|
|
37
|
+
import type { RadiusProp } from "../lib/useComponentProps"
|
|
38
|
+
|
|
39
|
+
interface Props {
|
|
40
|
+
/** Lignes de données */
|
|
41
|
+
rows?: any[]
|
|
42
|
+
/** Colonnes ({ name, label, field, align, sortable, sort, format, classes, style, headerClasses, headerStyle }) */
|
|
43
|
+
columns?: QTableColumn[]
|
|
44
|
+
/** Clé de ligne : string (propriété) ou fonction (défaut "id") */
|
|
45
|
+
rowKey?: string | ((row: any) => any)
|
|
46
|
+
/** Pagination contrôlée (v-model:pagination) */
|
|
47
|
+
pagination?: QTablePagination | null
|
|
48
|
+
/** Lignes sélectionnées (v-model:selected) */
|
|
49
|
+
selected?: any[]
|
|
50
|
+
/** Type de sélection */
|
|
51
|
+
selection?: "none" | "single" | "multiple"
|
|
52
|
+
/** Hauteur réduite */
|
|
53
|
+
dense?: boolean
|
|
54
|
+
/** Sans ombre */
|
|
55
|
+
flat?: boolean
|
|
56
|
+
/** Bordure */
|
|
57
|
+
bordered?: boolean
|
|
58
|
+
/** Séparateurs : horizontal | vertical | cell | none */
|
|
59
|
+
separator?: "horizontal" | "vertical" | "cell" | "none"
|
|
60
|
+
/** Overlay de chargement */
|
|
61
|
+
loading?: boolean
|
|
62
|
+
title?: string
|
|
63
|
+
noDataLabel?: string
|
|
64
|
+
dark?: boolean
|
|
65
|
+
hideHeader?: boolean
|
|
66
|
+
hideBottom?: boolean
|
|
67
|
+
/** Options du sélecteur de lignes par page (0 = toutes) */
|
|
68
|
+
rowsPerPageOptions?: number[]
|
|
69
|
+
/** Hauteur max du conteneur scrollable (header fixe) — ex. "60vh", "400px" */
|
|
70
|
+
maxHeight?: string
|
|
71
|
+
/** Header sticky quand max-height est défini (défaut : true) */
|
|
72
|
+
fixedHeader?: boolean
|
|
73
|
+
/** Virtualisation (windowing) des lignes pour les gros jeux de données */
|
|
74
|
+
virtualScroll?: boolean
|
|
75
|
+
/** Hauteur estimée d'une ligne pour la virtualisation (px) */
|
|
76
|
+
virtualScrollItemSize?: number
|
|
77
|
+
/** Classe CSS appliquée à toutes les cellules d'en-tête */
|
|
78
|
+
headerClass?: string
|
|
79
|
+
/** Style CSS appliqué à toutes les cellules d'en-tête (ex. couleur de fond) */
|
|
80
|
+
headerStyle?: string
|
|
81
|
+
/** Coins arrondis : true = md, ou échelle xs|sm|md|lg (none = carré) — défaut : composantProps */
|
|
82
|
+
radius?: RadiusProp
|
|
83
|
+
tableClass?: string
|
|
84
|
+
tableStyle?: string
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
const props = withDefaults(defineProps<Props>(), {
|
|
88
|
+
rows: () => [],
|
|
89
|
+
columns: () => [],
|
|
90
|
+
rowKey: "id",
|
|
91
|
+
pagination: null,
|
|
92
|
+
selected: undefined,
|
|
93
|
+
selection: "none",
|
|
94
|
+
dense: false,
|
|
95
|
+
flat: false,
|
|
96
|
+
bordered: false,
|
|
97
|
+
separator: "horizontal",
|
|
98
|
+
loading: false,
|
|
99
|
+
noDataLabel: "No data available",
|
|
100
|
+
dark: false,
|
|
101
|
+
hideHeader: false,
|
|
102
|
+
hideBottom: false,
|
|
103
|
+
rowsPerPageOptions: () => [3, 5, 7, 10, 15, 20, 25, 50, 0],
|
|
104
|
+
maxHeight: "",
|
|
105
|
+
fixedHeader: true,
|
|
106
|
+
virtualScroll: false,
|
|
107
|
+
virtualScrollItemSize: 48,
|
|
108
|
+
headerClass: "",
|
|
109
|
+
headerStyle: "",
|
|
110
|
+
tableClass: "",
|
|
111
|
+
tableStyle: "",
|
|
112
|
+
})
|
|
113
|
+
|
|
114
|
+
const emit = defineEmits<{
|
|
115
|
+
"update:pagination": [value: QTablePagination]
|
|
116
|
+
"update:selected": [value: any[]]
|
|
117
|
+
"update:sorting": [value: { column: string; descending: boolean }]
|
|
118
|
+
request: [payload: { pagination: QTablePagination; getCellValue: (col: QTableColumn, row: any) => any }]
|
|
119
|
+
"row-click": [event: MouseEvent, row: any, index: number]
|
|
120
|
+
}>()
|
|
121
|
+
|
|
122
|
+
defineSlots<{
|
|
123
|
+
top?: (props: Record<string, any>) => any
|
|
124
|
+
bottom?: (props: Record<string, any>) => any
|
|
125
|
+
noData?: (props: Record<string, any>) => any
|
|
126
|
+
loading?: (props: Record<string, any>) => any
|
|
127
|
+
header?: (props: Record<string, any>) => any
|
|
128
|
+
body?: (props: Record<string, any>) => any
|
|
129
|
+
}>()
|
|
130
|
+
|
|
131
|
+
const instance = getCurrentInstance()
|
|
132
|
+
const hasRequestHandler = computed(() => !!instance?.vnode.props?.onRequest)
|
|
133
|
+
|
|
134
|
+
// — Pagination (interne, synchronisée avec v-model:pagination) —
|
|
135
|
+
const internalPagination = ref<QTablePagination>({ sortBy: null, descending: false, page: 1, rowsPerPage: 0 })
|
|
136
|
+
const pagination = computed(() => props.pagination ?? internalPagination.value)
|
|
137
|
+
|
|
138
|
+
watch(
|
|
139
|
+
() => props.pagination,
|
|
140
|
+
(v) => {
|
|
141
|
+
if (v) internalPagination.value = { ...internalPagination.value, ...v }
|
|
142
|
+
},
|
|
143
|
+
)
|
|
144
|
+
|
|
145
|
+
const setPagination = (patch: Partial<QTablePagination>) => {
|
|
146
|
+
internalPagination.value = { ...pagination.value, ...patch }
|
|
147
|
+
emit("update:pagination", internalPagination.value)
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
const page = computed({
|
|
151
|
+
get: () => pagination.value.page,
|
|
152
|
+
set: (v) => setPagination({ page: v }),
|
|
153
|
+
})
|
|
154
|
+
const rowsPerPage = computed({
|
|
155
|
+
get: () => pagination.value.rowsPerPage,
|
|
156
|
+
set: (v) => setPagination({ page: 1, rowsPerPage: v }),
|
|
157
|
+
})
|
|
158
|
+
const sortBy = computed(() => pagination.value.sortBy)
|
|
159
|
+
const descending = computed(() => pagination.value.descending)
|
|
160
|
+
|
|
161
|
+
// — Valeurs de cellule —
|
|
162
|
+
const getValue = (row: any, field: QTableColumn["field"]): any => {
|
|
163
|
+
if (typeof field === "function") return field(row)
|
|
164
|
+
if (Array.isArray(field)) return field.reduce((acc, k) => acc?.[k], row)
|
|
165
|
+
return field ? row?.[field] : undefined
|
|
166
|
+
}
|
|
167
|
+
const getCellValue = (col: QTableColumn, row: any): any => {
|
|
168
|
+
const val = getValue(row, col.field)
|
|
169
|
+
return col.format ? col.format(val, row) : val
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
const getRowKey = (row: any, index: number): unknown => {
|
|
173
|
+
if (typeof props.rowKey === "function") return props.rowKey(row)
|
|
174
|
+
return row?.[props.rowKey] ?? index
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
const colName = (col: QTableColumn): string => col.name ?? (typeof col.field === "string" ? col.field : "")
|
|
178
|
+
|
|
179
|
+
// Nom de slot dynamique body-cell-<name> : cast vers l'union des noms pour satisfaire le typage VLS
|
|
180
|
+
const bodyCellSlot = (col: QTableColumn): "top" | "bottom" | "noData" | "loading" | "header" | "body" =>
|
|
181
|
+
`body-cell-${colName(col)}` as "top" | "bottom" | "noData" | "loading" | "header" | "body"
|
|
182
|
+
|
|
183
|
+
// — Tri client (sauté si @request est branché : mode serveur) —
|
|
184
|
+
const sortedRows = computed(() => {
|
|
185
|
+
if (hasRequestHandler.value) return props.rows
|
|
186
|
+
const col = sortBy.value ? props.columns.find((c) => colName(c) === sortBy.value) : null
|
|
187
|
+
if (!col?.sortable) return props.rows
|
|
188
|
+
const dir = descending.value ? -1 : 1
|
|
189
|
+
return [...props.rows].sort((a, b) => {
|
|
190
|
+
if (col.sort) return dir * col.sort(a, b)
|
|
191
|
+
const va = getValue(a, col.field)
|
|
192
|
+
const vb = getValue(b, col.field)
|
|
193
|
+
if (va === vb) return 0
|
|
194
|
+
return (va > vb ? 1 : -1) * dir
|
|
195
|
+
})
|
|
196
|
+
})
|
|
197
|
+
|
|
198
|
+
const pageCount = computed(() =>
|
|
199
|
+
rowsPerPage.value > 0 ? Math.max(1, Math.ceil(sortedRows.value.length / rowsPerPage.value)) : 1,
|
|
200
|
+
)
|
|
201
|
+
|
|
202
|
+
const pagedRows = computed(() => {
|
|
203
|
+
if (hasRequestHandler.value || rowsPerPage.value === 0) return sortedRows.value
|
|
204
|
+
const start = (page.value - 1) * rowsPerPage.value
|
|
205
|
+
return sortedRows.value.slice(start, start + rowsPerPage.value)
|
|
206
|
+
})
|
|
207
|
+
|
|
208
|
+
// @request (mode serveur) : émis à chaque changement de pagination/tri/lignes
|
|
209
|
+
watch(
|
|
210
|
+
[() => pagination.value, () => props.rows],
|
|
211
|
+
() => {
|
|
212
|
+
if (hasRequestHandler.value) emit("request", { pagination: pagination.value, getCellValue })
|
|
213
|
+
},
|
|
214
|
+
)
|
|
215
|
+
|
|
216
|
+
const onSort = (col: QTableColumn) => {
|
|
217
|
+
const name = colName(col)
|
|
218
|
+
const nextDesc = sortBy.value === name ? !descending.value : false
|
|
219
|
+
setPagination({ sortBy: name, descending: nextDesc, page: 1 })
|
|
220
|
+
emit("update:sorting", { column: name, descending: nextDesc })
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
const sortIcon = (col: QTableColumn) => {
|
|
224
|
+
if (colName(col) !== sortBy.value) return icons.arrowUpDown
|
|
225
|
+
return descending.value ? icons.arrowDown : icons.arrowUp
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
// — Sélection —
|
|
229
|
+
const selectedRows = computed(() => props.selected ?? [])
|
|
230
|
+
const isSelected = (row: any, index: number) =>
|
|
231
|
+
selectedRows.value.some((r) => getRowKey(r, index) === getRowKey(row, index))
|
|
232
|
+
|
|
233
|
+
const toggleSelect = (row: any, index: number) => {
|
|
234
|
+
if (props.selection === "single") {
|
|
235
|
+
emit("update:selected", isSelected(row, index) ? [] : [row])
|
|
236
|
+
return
|
|
237
|
+
}
|
|
238
|
+
const next = isSelected(row, index)
|
|
239
|
+
? selectedRows.value.filter((r) => getRowKey(r, index) !== getRowKey(row, index))
|
|
240
|
+
: [...selectedRows.value, row]
|
|
241
|
+
emit("update:selected", next)
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
const allSelected = computed(
|
|
245
|
+
() => pagedRows.value.length > 0 && pagedRows.value.every((r, i) => isSelected(r, i)),
|
|
246
|
+
)
|
|
247
|
+
|
|
248
|
+
// Sélection partielle (header en état indéterminé)
|
|
249
|
+
const someSelected = computed(() => {
|
|
250
|
+
if (allSelected.value) return false
|
|
251
|
+
return pagedRows.value.some((r, i) => isSelected(r, i))
|
|
252
|
+
})
|
|
253
|
+
|
|
254
|
+
const toggleAll = () => {
|
|
255
|
+
if (allSelected.value) {
|
|
256
|
+
const keys = new Set(pagedRows.value.map((r, i) => String(getRowKey(r, i))))
|
|
257
|
+
emit("update:selected", selectedRows.value.filter((r, i) => !keys.has(String(getRowKey(r, i)))))
|
|
258
|
+
}
|
|
259
|
+
else {
|
|
260
|
+
const seen = new Set(selectedRows.value.map((r, i) => String(getRowKey(r, i))))
|
|
261
|
+
emit("update:selected", [...selectedRows.value, ...pagedRows.value.filter((r, i) => !seen.has(String(getRowKey(r, i))))])
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
const onRowClick = (e: MouseEvent, row: any, index: number) => {
|
|
266
|
+
if (props.selection === "single" && !isSelected(row, index)) {
|
|
267
|
+
emit("update:selected", [row])
|
|
268
|
+
}
|
|
269
|
+
emit("row-click", e, row, index)
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
// — Rendu —
|
|
273
|
+
const alignClass = (col: QTableColumn) =>
|
|
274
|
+
col.align && col.align !== "left" ? `q-table__cell--align-${col.align}` : ""
|
|
275
|
+
|
|
276
|
+
// Une colonne épinglee à gauche → la colonne de sélection s'épingle aussi (offset 0)
|
|
277
|
+
const hasPinnedLeft = computed(() => props.columns.some((c) => c.pinned === "left"))
|
|
278
|
+
const pinnedLeftOffset = computed(() => (props.selection !== "none" ? 36 : 0))
|
|
279
|
+
|
|
280
|
+
const colspan = computed(() => props.columns.length + (props.selection !== "none" ? 1 : 0))
|
|
281
|
+
|
|
282
|
+
const rangeLabel = computed(() => {
|
|
283
|
+
const total = sortedRows.value.length
|
|
284
|
+
if (rowsPerPage.value === 0 || total === 0) return `1-${total} of ${total}`
|
|
285
|
+
const first = (page.value - 1) * rowsPerPage.value + 1
|
|
286
|
+
const last = Math.min(page.value * rowsPerPage.value, total)
|
|
287
|
+
return `${first}-${last} of ${total}`
|
|
288
|
+
})
|
|
289
|
+
|
|
290
|
+
const tableClasses = computed(() =>
|
|
291
|
+
cn(
|
|
292
|
+
"q-table",
|
|
293
|
+
props.dense && "q-table--dense",
|
|
294
|
+
props.flat && "q-table--flat",
|
|
295
|
+
props.bordered && "q-table--bordered",
|
|
296
|
+
props.dark && "q-table--dark",
|
|
297
|
+
`q-table--separator-${props.separator}`,
|
|
298
|
+
),
|
|
299
|
+
)
|
|
300
|
+
|
|
301
|
+
// radius : prop explicite > composantProps.QTable.radius > composantProps.default.radius
|
|
302
|
+
const effectiveRadius = useRadius("QTable", () => props.radius)
|
|
303
|
+
const roundedStyle = computed(() => radiusStyle(effectiveRadius.value))
|
|
304
|
+
|
|
305
|
+
const sortThClass = computed(() => "q-table__sort")
|
|
306
|
+
|
|
307
|
+
// — Header fixe & virtual scroll —
|
|
308
|
+
const scrollEl = ref<HTMLElement | null>(null)
|
|
309
|
+
const scrollTop = ref(0)
|
|
310
|
+
const viewportH = ref(0)
|
|
311
|
+
|
|
312
|
+
const onScroll = () => {
|
|
313
|
+
const el = scrollEl.value
|
|
314
|
+
if (!el) return
|
|
315
|
+
scrollTop.value = el.scrollTop
|
|
316
|
+
viewportH.value = el.clientHeight
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
onMounted(() => {
|
|
320
|
+
onScroll()
|
|
321
|
+
if (typeof window !== "undefined") window.addEventListener("resize", onScroll)
|
|
322
|
+
})
|
|
323
|
+
onBeforeUnmount(() => {
|
|
324
|
+
if (typeof window !== "undefined") window.removeEventListener("resize", onScroll)
|
|
325
|
+
})
|
|
326
|
+
|
|
327
|
+
// Window sur les lignes visibles (spacers haut/bas + buffer)
|
|
328
|
+
const VIRTUAL_OVERSCAN = 6
|
|
329
|
+
const virtual = computed(() => {
|
|
330
|
+
const total = pagedRows.value.length
|
|
331
|
+
if (!props.virtualScroll || total === 0)
|
|
332
|
+
return { start: 0, rows: pagedRows.value, top: 0, bottom: 0 }
|
|
333
|
+
const rowH = Math.max(1, props.virtualScrollItemSize)
|
|
334
|
+
const start = Math.max(0, Math.floor(scrollTop.value / rowH) - VIRTUAL_OVERSCAN)
|
|
335
|
+
const end = Math.min(
|
|
336
|
+
total,
|
|
337
|
+
Math.ceil((scrollTop.value + Math.max(viewportH.value, 1)) / rowH) + VIRTUAL_OVERSCAN,
|
|
338
|
+
)
|
|
339
|
+
return {
|
|
340
|
+
start,
|
|
341
|
+
rows: pagedRows.value.slice(start, end),
|
|
342
|
+
top: start * rowH,
|
|
343
|
+
bottom: (total - end) * rowH,
|
|
344
|
+
}
|
|
345
|
+
})
|
|
346
|
+
</script>
|
|
347
|
+
|
|
348
|
+
<template>
|
|
349
|
+
<div class="q-table" :class="tableClasses" :style="roundedStyle">
|
|
350
|
+
<div v-if="title || $slots.top" class="q-table__top">
|
|
351
|
+
<slot name="top">
|
|
352
|
+
<h3 v-if="title" class="q-table__title">{{ title }}</h3>
|
|
353
|
+
</slot>
|
|
354
|
+
</div>
|
|
355
|
+
|
|
356
|
+
<div
|
|
357
|
+
ref="scrollEl"
|
|
358
|
+
class="q-table__scroll"
|
|
359
|
+
:class="{ 'q-table__scroll--fixed-header': !!maxHeight && fixedHeader }"
|
|
360
|
+
:style="maxHeight ? { maxHeight } : undefined"
|
|
361
|
+
@scroll.passive="onScroll"
|
|
362
|
+
>
|
|
363
|
+
<table class="q-table__table" :class="tableClass" :style="tableStyle">
|
|
364
|
+
<thead v-if="!hideHeader">
|
|
365
|
+
<slot name="header">
|
|
366
|
+
<tr>
|
|
367
|
+
<th
|
|
368
|
+
v-if="selection === 'multiple'"
|
|
369
|
+
class="q-table__selection-col"
|
|
370
|
+
:class="{ 'q-table__cell--pinned-left': hasPinnedLeft }"
|
|
371
|
+
>
|
|
372
|
+
<q-checkbox
|
|
373
|
+
:model-value="allSelected ? true : someSelected ? 'mixed' : false"
|
|
374
|
+
indeterminate-value="mixed"
|
|
375
|
+
dense
|
|
376
|
+
aria-label="Tout sélectionner"
|
|
377
|
+
@update:model-value="toggleAll"
|
|
378
|
+
/>
|
|
379
|
+
</th>
|
|
380
|
+
<th
|
|
381
|
+
v-for="col in columns"
|
|
382
|
+
:key="colName(col)"
|
|
383
|
+
:class="[
|
|
384
|
+
alignClass(col),
|
|
385
|
+
col.sortable && sortThClass,
|
|
386
|
+
headerClass,
|
|
387
|
+
col.headerClasses,
|
|
388
|
+
col.pinned === 'left' && 'q-table__cell--pinned-left',
|
|
389
|
+
col.pinned === 'right' && 'q-table__cell--pinned-right',
|
|
390
|
+
]"
|
|
391
|
+
:style="[
|
|
392
|
+
col.headerStyle,
|
|
393
|
+
headerStyle,
|
|
394
|
+
col.pinned === 'left' ? { left: `${pinnedLeftOffset}px` } : null,
|
|
395
|
+
]"
|
|
396
|
+
>
|
|
397
|
+
<button
|
|
398
|
+
v-if="col.sortable"
|
|
399
|
+
type="button"
|
|
400
|
+
class="q-table__sort-btn"
|
|
401
|
+
@click="onSort(col)"
|
|
402
|
+
>
|
|
403
|
+
<span>{{ col.label }}</span>
|
|
404
|
+
<Icon :icon="sortIcon(col)" class="q-table__sort-icon" aria-hidden="true" />
|
|
405
|
+
</button>
|
|
406
|
+
<span v-else>{{ col.label }}</span>
|
|
407
|
+
</th>
|
|
408
|
+
</tr>
|
|
409
|
+
</slot>
|
|
410
|
+
</thead>
|
|
411
|
+
|
|
412
|
+
<!-- Virtual scroll : spacers + fenêtre de lignes visibles -->
|
|
413
|
+
<tbody v-if="virtualScroll && !$slots.body">
|
|
414
|
+
<tr class="q-table__spacer" aria-hidden="true" :style="{ height: `${virtual.top}px` }">
|
|
415
|
+
<td :colspan="colspan" />
|
|
416
|
+
</tr>
|
|
417
|
+
<tr
|
|
418
|
+
v-for="(row, vi) in virtual.rows"
|
|
419
|
+
:key="String(getRowKey(row, virtual.start + vi))"
|
|
420
|
+
@click="(e: MouseEvent) => onRowClick(e, row, virtual.start + vi)"
|
|
421
|
+
>
|
|
422
|
+
<td
|
|
423
|
+
v-if="selection === 'multiple'"
|
|
424
|
+
class="q-table__selection-col"
|
|
425
|
+
:class="{ 'q-table__cell--pinned-left': hasPinnedLeft }"
|
|
426
|
+
@click.stop
|
|
427
|
+
>
|
|
428
|
+
<q-checkbox
|
|
429
|
+
:model-value="isSelected(row, virtual.start + vi)"
|
|
430
|
+
dense
|
|
431
|
+
:aria-label="'Sélectionner la ligne ' + (virtual.start + vi + 1)"
|
|
432
|
+
@update:model-value="() => toggleSelect(row, virtual.start + vi)"
|
|
433
|
+
/>
|
|
434
|
+
</td>
|
|
435
|
+
<td
|
|
436
|
+
v-else-if="selection === 'single'"
|
|
437
|
+
class="q-table__selection-col"
|
|
438
|
+
:class="{ 'q-table__cell--pinned-left': hasPinnedLeft }"
|
|
439
|
+
@click.stop
|
|
440
|
+
>
|
|
441
|
+
<q-radio
|
|
442
|
+
:model-value="isSelected(row, virtual.start + vi)"
|
|
443
|
+
dense
|
|
444
|
+
:aria-label="'Sélectionner la ligne ' + (virtual.start + vi + 1)"
|
|
445
|
+
@update:model-value="() => toggleSelect(row, virtual.start + vi)"
|
|
446
|
+
/>
|
|
447
|
+
</td>
|
|
448
|
+
<td
|
|
449
|
+
v-for="col in columns"
|
|
450
|
+
:key="colName(col)"
|
|
451
|
+
:class="[
|
|
452
|
+
alignClass(col),
|
|
453
|
+
col.classes,
|
|
454
|
+
col.pinned === 'left' && 'q-table__cell--pinned-left',
|
|
455
|
+
col.pinned === 'right' && 'q-table__cell--pinned-right',
|
|
456
|
+
]"
|
|
457
|
+
:style="[col.style, col.pinned === 'left' ? { left: `${pinnedLeftOffset}px` } : null]"
|
|
458
|
+
>
|
|
459
|
+
<slot
|
|
460
|
+
:name="bodyCellSlot(col)"
|
|
461
|
+
:col="col"
|
|
462
|
+
:row="row"
|
|
463
|
+
:index="virtual.start + vi"
|
|
464
|
+
:value="getCellValue(col, row)"
|
|
465
|
+
>
|
|
466
|
+
{{ getCellValue(col, row) }}
|
|
467
|
+
</slot>
|
|
468
|
+
</td>
|
|
469
|
+
</tr>
|
|
470
|
+
<tr class="q-table__spacer" aria-hidden="true" :style="{ height: `${virtual.bottom}px` }">
|
|
471
|
+
<td :colspan="colspan" />
|
|
472
|
+
</tr>
|
|
473
|
+
</tbody>
|
|
474
|
+
|
|
475
|
+
<tbody v-else-if="!$slots.body">
|
|
476
|
+
<tr
|
|
477
|
+
v-for="(row, rIdx) in pagedRows"
|
|
478
|
+
:key="String(getRowKey(row, rIdx))"
|
|
479
|
+
@click="(e: MouseEvent) => onRowClick(e, row, rIdx)"
|
|
480
|
+
>
|
|
481
|
+
<td
|
|
482
|
+
v-if="selection === 'multiple'"
|
|
483
|
+
class="q-table__selection-col"
|
|
484
|
+
:class="{ 'q-table__cell--pinned-left': hasPinnedLeft }"
|
|
485
|
+
@click.stop
|
|
486
|
+
>
|
|
487
|
+
<q-checkbox
|
|
488
|
+
:model-value="isSelected(row, rIdx)"
|
|
489
|
+
dense
|
|
490
|
+
:aria-label="'Sélectionner la ligne ' + (rIdx + 1)"
|
|
491
|
+
@update:model-value="() => toggleSelect(row, rIdx)"
|
|
492
|
+
/>
|
|
493
|
+
</td>
|
|
494
|
+
<td
|
|
495
|
+
v-else-if="selection === 'single'"
|
|
496
|
+
class="q-table__selection-col"
|
|
497
|
+
:class="{ 'q-table__cell--pinned-left': hasPinnedLeft }"
|
|
498
|
+
@click.stop
|
|
499
|
+
>
|
|
500
|
+
<q-radio
|
|
501
|
+
:model-value="isSelected(row, rIdx)"
|
|
502
|
+
dense
|
|
503
|
+
:aria-label="'Sélectionner la ligne ' + (rIdx + 1)"
|
|
504
|
+
@update:model-value="() => toggleSelect(row, rIdx)"
|
|
505
|
+
/>
|
|
506
|
+
</td>
|
|
507
|
+
<td
|
|
508
|
+
v-for="col in columns"
|
|
509
|
+
:key="colName(col)"
|
|
510
|
+
:class="[
|
|
511
|
+
alignClass(col),
|
|
512
|
+
col.classes,
|
|
513
|
+
col.pinned === 'left' && 'q-table__cell--pinned-left',
|
|
514
|
+
col.pinned === 'right' && 'q-table__cell--pinned-right',
|
|
515
|
+
]"
|
|
516
|
+
:style="[col.style, col.pinned === 'left' ? { left: `${pinnedLeftOffset}px` } : null]"
|
|
517
|
+
>
|
|
518
|
+
<slot :name="bodyCellSlot(col)" :col="col" :row="row" :index="rIdx" :value="getCellValue(col, row)">
|
|
519
|
+
{{ getCellValue(col, row) }}
|
|
520
|
+
</slot>
|
|
521
|
+
</td>
|
|
522
|
+
</tr>
|
|
523
|
+
<tr v-if="pagedRows.length === 0" class="q-table__no-data">
|
|
524
|
+
<td :colspan="colspan">
|
|
525
|
+
<slot name="noData">{{ noDataLabel }}</slot>
|
|
526
|
+
</td>
|
|
527
|
+
</tr>
|
|
528
|
+
</tbody>
|
|
529
|
+
<tbody v-else>
|
|
530
|
+
<slot name="body" :rows="pagedRows" />
|
|
531
|
+
</tbody>
|
|
532
|
+
</table>
|
|
533
|
+
</div>
|
|
534
|
+
|
|
535
|
+
<div v-if="loading" class="q-table__loading">
|
|
536
|
+
<slot name="loading">
|
|
537
|
+
<span class="q-spinner-ring" aria-hidden="true" />
|
|
538
|
+
</slot>
|
|
539
|
+
</div>
|
|
540
|
+
|
|
541
|
+
<div v-if="!hideBottom" class="q-table__bottom">
|
|
542
|
+
<slot name="bottom" />
|
|
543
|
+
<div class="q-table__pagination">
|
|
544
|
+
<span class="q-table__range">{{ rangeLabel }}</span>
|
|
545
|
+
<label class="q-table__rows-select">
|
|
546
|
+
Rows per page
|
|
547
|
+
<select :value="rowsPerPage" @change="(e) => (rowsPerPage = Number((e.target as HTMLSelectElement).value))">
|
|
548
|
+
<option v-for="n in rowsPerPageOptions" :key="n" :value="n">{{ n === 0 ? 'All' : n }}</option>
|
|
549
|
+
</select>
|
|
550
|
+
</label>
|
|
551
|
+
<div class="q-table__page-nav">
|
|
552
|
+
<button
|
|
553
|
+
type="button"
|
|
554
|
+
class="q-table__nav-btn"
|
|
555
|
+
:disabled="page <= 1"
|
|
556
|
+
aria-label="Page précédente"
|
|
557
|
+
@click="page--"
|
|
558
|
+
>
|
|
559
|
+
‹
|
|
560
|
+
</button>
|
|
561
|
+
<span class="q-table__page">{{ page }} / {{ pageCount }}</span>
|
|
562
|
+
<button
|
|
563
|
+
type="button"
|
|
564
|
+
class="q-table__nav-btn"
|
|
565
|
+
:disabled="page >= pageCount"
|
|
566
|
+
aria-label="Page suivante"
|
|
567
|
+
@click="page++"
|
|
568
|
+
>
|
|
569
|
+
›
|
|
570
|
+
</button>
|
|
571
|
+
</div>
|
|
572
|
+
</div>
|
|
573
|
+
</div>
|
|
574
|
+
</div>
|
|
575
|
+
</template>
|