diff --git a/src/Entity/Paragraph.php b/src/Entity/Paragraph.php
index 00bc6cb..6b41f19 100644
--- a/src/Entity/Paragraph.php
+++ b/src/Entity/Paragraph.php
@@ -328,4 +328,22 @@ class Paragraph extends ContentEntityBase implements ParagraphInterface, EntityN
return array(\Drupal::currentUser()->id());
}
+ /**
+ * {@inheritdoc}
+ */
+ public function createDuplicate() {
+ $duplicate = parent::createDuplicate();
+ // Loop over entity fields and duplicate nested paragraphs.
+ foreach ($duplicate->getFields() as $field) {
+ if ($field->getFieldDefinition()->getType() == 'entity_reference_revisions') {
+ if ($field->getFieldDefinition()->getTargetEntityTypeId() == "paragraph") {
+ foreach ($field as $item) {
+ $item->entity = $item->entity->createDuplicate();
+ }
+ }
+ }
+ }
+ return $duplicate;
+ }
+
}
diff --git a/src/Plugin/Field/FieldWidget/InlineParagraphsWidget.php b/src/Plugin/Field/FieldWidget/InlineParagraphsWidget.php
index c43f46d..2ddbde0 100644
--- a/src/Plugin/Field/FieldWidget/InlineParagraphsWidget.php
+++ b/src/Plugin/Field/FieldWidget/InlineParagraphsWidget.php
@@ -429,6 +429,25 @@ class InlineParagraphsWidget extends WidgetBase {
'#paragraphs_mode' => 'edit',
);
+ $links['duplicate_button'] = [
+ '#type' => 'submit',
+ '#value' => $this->t('Duplicate'),
+ '#name' => strtr($id_prefix, '-', '_') . '_duplicate',
+ '#weight' => 502,
+ '#submit' => [[get_class($this), 'duplicateSubmit']],
+ '#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'),
+ '#prefix' => '
',
+ '#suffix' => '',
+ '#paragraphs_mode' => 'duplicate',
+ ];
+
$element['top']['paragraphs_remove_button_container']['paragraphs_remove_button'] = [
'#type' => 'submit',
'#value' => $this->t('Remove'),
@@ -1053,6 +1072,35 @@ class InlineParagraphsWidget extends WidgetBase {
$form_state->setRebuild();
}
+ /**
+ * Creates a duplicate of the paragraph entity.
+ */
+ 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));
+ $field_name = $element['#field_name'];
+ $parents = $element['#field_parents'];
+
+ // Inserting new element in the array.
+ $field_state = static::getWidgetState($parents, $field_name, $form_state);
+ $delta = $button['#delta'];
+ $field_state['items_count']++;
+ $field_state['real_item_count']++;
+ $field_state['original_deltas'] = array_merge($field_state['original_deltas'], ['1' => 1]) ;
+
+ // Create the duplicated paragraph and insert it below the original.
+ $paragraph[] = [
+ 'entity' => $field_state['paragraphs'][$delta]['entity']->createDuplicate(),
+ 'display' => $field_state['paragraphs'][$delta]['display'],
+ 'mode' => 'edit'
+ ];
+ array_splice($field_state['paragraphs'], $delta + 1, 0, $paragraph);
+
+ static::setWidgetState($parents, $field_name, $form_state, $field_state);
+ $form_state->setRebuild();
+ }
+
public static function paragraphsItemSubmit(array $form, FormStateInterface $form_state) {
$button = $form_state->getTriggeringElement();
diff --git a/src/Tests/ParagraphsTestBase.php b/src/Tests/ParagraphsTestBase.php
index 222ff96..fd92980 100644
--- a/src/Tests/ParagraphsTestBase.php
+++ b/src/Tests/ParagraphsTestBase.php
@@ -129,9 +129,9 @@ abstract class ParagraphsTestBase extends WebTestBase {
'field_name' => $paragraphs_field_name,
'entity_type' => $entity_type,
'type' => 'entity_reference_revisions',
+ 'cardinality' => '-1',
'settings' => [
'target_type' => 'paragraph',
- 'cardinality' => '-1',
],
]);
$field_storage->save();
diff --git a/src/Tests/ParagraphsWidgetButtonsTest.php b/src/Tests/ParagraphsWidgetButtonsTest.php
index 73d0b8a..3781a61 100644
--- a/src/Tests/ParagraphsWidgetButtonsTest.php
+++ b/src/Tests/ParagraphsWidgetButtonsTest.php
@@ -105,6 +105,106 @@ class ParagraphsWidgetButtonsTest extends ParagraphsTestBase {
}
/**
+ * Tests duplicate paragraph feature.
+ */
+ public function testDuplicateButton(){
+ $this->addParagraphedContentType('paragraphed_test', 'field_paragraphs');
+
+ $this->loginAsAdmin(['create paragraphed_test content', 'edit any paragraphed_test content']);
+ // Add a Paragraph type.
+ $paragraph_type = 'text_paragraph';
+ $this->addParagraphsType($paragraph_type);
+ $this->addParagraphsType('text');
+
+ // Add a text field to the text_paragraph type.
+ static::fieldUIAddNewField('admin/structure/paragraphs_type/' . $paragraph_type, 'text', 'Text', 'text_long', [], []);
+ $this->drupalPostAjaxForm('node/add/paragraphed_test', [], 'field_paragraphs_text_paragraph_add_more');
+
+ // Create a node with a Paragraph.
+ $text = 'recognizable_text';
+ $edit = [
+ 'title[0][value]' => 'paragraphs_mode_test',
+ 'field_paragraphs[0][subform][field_text][0][value]' => $text,
+ ];
+ $this->drupalPostForm(NULL, $edit, t('Save and publish'));
+ $node = $this->drupalGetNodeByTitle('paragraphs_mode_test');
+
+ // Change edit mode to "closed".
+ $this->setParagraphsWidgetMode('paragraphed_test', 'field_paragraphs', 'closed');
+ $this->drupalGet('node/' . $node->id() . '/edit');
+
+ // Click "Duplicate" button.
+ $this->drupalPostAjaxForm(NULL, [], 'field_paragraphs_0_duplicate');
+ $this->drupalPostAjaxForm(NULL, [], 'field_paragraphs_0_edit');
+ $this->assertFieldByName('field_paragraphs[0][subform][field_text][0][value]', $text);
+ $this->assertFieldByName('field_paragraphs[1][subform][field_text][0][value]', $text);
+
+ // Save and check if both paragraphs are present.
+ $this->drupalPostForm(NULL, $edit, t('Save and keep published'));
+ $this->assertNoUniqueText($text);
+ }
+
+ /**
+ * Tests duplicate paragraph feature with nested paragraphs.
+ */
+ public function testDuplicateButtonWithNesting(){
+ $this->addParagraphedContentType('paragraphed_test', 'field_paragraphs');
+
+ $this->loginAsAdmin(['create paragraphed_test content', 'edit any paragraphed_test content']);
+ // Add nested Paragraph type.
+ $nested_paragraph_type = 'nested_paragraph';
+ $this->addParagraphsType($nested_paragraph_type);
+ // Add text Paragraph type
+ $paragraph_type = 'text';
+ $this->addParagraphsType($paragraph_type);
+
+ // Add a text field to the text_paragraph type.
+ static::fieldUIAddNewField('admin/structure/paragraphs_type/' . $paragraph_type, 'text', 'Text', 'text_long', [], []);
+
+ // Add a ERR paragraph field to the nested_paragraph type.
+ static::fieldUIAddNewField('admin/structure/paragraphs_type/' . $nested_paragraph_type, 'nested', 'Nested', 'field_ui:entity_reference_revisions:paragraph', [
+ 'settings[target_type]' => 'paragraph',
+ 'cardinality' => '-1',
+ ], []);
+ $this->drupalPostAjaxForm('node/add/paragraphed_test', [], 'field_paragraphs_nested_paragraph_add_more');
+
+ // Create a node with a Paragraph.
+ $edit = [
+ 'title[0][value]' => 'paragraphs_mode_test',
+ ];
+ $this->drupalPostForm(NULL, $edit, t('Save and publish'));
+ $node = $this->drupalGetNodeByTitle('paragraphs_mode_test');
+
+ // Add a text field to nested paragraph
+ $text = 'recognizable_text';
+ $this->drupalPostAjaxForm('node/' . $node->id() . '/edit', [], 'field_paragraphs_0_subform_field_nested_text_add_more');
+ $edit = [
+ 'field_paragraphs[0][subform][field_nested][0][subform][field_text][0][value]' => $text,
+ ];
+ $this->drupalPostForm(NULL, $edit, 'Save and keep published');
+
+ // Switch mode to closed
+ $this->setParagraphsWidgetMode('paragraphed_test', 'field_paragraphs', 'closed');
+ $this->drupalGet('node/' . $node->id() . '/edit');
+
+ // Click "Duplicate" button.
+ $this->drupalPostAjaxForm(NULL, [], 'field_paragraphs_0_duplicate');
+ $this->drupalPostAjaxForm(NULL, [], 'field_paragraphs_0_edit');
+ $this->assertFieldByName('field_paragraphs[0][subform][field_nested][0][subform][field_text][0][value]', $text);
+
+ // Change the text paragraph value of duplicated nested paragraph
+ $second_paragraph_text = 'duplicated_text';
+ $edit = [
+ 'field_paragraphs[1][subform][field_nested][0][subform][field_text][0][value]' => $second_paragraph_text,
+ ];
+
+ // Save and check if the changed text paragraph value of the duplicated
+ // paragraph is not the same as in the original paragraph
+ $this->drupalPostForm(NULL, $edit, t('Save and keep published'));
+ $this->assertUniqueText($text);
+ }
+
+ /**
* Sets the Paragraphs widget display mode.
*
* @param string $content_type