@dative-gpi/foundation-shared-components 0.0.206 → 0.0.208

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.
@@ -5,30 +5,36 @@
5
5
  >
6
6
  <FSCol
7
7
  v-if="L"
8
- class="fs-map"
9
8
  width="fill"
9
+ :class="['fs-map', { 'fs-map-fullscreen': fullScreen }]"
10
10
  >
11
- <FSRow
12
- v-if="selectableLayers.length > 1"
13
- class="fs-map-overlay-layer-choice"
11
+ <FSCol
12
+ v-if="$slots.leftoverlay"
13
+ class="fs-map-overlay-left"
14
+ width="hug"
14
15
  gap="2px"
15
16
  >
16
- <FSChip
17
- v-for="mapLayer in mapLayers.filter((layer) => selectableLayers.includes(layer.name))"
18
- variant="full"
19
- :color="innerSelectedLayer === mapLayer.name ? 'dark' : 'light'"
20
- :label="mapLayer.label"
21
- :key="mapLayer.name"
22
- :editable="true"
23
- @click="setMapBaseLayer(mapLayer.name as 'osm' | 'imagery')"
24
- />
25
- </FSRow>
17
+ <FSCard
18
+ padding="4px"
19
+ :elevation="true"
20
+ :border="false"
21
+ >
22
+ <FSFadeOut
23
+ maskHeight="0"
24
+ :height="`calc(${$props.height} - 40px)`"
25
+ >
26
+ <slot
27
+ name="leftoverlay"
28
+ />
29
+ </FSFadeOut>
30
+ </FSCard>
31
+ </FSCol>
26
32
  <FSRow
27
33
  v-if="$props.editable && !editingLocation && $props.selectedLocationId !== null"
28
34
  class="fs-map-overlay-edit-button"
29
35
  >
30
36
  <FSButton
31
- prepend-icon="mdi-pencil"
37
+ prependIcon="mdi-pencil"
32
38
  :label="$tr('ui.map.modify', 'Modify')"
33
39
  @click="editingLocation = true"
34
40
  />
@@ -41,45 +47,73 @@
41
47
  :id="mapId"
42
48
  />
43
49
  </FSCol>
44
-
45
50
  <FSCol
46
- class="fs-map-overlay-container"
51
+ class="fs-map-overlay-right-top"
47
52
  align="center-center"
48
53
  >
49
- <FSCol
50
- class="fs-map-zoom-overlay"
51
- align="bottom-center"
52
- width="hug"
54
+ <slot
55
+ name="toprightoverlay"
53
56
  >
54
- <FSButton
55
- v-if="$props.showMyLocation"
56
- prependIcon="mdi-crosshairs-gps"
57
- color="primary"
58
- variant="full"
59
- :elevation="true"
60
- :border="false"
61
- @click="locate"
62
- />
63
- <FSCol
64
- v-if="$props.showZoomButtons"
65
- gap="0"
57
+ <FSRow
58
+ gap="2px"
66
59
  >
60
+ <FSMapLayerButton
61
+ v-if="$props.selectableLayers?.length && $props.selectableLayers.length > 1"
62
+ :layers="mapLayers.filter((layer) => $props.selectableLayers?.includes(layer.name) ?? true)"
63
+ v-model="innerSelectedLayer"
64
+ />
67
65
  <FSButton
68
- class="fs-map-zoom-plus"
69
- prependIcon="mdi-plus"
66
+ v-if="$props.showFullScreen"
67
+ prependIcon="mdi-fullscreen"
70
68
  :elevation="true"
71
- :border="false"
72
- @click="zoomIn"
69
+ @click="fullScreen = !fullScreen"
73
70
  />
71
+ </FSRow>
72
+ </slot>
73
+ </FSCol>
74
+ <FSCol
75
+ class="fs-map-overlay-right-bottom"
76
+ align="center-center"
77
+ >
78
+ <slot
79
+ name="bottomrightoverlay"
80
+ >
81
+ <FSCol
82
+ class="fs-map-zoom-overlay"
83
+ align="bottom-center"
84
+ width="hug"
85
+ >
74
86
  <FSButton
