diff options
Diffstat (limited to 'plugins/jetpack/extensions/blocks/map/component.js')
-rw-r--r-- | plugins/jetpack/extensions/blocks/map/component.js | 332 |
1 files changed, 332 insertions, 0 deletions
diff --git a/plugins/jetpack/extensions/blocks/map/component.js b/plugins/jetpack/extensions/blocks/map/component.js new file mode 100644 index 00000000..c4ea55ae --- /dev/null +++ b/plugins/jetpack/extensions/blocks/map/component.js @@ -0,0 +1,332 @@ +/** + * External dependencies + */ +import { __ } from '@wordpress/i18n'; +import { assign, debounce, get } from 'lodash'; +import { Button, Dashicon, TextareaControl, TextControl } from '@wordpress/components'; +import { Children, Component, createRef, Fragment } from '@wordpress/element'; + +/** + * Internal dependencies + */ +import MapMarker from './map-marker/'; +import InfoWindow from './info-window/'; +import { mapboxMapFormatter } from './mapbox-map-formatter/'; + +export class Map extends Component { + // Lifecycle + constructor() { + super( ...arguments ); + + this.state = { + map: null, + fit_to_bounds: false, + loaded: false, + mapboxgl: null, + }; + + // Refs + this.mapRef = createRef(); + + // Debouncers + this.debouncedSizeMap = debounce( this.sizeMap, 250 ); + } + render() { + const { points, admin, children, markerColor } = this.props; + const { map, activeMarker, mapboxgl } = this.state; + const { onMarkerClick, deleteActiveMarker, updateActiveMarker } = this; + const currentPoint = get( activeMarker, 'props.point' ) || {}; + const { title, caption } = currentPoint; + const addPoint = Children.map( children, child => { + const tagName = get( child, 'props.tagName' ); + if ( 'AddPoint' === tagName ) { + return child; + } + } ); + const mapMarkers = + map && + mapboxgl && + points.map( ( point, index ) => { + return ( + <MapMarker + key={ index } + point={ point } + index={ index } + map={ map } + mapboxgl={ mapboxgl } + markerColor={ markerColor } + onClick={ onMarkerClick } + /> + ); + } ); + const infoWindow = mapboxgl && ( + <InfoWindow + activeMarker={ activeMarker } + map={ map } + mapboxgl={ mapboxgl } + unsetActiveMarker={ () => this.setState( { activeMarker: null } ) } + > + { activeMarker && admin && ( + <Fragment> + <TextControl + label={ __( 'Marker Title', 'jetpack' ) } + value={ title } + onChange={ value => updateActiveMarker( { title: value } ) } + /> + <TextareaControl + className="wp-block-jetpack-map__marker-caption" + label={ __( 'Marker Caption', 'jetpack' ) } + value={ caption } + rows="2" + tag="textarea" + onChange={ value => updateActiveMarker( { caption: value } ) } + /> + <Button onClick={ deleteActiveMarker } className="wp-block-jetpack-map__delete-btn"> + <Dashicon icon="trash" size="15" /> { __( 'Delete Marker', 'jetpack' ) } + </Button> + </Fragment> + ) } + + { activeMarker && ! admin && ( + <Fragment> + <h3>{ title }</h3> + <p>{ caption }</p> + </Fragment> + ) } + </InfoWindow> + ); + return ( + <Fragment> + <div className="wp-block-jetpack-map__gm-container" ref={ this.mapRef }> + { mapMarkers } + </div> + { infoWindow } + { addPoint } + </Fragment> + ); + } + componentDidMount() { + const { apiKey } = this.props; + if ( apiKey ) { + this.loadMapLibraries(); + } + } + componentWillUnmount() { + this.debouncedSizeMap.cancel(); + } + componentDidUpdate( prevProps ) { + const { apiKey, children, points, mapStyle, mapDetails } = this.props; + const { map } = this.state; + if ( apiKey && apiKey.length > 0 && apiKey !== prevProps.apiKey ) { + this.loadMapLibraries(); + } + // If the user has just clicked to show the Add Point component, hide info window. + // AddPoint is the only possible child. + if ( children !== prevProps.children && children !== false ) { + this.clearCurrentMarker(); + } + if ( points !== prevProps.points ) { + this.setBoundsByMarkers(); + } + if ( points.length !== prevProps.points.length ) { + this.clearCurrentMarker(); + } + if ( mapStyle !== prevProps.mapStyle || mapDetails !== prevProps.mapDetails ) { + map.setStyle( this.getMapStyle() ); + } + } + /* Event handling */ + onMarkerClick = marker => { + const { onMarkerClick } = this.props; + this.setState( { activeMarker: marker } ); + onMarkerClick(); + }; + onMapClick = () => { + this.setState( { activeMarker: null } ); + }; + clearCurrentMarker = () => { + this.setState( { activeMarker: null } ); + }; + updateActiveMarker = updates => { + const { points } = this.props; + const { activeMarker } = this.state; + const { index } = activeMarker.props; + const newPoints = points.slice( 0 ); + + assign( newPoints[ index ], updates ); + this.props.onSetPoints( newPoints ); + }; + deleteActiveMarker = () => { + const { points } = this.props; + const { activeMarker } = this.state; + const { index } = activeMarker.props; + const newPoints = points.slice( 0 ); + + newPoints.splice( index, 1 ); + this.props.onSetPoints( newPoints ); + this.setState( { activeMarker: null } ); + }; + // Various map functions + sizeMap = () => { + const { map } = this.state; + const mapEl = this.mapRef.current; + const blockWidth = mapEl.offsetWidth; + const maxHeight = window.innerHeight * 0.8; + const blockHeight = Math.min( blockWidth * ( 3 / 4 ), maxHeight ); + mapEl.style.height = blockHeight + 'px'; + map.resize(); + this.setBoundsByMarkers(); + }; + setBoundsByMarkers = () => { + const { zoom, points, onSetZoom } = this.props; + const { map, activeMarker, mapboxgl, zoomControl, boundsSetProgrammatically } = this.state; + if ( ! map ) { + return; + } + // If there are no points at all, there is no data to set bounds to. Abort the function. + if ( ! points.length ) { + return; + } + // If there is an open info window, resizing will probably move the info window which complicates interaction. + if ( activeMarker ) { + return; + } + const bounds = new mapboxgl.LngLatBounds(); + points.forEach( point => { + bounds.extend( [ point.coordinates.longitude, point.coordinates.latitude ] ); + } ); + + // If there are multiple points, zoom is determined by the area they cover, and zoom control is removed. + if ( points.length > 1 ) { + map.fitBounds( bounds, { + padding: { + top: 40, + bottom: 40, + left: 20, + right: 20, + }, + } ); + this.setState( { boundsSetProgrammatically: true } ); + map.removeControl( zoomControl ); + return; + } + // If there is only one point, center map around it. + map.setCenter( bounds.getCenter() ); + + // If the number of markers has just changed from > 1 to 1, set an arbitrary tight zoom, which feels like the original default. + if ( boundsSetProgrammatically ) { + const newZoom = 12; + map.setZoom( newZoom ); + onSetZoom( newZoom ); + } else { + // If there are one (or zero) points, and this is not a recent change, respect user's chosen zoom. + map.setZoom( parseInt( zoom, 10 ) ); + } + map.addControl( zoomControl ); + this.setState( { boundsSetProgrammatically: false } ); + }; + getMapStyle() { + const { mapStyle, mapDetails } = this.props; + return mapboxMapFormatter( mapStyle, mapDetails ); + } + getMapType() { + const { mapStyle } = this.props; + switch ( mapStyle ) { + case 'satellite': + return 'HYBRID'; + case 'terrain': + return 'TERRAIN'; + case 'black_and_white': + default: + return 'ROADMAP'; + } + } + // Script loading, browser geolocation + scriptsLoaded = () => { + const { mapCenter, points } = this.props; + this.setState( { loaded: true } ); + + // If the map has any points, skip geolocation and use what we have. + if ( points.length > 0 ) { + this.initMap( mapCenter ); + return; + } + this.initMap( mapCenter ); + }; + loadMapLibraries() { + const { apiKey } = this.props; + Promise.all( [ + import( /* webpackChunkName: "map/mapbox-gl" */ 'mapbox-gl' ), + import( /* webpackChunkName: "map/mapbox-gl" */ 'mapbox-gl/dist/mapbox-gl.css' ), + ] ).then( ( [ { default: mapboxgl } ] ) => { + mapboxgl.accessToken = apiKey; + this.setState( { mapboxgl: mapboxgl }, this.scriptsLoaded ); + } ); + } + initMap( mapCenter ) { + const { mapboxgl } = this.state; + const { zoom, onMapLoaded, onError, admin } = this.props; + let map = null; + try { + map = new mapboxgl.Map( { + container: this.mapRef.current, + style: this.getMapStyle(), + center: this.googlePoint2Mapbox( mapCenter ), + zoom: parseInt( zoom, 10 ), + pitchWithRotate: false, + attributionControl: false, + dragRotate: false, + } ); + } catch ( e ) { + onError( 'mapbox_error', e.message ); + return; + } + map.on( 'error', e => { + onError( 'mapbox_error', e.error.message ); + } ); + const zoomControl = new mapboxgl.NavigationControl( { + showCompass: false, + showZoom: true, + } ); + map.on( 'zoomend', () => { + this.props.onSetZoom( map.getZoom() ); + } ); + + /* Listen for clicks on the Map background, which hides the current popup. */ + map.getCanvas().addEventListener( 'click', this.onMapClick ); + this.setState( { map, zoomControl }, () => { + this.debouncedSizeMap(); + map.addControl( zoomControl ); + if ( ! admin ) { + map.addControl( new mapboxgl.FullscreenControl() ); + } + this.mapRef.current.addEventListener( 'alignmentChanged', this.debouncedSizeMap ); + map.resize(); + onMapLoaded(); + this.setState( { loaded: true } ); + window.addEventListener( 'resize', this.debouncedSizeMap ); + } ); + } + googlePoint2Mapbox( google_point ) { + const mapCenter = [ + google_point.longitude ? google_point.longitude : 0, + google_point.latitude ? google_point.latitude : 0, + ]; + return mapCenter; + } +} + +Map.defaultProps = { + points: [], + mapStyle: 'default', + zoom: 13, + onSetZoom: () => {}, + onMapLoaded: () => {}, + onMarkerClick: () => {}, + onError: () => {}, + markerColor: 'red', + apiKey: null, + mapCenter: {}, +}; + +export default Map; |