Skip to content

Commit 9767a8c

Browse files
authored
add map picker feature to forms (#60)
1 parent b6be1df commit 9767a8c

10 files changed

Lines changed: 354 additions & 37 deletions

File tree

CHANGELOG.md

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,13 @@
1+
# 0.19.0 <small>2026-04-26</small>
2+
3+
## 🚀 Features
4+
- Add map picker for forms (`input-type="map"`). Configure latitude and longitude parameters with
5+
`input-type="map"` to render an interactive MapBox map inline in the form. Users can click or drag
6+
a pin to set coordinates, which sync to the editable numeric inputs. Typing directly in the inputs
7+
repositions the pin. Works in standard and modal form modes. See `docs/map-picker.md`.
8+
9+
<!-- CHANGELOG_BOUNDARY -->
10+
111
# 0.18.2 <small>2026-04-12</small>
212

313
## 💅 Improvements

docs/map-picker.md

Lines changed: 102 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,102 @@
1+
# Map Picker
2+
3+
The map picker is a form input type that renders a small interactive MapBox map inline in a form,
4+
allowing users to set a geographic location by clicking or dragging a pin. The selected coordinates
5+
are written to two numeric parameters — one for latitude and one for longitude.
6+
7+
## Configuration
8+
9+
Add `input-type="map"` to your latitude and longitude parameters. Both parameters must have
10+
`type="double"`.
11+
12+
```xml
13+
<parameters>
14+
<add name="Latitude"
15+
type="double"
16+
input-type="map"
17+
prompt="true"
18+
label="Latitude" />
19+
<add name="Longitude"
20+
type="double"
21+
input-type="map"
22+
prompt="true"
23+
label="Longitude" />
24+
</parameters>
25+
```
26+
27+
The system determines which parameter is latitude and which is longitude via `input-capture`.
28+
If your parameter names are `Latitude` and `Longitude` (case-insensitive), `input-capture` is
29+
set automatically. For any other names, set it explicitly:
30+
31+
```xml
32+
<add name="Lat"
33+
type="double"
34+
input-type="map"
35+
input-capture="latitude"
36+
prompt="true"
37+
label="Latitude" />
38+
<add name="Lng"
39+
type="double"
40+
input-type="map"
41+
input-capture="longitude"
42+
prompt="true"
43+
label="Longitude" />
44+
```
45+
46+
`input-capture` accepts `latitude` or `longitude`.
47+
48+
## How It Renders
49+
50+
The latitude parameter renders:
51+
1. A 300px MapBox map with a draggable red pin and navigation/geolocate controls
52+
2. A numeric text input for latitude below the map
53+
54+
The longitude parameter renders:
55+
1. A numeric text input for longitude (no second map)
56+
57+
The map appears above the latitude input. If you want the map to appear in a specific position
58+
in the form relative to other fields, place the latitude parameter where you want the map to appear.
59+
60+
## User Interactions
61+
62+
| Action | Effect |
63+
|---|---|
64+
| Click anywhere on the map | Moves the pin to that location; updates both inputs |
65+
| Drag the pin | Same as click — updates both inputs on release |
66+
| Type in the latitude input | Flies the map to the new position |
67+
| Type in the longitude input | Flies the map to the new position |
68+
| Use the geolocate button | Centers the map on the device's current location (does not set the pin — click after locating) |
69+
70+
## Existing vs. New Records
71+
72+
- **New record** (lat/lon values are `0` or empty): the map opens at zoom level 2 showing the
73+
full world. The pin is placed at 0,0. The user clicks to place it.
74+
- **Existing record** (lat/lon values are non-zero): the map opens at zoom level 14 centered on
75+
the existing coordinates with the pin already placed.
76+
77+
## Requirements
78+
79+
- A valid **MapBox token** must be configured in the site's Transformalize settings.
80+
- The `mapbox-gl` library version `3.6.0` must be registered as a named resource in the module.
81+
It is — no extra setup is required.
82+
83+
## Hidden vs. Visible Lat/Lon
84+
85+
If you want the numeric inputs hidden from the user (map-only interaction), set `prompt="false"`
86+
and `input="false"` on the lat/lon parameters, and use a separate mechanism to surface them. For
87+
typical edit-form usage, keeping `prompt="true"` gives power users the option to type coordinates
88+
directly.
89+
90+
## Modal Forms
91+
92+
The map picker works in modal forms without any extra configuration. `map.resize()` is called
93+
on map load to handle layout reflow inside the modal iframe.
94+
95+
## Validation
96+
97+
The Transformalize configuration parser validates `input-type="map"` parameters at load time:
98+
99+
- `type` must be `double`
100+
- `input-capture` must be `latitude` or `longitude` (or the parameter name must be `Latitude`/`Longitude`)
101+
102+
Misconfigured parameters produce a clear error in the process log before the form renders.

src/OrchardCore.BootswatchTheme.Settings/OrchardCore.BootswatchTheme.Settings.csproj

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5,9 +5,9 @@
55
<AddRazorSupportForMvc>true</AddRazorSupportForMvc>
66
<Nullable>enable</Nullable>
77
<ImplicitUsings>enable</ImplicitUsings>
8-
<Version>0.18.3</Version>
9-
<FileVersion>0.18.3</FileVersion>
10-
<AssemblyVersion>0.18.3</AssemblyVersion>
8+
<Version>0.19.0</Version>
9+
<FileVersion>0.19.0</FileVersion>
10+
<AssemblyVersion>0.19.0</AssemblyVersion>
1111
</PropertyGroup>
1212

1313
<ItemGroup>

src/OrchardCore.BootswatchTheme/OrchardCore.BootswatchTheme.csproj

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5,10 +5,10 @@
55
<AddRazorSupportForMvc>true</AddRazorSupportForMvc>
66
<Nullable>enable</Nullable>
77
<ImplicitUsings>enable</ImplicitUsings>
8-
<Version>0.18.3</Version>
8+
<Version>0.19.0</Version>
99
<AssemblyName>BootswatchTheme</AssemblyName>
10-
<FileVersion>0.18.3</FileVersion>
11-
<AssemblyVersion>0.18.3</AssemblyVersion>
10+
<FileVersion>0.19.0</FileVersion>
11+
<AssemblyVersion>0.19.0</AssemblyVersion>
1212
</PropertyGroup>
1313

1414
<ItemGroup>

src/OrchardCore.Proxy/OrchardCore.Proxy.csproj

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,9 +4,9 @@
44
<TargetFramework>net10.0</TargetFramework>
55
<AddRazorSupportForMvc>true</AddRazorSupportForMvc>
66
<RootNamespace>ProxyModule</RootNamespace>
7-
<Version>0.18.3</Version>
8-
<FileVersion>0.18.3</FileVersion>
9-
<AssemblyVersion>0.18.3</AssemblyVersion>
7+
<Version>0.19.0</Version>
8+
<FileVersion>0.19.0</FileVersion>
9+
<AssemblyVersion>0.19.0</AssemblyVersion>
1010
<GeneratePackageOnBuild>true</GeneratePackageOnBuild>
1111
<Authors>Dale Newman</Authors>
1212
<Copyright>Copyright © 2023-2026</Copyright>

src/OrchardCore.Transformalize/OrchardCore.Transformalize.csproj

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -3,9 +3,9 @@
33
<TargetFramework>net10.0</TargetFramework>
44
<AddRazorSupportForMvc>true</AddRazorSupportForMvc>
55
<RootNamespace>TransformalizeModule</RootNamespace>
6-
<Version>0.18.3</Version>
7-
<FileVersion>0.18.3</FileVersion>
8-
<AssemblyVersion>0.18.3</AssemblyVersion>
6+
<Version>0.19.0</Version>
7+
<FileVersion>0.19.0</FileVersion>
8+
<AssemblyVersion>0.19.0</AssemblyVersion>
99
<GeneratePackageOnBuild>true</GeneratePackageOnBuild>
1010
<Authors>Dale Newman</Authors>
1111
<Copyright>Copyright © 2013-2026</Copyright>
@@ -55,7 +55,7 @@
5555
<PackageReference Include="OrchardCoreContrib.ContentPermissions" Version="1.1.0" />
5656

5757
<!-- transformalize updated -->
58-
<PackageReference Include="Transformalize.Container.Autofac" Version="1.1.0" />
58+
<PackageReference Include="Transformalize.Container.Autofac" Version="1.2.1" />
5959
<PackageReference Include="Transformalize.Provider.Ado.Autofac" Version="1.0.0" />
6060
<PackageReference Include="Transformalize.Provider.Bogus.Autofac" Version="1.0.0" />
6161
<PackageReference Include="Transformalize.Provider.CsvHelper.Autofac" Version="1.0.0" />
@@ -64,7 +64,7 @@
6464
<PackageReference Include="Transformalize.Provider.File.Autofac" Version="1.0.0" />
6565
<PackageReference Include="Transformalize.Provider.GeoJson.Autofac" Version="1.0.0" />
6666
<PackageReference Include="Transformalize.Provider.Json.Autofac" Version="1.0.0" />
67-
<PackageReference Include="Transformalize.Provider.Mail.Autofac" Version="1.0.0" />
67+
<PackageReference Include="Transformalize.Provider.Mail.Autofac" Version="1.1.0" />
6868
<PackageReference Include="Transformalize.Provider.MySql.Autofac" Version="1.0.0" />
6969
<PackageReference Include="Transformalize.Provider.PostgreSql.Autofac" Version="1.0.0" />
7070
<PackageReference Include="Transformalize.Provider.Sqlite.Autofac" Version="1.0.0" />

src/OrchardCore.Transformalize/Views/Form/Index.cshtml

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212
var locationEnabled = Model.Process.Parameters.Any(p => p.InputType == "location");
1313
var editEnabled = Model.ContentItem != null && await auth.AuthorizeAsync(Context.User, OrchardCore.Contents.Permissions.EditContent, Model.ContentItem);
1414
var googlePlacesAutocomplete = Model.Process.Parameters.Any(p => p.InputType == "google-places-autocomplete");
15+
var mapPickerEnabled = Model.Process.Parameters.Any(p => p.InputType == "map");
1516
var isModal = Context.Request.Query["modal"] == "1";
1617
}
1718

