widget_engine-8.x-1.2/modules/widget_engine_entity_form/src/Plugin/Field/FieldWidget/WidgetEngineEntityReferenceBrowserWidget.php
modules/widget_engine_entity_form/src/Plugin/Field/FieldWidget/WidgetEngineEntityReferenceBrowserWidget.php
<?php
namespace Drupal\widget_engine_entity_form\Plugin\Field\FieldWidget;
use Drupal\Component\Utility\Html;
use Drupal\Component\Utility\NestedArray;
use Drupal\Core\Ajax\AjaxResponse;
use Drupal\Core\Ajax\OpenDialogCommand;
use Drupal\Core\Entity\ContentEntityInterface;
use Drupal\Core\Entity\EntityInterface;
use Drupal\Core\Field\FieldItemListInterface;
use Drupal\Core\Form\FormStateInterface;
use Drupal\Core\Plugin\ContainerFactoryPluginInterface;
use Drupal\Core\Render\RendererInterface;
use Drupal\Core\Url;
use Drupal\entity_browser\Element\EntityBrowserElement;
use Drupal\entity_browser\Plugin\Field\FieldWidget\EntityReferenceBrowserWidget;
use Symfony\Component\DependencyInjection\ContainerInterface;
/**
* Plugin implementation of the 'entity_reference' widget for entity browser.
*
* @FieldWidget(
* id = "widget_entity_browser_entity_reference",
* label = @Translation("Widget entity browser"),
* description = @Translation("Uses entity browser to select entities."),
* multiple_values = TRUE,
* field_types = {
* "entity_reference"
* }
* )
*/
class WidgetEngineEntityReferenceBrowserWidget extends EntityReferenceBrowserWidget implements ContainerFactoryPluginInterface {
/**
* The depth of the delete button.
*
* This property exists so it can be changed if subclasses.
*
* @var int
*/
protected static $deleteDepth = 5;
/**
* The renderer service.
*
* @var \Drupal\Core\Render\RendererInterface
*/
protected $renderer;
/**
* {@inheritdoc}
*/
public function __construct(RendererInterface $renderer, ...$default) {
parent::__construct(...$default);
$this->renderer = $renderer;
}
/**
* {@inheritdoc}
*/
public static function create(ContainerInterface $container, array $configuration, $plugin_id, $plugin_definition) {
return new static(
$container->get('renderer'),
$plugin_id,
$plugin_definition,
$configuration['field_definition'],
$configuration['settings'],
$configuration['third_party_settings']
);
}
/**
* {@inheritdoc}
*/
public static function defaultSettings() {
return [
'placeholder' => NULL,
] + parent::defaultSettings();
}
/**
* {@inheritdoc}
*/
public function settingsForm(array $form, FormStateInterface $form_state) {
$element = parent::settingsForm($form, $form_state);
$placeholder_default_value = $this->getSetting('placeholder');
$element['placeholder'] = [
'#type' => 'text_format',
'#title' => $this->t('Text placeholder'),
'#description' => $this->t('This text will be show when field is empty.'),
'#format' => !empty($placeholder_default_value) ? $placeholder_default_value['format'] : filter_default_format(),
'#default_value' => !empty($placeholder_default_value) ? $placeholder_default_value['value'] : '',
];
return $element;
}
/**
* {@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);
// Set placeholder text to form state storage.
$form_state->set('placeholder', $this->getSetting('placeholder'));
$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,
'class' => 'widgets-list-wrapper',
],
'#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',
],
],
];
// handle button position (top, bottom) and add new entities accordingly
$element['btn_position'] = [
'#type' => 'hidden',
'#default_value' => '',
];
$triggering_element = $form_state->getTriggeringElement();
$input = $form_state->getUserInput();
$btn_position = NestedArray::getValue($input, ['field_widgets', 'btn_position']);
if (!empty($triggering_element) && !empty($btn_position)) {
if ($btn_position == 'top') {
$this->setSetting('selection_mode', EntityBrowserElement::SELECTION_MODE_PREPEND);
}
elseif ($btn_position == 'bottom') {
$this->setSetting('selection_mode', EntityBrowserElement::SELECTION_MODE_APPEND);
}
}
// 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))) {
$element['entity_browser'] = [
'#type' => 'entity_browser',
'#entity_browser' => $this->getSetting('entity_browser'),
'#cardinality' => $cardinality,
'#selection_mode' => $selection_mode,
'#default_value' => $entities,
'#entity_browser_validators' => ['entity_type' => ['type' => $entity_type]],
'#custom_hidden_id' => $hidden_id,
'#process' => [
['\Drupal\entity_browser\Element\EntityBrowserElement', 'processEntityBrowser'],
[get_called_class(), 'processEntityBrowser'],
],
];
}
$element['#attached']['library'][] = 'entity_browser/entity_reference';
$element['#attached']['library'][] = 'widget_engine_entity_form/entity_browser_widget_preview';
$element['#attached']['drupalSettings']['tokens'] = [
'token_preview' => \Drupal::csrfToken()->get('widgetTokenPreview'),
'token_save' => \Drupal::csrfToken()->get('widgetTokenPreviewSave'),
];
$element['#attributes']['class'][] = 'widgets-list-wrapper';
$field_parents = $element['#field_parents'];
$element['current'] = $this->displayCurrentSelection($details_id, $field_parents, $entities, $form_state->get('langcode'));
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']);
if (empty($element['#default_value'])) {
$storage = $form_state->getStorage();
$placeholder = $storage['placeholder'];
$element['placeholder'] = [
'#type' => 'item',
'#title' => '',
'#markup' => check_markup($placeholder['value'], $placeholder['format']),
'#weight' => -1,
];
}
$element['#attached']['drupalSettings']['entity_browser'][$uuid]['selector'] = '#' . $element['#custom_hidden_id'];
// Update entity browser controls.
$element['entity_browser']['#attributes'] = [
'class' => ['eb-main-controls'],
];
$element['entity_browser']['open_modal']['#attributes']['class'] = [
'open-modal-main',
];
// Change label for basic button.
$element['entity_browser']['open_modal']['#value'] = t('Select widgets');
// Add new button "Create new widget".
$button = $element['entity_browser']['open_modal'];
$callback_class = get_called_class();
if (!empty($form_state->getStorage()['form_display'])) {
$form_display = $form_state->getStorage()['form_display'];
if (!empty($form_display->getPluginCollections()['widgets'])) {
$plugin_collections = $form_display->getPluginCollections()['widgets'];
if ($plugin_collections->has('widget_entity_browser_entity_reference')) {
$callback_class = $plugin_collections->get('widget_entity_browser_entity_reference');
}
}
}
$button['#ajax']['callback'] = [$callback_class, 'openModal'];
$element['entity_browser']['open_modal_add'] = $button;
$element['entity_browser']['open_modal_add']['#attributes']['class'] = [
'open-modal-add-main',
];
$element['entity_browser']['open_modal_add']['#weight'] = -1;
$element['entity_browser']['open_modal_add']['#value'] = t('Add a new widget');
// Get entity browser element.
$parents = $element['#array_parents'];
$copy_form = &$complete_form;
foreach ($parents as $parent) {
if ($parent == 'entity_browser') {
break;
}
$copy_form = &$copy_form[$parent];
}
if ($element['#default_value']) {
// Create new group of controls 'Secondary'.
$copy_form['secondary_controls'] = [
'#theme_wrappers' => ['container'],
'#attributes' => [
'class' => ['eb-secondary-controls'],
],
'open_modal_add_secondary' => [
'#type' => 'button',
'#value' => $element['entity_browser']['open_modal_add']['#value'],
'#attributes' => [
'data-uuid' => $element['entity_browser']['open_modal_add']['#attributes']['data-uuid'],
'class' => ['open-modal-add-secondary'],
],
],
'open_modal_secondary' => [
'#type' => 'button',
'#value' => $element['entity_browser']['open_modal']['#value'],
'#attributes' => [
'data-uuid' => $element['entity_browser']['open_modal']['#attributes']['data-uuid'],
'class' => [
'open-modal-secondary',
],
],
],
];
}
return $element;
}
/**
* Helper function for getting configs by entity browser machine name.
*
* @param string $entity_browser_name
* Machine name of entity browser.
*
* @return array
* Array with configs for provided entity browser by its name.
*
* @throws \Drupal\Component\Plugin\Exception\InvalidPluginDefinitionException
*/
public static function getEntityBrowserConfig($entity_browser_name) {
$display_settings = [
'width' => '',
'height' => '',
'link_text' => t('Select widgets'),
];
$configs = \Drupal::entityTypeManager()
->getStorage('entity_browser')
->load($entity_browser_name);
if ($configs) {
$display_settings = $configs->display_configuration;
}
return $display_settings;
}
/**
* Generates the content and opens the modal.
*
* @param array $form
* The form array.
* @param \Drupal\Core\Form\FormStateInterface $form_state
* The form state object.
*
* @return \Drupal\Core\Ajax\AjaxResponse
* An ajax response.
*
* @throws \Drupal\Component\Plugin\Exception\InvalidPluginDefinitionException
*/
public function openModal(array &$form, FormStateInterface $form_state) {
$triggering_element = $form_state->getTriggeringElement();
$parents = $triggering_element['#parents'];
array_pop($parents);
$parents = array_merge($parents, ['path']);
$input = $form_state->getUserInput();
$src = NestedArray::getValue($input, $parents);
$src .= '&add_tab=true';
$field_name = $triggering_element['#parents'][0];
$element_name = $form[$field_name]['widget']['entity_browser']['#entity_browser'];
$name = 'entity_browser_iframe_' . $element_name;
$settings = static::getEntityBrowserConfig($element_name);
$content = [
'#prefix' => '<div class="ajax-progress-throbber"></div>',
'#type' => 'html_tag',
'#tag' => 'iframe',
'#attributes' => [
'src' => $src,
'class' => 'entity-browser-modal-iframe',
'width' => '100%',
'height' => $settings['height'] - 90,
'frameborder' => 0,
'style' => 'padding:0; position:relative; z-index:10002;',
'name' => $name,
'id' => $name,
],
];
$html = $this->renderer->render($content);
$response = new AjaxResponse();
$response->addCommand(new OpenDialogCommand('#' . Html::getUniqueId($field_name . '-' . $element_name . '-dialog'), $settings['link_text'], $html, [
'width' => 'auto',
'height' => 'auto',
'modal' => TRUE,
'maxWidth' => $settings['width'],
'maxHeight' => $settings['height'],
'fluid' => 1,
'autoResize' => 0,
'resizable' => 0,
]));
return $response;
}
/**
* 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);
//Returns ajax response with WidgetPreviewRebuildCommand command for
// last added widget, if the widget doesn't have preview
$matches = [];
preg_match('/widget:(\d*)$/', $trigger['#value'], $matches);
if (($widget_id = end($matches)) && ($widget = Widget::load($widget_id)) && $widget->get('widget_preview')->isEmpty()) {
$html = NestedArray::getValue($form, $parents);
$response = new AjaxResponse();
$response->addCommand(new InsertCommand(NULL, $html));
$response->addCommand(new WidgetPreviewRebuildCommand($widget_id));
return $response;
}
}
elseif ($trigger['#type'] == 'submit' && strpos($trigger['#name'], '_remove_')) {
$parents = array_slice($trigger['#array_parents'], 0, -static::$deleteDepth);
}
return NestedArray::getValue($form, $parents);
}
/**
* 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, $langcode = NULL) {
$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, $langcode) {
$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,
'operations' => [
'#theme_wrappers' => ['container'],
'#attributes' => [
'class' => ['widget-actions'],
],
'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,
'langcode' => $langcode,
],
],
],
'#access' => $edit_button_access,
],
],
];
},
$entities,
empty($entities) ? [] : range(0, count($entities) - 1)
),
];
}
/**
* 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.
*
* @throws \Drupal\Component\Plugin\Exception\InvalidPluginDefinitionException
*/
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;
$trigger = $form_state->getTriggeringElement();
// Can be triggered by hidden target_id element or "Remove" button.
if ($trigger &&
(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;
}
}
}
