Upgraded imagemagick and manually altered pdf to image module to handle changes....
[yaffs-website] / web / modules / contrib / paragraphs / src / Plugin / Field / FieldWidget / ParagraphsWidget.php
1 <?php
2
3 namespace Drupal\paragraphs\Plugin\Field\FieldWidget;
4
5 use Drupal\Component\Utility\NestedArray;
6 use Drupal\Component\Utility\Html;
7 use Drupal\Core\Entity\ContentEntityInterface;
8 use Drupal\Core\Entity\Entity\EntityFormDisplay;
9 use Drupal\Core\Entity\FieldableEntityInterface;
10 use Drupal\Core\Field\FieldDefinitionInterface;
11 use Drupal\Core\Field\FieldFilteredMarkup;
12 use Drupal\Core\Field\FieldStorageDefinitionInterface;
13 use Drupal\Core\Field\WidgetBase;
14 use Drupal\Core\Form\FormStateInterface;
15 use Drupal\Core\Field\FieldItemListInterface;
16 use Drupal\Core\Form\SubformState;
17 use Drupal\Core\Render\Element;
18 use Drupal\paragraphs\ParagraphInterface;
19 use Drupal\paragraphs\Plugin\EntityReferenceSelection\ParagraphSelection;
20 use Symfony\Component\Validator\ConstraintViolationListInterface;
21
22 /**
23  * Plugin implementation of the 'entity_reference_revisions paragraphs' widget.
24  *
25  * @FieldWidget(
26  *   id = "paragraphs",
27  *   label = @Translation("Paragraphs EXPERIMENTAL"),
28  *   description = @Translation("An experimental paragraphs inline form widget."),
29  *   field_types = {
30  *     "entity_reference_revisions"
31  *   }
32  * )
33  */
34 class ParagraphsWidget extends WidgetBase {
35
36   /**
37    * Action position is in the add paragraphs place.
38    */
39   const ACTION_POSITION_BASE = 1;
40
41   /**
42    * Action position is in the table header section.
43    */
44   const ACTION_POSITION_HEADER = 2;
45
46   /**
47    * Action position is in the actions section of the widget.
48    */
49   const ACTION_POSITION_ACTIONS = 3;
50
51   /**
52    * Indicates whether the current widget instance is in translation.
53    *
54    * @var bool
55    */
56   protected $isTranslating;
57
58   /**
59    * Id to name ajax buttons that includes field parents and field name.
60    *
61    * @var string
62    */
63   protected $fieldIdPrefix;
64
65   /**
66    * Wrapper id to identify the paragraphs.
67    *
68    * @var string
69    */
70   protected $fieldWrapperId;
71
72   /**
73    * Number of paragraphs item on form.
74    *
75    * @var int
76    */
77   protected $realItemCount;
78
79   /**
80    * Parents for the current paragraph.
81    *
82    * @var array
83    */
84   protected $fieldParents;
85
86   /**
87    * Accessible paragraphs types.
88    *
89    * @var array
90    */
91   protected $accessOptions = NULL;
92
93   /**
94    * Constructs a ParagraphsWidget object.
95    *
96    * @param string $plugin_id
97    *   The plugin_id for the widget.
98    * @param mixed $plugin_definition
99    *   The plugin implementation definition.
100    * @param \Drupal\Core\Field\FieldDefinitionInterface $field_definition
101    *   The definition of the field to which the widget is associated.
102    * @param array $settings
103    *   The widget settings.
104    * @param array $third_party_settings
105    *   Any third party settings.
106    */
107   public function __construct($plugin_id, $plugin_definition, FieldDefinitionInterface $field_definition, array $settings, array $third_party_settings) {
108     // Modify settings that were set before https://www.drupal.org/node/2896115.
109     if(isset($settings['edit_mode']) && $settings['edit_mode'] === 'preview') {
110       $settings['edit_mode'] = 'closed';
111       $settings['closed_mode'] = 'preview';
112     }
113
114     parent::__construct($plugin_id, $plugin_definition, $field_definition, $settings, $third_party_settings);
115   }
116
117   /**
118    * {@inheritdoc}
119    */
120   public static function defaultSettings() {
121     return array(
122       'title' => t('Paragraph'),
123       'title_plural' => t('Paragraphs'),
124       'edit_mode' => 'open',
125       'closed_mode' => 'summary',
126       'autocollapse' => 'none',
127       'add_mode' => 'dropdown',
128       'form_display_mode' => 'default',
129       'default_paragraph_type' => '',
130     );
131   }
132
133   /**
134    * {@inheritdoc}
135    */
136   public function settingsForm(array $form, FormStateInterface $form_state) {
137     $elements = array();
138
139     $elements['title'] = array(
140       '#type' => 'textfield',
141       '#title' => $this->t('Paragraph Title'),
142       '#description' => $this->t('Label to appear as title on the button as "Add new [title]", this label is translatable'),
143       '#default_value' => $this->getSetting('title'),
144       '#required' => TRUE,
145     );
146
147     $elements['title_plural'] = array(
148       '#type' => 'textfield',
149       '#title' => $this->t('Plural Paragraph Title'),
150       '#description' => $this->t('Title in its plural form.'),
151       '#default_value' => $this->getSetting('title_plural'),
152       '#required' => TRUE,
153     );
154
155     $elements['edit_mode'] = array(
156       '#type' => 'select',
157       '#title' => $this->t('Edit mode'),
158       '#description' => $this->t('The mode the paragraph is in by default.'),
159       '#options' => $this->getSettingOptions('edit_mode'),
160       '#default_value' => $this->getSetting('edit_mode'),
161       '#required' => TRUE,
162     );
163
164     $elements['closed_mode'] = [
165       '#type' => 'select',
166       '#title' => $this->t('Closed mode'),
167       '#description' => $this->t('How to display the paragraphs, when the widget is closed. Preview will render the paragraph in the preview view mode and typically needs a custom admin theme.'),
168       '#options' => $this->getSettingOptions('closed_mode'),
169       '#default_value' => $this->getSetting('closed_mode'),
170       '#required' => TRUE,
171     ];
172
173     $elements['autocollapse'] = [
174       '#type' => 'select',
175       '#title' => $this->t('Autocollapse'),
176       '#description' => $this->t('When a paragraph is opened for editing, close others.'),
177       '#options' => $this->getSettingOptions('autocollapse'),
178       '#default_value' => $this->getSetting('autocollapse'),
179       '#required' => TRUE,
180     ];
181
182     $elements['add_mode'] = array(
183       '#type' => 'select',
184       '#title' => $this->t('Add mode'),
185       '#description' => $this->t('The way to add new Paragraphs.'),
186       '#options' => $this->getSettingOptions('add_mode'),
187       '#default_value' => $this->getSetting('add_mode'),
188       '#required' => TRUE,
189     );
190
191     $elements['form_display_mode'] = array(
192       '#type' => 'select',
193       '#options' => \Drupal::service('entity_display.repository')->getFormModeOptions($this->getFieldSetting('target_type')),
194       '#description' => $this->t('The form display mode to use when rendering the paragraph form.'),
195       '#title' => $this->t('Form display mode'),
196       '#default_value' => $this->getSetting('form_display_mode'),
197       '#required' => TRUE,
198     );
199
200     $options  = [];
201     foreach ($this->getAllowedTypes() as $key => $bundle) {
202       $options[$key] = $bundle['label'];
203     }
204
205     $elements['default_paragraph_type'] = [
206       '#type' => 'select',
207       '#title' => $this->t('Default paragraph type'),
208       '#empty_value' => '_none',
209       '#default_value' => $this->getDefaultParagraphTypeMachineName(),
210       '#options' => $options,
211       '#description' => $this->t('When creating a new host entity, a paragraph of this type is added.'),
212     ];
213
214     return $elements;
215   }
216
217   /**
218    * Returns select options for a plugin setting.
219    *
220    * This is done to allow
221    * \Drupal\paragraphs\Plugin\Field\FieldWidget\ParagraphsWidget::settingsSummary()
222    * to access option labels. Not all plugin setting are available.
223    *
224    * @param string $setting_name
225    *   The name of the widget setting. Supported settings:
226    *   - "edit_mode"
227    *   - "closed_mode"
228    *   - "autocollapse"
229    *   - "add_mode"
230    *
231    * @return array|null
232    *   An array of setting option usable as a value for a "#options" key.
233    */
234   protected function getSettingOptions($setting_name) {
235     switch($setting_name) {
236       case 'edit_mode':
237         $options = [
238           'open' => $this->t('Open'),
239           'closed' => $this->t('Closed'),
240         ];
241         break;
242       case 'closed_mode':
243         $options = [
244           'summary' => $this->t('Summary'),
245           'preview' => $this->t('Preview'),
246         ];
247         break;
248       case 'autocollapse':
249         $options = [
250           'none' => $this->t('None'),
251           'all' => $this->t('All'),
252         ];
253         break;
254       case 'add_mode':
255         $options = [
256           'select' => $this->t('Select list'),
257           'button' => $this->t('Buttons'),
258           'dropdown' => $this->t('Dropdown button'),
259           'modal' => $this->t('Modal form'),
260         ];
261         break;
262     }
263
264     return isset($options) ? $options : NULL;
265   }
266
267   /**
268    * {@inheritdoc}
269    */
270   public function settingsSummary() {
271     $summary = array();
272     $summary[] = $this->t('Title: @title', ['@title' => $this->getSetting('title')]);
273     $summary[] = $this->t('Plural title: @title_plural', [
274       '@title_plural' => $this->getSetting('title_plural')
275     ]);
276
277     $edit_mode = $this->getSettingOptions('edit_mode')[$this->getSetting('edit_mode')];
278     $closed_mode = $this->getSettingOptions('closed_mode')[$this->getSetting('closed_mode')];
279     $autocollapse = $this->getSettingOptions('autocollapse')[$this->getSetting('autocollapse')];
280     $add_mode = $this->getSettingOptions('add_mode')[$this->getSetting('add_mode')];
281
282     $summary[] = $this->t('Edit mode: @edit_mode', ['@edit_mode' => $edit_mode]);
283     $summary[] = $this->t('Closed mode: @closed_mode', ['@closed_mode' => $closed_mode]);
284     $summary[] = $this->t('Autocollapse: @autocollapse', ['@autocollapse' => $autocollapse]);
285     $summary[] = $this->t('Add mode: @add_mode', ['@add_mode' => $add_mode]);
286
287     $summary[] = $this->t('Form display mode: @form_display_mode', [
288       '@form_display_mode' => $this->getSetting('form_display_mode')
289     ]);
290     if ($this->getDefaultParagraphTypeLabelName() !== NULL) {
291       $summary[] = $this->t('Default paragraph type: @default_paragraph_type', [
292         '@default_paragraph_type' => $this->getDefaultParagraphTypeLabelName()
293       ]);
294     }
295
296     return $summary;
297   }
298
299   /**
300    * {@inheritdoc}
301    *
302    * @see \Drupal\content_translation\Controller\ContentTranslationController::prepareTranslation()
303    *   Uses a similar approach to populate a new translation.
304    */
305   public function formElement(FieldItemListInterface $items, $delta, array $element, array &$form, FormStateInterface $form_state) {
306     $field_name = $this->fieldDefinition->getName();
307     $parents = $element['#field_parents'];
308     $info = [];
309
310     /** @var \Drupal\paragraphs\Entity\Paragraph $paragraphs_entity */
311     $paragraphs_entity = NULL;
312     $host = $items->getEntity();
313     $widget_state = static::getWidgetState($parents, $field_name, $form_state);
314
315     $entity_type_manager = \Drupal::entityTypeManager();
316     $target_type = $this->getFieldSetting('target_type');
317
318     $item_mode = isset($widget_state['paragraphs'][$delta]['mode']) ? $widget_state['paragraphs'][$delta]['mode'] : 'edit';
319     $default_edit_mode = $this->getSetting('edit_mode');
320
321     $closed_mode_setting = isset($widget_state['closed_mode']) ? $widget_state['closed_mode'] : $this->getSetting('closed_mode');
322     $autocollapse_setting = isset($widget_state['autocollapse']) ? $widget_state['autocollapse'] : $this->getSetting('autocollapse');
323
324     $show_must_be_saved_warning = !empty($widget_state['paragraphs'][$delta]['show_warning']);
325
326     if (isset($widget_state['paragraphs'][$delta]['entity'])) {
327       $paragraphs_entity = $widget_state['paragraphs'][$delta]['entity'];
328     }
329     elseif (isset($items[$delta]->entity)) {
330       $paragraphs_entity = $items[$delta]->entity;
331
332       // We don't have a widget state yet, get from selector settings.
333       if (!isset($widget_state['paragraphs'][$delta]['mode'])) {
334
335         if ($default_edit_mode == 'open') {
336           $item_mode = 'edit';
337         }
338         elseif ($default_edit_mode == 'closed') {
339           $item_mode = 'closed';
340         }
341       }
342     }
343     elseif (isset($widget_state['selected_bundle'])) {
344
345       $entity_type = $entity_type_manager->getDefinition($target_type);
346       $bundle_key = $entity_type->getKey('bundle');
347
348       $paragraphs_entity = $entity_type_manager->getStorage($target_type)->create(array(
349         $bundle_key => $widget_state['selected_bundle'],
350       ));
351
352       $item_mode = 'edit';
353     }
354
355     if ($item_mode == 'closed') {
356       // Validate closed paragraphs and expand if needed.
357       // @todo Consider recursion.
358       $violations = $paragraphs_entity->validate();
359       $violations->filterByFieldAccess();
360       if (count($violations) > 0) {
361         $item_mode = 'edit';
362         $messages = [];
363         foreach ($violations as $violation) {
364           $messages[] = $violation->getMessage();
365         }
366         $info['validation_error'] = $this->createMessage($this->t('@messages', ['@messages' => strip_tags(implode('\n', $messages))]));
367       }
368     }
369
370     if ($paragraphs_entity) {
371       // Detect if we are translating.
372       $this->initIsTranslating($form_state, $host);
373       $langcode = $form_state->get('langcode');
374
375       if (!$this->isTranslating) {
376         // Set the langcode if we are not translating.
377         $langcode_key = $paragraphs_entity->getEntityType()->getKey('langcode');
378         if ($paragraphs_entity->get($langcode_key)->value != $langcode) {
379           // If a translation in the given language already exists, switch to
380           // that. If there is none yet, update the language.
381           if ($paragraphs_entity->hasTranslation($langcode)) {
382             $paragraphs_entity = $paragraphs_entity->getTranslation($langcode);
383           }
384           else {
385             $paragraphs_entity->set($langcode_key, $langcode);
386           }
387         }
388       }
389       else {
390         // Add translation if missing for the target language.
391         if (!$paragraphs_entity->hasTranslation($langcode)) {
392           // Get the selected translation of the paragraph entity.
393           $entity_langcode = $paragraphs_entity->language()->getId();
394           $source = $form_state->get(['content_translation', 'source']);
395           $source_langcode = $source ? $source->getId() : $entity_langcode;
396           // Make sure the source language version is used if available. It is a
397           // the host and fetching the translation without this check could lead
398           // valid scenario to have no paragraphs items in the source version of
399           // to an exception.
400           if ($paragraphs_entity->hasTranslation($source_langcode)) {
401             $paragraphs_entity = $paragraphs_entity->getTranslation($source_langcode);
402           }
403           // The paragraphs entity has no content translation source field if
404           // no paragraph entity field is translatable, even if the host is.
405           if ($paragraphs_entity->hasField('content_translation_source')) {
406             // Initialise the translation with source language values.
407             $paragraphs_entity->addTranslation($langcode, $paragraphs_entity->toArray());
408             $translation = $paragraphs_entity->getTranslation($langcode);
409             $manager = \Drupal::service('content_translation.manager');
410             $manager->getTranslationMetadata($translation)->setSource($paragraphs_entity->language()->getId());
411           }
412         }
413         // If any paragraphs type is translatable do not switch.
414         if ($paragraphs_entity->hasField('content_translation_source')) {
415           // Switch the paragraph to the translation.
416           $paragraphs_entity = $paragraphs_entity->getTranslation($langcode);
417         }
418       }
419
420       $element_parents = $parents;
421       $element_parents[] = $field_name;
422       $element_parents[] = $delta;
423       $element_parents[] = 'subform';
424
425       $id_prefix = implode('-', array_merge($parents, array($field_name, $delta)));
426       $wrapper_id = Html::getUniqueId($id_prefix . '-item-wrapper');
427
428       $element += array(
429         '#type' => 'container',
430         '#element_validate' => array(array($this, 'elementValidate')),
431         'subform' => array(
432           '#type' => 'container',
433           '#parents' => $element_parents,
434         ),
435       );
436
437       $element['#prefix'] = '<div id="' . $wrapper_id . '">';
438       $element['#suffix'] = '</div>';
439
440       $item_bundles = \Drupal::service('entity_type.bundle.info')->getBundleInfo($target_type);
441       if (isset($item_bundles[$paragraphs_entity->bundle()])) {
442         $bundle_info = $item_bundles[$paragraphs_entity->bundle()];
443
444         // Create top section structure with all needed subsections.
445         $element['top'] = [
446           '#type' => 'container',
447           '#weight' => -1000,
448           '#attributes' => ['class' => ['paragraph-type-top']],
449           // Section for paragraph type information.
450           'type' => [
451             '#type' => 'container',
452             '#attributes' => ['class' => ['paragraph-type-title']],
453             'label' => ['#markup' => $bundle_info['label']],
454           ],
455           // Section for information icons.
456           'info' => [
457             '#type' => 'container',
458             '#attributes' => ['class' => ['paragraph-type-info']],
459           ],
460           'summary' => [
461             '#type' => 'container',
462             '#attributes' => ['class' => ['paragraph-type-summary']],
463           ],
464           // Paragraphs actions element for actions and dropdown actions.
465           'actions' => [
466             '#type' => 'paragraphs_actions',
467           ],
468         ];
469
470         // Type icon and label bundle.
471         if ($icon_url = $paragraphs_entity->type->entity->getIconUrl()) {
472           $element['top']['type']['icon'] = [
473             '#theme' => 'image',
474             '#uri' => $icon_url,
475             '#attributes' => [
476               'class' => ['paragraph-type-icon'],
477               'title' => $bundle_info['label'],
478             ],
479             '#weight' => 0,
480             // We set inline height and width so icon don't resize on first load
481             // while CSS is still not loaded.
482             '#height' => 16,
483             '#width' => 16,
484           ];
485         }
486         $element['top']['type']['label'] = [
487           '#markup' => '<span class="paragraph-type-label">' . $bundle_info['label'] . '</span>',
488           '#weight' => 1,
489         ];
490
491         // Widget actions.
492         $widget_actions = [
493           'actions' => [],
494           'dropdown_actions' => [],
495         ];
496
497         $widget_actions['dropdown_actions']['duplicate_button'] = [
498           '#type' => 'submit',
499           '#value' => $this->t('Duplicate'),
500           '#name' => $id_prefix . '_duplicate',
501           '#weight' => 502,
502           '#submit' => [[get_class($this), 'duplicateSubmit']],
503           '#limit_validation_errors' => [array_merge($parents, [$field_name, 'add_more'])],
504           '#delta' => $delta,
505           '#ajax' => [
506             'callback' => [get_class($this), 'itemAjax'],
507             'wrapper' => $widget_state['ajax_wrapper_id'],
508           ],
509           '#access' => $paragraphs_entity->access('update'),
510         ];
511
512         if ($item_mode != 'remove') {
513           $widget_actions['dropdown_actions']['remove_button'] = [
514             '#type' => 'submit',
515             '#value' => $this->t('Remove'),
516             '#name' => $id_prefix . '_remove',
517             '#weight' => 501,
518             '#submit' => [[get_class($this), 'paragraphsItemSubmit']],
519             '#limit_validation_errors' => [array_merge($parents, [$field_name, 'add_more'])],
520             '#delta' => $delta,
521             '#ajax' => [
522               'callback' => array(get_class($this), 'itemAjax'),
523               'wrapper' => $widget_state['ajax_wrapper_id'],
524             ],
525             // Hide the button when translating.
526             '#access' => $paragraphs_entity->access('delete') && !$this->isTranslating,
527             '#paragraphs_mode' => 'remove',
528           ];
529         }
530
531         if ($item_mode == 'edit') {
532           if (isset($paragraphs_entity)) {
533             $widget_actions['actions']['collapse_button'] = [
534               '#value' => $this->t('Collapse'),
535               '#name' => $id_prefix . '_collapse',
536               '#weight' => 1,
537               '#submit' => [[get_class($this), 'paragraphsItemSubmit']],
538               '#limit_validation_errors' => [array_merge($parents, [$field_name, 'add_more'])],
539               '#delta' => $delta,
540               '#ajax' => [
541                 'callback' => [get_class($this), 'itemAjax'],
542                 'wrapper' => $widget_state['ajax_wrapper_id'],
543               ],
544               '#access' => $paragraphs_entity->access('update'),
545               '#paragraphs_mode' => 'closed',
546               '#paragraphs_show_warning' => TRUE,
547               '#attributes' => [
548                 'class' => ['paragraphs-icon-button', 'paragraphs-icon-button-collapse'],
549                 'title' => $this->t('Collapse'),
550               ],
551             ];
552           }
553         }
554         else {
555           $widget_actions['actions']['edit_button'] = $this->expandButton([
556             '#type' => 'submit',
557             '#value' => $this->t('Edit'),
558             '#name' => $id_prefix . '_edit',
559             '#weight' => 1,
560             '#attributes' => ['class' => ['paragraphs-button']],
561             '#submit' => [[get_class($this), 'paragraphsItemSubmit']],
562             '#limit_validation_errors' => [
563               array_merge($parents, [$field_name, 'add_more']),
564             ],
565             '#delta' => $delta,
566             '#ajax' => [
567               'callback' => [get_class($this), 'itemAjax'],
568               'wrapper' => $widget_state['ajax_wrapper_id'],
569             ],
570             '#access' => $paragraphs_entity->access('update'),
571             '#paragraphs_mode' => 'edit',
572             '#attributes' => [
573               'class' => ['paragraphs-icon-button', 'paragraphs-icon-button-edit'],
574               'title' => $this->t('Edit'),
575             ],
576           ]);
577
578           if ($show_must_be_saved_warning && $paragraphs_entity->isChanged()) {
579             $info['changed'] = [
580               '#theme' => 'paragraphs_info_icon',
581               '#message' => $this->t('You have unsaved changes on this @title item.', ['@title' => $this->getSetting('title')]),
582               '#icon' => 'changed',
583             ];
584           }
585
586           if (!$paragraphs_entity->access('view')) {
587             $info['preview'] = [
588               '#theme' => 'paragraphs_info_icon',
589               '#message' => $this->t('You are not allowed to view this @title.', array('@title' => $this->getSetting('title'))),
590               '#icon' => 'view',
591             ];
592           }
593         }
594
595         // If update is disabled we will show lock icon in actions section.
596         if (!$paragraphs_entity->access('update')) {
597           $widget_actions['actions']['edit_disabled'] = [
598             '#theme' => 'paragraphs_info_icon',
599             '#message' => $this->t('You are not allowed to edit or remove this @title.', ['@title' => $this->getSetting('title')]),
600             '#icon' => 'lock',
601             '#weight' => 1,
602           ];
603         }
604
605         if (!$paragraphs_entity->access('update') && !$paragraphs_entity->access('delete')) {
606           $info['edit'] = [
607             '#theme' => 'paragraphs_info_icon',
608             '#message' => $this->t('You are not allowed to edit or remove this @title.', ['@title' => $this->getSetting('title')]),
609             '#icon' => 'lock',
610           ];
611         }
612         elseif (!$paragraphs_entity->access('update')) {
613           $info['edit'] = [
614             '#theme' => 'paragraphs_info_icon',
615             '#message' => $this->t('You are not allowed to edit this @title.', ['@title' => $this->getSetting('title')]),
616             '#icon' => 'edit-disabled',
617           ];
618         }
619         elseif (!$paragraphs_entity->access('delete')) {
620           $info['remove'] = [
621             '#theme' => 'paragraphs_info_icon',
622             '#message' => $this->t('You are not allowed to remove this @title.', ['@title' => $this->getSetting('title')]),
623             '#icon' => 'delete-disabled',
624           ];
625         }
626
627         $context = [
628           'form' => $form,
629           'widget' => self::getWidgetState($parents, $field_name, $form_state, $widget_state),
630           'items' => $items,
631           'delta' => $delta,
632           'element' => $element,
633           'form_state' => $form_state,
634           'paragraphs_entity' => $paragraphs_entity,
635         ];
636
637         // Allow modules to alter widget actions.
638         \Drupal::moduleHandler()->alter('paragraphs_widget_actions', $widget_actions, $context);
639
640         if (count($widget_actions['actions'])) {
641           // Expand all actions to proper submit elements and add it to top
642           // actions sub component.
643           $element['top']['actions']['actions'] = array_map([$this, 'expandButton'], $widget_actions['actions']);
644         }
645
646         if (count($widget_actions['dropdown_actions'])) {
647           // Expand all dropdown actions to proper submit elements and add
648           // them to top dropdown actions sub component.
649           $element['top']['actions']['dropdown_actions'] = array_map([$this, 'expandButton'], $widget_actions['dropdown_actions']);
650         }
651
652         if (count($info)) {
653           foreach ($info as $info_item) {
654             if (!isset($info_item['#access']) || $info_item['#access']) {
655               $element['top']['info']['items'] = $info;
656               break;
657             }
658           }
659         }
660       }
661
662       $display = EntityFormDisplay::collectRenderDisplay($paragraphs_entity, $this->getSetting('form_display_mode'));
663
664       // @todo Remove as part of https://www.drupal.org/node/2640056
665       if (\Drupal::moduleHandler()->moduleExists('field_group')) {
666         $context = [
667           'entity_type' => $paragraphs_entity->getEntityTypeId(),
668           'bundle' => $paragraphs_entity->bundle(),
669           'entity' => $paragraphs_entity,
670           'context' => 'form',
671           'display_context' => 'form',
672           'mode' => $display->getMode(),
673         ];
674
675         field_group_attach_groups($element['subform'], $context);
676         $element['subform']['#pre_render'][] = 'field_group_form_pre_render';
677       }
678
679       if ($item_mode == 'edit') {
680         $display->buildForm($paragraphs_entity, $element['subform'], $form_state);
681         // Get the field definitions of the paragraphs_entity.
682         // We need them to filter out entity reference revisions fields that
683         // reference paragraphs, cause otherwise we have problems with showing
684         // and hiding the right fields in nested paragraphs.
685         $field_definitions = $paragraphs_entity->getFieldDefinitions();
686
687         foreach (Element::children($element['subform']) as $field) {
688           // Do a check if we have to add a class to the form element. We need
689           // those classes (paragraphs-content and paragraphs-behavior) to show
690           // and hide elements, depending of the active perspective.
691           $omit_class = FALSE;
692           if (isset($field_definitions[$field])) {
693             $type = $field_definitions[$field]->getType();
694             if ($type == 'entity_reference_revisions') {
695               // Check if we are referencing paragraphs.
696               $target_entity_type = $field_definitions[$field]->get('entity_type');
697               if ($target_entity_type && $target_entity_type == 'paragraph') {
698                 $omit_class = TRUE;
699               }
700             }
701           }
702
703           if ($paragraphs_entity->hasField($field)) {
704             if (!$omit_class) {
705               $element['subform'][$field]['#attributes']['class'][] = 'paragraphs-content';
706             }
707             $translatable = $paragraphs_entity->{$field}->getFieldDefinition()->isTranslatable();
708             if ($translatable) {
709               $element['subform'][$field]['widget']['#after_build'][] = [
710                 static::class,
711                 'removeTranslatabilityClue',
712               ];
713             }
714           }
715         }
716
717         // Build the behavior plugins fields.
718         $paragraphs_type = $paragraphs_entity->getParagraphType();
719         if ($paragraphs_type && \Drupal::currentUser()->hasPermission('edit behavior plugin settings')) {
720           $element['behavior_plugins']['#weight'] = -99;
721           foreach ($paragraphs_type->getEnabledBehaviorPlugins() as $plugin_id => $plugin) {
722             $element['behavior_plugins'][$plugin_id] = [
723               '#type' => 'container',
724               '#group' => implode('][', array_merge($element_parents, ['paragraph_behavior'])),
725             ];
726             $subform_state = SubformState::createForSubform($element['behavior_plugins'][$plugin_id], $form, $form_state);
727             if ($plugin_form = $plugin->buildBehaviorForm($paragraphs_entity, $element['behavior_plugins'][$plugin_id], $subform_state)) {
728               $element['behavior_plugins'][$plugin_id] = $plugin_form;
729               // Add the paragraphs-behavior class, so that we are able to show
730               // and hide behavior fields, depending on the active perspective.
731               $element['behavior_plugins'][$plugin_id]['#attributes']['class'][] = 'paragraphs-behavior';
732             }
733           }
734         }
735       }
736       elseif ($item_mode == 'closed') {
737         $element['subform'] = [];
738         $element['behavior_plugins'] = [];
739         if ($closed_mode_setting === 'preview') {
740           // The closed paragraph is displayed as a rendered preview.
741           $view_builder = $entity_type_manager->getViewBuilder('paragraph');
742
743           $element['preview'] = $view_builder->view($paragraphs_entity, 'preview', $paragraphs_entity->language()->getId());
744           $element['preview']['#access'] = $paragraphs_entity->access('view');
745         }
746         else {
747           // The closed paragraph is displayed as a summary.
748           if ($paragraphs_entity) {
749             $summary = $paragraphs_entity->getSummary();
750             if (!empty($summary)) {
751               $element['top']['summary']['fields_info'] = [
752                 '#markup' => $summary,
753                 '#prefix' => '<div class="paragraphs-collapsed-description">',
754                 '#suffix' => '</div>',
755                 '#access' => $paragraphs_entity->access('view'),
756               ];
757             }
758           }
759         }
760       }
761       else {
762         $element['subform'] = array();
763       }
764
765       $element['subform']['#attributes']['class'][] = 'paragraphs-subform';
766       $element['subform']['#access'] = $paragraphs_entity->access('update');
767
768       if ($item_mode == 'remove') {
769         $element['#access'] = FALSE;
770       }
771
772       $widget_state['paragraphs'][$delta]['entity'] = $paragraphs_entity;
773       $widget_state['paragraphs'][$delta]['display'] = $display;
774       $widget_state['paragraphs'][$delta]['mode'] = $item_mode;
775       $widget_state['closed_mode'] = $closed_mode_setting;
776       $widget_state['autocollapse'] = $autocollapse_setting;
777
778       static::setWidgetState($parents, $field_name, $form_state, $widget_state);
779     }
780     else {
781       $element['#access'] = FALSE;
782     }
783
784     return $element;
785   }
786
787   /**
788    * Builds an add paragraph button for opening of modal form.
789    *
790    * @param array $element
791    *   Render element.
792    */
793   protected function buildModalAddForm(array &$element) {
794     // Attach the theme for the dialog template.
795     $element['#theme'] = 'paragraphs_add_dialog';
796
797     $element['add_modal_form_area'] = [
798       '#type' => 'container',
799       '#attributes' => [
800         'class' => [
801           'paragraph-type-add-modal',
802           'first-button',
803         ],
804       ],
805       '#access' => !$this->isTranslating,
806       '#weight' => -2000,
807     ];
808
809     $element['add_modal_form_area']['add_more'] = [
810       '#type' => 'submit',
811       '#value' => $this->t('Add @title', ['@title' => $this->getSetting('title')]),
812       '#name' => 'button_add_modal',
813       '#attributes' => [
814         'class' => [
815           'paragraph-type-add-modal-button',
816           'js-show',
817         ],
818       ],
819     ];
820
821     $element['#attached']['library'][] = 'paragraphs/drupal.paragraphs.modal';
822   }
823
824   /**
825    * Returns the sorted allowed types for a entity reference field.
826    *
827    * @param \Drupal\Core\Field\FieldDefinitionInterface $field_definition
828    *  (optional) The field definition forwhich the allowed types should be
829    *  returned, defaults to the current field.
830    *
831    * @return array
832    *   A list of arrays keyed by the paragraph type machine name with the following properties.
833    *     - label: The label of the paragraph type.
834    *     - weight: The weight of the paragraph type.
835    */
836   public function getAllowedTypes(FieldDefinitionInterface $field_definition = NULL) {
837
838     $return_bundles = array();
839     /** @var \Drupal\Core\Entity\EntityReferenceSelection\SelectionPluginManagerInterface $selection_manager */
840     $selection_manager = \Drupal::service('plugin.manager.entity_reference_selection');
841     $handler = $selection_manager->getSelectionHandler($field_definition ?: $this->fieldDefinition);
842     if ($handler instanceof ParagraphSelection) {
843       $return_bundles = $handler->getSortedAllowedTypes();
844     }
845     // Support for other reference types.
846     else {
847       $bundles = \Drupal::service('entity_type.bundle.info')->getBundleInfo($field_definition ? $field_definition->getSetting('target_type') : $this->fieldDefinition->getSetting('target_type'));
848       $weight = 0;
849       foreach ($bundles as $machine_name => $bundle) {
850         if (!count($this->getSelectionHandlerSetting('target_bundles'))
851           || in_array($machine_name, $this->getSelectionHandlerSetting('target_bundles'))) {
852
853           $return_bundles[$machine_name] = array(
854             'label' => $bundle['label'],
855             'weight' => $weight,
856           );
857
858           $weight++;
859         }
860       }
861     }
862
863
864     return $return_bundles;
865   }
866
867   /**
868    * {@inheritdoc}
869    */
870   public function formMultipleElements(FieldItemListInterface $items, array &$form, FormStateInterface $form_state) {
871     $field_name = $this->fieldDefinition->getName();
872     $cardinality = $this->fieldDefinition->getFieldStorageDefinition()->getCardinality();
873     $this->fieldParents = $form['#parents'];
874     $field_state = static::getWidgetState($this->fieldParents, $field_name, $form_state);
875
876     $max = $field_state['items_count'];
877     $entity_type_manager = \Drupal::entityTypeManager();
878
879     // Consider adding a default paragraph for new host entities.
880     if ($max == 0 && $items->getEntity()->isNew()) {
881       $default_type = $this->getDefaultParagraphTypeMachineName();
882
883       // Checking if default_type is not none and if is allowed.
884       if ($default_type) {
885         // Place the default paragraph.
886         $target_type = $this->getFieldSetting('target_type');
887         $paragraphs_entity = $entity_type_manager->getStorage($target_type)->create([
888           'type' => $default_type,
889         ]);
890         $field_state['selected_bundle'] = $default_type;
891         $display = EntityFormDisplay::collectRenderDisplay($paragraphs_entity, $this->getSetting('form_display_mode'));
892         $field_state['paragraphs'][0] = [
893           'entity' => $paragraphs_entity,
894           'display' => $display,
895           'mode' => 'edit',
896           'original_delta' => 1
897         ];
898         $max = 1;
899         $field_state['items_count'] = $max;
900       }
901     }
902
903     $this->realItemCount = $max;
904     $is_multiple = $this->fieldDefinition->getFieldStorageDefinition()->isMultiple();
905
906     $field_title = $this->fieldDefinition->getLabel();
907     $description = FieldFilteredMarkup::create(\Drupal::token()->replace($this->fieldDefinition->getDescription()));
908
909     $elements = array();
910     $tabs = '';
911     $this->fieldIdPrefix = implode('-', array_merge($this->fieldParents, array($field_name)));
912     $this->fieldWrapperId = Html::getUniqueId($this->fieldIdPrefix . '-add-more-wrapper');
913
914     // If the parent entity is paragraph add the nested class if not then add
915     // the perspective tabs.
916     $field_prefix = strtr($this->fieldIdPrefix, '_', '-');
917     if (count($this->fieldParents) == 0) {
918       if ($items->getEntity()->getEntityTypeId() != 'paragraph') {
919         $tabs = '<ul class="paragraphs-tabs tabs primary clearfix"><li id="content" class="tabs__tab"><a href="#' . $field_prefix . '-values">Content</a></li><li id="behavior" class="tabs__tab"><a href="#' . $field_prefix . '-values">Behavior</a></li></ul>';
920       }
921     }
922     if (count($this->fieldParents) > 0) {
923       if ($items->getEntity()->getEntityTypeId() === 'paragraph') {
924         $form['#attributes']['class'][] = 'paragraphs-nested';
925       }
926     }
927     $elements['#prefix'] = '<div class="is-horizontal paragraphs-tabs-wrapper" id="' . $this->fieldWrapperId . '">' . $tabs;
928     $elements['#suffix'] = '</div>';
929
930     $field_state['ajax_wrapper_id'] = $this->fieldWrapperId;
931     // Persist the widget state so formElement() can access it.
932     static::setWidgetState($this->fieldParents, $field_name, $form_state, $field_state);
933
934     $header_actions = $this->buildHeaderActions($field_state, $form_state);
935     if ($header_actions) {
936       $elements['header_actions'] = $header_actions;
937       // Add a weight element so we guaranty that header actions will stay in
938       // first row. We will use this later in
939       // paragraphs_preprocess_field_multiple_value_form().
940       $elements['header_actions']['_weight'] = [
941         '#type' => 'weight',
942         '#default_value' => -100,
943       ];
944     }
945
946     if (!empty($field_state['dragdrop'])) {
947       $elements['#attached']['library'][] = 'paragraphs/paragraphs-dragdrop';
948       //$elements['dragdrop_mode']['#button_type'] = 'primary';
949       $elements['dragdrop'] = $this->buildNestedParagraphsFoDragDrop($form_state, NULL, []);
950       return $elements;
951     }
952
953     if ($max > 0) {
954       for ($delta = 0; $delta < $max; $delta++) {
955
956         // Add a new empty item if it doesn't exist yet at this delta.
957         if (!isset($items[$delta])) {
958           $items->appendItem();
959         }
960
961         // For multiple fields, title and description are handled by the wrapping
962         // table.
963         $element = array(
964           '#title' => $is_multiple ? '' : $field_title,
965           '#description' => $is_multiple ? '' : $description,
966         );
967         $element = $this->formSingleElement($items, $delta, $element, $form, $form_state);
968
969         if ($element) {
970           // Input field for the delta (drag-n-drop reordering).
971           if ($is_multiple) {
972             // We name the element '_weight' to avoid clashing with elements
973             // defined by widget.
974             $element['_weight'] = array(
975               '#type' => 'weight',
976               '#title' => $this->t('Weight for row @number', array('@number' => $delta + 1)),
977               '#title_display' => 'invisible',
978               // Note: this 'delta' is the FAPI #type 'weight' element's property.
979               '#delta' => $max,
980               '#default_value' => $items[$delta]->_weight ?: $delta,
981               '#weight' => 100,
982             );
983           }
984
985           // Access for the top element is set to FALSE only when the paragraph
986           // was removed. A paragraphs that a user can not edit has access on
987           // lower level.
988           if (isset($element['#access']) && !$element['#access']) {
989             $this->realItemCount--;
990           }
991           else {
992             $elements[$delta] = $element;
993           }
994         }
995       }
996     }
997
998     $field_state = static::getWidgetState($this->fieldParents, $field_name, $form_state);
999     $field_state['real_item_count'] = $this->realItemCount;
1000     $field_state['add_mode'] = $this->getSetting('add_mode');
1001     static::setWidgetState($this->fieldParents, $field_name, $form_state, $field_state);
1002
1003     $elements += [
1004       '#element_validate' => [[$this, 'multipleElementValidate']],
1005       '#required' => $this->fieldDefinition->isRequired(),
1006       '#field_name' => $field_name,
1007       '#cardinality' => $cardinality,
1008       '#max_delta' => $max - 1,
1009     ];
1010
1011     if ($this->realItemCount > 0) {
1012       $elements += array(
1013         '#theme' => 'field_multiple_value_form',
1014         '#cardinality_multiple' => $is_multiple,
1015         '#title' => $field_title,
1016         '#description' => $description,
1017       );
1018
1019     }
1020     else {
1021       $classes = $this->fieldDefinition->isRequired() ? ['form-required'] : [];
1022       $elements += [
1023         '#type' => 'container',
1024         '#theme_wrappers' => ['container'],
1025         '#cardinality_multiple' => TRUE,
1026         'title' => [
1027           '#type' => 'html_tag',
1028           '#tag' => 'strong',
1029           '#value' => $field_title,
1030           '#attributes' => ['class' => $classes],
1031         ],
1032         'text' => [
1033           '#type' => 'container',
1034           'value' => [
1035             '#markup' => $this->t('No @title added yet.', ['@title' => $this->getSetting('title')]),
1036             '#prefix' => '<em>',
1037             '#suffix' => '</em>',
1038           ]
1039         ],
1040       ];
1041
1042       if ($description) {
1043         $elements['description'] = [
1044           '#type' => 'container',
1045           'value' => ['#markup' => $description],
1046           '#attributes' => ['class' => ['description']],
1047         ];
1048       }
1049     }
1050
1051     $host = $items->getEntity();
1052     $this->initIsTranslating($form_state, $host);
1053
1054     if (($this->realItemCount < $cardinality || $cardinality == FieldStorageDefinitionInterface::CARDINALITY_UNLIMITED) && !$form_state->isProgrammed() && !$this->isTranslating) {
1055       $elements['add_more'] = $this->buildAddActions();
1056     }
1057
1058     $elements['#attached']['library'][] = 'paragraphs/drupal.paragraphs.widget';
1059
1060     return $elements;
1061   }
1062
1063   /**
1064    * {@inheritdoc}
1065    */
1066   public function form(FieldItemListInterface $items, array &$form, FormStateInterface $form_state, $get_delta = NULL) {
1067     $parents = $form['#parents'];
1068
1069     // Identify the manage field settings default value form.
1070     if (in_array('default_value_input', $parents, TRUE)) {
1071       // Since the entity is not reusable neither cloneable, having a default
1072       // value is not supported.
1073       return ['#markup' => $this->t('No widget available for: %label.', ['%label' => $items->getFieldDefinition()->getLabel()])];
1074     }
1075
1076     return parent::form($items, $form, $form_state, $get_delta);
1077   }
1078
1079   /**
1080    * Returns a list of child paragraphs for a given field to loop over.
1081    *
1082    * @param \Drupal\Core\Form\FormStateInterface $form_state
1083    *   The form state.
1084    * @param string $field_name
1085    *   The field name for which to find child paragraphs.
1086    * @param \Drupal\paragraphs\ParagraphInterface $paragraph
1087    *   The current paragraph.
1088    * @param array $array_parents
1089    *   The current field parent structure.
1090    *
1091    * @return \Drupal\paragraphs\Entity\Paragraph[]
1092    *   Child paragraphs.
1093    */
1094   protected function getChildParagraphs(FormStateInterface $form_state, $field_name, ParagraphInterface $paragraph = NULL, array $array_parents = []) {
1095
1096     // Convert the parents structure which only includes field names and delta
1097     // to the full storage array key which includes a prefix and a subform.
1098     $full_parents_key = ['field_storage', '#parents'];
1099     foreach ($array_parents as $i => $parent) {
1100       $full_parents_key[] = $parent;
1101       if ($i % 2) {
1102         $full_parents_key[] = 'subform';
1103       }
1104     }
1105
1106     $current_parents = array_merge($full_parents_key, ['#fields', $field_name]);
1107     $child_field_state = NestedArray::getValue($form_state->getStorage(), $current_parents);
1108     $entities = [];
1109     if ($child_field_state && isset($child_field_state['paragraphs'])) {
1110       // Fetch the paragraphs from the field state. Use the original delta
1111       // to get the right position. Also reorder the paragraphs in the widget
1112       // state accordingly.
1113       $new_widget_paragraphs = [];
1114       foreach ($child_field_state['paragraphs'] as $child_delta => $child_field_item_state) {
1115         $entities[array_search($child_delta, $child_field_state['original_deltas'])] = $child_field_item_state['entity'];
1116         $new_widget_paragraphs[array_search($child_delta, $child_field_state['original_deltas'])] = $child_field_item_state;
1117       }
1118       ksort($entities);
1119
1120       // Set the orderd paragraphs into the widget state and reset original
1121       // deltas.
1122       ksort($new_widget_paragraphs);
1123       $child_field_state['paragraphs'] = $new_widget_paragraphs;
1124       $child_field_state['original_deltas'] = range(0, count($child_field_state['paragraphs']) - 1);
1125       NestedArray::setValue($form_state->getStorage(), $current_parents, $child_field_state);
1126     }
1127     elseif ($paragraph) {
1128       // If there is no field state, return the paragraphs directly from the
1129       // entity.
1130       foreach ($paragraph->get($field_name) as $child_delta => $item) {
1131         if ($item->entity) {
1132           $entities[$child_delta] = $item->entity;
1133         }
1134       }
1135     }
1136
1137     return $entities;
1138   }
1139
1140   /**
1141    * Builds the nested drag and drop structure.
1142    *
1143    * @param \Drupal\Core\Form\FormStateInterface $form_state
1144    *   The form state.
1145    * @param \Drupal\paragraphs\ParagraphInterface|null $paragraph
1146    *   The parent paragraph, NULL for the initial call.
1147    * @param string[] $array_parents
1148    *   The array parents for nested paragraphs.
1149    *
1150    * @return array
1151    *   The built form structure.
1152    */
1153   protected function buildNestedParagraphsFoDragDrop(FormStateInterface $form_state, ParagraphInterface $paragraph = NULL, array $array_parents = []) {
1154     // Look for nested elements.
1155     $elements = [];
1156     $field_definitions = [];
1157     if ($paragraph) {
1158       foreach ($paragraph->getFieldDefinitions() as $child_field_name => $field_definition) {
1159         /** @var \Drupal\Core\Field\FieldDefinitionInterface $field_definition */
1160         if ($field_definition->getType() == 'entity_reference_revisions' && $field_definition->getSetting('target_type') == 'paragraph') {
1161           $field_definitions[$child_field_name] = $field_definition;
1162         }
1163       }
1164     }
1165     else {
1166       $field_definitions = [$this->fieldDefinition->getName() => $this->fieldDefinition];
1167     }
1168
1169     /** @var \Drupal\Core\Field\FieldDefinitionInterface $field_definition */
1170     foreach ($field_definitions as $child_field_name => $field_definition) {
1171       $child_path = implode('][', array_merge($array_parents, [$child_field_name]));
1172       $cardinality = $field_definition->getFieldStorageDefinition()->getCardinality();
1173       $allowed_types = implode(array_keys($this->getAllowedTypes($field_definition)), ',');
1174       $elements[$child_field_name] = [
1175         '#type' => 'container',
1176         '#attributes' => ['class' => ['paragraphs-dragdrop-wrapper']],
1177       ];
1178
1179       // Only show a field label if there is more than one paragraph field.
1180       $label = count($field_definitions) > 1 || !$paragraph ? '<label><strong>' . $field_definition->getLabel() . '</strong></label>' : '';
1181
1182       $elements[$child_field_name]['list'] = [
1183         '#type' => 'markup',
1184         '#prefix' => $label . '<ul class="paragraphs-dragdrop" data-paragraphs-dragdrop-cardinality="' . $cardinality . '" data-paragraphs-dragdrop-allowed-types="' . $allowed_types . '" data-paragraphs-dragdrop-path="' . $child_path . '">',
1185         '#suffix' => '</ul>',
1186       ];
1187
1188       /** @var \Drupal\paragraphs\Entity\Paragraph $child_paragraph */
1189       foreach ($this->getChildParagraphs($form_state, $child_field_name, $paragraph, $array_parents) as $child_delta => $child_paragraph) {
1190         $element = [];
1191         $element['top'] = [
1192           '#type' => 'container',
1193           '#attributes' => ['class' => ['paragraphs-summary-wrapper']],
1194         ];
1195         $element['top']['paragraph_summary']['type'] = [
1196           '#markup' => '<strong>' . $child_paragraph->getParagraphType()->label() . '</strong>',
1197         ];
1198
1199         // We name the element '_weight' to avoid clashing with elements
1200         // defined by widget.
1201         $element['_weight'] = array(
1202           '#type' => 'hidden',
1203           '#default_value' => $child_delta,
1204           '#attributes' => [
1205             'class' => ['paragraphs-dragdrop__weight'],
1206           ]
1207         );
1208
1209         $element['_path'] = [
1210           '#type' => 'hidden',
1211           '#title' => $this->t('Current path for @number', ['@number' => $delta = 1]),
1212           '#title_display' => 'invisible',
1213           '#default_value' => $child_path,
1214           '#attributes' => [
1215             'class' => ['paragraphs-dragdrop__path'],
1216           ]
1217         ];
1218
1219         $summary_options = [];
1220
1221         $element['#prefix'] = '<li data-paragraphs-dragdrop-bundle="' . $child_paragraph->bundle() . '"><a href="#" class="tabledrag-handle"><div class="handle">&nbsp;</div></a>';
1222         $element['#suffix'] = '</li>';
1223         $child_array_parents = array_merge($array_parents,  [$child_field_name, $child_delta]);
1224
1225         if ($child_elements = $this->buildNestedParagraphsFoDragDrop($form_state, $child_paragraph, $child_array_parents)) {
1226           $element['dragdrop'] = $child_elements;
1227
1228           // Set the depth limit to 0 to avoid displaying a summary for the
1229           // children.
1230           $summary_options['depth_limit'] = 0;
1231         }
1232
1233         $element['top']['summary']['fields_info'] = [
1234           '#markup' => $child_paragraph->getSummary($summary_options),
1235           '#prefix' => '<div class="paragraphs-collapsed-description">',
1236           '#suffix' => '</div>',
1237         ];
1238
1239         $elements[$child_field_name]['list'][$child_delta] = $element;
1240       }
1241     }
1242     return $elements;
1243   }
1244
1245   /**
1246    * Add 'add more' button, if not working with a programmed form.
1247    *
1248    * @return array
1249    *    The form element array.
1250    */
1251   protected function buildAddActions() {
1252     if (count($this->getAccessibleOptions()) === 0) {
1253       if (count($this->getAllowedTypes()) === 0) {
1254         $add_more_elements['info'] = $this->createMessage($this->t('You are not allowed to add any of the @title types.', ['@title' => $this->getSetting('title')]));
1255       }
1256       else {
1257         $add_more_elements['info'] = $this->createMessage($this->t('You did not add any @title types yet.', ['@title' => $this->getSetting('title')]));
1258       }
1259
1260       return $add_more_elements;
1261     }
1262
1263     if (in_array($this->getSetting('add_mode'), ['button', 'dropdown', 'modal'])) {
1264       return $this->buildButtonsAddMode();
1265     }
1266
1267     return $this->buildSelectAddMode();
1268   }
1269
1270   /**
1271    * Returns the available paragraphs type.
1272    *
1273    * @return array
1274    *   Available paragraphs types.
1275    */
1276   protected function getAccessibleOptions() {
1277     if ($this->accessOptions !== NULL) {
1278       return $this->accessOptions;
1279     }
1280
1281     $entity_type_manager = \Drupal::entityTypeManager();
1282     $target_type = $this->getFieldSetting('target_type');
1283     $bundles = $this->getAllowedTypes();
1284     $access_control_handler = $entity_type_manager->getAccessControlHandler($target_type);
1285     $dragdrop_settings = $this->getSelectionHandlerSetting('target_bundles_drag_drop');
1286
1287     foreach ($bundles as $machine_name => $bundle) {
1288       if ($dragdrop_settings || (!count($this->getSelectionHandlerSetting('target_bundles'))
1289           || in_array($machine_name, $this->getSelectionHandlerSetting('target_bundles')))) {
1290         if ($access_control_handler->createAccess($machine_name)) {
1291           $this->accessOptions[$machine_name] = $bundle['label'];
1292         }
1293       }
1294     }
1295
1296     return $this->accessOptions;
1297   }
1298
1299   /**
1300    * Helper to create a paragraph UI message.
1301    *
1302    * @param string $message
1303    *   Message text.
1304    * @param string $type
1305    *   Message type.
1306    *
1307    * @return array
1308    *   Render array of message.
1309    */
1310   public function createMessage($message, $type = 'warning') {
1311     return [
1312       '#type' => 'container',
1313       '#markup' => $message,
1314       '#attributes' => ['class' => ['messages', 'messages--' . $type]],
1315     ];
1316   }
1317
1318   /**
1319    * Expand button base array into a paragraph widget action button.
1320    *
1321    * @param array $button_base
1322    *   Button base render array.
1323    *
1324    * @return array
1325    *   Button render array.
1326    */
1327   public static function expandButton(array $button_base) {
1328     // Do not expand elements that do not have submit handler.
1329     if (empty($button_base['#submit'])) {
1330       return $button_base;
1331     }
1332
1333     $button = $button_base + [
1334       '#type' => 'submit',
1335       '#theme_wrappers' => ['input__submit__paragraph_action'],
1336     ];
1337
1338     // Html::getId will give us '-' char in name but we want '_' for now so
1339     // we use strtr to search&replace '-' to '_'.
1340     $button['#name'] = strtr(Html::getId($button_base['#name']), '-', '_');
1341     $button['#id'] = Html::getUniqueId($button['#name']);
1342
1343     if (isset($button['#ajax'])) {
1344       $button['#ajax'] += [
1345         'effect' => 'fade',
1346         // Since a normal throbber is added inline, this has the potential to
1347         // break a layout if the button is located in dropbuttons. Instead,
1348         // it's safer to just show the fullscreen progress element instead.
1349         'progress' => ['type' => 'fullscreen'],
1350       ];
1351     }
1352
1353     return $button;
1354   }
1355
1356   /**
1357    * Get common submit element information for processing ajax submit handlers.
1358    *
1359    * @param array $form
1360    *   Form array.
1361    * @param FormStateInterface $form_state
1362    *   Form state object.
1363    * @param int $position
1364    *   Position of triggering element.
1365    *
1366    * @return array
1367    *   Submit element information.
1368    */
1369   public static function getSubmitElementInfo(array $form, FormStateInterface $form_state, $position = ParagraphsWidget::ACTION_POSITION_BASE) {
1370     $submit['button'] = $form_state->getTriggeringElement();
1371
1372     // Go up in the form, to the widgets container.
1373     if ($position == ParagraphsWidget::ACTION_POSITION_BASE) {
1374       $submit['element'] = NestedArray::getValue($form, array_slice($submit['button']['#array_parents'], 0, -2));
1375     }
1376     if ($position == ParagraphsWidget::ACTION_POSITION_HEADER) {
1377       $submit['element'] = NestedArray::getValue($form, array_slice($submit['button']['#array_parents'], 0, -3));
1378     }
1379     elseif ($position == ParagraphsWidget::ACTION_POSITION_ACTIONS) {
1380       $submit['element'] = NestedArray::getValue($form, array_slice($submit['button']['#array_parents'], 0, -5));
1381       $delta = array_slice($submit['button']['#array_parents'], -5, -4);
1382       $submit['delta'] = $delta[0];
1383     }
1384
1385     $submit['field_name'] = $submit['element']['#field_name'];
1386     $submit['parents'] = $submit['element']['#field_parents'];
1387
1388     // Get widget state.
1389     $submit['widget_state'] = static::getWidgetState($submit['parents'], $submit['field_name'], $form_state);
1390
1391     return $submit;
1392   }
1393
1394   /**
1395    * Build drop button.
1396    *
1397    * @param array $elements
1398    *   Elements for drop button.
1399    *
1400    * @return array
1401    *   Drop button array.
1402    */
1403   protected function buildDropbutton(array $elements = []) {
1404     $build = [
1405       '#type' => 'container',
1406       '#attributes' => ['class' => ['paragraphs-dropbutton-wrapper']],
1407     ];
1408
1409     $operations = [];
1410     // Because we are cloning the elements into title sub element we need to
1411     // sort children first.
1412     foreach (Element::children($elements, TRUE) as $child) {
1413       // Clone the element as an operation.
1414       $operations[$child] = ['title' => $elements[$child]];
1415
1416       // Flag the original element as printed so it doesn't render twice.
1417       $elements[$child]['#printed'] = TRUE;
1418     }
1419
1420     $build['operations'] = [
1421       '#type' => 'paragraph_operations',
1422       // Even though operations are run through the "links" element type, the
1423       // theme system will render any render array passed as a link "title".
1424       '#links' => $operations,
1425     ];
1426
1427     return $build + $elements;
1428   }
1429
1430   /**
1431    * Builds dropdown button for adding new paragraph.
1432    *
1433    * @return array
1434    *   The form element array.
1435    */
1436   protected function buildButtonsAddMode() {
1437     $options = $this->getAccessibleOptions();
1438     $add_mode = $this->getSetting('add_mode');
1439     $paragraphs_type_storage = \Drupal::entityTypeManager()->getStorage('paragraphs_type');
1440
1441     // Build the buttons.
1442     $add_more_elements = [];
1443     foreach ($options as $machine_name => $label) {
1444       $button_key = 'add_more_button_' . $machine_name;
1445       $add_more_elements[$button_key] = $this->expandButton([
1446         '#type' => 'submit',
1447         '#name' => $this->fieldIdPrefix . '_' . $machine_name . '_add_more',
1448         '#value' => $add_mode == 'modal' ? $label : $this->t('Add @type', ['@type' => $label]),
1449         '#attributes' => ['class' => ['field-add-more-submit']],
1450         '#limit_validation_errors' => [array_merge($this->fieldParents, [$this->fieldDefinition->getName(), 'add_more'])],
1451         '#submit' => [[get_class($this), 'addMoreSubmit']],
1452         '#ajax' => [
1453           'callback' => [get_class($this), 'addMoreAjax'],
1454           'wrapper' => $this->fieldWrapperId,
1455         ],
1456         '#bundle_machine_name' => $machine_name,
1457       ]);
1458
1459       if ($add_mode === 'modal' && $icon_url = $paragraphs_type_storage->load($machine_name)->getIconUrl()) {
1460         $add_more_elements[$button_key]['#attributes']['style'] = 'background-image: url(' . $icon_url . ');';
1461       }
1462     }
1463
1464     // Determine if buttons should be rendered as dropbuttons.
1465     if (count($options) > 1 && $add_mode == 'dropdown') {
1466       $add_more_elements = $this->buildDropbutton($add_more_elements);
1467       $add_more_elements['#suffix'] = $this->t('to %type', ['%type' => $this->fieldDefinition->getLabel()]);
1468     }
1469     elseif ($add_mode == 'modal') {
1470       $this->buildModalAddForm($add_more_elements);
1471       $add_more_elements['add_modal_form_area']['#suffix'] = $this->t('to %type', ['%type' => $this->fieldDefinition->getLabel()]);
1472     }
1473     $add_more_elements['#weight'] = 1;
1474
1475     return $add_more_elements;
1476   }
1477
1478   /**
1479    * Builds list of actions based on paragraphs type.
1480    *
1481    * @return array
1482    *   The form element array.
1483    */
1484   protected function buildSelectAddMode() {
1485     $field_name = $this->fieldDefinition->getName();
1486     $field_title = $this->fieldDefinition->getLabel();
1487     $setting_title = $this->getSetting('title');
1488     $add_more_elements['add_more_select'] = [
1489       '#type' => 'select',
1490       '#options' => $this->getAccessibleOptions(),
1491       '#title' => $this->t('@title type', ['@title' => $setting_title]),
1492       '#label_display' => 'hidden',
1493     ];
1494
1495     $text = $this->t('Add @title', ['@title' => $setting_title]);
1496
1497     if ($this->realItemCount > 0) {
1498       $text = $this->t('Add another @title', ['@title' => $setting_title]);
1499     }
1500
1501     $add_more_elements['add_more_button'] = [
1502       '#type' => 'submit',
1503       '#name' => strtr($this->fieldIdPrefix, '-', '_') . '_add_more',
1504       '#value' => $text,
1505       '#attributes' => ['class' => ['field-add-more-submit']],
1506       '#limit_validation_errors' => [array_merge($this->fieldParents, [$field_name, 'add_more'])],
1507       '#submit' => [[get_class($this), 'addMoreSubmit']],
1508       '#ajax' => [
1509         'callback' => [get_class($this), 'addMoreAjax'],
1510         'wrapper' => $this->fieldWrapperId,
1511         'effect' => 'fade',
1512       ],
1513     ];
1514
1515     $add_more_elements['add_more_button']['#suffix'] = $this->t(' to %type', ['%type' => $field_title]);
1516     return $add_more_elements;
1517   }
1518
1519   /**
1520    * {@inheritdoc}
1521    */
1522   public static function addMoreAjax(array $form, FormStateInterface $form_state) {
1523     $submit = ParagraphsWidget::getSubmitElementInfo($form, $form_state);
1524     $element = $submit['element'];
1525
1526     // Add a DIV around the delta receiving the Ajax effect.
1527     $delta = $submit['element']['#max_delta'];
1528     $element[$delta]['#prefix'] = '<div class="ajax-new-content">' . (isset($element[$delta]['#prefix']) ? $element[$delta]['#prefix'] : '');
1529     $element[$delta]['#suffix'] = (isset($element[$delta]['#suffix']) ? $element[$delta]['#suffix'] : '') . '</div>';
1530
1531     return $element;
1532   }
1533
1534   /**
1535    * Ajax callback for all actions.
1536    */
1537   public static function allActionsAjax(array $form, FormStateInterface $form_state) {
1538     $submit = ParagraphsWidget::getSubmitElementInfo($form, $form_state, ParagraphsWidget::ACTION_POSITION_HEADER);
1539     $element = $submit['element'];
1540
1541     // Add a DIV around the delta receiving the Ajax effect.
1542     $delta = $submit['element']['#max_delta'];
1543     $element[$delta]['#prefix'] = '<div class="ajax-new-content">' . (isset($element[$delta]['#prefix']) ? $element[$delta]['#prefix'] : '');
1544     $element[$delta]['#suffix'] = (isset($element[$delta]['#suffix']) ? $element[$delta]['#suffix'] : '') . '</div>';
1545
1546     return $element;
1547   }
1548
1549   /**
1550    * {@inheritdoc}
1551    */
1552   public static function addMoreSubmit(array $form, FormStateInterface $form_state) {
1553     $submit = ParagraphsWidget::getSubmitElementInfo($form, $form_state);
1554
1555     if ($submit['widget_state']['real_item_count'] < $submit['element']['#cardinality'] || $submit['element']['#cardinality'] == FieldStorageDefinitionInterface::CARDINALITY_UNLIMITED) {
1556       $submit['widget_state']['items_count']++;
1557     }
1558
1559     if (isset($submit['button']['#bundle_machine_name'])) {
1560       $submit['widget_state']['selected_bundle'] = $submit['button']['#bundle_machine_name'];
1561     }
1562     else {
1563       $submit['widget_state']['selected_bundle'] = $submit['element']['add_more']['add_more_select']['#value'];
1564     }
1565
1566     $submit['widget_state'] = static::autocollapse($submit['widget_state']);
1567
1568     static::setWidgetState($submit['parents'], $submit['field_name'], $form_state, $submit['widget_state']);
1569
1570     $form_state->setRebuild();
1571   }
1572
1573   /**
1574    * Creates a duplicate of the paragraph entity.
1575    */
1576   public static function duplicateSubmit(array $form, FormStateInterface $form_state) {
1577     $button = $form_state->getTriggeringElement();
1578     // Go one level up in the form, to the widgets container.
1579     $element = NestedArray::getValue($form, array_slice($button['#array_parents'], 0, -5));
1580     $field_name = $element['#field_name'];
1581     $parents = $element['#field_parents'];
1582
1583     // Inserting new element in the array.
1584     $widget_state = static::getWidgetState($parents, $field_name, $form_state);
1585
1586     // Map the button delta to the actual delta.
1587     $original_button_delta = $button['#delta'];
1588     $current_button_delta = array_search($button['#delta'], $widget_state['original_deltas']);
1589
1590     $widget_state['items_count']++;
1591     $widget_state['real_item_count']++;
1592
1593     // Initialize the new original delta map with the new entry.
1594     $new_original_deltas = [
1595       $current_button_delta + 1 => count($widget_state['original_deltas']),
1596     ];
1597
1598     $user_input = NestedArray::getValue($form_state->getUserInput(), array_slice($button['#parents'], 0, -5));
1599     $user_input[count($widget_state['original_deltas'])]['_weight'] = $current_button_delta + 1;
1600
1601     // Increase all original deltas bigger than the delta of the duplicated
1602     // element by one.
1603     foreach ($widget_state['original_deltas'] as $current_delta => $original_delta) {
1604       $new_delta = $current_delta > $current_button_delta ? $current_delta + 1 : $current_delta;
1605       $new_original_deltas[$new_delta] = $original_delta;
1606       $user_input[$original_delta]['_weight'] = $new_delta;
1607     }
1608     $widget_state['original_deltas'] = $new_original_deltas;
1609     /** @var \Drupal\Core\Entity\EntityInterface $entity */
1610     $entity = $widget_state['paragraphs'][$original_button_delta]['entity'];
1611
1612     $widget_state = static::autocollapse($widget_state);
1613
1614     // Check if the replicate module is enabled.
1615     if (\Drupal::hasService('replicate.replicator')) {
1616       $duplicate_entity = \Drupal::getContainer()->get('replicate.replicator')->replicateEntity($entity);
1617     }
1618     else {
1619       $duplicate_entity = $entity->createDuplicate();
1620     }
1621     // Create the duplicated paragraph and insert it below the original.
1622     $widget_state['paragraphs'][] = [
1623       'entity' => $duplicate_entity,
1624       'display' => $widget_state['paragraphs'][$original_button_delta]['display'],
1625       'mode' => 'edit',
1626     ];
1627
1628     NestedArray::setValue($form_state->getUserInput(), array_slice($button['#parents'], 0, -5), $user_input);
1629     static::setWidgetState($parents, $field_name, $form_state, $widget_state);
1630     $form_state->setRebuild();
1631   }
1632
1633   public static function paragraphsItemSubmit(array $form, FormStateInterface $form_state) {
1634     $submit = ParagraphsWidget::getSubmitElementInfo($form, $form_state, ParagraphsWidget::ACTION_POSITION_ACTIONS);
1635
1636     $new_mode = $submit['button']['#paragraphs_mode'];
1637
1638     if ($new_mode === 'edit') {
1639       $submit['widget_state'] = static::autocollapse($submit['widget_state']);
1640     }
1641
1642     $submit['widget_state']['paragraphs'][$submit['delta']]['mode'] = $new_mode;
1643
1644     if (!empty($submit['button']['#paragraphs_show_warning'])) {
1645       $submit['widget_state']['paragraphs'][$submit['delta']]['show_warning'] = $submit['button']['#paragraphs_show_warning'];
1646     }
1647
1648     static::setWidgetState($submit['parents'], $submit['field_name'], $form_state, $submit['widget_state']);
1649
1650     $form_state->setRebuild();
1651   }
1652
1653   public static function itemAjax(array $form, FormStateInterface $form_state) {
1654     $submit = ParagraphsWidget::getSubmitElementInfo($form, $form_state, ParagraphsWidget::ACTION_POSITION_ACTIONS);
1655
1656     $submit['element']['#prefix'] = '<div class="ajax-new-content">' . (isset($submit['element']['#prefix']) ? $submit['element']['#prefix'] : '');
1657     $submit['element']['#suffix'] = (isset($submit['element']['#suffix']) ? $submit['element']['#suffix'] : '') . '</div>';
1658
1659     return $submit['element'];
1660   }
1661
1662   /**
1663    * Sets the form mode accordingly.
1664    *
1665    * @param array $form
1666    *   An associate array containing the structure of the form.
1667    * @param \Drupal\Core\Form\FormStateInterface $form_state
1668    *   The current state of the form.
1669    */
1670   public static function dragDropModeSubmit(array $form, FormStateInterface $form_state) {
1671     $submit = ParagraphsWidget::getSubmitElementInfo($form, $form_state, ParagraphsWidget::ACTION_POSITION_HEADER);
1672
1673     if (empty($submit['widget_state']['dragdrop'])) {
1674       $submit['widget_state']['dragdrop'] = TRUE;
1675     }
1676     else {
1677       $submit['widget_state']['dragdrop'] = FALSE;
1678     }
1679
1680     // Make sure that flag that we already reordered is unset when the mode is
1681     // switched.
1682     unset($submit['widget_state']['reordered']);
1683
1684     // Switch the form mode accordingly.
1685     static::setWidgetState($submit['parents'], $submit['field_name'], $form_state, $submit['widget_state']);
1686
1687     $form_state->setRebuild();
1688   }
1689
1690
1691   /**
1692    * Reorder paragraphs.
1693    *
1694    * @param \Drupal\Core\Form\FormStateInterface $form_state
1695    *   The form state.
1696    * @param $field_values_parents
1697    *   The field value parents.
1698    */
1699   protected static function reorderParagraphs(FormStateInterface $form_state, $field_values_parents) {
1700     $field_name = end($field_values_parents);
1701     $field_values = NestedArray::getValue($form_state->getValues(), $field_values_parents);
1702     $complete_field_storage = NestedArray::getValue(
1703       $form_state->getStorage(), [
1704         'field_storage',
1705         '#parents'
1706       ]
1707     );
1708     $new_field_storage = $complete_field_storage;
1709
1710     // Set a flag to prevent this from running twice, as the entity is built
1711     // for validation as well as saving and would fail the second time as we
1712     // already altered the field storage.
1713     if (!empty($new_field_storage['#fields'][$field_name]['reordered'])) {
1714       return;
1715     }
1716     $new_field_storage['#fields'][$field_name]['reordered'] = TRUE;
1717
1718     // Clear out all current paragraphs keys in all nested paragraph widgets
1719     // as there might be fewer than before or none in a certain widget.
1720     $clear_paragraphs = function ($field_storage) use (&$clear_paragraphs) {
1721       foreach ($field_storage as $key => $value) {
1722         if ($key === '#fields') {
1723           foreach ($value as $field_name => $widget_state) {
1724             if (isset($widget_state['paragraphs'])) {
1725               $field_storage['#fields'][$field_name]['paragraphs'] = [];
1726             }
1727           }
1728         }
1729         else {
1730           $field_storage[$key] = $clear_paragraphs($field_storage[$key]);
1731         }
1732       }
1733       return $field_storage;
1734     };
1735
1736     // Only clear the current field and its children to avoid deleting
1737     // paragraph references in other fields.
1738     $new_field_storage['#fields'][$field_name]['paragraphs'] = [];
1739     if (isset($new_field_storage[$field_name])) {
1740       $new_field_storage[$field_name] = $clear_paragraphs($new_field_storage[$field_name]);
1741     }
1742
1743     $reorder_paragraphs = function ($reorder_values, $parents = [], FieldableEntityInterface $parent_entity = NULL) use ($complete_field_storage, &$new_field_storage, &$reorder_paragraphs) {
1744       foreach ($reorder_values as $field_name => $values) {
1745         foreach ($values['list'] as $delta => $item_values) {
1746           $old_keys = array_merge(
1747             $parents, [
1748               '#fields',
1749               $field_name,
1750               'paragraphs',
1751               $delta
1752             ]
1753           );
1754           $path = explode('][', $item_values['_path']);
1755           $new_field_name = array_pop($path);
1756           $key_parents = [];
1757           foreach ($path as $i => $key) {
1758             $key_parents[] = $key;
1759             if ($i % 2 == 1) {
1760               $key_parents[] = 'subform';
1761             }
1762           }
1763           $new_keys = array_merge(
1764             $key_parents, [
1765               '#fields',
1766               $new_field_name,
1767               'paragraphs',
1768               $item_values['_weight']
1769             ]
1770           );
1771           $key_exists = NULL;
1772           $item_state = NestedArray::getValue($complete_field_storage, $old_keys, $key_exists);
1773           if (!$key_exists && $parent_entity) {
1774             // If key does not exist, then this parent widget was previously
1775             // not expanded. This can only happen on nested levels. In that
1776             // case, initialize a new item state and set the widget state to
1777             // an empty array if it is not already set from an earlier item.
1778             // If something else is placed there, it will be put in there,
1779             // otherwise the widget will know that nothing is there anymore.
1780             $item_state = [
1781               'entity' => $parent_entity->get($field_name)->get($delta)->entity,
1782               'mode' => 'closed',
1783             ];
1784             $widget_state_keys = array_slice($old_keys, 0, count($old_keys) - 2);
1785             if (!NestedArray::getValue($new_field_storage, $widget_state_keys)) {
1786               NestedArray::setValue($new_field_storage, $widget_state_keys, ['paragraphs' => []]);
1787             }
1788           }
1789
1790           // Ensure the referenced paragraph will be saved.
1791           $item_state['entity']->setNeedsSave(TRUE);
1792
1793           NestedArray::setValue($new_field_storage, $new_keys, $item_state);
1794           if (isset($item_values['dragdrop'])) {
1795             $reorder_paragraphs(
1796               $item_values['dragdrop'], array_merge(
1797               $parents, [
1798                 $field_name,
1799                 $delta,
1800                 'subform'
1801               ]
1802             ), $item_state['entity']
1803             );
1804           }
1805         }
1806       }
1807     };
1808     $reorder_paragraphs($field_values['dragdrop']);
1809
1810     // Recalculate original deltas.
1811     $recalculate_original_deltas = function ($field_storage, ContentEntityInterface $parent_entity) use (&$recalculate_original_deltas) {
1812       if (isset($field_storage['#fields'])) {
1813         foreach ($field_storage['#fields'] as $field_name => $widget_state) {
1814           if (isset($widget_state['paragraphs'])) {
1815
1816             // If the parent field does not exist but we have paragraphs in
1817             // widget state, something went wrong and we have a mismatch.
1818             // Throw an exception.
1819             if (!$parent_entity->hasField($field_name) && !empty($widget_state['paragraphs'])) {
1820               throw new \LogicException('Reordering paragraphs resulted in paragraphs on non-existing field ' . $field_name . ' on parent entity ' . $parent_entity->getEntityTypeId() . '/' . $parent_entity->id());
1821             }
1822
1823             // Sort the paragraphs by key so that they will be assigned to
1824             // the entity in the right order. Reset the deltas.
1825             ksort($widget_state['paragraphs']);
1826             $widget_state['paragraphs'] = array_values($widget_state['paragraphs']);
1827
1828             $original_deltas = range(0, count($widget_state['paragraphs']) - 1);
1829             $field_storage['#fields'][$field_name]['original_deltas'] = $original_deltas;
1830             $field_storage['#fields'][$field_name]['items_count'] = count($widget_state['paragraphs']);
1831             $field_storage['#fields'][$field_name]['real_item_count'] = count($widget_state['paragraphs']);
1832
1833             // Update the parent entity and point to the new children, if the
1834             // parent field does not exist, we also have no paragraphs, so
1835             // we can just skip this, this is a dead leaf after re-ordering.
1836             // @todo Clean this up somehow?
1837             if ($parent_entity->hasField($field_name)) {
1838               $parent_entity->set($field_name, array_column($widget_state['paragraphs'], 'entity'));
1839
1840               // Next process that field recursively.
1841               foreach (array_keys($widget_state['paragraphs']) as $delta) {
1842                 if (isset($field_storage[$field_name][$delta]['subform'])) {
1843                   $field_storage[$field_name][$delta]['subform'] = $recalculate_original_deltas($field_storage[$field_name][$delta]['subform'], $parent_entity->get($field_name)->get($delta)->entity);
1844                 }
1845               }
1846             }
1847
1848           }
1849         }
1850       }
1851       return $field_storage;
1852     };
1853
1854     $parent_entity = $form_state->getFormObject()->getEntity();
1855     $new_field_storage = $recalculate_original_deltas($new_field_storage, $parent_entity);
1856
1857     $form_state->set(['field_storage', '#parents'], $new_field_storage);
1858   }
1859
1860   /**
1861    * Ajax callback for the dragdrop mode.
1862    *
1863    * @param array $form
1864    *   An associate array containing the structure of the form.
1865    * @param \Drupal\Core\Form\FormStateInterface $form_state
1866    *   The current state of the form.
1867    *
1868    * @return array
1869    *   The container form element.
1870    */
1871   public static function dragDropModeAjax(array $form, FormStateInterface $form_state) {
1872     $submit = ParagraphsWidget::getSubmitElementInfo($form, $form_state, ParagraphsWidget::ACTION_POSITION_HEADER);
1873
1874     $submit['element']['#prefix'] = '<div class="ajax-new-content">' . (isset($submit['element']['#prefix']) ? $submit['element']['#prefix'] : '');
1875     $submit['element']['#suffix'] = (isset($submit['element']['#suffix']) ? $submit['element']['#suffix'] : '') . '</div>';
1876
1877     return $submit['element'];
1878   }
1879
1880   /**
1881    * Returns the value of a setting for the entity reference selection handler.
1882    *
1883    * @param string $setting_name
1884    *   The setting name.
1885    *
1886    * @return mixed
1887    *   The setting value.
1888    */
1889   protected function getSelectionHandlerSetting($setting_name) {
1890     $settings = $this->getFieldSetting('handler_settings');
1891     return isset($settings[$setting_name]) ? $settings[$setting_name] : NULL;
1892   }
1893
1894   /**
1895    * {@inheritdoc}
1896    */
1897   public function elementValidate($element, FormStateInterface $form_state, $form) {
1898     $field_name = $this->fieldDefinition->getName();
1899     $widget_state = static::getWidgetState($element['#field_parents'], $field_name, $form_state);
1900     $delta = $element['#delta'];
1901
1902     if (isset($widget_state['paragraphs'][$delta]['entity'])) {
1903       /** @var \Drupal\paragraphs\ParagraphInterface $paragraphs_entity */
1904       $entity = $widget_state['paragraphs'][$delta]['entity'];
1905
1906       /** @var \Drupal\Core\Entity\Display\EntityFormDisplayInterface $display */
1907       $display = $widget_state['paragraphs'][$delta]['display'];
1908
1909       if ($widget_state['paragraphs'][$delta]['mode'] == 'edit') {
1910         // Extract the form values on submit for getting the current paragraph.
1911         $display->extractFormValues($entity, $element['subform'], $form_state);
1912         $display->validateFormValues($entity, $element['subform'], $form_state);
1913
1914         // Validate all enabled behavior plugins.
1915         $paragraphs_type = $entity->getParagraphType();
1916         if (\Drupal::currentUser()->hasPermission('edit behavior plugin settings')) {
1917           foreach ($paragraphs_type->getEnabledBehaviorPlugins() as $plugin_id => $plugin_values) {
1918             $subform_state = SubformState::createForSubform($element['behavior_plugins'][$plugin_id], $form_state->getCompleteForm(), $form_state);
1919             $plugin_values->validateBehaviorForm($entity, $element['behavior_plugins'][$plugin_id], $subform_state);
1920           }
1921         }
1922       }
1923     }
1924
1925     static::setWidgetState($element['#field_parents'], $field_name, $form_state, $widget_state);
1926   }
1927
1928   /**
1929    * {@inheritdoc}
1930    */
1931   public function flagErrors(FieldItemListInterface $items, ConstraintViolationListInterface $violations, array $form, FormStateInterface $form_state) {
1932     $field_name = $this->fieldDefinition->getName();
1933
1934     $field_state = static::getWidgetState($form['#parents'], $field_name, $form_state);
1935
1936     // In dragdrop mode, validation errors can not be mapped to form elements,
1937     // add them on the top level widget element.
1938     if (!empty($field_state['dragdrop'])) {
1939       if ($violations->count()) {
1940         $element = NestedArray::getValue($form_state->getCompleteForm(), $field_state['array_parents']);
1941         foreach ($violations as $violation) {
1942           $form_state->setError($element, $violation->getMessage());
1943         }
1944       }
1945     }
1946     else {
1947       return parent::flagErrors($items, $violations, $form, $form_state);
1948     }
1949   }
1950
1951   /**
1952    * Special handling to validate form elements with multiple values.
1953    *
1954    * @param array $elements
1955    *   An associative array containing the substructure of the form to be
1956    *   validated in this call.
1957    * @param \Drupal\Core\Form\FormStateInterface $form_state
1958    *   The current state of the form.
1959    * @param array $form
1960    *   The complete form array.
1961    */
1962   public function multipleElementValidate(array $elements, FormStateInterface $form_state, array $form) {
1963     $field_name = $this->fieldDefinition->getName();
1964     $widget_state = static::getWidgetState($elements['#field_parents'], $field_name, $form_state);
1965
1966     if ($elements['#required'] && $widget_state['real_item_count'] < 1) {
1967       $form_state->setError($elements, t('@name field is required.', ['@name' => $this->fieldDefinition->getLabel()]));
1968     }
1969
1970     static::setWidgetState($elements['#field_parents'], $field_name, $form_state, $widget_state);
1971   }
1972
1973   /**
1974    * {@inheritdoc}
1975    */
1976   public function massageFormValues(array $values, array $form, FormStateInterface $form_state) {
1977     $field_name = $this->fieldDefinition->getName();
1978     $widget_state = static::getWidgetState($form['#parents'], $field_name, $form_state);
1979     $element = NestedArray::getValue($form_state->getCompleteForm(), $widget_state['array_parents']);
1980
1981     if (!empty($widget_state['dragdrop'])) {
1982       $path = array_merge($form['#parents'], array($field_name));
1983       static::reorderParagraphs($form_state, $path);
1984
1985       // After re-ordering, get the updated widget state.
1986       $widget_state = static::getWidgetState($form['#parents'], $field_name, $form_state);
1987
1988       // Re-create values based on current widget state.
1989       $values = [];
1990       foreach ($widget_state['paragraphs'] as $delta => $paragraph_state) {
1991         $values[$delta]['entity'] = $paragraph_state['entity'];
1992       }
1993       return $values;
1994     }
1995
1996     foreach ($values as $delta => &$item) {
1997       if (isset($widget_state['paragraphs'][$item['_original_delta']]['entity'])
1998         && $widget_state['paragraphs'][$item['_original_delta']]['mode'] != 'remove') {
1999         /** @var \Drupal\paragraphs\ParagraphInterface $paragraphs_entity */
2000         $paragraphs_entity = $widget_state['paragraphs'][$item['_original_delta']]['entity'];
2001
2002         /** @var \Drupal\Core\Entity\Display\EntityFormDisplayInterface $display */
2003         $display = $widget_state['paragraphs'][$item['_original_delta']]['display'];
2004         if ($widget_state['paragraphs'][$item['_original_delta']]['mode'] == 'edit') {
2005           $display->extractFormValues($paragraphs_entity, $element[$item['_original_delta']]['subform'], $form_state);
2006         }
2007         // A content entity form saves without any rebuild. It needs to set the
2008         // language to update it in case of language change.
2009         $langcode_key = $paragraphs_entity->getEntityType()->getKey('langcode');
2010         if ($paragraphs_entity->get($langcode_key)->value != $form_state->get('langcode')) {
2011           // If a translation in the given language already exists, switch to
2012           // that. If there is none yet, update the language.
2013           if ($paragraphs_entity->hasTranslation($form_state->get('langcode'))) {
2014             $paragraphs_entity = $paragraphs_entity->getTranslation($form_state->get('langcode'));
2015           }
2016           else {
2017             $paragraphs_entity->set($langcode_key, $form_state->get('langcode'));
2018           }
2019         }
2020         if (isset($item['behavior_plugins'])) {
2021           // Submit all enabled behavior plugins.
2022           $paragraphs_type = $paragraphs_entity->getParagraphType();
2023           foreach ($paragraphs_type->getEnabledBehaviorPlugins() as $plugin_id => $plugin_values) {
2024             if (!isset($item['behavior_plugins'][$plugin_id])) {
2025               $item['behavior_plugins'][$plugin_id] = [];
2026             }
2027             $original_delta = $item['_original_delta'];
2028             if (isset($element[$original_delta]) && isset($element[$original_delta]['behavior_plugins'][$plugin_id]) && $form_state->getCompleteForm() && \Drupal::currentUser()->hasPermission('edit behavior plugin settings')) {
2029               $subform_state = SubformState::createForSubform($element[$original_delta]['behavior_plugins'][$plugin_id], $form_state->getCompleteForm(), $form_state);
2030               if (isset($item['behavior_plugins'][$plugin_id])) {
2031                 $plugin_values->submitBehaviorForm($paragraphs_entity, $item['behavior_plugins'][$plugin_id], $subform_state);
2032               }
2033             }
2034           }
2035         }
2036
2037         $paragraphs_entity->setNeedsSave(TRUE);
2038         $item['entity'] = $paragraphs_entity;
2039         $item['target_id'] = $paragraphs_entity->id();
2040         $item['target_revision_id'] = $paragraphs_entity->getRevisionId();
2041       }
2042       // If our mode is remove don't save or reference this entity.
2043       // @todo: Maybe we should actually delete it here?
2044       elseif (isset($widget_state['paragraphs'][$item['_original_delta']]['mode']) && $widget_state['paragraphs'][$item['_original_delta']]['mode'] == 'remove') {
2045         $item['target_id'] = NULL;
2046         $item['target_revision_id'] = NULL;
2047       }
2048     }
2049     return $values;
2050   }
2051
2052   /**
2053    * {@inheritdoc}
2054    */
2055   public function extractFormValues(FieldItemListInterface $items, array $form, FormStateInterface $form_state) {
2056     // Filter possible empty items.
2057     $items->filterEmptyItems();
2058
2059     // Remove buttons from header actions.
2060     $field_name = $this->fieldDefinition->getName();
2061     $path = array_merge($form['#parents'], array($field_name));
2062     $form_state_variables = $form_state->getValues();
2063     $key_exists = NULL;
2064     $values = NestedArray::getValue($form_state_variables, $path, $key_exists);
2065
2066     if ($key_exists) {
2067       unset($values['header_actions']);
2068
2069       NestedArray::setValue($form_state_variables, $path, $values);
2070       $form_state->setValues($form_state_variables);
2071     }
2072
2073     return parent::extractFormValues($items, $form, $form_state);
2074   }
2075
2076   /**
2077    * Initializes the translation form state.
2078    *
2079    * @param \Drupal\Core\Form\FormStateInterface $form_state
2080    * @param \Drupal\Core\Entity\ContentEntityInterface $host
2081    */
2082   protected function initIsTranslating(FormStateInterface $form_state, ContentEntityInterface $host) {
2083     if ($this->isTranslating != NULL) {
2084       return;
2085     }
2086     $this->isTranslating = FALSE;
2087     if (!$host->isTranslatable()) {
2088       return;
2089     }
2090     if (!$host->getEntityType()->hasKey('default_langcode')) {
2091       return;
2092     }
2093     $default_langcode_key = $host->getEntityType()->getKey('default_langcode');
2094     if (!$host->hasField($default_langcode_key)) {
2095       return;
2096     }
2097
2098     if (!empty($form_state->get('content_translation'))) {
2099       // Adding a language through the ContentTranslationController.
2100       $this->isTranslating = TRUE;
2101     }
2102     if ($host->hasTranslation($form_state->get('langcode')) && $host->getTranslation($form_state->get('langcode'))->get($default_langcode_key)->value == 0) {
2103       // Editing a translation.
2104       $this->isTranslating = TRUE;
2105     }
2106   }
2107
2108   /**
2109    * After-build callback for removing the translatability clue from the widget.
2110    *
2111    * If the fields on the paragraph type are translatable,
2112    * ContentTranslationHandler::addTranslatabilityClue()adds an
2113    * "(all languages)" suffix to the widget title. That suffix is incorrect and
2114    * is being removed by this method using a #after_build on the field widget.
2115    *
2116    * @param array $element
2117    * @param \Drupal\Core\Form\FormStateInterface $form_state
2118    *
2119    * @return array
2120    */
2121   public static function removeTranslatabilityClue(array $element, FormStateInterface $form_state) {
2122     // Widgets could have multiple elements with their own titles, so remove the
2123     // suffix if it exists, do not recurse lower than this to avoid going into
2124     // nested paragraphs or similar nested field types.
2125     $suffix = ' <span class="translation-entity-all-languages">(' . t('all languages') . ')</span>';
2126     if (isset($element['#title']) && strpos($element['#title'], $suffix)) {
2127       $element['#title'] = str_replace($suffix, '', $element['#title']);
2128     }
2129     // Loop over all widget deltas.
2130     foreach (Element::children($element) as $delta) {
2131       if (isset($element[$delta]['#title']) && strpos($element[$delta]['#title'], $suffix)) {
2132         $element[$delta]['#title'] = str_replace($suffix, '', $element[$delta]['#title']);
2133       }
2134       // Loop over all form elements within the current delta.
2135       foreach (Element::children($element[$delta]) as $field) {
2136         if (isset($element[$delta][$field]['#title']) && strpos($element[$delta][$field]['#title'], $suffix)) {
2137           $element[$delta][$field]['#title'] = str_replace($suffix, '', $element[$delta][$field]['#title']);
2138         }
2139       }
2140     }
2141     return $element;
2142   }
2143
2144   /**
2145    * Returns the default paragraph type.
2146    *
2147    * @return string
2148    *   Label name for default paragraph type.
2149    */
2150   protected function getDefaultParagraphTypeLabelName() {
2151     if ($this->getDefaultParagraphTypeMachineName() !== NULL) {
2152       $allowed_types = $this->getAllowedTypes();
2153       return $allowed_types[$this->getDefaultParagraphTypeMachineName()]['label'];
2154     }
2155
2156     return NULL;
2157   }
2158
2159   /**
2160    * Returns the machine name for default paragraph type.
2161    *
2162    * @return string
2163    *   Machine name for default paragraph type.
2164    */
2165   protected function getDefaultParagraphTypeMachineName() {
2166     $default_type = $this->getSetting('default_paragraph_type');
2167     $allowed_types = $this->getAllowedTypes();
2168     if ($default_type && isset($allowed_types[$default_type])) {
2169       return $default_type;
2170     }
2171     // Check if the user explicitly selected not to have any default Paragraph
2172     // type. Othewise, if there is only one type available, that one is the
2173     // default.
2174     if ($default_type === '_none') {
2175       return NULL;
2176     }
2177     if (count($allowed_types) === 1) {
2178       return key($allowed_types);
2179     }
2180
2181     return NULL;
2182   }
2183
2184   /**
2185    * Counts the number of paragraphs in a certain mode in a form substructure.
2186    *
2187    * @param array $widget_state
2188    *   The widget state for the form substructure containing information about
2189    *   the paragraphs within.
2190    * @param string $mode
2191    *   The mode to look for.
2192    *
2193    * @return int
2194    *   The number of paragraphs is the given mode.
2195    */
2196   protected function getNumberOfParagraphsInMode(array $widget_state, $mode) {
2197     if (!isset($widget_state['paragraphs'])) {
2198       return 0;
2199     }
2200
2201     $paragraphs_count = 0;
2202     foreach ($widget_state['paragraphs'] as $paragraph) {
2203       if ($paragraph['mode'] == $mode) {
2204         $paragraphs_count++;
2205       }
2206     }
2207
2208     return $paragraphs_count;
2209   }
2210
2211   /**
2212    * {@inheritdoc}
2213    */
2214   public static function isApplicable(FieldDefinitionInterface $field_definition) {
2215     $target_type = $field_definition->getSetting('target_type');
2216     $paragraph_type = \Drupal::entityTypeManager()->getDefinition($target_type);
2217     if ($paragraph_type) {
2218       return $paragraph_type->entityClassImplements(ParagraphInterface::class);
2219     }
2220
2221     return FALSE;
2222   }
2223
2224   /**
2225    * Builds header actions.
2226    *
2227    * @param array[] $field_state
2228    *   Field widget state.
2229    * @param \Drupal\Core\Form\FormStateInterface $form_state
2230    *   Current form state.
2231    *
2232    * @return array[]
2233    *   The form element array.
2234    */
2235   public function buildHeaderActions(array $field_state, FormStateInterface $form_state) {
2236     $actions = [];
2237     if (empty($this->fieldParents)) {
2238       // Set actions.
2239       $actions = [
2240         '#type' => 'paragraphs_actions',
2241       ];
2242
2243       $field_name = $this->fieldDefinition->getName();
2244       $id_prefix = implode('-', array_merge($this->fieldParents, [$field_name]));
2245
2246       // Only show the dragdrop mode if we can find the sortable library.
2247       $library_discovery = \Drupal::service('library.discovery');
2248       $library = $library_discovery->getLibraryByName('paragraphs', 'paragraphs-dragdrop');
2249       if ($library || \Drupal::state()->get('paragraphs_test_dragdrop_force_show', FALSE)) {
2250         $dragdrop_mode = $this->expandButton([
2251           '#type' => 'submit',
2252           '#name' => $this->fieldIdPrefix . '_dragdrop_mode',
2253           '#value' => !empty($field_state['dragdrop']) ? $this->t('Complete drag & drop') : $this->t('Drag & drop'),
2254           '#attributes' => ['class' => ['field-dragdrop-mode-submit']],
2255           '#submit' => [[get_class($this), 'dragDropModeSubmit']],
2256           '#weight' => 8,
2257           '#ajax' => [
2258             'callback' => [get_class($this), 'dragDropModeAjax'],
2259             'wrapper' => $this->fieldWrapperId,
2260           ],
2261         ]);
2262
2263         // Make the complete button a primary button, limit validation errors
2264         // only for enabling drag and drop mode.
2265         if (!empty($field_state['dragdrop'])) {
2266           $dragdrop_mode['#button_type'] = 'primary';
2267           $actions['actions']['dragdrop_mode'] = $dragdrop_mode;
2268         }
2269         else {
2270           $dragdrop_mode['#limit_validation_errors'] = [
2271             array_merge($this->fieldParents, [$field_name, 'dragdrop_mode']),
2272           ];
2273           $actions['dropdown_actions']['dragdrop_mode'] = $dragdrop_mode;
2274         }
2275       }
2276
2277       if ($this->realItemCount > 1 && empty($field_state['dragdrop'])) {
2278
2279         $collapse_all = $this->expandButton([
2280           '#type' => 'submit',
2281           '#value' => $this->t('Collapse all'),
2282           '#submit' => [[get_class($this), 'changeAllEditModeSubmit']],
2283           '#name' => $id_prefix . '_collapse_all',
2284           '#paragraphs_mode' => 'closed',
2285           '#limit_validation_errors' => [
2286             array_merge($this->fieldParents, [$field_name, 'collapse_all']),
2287           ],
2288           '#ajax' => [
2289             'callback' => [get_class($this), 'allActionsAjax'],
2290             'wrapper' => $this->fieldWrapperId,
2291           ],
2292           '#weight' => -1,
2293           '#paragraphs_show_warning' => TRUE,
2294         ]);
2295
2296         $edit_all = $this->expandButton([
2297           '#type' => 'submit',
2298           '#value' => $this->t('Edit all'),
2299           '#submit' => [[get_class($this), 'changeAllEditModeSubmit']],
2300           '#name' => $id_prefix . '_edit-all',
2301           '#paragraphs_mode' => 'edit',
2302           '#limit_validation_errors' => [],
2303           '#ajax' => [
2304             'callback' => [get_class($this), 'allActionsAjax'],
2305             'wrapper' => $this->fieldWrapperId,
2306           ],
2307         ]);
2308
2309         if (isset($field_state['paragraphs'][0]['mode']) && $field_state['paragraphs'][0]['mode'] === 'closed') {
2310           $edit_all['#attributes'] = [
2311             'class' => ['paragraphs-icon-button', 'paragraphs-icon-button-edit'],
2312             'title' => $this->t('Edit all'),
2313           ];
2314           $edit_all['#title'] = $this->t('Edit All');
2315           $actions['actions']['edit_all'] = $edit_all;
2316           $actions['dropdown_actions']['collapse_all'] = $collapse_all;
2317         }
2318         else {
2319           $collapse_all['#attributes'] = [
2320             'class' => ['paragraphs-icon-button', 'paragraphs-icon-button-collapse'],
2321             'title' => $this->t('Collapse all'),
2322           ];
2323           $actions['actions']['collapse_all'] = $collapse_all;
2324           $actions['dropdown_actions']['edit_all'] = $edit_all;
2325         }
2326       }
2327     }
2328
2329     // Add paragraphs_header flag which we use later in preprocessor to move
2330     // header actions to table header.
2331     if ($actions) {
2332       $actions['#paragraphs_header'] = TRUE;
2333     }
2334
2335     return $actions;
2336   }
2337
2338   /**
2339    * Loops through all paragraphs and change mode for each paragraph instance.
2340    *
2341    * @param array $form
2342    *   Current form state.
2343    * @param \Drupal\Core\Form\FormStateInterface $form_state
2344    *   Current form state.
2345    */
2346   public static function changeAllEditModeSubmit(array $form, FormStateInterface $form_state) {
2347     $submit = ParagraphsWidget::getSubmitElementInfo($form, $form_state, ParagraphsWidget::ACTION_POSITION_HEADER);
2348
2349     // Change edit mode for each paragraph.
2350     foreach ($submit['widget_state']['paragraphs'] as $delta => &$paragraph) {
2351       if ($submit['widget_state']['paragraphs'][$delta]['mode'] !== 'remove') {
2352         $submit['widget_state']['paragraphs'][$delta]['mode'] = $submit['button']['#paragraphs_mode'];
2353         if (!empty($submit['button']['#paragraphs_show_warning'])) {
2354           $submit['widget_state']['paragraphs'][$delta]['show_warning'] = $submit['button']['#paragraphs_show_warning'];
2355         }
2356       }
2357     }
2358
2359     // Disable autocollapse when editing all and enable it when closing all.
2360     if ($submit['button']['#paragraphs_mode'] === 'edit') {
2361       $submit['widget_state']['autocollapse'] = 'none';
2362     }
2363     elseif ($submit['button']['#paragraphs_mode'] === 'closed') {
2364       $submit['widget_state']['autocollapse'] = 'all';
2365     }
2366
2367     static::setWidgetState($submit['parents'], $submit['field_name'], $form_state, $submit['widget_state']);
2368     $form_state->setRebuild();
2369   }
2370
2371   /**
2372    * Returns a state with all paragraphs closed, if autocollapse is enabled.
2373    *
2374    * @param array $widget_state
2375    *   The current widget state.
2376    *
2377    * @return array
2378    *   The widget state altered by closing all paragraphs.
2379    */
2380   public static function autocollapse(array $widget_state) {
2381     if ($widget_state['real_item_count'] > 0 && $widget_state['autocollapse'] !== 'none') {
2382       foreach ($widget_state['paragraphs'] as $delta => $value) {
2383         if ($widget_state['paragraphs'][$delta]['mode'] === 'edit') {
2384           $widget_state['paragraphs'][$delta]['mode'] = 'closed';
2385         }
2386       }
2387     }
2388
2389     return $widget_state;
2390   }
2391
2392 }