@@ -42,6 +43,17 @@
4243
<script src="https://maps.googleapis.com/maps/api/js?key=@settings.GoogleApiKey&callback=initAutocomplete&libraries=places&v=weekly" async="async"></script>
4344
}
4445

46+
@if (mapPickerEnabled) {
47+
var mapSiteService = Context.RequestServices.GetService<ISiteService>();
48+
var mapSiteResult = mapSiteService.GetSiteSettingsAsync().Result;
49+
var mapSettings = mapSiteResult.As<TransformalizeSettings>();
50+
<script asp-name="mapbox-gl" version="3.6.0" at="Head"></script>
51+
<style asp-name="mapbox-gl" version="3.6.0"></style>
52+
<script type="text/javascript">
53+
var mapPickerToken = '@mapSettings.MapBoxToken';
54+
</script>
55+
}
56+
4557
<script asp-src="~/@Common.ModuleName/Scripts/form.js?v=8" at="Foot"></script>
4658
<script asp-src="~/@Common.ModuleName/Scripts/file.handler.js?v=2" at="Foot"></script>
4759

src/OrchardCore.Transformalize/Views/Shared/Form.cshtml

Lines changed: 95 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -572,6 +572,101 @@
572572
</div>
573573
break;
574574
575+
case "map":
576+
577+
var mapParamValue = value?.ToString() ?? string.Empty;
578+
579+
@if (parameter.InputCapture == "latitude") {
580+
var lngPartner = Model.Parameters.FirstOrDefault(p => p.InputType == "map" && p.InputCapture == "longitude");
581+
var lngName = lngPartner?.Name ?? string.Empty;
582+
<div id="id_map_@parameter.Name" style="height:300px; width:100%; border-radius:4px; margin-bottom:0.5rem;"></div>
583+
<script language="javascript">
584+
(function () {
585+
$(document).ready(function () {
586+
mapboxgl.accessToken = mapPickerToken;
587+
588+
var latEl = document.getElementById('id_@parameter.Name');
589+
var lngEl = document.getElementById('id_@lngName');
590+
591+
var lat = parseFloat(latEl ? latEl.value : '') || 0;
592+
var lng = parseFloat(lngEl ? lngEl.value : '') || 0;
593+
var hasCoords = lat !== 0 || lng !== 0;
594+
595+
var map = new mapboxgl.Map({
596+
container: 'id_map_@parameter.Name',
597+
style: 'mapbox://styles/mapbox/streets-v11',
598+
center: [hasCoords ? lng : 0, hasCoords ? lat : 0],
599+
zoom: hasCoords ? 14 : 2
600+
});
601+
602+
map.addControl(new mapboxgl.NavigationControl());
603+
map.addControl(new mapboxgl.GeolocateControl({ positionOptions: { enableHighAccuracy: true } }));
604+
605+
var marker = new mapboxgl.Marker({ draggable: true, color: '#e74c3c' })
606+
.setLngLat([hasCoords ? lng : 0, hasCoords ? lat : 0])
607+
.addTo(map);
608+
609+
var syncing = false;
610+
611+
function setLocation(lngLat) {
612+
syncing = true;
613+
marker.setLngLat(lngLat);
614+
if (latEl) { latEl.value = lngLat.lat.toFixed(7); $(latEl).trigger('change'); }
615+
if (lngEl) { lngEl.value = lngLat.lng.toFixed(7); $(lngEl).trigger('change'); }
616+
syncing = false;
617+
}
618+
619+
marker.on('dragend', function () {
620+
setLocation(marker.getLngLat());
621+
});
622+
623+
map.on('click', function (e) {
624+
setLocation(e.lngLat);
625+
});
626+
627+
function syncMarker() {
628+
if (syncing) return;
629+
var la = parseFloat(latEl ? latEl.value : NaN);
630+
var ln = parseFloat(lngEl ? lngEl.value : NaN);
631+
if (!isNaN(la) && !isNaN(ln)) {
632+
marker.setLngLat([ln, la]);
633+
map.flyTo({ center: [ln, la] });
634+
}
635+
}
636+
637+
if (latEl) $(latEl).on('change blur', syncMarker);
638+
if (lngEl) $(lngEl).on('change blur', syncMarker);
639+
640+
map.on('load', function () { map.resize(); });
641+
});
642+
})();
643+
</script>
644+
}
645+
646+
<div class="form-group @(parameter.Valid ? "is-valid" : "is-invalid") @parameter.Class">
647+
<label for="@parameter.Name" class="form-label">@(parameter.Label == string.Empty ? parameter.Name : parameter.Label)</label>
648+
@if (parameter.Hint != string.Empty) {
649+
<span class="text-muted float-end"> @parameter.Hint</span>
650+
}
651+
<input type="number"
652+
class="form-control @(parameter.Valid ? "is-valid" : "is-invalid")"
653+
name="@parameter.Name"
654+
id="id_@parameter.Name"
655+
placeholder="@parameter.Label"
656+
value="@mapParamValue"
657+
step="0.0000001"
658+
data-tfl-post-back="@(parameter.PostBack)"
659+
@Html.Raw(parameter.ToParsley()) />
660+
@* if you change location you must update parsley error container *@
661+
<span class="help-container">
662+
@if (!parameter.Valid) {
663+
<span class="help-block">@(parameter.Message.Replace('|', ' '))</span>
664+
}
665+
</span>
666+
</div>
667+
668+
break;
669+
575670
default:
576671
int length;
577672
<div class="form-group @(parameter.Valid ? "is-valid" : "is-invalid") @parameter.Class">

src/Site/App_Data/samples/gotup/got-up-delete.xml

Lines changed: 0 additions & 23 deletions
This file was deleted.

0 commit comments

Comments
 (0)