Updated to Drupal 8.5. Core Media not yet in use.
[yaffs-website] / web / modules / contrib / paragraphs / src / Plugin / Field / FieldWidget / ParagraphsWidget.php
index eda841509cd3d5507fc7c66e64fa2750c683cadf..84b47eae45446909439747f4e838eca86d1ecf90 100644 (file)
@@ -4,9 +4,9 @@ namespace Drupal\paragraphs\Plugin\Field\FieldWidget;
 
 use Drupal\Component\Utility\NestedArray;
 use Drupal\Component\Utility\Html;
+use Drupal\Core\Entity\ContentEntityInterface;
 use Drupal\Core\Entity\Entity\EntityFormDisplay;
-use Drupal\Core\Entity\EntityInterface;
-use Drupal\Core\Entity\RevisionableInterface;
+use Drupal\Core\Entity\FieldableEntityInterface;
 use Drupal\Core\Field\FieldDefinitionInterface;
 use Drupal\Core\Field\FieldFilteredMarkup;
 use Drupal\Core\Field\FieldStorageDefinitionInterface;
@@ -15,8 +15,9 @@ use Drupal\Core\Form\FormStateInterface;
 use Drupal\Core\Field\FieldItemListInterface;
 use Drupal\Core\Form\SubformState;
 use Drupal\Core\Render\Element;
-use Drupal\paragraphs;
 use Drupal\paragraphs\ParagraphInterface;
