@dative-gpi/foundation-shared-components 0.0.230 → 0.1.68

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 (228) hide show
  1. package/components/FSAccordionPanel.vue +8 -10
  2. package/components/FSBadge.vue +2 -4
  3. package/components/FSBreadcrumbs.vue +9 -10
  4. package/components/FSButton.vue +81 -31
  5. package/components/FSCalendar.vue +35 -33
  6. package/components/FSCalendarTwin.vue +77 -84
  7. package/components/FSCard.vue +29 -16
  8. package/components/FSCardPlaceholder.vue +1 -2
  9. package/components/FSCheckbox.vue +5 -7
  10. package/components/FSChip.vue +9 -11
  11. package/components/FSClickable.vue +67 -57
  12. package/components/FSClock.vue +4 -4
  13. package/components/FSCol.vue +2 -3
  14. package/components/FSColor.vue +13 -31
  15. package/components/FSColorIcon.vue +25 -9
  16. package/components/FSDialog.vue +1 -2
  17. package/components/FSDialogFormBody.vue +4 -4
  18. package/components/FSDialogMenu.vue +2 -4
  19. package/components/FSDialogMultiFormBody.vue +63 -50
  20. package/components/FSDialogRemove.vue +1 -0
  21. package/components/FSDialogSubmit.vue +5 -7
  22. package/components/FSDivider.vue +7 -9
  23. package/components/FSEditImage.vue +27 -274
  24. package/components/FSEditImageUI.vue +303 -0
  25. package/components/FSErrorToast.vue +3 -4
  26. package/components/FSFadeOut.vue +18 -13
  27. package/components/FSGrid.vue +6 -9
  28. package/components/FSGridMosaic.vue +1 -2
  29. package/components/FSIcon.vue +7 -8
  30. package/components/FSIconCard.vue +23 -30
  31. package/components/FSIconCheck.vue +1 -2
  32. package/components/FSImage.vue +21 -193
  33. package/components/FSImageCard.vue +2 -2
  34. package/components/FSImageUI.vue +212 -0
  35. package/components/FSLabel.vue +18 -20
  36. package/components/FSLink.vue +19 -21
  37. package/components/FSLoader.vue +10 -14
  38. package/components/FSOptionGroup.vue +58 -12
  39. package/components/FSPagination.vue +3 -5
  40. package/components/FSRadio.vue +5 -7
  41. package/components/FSRadioGroup.vue +2 -4
  42. package/components/FSRow.vue +3 -4
  43. package/components/FSSlideGroup.vue +15 -4
  44. package/components/FSSlider.vue +9 -11
  45. package/components/FSSpan.vue +11 -9
  46. package/components/FSSwitch.vue +65 -37
  47. package/components/FSTab.vue +2 -4
  48. package/components/FSTabs.vue +35 -13
  49. package/components/FSTag.vue +23 -14
  50. package/components/FSTagGroup.vue +3 -5
  51. package/components/FSText.vue +8 -12
  52. package/components/FSWindow.vue +10 -3
  53. package/components/FSWrapGroup.vue +15 -4
  54. package/components/agenda/FSAgenda.vue +204 -0
  55. package/components/agenda/FSAgendaDialogCalendar.vue +76 -0
  56. package/components/agenda/FSAgendaHeader.vue +190 -0
  57. package/components/agenda/FSAgendaHorizontalEvent.vue +162 -0
  58. package/components/agenda/FSAgendaHorizontalTimeLineMarker.vue +46 -0
  59. package/components/agenda/FSAgendaHoursCol.vue +103 -0
  60. package/components/agenda/FSAgendaHoursRow.vue +124 -0
  61. package/components/agenda/FSAgendaVerticalEvent.vue +160 -0
  62. package/components/agenda/FSAgendaVerticalTimeLineMarker.vue +46 -0
  63. package/components/agenda/FSDayAgenda.vue +200 -0
  64. package/components/agenda/FSMonthAgenda.vue +257 -0
  65. package/components/agenda/FSSelectAgendaMode.vue +54 -0
  66. package/components/agenda/FSWeekAgenda.vue +328 -0
  67. package/components/autocompletes/FSAutocompleteLanguage.vue +18 -39
  68. package/components/autocompletes/FSAutocompleteTimeZone.vue +20 -38
  69. package/components/buttons/FSButtonCancelMini.vue +1 -0
  70. package/components/buttons/FSButtonCopy.vue +28 -0
  71. package/components/buttons/FSButtonCopyIcon.vue +28 -0
  72. package/components/buttons/FSButtonCopyLabel.vue +27 -0
  73. package/components/buttons/FSButtonCopyMini.vue +28 -0
  74. package/components/buttons/FSButtonDragIcon.vue +27 -0
  75. package/components/buttons/FSButtonDuplicateMini.vue +1 -0
  76. package/components/buttons/FSButtonEditMini.vue +1 -0
  77. package/components/buttons/FSButtonFileMini.vue +1 -0
  78. package/components/buttons/FSButtonNextMini.vue +1 -0
  79. package/components/buttons/FSButtonPreviousMini.vue +1 -0
  80. package/components/buttons/FSButtonRedoMini.vue +1 -0
  81. package/components/buttons/FSButtonRemoveMini.vue +1 -0
  82. package/components/buttons/FSButtonSaveMini.vue +1 -0
  83. package/components/buttons/FSButtonSearchMini.vue +1 -0
  84. package/components/buttons/FSButtonUndoMini.vue +1 -0
  85. package/components/buttons/FSButtonUpdateMini.vue +1 -0
  86. package/components/buttons/FSButtonValidateMini.vue +1 -0
  87. package/components/deviceOrganisations/FSConnectivity.vue +11 -1
  88. package/components/deviceOrganisations/FSConnectivityCard.vue +20 -49
  89. package/components/deviceOrganisations/FSStatus.vue +11 -1
  90. package/components/deviceOrganisations/FSStatusCard.vue +40 -60
  91. package/components/deviceOrganisations/FSStatusesCarousel.vue +1 -0
  92. package/components/deviceOrganisations/FSStatusesRow.vue +10 -5
  93. package/components/deviceOrganisations/FSWorstAlert.vue +19 -13
  94. package/components/deviceOrganisations/FSWorstAlertCard.vue +36 -47
  95. package/components/fields/FSAutocompleteField.vue +501 -323
  96. package/components/fields/FSAutocompleteTag.vue +100 -0
  97. package/components/fields/FSBaseField.vue +26 -16
  98. package/components/fields/FSColorField.vue +63 -55
  99. package/components/fields/FSCommentField.vue +93 -0
  100. package/components/fields/FSDateField.vue +2 -2
  101. package/components/fields/FSDateRangeField.vue +2 -2
  102. package/components/fields/FSDateTimeField.vue +4 -3
  103. package/components/fields/FSDateTimeRangeField.vue +7 -6
  104. package/components/fields/FSEntityFieldUI.vue +271 -0
  105. package/components/fields/FSGradientField.vue +27 -33
  106. package/components/fields/FSIconField.vue +2 -3
  107. package/components/fields/FSMagicConfigField.vue +28 -19
  108. package/components/fields/FSMagicField.vue +25 -17
  109. package/components/fields/FSNumberField.vue +9 -7
  110. package/components/fields/FSPasswordField.vue +2 -3
  111. package/components/fields/FSRichTextField.vue +32 -5
  112. package/components/fields/FSSearchField.vue +2 -2
  113. package/components/fields/FSSelectField.vue +483 -251
  114. package/components/fields/FSTagField.vue +4 -6
  115. package/components/fields/FSTermField.vue +25 -13
  116. package/components/fields/FSTextArea.vue +18 -6
  117. package/components/fields/FSTextField.vue +13 -10
  118. package/components/fields/FSTimeField.vue +1 -1
  119. package/components/fields/FSTimeSlotField.vue +3 -4
  120. package/components/fields/FSTimeStepField.vue +157 -0
  121. package/components/fields/FSTranslateField.vue +3 -2
  122. package/components/fields/FSTranslateRichTextField.vue +4 -3
  123. package/components/fields/FSTranslateTextArea.vue +233 -0
  124. package/components/fields/FSTreeViewField.vue +7 -9
  125. package/components/fields/periodicField/FSPeriodicDailyField.vue +120 -0
  126. package/components/fields/periodicField/FSPeriodicField.vue +131 -0
  127. package/components/fields/periodicField/FSPeriodicMonthlyField.vue +222 -0
  128. package/components/fields/periodicField/FSPeriodicWeeklyField.vue +120 -0
  129. package/components/fields/periodicField/FSPeriodicYearlyField.vue +144 -0
  130. package/components/lists/FSDataIteratorItem.vue +8 -10
  131. package/components/lists/FSDataTableUI.vue +47 -39
  132. package/components/lists/FSFilterButton.vue +20 -22
  133. package/components/lists/FSHiddenButton.vue +10 -12
  134. package/components/lists/FSLoadDataTable.vue +4 -6
  135. package/components/lists/FSSimpleList.vue +229 -0
  136. package/components/map/FSMap.vue +256 -405
  137. package/components/map/FSMapFeatureGroup.vue +51 -0
  138. package/components/map/FSMapLayerButton.vue +6 -3
  139. package/components/map/FSMapMarker.vue +116 -0
  140. package/components/map/FSMapMarkerClusterGroup.vue +72 -0
  141. package/components/map/FSMapOverlay.vue +69 -83
  142. package/components/map/FSMapPolygon.vue +81 -0
  143. package/components/map/FSMapTileLayer.vue +50 -0
  144. package/components/map/keys.ts +4 -0
  145. package/components/selects/FSSelectAutoRefresh.vue +1 -1
  146. package/components/selects/FSSelectDashboardVariableType.vue +1 -1
  147. package/components/selects/FSSelectDateSetting.vue +3 -3
  148. package/components/selects/FSSelectDays.vue +1 -1
  149. package/components/selects/FSSelectListMode.vue +51 -0
  150. package/components/selects/FSSelectMonths.vue +67 -0
  151. package/components/tiles/FSChartTileUI.vue +116 -0
  152. package/components/tiles/FSCommentTileUI.vue +149 -0
  153. package/components/tiles/FSDashboardOrganisationTileUI.vue +6 -8
  154. package/components/tiles/FSDashboardOrganisationTypeTileUI.vue +6 -8
  155. package/components/tiles/FSDashboardShallowTileUI.vue +6 -8
  156. package/components/tiles/FSDeviceOrganisationTileUI.vue +30 -18
  157. package/components/tiles/FSFolderTileUI.vue +6 -6
  158. package/components/tiles/FSGroupTileUI.vue +31 -22
  159. package/components/tiles/FSLoadTile.vue +5 -7
  160. package/components/tiles/FSLocationTileUI.vue +157 -0
  161. package/components/tiles/FSModelTileUI.vue +18 -0
  162. package/components/tiles/FSServiceAccountOrganisationTileUI.vue +142 -0
  163. package/components/tiles/FSSimpleTileUI.vue +57 -36
  164. package/components/tiles/FSTile.vue +115 -55
  165. package/components/tiles/FSUserOrganisationTileUI.vue +25 -35
  166. package/components/toggleSets/FSToggleSetPosition.vue +1 -1
  167. package/components/views/FSEntityHeader.vue +343 -0
  168. package/components/views/FSEntityView.vue +163 -0
  169. package/components/views/FSListHeader.vue +83 -0
  170. package/components/views/FSListView.vue +83 -0
  171. package/components/views/FSSkeletonView.vue +100 -0
  172. package/composables/useAddress.ts +2 -2
  173. package/composables/useColors.ts +15 -5
  174. package/composables/useMagicFieldProvider.ts +7 -6
  175. package/composables/useSlots.ts +34 -30
  176. package/models/agenda.ts +9 -0
  177. package/models/deviceAlerts.ts +1 -1
  178. package/models/deviceConnectivities.ts +1 -1
  179. package/models/index.ts +1 -0
  180. package/models/magicFields.ts +1 -0
  181. package/models/map.ts +2 -2
  182. package/models/rules.ts +10 -5
  183. package/models/tables.ts +3 -1
  184. package/package.json +4 -4
  185. package/styles/components/fs_agenda.scss +32 -0
  186. package/styles/components/fs_agenda_event.scss +41 -0
  187. package/styles/components/fs_agenda_hours_col.scss +4 -0
  188. package/styles/components/fs_agenda_hours_row.scss +13 -0
  189. package/styles/components/fs_agenda_time_line_marker.scss +19 -0
  190. package/styles/components/fs_card.scss +0 -1
  191. package/styles/components/fs_clickable.scss +5 -3
  192. package/styles/components/fs_color_field.scss +1 -0
  193. package/styles/components/fs_data_table.scss +2 -0
  194. package/styles/components/fs_dialog.scss +11 -15
  195. package/styles/components/fs_edit_image.scss +8 -0
  196. package/styles/components/fs_fade_out.scss +2 -1
  197. package/styles/components/fs_filter_button.scss +1 -6
  198. package/styles/components/fs_gradient_field.scss +11 -11
  199. package/styles/components/fs_magic_config_field.scss +2 -2
  200. package/styles/components/fs_map.scss +36 -30
  201. package/styles/components/fs_option_group.scss +15 -5
  202. package/styles/components/fs_radio.scss +11 -0
  203. package/styles/components/fs_rich_text_field.scss +2 -1
  204. package/styles/components/fs_search_field.scss +3 -0
  205. package/styles/components/fs_select_date_settings.scss +3 -0
  206. package/styles/components/fs_slide_group.scss +5 -0
  207. package/styles/components/fs_span.scss +2 -1
  208. package/styles/components/fs_switch.scss +1 -0
  209. package/styles/components/fs_tabs.scss +9 -0
  210. package/styles/components/fs_text_area.scss +15 -1
  211. package/styles/components/fs_tile.scss +22 -6
  212. package/styles/components/fs_window.scss +5 -0
  213. package/styles/components/fs_wrap_group.scss +4 -0
  214. package/styles/components/index.scss +6 -1
  215. package/styles/globals/overrides.scss +22 -19
  216. package/styles/globals/text_fonts.scss +17 -55
  217. package/tools/alertsTools.ts +54 -0
  218. package/tools/chartsTools.ts +300 -0
  219. package/tools/index.ts +2 -0
  220. package/utils/badge.ts +9 -0
  221. package/utils/filter.ts +18 -0
  222. package/utils/index.ts +2 -0
  223. package/utils/leafletMarkers.ts +8 -2
  224. package/utils/statuses.ts +1 -1
  225. package/utils/time.ts +27 -1
  226. package/components/autocompletes/FSAutocompleteTag.vue +0 -138
  227. package/components/map/FSMapEditPointAddressOverlay.vue +0 -164
  228. package/styles/components/fs_map_overlay.scss +0 -38
