@mapka/maplibre-gl-sdk 0.8.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
@@ -804,5 +804,181 @@ a.maplibregl-ctrl-logo.maplibregl-compact {
804
804
  position: fixed !important;
805
805
  top: 0 !important;
806
806
  width: 100% !important;
807
- z-index: 99999
807
+ }
808
+
809
+ /* Mapka Tooltip Styles */
810
+ .mapka-tooltip {
811
+ border-radius: 12px;
812
+ overflow: hidden;
813
+ background: #fff;
814
+ font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
815
+ }
816
+
817
+ .mapka-popup-icon {
818
+ display: block;
819
+ fill: none;
820
+ height: 16px;
821
+ width: 16px;
822
+ stroke: currentColor;
823
+ overflow: visible;
824
+ }
825
+
826
+ .mapka-popup-icon-sm {
827
+ height: 12px;
828
+ width: 12px;
829
+ }
830
+
831
+ .mapka-popup-icon-star {
832
+ height: 12px;
833
+ width: 12px;
834
+ fill: currentColor;
835
+ stroke: none;
836
+ }
837
+
838
+ /* Carousel */
839
+ .mapka-popup-carousel {
840
+ position: relative;
841
+ width: 100%;
842
+ height: 200px;
843
+ overflow: hidden;
844
+ border-radius: 12px 12px 0 0;
845
+ }
846
+
847
+ .mapka-popup-carousel-track {
848
+ display: flex;
849
+ height: 100%;
850
+ transition: transform 0.3s ease;
851
+ }
852
+
853
+ .mapka-popup-carousel-image {
854
+ min-width: 100%;
855
+ height: 100%;
856
+ object-fit: cover;
857
+ flex-shrink: 0;
858
+ }
859
+
860
+ .mapka-popup-carousel-actions {
861
+ position: absolute;
862
+ top: 12px;
863
+ right: 12px;
864
+ display: flex;
865
+ gap: 8px;
866
+ z-index: 3;
867
+ }
868
+
869
+ .mapka-popup-action-btn {
870
+ background: rgba(255, 255, 255, 0.95);
871
+ border: none;
872
+ border-radius: 50%;
873
+ width: 32px;
874
+ height: 32px;
875
+ cursor: pointer;
876
+ display: flex;
877
+ align-items: center;
878
+ justify-content: center;
879
+ box-shadow: 0 2px 4px rgba(0, 0, 0, 0.18);
880
+ transition: background 0.2s ease, transform 0.2s ease;
881
+ color: #222;
882
+ }
883
+
884
+ .mapka-popup-action-btn:hover {
885
+ background: #fff;
886
+ transform: scale(1.05);
887
+ }
888
+
889
+ .mapka-popup-carousel-btn {
890
+ position: absolute;
891
+ top: 50%;
892
+ transform: translateY(-50%);
893
+ background: rgba(255, 255, 255, 0.95);
894
+ border: none;
895
+ border-radius: 50%;
896
+ width: 28px;
897
+ height: 28px;
898
+ cursor: pointer;
899
+ display: flex;
900
+ align-items: center;
901
+ justify-content: center;
902
+ z-index: 2;
903
+ transition: background 0.2s ease, transform 0.2s ease;
904
+ box-shadow: 0 2px 4px rgba(0, 0, 0, 0.18);
905
+ color: #222;
906
+ }
907
+
908
+ .mapka-popup-carousel-btn:hover {
909
+ background: #fff;
910
+ transform: translateY(-50%) scale(1.05);
911
+ }
912
+
913
+ .mapka-popup-carousel-prev {
914
+ left: 12px;
915
+ }
916
+
917
+ .mapka-popup-carousel-next {
918
+ right: 12px;
919
+ }
920
+
921
+ .mapka-popup-dots {
922
+ position: absolute;
923
+ bottom: 12px;
924
+ left: 50%;
925
+ transform: translateX(-50%);
926
+ display: flex;
927
+ gap: 6px;
928
+ z-index: 2;
929
+ }
930
+
931
+ .mapka-popup-dot {
932
+ width: 6px;
933
+ height: 6px;
934
+ border-radius: 50%;
935
+ background: rgba(255, 255, 255, 0.6);
936
+ border: none;
937
+ cursor: pointer;
938
+ padding: 0;
939
+ transition: background 0.2s ease, transform 0.2s ease;
940
+ }
941
+
942
+ .mapka-popup-dot:hover {
943
+ background: rgba(255, 255, 255, 0.8);
944
+ transform: scale(1.2);
945
+ }
946
+
947
+ .mapka-popup-dot-active {
948
+ background: #fff;
949
+ }
950
+
951
+ /* Content */
952
+ .mapka-popup-content {
953
+ padding: 12px 16px 16px;
954
+ }
955
+
956
+ .mapka-popup-header {
957
+ display: flex;
958
+ justify-content: space-between;
959
+ align-items: flex-start;
960
+ gap: 8px;
961
+ margin-bottom: 4px;
962
+ }
963
+
964
+ .mapka-popup-title {
965
+ margin: 0;
966
+ font-size: 15px;
967
+ font-weight: 600;
968
+ color: #222;
969
+ line-height: 1.3;
970
+ flex: 1;
971
+ }
972
+
973
+
974
+ .mapka-popup-description {
975
+ margin: 0 0 4px 0;
976
+ font-size: 14px;
977
+ color: #717171;
978
+ line-height: 1.4;
979
+ overflow: hidden;
980
+ text-overflow: ellipsis;
981
+ display: -webkit-box;
982
+ -webkit-line-clamp: 2;
983
+ -webkit-box-orient: vertical;
808
984
  }
