layout_builder.module 19 KB
Newer Older
1 2 3 4 5 6 7
<?php

/**
 * @file
 * Provides hook implementations for Layout Builder.
 */

8 9
use Drupal\Core\Breadcrumb\Breadcrumb;
use Drupal\Core\Cache\CacheableMetadata;
10
use Drupal\Core\Entity\EntityInterface;
11
use Drupal\Core\Entity\FieldableEntityInterface;
12
use Drupal\Core\Form\FormStateInterface;
13
use Drupal\Core\Link;
14
use Drupal\Core\Render\Element;
15 16
use Drupal\Core\Routing\RouteMatchInterface;
use Drupal\Core\Url;
17 18 19
use Drupal\field\FieldConfigInterface;
use Drupal\layout_builder\Entity\LayoutBuilderEntityViewDisplay;
use Drupal\layout_builder\Entity\LayoutBuilderEntityViewDisplayStorage;
20
use Drupal\layout_builder\Form\DefaultsEntityForm;
21
use Drupal\layout_builder\Form\LayoutBuilderEntityViewDisplayForm;
22
use Drupal\layout_builder\Form\OverridesEntityForm;
23 24
use Drupal\Core\Entity\Display\EntityViewDisplayInterface;
use Drupal\layout_builder\Plugin\Block\ExtraFieldBlock;
25 26 27
use Drupal\layout_builder\InlineBlockEntityOperations;
use Drupal\Core\Session\AccountInterface;
use Drupal\Core\Access\AccessResult;
28
use Drupal\layout_builder\Plugin\SectionStorage\OverridesSectionStorage;
29
use Drupal\layout_builder\QuickEditIntegration;
30 31 32 33

/**
 * Implements hook_help().
 */
34 35 36 37 38 39 40 41 42 43
function layout_builder_help($route_name, RouteMatchInterface $route_match) {
  // Add help text to the Layout Builder UI.
  if ($route_match->getRouteObject()->getOption('_layout_builder')) {
    $output = '<p>' . t('This layout builder tool allows you to configure the layout of the main content area.') . '</p>';
    if (\Drupal::currentUser()->hasPermission('administer blocks')) {
      $output .= '<p>' . t('To manage other areas of the page, use the <a href="@block-ui">block administration page</a>.', ['@block-ui' => Url::fromRoute('block.admin_display')->toString()]) . '</p>';
    }
    else {
      $output .= '<p>' . t('To manage other areas of the page, use the block administration page.') . '</p>';
    }
44
    $output .= '<p>' . t('Forms and links inside the content of the layout builder tool have been disabled.') . '</p>';
45 46 47
    return $output;
  }

48 49 50
  switch ($route_name) {
    case 'help.page.layout_builder':
      $output = '<h3>' . t('About') . '</h3>';
51 52 53 54 55 56 57 58
      $output .= '<p>' . t('Layout Builder allows you to use layouts to customize how content, custom blocks, and other <a href=":field_help" title="Field module help, with background on content entities">content entities</a> are displayed.', [':field_help' => Url::fromRoute('help.page', ['name' => 'field'])->toString()]) . '</p>';
      $output .= '<p>' . t('For more information, see the <a href=":layout-builder-documentation">online documentation for the Layout Builder module</a>.', [':layout-builder-documentation' => 'https://www.drupal.org/docs/8/core/modules/layout-builder']) . '</p>';
      $output .= '<h3>' . t('Uses') . '</h3>';
      $output .= '<dl>';
      $output .= '<dt>' . t('Default layouts') . '</dt>';
      $output .= '<dd>' . t('Layout Builder can be selectively enabled on the "Manage Display" page in the <a href=":field_ui">Field UI</a>. This allows you to control the output of each type of display individually. For example, a "Basic page" might have view modes such as Full and Teaser, with each view mode having different layouts selected.', [':field_ui' => Url::fromRoute('help.page', ['name' => 'field_ui'])->toString()]) . '</dd>';
      $output .= '<dt>' . t('Overridden layouts') . '</dt>';
      $output .= '<dd>' . t('If enabled, each individual content item can have a custom layout. Once the layout for an individual content item is overridden, changes to the Default layout will no longer affect it. Overridden layouts may be reverted to return to matching and being synchronized to their Default layout.') . '</dd>';
59 60 61 62 63
      $output .= '<dt>' . t('User permissions') . '</dt>';
      $output .= '<dd>' . t('The Layout Builder module makes a number of permissions available, which can be set by role on the <a href=":permissions">permissions page</a>. For more information, see the <a href=":layout-builder-permissions">Configuring Layout Builder permissions</a> online documentation.', [
        ':permissions' => Url::fromRoute('user.admin_permissions', [], ['fragment' => 'module-layout_builder'])->toString(),
        ':layout-builder-permissions' => 'https://www.drupal.org/docs/8/core/modules/layout-builder/configuring-layout-builder-permissions',
      ]) . '</dd>';
64
      $output .= '</dl>';
65 66 67 68 69 70 71 72 73
      return $output;
  }
}

