@mapka/maplibre-gl-sdk 0.9.0 → 0.10.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.
@@ -1,59 +1,152 @@
1
- import * as maplibregl from "maplibre-gl";
1
+ import { Marker } from "maplibre-gl";
2
2
  import { get } from "es-toolkit/compat";
3
- import type { StyleSpecification } from "maplibre-gl";
4
- import type { MapkaMap } from "../map.js";
5
- import type { MapkaMarkerOptions } from "../types/marker.js";
6
- import { showTooltip, hideTooltip } from "./tooltip.js";
3
+ import { getPopupId } from "./popup.js";
4
+ import type { Offset, StyleSpecification } from "maplibre-gl";
5
+ import type { MapkaMap, MapMapkaMarker } from "../map.js";
6
+ import type { MapkaMarkerOptions, MapkaPopupOptions } from "../types/marker.js";
7
+ import { remove } from "es-toolkit";
7
8
 
8
- const prevMarkers = new Set<maplibregl.Marker>();
9
+ /**
10
+ * Default marker offset
11
+ * @see https://github.com/maplibre/maplibre-gl-js/blob/master/src/ui/marker.ts#L457
12
+ */
13
+ const DEFAULT_MARKET_OFFSET = {
14
+ top: [0, 0],
15
+ "top-left": [0, 0],
16
+ "top-right": [0, 0],
17
+ bottom: [0, -38.1],
18
+ "bottom-left": [9.54594154601839, -34.14594154601839],
19
+ "bottom-right": [-9.54594154601839, -34.14594154601839],
20
+ left: [13.5, -24.6],
21
+ right: [-13.5, -24.6],
22
+ } as Offset;
9
23
 
