@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
|
@@ -0,0 +1,410 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
// QMap — carte interactive, plusieurs fournisseurs (moteurs dans `lib/mapEngine.ts`).
|
|
3
|
+
//
|
|
4
|
+
// <q-map center="[2.35, 48.85]" :zoom="12" :marks="[{ type: 'marker', position: [2.35, 48.85], label: 'Paris' }]" />
|
|
5
|
+
// <q-map provider="leaflet" tiles="https://mon-serveur/{z}/{x}/{y}.png" />
|
|
6
|
+
//
|
|
7
|
+
// - **Client seulement** : le moteur (SDK JS MapTiler ou Leaflet) est importé
|
|
8
|
+
// dynamiquement au montage → ni WebGL ni DOM au prerender SSR.
|
|
9
|
+
// - **Fournisseurs** : `maptiler` (défaut, clé requise), `maplibre` (MapLibre GL JS seul,
|
|
10
|
+
// style libre sans clé), `openstreetmap` (raster sans clé, via le SDK MapTiler),
|
|
11
|
+
// `leaflet` (moteur Leaflet, raster sans clé, tuiles XYZ quelconques). Sans clé
|
|
12
|
+
// MapTiler, `maptiler` retombe sur le raster OpenStreetMap avec un avertissement — le
|
|
13
|
+
// cadre n'est jamais vide.
|
|
14
|
+
// - `marks` : marques en objets littéraux plats (`marker`, `popup`), comme les marks de
|
|
15
|
+
// `<q-chart>` — voir `lib/map.ts`.
|
|
16
|
+
// - Thème : la variante sombre du style suit la classe `.dark` du document (styles
|
|
17
|
+
// vectoriels) ; les bulles et les contrôles des deux moteurs sont peints avec les tokens
|
|
18
|
+
// (`styles/main.css`).
|
|
19
|
+
import { computed, onBeforeUnmount, onMounted, ref, shallowRef, watch } from "vue"
|
|
20
|
+
import type { CSSProperties } from "vue"
|
|
21
|
+
import { COLOR_TOKENS } from "../lib/chart"
|
|
22
|
+
import { normalizeCssColor, probeContext } from "../lib/color"
|
|
23
|
+
import {
|
|
24
|
+
DEFAULT_CENTER,
|
|
25
|
+
DEFAULT_ZOOM,
|
|
26
|
+
isKnownProvider,
|
|
27
|
+
leafletOptionsOf,
|
|
28
|
+
mapHeight,
|
|
29
|
+
maplibreOptionsOf,
|
|
30
|
+
mapOptionsOf,
|
|
31
|
+
marksOf,
|
|
32
|
+
parsePosition,
|
|
33
|
+
parseZoom,
|
|
34
|
+
providerOf,
|
|
35
|
+
type QMapMark,
|
|
36
|
+
type QMapPick,
|
|
37
|
+
type QMapPosition,
|
|
38
|
+
type QMapProvider,
|
|
39
|
+
} from "../lib/map"
|
|
40
|
+
import { createMapEngine, type MapEngine } from "../lib/mapEngine"
|
|
41
|
+
import { useComponentProps } from "../lib/useComponentProps"
|
|
42
|
+
|
|
43
|
+
interface Props {
|
|
44
|
+
/** Fournisseur : `maptiler` (défaut), `maplibre`, `openstreetmap` ou `leaflet` —
|
|
45
|
+
* `osm` et `openstreet` sont acceptés comme alias d'`openstreetmap` */
|
|
46
|
+
provider?: "maptiler" | "maplibre" | "openstreetmap" | "leaflet" | "osm"
|
|
47
|
+
/** Clé d'API MapTiler (repli : `componentProps.QMap.apiKey` du QConfigProvider).
|
|
48
|
+
* Sans effet avec `leaflet`. */
|
|
49
|
+
apiKey?: string
|
|
50
|
+
/** Style de carte : nom court (`streets`, `satellite`, `outdoor`, `dataviz`…), ID ou URL
|
|
51
|
+
* de style. Avec `provider="maplibre"` : `demotiles` (défaut), `openstreetmap`, une URL
|
|
52
|
+
* de style ou un objet de style. Sans effet avec `leaflet` (tuiles raster).
|
|
53
|
+
* Nommé `map-style` (et non `style`) pour ne pas entrer en collision avec l'attribut
|
|
54
|
+
* CSS `style` de Vue. */
|
|
55
|
+
mapStyle?: string
|
|
56
|
+
/** Gabarit de tuiles XYZ des fournisseurs raster (`openstreetmap`, `leaflet` — et
|
|
57
|
+
* `maplibre` avec `map-style="openstreetmap"`) — défaut : les tuiles OpenStreetMap */
|
|
58
|
+
tiles?: string
|
|
59
|
+
/** Attribution des tuiles (obligatoire pour OpenStreetMap) */
|
|
60
|
+
attribution?: string
|
|
61
|
+
/** Centre de la carte : `[lng, lat]`, `"lng,lat"` ou `{ lng, lat }` — Leaflet reçoit la
|
|
62
|
+
* même valeur (la conversion `[lat, lng]` est faite par le moteur) */
|
|
63
|
+
center?: QMapPosition
|
|
64
|
+
/** Zoom (0–24) */
|
|
65
|
+
zoom?: number | string
|
|
66
|
+
/** Hauteur du cadre : nombre → px, ou longueur CSS (`"40vh"`) */
|
|
67
|
+
height?: number | string
|
|
68
|
+
/** Marques : marqueurs (bulle optionnelle) et bulles seules */
|
|
69
|
+
marks?: QMapMark[]
|
|
70
|
+
/** Contrôles : `true` (position par défaut), `false`, ou un coin (`top-right`…).
|
|
71
|
+
* Ignorés par `leaflet` (contrôles natifs de Leaflet). */
|
|
72
|
+
navigation?: boolean | string
|
|
73
|
+
geolocate?: boolean | string
|
|
74
|
+
scale?: boolean | string
|
|
75
|
+
fullscreen?: boolean | string
|
|
76
|
+
/** Relief 3D — styles MapTiler uniquement (`maplibre` : passez une source DEM via
|
|
77
|
+
* `options.terrain`) */
|
|
78
|
+
terrain?: boolean
|
|
79
|
+
/** Projection : `mercator` (défaut) ou `globe` — MapTiler et MapLibre */
|
|
80
|
+
projection?: "mercator" | "globe"
|
|
81
|
+
/** Force la variante sombre du style (sinon suivi du mode clair/sombre du document) */
|
|
82
|
+
dark?: boolean
|
|
83
|
+
/** Libellé accessible du cadre */
|
|
84
|
+
label?: string
|
|
85
|
+
/** Options brutes du moteur — fusionnées en dernier. Pour Leaflet : options de `L.map()`,
|
|
86
|
+
* `options.tileLayer` allant à `L.tileLayer()`. */
|
|
87
|
+
options?: Record<string, any>
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
const props = withDefaults(defineProps<Props>(), {
|
|
91
|
+
provider: "maptiler",
|
|
92
|
+
apiKey: "",
|
|
93
|
+
mapStyle: "streets",
|
|
94
|
+
tiles: "",
|
|
95
|
+
attribution: "",
|
|
96
|
+
label: "Carte",
|
|
97
|
+
})
|
|
98
|
+
|
|
99
|
+
const emit = defineEmits<{
|
|
100
|
+
/** Carte prête — reçoit l'instance native (`Map` du SDK MapTiler, `L.Map` de Leaflet) */
|
|
101
|
+
ready: [map: any]
|
|
102
|
+
/** Une marque a été cliquée */
|
|
103
|
+
pick: [payload: QMapPick]
|
|
104
|
+
/** Erreur du moteur : échec de construction (surcouche « Carte indisponible ») ou
|
|
105
|
+
* erreur signalée ensuite (tuile, style, réseau) — seule la première est émise */
|
|
106
|
+
error: [error: unknown]
|
|
107
|
+
}>()
|
|
108
|
+
|
|
109
|
+
/** Valeurs globales du composant (`componentProps.QMap` du QConfigProvider) */
|
|
110
|
+
const defaults = useComponentProps("QMap")
|
|
111
|
+
|
|
112
|
+
const el = ref<HTMLElement | null>(null)
|
|
113
|
+
const engine = shallowRef<MapEngine | null>(null)
|
|
114
|
+
const loading = ref(true)
|
|
115
|
+
const failure = ref<string | null>(null)
|
|
116
|
+
|
|
117
|
+
// ─── Thème clair / sombre ──────────────────────────────────────────────────────
|
|
118
|
+
|
|
119
|
+
const documentDark = ref(false)
|
|
120
|
+
let themeObserver: MutationObserver | null = null
|
|
121
|
+
|
|
122
|
+
const isDocumentDark = () =>
|
|
123
|
+
typeof document !== "undefined" && document.documentElement.classList.contains("dark")
|
|
124
|
+
|
|
125
|
+
/** `dark` explicite, sinon le mode du document (classe `.dark` sur `<html>`) */
|
|
126
|
+
const isDark = computed(() => props.dark ?? documentDark.value)
|
|
127
|
+
|
|
128
|
+
// ─── Valeurs résolues ──────────────────────────────────────────────────────────
|
|
129
|
+
|
|
130
|
+
const center = computed(() => parsePosition(props.center) ?? ([...DEFAULT_CENTER] as [number, number]))
|
|
131
|
+
const zoom = computed(() => parseZoom(props.zoom) ?? DEFAULT_ZOOM)
|
|
132
|
+
const frameStyle = computed<CSSProperties>(() => ({ height: mapHeight(props.height) }))
|
|
133
|
+
const apiKey = computed(() => props.apiKey || String(defaults.value.apiKey ?? ""))
|
|
134
|
+
|
|
135
|
+
/** Fournisseur effectif : `maptiler` sans clé retombe sur le raster OpenStreetMap. */
|
|
136
|
+
const provider = computed<QMapProvider>(() => {
|
|
137
|
+
const requested = providerOf(props.provider)
|
|
138
|
+
return requested === "maptiler" && !apiKey.value ? "openstreetmap" : requested
|
|
139
|
+
})
|
|
140
|
+
|
|
141
|
+
/** Marques normalisées (positions résolues, marques invalides écartées) */
|
|
142
|
+
const markSpecs = computed(() => marksOf(props.marks))
|
|
143
|
+
|
|
144
|
+
/** Options passées au moteur, selon le fournisseur. Pour MapTiler, `style` reste la
|
|
145
|
+
* valeur brute de la prop : c'est le moteur qui résout les noms courts (`MapStyle`). */
|
|
146
|
+
const engineOptions = computed<Record<string, any>>(() => {
|
|
147
|
+
const shared = {
|
|
148
|
+
center: center.value,
|
|
149
|
+
zoom: zoom.value,
|
|
150
|
+
tiles: props.tiles,
|
|
151
|
+
attribution: props.attribution,
|
|
152
|
+
options: props.options,
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
if (provider.value === "leaflet") return leafletOptionsOf(shared)
|
|
156
|
+
if (provider.value === "maplibre") return maplibreOptionsOf({ ...shared, style: props.mapStyle })
|
|
157
|
+
|
|
158
|
+
return mapOptionsOf({
|
|
159
|
+
...shared,
|
|
160
|
+
provider: provider.value,
|
|
161
|
+
apiKey: apiKey.value,
|
|
162
|
+
style: props.mapStyle,
|
|
163
|
+
navigation: props.navigation,
|
|
164
|
+
geolocate: props.geolocate,
|
|
165
|
+
scale: props.scale,
|
|
166
|
+
fullscreen: props.fullscreen,
|
|
167
|
+
})
|
|
168
|
+
})
|
|
169
|
+
|
|
170
|
+
/** Style à appliquer : la prop pour MapTiler (nom court, ID ou URL) et MapLibre (le
|
|
171
|
+
* moteur résout `demotiles` / `openstreetmap` / URL), le style raster construit pour
|
|
172
|
+
* `openstreetmap`, `undefined` pour Leaflet (pas de notion de style). */
|
|
173
|
+
const styleForProvider = computed<any>(() =>
|
|
174
|
+
provider.value === "leaflet"
|
|
175
|
+
? undefined
|
|
176
|
+
: provider.value === "openstreetmap"
|
|
177
|
+
? engineOptions.value.style
|
|
178
|
+
: props.mapStyle,
|
|
179
|
+
)
|
|
180
|
+
|
|
181
|
+
// ─── Couleurs des marques ──────────────────────────────────────────────────────
|
|
182
|
+
|
|
183
|
+
/** Token dnax (`primary`, `chart-2`…) lu sur le thème, ou couleur CSS — normalisée
|
|
184
|
+
* (`lib/color.ts`) car elle finit dans un attribut SVG/markup (cf. `.q-map` de main.css). */
|
|
185
|
+
const colorOf = (color?: string): string | undefined => {
|
|
186
|
+
if (typeof color !== "string" || color.trim() === "") return undefined
|
|
187
|
+
const value = color.trim()
|
|
188
|
+
const node = el.value
|
|
189
|
+
const isToken = !!node && (COLOR_TOKENS as readonly string[]).includes(value)
|
|
190
|
+
const raw = isToken ? getComputedStyle(node).getPropertyValue(`--${value}`).trim() : value
|
|
191
|
+
if (!raw) return undefined
|
|
192
|
+
return normalizeCssColor(raw, probeContext()) ?? raw
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
// ─── Cycle de vie ─────────────────────────────────────────────────────────────
|
|
196
|
+
|
|
197
|
+
let resizeObserver: ResizeObserver | null = null
|
|
198
|
+
/** Style (et mode sombre) appliqué par la dernière fois — évite un `setStyle` au montage */
|
|
199
|
+
let appliedStyle: unknown = null
|
|
200
|
+
let appliedDark: boolean | undefined
|
|
201
|
+
/** La carte a fini de charger (moteur prêt) */
|
|
202
|
+
let isReady = false
|
|
203
|
+
/** Minuteur de repli : ne jamais laisser la surcouche « Chargement… » indéfiniment */
|
|
204
|
+
let readyTimer: ReturnType<typeof setTimeout> | null = null
|
|
205
|
+
/** Erreurs signalées par le moteur (la 1re est journalisée et émise, les suivantes tues) */
|
|
206
|
+
let errorCount = 0
|
|
207
|
+
|
|
208
|
+
/** Délai au bout duquel on retire la surcouche même sans `ready` : les tuiles peuvent
|
|
209
|
+
* échouer (réseau, quota) sans que la carte soit inutilisable. */
|
|
210
|
+
const READY_TIMEOUT = 8000
|
|
211
|
+
|
|
212
|
+
const messageOf = (event: any): string => {
|
|
213
|
+
const error = event?.error ?? event
|
|
214
|
+
const message = error?.message ?? (typeof error === "string" ? error : "")
|
|
215
|
+
return message ? `Carte indisponible — ${message}` : "Carte indisponible"
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
const fail = (error: unknown) => {
|
|
219
|
+
console.warn("[q-map]", error)
|
|
220
|
+
loading.value = false
|
|
221
|
+
failure.value = messageOf(error)
|
|
222
|
+
emit("error", error)
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
/** Erreur signalée **par le moteur** une fois la carte lancée (tuile, style, réseau…) :
|
|
226
|
+
* la carte reste utilisable, on n'affiche donc pas d'erreur plein cadre — seulement le
|
|
227
|
+
* premier avertissement et l'événement `error`. Les échecs de construction, eux,
|
|
228
|
+
* passent par `fail()` (surcouche « Carte indisponible »). */
|
|
229
|
+
const noteError = (error: unknown) => {
|
|
230
|
+
if (errorCount++ > 0) return
|
|
231
|
+
console.warn("[q-map]", error)
|
|
232
|
+
emit("error", error)
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
/** Fournisseurs dont la carte est un raster : ils ignorent `map-style` (les tuiles
|
|
236
|
+
* passent par `tiles`), alors que `maptiler` et `maplibre` portent des styles. */
|
|
237
|
+
const RASTER_PROVIDERS: readonly QMapProvider[] = ["openstreetmap", "leaflet"]
|
|
238
|
+
|
|
239
|
+
/** Relief / projection : des méthodes du moteur, à appliquer une fois la carte chargée
|
|
240
|
+
* (au constructeur, MapTiler jette « Style is not done loading » sur `terrain`, et
|
|
241
|
+
* MapLibre attend un style chargé) puis à chaque changement. Chaque moteur ne fait que
|
|
242
|
+
* ce qu'il sait faire : `openstreetmap` et `leaflet` sont des no-op. */
|
|
243
|
+
const applyTerrainProjection = (target: MapEngine) => {
|
|
244
|
+
if (props.terrain) target.setTerrain(true)
|
|
245
|
+
if (props.projection) target.setProjection(props.projection)
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
onMounted(async () => {
|
|
249
|
+
documentDark.value = isDocumentDark()
|
|
250
|
+
if (typeof MutationObserver !== "undefined") {
|
|
251
|
+
themeObserver = new MutationObserver(() => {
|
|
252
|
+
documentDark.value = isDocumentDark()
|
|
253
|
+
})
|
|
254
|
+
themeObserver.observe(document.documentElement, {
|
|
255
|
+
attributes: true,
|
|
256
|
+
attributeFilter: ["class", "data-theme"],
|
|
257
|
+
})
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
const node = el.value
|
|
261
|
+
if (!node) return
|
|
262
|
+
|
|
263
|
+
if (props.provider === "maptiler" && !apiKey.value) {
|
|
264
|
+
console.warn(
|
|
265
|
+
"[q-map] aucune clé d'API MapTiler (prop `api-key` ou componentProps.QMap.apiKey) : " +
|
|
266
|
+
'repli sur le raster OpenStreetMap (provider="openstreetmap")',
|
|
267
|
+
)
|
|
268
|
+
}
|
|
269
|
+
if (!isKnownProvider(props.provider)) {
|
|
270
|
+
console.warn(
|
|
271
|
+
`[q-map] fournisseur \`${props.provider}\` inconnu : repli sur \`maptiler\` ` +
|
|
272
|
+
'(valeurs : "maptiler", "openstreetmap", "leaflet" — alias "osm", "openstreet")',
|
|
273
|
+
)
|
|
274
|
+
}
|
|
275
|
+
if (RASTER_PROVIDERS.includes(provider.value) && props.mapStyle !== "streets") {
|
|
276
|
+
console.warn(
|
|
277
|
+
`[q-map] \`map-style\` est sans effet avec provider="${props.provider}" (tuiles raster) : ` +
|
|
278
|
+
"utilisez la prop `tiles` pour choisir la source",
|
|
279
|
+
)
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
try {
|
|
283
|
+
const next = await createMapEngine({
|
|
284
|
+
provider: provider.value,
|
|
285
|
+
el: node,
|
|
286
|
+
options: engineOptions.value,
|
|
287
|
+
center: center.value,
|
|
288
|
+
zoom: zoom.value,
|
|
289
|
+
marks: markSpecs.value,
|
|
290
|
+
dark: isDark.value,
|
|
291
|
+
controls: {
|
|
292
|
+
navigation: props.navigation,
|
|
293
|
+
geolocate: props.geolocate,
|
|
294
|
+
scale: props.scale,
|
|
295
|
+
fullscreen: props.fullscreen,
|
|
296
|
+
},
|
|
297
|
+
color: colorOf,
|
|
298
|
+
pick: (payload) => emit("pick", payload),
|
|
299
|
+
error: noteError,
|
|
300
|
+
})
|
|
301
|
+
|
|
302
|
+
if (!el.value) {
|
|
303
|
+
next.destroy() // démonté pendant le chargement du moteur
|
|
304
|
+
return
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
engine.value = next
|
|
308
|
+
appliedStyle = styleForProvider.value
|
|
309
|
+
appliedDark = isDark.value
|
|
310
|
+
|
|
311
|
+
next.onReady((ready) => {
|
|
312
|
+
isReady = true
|
|
313
|
+
loading.value = false
|
|
314
|
+
if (readyTimer) clearTimeout(readyTimer)
|
|
315
|
+
readyTimer = null
|
|
316
|
+
applyTerrainProjection(ready)
|
|
317
|
+
emit("ready", ready.native)
|
|
318
|
+
})
|
|
319
|
+
|
|
320
|
+
// Repli : une carte dont les tuiles n'arrivent pas ne doit pas rester sous la
|
|
321
|
+
// surcouche — l'utilisateur voit la carte (fond neutre) et la console explique.
|
|
322
|
+
readyTimer = setTimeout(() => {
|
|
323
|
+
readyTimer = null
|
|
324
|
+
if (!isReady) {
|
|
325
|
+
console.warn("[q-map] la carte n'a pas signalé son chargement — surcouche retirée")
|
|
326
|
+
loading.value = false
|
|
327
|
+
}
|
|
328
|
+
}, READY_TIMEOUT)
|
|
329
|
+
|
|
330
|
+
if (typeof ResizeObserver !== "undefined") {
|
|
331
|
+
resizeObserver = new ResizeObserver(() => engine.value?.resize())
|
|
332
|
+
resizeObserver.observe(node)
|
|
333
|
+
}
|
|
334
|
+
} catch (error) {
|
|
335
|
+
fail(error)
|
|
336
|
+
}
|
|
337
|
+
})
|
|
338
|
+
|
|
339
|
+
onBeforeUnmount(() => {
|
|
340
|
+
themeObserver?.disconnect()
|
|
341
|
+
themeObserver = null
|
|
342
|
+
resizeObserver?.disconnect()
|
|
343
|
+
resizeObserver = null
|
|
344
|
+
if (readyTimer) clearTimeout(readyTimer)
|
|
345
|
+
readyTimer = null
|
|
346
|
+
isReady = false
|
|
347
|
+
engine.value?.destroy()
|
|
348
|
+
engine.value = null
|
|
349
|
+
})
|
|
350
|
+
|
|
351
|
+
// ─── Réactivité ───────────────────────────────────────────────────────────────
|
|
352
|
+
|
|
353
|
+
// Style (et bascule clair/sombre : la variante `.DARK` est résolue par le moteur)
|
|
354
|
+
watch([styleForProvider, isDark], ([style, dark]) => {
|
|
355
|
+
if (!isReady || style === undefined) return
|
|
356
|
+
if (style === appliedStyle && dark === appliedDark) return
|
|
357
|
+
appliedStyle = style
|
|
358
|
+
appliedDark = dark
|
|
359
|
+
engine.value?.setStyle(style, dark)
|
|
360
|
+
})
|
|
361
|
+
|
|
362
|
+
// Tuiles raster (Leaflet) — `mapOptionsOf` gère déjà le cas d'`openstreetmap`.
|
|
363
|
+
watch([() => props.tiles, () => props.attribution], () => {
|
|
364
|
+
const target = engine.value
|
|
365
|
+
if (!isReady || provider.value !== "leaflet" || !target?.setRaster) return
|
|
366
|
+
const next = leafletOptionsOf({ tiles: props.tiles, attribution: props.attribution })
|
|
367
|
+
target.setRaster(next.tiles, next.attribution)
|
|
368
|
+
})
|
|
369
|
+
|
|
370
|
+
watch([center, zoom], ([nextCenter, nextZoom]) => {
|
|
371
|
+
if (isReady) engine.value?.setView(nextCenter, nextZoom)
|
|
372
|
+
})
|
|
373
|
+
|
|
374
|
+
watch(markSpecs, (specs) => engine.value?.setMarks(specs), { deep: true })
|
|
375
|
+
|
|
376
|
+
watch(
|
|
377
|
+
() => props.terrain,
|
|
378
|
+
(value) => {
|
|
379
|
+
const target = engine.value
|
|
380
|
+
if (!target || !isReady || typeof value !== "boolean") return
|
|
381
|
+
target.setTerrain(value)
|
|
382
|
+
},
|
|
383
|
+
)
|
|
384
|
+
|
|
385
|
+
watch(
|
|
386
|
+
() => props.projection,
|
|
387
|
+
(value) => {
|
|
388
|
+
const target = engine.value
|
|
389
|
+
if (!target || !isReady || !value) return
|
|
390
|
+
target.setProjection(value)
|
|
391
|
+
},
|
|
392
|
+
)
|
|
393
|
+
|
|
394
|
+
defineExpose({
|
|
395
|
+
/** Instance native (`Map` du SDK MapTiler, `L.Map` de Leaflet) — API complète du moteur */
|
|
396
|
+
map: computed(() => engine.value?.native ?? null),
|
|
397
|
+
/** Recalcule la taille du canvas (cadre qui vient d'apparaître) */
|
|
398
|
+
refresh: () => engine.value?.resize(),
|
|
399
|
+
})
|
|
400
|
+
</script>
|
|
401
|
+
|
|
402
|
+
<template>
|
|
403
|
+
<div class="q-map" :style="frameStyle" v-bind="$attrs">
|
|
404
|
+
<div ref="el" class="q-map__canvas" role="application" :aria-label="label" />
|
|
405
|
+
|
|
406
|
+
<div v-if="loading || failure" class="q-map__overlay">
|
|
407
|
+
<p class="q-map__note">{{ failure ?? "Chargement de la carte…" }}</p>
|
|
408
|
+
</div>
|
|
409
|
+
</div>
|
|
410
|
+
</template>
|