/**
 * Implements hook_entity_type_alter().
 */
function layout_builder_entity_type_alter(array &$entity_types) {
  /** @var \Drupal\Core\Entity\EntityTypeInterface[] $entity_types */
74 75 76
  $entity_types['entity_view_display']
    ->setClass(LayoutBuilderEntityViewDisplay::class)
    ->setStorageClass(LayoutBuilderEntityViewDisplayStorage::class)
77
    ->setFormClass('layout_builder', DefaultsEntityForm::class)
78
    ->setFormClass('edit', LayoutBuilderEntityViewDisplayForm::class);
79 80 81 82 83 84 85

  // Ensure every fieldable entity type has a layout form.
  foreach ($entity_types as $entity_type) {
    if ($entity_type->entityClassImplements(FieldableEntityInterface::class)) {
      $entity_type->setFormClass('layout_builder', OverridesEntityForm::class);
    }
  }
86 87 88
}

/**
89
 * Implements hook_form_FORM_ID_alter() for \Drupal\field_ui\Form\EntityFormDisplayEditForm.
90
 */
91 92 93
function layout_builder_form_entity_form_display_edit_form_alter(&$form, FormStateInterface $form_state) {
  // Hides the Layout Builder field. It is rendered directly in
  // \Drupal\layout_builder\Entity\LayoutBuilderEntityViewDisplay::buildMultiple().
94 95
  unset($form['fields'][OverridesSectionStorage::FIELD_NAME]);
  $key = array_search(OverridesSectionStorage::FIELD_NAME, $form['#fields']);
96 97 98 99 100 101
  if ($key !== FALSE) {
    unset($form['#fields'][$key]);
  }
}

/**
102
 * Implements hook_field_config_insert().
103
 */
104 105
function layout_builder_field_config_insert(FieldConfigInterface $field_config) {
  // Clear the sample entity for this entity type and bundle.
106 107
  $sample_entity_generator = \Drupal::service('layout_builder.sample_entity_generator');
  $sample_entity_generator->delete($field_config->getTargetEntityTypeId(), $field_config->getTargetBundle());
108
  \Drupal::service('plugin.manager.block')->clearCachedDefinitions();
109 110 111
}

/**
112
 * Implements hook_field_config_delete().
113
 */
114 115
function layout_builder_field_config_delete(FieldConfigInterface $field_config) {
  // Clear the sample entity for this entity type and bundle.
116 117
  $sample_entity_generator = \Drupal::service('layout_builder.sample_entity_generator');
  $sample_entity_generator->delete($field_config->getTargetEntityTypeId(), $field_config->getTargetBundle());
118
  \Drupal::service('plugin.manager.block')->clearCachedDefinitions();
119
}
120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136

/**
 * Implements hook_entity_view_alter().
 *
 * ExtraFieldBlock block plugins add placeholders for each extra field which is
 * configured to be displayed. Those placeholders are replaced by this hook.
 * Modules that implement hook_entity_extra_field_info() use their
 * implementations of hook_entity_view_alter() to add the rendered output of
 * the extra fields they provide, so we cannot get the rendered output of extra
 * fields before this point in the view process.
 * layout_builder_module_implements_alter() moves this implementation of
 * hook_entity_view_alter() to the end of the list.
 *
 * @see \Drupal\layout_builder\Plugin\Block\ExtraFieldBlock::build()
 * @see layout_builder_module_implements_alter()
 */
