Newer
Older
<?php
/**
* @file
Alex Pott
committed
* Contains \Drupal\Core\Field\WidgetBase.
*/
Alex Pott
committed
namespace Drupal\Core\Field;
Dries Buytaert
committed
use Drupal\Component\Utility\NestedArray;
Alex Pott
committed
use Drupal\Component\Utility\SortArray;
use Drupal\Component\Utility\String;
use Symfony\Component\Validator\ConstraintViolationInterface;
use Symfony\Component\Validator\ConstraintViolationListInterface;
/**
* Base class for 'Field widget' plugin implementations.
Jennifer Hodgdon
committed
*
* @ingroup field_widget
*/
abstract class WidgetBase extends PluginSettingsBase implements WidgetInterface {
/**
* The field definition.
*
Alex Pott
committed
* @var \Drupal\Core\Field\FieldDefinitionInterface
*/
protected $fieldDefinition;
/**
* The widget settings.
*
* @var array
*/
protected $settings;
/**
* Constructs a WidgetBase object.
*
* @param array $plugin_id
* The plugin_id for the widget.
* @param mixed $plugin_definition
Angie Byron
committed
* The plugin implementation definition.
Alex Pott
committed
* @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 settings.
*/
public function __construct($plugin_id, $plugin_definition, FieldDefinitionInterface $field_definition, array $settings, array $third_party_settings) {
Angie Byron
committed
parent::__construct(array(), $plugin_id, $plugin_definition);
$this->fieldDefinition = $field_definition;
$this->settings = $settings;
$this->thirdPartySettings = $third_party_settings;
}
/**
* {@inheritdoc}
*/
public function form(FieldItemListInterface $items, array &$form, array &$form_state, $get_delta = NULL) {
Alex Pott
committed
$field_name = $this->fieldDefinition->getName();
$parents = $form['#parents'];
// Store field information in $form_state.
Alex Pott
committed
if (!static::getWidgetState($parents, $field_name, $form_state)) {
$field_state = array(
'items_count' => count($items),
'array_parents' => array(),
);
Alex Pott
committed
static::setWidgetState($parents, $field_name, $form_state, $field_state);
}
// Collect widget elements.
$elements = array();
// 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.
Alex Pott
committed
if ($this->handlesMultipleValues() || isset($get_delta)) {
$delta = isset($get_delta) ? $get_delta : 0;
$element = array(
'#title' => String::checkPlain($this->fieldDefinition->getLabel()),
Alex Pott
committed
'#description' => field_filter_xss(\Drupal::token()->replace($this->fieldDefinition->getDescription())),
);
Angie Byron
committed
$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 {
Angie Byron
committed
$elements = $this->formMultipleElements($items, $form, $form_state);
}
// Populate the 'array_parents' information in $form_state['field'] after
Alex Pott
committed
// the form is built, so that we catch changes in the form structure
// performed in alter() hooks.
$elements['#after_build'][] = array(get_class($this), 'afterBuild');
$elements['#field_name'] = $field_name;
$elements['#field_parents'] = $parents;
Angie Byron
committed
// Enforce the structure of submitted values.
$elements['#parents'] = array_merge($parents, array($field_name));
// Most widgets need their internal structure preserved in submitted values.
$elements += array('#tree' => TRUE);
return array(
// 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, array($field_name . '_wrapper')),
'#attributes' => array(
'class' => array(
'field-type-' . drupal_html_class($this->fieldDefinition->getType()),
'field-name-' . drupal_html_class($field_name),
'field-widget-' . drupal_html_class($this->getPluginId()),
Angie Byron
committed
),
),
'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, array &$form_state) {
Alex Pott
committed
$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:
Alex Pott
committed
$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 = String::checkPlain($this->fieldDefinition->getLabel());
Alex Pott
committed
$description = field_filter_xss(\Drupal::token()->replace($this->fieldDefinition->getDescription()));
$elements = array();
for ($delta = 0; $delta <= $max; $delta++) {
// For multiple fields, title and description are handled by the wrapping
// table.
$element = array(
'#title' => $is_multiple ? '' : $title,
'#description' => $is_multiple ? '' : $description,
);
Angie Byron
committed
$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'] = array(
'#type' => 'weight',
'#title' => t('Weight for row @number', array('@number' => $delta + 1)),
'#title_display' => 'invisible',
// Note: this 'delta' is the FAPI #type 'weight' element's property.
'#delta' => $max,
Dries Buytaert
committed
'#default_value' => $items[$delta]->_weight ?: $delta,
'#weight' => 100,
);
}
$elements[$delta] = $element;
}
}
if ($elements) {
$elements += array(
'#theme' => 'field_multiple_value_form',
'#field_name' => $field_name,
'#cardinality' => $cardinality,
'#cardinality_multiple' => $this->fieldDefinition->getFieldStorageDefinition()->isMultiple(),
Alex Pott
committed
'#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 && empty($form_state['programmed'])) {
Angie Byron
committed
$id_prefix = implode('-', array_merge($parents, array($field_name)));
$wrapper_id = drupal_html_id($id_prefix . '-add-more-wrapper');
$elements['#prefix'] = '<div id="' . $wrapper_id . '">';
$elements['#suffix'] = '</div>';
$elements['add_more'] = array(
'#type' => 'submit',
'#name' => strtr($id_prefix, '-', '_') . '_add_more',
'#value' => t('Add another item'),
'#attributes' => array('class' => array('field-add-more-submit')),
Angie Byron
committed
'#limit_validation_errors' => array(array_merge($parents, array($field_name))),
'#submit' => array(array(get_class($this), 'addMoreSubmit')),
'#ajax' => array(
'callback' => array(get_class($this), 'addMoreAjax'),
'wrapper' => $wrapper_id,
'effect' => 'fade',
),
);
}
}
return $elements;
}
Alex Pott
committed
/**
* 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, array &$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, array &$form_state) {
$button = $form_state['triggering_element'];
// 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.
Alex Pott
committed
$field_state = static::getWidgetState($parents, $field_name, $form_state);
$field_state['items_count']++;
Alex Pott
committed
static::setWidgetState($parents, $field_name, $form_state, $field_state);
$form_state['rebuild'] = TRUE;
}
/**
* 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, array $form_state) {
$button = $form_state['triggering_element'];
// 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, array &$form_state) {
Angie Byron
committed
$entity = $items->getEntity();
$element += array(
'#field_parents' => $form['#parents'],
// Only the first widget should be required.
Alex Pott
committed
'#required' => $delta == 0 && $this->fieldDefinition->isRequired(),
'#delta' => $delta,
'#weight' => $delta,
);
Angie Byron
committed
$element = $this->formElement($items, $delta, $element, $form, $form_state);
if ($element) {
// Allow modules to alter the field widget form element.
$context = array(
'form' => $form,
'widget' => $this,
'items' => $items,
'delta' => $delta,
'default' => !empty($entity->field_ui_default_value),
);
\Drupal::moduleHandler()->alter(array('field_widget_form', 'field_widget_' . $this->getPluginId() . '_form'), $element, $form_state, $context);
}
return $element;
}
/**
* {@inheritdoc}
*/
public function extractFormValues(FieldItemListInterface $items, array $form, array &$form_state) {
Alex Pott
committed
$field_name = $this->fieldDefinition->getName();
// Extract the values from $form_state['values'].
Angie Byron
committed
$path = array_merge($form['#parents'], array($field_name));
$key_exists = NULL;
Dries Buytaert
committed
$values = NestedArray::getValue($form_state['values'], $path, $key_exists);
if ($key_exists) {
Alex Pott
committed
// 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 corect form element.
foreach ($values as $delta => &$value) {
$value['_original_delta'] = $delta;
}
Alex Pott
committed
usort($values, function ($a, $b) {
return SortArray::sortByKeyInt($a, $b, '_weight');
});
}
Alex Pott
committed
// Let the widget massage the submitted values.
$values = $this->massageFormValues($values, $form, $form_state);
Alex Pott
committed
// 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.
Alex Pott
committed
$field_state = static::getWidgetState($form['#parents'], $field_name, $form_state);
Dries Buytaert
committed
foreach ($items as $delta => $item) {
Alex Pott
committed
$field_state['original_deltas'][$delta] = isset($item->_original_delta) ? $item->_original_delta : $delta;
unset($item->_original_delta, $item->_weight);
}
Alex Pott
committed
static::setWidgetState($form['#parents'], $field_name, $form_state, $field_state);
}
}
/**
* {@inheritdoc}
*/
public function flagErrors(FieldItemListInterface $items, ConstraintViolationListInterface $violations, array $form, array &$form_state) {
Alex Pott
committed
$field_name = $this->fieldDefinition->getName();
Alex Pott
committed
$field_state = static::getWidgetState($form['#parents'], $field_name, $form_state);
if ($violations->count()) {
$form_builder = \Drupal::formBuilder();
// Locate the correct element in the the form.
Dries Buytaert
committed
$element = NestedArray::getValue($form_state['complete_form'], $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://drupal.org/node/2070429.
$element_path = implode('][', $element['#parents']);
if ($reported_errors = $form_builder->getErrors($form_state)) {
foreach (array_keys($reported_errors) as $error_path) {
if (strpos($error_path, $element_path) === 0) {
return;
}
}
}
// Only set errors if the element is accessible.
if (!isset($element['#access']) || $element['#access']) {
Alex Pott
committed
$handles_multiple = $this->handlesMultipleValues();
$violations_by_delta = array();
foreach ($violations as $violation) {
// Separate violations by delta.
$property_path = explode('.', $violation->getPropertyPath());
$delta = array_shift($property_path);
// Violations at the ItemList level are not associated to any delta,
// we file them under $delta NULL.
$delta = is_numeric($delta) ? $delta : NULL;
$violations_by_delta[$delta][] = $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,
// - or if the violations are at the ItemList level.
Alex Pott
committed
if ($handles_multiple || $delta === NULL) {
$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);
Alex Pott
committed
if ($error_element !== FALSE) {
$form_builder->setError($error_element, $form_state, $violation->getMessage());
Alex Pott
committed
}
}
}
}
}
}
Alex Pott
committed
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
/**
* {@inheritdoc}
*/
public static function getWidgetState(array $parents, $field_name, array &$form_state) {
return NestedArray::getValue($form_state, static::getWidgetStateParents($parents, $field_name));
}
/**
* {@inheritdoc}
*/
public static function setWidgetState(array $parents, $field_name, array &$form_state, array $field_state) {
NestedArray::setValue($form_state, 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['field']['#parents'][...$parents...]['#fields'][$field_name],
// to avoid clashes between field names and $parents parts.
return array_merge(array('field', '#parents'), $parents, array('#fields', $field_name));
}
/**
Alex Pott
committed
* {@inheritdoc}
*/
public function settingsForm(array $form, array &$form_state) {
return array();
}
Alex Pott
committed
/**
* {@inheritdoc}
*/
public function settingsSummary() {
return array();
}
/**
Alex Pott
committed
* {@inheritdoc}
*/
public function errorElement(array $element, ConstraintViolationInterface $error, array $form, array &$form_state) {
return $element;
}
/**
Alex Pott
committed
* {@inheritdoc}
*/
public function massageFormValues(array $values, array $form, array &$form_state) {
return $values;
}
/**
* Returns the array of field settings.
*
* @return array
* The array of settings.
*/
protected function getFieldSettings() {
Alex Pott
committed
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) {
Alex Pott
committed
return $this->fieldDefinition->getSetting($setting_name);
}
Alex Pott
committed
/**
* 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'];
}