diff --git a/projects/plugins/jetpack/changelog/add-map-block-mapkit-address-attribute b/projects/plugins/jetpack/changelog/add-map-block-mapkit-address-attribute new file mode 100644 index 0000000000000..d3d013017be61 --- /dev/null +++ b/projects/plugins/jetpack/changelog/add-map-block-mapkit-address-attribute @@ -0,0 +1,4 @@ +Significance: minor +Type: other + +Mapkit support for the address attribute diff --git a/projects/plugins/jetpack/changelog/add-map-block-mapkit-editor b/projects/plugins/jetpack/changelog/add-map-block-mapkit-editor new file mode 100644 index 0000000000000..9777de0890edc --- /dev/null +++ b/projects/plugins/jetpack/changelog/add-map-block-mapkit-editor @@ -0,0 +1,4 @@ +Significance: minor +Type: enhancement + +Editor support for Mapkit in the Map block diff --git a/projects/plugins/jetpack/changelog/add-map-block-mapkit-geocoder b/projects/plugins/jetpack/changelog/add-map-block-mapkit-geocoder new file mode 100644 index 0000000000000..c36c02cfcbbdc --- /dev/null +++ b/projects/plugins/jetpack/changelog/add-map-block-mapkit-geocoder @@ -0,0 +1,4 @@ +Significance: minor +Type: other + +Add Mapkit geocoder for use on WPCOM. diff --git a/projects/plugins/jetpack/changelog/add-map-block-mapkit-hide-unimplemented-options b/projects/plugins/jetpack/changelog/add-map-block-mapkit-hide-unimplemented-options new file mode 100644 index 0000000000000..1e4663da586c3 --- /dev/null +++ b/projects/plugins/jetpack/changelog/add-map-block-mapkit-hide-unimplemented-options @@ -0,0 +1,4 @@ +Significance: minor +Type: enhancement + +Hide Mapbox specific options when using Mapkit diff --git a/projects/plugins/jetpack/changelog/add-map-block-mapkit-map-provider-logic b/projects/plugins/jetpack/changelog/add-map-block-mapkit-map-provider-logic new file mode 100644 index 0000000000000..dd330847af475 --- /dev/null +++ b/projects/plugins/jetpack/changelog/add-map-block-mapkit-map-provider-logic @@ -0,0 +1,4 @@ +Significance: minor +Type: other + +Move logic to getMapProvider diff --git a/projects/plugins/jetpack/changelog/add-map-block-mapkit-recenter b/projects/plugins/jetpack/changelog/add-map-block-mapkit-recenter new file mode 100644 index 0000000000000..18007f1851f71 --- /dev/null +++ b/projects/plugins/jetpack/changelog/add-map-block-mapkit-recenter @@ -0,0 +1,4 @@ +Significance: minor +Type: other + +Recenter map after closing marker. diff --git a/projects/plugins/jetpack/changelog/add-map-block-mapkit-remove-token-input b/projects/plugins/jetpack/changelog/add-map-block-mapkit-remove-token-input new file mode 100644 index 0000000000000..912544669dc42 --- /dev/null +++ b/projects/plugins/jetpack/changelog/add-map-block-mapkit-remove-token-input @@ -0,0 +1,4 @@ +Significance: minor +Type: enhancement + +Remove the Mapbox API key box, when the mapping provider isn't Mapbox diff --git a/projects/plugins/jetpack/changelog/add-map-block-mapkit-use-mapkit-setup b/projects/plugins/jetpack/changelog/add-map-block-mapkit-use-mapkit-setup new file mode 100644 index 0000000000000..f6dee9227e3cc --- /dev/null +++ b/projects/plugins/jetpack/changelog/add-map-block-mapkit-use-mapkit-setup @@ -0,0 +1,4 @@ +Significance: minor +Type: other + +Adds useMapkitSetup hook diff --git a/projects/plugins/jetpack/changelog/add-map-block-mapkit-zoom-conversion b/projects/plugins/jetpack/changelog/add-map-block-mapkit-zoom-conversion new file mode 100644 index 0000000000000..955697ec9630a --- /dev/null +++ b/projects/plugins/jetpack/changelog/add-map-block-mapkit-zoom-conversion @@ -0,0 +1,4 @@ +Significance: minor +Type: other + +Adds util functions for mapkit that allow converting zoom levels to camera distance, and back diff --git a/projects/plugins/jetpack/changelog/add-map-mapkit b/projects/plugins/jetpack/changelog/add-map-mapkit new file mode 100644 index 0000000000000..58b9731b1ea4c --- /dev/null +++ b/projects/plugins/jetpack/changelog/add-map-mapkit @@ -0,0 +1,4 @@ +Significance: minor +Type: enhancement + +Add Mapkit maps diff --git a/projects/plugins/jetpack/changelog/fix-map-block-mapkit-catch-faillure b/projects/plugins/jetpack/changelog/fix-map-block-mapkit-catch-faillure new file mode 100644 index 0000000000000..d024385e4240e --- /dev/null +++ b/projects/plugins/jetpack/changelog/fix-map-block-mapkit-catch-faillure @@ -0,0 +1,4 @@ +Significance: minor +Type: other + +Show error message when fetching Mapkit API key fails diff --git a/projects/plugins/jetpack/changelog/fix-map-block-mapkit-default-latlng b/projects/plugins/jetpack/changelog/fix-map-block-mapkit-default-latlng new file mode 100644 index 0000000000000..689cead2e3412 --- /dev/null +++ b/projects/plugins/jetpack/changelog/fix-map-block-mapkit-default-latlng @@ -0,0 +1,4 @@ +Significance: minor +Type: other + +Support latitude & longitude next to lat/lng diff --git a/projects/plugins/jetpack/changelog/fix-map-block-mapkit-duplicate-loads b/projects/plugins/jetpack/changelog/fix-map-block-mapkit-duplicate-loads new file mode 100644 index 0000000000000..92a1063d88936 --- /dev/null +++ b/projects/plugins/jetpack/changelog/fix-map-block-mapkit-duplicate-loads @@ -0,0 +1,4 @@ +Significance: minor +Type: other + +Only add mapkit.js once diff --git a/projects/plugins/jetpack/extensions/blocks/map/add-point/index.js b/projects/plugins/jetpack/extensions/blocks/map/add-point/index.js index 842abe3480c99..893bdeae9e523 100644 --- a/projects/plugins/jetpack/extensions/blocks/map/add-point/index.js +++ b/projects/plugins/jetpack/extensions/blocks/map/add-point/index.js @@ -7,7 +7,7 @@ import './style.scss'; export class AddPoint extends Component { render() { - const { onClose, onAddPoint, onError, apiKey } = this.props; + const { onClose, onAddPoint, onError, apiKey, mapProvider } = this.props; return ( @@ -31,6 +32,7 @@ AddPoint.defaultProps = { onAddPoint: () => {}, onClose: () => {}, onError: () => {}, + mapProvider: 'mapbox', }; export default AddPoint; diff --git a/projects/plugins/jetpack/extensions/blocks/map/component/index.js b/projects/plugins/jetpack/extensions/blocks/map/component/index.js new file mode 100644 index 0000000000000..cb90daf0b63a0 --- /dev/null +++ b/projects/plugins/jetpack/extensions/blocks/map/component/index.js @@ -0,0 +1,13 @@ +import { forwardRef } from '@wordpress/element'; +import MapboxComponent from './mapbox'; +import MapkitComponent from './mapkit'; + +const MapComponent = forwardRef( ( props, ref ) => { + if ( props.mapProvider === 'mapkit' ) { + const mapkitProps = { ...props, ref: null }; + return ; + } + return ; +} ); + +export default MapComponent; diff --git a/projects/plugins/jetpack/extensions/blocks/map/component/info-window/index.js b/projects/plugins/jetpack/extensions/blocks/map/component/info-window/index.js new file mode 100644 index 0000000000000..3c788e6971685 --- /dev/null +++ b/projects/plugins/jetpack/extensions/blocks/map/component/info-window/index.js @@ -0,0 +1,11 @@ +import MapboxInfoWindow from './mapbox'; +import MapkitInfoWindow from './mapkit'; + +const InfoWindow = props => { + if ( props.mapProvider === 'mapkit' ) { + return ; + } + return ; +}; + +export default InfoWindow; diff --git a/projects/plugins/jetpack/extensions/blocks/map/info-window/index.js b/projects/plugins/jetpack/extensions/blocks/map/component/info-window/mapbox.js similarity index 94% rename from projects/plugins/jetpack/extensions/blocks/map/info-window/index.js rename to projects/plugins/jetpack/extensions/blocks/map/component/info-window/mapbox.js index a5e1930bd47d5..c4e92afb2828e 100644 --- a/projects/plugins/jetpack/extensions/blocks/map/info-window/index.js +++ b/projects/plugins/jetpack/extensions/blocks/map/component/info-window/mapbox.js @@ -1,5 +1,5 @@ import { Component, createPortal } from '@wordpress/element'; -import { createInfoWindowPopup } from '../mapbox-utils'; +import { createInfoWindowPopup } from '../../mapbox-utils'; export class InfoWindow extends Component { componentDidMount() { diff --git a/projects/plugins/jetpack/extensions/blocks/map/component/info-window/mapkit.js b/projects/plugins/jetpack/extensions/blocks/map/component/info-window/mapkit.js new file mode 100644 index 0000000000000..e17dfc1bb3590 --- /dev/null +++ b/projects/plugins/jetpack/extensions/blocks/map/component/info-window/mapkit.js @@ -0,0 +1,83 @@ +import { Button, Dashicon, TextareaControl, TextControl } from '@wordpress/components'; +import { createPortal, Fragment, useEffect, useState } from '@wordpress/element'; +import { __ } from '@wordpress/i18n'; +import { useMapkit } from '../../mapkit/hooks'; + +const InfoWindow = () => { + const { activeMarker, setActiveMarker, calloutReference, admin, points, setPoints } = useMapkit(); + const [ pointsCopy, setPointsCopy ] = useState( points ); + const [ isDirty, setIsDirty ] = useState( false ); + const { title, caption } = pointsCopy.find( p => p.id === activeMarker?.id ) || {}; + + const updateActiveMarker = value => { + const newPoints = points.map( point => { + if ( point.id === activeMarker.id ) { + return { ...point, ...value }; + } + return point; + } ); + setPointsCopy( newPoints ); + setIsDirty( true ); + }; + + const deleteActiveMarker = () => { + const newPoints = points.filter( point => point.id !== activeMarker.id ); + setPointsCopy( newPoints ); + setIsDirty( true ); + // force closing of window + setActiveMarker( null ); + }; + + useEffect( () => { + if ( ! activeMarker && isDirty ) { + // commit the points when callout is closed, and content is dirty + setPoints( pointsCopy ); + } + }, [ activeMarker, isDirty, pointsCopy, setPoints ] ); + + useEffect( () => { + setPointsCopy( points ); + setIsDirty( false ); + }, [ points ] ); + + if ( ! activeMarker || ! calloutReference ) { + return null; + } + + return createPortal( + + { admin && ( + + updateActiveMarker( { title: value } ) } + /> + updateActiveMarker( { caption: value } ) } + /> + + + ) } + + { ! admin && ( + +