function layout_builder_entity_view_alter(array &$build, EntityInterface $entity, EntityViewDisplayInterface $display) {
137 138 139
  // Only replace extra fields when Layout Builder has been used to alter the
  // build. See \Drupal\layout_builder\Entity\LayoutBuilderEntityViewDisplay::buildMultiple().
  if (isset($build['_layout_builder']) && !Element::isEmpty($build['_layout_builder'])) {
140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155
    /** @var \Drupal\Core\Entity\EntityFieldManagerInterface $field_manager */
    $field_manager = \Drupal::service('entity_field.manager');
    $extra_fields = $field_manager->getExtraFields($entity->getEntityTypeId(), $entity->bundle());
    if (!empty($extra_fields['display'])) {
      foreach ($extra_fields['display'] as $field_name => $extra_field) {
        // If the extra field is not set replace with an empty array to avoid
        // the placeholder text from being rendered.
        $replacement = isset($build[$field_name]) ? $build[$field_name] : [];
        ExtraFieldBlock::replaceFieldPlaceholder($build, $replacement, $field_name);
        // After the rendered field in $build has been copied over to the
        // ExtraFieldBlock block we must remove it from its original location or
        // else it will be rendered twice.
        unset($build[$field_name]);
      }
    }
  }
156 157 158 159 160 161 162 163 164

  $route_name = \Drupal::routeMatch()->getRouteName();

  // If the entity is displayed within a Layout Builder block and the current
  // route is in the Layout Builder UI, then remove all contextual link
  // placeholders.
  if ($display instanceof LayoutBuilderEntityViewDisplay && strpos($route_name, 'layout_builder.') === 0) {
    unset($build['#contextual_links']);
  }
165 166 167 168 169 170

  if (\Drupal::moduleHandler()->moduleExists('quickedit')) {
    /** @var \Drupal\layout_builder\QuickEditIntegration $quick_edit_integration */
    $quick_edit_integration = \Drupal::classResolver(QuickEditIntegration::class);
    $quick_edit_integration->entityViewAlter($build, $entity, $display);
  }
171 172 173 174 175 176 177 178 179 180 181 182
}

/**
 * Implements hook_entity_build_defaults_alter().
 */
function layout_builder_entity_build_defaults_alter(array &$build, EntityInterface $entity, $view_mode) {
  // Contextual links are removed for entities viewed in Layout Builder's UI.
  // The route.name.is_layout_builder_ui cache context accounts for this
  // difference.
  // @see layout_builder_entity_view_alter()
  // @see \Drupal\layout_builder\Cache\LayoutBuilderUiCacheContext
  $build['#cache']['contexts'][] = 'route.name.is_layout_builder_ui';
183 184 185
}

/**
186
 * Implements hook_module_implements_alter().
187 188 189 190 191 192 193 194 195 196 197
 */
function layout_builder_module_implements_alter(&$implementations, $hook) {
  if ($hook === 'entity_view_alter') {
    // Ensure that this module's implementation of hook_entity_view_alter() runs
    // last so that other modules that use this hook to render extra fields will
    // run before it.
    $group = $implementations['layout_builder'];
    unset($implementations['layout_builder']);
    $implementations['layout_builder'] = $group;
  }
}
198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231

/**
 * Implements hook_entity_presave().
 */
function layout_builder_entity_presave(EntityInterface $entity) {
  if (\Drupal::moduleHandler()->moduleExists('block_content')) {
    /** @var \Drupal\layout_builder\InlineBlockEntityOperations $entity_operations */
    $entity_operations = \Drupal::classResolver(InlineBlockEntityOperations::class);
    $entity_operations->handlePreSave($entity);
  }
}

/**
 * Implements hook_entity_delete().
 */
function layout_builder_entity_delete(EntityInterface $entity) {
  if (\Drupal::moduleHandler()->moduleExists('block_content')) {
    /** @var \Drupal\layout_builder\InlineBlockEntityOperations $entity_operations */
    $entity_operations = \Drupal::classResolver(InlineBlockEntityOperations::class);
    $entity_operations->handleEntityDelete($entity);
  }
}

/**
 * Implements hook_cron().
 */
function layout_builder_cron() {
  if (\Drupal::moduleHandler()->moduleExists('block_content')) {
    /** @var \Drupal\layout_builder\InlineBlockEntityOperations $entity_operations */
    $entity_operations = \Drupal::classResolver(InlineBlockEntityOperations::class);
    $entity_operations->removeUnused();
  }
}

