@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.
@@ -0,0 +1,177 @@
1
+
2
+ /* Mapka Tooltip Styles */
3
+ .mapka-tooltip {
4
+ border-radius: 12px;
5
+ overflow: hidden;
6
+ background: #fff;
7
+ font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
8
+ }
9
+
10
+ .mapka-popup-icon {
11
+ display: block;
12
+ fill: none;
13
+ height: 16px;
14
+ width: 16px;
15
+ stroke: currentColor;
16
+ overflow: visible;
17
+ }
18
+
19
+ .mapka-popup-icon-sm {
20
+ height: 12px;
21
+ width: 12px;
22
+ }
23
+
24
+ .mapka-popup-icon-star {
25
+ height: 12px;
26
+ width: 12px;
27
+ fill: currentColor;
28
+ stroke: none;
29
+ }
30
+
31
+ /* Carousel */
32
+ .mapka-popup-carousel {
33
+ position: relative;
34
+ width: 100%;
35
+ height: 200px;
36
+ overflow: hidden;
37
+ border-radius: 12px 12px 0 0;
38
+ }
39
+
40
+ .mapka-popup-carousel-track {
41
+ display: flex;
42
+ height: 100%;
43
+ transition: transform 0.3s ease;
44
+ }
45
+
46
+ .mapka-popup-carousel-image {
47
+ min-width: 100%;
48
+ height: 100%;
49
+ object-fit: cover;
50
+ flex-shrink: 0;
51
+ }
52
+
53
+ .mapka-popup-carousel-actions {
54
+ position: absolute;
55
+ top: 12px;
56
+ right: 12px;
57
+ display: flex;
58
+ gap: 8px;
59
+ z-index: 3;
60
+ }
61
+
62
+ .mapka-popup-action-btn {
63
+ background: rgba(255, 255, 255, 0.95);
64
+ border: none;
65
+ border-radius: 50%;
66
+ width: 32px;
67
+ height: 32px;
68
+ cursor: pointer;
69
+ display: flex;
70
+ align-items: center;
71
+ justify-content: center;
72
+ box-shadow: 0 2px 4px rgba(0, 0, 0, 0.18);
73
+ transition: background 0.2s ease, transform 0.2s ease;
74
+ color: #222;
75
+ }
76
+
77
+ .mapka-popup-action-btn:hover {
78
+ background: #fff;
79
+ transform: scale(1.05);
80
+ }
81
+
82
+ .mapka-popup-carousel-btn {
83
+ position: absolute;
84
+ top: 50%;
85
+ transform: translateY(-50%);
86
+ background: rgba(255, 255, 255, 0.95);
87
+ border: none;
88
+ border-radius: 50%;
89
+ width: 28px;
90
+ height: 28px;
91
+ cursor: pointer;
92
+ display: flex;
93
+ align-items: center;
94
+ justify-content: center;
95
+ z-index: 2;
96
+ transition: background 0.2s ease, transform 0.2s ease;
97
+ box-shadow: 0 2px 4px rgba(0, 0, 0, 0.18);
98
+ color: #222;
99
+ }
100
+
101
+ .mapka-popup-carousel-btn:hover {
102
+ background: #fff;
103
+ transform: translateY(-50%) scale(1.05);
104
+ }
105
+
106
+ .mapka-popup-carousel-prev {
107
+ left: 12px;
108
+ }
109
+
110
+ .mapka-popup-carousel-next {
111
+ right: 12px;
112
+ }
113
+
114
+ .mapka-popup-dots {
115
+ position: absolute;
116
+ bottom: 12px;
117
+ left: 50%;
118
+ transform: translateX(-50%);
119
+ display: flex;
120
+ gap: 6px;
121
+ z-index: 2;
122
+ }
123
+
124
+ .mapka-popup-dot {
125
+ width: 6px;
126
+ height: 6px;
127
+ border-radius: 50%;
128
+ background: rgba(255, 255, 255, 0.6);
129
+ border: none;
130
+ cursor: pointer;
131
+ padding: 0;
132
+ transition: background 0.2s ease, transform 0.2s ease;
133
+ }
134
+
135
+ .mapka-popup-dot:hover {
136
+ background: rgba(255, 255, 255, 0.8);
137
+ transform: scale(1.2);
138
+ }
139
+
140
+ .mapka-popup-dot-active {
141
+ background: #fff;
142
+ }
143
+
144
+ /* Content */
145
+ .mapka-popup-content {
146
+ padding: 12px 16px 16px;
147
+ }
148
+
149
+ .mapka-popup-header {
150
+ display: flex;
151
+ justify-content: space-between;
152
+ align-items: flex-start;
153
+ gap: 8px;
154
+ margin-bottom: 4px;
155
+ }
156
+
157
+ .mapka-popup-title {
158
+ margin: 0;
159
+ font-size: 15px;
160
+ font-weight: 600;
161
+ color: #222;
162
+ line-height: 1.3;
163
+ flex: 1;
164
+ }
165
+
166
+
167
+ .mapka-popup-description {
168
+ margin: 0 0 4px 0;
169
+ font-size: 14px;
170
+ color: #717171;
171
+ line-height: 1.4;
172
+ overflow: hidden;
173
+ text-overflow: ellipsis;
174
+ display: -webkit-box;
175
+ -webkit-line-clamp: 2;
176
+ -webkit-box-orient: vertical;
177
+ }
@@ -0,0 +1,227 @@
1
+ /** biome-ignore-all lint/correctness/noUnusedImports: <explanation> */
2
+ import { h, Fragment } from "preact";
3
+ import { useState } from "preact/hooks";
4
+ import type { MapkaPopupContent } from "../types/marker.js";
5
+
6
+ interface PopupProps extends MapkaPopupContent {
7
+ onClose?: () => void;
8
+ closeButton?: boolean;
9
+ }
10
+
11
+ function HeartIcon({ filled }: { filled?: boolean }) {
12
+ return (
13
+ <svg
14
+ viewBox="0 0 32 32"
15
+ xmlns="http://www.w3.org/2000/svg"
16
+ aria-hidden="true"
17
+ focusable="false"
18
+ class="mapka-popup-icon"
19
+ >
20
+ <path
21
+ d="M16 28c7-4.73 14-10 14-17a6.98 6.98 0 0 0-7-7c-1.8 0-3.58.68-4.95 2.05L16 8.1l-2.05-2.05a6.98 6.98 0 0 0-9.9 0A6.98 6.98 0 0 0 2 11c0 7 7 12.27 14 17z"
22
+ fill={filled ? "currentColor" : "none"}
23
+ stroke="currentColor"
24
+ stroke-width="2"
25
+ />
26
+ </svg>
27
+ );
28
+ }
29
+
30
+ function CloseIcon() {
31
+ return (
32
+ <svg
33
+ viewBox="0 0 32 32"
34
+ xmlns="http://www.w3.org/2000/svg"
35
+ aria-hidden="true"
36
+ focusable="false"
37
+ class="mapka-popup-icon"
38
+ >
39
+ <path d="m6 6 20 20M26 6 6 26" fill="none" stroke="currentColor" stroke-width="3" />
40
+ </svg>
41
+ );
42
+ }
43
+
44
+ function ChevronLeftIcon() {
45
+ return (
46
+ <svg
47
+ viewBox="0 0 32 32"
48
+ xmlns="http://www.w3.org/2000/svg"
49
+ aria-hidden="true"
50
+ focusable="false"
51
+ class="mapka-popup-icon mapka-popup-icon-sm"
52
+ >
53
+ <path
54
+ d="M20 28 8.7 16.7a1 1 0 0 1 0-1.4L20 4"
55
+ fill="none"
56
+ stroke="currentColor"
57
+ stroke-width="4"
58
+ />
59
+ </svg>
60
+ );
61
+ }
62
+
63
+ function ChevronRightIcon() {
64
+ return (
65
+ <svg
66
+ viewBox="0 0 32 32"
67
+ xmlns="http://www.w3.org/2000/svg"
68
+ aria-hidden="true"
69
+ focusable="false"
70
+ class="mapka-popup-icon mapka-popup-icon-sm"
71
+ >
72
+ <path
73
+ d="m12 4 11.3 11.3a1 1 0 0 1 0 1.4L12 28"
74
+ fill="none"
75
+ stroke="currentColor"
76
+ stroke-width="4"
77
+ />
78
+ </svg>
79
+ );
80
+ }
81
+
82
+ function ImageCarousel({
83
+ imageUrls,
84
+ title,
85
+ onFavorite,
86
+ id,
87
+ closeButton,
88
+ onClose,
89
+ }: {
90
+ imageUrls: string[];
91
+ title?: string;
92
+ closeButton?: boolean;
93
+ onFavorite?: (id: string) => void;
94
+ id?: string;
95
+ onClose?: () => void;
96
+ }) {
97
+ const [currentIndex, setCurrentIndex] = useState(0);
98
+
99
+ const handlePrev = (e: Event) => {
100
+ e.stopPropagation();
101
+ setCurrentIndex((prev) => (prev === 0 ? imageUrls.length - 1 : prev - 1));
102
+ };
103
+
104
+ const handleNext = (e: Event) => {
105
+ e.stopPropagation();
106
+ setCurrentIndex((prev) => (prev + 1) % imageUrls.length);
107
+ };
108
+
109
+ const handleFavoriteClick = (e: Event) => {
110
+ e.stopPropagation();
111
+ if (onFavorite && id) {
112
+ onFavorite(id);
113
+ }
114
+ };
115
+
116
+ const handleCloseClick = (e: Event) => {
117
+ e.stopPropagation();
118
+ onClose?.();
119
+ };
120
+
121
+ return (
122
+ <div class="mapka-popup-carousel">
123
+ <div
124
+ class="mapka-popup-carousel-track"
125
+ style={{ transform: `translateX(-${currentIndex * 100}%)` }}
126
+ >
127
+ {imageUrls.map((url, index) => (
128
+ <img
129
+ key={index}
130
+ src={url}
131
+ alt={title || `Image ${index + 1}`}
132
+ class="mapka-popup-carousel-image"
133
+ />
134
+ ))}
135
+ </div>
136
+
137
+ <div class="mapka-popup-carousel-actions">
138
+ {onFavorite && (
139
+ <button
140
+ type="button"
141
+ class="mapka-popup-action-btn"
142
+ onClick={handleFavoriteClick}
143
+ aria-label="Add to favorites"
144
+ >
145
+ <HeartIcon />
146
+ </button>
147
+ )}
148
+ {closeButton && (
149
+ <button
150
+ type="button"
151
+ class="mapka-popup-action-btn"
152
+ onClick={handleCloseClick}
153
+ aria-label="Close"
154
+ >
155
+ <CloseIcon />
156
+ </button>
157
+ )}
158
+ </div>
159
+
160
+ {imageUrls.length > 1 && (
161
+ <Fragment>
162
+ <button
163
+ type="button"
164
+ class="mapka-popup-carousel-btn mapka-popup-carousel-prev"
165
+ onClick={handlePrev}
166
+ aria-label="Previous image"
167
+ >
168
+ <ChevronLeftIcon />
169
+ </button>
170
+ <button
171
+ type="button"
172
+ class="mapka-popup-carousel-btn mapka-popup-carousel-next"
173
+ onClick={handleNext}
174
+ aria-label="Next image"
175
+ >
176
+ <ChevronRightIcon />
177
+ </button>
178
+
179
+ <div class="mapka-popup-dots">
180
+ {imageUrls.map((_, index) => (
181
+ <button
182
+ key={index}
183
+ type="button"
184
+ class={`mapka-popup-dot ${index === currentIndex ? "mapka-popup-dot-active" : ""}`}
185
+ onClick={(e) => {
186
+ e.stopPropagation();
187
+ setCurrentIndex(index);
188
+ }}
189
+ aria-label={`Go to image ${index + 1}`}
190
+ />
191
+ ))}
192
+ </div>
193
+ </Fragment>
194
+ )}
195
+ </div>
196
+ );
197
+ }
198
+
199
+ export function PopupContent({
200
+ title,
201
+ description,
202
+ closeButton,
203
+ imageUrls,
204
+ onFavorite,
205
+ onClose,
206
+ }: PopupProps) {
207
+ const hasImages = imageUrls && imageUrls.length > 0;
208
+
209
+ return (
210
+ <div class="mapka-tooltip">
211
+ {hasImages && (
212
+ <ImageCarousel
213
+ imageUrls={imageUrls}
214
+ title={title}
215
+ closeButton={closeButton}
216
+ onFavorite={onFavorite}
217
+ onClose={onClose}
218
+ />
219
+ )}
220
+
221
+ <div class="mapka-popup-content">
222
+ {title && <h3 class="mapka-popup-title">{title}</h3>}
223
+ {description && <p class="mapka-popup-description">{description}</p>}
224
+ </div>
225
+ </div>
226
+ );
227
+ }
package/src/map.ts CHANGED
@@ -1,21 +1,40 @@
1
1
  import * as maplibregl from "maplibre-gl";