@@ -1,164 +1,159 @@
1
1
  <template>
2
2
  <FSCard
3
+ class="fs-map"
3
4
  :width="$props.width"
5
+ :height="$props.height"
4
6
  :style="style"
5
7
  v-bind="$attrs"
6
8
  >
7
- <FSCol
8
- v-if="L"
9
- width="fill"
10
- :class="['fs-map', { 'fs-map-fullscreen': fullScreen }]"
9
+ <div
10
+ ref="leafletContainer"
11
+ class="fs-leaflet-container"
11
12
  >
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)"
13
+ <template
14
+ v-if="map"
18
15
  >
19
- <template
20
- v-slot:leftoverlay-header
16
+ <FSMapTileLayer
17
+ :layer="actualLayer"
18
+ />
19
+ <FSMapMarker
20
+ v-if="gpsPosition"
21
+ variant="gps"
22
+ :color="ColorEnum.Primary"
23
+ :latlng="gpsPosition"
24
+ />
25
+
26
+ <FSMapFeatureGroup
27
+ v-if="$props.areas"
28
+ :expected-layers="$props.areas.length"
29
+ @update:bounds="(bounds) => areaGroupBounds = bounds"
21
30
  >
22
- <slot
23
- name="leftoverlay-header"
31
+ <FSMapPolygon
32
+ v-for="area in areas"
33
+ :key="area.id"
34
+ :color="area.color"
35
+ :latlngs="area.coordinates.map((coord) => ({lat: coord.latitude, lng: coord.longitude}))"
36
+ @click="$emit('update:selectedAreaId', area.id)"
24
37
  />
