Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 10 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,13 @@
# 0.19.0 <small>2026-04-26</small>

## 🚀 Features
- Add map picker for forms (`input-type="map"`). Configure latitude and longitude parameters with
`input-type="map"` to render an interactive MapBox map inline in the form. Users can click or drag
a pin to set coordinates, which sync to the editable numeric inputs. Typing directly in the inputs
repositions the pin. Works in standard and modal form modes. See `docs/map-picker.md`.

<!-- CHANGELOG_BOUNDARY -->

# 0.18.2 <small>2026-04-12</small>

## 💅 Improvements
Expand Down
102 changes: 102 additions & 0 deletions docs/map-picker.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
# Map Picker

The map picker is a form input type that renders a small interactive MapBox map inline in a form,
allowing users to set a geographic location by clicking or dragging a pin. The selected coordinates
are written to two numeric parameters — one for latitude and one for longitude.

## Configuration

Add `input-type="map"` to your latitude and longitude parameters. Both parameters must have
`type="double"`.

```xml
<parameters>
<add name="Latitude"
type="double"
input-type="map"
prompt="true"
label="Latitude" />
<add name="Longitude"
type="double"
input-type="map"
prompt="true"
label="Longitude" />
</parameters>
```

The system determines which parameter is latitude and which is longitude via `input-capture`.
If your parameter names are `Latitude` and `Longitude` (case-insensitive), `input-capture` is
set automatically. For any other names, set it explicitly:

```xml
<add name="Lat"
type="double"
input-type="map"
input-capture="latitude"
prompt="true"
label="Latitude" />
<add name="Lng"
type="double"
input-type="map"
input-capture="longitude"
prompt="true"
label="Longitude" />
```

`input-capture` accepts `latitude` or `longitude`.

## How It Renders

The latitude parameter renders:
1. A 300px MapBox map with a draggable red pin and navigation/geolocate controls
2. A numeric text input for latitude below the map

The longitude parameter renders:
1. A numeric text input for longitude (no second map)

The map appears above the latitude input. If you want the map to appear in a specific position
in the form relative to other fields, place the latitude parameter where you want the map to appear.

## User Interactions

| Action | Effect |
|---|---|
| Click anywhere on the map | Moves the pin to that location; updates both inputs |
| Drag the pin | Same as click — updates both inputs on release |
| Type in the latitude input | Flies the map to the new position |
| Type in the longitude input | Flies the map to the new position |
| Use the geolocate button | Centers the map on the device's current location (does not set the pin — click after locating) |

## Existing vs. New Records

- **New record** (lat/lon values are `0` or empty): the map opens at zoom level 2 showing the
full world. The pin is placed at 0,0. The user clicks to place it.
- **Existing record** (lat/lon values are non-zero): the map opens at zoom level 14 centered on
the existing coordinates with the pin already placed.

## Requirements

- A valid **MapBox token** must be configured in the site's Transformalize settings.
- The `mapbox-gl` library version `3.6.0` must be registered as a named resource in the module.
It is — no extra setup is required.

## Hidden vs. Visible Lat/Lon

If you want the numeric inputs hidden from the user (map-only interaction), set `prompt="false"`
and `input="false"` on the lat/lon parameters, and use a separate mechanism to surface them. For
typical edit-form usage, keeping `prompt="true"` gives power users the option to type coordinates
directly.

## Modal Forms

The map picker works in modal forms without any extra configuration. `map.resize()` is called
on map load to handle layout reflow inside the modal iframe.

## Validation

The Transformalize configuration parser validates `input-type="map"` parameters at load time:

- `type` must be `double`
- `input-capture` must be `latitude` or `longitude` (or the parameter name must be `Latitude`/`Longitude`)

Misconfigured parameters produce a clear error in the process log before the form renders.
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,9 @@
<AddRazorSupportForMvc>true</AddRazorSupportForMvc>
<Nullable>enable</Nullable>
<ImplicitUsings>enable</ImplicitUsings>
<Version>0.18.3</Version>
<FileVersion>0.18.3</FileVersion>
<AssemblyVersion>0.18.3</AssemblyVersion>
<Version>0.19.0</Version>
<FileVersion>0.19.0</FileVersion>
<AssemblyVersion>0.19.0</AssemblyVersion>
</PropertyGroup>

