@vis.gl/react-google-maps
Advanced tools
Comparing version 0.2.1 to 0.3.0
@@ -8,5 +8,31 @@ /// <reference types="google.maps" /> | ||
/** | ||
* Handlers for all events that could be emitted by map-instances. | ||
*/ | ||
type MapEventProps = Partial<{ | ||
onBoundsChanged: (event: MapCameraChangedEvent) => void; | ||
onCenterChanged: (event: MapCameraChangedEvent) => void; | ||
onHeadingChanged: (event: MapCameraChangedEvent) => void; | ||
onTiltChanged: (event: MapCameraChangedEvent) => void; | ||
onZoomChanged: (event: MapCameraChangedEvent) => void; | ||
onProjectionChanged: (event: MapCameraChangedEvent) => void; | ||
onClick: (event: MapMouseEvent) => void; | ||
onDblclick: (event: MapMouseEvent) => void; | ||
onContextmenu: (event: MapMouseEvent) => void; | ||
onMousemove: (event: MapMouseEvent) => void; | ||
onMouseover: (event: MapMouseEvent) => void; | ||
onMouseout: (event: MapMouseEvent) => void; | ||
onDrag: (event: MapEvent) => void; | ||
onDragend: (event: MapEvent) => void; | ||
onDragstart: (event: MapEvent) => void; | ||
onTilesLoaded: (event: MapEvent) => void; | ||
onIdle: (event: MapEvent) => void; | ||
onIsFractionalZoomEnabledChanged: (event: MapEvent) => void; | ||
onMapCapabilitiesChanged: (event: MapEvent) => void; | ||
onMapTypeIdChanged: (event: MapEvent) => void; | ||
onRenderingTypeChanged: (event: MapEvent) => void; | ||
}>; | ||
/** | ||
* Props for the Google Maps Map Component | ||
*/ | ||
export type MapProps = google.maps.MapOptions & { | ||
export type MapProps = google.maps.MapOptions & MapEventProps & { | ||
style?: CSSProperties; | ||
@@ -28,6 +54,2 @@ /** | ||
/** | ||
* A callback function that is called, when the Google Map is loaded. | ||
*/ | ||
onLoadMap?: (map: google.maps.Map) => void; | ||
/** | ||
* Viewport from deck.gl | ||
@@ -52,1 +74,22 @@ */ | ||
}; | ||
export type MapEvent<T = unknown> = { | ||
type: string; | ||
map: google.maps.Map; | ||
detail: T; | ||
stoppable: boolean; | ||
stop: () => void; | ||
domEvent?: MouseEvent | TouchEvent | PointerEvent | KeyboardEvent | Event; | ||
}; | ||
export type MapMouseEvent = MapEvent<{ | ||
latLng: google.maps.LatLngLiteral | null; | ||
placeId: string | null; | ||
}>; | ||
export type MapCameraChangedEvent = MapEvent<{ | ||
center: google.maps.LatLngLiteral; | ||
bounds: google.maps.LatLngBoundsLiteral; | ||
zoom: number; | ||
heading: number; | ||
tilt: number; | ||
}>; | ||
export declare function createMapEvent(type: string, map: google.maps.Map, srcEvent?: google.maps.MapMouseEvent | google.maps.IconMouseEvent): MapEvent; | ||
export {}; |
@@ -36,2 +36,33 @@ (function (global, factory) { | ||
} | ||
function _unsupportedIterableToArray(o, minLen) { | ||
if (!o) return; | ||
if (typeof o === "string") return _arrayLikeToArray(o, minLen); | ||
var n = Object.prototype.toString.call(o).slice(8, -1); | ||
if (n === "Object" && o.constructor) n = o.constructor.name; | ||
if (n === "Map" || n === "Set") return Array.from(o); | ||
if (n === "Arguments" || /^(?:Ui|I)nt(?:8|16|32)(?:Clamped)?Array$/.test(n)) return _arrayLikeToArray(o, minLen); | ||
} | ||
function _arrayLikeToArray(arr, len) { | ||
if (len == null || len > arr.length) len = arr.length; | ||
for (var i = 0, arr2 = new Array(len); i < len; i++) arr2[i] = arr[i]; | ||
return arr2; | ||
} | ||
function _createForOfIteratorHelperLoose(o, allowArrayLike) { | ||
var it = typeof Symbol !== "undefined" && o[Symbol.iterator] || o["@@iterator"]; | ||
if (it) return (it = it.call(o)).next.bind(it); | ||
if (Array.isArray(o) || (it = _unsupportedIterableToArray(o)) || allowArrayLike && o && typeof o.length === "number") { | ||
if (it) o = it; | ||
var i = 0; | ||
return function () { | ||
if (i >= o.length) return { | ||
done: true | ||
}; | ||
return { | ||
done: false, | ||
value: o[i++] | ||
}; | ||
}; | ||
} | ||
throw new TypeError("Invalid attempt to iterate non-iterable instance.\nIn order to be iterable, non-array objects must have a [Symbol.iterator]() method."); | ||
} | ||
function _toPrimitive(input, hint) { | ||
@@ -646,6 +677,45 @@ if (typeof input !== "object" || input === null) return input; | ||
var _excluded$2 = ["id", "initialBounds", "onLoadMap"], | ||
_excluded2 = ["center", "zoom", "heading", "tilt", "mapId"]; | ||
function useCallbackRef() { | ||
var _useState = React.useState(null), | ||
el = _useState[0], | ||
setEl = _useState[1]; | ||
var ref = React.useCallback(function (value) { | ||
return setEl(value); | ||
}, [setEl]); | ||
return [el, ref]; | ||
} | ||
var _excluded$2 = ["id", "initialBounds"], | ||
_excluded2 = ["center", "zoom", "heading", "tilt"], | ||
_excluded3 = ["mapId"]; | ||
var GoogleMapsContext = React__default["default"].createContext(null); | ||
/** | ||
* Maps the camelCased names of event-props to the corresponding event-types | ||
* used in the maps API. | ||
*/ | ||
var propNameToEventType = { | ||
onBoundsChanged: 'bounds_changed', | ||
onCenterChanged: 'center_changed', | ||
onClick: 'click', | ||
onContextmenu: 'contextmenu', | ||
onDblclick: 'dblclick', | ||
onDrag: 'drag', | ||
onDragend: 'dragend', | ||
onDragstart: 'dragstart', | ||
onHeadingChanged: 'heading_changed', | ||
onIdle: 'idle', | ||
onIsFractionalZoomEnabledChanged: 'isfractionalzoomenabled_changed', | ||
onMapCapabilitiesChanged: 'mapcapabilities_changed', | ||
onMapTypeIdChanged: 'maptypeid_changed', | ||
onMousemove: 'mousemove', | ||
onMouseout: 'mouseout', | ||
onMouseover: 'mouseover', | ||
onProjectionChanged: 'projection_changed', | ||
onRenderingTypeChanged: 'renderingtype_changed', | ||
onTilesLoaded: 'tilesloaded', | ||
onTiltChanged: 'tilt_changed', | ||
onZoomChanged: 'zoom_changed' | ||
}; | ||
var eventPropNames = Object.freeze(Object.keys(propNameToEventType)); | ||
/** | ||
* Component to render a Google Maps map | ||
@@ -664,5 +734,7 @@ */ | ||
} | ||
var _useMapInstanceHandle = useMapInstanceHandlerEffects(props, context), | ||
map = _useMapInstanceHandle[0], | ||
mapRef = _useMapInstanceHandle[1]; | ||
var _useMapInstanceEffect = useMapInstanceEffects(props, context), | ||
map = _useMapInstanceEffect[0], | ||
mapRef = _useMapInstanceEffect[1]; | ||
useMapOptionsEffects(map, props); | ||
useMapEvents(map, props); | ||
useDeckGLCameraUpdateEffect(map, viewState); | ||
@@ -701,3 +773,3 @@ var isViewportSet = React.useMemo(function () { | ||
*/ | ||
function useMapInstanceHandlerEffects(props, context) { | ||
function useMapInstanceEffects(props, context) { | ||
var apiIsLoaded = useApiIsLoaded(); | ||
@@ -707,21 +779,11 @@ var _useState = React.useState(null), | ||
setMap = _useState[1]; | ||
var _useState2 = React.useState(null), | ||
container = _useState2[0], | ||
setContainer = _useState2[1]; | ||
var _useCallbackRef = useCallbackRef(), | ||
container = _useCallbackRef[0], | ||
containerRef = _useCallbackRef[1]; | ||
var id = props.id, | ||
initialBounds = props.initialBounds, | ||
onLoadMap = props.onLoadMap, | ||
mapOptions = _objectWithoutPropertiesLoose(props, _excluded$2); | ||
var mapRef = React.useCallback(function (node) { | ||
setContainer(node || null); | ||
}, []); | ||
// create the map instance and register it in the context | ||
React.useEffect(function () { | ||
// this will be called a couple of times before the dependencies are | ||
// actually ready to create the map | ||
if (!container || !apiIsLoaded) return; | ||
// Since we can't know the map-ids used in sibling components during | ||
// rendering, we can't check for existing maps with the same id here. | ||
// We do have a seperate hook below that keeps an eye on mapIds and will | ||
// write an error-message to the console if reused ids are detected. | ||
var addMapInstance = context.addMapInstance, | ||
@@ -732,7 +794,2 @@ removeMapInstance = context.removeMapInstance; | ||
addMapInstance(newMap, id); | ||
if (onLoadMap) { | ||
google.maps.event.addListenerOnce(newMap, 'idle', function () { | ||
onLoadMap(newMap); | ||
}); | ||
} | ||
if (initialBounds) { | ||
@@ -743,3 +800,4 @@ newMap.fitBounds(initialBounds); | ||
if (!container || !apiIsLoaded) return; | ||
google.maps.event.clearInstanceListeners(container); | ||
// remove all event-listeners to minimize memory-leaks | ||
google.maps.event.clearInstanceListeners(newMap); | ||
setMap(null); | ||
@@ -749,35 +807,12 @@ removeMapInstance(id); | ||
}, | ||
// Dependencies need to be inaccurately limited here. The cleanup function | ||
// will remove the map-instance with all it's internal state, and we can't | ||
// have that happening. This is only ok when the id or mapId is changed, | ||
// since this requires a new map to be created anyway. | ||
// FIXME: we should rethink if it could be possible to keep the state | ||
// around when a map gets re-initialized (id or mapId changed). This | ||
// should keep the viewport as it is (so also no initial viewport in | ||
// this case) and any added features should of course stay as well (though | ||
// that is for those components to figure out to always be attached to | ||
// the correct map-instance from the context) . | ||
// this case) and any added features should of course get re-added as | ||
// well. | ||
// eslint-disable-next-line react-hooks/exhaustive-deps | ||
[id, container, apiIsLoaded, props.mapId]); | ||
// update the map options when mapOptions is changed | ||
React.useEffect(function () { | ||
if (!map) { | ||
return; | ||
} | ||
// FIXME: for now, we have to filter all options describing the viewport | ||
// here, since those are updated in google maps internally or using the | ||
// viewState parameter externally. | ||
// eslint-disable-next-line @typescript-eslint/no-unused-vars | ||
var otherOptions = _objectWithoutPropertiesLoose(mapOptions, _excluded2); | ||
map.setOptions(otherOptions); | ||
}, | ||
// Not triggered when the map is changed, since in that case the | ||
// options have already been passed to the map constructor. | ||
// eslint-disable-next-line react-hooks/exhaustive-deps | ||
[mapOptions]); | ||
// report an error if the same map-id is used multiple times | ||
React.useEffect(function () { | ||
if (!id) { | ||
return; | ||
} | ||
if (!id) return; | ||
var mapInstances = context.mapInstances; | ||
@@ -788,5 +823,79 @@ if (mapInstances[id] && mapInstances[id] !== map) { | ||
}, [id, context, map]); | ||
return [map, mapRef]; | ||
return [map, containerRef]; | ||
} | ||
/** | ||
* Internal hook to update the map-options and view-parameters when | ||
* props are changed. | ||
* @internal | ||
*/ | ||
function useMapOptionsEffects(map, mapProps) { | ||
var center = mapProps.center, | ||
zoom = mapProps.zoom, | ||
heading = mapProps.heading, | ||
tilt = mapProps.tilt, | ||
mapOptions = _objectWithoutPropertiesLoose(mapProps, _excluded2); | ||
/* eslint-disable react-hooks/exhaustive-deps -- | ||
* | ||
* The following effects aren't triggered when the map is changed. | ||
* In that case, the values will be or have been passed to the map | ||
* constructor as mapOptions. | ||
*/ | ||
// update the map options when mapOptions is changed | ||
React.useEffect(function () { | ||
if (!map) return; | ||
// NOTE: passing a mapId to setOptions triggers an error-message we don't need to see here | ||
// eslint-disable-next-line @typescript-eslint/no-unused-vars | ||
var opts = _objectWithoutPropertiesLoose(mapOptions, _excluded3); | ||
map.setOptions(opts); | ||
}, [mapProps]); | ||
React.useEffect(function () { | ||
if (!map || !center) return; | ||
map.setCenter(center); | ||
}, [center]); | ||
React.useEffect(function () { | ||
if (!map || !Number.isFinite(zoom)) return; | ||
map.setZoom(zoom); | ||
}, [zoom]); | ||
React.useEffect(function () { | ||
if (!map || !Number.isFinite(heading)) return; | ||
map.setHeading(heading); | ||
}, [heading]); | ||
React.useEffect(function () { | ||
if (!map || !Number.isFinite(tilt)) return; | ||
map.setTilt(tilt); | ||
}, [tilt]); | ||
/* eslint-enable react-hooks/exhaustive-deps */ | ||
} | ||
/** | ||
* Sets up effects to bind event-handlers for all event-props in MapEventProps. | ||
* @internal | ||
*/ | ||
function useMapEvents(map, props) { | ||
var _loop = function _loop() { | ||
var propName = _step.value; | ||
// fixme: this cast is essentially a 'trust me, bro' for typescript, but | ||
// a proper solution seems way too complicated right now | ||
var handler = props[propName]; | ||
var eventType = propNameToEventType[propName]; | ||
// eslint-disable-next-line react-hooks/rules-of-hooks | ||
React.useEffect(function () { | ||
if (!map) return; | ||
if (!handler) return; | ||
var listener = map.addListener(eventType, function (ev) { | ||
handler(createMapEvent(eventType, map, ev)); | ||
}); | ||
return function () { | ||
return listener.remove(); | ||
}; | ||
}, [map, eventType, handler]); | ||
}; | ||
// note: calling a useEffect hook from within a loop is prohibited by the | ||
// rules of hooks, but it's ok here since it's unconditional and the number | ||
// and order of iterations is always strictly the same. | ||
// (see https://legacy.reactjs.org/docs/hooks-rules.html) | ||
for (var _iterator = _createForOfIteratorHelperLoose(eventPropNames), _step; !(_step = _iterator()).done;) { | ||
_loop(); | ||
} | ||
} | ||
/** | ||
* Internal hook that updates the camera when deck.gl viewState changes. | ||
@@ -824,2 +933,55 @@ * @internal | ||
} | ||
var cameraEventTypes = ['bounds_changed', 'center_changed', 'heading_changed', 'projection_changed', 'tilt_changed', 'zoom_changed']; | ||
var mouseEventTypes = ['click', 'contextmenu', 'dblclick', 'mousemove', 'mouseout', 'mouseover']; | ||
function createMapEvent(type, map, srcEvent) { | ||
var ev = { | ||
type: type, | ||
map: map, | ||
detail: {}, | ||
stoppable: false, | ||
stop: function stop() {} | ||
}; | ||
if (cameraEventTypes.includes(type)) { | ||
var camEvent = ev; | ||
var center = map.getCenter(); | ||
var zoom = map.getZoom(); | ||
var heading = map.getHeading() || 0; | ||
var tilt = map.getTilt() || 0; | ||
var bounds = map.getBounds(); | ||
if (!center || !bounds || !Number.isFinite(zoom)) { | ||
console.warn('[createEvent] at least one of the values from the map ' + 'returned undefined. This is not expected to happen. Please ' + 'report an issue at https://github.com/visgl/react-google-maps/issues/new'); | ||
} | ||
camEvent.detail = { | ||
center: (center == null ? void 0 : center.toJSON()) || { | ||
lat: 0, | ||
lng: 0 | ||
}, | ||
zoom: zoom, | ||
heading: heading, | ||
tilt: tilt, | ||
bounds: (bounds == null ? void 0 : bounds.toJSON()) || { | ||
north: 90, | ||
east: 180, | ||
south: -90, | ||
west: -180 | ||
} | ||
}; | ||
return camEvent; | ||
} else if (mouseEventTypes.includes(type)) { | ||
var _srcEvent$latLng; | ||
if (!srcEvent) throw new Error('[createEvent] mouse events must provide a srcEvent'); | ||
var mouseEvent = ev; | ||
mouseEvent.domEvent = srcEvent.domEvent; | ||
mouseEvent.stoppable = true; | ||
mouseEvent.stop = function () { | ||
return srcEvent.stop(); | ||
}; | ||
mouseEvent.detail = { | ||
latLng: ((_srcEvent$latLng = srcEvent.latLng) == null ? void 0 : _srcEvent$latLng.toJSON()) || null, | ||
placeId: srcEvent.placeId | ||
}; | ||
return mouseEvent; | ||
} | ||
return ev; | ||
} | ||
@@ -1366,2 +1528,3 @@ function useMapsLibrary(name) { | ||
exports.Pin = Pin; | ||
exports.createMapEvent = createMapEvent; | ||
exports.limitTiltRange = limitTiltRange; | ||
@@ -1368,0 +1531,0 @@ exports.useAdvancedMarkerRef = useAdvancedMarkerRef; |
{ | ||
"name": "@vis.gl/react-google-maps", | ||
"version": "0.2.1", | ||
"version": "0.3.0", | ||
"description": "React components and hooks for Google Maps.", | ||
@@ -5,0 +5,0 @@ "source": "src/index.ts", |
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
386767
70
5449