Skip to content
Snippets Groups Projects
Unverified Commit 9d1b51a9 authored by Volker Killesreiter's avatar Volker Killesreiter Committed by GitHub
Browse files

Issue #2908496 Paragraphs paste feature

parent 1503bbf9
Branches
Tags
No related merge requests found
.nvmrc 0 → 100644
v16
......@@ -42,6 +42,7 @@ module.exports = {
},
'/developer-guide/testing.md',
'/developer-guide/headless.md',
'/developer-guide/paragraphs_paste.md',
{
text: 'Migration',
children: [
......
......@@ -282,6 +282,7 @@ ## Documentation
```bash
git clone git@github.com:thunder/thunder-distribution.git
cd thunder-distribution
nvm use
npm install
npm run docs:dev
```
......
# Paragraphs Paste
Paragraph paste is a module that enables editors to use copy and paste to create multiple paragraphs from pre-written
content.
To accommodate various paragraph types available in a project, it is possible to map different types of data to their
relevant paragraph types by changing the configuration on the form display settings for paragraphs fields, f.e.
`admin/structure/types/manage/article/form-display`. Set a property path in the pattern of
`{entity_type}.{bundle}.{field_name}` or
`{entity_type}.{bundle}.{entity_reference_field_name}:{referenced_entity_bundle}.{field_name}` for the plugin to map to
the paragraphs source field.
To support inserting content into paragraph types containing multiple fields, it is possible to create custom paragraphs
paste plugins. Implement the `ParagraphsPastePluginInterface` by extending the `ParagraphsPastePluginBase`class.
First provide an `isApplicable()` method to determine for which type of data the plugin should, usually this would
consist of grepping for some kind of keyword on the passed data. Next, implement the `createParagraphEntity()` method
to create the desired paragraph entity and fill its fields with a processed value from the passed data.
Here is an example for a paragraph type with additional headline fields.
```php
<?php
namespace Drupal\custom\Plugin\ParagraphsPastePlugin;
use Drupal\Core\Field\FieldDefinitionInterface;
use Drupal\paragraphs_paste\Plugin\ParagraphsPastePlugin\Textile;
/**
* Defines a paragraphs_paste plugin.
*
* @ParagraphsPastePlugin(
* id = "custom_text",
* label = @Translation("Custom text"),
* module = "custom",
* weight = 0,
* allowed_field_types = {"text", "text_long", "text_with_summary", "string",
* "string_long"}
* )
*/
class CustomText extends Textile {
/**
* {@inheritdoc}
*/
public static function isApplicable($input, array $definition) {
return (bool) preg_match('~^InsertCustomText~', trim($input));
}
/**
* {@inheritdoc}
*/
public function createParagraphEntity($input) {
$property_path = explode('.', $this->configuration['property_path']);
$target_entity_type = array_shift($property_path);
$target_bundle = array_shift($property_path);
$entity_type = $this->entityTypeManager->getDefinition($target_entity_type);
$paragraph_entity = $this->entityTypeManager->getStorage($target_entity_type)
->create([
$entity_type->getKey('bundle') => $target_bundle,
]);
$lines = preg_split('~((\r?\n){2,})~', $input);
$out = [];
foreach ($lines as $line) {
if (preg_match('~^InsertCustomText.*~', $line)) {
$this->setFieldValue($paragraph_entity, ['field_headline'], substr($line, 18));
}
elseif (preg_match('~^[hH][23]~', $line)) {
// Move first headline to dedicated fields.
if (empty($out)) {
$this->setFieldValue($paragraph_entity, ['field_another_headline'], substr($line, 3));
}
else {
$out[] = $line;
}
}
else {
$out[] = $line;
}
}
$this->setFieldValue($paragraph_entity, $property_path, $this->parseTextileInput(implode("\n\n", $out)));
return $paragraph_entity;
}
}
```
name: 'Thunder Paragraphs Paste'
description: 'Add paste area to the article content type to easily import text from other sources.'
type: module
core_version_requirement: ^9
package: Thunder Optional
dependencies:
- paragraphs_paste:paragraphs_paste
lifecycle: experimental
<?php
/**
* @file
* Thunder Paragraphs Paste install hooks.
*/
/**
* Implements hook_install().
*/
function thunder_paragraphs_paste_install(): void {
try {
/** @var \Drupal\Core\Entity\Display\EntityFormDisplayInterface $form_display */
$form_display = Drupal::service('entity_display.repository')
->getFormDisplay('node', 'article', 'default');
$config = $form_display->get('content');
$config['field_paragraphs']['third_party_settings']['paragraphs_paste'] = [
'enabled' => TRUE,
'property_path_mapping' => [
'oembed_url:video' => 'paragraph.video.field_video:video.field_media_video_embed_field',
'oembed_url:twitter' => 'paragraph.twitter.field_media:twitter.field_url',
'text' => 'paragraph.text.field_text',
],
'custom_split_method' => FALSE,
'custom_split_method_regex' => '',
];
$form_display->set('content', $config)
->save();
}
catch (Exception $e) {
\Drupal::logger('thunder')->info(t('Could not enable paragraphs_paste on article node: "@message"', ['@message' => $e->getMessage()]));
}
}
/* hook_update_n implementations should be in the profile instead of this
submodule. */
This diff is collapsed.
......@@ -22,7 +22,6 @@
"request-promise": "^4.2.6",
"vuepress": "^2.0.0-beta.33"
},
"dependencies": {},
"scripts": {
"docs:dev": "vuepress dev docs",
"docs:build": "vuepress build docs"
......
......@@ -16,6 +16,7 @@
use Drupal\user\PermissionHandlerInterface;
use Drupal\Component\Utility\Environment;
use Symfony\Component\DependencyInjection\ContainerInterface;
use Drupal\Core\Extension\ExtensionLifecycle;
/**
* Provides the site configuration form.
......@@ -211,6 +212,13 @@ public function buildForm(array $form, FormStateInterface $form_state): array {
'#context' => ['list_style' => 'comma-list'],
];
$form['install_modules'][$id]['info']['lifecycle'] = [
'#prefix' => '<div class="admin-requirements">',
'#suffix' => '</div>',
'#type' => 'item',
'#markup' => $this->t('Lifecycle status: @lifecycle', ['@lifecycle' => $module->info[ExtensionLifecycle::LIFECYCLE_IDENTIFIER]]),
];
if ($module->status) {
// Generate link for module's help page. Assume that if a hook_help()
......
<?php
namespace Drupal\Tests\thunder\FunctionalJavascript\Integration;
use Drupal\Core\Entity\Entity\EntityFormDisplay;
use Drupal\field\Entity\FieldConfig;
use Drupal\field\Entity\FieldStorageConfig;
use Drupal\Tests\node\Traits\NodeCreationTrait;
use Drupal\Tests\paragraphs\FunctionalJavascript\LoginAdminTrait;
use Drupal\Tests\paragraphs_paste\FunctionalJavascript\ParagraphsPasteJavascriptTestTrait;
use Drupal\Tests\thunder\FunctionalJavascript\ThunderArticleTestTrait;
use Drupal\Tests\thunder\FunctionalJavascript\ThunderJavascriptTestBase;
use Drupal\Tests\thunder\FunctionalJavascript\ThunderParagraphsTestTrait;
/**
* Tests the creation of paragraphs by pasting random data.
*
* @group Thunder
*/
class ParagraphsPasteTest extends ThunderJavascriptTestBase {
use LoginAdminTrait;
use ParagraphsPasteJavascriptTestTrait;
use ThunderParagraphsTestTrait;
use ThunderArticleTestTrait;
use NodeCreationTrait;
/**
* {@inheritdoc}
*/
protected static $modules = [
'thunder_paragraphs_paste',
'thunder_testing_demo',
'thunder_workflow',
'thunder_test_mock_request',
];
/**
* Field name for paragraphs in article content.
*
* @var string
*/
protected static $paragraphsField = 'field_paragraphs';
/**
* Test paste functionality.
*/
public function testPaste(): void {
$paragraphsField = static::$paragraphsField;
$text = 'Lorem ipsum dolor sit amet.';
$this->drupalGet("node/add/article");
usleep(50000);
$this->assertTrue($this->getSession()->getDriver()->isVisible('//*[@data-paragraphs-paste-target="field_paragraphs"]'), 'Paragraphs Paste should be visible.');
$this->scrollElementInView("[data-paragraphs-paste-target=\"" . static::$paragraphsField . "\"]");
$this->simulatePasteEvent($paragraphsField, $text);
$this->waitForElementPresent('[data-drupal-selector="edit-field-paragraphs-0-subform-field-text-0-value"]', 10000, 'Text field in paragraph form should be present.');
$this->assertEquals(sprintf('<p>%s</p>', $text), $this->getSession()->getPage()->find('xpath', '//textarea[@data-drupal-selector="edit-field-paragraphs-0-subform-field-text-0-value"]')->getValue(), 'Text should be pasted into paragraph subform.');
}
/**
* Test multiline text with video functionality.
*/
public function testMultilineTextPaste(): void {
$text = [
'Spicy jalapeno bacon ipsum dolor amet short ribs ribeye chislic, turkey shank chuck cupim bacon bresaola.',
'https://www.youtube.com/watch?v=PWjcqE3QKBg',
'Picanha porchetta cupim, salami jerky alcatra doner strip steak pork loin short loin pork belly tail ham hock cow shoulder.',
];
$text = implode('\n\n\n', $text);
$this->drupalGet("node/add/article");
usleep(50000);
$this->assertTrue($this->getSession()->getDriver()->isVisible('//*[@data-paragraphs-paste-target="field_paragraphs"]'), 'Paragraphs Paste should be visible.');
$this->scrollElementInView("[data-paragraphs-paste-target=\"" . static::$paragraphsField . "\"]");
$this->simulatePasteEvent(static::$paragraphsField, $text);
$this->waitForElementPresent('[data-drupal-selector="edit-field-paragraphs-0-subform-field-text-0-value"]', 10000, 'Text field in paragraph form should be present.');
$this->assertEquals(sprintf('<p>%s</p>', "Spicy jalapeno bacon ipsum dolor amet short ribs ribeye chislic, turkey shank chuck cupim bacon bresaola."), $this->getSession()->getPage()->find('xpath', '//textarea[@data-drupal-selector="edit-field-paragraphs-0-subform-field-text-0-value"]')->getValue(), 'Text should be pasted into paragraph subform.');
$this->assertEquals("media:20", $this->getSession()->getPage()->find('xpath', '//input[@name="field_paragraphs[1][subform][field_video][target_id]"]')->getValue(), 'Video should be connected to the paragraph subform.');
$this->assertEquals(sprintf('<p>%s</p>', "Picanha porchetta cupim, salami jerky alcatra doner strip steak pork loin short loin pork belly tail ham hock cow shoulder."), $this->getSession()->getPage()->find('xpath', '//textarea[@data-drupal-selector="edit-field-paragraphs-2-subform-field-text-0-value"]')->getValue(), 'Text should be pasted into paragraph subform.');
}
/**
* Verify that the paste area stays after a first paste.
*/
public function testPastingTwice(): void {
$this->testPaste();
$text = 'Bacon ipsum dolor amet cow picanha andouille strip steak tongue..';
// Wait for scrollHeight to update.
sleep(1);
$this->scrollElementInView("[data-paragraphs-paste-target=\"" . static::$paragraphsField . "\"]");
$this->simulatePasteEvent(static::$paragraphsField, $text);
$this->waitForElementPresent('[data-drupal-selector="edit-field-paragraphs-1-subform-field-text-0-value"]', 10000, 'Text field in paragraph form should be present.');
$this->assertEquals(sprintf('<p>%s</p>', $text), $this->getSession()->getPage()->find('xpath', '//textarea[@data-drupal-selector="edit-field-paragraphs-1-subform-field-text-0-value"]')->getValue(), 'Text should be pasted into paragraph subform.');
}
/**
* Test paste functionality with two paste areas in the form.
*/
public function testPastingInTwoAreas(): void {
$content_type = 'article';
$field_name = 'field_second_paragraphs';
$field_storage = FieldStorageConfig::create([
'field_name' => $field_name,
'entity_type' => 'node',
'type' => 'entity_reference_revisions',
'cardinality' => '-1',
'settings' => ['target_type' => 'paragraph'],
]);
$field_storage->save();
$field = FieldConfig::create([
'field_storage' => $field_storage,
'bundle' => $content_type,
'settings' => [
'handler' => 'default:paragraph',
'handler_settings' => ['target_bundles' => NULL],
],
]);
$field->save();
/** @var \Drupal\Core\Entity\Display\EntityFormDisplayInterface $display */
$display = EntityFormDisplay::load("node.$content_type.default");
$display->setComponent($field_name, $display->getComponent('field_paragraphs'));
$display->save();
// Check that paste functionality is working with default config.
$this->drupalGet("node/add/$content_type");
$this->assertTrue($this->getSession()->getDriver()->isVisible('//*[@data-paragraphs-paste-target="field_paragraphs"]'), 'Paragraphs Paste area should be visible.');
$this->assertTrue($this->getSession()->getDriver()->isVisible('//*[@data-paragraphs-paste-target="field_second_paragraphs"]'), 'Second Paragraphs Paste area should be visible.');
$text = 'Lorem ipsum dolor sit amet.';
$this->scrollElementInView("[data-paragraphs-paste-target=\"" . static::$paragraphsField . "\"]");
$this->simulatePasteEvent(static::$paragraphsField, $text);
$this->waitForElementPresent('[data-drupal-selector="edit-field-paragraphs-0-subform-field-text-0-value"]', 10000, 'Text field in paragraph form should be present.');
$this->assertEquals(sprintf('<p>%s</p>', $text), $this->getSession()->getPage()->find('xpath', '//textarea[@data-drupal-selector="edit-field-paragraphs-0-subform-field-text-0-value"]')->getValue(), 'Text should be pasted into paragraph subform.');
$text = 'Bacon ipsum dolor amet cow picanha andouille strip steak tongue..';
// Wait for scrollHeight to update.
sleep(1);
$this->scrollElementInView("[data-paragraphs-paste-target=\"{$field_name}\"]");
$this->simulatePasteEvent($field_name, $text);
$this->waitForElementPresent('[data-drupal-selector="edit-field-second-paragraphs-0-subform-field-text-0-value"]', 10000, 'Text field in second paragraph form should be present.');
$this->assertEquals(sprintf('<p>%s</p>', $text), $this->getSession()->getPage()->find('xpath', '//textarea[@data-drupal-selector="edit-field-second-paragraphs-0-subform-field-text-0-value"]')->getValue(), 'Text should be pasted into the second paragraph subform.');
}
}
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Please register or to comment