232 233 234 235 236 237 238 239
/**
 * Implements hook_plugin_filter_TYPE__CONSUMER_alter().
 */
function layout_builder_plugin_filter_block__layout_builder_alter(array &$definitions, array $extra) {
  // @todo Restore the page title block in https://www.drupal.org/node/2938129.
  unset($definitions['page_title_block']);
}

240 241 242 243 244 245
/**
 * Implements hook_plugin_filter_TYPE_alter().
 */
function layout_builder_plugin_filter_block_alter(array &$definitions, array $extra, $consumer) {
  // @todo Determine the 'inline_block' blocks should be allowed outside
  //   of layout_builder https://www.drupal.org/node/2979142.
246
  if ($consumer !== 'layout_builder' || !isset($extra['list']) || $extra['list'] !== 'inline_blocks') {
247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265
    foreach ($definitions as $id => $definition) {
      if ($definition['id'] === 'inline_block') {
        unset($definitions[$id]);
      }
    }
  }
}

/**
 * Implements hook_ENTITY_TYPE_access().
 */
function layout_builder_block_content_access(EntityInterface $entity, $operation, AccountInterface $account) {
  /** @var \Drupal\block_content\BlockContentInterface $entity */
  if ($operation === 'view' || $entity->isReusable() || empty(\Drupal::service('inline_block.usage')->getUsage($entity->id()))) {
    // If the operation is 'view' or this is reusable block or if this is
    // non-reusable that isn't used by this module then don't alter the access.
    return AccessResult::neutral();
  }

266
  if ($account->hasPermission('create and edit custom blocks')) {
267 268 269 270
    return AccessResult::allowed();
  }
  return AccessResult::forbidden();
}
271 272 273 274 275 276

/**
 * Implements hook_plugin_filter_TYPE__CONSUMER_alter().
 */
function layout_builder_plugin_filter_block__block_ui_alter(array &$definitions, array $extra) {
  foreach ($definitions as $id => $definition) {
277 278 279 280 281 282
    // Filter out any layout_builder-provided block that has required context
    // definitions.
    if ($definition['provider'] === 'layout_builder' && !empty($definition['context_definitions'])) {
      /** @var \Drupal\Core\Plugin\Context\ContextDefinitionInterface $context_definition */
      foreach ($definition['context_definitions'] as $context_definition) {
        if ($context_definition->isRequired()) {
283 284 285 286 287 288 289
          unset($definitions[$id]);
          break;
        }
      }
    }
  }
}
290

291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321
/**
 * Implements hook_plugin_filter_TYPE__CONSUMER_alter().
 */
function layout_builder_plugin_filter_layout__layout_builder_alter(array &$definitions, array $extra) {
  // Remove layouts provide by layout discovery that are not needed because of
  // layouts provided by this module.
  $duplicate_layouts = [
    'layout_twocol',
    'layout_twocol_bricks',
    'layout_threecol_25_50_25',
    'layout_threecol_33_34_33',
  ];

  foreach ($duplicate_layouts as $duplicate_layout) {
    /** @var \Drupal\Core\Layout\LayoutDefinition[] $definitions */
    if (isset($definitions[$duplicate_layout])) {
      if ($definitions[$duplicate_layout]->getProvider() === 'layout_discovery') {
        unset($definitions[$duplicate_layout]);
      }
    }
  }

  // Move the one column layout to the top.
  if (isset($definitions['layout_onecol']) && $definitions['layout_onecol']->getProvider() === 'layout_discovery') {
    $one_col = $definitions['layout_onecol'];
    unset($definitions['layout_onecol']);
    $definitions = [
      'layout_onecol' => $one_col,
    ] + $definitions;
  }
}
322

323 324 325 326 327 328 329 330
/**
 * Implements hook_plugin_filter_TYPE_alter().
 */
function layout_builder_plugin_filter_layout_alter(array &$definitions, array $extra, $consumer) {
  // Hide the blank layout plugin from listings.
  unset($definitions['layout_builder_blank']);
}

331 332 333 334 335 336 337 338
/**
 * Implements hook_system_breadcrumb_alter().
 */
