@dative-gpi/foundation-shared-components 0.1.120 → 1.0.0

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.
Files changed (196) hide show
  1. package/assets/images/map/imagery.png +0 -0
  2. package/assets/images/map/map.png +0 -0
  3. package/components/FSAccordion.vue +2 -1
  4. package/components/FSAccordionPanel.vue +20 -1
  5. package/components/FSBadge.vue +7 -3
  6. package/components/FSBreadcrumbs.vue +4 -2
  7. package/components/FSButton.vue +15 -8
  8. package/components/FSCalendar.vue +5 -2
  9. package/components/FSCalendarTwin.vue +6 -3
  10. package/components/FSCard.vue +4 -2
  11. package/components/FSCardPlaceholder.vue +80 -0
  12. package/components/FSCheckbox.vue +10 -7
  13. package/components/FSChip.vue +4 -2
  14. package/components/FSClickable.vue +5 -3
  15. package/components/FSClock.vue +18 -4
  16. package/components/FSCol.vue +12 -5
  17. package/components/FSColor.vue +4 -2
  18. package/components/FSColorIcon.vue +5 -3
  19. package/components/FSDialog.vue +28 -87
  20. package/components/FSDialogContent.vue +133 -0
  21. package/components/FSDialogForm.vue +25 -236
  22. package/components/FSDialogFormBody.vue +273 -0
  23. package/components/FSDialogMenu.vue +5 -2
  24. package/components/FSDialogMultiForm.vue +21 -197
  25. package/components/FSDialogMultiFormBody.vue +231 -0
  26. package/components/FSDialogSubmit.vue +4 -2
  27. package/components/FSDivider.vue +6 -4
  28. package/components/FSEditImage.vue +16 -9
  29. package/components/FSErrorToast.vue +2 -1
  30. package/components/FSFadeOut.vue +1 -1
  31. package/components/FSForm.vue +7 -7
  32. package/components/FSGrid.vue +4 -2
  33. package/components/FSGridMosaic.vue +3 -2
  34. package/components/FSIcon.vue +3 -2
  35. package/components/FSIconCard.vue +10 -3
  36. package/components/FSIconCheck.vue +2 -1
  37. package/components/FSIconFlag.vue +2 -1
  38. package/components/FSImage.vue +2 -1
  39. package/components/FSImageCard.vue +72 -0
  40. package/components/FSLabel.vue +4 -2
  41. package/components/FSLink.vue +5 -3
  42. package/components/FSLoader.vue +2 -1
  43. package/components/FSOptionGroup.vue +28 -20
  44. package/components/FSOptionItem.vue +8 -18
  45. package/components/FSPagination.vue +4 -2
  46. package/components/FSRadio.vue +23 -11
  47. package/components/FSRadioGroup.vue +23 -10
  48. package/components/FSRow.vue +8 -1
  49. package/components/FSSlideGroup.vue +27 -6
  50. package/components/FSSlider.vue +4 -2
  51. package/components/FSSpan.vue +2 -1
  52. package/components/FSSwitch.vue +13 -10
  53. package/components/FSTab.vue +4 -2
  54. package/components/FSTabs.vue +5 -14
  55. package/components/FSTag.vue +11 -4
  56. package/components/FSTagGroup.vue +4 -2
  57. package/components/FSText.vue +4 -2
  58. package/components/FSToggleSet.vue +30 -17
  59. package/components/FSTooltip.vue +15 -4
  60. package/components/FSWindow.vue +2 -2
  61. package/components/FSWrapGroup.vue +2 -1
  62. package/components/autocompletes/FSAutoCompleteAddress.vue +104 -0
  63. package/components/autocompletes/FSAutocompleteLanguage.vue +18 -26
  64. package/components/autocompletes/FSAutocompleteTag.vue +138 -0
  65. package/components/autocompletes/FSAutocompleteTimeZone.vue +19 -30
  66. package/components/buttons/FSButtonAdd.vue +28 -0
  67. package/components/buttons/FSButtonAddIcon.vue +28 -0
  68. package/components/buttons/FSButtonAddLabel.vue +27 -0
  69. package/components/buttons/FSButtonAddMini.vue +27 -0
  70. package/components/buttons/FSButtonFile.vue +4 -4
  71. package/components/buttons/FSButtonFileIcon.vue +4 -4
  72. package/components/buttons/FSButtonFileLabel.vue +4 -4
  73. package/components/buttons/FSButtonFileMini.vue +4 -4
  74. package/components/deviceOrganisations/FSConnectivity.vue +3 -2
  75. package/components/deviceOrganisations/FSConnectivityCard.vue +3 -2
  76. package/components/deviceOrganisations/FSStatus.vue +3 -2
  77. package/components/deviceOrganisations/FSStatusCard.vue +3 -2
  78. package/components/deviceOrganisations/FSStatusesCarousel.vue +3 -2
  79. package/components/deviceOrganisations/FSStatusesRow.vue +3 -2
  80. package/components/deviceOrganisations/FSWorstAlert.vue +5 -4
  81. package/components/deviceOrganisations/FSWorstAlertCard.vue +4 -2
  82. package/components/fields/FSAutocompleteField.vue +210 -97
  83. package/components/fields/FSBaseField.vue +2 -1
  84. package/components/fields/FSColorField.vue +65 -94
  85. package/components/fields/FSDateField.vue +12 -25
  86. package/components/fields/FSDateRangeField.vue +15 -27
  87. package/components/fields/FSDateTimeField.vue +22 -32
  88. package/components/fields/FSDateTimeRangeField.vue +43 -51
  89. package/components/fields/FSGradientField.vue +143 -0
  90. package/components/fields/FSIconField.vue +9 -6
  91. package/components/fields/FSMagicConfigField.vue +154 -0
  92. package/components/fields/FSMagicField.vue +185 -0
  93. package/components/fields/FSNumberField.vue +3 -1
  94. package/components/fields/FSPasswordField.vue +10 -10
  95. package/components/fields/FSRichTextField.vue +136 -50
  96. package/components/fields/FSSearchField.vue +41 -62
  97. package/components/fields/FSSelectField.vue +148 -53
  98. package/components/fields/FSTagField.vue +19 -16
  99. package/components/fields/FSTermField.vue +192 -186
  100. package/components/fields/FSTextArea.vue +4 -4
  101. package/components/fields/FSTextField.vue +29 -6
  102. package/components/fields/FSTimeField.vue +55 -20
  103. package/components/fields/FSTimeSlotField.vue +6 -5
  104. package/components/fields/FSTranslateField.vue +234 -0
  105. package/components/fields/FSTranslateRichTextField.vue +185 -0
  106. package/components/fields/FSTreeViewField.vue +520 -0
  107. package/components/lists/FSDataIteratorItem.vue +18 -3
  108. package/components/lists/FSDataTableUI.vue +138 -51
  109. package/components/lists/FSFilterButton.vue +4 -2
  110. package/components/lists/FSHiddenButton.vue +4 -2
  111. package/components/map/FSMap.vue +598 -0
  112. package/components/map/FSMapEditPointAddressOverlay.vue +164 -0
  113. package/components/map/FSMapLayerButton.vue +77 -0
  114. package/components/map/FSMapOverlay.vue +150 -0
  115. package/components/selects/FSSelectAutoRefresh.vue +62 -0
  116. package/components/selects/FSSelectDashboardVariableType.vue +47 -0
  117. package/components/selects/FSSelectDateSetting.vue +39 -37
  118. package/components/selects/FSSelectDays.vue +62 -0
  119. package/components/tiles/FSDashboardOrganisationTileUI.vue +7 -5
  120. package/components/tiles/FSDashboardOrganisationTypeTileUI.vue +7 -5
  121. package/components/tiles/FSDashboardShallowTileUI.vue +7 -5
  122. package/components/tiles/FSDeviceOrganisationTileUI.vue +11 -12
  123. package/components/tiles/FSFolderTileUI.vue +8 -6
  124. package/components/tiles/FSGroupTileUI.vue +13 -15
  125. package/components/tiles/{FSSimpleIconTileUI.vue → FSSimpleTileUI.vue} +29 -15
  126. package/components/tiles/FSTile.vue +5 -11
  127. package/components/tiles/FSUserOrganisationTileUI.vue +2 -1
  128. package/components/toggleSets/FSToggleSetPosition.vue +61 -0
  129. package/composables/index.ts +5 -1
  130. package/composables/useAddress.ts +158 -0
  131. package/composables/useAutocomplete.ts +4 -3
  132. package/composables/useColors.ts +8 -25
  133. package/composables/useDebounce.ts +2 -1
  134. package/composables/useMagicFieldProvider.ts +22 -0
  135. package/composables/useRules.ts +4 -12
  136. package/composables/useSlots.ts +46 -26
  137. package/composables/useTables.ts +29 -0
  138. package/composables/useTreeView.ts +48 -0
  139. package/elements/FSFormElement.ts +2 -1
  140. package/icons/flags/France.vue +21 -5
  141. package/icons/flags/Germany.vue +16 -4
  142. package/icons/flags/GreatBritain.vue +25 -6
  143. package/icons/flags/Italy.vue +21 -5
  144. package/icons/flags/Portugal.vue +225 -51
  145. package/icons/flags/Spain.vue +2781 -543
  146. package/icons/flags/UnitedStates.vue +31 -7
  147. package/icons/widgetTemplates/DevicesWidget.vue +189 -189
  148. package/icons/widgetTemplates/ProfileWidget.vue +9 -9
  149. package/icons/widgetTemplates/TextWidget.vue +6 -6
  150. package/models/breadcrumbs.ts +1 -1
  151. package/models/deviceAlerts.ts +1 -1
  152. package/models/deviceConnectivities.ts +1 -1
  153. package/models/index.ts +2 -0
  154. package/models/magicFields.ts +9 -0
  155. package/models/map.ts +18 -0
  156. package/models/richTextVariable.ts +5 -0
  157. package/models/rules.ts +11 -2
  158. package/models/tables.ts +30 -21
  159. package/models/variableNode.ts +104 -0
  160. package/package.json +21 -18
  161. package/plugins/colorPlugin.ts +2 -2
  162. package/plugins/index.ts +2 -1
  163. package/plugins/mapsPlugin.ts +37 -0
  164. package/shims-plugin.d.ts +2 -2
  165. package/shims-window.d.ts +3 -0
  166. package/styles/components/fs_button.scss +5 -0
  167. package/styles/components/fs_card.scss +0 -1
  168. package/styles/components/fs_col.scss +1 -0
  169. package/styles/components/fs_color_field.scss +12 -2
  170. package/styles/components/fs_data_iterator_item.scss +19 -6
  171. package/styles/components/fs_dialog.scss +12 -22
  172. package/styles/components/fs_gradient_field.scss +16 -0
  173. package/styles/components/fs_image_card.scss +18 -0
  174. package/styles/components/fs_magic_config_field.scss +13 -0
  175. package/styles/components/fs_map.scss +129 -0
  176. package/styles/components/fs_map_overlay.scss +38 -0
  177. package/styles/components/fs_meta_field.scss +6 -0
  178. package/styles/components/fs_option_group.scss +1 -0
  179. package/styles/components/fs_radio.scss +1 -1
  180. package/styles/components/fs_rich_text_field.scss +17 -5
  181. package/styles/components/fs_row.scss +1 -1
  182. package/styles/components/fs_select_field.scss +9 -14
  183. package/styles/components/fs_text.scss +1 -1
  184. package/styles/components/fs_time_field.scss +0 -4
  185. package/styles/components/fs_translate_field.scss +3 -0
  186. package/styles/components/fs_tree_view_field.scss +53 -0
  187. package/styles/components/index.scss +8 -1
  188. package/styles/globals/overrides.scss +54 -8
  189. package/styles/globals/scrollbars.scss +2 -2
  190. package/themes/default.ts +1 -1
  191. package/utils/gradient.ts +1601 -0
  192. package/utils/index.ts +3 -1
  193. package/utils/leafletMarkers.ts +23 -0
  194. package/utils/lexical.ts +3 -1
  195. package/components/selects/FSSelectTimeZone.vue +0 -67
  196. package/styles/components/fs_date_field.scss +0 -8
