@vinctus/react-maplibre 1.2.0 → 1.3.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/dist/Layer.js CHANGED
@@ -1,31 +1,106 @@
1
- import { useContext, useEffect } from 'react';
1
+ var __rest = (this && this.__rest) || function (s, e) {
2
+ var t = {};
3
+ for (var p in s) if (Object.prototype.hasOwnProperty.call(s, p) && e.indexOf(p) < 0)
4
+ t[p] = s[p];
5
+ if (s != null && typeof Object.getOwnPropertySymbols === "function")
6
+ for (var i = 0, p = Object.getOwnPropertySymbols(s); i < p.length; i++) {
7
+ if (e.indexOf(p[i]) < 0 && Object.prototype.propertyIsEnumerable.call(s, p[i]))
8
+ t[p[i]] = s[p[i]];
9
+ }
10
+ return t;
11
+ };
12
+ import { useContext, useEffect, useRef } from 'react';
2
13
  import { MapContext } from './Map';
14
+ const addSourceAndLayer = (map, id, strings) => {
15
+ const paint = JSON.parse(strings.paintString);
16
+ const layout = JSON.parse(strings.layoutString);
17
+ // The layer must be removed before its source, and the source added before its layer.
18
+ if (map.getLayer(id)) {
19
+ map.removeLayer(id);
20
+ }
21
+ if (map.getSource(id)) {
22
+ map.removeSource(id);
23
+ }
24
+ map.addSource(id, Object.assign(Object.assign({}, JSON.parse(strings.sourceOptionsString)), { data: JSON.parse(strings.dataString) }));
25
+ map.addLayer(Object.assign(Object.assign({ source: id }, JSON.parse(strings.layerStructureString)), { paint, layout }));
26
+ return { dataString: strings.dataString, paint, layout };
27
+ };
28
+ const updateProperties = (previous, next, setProperty) => {
29
+ const keys = new Set([...Object.keys(previous), ...Object.keys(next)]);
30
+ // A key missing from `next` is set to undefined, which resets it to the style default.
31
+ keys.forEach((key) => {
32
+ if (JSON.stringify(previous[key]) !== JSON.stringify(next[key])) {
33
+ setProperty(key, next[key]);
34
+ }
35
+ });
36
+ };
3
37
  export const Layer = ({ layer, source }) => {
4
38
  const { map, mapLoaded } = useContext(MapContext);
5
- const layerString = JSON.stringify(Object.assign({ source: layer.id }, layer));
6
- const sourceString = JSON.stringify(source);
39
+ const { id } = layer;
40
+ const { paint, layout } = layer, layerStructure = __rest(layer, ["paint", "layout"]);
41
+ const { data } = source, sourceOptions = __rest(source, ["data"]);
42
+ const strings = {
43
+ layerStructureString: JSON.stringify(layerStructure),
44
+ paintString: JSON.stringify(paint !== null && paint !== void 0 ? paint : {}),
45
+ layoutString: JSON.stringify(layout !== null && layout !== void 0 ? layout : {}),
46
+ sourceOptionsString: JSON.stringify(sourceOptions),
47
+ dataString: JSON.stringify(data),
48
+ };
49
+ const { layerStructureString, paintString, layoutString, sourceOptionsString, dataString } = strings;
50
+ // Keeps the newest props available to the creation effect without making it depend on them, so
51
+ // that data, paint and layout changes update the existing source and layer in place.
52
+ const latest = useRef(strings);
53
+ const applied = useRef(null);
54
+ latest.current = strings;
7
55
  useEffect(() => {
8
- const parsedSource = JSON.parse(sourceString);
9
- const parsedLayer = JSON.parse(layerString);
10
- if (map && mapLoaded) {
11
- map.addSource(parsedLayer.id, parsedSource); // when adding, the source must be added first, and then the layer
12
- map.addLayer(parsedLayer);
56
+ if (!map || !mapLoaded) {
57
+ return;
13
58
  }
59
+ applied.current = addSourceAndLayer(map, id, latest.current);
14
60
  return () => {
15
61
  try {
16
- if (map && mapLoaded && !map._removed) {
17
- if (map.getLayer(parsedLayer.id)) {
18
- map.removeLayer(parsedLayer.id); // when removing, the layer must be removed first, and then the source
62
+ if (!map._removed) {
63
+ if (map.getLayer(id)) {
64
+ map.removeLayer(id);
19
65
  }
20
- if (map.getSource(parsedLayer.id)) {
21
- map.removeSource(parsedLayer.id);
66
+ if (map.getSource(id)) {
67
+ map.removeSource(id);
22
68
  }
23
69
  }
24
70
  }
25
71
  catch (_a) {
26
72
  console.warn('Error cleaning up Layer');
27
73
  }
74
+ applied.current = null;
28
75
  };
29
- }, [layerString, sourceString, map, mapLoaded]);
76
+ }, [id, layerStructureString, sourceOptionsString, map, mapLoaded]);
77
+ useEffect(() => {
78
+ const current = applied.current;
79
+ if (!map || !mapLoaded || !current) {
80
+ return;
81
+ }
82
+ // The source disappears when the map's style is replaced, so the source and layer are rebuilt
83
+ // from the newest props.
84
+ if (!map.getSource(id)) {
85
+ try {
86
+ applied.current = addSourceAndLayer(map, id, latest.current);
87
+ }
88
+ catch (_a) {
89
+ console.warn('Error restoring Layer');
90
+ }
91
+ return;
92
+ }
93
+ if (current.dataString !== dataString) {
94
+ const geoJsonSource = map.getSource(id);
95
+ geoJsonSource.setData(JSON.parse(dataString));
96
+ current.dataString = dataString;
97
+ }
98
+ const nextPaint = JSON.parse(paintString);
99
+ const nextLayout = JSON.parse(layoutString);
100
+ updateProperties(current.paint, nextPaint, (key, value) => map.setPaintProperty(id, key, value));
101
+ updateProperties(current.layout, nextLayout, (key, value) => map.setLayoutProperty(id, key, value));
102
+ current.paint = nextPaint;
103
+ current.layout = nextLayout;
104
+ }, [id, dataString, paintString, layoutString, map, mapLoaded]);
30
105
  return null;
31
106
  };
package/dist/Marker.js CHANGED
@@ -13,6 +13,14 @@ export const Marker = ({ children, longitude, latitude, className, anchor, color
13
13
  // them, so that a position change moves the existing marker instead of recreating it.
14
14
  const coordinates = useRef([longitude, latitude]);
15
15
  coordinates.current = [longitude, latitude];
16
+ // The popup is compared by its rendered markup because the `popup` element has a new identity on
17
+ // every parent render.
18
+ const popupHtml = popup ? ReactDOMServer.renderToStaticMarkup(popup) : undefined;
19
+ const hasPopup = popupHtml !== undefined;
20
+ const popupHtmlRef = useRef(popupHtml);
21
+ const popupInstance = useRef(null);
22
+ const appliedPopupHtml = useRef(undefined);
23
+ popupHtmlRef.current = popupHtml;
16
24
  useEffect(() => {
17
25
  if (!map || !mapLoaded) {
18
26
  return;
@@ -43,7 +51,8 @@ export const Marker = ({ children, longitude, latitude, className, anchor, color
43
51
  }
44
52
  }, [marker, latitude, longitude]);
45
53
  useEffect(() => {
46
- if (!marker || !popup) {
54
+ var _a;
55
+ if (!marker || !hasPopup) {
47
56
  return;
48
57
  }
49
58
  const newPopup = new maplibre.Popup({
@@ -51,7 +60,9 @@ export const Marker = ({ children, longitude, latitude, className, anchor, color
51
60
  closeButton,
52
61
  closeOnClick,
53
62
  className: popupClassName,
54
- }).setHTML(ReactDOMServer.renderToStaticMarkup(popup));
63
+ }).setHTML((_a = popupHtmlRef.current) !== null && _a !== void 0 ? _a : '');
64
+ popupInstance.current = newPopup;
65
+ appliedPopupHtml.current = popupHtmlRef.current;
55
66
  // Attaching the popup to the marker lets MapLibre toggle it on click and keep it anchored to
56
67
  // the marker as the marker moves.
57
68
  marker.setPopup(newPopup);
@@ -63,7 +74,17 @@ export const Marker = ({ children, longitude, latitude, className, anchor, color
63
74
  catch (_a) {
64
75
  console.warn('Error cleaning up Marker popup');
65
76
  }
77
+ popupInstance.current = null;
78
+ appliedPopupHtml.current = undefined;
66
79
  };
67
- }, [marker, popup, popupClassName, closeButton, closeOnClick]);
80
+ }, [marker, hasPopup, popupClassName, closeButton, closeOnClick]);
81
+ // Updating the content in place keeps an open popup open across parent re-renders.
82
+ useEffect(() => {
83
+ const existingPopup = popupInstance.current;
84
+ if (existingPopup && popupHtml !== undefined && popupHtml !== appliedPopupHtml.current) {
85
+ existingPopup.setHTML(popupHtml);
86
+ appliedPopupHtml.current = popupHtml;
87
+ }
88
+ }, [popupHtml]);
68
89
  return createPortal(children, element);
69
90
  };
package/dist/Popup.js CHANGED
@@ -1,27 +1,57 @@
1
- import { useEffect, useContext } from 'react';
1
+ import { useEffect, useContext, useRef } from 'react';
2
2
  import maplibre from 'maplibre-gl';
3
3
  import ReactDOMServer from 'react-dom/server';
4
4
  import { MapContext } from './Map';
5
5
  export const Popup = ({ children, longitude, latitude, className, closeOnClick, closeButton, }) => {
6
6
  const { map, mapLoaded } = useContext(MapContext);
7
+ // The content is compared by its rendered markup because `children` has a new identity on every
8
+ // parent render.
9
+ const html = ReactDOMServer.renderToStaticMarkup(children);
10
+ // Keep the newest props available to the creation effect without making it depend on them, so
11
+ // that content and position changes update the existing popup instead of recreating it.
12
+ const latest = useRef({ html, coordinates: [longitude, latitude] });
13
+ const popupInstance = useRef(null);
14
+ const applied = useRef(null);
15
+ latest.current = { html, coordinates: [longitude, latitude] };
7
16
  useEffect(() => {
8
- let popup = null;
9
- if (map && mapLoaded) {
10
- popup = new maplibre.Popup({ offset: [0, -15], className, closeOnClick, closeButton })
11
- .setLngLat([longitude, latitude])
12
- .setHTML(ReactDOMServer.renderToStaticMarkup(children))
13
- .addTo(map);
17
+ if (!map || !mapLoaded) {
18
+ return;
14
19
  }
20
+ const { html: initialHtml, coordinates } = latest.current;
21
+ const popup = new maplibre.Popup({ offset: [0, -15], className, closeOnClick, closeButton })
22
+ .setLngLat(coordinates)
23
+ .setHTML(initialHtml)
24
+ .addTo(map);
25
+ popupInstance.current = popup;
26
+ applied.current = { html: initialHtml, coordinates };
15
27
  return () => {
16
28
  try {
17
- if (popup) {
18
- popup.remove();
19
- }
29
+ popup.remove();
20
30
  }
21
31
  catch (_a) {
22
32
  console.warn('Error cleaning up Popup');
23
33
  }
34
+ popupInstance.current = null;
35
+ applied.current = null;
24
36
  };
25
- }, [children, latitude, longitude, map, className, closeOnClick, closeButton, mapLoaded]);
37
+ }, [map, mapLoaded, className, closeOnClick, closeButton]);
38
+ useEffect(() => {
39
+ const popup = popupInstance.current;
40
+ const current = applied.current;
41
+ if (popup &&
42
+ current &&
43
+ (current.coordinates[0] !== longitude || current.coordinates[1] !== latitude)) {
44
+ popup.setLngLat([longitude, latitude]);
45
+ current.coordinates = [longitude, latitude];
46
+ }
47
+ }, [longitude, latitude]);
48
+ useEffect(() => {
49
+ const popup = popupInstance.current;
50
+ const current = applied.current;
51
+ if (popup && current && current.html !== html) {
52
+ popup.setHTML(html);
53
+ current.html = html;
54
+ }
55
+ }, [html]);
26
56
  return null;
27
57
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@vinctus/react-maplibre",
3
- "version": "1.2.0",
3
+ "version": "1.3.0",
4
4
  "description": "Minimalist MapLibre GL JS React wrapper library",
5
5
  "type": "module",
6
6
  "types": "dist/index.d.ts",
@@ -1 +0,0 @@
1
- {"root":["../src/errorboundary.tsx","../src/geolocatecontrol.tsx","../src/layer.tsx","../src/map.tsx","../src/marker.tsx","../src/navigationcontrol.tsx","../src/popup.tsx","../src/scalecontrol.tsx","../src/index.ts"],"version":"5.8.2"}