@@ -0,0 +1 @@
1
+ export type WithId<T extends { id: string }> = T & { id: string };
@@ -1,13 +1,28 @@
1
- export interface MapkaTooltipOptions {
2
- trigger?: "hover" | "click";
1
+ import type { MarkerOptions, PopupOptions } from "maplibre-gl";
2
+
3
+ export interface MapkaPopupContent {
3
4
  title?: string;
4
5
  description?: string;
5
6
  imageUrls?: string[];
7
+ onFavorite?: (id: string) => void;
6
8
  }
7
9
 
8
- export interface MapkaMarkerOptions {
9
- 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];
10
25
  color?: string;
11
26
  icon?: string;
12
- tooltip?: MapkaTooltipOptions;
27
+ popup?: Omit<MapkaPopupOptions, "lngLat">;
13
28
  }
@@ -1,15 +0,0 @@
1
- import * as maplibregl from "maplibre-gl";
2
- import type { MapkaTooltipOptions } from "../types/marker.js";
3
- /**
4
- * Shows a tooltip for a marker
5
- */
6
- export declare function showTooltip(marker: maplibregl.Marker, options: MapkaTooltipOptions, map: maplibregl.Map): void;
7
- /**
8
- * Hides the current tooltip
9
- */
10
- export declare function hideTooltip(): void;
11
- /**
12
- * Gets the current visible popup
13
- */
14
- export declare function getCurrentTooltip(): maplibregl.Popup | null;
15
- //# sourceMappingURL=tooltip.d.ts.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"tooltip.d.ts","sourceRoot":"","sources":["../../src/modules/tooltip.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,UAAU,MAAM,aAAa,CAAC;AAC1C,OAAO,KAAK,EAAE,mBAAmB,EAAE,MAAM,oBAAoB,CAAC;AAuS9D;;GAEG;AACH,wBAAgB,WAAW,CACzB,MAAM,EAAE,UAAU,CAAC,MAAM,EACzB,OAAO,EAAE,mBAAmB,EAC5B,GAAG,EAAE,UAAU,CAAC,GAAG,QAwBpB;AAED;;GAEG;AACH,wBAAgB,WAAW,SAK1B;AAED;;GAEG;AACH,wBAAgB,iBAAiB,IAAI,UAAU,CAAC,KAAK,GAAG,IAAI,CAE3D"}