diff --git a/com.woltlab.wcf/templates/googleMapsElement.tpl b/com.woltlab.wcf/templates/shared_googleMapsElement.tpl
similarity index 84%
rename from com.woltlab.wcf/templates/googleMapsElement.tpl
rename to com.woltlab.wcf/templates/shared_googleMapsElement.tpl
index 7209d793ab5..75459513158 100644
--- a/com.woltlab.wcf/templates/googleMapsElement.tpl
+++ b/com.woltlab.wcf/templates/shared_googleMapsElement.tpl
@@ -17,5 +17,5 @@
>
{if $googleMapsHidden}
- {include file='messageUserConsent' host="maps.google.com" url="https://www.google.com/maps/" target=$googleMapsElementID sandbox=true}
+ {include file='shared_messageUserConsent' host="maps.google.com" url="https://www.google.com/maps/" target=$googleMapsElementID sandbox=true}
{/if}
diff --git a/com.woltlab.wcf/templates/shared_googleMapsFormField.tpl b/com.woltlab.wcf/templates/shared_googleMapsFormField.tpl
new file mode 100644
index 00000000000..6152e17c0c9
--- /dev/null
+++ b/com.woltlab.wcf/templates/shared_googleMapsFormField.tpl
@@ -0,0 +1,19 @@
+{capture assign='googleMapsElementID'}{$field->getPrefixedId()}_map{/capture}
+{include file='shared_googleMapsElement' accessUserLocation=true googleMapsLat=$field->getLatitude() googleMapsLng=$field->getLongitude()}
+
+getFieldClasses()|empty} class="{implode from=$field->getFieldClasses() item='class' glue=' '}{$class}{/implode}"{/if}
+ value="{$field->getValue()}"
+ {if $field->isAutofocused()} autofocus{/if}
+ {if $field->isRequired()} required{/if}
+ {if $field->isImmutable()} disabled{/if}
+ {if $field->getPlaceholder() !== null} placeholder="{$field->getPlaceholder()}"{/if}
+ {if $field->getDocument()->isAjax()} data-dialog-submit-on-enter="true"{/if}
+ {foreach from=$field->getFieldAttributes() key='attributeName' item='attributeValue'} {$attributeName}="{$attributeValue}"{/foreach}
+ data-google-maps-geocoding="{$googleMapsElementID}"
+ data-google-maps-geocoding-store="{$field->getPrefixedId()}_"
+ data-google-maps-marker
+>
diff --git a/com.woltlab.wcf/templates/messageUserConsent.tpl b/com.woltlab.wcf/templates/shared_messageUserConsent.tpl
similarity index 100%
rename from com.woltlab.wcf/templates/messageUserConsent.tpl
rename to com.woltlab.wcf/templates/shared_messageUserConsent.tpl
diff --git a/ts/WoltLabSuite/Core/Form/Builder/Field/GoogleMaps.ts b/ts/WoltLabSuite/Core/Form/Builder/Field/GoogleMaps.ts
new file mode 100644
index 00000000000..6e15524962e
--- /dev/null
+++ b/ts/WoltLabSuite/Core/Form/Builder/Field/GoogleMaps.ts
@@ -0,0 +1,24 @@
+/**
+ * Data handler for a Google Maps form builder field in an Ajax form.
+ *
+ * @author Marcel Werk
+ * @copyright 2001-2025 WoltLab GmbH
+ * @license GNU Lesser General Public License
+ * @since 6.2
+ */
+import { FormBuilderData } from "../Data";
+import Value from "./Value";
+import type WoltlabCoreGoogleMapsElement from "WoltLabSuite/Core/Component/GoogleMaps/woltlab-core-google-maps";
+
+class GoogleMaps extends Value {
+ protected _getData(): FormBuilderData {
+ const map = document.getElementById(this._fieldId + "_map") as WoltlabCoreGoogleMapsElement;
+
+ return {
+ [this._fieldId]: (this._field as HTMLInputElement).value,
+ [this._fieldId + "_coordinates"]: `${map.lat},${map.lng}`,
+ };
+ }
+}
+
+export = GoogleMaps;
diff --git a/wcfsetup/install/files/js/WoltLabSuite/Core/Form/Builder/Field/GoogleMaps.js b/wcfsetup/install/files/js/WoltLabSuite/Core/Form/Builder/Field/GoogleMaps.js
new file mode 100644
index 00000000000..1e05511fee0
--- /dev/null
+++ b/wcfsetup/install/files/js/WoltLabSuite/Core/Form/Builder/Field/GoogleMaps.js
@@ -0,0 +1,14 @@
+define(["require", "exports", "tslib", "./Value"], function (require, exports, tslib_1, Value_1) {
+ "use strict";
+ Value_1 = tslib_1.__importDefault(Value_1);
+ class GoogleMaps extends Value_1.default {
+ _getData() {
+ const map = document.getElementById(this._fieldId + "_map");
+ return {
+ [this._fieldId]: this._field.value,
+ [this._fieldId + "_coordinates"]: `${map.lat},${map.lng}`,
+ };
+ }
+ }
+ return GoogleMaps;
+});
diff --git a/wcfsetup/install/files/lib/system/form/builder/field/GoogleMapsFormField.class.php b/wcfsetup/install/files/lib/system/form/builder/field/GoogleMapsFormField.class.php
new file mode 100644
index 00000000000..bcd3e7894bd
--- /dev/null
+++ b/wcfsetup/install/files/lib/system/form/builder/field/GoogleMapsFormField.class.php
@@ -0,0 +1,123 @@
+
+ * @since 6.2
+ */
+final class GoogleMapsFormField extends AbstractFormField implements
+ IAttributeFormField,
+ IAutoFocusFormField,
+ ICssClassFormField,
+ IImmutableFormField,
+ IPlaceholderFormField
+{
+ use TInputAttributeFormField {
+ getReservedFieldAttributes as private getDefaultReservedFieldAttributes;
+ }
+ use TAutoFocusFormField;
+ use TCssClassFormField;
+ use TImmutableFormField;
+ use TPlaceholderFormField;
+
+ /**
+ * @inheritDoc
+ */
+ protected $javaScriptDataHandlerModule = 'WoltLabSuite/Core/Form/Builder/Field/GoogleMaps';
+
+ /**
+ * @inheritDoc
+ */
+ protected $templateName = 'shared_googleMapsFormField';
+
+ private float $latitude = 0;
+ private float $longitude = 0;
+
+ public function __construct()
+ {
+ $this->addFieldClass('long');
+ }
+
+ /**
+ * @return string[]
+ */
+ protected static function getReservedFieldAttributes(): array
+ {
+ return \array_merge(
+ static::getDefaultReservedFieldAttributes(),
+ [
+ 'data-google-maps-geocoding-store',
+ 'data-google-maps-geocoding',
+ 'data-google-maps-marker',
+ ]
+ );
+ }
+
+ #[\Override]
+ public function readValue()
+ {
+ if ($this->getDocument()->hasRequestData($this->getPrefixedId())) {
+ $this->value = $this->getDocument()->getRequestData($this->getPrefixedId());
+ }
+
+ if ($this->getDocument()->hasRequestData($this->getPrefixedId() . '_coordinates')) {
+ $coordinates = explode(',', $this->getDocument()->getRequestData(
+ $this->getPrefixedId() . '_coordinates'
+ ));
+ if (\count($coordinates) === 2) {
+ $this->latitude = \floatval($coordinates[0]);
+ $this->longitude = \floatval($coordinates[1]);
+ }
+ }
+
+ return $this;
+ }
+
+ #[\Override]
+ public function populate()
+ {
+ parent::populate();
+
+ $this->getDocument()->getDataHandler()->addProcessor(new CustomFormDataProcessor(
+ 'coordinates',
+ function (IFormDocument $document, array $parameters) {
+ if ($this->getValue()) {
+ $parameters[$this->getPrefixedId() . '_coordinates'] = [
+ 'latitude' => $this->getLatitude(),
+ 'longitude' => $this->getLongitude(),
+ ];
+ }
+
+ return $parameters;
+ }
+ ));
+
+ return $this;
+ }
+
+ public function getLatitude(): float
+ {
+ return $this->latitude;
+ }
+
+ public function getLongitude(): float
+ {
+ return $this->longitude;
+ }
+
+ public function coordinates(float $latitude, float $longitude): static
+ {
+ $this->latitude = $latitude;
+ $this->longitude = $longitude;
+
+ return $this;
+ }
+}
diff --git a/wcfsetup/install/files/lib/system/template/TemplateEngine.class.php b/wcfsetup/install/files/lib/system/template/TemplateEngine.class.php
index d268f7217cc..725675491fc 100755
--- a/wcfsetup/install/files/lib/system/template/TemplateEngine.class.php
+++ b/wcfsetup/install/files/lib/system/template/TemplateEngine.class.php
@@ -109,11 +109,13 @@ class TemplateEngine extends SingletonFactory
'formError' => 'shared_formError',
'formNotice' => 'shared_formNotice',
'formSuccess' => 'shared_formSuccess',
+ 'googleMapsElement' => 'shared_googleMapsElement',
'languageChooser' => 'shared_languageChooser',
'lineBreakSeparatedTextOptionType' => 'shared_lineBreakSeparatedTextOptionType',
'mediaManager' => 'shared_mediaManager',
'messageFormAttachments' => 'shared_messageFormAttachments',
'messageTableOfContents' => 'shared_messageTableOfContents',
+ 'messageUserConsent' => 'shared_messageUserConsent',
'multipleLanguageInputJavascript' => 'shared_multipleLanguageInputJavascript',
'passwordStrengthLanguage' => 'shared_passwordStrengthLanguage',
'quoteMetaCode' => 'shared_quoteMetaCode',