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: }