From 34fabf31ff47bfa8dd02eac369df20b5cec7fd71 Mon Sep 17 00:00:00 2001 From: goodlifeinc Date: Fri, 4 Aug 2023 17:00:22 +0300 Subject: [PATCH 1/2] feat: OpenStreetMap initial widget --- package.json | 3 + src/components/Card/input.tsx | 3 +- .../Card/inputs/LeafletMap/LMap.types.ts | 18 +++ .../Card/inputs/LeafletMap/index.tsx | 112 ++++++++++++++++++ src/components/Text/context.ts | 2 + src/components/test/input.ts | 7 ++ src/components/types/index.ts | 1 + 7 files changed, 145 insertions(+), 1 deletion(-) create mode 100644 src/components/Card/inputs/LeafletMap/LMap.types.ts create mode 100644 src/components/Card/inputs/LeafletMap/index.tsx 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..7aeece35 --- /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 {key, region, language, ...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 } From 2bdf2ea32296969096fb61d68ab1c98b56ccdd87 Mon Sep 17 00:00:00 2001 From: goodlifeinc Date: Fri, 4 Aug 2023 17:21:30 +0300 Subject: [PATCH 2/2] fix: unnecessary props --- src/components/Card/inputs/LeafletMap/index.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/Card/inputs/LeafletMap/index.tsx b/src/components/Card/inputs/LeafletMap/index.tsx index 7aeece35..a8bd3a02 100644 --- a/src/components/Card/inputs/LeafletMap/index.tsx +++ b/src/components/Card/inputs/LeafletMap/index.tsx @@ -69,7 +69,7 @@ export default React.forwardRef(function LeafletMap(props, ref) { const [selectedPosition, setSelectedPosition] = React.useState(value); const { configuration: { 'portal.utPrime.LMap': coreConfig = {} } = {} } = React.useContext(Context); - const {key, region, language, ...mapOptions} = React.useMemo(() => { + const mapOptions = React.useMemo(() => { return merge( [ {},