feat: refactor maplibre for reactive signals and Via-native API
- Export Signal type (signal → Signal) so subpackages can reference it - Expose viewport signals as public fields (CenterLng, CenterLat, Zoom, Bearing, Pitch) for .Text() display and .Bind() usage - Add signal-backed marker positions (LngSignal/LatSignal) with data-effect reactivity for server push and dragend writeback - Add event system (MapEvent, OnClick, OnLayerClick, OnMouseMove, OnContextMenu) using hidden inputs + action triggers - Add Source interface replacing type-switch, with RawSource escape hatch - Add CameraOptions for FlyTo/EaseTo/JumpTo/FitBounds/Stop - Add Control interface with NavigationControl, ScaleControl, GeolocateControl, FullscreenControl - Expand Options with interaction toggles, MaxBounds, and Extra map - Add effectspike example to validate data-effect with server-pushed signals - Update maplibre example to showcase all new features
This commit is contained in:
220
maplibre/js.go
220
maplibre/js.go
@@ -36,8 +36,9 @@ func initScript(m *Map) string {
|
||||
jsonStr(m.id),
|
||||
))
|
||||
|
||||
// Build constructor options object
|
||||
b.WriteString(fmt.Sprintf(
|
||||
`var map=new maplibregl.Map({container:%s,style:%s,center:[%s,%s],zoom:%s`,
|
||||
`var opts={container:%s,style:%s,center:[%s,%s],zoom:%s`,
|
||||
jsonStr("_vmap_"+m.id),
|
||||
jsonStr(m.opts.Style),
|
||||
formatFloat(m.opts.Center.Lng),
|
||||
@@ -56,14 +57,49 @@ func initScript(m *Map) string {
|
||||
if m.opts.MaxZoom != 0 {
|
||||
b.WriteString(fmt.Sprintf(`,maxZoom:%s`, formatFloat(m.opts.MaxZoom)))
|
||||
}
|
||||
b.WriteString(`});`)
|
||||
|
||||
// Interaction toggles
|
||||
writeBoolOpt := func(name string, val *bool) {
|
||||
if val != nil {
|
||||
if *val {
|
||||
b.WriteString(fmt.Sprintf(`,%s:true`, name))
|
||||
} else {
|
||||
b.WriteString(fmt.Sprintf(`,%s:false`, name))
|
||||
}
|
||||
}
|
||||
}
|
||||
writeBoolOpt("scrollZoom", m.opts.ScrollZoom)
|
||||
writeBoolOpt("boxZoom", m.opts.BoxZoom)
|
||||
writeBoolOpt("dragRotate", m.opts.DragRotate)
|
||||
writeBoolOpt("dragPan", m.opts.DragPan)
|
||||
writeBoolOpt("keyboard", m.opts.Keyboard)
|
||||
writeBoolOpt("doubleClickZoom", m.opts.DoubleClickZoom)
|
||||
writeBoolOpt("touchZoomRotate", m.opts.TouchZoomRotate)
|
||||
writeBoolOpt("touchPitch", m.opts.TouchPitch)
|
||||
writeBoolOpt("renderWorldCopies", m.opts.RenderWorldCopies)
|
||||
|
||||
if m.opts.MaxBounds != nil {
|
||||
b.WriteString(fmt.Sprintf(`,maxBounds:[[%s,%s],[%s,%s]]`,
|
||||
formatFloat(m.opts.MaxBounds.SW.Lng), formatFloat(m.opts.MaxBounds.SW.Lat),
|
||||
formatFloat(m.opts.MaxBounds.NE.Lng), formatFloat(m.opts.MaxBounds.NE.Lat)))
|
||||
}
|
||||
|
||||
b.WriteString(`};`)
|
||||
|
||||
// Merge Extra options
|
||||
if len(m.opts.Extra) > 0 {
|
||||
extra, _ := json.Marshal(m.opts.Extra)
|
||||
b.WriteString(fmt.Sprintf(`Object.assign(opts,%s);`, string(extra)))
|
||||
}
|
||||
|
||||
b.WriteString(`var map=new maplibregl.Map(opts);`)
|
||||
b.WriteString(`if(!window.__via_maps)window.__via_maps={};`)
|
||||
b.WriteString(fmt.Sprintf(`window.__via_maps[%s]=map;`, jsonStr(m.id)))
|
||||
b.WriteString(`map._via_markers={};map._via_popups={};`)
|
||||
b.WriteString(`map._via_markers={};map._via_popups={};map._via_controls={};`)
|
||||
|
||||
// Pre-render sources, layers, markers, popups run on 'load'
|
||||
if len(m.sources) > 0 || len(m.layers) > 0 || len(m.markers) > 0 || len(m.popups) > 0 {
|
||||
// Pre-render sources, layers, markers, popups, controls run on 'load'
|
||||
hasLoad := len(m.sources) > 0 || len(m.layers) > 0 || len(m.markers) > 0 || len(m.popups) > 0 || len(m.controls) > 0
|
||||
if hasLoad {
|
||||
b.WriteString(`map.on('load',function(){`)
|
||||
for _, src := range m.sources {
|
||||
b.WriteString(fmt.Sprintf(`map.addSource(%s,%s);`, jsonStr(src.id), src.js))
|
||||
@@ -76,14 +112,22 @@ func initScript(m *Map) string {
|
||||
}
|
||||
}
|
||||
for _, me := range m.markers {
|
||||
b.WriteString(markerBodyJS(me.id, me.marker))
|
||||
b.WriteString(markerBodyJS(m.id, me.id, me.marker))
|
||||
}
|
||||
for _, pe := range m.popups {
|
||||
b.WriteString(popupBodyJS(pe.id, pe.popup))
|
||||
}
|
||||
for _, ce := range m.controls {
|
||||
b.WriteString(controlBodyJS(ce.id, ce.ctrl))
|
||||
}
|
||||
b.WriteString(`});`)
|
||||
}
|
||||
|
||||
// Event listeners
|
||||
for _, ev := range m.events {
|
||||
b.WriteString(eventListenerJS(m.id, ev))
|
||||
}
|
||||
|
||||
// Sync viewport signals on moveend via hidden inputs
|
||||
b.WriteString(fmt.Sprintf(`map.on('moveend',function(){`+
|
||||
`var c=map.getCenter();`+
|
||||
@@ -100,11 +144,11 @@ func initScript(m *Map) string {
|
||||
`});`+
|
||||
`});`,
|
||||
jsonStr("_vwrap_"+m.id),
|
||||
jsonStr(m.centerLng.ID()),
|
||||
jsonStr(m.centerLat.ID()),
|
||||
jsonStr(m.zoom.ID()),
|
||||
jsonStr(m.bearing.ID()),
|
||||
jsonStr(m.pitch.ID()),
|
||||
jsonStr(m.CenterLng.ID()),
|
||||
jsonStr(m.CenterLat.ID()),
|
||||
jsonStr(m.Zoom.ID()),
|
||||
jsonStr(m.Bearing.ID()),
|
||||
jsonStr(m.Pitch.ID()),
|
||||
))
|
||||
|
||||
// ResizeObserver for auto-resize
|
||||
@@ -132,8 +176,7 @@ func initScript(m *Map) string {
|
||||
}
|
||||
|
||||
// markerBodyJS generates JS to add a marker, assuming `map` is in scope.
|
||||
// Used inside the init script's load callback.
|
||||
func markerBodyJS(markerID string, mk Marker) string {
|
||||
func markerBodyJS(mapID, markerID string, mk Marker) string {
|
||||
var b strings.Builder
|
||||
opts := "{"
|
||||
if mk.Color != "" {
|
||||
@@ -143,16 +186,61 @@ func markerBodyJS(markerID string, mk Marker) string {
|
||||
opts += `draggable:true,`
|
||||
}
|
||||
opts += "}"
|
||||
b.WriteString(fmt.Sprintf(`var mk=new maplibregl.Marker(%s).setLngLat([%s,%s]);`,
|
||||
opts, formatFloat(mk.LngLat.Lng), formatFloat(mk.LngLat.Lat)))
|
||||
|
||||
// Determine initial position
|
||||
if mk.LngSignal != nil && mk.LatSignal != nil {
|
||||
b.WriteString(fmt.Sprintf(`var mk=new maplibregl.Marker(%s).setLngLat([%s,%s]);`,
|
||||
opts, mk.LngSignal.String(), mk.LatSignal.String()))
|
||||
} else {
|
||||
b.WriteString(fmt.Sprintf(`var mk=new maplibregl.Marker(%s).setLngLat([%s,%s]);`,
|
||||
opts, formatFloat(mk.LngLat.Lng), formatFloat(mk.LngLat.Lat)))
|
||||
}
|
||||
|
||||
if mk.Popup != nil {
|
||||
b.WriteString(popupConstructorJS(*mk.Popup, "pk"))
|
||||
b.WriteString(`mk.setPopup(pk);`)
|
||||
}
|
||||
b.WriteString(fmt.Sprintf(`mk.addTo(map);map._via_markers[%s]=mk;`, jsonStr(markerID)))
|
||||
|
||||
// Dragend → signal writeback
|
||||
if mk.Draggable && mk.LngSignal != nil && mk.LatSignal != nil {
|
||||
b.WriteString(dragendHandlerJS(mapID, markerID, mk))
|
||||
}
|
||||
|
||||
return b.String()
|
||||
}
|
||||
|
||||
// dragendHandlerJS generates JS that writes marker position back to signal hidden inputs on dragend.
|
||||
func dragendHandlerJS(mapID, markerID string, mk Marker) string {
|
||||
return fmt.Sprintf(
|
||||
`mk.on('dragend',function(){`+
|
||||
`var pos=mk.getLngLat();`+
|
||||
`var el=document.getElementById(%[1]s);if(!el)return;`+
|
||||
`var inputs=el.querySelectorAll('input[data-bind]');`+
|
||||
`inputs.forEach(function(inp){`+
|
||||
`var sig=inp.getAttribute('data-bind');`+
|
||||
`if(sig===%[2]s){inp.value=pos.lng;inp.dispatchEvent(new Event('input',{bubbles:true}))}`+
|
||||
`if(sig===%[3]s){inp.value=pos.lat;inp.dispatchEvent(new Event('input',{bubbles:true}))}`+
|
||||
`});`+
|
||||
`});`,
|
||||
jsonStr("_vwrap_"+mapID),
|
||||
jsonStr(mk.LngSignal.ID()),
|
||||
jsonStr(mk.LatSignal.ID()),
|
||||
)
|
||||
}
|
||||
|
||||
// markerEffectExpr generates a data-effect expression that moves a signal-backed marker
|
||||
// when its signals change.
|
||||
func markerEffectExpr(mapID, markerID string, mk Marker) string {
|
||||
return fmt.Sprintf(
|
||||
`var m=window.__via_maps&&window.__via_maps[%s];`+
|
||||
`if(m&&m._via_markers[%s]){`+
|
||||
`m._via_markers[%s].setLngLat([$%s,$%s])}`,
|
||||
jsonStr(mapID), jsonStr(markerID), jsonStr(markerID),
|
||||
mk.LngSignal.ID(), mk.LatSignal.ID(),
|
||||
)
|
||||
}
|
||||
|
||||
// addMarkerJS generates a self-contained IIFE to add a marker post-render.
|
||||
func addMarkerJS(mapID, markerID string, mk Marker) string {
|
||||
var b strings.Builder
|
||||
@@ -163,7 +251,7 @@ func addMarkerJS(mapID, markerID string, mk Marker) string {
|
||||
b.WriteString(fmt.Sprintf(
|
||||
`if(map._via_markers[%[1]s]){map._via_markers[%[1]s].remove();delete map._via_markers[%[1]s];}`,
|
||||
jsonStr(markerID)))
|
||||
b.WriteString(markerBodyJS(markerID, mk))
|
||||
b.WriteString(markerBodyJS(mapID, markerID, mk))
|
||||
b.WriteString(`})()`)
|
||||
return b.String()
|
||||
}
|
||||
@@ -221,6 +309,106 @@ func popupConstructorJS(p Popup, varName string) string {
|
||||
varName, opts, jsonStr(p.Content))
|
||||
}
|
||||
|
||||
// --- Control JS ---
|
||||
|
||||
// controlBodyJS generates JS to add a control, assuming `map` is in scope.
|
||||
func controlBodyJS(controlID string, ctrl Control) string {
|
||||
return fmt.Sprintf(
|
||||
`var ctrl=%s;map.addControl(ctrl,%s);map._via_controls[%s]=ctrl;`,
|
||||
ctrl.controlJS(), jsonStr(ctrl.controlPosition()), jsonStr(controlID))
|
||||
}
|
||||
|
||||
// addControlJS generates a self-contained IIFE to add a control post-render.
|
||||
func addControlJS(mapID, controlID string, ctrl Control) string {
|
||||
return fmt.Sprintf(
|
||||
`(function(){var map=window.__via_maps&&window.__via_maps[%[1]s];if(!map)return;`+
|
||||
`if(map._via_controls[%[2]s]){map.removeControl(map._via_controls[%[2]s]);delete map._via_controls[%[2]s];}`+
|
||||
`var ctrl=%[3]s;map.addControl(ctrl,%[4]s);map._via_controls[%[2]s]=ctrl;`+
|
||||
`})()`,
|
||||
jsonStr(mapID), jsonStr(controlID), ctrl.controlJS(), jsonStr(ctrl.controlPosition()))
|
||||
}
|
||||
|
||||
// removeControlJS generates JS to remove a control. Expects `m` in scope.
|
||||
func removeControlJS(controlID string) string {
|
||||
return fmt.Sprintf(
|
||||
`if(m._via_controls[%[1]s]){m.removeControl(m._via_controls[%[1]s]);delete m._via_controls[%[1]s];}`,
|
||||
jsonStr(controlID))
|
||||
}
|
||||
|
||||
// --- Event JS ---
|
||||
|
||||
// eventListenerJS generates JS to register a map event listener that writes
|
||||
// event data to a hidden signal input.
|
||||
func eventListenerJS(mapID string, ev eventEntry) string {
|
||||
var handler string
|
||||
if ev.layerID != "" {
|
||||
handler = fmt.Sprintf(
|
||||
`map.on(%[1]s,%[2]s,function(e){`+
|
||||
`var d={lngLat:{Lng:e.lngLat.lng,Lat:e.lngLat.lat},point:[e.point.x,e.point.y],layerID:%[2]s};`+
|
||||
`if(e.features)d.features=e.features.map(function(f){return JSON.parse(JSON.stringify(f))});`+
|
||||
`var el=document.getElementById(%[3]s);if(!el)return;`+
|
||||
`var inp=el.querySelector('input[data-bind=%[4]s]');`+
|
||||
`if(inp){inp.value=JSON.stringify(d);inp.dispatchEvent(new Event('input',{bubbles:true}))}`+
|
||||
`});`,
|
||||
jsonStr(ev.event), jsonStr(ev.layerID),
|
||||
jsonStr("_vwrap_"+mapID), jsonStr(ev.signal.ID()),
|
||||
)
|
||||
} else {
|
||||
handler = fmt.Sprintf(
|
||||
`map.on(%[1]s,function(e){`+
|
||||
`var d={lngLat:{Lng:e.lngLat.lng,Lat:e.lngLat.lat},point:[e.point.x,e.point.y]};`+
|
||||
`var el=document.getElementById(%[2]s);if(!el)return;`+
|
||||
`var inp=el.querySelector('input[data-bind=%[3]s]');`+
|
||||
`if(inp){inp.value=JSON.stringify(d);inp.dispatchEvent(new Event('input',{bubbles:true}))}`+
|
||||
`});`,
|
||||
jsonStr(ev.event),
|
||||
jsonStr("_vwrap_"+mapID), jsonStr(ev.signal.ID()),
|
||||
)
|
||||
}
|
||||
return handler
|
||||
}
|
||||
|
||||
// --- Camera options JS ---
|
||||
|
||||
// cameraOptionsJS converts CameraOptions to a JS object literal string.
|
||||
func cameraOptionsJS(opts CameraOptions) string {
|
||||
obj := map[string]any{}
|
||||
if opts.Center != nil {
|
||||
obj["center"] = []float64{opts.Center.Lng, opts.Center.Lat}
|
||||
}
|
||||
if opts.Zoom != nil {
|
||||
obj["zoom"] = *opts.Zoom
|
||||
}
|
||||
if opts.Bearing != nil {
|
||||
obj["bearing"] = *opts.Bearing
|
||||
}
|
||||
if opts.Pitch != nil {
|
||||
obj["pitch"] = *opts.Pitch
|
||||
}
|
||||
if opts.Duration != nil {
|
||||
obj["duration"] = *opts.Duration
|
||||
}
|
||||
if opts.Speed != nil {
|
||||
obj["speed"] = *opts.Speed
|
||||
}
|
||||
if opts.Curve != nil {
|
||||
obj["curve"] = *opts.Curve
|
||||
}
|
||||
if opts.Padding != nil {
|
||||
obj["padding"] = map[string]int{
|
||||
"top": opts.Padding.Top,
|
||||
"bottom": opts.Padding.Bottom,
|
||||
"left": opts.Padding.Left,
|
||||
"right": opts.Padding.Right,
|
||||
}
|
||||
}
|
||||
if opts.Animate != nil {
|
||||
obj["animate"] = *opts.Animate
|
||||
}
|
||||
b, _ := json.Marshal(obj)
|
||||
return string(b)
|
||||
}
|
||||
|
||||
func formatFloat(f float64) string {
|
||||
return fmt.Sprintf("%g", f)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user