<ItemGroup>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,10 @@
<AddRazorSupportForMvc>true</AddRazorSupportForMvc>
<Nullable>enable</Nullable>
<ImplicitUsings>enable</ImplicitUsings>
<Version>0.18.3</Version>
<Version>0.19.0</Version>
<AssemblyName>BootswatchTheme</AssemblyName>
<FileVersion>0.18.3</FileVersion>
<AssemblyVersion>0.18.3</AssemblyVersion>
<FileVersion>0.19.0</FileVersion>
<AssemblyVersion>0.19.0</AssemblyVersion>
</PropertyGroup>

<ItemGroup>
Expand Down
6 changes: 3 additions & 3 deletions src/OrchardCore.Proxy/OrchardCore.Proxy.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,9 @@
<TargetFramework>net10.0</TargetFramework>
<AddRazorSupportForMvc>true</AddRazorSupportForMvc>
<RootNamespace>ProxyModule</RootNamespace>
<Version>0.18.3</Version>
<FileVersion>0.18.3</FileVersion>
<AssemblyVersion>0.18.3</AssemblyVersion>
<Version>0.19.0</Version>
<FileVersion>0.19.0</FileVersion>
<AssemblyVersion>0.19.0</AssemblyVersion>
<GeneratePackageOnBuild>true</GeneratePackageOnBuild>
<Authors>Dale Newman</Authors>
<Copyright>Copyright © 2023-2026</Copyright>
Expand Down
10 changes: 5 additions & 5 deletions src/OrchardCore.Transformalize/OrchardCore.Transformalize.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,9 @@
<TargetFramework>net10.0</TargetFramework>
<AddRazorSupportForMvc>true</AddRazorSupportForMvc>
<RootNamespace>TransformalizeModule</RootNamespace>
<Version>0.18.3</Version>
<FileVersion>0.18.3</FileVersion>
<AssemblyVersion>0.18.3</AssemblyVersion>
<Version>0.19.0</Version>
<FileVersion>0.19.0</FileVersion>
<AssemblyVersion>0.19.0</AssemblyVersion>
<GeneratePackageOnBuild>true</GeneratePackageOnBuild>
<Authors>Dale Newman</Authors>
<Copyright>Copyright © 2013-2026</Copyright>
Expand Down Expand Up @@ -55,7 +55,7 @@
<PackageReference Include="OrchardCoreContrib.ContentPermissions" Version="1.1.0" />

<!-- transformalize updated -->
<PackageReference Include="Transformalize.Container.Autofac" Version="1.1.0" />
<PackageReference Include="Transformalize.Container.Autofac" Version="1.2.1" />
<PackageReference Include="Transformalize.Provider.Ado.Autofac" Version="1.0.0" />
<PackageReference Include="Transformalize.Provider.Bogus.Autofac" Version="1.0.0" />
<PackageReference Include="Transformalize.Provider.CsvHelper.Autofac" Version="1.0.0" />
Expand All @@ -64,7 +64,7 @@
<PackageReference Include="Transformalize.Provider.File.Autofac" Version="1.0.0" />
<PackageReference Include="Transformalize.Provider.GeoJson.Autofac" Version="1.0.0" />
<PackageReference Include="Transformalize.Provider.Json.Autofac" Version="1.0.0" />
<PackageReference Include="Transformalize.Provider.Mail.Autofac" Version="1.0.0" />
<PackageReference Include="Transformalize.Provider.Mail.Autofac" Version="1.1.0" />
<PackageReference Include="Transformalize.Provider.MySql.Autofac" Version="1.0.0" />
<PackageReference Include="Transformalize.Provider.PostgreSql.Autofac" Version="1.0.0" />
<PackageReference Include="Transformalize.Provider.Sqlite.Autofac" Version="1.0.0" />
Expand Down
12 changes: 12 additions & 0 deletions src/OrchardCore.Transformalize/Views/Form/Index.cshtml
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
var locationEnabled = Model.Process.Parameters.Any(p => p.InputType == "location");
var editEnabled = Model.ContentItem != null && await auth.AuthorizeAsync(Context.User, OrchardCore.Contents.Permissions.EditContent, Model.ContentItem);
var googlePlacesAutocomplete = Model.Process.Parameters.Any(p => p.InputType == "google-places-autocomplete");
var mapPickerEnabled = Model.Process.Parameters.Any(p => p.InputType == "map");
var isModal = Context.Request.Query["modal"] == "1";
}

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

