3 namespace Drupal\paragraphs\Plugin\Field\FieldWidget;
5 use Drupal\Component\Utility\NestedArray;
6 use Drupal\Component\Utility\Html;
7 use Drupal\Core\Entity\Entity\EntityFormDisplay;
8 use Drupal\Core\Entity\EntityInterface;
9 use Drupal\Core\Entity\EntityReferenceSelection\SelectionPluginManagerInterface;
10 use Drupal\Core\Entity\RevisionableInterface;
11 use Drupal\Core\Field\FieldDefinitionInterface;
12 use Drupal\Core\Field\FieldFilteredMarkup;
13 use Drupal\Core\Field\FieldStorageDefinitionInterface;
14 use Drupal\Core\Field\WidgetBase;
15 use Drupal\Core\Form\FormStateInterface;
16 use Drupal\Core\Field\FieldItemListInterface;
17 use Drupal\Core\Render\Element;
18 use Drupal\node\Entity\Node;
19 use Drupal\paragraphs;
20 use Drupal\paragraphs\ParagraphInterface;
21 use Symfony\Component\Validator\ConstraintViolationInterface;
22 use Drupal\paragraphs\Plugin\EntityReferenceSelection\ParagraphSelection;
26 * Plugin implementation of the 'entity_reference paragraphs' widget.
28 * We hide add / remove buttons when translating to avoid accidental loss of
29 * data because these actions effect all languages.
32 * id = "entity_reference_paragraphs",
33 * label = @Translation("Paragraphs Classic"),
34 * description = @Translation("A paragraphs inline form widget."),
36 * "entity_reference_revisions"
40 class InlineParagraphsWidget extends WidgetBase {
43 * Indicates whether the current widget instance is in translation.
47 private $isTranslating;
50 * Id to name ajax buttons that includes field parents and field name.
54 protected $fieldIdPrefix;
57 * Wrapper id to identify the paragraphs.
61 protected $fieldWrapperId;
64 * Number of paragraphs item on form.
68 protected $realItemCount;
71 * Parents for the current paragraph.
75 protected $fieldParents;
78 * Accessible paragraphs types.
82 protected $accessOptions = NULL;
87 public static function defaultSettings() {
89 'title' => t('Paragraph'),
90 'title_plural' => t('Paragraphs'),
91 'edit_mode' => 'open',
92 'add_mode' => 'dropdown',
93 'form_display_mode' => 'default',
94 'default_paragraph_type' => '',
101 public function settingsForm(array $form, FormStateInterface $form_state) {
104 $elements['title'] = array(
105 '#type' => 'textfield',
106 '#title' => $this->t('Paragraph Title'),
107 '#description' => $this->t('Label to appear as title on the button as "Add new [title]", this label is translatable'),
108 '#default_value' => $this->getSetting('title'),
112 $elements['title_plural'] = array(
113 '#type' => 'textfield',
114 '#title' => $this->t('Plural Paragraph Title'),
115 '#description' => $this->t('Title in its plural form.'),
116 '#default_value' => $this->getSetting('title_plural'),
120 $elements['edit_mode'] = array(
122 '#title' => $this->t('Edit mode'),
123 '#description' => $this->t('The mode the paragraph is in by default. Preview will render the paragraph in the preview view mode.'),
125 'open' => $this->t('Open'),
126 'closed' => $this->t('Closed'),
127 'preview' => $this->t('Preview'),
129 '#default_value' => $this->getSetting('edit_mode'),
133 $elements['add_mode'] = array(
135 '#title' => $this->t('Add mode'),
136 '#description' => $this->t('The way to add new Paragraphs.'),
138 'select' => $this->t('Select list'),
139 'button' => $this->t('Buttons'),
140 'dropdown' => $this->t('Dropdown button')
142 '#default_value' => $this->getSetting('add_mode'),
146 $elements['form_display_mode'] = array(
148 '#options' => \Drupal::service('entity_display.repository')->getFormModeOptions($this->getFieldSetting('target_type')),
149 '#description' => $this->t('The form display mode to use when rendering the paragraph form.'),
150 '#title' => $this->t('Form display mode'),
151 '#default_value' => $this->getSetting('form_display_mode'),
156 foreach ($this->getAllowedTypes() as $key => $bundle) {
157 $options[$key] = $bundle['label'];
160 $elements['default_paragraph_type'] = [
162 '#title' => $this->t('Default paragraph type'),
163 '#empty_value' => '_none',
164 '#default_value' => $this->getDefaultParagraphTypeMachineName(),
165 '#options' => $options,
166 '#description' => $this->t('When creating a new host entity, a paragraph of this type is added.'),
175 public function settingsSummary() {
177 $summary[] = $this->t('Title: @title', ['@title' => $this->getSetting('title')]);
178 $summary[] = $this->t('Plural title: @title_plural', [
179 '@title_plural' => $this->getSetting('title_plural')
182 switch($this->getSetting('edit_mode')) {
185 $edit_mode = $this->t('Open');
188 $edit_mode = $this->t('Closed');
191 $edit_mode = $this->t('Preview');
195 switch($this->getSetting('add_mode')) {
198 $add_mode = $this->t('Select list');
201 $add_mode = $this->t('Buttons');
204 $add_mode = $this->t('Dropdown button');
208 $summary[] = $this->t('Edit mode: @edit_mode', ['@edit_mode' => $edit_mode]);
209 $summary[] = $this->t('Add mode: @add_mode', ['@add_mode' => $add_mode]);
210 $summary[] = $this->t('Form display mode: @form_display_mode', [
211 '@form_display_mode' => $this->getSetting('form_display_mode')
213 if ($this->getDefaultParagraphTypeLabelName() !== NULL) {
214 $summary[] = $this->t('Default paragraph type: @default_paragraph_type', [
215 '@default_paragraph_type' => $this->getDefaultParagraphTypeLabelName()
225 * @see \Drupal\content_translation\Controller\ContentTranslationController::prepareTranslation()
226 * Uses a similar approach to populate a new translation.
228 public function formElement(FieldItemListInterface $items, $delta, array $element, array &$form, FormStateInterface $form_state) {
229 $field_name = $this->fieldDefinition->getName();
230 $parents = $element['#field_parents'];
233 $paragraphs_entity = NULL;
234 $host = $items->getEntity();
235 $widget_state = static::getWidgetState($parents, $field_name, $form_state);
237 $entity_manager = \Drupal::entityTypeManager();
238 $target_type = $this->getFieldSetting('target_type');
240 $item_mode = isset($widget_state['paragraphs'][$delta]['mode']) ? $widget_state['paragraphs'][$delta]['mode'] : 'edit';
241 $default_edit_mode = $this->getSetting('edit_mode');
243 $show_must_be_saved_warning = !empty($widget_state['paragraphs'][$delta]['show_warning']);
245 if (isset($widget_state['paragraphs'][$delta]['entity'])) {
246 $paragraphs_entity = $widget_state['paragraphs'][$delta]['entity'];
248 elseif (isset($items[$delta]->entity)) {
249 $paragraphs_entity = $items[$delta]->entity;
251 // We don't have a widget state yet, get from selector settings.
252 if (!isset($widget_state['paragraphs'][$delta]['mode'])) {
254 if ($default_edit_mode == 'open') {
257 elseif ($default_edit_mode == 'closed') {
258 $item_mode = 'closed';
260 elseif ($default_edit_mode == 'preview') {
261 $item_mode = 'preview';
265 elseif (isset($widget_state['selected_bundle'])) {
267 $entity_type = $entity_manager->getDefinition($target_type);
268 $bundle_key = $entity_type->getKey('bundle');
270 $paragraphs_entity = $entity_manager->getStorage($target_type)->create(array(
271 $bundle_key => $widget_state['selected_bundle'],
277 if ($item_mode == 'collapsed') {
278 $item_mode = $default_edit_mode;
281 if ($item_mode == 'closed') {
282 // Validate closed paragraphs and expand if needed.
283 // @todo Consider recursion.
284 $violations = $paragraphs_entity->validate();
285 $violations->filterByFieldAccess();
286 if (count($violations) > 0) {
289 foreach ($violations as $violation) {
290 $messages[] = $violation->getMessage();
292 $info['validation_error'] = array(
293 '#type' => 'container',
294 '#markup' => $this->t('@messages', ['@messages' => strip_tags(implode('\n', $messages))]),
295 '#attributes' => ['class' => ['messages', 'messages--warning']],
300 if ($paragraphs_entity) {
301 // Detect if we are translating.
302 $this->initIsTranslating($form_state, $host);
303 $langcode = $form_state->get('langcode');
305 if (!$this->isTranslating) {
306 // Set the langcode if we are not translating.
307 $langcode_key = $paragraphs_entity->getEntityType()->getKey('langcode');
308 if ($paragraphs_entity->get($langcode_key)->value != $langcode) {
309 // If a translation in the given language already exists, switch to
310 // that. If there is none yet, update the language.
311 if ($paragraphs_entity->hasTranslation($langcode)) {
312 $paragraphs_entity = $paragraphs_entity->getTranslation($langcode);
315 $paragraphs_entity->set($langcode_key, $langcode);
320 // Add translation if missing for the target language.
321 if (!$paragraphs_entity->hasTranslation($langcode)) {
322 // Get the selected translation of the paragraph entity.
323 $entity_langcode = $paragraphs_entity->language()->getId();
324 $source = $form_state->get(['content_translation', 'source']);
325 $source_langcode = $source ? $source->getId() : $entity_langcode;
326 // Make sure the source language version is used if available. It is a
327 // valid scenario to have no paragraphs items in the source version of
328 // the host and fetching the translation without this check could lead
330 if ($paragraphs_entity->hasTranslation($source_langcode)) {
331 $paragraphs_entity = $paragraphs_entity->getTranslation($source_langcode);
333 // The paragraphs entity has no content translation source field if
334 // no paragraph entity field is translatable, even if the host is.
335 if ($paragraphs_entity->hasField('content_translation_source')) {
336 // Initialise the translation with source language values.
337 $paragraphs_entity->addTranslation($langcode, $paragraphs_entity->toArray());
338 $translation = $paragraphs_entity->getTranslation($langcode);
339 $manager = \Drupal::service('content_translation.manager');
340 $manager->getTranslationMetadata($translation)->setSource($paragraphs_entity->language()->getId());
343 // If any paragraphs type is translatable do not switch.
344 if ($paragraphs_entity->hasField('content_translation_source')) {
345 // Switch the paragraph to the translation.
346 $paragraphs_entity = $paragraphs_entity->getTranslation($langcode);
350 $element_parents = $parents;
351 $element_parents[] = $field_name;
352 $element_parents[] = $delta;
353 $element_parents[] = 'subform';
355 $id_prefix = implode('-', array_merge($parents, array($field_name, $delta)));
356 $wrapper_id = Html::getUniqueId($id_prefix . '-item-wrapper');
359 '#type' => 'container',
360 '#element_validate' => array(array($this, 'elementValidate')),
362 '#type' => 'container',
363 '#parents' => $element_parents,
367 $element['#prefix'] = '<div id="' . $wrapper_id . '">';
368 $element['#suffix'] = '</div>';
370 $item_bundles = \Drupal::service('entity_type.bundle.info')->getBundleInfo($target_type);
371 if (isset($item_bundles[$paragraphs_entity->bundle()])) {
372 $bundle_info = $item_bundles[$paragraphs_entity->bundle()];
374 $element['top'] = array(
375 '#type' => 'container',
377 '#attributes' => array(
379 'paragraph-type-top',
384 $element['top']['paragraph_type_title'] = array(
385 '#type' => 'container',
387 '#attributes' => array(
389 'paragraph-type-title',
394 $element['top']['paragraph_type_title']['info'] = array(
395 '#markup' => $bundle_info['label'],
401 // Hide the button when translating.
402 $button_access = $paragraphs_entity->access('delete') && !$this->isTranslating;
403 if ($item_mode != 'remove') {
404 $links['remove_button'] = [
406 '#value' => $this->t('Remove'),
407 '#name' => strtr($id_prefix, '-', '_') . '_remove',
409 '#submit' => [[get_class($this), 'paragraphsItemSubmit']],
410 '#limit_validation_errors' => [array_merge($parents, [$field_name, 'add_more'])],
413 'callback' => [get_class($this), 'itemAjax'],
414 'wrapper' => $widget_state['ajax_wrapper_id'],
417 '#access' => $button_access,
418 '#prefix' => '<li class="remove">',
419 '#suffix' => '</li>',
420 '#paragraphs_mode' => 'remove',
425 if ($item_mode == 'edit') {
427 if (isset($items[$delta]->entity) && ($default_edit_mode == 'preview' || $default_edit_mode == 'closed')) {
428 $links['collapse_button'] = array(
430 '#value' => $this->t('Collapse'),
431 '#name' => strtr($id_prefix, '-', '_') . '_collapse',
433 '#submit' => array(array(get_class($this), 'paragraphsItemSubmit')),
435 '#limit_validation_errors' => [array_merge($parents, [$field_name, 'add_more'])],
437 'callback' => array(get_class($this), 'itemAjax'),
438 'wrapper' => $widget_state['ajax_wrapper_id'],
441 '#access' => $paragraphs_entity->access('update'),
442 '#prefix' => '<li class="collapse">',
443 '#suffix' => '</li>',
444 '#paragraphs_mode' => 'collapsed',
445 '#paragraphs_show_warning' => TRUE,
449 // Hide the button when translating.
450 $button_access = $paragraphs_entity->access('delete') && !$this->isTranslating;
452 $info['edit_button_info'] = array(
453 '#type' => 'container',
454 '#markup' => $this->t('You are not allowed to edit this @title.', array('@title' => $this->getSetting('title'))),
455 '#attributes' => ['class' => ['messages', 'messages--warning']],
456 '#access' => !$paragraphs_entity->access('update') && $paragraphs_entity->access('delete'),
459 $info['remove_button_info'] = array(
460 '#type' => 'container',
461 '#markup' => $this->t('You are not allowed to remove this @title.', array('@title' => $this->getSetting('title'))),
462 '#attributes' => ['class' => ['messages', 'messages--warning']],
463 '#access' => !$paragraphs_entity->access('delete') && $paragraphs_entity->access('update'),
466 $info['edit_remove_button_info'] = array(
467 '#type' => 'container',
468 '#markup' => $this->t('You are not allowed to edit or remove this @title.', array('@title' => $this->getSetting('title'))),
469 '#attributes' => ['class' => ['messages', 'messages--warning']],
470 '#access' => !$paragraphs_entity->access('update') && !$paragraphs_entity->access('delete'),
473 elseif ($item_mode == 'preview' || $item_mode == 'closed') {
474 $links['edit_button'] = array(
476 '#value' => $this->t('Edit'),
477 '#name' => strtr($id_prefix, '-', '_') . '_edit',
479 '#submit' => array(array(get_class($this), 'paragraphsItemSubmit')),
480 '#limit_validation_errors' => array(array_merge($parents, array($field_name, 'add_more'))),
483 'callback' => array(get_class($this), 'itemAjax'),
484 'wrapper' => $widget_state['ajax_wrapper_id'],
487 '#access' => $paragraphs_entity->access('update'),
488 '#prefix' => '<li class="edit">',
489 '#suffix' => '</li>',
490 '#paragraphs_mode' => 'edit',
493 if ($show_must_be_saved_warning) {
494 $info['must_be_saved_info'] = array(
495 '#type' => 'container',
496 '#markup' => $this->t('You have unsaved changes on this @title item.', array('@title' => $this->getSetting('title'))),
497 '#attributes' => ['class' => ['messages', 'messages--warning']],
501 $info['preview_info'] = array(
502 '#type' => 'container',
503 '#markup' => $this->t('You are not allowed to view this @title.', array('@title' => $this->getSetting('title'))),
504 '#attributes' => ['class' => ['messages', 'messages--warning']],
505 '#access' => !$paragraphs_entity->access('view'),
508 $info['edit_button_info'] = array(
509 '#type' => 'container',
510 '#markup' => $this->t('You are not allowed to edit this @title.', array('@title' => $this->getSetting('title'))),
511 '#attributes' => ['class' => ['messages', 'messages--warning']],
512 '#access' => !$paragraphs_entity->access('update') && $paragraphs_entity->access('delete'),
515 $info['remove_button_info'] = array(
516 '#type' => 'container',
517 '#markup' => $this->t('You are not allowed to remove this @title.', array('@title' => $this->getSetting('title'))),
518 '#attributes' => ['class' => ['messages', 'messages--warning']],
519 '#access' => !$paragraphs_entity->access('delete') && $paragraphs_entity->access('update'),
522 $info['edit_remove_button_info'] = array(
523 '#type' => 'container',
524 '#markup' => $this->t('You are not allowed to edit or remove this @title.', array('@title' => $this->getSetting('title'))),
525 '#attributes' => ['class' => ['messages', 'messages--warning']],
526 '#access' => !$paragraphs_entity->access('update') && !$paragraphs_entity->access('delete'),
529 elseif ($item_mode == 'remove') {
531 $element['top']['paragraph_type_title']['info'] = [
532 '#markup' => $this->t('Deleted @title: %type', ['@title' => $this->getSetting('title'), '%type' => $bundle_info['label']]),
535 $links['confirm_remove_button'] = [
537 '#value' => $this->t('Confirm removal'),
538 '#name' => strtr($id_prefix, '-', '_') . '_confirm_remove',
540 '#submit' => [[get_class($this), 'paragraphsItemSubmit']],
541 '#limit_validation_errors' => [array_merge($parents, [$field_name, 'add_more'])],
544 'callback' => [get_class($this), 'itemAjax'],
545 'wrapper' => $widget_state['ajax_wrapper_id'],
548 '#prefix' => '<li class="confirm-remove">',
549 '#suffix' => '</li>',
550 '#paragraphs_mode' => 'removed',
553 $links['restore_button'] = [
555 '#value' => $this->t('Restore'),
556 '#name' => strtr($id_prefix, '-', '_') . '_restore',
558 '#submit' => [[get_class($this), 'paragraphsItemSubmit']],
559 '#limit_validation_errors' => [array_merge($parents, [$field_name, 'add_more'])],
562 'callback' => [get_class($this), 'itemAjax'],
563 'wrapper' => $widget_state['ajax_wrapper_id'],
566 '#prefix' => '<li class="restore">',
567 '#suffix' => '</li>',
568 '#paragraphs_mode' => 'edit',
574 foreach($links as $link_item) {
575 if (!isset($link_item['#access']) || $link_item['#access']) {
580 if ($show_links > 0) {
582 $element['top']['links'] = $links;
583 if ($show_links > 1) {
584 $element['top']['links']['#theme_wrappers'] = array('dropbutton_wrapper', 'paragraphs_dropbutton_wrapper');
585 $element['top']['links']['prefix'] = array(
586 '#markup' => '<ul class="dropbutton">',
589 $element['top']['links']['suffix'] = array(
590 '#markup' => '</li>',
595 $element['top']['links']['#theme_wrappers'] = array('paragraphs_dropbutton_wrapper');
596 foreach($links as $key => $link_item) {
597 unset($element['top']['links'][$key]['#prefix']);
598 unset($element['top']['links'][$key]['#suffix']);
601 $element['top']['links']['#weight'] = 2;
607 foreach($info as $info_item) {
608 if (!isset($info_item['#access']) || $info_item['#access']) {
615 $element['info'] = $info;
616 $element['info']['#weight'] = 998;
620 if (count($actions)) {
621 $show_actions = FALSE;
622 foreach($actions as $action_item) {
623 if (!isset($action_item['#access']) || $action_item['#access']) {
624 $show_actions = TRUE;
630 $element['actions'] = $actions;
631 $element['actions']['#type'] = 'actions';
632 $element['actions']['#weight'] = 999;
637 $display = EntityFormDisplay::collectRenderDisplay($paragraphs_entity, $this->getSetting('form_display_mode'));
639 // @todo Remove as part of https://www.drupal.org/node/2640056
640 if (\Drupal::moduleHandler()->moduleExists('field_group')) {
642 'entity_type' => $paragraphs_entity->getEntityTypeId(),
643 'bundle' => $paragraphs_entity->bundle(),
644 'entity' => $paragraphs_entity,
646 'display_context' => 'form',
647 'mode' => $display->getMode(),
650 field_group_attach_groups($element['subform'], $context);
651 $element['subform']['#pre_render'][] = 'field_group_form_pre_render';
654 if ($item_mode == 'edit') {
655 $display->buildForm($paragraphs_entity, $element['subform'], $form_state);
656 foreach (Element::children($element['subform']) as $field) {
657 if ($paragraphs_entity->hasField($field)) {
658 $translatable = $paragraphs_entity->{$field}->getFieldDefinition()->isTranslatable();
660 $element['subform'][$field]['widget']['#after_build'][] = [
662 'removeTranslatabilityClue'
668 elseif ($item_mode == 'preview') {
669 $element['subform'] = array();
670 $element['behavior_plugins'] = [];
671 $element['preview'] = entity_view($paragraphs_entity, 'preview', $paragraphs_entity->language()->getId());
672 $element['preview']['#access'] = $paragraphs_entity->access('view');
674 elseif ($item_mode == 'closed') {
675 $element['subform'] = array();
676 $element['behavior_plugins'] = [];
677 if ($paragraphs_entity) {
678 $summary = $paragraphs_entity->getSummary();
679 $element['top']['paragraph_summary']['fields_info'] = [
680 '#markup' => $summary,
681 '#prefix' => '<div class="paragraphs-collapsed-description">',
682 '#suffix' => '</div>',
687 $element['subform'] = array();
690 $element['subform']['#attributes']['class'][] = 'paragraphs-subform';
691 $element['subform']['#access'] = $paragraphs_entity->access('update');
693 if ($item_mode == 'removed') {
694 $element['#access'] = FALSE;
697 $widget_state['paragraphs'][$delta]['entity'] = $paragraphs_entity;
698 $widget_state['paragraphs'][$delta]['display'] = $display;
699 $widget_state['paragraphs'][$delta]['mode'] = $item_mode;
701 static::setWidgetState($parents, $field_name, $form_state, $widget_state);
704 $element['#access'] = FALSE;
711 * Returns the sorted allowed types for a entity reference field.
714 * A list of arrays keyed by the paragraph type machine name with the following properties.
715 * - label: The label of the paragraph type.
716 * - weight: The weight of the paragraph type.
718 public function getAllowedTypes() {
720 $return_bundles = array();
721 /** @var \Drupal\Core\Entity\EntityReferenceSelection\SelectionPluginManagerInterface $selection_manager */
722 $selection_manager = \Drupal::service('plugin.manager.entity_reference_selection');
723 $handler = $selection_manager->getSelectionHandler($this->fieldDefinition);
724 if ($handler instanceof ParagraphSelection) {
725 $return_bundles = $handler->getSortedAllowedTypes();
727 // Support for other reference types.
729 $bundles = \Drupal::service('entity_type.bundle.info')->getBundleInfo($this->getFieldSetting('target_type'));
731 foreach ($bundles as $machine_name => $bundle) {
732 if (!count($this->getSelectionHandlerSetting('target_bundles'))
733 || in_array($machine_name, $this->getSelectionHandlerSetting('target_bundles'))) {
735 $return_bundles[$machine_name] = array(
736 'label' => $bundle['label'],
746 return $return_bundles;
749 public function formMultipleElements(FieldItemListInterface $items, array &$form, FormStateInterface $form_state) {
750 $field_name = $this->fieldDefinition->getName();
751 $cardinality = $this->fieldDefinition->getFieldStorageDefinition()->getCardinality();
752 $this->fieldParents = $form['#parents'];
753 $field_state = static::getWidgetState($this->fieldParents, $field_name, $form_state);
755 $max = $field_state['items_count'];
756 $entity_type_manager = \Drupal::entityTypeManager();
758 // Consider adding a default paragraph for new host entities.
759 if ($max == 0 && $items->getEntity()->isNew()) {
760 $default_type = $this->getDefaultParagraphTypeMachineName();
762 // Checking if default_type is not none and if is allowed.
764 // Place the default paragraph.
765 $target_type = $this->getFieldSetting('target_type');
766 $paragraphs_entity = $entity_type_manager->getStorage($target_type)->create([
767 'type' => $default_type,
769 $field_state['selected_bundle'] = $default_type;
770 $display = EntityFormDisplay::collectRenderDisplay($paragraphs_entity, $this->getSetting('form_display_mode'));
771 $field_state['paragraphs'][0] = [
772 'entity' => $paragraphs_entity,
773 'display' => $display,
775 'original_delta' => 1
778 $field_state['items_count'] = $max;
782 $this->realItemCount = $max;
783 $is_multiple = $this->fieldDefinition->getFieldStorageDefinition()->isMultiple();
785 $title = $this->fieldDefinition->getLabel();
786 $description = FieldFilteredMarkup::create(\Drupal::token()->replace($this->fieldDefinition->getDescription()));
789 $this->fieldIdPrefix = implode('-', array_merge($this->fieldParents, array($field_name)));
790 $this->fieldWrapperId = Html::getUniqueId($this->fieldIdPrefix . '-add-more-wrapper');
791 $elements['#prefix'] = '<div id="' . $this->fieldWrapperId . '">';
792 $elements['#suffix'] = '</div>';
794 $field_state['ajax_wrapper_id'] = $this->fieldWrapperId;
795 // Persist the widget state so formElement() can access it.
796 static::setWidgetState($this->fieldParents, $field_name, $form_state, $field_state);
799 for ($delta = 0; $delta < $max; $delta++) {
801 // Add a new empty item if it doesn't exist yet at this delta.
802 if (!isset($items[$delta])) {
803 $items->appendItem();
806 // For multiple fields, title and description are handled by the wrapping
809 '#title' => $is_multiple ? '' : $title,
810 '#description' => $is_multiple ? '' : $description,
812 $element = $this->formSingleElement($items, $delta, $element, $form, $form_state);
815 // Input field for the delta (drag-n-drop reordering).
817 // We name the element '_weight' to avoid clashing with elements
818 // defined by widget.
819 $element['_weight'] = array(
821 '#title' => $this->t('Weight for row @number', array('@number' => $delta + 1)),
822 '#title_display' => 'invisible',
823 // Note: this 'delta' is the FAPI #type 'weight' element's property.
825 '#default_value' => $items[$delta]->_weight ?: $delta,
830 // Access for the top element is set to FALSE only when the paragraph
831 // was removed. A paragraphs that a user can not edit has access on
833 if (isset($element['#access']) && !$element['#access']) {
834 $this->realItemCount--;
837 $elements[$delta] = $element;
843 $field_state = static::getWidgetState($this->fieldParents, $field_name, $form_state);
844 $field_state['real_item_count'] = $this->realItemCount;
845 static::setWidgetState($this->fieldParents, $field_name, $form_state, $field_state);
848 '#element_validate' => [[$this, 'multipleElementValidate']],
849 '#required' => $this->fieldDefinition->isRequired(),
850 '#field_name' => $field_name,
851 '#cardinality' => $cardinality,
852 '#max_delta' => $max - 1,
855 if ($this->realItemCount > 0) {
857 '#theme' => 'field_multiple_value_form',
858 '#cardinality_multiple' => $is_multiple,
860 '#description' => $description,
864 $classes = $this->fieldDefinition->isRequired() ? ['form-required'] : [];
866 '#type' => 'container',
867 '#theme_wrappers' => ['container'],
868 '#cardinality_multiple' => TRUE,
870 '#type' => 'html_tag',
873 '#attributes' => ['class' => $classes],
876 '#type' => 'container',
878 '#markup' => $this->t('No @title added yet.', ['@title' => $this->getSetting('title')]),
880 '#suffix' => '</em>',
885 if ($this->fieldDefinition->isRequired()) {
886 $elements['title']['#attributes']['class'][] = 'form-required';
890 $elements['description'] = [
891 '#type' => 'container',
892 'value' => ['#markup' => $description],
893 '#attributes' => ['class' => ['description']],
898 $host = $items->getEntity();
899 $this->initIsTranslating($form_state, $host);
901 if (($this->realItemCount < $cardinality || $cardinality == FieldStorageDefinitionInterface::CARDINALITY_UNLIMITED) && !$form_state->isProgrammed() && !$this->isTranslating) {
902 $elements['add_more'] = $this->buildAddActions();
905 $elements['#attached']['library'][] = 'paragraphs/drupal.paragraphs.admin';
913 public function form(FieldItemListInterface $items, array &$form, FormStateInterface $form_state, $get_delta = NULL) {
914 $parents = $form['#parents'];
916 // Identify the manage field settings default value form.
917 if (in_array('default_value_input', $parents, TRUE)) {
918 // Since the entity is not reusable neither cloneable, having a default
919 // value is not supported.
920 return ['#markup' => $this->t('No widget available for: %label.', ['%label' => $items->getFieldDefinition()->getLabel()])];
923 return parent::form($items, $form, $form_state, $get_delta);
927 * Add 'add more' button, if not working with a programmed form.
930 * The form element array.
932 protected function buildAddActions() {
933 if (count($this->getAccessibleOptions()) === 0) {
934 if (count($this->getAllowedTypes()) === 0) {
935 $add_more_elements['info'] = [
936 '#type' => 'container',
937 '#markup' => $this->t('You are not allowed to add any of the @title types.', ['@title' => $this->getSetting('title')]),
938 '#attributes' => ['class' => ['messages', 'messages--warning']],
942 $add_more_elements['info'] = [
943 '#type' => 'container',
944 '#markup' => $this->t('You did not add any @title types yet.', ['@title' => $this->getSetting('title')]),
945 '#attributes' => ['class' => ['messages', 'messages--warning']],
949 return $add_more_elements ;
952 if ($this->getSetting('add_mode') == 'button' || $this->getSetting('add_mode') == 'dropdown') {
953 return $this->buildButtonsAddMode();
956 return $this->buildSelectAddMode();
960 * Returns the available paragraphs type.
963 * Available paragraphs types.
965 protected function getAccessibleOptions() {
966 if ($this->accessOptions !== NULL) {
967 return $this->accessOptions;
970 $entity_type_manager = \Drupal::entityTypeManager();
971 $target_type = $this->getFieldSetting('target_type');
972 $bundles = $this->getAllowedTypes();
973 $access_control_handler = $entity_type_manager->getAccessControlHandler($target_type);
974 $dragdrop_settings = $this->getSelectionHandlerSetting('target_bundles_drag_drop');
976 foreach ($bundles as $machine_name => $bundle) {
977 if ($dragdrop_settings || (!count($this->getSelectionHandlerSetting('target_bundles'))
978 || in_array($machine_name, $this->getSelectionHandlerSetting('target_bundles')))) {
979 if ($access_control_handler->createAccess($machine_name)) {
980 $this->accessOptions[$machine_name] = $bundle['label'];
985 return $this->accessOptions;
989 * Builds dropdown button for adding new paragraph.
992 * The form element array.
994 protected function buildButtonsAddMode() {
995 // Hide the button when translating.
996 $add_more_elements = [
997 '#type' => 'container',
998 '#theme_wrappers' => ['paragraphs_dropbutton_wrapper'],
1000 $field_name = $this->fieldDefinition->getName();
1001 $title = $this->fieldDefinition->getLabel();
1003 $drop_button = FALSE;
1004 if (count($this->getAccessibleOptions()) > 1 && $this->getSetting('add_mode') == 'dropdown') {
1005 $drop_button = TRUE;
1006 $add_more_elements['#theme_wrappers'] = ['dropbutton_wrapper'];
1007 $add_more_elements['prefix'] = [
1008 '#markup' => '<ul class="dropbutton">',
1011 $add_more_elements['suffix'] = [
1012 '#markup' => '</ul>',
1015 $add_more_elements['#suffix'] = $this->t(' to %type', ['%type' => $title]);
1018 foreach ($this->getAccessibleOptions() as $machine_name => $label) {
1019 $add_more_elements['add_more_button_' . $machine_name] = [
1020 '#type' => 'submit',
1021 '#name' => strtr($this->fieldIdPrefix, '-', '_') . '_' . $machine_name . '_add_more',
1022 '#value' => $this->t('Add @type', ['@type' => $label]),
1023 '#attributes' => ['class' => ['field-add-more-submit']],
1024 '#limit_validation_errors' => [array_merge($this->fieldParents, [$field_name, 'add_more'])],
1025 '#submit' => [[get_class($this), 'addMoreSubmit']],
1027 'callback' => [get_class($this), 'addMoreAjax'],
1028 'wrapper' => $this->fieldWrapperId,
1031 '#bundle_machine_name' => $machine_name,
1035 $add_more_elements['add_more_button_' . $machine_name]['#prefix'] = '<li>';
1036 $add_more_elements['add_more_button_' . $machine_name]['#suffix'] = '</li>';
1040 return $add_more_elements;
1044 * Builds list of actions based on paragraphs type.
1047 * The form element array.
1049 protected function buildSelectAddMode() {
1050 $field_name = $this->fieldDefinition->getName();
1051 $title = $this->fieldDefinition->getLabel();
1052 $add_more_elements['add_more_select'] = [
1053 '#type' => 'select',
1054 '#options' => $this->getAccessibleOptions(),
1055 '#title' => $this->t('@title type', ['@title' => $this->getSetting('title')]),
1056 '#label_display' => 'hidden',
1059 $text = $this->t('Add @title', ['@title' => $this->getSetting('title')]);
1061 if ($this->realItemCount > 0) {
1062 $text = $this->t('Add another @title', ['@title' => $this->getSetting('title')]);
1065 $add_more_elements['add_more_button'] = [
1066 '#type' => 'submit',
1067 '#name' => strtr($this->fieldIdPrefix, '-', '_') . '_add_more',
1069 '#attributes' => ['class' => ['field-add-more-submit']],
1070 '#limit_validation_errors' => [array_merge($this->fieldParents, [$field_name, 'add_more'])],
1071 '#submit' => [[get_class($this), 'addMoreSubmit']],
1073 'callback' => [get_class($this), 'addMoreAjax'],
1074 'wrapper' => $this->fieldWrapperId,
1079 $add_more_elements['add_more_button']['#suffix'] = $this->t(' to %type', ['%type' => $title]);
1080 return $add_more_elements;
1084 * Gets current language code from the form state or item.
1086 * Since the paragraph field is not set as translatable, the item language
1087 * code is set to the source language. The intended translation language
1088 * is only accessibly through the form state.
1090 * @param \Drupal\Core\Form\FormStateInterface $form_state
1091 * @param \Drupal\Core\Field\FieldItemListInterface $items
1094 protected function getCurrentLangcode(FormStateInterface $form_state, FieldItemListInterface $items) {
1095 return $form_state->get('langcode') ?: $items->getEntity()->language()->getId();
1101 public static function addMoreAjax(array $form, FormStateInterface $form_state) {
1102 $button = $form_state->getTriggeringElement();
1103 // Go one level up in the form, to the widgets container.
1104 $element = NestedArray::getValue($form, array_slice($button['#array_parents'], 0, -2));
1106 // Add a DIV around the delta receiving the Ajax effect.
1107 $delta = $element['#max_delta'];
1108 $element[$delta]['#prefix'] = '<div class="ajax-new-content">' . (isset($element[$delta]['#prefix']) ? $element[$delta]['#prefix'] : '');
1109 $element[$delta]['#suffix'] = (isset($element[$delta]['#suffix']) ? $element[$delta]['#suffix'] : '') . '</div>';
1117 public static function addMoreSubmit(array $form, FormStateInterface $form_state) {
1118 $button = $form_state->getTriggeringElement();
1120 // Go one level up in the form, to the widgets container.
1121 $element = NestedArray::getValue($form, array_slice($button['#array_parents'], 0, -2));
1122 $field_name = $element['#field_name'];
1123 $parents = $element['#field_parents'];
1125 // Increment the items count.
1126 $widget_state = static::getWidgetState($parents, $field_name, $form_state);
1128 if ($widget_state['real_item_count'] < $element['#cardinality'] || $element['#cardinality'] == FieldStorageDefinitionInterface::CARDINALITY_UNLIMITED) {
1129 $widget_state['items_count']++;
1132 if (isset($button['#bundle_machine_name'])) {
1133 $widget_state['selected_bundle'] = $button['#bundle_machine_name'];
1136 $widget_state['selected_bundle'] = $element['add_more']['add_more_select']['#value'];
1139 static::setWidgetState($parents, $field_name, $form_state, $widget_state);
1141 $form_state->setRebuild();
1144 public static function paragraphsItemSubmit(array $form, FormStateInterface $form_state) {
1145 $button = $form_state->getTriggeringElement();
1147 // Go one level up in the form, to the widgets container.
1148 $element = NestedArray::getValue($form, array_slice($button['#array_parents'], 0, -4));
1150 $delta = array_slice($button['#array_parents'], -4, -3);
1153 $field_name = $element['#field_name'];
1154 $parents = $element['#field_parents'];
1156 $widget_state = static::getWidgetState($parents, $field_name, $form_state);
1158 $widget_state['paragraphs'][$delta]['mode'] = $button['#paragraphs_mode'];
1160 if (!empty($button['#paragraphs_show_warning'])) {
1161 $widget_state['paragraphs'][$delta]['show_warning'] = $button['#paragraphs_show_warning'];
1164 static::setWidgetState($parents, $field_name, $form_state, $widget_state);
1166 $form_state->setRebuild();
1169 public static function itemAjax(array $form, FormStateInterface $form_state) {
1170 $button = $form_state->getTriggeringElement();
1171 // Go one level up in the form, to the widgets container.
1172 $element = NestedArray::getValue($form, array_slice($button['#array_parents'], 0, -4));
1174 $element['#prefix'] = '<div class="ajax-new-content">' . (isset($element['#prefix']) ? $element['#prefix'] : '');
1175 $element['#suffix'] = (isset($element['#suffix']) ? $element['#suffix'] : '') . '</div>';
1183 public function errorElement(array $element, ConstraintViolationInterface $error, array $form, FormStateInterface $form_state) {
1188 * Returns the value of a setting for the entity reference selection handler.
1190 * @param string $setting_name
1194 * The setting value.
1196 protected function getSelectionHandlerSetting($setting_name) {
1197 $settings = $this->getFieldSetting('handler_settings');
1198 return isset($settings[$setting_name]) ? $settings[$setting_name] : NULL;
1202 * Checks whether a content entity is referenced.
1206 protected function isContentReferenced() {
1207 $target_type = $this->getFieldSetting('target_type');
1208 $target_type_info = \Drupal::entityTypeManager()->getDefinition($target_type);
1209 return $target_type_info->isSubclassOf('\Drupal\Core\Entity\ContentEntityInterface');
1215 public function elementValidate($element, FormStateInterface $form_state, $form) {
1216 $field_name = $this->fieldDefinition->getName();
1217 $widget_state = static::getWidgetState($element['#field_parents'], $field_name, $form_state);
1218 $delta = $element['#delta'];
1220 if (isset($widget_state['paragraphs'][$delta]['entity'])) {
1221 $entity = $widget_state['paragraphs'][$delta]['entity'];
1223 /** @var \Drupal\Core\Entity\Display\EntityFormDisplayInterface $display */
1224 $display = $widget_state['paragraphs'][$delta]['display'];
1226 if ($widget_state['paragraphs'][$delta]['mode'] == 'edit') {
1227 // Extract the form values on submit for getting the current paragraph.
1228 $display->extractFormValues($entity, $element['subform'], $form_state);
1229 $display->validateFormValues($entity, $element['subform'], $form_state);
1233 static::setWidgetState($element['#field_parents'], $field_name, $form_state, $widget_state);
1237 * Special handling to validate form elements with multiple values.
1239 * @param array $elements
1240 * An associative array containing the substructure of the form to be
1241 * validated in this call.
1242 * @param \Drupal\Core\Form\FormStateInterface $form_state
1243 * The current state of the form.
1244 * @param array $form
1245 * The complete form array.
1247 public function multipleElementValidate(array $elements, FormStateInterface $form_state, array $form) {
1248 $field_name = $this->fieldDefinition->getName();
1249 $widget_state = static::getWidgetState($elements['#field_parents'], $field_name, $form_state);
1251 $remove_mode_item_count = $this->getNumberOfParagraphsInMode($widget_state, 'remove');
1252 $non_remove_mode_item_count = $widget_state['real_item_count'] - $remove_mode_item_count;
1254 if ($elements['#required'] && $non_remove_mode_item_count < 1) {
1255 $form_state->setError($elements, t('@name field is required.', ['@name' => $this->fieldDefinition->getLabel()]));
1258 static::setWidgetState($elements['#field_parents'], $field_name, $form_state, $widget_state);
1264 public function massageFormValues(array $values, array $form, FormStateInterface $form_state) {
1265 $field_name = $this->fieldDefinition->getName();
1266 $widget_state = static::getWidgetState($form['#parents'], $field_name, $form_state);
1267 $element = NestedArray::getValue($form_state->getCompleteForm(), $widget_state['array_parents']);
1269 foreach ($values as $delta => &$item) {
1270 if (isset($widget_state['paragraphs'][$item['_original_delta']]['entity'])
1271 && $widget_state['paragraphs'][$item['_original_delta']]['mode'] != 'remove') {
1272 $paragraphs_entity = $widget_state['paragraphs'][$item['_original_delta']]['entity'];
1274 /** @var \Drupal\Core\Entity\Display\EntityFormDisplayInterface $display */
1275 $display = $widget_state['paragraphs'][$item['_original_delta']]['display'];
1276 if ($widget_state['paragraphs'][$item['_original_delta']]['mode'] == 'edit') {
1277 $display->extractFormValues($paragraphs_entity, $element[$item['_original_delta']]['subform'], $form_state);
1279 // A content entity form saves without any rebuild. It needs to set the
1280 // language to update it in case of language change.
1281 $langcode_key = $paragraphs_entity->getEntityType()->getKey('langcode');
1282 if ($paragraphs_entity->get($langcode_key)->value != $form_state->get('langcode')) {
1283 // If a translation in the given language already exists, switch to
1284 // that. If there is none yet, update the language.
1285 if ($paragraphs_entity->hasTranslation($form_state->get('langcode'))) {
1286 $paragraphs_entity = $paragraphs_entity->getTranslation($form_state->get('langcode'));
1289 $paragraphs_entity->set($langcode_key, $form_state->get('langcode'));
1293 $paragraphs_entity->setNeedsSave(TRUE);
1294 $item['entity'] = $paragraphs_entity;
1295 $item['target_id'] = $paragraphs_entity->id();
1296 $item['target_revision_id'] = $paragraphs_entity->getRevisionId();
1298 // If our mode is remove don't save or reference this entity.
1299 // @todo: Maybe we should actually delete it here?
1300 elseif($widget_state['paragraphs'][$item['_original_delta']]['mode'] == 'remove' || $widget_state['paragraphs'][$item['_original_delta']]['mode'] == 'removed') {
1301 $item['target_id'] = NULL;
1302 $item['target_revision_id'] = NULL;
1311 public function extractFormValues(FieldItemListInterface $items, array $form, FormStateInterface $form_state) {
1312 // Filter possible empty items.
1313 $items->filterEmptyItems();
1314 return parent::extractFormValues($items, $form, $form_state);
1318 * Initializes the translation form state.
1320 * @param \Drupal\Core\Form\FormStateInterface $form_state
1321 * @param \Drupal\Core\Entity\EntityInterface $host
1323 protected function initIsTranslating(FormStateInterface $form_state, EntityInterface $host) {
1324 if ($this->isTranslating != NULL) {
1327 $this->isTranslating = FALSE;
1328 if (!$host->isTranslatable()) {
1331 if (!$host->getEntityType()->hasKey('default_langcode')) {
1334 $default_langcode_key = $host->getEntityType()->getKey('default_langcode');
1335 if (!$host->hasField($default_langcode_key)) {
1339 if (!empty($form_state->get('content_translation'))) {
1340 // Adding a language through the ContentTranslationController.
1341 $this->isTranslating = TRUE;
1343 if ($host->hasTranslation($form_state->get('langcode')) && $host->getTranslation($form_state->get('langcode'))->get($default_langcode_key)->value == 0) {
1344 // Editing a translation.
1345 $this->isTranslating = TRUE;
1350 * After-build callback for removing the translatability clue from the widget.
1352 * If the fields on the paragraph type are translatable,
1353 * ContentTranslationHandler::addTranslatabilityClue()adds an
1354 * "(all languages)" suffix to the widget title. That suffix is incorrect and
1355 * is being removed by this method using a #after_build on the field widget.
1357 * @param array $element
1358 * @param \Drupal\Core\Form\FormStateInterface $form_state
1362 public static function removeTranslatabilityClue(array $element, FormStateInterface $form_state) {
1363 // Widgets could have multiple elements with their own titles, so remove the
1364 // suffix if it exists, do not recurse lower than this to avoid going into
1365 // nested paragraphs or similar nested field types.
1366 $suffix = ' <span class="translation-entity-all-languages">(' . t('all languages') . ')</span>';
1367 if (isset($element['#title']) && strpos($element['#title'], $suffix)) {
1368 $element['#title'] = str_replace($suffix, '', $element['#title']);
1370 // Loop over all widget deltas.
1371 foreach (Element::children($element) as $delta) {
1372 if (isset($element[$delta]['#title']) && strpos($element[$delta]['#title'], $suffix)) {
1373 $element[$delta]['#title'] = str_replace($suffix, '', $element[$delta]['#title']);
1375 // Loop over all form elements within the current delta.
1376 foreach (Element::children($element[$delta]) as $field) {
1377 if (isset($element[$delta][$field]['#title']) && strpos($element[$delta][$field]['#title'], $suffix)) {
1378 $element[$delta][$field]['#title'] = str_replace($suffix, '', $element[$delta][$field]['#title']);
1386 * Returns the default paragraph type.
1388 * @return string $default_paragraph_type
1389 * Label name for default paragraph type.
1391 protected function getDefaultParagraphTypeLabelName(){
1392 if ($this->getDefaultParagraphTypeMachineName() !== NULL) {
1393 $allowed_types = $this->getAllowedTypes();
1394 return $allowed_types[$this->getDefaultParagraphTypeMachineName()]['label'];
1401 * Returns the machine name for default paragraph type.
1404 * Machine name for default paragraph type.
1406 protected function getDefaultParagraphTypeMachineName() {
1407 $default_type = $this->getSetting('default_paragraph_type');
1408 $allowed_types = $this->getAllowedTypes();
1409 if ($default_type && isset($allowed_types[$default_type])) {
1410 return $default_type;
1412 // Check if the user explicitly selected not to have any default Paragraph
1413 // type. Othewise, if there is only one type available, that one is the
1415 if ($default_type === '_none') {
1418 if (count($allowed_types) === 1) {
1419 return key($allowed_types);
1426 * Counts the number of paragraphs in a certain mode in a form substructure.
1428 * @param array $widget_state
1429 * The widget state for the form substructure containing information about
1430 * the paragraphs within.
1431 * @param string $mode
1432 * The mode to look for.
1435 * The number of paragraphs is the given mode.
1437 protected function getNumberOfParagraphsInMode(array $widget_state, $mode) {
1438 if (!isset($widget_state['paragraphs'])) {
1442 $paragraphs_count = 0;
1443 foreach ($widget_state['paragraphs'] as $paragraph) {
1444 if ($paragraph['mode'] == $mode) {
1445 $paragraphs_count++;
1449 return $paragraphs_count;
1455 public static function isApplicable(FieldDefinitionInterface $field_definition) {
1456 $target_type = $field_definition->getSetting('target_type');
1457 $paragraph_type = \Drupal::entityTypeManager()->getDefinition($target_type);
1458 if ($paragraph_type) {
1459 return $paragraph_type->isSubclassOf(ParagraphInterface::class);