Commit b8364ecf authored by webchick's avatar webchick

Issue #1959806 by amateescu, jibran, goldorak, Wim Leers, yched, dawehner:...

Issue #1959806 by amateescu, jibran, goldorak, Wim Leers, yched, dawehner: Provide a generic 'entity_autocomplete' Form API element
parent 58e0453a
......@@ -200,6 +200,34 @@ field.widget.settings.checkbox:
type: boolean
label: 'Use field label instead of the "On value" as label'
field.widget.settings.entity_reference_autocomplete_tags:
type: mapping
label: 'Entity reference autocomplete (Tags style) display format settings'
mapping:
match_operator:
type: string
label: 'Autocomplete matching'
size:
type: integer
label: 'Size of textfield'
placeholder:
type: label
label: 'Placeholder'
field.widget.settings.entity_reference_autocomplete:
type: mapping
label: 'Entity reference autocomplete display format settings'
mapping:
match_operator:
type: string
label: 'Autocomplete matching'
size:
type: integer
label: 'Size of textfield'
placeholder:
type: label
label: 'Placeholder'
field.formatter.settings.boolean:
type: mapping
mapping:
......
......@@ -357,6 +357,9 @@ services:
arguments: ['@config.manager', '@entity.manager']
tags:
- { name: event_subscriber }
entity.autocomplete_matcher:
class: Drupal\Core\Entity\EntityAutocompleteMatcher
arguments: ['@plugin.manager.entity_reference_selection']
plugin.manager.entity_reference_selection:
class: Drupal\Core\Entity\EntityReferenceSelection\SelectionPluginManager
parent: default_plugin_manager
......
<?php
/**
* @file
* Contains \Drupal\Core\Entity\Element\EntityAutocomplete.
*/
namespace Drupal\Core\Entity\Element;
use Drupal\Component\Utility\Tags;
use Drupal\Core\Form\FormStateInterface;
use Drupal\Core\Render\Element\Textfield;
use Drupal\user\EntityOwnerInterface;
/**
* Provides an entity autocomplete form element.
*
* @FormElement("entity_autocomplete")
*/
class EntityAutocomplete extends Textfield {
/**
* {@inheritdoc}
*/
public function getInfo() {
$info = parent::getInfo();
$class = get_class($this);
// Apply default form element properties.
$info['#target_type'] = NULL;
$info['#selection_handler'] = 'default';
$info['#selection_settings'] = array();
$info['#tags'] = FALSE;
$info['#autocreate'] = NULL;
// This should only be set to FALSE if proper validation by the selection
// handler is performed at another level on the extracted form values.
$info['#validate_reference'] = TRUE;
$info['#element_validate'] = array(array($class, 'validateEntityAutocomplete'));
array_unshift($info['#process'], array($class, 'processEntityAutocomplete'));
// @todo Consider providing better DX for #default_value? Maybe we impose an
// array('label' => .., 'value' => ..) structure instead of manually
// composing the textfield string?. See https://www.drupal.org/node/2418249.
return $info;
}
/**
* Adds entity autocomplete functionality to a form element.
*
* @param array $element
* The form element to process. Properties used:
* - #target_type: The ID of the target entity type.
* - #selection_handler: The plugin ID of the entity reference selection
* handler.
* - #selection_settings: An array of settings that will be passed to the
* selection handler.
* @param \Drupal\Core\Form\FormStateInterface $form_state
* The current state of the form.
* @param array $complete_form
* The complete form structure.
*
* @return array
* The form element.
*
* @throws \InvalidArgumentException
* Exception thrown when the #target_type or #autocreate['bundle'] are
* missing.
*/
public static function processEntityAutocomplete(array &$element, FormStateInterface $form_state, array &$complete_form) {
// Nothing to do if there is no target entity type.
if (empty($element['#target_type'])) {
throw new \InvalidArgumentException('Missing required #target_type parameter.');
}
// Provide default values and sanity checks for the #autocreate parameter.
if ($element['#autocreate']) {
if (!isset($element['#autocreate']['bundle'])) {
throw new \InvalidArgumentException("Missing required #autocreate['bundle'] parameter.");
}
// Default the autocreate user ID to the current user.
$element['#autocreate']['uid'] = isset($element['#autocreate']['uid']) ? $element['#autocreate']['uid'] : \Drupal::currentUser()->id();
}
$element['#autocomplete_route_name'] = 'system.entity_autocomplete';
$element['#autocomplete_route_parameters'] = array(
'target_type' => $element['#target_type'],
'selection_handler' => $element['#selection_handler'],
'selection_settings' => $element['#selection_settings'] ? base64_encode(serialize($element['#selection_settings'])) : '',
);
return $element;
}
/**
* Form element validation handler for entity_autocomplete elements.
*/
public static function validateEntityAutocomplete(array &$element, FormStateInterface $form_state, array &$complete_form) {
$value = NULL;
if (!empty($element['#value'])) {
$options = array(
'target_type' => $element['#target_type'],
'handler' => $element['#selection_handler'],
'handler_settings' => $element['#selection_settings'],
);
$handler = \Drupal::service('plugin.manager.entity_reference_selection')->getInstance($options);
$autocreate = (bool) $element['#autocreate'];
foreach (Tags::explode($element['#value']) as $input) {
$match = static::extractEntityIdFromAutocompleteInput($input);
if ($match === NULL) {
// Try to get a match from the input string when the user didn't use
// the autocomplete but filled in a value manually.
$match = $handler->validateAutocompleteInput($input, $element, $form_state, $complete_form, !$autocreate);
}
if ($match !== NULL) {
$value[] = array(
'target_id' => $match,
);
}
elseif ($autocreate) {
// Auto-create item. See an example of how this is handled in
// \Drupal\Core\Field\Plugin\Field\FieldType\EntityReferenceItem::presave().
$value[] = array(
'entity' => static::createNewEntity($element['#target_type'], $element['#autocreate']['bundle'], $input, $element['#autocreate']['uid'])
);
}
}
// Check that the referenced entities are valid, if needed.
if ($element['#validate_reference'] && !$autocreate && !empty($value)) {
$ids = array_reduce($value, function ($return, $item) {
if (isset($item['target_id'])) {
$return[] = $item['target_id'];
}
return $return;
});
if ($ids) {
$valid_ids = $handler->validateReferenceableEntities($ids);
if ($invalid_ids = array_diff($ids, $valid_ids)) {
foreach ($invalid_ids as $invalid_id) {
$form_state->setError($element, t('The referenced entity (%type: %id) does not exist.', array('%type' => $element['#target_type'], '%id' => $invalid_id)));
}
}
}
}
// Use only the last value if the form element does not support multiple
// matches (tags).
if (!$element['#tags'] && !empty($value)) {
$last_value = $value[count($value) - 1];
$value = isset($last_value['target_id']) ? $last_value['target_id'] : $last_value;
}
}
$form_state->setValueForElement($element, $value);
}
/**
* Extracts the entity ID from the autocompletion result.
*
* @param string $input
* The input coming from the autocompletion result.
*
* @return mixed|null
* An entity ID or NULL if the input does not contain one.
*/
public static function extractEntityIdFromAutocompleteInput($input) {
$match = NULL;
// Take "label (entity id)', match the ID from parenthesis when it's a
// number.
if (preg_match("/.+\((\d+)\)/", $input, $matches)) {
$match = $matches[1];
}
// Match the ID when it's a string (e.g. for config entity types).
elseif (preg_match("/.+\(([\w.]+)\)/", $input, $matches)) {
$match = $matches[1];
}
return $match;
}
/**
* Creates a new entity from a label entered in the autocomplete input.
*
* @param string $entity_type_id
* The entity type ID.
* @param string $bundle
* The bundle name.
* @param string $label
* The entity label.
* @param int $uid
* The entity owner ID.
*
* @return \Drupal\Core\Entity\EntityInterface
*/
protected static function createNewEntity($entity_type_id, $bundle, $label, $uid) {
$entity_manager = \Drupal::entityManager();
$entity_type = $entity_manager->getDefinition($entity_type_id);
$bundle_key = $entity_type->getKey('bundle');
$label_key = $entity_type->getKey('label');
$entity = $entity_manager->getStorage($entity_type_id)->create(array(
$bundle_key => $bundle,
$label_key => $label,
));
if ($entity instanceof EntityOwnerInterface) {
$entity->setOwnerId($uid);
}
return $entity;
}
}
......@@ -2,67 +2,46 @@
/**
* @file
* Contains \Drupal\entity_reference/EntityReferenceAutocomplete.
* Contains \Drupal\Core\Entity\EntityAutocompleteMatcher.
*/
namespace Drupal\entity_reference;
namespace Drupal\Core\Entity;
use Drupal\Component\Utility\String;
use Drupal\Component\Utility\Tags;
use Drupal\Core\Entity\EntityManagerInterface;
use Drupal\Core\Entity\EntityReferenceSelection\SelectionPluginManagerInterface;
use Drupal\Core\Field\FieldDefinitionInterface;
use Symfony\Component\HttpKernel\Exception\AccessDeniedHttpException;
/**
* Helper class to get autocompletion results for entity reference.
* Matcher class to get autocompletion results for entity reference.
*/
class EntityReferenceAutocomplete {
class EntityAutocompleteMatcher {
/**
* The entity manager.
*
* @var \Drupal\Core\Entity\EntityManagerInterface
*/
protected $entityManager;
/**
* The Entity reference selection handler plugin manager.
* The entity reference selection handler plugin manager.
*
* @var \Drupal\Core\Entity\EntityReferenceSelection\SelectionPluginManagerInterface
*/
protected $selectionHandlerManager;
protected $selectionManager;
/**
* Constructs a EntityReferenceAutocomplete object.
* Constructs a EntityAutocompleteMatcher object.
*
* @param \Drupal\Core\Entity\EntityManagerInterface $entity_manager
* The entity manager.
* @param \Drupal\Core\Entity\EntityReferenceSelection\SelectionPluginManagerInterface $selection_manager
* The Entity reference selection handler plugin manager.
* The entity reference selection handler plugin manager.
*/
public function __construct(EntityManagerInterface $entity_manager, SelectionPluginManagerInterface $selection_manager) {
$this->entityManager = $entity_manager;
$this->selectionHandlerManager = $selection_manager;
public function __construct(SelectionPluginManagerInterface $selection_manager) {
$this->selectionManager = $selection_manager;
}
/**
* Returns matched labels based on a given search string.
*
* This function can be used by other modules that wish to pass a mocked
* definition of the field on instance.
*
* @param \Drupal\Core\Field\FieldDefinitionInterface $field_definition
* The field definition.
* @param string $entity_type
* The entity type.
* @param string $bundle
* The entity bundle.
* @param string $entity_id
* (optional) The entity ID the entity reference field is attached to.
* Defaults to ''.
* @param string $prefix
* (optional) A prefix for all the keys returned by this function.
* @param string $target_type
* The ID of the target entity type.
* @param string $selection_handler
* The plugin ID of the entity reference selection handler.
* @param array $selection_settings
* An array of settings that will be passed to the selection handler.
* @param string $string
* (optional) The label of the entity to query by.
*
......@@ -70,26 +49,24 @@ public function __construct(EntityManagerInterface $entity_manager, SelectionPlu
* Thrown when the current user doesn't have access to the specifies entity.
*
* @return array
* A list of matched entity labels.
* An array of matched entity labels, in the format required by the AJAX
* autocomplete API (e.g. array('value' => $value, 'label' => $label)).
*
* @see \Drupal\entity_reference\EntityReferenceController
* @see \Drupal\system\Controller\EntityAutocompleteController
*/
public function getMatches(FieldDefinitionInterface $field_definition, $entity_type, $bundle, $entity_id = '', $prefix = '', $string = '') {
public function getMatches($target_type, $selection_handler, $selection_settings, $string = '') {
$matches = array();
$entity = NULL;
if ($entity_id !== 'NULL') {
$entity = $this->entityManager->getStorage($entity_type)->load($entity_id);
if (!$entity || !$entity->access('view')) {
throw new AccessDeniedHttpException();
}
}
$handler = $this->selectionHandlerManager->getSelectionHandler($field_definition, $entity);
$options = array(
'target_type' => $target_type,
'handler' => $selection_handler,
'handler_settings' => $selection_settings,
);
$handler = $this->selectionManager->getInstance($options);
if (isset($string)) {
// Get an array of matching entities.
$widget = entity_get_form_display($entity_type, $bundle, 'default')->getComponent($field_definition->getName());
$match_operator = !empty($widget['settings']['match_operator']) ? $widget['settings']['match_operator'] : 'CONTAINS';
$match_operator = !empty($selection_settings['match_operator']) ? $selection_settings['match_operator'] : 'CONTAINS';
$entity_labels = $handler->getReferenceableEntities($string, $match_operator, 10);
// Loop through the entities and convert them into autocomplete output.
......@@ -101,7 +78,7 @@ public function getMatches(FieldDefinitionInterface $field_definition, $entity_t
$key = preg_replace('/\s\s+/', ' ', str_replace("\n", '', trim(String::decodeEntities(strip_tags($key)))));
// Names containing commas or quotes must be wrapped in quotes.
$key = Tags::encode($key);
$matches[] = array('value' => $prefix . $key, 'label' => $label);
$matches[] = array('value' => $key, 'label' => $label);
}
}
}
......
<?php
/**
* @file
* Contains \Drupal\Core\Field\Plugin\Field\FieldWidget\AutocompleteTagsWidget.
*/
namespace Drupal\Core\Field\Plugin\Field\FieldWidget;
use Drupal\Core\Field\FieldItemListInterface;
use Drupal\Core\Form\FormStateInterface;
/**
* Plugin implementation of the 'entity_reference_autocomplete_tags' widget.
*
* @FieldWidget(
* id = "entity_reference_autocomplete_tags",
* label = @Translation("Autocomplete (Tags style)"),
* description = @Translation("An autocomplete text field with tagging support."),
* field_types = {
* "entity_reference"
* },
* multiple_values = TRUE
* )
*/
class EntityReferenceAutocompleteTagsWidget extends EntityReferenceAutocompleteWidget {
/**
* {@inheritdoc}
*/
public function formElement(FieldItemListInterface $items, $delta, array $element, array &$form, FormStateInterface $form_state) {
$element = parent::formElement($items, $delta, $element, $form, $form_state);
$element['target_id']['#tags'] = TRUE;
return $element;
}
/**
* {@inheritdoc}
*/
public function massageFormValues(array $values, array $form, FormStateInterface $form_state) {
return $values['target_id'];
}
}
......@@ -2,10 +2,10 @@
/**
* @file
* Contains \Drupal\entity_reference\Plugin\Field\FieldWidget\AutocompleteWidgetBase.
* Contains \Drupal\Core\Field\Plugin\Field\FieldWidget\AutocompleteWidget.
*/
namespace Drupal\entity_reference\Plugin\Field\FieldWidget;
namespace Drupal\Core\Field\Plugin\Field\FieldWidget;
use Drupal\Component\Utility\Tags;
use Drupal\Core\Field\EntityReferenceFieldItemListInterface;
......@@ -16,9 +16,29 @@
use Symfony\Component\Validator\ConstraintViolationInterface;
/**
* Parent plugin for entity reference autocomplete widgets.
* Plugin implementation of the 'entity_reference_autocomplete' widget.
*
* @FieldWidget(
* id = "entity_reference_autocomplete",
* label = @Translation("Autocomplete"),
* description = @Translation("An autocomplete text field."),
* field_types = {
* "entity_reference"
* }
* )
*/
abstract class AutocompleteWidgetBase extends WidgetBase {
class EntityReferenceAutocompleteWidget extends WidgetBase {
/**
* {@inheritdoc}
*/
public static function defaultSettings() {
return array(
'match_operator' => 'CONTAINS',
'size' => '60',
'placeholder' => '',
) + parent::defaultSettings();
}
/**
* {@inheritdoc}
......@@ -75,30 +95,27 @@ public function settingsSummary() {
public function formElement(FieldItemListInterface $items, $delta, array $element, array &$form, FormStateInterface $form_state) {
$entity = $items->getEntity();
// Prepare the autocomplete route parameters.
$autocomplete_route_parameters = array(
'type' => $this->getSetting('autocomplete_type'),
'field_name' => $this->fieldDefinition->getName(),
'entity_type' => $entity->getEntityTypeId(),
'bundle_name' => $entity->bundle(),
);
if ($entity_id = $entity->id()) {
$autocomplete_route_parameters['entity_id'] = $entity_id;
}
$element += array(
'#type' => 'textfield',
'#type' => 'entity_autocomplete',
'#target_type' => $this->getFieldSetting('target_type'),
'#selection_handler' => $this->getFieldSetting('handler'),
'#selection_settings' => $this->getFieldSetting('handler_settings'),
// Entity reference field items are handling validation themselves via
// the 'ValidReference' constraint.
'#validate_reference' => FALSE,
'#maxlength' => 1024,
'#default_value' => implode(', ', $this->getLabels($items, $delta)),
'#autocomplete_route_name' => 'entity_reference.autocomplete',
'#autocomplete_route_parameters' => $autocomplete_route_parameters,
'#size' => $this->getSetting('size'),
'#placeholder' => $this->getSetting('placeholder'),
'#element_validate' => array(array($this, 'elementValidate')),
'#autocreate_uid' => ($entity instanceof EntityOwnerInterface) ? $entity->getOwnerId() : \Drupal::currentUser()->id(),
);
if ($this->getSelectionHandlerSetting('auto_create')) {
$element['#autocreate'] = array(
'bundle' => $this->getAutocreateBundle(),
'uid' => ($entity instanceof EntityOwnerInterface) ? $entity->getOwnerId() : \Drupal::currentUser()->id()
);
}
return array('target_id' => $element);
}
......@@ -110,9 +127,20 @@ public function errorElement(array $element, ConstraintViolationInterface $error
}
/**
* Validates an element.
* {@inheritdoc}
*/
public function elementValidate($element, FormStateInterface $form_state, $form) { }
public function massageFormValues(array $values, array $form, FormStateInterface $form_state) {
foreach ($values as $key => $value) {
// The entity_autocomplete form element returns an array when an entity
// was "autocreated", so we need to move it up a level.
if (is_array($value['target_id'])) {
unset($values[$key]['target_id']);
$values[$key] += $value['target_id'];
}
}
return $values;
}
/**
* Gets the entity labels.
......@@ -144,43 +172,29 @@ protected function getLabels(EntityReferenceFieldItemListInterface $items, $delt
}
/**
* Creates a new entity from a label entered in the autocomplete input.
*
* @param string $label
* The entity label.
* @param int $uid
* The entity uid.
* Returns the name of the bundle which will be used for autocreated entities.
*
* @return \Drupal\Core\Entity\EntityInterface
* @return string
* The bundle name.
*/
protected function createNewEntity($label, $uid) {
$entity_manager = \Drupal::entityManager();
$target_type = $this->getFieldSetting('target_type');
$target_bundles = $this->getSelectionHandlerSetting('target_bundles');
// Get the bundle.
if (!empty($target_bundles)) {
$bundle = reset($target_bundles);
}
else {
$bundles = entity_get_bundles($target_type);
$bundle = reset($bundles);
}
$entity_type = $entity_manager->getDefinition($target_type);
$bundle_key = $entity_type->getKey('bundle');
$label_key = $entity_type->getKey('label');
$entity = $entity_manager->getStorage($target_type)->create(array(
$label_key => $label,
$bundle_key => $bundle,
));
if ($entity instanceof EntityOwnerInterface) {
$entity->setOwnerId($uid);
protected function getAutocreateBundle() {
$bundle = NULL;
if ($this->getSelectionHandlerSetting('auto_create')) {
// If the 'target_bundles' setting is restricted to a single choice, we
// can use that.
if (($target_bundles = $this->getSelectionHandlerSetting('target_bundles')) && count($target_bundles) == 1) {
$bundle = reset($target_bundles);
}
// Otherwise use the first bundle as a fallback.
else {
// @todo Expose a proper UI for choosing the bundle for autocreated
// entities in https://www.drupal.org/node/2412569.
$bundles = entity_get_bundles($this->getFieldSetting('target_type'));
$bundle = key($bundles);
}
}
return $entity;
return $bundle;
}
/**
......@@ -197,15 +211,4 @@ protected function getSelectionHandlerSetting($setting_name) {
return isset($settings[$setting_name]) ? $settings[$setting_name] : NULL;
}
/**
* Checks whether a content entity is referenced.
*
* @return bool
*/
protected function isContentReferenced() {
$target_type = $this->getFieldSetting('target_type');
$target_type_info = \Drupal::entityManager()->getDefinition($target_type);
return $target_type_info->isSubclassOf('\Drupal\Core\Entity\ContentEntityInterface');
}
}
......@@ -25,7 +25,6 @@ content:
settings:
match_operator: CONTAINS
size: 60
autocomplete_type: tags
placeholder: ''
third_party_settings: { }
created:
......
......@@ -36,37 +36,3 @@ entity_reference.default.handler_settings:
auto_create:
type: boolean
label: 'Create referenced entities if they don''t already exist'
field.widget.settings.entity_reference_autocomplete_tags:
type: mapping
label: 'Entity reference autocomplete (Tags style) display format settings'
mapping:
match_operator:
type: string
label: 'Autocomplete matching'
size:
type: integer
label: 'Size of textfield'
autocomplete_type:
type: string
label: 'Autocomplete type'
placeholder:
type: label
label: 'Placeholder'
field.widget.settings.entity_reference_autocomplete:
type: mapping
label: 'Entity reference autocomplete display format settings'
mapping:
match_operator:
type: string
label: 'Autocomplete matching'
size:
type: integer
label: 'Size of textfield'
autocomplete_type:
type: string
label: 'Autocomplete type'
placeholder:
type: label
label: 'Placeholder'
entity_reference.autocomplete:
path: '/entity_reference/autocomplete/{type}/{field_name}/{entity_type}/{bundle_name}/{entity_id}'
defaults:
_controller: '\Drupal\entity_reference\EntityReferenceController::handleAutocomplete'
entity_id: 'NULL'