25
- </template>
26
- <template
27
- v-slot:leftoverlay-body
38
+ </FSMapFeatureGroup>
39
+
40
+ <FSMapMarkerClusterGroup
41
+ v-if="$props.locations"
42
+ :expected-layers="$props.locations.length"
43
+ :disableClusteringAtZoom="defaultZoom"
44
+ @update:bounds="(bounds) => locationGroupBounds = bounds"
28
45
  >
29
- <slot
30
- name="leftoverlay-body"
46
+ <FSMapMarker
47
+ v-for="location in $props.locations"
48
+ :selected="location.id === $props.selectedLocationId"
49
+ :key="location.id"
50
+ :color="location.color ?? ColorEnum.Primary"
51
+ :icon="location.icon ?? 'mdi-map-marker'"
52
+ :latlng="{lat: location.address.latitude, lng: location.address.longitude}"
53
+ @click="$emit('update:selectedLocationId', location.id)"
31
54
  />
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"
55
+ </FSMapMarkerClusterGroup>
56
+ </template>
57
+ </div>
58
+
59
+ <FSMapLayerButton
60
+ v-if="$props.allowedLayers?.length && $props.allowedLayers.length > 1"
61
+ :layers="mapLayers.filter((layer) => $props.allowedLayers?.includes(layer.name) ?? true)"
62
+ :modelValue="$props.currentLayer"
63
+ @update:model-value="$emit('update:currentLayer', $event)"
64
+ />
65
+
66
+ <FSCol
67
+ v-if="map"
68
+ class="fs-map-control-buttons"
69
+ >
70
+ <FSButton
71
+ v-if="$props.showMyLocation"
72
+ icon="mdi-crosshairs-gps"
73
+ color="primary"
74
+ variant="full"
75
+ :elevation="true"
76
+ @click="() => map!.locate()"
77
+ />
78
+ <FSCard
79
+ v-if="$props.showZoomButtons"
80
+ :elevation="true"
53
81
  >
