@excom/mapbox-view 0.1.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.
Files changed (33) hide show
  1. package/.rush/temp/chunked-rush-logs/mapbox-view.apply-exports.chunks.jsonl +1 -0
  2. package/.rush/temp/chunked-rush-logs/mapbox-view.build_docs.chunks.jsonl +1 -0
  3. package/.rush/temp/chunked-rush-logs/mapbox-view.build_package-metas.chunks.jsonl +1 -0
  4. package/.rush/temp/operation/apply-exports/all.log +1 -0
  5. package/.rush/temp/operation/apply-exports/log-chunks.jsonl +1 -0
  6. package/.rush/temp/operation/apply-exports/state.json +3 -0
  7. package/.rush/temp/operation/build_docs/all.log +1 -0
  8. package/.rush/temp/operation/build_docs/log-chunks.jsonl +1 -0
  9. package/.rush/temp/operation/build_docs/state.json +3 -0
  10. package/.rush/temp/operation/build_package-metas/all.log +1 -0
  11. package/.rush/temp/operation/build_package-metas/log-chunks.jsonl +1 -0
  12. package/.rush/temp/operation/build_package-metas/state.json +3 -0
  13. package/.rush/temp/shrinkwrap-deps.json +35 -0
  14. package/config/rig.json +5 -0
  15. package/index.css +5 -0
  16. package/index.ts +7 -0
  17. package/mapbox-view-marker.ts +279 -0
  18. package/mapbox-view.ts +582 -0
  19. package/package.json +46 -0
  20. package/rush-logs/mapbox-view.apply-exports.cache.log +1 -0
  21. package/rush-logs/mapbox-view.apply-exports.log +1 -0
  22. package/rush-logs/mapbox-view.build_docs.cache.log +1 -0
  23. package/rush-logs/mapbox-view.build_docs.log +1 -0
  24. package/rush-logs/mapbox-view.build_package-metas.cache.log +1 -0
  25. package/rush-logs/mapbox-view.build_package-metas.log +1 -0
  26. package/src/mapbox-view.css +10 -0
  27. package/src/types.ts +32 -0
  28. package/src/utils.ts +78 -0
  29. package/support/custom-elements.json +77 -0
  30. package/support/dist-docs/mapbox-view-marker.md +3 -0
  31. package/support/dist-docs/mapbox-view.md +33 -0
  32. package/support/tests/mapbox-view.test.ts +41 -0
  33. package/tsconfig.json +11 -0
