├── .gitignore ├── config ├── install │ └── entity_embed.settings.yml ├── optional │ └── embed.button.node.yml └── schema │ └── entity_embed.schema.yml ├── js ├── plugins │ └── drupalentity │ │ ├── entity.png │ │ └── plugin.js └── entity_embed.dialog.js ├── DEVELOPING.md ├── tests ├── modules │ └── entity_embed_test │ │ ├── entity_embed_test.info.yml │ │ ├── templates │ │ └── entity_embed_twig_test.html.twig │ │ ├── entity_embed_test.routing.yml │ │ ├── src │ │ └── EntityEmbedTestTwigController.php │ │ └── entity_embed_test.module └── fixtures │ └── update │ └── entity_embed.update-hook-test.php ├── src ├── Exception │ ├── EntityNotFoundException.php │ └── RecursiveRenderingException.php ├── EntityEmbedBuilderInterface.php ├── Plugin │ ├── entity_embed │ │ └── EntityEmbedDisplay │ │ │ ├── EntityReferenceFieldFormatter.php │ │ │ ├── FileFieldFormatter.php │ │ │ ├── ViewModeFieldFormatter.php │ │ │ └── ImageFieldFormatter.php │ ├── CKEditorPlugin │ │ └── DrupalEntity.php │ ├── Derivative │ │ ├── ViewModeDeriver.php │ │ └── FieldFormatterDeriver.php │ ├── Filter │ │ └── EntityEmbedFilter.php │ └── EmbedType │ │ └── Entity.php ├── Annotation │ └── EntityEmbedDisplay.php ├── Tests │ ├── ViewModeFieldFormatterTest.php │ ├── EntityEmbedTwigTest.php │ ├── EntityEmbedEntityBrowserTest.php │ ├── EntityEmbedUpdateHookTest.php │ ├── FileFieldFormatterTest.php │ ├── EntityEmbedTestBase.php │ ├── EntityEmbedHooksTest.php │ ├── ImageFieldFormatterTest.php │ ├── EntityEmbedDialogTest.php │ ├── EntityReferenceFieldFormatterTest.php │ └── EntityEmbedFilterTest.php ├── Twig │ └── EntityEmbedTwigExtension.php ├── EntityEmbedDisplay │ ├── EntityEmbedDisplayInterface.php │ ├── EntityEmbedDisplayManager.php │ ├── FieldFormatterEntityEmbedDisplayBase.php │ └── EntityEmbedDisplayBase.php ├── EntityEmbedBuilder.php └── Form │ └── EntityEmbedDialog.php ├── entity_embed.libraries.yml ├── entity_embed.info.yml ├── entity_embed.routing.yml ├── .travis-before-script.sh ├── composer.json ├── templates └── entity-embed-container.html.twig ├── entity_embed.services.yml ├── css └── entity_embed.dialog.css ├── entity_embed.module ├── entity_embed.install ├── .travis.yml ├── entity_embed.api.php └── README.md /.gitignore: -------------------------------------------------------------------------------- 1 | vendor 2 | composer.lock 3 | -------------------------------------------------------------------------------- /config/install/entity_embed.settings.yml: -------------------------------------------------------------------------------- 1 | rendered_entity_mode: false 2 | -------------------------------------------------------------------------------- /js/plugins/drupalentity/entity.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/drupal-media/entity_embed/HEAD/js/plugins/drupalentity/entity.png -------------------------------------------------------------------------------- /DEVELOPING.md: -------------------------------------------------------------------------------- 1 | # Developing 2 | 3 | * Issues should be filed at http://drupal.org/project/issues/entity_embed 4 | * Pull requests can be made against https://github.com/drupal-media/entity_embed/pulls 5 | -------------------------------------------------------------------------------- /tests/modules/entity_embed_test/entity_embed_test.info.yml: -------------------------------------------------------------------------------- 1 | name: 'Entity Embed test' 2 | type: module 3 | description: 'Support module for the Entity Embed module tests.' 4 | core: 8.x 5 | package: Testing 6 | version: VERSION 7 | -------------------------------------------------------------------------------- /src/Exception/EntityNotFoundException.php: -------------------------------------------------------------------------------- 1 | {{ children }} 16 | -------------------------------------------------------------------------------- /tests/modules/entity_embed_test/templates/entity_embed_twig_test.html.twig: -------------------------------------------------------------------------------- 1 | {# 2 | /** 3 | * Template for testing entity_embed twig extension. 4 | * 5 | * Available variables: 6 | * - entity_type: Machine name of the entity type. 7 | * - id: ID or UUID of the entity to be embedded. 8 | * - display_plugin: Machine name of the Entity Embed Display plugin. 9 | * - display_settings: An array of settings to be passed to the selected Entity 10 | * Embed Display plugin. 11 | #} 12 | {{ entity_embed(entity_type, id, display_plugin, display_settings) }} 13 | -------------------------------------------------------------------------------- /entity_embed.services.yml: -------------------------------------------------------------------------------- 1 | services: 2 | plugin.manager.entity_embed.display: 3 | class: Drupal\entity_embed\EntityEmbedDisplay\EntityEmbedDisplayManager 4 | arguments: ['@container.namespaces', '@cache.discovery', '@module_handler'] 5 | entity_embed.twig.entity_embed_twig_extension: 6 | class: Drupal\entity_embed\Twig\EntityEmbedTwigExtension 7 | arguments: ['@entity.manager', '@entity_embed.builder'] 8 | tags: 9 | - { name: twig.extension } 10 | entity_embed.builder: 11 | class: Drupal\entity_embed\EntityEmbedBuilder 12 | arguments: ['@module_handler', '@plugin.manager.entity_embed.display'] 13 | -------------------------------------------------------------------------------- /css/entity_embed.dialog.css: -------------------------------------------------------------------------------- 1 | /** 2 | * @file 3 | * Styles for Entity Browser's modal windows. 4 | */ 5 | 6 | .entity-embed-dialog iframe { 7 | border: none; 8 | } 9 | 10 | .ui-dialog--narrow.entity-select-dialog { 11 | max-width: 1200px; 12 | } 13 | 14 | @media screen and (min-width: 768px) { 15 | .ui-dialog--narrow.entity-select-dialog .entity-embed-dialog-step--select { 16 | min-width: 730px; // 95% 17 | } 18 | } 19 | 20 | @media screen and (min-width: 1000px) { 21 | .ui-dialog--narrow.entity-select-dialog .entity-embed-dialog-step--select { 22 | min-width: 950px; // 95% 23 | } 24 | } 25 | 26 | @media screen and (min-width: 1200px) { 27 | .ui-dialog--narrow.entity-select-dialog .entity-embed-dialog-step--select { 28 | min-width: 1140px; // 95% 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /src/EntityEmbedBuilderInterface.php: -------------------------------------------------------------------------------- 1 | fieldDefinition)) { 26 | $this->fieldDefinition = parent::getFieldDefinition(); 27 | $this->fieldDefinition->setSetting('target_type', $this->getEntityTypeFromContext()); 28 | } 29 | return $this->fieldDefinition; 30 | } 31 | 32 | /** 33 | * {@inheritdoc} 34 | */ 35 | public function getFieldValue() { 36 | return array('target_id' => $this->getContextValue('entity')->id()); 37 | } 38 | 39 | } 40 | -------------------------------------------------------------------------------- /src/Plugin/CKEditorPlugin/DrupalEntity.php: -------------------------------------------------------------------------------- 1 | getTypeSetting('entity_type'); 26 | return $button; 27 | } 28 | 29 | /** 30 | * {@inheritdoc} 31 | */ 32 | public function getFile() { 33 | return drupal_get_path('module', 'entity_embed') . '/js/plugins/drupalentity/plugin.js'; 34 | } 35 | 36 | /** 37 | * {@inheritdoc} 38 | */ 39 | public function getConfig(Editor $editor) { 40 | return array( 41 | 'DrupalEntity_dialogTitleAdd' => t('Insert entity'), 42 | 'DrupalEntity_dialogTitleEdit' => t('Edit entity'), 43 | 'DrupalEntity_buttons' => $this->getButtons(), 44 | ); 45 | } 46 | 47 | } 48 | -------------------------------------------------------------------------------- /tests/modules/entity_embed_test/src/EntityEmbedTestTwigController.php: -------------------------------------------------------------------------------- 1 | 'entity_embed_twig_test', 16 | '#entity_type' => 'node', 17 | '#id' => '1', 18 | ); 19 | } 20 | 21 | /** 22 | * Menu callback for testing entity_embed twig extension using 'label' Entity Embed Display plugin. 23 | */ 24 | public function labelPluginRender() { 25 | return array( 26 | '#theme' => 'entity_embed_twig_test', 27 | '#entity_type' => 'node', 28 | '#id' => '1', 29 | '#display_plugin' => 'entity_reference:entity_reference_label', 30 | ); 31 | } 32 | 33 | /** 34 | * Menu callback for testing entity_embed twig extension using 'label' Entity Embed Display plugin without linking to the node. 35 | */ 36 | public function labelPluginNoLinkRender() { 37 | return array( 38 | '#theme' => 'entity_embed_twig_test', 39 | '#entity_type' => 'node', 40 | '#id' => '1', 41 | '#display_plugin' => 'entity_reference:entity_reference_label', 42 | '#display_settings' => array('link' => 0), 43 | ); 44 | } 45 | 46 | } 47 | -------------------------------------------------------------------------------- /tests/fixtures/update/entity_embed.update-hook-test.php: -------------------------------------------------------------------------------- 1 | merge('key_value') 14 | ->condition('collection', 'system.schema') 15 | ->condition('name', 'entity_embed') 16 | ->fields([ 17 | 'collection' => 'system.schema', 18 | 'name' => 'entity_embed', 19 | 'value' => 's:4:"8001";', 20 | ]) 21 | ->execute(); 22 | 23 | $config = Yaml::decode(file_get_contents(__DIR__ . '/../../../config/optional/embed.button.node.yml')); 24 | $connection->insert('config') 25 | ->fields([ 26 | 'collection', 27 | 'name', 28 | 'data', 29 | ]) 30 | ->values([ 31 | 'collection' => '', 32 | 'name' => 'embed.button.node', 33 | 'data' => serialize($config), 34 | ]) 35 | ->execute(); 36 | 37 | // Update core.extension. 38 | $extensions = $connection->select('config') 39 | ->fields('config', ['data']) 40 | ->condition('collection', '') 41 | ->condition('name', 'core.extension') 42 | ->execute() 43 | ->fetchField(); 44 | $extensions = unserialize($extensions); 45 | $extensions['module']['embed'] = 8000; 46 | $extensions['module']['entity_embed'] = 8001; 47 | $extensions['module']['embed'] = 8000; 48 | $connection->update('config') 49 | ->fields([ 50 | 'data' => serialize($extensions), 51 | ]) 52 | ->condition('collection', '') 53 | ->condition('name', 'core.extension') 54 | ->execute(); 55 | -------------------------------------------------------------------------------- /src/Annotation/EntityEmbedDisplay.php: -------------------------------------------------------------------------------- 1 | getConfiguration(), array('description' => '')); 29 | return $value; 30 | } 31 | 32 | /** 33 | * {@inheritdoc} 34 | */ 35 | public function defaultConfiguration() { 36 | $defaults = parent::defaultConfiguration(); 37 | // Add support to store file description. 38 | $defaults['description'] = ''; 39 | return $defaults; 40 | } 41 | 42 | /** 43 | * {@inheritdoc} 44 | */ 45 | public function buildConfigurationForm(array $form, FormStateInterface $form_state) { 46 | $form = parent::buildConfigurationForm($form, $form_state); 47 | 48 | // Description is stored in the configuration since it doesn't map to an 49 | // actual HTML attribute. 50 | $form['description'] = array( 51 | '#type' => 'textfield', 52 | '#title' => $this->t('Description'), 53 | '#default_value' => $this->getConfigurationValue('description'), 54 | '#description' => $this->t('The description may be used as the label of the link to the file.'), 55 | ); 56 | 57 | return $form; 58 | } 59 | 60 | } 61 | -------------------------------------------------------------------------------- /src/Plugin/entity_embed/EntityEmbedDisplay/ViewModeFieldFormatter.php: -------------------------------------------------------------------------------- 1 | fieldFormatter)) { 26 | $display = [ 27 | 'type' => $this->getFieldFormatterId(), 28 | 'settings' => [ 29 | 'view_mode' => $this->getPluginDefinition()['view_mode'], 30 | ], 31 | 'label' => 'hidden', 32 | ]; 33 | 34 | // Create the formatter plugin. Will use the default formatter for that 35 | // field type if none is passed. 36 | $this->fieldFormatter = $this->formatterPluginManager->getInstance( 37 | [ 38 | 'field_definition' => $this->getFieldDefinition(), 39 | 'view_mode' => '_entity_embed', 40 | 'configuration' => $display, 41 | ] 42 | ); 43 | } 44 | return $this->fieldFormatter; 45 | } 46 | 47 | /** 48 | * {@inheritdoc} 49 | */ 50 | public function buildConfigurationForm(array $form, FormStateInterface $form_state) { 51 | // Configuration form is not needed as the view mode is defined implicitly. 52 | return []; 53 | } 54 | 55 | /** 56 | * {@inheritdoc} 57 | */ 58 | public function getFieldFormatterId() { 59 | return 'entity_reference_entity_view'; 60 | } 61 | 62 | } 63 | -------------------------------------------------------------------------------- /js/entity_embed.dialog.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @file 3 | * Provides JavaScript additions to entity embed dialog. 4 | * 5 | * This file provides popup windows for previewing embedded entities from the 6 | * embed dialog. 7 | */ 8 | 9 | (function ($, Drupal) { 10 | 11 | "use strict"; 12 | 13 | /** 14 | * Attach behaviors to links for entities. 15 | */ 16 | Drupal.behaviors.entityEmbedPreviewEntities = { 17 | attach: function (context) { 18 | $(context).find('form.entity-embed-dialog .form-item-entity a').on('click', Drupal.entityEmbedDialog.openInNewWindow); 19 | }, 20 | detach: function (context) { 21 | $(context).find('form.entity-embed-dialog .form-item-entity a').off('click', Drupal.entityEmbedDialog.openInNewWindow); 22 | } 23 | }; 24 | 25 | /** 26 | * Behaviors for the entityEmbedDialog iframe. 27 | */ 28 | Drupal.behaviors.entityEmbedDialog = { 29 | attach: function (context, settings) { 30 | $('body').once('js-entity-embed-dialog').on('entityBrowserIFrameAppend', function () { 31 | $('.entity-select-dialog').trigger('resize'); 32 | // Hide the next button, the click is triggered by Drupal.entityEmbedDialog.selectionCompleted. 33 | $('#drupal-modal').parent().find('.js-button-next').addClass('visually-hidden'); 34 | }); 35 | } 36 | }; 37 | 38 | /** 39 | * Entity Embed dialog utility functions. 40 | */ 41 | Drupal.entityEmbedDialog = Drupal.entityEmbedDialog || { 42 | /** 43 | * Open links to entities within forms in a new window. 44 | */ 45 | openInNewWindow: function (event) { 46 | event.preventDefault(); 47 | $(this).attr('target', '_blank'); 48 | window.open(this.href, 'entityPreview', 'toolbar=0,scrollbars=1,location=1,statusbar=1,menubar=0,resizable=1'); 49 | }, 50 | selectionCompleted: function(event, uuid, entities) { 51 | $('.entity-select-dialog .js-button-next').click(); 52 | } 53 | }; 54 | 55 | })(jQuery, Drupal); 56 | -------------------------------------------------------------------------------- /src/Tests/ViewModeFieldFormatterTest.php: -------------------------------------------------------------------------------- 1 | plugins as $plugin) { 28 | $form = []; 29 | $form_state = new FormState(); 30 | $display = $this->container->get('plugin.manager.entity_embed.display') 31 | ->createInstance($plugin, []); 32 | $display->setContextValue('entity', $this->node); 33 | $conf_form = $display->buildConfigurationForm($form, $form_state); 34 | $this->assertIdentical(array_keys($conf_form), []); 35 | } 36 | } 37 | 38 | /** 39 | * Tests filter using view mode entity embed display plugins. 40 | */ 41 | public function testFilterViewModePlugins() { 42 | foreach ($this->plugins as $plugin) { 43 | $content = ''; 44 | $settings = []; 45 | $settings['type'] = 'page'; 46 | $settings['title'] = 'Test ' . $plugin . ' Entity Embed Display plugin'; 47 | $settings['body'] = [['value' => $content, 'format' => 'custom_format']]; 48 | $node = $this->drupalCreateNode($settings); 49 | $this->drupalGet('node/' . $node->id()); 50 | $plugin = explode('.', $plugin); 51 | $view_mode = str_replace('_', '-', end($plugin)); 52 | $this->assertRaw('node--view-mode-' . $view_mode, 'Node rendered in the correct view mode: ' . $view_mode . '.'); 53 | } 54 | } 55 | 56 | } 57 | -------------------------------------------------------------------------------- /entity_embed.module: -------------------------------------------------------------------------------- 1 | [ 15 | 'render element' => 'element', 16 | ], 17 | ]; 18 | } 19 | 20 | /** 21 | * Prepares variables for entity embed container templates. 22 | * 23 | * Default template: entity-embed-container.html.twig. 24 | * 25 | * @param array $variables 26 | * An associative array containing: 27 | * - element: An associative array containing the properties of the element. 28 | * Properties used: #attributes, #children. 29 | */ 30 | function template_preprocess_entity_embed_container(&$variables) { 31 | $variables['element'] += ['#attributes' => []]; 32 | $variables['attributes'] = $variables['element']['#attributes']; 33 | $variables['children'] = $variables['element']['#children']; 34 | } 35 | 36 | /** 37 | * Implements hook_entity_embed_display_plugins_alter() on behalf of file.module. 38 | */ 39 | function file_entity_embed_display_plugins_alter(array &$plugins) { 40 | // The RSS enclosure field formatter is not usable for Entity Embed. 41 | unset($plugins['file:file_rss_enclosure']); 42 | } 43 | 44 | /** 45 | * Implements hook_entity_embed_display_plugins_alter() on behalf of taxonomy.module. 46 | */ 47 | function taxonomy_entity_embed_display_plugins_alter(array &$plugins) { 48 | // The RSS category field formatter is not usable for Entity Embed. 49 | unset($plugins['entity_reference:entity_reference_rss_category']); 50 | } 51 | 52 | /** 53 | * Implements hook_entity_embed_display_plugins_for_context_alter(). 54 | * 55 | * The 'Rendered entity' formatter can not be used for files unless the 56 | * file_entity module is available. 57 | * 58 | * @see https://www.drupal.org/node/2468387 59 | * 60 | * @todo Remove when https://www.drupal.org/node/2567919 is fixed in core. 61 | */ 62 | function entity_embed_entity_embed_display_plugins_for_context_alter(array &$definitions, array $context) { 63 | if ($context['entity_type'] === 'file' && !\Drupal::moduleHandler()->moduleExists('file_entity')) { 64 | unset($definitions['entity_reference:entity_reference_entity_view']); 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /src/Tests/EntityEmbedTwigTest.php: -------------------------------------------------------------------------------- 1 | install(array('test_theme')); 18 | } 19 | 20 | /** 21 | * Tests that the provided Twig extension loads the service appropriately. 22 | */ 23 | public function testTwigExtensionLoaded() { 24 | $twig_service = \Drupal::service('twig'); 25 | 26 | $ext = $twig_service->getExtension('entity_embed.twig.entity_embed_twig_extension'); 27 | 28 | // @todo why is the string 29 | // 'Drupal\\entity_embed\\Twig\\EntityEmbedTwigExtension' 30 | // and not '\Drupal\entity_embed\Twig\EntityEmbedTwigExtension' ? 31 | $this->assertEqual(get_class($ext), 'Drupal\\entity_embed\\Twig\\EntityEmbedTwigExtension', 'Extension loaded successfully.'); 32 | } 33 | 34 | /** 35 | * Tests that the Twig extension's filter produces expected output. 36 | */ 37 | public function testEntityEmbedTwigFunction() { 38 | // Test embedding a node using entity ID. 39 | $this->drupalGet('entity_embed_twig_test/id'); 40 | $this->assertText($this->node->body->value, 'Embedded node exists in page'); 41 | 42 | // Test 'Label' Entity Embed Display plugin. 43 | $this->drupalGet('entity_embed_twig_test/label_plugin'); 44 | $this->assertText($this->node->title->value, 'Title of the embedded node exists in page.'); 45 | $this->assertNoText($this->node->body->value, 'Body of embedded node does not exists in page when "Label" plugin is used.'); 46 | $this->assertLinkByHref('node/' . $this->node->id(), 0, 'Link to the embedded node exists when "Label" plugin is used.'); 47 | 48 | // Test 'Label' Entity Embed Display plugin without linking to the node. 49 | $this->drupalGet('entity_embed_twig_test/label_plugin_no_link'); 50 | $this->assertText($this->node->title->value, 'Title of the embedded node exists in page.'); 51 | $this->assertNoText($this->node->body->value, 'Body of embedded node does not exists in page when "Label" plugin is used.'); 52 | $this->assertNoLinkByHref('node/' . $this->node->id(), 0, 'Link to the embedded node does not exists.'); 53 | } 54 | 55 | } 56 | -------------------------------------------------------------------------------- /src/Tests/EntityEmbedEntityBrowserTest.php: -------------------------------------------------------------------------------- 1 | getEmbedDialog('custom_format', 'node'); 29 | $this->assertResponse(200, 'Embed dialog is accessible with custom filter format and default embed button.'); 30 | 31 | // Verify that an autocomplete field is available by default. 32 | $this->assertFieldByName('entity_id', '', 'Entity ID/UUID field is present.'); 33 | $this->assertNoText('Select entities to embed', 'Entity browser button is not present.'); 34 | 35 | // Set up entity browser. 36 | $entity_browser = EntityBrowser::create([ 37 | "name" => 'entity_embed_entity_browser_test', 38 | "label" => 'Test Entity Browser for Entity Embed', 39 | "display" => 'modal', 40 | "display_configuration" => [ 41 | 'width' => '650', 42 | 'height' => '500', 43 | 'link_text' => 'Select entities to embed', 44 | ], 45 | "selection_display" => 'no_display', 46 | "selection_display_configuration" => [], 47 | "widget_selector" => 'single', 48 | "widget_selector_configuration" => [], 49 | "widgets" => [], 50 | ]); 51 | $entity_browser->save(); 52 | 53 | // Enable entity browser for the default entity embed button. 54 | $embed_button = EmbedButton::load('node'); 55 | $embed_button->type_settings['entity_browser'] = 'entity_embed_entity_browser_test'; 56 | $embed_button->save(); 57 | 58 | $this->getEmbedDialog('custom_format', 'node'); 59 | $this->assertResponse(200, 'Embed dialog is accessible with custom filter format and default embed button.'); 60 | 61 | // Verify that the autocomplete field is replaced by an entity browser 62 | // button. 63 | $this->assertNoFieldByName('entity_id', '', 'Entity ID/UUID field is present.'); 64 | $this->assertText('Select entities to embed', 'Entity browser button is present.'); 65 | } 66 | 67 | } 68 | -------------------------------------------------------------------------------- /src/Plugin/Derivative/ViewModeDeriver.php: -------------------------------------------------------------------------------- 1 | entityDisplayRepository = $entity_display_repository; 42 | $this->configFactory = $config_factory; 43 | } 44 | 45 | /** 46 | * {@inheritdoc} 47 | */ 48 | public static function create(ContainerInterface $container, $base_plugin_id) { 49 | return new static( 50 | $container->get('entity_display.repository'), 51 | $container->get('config.factory') 52 | ); 53 | } 54 | 55 | /** 56 | * {@inheritdoc} 57 | */ 58 | public function getDerivativeDefinitions($base_plugin_definition) { 59 | $mode = $this->configFactory->get('entity_embed.settings')->get('rendered_entity_mode'); 60 | foreach ($this->entityDisplayRepository->getAllViewModes() as $view_modes) { 61 | foreach ($view_modes as $view_mode => $definition) { 62 | $this->derivatives[$definition['id']] = $base_plugin_definition; 63 | $this->derivatives[$definition['id']]['label'] = $definition['label']; 64 | $this->derivatives[$definition['id']]['view_mode'] = $view_mode; 65 | $this->derivatives[$definition['id']]['entity_types'] = $definition['targetEntityType']; 66 | $this->derivatives[$definition['id']]['no_ui'] = $mode; 67 | } 68 | } 69 | return $this->derivatives; 70 | } 71 | 72 | } 73 | -------------------------------------------------------------------------------- /tests/modules/entity_embed_test/entity_embed_test.module: -------------------------------------------------------------------------------- 1 | 'entity_embed_twig_test', 18 | 'variables' => array( 19 | 'entity_type' => '', 20 | 'id' => '', 21 | 'display_plugin' => 'default', 22 | 'display_settings' => array(), 23 | ), 24 | ); 25 | return $items; 26 | } 27 | 28 | /** 29 | * Implements hook_entity_embed_display_plugins_alter(). 30 | */ 31 | function entity_embed_test_entity_embed_display_plugins_alter(&$info) { 32 | // Allow tests to enable or disable this hook. 33 | if (!\Drupal::state()->get('entity_embed_test_entity_embed_display_plugins_alter', FALSE)) { 34 | return; 35 | } 36 | 37 | // Prefix each plugin name with 'testing_hook:'. 38 | $new_info = array(); 39 | foreach ($info as $key => $value) { 40 | $new_key = "testing_hook:" . $key; 41 | $new_info[$new_key] = $info[$key]; 42 | unset($info[$key]); 43 | } 44 | $info = $new_info; 45 | } 46 | 47 | /** 48 | * Implements hook_entity_embed_context_alter(). 49 | */ 50 | function entity_embed_test_entity_embed_context_alter(array &$context, EntityInterface $entity) { 51 | // Allow tests to enable or disable this hook. 52 | if (!\Drupal::state()->get('entity_embed_test_entity_embed_context_alter', FALSE)) { 53 | return; 54 | } 55 | 56 | // Force to use 'Label' plugin. 57 | $context['data-entity-embed-display'] = 'entity_reference:entity_reference_label'; 58 | $context['data-entity-embed-display-settings'] = array('link' => 1); 59 | 60 | // Set title of the entity. 61 | $entity->setTitle("Title set by hook_entity_embed_context_alter"); 62 | } 63 | 64 | /** 65 | * Implements hook_entity_embed_alter(). 66 | */ 67 | function entity_embed_test_entity_embed_alter(array &$build, EntityInterface $entity, array $context) { 68 | // Allow tests to enable or disable this hook. 69 | if (!\Drupal::state()->get('entity_embed_test_entity_embed_alter', FALSE)) { 70 | return; 71 | } 72 | 73 | // Set title of the 'node' entity. 74 | $entity->setTitle("Title set by hook_entity_embed_alter"); 75 | } 76 | 77 | /** 78 | * Implements hook_entity_access(). 79 | */ 80 | function entity_embed_test_entity_access(EntityInterface $entity, $operation, AccountInterface $account) { 81 | if ($entity->label() == 'Embed Test Node') { 82 | return AccessResult::neutral()->addCacheTags(['foo:' . $entity->id()]); 83 | } 84 | } 85 | -------------------------------------------------------------------------------- /src/Twig/EntityEmbedTwigExtension.php: -------------------------------------------------------------------------------- 1 | entityTypeManager = $entity_type_manager; 36 | $this->builder = $builder; 37 | } 38 | 39 | /** 40 | * {@inheritdoc} 41 | */ 42 | public static function create(ContainerInterface $container) { 43 | return new static( 44 | $container->get('entity_type.manager'), 45 | $container->get('entity_embed.builder') 46 | ); 47 | } 48 | 49 | /** 50 | * {@inheritdoc} 51 | */ 52 | public function getName() { 53 | return 'entity_embed.twig.entity_embed_twig_extension'; 54 | } 55 | 56 | /** 57 | * {@inheritdoc} 58 | */ 59 | public function getFunctions() { 60 | return array( 61 | new \Twig_SimpleFunction('entity_embed', [$this, 'getRenderArray']), 62 | ); 63 | } 64 | 65 | /** 66 | * Return the render array for an entity. 67 | * 68 | * @param string $entity_type 69 | * The machine name of an entity_type like 'node'. 70 | * @param string $entity_id 71 | * The entity ID. 72 | * @param string $display_plugin 73 | * (optional) The Entity Embed Display plugin to be used to render the 74 | * entity. 75 | * @param array $display_settings 76 | * (optional) A list of settings for the Entity Embed Display plugin. 77 | * 78 | * @return array 79 | * A render array from entity_view(). 80 | */ 81 | public function getRenderArray($entity_type, $entity_id, $display_plugin = 'default', array $display_settings = []) { 82 | $entity = $this->entityTypeManager->getStorage($entity_type)->load($entity_id); 83 | $context = [ 84 | 'data-entity-type' => $entity_type, 85 | 'data-entity-uuid' => $entity->uuid(), 86 | 'data-entity-embed-display' => $display_plugin, 87 | 'data-entity-embed-display-settings' => $display_settings, 88 | ]; 89 | return $this->builder->buildEntityEmbed($entity, $context); 90 | } 91 | 92 | } 93 | -------------------------------------------------------------------------------- /src/EntityEmbedDisplay/EntityEmbedDisplayInterface.php: -------------------------------------------------------------------------------- 1 | formatterManager = $formatter_manager; 42 | $this->configFactory = $config_factory; 43 | } 44 | 45 | /** 46 | * {@inheritdoc} 47 | */ 48 | public static function create(ContainerInterface $container, $base_plugin_id) { 49 | return new static( 50 | $container->get('plugin.manager.field.formatter'), 51 | $container->get('config.factory') 52 | ); 53 | } 54 | 55 | /** 56 | * {@inheritdoc} 57 | * 58 | * @throws \LogicException 59 | * Throws an exception if field type is not defined in the annotation of the 60 | * Entity Embed Display plugin. 61 | */ 62 | public function getDerivativeDefinitions($base_plugin_definition) { 63 | // The field type must be defined in the annotation of the Entity Embed 64 | // Display plugin. 65 | if (!isset($base_plugin_definition['field_type'])) { 66 | throw new \LogicException("Undefined field_type definition in plugin {$base_plugin_definition['id']}."); 67 | } 68 | $mode = $this->configFactory->get('entity_embed.settings')->get('rendered_entity_mode'); 69 | foreach ($this->formatterManager->getOptions($base_plugin_definition['field_type']) as $formatter => $label) { 70 | $this->derivatives[$formatter] = $base_plugin_definition; 71 | $this->derivatives[$formatter]['label'] = $label; 72 | // Don't show entity_reference_entity_view in the UI if the rendered 73 | // entity mode is FALSE. In that case we show view modes from 74 | // ViewModeDeriver, entity_reference_entity_view is kept for backwards 75 | // compatibility. 76 | if ($formatter == 'entity_reference_entity_view' && $mode == FALSE) { 77 | $this->derivatives[$formatter]['no_ui'] = TRUE; 78 | } 79 | } 80 | return $this->derivatives; 81 | } 82 | 83 | } 84 | -------------------------------------------------------------------------------- /src/Tests/EntityEmbedUpdateHookTest.php: -------------------------------------------------------------------------------- 1 | databaseDumpFiles = [ 19 | DRUPAL_ROOT . '/core/modules/system/tests/fixtures/update/drupal-8.bare.standard.php.gz', 20 | __DIR__ . '/../../tests/fixtures/update/entity_embed.update-hook-test.php', 21 | ]; 22 | } 23 | 24 | /** 25 | * {@inheritdoc} 26 | */ 27 | protected function setUp() { 28 | parent::setUp(); 29 | $button = $this->container 30 | ->get('entity_type.manager') 31 | ->getDefinition('embed_button'); 32 | 33 | $this->container 34 | ->get('entity.last_installed_schema.repository') 35 | ->setLastInstalledDefinition($button); 36 | } 37 | 38 | /** 39 | * {@inheritdoc} 40 | */ 41 | protected function doSelectionTest() { 42 | parent::doSelectionTest(); 43 | $this->assertRaw('8002 - Updates the default mode settings.'); 44 | $this->assertRaw('8003 - Updates allowed HTML for all filter format config entities that have an Entity Embed button.'); 45 | } 46 | 47 | /** 48 | * Tests entity_embed_update_8002(). 49 | */ 50 | public function todotestPostUpdate() { 51 | $this->runUpdates(); 52 | $mode = $this->container->get('config.factory') 53 | ->get('entity_embed.settings') 54 | ->get('rendered_entity_mode'); 55 | $this->assertTrue($mode, 'Render entity mode settings after update is correct.'); 56 | } 57 | 58 | /** 59 | * Tests entity_embed_update_8003(). 60 | */ 61 | public function testAllowedHTML() { 62 | $allowed_html = ''; 63 | $expected_allowed_html = ''; 64 | $filter_format = $this->container->get('entity_type.manager')->getStorage('filter_format')->load('full_html'); 65 | $filter_format->setFilterConfig('filter_html', [ 66 | 'status' => TRUE, 67 | 'settings' => [ 68 | 'allowed_html' => $allowed_html, 69 | ], 70 | ])->save(); 71 | $editor = $this->container->get('entity_type.manager')->getStorage('editor')->load('full_html'); 72 | $button = $this->container->get('entity_type.manager')->getStorage('embed_button')->load('node'); 73 | $editor->setSettings(['toolbar' => ['rows' => [0 => [0 => ['items' => [0 => $button->id()]]]]]])->save(); 74 | $this->runUpdates(); 75 | $filter_format = $this->container->get('entity_type.manager')->getStorage('filter_format')->load('full_html'); 76 | $filter_html = $filter_format->filters('filter_html'); 77 | $this->assertEqual($expected_allowed_html, $filter_html->getConfiguration()['settings']['allowed_html'], 'Allowed html is correct'); 78 | } 79 | 80 | } 81 | -------------------------------------------------------------------------------- /src/Tests/FileFieldFormatterTest.php: -------------------------------------------------------------------------------- 1 | file = $this->getTestFile('text'); 35 | } 36 | 37 | /** 38 | * Tests file field formatter Entity Embed Display plugins. 39 | */ 40 | public function testFileFieldFormatter() { 41 | // Ensure that file field formatters are available as plugins. 42 | $this->assertAvailableDisplayPlugins($this->file, [ 43 | 'entity_reference:entity_reference_label', 44 | 'entity_reference:entity_reference_entity_id', 45 | 'file:file_default', 46 | 'file:file_table', 47 | 'file:file_url_plain', 48 | ]); 49 | 50 | // Ensure that correct form attributes are returned for the file field 51 | // formatter plugins. 52 | $form = array(); 53 | $form_state = new FormState(); 54 | $plugins = array( 55 | 'file:file_table', 56 | 'file:file_default', 57 | 'file:file_url_plain', 58 | ); 59 | // Ensure that description field is available for all the 'file' plugins. 60 | foreach ($plugins as $plugin) { 61 | $display = $this->container->get('plugin.manager.entity_embed.display') 62 | ->createInstance($plugin, []); 63 | $display->setContextValue('entity', $this->file); 64 | $conf_form = $display->buildConfigurationForm($form, $form_state); 65 | $this->assertIdentical(array_keys($conf_form), array('description')); 66 | $this->assertIdentical($conf_form['description']['#type'], 'textfield'); 67 | $this->assertIdentical((string) $conf_form['description']['#title'], 'Description'); 68 | } 69 | 70 | // Test entity embed using 'Generic file' Entity Embed Display plugin. 71 | $embed_settings = array('description' => "This is sample description"); 72 | $content = 'This placeholder should not be rendered.'; 73 | $settings = array(); 74 | $settings['type'] = 'page'; 75 | $settings['title'] = 'Test entity embed with file:file_default'; 76 | $settings['body'] = array(array('value' => $content, 'format' => 'custom_format')); 77 | $node = $this->drupalCreateNode($settings); 78 | $this->drupalGet('node/' . $node->id()); 79 | $this->assertText($embed_settings['description'], 'Description of the embedded file exists in page.'); 80 | $this->assertNoText(strip_tags($content), 'Placeholder does not appears in the output when embed is successful.'); 81 | $this->assertLinkByHref(file_create_url($this->file->getFileUri()), 0, 'Link to the embedded file exists.'); 82 | } 83 | 84 | } 85 | -------------------------------------------------------------------------------- /entity_embed.install: -------------------------------------------------------------------------------- 1 | listAll('entity_embed.embed_button.') as $config_name) { 20 | $old_embed_button = $config_factory->getEditable($config_name); 21 | $values = $old_embed_button->getRawData(); 22 | 23 | if (EmbedButton::load($values['id'])) { 24 | throw new UpdateException('Unable to convert entity_embed.embed_button.' . $values['id'] . ' to embed.button.' . $values['id'] . ' since the latter already exists.'); 25 | } 26 | 27 | // Move some data around. 28 | $values['type_id'] = 'entity'; 29 | $values['type_settings'] = [ 30 | 'entity_type' => $values['entity_type'], 31 | 'bundles' => array_keys(array_filter($values['entity_type_bundles'])), 32 | 'display_plugins' => array_keys(array_filter($values['display_plugins'])), 33 | ]; 34 | $values['icon_uuid'] = $values['button_icon_uuid']; 35 | unset($values['entity_type']); 36 | unset($values['entity_type_bundles']); 37 | unset($values['display_plugins']); 38 | unset($values['button_icon_uuid']); 39 | 40 | // Save the new embed button and delete the old one. 41 | $embed_button = EmbedButton::create($values); 42 | $embed_button->save(); 43 | $old_embed_button->delete(); 44 | } 45 | } 46 | 47 | /** 48 | * Updates the default mode settings. 49 | */ 50 | function entity_embed_update_8002() { 51 | \Drupal::configFactory() 52 | ->getEditable('entity_embed.settings') 53 | ->set('rendered_entity_mode', TRUE) 54 | ->save(); 55 | } 56 | 57 | /** 58 | * Updates allowed HTML for all filter format config entities that have an 59 | * Entity Embed button. 60 | */ 61 | function entity_embed_update_8003() { 62 | $buttons = \Drupal::entityTypeManager()->getStorage('embed_button')->loadMultiple(); 63 | $filter_formats_with_embed_button = []; 64 | 65 | // Get filter formats from editors with entity embed button. 66 | foreach (\Drupal::entityTypeManager()->getStorage('editor')->loadMultiple() as $editor) { 67 | foreach (new RecursiveIteratorIterator(new RecursiveArrayIterator($editor->getSettings())) as $settings_value) { 68 | foreach ($buttons as $button) { 69 | if ($settings_value == $button->id()) { 70 | $filter_formats_with_embed_button[] = $editor->getFilterFormat(); 71 | } 72 | } 73 | } 74 | } 75 | foreach ($filter_formats_with_embed_button as $filter_format) { 76 | foreach ($filter_format->filters()->getAll() as $filter) { 77 | if (isset($filter->getConfiguration()['settings']['allowed_html'])) { 78 | $allowed_html = $filter->getConfiguration()['settings']['allowed_html']; 79 | if (strpos($allowed_html, 'data-entity-embed-settings')) { 80 | $allowed_html = str_replace('data-entity-embed-settings', 'data-entity-embed-settings data-entity-embed-display-settings', $allowed_html); 81 | $filter_format->setFilterConfig($filter->getPluginId(), ['settings' => ['allowed_html' => $allowed_html]]); 82 | $filter_format->save(); 83 | } 84 | } 85 | } 86 | } 87 | } 88 | -------------------------------------------------------------------------------- /src/Tests/EntityEmbedTestBase.php: -------------------------------------------------------------------------------- 1 | drupalCreateContentType(['type' => 'page', 'name' => 'Basic page']); 45 | 46 | // Create a text format and enable the entity_embed filter. 47 | $format = FilterFormat::create([ 48 | 'format' => 'custom_format', 49 | 'name' => 'Custom format', 50 | 'filters' => [ 51 | 'entity_embed' => [ 52 | 'status' => 1, 53 | ], 54 | ], 55 | ]); 56 | $format->save(); 57 | 58 | $editor_group = [ 59 | 'name' => 'Entity Embed', 60 | 'items' => [ 61 | 'node', 62 | ], 63 | ]; 64 | $editor = Editor::create([ 65 | 'format' => 'custom_format', 66 | 'editor' => 'ckeditor', 67 | 'settings' => [ 68 | 'toolbar' => [ 69 | 'rows' => [[$editor_group]], 70 | ], 71 | ], 72 | ]); 73 | $editor->save(); 74 | 75 | // Create a user with required permissions. 76 | $this->webUser = $this->drupalCreateUser([ 77 | 'access content', 78 | 'create page content', 79 | 'use text format custom_format', 80 | ]); 81 | $this->drupalLogin($this->webUser); 82 | 83 | // Create a sample node to be embedded. 84 | $settings = array(); 85 | $settings['type'] = 'page'; 86 | $settings['title'] = 'Embed Test Node'; 87 | $settings['body'] = array('value' => 'This node is to be used for embedding in other nodes.', 'format' => 'custom_format'); 88 | $this->node = $this->drupalCreateNode($settings); 89 | } 90 | 91 | /** 92 | * Retrieves a sample file of the specified type. 93 | * 94 | * @return \Drupal\file\FileInterface 95 | */ 96 | protected function getTestFile($type_name, $size = NULL) { 97 | // Get a file to upload. 98 | $file = current($this->drupalGetTestFiles($type_name, $size)); 99 | 100 | // Add a filesize property to files as would be read by 101 | // \Drupal\file\Entity\File::load(). 102 | $file->filesize = filesize($file->uri); 103 | 104 | $file = File::create((array) $file); 105 | $file->save(); 106 | return $file; 107 | } 108 | 109 | /** 110 | * Assert that the expected display plugins are available for the entity. 111 | */ 112 | public function assertAvailableDisplayPlugins(EntityInterface $entity, array $expected_plugins, $message = '') { 113 | $plugin_options = $this->container->get('plugin.manager.entity_embed.display') 114 | ->getDefinitionOptionsForEntity($entity); 115 | $this->assertEqual([], array_diff($expected_plugins, array_keys($plugin_options)), $message); 116 | } 117 | 118 | } 119 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | # @file 2 | # .travis.yml - Drupal for Travis CI Integration 3 | # 4 | # Template provided by https://github.com/LionsAd/drupal_ti. 5 | # 6 | # Based for simpletest upon: 7 | # https://github.com/sonnym/travis-ci-drupal-module-example 8 | 9 | language: php 10 | sudo: false 11 | 12 | cache: 13 | directories: 14 | - $HOME/.composer/cache 15 | 16 | php: 17 | - 5.5 18 | - 5.6 19 | - 7 20 | - hhvm 21 | 22 | matrix: 23 | fast_finish: true 24 | allow_failures: 25 | - php: 7 26 | - php: hhvm 27 | 28 | branches: 29 | only: 30 | - "8.x-1.x" 31 | 32 | env: 33 | global: 34 | # add composer's global bin directory to the path 35 | # see: https://github.com/drush-ops/drush#install---composer 36 | - PATH="$PATH:$HOME/.composer/vendor/bin" 37 | 38 | # Configuration variables. 39 | - DRUPAL_TI_MODULE_NAME="entity_embed" 40 | - DRUPAL_TI_SIMPLETEST_GROUP="entity_embed" 41 | 42 | # Define runners and environment vars to include before and after the 43 | # main runners / environment vars. 44 | #- DRUPAL_TI_SCRIPT_DIR_BEFORE="./drupal_ti/before" 45 | #- DRUPAL_TI_SCRIPT_DIR_AFTER="./drupal_ti/after" 46 | 47 | # The environment to use, supported are: drupal-7, drupal-8 48 | - DRUPAL_TI_ENVIRONMENT="drupal-8" 49 | 50 | # Drupal specific variables. 51 | - DRUPAL_TI_DB="drupal_travis_db" 52 | - DRUPAL_TI_DB_URL="mysql://root:@127.0.0.1/drupal_travis_db" 53 | # Note: Do not add a trailing slash here. 54 | - DRUPAL_TI_WEBSERVER_URL="http://127.0.0.1" 55 | - DRUPAL_TI_WEBSERVER_PORT="8080" 56 | 57 | # Simpletest specific commandline arguments, the DRUPAL_TI_SIMPLETEST_GROUP is appended at the end. 58 | - DRUPAL_TI_SIMPLETEST_ARGS="--verbose --color --concurrency 4 --url $DRUPAL_TI_WEBSERVER_URL:$DRUPAL_TI_WEBSERVER_PORT" 59 | 60 | # === Behat specific variables. 61 | # This is relative to $TRAVIS_BUILD_DIR 62 | - DRUPAL_TI_BEHAT_DIR="./tests/behat" 63 | # These arguments are passed to the bin/behat command. 64 | - DRUPAL_TI_BEHAT_ARGS="" 65 | # Specify the filename of the behat.yml with the $DRUPAL_TI_DRUPAL_DIR variables. 66 | - DRUPAL_TI_BEHAT_YML="behat.yml.dist" 67 | # This is used to setup Xvfb. 68 | - DRUPAL_TI_BEHAT_SCREENSIZE_COLOR="1280x1024x16" 69 | # The version of seleniumthat should be used. 70 | - DRUPAL_TI_BEHAT_SELENIUM_VERSION="2.44" 71 | # Set DRUPAL_TI_BEHAT_DRIVER to "selenium" to use "firefox" or "chrome" here. 72 | - DRUPAL_TI_BEHAT_DRIVER="phantomjs" 73 | - DRUPAL_TI_BEHAT_BROWSER="firefox" 74 | 75 | # PHPUnit specific commandline arguments. 76 | - DRUPAL_TI_PHPUNIT_ARGS="" 77 | 78 | # Code coverage via coveralls.io 79 | - DRUPAL_TI_COVERAGE="satooshi/php-coveralls:0.6.*" 80 | # This needs to match your .coveralls.yml file. 81 | - DRUPAL_TI_COVERAGE_FILE="build/logs/clover.xml" 82 | 83 | # Debug options 84 | #- DRUPAL_TI_DEBUG="-x -v" 85 | # Set to "all" to output all files, set to e.g. "xvfb selenium" or "selenium", 86 | # etc. to only output those channels. 87 | #- DRUPAL_TI_DEBUG_FILE_OUTPUT="selenium xvfb webserver" 88 | 89 | matrix: 90 | # [[[ SELECT ANY OR MORE OPTIONS ]]] 91 | #- DRUPAL_TI_RUNNERS="phpunit" 92 | - DRUPAL_TI_RUNNERS="simpletest" 93 | #- DRUPAL_TI_RUNNERS="behat" 94 | #- DRUPAL_TI_RUNNERS="phpunit simpletest behat" 95 | 96 | mysql: 97 | database: drupal_travis_db 98 | username: root 99 | encoding: utf8 100 | 101 | before_install: 102 | - composer self-update 103 | - composer global require "lionsad/drupal_ti:1.*" 104 | - drupal-ti before_install 105 | 106 | install: 107 | - drupal-ti install 108 | 109 | before_script: 110 | - drupal-ti --include .travis-before-script.sh 111 | - drupal-ti before_script 112 | 113 | script: 114 | - drupal-ti script 115 | 116 | after_script: 117 | - drupal-ti after_script 118 | -------------------------------------------------------------------------------- /entity_embed.api.php: -------------------------------------------------------------------------------- 1 | bundle(), ['audio', 'video'])) { 44 | $definitions = array_intersect_key($definitions, array_flip(['file:jwplayer_formatter'])); 45 | } 46 | 47 | // For images, use the image formatter. 48 | if ($entity instanceof \Drupal\file\FileInterface && in_array($entity->bundle(), ['image'])) { 49 | $definitions = array_intersect_key($definitions, array_flip(['image:image'])); 50 | } 51 | 52 | // For nodes, use the default option. 53 | if ($entity instanceof \Drupal\node\NodeInterface) { 54 | $definitions = array_intersect_key($definitions, array_flip(['entity_reference:entity_reference_entity_view'])); 55 | } 56 | } 57 | 58 | /** 59 | * Alter the context of an embedded entity before it is rendered. 60 | * 61 | * @param array &$context 62 | * The context array. 63 | * @param \Drupal\Core\Entity\EntityInterface $entity 64 | * The entity object. 65 | */ 66 | function hook_entity_embed_context_alter(array &$context, \Drupal\Core\Entity\EntityInterface $entity) { 67 | if (isset($context['overrides']) && is_array($context['overrides'])) { 68 | foreach ($context['overrides'] as $key => $value) { 69 | $entity->key = $value; 70 | } 71 | } 72 | } 73 | 74 | /** 75 | * Alter the context of an particular embedded entity type before it is rendered. 76 | * 77 | * @param array &$context 78 | * The context array. 79 | * @param \Drupal\Core\Entity\EntityInterface $entity 80 | * The entity object. 81 | */ 82 | function hook_ENTITY_TYPE_embed_context_alter(array &$context, \Drupal\Core\Entity\EntityInterface $entity) { 83 | if (isset($context['overrides']) && is_array($context['overrides'])) { 84 | foreach ($context['overrides'] as $key => $value) { 85 | $entity->key = $value; 86 | } 87 | } 88 | } 89 | 90 | /** 91 | * Alter the results of an embedded entity build array. 92 | * 93 | * This hook is called after the content has been assembled in a structured 94 | * array and may be used for doing processing which requires that the complete 95 | * block content structure has been built. 96 | * 97 | * @param array &$build 98 | * A renderable array representing the embedded entity content. 99 | * @param \Drupal\Core\Entity\EntityInterface $entity 100 | * The embedded entity object. 101 | * @param array $context 102 | * The context array. 103 | */ 104 | function hook_entity_embed_alter(array &$build, \Drupal\Core\Entity\EntityInterface $entity, array &$context) { 105 | // Remove the contextual links. 106 | if (isset($build['#contextual_links'])) { 107 | unset($build['#contextual_links']); 108 | } 109 | } 110 | 111 | /** 112 | * Alter the results of the particular embedded entity type build array. 113 | * 114 | * @param array &$build 115 | * A renderable array representing the embedded entity content. 116 | * @param \Drupal\Core\Entity\EntityInterface $entity 117 | * The embedded entity object. 118 | * @param array $context 119 | * The context array. 120 | */ 121 | function hook_ENTITY_TYPE_embed_alter(array &$build, \Drupal\Core\Entity\EntityInterface $entity, array &$context) { 122 | // Remove the contextual links. 123 | if (isset($build['#contextual_links'])) { 124 | unset($build['#contextual_links']); 125 | } 126 | } 127 | 128 | /** 129 | * @} End of "addtogroup hooks". 130 | */ 131 | -------------------------------------------------------------------------------- /src/Tests/EntityEmbedHooksTest.php: -------------------------------------------------------------------------------- 1 | state = $this->container->get('state'); 25 | } 26 | 27 | /** 28 | * Tests hook_entity_embed_display_plugins_alter(). 29 | */ 30 | public function testDisplayPluginAlterHooks() { 31 | // Enable entity_embed_test.module's 32 | // hook_entity_embed_display_plugins_alter() implementation and ensure it is 33 | // working as designed. 34 | $this->state->set('entity_embed_test_entity_embed_display_plugins_alter', TRUE); 35 | $plugins = $this->container->get('plugin.manager.entity_embed.display') 36 | ->getDefinitionOptionsForEntity($this->node); 37 | // Ensure that name of each plugin is prefixed with 'testing_hook:'. 38 | foreach ($plugins as $plugin => $plugin_info) { 39 | $this->assertTrue(strpos($plugin, 'testing_hook:') === 0, 'Name of the plugin is prefixed by hook_entity_embed_display_plugins_alter()'); 40 | } 41 | } 42 | 43 | /** 44 | * Tests the hooks provided by entity_embed module. 45 | * 46 | * This method tests all the hooks provided by entity_embed except 47 | * hook_entity_embed_display_plugins_alter, which is tested by a separate 48 | * method. 49 | */ 50 | public function testEntityEmbedHooks() { 51 | // Enable entity_embed_test.module's hook_entity_embed_alter() 52 | // implementation and ensure it is working as designed. 53 | $this->state->set('entity_embed_test_entity_embed_alter', TRUE); 54 | $content = 'This placeholder should not be rendered.'; 55 | $settings = array(); 56 | $settings['type'] = 'page'; 57 | $settings['title'] = 'Test hook_entity_embed_alter()'; 58 | $settings['body'] = array(array('value' => $content, 'format' => 'custom_format')); 59 | $node = $this->drupalCreateNode($settings); 60 | $this->drupalGet('node/' . $node->id()); 61 | $this->assertText($this->node->body->value, 'Embedded node exists in page.'); 62 | $this->assertNoText(strip_tags($content), 'Placeholder does not appears in the output when embed is successful.'); 63 | // Ensure that embedded node's title has been replaced. 64 | $this->assertText('Title set by hook_entity_embed_alter', 'Title of the embedded node is replaced by hook_entity_embed_alter()'); 65 | $this->assertNoText($this->node->title->value, 'Original title of the embedded node is not visible.'); 66 | $this->state->set('entity_embed_test_entity_embed_alter', FALSE); 67 | 68 | // Enable entity_embed_test.module's hook_entity_embed_context_alter() 69 | // implementation and ensure it is working as designed. 70 | $this->state->set('entity_embed_test_entity_embed_context_alter', TRUE); 71 | $content = 'This placeholder should not be rendered.'; 72 | $settings = array(); 73 | $settings['type'] = 'page'; 74 | $settings['title'] = 'Test hook_entity_embed_context_alter()'; 75 | $settings['body'] = array(array('value' => $content, 'format' => 'custom_format')); 76 | $node = $this->drupalCreateNode($settings); 77 | $this->drupalGet('node/' . $node->id()); 78 | $this->assertNoText(strip_tags($content), 'Placeholder does not appears in the output when embed is successful.'); 79 | // To ensure that 'label' plugin is used, verify that the body of the 80 | // embedded node is not visible and the title links to the embedded node. 81 | $this->assertNoText($this->node->body->value, 'Body of the embedded node does not exists in page.'); 82 | $this->assertText('Title set by hook_entity_embed_context_alter', 'Title of the embedded node is replaced by hook_entity_embed_context_alter()'); 83 | $this->assertNoText($this->node->title->value, 'Original title of the embedded node is not visible.'); 84 | $this->assertLinkByHref('node/' . $this->node->id(), 0, 'Link to the embedded node exists.'); 85 | $this->state->set('entity_embed_test_entity_embed_context_alter', FALSE); 86 | } 87 | 88 | } 89 | -------------------------------------------------------------------------------- /src/Tests/ImageFieldFormatterTest.php: -------------------------------------------------------------------------------- 1 | image = $this->getTestFile('image'); 42 | $this->file = $this->getTestFile('text'); 43 | } 44 | 45 | /** 46 | * Tests image field formatter Entity Embed Display plugin. 47 | */ 48 | public function testImageFieldFormatter() { 49 | // Ensure that image field formatters are available as plugins. 50 | $this->assertAvailableDisplayPlugins($this->image, [ 51 | 'entity_reference:entity_reference_label', 52 | 'entity_reference:entity_reference_entity_id', 53 | 'file:file_default', 54 | 'file:file_table', 55 | 'file:file_url_plain', 56 | 'image:responsive_image', 57 | 'image:image', 58 | ]); 59 | 60 | // Ensure that correct form attributes are returned for the image plugin. 61 | $form = array(); 62 | $form_state = new FormState(); 63 | $display = $this->container->get('plugin.manager.entity_embed.display') 64 | ->createInstance('image:image', []); 65 | $display->setContextValue('entity', $this->image); 66 | $conf_form = $display->buildConfigurationForm($form, $form_state); 67 | $this->assertIdentical(array_keys($conf_form), array( 68 | 'image_style', 69 | 'image_link', 70 | 'alt', 71 | 'title', 72 | )); 73 | $this->assertIdentical($conf_form['image_style']['#type'], 'select'); 74 | $this->assertIdentical((string) $conf_form['image_style']['#title'], 'Image style'); 75 | $this->assertIdentical($conf_form['image_link']['#type'], 'select'); 76 | $this->assertIdentical((string) $conf_form['image_link']['#title'], 'Link image to'); 77 | $this->assertIdentical($conf_form['alt']['#type'], 'textfield'); 78 | $this->assertIdentical((string) $conf_form['alt']['#title'], 'Alternate text'); 79 | $this->assertIdentical($conf_form['title']['#type'], 'textfield'); 80 | $this->assertIdentical((string) $conf_form['title']['#title'], 'Title'); 81 | 82 | // Test entity embed using 'Image' Entity Embed Display plugin. 83 | $alt_text = "This is sample description"; 84 | $title = "This is sample title"; 85 | $embed_settings = array('image_link' => 'file'); 86 | $content = 'This placeholder should not be rendered.'; 87 | $settings = array(); 88 | $settings['type'] = 'page'; 89 | $settings['title'] = 'Test entity embed with image:image'; 90 | $settings['body'] = array(array('value' => $content, 'format' => 'custom_format')); 91 | $node = $this->drupalCreateNode($settings); 92 | $this->drupalGet('node/' . $node->id()); 93 | $this->assertRaw($alt_text, 'Alternate text for the embedded image is visible when embed is successful.'); 94 | $this->assertNoText(strip_tags($content), 'Placeholder does not appears in the output when embed is successful.'); 95 | $this->assertLinkByHref(file_create_url($this->image->getFileUri()), 0, 'Link to the embedded image exists.'); 96 | 97 | // Embed all three field types in one, to ensure they all render correctly. 98 | $content = ''; 99 | $content .= ''; 100 | $content .= ''; 101 | $settings = array(); 102 | $settings['type'] = 'page'; 103 | $settings['title'] = 'Test node entity embedded first then a file entity'; 104 | $settings['body'] = array(array('value' => $content, 'format' => 'custom_format')); 105 | $node = $this->drupalCreateNode($settings); 106 | $this->drupalGet('node/' . $node->id()); 107 | } 108 | 109 | } 110 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Entity Embed Module 2 | 3 | [![Travis build status](https://img.shields.io/travis/drupal-media/entity_embed/8.x-1.x.svg)](https://travis-ci.org/drupal-media/entity_embed) 4 | [![Scrutinizer code quality](https://img.shields.io/scrutinizer/g/drupal-media/entity_embed/8.x-1.x.svg)](https://scrutinizer-ci.com/g/drupal-media/entity_embed) 5 | 6 | [Entity Embed](https://www.drupal.org/project/entity_embed) module 7 | allows any entity to be embedded using a text editor. 8 | 9 | ## Requirements 10 | 11 | * Drupal 8 12 | * [Embed](https://www.drupal.org/project/embed) module 13 | 14 | ## Installation 15 | 16 | Entity Embed can be installed via the 17 | [standard Drupal installation process](http://drupal.org/node/895232). 18 | 19 | ## Configuration 20 | 21 | * Install and enable [Embed](https://www.drupal.org/project/embed) module. 22 | * Install and enable [Entity Embed](https://www.drupal.org/project/entity_embed) 23 | module. 24 | * Go to the 'Text formats and editors' configuration page: `/admin/config/content/formats`, 25 | and for each text format/editor combo where you want to embed entities, 26 | do the following: 27 | * Enable the "Display embedded entities" filter for the desired text formats 28 | on the Text Formats configuration page. 29 | * Drag and drop the 'E' button into the Active toolbar. 30 | * If the text format uses the 'Limit allowed HTML tags and correct 31 | faulty HTML' filter, ensure the necessary tags and attributes were 32 | automatically whitelisted: 33 | `````` 34 | appears in the 'Allowed HTML tags' setting. 35 | *Warning: If you were using the module in very early pre-alpha 36 | stages you might need to add `data-entity-id` to the list of allowed 37 | attributes. Similarly, if you have been using the module in pre-beta stages, 38 | you need to white-list the `data-entity-embed-settings` attribute.* 39 | * If you're using both the 'Align images' and 'Caption images' filters make 40 | sure the 'Align images' filter is run before the 'Caption images' filter in 41 | the **Filter processing order** section. (Explanation: Due to the 42 | implementation details of the two filters it is important to execute them in 43 | the right sequence in order to obtain a sensible final markup. In practice 44 | this means that the alignment filter has to be run before the caption 45 | filter, otherwise the alignment class will appear inside the
tag 46 | (instead of appearing on it) the caption filter produces.) 47 | 48 | ## Usage 49 | 50 | * For example, create a new *Article* content. 51 | * Click on the 'E' button in the text editor. 52 | * Enter part of the title of the entity you're looking for and select 53 | one of the search results. 54 | * If the entity you select is a node entity, for **Display as** you can choose 55 | one of the following options: 56 | * Entity ID 57 | * Label 58 | * Full content 59 | * RSS 60 | * Search index 61 | * Search result highlighting input 62 | * Teaser 63 | * The last five options depend on the view modes you have on the entity. 64 | * Optionally, choose to align left, center or right. 65 | **Rendered Entity** was available before but now the view modes are 66 | available as entity embed display plugins. 67 | 68 | ## Embedding entities without WYSIWYG 69 | 70 | Users should be embedding entities using the CKEditor WYSIWYG button as 71 | described above. This section is more technical about the HTML markup 72 | that is used to embed the actual entity. 73 | 74 | ### Example: 75 | ```html 76 | 77 | ``` 78 | 79 | ## Entity Embed Display Plugins 80 | 81 | Embedding entities uses an Entity Embed Display plugin, provided in the 82 | `data-entity-embed-display` attribute. By default we provide four 83 | different Entity Embed Display plugins out of the box: 84 | 85 | - entity_reference:_formatter_id_: Renders the entity using a specific 86 | Entity Reference field formatter. 87 | - entity_reference:_entity_reference_label_: Renders the entity using 88 | the "Label" formatter. 89 | - file:_formatter_id_: Renders the entity using a specific File field 90 | formatter. This will only work if the entity is a file entity type. 91 | - image:_formatter_id_: Renders the entity using a specific Image field 92 | formatter. This will only work if the entity is a file entity type, 93 | and the file is an image. 94 | 95 | Configuration for the Entity Embed Display plugin can be provided by 96 | using a `data-entity-embed-display-settings` attribute, which contains a 97 | JSON-encoded array value. Note that care must be used to use single 98 | quotes around the attribute value since JSON-encoded arrays typically 99 | contain double quotes. 100 | 101 | The above examples render the entity using the 102 | _entity_reference_entity_view_ formatter from the Entity Reference 103 | module, using the _teaser_ view mode. 104 | -------------------------------------------------------------------------------- /src/EntityEmbedDisplay/EntityEmbedDisplayManager.php: -------------------------------------------------------------------------------- 1 | alterInfo('entity_embed_display_plugins'); 35 | $this->setCacheBackend($cache_backend, 'entity_embed_display_plugins'); 36 | } 37 | 38 | /** 39 | * Overrides DefaultPluginManager::processDefinition(). 40 | */ 41 | public function processDefinition(&$definition, $plugin_id) { 42 | $definition += array( 43 | 'entity_types' => FALSE, 44 | ); 45 | 46 | if ($definition['entity_types'] !== FALSE && !is_array($definition['entity_types'])) { 47 | $definition['entity_types'] = array($definition['entity_types']); 48 | } 49 | } 50 | 51 | /** 52 | * Determines plugins whose constraints are satisfied by a set of contexts. 53 | * 54 | * @param array $contexts 55 | * An array of contexts. 56 | * 57 | * @return array 58 | * An array of plugin definitions. 59 | * 60 | * @todo At some point convert this to use ContextAwarePluginManagerTrait 61 | * 62 | * @see https://drupal.org/node/2277981 63 | */ 64 | public function getDefinitionsForContexts(array $contexts = array()) { 65 | $definitions = $this->getDefinitions(); 66 | $valid_ids = array_filter(array_keys($definitions), function ($id) use ($contexts) { 67 | try { 68 | $display = $this->createInstance($id); 69 | foreach ($contexts as $name => $value) { 70 | $display->setContextValue($name, $value); 71 | } 72 | // We lose cacheability metadata at this point. We should refactor to 73 | // avoid this. @see https://www.drupal.org/node/2593379#comment-11368447 74 | return $display->access()->isAllowed(); 75 | } 76 | catch (PluginException $e) { 77 | return FALSE; 78 | } 79 | }); 80 | $definitions_for_context = array_intersect_key($definitions, array_flip($valid_ids)); 81 | $this->moduleHandler->alter('entity_embed_display_plugins_for_context', $definitions_for_context, $contexts); 82 | return $definitions_for_context; 83 | } 84 | 85 | /** 86 | * Gets definition options for entity. 87 | * 88 | * Provides a list of plugins that can be used for a certain entity and 89 | * filters out plugins that should be hidden in the UI. 90 | * 91 | * @param \Drupal\Core\Entity\EntityInterface $entity 92 | * An entity object. 93 | * 94 | * @return array 95 | * An array of valid plugin labels, keyed by plugin ID. 96 | */ 97 | public function getDefinitionOptionsForEntity(EntityInterface $entity) { 98 | $definitions = $this->getDefinitionsForContexts(array('entity' => $entity, 'entity_type' => $entity->getEntityTypeId())); 99 | $definitions = $this->filterExposedDefinitions($definitions); 100 | return array_map(function ($definition) { 101 | return (string) $definition['label']; 102 | }, $definitions); 103 | } 104 | 105 | /** 106 | * Filters out plugins from definitions that should be hidden in the UI. 107 | * 108 | * @param array $definitions 109 | * The array of plugin definitions. 110 | * 111 | * @return array 112 | * Returns plugin definitions that should be displayed in the UI. 113 | */ 114 | protected function filterExposedDefinitions(array $definitions) { 115 | return array_filter($definitions, function($definition) { 116 | return empty($definition['no_ui']); 117 | }); 118 | } 119 | 120 | /** 121 | * Gets definition options for entity type. 122 | * 123 | * Provides a list of plugins that can be used for a certain entity type and 124 | * filters out plugins that should be hidden in the UI. 125 | * 126 | * @param string $entity_type 127 | * The entity type id. 128 | * 129 | * @return array 130 | * An array of valid plugin labels, keyed by plugin ID. 131 | */ 132 | public function getDefinitionOptionsForEntityType($entity_type) { 133 | $definitions = $this->getDefinitionsForContexts(array('entity_type' => $entity_type)); 134 | $definitions = $this->filterExposedDefinitions($definitions); 135 | return array_map(function ($definition) { 136 | return (string) $definition['label']; 137 | }, $definitions); 138 | } 139 | 140 | } 141 | -------------------------------------------------------------------------------- /src/EntityEmbedBuilder.php: -------------------------------------------------------------------------------- 1 | moduleHandler = $module_handler; 40 | $this->displayPluginManager = $display_manager; 41 | } 42 | 43 | /** 44 | * {@inheritdoc} 45 | */ 46 | public function buildEntityEmbed(EntityInterface $entity, array $context = []) { 47 | // Support the deprecated view-mode data attribute. 48 | if (isset($context['data-view-mode']) && !isset($context['data-entity-embed-display']) && !isset($context['data-entity-embed-display-settings'])) { 49 | $context['data-entity-embed-display'] = 'entity_reference:entity_reference_entity_view'; 50 | $context['data-entity-embed-display-settings'] = ['view_mode' => &$context['data-view-mode']]; 51 | } 52 | 53 | // Merge in default attributes. 54 | $context += [ 55 | 'data-entity-type' => $entity->getEntityTypeId(), 56 | 'data-entity-uuid' => $entity->uuid(), 57 | 'data-entity-embed-display' => 'entity_reference:entity_reference_entity_view', 58 | 'data-entity-embed-display-settings' => [], 59 | ]; 60 | 61 | // The default Entity Embed Display plugin has been deprecated by the 62 | // rendered entity field formatter. 63 | if ($context['data-entity-embed-display'] === 'default') { 64 | $context['data-entity-embed-display'] = 'entity_reference:entity_reference_entity_view'; 65 | } 66 | 67 | // The caption text is double-encoded, so decode it here. 68 | if (isset($context['data-caption'])) { 69 | $context['data-caption'] = Html::decodeEntities($context['data-caption']); 70 | } 71 | 72 | // Allow modules to alter the entity prior to embed rendering. 73 | $this->moduleHandler->alter(["{$context['data-entity-type']}_embed_context", 'entity_embed_context'], $context, $entity); 74 | 75 | // Build and render the Entity Embed Display plugin, allowing modules to 76 | // alter the result before rendering. 77 | $build = [ 78 | '#theme_wrappers' => ['entity_embed_container'], 79 | '#attributes' => ['class' => ['embedded-entity']], 80 | '#entity' => $entity, 81 | '#context' => $context, 82 | ]; 83 | $build['entity'] = $this->buildEntityEmbedDisplayPlugin( 84 | $entity, 85 | $context['data-entity-embed-display'], 86 | $context['data-entity-embed-display-settings'], 87 | $context 88 | ); 89 | 90 | // Maintain data-align if it is there. 91 | if (isset($context['data-align'])) { 92 | $build['#attributes']['data-align'] = $context['data-align']; 93 | } 94 | elseif ((isset($context['class']))) { 95 | $build['#attributes']['class'][] = $context['class']; 96 | } 97 | 98 | // Maintain data-caption if it is there. 99 | if (isset($context['data-caption'])) { 100 | $build['#attributes']['data-caption'] = $context['data-caption']; 101 | } 102 | 103 | // Make sure that access to the entity is respected. 104 | $build['#access'] = $entity->access('view', NULL, TRUE); 105 | 106 | // @todo Should this hook get invoked if $build is an empty array? 107 | $this->moduleHandler->alter(["{$context['data-entity-type']}_embed", 'entity_embed'], $build, $entity, $context); 108 | return $build; 109 | } 110 | 111 | /** 112 | * Builds the render array for an entity using an Entity Embed Display plugin. 113 | * 114 | * @param \Drupal\Core\Entity\EntityInterface $entity 115 | * The entity to be rendered. 116 | * @param string $plugin_id 117 | * The Entity Embed Display plugin ID. 118 | * @param array $plugin_configuration 119 | * (optional) Array of plugin configuration values. 120 | * @param array $context 121 | * (optional) Array of additional context values, usually the embed HTML 122 | * tag's attributes. 123 | * 124 | * @return array 125 | * A render array for the Entity Embed Display plugin. 126 | */ 127 | protected function buildEntityEmbedDisplayPlugin(EntityInterface $entity, $plugin_id, array $plugin_configuration = [], array $context = []) { 128 | // Build the Entity Embed Display plugin. 129 | /** @var \Drupal\entity_embed\EntityEmbedDisplay\EntityEmbedDisplayBase $display */ 130 | $display = $this->displayPluginManager->createInstance($plugin_id, $plugin_configuration); 131 | $display->setContextValue('entity', $entity); 132 | $display->setAttributes($context); 133 | 134 | // Check if the Entity Embed Display plugin is accessible. This also checks 135 | // entity access, which is why we never call $entity->access() here. 136 | if (!$display->access()) { 137 | return []; 138 | } 139 | 140 | return $display->build(); 141 | } 142 | 143 | } 144 | -------------------------------------------------------------------------------- /src/Tests/EntityEmbedDialogTest.php: -------------------------------------------------------------------------------- 1 | getEmbedDialog(); 28 | $this->assertResponse(404, 'Embed dialog is not accessible without specifying filter format and embed button.'); 29 | $this->getEmbedDialog('custom_format'); 30 | $this->assertResponse(404, 'Embed dialog is not accessible without specifying embed button.'); 31 | 32 | // Ensure that the route is not accessible with an invalid embed button. 33 | $this->getEmbedDialog('custom_format', 'invalid_button'); 34 | $this->assertResponse(404, 'Embed dialog is not accessible without specifying filter format and embed button.'); 35 | 36 | // Ensure that the route is not accessible with text format without the 37 | // button configured. 38 | $this->getEmbedDialog('plain_text', 'node'); 39 | $this->assertResponse(404, 'Embed dialog is not accessible with a filter that does not have an editor configuration.'); 40 | 41 | // Add an empty configuration for the plain_text editor configuration. 42 | $editor = Editor::create([ 43 | 'format' => 'plain_text', 44 | 'editor' => 'ckeditor', 45 | ]); 46 | $editor->save(); 47 | $this->getEmbedDialog('plain_text', 'node'); 48 | $this->assertResponse(403, 'Embed dialog is not accessible with a filter that does not have the embed button assigned to it.'); 49 | 50 | // Ensure that the route is accessible with a valid embed button. 51 | // 'Node' embed button is provided by default by the module and hence the 52 | // request must be successful. 53 | $this->getEmbedDialog('custom_format', 'node'); 54 | $this->assertResponse(200, 'Embed dialog is accessible with correct filter format and embed button.'); 55 | 56 | // Ensure form structure of the 'select' step and submit form. 57 | $this->assertFieldByName('entity_id', '', 'Entity ID/UUID field is present.'); 58 | 59 | // $edit = ['attributes[data-entity-id]' => $this->node->id()]; 60 | // $this->drupalPostAjaxForm(NULL, $edit, 'op'); 61 | // Ensure form structure of the 'embed' step and submit form. 62 | // $this->assertFieldByName('attributes[data-entity-embed-display]', 'Entity Embed Display plugin field is present.');. 63 | } 64 | 65 | /** 66 | * Tests the entity embed button markup. 67 | */ 68 | public function testEntityEmbedButtonMarkup() { 69 | // Ensure that the route is not accessible with text format without the 70 | // button configured. 71 | $this->getEmbedDialog('plain_text', 'node'); 72 | $this->assertResponse(404, 'Embed dialog is not accessible with a filter that does not have an editor configuration.'); 73 | 74 | // Add an empty configuration for the plain_text editor configuration. 75 | $editor = Editor::create([ 76 | 'format' => 'plain_text', 77 | 'editor' => 'ckeditor', 78 | ]); 79 | $editor->save(); 80 | $this->getEmbedDialog('plain_text', 'node'); 81 | $this->assertResponse(403, 'Embed dialog is not accessible with a filter that does not have the embed button assigned to it.'); 82 | 83 | // Ensure that the route is accessible with a valid embed button. 84 | // 'Node' embed button is provided by default by the module and hence the 85 | // request must be successful. 86 | $this->getEmbedDialog('custom_format', 'node'); 87 | $this->assertResponse(200, 'Embed dialog is accessible with correct filter format and embed button.'); 88 | 89 | // Ensure form structure of the 'select' step and submit form. 90 | $this->assertFieldByName('entity_id', '', 'Entity ID/UUID field is present.'); 91 | 92 | // Check that 'Next' is a primary button. 93 | $this->assertFieldByXPath('//input[contains(@class, "button--primary")]', 'Next', 'Next is a primary button'); 94 | 95 | $title = $this->node->getTitle() . ' (' . $this->node->id() . ')'; 96 | $edit = ['entity_id' => $title]; 97 | $response = $this->drupalPostAjaxForm(NULL, $edit, 'op'); 98 | $plugins = [ 99 | 'entity_reference:entity_reference_label', 100 | 'entity_reference:entity_reference_entity_id', 101 | 'view_mode:node.full', 102 | 'view_mode:node.rss', 103 | 'view_mode:node.search_index', 104 | 'view_mode:node.search_result', 105 | 'view_mode:node.teaser', 106 | ]; 107 | foreach ($plugins as $plugin) { 108 | $this->assertTrue(strpos($response[2]['data'], $plugin), 'Plugin ' . $plugin . ' is available in selection.'); 109 | } 110 | 111 | $this->container->get('config.factory')->getEditable('entity_embed.settings') 112 | ->set('rendered_entity_mode', TRUE)->save(); 113 | $this->container->get('plugin.manager.entity_embed.display')->clearCachedDefinitions(); 114 | 115 | $this->getEmbedDialog('custom_format', 'node'); 116 | $title = $this->node->getTitle() . ' (' . $this->node->id() . ')'; 117 | $edit = ['entity_id' => $title]; 118 | $response = $this->drupalPostAjaxForm(NULL, $edit, 'op'); 119 | 120 | $plugins = [ 121 | 'entity_reference:entity_reference_label', 122 | 'entity_reference:entity_reference_entity_id', 123 | 'entity_reference:entity_reference_entity_view', 124 | ]; 125 | foreach ($plugins as $plugin) { 126 | $this->assertTrue(strpos($response[2]['data'], $plugin), 'Plugin ' . $plugin . ' is available in selection.'); 127 | } 128 | /*$this->drupalPostForm(NULL, $edit, 'Next'); 129 | // Ensure form structure of the 'embed' step and submit form. 130 | $this->assertFieldByName('attributes[data-entity-embed-display]', 'Entity Embed Display plugin field is present.'); 131 | 132 | // Check that 'Embed' is a primary button. 133 | $this->assertFieldByXPath('//input[contains(@class, "button--primary")]', 'Embed', 'Embed is a primary button');*/ 134 | } 135 | 136 | /** 137 | * Tests entity embed functionality. 138 | */ 139 | public function testEntityEmbedFunctionality() { 140 | $edit = [ 141 | 'entity_id' => $this->node->getTitle() . ' (' . $this->node->id() . ')', 142 | ]; 143 | $this->getEmbedDialog('custom_format', 'node'); 144 | $this->drupalPostForm(NULL, $edit, t('Next')); 145 | // Tests that the embed dialog doesn't trow a fatal in 146 | // ImageFieldFormatter::isValidImage() 147 | $this->assertResponse(200); 148 | } 149 | 150 | /** 151 | * Retrieves an embed dialog based on given parameters. 152 | * 153 | * @param string $filter_format_id 154 | * ID of the filter format. 155 | * @param string $embed_button_id 156 | * ID of the embed button. 157 | * 158 | * @return string 159 | * The retrieved HTML string. 160 | */ 161 | public function getEmbedDialog($filter_format_id = NULL, $embed_button_id = NULL) { 162 | $url = 'entity-embed/dialog'; 163 | if (!empty($filter_format_id)) { 164 | $url .= '/' . $filter_format_id; 165 | if (!empty($embed_button_id)) { 166 | $url .= '/' . $embed_button_id; 167 | } 168 | } 169 | return $this->drupalGet($url); 170 | } 171 | 172 | } 173 | -------------------------------------------------------------------------------- /src/Tests/EntityReferenceFieldFormatterTest.php: -------------------------------------------------------------------------------- 1 | menu = entity_create('menu', array( 29 | 'id' => 'menu_name', 30 | 'label' => 'Label', 31 | 'description' => 'Description text', 32 | )); 33 | $this->menu->save(); 34 | } 35 | 36 | /** 37 | * Tests entity reference field formatters. 38 | */ 39 | public function testEntityReferenceFieldFormatter() { 40 | // Ensure that entity reference field formatters are available as plugins. 41 | $this->assertAvailableDisplayPlugins($this->node, [ 42 | 'entity_reference:entity_reference_label', 43 | 'entity_reference:entity_reference_entity_id', 44 | 'view_mode:node.full', 45 | 'view_mode:node.rss', 46 | 'view_mode:node.search_index', 47 | 'view_mode:node.search_result', 48 | 'view_mode:node.teaser', 49 | ]); 50 | 51 | $this->container->get('config.factory')->getEditable('entity_embed.settings') 52 | ->set('rendered_entity_mode', TRUE)->save(); 53 | $this->container->get('plugin.manager.entity_embed.display')->clearCachedDefinitions(); 54 | 55 | $this->assertAvailableDisplayPlugins($this->node, [ 56 | 'entity_reference:entity_reference_label', 57 | 'entity_reference:entity_reference_entity_id', 58 | 'entity_reference:entity_reference_entity_view', 59 | ]); 60 | 61 | // Ensure that correct form attributes are returned for 62 | // 'entity_reference:entity_reference_entity_id' plugin. 63 | $form = array(); 64 | $form_state = new FormState(); 65 | $display = $this->container->get('plugin.manager.entity_embed.display')->createInstance('entity_reference:entity_reference_entity_id', array()); 66 | $display->setContextValue('entity', $this->node); 67 | $conf_form = $display->buildConfigurationForm($form, $form_state); 68 | $this->assertIdentical(array_keys($conf_form), array()); 69 | 70 | // Ensure that correct form attributes are returned for 71 | // 'entity_reference:entity_reference_entity_view' plugin. 72 | $form = array(); 73 | $form_state = new FormState(); 74 | $display = $this->container->get('plugin.manager.entity_embed.display')->createInstance('entity_reference:entity_reference_entity_view', array()); 75 | $display->setContextValue('entity', $this->node); 76 | $conf_form = $display->buildConfigurationForm($form, $form_state); 77 | $this->assertIdentical($conf_form['view_mode']['#type'], 'select'); 78 | $this->assertIdentical((string) $conf_form['view_mode']['#title'], 'View mode'); 79 | 80 | // Ensure that correct form attributes are returned for 81 | // 'entity_reference:entity_reference_label' plugin. 82 | $form = array(); 83 | $form_state = new FormState(); 84 | $display = $this->container->get('plugin.manager.entity_embed.display')->createInstance('entity_reference:entity_reference_label', array()); 85 | $display->setContextValue('entity', $this->node); 86 | $conf_form = $display->buildConfigurationForm($form, $form_state); 87 | $this->assertIdentical(array_keys($conf_form), array('link')); 88 | $this->assertIdentical($conf_form['link']['#type'], 'checkbox'); 89 | $this->assertIdentical((string) $conf_form['link']['#title'], 'Link label to the referenced entity'); 90 | 91 | // Ensure that 'Rendered Entity' plugin is not available for an entity not 92 | // having a view controller. 93 | $plugin_options = $this->container->get('plugin.manager.entity_embed.display')->getDefinitionOptionsForEntity($this->menu); 94 | $this->assertFalse(array_key_exists('entity_reference:entity_reference_entity_view', $plugin_options), "The 'Rendered entity' plugin is not available."); 95 | } 96 | 97 | /** 98 | * Tests filter using entity reference Entity Embed Display plugins. 99 | */ 100 | public function testFilterEntityReferencePlugins() { 101 | // Test 'Label' Entity Embed Display plugin. 102 | $content = 'This placeholder should not be rendered.'; 103 | $settings = array(); 104 | $settings['type'] = 'page'; 105 | $settings['title'] = 'Test entity_reference:entity_reference_label Entity Embed Display plugin'; 106 | $settings['body'] = array(array('value' => $content, 'format' => 'custom_format')); 107 | $node = $this->drupalCreateNode($settings); 108 | $this->drupalGet('node/' . $node->id()); 109 | $this->assertText($this->node->title->value, 'Title of the embedded node exists in page.'); 110 | $this->assertNoText($this->node->body->value, 'Body of embedded node does not exists in page.'); 111 | $this->assertNoText(strip_tags($content), 'Placeholder does not appears in the output when embed is successful.'); 112 | $this->assertLinkByHref('node/' . $this->node->id(), 0, 'Link to the embedded node exists.'); 113 | 114 | // Test 'Entity ID' Entity Embed Display plugin. 115 | $content = 'This placeholder should not be rendered.'; 116 | $settings = array(); 117 | $settings['type'] = 'page'; 118 | $settings['title'] = 'Test entity_reference:entity_reference_entity_id Entity Embed Display plugin'; 119 | $settings['body'] = array(array('value' => $content, 'format' => 'custom_format')); 120 | $node = $this->drupalCreateNode($settings); 121 | $this->drupalGet('node/' . $node->id()); 122 | $this->assertText($this->node->id(), 'ID of the embedded node exists in page.'); 123 | $this->assertNoText($this->node->title->value, 'Title of the embedded node does not exists in page.'); 124 | $this->assertNoText($this->node->body->value, 'Body of embedded node does not exists in page.'); 125 | $this->assertNoText(strip_tags($content), 'Placeholder does not appears in the output when embed is successful.'); 126 | $this->assertNoLinkByHref('node/' . $this->node->id(), 'Link to the embedded node does not exists.'); 127 | 128 | // Test 'Rendered entity' Entity Embed Display plugin. 129 | $content = 'This placeholder should not be rendered.'; 130 | $settings = array(); 131 | $settings['type'] = 'page'; 132 | $settings['title'] = 'Test entity_reference:entity_reference_label Entity Embed Display plugin'; 133 | $settings['body'] = array(array('value' => $content, 'format' => 'custom_format')); 134 | $node = $this->drupalCreateNode($settings); 135 | $this->drupalGet('node/' . $node->id()); 136 | $this->assertText($this->node->body->value, 'Body of embedded node does not exists in page.'); 137 | $this->assertNoText(strip_tags($content), 'Placeholder does not appears in the output when embed is successful.'); 138 | } 139 | 140 | } 141 | -------------------------------------------------------------------------------- /src/Plugin/Filter/EntityEmbedFilter.php: -------------------------------------------------------------------------------- 1 | entityTypeManager = $entity_type_manager; 73 | $this->renderer = $renderer; 74 | $this->builder = $builder; 75 | } 76 | 77 | /** 78 | * {@inheritdoc} 79 | */ 80 | public static function create(ContainerInterface $container, array $configuration, $plugin_id, $plugin_definition) { 81 | return new static( 82 | $configuration, 83 | $plugin_id, 84 | $plugin_definition, 85 | $container->get('entity_type.manager'), 86 | $container->get('renderer'), 87 | $container->get('entity_embed.builder') 88 | ); 89 | } 90 | 91 | /** 92 | * {@inheritdoc} 93 | */ 94 | public function process($text, $langcode) { 95 | $result = new FilterProcessResult($text); 96 | 97 | if (strpos($text, 'data-entity-type') !== FALSE && (strpos($text, 'data-entity-embed-display') !== FALSE || strpos($text, 'data-view-mode') !== FALSE)) { 98 | $dom = Html::load($text); 99 | $xpath = new \DOMXPath($dom); 100 | 101 | foreach ($xpath->query('//drupal-entity[@data-entity-type and (@data-entity-uuid or @data-entity-id) and (@data-entity-embed-display or @data-view-mode)]') as $node) { 102 | /** @var \DOMElement $node */ 103 | $entity_type = $node->getAttribute('data-entity-type'); 104 | $entity = NULL; 105 | $entity_output = ''; 106 | 107 | // data-entity-embed-settings is deprecated, make sure we convert it to 108 | // data-entity-embed-display-settings. 109 | if (($settings = $node->getAttribute('data-entity-embed-settings')) && !$node->hasAttribute('data-entity-embed-display-settings')) { 110 | $node->setAttribute('data-entity-embed-display-settings', $settings); 111 | $node->removeAttribute('data-entity-embed-settings'); 112 | } 113 | 114 | try { 115 | // Load the entity either by UUID (preferred) or ID. 116 | $id = NULL; 117 | $entity = NULL; 118 | if ($id = $node->getAttribute('data-entity-uuid')) { 119 | $entity = $this->entityTypeManager->getStorage($entity_type) 120 | ->loadByProperties(['uuid' => $id]); 121 | $entity = current($entity); 122 | } 123 | else { 124 | $id = $node->getAttribute('data-entity-id'); 125 | $entity = $this->entityTypeManager->getStorage($entity_type)->load($id); 126 | } 127 | 128 | if ($entity) { 129 | // Protect ourselves from recursive rendering. 130 | static $depth = 0; 131 | $depth++; 132 | if ($depth > 20) { 133 | throw new RecursiveRenderingException(sprintf('Recursive rendering detected when rendering embedded %s entity %s.', $entity_type, $entity->id())); 134 | } 135 | 136 | // If a UUID was not used, but is available, add it to the HTML. 137 | if (!$node->getAttribute('data-entity-uuid') && $uuid = $entity->uuid()) { 138 | $node->setAttribute('data-entity-uuid', $uuid); 139 | } 140 | 141 | $context = $this->getNodeAttributesAsArray($node); 142 | $context += array('data-langcode' => $langcode); 143 | $build = $this->builder->buildEntityEmbed($entity, $context); 144 | // We need to render the embedded entity: 145 | // - without replacing placeholders, so that the placeholders are 146 | // only replaced at the last possible moment. Hence we cannot use 147 | // either renderPlain() or renderRoot(), so we must use render(). 148 | // - without bubbling beyond this filter, because filters must 149 | // ensure that the bubbleable metadata for the changes they make 150 | // when filtering text makes it onto the FilterProcessResult 151 | // object that they return ($result). To prevent that bubbling, we 152 | // must wrap the call to render() in a render context. 153 | $entity_output = $this->renderer->executeInRenderContext(new RenderContext(), function () use (&$build) { 154 | return $this->renderer->render($build); 155 | }); 156 | $result = $result->merge(BubbleableMetadata::createFromRenderArray($build)); 157 | 158 | $depth--; 159 | } 160 | else { 161 | throw new EntityNotFoundException(sprintf('Unable to load embedded %s entity %s.', $entity_type, $id)); 162 | } 163 | } 164 | catch (\Exception $e) { 165 | watchdog_exception('entity_embed', $e); 166 | } 167 | 168 | $this->replaceNodeContent($node, $entity_output); 169 | } 170 | 171 | $result->setProcessedText(Html::serialize($dom)); 172 | } 173 | 174 | return $result; 175 | } 176 | 177 | /** 178 | * {@inheritdoc} 179 | */ 180 | public function tips($long = FALSE) { 181 | if ($long) { 182 | return $this->t(' 183 |