75
- class="fs-map-zoom-minus"
76
- prependIcon="mdi-minus"
87
+ v-if="$props.showMyLocation"
88
+ prependIcon="mdi-crosshairs-gps"
89
+ color="primary"
90
+ variant="full"
77
91
  :elevation="true"
78
92
  :border="false"
79
- @click="zoomOut"
93
+ @click="locate"
80
94
  />
95
+ <FSCol
96
+ v-if="$props.showZoomButtons"
97
+ gap="0"
98
+ >
99
+
100
+ <FSButton
101
+ class="fs-map-zoom-plus"
102
+ prependIcon="mdi-plus"
103
+ :elevation="true"
104
+ :border="false"
105
+ @click="zoomIn"
106
+ />
107
+ <FSButton
108
+ class="fs-map-zoom-minus"
109
+ prependIcon="mdi-minus"
110
+ :elevation="true"
111
+ :border="false"
112
+ @click="zoomOut"
113
+ />
114
+ </FSCol>
81
115
  </FSCol>
82
- </FSCol>
116
+ </slot>
83
117
  <FSMapEditPointAddressOverlay
84
118
  v-if="editingLocation"
85
119
  :label="$tr('ui.map.address', 'Address')"
@@ -102,15 +136,16 @@ import "leaflet.markercluster";
102
136
 
103
137
  import { useTranslations as useTranslationsProvider } from "@dative-gpi/bones-ui/composables";
104
138
 
105
- import { type Address, type SiteInfos } from '@dative-gpi/foundation-shared-domain/models';
139
+ import { type Address, type FSArea } from '@dative-gpi/foundation-shared-domain/models';
106
140
 
107
141
  import { ColorEnum, type FSLocation, type MapLayer } from "../../models";
108
142
  import { useColors, useAddress } from "../../composables";
109
143
 
110
144
  import FSMapEditPointAddressOverlay from "./FSMapEditPointAddressOverlay.vue";
145
+ import FSMapLayerButton from "./FSMapLayerButton.vue";
146
+ import FSFadeOut from "../FSFadeOut.vue";
111
147
  import FSButton from "../FSButton.vue";
112
148
  import FSCard from "../FSCard.vue";
113
- import FSChip from "../FSChip.vue";
114
149
  import FSCol from "../FSCol.vue";
115
150
  import FSRow from "../FSRow.vue";
116
151
 