@if (mapPickerEnabled) {
var mapSiteService = Context.RequestServices.GetService<ISiteService>();
var mapSiteResult = mapSiteService.GetSiteSettingsAsync().Result;
var mapSettings = mapSiteResult.As<TransformalizeSettings>();
<script asp-name="mapbox-gl" version="3.6.0" at="Head"></script>
<style asp-name="mapbox-gl" version="3.6.0"></style>
<script type="text/javascript">
var mapPickerToken = '@mapSettings.MapBoxToken';
</script>
}

<script asp-src="~/@Common.ModuleName/Scripts/form.js?v=8" at="Foot"></script>
<script asp-src="~/@Common.ModuleName/Scripts/file.handler.js?v=2" at="Foot"></script>

Expand Down
95 changes: 95 additions & 0 deletions src/OrchardCore.Transformalize/Views/Shared/Form.cshtml
Original file line number Diff line number Diff line change
Expand Up @@ -572,6 +572,101 @@
</div>
break;

case "map":

var mapParamValue = value?.ToString() ?? string.Empty;

@if (parameter.InputCapture == "latitude") {
var lngPartner = Model.Parameters.FirstOrDefault(p => p.InputType == "map" && p.InputCapture == "longitude");
var lngName = lngPartner?.Name ?? string.Empty;
<div id="id_map_@parameter.Name" style="height:300px; width:100%; border-radius:4px; margin-bottom:0.5rem;"></div>
<script language="javascript">
(function () {
$(document).ready(function () {
mapboxgl.accessToken = mapPickerToken;

var latEl = document.getElementById('id_@parameter.Name');
var lngEl = document.getElementById('id_@lngName');

var lat = parseFloat(latEl ? latEl.value : '') || 0;
var lng = parseFloat(lngEl ? lngEl.value : '') || 0;
var hasCoords = lat !== 0 || lng !== 0;

var map = new mapboxgl.Map({
container: 'id_map_@parameter.Name',
style: 'mapbox://styles/mapbox/streets-v11',
center: [hasCoords ? lng : 0, hasCoords ? lat : 0],
zoom: hasCoords ? 14 : 2
});

map.addControl(new mapboxgl.NavigationControl());
map.addControl(new mapboxgl.GeolocateControl({ positionOptions: { enableHighAccuracy: true } }));

var marker = new mapboxgl.Marker({ draggable: true, color: '#e74c3c' })
.setLngLat([hasCoords ? lng : 0, hasCoords ? lat : 0])
.addTo(map);

var syncing = false;

function setLocation(lngLat) {
syncing = true;
marker.setLngLat(lngLat);
if (latEl) { latEl.value = lngLat.lat.toFixed(7); $(latEl).trigger('change'); }
if (lngEl) { lngEl.value = lngLat.lng.toFixed(7); $(lngEl).trigger('change'); }
syncing = false;
}

marker.on('dragend', function () {
setLocation(marker.getLngLat());
});

map.on('click', function (e) {
setLocation(e.lngLat);
});

function syncMarker() {
if (syncing) return;
var la = parseFloat(latEl ? latEl.value : NaN);
var ln = parseFloat(lngEl ? lngEl.value : NaN);
if (!isNaN(la) && !isNaN(ln)) {
marker.setLngLat([ln, la]);
map.flyTo({ center: [ln, la] });
}
}

if (latEl) $(latEl).on('change blur', syncMarker);
if (lngEl) $(lngEl).on('change blur', syncMarker);

map.on('load', function () { map.resize(); });
});
})();
</script>
}

<div class="form-group @(parameter.Valid ? "is-valid" : "is-invalid") @parameter.Class">
<label for="@parameter.Name" class="form-label">@(parameter.Label == string.Empty ? parameter.Name : parameter.Label)</label>
@if (parameter.Hint != string.Empty) {
<span class="text-muted float-end"> @parameter.Hint</span>
}
<input type="number"
class="form-control @(parameter.Valid ? "is-valid" : "is-invalid")"
name="@parameter.Name"
id="id_@parameter.Name"
placeholder="@parameter.Label"
value="@mapParamValue"
step="0.0000001"
data-tfl-post-back="@(parameter.PostBack)"
@Html.Raw(parameter.ToParsley()) />
@* if you change location you must update parsley error container *@
<span class="help-container">
@if (!parameter.Valid) {
<span class="help-block">@(parameter.Message.Replace('|', ' '))</span>
}
</span>
</div>

break;

default:
int length;
<div class="form-group @(parameter.Valid ? "is-valid" : "is-invalid") @parameter.Class">
Expand Down
23 changes: 0 additions & 23 deletions src/Site/App_Data/samples/gotup/got-up-delete.xml

This file was deleted.

Loading
Loading