You can embed entities. Additional properties can be added to the embed tag like data-caption and data-align if supported. Example:

184 | <drupal-entity data-entity-type="node" data-entity-uuid="07bf3a2e-1941-4a44-9b02-2d1d7a41ec0e" data-view-mode="teaser" />'); 185 | } 186 | else { 187 | return $this->t('You can embed entities.'); 188 | } 189 | } 190 | 191 | } 192 | -------------------------------------------------------------------------------- /src/Plugin/entity_embed/EntityEmbedDisplay/ImageFieldFormatter.php: -------------------------------------------------------------------------------- 1 | imageFactory = $image_factory; 55 | } 56 | 57 | /** 58 | * {@inheritdoc} 59 | */ 60 | public static function create(ContainerInterface $container, array $configuration, $plugin_id, $plugin_definition) { 61 | return new static( 62 | $configuration, 63 | $plugin_id, 64 | $plugin_definition, 65 | $container->get('entity_type.manager'), 66 | $container->get('plugin.manager.field.formatter'), 67 | $container->get('typed_data_manager'), 68 | $container->get('image.factory'), 69 | $container->get('language_manager') 70 | ); 71 | } 72 | 73 | /** 74 | * {@inheritdoc} 75 | */ 76 | public function getFieldValue() { 77 | $value = parent::getFieldValue(); 78 | // File field support descriptions, but images do not. 79 | unset($value['description']); 80 | $value += array_intersect_key($this->getAttributeValues(), array('alt' => '', 'title' => '')); 81 | return $value; 82 | } 83 | 84 | /** 85 | * {@inheritdoc} 86 | */ 87 | public function access(AccountInterface $account = NULL) { 88 | return parent::access($account)->andIf($this->isValidImage()); 89 | } 90 | 91 | /** 92 | * Checks if the image is valid. 93 | * 94 | * @return \Drupal\Core\Access\AccessResult 95 | * Returns the access result. 96 | */ 97 | protected function isValidImage() { 98 | // If entity type is not file we have to return early to prevent fatal in 99 | // the condition above. Access should already be forbidden at this point, 100 | // which means this won't have any effect. 101 | // @see EntityEmbedDisplayBase::access() 102 | if ($this->getEntityTypeFromContext() != 'file') { 103 | return AccessResult::forbidden(); 104 | } 105 | $access = AccessResult::allowed(); 106 | 107 | // @todo needs cacheability metadata for getEntityFromContext. 108 | // @see \Drupal\entity_embed\EntityEmbedDisplay\EntityEmbedDisplayBase::getEntityFromContext() 109 | /** @var \Drupal\file\FileInterface $entity */ 110 | if ($entity = $this->getEntityFromContext()) { 111 | // Loading large files is slow, make sure it is an image mime type before 112 | // doing that. 113 | list($type,) = explode('/', $entity->getMimeType(), 2); 114 | $access = AccessResult::allowedIf($type == 'image' && $this->imageFactory->get($entity->getFileUri())->isValid()) 115 | // See the above @todo, this is the best we can do for now. 116 | ->addCacheableDependency($entity); 117 | } 118 | 119 | return $access; 120 | } 121 | 122 | /** 123 | * {@inheritdoc} 124 | */ 125 | public function buildConfigurationForm(array $form, FormStateInterface $form_state) { 126 | $form = parent::buildConfigurationForm($form, $form_state); 127 | 128 | // File field support descriptions, but images do not. 129 | unset($form['description']); 130 | 131 | // Ensure that the 'Link image to: Content' setting is not available. 132 | if ($this->getDerivativeId() == 'image') { 133 | unset($form['image_link']['#options']['content']); 134 | } 135 | 136 | $entity_element = $form_state->get('entity_element'); 137 | // The alt attribute is *required*, but we allow users to opt-in to empty 138 | // alt attributes for the very rare edge cases where that is valid by 139 | // specifying two double quotes as the alternative text in the dialog. 140 | // However, that *is* stored as an empty alt attribute, so if we're editing 141 | // an existing image (which means the src attribute is set) and its alt 142 | // attribute is empty, then we show that as two double quotes in the dialog. 143 | // @see https://www.drupal.org/node/2307647 144 | // Alt attribute behavior is taken from the Core image dialog to ensure a 145 | // consistent UX across various forms. 146 | // @see Drupal\editor\Form\EditorImageDialog::buildForm() 147 | $alt = $this->getAttributeValue('alt', ''); 148 | if ($alt === '') { 149 | // Do not change empty alt text to two double quotes if the previously 150 | // used Entity Embed Display plugin was not 'image:image'. That means that 151 | // some other plugin was used so if this image formatter is selected at a 152 | // later stage, then this should be treated as a new edit. We show two 153 | // double quotes in place of empty alt text only if that was filled 154 | // intentionally by the user. 155 | if (!empty($entity_element) && $entity_element['data-entity-embed-display'] == 'image:image') { 156 | $alt = '""'; 157 | } 158 | } 159 | 160 | // Add support for editing the alternate and title text attributes. 161 | $form['alt'] = array( 162 | '#type' => 'textfield', 163 | '#title' => $this->t('Alternate text'), 164 | '#default_value' => $alt, 165 | '#description' => $this->t('This text will be used by screen readers, search engines, or when the image cannot be loaded.'), 166 | '#parents' => array('attributes', 'alt'), 167 | '#required' => TRUE, 168 | '#required_error' => $this->t('Alternative text is required.
(Only in rare cases should this be left empty. To create empty alternative text, enter "" — two double quotes without any content).'), 169 | '#maxlength' => 512, 170 | ); 171 | $form['title'] = array( 172 | '#type' => 'textfield', 173 | '#title' => $this->t('Title'), 174 | '#default_value' => $this->getAttributeValue('title', ''), 175 | '#description' => t('The title is used as a tool tip when the user hovers the mouse over the image.'), 176 | '#parents' => array('attributes', 'title'), 177 | '#maxlength' => 1024, 178 | ); 179 | 180 | return $form; 181 | } 182 | 183 | /** 184 | * {@inheritdoc} 185 | */ 186 | public function submitConfigurationForm(array &$form, FormStateInterface $form_state) { 187 | // When the alt attribute is set to two double quotes, transform it to the 188 | // empty string: two double quotes signify "empty alt attribute". See above. 189 | if (trim($form_state->getValue(array('attributes', 'alt'))) === '""') { 190 | $form_state->setValue(array('attributes', 'alt'), ''); 191 | } 192 | } 193 | 194 | } 195 | -------------------------------------------------------------------------------- /src/EntityEmbedDisplay/FieldFormatterEntityEmbedDisplayBase.php: -------------------------------------------------------------------------------- 1 | formatterPluginManager = $formatter_plugin_manager; 65 | $this->setConfiguration($configuration); 66 | $this->typedDataManager = $typed_data_manager; 67 | parent::__construct($configuration, $plugin_id, $plugin_definition, $entity_type_manager, $language_manager); 68 | } 69 | 70 | /** 71 | * {@inheritdoc} 72 | */ 73 | public static function create(ContainerInterface $container, array $configuration, $plugin_id, $plugin_definition) { 74 | return new static( 75 | $configuration, 76 | $plugin_id, 77 | $plugin_definition, 78 | $container->get('entity_type.manager'), 79 | $container->get('plugin.manager.field.formatter'), 80 | $container->get('typed_data_manager'), 81 | $container->get('language_manager') 82 | ); 83 | } 84 | 85 | /** 86 | * Get the FieldDefinition object required to render this field's formatter. 87 | * 88 | * @return \Drupal\Core\Field\BaseFieldDefinition 89 | * The field definition. 90 | * 91 | * @see \Drupal\entity_embed\FieldFormatterEntityEmbedDisplayBase::build() 92 | */ 93 | public function getFieldDefinition() { 94 | if (!isset($this->fieldDefinition)) { 95 | $field_type = $this->getPluginDefinition()['field_type']; 96 | $this->fieldDefinition = BaseFieldDefinition::create($field_type); 97 | // Ensure the field name is unique for each Entity Embed Display plugin 98 | // instance. 99 | static $index = 0; 100 | $this->fieldDefinition->setName('_entity_embed_' . $index++); 101 | } 102 | return $this->fieldDefinition; 103 | } 104 | 105 | /** 106 | * Get the field value required to pass into the field formatter. 107 | * 108 | * @return mixed 109 | * The field value. 110 | */ 111 | abstract public function getFieldValue(); 112 | 113 | /** 114 | * {@inheritdoc} 115 | */ 116 | public function access(AccountInterface $account = NULL) { 117 | return parent::access($account)->andIf($this->isApplicableFieldFormatter()); 118 | } 119 | 120 | /** 121 | * Checks if the field formatter is applicable. 122 | * 123 | * @return \Drupal\Core\Access\AccessResult 124 | * Returns the access result. 125 | */ 126 | protected function isApplicableFieldFormatter() { 127 | $definition = $this->formatterPluginManager->getDefinition($this->getFieldFormatterId()); 128 | return AccessResult::allowedIf($definition['class']::isApplicable($this->getFieldDefinition())); 129 | } 130 | 131 | /** 132 | * Returns the field formatter id. 133 | * 134 | * @return string|null 135 | * Returns field formatter id or null. 136 | */ 137 | public function getFieldFormatterId() { 138 | return $this->getDerivativeId(); 139 | } 140 | 141 | /** 142 | * {@inheritdoc} 143 | */ 144 | public function build() { 145 | // Create a temporary node object to which our fake field value can be 146 | // added. 147 | $node = Node::create(array('type' => '_entity_embed')); 148 | 149 | $definition = $this->getFieldDefinition(); 150 | 151 | /* @var \Drupal\Core\Field\FieldItemListInterface $items $items */ 152 | // Create a field item list object, 1 is the value, array('target_id' => 1) 153 | // would work too, or multiple values. 1 is passed down from the list to the 154 | // field item, which knows that an integer is the ID. 155 | $items = $this->typedDataManager->create( 156 | $definition, 157 | $this->getFieldValue($definition), 158 | $definition->getName(), 159 | $node->getTypedData() 160 | ); 161 | 162 | // Prepare, expects an array of items, keyed by parent entity ID. 163 | $formatter = $this->getFieldFormatter(); 164 | $formatter->prepareView(array($node->id() => $items)); 165 | $build = $formatter->viewElements($items, $this->getLangcode()); 166 | // For some reason $build[0]['#printed'] is TRUE, which means it will fail 167 | // to render later. So for now we manually fix that. 168 | // @todo Investigate why this is needed. 169 | show($build[0]); 170 | return $build[0]; 171 | } 172 | 173 | /** 174 | * {@inheritdoc} 175 | */ 176 | public function defaultConfiguration() { 177 | return $this->formatterPluginManager->getDefaultSettings($this->getFieldFormatterId()); 178 | } 179 | 180 | /** 181 | * {@inheritdoc} 182 | */ 183 | public function buildConfigurationForm(array $form, FormStateInterface $form_state) { 184 | return $this->getFieldFormatter()->settingsForm($form, $form_state); 185 | } 186 | 187 | /** 188 | * Constructs a field formatter. 189 | * 190 | * @return \Drupal\Core\Field\FormatterInterface 191 | * The formatter object. 192 | */ 193 | public function getFieldFormatter() { 194 | if (!isset($this->fieldFormatter)) { 195 | $display = array( 196 | 'type' => $this->getFieldFormatterId(), 197 | 'settings' => $this->getConfiguration(), 198 | 'label' => 'hidden', 199 | ); 200 | 201 | // Create the formatter plugin. Will use the default formatter for that 202 | // field type if none is passed. 203 | $this->fieldFormatter = $this->formatterPluginManager->getInstance( 204 | array( 205 | 'field_definition' => $this->getFieldDefinition(), 206 | 'view_mode' => '_entity_embed', 207 | 'configuration' => $display, 208 | ) 209 | ); 210 | } 211 | 212 | return $this->fieldFormatter; 213 | } 214 | 215 | /** 216 | * Creates a new faux-field definition. 217 | * 218 | * @param string $type 219 | * The type of the field. 220 | * 221 | * @return \Drupal\Core\Field\BaseFieldDefinition 222 | * A new field definition. 223 | */ 224 | protected function createFieldDefinition($type) { 225 | $definition = BaseFieldDefinition::create($type); 226 | static $index = 0; 227 | $definition->setName('_entity_embed_' . $index++); 228 | return $definition; 229 | } 230 | 231 | /** 232 | * {@inheritdoc} 233 | */ 234 | public function calculateDependencies() { 235 | $this->addDependencies(parent::calculateDependencies()); 236 | 237 | $definition = $this->formatterPluginManager->getDefinition($this->getFieldFormatterId()); 238 | $this->addDependency('module', $definition['provider']); 239 | // @todo Investigate why this does not work currently. 240 | // $this->calculatePluginDependencies($this->getFieldFormatter()); 241 | return $this->dependencies; 242 | } 243 | 244 | } 245 | -------------------------------------------------------------------------------- /js/plugins/drupalentity/plugin.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @file 3 | * Drupal Entity embed plugin. 4 | */ 5 | 6 | (function ($, Drupal, CKEDITOR) { 7 | 8 | "use strict"; 9 | 10 | CKEDITOR.plugins.add('drupalentity', { 11 | // This plugin requires the Widgets System defined in the 'widget' plugin. 12 | requires: 'widget', 13 | 14 | // The plugin initialization logic goes inside this method. 15 | beforeInit: function (editor) { 16 | // Configure CKEditor DTD for custom drupal-entity element. 17 | // @see https://www.drupal.org/node/2448449#comment-9717735 18 | var dtd = CKEDITOR.dtd, tagName; 19 | dtd['drupal-entity'] = {'#': 1}; 20 | // Register drupal-entity element as allowed child, in each tag that can 21 | // contain a div element. 22 | for (tagName in dtd) { 23 | if (dtd[tagName].div) { 24 | dtd[tagName]['drupal-entity'] = 1; 25 | } 26 | } 27 | 28 | // Generic command for adding/editing entities of all types. 29 | editor.addCommand('editdrupalentity', { 30 | allowedContent: 'drupal-entity[data-embed-button,data-entity-type,data-entity-uuid,data-entity-embed-display,data-entity-embed-display-settings,data-align,data-caption]', 31 | requiredContent: 'drupal-entity[data-embed-button,data-entity-type,data-entity-uuid,data-entity-embed-display,data-entity-embed-display-settings,data-align,data-caption]', 32 | modes: { wysiwyg : 1 }, 33 | canUndo: true, 34 | exec: function (editor, data) { 35 | data = data || {}; 36 | 37 | var existingElement = getSelectedEmbeddedEntity(editor); 38 | 39 | var existingValues = {}; 40 | if (existingElement && existingElement.$ && existingElement.$.firstChild) { 41 | var embedDOMElement = existingElement.$.firstChild; 42 | // Populate array with the entity's current attributes. 43 | var attribute = null, attributeName; 44 | for (var key = 0; key < embedDOMElement.attributes.length; key++) { 45 | attribute = embedDOMElement.attributes.item(key); 46 | attributeName = attribute.nodeName.toLowerCase(); 47 | if (attributeName.substring(0, 15) === 'data-cke-saved-') { 48 | continue; 49 | } 50 | existingValues[attributeName] = existingElement.data('cke-saved-' + attributeName) || attribute.nodeValue; 51 | } 52 | } 53 | 54 | var embed_button_id = data.id ? data.id : existingValues['data-embed-button']; 55 | 56 | var dialogSettings = { 57 | dialogClass: 'entity-select-dialog', 58 | resizable: false 59 | }; 60 | 61 | var saveCallback = function (values) { 62 | var entityElement = editor.document.createElement('drupal-entity'); 63 | var attributes = values.attributes; 64 | for (var key in attributes) { 65 | entityElement.setAttribute(key, attributes[key]); 66 | } 67 | 68 | editor.insertHtml(entityElement.getOuterHtml()); 69 | if (existingElement) { 70 | // Detach the behaviors that were attached when the entity content 71 | // was inserted. 72 | Drupal.runEmbedBehaviors('detach', existingElement.$); 73 | existingElement.remove(); 74 | } 75 | }; 76 | 77 | // Open the entity embed dialog for corresponding EmbedButton. 78 | Drupal.ckeditor.openDialog(editor, Drupal.url('entity-embed/dialog/' + editor.config.drupal.format + '/' + embed_button_id), existingValues, saveCallback, dialogSettings); 79 | } 80 | }); 81 | 82 | // Register the entity embed widget. 83 | editor.widgets.add('drupalentity', { 84 | // Minimum HTML which is required by this widget to work. 85 | allowedContent: 'drupal-entity[data-entity-type,data-entity-uuid,data-entity-embed-display,data-entity-embed-display-settings,data-align,data-caption]', 86 | requiredContent: 'drupal-entity[data-entity-type,data-entity-uuid,data-entity-embed-display,data-entity-embed-display-settings,data-align,data-caption]', 87 | 88 | // Simply recognize the element as our own. The inner markup if fetched 89 | // and inserted the init() callback, since it requires the actual DOM 90 | // element. 91 | upcast: function (element) { 92 | var attributes = element.attributes; 93 | if (attributes['data-entity-type'] === undefined || (attributes['data-entity-id'] === undefined && attributes['data-entity-uuid'] === undefined) || (attributes['data-view-mode'] === undefined && attributes['data-entity-embed-display'] === undefined)) { 94 | return; 95 | } 96 | // Generate an ID for the element, so that we can use the Ajax 97 | // framework. 98 | element.attributes.id = generateEmbedId(); 99 | return element; 100 | }, 101 | 102 | // Fetch the rendered entity. 103 | init: function () { 104 | /** @type {CKEDITOR.dom.element} */ 105 | var element = this.element; 106 | // Use the Ajax framework to fetch the HTML, so that we can retrieve 107 | // out-of-band assets (JS, CSS...). 108 | var entityEmbedPreview = Drupal.ajax({ 109 | base: element.getId(), 110 | element: element.$, 111 | url: Drupal.url('embed/preview/' + editor.config.drupal.format + '?' + $.param({ 112 | value: element.getOuterHtml() 113 | })), 114 | progress: {type: 'none'}, 115 | // Use a custom event to trigger the call. 116 | event: 'entity_embed_dummy_event' 117 | }); 118 | entityEmbedPreview.execute(); 119 | }, 120 | 121 | // Downcast the element. 122 | downcast: function (element) { 123 | // Only keep the wrapping element. 124 | element.setHtml(''); 125 | // Remove the auto-generated ID. 126 | delete element.attributes.id; 127 | return element; 128 | } 129 | }); 130 | 131 | // Register the toolbar buttons. 132 | if (editor.ui.addButton) { 133 | for (var key in editor.config.DrupalEntity_buttons) { 134 | var button = editor.config.DrupalEntity_buttons[key]; 135 | editor.ui.addButton(button.id, { 136 | label: button.label, 137 | data: button, 138 | allowedContent: 'drupal-entity[!data-entity-type,!data-entity-uuid,!data-entity-embed-display,!data-entity-embed-display-settings,!data-align,!data-caption,!data-embed-button]', 139 | click: function(editor) { 140 | editor.execCommand('editdrupalentity', this.data); 141 | }, 142 | icon: button.image 143 | }); 144 | } 145 | } 146 | 147 | // Register context menu option for editing widget. 148 | if (editor.contextMenu) { 149 | editor.addMenuGroup('drupalentity'); 150 | editor.addMenuItem('drupalentity', { 151 | label: Drupal.t('Edit Entity'), 152 | icon: this.path + 'entity.png', 153 | command: 'editdrupalentity', 154 | group: 'drupalentity' 155 | }); 156 | 157 | editor.contextMenu.addListener(function(element) { 158 | if (isEditableEntityWidget(editor, element)) { 159 | return { drupalentity: CKEDITOR.TRISTATE_OFF }; 160 | } 161 | }); 162 | } 163 | 164 | // Execute widget editing action on double click. 165 | editor.on('doubleclick', function (evt) { 166 | var element = getSelectedEmbeddedEntity(editor) || evt.data.element; 167 | 168 | if (isEditableEntityWidget(editor, element)) { 169 | editor.execCommand('editdrupalentity'); 170 | } 171 | }); 172 | } 173 | }); 174 | 175 | /** 176 | * Get the surrounding drupalentity widget element. 177 | * 178 | * @param {CKEDITOR.editor} editor 179 | */ 180 | function getSelectedEmbeddedEntity(editor) { 181 | var selection = editor.getSelection(); 182 | var selectedElement = selection.getSelectedElement(); 183 | if (isEditableEntityWidget(editor, selectedElement)) { 184 | return selectedElement; 185 | } 186 | 187 | return null; 188 | } 189 | 190 | /** 191 | * Checks if the given element is an editable drupalentity widget. 192 | * 193 | * @param {CKEDITOR.editor} editor 194 | * @param {CKEDITOR.htmlParser.element} element 195 | */ 196 | function isEditableEntityWidget (editor, element) { 197 | var widget = editor.widgets.getByElement(element, true); 198 | if (!widget || widget.name !== 'drupalentity') { 199 | return false; 200 | } 201 | 202 | var button = $(element.$.firstChild).attr('data-embed-button'); 203 | if (!button) { 204 | // If there was no data-embed-button attribute, not editable. 205 | return false; 206 | } 207 | 208 | // The button itself must be valid. 209 | return editor.config.DrupalEntity_buttons.hasOwnProperty(button); 210 | } 211 | 212 | /** 213 | * Generates unique HTML IDs for the widgets. 214 | * 215 | * @returns {string} 216 | */ 217 | function generateEmbedId() { 218 | if (typeof generateEmbedId.counter == 'undefined') { 219 | generateEmbedId.counter = 0; 220 | } 221 | return 'entity-embed-' + generateEmbedId.counter++; 222 | } 223 | 224 | })(jQuery, Drupal, CKEDITOR); 225 | -------------------------------------------------------------------------------- /src/EntityEmbedDisplay/EntityEmbedDisplayBase.php: -------------------------------------------------------------------------------- 1 | setConfiguration($configuration); 67 | $this->entityTypeManager = $entity_type_manager; 68 | $this->languageManager = $language_manager; 69 | } 70 | 71 | /** 72 | * {@inheritdoc} 73 | */ 74 | public static function create(ContainerInterface $container, array $configuration, $plugin_id, $plugin_definition) { 75 | return new static( 76 | $configuration, 77 | $plugin_id, 78 | $plugin_definition, 79 | $container->get('entity_type.manager'), 80 | $container->get('language_manager') 81 | ); 82 | } 83 | 84 | /** 85 | * {@inheritdoc} 86 | */ 87 | public function access(AccountInterface $account = NULL) { 88 | // @todo Add a hook_entity_embed_display_access()? 89 | // Check that the plugin's registered entity types matches the current 90 | // entity type. 91 | return AccessResult::allowedIf($this->isValidEntityType()) 92 | // @see \Drupal\Core\Entity\EntityTypeManager 93 | ->addCacheTags(['entity_types']); 94 | } 95 | 96 | /** 97 | * Validates that this Entity Embed Display plugin applies to the current 98 | * entity type. 99 | * 100 | * This checks the plugin annotation's 'entity_types' value, which should be 101 | * an array of entity types that this plugin can process, or FALSE if the 102 | * plugin applies to all entity types. 103 | * 104 | * @return bool 105 | * TRUE if the plugin can display the current entity type, or FALSE 106 | * otherwise. 107 | */ 108 | protected function isValidEntityType() { 109 | // First, determine whether or not the entity type id is valid. Return FALSE 110 | // if the specified id is not valid. 111 | $entity_type = $this->getEntityTypeFromContext(); 112 | if (!$this->entityTypeManager->getDefinition($entity_type)) { 113 | return FALSE; 114 | } 115 | 116 | $definition = $this->getPluginDefinition(); 117 | if ($definition['entity_types'] === FALSE) { 118 | return TRUE; 119 | } 120 | else { 121 | return in_array($entity_type, $definition['entity_types']); 122 | } 123 | } 124 | 125 | /** 126 | * {@inheritdoc} 127 | */ 128 | abstract public function build(); 129 | 130 | /** 131 | * {@inheritdoc} 132 | */ 133 | public function calculateDependencies() { 134 | return array(); 135 | } 136 | 137 | /** 138 | * {@inheritdoc} 139 | */ 140 | public function defaultConfiguration() { 141 | return array(); 142 | } 143 | 144 | /** 145 | * {@inheritdoc} 146 | */ 147 | public function getConfiguration() { 148 | return $this->configuration; 149 | } 150 | 151 | /** 152 | * {@inheritdoc} 153 | */ 154 | public function setConfiguration(array $configuration) { 155 | $this->configuration = NestedArray::mergeDeep( 156 | $this->defaultConfiguration(), 157 | $configuration 158 | ); 159 | return $this; 160 | } 161 | 162 | /** 163 | * {@inheritdoc} 164 | */ 165 | public function buildConfigurationForm(array $form, FormStateInterface $form_state) { 166 | return $form; 167 | } 168 | 169 | /** 170 | * {@inheritdoc} 171 | */ 172 | public function validateConfigurationForm(array &$form, FormStateInterface $form_state) { 173 | // Do nothing. 174 | } 175 | 176 | /** 177 | * {@inheritdoc} 178 | */ 179 | public function submitConfigurationForm(array &$form, FormStateInterface $form_state) { 180 | if (!$form_state->getErrors()) { 181 | $this->configuration = array_intersect_key($form_state->getValues(), $this->defaultConfiguration()); 182 | } 183 | } 184 | 185 | /** 186 | * Gets a configuration value. 187 | * 188 | * @param string $name 189 | * The name of the plugin configuration value. 190 | * @param mixed $default 191 | * The default value to return if the configuration value does not exist. 192 | * 193 | * @return mixed 194 | * The currently set configuration value, or the value of $default if the 195 | * configuration value is not set. 196 | */ 197 | public function getConfigurationValue($name, $default = NULL) { 198 | $configuration = $this->getConfiguration(); 199 | return array_key_exists($name, $configuration) ? $configuration[$name] : $default; 200 | } 201 | 202 | /** 203 | * Sets the value for a defined context. 204 | * 205 | * @param string $name 206 | * The name of the context in the plugin definition. 207 | * @param mixed $value 208 | * The value to set the context to. The value has to validate against the 209 | * provided context definition. 210 | */ 211 | public function setContextValue($name, $value) { 212 | $this->context[$name] = $value; 213 | } 214 | 215 | /** 216 | * Gets the values for all defined contexts. 217 | * 218 | * @return array 219 | * An array of set context values, keyed by context name. 220 | */ 221 | public function getContextValues() { 222 | return $this->context; 223 | } 224 | 225 | /** 226 | * Gets the value for a defined context. 227 | * 228 | * @param string $name 229 | * The name of the context in the plugin configuration. 230 | * 231 | * @return mixed 232 | * The currently set context value. 233 | */ 234 | public function getContextValue($name) { 235 | return $this->context[$name]; 236 | } 237 | 238 | /** 239 | * Returns whether or not value is set for a defined context. 240 | * 241 | * @param string $name 242 | * The name of the context in the plugin configuration. 243 | * 244 | * @return bool 245 | * True if context value exists, false otherwise. 246 | */ 247 | public function hasContextValue($name) { 248 | return array_key_exists($name, $this->context); 249 | } 250 | 251 | /** 252 | * Gets the entity type from the current context. 253 | * 254 | * @return string 255 | * The entity type id. 256 | */ 257 | public function getEntityTypeFromContext() { 258 | if ($this->hasContextValue('entity')) { 259 | return $this->getContextValue('entity')->getEntityTypeId(); 260 | } 261 | else { 262 | return $this->getContextValue('entity_type'); 263 | } 264 | } 265 | 266 | /** 267 | * Gets the entity from the current context. 268 | * 269 | * @todo Where doe sthis come from? The value must come from somewhere, yet 270 | * this does not implement any context-related interfaces. This is an *input*, 271 | * so we need cache contexts and possibly cache tags to reflect where this 272 | * came from. We need that for *everything* that this class does that relies 273 | * on this, plus any of its subclasses. Right now, this is effectively a 274 | * global that breaks cacheability metadata. 275 | * 276 | * @return \Drupal\Core\Entity\EntityInterface 277 | */ 278 | public function getEntityFromContext() { 279 | if ($this->hasContextValue('entity')) { 280 | return $this->getContextValue('entity'); 281 | } 282 | } 283 | 284 | /** 285 | * Sets the values for all attributes. 286 | * 287 | * @param array $attributes 288 | * An array of attributes, keyed by attribute name. 289 | */ 290 | public function setAttributes(array $attributes) { 291 | $this->attributes = $attributes; 292 | } 293 | 294 | /** 295 | * Gets the values for all attributes. 296 | * 297 | * @return array 298 | * An array of set attribute values, keyed by attribute name. 299 | */ 300 | public function getAttributeValues() { 301 | return $this->attributes; 302 | } 303 | 304 | /** 305 | * Gets the value for an attribute. 306 | * 307 | * @param string $name 308 | * The name of the attribute. 309 | * @param mixed $default 310 | * The default value to return if the attribute value does not exist. 311 | * 312 | * @return mixed 313 | * The currently set attribute value. 314 | */ 315 | public function getAttributeValue($name, $default = NULL) { 316 | $attributes = $this->getAttributeValues(); 317 | return array_key_exists($name, $attributes) ? $attributes[$name] : $default; 318 | } 319 | 320 | /** 321 | * Gets the current language code. 322 | * 323 | * @return string 324 | */ 325 | public function getLangcode() { 326 | $langcode = $this->getAttributeValue('data-langcode'); 327 | if (empty($langcode)) { 328 | $langcode = $this->languageManager->getCurrentLanguage(LanguageInterface::TYPE_CONTENT)->getId(); 329 | } 330 | return $langcode; 331 | } 332 | 333 | } 334 | -------------------------------------------------------------------------------- /src/Plugin/EmbedType/Entity.php: -------------------------------------------------------------------------------- 1 | entityTypeManager = $entity_type_manager; 76 | $this->entityTypeRepository = $entity_type_repository; 77 | $this->entityTypeBundleInfo = $bundle_info; 78 | $this->displayPluginManager = $display_plugin_manager; 79 | } 80 | 81 | /** 82 | * {@inheritdoc} 83 | */ 84 | public static function create(ContainerInterface $container, array $configuration, $plugin_id, $plugin_definition) { 85 | return new static( 86 | $configuration, 87 | $plugin_id, 88 | $plugin_definition, 89 | $container->get('entity_type.manager'), 90 | $container->get('entity_type.repository'), 91 | $container->get('entity_type.bundle.info'), 92 | $container->get('plugin.manager.entity_embed.display') 93 | ); 94 | } 95 | 96 | /** 97 | * {@inheritdoc} 98 | */ 99 | public function defaultConfiguration() { 100 | return [ 101 | 'entity_type' => 'node', 102 | 'bundles' => [], 103 | 'display_plugins' => [], 104 | 'entity_browser' => '', 105 | 'entity_browser_settings' => [], 106 | ]; 107 | } 108 | 109 | /** 110 | * {@inheritdoc} 111 | */ 112 | public function buildConfigurationForm(array $form, FormStateInterface $form_state) { 113 | $embed_button = $form_state->getTemporaryValue('embed_button'); 114 | $entity_type_id = $this->getConfigurationValue('entity_type'); 115 | 116 | $form['entity_type'] = array( 117 | '#type' => 'select', 118 | '#title' => $this->t('Entity type'), 119 | '#options' => $this->getEntityTypeOptions(), 120 | '#default_value' => $entity_type_id, 121 | '#description' => $this->t("Entity type for which this button is to enabled."), 122 | '#required' => TRUE, 123 | '#ajax' => array( 124 | 'callback' => array($form_state->getFormObject(), 'updateTypeSettings'), 125 | 'effect' => 'fade', 126 | ), 127 | '#disabled' => !$embed_button->isNew(), 128 | ); 129 | 130 | if ($entity_type_id) { 131 | $entity_type = $this->entityTypeManager->getDefinition($entity_type_id); 132 | $form['bundles'] = array( 133 | '#type' => 'checkboxes', 134 | '#title' => $entity_type->getBundleLabel() ?: $this->t('Bundles'), 135 | '#options' => $this->getEntityBundleOptions($entity_type), 136 | '#default_value' => $this->getConfigurationValue('bundles'), 137 | '#description' => $this->t('If none are selected, all are allowed.'), 138 | ); 139 | $form['bundles']['#access'] = !empty($form['bundles']['#options']); 140 | 141 | // Allow option to limit Entity Embed Display plugins. 142 | $form['display_plugins'] = array( 143 | '#type' => 'checkboxes', 144 | '#title' => $this->t('Allowed Entity Embed Display plugins'), 145 | '#options' => $this->displayPluginManager->getDefinitionOptionsForEntityType($entity_type_id), 146 | '#default_value' => $this->getConfigurationValue('display_plugins'), 147 | '#description' => $this->t('If none are selected, all are allowed. Note that these are the plugins which are allowed for this entity type, all of these might not be available for the selected entity.'), 148 | ); 149 | $form['display_plugins']['#access'] = !empty($form['display_plugins']['#options']); 150 | 151 | /** @var \Drupal\entity_browser\EntityBrowserInterface[] $browsers */ 152 | if ($this->entityTypeManager->hasDefinition('entity_browser') && ($browsers = $this->entityTypeManager->getStorage('entity_browser')->loadMultiple())) { 153 | $ids = array_keys($browsers); 154 | $labels = array_map( 155 | function ($item) { 156 | /** @var \Drupal\entity_browser\EntityBrowserInterface $item */ 157 | return $item->label(); 158 | }, 159 | $browsers 160 | ); 161 | $options = ['_none' => $this->t('None (autocomplete)')] + array_combine($ids, $labels); 162 | $form['entity_browser'] = [ 163 | '#type' => 'select', 164 | '#title' => $this->t('Entity browser'), 165 | '#description' => $this->t('Entity browser to be used to select entities to be embedded. Note that not all display plugins from Entity Browser are compatible with Entity Embed. For example, the "iFrame" display is compatible, while the "Modal" display is not.'), 166 | '#options' => $options, 167 | '#default_value' => $this->getConfigurationValue('entity_browser'), 168 | ]; 169 | $form['entity_browser_settings'] = [ 170 | '#type' => 'details', 171 | '#title' => $this->t('Entity browser settings'), 172 | '#open' => TRUE, 173 | '#states' => [ 174 | 'invisible' => [ 175 | ':input[name="type_settings[entity_browser]"]' => ['value' => '_none'], 176 | ], 177 | ], 178 | ]; 179 | $form['entity_browser_settings']['display_review'] = [ 180 | '#type' => 'checkbox', 181 | '#title' => 'Display the entity after selection', 182 | '#default_value' => $this->getConfigurationValue('entity_browser_settings')['display_review'], 183 | ]; 184 | } 185 | else { 186 | $form['entity_browser'] = [ 187 | '#type' => 'value', 188 | '#value' => '', 189 | ]; 190 | } 191 | } 192 | 193 | return $form; 194 | } 195 | 196 | /** 197 | * {@inheritdoc} 198 | */ 199 | public function submitConfigurationForm(array &$form, FormStateInterface $form_state) { 200 | // Filter down the bundles and allowed Entity Embed Display plugins. 201 | $bundles = $form_state->getValue('bundles'); 202 | $form_state->setValue('bundles', array_keys(array_filter($bundles))); 203 | $display_plugins = $form_state->getValue('display_plugins'); 204 | $form_state->setValue('display_plugins', array_keys(array_filter($display_plugins))); 205 | $entity_browser = $form_state->getValue('entity_browser') == '_none' ? '' : $form_state->getValue('entity_browser'); 206 | $form_state->setValue('entity_browser', $entity_browser); 207 | $form_state->setValue('entity_browser_settings', $form_state->getValue('entity_browser_settings')); 208 | 209 | parent::submitConfigurationForm($form, $form_state); 210 | } 211 | 212 | /** 213 | * Builds a list of entity type options. 214 | * 215 | * Configuration entity types without a view builder are filtered out while 216 | * all other entity types are kept. 217 | * 218 | * @return array 219 | * An array of entity type labels, keyed by entity type name. 220 | */ 221 | protected function getEntityTypeOptions() { 222 | $options = $this->entityTypeRepository->getEntityTypeLabels(TRUE); 223 | 224 | foreach ($options as $group => $group_types) { 225 | foreach (array_keys($group_types) as $entity_type_id) { 226 | // Filter out entity types that do not have a view builder class. 227 | if (!$this->entityTypeManager->getDefinition($entity_type_id)->hasViewBuilderClass()) { 228 | unset($options[$group][$entity_type_id]); 229 | } 230 | // Filter out entity types that do not support UUIDs. 231 | if (!$this->entityTypeManager->getDefinition($entity_type_id)->hasKey('uuid')) { 232 | unset($options[$group][$entity_type_id]); 233 | } 234 | // Filter out entity types that will not have any Entity Embed Display 235 | // plugins. 236 | if (!$this->displayPluginManager->getDefinitionOptionsForEntityType($entity_type_id)) { 237 | unset($options[$group][$entity_type_id]); 238 | } 239 | } 240 | } 241 | 242 | return $options; 243 | } 244 | 245 | /** 246 | * Builds a list of entity type bundle options. 247 | * 248 | * Configuration entity types without a view builder are filtered out while 249 | * all other entity types are kept. 250 | * 251 | * @return array 252 | * An array of bundle labels, keyed by bundle name. 253 | */ 254 | protected function getEntityBundleOptions(EntityTypeInterface $entity_type) { 255 | $bundle_options = array(); 256 | // If the entity has bundles, allow option to restrict to bundle(s). 257 | if ($entity_type->hasKey('bundle')) { 258 | foreach ($this->entityTypeBundleInfo->getBundleInfo($entity_type->id()) as $bundle_id => $bundle_info) { 259 | $bundle_options[$bundle_id] = $bundle_info['label']; 260 | } 261 | natsort($bundle_options); 262 | } 263 | return $bundle_options; 264 | } 265 | 266 | /** 267 | * {@inheritdoc} 268 | */ 269 | public function getDefaultIconUrl() { 270 | return file_create_url(drupal_get_path('module', 'entity_embed') . '/js/plugins/drupalentity/entity.png'); 271 | } 272 | 273 | /** 274 | * {@inheritdoc} 275 | */ 276 | public function calculateDependencies() { 277 | $this->addDependencies(parent::calculateDependencies()); 278 | 279 | $entity_type_id = $this->getConfigurationValue('entity_type'); 280 | $entity_type = $this->entityTypeManager->getDefinition($entity_type_id); 281 | $this->addDependency('module', $entity_type->getProvider()); 282 | 283 | // Calculate bundle dependencies. 284 | foreach ($this->getConfigurationValue('bundles') as $bundle) { 285 | $bundle_dependency = $entity_type->getBundleConfigDependency($bundle); 286 | $this->addDependency($bundle_dependency['type'], $bundle_dependency['name']); 287 | } 288 | 289 | // Calculate display Entity Embed Display dependencies. 290 | foreach ($this->getConfigurationValue('display_plugins') as $display_plugin) { 291 | $instance = $this->displayPluginManager->createInstance($display_plugin); 292 | $this->calculatePluginDependencies($instance); 293 | } 294 | 295 | return $this->dependencies; 296 | } 297 | 298 | } 299 | -------------------------------------------------------------------------------- /src/Tests/EntityEmbedFilterTest.php: -------------------------------------------------------------------------------- 1 | node->id() . '" data-view-mode="teaser">This placeholder should not be rendered.
'; 35 | $settings = array(); 36 | $settings['type'] = 'page'; 37 | $settings['title'] = 'Test entity embed with entity-id and view-mode'; 38 | $settings['body'] = array(array('value' => $content, 'format' => 'custom_format')); 39 | $node = $this->drupalCreateNode($settings); 40 | $this->drupalGet('node/' . $node->id()); 41 | $this->assertNoRaw('assertText($this->node->body->value, 'Embedded node exists in page'); 43 | $this->assertNoText(strip_tags($content), 'Placeholder does not appear in the output when embed is successful.'); 44 | $this->assertRaw('
', 'Embed container found.'); 45 | 46 | // Tests that embedded entity is not rendered if not accessible. 47 | $this->node->setPublished(FALSE)->save(); 48 | $settings = []; 49 | $settings['type'] = 'page'; 50 | $settings['title'] = 'Test un-accessible entity embed with entity-id and view-mode'; 51 | $settings['body'] = [['value' => $content, 'format' => 'custom_format']]; 52 | $node = $this->drupalCreateNode($settings); 53 | $this->drupalGet('node/' . $node->id()); 54 | $this->assertNoRaw('assertNoText($this->node->body->value, 'Embedded node does not exist in the page.'); 56 | $this->assertNoText(strip_tags($content), 'Placeholder does not appear in the output when embed is successful.'); 57 | // Tests that embedded entity is displayed to the user who has the view 58 | // unpublished content permission. 59 | $this->createRole(['view own unpublished content'], 'access_unpublished'); 60 | $this->webUser->addRole('access_unpublished'); 61 | $this->webUser->save(); 62 | $this->drupalGet('node/' . $node->id()); 63 | $this->assertNoRaw('assertText($this->node->body->value, 'Embedded node exists in the page.'); 65 | $this->assertNoText(strip_tags($content), 'Placeholder does not appear in the output when embed is successful.'); 66 | $this->assertRaw('
', 'Embed container found.'); 67 | $this->webUser->removeRole('access_unpublished'); 68 | $this->webUser->save(); 69 | $this->node->setPublished(TRUE)->save(); 70 | 71 | // Tests entity embed using entity UUID and view mode. 72 | $content = 'This placeholder should not be rendered.'; 73 | $settings = array(); 74 | $settings['type'] = 'page'; 75 | $settings['title'] = 'Test entity embed with entity-uuid and view-mode'; 76 | $settings['body'] = array(array('value' => $content, 'format' => 'custom_format')); 77 | $node = $this->drupalCreateNode($settings); 78 | $this->drupalGet('node/' . $node->id()); 79 | $this->assertNoRaw('assertText($this->node->body->value, 'Embedded node exists in page.'); 81 | $this->assertNoText(strip_tags($content), 'Placeholder does not appear in the output when embed is successful.'); 82 | $this->assertRaw('
', 'Embed container found.'); 83 | $this->assertCacheTag('foo:' . $this->node->id()); 84 | 85 | // Ensure that placeholder is not replaced when embed is unsuccessful. 86 | $content = 'This placeholder should be rendered since specified entity does not exists.'; 87 | $settings = array(); 88 | $settings['type'] = 'page'; 89 | $settings['title'] = 'Test that placeholder is retained when specified entity does not exists'; 90 | $settings['body'] = array(array('value' => $content, 'format' => 'custom_format')); 91 | $node = $this->drupalCreateNode($settings); 92 | $this->drupalGet('node/' . $node->id()); 93 | $this->assertNoRaw('assertNoText(strip_tags($content), 'Placeholder does not appear in the output when embed is unsuccessful.'); 95 | 96 | // Ensure that UUID is preferred over ID when both attributes are present. 97 | $sample_node = $this->drupalCreateNode(); 98 | $content = 'This placeholder should not be rendered.'; 99 | $settings = array(); 100 | $settings['type'] = 'page'; 101 | $settings['title'] = 'Test that entity-uuid is preferred over entity-id when both attributes are present'; 102 | $settings['body'] = array(array('value' => $content, 'format' => 'custom_format')); 103 | $node = $this->drupalCreateNode($settings); 104 | $this->drupalGet('node/' . $node->id()); 105 | $this->assertNoRaw('assertText($this->node->body->value, 'Entity specifed with UUID exists in the page.'); 107 | $this->assertNoText($sample_node->body->value, 'Entity specifed with ID does not exists in the page.'); 108 | $this->assertNoText(strip_tags($content), 'Placeholder does not appear in the output when embed is successful.'); 109 | $this->assertRaw('
node->uuid() . '" data-entity-embed-display="default" data-entity-embed-display-settings=\'{"view_mode":"teaser"}\'>This placeholder should not be rendered.'; 113 | $settings = array(); 114 | $settings['type'] = 'page'; 115 | $settings['title'] = 'Test entity embed with entity-embed-display and data-entity-embed-display-settings'; 116 | $settings['body'] = array(array('value' => $content, 'format' => 'custom_format')); 117 | $node = $this->drupalCreateNode($settings); 118 | $this->drupalGet('node/' . $node->id()); 119 | $this->assertText($this->node->body->value, 'Embedded node exists in page.'); 120 | $this->assertNoText(strip_tags($content), 'Placeholder does not appear in the output when embed is successful.'); 121 | $this->assertRaw('
node->uuid() . '" data-entity-embed-display="default" data-entity-embed-display-settings=\'{"view_mode":"full"}\' data-view-mode="some-invalid-view-mode" data-align="left" data-caption="test caption">This placeholder should not be rendered.'; 126 | $settings = array(); 127 | $settings['type'] = 'page'; 128 | $settings['title'] = 'Test entity embed with entity-embed-display and data-entity-embed-display-settings'; 129 | $settings['body'] = array(array('value' => $content, 'format' => 'custom_format')); 130 | $node = $this->drupalCreateNode($settings); 131 | $this->drupalGet('node/' . $node->id()); 132 | $this->assertText($this->node->body->value, 'Embedded node exists in page with the view mode specified by entity-embed-settings.'); 133 | $this->assertNoText(strip_tags($content), 'Placeholder does not appear in the output when embed is successful.'); 134 | $this->assertRaw('
drupalGet('node/' . $this->node->id()); 138 | $this->assertNoRaw('data-align="left"', 'Align data attribute not found.'); 139 | $this->assertNoRaw('data-caption="test caption"', 'Caption data attribute not found.'); 140 | 141 | // Test that tag of container element is not replaced when it's not 142 | // . 143 | $content = 'this placeholder should not be rendered.'; 144 | $settings = array(); 145 | $settings['type'] = 'page'; 146 | $settings['title'] = 'test entity embed with entity-id and view-mode'; 147 | $settings['body'] = array(array('value' => $content, 'format' => 'custom_format')); 148 | $node = $this->drupalCreateNode($settings); 149 | $this->drupalget('node/' . $node->id()); 150 | $this->assertNoText($this->node->body->value, 'embedded node exists in page'); 151 | $this->assertRaw(''); 152 | $content = '
this placeholder should not be rendered.
'; 153 | $settings = array(); 154 | $settings['type'] = 'page'; 155 | $settings['title'] = 'test entity embed with entity-id and view-mode'; 156 | $settings['body'] = array(array('value' => $content, 'format' => 'custom_format')); 157 | $node = $this->drupalCreateNode($settings); 158 | $this->drupalget('node/' . $node->id()); 159 | $this->assertNoText($this->node->body->value, 'embedded node exists in page'); 160 | $this->assertRaw('
getTestFile('image'); 165 | $image->setPermanent(); 166 | $image->save(); 167 | $content = 'This placeholder should not be rendered.'; 168 | $settings = []; 169 | $settings['type'] = 'page'; 170 | $settings['title'] = 'test entity image formatter'; 171 | $settings['body'] = [['value' => $content, 'format' => 'custom_format']]; 172 | $node = $this->drupalCreateNode($settings); 173 | $this->drupalget('node/' . $node->id()); 174 | $this->assertRaw('assertRaw('data-caption="test caption"', 'Caption found.'); 176 | $this->assertRaw('data-align="left"', 'Alignment information found.'); 177 | $this->assertTrue((bool) $this->xpath("//img[@alt='This is alt text']"), 'Alt text found'); 178 | $this->assertTrue((bool) $this->xpath("//img[@title='This is title text']"), 'Title text found'); 179 | $this->assertRaw('
node->uuid() . '" data-entity-embed-display="entity_reference:entity_reference_label" data-entity-embed-settings=\'{"link":"0"}\' data-align="left" data-caption="test caption">This placeholder should not be rendered.'; 185 | $settings = []; 186 | $settings['type'] = 'page'; 187 | $settings['title'] = 'Test entity embed with data-entity-embed-settings'; 188 | $settings['body'] = [['value' => $content, 'format' => 'custom_format']]; 189 | $node = $this->drupalCreateNode($settings); 190 | $this->drupalGet('node/' . $node->id()); 191 | $this->assertText($this->node->getTitle(), 'Embeded node title is displayed.'); 192 | $this->assertNoLink($this->node->getTitle(), 'Embed settings are respected.'); 193 | $this->assertNoText($this->node->body->value, 'Embedded node exists in page.'); 194 | $this->assertNoText(strip_tags($content), 'Placeholder does not appear in the output when embed is successful.'); 195 | $this->assertRaw('
entityEmbedDisplayManager = $entity_embed_display_manager; 105 | $this->formBuilder = $form_builder; 106 | $this->entityTypeManager = $entity_type_manager; 107 | $this->eventDispatcher = $event_dispatcher; 108 | $this->entityFieldManager = $entity_field_manager; 109 | $this->moduleHandler = $module_handler; 110 | } 111 | 112 | /** 113 | * {@inheritdoc} 114 | */ 115 | public static function create(ContainerInterface $container) { 116 | return new static( 117 | $container->get('plugin.manager.entity_embed.display'), 118 | $container->get('form_builder'), 119 | $container->get('entity_type.manager'), 120 | $container->get('event_dispatcher'), 121 | $container->get('entity_field.manager'), 122 | $container->get('module_handler') 123 | ); 124 | } 125 | 126 | /** 127 | * {@inheritdoc} 128 | */ 129 | public function getFormId() { 130 | return 'entity_embed_dialog'; 131 | } 132 | 133 | /** 134 | * {@inheritdoc} 135 | * 136 | * @param \Drupal\editor\EditorInterface $editor 137 | * The editor to which this dialog corresponds. 138 | * @param \Drupal\embed\EmbedButtonInterface $embed_button 139 | * The URL button to which this dialog corresponds. 140 | */ 141 | public function buildForm(array $form, FormStateInterface $form_state, EditorInterface $editor = NULL, EmbedButtonInterface $embed_button = NULL) { 142 | $values = $form_state->getValues(); 143 | $input = $form_state->getUserInput(); 144 | // Set embed button element in form state, so that it can be used later in 145 | // validateForm() function. 146 | $form_state->set('embed_button', $embed_button); 147 | $form_state->set('editor', $editor); 148 | // Initialize entity element with form attributes, if present. 149 | $entity_element = empty($values['attributes']) ? array() : $values['attributes']; 150 | $entity_element += empty($input['attributes']) ? array() : $input['attributes']; 151 | // The default values are set directly from \Drupal::request()->request, 152 | // provided by the editor plugin opening the dialog. 153 | if (!$form_state->get('entity_element')) { 154 | $form_state->set('entity_element', isset($input['editor_object']) ? $input['editor_object'] : array()); 155 | } 156 | $entity_element += $form_state->get('entity_element'); 157 | $entity_element += [ 158 | 'data-entity-type' => $embed_button->getTypeSetting('entity_type'), 159 | 'data-entity-uuid' => '', 160 | 'data-entity-embed-display' => 'entity_reference:entity_reference_entity_view', 161 | 'data-entity-embed-display-settings' => isset($form_state->get('entity_element')['data-entity-embed-settings']) ? $form_state->get('entity_element')['data-entity-embed-settings'] : [], 162 | ]; 163 | $form_state->set('entity_element', $entity_element); 164 | $entity = $this->entityTypeManager->getStorage($entity_element['data-entity-type']) 165 | ->loadByProperties(['uuid' => $entity_element['data-entity-uuid']]); 166 | $form_state->set('entity', current($entity) ?: NULL); 167 | 168 | if (!$form_state->get('step')) { 169 | // If an entity has been selected, then always skip to the embed options. 170 | if ($form_state->get('entity')) { 171 | $form_state->set('step', 'embed'); 172 | } 173 | else { 174 | $form_state->set('step', 'select'); 175 | } 176 | } 177 | 178 | $form['#tree'] = TRUE; 179 | $form['#attached']['library'][] = 'editor/drupal.editor.dialog'; 180 | $form['#attached']['library'][] = 'entity_embed/drupal.entity_embed.dialog'; 181 | $form['#prefix'] = '
'; 182 | $form['#suffix'] = '
'; 183 | $form['#attributes']['class'][] = 'entity-embed-dialog-step--' . $form_state->get('step'); 184 | 185 | $this->loadEntityBrowser($form_state); 186 | 187 | if ($form_state->get('step') == 'select') { 188 | $form = $this->buildSelectStep($form, $form_state); 189 | } 190 | elseif ($form_state->get('step') == 'review') { 191 | $form = $this->buildReviewStep($form, $form_state); 192 | } 193 | elseif ($form_state->get('step') == 'embed') { 194 | $form = $this->buildEmbedStep($form, $form_state); 195 | } 196 | 197 | return $form; 198 | } 199 | 200 | /** 201 | * Form constructor for the entity selection step. 202 | * 203 | * @param array $form 204 | * An associative array containing the structure of the form. 205 | * @param \Drupal\Core\Form\FormStateInterface $form_state 206 | * The current state of the form. 207 | * 208 | * @return array 209 | * The form structure. 210 | */ 211 | public function buildSelectStep(array &$form, FormStateInterface $form_state) { 212 | // Entity element is calculated on every AJAX request/submit. See ::buildForm(). 213 | $entity_element = $form_state->get('entity_element'); 214 | /** @var \Drupal\embed\EmbedButtonInterface $embed_button */ 215 | $embed_button = $form_state->get('embed_button'); 216 | $entity = $form_state->get('entity'); 217 | 218 | $form['attributes']['data-entity-type'] = array( 219 | '#type' => 'value', 220 | '#value' => $entity_element['data-entity-type'], 221 | ); 222 | 223 | $label = $this->t('Label'); 224 | // Attempt to display a better label if we can by getting it from 225 | // the label field definition. 226 | $entity_type = $this->entityTypeManager->getDefinition($entity_element['data-entity-type']); 227 | if ($entity_type->isSubclassOf('\Drupal\Core\Entity\FieldableEntityInterface') && $entity_type->hasKey('label')) { 228 | $field_definitions = $this->entityFieldManager->getBaseFieldDefinitions($entity_type->id()); 229 | if (isset($field_definitions[$entity_type->getKey('label')])) { 230 | $label = $field_definitions[$entity_type->getKey('label')]->getLabel(); 231 | } 232 | } 233 | 234 | $form['#title'] = $this->t('Select @type to embed', array('@type' => $entity_type->getLowercaseLabel())); 235 | 236 | if ($this->entityBrowser) { 237 | $this->eventDispatcher->addListener(Events::REGISTER_JS_CALLBACKS, [$this, 'registerJSCallback']); 238 | $form['entity_browser'] = [ 239 | '#type' => 'entity_browser', 240 | '#entity_browser' => $this->entityBrowser->id(), 241 | '#cardinality' => 1, 242 | '#entity_browser_validators' => [ 243 | 'entity_type' => ['type' => $entity_element['data-entity-type']], 244 | ], 245 | ]; 246 | } 247 | else { 248 | $form['entity_id'] = array( 249 | '#type' => 'entity_autocomplete', 250 | '#target_type' => $entity_element['data-entity-type'], 251 | '#title' => $label, 252 | '#default_value' => $entity, 253 | '#required' => TRUE, 254 | '#description' => $this->t('Type label and pick the right one from suggestions. Note that the unique ID will be saved.'), 255 | ); 256 | if ($bundles = $embed_button->getTypeSetting('bundles')) { 257 | $form['entity_id']['#selection_settings']['target_bundles'] = $bundles; 258 | } 259 | } 260 | 261 | $form['attributes']['data-entity-uuid'] = array( 262 | '#type' => 'value', 263 | '#title' => $entity_element['data-entity-uuid'], 264 | ); 265 | $form['actions'] = array( 266 | '#type' => 'actions', 267 | ); 268 | 269 | $form['actions']['save_modal'] = array( 270 | '#type' => 'submit', 271 | '#value' => $this->t('Next'), 272 | '#button_type' => 'primary', 273 | // No regular submit-handler. This form only works via JavaScript. 274 | '#submit' => array(), 275 | '#ajax' => array( 276 | 'callback' => '::submitSelectStep', 277 | 'event' => 'click', 278 | ), 279 | '#attributes' => [ 280 | 'class' => [ 281 | 'js-button-next', 282 | ], 283 | ], 284 | ); 285 | 286 | return $form; 287 | } 288 | 289 | /** 290 | * Form constructor for the entity review step. 291 | * 292 | * @param array $form 293 | * An associative array containing the structure of the form. 294 | * @param \Drupal\Core\Form\FormStateInterface $form_state 295 | * The current state of the form. 296 | * 297 | * @return array 298 | * The form structure. 299 | */ 300 | public function buildReviewStep(array &$form, FormStateInterface $form_state) { 301 | /** @var \Drupal\Core\Entity\EntityInterface $entity */ 302 | $entity = $form_state->get('entity'); 303 | 304 | $form['#title'] = $this->t('Review selected @type', array('@type' => $entity->getEntityType()->getLowercaseLabel())); 305 | 306 | $form['selection'] = [ 307 | '#markup' => $entity->label(), 308 | ]; 309 | 310 | $form['actions'] = array( 311 | '#type' => 'actions', 312 | ); 313 | 314 | $form['actions']['back'] = array( 315 | '#type' => 'submit', 316 | '#value' => $this->t('Replace selection'), 317 | // No regular submit-handler. This form only works via JavaScript. 318 | '#submit' => array(), 319 | '#ajax' => array( 320 | 'callback' => '::submitAndShowSelect', 321 | 'event' => 'click', 322 | ), 323 | ); 324 | 325 | $form['actions']['save_modal'] = array( 326 | '#type' => 'submit', 327 | '#value' => $this->t('Next'), 328 | '#button_type' => 'primary', 329 | // No regular submit-handler. This form only works via JavaScript. 330 | '#submit' => array(), 331 | '#ajax' => array( 332 | 'callback' => '::submitAndShowEmbed', 333 | 'event' => 'click', 334 | ), 335 | '#attributes' => [ 336 | 'class' => [ 337 | 'js-button-next', 338 | ], 339 | ], 340 | ); 341 | 342 | return $form; 343 | } 344 | 345 | /** 346 | * Form constructor for the entity embedding step. 347 | * 348 | * @param array $form 349 | * An associative array containing the structure of the form. 350 | * @param \Drupal\Core\Form\FormStateInterface $form_state 351 | * The current state of the form. 352 | * 353 | * @return array 354 | * The form structure. 355 | */ 356 | public function buildEmbedStep(array $form, FormStateInterface $form_state) { 357 | // Entity element is calculated on every AJAX request/submit. See ::buildForm(). 358 | $entity_element = $form_state->get('entity_element'); 359 | /** @var \Drupal\embed\EmbedButtonInterface $embed_button */ 360 | $embed_button = $form_state->get('embed_button'); 361 | /** @var \Drupal\editor\EditorInterface $editor */ 362 | $editor = $form_state->get('editor'); 363 | /** @var \Drupal\Core\Entity\EntityInterface $entity */ 364 | $entity = $form_state->get('entity'); 365 | $values = $form_state->getValues(); 366 | 367 | $form['#title'] = $this->t('Embed @type', array('@type' => $entity->getEntityType()->getLowercaseLabel())); 368 | 369 | $entity_label = ''; 370 | try { 371 | $entity_label = $entity->link(); 372 | } 373 | catch (\Exception $e) { 374 | // Construct markup of the link to the entity manually if link() fails. 375 | // @see https://www.drupal.org/node/2402533 376 | $entity_label = '' . $entity->label() . ''; 377 | } 378 | 379 | $form['entity'] = array( 380 | '#type' => 'item', 381 | '#title' => $this->t('Selected entity'), 382 | '#markup' => $entity_label, 383 | ); 384 | $form['attributes']['data-entity-type'] = array( 385 | '#type' => 'hidden', 386 | '#value' => $entity_element['data-entity-type'], 387 | ); 388 | $form['attributes']['data-entity-uuid'] = array( 389 | '#type' => 'hidden', 390 | '#value' => $entity_element['data-entity-uuid'], 391 | ); 392 | 393 | // Build the list of allowed Entity Embed Display plugins. 394 | $display_plugin_options = $this->getDisplayPluginOptions($embed_button, $entity); 395 | 396 | // If the currently selected display is not in the available options, 397 | // use the first from the list instead. This can happen if an alter 398 | // hook customizes the list based on the entity. 399 | if (!isset($display_plugin_options[$entity_element['data-entity-embed-display']])) { 400 | $entity_element['data-entity-embed-display'] = key($display_plugin_options); 401 | } 402 | 403 | // The default Entity Embed Display plugin has been deprecated by the 404 | // rendered entity field formatter. 405 | if ($entity_element['data-entity-embed-display'] === 'default') { 406 | $entity_element['data-entity-embed-display'] = 'entity_reference:entity_reference_entity_view'; 407 | } 408 | 409 | $form['attributes']['data-entity-embed-display'] = array( 410 | '#type' => 'select', 411 | '#title' => $this->t('Display as'), 412 | '#options' => $display_plugin_options, 413 | '#default_value' => $entity_element['data-entity-embed-display'], 414 | '#required' => TRUE, 415 | '#ajax' => array( 416 | 'callback' => '::updatePluginConfigurationForm', 417 | 'wrapper' => 'data-entity-embed-display-settings-wrapper', 418 | 'effect' => 'fade', 419 | ), 420 | // Hide the selection if only one option is available. 421 | '#access' => count($display_plugin_options) > 1, 422 | ); 423 | $form['attributes']['data-entity-embed-display-settings'] = array( 424 | '#type' => 'container', 425 | '#prefix' => '
', 426 | '#suffix' => '
', 427 | ); 428 | $form['attributes']['data-embed-button'] = array( 429 | '#type' => 'value', 430 | '#value' => $embed_button->id(), 431 | ); 432 | $plugin_id = !empty($values['attributes']['data-entity-embed-display']) ? $values['attributes']['data-entity-embed-display'] : $entity_element['data-entity-embed-display']; 433 | if (!empty($plugin_id)) { 434 | if (is_string($entity_element['data-entity-embed-display-settings'])) { 435 | $entity_element['data-entity-embed-display-settings'] = Json::decode($entity_element['data-entity-embed-display-settings']); 436 | } 437 | $display = $this->entityEmbedDisplayManager->createInstance($plugin_id, $entity_element['data-entity-embed-display-settings']); 438 | $display->setContextValue('entity', $entity); 439 | $display->setAttributes($entity_element); 440 | $form['attributes']['data-entity-embed-display-settings'] += $display->buildConfigurationForm($form, $form_state); 441 | } 442 | 443 | // When Drupal core's filter_align is being used, the text editor may 444 | // offer the ability to change the alignment. 445 | if ($editor->getFilterFormat()->filters('filter_align')->status) { 446 | $form['attributes']['data-align'] = array( 447 | '#title' => $this->t('Align'), 448 | '#type' => 'radios', 449 | '#options' => array( 450 | '' => $this->t('None'), 451 | 'left' => $this->t('Left'), 452 | 'center' => $this->t('Center'), 453 | 'right' => $this->t('Right'), 454 | ), 455 | '#default_value' => isset($entity_element['data-align']) ? $entity_element['data-align'] : '', 456 | '#wrapper_attributes' => array('class' => array('container-inline')), 457 | '#attributes' => array('class' => array('container-inline')), 458 | ); 459 | } 460 | 461 | // When Drupal core's filter_caption is being used, the text editor may 462 | // offer the ability to add a caption. 463 | if ($editor->getFilterFormat()->filters('filter_caption')->status) { 464 | $form['attributes']['data-caption'] = array( 465 | '#title' => $this->t('Caption'), 466 | '#type' => 'textfield', 467 | '#default_value' => isset($entity_element['data-caption']) ? Html::decodeEntities($entity_element['data-caption']) : '', 468 | '#element_validate' => array('::escapeValue'), 469 | ); 470 | } 471 | 472 | $form['actions'] = array( 473 | '#type' => 'actions', 474 | ); 475 | $form['actions']['back'] = array( 476 | '#type' => 'submit', 477 | '#value' => $this->t('Back'), 478 | // No regular submit-handler. This form only works via JavaScript. 479 | '#submit' => array(), 480 | '#ajax' => array( 481 | 'callback' => !empty($this->entityBrowserSettings['display_review']) ? '::submitAndShowReview' : '::submitAndShowSelect', 482 | 'event' => 'click', 483 | ), 484 | ); 485 | $form['actions']['save_modal'] = array( 486 | '#type' => 'submit', 487 | '#value' => $this->t('Embed'), 488 | '#button_type' => 'primary', 489 | // No regular submit-handler. This form only works via JavaScript. 490 | '#submit' => array(), 491 | '#ajax' => array( 492 | 'callback' => '::submitEmbedStep', 493 | 'event' => 'click', 494 | ), 495 | ); 496 | 497 | return $form; 498 | } 499 | 500 | /** 501 | * {@inheritdoc} 502 | */ 503 | public function validateForm(array &$form, FormStateInterface $form_state) { 504 | parent::validateForm($form, $form_state); 505 | 506 | if ($form_state->get('step') == 'select') { 507 | $this->validateSelectStep($form, $form_state); 508 | } 509 | elseif ($form_state->get('step') == 'embed') { 510 | $this->validateEmbedStep($form, $form_state); 511 | } 512 | } 513 | 514 | /** 515 | * Form validation handler for the entity selection step. 516 | * 517 | * @param array $form 518 | * An associative array containing the structure of the form. 519 | * @param \Drupal\Core\Form\FormStateInterface $form_state 520 | * The current state of the form. 521 | */ 522 | public function validateSelectStep(array $form, FormStateInterface $form_state) { 523 | if ($form_state->hasValue(['entity_browser', 'entities'])) { 524 | $id = $form_state->getValue(['entity_browser', 'entities', 0])->id(); 525 | $element = $form['entity_browser']; 526 | } 527 | else { 528 | $id = trim($form_state->getValue(['entity_id'])); 529 | $element = $form['entity_id']; 530 | } 531 | 532 | $entity_type = $form_state->getValue(['attributes', 'data-entity-type']); 533 | 534 | if ($entity = $this->entityTypeManager->getStorage($entity_type)->load($id)) { 535 | if (!$entity->access('view')) { 536 | $form_state->setError($element, $this->t('Unable to access @type entity @id.', array('@type' => $entity_type, '@id' => $id))); 537 | } 538 | else { 539 | if ($uuid = $entity->uuid()) { 540 | $form_state->setValueForElement($form['attributes']['data-entity-uuid'], $uuid); 541 | } 542 | else { 543 | $form_state->setError($element, $this->t('Cannot embed @type entity @id because it does not have a UUID.', array('@type' => $entity_type, '@id' => $id))); 544 | } 545 | 546 | // Ensure that at least one Entity Embed Display plugin is present 547 | // before proceeding to the next step. Raise an error otherwise. 548 | $embed_button = $form_state->get('embed_button'); 549 | $display_plugin_options = $this->getDisplayPluginOptions($embed_button, $entity); 550 | // If no plugin is available after taking the intersection, raise error. 551 | // Also log an exception. 552 | if (empty($display_plugin_options)) { 553 | $form_state->setError($element, $this->t('No display options available for the selected entity. Please select another entity.')); 554 | $this->logger('entity_embed')->warning('No display options available for "@type:" entity "@id" while embedding using button "@button". Please ensure that at least one Entity Embed Display plugin is allowed for this embed button which is available for this entity.', array('@type' => $entity_type, '@id' => $entity->id(), '@button' => $embed_button->id())); 555 | } 556 | } 557 | } 558 | else { 559 | $form_state->setError($element, $this->t('Unable to load @type entity @id.', array('@type' => $entity_type, '@id' => $id))); 560 | } 561 | } 562 | 563 | /** 564 | * Form validation handler for the entity embedding step. 565 | * 566 | * @param array $form 567 | * An associative array containing the structure of the form. 568 | * @param \Drupal\Core\Form\FormStateInterface $form_state 569 | * The current state of the form. 570 | */ 571 | public function validateEmbedStep(array $form, FormStateInterface $form_state) { 572 | // Validate configuration forms for the Entity Embed Display plugin used. 573 | $entity_element = $form_state->getValue('attributes'); 574 | $entity = $this->entityTypeManager->getStorage($entity_element['data-entity-type']) 575 | ->loadByProperties(['uuid' => $entity_element['data-entity-uuid']]); 576 | $entity = current($entity) ?: NULL; 577 | $plugin_id = $entity_element['data-entity-embed-display']; 578 | $plugin_settings = !empty($entity_element['data-entity-embed-display-settings']) ? $entity_element['data-entity-embed-display-settings'] : array(); 579 | $display = $this->entityEmbedDisplayManager->createInstance($plugin_id, $plugin_settings); 580 | $display->setContextValue('entity', $entity); 581 | $display->setAttributes($entity_element); 582 | $display->validateConfigurationForm($form, $form_state); 583 | } 584 | 585 | /** 586 | * {@inheritdoc} 587 | */ 588 | public function submitForm(array &$form, FormStateInterface $form_state) {} 589 | 590 | /** 591 | * Form submission handler to update the plugin configuration form. 592 | * 593 | * @param array $form 594 | * The form array. 595 | * @param \Drupal\Core\Form\FormStateInterface $form_state 596 | * The form state. 597 | */ 598 | public function updatePluginConfigurationForm(array &$form, FormStateInterface $form_state) { 599 | return $form['attributes']['data-entity-embed-display-settings']; 600 | } 601 | 602 | /** 603 | * Form submission handler to to another step of the form. 604 | * 605 | * @param array $form 606 | * The form array. 607 | * @param \Drupal\Core\Form\FormStateInterface $form_state 608 | * The form state. 609 | * 610 | * @return \Drupal\Core\Ajax\AjaxResponse 611 | * The ajax response. 612 | */ 613 | public function submitStep(array &$form, FormStateInterface $form_state, $step) { 614 | $response = new AjaxResponse(); 615 | 616 | $form_state->set('step', $step); 617 | $form_state->setRebuild(TRUE); 618 | $rebuild_form = $this->formBuilder->rebuildForm('entity_embed_dialog', $form_state, $form); 619 | unset($rebuild_form['#prefix'], $rebuild_form['#suffix']); 620 | $response->addCommand(new HtmlCommand('#entity-embed-dialog-form', $rebuild_form)); 621 | $response->addCommand(new SetDialogTitleCommand('', $rebuild_form['#title'])); 622 | 623 | return $response; 624 | } 625 | 626 | /** 627 | * Form submission handler for the entity selection step. 628 | * 629 | * On success will send the user to the next step of the form to select the 630 | * embed display settings. On form errors, this will rebuild the form and 631 | * display the error messages. 632 | * 633 | * @param array $form 634 | * The form array. 635 | * @param \Drupal\Core\Form\FormStateInterface $form_state 636 | * The form state. 637 | * 638 | * @return \Drupal\Core\Ajax\AjaxResponse 639 | * The ajax response. 640 | */ 641 | public function submitSelectStep(array &$form, FormStateInterface $form_state) { 642 | $response = new AjaxResponse(); 643 | 644 | // Display errors in form, if any. 645 | if ($form_state->hasAnyErrors()) { 646 | unset($form['#prefix'], $form['#suffix']); 647 | $form['status_messages'] = array( 648 | '#type' => 'status_messages', 649 | '#weight' => -10, 650 | ); 651 | $response->addCommand(new HtmlCommand('#entity-embed-dialog-form', $form)); 652 | } 653 | else { 654 | $form_state->set('step', !empty($this->entityBrowserSettings['display_review']) ? 'review' : 'embed'); 655 | $form_state->setRebuild(TRUE); 656 | $rebuild_form = $this->formBuilder->rebuildForm('entity_embed_dialog', $form_state, $form); 657 | unset($rebuild_form['#prefix'], $rebuild_form['#suffix']); 658 | $response->addCommand(new HtmlCommand('#entity-embed-dialog-form', $rebuild_form)); 659 | $response->addCommand(new SetDialogTitleCommand('', $rebuild_form['#title'])); 660 | } 661 | 662 | return $response; 663 | } 664 | 665 | /** 666 | * Submit and show select step after submit. 667 | * 668 | * @param array $form 669 | * The form array. 670 | * @param \Drupal\Core\Form\FormStateInterface $form_state 671 | * The form state. 672 | * 673 | * @return \Drupal\Core\Ajax\AjaxResponse 674 | * The ajax response. 675 | */ 676 | public function submitAndShowSelect(array &$form, FormStateInterface $form_state) { 677 | return $this->submitStep($form, $form_state, 'select'); 678 | } 679 | 680 | /** 681 | * Submit and show review step after submit. 682 | * 683 | * @param array $form 684 | * The form array. 685 | * @param \Drupal\Core\Form\FormStateInterface $form_state 686 | * The form state. 687 | * 688 | * @return \Drupal\Core\Ajax\AjaxResponse 689 | * The ajax response. 690 | */ 691 | public function submitAndShowReview(array &$form, FormStateInterface $form_state) { 692 | return $this->submitStep($form, $form_state, 'review'); 693 | } 694 | 695 | /** 696 | * Submit and show embed step after submit. 697 | * 698 | * @param array $form 699 | * The form array. 700 | * @param \Drupal\Core\Form\FormStateInterface $form_state 701 | * The form state. 702 | * 703 | * @return \Drupal\Core\Ajax\AjaxResponse 704 | * The ajax response. 705 | */ 706 | public function submitAndShowEmbed(array $form, FormStateInterface $form_state) { 707 | return $this->submitStep($form, $form_state, 'embed'); 708 | } 709 | 710 | /** 711 | * Form submission handler for the entity embedding step. 712 | * 713 | * On success this will submit the command to save the embedded entity with 714 | * the configured display settings to the WYSIWYG element, and then close the 715 | * modal dialog. On form errors, this will rebuild the form and display the 716 | * error messages. 717 | * 718 | * @param array $form 719 | * An associative array containing the structure of the form. 720 | * @param FormStateInterface $form_state 721 | * An associative array containing the current state of the form. 722 | * 723 | * @return \Drupal\Core\Ajax\AjaxResponse 724 | * The ajax response. 725 | */ 726 | public function submitEmbedStep(array &$form, FormStateInterface $form_state) { 727 | $response = new AjaxResponse(); 728 | 729 | // Submit configuration form the selected Entity Embed Display plugin. 730 | $entity_element = $form_state->getValue('attributes'); 731 | $entity = $this->entityTypeManager->getStorage($entity_element['data-entity-type']) 732 | ->loadByProperties(['uuid' => $entity_element['data-entity-uuid']]); 733 | $entity = current($entity); 734 | $plugin_id = $entity_element['data-entity-embed-display']; 735 | $plugin_settings = !empty($entity_element['data-entity-embed-display-settings']) ? $entity_element['data-entity-embed-display-settings'] : array(); 736 | $display = $this->entityEmbedDisplayManager->createInstance($plugin_id, $plugin_settings); 737 | $display->setContextValue('entity', $entity); 738 | $display->setAttributes($entity_element); 739 | $display->submitConfigurationForm($form, $form_state); 740 | 741 | $values = $form_state->getValues(); 742 | // Display errors in form, if any. 743 | if ($form_state->hasAnyErrors()) { 744 | unset($form['#prefix'], $form['#suffix']); 745 | $form['status_messages'] = array( 746 | '#type' => 'status_messages', 747 | '#weight' => -10, 748 | ); 749 | $response->addCommand(new HtmlCommand('#entity-embed-dialog-form', $form)); 750 | } 751 | else { 752 | // Serialize entity embed settings to JSON string. 753 | if (!empty($values['attributes']['data-entity-embed-display-settings'])) { 754 | $values['attributes']['data-entity-embed-display-settings'] = Json::encode($values['attributes']['data-entity-embed-display-settings']); 755 | } 756 | 757 | // Filter out empty attributes. 758 | $values['attributes'] = array_filter($values['attributes'], function($value) { 759 | return (bool) Unicode::strlen((string) $value); 760 | }); 761 | 762 | // Allow other modules to alter the values before getting submitted to the WYSIWYG. 763 | $this->moduleHandler->alter('entity_embed_values', $values, $entity, $display, $form_state); 764 | 765 | $response->addCommand(new EditorDialogSave($values)); 766 | $response->addCommand(new CloseModalDialogCommand()); 767 | } 768 | 769 | return $response; 770 | } 771 | 772 | /** 773 | * Form element validation handler; Escapes the value an element. 774 | * 775 | * This should be used for any element in the embed form which may contain 776 | * HTML that should be serialized as an attribute element on the embed. 777 | */ 778 | public static function escapeValue($element, FormStateInterface $form_state) { 779 | if ($value = trim($element['#value'])) { 780 | $form_state->setValueForElement($element, Html::escape($value)); 781 | } 782 | } 783 | 784 | /** 785 | * Returns the allowed Entity Embed Display plugins given an embed button and 786 | * an entity. 787 | * 788 | * @param \Drupal\embed\EmbedButtonInterface $embed_button 789 | * The embed button. 790 | * @param \Drupal\Core\Entity\EntityInterface $entity 791 | * The entity. 792 | * 793 | * @return array 794 | * List of allowed Entity Embed Display plugins. 795 | */ 796 | public function getDisplayPluginOptions(EmbedButtonInterface $embed_button, EntityInterface $entity) { 797 | $plugins = $this->entityEmbedDisplayManager->getDefinitionOptionsForEntity($entity); 798 | 799 | if ($allowed_plugins = $embed_button->getTypeSetting('display_plugins')) { 800 | $plugins = array_intersect_key($plugins, array_flip($allowed_plugins)); 801 | } 802 | 803 | natsort($plugins); 804 | return $plugins; 805 | } 806 | 807 | /** 808 | * Registers JS callback that gets entities from entity browser and updates 809 | * form values accordingly. 810 | */ 811 | public function registerJSCallback(RegisterJSCallbacks $event) { 812 | if ($event->getBrowserID() == $this->entityBrowser->id()) { 813 | $event->registerCallback('Drupal.entityEmbedDialog.selectionCompleted'); 814 | } 815 | } 816 | 817 | /** 818 | * Load the current entity browser and its settings from the form state. 819 | * 820 | * @param \Drupal\Core\Form\FormStateInterface $form_state 821 | */ 822 | protected function loadEntityBrowser(FormStateInterface $form_state) { 823 | $this->entityBrowser = NULL; 824 | $this->entityBrowserSettings = []; 825 | 826 | /** @var \Drupal\embed\EmbedButtonInterface $embed_button */ 827 | $embed_button = $form_state->get('embed_button'); 828 | 829 | if ($embed_button && $entity_browser_id = $embed_button->getTypePlugin()->getConfigurationValue('entity_browser')) { 830 | $this->entityBrowser = $this->entityTypeManager->getStorage('entity_browser')->load($entity_browser_id); 831 | $this->entityBrowserSettings = $embed_button->getTypePlugin()->getConfigurationValue('entity_browser_settings'); 832 | } 833 | } 834 | 835 | } 836 | --------------------------------------------------------------------------------