54
- <slot
55
- name="toprightoverlay"
82
+ <FSCol
83
+ gap="0"
56
84
  >
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"
85
+ <FSButton
86
+ class="fs-map-zoom-plus-button"
87
+ icon="mdi-plus"
88
+ @click="() => map!.zoomIn()"
89
+ :border="false"
90
+ />
91
+ <FSButton
92
+ class="fs-map-zoom-minus-button"
93
+ icon="mdi-minus"
94
+ @click="() => map!.zoomOut()"
95
+ :border="false"
96
+ />
97
+ </FSCol>
98
+ </FSCard>
99
+ </FSCol>
100
+
101
+ <FSMapOverlay
102
+ v-if="$slots['overlay']"
103
+ :mode="$props.overlayMode"
104
+ @update:mode="$emit('update:overlayMode', $event)"
105
+ @update:height="(height) => overlayHeight = height"
106
+ @update:width="(width) => overlayWidth = width"
107
+ >
108
+ <template
109
+ #body
77
110
  >
78
111
  <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"
112
+ name="overlay"
125
113
  />
126
- </FSCol>
127
- </FSCol>
114
+ </template>
115
+ </FSMapOverlay>
128
116
  </FSCard>
129
117
  </template>
130
118
 
131
119
  <script lang="ts">
