Приветствую коллеги! Заморочился я тут с Inline Elements, как мы уже знаем там нет функционала как в Group и добавлять существующие записи уже невозможно, а хотелось бы.. можно конечно использовать Group c "+", но тогда если нам нужно создать, нас будет кидать на новую страницу.. что совсем не камильфо, нужно здесь и сейчас, т.е. нам все таки нужен функционал Inline.
Мне вот тут приперло, так что делюсь своим решением это задачи.. опять же у кого есть задумки лучше и проще, прошу в студию.
На выходе мы получим вот такую красоту, поиск с автокомплитом и доп кнопку на "Отсоединение элемента", так что бы старые элементы не удалялись, а отсоединялись.
Поехали...
1. создаем новый ajaxRoute -> Cofiguration/Backend/AjaxRoutes.php
PHP код:
<?php
use TYPO3\CMS\Backend\Controller;
return [
'wizard_get_related' => [
'path' => '/wizard/get-related',
'target' => \Extension\Backend\Form\Wizard\GetRelatedWizard::class . '::searchAction'
],
];
В визард будем слать запросы на поиск.
2. в ext_tables.php добавляем иконку кнопки и Hook для InlineElements
PHP код:
$iconRegistry = \TYPO3\CMS\Core\Utility\GeneralUtility::makeInstance(\TYPO3\CMS\Core\Imaging\IconRegistry::class);
$iconRegistry->registerIcon(
'chain-broken',
\TYPO3\CMS\Core\Imaging\IconProvider\BitmapIconProvider::class,
['source' => 'EXT:iconfont/Resources/Public/Image/font-awesome/chain-broken.png']
);
// У меня стоит ext iconfont вы можете подгрузить любую свою (svg или png)
$GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['t3lib/class.t3lib_tceforms_inline.php']['tceformsInlineHook'][$_EXTKEY] =
\Extension\Hooks\InlineElementHook::class;
// Этот Хук нужен для создания кнопки Unchain
3. Ищем FormInlineAjaxController в sysext и забираем его целиком (Можно конечно дописать свою функци, потом сделать extend.. но как по мне проще было просто скопировать)
его кладем сюда: Classes\Controller\FormInlineAjaxController.php
И меняем всего пару строк:
PHP код:
...
//ищем вот это:
if ($childChildUid) {
$formDataCompilerInput['inlineChildChildUid'] = $childChildUid;
}
// далее эту строку:
$childData = $formDataCompiler->compile($formDataCompilerInput);
// меняем на это:
if($parentConfig['addrelated'] && !$parentConfig['foreign_selector'] && $childChildUid)
$childData = $this->compileChild($parentData, $parentFieldName, (int)$childChildUid, $inlineStackProcessor->getStructure());
else
$childData = $formDataCompiler->compile($formDataCompilerInput);
//$parentConfig['addrelated'] - это новый параметр в TCA config, что бы отсеивать рендеринг добавляем элементов. Обязательно при условии !$parentConfig['foreign_selector'] это условие что мы не рендерим все через вспомогательные таблицы и $childChildUid это что бы наверняка!
$childData = $this->compileChild -> тут получаем всю инфу по добавляемому элементу. Это позволит получить все напрямую, без foreign_selectorи т.д. в нашем случае мы работаем с таблицей напрямую, а не через Intermediate tables.
4. Classes/Form/Wizard/GetRelatedWizard.php
Тут многое из SuggestWizard.. только в немного упрощенном виде
Думаю еще можно сократить...
PHP код:
<?php
namespace Extension\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\Lang\LanguageService;
use TYPO3\CMS\Core\Imaging\Icon;
use TYPO3\CMS\Core\Imaging\IconFactory;
class GetRelatedWizard
{
/**
* Renders an ajax-enabled text field. Also adds required JS
*
* @param string $fieldname The fieldname in the form
* @param string $table The table we render this selector for
* @param string $field The field we render this selector for
* @param array $row The row which is currently edited
* @param array $config The TSconfig of the field
* @return string The HTML code for the selector
*/
public function renderSuggestSelector($fieldname, $table, $field, array $row, array $config)
{
/** @var $iconFactory IconFactory */
$iconFactory = GeneralUtility::makeInstance(IconFactory::class);
$languageService = $this->getLanguageService();
$minChars = 2;
$uids = $row[$field];
// fetch the TCA field type to hand it over to the JS class
$type = 'inline';
$selector = '
<div class="autocomplete t3-form-suggest-container">
<div class="input-group">
<span class="input-group-addon">' . $iconFactory->getIcon('actions-search', Icon::SIZE_SMALL)->render() . '</span>
<input type="search" class="t3-form-suggest-inline form-control"
placeholder="' . $languageService->sL('LLL:EXT:lang/locallang_core.xlf:labels.findRecord') . '"
data-fieldname="' . $fieldname . '"
data-table="' . $table . '"
data-field="' . $field . '"
data-uid="' . $row['uid'] . '"
data-pid="' . $row['pid'] . '"
data-uids="' . $row[$field] . '" // сюда добавляем все юиды элементов которые у нас уже есть
data-fieldtype="' . $type . '"
data-minchars="' . $minChars . '"
data-recorddata="' . htmlspecialchars($jsRow) . '"
/>
</div>
</div>';
return $selector;
}
/**
* Ajax handler for the "suggest" feature in FormEngine.
*
* @param ServerRequestInterface $request
* @param ResponseInterface $response
* @return ResponseInterface
*/
public function searchAction(ServerRequestInterface $request, ResponseInterface $response)
{
$parsedBody = $request->getParsedBody();
$queryParams = $request->getQueryParams();
// Get parameters from $_GET/$_POST
$search = isset($parsedBody['value']) ? $parsedBody['value'] : $queryParams['value'];
$table = isset($parsedBody['table']) ? $parsedBody['table'] : $queryParams['table'];
$field = isset($parsedBody['field']) ? $parsedBody['field'] : $queryParams['field'];
$uid = isset($parsedBody['uid']) ? $parsedBody['uid'] : $queryParams['uid'];
$pageId = (int)(isset($parsedBody['pid']) ? $parsedBody['pid'] : $queryParams['pid']);
$uids = isset($parsedBody['uids']) ? $parsedBody['uids'] : $queryParams['uids'];
$newRecordRow = isset($parsedBody['newRecordRow']) ? $parsedBody['newRecordRow'] : $queryParams['newRecordRow'];
$fieldConfig = $GLOBALS['TCA'][$table]['columns'][$field]['config'];
$queryTables = $this->getTablesToQueryFromFieldConfiguration($fieldConfig);
$whereClause = $this->getWhereClause($fieldConfig, $uids);
$resultRows = array();
foreach ($queryTables as $queryTable) {
// if the table does not exist, skip it
if (!is_array($GLOBALS['TCA'][$queryTable]) || empty($GLOBALS['TCA'][$queryTable])) {
continue;
}
$config = array('searchWholePhrase'=> 1);
// process addWhere
if ($whereClause) {
$config['addWhere'] = $whereClause;
}
$config['maxItemsInResultList'] = 10;
$receiverClassName = \TYPO3\CMS\Backend\Form\Wizard\SuggestWizardDefaultReceiver::class;
$receiverObj = GeneralUtility::makeInstance($receiverClassName, $queryTable, $config);
$params = array('value' => $search);
$rows = $receiverObj->queryTable($params);
}
$maxItems = $config['maxItemsInResultList'];
$maxItems = min(count($rows), $maxItems);
$listItems = $this->createListItemsFromResultRow($rows, $maxItems);
$response->getBody()->write(json_encode($listItems));
return $response;
}
/**
* @return LanguageService
*/
protected function getLanguageService()
{
return $GLOBALS['LANG'];
}
/**
* Checks the given field configuration for the tables that should be used for querying and returns them as an
* array.
*
* @param array $fieldConfig
* @return array
*/
protected function getTablesToQueryFromFieldConfiguration(array $fieldConfig)
{
$queryTables = array();
if (isset($fieldConfig['allowed'])) {
if ($fieldConfig['allowed'] !== '*') {
// list of allowed tables
$queryTables = GeneralUtility::trimExplode(',', $fieldConfig['allowed']);
} else {
// all tables are allowed, if the user can access them
foreach ($GLOBALS['TCA'] as $tableName => $tableConfig) {
if (!$this->isTableHidden($tableConfig) && $this->currentBackendUserMayAccessTable($tableConfig)) {
$queryTables[] = $tableName;
}
}
unset($tableName, $tableConfig);
}
} elseif (isset($fieldConfig['foreign_table'])) {
// use the foreign table
$queryTables = array($fieldConfig['foreign_table']);
}
return $queryTables;
}
/**
* Returns the SQL WHERE clause to use for querying records. This is currently only relevant if a foreign_table
* is configured and should be used; it could e.g. be used to limit to a certain subset of records from the
* foreign table
*
* @param array $fieldConfig
* @param string $usedUids
* @return string
*/
protected function getWhereClause(array $fieldConfig, $usedUids)
{
if(!empty($usedUids)){
return 'AND uid NOT IN ('.$usedUids.')';
} else {
return '';
}
// показываем только те элементы которых нет в уже добавленных
}
/**
* Creates a list of <li> elements from a list of results returned by the receiver.
*
* @param array $resultRows
* @param int $maxItems
* @param string $rowIdSuffix
* @return array
*/
protected function createListItemsFromResultRow(array $resultRows, $maxItems)
{
if (empty($resultRows)) {
return array();
}
$listItems = array();
// traverse all found records and sort them
$rowsSort = array();
foreach ($resultRows as $key => $row) {
$rowsSort[$key] = $row['text'];
}
asort($rowsSort);
$rowsSort = array_keys($rowsSort);
// put together the selector entries
for ($i = 0; $i < $maxItems; ++$i) {
$listItems[] = $resultRows[$rowsSort[$i]];
}
return $listItems;
}
}
Комментировать тут особо нечего, убрал все что ненужно и не используется.
5. Classes\Hooks\InlineElementHook.php
Тут добавим нашу новую кнопку Unchain
PHP код:
<?php
namespace Extension\Hooks;
use TYPO3\CMS\Core\Utility\GeneralUtility;
use TYPO3\CMS\Lang\LanguageService;
use TYPO3\CMS\Core\Imaging\IconFactory;
use TYPO3\CMS\Core\Imaging\Icon;
use TYPO3\CMS\Backend\Form\InlineStackProcessor;
/**
* Inline Element Hook
*
*/
class InlineElementHook implements \TYPO3\CMS\Backend\Form\Element\InlineElementHookInterface
{
/**
* Initializes this hook object.
*
* @param \TYPO3\CMS\Backend\Form\Element\InlineElement $parentObject
* @return void
*/
public function init(&$parentObject)
{
}
/**
* Pre-processing to define which control items are enabled or disabled.
*
* @param string $parentUid The uid of the parent (embedding) record (uid or NEW...)
* @param string $foreignTable The table (foreign_table) we create control-icons for
* @param array $childRecord The current record of that foreign_table
* @param array $childConfig TCA configuration of the current field of the child record
* @param bool $isVirtual Defines whether the current records is only virtually shown and not physically part of the parent record
* @param array &$enabledControls (reference) Associative array with the enabled control items
* @return void
*/
public function renderForeignRecordHeaderControl_preProcess(
$parentUid,
$foreignTable,
array $childRecord,
array $childConfig,
$isVirtual,
array &$enabledControls
) {
}
/**
* Post-processing to define which control items to show. Possibly own icons can be added here.
*
* @param string $parentUid The uid of the parent (embedding) record (uid or NEW...)
* @param string $foreignTable The table (foreign_table) we create control-icons for
* @param array $childRecord The current record of that foreign_table
* @param array $childConfig TCA configuration of the current field of the child record
* @param bool $isVirtual Defines whether the current records is only virtually shown and not physically part of the parent record
* @param array &$cells (reference) Associative array with the currently available control items
* @return void
*/
public function renderForeignRecordHeaderControl_postProcess(
$parentUid,
$foreignTable,
array $childRecord,
array $childConfig,
$isVirtual,
array &$cells
) {
\\ кнопку можно убрать и вернуть старый добрый делит..
if($childConfig['appearance']['enabledControls']['unchain']){
;
$iconFactory = GeneralUtility::makeInstance(IconFactory::class);
$languageService = GeneralUtility::makeInstance(LanguageService::class);
$title = $languageService->sL('LLL:EXT:lang/locallang_core.xlf:labels.remove_selected', true);
$unchain = '
<a class="btn btn-default t3js-editform-unchain-inline-record" href="#" >
' . '<span title="' . $title . '">' . $iconFactory->getIcon('chain-broken', Icon::SIZE_SMALL)->render() . '</span>' . '
</a>';
$cells = array_slice($cells, 0, 0, true) + array('unchain' => $unchain) + array_slice($cells, 0, count($cells), true);
//' . htmlspecialchars($nameObjectFtId) . '
}
}
}
6. Classes\UserFunc\ExetndControls.php
Тут мы срендерим наш инпут поиска
PHP код:
<?php
namespace Extension\UserFunc;
use TYPO3\CMS\Core\Utility\GeneralUtility;
use TYPO3\CMS\Lang\LanguageService;
class ExtendControls {
/**
* Return standard host name for the local machine
*
* @param array $params
* @param array $ref
* @return void
*/
public function searchRelated(array &$params, &$ref){
$languageService = GeneralUtility::makeInstance(LanguageService::class);
$table = $params['table'];
$filedname = $params['nameForm'];
$field = $params['field'];
$row = $params['row'];
$title = $languageService->sL('LLL:EXT:lang/locallang_core.xlf:cm.createNewRelation', true);
$suggestProcessor = GeneralUtility::makeInstance(\ER\ErPlaces\Backend\Form\Wizard\GetRelatedWizard::class);
$suggest = $suggestProcessor->renderSuggestSelector($filedname, $table, $field, $row, $params);
// По умолчанию extended config появится под всеми полями.. так что просто убираем поиск вверх через position:absolute
$html = '<div style="width:300px; position: absolute; right:0; top: 23px; margin-right: 15px;" ' . ($className ? ' class="' . $className . '"' : '') . 'title="' . $title . '">' . $suggest . '</div>';
// скрипты пока не придумал как добавить по нормальному, т.к. &$ref -> resultArray (Protected) и добавить туда что либо из юзерфунка не выйдет.. так что пока вот по глупому все будет копироваться, два инпута два скрипта..
// ! можно конечно напрямую Backend/Classes/Form/Container/InlineControlContainer.php меняем protected $requireJsModules на public и добавляем скрипт.
// $ref->requireJsModules[] = array(
// '../typo3conf/ext/service/Resources/Public/JavaScript/GetRelated' => 'function(GetRelatedInit){GetRelatedInit(".t3-form-suggest-inline")}'
// );
$html .= '
<script type="text/javascript">
/*<![CDATA[*/
require(["../typo3conf/ext/er_places/Resources/Public/JavaScript/GetRelated"], function(GetRelatedInit){GetRelatedInit(".t3-form-suggest-inline")});
/*]]>*/
</script>
';
return $html;
}
}
7. Resources/Public/JavaScript/GetRelated.js
использовать обычный FormEngineSuggest.js не выйдет, у нас другой класс формы и инициализация немного по другому + тут слушаем нашу кнопку Unchain
Код HTML:
define(['jquery', 'jquery/autocomplete'], function ($) {
var GetRelatedInit = function(searchField){
var $searchField = $(searchField);
$.each($searchField, function(key, value){
GetRelated($(value));
})
};
var GetRelated = function($searchField) {
var $containerElement = $searchField.closest('.t3-form-suggest-container');
$searchField.unbind();
var table = $searchField.data('table'),
field = $searchField.data('field'),
uid = $searchField.data('uid'),
uids = $searchField.data('uids'),
pid = $searchField.data('pid'),
newRecordRow = $searchField.data('recorddata'),
minimumCharacters = $searchField.data('minchars'),
url = TYPO3.settings.ajaxUrls['wizard_get_related'],
params = {
'table': table,
'field': field,
'uid': uid,
'pid': pid,
'newRecordRow': newRecordRow,
'uids': uids
};
$searchField.autocomplete({
// ajax options
serviceUrl: url,
params: params,
type: 'POST',
paramName: 'value',
dataType: 'json',
minChars: minimumCharacters,
groupBy: 'typeLabel',
containerClass: 'autocomplete-results',
appendTo: $containerElement,
forceFixPosition: false,
preserveInput: true,
showNoSuggestionNotice: true,
noSuggestionNotice: '<div class="autocomplete-info">No results</div>',
minLength: minimumCharacters,
// put the AJAX results in the right format
transformResult: function(response) {
return {
suggestions: $.map(response, function(dataItem) {
return { value: dataItem.text, data: dataItem };
})
};
},
// Rendering of each item
formatResult: function(suggestion, value) {
console.log(suggestion);
return $('<div>').append(
$('<a class="autocomplete-suggestion-link" href="#">' +
suggestion.data.sprite + suggestion.data.text +
'</a></div>').attr({
'data-label': suggestion.data.label,
'data-table': suggestion.data.table,
'data-uid': suggestion.data.uid
})).html();
},
onSearchComplete: function() {
$containerElement.addClass('open');
},
beforeRender: function(container) {
// Unset height, width and z-index again, should be fixed by the plugin at a later point
container.attr('style', ' width:300px');
$containerElement.addClass('open');
},
onHide: function() {
$containerElement.removeClass('open');
}
});
// set up the events
$containerElement.on('click', '.autocomplete-suggestion-link', function(evt) {
evt.preventDefault();
var insertData = $(this).data('uid'),
objectidPart1 = $(this).parents('.form-group').attr('id'),
table = $(this).data('table'),
objectid = $(this).parents('.form-group').attr('id')+'-'+table;
inline.importElement(objectid, table, insertData, 'db');
// используем import раз уж он есть
});
require(['jquery', 'TYPO3/CMS/Backend/Modal'], function ($, Modal) {
$(document).on('click', '.t3js-editform-unchain-inline-record', function(e) {
e.preventDefault();
var $objectid = $(this).parents('.panel-heading').attr('id').slice(0, -7),
shortName = inline.parseObjectId('parts', $objectid, 2, 0, true);,
title = 'Unchain this record?',
content = 'Are you sure you want to unchain this record?';
var $modal = Modal.confirm(title, content, top.TYPO3.Severity.warning, [
{
text: TYPO3.lang['buttons.confirm.delete_record.no'] || 'Cancel',
active: true,
btnClass: 'btn-default',
name: 'no'
},
{
text: 'Yes, unchain this record',
btnClass: 'btn-warning',
name: 'yes'
}
]);
$modal.on('button.clicked', function(e) {
if (e.target.name === 'no') {
Modal.dismiss();
} else if (e.target.name === 'yes') {
inline.deleteRecord($objectid);
if(document.getElementsByName('cmd' + shortName + '[delete]').length){
$(document.getElementsByName('cmd' + shortName + '[delete]')).attr('disabled', 'disabled');
}
Modal.dismiss();
}
});
});
});
}
return GetRelatedInit;
});
По JS да же не знаю что комментировать, так что если будут вопросы спрашивайте.. а на этом вроде все.
Чуть не забыл, вот пример TCA:
PHP код:
...
'type' => 'inline',
'foreign_table' => 'tx_example_domain_model_object',
'MM' => 'tx_example_domain_model_object_contacts_mm',
// Special Field for additional search
'addrelated' => true,
...
'enabledControls' => array(
// our button UNCHAIN
'unchain' => TRUE,
'delete' => 0,
),
...
P.S.
Мой вам совет, что бы в поиске иконка всплывала нужно в TCA для каждой таблицы указывать
'ctrl' => array (
...
'typeicon_classes' => array(
'default' => 'ext-example-icon'
),
..
)
с добавлением в ext_tables.php