@@ -0,0 +1 @@
1
+ {"kind":"O","text":"Invoking: cd \"$RUSH_PROJECT_FOLDER\" && node ../heft-rig/scripts/apply-exports.mjs \n"}
@@ -0,0 +1 @@
1
+ {"kind":"O","text":"Invoking: node node_modules/@excom/heft-rig/scripts/build-docs.mjs \n"}
@@ -0,0 +1 @@
1
+ {"kind":"O","text":"Invoking: node node_modules/@excom/heft-rig/scripts/build-package-metas.mjs \n"}
@@ -0,0 +1 @@
1
+ Invoking: cd "$RUSH_PROJECT_FOLDER" && node ../heft-rig/scripts/apply-exports.mjs
@@ -0,0 +1 @@
1
+ {"kind":"O","text":"Invoking: cd \"$RUSH_PROJECT_FOLDER\" && node ../heft-rig/scripts/apply-exports.mjs \n"}
@@ -0,0 +1,3 @@
1
+ {
2
+ "nonCachedDurationMs": 35.808496000000105
3
+ }
@@ -0,0 +1 @@
1
+ Invoking: node node_modules/@excom/heft-rig/scripts/build-docs.mjs
@@ -0,0 +1 @@
1
+ {"kind":"O","text":"Invoking: node node_modules/@excom/heft-rig/scripts/build-docs.mjs \n"}
@@ -0,0 +1,3 @@
1
+ {
2
+ "nonCachedDurationMs": 42.559968000000026
3
+ }
@@ -0,0 +1 @@
1
+ Invoking: node node_modules/@excom/heft-rig/scripts/build-package-metas.mjs
@@ -0,0 +1 @@
1
+ {"kind":"O","text":"Invoking: node node_modules/@excom/heft-rig/scripts/build-package-metas.mjs \n"}
@@ -0,0 +1,3 @@
1
+ {
2
+ "nonCachedDurationMs": 68.63088599999992
3
+ }
@@ -0,0 +1,35 @@
1
+ {
2
+ "../../packages/mapbox-view": "../../packages/mapbox-view:waN6IYkT0kWJoHv+Il2v7PKH1oqmE6rt1niWmjXrbj4=:",
3
+ "@mapbox/jsonlint-lines-primitives@2.0.2": "@mapbox/jsonlint-lines-primitives@2.0.2:pTMNfjSWqxSbYnhqr8pm9NAFrBp+ZeCfZLGTVw0e0pM=:",
4
+ "@mapbox/mapbox-gl-supported@3.0.0": "@mapbox/mapbox-gl-supported@3.0.0:IhPd0a0B6DvTT119ufIe8zot6xYG5lAIRFAdFb6JeUA=:",
5
+ "@mapbox/point-geometry@1.1.0": "@mapbox/point-geometry@1.1.0:Bm0cSoJTXQgIUMfVOHRP/37hAUqNcit6uJkjCmqmFOc=:",
6
+ "@mapbox/tiny-sdf@2.0.7": "@mapbox/tiny-sdf@2.0.7:AXIff3JeANbTLZP6eJCp/Mk2QZexnR9MnxCMjX9sBTU=:",
7
+ "@mapbox/unitbezier@0.0.1": "@mapbox/unitbezier@0.0.1:sGC7+jQh2i9whO8vuKDKffOHvdDMTmePjF2ooKlFlio=:",
8
+ "@mapbox/vector-tile@2.0.4": "@mapbox/vector-tile@2.0.4:hh5E9i+Eclgnk5M529e59ytFHhJ6dZmUwq88DHmwBVM=:",
9
+ "@mapbox/whoots-js@3.1.0": "@mapbox/whoots-js@3.1.0:Vk9XwLe0n7cWbOaabSlAesXX0UDIgp+mJGtLX0GHIqQ=:",
10
+ "@types/geojson-vt@3.2.5": "@types/geojson-vt@3.2.5:UvTNEt3a+ghKLyVCTroNhY23UAAeXdabZUMrEvPXWB8=:",
11
+ "@types/geojson@7946.0.16": "@types/geojson@7946.0.16:gcMlSqEHJU4IX6+c7qnAWFvEefycVi66cMxuADjE2B0=:",
12
+ "@types/mapbox__point-geometry@0.1.4": "@types/mapbox__point-geometry@0.1.4:GygVrmKyj0wDWwdQcNQ5I0saPTBG2bzUI4Nrdm2Z0ZU=:",
13
+ "@types/pbf@3.0.5": "@types/pbf@3.0.5:0S+vW4kiwqZjQXiuTNXGnKze6PvnUmgQ6rtCtMncWtg=:",
14
+ "@types/supercluster@7.1.3": "@types/supercluster@7.1.3:JJw17OdaacIMfw3OSnYTBqj5hvfH8lzR+udEUSxNWWI=:",
15
+ "cheap-ruler@4.0.0": "cheap-ruler@4.0.0:yiDcBxPrql3UwD/Uox6B4cGVBbkPoRGkLGyQ1wi0LzI=:",
16
+ "csscolorparser@1.0.3": "csscolorparser@1.0.3:S3na1TrpSMzZ48LFKxCgcGaL/GayChj6fZtGMOYQYOc=:",
17
+ "earcut@3.0.2": "earcut@3.0.2:h0+7LalGP3yo5ywfF/AwqcULA73JcRn8Q4bD4g0Sjhw=:",
18
+ "geojson-vt@4.0.2": "geojson-vt@4.0.2:0jHmKg55n/7M6aFWRHOB0rFJ7xm4zO5UteGqnXsTKas=:",
19
+ "gl-matrix@3.4.4": "gl-matrix@3.4.4:ZP629NFaBeO6xsS7/LcCCBKRCikT7jTFadMFSsQpX6M=:",
20
+ "grid-index@1.1.0": "grid-index@1.1.0:qzgVCoYo9QfK5abOV1Q3VnyhPqa2mRYJS5fLX6EWmTY=:",
21
+ "kdbush@4.0.2": "kdbush@4.0.2:+tgNxS9hvxLrm+MYkGLLPUmiBDxnM9zTJUMxzogXQiQ=:",
22
+ "mapbox-gl@3.17.0": "mapbox-gl@3.17.0:N95/XxVSFICm4Ki8C926ltIrBw1PFDSXlitMPtJ6ias=:",
23
+ "martinez-polygon-clipping@0.7.4": "martinez-polygon-clipping@0.7.4:z26W/5T0GI+vPPsdA2VZNWwQRByfLefZU9i1aHjncLA=:",
24
+ "murmurhash-js@1.0.0": "murmurhash-js@1.0.0:4fRjg8epvWyUGtJngQ0YSpU5ay8QkF4cWsUUybHnDIA=:",
25
+ "pbf@4.0.1": "pbf@4.0.1:62nhmgjuKKsgpt20FlfO4XgIh1v2U8ppQsVz4tDU+g8=:",
26
+ "potpack@2.1.0": "potpack@2.1.0:4AusZbGjTmOT1kl7bYViTmsF5+Jm/P20F86FTRJAC9Y=:",
27
+ "protocol-buffers-schema@3.6.0": "protocol-buffers-schema@3.6.0:Y5nXV/1DfdBj9epKmmnoDYDnxbbhQWBdE9OM3eWs6y4=:",
28
+ "quickselect@3.0.0": "quickselect@3.0.0:3hNUJ5B8vj6I5Dzp6CvCvxglDbhHnQS6NImDgEV4V8A=:",
29
+ "resolve-protobuf-schema@2.1.0": "resolve-protobuf-schema@2.1.0:uOoqxXADHusstNUJDzpYjj+Mckn0qAlKYaSzULxUwQk=:",
30
+ "robust-predicates@2.0.4": "robust-predicates@2.0.4:IFDQpMXlUKSPbQFa277oYEL5MbvjlW7HjIQRkhac0/U=:",
31
+ "splaytree@0.1.4": "splaytree@0.1.4:hxchdprRjVa5N/GohBO7ymNF2j+Py1vqN/+yBAJo9JY=:",
32
+ "supercluster@8.0.1": "supercluster@8.0.1:9QZIbpvOBX1e4YwV2UFvF35hAK2QDzEpi0yoFCdP51I=:",
33
+ "tinyqueue@1.2.3": "tinyqueue@1.2.3:TFqtbhraVvlBBp22m8Yjhqu+iBsracFKYBqVOPrzo2g=:",
34
+ "tinyqueue@3.0.0": "tinyqueue@3.0.0:WYmogjNW144eAmI23uxK91JRv9Q0SXiCck2JAbE8hGY=:"
35
+ }
@@ -0,0 +1,5 @@
1
+ {
2
+ "$schema": "https://developer.microsoft.com/json-schemas/rig-package/rig.schema.json",
3
+ "rigPackageName": "@excom/heft-rig",
4
+ "rigProfile": "default"
5
+ }
package/index.css ADDED
@@ -0,0 +1,5 @@
1
+ @import "./src/mapbox-view.css";
2
+
3
+ @layer {
4
+ @mixin module-mapbox-view;
5
+ }
package/index.ts ADDED
@@ -0,0 +1,7 @@
1
+ import { MapboxView } from "./mapbox-view";
2
+ import { MapboxViewMarker } from "./mapbox-view-marker";
3
+
4
+ MapboxView.define();
5
+ MapboxViewMarker.define();
6
+
7
+ export { MapboxView, MapboxViewMarker };
@@ -0,0 +1,279 @@
1
+ import type { MapboxView as TMapboxView } from "./mapbox-view";
2
+ import type {
3
+ MapInstance,
4
+ TMapboxgl,
5
+ TMapboxglMarkerOptions,
6
+ } from "./src/types";
7
+ import {
8
+ parseCoordsFromTokens,
9
+ parseNumsFromTokens,
10
+ queueTask,
11
+ } from "./src/utils";
12
+ import { deleteUndefined, selectOne } from "@excom/kit-utils";
13
+ import { ConstructorType, Neutron, TokenList } from "@excom/neutron";
14
+
15
+ // Workaround until partial type inference lands: https://github.com/Microsoft/TypeScript/pull/26349
16
+
17
+ type T_HTMLMapboxViewMarkerElement = typeof TMapboxView.CustomElement;
18
+
19
+ export const MapboxViewMarker = Neutron({
20
+ tag: "mapbox-view-marker",
21
+ props: {
22
+ mapRef: String,
23
+ mapElement:
24
+ HTMLElement as unknown as ConstructorType<T_HTMLMapboxViewMarkerElement>,
25
+ mapInstance: Object as unknown as ConstructorType<MapInstance>,
26
+ markerInstance: Object as unknown as TMapboxgl["Marker"],
27
+ markerElement: HTMLElement,
28
+ pinTag: String,
29
+ pinDataText: String,
30
+ pinDataPopoverText: String,
31
+ pinClass: String,
32
+ pinCoords: TokenList,
33
+ pinOffset: TokenList,
34
+ pinAnchor: String,
35
+ pinColor: String,
36
+ pinScale: Number,
37
+ pinDraggable: Boolean,
38
+ pinClickTolerance: Number,
39
+ pinRotation: Number,
40
+ pinRotationAlignment: String,
41
+ pinPitchAlignment: String,
42
+ pinOccludedOpacity: Number,
43
+ pinStyle: String,
44
+ focusEvents: TokenList,
45
+ panBoundaries: TokenList,
46
+ blurOnMapClick: Boolean,
47
+ singletonName: String,
48
+ },
49
+ }).defineMethods({
50
+ panToMarker: ({ panBoundaries, markerElement, mapElement, pinCoords }) => {
51
+ const markerRect = (markerElement as Element).getBoundingClientRect?.();
52
+ const mapRect = (mapElement as Element).getBoundingClientRect?.();
53
+ const _panBoundaries = parseNumsFromTokens(panBoundaries) || [
54
+ 15, 15, 15, 15,
55
+ ];
56
+ const isTop = markerRect.top < mapRect.height * (_panBoundaries[0] / 100);
57
+ const isRight =
58
+ markerRect.right > mapRect.width * (1 - _panBoundaries[1] / 100);
59
+ const isBottom =
60
+ markerRect.bottom > mapRect.height * (1 - _panBoundaries[2] / 100);
61
+ const isLeft = markerRect.left < mapRect.width * (_panBoundaries[3] / 100);
62
+ return {
63
+ ...((isTop || isRight || isBottom || isLeft) && {
64
+ mapElement: {
65
+ activeCoords: pinCoords || [],
66
+ emit: ["mapbox-view-reactive"],
67
+ },
68
+ }),
69
+ emit: ["mapbox-view-marker-focus"],
70
+ };
71
+ },
72
+ handleMarkerClick: ({ blurOnMapClick }, e: Event) => {
73
+ if (blurOnMapClick) {
74
+ e.stopPropagation();
75
+ }
76
+ return { panToMarker: [], emit: ["mapbox-view-marker-interacted"] };
77
+ },
78
+ setMapElement: (element) => {
79
+ const existingMapElement = element.mapElement;
80
+ const mapElement = selectOne(element.mapRef || "mapbox-view", {
81
+ scope: element,
82
+ }) as T_HTMLMapboxViewMarkerElement;
83
+ if (mapElement && mapElement !== existingMapElement) {
84
+ return {
85
+ mapElement,
86
+ };
87
+ }
88
+ },
89
+ setMapAndMarker: (element, mapInstance: MapInstance, mb: TMapboxgl) => ({
90
+ mapInstance,
91
+ markerInstance: new mb.Marker(markerOpts(element)),
92
+ }),
93
+ blurCallback: () => ({
94
+ emit: ["mapbox-view-marker-blur"],
95
+ }),
96
+ singletonCallback: (element, { detail }: CustomEvent<typeof element>) => {
97
+ if (
98
+ element !== detail &&
99
+ element.singletonName &&
100
+ detail?.singletonName &&
101
+ element.singletonName === detail?.singletonName
102
+ ) {
103
+ element.markerElement?.classList.remove("is-active");
104
+ }
105
+ },
106
+ });
107
+
108
+ type PartialProps = Partial<(typeof MapboxViewMarker)["Props"]>;
109
+
110
+ const markerOpts = (element: PartialProps): TMapboxglMarkerOptions => {
111
+ // @ts-ignore
112
+ return deleteUndefined({
113
+ element: element.pinTag
114
+ ? document.createElement(element.pinTag)
115
+ : undefined,
116
+ offset: parseCoordsFromTokens(element.pinOffset),
117
+ color: element.pinColor,
118
+ scale: element.pinScale,
119
+ anchor: (element.pinAnchor ||
120
+ undefined) as TMapboxglMarkerOptions["anchor"],
121
+ draggable: element.pinDraggable,
122
+ rotation: element.pinRotation,
123
+ rotationAlignment: element.pinRotationAlignment,
124
+ pitchAlignment: element.pinPitchAlignment,
125
+ occludedOpacity: element.pinOccludedOpacity,
126
+ className: element.pinClass,
127
+ });
128
+ };
129
+
130
+ MapboxViewMarker.onConnected(({ isMoving, mapRef, setMapElement }) => {
131
+ // setTimeout to ensure map element is instantiated; the timer id must not
132
+ // be returned
133
+ if (!isMoving && !mapRef) setTimeout(setMapElement, 0);
134
+ })
135
+ .onPropSet("mapRef", ({ setMapElement }) => {
136
+ // Defer so the map element is instantiated
137
+ setTimeout(setMapElement, 0);
138
+ })
139
+ .onPropSet("mapElement", ({ mapElement, setMapAndMarker }) => {
140
+ if (mapElement.mapInstance) {
141
+ queueTask(mapElement.mapInstance, setMapAndMarker);
142
+ } else {
143
+ // Map not ready yet (first load / Mapbox GL still loading).
144
+ // Wait for load, then init the marker.
145
+ mapElement.addEventListener(
146
+ "mapbox-view-loaded",
147
+ () => queueTask(mapElement.mapInstance, setMapAndMarker),
148
+ { once: true }
149
+ );
150
+ }
151
+ })
152
+ .onDisconnected(({ markerInstance, isMoving }) => {
153
+ if (!isMoving) {
154
+ if (markerInstance) {
155
+ markerInstance.remove();
156
+ }
157
+ return {
158
+ mapElement: null,
159
+ mapInstance: null,
160
+ markerInstance: null,
161
+ markerElement: null,
162
+ };
163
+ }
164
+ })
165
+ .onPropChanged(
166
+ ["pinCoords", "markerInstance"],
167
+ ({ pinCoords, markerInstance, mapInstance, markerElement }) => {
168
+ const coords = parseCoordsFromTokens(pinCoords);
169
+ if (markerInstance && coords && mapInstance) {
170
+ markerInstance.setLngLat(coords);
171
+ markerInstance.addTo(mapInstance);
172
+ return (
173
+ !markerElement && {
174
+ markerElement: markerInstance.getElement(),
175
+ }
176
+ );
177
+ }
178
+ }
179
+ )
180
+ .onPropChanged(
181
+ ["markerInstance", "pinClass"],
182
+ ({ markerInstance, pinClass }, previous) => {
183
+ if (markerInstance) {
184
+ if (previous?.pinClass) {
185
+ previous.pinClass
186
+ .split(" ")
187
+ .forEach((c) => markerInstance.removeClassName(c));
188
+ }
189
+ pinClass?.split(" ")?.forEach((c) => markerInstance.addClassName(c));
190
+ }
191
+ }
192
+ )
193
+ .onPropChanged(
194
+ ["markerElement", "pinDataText"],
195
+ ({ markerElement, pinDataText }) => {
196
+ if (markerElement) {
197
+ return {
198
+ markerElement: {
199
+ setAttribute: ["data-text", pinDataText || ""],
200
+ },
201
+ };
202
+ }
203
+ }
204
+ )
205
+ .onPropChanged(
206
+ ["markerElement", "pinDataPopoverText"],
207
+ ({ markerElement, pinDataPopoverText }) => {
208
+ if (markerElement) {
209
+ return {
210
+ markerElement: {
211
+ setAttribute: ["data-popover-text", pinDataPopoverText || ""],
212
+ },
213
+ };
214
+ }
215
+ }
216
+ )
217
+ .onPropChanged(
218
+ ["pinStyle", "markerElement"],
219
+ ({ pinStyle, markerElement }) => {
220
+ if (markerElement) {
221
+ markerElement.setAttribute(
222
+ "style",
223
+ `${markerElement.getAttribute("style") ?? ""}; ${pinStyle ?? ""}`
224
+ );
225
+ }
226
+ }
227
+ )
228
+ .onPropChanged(
229
+ ["markerElement", "focusEvents"],
230
+ ({ markerElement, handleMarkerClick, focusEvents }, previous) => {
231
+ if (markerElement) {
232
+ return {
233
+ markerElement: {
234
+ toggleListeners: [
235
+ ...(focusEvents || []).map((k) => [k, handleMarkerClick, true]),
236
+ ...(previous?.focusEvents
237
+ ? (previous.focusEvents || []).map((k) => [
238
+ k,
239
+ handleMarkerClick,
240
+ false,
241
+ ])
242
+ : []),
243
+ ],
244
+ },
245
+ };
246
+ }
247
+ }
248
+ )
249
+ .onEvent("mapbox-view-marker-focus-trigger", (_, e) => {
250
+ e.stopPropagation();
251
+ return {
252
+ panToMarker: [],
253
+ };
254
+ })
255
+ .onEventDefault("mapbox-view-marker-focus", ({ markerElement }) => {
256
+ markerElement?.classList.add("is-active");
257
+ return {
258
+ broadcast: ["mapbox-view-marker-focus", { detail: markerElement }],
259
+ };
260
+ })
261
+ .onEvent("mapbox-view-marker-blur-trigger", (_, e) => {
262
+ e.stopPropagation();
263
+ return {
264
+ emit: ["mapbox-view-marker-blur"],
265
+ };
266
+ })
267
+ .onEventDefault("mapbox-view-marker-blur", ({ markerElement }) => {
268
+ markerElement?.classList.remove("is-active");
269
+ })
270
+ .onPropChanged("blurOnMapClick", ({ blurOnMapClick, blurCallback }) => ({
271
+ toggleBroadcastListeners: [
272
+ ["mapbox-view-clicked", blurCallback, !!blurOnMapClick],
273
+ ],
274
+ }))
275
+ .onPropChanged("singletonName", ({ singletonName, singletonCallback }) => ({
276
+ toggleBroadcastListeners: [
277
+ ["mapbox-view-marker-focus", singletonCallback, !!singletonName],
278
+ ],
279
+ }));
package/mapbox-view.ts ADDED
@@ -0,0 +1,582 @@
1
+ import type {
2
+ LayersConfig,
3
+ MapInstance,
4
+ TAnimationOptions,
5
+ TCameraOptions,
6
+ TMapboxgl,
7
+ TMapTouchEvent,
8
+ } from "./src/types";
9
+ import { EASINGS, parseCoordsFromTokens, queueTask } from "./src/utils";
10
+ import { createElement, deleteUndefined, isPojo } from "@excom/kit-utils";
11
+ import { loadDependency } from "@excom/kit-utils/load-dependency";
12
+ import { ConstructorType, Neutron, TokenList } from "@excom/neutron";
13
+
14
+ const centerCameraOpts = (el: any): TCameraOptions => {
15
+ const coords = parseCoordsFromTokens(el.centerCoords, el.camOffset) || [0, 0];
16
+ // `maxBounds` must be a TokenList / array of 4 finite numbers
17
+ let maxBounds: [[number, number], [number, number]] | undefined = undefined;
18
+ if (el.maxBounds?.length === 4) {
19
+ const nums = Array.from(el.maxBounds, (v) => Number(v));
20
+ if (nums.every((v) => typeof v === "number" && isFinite(v))) {
21
+ maxBounds = [
22
+ [nums[0], nums[1]], // SW [lng, lat]
23
+ [nums[2], nums[3]], // NE [lng, lat]
24
+ ];
25
+ }
26
+ }
27
+ return deleteUndefined({
28
+ center: coords, // [lng, lat]; lat must be -90..90
29
+ zoom: el.centerZoom || 1, // starting zoom
30
+ pitch: el.centerPitch || 0, // degrees
31
+ bearing: el.centerBearing || 0, // degrees
32
+ minZoom: el.minZoom ?? 10,
33
+ maxZoom: el.maxZoom ?? 20,
34
+ maxBounds,
35
+ });
36
+ };
37
+ const activeCameraOpts = (el: any): TCameraOptions => {
38
+ const coords = parseCoordsFromTokens(el.activeCoords, el.camOffset) ||
39
+ parseCoordsFromTokens(el.centerCoords, el.camOffset) || [0, 0];
40
+ return deleteUndefined({
41
+ center: coords,
42
+ zoom: el.activeZoom ?? undefined,
43
+ pitch: el.activePitch ?? undefined,
44
+ bearing: el.activeBearing ?? undefined,
45
+ });
46
+ };
47
+ const shouldAnimate = (el: any): boolean => {
48
+ return !!(
49
+ typeof el.camAnimation === "string" ||
50
+ el.camAnimationDuration ||
51
+ el.camAnimationOffset?.length ||
52
+ el.camAnimationEssential
53
+ );
54
+ };
55
+ const animateCameraOpts = (el: any): TAnimationOptions => {
56
+ return deleteUndefined({
57
+ // Even `cam-animate=""` still animates with the default easing
58
+ animate: shouldAnimate(el),
59
+ easing:
60
+ (el.camAnimation && EASINGS[el.camAnimation]) ||
61
+ EASINGS["ease-out-quint"],
62
+ duration: el.camAnimationDuration || undefined,
63
+ offset: (el.camAnimationOffset?.length === 2
64
+ ? el.camAnimationOffset.map((n) => Number(n))
65
+ : undefined) as [number, number],
66
+ essential: el.camAnimationEssential || undefined,
67
+ });
68
+ };
69
+ const checkMapMoved = (el: any): boolean => {
70
+ if (!el.mapInstance) return false;
71
+ const opts = centerCameraOpts(el);
72
+ const camCoords = el.mapInstance?.getCenter();
73
+ const coordsTolerance = el.centerCoordsTolerance || 0.0005;
74
+ const zoomTolerance = el.centerZoomTolerance || 0.5;
75
+ const pitchTolerance = el.centerPitchTolerance || 0;
76
+ const bearingTolerance = el.centerBearingTolerance || 0;
77
+
78
+ if (
79
+ !opts.center ||
80
+ opts.zoom === undefined ||
81
+ opts.pitch === undefined ||
82
+ opts.bearing === undefined
83
+ ) {
84
+ return false;
85
+ }
86
+
87
+ const r = el.preserveCamera || [];
88
+
89
+ return !(
90
+ (r.includes("coords") ||
91
+ Math.abs(camCoords.lng - opts.center[0]) <= coordsTolerance) &&
92
+ (r.includes("coords") ||
93
+ Math.abs(camCoords.lat - opts.center[1]) <= coordsTolerance) &&
94
+ (r.includes("zoom") ||
95
+ Math.abs(el.mapInstance.getZoom() - opts.zoom) <= zoomTolerance) &&
96
+ (r.includes("pitch") ||
97
+ Math.abs(el.mapInstance.getPitch() - opts.pitch) <= pitchTolerance) &&
98
+ (r.includes("bearing") ||
99
+ Math.abs(el.mapInstance.getBearing() - opts.bearing) <= bearingTolerance)
100
+ );
101
+ };
102
+
103
+ export const MapboxView = Neutron({
104
+ tag: "mapbox-view",
105
+ renderRoot: {
106
+ tag: "div",
107
+ shadow: "open",
108
+ },
109
+ props: {
110
+ mapboxPromise: Promise as ConstructorType<Promise<TMapboxgl>>,
111
+ containerElement: HTMLElement,
112
+ mapInstance: Object as unknown as ConstructorType<MapInstance>,
113
+ accessToken: String,
114
+ mapStyle: String,
115
+ isLoaded: Boolean,
116
+ isAnimated: Boolean,
117
+ isMoved: Boolean,
118
+ centerCoords: TokenList,
119
+ centerCoordsTolerance: Number,
120
+ centerZoom: Number,
121
+ centerZoomTolerance: Number,
122
+ centerPitch: Number,
123
+ centerPitchTolerance: Number,
124
+ centerBearing: Number,
125
+ centerBearingTolerance: Number,
126
+ activeCoords: TokenList,
127
+ activeZoom: Number,
128
+ activePitch: Number,
129
+ activeBearing: Number,
130
+ preserveCamera: TokenList,
131
+ minZoom: Number,
132
+ maxZoom: Number,
133
+ maxBounds: TokenList,
134
+ camOffset: TokenList,
135
+ camAnimation: String,
136
+ camAnimationDuration: Number,
137
+ camAnimationOffset: TokenList,
138
+ camAnimationEssential: Boolean,
139
+ timerId: Number,
140
+ isDragging: Boolean,
141
+ clickholdMs: Number,
142
+ droppedPin: Object,
143
+ layers: Object as unknown as ConstructorType<LayersConfig>,
144
+ activeLayer: String,
145
+ visibleLayers: TokenList,
146
+ camPadding: TokenList,
147
+ },
148
+ })
149
+ .defineMethods({
150
+ holdStartCb: (
151
+ // @ts-ignore TODO defineMethods
152
+ { isDragging, clickholdMs, emitClickhold },
153
+ e: TMapTouchEvent
154
+ ) => {
155
+ // Ignore multi-touch
156
+ if (e.originalEvent?.touches && e.originalEvent.touches.length !== 1)
157
+ return;
158
+ return (
159
+ !isDragging &&
160
+ clickholdMs && {
161
+ timerId: setTimeout(
162
+ () => emitClickhold(e.lngLat),
163
+ clickholdMs
164
+ ) as unknown as number,
165
+ }
166
+ );
167
+ },
168
+ holdEndCb: ({ timerId }) => {
169
+ if (timerId) clearTimeout(timerId);
170
+ },
171
+ holdDragstartCb: ({ timerId }) => {
172
+ if (timerId) clearTimeout(timerId);
173
+ return {
174
+ isDragging: true,
175
+ };
176
+ },
177
+ holdDragendCb: () => {
178
+ return {
179
+ isDragging: false,
180
+ };
181
+ },
182
+ mapClickedCb: (_, e: TMapTouchEvent) => ({
183
+ broadcast: ["mapbox-view-clicked", { detail: e }],
184
+ }),
185
+ emitClickhold: (_, lngLat) => {
186
+ return {
187
+ emit: [
188
+ "mapbox-view-clickhold",
189
+ {
190
+ detail: lngLat,
191
+ },
192
+ ],
193
+ };
194
+ },
195
+ loadedCallback: () => ({
196
+ isLoaded: true,
197
+ emit: ["mapbox-view-loaded"],
198
+ }),
199
+ calcIsMoved: (element, override: boolean) => ({
200
+ isMoved: override ?? checkMapMoved(element),
201
+ }),
202
+ updateCamera: (
203
+ element,
204
+ destination: "center" | "active",
205
+ cb?: () => Record<string, any> | any
206
+ ) => {
207
+ if (element.mapInstance) {
208
+ // Already at the destination (within tolerance): skip animation,
209
+ // but still update `isMoved` to match.
210
+ if (destination === "center" && !checkMapMoved(element)) {
211
+ return { calcIsMoved: [false] };
212
+ }
213
+ queueTask(element.mapInstance, (m) => {
214
+ const cbResult = cb?.();
215
+ if (!cb || cbResult) {
216
+ const updateObject = isPojo(cbResult) ? cbResult : {};
217
+ const cameraOpts = deleteUndefined({
218
+ ...(destination === "center"
219
+ ? centerCameraOpts(element)
220
+ : activeCameraOpts(element)),
221
+ // Drop any properties in the `preserveCamera` list
222
+ ...Object.fromEntries(
223
+ (element.preserveCamera || []).map((key: string) => [
224
+ key === "coords" ? "center" : key,
225
+ undefined,
226
+ ])
227
+ ),
228
+ ...deleteUndefined(updateObject),
229
+ });
230
+ if (destination === "center" && element.isAnimated) {
231
+ // Continuous tracking: `easeTo` for short incremental updates
232
+ m.easeTo({ ...cameraOpts, duration: 250 });
233
+ } else {
234
+ m[element.isAnimated ? "flyTo" : "jumpTo"]({
235
+ ...cameraOpts,
236
+ ...(element.isAnimated ? animateCameraOpts(element) : {}),
237
+ });
238
+ }
239
+ }
240
+ });
241
+ /* Return outside `queueTask` so Neutron sees the state update.
242
+ A return inside the callback was discarded, leaving `isMoved`
243
+ stuck after programmatic camera moves (recenter, bearing). */
244
+ return {
245
+ calcIsMoved: [destination === "active"],
246
+ };
247
+ }
248
+ },
249
+ zoomToOverview: (element) => {
250
+ if (element.mapInstance) {
251
+ queueTask(element.mapInstance, (m) => {
252
+ m[element.isAnimated ? "easeTo" : "jumpTo"]({
253
+ zoom: element.centerZoom ?? undefined,
254
+ });
255
+ });
256
+ }
257
+ },
258
+ })
259
+ .onConnected((el) => {
260
+ if (el.isMoving) return;
261
+ const cssLink = createElement("link", {
262
+ rel: "stylesheet",
263
+ href: "https://api.mapbox.com/mapbox-gl-js/v3.9.2/mapbox-gl.css",
264
+ });
265
+ const div = createElement("div", {
266
+ style: "height: 100%; width: 100%;",
267
+ id: `mapbox-container-${Math.random().toString(36).substr(2, 9)}`,
268
+ });
269
+
270
+ // Layers from a script slot
271
+ const layersScript = el.querySelector('script[slot="layers"]');
272
+ if (layersScript && layersScript.textContent) {
273
+ try {
274
+ const layersData = JSON.parse(layersScript.textContent);
275
+ el.layers = layersData;
276
+ } catch (error) {
277
+ console.error("Failed to parse layers JSON:", error);
278
+ }
279
+ }
280
+
281
+ return {
282
+ renderRoot: {
283
+ append: [cssLink, div],
284
+ },
285
+ containerElement: div,
286
+ };
287
+ })
288
+ .onPropChanged(["accessToken", "containerElement"], (el, previous) => {
289
+ if (el.accessToken && el.containerElement) {
290
+ // `accessToken` changed: recreate the map
291
+ const accessTokenChanged =
292
+ previous?.accessToken && previous.accessToken !== el.accessToken;
293
+
294
+ if (!el.mapInstance || accessTokenChanged) {
295
+ // Tear down the existing map after an access-token change
296
+ if (accessTokenChanged && el.mapInstance) {
297
+ el.mapInstance.remove();
298
+ }
299
+
300
+ // Empty the container before creating a new map
301
+ el.containerElement.innerHTML = "";
302
+
303
+ return {
304
+ mapboxPromise: loadDependency<TMapboxgl>(
305
+ "umd",
306
+ "https://api.mapbox.com/mapbox-gl-js/v3.9.2/mapbox-gl.js",
307
+ "mapboxgl"
308
+ ),
309
+ };
310
+ }
311
+ }
312
+ })
313
+ .onPromiseResolved("mapboxPromise", (el, result) => {
314
+ const mapbox = result.mapboxPromise as unknown as TMapboxgl;
315
+ if (mapbox) {
316
+ mapbox.accessToken = el.accessToken;
317
+ return {
318
+ mapboxPromise: null,
319
+ // TODO support dark mode
320
+ mapInstance: new mapbox.Map({
321
+ container: el.containerElement!,
322
+ style: el.mapStyle || undefined,
323
+ ...centerCameraOpts(el),
324
+ }),
325
+ };
326
+ }
327
+ })
328
+ .onPropSet(
329
+ "mapInstance",
330
+ ({ mapInstance, loadedCallback, mapClickedCb, calcIsMoved }) => {
331
+ // Recalc `isMoved` for user gestures only, not programmatic `flyTo` / `easeTo`
332
+ mapInstance.on("movestart", (e) => e.originalEvent && calcIsMoved(true));
333
+ mapInstance.on("moveend", (e) => e.originalEvent && calcIsMoved());
334
+ mapInstance.on("load", () => loadedCallback());
335
+ queueTask(mapInstance, (m) => m.on("click", mapClickedCb));
336
+ }
337
+ )
338
+ .onPropChanged("mapStyle", ({ mapInstance, mapStyle }) => {
339
+ if (mapInstance && mapStyle) {
340
+ queueTask(mapInstance, (m) => {
341
+ try {
342
+ m.setStyle(mapStyle);
343
+ } catch (error) {
344
+ console.warn("Failed to set map style:", error);
345
+ }
346
+ });
347
+ }
348
+ })
349
+ .onPropChanged("camPadding", ({ mapInstance, camPadding }) => {
350
+ if (mapInstance) {
351
+ if (Array.isArray(camPadding) && camPadding.length >= 1) {
352
+ const nums = camPadding.map((v: any) => Number(v) || 0);
353
+ // 1-4 values like CSS: [all], [vertical horizontal], [top right bottom left]
354
+ const top = nums[0];
355
+ const right = nums.length > 1 ? nums[1] : top;
356
+ const bottom = nums.length > 2 ? nums[2] : top;
357
+ const left = nums.length > 3 ? nums[3] : right;
358
+ queueTask(mapInstance, (m) =>
359
+ m.setPadding({ top, right, bottom, left })
360
+ );
361
+ } else {
362
+ queueTask(mapInstance, (m) =>
363
+ m.setPadding({ top: 0, right: 0, bottom: 0, left: 0 })
364
+ );
365
+ }
366
+ }
367
+ })
368
+ .onPropChanged("maxBounds", (el) => {
369
+ if (el.mapInstance && el.maxBounds?.length === 4) {
370
+ const nums = Array.from(el.maxBounds, (v) => Number(v));
371
+ if (nums.every((v) => isFinite(v))) {
372
+ queueTask(el.mapInstance, (m) =>
373
+ m.setMaxBounds([
374
+ [nums[0], nums[1]],
375
+ [nums[2], nums[3]],
376
+ ])
377
+ );
378
+ }
379
+ }
380
+ })
381
+ .onPropChanged(
382
+ ["centerCoords", "centerZoom", "centerPitch", "centerBearing"],
383
+ ({ isMoved }) => !isMoved && { updateCamera: ["center"] }
384
+ )
385
+ .onPropChanged(
386
+ ["activeCoords", "activeZoom", "activePitch", "activeBearing"],
387
+ () => ({
388
+ updateCamera: ["active"],
389
+ })
390
+ )
391
+ .onEvent(
392
+ "mapbox-view-recenter",
393
+ ({ isMoved }) => isMoved && { updateCamera: ["center"] }
394
+ )
395
+ .onEvent("mapbox-view-reactive", () => ({
396
+ updateCamera: ["active"],
397
+ }))
398
+ .onEvent("mapbox-view-zoom-in", ({ activeZoom, centerZoom }) => ({
399
+ activeZoom: (activeZoom ?? centerZoom ?? 1) + 1,
400
+ }))
401
+ .onEvent("mapbox-view-zoom-out", ({ activeZoom, centerZoom }) => ({
402
+ activeZoom: (activeZoom ?? centerZoom ?? 1) - 1,
403
+ }))
404
+ .onEvent("mapbox-view-zoom-overview", () => ({
405
+ zoomToOverview: [],
406
+ }))
407
+ .onPropChanged(
408
+ ["clickholdMs", "mapInstance"],
409
+ ({
410
+ clickholdMs,
411
+ mapInstance,
412
+ holdStartCb,
413
+ holdEndCb,
414
+ holdDragstartCb,
415
+ holdDragendCb,
416
+ }) => {
417
+ if (mapInstance) {
418
+ // Drop previous mouse + touch listeners
419
+ mapInstance.off("mousedown", holdStartCb);
420
+ mapInstance.off("mouseup", holdEndCb);
421
+ mapInstance.off("touchstart", holdStartCb);
422
+ mapInstance.off("touchend", holdEndCb);
423
+ mapInstance.off("touchmove", holdEndCb);
424
+ mapInstance.off("touchcancel", holdEndCb);
425
+ mapInstance.off("dragstart", holdDragstartCb);
426
+ mapInstance.off("dragend", holdDragendCb);
427
+ if (clickholdMs) {
428
+ const nav = navigator as any;
429
+ const isTouch = "ontouchstart" in window || nav.maxTouchPoints > 0;
430
+ if (isTouch) {
431
+ mapInstance.on("touchstart", holdStartCb);
432
+ mapInstance.on("touchend", holdEndCb);
433
+ mapInstance.on("touchmove", holdEndCb);
434
+ mapInstance.on("touchcancel", holdEndCb);
435
+ } else {
436
+ mapInstance.on("mousedown", holdStartCb);
437
+ mapInstance.on("mouseup", holdEndCb);
438
+ }
439
+ // A drag cancels the hold
440
+ mapInstance.on("dragstart", holdDragstartCb);
441
+ mapInstance.on("dragend", holdDragendCb);
442
+ }
443
+ }
444
+ }
445
+ )
446
+ .onPropSet("droppedPin", (_, previous) => {
447
+ (previous.droppedPin as any)?.remove();
448
+ })
449
+ .onPropChanged("activeLayer", ({ mapInstance, layers, activeLayer }) => {
450
+ if (!mapInstance || !activeLayer) return;
451
+
452
+ // Toggle custom layers from `layers`
453
+ if (layers) {
454
+ Object.keys(layers).forEach((layerId) => {
455
+ const fullLayerId = `custom-layer-${layerId}`;
456
+ if (mapInstance.getLayer(fullLayerId)) {
457
+ mapInstance.setLayoutProperty(
458
+ fullLayerId,
459
+ "visibility",
460
+ layerId === activeLayer ? "visible" : "none"
461
+ );
462
+ }
463
+ });
464
+ }
465
+
466
+ // Toggle style layers: hide others of the same type, show the active one
467
+ queueTask(mapInstance, (m) => {
468
+ try {
469
+ const targetLayer = m.getLayer(activeLayer);
470
+ if (!targetLayer) return;
471
+ const style = m.getStyle();
472
+ style.layers.forEach((layer) => {
473
+ if (layer.type === targetLayer.type) {
474
+ m.setLayoutProperty(
475
+ layer.id,
476
+ "visibility",
477
+ layer.id === activeLayer ? "visible" : "none"
478
+ );
479
+ }
480
+ });
481
+ } catch (error) {
482
+ console.error(`Error setting active layer ${activeLayer}:`, error);
483
+ }
484
+ });
485
+ })
486
+ .onPropChanged("visibleLayers", ({ mapInstance, visibleLayers }) => {
487
+ if (!mapInstance) return;
488
+ queueTask(mapInstance, (m) => {
489
+ const style = m.getStyle();
490
+ if (!style?.layers) return;
491
+ style.layers.forEach((layer) => {
492
+ try {
493
+ // Only layers with a layout property (skip required base layers)
494
+ if (layer.layout !== undefined) {
495
+ const shouldBeVisible = visibleLayers?.includes(layer.id);
496
+ const currentVisibility = m.getLayoutProperty(
497
+ layer.id,
498
+ "visibility"
499
+ );
500
+ if (currentVisibility !== (shouldBeVisible ? "visible" : "none")) {
501
+ m.setLayoutProperty(
502
+ layer.id,
503
+ "visibility",
504
+ shouldBeVisible ? "visible" : "none"
505
+ );
506
+ }
507
+ }
508
+ } catch (error) {
509
+ console.warn(
510
+ `Could not set visibility for layer ${layer.id}:`,
511
+ error
512
+ );
513
+ }
514
+ });
515
+ });
516
+ })
517
+ .onPropChanged(
518
+ ["layers", "mapInstance"],
519
+ ({ layers, mapInstance, activeLayer }) => {
520
+ if (mapInstance && layers) {
521
+ // Remove existing layers
522
+ const existingLayers = mapInstance.getStyle()?.layers || [];
523
+ existingLayers.forEach((layer) => {
524
+ if (layer.id.startsWith("custom-layer-")) {
525
+ mapInstance.removeLayer(layer.id);
526
+ }
527
+ });
528
+
529
+ // Add the new layers
530
+ Object.entries(layers).forEach(
531
+ ([layerId, layerConfig]: [string, any]) => {
532
+ const fullLayerId = `custom-layer-${layerId}`;
533
+
534
+ // Add the source if it's missing
535
+ if (!mapInstance.getSource(fullLayerId)) {
536
+ mapInstance.addSource(fullLayerId, layerConfig.source);
537
+ }
538
+
539
+ // Add the layer
540
+ mapInstance.addLayer({
541
+ id: fullLayerId,
542
+ ...layerConfig.layer,
543
+ source: fullLayerId,
544
+ layout: {
545
+ ...layerConfig.layer.layout,
546
+ visibility: layerId === activeLayer ? "visible" : "none",
547
+ },
548
+ });
549
+ }
550
+ );
551
+ }
552
+ }
553
+ )
554
+ .onDisconnected(
555
+ ({
556
+ mapInstance,
557
+ shadowRoot,
558
+ isMoving,
559
+ holdStartCb,
560
+ holdEndCb,
561
+ holdDragstartCb,
562
+ holdDragendCb,
563
+ }) => {
564
+ if (isMoving) return;
565
+ if (mapInstance) {
566
+ // Drop holdstart / holdend listeners from click-hold
567
+ mapInstance.off("mousedown", holdStartCb);
568
+ mapInstance.off("mouseup", holdEndCb);
569
+ mapInstance.off("touchstart", holdStartCb);
570
+ mapInstance.off("touchend", holdEndCb);
571
+ mapInstance.off("touchmove", holdEndCb);
572
+ mapInstance.off("touchcancel", holdEndCb);
573
+ mapInstance.off("dragstart", holdDragstartCb);
574
+ mapInstance.off("dragend", holdDragendCb);
575
+ // `remove()` drops built-in listeners (movestart, moveend, load, click)
576
+ mapInstance.remove();
577
+ }
578
+ if (shadowRoot) {
579
+ shadowRoot.innerHTML = "";
580
+ }
581
+ }
582
+ );
package/package.json ADDED
@@ -0,0 +1,46 @@
1
+ {
2
+ "name": "@excom/mapbox-view",
3
+ "version": "0.1.0",
4
+ "description": "<mapbox-view> custom element",
5
+ "license": "MIT",
6
+ "engines": {
7
+ "node": ">=24.13.0"
8
+ },
9
+ "type": "module",
10
+ "dependencies": {
11
+ "mapbox-gl": "^3.9.2",
12
+ "@excom/neutron": "^0.1.0",
13
+ "@excom/kit-utils": "^0.1.0"
14
+ },
15
+ "peerDependencies": {},
16
+ "devDependencies": {
17
+ "@excom/heft-rig": "^0.1.0"
18
+ },
19
+ "repository": {
20
+ "url": "excom-dev/nucleus",
21
+ "directory": "packages/mapbox-view"
22
+ },
23
+ "homepage": "https://github.com/excom-dev/nucleus/tree/main/packages/mapbox-view/support/docs/README.md",
24
+ "bugs": "https://github.com/excom-dev/nucleus/issues",
25
+ "keywords": [
26
+ "mapbox-view",
27
+ "neutron",
28
+ "custom-elements"
29
+ ],
30
+ "excom": {
31
+ "coverageThreshold": false,
32
+ "documented": false,
33
+ "packageType": "kit-element"
34
+ },
35
+ "scripts": {
36
+ "build": "node node_modules/@excom/heft-rig/scripts/vite-build.mjs",
37
+ "build:watch": "node node_modules/@excom/heft-rig/scripts/vite-build-watch.mjs",
38
+ "format": "node node_modules/@excom/heft-rig/scripts/format.mjs",
39
+ "test": "node node_modules/@excom/heft-rig/scripts/vitest.mjs",
40
+ "coverage": "node node_modules/@excom/heft-rig/scripts/coverage.mjs",
41
+ "dev": "node node_modules/@excom/heft-rig/scripts/vite-dev.mjs",
42
+ "preview": "node node_modules/@excom/heft-rig/scripts/vite-preview.mjs",
43
+ "build:package-metas": "node node_modules/@excom/heft-rig/scripts/build-package-metas.mjs",
44
+ "build:docs": "node node_modules/@excom/heft-rig/scripts/build-docs.mjs"
45
+ }
46
+ }
@@ -0,0 +1 @@
1
+ Caching has been disabled for this project's "apply-exports" command.
@@ -0,0 +1 @@
1
+ Invoking: cd "$RUSH_PROJECT_FOLDER" && node ../heft-rig/scripts/apply-exports.mjs
@@ -0,0 +1 @@
1
+ This project does not define the caching behavior of the "build:docs" command, so caching has been disabled.
@@ -0,0 +1 @@
1
+ Invoking: node node_modules/@excom/heft-rig/scripts/build-docs.mjs
@@ -0,0 +1 @@
1
+ This project does not define the caching behavior of the "build:package-metas" command, so caching has been disabled.
@@ -0,0 +1 @@
1
+ Invoking: node node_modules/@excom/heft-rig/scripts/build-package-metas.mjs
@@ -0,0 +1,10 @@
1
+ /* Aliases */
2
+ @custom-selector :--mapbox-view mapbox-view, .tag-mapbox-view;
3
+
4
+ @define-mixin module-mapbox-view {
5
+ :--mapbox-view {
6
+ display: block;
7
+ height: 100%;
8
+ width: 100%;
9
+ }
10
+ }
package/src/types.ts ADDED
@@ -0,0 +1,32 @@
1
+ // Import mapbox-gl as `type` only. Never bundle it.
2
+ import type mapboxglDefault from "mapbox-gl";
3
+
4
+ export type TMapboxgl = typeof mapboxglDefault;
5
+ export type TMapboxglMarkerOptions = mapboxgl.MarkerOptions;
6
+ export type TMapTouchEvent = mapboxgl.MapTouchEvent;
7
+ export type TCameraOptions = mapboxgl.CameraOptions;
8
+ export type TAnimationOptions = mapboxgl.AnimationOptions;
9
+ export type MapInstance = InstanceType<TMapboxgl["Map"]>;
10
+
11
+ export interface LayerSource {
12
+ type: string;
13
+ data?: any;
14
+ url?: string;
15
+ tiles?: string[];
16
+ [key: string]: any;
17
+ }
18
+
19
+ export interface LayerConfig {
20
+ source: LayerSource;
21
+ layer: {
22
+ type: string;
23
+ paint?: Record<string, any>;
24
+ layout?: Record<string, any>;
25
+ filter?: any[];
26
+ [key: string]: any;
27
+ };
28
+ }
29
+
30
+ export interface LayersConfig {
31
+ [layerId: string]: LayerConfig;
32
+ }
package/src/utils.ts ADDED
@@ -0,0 +1,78 @@
1
+ import type { MapInstance } from "./types";
2
+ import type { TMapboxgl } from "./types";
3
+ import { isNumber } from "@excom/kit-utils";
4
+
5
+ export const EASINGS = {
6
+ // Slow start, then speed up
7
+ ["ease-in-cubic"]: function (t) {
8
+ return t * t * t;
9
+ },
10
+ // Fast start, long slow wind-down
11
+ ["ease-out-quint"]: function (t) {
12
+ return 1 - Math.pow(1 - t, 5);
13
+ },
14
+ // Slow start and finish, fast middle
15
+ ["ease-in-out-circ"]: function (t) {
16
+ return t < 0.5
17
+ ? (1 - Math.sqrt(1 - Math.pow(2 * t, 2))) / 2
18
+ : (Math.sqrt(1 - Math.pow(-2 * t + 2, 2)) + 1) / 2;
19
+ },
20
+ // Fast start, bounce at the end
21
+ ["ease-out-bounce"]: function (t) {
22
+ const n1 = 7.5625;
23
+ const d1 = 2.75;
24
+
25
+ if (t < 1 / d1) {
26
+ return n1 * t * t;
27
+ } else if (t < 2 / d1) {
28
+ return n1 * (t -= 1.5 / d1) * t + 0.75;
29
+ } else if (t < 2.5 / d1) {
30
+ return n1 * (t -= 2.25 / d1) * t + 0.9375;
31
+ } else {
32
+ return n1 * (t -= 2.625 / d1) * t + 0.984375;
33
+ }
34
+ },
35
+ };
36
+ export const parseCoordsFromTokens = (
37
+ tokens: string[] | null = [],
38
+ offsetTokens: string[] | null = []
39
+ ): [number, number] | undefined => {
40
+ if (tokens?.length === 2) {
41
+ const long = Number(tokens[0]);
42
+ const lat = Number(tokens[1]);
43
+ if (isNumber(long) && isNumber(lat)) {
44
+ const offset = parseCoordsFromTokens(offsetTokens);
45
+ if (offset) {
46
+ return [long + offset[0], lat + offset[1]];
47
+ } else {
48
+ return [long, lat];
49
+ }
50
+ }
51
+ }
52
+ return undefined;
53
+ };
54
+
55
+ export const parseNumsFromTokens = (
56
+ tokens: string[] | null = []
57
+ ): number[] | undefined => {
58
+ const parsed = tokens?.map((t) => Number(t));
59
+ if (parsed?.every(isNumber)) {
60
+ return parsed as number[];
61
+ }
62
+ return undefined;
63
+ };
64
+
65
+ export const queueTask = (
66
+ mapInstance?: MapInstance | null,
67
+ task?: (m: MapInstance, mapboxgl: TMapboxgl) => any
68
+ ) => {
69
+ if (mapInstance) {
70
+ if (mapInstance._loaded) {
71
+ task?.(mapInstance, window["mapboxgl"] as unknown as TMapboxgl);
72
+ } else {
73
+ mapInstance.on("load", () => {
74
+ task?.(mapInstance, window["mapboxgl"] as unknown as TMapboxgl);
75
+ });
76
+ }
77
+ }
78
+ };
@@ -0,0 +1,77 @@
1
+ {
2
+ "schemaVersion": "1.0.0",
3
+ "modules": [
4
+ {
5
+ "kind": "javascript-module",
6
+ "path": "mapbox-view.ts",
7
+ "declarations": [
8
+ {
9
+ "kind": "class",
10
+ "name": "MapboxView",
11
+ "customElement": true,
12
+ "tagName": "mapbox-view",
13
+ "_neutron": {
14
+ "cssAliases": [
15
+ {
16
+ "name": ":--mapbox-view",
17
+ "selectors": [
18
+ "mapbox-view",
19
+ ".tag-mapbox-view"
20
+ ],
21
+ "kind": "element"
22
+ }
23
+ ]
24
+ }
25
+ }
26
+ ],
27
+ "exports": [
28
+ {
29
+ "kind": "js",
30
+ "name": "MapboxView",
31
+ "declaration": {
32
+ "name": "MapboxView",
33
+ "module": "mapbox-view.ts"
34
+ }
35
+ },
36
+ {
37
+ "kind": "custom-element-definition",
38
+ "name": "mapbox-view",
39
+ "declaration": {
40
+ "name": "MapboxView",
41
+ "module": "mapbox-view.ts"
42
+ }
43
+ }
44
+ ]
45
+ },
46
+ {
47
+ "kind": "javascript-module",
48
+ "path": "mapbox-view-marker.ts",
49
+ "declarations": [
50
+ {
51
+ "kind": "class",
52
+ "name": "MapboxViewMarker",
53
+ "customElement": true,
54
+ "tagName": "mapbox-view-marker"
55
+ }
56
+ ],
57
+ "exports": [
58
+ {
59
+ "kind": "js",
60
+ "name": "MapboxViewMarker",
61
+ "declaration": {
62
+ "name": "MapboxViewMarker",
63
+ "module": "mapbox-view-marker.ts"
64
+ }
65
+ },
66
+ {
67
+ "kind": "custom-element-definition",
68
+ "name": "mapbox-view-marker",
69
+ "declaration": {
70
+ "name": "MapboxViewMarker",
71
+ "module": "mapbox-view-marker.ts"
72
+ }
73
+ }
74
+ ]
75
+ }
76
+ ]
77
+ }
@@ -0,0 +1,3 @@
1
+ # `<mapbox-view-marker>`
2
+
3
+ **Tag:** `<mapbox-view-marker>`
@@ -0,0 +1,33 @@
1
+ # `<mapbox-view>`
2
+
3
+ **Tag:** `<mapbox-view>`
4
+
5
+ ## Installation
6
+
7
+ `@excom/mapbox-view` v0.1.0
8
+
9
+ ```bash
10
+ pnpm add @excom/mapbox-view
11
+ ```
12
+
13
+ ```bash
14
+ npm install @excom/mapbox-view
15
+ ```
16
+
17
+ ```bash
18
+ yarn add @excom/mapbox-view
19
+ ```
20
+
21
+ ### Import
22
+
23
+ ```ts
24
+ import "@excom/mapbox-view";
25
+ ```
26
+
27
+ ## API
28
+
29
+ ### CSS Aliases
30
+
31
+ | Alias | Kind | Matches | Description |
32
+ | --- | --- | --- | --- |
33
+ | `:--mapbox-view` | element | `mapbox-view`, `.tag-mapbox-view` | |
@@ -0,0 +1,41 @@
1
+ import {
2
+ parseCoordsFromTokens,
3
+ parseNumsFromTokens,
4
+ queueTask,
5
+ } from "../../src/utils";
6
+ import {
7
+ describe,
8
+ expect,
9
+ it,
10
+ vi,
11
+ } from "@excom/heft-rig/node_modules/vitest";
12
+
13
+ describe("mapbox-view utils", () => {
14
+ it("parses coordinate tokens with offsets", () => {
15
+ const result = parseCoordsFromTokens(["1", "2"], ["3", "4"]);
16
+ expect(result).toEqual([4, 6]);
17
+ });
18
+
19
+ it("parses numeric tokens", () => {
20
+ expect(parseNumsFromTokens(["1", "2", "3"])).toEqual([1, 2, 3]);
21
+ expect(parseNumsFromTokens(["1", "x"])).toBeUndefined();
22
+ });
23
+
24
+ it("queues tasks until map is loaded", () => {
25
+ const task = vi.fn();
26
+ (window as unknown as { mapboxgl?: object }).mapboxgl = { version: "1" };
27
+ const loadedMap = { _loaded: true, on: vi.fn() };
28
+ queueTask(loadedMap as any, task);
29
+ expect(task).toHaveBeenCalledWith(loadedMap, (window as any).mapboxgl);
30
+
31
+ const on = vi.fn((_, cb: () => void) => cb());
32
+ const pendingMap = { _loaded: false, on };
33
+ const pendingTask = vi.fn();
34
+ queueTask(pendingMap as any, pendingTask);
35
+ expect(on).toHaveBeenCalled();
36
+ expect(pendingTask).toHaveBeenCalledWith(
37
+ pendingMap,
38
+ (window as any).mapboxgl
39
+ );
40
+ });
41
+ });
package/tsconfig.json ADDED
@@ -0,0 +1,11 @@
1
+ {
2
+ "extends": "@excom/heft-rig/profiles/default/config/tsconfig.json",
3
+ "include": ["./*.ts"],
4
+ "exclude": ["node_modules", "dist", "**/*.test.ts", "**/*.spec.ts"],
5
+ "compilerOptions": {
6
+ "declaration": false,
7
+ "declarationMap": false,
8
+ "emitDeclarationOnly": false,
9
+ "noEmit": true
10
+ }
11
+ }