@@ -0,0 +1,598 @@
1
+ <template>
2
+ <FSCard
3
+ :width="$props.width"
4
+ :style="style"
5
+ v-bind="$attrs"
6
+ >
7
+ <FSCol
8
+ v-if="L"
9
+ width="fill"
10
+ :class="['fs-map', { 'fs-map-fullscreen': fullScreen }]"
11
+ >
12
+ <FSMapOverlay
13
+ v-if="$slots['leftoverlay-header'] || $slots['leftoverlay-body']"
14
+ :mode="$props.overlayMode"
15
+ :height="$props.height"
16
+ :mapId="mapId"
17
+ @update:mode="$emit('update:overlayMode', $event)"
18
+ >
19
+ <template
20
+ v-slot:leftoverlay-header
21
+ >
22
+ <slot
23
+ name="leftoverlay-header"
24
+ />
25
+ </template>
26
+ <template
27
+ v-slot:leftoverlay-body
28
+ >
29
+ <slot
30
+ name="leftoverlay-body"
31
+ />
32
+ </template>
33
+ </FSMapOverlay>
34
+ <FSRow
35
+ v-if="$props.editable && !editingLocation && $props.selectedLocationId !== null"
36
+ class="fs-map-overlay-edit-button"
37
+ >
38
+ <FSButton
39
+ prependIcon="mdi-pencil-outline"
40
+ :label="$tr('ui.map.modify', 'Modify')"
41
+ @click="editingLocation = true"
42
+ />
43
+ </FSRow>
44
+ <FSCol>
45
+ <div
46
+ class="fs-leaflet-container"
47
+ :id="mapId"
48
+ />
49
+ </FSCol>
50
+ <FSCol
51
+ class="fs-map-overlay-right-top"
52
+ align="center-center"
53
+ >
54
+ <slot
55
+ name="toprightoverlay"
56
+ >
57
+ <FSRow
58
+ gap="2px"
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
+ />
65
+ <FSButton
66
+ v-if="$props.showFullScreen"
67
+ prependIcon="mdi-fullscreen"
68
+ :elevation="true"
69
+ @click="fullScreen = !fullScreen"
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
+ >
86
+ <FSButton
87
+ v-if="$props.showMyLocation"
88
+ prependIcon="mdi-crosshairs-gps"
89
+ color="primary"
90
+ variant="full"
91
+ :elevation="true"
92
+ :border="false"
93
+ @click="locate"
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>
115
+ </FSCol>
116
+ </slot>
117
+ <FSMapEditPointAddressOverlay
118
+ v-if="editingLocation"
119
+ :label="$tr('ui.map.address', 'Address')"
120
+ :modelValue="(innerModelValue.find((loc) => loc.id === $props.selectedLocationId))?.address"
121
+ @update:locationCoordinates="($event: Address) => onNewCoordEntered($event.latitude, $event.longitude)"
122
+ @update:modelValue="($event: Address) => onNewAddressEntered($event)"
123
+ @cancel="onCancel"
124
+ @submit="onSubmit"
125
+ />
126
+ </FSCol>
127
+ </FSCol>
128
+ </FSCard>
129
+ </template>
130
+
131
+ <script lang="ts">
132
+ import { computed, defineComponent, onMounted, onUnmounted, type PropType, ref, watch } from "vue";
133
+
134
+ import * as L from "leaflet";
135
+ import "leaflet.markercluster";
136
+
137
+ import { useTranslations as useTranslationsProvider } from "@dative-gpi/bones-ui/composables";
138
+ import { type Address, type FSArea } from '@dative-gpi/foundation-shared-domain/models';
139
+
140
+ import { clusterMarkerHtml, locationMarkerHtml, myLocationMarkerHtml } from "../../utils";
141
+ import { ColorEnum, type FSLocation, type MapLayer } from "../../models";
142
+ import { useColors, useAddress, useBreakpoints } from "../../composables";
143
+
144
+ import FSMapEditPointAddressOverlay from "./FSMapEditPointAddressOverlay.vue";
145
+ import FSMapLayerButton from "./FSMapLayerButton.vue";
146
+ import FSMapOverlay from "./FSMapOverlay.vue";
147
+ import FSButton from "../FSButton.vue";
148
+ import FSCard from "../FSCard.vue";
149
+ import FSCol from "../FSCol.vue";
150
+ import FSRow from "../FSRow.vue";
151
+
152
+ export default defineComponent({
153
+ name: "FSMap",
154
+ components: {
155
+ FSMapEditPointAddressOverlay,
156
+ FSMapLayerButton,
157
+ FSMapOverlay,
158
+ FSButton,
159
+ FSCard,
160
+ FSCol,
161
+ FSRow
162
+ },
163
+ props: {
164
+ height: {
165
+ type: [String, Number] as PropType<string | number | null>,
166
+ required: false,
167
+ default: '400px'
168
+ },
169
+ width: {
170
+ type: [Array, String, Number] as PropType<string[] | number[] | string | number | null>,
171
+ required: false,
172
+ default: '100%'
173
+ },
174
+ grayscale: {
175
+ type: Boolean,
176
+ required: false,
177
+ default: false
178
+ },
179
+ editable: {
180
+ type: Boolean,
181
+ required: false,
182
+ default: false
183
+ },
184
+ overlayMode: {
185
+ type: String as PropType<'collapse' | 'half' | 'expand'>,
186
+ required: false,
187
+ default: 'collapse'
188
+ },
189
+ showMyLocation: {
190
+ type: Boolean,
191
+ required: false,
192
+ default: true
193
+ },
194
+ showZoomButtons: {
195
+ type: Boolean,
196
+ required: false,
197
+ default: true
198
+ },
199
+ showFullScreen: {
200
+ type: Boolean,
201
+ required: false,
202
+ default: false
203
+ },
204
+ enableScrollWheelZoom: {
205
+ type: Boolean,
206
+ required: false,
207
+ default: false
208
+ },
209
+ center: {
210
+ type: Array as PropType<number[]>,
211
+ required: false,
212
+ default: () => [45.71, 5.07]
213
+ },
214
+ modelValue: {
215
+ type: Array as PropType<FSLocation[]>,
216
+ required: false,
217
+ default: () => [],
218
+ },
219
+ areas: {
220
+ type: Array as PropType<FSArea[]>,
221
+ required: false,
222
+ default: () => [],
223
+ },
224
+ selectedLayer: {
225
+ type: String as PropType<"map" | "imagery">,
226
+ required: false,
227
+ default: "map"
228
+ },
229
+ selectableLayers: {
230
+ type: Array as PropType<string[]>,
231
+ required: false,
232
+ default: () => ["map", "imagery"]
233
+ },
234
+ selectedLocationId: {
235
+ type: String as PropType<string | null>,
236
+ required: false,
237
+ default: null
238
+ },
239
+ selectedAreaId: {
240
+ type: String as PropType<string | null>,
241
+ required: false,
242
+ default: null
243
+ }
244
+ },
245
+ emits: ["update:modelValue", "update:selectedLocationId", "update:selectedAreaId", 'update:overlayMode'],
246
+ setup(props, { emit }) {
247
+ const { $tr } = useTranslationsProvider();
248
+ const { reverseSearch } = useAddress();
249
+ const { getColors } = useColors();
250
+ const { isExtraSmall } = useBreakpoints();
251
+
252
+ const LL = window.L;
253
+
254
+ const innerSelectedLayer = ref(props.selectedLayer);
255
+ const innerModelValue = ref(props.modelValue);
256
+ const editingLocation = ref(false);
257
+ const fullScreen = ref(false);
258
+ const leftOverlayHeight = ref<number>();
259
+ const leftOverlayWidth = ref<number>();
260
+ const resizeObserver = ref<ResizeObserver | null>(null);
261
+
262
+ const mapId = `map-${Math.random().toString(36).substring(7)}`;
263
+ const defaultZoom = 15;
264
+ const markers: { [key: string]: L.Marker } = {};
265
+ const areas: { [key: string]: L.Polygon } = {};
266
+ const areaLayerGroup = new LL.FeatureGroup();
267
+ const baseLayerGroup = new LL.LayerGroup();
268
+ const myLocationLayerGroup = new LL.LayerGroup();
269
+
270
+ let map: L.Map;
271
+ let markerLayerGroup: L.FeatureGroup | any;
272
+
273
+ const mapLayers: MapLayer[] = [
274
+ {
275
+ name: "map",
276
+ label: $tr("ui.map.layer.map", "Map"),
277
+ image: new URL("../../assets/images/map/map.png", import.meta.url).href,
278
+ layer: LL.tileLayer(`http://{s}.google.com/vt/lyrs=m&x={x}&y={y}&z={z}&key=${import.meta.env.VITE_GOOGLE_MAPS_API_KEY ?? ""}`, {
279
+ maxZoom: 22,
280
+ subdomains: ['mt0', 'mt1', 'mt2', 'mt3'],
281
+ attribution: '© Google Map Data'
282
+ })
283
+ },
284
+ {
285
+ name: "imagery",
286
+ label: $tr("ui.map.layer.imagery", "Imagery"),
287
+ image: new URL("../../assets/images/map/imagery.png", import.meta.url).href,
288
+ layer: LL.tileLayer(`http://{s}.google.com/vt/lyrs=s,h&x={x}&y={y}&z={z}&key=${import.meta.env.VITE_GOOGLE_MAPS_API_KEY ?? ""}`, {
289
+ maxZoom: 22,
290
+ subdomains: ['mt0', 'mt1', 'mt2', 'mt3'],
291
+ attribution: '© Google Map Data'
292
+ })
293
+ }
294
+ ];
295
+
296
+ const bottomMargin = computed(() => {
297
+ let margin = 0;
298
+ if (props.overlayMode !== 'expand' && leftOverlayHeight.value && isExtraSmall.value) {
299
+ margin += leftOverlayHeight.value;
300
+ }
301
+ return margin;
302
+ });
303
+
304
+ const leftMargin = computed(() => {
305
+ let margin = 0;
306
+ if (leftOverlayWidth.value && !isExtraSmall.value) {
307
+ margin += leftOverlayWidth.value;
308
+ }
309
+ return margin;
310
+ });
311
+
312
+ const style = computed((): { [key: string]: string | undefined } => {
313
+ return {
314
+ "--fs-map-location-pin-color": getColors(ColorEnum.Primary).base,
315
+ "--fs-map-mylocation-pin-color": getColors(ColorEnum.Primary).base,
316
+ "--fs-map-mylocation-pin-color-alpha": getColors(ColorEnum.Primary).base + "50",
317
+ "--fs-map-leaflet-container-height": props.height as string,
318
+ "--fs-map-leaflet-bottom-overlay-margin": `${bottomMargin.value}px`,
319
+ "--fs-map-container-grayscale": props.grayscale ? '0.9' : '0'
320
+ };
321
+ });
322
+
323
+ const displayLocations = () => {
324
+ markerLayerGroup.clearLayers();
325
+ innerModelValue.value.forEach((location) => {
326
+ const size = 36;
327
+ const icon = L.divIcon({
328
+ html: locationMarkerHtml(location.icon, getColors(location.color).base),
329
+ iconSize: [size, size],
330
+ className: 'fs-map-location',
331
+ iconAnchor: [size / 2, size / 2],
332
+ });
333
+ const marker = LL.marker([location.address.latitude, location.address.longitude], { icon }).addTo(markerLayerGroup);
334
+ markers[location.id] = marker;
335
+ marker.on('click', () => emit('update:selectedLocationId', location.id));
336
+ });
337
+ };
338
+
339
+ const displayAreas = () => {
340
+ areaLayerGroup.clearLayers();
341
+ props.areas.forEach((area) => {
342
+ const areaPolygon = LL.polygon(area.coordinates.map((coord) => [coord.latitude, coord.longitude]), {
343
+ color: area.color,
344
+ fillColor: area.color + "50",
345
+ fillOpacity: 0.5,
346
+ className: 'fs-map-area',
347
+ }).addTo(areaLayerGroup);
348
+
349
+ areas[area.id] = areaPolygon;
350
+ areaPolygon.on('click', () => emit('update:selectedAreaId', area.id));
351
+ });
352
+ };
353
+
354
+ const modifyLocationAddress = (locationId: string, newAddress: Address) => {
355
+ const location = innerModelValue.value.find((loc) => loc.id === locationId);
356
+ if (!location) {
357
+ return;
358
+ }
359
+ const newLocation = {
360
+ ...location,
361
+ address: {
362
+ ...newAddress
363
+ },
364
+ };
365
+ innerModelValue.value = innerModelValue.value.map((loc) => loc.id === locationId ? newLocation : loc);
366
+ };
367
+
368
+ const initMap = () => {
369
+ if (props.editable) {
370
+ markerLayerGroup = new LL.FeatureGroup();
371
+ }
372
+ else {
373
+ markerLayerGroup = new LL.MarkerClusterGroup({
374
+ spiderfyOnMaxZoom: false,
375
+ showCoverageOnHover: false,
376
+ disableClusteringAtZoom: 17,
377
+ iconCreateFunction: function (cluster: any) {
378
+ const size = 36;
379
+ return L.divIcon({
380
+ html: clusterMarkerHtml(cluster.getChildCount()),
381
+ className: 'fs-map-location fs-map-location-full',
382
+ iconSize: [size, size],
383
+ iconAnchor: [size / 2, size / 2],
384
+ });
385
+ }
386
+ });
387
+ }
388
+ const mapOptions = {
389
+ zoomControl: false,
390
+ scrollWheelZoom: props.enableScrollWheelZoom,
391
+ minZoom: 2,
392
+ maxBounds: LL.latLngBounds(LL.latLng(-90, -180), LL.latLng(90, 180)),
393
+ maxBoundsViscosity: 1.0
394
+ };
395
+ map = LL.map(mapId, mapOptions).setView([props.center[0], props.center[1]], defaultZoom);
396
+ map.attributionControl.remove();
397
+ LL.control.attribution({ position: 'bottomleft' }).addTo(map);
398
+
399
+ baseLayerGroup.addTo(map);
400
+ areaLayerGroup.addTo(map);
401
+ myLocationLayerGroup.addTo(map);
402
+ setMapBaseLayer(innerSelectedLayer.value);
403
+ displayAreas();
404
+ displayLocations();
405
+ markerLayerGroup.addTo(map);
406
+
407
+ if (innerModelValue.value.length > 0) {
408
+ map.fitBounds(markerLayerGroup.getBounds(), { maxZoom: defaultZoom });
409
+ }
410
+
411
+ map.on('click', (e: L.LeafletMouseEvent) => {
412
+ if (editingLocation.value) {
413
+ onNewCoordEntered(+e.latlng.lat.toFixed(6), +e.latlng.lng.toFixed(6));
414
+ }
415
+ });
416
+ };
417
+
418
+ const setMapBaseLayer = (layerName: 'map' | 'imagery') => {
419
+ const layer = mapLayers.find((mapLayer) => mapLayer.name === layerName) ?? mapLayers[0];
420
+ baseLayerGroup.clearLayers();
421
+ layer.layer.addTo(baseLayerGroup);
422
+ };
423
+
424
+ const onNewAddressEntered = (address: Address) => {
425
+ if (!props.selectedLocationId || !map) {
426
+ return;
427
+ }
428
+ modifyLocationAddress(props.selectedLocationId, address);
429
+ map.panTo(calculateTargetPosition(new L.LatLng(address.latitude, address.longitude)));
430
+ };
431
+
432
+ const onNewCoordEntered = async (lat: number, lng: number) => {
433
+ const address = await reverseSearch(lat, lng);
434
+
435
+ onNewAddressEntered({
436
+ ...address,
437
+ latitude: lat,
438
+ longitude: lng,
439
+ });
440
+ };
441
+
442
+ const zoomIn = () => {
443
+ if (!map) {
444
+ return;
445
+ }
446
+ map.zoomIn();
447
+ };
448
+
449
+ const zoomOut = () => {
450
+ if (!map) {
451
+ return;
452
+ }
453
+ map.zoomOut();
454
+ };
455
+
456
+ const locate = () => {
457
+ if (!map) {
458
+ return;
459
+ }
460
+ map.locate();
461
+ map.on('locationfound', (e: L.LocationEvent) => {
462
+ map.panTo(calculateTargetPosition(e.latlng));
463
+ const size= 16;
464
+ const icon = L.divIcon({
465
+ html: myLocationMarkerHtml(L),
466
+ className: 'fs-map-mylocation',
467
+ iconSize: [size, size],
468
+ iconAnchor: [size / 2, size / 2],
469
+ });
470
+ myLocationLayerGroup.clearLayers();
471
+ LL.marker(e.latlng, { icon }).addTo(myLocationLayerGroup);
472
+ });
473
+ };
474
+
475
+ const calculateTargetPosition = (target: L.LatLng, zoom: number = map.getZoom()) => {
476
+ const targetPoint = map.project(target, zoom).subtract([leftMargin.value / 2, -bottomMargin.value / 2]);
477
+ return map.unproject(targetPoint, zoom);
478
+ }
479
+
480
+ const onCancel = () => {
481
+ editingLocation.value = false;
482
+ innerModelValue.value = props.modelValue;
483
+ if (!map) {
484
+ return;
485
+ }
486
+ displayLocations();
487
+ if (innerModelValue.value.length > 0) {
488
+ map.fitBounds(markerLayerGroup.getBounds(), { maxZoom: defaultZoom });
489
+ }
490
+ else {
491
+ map.panTo(calculateTargetPosition(new L.LatLng(props.center[0], props.center[1])), { animate: false });
492
+ }
493
+ if (props.modelValue.length > 1) {
494
+ emit('update:selectedLocationId', null);
495
+ }
496
+ };
497
+
498
+ const onSubmit = () => {
499
+ emit('update:modelValue', innerModelValue.value);
500
+ if (!map) {
501
+ return;
502
+ }
503
+ editingLocation.value = false;
504
+ if (innerModelValue.value.length > 0) {
505
+ map.fitBounds(markerLayerGroup.getBounds(), { maxZoom: defaultZoom });
506
+ }
507
+ else {
508
+ map.panTo(calculateTargetPosition(new L.LatLng(props.center[0], props.center[1])), { animate: false });
509
+ }
510
+ if (props.modelValue.length > 1) {
511
+ emit('update:selectedLocationId', null);
512
+ }
513
+ };
514
+
515
+ onMounted(() => {
516
+ initMap();
517
+ if (props.selectedLocationId && props.modelValue.length === 1) {
518
+ editingLocation.value = true;
519
+ }
520
+
521
+ resizeObserver.value = new ResizeObserver(entries => {
522
+ entries.forEach((entry) => {
523
+ if (entry.target.id === `left-overlay-${mapId}`) {
524
+ leftOverlayWidth.value = entry.contentRect.width;
525
+ }
526
+ if (entry.target.id === `left-overlay-mobile-${mapId}`) {
527
+ leftOverlayHeight.value = entry.contentRect.height;
528
+ }
529
+ });
530
+ });
531
+ if (document.querySelector(`#left-overlay-mobile-${mapId}`)) {
532
+ resizeObserver.value.observe(document.querySelector(`#left-overlay-mobile-${mapId}`)!);
533
+ }
534
+ if (document.querySelector(`#left-overlay-${mapId}`)) {
535
+ resizeObserver.value.observe(document.querySelector(`#left-overlay-${mapId}`)!);
536
+ }
537
+ });
538
+
539
+ onUnmounted((): void => {
540
+ if (resizeObserver.value) {
541
+ resizeObserver.value.disconnect();
542
+ }
543
+ });
544
+
545
+ watch(() => innerModelValue.value, () => {
546
+ displayLocations();
547
+ });
548
+
549
+ watch(() => props.selectedLocationId, () => {
550
+ if (!props.selectedLocationId || !map) {
551
+ return;
552
+ }
553
+
554
+ Object.values(markers).forEach((marker) => {
555
+ marker.getElement()?.classList.remove('fs-map-location-selected');
556
+ });
557
+
558
+ const marker = markers[props.selectedLocationId];
559
+ map.flyTo(calculateTargetPosition(marker.getLatLng(), 17), 17, { animate: false });
560
+ marker.getElement()?.classList.add('fs-map-location-selected');
561
+ })
562
+
563
+ watch(() => props.selectedAreaId, () => {
564
+ if (!props.selectedAreaId || !map) {
565
+ return;
566
+ }
567
+ const area = areas[props.selectedAreaId];
568
+ if (area) {
569
+ map.fitBounds(area.getBounds(), { maxZoom: 17 });
570
+ }
571
+ });
572
+
573
+ watch(innerSelectedLayer, () => {
574
+ setMapBaseLayer(innerSelectedLayer.value);
575
+ });
576
+
577
+ return {
578
+ bottomMargin,
579
+ editingLocation,
580
+ fullScreen,
581
+ innerModelValue,
582
+ innerSelectedLayer,
583
+ mapLayers,
584
+ mapId,
585
+ style,
586
+ L,
587
+ onNewAddressEntered,
588
+ onNewCoordEntered,
589
+ setMapBaseLayer,
590
+ onCancel,
591
+ onSubmit,
592
+ zoomOut,
593
+ locate,
594
+ zoomIn
595
+ };
596
+ }
597
+ });
598
+ </script>