+use Drupal\paragraphs\Plugin\EntityReferenceSelection\ParagraphSelection;
+use Symfony\Component\Validator\ConstraintViolationListInterface;
 
 /**
  * Plugin implementation of the 'entity_reference_revisions paragraphs' widget.
@@ -32,6 +33,21 @@ use Drupal\paragraphs\ParagraphInterface;
  */
 class ParagraphsWidget extends WidgetBase {
 
+  /**
+   * Action position is in the add paragraphs place.
+   */
+  const ACTION_POSITION_BASE = 1;
+
+  /**
+   * Action position is in the table header section.
+   */
+  const ACTION_POSITION_HEADER = 2;
+
+  /**
+   * Action position is in the actions section of the widget.
+   */
+  const ACTION_POSITION_ACTIONS = 3;
+
   /**
    * Indicates whether the current widget instance is in translation.
    *
@@ -74,6 +90,30 @@ class ParagraphsWidget extends WidgetBase {
    */
   protected $accessOptions = NULL;
 
+  /**
+   * Constructs a ParagraphsWidget object.
+   *
+   * @param string $plugin_id
+   *   The plugin_id for the widget.
+   * @param mixed $plugin_definition
+   *   The plugin implementation definition.
+   * @param \Drupal\Core\Field\FieldDefinitionInterface $field_definition
+   *   The definition of the field to which the widget is associated.
+   * @param array $settings
+   *   The widget settings.
+   * @param array $third_party_settings
+   *   Any third party settings.
+   */
+  public function __construct($plugin_id, $plugin_definition, FieldDefinitionInterface $field_definition, array $settings, array $third_party_settings) {
+    // Modify settings that were set before https://www.drupal.org/node/2896115.
+    if(isset($settings['edit_mode']) && $settings['edit_mode'] === 'preview') {
+      $settings['edit_mode'] = 'closed';
+      $settings['closed_mode'] = 'preview';
+    }
+
+    parent::__construct($plugin_id, $plugin_definition, $field_definition, $settings, $third_party_settings);
+  }
+
   /**
    * {@inheritdoc}
    */
@@ -82,6 +122,8 @@ class ParagraphsWidget extends WidgetBase {
       'title' => t('Paragraph'),
       'title_plural' => t('Paragraphs'),
       'edit_mode' => 'open',
+      'closed_mode' => 'summary',
+      'autocollapse' => 'none',
       'add_mode' => 'dropdown',
       'form_display_mode' => 'default',
       'default_paragraph_type' => '',
@@ -113,25 +155,35 @@ class ParagraphsWidget extends WidgetBase {
     $elements['edit_mode'] = array(
       '#type' => 'select',
       '#title' => $this->t('Edit mode'),
-      '#description' => $this->t('The mode the paragraph is in by default. Preview will render the paragraph in the preview view mode.'),
-      '#options' => array(
-        'open' => $this->t('Open'),
-        'closed' => $this->t('Closed'),
-        'preview' => $this->t('Preview'),
-      ),
+      '#description' => $this->t('The mode the paragraph is in by default.'),
+      '#options' => $this->getSettingOptions('edit_mode'),
       '#default_value' => $this->getSetting('edit_mode'),
       '#required' => TRUE,
     );
 
+    $elements['closed_mode'] = [
+      '#type' => 'select',
+      '#title' => $this->t('Closed mode'),
+      '#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.'),
+      '#options' => $this->getSettingOptions('closed_mode'),
+      '#default_value' => $this->getSetting('closed_mode'),
+      '#required' => TRUE,
+    ];
+
+    $elements['autocollapse'] = [
+      '#type' => 'select',
+      '#title' => $this->t('Autocollapse'),
+      '#description' => $this->t('When a paragraph is opened for editing, close others.'),
+      '#options' => $this->getSettingOptions('autocollapse'),
+      '#default_value' => $this->getSetting('autocollapse'),
+      '#required' => TRUE,
+    ];
+
     $elements['add_mode'] = array(
       '#type' => 'select',
       '#title' => $this->t('Add mode'),
-      '#description' => $this->t('The way to add new paragraphs.'),
-      '#options' => array(
-        'select' => $this->t('Select list'),
-        'button' => $this->t('Buttons'),
-        'dropdown' => $this->t('Dropdown button')
-      ),
+      '#description' => $this->t('The way to add new Paragraphs.'),
+      '#options' => $this->getSettingOptions('add_mode'),
       '#default_value' => $this->getSetting('add_mode'),
       '#required' => TRUE,
     );
@@ -162,6 +214,56 @@ class ParagraphsWidget extends WidgetBase {
     return $elements;
   }
 
+  /**
+   * Returns select options for a plugin setting.
+   *
+   * This is done to allow
+   * \Drupal\paragraphs\Plugin\Field\FieldWidget\ParagraphsWidget::settingsSummary()
+   * to access option labels. Not all plugin setting are available.
+   *
+   * @param string $setting_name
+   *   The name of the widget setting. Supported settings:
+   *   - "edit_mode"
+   *   - "closed_mode"
+   *   - "autocollapse"
+   *   - "add_mode"
+   *
+   * @return array|null
+   *   An array of setting option usable as a value for a "#options" key.
+   */
+  protected function getSettingOptions($setting_name) {
+    switch($setting_name) {
+      case 'edit_mode':
+        $options = [
+          'open' => $this->t('Open'),
+          'closed' => $this->t('Closed'),
+        ];
+        break;
+      case 'closed_mode':
+        $options = [
+          'summary' => $this->t('Summary'),
+          'preview' => $this->t('Preview'),
+        ];
+        break;
+      case 'autocollapse':
+        $options = [
+          'none' => $this->t('None'),
+          'all' => $this->t('All'),
+        ];
+        break;
+      case 'add_mode':
+        $options = [
+          'select' => $this->t('Select list'),
+          'button' => $this->t('Buttons'),
+          'dropdown' => $this->t('Dropdown button'),
+          'modal' => $this->t('Modal form'),
+        ];
+        break;
+    }
+
+    return isset($options) ? $options : NULL;
+  }
+
   /**
    * {@inheritdoc}
    */
@@ -172,34 +274,16 @@ class ParagraphsWidget extends WidgetBase {
       '@title_plural' => $this->getSetting('title_plural')
     ]);
 
-    switch($this->getSetting('edit_mode')) {
-      case 'open':
-      default:
-        $edit_mode = $this->t('Open');
-        break;
-      case 'closed':
-        $edit_mode = $this->t('Closed');
-        break;
-      case 'preview':
-        $edit_mode = $this->t('Preview');
-        break;
-    }
-
-    switch($this->getSetting('add_mode')) {
-      case 'select':
-      default:
-        $add_mode = $this->t('Select list');
-        break;
-      case 'button':
-        $add_mode = $this->t('Buttons');
-        break;
-      case 'dropdown':
-        $add_mode = $this->t('Dropdown button');
-        break;
-    }
+    $edit_mode = $this->getSettingOptions('edit_mode')[$this->getSetting('edit_mode')];
+    $closed_mode = $this->getSettingOptions('closed_mode')[$this->getSetting('closed_mode')];
+    $autocollapse = $this->getSettingOptions('autocollapse')[$this->getSetting('autocollapse')];
+    $add_mode = $this->getSettingOptions('add_mode')[$this->getSetting('add_mode')];
 
     $summary[] = $this->t('Edit mode: @edit_mode', ['@edit_mode' => $edit_mode]);
+    $summary[] = $this->t('Closed mode: @closed_mode', ['@closed_mode' => $closed_mode]);
+    $summary[] = $this->t('Autocollapse: @autocollapse', ['@autocollapse' => $autocollapse]);
     $summary[] = $this->t('Add mode: @add_mode', ['@add_mode' => $add_mode]);
+
     $summary[] = $this->t('Form display mode: @form_display_mode', [
       '@form_display_mode' => $this->getSetting('form_display_mode')
     ]);
@@ -223,16 +307,20 @@ class ParagraphsWidget extends WidgetBase {
     $parents = $element['#field_parents'];
     $info = [];
 
+    /** @var \Drupal\paragraphs\Entity\Paragraph $paragraphs_entity */
     $paragraphs_entity = NULL;
     $host = $items->getEntity();
     $widget_state = static::getWidgetState($parents, $field_name, $form_state);
 
-    $entity_manager = \Drupal::entityTypeManager();
+    $entity_type_manager = \Drupal::entityTypeManager();
     $target_type = $this->getFieldSetting('target_type');
 
     $item_mode = isset($widget_state['paragraphs'][$delta]['mode']) ? $widget_state['paragraphs'][$delta]['mode'] : 'edit';
     $default_edit_mode = $this->getSetting('edit_mode');
 
+    $closed_mode_setting = isset($widget_state['closed_mode']) ? $widget_state['closed_mode'] : $this->getSetting('closed_mode');
+    $autocollapse_setting = isset($widget_state['autocollapse']) ? $widget_state['autocollapse'] : $this->getSetting('autocollapse');
+
     $show_must_be_saved_warning = !empty($widget_state['paragraphs'][$delta]['show_warning']);
 
     if (isset($widget_state['paragraphs'][$delta]['entity'])) {
@@ -250,17 +338,14 @@ class ParagraphsWidget extends WidgetBase {
         elseif ($default_edit_mode == 'closed') {
           $item_mode = 'closed';
         }
-        elseif ($default_edit_mode == 'preview') {
-          $item_mode = 'preview';
-        }
       }
     }
     elseif (isset($widget_state['selected_bundle'])) {
 
-      $entity_type = $entity_manager->getDefinition($target_type);
+      $entity_type = $entity_type_manager->getDefinition($target_type);
       $bundle_key = $entity_type->getKey('bundle');
 
-      $paragraphs_entity = $entity_manager->getStorage($target_type)->create(array(
+      $paragraphs_entity = $entity_type_manager->getStorage($target_type)->create(array(
         $bundle_key => $widget_state['selected_bundle'],
       ));
 
@@ -278,11 +363,7 @@ class ParagraphsWidget extends WidgetBase {
         foreach ($violations as $violation) {
           $messages[] = $violation->getMessage();
         }
-        $info['validation_error'] = array(
-          '#type' => 'container',
-          '#markup' => $this->t('@messages', ['@messages' => strip_tags(implode('\n', $messages))]),
-          '#attributes' => ['class' => ['messages', 'messages--warning']],
-        );
+        $info['validation_error'] = $this->createMessage($this->t('@messages', ['@messages' => strip_tags(implode('\n', $messages))]));
       }
     }
 
@@ -312,7 +393,13 @@ class ParagraphsWidget extends WidgetBase {
           $entity_langcode = $paragraphs_entity->language()->getId();
           $source = $form_state->get(['content_translation', 'source']);
           $source_langcode = $source ? $source->getId() : $entity_langcode;
-          $paragraphs_entity = $paragraphs_entity->getTranslation($source_langcode);
+          // Make sure the source language version is used if available. It is a
+          // the host and fetching the translation without this check could lead
+          // valid scenario to have no paragraphs items in the source version of
+          // to an exception.
+          if ($paragraphs_entity->hasTranslation($source_langcode)) {
+            $paragraphs_entity = $paragraphs_entity->getTranslation($source_langcode);
+          }
           // The paragraphs entity has no content translation source field if
           // no paragraph entity field is translatable, even if the host is.
           if ($paragraphs_entity->hasField('content_translation_source')) {
@@ -354,37 +441,63 @@ class ParagraphsWidget extends WidgetBase {
       if (isset($item_bundles[$paragraphs_entity->bundle()])) {
         $bundle_info = $item_bundles[$paragraphs_entity->bundle()];
 
-        $element['top'] = array(
+        // Create top section structure with all needed subsections.
+        $element['top'] = [
           '#type' => 'container',
           '#weight' => -1000,
-          '#attributes' => array(
-            'class' => array(
-              'paragraph-type-top',
-            ),
-          ),
-        );
-
-        $element['top']['paragraph_type_title'] = array(
-          '#type' => 'container',
-          '#weight' => 0,
-          '#attributes' => array(
-            'class' => array(
-              'paragraph-type-title',
-            ),
-          ),
-        );
+          '#attributes' => ['class' => ['paragraph-type-top']],
+          // Section for paragraph type information.
+          'type' => [
+            '#type' => 'container',
+            '#attributes' => ['class' => ['paragraph-type-title']],
+            'label' => ['#markup' => $bundle_info['label']],
+          ],
+          // Section for information icons.
+          'info' => [
+            '#type' => 'container',
+            '#attributes' => ['class' => ['paragraph-type-info']],
+          ],
+          'summary' => [
+            '#type' => 'container',
+            '#attributes' => ['class' => ['paragraph-type-summary']],
+          ],
+          // Paragraphs actions element for actions and dropdown actions.
+          'actions' => [
+            '#type' => 'paragraphs_actions',
+          ],
+        ];
 
-        $element['top']['paragraph_type_title']['info'] = array(
-          '#markup' => $bundle_info['label'],
-        );
+        // Type icon and label bundle.
+        if ($icon_url = $paragraphs_entity->type->entity->getIconUrl()) {
+          $element['top']['type']['icon'] = [
+            '#theme' => 'image',
+            '#uri' => $icon_url,
+            '#attributes' => [
+              'class' => ['paragraph-type-icon'],
+              'title' => $bundle_info['label'],
+            ],
+            '#weight' => 0,
+            // We set inline height and width so icon don't resize on first load
+            // while CSS is still not loaded.
+            '#height' => 16,
+            '#width' => 16,
+          ];
+        }
+        $element['top']['type']['label'] = [
+          '#markup' => '<span class="paragraph-type-label">' . $bundle_info['label'] . '</span>',
+          '#weight' => 1,
+        ];
 
-        $actions = [];
-        $links = [];
+        // Widget actions.
+        $widget_actions = [
+          'actions' => [],
+          'dropdown_actions' => [],
+        ];
 
-        $links['duplicate_button'] = [
+        $widget_actions['dropdown_actions']['duplicate_button'] = [
           '#type' => 'submit',
           '#value' => $this->t('Duplicate'),
-          '#name' => strtr($id_prefix, '-', '_') . '_duplicate',
+          '#name' => $id_prefix . '_duplicate',
           '#weight' => 502,
           '#submit' => [[get_class($this), 'duplicateSubmit']],
           '#limit_validation_errors' => [array_merge($parents, [$field_name, 'add_more'])],
@@ -392,211 +505,157 @@ class ParagraphsWidget extends WidgetBase {
           '#ajax' => [
             'callback' => [get_class($this), 'itemAjax'],
             'wrapper' => $widget_state['ajax_wrapper_id'],
-            'effect' => 'fade',
           ],
           '#access' => $paragraphs_entity->access('update'),
-          '#prefix' => '<li class="duplicate">',
-          '#suffix' => '</li>',
         ];
 
-        // Hide the button when translating.
-        $button_access = $paragraphs_entity->access('delete') && !$this->isTranslating;
-        if($item_mode != 'remove') {
-          $links['remove_button'] = [
+        if ($item_mode != 'remove') {
+          $widget_actions['dropdown_actions']['remove_button'] = [
             '#type' => 'submit',
             '#value' => $this->t('Remove'),
-            '#name' => strtr($id_prefix, '-', '_') . '_remove',
-            '#weight' => 501 ,
+            '#name' => $id_prefix . '_remove',
+            '#weight' => 501,
             '#submit' => [[get_class($this), 'paragraphsItemSubmit']],
             '#limit_validation_errors' => [array_merge($parents, [$field_name, 'add_more'])],
             '#delta' => $delta,
             '#ajax' => [
               'callback' => array(get_class($this), 'itemAjax'),
               'wrapper' => $widget_state['ajax_wrapper_id'],
-              'effect' => 'fade',
             ],
-            '#access' => $button_access,
-            '#prefix' => '<li class="remove">',
-            '#suffix' => '</li>',
+            // Hide the button when translating.
+            '#access' => $paragraphs_entity->access('delete') && !$this->isTranslating,
             '#paragraphs_mode' => 'remove',
           ];
         }
 
         if ($item_mode == 'edit') {
-
           if (isset($paragraphs_entity)) {
-            $links['collapse_button'] = array(
-              '#type' => 'submit',
+            $widget_actions['actions']['collapse_button'] = [
               '#value' => $this->t('Collapse'),
-              '#name' => strtr($id_prefix, '-', '_') . '_collapse',
-              '#weight' => 499,
-              '#submit' => array(array(get_class($this), 'paragraphsItemSubmit')),
+              '#name' => $id_prefix . '_collapse',
+              '#weight' => 1,
+              '#submit' => [[get_class($this), 'paragraphsItemSubmit']],
+              '#limit_validation_errors' => [array_merge($parents, [$field_name, 'add_more'])],
               '#delta' => $delta,
-              '#ajax' => array(
-                'callback' => array(get_class($this), 'itemAjax'),
+              '#ajax' => [
+                'callback' => [get_class($this), 'itemAjax'],
                 'wrapper' => $widget_state['ajax_wrapper_id'],
-                'effect' => 'fade',
-              ),
+              ],
               '#access' => $paragraphs_entity->access('update'),
-              '#prefix' => '<li class="collapse">',
-              '#suffix' => '</li>',
               '#paragraphs_mode' => 'closed',
               '#paragraphs_show_warning' => TRUE,
-            );
+              '#attributes' => [
+                'class' => ['paragraphs-icon-button', 'paragraphs-icon-button-collapse'],
+                'title' => $this->t('Collapse'),
+              ],
+            ];
           }
-
-          $info['edit_button_info'] = array(
-            '#type' => 'container',
-            '#markup' => $this->t('You are not allowed to edit this @title.', array('@title' => $this->getSetting('title'))),
-            '#attributes' => ['class' => ['messages', 'messages--warning']],
-            '#access' => !$paragraphs_entity->access('update') && $paragraphs_entity->access('delete'),
-          );
-
-          $info['remove_button_info'] = array(
-            '#type' => 'container',
-            '#markup' => $this->t('You are not allowed to remove this @title.', array('@title' => $this->getSetting('title'))),
-            '#attributes' => ['class' => ['messages', 'messages--warning']],
-            '#access' => !$paragraphs_entity->access('delete') && $paragraphs_entity->access('update'),
-          );
-
-          $info['edit_remove_button_info'] = array(
-            '#type' => 'container',
-            '#markup' => $this->t('You are not allowed to edit or remove this @title.', array('@title' => $this->getSetting('title'))),
-            '#attributes' => ['class' => ['messages', 'messages--warning']],
-            '#access' => !$paragraphs_entity->access('update') && !$paragraphs_entity->access('delete'),
-          );
         }
         else {
-          $element['top']['paragraphs_edit_button_container'] = [
-            '#type' => 'container',
+          $widget_actions['actions']['edit_button'] = $this->expandButton([
+            '#type' => 'submit',
+            '#value' => $this->t('Edit'),
+            '#name' => $id_prefix . '_edit',
             '#weight' => 1,
+            '#attributes' => ['class' => ['paragraphs-button']],
+            '#submit' => [[get_class($this), 'paragraphsItemSubmit']],
+            '#limit_validation_errors' => [
+              array_merge($parents, [$field_name, 'add_more']),
+            ],
+            '#delta' => $delta,
+            '#ajax' => [
+              'callback' => [get_class($this), 'itemAjax'],
+              'wrapper' => $widget_state['ajax_wrapper_id'],
+            ],
+            '#access' => $paragraphs_entity->access('update'),
+            '#paragraphs_mode' => 'edit',
             '#attributes' => [
-              'class' => [
-                'paragraphs-edit-button-container',
-              ],
+              'class' => ['paragraphs-icon-button', 'paragraphs-icon-button-edit'],
+              'title' => $this->t('Edit'),
             ],
-            'paragraphs_edit_button' => [
-              '#type' => 'submit',
-              '#value' => $this->t('Edit'),
-              '#name' => strtr($id_prefix, '-', '_') . '_edit',
-              '#weight' => 500,
-              '#attributes' => [
-                'class' => [
-                  'paragraphs-edit-button',
-                ],
-              ],
-              '#submit' => [[get_class($this), 'paragraphsItemSubmit']],
-              '#limit_validation_errors' => [array_merge($parents, [$field_name, 'add_more'])],
-              '#delta' => $delta,
-              '#ajax' => [
-                'callback' => [get_class($this), 'itemAjax'],
-                'wrapper' => $widget_state['ajax_wrapper_id'],
-                'effect' => 'fade',
-              ],
-              '#access' => $paragraphs_entity->access('update'),
-              '#paragraphs_mode' => 'edit',
-            ]
-          ];
+          ]);
+
+          if ($show_must_be_saved_warning && $paragraphs_entity->isChanged()) {
+            $info['changed'] = [
+              '#theme' => 'paragraphs_info_icon',
+              '#message' => $this->t('You have unsaved changes on this @title item.', ['@title' => $this->getSetting('title')]),
+              '#icon' => 'changed',
+            ];
+          }
 
-          if ($show_must_be_saved_warning) {
-            $info['must_be_saved_info'] = array(
-              '#type' => 'container',
-              '#markup' => $this->t('You have unsaved changes on this @title item.', array('@title' => $this->getSetting('title'))),
-              '#attributes' => ['class' => ['messages', 'messages--warning']],
-            );
+          if (!$paragraphs_entity->access('view')) {
+            $info['preview'] = [
+              '#theme' => 'paragraphs_info_icon',
+              '#message' => $this->t('You are not allowed to view this @title.', array('@title' => $this->getSetting('title'))),
+              '#icon' => 'view',
+            ];
           }
+        }
 
-          $info['preview_info'] = array(
-            '#type' => 'container',
-            '#markup' => $this->t('You are not allowed to view this @title.', array('@title' => $this->getSetting('title'))),
-            '#attributes' => ['class' => ['messages', 'messages--warning']],
-            '#access' => !$paragraphs_entity->access('view'),
-          );
+        // If update is disabled we will show lock icon in actions section.
+        if (!$paragraphs_entity->access('update')) {
+          $widget_actions['actions']['edit_disabled'] = [
+            '#theme' => 'paragraphs_info_icon',
+            '#message' => $this->t('You are not allowed to edit or remove this @title.', ['@title' => $this->getSetting('title')]),
+            '#icon' => 'lock',
+            '#weight' => 1,
+          ];
+        }
 
-          $info['edit_button_info'] = array(
-            '#type' => 'container',
-            '#markup' => $this->t('You are not allowed to edit this @title.', array('@title' => $this->getSetting('title'))),
-            '#attributes' => ['class' => ['messages', 'messages--warning']],
-            '#access' => !$paragraphs_entity->access('update') && $paragraphs_entity->access('delete'),
-          );
+        if (!$paragraphs_entity->access('update') && !$paragraphs_entity->access('delete')) {
+          $info['edit'] = [
+            '#theme' => 'paragraphs_info_icon',
+            '#message' => $this->t('You are not allowed to edit or remove this @title.', ['@title' => $this->getSetting('title')]),
+            '#icon' => 'lock',
+          ];
+        }
+        elseif (!$paragraphs_entity->access('update')) {
+          $info['edit'] = [
+            '#theme' => 'paragraphs_info_icon',
+            '#message' => $this->t('You are not allowed to edit this @title.', ['@title' => $this->getSetting('title')]),
+            '#icon' => 'edit-disabled',
+          ];
+        }
+        elseif (!$paragraphs_entity->access('delete')) {
+          $info['remove'] = [
+            '#theme' => 'paragraphs_info_icon',
+            '#message' => $this->t('You are not allowed to remove this @title.', ['@title' => $this->getSetting('title')]),
+            '#icon' => 'delete-disabled',
+          ];
+        }
 
-          $info['remove_button_info'] = array(
-            '#type' => 'container',
-            '#markup' => $this->t('You are not allowed to remove this @title.', array('@title' => $this->getSetting('title'))),
-            '#attributes' => ['class' => ['messages', 'messages--warning']],
-            '#access' => !$paragraphs_entity->access('delete') && $paragraphs_entity->access('update'),
-          );
+        $context = [
+          'form' => $form,
+          'widget' => self::getWidgetState($parents, $field_name, $form_state, $widget_state),
+          'items' => $items,
+          'delta' => $delta,
+          'element' => $element,
+          'form_state' => $form_state,
+          'paragraphs_entity' => $paragraphs_entity,
+        ];
 
-          $info['edit_remove_button_info'] = array(
-            '#type' => 'container',
-            '#markup' => $this->t('You are not allowed to edit or remove this @title.', array('@title' => $this->getSetting('title'))),
-            '#attributes' => ['class' => ['messages', 'messages--warning']],
-            '#access' => !$paragraphs_entity->access('update') && !$paragraphs_entity->access('delete'),
-          );
-        }
+        // Allow modules to alter widget actions.
+        \Drupal::moduleHandler()->alter('paragraphs_widget_actions', $widget_actions, $context);
 
-        if (count($links)) {
-          $show_links = 0;
-          foreach($links as $link_item) {
-            if (!isset($link_item['#access']) || $link_item['#access']) {
-              $show_links++;
-            }
-          }
+        if (count($widget_actions['actions'])) {
+          // Expand all actions to proper submit elements and add it to top
+          // actions sub component.
+          $element['top']['actions']['actions'] = array_map([$this, 'expandButton'], $widget_actions['actions']);
+        }
 
-          if ($show_links > 0) {
-
-            $element['top']['links'] = $links;
-            if ($show_links > 1) {
-              $element['top']['links']['#theme_wrappers'] = array('dropbutton_wrapper', 'paragraphs_dropbutton_wrapper');
-              $element['top']['links']['prefix'] = array(
-                '#markup' => '<ul class="dropbutton">',
-                '#weight' => -999,
-              );
-              $element['top']['links']['suffix'] = array(
-                '#markup' => '</li>',
-                '#weight' => 999,
-              );
-            }
-            else {
-              $element['top']['links']['#theme_wrappers'] = array('paragraphs_dropbutton_wrapper');
-              foreach($links as $key => $link_item) {
-                unset($element['top']['links'][$key]['#prefix']);
-                unset($element['top']['links'][$key]['#suffix']);
-              }
-            }
-            $element['top']['links']['#weight'] = 1;
-          }
+        if (count($widget_actions['dropdown_actions'])) {
+          // Expand all dropdown actions to proper submit elements and add
+          // them to top dropdown actions sub component.
+          $element['top']['actions']['dropdown_actions'] = array_map([$this, 'expandButton'], $widget_actions['dropdown_actions']);
         }
 
         if (count($info)) {
-          $show_info = FALSE;
-          foreach($info as $info_item) {
+          foreach ($info as $info_item) {
             if (!isset($info_item['#access']) || $info_item['#access']) {
-              $show_info = TRUE;
-              break;
-            }
-          }
-
-          if ($show_info) {
-            $element['info'] = $info;
-            $element['info']['#weight'] = 998;
-          }
-        }
-
-        if (count($actions)) {
-          $show_actions = FALSE;
-          foreach($actions as $action_item) {
-            if (!isset($action_item['#access']) || $action_item['#access']) {
-              $show_actions = TRUE;
+              $element['top']['info']['items'] = $info;
               break;
             }
           }
-
-          if ($show_actions) {
-            $element['actions'] = $actions;
-            $element['actions']['#type'] = 'actions';
-            $element['actions']['#weight'] = 999;
-          }
         }
       }
 
@@ -619,13 +678,37 @@ class ParagraphsWidget extends WidgetBase {
 
       if ($item_mode == 'edit') {
         $display->buildForm($paragraphs_entity, $element['subform'], $form_state);
+        // Get the field definitions of the paragraphs_entity.
+        // We need them to filter out entity reference revisions fields that
+        // reference paragraphs, cause otherwise we have problems with showing
+        // and hiding the right fields in nested paragraphs.
+        $field_definitions = $paragraphs_entity->getFieldDefinitions();
+
         foreach (Element::children($element['subform']) as $field) {
+          // Do a check if we have to add a class to the form element. We need
+          // those classes (paragraphs-content and paragraphs-behavior) to show
+          // and hide elements, depending of the active perspective.
+          $omit_class = FALSE;
+          if (isset($field_definitions[$field])) {
+            $type = $field_definitions[$field]->getType();
+            if ($type == 'entity_reference_revisions') {
+              // Check if we are referencing paragraphs.
+              $target_entity_type = $field_definitions[$field]->get('entity_type');
+              if ($target_entity_type && $target_entity_type == 'paragraph') {
+                $omit_class = TRUE;
+              }
+            }
+          }
+
           if ($paragraphs_entity->hasField($field)) {
+            if (!$omit_class) {
+              $element['subform'][$field]['#attributes']['class'][] = 'paragraphs-content';
+            }
             $translatable = $paragraphs_entity->{$field}->getFieldDefinition()->isTranslatable();
             if ($translatable) {
               $element['subform'][$field]['widget']['#after_build'][] = [
                 static::class,
-                'removeTranslatabilityClue'
+                'removeTranslatabilityClue',
               ];
             }
           }
@@ -633,32 +716,46 @@ class ParagraphsWidget extends WidgetBase {
 
         // Build the behavior plugins fields.
         $paragraphs_type = $paragraphs_entity->getParagraphType();
-        if ($paragraphs_type) {
+        if ($paragraphs_type && \Drupal::currentUser()->hasPermission('edit behavior plugin settings')) {
+          $element['behavior_plugins']['#weight'] = -99;
           foreach ($paragraphs_type->getEnabledBehaviorPlugins() as $plugin_id => $plugin) {
-            $element['behavior_plugins'][$plugin_id] = [];
+            $element['behavior_plugins'][$plugin_id] = [
+              '#type' => 'container',
+              '#group' => implode('][', array_merge($element_parents, ['paragraph_behavior'])),
+            ];
             $subform_state = SubformState::createForSubform($element['behavior_plugins'][$plugin_id], $form, $form_state);
             if ($plugin_form = $plugin->buildBehaviorForm($paragraphs_entity, $element['behavior_plugins'][$plugin_id], $subform_state)) {
               $element['behavior_plugins'][$plugin_id] = $plugin_form;
+              // Add the paragraphs-behavior class, so that we are able to show
+              // and hide behavior fields, depending on the active perspective.
+              $element['behavior_plugins'][$plugin_id]['#attributes']['class'][] = 'paragraphs-behavior';
             }
           }
         }
       }
-      elseif ($item_mode == 'preview') {
-        $element['subform'] = array();
-        $element['behavior_plugins'] = [];
-        $element['preview'] = entity_view($paragraphs_entity, 'preview', $paragraphs_entity->language()->getId());
-        $element['preview']['#access'] = $paragraphs_entity->access('view');
-      }
       elseif ($item_mode == 'closed') {
-        $element['subform'] = array();
+        $element['subform'] = [];
         $element['behavior_plugins'] = [];
-        if ($paragraphs_entity) {
-          $summary = $this->addCollapsedSummary($paragraphs_entity);
-          $element['top']['paragraph_summary']['fields_info'] = [
-            '#markup' => $summary,
-            '#prefix' => '<div class="paragraphs-collapsed-description">',
-            '#suffix' => '</div>',
-          ];
+        if ($closed_mode_setting === 'preview') {
+          // The closed paragraph is displayed as a rendered preview.
+          $view_builder = $entity_type_manager->getViewBuilder('paragraph');
+
+          $element['preview'] = $view_builder->view($paragraphs_entity, 'preview', $paragraphs_entity->language()->getId());
+          $element['preview']['#access'] = $paragraphs_entity->access('view');
+        }
+        else {
+          // The closed paragraph is displayed as a summary.
+          if ($paragraphs_entity) {
+            $summary = $paragraphs_entity->getSummary();
+            if (!empty($summary)) {
+              $element['top']['summary']['fields_info'] = [
+                '#markup' => $summary,
+                '#prefix' => '<div class="paragraphs-collapsed-description">',
+                '#suffix' => '</div>',
+                '#access' => $paragraphs_entity->access('view'),
+              ];
+            }
+          }
         }
       }
       else {
@@ -675,6 +772,8 @@ class ParagraphsWidget extends WidgetBase {
       $widget_state['paragraphs'][$delta]['entity'] = $paragraphs_entity;
       $widget_state['paragraphs'][$delta]['display'] = $display;
       $widget_state['paragraphs'][$delta]['mode'] = $item_mode;
+      $widget_state['closed_mode'] = $closed_mode_setting;
+      $widget_state['autocollapse'] = $autocollapse_setting;
 
       static::setWidgetState($parents, $field_name, $form_state, $widget_state);
     }
@@ -685,40 +784,67 @@ class ParagraphsWidget extends WidgetBase {
     return $element;
   }
 
-  public function getAllowedTypes() {
-
-    $return_bundles = array();
+  /**
+   * Builds an add paragraph button for opening of modal form.
+   *
+   * @param array $element
+   *   Render element.
+   */
+  protected function buildModalAddForm(array &$element) {
+    // Attach the theme for the dialog template.
+    $element['#theme'] = 'paragraphs_add_dialog';
 
-    $target_type = $this->getFieldSetting('target_type');
-    $bundles = \Drupal::service('entity_type.bundle.info')->getBundleInfo($target_type);
+    $element['add_modal_form_area'] = [
+      '#type' => 'container',
+      '#attributes' => [
+        'class' => [
+          'paragraph-type-add-modal',
+          'first-button',
+        ],
+      ],
+      '#access' => !$this->isTranslating,
+      '#weight' => -2000,
+    ];
 
-    if ($this->getSelectionHandlerSetting('target_bundles') !== NULL) {
-      $bundles = array_intersect_key($bundles, $this->getSelectionHandlerSetting('target_bundles'));
-    }
+    $element['add_modal_form_area']['add_more'] = [
+      '#type' => 'submit',
+      '#value' => $this->t('Add @title', ['@title' => $this->getSetting('title')]),
+      '#name' => 'button_add_modal',
+      '#attributes' => [
+        'class' => [
+          'paragraph-type-add-modal-button',
+          'js-show',
+        ],
+      ],
+    ];
 
-    // Support for the paragraphs reference type.
-    $drag_drop_settings = $this->getSelectionHandlerSetting('target_bundles_drag_drop');
-    if ($drag_drop_settings) {
-      $max_weight = count($bundles);
+    $element['#attached']['library'][] = 'paragraphs/drupal.paragraphs.modal';
+  }
 
-      foreach ($drag_drop_settings as $bundle_info) {
-        if (isset($bundle_info['weight']) && $bundle_info['weight'] && $bundle_info['weight'] > $max_weight) {
-          $max_weight = $bundle_info['weight'];
-        }
-      }
+  /**
+   * Returns the sorted allowed types for a entity reference field.
+   *
+   * @param \Drupal\Core\Field\FieldDefinitionInterface $field_definition
+   *  (optional) The field definition forwhich the allowed types should be
+   *  returned, defaults to the current field.
+   *
+   * @return array
+   *   A list of arrays keyed by the paragraph type machine name with the following properties.
+   *     - label: The label of the paragraph type.
+   *     - weight: The weight of the paragraph type.
+   */
+  public function getAllowedTypes(FieldDefinitionInterface $field_definition = NULL) {
 
-      // Default weight for new items.
-      $weight = $max_weight + 1;
-      foreach ($bundles as $machine_name => $bundle) {
-        $return_bundles[$machine_name] = array(
-          'label' => $bundle['label'],
-          'weight' => isset($drag_drop_settings[$machine_name]['weight']) ? $drag_drop_settings[$machine_name]['weight'] : $weight,
-        );
-        $weight++;
-      }
+    $return_bundles = array();
+    /** @var \Drupal\Core\Entity\EntityReferenceSelection\SelectionPluginManagerInterface $selection_manager */
+    $selection_manager = \Drupal::service('plugin.manager.entity_reference_selection');
+    $handler = $selection_manager->getSelectionHandler($field_definition ?: $this->fieldDefinition);
+    if ($handler instanceof ParagraphSelection) {
+      $return_bundles = $handler->getSortedAllowedTypes();
     }
     // Support for other reference types.
     else {
+      $bundles = \Drupal::service('entity_type.bundle.info')->getBundleInfo($field_definition ? $field_definition->getSetting('target_type') : $this->fieldDefinition->getSetting('target_type'));
       $weight = 0;
       foreach ($bundles as $machine_name => $bundle) {
         if (!count($this->getSelectionHandlerSetting('target_bundles'))
@@ -734,7 +860,6 @@ class ParagraphsWidget extends WidgetBase {
       }
     }
 
-    uasort($return_bundles, 'Drupal\Component\Utility\SortArray::sortByWeightElement');
 
     return $return_bundles;
   }
@@ -778,19 +903,53 @@ class ParagraphsWidget extends WidgetBase {
     $this->realItemCount = $max;
     $is_multiple = $this->fieldDefinition->getFieldStorageDefinition()->isMultiple();
 
-    $title = $this->fieldDefinition->getLabel();
+    $field_title = $this->fieldDefinition->getLabel();
     $description = FieldFilteredMarkup::create(\Drupal::token()->replace($this->fieldDefinition->getDescription()));
 
     $elements = array();
+    $tabs = '';
     $this->fieldIdPrefix = implode('-', array_merge($this->fieldParents, array($field_name)));
     $this->fieldWrapperId = Html::getUniqueId($this->fieldIdPrefix . '-add-more-wrapper');
-    $elements['#prefix'] = '<div id="' . $this->fieldWrapperId . '">';
+
+    // If the parent entity is paragraph add the nested class if not then add
+    // the perspective tabs.
+    $field_prefix = strtr($this->fieldIdPrefix, '_', '-');
+    if (count($this->fieldParents) == 0) {
+      if ($items->getEntity()->getEntityTypeId() != 'paragraph') {
+        $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>';
+      }
+    }
+    if (count($this->fieldParents) > 0) {
+      if ($items->getEntity()->getEntityTypeId() === 'paragraph') {
+        $form['#attributes']['class'][] = 'paragraphs-nested';
+      }
+    }
+    $elements['#prefix'] = '<div class="is-horizontal paragraphs-tabs-wrapper" id="' . $this->fieldWrapperId . '">' . $tabs;
     $elements['#suffix'] = '</div>';
 
     $field_state['ajax_wrapper_id'] = $this->fieldWrapperId;
     // Persist the widget state so formElement() can access it.
     static::setWidgetState($this->fieldParents, $field_name, $form_state, $field_state);
 
+    $header_actions = $this->buildHeaderActions($field_state, $form_state);
+    if ($header_actions) {
+      $elements['header_actions'] = $header_actions;
+      // Add a weight element so we guaranty that header actions will stay in
+      // first row. We will use this later in
+      // paragraphs_preprocess_field_multiple_value_form().
+      $elements['header_actions']['_weight'] = [
+        '#type' => 'weight',
+        '#default_value' => -100,
+      ];
+    }
+
+    if (!empty($field_state['dragdrop'])) {
+      $elements['#attached']['library'][] = 'paragraphs/paragraphs-dragdrop';
+      //$elements['dragdrop_mode']['#button_type'] = 'primary';
+      $elements['dragdrop'] = $this->buildNestedParagraphsFoDragDrop($form_state, NULL, []);
+      return $elements;
+    }
+
     if ($max > 0) {
       for ($delta = 0; $delta < $max; $delta++) {
 
@@ -802,7 +961,7 @@ class ParagraphsWidget extends WidgetBase {
         // For multiple fields, title and description are handled by the wrapping
         // table.
         $element = array(
-          '#title' => $is_multiple ? '' : $title,
+          '#title' => $is_multiple ? '' : $field_title,
           '#description' => $is_multiple ? '' : $description,
         );
         $element = $this->formSingleElement($items, $delta, $element, $form, $form_state);
@@ -838,32 +997,37 @@ class ParagraphsWidget extends WidgetBase {
 
     $field_state = static::getWidgetState($this->fieldParents, $field_name, $form_state);
     $field_state['real_item_count'] = $this->realItemCount;
+    $field_state['add_mode'] = $this->getSetting('add_mode');
     static::setWidgetState($this->fieldParents, $field_name, $form_state, $field_state);
 
+    $elements += [
+      '#element_validate' => [[$this, 'multipleElementValidate']],
+      '#required' => $this->fieldDefinition->isRequired(),
+      '#field_name' => $field_name,
+      '#cardinality' => $cardinality,
+      '#max_delta' => $max - 1,
+    ];
+
     if ($this->realItemCount > 0) {
       $elements += array(
         '#theme' => 'field_multiple_value_form',
-        '#field_name' => $field_name,
-        '#cardinality' => $cardinality,
         '#cardinality_multiple' => $is_multiple,
-        '#required' => $this->fieldDefinition->isRequired(),
-        '#title' => $title,
+        '#title' => $field_title,
         '#description' => $description,
-        '#max_delta' => $max-1,
       );
+
     }
     else {
+      $classes = $this->fieldDefinition->isRequired() ? ['form-required'] : [];
       $elements += [
         '#type' => 'container',
         '#theme_wrappers' => ['container'],
-        '#field_name' => $field_name,
-        '#cardinality' => $cardinality,
         '#cardinality_multiple' => TRUE,
-        '#max_delta' => $max-1,
         'title' => [
           '#type' => 'html_tag',
           '#tag' => 'strong',
-          '#value' => $title,
+          '#value' => $field_title,
+          '#attributes' => ['class' => $classes],
         ],
         'text' => [
           '#type' => 'container',
@@ -913,60 +1077,218 @@ class ParagraphsWidget extends WidgetBase {
   }
 
   /**
-   * Add 'add more' button, if not working with a programmed form.
+   * Returns a list of child paragraphs for a given field to loop over.
    *
-   * @return array
-   *    The form element array.
+   * @param \Drupal\Core\Form\FormStateInterface $form_state
+   *   The form state.
+   * @param string $field_name
+   *   The field name for which to find child paragraphs.
+   * @param \Drupal\paragraphs\ParagraphInterface $paragraph
+   *   The current paragraph.
+   * @param array $array_parents
+   *   The current field parent structure.
+   *
+   * @return \Drupal\paragraphs\Entity\Paragraph[]
+   *   Child paragraphs.
    */
-  protected function buildAddActions() {
-    if (count($this->getAccessibleOptions()) === 0) {
-      if (count($this->getAllowedTypes()) === 0) {
-        $add_more_elements['info'] = [
-          '#type' => 'container',
-          '#markup' => $this->t('You are not allowed to add any of the @title types.', ['@title' => $this->getSetting('title')]),
-          '#attributes' => ['class' => ['messages', 'messages--warning']],
-        ];
-      }
-      else {
-        $add_more_elements['info'] = [
-          '#type' => 'container',
-          '#markup' => $this->t('You did not add any @title types yet.', ['@title' => $this->getSetting('title')]),
-          '#attributes' => ['class' => ['messages', 'messages--warning']],
-        ];
+  protected function getChildParagraphs(FormStateInterface $form_state, $field_name, ParagraphInterface $paragraph = NULL, array $array_parents = []) {
+
+    // Convert the parents structure which only includes field names and delta
+    // to the full storage array key which includes a prefix and a subform.
+    $full_parents_key = ['field_storage', '#parents'];
+    foreach ($array_parents as $i => $parent) {
+      $full_parents_key[] = $parent;
+      if ($i % 2) {
+        $full_parents_key[] = 'subform';
       }
-
-      return $add_more_elements ;
     }
 
-    if ($this->getSetting('add_mode') == 'button' || $this->getSetting('add_mode') == 'dropdown') {
-      return $this->buildButtonsAddMode();
+    $current_parents = array_merge($full_parents_key, ['#fields', $field_name]);
+    $child_field_state = NestedArray::getValue($form_state->getStorage(), $current_parents);
+    $entities = [];
+    if ($child_field_state && isset($child_field_state['paragraphs'])) {
+      // Fetch the paragraphs from the field state. Use the original delta
+      // to get the right position. Also reorder the paragraphs in the widget
+      // state accordingly.
+      $new_widget_paragraphs = [];
+      foreach ($child_field_state['paragraphs'] as $child_delta => $child_field_item_state) {
+        $entities[array_search($child_delta, $child_field_state['original_deltas'])] = $child_field_item_state['entity'];
+        $new_widget_paragraphs[array_search($child_delta, $child_field_state['original_deltas'])] = $child_field_item_state;
+      }
+      ksort($entities);
+
+      // Set the orderd paragraphs into the widget state and reset original
+      // deltas.
+      ksort($new_widget_paragraphs);
+      $child_field_state['paragraphs'] = $new_widget_paragraphs;
+      $child_field_state['original_deltas'] = range(0, count($child_field_state['paragraphs']) - 1);
+      NestedArray::setValue($form_state->getStorage(), $current_parents, $child_field_state);
+    }
+    elseif ($paragraph) {
+      // If there is no field state, return the paragraphs directly from the
+      // entity.
+      foreach ($paragraph->get($field_name) as $child_delta => $item) {
+        if ($item->entity) {
+          $entities[$child_delta] = $item->entity;
+        }
+      }
     }
 
-    return $this->buildSelectAddMode();
+    return $entities;
   }
 
   /**
-   * Returns the available paragraphs type.
+   * Builds the nested drag and drop structure.
+   *
+   * @param \Drupal\Core\Form\FormStateInterface $form_state
+   *   The form state.
+   * @param \Drupal\paragraphs\ParagraphInterface|null $paragraph
+   *   The parent paragraph, NULL for the initial call.
+   * @param string[] $array_parents
+   *   The array parents for nested paragraphs.
    *
    * @return array
-   *   Available paragraphs types.
+   *   The built form structure.
    */
-  protected function getAccessibleOptions() {
-    if ($this->accessOptions !== NULL) {
-      return $this->accessOptions;
-    }
-
-    $entity_type_manager = \Drupal::entityTypeManager();
-    $target_type = $this->getFieldSetting('target_type');
-    $bundles = $this->getAllowedTypes();
-    $access_control_handler = $entity_type_manager->getAccessControlHandler($target_type);
-    $dragdrop_settings = $this->getSelectionHandlerSetting('target_bundles_drag_drop');
-
-    foreach ($bundles as $machine_name => $bundle) {
-      if ($dragdrop_settings || (!count($this->getSelectionHandlerSetting('target_bundles'))
-          || in_array($machine_name, $this->getSelectionHandlerSetting('target_bundles')))) {
-        if ($access_control_handler->createAccess($machine_name)) {
-          $this->accessOptions[$machine_name] = $bundle['label'];
+  protected function buildNestedParagraphsFoDragDrop(FormStateInterface $form_state, ParagraphInterface $paragraph = NULL, array $array_parents = []) {
+    // Look for nested elements.
+    $elements = [];
+    $field_definitions = [];
+    if ($paragraph) {
+      foreach ($paragraph->getFieldDefinitions() as $child_field_name => $field_definition) {
+        /** @var \Drupal\Core\Field\FieldDefinitionInterface $field_definition */
+        if ($field_definition->getType() == 'entity_reference_revisions' && $field_definition->getSetting('target_type') == 'paragraph') {
+          $field_definitions[$child_field_name] = $field_definition;
+        }
+      }
+    }
+    else {
+      $field_definitions = [$this->fieldDefinition->getName() => $this->fieldDefinition];
+    }
+
+    /** @var \Drupal\Core\Field\FieldDefinitionInterface $field_definition */
+    foreach ($field_definitions as $child_field_name => $field_definition) {
+      $child_path = implode('][', array_merge($array_parents, [$child_field_name]));
+      $cardinality = $field_definition->getFieldStorageDefinition()->getCardinality();
+      $allowed_types = implode(array_keys($this->getAllowedTypes($field_definition)), ',');
+      $elements[$child_field_name] = [
+        '#type' => 'container',
+        '#attributes' => ['class' => ['paragraphs-dragdrop-wrapper']],
+      ];
+
+      // Only show a field label if there is more than one paragraph field.
+      $label = count($field_definitions) > 1 || !$paragraph ? '<label><strong>' . $field_definition->getLabel() . '</strong></label>' : '';
+
+      $elements[$child_field_name]['list'] = [
+        '#type' => 'markup',
+        '#prefix' => $label . '<ul class="paragraphs-dragdrop" data-paragraphs-dragdrop-cardinality="' . $cardinality . '" data-paragraphs-dragdrop-allowed-types="' . $allowed_types . '" data-paragraphs-dragdrop-path="' . $child_path . '">',
+        '#suffix' => '</ul>',
+      ];
+
+      /** @var \Drupal\paragraphs\Entity\Paragraph $child_paragraph */
+      foreach ($this->getChildParagraphs($form_state, $child_field_name, $paragraph, $array_parents) as $child_delta => $child_paragraph) {
+        $element = [];
+        $element['top'] = [
+          '#type' => 'container',
+          '#attributes' => ['class' => ['paragraphs-summary-wrapper']],
+        ];
+        $element['top']['paragraph_summary']['type'] = [
+          '#markup' => '<strong>' . $child_paragraph->getParagraphType()->label() . '</strong>',
+        ];
+
+        // We name the element '_weight' to avoid clashing with elements
+        // defined by widget.
+        $element['_weight'] = array(
+          '#type' => 'hidden',
+          '#default_value' => $child_delta,
+          '#attributes' => [
+            'class' => ['paragraphs-dragdrop__weight'],
+          ]
+        );
+
+        $element['_path'] = [
+          '#type' => 'hidden',
+          '#title' => $this->t('Current path for @number', ['@number' => $delta = 1]),
+          '#title_display' => 'invisible',
+          '#default_value' => $child_path,
+          '#attributes' => [
+            'class' => ['paragraphs-dragdrop__path'],
+          ]
+        ];
+
+        $summary_options = [];
+
+        $element['#prefix'] = '<li data-paragraphs-dragdrop-bundle="' . $child_paragraph->bundle() . '"><a href="#" class="tabledrag-handle"><div class="handle">&nbsp;</div></a>';
+        $element['#suffix'] = '</li>';
+        $child_array_parents = array_merge($array_parents,  [$child_field_name, $child_delta]);
+
+        if ($child_elements = $this->buildNestedParagraphsFoDragDrop($form_state, $child_paragraph, $child_array_parents)) {
+          $element['dragdrop'] = $child_elements;
+
+          // Set the depth limit to 0 to avoid displaying a summary for the
+          // children.
+          $summary_options['depth_limit'] = 0;
+        }
+
+        $element['top']['summary']['fields_info'] = [
+          '#markup' => $child_paragraph->getSummary($summary_options),
+          '#prefix' => '<div class="paragraphs-collapsed-description">',
+          '#suffix' => '</div>',
+        ];
+
+        $elements[$child_field_name]['list'][$child_delta] = $element;
+      }
+    }
+    return $elements;
+  }
+
+  /**
+   * Add 'add more' button, if not working with a programmed form.
+   *
+   * @return array
+   *    The form element array.
+   */
+  protected function buildAddActions() {
+    if (count($this->getAccessibleOptions()) === 0) {
+      if (count($this->getAllowedTypes()) === 0) {
+        $add_more_elements['info'] = $this->createMessage($this->t('You are not allowed to add any of the @title types.', ['@title' => $this->getSetting('title')]));
+      }
+      else {
+        $add_more_elements['info'] = $this->createMessage($this->t('You did not add any @title types yet.', ['@title' => $this->getSetting('title')]));
+      }
+
+      return $add_more_elements;
+    }
+
+    if (in_array($this->getSetting('add_mode'), ['button', 'dropdown', 'modal'])) {
+      return $this->buildButtonsAddMode();
+    }
+
+    return $this->buildSelectAddMode();
+  }
+
+  /**
+   * Returns the available paragraphs type.
+   *
+   * @return array
+   *   Available paragraphs types.
+   */
+  protected function getAccessibleOptions() {
+    if ($this->accessOptions !== NULL) {
+      return $this->accessOptions;
+    }
+
+    $entity_type_manager = \Drupal::entityTypeManager();
+    $target_type = $this->getFieldSetting('target_type');
+    $bundles = $this->getAllowedTypes();
+    $access_control_handler = $entity_type_manager->getAccessControlHandler($target_type);
+    $dragdrop_settings = $this->getSelectionHandlerSetting('target_bundles_drag_drop');
+
+    foreach ($bundles as $machine_name => $bundle) {
+      if ($dragdrop_settings || (!count($this->getSelectionHandlerSetting('target_bundles'))
+          || in_array($machine_name, $this->getSelectionHandlerSetting('target_bundles')))) {
+        if ($access_control_handler->createAccess($machine_name)) {
+          $this->accessOptions[$machine_name] = $bundle['label'];
         }
       }
     }
@@ -975,57 +1297,181 @@ class ParagraphsWidget extends WidgetBase {
   }
 
   /**
-   * Builds dropdown button for adding new paragraph.
+   * Helper to create a paragraph UI message.
+   *
+   * @param string $message
+   *   Message text.
+   * @param string $type
+   *   Message type.
    *
    * @return array
-   *   The form element array.
+   *   Render array of message.
    */
-  protected function buildButtonsAddMode() {
-    // Hide the button when translating.
-    $add_more_elements = [
+  public function createMessage($message, $type = 'warning') {
+    return [
       '#type' => 'container',
-      '#theme_wrappers' => ['paragraphs_dropbutton_wrapper'],
+      '#markup' => $message,
+      '#attributes' => ['class' => ['messages', 'messages--' . $type]],
     ];
-    $field_name = $this->fieldDefinition->getName();
-    $title = $this->fieldDefinition->getLabel();
-
-    $drop_button = FALSE;
-    if (count($this->getAccessibleOptions()) > 1 && $this->getSetting('add_mode') == 'dropdown') {
-      $drop_button = TRUE;
-      $add_more_elements['#theme_wrappers'] = ['dropbutton_wrapper'];
-      $add_more_elements['prefix'] = [
-        '#markup' => '<ul class="dropbutton">',
-        '#weight' => -999,
-      ];
-      $add_more_elements['suffix'] = [
-        '#markup' => '</ul>',
-        '#weight' => 999,
+  }
+
+  /**
+   * Expand button base array into a paragraph widget action button.
+   *
+   * @param array $button_base
+   *   Button base render array.
+   *
+   * @return array
+   *   Button render array.
+   */
+  public static function expandButton(array $button_base) {
+    // Do not expand elements that do not have submit handler.
+    if (empty($button_base['#submit'])) {
+      return $button_base;
+    }
+
+    $button = $button_base + [
+      '#type' => 'submit',
+      '#theme_wrappers' => ['input__submit__paragraph_action'],
+    ];
+
+    // Html::getId will give us '-' char in name but we want '_' for now so
+    // we use strtr to search&replace '-' to '_'.
+    $button['#name'] = strtr(Html::getId($button_base['#name']), '-', '_');
+    $button['#id'] = Html::getUniqueId($button['#name']);
+
+    if (isset($button['#ajax'])) {
+      $button['#ajax'] += [
+        'effect' => 'fade',
+        // Since a normal throbber is added inline, this has the potential to
+        // break a layout if the button is located in dropbuttons. Instead,
+        // it's safer to just show the fullscreen progress element instead.
+        'progress' => ['type' => 'fullscreen'],
       ];
-      $add_more_elements['#suffix'] = $this->t(' to %type', ['%type' => $title]);
     }
 
-    foreach ($this->getAccessibleOptions() as $machine_name => $label) {
-      $add_more_elements['add_more_button_' . $machine_name] = [
+    return $button;
+  }
+
+  /**
+   * Get common submit element information for processing ajax submit handlers.
+   *
+   * @param array $form
+   *   Form array.
+   * @param FormStateInterface $form_state
+   *   Form state object.
+   * @param int $position
+   *   Position of triggering element.
+   *
+   * @return array
+   *   Submit element information.
+   */
+  public static function getSubmitElementInfo(array $form, FormStateInterface $form_state, $position = ParagraphsWidget::ACTION_POSITION_BASE) {
+    $submit['button'] = $form_state->getTriggeringElement();
+
+    // Go up in the form, to the widgets container.
+    if ($position == ParagraphsWidget::ACTION_POSITION_BASE) {
+      $submit['element'] = NestedArray::getValue($form, array_slice($submit['button']['#array_parents'], 0, -2));
+    }
+    if ($position == ParagraphsWidget::ACTION_POSITION_HEADER) {
+      $submit['element'] = NestedArray::getValue($form, array_slice($submit['button']['#array_parents'], 0, -3));
+    }
+    elseif ($position == ParagraphsWidget::ACTION_POSITION_ACTIONS) {
+      $submit['element'] = NestedArray::getValue($form, array_slice($submit['button']['#array_parents'], 0, -5));
+      $delta = array_slice($submit['button']['#array_parents'], -5, -4);
+      $submit['delta'] = $delta[0];
+    }
+
+    $submit['field_name'] = $submit['element']['#field_name'];
+    $submit['parents'] = $submit['element']['#field_parents'];
+
+    // Get widget state.
+    $submit['widget_state'] = static::getWidgetState($submit['parents'], $submit['field_name'], $form_state);
+
+    return $submit;
+  }
+
+  /**
+   * Build drop button.
+   *
+   * @param array $elements
+   *   Elements for drop button.
+   *
+   * @return array
+   *   Drop button array.
+   */
+  protected function buildDropbutton(array $elements = []) {
+    $build = [
+      '#type' => 'container',
+      '#attributes' => ['class' => ['paragraphs-dropbutton-wrapper']],
+    ];
+
+    $operations = [];
+    // Because we are cloning the elements into title sub element we need to
+    // sort children first.
+    foreach (Element::children($elements, TRUE) as $child) {
+      // Clone the element as an operation.
+      $operations[$child] = ['title' => $elements[$child]];
+
+      // Flag the original element as printed so it doesn't render twice.
+      $elements[$child]['#printed'] = TRUE;
+    }
+
+    $build['operations'] = [
+      '#type' => 'paragraph_operations',
+      // Even though operations are run through the "links" element type, the
+      // theme system will render any render array passed as a link "title".
+      '#links' => $operations,
+    ];
+
+    return $build + $elements;
+  }
+
+  /**
+   * Builds dropdown button for adding new paragraph.
+   *
+   * @return array
+   *   The form element array.
+   */
+  protected function buildButtonsAddMode() {
+    $options = $this->getAccessibleOptions();
+    $add_mode = $this->getSetting('add_mode');
+    $paragraphs_type_storage = \Drupal::entityTypeManager()->getStorage('paragraphs_type');
+
+    // Build the buttons.
+    $add_more_elements = [];
+    foreach ($options as $machine_name => $label) {
+      $button_key = 'add_more_button_' . $machine_name;
+      $add_more_elements[$button_key] = $this->expandButton([
         '#type' => 'submit',
-        '#name' => strtr($this->fieldIdPrefix, '-', '_') . '_' . $machine_name . '_add_more',
-        '#value' => $this->t('Add @type', ['@type' => $label]),
+        '#name' => $this->fieldIdPrefix . '_' . $machine_name . '_add_more',
+        '#value' => $add_mode == 'modal' ? $label : $this->t('Add @type', ['@type' => $label]),
         '#attributes' => ['class' => ['field-add-more-submit']],
-        '#limit_validation_errors' => [array_merge($this->fieldParents, [$field_name, 'add_more'])],
+        '#limit_validation_errors' => [array_merge($this->fieldParents, [$this->fieldDefinition->getName(), 'add_more'])],
         '#submit' => [[get_class($this), 'addMoreSubmit']],
         '#ajax' => [
           'callback' => [get_class($this), 'addMoreAjax'],
           'wrapper' => $this->fieldWrapperId,
-          'effect' => 'fade',
         ],
         '#bundle_machine_name' => $machine_name,
-      ];
+      ]);
 
-      if ($drop_button) {
-        $add_more_elements['add_more_button_' . $machine_name]['#prefix'] = '<li>';
-        $add_more_elements['add_more_button_' . $machine_name]['#suffix'] = '</li>';
+      if ($add_mode === 'modal' && $icon_url = $paragraphs_type_storage->load($machine_name)->getIconUrl()) {
+        $add_more_elements[$button_key]['#attributes']['style'] = 'background-image: url(' . $icon_url . ');';
       }
     }
 
+    // Determine if buttons should be rendered as dropbuttons.
+    if (count($options) > 1 && $add_mode == 'dropdown') {
+      $add_more_elements = $this->buildDropbutton($add_more_elements);
+      $add_more_elements['#suffix'] = $this->t('to %type', ['%type' => $this->fieldDefinition->getLabel()]);
+    }
+    elseif ($add_mode == 'modal') {
+      $this->buildModalAddForm($add_more_elements);
+      $add_more_elements['add_modal_form_area']['#suffix'] = $this->t('to %type', ['%type' => $this->fieldDefinition->getLabel()]);
+    }
+    $add_more_elements['#weight'] = 1;
+
     return $add_more_elements;
   }
 
@@ -1037,18 +1483,19 @@ class ParagraphsWidget extends WidgetBase {
    */
   protected function buildSelectAddMode() {
     $field_name = $this->fieldDefinition->getName();
-    $title = $this->fieldDefinition->getLabel();
+    $field_title = $this->fieldDefinition->getLabel();
+    $setting_title = $this->getSetting('title');
     $add_more_elements['add_more_select'] = [
       '#type' => 'select',
       '#options' => $this->getAccessibleOptions(),
-      '#title' => $this->t('@title type', ['@title' => $this->getSetting('title')]),
+      '#title' => $this->t('@title type', ['@title' => $setting_title]),
       '#label_display' => 'hidden',
     ];
 
-    $text = $this->t('Add @title', ['@title' => $this->getSetting('title')]);
+    $text = $this->t('Add @title', ['@title' => $setting_title]);
 
     if ($this->realItemCount > 0) {
-      $text = $this->t('Add another @title', ['@title' => $this->getSetting('title')]);
+      $text = $this->t('Add another @title', ['@title' => $setting_title]);
     }
 
     $add_more_elements['add_more_button'] = [
@@ -1065,7 +1512,7 @@ class ParagraphsWidget extends WidgetBase {
       ],
     ];
 
-    $add_more_elements['add_more_button']['#suffix'] = $this->t(' to %type', ['%type' => $title]);
+    $add_more_elements['add_more_button']['#suffix'] = $this->t(' to %type', ['%type' => $field_title]);
     return $add_more_elements;
   }
 
@@ -1073,12 +1520,11 @@ class ParagraphsWidget extends WidgetBase {
    * {@inheritdoc}
    */
   public static function addMoreAjax(array $form, FormStateInterface $form_state) {
-    $button = $form_state->getTriggeringElement();
-    // Go one level up in the form, to the widgets container.
-    $element = NestedArray::getValue($form, array_slice($button['#array_parents'], 0, -2));
+    $submit = ParagraphsWidget::getSubmitElementInfo($form, $form_state);
+    $element = $submit['element'];
 
     // Add a DIV around the delta receiving the Ajax effect.
-    $delta = $element['#max_delta'];
+    $delta = $submit['element']['#max_delta'];
     $element[$delta]['#prefix'] = '<div class="ajax-new-content">' . (isset($element[$delta]['#prefix']) ? $element[$delta]['#prefix'] : '');
     $element[$delta]['#suffix'] = (isset($element[$delta]['#suffix']) ? $element[$delta]['#suffix'] : '') . '</div>';
 
@@ -1086,31 +1532,40 @@ class ParagraphsWidget extends WidgetBase {
   }
 
   /**
-   * {@inheritdoc}
+   * Ajax callback for all actions.
    */
-  public static function addMoreSubmit(array $form, FormStateInterface $form_state) {
-    $button = $form_state->getTriggeringElement();
+  public static function allActionsAjax(array $form, FormStateInterface $form_state) {
+    $submit = ParagraphsWidget::getSubmitElementInfo($form, $form_state, ParagraphsWidget::ACTION_POSITION_HEADER);
+    $element = $submit['element'];
 
-    // Go one level up in the form, to the widgets container.
-    $element = NestedArray::getValue($form, array_slice($button['#array_parents'], 0, -2));
-    $field_name = $element['#field_name'];
-    $parents = $element['#field_parents'];
+    // Add a DIV around the delta receiving the Ajax effect.
+    $delta = $submit['element']['#max_delta'];
+    $element[$delta]['#prefix'] = '<div class="ajax-new-content">' . (isset($element[$delta]['#prefix']) ? $element[$delta]['#prefix'] : '');
+    $element[$delta]['#suffix'] = (isset($element[$delta]['#suffix']) ? $element[$delta]['#suffix'] : '') . '</div>';
 
-    // Increment the items count.
-    $widget_state = static::getWidgetState($parents, $field_name, $form_state);
+    return $element;
+  }
 
-    if ($widget_state['real_item_count'] < $element['#cardinality'] || $element['#cardinality'] == FieldStorageDefinitionInterface::CARDINALITY_UNLIMITED) {
-      $widget_state['items_count']++;
+  /**
+   * {@inheritdoc}
+   */
+  public static function addMoreSubmit(array $form, FormStateInterface $form_state) {
+    $submit = ParagraphsWidget::getSubmitElementInfo($form, $form_state);
+
+    if ($submit['widget_state']['real_item_count'] < $submit['element']['#cardinality'] || $submit['element']['#cardinality'] == FieldStorageDefinitionInterface::CARDINALITY_UNLIMITED) {
+      $submit['widget_state']['items_count']++;
     }
 
-    if (isset($button['#bundle_machine_name'])) {
-      $widget_state['selected_bundle'] = $button['#bundle_machine_name'];
+    if (isset($submit['button']['#bundle_machine_name'])) {
+      $submit['widget_state']['selected_bundle'] = $submit['button']['#bundle_machine_name'];
     }
     else {
-      $widget_state['selected_bundle'] = $element['add_more']['add_more_select']['#value'];
+      $submit['widget_state']['selected_bundle'] = $submit['element']['add_more']['add_more_select']['#value'];
     }
 
-    static::setWidgetState($parents, $field_name, $form_state, $widget_state);
+    $submit['widget_state'] = static::autocollapse($submit['widget_state']);
+
+    static::setWidgetState($submit['parents'], $submit['field_name'], $form_state, $submit['widget_state']);
 
     $form_state->setRebuild();
   }
@@ -1121,71 +1576,305 @@ class ParagraphsWidget extends WidgetBase {
   public static function duplicateSubmit(array $form, FormStateInterface $form_state) {
     $button = $form_state->getTriggeringElement();
     // Go one level up in the form, to the widgets container.
-    $element = NestedArray::getValue($form, array_slice($button['#array_parents'], 0, -4));
+    $element = NestedArray::getValue($form, array_slice($button['#array_parents'], 0, -5));
     $field_name = $element['#field_name'];
     $parents = $element['#field_parents'];
 
     // Inserting new element in the array.
     $widget_state = static::getWidgetState($parents, $field_name, $form_state);
-    $delta = $button['#delta'];
+
+    // Map the button delta to the actual delta.
+    $original_button_delta = $button['#delta'];
+    $current_button_delta = array_search($button['#delta'], $widget_state['original_deltas']);
+
     $widget_state['items_count']++;
     $widget_state['real_item_count']++;
-    $widget_state['original_deltas'] = array_merge($widget_state['original_deltas'], ['1' => 1]) ;
 
-    // Check if the replicate module is enabled
+    // Initialize the new original delta map with the new entry.
+    $new_original_deltas = [
+      $current_button_delta + 1 => count($widget_state['original_deltas']),
+    ];
+
+    $user_input = NestedArray::getValue($form_state->getUserInput(), array_slice($button['#parents'], 0, -5));
+    $user_input[count($widget_state['original_deltas'])]['_weight'] = $current_button_delta + 1;
+
+    // Increase all original deltas bigger than the delta of the duplicated
+    // element by one.
+    foreach ($widget_state['original_deltas'] as $current_delta => $original_delta) {
+      $new_delta = $current_delta > $current_button_delta ? $current_delta + 1 : $current_delta;
+      $new_original_deltas[$new_delta] = $original_delta;
+      $user_input[$original_delta]['_weight'] = $new_delta;
+    }
+    $widget_state['original_deltas'] = $new_original_deltas;
+    /** @var \Drupal\Core\Entity\EntityInterface $entity */
+    $entity = $widget_state['paragraphs'][$original_button_delta]['entity'];
+
+    $widget_state = static::autocollapse($widget_state);
+
+    // Check if the replicate module is enabled.
     if (\Drupal::hasService('replicate.replicator')) {
-      $duplicate_entity = \Drupal::getContainer()->get('replicate.replicator')->replicateEntity($widget_state['paragraphs'][$delta]['entity']);
-      }
+      $duplicate_entity = \Drupal::getContainer()->get('replicate.replicator')->replicateEntity($entity);
+    }
     else {
-      $duplicate_entity = $widget_state['paragraphs'][$delta]['entity']->createDuplicate();
-     }
+      $duplicate_entity = $entity->createDuplicate();
+    }
     // Create the duplicated paragraph and insert it below the original.
-    $paragraph[] = [
+    $widget_state['paragraphs'][] = [
       'entity' => $duplicate_entity,
-      'display' => $widget_state['paragraphs'][$delta]['display'],
-      'mode' => 'edit'
+      'display' => $widget_state['paragraphs'][$original_button_delta]['display'],
+      'mode' => 'edit',
     ];
 
-    array_splice($widget_state['paragraphs'], $delta + 1, 0, $paragraph);
-
+    NestedArray::setValue($form_state->getUserInput(), array_slice($button['#parents'], 0, -5), $user_input);
     static::setWidgetState($parents, $field_name, $form_state, $widget_state);
     $form_state->setRebuild();
   }
 
   public static function paragraphsItemSubmit(array $form, FormStateInterface $form_state) {
-    $button = $form_state->getTriggeringElement();
+    $submit = ParagraphsWidget::getSubmitElementInfo($form, $form_state, ParagraphsWidget::ACTION_POSITION_ACTIONS);
 
-    // Go one level up in the form, to the widgets container.
-    $element = NestedArray::getValue($form, array_slice($button['#array_parents'], 0, -4));
+    $new_mode = $submit['button']['#paragraphs_mode'];
 
-    $delta = array_slice($button['#array_parents'], -4, -3);
-    $delta = $delta[0];
+    if ($new_mode === 'edit') {
+      $submit['widget_state'] = static::autocollapse($submit['widget_state']);
+    }
 
-    $field_name = $element['#field_name'];
-    $parents = $element['#field_parents'];
+    $submit['widget_state']['paragraphs'][$submit['delta']]['mode'] = $new_mode;
 
-    $widget_state = static::getWidgetState($parents, $field_name, $form_state);
+    if (!empty($submit['button']['#paragraphs_show_warning'])) {
+      $submit['widget_state']['paragraphs'][$submit['delta']]['show_warning'] = $submit['button']['#paragraphs_show_warning'];
+    }
+
+    static::setWidgetState($submit['parents'], $submit['field_name'], $form_state, $submit['widget_state']);
+
+    $form_state->setRebuild();
+  }
+
+  public static function itemAjax(array $form, FormStateInterface $form_state) {
+    $submit = ParagraphsWidget::getSubmitElementInfo($form, $form_state, ParagraphsWidget::ACTION_POSITION_ACTIONS);
 
-    $widget_state['paragraphs'][$delta]['mode'] = $button['#paragraphs_mode'];
+    $submit['element']['#prefix'] = '<div class="ajax-new-content">' . (isset($submit['element']['#prefix']) ? $submit['element']['#prefix'] : '');
+    $submit['element']['#suffix'] = (isset($submit['element']['#suffix']) ? $submit['element']['#suffix'] : '') . '</div>';
 
-    if (!empty($button['#paragraphs_show_warning'])) {
-      $widget_state['paragraphs'][$delta]['show_warning'] = $button['#paragraphs_show_warning'];
+    return $submit['element'];
+  }
+
+  /**
+   * Sets the form mode accordingly.
+   *
+   * @param array $form
+   *   An associate array containing the structure of the form.
+   * @param \Drupal\Core\Form\FormStateInterface $form_state
+   *   The current state of the form.
+   */
+  public static function dragDropModeSubmit(array $form, FormStateInterface $form_state) {
+    $submit = ParagraphsWidget::getSubmitElementInfo($form, $form_state, ParagraphsWidget::ACTION_POSITION_HEADER);
+
+    if (empty($submit['widget_state']['dragdrop'])) {
+      $submit['widget_state']['dragdrop'] = TRUE;
+    }
+    else {
+      $submit['widget_state']['dragdrop'] = FALSE;
     }
 
-    static::setWidgetState($parents, $field_name, $form_state, $widget_state);
+    // Make sure that flag that we already reordered is unset when the mode is
+    // switched.
+    unset($submit['widget_state']['reordered']);
+
+    // Switch the form mode accordingly.
+    static::setWidgetState($submit['parents'], $submit['field_name'], $form_state, $submit['widget_state']);
 
     $form_state->setRebuild();
   }
 
-  public static function itemAjax(array $form, FormStateInterface $form_state) {
-    $button = $form_state->getTriggeringElement();
-    // Go one level up in the form, to the widgets container.
-    $element = NestedArray::getValue($form, array_slice($button['#array_parents'], 0, -4));
 
-    $element['#prefix'] = '<div class="ajax-new-content">' . (isset($element['#prefix']) ? $element['#prefix'] : '');
-    $element['#suffix'] = (isset($element['#suffix']) ? $element['#suffix'] : '') . '</div>';
+  /**
+   * Reorder paragraphs.
+   *
+   * @param \Drupal\Core\Form\FormStateInterface $form_state
+   *   The form state.
+   * @param $field_values_parents
+   *   The field value parents.
+   */
+  protected static function reorderParagraphs(FormStateInterface $form_state, $field_values_parents) {
+    $field_name = end($field_values_parents);
+    $field_values = NestedArray::getValue($form_state->getValues(), $field_values_parents);
+    $complete_field_storage = NestedArray::getValue(
+      $form_state->getStorage(), [
+        'field_storage',
+        '#parents'
+      ]
+    );
+    $new_field_storage = $complete_field_storage;
 
-    return $element;
+    // Set a flag to prevent this from running twice, as the entity is built
+    // for validation as well as saving and would fail the second time as we
+    // already altered the field storage.
+    if (!empty($new_field_storage['#fields'][$field_name]['reordered'])) {
+      return;
+    }
+    $new_field_storage['#fields'][$field_name]['reordered'] = TRUE;
+
+    // Clear out all current paragraphs keys in all nested paragraph widgets
+    // as there might be fewer than before or none in a certain widget.
+    $clear_paragraphs = function ($field_storage) use (&$clear_paragraphs) {
+      foreach ($field_storage as $key => $value) {
+        if ($key === '#fields') {
+          foreach ($value as $field_name => $widget_state) {
+            if (isset($widget_state['paragraphs'])) {
+              $field_storage['#fields'][$field_name]['paragraphs'] = [];
+            }
+          }
+        }
+        else {
+          $field_storage[$key] = $clear_paragraphs($field_storage[$key]);
+        }
+      }
+      return $field_storage;
+    };
+
+    // Only clear the current field and its children to avoid deleting
+    // paragraph references in other fields.
+    $new_field_storage['#fields'][$field_name]['paragraphs'] = [];
+    if (isset($new_field_storage[$field_name])) {
+      $new_field_storage[$field_name] = $clear_paragraphs($new_field_storage[$field_name]);
+    }
+
+    $reorder_paragraphs = function ($reorder_values, $parents = [], FieldableEntityInterface $parent_entity = NULL) use ($complete_field_storage, &$new_field_storage, &$reorder_paragraphs) {
+      foreach ($reorder_values as $field_name => $values) {
+        foreach ($values['list'] as $delta => $item_values) {
+          $old_keys = array_merge(
+            $parents, [
+              '#fields',
+              $field_name,
+              'paragraphs',
+              $delta
+            ]
+          );
+          $path = explode('][', $item_values['_path']);
+          $new_field_name = array_pop($path);
+          $key_parents = [];
+          foreach ($path as $i => $key) {
+            $key_parents[] = $key;
+            if ($i % 2 == 1) {
+              $key_parents[] = 'subform';
+            }
+          }
+          $new_keys = array_merge(
+            $key_parents, [
+              '#fields',
+              $new_field_name,
+              'paragraphs',
+              $item_values['_weight']
+            ]
+          );
+          $key_exists = NULL;
+          $item_state = NestedArray::getValue($complete_field_storage, $old_keys, $key_exists);
+          if (!$key_exists && $parent_entity) {
+            // If key does not exist, then this parent widget was previously
+            // not expanded. This can only happen on nested levels. In that
+            // case, initialize a new item state and set the widget state to
+            // an empty array if it is not already set from an earlier item.
+            // If something else is placed there, it will be put in there,
+            // otherwise the widget will know that nothing is there anymore.
+            $item_state = [
+              'entity' => $parent_entity->get($field_name)->get($delta)->entity,
+              'mode' => 'closed',
+            ];
+            $widget_state_keys = array_slice($old_keys, 0, count($old_keys) - 2);
+            if (!NestedArray::getValue($new_field_storage, $widget_state_keys)) {
+              NestedArray::setValue($new_field_storage, $widget_state_keys, ['paragraphs' => []]);
+            }
+          }
+
+          // Ensure the referenced paragraph will be saved.
+          $item_state['entity']->setNeedsSave(TRUE);
+
+          NestedArray::setValue($new_field_storage, $new_keys, $item_state);
+          if (isset($item_values['dragdrop'])) {
+            $reorder_paragraphs(
+              $item_values['dragdrop'], array_merge(
+              $parents, [
+                $field_name,
+                $delta,
+                'subform'
+              ]
+            ), $item_state['entity']
+            );
+          }
+        }
+      }
+    };
+    $reorder_paragraphs($field_values['dragdrop']);
+
+    // Recalculate original deltas.
+    $recalculate_original_deltas = function ($field_storage, ContentEntityInterface $parent_entity) use (&$recalculate_original_deltas) {
+      if (isset($field_storage['#fields'])) {
+        foreach ($field_storage['#fields'] as $field_name => $widget_state) {
+          if (isset($widget_state['paragraphs'])) {
+
+            // If the parent field does not exist but we have paragraphs in
+            // widget state, something went wrong and we have a mismatch.
+            // Throw an exception.
+            if (!$parent_entity->hasField($field_name) && !empty($widget_state['paragraphs'])) {
+              throw new \LogicException('Reordering paragraphs resulted in paragraphs on non-existing field ' . $field_name . ' on parent entity ' . $parent_entity->getEntityTypeId() . '/' . $parent_entity->id());
+            }
+
+            // Sort the paragraphs by key so that they will be assigned to
+            // the entity in the right order. Reset the deltas.
+            ksort($widget_state['paragraphs']);
+            $widget_state['paragraphs'] = array_values($widget_state['paragraphs']);
+
+            $original_deltas = range(0, count($widget_state['paragraphs']) - 1);
+            $field_storage['#fields'][$field_name]['original_deltas'] = $original_deltas;
+            $field_storage['#fields'][$field_name]['items_count'] = count($widget_state['paragraphs']);
+            $field_storage['#fields'][$field_name]['real_item_count'] = count($widget_state['paragraphs']);
+
+            // Update the parent entity and point to the new children, if the
+            // parent field does not exist, we also have no paragraphs, so
+            // we can just skip this, this is a dead leaf after re-ordering.
+            // @todo Clean this up somehow?
+            if ($parent_entity->hasField($field_name)) {
+              $parent_entity->set($field_name, array_column($widget_state['paragraphs'], 'entity'));
+
+              // Next process that field recursively.
+              foreach (array_keys($widget_state['paragraphs']) as $delta) {
+                if (isset($field_storage[$field_name][$delta]['subform'])) {
+                  $field_storage[$field_name][$delta]['subform'] = $recalculate_original_deltas($field_storage[$field_name][$delta]['subform'], $parent_entity->get($field_name)->get($delta)->entity);
+                }
+              }
+            }
+
+          }
+        }
+      }
+      return $field_storage;
+    };
+
+    $parent_entity = $form_state->getFormObject()->getEntity();
+    $new_field_storage = $recalculate_original_deltas($new_field_storage, $parent_entity);
+
+    $form_state->set(['field_storage', '#parents'], $new_field_storage);
+  }
+
+  /**
+   * Ajax callback for the dragdrop mode.
+   *
+   * @param array $form
+   *   An associate array containing the structure of the form.
+   * @param \Drupal\Core\Form\FormStateInterface $form_state
+   *   The current state of the form.
+   *
+   * @return array
+   *   The container form element.
+   */
+  public static function dragDropModeAjax(array $form, FormStateInterface $form_state) {
+    $submit = ParagraphsWidget::getSubmitElementInfo($form, $form_state, ParagraphsWidget::ACTION_POSITION_HEADER);
+
+    $submit['element']['#prefix'] = '<div class="ajax-new-content">' . (isset($submit['element']['#prefix']) ? $submit['element']['#prefix'] : '');
+    $submit['element']['#suffix'] = (isset($submit['element']['#suffix']) ? $submit['element']['#suffix'] : '') . '</div>';
+
+    return $submit['element'];
   }
 
   /**
@@ -1211,6 +1900,7 @@ class ParagraphsWidget extends WidgetBase {
     $delta = $element['#delta'];
 
     if (isset($widget_state['paragraphs'][$delta]['entity'])) {
+      /** @var \Drupal\paragraphs\ParagraphInterface $paragraphs_entity */
       $entity = $widget_state['paragraphs'][$delta]['entity'];
 
       /** @var \Drupal\Core\Entity\Display\EntityFormDisplayInterface $display */
@@ -1223,9 +1913,11 @@ class ParagraphsWidget extends WidgetBase {
 
         // Validate all enabled behavior plugins.
         $paragraphs_type = $entity->getParagraphType();
-        foreach ($paragraphs_type->getEnabledBehaviorPlugins() as $plugin_id => $plugin_values) {
-          $subform_state = SubformState::createForSubform($element['behavior_plugins'][$plugin_id], $form_state->getCompleteForm(), $form_state);
-          $plugin_values->validateBehaviorForm($entity, $element['behavior_plugins'][$plugin_id], $subform_state);
+        if (\Drupal::currentUser()->hasPermission('edit behavior plugin settings')) {
+          foreach ($paragraphs_type->getEnabledBehaviorPlugins() as $plugin_id => $plugin_values) {
+            $subform_state = SubformState::createForSubform($element['behavior_plugins'][$plugin_id], $form_state->getCompleteForm(), $form_state);
+            $plugin_values->validateBehaviorForm($entity, $element['behavior_plugins'][$plugin_id], $subform_state);
+          }
         }
       }
     }
@@ -1233,41 +1925,85 @@ class ParagraphsWidget extends WidgetBase {
     static::setWidgetState($element['#field_parents'], $field_name, $form_state, $widget_state);
   }
 
+  /**
+   * {@inheritdoc}
+   */
+  public function flagErrors(FieldItemListInterface $items, ConstraintViolationListInterface $violations, array $form, FormStateInterface $form_state) {
+    $field_name = $this->fieldDefinition->getName();
+
+    $field_state = static::getWidgetState($form['#parents'], $field_name, $form_state);
+
+    // In dragdrop mode, validation errors can not be mapped to form elements,
+    // add them on the top level widget element.
+    if (!empty($field_state['dragdrop'])) {
+      if ($violations->count()) {
+        $element = NestedArray::getValue($form_state->getCompleteForm(), $field_state['array_parents']);
+        foreach ($violations as $violation) {
+          $form_state->setError($element, $violation->getMessage());
+        }
+      }
+    }
+    else {
+      return parent::flagErrors($items, $violations, $form, $form_state);
+    }
+  }
+
+  /**
+   * Special handling to validate form elements with multiple values.
+   *
+   * @param array $elements
+   *   An associative array containing the substructure of the form to be
+   *   validated in this call.
+   * @param \Drupal\Core\Form\FormStateInterface $form_state
+   *   The current state of the form.
+   * @param array $form
+   *   The complete form array.
+   */
+  public function multipleElementValidate(array $elements, FormStateInterface $form_state, array $form) {
+    $field_name = $this->fieldDefinition->getName();
+    $widget_state = static::getWidgetState($elements['#field_parents'], $field_name, $form_state);
+
+    if ($elements['#required'] && $widget_state['real_item_count'] < 1) {
+      $form_state->setError($elements, t('@name field is required.', ['@name' => $this->fieldDefinition->getLabel()]));
+    }
+
+    static::setWidgetState($elements['#field_parents'], $field_name, $form_state, $widget_state);
+  }
+
   /**
    * {@inheritdoc}
    */
   public function massageFormValues(array $values, array $form, FormStateInterface $form_state) {
-    $entity = $form_state->getFormObject()->getEntity();
     $field_name = $this->fieldDefinition->getName();
     $widget_state = static::getWidgetState($form['#parents'], $field_name, $form_state);
     $element = NestedArray::getValue($form_state->getCompleteForm(), $widget_state['array_parents']);
 
-    $new_revision = FALSE;
-    if ($entity instanceof RevisionableInterface) {
-      if ($entity->isNewRevision()) {
-        $new_revision = TRUE;
-      }
-      // Most of the time we don't know yet if the host entity is going to be
-      // saved as a new revision using RevisionableInterface::isNewRevision().
-      // Most entity types (at least nodes) however use a boolean property named
-      // "revision" to indicate whether a new revision should be saved. Use that
-      // property.
-      elseif ($entity->getEntityType()->hasKey('revision') && $form_state->getValue('revision')) {
-        $new_revision = TRUE;
+    if (!empty($widget_state['dragdrop'])) {
+      $path = array_merge($form['#parents'], array($field_name));
+      static::reorderParagraphs($form_state, $path);
+
+      // After re-ordering, get the updated widget state.
+      $widget_state = static::getWidgetState($form['#parents'], $field_name, $form_state);
+
+      // Re-create values based on current widget state.
+      $values = [];
+      foreach ($widget_state['paragraphs'] as $delta => $paragraph_state) {
+        $values[$delta]['entity'] = $paragraph_state['entity'];
       }
+      return $values;
     }
 
     foreach ($values as $delta => &$item) {
       if (isset($widget_state['paragraphs'][$item['_original_delta']]['entity'])
         && $widget_state['paragraphs'][$item['_original_delta']]['mode'] != 'remove') {
+        /** @var \Drupal\paragraphs\ParagraphInterface $paragraphs_entity */
         $paragraphs_entity = $widget_state['paragraphs'][$item['_original_delta']]['entity'];
 
         /** @var \Drupal\Core\Entity\Display\EntityFormDisplayInterface $display */
-        $display =  $widget_state['paragraphs'][$item['_original_delta']]['display'];
+        $display = $widget_state['paragraphs'][$item['_original_delta']]['display'];
         if ($widget_state['paragraphs'][$item['_original_delta']]['mode'] == 'edit') {
           $display->extractFormValues($paragraphs_entity, $element[$item['_original_delta']]['subform'], $form_state);
         }
-        $paragraphs_entity->setNewRevision($new_revision);
         // A content entity form saves without any rebuild. It needs to set the
         // language to update it in case of language change.
         $langcode_key = $paragraphs_entity->getEntityType()->getKey('langcode');
@@ -1288,9 +2024,12 @@ class ParagraphsWidget extends WidgetBase {
             if (!isset($item['behavior_plugins'][$plugin_id])) {
               $item['behavior_plugins'][$plugin_id] = [];
             }
-            if (isset($element[$delta]) && isset($element[$delta]['behavior_plugins'][$plugin_id]) && $form_state->getCompleteForm()) {
-              $subform_state = SubformState::createForSubform($element[$delta]['behavior_plugins'][$plugin_id], $form_state->getCompleteForm(), $form_state);
-              $plugin_values->submitBehaviorForm($paragraphs_entity, $item['behavior_plugins'][$plugin_id], $subform_state);
+            $original_delta = $item['_original_delta'];
+            if (isset($element[$original_delta]) && isset($element[$original_delta]['behavior_plugins'][$plugin_id]) && $form_state->getCompleteForm() && \Drupal::currentUser()->hasPermission('edit behavior plugin settings')) {
+              $subform_state = SubformState::createForSubform($element[$original_delta]['behavior_plugins'][$plugin_id], $form_state->getCompleteForm(), $form_state);
+              if (isset($item['behavior_plugins'][$plugin_id])) {
+                $plugin_values->submitBehaviorForm($paragraphs_entity, $item['behavior_plugins'][$plugin_id], $subform_state);
+              }
             }
           }
         }
@@ -1302,7 +2041,7 @@ class ParagraphsWidget extends WidgetBase {
       }
       // If our mode is remove don't save or reference this entity.
       // @todo: Maybe we should actually delete it here?
-      elseif($widget_state['paragraphs'][$item['_original_delta']]['mode'] == 'remove') {
+      elseif (isset($widget_state['paragraphs'][$item['_original_delta']]['mode']) && $widget_state['paragraphs'][$item['_original_delta']]['mode'] == 'remove') {
         $item['target_id'] = NULL;
         $item['target_revision_id'] = NULL;
       }
@@ -1316,6 +2055,21 @@ class ParagraphsWidget extends WidgetBase {
   public function extractFormValues(FieldItemListInterface $items, array $form, FormStateInterface $form_state) {
     // Filter possible empty items.
     $items->filterEmptyItems();
+
+    // Remove buttons from header actions.
+    $field_name = $this->fieldDefinition->getName();
+    $path = array_merge($form['#parents'], array($field_name));
+    $form_state_variables = $form_state->getValues();
+    $key_exists = NULL;
+    $values = NestedArray::getValue($form_state_variables, $path, $key_exists);
+
+    if ($key_exists) {
+      unset($values['header_actions']);
+
+      NestedArray::setValue($form_state_variables, $path, $values);
+      $form_state->setValues($form_state_variables);
+    }
+
     return parent::extractFormValues($items, $form, $form_state);
   }
 
@@ -1323,9 +2077,9 @@ class ParagraphsWidget extends WidgetBase {
    * Initializes the translation form state.
    *
    * @param \Drupal\Core\Form\FormStateInterface $form_state
-   * @param \Drupal\Core\Entity\EntityInterface $host
+   * @param \Drupal\Core\Entity\ContentEntityInterface $host
    */
-  protected function initIsTranslating(FormStateInterface $form_state, EntityInterface $host) {
+  protected function initIsTranslating(FormStateInterface $form_state, ContentEntityInterface $host) {
     if ($this->isTranslating != NULL) {
       return;
     }
@@ -1390,10 +2144,10 @@ class ParagraphsWidget extends WidgetBase {
   /**
    * Returns the default paragraph type.
    *
-   * @return string $default_paragraph_type
+   * @return string
    *   Label name for default paragraph type.
    */
-  protected function getDefaultParagraphTypeLabelName(){
+  protected function getDefaultParagraphTypeLabelName() {
     if ($this->getDefaultParagraphTypeMachineName() !== NULL) {
       $allowed_types = $this->getAllowedTypes();
       return $allowed_types[$this->getDefaultParagraphTypeMachineName()]['label'];
@@ -1428,63 +2182,30 @@ class ParagraphsWidget extends WidgetBase {
   }
 
   /**
-   * @param \Drupal\paragraphs\Entity\Paragraph $paragraphs_entity
-   *   Entity where to extract the values.
+   * Counts the number of paragraphs in a certain mode in a form substructure.
+   *
+   * @param array $widget_state
+   *   The widget state for the form substructure containing information about
+   *   the paragraphs within.
+   * @param string $mode
+   *   The mode to look for.
    *
-   * @return string $collapsed_summary_text
-   *   The text without tags to return.
+   * @return int
+   *   The number of paragraphs is the given mode.
    */
-  public function addCollapsedSummary(paragraphs\Entity\Paragraph $paragraphs_entity) {
-    $text_types = ['text_with_summary', 'text', 'text_long', 'list_string'];
-    $summary = [];
-    foreach ($paragraphs_entity->getFieldDefinitions() as $key => $value) {
-      if ($value->getType() == 'image') {
-        if ($paragraphs_entity->get($key)->entity) {
-          foreach ($paragraphs_entity->get($key) as $image_key => $image_value) {
-            if ($image_value->title != '') {
-              $text = $image_value->title;
-            }
-            elseif ($image_value->alt != '') {
-              $text = $image_value->alt;
-            }
-            elseif ($text = $image_value->entity->filename->value) {
-              $text = $image_value->entity->filename->value;
-            }
-            if (strlen($text) > 50) {
-              $text = strip_tags(substr($text, 0, 150));
-            }
-            $summary[] = $text;
-          }
-        }
-      }
-      if (in_array($value->getType(), $text_types)) {
-        $text = $paragraphs_entity->get($key)->value;
-        if (strlen($text) > 50) {
-          $text = strip_tags(substr($text, 0, 150));
-        }
-        $summary[] = $text;
-      }
-      if ($field_type = $value->getType() == 'entity_reference_revisions') {
-        if ($paragraphs_entity->get($key) && $paragraphs_entity->get($key)->entity) {
-          $summary[] = $this->addCollapsedSummary($paragraphs_entity->get($key)->entity);
-        }
-      }
-      if ($field_type = $value->getType() == 'entity_reference') {
-        if (!in_array($key, ['type', 'uid', 'revision_uid'])) {
-          if ($paragraphs_entity->get($key)->entity) {
-            $summary[] = $paragraphs_entity->get($key)->entity->label();
-          }
-        }
-      }
+  protected function getNumberOfParagraphsInMode(array $widget_state, $mode) {
+    if (!isset($widget_state['paragraphs'])) {
+      return 0;
     }
-    $paragraphs_type = $paragraphs_entity->getParagraphType();
-    foreach ($paragraphs_type->getEnabledBehaviorPlugins() as $plugin_id => $plugin) {
-      if ($plugin_summary = $plugin->settingsSummary($paragraphs_entity)) {
-        $summary = array_merge($summary, $plugin_summary);
+
+    $paragraphs_count = 0;
+    foreach ($widget_state['paragraphs'] as $paragraph) {
+      if ($paragraph['mode'] == $mode) {
+        $paragraphs_count++;
       }
     }
-    $collapsed_summary_text = implode(', ', $summary);
-    return strip_tags($collapsed_summary_text);
+
+    return $paragraphs_count;
   }
 
   /**
@@ -1494,10 +2215,178 @@ class ParagraphsWidget extends WidgetBase {
     $target_type = $field_definition->getSetting('target_type');
     $paragraph_type = \Drupal::entityTypeManager()->getDefinition($target_type);
     if ($paragraph_type) {
-      return $paragraph_type->isSubclassOf(ParagraphInterface::class);
+      return $paragraph_type->entityClassImplements(ParagraphInterface::class);
     }
 
     return FALSE;
   }
 
+  /**
+   * Builds header actions.
+   *
+   * @param array[] $field_state
+   *   Field widget state.
+   * @param \Drupal\Core\Form\FormStateInterface $form_state
+   *   Current form state.
+   *
+   * @return array[]
+   *   The form element array.
+   */
+  public function buildHeaderActions(array $field_state, FormStateInterface $form_state) {
+    $actions = [];
+    if (empty($this->fieldParents)) {
+      // Set actions.
+      $actions = [
+        '#type' => 'paragraphs_actions',
+      ];
+
+      $field_name = $this->fieldDefinition->getName();
+      $id_prefix = implode('-', array_merge($this->fieldParents, [$field_name]));
+
+      // Only show the dragdrop mode if we can find the sortable library.
+      $library_discovery = \Drupal::service('library.discovery');
+      $library = $library_discovery->getLibraryByName('paragraphs', 'paragraphs-dragdrop');
+      if ($library || \Drupal::state()->get('paragraphs_test_dragdrop_force_show', FALSE)) {
+        $dragdrop_mode = $this->expandButton([
+          '#type' => 'submit',
+          '#name' => $this->fieldIdPrefix . '_dragdrop_mode',
+          '#value' => !empty($field_state['dragdrop']) ? $this->t('Complete drag & drop') : $this->t('Drag & drop'),
+          '#attributes' => ['class' => ['field-dragdrop-mode-submit']],
+          '#submit' => [[get_class($this), 'dragDropModeSubmit']],
+          '#weight' => 8,
+          '#ajax' => [
+            'callback' => [get_class($this), 'dragDropModeAjax'],
+            'wrapper' => $this->fieldWrapperId,
+          ],
+        ]);
+
+        // Make the complete button a primary button, limit validation errors
+        // only for enabling drag and drop mode.
+        if (!empty($field_state['dragdrop'])) {
+          $dragdrop_mode['#button_type'] = 'primary';
+          $actions['actions']['dragdrop_mode'] = $dragdrop_mode;
+        }
+        else {
+          $dragdrop_mode['#limit_validation_errors'] = [
+            array_merge($this->fieldParents, [$field_name, 'dragdrop_mode']),
+          ];
+          $actions['dropdown_actions']['dragdrop_mode'] = $dragdrop_mode;
+        }
+      }
+
+      if ($this->realItemCount > 1 && empty($field_state['dragdrop'])) {
+
+        $collapse_all = $this->expandButton([
+          '#type' => 'submit',
+          '#value' => $this->t('Collapse all'),
+          '#submit' => [[get_class($this), 'changeAllEditModeSubmit']],
+          '#name' => $id_prefix . '_collapse_all',
+          '#paragraphs_mode' => 'closed',
+          '#limit_validation_errors' => [
+            array_merge($this->fieldParents, [$field_name, 'collapse_all']),
+          ],
+          '#ajax' => [
+            'callback' => [get_class($this), 'allActionsAjax'],
+            'wrapper' => $this->fieldWrapperId,
+          ],
+          '#weight' => -1,
+          '#paragraphs_show_warning' => TRUE,
+        ]);
+
+        $edit_all = $this->expandButton([
+          '#type' => 'submit',
+          '#value' => $this->t('Edit all'),
+          '#submit' => [[get_class($this), 'changeAllEditModeSubmit']],
+          '#name' => $id_prefix . '_edit-all',
+          '#paragraphs_mode' => 'edit',
+          '#limit_validation_errors' => [],
+          '#ajax' => [
+            'callback' => [get_class($this), 'allActionsAjax'],
+            'wrapper' => $this->fieldWrapperId,
+          ],
+        ]);
+
+        if (isset($field_state['paragraphs'][0]['mode']) && $field_state['paragraphs'][0]['mode'] === 'closed') {
+          $edit_all['#attributes'] = [
+            'class' => ['paragraphs-icon-button', 'paragraphs-icon-button-edit'],
+            'title' => $this->t('Edit all'),
+          ];
+          $edit_all['#title'] = $this->t('Edit All');
+          $actions['actions']['edit_all'] = $edit_all;
+          $actions['dropdown_actions']['collapse_all'] = $collapse_all;
+        }
+        else {
+          $collapse_all['#attributes'] = [
+            'class' => ['paragraphs-icon-button', 'paragraphs-icon-button-collapse'],
+            'title' => $this->t('Collapse all'),
+          ];
+          $actions['actions']['collapse_all'] = $collapse_all;
+          $actions['dropdown_actions']['edit_all'] = $edit_all;
+        }
+      }
+    }
+
+    // Add paragraphs_header flag which we use later in preprocessor to move
+    // header actions to table header.
+    if ($actions) {
+      $actions['#paragraphs_header'] = TRUE;
+    }
+
+    return $actions;
+  }
+
+  /**
+   * Loops through all paragraphs and change mode for each paragraph instance.
+   *
+   * @param array $form
+   *   Current form state.
+   * @param \Drupal\Core\Form\FormStateInterface $form_state
+   *   Current form state.
+   */
+  public static function changeAllEditModeSubmit(array $form, FormStateInterface $form_state) {
+    $submit = ParagraphsWidget::getSubmitElementInfo($form, $form_state, ParagraphsWidget::ACTION_POSITION_HEADER);
+
+    // Change edit mode for each paragraph.
+    foreach ($submit['widget_state']['paragraphs'] as $delta => &$paragraph) {
+      if ($submit['widget_state']['paragraphs'][$delta]['mode'] !== 'remove') {
+        $submit['widget_state']['paragraphs'][$delta]['mode'] = $submit['button']['#paragraphs_mode'];
+        if (!empty($submit['button']['#paragraphs_show_warning'])) {
+          $submit['widget_state']['paragraphs'][$delta]['show_warning'] = $submit['button']['#paragraphs_show_warning'];
+        }
+      }
+    }
+
+    // Disable autocollapse when editing all and enable it when closing all.
+    if ($submit['button']['#paragraphs_mode'] === 'edit') {
+      $submit['widget_state']['autocollapse'] = 'none';
+    }
+    elseif ($submit['button']['#paragraphs_mode'] === 'closed') {
+      $submit['widget_state']['autocollapse'] = 'all';
+    }
+
+    static::setWidgetState($submit['parents'], $submit['field_name'], $form_state, $submit['widget_state']);
+    $form_state->setRebuild();
+  }
+
+  /**
+   * Returns a state with all paragraphs closed, if autocollapse is enabled.
+   *
+   * @param array $widget_state
+   *   The current widget state.
+   *
+   * @return array
+   *   The widget state altered by closing all paragraphs.
+   */
+  public static function autocollapse(array $widget_state) {
+    if ($widget_state['real_item_count'] > 0 && $widget_state['autocollapse'] !== 'none') {
+      foreach ($widget_state['paragraphs'] as $delta => $value) {
+        if ($widget_state['paragraphs'][$delta]['mode'] === 'edit') {
+          $widget_state['paragraphs'][$delta]['mode'] = 'closed';
+        }
+      }
+    }
+
+    return $widget_state;
+  }
+
 }