@@ -118,15 +153,16 @@ export default defineComponent({
118
153
  name: "FSMap",
119
154
  components: {
120
155
  FSMapEditPointAddressOverlay,
156
+ FSMapLayerButton,
157
+ FSFadeOut,
121
158
  FSButton,
122
159
  FSCard,
123
- FSChip,
124
160
  FSCol,
125
161
  FSRow
126
162
  },
127
163
  props: {
128
164
  height: {
129
- type: [Array, String, Number] as PropType<string[] | number[] | string | number | null>,
165
+ type: [String, Number] as PropType<string | number | null>,
130
166
  required: false,
131
167
  default: '400px'
132
168
  },
@@ -135,16 +171,46 @@ export default defineComponent({
135
171
  required: false,
136
172
  default: '100%'
137
173
  },
138
- sites: {
139
- type: Array as PropType<SiteInfos[]>,
174
+ grayscale: {
175
+ type: Boolean,
140
176
  required: false,
141
- default: () => [],
177
+ default: false
178
+ },
179
+ editable: {
180
+ type: Boolean,
181
+ required: false,
182
+ default: false
183
+ },
184
+ showMyLocation: {
185
+ type: Boolean,
186
+ required: false,
187
+ default: true
188
+ },
189
+ showZoomButtons: {
190
+ type: Boolean,
191
+ required: false,
192
+ default: true
193
+ },
194
+ showFullScreen: {
195
+ type: Boolean,
196
+ required: false,
197
+ default: false
142
198
  },
143
199
  center: {
144
200
  type: Array as PropType<number[]>,
145
201
  required: false,
146
202
  default: () => [45.71, 5.07]
147
203
  },
204
+ modelValue: {
205
+ type: Array as PropType<FSLocation[]>,
206
+ required: false,
207
+ default: () => [],
208
+ },
209
+ areas: {
210
+ type: Array as PropType<FSArea[]>,
211
+ required: false,
212
+ default: () => [],
213
+ },
148
214
  selectedLayer: {
149
215
  type: String as PropType<"osm" | "imagery">,
150
216
  required: false,
@@ -160,38 +226,13 @@ export default defineComponent({
160
226
  required: false,
161
227
  default: null
162
228
  },
163
- selectedSiteId: {
229
+ selectedAreaId: {
164
230
  type: String as PropType<string | null>,
165
231
  required: false,
166
232
  default: null
167
- },
168
- modelValue: {
169
- type: Array as PropType<FSLocation[]>,
170
- required: false,
171
- default: () => [],
172
- },
173
- editable: {
174
- type: Boolean,
175
- required: false,
176
- default: false
177
- },
178
- showMyLocation: {
179
- type: Boolean,
180
- required: false,
181
- default: true
182
- },
183
- showZoomButtons: {
184
- type: Boolean,
185
- required: false,
186
- default: true
187
- },
188
- grayscale: {
189
- type: Boolean,
190
- required: false,
191
- default: false
192
233
  }
193
234
  },
194
- emits: ["update:modelValue", "update:selectedLocationId", "update:selectedSiteId"],
235
+ emits: ["update:modelValue", "update:selectedLocationId", "update:selectedAreaId"],
195
236
  setup(props, { emit }) {
196
237
  const { reverseSearch } = useAddress();
197
238
  const { getColors } = useColors();
@@ -202,12 +243,13 @@ export default defineComponent({
202
243
  const innerSelectedLayer = ref(props.selectedLayer);
203
244
  const innerModelValue = ref(props.modelValue);
204
245
  const editingLocation = ref(false);
246
+ const fullScreen = ref(false);
205
247
 
206
248
  const mapId = `map-${Math.random().toString(36).substring(7)}`;
207
249
  const defaultZoom = 15;
208
250
  const markers: { [key: string]: L.Marker } = {};
209
- const sites: { [key: string]: L.Polygon } = {};
210
- const siteLayerGroup = new LL.FeatureGroup();
251
+ const areas: { [key: string]: L.Polygon } = {};
252
+ const areaLayerGroup = new LL.FeatureGroup();
211
253
  const baseLayerGroup = new LL.LayerGroup();
212
254
  const myLocationLayerGroup = new LL.LayerGroup();
213
255
 
@@ -238,6 +280,7 @@ export default defineComponent({
238
280
  {
239
281
  name: "osm",
240
282
  label: $tr("ui.map.layer.osm", "Map"),
283
+ image: new URL("../../assets/images/map/osm.png", import.meta.url).href,
241
284
  layer: LL.tileLayer('https://tile.openstreetmap.org/{z}/{x}/{y}.png', {
242
285
  maxZoom: 20,
243
286
  attribution: '© OpenStreetMap'
@@ -246,10 +289,11 @@ export default defineComponent({
246
289
  {
247
290
  name: "imagery",
248
291
  label: $tr("ui.map.layer.imagery", "Imagery"),
292
+ image: new URL("../../assets/images/map/imagery.png", import.meta.url).href,
249
293
  layer: LL.tileLayer('https://server.arcgisonline.com/ArcGIS/rest/services/World_Imagery/MapServer/tile/{z}/{y}/{x}', {
250
294
  attribution: 'Tiles &copy; Esri &mdash; Source: Esri, i-cubed, USDA, USGS, AEX, GeoEye, Getmapping, Aerogrid, IGN, IGP, UPR-EGP, and the GIS User Community',
251
295
  maxZoom: 19
252
- }),
296
+ })
253
297
  }
254
298
  ];
255
299
 
@@ -265,34 +309,38 @@ export default defineComponent({
265
309
  const displayLocations = () => {
266
310
  markerLayerGroup.clearLayers();
267
311
  innerModelValue.value.forEach((location) => {
268
- const iconHtml = `<div class="fs-map-location-pin"><i class="${location.icon} mdi v-icon notranslate v-theme--DefaultTheme fs-icon" aria-hidden="true" style="--fs-icon-font-size: 22px;" ></i></div>`;
312
+ const iconHtml = `
313
+ <div style="--fs-map-mylocation-pin-color-alpha:${getColors(location.color).base}50;--fs-map-location-pin-color: ${getColors(location.color).base}">
314
+ <div class="fs-map-location-pin">
315
+ <i class="${location.icon} mdi v-icon notranslate v-theme--DefaultTheme fs-icon" aria-hidden="true" style="--fs-icon-font-size: 22px;" ></i>
316
+ </div>
317
+ </div>`;
269
318
  const icon = LL.divIcon({
270
319
  html: iconHtml,
271
- className: 'fs-map-location',
272
320
  iconSize: [36, 36],
321
+ className: 'fs-map-location',
273
322
  iconAnchor: [18, 18],
274
323
  });
275
324
  const marker = LL.marker([location.address.latitude, location.address.longitude], { icon }).addTo(markerLayerGroup);
276
325
  markers[location.id] = marker;
277
326
  marker.on('click', () => emit('update:selectedLocationId', location.id));
278
-
279
327
  });
280
328
  };
281
329
 
282
- const displaySites = () => {
283
- siteLayerGroup.clearLayers();
284
- props.sites.forEach((site) => {
285
- const sitePolygon = LL.polygon(site.coordinates.map((coord) => [coord.latitude, coord.longitude]), {
286
- color: site.color,
287
- fillColor: site.color + "50",
330
+ const displayAreas = () => {
331
+ areaLayerGroup.clearLayers();
332
+ props.areas.forEach((area) => {
333
+ const areaPolygon = LL.polygon(area.coordinates.map((coord) => [coord.latitude, coord.longitude]), {
334
+ color: area.color,
335
+ fillColor: area.color + "50",
288
336
  fillOpacity: 0.5,
289
- className: 'fs-map-site',
290
- }).addTo(siteLayerGroup);
337
+ className: 'fs-map-area',
338
+ }).addTo(areaLayerGroup);
291
339
 
292
- sites[site.id] = sitePolygon;
293
- sitePolygon.on('click', () => emit('update:selectedSiteId', site.id));
340
+ areas[area.id] = areaPolygon;
341
+ areaPolygon.on('click', () => emit('update:selectedAreaId', area.id));
294
342
  });
295
- }
343
+ };
296
344
 
297
345
  const modifyLocationAddress = (locationId: string, newAddress: Address) => {
298
346
  const location = innerModelValue.value.find((loc) => loc.id === locationId);
@@ -321,10 +369,10 @@ export default defineComponent({
321
369
  LL.control.attribution({ position: 'bottomleft' }).addTo(map);
322
370
 
323
371
  baseLayerGroup.addTo(map);
324
- siteLayerGroup.addTo(map);
372
+ areaLayerGroup.addTo(map);
325
373
  myLocationLayerGroup.addTo(map);
326
- setMapBaseLayer(props.selectedLayer);
327
- displaySites();
374
+ setMapBaseLayer(innerSelectedLayer.value);
375
+ displayAreas();
328
376
  displayLocations();
329
377
  markerLayerGroup.addTo(map);
330
378
 
@@ -340,18 +388,17 @@ export default defineComponent({
340
388
  };
341
389
 
342
390
  const setMapBaseLayer = (layerName: 'osm' | 'imagery') => {
343
- innerSelectedLayer.value = layerName;
344
391
  const layer = mapLayers.find((mapLayer) => mapLayer.name === layerName) ?? mapLayers[0];
345
392
  baseLayerGroup.clearLayers();
346
393
  layer.layer.addTo(baseLayerGroup);
347
394
  };
348
395
 
349
396
  const onNewAddressEntered = (address: Address) => {
350
- if (!props.selectedLocationId) {
397
+ if (!props.selectedLocationId || !map) {
351
398
  return;
352
399
  }
353
400
  modifyLocationAddress(props.selectedLocationId, address);
354
- map?.panTo([address.latitude, address.longitude]);
401
+ map.panTo([address.latitude, address.longitude]);
355
402
  };
356
403
 
357
404
  const onNewCoordEntered = async (lat: number, lng: number) => {
@@ -365,17 +412,26 @@ export default defineComponent({
365
412
  };
366
413
 
367
414
  const zoomIn = () => {
368
- map?.zoomIn();
415
+ if (!map) {
416
+ return;
417
+ }
418
+ map.zoomIn();
369
419
  };
370
420
 
371
421
  const zoomOut = () => {
372
- map?.zoomOut();
422
+ if (!map) {
423
+ return;
424
+ }
425
+ map.zoomOut();
373
426
  };
374
427
 
375
428
  const locate = () => {
376
- map?.locate();
377
- map?.on('locationfound', (e: L.LocationEvent) => {
378
- map?.panTo(e.latlng);
429
+ if (!map) {
430
+ return;
431
+ }
432
+ map.locate();
433
+ map.on('locationfound', (e: L.LocationEvent) => {
434
+ map.panTo(e.latlng);
379
435
  const iconHtml = `<div class="fs-map-mylocation-pin"></div>`;
380
436
  const icon = LL.divIcon({
381
437
  html: iconHtml,
@@ -391,12 +447,15 @@ export default defineComponent({
391
447
  const onCancel = () => {
392
448
  editingLocation.value = false;
393
449
  innerModelValue.value = props.modelValue;
450
+ if (!map) {
451
+ return;
452
+ }
394
453
  displayLocations();
395
454
  if (innerModelValue.value.length > 0) {
396
- map?.fitBounds(markerLayerGroup.getBounds(), { maxZoom: defaultZoom });
455
+ map.fitBounds(markerLayerGroup.getBounds(), { maxZoom: defaultZoom });
397
456
  }
398
457
  else {
399
- map?.panTo([props.center[0], props.center[1]]);
458
+ map.panTo([props.center[0], props.center[1]]);
400
459
  }
401
460
  if (props.modelValue.length > 1) {
402
461
  emit('update:selectedLocationId', null);
@@ -405,12 +464,15 @@ export default defineComponent({
405
464
 
406
465
  const onSubmit = () => {
407
466
  emit('update:modelValue', innerModelValue.value);
467
+ if (!map) {
468
+ return;
469
+ }
408
470
  editingLocation.value = false;
409
471
  if (innerModelValue.value.length > 0) {
410
- map?.fitBounds(markerLayerGroup.getBounds(), { maxZoom: defaultZoom });
472
+ map.fitBounds(markerLayerGroup.getBounds(), { maxZoom: defaultZoom });
411
473
  }
412
474
  else {
413
- map?.flyTo([props.center[0], props.center[1]], map?.getZoom() ?? defaultZoom, { animate: false });
475
+ map.flyTo([props.center[0], props.center[1]], map.getZoom() ?? defaultZoom, { animate: false });
414
476
  }
415
477
  if (props.modelValue.length > 1) {
416
478
  emit('update:selectedLocationId', null);
@@ -419,6 +481,9 @@ export default defineComponent({
419
481
 
420
482
  onMounted(() => {
421
483
  initMap();
484
+ if (props.selectedLocationId && props.modelValue.length === 1) {
485
+ editingLocation.value = true;
486
+ }
422
487
  });
423
488
 
424
489
  watch(() => innerModelValue.value, () => {
@@ -426,36 +491,42 @@ export default defineComponent({
426
491
  });
427
492
 
428
493
  watch(() => props.selectedLocationId, () => {
494
+ if (!props.selectedLocationId || !map) {
495
+ return;
496
+ }
497
+
429
498
  Object.values(markers).forEach((marker) => {
430
499
  marker.getElement()?.classList.remove('fs-map-location-selected');
431
500
  });
432
501
 
433
- if (!props.selectedLocationId) {
434
- return;
435
- }
436
502
  const marker = markers[props.selectedLocationId];
503
+ map.flyTo(marker.getLatLng(), 17, { animate: false });
437
504
  marker.getElement()?.classList.add('fs-map-location-selected');
438
- map?.flyTo(marker.getLatLng(), 17, { animate: false });
439
505
  })
440
506
 
441
- watch(() => props.selectedSiteId, () => {
442
- if (!props.selectedSiteId) {
507
+ watch(() => props.selectedAreaId, () => {
508
+ if (!props.selectedAreaId || !map) {
443
509
  return;
444
510
  }
445
- const site = sites[props.selectedSiteId];
446
- if (site) {
447
- map?.fitBounds(site.getBounds(), { maxZoom: 17 });
511
+ const area = areas[props.selectedAreaId];
512
+ if (area) {
513
+ map.fitBounds(area.getBounds(), { maxZoom: 17 });
448
514
  }
449
515
  });
450
516
 
517
+ watch(innerSelectedLayer, () => {
518
+ setMapBaseLayer(innerSelectedLayer.value);
519
+ });
520
+
451
521
  return {
452
- L,
453
522
  innerSelectedLayer,
454
523
  editingLocation,
455
524
  innerModelValue,
525
+ fullScreen,
456
526
  mapLayers,
457
527
  mapId,
458
528
  style,
529
+ L,
459
530
  onNewAddressEntered,
460
531
  onNewCoordEntered,
461
532
  setMapBaseLayer,
@@ -1,8 +1,8 @@
1
1
  <template>
2
2
  <FSCard
3
3
  padding="16px"
4
- width="100%"
5
4
  height="100%"
5
+ width="100%"
6
6
  :elevation="true"
7
7
  >
8
8
  <FSCol
@@ -50,11 +50,11 @@
50
50
  />
51
51
  </FSRow>
52
52
  <FSButton
53
- :label="$tr('ui.map.save', 'Save')"
53
+ prependIcon="mdi-content-save"
54
+ style="display: none;"
54
55
  color="primary"
55
- prepend-icon="mdi-content-save"
56
56
  type="submit"
57
- style="display: none;"
57
+ :label="$tr('ui.map.save', 'Save')"
58
58
  />
59
59
  </FSForm>
60
60
  </FSCol>
@@ -66,9 +66,9 @@
66
66
  @click="onCancel"
67
67
  />
68
68
  <FSButton
69
- :label="$tr('ui.map.save', 'Save')"
69
+ prependIcon="mdi-content-save"
70
70
  color="primary"
71
- prepend-icon="mdi-content-save"
71
+ :label="$tr('ui.map.save', 'Save')"
72
72
  @click="onSubmit"
73
73
  />
74
74
  </FSRow>
@@ -104,17 +104,16 @@ export default defineComponent({
104
104
  },
105
105
  props: {
106
106
  modelValue: {
107
- type: Object as PropType<Address>,
108
- default: null,
109
- required: false,
107
+ type: Object as PropType<Address | null>,
108
+ default: null
110
109
  }
111
110
  },
112
111
  emits: ["update:modelValue", "update:locationCoordinates", "submit", "cancel"],
113
112
  setup(props, { emit }) {
114
113
  const menuLocationCoordinates = ref(false);
115
114
 
116
- const latitude = ref(props.modelValue.latitude);
117
- const longitude = ref(props.modelValue.longitude);
115
+ const latitude = ref(0);
116
+ const longitude = ref(0);
118
117
 
119
118
  const onCoordinatesChange = () => {
120
119
  const newModelValue = new Address({
@@ -130,7 +129,7 @@ export default defineComponent({
130
129
  };
131
130
 
132
131
  const onAddressFieldSubmit = (address: Address|null) => {
133
- if(address === null) {
132
+ if(!address) {
134
133
  return;
135
134
  }
136
135
  emit('update:modelValue', address);
@@ -144,10 +143,12 @@ export default defineComponent({
144
143
  emit('cancel');
145
144
  };
146
145
 
147
- watch(() => props.modelValue, (value) => {
148
- latitude.value = value.latitude;
149
- longitude.value = value.longitude;
150
- });
146
+ watch(() => props.modelValue, () => {
147
+ if (props.modelValue) {
148
+ latitude.value = props.modelValue.latitude;
149
+ longitude.value = props.modelValue.longitude;
150
+ }
151
+ }, { immediate: true });
151
152
 
152
153
  return {
153
154
  menuLocationCoordinates,
@@ -155,8 +156,8 @@ export default defineComponent({
155
156
  latitude,
156
157
  onAddressFieldSubmit,
157
158
  onCoordinatesChange,
158
- onSubmit,
159
- onCancel
159
+ onCancel,
160
+ onSubmit
160
161
  };
161
162
  }
162
163
  });
@@ -0,0 +1,71 @@
1
+ <template>
2
+ <FSButton
3
+ prependIcon="mdi-layers-outline"
4
+ :elevation="true"
5
+ @click="dialog = true"
6
+ />
7
+ <FSDialog
8
+ v-model="dialog"
9
+ title="Select Layers"
10
+ width="460px"
11
+ >
12
+ <template
13
+ v-slot:body
14
+ >
15
+ <FSImageCard
16
+ v-for="layer in layers"
17
+ :variant="modelValue === layer.name ? 'full' : 'background'"
18
+ :color="modelValue === layer.name ? 'primary' : 'light'"
19
+ :label="layer.label"
20
+ :src="layer.image"
21
+ :key="layer.name"
22
+ @click="onLayerClick(layer.name)"
23
+ />
24
+ </template>
25
+ </FSDialog>
26
+ </template>
27
+
28
+ <script lang="ts">
29
+ import { defineComponent, type PropType, ref } from "vue";
30
+
31
+ import { type MapLayer } from "../../models";
32
+
33
+ import FSImageCard from "../FSImageCard.vue";
34
+ import FSButton from "../FSButton.vue";
35
+ import FSDialog from "../FSDialog.vue";
36
+
37
+ export default defineComponent({
38
+ name: "FSMapLayerButton",
39
+ components: {
40
+ FSImageCard,
41
+ FSButton,
42
+ FSDialog
43
+ },
44
+ props: {
45
+ layers: {
46
+ type: Array as PropType<MapLayer[]>,
47
+ required: false,
48
+ default: () => []
49
+ },
50
+ modelValue: {
51
+ type: String,
52
+ required: false,
53
+ default: ""
54
+ }
55
+ },
56
+ emits: ["update:modelValue"],
57
+ setup(_, { emit }) {
58
+ const dialog = ref(false);
59
+
60
+ const onLayerClick = (layer: string) => {
61
+ emit("update:modelValue", layer);
62
+ dialog.value = false;
63
+ };
64
+
65
+ return {
66
+ dialog,
67
+ onLayerClick
68
+ };
69
+ }
70
+ });
71
+ </script>