WidgetBase.php 21 KB

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