2
2
  import { loadLayersIcons } from "./modules/icons.js";
3
- import { addMarkers, addMarkersStyleDiff } from "./modules/markers.js";
3
+ import {
4
+ closeOnMapClickPopups,
5
+ closePopupsById,
6
+ getPopupId,
7
+ openPopup,
8
+ updatePopup,
9
+ } from "./modules/popup.js";
10
+ import {
11
+ addMarkers,
12
+ addStyleDiffMarkers,
13
+ addStyleMarkers,
14
+ clearMarkers,
15
+ updateMarkers,
16
+ } from "./modules/markers.js";
4
17
  import type {
18
+ Marker,
19
+ Popup,
5
20
  RequestTransformFunction,
6
21
  MapStyleImageMissingEvent,
7
22
  MapOptions,
23
+ TransformStyleFunction,
8
24
  StyleSwapOptions,
9
25
  StyleOptions,
10
26
  StyleSpecification,
11
27
  } from "maplibre-gl";
28
+ import type { MapkaMarkerOptions, MapkaPopupOptions } from "./types/marker.js";
12
29
 
13
30
  export interface MapkaMapOptions extends MapOptions {
31
+ maxPopups?: number;
32
+
14
33
  apiKey: string;
15
34
  env?: "dev" | "prod" | "local";
16
35
  }
