layout_builder_ipe-1.0.x-dev/layout_builder_ipe.module
layout_builder_ipe.module
<?php
/**
* @file
* Module file for Layout Builder IPE.
*/
use Drupal\Core\Cache\Cache;
use Drupal\Core\Entity\ContentEntityForm;
use Drupal\Core\Entity\ContentEntityInterface;
use Drupal\Core\Entity\Display\EntityViewDisplayInterface;
use Drupal\Core\Entity\Entity\EntityViewDisplay;
use Drupal\Core\Entity\EntityInterface;
use Drupal\Core\Form\FormStateInterface;
use Drupal\Core\Routing\RouteMatchInterface;
use Drupal\layout_builder\Entity\LayoutEntityDisplayInterface;
use Drupal\layout_builder\Form\OverridesEntityForm;
use Drupal\layout_builder\Plugin\SectionStorage\OverridesSectionStorage;
/**
* Implements hook_help().
*/
function layout_builder_ipe_help($route_name, RouteMatchInterface $arg) {
switch ($route_name) {
case 'help.page.layout_builder_ipe':
$output = '';
$output .= '<h3>' . t('About') . '</h3>';
$output .= '<p>' . t('The Layout Builder IPE module provides frontend In-Place-Editing (IPE) for Layout Builder, similar to what Panels IPE used to do in Drupal 7.') . '</p>';
// Add a link to the Drupal.org project.
$output .= '<p>';
$output .= t('Visit the <a href=":project_link">Layout Builder IPE</a> on Drupal.org for more information.', [
':project_link' => 'https://www.drupal.org/project/layout_builder_ipe',
]);
$output .= '</p>';
return $output;
}
}
/**
* Implements hook_entity_view_alter().
*
* This attaches the IPE frontend to entity displays.
*/
function layout_builder_ipe_entity_view_alter(array &$build, EntityInterface $entity, EntityViewDisplayInterface $display) {
$current_route_name = \Drupal::routeMatch()->getRouteName();
if (!$entity->id() || !$entity->hasLinkTemplate('canonical') || !$entity->toUrl()->isRouted() || $current_route_name != $entity->toUrl()->getRouteName()) {
// We can only act on full page views on the canonical url.
return;
}
$layout_builder_ipe = layout_builder_ipe_service();
if ($entity != $layout_builder_ipe->getContentEntityFromRoute()) {
// This entity is not the routes main entity, so skip this.
return;
}
$section_storage = $layout_builder_ipe->getSectionStorage($display->getMode());
if (!$section_storage) {
return;
}
$can_override = $section_storage->getPluginId() == 'defaults' && $section_storage->isOverridable();
$is_overridden = $section_storage->getPluginId() == 'overrides';
if (!$can_override && !$is_overridden) {
return;
}
if (!$display->getThirdPartySetting('layout_builder_ipe', 'enabled', FALSE)) {
// Disabled in the display settings.
return;
}
$layout_builder_ipe->attachIpe($build, $section_storage, $entity);
}
/**
* Implements hook_preprocess_page().
*
* This attaches the IPE frontend to page manager pages.
*/
function layout_builder_ipe_preprocess_page(&$variables) {
$layout_builder_ipe = layout_builder_ipe_service();
$entity = $layout_builder_ipe->getEntity();
if (!$entity) {
return;
}
$section_storage = $layout_builder_ipe->getSectionStorageForEntity($entity);
if (!$section_storage || !$layout_builder_ipe->access($section_storage, $entity, FALSE)->isAllowed()) {
return;
}
// Check for gin_lb and add additional styling.
if ($layout_builder_ipe->isGinLb()) {
$variables['page']['content']['#attached']['library'][] = 'layout_builder_ipe/gin_lb';
if ($layout_builder_ipe->needsGinLegacy()) {
$variables['page']['content']['#attached']['library'][] = 'gin/legacy_css';
}
}
if ($section_storage->getPluginId() != 'page_manager') {
return;
}
// Some themes declare their content under an additional key, so let's try
// that.
$build = NULL;
$active_theme = $layout_builder_ipe->getActiveTheme();
if (array_key_exists($active_theme . '_content', $variables['page']['content'])) {
$build = &$variables['page']['content'][$active_theme . '_content'];
}
elseif (array_key_exists('content', $variables['page']['content'])) {
$build = &$variables['page']['content']['content'];
}
else {
// Otherwise we fall back to the only item we know.
$build = &$variables['page']['content'];
}
$layout_builder_ipe->attachIpe($build, $section_storage, $entity);
}
/**
* Implements hook_gin_lb_is_layout_builder_route_alter().
*
* Classify IPE routes as Layout Builder so that Gin LB get's attached.
*/
function layout_builder_ipe_gin_lb_is_layout_builder_route_alter(&$gin_lb_is_layout_builder_route, $context) {
$route_name = \Drupal::routeMatch()->getRouteName() ?? NULL;
if (!$route_name) {
return;
}
if (strpos($route_name, 'layout_builder_ipe.') === 0) {
$gin_lb_is_layout_builder_route = TRUE;
}
// Also check for POST data. If it has an op, we mark this as not being a
// layout builder route, because the display will return to the entity view.
// Without this, the secondary toolbar from Gin LB will stay visible on page
// manager pages.
if (strpos($route_name, 'layout_builder_ipe.page_variant.') === 0 && \Drupal::request()->request->has('op')) {
$gin_lb_is_layout_builder_route = FALSE;
}
}
/**
* Implements hook_gin_lb_show_toolbar_alter().
*
* Force IPE routes to use the toolbar.
*/
function layout_builder_ipe_gin_lb_show_toolbar_alter(&$gin_lb_show_toolbar) {
$route_name = \Drupal::routeMatch()->getRouteName() ?? NULL;
if ($route_name && strpos($route_name, 'layout_builder_ipe.') === 0) {
$gin_lb_show_toolbar = TRUE;
}
}
/**
* Implements hook_form_FORM_ID_alter().
*/
function layout_builder_ipe_form_entity_view_display_edit_form_alter(array &$form, FormStateInterface $form_state) {
/** @var \Drupal\layout_builder\Form\LayoutBuilderEntityViewDisplayForm $form_object */
$form_object = $form_state->getFormObject();
/** @var \Drupal\layout_builder\Entity\LayoutEntityDisplayInterface $display */
$display = $form_object->getEntity();
$entity_type = \Drupal::entityTypeManager()->getDefinition($display->getTargetEntityTypeId());
$form['layout']['layout_builder_ipe'] = [
'#type' => 'checkbox',
'#title' => t('Use the Layout Builder IPE frontend'),
'#description' => t('This only applies if the @type is displayed on its own page', [
'@type' => strtolower($entity_type->getLabel()),
]),
'#default_value' => $display->getThirdPartySetting('layout_builder_ipe', 'enabled', FALSE),
'#states' => [
'disabled' => [
':input[name="layout[allow_custom]"]' => ['checked' => FALSE],
],
'invisible' => [
':input[name="layout[enabled]"]' => ['checked' => FALSE],
],
],
'#access' => \Drupal::currentUser()->hasPermission('administer layout builder ipe'),
];
$form['#entity_builders'][] = 'layout_builder_ipe_form_entity_view_display_form_builder';
}
/**
* Entity builder for the entity view display form.
*/
function layout_builder_ipe_form_entity_view_display_form_builder(string $entity_type, LayoutEntityDisplayInterface $display, array &$form, FormStateInterface $form_state) {
$layout = $form_state->getValue(['layout']);
$layout_builder_enabled = !empty($layout['enabled']) && !empty($layout['allow_custom']) && !empty($layout['layout_builder_ipe']);
$display->setThirdPartySetting('layout_builder_ipe', 'enabled', $layout_builder_enabled);
}
/**
* Implements hook_ENTITY_TYPE_update().
*
* Clear the route cache, so that the access checks for the layout page can be
* applied immediately.
*/
function layout_builder_ipe_entity_view_display_update(EntityViewDisplay $entity) {
Cache::invalidateTags([
'local_task',
]);
\Drupal::service('router.builder')->rebuild();
}
/**
* Implements hook_form_alter().
*
* Modify forms for IPE enabled entities, so that we can identify these in
* post submission or pre save hooks, mostly to support the improved entity
* change detection and the features around concurrent editing.
*
* @see layout_builder_ipe_entity_presave()
*/
function layout_builder_ipe_form_alter(array &$form, FormStateInterface $form_state, $form_id) {
$form_object = $form_state->getFormObject();
if (!$form_object instanceof ContentEntityForm) {
return;
}
$entity = $form_object->getEntity();
if ($entity->isNew()) {
// For new entities we don't need to do anything.
return;
}
$layout_builder_ipe = layout_builder_ipe_service();
if (!$layout_builder_ipe->ipeEnabled($entity)) {
return;
}
// Get the original entity for comparison.
$original_entity = \Drupal::entityTypeManager()->getStorage($entity->getEntityTypeId())->loadUnchanged($entity->id());
// Add a token to identify this form as part of Layout Builder IPE.
$form['layout_builder_ipe_token'] = [
'#type' => 'hidden',
'#value' => $layout_builder_ipe->getEditToken($original_entity),
];
// Check if this is a layout builder form for entity overrides.
if ($form_object instanceof OverridesEntityForm) {
// Set some form handlers.
/** @var \Drupal\layout_builder_ipe\LayoutBuilder\LayoutBuilderSubmitForm $layout_builder_submit_form */
$layout_builder_submit_form = \Drupal::service('layout_builder_ipe.layout_builder_submit_form');
$layout_builder_submit_form->setSubmitFormHandler($form, $form_state);
/** @var \Drupal\layout_builder_ipe\LayoutBuilder\LayoutBuilderConfirmForm $layout_builder_confirm_form */
$layout_builder_confirm_form = \Drupal::service('layout_builder_ipe.layout_builder_confirm_form');
$layout_builder_confirm_form->setConfirmButtonHandler($form, $form_state, 'discard_changes');
$layout_builder_confirm_form->setConfirmButtonHandler($form, $form_state, 'revert');
$section_storage = $layout_builder_ipe->getSectionStorageForEntity($original_entity);
// Add a hash for the current layout state. This is used in pre save hooks
// and validations to determine whether the layout has been changed or not.
$form['layout_builder_ipe_layout_hash'] = [
'#type' => 'hidden',
'#value' => \Drupal::request()->get('layout_builder_ipe_layout_hash') ?? $layout_builder_ipe->hashLayout($section_storage),
];
}
elseif ($form_object instanceof ContentEntityForm) {
// Add a hash for the current entity state. This is used in pre save hooks
// and validations to determine whether the entity has been changed or not.
$form['layout_builder_ipe_entity_hash'] = [
'#type' => 'hidden',
'#value' => \Drupal::request()->get('layout_builder_ipe_entity_hash') ?? $layout_builder_ipe->hashEntity($original_entity),
];
}
}
/**
* Implements hook_form_FORM_ID_alter().
*
* This is used to make some modifications to the confirmable forms in the
* layout builder interface.
*/
function layout_builder_ipe_form_layout_builder_discard_changes_alter(array &$form, FormStateInterface $form_state) {
/** @var \Drupal\layout_builder_ipe\LayoutBuilder\LayoutBuilderConfirmForm $layout_builder_confirm_form */
$layout_builder_confirm_form = \Drupal::service('layout_builder_ipe.layout_builder_confirm_form');
$layout_builder_confirm_form->alterConfirmationForm($form, $form_state, 'layout_builder_discard_changes');
}
/**
* Implements hook_form_FORM_ID_alter().
*
* This is used to make some modifications to the confirmable forms in the
* layout builder interface.
*/
function layout_builder_ipe_form_layout_builder_revert_overrides_alter(array &$form, FormStateInterface $form_state) {
/** @var \Drupal\layout_builder_ipe\LayoutBuilder\LayoutBuilderConfirmForm $layout_builder_confirm_form */
$layout_builder_confirm_form = \Drupal::service('layout_builder_ipe.layout_builder_confirm_form');
$layout_builder_confirm_form->alterConfirmationForm($form, $form_state, 'layout_builder_revert_overrides');
}
/**
* Implements hook_form_FORM_ID_alter().
*
* This is used to make some modifications to the confirmable forms in the
* layout builder interface.
*/
function layout_builder_ipe_form_layout_builder_add_block_alter(array &$form, FormStateInterface $form_state) {
// Add our own submit handler.
$form['#submit'][] = 'layout_builder_ipe_add_block_form_submit';
}
/**
* Submit handler for the "Add block" form.
*/
function layout_builder_ipe_add_block_form_submit(array &$form, FormStateInterface $form_state) {
$layout_builder_ui = \Drupal::service('layout_builder_ipe.layout_builder_ui');
$layout_builder_ui->blockFormSubmit($form, $form_state);
}
/**
* Implements hook_entity_presave().
*
* Reset most entity data besides layouts to it's original values to account
* for the scenario where user A starts working on the layout of an entity,
* another user B edits the entity data (e.g. title, specific field values,
* ...) and saves the entity, and then user A saves it's layout changes. In
* this case we don't want the changes from user B to be overwritten by the
* outdated data of user A.
*/
function layout_builder_ipe_entity_presave(EntityInterface $entity) {
if (!$entity instanceof ContentEntityInterface) {
return;
}
if ($entity->isNew() || !isset($entity->original)) {
return;
}
/** @var */
$layout_builder_ipe = layout_builder_ipe_service();
if (!$layout_builder_ipe->ipeEnabled($entity) || !$layout_builder_ipe->useOverrideEntityChangedConstraint()) {
// IPE is not enabled, so ignore.
return;
}
if (!$layout_builder_ipe->isLayoutBuilderIpeFormSubmission($entity)) {
// This entity save operation is not the result of a form submission, so
// ignore this too. Otherwise programmatic updates to nodes won't be
// possible anymore.
return;
}
if (\Drupal::request()->get('layout_builder_ipe_layout_hash')) {
// This is an edit to the layout, se we reset all fields besides layout,
// changed and owner.
$exclude_fields = [
'changed',
'uid',
'vid',
OverridesSectionStorage::FIELD_NAME,
];
$fields = $entity->getFields();
foreach ($fields as $field) {
$field_name = $field->getName();
if (in_array($field_name, $exclude_fields)) {
continue;
}
$entity->$field_name = $entity->original->$field_name;
}
}
else {
// This is an edit to the node form, se we reset the layout.
$field_name = OverridesSectionStorage::FIELD_NAME;
$entity->$field_name = $entity->original->$field_name;
}
}
/**
* Implements hook_link_alter().
*
* This is used add a position argument to the add block links.
*/
function layout_builder_ipe_link_alter(&$variables) {
/** @var Drupal\Core\Url $url */
$url = $variables['url'];
if (!$url->isRouted() || $url->getRouteName() != 'layout_builder.add_block') {
return;
}
$position = \Drupal::request()->query->get('position');
if ($position === NULL) {
return;
}
$query = $variables['options']['query'] ?? [];
$query['position'] = $position;
$variables['options']['query'] = $query;
}
/**
* Implements hook_validation_constraint_alter().
*/
function layout_builder_ipe_validation_constraint_alter(array &$definitions) {
$layout_builder_ipe = layout_builder_ipe_service();
if (isset($definitions['EntityChanged']) && $layout_builder_ipe->useOverrideEntityChangedConstraint()) {
$definitions['EntityChanged']['class'] = 'Drupal\layout_builder_ipe\Validation\Constraint\LayoutBuilderEntityChangedConstraint';
}
}
/**
* Get the layout builder IPE service class.
*
* @return \Drupal\layout_builder_ipe\LayoutBuilderIpeService
* The layout builder IPE service.
*/
function layout_builder_ipe_service() {
return \Drupal::service('layout_builder_ipe');
}
