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