function layout_builder_system_breadcrumb_alter(Breadcrumb &$breadcrumb, RouteMatchInterface $route_match, array $context) {
  // Remove the extra 'Manage display' breadcrumb for Layout Builder defaults.
  if ($route_match->getRouteObject()->hasOption('_layout_builder') && $route_match->getParameter('section_storage_type') === 'defaults') {
    $links = array_filter($breadcrumb->getLinks(), function (Link $link) use ($route_match) {
      $entity_type_id = $route_match->getParameter('entity_type_id');
339 340 341
      if (!$link->getUrl()->isRouted()) {
        return TRUE;
      }
342 343 344 345 346 347 348 349 350 351
      return $link->getUrl()->getRouteName() !== "entity.entity_view_display.$entity_type_id.default";
    });
    // Links cannot be removed from an existing breadcrumb object. Create a new
    // object but carry over the cacheable metadata.
    $cacheability = CacheableMetadata::createFromObject($breadcrumb);
    $breadcrumb = new Breadcrumb();
    $breadcrumb->setLinks($links);
    $breadcrumb->addCacheableDependency($cacheability);
  }
}
352 353 354 355 356 357 358 359 360

/**
 * Implements hook_quickedit_render_field().
 */
function layout_builder_quickedit_render_field(EntityInterface $entity, $field_name, $view_mode_id, $langcode) {
  /** @var \Drupal\layout_builder\QuickEditIntegration $quick_edit_integration */
  $quick_edit_integration = \Drupal::classResolver(QuickEditIntegration::class);
  return $quick_edit_integration->quickEditRenderField($entity, $field_name, $view_mode_id, $langcode);
}
361 362 363 364 365 366 367 368 369 370 371 372

/**
 * Implements hook_entity_translation_create().
 */
function layout_builder_entity_translation_create(EntityInterface $translation) {
  /** @var \Drupal\Core\Entity\FieldableEntityInterface $translation */
  if ($translation->hasField(OverridesSectionStorage::FIELD_NAME) && $translation->getFieldDefinition(OverridesSectionStorage::FIELD_NAME)->isTranslatable()) {
    // When creating a new translation do not copy untranslated sections because
    // per-language layouts are not supported.
    $translation->set(OverridesSectionStorage::FIELD_NAME, []);
  }
}
373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 402 403 404 405 406 407 408 409 410

/**
 * Implements hook_theme_registry_alter().
 */
function layout_builder_theme_registry_alter(&$theme_registry) {
  // Move our preprocess to run after
  // content_translation_preprocess_language_content_settings_table().
  if (!empty($theme_registry['language_content_settings_table']['preprocess functions'])) {
    $preprocess_functions = &$theme_registry['language_content_settings_table']['preprocess functions'];
    $index = array_search('layout_builder_preprocess_language_content_settings_table', $preprocess_functions);
    if ($index !== FALSE) {
      unset($preprocess_functions[$index]);
      $preprocess_functions[] = 'layout_builder_preprocess_language_content_settings_table';
    }
  }
}

/**
 * Implements hook_preprocess_HOOK() for language-content-settings-table.html.twig.
 */
function layout_builder_preprocess_language_content_settings_table(&$variables) {
  foreach ($variables['build']['#rows'] as &$row) {
    if (isset($row['#field_name']) && $row['#field_name'] === OverridesSectionStorage::FIELD_NAME) {
      // Rebuild the label to include a warning about using translations with
      // layouts.
      $row['data'][1]['data']['field'] = [
        'label' => $row['data'][1]['data']['field'],
        'description' => [
          '#type' => 'container',
          '#markup' => t('<strong>Warning</strong>: Layout Builder does not support translating layouts. (<a href="https://www.drupal.org/docs/8/core/modules/layout-builder/layout-builder-and-content-translation">online documentation</a>)'),
          '#attributes' => [
            'class' => ['layout-builder-translation-warning'],
          ],
        ],
      ];
    }
  }
}
411 412 413 414 415 416 417 418 419 420 421 422

/**
 * Implements hook_theme_suggestions_HOOK_alter().
 */
function layout_builder_theme_suggestions_field_alter(&$suggestions, array $variables) {
  $element = $variables['element'];
  if (isset($element['#third_party_settings']['layout_builder']['view_mode'])) {
    // See system_theme_suggestions_field().
    $suggestions[] = 'field__' . $element['#entity_type'] . '__' . $element['#field_name'] . '__' . $element['#bundle'] . '__' . $element['#third_party_settings']['layout_builder']['view_mode'];
  }
  return $suggestions;
}