entityTypeManager = $entity_type_manager;
$this->fieldDisplayManager = $field_display_manager;
$this->moduleHandler = $module_handler;
}
/**
* {@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('entity_type.manager'),
$container->get('event_dispatcher'),
$container->get('plugin.manager.entity_browser.field_widget_display'),
$container->get('module_handler')
);
}
/**
* {@inheritdoc}
*/
public static function defaultSettings() {
return array(
'entity_browser' => NULL,
'open' => FALSE,
'field_widget_display' => 'label',
'field_widget_edit' => TRUE,
'field_widget_remove' => TRUE,
'field_widget_display_settings' => [],
'selection_mode' => EntityBrowserElement::SELECTION_MODE_APPEND,
) + parent::defaultSettings();
}
/**
* {@inheritdoc}
*/
public function settingsForm(array $form, FormStateInterface $form_state) {
$element = parent::settingsForm($form, $form_state);
$browsers = [];
/** @var \Drupal\entity_browser\EntityBrowserInterface $browser */
foreach ($this->entityTypeManager->getStorage('entity_browser')->loadMultiple() as $browser) {
$browsers[$browser->id()] = $browser->label();
}
$element['entity_browser'] = [
'#title' => $this->t('Entity browser'),
'#type' => 'select',
'#default_value' => $this->getSetting('entity_browser'),
'#options' => $browsers,
];
$target_type = $this->fieldDefinition->getFieldStorageDefinition()->getSetting('target_type');
$entity_type = $this->entityTypeManager->getStorage($target_type)->getEntityType();
$displays = [];
foreach ($this->fieldDisplayManager->getDefinitions() as $id => $definition) {
if ($this->fieldDisplayManager->createInstance($id)->isApplicable($entity_type)) {
$displays[$id] = $definition['label'];
}
}
$id = Html::getUniqueId('field-' . $this->fieldDefinition->getName() . '-display-settings-wrapper');
$element['field_widget_display'] = [
'#title' => $this->t('Entity display plugin'),
'#type' => 'select',
'#default_value' => $this->getSetting('field_widget_display'),
'#options' => $displays,
'#ajax' => [
'callback' => array($this, 'updateSettingsAjax'),
'wrapper' => $id,
],
];
$edit_button_access = TRUE;
if ($entity_type->id() == 'file') {
// For entities of type "file", it only makes sense to have the edit
// button if the module "file_entity" is present.
$edit_button_access = $this->moduleHandler->moduleExists('file_entity');
}
$element['field_widget_edit'] = [
'#title' => $this->t('Display Edit button'),
'#type' => 'checkbox',
'#default_value' => $this->getSetting('field_widget_edit'),
'#access' => $edit_button_access,
];
$element['field_widget_remove'] = [
'#title' => $this->t('Display Remove button'),
'#type' => 'checkbox',
'#default_value' => $this->getSetting('field_widget_remove'),
];
$element['open'] = [
'#title' => $this->t('Show widget details as open by default'),
'#description' => $this->t('If marked, the fieldset container that wraps the browser on the entity form will be loaded initially expanded.'),
'#type' => 'checkbox',
'#default_value' => $this->getSetting('open'),
];
$element['selection_mode'] = [
'#title' => $this->t('Selection mode'),
'#description' => $this->t('Determines how selection in entity browser will be handled. Will selection be appended/prepended or it will be replaced in case of editing.'),
'#type' => 'select',
'#options' => EntityBrowserElement::getSelectionModeOptions(),
'#default_value' => $this->getSetting('selection_mode'),
];
$element['field_widget_display_settings'] = [
'#type' => 'fieldset',
'#title' => $this->t('Entity display plugin configuration'),
'#tree' => TRUE,
'#prefix' => '
',
'#suffix' => '
',
];
if ($this->getSetting('field_widget_display')) {
$element['field_widget_display_settings'] += $this->fieldDisplayManager
->createInstance(
$form_state->getValue(
['fields', $this->fieldDefinition->getName(), 'settings_edit_form', 'settings', 'field_widget_display'],
$this->getSetting('field_widget_display')
),
$form_state->getValue(
['fields', $this->fieldDefinition->getName(), 'settings_edit_form', 'settings', 'field_widget_display_settings'],
$this->getSetting('field_widget_display_settings')
) + ['entity_type' => $this->fieldDefinition->getFieldStorageDefinition()->getSetting('target_type')]
)
->settingsForm($form, $form_state);
}
return $element;
}
/**
* Ajax callback that updates field widget display settings fieldset.
*/
public function updateSettingsAjax(array $form, FormStateInterface $form_state) {
return $form['fields'][$this->fieldDefinition->getName()]['plugin']['settings_edit_form']['settings']['field_widget_display_settings'];
}
/**
* {@inheritdoc}
*/
public function settingsSummary() {
$summary = $this->summaryBase();
$field_widget_display = $this->getSetting('field_widget_display');
if (!empty($field_widget_display)) {
$plugin = $this->fieldDisplayManager->getDefinition($field_widget_display);
$summary[] = $this->t('Entity display: @name', ['@name' => $plugin['label']]);
}
return $summary;
}
/**
* {@inheritdoc}
*/
public function flagErrors(FieldItemListInterface $items, ConstraintViolationListInterface $violations, array $form, FormStateInterface $form_state) {
if ($violations->count() > 0) {
/** @var \Symfony\Component\Validator\ConstraintViolation $violation */
foreach ($violations as $offset => $violation) {
// The value of the required field is checked through the "not null"
// constraint, whose message is not very useful. We override it here for
// better UX.
if ($violation->getConstraint() instanceof NotNullConstraint) {
$violations->set($offset, new ConstraintViolation(
$this->t('@name field is required.', ['@name' => $items->getFieldDefinition()->getLabel()]),
'',
[],
$violation->getRoot(),
$violation->getPropertyPath(),
$violation->getInvalidValue(),
$violation->getPlural(),
$violation->getCode(),
$violation->getConstraint(),
$violation->getCause()
));
}
}
}
parent::flagErrors($items, $violations, $form, $form_state);
}
/**
* Returns a key used to store the previously loaded entity.
*
* @param \Drupal\Core\Field\FieldItemListInterface $items
* The field items.
*
* @return string
* A key for form state storage.
*/
protected function getFormStateKey(FieldItemListInterface $items) {
return $items->getEntity()->uuid() . ':' . $items->getFieldDefinition()->getName();
}
/**
* {@inheritdoc}
*/
public function formElement(FieldItemListInterface $items, $delta, array $element, array &$form, FormStateInterface $form_state) {
$entity_type = $this->fieldDefinition->getFieldStorageDefinition()->getSetting('target_type');
$entities = $this->formElementEntities($items, $element, $form_state);
// Get correct ordered list of entity IDs.
$ids = array_map(
function (EntityInterface $entity) {
return $entity->id();
},
$entities
);
// We store current entity IDs as we might need them in future requests. If
// some other part of the form triggers an AJAX request with
// #limit_validation_errors we won't have access to the value of the
// target_id element and won't be able to build the form as a result of
// that. This will cause missing submit (Remove, Edit, ...) elements, which
// might result in unpredictable results.
$form_state->set(['entity_browser_widget', $this->getFormStateKey($items)], $ids);
$hidden_id = Html::getUniqueId('edit-' . $this->fieldDefinition->getName() . '-target-id');
$details_id = Html::getUniqueId('edit-' . $this->fieldDefinition->getName());
$element += [
'#id' => $details_id,
'#type' => 'details',
'#open' => !empty($entities) || $this->getSetting('open'),
'#required' => $this->fieldDefinition->isRequired(),
// We are not using Entity browser's hidden element since we maintain
// selected entities in it during entire process.
'target_id' => [
'#type' => 'hidden',
'#id' => $hidden_id,
// We need to repeat ID here as it is otherwise skipped when rendering.
'#attributes' => ['id' => $hidden_id],
'#default_value' => implode(' ', array_map(
function (EntityInterface $item) {
return $item->getEntityTypeId() . ':' . $item->id();
},
$entities
)),
// #ajax is officially not supported for hidden elements but if we
// specify event manually it works.
'#ajax' => [
'callback' => [get_class($this), 'updateWidgetCallback'],
'wrapper' => $details_id,
'event' => 'entity_browser_value_updated',
],
],
];
// Get configuration required to check entity browser availability.
$cardinality = $this->fieldDefinition->getFieldStorageDefinition()->getCardinality();
$selection_mode = $this->getSetting('selection_mode');
// Enable entity browser if requirements for that are fulfilled.
if (EntityBrowserElement::isEntityBrowserAvailable($selection_mode, $cardinality, count($ids))) {
$persistentData = $this->getPersistentData();
$element['entity_browser'] = [
'#type' => 'entity_browser',
'#entity_browser' => $this->getSetting('entity_browser'),
'#cardinality' => $cardinality,
'#selection_mode' => $selection_mode,
'#default_value' => $entities,
'#entity_browser_validators' => $persistentData['validators'],
'#widget_context' => $persistentData['widget_context'],
'#custom_hidden_id' => $hidden_id,
'#process' => [
['\Drupal\entity_browser\Element\EntityBrowserElement', 'processEntityBrowser'],
[get_called_class(), 'processEntityBrowser'],
],
];
}
$element['#attached']['library'][] = 'entity_browser/entity_reference';
$field_parents = $element['#field_parents'];
$element['current'] = $this->displayCurrentSelection($details_id, $field_parents, $entities);
return $element;
}
/**
* Render API callback: Processes the entity browser element.
*/
public static function processEntityBrowser(&$element, FormStateInterface $form_state, &$complete_form) {
$uuid = key($element['#attached']['drupalSettings']['entity_browser']);
$element['#attached']['drupalSettings']['entity_browser'][$uuid]['selector'] = '#' . $element['#custom_hidden_id'];
return $element;
}
/**
* {@inheritdoc}
*/
public function massageFormValues(array $values, array $form, FormStateInterface $form_state) {
$entities = empty($values['target_id']) ? [] : explode(' ', trim($values['target_id']));
$return = [];
foreach ($entities as $entity) {
$return[]['target_id'] = explode(':', $entity)[1];
}
return $return;
}
/**
* AJAX form callback.
*/
public static function updateWidgetCallback(array &$form, FormStateInterface $form_state) {
$trigger = $form_state->getTriggeringElement();
// AJAX requests can be triggered by hidden "target_id" element when
// entities are added or by one of the "Remove" buttons. Depending on that
// we need to figure out where root of the widget is in the form structure
// and use this information to return correct part of the form.
if (!empty($trigger['#ajax']['event']) && $trigger['#ajax']['event'] == 'entity_browser_value_updated') {
$parents = array_slice($trigger['#array_parents'], 0, -1);
}
elseif ($trigger['#type'] == 'submit' && strpos($trigger['#name'], '_remove_')) {
$parents = array_slice($trigger['#array_parents'], 0, -static::$deleteDepth);
}
return NestedArray::getValue($form, $parents);
}
/**
* {@inheritdoc}
*/
public function errorElement(array $element, ConstraintViolationInterface $violation, array $form, FormStateInterface $form_state) {
if (($trigger = $form_state->getTriggeringElement())) {
// Can be triggered by "Remove" button.
if (end($trigger['#parents']) === 'remove_button') {
return FALSE;
}
}
return parent::errorElement($element, $violation, $form, $form_state);
}
/**
* Submit callback for remove buttons.
*/
public static function removeItemSubmit(&$form, FormStateInterface $form_state) {
$triggering_element = $form_state->getTriggeringElement();
if (!empty($triggering_element['#attributes']['data-entity-id']) && isset($triggering_element['#attributes']['data-row-id'])) {
$id = $triggering_element['#attributes']['data-entity-id'];
$row_id = $triggering_element['#attributes']['data-row-id'];
$parents = array_slice($triggering_element['#parents'], 0, -static::$deleteDepth);
$array_parents = array_slice($triggering_element['#array_parents'], 0, -static::$deleteDepth);
// Find and remove correct entity.
$values = explode(' ', $form_state->getValue(array_merge($parents, ['target_id'])));
foreach ($values as $index => $item) {
if ($item == $id && $index == $row_id) {
array_splice($values, $index, 1);
break;
}
}
$target_id_value = implode(' ', $values);
// Set new value for this widget.
$target_id_element = &NestedArray::getValue($form, array_merge($array_parents, ['target_id']));
$form_state->setValueForElement($target_id_element, $target_id_value);
NestedArray::setValue($form_state->getUserInput(), $target_id_element['#parents'], $target_id_value);
// Rebuild form.
$form_state->setRebuild();
}
}
/**
* Builds the render array for displaying the current results.
*
* @param string $details_id
* The ID for the details element.
* @param string[] $field_parents
* Field parents.
* @param \Drupal\Core\Entity\ContentEntityInterface[] $entities
* Array of referenced entities.
*
* @return array
* The render array for the current selection.
*/
protected function displayCurrentSelection($details_id, $field_parents, $entities) {
$field_widget_display = $this->fieldDisplayManager->createInstance(
$this->getSetting('field_widget_display'),
$this->getSetting('field_widget_display_settings') + ['entity_type' => $this->fieldDefinition->getFieldStorageDefinition()->getSetting('target_type')]
);
return [
'#theme_wrappers' => ['container'],
'#attributes' => ['class' => ['entities-list']],
'items' => array_map(
function (ContentEntityInterface $entity, $row_id) use ($field_widget_display, $details_id, $field_parents) {
$display = $field_widget_display->view($entity);
$edit_button_access = $this->getSetting('field_widget_edit');
if ($entity->getEntityTypeId() == 'file') {
// On file entities, the "edit" button shouldn't be visible unless
// the module "file_entity" is present, which will allow them to be
// edited on their own form.
$edit_button_access &= $this->moduleHandler->moduleExists('file_entity');
}
if (is_string($display)) {
$display = ['#markup' => $display];
}
return [
'#theme_wrappers' => ['container'],
'#attributes' => [
'class' => ['item-container', Html::getClass($field_widget_display->getPluginId())],
'data-entity-id' => $entity->getEntityTypeId() . ':' . $entity->id(),
'data-row-id' => $row_id,
],
'display' => $display,
'remove_button' => [
'#type' => 'submit',
'#value' => $this->t('Remove'),
'#ajax' => [
'callback' => [get_class($this), 'updateWidgetCallback'],
'wrapper' => $details_id,
],
'#submit' => [[get_class($this), 'removeItemSubmit']],
'#name' => $this->fieldDefinition->getName() . '_remove_' . $entity->id() . '_' . $row_id . '_' . md5(json_encode($field_parents)),
'#limit_validation_errors' => [array_merge($field_parents, [$this->fieldDefinition->getName()])],
'#attributes' => [
'data-entity-id' => $entity->getEntityTypeId() . ':' . $entity->id(),
'data-row-id' => $row_id,
],
'#access' => (bool) $this->getSetting('field_widget_remove'),
],
'edit_button' => [
'#type' => 'submit',
'#value' => $this->t('Edit'),
'#ajax' => [
'url' => Url::fromRoute(
'entity_browser.edit_form', [
'entity_type' => $entity->getEntityTypeId(),
'entity' => $entity->id(),
]
),
'options' => [
'query' => [
'details_id' => $details_id,
],
],
],
'#access' => $edit_button_access,
],
];
},
$entities,
empty($entities) ? [] : range(0, count($entities) - 1)
),
];
}
/**
* Gets data that should persist across Entity Browser renders.
*
* @return array
* Data that should persist after the Entity Browser is rendered.
*/
protected function getPersistentData() {
return [
'validators' => [
'entity_type' => ['type' => $this->fieldDefinition->getFieldStorageDefinition()->getSetting('target_type')],
],
'widget_context' => [],
];
}
/**
* Gets options that define where newly added entities are inserted.
*
* @return array
* Mode labels indexed by key.
*/
protected function selectionModeOptions() {
return ['append' => $this->t('Append'), 'prepend' => $this->t('Prepend')];
}
/**
* Provides base for settings summary shared by all EB widgets.
*
* @return array
* A short summary of the widget settings.
*/
protected function summaryBase() {
$summary = [];
$entity_browser_id = $this->getSetting('entity_browser');
if (empty($entity_browser_id)) {
return [$this->t('No entity browser selected.')];
}
else {
if ($browser = $this->entityTypeManager->getStorage('entity_browser')->load($entity_browser_id)) {
$summary[] = $this->t('Entity browser: @browser', ['@browser' => $browser->label()]);
}
else {
drupal_set_message($this->t('Missing entity browser!'), 'error');
return [$this->t('Missing entity browser!')];
}
}
$selection_mode = $this->getSetting('selection_mode');
$selection_mode_options = EntityBrowserElement::getSelectionModeOptions();
if (isset($selection_mode_options[$selection_mode])) {
$summary[] = $this->t('Selection mode: @selection_mode', ['@selection_mode' => $selection_mode_options[$selection_mode]]);
}
else {
$summary[] = $this->t('Undefined selection mode.');
}
return $summary;
}
/**
* Determines the entities used for the form element.
*
* @param \Drupal\Core\Field\FieldItemListInterface $items
* The field item to extract the entities from.
* @param array $element
* The form element.
* @param \Drupal\Core\Form\FormStateInterface $form_state
* The form state.
*
* @return \Drupal\Core\Entity\EntityInterface[]
* The list of entities for the form element.
*/
protected function formElementEntities(FieldItemListInterface $items, array $element, FormStateInterface $form_state) {
$entities = [];
$entity_type = $this->fieldDefinition->getFieldStorageDefinition()->getSetting('target_type');
$entity_storage = $this->entityTypeManager->getStorage($entity_type);
// Find IDs from target_id element (it stores selected entities in form).
// This was added to help solve a really edge casey bug in IEF.
if (($target_id_entities = $this->getEntitiesByTargetId($element, $form_state)) !== FALSE) {
return $target_id_entities;
}
// Determine if we're submitting and if submit came from this widget.
$is_relevant_submit = FALSE;
if (($trigger = $form_state->getTriggeringElement())) {
// Can be triggered by hidden target_id element or "Remove" button.
if (end($trigger['#parents']) === 'target_id' || (end($trigger['#parents']) === 'remove_button')) {
$is_relevant_submit = TRUE;
// In case there are more instances of this widget on the same page we
// need to check if submit came from this instance.
$field_name_key = end($trigger['#parents']) === 'target_id' ? 2 : static::$deleteDepth + 1;
$field_name_key = count($trigger['#parents']) - $field_name_key;
$is_relevant_submit &= ($trigger['#parents'][$field_name_key] === $this->fieldDefinition->getName()) &&
(array_slice($trigger['#parents'], 0, count($element['#field_parents'])) == $element['#field_parents']);
}
};
if ($is_relevant_submit) {
// Submit was triggered by hidden "target_id" element when entities were
// added via entity browser.
if (!empty($trigger['#ajax']['event']) && $trigger['#ajax']['event'] == 'entity_browser_value_updated') {
$parents = $trigger['#parents'];
}
// Submit was triggered by one of the "Remove" buttons. We need to walk
// few levels up to read value of "target_id" element.
elseif ($trigger['#type'] == 'submit' && strpos($trigger['#name'], $this->fieldDefinition->getName() . '_remove_') === 0) {
$parents = array_merge(array_slice($trigger['#parents'], 0, -static::$deleteDepth), ['target_id']);
}
if (isset($parents) && $value = $form_state->getValue($parents)) {
$entities = EntityBrowserElement::processEntityIds($value);
return $entities;
}
return $entities;
}
// IDs from a previous request might be saved in the form state.
elseif ($form_state->has([
'entity_browser_widget',
$this->getFormStateKey($items),
])
) {
$stored_ids = $form_state->get([
'entity_browser_widget',
$this->getFormStateKey($items),
]);
$indexed_entities = $entity_storage->loadMultiple($stored_ids);
// Selection can contain same entity multiple times. Since loadMultiple()
// returns unique list of entities, it's necessary to recreate list of
// entities in order to preserve selection of duplicated entities.
foreach ($stored_ids as $entity_id) {
if (isset($indexed_entities[$entity_id])) {
$entities[] = $indexed_entities[$entity_id];
}
}
return $entities;
}
// We are loading for for the first time so we need to load any existing
// values that might already exist on the entity. Also, remove any leftover
// data from removed entity references.
else {
foreach ($items as $item) {
if (isset($item->target_id)) {
$entity = $entity_storage->load($item->target_id);
if (!empty($entity)) {
$entities[] = $entity;
}
}
}
return $entities;
}
}
/**
* {@inheritdoc}
*/
public function calculateDependencies() {
$dependencies = parent::calculateDependencies();
// If an entity browser is being used in this widget, add it as a config
// dependency.
if ($browser_name = $this->getSetting('entity_browser')) {
$dependencies['config'][] = 'entity_browser.browser.' . $browser_name;
}
return $dependencies;
}
/**
* Get selected elements from target_id element on form.
*
* @param array $element
* The form element.
* @param \Drupal\Core\Form\FormStateInterface $form_state
* The form state.
*
* @return \Drupal\Core\Entity\EntityInterface[]|false
* Return list of entities if they are available or false.
*/
protected function getEntitiesByTargetId(array $element, FormStateInterface $form_state) {
$target_id_element_path = array_merge(
$element['#field_parents'],
[$this->fieldDefinition->getName(), 'target_id']
);
if (!NestedArray::keyExists($form_state->getUserInput(), $target_id_element_path)) {
return FALSE;
}
// TODO Figure out how to avoid using raw user input.
$current_user_input = NestedArray::getValue($form_state->getUserInput(), $target_id_element_path);
if (!is_array($current_user_input)) {
$entities = EntityBrowserElement::processEntityIds($current_user_input);
return $entities;
}
return FALSE;
}
}