17
36
 
18
- const noopTransformRequest: maplibregl.RequestTransformFunction = (url) => {
37
+ const noopTransformRequest: RequestTransformFunction = (url) => {
19
38
  return {
20
39
  url,
21
40
  };
@@ -35,26 +54,49 @@ const createTransformRequest =
35
54
  return transformRequest ? transformRequest(url) : noopTransformRequest(url);
36
55
  };
37
56
 
38
- const noopTransformStyle: maplibregl.TransformStyleFunction = (_, next) => {
57
+ const noopTransformStyle: TransformStyleFunction = (_, next) => {
39
58
  return next;
40
59
  };
41
60
 
61
+ export type MapMapkaPopup = {
62
+ id: string;
63
+ container: HTMLElement;
64
+ options: MapkaPopupOptions;
65
+ popup: Popup;
66
+ };
67
+
68
+ export type MapMapkaMarker = {
69
+ id: string;
70
+ options: MapkaMarkerOptions;
71
+ marker: Marker;
72
+ };
73
+
42
74
  export class MapkaMap extends maplibregl.Map {
43
75
  static env: string = "prod";
44
76
 
45
- public constructor({ transformRequest, apiKey, ...options }: MapkaMapOptions) {
77
+ public markers: MapMapkaMarker[] = [];
78
+
79
+ public maxPopups: number = 1;
80
+ public popups: MapMapkaPopup[] = [];
81
+
82
+ public constructor({ transformRequest, apiKey, maxPopups = 1, ...options }: MapkaMapOptions) {
46
83
  super({
47
84
  ...options,
48
85
  transformRequest: createTransformRequest(apiKey, transformRequest),
49
86
  });
50
87
 
51
- super.on("styledata", (_: maplibregl.MapStyleDataEvent) => {
52
- addMarkers(this);
53
- });
88
+ this.maxPopups = maxPopups;
54
89
 
55
90
  super.on("styleimagemissing", (event: MapStyleImageMissingEvent) => {
56
91
  loadLayersIcons(this, event);
57
92
  });
93
+ super.on("click", () => {
94
+ closeOnMapClickPopups(this);
95
+ });
96
+
97
+ super.on("style.load", () => {
98
+ addStyleMarkers(this);
99
+ });
58
100
  }
59
101
 
60
102
  public setStyle(
@@ -66,11 +108,35 @@ export class MapkaMap extends maplibregl.Map {
66
108
  super.setStyle(style, {
67
109
  ...rest,
68
110
  transformStyle: (prev, next) => {
69
- addMarkersStyleDiff(this, next);
111
+ addStyleDiffMarkers(this, next);
70
112
  return transformStyle(prev, next);
71
113
  },
72
114
  });
73
115
 
74
116
  return this;
75
117
  }
118
+
119
+ public addMarkers(markers: MapkaMarkerOptions[]) {
120
+ addMarkers(this, markers);
121
+ }
122
+
123
+ public updateMarkers(markers: MapkaMarkerOptions[]) {
124
+ updateMarkers(this, markers);
125
+ }
126
+
127
+ public clearMarkers() {
128
+ clearMarkers(this);
129
+ }
130
+
131
+ public openPopup(popup: MapkaPopupOptions, id: string = getPopupId(popup)) {
132
+ return openPopup(this, popup, id);
133
+ }
134
+
135
+ public closePopup(id: string) {
136
+ closePopupsById(this, id);
137
+ }
138
+
139
+ public updatePopup(popup: MapkaPopupOptions, id: string = getPopupId(popup)) {
140
+ return updatePopup(this, popup, id);
141
+ }
76
142
  }