Приветствую коллеги! Пишу сейчас очень большой проект с кучей ништяков и приходится дописывать кучу плагинов своих и модулей и т.д. для typo3 7.6 (на версиях ниже не проверял и не хочу).
Возникла задача написать Гео пикер, т.е. нужно с гугла вытаскивать адрес, город, страну и лат\лонг. Есть решения типа экста geopicker, но он делает во всплывающем окошке, что выглядит старомодно, да и нет там нужных мне полей.
И так модуль я написал, без окошка но в модальном окне!
Так что кому будет полезно или интересно, прошу читать далее.
Итак поехали:
1. создаем FrontEnd plagin (я пользую kickstarter, лени ради). Можно конечно не ФронтЭнд, а сервис..
sic!
ext_tables.php и контроллер, модели и репозитории нам ненужны.
так что структура у нас такая:
Код:
Classes
-Form
--Element
---GeoPickerElement.php // рендерим конфиг для нового элемента
--Wizard
---GeoPickerWizard.php // Wizard для нового элемента
-Utility
--ExtConfiguration.php // Это мой хитрый файл для сбора всех конфигов, опишу ниже может пригодится.
Configuration
-Backend
--AjaxRoutes.php // добавляем ajax handler
Resources
-Private
--Language
---locallang.xlf
--Templates
---GeoPickerWizard.html
-Public
--Css
---geopicker.css
--Icons
---icon-blue.png (я использую не дефотный маркер)
--JavaScript
---GeoPosition.js
ext_icon.gif
ext_emconf.php
ext_localconf.php
ext_conf_template.txt
1.1 ext_emconf.php
PHP код:
defined('TYPO3_MODE') or die();
$GLOBALS['TYPO3_CONF_VARS']['SYS']['formEngine']['nodeRegistry']['geopicker'] = array(
'nodeName' => 'geopicker',
'priority' => 100,
'class' => \ER\ErGeopicker\Backend\Form\Element\GeoPickerElement::class,
);
Тем кто забыл, \ER\ - Vendor, \ErGeopicker\ - название расширения
Добавляем новый тип элемента TCA, в нашем расширении (в TCA )вставляем конопку
'geopicker' => array(
'label' =>'Geopicker',
'config'=> array(
'type' => 'geopicker',
'fields' => array(
'country' => 'country_field', <= поля куда будем возвращать значения, всего 5 (я испоьзую два)
'city' => 'city_field',
//'latitude' => null,
//'longitude' => null,
//'address' => null,
'title' => 'title_field'
)
)
)
1.2 ext_emconf.php
Тут все как обычно
1.3 ext_conf_template.txt
Настройки плагина, Google MAP API Key (для JS API), дефлотная широта и долгота испоьзуем при инициаизации карты
Код:
# cat=basic; type=string; label=Google Developers KEY
googleKey =
# cat=basic; type=string; label=Default Latitude
default_lat =
# cat=basic; type=string; label=Default Longitude
default_long =
1.4 Classes\Form\Element\GeoPickerElement.php
Тут будем рендерить наш новый элемент GEOPICKER
PHP код:
namespace ER\ErGeopicker\Backend\Form\Element;
Набор утилит: Debuger для проверки
use TYPO3\CMS\Core\Utility\GeneralUtility;
use TYPO3\CMS\Extbase\Utility\ArrayUtility;
use TYPO3\CMS\Backend\Form\Element\AbstractFormElement;
use TYPO3\CMS\Lang\LanguageService;
use TYPO3\CMS\Extbase\Utility\DebuggerUtility;
use TYPO3\CMS\Core\Utility\StringUtility;
use TYPO3\CMS\Backend\Utility\BackendUtility;
/**
* Generation of geopicker TCEform element
*/
class GeoPickerElement extends AbstractFormElement
{
/**
* Handler for geopicker type.
*
* @return array As defined in initializeResultArray() of AbstractNode
*/
public function render()
{
$resultArray = $this->initializeResultArray(); // сюда будем вставять свойства нашего элемента
$languageService = $this->getLanguageService(); // Просто языковой севрвис, нужен для языка карт (можно убрать и поставить язык по умолчанию)
$ll = 'LLL:EXT:er_geopicker/Resources/Private/Language/locallang.xlf:'; //линк на языковой файл
$row = $this->data['databaseRow']; // тут модель нашего элемента куда вставлен geopicker (я вытаскиваю title и только)
$parameterArray = $this->data['parameterArray']; // тут конфиг нашего поля TCA потом вытащим поля из него.
$config = ArrayUtility::arrayMergeRecursiveOverrule($extConfig, $parameterArray['fieldConf']['config']); // сливаем конфиги в один массив.. можно и не сливать.
$formFieldId = StringUtility::getUniqueId('formengine-geopicker-'); // hash что бы сдеать уникальный ID нашей кнокпи.
$title = '';
if($config['fields']['title']){
$title = $row[$config['fields']['title']];
unset( $config['fields']['title']);
}
$wizardData = array(
'default_lat' => $config['default_lat'],
'default_long' => $config['default_long'],
'title' => $title,
'lang' => null,
'unic_id' => $formFieldId
);
$wizardData['token'] = GeneralUtility::hmac(implode('|', $wizardData), 'GeoPickerWizard'); //TYPO3 encryption key
$buttonAttributes = array(
'data-url' => BackendUtility::getAjaxUrl('wizard_geo_picker', $wizardData), // wizard_geo_picker именно так /Wizard/GeoPicker.
'data-severity' => 'notice',
'data-field' => $formFieldId,
'data-title' => $title,
'data-fields' => json_encode($config['fields']), // массив с полями
'data-table' => $this->data['tableName'] // таблица нашего элемента
);
if($languageService->lang == 'default')
$langflag = 'en'; // если в бэкэнд язык дефолтный, то и флаг у него "default" у меня дэф ангийски, т.е. для англ карты мне нужен lang=en
else
$langflag = $languageService->lang; // для остальных все норм
// рендерим кнопку и и вставляем наши скрипты, тут все должно быть понятно
$button = '<button class="btn btn-default t3js-geopicker-trigger"';
foreach ($buttonAttributes as $key => $value) {
$button .= ' ' . $key . '="' . htmlspecialchars($value) . '"';
}
$button .= '><span class="t3-icon fa fa-map-marker"></span>';
$button .= $languageService->sL($ll.'geopicker.open', true);
$button .= '</button>';
$content = '<div class="form-control-wrap">'.$button.'</div>';
$resultArray['requireJsModules'][] = '//maps.googleapis.com/maps/api/js?key='.$config['googleKey'].'&language='.$langflag;
$resultArray['requireJsModules'][] = array(
'../typo3conf/ext/er_geopicker/Resources/Public/JavaScript/GeoPosition' => 'function(GeoPosition){
GeoPosition.initializeTrigger('.$config['default_lat'].', '.$config['default_long'].')}'
);
$resultArray['html'] = $content;
return $resultArray;
// все все параметры забиты, поехали в Wizard.
}
1.5 Classes\Form\Wizard\GeoPickerWizard.php
Важно! ни каких AjaxHandler, мы юзаем
use Psr\Http\Message\ResponseInterface;
use Psr\Http\Message\ServerRequestInterface;
Тут надеюсь все более менее понятно, комментировать особо нечего.
PHP код:
namespace ER\ErGeopicker\Backend\Form\Wizard;
use Psr\Http\Message\ResponseInterface;
use Psr\Http\Message\ServerRequestInterface;
use TYPO3\CMS\Core\Utility\GeneralUtility;
use TYPO3\CMS\Fluid\View\StandaloneView;
use TYPO3\CMS\Extbase\Utility\DebuggerUtility;
/**
* Wizard for rendering GoogleMap and Geopicker view
*/
class GeoPickerWizard
{
/**
* @var string
*/
protected $templatePath = 'EXT:er_geopicker/Resources/Private/Templates/';
/**
* @param ServerRequestInterface $request
* @param ResponseInterface $response
* @return ResponseInterface $response
*/
public function getWizardAction(ServerRequestInterface $request, ResponseInterface $response)
{
$assignedValues = array(
'title' => $request->getQueryParams()['title'],
'id' => $request->getQueryParams()['unic_id'] // сомнительная надобность в ID для DIV c гугл картой, т.к. даже при нескольких геопикерах в форме, модальное окно полностью убирается из DOM, пусть будет для красоты.
);
$view = $this->getFluidTemplateObject($this->templatePath . 'GeoPickerWizard.html'); // получаем наш тэмплэйт модального окошка
$view->assignMultiple($assignedValues);
$content = $view->render();
$response->getBody()->write($content);
return $response;
}
/**
* Returns a new standalone view, shorthand function
*
* @param string $templatePathAndFileName optional the path to set the template path and filename
* @return StandaloneView
*/
protected function getFluidTemplateObject($templatePathAndFileName = null)
{
$view = GeneralUtility::makeInstance(StandaloneView::class);
if ($templatePathAndFileName) {
$view->setTemplatePathAndFilename(GeneralUtility::getFileAbsFileName($templatePathAndFileName));
}
return $view;
}
}
1.6 Classes\Utility\ExtConfiguration.php
Кокретно в этом плагине весь функционал файла неиспоьзуется, но приведу целиком, может быдет полезен. Использую его в основном для вставки полей из ext_template_conf в TCA (например root category uid) или во вспомагательных скриптах для доступа к настройкам плагина.
PHP код:
namespace ER\ErGeopicker\Utility;
use TYPO3\CMS\Core\Utility\ExtensionManagementUtility;
use TYPO3\CMS\Core\Utility\GeneralUtility;
use TYPO3\CMS\Extbase\Configuration\ConfigurationManagerInterface;
/**
* Extension Manager configuration
*
*/
class ExtConfiguration {
/**
* Return the extension settings.
*
*/
public static function getSettings() {
$objectManager = GeneralUtility::makeInstance('TYPO3\\CMS\\Extbase\\Object\\ObjectManager');
$configurationManager = $objectManager->get('TYPO3\\CMS\\Extbase\\Configuration\\ConfigurationManager');
$settings = $configurationManager->getConfiguration(ConfigurationManagerInterface::CONFIGURATION_TYPE_SETTINGS, 'ergeopicker'); // 'ergeopicker' как в setup.txt
return $settings;
}
/**
* Return the All extension settings.
*
*/
public function getAllSettings() {
return array_merge(self::getSettings(), self::getConfiguratuion());
}
/**
* Parse settings and return it as array
*
* @return array unserialized extconf settings
*/
public static function getConfiguratuion()
{
$settings = unserialize($GLOBALS['TYPO3_CONF_VARS']['EXT']['extConf']['er_geopicker']); // название расширения
if (!is_array($settings)) {
$settings = [];
}
return $settings;
}
}
1.7 Configuration\Backend\AjaxRoutes.php
Прописываем ajax handler
PHP код:
use TYPO3\CMS\Backend\Controller;
/**
* Definitions for routes provided by EXT:er_geopicker
*/
return [
'wizard_geo_picker' => [
'path' => '/wizard/geo-picker',
'target' => \ER\ErGeopicker\Backend\Form\Wizard\GeoPickerWizard::class . '::getWizardAction'
],
];
Тут все вроде понятно должно быть.
Так с северной частью закончили, осталось Language, CSS, Template и JS
Забираем их (кроме JS, его напишем)
locallang.xlf
GeoPickerWizard.html
geopicker.css
icon-blue.png
1.8 \Resources\Public\JavaScript\GeoPosition.js
Заключительная часть кордебалета!
Сразу скажу, не занимался ювелирным кодингом на JS...Если сможите написать лучше! Бог в помощь и барабан на шею
Код:
/**
* Module: ER/ErGeopicker
* Contains all logic for the googlemap API
*/
define(['jquery', 'TYPO3/CMS/Backend/Modal'], function ($, Modal) {
/**
*
* @exports ER/ErGeopicker
*/
var GeoPosition = {
$trigger: null,
zoom: 12,
$map: null,
$pos:null,
$image:null,
latitude:null,
longitude:null,
address: null,
city: null,
country:null,
$marker:[],
$geocoder:null
};
Тут у меня микс глобальных и полуглобальных и вообще хз каких переменных. Чтоб были.
GeoPosition.initializeTrigger = function(lat, long){
GeoPosition.$pos = { lat: lat, lng: long }; // Дефолтные широта и долгота..
var $triggers = $('.t3js-geopicker-trigger');
// Remove existing bind function
$triggers.off('click', GeoPosition.buttonClick);
// Bind new function
$triggers.on('click', GeoPosition.buttonClick);
}
// кликалка с защитой от даблклика
GeoPosition.buttonClick = function(e) {
e.preventDefault();
// Prevent double trigger
if (GeoPosition.$trigger !== $(this)) {
GeoPosition.$trigger = $(this);
GeoPosition.show();
}
};
// открываем модальное окошко
GeoPosition.show = function() {
GeoPosition.currentModal = Modal.loadUrl(
GeoPosition.$trigger.data('title'),
TYPO3.Severity.notice,
[],
GeoPosition.$trigger.data('url'),
GeoPosition.showMap,
'.modal-content'
);
var modalWidth = $(window).width();
GeoPosition.currentModal.addClass('modal-dark');
GeoPosition.currentModal.find('.modal-dialog')
.addClass('modal-geopicker modal-resize')
.css({width: modalWidth});
GeoPosition.currentModal.find('.modal-content').css({marginTop:50});
setTimeout(function() {
var sidebarWidth = GeoPosition.currentModal.find('.modal-panel-sidebar').outerWidth();
var bodyWidth = modalWidth-sidebarWidth-2;
GeoPosition.currentModal.find('.modal-panel-body').css({width:bodyWidth});
}, 500);
// Т.к. открытие и погрузка шаблона требует некоторого времени, то лучше все действия делать с небольшой задержкой.
};
//после открытия окна, callback'ом вызываем карту, опять же с задержкой.. без задержки бывает google скрипт не успевает прогружатся.. но это на локальном хосте.
GeoPosition.showMap = function(){
setTimeout(function() {
GeoPosition.initializeMap();
}, 300);
};
// собственно сама карта, я делал без стилей и т.д. чисто карта как есть.
GeoPosition.initializeMap = function(){
$map = new google.maps.Map(GeoPosition.currentModal.find('.gmap')[0], {
center: GeoPosition.$pos,
zoom: GeoPosition.zoom
});
$geocoder = new google.maps.Geocoder(); // Важно, ваш АПИ код должен быть активирован для использования geocodera
// наш не дефолтный маркер
GeoPosition.$image = {
// This marker is 64 pixels wide by 64 pixels high.
size: new google.maps.Size(64, 64),
// The origin for this image is (0, 0).
origin: new google.maps.Point(0, 0),
// The anchor for this image is the base of the flagpole at (0, 32).
anchor: new google.maps.Point(24, 48),
scaledSize: new google.maps.Size(48, 48),
url: '../typo3conf/ext/er_geopicker/Resources/Public/Icons/map-icons/icon-blue.png',
}
// определяем текущее положение пользователя.
if (navigator.geolocation) {
navigator.geolocation.getCurrentPosition(
function( position ){
GeoPosition.$pos = {
lat: position.coords.latitude,
lng: position.coords.longitude
}
GeoPosition.findByLatLng(GeoPosition.$pos);
}
)
}
// Инпут с поиском по адресу, ищем по клику
GeoPosition.currentModal.find('#submit').on('click', function(e){
var address = GeoPosition.currentModal.find('#gsearch').val();
GeoPosition.findByAdress(address);
return false;
});
// Инициализируем Сохранить и Закрыть
GeoPosition.initializeActions();
};
// поиск по адресу
GeoPosition.findByAdress = function (address) {
var componentRestrictions = {}; // тут можно вписать ограничения по региону и т.д.
$geocoder.geocode({'address': address, 'componentRestrictions': componentRestrictions}, function(results, status) {
if (status === google.maps.GeocoderStatus.OK) {
$map.setCenter(results[0].geometry.location);
GeoPosition.addMarker(results[0].geometry.location);
GeoPosition.setresult(results);
} else {
console.log(status);
}
});
};
// поиск по координатам
GeoPosition.findByLatLng = function(pos){
var componentRestrictions = {};
$geocoder.geocode({'location': pos, 'componentRestrictions': componentRestrictions}, function(results, status) {
if (status === google.maps.GeocoderStatus.OK) {
GeoPosition.addMarker(pos);
GeoPosition.setresult(results);
} else {
console.log(status);
}
})
};
// Создаем маркер, т.к. маркер всегда один на карте то перед созданием обнуляем маркеры на карте и в массиве
GeoPosition.addMarker = function(position){
GeoPosition.setMarkers(null);
var marker = new google.maps.Marker({
map: $map,
position: position,
icon: GeoPosition.$image,
draggable: true,
animation: google.maps.Animation.DROP,
title: GeoPosition.$trigger.data('title')
});
GeoPosition.$marker.push(marker);
GeoPosition.setMarkers($map);
};
//вставляем маркер на карту если map==null то убираем все что есть
GeoPosition.setMarkers = function(map){
for (var i = 0; i < GeoPosition.$marker.length; i++) {
GeoPosition.$marker[i].setMap(map);
GeoPosition.listner(GeoPosition.$marker[i]);
}
if(!map)
GeoPosition.$marker=[];
};
// добавляем слушателей на карту и маркер check - нужен что бы убрать даблклики и т.д. т.к. у гугл есть лимит на обращения к geocoder и не только.
GeoPosition.listner = function(marker){
var check = true;
google.maps.event.addListener(marker, 'dragend', function (results) {
if(check){
GeoPosition.findByLatLng(marker.getPosition());
check = false;
}
setTimeout(function() {
check = true;
}, 10000);
});
$map.addListener('click', function(event) {
if(check){
GeoPosition.findByLatLng(event.latLng);
check = false;
}
setTimeout(function() {
check = true;
}, 10000);
});
};
// Из гугловского ответа выбираем только город, страну и адрес. Детали и параметры ответа можно узнать у гугла детальнее.
GeoPosition.setresult = function(results){
// default field: country, city, latitude, lonitude, address
var modal = GeoPosition.currentModal;
var checkLocality = true;
var checkRoute = true;
$.each(results, function(i, val){
$.each(val['types'], function(k,type){
if(type === 'locality') {
if(checkLocality) {
GeoPosition.city = val.address_components[0].long_name;
GeoPosition.country = val.address_components[val.address_components.length-1].long_name;
}
checkLocality = false;
}
if(type === 'route') {
if(checkRoute) {
GeoPosition.address = val.formatted_address;
}
checkRoute = false;
}
});
});
if(checkRoute)
GeoPosition.address = ''; // бывает адреса и нет...
GeoPosition.latitude = results[0].geometry.location.lat();
GeoPosition.longitude = results[0].geometry.location.lng();
modal.find('#city').text(GeoPosition.city);
modal.find('#country').text(GeoPosition.country);
modal.find('#longitude').text(GeoPosition.longitude);
modal.find('#latitude').text(GeoPosition.latitude);
modal.find('#route').text(GeoPosition.address);
};
// А вот наши Сохранить и закрыть
GeoPosition.initializeActions = function() {
GeoPosition.currentModal.find('[data-method]').click(function(e) {
e.preventDefault();
var method = $(this).data('method');
var options = $(this).data('option') || {};
if (typeof GeoPosition[method] === 'function') {
GeoPosition[method](options);
}
});
};
// Сохраняем, т.е. отправляем данные обатно в нашу форму.
GeoPosition.save = function(){
// default field: country, city, latitude, lonitude, address
var formFields = GeoPosition.$trigger.data('fields');
var table = GeoPosition.$trigger.data('table');
var form = GeoPosition.$trigger.closest('form');
// если бы не убрали title тот тут бы он и всплыл.. и попытался за заменится на что нибудь.
$.each(formFields, function(key, field){
var selector = form.find('[data-formengine-input-name*="'+field+'"]');
$(selector[0]).val(GeoPosition[key]);
TBE_EDITOR.fieldChanged(table, 1, field, $(selector[0]).data('formengine-input-name'));
});
GeoPosition.dismiss();
};
// Закрыть окошко
GeoPosition.dismiss = function() {
if (GeoPosition.currentModal) {
GeoPosition.currentModal.modal('hide');
GeoPosition.currentModal = null;
}
};
return GeoPosition;
мой плагин er_geopicker соберетесь поменять, не забудьте сделать это везде.
За основу и в качестве рабочего примера смотрел в плагины t3editor и в ImageManipulationElement