{ title }

+

{ caption }

+
+ ) } +
, + calloutReference + ); +}; + +InfoWindow.defaultProps = {}; + +export default InfoWindow; diff --git a/projects/plugins/jetpack/extensions/blocks/map/map-marker/index.js b/projects/plugins/jetpack/extensions/blocks/map/component/map-marker/index.js similarity index 91% rename from projects/plugins/jetpack/extensions/blocks/map/map-marker/index.js rename to projects/plugins/jetpack/extensions/blocks/map/component/map-marker/index.js index 51df4a6b5c4a4..96436395936dd 100644 --- a/projects/plugins/jetpack/extensions/blocks/map/map-marker/index.js +++ b/projects/plugins/jetpack/extensions/blocks/map/component/map-marker/index.js @@ -1,6 +1,6 @@ import { Component } from '@wordpress/element'; -import { getLoadContext } from '../../../shared/block-editor-asset-loader'; -import { setMarkerHTML } from '../mapbox-utils'; +import { getLoadContext } from '../../../../shared/block-editor-asset-loader'; +import { setMarkerHTML } from '../../mapbox-utils'; import './style.scss'; diff --git a/projects/plugins/jetpack/extensions/blocks/map/map-marker/style.scss b/projects/plugins/jetpack/extensions/blocks/map/component/map-marker/style.scss similarity index 100% rename from projects/plugins/jetpack/extensions/blocks/map/map-marker/style.scss rename to projects/plugins/jetpack/extensions/blocks/map/component/map-marker/style.scss diff --git a/projects/plugins/jetpack/extensions/blocks/map/component.js b/projects/plugins/jetpack/extensions/blocks/map/component/mapbox.js similarity index 95% rename from projects/plugins/jetpack/extensions/blocks/map/component.js rename to projects/plugins/jetpack/extensions/blocks/map/component/mapbox.js index 9d8a604f701a0..4e7e258d058ff 100644 --- a/projects/plugins/jetpack/extensions/blocks/map/component.js +++ b/projects/plugins/jetpack/extensions/blocks/map/component/mapbox.js @@ -6,19 +6,15 @@ import { getLoadContext, loadBlockEditorAssets, waitForObject, -} from '../../shared/block-editor-asset-loader'; -import editorAssets from './block-editor-assets.json'; -import InfoWindow from './info-window/'; -import MapMarker from './map-marker/'; -import { mapboxMapFormatter } from './mapbox-map-formatter/'; -import { - fitMapToBounds, - getMapBounds, - googlePoint2Mapbox, - resizeMapContainer, -} from './mapbox-utils'; +} from '../../../shared/block-editor-asset-loader'; +import editorAssets from '../block-editor-assets.json'; +import { mapboxMapFormatter } from '../mapbox-map-formatter'; +import { fitMapToBounds, getMapBounds, googlePoint2Mapbox } from '../mapbox-utils'; +import { resizeMapContainer } from '../utils'; +import InfoWindow from './info-window'; +import MapMarker from './map-marker'; -export class Map extends Component { +export class MapBoxComponent extends Component { // Lifecycle constructor() { super( ...arguments ); @@ -216,7 +212,6 @@ export class Map extends Component { const { map } = this.state; map.setZoom( zoom ); - map.updateZoom( zoom ); }; setBoundsByMarkers = () => { const { admin, onSetMapCenter, onSetZoom, points, zoom } = this.props; @@ -375,7 +370,7 @@ export class Map extends Component { } } -Map.defaultProps = { +MapBoxComponent.defaultProps = { points: [], mapStyle: 'default', zoom: 13, @@ -387,6 +382,7 @@ Map.defaultProps = { markerColor: 'red', apiKey: null, mapCenter: {}, + address: null, }; -export default Map; +export default MapBoxComponent; diff --git a/projects/plugins/jetpack/extensions/blocks/map/component/mapkit.js b/projects/plugins/jetpack/extensions/blocks/map/component/mapkit.js new file mode 100644 index 0000000000000..e656cc138b9bd --- /dev/null +++ b/projects/plugins/jetpack/extensions/blocks/map/component/mapkit.js @@ -0,0 +1,131 @@ +import { Children, forwardRef, memo, useCallback, useEffect, useRef } from '@wordpress/element'; +import { get } from 'lodash'; +import { MapkitProvider } from '../mapkit/context'; +import { + useMapkit, + useMapkitSetup, + useMapkitInit, + useMapkitType, + useMapkitCenter, + useMapkitOnMapLoad, + useMapkitOnMapTap, + useMapkitZoom, + useMapkitPoints, + useMapkitAddressLookup, +} from '../mapkit/hooks'; +import { createCalloutElementCallback } from '../mapkit-utils'; +import InfoWindow from './info-window'; + +const MapkitComponent = forwardRef( ( props, mapRef ) => { + const { admin, points, onError, onSetPoints } = props; + const { loaded, error, mapkit, currentDoc, currentWindow } = useMapkitSetup( mapRef ); + const { map } = useMapkitInit( mapkit, loaded, mapRef ); + const addPoint = Children.map( props.children, child => { + const tagName = get( child, 'props.tagName' ); + if ( 'AddPoint' === tagName ) { + return child; + } + } ); + + useEffect( () => { + if ( error ) { + onError( 'mapkit_error', error ); + } + }, [ error, onError ] ); + + return ( + + { loaded && mapkit && map ? : null } +
+ { addPoint } + + + ); +} ); + +const MapkitHelpers = memo( + ( { + address, + mapCenter, + mapStyle, + zoom, + onSetMapCenter, + onSetZoom, + onSetPoints, + points, + markerColor, + onMarkerClick, + onMapLoaded, + } ) => { + const { map, mapkit, setActiveMarker, setPreviousCenter, setCalloutReference, currentDoc } = + useMapkit(); + // Save these in a ref to prevent unwanted rerenders + const onMarkerClickRef = useRef( onMarkerClick ); + const onSetPointsRef = useRef( onSetPoints ); + + const onSelect = useCallback( + marker => { + setActiveMarker( marker ); + setPreviousCenter( map.center ); + if ( onMarkerClickRef.current ) { + onMarkerClickRef.current( marker ); + } + map.setCenterAnimated( + new mapkit.Coordinate( marker.coordinates.latitude, marker.coordinates.longitude ) + ); + }, + [ map, mapkit, setActiveMarker, setPreviousCenter, onMarkerClickRef ] + ); + + useMapkitCenter( mapCenter, onSetMapCenter ); + useMapkitType( mapStyle ); + useMapkitZoom( zoom, onSetZoom ); + useMapkitPoints( + points, + markerColor, + createCalloutElementCallback( currentDoc, setCalloutReference ), + onSelect + ); + useMapkitOnMapLoad( onMapLoaded ); + useMapkitOnMapTap( previousCenter => { + setActiveMarker( null ); + if ( previousCenter ) { + map.setCenterAnimated( previousCenter ); + } + } ); + + useMapkitAddressLookup( address, onSetPointsRef ); + return null; + } +); + +MapkitComponent.defaultProps = { + points: [], + mapStyle: 'default', + zoom: 13, + onSetZoom: () => {}, + onSetMapCenter: () => {}, + onMapLoaded: () => {}, + onMarkerClick: () => {}, + onError: () => {}, + markerColor: 'red', + mapCenter: {}, + address: null, +}; + +export default MapkitComponent; diff --git a/projects/plugins/jetpack/extensions/blocks/map/controls.js b/projects/plugins/jetpack/extensions/blocks/map/controls.js index ae09697537162..307a6c6bbbfee 100644 --- a/projects/plugins/jetpack/extensions/blocks/map/controls.js +++ b/projects/plugins/jetpack/extensions/blocks/map/controls.js @@ -27,12 +27,15 @@ export default ( { removeAPIKey, updateAPIKey, setPointVisibility, + mapProvider, } ) => { const updateAlignment = value => { setAttributes( { align: value } ); // Allow one cycle for alignment change to take effect - setTimeout( mapRef.current.sizeMap, 0 ); + if ( mapRef.current?.sizeMap ) { + setTimeout( mapRef.current.sizeMap, 0 ); + } }; /** @@ -51,7 +54,8 @@ export default ( { height = null; } else if ( null == mapHeight ) { // There was previously no height defined, so set the default. - height = mapRef.current.mapRef.current.offsetHeight; + const ref = mapRef?.current?.mapRef ?? mapRef; + height = ref?.current.offsetHeight; } else if ( height < minHeight ) { // Set map height to minimum size height = minHeight; @@ -61,7 +65,9 @@ export default ( { mapHeight: height, } ); - setTimeout( mapRef.current.sizeMap, 0 ); + if ( mapRef.current.sizeMap ) { + setTimeout( mapRef.current.sizeMap, 0 ); + } }; if ( context === 'toolbar' ) { @@ -110,7 +116,9 @@ export default ( { // If this input isn't focussed, the onBlur handler won't be triggered // to commit the map size, so we need to check for that. if ( event.target !== document.activeElement ) { - setTimeout( mapRef.current.sizeMap, 0 ); + if ( mapRef.current ) { + setTimeout( mapRef.current.sizeMap, 0 ); + } } } } onBlur={ onHeightChange } @@ -129,31 +137,39 @@ export default ( { ) } disabled={ attributes.points.length > 1 } - value={ attributes.zoom } + value={ Math.round( attributes.zoom ) } onChange={ value => { setAttributes( { zoom: value } ); - setTimeout( mapRef.current.updateZoom, 0 ); + if ( mapRef.current && mapRef.current.updateZoom ) { + setTimeout( mapRef.current.updateZoom, 0 ); + } } } min={ 0 } max={ 22 } /> - setAttributes( { mapDetails: value } ) } - /> + { mapProvider === 'mapbox' ? ( + setAttributes( { mapDetails: value } ) } + /> + ) : null } + setAttributes( { scrollToZoom: value } ) } /> - setAttributes( { showFullscreenButton: value } ) } - /> + + { mapProvider === 'mapbox' ? ( + setAttributes( { showFullscreenButton: value } ) } + /> + ) : null } { attributes.points.length ? ( @@ -165,40 +181,42 @@ export default ( { /> ) : null } - - - { __( 'You can optionally enter your own access token.', 'jetpack' ) }{ ' ' } - - { __( 'Find it on Mapbox', 'jetpack' ) } - - - ) - } - label={ __( 'Mapbox Access Token', 'jetpack' ) } - value={ state.apiKeyControl } - onChange={ value => setState( { apiKeyControl: value } ) } - /> - - - - - + { mapProvider === 'mapbox' ? ( + + + { __( 'You can optionally enter your own access token.', 'jetpack' ) }{ ' ' } + + { __( 'Find it on Mapbox', 'jetpack' ) } + + + ) + } + label={ __( 'Mapbox Access Token', 'jetpack' ) } + value={ state.apiKeyControl } + onChange={ value => setState( { apiKeyControl: value } ) } + /> + + + + + + ) : null } ); }; diff --git a/projects/plugins/jetpack/extensions/blocks/map/edit.js b/projects/plugins/jetpack/extensions/blocks/map/edit.js index c8a83bdc8e0b4..c477b82bd637f 100644 --- a/projects/plugins/jetpack/extensions/blocks/map/edit.js +++ b/projects/plugins/jetpack/extensions/blocks/map/edit.js @@ -14,11 +14,12 @@ import { Component, createRef, Fragment } from '@wordpress/element'; import { __ } from '@wordpress/i18n'; import { getActiveStyleName } from '../../shared/block-styles'; import AddPoint from './add-point'; -import Map from './component.js'; +import Map from './component'; import Controls from './controls'; import { getCoordinates } from './get-coordinates.js'; import previewPlaceholder from './map-preview.jpg'; import { settings } from './settings.js'; +import getMapProvider from './utils/get-map-provider'; const API_STATE_LOADING = 0; const API_STATE_FAILURE = 1; @@ -38,6 +39,7 @@ const RESIZABLE_BOX_ENABLE_OPTION = { bottomLeft: false, topLeft: false, }; + class MapEdit extends Component { constructor() { super( ...arguments ); @@ -48,7 +50,7 @@ class MapEdit extends Component { this.mapRef = createRef(); } geoCodeAddress = ( address, apiKey ) => { - if ( ! apiKey ) { + if ( ! apiKey || this.getMapProvider() === 'mapkit' ) { return; } getCoordinates( address, apiKey ) @@ -83,9 +85,17 @@ class MapEdit extends Component { componentDidUpdate = previousProps => { const address = this.props.attributes?.address; const previousAddress = previousProps.attributes?.address; + const className = this.props.attributes?.className; + const previousClassName = previousProps.attributes?.className; + if ( address && previousAddress !== address ) { this.geoCodeAddress( address, this.state.apiKey ); } + // fetch API key when switching from mapkit to mapbox + if ( className && previousClassName !== className && ! this.state.apiKey ) { + this.setState( { apiState: API_STATE_LOADING } ); + this.apiCall(); + } }; addPoint = point => { const { attributes, setAttributes } = this.props; @@ -157,12 +167,23 @@ class MapEdit extends Component { } ); } ); } + getMapProvider = () => { + const mapStyle = getActiveStyleName( settings.styles, this.props?.attributes?.className ); + return getMapProvider( { mapStyle } ); + }; + componentDidMount() { - this.apiCall().then( () => { - if ( this.props.attributes?.address ) { - this.geoCodeAddress( this.props.attributes?.address, this.state.apiKey ); - } - } ); + if ( this.getMapProvider() === 'mapbox' ) { + this.apiCall().then( () => { + if ( this.props.attributes?.address ) { + this.geoCodeAddress( this.props.attributes?.address, this.state.apiKey ); + } + } ); + } else { + this.setState( { + apiState: API_STATE_SUCCESS, + } ); + } } onError = ( code, message ) => { const { noticeOperations } = this.props; @@ -184,13 +205,19 @@ class MapEdit extends Component { onResizeStop(); - const height = parseInt( this.mapRef.current.mapRef.current.offsetHeight + delta.height, 10 ); + const ref = this.mapRef?.current?.mapRef ?? this.mapRef; - setAttributes( { - mapHeight: height, - } ); + if ( ref ) { + const height = parseInt( ref.current.offsetHeight + delta.height, 10 ); - setTimeout( this.mapRef.current.sizeMap, 0 ); + setAttributes( { + mapHeight: height, + } ); + + if ( ref.current.sizeMap ) { + setTimeout( ref.current.sizeMap, 0 ); + } + } }; render() { @@ -205,6 +232,7 @@ class MapEdit extends Component { onResizeStart, } = this.props; const { + address, mapDetails, points, zoom, @@ -214,8 +242,12 @@ class MapEdit extends Component { mapHeight, showFullscreenButton, } = attributes; + const { addPointVisibility, apiKey, apiKeyControl, apiState, apiRequestOutstanding } = this.state; + + const mapProvider = this.getMapProvider(); + const inspectorControls = ( <> @@ -226,6 +258,7 @@ class MapEdit extends Component { setPointVisibility={ this.setPointVisibility } context="toolbar" mapRef={ this.mapRef } + mapProvider={ mapProvider } /> @@ -239,6 +272,7 @@ class MapEdit extends Component { minHeight={ MIN_HEIGHT } removeAPIKey={ this.removeAPIKey } updateAPIKey={ this.updateAPIKey } + mapProvider={ mapProvider } /> @@ -313,6 +347,7 @@ class MapEdit extends Component {
this.setState( { addPointVisibility: ! points.length } ) } onMarkerClick={ () => this.setState( { addPointVisibility: false } ) } onError={ this.onError } + mapProvider={ mapProvider } > { isSelected && addPointVisibility && ( ) } diff --git a/projects/plugins/jetpack/extensions/blocks/map/location-search/index.js b/projects/plugins/jetpack/extensions/blocks/map/location-search/index.js index f169ff2cf1b14..c4b10aea57303 100644 --- a/projects/plugins/jetpack/extensions/blocks/map/location-search/index.js +++ b/projects/plugins/jetpack/extensions/blocks/map/location-search/index.js @@ -1,101 +1,10 @@ -import { BaseControl, TextControl } from '@wordpress/components'; -import { Component, createRef } from '@wordpress/element'; -import { __ } from '@wordpress/i18n'; -import Lookup from '../lookup'; +import MapboxLocationSearch from './mapbox'; +import MapkitLocationSearch from './mapkit'; -const placeholderText = __( 'Add a marker…', 'jetpack' ); - -export class LocationSearch extends Component { - constructor() { - super( ...arguments ); - - this.textRef = createRef(); - this.containerRef = createRef(); - this.state = { - isEmpty: true, - }; - this.autocompleter = { - name: 'placeSearch', - options: this.search, - isDebounced: true, - getOptionLabel: option => { option.place_name }, - getOptionKeywords: option => [ option.place_name ], - getOptionCompletion: this.getOptionCompletion, - }; - } - componentDidMount() { - setTimeout( () => { - this.containerRef.current.querySelector( 'input' ).focus(); - }, 50 ); - } - getOptionCompletion = option => { - const { value } = option; - const point = { - placeTitle: value.text, - title: value.text, - caption: value.place_name, - id: value.id, - coordinates: { - longitude: value.geometry.coordinates[ 0 ], - latitude: value.geometry.coordinates[ 1 ], - }, - }; - this.props.onAddPoint( point ); - return value.text; - }; - - search = value => { - const { apiKey, onError } = this.props; - const url = - 'https://api.mapbox.com/geocoding/v5/mapbox.places/' + - encodeURI( value ) + - '.json?access_token=' + - apiKey; - return new Promise( function ( resolve, reject ) { - const xhr = new XMLHttpRequest(); - xhr.open( 'GET', url ); - xhr.onload = function () { - if ( xhr.status === 200 ) { - const res = JSON.parse( xhr.responseText ); - resolve( res.features ); - } else { - const res = JSON.parse( xhr.responseText ); - onError( res.statusText, res.responseJSON.message ); - reject( new Error( 'Mapbox Places Error' ) ); - } - }; - xhr.send(); - } ); - }; - onReset = () => { - this.textRef.current.value = null; - }; - render() { - const { label } = this.props; - return ( -
- - - { ( { isExpanded, listBoxId, activeId, onChange, onKeyDown } ) => ( - - ) } - - -
- ); - } -} - -LocationSearch.defaultProps = { - onError: () => {}, +const LocationSearch = props => { + const LocationSearchComponent = + props.mapProvider === 'mapbox' ? MapboxLocationSearch : MapkitLocationSearch; + return ; }; export default LocationSearch; diff --git a/projects/plugins/jetpack/extensions/blocks/map/location-search/mapbox.js b/projects/plugins/jetpack/extensions/blocks/map/location-search/mapbox.js new file mode 100644 index 0000000000000..75ebd48cb1f37 --- /dev/null +++ b/projects/plugins/jetpack/extensions/blocks/map/location-search/mapbox.js @@ -0,0 +1,101 @@ +import { BaseControl, TextControl } from '@wordpress/components'; +import { Component, createRef } from '@wordpress/element'; +import { __ } from '@wordpress/i18n'; +import Lookup from '../lookup'; + +const placeholderText = __( 'Add a marker…', 'jetpack' ); + +export class MapboxLocationSearch extends Component { + constructor() { + super( ...arguments ); + + this.textRef = createRef(); + this.containerRef = createRef(); + this.state = { + isEmpty: true, + }; + this.autocompleter = { + name: 'placeSearch', + options: this.search, + isDebounced: true, + getOptionLabel: option => { option.place_name }, + getOptionKeywords: option => [ option.place_name ], + getOptionCompletion: this.getOptionCompletion, + }; + } + componentDidMount() { + setTimeout( () => { + this.containerRef.current.querySelector( 'input' ).focus(); + }, 50 ); + } + getOptionCompletion = option => { + const { value } = option; + const point = { + placeTitle: value.text, + title: value.text, + caption: value.place_name, + id: value.id, + coordinates: { + longitude: value.geometry.coordinates[ 0 ], + latitude: value.geometry.coordinates[ 1 ], + }, + }; + this.props.onAddPoint( point ); + return value.text; + }; + + search = value => { + const { apiKey, onError } = this.props; + const url = + 'https://api.mapbox.com/geocoding/v5/mapbox.places/' + + encodeURI( value ) + + '.json?access_token=' + + apiKey; + return new Promise( function ( resolve, reject ) { + const xhr = new XMLHttpRequest(); + xhr.open( 'GET', url ); + xhr.onload = function () { + if ( xhr.status === 200 ) { + const res = JSON.parse( xhr.responseText ); + resolve( res.features ); + } else { + const res = JSON.parse( xhr.responseText ); + onError( res.statusText, res.responseJSON.message ); + reject( new Error( 'Mapbox Places Error' ) ); + } + }; + xhr.send(); + } ); + }; + onReset = () => { + this.textRef.current.value = null; + }; + render() { + const { label } = this.props; + return ( +
+ + + { ( { isExpanded, listBoxId, activeId, onChange, onKeyDown } ) => ( + + ) } + + +
+ ); + } +} + +MapboxLocationSearch.defaultProps = { + onError: () => {}, +}; + +export default MapboxLocationSearch; diff --git a/projects/plugins/jetpack/extensions/blocks/map/location-search/mapkit.js b/projects/plugins/jetpack/extensions/blocks/map/location-search/mapkit.js new file mode 100644 index 0000000000000..74a2c6edcb6bf --- /dev/null +++ b/projects/plugins/jetpack/extensions/blocks/map/location-search/mapkit.js @@ -0,0 +1,95 @@ +import { BaseControl, TextControl } from '@wordpress/components'; +import { useEffect, useRef } from '@wordpress/element'; +import { __ } from '@wordpress/i18n'; +import Lookup from '../lookup'; +import { useMapkit } from '../mapkit/hooks'; + +const placeholderText = __( 'Add a marker…', 'jetpack' ); + +const MapkitLocationSearch = ( { label, onAddPoint } ) => { + const containerRef = useRef(); + const textRef = useRef(); + const { mapkit } = useMapkit(); + + const autocompleter = { + name: 'placeSearch', + options: async value => { + return new Promise( function ( resolve, reject ) { + const search = new mapkit.Search( { + includePointsOfInterest: false, + } ); + search.autocomplete( value, ( err, results ) => { + if ( err ) { + reject( err ); + return; + } + // filter out results without coordinates + const filtered = results?.results.filter( result => result.coordinate ) ?? []; + + // add placeName + const withPlaceName = filtered.map( result => ( { + ...result, + placeName: result.displayLines?.join( ', ' ), + } ) ); + + resolve( withPlaceName ); + } ); + } ); + }, + isDebounced: true, + getOptionLabel: option => { + return { option.placeName }; + }, + getOptionKeywords: option => [ option.placeName ], + getOptionCompletion: option => { + const { value } = option; + const point = { + placeTitle: value.placeName, + title: value.placeName, + caption: value.placeName, + coordinates: { + longitude: value.coordinate.longitude, + latitude: value.coordinate.latitude, + }, + // mapkit doesn't give us an id, so we'll make one containing the place name and coordinates + id: `${ value.placeName } ${ Number( value.coordinate.latitude ).toFixed( 2 ) } ${ Number( + value.coordinate.longitude + ).toFixed( 2 ) }`, + }; + onAddPoint( point ); + return value.placeName; + }, + }; + + const onReset = () => { + textRef.current.value = ''; + }; + + useEffect( () => { + setTimeout( () => { + containerRef.current.querySelector( 'input' ).focus(); + }, 50 ); + }, [] ); + + return ( +
+ + + { ( { isExpanded, listBoxId, activeId, onChange, onKeyDown } ) => ( + + ) } + + +
+ ); +}; + +export default MapkitLocationSearch; diff --git a/projects/plugins/jetpack/extensions/blocks/map/map.php b/projects/plugins/jetpack/extensions/blocks/map/map.php index f6ce0365952cb..7f351e95ed181 100644 --- a/projects/plugins/jetpack/extensions/blocks/map/map.php +++ b/projects/plugins/jetpack/extensions/blocks/map/map.php @@ -58,6 +58,31 @@ function wpcom_load_event( $access_token_source ) { } } +/** + * Function to determine which map provider to choose + * + * @param array $html The block's HTML - needed for the class name. + * + * @return string The name of the map provider. + */ +function get_map_provider( $html ) { + $mapbox_styles = array( 'is-style-terrain' ); + // return mapbox if html contains one of the mapbox styles + foreach ( $mapbox_styles as $style ) { + if ( strpos( $html, $style ) !== false ) { + return 'mapbox'; + } + } + + // you can override the map provider with a cookie + if ( isset( $_COOKIE['map_provider'] ) ) { + return sanitize_text_field( wp_unslash( $_COOKIE['map_provider'] ) ); + } + + // if we don't apply the filters & default to mapbox + return apply_filters( 'wpcom_map_block_map_provider', 'mapbox' ); +} + /** * Map block registration/dependency declaration. * @@ -68,7 +93,6 @@ function wpcom_load_event( $access_token_source ) { */ function load_assets( $attr, $content ) { $access_token = Jetpack_Mapbox_Helper::get_access_token(); - wpcom_load_event( $access_token['source'] ); if ( Blocks::is_amp_request() ) { @@ -101,7 +125,12 @@ function load_assets( $attr, $content ) { Jetpack_Gutenberg::load_assets_as_required( FEATURE_NAME ); - return preg_replace( '/
-1 - ? window.innerHeight - : window.innerHeight * 0.8; - const blockHeight = Math.min( blockWidth * ( 3 / 4 ), maxHeight ); - container.style.height = blockHeight + 'px'; - } -} - export function setMarkerHTML( el, markerColor ) { el.innerHTML = ` diff --git a/projects/plugins/jetpack/extensions/blocks/map/mapkit-utils/index.js b/projects/plugins/jetpack/extensions/blocks/map/mapkit-utils/index.js new file mode 100644 index 0000000000000..604443b76a251 --- /dev/null +++ b/projects/plugins/jetpack/extensions/blocks/map/mapkit-utils/index.js @@ -0,0 +1,141 @@ +import { waitForObject } from '../../../shared/block-editor-asset-loader'; + +const earthRadius = 6.371e6; + +function getMetersPerPixel( latitude ) { + return Math.abs( ( earthRadius * Math.cos( ( latitude * Math.PI ) / 180 ) * 2 * Math.PI ) / 256 ); +} + +function convertZoomLevelToCameraDistance( zoomLevel, latitude ) { + const altitude = ( 512 / Math.pow( 2, zoomLevel ) ) * 0.5; // altitude in pixels + return altitude * getMetersPerPixel( latitude ); +} + +function convertCameraDistanceToZoomLevel( cameraDistance, latitude ) { + const altitude = cameraDistance / getMetersPerPixel( latitude ); + return Math.log2( 512 / ( altitude / 0.5 ) ); +} + +function pointsToMapRegion( mapkit, points ) { + if ( points.length === 0 ) { + return null; + } + + const topLeftCoord = new mapkit.Coordinate( -90, 180 ); + const bottomRightCoord = new mapkit.Coordinate( 90, -180 ); + + points.forEach( point => { + topLeftCoord.latitude = Math.max( topLeftCoord.latitude, point.coordinates.latitude ); + topLeftCoord.longitude = Math.min( topLeftCoord.longitude, point.coordinates.longitude ); + bottomRightCoord.latitude = Math.min( bottomRightCoord.latitude, point.coordinates.latitude ); + bottomRightCoord.longitude = Math.max( + bottomRightCoord.longitude, + point.coordinates.longitude + ); + } ); + + const center = new mapkit.Coordinate( + topLeftCoord.latitude - ( topLeftCoord.latitude - bottomRightCoord.latitude ) * 0.5, + topLeftCoord.longitude + ( bottomRightCoord.longitude - topLeftCoord.longitude ) * 0.5 + ); + + const span = new mapkit.CoordinateSpan( + Math.abs( topLeftCoord.latitude - bottomRightCoord.latitude ) * 1.3, + Math.abs( bottomRightCoord.longitude - topLeftCoord.longitude ) * 1.3 + ); + + return new mapkit.CoordinateRegion( center, span ); +} + +function createCalloutElementCallback( currentDoc, callback ) { + return () => { + const element = currentDoc.createElement( 'div' ); + element.classList.add( 'mapkit-popup-content' ); + callback( element ); + return element; + }; +} + +function waitUntilMapkitIsInitialized( currentWindow ) { + return new Promise( ( resolve, reject ) => { + const check = () => { + if ( typeof currentWindow.mapkitIsInitializing === 'undefined' ) { + reject(); + } else if ( currentWindow.mapkitIsInitializing === false ) { + resolve(); + } else { + currentWindow.requestAnimationFrame( check ); + } + }; + check(); + } ); +} + +function loadMapkitLibrary( currentDoc, currentWindow ) { + return new Promise( resolve => { + if ( currentWindow.mapkitScriptIsLoading ) { + waitForObject( currentWindow, 'mapkit' ).then( mapkitObj => { + resolve( mapkitObj ); + } ); + } else { + currentWindow.mapkitScriptIsLoading = true; + + const element = currentDoc.createElement( 'script' ); + element.addEventListener( + 'load', + async () => { + const mapkitObj = await waitForObject( currentWindow, 'mapkit' ); + currentWindow.mapkitScriptIsLoading = false; + + resolve( mapkitObj ); + }, + { once: true } + ); + element.src = 'https://cdn.apple-mapkit.com/mk/5.x.x/mapkit.js'; + element.crossOrigin = 'anonymous'; + currentDoc.head.appendChild( element ); + } + } ); +} + +function fetchMapkitKey( mapkitObj, blogId, currentWindow ) { + return new Promise( ( resolve, reject ) => { + if ( currentWindow.mapkitIsInitialized ) { + resolve(); + } else if ( currentWindow.mapkitIsInitializing ) { + waitUntilMapkitIsInitialized( currentWindow ).then( () => { + resolve(); + } ); + } else { + currentWindow.mapkitIsInitializing = true; + currentWindow.mapkitIsInitialized = false; + mapkitObj.init( { + authorizationCallback: async done => { + try { + const response = await fetch( `https://public-api.wordpress.com/wpcom/v2/mapkit` ); + if ( response.status === 200 ) { + const data = await response.json(); + done( data.wpcom_mapkit_access_token ); + } else { + reject(); + } + currentWindow.mapkitIsInitializing = false; + currentWindow.mapkitIsInitialized = true; + resolve(); + } catch ( error ) { + reject(); + } + }, + } ); + } + } ); +} + +export { + convertZoomLevelToCameraDistance, + convertCameraDistanceToZoomLevel, + createCalloutElementCallback, + fetchMapkitKey, + loadMapkitLibrary, + pointsToMapRegion, +}; diff --git a/projects/plugins/jetpack/extensions/blocks/map/mapkit/context/index.js b/projects/plugins/jetpack/extensions/blocks/map/mapkit/context/index.js new file mode 100644 index 0000000000000..475ad2a2aa670 --- /dev/null +++ b/projects/plugins/jetpack/extensions/blocks/map/mapkit/context/index.js @@ -0,0 +1,38 @@ +import { createContext, useState } from '@wordpress/element'; + +const MapkitContext = createContext( { + map: null, + mapkit: null, + loaded: false, + activeMarker: null, + calloutReference: null, + currentDoc: null, + currentWindow: null, + admin: false, + setPoints: () => {}, + points: [], + previousCenter: null, +} ); + +const MapkitProvider = ( { value, children } ) => { + const [ activeMarker, setActiveMarker ] = useState( null ); + const [ calloutReference, setCalloutReference ] = useState( null ); + const [ previousCenter, setPreviousCenter ] = useState( null ); + return ( + + { children } + + ); +}; + +export { MapkitContext, MapkitProvider }; diff --git a/projects/plugins/jetpack/extensions/blocks/map/mapkit/hooks/index.js b/projects/plugins/jetpack/extensions/blocks/map/mapkit/hooks/index.js new file mode 100644 index 0000000000000..84ca2f9eb36d1 --- /dev/null +++ b/projects/plugins/jetpack/extensions/blocks/map/mapkit/hooks/index.js @@ -0,0 +1,285 @@ +import { CONNECTION_STORE_ID } from '@automattic/jetpack-connection'; +import { select } from '@wordpress/data'; +import { useContext, useEffect, useRef, useState } from '@wordpress/element'; +import { __ } from '@wordpress/i18n'; +import { debounce } from 'lodash'; +import { getLoadContext } from '../../../../shared/block-editor-asset-loader'; +import { + convertZoomLevelToCameraDistance, + convertCameraDistanceToZoomLevel, + fetchMapkitKey, + loadMapkitLibrary, + pointsToMapRegion, +} from '../../mapkit-utils'; +import { MapkitContext } from '../context'; + +const useMapkit = () => { + return useContext( MapkitContext ); +}; + +// mapRef can be a ref to the element that will render the map +// or a ref to the element that will be on the page when the map is rendered. +// It is only used here to determine the document and window to use. +const useMapkitSetup = mapRef => { + const [ loaded, setLoaded ] = useState( false ); + const [ error, setError ] = useState( false ); + const [ mapkit, setMapkit ] = useState( null ); + const [ _currentWindow, setCurrentWindow ] = useState( null ); + const [ _currentDoc, setCurrentDoc ] = useState( null ); + + useEffect( () => { + const blog_id = select( CONNECTION_STORE_ID ).getBlogId(); + const { currentDoc, currentWindow } = getLoadContext( mapRef.current ); + + if ( mapRef.current ) { + setCurrentWindow( currentWindow ); + setCurrentDoc( currentDoc ); + + const fetchMapkitKeyErrorMessage = __( + 'Failed to retrieve a Mapkit API token. Please try refreshing.', + 'jetpack' + ); + + // If mapkit is already loaded, reuse it. + if ( currentWindow.mapkit ) { + setMapkit( currentWindow.mapkit ); + // Fetch API key in the off chance that mapkit is available but not initialized for some reason + // It will just resolve in case it is already initialized. + fetchMapkitKey( currentWindow.mapkit, blog_id, currentWindow ).then( + () => { + setLoaded( true ); + }, + () => { + setError( fetchMapkitKeyErrorMessage ); + } + ); + } else { + loadMapkitLibrary( currentDoc, currentWindow ).then( mapkitObj => { + setMapkit( mapkitObj ); + + fetchMapkitKey( mapkitObj, blog_id, currentWindow ).then( + () => { + setLoaded( true ); + }, + () => { + setError( fetchMapkitKeyErrorMessage ); + } + ); + } ); + } + } + }, [ mapRef ] ); + + return { loaded, error, mapkit, currentDoc: _currentDoc, currentWindow: _currentWindow }; +}; + +const useMapkitInit = ( mapkit, loaded, mapRef ) => { + const [ map, setMap ] = useState( null ); + useEffect( () => { + if ( mapkit && loaded ) { + setMap( new mapkit.Map( mapRef.current ) ); + } + }, [ mapkit, loaded, mapRef ] ); + return { map }; +}; + +const useMapkitCenter = ( center, setCenter ) => { + const { mapkit, map } = useMapkit(); + const memoizedCenter = useRef( center ); + const memoizedSetCenter = useRef( setCenter ); + + useEffect( () => { + if ( ! mapkit || ! map || ! memoizedCenter.current ) { + return; + } + + const lat = memoizedCenter.current?.lat ?? memoizedCenter.current?.latitude; + const lng = memoizedCenter.current?.lng ?? memoizedCenter.current?.longitude; + + if ( typeof lat === 'number' && typeof lng === 'number' ) { + map.center = new mapkit.Coordinate( lat, lng ); + } + }, [ mapkit, map, memoizedCenter ] ); + + useEffect( () => { + if ( ! mapkit || ! map ) { + return; + } + + const changeRegion = () => { + if ( map.center ) { + const { latitude, longitude } = map.center; + memoizedSetCenter.current( { lat: latitude, lng: longitude } ); + } + }; + + map.addEventListener( 'region-change-end', debounce( changeRegion, 1000 ) ); + + return () => { + map.removeEventListener( 'region-change-end', changeRegion ); + }; + }, [ mapkit, map, memoizedSetCenter ] ); +}; + +const useMapkitType = mapStyle => { + const { mapkit, map } = useMapkit(); + + useEffect( () => { + if ( ! mapkit || ! map ) { + return; + } + map.mapType = ( () => { + switch ( mapStyle ) { + case 'satellite': + return mapkit.Map.MapTypes.Satellite; + case 'black_and_white': + return mapkit.Map.MapTypes.MutedStandard; + case 'hybrid': + return mapkit.Map.MapTypes.Hybrid; + default: + return mapkit.Map.MapTypes.Standard; + } + } )(); + }, [ mapkit, map, mapStyle ] ); +}; + +const useMapkitZoom = ( zoom, setZoom ) => { + const { mapkit, map, points } = useMapkit(); + + useEffect( () => { + if ( mapkit && map ) { + if ( points && points.length <= 1 ) { + const defaultCameraDistance = convertZoomLevelToCameraDistance( 13, map.center.latitude ); + + if ( zoom ) { + const cameraDistance = convertZoomLevelToCameraDistance( zoom, map.center.latitude ); + if ( cameraDistance !== map.cameraDistance ) { + map.cameraDistance = cameraDistance; + } + } else if ( defaultCameraDistance !== map.cameraDistance ) { + map.cameraDistance = defaultCameraDistance; + } + // Zooming and scrolling are enabled when there are 0 or 1 points. + map.isZoomEnabled = true; + map.isScrollEnabled = true; + } else { + map.region = pointsToMapRegion( mapkit, points ); + // Zooming and scrolling are disabled when there are multiple points. + map.isZoomEnabled = false; + map.isScrollEnabled = false; + } + } + }, [ mapkit, map, zoom, points ] ); + + useEffect( () => { + const changeZoom = () => { + setZoom( convertCameraDistanceToZoomLevel( map.cameraDistance, map.center.latitude ) ); + }; + + map.addEventListener( 'zoom-end', changeZoom ); + + return () => { + map.removeEventListener( 'zoom-end', changeZoom ); + }; + }, [ mapkit, map, setZoom ] ); +}; + +const useMapkitPoints = ( points, markerColor, callOutElement = null, onSelect = null ) => { + const { mapkit, map, loaded } = useMapkit(); + + // avoid rerenders by making these refs + const callOutElementRef = useRef( callOutElement ); + const onSelectRef = useRef( onSelect ); + + useEffect( () => { + if ( loaded ) { + map.removeAnnotations( map.annotations ); + const annotations = points.map( point => { + const marker = new mapkit.MarkerAnnotation( + new mapkit.Coordinate( point.coordinates.latitude, point.coordinates.longitude ), + { color: markerColor } + ); + marker.calloutEnabled = true; + marker.title = point.title; + if ( callOutElementRef.current ) { + marker.callout = { + calloutElementForAnnotation: callOutElementRef.current, + }; + } + if ( onSelectRef.current ) { + marker.addEventListener( 'select', () => onSelectRef.current( point, map ) ); + } + return marker; + } ); + map.showItems( annotations ); + } + }, [ points, loaded, map, mapkit, markerColor, callOutElementRef, onSelectRef ] ); +}; + +const useMapkitOnMapLoad = onMapLoad => { + const { map, loaded } = useMapkit(); + const onMapLoadRef = useRef( onMapLoad ); + + useEffect( () => { + if ( loaded ) { + onMapLoadRef.current( map ); + } + }, [ loaded, map, onMapLoadRef ] ); +}; + +const useMapkitOnMapTap = onMapTap => { + const { map, previousCenter, loaded } = useMapkit(); + const onMapTapRef = useRef( onMapTap ); + + useEffect( () => { + if ( loaded ) { + map.addEventListener( 'single-tap', () => { + onMapTapRef.current( previousCenter ); + } ); + } + }, [ loaded, map, previousCenter, onMapTapRef ] ); +}; + +const useMapkitAddressLookup = ( address, onSetPointsRef ) => { + const { mapkit, map } = useMapkit(); + + useEffect( () => { + if ( mapkit && map && address?.length ) { + const geocoder = new mapkit.Geocoder(); + geocoder.lookup( address, ( error, data ) => { + if ( data?.results?.length ) { + const place = data.results[ 0 ]; + const title = place.formattedAddress; + const point = { + placeTitle: title, + title: title, + caption: title, + coordinates: { + longitude: place.coordinate.longitude, + latitude: place.coordinate.latitude, + }, + // mapkit doesn't give us an id, so we'll make one containing the place name and coordinates + id: `${ title } ${ Number( place.coordinate.latitude ).toFixed( 2 ) } ${ Number( + place.coordinate.longitude + ).toFixed( 2 ) }`, + }; + + onSetPointsRef.current( [ point ] ); + } + } ); + } + }, [ mapkit, map, address, onSetPointsRef ] ); +}; + +export { + useMapkit, + useMapkitSetup, + useMapkitInit, + useMapkitZoom, + useMapkitType, + useMapkitCenter, + useMapkitPoints, + useMapkitOnMapLoad, + useMapkitOnMapTap, + useMapkitAddressLookup, +}; diff --git a/projects/plugins/jetpack/extensions/blocks/map/map-theme_black_and_white.jpg b/projects/plugins/jetpack/extensions/blocks/map/previews/map-theme_black_and_white.jpg similarity index 100% rename from projects/plugins/jetpack/extensions/blocks/map/map-theme_black_and_white.jpg rename to projects/plugins/jetpack/extensions/blocks/map/previews/map-theme_black_and_white.jpg diff --git a/projects/plugins/jetpack/extensions/blocks/map/previews/map-theme_black_and_white_mapkit.jpg b/projects/plugins/jetpack/extensions/blocks/map/previews/map-theme_black_and_white_mapkit.jpg new file mode 100644 index 0000000000000..5cb7f513b90e1 Binary files /dev/null and b/projects/plugins/jetpack/extensions/blocks/map/previews/map-theme_black_and_white_mapkit.jpg differ diff --git a/projects/plugins/jetpack/extensions/blocks/map/map-theme_default.jpg b/projects/plugins/jetpack/extensions/blocks/map/previews/map-theme_default.jpg similarity index 100% rename from projects/plugins/jetpack/extensions/blocks/map/map-theme_default.jpg rename to projects/plugins/jetpack/extensions/blocks/map/previews/map-theme_default.jpg diff --git a/projects/plugins/jetpack/extensions/blocks/map/previews/map-theme_default_mapkit.jpg b/projects/plugins/jetpack/extensions/blocks/map/previews/map-theme_default_mapkit.jpg new file mode 100644 index 0000000000000..fff7a89ae2c85 Binary files /dev/null and b/projects/plugins/jetpack/extensions/blocks/map/previews/map-theme_default_mapkit.jpg differ diff --git a/projects/plugins/jetpack/extensions/blocks/map/map-theme_satellite.jpg b/projects/plugins/jetpack/extensions/blocks/map/previews/map-theme_satellite.jpg similarity index 100% rename from projects/plugins/jetpack/extensions/blocks/map/map-theme_satellite.jpg rename to projects/plugins/jetpack/extensions/blocks/map/previews/map-theme_satellite.jpg diff --git a/projects/plugins/jetpack/extensions/blocks/map/previews/map-theme_satellite_mapkit.jpg b/projects/plugins/jetpack/extensions/blocks/map/previews/map-theme_satellite_mapkit.jpg new file mode 100644 index 0000000000000..30f90a70cf719 Binary files /dev/null and b/projects/plugins/jetpack/extensions/blocks/map/previews/map-theme_satellite_mapkit.jpg differ diff --git a/projects/plugins/jetpack/extensions/blocks/map/map-theme_terrain.jpg b/projects/plugins/jetpack/extensions/blocks/map/previews/map-theme_terrain.jpg similarity index 100% rename from projects/plugins/jetpack/extensions/blocks/map/map-theme_terrain.jpg rename to projects/plugins/jetpack/extensions/blocks/map/previews/map-theme_terrain.jpg diff --git a/projects/plugins/jetpack/extensions/blocks/map/settings.js b/projects/plugins/jetpack/extensions/blocks/map/settings.js index 1f09c3c6211df..82d63740ee180 100644 --- a/projects/plugins/jetpack/extensions/blocks/map/settings.js +++ b/projects/plugins/jetpack/extensions/blocks/map/settings.js @@ -1,10 +1,16 @@ // Disable forbidden etc. so that frontend component does not depend on @wordpress/component /* eslint-disable react/forbid-elements */ import { __, _x } from '@wordpress/i18n'; -import blackAndWhiteTheme from './map-theme_black_and_white.jpg'; -import defaultTheme from './map-theme_default.jpg'; -import satelliteTheme from './map-theme_satellite.jpg'; -import terrainTheme from './map-theme_terrain.jpg'; +import blackAndWhiteTheme from './previews/map-theme_black_and_white.jpg'; +import blackAndWhiteThemeMapkit from './previews/map-theme_black_and_white_mapkit.jpg'; +import defaultTheme from './previews/map-theme_default.jpg'; +import defaultThemeMapkit from './previews/map-theme_default_mapkit.jpg'; +import satelliteTheme from './previews/map-theme_satellite.jpg'; +import satelliteThemeMapkit from './previews/map-theme_satellite_mapkit.jpg'; +import terrainTheme from './previews/map-theme_terrain.jpg'; +import { getMapProvider } from './utils'; + +const provider = getMapProvider(); export const settings = { name: 'map', @@ -87,18 +93,21 @@ export const settings = { { name: 'default', label: __( 'Basic', 'jetpack' ), - preview: defaultTheme, + preview: provider === 'mapkit' ? defaultThemeMapkit : defaultTheme, isDefault: true, }, { name: 'black_and_white', - label: __( 'Black and white', 'jetpack' ), - preview: blackAndWhiteTheme, + label: + provider === 'mapkit' + ? __( 'Muted', 'jetpack', /* dummy arg to avoid bad minification */ 0 ) + : __( 'Black & White', 'jetpack' ), + preview: provider === 'mapkit' ? blackAndWhiteThemeMapkit : blackAndWhiteTheme, }, { name: 'satellite', label: __( 'Satellite', 'jetpack' ), - preview: satelliteTheme, + preview: provider === 'mapkit' ? satelliteThemeMapkit : satelliteTheme, }, { name: 'terrain', diff --git a/projects/plugins/jetpack/extensions/blocks/map/style.scss b/projects/plugins/jetpack/extensions/blocks/map/style.scss index d3f5dc567196d..2b96907f1c0ea 100644 --- a/projects/plugins/jetpack/extensions/blocks/map/style.scss +++ b/projects/plugins/jetpack/extensions/blocks/map/style.scss @@ -8,6 +8,30 @@ min-height: 400px; text-align: left; } + + .mapkit-popup-content { + background: #fff; + border-radius: 3px; + box-shadow: 0 1px 2px rgb(0 0 0 / 10%); + max-width: 300px; + min-width: 150px; + padding: 10px 10px 15px; + pointer-events: auto; + + h3 { + font-size: 1.3125em; + font-weight: 400; + margin-bottom: 0.5rem; + } + p { + margin-bottom: 0; + } + } + + .wp-block-jetpack-map__mb-container { + height: 400px; + } + .mapboxgl-popup { h3 { font-size: 1.3125em; diff --git a/projects/plugins/jetpack/extensions/blocks/map/test/controls.js b/projects/plugins/jetpack/extensions/blocks/map/test/controls.js index a185fd1d5af43..209b78be1f776 100644 --- a/projects/plugins/jetpack/extensions/blocks/map/test/controls.js +++ b/projects/plugins/jetpack/extensions/blocks/map/test/controls.js @@ -44,6 +44,7 @@ const defaultProps = { apiRequestOutstanding: false, }, setState: jest.fn(), + mapProvider: 'mapbox', }; describe( 'Inspector controls', () => { @@ -72,30 +73,59 @@ describe( 'Inspector controls', () => { expect( screen.getByText( 'Zoom level' ) ).toBeInTheDocument(); } ); - test( 'street names toggle shows correctly', () => { + test( 'street names toggle shows correctly when mapProvider is mapbox', () => { render( ); expect( screen.getByText( 'Show street names' ) ).toBeInTheDocument(); } ); - test( 'scroll to zoom toggle shows correctly', () => { + test( "street names toggle shows doesn't show when mapProvider is mapkit", () => { + const props = { ...defaultProps, mapProvider: 'mapkit' }; + + render( ); + + expect( screen.queryByText( 'Show street names' ) ).not.toBeInTheDocument(); + } ); + + test( 'scroll to zoom toggle shows correctly when mapProvider is mapbox', () => { render( ); expect( screen.getByText( 'Scroll to zoom' ) ).toBeInTheDocument(); } ); - test( 'show fullscreen button toggle shows correctly', () => { + test( 'scroll to zoom toggle shows correctly when mapProvider when mapProvider is mapkit', () => { + const props = { ...defaultProps, mapProvider: 'mapkit' }; + + render( ); + + expect( screen.getByText( 'Scroll to zoom' ) ).toBeInTheDocument(); + } ); + + test( 'show fullscreen button toggle shows correctly when mapProvider is mapbox', () => { render( ); expect( screen.getByText( 'Show Fullscreen Button' ) ).toBeInTheDocument(); } ); + + test( 'show fullscreen button toggle shows correctly', () => { + const props = { ...defaultProps, mapProvider: 'mapkit' }; + + render( ); + + expect( screen.queryByText( 'Show Fullscreen Button' ) ).not.toBeInTheDocument(); + } ); } ); describe( 'Mapbox access token panel', () => { - test( 'mapbox access token input shows correctly', () => { + test( 'mapbox access token input shows correctly when mapProvider is mapbox', () => { render( ); - expect( screen.getByText( 'Mapbox Access Token' ) ).toBeInTheDocument(); } ); + + test( "mapbox access token input doesn't show when mapProvider is mapkit", () => { + const props = { ...defaultProps, mapProvider: 'mapkit' }; + render( ); + expect( screen.queryByText( 'Mapbox Access Token' ) ).not.toBeInTheDocument(); + } ); } ); } ); diff --git a/projects/plugins/jetpack/extensions/blocks/map/utils/get-map-provider.js b/projects/plugins/jetpack/extensions/blocks/map/utils/get-map-provider.js new file mode 100644 index 0000000000000..bd57354dbaa57 --- /dev/null +++ b/projects/plugins/jetpack/extensions/blocks/map/utils/get-map-provider.js @@ -0,0 +1,28 @@ +function getCookie( name ) { + const value = `; ${ document.cookie }`; + const parts = value.split( `; ${ name }=` ); + if ( parts.length === 2 ) { + return parts.pop().split( ';' ).shift(); + } +} + +const getMapProvider = props => { + const mapboxStyles = [ 'terrain' ]; + if ( props?.mapStyle && mapboxStyles.includes( props.mapStyle ) ) { + return 'mapbox'; + } + + const mapProviderCookie = getCookie( 'map_provider' ); + if ( mapProviderCookie ) { + return mapProviderCookie; + } + + if ( window && typeof window.Jetpack_Maps?.provider === 'string' ) { + if ( [ 'mapbox', 'mapkit' ].includes( window.Jetpack_Maps?.provider ) ) { + return window.map_block_settings?.provider; + } + } + return 'mapbox'; +}; + +export default getMapProvider; diff --git a/projects/plugins/jetpack/extensions/blocks/map/utils/index.js b/projects/plugins/jetpack/extensions/blocks/map/utils/index.js new file mode 100644 index 0000000000000..0814b85627724 --- /dev/null +++ b/projects/plugins/jetpack/extensions/blocks/map/utils/index.js @@ -0,0 +1,4 @@ +import getMapProvider from './get-map-provider'; +import resizeMapContainer from './resize-map-container'; + +export { getMapProvider, resizeMapContainer }; diff --git a/projects/plugins/jetpack/extensions/blocks/map/utils/resize-map-container.js b/projects/plugins/jetpack/extensions/blocks/map/utils/resize-map-container.js new file mode 100644 index 0000000000000..dd566ed01931d --- /dev/null +++ b/projects/plugins/jetpack/extensions/blocks/map/utils/resize-map-container.js @@ -0,0 +1,15 @@ +const resizeMapContainer = ( container, fixedHeight = null ) => { + if ( fixedHeight ) { + container.style.height = fixedHeight + 'px'; + } else { + const blockWidth = container.offsetWidth; + const maxHeight = + window.location.search.indexOf( 'map-block-counter' ) > -1 + ? window.innerHeight + : window.innerHeight * 0.8; + const blockHeight = Math.min( blockWidth * ( 3 / 4 ), maxHeight ); + container.style.height = blockHeight + 'px'; + } +}; + +export default resizeMapContainer; diff --git a/projects/plugins/jetpack/extensions/blocks/map/view.js b/projects/plugins/jetpack/extensions/blocks/map/view.js index 76a909dc95fb3..91ff1bfbb364c 100644 --- a/projects/plugins/jetpack/extensions/blocks/map/view.js +++ b/projects/plugins/jetpack/extensions/blocks/map/view.js @@ -1,210 +1,19 @@ import domReady from '@wordpress/dom-ready'; -import { - getLoadContext, - loadBlockEditorAssets, - waitForObject, -} from '../../shared/block-editor-asset-loader'; -import { debounce } from '../../shared/debounce'; -import editorAssets from './block-editor-assets.json'; -import { mapboxMapFormatter } from './mapbox-map-formatter/'; -import { - createInfoWindowPopup, - fitMapToBounds, - getMapBounds, - googlePoint2Mapbox, - resizeMapContainer, - setMarkerHTML, -} from './mapbox-utils'; - import './style.scss'; -import './map-marker/style.scss'; - -class MapBlock { - constructor( root, onError = () => {} ) { - // Root element for the block. - this.root = root; - - const { currentDoc, currentWindow } = getLoadContext( this.root ); - this.document = currentDoc; - this.window = currentWindow; - this.onError = onError; - - // Block attributes. - this.mapStyle = this.root.getAttribute( 'data-map-style' ) || 'default'; - this.mapDetails = this.root.getAttribute( 'data-map-details' ) === 'true'; - this.apiKey = this.root.getAttribute( 'data-api-key' ) || null; - this.scrollToZoom = this.root.getAttribute( 'data-scroll-to-zoom' ) === 'true'; - this.showFullscreenButton = this.root.getAttribute( 'data-show-fullscreen-button' ) === 'true'; - this.points = JSON.parse( this.root.getAttribute( 'data-points' ) || '[]' ); - this.mapCenter = JSON.parse( this.root.getAttribute( 'data-map-center' ) || '{}' ); - this.mapHeight = this.root.getAttribute( 'data-map-height' ) || null; - this.markerColor = this.root.getAttribute( 'data-marker-color' ) || 'red'; - const zoom = this.root.getAttribute( 'data-zoom' ); - this.zoom = zoom && zoom.length ? parseInt( this.root.getAttribute( 'data-zoom' ), 10 ) : 13; - - this.activeMarker = null; - - // Hide list of markers, if present. - const markerList = this.root.querySelector( 'ul' ); - if ( markerList ) { - markerList.style.display = 'none'; - } - - if ( ! this.apiKey || ! this.apiKey.length ) { - throw new Error( 'API key missing' ); - } - } - - initDOM() { - this.root.innerHTML = `
`; - this.container = this.root.querySelector( '.wp-block-jetpack-map__gm-container' ); - } - - loadMapLibraries() { - return new Promise( resolve => { - const callbacks = { - 'mapbox-gl-js': () => { - waitForObject( this.window, 'mapboxgl' ).then( mapboxgl => { - this.mapboxgl = mapboxgl; - mapboxgl.accessToken = this.apiKey; - resolve( mapboxgl ); - } ); - }, - }; - - loadBlockEditorAssets( editorAssets, callbacks, this.root ); - } ); - } - - initMap() { - try { - this.map = new this.mapboxgl.Map( { - container: this.container, - style: mapboxMapFormatter( this.mapStyle, this.mapDetails ), - center: googlePoint2Mapbox( this.mapCenter ), - zoom: this.zoom, - pitchWithRotate: false, - attributionControl: false, - dragRotate: false, - } ); - } catch ( e ) { - this.onError( 'mapbox_error', e.message ); - return; - } - - // Disable scroll zooming if not enabled in block options. - if ( ! this.scrollToZoom ) { - this.map.scrollZoom.disable(); - } - - if ( this.showFullscreenButton ) { - this.map.addControl( new this.mapboxgl.FullscreenControl() ); - } - - this.map.on( 'error', e => { - this.onError( 'mapbox_error', e.error.message ); - } ); - - this.zoomControl = new this.mapboxgl.NavigationControl( { - showCompass: false, - showZoom: true, - } ); - } - - initInfoWindow() { - this.infoWindowContent = this.document.createElement( 'div' ); - this.infoWindow = createInfoWindowPopup( this.mapboxgl ); - this.infoWindow.setDOMContent( this.infoWindowContent ); - } - - setBoundsByMarkers() { - if ( ! this.map ) { - return; - } - this.map.dragPan.enable(); - // If there are no points at all, there is no data to set bounds to. Abort the function. - if ( ! this.points.length ) { - return; - } - // If there is an open info window, resizing will probably move the info window which complicates interaction. - if ( this.activeMarker ) { - return; - } - - const bounds = getMapBounds( this.mapboxgl, this.points ); - - if ( this.points.length > 1 ) { - fitMapToBounds( this.map, bounds ); - } else { - // If there is only one point, center map around it. - this.map.setCenter( bounds.getCenter() ); - this.map.addControl( this.zoomControl ); - } - } - - sizeMap = () => { - resizeMapContainer( this.container, this.mapHeight ); - this.map.resize(); - this.setBoundsByMarkers(); - }; - - initMapSize() { - this.setBoundsByMarkers(); - this.debouncedSizeMap = debounce( this.sizeMap, 250 ); - this.debouncedSizeMap(); - } - - closeInfoWindow = () => { - this.activeMarker = null; - this.infoWindow.remove(); - }; - - initHandlers() { - this.map.getCanvas().addEventListener( 'click', this.closeInfoWindow ); - window.addEventListener( 'resize', this.debouncedSizeMap ); - } - - showInfoWindow( marker, point ) { - const mapboxPoint = [ point.coordinates.longitude, point.coordinates.latitude ]; - this.activeMarker = marker; - this.infoWindowContent.innerHTML = `

`; - this.infoWindowContent.querySelector( 'h3' ).textContent = point.title; - this.infoWindowContent.querySelector( 'p' ).textContent = point.caption; - this.infoWindow.setLngLat( mapboxPoint ).addTo( this.map ); - } - - initMarkers() { - this.points.forEach( point => { - const mapboxPoint = [ point.coordinates.longitude, point.coordinates.latitude ]; - const el = this.document.createElement( 'div' ); - el.className = 'wp-block-jetpack-map-marker'; - const marker = new this.mapboxgl.Marker( el ) - .setLngLat( mapboxPoint ) - .setOffset( [ 0, -19 ] ) - .addTo( this.map ); - - marker.getElement().addEventListener( 'click', () => this.showInfoWindow( marker, point ) ); - setMarkerHTML( el, this.markerColor ); - } ); - } - - async init() { - this.initDOM(); - await this.loadMapLibraries(); - this.initMap(); - this.initInfoWindow(); - this.initMapSize(); - this.initHandlers(); - this.initMarkers(); - } -} +import './component/map-marker/style.scss'; +import { MapBoxBlock, MapkitBlock } from './view/'; domReady( function () { Array.from( document.querySelectorAll( '.wp-block-jetpack-map' ) ).forEach( async blockRoot => { try { - const block = new MapBlock( blockRoot ); - block.init(); - } catch { + if ( blockRoot.getAttribute( 'data-map-provider' ) === 'mapkit' ) { + const block = new MapkitBlock( blockRoot ); + block.init(); + } else { + const block = new MapBoxBlock( blockRoot ); + block.init(); + } + } catch ( e ) { // Ignore error. } } ); diff --git a/projects/plugins/jetpack/extensions/blocks/map/view/index.js b/projects/plugins/jetpack/extensions/blocks/map/view/index.js new file mode 100644 index 0000000000000..9d29daa0c693a --- /dev/null +++ b/projects/plugins/jetpack/extensions/blocks/map/view/index.js @@ -0,0 +1,3 @@ +import MapBoxBlock from './mapbox'; +import MapkitBlock from './mapkit'; +export { MapBoxBlock, MapkitBlock }; diff --git a/projects/plugins/jetpack/extensions/blocks/map/view/mapbox.js b/projects/plugins/jetpack/extensions/blocks/map/view/mapbox.js new file mode 100644 index 0000000000000..1bfafe00d1c33 --- /dev/null +++ b/projects/plugins/jetpack/extensions/blocks/map/view/mapbox.js @@ -0,0 +1,201 @@ +import { + getLoadContext, + loadBlockEditorAssets, + waitForObject, +} from '../../../shared/block-editor-asset-loader'; +import { debounce } from '../../../shared/debounce'; +import editorAssets from '../block-editor-assets.json'; +import { mapboxMapFormatter } from '../mapbox-map-formatter/'; +import { + createInfoWindowPopup, + fitMapToBounds, + getMapBounds, + googlePoint2Mapbox, + setMarkerHTML, +} from '../mapbox-utils'; +import resizeMapContainer from '../utils/resize-map-container'; + +import '../style.scss'; +import '../component/map-marker/style.scss'; + +class MapBoxBlock { + constructor( root, onError = () => {} ) { + // Root element for the block. + this.root = root; + + const { currentDoc, currentWindow } = getLoadContext( this.root ); + this.document = currentDoc; + this.window = currentWindow; + this.onError = onError; + + // Block attributes. + this.mapStyle = this.root.getAttribute( 'data-map-style' ) || 'default'; + this.mapDetails = this.root.getAttribute( 'data-map-details' ) === 'true'; + this.apiKey = this.root.getAttribute( 'data-api-key' ) || null; + this.scrollToZoom = this.root.getAttribute( 'data-scroll-to-zoom' ) === 'true'; + this.showFullscreenButton = this.root.getAttribute( 'data-show-fullscreen-button' ) === 'true'; + this.points = JSON.parse( this.root.getAttribute( 'data-points' ) || '[]' ); + this.mapCenter = JSON.parse( this.root.getAttribute( 'data-map-center' ) || '{}' ); + this.mapHeight = this.root.getAttribute( 'data-map-height' ) || null; + this.markerColor = this.root.getAttribute( 'data-marker-color' ) || 'red'; + const zoom = this.root.getAttribute( 'data-zoom' ); + this.zoom = zoom && zoom.length ? parseInt( this.root.getAttribute( 'data-zoom' ), 10 ) : 13; + + this.activeMarker = null; + + // Hide list of markers, if present. + const markerList = this.root.querySelector( 'ul' ); + if ( markerList ) { + markerList.style.display = 'none'; + } + + if ( ! this.apiKey || ! this.apiKey.length ) { + throw new Error( 'API key missing' ); + } + } + + initDOM() { + this.root.innerHTML = `
`; + this.container = this.root.querySelector( '.wp-block-jetpack-map__gm-container' ); + } + + loadMapLibraries() { + return new Promise( resolve => { + const callbacks = { + 'mapbox-gl-js': () => { + waitForObject( this.window, 'mapboxgl' ).then( mapboxgl => { + this.mapboxgl = mapboxgl; + mapboxgl.accessToken = this.apiKey; + resolve( mapboxgl ); + } ); + }, + }; + + loadBlockEditorAssets( editorAssets, callbacks, this.root ); + } ); + } + + initMap() { + try { + this.map = new this.mapboxgl.Map( { + container: this.container, + style: mapboxMapFormatter( this.mapStyle, this.mapDetails ), + center: googlePoint2Mapbox( this.mapCenter ), + zoom: this.zoom, + pitchWithRotate: false, + attributionControl: false, + dragRotate: false, + } ); + } catch ( e ) { + this.onError( 'mapbox_error', e.message ); + return; + } + + // Disable scroll zooming if not enabled in block options. + if ( ! this.scrollToZoom ) { + this.map.scrollZoom.disable(); + } + + if ( this.showFullscreenButton ) { + this.map.addControl( new this.mapboxgl.FullscreenControl() ); + } + + this.map.on( 'error', e => { + this.onError( 'mapbox_error', e.error.message ); + } ); + + this.zoomControl = new this.mapboxgl.NavigationControl( { + showCompass: false, + showZoom: true, + } ); + } + + initInfoWindow() { + this.infoWindowContent = this.document.createElement( 'div' ); + this.infoWindow = createInfoWindowPopup( this.mapboxgl ); + this.infoWindow.setDOMContent( this.infoWindowContent ); + } + + setBoundsByMarkers() { + if ( ! this.map ) { + return; + } + this.map.dragPan.enable(); + // If there are no points at all, there is no data to set bounds to. Abort the function. + if ( ! this.points.length ) { + return; + } + // If there is an open info window, resizing will probably move the info window which complicates interaction. + if ( this.activeMarker ) { + return; + } + + const bounds = getMapBounds( this.mapboxgl, this.points ); + + if ( this.points.length > 1 ) { + fitMapToBounds( this.map, bounds ); + } else { + // If there is only one point, center map around it. + this.map.setCenter( bounds.getCenter() ); + this.map.addControl( this.zoomControl ); + } + } + + sizeMap = () => { + resizeMapContainer( this.container, this.mapHeight ); + this.map.resize(); + this.setBoundsByMarkers(); + }; + + initMapSize() { + this.setBoundsByMarkers(); + this.debouncedSizeMap = debounce( this.sizeMap, 250 ); + this.debouncedSizeMap(); + } + + closeInfoWindow = () => { + this.activeMarker = null; + this.infoWindow.remove(); + }; + + initHandlers() { + this.map.getCanvas().addEventListener( 'click', this.closeInfoWindow ); + window.addEventListener( 'resize', this.debouncedSizeMap ); + } + + showInfoWindow( marker, point ) { + const mapboxPoint = [ point.coordinates.longitude, point.coordinates.latitude ]; + this.activeMarker = marker; + this.infoWindowContent.innerHTML = `

`; + this.infoWindowContent.querySelector( 'h3' ).textContent = point.title; + this.infoWindowContent.querySelector( 'p' ).textContent = point.caption; + this.infoWindow.setLngLat( mapboxPoint ).addTo( this.map ); + } + + initMarkers() { + this.points.forEach( point => { + const mapboxPoint = [ point.coordinates.longitude, point.coordinates.latitude ]; + const el = this.document.createElement( 'div' ); + el.className = 'wp-block-jetpack-map-marker'; + const marker = new this.mapboxgl.Marker( el ) + .setLngLat( mapboxPoint ) + .setOffset( [ 0, -19 ] ) + .addTo( this.map ); + + marker.getElement().addEventListener( 'click', () => this.showInfoWindow( marker, point ) ); + setMarkerHTML( el, this.markerColor ); + } ); + } + + async init() { + this.initDOM(); + await this.loadMapLibraries(); + this.initMap(); + this.initInfoWindow(); + this.initMapSize(); + this.initHandlers(); + this.initMarkers(); + } +} + +export default MapBoxBlock; diff --git a/projects/plugins/jetpack/extensions/blocks/map/view/mapkit.js b/projects/plugins/jetpack/extensions/blocks/map/view/mapkit.js new file mode 100644 index 0000000000000..1371962e8ed3c --- /dev/null +++ b/projects/plugins/jetpack/extensions/blocks/map/view/mapkit.js @@ -0,0 +1,114 @@ +import { debounce } from '../../../shared/debounce'; +import { + convertZoomLevelToCameraDistance, + loadMapkitLibrary, + fetchMapkitKey, +} from '../mapkit-utils'; +import resizeMapContainer from '../utils/resize-map-container'; + +class MapkitBlock { + constructor( root ) { + this.root = root; + this.blog_id = this.root.getAttribute( 'data-blog-id' ); + this.center = JSON.parse( this.root.getAttribute( 'data-map-center' || '{}' ) ); + this.points = JSON.parse( this.root.getAttribute( 'data-points' ) || '[]' ); + this.color = this.root.getAttribute( 'data-marker-color' ) || 'red'; + this.zoom = parseFloat( this.root.getAttribute( 'data-zoom' ) ) || 10; + this.scrollToZoom = this.root.getAttribute( 'data-scroll-to-zoom' ) === 'true'; + this.mapStyle = this.root.getAttribute( 'data-map-style' ) || 'default'; + this.mapHeight = this.root.getAttribute( 'data-map-height' ) || null; + } + + async init() { + this.initDOM(); + await this.loadLibrary(); + await this.fetchKey(); + this.initMapSize(); + this.initMap(); + this.addPoints(); + this.initHandlers(); + } + + initDOM() { + this.root.innerHTML = `
`; + this.container = this.root.querySelector( '.wp-block-jetpack-map__mb-container' ); + } + + sizeMap = () => { + resizeMapContainer( this.container, this.mapHeight ); + }; + + initMapSize() { + this.debouncedSizeMap = debounce( this.sizeMap, 250 ); + this.sizeMap(); + } + + initHandlers() { + window.addEventListener( 'resize', this.debouncedSizeMap ); + } + + loadLibrary() { + return new Promise( resolve => { + loadMapkitLibrary( document, window ).then( mapkit => { + this.mapkit = mapkit; + resolve(); + } ); + } ); + } + + fetchKey() { + return fetchMapkitKey( this.mapkit, this.blog_id, window ); + } + + initMap() { + const center = new this.mapkit.Coordinate( this.center.lat, this.center.lng ); + const mapType = ( () => { + switch ( this.mapStyle ) { + case 'satellite': + return this.mapkit.Map.MapTypes.Satellite; + case 'black_and_white': + return this.mapkit.Map.MapTypes.MutedStandard; + case 'hybrid': + return this.mapkit.Map.MapTypes.Hybrid; + default: + return this.mapkit.Map.MapTypes.Standard; + } + } )(); + + this.map = new this.mapkit.Map( this.container, { + center, + mapType, + } ); + + if ( this.points.length < 2 && this.zoom ) { + this.setZoom(); + } + + if ( this.scrollToZoom ) { + this.map._allowWheelToZoom = true; + } + } + + setZoom() { + this.map.cameraDistance = convertZoomLevelToCameraDistance( this.zoom, this.center.lat ); + } + + addPoints() { + const annotations = this.points.map( point => { + const coordinate = new this.mapkit.Coordinate( + point.coordinates.latitude, + point.coordinates.longitude + ); + const annotation = new this.mapkit.MarkerAnnotation( coordinate, { + color: this.color, + } ); + annotation.title = point.title; + annotation.callout = {}; + annotation.calloutEnabled = true; + return annotation; + } ); + this.map.showItems( annotations ); + } +} + +export default MapkitBlock;