132
- import { computed, defineComponent, onMounted, onUnmounted, type PropType, ref, watch } from "vue";
120
+ import { computed, defineComponent, onMounted, type Ref, provide, type PropType, ref, type StyleValue, watch, onUnmounted, markRaw } from "vue";
133
121
 
134
- import * as L from "leaflet";
135
- import "leaflet.markercluster";
122
+ import type {} from "leaflet.markercluster";
123
+ import { map as createMap, control, tileLayer, latLngBounds, latLng, type LatLng, LatLngBounds, type FitBoundsOptions } from "leaflet";
136
124
 
137
125
  import { useTranslations as useTranslationsProvider } from "@dative-gpi/bones-ui/composables";
138
- import { type Address, type FSArea } from '@dative-gpi/foundation-shared-domain/models';
126
+ import { type FSArea } from '@dative-gpi/foundation-shared-domain/models';
139
127
 
140
- import { clusterMarkerHtml, locationMarkerHtml, myLocationMarkerHtml } from "../../utils";
128
+ import { useBreakpoints, useColors } from "../../composables";
141
129
  import { ColorEnum, type FSLocation, type MapLayer } from "../../models";
142
- import { useColors, useAddress, useBreakpoints } from "../../composables";
143
130
 
144
- import FSMapEditPointAddressOverlay from "./FSMapEditPointAddressOverlay.vue";
145
131
  import FSMapLayerButton from "./FSMapLayerButton.vue";
146
132
  import FSMapOverlay from "./FSMapOverlay.vue";
147
133
  import FSButton from "../FSButton.vue";
148
134
  import FSCard from "../FSCard.vue";
149
135
  import FSCol from "../FSCol.vue";
150
- import FSRow from "../FSRow.vue";
136
+
137
+ import FSMapMarker from "./FSMapMarker.vue";
138
+ import FSMapTileLayer from "./FSMapTileLayer.vue";
139
+ import FSMapFeatureGroup from "./FSMapFeatureGroup.vue";
140
+ import FSMapMarkerClusterGroup from "./FSMapMarkerClusterGroup.vue";
141
+ import FSMapPolygon from "./FSMapPolygon.vue";
151
142
 
