layout_builder_paragraphs-1.0.x-dev/layout_builder_paragraphs.module
layout_builder_paragraphs.module
<?php
/**
* @file
* Layout Builder Paragraphs module.
*/
use Drupal\Core\Form\FormStateInterface;
use Drupal\Core\Url;
use Drupal\layout_builder_perms\LayoutBuilderElement;
use Drupal\Core\Language\LanguageInterface;
/**
* Default paragraph field.
*
* @var string
*/
const PARAGRAPH_FIELD = 'field_contents';
/**
* Implements hook_plugin_filter_TYPE__CONSUMER_alter().
*
* No longer used; kept for backwards compatibility with modules still implementing
* hook_layout_builder_paragraphs_allowed_plugins() or
* hook_layout_builder_paragraphs_disallowed_plugins().
*/
function layout_builder_paragraphs_plugin_filter_block__layout_builder_alter(array &$definitions) {
// Default allow all, disallow none.
$allowed_plugin_ids = array_keys($definitions);
$disallowed_plugin_ids = [];
// Allow other modules to alter the array.
\Drupal::moduleHandler()->alter('layout_builder_paragraphs_allowed_plugins', $allowed_plugin_ids);
// Allow other modules to alter the array.
\Drupal::moduleHandler()->alter('layout_builder_paragraphs_disallowed_plugins', $disallowed_plugin_ids);
// First, filter the allowed blocks in layout builder.
$definitions = array_filter($definitions, function (string $plugin_id) use (&$allowed_plugin_ids): bool {
foreach ($allowed_plugin_ids as $allowed_plugin_id) {
if (preg_match("~{$allowed_plugin_id}~", $plugin_id)) {
return TRUE;
}
}
return FALSE;
}, ARRAY_FILTER_USE_KEY);
// Next, filter out the disallowed blocks.
$definitions = array_filter($definitions, function (string $plugin_id) use (&$disallowed_plugin_ids): bool {
foreach ($disallowed_plugin_ids as $disallowed_plugin_id) {
if (preg_match("~{$disallowed_plugin_id}~", $plugin_id)) {
return FALSE;
}
}
return TRUE;
}, ARRAY_FILTER_USE_KEY);
}
/**
* Implements hook_form_alter().
*/
function layout_builder_paragraphs_form_alter(&$form, FormStateInterface $form_state, $form_id) {
// Only alter the node edit form when the content language is default. This is
// related to the fact that paragraph field is symmetrically translated.
// @TODO: consider nulling this logic IF paragraphs_asymmetric_translation_widgets
// module is enabled.
if (!\Drupal::languageManager()->getCurrentLanguage(LanguageInterface::TYPE_CONTENT)->isDefault()) return;
$route = \Drupal::routeMatch()->getRouteName();
// Add the Save and edit layout button for node edit form with layout builder.
if ($route === 'node.add' || $route === 'entity.node.edit_form') {
if (!empty($form_state->getStorage()['form_display'])) {
// Check if this bundle allows custom layout builder per node.
$bundle = $form_state->getStorage()['form_display']->getTargetBundle();
$fields = \Drupal::service('entity_field.manager')->getFieldDefinitions('node', $bundle);
if (isset($fields['layout_builder__layout'])) {
$is_edit_form = $form_state->getFormObject()->getOperation() === 'edit';
// Clone submit button to a new button and set it as primary. Add new
// submit handler on top of those coming from the default submit button.
$form['actions']['save_and_edit_layout'] = $form['actions']['submit'];
$form['actions']['save_and_edit_layout']['#value'] = t('Save and edit layout');
$form['actions']['save_and_edit_layout']['#submit'][] = 'layout_builder_paragraphs_save_and_edit_layout';
// If it's an edit entity form, put the new button after the default
// Save button, remove primary class from the new button.
if ($is_edit_form) {
$form['actions']['save_and_edit_layout']['#weight'] = 10;
unset($form['actions']['save_and_edit_layout']['#button_type']);
}
// Otherwise it should be a create entity form. Put the new button as
// the first and primary button. Remove the primary class for the
// default Save button.
else {
$form['actions']['save_and_edit_layout']['#weight'] = -50;
if (!empty($form['actions']['submit']['#button_type']))
unset($form['actions']['submit']['#button_type']);
}
}
}
}
// When it is a quick node clone interface and there is PARAGRAPH_FIELD,
// disable PARAGRAPH_FIELD and provide explanation.
elseif ($route === 'quick_node_clone.node.quick_clone' && isset($form[PARAGRAPH_FIELD])) {
$form[PARAGRAPH_FIELD]['#disabled'] = TRUE;
$form[PARAGRAPH_FIELD]['quick_node_clone_warning'] = [
'#type' => 'markup',
'#weight' => -100,
'#markup' => t('Please proceed to save the cloned node first before editing this field.'),
];
$form['#attached']['library'][] = 'layout_builder_paragraphs/layout-builder-paragraphs-quick-node-clone';
}
}
/**
* Custom submit form to redirect node add and edit form to layout builder form.
*/
function layout_builder_paragraphs_save_and_edit_layout(array &$form, FormStateInterface $form_state) {
// Clear the way for the redirection to the layout builder page, by
// saving and unsetting the 'destination', if there is any.
$request = \Drupal::request();
if ($request->query->has('destination')) {
$request->query->get('destination');
$request->query->remove('destination');
}
// Set the redirect to layout builder.
$nid = $form_state->getValues()['nid'];
$layout_builder_url = Url::fromRoute('layout_builder.overrides.node.view', ['node' => $nid]);
$form_state->setRedirectUrl($layout_builder_url);
}
/**
* Implements hook_field_widget_WIDGET_ID_form_alter().
* @TODO: check if this is invalid. The code is taken from
* https://github.com/nathandentzau/layout-builder-examples but the hook may
* not be valid. The hook somehow works though.
*/
function layout_builder_paragraphs_field_widget_layout_builder_widget_form_alter(array &$element) {
$element['#type'] = 'modal_layout_builder';
}
/**
* Implements hook_contextual_links_alter().
*/
function layout_builder_paragraphs_contextual_links_alter(array &$links, string $group, array $route_parameters) {
if ($group !== 'layout_builder_block') {
return;
}
foreach ($links as &$link) {
$link['localized_options']['attributes']['data-dialog-type'] = 'modal';
unset($link['localized_options']['attributes']['data-dialog-renderer']);
}
}
/**
* Implements hook_element_info_alter().
*/
function layout_builder_paragraphs_element_info_alter(array &$element) {
// Apply the layout_builder_perms preRender method when the relevant two
// modules are enabled.
if (!\Drupal::moduleHandler()->moduleExists('layout_builder_suite') ||
!\Drupal::moduleHandler()->moduleExists('layout_builder_perms')) return;
$element['modal_layout_builder']['#pre_render'][] = [LayoutBuilderElement::class, 'preRender'];
}
/**
* Implements hook_field_widget_WIDGET_TYPE_form_alter().
*
* Used for D8, D9.
*/
function layout_builder_paragraphs_field_widget_paragraphs_form_alter(&$element, FormStateInterface $form_state, $context) {
_layout_builder_paragraphs_process_paragraphs_field_widget($element, $form_state, $context);
}
/**
* Implements hook_field_widget_single_element_WIDGET_TYPE_form_alter().
*/
function layout_builder_paragraphs_field_widget_single_element_paragraphs_form_alter(&$element, FormStateInterface $form_state, $context) {
_layout_builder_paragraphs_process_paragraphs_field_widget($element, $form_state, $context);
}
/**
* Identify unused paragraph deltas in the layout.
*/
function _layout_builder_paragraphs_process_paragraphs_field_widget(&$element, FormStateInterface $form_state, $context) {
// Only proceed when field name matches PARAGRAPH_FIELD.
if ($context['items']->getFieldDefinition()->getName() != PARAGRAPH_FIELD) return;
// Only proceed when delta has not been processed.
$id = $context['items']->getFieldDefinition()->getName() . '_' . $context['delta'] . '_processed';
if ($form_state->get($id)) return;
// First, extract all the paragraph deltas used in the layout.
// Check if paragraph_deltas_used_in_layout is already set in form_state.
if ($deltas = $form_state->get('paragraph_deltas_used_in_layout')) {
$paragraph_deltas_used_in_layout = $deltas;
}
else {
$paragraph_deltas_used_in_layout = [];
$paragraph = $context['items']->get($context['delta']);
$node = $paragraph->getEntity();
// Only proceed if node has layout.
if (empty($node->layout_builder__layout)) return;
// Iterate through each section and its components.
foreach ($node->layout_builder__layout->getSections() as $section) {
foreach ($section->getComponents() as $component) {
// If the component is a paragraph_blocks plugin, extract the delta.
$prefix = 'paragraph_field:node:' . PARAGRAPH_FIELD . ':';
$plugin_id = $component->getPluginId();
if (strpos($plugin_id, $prefix) === 0) {
$paragraph_deltas_used_in_layout[substr($plugin_id, strlen($prefix), 1)] = TRUE;
}
}
}
$form_state->set('paragraph_deltas_used_in_layout', $paragraph_deltas_used_in_layout);
}
// Next, if the delta is not used in the layout, add special class.
if (!isset($paragraph_deltas_used_in_layout[$context['delta']])) {
$element['#attributes']['class'][] = 'missing-delta-in-layout';
$element['#attached']['library'][] = 'layout_builder_paragraphs/layout-builder-paragraphs-missing-delta';
$description = t('This paragraph is not used in the layout.');
$element['#suffix'] = '<div class="missing-delta-description">' . $description . '</div>' . $element['#suffix'];
// Indicate in form state that this delta has been processed.
$form_state->set($id, TRUE);
}
}
