| 1 |
const { registerBlockType } = wp.blocks; |
| 2 |
const { __ } = wp.i18n; |
| 3 |
const { useEffect, useState } = wp.element; |
| 4 |
const { |
| 5 |
PanelBody, |
| 6 |
SelectControl, |
| 7 |
TextControl, |
| 8 |
Button, |
| 9 |
RangeControl, |
| 10 |
ToggleControl, |
| 11 |
} = wp.components; |
| 12 |
const { InspectorControls, useBlockProps } = wp.blockEditor || wp.editor; |
| 13 |
import "./style.css"; |
| 14 |
|
| 15 |
// Marker row component for editor UI |
| 16 |
function MarkerRow({ marker, index, onChange, onRemove }) { |
| 17 |
const [lat, setLat] = useState(marker.lat); |
| 18 |
const [lng, setLng] = useState(marker.lng); |
| 19 |
const [title, setTitle] = useState(marker.title || ""); |
| 20 |
useEffect(() => { |
| 21 |
onChange(index, { lat: parseFloat(lat) || 0, lng: parseFloat(lng) || 0, title }); |
| 22 |
// eslint-disable-next-line react-hooks/exhaustive-deps |
| 23 |
}, [lat, lng, title]); |
| 24 |
return ( |
| 25 |
<div className="aibui-map-marker-row"> |
| 26 |
<div className="aibui-map-marker-fields"> |
| 27 |
<TextControl label={__("Lat", "ai-builder")} value={String(lat)} onChange={setLat} /> |
| 28 |
<TextControl label={__("Lng", "ai-builder")} value={String(lng)} onChange={setLng} /> |
| 29 |
<TextControl label={__("Title", "ai-builder")} value={title} onChange={setTitle} /> |
| 30 |
</div> |
| 31 |
<Button isDestructive onClick={() => onRemove(index)}> |
| 32 |
{__("Remove", "ai-builder")} |
| 33 |
</Button> |
| 34 |
</div> |
| 35 |
); |
| 36 |
} |
| 37 |
|
| 38 |
registerBlockType("ai-builder/aibui-map", { |
| 39 |
title: __("Map (AI Builder)", "ai-builder"), |
| 40 |
icon: "location", |
| 41 |
category: "widgets", |
| 42 |
supports: { align: ["wide", "full"], html: false }, |
| 43 |
attributes: { |
| 44 |
provider: { type: "string", default: "osm" }, // 'osm' | 'google' |
| 45 |
apiKey: { type: "string", default: "" }, |
| 46 |
centerLat: { type: "number", default: 48.8566 }, |
| 47 |
centerLng: { type: "number", default: 2.3522 }, |
| 48 |
zoom: { type: "number", default: 12 }, |
| 49 |
height: { type: "string", default: "360px" }, |
| 50 |
showZoomControl: { type: "boolean", default: true }, |
| 51 |
draggable: { type: "boolean", default: true }, |
| 52 |
markers: { |
| 53 |
type: "array", |
| 54 |
default: [], // [{lat, lng, title}] |
| 55 |
}, |
| 56 |
}, |
| 57 |
edit: (props) => { |
| 58 |
const { attributes, setAttributes } = props; |
| 59 |
const { provider, apiKey, centerLat, centerLng, zoom, height, markers, showZoomControl, draggable } = attributes; |
| 60 |
const blockProps = useBlockProps({ className: "aibui-map" }); |
| 61 |
|
| 62 |
const setCenter = (patch) => setAttributes({ ...patch }); |
| 63 |
const addMarker = () => setAttributes({ markers: [...markers, { lat: centerLat, lng: centerLng, title: "" }] }); |
| 64 |
const updateMarker = (idx, patch) => setAttributes({ markers: markers.map((m, i) => (i === idx ? { ...m, ...patch } : m)) }); |
| 65 |
const removeMarker = (idx) => setAttributes({ markers: markers.filter((_, i) => i !== idx) }); |
| 66 |
|
| 67 |
useEffect(() => { |
| 68 |
const root = document.querySelector(`#block-${props.clientId}`) || null; |
| 69 |
if (!root) return; |
| 70 |
const canvas = root.querySelector('.aibui-map-canvas'); |
| 71 |
if (!canvas) return; |
| 72 |
// Clean up any previous map instance bound to this canvas |
| 73 |
if (canvas.__aibuiMap && typeof canvas.__aibuiMap.remove === 'function') { |
| 74 |
try { canvas.__aibuiMap.remove(); } catch (e) { } |
| 75 |
} |
| 76 |
canvas.__aibuiMap = null; |
| 77 |
canvas.innerHTML = ''; |
| 78 |
const initOSM = () => { |
| 79 |
if (window.L && window.L.map) { |
| 80 |
const map = window.L.map(canvas, { zoomControl: !!showZoomControl, dragging: !!draggable }).setView([centerLat, centerLng], zoom); |
| 81 |
window.L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', { maxZoom: 19, attribution: '© OpenStreetMap' }).addTo(map); |
| 82 |
(markers || []).forEach(m => { |
| 83 |
const mk = window.L.marker([parseFloat(m.lat) || 0, parseFloat(m.lng) || 0]).addTo(map); |
| 84 |
if (m.title) mk.bindPopup(m.title); |
| 85 |
}); |
| 86 |
canvas.__aibuiMap = map; |
| 87 |
return; |
| 88 |
} |
| 89 |
const css = document.createElement('link'); css.rel = 'stylesheet'; css.href = 'https://unpkg.com/leaflet@1.9.4/dist/leaflet.css'; document.head.appendChild(css); |
| 90 |
const s = document.createElement('script'); s.src = 'https://unpkg.com/leaflet@1.9.4/dist/leaflet.js'; s.onload = initOSM; document.head.appendChild(s); |
| 91 |
}; |
| 92 |
const initGoogle = () => { |
| 93 |
if (window.google && window.google.maps) { |
| 94 |
const map = new window.google.maps.Map(canvas, { |
| 95 |
center: { lat: centerLat, lng: centerLng }, |
| 96 |
zoom: zoom, |
| 97 |
zoomControl: !!showZoomControl, |
| 98 |
draggable: !!draggable, |
| 99 |
}); |
| 100 |
(markers || []).forEach(m => { |
| 101 |
const mk = new window.google.maps.Marker({ position: { lat: parseFloat(m.lat) || 0, lng: parseFloat(m.lng) || 0 }, map, title: m.title || '' }); |
| 102 |
if (m.title) { |
| 103 |
const iw = new window.google.maps.InfoWindow({ content: m.title }); |
| 104 |
mk.addListener('click', () => iw.open({ anchor: mk, map })); |
| 105 |
} |
| 106 |
}); |
| 107 |
canvas.__aibuiMap = map; |
| 108 |
return; |
| 109 |
} |
| 110 |
const cbName = '__aibui_gmaps_cb_' + Math.random().toString(36).slice(2); |
| 111 |
window[cbName] = () => { initGoogle(); delete window[cbName]; }; |
| 112 |
const s = document.createElement('script'); |
| 113 |
const base = 'https://maps.googleapis.com/maps/api/js'; |
| 114 |
const qs = (apiKey ? ('key=' + encodeURIComponent(apiKey) + '&') : '') + 'callback=' + cbName; |
| 115 |
s.src = base + '?' + qs; s.async = true; s.defer = true; document.head.appendChild(s); |
| 116 |
}; |
| 117 |
if (provider === 'google') initGoogle(); else initOSM(); |
| 118 |
}, [provider, apiKey, centerLat, centerLng, zoom, height, JSON.stringify(markers), showZoomControl, draggable, props.clientId]); |
| 119 |
|
| 120 |
return ( |
| 121 |
<div {...blockProps}> |
| 122 |
<InspectorControls> |
| 123 |
<PanelBody title={__("Map Provider", "ai-builder")} initialOpen> |
| 124 |
<SelectControl |
| 125 |
label={__("Provider", "ai-builder")} |
| 126 |
value={provider} |
| 127 |
options={[ |
| 128 |
{ label: "OpenStreetMap", value: "osm" }, |
| 129 |
{ label: "Google Maps", value: "google" }, |
| 130 |
]} |
| 131 |
onChange={(v) => setAttributes({ provider: v })} |
| 132 |
/> |
| 133 |
{provider === "google" && ( |
| 134 |
<TextControl |
| 135 |
label={__("Google Maps API key", "ai-builder")} |
| 136 |
value={apiKey} |
| 137 |
onChange={(v) => setAttributes({ apiKey: v })} |
| 138 |
help={__("Required to load Google Maps.", "ai-builder")} |
| 139 |
/> |
| 140 |
)} |
| 141 |
</PanelBody> |
| 142 |
<PanelBody title={__("Map Settings", "ai-builder")} initialOpen> |
| 143 |
<TextControl |
| 144 |
label={__("Center latitude", "ai-builder")} |
| 145 |
value={String(centerLat)} |
| 146 |
onChange={(v) => setCenter({ centerLat: parseFloat(v) || 0 })} |
| 147 |
/> |
| 148 |
<TextControl |
| 149 |
label={__("Center longitude", "ai-builder")} |
| 150 |
value={String(centerLng)} |
| 151 |
onChange={(v) => setCenter({ centerLng: parseFloat(v) || 0 })} |
| 152 |
/> |
| 153 |
<RangeControl |
| 154 |
label={__("Zoom", "ai-builder")} |
| 155 |
min={1} |
| 156 |
max={20} |
| 157 |
value={zoom} |
| 158 |
onChange={(v) => setAttributes({ zoom: v })} |
| 159 |
/> |
| 160 |
<TextControl |
| 161 |
label={__("Height", "ai-builder")} |
| 162 |
value={height} |
| 163 |
onChange={(v) => setAttributes({ height: v })} |
| 164 |
help={__("Any CSS size (e.g. 360px, 50vh)", "ai-builder")} |
| 165 |
/> |
| 166 |
<ToggleControl |
| 167 |
label={__("Show zoom control", "ai-builder")} |
| 168 |
checked={showZoomControl} |
| 169 |
onChange={(v) => setAttributes({ showZoomControl: v })} |
| 170 |
/> |
| 171 |
<ToggleControl |
| 172 |
label={__("Draggable", "ai-builder")} |
| 173 |
checked={draggable} |
| 174 |
onChange={(v) => setAttributes({ draggable: v })} |
| 175 |
/> |
| 176 |
</PanelBody> |
| 177 |
<PanelBody title={__("Markers", "ai-builder")} initialOpen> |
| 178 |
{markers.map((m, i) => ( |
| 179 |
<MarkerRow key={i} index={i} marker={m} onChange={updateMarker} onRemove={removeMarker} /> |
| 180 |
))} |
| 181 |
<Button isPrimary onClick={addMarker} style={{ marginTop: 8 }}> |
| 182 |
{__("Add marker", "ai-builder")} |
| 183 |
</Button> |
| 184 |
</PanelBody> |
| 185 |
</InspectorControls> |
| 186 |
|
| 187 |
<div className="aibui-map-preview" style={{ height }}> |
| 188 |
<div className="aibui-map-canvas" style={{ height: '100%', width: '100%' }} /> |
| 189 |
</div> |
| 190 |
</div> |
| 191 |
); |
| 192 |
}, |
| 193 |
save: (props) => { |
| 194 |
const { attributes } = props; |
| 195 |
const { provider, apiKey, centerLat, centerLng, zoom, height, markers, showZoomControl, draggable } = attributes; |
| 196 |
const blockProps = wp.blockEditor ? wp.blockEditor.useBlockProps.save({ className: "aibui-map" }) : {}; |
| 197 |
return ( |
| 198 |
<div |
| 199 |
{...blockProps} |
| 200 |
data-provider={provider} |
| 201 |
data-api-key={provider === "google" ? (apiKey || "") : ""} |
| 202 |
data-center-lat={centerLat} |
| 203 |
data-center-lng={centerLng} |
| 204 |
data-zoom={zoom} |
| 205 |
data-zoom-control={showZoomControl ? "1" : "0"} |
| 206 |
data-draggable={draggable ? "1" : "0"} |
| 207 |
> |
| 208 |
<div className="aibui-map-canvas" style={{ height }} /> |
| 209 |
<script type="application/json" className="aibui-map-markers" dangerouslySetInnerHTML={{ __html: JSON.stringify(markers || []) }} /> |
| 210 |
</div> |
| 211 |
); |
| 212 |
}, |
| 213 |
}); |
| 214 |
|
| 215 |
|
| 216 |
|