diff --git a/core/Field/Map_Field.php b/core/Field/Map_Field.php index 20acf5d19..521dba517 100644 --- a/core/Field/Map_Field.php +++ b/core/Field/Map_Field.php @@ -40,9 +40,15 @@ public function __construct( $type, $name, $label ) { */ public static function admin_enqueue_scripts() { $api_key = apply_filters( 'carbon_fields_map_field_api_key', false ); - $url = apply_filters( 'carbon_fields_map_field_api_url', '//maps.googleapis.com/maps/api/js?' . ( $api_key ? 'key=' . $api_key : '' ), $api_key ); - wp_enqueue_script( 'carbon-google-maps', $url, array(), null ); + if( $api_key ) { + $url = apply_filters( 'carbon_fields_map_field_api_url', '//maps.googleapis.com/maps/api/js?' . ( $api_key ? 'key=' . $api_key : '' ), $api_key ); + wp_enqueue_script( 'carbon-google-maps', $url, array(), null ); + } else { + // Use Leaflet.js as a fallback when no Google Maps API key is set + wp_enqueue_style( 'carbon-leaflet', '//unpkg.com/leaflet@1.9.3/dist/leaflet.css', array(), null ); + wp_enqueue_script( 'carbon-leaflet', '//unpkg.com/leaflet@1.9.3/dist/leaflet.js', array(), null ); + } } /** diff --git a/packages/core/fields/map/google-map.js b/packages/core/fields/map/google-map.js index 971ce9d8c..41945e229 100644 --- a/packages/core/fields/map/google-map.js +++ b/packages/core/fields/map/google-map.js @@ -3,6 +3,28 @@ */ import observeResize from 'observe-resize'; import { Component, createRef } from '@wordpress/element'; +import { __ } from '@wordpress/i18n'; + +export async function googleGeocode( address ) { + const geocoder = new window.google.maps.Geocoder(); + + return new Promise( ( resolve, reject ) => { + geocoder.geocode( { address }, ( results, status ) => { + if ( status === window.google.maps.GeocoderStatus.OK ) { + const { location } = results[ 0 ].geometry; + + resolve( { + lat: location.lat(), + lng: location.lng() + } ); + } else if ( status === 'ZERO_RESULTS' ) { + reject( __( 'The address could not be found.', 'carbon-fields-ui' ) ); + } else { + reject( `${ __( 'Geocode was not successful for the following reason: ', 'carbon-fields-ui' ) } ${ status }` ); + } + } ); + } ); +} class GoogleMap extends Component { /** diff --git a/packages/core/fields/map/index.js b/packages/core/fields/map/index.js index 4944fbf05..d507f6a95 100644 --- a/packages/core/fields/map/index.js +++ b/packages/core/fields/map/index.js @@ -17,7 +17,13 @@ import { */ import './style.scss'; import SearchInput from '../../components/search-input'; -import GoogleMap from './google-map'; +import GoogleMap, { googleGeocode } from './google-map'; +import LeafletMap, { nominatimGeocode } from './leaflet-map'; + +const isGoogleMapsLoaded = Boolean( document.querySelector( 'script#carbon-google-maps-js' ) ); + +const Map = isGoogleMapsLoaded ? GoogleMap : LeafletMap; +const geocode = isGoogleMapsLoaded ? googleGeocode : nominatimGeocode; class MapField extends Component { /** @@ -30,7 +36,7 @@ class MapField extends Component { if ( address ) { this.props.onGeocodeAddress( { address } ); } - }, 250 ) + }, 500 ) /** * Handles the change of map location. @@ -73,7 +79,7 @@ class MapField extends Component { onChange={ this.handleSearchChange } /> - { - return new Promise( ( resolve, reject ) => { - const geocoder = new window.google.maps.Geocoder(); - - geocoder.geocode( { address }, ( results, status ) => { - if ( status === window.google.maps.GeocoderStatus.OK ) { - const { location } = results[ 0 ].geometry; - - resolve( { - lat: location.lat(), - lng: location.lng() - } ); - } else if ( status === 'ZERO_RESULTS' ) { - reject( __( 'The address could not be found.', 'carbon-fields-ui' ) ); - } else { - reject( `${ __( 'Geocode was not successful for the following reason: ', 'carbon-fields-ui' ) } ${ status }` ); - } - } ); - } ); - }; - geocode( payload.address ) .then( ( { lat, lng } ) => { onChange( id, { diff --git a/packages/core/fields/map/leaflet-map.js b/packages/core/fields/map/leaflet-map.js new file mode 100644 index 000000000..a4173f976 --- /dev/null +++ b/packages/core/fields/map/leaflet-map.js @@ -0,0 +1,148 @@ +/** + * Based on `google-maps.js`, adapted for Leaflet.js + */ + +// External dependencies +import observeResize from 'observe-resize'; +import { Component, createRef } from '@wordpress/element'; +import { __ } from '@wordpress/i18n'; + +export async function nominatimGeocode( address ) { + // Usage Policy: https://operations.osmfoundation.org/policies/nominatim/ + const nominatimSearchResults = await new Promise( ( resolve, reject ) => { + const request = window.jQuery.ajax( { + url: 'https://nominatim.openstreetmap.org/search', + type: 'GET', + data: { q: address, format: 'json', limit: 1 }, + headers: { 'User-Agent': 'Carbon Fields Map Field' } + } ); + + request.done( resolve ); + request.fail( () => { + reject( __( 'An error occured.', 'carbon-fields-ui' ) ); + } ); + } ); + + if ( nominatimSearchResults?.length !== 1 ) { + throw __( 'The address could not be found.', 'carbon-fields-ui' ); + } + + const { lat, lon: lng } = nominatimSearchResults[ 0 ]; + return { lat, lng }; +} + +class LeafletMap extends Component { + /** + * Keeps references to the DOM node. + * + * @type {Object} + */ + node = createRef(); + + /** + * Lifecycle hook. + * + * @return {void} + */ + componentDidMount() { + this.setupMap(); + this.setupMapEvents(); + } + + /** + * Lifecycle hook. + * + * @return {void} + */ + componentDidUpdate() { + const { + lat, + lng, + zoom + } = this.props; + + const markerPosition = this.marker.getLatLng(); + if ( ! markerPosition.equals( [ lat, lng ] ) ) { + this.marker.setLatLng( [ lat, lng ] ); + this.map.panTo( [ lat, lng ] ); + } + + const mapZoom = this.map.getZoom(); + if ( zoom !== mapZoom ) { + this.map.setZoom( zoom ); + } + } + + /** + * Lifecycle hook. + * + * @return {void} + */ + componentWillUnmount() { + this.cancelResizeObserver(); + this.map.remove(); + } + + /** + * Initializes the map into placeholder element. + * + * @return {void} + */ + setupMap() { + const { lat, lng, zoom } = this.props; + const Leaflet = window.L; + + this.map = Leaflet.map( this.node.current, { + center: [ lat, lng ], + zoom, + scrollWheelZoom: false + } ); + + Leaflet.tileLayer( 'https://tile.openstreetmap.org/{z}/{x}/{y}.png', { + attribution: '© OpenStreetMap contributors' + } ).addTo( this.map ); + + this.marker = Leaflet.marker( [ lat, lng ], { + draggable: true + } ).addTo( this.map ); + + this.cancelResizeObserver = observeResize( this.node.current, () => { + this.map.invalidateSize(); + } ); + } + + /** + * Adds the listeners for the map's events. + * + * @return {void} + */ + setupMapEvents() { + // Enable scroll wheel zoom on focus + this.map.once( 'focus', () => { + this.map.scrollWheelZoom.enable(); + } ); + + // Update zoom when changed + this.map.on( 'zoomend', () => { + this.props.onChange( { zoom: this.map.getZoom() } ); + } ); + + // Update the position when the marker is moved + this.marker.on( 'dragend', () => { + const position = this.marker.getLatLng(); + this.map.panTo( position ); + this.props.onChange( { lat: position.lat, lng: position.lng } ); + } ); + } + + /** + * Renders the component. + * + * @return {Object} + */ + render() { + return
; + } +} + +export default LeafletMap;