Updated Drupal to 8.6. This goes with the following updates because it's possible...
[yaffs-website] / web / core / lib / Drupal / Core / Field / WidgetBase.php
1 <?php
2
3 namespace Drupal\Core\Field;
4
5 use Drupal\Component\Utility\Html;
6 use Drupal\Component\Utility\NestedArray;
7 use Drupal\Component\Utility\SortArray;
8 use Drupal\Core\Form\FormStateInterface;
9 use Drupal\Core\Render\Element;
10 use Symfony\Component\Validator\ConstraintViolationInterface;
11 use Symfony\Component\Validator\ConstraintViolationListInterface;
12
13 /**
14  * Base class for 'Field widget' plugin implementations.
15  *
16  * @ingroup field_widget
17  */
18 abstract class WidgetBase extends PluginSettingsBase implements WidgetInterface {
19
20   use AllowedTagsXssTrait;
21
22   /**
23    * The field definition.
24    *
25    * @var \Drupal\Core\Field\FieldDefinitionInterface
26    */
27   protected $fieldDefinition;
28
29   /**
30    * The widget settings.
31    *
32    * @var array
33    */
34   protected $settings;
35
36   /**
37    * Constructs a WidgetBase object.
38    *
39    * @param string $plugin_id
40    *   The plugin_id for the widget.
41    * @param mixed $plugin_definition
42    *   The plugin implementation definition.
43    * @param \Drupal\Core\Field\FieldDefinitionInterface $field_definition
44    *   The definition of the field to which the widget is associated.
45    * @param array $settings
46    *   The widget settings.
47    * @param array $third_party_settings
48    *   Any third party settings.
49    */
50   public function __construct($plugin_id, $plugin_definition, FieldDefinitionInterface $field_definition, array $settings, array $third_party_settings) {
51     parent::__construct([], $plugin_id, $plugin_definition);
52     $this->fieldDefinition = $field_definition;
53     $this->settings = $settings;
54     $this->thirdPartySettings = $third_party_settings;
55   }
56
57   /**
58    * {@inheritdoc}
59    */
60   public function form(FieldItemListInterface $items, array &$form, FormStateInterface $form_state, $get_delta = NULL) {
61     $field_name = $this->fieldDefinition->getName();
62     $parents = $form['#parents'];
63
64     // Store field information in $form_state.
65     if (!static::getWidgetState($parents, $field_name, $form_state)) {
66       $field_state = [
67         'items_count' => count($items),
68         'array_parents' => [],
69       ];
70       static::setWidgetState($parents, $field_name, $form_state, $field_state);
71     }
72
73     // Collect widget elements.
74     $elements = [];
75
76     // If the widget is handling multiple values (e.g Options), or if we are
77     // displaying an individual element, just get a single form element and make
78     // it the $delta value.
79     if ($this->handlesMultipleValues() || isset($get_delta)) {
80       $delta = isset($get_delta) ? $get_delta : 0;
81       $element = [
82         '#title' => $this->fieldDefinition->getLabel(),
83         '#description' => FieldFilteredMarkup::create(\Drupal::token()->replace($this->fieldDefinition->getDescription())),
84       ];
85       $element = $this->formSingleElement($items, $delta, $element, $form, $form_state);
86
87       if ($element) {
88         if (isset($get_delta)) {
89           // If we are processing a specific delta value for a field where the
90           // field module handles multiples, set the delta in the result.
91           $elements[$delta] = $element;
92         }
93         else {
94           // For fields that handle their own processing, we cannot make
95           // assumptions about how the field is structured, just merge in the
96           // returned element.
97           $elements = $element;
98         }
99       }
100     }
101     // If the widget does not handle multiple values itself, (and we are not
102     // displaying an individual element), process the multiple value form.
103     else {
104       $elements = $this->formMultipleElements($items, $form, $form_state);
105     }
106
107     // Allow modules to alter the field multi-value widget form element.
108     // This hook can also be used for single-value fields.
109     $context = [
110       'form' => $form,
111       'widget' => $this,
112       'items' => $items,
113       'default' => $this->isDefaultValueWidget($form_state),
114     ];
115     \Drupal::moduleHandler()->alter([
116       'field_widget_multivalue_form',
117       'field_widget_multivalue_' . $this->getPluginId() . '_form',
118     ], $elements, $form_state, $context);
119
120     // Populate the 'array_parents' information in $form_state->get('field')
121     // after the form is built, so that we catch changes in the form structure
122     // performed in alter() hooks.
123     $elements['#after_build'][] = [get_class($this), 'afterBuild'];
124     $elements['#field_name'] = $field_name;
125     $elements['#field_parents'] = $parents;
126     // Enforce the structure of submitted values.
127     $elements['#parents'] = array_merge($parents, [$field_name]);
128     // Most widgets need their internal structure preserved in submitted values.
129     $elements += ['#tree' => TRUE];
130
131     return [
132       // Aid in theming of widgets by rendering a classified container.
133       '#type' => 'container',
134       // Assign a different parent, to keep the main id for the widget itself.
135       '#parents' => array_merge($parents, [$field_name . '_wrapper']),
136       '#attributes' => [
137         'class' => [
138           'field--type-' . Html::getClass($this->fieldDefinition->getType()),
139           'field--name-' . Html::getClass($field_name),
140           'field--widget-' . Html::getClass($this->getPluginId()),
141         ],
142       ],
143       'widget' => $elements,
144     ];
145   }
146
147   /**
148    * Special handling to create form elements for multiple values.
149    *
150    * Handles generic features for multiple fields:
151    * - number of widgets
152    * - AHAH-'add more' button
153    * - table display and drag-n-drop value reordering
154    */
155   protected function formMultipleElements(FieldItemListInterface $items, array &$form, FormStateInterface $form_state) {
156     $field_name = $this->fieldDefinition->getName();
157     $cardinality = $this->fieldDefinition->getFieldStorageDefinition()->getCardinality();
158     $parents = $form['#parents'];
159
160     // Determine the number of widgets to display.
161     switch ($cardinality) {
162       case FieldStorageDefinitionInterface::CARDINALITY_UNLIMITED:
163         $field_state = static::getWidgetState($parents, $field_name, $form_state);
164         $max = $field_state['items_count'];
165         $is_multiple = TRUE;
166         break;
167
168       default:
169         $max = $cardinality - 1;
170         $is_multiple = ($cardinality > 1);
171         break;
172     }
173
174     $title = $this->fieldDefinition->getLabel();
175     $description = FieldFilteredMarkup::create(\Drupal::token()->replace($this->fieldDefinition->getDescription()));
176
177     $elements = [];
178
179     for ($delta = 0; $delta <= $max; $delta++) {
180       // Add a new empty item if it doesn't exist yet at this delta.
181       if (!isset($items[$delta])) {
182         $items->appendItem();
183       }
184
185       // For multiple fields, title and description are handled by the wrapping
186       // table.
187       if ($is_multiple) {
188         $element = [
189           '#title' => $this->t('@title (value @number)', ['@title' => $title, '@number' => $delta + 1]),
190           '#title_display' => 'invisible',
191           '#description' => '',
192         ];
193       }
194       else {
195         $element = [
196           '#title' => $title,
197           '#title_display' => 'before',
198           '#description' => $description,
199         ];
200       }
201
202       $element = $this->formSingleElement($items, $delta, $element, $form, $form_state);
203
204       if ($element) {
205         // Input field for the delta (drag-n-drop reordering).
206         if ($is_multiple) {
207           // We name the element '_weight' to avoid clashing with elements
208           // defined by widget.
209           $element['_weight'] = [
210             '#type' => 'weight',
211             '#title' => $this->t('Weight for row @number', ['@number' => $delta + 1]),
212             '#title_display' => 'invisible',
213             // Note: this 'delta' is the FAPI #type 'weight' element's property.
214             '#delta' => $max,
215             '#default_value' => $items[$delta]->_weight ?: $delta,
216             '#weight' => 100,
217           ];
218         }
219
220         $elements[$delta] = $element;
221       }
222     }
223
224     if ($elements) {
225       $elements += [
226         '#theme' => 'field_multiple_value_form',
227         '#field_name' => $field_name,
228         '#cardinality' => $cardinality,
229         '#cardinality_multiple' => $this->fieldDefinition->getFieldStorageDefinition()->isMultiple(),
230         '#required' => $this->fieldDefinition->isRequired(),
231         '#title' => $title,
232         '#description' => $description,
233         '#max_delta' => $max,
234       ];
235
236       // Add 'add more' button, if not working with a programmed form.
237       if ($cardinality == FieldStorageDefinitionInterface::CARDINALITY_UNLIMITED && !$form_state->isProgrammed()) {
238         $id_prefix = implode('-', array_merge($parents, [$field_name]));
239         $wrapper_id = Html::getUniqueId($id_prefix . '-add-more-wrapper');
240         $elements['#prefix'] = '<div id="' . $wrapper_id . '">';
241         $elements['#suffix'] = '</div>';
242
243         $elements['add_more'] = [
244           '#type' => 'submit',
245           '#name' => strtr($id_prefix, '-', '_') . '_add_more',
246           '#value' => t('Add another item'),
247           '#attributes' => ['class' => ['field-add-more-submit']],
248           '#limit_validation_errors' => [array_merge($parents, [$field_name])],
249           '#submit' => [[get_class($this), 'addMoreSubmit']],
250           '#ajax' => [
251             'callback' => [get_class($this), 'addMoreAjax'],
252             'wrapper' => $wrapper_id,
253             'effect' => 'fade',
254           ],
255         ];
256       }
257     }
258
259     return $elements;
260   }
261
262   /**
263    * After-build handler for field elements in a form.
264    *
265    * This stores the final location of the field within the form structure so
266    * that flagErrors() can assign validation errors to the right form element.
267    */
268   public static function afterBuild(array $element, FormStateInterface $form_state) {
269     $parents = $element['#field_parents'];
270     $field_name = $element['#field_name'];
271
272     $field_state = static::getWidgetState($parents, $field_name, $form_state);
273     $field_state['array_parents'] = $element['#array_parents'];
274     static::setWidgetState($parents, $field_name, $form_state, $field_state);
275
276     return $element;
277   }
278
279   /**
280    * Submission handler for the "Add another item" button.
281    */
282   public static function addMoreSubmit(array $form, FormStateInterface $form_state) {
283     $button = $form_state->getTriggeringElement();
284
285     // Go one level up in the form, to the widgets container.
286     $element = NestedArray::getValue($form, array_slice($button['#array_parents'], 0, -1));
287     $field_name = $element['#field_name'];
288     $parents = $element['#field_parents'];
289
290     // Increment the items count.
291     $field_state = static::getWidgetState($parents, $field_name, $form_state);
292     $field_state['items_count']++;
293     static::setWidgetState($parents, $field_name, $form_state, $field_state);
294
295     $form_state->setRebuild();
296   }
297
298   /**
299    * Ajax callback for the "Add another item" button.
300    *
301    * This returns the new page content to replace the page content made obsolete
302    * by the form submission.
303    */
304   public static function addMoreAjax(array $form, FormStateInterface $form_state) {
305     $button = $form_state->getTriggeringElement();
306
307     // Go one level up in the form, to the widgets container.
308     $element = NestedArray::getValue($form, array_slice($button['#array_parents'], 0, -1));
309
310     // Ensure the widget allows adding additional items.
311     if ($element['#cardinality'] != FieldStorageDefinitionInterface::CARDINALITY_UNLIMITED) {
312       return;
313     }
314
315     // Add a DIV around the delta receiving the Ajax effect.
316     $delta = $element['#max_delta'];
317     $element[$delta]['#prefix'] = '<div class="ajax-new-content">' . (isset($element[$delta]['#prefix']) ? $element[$delta]['#prefix'] : '');
318     $element[$delta]['#suffix'] = (isset($element[$delta]['#suffix']) ? $element[$delta]['#suffix'] : '') . '</div>';
319
320     return $element;
321   }
322
323   /**
324    * Generates the form element for a single copy of the widget.
325    */
326   protected function formSingleElement(FieldItemListInterface $items, $delta, array $element, array &$form, FormStateInterface $form_state) {
327     $element += [
328       '#field_parents' => $form['#parents'],
329       // Only the first widget should be required.
330       '#required' => $delta == 0 && $this->fieldDefinition->isRequired(),
331       '#delta' => $delta,
332       '#weight' => $delta,
333     ];
334
335     $element = $this->formElement($items, $delta, $element, $form, $form_state);
336
337     if ($element) {
338       // Allow modules to alter the field widget form element.
339       $context = [
340         'form' => $form,
341         'widget' => $this,
342         'items' => $items,
343         'delta' => $delta,
344         'default' => $this->isDefaultValueWidget($form_state),
345       ];
346       \Drupal::moduleHandler()->alter(['field_widget_form', 'field_widget_' . $this->getPluginId() . '_form'], $element, $form_state, $context);
347     }
348
349     return $element;
350   }
351
352   /**
353    * {@inheritdoc}
354    */
355   public function extractFormValues(FieldItemListInterface $items, array $form, FormStateInterface $form_state) {
356     $field_name = $this->fieldDefinition->getName();
357
358     // Extract the values from $form_state->getValues().
359     $path = array_merge($form['#parents'], [$field_name]);
360     $key_exists = NULL;
361     $values = NestedArray::getValue($form_state->getValues(), $path, $key_exists);
362
363     if ($key_exists) {
364       // Account for drag-and-drop reordering if needed.
365       if (!$this->handlesMultipleValues()) {
366         // Remove the 'value' of the 'add more' button.
367         unset($values['add_more']);
368
369         // The original delta, before drag-and-drop reordering, is needed to
370         // route errors to the correct form element.
371         foreach ($values as $delta => &$value) {
372           $value['_original_delta'] = $delta;
373         }
374
375         usort($values, function ($a, $b) {
376           return SortArray::sortByKeyInt($a, $b, '_weight');
377         });
378       }
379
380       // Let the widget massage the submitted values.
381       $values = $this->massageFormValues($values, $form, $form_state);
382
383       // Assign the values and remove the empty ones.
384       $items->setValue($values);
385       $items->filterEmptyItems();
386
387       // Put delta mapping in $form_state, so that flagErrors() can use it.
388       $field_state = static::getWidgetState($form['#parents'], $field_name, $form_state);
389       foreach ($items as $delta => $item) {
390         $field_state['original_deltas'][$delta] = isset($item->_original_delta) ? $item->_original_delta : $delta;
391         unset($item->_original_delta, $item->_weight);
392       }
393       static::setWidgetState($form['#parents'], $field_name, $form_state, $field_state);
394     }
395   }
396
397   /**
398    * {@inheritdoc}
399    */
400   public function flagErrors(FieldItemListInterface $items, ConstraintViolationListInterface $violations, array $form, FormStateInterface $form_state) {
401     $field_name = $this->fieldDefinition->getName();
402
403     $field_state = static::getWidgetState($form['#parents'], $field_name, $form_state);
404
405     if ($violations->count()) {
406       // Locate the correct element in the form.
407       $element = NestedArray::getValue($form_state->getCompleteForm(), $field_state['array_parents']);
408
409       // Do not report entity-level validation errors if Form API errors have
410       // already been reported for the field.
411       // @todo Field validation should not be run on fields with FAPI errors to
412       //   begin with. See https://www.drupal.org/node/2070429.
413       $element_path = implode('][', $element['#parents']);
414       if ($reported_errors = $form_state->getErrors()) {
415         foreach (array_keys($reported_errors) as $error_path) {
416           if (strpos($error_path, $element_path) === 0) {
417             return;
418           }
419         }
420       }
421
422       // Only set errors if the element is visible.
423       if (Element::isVisibleElement($element)) {
424         $handles_multiple = $this->handlesMultipleValues();
425
426         $violations_by_delta = $item_list_violations = [];
427         foreach ($violations as $violation) {
428           // Separate violations by delta.
429           $property_path = explode('.', $violation->getPropertyPath());
430           $delta = array_shift($property_path);
431           if (is_numeric($delta)) {
432             $violations_by_delta[$delta][] = $violation;
433           }
434           // Violations at the ItemList level are not associated to any delta.
435           else {
436             $item_list_violations[] = $violation;
437           }
438           $violation->arrayPropertyPath = $property_path;
439         }
440
441         /** @var \Symfony\Component\Validator\ConstraintViolationInterface[] $delta_violations */
442         foreach ($violations_by_delta as $delta => $delta_violations) {
443           // Pass violations to the main element if this is a multiple-value
444           // widget.
445           if ($handles_multiple) {
446             $delta_element = $element;
447           }
448           // Otherwise, pass errors by delta to the corresponding sub-element.
449           else {
450             $original_delta = $field_state['original_deltas'][$delta];
451             $delta_element = $element[$original_delta];
452           }
453           foreach ($delta_violations as $violation) {
454             // @todo: Pass $violation->arrayPropertyPath as property path.
455             $error_element = $this->errorElement($delta_element, $violation, $form, $form_state);
456             if ($error_element !== FALSE) {
457               $form_state->setError($error_element, $violation->getMessage());
458             }
459           }
460         }
461
462         /** @var \Symfony\Component\Validator\ConstraintViolationInterface[] $item_list_violations */
463         // Pass violations to the main element without going through
464         // errorElement() if the violations are at the ItemList level.
465         foreach ($item_list_violations as $violation) {
466           $form_state->setError($element, $violation->getMessage());
467         }
468       }
469     }
470   }
471
472   /**
473    * {@inheritdoc}
474    */
475   public static function getWidgetState(array $parents, $field_name, FormStateInterface $form_state) {
476     return NestedArray::getValue($form_state->getStorage(), static::getWidgetStateParents($parents, $field_name));
477   }
478
479   /**
480    * {@inheritdoc}
481    */
482   public static function setWidgetState(array $parents, $field_name, FormStateInterface $form_state, array $field_state) {
483     NestedArray::setValue($form_state->getStorage(), static::getWidgetStateParents($parents, $field_name), $field_state);
484   }
485
486   /**
487    * Returns the location of processing information within $form_state.
488    *
489    * @param array $parents
490    *   The array of #parents where the widget lives in the form.
491    * @param string $field_name
492    *   The field name.
493    *
494    * @return array
495    *   The location of processing information within $form_state.
496    */
497   protected static function getWidgetStateParents(array $parents, $field_name) {
498     // Field processing data is placed at
499     // $form_state->get(['field_storage', '#parents', ...$parents..., '#fields', $field_name]),
500     // to avoid clashes between field names and $parents parts.
501     return array_merge(['field_storage', '#parents'], $parents, ['#fields', $field_name]);
502   }
503
504   /**
505    * {@inheritdoc}
506    */
507   public function settingsForm(array $form, FormStateInterface $form_state) {
508     return [];
509   }
510
511   /**
512    * {@inheritdoc}
513    */
514   public function settingsSummary() {
515     return [];
516   }
517
518   /**
519    * {@inheritdoc}
520    */
521   public function errorElement(array $element, ConstraintViolationInterface $error, array $form, FormStateInterface $form_state) {
522     return $element;
523   }
524
525   /**
526    * {@inheritdoc}
527    */
528   public function massageFormValues(array $values, array $form, FormStateInterface $form_state) {
529     return $values;
530   }
531
532   /**
533    * Returns the array of field settings.
534    *
535    * @return array
536    *   The array of settings.
537    */
538   protected function getFieldSettings() {
539     return $this->fieldDefinition->getSettings();
540   }
541
542   /**
543    * Returns the value of a field setting.
544    *
545    * @param string $setting_name
546    *   The setting name.
547    *
548    * @return mixed
549    *   The setting value.
550    */
551   protected function getFieldSetting($setting_name) {
552     return $this->fieldDefinition->getSetting($setting_name);
553   }
554
555   /**
556    * Returns whether the widget handles multiple values.
557    *
558    * @return bool
559    *   TRUE if a single copy of formElement() can handle multiple field values,
560    *   FALSE if multiple values require separate copies of formElement().
561    */
562   protected function handlesMultipleValues() {
563     $definition = $this->getPluginDefinition();
564     return $definition['multiple_values'];
565   }
566
567   /**
568    * {@inheritdoc}
569    */
570   public static function isApplicable(FieldDefinitionInterface $field_definition) {
571     // By default, widgets are available for all fields.
572     return TRUE;
573   }
574
575   /**
576    * Returns whether the widget used for default value form.
577    *
578    * @param \Drupal\Core\Form\FormStateInterface $form_state
579    *   The current state of the form.
580    *
581    * @return bool
582    *   TRUE if a widget used to input default value, FALSE otherwise.
583    */
584   protected function isDefaultValueWidget(FormStateInterface $form_state) {
585     return (bool) $form_state->get('default_value_widget');
586   }
587
588   /**
589    * Returns the filtered field description.
590    *
591    * @return \Drupal\Core\Field\FieldFilteredMarkup
592    *   The filtered field description, with tokens replaced.
593    */
594   protected function getFilteredDescription() {
595     return FieldFilteredMarkup::create(\Drupal::token()->replace($this->fieldDefinition->getDescription()));
596   }
597
598 }