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