@dnax/ui 0.0.76 → 0.0.78
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 +32 -9
- package/lib/cellSize.test.ts +39 -1
- package/lib/cellSize.ts +36 -0
- package/lib/spreadsheetDisplay.test.ts +49 -0
- package/lib/spreadsheetDisplay.ts +42 -0
- package/package.json +1 -1
- package/styles/main.css +9 -9
|
@@ -234,7 +234,8 @@ import {
|
|
|
234
234
|
} from "../lib/spreadsheet"
|
|
235
235
|
import { checkValidation, type ArkCheck } from "../lib/spreadsheetValidation"
|
|
236
236
|
import { pageInfo } from "../lib/pagination"
|
|
237
|
-
import {
|
|
237
|
+
import { fxTextFor, searchTextFor } from "../lib/spreadsheetDisplay"
|
|
238
|
+
import { cellFontFor, cellSizePx, cellWidthFor, headerMetricsFor } from "../lib/cellSize"
|
|
238
239
|
import type { CellSizePreset } from "../lib/cellSize"
|
|
239
240
|
import { attachmentsOf as fileAttachmentsOf, fileIconKind, formatFileSize, isAttachment, selectFiles } from "../lib/fileList"
|
|
240
241
|
import type { FileIconKind } from "../lib/fileList"
|
|
@@ -672,11 +673,19 @@ const emit = defineEmits<{
|
|
|
672
673
|
const cellHeightPx = computed(() => props.rowHeight ?? cellSizePx(props.sizeCell))
|
|
673
674
|
const cellWidthPx = computed(() => props.defaultColWidth ?? cellWidthFor(cellHeightPx.value))
|
|
674
675
|
const cellFontPx = computed(() => cellFontFor(cellHeightPx.value))
|
|
676
|
+
/** En-têtes accordés aux cellules (hauteur, gouttière, polices) */
|
|
677
|
+
const headerMetrics = computed(() => headerMetricsFor(cellHeightPx.value))
|
|
675
678
|
|
|
676
679
|
/** Variables CSS de la grille (police des cellules + hauteur de repli) */
|
|
677
680
|
const cellSizeStyle = computed(() => ({
|
|
678
681
|
"--q-spreadsheet-font": `${cellFontPx.value}px`,
|
|
679
682
|
"--q-spreadsheet-row-height": `${cellHeightPx.value}px`,
|
|
683
|
+
// En-têtes : mêmes proportions que les cellules
|
|
684
|
+
"--q-spreadsheet-head-height": `${headerMetrics.value.height}px`,
|
|
685
|
+
"--q-spreadsheet-head-font": `${headerMetrics.value.labelFont}px`,
|
|
686
|
+
"--q-spreadsheet-head-letter-font": `${headerMetrics.value.letterFont}px`,
|
|
687
|
+
"--q-spreadsheet-row-font": `${headerMetrics.value.rowFont}px`,
|
|
688
|
+
"--q-spreadsheet-gutter-width": `${headerMetrics.value.gutter}px`,
|
|
680
689
|
}))
|
|
681
690
|
|
|
682
691
|
// ─── Clés de ligne (`_key`) ───
|
|
@@ -913,12 +922,14 @@ const choiceText = (col: QSpreadsheetColumn | undefined, raw: any): string => {
|
|
|
913
922
|
|
|
914
923
|
// ─── Sélection (ancre + plage) ───
|
|
915
924
|
const sel = ref<QSpreadsheetSelection | null>(null)
|
|
925
|
+
// `immediate` : une cellule **présélectionnée** (`:selected`) doit être active dès le premier
|
|
926
|
+
// rendu (SSR compris) — sans ça, elle était ignorée jusqu'au premier changement de prop.
|
|
916
927
|
watch(
|
|
917
928
|
() => props.selected,
|
|
918
929
|
(v) => {
|
|
919
930
|
if (v !== sel.value) sel.value = v ? { ...v } : null
|
|
920
931
|
},
|
|
921
|
-
{ deep: true },
|
|
932
|
+
{ deep: true, immediate: true },
|
|
922
933
|
)
|
|
923
934
|
const setSel = (s: QSpreadsheetSelection | null) => {
|
|
924
935
|
sel.value = s
|
|
@@ -2339,13 +2350,17 @@ const fxInputEl = ref<HTMLInputElement | null>(null)
|
|
|
2339
2350
|
|
|
2340
2351
|
const syncFx = () => {
|
|
2341
2352
|
const s = sel.value
|
|
2353
|
+
const col = s ? colOf(s.column) : undefined
|
|
2342
2354
|
const raw =
|
|
2343
2355
|
s && s.row >= 0 && s.row < state.value.length
|
|
2344
2356
|
? state.value[s.row]?.[s.column]
|
|
2345
2357
|
: undefined
|
|
2346
|
-
|
|
2358
|
+
// Fichier joint : rien à montrer en fx (`String(raw)` affichait « [object Object]… »)
|
|
2359
|
+
fxDraft.value = fxTextFor(raw, col?.type)
|
|
2347
2360
|
}
|
|
2348
|
-
|
|
2361
|
+
// `immediate` : avec une cellule présélectionnée (`selected`), la barre fx affiche la
|
|
2362
|
+
// valeur dès le premier rendu (et en SSR) au lieu de rester vide jusqu'au premier clic.
|
|
2363
|
+
watch([sel, state], syncFx, { immediate: true })
|
|
2349
2364
|
|
|
2350
2365
|
const fxCanEdit = computed(() => {
|
|
2351
2366
|
if (!sel.value) return false
|
|
@@ -2356,10 +2371,17 @@ const fxCanEdit = computed(() => {
|
|
|
2356
2371
|
!!col &&
|
|
2357
2372
|
col.type !== "boolean" &&
|
|
2358
2373
|
col.type !== "select" &&
|
|
2359
|
-
col.type !== "multiselect"
|
|
2374
|
+
col.type !== "multiselect" &&
|
|
2375
|
+
col.type !== "attachment"
|
|
2360
2376
|
)
|
|
2361
2377
|
})
|
|
2362
2378
|
|
|
2379
|
+
/** Placeholder de la barre fx : vide pour un fichier joint (rien à éditer ni à montrer) */
|
|
2380
|
+
const fxPlaceholderText = computed(() => {
|
|
2381
|
+
if (sel.value && colOf(sel.value.column)?.type === "attachment") return ""
|
|
2382
|
+
return fxCanEdit.value ? t("fxPlaceholder") : t("fxNoSel")
|
|
2383
|
+
})
|
|
2384
|
+
|
|
2363
2385
|
/**
|
|
2364
2386
|
* Aperçu **live** du résultat de la formule en cours de saisie dans la barre fx.
|
|
2365
2387
|
* Évaluée dans le contexte de la ligne sélectionnée (références par nom de colonne
|
|
@@ -2833,8 +2855,8 @@ const clearFormatSelection = () => {
|
|
|
2833
2855
|
// ════════ Freeze panes (gel lignes / colonnes) ════════
|
|
2834
2856
|
const frozenRowsN = computed(() => Math.max(0, Math.min(props.frozenRows ?? 0, state.value.length)))
|
|
2835
2857
|
const frozenColsN = computed(() => Math.max(0, Math.min(props.frozenCols ?? 0, cols.value.length)))
|
|
2836
|
-
const gutterW = computed(() => (props.showRowNumbers ?
|
|
2837
|
-
const headH = computed(() => (props.showColumnHeaders ?
|
|
2858
|
+
const gutterW = computed(() => (props.showRowNumbers ? headerMetrics.value.gutter : 0))
|
|
2859
|
+
const headH = computed(() => (props.showColumnHeaders ? headerMetrics.value.height : 0))
|
|
2838
2860
|
const colWNum = (ci: number) => {
|
|
2839
2861
|
const c = cols.value[ci]
|
|
2840
2862
|
if (!c) return cellWidthPx.value
|
|
@@ -4192,7 +4214,8 @@ const findTextOf = (r: number, ci: number): string => {
|
|
|
4192
4214
|
const raw = state.value[r]?.[col.name]
|
|
4193
4215
|
// `label` peut être un nombre : findScan fait `.toLowerCase()` sur le retour
|
|
4194
4216
|
if (col.type === "select" || col.type === "multiselect") return choiceText(col, raw)
|
|
4195
|
-
|
|
4217
|
+
// Fichier joint → noms de fichiers ; jamais « [object Object] » (cf. lib/spreadsheetDisplay)
|
|
4218
|
+
return searchTextFor(raw, col.type)
|
|
4196
4219
|
}
|
|
4197
4220
|
const findScan = () => {
|
|
4198
4221
|
const q = findQuery.value
|
|
@@ -5171,7 +5194,7 @@ defineExpose({
|
|
|
5171
5194
|
class="q-spreadsheet__fx-input"
|
|
5172
5195
|
:value="fxDraft"
|
|
5173
5196
|
:disabled="readonly || disable"
|
|
5174
|
-
:placeholder="
|
|
5197
|
+
:placeholder="fxPlaceholderText"
|
|
5175
5198
|
spellcheck="false"
|
|
5176
5199
|
@input="onFxInput"
|
|
5177
5200
|
@keyup="syncFxCaret"
|
package/lib/cellSize.test.ts
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
// Le point sensible est la **non-régression** : `md` doit reproduire exactement les valeurs
|
|
3
3
|
// historiques (32 px de haut, 160 px de large, 13 px de police).
|
|
4
4
|
import { describe, expect, it } from "bun:test"
|
|
5
|
-
import { CELL_SIZE_MAX, CELL_SIZE_MIN, CELL_SIZE_PX, cellFontFor, cellSizePx, cellWidthFor } from "./cellSize"
|
|
5
|
+
import { CELL_SIZE_MAX, CELL_SIZE_MIN, CELL_SIZE_PX, cellFontFor, cellSizePx, cellWidthFor, headerMetricsFor } from "./cellSize"
|
|
6
6
|
|
|
7
7
|
describe("cellSizePx", () => {
|
|
8
8
|
it("md est le défaut historique (32 px)", () => {
|
|
@@ -50,3 +50,41 @@ describe("cellFontFor", () => {
|
|
|
50
50
|
expect(cellFontFor(120)).toBe(20)
|
|
51
51
|
})
|
|
52
52
|
})
|
|
53
|
+
|
|
54
|
+
describe("headerMetricsFor", () => {
|
|
55
|
+
it("md reproduit exactement l'historique (aucune régression)", () => {
|
|
56
|
+
expect(headerMetricsFor(32)).toEqual({
|
|
57
|
+
height: 28,
|
|
58
|
+
gutter: 34,
|
|
59
|
+
labelFont: 11.5,
|
|
60
|
+
letterFont: 9.5,
|
|
61
|
+
rowFont: 11,
|
|
62
|
+
})
|
|
63
|
+
})
|
|
64
|
+
|
|
65
|
+
it("sm réduit l'en-tête", () => {
|
|
66
|
+
const m = headerMetricsFor(28)
|
|
67
|
+
expect(m.height).toBe(25)
|
|
68
|
+
expect(m.gutter).toBe(28)
|
|
69
|
+
expect(m.labelFont).toBe(9.5)
|
|
70
|
+
})
|
|
71
|
+
|
|
72
|
+
it("lg et 66 agrandissent l'en-tête", () => {
|
|
73
|
+
expect(headerMetricsFor(40)).toEqual({
|
|
74
|
+
height: 35,
|
|
75
|
+
gutter: 41,
|
|
76
|
+
labelFont: 14,
|
|
77
|
+
letterFont: 12,
|
|
78
|
+
rowFont: 14,
|
|
79
|
+
})
|
|
80
|
+
const big = headerMetricsFor(66)
|
|
81
|
+
expect(big.height).toBe(56) // plafonné
|
|
82
|
+
expect(big.gutter).toBe(47)
|
|
83
|
+
expect(big.labelFont).toBe(16)
|
|
84
|
+
})
|
|
85
|
+
|
|
86
|
+
it("la gouttière s'élargit avec la police (numéros à 4-5 chiffres)", () => {
|
|
87
|
+
expect(headerMetricsFor(48).gutter).toBeGreaterThan(headerMetricsFor(32).gutter)
|
|
88
|
+
expect(headerMetricsFor(120).gutter).toBeLessThanOrEqual(56)
|
|
89
|
+
})
|
|
90
|
+
})
|
package/lib/cellSize.ts
CHANGED
|
@@ -41,3 +41,39 @@ export const cellWidthFor = (heightPx: number): number => Math.round(heightPx *
|
|
|
41
41
|
*/
|
|
42
42
|
export const cellFontFor = (heightPx: number): number =>
|
|
43
43
|
Math.min(20, Math.max(11, Math.round(heightPx * 0.406)))
|
|
44
|
+
|
|
45
|
+
/** Métriques de la ligne d'en-tête (noms de colonnes + numéros de ligne). */
|
|
46
|
+
export interface HeaderMetrics {
|
|
47
|
+
/** Hauteur de la ligne d'en-tête, en px */
|
|
48
|
+
height: number
|
|
49
|
+
/** Largeur de la gouttière des numéros de ligne, en px */
|
|
50
|
+
gutter: number
|
|
51
|
+
/** Police du libellé de colonne, en px */
|
|
52
|
+
labelFont: number
|
|
53
|
+
/** Police de la lettre de colonne (A, B, C…), en px */
|
|
54
|
+
letterFont: number
|
|
55
|
+
/** Police des numéros de ligne, en px */
|
|
56
|
+
rowFont: number
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
const clamp = (value: number, min: number, max: number): number =>
|
|
60
|
+
Math.min(max, Math.max(min, value))
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* En-têtes **accordés à la taille des cellules** : une cellule de 66 px avec un en-tête de
|
|
64
|
+
* 28 px (et une police de 11 px) jurerait. À `md` (32 px), toutes les valeurs retombent
|
|
65
|
+
* **exactement** sur l'historique (28 px de haut, gouttière 34, libellé 11.5, lettre 9.5,
|
|
66
|
+
* numéro 11) — donc aucune régression sans `sizeCell`.
|
|
67
|
+
*/
|
|
68
|
+
export const headerMetricsFor = (cellHeightPx: number): HeaderMetrics => {
|
|
69
|
+
const cellFont = cellFontFor(cellHeightPx)
|
|
70
|
+
// Arrondi au demi-px : `md` tombe pile sur 11.5 (valeur historique)
|
|
71
|
+
const labelFont = clamp(Math.round(cellFont * 1.77) / 2, 9.5, 16)
|
|
72
|
+
return {
|
|
73
|
+
height: clamp(Math.round(cellHeightPx * 0.875), 24, 56),
|
|
74
|
+
gutter: clamp(Math.round(labelFont * 2.96), 28, 56),
|
|
75
|
+
labelFont,
|
|
76
|
+
letterFont: clamp(labelFont - 2, 8, 13),
|
|
77
|
+
rowFont: clamp(Math.round(cellFont * 0.846), 9, 15),
|
|
78
|
+
}
|
|
79
|
+
}
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
// Textes « plats » d'une cellule : barre de formule (fx) et recherche.
|
|
2
|
+
// Le cas d'origine : une cellule `attachment` (tableau d'objets) affichait
|
|
3
|
+
// « [object Object],[object Object] » dans fx.
|
|
4
|
+
import { describe, expect, it } from "bun:test"
|
|
5
|
+
import { fxTextFor, searchTextFor } from "./spreadsheetDisplay"
|
|
6
|
+
|
|
7
|
+
const files = [
|
|
8
|
+
{ id: "a", name: "login.png", size: 12, type: "image/png" },
|
|
9
|
+
{ id: "b", name: "dashboard.png", size: 34, type: "image/png" },
|
|
10
|
+
]
|
|
11
|
+
|
|
12
|
+
describe("fxTextFor", () => {
|
|
13
|
+
it("n'affiche rien pour un fichier joint", () => {
|
|
14
|
+
expect(fxTextFor(files, "attachment")).toBe("")
|
|
15
|
+
expect(fxTextFor([], "attachment")).toBe("")
|
|
16
|
+
expect(fxTextFor(null, "attachment")).toBe("")
|
|
17
|
+
})
|
|
18
|
+
|
|
19
|
+
it("ne laisse jamais fuiter [object Object]", () => {
|
|
20
|
+
expect(fxTextFor({ name: "x" })).toBe("")
|
|
21
|
+
expect(fxTextFor([{ a: 1 }, { b: 2 }])).toBe("")
|
|
22
|
+
})
|
|
23
|
+
|
|
24
|
+
it("garde le texte des valeurs simples", () => {
|
|
25
|
+
expect(fxTextFor("Ada")).toBe("Ada")
|
|
26
|
+
expect(fxTextFor(18.5)).toBe("18.5")
|
|
27
|
+
expect(fxTextFor(true)).toBe("true")
|
|
28
|
+
expect(fxTextFor("=A1*2")).toBe("=A1*2")
|
|
29
|
+
expect(fxTextFor(null)).toBe("")
|
|
30
|
+
expect(fxTextFor(undefined)).toBe("")
|
|
31
|
+
})
|
|
32
|
+
|
|
33
|
+
it("joint proprement un tableau scalaire (multiselect)", () => {
|
|
34
|
+
expect(fxTextFor(["tea", "beer"])).toBe("tea, beer")
|
|
35
|
+
expect(fxTextFor(["tea", null, "beer"])).toBe("tea, beer")
|
|
36
|
+
})
|
|
37
|
+
})
|
|
38
|
+
|
|
39
|
+
describe("searchTextFor", () => {
|
|
40
|
+
it("indexe les noms de fichiers", () => {
|
|
41
|
+
expect(searchTextFor(files, "attachment")).toBe("login.png, dashboard.png")
|
|
42
|
+
})
|
|
43
|
+
|
|
44
|
+
it("retombe sur le texte simple", () => {
|
|
45
|
+
expect(searchTextFor("rapport.pdf", "string")).toBe("rapport.pdf")
|
|
46
|
+
expect(searchTextFor(null, "string")).toBe("")
|
|
47
|
+
expect(searchTextFor({ a: 1 })).toBe("")
|
|
48
|
+
})
|
|
49
|
+
})
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
// Texte d'une valeur de cellule pour les surfaces **textuelles** du tableur :
|
|
2
|
+
// barre de formule (`fx`) et moteur de recherche (Find).
|
|
3
|
+
//
|
|
4
|
+
// Piège visé : `String(valeur)` sur un objet/tableau produit « [object Object] ». Les types
|
|
5
|
+
// à valeur structurée (`attachment`, et tout futur type objet) doivent donc passer par ici
|
|
6
|
+
// au lieu d'un `String()` sauvage.
|
|
7
|
+
import { attachmentsOf } from "./fileList"
|
|
8
|
+
|
|
9
|
+
/** Valeur affichable en texte : `null`/objet → `""`, tableau → éléments joints. */
|
|
10
|
+
const plain = (value: unknown): string => {
|
|
11
|
+
if (value === null || value === undefined) return ""
|
|
12
|
+
if (Array.isArray(value)) {
|
|
13
|
+
return value
|
|
14
|
+
.map((entry) => (entry === null || entry === undefined || typeof entry === "object" ? "" : String(entry)))
|
|
15
|
+
.filter((text) => text !== "")
|
|
16
|
+
.join(", ")
|
|
17
|
+
}
|
|
18
|
+
if (typeof value === "object") return ""
|
|
19
|
+
return String(value)
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Contenu de la **barre de formule** pour une cellule.
|
|
24
|
+
* `attachment` → `""` (la valeur est un tableau d'objets : rien de pertinent à éditer).
|
|
25
|
+
*/
|
|
26
|
+
export const fxTextFor = (value: unknown, type?: string): string => {
|
|
27
|
+
if (type === "attachment") return ""
|
|
28
|
+
return plain(value)
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Texte indexé par la **recherche** (Find & Replace).
|
|
33
|
+
* `attachment` → les **noms de fichiers**, pour pouvoir chercher par nom.
|
|
34
|
+
*/
|
|
35
|
+
export const searchTextFor = (value: unknown, type?: string): string => {
|
|
36
|
+
if (type === "attachment") {
|
|
37
|
+
return attachmentsOf(value)
|
|
38
|
+
.map((att) => att.name)
|
|
39
|
+
.join(", ")
|
|
40
|
+
}
|
|
41
|
+
return plain(value)
|
|
42
|
+
}
|
package/package.json
CHANGED
package/styles/main.css
CHANGED
|
@@ -9690,9 +9690,9 @@ a.q-breadcrumbs__el:hover {
|
|
|
9690
9690
|
top: 0;
|
|
9691
9691
|
left: 0;
|
|
9692
9692
|
z-index: 6;
|
|
9693
|
-
width: 34px;
|
|
9694
|
-
min-width: 34px;
|
|
9695
|
-
height: 28px;
|
|
9693
|
+
width: var(--q-spreadsheet-gutter-width, 34px);
|
|
9694
|
+
min-width: var(--q-spreadsheet-gutter-width, 34px);
|
|
9695
|
+
height: var(--q-spreadsheet-head-height, 28px);
|
|
9696
9696
|
border-right: 1px solid var(--q-spreadsheet-border);
|
|
9697
9697
|
border-bottom: 1px solid var(--q-spreadsheet-border);
|
|
9698
9698
|
text-align: center;
|
|
@@ -9702,7 +9702,7 @@ a.q-breadcrumbs__el:hover {
|
|
|
9702
9702
|
.q-spreadsheet__colhead {
|
|
9703
9703
|
top: 0;
|
|
9704
9704
|
z-index: 4;
|
|
9705
|
-
height: 28px;
|
|
9705
|
+
height: var(--q-spreadsheet-head-height, 28px);
|
|
9706
9706
|
padding: 0 10px 0 6px;
|
|
9707
9707
|
border-right: 1px solid var(--q-spreadsheet-border);
|
|
9708
9708
|
border-bottom: 1px solid var(--q-spreadsheet-border);
|
|
@@ -9723,7 +9723,7 @@ a.q-breadcrumbs__el:hover {
|
|
|
9723
9723
|
padding-right: 6px;
|
|
9724
9724
|
}
|
|
9725
9725
|
.q-spreadsheet__colhead-letter {
|
|
9726
|
-
font-size: 9.5px;
|
|
9726
|
+
font-size: var(--q-spreadsheet-head-letter-font, 9.5px);
|
|
9727
9727
|
font-weight: 700;
|
|
9728
9728
|
line-height: 1.1;
|
|
9729
9729
|
letter-spacing: 0.04em;
|
|
@@ -9731,7 +9731,7 @@ a.q-breadcrumbs__el:hover {
|
|
|
9731
9731
|
color: var(--primary, #1976d2);
|
|
9732
9732
|
}
|
|
9733
9733
|
.q-spreadsheet__colhead-label {
|
|
9734
|
-
font-size: 11.5px;
|
|
9734
|
+
font-size: var(--q-spreadsheet-head-font, 11.5px);
|
|
9735
9735
|
line-height: 1.25;
|
|
9736
9736
|
overflow: hidden;
|
|
9737
9737
|
text-overflow: ellipsis;
|
|
@@ -9739,11 +9739,11 @@ a.q-breadcrumbs__el:hover {
|
|
|
9739
9739
|
.q-spreadsheet__rownum {
|
|
9740
9740
|
left: 0;
|
|
9741
9741
|
z-index: 3;
|
|
9742
|
-
width: 34px;
|
|
9743
|
-
min-width: 34px;
|
|
9742
|
+
width: var(--q-spreadsheet-gutter-width, 34px);
|
|
9743
|
+
min-width: var(--q-spreadsheet-gutter-width, 34px);
|
|
9744
9744
|
border-right: 1px solid var(--q-spreadsheet-border);
|
|
9745
9745
|
border-bottom: 1px solid var(--q-spreadsheet-border);
|
|
9746
|
-
font-size: 11px;
|
|
9746
|
+
font-size: var(--q-spreadsheet-row-font, 11px);
|
|
9747
9747
|
font-weight: 500;
|
|
9748
9748
|
text-align: center;
|
|
9749
9749
|
vertical-align: middle;
|