layout_paragraphs-1.0.x-dev/layout_paragraphs.module
layout_paragraphs.module
<?php
/**
* @file
* Contains layout_paragraphs.module.
*/
use Drupal\Core\Url;
use Drupal\Component\Utility\Html;
use Drupal\Component\Serialization\Json;
use Drupal\Core\Form\FormStateInterface;
use Drupal\paragraphs\ParagraphInterface;
use Drupal\Core\Routing\RouteMatchInterface;
use Drupal\layout_paragraphs\Utility\Dialog;
use Drupal\paragraphs\Entity\ParagraphsType;
use Drupal\Core\Entity\FieldableEntityInterface;
/**
* Implements hook_help().
*/
function layout_paragraphs_help($route_name, RouteMatchInterface $route_match) {
$output = '';
switch ($route_name) {
// Main module help for the layout_paragraphs module.
case 'help.page.layout_paragraphs':
$output = '';
$output .= '<h3>' . t('About') . '</h3>';
$output .= '<p>' . t('Provide layout integration for paragraph fields.') . '</p>';
break;
}
return $output;
}
/**
* Implements hook_theme().
*/
function layout_paragraphs_theme() {
return [
'layout_paragraphs' => [
'variables' => [
'elements' => '',
'content' => '',
],
],
'layout_paragraphs_builder' => [
'variables' => [
'attributes' => [],
'id' => '',
'root_components' => [],
'is_empty' => FALSE,
'empty_message' => '',
'insert_button' => '',
'translation_warning' => '',
'layout_paragraphs_layout' => NULL,
],
],
'layout_paragraphs_builder_formatter' => [
'variables' => [
'link_url' => NULL,
'link_text' => NULL,
'field_label' => NULL,
'is_empty' => FALSE,
'root_components' => [],
],
],
'layout_paragraphs_builder_controls' => [
'variables' => [
'attributes' => [],
'controls' => [],
'layout_paragraphs_layout' => NULL,
'uuid' => NULL,
'edit_access' => FALSE,
'duplicate_access' => FALSE,
'delete_access' => FALSE,
],
],
'layout_paragraphs_insert_component_btn' => [
'variables' => [
'title' => NULL,
'weight' => NULL,
'attributes' => [],
'url' => NULL,
],
],
'layout_paragraphs_builder_component_menu' => [
'variables' => [
'attributes' => [],
'types' => NULL,
'empty_message' => NULL,
'status_messages' => NULL,
],
],
];
}
/**
* Implements hook_theme_suggestions().
*/
function layout_paragraphs_theme_suggestions_layout_paragraphs(array $variables) {
$suggestions = [];
$sanitized_view_mode = strtr($variables['elements']['#view_mode'], '.', '_');
$suggestions[] = 'layout_paragraphs__' . $sanitized_view_mode;
$suggestions[] = 'layout_paragraphs__' . $variables['elements']['field_name'];
$suggestions[] = 'layout_paragraphs__' . $variables['elements']['field_name'] . '__' . $sanitized_view_mode;
return $suggestions;
}
/**
* Implements hook_module_implements_alter().
*
* If "content_translation", move the form_alter implementation by the
* layout_paragraphs at the end of the list, so that it might be
* called after the content_translation one.
* Otherwise the $form['translatable'] won't be defined in
* layout_paragraphs_form_field_config_edit_form_alter.
*
* @see: https://www.hashbangcode.com/article/drupal-8-altering-hook-weights.
*/
function layout_paragraphs_module_implements_alter(&$implementations, $hook) {
// Move our hook_entity_type_alter() implementation to the end of the list.
if ($hook == 'form_alter' && isset($implementations['layout_paragraphs']) && isset($implementations['content_translation'])) {
$hook_init = $implementations['layout_paragraphs'];
unset($implementations['layout_paragraphs']);
$implementations['layout_paragraphs'] = $hook_init;
}
}
/**
* Implements hook_preprocess_radios().
*
* Add wrapper class for layout selection.
*/
function layout_paragraphs_preprocess_radios(&$variables) {
if (isset($variables['element']['#wrapper_attributes'])) {
$variables['attributes'] += $variables['element']['#wrapper_attributes'];
}
}
/**
* Implements hook_preprocess_layout_paragraphs_builder_controls().
*
* @todo Consider adding an alter/info hook for altering this output.
*/
function layout_paragraphs_preprocess_layout_paragraphs_builder_controls(&$variables) {
static $show_layout_plugin_labels = NULL;
if ($show_layout_plugin_labels === NULL) {
$settings = \Drupal::config('layout_paragraphs.settings');
$show_layout_plugin_labels = \Drupal::config('layout_paragraphs.settings')
->get('show_layout_plugin_labels');
}
/** @var \Drupal\layout_paragraphs\LayoutParagraphsLayout $layout */
$layout = $variables['layout_paragraphs_layout'];
$uuid = $variables['uuid'];
$component = $layout->getComponentByUuid($uuid);
$entity = $component->getEntity();
$id = Html::getUniqueId('lpb-controls');
$variables['controls'] += [
'drag_handle' => [
'#type' => 'link',
'#title' => t('Drag'),
'#url' => Url::fromUri('internal:#move'),
'#attributes' => [
'class' => [
'lpb-drag',
'lpb-tooltip--hover',
'lpb-tooltip--focus',
],
'aria-describedby' => $id . '--tip',
],
'#weight' => 10,
],
'label' => [
'#type' => 'html_tag',
'#tag' => 'span',
'#attributes' => [
'class' => ['lpb-controls-label'],
],
'#value' => $entity->getParagraphType()->get('label'),
'#weight' => 30,
],
'move_up' => [
'#type' => 'link',
'#title' => t('Move up'),
'#url' => Url::fromUri('internal:#move-up'),
'#attributes' => [
'class' => ['lpb-up'],
'title' => t('Move up'),
],
'#weight' => 40,
],
'move_down' => [
'#type' => 'link',
'#url' => Url::fromUri('internal:#move-down'),
'#title' => t('Move down'),
'#attributes' => [
'class' => ['lpb-down'],
'title' => t('Move down'),
],
'#weight' => 50,
],
'edit_link' => [
'#type' => 'link',
'#url' => Url::fromRoute('layout_paragraphs.builder.edit_item', [
'layout_paragraphs_layout' => $layout->id(),
'component_uuid' => $entity->uuid(),
]),
'#title' => t('Edit'),
'#attributes' => [
'class' => [
'lpb-edit',
'use-ajax',
],
'data-dialog-type' => 'dialog',
'data-dialog-options' => Json::encode(Dialog::dialogSettings($layout)),
'title' => t('Edit'),
],
'#access' => $variables['edit_access'],
'#weight' => 60,
],
'duplicate_link' => [
'#type' => 'link',
'#url' => Url::fromRoute('layout_paragraphs.builder.duplicate_item', [
'layout_paragraphs_layout' => $layout->id(),
'source_uuid' => $entity->uuid(),
]),
'#title' => t('Duplicate'),
'#attributes' => [
'class' => [
'lpb-duplicate',
'use-ajax',
],
'title' => t('Duplicate'),
],
'#access' => $variables['duplicate_access'],
'#weight' => 65,
],
'delete_link' => [
'#type' => 'link',
'#url' => Url::fromRoute('layout_paragraphs.builder.delete_item', [
'layout_paragraphs_layout' => $layout->id(),
'component_uuid' => $entity->uuid(),
]),
'#title' => t('Delete'),
'#attributes' => [
'class' => [
'lpb-delete',
'use-ajax',
],
'data-dialog-type' => 'dialog',
'data-dialog-options' => Json::encode([
'modal' => TRUE,
'target' => Dialog::dialogId($layout),
'dialogClass' => 'lpb-dialog',
'width' => 'auto',
]),
'title' => t('Delete'),
],
'#access' => $variables['delete_access'],
'#weight' => 70,
],
];
if ($component->isLayout()) {
if ($show_layout_plugin_labels) {
$section = $layout->getLayoutSection($entity);
$layout_plugin_id = $section->getLayoutId();
$definition = \Drupal::service('plugin.manager.core.layout')->getDefinition($layout_plugin_id);
$variables['controls']['label']['#value'] .= ' - ' . $definition->getLabel();
}
$variables['attributes']['class'][] = 'is-layout';
}
}
/**
* Implements hook_entity_extra_field_info().
*
* Adds the layout paragraphs form fields to "Manage form display" tab.
*/
function layout_paragraphs_entity_extra_field_info() {
$extra = [];
foreach (ParagraphsType::loadMultiple() as $paragraphs_type) {
if ($paragraphs_type->hasEnabledBehaviorPlugin('layout_paragraphs')) {
$extra['paragraph'][$paragraphs_type->id()]['form']['layout_paragraphs_fields'] = [
'label' => t('Layout selection and configuration form'),
'visible' => TRUE,
'weight' => -200,
];
}
}
return $extra;
}
/**
* Implements hook_form_FORM_ID_alter().
*/
function layout_paragraphs_form_layout_paragraphs_component_form_alter(&$form, FormStateInterface $form_state) {
$display = $form['#display'];
if ($layout_paragraphs_fields = $display->getComponent('layout_paragraphs_fields')) {
$form['layout_paragraphs']['#weight'] = $layout_paragraphs_fields['weight'];
}
}
/**
* Adjusts the field weight based on settings from "Manage form display" tab.
*/
function _layout_paragraphs_form_field_weights(array &$form, FormStateInterface $form_state) {
$display = $form['#display'];
if ($layout_paragraphs_fields = $display->getComponent('layout_paragraphs_fields')) {
$form['layout_paragraphs']['#weight'] = $layout_paragraphs_fields['weight'];
}
}
/**
* Implements hook_ENTITY_presave().
*
* Updates references to parent layout section uuids in cases
* where paragraphs are duplicated, for example when using the
* Replicate module.
*
* @todo Consider changing approach if this issue is addressed in core:
* https://www.drupal.org/project/drupal/issues/3040556
* (It is not possible to react to an entity being duplicated)
*/
function layout_paragraphs_paragraph_presave(ParagraphInterface $paragraph) {
$behavior_settings = $paragraph->getAllBehaviorSettings();
$parent_uuid = $behavior_settings['layout_paragraphs']['parent_uuid'] ?? NULL;
if (empty($parent_uuid)) {
return;
}
// If there is no host, the parent hasn't been saved and we are not cloning.
$host = $paragraph->getParentEntity();
if (empty($host)) {
return;
}
// If the parent component cannot be loaded, we are not cloning.
/** @var \Drupal\paragraphs\Entity\Paragraph|NULL $parent_component */
$parent_component = \Drupal::service('entity.repository')
->loadEntityByUuid('paragraph', $parent_uuid);
if (empty($parent_component)) {
return;
}
// If the parent component does not have a parent entity, we are not cloning.
$parent_host = $parent_component->getParentEntity();
if (empty($parent_host)) {
return;
}
// If the current paragraph's host's id does not match
// the paragraph's parent's host's id, we ARE cloning.
if ($host->id() !== $parent_host->id()) {
$uuid_map = [];
// Map the UUIDs to deltas from the clone source.
$items = _layout_paragraphs_get_paragraphs($parent_component);
/** @var \Drupal\paragraphs\Entity\Paragraph $item */
foreach ($items as $delta => $item) {
$uuid_map[$item->uuid()] = $delta;
}
// Map the deltas to UUIds from the clone destination.
$items = _layout_paragraphs_get_paragraphs($paragraph);
$delta_map = [];
/** @var \Drupal\paragraphs\Entity\Paragraph $item */
foreach ($items as $delta => $item) {
$delta_map[$delta] = $item->uuid();
}
// Assign the correct uuid.
$parent_delta = $uuid_map[$parent_uuid];
$correct_uuid = $delta_map[$parent_delta];
// Since paragraph::preSave() has already been called,
// we have to set the serialized behavior settings directly
// rather than using setBehaviorSettings().
$behavior_settings['layout_paragraphs']['parent_uuid'] = $correct_uuid;
$paragraph->set('behavior_settings', serialize($behavior_settings));
}
}
/**
* Implements hook_entity_translation_create().
*
* Supports asynchronous translations by duplicating paragraphs referenced
* by an entity reference revisions field that is translatable, updating parent
* uuid references in layouts.
*/
function layout_paragraphs_entity_translation_create(FieldableEntityInterface $entity) {
$entity_type_manager = \Drupal::entityTypeManager();
/** @var \Drupal\Core\Entity\RevisionableStorageInterface $paragraph_storage */
$paragraph_storage = $entity_type_manager->getStorage('paragraph');
/** @var \Drupal\field\FieldConfigInterface[] $field_definitions */
$field_definitions = $entity_type_manager->getStorage('field_config')
->loadByProperties([
'entity_type' => $entity->getEntityTypeId(),
'bundle' => $entity->bundle(),
'field_type' => 'entity_reference_revisions',
]);
$langcode_key = $entity->getEntityType()->getKey('langcode');
$langcode = $entity->get($langcode_key)->value;
foreach ($field_definitions as $field_definition) {
if ($field_definition->isTranslatable() === FALSE) {
continue;
}
if ($field_definition->getFieldStorageDefinition()->getSetting('target_type') !== 'paragraph') {
continue;
}
$async_values = [];
$values = $entity->get($field_definition->getName())->getValue() ?? [];
// Get the list of referenced paragraphs using the available property.
$paragraphs = array_map(function ($value) use ($paragraph_storage) {
if (isset($value['entity'])) {
$paragraph = $value['entity'];
}
elseif (isset($value['target_revision_id'])) {
$paragraph = $paragraph_storage->loadRevision($value['target_revision_id']);
}
elseif (isset($value['target_id'])) {
$paragraph = $paragraph_storage->load($value['target_id']);
}
return $paragraph ?? NULL;
}, $values);
// Save an array of the original uuids.
$original_uuids = array_map(function ($paragraph) {
return $paragraph->uuid();
}, $paragraphs);
// Duplicate the paragraphs.
$duplicates = array_map(function ($paragraph) use ($langcode) {
$duplicate = $paragraph->createDuplicate();
$langcode_key = $duplicate->getEntityType()->getKey('langcode');
$duplicate->set($langcode_key, $langcode);
return $duplicate;
}, $paragraphs);
// Get an array of new uuids so we can map references to parents uuids.
$new_uuids = array_map(function ($paragraph) {
return $paragraph->uuid();
}, $duplicates);
// Map old uuids to new uuids.
$uuid_map = array_combine($original_uuids, $new_uuids);
// Update references to the correct, new parent uuids in the
// behavior settings.
$async_values = array_map(function ($paragraph) use ($uuid_map) {
$behavior_settings = $paragraph->getAllBehaviorSettings();
$parent_uuid = $behavior_settings['layout_paragraphs']['parent_uuid'] ?? NULL;
if (!empty($parent_uuid)) {
$behavior_settings['layout_paragraphs']['parent_uuid'] = $uuid_map[$parent_uuid];
$paragraph->setAllBehaviorSettings($behavior_settings);
}
return $paragraph;
}, $duplicates);
$entity->set($field_definition->getName(), $async_values);
}
}
/**
* Returns a list of all sibling paragraphs given a single paragraph.
*
* The returned list contains all of the referenced entities,
* including the passed $paragraph.
*
* @param \Drupal\paragraphs\ParagraphInterface $paragraph
* The paragraph to use for finding the complete list of siblings.
*
* @return array
* The list of paragraph entities.
*/
function _layout_paragraphs_get_paragraphs(ParagraphInterface $paragraph) {
$host = $paragraph->getParentEntity();
$parent_field = $paragraph->get('parent_field_name');
$field_name = $parent_field->first()->getString();
/** @var \Drupal\Core\Field\EntityReferenceFieldItemList $item_list */
$item_list = $host->get($field_name);
return $item_list->referencedEntities();
}
/**
* Implements hook_library_info_alter().
*/
function layout_paragraphs_library_info_alter(&$libraries, $extension) {
if ($extension == 'gin') {
if (isset($libraries['layout_paragraphs'])) {
unset($libraries['layout_paragraphs']['css']['component']);
}
if (isset($libraries['layout_paragraphs2'])) {
unset($libraries['layout_paragraphs2']['css']['component']);
}
}
if ($extension !== 'layout_paragraphs') {
return;
}
foreach ($libraries as $name => &$library) {
if (!isset($library['cdn']) || !isset($library['directory'])) {
continue;
}
if (\Drupal::service('library.libraries_directory_file_finder')->find($name)) {
continue;
}
layout_paragraphs_alter_library_recursive($library, $library['cdn']);
}
}
/**
* Alter layout_paragraph libraries.
*
* @param array $library
* A layout_paragraphs library defined in layout_paragraphs.libraries.yml.
* @param array $cdn
* A associative array of library paths mapped to CDN URL.
*/
function layout_paragraphs_alter_library_recursive(array &$library, array $cdn) {
foreach ($library as $key => &$value) {
// CSS and JS files are listed in associative arrays keyed via string.
if (!is_string($key) || !is_array($value)) {
continue;
}
// Ignore the CDN's associative array.
if ($key === 'cdn') {
continue;
}
// Replace the CDN sources (i.e. /library/*) with the CDN URL destination
// (https://cdnjs.cloudflare.com/ajax/libs/*).
foreach ($cdn as $source => $destination) {
if (strpos($key, $source) === 0) {
$uri = str_replace($source, $destination, $key);
$library[$uri] = $value;
$library[$uri]['type'] = 'external';
unset($library[$key]);
break;
}
}
// Recurse downward to find nested libraries.
layout_paragraphs_alter_library_recursive($value, $cdn);
}
}
