@dnax/ui 0.0.39 → 0.0.41
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/QSpreadsheet.vue +69 -26
- package/components/QTable.vue +189 -2
- package/package.json +2 -1
- package/styles/main.css +97 -3
|
@@ -383,12 +383,43 @@ const emit = defineEmits<{
|
|
|
383
383
|
"structure-change": [payload: { rows: Record<string, any>[]; columns: QSpreadsheetColumn[]; reason: string }]
|
|
384
384
|
}>()
|
|
385
385
|
|
|
386
|
+
// ─── Clés de ligne (`_key`) ───
|
|
387
|
+
/**
|
|
388
|
+
* Propriété injectée automatiquement sur chaque ligne : une clé stable (uuid) qui
|
|
389
|
+
* sert d'identité (suivre une ligne après un tri, `:key` côté parent, diff…).
|
|
390
|
+
* Elle résiste aux copies internes (`{ ...row }`), aux snapshots undo/redo et à
|
|
391
|
+
* `toJSON()` / `loadDocument()`. Le CSV et le presse-papiers ne l'exportent pas
|
|
392
|
+
* (ils n'itèrent que sur les colonnes déclarées).
|
|
393
|
+
*/
|
|
394
|
+
const ROW_KEY = "_key"
|
|
395
|
+
|
|
396
|
+
/** uuid v4 si le contexte le permet, sinon repli horodaté (SSR, http non sécurisé…) */
|
|
397
|
+
const newRowKey = (): string => {
|
|
398
|
+
const c = typeof globalThis !== "undefined" ? (globalThis.crypto as Crypto | undefined) : undefined
|
|
399
|
+
if (typeof c?.randomUUID === "function") return c.randomUUID()
|
|
400
|
+
return `row-${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 10)}`
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
/** Injecte `_key` (mutatif) sur les lignes qui n'en ont pas encore */
|
|
404
|
+
const ensureRowKeys = (rows: Record<string, any>[] | undefined) => {
|
|
405
|
+
if (!Array.isArray(rows)) return
|
|
406
|
+
for (const row of rows) {
|
|
407
|
+
if (!row || typeof row !== "object") continue
|
|
408
|
+
if (typeof row[ROW_KEY] !== "string" || !row[ROW_KEY]) row[ROW_KEY] = newRowKey()
|
|
409
|
+
}
|
|
410
|
+
}
|
|
411
|
+
|
|
386
412
|
// ─── Données internes (copie éditable des rows) ───
|
|
387
413
|
const state = ref<Record<string, any>[]>([])
|
|
388
414
|
watch(
|
|
389
415
|
() => props.rows,
|
|
390
416
|
(v) => {
|
|
391
|
-
if (v && v !== state.value)
|
|
417
|
+
if (v && v !== state.value) {
|
|
418
|
+
// Les clés sont injectées sur les objets reçus PUIS recopiées : parent et
|
|
419
|
+
// état interne partagent les mêmes `_key`.
|
|
420
|
+
ensureRowKeys(v)
|
|
421
|
+
state.value = v.map((r) => ({ ...r }))
|
|
422
|
+
}
|
|
392
423
|
},
|
|
393
424
|
{ immediate: true },
|
|
394
425
|
)
|
|
@@ -609,7 +640,9 @@ const cellTitle = (row: number, column: string): string | undefined => {
|
|
|
609
640
|
if (isFormulaRaw(v)) return String(v)
|
|
610
641
|
if (col?.type === "select") {
|
|
611
642
|
const opt = col.options?.find((o) => o.value === v)
|
|
612
|
-
|
|
643
|
+
// `label` peut être un nombre → `:title` attend une string
|
|
644
|
+
if (opt?.label !== undefined) return String(opt.label)
|
|
645
|
+
return v === null || v === undefined ? undefined : String(v)
|
|
613
646
|
}
|
|
614
647
|
return undefined
|
|
615
648
|
}
|
|
@@ -698,11 +731,13 @@ const startEdit = (row: number, column: string, initial?: string) => {
|
|
|
698
731
|
const raw = state.value[row]?.[column]
|
|
699
732
|
if (col?.type === "select") {
|
|
700
733
|
const opt = col.options?.find((o) => o.value === raw)
|
|
701
|
-
|
|
734
|
+
// `label` peut être un nombre (options numériques) : `draft` alimente un
|
|
735
|
+
// <input>/<textarea> et sert à `.trim()` / `.toLowerCase()` → toujours une string.
|
|
736
|
+
draft.value = String(initial ?? opt?.label ?? "")
|
|
702
737
|
} else {
|
|
703
738
|
let rawStr = String(raw ?? "")
|
|
704
739
|
if (rawStr.startsWith("'")) rawStr = rawStr.slice(1)
|
|
705
|
-
draft.value = initial ?? rawStr
|
|
740
|
+
draft.value = String(initial ?? rawStr)
|
|
706
741
|
}
|
|
707
742
|
editing.value = { row, column }
|
|
708
743
|
emit("cell-edit-start", { row, column })
|
|
@@ -755,30 +790,32 @@ const cancelEdit = () => {
|
|
|
755
790
|
nextTick(() => focusCell(cell.row, cell.column))
|
|
756
791
|
}
|
|
757
792
|
|
|
758
|
-
const coerceValue = (col: QSpreadsheetColumn | undefined, text:
|
|
759
|
-
|
|
793
|
+
const coerceValue = (col: QSpreadsheetColumn | undefined, text: unknown, old: any) => {
|
|
794
|
+
// `text` peut arriver brut (nombre, booléen…) : on normalise une fois pour toutes
|
|
795
|
+
const raw = String(text ?? "")
|
|
796
|
+
const t = raw.trim()
|
|
760
797
|
if (t === "") return null
|
|
761
798
|
// Formule A1 : conservée brute dans la cellule (booléen/select exclus)
|
|
762
799
|
if (t.startsWith("=")) {
|
|
763
800
|
if (col?.type === "boolean" || col?.type === "select") return old
|
|
764
|
-
return
|
|
801
|
+
return raw
|
|
765
802
|
}
|
|
766
803
|
if (col?.type === "number") {
|
|
767
|
-
const n = Number(
|
|
804
|
+
const n = Number(raw)
|
|
768
805
|
return Number.isNaN(n) ? old : n
|
|
769
806
|
}
|
|
770
807
|
if (col?.type === "integer") {
|
|
771
|
-
const n = Number(
|
|
808
|
+
const n = Number(raw)
|
|
772
809
|
return Number.isNaN(n) ? old : Math.trunc(n)
|
|
773
810
|
}
|
|
774
811
|
if (col?.type === "boolean") {
|
|
775
|
-
return
|
|
812
|
+
return raw === "true" ? true : raw === "false" ? false : old
|
|
776
813
|
}
|
|
777
814
|
if (col?.type === "select") {
|
|
778
|
-
const opt = col.options?.find((o) => o.label ===
|
|
815
|
+
const opt = col.options?.find((o) => String(o.label ?? "") === raw || String(o.value) === raw)
|
|
779
816
|
return opt ? opt.value : old
|
|
780
817
|
}
|
|
781
|
-
return
|
|
818
|
+
return raw
|
|
782
819
|
}
|
|
783
820
|
|
|
784
821
|
const commitEdit = async () => {
|
|
@@ -970,7 +1007,8 @@ const redo = () => {
|
|
|
970
1007
|
|
|
971
1008
|
// ─── Lignes & colonnes ───
|
|
972
1009
|
const blankRow = (): Record<string, any> => {
|
|
973
|
-
|
|
1010
|
+
// `_key` posée dès la création : la ligne est identifiable sans attendre un emit
|
|
1011
|
+
const row: Record<string, any> = { [ROW_KEY]: newRowKey() }
|
|
974
1012
|
for (const col of cols.value) row[col.name] = col.type === "boolean" ? false : ""
|
|
975
1013
|
return row
|
|
976
1014
|
}
|
|
@@ -1073,7 +1111,8 @@ const sortByColumn = (column?: string, desc = false) => {
|
|
|
1073
1111
|
if (col?.type === "boolean") return ev ? 1 : 0
|
|
1074
1112
|
if (col?.type === "select") {
|
|
1075
1113
|
const opt = col.options?.find((o) => o.value === raw)
|
|
1076
|
-
|
|
1114
|
+
// `label` peut être numérique → toString avant comparaison
|
|
1115
|
+
return String(opt?.label ?? ev ?? "").toLowerCase()
|
|
1077
1116
|
}
|
|
1078
1117
|
return String(ev).toLowerCase()
|
|
1079
1118
|
}
|
|
@@ -1108,11 +1147,7 @@ const copySelection = async () => {
|
|
|
1108
1147
|
let text: string
|
|
1109
1148
|
if (col?.type === "select") {
|
|
1110
1149
|
const opt = col.options?.find((o) => o.value === raw)
|
|
1111
|
-
text = opt
|
|
1112
|
-
? opt.label
|
|
1113
|
-
: raw === null || raw === undefined || raw === ""
|
|
1114
|
-
? ""
|
|
1115
|
-
: String(raw)
|
|
1150
|
+
text = opt ? String(opt.label ?? "") : raw === null || raw === undefined ? "" : String(raw)
|
|
1116
1151
|
} else {
|
|
1117
1152
|
// Formules : copie la valeur AFFICHÉE (évaluée), comme Excel
|
|
1118
1153
|
text = cellText(r, name)
|
|
@@ -1466,7 +1501,7 @@ const selectOptions = computed<QSpreadsheetCellOption[]>(() => {
|
|
|
1466
1501
|
if (col?.type !== "select") return []
|
|
1467
1502
|
const q = draft.value.trim().toLowerCase()
|
|
1468
1503
|
return (col.options ?? []).filter(
|
|
1469
|
-
(o) => !q || (o.label ??
|
|
1504
|
+
(o) => !q || String(o.label ?? o.value ?? "").toLowerCase().includes(q),
|
|
1470
1505
|
)
|
|
1471
1506
|
})
|
|
1472
1507
|
watch(selectOptions, () => {
|
|
@@ -1475,7 +1510,7 @@ watch(selectOptions, () => {
|
|
|
1475
1510
|
}
|
|
1476
1511
|
})
|
|
1477
1512
|
const pickOption = (opt: QSpreadsheetCellOption) => {
|
|
1478
|
-
draft.value = opt.label
|
|
1513
|
+
draft.value = String(opt.label ?? "")
|
|
1479
1514
|
commitEdit()
|
|
1480
1515
|
}
|
|
1481
1516
|
|
|
@@ -1611,7 +1646,9 @@ const FORMULA_FNS: { name: string; sig: string }[] = [
|
|
|
1611
1646
|
{ name: "DAY", sig: "date" },
|
|
1612
1647
|
{ name: "EDATE", sig: "date, months" },
|
|
1613
1648
|
]
|
|
1614
|
-
|
|
1649
|
+
// La source alimente des méthodes de chaîne (`.trim()`, `.startsWith()`) :
|
|
1650
|
+
// la valeur brute d'une cellule peut être un nombre / booléen → on coerce.
|
|
1651
|
+
const fxSource = computed(() => String((editing.value ? draft.value : fxDraft.value) ?? ""))
|
|
1615
1652
|
const fxQuery = computed(() => {
|
|
1616
1653
|
const s = fxSource.value
|
|
1617
1654
|
if (!s.trim().startsWith("=")) return ""
|
|
@@ -2084,7 +2121,8 @@ const filterValueItems = (name: string): FilterValueItem[] => {
|
|
|
2084
2121
|
if (info.blank) label = t("blanks")
|
|
2085
2122
|
else if (col?.type === "select") {
|
|
2086
2123
|
const opt = col.options?.find((o) => o.value === info.raw)
|
|
2087
|
-
label
|
|
2124
|
+
// `label` peut être numérique → normalisé (label.localeCompare plus bas)
|
|
2125
|
+
label = opt?.label === undefined ? String(info.raw) : String(opt.label)
|
|
2088
2126
|
} else label = String(info.raw)
|
|
2089
2127
|
return { key, blank: info.blank, label, count: info.count, active: allowed ? allowed.includes(key) : true }
|
|
2090
2128
|
})
|
|
@@ -2720,6 +2758,7 @@ const loadSheetIntoEngine = (idx: number) => {
|
|
|
2720
2758
|
sheetMeta.value = { ...sheetMeta.value, [key]: ext }
|
|
2721
2759
|
}
|
|
2722
2760
|
cols.value = ext.cols.map((c) => ({ ...c, type: c.type ?? "text", options: c.options ?? [] }))
|
|
2761
|
+
ensureRowKeys(s.rows)
|
|
2723
2762
|
state.value = (s.rows ?? []).map((r) => ({ ...r }))
|
|
2724
2763
|
cellFmt.value = { ...ext.formats }
|
|
2725
2764
|
rowHeights.value = { ...ext.rowHeights }
|
|
@@ -2986,9 +3025,11 @@ const findTextOf = (r: number, ci: number): string => {
|
|
|
2986
3025
|
const col = cols.value[ci]
|
|
2987
3026
|
const raw = state.value[r]?.[col?.name ?? ""]
|
|
2988
3027
|
if (!col) return ""
|
|
2989
|
-
if (col
|
|
3028
|
+
if (col?.type === "select") {
|
|
2990
3029
|
const opt = col.options?.find((o) => o.value === raw)
|
|
2991
|
-
|
|
3030
|
+
// `label` peut être un nombre : findScan fait `.toLowerCase()` sur le retour
|
|
3031
|
+
if (opt?.label !== undefined) return String(opt.label)
|
|
3032
|
+
return raw === null || raw === undefined ? "" : String(raw)
|
|
2992
3033
|
}
|
|
2993
3034
|
return raw === null || raw === undefined ? "" : String(raw)
|
|
2994
3035
|
}
|
|
@@ -3123,7 +3164,7 @@ const importCsv = (text: string, opts: { delimiter?: string; headers?: boolean }
|
|
|
3123
3164
|
data = rows
|
|
3124
3165
|
}
|
|
3125
3166
|
const rowObjs = data.map((row) => {
|
|
3126
|
-
const obj: Record<string, any> = {}
|
|
3167
|
+
const obj: Record<string, any> = { [ROW_KEY]: newRowKey() }
|
|
3127
3168
|
colsArr.forEach((c, ci) => {
|
|
3128
3169
|
obj[c.name] = ci < row.length && row[ci]!.trim() !== "" ? row[ci]! : null
|
|
3129
3170
|
})
|
|
@@ -3895,12 +3936,14 @@ defineExpose({
|
|
|
3895
3936
|
<span class="q-spreadsheet__tb-group">
|
|
3896
3937
|
<button class="q-spreadsheet__tool" type="button" :disabled="readonly || disable" title="Add row" aria-label="Add row" @click="addRow()">
|
|
3897
3938
|
<Icon :icon="icons.rows3" aria-hidden="true" />
|
|
3939
|
+
<Icon :icon="icons.plus" class="q-spreadsheet__tool-badge" aria-hidden="true" />
|
|
3898
3940
|
</button>
|
|
3899
3941
|
<button class="q-spreadsheet__tool" type="button" :disabled="readonly || disable || !selRect" title="Remove selected row(s)" aria-label="Remove rows" @click="removeSelectedRows">
|
|
3900
3942
|
<Icon :icon="icons.minus" aria-hidden="true" />
|
|
3901
3943
|
</button>
|
|
3902
3944
|
<button class="q-spreadsheet__tool" type="button" :disabled="readonly || disable" title="Add column" aria-label="Add column" @click="addColumn()">
|
|
3903
3945
|
<Icon :icon="icons.columns3" aria-hidden="true" />
|
|
3946
|
+
<Icon :icon="icons.plus" class="q-spreadsheet__tool-badge" aria-hidden="true" />
|
|
3904
3947
|
</button>
|
|
3905
3948
|
<button class="q-spreadsheet__tool" type="button" :disabled="readonly || disable || !selRect" title="Remove selected column(s)" aria-label="Remove columns" @click="removeSelectedColumns">
|
|
3906
3949
|
<Icon :icon="icons.x" aria-hidden="true" />
|
package/components/QTable.vue
CHANGED
|
@@ -49,6 +49,12 @@ interface Props {
|
|
|
49
49
|
selected?: any[]
|
|
50
50
|
/** Type de sélection */
|
|
51
51
|
selection?: "none" | "single" | "multiple"
|
|
52
|
+
/**
|
|
53
|
+
* Réordonnancement des lignes au glisser-déposer : une gouttière apparaît à gauche
|
|
54
|
+
* (poignée à tirer, ou Alt + ↑/↓ au clavier). Chaque déplacement **agit sur
|
|
55
|
+
* `rows`** : émet `update:rows` (donc compatible `v-model:rows`) + `row-reorder`.
|
|
56
|
+
*/
|
|
57
|
+
reorderableRows?: boolean
|
|
52
58
|
/** Hauteur réduite */
|
|
53
59
|
dense?: boolean
|
|
54
60
|
/** Sans ombre */
|
|
@@ -91,6 +97,7 @@ const props = withDefaults(defineProps<Props>(), {
|
|
|
91
97
|
pagination: null,
|
|
92
98
|
selected: undefined,
|
|
93
99
|
selection: "none",
|
|
100
|
+
reorderableRows: false,
|
|
94
101
|
dense: false,
|
|
95
102
|
flat: false,
|
|
96
103
|
bordered: false,
|
|
@@ -115,6 +122,8 @@ const emit = defineEmits<{
|
|
|
115
122
|
"update:pagination": [value: QTablePagination]
|
|
116
123
|
"update:selected": [value: any[]]
|
|
117
124
|
"update:sorting": [value: { column: string; descending: boolean }]
|
|
125
|
+
"update:rows": [value: any[]]
|
|
126
|
+
"row-reorder": [payload: { rows: any[]; row: any; from: number; to: number }]
|
|
118
127
|
request: [payload: { pagination: QTablePagination; getCellValue: (col: QTableColumn, row: any) => any }]
|
|
119
128
|
"row-click": [event: MouseEvent, row: any, index: number]
|
|
120
129
|
}>()
|
|
@@ -269,15 +278,135 @@ const onRowClick = (e: MouseEvent, row: any, index: number) => {
|
|
|
269
278
|
emit("row-click", e, row, index)
|
|
270
279
|
}
|
|
271
280
|
|
|
281
|
+
// ════════ Réordonnancement des lignes (glisser depuis la gouttière) ════════
|
|
282
|
+
/** Largeur de la gouttière de glissement (px) */
|
|
283
|
+
const REORDER_W = 28
|
|
284
|
+
|
|
285
|
+
const reorderEnabled = computed(() => !!props.reorderableRows)
|
|
286
|
+
/** Gouttière affichée (0 = pas de colonne) */
|
|
287
|
+
const gutterW = computed(() => (reorderEnabled.value ? REORDER_W : 0))
|
|
288
|
+
|
|
289
|
+
/** Ligne en cours de glissement + position de dépôt (index VISIBLE) */
|
|
290
|
+
const draggingRow = ref<any>(null)
|
|
291
|
+
const dropAt = ref<{ index: number; after: boolean } | null>(null)
|
|
292
|
+
|
|
293
|
+
/**
|
|
294
|
+
* Index d'une ligne dans `props.rows`. L'identité d'objet suffit : le tri
|
|
295
|
+
* (`[...rows].sort`) et la pagination (`slice`) conservent les mêmes références,
|
|
296
|
+
* et ça marche même sans `rowKey` unique.
|
|
297
|
+
*/
|
|
298
|
+
const sourceIndexOf = (row: any) => props.rows.indexOf(row)
|
|
299
|
+
|
|
300
|
+
/** Déplace `from` juste avant/après `target` dans `rows` et émet le nouvel ordre */
|
|
301
|
+
const moveRowInSource = (from: any, target: any, after: boolean) => {
|
|
302
|
+
const fi = sourceIndexOf(from)
|
|
303
|
+
const ti = sourceIndexOf(target)
|
|
304
|
+
if (fi === -1 || ti === -1 || fi === ti) return
|
|
305
|
+
|
|
306
|
+
const next = [...props.rows]
|
|
307
|
+
const [moved] = next.splice(fi, 1)
|
|
308
|
+
let to = after ? ti + 1 : ti
|
|
309
|
+
if (fi < to) to -= 1
|
|
310
|
+
next.splice(to, 0, moved)
|
|
311
|
+
|
|
312
|
+
emit("update:rows", next)
|
|
313
|
+
emit("row-reorder", { rows: next, row: moved, from: fi, to })
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
/** Ligne visée par un point de l'écran + insertion avant/après selon la moitié survolée */
|
|
317
|
+
const dropTargetAt = (x: number, y: number) => {
|
|
318
|
+
const el = typeof document !== "undefined" ? document.elementFromPoint(x, y) : null
|
|
319
|
+
const tr = (el?.closest?.("tr[data-qrow]") ?? null) as HTMLElement | null
|
|
320
|
+
if (!tr) return null
|
|
321
|
+
const index = Number(tr.dataset.qrow)
|
|
322
|
+
if (!Number.isInteger(index) || !pagedRows.value[index]) return null
|
|
323
|
+
const rect = tr.getBoundingClientRect()
|
|
324
|
+
return { index, after: y > rect.top + rect.height / 2 }
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
const onReorderPointerDown = (e: PointerEvent, row: any) => {
|
|
328
|
+
if (!reorderEnabled.value || e.button !== 0) return
|
|
329
|
+
e.preventDefault() // pas de sélection de texte pendant le glissement
|
|
330
|
+
e.stopPropagation()
|
|
331
|
+
draggingRow.value = row
|
|
332
|
+
dropAt.value = null
|
|
333
|
+
|
|
334
|
+
const onMove = (ev: PointerEvent) => {
|
|
335
|
+
dropAt.value = dropTargetAt(ev.clientX, ev.clientY)
|
|
336
|
+
}
|
|
337
|
+
const onUp = () => {
|
|
338
|
+
window.removeEventListener("pointermove", onMove)
|
|
339
|
+
window.removeEventListener("pointerup", onUp)
|
|
340
|
+
window.removeEventListener("pointercancel", onUp)
|
|
341
|
+
const target = dropAt.value
|
|
342
|
+
const targetRow = target ? pagedRows.value[target.index] : null
|
|
343
|
+
if (target && targetRow) moveRowInSource(row, targetRow, target.after)
|
|
344
|
+
draggingRow.value = null
|
|
345
|
+
dropAt.value = null
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
window.addEventListener("pointermove", onMove)
|
|
349
|
+
window.addEventListener("pointerup", onUp)
|
|
350
|
+
window.addEventListener("pointercancel", onUp)
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
/** Déplacement clavier (Alt + ↑/↓) depuis la poignée */
|
|
354
|
+
const nudgeRow = (visibleIndex: number, delta: number) => {
|
|
355
|
+
const row = pagedRows.value[visibleIndex]
|
|
356
|
+
const target = pagedRows.value[visibleIndex + delta]
|
|
357
|
+
if (!row || !target) return
|
|
358
|
+
moveRowInSource(row, target, delta > 0)
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
/** Classes d'une ligne selon le glissement en cours (source + cible) */
|
|
362
|
+
const reorderRowClass = (visibleIndex: number) => [
|
|
363
|
+
draggingRow.value !== null && draggingRow.value === pagedRows.value[visibleIndex]
|
|
364
|
+
? "q-table__row--dragging"
|
|
365
|
+
: "",
|
|
366
|
+
dropAt.value?.index === visibleIndex
|
|
367
|
+
? dropAt.value.after
|
|
368
|
+
? "q-table__row--drop-after"
|
|
369
|
+
: "q-table__row--drop-before"
|
|
370
|
+
: "",
|
|
371
|
+
]
|
|
372
|
+
|
|
373
|
+
defineExpose({
|
|
374
|
+
/**
|
|
375
|
+
* Déplace une ligne de `rows` (indices dans la source) — émet `update:rows` et
|
|
376
|
+
* `row-reorder`. Utilisable depuis un bouton « monter / descendre » :
|
|
377
|
+
* `tableRef.value?.reorderRows(2, 0)`.
|
|
378
|
+
*/
|
|
379
|
+
reorderRows: (from: number, to: number) => {
|
|
380
|
+
const len = props.rows.length
|
|
381
|
+
if (from === to || from < 0 || to < 0 || from >= len || to >= len) return
|
|
382
|
+
const next = [...props.rows]
|
|
383
|
+
const [moved] = next.splice(from, 1)
|
|
384
|
+
next.splice(to, 0, moved)
|
|
385
|
+
emit("update:rows", next)
|
|
386
|
+
emit("row-reorder", { rows: next, row: moved, from, to })
|
|
387
|
+
},
|
|
388
|
+
})
|
|
389
|
+
|
|
272
390
|
// — Rendu —
|
|
273
391
|
const alignClass = (col: QTableColumn) =>
|
|
274
392
|
col.align && col.align !== "left" ? `q-table__cell--align-${col.align}` : ""
|
|
275
393
|
|
|
276
394
|
// Une colonne épinglee à gauche → la colonne de sélection s'épingle aussi (offset 0)
|
|
277
395
|
const hasPinnedLeft = computed(() => props.columns.some((c) => c.pinned === "left"))
|
|
278
|
-
const pinnedLeftOffset = computed(
|
|
396
|
+
const pinnedLeftOffset = computed(
|
|
397
|
+
() => gutterW.value + (props.selection !== "none" ? 36 : 0),
|
|
398
|
+
)
|
|
399
|
+
/** La colonne de sélection suit la gouttière de glissement quand elle est épinglée */
|
|
400
|
+
const selectionPinnedStyle = computed(() =>
|
|
401
|
+
hasPinnedLeft.value && gutterW.value ? { left: `${gutterW.value}px` } : null,
|
|
402
|
+
)
|
|
279
403
|
|
|
280
|
-
const colspan = computed(
|
|
404
|
+
const colspan = computed(
|
|
405
|
+
() =>
|
|
406
|
+
props.columns.length +
|
|
407
|
+
(props.selection !== "none" ? 1 : 0) +
|
|
408
|
+
(gutterW.value ? 1 : 0),
|
|
409
|
+
)
|
|
281
410
|
|
|
282
411
|
const rangeLabel = computed(() => {
|
|
283
412
|
const total = sortedRows.value.length
|
|
@@ -364,10 +493,17 @@ const virtual = computed(() => {
|
|
|
364
493
|
<thead v-if="!hideHeader">
|
|
365
494
|
<slot name="header">
|
|
366
495
|
<tr>
|
|
496
|
+
<th
|
|
497
|
+
v-if="reorderEnabled"
|
|
498
|
+
class="q-table__reorder-col"
|
|
499
|
+
:class="{ 'q-table__cell--pinned-left': hasPinnedLeft }"
|
|
500
|
+
aria-hidden="true"
|
|
501
|
+
></th>
|
|
367
502
|
<th
|
|
368
503
|
v-if="selection === 'multiple'"
|
|
369
504
|
class="q-table__selection-col"
|
|
370
505
|
:class="{ 'q-table__cell--pinned-left': hasPinnedLeft }"
|
|
506
|
+
:style="selectionPinnedStyle"
|
|
371
507
|
>
|
|
372
508
|
<q-checkbox
|
|
373
509
|
:model-value="allSelected ? true : someSelected ? 'mixed' : false"
|
|
@@ -377,6 +513,13 @@ const virtual = computed(() => {
|
|
|
377
513
|
@update:model-value="toggleAll"
|
|
378
514
|
/>
|
|
379
515
|
</th>
|
|
516
|
+
<th
|
|
517
|
+
v-else-if="selection === 'single'"
|
|
518
|
+
class="q-table__selection-col"
|
|
519
|
+
:class="{ 'q-table__cell--pinned-left': hasPinnedLeft }"
|
|
520
|
+
:style="selectionPinnedStyle"
|
|
521
|
+
aria-hidden="true"
|
|
522
|
+
></th>
|
|
380
523
|
<th
|
|
381
524
|
v-for="col in columns"
|
|
382
525
|
:key="colName(col)"
|
|
@@ -417,12 +560,33 @@ const virtual = computed(() => {
|
|
|
417
560
|
<tr
|
|
418
561
|
v-for="(row, vi) in virtual.rows"
|
|
419
562
|
:key="String(getRowKey(row, virtual.start + vi))"
|
|
563
|
+
:data-qrow="virtual.start + vi"
|
|
564
|
+
:class="reorderRowClass(virtual.start + vi)"
|
|
420
565
|
@click="(e: MouseEvent) => onRowClick(e, row, virtual.start + vi)"
|
|
421
566
|
>
|
|
567
|
+
<td
|
|
568
|
+
v-if="reorderEnabled"
|
|
569
|
+
class="q-table__reorder-col"
|
|
570
|
+
:class="{ 'q-table__cell--pinned-left': hasPinnedLeft }"
|
|
571
|
+
@click.stop
|
|
572
|
+
>
|
|
573
|
+
<button
|
|
574
|
+
type="button"
|
|
575
|
+
class="q-table__drag-handle"
|
|
576
|
+
title="Drag to reorder (Alt + ↑/↓)"
|
|
577
|
+
:aria-label="'Reorder row ' + (virtual.start + vi + 1)"
|
|
578
|
+
@pointerdown="onReorderPointerDown($event, row)"
|
|
579
|
+
@keydown.alt.up.prevent.stop="nudgeRow(virtual.start + vi, -1)"
|
|
580
|
+
@keydown.alt.down.prevent.stop="nudgeRow(virtual.start + vi, 1)"
|
|
581
|
+
>
|
|
582
|
+
<Icon :icon="icons.gripVertical" aria-hidden="true" />
|
|
583
|
+
</button>
|
|
584
|
+
</td>
|
|
422
585
|
<td
|
|
423
586
|
v-if="selection === 'multiple'"
|
|
424
587
|
class="q-table__selection-col"
|
|
425
588
|
:class="{ 'q-table__cell--pinned-left': hasPinnedLeft }"
|
|
589
|
+
:style="selectionPinnedStyle"
|
|
426
590
|
@click.stop
|
|
427
591
|
>
|
|
428
592
|
<q-checkbox
|
|
@@ -436,6 +600,7 @@ const virtual = computed(() => {
|
|
|
436
600
|
v-else-if="selection === 'single'"
|
|
437
601
|
class="q-table__selection-col"
|
|
438
602
|
:class="{ 'q-table__cell--pinned-left': hasPinnedLeft }"
|
|
603
|
+
:style="selectionPinnedStyle"
|
|
439
604
|
@click.stop
|
|
440
605
|
>
|
|
441
606
|
<q-radio
|
|
@@ -476,12 +641,33 @@ const virtual = computed(() => {
|
|
|
476
641
|
<tr
|
|
477
642
|
v-for="(row, rIdx) in pagedRows"
|
|
478
643
|
:key="String(getRowKey(row, rIdx))"
|
|
644
|
+
:data-qrow="rIdx"
|
|
645
|
+
:class="reorderRowClass(rIdx)"
|
|
479
646
|
@click="(e: MouseEvent) => onRowClick(e, row, rIdx)"
|
|
480
647
|
>
|
|
648
|
+
<td
|
|
649
|
+
v-if="reorderEnabled"
|
|
650
|
+
class="q-table__reorder-col"
|
|
651
|
+
:class="{ 'q-table__cell--pinned-left': hasPinnedLeft }"
|
|
652
|
+
@click.stop
|
|
653
|
+
>
|
|
654
|
+
<button
|
|
655
|
+
type="button"
|
|
656
|
+
class="q-table__drag-handle"
|
|
657
|
+
title="Drag to reorder (Alt + ↑/↓)"
|
|
658
|
+
:aria-label="'Reorder row ' + (rIdx + 1)"
|
|
659
|
+
@pointerdown="onReorderPointerDown($event, row)"
|
|
660
|
+
@keydown.alt.up.prevent.stop="nudgeRow(rIdx, -1)"
|
|
661
|
+
@keydown.alt.down.prevent.stop="nudgeRow(rIdx, 1)"
|
|
662
|
+
>
|
|
663
|
+
<Icon :icon="icons.gripVertical" aria-hidden="true" />
|
|
664
|
+
</button>
|
|
665
|
+
</td>
|
|
481
666
|
<td
|
|
482
667
|
v-if="selection === 'multiple'"
|
|
483
668
|
class="q-table__selection-col"
|
|
484
669
|
:class="{ 'q-table__cell--pinned-left': hasPinnedLeft }"
|
|
670
|
+
:style="selectionPinnedStyle"
|
|
485
671
|
@click.stop
|
|
486
672
|
>
|
|
487
673
|
<q-checkbox
|
|
@@ -495,6 +681,7 @@ const virtual = computed(() => {
|
|
|
495
681
|
v-else-if="selection === 'single'"
|
|
496
682
|
class="q-table__selection-col"
|
|
497
683
|
:class="{ 'q-table__cell--pinned-left': hasPinnedLeft }"
|
|
684
|
+
:style="selectionPinnedStyle"
|
|
498
685
|
@click.stop
|
|
499
686
|
>
|
|
500
687
|
<q-radio
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@dnax/ui",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.41",
|
|
4
4
|
"module": "index.ts",
|
|
5
5
|
"description": "UI design System",
|
|
6
6
|
"type": "module",
|
|
@@ -61,6 +61,7 @@
|
|
|
61
61
|
"swiper": "^14.2.0",
|
|
62
62
|
"tailwind-merge": "^3.6.0",
|
|
63
63
|
"tw-animate-css": "^1.4.0",
|
|
64
|
+
"uuid": "^14.0.2",
|
|
64
65
|
"vue-router": "^5.3.0",
|
|
65
66
|
"vue-sonner": "^2.0.9"
|
|
66
67
|
}
|
package/styles/main.css
CHANGED
|
@@ -29,6 +29,23 @@ body {
|
|
|
29
29
|
--q-radius-lg: 16px;
|
|
30
30
|
}
|
|
31
31
|
|
|
32
|
+
/* ===== Échelle des z-index =====
|
|
33
|
+
Règle : un menu superposé ne doit JAMAIS passer sous un overlay. Les menus
|
|
34
|
+
téléportés dans <body> sont donc au-dessus de la couche modale.
|
|
35
|
+
1000 popups en flux (`position: absolute` dans leur champ : select,
|
|
36
|
+
autocomplete, nav-menu…) → pas concernés par les overlays
|
|
37
|
+
1999/2000 sidebar offcanvas + backdrop
|
|
38
|
+
3000 overlays modaux : dialog, bottom sheet, action sheet,
|
|
39
|
+
date-picker, country-picker, select sheet
|
|
40
|
+
3200 menus téléportés dans <body> : panneau QBtnActions, menu
|
|
41
|
+
contextuel / filtre / suggestions du tableur → --q-z-menu
|
|
42
|
+
4000 palettes tiptap · 7000 loading global · 9999 tooltip ·
|
|
43
|
+
10000 image preview
|
|
44
|
+
Surchargez --q-z-menu si votre application a un overlay plus haut que 3200. */
|
|
45
|
+
:root {
|
|
46
|
+
--q-z-menu: 3200;
|
|
47
|
+
}
|
|
48
|
+
|
|
32
49
|
/* ===== Safe-area insets (encoche & barre d'accueil) =====
|
|
33
50
|
constant() : iOS 11.0–11.2 ; env() : iOS 11.2+ et navigateurs modernes.
|
|
34
51
|
Sur desktop (Windows, macOS) les insets valent 0 (pas d'encoche). */
|
|
@@ -2505,7 +2522,7 @@ html.dark body {
|
|
|
2505
2522
|
|
|
2506
2523
|
.q-btn-actions__panel {
|
|
2507
2524
|
position: fixed;
|
|
2508
|
-
z-index:
|
|
2525
|
+
z-index: var(--q-z-menu, 3200);
|
|
2509
2526
|
padding: 4px;
|
|
2510
2527
|
background-color: #fff;
|
|
2511
2528
|
border: 1px solid rgb(0 0 0 / 0.12);
|
|
@@ -7710,6 +7727,65 @@ a.q-breadcrumbs__el:hover {
|
|
|
7710
7727
|
width: 36px;
|
|
7711
7728
|
text-align: center;
|
|
7712
7729
|
}
|
|
7730
|
+
|
|
7731
|
+
/* ─── Réordonnancement des lignes (glisser depuis la gouttière) ─── */
|
|
7732
|
+
.q-table__table .q-table__reorder-col {
|
|
7733
|
+
width: 28px;
|
|
7734
|
+
padding: 0 2px;
|
|
7735
|
+
text-align: center;
|
|
7736
|
+
}
|
|
7737
|
+
.q-table__drag-handle {
|
|
7738
|
+
display: inline-flex;
|
|
7739
|
+
align-items: center;
|
|
7740
|
+
justify-content: center;
|
|
7741
|
+
padding: 2px;
|
|
7742
|
+
border: 0;
|
|
7743
|
+
border-radius: 4px;
|
|
7744
|
+
background: transparent;
|
|
7745
|
+
color: rgb(0 0 0 / 0.35);
|
|
7746
|
+
cursor: grab;
|
|
7747
|
+
user-select: none;
|
|
7748
|
+
touch-action: none; /* le glissement ne doit pas partir en scroll tactile */
|
|
7749
|
+
}
|
|
7750
|
+
.q-table__drag-handle:hover {
|
|
7751
|
+
color: var(--primary);
|
|
7752
|
+
background: rgb(0 0 0 / 0.06);
|
|
7753
|
+
}
|
|
7754
|
+
.q-table__drag-handle:focus-visible {
|
|
7755
|
+
outline: 2px solid var(--primary);
|
|
7756
|
+
outline-offset: 1px;
|
|
7757
|
+
}
|
|
7758
|
+
.q-table__drag-handle:active {
|
|
7759
|
+
cursor: grabbing;
|
|
7760
|
+
}
|
|
7761
|
+
.q-table__drag-handle svg {
|
|
7762
|
+
width: 14px;
|
|
7763
|
+
height: 14px;
|
|
7764
|
+
}
|
|
7765
|
+
.q-table__row--dragging {
|
|
7766
|
+
opacity: 0.45;
|
|
7767
|
+
}
|
|
7768
|
+
/* Ligne d'insertion : au-dessus ou en dessous de la ligne survolée */
|
|
7769
|
+
.q-table__row--drop-before > th,
|
|
7770
|
+
.q-table__row--drop-before > td {
|
|
7771
|
+
box-shadow: inset 0 2px 0 0 var(--primary);
|
|
7772
|
+
}
|
|
7773
|
+
.q-table__row--drop-after > th,
|
|
7774
|
+
.q-table__row--drop-after > td {
|
|
7775
|
+
box-shadow: inset 0 -2px 0 0 var(--primary);
|
|
7776
|
+
}
|
|
7777
|
+
.dark .q-table__drag-handle {
|
|
7778
|
+
color: rgb(255 255 255 / 0.4);
|
|
7779
|
+
}
|
|
7780
|
+
.dark .q-table__drag-handle:hover {
|
|
7781
|
+
background: rgb(255 255 255 / 0.08);
|
|
7782
|
+
}
|
|
7783
|
+
.q-table--dark .q-table__drag-handle {
|
|
7784
|
+
color: rgb(255 255 255 / 0.4);
|
|
7785
|
+
}
|
|
7786
|
+
.q-table--dark .q-table__drag-handle:hover {
|
|
7787
|
+
background: rgb(255 255 255 / 0.08);
|
|
7788
|
+
}
|
|
7713
7789
|
.q-table--dark { --q-table-pinned-bg: #1d1d1d; }
|
|
7714
7790
|
|
|
7715
7791
|
/* ===== QInfiniteScroll ===== */
|
|
@@ -9579,6 +9655,7 @@ a.q-breadcrumbs__el:hover {
|
|
|
9579
9655
|
border-left: 1px solid var(--q-spreadsheet-border);
|
|
9580
9656
|
}
|
|
9581
9657
|
.q-spreadsheet__tool {
|
|
9658
|
+
position: relative; /* support du badge « + » des boutons d'ajout */
|
|
9582
9659
|
display: inline-flex;
|
|
9583
9660
|
align-items: center;
|
|
9584
9661
|
justify-content: center;
|
|
@@ -9603,6 +9680,22 @@ a.q-breadcrumbs__el:hover {
|
|
|
9603
9680
|
.q-spreadsheet__tool .iconify {
|
|
9604
9681
|
font-size: 17px;
|
|
9605
9682
|
}
|
|
9683
|
+
|
|
9684
|
+
/* Badge « + » des boutons d'ajout de ligne / colonne : le glyphe de fond dit
|
|
9685
|
+
l'axe (rows / columns), le + dit l'action d'ajout — sans lui, l'icône seule se
|
|
9686
|
+
lit comme « des lignes existantes », pas comme « ajouter une ligne ».
|
|
9687
|
+
(Après `.iconify` à spécificité égale → c'est cette taille qui gagne.) */
|
|
9688
|
+
.q-spreadsheet__tool .q-spreadsheet__tool-badge {
|
|
9689
|
+
position: absolute;
|
|
9690
|
+
right: 1px;
|
|
9691
|
+
bottom: 1px;
|
|
9692
|
+
padding: 1px;
|
|
9693
|
+
border-radius: 50%;
|
|
9694
|
+
font-size: 11px;
|
|
9695
|
+
background: var(--q-spreadsheet-bg, #fff);
|
|
9696
|
+
color: var(--primary, #1976d2);
|
|
9697
|
+
box-shadow: 0 0 0 1px var(--q-spreadsheet-border);
|
|
9698
|
+
}
|
|
9606
9699
|
.q-spreadsheet--dense .q-spreadsheet__tool {
|
|
9607
9700
|
width: 24px;
|
|
9608
9701
|
height: 24px;
|
|
@@ -10064,7 +10157,8 @@ a.q-breadcrumbs__el:hover {
|
|
|
10064
10157
|
/* ─── Menu contextuel / popup filtre (téléportés body) ─── */
|
|
10065
10158
|
.q-spreadsheet__ctx,
|
|
10066
10159
|
.q-spreadsheet__fpop {
|
|
10067
|
-
z-index
|
|
10160
|
+
/* téléportés : au-dessus des overlays modaux (cf. échelle des z-index) */
|
|
10161
|
+
z-index: var(--q-z-menu, 3200);
|
|
10068
10162
|
min-width: 214px;
|
|
10069
10163
|
max-width: 300px;
|
|
10070
10164
|
padding: 5px;
|
|
@@ -10328,7 +10422,7 @@ a.q-breadcrumbs__el:hover {
|
|
|
10328
10422
|
}
|
|
10329
10423
|
.q-spreadsheet__fnsug--fx {
|
|
10330
10424
|
position: fixed;
|
|
10331
|
-
z-index:
|
|
10425
|
+
z-index: calc(var(--q-z-menu, 3200) + 30);
|
|
10332
10426
|
border: 1px solid var(--border, rgb(0 0 0 / 0.12));
|
|
10333
10427
|
border-radius: 8px;
|
|
10334
10428
|
box-shadow: 0 10px 26px rgb(0 0 0 / 0.16);
|