@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.
Files changed (150) hide show
  1. package/README.md +15 -0
  2. package/components/QAccordion.vue +68 -0
  3. package/components/QAccordionContent.vue +62 -0
  4. package/components/QAccordionItem.vue +24 -0
  5. package/components/QAccordionTrigger.vue +51 -0
  6. package/components/QActionSheet.vue +159 -0
  7. package/components/QApp.vue +8 -0
  8. package/components/QAutocomplete.vue +316 -0
  9. package/components/QAvatar.vue +92 -0
  10. package/components/QBackHeader.vue +69 -0
  11. package/components/QBackTop.vue +125 -0
  12. package/components/QBadge.vue +55 -0
  13. package/components/QBottomSheet.vue +177 -0
  14. package/components/QBottomSheetFooter.vue +9 -0
  15. package/components/QBottomSheetHeader.vue +35 -0
  16. package/components/QBottomSheetProvider.vue +62 -0
  17. package/components/QBottomSheetTrigger.vue +18 -0
  18. package/components/QBtn.vue +144 -0
  19. package/components/QBtnGroup.vue +31 -0
  20. package/components/QBubble.vue +36 -0
  21. package/components/QBubbleContent.vue +28 -0
  22. package/components/QBubbleGroup.vue +25 -0
  23. package/components/QBubbleReactions.vue +40 -0
  24. package/components/QCard.vue +63 -0
  25. package/components/QCardActions.vue +27 -0
  26. package/components/QCardContent.vue +29 -0
  27. package/components/QCarousel.vue +115 -0
  28. package/components/QCarouselContent.vue +23 -0
  29. package/components/QCarouselItem.vue +20 -0
  30. package/components/QCarouselNav.vue +51 -0
  31. package/components/QCheckbox.vue +146 -0
  32. package/components/QChip.vue +91 -0
  33. package/components/QCircularProgress.vue +122 -0
  34. package/components/QCol.vue +52 -0
  35. package/components/QCollapse.vue +141 -0
  36. package/components/QConfigProvider.vue +164 -0
  37. package/components/QContainer.vue +201 -0
  38. package/components/QCountDown.vue +164 -0
  39. package/components/QCountryPicker.vue +593 -0
  40. package/components/QDataGrid.vue +491 -0
  41. package/components/QDatePicker.vue +250 -0
  42. package/components/QDialog.vue +163 -0
  43. package/components/QDialogFooter.vue +9 -0
  44. package/components/QDialogHeader.vue +41 -0
  45. package/components/QDialogProvider.vue +65 -0
  46. package/components/QDialogTrigger.vue +18 -0
  47. package/components/QFab.vue +80 -0
  48. package/components/QFabAction.vue +48 -0
  49. package/components/QFilePicker.vue +294 -0
  50. package/components/QFooter.vue +72 -0
  51. package/components/QGallery.vue +280 -0
  52. package/components/QGrid.vue +54 -0
  53. package/components/QHeader.vue +38 -0
  54. package/components/QIcon.vue +53 -0
  55. package/components/QImagePicker.vue +248 -0
  56. package/components/QImagePreview.vue +225 -0
  57. package/components/QImagePreviewProvider.vue +35 -0
  58. package/components/QImg.vue +142 -0
  59. package/components/QInfiniteScroll.vue +109 -0
  60. package/components/QInnerLoading.vue +67 -0
  61. package/components/QInput.vue +289 -0
  62. package/components/QInputOtp.vue +238 -0
  63. package/components/QInputPassword.vue +67 -0
  64. package/components/QInputTag.vue +281 -0
  65. package/components/QIntersection.vue +119 -0
  66. package/components/QItem.vue +60 -0
  67. package/components/QItemSection.vue +47 -0
  68. package/components/QLinearProgress.vue +94 -0
  69. package/components/QList.vue +39 -0
  70. package/components/QLoading.vue +77 -0
  71. package/components/QLoadingProvider.vue +41 -0
  72. package/components/QMarquee.vue +56 -0
  73. package/components/QMessageScroller.vue +10 -0
  74. package/components/QMessageScrollerButton.vue +48 -0
  75. package/components/QMessageScrollerContent.vue +31 -0
  76. package/components/QMessageScrollerItem.vue +49 -0
  77. package/components/QMessageScrollerProvider.vue +198 -0
  78. package/components/QMessageScrollerViewport.vue +109 -0
  79. package/components/QNavMenu.vue +75 -0
  80. package/components/QNavMenuContent.vue +10 -0
  81. package/components/QNavMenuItem.vue +60 -0
  82. package/components/QNavMenuTrigger.vue +96 -0
  83. package/components/QNotifyProvider.vue +33 -0
  84. package/components/QNotifyToast.vue +60 -0
  85. package/components/QPage.vue +56 -0
  86. package/components/QPagination.vue +195 -0
  87. package/components/QParallax.vue +124 -0
  88. package/components/QPullToRefresh.vue +138 -0
  89. package/components/QRadio.vue +99 -0
  90. package/components/QRating.vue +105 -0
  91. package/components/QReorder.vue +191 -0
  92. package/components/QRollingText.vue +225 -0
  93. package/components/QRow.vue +23 -0
  94. package/components/QSafeArea.vue +39 -0
  95. package/components/QScrollArea.vue +118 -0
  96. package/components/QSelect.vue +698 -0
  97. package/components/QSeparator.vue +39 -0
  98. package/components/QSidebar.vue +153 -0
  99. package/components/QSidebarContent.vue +14 -0
  100. package/components/QSidebarFooter.vue +14 -0
  101. package/components/QSidebarHeader.vue +14 -0
  102. package/components/QSidebarMenu.vue +9 -0
  103. package/components/QSidebarMenuButton.vue +59 -0
  104. package/components/QSidebarMenuItem.vue +9 -0
  105. package/components/QSidebarTrigger.vue +32 -0
  106. package/components/QSkeleton.vue +95 -0
  107. package/components/QSlider.vue +205 -0
  108. package/components/QSpace.vue +10 -0
  109. package/components/QSpinner.vue +172 -0
  110. package/components/QSplitter.vue +145 -0
  111. package/components/QSticky.vue +181 -0
  112. package/components/QSwipeCell.vue +217 -0
  113. package/components/QSwiper.vue +102 -0
  114. package/components/QSwiperSlide.vue +19 -0
  115. package/components/QSyntax.vue +171 -0
  116. package/components/QTab.vue +175 -0
  117. package/components/QTabPanel.vue +50 -0
  118. package/components/QTabPanels.vue +106 -0
  119. package/components/QTable.vue +575 -0
  120. package/components/QTabs.vue +188 -0
  121. package/components/QText.vue +316 -0
  122. package/components/QTextCaption.vue +122 -0
  123. package/components/QToolbar.vue +28 -0
  124. package/components/QTooltip.vue +313 -0
  125. package/components/QUploader.vue +473 -0
  126. package/components/QVideo.vue +289 -0
  127. package/components/QVirtualScroll.vue +190 -0
  128. package/components/internal/QDateCalendar.vue +153 -0
  129. package/index.ts +151 -0
  130. package/lib/breakpoints.ts +44 -0
  131. package/lib/colors.ts +24 -0
  132. package/lib/config.ts +34 -0
  133. package/lib/countries.ts +261 -0
  134. package/lib/formula.ts +630 -0
  135. package/lib/icons.ts +38 -0
  136. package/lib/loading.ts +75 -0
  137. package/lib/overlayBack.ts +124 -0
  138. package/lib/platform.ts +146 -0
  139. package/lib/primitive.ts +25 -0
  140. package/lib/q.ts +287 -0
  141. package/lib/screen.ts +105 -0
  142. package/lib/search.ts +35 -0
  143. package/lib/themeVars.ts +22 -0
  144. package/lib/useComponentProps.ts +54 -0
  145. package/lib/utils.ts +7 -0
  146. package/module.ts +89 -0
  147. package/package.json +41 -0
  148. package/scripts/generate-exports.ts +56 -0
  149. package/styles/main.css +7627 -0
  150. 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>