@eightshift/frontend-libs
Version:
A collection of useful frontend utility modules. powered by Eightshift
592 lines (522 loc) • 19.8 kB
JavaScript
import React from 'react';
import { __ } from '@wordpress/i18n';
import { PanelBody, TextControl, Button } from '@wordpress/components';
import { MediaPlaceholder } from '@wordpress/block-editor';
import {
Collapsable,
Control,
IconLabel,
IconToggle,
Menu,
MenuItem,
MenuSeparator,
Notification,
NumberPicker,
Repeater,
RepeaterItem,
Section,
checkAttr,
getAttrKey,
} from '@eightshift/frontend-libs/scripts';
import { icons } from '@eightshift/ui-components/icons';
import { truncateMiddle } from '@eightshift/ui-components/utilities';
import manifest from '../manifest.json';
export const MapOptions = ({ attributes, setAttributes }) => {
const mapLayers = checkAttr('mapLayers', attributes, manifest);
const mapCenterLat = checkAttr('mapCenterLat', attributes, manifest);
const mapCenterLon = checkAttr('mapCenterLon', attributes, manifest);
const mapZoom = checkAttr('mapZoom', attributes, manifest);
const mapInteractions = checkAttr('mapInteractions', attributes, manifest);
const mapControls = checkAttr('mapControls', attributes, manifest);
const layerTypes = {
openStreetMap: {
icon: icons.mapLayer,
title: __('OpenStreetMap', '%g_textdomain%'),
hasSeparator: true,
},
vectorJson: {
icon: icons.mapLayerJson, title: __('Vector map', '%g_textdomain%'),
subtitle: __('with JSON styles', '%g_textdomain%'),
hasSeparator: true,
},
mapBoxVector: {
icon: icons.mapLayerVector, title: __('Mapbox map', '%g_textdomain%'),
subtitle: __('Vector tiles', '%g_textdomain%'),
},
mapBoxRaster: {
icon: icons.mapLayerRaster, title: __('Mapbox map', '%g_textdomain%'),
subtitle: __('Raster tiles', '%g_textdomain%'),
hasSeparator: true,
},
mapTilerVector: {
icon: icons.mapLayerVector, title: __('MapTiler tiles', '%g_textdomain%'),
subtitle: __('Vector - XYZ (PBF)', '%g_textdomain%'),
},
mapTilerRasterXyz: {
icon: icons.mapLayerRaster, title: __('MapTiler map/tiles', '%g_textdomain%'),
subtitle: __('Raster - XYZ', '%g_textdomain%'),
},
mapTilerRasterJson: {
icon: icons.mapLayerRaster, title: __('MapTiler map/tiles', '%g_textdomain%'),
subtitle: __('Raster - JSON', '%g_textdomain%'),
hasSeparator: true,
},
geoJson: { icon: icons.fileMetadata, title: __('GeoJSON', '%g_textdomain%') },
};
return (
<PanelBody title={__('Map', '%g_textdomain%')}>
<Section icon={icons.play} label={__('Initial view', '%g_textdomain%')}>
<Control icon={icons.alignHorizontalVerticalAlt} label={__('Center point', '%g_textdomain%')} additionalLabelClasses='es-mb-1!'>
<div className='es-fifty-fifty-h'>
<TextControl
label={__('Lat', '%g_textdomain%')}
value={mapCenterLat}
onChange={(value) => setAttributes({ [getAttrKey('mapCenterLat', attributes, manifest)]: value })}
className='es-m-0! es-m-0-bcf!'
/>
<TextControl
label={__('Lon', '%g_textdomain%')}
value={mapCenterLon}
onChange={(value) => setAttributes({ [getAttrKey('mapCenterLon', attributes, manifest)]: value })}
className='es-m-0! es-m-0-bcf!'
/>
</div>
</Control>
<NumberPicker
icon={icons.search}
label={__('Zoom', '%g_textdomain%')}
value={mapZoom}
onChange={(value) => setAttributes({ [getAttrKey('mapZoom', attributes, manifest)]: value })}
min={1}
max={30}
additionalClasses='es-flex-shrink-0'
inlineLabel
/>
</Section>
<Section icon={icons.options} label={__('Configuration', '%g_textdomain%')} noBottomSpacing>
<Repeater
icon={icons.layers}
label={__('Layers', '%g_textdomain%')}
items={mapLayers}
attributeName={getAttrKey('mapLayers', attributes, manifest)}
setAttributes={setAttributes}
customAddButton={({ disabled }) => (
<Menu
buttonClass='es-button-square-28 es-button-icon-24 es-nested-color-cool-gray-650 es-rounded-1'
icon={icons.plusCircle}
disabled={disabled}
>
{Object.entries(layerTypes).map(([value, { icon, title, subtitle, hasSeparator }], index) => {
return (
<>
<MenuItem
key={index}
icon={icon}
label={<IconLabel label={title} subtitle={subtitle} standalone />}
additionalClass='es-content-start!'
onClick={() => {
const modifiedData = [...mapLayers];
modifiedData.push({
id: modifiedData.length + 1,
type: value,
hidden: false,
});
setAttributes({ [getAttrKey('mapLayers', attributes, manifest)]: modifiedData });
}}
/>
{hasSeparator && <MenuSeparator />}
</>
);
})}
</Menu>
)}
>
{mapLayers.map((layer, index) => {
const needsApiKey = ['mapBoxVector', 'mapBoxRaster', 'mapTilerVector', 'vectorJson', 'mapTilerRasterXyz', 'mapTilerRasterJson'].includes(layer?.type);
const hasMapStyleOptions = ['mapBoxVector', 'mapBoxRaster', 'mapTilerVector', 'vectorJson', 'mapTilerRasterXyz', 'mapTilerRasterJson'].includes(layer?.type);
return (
<RepeaterItem
key={layer.id}
icon={layer?.type ? layerTypes?.[layer?.type]?.icon ?? icons.mapLayer : icons.layerOff}
title={layerTypes?.[layer?.type]?.title ?? __('New layer', '%g_textdomain%')}
subtitle={
layer?.type === 'geoJson'
? truncateMiddle(layer?.geoJsonUrl?.slice(layer?.geoJsonUrl?.lastIndexOf('/') + 1) ?? '', 20)
: layerTypes?.[layer?.type]?.subtitle
}
preIcon={
layer?.type?.length < 1 ? icons.dummySpacer : (
<Button
icon={mapLayers[index]?.hidden ? icons.dummySpacer : icons.visible}
onClick={() => {
const modifiedData = [...mapLayers];
modifiedData[index].hidden = !modifiedData[index].hidden;
setAttributes({ [getAttrKey('mapLayers', attributes, manifest)]: modifiedData });
}}
className='es-mr-1 es-button-square-20 es-button-icon-16 es-rounded-1 es-border-cool-gray-300 es-hover-border-cool-gray-300 es-focus-border-transparent es-transition-colors'
label={mapLayers[index]?.hidden ? __('Show', '%g_textdomain%') : __('Hide', '%g_textdomain%')}
showTooltip
/>
)
}
>
{!layer?.type &&
<Notification
type='error'
text={__('Error', '%g_textdomain%')}
subtitle={__('Layer type is missing, please remove the layer and add a new one.', '%g_textdomain%')}
/>
}
{needsApiKey &&
<TextControl
label={<IconLabel icon={icons.key} label={__('API key', '%g_textdomain%')} />}
value={mapLayers[index]?.apiKey}
onChange={(value) => {
const modifiedData = [...mapLayers];
modifiedData[index].apiKey = value;
setAttributes({ [getAttrKey('mapLayers', attributes, manifest)]: modifiedData });
}}
/>
}
{hasMapStyleOptions &&
<TextControl
label={<IconLabel icon={icons.color} label={__('Map style', '%g_textdomain%')} />}
value={mapLayers[index]?.styleUrl}
onChange={(value) => {
const modifiedData = [...mapLayers];
modifiedData[index].styleUrl = value;
setAttributes({ [getAttrKey('mapLayers', attributes, manifest)]: modifiedData });
}}
help={
<>
{!layer?.type?.startsWith('mapBox') &&
__('Copy the full style URL from MapTiler. Keep the API key inside the URL.', '%g_textdomain%')
}
{layer?.type === 'mapBoxVector' &&
__('Copy the full style URL from Mapbox.', '%g_textdomain%')
}
{layer?.type === 'mapBoxRaster' &&
__('Copy the full style URL from Mapbox or a Mapbox-compatible source. Keep the access token inside the URL.', '%g_textdomain%')
}
<br />
<br />
{['mapBoxRaster', 'mapTilerVector', 'mapTilerRasterXyz'].includes(layer?.type) &&
<>
<code className='es-bg-transparent es-p-0 es-text-3'>{'{z}/{x}/{y}'}</code>
{
__("should be left as they are in the URL; they're needed for the map to work properly.", '%g_textdomain%')
}
<br />
<br />
</>
}
{__('Example', '%g_textdomain%')}:
<br />
<span className='es-word-break-all'>
{['mapTilerRasterJson', 'vectorJson'].includes(layer?.type) &&
'https://api.maptiler.com/maps/{styleName}/tiles.json?key={apiKey}'
}
{layer?.type === 'mapTilerVector' && 'https://api.maptiler.com/tiles/v3/{z}/{x}/{y}.pbf?key={apiKey}'}
{layer?.type === 'mapBoxVector' &&
<>
{'mapbox://styles/{styleName},'}
<br />
{'mapbox://styles/{userId}/{styleId}'}
</>
}
{layer?.type === 'mapBoxRaster' &&
'https://api.mapbox.com/v4/{tilesetId}/{z}/{x}/{y}[@2x].{imageFormat}?acess_token={apiKey}'
}
{layer?.type === 'mapTilerRasterXyz' &&
'https://api.maptiler.com/maps/{styleName}/{z}/{x}/{y}.png?key={apiKey}'
}
</span>
</>
}
className='es-m-0! es-mb-0-bcf!'
/>
}
{layer?.type === 'geoJson' && (layer?.geoJsonUrl?.length < 1 || !layer?.geoJsonUrl) &&
<MediaPlaceholder
icon={icons.file}
accept={['.json', '.geojson']}
labels={{ title: __('GeoJSON file', '%g_textdomain%') }}
onSelect={
(file) => {
const modifiedData = [...mapLayers];
modifiedData[index].geoJsonUrl = file.url;
setAttributes({ [getAttrKey('mapLayers', attributes, manifest)]: modifiedData });
}
}
/>
}
{layer?.type === 'geoJson' && layer?.geoJsonUrl?.length > 0 &&
<div className='es-h-between'>
<IconLabel
icon={icons.file}
label={
<code className='es-word-break-all es-bg-transparent es-p-0 es-text-2.75!'>
{layer.geoJsonUrl.slice(layer.geoJsonUrl.lastIndexOf('/') + 1)}
</code>
}
additionalClasses='es-flex-shrink-1'
standalone
/>
<Button
onClick={() => {
const modifiedData = [...mapLayers];
delete modifiedData[index].geoJsonUrl;
setAttributes({ [getAttrKey('mapLayers', attributes, manifest)]: modifiedData });
}}
className='es-button-icon-24 es-border-cool-gray-100 es-hover-border-cool-gray-200 es-hover-color-admin-accent es-rounded-1.5 es-nested-color-cool-gray-650'
>
{__('Replace', '%g_textdomain%')}
</Button>
</div>
}
</RepeaterItem>
);
})}
</Repeater>
<Collapsable label={__('Controls', '%g_textdomain%')} icon={icons.buttonOutline}>
<IconToggle
icon={icons.tag}
label={__('Attribution', '%g_textdomain%')}
checked={mapControls.attribution}
onChange={(value) => {
const newValue = { ...mapControls };
newValue.attribution = value;
setAttributes({ [getAttrKey('mapControls', attributes, manifest)]: newValue });
}}
/>
<IconToggle
icon={icons.expandXl}
label={__('Full screen', '%g_textdomain%')}
checked={mapControls.fullScreen}
onChange={(value) => {
const newValue = { ...mapControls };
newValue.fullScreen = value;
setAttributes({ [getAttrKey('mapControls', attributes, manifest)]: newValue });
}}
/>
<IconToggle
icon={icons.mouseCursor}
label={__('Pointer position', '%g_textdomain%')}
checked={mapControls.mousePosition}
onChange={(value) => {
const newValue = { ...mapControls };
newValue.mousePosition = value;
setAttributes({ [getAttrKey('mapControls', attributes, manifest)]: newValue });
}}
/>
<IconToggle
icon={icons.mapPin}
label={__('Minimap', '%g_textdomain%')}
checked={mapControls.overviewMap}
onChange={(value) => {
const newValue = { ...mapControls };
newValue.overviewMap = value;
setAttributes({ [getAttrKey('mapControls', attributes, manifest)]: newValue });
}}
/>
<IconToggle
icon={icons.rotateLeft}
label={__('Reset rotation', '%g_textdomain%')}
checked={mapControls.rotate}
onChange={(value) => {
const newValue = { ...mapControls };
newValue.rotate = value;
setAttributes({ [getAttrKey('mapControls', attributes, manifest)]: newValue });
}}
/>
<IconToggle
icon={icons.ruler}
label={__('Map scale', '%g_textdomain%')}
checked={mapControls.scaleLine}
onChange={(value) => {
const newValue = { ...mapControls };
newValue.scaleLine = value;
setAttributes({ [getAttrKey('mapControls', attributes, manifest)]: newValue });
}}
/>
<IconLabel icon={icons.search} label={__('Zoom', '%g_textdomain%')} additionalClasses='es-mb-1.5 es-font-weight-500' standalone />
<IconToggle
icon={icons.dummySpacer}
label={__('Slider', '%g_textdomain%')}
checked={mapControls.zoomSlider}
onChange={(value) => {
const newValue = { ...mapControls };
newValue.zoomSlider = value;
setAttributes({ [getAttrKey('mapControls', attributes, manifest)]: newValue });
}}
reducedBottomSpacing
/>
<IconToggle
icon={icons.dummySpacer}
label={__('Buttons', '%g_textdomain%')}
checked={mapControls.zoom}
onChange={(value) => {
const newValue = { ...mapControls };
newValue.zoom = value;
setAttributes({ [getAttrKey('mapControls', attributes, manifest)]: newValue });
}}
reducedBottomSpacing
/>
<IconToggle
icon={icons.dummySpacer}
label={__('To extent', '%g_textdomain%')}
checked={mapControls.zoomToExtent}
onChange={(value) => {
const newValue = { ...mapControls };
newValue.zoomToExtent = value;
setAttributes({ [getAttrKey('mapControls', attributes, manifest)]: newValue });
}}
/>
<Notification
type='info'
text={__('Note', '%g_textdomain%')}
subtitle={__('Some of the options might not be reflected in the editor', '%g_textdomain%')}
noBottomSpacing
/>
</Collapsable>
<Collapsable label={__('Interactions', '%g_textdomain%')} icon={icons.pointerHand} noBottomSpacing>
<IconToggle
icon={icons.focus}
label={__('Only when map is focused', '%g_textdomain%')}
checked={mapInteractions.onFocusOnly}
onChange={(value) => {
const newValue = { ...mapInteractions };
newValue.onFocusOnly = value;
setAttributes({ [getAttrKey('mapInteractions', attributes, manifest)]: newValue });
}}
/>
<IconLabel
icon={icons.rotateRight}
label={__('Rotate', '%g_textdomain%')}
additionalClasses='es-mb-1.5 es-font-weight-500'
standalone
/>
<IconToggle
icon={icons.dummySpacer}
label={__('Alt+Shift and drag to rotate', '%g_textdomain%')}
checked={mapInteractions.altShiftDragRotate}
onChange={(value) => {
const newValue = { ...mapInteractions };
newValue.altShiftDragRotate = value;
setAttributes({ [getAttrKey('mapInteractions', attributes, manifest)]: newValue });
}}
reducedBottomSpacing
/>
<IconToggle
icon={icons.dummySpacer}
label={__('Pinch to rotate', '%g_textdomain%')}
checked={mapInteractions.pinchRotate}
onChange={(value) => {
const newValue = { ...mapInteractions };
newValue.pinchRotate = value;
setAttributes({ [getAttrKey('mapInteractions', attributes, manifest)]: newValue });
}}
/>
<IconLabel icon={icons.search} label={__('Zoom', '%g_textdomain%')} additionalClasses='es-mb-1.5 es-font-weight-500' standalone />
<IconToggle
icon={icons.dummySpacer}
label={__('Double-click to zoom', '%g_textdomain%')}
checked={mapInteractions.doubleClickZoom}
onChange={(value) => {
const newValue = { ...mapInteractions };
newValue.doubleClickZoom = value;
setAttributes({ [getAttrKey('mapInteractions', attributes, manifest)]: newValue });
}}
reducedBottomSpacing
/>
<IconToggle
icon={icons.dummySpacer}
label={__('Zoom with mousewheel', '%g_textdomain%')}
checked={mapInteractions.mouseWheelZoom}
onChange={(value) => {
const newValue = { ...mapInteractions };
newValue.mouseWheelZoom = value;
setAttributes({ [getAttrKey('mapInteractions', attributes, manifest)]: newValue });
}}
reducedBottomSpacing
/>
<IconToggle
icon={icons.dummySpacer}
label={__('Shift and drag to zoom', '%g_textdomain%')}
checked={mapInteractions.shiftDragZoom}
onChange={(value) => {
const newValue = { ...mapInteractions };
newValue.shiftDragZoom = value;
setAttributes({ [getAttrKey('mapInteractions', attributes, manifest)]: newValue });
}}
reducedBottomSpacing
/>
<IconToggle
icon={icons.dummySpacer}
label={__('Pinch to zoom', '%g_textdomain%')}
checked={mapInteractions.pinchZoom}
onChange={(value) => {
const newValue = { ...mapInteractions };
newValue.pinchZoom = value;
setAttributes({ [getAttrKey('mapInteractions', attributes, manifest)]: newValue });
}}
reducedBottomSpacing
/>
<NumberPicker
icon={icons.dummySpacer}
label={__('Animation duration (ms)', '%g_textdomain%')}
value={mapInteractions.zoomDuration}
onChange={(value) => {
const newValue = { ...mapInteractions };
newValue.zoomDuration = value;
setAttributes({ [getAttrKey('mapInteractions', attributes, manifest)]: newValue });
}}
min={0}
max={10000}
reducedBottomSpacing
inlineLabel
/>
<NumberPicker
icon={icons.dummySpacer}
label={__('Zoom-in step', '%g_textdomain%')}
value={mapInteractions.zoomDelta}
onChange={(value) => {
const newValue = { ...mapInteractions };
newValue.zoomDelta = value;
setAttributes({ [getAttrKey('mapInteractions', attributes, manifest)]: newValue });
}}
inlineLabel
min={1}
max={10}
/>
<IconToggle
icon={icons.keyboard}
label={__('Keyboard interactions', '%g_textdomain%')}
checked={mapInteractions.keyboard}
onChange={(value) => {
const newValue = { ...mapInteractions };
newValue.keyboard = value;
setAttributes({ [getAttrKey('mapInteractions', attributes, manifest)]: newValue });
}}
/>
<IconToggle
icon={icons.cursorMove}
label={__('Drag to move map', '%g_textdomain%')}
checked={mapInteractions.dragPan}
onChange={(value) => {
const newValue = { ...mapInteractions };
newValue.dragPan = value;
setAttributes({ [getAttrKey('mapInteractions', attributes, manifest)]: newValue });
}}
/>
<Notification
type='info'
text={__('Note', '%g_textdomain%')}
subtitle={__('Some of the options might not be reflected in the editor', '%g_textdomain%')}
noBottomSpacing
/>
</Collapsable>
</Section>
</PanelBody>
);
};