123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598 |
- <?php
- namespace Drupal\Core\Field;
- use Drupal\Component\Utility\Html;
- use Drupal\Component\Utility\NestedArray;
- use Drupal\Component\Utility\SortArray;
- use Drupal\Core\Form\FormStateInterface;
- use Drupal\Core\Render\Element;
- use Symfony\Component\Validator\ConstraintViolationInterface;
- use Symfony\Component\Validator\ConstraintViolationListInterface;
- /**
- * Base class for 'Field widget' plugin implementations.
- *
- * @ingroup field_widget
- */
- abstract class WidgetBase extends PluginSettingsBase implements WidgetInterface {
- use AllowedTagsXssTrait;
- /**
- * The field definition.
- *
- * @var \Drupal\Core\Field\FieldDefinitionInterface
- */
- protected $fieldDefinition;
- /**
- * The widget settings.
- *
- * @var array
- */
- protected $settings;
- /**
- * Constructs a WidgetBase object.
- *
- * @param string $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.
- */
- public function __construct($plugin_id, $plugin_definition, FieldDefinitionInterface $field_definition, array $settings, array $third_party_settings) {
- parent::__construct([], $plugin_id, $plugin_definition);
- $this->fieldDefinition = $field_definition;
- $this->settings = $settings;
- $this->thirdPartySettings = $third_party_settings;
- }
- /**
- * {@inheritdoc}
- */
- 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 = [
- 'items_count' => count($items),
- 'array_parents' => [],
- ];
- static::setWidgetState($parents, $field_name, $form_state, $field_state);
- }
- // Collect widget elements.
- $elements = [];
- // If the widget is handling multiple values (e.g Options), or if we are
- // displaying an individual element, just get a single form element and make
- // it the $delta value.
- if ($this->handlesMultipleValues() || isset($get_delta)) {
- $delta = isset($get_delta) ? $get_delta : 0;
- $element = [
- '#title' => $this->fieldDefinition->getLabel(),
- '#description' => FieldFilteredMarkup::create(\Drupal::token()->replace($this->fieldDefinition->getDescription())),
- ];
- $element = $this->formSingleElement($items, $delta, $element, $form, $form_state);
- if ($element) {
- if (isset($get_delta)) {
- // If we are processing a specific delta value for a field where the
- // field module handles multiples, set the delta in the result.
- $elements[$delta] = $element;
- }
- else {
- // For fields that handle their own processing, we cannot make
- // assumptions about how the field is structured, just merge in the
- // returned element.
- $elements = $element;
- }
- }
- }
- // If the widget does not handle multiple values itself, (and we are not
- // displaying an individual element), process the multiple value form.
- else {
- $elements = $this->formMultipleElements($items, $form, $form_state);
- }
- // Allow modules to alter the field multi-value widget form element.
- // This hook can also be used for single-value fields.
- $context = [
- 'form' => $form,
- 'widget' => $this,
- 'items' => $items,
- 'default' => $this->isDefaultValueWidget($form_state),
- ];
- \Drupal::moduleHandler()->alter([
- 'field_widget_multivalue_form',
- 'field_widget_multivalue_' . $this->getPluginId() . '_form',
- ], $elements, $form_state, $context);
- // Populate the 'array_parents' information in $form_state->get('field')
- // after the form is built, so that we catch changes in the form structure
- // performed in alter() hooks.
- $elements['#after_build'][] = [get_class($this), 'afterBuild'];
- $elements['#field_name'] = $field_name;
- $elements['#field_parents'] = $parents;
- // Enforce the structure of submitted values.
- $elements['#parents'] = array_merge($parents, [$field_name]);
- // Most widgets need their internal structure preserved in submitted values.
- $elements += ['#tree' => TRUE];
- return [
- // Aid in theming of widgets by rendering a classified container.
- '#type' => 'container',
- // Assign a different parent, to keep the main id for the widget itself.
- '#parents' => array_merge($parents, [$field_name . '_wrapper']),
- '#attributes' => [
- 'class' => [
- 'field--type-' . Html::getClass($this->fieldDefinition->getType()),
- 'field--name-' . Html::getClass($field_name),
- 'field--widget-' . Html::getClass($this->getPluginId()),
- ],
- ],
- 'widget' => $elements,
- ];
- }
- /**
- * Special handling to create form elements for multiple values.
- *
- * Handles generic features for multiple fields:
- * - number of widgets
- * - AHAH-'add more' button
- * - table display and drag-n-drop value reordering
- */
- protected function formMultipleElements(FieldItemListInterface $items, array &$form, FormStateInterface $form_state) {
- $field_name = $this->fieldDefinition->getName();
- $cardinality = $this->fieldDefinition->getFieldStorageDefinition()->getCardinality();
- $parents = $form['#parents'];
- // Determine the number of widgets to display.
- switch ($cardinality) {
- case FieldStorageDefinitionInterface::CARDINALITY_UNLIMITED:
- $field_state = static::getWidgetState($parents, $field_name, $form_state);
- $max = $field_state['items_count'];
- $is_multiple = TRUE;
- break;
- default:
- $max = $cardinality - 1;
- $is_multiple = ($cardinality > 1);
- break;
- }
- $title = $this->fieldDefinition->getLabel();
- $description = FieldFilteredMarkup::create(\Drupal::token()->replace($this->fieldDefinition->getDescription()));
- $elements = [];
- for ($delta = 0; $delta <= $max; $delta++) {
- // Add a new empty item if it doesn't exist yet at this delta.
- if (!isset($items[$delta])) {
- $items->appendItem();
- }
- // For multiple fields, title and description are handled by the wrapping
- // table.
- if ($is_multiple) {
- $element = [
- '#title' => $this->t('@title (value @number)', ['@title' => $title, '@number' => $delta + 1]),
- '#title_display' => 'invisible',
- '#description' => '',
- ];
- }
- else {
- $element = [
- '#title' => $title,
- '#title_display' => 'before',
- '#description' => $description,
- ];
- }
- $element = $this->formSingleElement($items, $delta, $element, $form, $form_state);
- if ($element) {
- // Input field for the delta (drag-n-drop reordering).
- if ($is_multiple) {
- // We name the element '_weight' to avoid clashing with elements
- // defined by widget.
- $element['_weight'] = [
- '#type' => 'weight',
- '#title' => $this->t('Weight for row @number', ['@number' => $delta + 1]),
- '#title_display' => 'invisible',
- // Note: this 'delta' is the FAPI #type 'weight' element's property.
- '#delta' => $max,
- '#default_value' => $items[$delta]->_weight ?: $delta,
- '#weight' => 100,
- ];
- }
- $elements[$delta] = $element;
- }
- }
- if ($elements) {
- $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' => $max,
- ];
- // Add 'add more' button, if not working with a programmed form.
- if ($cardinality == FieldStorageDefinitionInterface::CARDINALITY_UNLIMITED && !$form_state->isProgrammed()) {
- $id_prefix = implode('-', array_merge($parents, [$field_name]));
- $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' => t('Add another item'),
- '#attributes' => ['class' => ['field-add-more-submit']],
- '#limit_validation_errors' => [array_merge($parents, [$field_name])],
- '#submit' => [[get_class($this), 'addMoreSubmit']],
- '#ajax' => [
- 'callback' => [get_class($this), 'addMoreAjax'],
- 'wrapper' => $wrapper_id,
- 'effect' => 'fade',
- ],
- ];
- }
- }
- return $elements;
- }
- /**
- * After-build handler for field elements in a form.
- *
- * This stores the final location of the field within the form structure so
- * that flagErrors() can assign validation errors to the right form element.
- */
- public static function afterBuild(array $element, FormStateInterface $form_state) {
- $parents = $element['#field_parents'];
- $field_name = $element['#field_name'];
- $field_state = static::getWidgetState($parents, $field_name, $form_state);
- $field_state['array_parents'] = $element['#array_parents'];
- static::setWidgetState($parents, $field_name, $form_state, $field_state);
- return $element;
- }
- /**
- * Submission handler for the "Add another item" button.
- */
- public static function addMoreSubmit(array $form, FormStateInterface $form_state) {
- $button = $form_state->getTriggeringElement();
- // Go one level up in the form, to the widgets container.
- $element = NestedArray::getValue($form, array_slice($button['#array_parents'], 0, -1));
- $field_name = $element['#field_name'];
- $parents = $element['#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();
- }
- /**
- * Ajax callback for the "Add another item" button.
- *
- * This returns the new page content to replace the page content made obsolete
- * by the form submission.
- */
- public static function addMoreAjax(array $form, FormStateInterface $form_state) {
- $button = $form_state->getTriggeringElement();
- // Go one level up in the form, to the widgets container.
- $element = NestedArray::getValue($form, array_slice($button['#array_parents'], 0, -1));
- // Ensure the widget allows adding additional items.
- if ($element['#cardinality'] != FieldStorageDefinitionInterface::CARDINALITY_UNLIMITED) {
- return;
- }
- // Add a DIV around the delta receiving the Ajax effect.
- $delta = $element['#max_delta'];
- $element[$delta]['#prefix'] = '<div class="ajax-new-content">' . (isset($element[$delta]['#prefix']) ? $element[$delta]['#prefix'] : '');
- $element[$delta]['#suffix'] = (isset($element[$delta]['#suffix']) ? $element[$delta]['#suffix'] : '') . '</div>';
- return $element;
- }
- /**
- * Generates the form element for a single copy of the widget.
- */
- protected function formSingleElement(FieldItemListInterface $items, $delta, array $element, array &$form, FormStateInterface $form_state) {
- $element += [
- '#field_parents' => $form['#parents'],
- // Only the first widget should be required.
- '#required' => $delta == 0 && $this->fieldDefinition->isRequired(),
- '#delta' => $delta,
- '#weight' => $delta,
- ];
- $element = $this->formElement($items, $delta, $element, $form, $form_state);
- if ($element) {
- // Allow modules to alter the field widget form element.
- $context = [
- 'form' => $form,
- 'widget' => $this,
- 'items' => $items,
- 'delta' => $delta,
- 'default' => $this->isDefaultValueWidget($form_state),
- ];
- \Drupal::moduleHandler()->alter(['field_widget_form', 'field_widget_' . $this->getPluginId() . '_form'], $element, $form_state, $context);
- }
- return $element;
- }
- /**
- * {@inheritdoc}
- */
- public function extractFormValues(FieldItemListInterface $items, array $form, FormStateInterface $form_state) {
- $field_name = $this->fieldDefinition->getName();
- // Extract the values from $form_state->getValues().
- $path = array_merge($form['#parents'], [$field_name]);
- $key_exists = NULL;
- $values = NestedArray::getValue($form_state->getValues(), $path, $key_exists);
- if ($key_exists) {
- // Account for drag-and-drop reordering if needed.
- if (!$this->handlesMultipleValues()) {
- // Remove the 'value' of the 'add more' button.
- unset($values['add_more']);
- // The original delta, before drag-and-drop reordering, is needed to
- // route errors to the correct form element.
- foreach ($values as $delta => &$value) {
- $value['_original_delta'] = $delta;
- }
- usort($values, function ($a, $b) {
- return SortArray::sortByKeyInt($a, $b, '_weight');
- });
- }
- // Let the widget massage the submitted values.
- $values = $this->massageFormValues($values, $form, $form_state);
- // Assign the values and remove the empty ones.
- $items->setValue($values);
- $items->filterEmptyItems();
- // Put delta mapping in $form_state, so that flagErrors() can use it.
- $field_state = static::getWidgetState($form['#parents'], $field_name, $form_state);
- foreach ($items as $delta => $item) {
- $field_state['original_deltas'][$delta] = isset($item->_original_delta) ? $item->_original_delta : $delta;
- unset($item->_original_delta, $item->_weight);
- }
- static::setWidgetState($form['#parents'], $field_name, $form_state, $field_state);
- }
- }
- /**
- * {@inheritdoc}
- */
- public function flagErrors(FieldItemListInterface $items, ConstraintViolationListInterface $violations, array $form, FormStateInterface $form_state) {
- $field_name = $this->fieldDefinition->getName();
- $field_state = static::getWidgetState($form['#parents'], $field_name, $form_state);
- if ($violations->count()) {
- // Locate the correct element in the form.
- $element = NestedArray::getValue($form_state->getCompleteForm(), $field_state['array_parents']);
- // Do not report entity-level validation errors if Form API errors have
- // already been reported for the field.
- // @todo Field validation should not be run on fields with FAPI errors to
- // begin with. See https://www.drupal.org/node/2070429.
- $element_path = implode('][', $element['#parents']);
- if ($reported_errors = $form_state->getErrors()) {
- foreach (array_keys($reported_errors) as $error_path) {
- if (strpos($error_path, $element_path) === 0) {
- return;
- }
- }
- }
- // Only set errors if the element is visible.
- if (Element::isVisibleElement($element)) {
- $handles_multiple = $this->handlesMultipleValues();
- $violations_by_delta = $item_list_violations = [];
- foreach ($violations as $violation) {
- // Separate violations by delta.
- $property_path = explode('.', $violation->getPropertyPath());
- $delta = array_shift($property_path);
- if (is_numeric($delta)) {
- $violations_by_delta[$delta][] = $violation;
- }
- // Violations at the ItemList level are not associated to any delta.
- else {
- $item_list_violations[] = $violation;
- }
- $violation->arrayPropertyPath = $property_path;
- }
- /** @var \Symfony\Component\Validator\ConstraintViolationInterface[] $delta_violations */
- foreach ($violations_by_delta as $delta => $delta_violations) {
- // Pass violations to the main element if this is a multiple-value
- // widget.
- if ($handles_multiple) {
- $delta_element = $element;
- }
- // Otherwise, pass errors by delta to the corresponding sub-element.
- else {
- $original_delta = $field_state['original_deltas'][$delta];
- $delta_element = $element[$original_delta];
- }
- foreach ($delta_violations as $violation) {
- // @todo: Pass $violation->arrayPropertyPath as property path.
- $error_element = $this->errorElement($delta_element, $violation, $form, $form_state);
- if ($error_element !== FALSE) {
- $form_state->setError($error_element, $violation->getMessage());
- }
- }
- }
- /** @var \Symfony\Component\Validator\ConstraintViolationInterface[] $item_list_violations */
- // Pass violations to the main element without going through
- // errorElement() if the violations are at the ItemList level.
- foreach ($item_list_violations as $violation) {
- $form_state->setError($element, $violation->getMessage());
- }
- }
- }
- }
- /**
- * {@inheritdoc}
- */
- public static function getWidgetState(array $parents, $field_name, FormStateInterface $form_state) {
- return NestedArray::getValue($form_state->getStorage(), static::getWidgetStateParents($parents, $field_name));
- }
- /**
- * {@inheritdoc}
- */
- public static function setWidgetState(array $parents, $field_name, FormStateInterface $form_state, array $field_state) {
- NestedArray::setValue($form_state->getStorage(), static::getWidgetStateParents($parents, $field_name), $field_state);
- }
- /**
- * Returns the location of processing information within $form_state.
- *
- * @param array $parents
- * The array of #parents where the widget lives in the form.
- * @param string $field_name
- * The field name.
- *
- * @return array
- * The location of processing information within $form_state.
- */
- protected static function getWidgetStateParents(array $parents, $field_name) {
- // Field processing data is placed at
- // $form_state->get(['field_storage', '#parents', ...$parents..., '#fields', $field_name]),
- // to avoid clashes between field names and $parents parts.
- return array_merge(['field_storage', '#parents'], $parents, ['#fields', $field_name]);
- }
- /**
- * {@inheritdoc}
- */
- public function settingsForm(array $form, FormStateInterface $form_state) {
- return [];
- }
- /**
- * {@inheritdoc}
- */
- public function settingsSummary() {
- return [];
- }
- /**
- * {@inheritdoc}
- */
- public function errorElement(array $element, ConstraintViolationInterface $error, array $form, FormStateInterface $form_state) {
- return $element;
- }
- /**
- * {@inheritdoc}
- */
- public function massageFormValues(array $values, array $form, FormStateInterface $form_state) {
- return $values;
- }
- /**
- * Returns the array of field settings.
- *
- * @return array
- * The array of settings.
- */
- protected function getFieldSettings() {
- return $this->fieldDefinition->getSettings();
- }
- /**
- * Returns the value of a field setting.
- *
- * @param string $setting_name
- * The setting name.
- *
- * @return mixed
- * The setting value.
- */
- protected function getFieldSetting($setting_name) {
- return $this->fieldDefinition->getSetting($setting_name);
- }
- /**
- * Returns whether the widget handles multiple values.
- *
- * @return bool
- * TRUE if a single copy of formElement() can handle multiple field values,
- * FALSE if multiple values require separate copies of formElement().
- */
- protected function handlesMultipleValues() {
- $definition = $this->getPluginDefinition();
- return $definition['multiple_values'];
- }
- /**
- * {@inheritdoc}
- */
- public static function isApplicable(FieldDefinitionInterface $field_definition) {
- // By default, widgets are available for all fields.
- return TRUE;
- }
- /**
- * Returns whether the widget used for default value form.
- *
- * @param \Drupal\Core\Form\FormStateInterface $form_state
- * The current state of the form.
- *
- * @return bool
- * TRUE if a widget used to input default value, FALSE otherwise.
- */
- protected function isDefaultValueWidget(FormStateInterface $form_state) {
- return (bool) $form_state->get('default_value_widget');
- }
- /**
- * Returns the filtered field description.
- *
- * @return \Drupal\Core\Field\FieldFilteredMarkup
- * The filtered field description, with tokens replaced.
- */
- protected function getFilteredDescription() {
- return FieldFilteredMarkup::create(\Drupal::token()->replace($this->fieldDefinition->getDescription()));
- }
- }
|