10
- function addMarkersToMap(map: MapkaMap, markers: MapkaMarkerOptions[]) {
11
- for (const marker of prevMarkers) {
12
- marker.remove();
24
+ export function getMarkerId(marker: { id?: string }) {
25
+ return marker.id ?? `marker-${crypto.randomUUID()}`;
26
+ }
27
+
28
+ const markerPopupOptions = (marker: Marker, popupOptions: Omit<MapkaPopupOptions, "lngLat">) => {
29
+ const latLng = marker.getLngLat().toArray();
30
+
31
+ if ("offset" in popupOptions) {
32
+ return {
33
+ ...popupOptions,
34
+ lngLat: latLng,
35
+ };
36
+ } else {
37
+ return {
38
+ ...popupOptions,
39
+ lngLat: latLng,
40
+ offset: DEFAULT_MARKET_OFFSET,
41
+ };
42
+ }
43
+ };
44
+
45
+ function setupMarkerPopupListeners(
46
+ map: MapkaMap,
47
+ marker: Marker,
48
+ popup: Omit<MapkaPopupOptions, "lngLat">,
49
+ options: MapkaMarkerOptions,
50
+ ) {
51
+ const popupId = getPopupId(popup);
52
+ const markerElement = marker.getElement();
53
+
54
+ if (options.draggable) {
55
+ marker.on("dragend", () => {
56
+ if (map.popups.find((p) => p.id === popupId)) {
57
+ map.updatePopup(markerPopupOptions(marker, popup), popupId);
58
+ }
59
+ });
60
+ marker.on("drag", () => {
61
+ if (map.popups.find((p) => p.id === popupId)) {
62
+ map.updatePopup(markerPopupOptions(marker, popup), popupId);
63
+ }
64
+ });
13
65
  }
14
- prevMarkers.clear();
15
-
16
- for (const markerConfig of markers) {
17
- const { position, color, tooltip } = markerConfig;
18
- const newMarker = new maplibregl.Marker({
19
- color,
20
- })
21
- .setLngLat(position)
22
- .addTo(map);
23
-
24
- if (tooltip?.trigger === "click") {
25
- const markerElement = newMarker.getElement();
26
-
27
- markerElement.style.cursor = "pointer";
28
-
29
- markerElement.addEventListener("click", (e) => {
30
- e.stopPropagation();
31
- showTooltip(newMarker, tooltip, map);
32
- });
33
- } else if (tooltip?.trigger === "hover") {
34
- const markerElement = newMarker.getElement();
35
-
36
- markerElement.addEventListener("mouseenter", (e) => {
37
- e.stopPropagation();
38
- showTooltip(newMarker, tooltip, map);
39
- });
40
- markerElement.addEventListener("mouseleave", (e) => {
41
- e.stopPropagation();
42
- hideTooltip();
43
- });
66
+
67
+ if (popup.trigger === "always") {
68
+ if (!map.popups.find((p) => p.id === popupId)) {
69
+ map.openPopup(markerPopupOptions(marker, popup), popupId);
44
70
  }
45
71
 
46
- prevMarkers.add(newMarker);
72
+ markerElement.addEventListener("click", (e) => {
73
+ e.stopPropagation();
74
+ if (!map.popups.find((p) => p.id === popupId)) {
75
+ map.openPopup(markerPopupOptions(marker, popup), popupId);
76
+ }
77
+ });
78
+ } else if (popup.trigger === "click") {
79
+ markerElement.style.cursor = "pointer";
80
+ markerElement.addEventListener("click", (e) => {
81
+ e.stopPropagation();
82
+ if (!map.popups.find((p) => p.id === popupId)) {
83
+ map.openPopup(markerPopupOptions(marker, popup), popupId);
84
+ }
85
+ });
86
+ } else if (popup.trigger === "hover") {
87
+ markerElement.addEventListener("mouseenter", (e) => {
88
+ e.stopPropagation();
89
+ if (!map.popups.find((p) => p.id === popupId)) {
90
+ map.openPopup(markerPopupOptions(marker, popup), popupId);
91
+ }
92
+ });
93
+ markerElement.addEventListener("mouseleave", (e) => {
94
+ e.stopPropagation();
95
+ if (map.popups.find((p) => p.id === popupId)) {
96
+ map.closePopup(popupId);
97
+ }
98
+ });
47
99
  }
48
100
  }
49
101
 
50
- export function addMarkers(map: MapkaMap) {
102
+ export function addMarkers(currentMap: MapkaMap, markersOptions: MapkaMarkerOptions[]) {
103
+ const markers: MapMapkaMarker[] = [];
104
+
105
+ for (const markerOptions of markersOptions) {
106
+ const { lngLat, popup, ...options } = markerOptions;
107
+ const newMarker = new Marker(options).setLngLat(lngLat).addTo(currentMap);
108
+
109
+ markers.push({
110
+ id: getMarkerId(markerOptions),
111
+ options: markerOptions,
112
+ marker: newMarker,
113
+ });
114
+ if (!popup) continue;
115
+
116
+ setupMarkerPopupListeners(currentMap, newMarker, popup, markerOptions);
117
+ }
118
+ currentMap.markers.push(...markers);
119
+ }
120
+
121
+ export function removeMarkersByIds(map: MapkaMap, ids: string[]) {
122
+ const removedMarkers = remove(map.markers, (marker) => ids.includes(marker.id));
123
+ for (const marker of removedMarkers) {
124
+ marker.marker.remove();
125
+ }
126
+ }
127
+
128
+ export function updateMarkers(map: MapkaMap, markersOptions: MapkaMarkerOptions[]) {
129
+ const markersIds = markersOptions.map(getMarkerId);
130
+
131
+ removeMarkersByIds(map, markersIds);
132
+ addMarkers(map, markersOptions);
133
+ }
134
+
135
+ export function clearMarkers(map: MapkaMap) {
136
+ for (const marker of map.markers) {
137
+ marker.marker.remove();
138
+ }
139
+ map.markers = [];
140
+ }
141
+
142
+ export function addStyleMarkers(map: MapkaMap) {
51
143
  const style = map.getStyle();
144
+
52
145
  const markers = get(style, "metadata.mapka.markers", []) as MapkaMarkerOptions[];
53
- addMarkersToMap(map, markers);
146
+ return addMarkers(map, markers);
54
147
  }
55
148
 
56
- export function addMarkersStyleDiff(map: MapkaMap, next: StyleSpecification) {
149
+ export function addStyleDiffMarkers(map: MapkaMap, next: StyleSpecification) {
57
150
  const markers = get(next, "metadata.mapka.markers", []) as MapkaMarkerOptions[];
58
- addMarkersToMap(map, markers);
151
+ return addMarkers(map, markers);
59
152
  }
@@ -0,0 +1,155 @@
1
+ // biome-ignore lint/correctness/noUnusedImports: later fix
2
+ import { h } from "preact";
3
+ import { Popup } from "maplibre-gl";
4
+ import { PopupContent } from "../components/PopupContent.js";
5
+ import { render } from "preact";
6
+ import { remove } from "es-toolkit/array";
7
+ import type { MapkaPopupOptions } from "../types/marker.js";
8
+ import type { MapkaMap } from "../map.js";
9
+ import { isEqual } from "es-toolkit";
10
+
11
+ export function getPopupId(popup: { id?: string }) {
12
+ return popup.id ?? `popup-${crypto.randomUUID()}`;
13
+ }
14
+
15
+ export function getOnClose(map: MapkaMap, id: string) {
16
+ return () => map.closePopup(id);
17
+ }
18
+
19
+ export function enforceMaxPopups(map: MapkaMap) {
20
+ if (map.popups.length > map.maxPopups) {
21
+ const popupToRemove = map.popups.shift();
22
+ popupToRemove?.popup.remove();
23
+ popupToRemove?.container.remove();
24
+ }
25
+ }
26
+
27
+ export function openPopup(map: MapkaMap, options: MapkaPopupOptions, id: string) {
28
+ const { lngLat, content, closeButton, ...popupOptions } = options;
29
+ if (content instanceof HTMLElement) {
30
+ const popup = new Popup({
31
+ ...popupOptions,
32
+ closeButton: false,
33
+ closeOnClick: false,
34
+ })
35
+ .setLngLat(lngLat)
36
+ .setDOMContent(content)
37
+ .addTo(map);
38
+
39
+ map.popups.push({
40
+ container: content,
41
+ id,
42
+ options,
43
+ popup,
44
+ });
45
+ enforceMaxPopups(map);
46
+ return id;
47
+ } else if (typeof content === "object") {
48
+ const onClose = getOnClose(map, id);
49
+ const container = document.createElement("div");
50
+ container.classList.add("mapka-popup-container");
51
+
52
+ render(<PopupContent {...content} closeButton={closeButton} onClose={onClose} />, container);
53
+
54
+ const popup = new Popup({
55
+ ...popupOptions,
56
+ closeButton: false,
57
+ closeOnClick: false,
58
+ })
59
+ .setLngLat(lngLat)
60
+ .setDOMContent(container)
61
+ .addTo(map);
62
+
63
+ map.popups.push({
64
+ container,
65
+ id,
66
+ options,
67
+ popup,
68
+ });
69
+ enforceMaxPopups(map);
70
+ return id;
71
+ } else if (typeof content === "function") {
72
+ const newContent = content(id);
73
+ return openPopup(
74
+ map,
75
+ {
76
+ ...options,
77
+ content: newContent,
78
+ },
79
+ id,
80
+ );
81
+ }
82
+
83
+ throw new Error("Invalid popup content");
84
+ }
85
+
86
+ const DEFAULT_POPUP_MAX_WIDTH = "240px";
87
+
88
+ export function updatePopupBaseOptions(
89
+ popup: Popup,
90
+ options: MapkaPopupOptions,
91
+ newOptions: Omit<MapkaPopupOptions, "content">,
92
+ ) {
93
+ if (!isEqual(options.maxWidth, newOptions.maxWidth)) {
94
+ popup.setMaxWidth(newOptions.maxWidth ?? DEFAULT_POPUP_MAX_WIDTH);
95
+ }
96
+ if (!isEqual(options.offset, newOptions.offset)) {
97
+ popup.setOffset(newOptions.offset);
98
+ }
99
+ if (!isEqual(options.lngLat, newOptions.lngLat)) {
100
+ popup.setLngLat(newOptions.lngLat);
101
+ }
102
+ return popup;
103
+ }
104
+
105
+ export function updatePopup(
106
+ map: MapkaMap,
107
+ { content, ...newOptions }: MapkaPopupOptions,
108
+ id: string,
109
+ ) {
110
+ if (content instanceof HTMLElement) {
111
+ const mapkaPopups = map.popups.filter((popup) => popup.id === id);
112
+ for (const { popup, options } of mapkaPopups) {
113
+ updatePopupBaseOptions(popup, options, newOptions);
114
+ popup.setDOMContent(content);
115
+ }
116
+ } else if (typeof content === "object") {
117
+ const onClose = getOnClose(map, id);
118
+ const mapkaPopups = map.popups.filter((popup) => popup.id === id);
119
+
120
+ for (const { popup, container, options } of mapkaPopups) {
121
+ const { closeButton } = options;
122
+ render(<PopupContent {...content} closeButton={closeButton} onClose={onClose} />, container);
123
+ updatePopupBaseOptions(popup, options, newOptions);
124
+ popup.setDOMContent(container);
125
+ }
126
+ } else if (typeof content === "function") {
127
+ const newContent = content(id);
128
+ return updatePopup(
129
+ map,
130
+ {
131
+ ...newOptions,
132
+ content: newContent,
133
+ },
134
+ id,
135
+ );
136
+ }
137
+ }
138
+
139
+ export function closeOnMapClickPopups(map: MapkaMap) {
140
+ const popupsToCloseOnMapClick = remove(map.popups, (popup) =>
141
+ Boolean(popup.options.closeOnClick),
142
+ );
143
+ for (const popup of popupsToCloseOnMapClick) {
144
+ popup.popup.remove();
145
+ popup.container.remove();
146
+ }
147
+ }
148
+
149
+ export function closePopupsById(map: MapkaMap, id: string) {
150
+ const removedPopups = remove(map.popups, (popup) => popup.id === id);
151
+ for (const popup of removedPopups) {
152
+ popup.popup.remove();
153
+ popup.container.remove();
154
+ }
155
+ }
package/src/styles.css CHANGED
@@ -814,7 +814,7 @@ a.maplibregl-ctrl-logo.maplibregl-compact {
814
814
  font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
815
815
  }
816
816
 
817
- .mapka-tooltip-icon {
817
+ .mapka-popup-icon {
818
818
  display: block;
819
819
  fill: none;
820
820
  height: 16px;
@@ -823,12 +823,12 @@ a.maplibregl-ctrl-logo.maplibregl-compact {
823
823
  overflow: visible;
824
824
  }
825
825
 
826
- .mapka-tooltip-icon-sm {
826
+ .mapka-popup-icon-sm {
827
827
  height: 12px;
828
828
  width: 12px;
829
829
  }
830
830
 
831
- .mapka-tooltip-icon-star {
831
+ .mapka-popup-icon-star {
832
832
  height: 12px;
833
833
  width: 12px;
834
834
  fill: currentColor;
@@ -836,7 +836,7 @@ a.maplibregl-ctrl-logo.maplibregl-compact {
836
836
  }
837
837
 
838
838
  /* Carousel */
839
- .mapka-tooltip-carousel {
839
+ .mapka-popup-carousel {
840
840
  position: relative;
841
841
  width: 100%;
842
842
  height: 200px;
@@ -844,20 +844,20 @@ a.maplibregl-ctrl-logo.maplibregl-compact {
844
844
  border-radius: 12px 12px 0 0;
845
845
  }
846
846
 
847
- .mapka-tooltip-carousel-track {
847
+ .mapka-popup-carousel-track {
848
848
  display: flex;
849
849
  height: 100%;
850
850
  transition: transform 0.3s ease;
851
851
  }
852
852
 
853
- .mapka-tooltip-carousel-image {
853
+ .mapka-popup-carousel-image {
854
854
  min-width: 100%;
855
855
  height: 100%;
856
856
  object-fit: cover;
857
857
  flex-shrink: 0;
858
858
  }
859
859
 
860
- .mapka-tooltip-carousel-actions {
860
+ .mapka-popup-carousel-actions {
861
861
  position: absolute;
862
862
  top: 12px;
863
863
  right: 12px;
@@ -866,7 +866,7 @@ a.maplibregl-ctrl-logo.maplibregl-compact {
866
866
  z-index: 3;
867
867
  }
868
868
 
869
- .mapka-tooltip-action-btn {
869
+ .mapka-popup-action-btn {
870
870
  background: rgba(255, 255, 255, 0.95);
871
871
  border: none;
872
872
  border-radius: 50%;
@@ -881,12 +881,12 @@ a.maplibregl-ctrl-logo.maplibregl-compact {
881
881
  color: #222;
882
882
  }
883
883
 
884
- .mapka-tooltip-action-btn:hover {
884
+ .mapka-popup-action-btn:hover {
885
885
  background: #fff;
886
886
  transform: scale(1.05);
887
887
  }
888
888
 
889
- .mapka-tooltip-carousel-btn {
889
+ .mapka-popup-carousel-btn {
890
890
  position: absolute;
891
891
  top: 50%;
892
892
  transform: translateY(-50%);
@@ -905,20 +905,20 @@ a.maplibregl-ctrl-logo.maplibregl-compact {
905
905
  color: #222;
906
906
  }
907
907
 
908
- .mapka-tooltip-carousel-btn:hover {
908
+ .mapka-popup-carousel-btn:hover {
909
909
  background: #fff;
910
910
  transform: translateY(-50%) scale(1.05);
911
911
  }
912
912
 
913
- .mapka-tooltip-carousel-prev {
913
+ .mapka-popup-carousel-prev {
914
914
  left: 12px;
915
915
  }
916
916
 
917
- .mapka-tooltip-carousel-next {
917
+ .mapka-popup-carousel-next {
918
918
  right: 12px;
919
919
  }
920
920
 
921
- .mapka-tooltip-dots {
921
+ .mapka-popup-dots {
922
922
  position: absolute;
923
923
  bottom: 12px;
924
924
  left: 50%;
@@ -928,7 +928,7 @@ a.maplibregl-ctrl-logo.maplibregl-compact {
928
928
  z-index: 2;
929
929
  }
930
930
 
931
- .mapka-tooltip-dot {
931
+ .mapka-popup-dot {
932
932
  width: 6px;
933
933
  height: 6px;
934
934
  border-radius: 50%;
@@ -939,21 +939,21 @@ a.maplibregl-ctrl-logo.maplibregl-compact {
939
939
  transition: background 0.2s ease, transform 0.2s ease;
940
940
  }
941
941
 
942
- .mapka-tooltip-dot:hover {
942
+ .mapka-popup-dot:hover {
943
943
  background: rgba(255, 255, 255, 0.8);
944
944
  transform: scale(1.2);
945
945
  }
946
946
 
947
- .mapka-tooltip-dot-active {
947
+ .mapka-popup-dot-active {
948
948
  background: #fff;
949
949
  }
950
950
 
951
951
  /* Content */
952
- .mapka-tooltip-content {
952
+ .mapka-popup-content {
953
953
  padding: 12px 16px 16px;
954
954
  }
955
955
 
956
- .mapka-tooltip-header {
956
+ .mapka-popup-header {
957
957
  display: flex;
958
958
  justify-content: space-between;
959
959
  align-items: flex-start;
@@ -961,7 +961,7 @@ a.maplibregl-ctrl-logo.maplibregl-compact {
961
961
  margin-bottom: 4px;
962
962
  }
963
963
 
964
- .mapka-tooltip-title {
964
+ .mapka-popup-title {
965
965
  margin: 0;
966
966
  font-size: 15px;
967
967
  font-weight: 600;
@@ -970,24 +970,8 @@ a.maplibregl-ctrl-logo.maplibregl-compact {
970
970
  flex: 1;
971
971
  }
972
972
 
973
- .mapka-tooltip-rating {
974
- display: flex;
975
- align-items: center;
976
- gap: 4px;
977
- color: #222;
978
- font-size: 14px;
979
- flex-shrink: 0;
980
- }
981
-
982
- .mapka-tooltip-rating-value {
983
- font-weight: 600;
984
- }
985
-
986
- .mapka-tooltip-rating-count {
987
- color: #717171;
988
- }
989
973
 
990
- .mapka-tooltip-description {
974
+ .mapka-popup-description {
991
975
  margin: 0 0 4px 0;
992
976
  font-size: 14px;
993
977
  color: #717171;
@@ -997,32 +981,4 @@ a.maplibregl-ctrl-logo.maplibregl-compact {
997
981
  display: -webkit-box;
998
982
  -webkit-line-clamp: 2;
999
983
  -webkit-box-orient: vertical;
1000
- }
1001
-
1002
- .mapka-tooltip-subtitle {
1003
- margin: 0 0 8px 0;
1004
- font-size: 14px;
1005
- color: #717171;
1006
- line-height: 1.4;
1007
- }
1008
-
1009
- .mapka-tooltip-price {
1010
- display: flex;
1011
- align-items: baseline;
1012
- gap: 6px;
1013
- font-size: 14px;
1014
- }
1015
-
1016
- .mapka-tooltip-price-original {
1017
- color: #717171;
1018
- text-decoration: line-through;
1019
- }
1020
-
1021
- .mapka-tooltip-price-current {
1022
- font-weight: 600;
1023
- color: #222;
1024
- }
1025
-
1026
- .mapka-tooltip-price-suffix {
1027
- color: #717171;
1028
984
  }
@@ -0,0 +1 @@
1
+ export type WithId<T extends { id: string }> = T & { id: string };
@@ -1,29 +1,28 @@
1
- export interface MapkaTooltipRating {
2
- value: number;
3
- count: number;
4
- }
5
-
6
- export interface MapkaTooltipPrice {
7
- current: string;
8
- original?: string;
9
- suffix?: string;
10
- }
1
+ import type { MarkerOptions, PopupOptions } from "maplibre-gl";
11
2
 
12
- export interface MapkaTooltipOptions {
13
- id?: string;
14
- trigger?: "hover" | "click";
3
+ export interface MapkaPopupContent {
15
4
  title?: string;
16
- rating?: MapkaTooltipRating;
17
5
  description?: string;
18
- subtitle?: string;
19
- price?: MapkaTooltipPrice;
20
6
  imageUrls?: string[];
21
7
  onFavorite?: (id: string) => void;
22
8
  }
23
9
 
24
- export interface MapkaMarkerOptions {
25
- position: [number, number];
10
+ type CreatePopupElement = (id: string) => HTMLElement;
11
+ type CreatePopupContent = (id: string) => MapkaPopupContent;
12
+
13
+ export interface MapkaPopupOptions extends PopupOptions {
14
+ id?: string;
15
+ lngLat: [number, number];
16
+ trigger?: "hover" | "click" | "always";
17
+ content: MapkaPopupContent | HTMLElement | CreatePopupElement | CreatePopupContent;
18
+ }
19
+
20
+ export type MapkaMarkerPopupOptions = Omit<MapkaPopupOptions, "lngLat">;
21
+
22
+ export interface MapkaMarkerOptions extends MarkerOptions {
23
+ id?: string;
24
+ lngLat: [number, number];
26
25
  color?: string;
27
26
  icon?: string;
28
- tooltip?: MapkaTooltipOptions;
27
+ popup?: Omit<MapkaPopupOptions, "lngLat">;
29
28
  }
@@ -1,9 +0,0 @@
1
- /** biome-ignore-all lint/correctness/noUnusedImports: <explanation> */
2
- import { h } from "preact";
3
- import type { MapkaTooltipOptions } from "../types/marker.js";
4
- interface TooltipProps extends MapkaTooltipOptions {
5
- onClose?: () => void;
6
- }
7
- export declare function Tooltip({ id, title, rating, description, subtitle, price, imageUrls, onFavorite, onClose, }: TooltipProps): h.JSX.Element;
8
- export {};
9
- //# sourceMappingURL=Tooltip.d.ts.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"Tooltip.d.ts","sourceRoot":"","sources":["../../src/components/Tooltip.tsx"],"names":[],"mappings":"AAAA,uEAAuE;AACvE,OAAO,EAAE,CAAC,EAAY,MAAM,QAAQ,CAAC;AAErC,OAAO,KAAK,EAAE,mBAAmB,EAAE,MAAM,oBAAoB,CAAC;AAE9D,UAAU,YAAa,SAAQ,mBAAmB;IAChD,OAAO,CAAC,EAAE,MAAM,IAAI,CAAC;CACtB;AA2MD,wBAAgB,OAAO,CAAC,EACtB,EAAE,EACF,KAAK,EACL,MAAM,EACN,WAAW,EACX,QAAQ,EACR,KAAK,EACL,SAAS,EACT,UAAU,EACV,OAAO,GACR,EAAE,YAAY,iBA4Cd"}