UNPKG

@amicaldo/strapi-google-maps

Version:

A Google Maps custom field for Strapi, allowing you to pick a location.

316 lines (315 loc) 10.2 kB
import { jsx, jsxs, Fragment } from "react/jsx-runtime"; import { useState, useReducer, useEffect, useRef } from "react"; import { Loader, Grid, TextInput, Typography, Box, Button } from "@strapi/design-system"; import { useIntl } from "react-intl"; import { u as useConfig } from "./useConfig-BqNqw-Xi.mjs"; import { StandaloneSearchBox, LoadScript, GoogleMap, Marker } from "@react-google-maps/api"; import { ArrowClockwise } from "@strapi/icons"; import Geohash from "latlon-geohash"; import { useGeolocated } from "react-geolocated"; const noPoint = { lat: NaN, lng: NaN }; const isValidPoint = (point) => { return !isNaN(point.lat) && !isNaN(point.lng); }; const isSamePoint = (point1, point2) => { return point1.lat === point2.lat && point1.lng === point2.lng; }; function Search({ userCoords, currentAddress, onPlaceSelected, onAddressEdited }) { const { formatMessage } = useIntl(); const bounds = !!userCoords ? new google.maps.LatLngBounds({ lat: userCoords.latitude, lng: userCoords.longitude }) : void 0; const [searchBox, setSearchBox] = useState(); const onPlacesChanged = () => { const results = searchBox?.getPlaces(); if (results && results.length) { const place = results[0]; onPlaceSelected({ address: place.formatted_address || "", coordinates: place.geometry?.location?.toJSON() }); } }; return /* @__PURE__ */ jsx( StandaloneSearchBox, { bounds, onLoad: (ref) => setSearchBox(ref), onPlacesChanged, children: /* @__PURE__ */ jsx( "input", { type: "text", placeholder: formatMessage({ id: "google-maps.input.search.placeholder", defaultMessage: "Search for a place" }), value: currentAddress, onChange: (e) => onAddressEdited(e.target.value), style: { boxSizing: `border-box`, border: `1px solid transparent`, width: `300px`, height: `40px`, padding: `0 12px`, borderRadius: `3px`, boxShadow: `0 2px 6px rgba(0, 0, 0, 0.3)`, fontSize: `14px`, outline: `none`, textOverflow: `ellipses`, position: "absolute", left: "50%", marginLeft: "-140px", marginTop: "10px" } } ) } ); } const fallbackCenter = { lat: 51.51652494189269, lng: 7.45560626859687 }; const libraries = ["places"]; function MapView({ children, config, focusPoint, currentAddress, onCoordsChange, onAddressChange }) { const [scriptLoaded, onScriptLoaded] = useReducer(() => true, false); const [center, setCenter] = useState(fallbackCenter); const { coords: userCoords } = useGeolocated({ positionOptions: { enableHighAccuracy: false } }); useEffect(() => { if (focusPoint) { setCenter(focusPoint); } else if (userCoords) { setCenter({ lat: userCoords.latitude, lng: userCoords.longitude }); } else { setCenter(fallbackCenter); } }, [focusPoint, userCoords]); const onPlaceSelected = (place) => { onCoordsChange({ origin: "placeSearch", value: place.coordinates }); onAddressChange(place.address); }; return /* @__PURE__ */ jsxs(Fragment, { children: [ !!config && /* @__PURE__ */ jsx( LoadScript, { googleMapsApiKey: config.googleMapsKey, libraries, onLoad: onScriptLoaded } ), scriptLoaded ? /* @__PURE__ */ jsxs( GoogleMap, { mapContainerStyle: { width: "100%", height: "400px" }, center, zoom: 20, onClick: ({ latLng }) => onCoordsChange({ origin: "map", value: latLng?.toJSON() }), children: [ /* @__PURE__ */ jsx( Search, { userCoords, currentAddress, onPlaceSelected, onAddressEdited: onAddressChange } ), children ] } ) : /* @__PURE__ */ jsx("div", { style: { display: "flex", justifyContent: "center" }, children: /* @__PURE__ */ jsx(Loader, { small: true }) }) ] }); } function generateId(len) { function dec2hex(dec) { return dec.toString(16).padStart(2, "0"); } var arr = new Uint8Array(len / 2); window.crypto.getRandomValues(arr); return Array.from(arr, dec2hex).join(""); } function NumberFields({ coords, onChange }) { const windowInputValueDescriptor = Object.getOwnPropertyDescriptor( window.HTMLInputElement.prototype, "value" ); if (!windowInputValueDescriptor) return null; const latInputId = generateId(10); const lngInputId = generateId(10); const { lat, lng } = coords; useEffect(() => { const latInput = document.getElementById(latInputId); const lngInput = document.getElementById(lngInputId); const setInputValueNatively = (input, value) => windowInputValueDescriptor.set.call(input, isNaN(value) ? null : value); setInputValueNatively(latInput, lat); setInputValueNatively(lngInput, lng); const changeEvent = new Event("change", { bubbles: true }); latInput.dispatchEvent(changeEvent); lngInput.dispatchEvent(changeEvent); }, [lat, lng]); return /* @__PURE__ */ jsxs(Grid.Root, { gap: 3, children: [ /* @__PURE__ */ jsx(Grid.Item, { col: 6, children: /* @__PURE__ */ jsx( TextInput, { id: latInputId, placeholder: "Latitude", "aria-label": "Latitude", hint: "Latitude", name: "latitude", onChange: (e) => { if (!e.target.value) return; onChange({ lat: Number(e.target.value), lng }); }, size: "S" } ) }), /* @__PURE__ */ jsx(Grid.Item, { col: 6, children: /* @__PURE__ */ jsx( TextInput, { id: lngInputId, placeholder: "Longtitude", "aria-label": "Longtitude", hint: "Longtitude", name: "longtitude", onChange: (e) => { if (!e.target.value) return; onChange({ lat, lng: Number(e.target.value) }); }, size: "S" } ) }) ] }); } function Input({ attribute, onChange, value, name, required }) { const { formatMessage } = useIntl(); const config = useConfig(); const isInitialMount = useRef(true); const [focusPoint, setFocusPoint] = useState(); const [currentPoint, setCurrentPoint] = useReducer((state, action) => { const { origin, value: value2 } = action; if ((origin === "coordsInput" || origin === "placeSearch" || origin === "fieldValue") && isValidPoint(value2) && !isSamePoint(state, value2)) { setFocusPoint(value2); } return value2; }, noPoint); const [currentAddress, setCurrentAddress] = useState(""); const [nothingSelectedWarning, setNothingSelectedWarning] = useState(false); useEffect(() => { if (required && !isValidPoint(currentPoint)) { setNothingSelectedWarning(true); } else if (nothingSelectedWarning) { setNothingSelectedWarning(false); } }, [currentPoint]); useEffect(() => { if (!value) return; let parsedValue = value; if (typeof value === "string") { parsedValue = JSON.parse(value); } if (!parsedValue) return; const { address, coordinates } = parsedValue; if (address === currentAddress && isSamePoint(currentPoint, coordinates)) return; setCurrentPoint({ origin: "fieldValue", value: coordinates }); setCurrentAddress(address); }, [value]); useEffect(() => { if (isInitialMount.current) { isInitialMount.current = false; return; } const newValue = isValidPoint(currentPoint) ? JSON.stringify({ address: currentAddress, coordinates: currentPoint, geohash: Geohash.encode(currentPoint.lat, currentPoint.lng) }) : null; if (newValue === (typeof value === "string" ? value : JSON.stringify(value))) return; onChange({ target: { name, value: newValue, type: attribute.type } }); }, [currentPoint, currentAddress]); useEffect(() => { if (config?.defaultLatitude && config?.defaultLongitude && !value) { setFocusPoint({ lat: parseFloat(config.defaultLatitude), lng: parseFloat(config.defaultLongitude) }); } }, [config]); const onReset = () => { setCurrentPoint({ origin: "reset", value: noPoint }); setCurrentAddress(""); }; return /* @__PURE__ */ jsxs(Fragment, { children: [ /* @__PURE__ */ jsx(Typography, { variant: "pi", fontWeight: "bold", children: formatMessage({ id: "input.label" }) }), !config && /* @__PURE__ */ jsx("div", { style: { display: "flex", justifyContent: "center" }, children: /* @__PURE__ */ jsx(Loader, { small: true }) }), !!config && /* @__PURE__ */ jsxs(Fragment, { children: [ /* @__PURE__ */ jsx(Box, { marginTop: 1, borderColor: nothingSelectedWarning ? "danger600" : "primary200", children: /* @__PURE__ */ jsx( MapView, { config, focusPoint, currentAddress, onCoordsChange: setCurrentPoint, onAddressChange: setCurrentAddress, children: isValidPoint(currentPoint) && /* @__PURE__ */ jsx(Marker, { position: currentPoint }) } ) }), nothingSelectedWarning && /* @__PURE__ */ jsx(Box, { paddingTop: 1, children: /* @__PURE__ */ jsx(Typography, { variant: "pi", textColor: "danger600", children: formatMessage({ id: "input.error.required" }) }) }), /* @__PURE__ */ jsx(Box, { paddingTop: 2, children: /* @__PURE__ */ jsx( NumberFields, { coords: currentPoint, onChange: (point) => setCurrentPoint({ origin: "coordsInput", value: point }) } ) }), /* @__PURE__ */ jsx(Box, { paddingTop: 2, children: /* @__PURE__ */ jsx(Button, { startIcon: /* @__PURE__ */ jsx(ArrowClockwise, {}), onClick: onReset, children: formatMessage({ id: "input.button.reset" }) }) }) ] }) ] }); } export { Input as default };