@dnax/ui 0.0.48 → 0.0.50
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/QBottomSheet.vue +172 -6
- package/components/QChart.vue +1 -0
- package/components/QDatePicker.vue +1 -1
- package/components/QImagePicker.vue +28 -1
- package/components/QInputChat.vue +859 -0
- package/components/QInputCurrency.vue +319 -0
- package/components/QItemLabel.vue +99 -0
- package/components/QMap.vue +410 -0
- package/components/QNumericKeyboard.vue +304 -0
- package/components/QPage.vue +26 -6
- package/components/QPageContainer.vue +32 -0
- package/components/QPopupProxy.vue +378 -0
- package/components/QQrcode.vue +132 -0
- package/components/QSelect.vue +12 -0
- package/components/QSpreadsheet.vue +395 -69
- package/components/QTime.vue +845 -0
- package/components/QTimeline.vue +111 -0
- package/components/QTimelineEntry.vue +271 -0
- package/index.ts +11 -1
- package/lib/bottomSheet.test.ts +121 -0
- package/lib/bottomSheet.ts +115 -0
- package/lib/color.ts +13 -0
- package/lib/currency.test.ts +193 -0
- package/lib/currency.ts +301 -0
- package/lib/fixedLayout.ts +12 -11
- package/lib/icons.ts +3 -20
- package/lib/map.test.ts +365 -0
- package/lib/map.ts +438 -0
- package/lib/mapEngine.ts +158 -0
- package/lib/mapLeaflet.ts +142 -0
- package/lib/mapMaplibre.ts +106 -0
- package/lib/mapMaptiler.ts +93 -0
- package/lib/numericKeyboard.test.ts +179 -0
- package/lib/numericKeyboard.ts +166 -0
- package/lib/pagePadding.test.ts +37 -0
- package/lib/pagePadding.ts +32 -0
- package/lib/qrcode.test.ts +103 -0
- package/lib/qrcode.ts +125 -0
- package/lib/spreadsheetChanges.test.ts +230 -0
- package/lib/spreadsheetChanges.ts +331 -0
- package/package.json +5 -17
- package/shims.d.ts +19 -0
- package/styles/main.css +689 -884
- package/components/QTiptap.vue +0 -996
- package/components/internal/QTiptapTableView.vue +0 -123
- package/components/internal/QTiptapTaskItemView.vue +0 -49
- package/lib/tiptap-table.ts +0 -19
- package/lib/tiptap-task-list.ts +0 -16
|
@@ -2,6 +2,11 @@
|
|
|
2
2
|
// QBottomSheet — bottom sheet type Drawer shadcn-vue (vaul), style Quasar :
|
|
3
3
|
// <q-bottom-sheet v-model="open"> + slot trigger / QBottomSheetHeader / QBottomSheetFooter
|
|
4
4
|
// Drag vers le bas pour fermer, backdrop, Échap, safe-area bottom.
|
|
5
|
+
//
|
|
6
|
+
// Points d'ancrage (style Ionic) : `:breakpoints="[0.25, 0.5, 0.75]"` pose le panneau sur
|
|
7
|
+
// des fractions de la hauteur de vue, la poignée (ou le clavier) passant de l'une à
|
|
8
|
+
// l'autre. Le contenu reste **défilable à chaque point d'ancrage** : c'est la poignée qui
|
|
9
|
+
// pilote la hauteur, jamais le scroll.
|
|
5
10
|
import type { InjectionKey, Ref } from "vue"
|
|
6
11
|
|
|
7
12
|
export interface BottomSheetContext {
|
|
@@ -14,6 +19,14 @@ export const qBottomSheetKey: InjectionKey<BottomSheetContext> = Symbol("q-botto
|
|
|
14
19
|
|
|
15
20
|
<script setup lang="ts">
|
|
16
21
|
import { computed, onBeforeUnmount, onMounted, provide, ref, watch } from "vue"
|
|
22
|
+
import {
|
|
23
|
+
clampRatio,
|
|
24
|
+
nearestBreakpoint,
|
|
25
|
+
normalizeBreakpoints,
|
|
26
|
+
ratioFromDrag,
|
|
27
|
+
releaseBreakpoint,
|
|
28
|
+
stepBreakpoint,
|
|
29
|
+
} from "../lib/bottomSheet"
|
|
17
30
|
import { cn } from "../lib/utils"
|
|
18
31
|
import { useOverlayBack } from "../lib/overlayBack"
|
|
19
32
|
import { markOverlayClose } from "../lib/closeOverlay"
|
|
@@ -23,15 +36,27 @@ interface Props {
|
|
|
23
36
|
modelValue?: boolean
|
|
24
37
|
/** Ne se ferme ni au backdrop ni à Échap */
|
|
25
38
|
persistent?: boolean
|
|
39
|
+
/** **Sans backdrop** : le fond n'est pas assombri et reste **interactif** (les clics le
|
|
40
|
+
* traversent) — le clic à côté ne ferme donc plus le panneau. Échap, bouton « retour »
|
|
41
|
+
* et `v-model` restent les moyens de le fermer (voir aussi `persistent`). */
|
|
42
|
+
seamless?: boolean
|
|
26
43
|
/** Largeur max du panneau */
|
|
27
44
|
width?: string
|
|
28
45
|
/** Hauteur du panneau (sinon max-height 90vh) */
|
|
29
46
|
height?: string
|
|
30
47
|
/** Arrondi des coins hauts : true | false | valeur CSS */
|
|
31
48
|
rounded?: boolean | string
|
|
49
|
+
/** Ombre du panneau : `true` (défaut — automatiquement **adoucie** en mode `seamless`,
|
|
50
|
+
* où il n'y a pas de fond assombri derrière), `false` (aucune ombre) ou une valeur CSS
|
|
51
|
+
* (`"0 8px 30px rgb(0 0 0 / 0.25)"`). Surchargeable en CSS via `--q-bs-shadow`. */
|
|
52
|
+
shadow?: boolean | string
|
|
32
53
|
dark?: boolean
|
|
33
54
|
/** Fond translucide (frosted glass) : true = 70%, ou valeur % */
|
|
34
55
|
translucent?: boolean | number
|
|
56
|
+
/** Glassmorphism marqué (même recette que `q-header` / `q-footer`) : fond très translucide
|
|
57
|
+
* + flou fort + bordure claire. **Prioritaire sur `translucent`** ; le flou et le fond
|
|
58
|
+
* des textes se règlent par `--q-glass-blur` / `--q-glass-bg`. */
|
|
59
|
+
glass?: boolean
|
|
35
60
|
/** Seuil de drag (px) au-delà duquel on ferme */
|
|
36
61
|
dragThreshold?: number
|
|
37
62
|
/** Animation d'ouverture : slide-up (défaut) | fade | zoom */
|
|
@@ -40,18 +65,34 @@ interface Props {
|
|
|
40
65
|
transitionDuration?: number
|
|
41
66
|
/** Style du panneau (variables de thème pour les contenus téléportés) */
|
|
42
67
|
contentStyle?: Record<string, string>
|
|
68
|
+
/** Points d'ancrage : fractions de la hauteur de vue où le panneau peut se poser,
|
|
69
|
+
* ex. `[0.25, 0.5, 0.75]` (comme Ionic). Le contenu reste défilable à chaque point
|
|
70
|
+
* d'ancrage et c'est la poignée qui fait grandir/rétrécir le panneau ; `0` est accepté
|
|
71
|
+
* et vaut « fermé ». Sans cette prop, le panneau prend la hauteur de son contenu
|
|
72
|
+
* (bornée par `max-height` / 90vh). */
|
|
73
|
+
breakpoints?: number[] | string
|
|
74
|
+
/** Point d'ancrage courant (`v-model:breakpoint`) : point de départ à l'ouverture, puis
|
|
75
|
+
* suivi des drags. Une valeur absente de `breakpoints` est ramenée au plus proche. */
|
|
76
|
+
breakpoint?: number
|
|
43
77
|
}
|
|
44
78
|
|
|
45
79
|
const props = withDefaults(defineProps<Props>(), {
|
|
46
80
|
modelValue: false,
|
|
47
81
|
persistent: false,
|
|
82
|
+
seamless: false,
|
|
48
83
|
width: "640px",
|
|
49
84
|
rounded: true,
|
|
85
|
+
shadow: true,
|
|
50
86
|
dark: false,
|
|
87
|
+
glass: false,
|
|
51
88
|
dragThreshold: 80,
|
|
52
89
|
})
|
|
53
90
|
|
|
54
|
-
const emit = defineEmits<{
|
|
91
|
+
const emit = defineEmits<{
|
|
92
|
+
"update:modelValue": [value: boolean]
|
|
93
|
+
/** Nouveau point d'ancrage retenu (`v-model:breakpoint`) */
|
|
94
|
+
"update:breakpoint": [value: number]
|
|
95
|
+
}>()
|
|
55
96
|
|
|
56
97
|
const open = computed({
|
|
57
98
|
get: () => props.modelValue,
|
|
@@ -92,36 +133,132 @@ onBeforeUnmount(() => {
|
|
|
92
133
|
}
|
|
93
134
|
})
|
|
94
135
|
|
|
136
|
+
// — Points d'ancrage (breakpoints) : le panneau se pose sur des fractions de la hauteur
|
|
137
|
+
// de vue (`:breakpoints="[0.25, 0.5, 0.75]"`). Contrairement au drag historique (qui
|
|
138
|
+
// translate le panneau), on pilote ici sa **hauteur** : le contenu se réagence et reste
|
|
139
|
+
// défilable à chaque point d'ancrage (cf. `lib/bottomSheet.ts`, pur et testé).
|
|
140
|
+
const anchors = computed(() => normalizeBreakpoints(props.breakpoints))
|
|
141
|
+
const usesBreakpoints = computed(() => anchors.value.length > 0)
|
|
142
|
+
|
|
143
|
+
/** Fraction courante du panneau (1 = pleine hauteur) */
|
|
144
|
+
const currentRatio = ref(1)
|
|
145
|
+
|
|
146
|
+
/** Dernière valeur **communiquée** au parent (`v-model:breakpoint`) : évite un événement
|
|
147
|
+
* inutile quand le panneau retombe sur le même point d'ancrage. */
|
|
148
|
+
let committedRatio = 1
|
|
149
|
+
|
|
150
|
+
/** Point d'ancrage demandé par la prop `breakpoint`, ramené à la liste */
|
|
151
|
+
const requestedBreakpoint = computed(() => {
|
|
152
|
+
if (typeof props.breakpoint !== "number") return undefined
|
|
153
|
+
return usesBreakpoints.value
|
|
154
|
+
? nearestBreakpoint(props.breakpoint, anchors.value)
|
|
155
|
+
: clampRatio(props.breakpoint)
|
|
156
|
+
})
|
|
157
|
+
|
|
158
|
+
watch(
|
|
159
|
+
[open, requestedBreakpoint, anchors],
|
|
160
|
+
() => {
|
|
161
|
+
if (!open.value) return
|
|
162
|
+
currentRatio.value = requestedBreakpoint.value ?? anchors.value[0] ?? 1
|
|
163
|
+
committedRatio = currentRatio.value
|
|
164
|
+
},
|
|
165
|
+
{ immediate: true },
|
|
166
|
+
)
|
|
167
|
+
|
|
168
|
+
/** Hauteur de vue de référence (barre d'URL mobile exclue quand elle est connue) */
|
|
169
|
+
const viewportHeight = () =>
|
|
170
|
+
typeof window === "undefined" ? 0 : (window.visualViewport?.height ?? window.innerHeight)
|
|
171
|
+
|
|
172
|
+
/** Pose le panneau sur un point d'ancrage et prévient le parent si la valeur change */
|
|
173
|
+
const applyBreakpoint = (value: number) => {
|
|
174
|
+
currentRatio.value = value
|
|
175
|
+
if (value === committedRatio) return
|
|
176
|
+
committedRatio = value
|
|
177
|
+
emit("update:breakpoint", value)
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
const stepToBreakpoint = (direction: 1 | -1) => {
|
|
181
|
+
if (!usesBreakpoints.value) return
|
|
182
|
+
applyBreakpoint(stepBreakpoint(currentRatio.value, anchors.value, direction))
|
|
183
|
+
}
|
|
184
|
+
|
|
95
185
|
// — Drag to dismiss (pattern vaul) —
|
|
96
186
|
const panelRef = ref<HTMLElement | null>(null)
|
|
97
187
|
const dragging = ref(false)
|
|
98
188
|
let startY = 0
|
|
99
189
|
let currentDy = 0
|
|
190
|
+
let startRatio = 1
|
|
100
191
|
|
|
101
192
|
const onHandleDown = (e: PointerEvent) => {
|
|
102
|
-
|
|
193
|
+
// capture du pointeur : peut échouer (pointeur déjà relâché, événement synthétique)
|
|
194
|
+
try {
|
|
195
|
+
;(e.target as HTMLElement).setPointerCapture(e.pointerId)
|
|
196
|
+
} catch {
|
|
197
|
+
/* sans capture, le drag suit tant que le pointeur reste sur la poignée */
|
|
198
|
+
}
|
|
103
199
|
startY = e.clientY
|
|
104
200
|
currentDy = 0
|
|
201
|
+
startRatio = currentRatio.value
|
|
105
202
|
dragging.value = true
|
|
106
203
|
}
|
|
107
204
|
|
|
108
205
|
const onHandleMove = (e: PointerEvent) => {
|
|
109
206
|
if (!dragging.value) return
|
|
110
|
-
currentDy =
|
|
207
|
+
currentDy = e.clientY - startY
|
|
208
|
+
|
|
209
|
+
if (usesBreakpoints.value) {
|
|
210
|
+
// La hauteur suit le doigt (bornée au plus haut point d'ancrage)
|
|
211
|
+
const highest = Math.max(...anchors.value)
|
|
212
|
+
currentRatio.value = Math.min(
|
|
213
|
+
highest,
|
|
214
|
+
ratioFromDrag(startRatio, currentDy, viewportHeight()),
|
|
215
|
+
)
|
|
216
|
+
return
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
currentDy = Math.max(0, currentDy)
|
|
111
220
|
if (panelRef.value) panelRef.value.style.transform = `translateY(${currentDy}px)`
|
|
112
221
|
}
|
|
113
222
|
|
|
114
223
|
const onHandleUp = () => {
|
|
115
224
|
if (!dragging.value) return
|
|
116
225
|
dragging.value = false
|
|
226
|
+
|
|
227
|
+
if (usesBreakpoints.value) {
|
|
228
|
+
const height = viewportHeight()
|
|
229
|
+
const threshold = height > 0 ? props.dragThreshold / height : 0.15
|
|
230
|
+
const release = releaseBreakpoint(currentRatio.value, anchors.value, threshold)
|
|
231
|
+
|
|
232
|
+
if (release.close) {
|
|
233
|
+
open.value = false
|
|
234
|
+
currentRatio.value = anchors.value[0] ?? 1
|
|
235
|
+
committedRatio = currentRatio.value
|
|
236
|
+
} else if (release.breakpoint !== undefined) {
|
|
237
|
+
applyBreakpoint(release.breakpoint)
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
currentDy = 0
|
|
241
|
+
return
|
|
242
|
+
}
|
|
243
|
+
|
|
117
244
|
if (currentDy > props.dragThreshold) open.value = false
|
|
118
245
|
else if (panelRef.value) panelRef.value.style.transform = ""
|
|
119
246
|
currentDy = 0
|
|
120
247
|
}
|
|
121
248
|
|
|
249
|
+
/** Poignée au clavier : Entrée / Espace passe au point d'ancrage suivant (Maj : précédent) */
|
|
250
|
+
const onHandleKeydown = (e: KeyboardEvent) => {
|
|
251
|
+
if (!usesBreakpoints.value) return
|
|
252
|
+
if (e.key !== "Enter" && e.key !== " ") return
|
|
253
|
+
e.preventDefault()
|
|
254
|
+
stepToBreakpoint(e.shiftKey ? -1 : 1)
|
|
255
|
+
}
|
|
256
|
+
|
|
122
257
|
const panelStyle = computed<Record<string, string>>(() => ({
|
|
123
258
|
maxWidth: props.width,
|
|
124
|
-
|
|
259
|
+
// `height` est ignoré en mode breakpoints : la hauteur vient du point d'ancrage
|
|
260
|
+
...(props.height && !usesBreakpoints.value ? { height: props.height } : {}),
|
|
261
|
+
...(usesBreakpoints.value ? { "--q-bs-breakpoint": `${currentRatio.value}` } : {}),
|
|
125
262
|
}))
|
|
126
263
|
|
|
127
264
|
const radiusStyle = computed<Record<string, string> | undefined>(() => {
|
|
@@ -130,6 +267,16 @@ const radiusStyle = computed<Record<string, string> | undefined>(() => {
|
|
|
130
267
|
return undefined
|
|
131
268
|
})
|
|
132
269
|
|
|
270
|
+
/** Ombre : `false` → aucune, une chaîne → telle quelle, `true` → le défaut CSS (adouci en
|
|
271
|
+
* `seamless`). On passe par `--q-bs-shadow` pour que le défaut reste thémable en CSS. */
|
|
272
|
+
const shadowStyle = computed<Record<string, string> | undefined>(() => {
|
|
273
|
+
if (props.shadow === false) return { "--q-bs-shadow": "none" }
|
|
274
|
+
if (typeof props.shadow === "string" && props.shadow.trim() !== "") {
|
|
275
|
+
return { "--q-bs-shadow": props.shadow.trim() }
|
|
276
|
+
}
|
|
277
|
+
return undefined
|
|
278
|
+
})
|
|
279
|
+
|
|
133
280
|
// Nom de transition : q-bs (défaut slide-up) | q-bs-fade | q-bs-zoom
|
|
134
281
|
const transitionName = computed(() =>
|
|
135
282
|
props.transition ? `q-bs-${props.transition}` : "q-bs",
|
|
@@ -148,9 +295,11 @@ const transitionStyle = computed<Record<string, string> | undefined>(() =>
|
|
|
148
295
|
const panelClasses = computed(() =>
|
|
149
296
|
cn(
|
|
150
297
|
"q-bottom-sheet__panel",
|
|
298
|
+
usesBreakpoints.value && "q-bottom-sheet__panel--breakpoints",
|
|
151
299
|
props.rounded === false && "q-bottom-sheet__panel--square",
|
|
152
300
|
props.dark && "q-bottom-sheet__panel--dark",
|
|
153
301
|
(props.translucent === true || typeof props.translucent === "number") && "q-bottom-sheet__panel--translucent",
|
|
302
|
+
props.glass && "q-bottom-sheet__panel--glass",
|
|
154
303
|
dragging.value && "q-bottom-sheet__panel--dragging",
|
|
155
304
|
),
|
|
156
305
|
)
|
|
@@ -160,6 +309,18 @@ const translucentStyle = computed<Record<string, string> | undefined>(() =>
|
|
|
160
309
|
? { "--q-translucent-opacity": `${typeof props.translucent === "number" ? props.translucent : 70}%` }
|
|
161
310
|
: undefined,
|
|
162
311
|
)
|
|
312
|
+
|
|
313
|
+
defineExpose({
|
|
314
|
+
/** Point d'ancrage courant (fraction), quand `breakpoints` est utilisé */
|
|
315
|
+
breakpoint: computed(() => (usesBreakpoints.value ? currentRatio.value : undefined)),
|
|
316
|
+
/** Amène le panneau au point d'ancrage le plus proche de `value` */
|
|
317
|
+
setBreakpoint(value: number) {
|
|
318
|
+
if (!usesBreakpoints.value) return
|
|
319
|
+
applyBreakpoint(nearestBreakpoint(value, anchors.value))
|
|
320
|
+
},
|
|
321
|
+
/** Passe au point d'ancrage suivant (`1`) ou précédent (`-1`) */
|
|
322
|
+
stepBreakpoint: stepToBreakpoint,
|
|
323
|
+
})
|
|
163
324
|
</script>
|
|
164
325
|
|
|
165
326
|
<template>
|
|
@@ -173,23 +334,28 @@ const translucentStyle = computed<Record<string, string> | undefined>(() =>
|
|
|
173
334
|
v-if="open"
|
|
174
335
|
:ref="markOverlay"
|
|
175
336
|
class="q-bottom-sheet__overlay"
|
|
176
|
-
|
|
337
|
+
:class="cn(seamless && 'q-bottom-sheet__overlay--seamless')"
|
|
338
|
+
@click="!persistent && !seamless && (open = false)"
|
|
177
339
|
>
|
|
178
340
|
<div
|
|
179
341
|
ref="panelRef"
|
|
180
342
|
class="q-bottom-sheet__panel"
|
|
181
343
|
:class="panelClasses"
|
|
182
|
-
:style="[panelStyle, radiusStyle, translucentStyle, transitionStyle, props.contentStyle]"
|
|
344
|
+
:style="[panelStyle, radiusStyle, shadowStyle, translucentStyle, transitionStyle, props.contentStyle]"
|
|
183
345
|
role="dialog"
|
|
184
346
|
aria-modal="true"
|
|
185
347
|
@click.stop
|
|
186
348
|
>
|
|
187
349
|
<div
|
|
188
350
|
class="q-bottom-sheet__handle"
|
|
351
|
+
:role="usesBreakpoints ? 'button' : undefined"
|
|
352
|
+
:tabindex="usesBreakpoints ? 0 : undefined"
|
|
353
|
+
:aria-label="usesBreakpoints ? 'Changer la hauteur du panneau' : undefined"
|
|
189
354
|
@pointerdown="onHandleDown"
|
|
190
355
|
@pointermove="onHandleMove"
|
|
191
356
|
@pointerup="onHandleUp"
|
|
192
357
|
@pointercancel="onHandleUp"
|
|
358
|
+
@keydown="onHandleKeydown"
|
|
193
359
|
>
|
|
194
360
|
<span class="q-bottom-sheet__handle-bar" />
|
|
195
361
|
</div>
|
package/components/QChart.vue
CHANGED
|
@@ -100,6 +100,7 @@ const loadECharts = () => {
|
|
|
100
100
|
components.GridComponent,
|
|
101
101
|
components.TooltipComponent,
|
|
102
102
|
components.LegendComponent,
|
|
103
|
+
components.TitleComponent, // titre (`title` prop)
|
|
103
104
|
components.MarkLineComponent,
|
|
104
105
|
components.VisualMapComponent, // échelle de couleurs des cartes de chaleur
|
|
105
106
|
renderers.CanvasRenderer,
|
|
@@ -268,7 +268,7 @@ onBeforeUnmount(() => {
|
|
|
268
268
|
>
|
|
269
269
|
<Icon :icon="icons.x" aria-hidden="true" />
|
|
270
270
|
</button>
|
|
271
|
-
<
|
|
271
|
+
<Icon :icon="icons.chevronDown" class="q-select__arrow" :class="{ 'q-select__arrow--rotated': open }" aria-hidden="true" />
|
|
272
272
|
</div>
|
|
273
273
|
<div class="q-field__bottom">
|
|
274
274
|
<div v-if="error" class="q-field__error">{{ errorMessage }}</div>
|
|
@@ -2,7 +2,8 @@
|
|
|
2
2
|
// QImagePicker — API Quasar : <q-image-picker v-model="files" multiple label="Photos" max-file-size="5242880" />
|
|
3
3
|
// Sélection d'images via input file masqué, avec aperçu en grille (object URLs),
|
|
4
4
|
// suppression par tuile et validation (type, taille, nombre).
|
|
5
|
-
|
|
5
|
+
// `capture` ouvre directement l'appareil photo sur mobile.
|
|
6
|
+
import { computed, getCurrentInstance, onBeforeUnmount, reactive, ref, watch } from "vue"
|
|
6
7
|
import { Icon } from "@iconify/vue"
|
|
7
8
|
import { icons } from "../lib/icons"
|
|
8
9
|
import { cn } from "../lib/utils"
|
|
@@ -18,6 +19,9 @@ interface Props {
|
|
|
18
19
|
maxFileSize?: number
|
|
19
20
|
/** Nombre max de fichiers (0 = illimité) */
|
|
20
21
|
maxFiles?: number
|
|
22
|
+
/** Ouvre l'appareil photo sur mobile : `true` (appareil par défaut), `"user"` (frontale)
|
|
23
|
+
* ou `"environment"` (arrière) — attribut `capture` de l'input */
|
|
24
|
+
capture?: boolean | "user" | "environment"
|
|
21
25
|
/** Label du champ */
|
|
22
26
|
label?: string
|
|
23
27
|
/** Texte de la tuile d'ajout (défaut : "Ajouter une image" / "Ajouter des images") */
|
|
@@ -38,6 +42,7 @@ const props = withDefaults(defineProps<Props>(), {
|
|
|
38
42
|
accept: "image/*",
|
|
39
43
|
maxFileSize: 0,
|
|
40
44
|
maxFiles: 0,
|
|
45
|
+
capture: false,
|
|
41
46
|
label: "",
|
|
42
47
|
addLabel: "",
|
|
43
48
|
hint: "",
|
|
@@ -122,6 +127,26 @@ const effectiveErrorMessage = computed(() => props.errorMessage || internalError
|
|
|
122
127
|
// — Sélection —
|
|
123
128
|
const inputEl = ref<HTMLInputElement | null>(null)
|
|
124
129
|
|
|
130
|
+
/** Attribut `capture` de l'input : `false` (défaut) → absent, `true` → appareil par
|
|
131
|
+
* défaut (`capture="true"`, tout navigateur y voit une demande de capture), sinon la
|
|
132
|
+
* valeur donnée (`user` / `environment`). */
|
|
133
|
+
const captureAttr = computed(() => (props.capture === false ? undefined : props.capture))
|
|
134
|
+
|
|
135
|
+
/** Garde-fou : sans écouteur `update:modelValue`, les fichiers choisis ne sont jamais
|
|
136
|
+
* conservés (la grille reste vide) — piège fréquent de ce champ. */
|
|
137
|
+
const instance = getCurrentInstance()
|
|
138
|
+
let warnedNoModel = false
|
|
139
|
+
const warnIfNotBound = () => {
|
|
140
|
+
if (warnedNoModel) return
|
|
141
|
+
if (!instance?.vnode.props?.["onUpdate:modelValue"]) {
|
|
142
|
+
warnedNoModel = true
|
|
143
|
+
console.warn(
|
|
144
|
+
"[q-image-picker] aucun `v-model` (ni écouteur `@update:model-value`) : les images " +
|
|
145
|
+
"choisies ne sont pas conservées",
|
|
146
|
+
)
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
|
|
125
150
|
const pick = () => {
|
|
126
151
|
if (props.disable || props.readonly) return
|
|
127
152
|
inputEl.value?.click()
|
|
@@ -159,6 +184,7 @@ const addFiles = (incoming: File[]) => {
|
|
|
159
184
|
for (const file of overflow) rejections.push({ file, reason: "count" })
|
|
160
185
|
|
|
161
186
|
if (accepted.length > 0) {
|
|
187
|
+
warnIfNotBound()
|
|
162
188
|
const next = props.multiple ? [...current, ...accepted] : accepted[0]!
|
|
163
189
|
emit("update:modelValue", next)
|
|
164
190
|
for (const file of accepted) emit("add", file)
|
|
@@ -242,6 +268,7 @@ const containerClasses = computed(() => cn("q-image-picker", props.disable && "q
|
|
|
242
268
|
class="q-image-picker__input"
|
|
243
269
|
:accept="accept"
|
|
244
270
|
:multiple="multiple"
|
|
271
|
+
:capture="captureAttr"
|
|
245
272
|
@change="onInputChange"
|
|
246
273
|
/>
|
|
247
274
|
</div>
|