@mapslibvn/web 0.4.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.
- package/LICENSE +21 -0
- package/README.md +40 -0
- package/THIRD_PARTY_NOTICES.md +393 -0
- package/dist/index.d.ts +91 -0
- package/dist/index.js +367 -0
- package/dist/maplibre-gl-shared.mjs +6 -0
- package/dist/maplibre-gl-worker.mjs +6 -0
- package/dist/mapslibvn.css +1 -0
- package/dist/mapslibvn.umd.js +839 -0
- package/dist/mapslibvn.umd.js.map +1 -0
- package/package.json +43 -0
package/dist/index.js
ADDED
|
@@ -0,0 +1,367 @@
|
|
|
1
|
+
// src/map.ts
|
|
2
|
+
import {
|
|
3
|
+
DEFAULT_POI_SOURCES,
|
|
4
|
+
POI_SOURCE_PROFILES,
|
|
5
|
+
attributionHtml,
|
|
6
|
+
createClient,
|
|
7
|
+
isPoiStyleLayer,
|
|
8
|
+
profileForSources
|
|
9
|
+
} from "@mapslibvn/core";
|
|
10
|
+
|
|
11
|
+
// src/language.ts
|
|
12
|
+
import { isNameLabelLayer, nameExpression } from "@mapslibvn/core";
|
|
13
|
+
function applyLanguage(gl, lang) {
|
|
14
|
+
for (const l of gl.getStyle()?.layers ?? []) {
|
|
15
|
+
if (isNameLabelLayer(l)) gl.setLayoutProperty(l.id, "text-field", nameExpression(lang));
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
// src/protocol.ts
|
|
20
|
+
import { Protocol } from "pmtiles";
|
|
21
|
+
var registered = false;
|
|
22
|
+
function ensurePmtilesProtocol(host) {
|
|
23
|
+
if (registered) return;
|
|
24
|
+
host.addProtocol("pmtiles", new Protocol().tile);
|
|
25
|
+
registered = true;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
// src/map.ts
|
|
29
|
+
var isTheme = (s) => s === "light" || s === "dark";
|
|
30
|
+
function createMap(opts, deps) {
|
|
31
|
+
const ml = deps?.maplibre ?? globalThis.maplibregl;
|
|
32
|
+
if (!ml)
|
|
33
|
+
throw new Error("C\u1EA7n maplibre-gl: import maplibre-gl ho\u1EB7c d\xF9ng b\u1EA3n UMD @mapslibvn/web/umd");
|
|
34
|
+
ensurePmtilesProtocol(ml);
|
|
35
|
+
const poiSources = opts.poiSources ?? DEFAULT_POI_SOURCES;
|
|
36
|
+
if (!profileForSources(poiSources)) {
|
|
37
|
+
const available = Object.values(POI_SOURCE_PROFILES).map((list) => list.join(",")).join(" | ");
|
|
38
|
+
throw new Error(
|
|
39
|
+
`poiSources "${poiSources.join(",")}" ch\u01B0a c\xF3 b\u1ED9 tiles; hi\u1EC7n h\u1ED7 tr\u1EE3: ${available}`
|
|
40
|
+
);
|
|
41
|
+
}
|
|
42
|
+
const places = createClient({ apiKey: opts.apiKey, baseUrl: opts.apiBase, poiSources });
|
|
43
|
+
const styleOpt = opts.style ?? "light";
|
|
44
|
+
const style = isTheme(styleOpt) ? places.styleUrl(styleOpt) : styleOpt;
|
|
45
|
+
const gl = new ml.Map({
|
|
46
|
+
container: opts.container,
|
|
47
|
+
style,
|
|
48
|
+
center: opts.center ?? [106.7, 10.776],
|
|
49
|
+
zoom: opts.zoom ?? 12,
|
|
50
|
+
attributionControl: false
|
|
51
|
+
});
|
|
52
|
+
gl.addControl(
|
|
53
|
+
new ml.AttributionControl({
|
|
54
|
+
compact: opts.compactAttribution ?? false,
|
|
55
|
+
customAttribution: attributionHtml()
|
|
56
|
+
})
|
|
57
|
+
);
|
|
58
|
+
const listeners = {
|
|
59
|
+
poiClick: /* @__PURE__ */ new Set(),
|
|
60
|
+
load: /* @__PURE__ */ new Set()
|
|
61
|
+
};
|
|
62
|
+
const emit = (k, e) => {
|
|
63
|
+
for (const fn of listeners[k]) fn(e);
|
|
64
|
+
};
|
|
65
|
+
gl.on("load", () => {
|
|
66
|
+
if (opts.lang && opts.lang !== "vi") applyLanguage(gl, opts.lang);
|
|
67
|
+
if (opts.poiLayer === false) {
|
|
68
|
+
for (const layer of gl.getStyle().layers ?? []) {
|
|
69
|
+
if (isPoiStyleLayer(layer) && gl.getLayer(layer.id)) {
|
|
70
|
+
gl.setLayoutProperty(layer.id, "visibility", "none");
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
emit("load", void 0);
|
|
75
|
+
});
|
|
76
|
+
gl.on("click", (e) => {
|
|
77
|
+
if (listeners.poiClick.size === 0 || !gl.getLayer("poi")) return;
|
|
78
|
+
const f = gl.queryRenderedFeatures(e.point, { layers: ["poi"] })[0];
|
|
79
|
+
if (!f || f.geometry.type !== "Point") return;
|
|
80
|
+
const p = f.properties;
|
|
81
|
+
emit("poiClick", {
|
|
82
|
+
id: String(p.id),
|
|
83
|
+
name: String(p.name ?? ""),
|
|
84
|
+
category: String(p.cat ?? ""),
|
|
85
|
+
group: String(p.grp ?? ""),
|
|
86
|
+
lngLat: f.geometry.coordinates
|
|
87
|
+
});
|
|
88
|
+
});
|
|
89
|
+
return {
|
|
90
|
+
gl,
|
|
91
|
+
places,
|
|
92
|
+
addMarker(o) {
|
|
93
|
+
const marker = new ml.Marker(o.color ? { color: o.color } : void 0).setLngLat([
|
|
94
|
+
o.lng,
|
|
95
|
+
o.lat
|
|
96
|
+
]);
|
|
97
|
+
if (o.popupHtml) marker.setPopup(new ml.Popup({ offset: 24 }).setHTML(o.popupHtml));
|
|
98
|
+
return marker.addTo(gl);
|
|
99
|
+
},
|
|
100
|
+
fitBounds(bbox, padding = 40) {
|
|
101
|
+
gl.fitBounds(
|
|
102
|
+
[
|
|
103
|
+
[bbox[0], bbox[1]],
|
|
104
|
+
[bbox[2], bbox[3]]
|
|
105
|
+
],
|
|
106
|
+
{ padding }
|
|
107
|
+
);
|
|
108
|
+
},
|
|
109
|
+
flyTo(center, zoom) {
|
|
110
|
+
gl.flyTo(zoom === void 0 ? { center } : { center, zoom });
|
|
111
|
+
},
|
|
112
|
+
on(event, handler) {
|
|
113
|
+
listeners[event].add(handler);
|
|
114
|
+
},
|
|
115
|
+
off(event, handler) {
|
|
116
|
+
listeners[event].delete(handler);
|
|
117
|
+
},
|
|
118
|
+
remove() {
|
|
119
|
+
gl.remove();
|
|
120
|
+
}
|
|
121
|
+
};
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
// src/autocomplete-element.ts
|
|
125
|
+
import {
|
|
126
|
+
createClient as createClient2,
|
|
127
|
+
parsePoiSourcesCsv
|
|
128
|
+
} from "@mapslibvn/core";
|
|
129
|
+
var STYLE = `
|
|
130
|
+
:host { display: block; position: relative; color: #172033; font: 14px system-ui, sans-serif; }
|
|
131
|
+
input { width: 100%; box-sizing: border-box; padding: 10px 12px; border: 1px solid #9ca8ba;
|
|
132
|
+
border-radius: 8px; color: inherit; background: #fff; font: inherit; outline: none; }
|
|
133
|
+
input:focus { border-color: #2458a6; box-shadow: 0 0 0 3px rgba(36,88,166,.18); }
|
|
134
|
+
ul { position: absolute; left: 0; right: 0; margin: 4px 0 0; padding: 4px; list-style: none;
|
|
135
|
+
background: #fff; border: 1px solid #c7cfda; border-radius: 8px; z-index: 30;
|
|
136
|
+
max-height: 280px; overflow-y: auto; box-shadow: 0 8px 24px rgba(23,32,51,.16); }
|
|
137
|
+
li { padding: 8px 10px; border-radius: 5px; cursor: pointer; }
|
|
138
|
+
li:hover, li[aria-selected="true"] { background: #e9f1fc; }
|
|
139
|
+
.secondary { color: #667085; font-size: 12px; display: block; margin-top: 2px; }
|
|
140
|
+
.icon { display: inline-block; width: 1.2em; color: #667085; font-size: 12px; }
|
|
141
|
+
li[data-type="area"] .icon { color: #2458a6; }
|
|
142
|
+
.status { position: absolute; width: 1px; height: 1px; padding: 0; margin: -1px;
|
|
143
|
+
overflow: hidden; clip: rect(0, 0, 0, 0); white-space: nowrap; border: 0; }
|
|
144
|
+
`;
|
|
145
|
+
var instanceId = 0;
|
|
146
|
+
var TYPE_ICON = {
|
|
147
|
+
poi: "\u25CF",
|
|
148
|
+
street: "\u2500",
|
|
149
|
+
address: "\u2302",
|
|
150
|
+
area: "\u25A3"
|
|
151
|
+
};
|
|
152
|
+
var MapsLibVNAutocomplete = class extends HTMLElement {
|
|
153
|
+
static observedAttributes = ["api-key", "api-base", "placeholder", "near", "sources"];
|
|
154
|
+
#map = null;
|
|
155
|
+
/** Gán map trả về từ createMap để dùng cùng Places client và tâm bản đồ. */
|
|
156
|
+
get map() {
|
|
157
|
+
return this.#map;
|
|
158
|
+
}
|
|
159
|
+
set map(value) {
|
|
160
|
+
if (value === this.#map) return;
|
|
161
|
+
this.#map = value;
|
|
162
|
+
clearTimeout(this.#timer);
|
|
163
|
+
this.#seq++;
|
|
164
|
+
this.#render([]);
|
|
165
|
+
}
|
|
166
|
+
#client = null;
|
|
167
|
+
#input = null;
|
|
168
|
+
#list = null;
|
|
169
|
+
#status = null;
|
|
170
|
+
#items = [];
|
|
171
|
+
#timer;
|
|
172
|
+
#seq = 0;
|
|
173
|
+
#activeIndex = -1;
|
|
174
|
+
#listId = `mapslibvn-autocomplete-${++instanceId}`;
|
|
175
|
+
connectedCallback() {
|
|
176
|
+
if (this.#input) return;
|
|
177
|
+
const root = this.shadowRoot ?? this.attachShadow({ mode: "open" });
|
|
178
|
+
root.innerHTML = `<style>${STYLE}</style><input type="search" role="combobox" autocomplete="off" aria-autocomplete="list" aria-expanded="false" aria-controls="${this.#listId}" /><ul id="${this.#listId}" role="listbox" hidden></ul><div class="status" role="status" aria-live="polite"></div>`;
|
|
179
|
+
this.#input = root.querySelector("input");
|
|
180
|
+
this.#list = root.querySelector("ul");
|
|
181
|
+
this.#status = root.querySelector(".status");
|
|
182
|
+
if (!this.#input || !this.#list || !this.#status) return;
|
|
183
|
+
this.#input.placeholder = this.getAttribute("placeholder") ?? "T\xECm \u0111\u1ECBa \u0111i\u1EC3m\u2026";
|
|
184
|
+
this.#input.addEventListener("input", this.#onInput);
|
|
185
|
+
this.#input.addEventListener("keydown", this.#onKeydown);
|
|
186
|
+
this.#input.addEventListener("blur", this.#onBlur);
|
|
187
|
+
this.#list.addEventListener("pointerdown", this.#onPointerDown);
|
|
188
|
+
}
|
|
189
|
+
disconnectedCallback() {
|
|
190
|
+
clearTimeout(this.#timer);
|
|
191
|
+
this.#seq++;
|
|
192
|
+
this.#input?.removeEventListener("input", this.#onInput);
|
|
193
|
+
this.#input?.removeEventListener("keydown", this.#onKeydown);
|
|
194
|
+
this.#input?.removeEventListener("blur", this.#onBlur);
|
|
195
|
+
this.#list?.removeEventListener("pointerdown", this.#onPointerDown);
|
|
196
|
+
this.#input = null;
|
|
197
|
+
this.#list = null;
|
|
198
|
+
this.#status = null;
|
|
199
|
+
}
|
|
200
|
+
attributeChangedCallback(name) {
|
|
201
|
+
if (name === "api-key" || name === "api-base" || name === "sources") {
|
|
202
|
+
this.#client = null;
|
|
203
|
+
this.#seq++;
|
|
204
|
+
}
|
|
205
|
+
if (name === "placeholder" && this.#input)
|
|
206
|
+
this.#input.placeholder = this.getAttribute("placeholder") ?? "T\xECm \u0111\u1ECBa \u0111i\u1EC3m\u2026";
|
|
207
|
+
}
|
|
208
|
+
#onInput = () => {
|
|
209
|
+
clearTimeout(this.#timer);
|
|
210
|
+
this.#seq++;
|
|
211
|
+
const value = this.#input?.value ?? "";
|
|
212
|
+
if (value.trim().length < 2) {
|
|
213
|
+
this.#seq++;
|
|
214
|
+
this.#render([]);
|
|
215
|
+
this.#announce("");
|
|
216
|
+
return;
|
|
217
|
+
}
|
|
218
|
+
this.#announce("\u0110ang t\xECm\u2026");
|
|
219
|
+
this.#timer = setTimeout(() => void this.#query(value), 200);
|
|
220
|
+
};
|
|
221
|
+
#onBlur = () => {
|
|
222
|
+
this.#seq++;
|
|
223
|
+
this.#timer = setTimeout(() => this.#render([]), 150);
|
|
224
|
+
};
|
|
225
|
+
#onKeydown = (event) => {
|
|
226
|
+
if (event.key === "Escape") {
|
|
227
|
+
clearTimeout(this.#timer);
|
|
228
|
+
this.#seq++;
|
|
229
|
+
this.#render([]);
|
|
230
|
+
return;
|
|
231
|
+
}
|
|
232
|
+
if (this.#items.length === 0) return;
|
|
233
|
+
if (event.key === "ArrowDown" || event.key === "ArrowUp") {
|
|
234
|
+
event.preventDefault();
|
|
235
|
+
const delta = event.key === "ArrowDown" ? 1 : -1;
|
|
236
|
+
const start = this.#activeIndex < 0 ? delta > 0 ? -1 : 0 : this.#activeIndex;
|
|
237
|
+
this.#setActive((start + delta + this.#items.length) % this.#items.length);
|
|
238
|
+
} else if (event.key === "Enter" && this.#activeIndex >= 0) {
|
|
239
|
+
event.preventDefault();
|
|
240
|
+
this.#select(this.#activeIndex);
|
|
241
|
+
}
|
|
242
|
+
};
|
|
243
|
+
#onPointerDown = (event) => {
|
|
244
|
+
event.preventDefault();
|
|
245
|
+
const option = event.target.closest('[role="option"]');
|
|
246
|
+
if (option) this.#select(Number(option.dataset.index));
|
|
247
|
+
};
|
|
248
|
+
#getClient() {
|
|
249
|
+
if (this.#map?.places) return this.#map.places;
|
|
250
|
+
if (this.#client) return this.#client;
|
|
251
|
+
const apiKey = this.getAttribute("api-key");
|
|
252
|
+
const baseUrl = this.getAttribute("api-base");
|
|
253
|
+
if (!apiKey || !baseUrl) return null;
|
|
254
|
+
const rawSources = this.getAttribute("sources");
|
|
255
|
+
const poiSources = rawSources === null ? null : parsePoiSourcesCsv(rawSources);
|
|
256
|
+
if (rawSources !== null && !poiSources) {
|
|
257
|
+
console.warn(`<mapslibvn-autocomplete sources="${rawSources}"> kh\xF4ng h\u1EE3p l\u1EC7 \u2014 d\xF9ng m\u1EB7c \u0111\u1ECBnh`);
|
|
258
|
+
}
|
|
259
|
+
this.#client = createClient2({
|
|
260
|
+
apiKey,
|
|
261
|
+
baseUrl,
|
|
262
|
+
...poiSources ? { poiSources } : {}
|
|
263
|
+
});
|
|
264
|
+
return this.#client;
|
|
265
|
+
}
|
|
266
|
+
#near() {
|
|
267
|
+
if (this.#map) {
|
|
268
|
+
const center = this.#map.gl.getCenter();
|
|
269
|
+
return [center.lat, center.lng];
|
|
270
|
+
}
|
|
271
|
+
const parts = this.getAttribute("near")?.split(",").map(Number);
|
|
272
|
+
const lat = parts?.[0];
|
|
273
|
+
const lng = parts?.[1];
|
|
274
|
+
return lat !== void 0 && lng !== void 0 && Number.isFinite(lat) && Number.isFinite(lng) ? [lat, lng] : void 0;
|
|
275
|
+
}
|
|
276
|
+
async #query(raw) {
|
|
277
|
+
const query = raw.trim();
|
|
278
|
+
const client = this.#getClient();
|
|
279
|
+
if (!client || query.length < 2) {
|
|
280
|
+
this.#render([]);
|
|
281
|
+
this.#announce(client ? "" : "Thi\u1EBFu c\u1EA5u h\xECnh API.");
|
|
282
|
+
return;
|
|
283
|
+
}
|
|
284
|
+
const seq = ++this.#seq;
|
|
285
|
+
try {
|
|
286
|
+
const near = this.#near();
|
|
287
|
+
const { items } = await client.autocomplete(query, near ? { near } : {});
|
|
288
|
+
if (seq !== this.#seq) return;
|
|
289
|
+
this.#render(items);
|
|
290
|
+
this.#announce(items.length > 0 ? `C\xF3 ${items.length} k\u1EBFt qu\u1EA3.` : "Kh\xF4ng t\xECm th\u1EA5y k\u1EBFt qu\u1EA3.");
|
|
291
|
+
} catch {
|
|
292
|
+
if (seq !== this.#seq) return;
|
|
293
|
+
this.#render([]);
|
|
294
|
+
this.#announce("Kh\xF4ng th\u1EC3 t\u1EA3i g\u1EE3i \xFD. Vui l\xF2ng th\u1EED l\u1EA1i.");
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
#render(items) {
|
|
298
|
+
if (!this.#list || !this.#input) return;
|
|
299
|
+
this.#items = items;
|
|
300
|
+
this.#activeIndex = -1;
|
|
301
|
+
this.#input.removeAttribute("aria-activedescendant");
|
|
302
|
+
this.#input.setAttribute("aria-expanded", String(items.length > 0));
|
|
303
|
+
this.#list.hidden = items.length === 0;
|
|
304
|
+
this.#list.replaceChildren(
|
|
305
|
+
...items.map((item, index) => {
|
|
306
|
+
const option = document.createElement("li");
|
|
307
|
+
option.id = `${this.#listId}-option-${index}`;
|
|
308
|
+
option.dataset.index = String(index);
|
|
309
|
+
option.dataset.type = item.type;
|
|
310
|
+
option.setAttribute("role", "option");
|
|
311
|
+
option.setAttribute("aria-selected", "false");
|
|
312
|
+
const icon = document.createElement("span");
|
|
313
|
+
icon.className = "icon";
|
|
314
|
+
icon.setAttribute("aria-hidden", "true");
|
|
315
|
+
icon.textContent = TYPE_ICON[item.type] ?? TYPE_ICON.poi;
|
|
316
|
+
const name = document.createElement("span");
|
|
317
|
+
name.textContent = item.name;
|
|
318
|
+
const secondary = document.createElement("span");
|
|
319
|
+
secondary.className = "secondary";
|
|
320
|
+
secondary.textContent = item.matched_alt ? `${item.secondary}${item.secondary ? " \xB7 " : ""}t\xEAn c\u0169: ${item.matched_alt}` : item.secondary;
|
|
321
|
+
option.append(icon, name, secondary);
|
|
322
|
+
return option;
|
|
323
|
+
})
|
|
324
|
+
);
|
|
325
|
+
}
|
|
326
|
+
#setActive(index) {
|
|
327
|
+
if (!this.#input || !this.#list) return;
|
|
328
|
+
this.#activeIndex = index;
|
|
329
|
+
for (const [optionIndex, option] of Array.from(this.#list.children).entries()) {
|
|
330
|
+
option.setAttribute("aria-selected", String(optionIndex === index));
|
|
331
|
+
}
|
|
332
|
+
const active = this.#list.children.item(index);
|
|
333
|
+
if (active) {
|
|
334
|
+
this.#input.setAttribute("aria-activedescendant", active.id);
|
|
335
|
+
active.scrollIntoView({ block: "nearest" });
|
|
336
|
+
}
|
|
337
|
+
}
|
|
338
|
+
#select(index) {
|
|
339
|
+
const item = this.#items[index];
|
|
340
|
+
if (!item || !this.#input) return;
|
|
341
|
+
this.#input.value = item.name;
|
|
342
|
+
this.#render([]);
|
|
343
|
+
this.#announce(`\u0110\xE3 ch\u1ECDn ${item.name}.`);
|
|
344
|
+
this.dispatchEvent(new CustomEvent("select", { detail: item, bubbles: true, composed: true }));
|
|
345
|
+
}
|
|
346
|
+
#announce(message) {
|
|
347
|
+
if (this.#status) this.#status.textContent = message;
|
|
348
|
+
}
|
|
349
|
+
};
|
|
350
|
+
function defineAutocomplete() {
|
|
351
|
+
if (!customElements.get("mapslibvn-autocomplete"))
|
|
352
|
+
customElements.define("mapslibvn-autocomplete", MapsLibVNAutocomplete);
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
// src/index.ts
|
|
356
|
+
import { attributionHtml as attributionHtml2, attributionText, createClient as createClient3, MapsLibVNError } from "@mapslibvn/core";
|
|
357
|
+
export {
|
|
358
|
+
MapsLibVNAutocomplete,
|
|
359
|
+
MapsLibVNError,
|
|
360
|
+
applyLanguage,
|
|
361
|
+
attributionHtml2 as attributionHtml,
|
|
362
|
+
attributionText,
|
|
363
|
+
createClient3 as createClient,
|
|
364
|
+
createMap,
|
|
365
|
+
defineAutocomplete,
|
|
366
|
+
nameExpression
|
|
367
|
+
};
|