@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,593 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
// QCountryPicker — sélecteur de pays préfait : drapeau, nom, indicatif, recherche.
|
|
3
|
+
// <q-country-picker v-model="code" label="Country" mode="sheet" outlined />
|
|
4
|
+
// v-model = code ISO 3166-1 alpha-2 (ex. "FR").
|
|
5
|
+
// Modes : inline (champ + liste déroulante) | modal (centré) | sheet (bottom sheet) | dialog (plein écran).
|
|
6
|
+
import { computed, nextTick, onBeforeUnmount, onMounted, ref, watch } from "vue"
|
|
7
|
+
import { Icon } from "@iconify/vue"
|
|
8
|
+
import { icons } from "../lib/icons"
|
|
9
|
+
import { cn } from "../lib/utils"
|
|
10
|
+
import { useOverlayBack } from "../lib/overlayBack"
|
|
11
|
+
import { COUNTRIES, flagOf } from "../lib/countries"
|
|
12
|
+
import type { Country } from "../lib/countries"
|
|
13
|
+
|
|
14
|
+
/** Options spécifiques à un mode d'ouverture (sheet / modal) */
|
|
15
|
+
export interface QCountryPickerModeOptions {
|
|
16
|
+
/** Largeur du panneau (remplace width) */
|
|
17
|
+
width?: string
|
|
18
|
+
/** Style appliqué au panneau */
|
|
19
|
+
style?: Record<string, string>
|
|
20
|
+
/** Classes ajoutées au panneau */
|
|
21
|
+
class?: string
|
|
22
|
+
/** Hauteur de la liste scrollable (ex. "45vh", "400px") */
|
|
23
|
+
height?: string
|
|
24
|
+
/** Placeholder du champ de recherche pour ce mode */
|
|
25
|
+
searchPlaceholder?: string
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
interface Props {
|
|
29
|
+
/** Code ISO 3166-1 alpha-2 sélectionné (ex. "FR") — v-model */
|
|
30
|
+
modelValue?: string
|
|
31
|
+
/** Liste des pays proposés (défaut : tous) */
|
|
32
|
+
countries?: Country[]
|
|
33
|
+
/** Langue des noms de pays : "en" | "fr" (défaut en) */
|
|
34
|
+
language?: "en" | "fr"
|
|
35
|
+
/** Mode d'affichage : inline (liste sous le champ) | sheet | modal | dialog */
|
|
36
|
+
mode?: "inline" | "sheet" | "modal" | "dialog"
|
|
37
|
+
/** Libellé du champ vide (défaut "Select a country") */
|
|
38
|
+
placeholder?: string
|
|
39
|
+
/** Champ de recherche dans la liste (défaut true) */
|
|
40
|
+
searchable?: boolean
|
|
41
|
+
/** Affiche l'indicatif téléphonique (défaut true) */
|
|
42
|
+
showDial?: boolean
|
|
43
|
+
/** Label au-dessus du champ */
|
|
44
|
+
label?: string
|
|
45
|
+
/** Titre du panneau (sheet/modal/dialog) — défaut : label ou placeholder */
|
|
46
|
+
title?: string
|
|
47
|
+
/** Largeur du panneau (sheet/modal/dialog) */
|
|
48
|
+
width?: string
|
|
49
|
+
/** Seuil de drag (px) au-delà duquel on ferme en swipant vers le bas (mode sheet, défaut 80) */
|
|
50
|
+
dragThreshold?: number
|
|
51
|
+
/** Options spécifiques au mode sheet (width, style, class, height de la liste, searchPlaceholder) */
|
|
52
|
+
sheetOptions?: QCountryPickerModeOptions
|
|
53
|
+
/** Options spécifiques au mode modal */
|
|
54
|
+
modalOptions?: QCountryPickerModeOptions
|
|
55
|
+
/** Ne se ferme ni au backdrop ni à Échap (le drag ferme toujours) */
|
|
56
|
+
persistent?: boolean
|
|
57
|
+
/** Fond translucide du panneau (frosted glass) : true = 70%, ou valeur % */
|
|
58
|
+
translucent?: boolean | number
|
|
59
|
+
/** Arrondi du panneau : true (défaut) | false | valeur CSS */
|
|
60
|
+
rounded?: boolean | string
|
|
61
|
+
/** Hauteur du panneau (sinon max-height 80vh) */
|
|
62
|
+
height?: string
|
|
63
|
+
/** Style du panneau (variables de thème pour les contenus téléportés) */
|
|
64
|
+
contentStyle?: Record<string, string>
|
|
65
|
+
/** Fond transparent (style des champs Quasar) */
|
|
66
|
+
outlined?: boolean
|
|
67
|
+
/** Fond grisé sans bordure (style Quasar) */
|
|
68
|
+
filled?: boolean
|
|
69
|
+
/** Aucune bordure ni fond */
|
|
70
|
+
borderless?: boolean
|
|
71
|
+
/** Champ compact */
|
|
72
|
+
dense?: boolean
|
|
73
|
+
/** Champ désactivé */
|
|
74
|
+
disable?: boolean
|
|
75
|
+
/** Lecture seule (le popup ne s'ouvre pas) */
|
|
76
|
+
readonly?: boolean
|
|
77
|
+
/** Thème sombre */
|
|
78
|
+
dark?: boolean
|
|
79
|
+
/** Texte quand aucun pays ne correspond (défaut "No matching countries") */
|
|
80
|
+
noOptionsLabel?: string
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
const props = withDefaults(defineProps<Props>(), {
|
|
84
|
+
modelValue: "",
|
|
85
|
+
countries: () => COUNTRIES,
|
|
86
|
+
language: "en",
|
|
87
|
+
mode: "inline",
|
|
88
|
+
placeholder: "Select a country",
|
|
89
|
+
searchable: true,
|
|
90
|
+
showDial: true,
|
|
91
|
+
label: "",
|
|
92
|
+
title: "",
|
|
93
|
+
width: "",
|
|
94
|
+
dragThreshold: 80,
|
|
95
|
+
outlined: false,
|
|
96
|
+
filled: false,
|
|
97
|
+
borderless: false,
|
|
98
|
+
dense: false,
|
|
99
|
+
disable: false,
|
|
100
|
+
readonly: false,
|
|
101
|
+
dark: false,
|
|
102
|
+
persistent: false,
|
|
103
|
+
translucent: false,
|
|
104
|
+
rounded: true,
|
|
105
|
+
height: "",
|
|
106
|
+
contentStyle: undefined,
|
|
107
|
+
noOptionsLabel: "No matching countries",
|
|
108
|
+
})
|
|
109
|
+
|
|
110
|
+
const emit = defineEmits<{ "update:modelValue": [value: string] }>()
|
|
111
|
+
|
|
112
|
+
const isPanelMode = computed(() => props.mode !== "inline")
|
|
113
|
+
|
|
114
|
+
const rootEl = ref<HTMLElement | null>(null)
|
|
115
|
+
const sheetRef = ref<HTMLElement | null>(null)
|
|
116
|
+
const searchInput = ref<HTMLInputElement | null>(null)
|
|
117
|
+
const open = ref(false)
|
|
118
|
+
const query = ref("")
|
|
119
|
+
const activeIndex = ref(0)
|
|
120
|
+
|
|
121
|
+
// « Retour » navigateur → ferme le panneau (sheet/modal/dialog) au lieu de naviguer
|
|
122
|
+
const panelOpen = computed(() => isPanelMode.value && open.value)
|
|
123
|
+
useOverlayBack(panelOpen, () => close(), "QCountryPicker")
|
|
124
|
+
|
|
125
|
+
const selected = computed(() =>
|
|
126
|
+
props.countries.find((c) => c.code === props.modelValue),
|
|
127
|
+
)
|
|
128
|
+
|
|
129
|
+
/** Nom du pays dans la langue courante (fallback : nom anglais) */
|
|
130
|
+
const countryName = (c: Country): string =>
|
|
131
|
+
props.language === "fr" ? (c.nameFr ?? c.name) : c.name
|
|
132
|
+
|
|
133
|
+
const filtered = computed(() => {
|
|
134
|
+
const q = query.value.trim().toLowerCase()
|
|
135
|
+
if (!q) return props.countries
|
|
136
|
+
return props.countries.filter(
|
|
137
|
+
(c) =>
|
|
138
|
+
countryName(c).toLowerCase().includes(q) ||
|
|
139
|
+
(props.language === "fr"
|
|
140
|
+
? c.name.toLowerCase().includes(q)
|
|
141
|
+
: (c.nameFr ?? "").toLowerCase().includes(q)) ||
|
|
142
|
+
c.code.toLowerCase().includes(q) ||
|
|
143
|
+
c.dial.replace(/\s+/g, "").includes(q.replace(/\s+/g, "")),
|
|
144
|
+
)
|
|
145
|
+
})
|
|
146
|
+
|
|
147
|
+
const select = (c: Country) => {
|
|
148
|
+
emit("update:modelValue", c.code)
|
|
149
|
+
close()
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
const close = () => {
|
|
153
|
+
open.value = false
|
|
154
|
+
query.value = ""
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
const openPopup = () => {
|
|
158
|
+
if (props.disable || props.readonly) return
|
|
159
|
+
open.value = true
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
const onFieldClick = () => {
|
|
163
|
+
if (isPanelMode.value) openPopup()
|
|
164
|
+
else if (open.value) close()
|
|
165
|
+
else openPopup()
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
// — clavier : flèches + entrée (liste ouverte), échap pour fermer —
|
|
169
|
+
const onKeydown = (e: KeyboardEvent) => {
|
|
170
|
+
if (!open.value) return
|
|
171
|
+
if (e.key === "Escape") {
|
|
172
|
+
e.preventDefault()
|
|
173
|
+
close()
|
|
174
|
+
return
|
|
175
|
+
}
|
|
176
|
+
if (e.key === "ArrowDown" || e.key === "ArrowUp") {
|
|
177
|
+
e.preventDefault()
|
|
178
|
+
const list = filtered.value
|
|
179
|
+
if (!list.length) return
|
|
180
|
+
const dir = e.key === "ArrowDown" ? 1 : -1
|
|
181
|
+
activeIndex.value = (activeIndex.value + dir + list.length) % list.length
|
|
182
|
+
return
|
|
183
|
+
}
|
|
184
|
+
if (e.key === "Enter" && filtered.value[activeIndex.value]) {
|
|
185
|
+
e.preventDefault()
|
|
186
|
+
select(filtered.value[activeIndex.value]!)
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
// — Fermetures : clic extérieur, Échap, scroll lock (modes panneau) —
|
|
191
|
+
const onDocMousedown = (e: MouseEvent) => {
|
|
192
|
+
const target = e.target as Node
|
|
193
|
+
if (rootEl.value?.contains(target)) return
|
|
194
|
+
if (sheetRef.value?.contains(target)) return
|
|
195
|
+
if (isPanelMode.value && props.persistent) return
|
|
196
|
+
close()
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
const onDocKeydown = (e: KeyboardEvent) => {
|
|
200
|
+
if (e.key === "Escape" && open.value && !props.persistent) close()
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
watch(open, (v) => {
|
|
204
|
+
activeIndex.value = 0
|
|
205
|
+
if (v) nextTick(() => searchInput.value?.focus())
|
|
206
|
+
if (isPanelMode.value) {
|
|
207
|
+
document.body.style.overflow = v ? "hidden" : ""
|
|
208
|
+
}
|
|
209
|
+
})
|
|
210
|
+
|
|
211
|
+
onMounted(() => {
|
|
212
|
+
if (typeof document !== "undefined") {
|
|
213
|
+
document.addEventListener("mousedown", onDocMousedown)
|
|
214
|
+
document.addEventListener("keydown", onDocKeydown)
|
|
215
|
+
}
|
|
216
|
+
})
|
|
217
|
+
onBeforeUnmount(() => {
|
|
218
|
+
if (typeof document !== "undefined") {
|
|
219
|
+
document.removeEventListener("mousedown", onDocMousedown)
|
|
220
|
+
document.removeEventListener("keydown", onDocKeydown)
|
|
221
|
+
document.body.style.overflow = ""
|
|
222
|
+
}
|
|
223
|
+
})
|
|
224
|
+
|
|
225
|
+
const sheetTitle = computed(() => props.title || props.label || props.placeholder)
|
|
226
|
+
|
|
227
|
+
/** Options du mode courant (sheet → sheetOptions, modal → modalOptions) */
|
|
228
|
+
const modeOptions = computed<QCountryPickerModeOptions | undefined>(() => {
|
|
229
|
+
if (props.mode === "sheet") return props.sheetOptions
|
|
230
|
+
if (props.mode === "modal") return props.modalOptions
|
|
231
|
+
return undefined
|
|
232
|
+
})
|
|
233
|
+
|
|
234
|
+
const sheetStyle = computed<Record<string, string> | undefined>(() => {
|
|
235
|
+
const w = modeOptions.value?.width ?? props.width
|
|
236
|
+
const style: Record<string, string> = {}
|
|
237
|
+
if (w) {
|
|
238
|
+
style.width = w
|
|
239
|
+
style.maxWidth = w
|
|
240
|
+
}
|
|
241
|
+
if (props.height) style.height = props.height
|
|
242
|
+
if (typeof props.rounded === "string") style.borderRadius = props.rounded
|
|
243
|
+
if (props.translucent === true || typeof props.translucent === "number") {
|
|
244
|
+
style["--q-translucent-opacity"] = `${typeof props.translucent === "number" ? props.translucent : 70}%`
|
|
245
|
+
}
|
|
246
|
+
return Object.keys(style).length ? style : undefined
|
|
247
|
+
})
|
|
248
|
+
|
|
249
|
+
/** Hauteur de la liste scrollable (options du mode courant) */
|
|
250
|
+
const listHeightStyle = computed<Record<string, string> | undefined>(() =>
|
|
251
|
+
modeOptions.value?.height ? { height: modeOptions.value.height } : undefined,
|
|
252
|
+
)
|
|
253
|
+
|
|
254
|
+
/** Classes additionnelles du panneau (options du mode + modifiers bottom-sheet) */
|
|
255
|
+
const sheetClass = computed(() =>
|
|
256
|
+
cn(
|
|
257
|
+
modeOptions.value?.class,
|
|
258
|
+
props.rounded === false && "q-country-picker__sheet--square",
|
|
259
|
+
props.dark && "q-country-picker__sheet--dark",
|
|
260
|
+
(props.translucent === true || typeof props.translucent === "number") &&
|
|
261
|
+
"q-country-picker__sheet--translucent",
|
|
262
|
+
),
|
|
263
|
+
)
|
|
264
|
+
|
|
265
|
+
const searchPlaceholder = computed(
|
|
266
|
+
() => modeOptions.value?.searchPlaceholder ?? props.placeholder,
|
|
267
|
+
)
|
|
268
|
+
|
|
269
|
+
// — Drag vers le bas pour fermer (mode sheet, pattern bottom-sheet) —
|
|
270
|
+
const dragging = ref(false)
|
|
271
|
+
let startY = 0
|
|
272
|
+
let currentDy = 0
|
|
273
|
+
|
|
274
|
+
const onHandleDown = (e: PointerEvent) => {
|
|
275
|
+
if (props.mode !== "sheet") return
|
|
276
|
+
;(e.target as HTMLElement).setPointerCapture(e.pointerId)
|
|
277
|
+
startY = e.clientY
|
|
278
|
+
currentDy = 0
|
|
279
|
+
dragging.value = true
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
const onHandleMove = (e: PointerEvent) => {
|
|
283
|
+
if (!dragging.value) return
|
|
284
|
+
currentDy = Math.max(0, e.clientY - startY)
|
|
285
|
+
if (sheetRef.value) sheetRef.value.style.transform = `translateY(${currentDy}px)`
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
const onHandleUp = () => {
|
|
289
|
+
if (!dragging.value) return
|
|
290
|
+
dragging.value = false
|
|
291
|
+
if (currentDy > props.dragThreshold) close()
|
|
292
|
+
else if (sheetRef.value) sheetRef.value.style.transform = ""
|
|
293
|
+
currentDy = 0
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
const fieldClasses = computed(() =>
|
|
297
|
+
cn(
|
|
298
|
+
"q-country-picker q-input",
|
|
299
|
+
props.outlined && "q-field--outlined",
|
|
300
|
+
props.filled && "q-field--filled",
|
|
301
|
+
props.borderless && "q-field--borderless",
|
|
302
|
+
props.dense && "q-field--dense",
|
|
303
|
+
props.disable && "q-field--disabled",
|
|
304
|
+
),
|
|
305
|
+
)
|
|
306
|
+
</script>
|
|
307
|
+
|
|
308
|
+
<template>
|
|
309
|
+
<div ref="rootEl" :class="fieldClasses">
|
|
310
|
+
<label v-if="label" class="q-field__label-stack">{{ label }}</label>
|
|
311
|
+
<div
|
|
312
|
+
class="q-field__control q-country-picker__field"
|
|
313
|
+
:class="{ 'q-country-picker__field--open': open }"
|
|
314
|
+
role="combobox"
|
|
315
|
+
tabindex="0"
|
|
316
|
+
:aria-expanded="open ? 'true' : 'false'"
|
|
317
|
+
aria-haspopup="listbox"
|
|
318
|
+
@click="onFieldClick"
|
|
319
|
+
@keydown="onKeydown"
|
|
320
|
+
>
|
|
321
|
+
<span v-if="selected" class="q-country-picker__flag" aria-hidden="true">
|
|
322
|
+
{{ flagOf(selected.code) }}
|
|
323
|
+
</span>
|
|
324
|
+
<span
|
|
325
|
+
class="q-country-picker__label"
|
|
326
|
+
:class="{ 'q-country-picker__label--placeholder': !selected }"
|
|
327
|
+
>
|
|
328
|
+
{{ selected ? countryName(selected) : placeholder }}
|
|
329
|
+
</span>
|
|
330
|
+
<span v-if="selected && showDial" class="q-country-picker__dial">
|
|
331
|
+
{{ selected.dial }}
|
|
332
|
+
</span>
|
|
333
|
+
<Icon
|
|
334
|
+
:icon="icons.chevronDown"
|
|
335
|
+
class="q-country-picker__arrow"
|
|
336
|
+
:class="{ 'q-country-picker__arrow--rotated': open }"
|
|
337
|
+
aria-hidden="true"
|
|
338
|
+
/>
|
|
339
|
+
</div>
|
|
340
|
+
|
|
341
|
+
<!-- Mode inline : liste sous le champ -->
|
|
342
|
+
<div v-if="open && mode === 'inline'" class="q-country-picker__popup" role="listbox">
|
|
343
|
+
<div v-if="searchable" class="q-country-picker__search">
|
|
344
|
+
<Icon :icon="icons.search" class="q-country-picker__search-icon" aria-hidden="true" />
|
|
345
|
+
<input
|
|
346
|
+
ref="searchInput"
|
|
347
|
+
v-model="query"
|
|
348
|
+
class="q-country-picker__search-input"
|
|
349
|
+
:placeholder="searchPlaceholder"
|
|
350
|
+
@keydown="onKeydown"
|
|
351
|
+
/>
|
|
352
|
+
</div>
|
|
353
|
+
<div class="q-country-picker__list" :style="listHeightStyle">
|
|
354
|
+
<button
|
|
355
|
+
v-for="(c, i) in filtered"
|
|
356
|
+
:key="c.code"
|
|
357
|
+
type="button"
|
|
358
|
+
role="option"
|
|
359
|
+
:aria-selected="c.code === modelValue ? 'true' : 'false'"
|
|
360
|
+
class="q-country-picker__option"
|
|
361
|
+
:class="{
|
|
362
|
+
'q-country-picker__option--active': i === activeIndex,
|
|
363
|
+
'q-country-picker__option--selected': c.code === modelValue,
|
|
364
|
+
}"
|
|
365
|
+
@mouseenter="activeIndex = i"
|
|
366
|
+
@mousedown.prevent="select(c)"
|
|
367
|
+
>
|
|
368
|
+
<span class="q-country-picker__flag" aria-hidden="true">{{ flagOf(c.code) }}</span>
|
|
369
|
+
<span class="q-country-picker__name">{{ countryName(c) }}</span>
|
|
370
|
+
<span v-if="showDial" class="q-country-picker__dial">{{ c.dial }}</span>
|
|
371
|
+
</button>
|
|
372
|
+
<div v-if="!filtered.length" class="q-country-picker__empty">
|
|
373
|
+
{{ noOptionsLabel }}
|
|
374
|
+
</div>
|
|
375
|
+
</div>
|
|
376
|
+
</div>
|
|
377
|
+
|
|
378
|
+
<!-- Modes panneau : overlay téléporté (modal / sheet / dialog) -->
|
|
379
|
+
<Teleport to="body">
|
|
380
|
+
<Transition name="q-country-modal">
|
|
381
|
+
<div
|
|
382
|
+
v-if="open && mode !== 'inline'"
|
|
383
|
+
class="q-country-picker__overlay"
|
|
384
|
+
:class="`q-country-picker__overlay--${mode}`"
|
|
385
|
+
role="presentation"
|
|
386
|
+
>
|
|
387
|
+
<div
|
|
388
|
+
ref="sheetRef"
|
|
389
|
+
class="q-country-picker__sheet"
|
|
390
|
+
:class="[
|
|
391
|
+
`q-country-picker__sheet--${mode}`,
|
|
392
|
+
sheetClass,
|
|
393
|
+
dragging && 'q-country-picker__sheet--dragging',
|
|
394
|
+
]"
|
|
395
|
+
:style="[sheetStyle, modeOptions?.style, props.contentStyle]"
|
|
396
|
+
role="dialog"
|
|
397
|
+
aria-modal="true"
|
|
398
|
+
:aria-label="sheetTitle"
|
|
399
|
+
>
|
|
400
|
+
<div
|
|
401
|
+
v-if="mode === 'sheet'"
|
|
402
|
+
class="q-country-picker__handle"
|
|
403
|
+
@pointerdown="onHandleDown"
|
|
404
|
+
@pointermove="onHandleMove"
|
|
405
|
+
@pointerup="onHandleUp"
|
|
406
|
+
@pointercancel="onHandleUp"
|
|
407
|
+
>
|
|
408
|
+
<div class="q-country-picker__handle-bar" />
|
|
409
|
+
</div>
|
|
410
|
+
<div class="q-country-picker__sheet-header">
|
|
411
|
+
<span class="q-country-picker__sheet-title">{{ sheetTitle }}</span>
|
|
412
|
+
<button
|
|
413
|
+
type="button"
|
|
414
|
+
class="q-country-picker__sheet-close"
|
|
415
|
+
:aria-label="`Close ${sheetTitle}`"
|
|
416
|
+
@click="close"
|
|
417
|
+
>
|
|
418
|
+
<Icon :icon="icons.x" aria-hidden="true" />
|
|
419
|
+
</button>
|
|
420
|
+
</div>
|
|
421
|
+
<div class="q-country-picker__sheet-body">
|
|
422
|
+
<div v-if="searchable" class="q-country-picker__search">
|
|
423
|
+
<Icon :icon="icons.search" class="q-country-picker__search-icon" aria-hidden="true" />
|
|
424
|
+
<input
|
|
425
|
+
ref="searchInput"
|
|
426
|
+
v-model="query"
|
|
427
|
+
class="q-country-picker__search-input"
|
|
428
|
+
:placeholder="searchPlaceholder"
|
|
429
|
+
@keydown="onKeydown"
|
|
430
|
+
/>
|
|
431
|
+
</div>
|
|
432
|
+
<div class="q-country-picker__list" :style="listHeightStyle">
|
|
433
|
+
<button
|
|
434
|
+
v-for="(c, i) in filtered"
|
|
435
|
+
:key="c.code"
|
|
436
|
+
type="button"
|
|
437
|
+
role="option"
|
|
438
|
+
:aria-selected="c.code === modelValue ? 'true' : 'false'"
|
|
439
|
+
class="q-country-picker__option"
|
|
440
|
+
:class="{
|
|
441
|
+
'q-country-picker__option--active': i === activeIndex,
|
|
442
|
+
'q-country-picker__option--selected': c.code === modelValue,
|
|
443
|
+
}"
|
|
444
|
+
@mouseenter="activeIndex = i"
|
|
445
|
+
@mousedown.prevent="select(c)"
|
|
446
|
+
>
|
|
447
|
+
<span class="q-country-picker__flag" aria-hidden="true">{{ flagOf(c.code) }}</span>
|
|
448
|
+
<span class="q-country-picker__name">{{ countryName(c) }}</span>
|
|
449
|
+
<span v-if="showDial" class="q-country-picker__dial">{{ c.dial }}</span>
|
|
450
|
+
</button>
|
|
451
|
+
<div v-if="!filtered.length" class="q-country-picker__empty">
|
|
452
|
+
{{ noOptionsLabel }}
|
|
453
|
+
</div>
|
|
454
|
+
</div>
|
|
455
|
+
</div>
|
|
456
|
+
</div>
|
|
457
|
+
</div>
|
|
458
|
+
</Transition>
|
|
459
|
+
</Teleport>
|
|
460
|
+
</div>
|
|
461
|
+
</template>
|
|
462
|
+
|
|
463
|
+
<style scoped>
|
|
464
|
+
.q-country-picker {
|
|
465
|
+
position: relative;
|
|
466
|
+
}
|
|
467
|
+
|
|
468
|
+
.q-country-picker__field {
|
|
469
|
+
cursor: pointer;
|
|
470
|
+
}
|
|
471
|
+
.q-country-picker__field--open,
|
|
472
|
+
.q-country-picker__field:focus-within {
|
|
473
|
+
border-color: var(--primary);
|
|
474
|
+
box-shadow: 0 0 0 1px var(--primary);
|
|
475
|
+
}
|
|
476
|
+
|
|
477
|
+
.q-country-picker__flag {
|
|
478
|
+
font-size: 18px;
|
|
479
|
+
line-height: 1;
|
|
480
|
+
}
|
|
481
|
+
|
|
482
|
+
.q-country-picker__label {
|
|
483
|
+
flex: 1;
|
|
484
|
+
min-width: 0;
|
|
485
|
+
overflow: hidden;
|
|
486
|
+
text-overflow: ellipsis;
|
|
487
|
+
white-space: nowrap;
|
|
488
|
+
font-size: 14px;
|
|
489
|
+
color: var(--foreground);
|
|
490
|
+
}
|
|
491
|
+
.q-country-picker__label--placeholder {
|
|
492
|
+
color: #8b93a1;
|
|
493
|
+
}
|
|
494
|
+
|
|
495
|
+
.q-country-picker__dial {
|
|
496
|
+
font-size: 12.5px;
|
|
497
|
+
color: #8b93a1;
|
|
498
|
+
font-variant-numeric: tabular-nums;
|
|
499
|
+
white-space: nowrap;
|
|
500
|
+
}
|
|
501
|
+
|
|
502
|
+
.q-country-picker__arrow {
|
|
503
|
+
font-size: 16px;
|
|
504
|
+
color: #8b93a1;
|
|
505
|
+
transition: transform 0.2s ease;
|
|
506
|
+
}
|
|
507
|
+
.q-country-picker__arrow--rotated {
|
|
508
|
+
transform: rotate(180deg);
|
|
509
|
+
}
|
|
510
|
+
|
|
511
|
+
/* — liste inline — */
|
|
512
|
+
.q-country-picker__popup {
|
|
513
|
+
position: absolute;
|
|
514
|
+
top: calc(100% + 4px);
|
|
515
|
+
left: 0;
|
|
516
|
+
right: 0;
|
|
517
|
+
z-index: 1000;
|
|
518
|
+
overflow: hidden;
|
|
519
|
+
background-color: #fff;
|
|
520
|
+
border: 1px solid rgb(0 0 0 / 0.12);
|
|
521
|
+
border-radius: var(--q-radius);
|
|
522
|
+
box-shadow: 0 8px 24px rgb(0 0 0 / 0.14);
|
|
523
|
+
}
|
|
524
|
+
.dark .q-country-picker__popup {
|
|
525
|
+
background-color: var(--card);
|
|
526
|
+
border-color: var(--border);
|
|
527
|
+
color: var(--foreground);
|
|
528
|
+
}
|
|
529
|
+
|
|
530
|
+
.q-country-picker__search {
|
|
531
|
+
display: flex;
|
|
532
|
+
align-items: center;
|
|
533
|
+
gap: 8px;
|
|
534
|
+
padding: 8px 10px;
|
|
535
|
+
border-bottom: 1px solid rgb(0 0 0 / 0.08);
|
|
536
|
+
}
|
|
537
|
+
.dark .q-country-picker__search {
|
|
538
|
+
border-bottom-color: var(--border);
|
|
539
|
+
}
|
|
540
|
+
.q-country-picker__search-icon {
|
|
541
|
+
color: #8b93a1;
|
|
542
|
+
font-size: 15px;
|
|
543
|
+
}
|
|
544
|
+
.q-country-picker__search-input {
|
|
545
|
+
flex: 1;
|
|
546
|
+
min-width: 0;
|
|
547
|
+
border: none;
|
|
548
|
+
outline: none;
|
|
549
|
+
background: transparent;
|
|
550
|
+
color: var(--foreground);
|
|
551
|
+
font-size: 13.5px;
|
|
552
|
+
}
|
|
553
|
+
|
|
554
|
+
.q-country-picker__list {
|
|
555
|
+
max-height: 260px;
|
|
556
|
+
overflow-y: auto;
|
|
557
|
+
padding: 4px;
|
|
558
|
+
}
|
|
559
|
+
.q-country-picker__option {
|
|
560
|
+
display: flex;
|
|
561
|
+
align-items: center;
|
|
562
|
+
gap: 10px;
|
|
563
|
+
width: 100%;
|
|
564
|
+
padding: 7px 10px;
|
|
565
|
+
border: none;
|
|
566
|
+
border-radius: 6px;
|
|
567
|
+
background: transparent;
|
|
568
|
+
color: var(--foreground);
|
|
569
|
+
font-size: 13.5px;
|
|
570
|
+
text-align: left;
|
|
571
|
+
cursor: pointer;
|
|
572
|
+
}
|
|
573
|
+
.q-country-picker__option--active {
|
|
574
|
+
background: rgb(25 118 210 / 0.08);
|
|
575
|
+
}
|
|
576
|
+
.q-country-picker__option--selected {
|
|
577
|
+
color: var(--primary);
|
|
578
|
+
font-weight: 600;
|
|
579
|
+
}
|
|
580
|
+
.q-country-picker__name {
|
|
581
|
+
flex: 1;
|
|
582
|
+
min-width: 0;
|
|
583
|
+
overflow: hidden;
|
|
584
|
+
text-overflow: ellipsis;
|
|
585
|
+
white-space: nowrap;
|
|
586
|
+
}
|
|
587
|
+
.q-country-picker__empty {
|
|
588
|
+
padding: 14px 10px;
|
|
589
|
+
font-size: 13px;
|
|
590
|
+
color: #8b93a1;
|
|
591
|
+
text-align: center;
|
|
592
|
+
}
|
|
593
|
+
</style>
|