diff --git a/package.json b/package.json index c9e7ba05..b7f69c32 100644 --- a/package.json +++ b/package.json @@ -114,6 +114,7 @@ "@types/google.maps": "^3.50.4", "@types/jest": "^27.5.2", "@types/json-schema": "^7.0.11", + "@types/leaflet": "^1.9.3", "@types/react": "^17.0.47", "@types/react-dom": "^17.0.17", "babel-jest": "^27.5.1", @@ -128,6 +129,7 @@ "jest-canvas-mock": "^2.4.0", "jest-cli": "^27.5.1", "joi": "17.4.2", + "leaflet": "^1.9.4", "lodash.clonedeep": "4.5.0", "lodash.debounce": "4.0.8", "lodash.get": "4.4.2", @@ -143,6 +145,7 @@ "react-error-boundary": "^3.1.4", "react-jss": "^10.9.0", "react-hook-form": "7.33.1", + "react-leaflet": "^4.2.1", "react-redux": "^7.2.8", "react-router": "^5.3.3", "react-router-dom": "^5.3.3", diff --git a/src/components/Card/input.tsx b/src/components/Card/input.tsx index ac1279f9..82c9371d 100644 --- a/src/components/Card/input.tsx +++ b/src/components/Card/input.tsx @@ -38,6 +38,7 @@ import Json from '../Json'; import Component from '../Component'; import {CHANGE} from './const'; import Controller from '../Controller'; +import LeafletMap from './inputs/LeafletMap'; const getFieldClass = (index, classes, name, className) => name === '' ? className : clsx( @@ -594,7 +595,7 @@ function useInput( case 'label': return (field?.name || title) ? {field?.name ? field?.value : title} : null; case 'icon': return (field?.name || title) ? : null; case 'gps': return - + {props.mapType === 'leaflet' ? : } ; default: return void; + value?: GPSValue; + disabled?: boolean; +} + +export type ComponentProps = React.FC; diff --git a/src/components/Card/inputs/LeafletMap/index.tsx b/src/components/Card/inputs/LeafletMap/index.tsx new file mode 100644 index 00000000..a8bd3a02 --- /dev/null +++ b/src/components/Card/inputs/LeafletMap/index.tsx @@ -0,0 +1,112 @@ +import merge from 'ut-function.merge'; +import * as React from 'react'; +import L from 'leaflet'; +import icon from 'leaflet/dist/images/marker-icon.png'; +import iconShadow from 'leaflet/dist/images/marker-shadow.png'; +import { MapContainer, TileLayer, Marker, useMapEvents } from 'react-leaflet'; + +import Context from '../../../Text/context'; +import { Props } from './LMap.types'; + +import 'leaflet/dist/leaflet.css'; + +const DefaultIcon = L.icon({ + iconUrl: icon, + shadowUrl: iconShadow, + iconSize: [24, 36], + iconAnchor: [12, 36] +}); + +L.Marker.prototype.options.icon = DefaultIcon; + +const defaultOptions = { + center: { lat: 42.69641881321328, lng: 23.323133750607305 }, + zoom: 12, + dragging: true, + touchZoom: false, + doubleClickZoom: false, + scrollWheelZoom: false, + zoomControl: true, + attributionControl: false +}; + +const Handlers = ({onMapClick, disabled, mapOptions}) => { + const map = useMapEvents({ + click(e) { + return onMapClick(e, map); + } + }); + React.useEffect(() => { + if (!map) return; + if (!disabled) { + Object.entries(mapOptions).forEach(([k, v]) => { + if (map[k] && v === true) { + if (k === 'zoomControl') { + map.addControl(map.zoomControl); + return; + } + map[k].enable(); + } + }); + } else { + Object.entries(mapOptions).forEach(([k, v]) => { + if (map[k] && v === false) { + if (k === 'zoomControl') { + map.removeControl(map.zoomControl); + return; + } + map[k].disable(); + } + }); + } + }, [disabled, map, mapOptions]); + return null; +}; + +export default React.forwardRef(function LeafletMap(props, ref) { + if (typeof ref === 'function') ref({}); + const { options = {}, onChange, value = null, disabled = false } = props; + const [selectedPosition, setSelectedPosition] = React.useState(value); + const { configuration: { 'portal.utPrime.LMap': coreConfig = {} } = {} } = React.useContext(Context); + + const mapOptions = React.useMemo(() => { + return merge( + [ + {}, + defaultOptions, + typeof coreConfig === 'string' ? JSON.parse(coreConfig) : coreConfig, + options, + selectedPosition && { center: selectedPosition }, + disabled && { dragging: false, touchZoom: false, doubleClickZoom: false, scrollWheelZoom: false, boxZoom: false, keyboard: false, zoomControl: false } + ].filter(Boolean) + ); + }, [options, selectedPosition, coreConfig, disabled]); + + const onMapClick = React.useCallback( + (event, map) => { + if (disabled) return; + map.flyTo(event.latlng, map.getZoom()); + const position = event.latlng; + setSelectedPosition(position); + onChange({ value: position }); + }, + [disabled, onChange] + ); + + return ( +
+ rest)(mapOptions)}> + + + {selectedPosition && } + + {selectedPosition && ( +
+ {[selectedPosition.lat, selectedPosition.lng].join(', ')} +
+ )} +
+ ); +}); diff --git a/src/components/Text/context.ts b/src/components/Text/context.ts index 159ac116..66c50eab 100644 --- a/src/components/Text/context.ts +++ b/src/components/Text/context.ts @@ -1,9 +1,11 @@ import React from 'react'; import type { GMapOptions } from '../prime/googlemap/GMap.types'; +import type { LMapOptions } from '../Card/inputs/LeafletMap/LMap.types'; import type { FormatOptions } from '../Gate/Gate.types'; export interface PortalConfiguration { 'portal.utPrime.GMap'?: string | GMapOptions; + 'portal.utPrime.LMap'?: string | LMapOptions; 'portal.utPrime.formatOptions'?: string | FormatOptions; } diff --git a/src/components/test/input.ts b/src/components/test/input.ts index 7b691c64..3024ef63 100644 --- a/src/components/test/input.ts +++ b/src/components/test/input.ts @@ -190,6 +190,12 @@ const properties: Properties = { type: 'gps' } }, + gpsLeaflet: { + widget: { + type: 'gps', + mapType: 'leaflet' + } + }, image: { widget: { type: 'image' @@ -344,6 +350,7 @@ export const input: { className: 'xl:col-4', widgets: [ 'input.gps', + 'input.gpsLeaflet', 'input.image', 'input.imageUpload', 'input.file', diff --git a/src/components/types/index.ts b/src/components/types/index.ts index 7fb3932a..40ed2311 100644 --- a/src/components/types/index.ts +++ b/src/components/types/index.ts @@ -77,6 +77,7 @@ export interface PropertyEditor { fieldClass?: string, labelClass?: string, translation?: boolean, + mapType?: 'leaflet' | 'google' [editorProperties: string]: unknown }