@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,491 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
// QDataGrid — grille type tableur (inspirée de MUI X Data Grid) :
|
|
3
|
+
// sélection de cellule au clavier (flèches, Tab), édition en place (Entrée/F2,
|
|
4
|
+
// double-clic, ou frappe directe), et formules style spreadsheet sur les colonnes
|
|
5
|
+
// allow-formulas : =price * quantity, =SUM(E1:E3), =E4 * $B$5 …
|
|
6
|
+
export interface QDataGridColumn {
|
|
7
|
+
/** Clé de la propriété dans la ligne */
|
|
8
|
+
name: string
|
|
9
|
+
label?: string
|
|
10
|
+
type?: "string" | "number" | "boolean"
|
|
11
|
+
/** Largeur de colonne (nombre px ou chaîne CSS) */
|
|
12
|
+
width?: number | string
|
|
13
|
+
align?: "left" | "center" | "right"
|
|
14
|
+
/** La cellule est éditable (Entrée / double-clic / frappe) */
|
|
15
|
+
editable?: boolean
|
|
16
|
+
/** Les valeurs "=…" sont évaluées comme formules (sinon chaîne littérale) */
|
|
17
|
+
allowFormulas?: boolean
|
|
18
|
+
/** Formatteur de la valeur affichée (après évaluation) */
|
|
19
|
+
format?: (val: any) => any
|
|
20
|
+
headerClass?: string
|
|
21
|
+
headerStyle?: string
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export interface QDataGridCell {
|
|
25
|
+
row: number
|
|
26
|
+
column: string
|
|
27
|
+
}
|
|
28
|
+
</script>
|
|
29
|
+
|
|
30
|
+
<script setup lang="ts">
|
|
31
|
+
import { computed, nextTick, ref, watch } from "vue"
|
|
32
|
+
import { cn } from "../lib/utils"
|
|
33
|
+
import { evaluateFormula, FormulaError, isError } from "../lib/formula"
|
|
34
|
+
import type { FormulaValue } from "../lib/formula"
|
|
35
|
+
import { radiusStyle, useRadius } from "../lib/useComponentProps"
|
|
36
|
+
import type { RadiusProp } from "../lib/useComponentProps"
|
|
37
|
+
|
|
38
|
+
interface Props {
|
|
39
|
+
/** Lignes (v-model:rows) */
|
|
40
|
+
rows?: Record<string, any>[]
|
|
41
|
+
/** Colonnes ({ name, label, type, width, align, editable, allowFormulas, format, headerClass, headerStyle }) */
|
|
42
|
+
columns?: QDataGridColumn[]
|
|
43
|
+
/** Hauteur du conteneur scrollable (nombre px ou chaîne CSS, défaut "320px") */
|
|
44
|
+
height?: string | number
|
|
45
|
+
/** Hauteur réduite */
|
|
46
|
+
dense?: boolean
|
|
47
|
+
/** Sans ombre */
|
|
48
|
+
flat?: boolean
|
|
49
|
+
/** Bordure */
|
|
50
|
+
bordered?: boolean
|
|
51
|
+
/** Séparateurs : horizontal | vertical | cell | none */
|
|
52
|
+
separator?: "horizontal" | "vertical" | "cell" | "none"
|
|
53
|
+
dark?: boolean
|
|
54
|
+
/** Désactive l'édition (lecture seule) */
|
|
55
|
+
readonly?: boolean
|
|
56
|
+
/** Cellules sélectionnées (v-model:selected) */
|
|
57
|
+
selected?: QDataGridCell[]
|
|
58
|
+
/** Classe CSS appliquée à toutes les cellules d'en-tête */
|
|
59
|
+
headerClass?: string
|
|
60
|
+
/** Style CSS appliqué à toutes les cellules d'en-tête */
|
|
61
|
+
headerStyle?: string
|
|
62
|
+
/** Coins arrondis (échelle xs|sm|md|lg ou none) — défaut : composantProps */
|
|
63
|
+
radius?: RadiusProp
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
const props = withDefaults(defineProps<Props>(), {
|
|
67
|
+
rows: () => [],
|
|
68
|
+
columns: () => [],
|
|
69
|
+
height: "320px",
|
|
70
|
+
dense: false,
|
|
71
|
+
flat: false,
|
|
72
|
+
bordered: false,
|
|
73
|
+
separator: "horizontal",
|
|
74
|
+
dark: false,
|
|
75
|
+
readonly: false,
|
|
76
|
+
selected: undefined,
|
|
77
|
+
headerClass: "",
|
|
78
|
+
headerStyle: "",
|
|
79
|
+
})
|
|
80
|
+
|
|
81
|
+
const emit = defineEmits<{
|
|
82
|
+
"update:rows": [value: Record<string, any>[]]
|
|
83
|
+
"update:selected": [value: QDataGridCell[]]
|
|
84
|
+
"cell-click": [payload: { row: number; column: string; value: any }]
|
|
85
|
+
"cell-change": [payload: { row: number; column: string; oldValue: any; newValue: any; rows: Record<string, any>[] }]
|
|
86
|
+
"cell-edit-start": [payload: { row: number; column: string }]
|
|
87
|
+
"cell-edit-end": [payload: { row: number; column: string; canceled: boolean }]
|
|
88
|
+
}>()
|
|
89
|
+
|
|
90
|
+
// ─── Données internes (copie éditable des rows) ───
|
|
91
|
+
const state = ref<Record<string, any>[]>([])
|
|
92
|
+
watch(
|
|
93
|
+
() => props.rows,
|
|
94
|
+
(v) => {
|
|
95
|
+
if (v && v !== state.value) state.value = v.map((r) => ({ ...r }))
|
|
96
|
+
},
|
|
97
|
+
{ immediate: true },
|
|
98
|
+
)
|
|
99
|
+
|
|
100
|
+
// ─── Sélection ───
|
|
101
|
+
const sel = ref<QDataGridCell[]>([])
|
|
102
|
+
watch(
|
|
103
|
+
() => props.selected,
|
|
104
|
+
(v) => {
|
|
105
|
+
if (v) sel.value = v
|
|
106
|
+
},
|
|
107
|
+
{ immediate: true, deep: true },
|
|
108
|
+
)
|
|
109
|
+
|
|
110
|
+
const isSelected = (row: number, column: string) =>
|
|
111
|
+
sel.value.some((c) => c.row === row && c.column === column)
|
|
112
|
+
|
|
113
|
+
const select = (row: number, column: string) => {
|
|
114
|
+
sel.value = [{ row, column }]
|
|
115
|
+
emit("update:selected", sel.value)
|
|
116
|
+
nextTick(focusCell)
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
const moveSelection = (dr: number, dc: number) => {
|
|
120
|
+
if (!sel.value.length || !props.columns.length) return
|
|
121
|
+
const { row, column } = sel.value[0]!
|
|
122
|
+
const ci = props.columns.findIndex((c) => c.name === column)
|
|
123
|
+
const nr = Math.max(0, Math.min(state.value.length - 1, row + dr))
|
|
124
|
+
const nc = Math.max(0, Math.min(props.columns.length - 1, ci + dc))
|
|
125
|
+
select(nr, props.columns[nc]!.name)
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
const escAttr = (s: string) => String(s).replace(/"/g, '\\"')
|
|
129
|
+
const focusCell = () => {
|
|
130
|
+
if (!sel.value.length) return
|
|
131
|
+
const { row, column } = sel.value[0]!
|
|
132
|
+
const td = scrollEl.value?.querySelector<HTMLElement>(
|
|
133
|
+
`td[data-row="${row}"][data-col="${escAttr(column)}"]`,
|
|
134
|
+
)
|
|
135
|
+
td?.focus()
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
// ─── Évaluation des cellules (formules + graphe de dépendances) ───
|
|
139
|
+
const colIndex = (name: string) =>
|
|
140
|
+
props.columns.findIndex((c) => c.name.toLowerCase() === name.toLowerCase())
|
|
141
|
+
|
|
142
|
+
const evaluated = computed<FormulaValue[][]>(() => {
|
|
143
|
+
const memo = new Map<string, FormulaValue>()
|
|
144
|
+
const visiting = new Set<string>()
|
|
145
|
+
|
|
146
|
+
const get = (r: number, c: number): FormulaValue => {
|
|
147
|
+
const key = r + ":" + c
|
|
148
|
+
if (memo.has(key)) return memo.get(key)!
|
|
149
|
+
if (visiting.has(key)) return new FormulaError("#CYCLE!")
|
|
150
|
+
const col = props.columns[c]
|
|
151
|
+
const row = state.value[r]
|
|
152
|
+
if (!col || !row) return new FormulaError("#REF!")
|
|
153
|
+
const raw = row[col.name]
|
|
154
|
+
let val: FormulaValue = raw ?? null
|
|
155
|
+
if (typeof raw === "string" && raw.startsWith("=") && col.allowFormulas) {
|
|
156
|
+
visiting.add(key)
|
|
157
|
+
val = evaluateFormula(raw, {
|
|
158
|
+
row: r,
|
|
159
|
+
resolveField: (name) => {
|
|
160
|
+
const i = colIndex(name)
|
|
161
|
+
return i === -1 ? undefined : get(r, i)
|
|
162
|
+
},
|
|
163
|
+
resolveCell: (rr, cc) => get(rr, cc),
|
|
164
|
+
})
|
|
165
|
+
visiting.delete(key)
|
|
166
|
+
}
|
|
167
|
+
memo.set(key, val)
|
|
168
|
+
return val
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
return state.value.map((_, r) => props.columns.map((_, c) => get(r, c)))
|
|
172
|
+
})
|
|
173
|
+
|
|
174
|
+
const cellValue = (r: number, c: number) => evaluated.value[r]?.[c]
|
|
175
|
+
const cellText = (r: number, c: number): string => {
|
|
176
|
+
const v = cellValue(r, c)
|
|
177
|
+
const col = props.columns[c]
|
|
178
|
+
if (isError(v)) return v.toString()
|
|
179
|
+
let out = v
|
|
180
|
+
if (col?.format) out = col.format(v)
|
|
181
|
+
if (out === null || out === undefined || out === "") return ""
|
|
182
|
+
return String(out)
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
const cellTitle = (r: number, c: number): string | undefined => {
|
|
186
|
+
const col = props.columns[c]
|
|
187
|
+
if (!col) return undefined
|
|
188
|
+
const raw = state.value[r]?.[col.name]
|
|
189
|
+
return typeof raw === "string" && raw.startsWith("=") && col.allowFormulas ? raw : undefined
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
// ─── Édition ───
|
|
193
|
+
const editing = ref<{ row: number; column: string } | null>(null)
|
|
194
|
+
const draft = ref("")
|
|
195
|
+
const scrollEl = ref<HTMLElement | null>(null)
|
|
196
|
+
const editorEl = ref<HTMLElement | null>(null)
|
|
197
|
+
const editInputEl = ref<HTMLInputElement | null>(null)
|
|
198
|
+
const editAreaEl = ref<HTMLTextAreaElement | null>(null)
|
|
199
|
+
const editorStyle = ref<Record<string, string>>({})
|
|
200
|
+
|
|
201
|
+
const columnOf = (name: string) => props.columns.find((c) => c.name === name)
|
|
202
|
+
const isFormulaDraft = computed(() => {
|
|
203
|
+
const col = editing.value ? columnOf(editing.value.column) : undefined
|
|
204
|
+
return !!col?.allowFormulas && draft.value.startsWith("=")
|
|
205
|
+
})
|
|
206
|
+
const draftLines = computed(() => Math.min(5, Math.max(1, draft.value.split("\n").length)))
|
|
207
|
+
const editorInputType = computed(() => {
|
|
208
|
+
const col = editing.value ? columnOf(editing.value.column) : undefined
|
|
209
|
+
return col?.type === "number" ? "number" : "text"
|
|
210
|
+
})
|
|
211
|
+
|
|
212
|
+
const startEdit = (row: number, column: string, initial?: string) => {
|
|
213
|
+
if (props.readonly) return
|
|
214
|
+
const col = columnOf(column)
|
|
215
|
+
if (!col?.editable) return
|
|
216
|
+
const raw = state.value[row]?.[column]
|
|
217
|
+
draft.value =
|
|
218
|
+
initial ??
|
|
219
|
+
(typeof raw === "string" && raw.startsWith("=") ? raw : String(raw ?? ""))
|
|
220
|
+
editing.value = { row, column }
|
|
221
|
+
emit("cell-edit-start", { row, column })
|
|
222
|
+
nextTick(positionEditor)
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
const positionEditor = () => {
|
|
226
|
+
if (!editing.value || !scrollEl.value) return
|
|
227
|
+
const td = scrollEl.value.querySelector<HTMLElement>(
|
|
228
|
+
`td[data-row="${editing.value.row}"][data-col="${escAttr(editing.value.column)}"]`,
|
|
229
|
+
)
|
|
230
|
+
if (!td) return
|
|
231
|
+
const sc = scrollEl.value.getBoundingClientRect()
|
|
232
|
+
const tr = td.getBoundingClientRect()
|
|
233
|
+
// L'éditeur de formule s'élargit avec le draft (monospace ≈ 7.6px/car),
|
|
234
|
+
// sans dépasser le bord droit du conteneur.
|
|
235
|
+
const formulaW = isFormulaDraft.value
|
|
236
|
+
? Math.min(480, Math.max(180, draft.value.length * 7.6 + 30))
|
|
237
|
+
: 0
|
|
238
|
+
const left = tr.left - sc.left + scrollEl.value.scrollLeft
|
|
239
|
+
const maxW = sc.width - (tr.left - sc.left) - 4
|
|
240
|
+
editorStyle.value = {
|
|
241
|
+
left: left + "px",
|
|
242
|
+
top: tr.top - sc.top + scrollEl.value.scrollTop + "px",
|
|
243
|
+
width: Math.min(maxW, Math.max(tr.width, formulaW)) + "px",
|
|
244
|
+
minHeight: tr.height + "px",
|
|
245
|
+
}
|
|
246
|
+
nextTick(() => {
|
|
247
|
+
;(isFormulaDraft.value ? editAreaEl.value : editInputEl.value)?.focus()
|
|
248
|
+
})
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
const cancelEdit = () => {
|
|
252
|
+
if (!editing.value) return
|
|
253
|
+
const cell = { ...editing.value }
|
|
254
|
+
editing.value = null
|
|
255
|
+
emit("cell-edit-end", { ...cell, canceled: true })
|
|
256
|
+
nextTick(focusCell)
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
const commitEdit = () => {
|
|
260
|
+
if (!editing.value) return
|
|
261
|
+
const { row, column } = editing.value
|
|
262
|
+
const col = columnOf(column)
|
|
263
|
+
const old = state.value[row]?.[column]
|
|
264
|
+
const text = draft.value
|
|
265
|
+
let next: any
|
|
266
|
+
if (text.trim() === "") next = null
|
|
267
|
+
else if (text.startsWith("=") && col?.allowFormulas) next = text
|
|
268
|
+
else if (col?.type === "number") {
|
|
269
|
+
const n = Number(text)
|
|
270
|
+
next = Number.isNaN(n) ? old : n
|
|
271
|
+
} else if (col?.type === "boolean") {
|
|
272
|
+
next = text === "true" ? true : text === "false" ? false : old
|
|
273
|
+
} else next = text
|
|
274
|
+
|
|
275
|
+
editing.value = null
|
|
276
|
+
emit("cell-edit-end", { row, column, canceled: false })
|
|
277
|
+
nextTick(focusCell)
|
|
278
|
+
|
|
279
|
+
if (next !== old) {
|
|
280
|
+
const nextRow = { ...state.value[row], [column]: next }
|
|
281
|
+
const nextRows = [...state.value]
|
|
282
|
+
nextRows[row] = nextRow
|
|
283
|
+
state.value = nextRows
|
|
284
|
+
emit("update:rows", nextRows)
|
|
285
|
+
emit("cell-change", { row, column, oldValue: old, newValue: next, rows: nextRows })
|
|
286
|
+
}
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
const clearCell = (row: number, column: string) => {
|
|
290
|
+
const col = columnOf(column)
|
|
291
|
+
if (!col?.editable || props.readonly) return
|
|
292
|
+
const old = state.value[row]?.[column]
|
|
293
|
+
if (old === null || old === undefined || old === "") return
|
|
294
|
+
const nextRow = { ...state.value[row], [column]: null }
|
|
295
|
+
const nextRows = [...state.value]
|
|
296
|
+
nextRows[row] = nextRow
|
|
297
|
+
state.value = nextRows
|
|
298
|
+
emit("update:rows", nextRows)
|
|
299
|
+
emit("cell-change", { row, column, oldValue: old, newValue: null, rows: nextRows })
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
// ─── Clavier ───
|
|
303
|
+
const onKeydown = (e: KeyboardEvent) => {
|
|
304
|
+
if (editing.value || e.defaultPrevented) return
|
|
305
|
+
if (!sel.value.length) return
|
|
306
|
+
const { row, column } = sel.value[0]!
|
|
307
|
+
const col = columnOf(column)
|
|
308
|
+
const colIdx = props.columns.findIndex((c) => c.name === column)
|
|
309
|
+
|
|
310
|
+
if (e.key === "ArrowUp") return moveSelection(-1, 0)
|
|
311
|
+
if (e.key === "ArrowDown") return moveSelection(1, 0)
|
|
312
|
+
if (e.key === "ArrowLeft") return moveSelection(0, -1)
|
|
313
|
+
if (e.key === "ArrowRight") return moveSelection(0, 1)
|
|
314
|
+
if (e.key === "Enter" || e.key === "F2") {
|
|
315
|
+
e.preventDefault()
|
|
316
|
+
if (col?.editable && !props.readonly) startEdit(row, column)
|
|
317
|
+
return
|
|
318
|
+
}
|
|
319
|
+
if (e.key === "Tab") {
|
|
320
|
+
e.preventDefault()
|
|
321
|
+
moveSelection(0, 1)
|
|
322
|
+
return
|
|
323
|
+
}
|
|
324
|
+
if (e.key === "Delete" || e.key === "Backspace") {
|
|
325
|
+
e.preventDefault()
|
|
326
|
+
clearCell(row, column)
|
|
327
|
+
return
|
|
328
|
+
}
|
|
329
|
+
if ((e.key === "c" || e.key === "C") && (e.ctrlKey || e.metaKey)) {
|
|
330
|
+
e.preventDefault()
|
|
331
|
+
copySelected()
|
|
332
|
+
return
|
|
333
|
+
}
|
|
334
|
+
// Frappe directe : démarre l'édition
|
|
335
|
+
if (e.key.length === 1 && !e.ctrlKey && !e.metaKey && !e.altKey) {
|
|
336
|
+
if (col?.editable && !props.readonly) {
|
|
337
|
+
e.preventDefault()
|
|
338
|
+
startEdit(row, column, e.key)
|
|
339
|
+
}
|
|
340
|
+
}
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
const onEditKeydown = (e: KeyboardEvent) => {
|
|
344
|
+
if (e.key === "Enter") {
|
|
345
|
+
if (isFormulaDraft.value && e.shiftKey) return // retour ligne dans la formule
|
|
346
|
+
e.preventDefault()
|
|
347
|
+
commitEdit()
|
|
348
|
+
moveSelection(1, 0) // Entrée commit puis descend (convention tableur)
|
|
349
|
+
} else if (e.key === "Escape") {
|
|
350
|
+
e.preventDefault()
|
|
351
|
+
cancelEdit()
|
|
352
|
+
} else if (e.key === "Tab") {
|
|
353
|
+
e.preventDefault()
|
|
354
|
+
commitEdit()
|
|
355
|
+
moveSelection(0, 1)
|
|
356
|
+
}
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
// Quand le draft devient une formule (=…), l'éditeur bascule input → textarea :
|
|
360
|
+
// le nouveau champ doit reprendre le focus.
|
|
361
|
+
watch(isFormulaDraft, () => {
|
|
362
|
+
if (!editing.value) return
|
|
363
|
+
nextTick(() => {
|
|
364
|
+
;(isFormulaDraft.value ? editAreaEl.value : editInputEl.value)?.focus()
|
|
365
|
+
})
|
|
366
|
+
})
|
|
367
|
+
|
|
368
|
+
const copySelected = async () => {
|
|
369
|
+
if (!sel.value.length) return
|
|
370
|
+
const { row, column } = sel.value[0]!
|
|
371
|
+
const ci = props.columns.findIndex((c) => c.name === column)
|
|
372
|
+
const text = cellText(row, ci)
|
|
373
|
+
try {
|
|
374
|
+
await navigator.clipboard.writeText(text)
|
|
375
|
+
} catch {
|
|
376
|
+
/* presse-papiers indisponible */
|
|
377
|
+
}
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
const onCellClick = (row: number, column: string) => {
|
|
381
|
+
select(row, column)
|
|
382
|
+
emit("cell-click", { row, column, value: state.value[row]?.[column] })
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
const onScroll = () => {
|
|
386
|
+
if (editing.value) positionEditor()
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
// ─── Styles ───
|
|
390
|
+
const effectiveRadius = useRadius("QDataGrid", () => props.radius)
|
|
391
|
+
|
|
392
|
+
const rootClasses = computed(() =>
|
|
393
|
+
cn(
|
|
394
|
+
"q-data-grid",
|
|
395
|
+
props.flat && "q-data-grid--flat",
|
|
396
|
+
props.bordered && "q-data-grid--bordered",
|
|
397
|
+
props.dark && "q-data-grid--dark",
|
|
398
|
+
props.dense && "q-data-grid--dense",
|
|
399
|
+
`q-data-grid--separator-${props.separator}`,
|
|
400
|
+
),
|
|
401
|
+
)
|
|
402
|
+
|
|
403
|
+
const cellClasses = (r: number, col: QDataGridColumn, c: number) =>
|
|
404
|
+
cn(
|
|
405
|
+
"q-data-grid__cell",
|
|
406
|
+
col.type === "number" && "q-data-grid__cell--number",
|
|
407
|
+
col.align === "center" && "q-table__cell--align-center",
|
|
408
|
+
col.align === "right" && "q-table__cell--align-right",
|
|
409
|
+
isError(cellValue(r, c)) && "q-data-grid__cell--error",
|
|
410
|
+
isSelected(r, col.name) && "q-data-grid__cell--selected",
|
|
411
|
+
editing.value?.row === r && editing.value?.column === col.name && "q-data-grid__cell--editing",
|
|
412
|
+
)
|
|
413
|
+
|
|
414
|
+
const colWidth = (col: QDataGridColumn) =>
|
|
415
|
+
col.width === undefined ? undefined : typeof col.width === "number" ? col.width + "px" : col.width
|
|
416
|
+
|
|
417
|
+
const heightCss = computed(() =>
|
|
418
|
+
typeof props.height === "number" ? props.height + "px" : props.height,
|
|
419
|
+
)
|
|
420
|
+
</script>
|
|
421
|
+
|
|
422
|
+
<template>
|
|
423
|
+
<div class="q-data-grid" :class="rootClasses" :style="radiusStyle(effectiveRadius)">
|
|
424
|
+
<div
|
|
425
|
+
ref="scrollEl"
|
|
426
|
+
class="q-data-grid__scroll"
|
|
427
|
+
:style="{ height: heightCss }"
|
|
428
|
+
@scroll="onScroll"
|
|
429
|
+
@keydown="onKeydown"
|
|
430
|
+
>
|
|
431
|
+
<table class="q-table__table q-data-grid__table">
|
|
432
|
+
<thead>
|
|
433
|
+
<tr>
|
|
434
|
+
<th
|
|
435
|
+
v-for="col in columns"
|
|
436
|
+
:key="col.name"
|
|
437
|
+
:class="[col.headerClass, headerClass]"
|
|
438
|
+
:style="[{ width: colWidth(col) }, col.headerStyle, headerStyle]"
|
|
439
|
+
>
|
|
440
|
+
{{ col.label ?? col.name }}
|
|
441
|
+
</th>
|
|
442
|
+
</tr>
|
|
443
|
+
</thead>
|
|
444
|
+
<tbody>
|
|
445
|
+
<tr v-for="(row, r) in state" :key="r">
|
|
446
|
+
<td
|
|
447
|
+
v-for="(col, c) in columns"
|
|
448
|
+
:key="col.name"
|
|
449
|
+
:data-row="r"
|
|
450
|
+
:data-col="col.name"
|
|
451
|
+
:tabindex="isSelected(r, col.name) ? 0 : -1"
|
|
452
|
+
:class="cellClasses(r, col, c)"
|
|
453
|
+
:title="cellTitle(r, c)"
|
|
454
|
+
@click="onCellClick(r, col.name)"
|
|
455
|
+
@dblclick="startEdit(r, col.name)"
|
|
456
|
+
>
|
|
457
|
+
{{ cellText(r, c) }}
|
|
458
|
+
</td>
|
|
459
|
+
</tr>
|
|
460
|
+
</tbody>
|
|
461
|
+
</table>
|
|
462
|
+
|
|
463
|
+
<div
|
|
464
|
+
v-if="editing"
|
|
465
|
+
ref="editorEl"
|
|
466
|
+
class="q-data-grid__editor"
|
|
467
|
+
:class="{ 'q-data-grid__editor--formula': isFormulaDraft }"
|
|
468
|
+
:style="editorStyle"
|
|
469
|
+
@mousedown.stop
|
|
470
|
+
>
|
|
471
|
+
<textarea
|
|
472
|
+
v-if="isFormulaDraft"
|
|
473
|
+
ref="editAreaEl"
|
|
474
|
+
v-model="draft"
|
|
475
|
+
:rows="draftLines"
|
|
476
|
+
spellcheck="false"
|
|
477
|
+
@keydown.stop="onEditKeydown"
|
|
478
|
+
@blur="commitEdit"
|
|
479
|
+
/>
|
|
480
|
+
<input
|
|
481
|
+
v-else
|
|
482
|
+
ref="editInputEl"
|
|
483
|
+
v-model="draft"
|
|
484
|
+
:type="editorInputType"
|
|
485
|
+
@keydown.stop="onEditKeydown"
|
|
486
|
+
@blur="commitEdit"
|
|
487
|
+
/>
|
|
488
|
+
</div>
|
|
489
|
+
</div>
|
|
490
|
+
</div>
|
|
491
|
+
</template>
|