entity_reference_inline-8.x-1.x-dev/src/Plugin/Field/FieldWidget/EntityReferenceInlineWidget.php
src/Plugin/Field/FieldWidget/EntityReferenceInlineWidget.php
<?php
/**
* @file
* Contains \Drupal\entity_reference_inline\Plugin\Field\FieldWidget\EntityReferenceInlineWidget.
*/
namespace Drupal\entity_reference_inline\Plugin\Field\FieldWidget;
use Drupal\Component\Utility\Html;
use Drupal\Component\Utility\NestedArray;
use Drupal\content_translation\ContentTranslationManagerInterface;
use Drupal\content_translation\Controller\ContentTranslationController;
use Drupal\Core\Ajax\AjaxResponse;
use Drupal\Core\Ajax\AppendCommand;
use Drupal\Core\Ajax\InvokeCommand;
use Drupal\Core\Ajax\OpenModalDialogCommand;
use Drupal\Core\Ajax\RemoveCommand;
use Drupal\Core\Ajax\RestripeCommand;
use Drupal\Core\Entity\ContentEntityFormInterface;
use Drupal\Core\Entity\ContentEntityInterface;
use Drupal\Core\Entity\Entity\EntityFormDisplay;
use Drupal\Core\Entity\EntityChangedInterface;
use Drupal\Core\Entity\EntityDisplayRepositoryInterface;
use Drupal\Core\Entity\EntityInterface;
use Drupal\Core\Entity\EntityTypeBundleInfoInterface;
use Drupal\Core\Entity\EntityTypeManagerInterface;
use Drupal\Core\Extension\ModuleHandlerInterface;
use Drupal\Core\Field\FieldDefinitionInterface;
use Drupal\Core\Field\FieldFilteredMarkup;
use Drupal\Core\Field\FieldItemListInterface;
use Drupal\Core\Field\FieldStorageDefinitionInterface;
use Drupal\Core\Field\WidgetBase;
use Drupal\Core\Form\FormState;
use Drupal\Core\Form\FormStateInterface;
use Drupal\Core\Language\LanguageInterface;
use Drupal\Core\Language\LanguageManagerInterface;
use Drupal\Core\Plugin\ContainerFactoryPluginInterface;
use Drupal\Core\Render\Element;
use Drupal\Core\Session\AccountProxyInterface;
use Symfony\Component\DependencyInjection\ContainerInterface;
use \QueryPath;
/**
* Plugin implementation of the 'entity_reference_inline' widget.
*
* @FieldWidget(
* id = "entity_reference_inline",
* label = @Translation("Entity reference inline widget"),
* description = @Translation("An inline entity form of the referenced entities."),
* entity_deep_serialization = TRUE,
* field_types = {
* "entity_reference_inline"
* }
* )
*/
class EntityReferenceInlineWidget extends WidgetBase implements ContainerFactoryPluginInterface {
const NEW_ELEMENT_RETURN_TABLE = 'return_table';
const NEW_ELEMENT_RETURN_SINGLE_ELEMENT = 'return_single_row';
/**
* The language manager.
*
* @var \Drupal\Core\Language\LanguageManagerInterface
*/
protected $languageManager;
/**
* The entity display repository.
*
* @var \Drupal\Core\Entity\EntityDisplayRepositoryInterface
*/
protected $entityDisplayRepository;
/**
* The entity type manager.
*
* @var \Drupal\Core\Entity\EntityTypeManagerInterface
*/
protected $entityTypeManager;
/**
* The entity form displays to be used a referenced entity bundle.
*
* An associative array, keyed by the entity form display id and valued by
* the corresponding entity form display.
*
* @var \Drupal\Core\Entity\Display\EntityFormDisplayInterface[]
*/
protected $referencedEntityFormDisplays;
/**
* The content translation controller.
*
* @var \Drupal\content_translation\Controller\ContentTranslationController
*/
protected $contentTranslationController;
/**
* The content translation manager.
*
* @var \Drupal\content_translation\ContentTranslationManagerInterface
*/
protected $contentTranslationManager;
/**
* The entity type bundle info.
*
* @var \Drupal\Core\Entity\EntityTypeBundleInfoInterface
*/
protected $entityTypeBundleInfo;
/**
* The module handler.
*
* @var \Drupal\Core\Extension\ModuleHandlerInterface
*/
protected $moduleHandler;
/**
* Proxy for the current user account.
*
* @var \Drupal\Core\Session\AccountProxyInterface
*/
protected $currentUser;
/**
* The referenced entity type.
*
* @var \Drupal\Core\Entity\ContentEntityTypeInterface
*/
protected $referencedEntityType;
/**
* A temporary storage of the field items argument of ::extractFormValues().
*
* We set this property in ::extractFormValues() before we call the parent in
* order to able to access the field item list in ::massageFormValues() when
* it is called from within ::extractFormValues() of the parent class.
*
* @var \Drupal\Core\Field\FieldItemListInterface
*/
protected $extractFormValuesFieldItemList;
/**
* A temporary storage for reused entities inside a form.
*
* This property will be used to hold the entity clones when the entity is
* build from the user input. If inside the same form a reused entity occurs
* then it will have at all the places the same entity reference, which will
* be needed by the fields in their preSave to properly update the properties.
*
* @var array
*/
public static $builtEntities = [];
/**
* Constructs a EntityReferenceInlineWidget object.
*
* @param array $plugin_id
* The plugin_id for the widget.
* @param mixed $plugin_definition
* The plugin implementation definition.
* @param \Drupal\Core\Field\FieldDefinitionInterface $field_definition
* The definition of the field to which the widget is associated.
* @param array $settings
* The widget settings.
* @param array $third_party_settings
* Any third party settings.
* @param \Drupal\Core\Language\LanguageManagerInterface $language_manager
* The language manager.
* @param \Drupal\Core\Entity\EntityDisplayRepositoryInterface $entity_display_repository
* The entity display repository.
* @param \Drupal\Core\Entity\EntityTypeManagerInterface $entity_type_manager
* The entity type manager service.
* @param \Drupal\content_translation\Controller\ContentTranslationController
* The content translation controller.
* @param \Drupal\content_translation\ContentTranslationManagerInterface $content_translation_manager
* A content translation manager instance.
* @param \Drupal\Core\Entity\EntityTypeBundleInfoInterface $entity_type_bundle_info
* The entity type bundle info.
* @param \Drupal\Core\Extension\ModuleHandlerInterface $module_handler
* The module handler to invoke the entity reference inline form alter hook with.
* @param \Drupal\Core\Session\AccountProxyInterface $current_user
* The current user account.
*/
public function __construct($plugin_id, $plugin_definition,
FieldDefinitionInterface $field_definition,
array $settings,
array $third_party_settings,
LanguageManagerInterface $language_manager,
EntityDisplayRepositoryInterface $entity_display_repository,
EntityTypeManagerInterface $entity_type_manager,
ContentTranslationController $content_translation_controller,
ContentTranslationManagerInterface $content_translation_manager,
EntityTypeBundleInfoInterface $entity_type_bundle_info,
ModuleHandlerInterface $module_handler,
AccountProxyInterface $current_user) {
parent::__construct($plugin_id, $plugin_definition, $field_definition, $settings, $third_party_settings);
$this->languageManager = $language_manager;
$this->entityDisplayRepository = $entity_display_repository;
$this->entityTypeManager = $entity_type_manager;
$this->contentTranslationController = $content_translation_controller;
$this->contentTranslationManager = $content_translation_manager;
$this->entityTypeBundleInfo = $entity_type_bundle_info;
$this->moduleHandler = $module_handler;
$this->currentUser = $current_user;
}
/**
* {@inheritdoc}
*/
public static function create(ContainerInterface $container, array $configuration, $plugin_id, $plugin_definition) {
return new static(
$plugin_id,
$plugin_definition,
$configuration['field_definition'],
$configuration['settings'],
$configuration['third_party_settings'],
$container->get('language_manager'),
$container->get('entity_display.repository'),
$container->get('entity_type.manager'),
$container->get('entity_reference_inline.content_translation.controller'),
$container->get('content_translation.manager'),
$container->get('entity_type.bundle.info'),
$container->get('module_handler'),
$container->get('current_user')
);
}
/**
* {@inheritdoc}
*/
public static function defaultSettings() {
return [
'form_mode' => 'default',
'form_modes_bundles' => [],
'new_element_return_mode' => static::NEW_ELEMENT_RETURN_SINGLE_ELEMENT
] + parent::defaultSettings();
}
/**
* {@inheritdoc}
*
* The only setting we support at the moment is the form mode, which should be
* used to build the form of the referenced entities.
*/
public function settingsForm(array $form, FormStateInterface $form_state) {
$bundle_options = $this->getBundleOptions();
if (empty($bundle_options)) {
$element['form_mode'] = [
'#type' => 'select',
'#title' => $this->t('Form modes'),
'#default_value' => $this->getSetting('form_mode'),
'#options' => $this->getFormModeOptions(),
'#description' => $this->t('Select the form mode to use for entity representation.'),
];
}
else {
$element['form_modes_bundles'] = [
'#type' => 'fieldset',
'#title' => $this->t('Form modes per bundle'),
'#description' => $this->t('Select the form mode to use for entity representation.'),
];
$form_modes_bundles = $this->getSetting('form_modes_bundles');
foreach ($bundle_options as $bundle_name => $bundle_label) {
$element['form_modes_bundles'][$bundle_name] = [
'#type' => 'select',
'#title' => $this->t('Form modes of bundle "%bundle_label"', ['%bundle_label' => $bundle_label]),
'#default_value' => isset($form_modes_bundles[$bundle_name]) ? $form_modes_bundles[$bundle_name] : 'default',
'#options' => $this->getFormModeOptions($bundle_name),
];
}
}
$element['new_element_return_mode'] = [
'#type' => 'select',
'#title' => $this->t('New element return modes'),
'#default_value' => $this->getSetting('new_element_return_mode'),
'#options' => [
static::NEW_ELEMENT_RETURN_SINGLE_ELEMENT => $this->t('Return and insert a single table row.'),
static::NEW_ELEMENT_RETURN_TABLE => $this->t('Return and replace the whole field table.')
],
'#description' => $this->t('Select new element return mode to use when creating new elements.'),
];
return $element;
}
/**
* {@inheritdoc}
*
* We set the widget state by ourselves to prevent appending an empty item and
* handle the removal of items. Additionally we provide meta information for
* entity_reference_inline_preprocess_field_multiple_value_form.
*/
public function form(FieldItemListInterface $items, array &$form, FormStateInterface $form_state, $get_delta = NULL) {
$field_name = $this->fieldDefinition->getName();
$parents = $form['#parents'];
// Store field information in $form_state.
if (!static::getWidgetState($parents, $field_name, $form_state)) {
$field_state = $this->initializeFieldState($items);
static::setWidgetState($parents, $field_name, $form_state, $field_state);
}
else {
$field_state = static::getWidgetState($parents, $field_name, $form_state);
}
$widget_form = parent::form($items, $form, $form_state, $get_delta);
// Remove field items, removed by the ajax callback "Remove".
foreach (array_keys($field_state['deltas_removed']) as $delta) {
unset($widget_form['widget'][$delta]);
}
$number_of_rows = $items->count() - count($field_state['deltas_removed']);
$last_delta = $items->count() - 1;
// Needed only for the remove button functionality.
if ($this->isCardinalityUnlimited()) {
$table_id = $this->getEntityReferenceFieldTableId($form);
$widget_form['widget'] += [
'#base_widget' => 'entity_reference_inline',
'#form_parents' => implode('-', $parents),
'#is_cardinality_unlimited' => TRUE,
'#table_id' => $table_id
];
$widget_form['widget']['add_more']['add_more']['#ajax']['table_id'] = $table_id;
$widget_form['widget']['add_more']['add_more']['#ajax']['number_of_rows'] = $number_of_rows;
$widget_form['widget']['add_more']['add_more']['#ajax']['last_delta'] = $last_delta;
}
$widget_form['#attached']['library'][] = 'entity_reference_inline/base-theme';
return $widget_form;
}
/**
* Prepares a table id based on the parents and the field name.
*
* @param $form
* The entity form.
*
* @return string
* A clean css identifier for the field table.
*/
protected function getEntityReferenceFieldTableId($form) {
$table_id_parts = $form['#parents'];
$table_id_parts[] = $this->fieldDefinition->getName();
$table_id_parts[] = 'entity-reference-inline-field-table';
return Html::cleanCssIdentifier(implode('-', $table_id_parts));
}
/**
* Prepares the initial field state.
*
* @param \Drupal\Core\Field\FieldItemListInterface $items
* An array of the field values. When creating a new entity this may be NULL
* or an empty array to use default values.
*
* @return array
* The initial field state.
*/
protected function initializeFieldState(FieldItemListInterface $items) {
return [
// Do not add an empty field item.
'items_count' => count($items) - 1,
'array_parents' => [],
'deltas_removed' => [],
'initial_delta_values' => [],
'new_element_return_mode' => $this->getSetting('new_element_return_mode'),
];
}
/**
* {@inheritdoc}
*/
protected function formMultipleElements(FieldItemListInterface $items, array &$form, FormStateInterface $form_state) {
$field_name = $this->fieldDefinition->getName();
$parents = $form['#parents'];
$cardinality = $this->fieldDefinition->getFieldStorageDefinition()->getCardinality();
$field_state = static::getWidgetState($parents, $field_name, $form_state);
// If initial_delta_values is emtpty then the addMoreSubmit is not the
// triggering element.
if ($field_state['initial_delta_values']) {
// Determine the number of widgets to display.
$max = $cardinality == FieldStorageDefinitionInterface::CARDINALITY_UNLIMITED ? $field_state['items_count'] : $cardinality - 1;
for ($delta = 0; $delta <= $max; $delta++) {
if (!isset($items[$delta])) {
$initial_delta_values = isset($field_state['initial_delta_values'][$delta]) ? $field_state['initial_delta_values'][$delta] : [];
// Add the current form language code as the language of the new
// entity being created.
if ($this->getReferencedEntityType()->isTranslatable() && ($langcode_key = $this->getReferencedEntityType()->getKey('langcode'))) {
if (!isset($initial_delta_values[$langcode_key]) && ($form_langcode = $form_state->getFormObject()->getFormLangcode($form_state))) {
$initial_delta_values[$langcode_key] = $form_langcode;
}
}
$initial_delta_values = ['entity' => $this->getReferencedEntityStorage()->create($initial_delta_values)];
$items->appendItem($initial_delta_values);
}
}
}
$elements = parent::formMultipleElements($items, $form, $form_state);
// We do not use unique id because this wrapper should not change on each
// ajax call because it is used in addMoreAjax as a selector and if it
// changes on each ajax call we we'll have the new name but the DOM will
// still have the old one and we'll not be able to address it with the
// returned ajax commands.
$id_prefix = implode('-', array_merge($parents, [$field_name]));
if (empty($elements)) {
$title = $this->fieldDefinition->getLabel();
$description = FieldFilteredMarkup::create(\Drupal::token()->replace($this->fieldDefinition->getDescription()));
$elements += [
'#theme' => 'field_multiple_value_form',
'#field_name' => $field_name,
'#cardinality' => $cardinality,
'#cardinality_multiple' => $this->fieldDefinition->getFieldStorageDefinition()->isMultiple(),
'#required' => $this->fieldDefinition->isRequired(),
'#title' => $title,
'#description' => $description,
// max delta is needed in ::addMoreAjax and if there no elements at the
// moment we want it to be < 0 as 0 means the first element.
'#max_delta' => -1,
];
// Add 'add more' button, if not working with a programmed form.
if ($cardinality == FieldStorageDefinitionInterface::CARDINALITY_UNLIMITED && !$form_state->isProgrammed()) {
$wrapper_id = Html::getUniqueId($id_prefix . '-add-more-wrapper');
$elements['#prefix'] = '<div id="' . $wrapper_id . '">';
$elements['#suffix'] = '</div>';
$elements['add_more'] = [
'#type' => 'submit',
'#name' => strtr($id_prefix, '-', '_') . '_add_more',
'#value' => $this->t('Add another item'),
'#attributes' => ['class' => ['field-add-more-submit']],
'#limit_validation_errors' => [array_merge($form['#parents'], [$field_name])],
'#validate' => [],
'#submit' => [[get_class($this), 'addMoreSubmit']],
'#ajax' => [
'callback' => [get_class($this), 'addMoreAjax'],
'wrapper' => $wrapper_id,
],
];
}
}
if (isset($elements['add_more'])) {
$field_name_css = Html::cleanCssIdentifier($this->fieldDefinition->getName());
$specific_class = "entity-reference-inline-add-more-container-{$field_name_css}";
// Because of the theme for fields we could only use the add_more key to
// add the bundle to it and therefore in order for the bundle list to be
// in front of the add_more button we have to place them in a container
// and put the bundle list before the add_more key.
$elements['add_more'] = [
'#type' => 'container',
'add_more_bundle' => [],
'add_more' => $elements['add_more'],
'#attributes' => ['class' => ['entity-reference-inline-add-more-container', $specific_class]],
];
$elements['add_more']['add_more']['#attributes']['class'][] = 'field-add-more-submit-' . strtr($field_name, ['_' => '-']);
$elements['add_more']['add_more']['#attributes']['class'][] = 'entity-reference-inline-add-more-submit';
// We have to place the bundle select under the 'add_more' key because
// otherwise the template function will (try to) convert it to a table
// row and will fail as the #row_id is not set on this element.
// @see template_preprocess_field_multiple_value_form()
// @see entity_reference_inline_preprocess_field_multiple_value_form()
$this->addBundleOptions($elements['add_more']);
}
if ($this->getSetting('new_element_return_mode') == static::NEW_ELEMENT_RETURN_SINGLE_ELEMENT) {
// We do not use the replace command but append our elements directly in
// the table, so there is no need for adding a prefix and defining a
// wrapper.
unset($elements['add_more']['#ajax']['wrapper']);
unset($elements['#prefix']);
unset($elements['#suffix']);
}
$elements['add_more']['#ajax']['effect'] = 'slide';
// Adding an empty array as #validate so that
// ContentEntityForm::validateForm is not executed, which if executed will
// call ContentEntityForm::buildEntity and for each field's widget
// WidgetBase::extractFormValues will be called so that the parent entity
// is build from the user input and then the entity is validated. However
// if the form gets really big validation in ajax calls might only slow
// down the system. Therefor we turn of the validation for this kind of
// ajax calls, as the whole form and the entity will be validated when
// submitting the form.
if (!isset($elements['add_more']['#validate'])) {
// $elements['add_more']['#validate'] = [];
}
// Invoke the entity reference inline form_multiple_elements alter hooks.
$context = [
'items' => $items,
];
$hooks = [
'entity_reference_inline_form_multiple_elements',
'entity_reference_inline_' . $this->getReferencedEntityType()->id() . '_form_multiple_elements',
'entity_reference_inline_' . $this->getReferencedEntityType()->id() . '_' . $this->getSetting('form_mode') . '_form_multiple_elements',
];
$this->moduleHandler->alter($hooks, $elements, $form_state, $context);
return $elements;
}
/**
* Sets available bundle options to the add more element.
*/
protected function addBundleOptions(&$element) {
if ($bundle_field_name = $this->getReferencedEntityType()->getKey('bundle')) {
$bundle_options = $this->getBundleOptions();
// Only show a select list if there is more than one bundle configured
// to be used on this field.
if (count($bundle_options) > 1) {
// Using the bundle field name as a key we do not need to add extra
// handling for the bundle field when creating the entity from the
// submitted form values.
$entity_type_label = $this->getReferencedEntityType()->getLabel();
$field_name_css = Html::cleanCssIdentifier($this->fieldDefinition->getName());
$specific_class = "entity-reference-inline-add-more-bundle-{$field_name_css}";
$element['add_more_bundle'] = [
'#type' => 'container',
'#attributes' => ['class' => ['entity-reference-inline-add-more-submit-bundle', $specific_class]],
$bundle_field_name => [
'#type' => 'select',
'#options' => $bundle_options,
'#empty_option' => $this->t('- Select element type to add -'),
'#entity_type_label' => $entity_type_label,
],
];
$element['add_more']['#validate'][] = [static::class, 'addMoreValidateBundle'];
}
else {
// We have to reset the array in order to access any key.
reset($bundle_options);
// We do not need a select list if only one bundle is available.
$element['add_more_bundle'] = [
'#type' => 'container',
$bundle_field_name => [
'#type' => 'value',
'#value' => key($bundle_options),
],
];
}
$element['add_more']['#bundle_field_name'] = $bundle_field_name;
$element['add_more']['#allowed_bundles'] = $bundle_options;
}
}
/**
* Returns the bundle options.
*
* @return array
* The bundle options, keyed by the bundle machine name, valued by the
* bundle label.
*/
protected function getBundleOptions() {
$bundle_options = [];
if ($this->getReferencedEntityType()->hasKey('bundle') && ($handler_settings = $this->fieldDefinition->getSetting('handler_settings')) && !empty($handler_settings['target_bundles'])) {
// Prepare the bundle options with labels.
$available_bundles = $this->entityTypeBundleInfo->getBundleInfo($this->getFieldSetting('target_type'));
// We use array flip in case a base field definition is not using for
// key and value the referenced bundle but only for value.
$bundle_options = array_intersect_key($available_bundles, array_flip($handler_settings['target_bundles']));
array_walk($bundle_options, function (&$bundle_info) {
$bundle_info = $bundle_info['label'];
});
}
return $bundle_options;
}
/**
* Validates that when adding a new entity a bundle is selected.
*
* @param array $form
* The form array.
* @param \Drupal\Core\Form\FormStateInterface $form_state
* The current state of the form.
*/
public static function addMoreValidateBundle(array $form, FormStateInterface $form_state) {
$triggering_element = $form_state->getTriggeringElement();
$parents = $triggering_element['#parents'];
array_pop($parents);
$parents[] = 'add_more_bundle';
$parents[] = $triggering_element['#bundle_field_name'];
$selected_bundle = $form_state->getValue($parents);
if (!empty($selected_bundle)) {
$name = array_shift($parents) . '[' . implode('][', $parents) . ']';
$form_state->setTemporaryValue('entity_reference_inline_reset_bundle_select', $name);
}
else {
$array_parents = $triggering_element['#array_parents'];
array_pop($array_parents);
$array_parents[] = 'add_more_bundle';
$array_parents[] = $triggering_element['#bundle_field_name'];
$add_more_bundle_element = NestedArray::getValue($form, $array_parents);
$form_state->setError($add_more_bundle_element, t('Please select the type of the %entity_type_label to add.', ['%entity_type_label' => $add_more_bundle_element['#entity_type_label']]));
}
}
/**
* {@inheritdoc}
*
* If the element at this delta has been removed by an ajax call, there is no
* need for it to be completely initialized and as a performance optimization
* we return a dummy element which is going to be removed in ::form before
* the widget form is returned.
*
* Note: If it is needed to extend from the widget and to alter the form
* element of an entity then ::formElement() should be overridden and not
* ::formSingleElement() in order for the changes to be available in the hook
* invocations.
*/
protected function formSingleElement(FieldItemListInterface $items, $delta, array $element, array &$form, FormStateInterface $form_state) {
$field_state = static::getWidgetState($form['#parents'], $this->fieldDefinition->getName(), $form_state);
if (isset($field_state['deltas_removed'][$delta])) {
$element = ['#type' => 'dummy'];
}
else {
$element = parent::formSingleElement($items, $delta, $element, $form, $form_state);
if ($this->getReferencedEntityType()->isTranslatable()) {
$referenced_entity = $items[$delta]->entity;
// If the entity is new it should be already in the correct translation!
$referenced_entity = $referenced_entity->isNew() ? $referenced_entity : $this->prepareTranslation($referenced_entity, $delta, $form['#parents'], $form_state);
}
else {
$referenced_entity = $items[$delta]->entity;
}
$form_display = $this->getFormDisplay($referenced_entity->getEntityTypeId(), $referenced_entity->bundle());
// Invoke the entity reference inline form alter hooks.
$context = [
'entity' => $referenced_entity,
'form_display' => $form_display,
'parent_item' => $items[$delta],
'wrapped_entity_form' => &$element
];
$hooks = [
'entity_reference_inline_form',
'entity_reference_inline_' . $referenced_entity->getEntityTypeId() . '_form',
'entity_reference_inline_' . $referenced_entity->getEntityTypeId() . '_' . $this->getSetting('form_mode') . '_form',
];
$this->moduleHandler->alter($hooks, $element['details_element'], $form_state, $context);
}
return $element;
}
/**
* Checks if the provided element is only a dummy one, an replacement for
* a removed delta element.
*
* @param array $element
* The element for which to check if it is removed.
*
* @return bool
* TRUE, if the element is removed, FALSE otherwise.
*/
protected static function isElementRemoved(array $element) {
return isset($element['#type']) && ($element['#type'] == 'dummy');
}
/**
* {@inheritdoc}
*/
public function formElement(FieldItemListInterface $items, $delta, array $element, array &$form, FormStateInterface $form_state) {
if ($this->getReferencedEntityType()->isTranslatable()) {
$referenced_entity = $items[$delta]->entity;
// If the entity is new it should be already in the correct translation!
$referenced_entity = $referenced_entity->isNew() ? $referenced_entity : $this->prepareTranslation($referenced_entity, $delta, $form['#parents'], $form_state);
}
else {
$referenced_entity = $items[$delta]->entity;
}
// The parents are not set by the parent class when this function is called,
// but EntityFormDisplay::buildForm will set them to an empty array if not
// already present. In order to not break the structure by calling
// EntityFormDisplay we have to ensure the parents are set correctly.
$element['#parents'] = array_merge($form['#parents'], [$this->fieldDefinition->getName(), $delta]);
$form_display = $this->getFormDisplay($referenced_entity->getEntityTypeId(), $referenced_entity->bundle());
$form_display->buildForm($referenced_entity, $element, $form_state);
if ($referenced_entity->isNew()) {
$entity_type = $referenced_entity->getEntityType();
if ($bundle_field_name = $entity_type->getKey('bundle')) {
$element[$bundle_field_name] = [
'#type' => 'value',
'#value' => $referenced_entity->bundle(),
];
}
}
if ($referenced_entity instanceof EntityChangedInterface) {
// Changed must be sent to the client, for later overwrite error checking.
// TODO find a better way to include the changed timestamp.
$element['changed'] = [
'#type' => 'hidden',
'#default_value' => $referenced_entity->getChangedTime(),
];
}
$this->addEntityMetaInformation($referenced_entity, $element);
// Add attributes to the form element.
$attributes = [
'class' => [
'entity-reference-inline-details',
'entity-reference-inline-' . str_replace('_', '-', $referenced_entity->getEntityTypeId()),
'entity-reference-inline-' . str_replace('_', '-', $referenced_entity->getEntityTypeId() . '--' . $referenced_entity->bundle()),
],
];
$entity_id = $referenced_entity->id();
if ($entity_id) {
$attributes['entity-reference-inline-id'] = $entity_id;
}
// Wrap the entity form into details for a better structure of the form.
$wrapped_entity_form = [
'#type' => 'details',
'#title' => $referenced_entity->label(),
'#open' => TRUE,
'#attributes' => $attributes,
'details_element' => &$element,
];
$this->addRemoveButton($wrapped_entity_form, $form);
return $wrapped_entity_form;
}
/**
* {@inheritdoc}
*/
public static function addMoreSubmit(array $form, FormStateInterface $form_state) {
// We don't call the parent as the add more button is wrapped in a
// container and widget element is two levels up instead of one.
$button = $form_state->getTriggeringElement();
// The widget form with all the elements.
// Go two levels up in the form, to the widgets container.
$elements = NestedArray::getValue($form, array_slice($button['#array_parents'], 0, -2));
// ----- parent::addMoreSubmit ---------
$field_name = $elements['#field_name'];
$parents = $elements['#field_parents'];
// Increment the items count.
$field_state = static::getWidgetState($parents, $field_name, $form_state);
$field_state['items_count']++;
#static::setWidgetState($parents, $field_name, $form_state, $field_state);
$form_state->setRebuild();
// ----- parent::addMoreSubmit ---------
if (isset($elements['add_more']['add_more_bundle']['#parents'])) {
$field_state['initial_delta_values'][$field_state['items_count']] = $form_state->getValue($elements['add_more']['add_more_bundle']['#parents']);
}
static::setWidgetState($parents, $field_name, $form_state, $field_state);
// Store the previous options which will be used in ::addMoreAjax.
static::addMorePreserveTemporaryWeightElementPreviousOptions($elements, $form_state);
}
/**
* Retrieves and stores the previous weight options into the form state.
*
* A helper method for ::addMoreSubmit which output will be used later in
* ::addMoreAjax.
*
* @param $elements
* The elements of which to estimate the weight options.
* @param \Drupal\Core\Form\FormStateInterface $form_state
* The current state of the form.
*/
protected static function addMorePreserveTemporaryWeightElementPreviousOptions($elements, FormStateInterface $form_state) {
// Store the previous options which will be used in ::addMoreAjax.
$weight_element_previous_options = [];
$form_state->set('weight_element_previous_options', []);
foreach ($elements as $key => $child) {
if (Element::child($key)) {
if (!static::isElementRemoved($elements[$key]) && isset($elements[$key]['_weight']['#type']) && $elements[$key]['_weight']['#type'] == 'select') {
$form_state->setTemporaryValue('weight_element_previous_options', $elements[$key]['_weight']['#options']);
$weight_element_previous_options = $elements[$key]['_weight']['#options'];
break;
}
}
}
$form_state->setTemporaryValue('weight_element_previous_options', $weight_element_previous_options);
}
/**
* {@inheritdoc}
*/
public static function addMoreAjax(array $form, FormStateInterface $form_state) {
$button = $form_state->getTriggeringElement();
// Go two levels up in the form, to the widgets container.
$elements = NestedArray::getValue($form, array_slice($button['#array_parents'], 0, -2));
$field_name = $elements['#field_name'];
$parents = $elements['#field_parents'];
$return_single_row = static::getNewElementReturnMode($parents, $field_name, $form_state) == static::NEW_ELEMENT_RETURN_SINGLE_ELEMENT;
if ($return_single_row) {
$response = new AjaxResponse();
// If any errors occurred return them without replacing the element as it
// might have not been properly initialized.
if (static::addFormErrorsToAjaxResponse($response, $form_state)) {
return $response;
}
$button = $form_state->getTriggeringElement();
$table_id = '#' . $button['#ajax']['table_id'];
$number_of_rows = $button['#ajax']['number_of_rows'];
// Core replaces hole table and set ajax-new-content class only on new
// content. Here only the new row is added and the added row still has
// the class ajax-new-content, which needs to be removed now as its not
// new anymore.
$response->addCommand(new InvokeCommand($table_id . ' .ajax-new-content', 'removeClass', ['ajax-new-content']));
// Add a DIV around the delta receifving the Ajax effect.
$delta = $elements['#max_delta'];
// A storage for the weight elements that have to be updated in case a
// select element is used instead of a number element, which is decided
// by Drupal\Core\Render\Element\Weight::processWeight.
$_weight_elements = [];
// Render as less as possible!
if ($number_of_rows > 1) {
foreach ($elements as $key => $child) {
if (Element::child($key) && ($key != $delta)) {
if (!static::isElementRemoved($elements[$key]) && isset($elements[$key]['_weight']['#type']) && ($elements[$key]['_weight']['#type'] == 'select')) {
$_weight_elements[$key] = $elements[$key]['_weight'];
unset($elements[$key]);
}
else {
unset($elements[$key]);
}
}
}
}
// Update the weight elements of the other rows if they are rendered as
// select instead as number elements. We do insert the new options
// instead of replacing only the previous weight elements as by doing so
// and cutting them out through query path might get extremely slow with
// a big html.
// Updating the weight is necessary in order for tabledrag.js to work
// properly after a new element is inserted and moved around.
$new_options = array_diff_assoc($elements[$delta]['_weight']['#options'], $form_state->getTemporaryValue('weight_element_previous_options'));
$form_state->setTemporaryValue('weight_element_previous_options', []);
array_walk($new_options, function (&$value, $key) {$value = '<option value="' . $key . '">' . $value . '</option>';});
foreach ($_weight_elements as $_weight_element) {
foreach ($new_options as $new_option) {
$response->addCommand(new AppendCommand('[name="' . $_weight_element['#name'] . '"]', $new_option));
}
}
// Needed by entity_reference_inline_preprocess_field_multiple_value_form.
$elements[$delta]['#new_ajax_row'] = TRUE;
// The library is needed only if a new element has been added to make its
// row draggable.
$elements['#attached']['library'][] = 'entity_reference_inline/entity-reference-inline-make-draggable';
// Now after we've minimized the content to be rendered we can render.
$html = (string) static::drupalRenderRoot($elements);
// If tbody already exists we just return the rendered row, otherwise we
// have to append the tbody in the first command and in the second command
// the row, so that no matter if it is the first row in the table or
// following one we get in attach behaviors as context always just a row
// and not e.g. "table > tbody > tr" or "tbody > tr".
// Get only the last row.
$last_table_row = QueryPath::withHTML5($html, $table_id . ' > tbody > tr:last')->html();
// If the last delta is bigger than 0 it means tbody is already present.
if ($delta > 0) {
$response->addCommand(new AppendCommand($table_id . ' > tbody', $last_table_row));
}
else {
// Two possibilities:
// 1. Insert tbody along with the first row in one command
// $tbody = qp($dom, $table_id_jquery_selector . ' > tbody')->html();
// $response->addCommand(new AppendCommand($table_id_jquery_selector, $tbody));
// 2. Insert first tbody and after that the row
// This unifies the insertion of rows and listening to changed context in
// js where with this implementation the changed context for each inserted
// row now will be the row and not like in option 1 at inserting the first
// row the tbody.
$response->addCommand(new AppendCommand($table_id, '<tbody></tbody>'));
$response->addCommand(new AppendCommand($table_id . ' > tbody', $last_table_row));
}
// Restripe the table.
$response->addCommand(new RestripeCommand($table_id));
// Add the attachments e.g. if we haven't initialized ckeditor yet it's
// library will be returned.
$response->setAttachments(isset($elements['#attached']) ? $elements['#attached'] : []);
// Reset selected bundle.
$identifier = "[name=\"{$form_state->getTemporaryValue('entity_reference_inline_reset_bundle_select')}\"]";
$response->addCommand(new InvokeCommand($identifier, 'prop', array('selectedIndex', 0)));
return $response;
}
else {
// ----- parent::addMoreAjax ---------
// Ensure the widget allows adding additional items.
if ($elements['#cardinality'] != FieldStorageDefinitionInterface::CARDINALITY_UNLIMITED) {
return;
}
// Add a DIV around the delta receiving the Ajax effect.
$delta = $elements['#max_delta'];
$elements[$delta]['#prefix'] = '<div class="ajax-new-content">' . (isset($elements[$delta]['#prefix']) ? $elements[$delta]['#prefix'] : '');
$elements[$delta]['#suffix'] = (isset($elements[$delta]['#suffix']) ? $elements[$delta]['#suffix'] : '') . '</div>';
return $elements;
// ----- parent::addMoreAjax ---------
}
}
/**
* Wraps the renderRoot function of the renderer service.
*/
protected static function drupalRenderRoot(&$elements) {
return \Drupal::service('renderer')->renderRoot($elements);
}
/**
* Adds status messages to the ajax response if any errors occurred.
*
* @param \Drupal\Core\Ajax\AjaxResponse $response
* The ajax response.
* @param \Drupal\Core\Form\FormStateInterface $form_state
* The form state.
*
* @return bool
* Returns TRUE if the form state contains errors and they have been added
* to the ajax response, FALSE otherwise.
*/
protected static function addFormErrorsToAjaxResponse(AjaxResponse $response, FormStateInterface $form_state) {
if ($errors = $form_state->getErrors()) {
$display = '';
$status_messages = ['#type' => 'status_messages'];
if ($messages = \Drupal::service('renderer')->renderRoot($status_messages)) {
$display = '<div class="views-messages">' . $messages . '</div>';
}
$options = [
'dialogClass' => 'views-ui-dialog',
'width' => '50%',
];
// Attach the library necessary for using the OpenModalDialogCommand and
// set the attachments for this Ajax response.
$status_messages['#attached']['library'][] = 'core/drupal.dialog.ajax';
$response->setAttachments($status_messages['#attached']);
$response->addCommand(new OpenModalDialogCommand(t('Error Messages'), $display, $options));
return TRUE;
}
return FALSE;
}
/**
* Prepares the translation for the referenced entity, in case of being on a
* entity translate page the target translation will added to the entity if
* not yet present, otherwise the target entity translation will be returned.
*
* @param \Drupal\Core\Entity\ContentEntityInterface $referenced_entity
* @param int $delta
* The order of this item in the array of sub-elements (0, 1, 2, etc.).
* @param \Drupal\Core\Form\FormStateInterface $form_state
*
* @return \Drupal\Core\Entity\ContentEntityInterface
*/
protected function prepareTranslation(ContentEntityInterface $referenced_entity, $delta, $parents, FormStateInterface $form_state) {
// Add translation page.
if (($source_language = $form_state->get(['content_translation', 'source'])) && ($target_language = $form_state->get(['content_translation', 'target']))) {
$src_langcode = $source_language->getId();
$target_langcode = $target_language->getId();
if ($referenced_entity->hasTranslation($target_langcode)) {
return $referenced_entity->getTranslation($target_langcode);
}
else {
// If the referenced entity does not have the source language we are
// translating the main entity from then use its current language as
// source.
if (!$referenced_entity->hasTranslation($src_langcode)) {
$source_language = $this->getTranslationSourceLanguage($referenced_entity, $delta, $parents, $form_state);
$src_langcode = $source_language->getId();
}
// Checks whether the entity is enabled for content translation.
if ($this->contentTranslationManager->isEnabled($referenced_entity->getEntityTypeId(), $referenced_entity->bundle())) {
$this->contentTranslationController->prepareTranslation($referenced_entity, $source_language, $target_language);
$translation = $referenced_entity->getTranslation($target_language->getId());
$metadata = $this->contentTranslationManager->getTranslationMetadata($translation);
$metadata->setSource($src_langcode);
return $translation;
}
else {
return $this->translateEntity($referenced_entity, $src_langcode, $target_langcode);
}
}
}
// Target langcode is the langcode of the entity being displayed at the
// moment. It might be as well the target translation if the entity is being
// translated at the moment. We retrieve the language from the from object
// as if the entity reference field is not translatable the parent entity
// will be loaded for the current field in its default language even if
// the parent entity form is shown in a different language.
$form_lang_code = $form_state->getFormObject()->getFormLangcode($form_state);
$target_langcode = $form_lang_code;
if ($referenced_entity->hasTranslation($target_langcode)) {
return $referenced_entity->getTranslation($target_langcode);
}
else {
$source_language = $this->getTranslationSourceLanguage($referenced_entity, $delta, $parents, $form_state);
// Checks whether the entity is enabled for content translation.
if ($this->contentTranslationManager->isEnabled($referenced_entity->getEntityTypeId(), $referenced_entity->bundle())) {
$target_language = $this->languageManager->getLanguage($target_langcode);
$this->contentTranslationController->prepareTranslation($referenced_entity, $source_language, $target_language);
$translation = $referenced_entity->getTranslation($target_language->getId());
$metadata = $this->contentTranslationManager->getTranslationMetadata($translation);
$metadata->setSource($source_language->getId());
return $translation;
}
else {
return $this->translateEntity($referenced_entity, $source_language->getId(), $target_langcode);
}
}
}
/**
* Determines the source language from which the entity will be translated.
*
* A helper method for ::prepareTranslation.
*
* @param \Drupal\Core\Entity\ContentEntityInterface $referenced_entity
* @param $delta
* @param $parents
* @param \Drupal\Core\Form\FormStateInterface $form_state
* @return LanguageInterface
*/
protected function getTranslationSourceLanguage(ContentEntityInterface $referenced_entity, $delta, $parents, FormStateInterface $form_state) {
return $referenced_entity->language();
}
/**
* Translates an entity.
*
* @param \Drupal\Core\Entity\ContentEntityInterface $entity
* The entity to translate.
* @param $src_langcode
* The source language code.
* @param $target_langcode
* The target language code.
*
* @return \Drupal\Core\Entity\ContentEntityInterface
* The translated entity.
*/
protected function translateEntity(ContentEntityInterface $entity, $src_langcode, $target_langcode) {
$source_translation = $entity->getTranslation($src_langcode);
$target_translation = $entity->addTranslation($target_langcode, $source_translation->toArray());
// TODO find a better way.
if (method_exists($target_translation, 'setCreatedTime')) {
$target_translation->setCreatedTime(REQUEST_TIME);
}
// TODO find a better way.
if (method_exists($target_translation, 'setAuthor')) {
/** @var \Drupal\user\UserInterface $user */
$user = $this->entityTypeManager->getStorage('user')->load($this->currentUser->id());
$target_translation->setAuthor($user);
}
// Make sure we do not inherit the affected status from the source values.
if ($target_translation->getEntityType()->isRevisionable()) {
$target_translation->setRevisionTranslationAffected(NULL);
}
return $target_translation;
}
/**
* Adds the remove button to the given form element.
*
* @param array $element
* @param array $form
*/
protected function addRemoveButton(array &$element, array $form) {
if ($this->isCardinalityUnlimited()) {
$field_name = $this->fieldDefinition->getName();
$parents = $element['details_element']['#parents'];
$id_parts = $parents;
$id_parts[] = 'entity-reference-inline-row';
$id_prefix = Html::cleanCssIdentifier(implode('-', $parents));
$element['#row_id'] = $id_prefix;
$element['remove'] = [
'#type' => 'submit',
'#name' => $id_prefix . '-remove',
'#value' => $this->t('Remove this item'),
'#attributes' => ['class' => ['field-remove-submit', 'field-remove-submit-' . strtr($field_name, ['_' => '-'])]],
'#limit_validation_errors' => [array_merge($parents, [$field_name])],
'#submit' => [[get_class($this), 'removeSubmit']],
'#ajax' => [
'callback' => [get_class($this), 'removeAjax'],
'effect' => 'fade',
'row_id' => $element['#row_id'],
'table_id' => $this->getEntityReferenceFieldTableId($form),
],
'#validate' => [],
'#weight' => 1000,
];
}
}
/**
* Checks whether the cardinality of the field is unlimited.
*
* @return bool
*/
protected function isCardinalityUnlimited() {
$cardinality = $this->fieldDefinition->getFieldStorageDefinition()->getCardinality();
return $cardinality == FieldStorageDefinitionInterface::CARDINALITY_UNLIMITED;
}
/**
* Adds entity meta information, which will be later used by
* ::loadEntityFromMetaInformation in ::massageFormValues to load the entity.
*
* @param \Drupal\Core\Entity\ContentEntityInterface $entity
* @param $element
*/
protected function addEntityMetaInformation(ContentEntityInterface $entity, &$element) {
$id_field_name = $entity->getEntityType()->getKey('id');
$element[$id_field_name] = [
'#type' => 'hidden',
'#value' => $entity->id()
];
}
/**
* Loads the entity from the given meta information as added by
* ::addEntityMetaInformation.
*
* @param $values
*
* @return \Drupal\Core\Entity\ContentEntityInterface|null
*/
protected function loadEntityFromMetaInformation($values) {
$id_field_name = $this->getReferencedEntityType()->getKey('id');
$entity = NULL;
if (isset($values[$id_field_name])) {
$entity = $this->getReferencedEntityStorage()->load($values[$id_field_name]);
}
return $entity;
}
/**
* {@inheritdoc}
*/
public function extractFormValues(FieldItemListInterface $items, array $form, FormStateInterface $form_state) {
// Allow access to the items for ::massageFormValues().
$this->extractFormValuesFieldItemList = $items;
parent::extractFormValues($items, $form, $form_state);
$this->extractFormValuesFieldItemList = NULL;
// Flag the parent entity with the language for which we have edited the
// entity in order to check only this language for translation changes on
// save in the pre-save of the field item.
// @see \Drupal\entity_reference_inline\Plugin\Field\FieldType\EntityReferenceInlineItem::preSave()
$parent = $items->getEntity();
// This method is called also when we are on the field config form, so we
// have to explicitly check that we are on a content entity form.
if (!isset($parent->inlineEditedLangcode) && ($form_object = $form_state->getFormObject()) && $form_object instanceof ContentEntityFormInterface) {
$parent->inlineEditedLangcode = $form_object->getFormLangcode($form_state);
}
}
/**
* {@inheritdoc}
*
* We extract the information from the submitted values needed to rebuild the
* referenced entities and then return the newly built entities instead of
* the values of their fields.
*/
public function massageFormValues(array $values, array $form, FormStateInterface $form_state) {
$form_langcode = $form_state->getFormObject()->getFormLangcode($form_state);
foreach ($values as $delta => &$delta_values) {
if (isset($delta_values['remove'])) {
unset($delta_values['remove']);
}
$original_delta = $delta_values['_original_delta'];
// Retrieve the sub-form.
if (isset($form[$this->fieldDefinition->getName()])) {
$element_form = &$form[$this->fieldDefinition->getName()]['widget'][$original_delta];
}
// The field name will not be present in the form structure on the field
// settings for its default value.
elseif (isset($form['widget'])) {
$element_form = &$form['widget'][$original_delta];
}
$element_form = $element_form['details_element'];
// Load the entity.
if (isset($this->extractFormValuesFieldItemList)) {
$entity = $this->extractFormValuesFieldItemList[$original_delta]->entity;
}
else {
// This should never happen.
@trigger_error('The entity should be loaded from the field items instead from the meta information.', E_USER_DEPRECATED);
$entity = $this->loadEntityFromMetaInformation($delta_values);
}
// ContentEntityForm::buildEntity is cloning the main form entity and
// setting the submitted values on the cloned entity, so that the
// original one is not altered and on form rebuild the form is rebuild
// based on the form values and new field items are added. Entity
// references are not cloned and this is why we clone the entity here
// before setting it on the cloned field, this way the whole structure
// will be cloned.
if (!$entity->isNew()) {
// We have to ensure that if an entity is reused inside the form state
// that at all the places we'll be using the same entity object
// reference instead of having different object references with the same
// values mapped. This widget doesn't take care of ensuring that a
// reused entity will be assigned the same form values - this has to be
// taken care of from the widget extending from the current one and
// offering this ability.
$entity_type_id = $entity->getEntityTypeId();
$entity_id = $entity->id();
if (!$this->getBuiltEntity($entity_type_id, $entity_id)) {
$this->setBuiltEntity(clone $entity);
}
$entity = $this->getBuiltEntity($entity_type_id, $entity_id);
}
else {
$entity = clone $entity;
}
// Process entity translation and language.
if ($this->getReferencedEntityType()->isTranslatable()) {
// If we've created a new entity with a previous form language code and
// now the form language code is changed then we want only to create the
// entity for the language for which we are going to save the form.
// The form language could be changed e.g. through the LanguageWidget.
if ($entity->isNew() && ($entity->language()->getId() != $form_langcode) && (count($entity->getTranslationLanguages()) == 1)) {
$entity_values = $entity->toArray();
$entity_values[$this->getReferencedEntityType()->getKey('langcode')] = $form_langcode;
// The bundle structure must be flat e.g. bundle=>bundle_type instead
// as returned by toArray - bundle=>[0 => [target_id => bundle_type]].
if ($bundle_field_name = $this->getReferencedEntityType()->getKey('bundle')) {
$entity_values[$bundle_field_name] = $entity->bundle();
}
$entity = $this->getReferencedEntityStorage()->create($entity_values);
}
else {
// If the entity is new it should be already in the correct translation!
$entity = $this->prepareTranslation($entity, $original_delta, $form['#parents'], $form_state);
}
}
// Build the entity.
$this->buildEntity($delta_values, $element_form, $form_state, $entity);
// Remove all the values which are entity fields, leave only the rest,
// such as '_original_delta' and '_weight'.
foreach ($entity as $name => $field) {
unset($delta_values[$name]);
}
$delta_values['entity'] = $entity;
}
return $values;
}
/**
* Builds an updated entity object based upon the submitted form values.
*
* For building the updated entity object the submitted form values are
* copied to entity properties and all the specified entity builders for
* copying form values to entity properties are invoked.
*
* @param array $delta_values
* The submitted delta form values produced by the widget.
* @param array $element_form
* The form structure of the delta element, a sub-element of a larger form.
* @param \Drupal\Core\Form\FormStateInterface $form_state
* The form state.
* @param \Drupal\Core\Entity\ContentEntityInterface $entity
* The entity mapped to the delta values. This parameter is given by
* reference to allow extending the class and exchanging the entity object.
*/
protected function buildEntity(array &$delta_values, array $element_form, FormStateInterface $form_state, ContentEntityInterface &$entity) {
$this->mapDeltaFormValuesToEntity($delta_values, $element_form, $form_state, $entity);
// Invoke all specified builders for copying form values to entity
// properties. Here we create a dedicated form state containing only the
// entity specific form values, as there are entity builders such
// ContentTranslationHandler::entityFormEntityBuild, which expect that the
// value are at the first level and does not search by field parents like
// WidgetBase does.
if (isset($element_form['#entity_builders'])) {
$entity_form_state = new FormState();
$entity_form_state->setValues($delta_values);
$entity_form_state->setStorage($form_state->getStorage());
$entity_form_state->setFormObject($form_state->getFormObject());
$entity_form_state->setValidationComplete($form_state->isValidationComplete());
$entity_form_state->setSubmitHandlers($form_state->getSubmitHandlers());
$entity_form_state->setUserInput($form_state->getUserInput());
// Add the form display to the form state as it might be needed by the
// entity builders.
$entity_form_display = $this->getFormDisplay($entity->getEntityTypeId(), $entity->bundle());
/** @var \Drupal\Core\Entity\ContentEntityFormInterface $form_object */
$form_object = $form_state->getFormObject();
$main_form_display = $form_object->getFormDisplay($form_state);
$form_object->setFormDisplay($entity_form_display, $entity_form_state);
foreach ($element_form['#entity_builders'] as $function) {
call_user_func_array($function, [$entity->getEntityTypeId(), $entity, &$element_form, &$entity_form_state]);
}
// Replace the delta values with the ones from the entity form state, as
// they could have changed in some of the entity builders functions.
$delta_values = $entity_form_state->getValues();
// As the entity builders might've altered the form state storage we have
// to set back the updated storage to the parent form state.
$form_state->setStorage($entity_form_state->getStorage());
$form_object->setFormDisplay($main_form_display, $form_state);
// A form builder might have altered the user input so we have to set it
// back.
$form_state->setUserInput($entity_form_state->getUserInput());
}
// Validate the generated entity. This shoudl be done after the entity
// builders have run.
$this->doValidate($element_form, $form_state, $entity);
// Flag the entity with the language for which we have edited the entity in
// order to check only this language for translation changes on save in the
// pre-save of the field item.
// @see \Drupal\entity_reference_inline\Plugin\Field\FieldType\EntityReferenceInlineItem::preSave()
$entity->inlineEditedLangcode = $entity->language()->getId();
}
/**
* Processes the delta form values and maps them to the corresponding entity.
*
* @param array $delta_values
* The submitted delta form values produced by the widget.
* @param array $element_form
* The form structure of the delta element, a sub-element of a larger form.
* @param \Drupal\Core\Form\FormStateInterface $form_state
* The form state.
* @param \Drupal\Core\Entity\ContentEntityInterface $entity
* The entity mapped to the delta values. This parameter is given by
* reference to allow extending the class and exchanging the entity object.
*/
protected function mapDeltaFormValuesToEntity(array &$delta_values, array $element_form, FormStateInterface $form_state, ContentEntityInterface &$entity) {
$entity_form_display = $this->getFormDisplay($entity->getEntityTypeId(), $entity->bundle());
// First, extract values from widgets for the form display mode 'edit_by_user_text'.
$extracted = $entity_form_display->extractFormValues($entity, $element_form, $form_state);
// Then extract the values of fields that are not rendered through widgets,
// by simply copying from top-level form values. This leaves the fields
// that are not being edited within this form untouched.
foreach ($delta_values as $name => $delta_value) {
if ($entity->hasField($name) && !isset($extracted[$name])) {
$entity->set($name, $delta_value);
}
}
// Update the entity changed timestamp after it has been validated if a new
// translation was added to it.
if ($entity->isNewTranslation()) {
// Checks whether the entity is enabled for content translation.
if ($this->contentTranslationManager->isEnabled($entity->getEntityTypeId(), $entity->bundle())) {
$metadata = $this->contentTranslationManager->getTranslationMetadata($entity);
$metadata->setChangedTime(REQUEST_TIME);
}
elseif ($entity instanceof EntityChangedInterface) {
$entity->setChangedTime(REQUEST_TIME);
}
}
}
/**
* Validates the generated entity.
*
* @param array $element_form
* The form structure of the delta element, a sub-element of a larger form.
* @param \Drupal\Core\Form\FormStateInterface $form_state
* The form state.
* @param \Drupal\Core\Entity\ContentEntityInterface $entity
* The entity to be validated.
*/
protected function doValidate(array $element_form, FormStateInterface $form_state, ContentEntityInterface $entity) {
// Only validate during form validation running and skip during submit!
if (!$form_state->isValidationComplete()) {
// Backup the errors so that we can estimate later the new one added
// by the validation and modify them.
$errors_previous = $form_state->getErrors();
$this->getFormDisplay($entity->getEntityTypeId(), $entity->bundle())->validateFormValues($entity, $element_form, $form_state);
$errors_after = $form_state->getErrors();
// Estimate the new errors and modify them.
$new_errors = array_diff_key($errors_after, $errors_previous);
foreach ($new_errors as $name => &$message) {
$message = $this->t('Referenced entity (:ref_entity_type) ":ref_entity": @message', [':ref_entity_type' => $entity->getEntityType()->getLabel(), ':ref_entity' => $entity->label(), '@message' => $message]);
}
// TODO ensure the corresponding entities are marked with an error class.
// Restore the errors.
$form_state->clearErrors();
foreach (array_merge($errors_previous, $new_errors) as $name => $message) {
$form_state->setErrorByName($name, $message);
}
}
}
/**
* Submission handler for the "Remove item" button.
*/
public static function removeSubmit(array $form, FormStateInterface $form_state) {
$button = $form_state->getTriggeringElement();
// Go one level up in the form, to the element to be removed.
$delta_parents = array_slice($button['#array_parents'], 0, -1);
$delta = end($delta_parents);
$field_parents = array_slice($button['#array_parents'], 0, -2);
$element = NestedArray::getValue($form, $field_parents);
$field_name = $element['#field_name'];
$parents = $element['#field_parents'];
// Flag the delta item as removed.
$field_state = static::getWidgetState($parents, $field_name, $form_state);
$field_state['deltas_removed'] = isset($field_state['deltas_removed']) ? $field_state['deltas_removed'] : [];
$field_state['deltas_removed'][$delta] = TRUE;
static::setWidgetState($parents, $field_name, $form_state, $field_state);
$form_state->setRebuild();
}
/**
* Ajax callback for the "Remove item" button.
*/
public static function removeAjax(array $form, FormStateInterface $form_state) {
$button = $form_state->getTriggeringElement();
$table_id = '#' . $button['#ajax']['table_id'];
$row_id = '#' . $button['#ajax']['row_id'];
$response = new AjaxResponse();
$response->addCommand(new RemoveCommand($row_id));
$response->addCommand(new RestripeCommand($table_id));
return $response;
}
/**
* Returns the referenced entity type.
*
* @return \Drupal\Core\Entity\EntityTypeInterface|null
*/
protected function getReferencedEntityType() {
if (!isset($this->referencedEntityType)) {
$target_type = $this->getFieldSetting('target_type');
$this->referencedEntityType = $this->entityTypeManager->getDefinition($target_type);
}
return $this->referencedEntityType;
}
/**
* Returns the entity storage for the referenced entity type.
*
* @return \Drupal\Core\Entity\EntityStorageInterface
*/
protected function getReferencedEntityStorage() {
$target_type = $this->getFieldSetting('target_type');
return $this->entityTypeManager->getStorage($target_type);
}
/**
* Returns the EntityFormDisplay for the referenced entity.
*
* @param string $entity_type_id
* The entity type id.
* @param string $bundle
* The entity bundle.
*
* @return \Drupal\Core\Entity\Display\EntityFormDisplayInterface
* The entity form display for the given entity.
*/
protected function getFormDisplay($entity_type_id, $bundle) {
if ($this->getReferencedEntityType()->hasKey('bundle')) {
$form_modes_bundles = $this->getSetting('form_modes_bundles');
// Fallback to the default.
$form_mode = isset($form_modes_bundles[$bundle]) ? $form_modes_bundles[$bundle] : $this->getSetting('form_mode');
}
else {
$form_mode = $this->getSetting('form_mode');
}
$entity_form_display_id = implode('.', [$entity_type_id, $bundle, $form_mode]);
if (!isset($this->referencedEntityFormDisplays[$entity_form_display_id])) {
$this->referencedEntityFormDisplays[$entity_form_display_id] = EntityFormDisplay::load($entity_form_display_id);
// The form display will not be present if not explicitly created, so
// at this place we do it just like the core does and create it on the
// fly without saving it.
if (!isset($this->referencedEntityFormDisplays[$entity_form_display_id])) {
$this->referencedEntityFormDisplays[$entity_form_display_id] = EntityFormDisplay::create([
'targetEntityType' => $entity_type_id,
'bundle' => $bundle,
'mode' => $form_mode,
'status' => TRUE,
]);
}
}
return $this->referencedEntityFormDisplays[$entity_form_display_id];
}
/**
* Returns the form display options.
*
* @param $bundle
* (optional) The bundle for which to retrieve the form mode options.
* @return array
* List of form modes.
*/
protected function getFormModeOptions($bundle = NULL) {
$target_type = $this->getFieldSetting('target_type');
$form_modes = isset($bundle) ? $this->entityDisplayRepository->getFormModeOptionsByBundle($target_type, $bundle) : $this->entityDisplayRepository->getFormModeOptions($target_type);
return $form_modes;
}
/**
* Get the new element return mode from the widget state.
*
* @return string
* The new element return mode.
*/
protected static function getNewElementReturnMode($parents, $field_name, FormStateInterface $form_state) {
$field_state = static::getWidgetState($parents, $field_name, $form_state);
return $field_state['new_element_return_mode'];
}
/**
* Returns the entity object that is used in case of reused entities.
*
* @param string $entity_type_id
* The entity type ID.
* @param mixed $entity_id
* The entity ID.
*
* @return \Drupal\Core\Entity\EntityInterface|NULL
* The built entity object or NULL if not set.
*/
protected function getBuiltEntity($entity_type_id, $entity_id) {
return isset(static::$builtEntities[$entity_type_id][$entity_id]) ? static::$builtEntities[$entity_type_id][$entity_id] : NULL;
}
/**
* Adds the entity object to the built entity list.
*
* @param \Drupal\Core\Entity\EntityInterface $entity
* The entity.
*/
protected function setBuiltEntity(EntityInterface $entity) {
static::$builtEntities[$entity->getEntityTypeId()][$entity->id()] = $entity;
}
}