152
143
  export default defineComponent({
153
144
  name: "FSMap",
154
145
  components: {
155
- FSMapEditPointAddressOverlay,
146
+ FSMapMarker,
147
+ FSMapTileLayer,
148
+ FSMapFeatureGroup,
149
+ FSMapMarkerClusterGroup,
150
+ FSMapPolygon,
151
+
156
152
  FSMapLayerButton,
157
153
  FSMapOverlay,
158
154
  FSButton,
159
155
  FSCard,
160
156
  FSCol,
161
- FSRow
162
157
  },
163
158
  props: {
164
159
  height: {
@@ -176,11 +171,6 @@ export default defineComponent({
176
171
  required: false,
177
172
  default: false
178
173
  },
179
- editable: {
180
- type: Boolean,
181
- required: false,
182
- default: false
183
- },
184
174
  overlayMode: {
185
175
  type: String as PropType<'collapse' | 'half' | 'expand'>,
186
176
  required: false,
@@ -196,11 +186,6 @@ export default defineComponent({
196
186
  required: false,
197
187
  default: true
198
188
  },
199
- showFullScreen: {
200
- type: Boolean,
201
- required: false,
202
- default: false
203
- },
204
189
  enableScrollWheelZoom: {
205
190
  type: Boolean,
206
191
  required: false,
@@ -211,7 +196,7 @@ export default defineComponent({
211
196
  required: false,
212
197
  default: () => [45.71, 5.07]
213
198
  },
214
- modelValue: {
199
+ locations: {
215
200
  type: Array as PropType<FSLocation[]>,
216
201
  required: false,
217
202
  default: () => [],
@@ -221,12 +206,12 @@ export default defineComponent({
221
206
  required: false,
222
207
  default: () => [],
223
208
  },
224
- selectedLayer: {
209
+ currentLayer: {
225
210
  type: String as PropType<"map" | "imagery">,
226
211
  required: false,
227
212
  default: "map"
228
213
  },
229
- selectableLayers: {
214
+ allowedLayers: {
230
215
  type: Array as PropType<string[]>,
231
216
  required: false,
232
217
  default: () => ["map", "imagery"]
@@ -242,40 +227,36 @@ export default defineComponent({
242
227
  default: null
243
228
  }
244
229
  },
245
- emits: ["update:modelValue", "update:selectedLocationId", "update:selectedAreaId", 'update:overlayMode'],
230
+ emits: ["update:modelValue", "update:selectedLocationId", "update:selectedAreaId", 'update:overlayMode', 'update:currentLayer', "click:latlng"],
246
231
  setup(props, { emit }) {
247
232
  const { $tr } = useTranslationsProvider();
248
- const { reverseSearch } = useAddress();
249
233
  const { getColors } = useColors();
250
234
  const { isExtraSmall } = useBreakpoints();
251
235
 
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);
236
+ const leafletContainer = ref<HTMLElement>();
237
+ const locationGroupBounds = ref<LatLngBounds>();
238
+ const areaGroupBounds = ref<LatLngBounds>();
239
+ const gpsPosition : Ref<LatLng | null> = ref(null);
240
+ const map: Ref<L.Map | null> = ref(null);
241
+ const overlayHeight = ref<number>();
242
+ const overlayWidth = ref<number>();
261
243
 
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();
244
+ provide('map', map);
269
245
 
270
- let map: L.Map;
271
- let markerLayerGroup: L.FeatureGroup | any;
246
+ const defaultZoom = 16;
247
+ const mapResizeObserver = new ResizeObserver(() => {
248
+ if(!map.value) {
249
+ return;
250
+ }
251
+ map.value.invalidateSize();
252
+ });
272
253
 
273
254
  const mapLayers: MapLayer[] = [
274
255
  {
275
256
  name: "map",
276
257
  label: $tr("ui.map.layer.map", "Map"),
277
258
  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 ?? ""}`, {
259
+ layer: tileLayer(`http://{s}.google.com/vt/lyrs=m&x={x}&y={y}&z={z}&key=${import.meta.env.VITE_GOOGLE_MAPS_API_KEY ?? ""}`, {
279
260
  maxZoom: 22,
280
261
  subdomains: ['mt0', 'mt1', 'mt2', 'mt3'],
281
262
  attribution: '© Google Map Data'
@@ -285,7 +266,7 @@ export default defineComponent({
285
266
  name: "imagery",
286
267
  label: $tr("ui.map.layer.imagery", "Imagery"),
287
268
  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 ?? ""}`, {
269
+ layer: 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
270
  maxZoom: 22,
290
271
  subdomains: ['mt0', 'mt1', 'mt2', 'mt3'],
291
272
  attribution: '© Google Map Data'
@@ -293,305 +274,175 @@ export default defineComponent({
293
274
  }
294
275
  ];
295
276
 
296
- const bottomMargin = computed(() => {
297
- let margin = 0;
298
- if (props.overlayMode !== 'expand' && leftOverlayHeight.value && isExtraSmall.value) {
299
- margin += leftOverlayHeight.value;
277
+ const bottomOffset = computed(() => {
278
+ if (props.overlayMode !== 'expand' && overlayHeight.value && isExtraSmall.value) {
279
+ return overlayHeight.value;
300
280
  }
301
- return margin;
281
+ return 0;
302
282
  });
303
283
 
304
- const leftMargin = computed(() => {
305
- let margin = 0;
306
- if (leftOverlayWidth.value && !isExtraSmall.value) {
307
- margin += leftOverlayWidth.value;
284
+ const leftOffset = computed(() => {
285
+ if (overlayWidth.value && !isExtraSmall.value) {
286
+ return overlayWidth.value;
308
287
  }
309
- return margin;
288
+ return 0;
310
289
  });
311
290
 
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
- };
291
+ const style = computed((): StyleValue => ({
292
+ "--fs-map-location-pin-color": getColors(ColorEnum.Primary).base,
293
+ "--fs-map-mylocation-pin-color": getColors(ColorEnum.Primary).base,
294
+ "--fs-map-mylocation-pin-color-alpha": getColors(ColorEnum.Primary).base + "50",
295
+ "--fs-map-container-grayscale": props.grayscale ? '0.9' : '0',
296
+ "--fs-map-control-buttons-margin-bottom": `${bottomOffset.value}px`,
297
+ }));
298
+
299
+ const actualLayer = computed(() => {
300
+ return mapLayers.find((layer) => layer.name === props.currentLayer)?.layer ?? mapLayers[0].layer;
321
301
  });
322
302
 
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
- };
303
+ const bounds = computed<LatLngBounds | null>(() => {
304
+ if(!locationGroupBounds.value && !areaGroupBounds.value) {
305
+ return null;
306
+ }
307
+ let bounds = locationGroupBounds.value;
308
+ if(bounds && areaGroupBounds.value) {
309
+ bounds.extend(areaGroupBounds.value);
310
+ } else if(areaGroupBounds.value) {
311
+ bounds = areaGroupBounds.value;
312
+ }
313
+ return bounds as LatLngBounds;
314
+ });
338
315
 
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
- };
316
+ const calculateTargetPosition = (target: L.LatLng, zoom?: number) => {
317
+ if(!map.value) {
318
+ return target;
319
+ }
320
+ zoom = zoom ?? map.value.getZoom();
321
+ const targetPoint = map.value.project(target, zoom).subtract([leftOffset.value / 2, -bottomOffset.value / 2]);
322
+ return map.value.unproject(targetPoint, zoom);
323
+ }
353
324
 
354
- const modifyLocationAddress = (locationId: string, newAddress: Address) => {
355
- const location = innerModelValue.value.find((loc) => loc.id === locationId);
356
- if (!location) {
325
+ const flyTo = (lat: number, lng: number, zoom: number = defaultZoom) => {
326
+ if(!map.value) {
357
327
  return;
358
328
  }
359
- const newLocation = {
360
- ...location,
361
- address: {
362
- ...newAddress
363
- },
364
- };
365
- innerModelValue.value = innerModelValue.value.map((loc) => loc.id === locationId ? newLocation : loc);
366
- };
329
+ map.value.flyTo(calculateTargetPosition(latLng(lat, lng), zoom), zoom);
330
+ }
367
331
 
368
- const initMap = () => {
369
- if (props.editable) {
370
- markerLayerGroup = new LL.FeatureGroup();
332
+ const setView = (lat: number, lng: number, zoom: number) => {
333
+ if(!map.value) {
334
+ return;
371
335
  }
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
- });
336
+ map.value.setView(calculateTargetPosition(latLng(lat, lng)), zoom);
337
+ }
338
+
339
+ const fitBounds = (bounds: LatLngBounds, options?: FitBoundsOptions) => {
340
+ if(!map.value) {
341
+ return;
387
342
  }
343
+ const calculatedBounds = new LatLngBounds(
344
+ calculateTargetPosition(bounds.getSouthWest()),
345
+ calculateTargetPosition(bounds.getNorthEast())
346
+ );
347
+ map.value.fitBounds(calculatedBounds, options);
348
+ }
349
+
350
+ onMounted(() => {
351
+ if(!leafletContainer.value) {
352
+ return;
353
+ }
354
+
388
355
  const mapOptions = {
389
356
  zoomControl: false,
390
357
  scrollWheelZoom: props.enableScrollWheelZoom,
391
358
  minZoom: 2,
392
- maxBounds: LL.latLngBounds(LL.latLng(-90, -180), LL.latLng(90, 180)),
359
+ maxZoom: 22,
360
+ maxBounds: latLngBounds(latLng(-90, -180), latLng(90, 180)),
393
361
  maxBoundsViscosity: 1.0
394
362
  };
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
363
 
411
- map.on('click', (e: L.LeafletMouseEvent) => {
412
- if (editingLocation.value) {
413
- onNewCoordEntered(+e.latlng.lat.toFixed(6), +e.latlng.lng.toFixed(6));
414
- }
364
+ map.value = markRaw(createMap(leafletContainer.value, mapOptions));
365
+ setView(props.center[0], props.center[1], defaultZoom);
366
+
367
+ map.value.on('click', (e: L.LeafletMouseEvent) => {
368
+ emit('click:latlng', e.latlng);
415
369
  });
416
- };
417
370
 
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
- };
371
+ map.value.attributionControl.remove();
372
+ // to display google attribution in bottom left corner
373
+ control.attribution({ position: 'bottomleft' }).addTo(map.value);
423
374
 
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
- };
375
+ map.value.on('locationfound', (e: L.LocationEvent) => {
376
+ if(!e.latlng) {
377
+ return;
378
+ }
431
379
 
432
- const onNewCoordEntered = async (lat: number, lng: number) => {
433
- const address = await reverseSearch(lat, lng);
380
+ gpsPosition.value = e.latlng;
434
381
 
435
- onNewAddressEntered({
436
- ...address,
437
- latitude: lat,
438
- longitude: lng,
382
+ if(!map.value) {
383
+ return;
384
+ }
385
+
386
+ flyTo(e.latlng.lat, e.latlng.lng);
439
387
  });
440
- };
388
+
389
+ mapResizeObserver.observe(leafletContainer.value);
390
+ });
441
391
 
442
- const zoomIn = () => {
443
- if (!map) {
444
- return;
445
- }
446
- map.zoomIn();
447
- };
392
+ onUnmounted(() => {
393
+ mapResizeObserver.disconnect();
394
+ });
448
395
 
449
- const zoomOut = () => {
450
- if (!map) {
396
+ watch (() => props.center, (center) => {
397
+ if(!map.value) {
451
398
  return;
452
399
  }
453
- map.zoomOut();
454
- };
400
+ setView(center[0], center[1], defaultZoom);
401
+ });
455
402
 
456
- const locate = () => {
457
- if (!map) {
403
+ watch (() => props.selectedLocationId, (selectedLocationId) => {
404
+ if(!map.value) {
458
405
  return;
459
406
  }
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) {
407
+ const selectedLocation = props.locations.find((location) => location.id === selectedLocationId);
408
+ if(!selectedLocation) {
484
409
  return;
485
410
  }
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
- };
411
+ flyTo(selectedLocation?.address.latitude, selectedLocation?.address.longitude);
412
+ }, { immediate: true });
497
413
 
498
- const onSubmit = () => {
499
- emit('update:modelValue', innerModelValue.value);
500
- if (!map) {
414
+ watch(() => props.selectedAreaId, (selectedAreaId) => {
415
+ if(!map.value) {
501
416
  return;
502
417
  }
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) {
418
+ const selectedArea = props.areas.find((area) => area.id === selectedAreaId);
419
+ if(!selectedArea) {
551
420
  return;
552
421
  }
422
+ const bounds = latLngBounds(selectedArea.coordinates.map((coord) => latLng(coord.latitude, coord.longitude)));
423
+ fitBounds(bounds);
424
+ }, { immediate: true });
553
425
 
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) {
426
+ watch( () => bounds.value, (bounds) => {
427
+ if(!map.value || !bounds) {
565
428
  return;
566
429
  }
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);
430
+ fitBounds(bounds, { maxZoom: defaultZoom });
575
431
  });
576
432
 
577
433
  return {
578
- bottomMargin,
579
- editingLocation,
580
- fullScreen,
581
- innerModelValue,
582
- innerSelectedLayer,
434
+ ColorEnum,
435
+ defaultZoom,
436
+ leafletContainer,
437
+ locationGroupBounds,
438
+ overlayHeight,
439
+ overlayWidth,
440
+ areaGroupBounds,
441
+ map,
442
+ actualLayer,
583
443
  mapLayers,
584
- mapId,
585
- style,
586
- L,
587
- onNewAddressEntered,
588
- onNewCoordEntered,
589
- setMapBaseLayer,
590
- onCancel,
591
- onSubmit,
592
- zoomOut,
593
- locate,
594
- zoomIn
444
+ gpsPosition,
445
+ style
595
446
  };
596
447
  }
597
448
  });