first commit

This commit is contained in:
2024-07-15 12:33:27 +02:00
commit ce50ae282b
22084 changed files with 2623791 additions and 0 deletions

View File

@@ -0,0 +1,51 @@
<?php
namespace Drupal\layout_builder\Access;
use Drupal\Core\Access\AccessResult;
use Drupal\Core\Cache\RefinableCacheableDependencyInterface;
use Drupal\Core\Routing\Access\AccessInterface;
use Drupal\Core\Session\AccountInterface;
use Drupal\layout_builder\SectionStorageInterface;
use Symfony\Component\Routing\Route;
/**
* Provides an access check for the Layout Builder defaults.
*
* @ingroup layout_builder_access
*
* @internal
* Tagged services are internal.
*/
class LayoutBuilderAccessCheck implements AccessInterface {
/**
* Checks routing access to the layout.
*
* @param \Drupal\layout_builder\SectionStorageInterface $section_storage
* The section storage.
* @param \Drupal\Core\Session\AccountInterface $account
* The current user.
* @param \Symfony\Component\Routing\Route $route
* The route to check against.
*
* @return \Drupal\Core\Access\AccessResultInterface
* The access result.
*/
public function access(SectionStorageInterface $section_storage, AccountInterface $account, Route $route) {
$operation = $route->getRequirement('_layout_builder_access');
$access = $section_storage->access($operation, $account, TRUE);
// Check for the global permission unless the section storage checks
// permissions itself.
if (!$section_storage->getPluginDefinition()->get('handles_permission_check')) {
$access = $access->andIf(AccessResult::allowedIfHasPermission($account, 'configure any layout'));
}
if ($access instanceof RefinableCacheableDependencyInterface) {
$access->addCacheableDependency($section_storage);
}
return $access;
}
}

View File

@@ -0,0 +1,28 @@
<?php
namespace Drupal\layout_builder\Access;
use Drupal\Core\Access\AccessibleInterface;
use Drupal\Core\Access\AccessResult;
use Drupal\Core\Session\AccountInterface;
/**
* Accessible class to allow access for inline blocks in the Layout Builder.
*
* @internal
* Tagged services are internal.
*/
class LayoutPreviewAccessAllowed implements AccessibleInterface {
/**
* {@inheritdoc}
*/
public function access($operation, ?AccountInterface $account = NULL, $return_as_object = FALSE) {
if ($operation === 'view') {
return $return_as_object ? AccessResult::allowed() : TRUE;
}
// The layout builder preview should only need 'view' access.
return $return_as_object ? AccessResult::forbidden() : FALSE;
}
}

View File

@@ -0,0 +1,70 @@
<?php
namespace Drupal\layout_builder\Annotation;
use Drupal\Component\Annotation\Plugin;
use Drupal\layout_builder\SectionStorage\SectionStorageDefinition;
/**
* Defines a Section Storage type annotation object.
*
* @see \Drupal\layout_builder\SectionStorage\SectionStorageManager
* @see plugin_api
*
* @Annotation
*/
class SectionStorage extends Plugin {
/**
* The plugin ID.
*
* @var string
*/
public $id;
/**
* The plugin weight, optional (defaults to 0).
*
* When an entity with layout is rendered, section storage plugins are
* checked, in order of their weight, to determine which one should be used
* to render the layout.
*
* @var int
*/
public $weight = 0;
/**
* Any required context definitions, optional.
*
* When an entity with layout is rendered, all section storage plugins which
* match a particular set of contexts are checked, in order of their weight,
* to determine which plugin should be used to render the layout.
*
* @var array
*
* @see \Drupal\layout_builder\SectionStorage\SectionStorageManagerInterface::findByContext()
*/
public $context_definitions = [];
/**
* Indicates that this section storage handles its own permission checking.
*
* If FALSE, the 'configure any layout' permission will be required during
* routing access. If TRUE, Layout Builder will not enforce any access
* restrictions for the storage, so the section storage's implementation of
* access() must perform the access checking itself. Defaults to FALSE.
*
* @var bool
*
* @see \Drupal\layout_builder\Access\LayoutBuilderAccessCheck
*/
public $handles_permission_check = FALSE;
/**
* {@inheritdoc}
*/
public function get() {
return new SectionStorageDefinition($this->definition);
}
}

View File

@@ -0,0 +1,65 @@
<?php
namespace Drupal\layout_builder\Attribute;
use Drupal\Component\Plugin\Attribute\Plugin;
use Drupal\layout_builder\SectionStorage\SectionStorageDefinition;
/**
* Defines a SectionStorage attribute.
*
* Plugin Namespace: Plugin\SectionStorage
*
* @see \Drupal\layout_builder\SectionStorage\SectionStorageManager
* @see plugin_api
*/
#[\Attribute(\Attribute::TARGET_CLASS)]
class SectionStorage extends Plugin {
/**
* Constructs a SectionStorage attribute.
*
* @param string $id
* The plugin ID.
* @param int $weight
* (optional) The plugin weight.
* When an entity with layout is rendered, section storage plugins are
* checked, in order of their weight, to determine which one should be used
* to render the layout.
* @param \Drupal\Component\Plugin\Context\ContextDefinitionInterface[] $context_definitions
* (optional) Any required context definitions.
* When an entity with layout is rendered, all section storage plugins which
* match a particular set of contexts are checked, in order of their weight,
* to determine which plugin should be used to render the layout.
* @see \Drupal\layout_builder\SectionStorage\SectionStorageManagerInterface::findByContext()
* @param bool $handles_permission_check
* (optional) Indicates that this section storage handles its own
* permission checking. If FALSE, the 'configure any layout' permission
* will be required during routing access. If TRUE, Layout Builder will
* not enforce any access restrictions for the storage, so the section
* storage's implementation of access() must perform the access checking itself.
* @param string|null $deriver
* (optional) The deriver class.
*/
public function __construct(
public readonly string $id,
public readonly int $weight = 0,
public readonly array $context_definitions = [],
public readonly bool $handles_permission_check = FALSE,
public readonly ?string $deriver = NULL,
) {}
/**
* {@inheritdoc}
*/
public function get(): SectionStorageDefinition {
return new SectionStorageDefinition([
'id' => $this->id,
'class' => $this->class,
'weight' => $this->weight,
'context_definitions' => $this->context_definitions,
'handles_permission_check' => $this->handles_permission_check,
]);
}
}

View File

@@ -0,0 +1,37 @@
<?php
namespace Drupal\layout_builder\Cache;
use Drupal\Component\Plugin\Discovery\CachedDiscoveryInterface;
use Drupal\Core\Block\BlockManagerInterface;
use Drupal\Core\Cache\CacheTagsInvalidatorInterface;
/**
* Provides a cache tag invalidator that clears the block cache.
*
* @internal
* Tagged services are internal.
*/
class ExtraFieldBlockCacheTagInvalidator implements CacheTagsInvalidatorInterface {
/**
* Constructs a new ExtraFieldBlockCacheTagInvalidator.
*
* @param \Drupal\Core\Block\BlockManagerInterface $blockManager
* The block manager.
*/
public function __construct(protected BlockManagerInterface $blockManager) {
}
/**
* {@inheritdoc}
*/
public function invalidateTags(array $tags) {
if (in_array('entity_field_info', $tags, TRUE)) {
if ($this->blockManager instanceof CachedDiscoveryInterface) {
$this->blockManager->clearCachedDefinitions();
}
}
}
}

View File

@@ -0,0 +1,98 @@
<?php
namespace Drupal\layout_builder\Cache;
use Drupal\Core\Cache\CacheableMetadata;
use Drupal\Core\Cache\Context\CalculatedCacheContextInterface;
use Drupal\Core\Entity\Entity\EntityViewDisplay;
use Drupal\Core\Entity\FieldableEntityInterface;
use Drupal\Core\Routing\RouteMatchInterface;
use Drupal\layout_builder\Entity\LayoutEntityDisplayInterface;
/**
* Determines whether Layout Builder is active for a given entity type or not.
*
* Cache context ID: 'layout_builder_is_active:%entity_type_id', e.g.
* 'layout_builder_is_active:node' (to vary by whether custom layout overrides
* are allowed for the Node entity specified by the route parameter).
*
* @internal
* Tagged services are internal.
*/
class LayoutBuilderIsActiveCacheContext implements CalculatedCacheContextInterface {
/**
* The current route match.
*
* @var \Drupal\Core\Routing\RouteMatchInterface
*/
protected $routeMatch;
/**
* LayoutBuilderCacheContext constructor.
*
* @param \Drupal\Core\Routing\RouteMatchInterface $route_match
* The current route match.
*/
public function __construct(RouteMatchInterface $route_match) {
$this->routeMatch = $route_match;
}
/**
* {@inheritdoc}
*/
public static function getLabel() {
return t('Layout Builder');
}
/**
* {@inheritdoc}
*/
public function getContext($entity_type_id = NULL) {
if (!$entity_type_id) {
throw new \LogicException('Missing entity type ID');
}
$display = $this->getDisplay($entity_type_id);
return ($display && $display->isOverridable()) ? '1' : '0';
}
/**
* {@inheritdoc}
*/
public function getCacheableMetadata($entity_type_id = NULL) {
if (!$entity_type_id) {
throw new \LogicException('Missing entity type ID');
}
$cacheable_metadata = new CacheableMetadata();
if ($display = $this->getDisplay($entity_type_id)) {
$cacheable_metadata->addCacheableDependency($display);
}
return $cacheable_metadata;
}
/**
* Returns the entity view display for a given entity type and view mode.
*
* @param string $entity_type_id
* The entity type ID.
*
* @return \Drupal\layout_builder\Entity\LayoutEntityDisplayInterface|null
* The entity view display, if it exists.
*/
protected function getDisplay($entity_type_id) {
if ($entity = $this->routeMatch->getParameter($entity_type_id)) {
if ($entity instanceof FieldableEntityInterface) {
// @todo Expand to work for all view modes in
// https://www.drupal.org/node/2907413.
$view_mode = 'full';
$display = EntityViewDisplay::collectRenderDisplay($entity, $view_mode);
if ($display instanceof LayoutEntityDisplayInterface) {
return $display;
}
}
}
}
}

View File

@@ -0,0 +1,35 @@
<?php
namespace Drupal\layout_builder\Cache;
use Drupal\Core\Cache\Context\RouteNameCacheContext;
/**
* Determines if an entity is being viewed in the Layout Builder UI.
*
* Cache context ID: 'route.name.is_layout_builder_ui'.
*
* @internal
* Tagged services are internal.
*/
class LayoutBuilderUiCacheContext extends RouteNameCacheContext {
/**
* {@inheritdoc}
*/
public static function getLabel() {
return t('Layout Builder user interface');
}
/**
* {@inheritdoc}
*/
public function getContext() {
$route_name = $this->routeMatch->getRouteName();
if ($route_name && str_starts_with($route_name, 'layout_builder.')) {
return 'is_layout_builder_ui.0';
}
return 'is_layout_builder_ui.1';
}
}

View File

@@ -0,0 +1,55 @@
<?php
namespace Drupal\layout_builder\Context;
use Drupal\Core\Plugin\Context\ContextInterface;
use Drupal\layout_builder\SectionStorageInterface;
/**
* Provides a wrapper around getting contexts from a section storage object.
*/
trait LayoutBuilderContextTrait {
/**
* The context repository.
*
* @var \Drupal\Core\Plugin\Context\ContextRepositoryInterface
*/
protected $contextRepository;
/**
* Gets the context repository service.
*
* @return \Drupal\Core\Plugin\Context\ContextRepositoryInterface
* The context repository service.
*/
protected function contextRepository() {
if (!$this->contextRepository) {
$this->contextRepository = \Drupal::service('context.repository');
}
return $this->contextRepository;
}
/**
* Returns all populated contexts, both global and section-storage-specific.
*
* @param \Drupal\layout_builder\SectionStorageInterface $section_storage
* The section storage.
*
* @return \Drupal\Core\Plugin\Context\ContextInterface[]
* The array of context objects.
*/
protected function getPopulatedContexts(SectionStorageInterface $section_storage): array {
// Get all known globally available contexts IDs.
$available_context_ids = array_keys($this->contextRepository()->getAvailableContexts());
// Filter to those that are populated.
$contexts = array_filter($this->contextRepository()->getRuntimeContexts($available_context_ids), function (ContextInterface $context) {
return $context->hasContextValue();
});
// Add in the per-section_storage contexts.
$contexts += $section_storage->getContextsDuringPreview();
return $contexts;
}
}

View File

@@ -0,0 +1,77 @@
<?php
namespace Drupal\layout_builder\Controller;
use Drupal\Core\Ajax\AjaxHelperTrait;
use Drupal\Core\DependencyInjection\ContainerInjectionInterface;
use Drupal\layout_builder\LayoutTempstoreRepositoryInterface;
use Drupal\layout_builder\Section;
use Drupal\layout_builder\SectionStorageInterface;
use Symfony\Component\DependencyInjection\ContainerInterface;
use Symfony\Component\HttpFoundation\RedirectResponse;
/**
* Defines a controller to add a new section.
*
* @internal
* Controller classes are internal.
*/
class AddSectionController implements ContainerInjectionInterface {
use AjaxHelperTrait;
use LayoutRebuildTrait;
/**
* The layout tempstore repository.
*
* @var \Drupal\layout_builder\LayoutTempstoreRepositoryInterface
*/
protected $layoutTempstoreRepository;
/**
* AddSectionController constructor.
*
* @param \Drupal\layout_builder\LayoutTempstoreRepositoryInterface $layout_tempstore_repository
* The layout tempstore repository.
*/
public function __construct(LayoutTempstoreRepositoryInterface $layout_tempstore_repository) {
$this->layoutTempstoreRepository = $layout_tempstore_repository;
}
/**
* {@inheritdoc}
*/
public static function create(ContainerInterface $container) {
return new static(
$container->get('layout_builder.tempstore_repository')
);
}
/**
* Adds the new section.
*
* @param \Drupal\layout_builder\SectionStorageInterface $section_storage
* The section storage.
* @param int $delta
* The delta of the section to splice.
* @param string $plugin_id
* The plugin ID of the layout to add.
*
* @return \Symfony\Component\HttpFoundation\Response
* The controller response.
*/
public function build(SectionStorageInterface $section_storage, int $delta, $plugin_id) {
$section_storage->insertSection($delta, new Section($plugin_id));
$this->layoutTempstoreRepository->set($section_storage);
if ($this->isAjax()) {
return $this->rebuildAndClose($section_storage);
}
else {
$url = $section_storage->getLayoutBuilderUrl();
return new RedirectResponse($url->setAbsolute()->toString());
}
}
}

View File

@@ -0,0 +1,267 @@
<?php
namespace Drupal\layout_builder\Controller;
use Drupal\Core\Ajax\AjaxHelperTrait;
use Drupal\Core\Block\BlockManagerInterface;
use Drupal\Core\DependencyInjection\ContainerInjectionInterface;
use Drupal\Core\Entity\EntityTypeManagerInterface;
use Drupal\Core\Session\AccountInterface;
use Drupal\Core\StringTranslation\StringTranslationTrait;
use Drupal\Core\Url;
use Drupal\layout_builder\Context\LayoutBuilderContextTrait;
use Drupal\layout_builder\LayoutBuilderHighlightTrait;
use Drupal\layout_builder\SectionStorageInterface;
use Symfony\Component\DependencyInjection\ContainerInterface;
/**
* Defines a controller to choose a new block.
*
* @internal
* Controller classes are internal.
*/
class ChooseBlockController implements ContainerInjectionInterface {
use AjaxHelperTrait;
use LayoutBuilderContextTrait;
use LayoutBuilderHighlightTrait;
use StringTranslationTrait;
/**
* The block manager.
*
* @var \Drupal\Core\Block\BlockManagerInterface
*/
protected $blockManager;
/**
* The entity type manager.
*
* @var \Drupal\Core\Entity\EntityTypeManagerInterface
*/
protected $entityTypeManager;
/**
* The current user.
*
* @var \Drupal\Core\Session\AccountInterface
*/
protected $currentUser;
/**
* ChooseBlockController constructor.
*
* @param \Drupal\Core\Block\BlockManagerInterface $block_manager
* The block manager.
* @param \Drupal\Core\Entity\EntityTypeManagerInterface $entity_type_manager
* The entity type manager.
* @param \Drupal\Core\Session\AccountInterface $current_user
* The current user.
*/
public function __construct(BlockManagerInterface $block_manager, EntityTypeManagerInterface $entity_type_manager, AccountInterface $current_user) {
$this->blockManager = $block_manager;
$this->entityTypeManager = $entity_type_manager;
$this->currentUser = $current_user;
}
/**
* {@inheritdoc}
*/
public static function create(ContainerInterface $container) {
return new static(
$container->get('plugin.manager.block'),
$container->get('entity_type.manager'),
$container->get('current_user')
);
}
/**
* Provides the UI for choosing a new block.
*
* @param \Drupal\layout_builder\SectionStorageInterface $section_storage
* The section storage.
* @param int $delta
* The delta of the section to splice.
* @param string $region
* The region the block is going in.
*
* @return array
* A render array.
*/
public function build(SectionStorageInterface $section_storage, int $delta, $region) {
if ($this->entityTypeManager->hasDefinition('block_content_type') && $types = $this->entityTypeManager->getStorage('block_content_type')->loadMultiple()) {
if (count($types) === 1) {
$type = reset($types);
$plugin_id = 'inline_block:' . $type->id();
if ($this->blockManager->hasDefinition($plugin_id)) {
$url = Url::fromRoute('layout_builder.add_block', [
'section_storage_type' => $section_storage->getStorageType(),
'section_storage' => $section_storage->getStorageId(),
'delta' => $delta,
'region' => $region,
'plugin_id' => $plugin_id,
]);
}
}
else {
$url = Url::fromRoute('layout_builder.choose_inline_block', [
'section_storage_type' => $section_storage->getStorageType(),
'section_storage' => $section_storage->getStorageId(),
'delta' => $delta,
'region' => $region,
]);
}
if (isset($url)) {
$build['add_block'] = [
'#type' => 'link',
'#url' => $url,
'#title' => $this->t('Create @entity_type', [
'@entity_type' => $this->entityTypeManager->getDefinition('block_content')->getSingularLabel(),
]),
'#attributes' => $this->getAjaxAttributes(),
'#access' => $this->currentUser->hasPermission('create and edit custom blocks'),
];
$build['add_block']['#attributes']['class'][] = 'inline-block-create-button';
}
}
$build['filter'] = [
'#type' => 'search',
'#title' => $this->t('Filter by block name'),
'#title_display' => 'invisible',
'#size' => 30,
'#placeholder' => $this->t('Filter by block name'),
'#attributes' => [
'class' => ['js-layout-builder-filter'],
'title' => $this->t('Enter a part of the block name to filter by.'),
],
];
$block_categories['#type'] = 'container';
$block_categories['#attributes']['class'][] = 'block-categories';
$block_categories['#attributes']['class'][] = 'js-layout-builder-categories';
$block_categories['#attributes']['data-layout-builder-target-highlight-id'] = $this->blockAddHighlightId($delta, $region);
$definitions = $this->blockManager->getFilteredDefinitions('layout_builder', $this->getPopulatedContexts($section_storage), [
'section_storage' => $section_storage,
'delta' => $delta,
'region' => $region,
]);
$grouped_definitions = $this->blockManager->getGroupedDefinitions($definitions);
foreach ($grouped_definitions as $category => $blocks) {
$block_categories[$category]['#type'] = 'details';
$block_categories[$category]['#attributes']['class'][] = 'js-layout-builder-category';
$block_categories[$category]['#open'] = TRUE;
$block_categories[$category]['#title'] = $category;
$block_categories[$category]['links'] = $this->getBlockLinks($section_storage, $delta, $region, $blocks);
}
$build['block_categories'] = $block_categories;
return $build;
}
/**
* Provides the UI for choosing a new inline block.
*
* @param \Drupal\layout_builder\SectionStorageInterface $section_storage
* The section storage.
* @param int $delta
* The delta of the section to splice.
* @param string $region
* The region the block is going in.
*
* @return array
* A render array.
*/
public function inlineBlockList(SectionStorageInterface $section_storage, int $delta, $region) {
$definitions = $this->blockManager->getFilteredDefinitions('layout_builder', $this->getPopulatedContexts($section_storage), [
'section_storage' => $section_storage,
'region' => $region,
'list' => 'inline_blocks',
]);
$blocks = $this->blockManager->getGroupedDefinitions($definitions);
$build = [];
$inline_blocks_category = (string) $this->t('Inline blocks');
if (isset($blocks[$inline_blocks_category])) {
$build['links'] = $this->getBlockLinks($section_storage, $delta, $region, $blocks[$inline_blocks_category]);
$build['links']['#attributes']['class'][] = 'inline-block-list';
foreach ($build['links']['#links'] as &$link) {
$link['attributes']['class'][] = 'inline-block-list__item';
}
$build['back_button'] = [
'#type' => 'link',
'#url' => Url::fromRoute('layout_builder.choose_block',
[
'section_storage_type' => $section_storage->getStorageType(),
'section_storage' => $section_storage->getStorageId(),
'delta' => $delta,
'region' => $region,
]
),
'#title' => $this->t('Back'),
'#attributes' => $this->getAjaxAttributes(),
];
}
$build['links']['#attributes']['data-layout-builder-target-highlight-id'] = $this->blockAddHighlightId($delta, $region);
return $build;
}
/**
* Gets a render array of block links.
*
* @param \Drupal\layout_builder\SectionStorageInterface $section_storage
* The section storage.
* @param int $delta
* The delta of the section to splice.
* @param string $region
* The region the block is going in.
* @param array $blocks
* The information for each block.
*
* @return array
* The block links render array.
*/
protected function getBlockLinks(SectionStorageInterface $section_storage, int $delta, $region, array $blocks) {
$links = [];
foreach ($blocks as $block_id => $block) {
$attributes = $this->getAjaxAttributes();
$attributes['class'][] = 'js-layout-builder-block-link';
$link = [
'title' => $block['admin_label'],
'url' => Url::fromRoute('layout_builder.add_block',
[
'section_storage_type' => $section_storage->getStorageType(),
'section_storage' => $section_storage->getStorageId(),
'delta' => $delta,
'region' => $region,
'plugin_id' => $block_id,
]
),
'attributes' => $attributes,
];
$links[] = $link;
}
return [
'#theme' => 'links',
'#links' => $links,
];
}
/**
* Get dialog attributes if an ajax request.
*
* @return array
* The attributes array.
*/
protected function getAjaxAttributes() {
if ($this->isAjax()) {
return [
'class' => ['use-ajax'],
'data-dialog-type' => 'dialog',
'data-dialog-renderer' => 'off_canvas',
];
}
return [];
}
}

View File

@@ -0,0 +1,111 @@
<?php
namespace Drupal\layout_builder\Controller;
use Drupal\Core\Ajax\AjaxHelperTrait;
use Drupal\Core\DependencyInjection\ContainerInjectionInterface;
use Drupal\Core\Layout\LayoutPluginManagerInterface;
use Drupal\Core\Plugin\PluginFormInterface;
use Drupal\Core\StringTranslation\StringTranslationTrait;
use Drupal\Core\Url;
use Drupal\layout_builder\Context\LayoutBuilderContextTrait;
use Drupal\layout_builder\LayoutBuilderHighlightTrait;
use Drupal\layout_builder\SectionStorageInterface;
use Symfony\Component\DependencyInjection\ContainerInterface;
/**
* Defines a controller to choose a new section.
*
* @internal
* Controller classes are internal.
*/
class ChooseSectionController implements ContainerInjectionInterface {
use AjaxHelperTrait;
use LayoutBuilderContextTrait;
use LayoutBuilderHighlightTrait;
use StringTranslationTrait;
/**
* The layout manager.
*
* @var \Drupal\Core\Layout\LayoutPluginManagerInterface
*/
protected $layoutManager;
/**
* ChooseSectionController constructor.
*
* @param \Drupal\Core\Layout\LayoutPluginManagerInterface $layout_manager
* The layout manager.
*/
public function __construct(LayoutPluginManagerInterface $layout_manager) {
$this->layoutManager = $layout_manager;
}
/**
* {@inheritdoc}
*/
public static function create(ContainerInterface $container) {
return new static(
$container->get('plugin.manager.core.layout')
);
}
/**
* Choose a layout plugin to add as a section.
*
* @param \Drupal\layout_builder\SectionStorageInterface $section_storage
* The section storage.
* @param int $delta
* The delta of the section to splice.
*
* @return array
* The render array.
*/
public function build(SectionStorageInterface $section_storage, int $delta) {
$items = [];
$definitions = $this->layoutManager->getFilteredDefinitions('layout_builder', $this->getPopulatedContexts($section_storage), ['section_storage' => $section_storage]);
foreach ($definitions as $plugin_id => $definition) {
$layout = $this->layoutManager->createInstance($plugin_id);
$item = [
'#type' => 'link',
'#title' => [
'icon' => $definition->getIcon(60, 80, 1, 3),
'label' => [
'#type' => 'container',
'#children' => $definition->getLabel(),
],
],
'#url' => Url::fromRoute(
$layout instanceof PluginFormInterface ? 'layout_builder.configure_section' : 'layout_builder.add_section',
[
'section_storage_type' => $section_storage->getStorageType(),
'section_storage' => $section_storage->getStorageId(),
'delta' => $delta,
'plugin_id' => $plugin_id,
]
),
];
if ($this->isAjax()) {
$item['#attributes']['class'][] = 'use-ajax';
$item['#attributes']['data-dialog-type'][] = 'dialog';
$item['#attributes']['data-dialog-renderer'][] = 'off_canvas';
}
$items[$plugin_id] = $item;
}
$output['layouts'] = [
'#theme' => 'item_list__layouts',
'#items' => $items,
'#attributes' => [
'class' => [
'layout-selection',
],
'data-layout-builder-target-highlight-id' => $this->sectionAddHighlightId($delta),
],
];
return $output;
}
}

View File

@@ -0,0 +1,49 @@
<?php
namespace Drupal\layout_builder\Controller;
use Drupal\Component\Assertion\Inspector;
use Drupal\Core\StringTranslation\StringTranslationTrait;
use Drupal\layout_builder\SectionStorageInterface;
/**
* Defines a controller to provide the Layout Builder admin UI.
*
* @internal
* Controller classes are internal.
*/
class LayoutBuilderController {
use StringTranslationTrait;
/**
* Provides a title callback.
*
* @param \Drupal\layout_builder\SectionStorageInterface $section_storage
* The section storage.
*
* @return string
* The title for the layout page.
*/
public function title(SectionStorageInterface $section_storage) {
assert(Inspector::assertStringable($section_storage->label()), 'Section storage label is expected to be a string.');
return $this->t('Edit layout for %label', ['%label' => $section_storage->label() ?? $section_storage->getStorageType() . ' ' . $section_storage->getStorageId()]);
}
/**
* Renders the Layout UI.
*
* @param \Drupal\layout_builder\SectionStorageInterface $section_storage
* The section storage.
*
* @return array
* A render array.
*/
public function layout(SectionStorageInterface $section_storage) {
return [
'#type' => 'layout_builder',
'#section_storage' => $section_storage,
];
}
}

View File

@@ -0,0 +1,73 @@
<?php
namespace Drupal\layout_builder\Controller;
use Drupal\Component\Utility\NestedArray;
use Drupal\Core\Controller\FormController;
use Drupal\Core\DependencyInjection\DependencySerializationTrait;
use Drupal\Core\Routing\RouteMatchInterface;
use Symfony\Component\HttpFoundation\Request;
/**
* Overrides the entity form controller service for layout builder operations.
*/
class LayoutBuilderHtmlEntityFormController extends FormController {
use DependencySerializationTrait;
/**
* The entity form controller being decorated.
*
* @var \Drupal\Core\Controller\FormController
*/
protected $entityFormController;
/**
* Constructs a LayoutBuilderHtmlEntityFormController object.
*
* @param \Drupal\Core\Controller\FormController $entity_form_controller
* The entity form controller being decorated.
*/
public function __construct(FormController $entity_form_controller) {
$this->entityFormController = $entity_form_controller;
}
/**
* {@inheritdoc}
*/
public function getContentResult(Request $request, RouteMatchInterface $route_match) {
$form = $this->entityFormController->getContentResult($request, $route_match);
// If the form render element has a #layout_builder_element_keys property,
// first set the form element as a child of the root render array. Use the
// keys to get the layout builder element from the form render array and
// copy it to a separate child element of the root element to prevent any
// forms within the layout builder element from being nested.
if (isset($form['#layout_builder_element_keys'])) {
$build['form'] = &$form;
$layout_builder_element = &NestedArray::getValue($form, $form['#layout_builder_element_keys']);
$build['layout_builder'] = $layout_builder_element;
// Remove the layout builder element within the form.
$layout_builder_element = [];
return $build;
}
// If no #layout_builder_element_keys property, return form as is.
return $form;
}
/**
* {@inheritdoc}
*/
protected function getFormArgument(RouteMatchInterface $route_match) {
return $this->entityFormController->getFormArgument($route_match);
}
/**
* {@inheritdoc}
*/
protected function getFormObject(RouteMatchInterface $route_match, $form_arg) {
return $this->entityFormController->getFormObject($route_match, $form_arg);
}
}

View File

@@ -0,0 +1,51 @@
<?php
namespace Drupal\layout_builder\Controller;
use Drupal\Core\Ajax\AjaxResponse;
use Drupal\Core\Ajax\CloseDialogCommand;
use Drupal\Core\Ajax\ReplaceCommand;
use Drupal\layout_builder\SectionStorageInterface;
/**
* Provides AJAX responses to rebuild the Layout Builder.
*/
trait LayoutRebuildTrait {
/**
* Rebuilds the layout.
*
* @param \Drupal\layout_builder\SectionStorageInterface $section_storage
* The section storage.
*
* @return \Drupal\Core\Ajax\AjaxResponse
* An AJAX response to either rebuild the layout and close the dialog, or
* reload the page.
*/
protected function rebuildAndClose(SectionStorageInterface $section_storage) {
$response = $this->rebuildLayout($section_storage);
$response->addCommand(new CloseDialogCommand('#drupal-off-canvas'));
return $response;
}
/**
* Rebuilds the layout.
*
* @param \Drupal\layout_builder\SectionStorageInterface $section_storage
* The section storage.
*
* @return \Drupal\Core\Ajax\AjaxResponse
* An AJAX response to either rebuild the layout and close the dialog, or
* reload the page.
*/
protected function rebuildLayout(SectionStorageInterface $section_storage) {
$response = new AjaxResponse();
$layout = [
'#type' => 'layout_builder',
'#section_storage' => $section_storage,
];
$response->addCommand(new ReplaceCommand('#layout-builder', $layout));
return $response;
}
}

View File

@@ -0,0 +1,91 @@
<?php
namespace Drupal\layout_builder\Controller;
use Drupal\Core\DependencyInjection\ContainerInjectionInterface;
use Drupal\layout_builder\LayoutTempstoreRepositoryInterface;
use Drupal\layout_builder\SectionStorageInterface;
use Symfony\Component\DependencyInjection\ContainerInterface;
/**
* Defines a controller to move a block.
*
* @internal
* Controller classes are internal.
*/
class MoveBlockController implements ContainerInjectionInterface {
use LayoutRebuildTrait;
/**
* The layout tempstore repository.
*
* @var \Drupal\layout_builder\LayoutTempstoreRepositoryInterface
*/
protected $layoutTempstoreRepository;
/**
* LayoutController constructor.
*
* @param \Drupal\layout_builder\LayoutTempstoreRepositoryInterface $layout_tempstore_repository
* The layout tempstore repository.
*/
public function __construct(LayoutTempstoreRepositoryInterface $layout_tempstore_repository) {
$this->layoutTempstoreRepository = $layout_tempstore_repository;
}
/**
* {@inheritdoc}
*/
public static function create(ContainerInterface $container) {
return new static(
$container->get('layout_builder.tempstore_repository')
);
}
/**
* Moves a block to another region.
*
* @param \Drupal\layout_builder\SectionStorageInterface $section_storage
* The section storage.
* @param int $delta_from
* The delta of the original section.
* @param int $delta_to
* The delta of the destination section.
* @param string $region_to
* The new region for this block.
* @param string $block_uuid
* The UUID for this block.
* @param string|null $preceding_block_uuid
* (optional) If provided, the UUID of the block to insert this block after.
*
* @return \Drupal\Core\Ajax\AjaxResponse
* An AJAX response.
*/
public function build(SectionStorageInterface $section_storage, int $delta_from, int $delta_to, $region_to, $block_uuid, $preceding_block_uuid = NULL) {
$section = $section_storage->getSection($delta_from);
$component = $section->getComponent($block_uuid);
$section->removeComponent($block_uuid);
// If the block is moving from one section to another, update the original
// section and load the new one.
if ($delta_from !== $delta_to) {
$section = $section_storage->getSection($delta_to);
}
// If a preceding block was specified, insert after that. Otherwise add the
// block to the front.
$component->setRegion($region_to);
if (isset($preceding_block_uuid)) {
$section->insertAfterComponent($preceding_block_uuid, $component);
}
else {
$section->insertComponent(0, $component);
}
$this->layoutTempstoreRepository->set($section_storage);
return $this->rebuildLayout($section_storage);
}
}

View File

@@ -0,0 +1,10 @@
<?php
namespace Drupal\layout_builder;
use Drupal\Core\Config\Entity\ThirdPartySettingsInterface;
/**
* Defines an interface for an object that stores layout sections for defaults.
*/
interface DefaultsSectionStorageInterface extends SectionStorageInterface, ThirdPartySettingsInterface, LayoutBuilderEnabledInterface, LayoutBuilderOverridableInterface {}

View File

@@ -0,0 +1,382 @@
<?php
namespace Drupal\layout_builder\Element;
use Drupal\Core\Ajax\AjaxHelperTrait;
use Drupal\Core\Plugin\ContainerFactoryPluginInterface;
use Drupal\Core\Plugin\PluginFormInterface;
use Drupal\Core\Render\Attribute\RenderElement;
use Drupal\Core\Render\Element;
use Drupal\Core\Render\Element\RenderElementBase;
use Drupal\Core\Url;
use Drupal\layout_builder\Context\LayoutBuilderContextTrait;
use Drupal\layout_builder\Event\PrepareLayoutEvent;
use Drupal\layout_builder\LayoutBuilderEvents;
use Drupal\layout_builder\LayoutBuilderHighlightTrait;
use Drupal\layout_builder\SectionStorageInterface;
use Symfony\Component\DependencyInjection\ContainerInterface;
use Symfony\Contracts\EventDispatcher\EventDispatcherInterface;
/**
* Defines a render element for building the Layout Builder UI.
*
* @internal
* Plugin classes are internal.
*/
#[RenderElement('layout_builder')]
class LayoutBuilder extends RenderElementBase implements ContainerFactoryPluginInterface {
use AjaxHelperTrait;
use LayoutBuilderContextTrait;
use LayoutBuilderHighlightTrait;
/**
* The event dispatcher.
*
* @var \Symfony\Contracts\EventDispatcher\EventDispatcherInterface
*/
protected $eventDispatcher;
/**
* Constructs a new LayoutBuilder.
*
* @param array $configuration
* A configuration array containing information about the plugin instance.
* @param string $plugin_id
* The plugin ID for the plugin instance.
* @param mixed $plugin_definition
* The plugin implementation definition.
* @param \Symfony\Contracts\EventDispatcher\EventDispatcherInterface $event_dispatcher
* The event dispatcher service.
*/
public function __construct(array $configuration, $plugin_id, $plugin_definition, EventDispatcherInterface $event_dispatcher) {
parent::__construct($configuration, $plugin_id, $plugin_definition);
$this->eventDispatcher = $event_dispatcher;
}
/**
* {@inheritdoc}
*/
public static function create(ContainerInterface $container, array $configuration, $plugin_id, $plugin_definition) {
return new static(
$configuration,
$plugin_id,
$plugin_definition,
$container->get('event_dispatcher')
);
}
/**
* {@inheritdoc}
*/
public function getInfo() {
return [
'#section_storage' => NULL,
'#pre_render' => [
[$this, 'preRender'],
],
];
}
/**
* Pre-render callback: Renders the Layout Builder UI.
*/
public function preRender($element) {
if ($element['#section_storage'] instanceof SectionStorageInterface) {
$element['layout_builder'] = $this->layout($element['#section_storage']);
}
return $element;
}
/**
* Renders the Layout UI.
*
* @param \Drupal\layout_builder\SectionStorageInterface $section_storage
* The section storage.
*
* @return array
* A render array.
*/
protected function layout(SectionStorageInterface $section_storage) {
$this->prepareLayout($section_storage);
$output = [];
if ($this->isAjax()) {
$output['status_messages'] = [
'#type' => 'status_messages',
];
}
$count = 0;
for ($i = 0; $i < $section_storage->count(); $i++) {
$output[] = $this->buildAddSectionLink($section_storage, $count);
$output[] = $this->buildAdministrativeSection($section_storage, $count);
$count++;
}
$output[] = $this->buildAddSectionLink($section_storage, $count);
$output['#attached']['library'][] = 'layout_builder/drupal.layout_builder';
// As the Layout Builder UI is typically displayed using the frontend theme,
// it is not marked as an administrative page at the route level even though
// it performs an administrative task. Mark this as an administrative page
// for JavaScript.
$output['#attached']['drupalSettings']['path']['currentPathIsAdmin'] = TRUE;
$output['#type'] = 'container';
$output['#attributes']['id'] = 'layout-builder';
$output['#attributes']['class'][] = 'layout-builder';
// Mark this UI as uncacheable.
$output['#cache']['max-age'] = 0;
return $output;
}
/**
* Prepares a layout for use in the UI.
*
* @param \Drupal\layout_builder\SectionStorageInterface $section_storage
* The section storage.
*/
protected function prepareLayout(SectionStorageInterface $section_storage) {
$event = new PrepareLayoutEvent($section_storage);
$this->eventDispatcher->dispatch($event, LayoutBuilderEvents::PREPARE_LAYOUT);
}
/**
* Builds a link to add a new section at a given delta.
*
* @param \Drupal\layout_builder\SectionStorageInterface $section_storage
* The section storage.
* @param int $delta
* The delta of the section to splice.
*
* @return array
* A render array for a link.
*/
protected function buildAddSectionLink(SectionStorageInterface $section_storage, $delta) {
$storage_type = $section_storage->getStorageType();
$storage_id = $section_storage->getStorageId();
// If the delta and the count are the same, it is either the end of the
// layout or an empty layout.
if ($delta === count($section_storage)) {
if ($delta === 0) {
$title = $this->t('Add section');
}
else {
$title = $this->t('Add section <span class="visually-hidden">at end of layout</span>');
}
}
// If the delta and the count are different, it is either the beginning of
// the layout or in between two sections.
else {
if ($delta === 0) {
$title = $this->t('Add section <span class="visually-hidden">at start of layout</span>');
}
else {
$title = $this->t('Add section <span class="visually-hidden">between @first and @second</span>', ['@first' => $delta, '@second' => $delta + 1]);
}
}
return [
'link' => [
'#type' => 'link',
'#title' => $title,
'#url' => Url::fromRoute('layout_builder.choose_section',
[
'section_storage_type' => $storage_type,
'section_storage' => $storage_id,
'delta' => $delta,
],
[
'attributes' => [
'class' => [
'use-ajax',
'layout-builder__link',
'layout-builder__link--add',
],
'data-dialog-type' => 'dialog',
'data-dialog-renderer' => 'off_canvas',
],
]
),
],
'#type' => 'container',
'#attributes' => [
'class' => ['layout-builder__add-section'],
'data-layout-builder-highlight-id' => $this->sectionAddHighlightId($delta),
],
];
}
/**
* Builds the render array for the layout section while editing.
*
* @param \Drupal\layout_builder\SectionStorageInterface $section_storage
* The section storage.
* @param int $delta
* The delta of the section.
*
* @return array
* The render array for a given section.
*/
protected function buildAdministrativeSection(SectionStorageInterface $section_storage, $delta) {
$storage_type = $section_storage->getStorageType();
$storage_id = $section_storage->getStorageId();
$section = $section_storage->getSection($delta);
$layout = $section->getLayout($this->getPopulatedContexts($section_storage));
$layout_settings = $section->getLayoutSettings();
$section_label = !empty($layout_settings['label']) ? $layout_settings['label'] : $this->t('Section @section', ['@section' => $delta + 1]);
$build = $section->toRenderArray($this->getPopulatedContexts($section_storage), TRUE);
$layout_definition = $layout->getPluginDefinition();
$region_labels = $layout_definition->getRegionLabels();
foreach ($layout_definition->getRegions() as $region => $info) {
if (!empty($build[$region])) {
foreach (Element::children($build[$region]) as $uuid) {
$build[$region][$uuid]['#attributes']['class'][] = 'js-layout-builder-block';
$build[$region][$uuid]['#attributes']['class'][] = 'layout-builder-block';
$build[$region][$uuid]['#attributes']['data-layout-block-uuid'] = $uuid;
$build[$region][$uuid]['#attributes']['data-layout-builder-highlight-id'] = $this->blockUpdateHighlightId($uuid);
$build[$region][$uuid]['#contextual_links'] = [
'layout_builder_block' => [
'route_parameters' => [
'section_storage_type' => $storage_type,
'section_storage' => $storage_id,
'delta' => $delta,
'region' => $region,
'uuid' => $uuid,
],
// Add metadata about the current operations available in
// contextual links. This will invalidate the client-side cache of
// links that were cached before the 'move' link was added.
// @see layout_builder.links.contextual.yml
'metadata' => [
'operations' => 'move:update:remove',
],
],
];
}
}
$build[$region]['layout_builder_add_block']['link'] = [
'#type' => 'link',
// Add one to the current delta since it is zero-indexed.
'#title' => $this->t('Add block <span class="visually-hidden">in @section, @region region</span>', ['@section' => $section_label, '@region' => $region_labels[$region]]),
'#url' => Url::fromRoute('layout_builder.choose_block',
[
'section_storage_type' => $storage_type,
'section_storage' => $storage_id,
'delta' => $delta,
'region' => $region,
],
[
'attributes' => [
'class' => [
'use-ajax',
'layout-builder__link',
'layout-builder__link--add',
],
'data-dialog-type' => 'dialog',
'data-dialog-renderer' => 'off_canvas',
],
]
),
];
$build[$region]['layout_builder_add_block']['#type'] = 'container';
$build[$region]['layout_builder_add_block']['#attributes'] = [
'class' => ['layout-builder__add-block'],
'data-layout-builder-highlight-id' => $this->blockAddHighlightId($delta, $region),
];
$build[$region]['layout_builder_add_block']['#weight'] = 1000;
$build[$region]['#attributes']['data-region'] = $region;
$build[$region]['#attributes']['class'][] = 'layout-builder__region';
$build[$region]['#attributes']['class'][] = 'js-layout-builder-region';
$build[$region]['#attributes']['role'] = 'group';
$build[$region]['#attributes']['aria-label'] = $this->t('@region region in @section', [
'@region' => $info['label'],
'@section' => $section_label,
]);
// Get weights of all children for use by the region label.
$weights = array_map(function ($a) {
return $a['#weight'] ?? 0;
}, $build[$region]);
// The region label is made visible when the move block dialog is open.
$build[$region]['region_label'] = [
'#type' => 'container',
'#attributes' => [
'class' => ['layout__region-info', 'layout-builder__region-label'],
// A more detailed version of this information is already read by
// screen readers, so this label can be hidden from them.
'aria-hidden' => TRUE,
],
'#markup' => $this->t('Region: @region', ['@region' => $info['label']]),
// Ensures the region label is displayed first.
'#weight' => min($weights) - 1,
];
}
$build['#attributes']['data-layout-update-url'] = Url::fromRoute('layout_builder.move_block', [
'section_storage_type' => $storage_type,
'section_storage' => $storage_id,
])->toString();
$build['#attributes']['data-layout-delta'] = $delta;
$build['#attributes']['class'][] = 'layout-builder__layout';
$build['#attributes']['data-layout-builder-highlight-id'] = $this->sectionUpdateHighlightId($delta);
return [
'#type' => 'container',
'#attributes' => [
'class' => ['layout-builder__section'],
'role' => 'group',
'aria-label' => $section_label,
],
'remove' => [
'#type' => 'link',
'#title' => $this->t('Remove @section', ['@section' => $section_label]),
'#url' => Url::fromRoute('layout_builder.remove_section', [
'section_storage_type' => $storage_type,
'section_storage' => $storage_id,
'delta' => $delta,
]),
'#attributes' => [
'class' => [
'use-ajax',
'layout-builder__link',
'layout-builder__link--remove',
],
'data-dialog-type' => 'dialog',
'data-dialog-renderer' => 'off_canvas',
],
],
// The section label is added to sections without a "Configure section"
// link, and is only visible when the move block dialog is open.
'section_label' => [
'#markup' => $this->t('<span class="layout-builder__section-label" aria-hidden="true">@section</span>', ['@section' => $section_label]),
'#access' => !$layout instanceof PluginFormInterface,
],
'configure' => [
'#type' => 'link',
'#title' => $this->t('Configure @section', ['@section' => $section_label]),
'#access' => $layout instanceof PluginFormInterface,
'#url' => Url::fromRoute('layout_builder.configure_section', [
'section_storage_type' => $storage_type,
'section_storage' => $storage_id,
'delta' => $delta,
]),
'#attributes' => [
'class' => [
'use-ajax',
'layout-builder__link',
'layout-builder__link--configure',
],
'data-dialog-type' => 'dialog',
'data-dialog-renderer' => 'off_canvas',
],
],
'layout-builder__section' => $build,
];
}
}

View File

@@ -0,0 +1,529 @@
<?php
namespace Drupal\layout_builder\Entity;
use Drupal\Component\Plugin\ConfigurableInterface;
use Drupal\Component\Plugin\DerivativeInspectionInterface;
use Drupal\Component\Plugin\PluginBase;
use Drupal\Core\Cache\CacheableMetadata;
use Drupal\Core\Entity\Entity\EntityViewDisplay as BaseEntityViewDisplay;
use Drupal\Core\Entity\EntityStorageInterface;
use Drupal\Core\Entity\FieldableEntityInterface;
use Drupal\Core\Plugin\Context\Context;
use Drupal\Core\Plugin\Context\ContextDefinition;
use Drupal\Core\Plugin\Context\EntityContext;
use Drupal\Core\Render\Element;
use Drupal\Core\StringTranslation\TranslatableMarkup;
use Drupal\field\Entity\FieldConfig;
use Drupal\field\Entity\FieldStorageConfig;
use Drupal\layout_builder\LayoutEntityHelperTrait;
use Drupal\layout_builder\Plugin\SectionStorage\OverridesSectionStorage;
use Drupal\layout_builder\Section;
use Drupal\layout_builder\SectionComponent;
use Drupal\layout_builder\SectionListTrait;
/**
* Provides an entity view display entity that has a layout.
*/
class LayoutBuilderEntityViewDisplay extends BaseEntityViewDisplay implements LayoutEntityDisplayInterface {
use LayoutEntityHelperTrait;
use SectionListTrait;
/**
* The entity field manager.
*
* @var \Drupal\Core\Entity\EntityFieldManagerInterface
*/
protected $entityFieldManager;
/**
* {@inheritdoc}
*/
public function __construct(array $values, $entity_type) {
// Set $entityFieldManager before calling the parent constructor because the
// constructor will call init() which then calls setComponent() which needs
// $entityFieldManager.
$this->entityFieldManager = \Drupal::service('entity_field.manager');
parent::__construct($values, $entity_type);
}
/**
* {@inheritdoc}
*/
public function isOverridable() {
return $this->isLayoutBuilderEnabled() && $this->getThirdPartySetting('layout_builder', 'allow_custom', FALSE);
}
/**
* {@inheritdoc}
*/
public function setOverridable($overridable = TRUE) {
$this->setThirdPartySetting('layout_builder', 'allow_custom', $overridable);
// Enable Layout Builder if it's not already enabled and overriding.
if ($overridable && !$this->isLayoutBuilderEnabled()) {
$this->enableLayoutBuilder();
}
return $this;
}
/**
* {@inheritdoc}
*/
public function isLayoutBuilderEnabled() {
// Layout Builder must not be enabled for the '_custom' view mode that is
// used for on-the-fly rendering of fields in isolation from the entity.
if ($this->isCustomMode()) {
return FALSE;
}
return (bool) $this->getThirdPartySetting('layout_builder', 'enabled');
}
/**
* {@inheritdoc}
*/
public function enableLayoutBuilder() {
$this->setThirdPartySetting('layout_builder', 'enabled', TRUE);
return $this;
}
/**
* {@inheritdoc}
*/
public function disableLayoutBuilder() {
$this->setOverridable(FALSE);
$this->setThirdPartySetting('layout_builder', 'enabled', FALSE);
return $this;
}
/**
* {@inheritdoc}
*/
public function getSections() {
return $this->getThirdPartySetting('layout_builder', 'sections', []);
}
/**
* {@inheritdoc}
*/
protected function setSections(array $sections) {
// Third-party settings must be completely unset instead of stored as an
// empty array.
if (!$sections) {
$this->unsetThirdPartySetting('layout_builder', 'sections');
}
else {
$this->setThirdPartySetting('layout_builder', 'sections', array_values($sections));
}
return $this;
}
/**
* {@inheritdoc}
*/
public function preSave(EntityStorageInterface $storage) {
$original_value = isset($this->original) ? $this->original->isOverridable() : FALSE;
$new_value = $this->isOverridable();
if ($original_value !== $new_value) {
$entity_type_id = $this->getTargetEntityTypeId();
$bundle = $this->getTargetBundle();
if ($new_value) {
$this->addSectionField($entity_type_id, $bundle, OverridesSectionStorage::FIELD_NAME);
}
else {
$this->removeSectionField($entity_type_id, $bundle, OverridesSectionStorage::FIELD_NAME);
}
}
parent::preSave($storage);
$already_enabled = isset($this->original) ? $this->original->isLayoutBuilderEnabled() : FALSE;
$set_enabled = $this->isLayoutBuilderEnabled();
if ($already_enabled !== $set_enabled) {
if ($set_enabled) {
// Loop through all existing field-based components and add them as
// section-based components.
$components = $this->getComponents();
// Sort the components by weight.
uasort($components, 'Drupal\Component\Utility\SortArray::sortByWeightElement');
foreach ($components as $name => $component) {
$this->setComponent($name, $component);
}
}
else {
// When being disabled, remove all existing section data.
$this->removeAllSections();
}
}
}
/**
* {@inheritdoc}
*/
public function save(): int {
$return = parent::save();
if (!\Drupal::moduleHandler()->moduleExists('layout_builder_expose_all_field_blocks')) {
// Invalidate the block cache in order to regenerate field block
// definitions.
\Drupal::service('plugin.manager.block')->clearCachedDefinitions();
}
return $return;
}
/**
* Removes a layout section field if it is no longer needed.
*
* Because the field is shared across all view modes, the field will only be
* removed if no other view modes are using it.
*
* @param string $entity_type_id
* The entity type ID.
* @param string $bundle
* The bundle.
* @param string $field_name
* The name for the layout section field.
*/
protected function removeSectionField($entity_type_id, $bundle, $field_name) {
/** @var \Drupal\Core\Config\Entity\ConfigEntityStorageInterface $storage */
$storage = $this->entityTypeManager()->getStorage($this->getEntityTypeId());
$query = $storage->getQuery()
->condition('targetEntityType', $this->getTargetEntityTypeId())
->condition('bundle', $this->getTargetBundle())
->condition('mode', $this->getMode(), '<>')
->condition('third_party_settings.layout_builder.allow_custom', TRUE);
$enabled = (bool) $query->count()->execute();
if (!$enabled && $field = FieldConfig::loadByName($entity_type_id, $bundle, $field_name)) {
$field->delete();
}
}
/**
* Adds a layout section field to a given bundle.
*
* @param string $entity_type_id
* The entity type ID.
* @param string $bundle
* The bundle.
* @param string $field_name
* The name for the layout section field.
*/
protected function addSectionField($entity_type_id, $bundle, $field_name) {
$field = FieldConfig::loadByName($entity_type_id, $bundle, $field_name);
if (!$field) {
$field_storage = FieldStorageConfig::loadByName($entity_type_id, $field_name);
if (!$field_storage) {
$field_storage = FieldStorageConfig::create([
'entity_type' => $entity_type_id,
'field_name' => $field_name,
'type' => 'layout_section',
'locked' => TRUE,
]);
$field_storage->setTranslatable(FALSE);
$field_storage->save();
}
$field = FieldConfig::create([
'field_storage' => $field_storage,
'bundle' => $bundle,
'label' => t('Layout'),
]);
$field->setTranslatable(FALSE);
$field->save();
}
}
/**
* {@inheritdoc}
*/
public function createCopy($mode) {
// Disable Layout Builder and remove any sections copied from the original.
return parent::createCopy($mode)
->setSections([])
->disableLayoutBuilder();
}
/**
* {@inheritdoc}
*/
protected function getDefaultRegion() {
if ($this->hasSection(0)) {
return $this->getSection(0)->getDefaultRegion();
}
return parent::getDefaultRegion();
}
/**
* Wraps the context repository service.
*
* @return \Drupal\Core\Plugin\Context\ContextRepositoryInterface
* The context repository service.
*/
protected function contextRepository() {
return \Drupal::service('context.repository');
}
/**
* Indicates if this display is using the '_custom' view mode.
*
* @return bool
* TRUE if this display is using the '_custom' view mode, FALSE otherwise.
*/
protected function isCustomMode() {
return $this->getOriginalMode() === static::CUSTOM_MODE;
}
/**
* {@inheritdoc}
*/
public function buildMultiple(array $entities) {
$build_list = parent::buildMultiple($entities);
// Layout Builder can not be enabled for the '_custom' view mode that is
// used for on-the-fly rendering of fields in isolation from the entity.
if ($this->isCustomMode()) {
return $build_list;
}
foreach ($entities as $id => $entity) {
$build_list[$id]['_layout_builder'] = $this->buildSections($entity);
// If there are any sections, remove all fields with configurable display
// from the existing build. These fields are replicated within sections as
// field blocks by ::setComponent().
if (!Element::isEmpty($build_list[$id]['_layout_builder'])) {
foreach ($build_list[$id] as $name => $build_part) {
$field_definition = $this->getFieldDefinition($name);
if ($field_definition && $field_definition->isDisplayConfigurable($this->displayContext)) {
unset($build_list[$id][$name]);
}
}
}
}
return $build_list;
}
/**
* Builds the render array for the sections of a given entity.
*
* @param \Drupal\Core\Entity\FieldableEntityInterface $entity
* The entity.
*
* @return array
* The render array representing the sections of the entity.
*/
protected function buildSections(FieldableEntityInterface $entity) {
$contexts = $this->getContextsForEntity($entity);
$label = new TranslatableMarkup('@entity being viewed', [
'@entity' => $entity->getEntityType()->getSingularLabel(),
]);
$contexts['layout_builder.entity'] = EntityContext::fromEntity($entity, $label);
$cacheability = new CacheableMetadata();
$storage = $this->sectionStorageManager()->findByContext($contexts, $cacheability);
$build = [];
if ($storage) {
foreach ($storage->getSections() as $delta => $section) {
$build[$delta] = $section->toRenderArray($contexts);
}
}
// The render array is built based on decisions made by SectionStorage
// plugins and therefore it needs to depend on the accumulated
// cacheability of those decisions.
$cacheability->applyTo($build);
return $build;
}
/**
* Gets the available contexts for a given entity.
*
* @param \Drupal\Core\Entity\FieldableEntityInterface $entity
* The entity.
*
* @return \Drupal\Core\Plugin\Context\ContextInterface[]
* An array of context objects for a given entity.
*/
protected function getContextsForEntity(FieldableEntityInterface $entity) {
$available_context_ids = array_keys($this->contextRepository()->getAvailableContexts());
return [
'view_mode' => new Context(ContextDefinition::create('string'), $this->getMode()),
'entity' => EntityContext::fromEntity($entity),
'display' => EntityContext::fromEntity($this),
] + $this->contextRepository()->getRuntimeContexts($available_context_ids);
}
/**
* {@inheritdoc}
*
* @todo Move this upstream in https://www.drupal.org/node/2939931.
*/
public function label() {
$bundle_info = \Drupal::service('entity_type.bundle.info')->getBundleInfo($this->getTargetEntityTypeId());
$bundle_label = $bundle_info[$this->getTargetBundle()]['label'];
$target_entity_type = $this->entityTypeManager()->getDefinition($this->getTargetEntityTypeId());
return new TranslatableMarkup('@bundle @label', ['@bundle' => $bundle_label, '@label' => $target_entity_type->getPluralLabel()]);
}
/**
* {@inheritdoc}
*/
public function calculateDependencies() {
parent::calculateDependencies();
foreach ($this->getSections() as $section) {
$this->calculatePluginDependencies($section->getLayout());
foreach ($section->getComponents() as $component) {
$this->calculatePluginDependencies($component->getPlugin());
}
}
return $this;
}
/**
* {@inheritdoc}
*/
public function onDependencyRemoval(array $dependencies) {
$changed = parent::onDependencyRemoval($dependencies);
// Loop through all sections and determine if the removed dependencies are
// used by their layout plugins.
foreach ($this->getSections() as $delta => $section) {
$layout_dependencies = $this->getPluginDependencies($section->getLayout());
$layout_removed_dependencies = $this->getPluginRemovedDependencies($layout_dependencies, $dependencies);
if ($layout_removed_dependencies) {
// @todo Allow the plugins to react to their dependency removal in
// https://www.drupal.org/project/drupal/issues/2579743.
$this->removeSection($delta);
$changed = TRUE;
}
// If the section is not removed, loop through all components.
else {
foreach ($section->getComponents() as $uuid => $component) {
$plugin_dependencies = $this->getPluginDependencies($component->getPlugin());
$component_removed_dependencies = $this->getPluginRemovedDependencies($plugin_dependencies, $dependencies);
if ($component_removed_dependencies) {
// @todo Allow the plugins to react to their dependency removal in
// https://www.drupal.org/project/drupal/issues/2579743.
$section->removeComponent($uuid);
$changed = TRUE;
}
}
}
}
return $changed;
}
/**
* {@inheritdoc}
*/
public function setComponent($name, array $options = []) {
parent::setComponent($name, $options);
// Only continue if Layout Builder is enabled.
if (!$this->isLayoutBuilderEnabled()) {
return $this;
}
// Retrieve the updated options after the parent:: call.
$options = $this->content[$name];
// Provide backwards compatibility by converting to a section component.
$field_definition = $this->getFieldDefinition($name);
$extra_fields = $this->entityFieldManager->getExtraFields($this->getTargetEntityTypeId(), $this->getTargetBundle());
$is_view_configurable_non_extra_field = $field_definition && $field_definition->isDisplayConfigurable('view') && isset($options['type']);
if ($is_view_configurable_non_extra_field || isset($extra_fields['display'][$name])) {
$configuration = [
'label_display' => '0',
'context_mapping' => ['entity' => 'layout_builder.entity'],
];
if ($is_view_configurable_non_extra_field) {
$configuration['id'] = 'field_block:' . $this->getTargetEntityTypeId() . ':' . $this->getTargetBundle() . ':' . $name;
$keys = array_flip(['type', 'label', 'settings', 'third_party_settings']);
$configuration['formatter'] = array_intersect_key($options, $keys);
}
else {
$configuration['id'] = 'extra_field_block:' . $this->getTargetEntityTypeId() . ':' . $this->getTargetBundle() . ':' . $name;
}
$section = $this->getDefaultSection();
$region = $options['region'] ?? $section->getDefaultRegion();
$new_component = (new SectionComponent(\Drupal::service('uuid')->generate(), $region, $configuration));
$section->appendComponent($new_component);
}
return $this;
}
/**
* Gets a default section.
*
* @return \Drupal\layout_builder\Section
* The default section.
*/
protected function getDefaultSection() {
// If no section exists, append a new one.
if (!$this->hasSection(0)) {
$this->appendSection(new Section('layout_onecol'));
}
// Return the first section.
return $this->getSection(0);
}
/**
* Gets the section storage manager.
*
* @return \Drupal\layout_builder\SectionStorage\SectionStorageManagerInterface
* The section storage manager.
*/
private function sectionStorageManager() {
return \Drupal::service('plugin.manager.layout_builder.section_storage');
}
/**
* {@inheritdoc}
*/
public function getComponent($name) {
if ($this->isLayoutBuilderEnabled() && $section_component = $this->getSectionComponentForFieldName($name)) {
$plugin = $section_component->getPlugin();
if ($plugin instanceof ConfigurableInterface) {
$configuration = $plugin->getConfiguration();
if (isset($configuration['formatter'])) {
return $configuration['formatter'];
}
}
}
return parent::getComponent($name);
}
/**
* Gets the component for a given field name if any.
*
* @param string $field_name
* The field name.
*
* @return \Drupal\layout_builder\SectionComponent|null
* The section component if it is available.
*/
private function getSectionComponentForFieldName($field_name) {
// Loop through every component until the first match is found.
foreach ($this->getSections() as $section) {
foreach ($section->getComponents() as $component) {
$plugin = $component->getPlugin();
if ($plugin instanceof DerivativeInspectionInterface && in_array($plugin->getBaseId(), ['field_block', 'extra_field_block'], TRUE)) {
// FieldBlock derivative IDs are in the format
// [entity_type]:[bundle]:[field].
[, , $field_block_field_name] = explode(PluginBase::DERIVATIVE_SEPARATOR, $plugin->getDerivativeId());
if ($field_block_field_name === $field_name) {
return $component;
}
}
}
}
return NULL;
}
}

View File

@@ -0,0 +1,44 @@
<?php
namespace Drupal\layout_builder\Entity;
use Drupal\Core\Config\Entity\ConfigEntityStorage;
use Drupal\Core\Entity\EntityInterface;
use Drupal\layout_builder\Section;
/**
* Provides storage for entity view display entities that have layouts.
*
* @internal
* Entity handlers are internal.
*/
class LayoutBuilderEntityViewDisplayStorage extends ConfigEntityStorage {
/**
* {@inheritdoc}
*/
protected function mapToStorageRecord(EntityInterface $entity) {
$record = parent::mapToStorageRecord($entity);
if (!empty($record['third_party_settings']['layout_builder']['sections'])) {
$record['third_party_settings']['layout_builder']['sections'] = array_map(function (Section $section) {
return $section->toArray();
}, $record['third_party_settings']['layout_builder']['sections']);
}
return $record;
}
/**
* {@inheritdoc}
*/
protected function mapFromStorageRecords(array $records) {
foreach ($records as &$record) {
if (!empty($record['third_party_settings']['layout_builder']['sections'])) {
$sections = &$record['third_party_settings']['layout_builder']['sections'];
$sections = array_map([Section::class, 'fromArray'], $sections);
}
}
return parent::mapFromStorageRecords($records);
}
}

View File

@@ -0,0 +1,71 @@
<?php
namespace Drupal\layout_builder\Entity;
use Drupal\Core\Entity\ContentEntityStorageInterface;
use Drupal\Core\Entity\EntityTypeManagerInterface;
use Drupal\Core\TempStore\SharedTempStoreFactory;
/**
* Generates a sample entity for use by the Layout Builder.
*/
class LayoutBuilderSampleEntityGenerator implements SampleEntityGeneratorInterface {
/**
* The shared tempstore factory.
*
* @var \Drupal\Core\TempStore\SharedTempStoreFactory
*/
protected $tempStoreFactory;
/**
* The entity type manager.
*
* @var \Drupal\Core\Entity\EntityTypeManagerInterface
*/
protected $entityTypeManager;
/**
* LayoutBuilderSampleEntityGenerator constructor.
*
* @param \Drupal\Core\TempStore\SharedTempStoreFactory $temp_store_factory
* The tempstore factory.
* @param \Drupal\Core\Entity\EntityTypeManagerInterface $entity_type_manager
* The entity type manager.
*/
public function __construct(SharedTempStoreFactory $temp_store_factory, EntityTypeManagerInterface $entity_type_manager) {
$this->tempStoreFactory = $temp_store_factory;
$this->entityTypeManager = $entity_type_manager;
}
/**
* {@inheritdoc}
*/
public function get($entity_type_id, $bundle_id) {
$tempstore = $this->tempStoreFactory->get('layout_builder.sample_entity');
if ($entity = $tempstore->get("$entity_type_id.$bundle_id")) {
return $entity;
}
$entity_storage = $this->entityTypeManager->getStorage($entity_type_id);
if (!$entity_storage instanceof ContentEntityStorageInterface) {
throw new \InvalidArgumentException(sprintf('The "%s" entity storage is not supported', $entity_type_id));
}
$entity = $entity_storage->createWithSampleValues($bundle_id);
// Mark the sample entity as being a preview.
$entity->in_preview = TRUE;
$tempstore->set("$entity_type_id.$bundle_id", $entity);
return $entity;
}
/**
* {@inheritdoc}
*/
public function delete($entity_type_id, $bundle_id) {
$tempstore = $this->tempStoreFactory->get('layout_builder.sample_entity');
$tempstore->delete("$entity_type_id.$bundle_id");
return $this;
}
}

View File

@@ -0,0 +1,13 @@
<?php
namespace Drupal\layout_builder\Entity;
use Drupal\Core\Entity\Display\EntityDisplayInterface;
use Drupal\layout_builder\LayoutBuilderEnabledInterface;
use Drupal\layout_builder\SectionListInterface;
use Drupal\layout_builder\LayoutBuilderOverridableInterface;
/**
* Provides an interface for entity displays that have layout.
*/
interface LayoutEntityDisplayInterface extends EntityDisplayInterface, SectionListInterface, LayoutBuilderEnabledInterface, LayoutBuilderOverridableInterface {}

View File

@@ -0,0 +1,35 @@
<?php
namespace Drupal\layout_builder\Entity;
/**
* Generates a sample entity.
*/
interface SampleEntityGeneratorInterface {
/**
* Gets a sample entity for a given entity type and bundle.
*
* @param string $entity_type_id
* The entity type ID.
* @param string $bundle_id
* The bundle ID.
*
* @return \Drupal\Core\Entity\EntityInterface
* An entity.
*/
public function get($entity_type_id, $bundle_id);
/**
* Deletes a sample entity for a given entity type and bundle.
*
* @param string $entity_type_id
* The entity type ID.
* @param string $bundle_id
* The bundle ID.
*
* @return $this
*/
public function delete($entity_type_id, $bundle_id);
}

View File

@@ -0,0 +1,45 @@
<?php
namespace Drupal\layout_builder\Event;
use Drupal\layout_builder\SectionStorageInterface;
use Drupal\Component\EventDispatcher\Event;
/**
* Event fired in #pre_render of \Drupal\layout_builder\Element\LayoutBuilder.
*
* Subscribers to this event can prepare section storage before rendering.
*
* @see \Drupal\layout_builder\LayoutBuilderEvents::PREPARE_LAYOUT
* @see \Drupal\layout_builder\Element\LayoutBuilder::prepareLayout()
*/
class PrepareLayoutEvent extends Event {
/**
* The section storage plugin.
*
* @var \Drupal\layout_builder\SectionStorageInterface
*/
protected $sectionStorage;
/**
* Constructs a new PrepareLayoutEvent.
*
* @param \Drupal\layout_builder\SectionStorageInterface $section_storage
* The section storage preparing the Layout.
*/
public function __construct(SectionStorageInterface $section_storage) {
$this->sectionStorage = $section_storage;
}
/**
* Gets the section storage.
*
* @return \Drupal\layout_builder\SectionStorageInterface
* The section storage.
*/
public function getSectionStorage(): SectionStorageInterface {
return $this->sectionStorage;
}
}

View File

@@ -0,0 +1,138 @@
<?php
namespace Drupal\layout_builder\Event;
use Drupal\Core\Cache\CacheableResponseTrait;
use Drupal\Core\Plugin\PreviewAwarePluginInterface;
use Drupal\layout_builder\SectionComponent;
use Drupal\Component\EventDispatcher\Event;
/**
* Event fired when a section component's render array is being built.
*
* Subscribers to this event should manipulate the cacheability object and the
* build array in this event.
*
* @see \Drupal\layout_builder\LayoutBuilderEvents::SECTION_COMPONENT_BUILD_RENDER_ARRAY
*/
class SectionComponentBuildRenderArrayEvent extends Event {
use CacheableResponseTrait;
/**
* The section component whose render array is being built.
*
* @var \Drupal\layout_builder\SectionComponent
*/
protected $component;
/**
* The available contexts.
*
* @var \Drupal\Core\Plugin\Context\ContextInterface[]
*/
protected $contexts;
/**
* The plugin for the section component being built.
*
* @var \Drupal\Component\Plugin\PluginInspectionInterface
*/
protected $plugin;
/**
* Whether the component is in preview mode or not.
*
* @var bool
*/
protected $inPreview;
/**
* The render array built by the event subscribers.
*
* @var array
*/
protected $build = [];
/**
* Creates a new SectionComponentBuildRenderArrayEvent object.
*
* @param \Drupal\layout_builder\SectionComponent $component
* The section component whose render array is being built.
* @param \Drupal\Core\Plugin\Context\ContextInterface[] $contexts
* The available contexts.
* @param bool $in_preview
* (optional) Whether the component is in preview mode or not.
*/
public function __construct(SectionComponent $component, array $contexts, $in_preview = FALSE) {
$this->component = $component;
$this->contexts = $contexts;
$this->plugin = $component->getPlugin($contexts);
$this->inPreview = $in_preview;
if ($this->plugin instanceof PreviewAwarePluginInterface) {
$this->plugin->setInPreview($in_preview);
}
}
/**
* Get the section component whose render array is being built.
*
* @return \Drupal\layout_builder\SectionComponent
* The section component whose render array is being built.
*/
public function getComponent() {
return $this->component;
}
/**
* Get the available contexts.
*
* @return array|\Drupal\Core\Plugin\Context\ContextInterface[]
* The available contexts.
*/
public function getContexts() {
return $this->contexts;
}
/**
* Get the plugin for the section component being built.
*
* @return \Drupal\Component\Plugin\PluginInspectionInterface
* The plugin for the section component being built.
*/
public function getPlugin() {
return $this->plugin;
}
/**
* Determine if the component is in preview mode.
*
* @return bool
* Whether the component is in preview mode or not.
*/
public function inPreview() {
return $this->inPreview;
}
/**
* Get the render array in its current state.
*
* @return array
* The render array built by the event subscribers.
*/
public function getBuild() {
return $this->build;
}
/**
* Set the render array.
*
* @param array $build
* A render array.
*/
public function setBuild(array $build) {
$this->build = $build;
}
}

View File

@@ -0,0 +1,175 @@
<?php
namespace Drupal\layout_builder\EventSubscriber;
use Drupal\block_content\Access\RefinableDependentAccessInterface;
use Drupal\Core\Access\AccessResult;
use Drupal\Core\Block\BlockPluginInterface;
use Drupal\Core\Cache\CacheableMetadata;
use Drupal\Core\Render\Element;
use Drupal\Core\Render\PreviewFallbackInterface;
use Drupal\Core\Session\AccountInterface;
use Drupal\Core\StringTranslation\StringTranslationTrait;
use Drupal\layout_builder\Access\LayoutPreviewAccessAllowed;
use Drupal\layout_builder\Event\SectionComponentBuildRenderArrayEvent;
use Drupal\layout_builder\Plugin\Block\InlineBlock;
use Drupal\layout_builder\LayoutBuilderEvents;
use Drupal\views\Plugin\Block\ViewsBlock;
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
/**
* Builds render arrays and handles access for all block components.
*
* @internal
* Tagged services are internal.
*/
class BlockComponentRenderArray implements EventSubscriberInterface {
use StringTranslationTrait;
/**
* The current user.
*
* @var \Drupal\Core\Session\AccountInterface
*/
protected $currentUser;
/**
* Creates a BlockComponentRenderArray object.
*
* @param \Drupal\Core\Session\AccountInterface $current_user
* The current user.
*/
public function __construct(AccountInterface $current_user) {
$this->currentUser = $current_user;
}
/**
* {@inheritdoc}
*/
public static function getSubscribedEvents(): array {
$events[LayoutBuilderEvents::SECTION_COMPONENT_BUILD_RENDER_ARRAY] = ['onBuildRender', 100];
return $events;
}
/**
* Builds render arrays for block plugins and sets it on the event.
*
* @param \Drupal\layout_builder\Event\SectionComponentBuildRenderArrayEvent $event
* The section component render event.
*/
public function onBuildRender(SectionComponentBuildRenderArrayEvent $event) {
$block = $event->getPlugin();
if (!$block instanceof BlockPluginInterface) {
return;
}
// Set block access dependency even if we are not checking access on
// this level. The block itself may render another
// RefinableDependentAccessInterface object and need to pass on this value.
if ($block instanceof RefinableDependentAccessInterface) {
$contexts = $event->getContexts();
if (isset($contexts['layout_builder.entity'])) {
if ($entity = $contexts['layout_builder.entity']->getContextValue()) {
if ($event->inPreview()) {
// If previewing in Layout Builder allow access.
$block->setAccessDependency(new LayoutPreviewAccessAllowed());
}
else {
$block->setAccessDependency($entity);
}
}
}
}
// Only check access if the component is not being previewed.
if ($event->inPreview()) {
$access = AccessResult::allowed()->setCacheMaxAge(0);
}
else {
$access = $block->access($this->currentUser, TRUE);
}
$event->addCacheableDependency($access);
if ($access->isAllowed()) {
$event->addCacheableDependency($block);
// @todo Revisit after https://www.drupal.org/node/3027653, as this will
// provide a better way to remove contextual links from Views blocks.
// Currently, doing this requires setting
// \Drupal\views\ViewExecutable::$showAdminLinks() to false before the
// Views block is built.
if ($block instanceof ViewsBlock && $event->inPreview()) {
$block->getViewExecutable()->setShowAdminLinks(FALSE);
}
$content = $block->build();
// We don't output the block render data if there are no render elements
// found, but we want to capture the cache metadata from the block
// regardless.
$event->addCacheableDependency(CacheableMetadata::createFromRenderArray($content));
$is_content_empty = Element::isEmpty($content);
$is_placeholder_ready = $event->inPreview() && $block instanceof PreviewFallbackInterface;
// If the content is empty and no placeholder is available, return.
if ($is_content_empty && !$is_placeholder_ready) {
return;
}
$build = [
// @todo Move this to BlockBase in https://www.drupal.org/node/2931040.
'#theme' => 'block',
'#configuration' => $block->getConfiguration(),
'#plugin_id' => $block->getPluginId(),
'#base_plugin_id' => $block->getBaseId(),
'#derivative_plugin_id' => $block->getDerivativeId(),
'#in_preview' => $event->inPreview(),
'#weight' => $event->getComponent()->getWeight(),
];
// Place the $content returned by the block plugin into a 'content' child
// element, as a way to allow the plugin to have complete control of its
// properties and rendering (for instance, its own #theme) without
// conflicting with the properties used above, or alternate ones used by
// alternate block rendering approaches in contributed modules. However,
// the use of a child element is an implementation detail of this
// particular block rendering approach. Semantically, the content returned
// by the block plugin, and in particular, attributes and contextual links
// are information that belong to the entire block. Therefore, we must
// move these properties from $content and merge them into the top-level
// element.
if (isset($content['#attributes'])) {
$build['#attributes'] = $content['#attributes'];
unset($content['#attributes']);
}
// Hide contextual links for inline blocks until the UX issues surrounding
// editing them directly are resolved.
// @see https://www.drupal.org/project/drupal/issues/3075308
if (!$block instanceof InlineBlock && !empty($content['#contextual_links'])) {
$build['#contextual_links'] = $content['#contextual_links'];
}
$build['content'] = $content;
if ($event->inPreview()) {
if ($block instanceof PreviewFallbackInterface) {
$preview_fallback_string = $block->getPreviewFallbackString();
}
else {
$preview_fallback_string = $this->t('"@block" block', ['@block' => $block->label()]);
}
// @todo Use new label methods so
// data-layout-content-preview-placeholder-label doesn't have to use
// preview fallback in https://www.drupal.org/node/2025649.
$build['#attributes']['data-layout-content-preview-placeholder-label'] = $preview_fallback_string;
if ($is_content_empty && $is_placeholder_ready) {
$build['content']['#markup'] = $this->t('Placeholder for the @preview_fallback', ['@preview_fallback' => $block->getPreviewFallbackString()]);
}
}
$event->setBuild($build);
}
}
}

View File

@@ -0,0 +1,88 @@
<?php
namespace Drupal\layout_builder\EventSubscriber;
use Drupal\Core\Messenger\MessengerInterface;
use Drupal\Core\StringTranslation\StringTranslationTrait;
use Drupal\layout_builder\Event\PrepareLayoutEvent;
use Drupal\layout_builder\LayoutBuilderEvents;
use Drupal\layout_builder\LayoutTempstoreRepositoryInterface;
use Drupal\layout_builder\OverridesSectionStorageInterface;
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
/**
* An event subscriber to prepare section storage.
*
* Section storage works via the
* \Drupal\layout_builder\Event\PrepareLayoutEvent.
*
* @see \Drupal\layout_builder\Event\PrepareLayoutEvent
* @see \Drupal\layout_builder\Element\LayoutBuilder::prepareLayout()
*/
class PrepareLayout implements EventSubscriberInterface {
use StringTranslationTrait;
/**
* The layout tempstore repository.
*
* @var \Drupal\layout_builder\LayoutTempstoreRepositoryInterface
*/
protected $layoutTempstoreRepository;
/**
* The messenger service.
*
* @var \Drupal\Core\Messenger\MessengerInterface
*/
protected $messenger;
/**
* Constructs a new PrepareLayout.
*
* @param \Drupal\layout_builder\LayoutTempstoreRepositoryInterface $layout_tempstore_repository
* The tempstore repository.
* @param \Drupal\Core\Messenger\MessengerInterface $messenger
* The messenger service.
*/
public function __construct(LayoutTempstoreRepositoryInterface $layout_tempstore_repository, MessengerInterface $messenger) {
$this->layoutTempstoreRepository = $layout_tempstore_repository;
$this->messenger = $messenger;
}
/**
* {@inheritdoc}
*/
public static function getSubscribedEvents(): array {
$events[LayoutBuilderEvents::PREPARE_LAYOUT][] = ['onPrepareLayout', 10];
return $events;
}
/**
* Prepares a layout for use in the UI.
*
* @param \Drupal\layout_builder\Event\PrepareLayoutEvent $event
* The prepare layout event.
*/
public function onPrepareLayout(PrepareLayoutEvent $event) {
$section_storage = $event->getSectionStorage();
// If the layout has pending changes, add a warning.
if ($this->layoutTempstoreRepository->has($section_storage)) {
$this->messenger->addWarning($this->t('You have unsaved changes.'));
}
else {
// If the layout is an override that has not yet been overridden, copy the
// sections from the corresponding default.
if ($section_storage instanceof OverridesSectionStorageInterface && !$section_storage->isOverridden()) {
$sections = $section_storage->getDefaultSectionStorage()->getSections();
foreach ($sections as $section) {
$section_storage->appendSection($section);
}
}
// Add storage to tempstore regardless of what the storage is.
$this->layoutTempstoreRepository->set($section_storage);
}
}
}

View File

@@ -0,0 +1,160 @@
<?php
namespace Drupal\layout_builder\EventSubscriber;
use Drupal\block_content\BlockContentEvents;
use Drupal\block_content\BlockContentInterface;
use Drupal\block_content\Event\BlockContentGetDependencyEvent;
use Drupal\Core\Database\Connection;
use Drupal\Core\Entity\EntityInterface;
use Drupal\Core\Entity\EntityTypeManagerInterface;
use Drupal\layout_builder\InlineBlockUsageInterface;
use Drupal\layout_builder\LayoutEntityHelperTrait;
use Drupal\layout_builder\SectionStorage\SectionStorageManagerInterface;
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
/**
* An event subscriber that returns an access dependency for inline blocks.
*
* When used within the layout builder the access dependency for inline blocks
* will be explicitly set but if access is evaluated outside of the layout
* builder then the dependency may not have been set.
*
* A known example of when the access dependency will not have been set is when
* determining 'view' or 'download' access to a file entity that is attached
* to a content block via a field that is using the private file system. The
* file access handler will evaluate access on the content block without setting
* the dependency.
*
* @internal
* Tagged services are internal.
*
* @see \Drupal\file\FileAccessControlHandler::checkAccess()
* @see \Drupal\block_content\BlockContentAccessControlHandler::checkAccess()
*/
class SetInlineBlockDependency implements EventSubscriberInterface {
use LayoutEntityHelperTrait;
/**
* The entity type manager.
*
* @var \Drupal\Core\Entity\EntityTypeManagerInterface
*/
protected $entityTypeManager;
/**
* The database connection.
*
* @var \Drupal\Core\Database\Connection
*/
protected $database;
/**
* The inline block usage service.
*
* @var \Drupal\layout_builder\InlineBlockUsageInterface
*/
protected $usage;
/**
* Constructs SetInlineBlockDependency object.
*
* @param \Drupal\Core\Entity\EntityTypeManagerInterface $entity_type_manager
* The entity type manager.
* @param \Drupal\Core\Database\Connection $database
* The database connection.
* @param \Drupal\layout_builder\InlineBlockUsageInterface $usage
* The inline block usage service.
* @param \Drupal\layout_builder\SectionStorage\SectionStorageManagerInterface $section_storage_manager
* The section storage manager.
*/
public function __construct(EntityTypeManagerInterface $entity_type_manager, Connection $database, InlineBlockUsageInterface $usage, SectionStorageManagerInterface $section_storage_manager) {
$this->entityTypeManager = $entity_type_manager;
$this->database = $database;
$this->usage = $usage;
$this->sectionStorageManager = $section_storage_manager;
}
/**
* {@inheritdoc}
*/
public static function getSubscribedEvents(): array {
return [
BlockContentEvents::BLOCK_CONTENT_GET_DEPENDENCY => 'onGetDependency',
];
}
/**
* Handles the BlockContentEvents::INLINE_BLOCK_GET_DEPENDENCY event.
*
* @param \Drupal\block_content\Event\BlockContentGetDependencyEvent $event
* The event.
*/
public function onGetDependency(BlockContentGetDependencyEvent $event) {
if ($dependency = $this->getInlineBlockDependency($event->getBlockContentEntity())) {
$event->setAccessDependency($dependency);
}
}
/**
* Get the access dependency of an inline block.
*
* If the block is used in an entity that entity will be returned as the
* dependency.
*
* For revisionable entities the entity will only be returned if it is used in
* the latest revision of the entity. For inline blocks that are not used in
* the latest revision but are used in a previous revision the entity will not
* be returned because calling
* \Drupal\Core\Access\AccessibleInterface::access() will only check access on
* the latest revision. Therefore if the previous revision of the entity was
* returned as the dependency access would be granted to inline block
* regardless of whether the user has access to the revision in which the
* inline block was used.
*
* @param \Drupal\block_content\BlockContentInterface $block_content
* The block content entity.
*
* @return \Drupal\Core\Entity\EntityInterface|null
* Returns the layout dependency.
*
* @see \Drupal\block_content\BlockContentAccessControlHandler::checkAccess()
* @see \Drupal\layout_builder\EventSubscriber\BlockComponentRenderArray::onBuildRender()
*/
protected function getInlineBlockDependency(BlockContentInterface $block_content) {
$layout_entity_info = $this->usage->getUsage($block_content->id());
if (empty($layout_entity_info)) {
// If the block does not have usage information then we cannot set a
// dependency. It may be used by another module besides layout builder.
return NULL;
}
$layout_entity_storage = $this->entityTypeManager->getStorage($layout_entity_info->layout_entity_type);
$layout_entity = $layout_entity_storage->load($layout_entity_info->layout_entity_id);
if ($this->isLayoutCompatibleEntity($layout_entity)) {
if ($this->isBlockRevisionUsedInEntity($layout_entity, $block_content)) {
return $layout_entity;
}
}
return NULL;
}
/**
* Determines if a block content revision is used in an entity.
*
* @param \Drupal\Core\Entity\EntityInterface $layout_entity
* The layout entity.
* @param \Drupal\block_content\BlockContentInterface $block_content
* The block content revision.
*
* @return bool
* TRUE if the block content revision is used as an inline block in the
* layout entity.
*/
protected function isBlockRevisionUsedInEntity(EntityInterface $layout_entity, BlockContentInterface $block_content) {
$sections_blocks_revision_ids = $this->getInlineBlockRevisionIdsInSections($this->getEntitySections($layout_entity));
return in_array($block_content->getRevisionId(), $sections_blocks_revision_ids);
}
}

View File

@@ -0,0 +1,108 @@
<?php
namespace Drupal\layout_builder\Field;
use Drupal\Core\Access\AccessResult;
use Drupal\Core\Field\FieldItemList;
use Drupal\Core\Field\FieldItemListInterface;
use Drupal\Core\Session\AccountInterface;
use Drupal\layout_builder\Section;
use Drupal\layout_builder\SectionListInterface;
use Drupal\layout_builder\SectionListTrait;
/**
* Defines an item list class for layout section fields.
*
* @internal
* Plugin classes are internal.
*
* @see \Drupal\layout_builder\Plugin\Field\FieldType\LayoutSectionItem
*/
class LayoutSectionItemList extends FieldItemList implements SectionListInterface {
use SectionListTrait;
/**
* Numerically indexed array of field items.
*
* @var \Drupal\layout_builder\Plugin\Field\FieldType\LayoutSectionItem[]
*/
protected $list = [];
/**
* {@inheritdoc}
*/
public function getSections() {
$sections = [];
foreach ($this->list as $delta => $item) {
$sections[$delta] = $item->section;
}
return $sections;
}
/**
* {@inheritdoc}
*/
protected function setSections(array $sections) {
$this->list = [];
$sections = array_values($sections);
/** @var \Drupal\layout_builder\Plugin\Field\FieldType\LayoutSectionItem $item */
foreach ($sections as $section) {
$item = $this->appendItem();
$item->section = $section;
}
return $this;
}
/**
* {@inheritdoc}
*/
public function getEntity() {
$entity = parent::getEntity();
// Ensure the entity is updated with the latest value.
$entity->set($this->getName(), $this->getValue());
return $entity;
}
/**
* {@inheritdoc}
*/
public function preSave() {
parent::preSave();
// Loop through each section and reconstruct it to ensure that all default
// values are present.
foreach ($this->list as $item) {
$item->section = Section::fromArray($item->section->toArray());
}
}
/**
* {@inheritdoc}
*/
public function equals(FieldItemListInterface $list_to_compare) {
if (!$list_to_compare instanceof LayoutSectionItemList) {
return FALSE;
}
// Convert arrays of section objects to array values for comparison.
$convert = function (LayoutSectionItemList $list) {
return array_map(function (Section $section) {
return $section->toArray();
}, $list->getSections());
};
return $convert($this) === $convert($list_to_compare);
}
/**
* Overrides \Drupal\Core\Field\FieldItemListInterface::defaultAccess().
*
* @ingroup layout_builder_access
*/
public function defaultAccess($operation = 'view', ?AccountInterface $account = NULL) {
// @todo Allow access in https://www.drupal.org/node/2942975.
return AccessResult::forbidden();
}
}

View File

@@ -0,0 +1,64 @@
<?php
namespace Drupal\layout_builder\Form;
use Drupal\Core\Form\FormStateInterface;
use Drupal\layout_builder\LayoutBuilderHighlightTrait;
use Drupal\layout_builder\SectionComponent;
use Drupal\layout_builder\SectionStorageInterface;
/**
* Provides a form to add a block.
*
* @internal
* Form classes are internal.
*/
class AddBlockForm extends ConfigureBlockFormBase {
use LayoutBuilderHighlightTrait;
/**
* {@inheritdoc}
*/
public function getFormId() {
return 'layout_builder_add_block';
}
/**
* {@inheritdoc}
*/
protected function submitLabel() {
return $this->t('Add block');
}
/**
* Builds the form for the block.
*
* @param array $form
* An associative array containing the structure of the form.
* @param \Drupal\Core\Form\FormStateInterface $form_state
* The current state of the form.
* @param \Drupal\layout_builder\SectionStorageInterface $section_storage
* The section storage being configured.
* @param int $delta
* The delta of the section.
* @param string $region
* The region of the block.
* @param string|null $plugin_id
* The plugin ID of the block to add.
*
* @return array
* The form array.
*/
public function buildForm(array $form, FormStateInterface $form_state, ?SectionStorageInterface $section_storage = NULL, $delta = NULL, $region = NULL, $plugin_id = NULL) {
// Only generate a new component once per form submission.
if (!$component = $form_state->get('layout_builder__component')) {
$component = new SectionComponent($this->uuidGenerator->generate(), $region, ['id' => $plugin_id]);
$section_storage->getSection($delta)->appendComponent($component);
$form_state->set('layout_builder__component', $component);
}
$form['#attributes']['data-layout-builder-target-highlight-id'] = $this->blockAddHighlightId($delta, $region);
return $this->doBuildForm($form, $form_state, $section_storage, $delta, $component);
}
}

View File

@@ -0,0 +1,290 @@
<?php
namespace Drupal\layout_builder\Form;
use Drupal\Component\Utility\Html;
use Drupal\Component\Uuid\UuidInterface;
use Drupal\Core\Ajax\AjaxFormHelperTrait;
use Drupal\Core\Block\BlockManagerInterface;
use Drupal\Core\Block\BlockPluginInterface;
use Drupal\Core\Form\BaseFormIdInterface;
use Drupal\Core\Form\FormBase;
use Drupal\Core\Form\FormStateInterface;
use Drupal\Core\Form\SubformState;
use Drupal\Core\Form\WorkspaceDynamicSafeFormInterface;
use Drupal\Core\Plugin\Context\ContextRepositoryInterface;
use Drupal\Core\Plugin\ContextAwarePluginAssignmentTrait;
use Drupal\Core\Plugin\ContextAwarePluginInterface;
use Drupal\Core\Plugin\PluginFormFactoryInterface;
use Drupal\Core\Plugin\PluginWithFormsInterface;
use Drupal\layout_builder\Context\LayoutBuilderContextTrait;
use Drupal\layout_builder\Controller\LayoutRebuildTrait;
use Drupal\layout_builder\LayoutTempstoreRepositoryInterface;
use Drupal\layout_builder\SectionComponent;
use Drupal\layout_builder\SectionStorageInterface;
use Symfony\Component\DependencyInjection\ContainerInterface;
/**
* Provides a base form for configuring a block.
*
* @internal
* Form classes are internal.
*/
abstract class ConfigureBlockFormBase extends FormBase implements BaseFormIdInterface, WorkspaceDynamicSafeFormInterface {
use AjaxFormHelperTrait;
use ContextAwarePluginAssignmentTrait;
use LayoutBuilderContextTrait;
use LayoutRebuildTrait;
use WorkspaceSafeFormTrait;
/**
* The plugin being configured.
*
* @var \Drupal\Core\Block\BlockPluginInterface
*/
protected $block;
/**
* The layout tempstore repository.
*
* @var \Drupal\layout_builder\LayoutTempstoreRepositoryInterface
*/
protected $layoutTempstoreRepository;
/**
* The block manager.
*
* @var \Drupal\Core\Block\BlockManagerInterface
*/
protected $blockManager;
/**
* The UUID generator.
*
* @var \Drupal\Component\Uuid\UuidInterface
*/
protected $uuidGenerator;
/**
* The plugin form manager.
*
* @var \Drupal\Core\Plugin\PluginFormFactoryInterface
*/
protected $pluginFormFactory;
/**
* The field delta.
*
* @var int
*/
protected $delta;
/**
* The current region.
*
* @var string
*/
protected $region;
/**
* The UUID of the component.
*
* @var string
*/
protected $uuid;
/**
* The section storage.
*
* @var \Drupal\layout_builder\SectionStorageInterface
*/
protected $sectionStorage;
/**
* Constructs a new block form.
*
* @param \Drupal\layout_builder\LayoutTempstoreRepositoryInterface $layout_tempstore_repository
* The layout tempstore repository.
* @param \Drupal\Core\Plugin\Context\ContextRepositoryInterface $context_repository
* The context repository.
* @param \Drupal\Core\Block\BlockManagerInterface $block_manager
* The block manager.
* @param \Drupal\Component\Uuid\UuidInterface $uuid
* The UUID generator.
* @param \Drupal\Core\Plugin\PluginFormFactoryInterface $plugin_form_manager
* The plugin form manager.
*/
public function __construct(LayoutTempstoreRepositoryInterface $layout_tempstore_repository, ContextRepositoryInterface $context_repository, BlockManagerInterface $block_manager, UuidInterface $uuid, PluginFormFactoryInterface $plugin_form_manager) {
$this->layoutTempstoreRepository = $layout_tempstore_repository;
$this->contextRepository = $context_repository;
$this->blockManager = $block_manager;
$this->uuidGenerator = $uuid;
$this->pluginFormFactory = $plugin_form_manager;
}
/**
* {@inheritdoc}
*/
public static function create(ContainerInterface $container) {
return new static(
$container->get('layout_builder.tempstore_repository'),
$container->get('context.repository'),
$container->get('plugin.manager.block'),
$container->get('uuid'),
$container->get('plugin_form.factory')
);
}
/**
* {@inheritdoc}
*/
public function getBaseFormId() {
return 'layout_builder_configure_block';
}
/**
* Builds the form for the block.
*
* @param array $form
* An associative array containing the structure of the form.
* @param \Drupal\Core\Form\FormStateInterface $form_state
* The current state of the form.
* @param \Drupal\layout_builder\SectionStorageInterface $section_storage
* The section storage being configured.
* @param int $delta
* The delta of the section.
* @param \Drupal\layout_builder\SectionComponent $component
* The section component containing the block.
*
* @return array
* The form array.
*/
public function doBuildForm(array $form, FormStateInterface $form_state, ?SectionStorageInterface $section_storage = NULL, $delta = NULL, ?SectionComponent $component = NULL) {
$this->sectionStorage = $section_storage;
$this->delta = $delta;
$this->uuid = $component->getUuid();
$this->block = $component->getPlugin();
$form_state->setTemporaryValue('gathered_contexts', $this->getPopulatedContexts($section_storage));
$form['#tree'] = TRUE;
$form['settings'] = [];
$subform_state = SubformState::createForSubform($form['settings'], $form, $form_state);
$form['settings'] = $this->getPluginForm($this->block)->buildConfigurationForm($form['settings'], $subform_state);
$form['actions']['submit'] = [
'#type' => 'submit',
'#value' => $this->submitLabel(),
'#button_type' => 'primary',
];
if ($this->isAjax()) {
$form['actions']['submit']['#ajax']['callback'] = '::ajaxSubmit';
// @todo static::ajaxSubmit() requires data-drupal-selector to be the same
// between the various Ajax requests. A bug in
// \Drupal\Core\Form\FormBuilder prevents that from happening unless
// $form['#id'] is also the same. Normally, #id is set to a unique HTML
// ID via Html::getUniqueId(), but here we bypass that in order to work
// around the data-drupal-selector bug. This is okay so long as we
// assume that this form only ever occurs once on a page. Remove this
// workaround in https://www.drupal.org/node/2897377.
$form['#id'] = Html::getId($form_state->getBuildInfo()['form_id']);
}
// Mark this as an administrative page for JavaScript ("Back to site" link).
$form['#attached']['drupalSettings']['path']['currentPathIsAdmin'] = TRUE;
return $form;
}
/**
* Returns the label for the submit button.
*
* @return string
* Submit label.
*/
abstract protected function submitLabel();
/**
* {@inheritdoc}
*/
public function validateForm(array &$form, FormStateInterface $form_state) {
$subform_state = SubformState::createForSubform($form['settings'], $form, $form_state);
$this->getPluginForm($this->block)->validateConfigurationForm($form['settings'], $subform_state);
}
/**
* {@inheritdoc}
*/
public function submitForm(array &$form, FormStateInterface $form_state) {
// Call the plugin submit handler.
$subform_state = SubformState::createForSubform($form['settings'], $form, $form_state);
$this->getPluginForm($this->block)->submitConfigurationForm($form, $subform_state);
// If this block is context-aware, set the context mapping.
if ($this->block instanceof ContextAwarePluginInterface) {
$this->block->setContextMapping($subform_state->getValue('context_mapping', []));
}
$configuration = $this->block->getConfiguration();
$section = $this->sectionStorage->getSection($this->delta);
$section->getComponent($this->uuid)->setConfiguration($configuration);
$this->layoutTempstoreRepository->set($this->sectionStorage);
$form_state->setRedirectUrl($this->sectionStorage->getLayoutBuilderUrl());
}
/**
* {@inheritdoc}
*/
protected function successfulAjaxSubmit(array $form, FormStateInterface $form_state) {
return $this->rebuildAndClose($this->sectionStorage);
}
/**
* Retrieves the plugin form for a given block.
*
* @param \Drupal\Core\Block\BlockPluginInterface $block
* The block plugin.
*
* @return \Drupal\Core\Plugin\PluginFormInterface
* The plugin form for the block.
*/
protected function getPluginForm(BlockPluginInterface $block) {
if ($block instanceof PluginWithFormsInterface) {
return $this->pluginFormFactory->createInstance($block, 'configure');
}
return $block;
}
/**
* Retrieves the section storage object.
*
* @return \Drupal\layout_builder\SectionStorageInterface
* The section storage for the current form.
*/
public function getSectionStorage() {
return $this->sectionStorage;
}
/**
* Retrieves the current layout section being edited by the form.
*
* @return \Drupal\layout_builder\Section
* The current layout section.
*/
public function getCurrentSection() {
return $this->sectionStorage->getSection($this->delta);
}
/**
* Retrieves the current component being edited by the form.
*
* @return \Drupal\layout_builder\SectionComponent
* The current section component.
*/
public function getCurrentComponent() {
return $this->getCurrentSection()->getComponent($this->uuid);
}
}

View File

@@ -0,0 +1,278 @@
<?php
namespace Drupal\layout_builder\Form;
use Drupal\Component\Utility\Html;
use Drupal\Core\Ajax\AjaxFormHelperTrait;
use Drupal\Core\Form\FormBase;
use Drupal\Core\Form\FormStateInterface;
use Drupal\Core\Form\SubformState;
use Drupal\Core\Form\WorkspaceDynamicSafeFormInterface;
use Drupal\Core\Layout\LayoutInterface;
use Drupal\Core\Plugin\ContextAwarePluginInterface;
use Drupal\Core\Plugin\PluginFormFactoryInterface;
use Drupal\Core\Plugin\PluginFormInterface;
use Drupal\Core\Plugin\PluginWithFormsInterface;
use Drupal\layout_builder\Context\LayoutBuilderContextTrait;
use Drupal\layout_builder\Controller\LayoutRebuildTrait;
use Drupal\layout_builder\LayoutBuilderHighlightTrait;
use Drupal\layout_builder\LayoutTempstoreRepositoryInterface;
use Drupal\layout_builder\Section;
use Drupal\layout_builder\SectionStorageInterface;
use Symfony\Component\DependencyInjection\ContainerInterface;
/**
* Provides a form for configuring a layout section.
*
* @internal
* Form classes are internal.
*/
class ConfigureSectionForm extends FormBase implements WorkspaceDynamicSafeFormInterface {
use AjaxFormHelperTrait;
use LayoutBuilderContextTrait;
use LayoutBuilderHighlightTrait;
use LayoutRebuildTrait;
use WorkspaceSafeFormTrait;
/**
* The layout tempstore repository.
*
* @var \Drupal\layout_builder\LayoutTempstoreRepositoryInterface
*/
protected $layoutTempstoreRepository;
/**
* The plugin being configured.
*
* @var \Drupal\Core\Layout\LayoutInterface|\Drupal\Core\Plugin\PluginFormInterface
*/
protected $layout;
/**
* The section being configured.
*
* @var \Drupal\layout_builder\Section
*/
protected $section;
/**
* The plugin form manager.
*
* @var \Drupal\Core\Plugin\PluginFormFactoryInterface
*/
protected $pluginFormFactory;
/**
* The section storage.
*
* @var \Drupal\layout_builder\SectionStorageInterface
*/
protected $sectionStorage;
/**
* The field delta.
*
* @var int
*/
protected $delta;
/**
* The plugin ID.
*
* @var string
*/
protected $pluginId;
/**
* Indicates whether the section is being added or updated.
*
* @var bool
*/
protected $isUpdate;
/**
* Constructs a new ConfigureSectionForm.
*
* @param \Drupal\layout_builder\LayoutTempstoreRepositoryInterface $layout_tempstore_repository
* The layout tempstore repository.
* @param \Drupal\Core\Plugin\PluginFormFactoryInterface $plugin_form_manager
* The plugin form manager.
*/
public function __construct(LayoutTempstoreRepositoryInterface $layout_tempstore_repository, PluginFormFactoryInterface $plugin_form_manager) {
$this->layoutTempstoreRepository = $layout_tempstore_repository;
$this->pluginFormFactory = $plugin_form_manager;
}
/**
* {@inheritdoc}
*/
public static function create(ContainerInterface $container) {
return new static(
$container->get('layout_builder.tempstore_repository'),
$container->get('plugin_form.factory')
);
}
/**
* {@inheritdoc}
*/
public function getFormId() {
return 'layout_builder_configure_section';
}
/**
* {@inheritdoc}
*/
public function buildForm(array $form, FormStateInterface $form_state, ?SectionStorageInterface $section_storage = NULL, $delta = NULL, $plugin_id = NULL) {
$this->sectionStorage = $section_storage;
$this->delta = $delta;
$this->isUpdate = is_null($plugin_id);
$this->pluginId = $plugin_id;
$section = $this->getCurrentSection();
if ($this->isUpdate) {
if ($label = $section->getLayoutSettings()['label']) {
$form['#title'] = $this->t('Configure @section', ['@section' => $label]);
}
}
// Passing available contexts to the layout plugin here could result in an
// exception since the layout may not have a context mapping for a required
// context slot on creation.
$this->layout = $section->getLayout();
$form_state->setTemporaryValue('gathered_contexts', $this->getPopulatedContexts($this->sectionStorage));
$form['#tree'] = TRUE;
$form['layout_settings'] = [];
$subform_state = SubformState::createForSubform($form['layout_settings'], $form, $form_state);
$form['layout_settings'] = $this->getPluginForm($this->layout)->buildConfigurationForm($form['layout_settings'], $subform_state);
$form['actions']['submit'] = [
'#type' => 'submit',
'#value' => $this->isUpdate ? $this->t('Update') : $this->t('Add section'),
'#button_type' => 'primary',
];
if ($this->isAjax()) {
$form['actions']['submit']['#ajax']['callback'] = '::ajaxSubmit';
// @todo static::ajaxSubmit() requires data-drupal-selector to be the same
// between the various Ajax requests. A bug in
// \Drupal\Core\Form\FormBuilder prevents that from happening unless
// $form['#id'] is also the same. Normally, #id is set to a unique HTML
// ID via Html::getUniqueId(), but here we bypass that in order to work
// around the data-drupal-selector bug. This is okay so long as we
// assume that this form only ever occurs once on a page. Remove this
// workaround in https://www.drupal.org/node/2897377.
$form['#id'] = Html::getId($form_state->getBuildInfo()['form_id']);
}
$target_highlight_id = $this->isUpdate ? $this->sectionUpdateHighlightId($delta) : $this->sectionAddHighlightId($delta);
$form['#attributes']['data-layout-builder-target-highlight-id'] = $target_highlight_id;
// Mark this as an administrative page for JavaScript ("Back to site" link).
$form['#attached']['drupalSettings']['path']['currentPathIsAdmin'] = TRUE;
return $form;
}
/**
* {@inheritdoc}
*/
public function validateForm(array &$form, FormStateInterface $form_state) {
$subform_state = SubformState::createForSubform($form['layout_settings'], $form, $form_state);
$this->getPluginForm($this->layout)->validateConfigurationForm($form['layout_settings'], $subform_state);
}
/**
* {@inheritdoc}
*/
public function submitForm(array &$form, FormStateInterface $form_state) {
// Call the plugin submit handler.
$subform_state = SubformState::createForSubform($form['layout_settings'], $form, $form_state);
$this->getPluginForm($this->layout)->submitConfigurationForm($form['layout_settings'], $subform_state);
// If this layout is context-aware, set the context mapping.
if ($this->layout instanceof ContextAwarePluginInterface) {
$this->layout->setContextMapping($subform_state->getValue('context_mapping', []));
}
$configuration = $this->layout->getConfiguration();
$section = $this->getCurrentSection();
$section->setLayoutSettings($configuration);
if (!$this->isUpdate) {
$this->sectionStorage->insertSection($this->delta, $section);
}
$this->layoutTempstoreRepository->set($this->sectionStorage);
$form_state->setRedirectUrl($this->sectionStorage->getLayoutBuilderUrl());
}
/**
* {@inheritdoc}
*/
protected function successfulAjaxSubmit(array $form, FormStateInterface $form_state) {
return $this->rebuildAndClose($this->sectionStorage);
}
/**
* Retrieves the plugin form for a given layout.
*
* @param \Drupal\Core\Layout\LayoutInterface $layout
* The layout plugin.
*
* @return \Drupal\Core\Plugin\PluginFormInterface
* The plugin form for the layout.
*
* @throws \Drupal\Component\Plugin\Exception\InvalidPluginDefinitionException
*/
protected function getPluginForm(LayoutInterface $layout) {
if ($layout instanceof PluginWithFormsInterface) {
return $this->pluginFormFactory->createInstance($layout, 'configure');
}
if ($layout instanceof PluginFormInterface) {
return $layout;
}
throw new \InvalidArgumentException(sprintf('The "%s" layout does not provide a configuration form', $layout->getPluginId()));
}
/**
* Retrieves the section storage property.
*
* @return \Drupal\layout_builder\SectionStorageInterface
* The section storage for the current form.
*/
public function getSectionStorage() {
return $this->sectionStorage;
}
/**
* Retrieves the layout being modified by the form.
*
* @return \Drupal\Core\Layout\LayoutInterface|\Drupal\Core\Plugin\PluginFormInterface
* The layout for the current form.
*/
public function getCurrentLayout(): LayoutInterface {
return $this->layout;
}
/**
* Retrieves the section being modified by the form.
*
* @return \Drupal\layout_builder\Section
* The section for the current form.
*/
public function getCurrentSection(): Section {
if (!isset($this->section)) {
if ($this->isUpdate) {
$this->section = $this->sectionStorage->getSection($this->delta);
}
else {
$this->section = new Section($this->pluginId);
}
}
return $this->section;
}
}

View File

@@ -0,0 +1,164 @@
<?php
namespace Drupal\layout_builder\Form;
use Drupal\Core\Entity\EntityForm;
use Drupal\Core\Entity\EntityTypeBundleInfoInterface;
use Drupal\Core\Form\FormStateInterface;
use Drupal\Core\Routing\RouteMatchInterface;
use Drupal\layout_builder\Entity\LayoutEntityDisplayInterface;
use Drupal\layout_builder\LayoutTempstoreRepositoryInterface;
use Drupal\layout_builder\SectionStorageInterface;
use Symfony\Component\DependencyInjection\ContainerInterface;
/**
* Provides a form containing the Layout Builder UI for defaults.
*
* @internal
* Form classes are internal.
*/
class DefaultsEntityForm extends EntityForm {
use PreviewToggleTrait;
use LayoutBuilderEntityFormTrait;
/**
* Layout tempstore repository.
*
* @var \Drupal\layout_builder\LayoutTempstoreRepositoryInterface
*/
protected $layoutTempstoreRepository;
/**
* The entity type bundle info service.
*
* @var \Drupal\Core\Entity\EntityTypeBundleInfoInterface
*/
protected $entityTypeBundleInfo;
/**
* The section storage.
*
* @var \Drupal\layout_builder\SectionStorageInterface
*/
protected $sectionStorage;
/**
* Constructs a new DefaultsEntityForm.
*
* @param \Drupal\layout_builder\LayoutTempstoreRepositoryInterface $layout_tempstore_repository
* The layout tempstore repository.
* @param \Drupal\Core\Entity\EntityTypeBundleInfoInterface $entity_type_bundle_info
* The entity type bundle info service.
*/
public function __construct(LayoutTempstoreRepositoryInterface $layout_tempstore_repository, EntityTypeBundleInfoInterface $entity_type_bundle_info) {
$this->layoutTempstoreRepository = $layout_tempstore_repository;
$this->entityTypeBundleInfo = $entity_type_bundle_info;
}
/**
* {@inheritdoc}
*/
public static function create(ContainerInterface $container) {
return new static(
$container->get('layout_builder.tempstore_repository'),
$container->get('entity_type.bundle.info')
);
}
/**
* {@inheritdoc}
*/
public function buildForm(array $form, FormStateInterface $form_state, ?SectionStorageInterface $section_storage = NULL) {
$form['#attributes']['class'][] = 'layout-builder-form';
$form['layout_builder'] = [
'#type' => 'layout_builder',
'#section_storage' => $section_storage,
'#process' => [[static::class, 'layoutBuilderElementGetKeys']],
];
$form['layout_builder_message'] = $this->buildMessage($section_storage->getContextValue('display'));
$this->sectionStorage = $section_storage;
return parent::buildForm($form, $form_state);
}
/**
* Form element #process callback.
*
* Save the layout builder element array parents as a property on the top form
* element so that they can be used to access the element within the whole
* render array later.
*
* @see \Drupal\layout_builder\Controller\LayoutBuilderHtmlEntityFormController
*/
public static function layoutBuilderElementGetKeys(array $element, FormStateInterface $form_state, &$form) {
$form['#layout_builder_element_keys'] = $element['#array_parents'];
return $element;
}
/**
* Renders a message to display at the top of the layout builder.
*
* @param \Drupal\layout_builder\Entity\LayoutEntityDisplayInterface $entity
* The entity view display being edited.
*
* @return array
* A renderable array containing the message.
*/
protected function buildMessage(LayoutEntityDisplayInterface $entity) {
$entity_type_id = $entity->getTargetEntityTypeId();
$entity_type = $this->entityTypeManager->getDefinition($entity_type_id);
$bundle_info = $this->entityTypeBundleInfo->getBundleInfo($entity_type_id);
$args = [
'@bundle' => $bundle_info[$entity->getTargetBundle()]['label'],
'@plural_label' => $entity_type->getPluralLabel(),
];
if ($entity_type->hasKey('bundle')) {
$message = $this->t('You are editing the layout template for all @bundle @plural_label.', $args);
}
else {
$message = $this->t('You are editing the layout template for all @plural_label.', $args);
}
return $this->buildMessageContainer($message, 'defaults');
}
/**
* {@inheritdoc}
*/
public function buildEntity(array $form, FormStateInterface $form_state) {
// \Drupal\Core\Entity\EntityForm::buildEntity() clones the entity object.
// Keep it in sync with the one used by the section storage.
$this->setEntity($this->sectionStorage->getContextValue('display'));
$entity = parent::buildEntity($form, $form_state);
$this->sectionStorage->setContextValue('display', $entity);
return $entity;
}
/**
* {@inheritdoc}
*/
public function getEntityFromRouteMatch(RouteMatchInterface $route_match, $entity_type_id) {
$route_parameters = $route_match->getParameters()->all();
return $this->entityTypeManager->getStorage('entity_view_display')->load($route_parameters['entity_type_id'] . '.' . $route_parameters['bundle'] . '.' . $route_parameters['view_mode_name']);
}
/**
* {@inheritdoc}
*/
protected function actions(array $form, FormStateInterface $form_state) {
$actions = parent::actions($form, $form_state);
return $this->buildActions($actions);
}
/**
* {@inheritdoc}
*/
public function save(array $form, FormStateInterface $form_state) {
$return = $this->sectionStorage->save();
$this->saveTasks($form_state, $this->t('The layout has been saved.'));
return $return;
}
}

View File

@@ -0,0 +1,109 @@
<?php
namespace Drupal\layout_builder\Form;
use Drupal\Core\Form\ConfirmFormBase;
use Drupal\Core\Form\FormStateInterface;
use Drupal\Core\Form\WorkspaceDynamicSafeFormInterface;
use Drupal\Core\Messenger\MessengerInterface;
use Drupal\layout_builder\LayoutTempstoreRepositoryInterface;
use Drupal\layout_builder\SectionStorageInterface;
use Symfony\Component\DependencyInjection\ContainerInterface;
/**
* Discards any pending changes to the layout.
*
* @internal
* Form classes are internal.
*/
class DiscardLayoutChangesForm extends ConfirmFormBase implements WorkspaceDynamicSafeFormInterface {
use WorkspaceSafeFormTrait;
/**
* The layout tempstore repository.
*
* @var \Drupal\layout_builder\LayoutTempstoreRepositoryInterface
*/
protected $layoutTempstoreRepository;
/**
* The messenger service.
*
* @var \Drupal\Core\Messenger\MessengerInterface
*/
protected $messenger;
/**
* The section storage.
*
* @var \Drupal\layout_builder\SectionStorageInterface
*/
protected $sectionStorage;
/**
* Constructs a new DiscardLayoutChangesForm.
*
* @param \Drupal\layout_builder\LayoutTempstoreRepositoryInterface $layout_tempstore_repository
* The layout tempstore repository.
* @param \Drupal\Core\Messenger\MessengerInterface $messenger
* The messenger service.
*/
public function __construct(LayoutTempstoreRepositoryInterface $layout_tempstore_repository, MessengerInterface $messenger) {
$this->layoutTempstoreRepository = $layout_tempstore_repository;
$this->messenger = $messenger;
}
/**
* {@inheritdoc}
*/
public static function create(ContainerInterface $container) {
return new static(
$container->get('layout_builder.tempstore_repository'),
$container->get('messenger')
);
}
/**
* {@inheritdoc}
*/
public function getFormId() {
return 'layout_builder_discard_changes';
}
/**
* {@inheritdoc}
*/
public function getQuestion() {
return $this->t('Are you sure you want to discard your layout changes?');
}
/**
* {@inheritdoc}
*/
public function getCancelUrl() {
return $this->sectionStorage->getLayoutBuilderUrl();
}
/**
* {@inheritdoc}
*/
public function buildForm(array $form, FormStateInterface $form_state, ?SectionStorageInterface $section_storage = NULL) {
$this->sectionStorage = $section_storage;
// Mark this as an administrative page for JavaScript ("Back to site" link).
$form['#attached']['drupalSettings']['path']['currentPathIsAdmin'] = TRUE;
return parent::buildForm($form, $form_state);
}
/**
* {@inheritdoc}
*/
public function submitForm(array &$form, FormStateInterface $form_state) {
$this->layoutTempstoreRepository->delete($this->sectionStorage);
$this->messenger->addMessage($this->t('The changes to the layout have been discarded.'));
$form_state->setRedirectUrl($this->sectionStorage->getRedirectUrl());
}
}

View File

@@ -0,0 +1,114 @@
<?php
namespace Drupal\layout_builder\Form;
use Drupal\Core\Form\ConfirmFormBase;
use Drupal\Core\Form\FormStateInterface;
use Drupal\Core\Form\WorkspaceDynamicSafeFormInterface;
use Drupal\Core\Messenger\MessengerInterface;
use Drupal\layout_builder\DefaultsSectionStorageInterface;
use Drupal\layout_builder\LayoutTempstoreRepositoryInterface;
use Drupal\layout_builder\SectionStorageInterface;
use Symfony\Component\DependencyInjection\ContainerInterface;
/**
* Disables Layout Builder for a given default.
*
* @internal
* Form classes are internal.
*/
class LayoutBuilderDisableForm extends ConfirmFormBase implements WorkspaceDynamicSafeFormInterface {
use WorkspaceSafeFormTrait;
/**
* The layout tempstore repository.
*
* @var \Drupal\layout_builder\LayoutTempstoreRepositoryInterface
*/
protected $layoutTempstoreRepository;
/**
* The section storage.
*
* @var \Drupal\layout_builder\DefaultsSectionStorageInterface
*/
protected $sectionStorage;
/**
* Constructs a new RevertOverridesForm.
*
* @param \Drupal\layout_builder\LayoutTempstoreRepositoryInterface $layout_tempstore_repository
* The layout tempstore repository.
* @param \Drupal\Core\Messenger\MessengerInterface $messenger
* The messenger service.
*/
public function __construct(LayoutTempstoreRepositoryInterface $layout_tempstore_repository, MessengerInterface $messenger) {
$this->layoutTempstoreRepository = $layout_tempstore_repository;
$this->setMessenger($messenger);
}
/**
* {@inheritdoc}
*/
public static function create(ContainerInterface $container) {
return new static(
$container->get('layout_builder.tempstore_repository'),
$container->get('messenger')
);
}
/**
* {@inheritdoc}
*/
public function getFormId() {
return 'layout_builder_disable_form';
}
/**
* {@inheritdoc}
*/
public function getQuestion() {
return $this->t('Are you sure you want to disable Layout Builder?');
}
/**
* {@inheritdoc}
*/
public function getDescription() {
return $this->t('All customizations will be removed. This action cannot be undone.');
}
/**
* {@inheritdoc}
*/
public function getCancelUrl() {
return $this->sectionStorage->getRedirectUrl();
}
/**
* {@inheritdoc}
*/
public function buildForm(array $form, FormStateInterface $form_state, ?SectionStorageInterface $section_storage = NULL) {
if (!$section_storage instanceof DefaultsSectionStorageInterface) {
throw new \InvalidArgumentException(sprintf('The section storage with type "%s" and ID "%s" does not provide defaults', $section_storage->getStorageType(), $section_storage->getStorageId()));
}
$this->sectionStorage = $section_storage;
// Mark this as an administrative page for JavaScript ("Back to site" link).
$form['#attached']['drupalSettings']['path']['currentPathIsAdmin'] = TRUE;
return parent::buildForm($form, $form_state);
}
/**
* {@inheritdoc}
*/
public function submitForm(array &$form, FormStateInterface $form_state) {
$this->sectionStorage->disableLayoutBuilder()->save();
$this->layoutTempstoreRepository->delete($this->sectionStorage);
$this->messenger()->addMessage($this->t('Layout Builder has been disabled.'));
$form_state->setRedirectUrl($this->getCancelUrl());
}
}

View File

@@ -0,0 +1,109 @@
<?php
namespace Drupal\layout_builder\Form;
use Drupal\Core\Form\FormStateInterface;
use Drupal\Core\StringTranslation\TranslatableMarkup;
use Drupal\layout_builder\SectionStorageInterface;
/**
* Provides a trait for common methods used in Layout Builder entity forms.
*/
trait LayoutBuilderEntityFormTrait {
use PreviewToggleTrait;
/**
* {@inheritdoc}
*/
public function getBaseFormId(): string {
return $this->getEntity()->getEntityTypeId() . '_layout_builder_form';
}
/**
* Build the message container.
*
* @param \Drupal\Core\StringTranslation\TranslatableMarkup $message
* The message to display.
* @param string $type
* The form type this is being attached to.
*
* @return array
* The render array.
*/
protected function buildMessageContainer(TranslatableMarkup $message, string $type): array {
return [
'#type' => 'container',
'#attributes' => [
'class' => [
'layout-builder__message',
sprintf('layout-builder__message--%s', $type),
],
],
'message' => [
'#theme' => 'status_messages',
'#message_list' => ['status' => [$message]],
'#status_headings' => [
'status' => $this->t('Status message'),
],
],
'#weight' => -900,
];
}
/**
* Form submission handler.
*/
public function redirectOnSubmit(array $form, FormStateInterface $form_state) {
$form_state->setRedirectUrl($this->sectionStorage->getLayoutBuilderUrl($form_state->getTriggeringElement()['#redirect']));
}
/**
* Retrieves the section storage object.
*
* @return \Drupal\layout_builder\SectionStorageInterface
* The section storage for the current form.
*/
public function getSectionStorage(): SectionStorageInterface {
return $this->sectionStorage;
}
/**
* Builds the actions for the form.
*
* @param array $actions
* The actions array to modify.
*
* @return array
* The modified actions array.
*/
protected function buildActions(array $actions): array {
$actions['#attributes']['role'] = 'region';
$actions['#attributes']['aria-label'] = $this->t('Layout Builder tools');
$actions['submit']['#value'] = $this->t('Save layout');
$actions['#weight'] = -1000;
$actions['discard_changes'] = [
'#type' => 'submit',
'#value' => $this->t('Discard changes'),
'#submit' => ['::redirectOnSubmit'],
'#redirect' => 'discard_changes',
];
$actions['preview_toggle'] = $this->buildContentPreviewToggle();
return $actions;
}
/**
* Performs tasks that are needed during the save process.
*
* @param \Drupal\Core\Form\FormStateInterface $formState
* The form state.
* @param \Drupal\Core\StringTranslation\TranslatableMarkup $message
* The message to display.
*/
protected function saveTasks(FormStateInterface $formState, TranslatableMarkup $message): void {
$this->layoutTempstoreRepository->delete($this->getSectionStorage());
$this->messenger()->addStatus($message);
$formState->setRedirectUrl($this->getSectionStorage()->getRedirectUrl());
}
}

View File

@@ -0,0 +1,241 @@
<?php
namespace Drupal\layout_builder\Form;
use Drupal\Core\Entity\EntityInterface;
use Drupal\Core\Field\FieldDefinitionInterface;
use Drupal\Core\Form\FormStateInterface;
use Drupal\field_ui\Form\EntityViewDisplayEditForm;
use Drupal\layout_builder\Entity\LayoutEntityDisplayInterface;
use Drupal\layout_builder\Plugin\SectionStorage\OverridesSectionStorage;
use Drupal\layout_builder\SectionStorageInterface;
/**
* Edit form for the LayoutBuilderEntityViewDisplay entity type.
*
* @internal
* Form classes are internal.
*/
class LayoutBuilderEntityViewDisplayForm extends EntityViewDisplayEditForm {
/**
* The entity being used by this form.
*
* @var \Drupal\layout_builder\Entity\LayoutEntityDisplayInterface
*/
protected $entity;
/**
* The storage section.
*
* @var \Drupal\layout_builder\DefaultsSectionStorageInterface
*/
protected $sectionStorage;
/**
* {@inheritdoc}
*/
public function buildForm(array $form, FormStateInterface $form_state, ?SectionStorageInterface $section_storage = NULL) {
$this->sectionStorage = $section_storage;
return parent::buildForm($form, $form_state);
}
/**
* {@inheritdoc}
*/
public function form(array $form, FormStateInterface $form_state) {
$form = parent::form($form, $form_state);
// Remove the Layout Builder field from the list.
$form['#fields'] = array_diff($form['#fields'], [OverridesSectionStorage::FIELD_NAME]);
unset($form['fields'][OverridesSectionStorage::FIELD_NAME]);
$is_enabled = $this->entity->isLayoutBuilderEnabled();
if ($is_enabled) {
// Hide the table of fields.
$form['fields']['#access'] = FALSE;
$form['#fields'] = [];
$form['#extra'] = [];
}
$form['manage_layout'] = [
'#type' => 'link',
'#title' => $this->t('Manage layout'),
'#weight' => -10,
'#attributes' => ['class' => ['button']],
'#url' => $this->sectionStorage->getLayoutBuilderUrl(),
'#access' => $is_enabled,
];
$form['layout'] = [
'#type' => 'details',
'#open' => TRUE,
'#title' => $this->t('Layout options'),
'#tree' => TRUE,
];
$form['layout']['enabled'] = [
'#type' => 'checkbox',
'#title' => $this->t('Use Layout Builder'),
'#default_value' => $is_enabled,
];
$form['#entity_builders']['layout_builder'] = '::entityFormEntityBuild';
// @todo Expand to work for all view modes in
// https://www.drupal.org/node/2907413.
if ($this->isCanonicalMode($this->entity->getMode())) {
$entity_type = $this->entityTypeManager->getDefinition($this->entity->getTargetEntityTypeId());
$form['layout']['allow_custom'] = [
'#type' => 'checkbox',
'#title' => $this->t('Allow each @entity to have its layout customized.', [
'@entity' => $entity_type->getSingularLabel(),
]),
'#default_value' => $this->entity->isOverridable(),
'#states' => [
'disabled' => [
':input[name="layout[enabled]"]' => ['checked' => FALSE],
],
'invisible' => [
':input[name="layout[enabled]"]' => ['checked' => FALSE],
],
],
];
if (!$is_enabled) {
$form['layout']['allow_custom']['#attributes']['disabled'] = 'disabled';
}
// Prevent turning off overrides while any exist.
if ($this->hasOverrides($this->entity)) {
$form['layout']['enabled']['#disabled'] = TRUE;
$form['layout']['enabled']['#description'] = $this->t('You must revert all customized layouts of this display before you can disable this option.');
$form['layout']['allow_custom']['#disabled'] = TRUE;
$form['layout']['allow_custom']['#description'] = $this->t('You must revert all customized layouts of this display before you can disable this option.');
unset($form['layout']['allow_custom']['#states']);
unset($form['#entity_builders']['layout_builder']);
}
}
// For non-canonical modes, the existing value should be preserved.
else {
$form['layout']['allow_custom'] = [
'#type' => 'value',
'#value' => $this->entity->isOverridable(),
];
}
return $form;
}
/**
* Determines if the mode is used by the canonical route.
*
* @param string $mode
* The view mode.
*
* @return bool
* TRUE if the mode is valid, FALSE otherwise.
*/
protected function isCanonicalMode($mode) {
// @todo This is a convention core uses but is not a given, nor is it easily
// introspectable. Address in https://www.drupal.org/node/2907413.
$canonical_mode = 'full';
if ($mode === $canonical_mode) {
return TRUE;
}
// The default mode is valid if the canonical mode is not enabled.
if ($mode === 'default') {
/** @var \Drupal\Core\Config\Entity\ConfigEntityStorageInterface $storage */
$storage = $this->entityTypeManager->getStorage($this->entity->getEntityTypeId());
$query = $storage->getQuery()
->condition('targetEntityType', $this->entity->getTargetEntityTypeId())
->condition('bundle', $this->entity->getTargetBundle())
->condition('status', TRUE)
->condition('mode', $canonical_mode);
return !$query->count()->execute();
}
return FALSE;
}
/**
* Determines if the defaults have any overrides.
*
* @param \Drupal\layout_builder\Entity\LayoutEntityDisplayInterface $display
* The entity display.
*
* @return bool
* TRUE if there are any overrides of this default, FALSE otherwise.
*/
protected function hasOverrides(LayoutEntityDisplayInterface $display) {
if (!$display->isOverridable()) {
return FALSE;
}
$entity_type = $this->entityTypeManager->getDefinition($display->getTargetEntityTypeId());
$query = $this->entityTypeManager->getStorage($display->getTargetEntityTypeId())->getQuery()
->accessCheck(FALSE)
->exists(OverridesSectionStorage::FIELD_NAME);
if ($bundle_key = $entity_type->getKey('bundle')) {
$query->condition($bundle_key, $display->getTargetBundle());
}
return (bool) $query->count()->execute();
}
/**
* {@inheritdoc}
*/
protected function copyFormValuesToEntity(EntityInterface $entity, array $form, FormStateInterface $form_state) {
// Do not process field values if Layout Builder is or will be enabled.
$set_enabled = (bool) $form_state->getValue(['layout', 'enabled'], FALSE);
/** @var \Drupal\layout_builder\Entity\LayoutEntityDisplayInterface $entity */
$already_enabled = $entity->isLayoutBuilderEnabled();
if ($already_enabled || $set_enabled) {
$form['#fields'] = [];
$form['#extra'] = [];
}
parent::copyFormValuesToEntity($entity, $form, $form_state);
}
/**
* Entity builder for layout options on the entity view display form.
*/
public function entityFormEntityBuild($entity_type_id, LayoutEntityDisplayInterface $display, &$form, FormStateInterface &$form_state) {
$set_enabled = (bool) $form_state->getValue(['layout', 'enabled'], FALSE);
$already_enabled = $display->isLayoutBuilderEnabled();
if ($set_enabled) {
$overridable = (bool) $form_state->getValue(['layout', 'allow_custom'], FALSE);
$display->setOverridable($overridable);
if (!$already_enabled) {
$display->enableLayoutBuilder();
}
}
elseif ($already_enabled) {
$form_state->setRedirectUrl($this->sectionStorage->getLayoutBuilderUrl('disable'));
}
}
/**
* {@inheritdoc}
*/
protected function buildFieldRow(FieldDefinitionInterface $field_definition, array $form, FormStateInterface $form_state) {
if ($this->entity->isLayoutBuilderEnabled()) {
return [];
}
return parent::buildFieldRow($field_definition, $form, $form_state);
}
/**
* {@inheritdoc}
*/
protected function buildExtraFieldRow($field_id, $extra_field) {
if ($this->entity->isLayoutBuilderEnabled()) {
return [];
}
return parent::buildExtraFieldRow($field_id, $extra_field);
}
}

View File

@@ -0,0 +1,128 @@
<?php
namespace Drupal\layout_builder\Form;
use Drupal\Core\Ajax\AjaxFormHelperTrait;
use Drupal\Core\Form\ConfirmFormBase;
use Drupal\Core\Form\FormStateInterface;
use Drupal\Core\Form\WorkspaceDynamicSafeFormInterface;
use Drupal\layout_builder\Controller\LayoutRebuildTrait;
use Drupal\layout_builder\LayoutBuilderHighlightTrait;
use Drupal\layout_builder\LayoutTempstoreRepositoryInterface;
use Drupal\layout_builder\SectionStorageInterface;
use Symfony\Component\DependencyInjection\ContainerInterface;
/**
* Provides a base class for confirmation forms that rebuild the Layout Builder.
*
* @internal
* Form classes are internal.
*/
abstract class LayoutRebuildConfirmFormBase extends ConfirmFormBase implements WorkspaceDynamicSafeFormInterface {
use AjaxFormHelperTrait;
use LayoutBuilderHighlightTrait;
use LayoutRebuildTrait;
use WorkspaceSafeFormTrait;
/**
* The layout tempstore repository.
*
* @var \Drupal\layout_builder\LayoutTempstoreRepositoryInterface
*/
protected $layoutTempstoreRepository;
/**
* The section storage.
*
* @var \Drupal\layout_builder\SectionStorageInterface
*/
protected $sectionStorage;
/**
* The field delta.
*
* @var int
*/
protected $delta;
/**
* Constructs a new RemoveSectionForm.
*
* @param \Drupal\layout_builder\LayoutTempstoreRepositoryInterface $layout_tempstore_repository
* The layout tempstore repository.
*/
public function __construct(LayoutTempstoreRepositoryInterface $layout_tempstore_repository) {
$this->layoutTempstoreRepository = $layout_tempstore_repository;
}
/**
* {@inheritdoc}
*/
public static function create(ContainerInterface $container) {
return new static(
$container->get('layout_builder.tempstore_repository')
);
}
/**
* {@inheritdoc}
*/
public function getCancelUrl() {
return $this->sectionStorage->getLayoutBuilderUrl();
}
/**
* {@inheritdoc}
*/
public function buildForm(array $form, FormStateInterface $form_state, ?SectionStorageInterface $section_storage = NULL, $delta = NULL) {
$this->sectionStorage = $section_storage;
$this->delta = $delta;
$form = parent::buildForm($form, $form_state);
if ($this->isAjax()) {
$form['actions']['submit']['#ajax']['callback'] = '::ajaxSubmit';
$form['actions']['cancel']['#attributes']['class'][] = 'dialog-cancel';
$target_highlight_id = !empty($this->uuid) ? $this->blockUpdateHighlightId($this->uuid) : $this->sectionUpdateHighlightId($delta);
$form['#attributes']['data-layout-builder-target-highlight-id'] = $target_highlight_id;
// The AJAX system automatically moves focus to the first tabbable
// element after closing a dialog, sometimes scrolling to a page top.
// Disable refocus on the button.
$form['actions']['submit']['#ajax']['disable-refocus'] = TRUE;
}
// Mark this as an administrative page for JavaScript ("Back to site" link).
$form['#attached']['drupalSettings']['path']['currentPathIsAdmin'] = TRUE;
return $form;
}
/**
* {@inheritdoc}
*/
public function submitForm(array &$form, FormStateInterface $form_state) {
$this->handleSectionStorage($this->sectionStorage, $form_state);
$this->layoutTempstoreRepository->set($this->sectionStorage);
$form_state->setRedirectUrl($this->getCancelUrl());
}
/**
* {@inheritdoc}
*/
protected function successfulAjaxSubmit(array $form, FormStateInterface $form_state) {
return $this->rebuildAndClose($this->sectionStorage);
}
/**
* Performs any actions on the section storage before saving.
*
* @param \Drupal\layout_builder\SectionStorageInterface $section_storage
* The section storage.
* @param \Drupal\Core\Form\FormStateInterface $form_state
* The current state of the form.
*/
abstract protected function handleSectionStorage(SectionStorageInterface $section_storage, FormStateInterface $form_state);
}

View File

@@ -0,0 +1,345 @@
<?php
namespace Drupal\layout_builder\Form;
use Drupal\Core\Ajax\AjaxFormHelperTrait;
use Drupal\Core\Form\FormBase;
use Drupal\Core\Form\FormStateInterface;
use Drupal\Core\Form\WorkspaceDynamicSafeFormInterface;
use Drupal\layout_builder\Context\LayoutBuilderContextTrait;
use Drupal\layout_builder\Controller\LayoutRebuildTrait;
use Drupal\layout_builder\LayoutBuilderHighlightTrait;
use Drupal\layout_builder\LayoutTempstoreRepositoryInterface;
use Drupal\layout_builder\SectionStorageInterface;
use Symfony\Component\DependencyInjection\ContainerInterface;
/**
* Provides a form for moving a block.
*
* @internal
* Form classes are internal.
*/
class MoveBlockForm extends FormBase implements WorkspaceDynamicSafeFormInterface {
use AjaxFormHelperTrait;
use LayoutBuilderContextTrait;
use LayoutBuilderHighlightTrait;
use LayoutRebuildTrait;
use WorkspaceSafeFormTrait;
/**
* The section storage.
*
* @var \Drupal\layout_builder\SectionStorageInterface
*/
protected $sectionStorage;
/**
* The section delta.
*
* @var int
*/
protected $delta;
/**
* The region name.
*
* @var string
*/
protected $region;
/**
* The component uuid.
*
* @var string
*/
protected $uuid;
/**
* The Layout Tempstore.
*
* @var \Drupal\layout_builder\LayoutTempstoreRepositoryInterface
*/
protected $layoutTempstore;
/**
* Constructs a new MoveBlockForm.
*
* @param \Drupal\layout_builder\LayoutTempstoreRepositoryInterface $layout_tempstore_repository
* The layout tempstore.
*/
public function __construct(LayoutTempstoreRepositoryInterface $layout_tempstore_repository) {
$this->layoutTempstore = $layout_tempstore_repository;
}
/**
* {@inheritdoc}
*/
public static function create(ContainerInterface $container) {
return new static(
$container->get('layout_builder.tempstore_repository')
);
}
/**
* {@inheritdoc}
*/
public function getFormId() {
return 'layout_builder_block_move';
}
/**
* Builds the move block form.
*
* @param array $form
* An associative array containing the structure of the form.
* @param \Drupal\Core\Form\FormStateInterface $form_state
* The current state of the form.
* @param \Drupal\layout_builder\SectionStorageInterface $section_storage
* The section storage being configured.
* @param int $delta
* The original delta of the section.
* @param string $region
* The original region of the block.
* @param string $uuid
* The UUID of the block being updated.
*
* @return array
* The form array.
*/
public function buildForm(array $form, FormStateInterface $form_state, ?SectionStorageInterface $section_storage = NULL, $delta = NULL, $region = NULL, $uuid = NULL) {
$parameters = array_slice(func_get_args(), 2);
foreach ($parameters as $parameter) {
if (is_null($parameter)) {
throw new \InvalidArgumentException('MoveBlockForm requires all parameters.');
}
}
$this->sectionStorage = $section_storage;
$this->delta = $delta;
$this->uuid = $uuid;
$this->region = $region;
$form['#attributes']['data-layout-builder-target-highlight-id'] = $this->blockUpdateHighlightId($uuid);
$sections = $section_storage->getSections();
$contexts = $this->getPopulatedContexts($section_storage);
$region_options = [];
foreach ($sections as $section_delta => $section) {
$layout = $section->getLayout($contexts);
$layout_definition = $layout->getPluginDefinition();
if (!($section_label = $section->getLayoutSettings()['label'])) {
$section_label = $this->t('Section: @delta', ['@delta' => $section_delta + 1])->render();
}
foreach ($layout_definition->getRegions() as $region_name => $region_info) {
// Group regions by section.
$region_options[$section_label]["$section_delta:$region_name"] = $this->t(
'@section, Region: @region',
['@section' => $section_label, '@region' => $region_info['label']]
);
}
}
// $this->region and $this->delta are where the block is currently placed.
// $selected_region and $selected_delta are the values from this form
// specifying where the block should be moved to.
$selected_region = $this->getSelectedRegion($form_state);
$selected_delta = $this->getSelectedDelta($form_state);
$form['region'] = [
'#type' => 'select',
'#options' => $region_options,
'#title' => $this->t('Region'),
'#default_value' => "$selected_delta:$selected_region",
'#ajax' => [
'wrapper' => 'layout-builder-components-table',
'callback' => '::getComponentsWrapper',
],
];
$current_section = $sections[$selected_delta];
$aria_label = $this->t('Blocks in Section: @section, Region: @region', ['@section' => $selected_delta + 1, '@region' => $selected_region]);
$form['components_wrapper']['components'] = [
'#type' => 'table',
'#header' => [
$this->t('Block label'),
$this->t('Weight'),
],
'#tabledrag' => [
[
'action' => 'order',
'relationship' => 'sibling',
'group' => 'table-sort-weight',
],
],
// Create a wrapping element so that the Ajax update also replaces the
// 'Show block weights' link.
'#theme_wrappers' => [
'container' => [
'#attributes' => [
'id' => 'layout-builder-components-table',
'class' => ['layout-builder-components-table'],
'aria-label' => $aria_label,
],
],
],
];
/** @var \Drupal\layout_builder\SectionComponent[] $components */
$components = $current_section->getComponentsByRegion($selected_region);
// If the component is not in this region, add it to the listed components.
if (!isset($components[$uuid])) {
$components[$uuid] = $sections[$delta]->getComponent($uuid);
}
$state_weight_delta = round(count($components) / 2);
foreach ($components as $component_uuid => $component) {
/** @var \Drupal\Core\Block\BlockPluginInterface $plugin */
$plugin = $component->getPlugin();
$is_current_block = $component_uuid === $uuid;
$row_classes = [
'draggable',
'layout-builder-components-table__row',
];
$label['#wrapper_attributes']['class'] = ['layout-builder-components-table__block-label'];
if ($is_current_block) {
// Highlight the current block.
$label['#markup'] = $this->t('@label (current)', ['@label' => $plugin->label()]);
$label['#wrapper_attributes']['class'][] = 'layout-builder-components-table__block-label--current';
$row_classes[] = 'layout-builder-components-table__row--current';
}
else {
$label['#markup'] = $plugin->label();
}
$form['components_wrapper']['components'][$component_uuid] = [
'#attributes' => ['class' => $row_classes],
'label' => $label,
'weight' => [
'#type' => 'weight',
'#default_value' => $component->getWeight(),
'#title' => $this->t('Weight for @block block', ['@block' => $plugin->label()]),
'#title_display' => 'invisible',
'#attributes' => [
'class' => ['table-sort-weight'],
],
'#delta' => $state_weight_delta,
],
];
}
$form['actions']['submit'] = [
'#type' => 'submit',
'#value' => $this->t('Move'),
'#button_type' => 'primary',
];
$form['#attributes']['data-add-layout-builder-wrapper'] = 'layout-builder--move-blocks-active';
if ($this->isAjax()) {
$form['actions']['submit']['#ajax']['callback'] = '::ajaxSubmit';
}
return $form;
}
/**
* {@inheritdoc}
*/
public function submitForm(array &$form, FormStateInterface $form_state) {
$region = $this->getSelectedRegion($form_state);
$delta = $this->getSelectedDelta($form_state);
$original_section = $this->sectionStorage->getSection($this->delta);
$component = $original_section->getComponent($this->uuid);
$section = $this->sectionStorage->getSection($delta);
if ($delta !== $this->delta) {
// Remove component from old section and add it to the new section.
$original_section->removeComponent($this->uuid);
$section->insertComponent(0, $component);
}
$component->setRegion($region);
foreach ($form_state->getValue('components') as $uuid => $component_info) {
$section->getComponent($uuid)->setWeight($component_info['weight']);
}
$this->layoutTempstore->set($this->sectionStorage);
}
/**
* Ajax callback for the region select element.
*
* @param array $form
* The form.
* @param \Drupal\Core\Form\FormStateInterface $form_state
* The form state.
*
* @return array
* The components wrapper render array.
*/
public function getComponentsWrapper(array $form, FormStateInterface $form_state) {
return $form['components_wrapper'];
}
/**
* {@inheritdoc}
*/
protected function successfulAjaxSubmit(array $form, FormStateInterface $form_state) {
return $this->rebuildAndClose($this->sectionStorage);
}
/**
* Gets the selected region.
*
* @param \Drupal\Core\Form\FormStateInterface $form_state
* The form state.
*
* @return string
* The current region name.
*/
protected function getSelectedRegion(FormStateInterface $form_state) {
if ($form_state->hasValue('region')) {
return explode(':', $form_state->getValue('region'), 2)[1];
}
return $this->region;
}
/**
* Gets the selected delta.
*
* @param \Drupal\Core\Form\FormStateInterface $form_state
* The form state.
*
* @return int
* The section delta.
*/
protected function getSelectedDelta(FormStateInterface $form_state) {
if ($form_state->hasValue('region')) {
return (int) explode(':', $form_state->getValue('region'))[0];
}
return (int) $this->delta;
}
/**
* Provides a title callback.
*
* @param \Drupal\layout_builder\SectionStorageInterface $section_storage
* The section storage.
* @param int $delta
* The original delta of the section.
* @param string $uuid
* The UUID of the block being updated.
*
* @return string
* The title for the move block form.
*/
public function title(SectionStorageInterface $section_storage, $delta, $uuid) {
$block_label = $section_storage
->getSection($delta)
->getComponent($uuid)
->getPlugin()
->label();
return $this->t('Move the @block_label block', ['@block_label' => $block_label]);
}
}

View File

@@ -0,0 +1,181 @@
<?php
namespace Drupal\layout_builder\Form;
use Drupal\Component\Datetime\TimeInterface;
use Drupal\Core\Entity\ContentEntityForm;
use Drupal\Core\Entity\Entity\EntityFormDisplay;
use Drupal\Core\Entity\EntityInterface;
use Drupal\Core\Entity\EntityRepositoryInterface;
use Drupal\Core\Entity\EntityTypeBundleInfoInterface;
use Drupal\Core\Form\FormStateInterface;
use Drupal\Core\Form\WorkspaceDynamicSafeFormInterface;
use Drupal\layout_builder\LayoutTempstoreRepositoryInterface;
use Drupal\layout_builder\OverridesSectionStorageInterface;
use Drupal\layout_builder\Plugin\SectionStorage\OverridesSectionStorage;
use Drupal\layout_builder\SectionStorageInterface;
use Symfony\Component\DependencyInjection\ContainerInterface;
/**
* Provides a form containing the Layout Builder UI for overrides.
*
* @internal
* Form classes are internal.
*/
class OverridesEntityForm extends ContentEntityForm implements WorkspaceDynamicSafeFormInterface {
use PreviewToggleTrait;
use LayoutBuilderEntityFormTrait;
use WorkspaceSafeFormTrait;
/**
* Layout tempstore repository.
*
* @var \Drupal\layout_builder\LayoutTempstoreRepositoryInterface
*/
protected $layoutTempstoreRepository;
/**
* The section storage.
*
* @var \Drupal\layout_builder\SectionStorageInterface
*/
protected $sectionStorage;
/**
* Constructs a new OverridesEntityForm.
*
* @param \Drupal\Core\Entity\EntityRepositoryInterface $entity_repository
* The entity repository service.
* @param \Drupal\Core\Entity\EntityTypeBundleInfoInterface $entity_type_bundle_info
* The entity type bundle service.
* @param \Drupal\Component\Datetime\TimeInterface $time
* The time service.
* @param \Drupal\layout_builder\LayoutTempstoreRepositoryInterface $layout_tempstore_repository
* The layout tempstore repository.
*/
public function __construct(EntityRepositoryInterface $entity_repository, EntityTypeBundleInfoInterface $entity_type_bundle_info, TimeInterface $time, LayoutTempstoreRepositoryInterface $layout_tempstore_repository) {
parent::__construct($entity_repository, $entity_type_bundle_info, $time);
$this->layoutTempstoreRepository = $layout_tempstore_repository;
}
/**
* {@inheritdoc}
*/
public static function create(ContainerInterface $container) {
return new static(
$container->get('entity.repository'),
$container->get('entity_type.bundle.info'),
$container->get('datetime.time'),
$container->get('layout_builder.tempstore_repository')
);
}
/**
* {@inheritdoc}
*/
protected function init(FormStateInterface $form_state) {
parent::init($form_state);
$form_display = EntityFormDisplay::collectRenderDisplay($this->entity, $this->getOperation(), FALSE);
$form_display->setComponent(OverridesSectionStorage::FIELD_NAME, [
'type' => 'layout_builder_widget',
'weight' => -10,
'settings' => [],
]);
$this->setFormDisplay($form_display, $form_state);
}
/**
* {@inheritdoc}
*/
public function buildForm(array $form, FormStateInterface $form_state, ?SectionStorageInterface $section_storage = NULL) {
$this->sectionStorage = $section_storage;
$form = parent::buildForm($form, $form_state);
$form['#attributes']['class'][] = 'layout-builder-form';
// @todo \Drupal\layout_builder\Field\LayoutSectionItemList::defaultAccess()
// restricts all access to the field, explicitly allow access here until
// https://www.drupal.org/node/2942975 is resolved.
$form[OverridesSectionStorage::FIELD_NAME]['#access'] = TRUE;
$form['layout_builder_message'] = $this->buildMessage($section_storage->getContextValue('entity'), $section_storage);
return $form;
}
/**
* Renders a message to display at the top of the layout builder.
*
* @param \Drupal\Core\Entity\EntityInterface $entity
* The entity whose layout is being edited.
* @param \Drupal\layout_builder\OverridesSectionStorageInterface $section_storage
* The current section storage.
*
* @return array
* A renderable array containing the message.
*/
protected function buildMessage(EntityInterface $entity, OverridesSectionStorageInterface $section_storage) {
$entity_type = $entity->getEntityType();
$bundle_info = $this->entityTypeBundleInfo->getBundleInfo($entity->getEntityTypeId());
$variables = [
'@bundle' => $bundle_info[$entity->bundle()]['label'],
'@singular_label' => $entity_type->getSingularLabel(),
'@plural_label' => $entity_type->getPluralLabel(),
];
$defaults_link = $section_storage
->getDefaultSectionStorage()
->getLayoutBuilderUrl();
if ($defaults_link->access($this->currentUser())) {
$variables[':link'] = $defaults_link->toString();
if ($entity_type->hasKey('bundle')) {
$message = $this->t('You are editing the layout for this @bundle @singular_label. <a href=":link">Edit the template for all @bundle @plural_label instead.</a>', $variables);
}
else {
$message = $this->t('You are editing the layout for this @singular_label. <a href=":link">Edit the template for all @plural_label instead.</a>', $variables);
}
}
else {
if ($entity_type->hasKey('bundle')) {
$message = $this->t('You are editing the layout for this @bundle @singular_label.', $variables);
}
else {
$message = $this->t('You are editing the layout for this @singular_label.', $variables);
}
}
return $this->buildMessageContainer($message, 'overrides');
}
/**
* {@inheritdoc}
*/
public function save(array $form, FormStateInterface $form_state) {
$return = parent::save($form, $form_state);
$this->saveTasks($form_state, $this->t('The layout override has been saved.'));
return $return;
}
/**
* {@inheritdoc}
*/
protected function actions(array $form, FormStateInterface $form_state) {
$actions = parent::actions($form, $form_state);
$actions = $this->buildActions($actions);
$actions['delete']['#access'] = FALSE;
$actions['discard_changes']['#limit_validation_errors'] = [];
// @todo This button should be conditionally displayed, see
// https://www.drupal.org/node/2917777.
$actions['revert'] = [
'#type' => 'submit',
'#value' => $this->t('Revert to defaults'),
'#submit' => ['::redirectOnSubmit'],
'#redirect' => 'revert',
];
return $actions;
}
}

View File

@@ -0,0 +1,43 @@
<?php
namespace Drupal\layout_builder\Form;
/**
* Provides a trait that provides a toggle for the content preview.
*/
trait PreviewToggleTrait {
/**
* Builds the content preview toggle input.
*
* @return array
* The render array for the content preview toggle.
*/
protected function buildContentPreviewToggle() {
return [
'#type' => 'container',
'#attributes' => [
'class' => ['js-show'],
],
'toggle_content_preview' => [
'#title' => $this->t('Show content preview'),
'#type' => 'checkbox',
'#value' => TRUE,
'#attributes' => [
// Set attribute used by local storage to get content preview status.
'data-content-preview-id' => "Drupal.layout_builder.content_preview.{$this->currentUser()->id()}",
],
'#id' => 'layout-builder-content-preview',
],
];
}
/**
* Gets the current user.
*
* @return \Drupal\Core\Session\AccountInterface
* The current user.
*/
abstract protected function currentUser();
}

View File

@@ -0,0 +1,73 @@
<?php
namespace Drupal\layout_builder\Form;
use Drupal\Core\Form\FormStateInterface;
use Drupal\layout_builder\SectionStorageInterface;
/**
* Provides a form to confirm the removal of a block.
*
* @internal
* Form classes are internal.
*/
class RemoveBlockForm extends LayoutRebuildConfirmFormBase {
/**
* The current region.
*
* @var string
*/
protected $region;
/**
* The UUID of the block being removed.
*
* @var string
*/
protected $uuid;
/**
* {@inheritdoc}
*/
public function getQuestion() {
$label = $this->sectionStorage
->getSection($this->delta)
->getComponent($this->uuid)
->getPlugin()
->label();
return $this->t('Are you sure you want to remove the %label block?', ['%label' => $label]);
}
/**
* {@inheritdoc}
*/
public function getConfirmText() {
return $this->t('Remove');
}
/**
* {@inheritdoc}
*/
public function getFormId() {
return 'layout_builder_remove_block';
}
/**
* {@inheritdoc}
*/
public function buildForm(array $form, FormStateInterface $form_state, ?SectionStorageInterface $section_storage = NULL, $delta = NULL, $region = NULL, $uuid = NULL) {
$this->region = $region;
$this->uuid = $uuid;
return parent::buildForm($form, $form_state, $section_storage, $delta);
}
/**
* {@inheritdoc}
*/
protected function handleSectionStorage(SectionStorageInterface $section_storage, FormStateInterface $form_state) {
$section_storage->getSection($this->delta)->removeComponent($this->uuid);
}
}

View File

@@ -0,0 +1,50 @@
<?php
namespace Drupal\layout_builder\Form;
use Drupal\Core\Form\FormStateInterface;
use Drupal\layout_builder\SectionStorageInterface;
/**
* Provides a form to confirm the removal of a section.
*
* @internal
* Form classes are internal.
*/
class RemoveSectionForm extends LayoutRebuildConfirmFormBase {
/**
* {@inheritdoc}
*/
public function getFormId() {
return 'layout_builder_remove_section';
}
/**
* {@inheritdoc}
*/
public function getQuestion() {
$configuration = $this->sectionStorage->getSection($this->delta)->getLayoutSettings();
// Layouts may choose to use a class that might not have a label
// configuration.
if (!empty($configuration['label'])) {
return $this->t('Are you sure you want to remove @section?', ['@section' => $configuration['label']]);
}
return $this->t('Are you sure you want to remove section @section?', ['@section' => $this->delta + 1]);
}
/**
* {@inheritdoc}
*/
public function getConfirmText() {
return $this->t('Remove');
}
/**
* {@inheritdoc}
*/
protected function handleSectionStorage(SectionStorageInterface $section_storage, FormStateInterface $form_state) {
$section_storage->removeSection($this->delta);
}
}

View File

@@ -0,0 +1,124 @@
<?php
namespace Drupal\layout_builder\Form;
use Drupal\Core\Form\ConfirmFormBase;
use Drupal\Core\Form\FormStateInterface;
use Drupal\Core\Form\WorkspaceDynamicSafeFormInterface;
use Drupal\Core\Messenger\MessengerInterface;
use Drupal\layout_builder\LayoutTempstoreRepositoryInterface;
use Drupal\layout_builder\OverridesSectionStorageInterface;
use Drupal\layout_builder\SectionStorageInterface;
use Symfony\Component\DependencyInjection\ContainerInterface;
/**
* Reverts the overridden layout to the defaults.
*
* @internal
* Form classes are internal.
*/
class RevertOverridesForm extends ConfirmFormBase implements WorkspaceDynamicSafeFormInterface {
use WorkspaceSafeFormTrait;
/**
* The layout tempstore repository.
*
* @var \Drupal\layout_builder\LayoutTempstoreRepositoryInterface
*/
protected $layoutTempstoreRepository;
/**
* The messenger service.
*
* @var \Drupal\Core\Messenger\MessengerInterface
*/
protected $messenger;
/**
* The section storage.
*
* @var \Drupal\layout_builder\SectionStorageInterface
*/
protected $sectionStorage;
/**
* Constructs a new RevertOverridesForm.
*
* @param \Drupal\layout_builder\LayoutTempstoreRepositoryInterface $layout_tempstore_repository
* The layout tempstore repository.
* @param \Drupal\Core\Messenger\MessengerInterface $messenger
* The messenger service.
*/
public function __construct(LayoutTempstoreRepositoryInterface $layout_tempstore_repository, MessengerInterface $messenger) {
$this->layoutTempstoreRepository = $layout_tempstore_repository;
$this->messenger = $messenger;
}
/**
* {@inheritdoc}
*/
public static function create(ContainerInterface $container) {
return new static(
$container->get('layout_builder.tempstore_repository'),
$container->get('messenger')
);
}
/**
* {@inheritdoc}
*/
public function getFormId() {
return 'layout_builder_revert_overrides';
}
/**
* {@inheritdoc}
*/
public function getQuestion() {
return $this->t('Are you sure you want to revert this to defaults?');
}
/**
* {@inheritdoc}
*/
public function getConfirmText() {
return $this->t('Revert');
}
/**
* {@inheritdoc}
*/
public function getCancelUrl() {
return $this->sectionStorage->getLayoutBuilderUrl();
}
/**
* {@inheritdoc}
*/
public function buildForm(array $form, FormStateInterface $form_state, ?SectionStorageInterface $section_storage = NULL) {
if (!$section_storage instanceof OverridesSectionStorageInterface) {
throw new \InvalidArgumentException(sprintf('The section storage with type "%s" and ID "%s" does not provide overrides', $section_storage->getStorageType(), $section_storage->getStorageId()));
}
$this->sectionStorage = $section_storage;
// Mark this as an administrative page for JavaScript ("Back to site" link).
$form['#attached']['drupalSettings']['path']['currentPathIsAdmin'] = TRUE;
return parent::buildForm($form, $form_state);
}
/**
* {@inheritdoc}
*/
public function submitForm(array &$form, FormStateInterface $form_state) {
// Remove all sections.
$this->sectionStorage
->removeAllSections()
->save();
$this->layoutTempstoreRepository->delete($this->sectionStorage);
$this->messenger->addMessage($this->t('The layout has been reverted back to defaults.'));
$form_state->setRedirectUrl($this->sectionStorage->getRedirectUrl());
}
}

View File

@@ -0,0 +1,58 @@
<?php
namespace Drupal\layout_builder\Form;
use Drupal\Core\Form\FormStateInterface;
use Drupal\layout_builder\LayoutBuilderHighlightTrait;
use Drupal\layout_builder\SectionStorageInterface;
/**
* Provides a form to update a block.
*
* @internal
* Form classes are internal.
*/
class UpdateBlockForm extends ConfigureBlockFormBase {
use LayoutBuilderHighlightTrait;
/**
* {@inheritdoc}
*/
public function getFormId() {
return 'layout_builder_update_block';
}
/**
* Builds the block form.
*
* @param array $form
* An associative array containing the structure of the form.
* @param \Drupal\Core\Form\FormStateInterface $form_state
* The current state of the form.
* @param \Drupal\layout_builder\SectionStorageInterface $section_storage
* The section storage being configured.
* @param int $delta
* The delta of the section.
* @param string $region
* The region of the block.
* @param string $uuid
* The UUID of the block being updated.
*
* @return array
* The form array.
*/
public function buildForm(array $form, FormStateInterface $form_state, ?SectionStorageInterface $section_storage = NULL, $delta = NULL, $region = NULL, $uuid = NULL) {
$component = $section_storage->getSection($delta)->getComponent($uuid);
$form['#attributes']['data-layout-builder-target-highlight-id'] = $this->blockUpdateHighlightId($uuid);
return $this->doBuildForm($form, $form_state, $section_storage, $delta, $component);
}
/**
* {@inheritdoc}
*/
protected function submitLabel() {
return $this->t('Update');
}
}

View File

@@ -0,0 +1,63 @@
<?php
declare(strict_types=1);
namespace Drupal\layout_builder\Form;
use Drupal\Core\Entity\Form\WorkspaceSafeFormTrait as EntityWorkspaceSafeFormTrait;
use Drupal\Core\Form\FormStateInterface;
use Drupal\layout_builder\SectionStorageInterface;
/**
* Provides a trait that marks Layout Builder forms as workspace-safe.
*/
trait WorkspaceSafeFormTrait {
use EntityWorkspaceSafeFormTrait;
/**
* Determines whether the current form is safe to be submitted in a workspace.
*
* @param array $form
* An associative array containing the structure of the form.
* @param \Drupal\Core\Form\FormStateInterface $form_state
* The current state of the form.
*
* @return bool
* TRUE if the form is workspace-safe, FALSE otherwise.
*/
public function isWorkspaceSafeForm(array $form, FormStateInterface $form_state): bool {
$section_storage = $this->sectionStorage ?: $this->getSectionStorageFromFormState($form_state);
if ($section_storage) {
$context_definitions = $section_storage->getContextDefinitions();
if (!empty($context_definitions['entity'])) {
/** @var \Drupal\Core\Entity\EntityInterface $entity */
$entity = $section_storage->getContextValue('entity');
return $this->isWorkspaceSafeEntity($entity);
}
}
return FALSE;
}
/**
* Retrieves the section storage from a form state object, if it exists.
*
* @param \Drupal\Core\Form\FormStateInterface $form_state
* The form state object.
*
* @return \Drupal\layout_builder\SectionStorageInterface|null
* The section storage or NULL if it doesn't exist.
*/
protected function getSectionStorageFromFormState(FormStateInterface $form_state): ?SectionStorageInterface {
foreach ($form_state->getBuildInfo()['args'] as $argument) {
if ($argument instanceof SectionStorageInterface) {
return $argument;
}
}
return NULL;
}
}

View File

@@ -0,0 +1,241 @@
<?php
namespace Drupal\layout_builder;
use Drupal\Core\DependencyInjection\ContainerInjectionInterface;
use Drupal\Core\Entity\EntityInterface;
use Drupal\Core\Entity\EntityTypeManagerInterface;
use Drupal\Core\Entity\RevisionableInterface;
use Drupal\Core\Entity\SynchronizableInterface;
use Drupal\layout_builder\SectionStorage\SectionStorageManagerInterface;
use Symfony\Component\DependencyInjection\ContainerInterface;
/**
* Defines a class for reacting to entity events related to Inline Blocks.
*
* @internal
* This is an internal utility class wrapping hook implementations.
*/
class InlineBlockEntityOperations implements ContainerInjectionInterface {
use LayoutEntityHelperTrait;
/**
* Inline block usage tracking service.
*
* @var \Drupal\layout_builder\InlineBlockUsageInterface
*/
protected $usage;
/**
* The block content storage.
*
* @var \Drupal\Core\Entity\EntityStorageInterface
*/
protected $blockContentStorage;
/**
* The entity type manager.
*
* @var \Drupal\Core\Entity\EntityTypeManagerInterface
*/
protected $entityTypeManager;
/**
* Constructs a new EntityOperations object.
*
* @param \Drupal\Core\Entity\EntityTypeManagerInterface $entityTypeManager
* The entity type manager service.
* @param \Drupal\layout_builder\InlineBlockUsageInterface $usage
* Inline block usage tracking service.
* @param \Drupal\layout_builder\SectionStorage\SectionStorageManagerInterface $section_storage_manager
* The section storage manager.
*/
public function __construct(EntityTypeManagerInterface $entityTypeManager, InlineBlockUsageInterface $usage, SectionStorageManagerInterface $section_storage_manager) {
$this->entityTypeManager = $entityTypeManager;
$this->blockContentStorage = $entityTypeManager->getStorage('block_content');
$this->usage = $usage;
$this->sectionStorageManager = $section_storage_manager;
}
/**
* {@inheritdoc}
*/
public static function create(ContainerInterface $container) {
return new static(
$container->get('entity_type.manager'),
$container->get('inline_block.usage'),
$container->get('plugin.manager.layout_builder.section_storage')
);
}
/**
* Remove all unused inline blocks on save.
*
* Entities that were used in prevision revisions will be removed if not
* saving a new revision.
*
* @param \Drupal\Core\Entity\EntityInterface $entity
* The parent entity.
*/
protected function removeUnusedForEntityOnSave(EntityInterface $entity) {
// If the entity is new or '$entity->original' is not set then there will
// not be any unused inline blocks to remove.
// If this is a revisionable entity then do not remove inline blocks. They
// could be referenced in previous revisions even if this is not a new
// revision.
if ($entity->isNew() || !isset($entity->original) || $entity instanceof RevisionableInterface) {
return;
}
// If the original entity used the default storage then we cannot remove
// unused inline blocks because they will still be referenced in the
// defaults.
if ($this->originalEntityUsesDefaultStorage($entity)) {
return;
}
// Delete and remove the usage for inline blocks that were removed.
if ($removed_block_ids = $this->getRemovedBlockIds($entity)) {
$this->deleteBlocksAndUsage($removed_block_ids);
}
}
/**
* Gets the IDs of the inline blocks that were removed.
*
* @param \Drupal\Core\Entity\EntityInterface $entity
* The layout entity.
*
* @return int[]
* The block content IDs that were removed.
*/
protected function getRemovedBlockIds(EntityInterface $entity) {
$original_sections = $this->getEntitySections($entity->original);
$current_sections = $this->getEntitySections($entity);
// Avoid un-needed conversion from revision IDs to block content IDs by
// first determining if there are any revisions in the original that are not
// also in the current sections.
$current_block_content_revision_ids = $this->getInlineBlockRevisionIdsInSections($current_sections);
$original_block_content_revision_ids = $this->getInlineBlockRevisionIdsInSections($original_sections);
if ($unused_original_revision_ids = array_diff($original_block_content_revision_ids, $current_block_content_revision_ids)) {
// If there are any revisions in the original that aren't in the current
// there may some blocks that need to be removed.
$current_block_content_ids = $this->getBlockIdsForRevisionIds($current_block_content_revision_ids);
$unused_original_block_content_ids = $this->getBlockIdsForRevisionIds($unused_original_revision_ids);
return array_diff($unused_original_block_content_ids, $current_block_content_ids);
}
return [];
}
/**
* Handles entity tracking on deleting a parent entity.
*
* @param \Drupal\Core\Entity\EntityInterface $entity
* The parent entity.
*/
public function handleEntityDelete(EntityInterface $entity) {
// @todo In https://www.drupal.org/node/3008943 call
// \Drupal\layout_builder\LayoutEntityHelperTrait::isLayoutCompatibleEntity().
$this->usage->removeByLayoutEntity($entity);
}
/**
* Handles saving a parent entity.
*
* @param \Drupal\Core\Entity\EntityInterface $entity
* The parent entity.
*/
public function handlePreSave(EntityInterface $entity) {
if (($entity instanceof SynchronizableInterface && $entity->isSyncing())
|| !$this->isLayoutCompatibleEntity($entity)
) {
return;
}
$duplicate_blocks = FALSE;
if ($sections = $this->getEntitySections($entity)) {
if ($this->originalEntityUsesDefaultStorage($entity)) {
// This is a new override from a default and the blocks need to be
// duplicated.
$duplicate_blocks = TRUE;
}
// Since multiple parent entity revisions may reference common block
// revisions, when a block is modified, it must always result in the
// creation of a new block revision.
$new_revision = $entity instanceof RevisionableInterface;
foreach ($this->getInlineBlockComponents($sections) as $component) {
$this->saveInlineBlockComponent($entity, $component, $new_revision, $duplicate_blocks);
}
}
$this->removeUnusedForEntityOnSave($entity);
}
/**
* Delete the inline blocks and the usage records.
*
* @param int[] $block_content_ids
* The block content entity IDs.
*/
protected function deleteBlocksAndUsage(array $block_content_ids) {
foreach ($block_content_ids as $block_content_id) {
if ($block = $this->blockContentStorage->load($block_content_id)) {
$block->delete();
}
}
$this->usage->deleteUsage($block_content_ids);
}
/**
* Removes unused inline blocks.
*
* @param int $limit
* The maximum number of inline blocks to remove.
*/
public function removeUnused($limit = 100) {
$this->deleteBlocksAndUsage($this->usage->getUnused($limit));
}
/**
* Gets blocks IDs for an array of revision IDs.
*
* @param int[] $revision_ids
* The revision IDs.
*
* @return int[]
* The block IDs.
*/
protected function getBlockIdsForRevisionIds(array $revision_ids) {
if ($revision_ids) {
$query = $this->blockContentStorage->getQuery()->accessCheck(FALSE);
$query->condition('revision_id', $revision_ids, 'IN');
$block_ids = $query->execute();
return $block_ids;
}
return [];
}
/**
* Saves an inline block component.
*
* @param \Drupal\Core\Entity\EntityInterface $entity
* The entity with the layout.
* @param \Drupal\layout_builder\SectionComponent $component
* The section component with an inline block.
* @param bool $new_revision
* Whether a new revision of the block should be created when modified.
* @param bool $duplicate_blocks
* Whether the blocks should be duplicated.
*/
protected function saveInlineBlockComponent(EntityInterface $entity, SectionComponent $component, $new_revision, $duplicate_blocks) {
/** @var \Drupal\layout_builder\Plugin\Block\InlineBlock $plugin */
$plugin = $component->getPlugin();
$pre_save_configuration = $plugin->getConfiguration();
$plugin->saveBlockContent($new_revision, $duplicate_blocks);
$post_save_configuration = $plugin->getConfiguration();
if ($duplicate_blocks || (empty($pre_save_configuration['block_revision_id']) && !empty($post_save_configuration['block_revision_id']))) {
$this->usage->addUsage($post_save_configuration['block_id'], $entity);
}
$component->setConfiguration($post_save_configuration);
}
}

View File

@@ -0,0 +1,88 @@
<?php
namespace Drupal\layout_builder;
use Drupal\Core\Database\Connection;
use Drupal\Core\Entity\EntityInterface;
/**
* Service class to track inline block usage.
*/
class InlineBlockUsage implements InlineBlockUsageInterface {
/**
* The database connection.
*
* @var \Drupal\Core\Database\Connection
*/
protected $database;
/**
* Creates an InlineBlockUsage object.
*
* @param \Drupal\Core\Database\Connection $database
* The database connection.
*/
public function __construct(Connection $database) {
$this->database = $database;
}
/**
* {@inheritdoc}
*/
public function addUsage($block_content_id, EntityInterface $entity) {
$this->database->merge('inline_block_usage')
->keys([
'block_content_id' => $block_content_id,
'layout_entity_id' => $entity->id(),
'layout_entity_type' => $entity->getEntityTypeId(),
])->execute();
}
/**
* {@inheritdoc}
*/
public function getUnused($limit = 100) {
$query = $this->database->select('inline_block_usage', 't');
$query->fields('t', ['block_content_id']);
$query->isNull('layout_entity_id');
$query->isNull('layout_entity_type');
return $query->range(0, $limit)->execute()->fetchCol();
}
/**
* {@inheritdoc}
*/
public function removeByLayoutEntity(EntityInterface $entity) {
$query = $this->database->update('inline_block_usage')
->fields([
'layout_entity_type' => NULL,
'layout_entity_id' => NULL,
]);
$query->condition('layout_entity_type', $entity->getEntityTypeId());
$query->condition('layout_entity_id', $entity->id());
$query->execute();
}
/**
* {@inheritdoc}
*/
public function deleteUsage(array $block_content_ids) {
if (!empty($block_content_ids)) {
$query = $this->database->delete('inline_block_usage')->condition('block_content_id', $block_content_ids, 'IN');
$query->execute();
}
}
/**
* {@inheritdoc}
*/
public function getUsage($block_content_id) {
$query = $this->database->select('inline_block_usage');
$query->condition('block_content_id', $block_content_id);
$query->fields('inline_block_usage', ['layout_entity_id', 'layout_entity_type']);
$query->range(0, 1);
return $query->execute()->fetchObject();
}
}

View File

@@ -0,0 +1,61 @@
<?php
namespace Drupal\layout_builder;
use Drupal\Core\Entity\EntityInterface;
/**
* Defines an interface for tracking inline block usage.
*/
interface InlineBlockUsageInterface {
/**
* Adds a usage record.
*
* @param int $block_content_id
* The block content ID.
* @param \Drupal\Core\Entity\EntityInterface $entity
* The layout entity.
*/
public function addUsage($block_content_id, EntityInterface $entity);
/**
* Gets unused inline block IDs.
*
* @param int $limit
* The maximum number of block content entity IDs to return.
*
* @return int[]
* The entity IDs.
*/
public function getUnused($limit = 100);
/**
* Remove usage record by layout entity.
*
* @param \Drupal\Core\Entity\EntityInterface $entity
* The layout entity.
*/
public function removeByLayoutEntity(EntityInterface $entity);
/**
* Delete the inline blocks' the usage records.
*
* @param int[] $block_content_ids
* The block content entity IDs.
*/
public function deleteUsage(array $block_content_ids);
/**
* Gets usage record for inline block by ID.
*
* @param int $block_content_id
* The block content entity ID.
*
* @return object|false
* The usage record with properties layout_entity_id and layout_entity_type
* or FALSE if there is no usage.
*/
public function getUsage($block_content_id);
}

View File

@@ -0,0 +1,32 @@
<?php
namespace Drupal\layout_builder;
/**
* Provides methods for enabling and disabling Layout Builder.
*/
interface LayoutBuilderEnabledInterface {
/**
* Determines if Layout Builder is enabled.
*
* @return bool
* TRUE if Layout Builder is enabled, FALSE otherwise.
*/
public function isLayoutBuilderEnabled();
/**
* Enables the Layout Builder.
*
* @return $this
*/
public function enableLayoutBuilder();
/**
* Disables the Layout Builder.
*
* @return $this
*/
public function disableLayoutBuilder();
}

View File

@@ -0,0 +1,42 @@
<?php
namespace Drupal\layout_builder;
/**
* Defines events for the layout_builder module.
*
* @see \Drupal\layout_builder\Event\SectionComponentBuildRenderArrayEvent
*/
final class LayoutBuilderEvents {
/**
* Name of the event fired when a component's render array is built.
*
* This event allows modules to collaborate on creating the render array of
* the SectionComponent object. The event listener method receives a
* \Drupal\layout_builder\Event\SectionComponentBuildRenderArrayEvent
* instance.
*
* @Event
*
* @see \Drupal\layout_builder\Event\SectionComponentBuildRenderArrayEvent
* @see \Drupal\layout_builder\SectionComponent::toRenderArray()
*
* @var string
*/
const SECTION_COMPONENT_BUILD_RENDER_ARRAY = 'section_component.build.render_array';
/**
* Name of the event fired in when preparing a layout builder element.
*
* This event allows modules to collaborate on creating the sections used in
* \Drupal\layout_builder\Element\LayoutBuilder during #pre_render.
*
* @see \Drupal\layout_builder\Event\PrepareLayoutEvent
* @see \Drupal\layout_builder\Element\LayoutBuilder
*
* @var string
*/
const PREPARE_LAYOUT = 'prepare_layout';
}

View File

@@ -0,0 +1,64 @@
<?php
namespace Drupal\layout_builder;
/**
* A trait for generating IDs used to highlight active UI elements.
*/
trait LayoutBuilderHighlightTrait {
/**
* Provides the ID used to highlight the active Layout Builder UI element.
*
* @param string $delta
* The section the block is in.
* @param string $region
* The section region in which the block is placed.
*
* @return string
* The highlight ID of the block.
*/
protected function blockAddHighlightId($delta, $region) {
return "block-$delta-$region";
}
/**
* Provides the ID used to highlight the active Layout Builder UI element.
*
* @param string $uuid
* The uuid of the block.
*
* @return string
* The highlight ID of the block.
*/
protected function blockUpdateHighlightId($uuid) {
return $uuid;
}
/**
* Provides the ID used to highlight the active Layout Builder UI element.
*
* @param string $delta
* The location of the section.
*
* @return string
* The highlight ID of the section.
*/
protected function sectionAddHighlightId($delta) {
return "section-$delta";
}
/**
* Provides the ID used to highlight the active Layout Builder UI element.
*
* @param string $delta
* The location of the section.
*
* @return string
* The highlight ID of the section.
*/
protected function sectionUpdateHighlightId($delta) {
return "section-update-$delta";
}
}

View File

@@ -0,0 +1,28 @@
<?php
namespace Drupal\layout_builder;
/**
* Provides an interface for displays that could be overridable.
*/
interface LayoutBuilderOverridableInterface {
/**
* Determines if the display allows custom overrides.
*
* @return bool
* TRUE if custom overrides are allowed, FALSE otherwise.
*/
public function isOverridable();
/**
* Sets the display to allow or disallow overrides.
*
* @param bool $overridable
* TRUE if the display should allow overrides, FALSE otherwise.
*
* @return $this
*/
public function setOverridable($overridable = TRUE);
}

View File

@@ -0,0 +1,115 @@
<?php
namespace Drupal\layout_builder;
use Drupal\Core\DependencyInjection\ContainerInjectionInterface;
use Drupal\Core\Entity\EntityTypeBundleInfoInterface;
use Drupal\Core\Entity\EntityTypeManagerInterface;
use Drupal\Core\StringTranslation\StringTranslationTrait;
use Symfony\Component\DependencyInjection\ContainerInterface;
/**
* Provides dynamic permissions for Layout Builder overrides.
*
* @see \Drupal\layout_builder\Plugin\SectionStorage\OverridesSectionStorage::access()
*
* @internal
* Dynamic permission callbacks are internal.
*/
class LayoutBuilderOverridesPermissions implements ContainerInjectionInterface {
use StringTranslationTrait;
/**
* The entity type manager.
*
* @var \Drupal\Core\Entity\EntityTypeManagerInterface
*/
protected $entityTypeManager;
/**
* The entity type bundle info service.
*
* @var \Drupal\Core\Entity\EntityTypeBundleInfoInterface
*/
protected $bundleInfo;
/**
* LayoutBuilderOverridesPermissions constructor.
*
* @param \Drupal\Core\Entity\EntityTypeManagerInterface $entity_type_manager
* The entity type manager.
* @param \Drupal\Core\Entity\EntityTypeBundleInfoInterface $bundle_info
* The bundle info service.
*/
public function __construct(EntityTypeManagerInterface $entity_type_manager, EntityTypeBundleInfoInterface $bundle_info) {
$this->entityTypeManager = $entity_type_manager;
$this->bundleInfo = $bundle_info;
}
/**
* {@inheritdoc}
*/
public static function create(ContainerInterface $container) {
return new static(
$container->get('entity_type.manager'),
$container->get('entity_type.bundle.info')
);
}
/**
* Returns an array of permissions.
*
* @return string[][]
* An array whose keys are permission names and whose corresponding values
* are defined in \Drupal\user\PermissionHandlerInterface::getPermissions().
*/
public function permissions() {
$permissions = [];
/** @var \Drupal\layout_builder\Entity\LayoutEntityDisplayInterface[] $entity_displays */
$entity_displays = $this->entityTypeManager->getStorage('entity_view_display')->loadByProperties(['third_party_settings.layout_builder.allow_custom' => TRUE]);
foreach ($entity_displays as $entity_display) {
$entity_type_id = $entity_display->getTargetEntityTypeId();
$entity_type = $this->entityTypeManager->getDefinition($entity_type_id);
$bundle = $entity_display->getTargetBundle();
$args = [
'%entity_type' => $entity_type->getCollectionLabel(),
'@entity_type_singular' => $entity_type->getSingularLabel(),
'@entity_type_plural' => $entity_type->getPluralLabel(),
'%bundle' => $this->bundleInfo->getBundleInfo($entity_type_id)[$bundle]['label'],
];
// These permissions are generated on behalf of $entity_display entity
// display, therefore add this entity display as a config dependency.
$dependencies = [
$entity_display->getConfigDependencyKey() => [
$entity_display->getConfigDependencyName(),
],
];
if ($entity_type->hasKey('bundle')) {
$permissions["configure all $bundle $entity_type_id layout overrides"] = [
'title' => $this->t('%entity_type - %bundle: Configure all layout overrides', $args),
'warning' => $this->t('Warning: Allows configuring the layout even if the user cannot edit the @entity_type_singular itself.', $args),
'dependencies' => $dependencies,
];
$permissions["configure editable $bundle $entity_type_id layout overrides"] = [
'title' => $this->t('%entity_type - %bundle: Configure layout overrides for @entity_type_plural that the user can edit', $args),
'dependencies' => $dependencies,
];
}
else {
$permissions["configure all $bundle $entity_type_id layout overrides"] = [
'title' => $this->t('%entity_type: Configure all layout overrides', $args),
'warning' => $this->t('Warning: Allows configuring the layout even if the user cannot edit the @entity_type_singular itself.', $args),
'dependencies' => $dependencies,
];
$permissions["configure editable $bundle $entity_type_id layout overrides"] = [
'title' => $this->t('%entity_type: Configure layout overrides for @entity_type_plural that the user can edit', $args),
'dependencies' => $dependencies,
];
}
}
return $permissions;
}
}

View File

@@ -0,0 +1,53 @@
<?php
namespace Drupal\layout_builder;
use Drupal\Core\DependencyInjection\ContainerBuilder;
use Drupal\Core\DependencyInjection\ServiceProviderInterface;
use Drupal\layout_builder\EventSubscriber\SetInlineBlockDependency;
use Drupal\layout_builder\Normalizer\LayoutEntityDisplayNormalizer;
use Symfony\Component\DependencyInjection\ChildDefinition;
use Symfony\Component\DependencyInjection\Definition;
use Symfony\Component\DependencyInjection\Reference;
/**
* Sets the layout_builder.get_block_dependency_subscriber service definition.
*
* This service is dependent on the block_content module so it must be provided
* dynamically.
*
* @internal
* Service providers are internal.
*
* @see \Drupal\layout_builder\EventSubscriber\SetInlineBlockDependency
*/
class LayoutBuilderServiceProvider implements ServiceProviderInterface {
/**
* {@inheritdoc}
*/
public function register(ContainerBuilder $container) {
$modules = $container->getParameter('container.modules');
if (isset($modules['block_content'])) {
$definition = new Definition(SetInlineBlockDependency::class);
$definition->setArguments([
new Reference('entity_type.manager'),
new Reference('database'),
new Reference('inline_block.usage'),
new Reference('plugin.manager.layout_builder.section_storage'),
]);
$definition->addTag('event_subscriber');
$definition->setPublic(TRUE);
$container->setDefinition('layout_builder.get_block_dependency_subscriber', $definition);
}
if (isset($modules['serialization'])) {
$definition = (new ChildDefinition('serializer.normalizer.config_entity'))
->setClass(LayoutEntityDisplayNormalizer::class)
// Ensure that this normalizer takes precedence for Layout Builder data
// over the generic serializer.normalizer.config_entity.
->addTag('normalizer', ['priority' => 5]);
$container->setDefinition('layout_builder.normalizer.layout_entity_display', $definition);
}
}
}

View File

@@ -0,0 +1,158 @@
<?php
namespace Drupal\layout_builder;
use Drupal\Component\Plugin\DerivativeInspectionInterface;
use Drupal\Core\Cache\CacheableMetadata;
use Drupal\Core\Entity\Entity\EntityViewDisplay;
use Drupal\Core\Entity\EntityInterface;
use Drupal\Core\Entity\FieldableEntityInterface;
use Drupal\Core\Plugin\Context\Context;
use Drupal\Core\Plugin\Context\ContextDefinition;
use Drupal\Core\Plugin\Context\EntityContext;
use Drupal\layout_builder\Entity\LayoutEntityDisplayInterface;
/**
* Methods to help with entities using the layout builder.
*/
trait LayoutEntityHelperTrait {
/**
* The section storage manager.
*
* @var \Drupal\layout_builder\SectionStorage\SectionStorageManagerInterface
*/
protected $sectionStorageManager;
/**
* Determines if an entity can have a layout.
*
* @param \Drupal\Core\Entity\EntityInterface $entity
* The entity to check.
*
* @return bool
* TRUE if the entity can have a layout otherwise FALSE.
*/
protected function isLayoutCompatibleEntity(EntityInterface $entity) {
return $this->getSectionStorageForEntity($entity) !== NULL;
}
/**
* Gets revision IDs for layout sections.
*
* @param \Drupal\layout_builder\Section[] $sections
* The layout sections.
*
* @return int[]
* The revision IDs.
*/
protected function getInlineBlockRevisionIdsInSections(array $sections) {
$revision_ids = [];
foreach ($this->getInlineBlockComponents($sections) as $component) {
$configuration = $component->getPlugin()->getConfiguration();
if (!empty($configuration['block_revision_id'])) {
$revision_ids[] = $configuration['block_revision_id'];
}
}
return $revision_ids;
}
/**
* Gets the sections for an entity if any.
*
* @param \Drupal\Core\Entity\EntityInterface $entity
* The entity.
*
* @return \Drupal\layout_builder\Section[]
* The entity layout sections if available.
*/
protected function getEntitySections(EntityInterface $entity) {
$section_storage = $this->getSectionStorageForEntity($entity);
return $section_storage ? $section_storage->getSections() : [];
}
/**
* Gets components that have Inline Block plugins.
*
* @param \Drupal\layout_builder\Section[] $sections
* The layout sections.
*
* @return \Drupal\layout_builder\SectionComponent[]
* The components that contain Inline Block plugins.
*/
protected function getInlineBlockComponents(array $sections) {
$inline_block_components = [];
foreach ($sections as $section) {
foreach ($section->getComponents() as $component) {
$plugin = $component->getPlugin();
if ($plugin instanceof DerivativeInspectionInterface && $plugin->getBaseId() === 'inline_block') {
$inline_block_components[] = $component;
}
}
}
return $inline_block_components;
}
/**
* Gets the section storage for an entity.
*
* @param \Drupal\Core\Entity\EntityInterface $entity
* The entity.
*
* @return \Drupal\layout_builder\SectionStorageInterface|null
* The section storage if found otherwise NULL.
*/
protected function getSectionStorageForEntity(EntityInterface $entity) {
// @todo Take into account other view modes in
// https://www.drupal.org/node/3008924.
$view_mode = 'full';
if ($entity instanceof LayoutEntityDisplayInterface) {
$contexts['display'] = EntityContext::fromEntity($entity);
$contexts['view_mode'] = new Context(new ContextDefinition('string'), $entity->getMode());
}
else {
$contexts['entity'] = EntityContext::fromEntity($entity);
if ($entity instanceof FieldableEntityInterface) {
$display = EntityViewDisplay::collectRenderDisplay($entity, $view_mode);
if ($display instanceof LayoutEntityDisplayInterface) {
$contexts['display'] = EntityContext::fromEntity($display);
}
$contexts['view_mode'] = new Context(new ContextDefinition('string'), $view_mode);
}
}
return $this->sectionStorageManager()->findByContext($contexts, new CacheableMetadata());
}
/**
* Determines if the original entity used the default section storage.
*
* This method can be used during the entity save process to determine whether
* $entity->original is set and used the default section storage plugin as
* determined by ::getSectionStorageForEntity().
*
* @param \Drupal\Core\Entity\EntityInterface $entity
* The entity.
*
* @return bool
* TRUE if the original entity used the default storage.
*/
protected function originalEntityUsesDefaultStorage(EntityInterface $entity) {
$section_storage = $this->getSectionStorageForEntity($entity);
if ($section_storage instanceof OverridesSectionStorageInterface && !$entity->isNew() && isset($entity->original)) {
$original_section_storage = $this->getSectionStorageForEntity($entity->original);
return $original_section_storage instanceof DefaultsSectionStorageInterface;
}
return FALSE;
}
/**
* Gets the section storage manager.
*
* @return \Drupal\layout_builder\SectionStorage\SectionStorageManagerInterface
* The section storage manager.
*/
private function sectionStorageManager() {
return $this->sectionStorageManager ?: \Drupal::service('plugin.manager.layout_builder.section_storage');
}
}

View File

@@ -0,0 +1,130 @@
<?php
namespace Drupal\layout_builder;
use Drupal\Core\TempStore\SharedTempStoreFactory;
/**
* Provides a mechanism for loading layouts from tempstore.
*/
class LayoutTempstoreRepository implements LayoutTempstoreRepositoryInterface {
/**
* The shared tempstore factory.
*
* @var \Drupal\Core\TempStore\SharedTempStoreFactory
*/
protected $tempStoreFactory;
/**
* The static cache of loaded values.
*
* @var \Drupal\layout_builder\SectionStorageInterface[]
*/
protected array $cache = [];
/**
* LayoutTempstoreRepository constructor.
*
* @param \Drupal\Core\TempStore\SharedTempStoreFactory $temp_store_factory
* The shared tempstore factory.
*/
public function __construct(SharedTempStoreFactory $temp_store_factory) {
$this->tempStoreFactory = $temp_store_factory;
}
/**
* {@inheritdoc}
*/
public function get(SectionStorageInterface $section_storage) {
$key = $this->getKey($section_storage);
// Check if the storage is present in the static cache.
if (isset($this->cache[$key])) {
return $this->cache[$key];
}
$tempstore = $this->getTempstore($section_storage)->get($key);
if (!empty($tempstore['section_storage'])) {
$storage_type = $section_storage->getStorageType();
$section_storage = $tempstore['section_storage'];
if (!($section_storage instanceof SectionStorageInterface)) {
throw new \UnexpectedValueException(sprintf('The entry with storage type "%s" and ID "%s" is invalid', $storage_type, $key));
}
// Set the storage in the static cache.
$this->cache[$key] = $section_storage;
}
return $section_storage;
}
/**
* {@inheritdoc}
*/
public function has(SectionStorageInterface $section_storage) {
$key = $this->getKey($section_storage);
// Check if the storage is present in the static cache.
if (isset($this->cache[$key])) {
return TRUE;
}
$tempstore = $this->getTempstore($section_storage)->get($key);
return !empty($tempstore['section_storage']);
}
/**
* {@inheritdoc}
*/
public function set(SectionStorageInterface $section_storage) {
$key = $this->getKey($section_storage);
$this->getTempstore($section_storage)->set($key, ['section_storage' => $section_storage]);
// Update the storage in the static cache.
$this->cache[$key] = $section_storage;
}
/**
* {@inheritdoc}
*/
public function delete(SectionStorageInterface $section_storage) {
$key = $this->getKey($section_storage);
$this->getTempstore($section_storage)->delete($key);
// Remove the storage from the static cache.
unset($this->cache[$key]);
}
/**
* Gets the shared tempstore.
*
* @param \Drupal\layout_builder\SectionStorageInterface $section_storage
* The section storage.
*
* @return \Drupal\Core\TempStore\SharedTempStore
* The tempstore.
*/
protected function getTempstore(SectionStorageInterface $section_storage) {
$collection = 'layout_builder.section_storage.' . $section_storage->getStorageType();
return $this->tempStoreFactory->get($collection);
}
/**
* Gets the string to use as the tempstore key.
*
* @param \Drupal\layout_builder\SectionStorageInterface $section_storage
* The section storage.
*
* @return string
* A unique string representing the section storage. This should include as
* much identifying information as possible about this particular storage,
* including information like the current language.
*/
protected function getKey(SectionStorageInterface $section_storage) {
if ($section_storage instanceof TempStoreIdentifierInterface) {
return $section_storage->getTempstoreKey();
}
return $section_storage->getStorageId();
}
}

View File

@@ -0,0 +1,52 @@
<?php
namespace Drupal\layout_builder;
/**
* Provides an interface for loading layouts from tempstore.
*/
interface LayoutTempstoreRepositoryInterface {
/**
* Gets the tempstore version of a section storage, if it exists.
*
* @param \Drupal\layout_builder\SectionStorageInterface $section_storage
* The section storage to check for in tempstore.
*
* @return \Drupal\layout_builder\SectionStorageInterface
* Either the version of this section storage from tempstore, or the passed
* section storage if none exists.
*
* @throw \UnexpectedValueException
* Thrown if a value exists, but is not a section storage.
*/
public function get(SectionStorageInterface $section_storage);
/**
* Stores this section storage in tempstore.
*
* @param \Drupal\layout_builder\SectionStorageInterface $section_storage
* The section storage to set in tempstore.
*/
public function set(SectionStorageInterface $section_storage);
/**
* Checks for the existence of a tempstore version of a section storage.
*
* @param \Drupal\layout_builder\SectionStorageInterface $section_storage
* The section storage to check for in tempstore.
*
* @return bool
* TRUE if there is a tempstore version of this section storage.
*/
public function has(SectionStorageInterface $section_storage);
/**
* Removes the tempstore version of a section storage.
*
* @param \Drupal\layout_builder\SectionStorageInterface $section_storage
* The section storage to remove from tempstore.
*/
public function delete(SectionStorageInterface $section_storage);
}

View File

@@ -0,0 +1,37 @@
<?php
namespace Drupal\layout_builder\Normalizer;
use Drupal\layout_builder\Entity\LayoutEntityDisplayInterface;
use Drupal\serialization\Normalizer\ConfigEntityNormalizer;
/**
* Normalizes/denormalizes LayoutEntityDisplay objects into an array structure.
*
* @internal
* Tagged services are internal.
*/
class LayoutEntityDisplayNormalizer extends ConfigEntityNormalizer {
/**
* {@inheritdoc}
*/
protected static function getDataWithoutInternals(array $data) {
$data = parent::getDataWithoutInternals($data);
// Do not expose the actual layout sections in normalization.
// @todo Determine what to expose here in
// https://www.drupal.org/node/2942975.
unset($data['third_party_settings']['layout_builder']['sections']);
return $data;
}
/**
* {@inheritdoc}
*/
public function getSupportedTypes(?string $format): array {
return [
LayoutEntityDisplayInterface::class => TRUE,
];
}
}

View File

@@ -0,0 +1,29 @@
<?php
namespace Drupal\layout_builder;
/**
* Defines an interface for an object that stores layout sections for overrides.
*/
interface OverridesSectionStorageInterface extends SectionStorageInterface {
/**
* Returns the corresponding defaults section storage for this override.
*
* @return \Drupal\layout_builder\DefaultsSectionStorageInterface
* The defaults section storage.
*
* @todo Determine if this method needs a parameter in
* https://www.drupal.org/project/drupal/issues/2907413.
*/
public function getDefaultSectionStorage();
/**
* Indicates if overrides are in use.
*
* @return bool
* TRUE if this overrides section storage is in use, otherwise FALSE.
*/
public function isOverridden();
}

View File

@@ -0,0 +1,188 @@
<?php
namespace Drupal\layout_builder\Plugin\Block;
use Drupal\Core\Block\Attribute\Block;
use Drupal\Core\Block\BlockBase;
use Drupal\Core\Cache\CacheableMetadata;
use Drupal\Core\Entity\EntityFieldManagerInterface;
use Drupal\Core\Entity\EntityTypeManagerInterface;
use Drupal\Core\Plugin\ContainerFactoryPluginInterface;
use Drupal\Core\Plugin\ContextAwarePluginInterface;
use Drupal\Core\Render\Element;
use Drupal\Core\Session\AccountInterface;
use Drupal\Core\StringTranslation\TranslatableMarkup;
use Drupal\layout_builder\Plugin\Derivative\ExtraFieldBlockDeriver;
use Symfony\Component\DependencyInjection\ContainerInterface;
/**
* Provides a block that renders an extra field from an entity.
*
* This block handles fields that are provided by implementations of
* hook_entity_extra_field_info().
*
* @see \Drupal\layout_builder\Plugin\Block\FieldBlock
* This block plugin handles all other field entities not provided by
* hook_entity_extra_field_info().
*
* @internal
* Plugin classes are internal.
*/
#[Block(
id: "extra_field_block",
deriver: ExtraFieldBlockDeriver::class
)]
class ExtraFieldBlock extends BlockBase implements ContextAwarePluginInterface, ContainerFactoryPluginInterface {
/**
* The entity field manager.
*
* @var \Drupal\Core\Entity\EntityFieldManagerInterface
*/
protected $entityFieldManager;
/**
* The field name.
*
* @var string
*/
protected $fieldName;
/**
* The entity type manager.
*
* @var \Drupal\Core\Entity\EntityTypeManagerInterface
*/
protected $entityTypeManager;
/**
* Constructs a new ExtraFieldBlock.
*
* @param array $configuration
* A configuration array containing information about the plugin instance.
* @param string $plugin_id
* The plugin ID for the plugin instance.
* @param mixed $plugin_definition
* The plugin implementation definition.
* @param \Drupal\Core\Entity\EntityTypeManagerInterface $entity_type_manager
* The entity type manager.
* @param \Drupal\Core\Entity\EntityFieldManagerInterface $entity_field_manager
* The entity field manager.
*/
public function __construct(array $configuration, $plugin_id, $plugin_definition, EntityTypeManagerInterface $entity_type_manager, EntityFieldManagerInterface $entity_field_manager) {
$this->entityTypeManager = $entity_type_manager;
$this->entityFieldManager = $entity_field_manager;
// Get field name from the plugin ID.
[, , , $field_name] = explode(static::DERIVATIVE_SEPARATOR, $plugin_id, 4);
assert(!empty($field_name));
$this->fieldName = $field_name;
parent::__construct($configuration, $plugin_id, $plugin_definition);
}
/**
* {@inheritdoc}
*/
public function defaultConfiguration() {
return [
'label_display' => FALSE,
'formatter' => [
'settings' => [],
'third_party_settings' => [],
],
];
}
/**
* {@inheritdoc}
*/
public static function create(ContainerInterface $container, array $configuration, $plugin_id, $plugin_definition) {
return new static(
$configuration,
$plugin_id,
$plugin_definition,
$container->get('entity_type.manager'),
$container->get('entity_field.manager')
);
}
/**
* Gets the entity that has the field.
*
* @return \Drupal\Core\Entity\FieldableEntityInterface
* The entity.
*/
protected function getEntity() {
return $this->getContextValue('entity');
}
/**
* {@inheritdoc}
*/
public function build() {
$entity = $this->getEntity();
// Add a placeholder to replace after the entity view is built.
// @see layout_builder_entity_view_alter().
$extra_fields = $this->entityFieldManager->getExtraFields($entity->getEntityTypeId(), $entity->bundle());
if (!isset($extra_fields['display'][$this->fieldName])) {
$build = [];
}
else {
$build = [
'#extra_field_placeholder_field_name' => $this->fieldName,
// Always provide a placeholder. The Layout Builder will NOT invoke
// hook_entity_view_alter() so extra fields will not be added to the
// render array. If the hook is invoked the placeholder will be
// replaced.
// @see ::replaceFieldPlaceholder()
'#markup' => $this->t('Placeholder for the @preview_fallback', ['@preview_fallback' => $this->getPreviewFallbackString()]),
];
}
CacheableMetadata::createFromObject($this)->applyTo($build);
return $build;
}
/**
* {@inheritdoc}
*/
public function getPreviewFallbackString() {
$entity = $this->getEntity();
$extra_fields = $this->entityFieldManager->getExtraFields($entity->getEntityTypeId(), $entity->bundle());
return new TranslatableMarkup('"@field" field', ['@field' => $extra_fields['display'][$this->fieldName]['label']]);
}
/**
* Replaces all placeholders for a given field.
*
* @param array $build
* The built render array for the elements.
* @param array $built_field
* The render array to replace the placeholder.
* @param string $field_name
* The field name.
*
* @see ::build()
*/
public static function replaceFieldPlaceholder(array &$build, array $built_field, $field_name) {
foreach (Element::children($build) as $child) {
if (isset($build[$child]['#extra_field_placeholder_field_name']) && $build[$child]['#extra_field_placeholder_field_name'] === $field_name) {
$placeholder_cache = CacheableMetadata::createFromRenderArray($build[$child]);
$built_cache = CacheableMetadata::createFromRenderArray($built_field);
$merged_cache = $placeholder_cache->merge($built_cache);
$build[$child] = $built_field;
$merged_cache->applyTo($build);
}
else {
static::replaceFieldPlaceholder($build[$child], $built_field, $field_name);
}
}
}
/**
* {@inheritdoc}
*/
protected function blockAccess(AccountInterface $account) {
return $this->getEntity()->access('view', $account, TRUE);
}
}

View File

@@ -0,0 +1,441 @@
<?php
namespace Drupal\layout_builder\Plugin\Block;
use Drupal\Component\Plugin\Factory\DefaultFactory;
use Drupal\Component\Utility\NestedArray;
use Drupal\Core\Access\AccessResult;
use Drupal\Core\Block\Attribute\Block;
use Drupal\Core\Block\BlockBase;
use Drupal\Core\Cache\CacheableMetadata;
use Drupal\Core\Entity\EntityDisplayBase;
use Drupal\Core\Entity\EntityFieldManagerInterface;
use Drupal\Core\Entity\FieldableEntityInterface;
use Drupal\Core\Extension\ModuleHandlerInterface;
use Drupal\Core\Field\FieldDefinitionInterface;
use Drupal\Core\Field\FormatterInterface;
use Drupal\Core\Field\FormatterPluginManager;
use Drupal\Core\Form\EnforcedResponseException;
use Drupal\Core\Form\FormHelper;
use Drupal\Core\Form\FormStateInterface;
use Drupal\Core\Plugin\ContainerFactoryPluginInterface;
use Drupal\Core\Plugin\ContextAwarePluginInterface;
use Drupal\Core\Session\AccountInterface;
use Drupal\Core\StringTranslation\TranslatableMarkup;
use Drupal\field\FieldConfigInterface;
use Drupal\layout_builder\Plugin\Derivative\FieldBlockDeriver;
use Psr\Log\LoggerInterface;
use Symfony\Component\DependencyInjection\ContainerInterface;
use Drupal\field\FieldLabelOptionsTrait;
/**
* Provides a block that renders a field from an entity.
*
* @internal
* Plugin classes are internal.
*/
#[Block(
id: "field_block",
deriver: FieldBlockDeriver::class
)]
class FieldBlock extends BlockBase implements ContextAwarePluginInterface, ContainerFactoryPluginInterface {
use FieldLabelOptionsTrait;
/**
* The entity field manager.
*
* @var \Drupal\Core\Entity\EntityFieldManagerInterface
*/
protected $entityFieldManager;
/**
* The formatter manager.
*
* @var \Drupal\Core\Field\FormatterPluginManager
*/
protected $formatterManager;
/**
* The entity type ID.
*
* @var string
*/
protected $entityTypeId;
/**
* The bundle ID.
*
* @var string
*/
protected $bundle;
/**
* The field name.
*
* @var string
*/
protected $fieldName;
/**
* The field definition.
*
* @var \Drupal\Core\Field\FieldDefinitionInterface
*/
protected $fieldDefinition;
/**
* The module handler.
*
* @var \Drupal\Core\Extension\ModuleHandlerInterface
*/
protected $moduleHandler;
/**
* The logger.
*
* @var \Psr\Log\LoggerInterface
*/
protected $logger;
/**
* Constructs a new FieldBlock.
*
* @param array $configuration
* A configuration array containing information about the plugin instance.
* @param string $plugin_id
* The plugin ID for the plugin instance.
* @param mixed $plugin_definition
* The plugin implementation definition.
* @param \Drupal\Core\Entity\EntityFieldManagerInterface $entity_field_manager
* The entity field manager.
* @param \Drupal\Core\Field\FormatterPluginManager $formatter_manager
* The formatter manager.
* @param \Drupal\Core\Extension\ModuleHandlerInterface $module_handler
* The module handler.
* @param \Psr\Log\LoggerInterface $logger
* The logger.
*/
public function __construct(array $configuration, $plugin_id, $plugin_definition, EntityFieldManagerInterface $entity_field_manager, FormatterPluginManager $formatter_manager, ModuleHandlerInterface $module_handler, LoggerInterface $logger) {
$this->entityFieldManager = $entity_field_manager;
$this->formatterManager = $formatter_manager;
$this->moduleHandler = $module_handler;
$this->logger = $logger;
// Get the entity type and field name from the plugin ID.
[, $entity_type_id, $bundle, $field_name] = explode(static::DERIVATIVE_SEPARATOR, $plugin_id, 4);
$this->entityTypeId = $entity_type_id;
$this->bundle = $bundle;
$this->fieldName = $field_name;
parent::__construct($configuration, $plugin_id, $plugin_definition);
}
/**
* {@inheritdoc}
*/
public static function create(ContainerInterface $container, array $configuration, $plugin_id, $plugin_definition) {
return new static(
$configuration,
$plugin_id,
$plugin_definition,
$container->get('entity_field.manager'),
$container->get('plugin.manager.field.formatter'),
$container->get('module_handler'),
$container->get('logger.channel.layout_builder')
);
}
/**
* Gets the entity that has the field.
*
* @return \Drupal\Core\Entity\FieldableEntityInterface
* The entity.
*/
protected function getEntity() {
return $this->getContextValue('entity');
}
/**
* {@inheritdoc}
*/
public function build() {
$display_settings = $this->getConfiguration()['formatter'];
$display_settings['third_party_settings']['layout_builder']['view_mode'] = $this->getContextValue('view_mode');
$entity = $this->getEntity();
try {
$build = [];
$view = $entity->get($this->fieldName)->view($display_settings);
if ($view) {
$build = [$view];
}
}
// @todo Remove in https://www.drupal.org/project/drupal/issues/2367555.
catch (EnforcedResponseException $e) {
throw $e;
}
catch (\Exception $e) {
$build = [];
$this->logger->warning('The field "%field" failed to render with the error of "%error".', ['%field' => $this->fieldName, '%error' => $e->getMessage()]);
}
CacheableMetadata::createFromRenderArray($build)->addCacheableDependency($this)->applyTo($build);
return $build;
}
/**
* {@inheritdoc}
*/
public function getPreviewFallbackString() {
return new TranslatableMarkup('"@field" field', ['@field' => $this->getFieldDefinition()->getLabel()]);
}
/**
* {@inheritdoc}
*/
protected function blockAccess(AccountInterface $account) {
$entity = $this->getEntity();
// First consult the entity.
$access = $entity->access('view', $account, TRUE);
if (!$access->isAllowed()) {
return $access;
}
// Check that the entity in question has this field.
if (!$entity instanceof FieldableEntityInterface || !$entity->hasField($this->fieldName)) {
return $access->andIf(AccessResult::forbidden());
}
// Check field access.
$field = $entity->get($this->fieldName);
$access = $access->andIf($field->access('view', $account, TRUE));
if (!$access->isAllowed()) {
return $access;
}
// Check to see if the field has any values or a default value.
if ($field->isEmpty() && !$this->entityFieldHasDefaultValue()) {
return $access->andIf(AccessResult::forbidden());
}
return $access;
}
/**
* {@inheritdoc}
*/
public function defaultConfiguration() {
return [
'label_display' => FALSE,
'formatter' => [
'label' => 'above',
'type' => $this->pluginDefinition['default_formatter'],
'settings' => [],
'third_party_settings' => [],
],
];
}
/**
* {@inheritdoc}
*/
public function blockForm($form, FormStateInterface $form_state) {
$config = $this->getConfiguration();
$form['formatter'] = [
'#tree' => TRUE,
'#process' => [
[$this, 'formatterSettingsProcessCallback'],
],
];
$form['formatter']['label'] = [
'#type' => 'select',
'#title' => $this->t('Label'),
'#options' => $this->getFieldLabelOptions(),
'#default_value' => $config['formatter']['label'],
];
$form['formatter']['type'] = [
'#type' => 'select',
'#title' => $this->t('Formatter'),
'#options' => $this->getApplicablePluginOptions($this->getFieldDefinition()),
'#required' => TRUE,
'#default_value' => $config['formatter']['type'],
'#ajax' => [
'callback' => [static::class, 'formatterSettingsAjaxCallback'],
'wrapper' => 'formatter-settings-wrapper',
],
];
// Add the formatter settings to the form via AJAX.
$form['formatter']['settings_wrapper'] = [
'#prefix' => '<div id="formatter-settings-wrapper">',
'#suffix' => '</div>',
];
return $form;
}
/**
* Render API callback: builds the formatter settings elements.
*/
public function formatterSettingsProcessCallback(array &$element, FormStateInterface $form_state, array &$complete_form) {
if ($formatter = $this->getFormatter($element['#parents'], $form_state)) {
$element['settings_wrapper']['settings'] = $formatter->settingsForm($complete_form, $form_state);
$element['settings_wrapper']['settings']['#parents'] = array_merge($element['#parents'], ['settings']);
$element['settings_wrapper']['third_party_settings'] = $this->thirdPartySettingsForm($formatter, $this->getFieldDefinition(), $complete_form, $form_state);
$element['settings_wrapper']['third_party_settings']['#parents'] = array_merge($element['#parents'], ['third_party_settings']);
FormHelper::rewriteStatesSelector($element['settings_wrapper'], "fields[$this->fieldName][settings_edit_form]", 'settings[formatter]');
// Store the array parents for our element so that we can retrieve the
// formatter settings in our AJAX callback.
$form_state->set('field_block_array_parents', $element['#array_parents']);
}
return $element;
}
/**
* Adds the formatter third party settings forms.
*
* @param \Drupal\Core\Field\FormatterInterface $plugin
* The formatter.
* @param \Drupal\Core\Field\FieldDefinitionInterface $field_definition
* The field definition.
* @param array $form
* The (entire) configuration form array.
* @param \Drupal\Core\Form\FormStateInterface $form_state
* The form state.
*
* @return array
* The formatter third party settings form.
*/
protected function thirdPartySettingsForm(FormatterInterface $plugin, FieldDefinitionInterface $field_definition, array $form, FormStateInterface $form_state) {
$settings_form = [];
// Invoke hook_field_formatter_third_party_settings_form(), keying resulting
// subforms by module name.
$this->moduleHandler->invokeAllWith(
'field_formatter_third_party_settings_form',
function (callable $hook, string $module) use (&$settings_form, $plugin, $field_definition, $form, $form_state) {
$settings_form[$module] = $hook(
$plugin,
$field_definition,
EntityDisplayBase::CUSTOM_MODE,
$form,
$form_state,
);
}
);
return $settings_form;
}
/**
* Render API callback: gets the layout settings elements.
*/
public static function formatterSettingsAjaxCallback(array $form, FormStateInterface $form_state) {
$formatter_array_parents = $form_state->get('field_block_array_parents');
return NestedArray::getValue($form, array_merge($formatter_array_parents, ['settings_wrapper']));
}
/**
* {@inheritdoc}
*/
public function blockSubmit($form, FormStateInterface $form_state) {
$this->configuration['formatter'] = $form_state->getValue('formatter');
}
/**
* Gets the field definition.
*
* @return \Drupal\Core\Field\FieldDefinitionInterface
* The field definition.
*/
protected function getFieldDefinition() {
if (empty($this->fieldDefinition)) {
$field_definitions = $this->entityFieldManager->getFieldDefinitions($this->entityTypeId, $this->bundle);
$this->fieldDefinition = $field_definitions[$this->fieldName];
}
return $this->fieldDefinition;
}
/**
* Returns an array of applicable formatter options for a field.
*
* @param \Drupal\Core\Field\FieldDefinitionInterface $field_definition
* The field definition.
*
* @return array
* An array of applicable formatter options.
*
* @see \Drupal\field_ui\Form\EntityDisplayFormBase::getApplicablePluginOptions()
*/
protected function getApplicablePluginOptions(FieldDefinitionInterface $field_definition) {
$options = $this->formatterManager->getOptions($field_definition->getType());
$applicable_options = [];
foreach ($options as $option => $label) {
$plugin_class = DefaultFactory::getPluginClass($option, $this->formatterManager->getDefinition($option));
if ($plugin_class::isApplicable($field_definition)) {
$applicable_options[$option] = $label;
}
}
return $applicable_options;
}
/**
* Gets the formatter object.
*
* @param array $parents
* The #parents of the element representing the formatter.
* @param \Drupal\Core\Form\FormStateInterface $form_state
* The current state of the form.
*
* @return \Drupal\Core\Field\FormatterInterface
* The formatter object.
*/
protected function getFormatter(array $parents, FormStateInterface $form_state) {
// Use the processed values, if available.
$configuration = NestedArray::getValue($form_state->getValues(), $parents);
if (!$configuration) {
// Next check the raw user input.
$configuration = NestedArray::getValue($form_state->getUserInput(), $parents);
if (!$configuration) {
// If no user input exists, use the default values.
$configuration = $this->getConfiguration()['formatter'];
}
}
return $this->formatterManager->getInstance([
'configuration' => $configuration,
'field_definition' => $this->getFieldDefinition(),
'view_mode' => EntityDisplayBase::CUSTOM_MODE,
'prepare' => TRUE,
]);
}
/**
* Checks whether there is a default value set on the field.
*
* @return bool
* TRUE if default value set, FALSE otherwise.
*/
protected function entityFieldHasDefaultValue(): bool {
$entity = $this->getEntity();
$field = $entity->get($this->fieldName);
$definition = $field->getFieldDefinition();
if ($definition->getDefaultValue($entity)) {
return TRUE;
}
// @todo Remove special handling of image fields after
// https://www.drupal.org/project/drupal/issues/3005528.
if ($definition->getType() !== 'image') {
return FALSE;
}
$default_image = $definition->getSetting('default_image');
// If we are dealing with a configurable field, look in both instance-level
// and field-level settings.
if (empty($default_image['uuid']) && ($definition instanceof FieldConfigInterface)) {
$default_image = $definition->getFieldStorageDefinition()->getSetting('default_image');
}
return !empty($default_image['uuid']);
}
}

View File

@@ -0,0 +1,299 @@
<?php
namespace Drupal\layout_builder\Plugin\Block;
use Drupal\block_content\Access\RefinableDependentAccessInterface;
use Drupal\block_content\Access\RefinableDependentAccessTrait;
use Drupal\Component\Utility\NestedArray;
use Drupal\Core\Access\AccessResult;
use Drupal\Core\Block\BlockBase;
use Drupal\Core\Entity\Entity\EntityFormDisplay;
use Drupal\Core\Entity\EntityDisplayRepositoryInterface;
use Drupal\Core\Entity\EntityTypeManagerInterface;
use Drupal\Core\Form\FormStateInterface;
use Drupal\Core\Form\SubformStateInterface;
use Drupal\Core\Plugin\ContainerFactoryPluginInterface;
use Drupal\Core\Session\AccountInterface;
use Symfony\Component\DependencyInjection\ContainerInterface;
/**
* Defines an inline block plugin type.
*
* @Block(
* id = "inline_block",
* admin_label = @Translation("Inline block"),
* category = @Translation("Inline blocks"),
* deriver = "Drupal\layout_builder\Plugin\Derivative\InlineBlockDeriver",
* )
*
* @internal
* Plugin classes are internal.
*/
class InlineBlock extends BlockBase implements ContainerFactoryPluginInterface, RefinableDependentAccessInterface {
use RefinableDependentAccessTrait;
/**
* The entity type manager.
*
* @var \Drupal\Core\Entity\EntityTypeManagerInterface
*/
protected $entityTypeManager;
/**
* The block content entity.
*
* @var \Drupal\block_content\BlockContentInterface
*/
protected $blockContent;
/**
* The entity display repository.
*
* @var \Drupal\Core\Entity\EntityDisplayRepositoryInterface
*/
protected $entityDisplayRepository;
/**
* Whether a new block is being created.
*
* @var bool
*/
protected $isNew = TRUE;
/**
* The current user.
*
* @var \Drupal\Core\Session\AccountInterface
*/
protected $currentUser;
/**
* Constructs a new InlineBlock.
*
* @param array $configuration
* A configuration array containing information about the plugin instance.
* @param string $plugin_id
* The plugin ID for the plugin instance.
* @param mixed $plugin_definition
* The plugin implementation definition.
* @param \Drupal\Core\Entity\EntityTypeManagerInterface $entity_type_manager
* The entity type manager service.
* @param \Drupal\Core\Entity\EntityDisplayRepositoryInterface $entity_display_repository
* The entity display repository.
* @param \Drupal\Core\Session\AccountInterface $current_user
* The current user.
*/
public function __construct(array $configuration, $plugin_id, $plugin_definition, EntityTypeManagerInterface $entity_type_manager, EntityDisplayRepositoryInterface $entity_display_repository, AccountInterface $current_user) {
parent::__construct($configuration, $plugin_id, $plugin_definition);
$this->entityTypeManager = $entity_type_manager;
$this->entityDisplayRepository = $entity_display_repository;
$this->currentUser = $current_user;
if (!empty($this->configuration['block_revision_id']) || !empty($this->configuration['block_serialized'])) {
$this->isNew = FALSE;
}
}
/**
* {@inheritdoc}
*/
public static function create(ContainerInterface $container, array $configuration, $plugin_id, $plugin_definition) {
return new static(
$configuration,
$plugin_id,
$plugin_definition,
$container->get('entity_type.manager'),
$container->get('entity_display.repository'),
$container->get('current_user')
);
}
/**
* {@inheritdoc}
*/
public function defaultConfiguration() {
return [
'view_mode' => 'full',
'block_id' => NULL,
'block_revision_id' => NULL,
'block_serialized' => NULL,
];
}
/**
* {@inheritdoc}
*/
public function blockForm($form, FormStateInterface $form_state) {
$block = $this->getEntity();
// Add the entity form display in a process callback so that #parents can
// be successfully propagated to field widgets.
$form['block_form'] = [
'#type' => 'container',
'#process' => [[static::class, 'processBlockForm']],
'#block' => $block,
'#access' => $this->currentUser->hasPermission('create and edit custom blocks'),
];
$options = $this->entityDisplayRepository->getViewModeOptionsByBundle('block_content', $block->bundle());
$form['view_mode'] = [
'#type' => 'select',
'#options' => $options,
'#title' => $this->t('View mode'),
'#description' => $this->t('The view mode in which to render the block.'),
'#default_value' => $this->configuration['view_mode'],
'#access' => count($options) > 1,
];
return $form;
}
/**
* Process callback to insert a Content Block form.
*
* @param array $element
* The containing element.
* @param \Drupal\Core\Form\FormStateInterface $form_state
* The form state.
*
* @return array
* The containing element, with the Content Block form inserted.
*/
public static function processBlockForm(array $element, FormStateInterface $form_state) {
/** @var \Drupal\block_content\BlockContentInterface $block */
$block = $element['#block'];
EntityFormDisplay::collectRenderDisplay($block, 'edit')->buildForm($block, $element, $form_state);
$element['revision_log']['#access'] = FALSE;
$element['info']['#access'] = FALSE;
return $element;
}
/**
* {@inheritdoc}
*/
public function blockValidate($form, FormStateInterface $form_state) {
$block_form = $form['block_form'];
/** @var \Drupal\block_content\BlockContentInterface $block */
$block = $block_form['#block'];
$form_display = EntityFormDisplay::collectRenderDisplay($block, 'edit');
$complete_form_state = $form_state instanceof SubformStateInterface ? $form_state->getCompleteFormState() : $form_state;
$form_display->extractFormValues($block, $block_form, $complete_form_state);
$form_display->validateFormValues($block, $block_form, $complete_form_state);
// @todo Remove when https://www.drupal.org/project/drupal/issues/2948549 is closed.
$form_state->setTemporaryValue('block_form_parents', $block_form['#parents']);
}
/**
* {@inheritdoc}
*/
public function blockSubmit($form, FormStateInterface $form_state) {
$this->configuration['view_mode'] = $form_state->getValue('view_mode');
// @todo Remove when https://www.drupal.org/project/drupal/issues/2948549 is closed.
$block_form = NestedArray::getValue($form, $form_state->getTemporaryValue('block_form_parents'));
/** @var \Drupal\block_content\BlockContentInterface $block */
$block = $block_form['#block'];
$form_display = EntityFormDisplay::collectRenderDisplay($block, 'edit');
$complete_form_state = $form_state instanceof SubformStateInterface ? $form_state->getCompleteFormState() : $form_state;
$form_display->extractFormValues($block, $block_form, $complete_form_state);
$block->setInfo($this->configuration['label']);
$this->configuration['block_serialized'] = serialize($block);
}
/**
* {@inheritdoc}
*/
protected function blockAccess(AccountInterface $account) {
if ($entity = $this->getEntity()) {
return $entity->access('view', $account, TRUE);
}
return AccessResult::forbidden();
}
/**
* {@inheritdoc}
*/
public function build() {
$block = $this->getEntity();
return $this->entityTypeManager->getViewBuilder($block->getEntityTypeId())->view($block, $this->configuration['view_mode']);
}
/**
* Loads or creates the block content entity of the block.
*
* @return \Drupal\block_content\BlockContentInterface
* The block content entity.
*/
protected function getEntity() {
if (!isset($this->blockContent)) {
if (!empty($this->configuration['block_serialized'])) {
$this->blockContent = unserialize($this->configuration['block_serialized']);
}
elseif (!empty($this->configuration['block_revision_id'])) {
$entity = $this->entityTypeManager->getStorage('block_content')->loadRevision($this->configuration['block_revision_id']);
$this->blockContent = $entity;
}
else {
$this->blockContent = $this->entityTypeManager->getStorage('block_content')->create([
'type' => $this->getDerivativeId(),
'reusable' => FALSE,
]);
}
if ($this->blockContent instanceof RefinableDependentAccessInterface && $dependee = $this->getAccessDependency()) {
$this->blockContent->setAccessDependency($dependee);
}
}
return $this->blockContent;
}
/**
* {@inheritdoc}
*/
public function buildConfigurationForm(array $form, FormStateInterface $form_state) {
$form = parent::buildConfigurationForm($form, $form_state);
if ($this->isNew) {
// If the Content Block is new then don't provide a default label.
unset($form['label']['#default_value']);
}
$form['label']['#description'] = $this->t('The title of the block as shown to the user.');
return $form;
}
/**
* Saves the block_content entity for this plugin.
*
* @param bool $new_revision
* Whether to create new revision, if the block was modified.
* @param bool $duplicate_block
* Whether to duplicate the "block_content" entity.
*/
public function saveBlockContent($new_revision = FALSE, $duplicate_block = FALSE) {
/** @var \Drupal\block_content\BlockContentInterface $block */
$block = NULL;
if (!empty($this->configuration['block_serialized'])) {
$block = unserialize($this->configuration['block_serialized']);
}
if ($duplicate_block) {
if (empty($block) && !empty($this->configuration['block_revision_id'])) {
$block = $this->entityTypeManager->getStorage('block_content')->loadRevision($this->configuration['block_revision_id']);
}
if ($block) {
$block = $block->createDuplicate();
}
}
if ($block) {
// Since the content block is only set if it was unserialized, the flag
// will only effect blocks which were modified or serialized originally.
if ($new_revision) {
$block->setNewRevision();
}
$block->save();
$this->configuration['block_id'] = $block->id();
$this->configuration['block_revision_id'] = $block->getRevisionId();
$this->configuration['block_serialized'] = NULL;
}
}
}

View File

@@ -0,0 +1,40 @@
<?php
namespace Drupal\layout_builder\Plugin\DataType;
use Drupal\Core\StringTranslation\TranslatableMarkup;
use Drupal\Core\TypedData\Attribute\DataType;
use Drupal\Core\TypedData\TypedData;
use Drupal\layout_builder\Section;
/**
* Provides a data type wrapping \Drupal\layout_builder\Section.
*
* @internal
* Plugin classes are internal.
*/
#[DataType(
id: "layout_section",
label: new TranslatableMarkup("Layout Section"),
description: new TranslatableMarkup("A layout section"),
)]
class SectionData extends TypedData {
/**
* The section object.
*
* @var \Drupal\layout_builder\Section
*/
protected $value;
/**
* {@inheritdoc}
*/
public function setValue($value, $notify = TRUE) {
if ($value && !$value instanceof Section) {
throw new \InvalidArgumentException(sprintf('Value assigned to "%s" is not a valid section', $this->getName()));
}
parent::setValue($value, $notify);
}
}

View File

@@ -0,0 +1,167 @@
<?php
namespace Drupal\layout_builder\Plugin\Derivative;
use Drupal\Component\Plugin\Derivative\DeriverBase;
use Drupal\Component\Plugin\PluginBase;
use Drupal\Core\Entity\EntityFieldManagerInterface;
use Drupal\Core\Entity\EntityTypeBundleInfoInterface;
use Drupal\Core\Entity\EntityTypeManagerInterface;
use Drupal\Core\Entity\FieldableEntityInterface;
use Drupal\Core\Extension\ModuleHandlerInterface;
use Drupal\Core\Plugin\Context\EntityContextDefinition;
use Drupal\Core\Plugin\Discovery\ContainerDeriverInterface;
use Drupal\Core\StringTranslation\StringTranslationTrait;
use Symfony\Component\DependencyInjection\ContainerInterface;
use Drupal\Core\Entity\EntityTypeRepositoryInterface;
/**
* Provides entity field block definitions for every field.
*
* @internal
* Plugin derivers are internal.
*/
class ExtraFieldBlockDeriver extends DeriverBase implements ContainerDeriverInterface {
use StringTranslationTrait;
/**
* The entity field manager.
*
* @var \Drupal\Core\Entity\EntityFieldManagerInterface
*/
protected $entityFieldManager;
/**
* The entity type manager.
*
* @var \Drupal\Core\Entity\EntityTypeManagerInterface
*/
protected $entityTypeManager;
/**
* The entity type bundle info.
*
* @var \Drupal\Core\Entity\EntityTypeBundleInfoInterface
*/
protected $entityTypeBundleInfo;
/**
* The entity type repository.
*
* @var \Drupal\Core\Entity\EntityTypeRepositoryInterface
*/
protected $entityTypeRepository;
/**
* Constructs new FieldBlockDeriver.
*
* @param \Drupal\Core\Entity\EntityFieldManagerInterface $entity_field_manager
* The entity field manager.
* @param \Drupal\Core\Entity\EntityTypeManagerInterface $entity_type_manager
* The entity type manager.
* @param \Drupal\Core\Entity\EntityTypeBundleInfoInterface $entity_type_bundle_info
* The entity type bundle info.
* @param \Drupal\Core\Entity\EntityTypeRepositoryInterface $entity_type_repository
* The entity type repository.
* @param \Drupal\Core\Extension\ModuleHandlerInterface $moduleHandler
* The module handler.
*/
public function __construct(
EntityFieldManagerInterface $entity_field_manager,
EntityTypeManagerInterface $entity_type_manager,
EntityTypeBundleInfoInterface $entity_type_bundle_info,
EntityTypeRepositoryInterface $entity_type_repository,
protected ModuleHandlerInterface $moduleHandler,
) {
$this->entityFieldManager = $entity_field_manager;
$this->entityTypeManager = $entity_type_manager;
$this->entityTypeBundleInfo = $entity_type_bundle_info;
$this->entityTypeRepository = $entity_type_repository;
}
/**
* {@inheritdoc}
*/
public static function create(ContainerInterface $container, $base_plugin_id) {
return new static(
$container->get('entity_field.manager'),
$container->get('entity_type.manager'),
$container->get('entity_type.bundle.info'),
$container->get('entity_type.repository'),
$container->get('module_handler')
);
}
/**
* {@inheritdoc}
*/
public function getDerivativeDefinitions($base_plugin_definition) {
$entity_type_labels = $this->entityTypeRepository->getEntityTypeLabels();
$enabled_bundle_ids = $this->bundleIdsWithLayoutBuilderDisplays();
$expose_all_fields = $this->moduleHandler->moduleExists('layout_builder_expose_all_field_blocks');
foreach ($this->entityTypeManager->getDefinitions() as $entity_type_id => $entity_type) {
// Only process fieldable entity types.
if (!$entity_type->entityClassImplements(FieldableEntityInterface::class)) {
continue;
}
// If not loading everything, skip entity types that aren't included.
if (!$expose_all_fields && !isset($enabled_bundle_ids[$entity_type_id])) {
continue;
}
$bundles = $this->entityTypeBundleInfo->getBundleInfo($entity_type_id);
foreach ($bundles as $bundle_id => $bundle) {
// If not loading everything, skip bundle types that aren't included.
if (!$expose_all_fields && !isset($enabled_bundle_ids[$entity_type_id][$bundle_id])) {
continue;
}
$extra_fields = $this->entityFieldManager->getExtraFields($entity_type_id, $bundle_id);
// Skip bundles without any extra fields.
if (empty($extra_fields['display'])) {
continue;
}
foreach ($extra_fields['display'] as $extra_field_id => $extra_field) {
$derivative = $base_plugin_definition;
$derivative['category'] = $this->t('@entity fields', ['@entity' => $entity_type_labels[$entity_type_id]]);
$derivative['admin_label'] = $extra_field['label'];
$context_definition = EntityContextDefinition::fromEntityType($entity_type)
->addConstraint('Bundle', [$bundle_id]);
$derivative['context_definitions'] = [
'entity' => $context_definition,
];
$derivative_id = $entity_type_id . PluginBase::DERIVATIVE_SEPARATOR . $bundle_id . PluginBase::DERIVATIVE_SEPARATOR . $extra_field_id;
$this->derivatives[$derivative_id] = $derivative;
}
}
}
return $this->derivatives;
}
/**
* Gets a list of entity type and bundle tuples that have layout builder enabled.
*
* @return array
* A structured array with entity type as first key, bundle as second.
*/
protected function bundleIdsWithLayoutBuilderDisplays(): array {
/** @var \Drupal\layout_builder\Entity\LayoutEntityDisplayInterface[] $displays */
$displays = $this->entityTypeManager->getStorage('entity_view_display')->loadByProperties([
'third_party_settings.layout_builder.enabled' => TRUE,
]);
$layout_bundles = [];
foreach ($displays as $display) {
$bundle = $display->getTargetBundle();
$layout_bundles[$display->getTargetEntityTypeId()][$bundle] = $bundle;
}
return $layout_bundles;
}
}

View File

@@ -0,0 +1,206 @@
<?php
namespace Drupal\layout_builder\Plugin\Derivative;
use Drupal\Component\Plugin\Derivative\DeriverBase;
use Drupal\Component\Plugin\PluginBase;
use Drupal\Core\Config\Entity\ConfigEntityStorageInterface;
use Drupal\Core\Entity\EntityFieldManagerInterface;
use Drupal\Core\Entity\EntityTypeRepositoryInterface;
use Drupal\Core\Extension\ModuleHandlerInterface;
use Drupal\Core\Field\FieldConfigInterface;
use Drupal\Core\Field\FieldTypePluginManagerInterface;
use Drupal\Core\Field\FormatterPluginManager;
use Drupal\Core\Logger\LoggerChannelTrait;
use Drupal\Core\Plugin\Context\ContextDefinition;
use Drupal\Core\Plugin\Context\EntityContextDefinition;
use Drupal\Core\Plugin\Discovery\ContainerDeriverInterface;
use Drupal\Core\StringTranslation\StringTranslationTrait;
use Symfony\Component\DependencyInjection\ContainerInterface;
/**
* Provides entity field block definitions for every field.
*
* @internal
* Plugin derivers are internal.
*/
class FieldBlockDeriver extends DeriverBase implements ContainerDeriverInterface {
use StringTranslationTrait;
use LoggerChannelTrait;
/**
* The entity type repository.
*
* @var \Drupal\Core\Entity\EntityTypeRepositoryInterface
*/
protected $entityTypeRepository;
/**
* The entity field manager.
*
* @var \Drupal\Core\Entity\EntityFieldManagerInterface
*/
protected $entityFieldManager;
/**
* The field type manager.
*
* @var \Drupal\Core\Field\FieldTypePluginManagerInterface
*/
protected $fieldTypeManager;
/**
* The formatter manager.
*
* @var \Drupal\Core\Field\FormatterPluginManager
*/
protected $formatterManager;
/**
* Constructs new FieldBlockDeriver.
*
* @param \Drupal\Core\Entity\EntityTypeRepositoryInterface $entity_type_repository
* The entity type repository.
* @param \Drupal\Core\Entity\EntityFieldManagerInterface $entity_field_manager
* The entity field manager.
* @param \Drupal\Core\Field\FieldTypePluginManagerInterface $field_type_manager
* The field type manager.
* @param \Drupal\Core\Field\FormatterPluginManager $formatter_manager
* The formatter manager.
* @param \Drupal\Core\Config\Entity\ConfigEntityStorageInterface $entityViewDisplayStorage
* The entity view display storage.
* @param \Drupal\Core\Extension\ModuleHandlerInterface $moduleHandler
* The module handler.
*/
public function __construct(
EntityTypeRepositoryInterface $entity_type_repository,
EntityFieldManagerInterface $entity_field_manager,
FieldTypePluginManagerInterface $field_type_manager,
FormatterPluginManager $formatter_manager,
protected ConfigEntityStorageInterface $entityViewDisplayStorage,
protected ModuleHandlerInterface $moduleHandler,
) {
$this->entityTypeRepository = $entity_type_repository;
$this->entityFieldManager = $entity_field_manager;
$this->fieldTypeManager = $field_type_manager;
$this->formatterManager = $formatter_manager;
}
/**
* {@inheritdoc}
*/
public static function create(ContainerInterface $container, $base_plugin_id) {
return new static(
$container->get('entity_type.repository'),
$container->get('entity_field.manager'),
$container->get('plugin.manager.field.field_type'),
$container->get('plugin.manager.field.formatter'),
$container->get('entity_type.manager')->getStorage('entity_view_display'),
$container->get('module_handler')
);
}
/**
* {@inheritdoc}
*/
public function getDerivativeDefinitions($base_plugin_definition) {
$entity_type_labels = $this->entityTypeRepository->getEntityTypeLabels();
foreach ($this->getFieldMap() as $entity_type_id => $entity_field_map) {
foreach ($entity_field_map as $field_name => $field_info) {
// Skip fields without any formatters.
$options = $this->formatterManager->getOptions($field_info['type']);
if (empty($options)) {
continue;
}
foreach ($field_info['bundles'] as $bundle) {
$derivative = $base_plugin_definition;
$field_definitions = $this->entityFieldManager->getFieldDefinitions($entity_type_id, $bundle);
if (empty($field_definitions[$field_name])) {
$this->getLogger('field')->error('Field %field_name exists but is missing a corresponding field definition and may be misconfigured.', ['%field_name' => "$entity_type_id.$bundle.$field_name"]);
continue;
}
$field_definition = $field_definitions[$field_name];
// Store the default formatter on the definition.
$derivative['default_formatter'] = '';
$field_type_definition = $this->fieldTypeManager->getDefinition($field_info['type']);
if (isset($field_type_definition['default_formatter'])) {
$derivative['default_formatter'] = $field_type_definition['default_formatter'];
}
$derivative['category'] = $this->t('@entity fields', ['@entity' => $entity_type_labels[$entity_type_id]]);
$derivative['admin_label'] = $field_definition->getLabel();
// Add a dependency on the field if it is configurable.
if ($field_definition instanceof FieldConfigInterface) {
$derivative['config_dependencies'][$field_definition->getConfigDependencyKey()][] = $field_definition->getConfigDependencyName();
}
// For any field that is not display configurable, mark it as
// unavailable to place in the block UI.
$derivative['_block_ui_hidden'] = !$field_definition->isDisplayConfigurable('view');
$context_definition = EntityContextDefinition::fromEntityTypeId($entity_type_id)->setLabel($entity_type_labels[$entity_type_id]);
$context_definition->addConstraint('Bundle', [$bundle]);
$derivative['context_definitions'] = [
'entity' => $context_definition,
'view_mode' => new ContextDefinition('string'),
];
$derivative_id = $entity_type_id . PluginBase::DERIVATIVE_SEPARATOR . $bundle . PluginBase::DERIVATIVE_SEPARATOR . $field_name;
$this->derivatives[$derivative_id] = $derivative;
}
}
}
return $this->derivatives;
}
/**
* Returns the entity field map for deriving block definitions.
*
* @return array
* The entity field map.
*
* @see \Drupal\Core\Entity\EntityFieldManagerInterface::getFieldMap()
*/
protected function getFieldMap(): array {
$field_map = $this->entityFieldManager->getFieldMap();
// If all fields are exposed as field blocks, just return the field map
// without any further processing.
if ($this->moduleHandler->moduleExists('layout_builder_expose_all_field_blocks')) {
return $field_map;
}
// Load all entity view displays which are using Layout Builder.
/** @var \Drupal\layout_builder\Entity\LayoutEntityDisplayInterface[] $displays */
$displays = $this->entityViewDisplayStorage->loadByProperties([
'third_party_settings.layout_builder.enabled' => TRUE,
]);
$layout_bundles = [];
foreach ($displays as $display) {
$bundle = $display->getTargetBundle();
$layout_bundles[$display->getTargetEntityTypeId()][$bundle] = $bundle;
}
// Process $field_map, removing any entity types which are not using Layout
// Builder.
$field_map = array_intersect_key($field_map, $layout_bundles);
foreach ($field_map as $entity_type_id => $fields) {
foreach ($fields as $field_name => $field_info) {
$field_map[$entity_type_id][$field_name]['bundles'] = array_intersect($field_info['bundles'], $layout_bundles[$entity_type_id]);
// If no bundles are using Layout Builder, remove this field from the
// field map.
if (empty($field_info['bundles'])) {
unset($field_map[$entity_type_id][$field_name]);
}
}
}
return $field_map;
}
}

View File

@@ -0,0 +1,60 @@
<?php
namespace Drupal\layout_builder\Plugin\Derivative;
use Drupal\Component\Plugin\Derivative\DeriverBase;
use Drupal\Core\Entity\EntityTypeManagerInterface;
use Drupal\Core\Plugin\Discovery\ContainerDeriverInterface;
use Symfony\Component\DependencyInjection\ContainerInterface;
/**
* Provides inline block plugin definitions for all block types.
*
* @internal
* Plugin derivers are internal.
*/
class InlineBlockDeriver extends DeriverBase implements ContainerDeriverInterface {
/**
* The entity type manager.
*
* @var \Drupal\Core\Entity\EntityTypeManagerInterface
*/
protected $entityTypeManager;
/**
* Constructs a BlockContentDeriver object.
*
* @param \Drupal\Core\Entity\EntityTypeManagerInterface $entity_type_manager
* The entity type manager.
*/
public function __construct(EntityTypeManagerInterface $entity_type_manager) {
$this->entityTypeManager = $entity_type_manager;
}
/**
* {@inheritdoc}
*/
public static function create(ContainerInterface $container, $base_plugin_id) {
return new static(
$container->get('entity_type.manager')
);
}
/**
* {@inheritdoc}
*/
public function getDerivativeDefinitions($base_plugin_definition) {
$this->derivatives = [];
if ($this->entityTypeManager->hasDefinition('block_content_type')) {
$block_content_types = $this->entityTypeManager->getStorage('block_content_type')->loadMultiple();
foreach ($block_content_types as $id => $type) {
$this->derivatives[$id] = $base_plugin_definition;
$this->derivatives[$id]['admin_label'] = $type->label();
$this->derivatives[$id]['config_dependencies'][$type->getConfigDependencyKey()][] = $type->getConfigDependencyName();
}
}
return parent::getDerivativeDefinitions($base_plugin_definition);
}
}

View File

@@ -0,0 +1,75 @@
<?php
namespace Drupal\layout_builder\Plugin\Derivative;
use Drupal\Component\Plugin\Derivative\DeriverBase;
use Drupal\Core\Entity\EntityTypeManagerInterface;
use Drupal\Core\Plugin\Discovery\ContainerDeriverInterface;
use Drupal\Core\StringTranslation\StringTranslationTrait;
use Drupal\layout_builder\Plugin\SectionStorage\SectionStorageLocalTaskProviderInterface;
use Drupal\layout_builder\SectionStorage\SectionStorageManagerInterface;
use Symfony\Component\DependencyInjection\ContainerInterface;
/**
* Provides local task definitions for the layout builder user interface.
*
* @todo Remove this in https://www.drupal.org/project/drupal/issues/2936655.
*
* @internal
* Plugin derivers are internal.
*/
class LayoutBuilderLocalTaskDeriver extends DeriverBase implements ContainerDeriverInterface {
use StringTranslationTrait;
/**
* The entity type manager.
*
* @var \Drupal\Core\Entity\EntityTypeManagerInterface
*/
protected $entityTypeManager;
/**
* The section storage manager.
*
* @var \Drupal\layout_builder\SectionStorage\SectionStorageManagerInterface
*/
protected $sectionStorageManager;
/**
* Constructs a new LayoutBuilderLocalTaskDeriver.
*
* @param \Drupal\Core\Entity\EntityTypeManagerInterface $entity_type_manager
* The entity type manager.
* @param \Drupal\layout_builder\SectionStorage\SectionStorageManagerInterface $section_storage_manager
* The section storage manager.
*/
public function __construct(EntityTypeManagerInterface $entity_type_manager, SectionStorageManagerInterface $section_storage_manager) {
$this->entityTypeManager = $entity_type_manager;
$this->sectionStorageManager = $section_storage_manager;
}
/**
* {@inheritdoc}
*/
public static function create(ContainerInterface $container, $base_plugin_id) {
return new static(
$container->get('entity_type.manager'),
$container->get('plugin.manager.layout_builder.section_storage')
);
}
/**
* {@inheritdoc}
*/
public function getDerivativeDefinitions($base_plugin_definition) {
foreach ($this->sectionStorageManager->getDefinitions() as $plugin_id => $definition) {
$section_storage = $this->sectionStorageManager->loadEmpty($plugin_id);
if ($section_storage instanceof SectionStorageLocalTaskProviderInterface) {
$this->derivatives += $section_storage->buildLocalTasks($base_plugin_definition);
}
}
return $this->derivatives;
}
}

View File

@@ -0,0 +1,96 @@
<?php
namespace Drupal\layout_builder\Plugin\Field\FieldType;
use Drupal\Core\Field\Attribute\FieldType;
use Drupal\Core\Field\FieldDefinitionInterface;
use Drupal\Core\Field\FieldItemBase;
use Drupal\Core\Field\FieldStorageDefinitionInterface;
use Drupal\Core\StringTranslation\TranslatableMarkup;
use Drupal\Core\TypedData\DataDefinition;
use Drupal\layout_builder\Field\LayoutSectionItemList;
use Drupal\layout_builder\Section;
/**
* Plugin implementation of the 'layout_section' field type.
*
* @internal
* Plugin classes are internal.
*
* @property \Drupal\layout_builder\Section $section
*/
#[FieldType(
id: "layout_section",
label: new TranslatableMarkup("Layout Section"),
description: new TranslatableMarkup("Layout Section"),
no_ui: TRUE,
list_class: LayoutSectionItemList::class,
cardinality: FieldStorageDefinitionInterface::CARDINALITY_UNLIMITED
)]
class LayoutSectionItem extends FieldItemBase {
/**
* {@inheritdoc}
*/
public static function propertyDefinitions(FieldStorageDefinitionInterface $field_definition) {
$properties['section'] = DataDefinition::create('layout_section')
->setLabel(new TranslatableMarkup('Layout Section'))
->setRequired(FALSE);
return $properties;
}
/**
* {@inheritdoc}
*/
public function __get($name) {
// @todo \Drupal\Core\Field\FieldItemBase::__get() does not return default
// values for un-instantiated properties. This will forcibly instantiate
// all properties with the side-effect of a performance hit, resolve
// properly in https://www.drupal.org/node/2413471.
$this->getProperties();
return parent::__get($name);
}
/**
* {@inheritdoc}
*/
public static function mainPropertyName() {
return 'section';
}
/**
* {@inheritdoc}
*/
public static function schema(FieldStorageDefinitionInterface $field_definition) {
$schema = [
'columns' => [
'section' => [
'type' => 'blob',
'size' => 'normal',
'serialize' => TRUE,
],
],
];
return $schema;
}
/**
* {@inheritdoc}
*/
public static function generateSampleValue(FieldDefinitionInterface $field_definition) {
// @todo Expand this in https://www.drupal.org/node/2912331.
$values['section'] = new Section('layout_onecol');
return $values;
}
/**
* {@inheritdoc}
*/
public function isEmpty() {
return empty($this->section);
}
}

View File

@@ -0,0 +1,78 @@
<?php
namespace Drupal\layout_builder\Plugin\Field\FieldWidget;
use Drupal\Core\Field\Attribute\FieldWidget;
use Drupal\Core\Field\FieldItemListInterface;
use Drupal\Core\Field\WidgetBase;
use Drupal\Core\Form\FormStateInterface;
use Drupal\Core\StringTranslation\TranslatableMarkup;
/**
* A widget to display the layout form.
*
* @internal
* Plugin classes are internal.
*/
#[FieldWidget(
id: 'layout_builder_widget',
label: new TranslatableMarkup('Layout Builder Widget'),
description: new TranslatableMarkup('A field widget for Layout Builder.'),
field_types: ['layout_section'],
multiple_values: TRUE,
)]
class LayoutBuilderWidget extends WidgetBase {
/**
* {@inheritdoc}
*/
public function formElement(FieldItemListInterface $items, $delta, array $element, array &$form, FormStateInterface $form_state) {
$element += [
'#type' => 'layout_builder',
'#section_storage' => $this->getSectionStorage($form_state),
];
$element['#process'][] = [static::class, 'layoutBuilderElementGetKeys'];
return $element;
}
/**
* Form element #process callback.
*
* Save the layout builder element array parents as a property on the top form
* element so that they can be used to access the element within the whole
* render array later.
*
* @see \Drupal\layout_builder\Controller\LayoutBuilderHtmlEntityFormController
*/
public static function layoutBuilderElementGetKeys(array $element, FormStateInterface $form_state, &$form) {
$form['#layout_builder_element_keys'] = $element['#array_parents'];
return $element;
}
/**
* {@inheritdoc}
*/
public function extractFormValues(FieldItemListInterface $items, array $form, FormStateInterface $form_state) {
// @todo This isn't resilient to being set twice, during validation and
// save https://www.drupal.org/project/drupal/issues/2833682.
if (!$form_state->isValidationComplete()) {
return;
}
$items->setValue($this->getSectionStorage($form_state)->getSections());
}
/**
* Gets the section storage.
*
* @param \Drupal\Core\Form\FormStateInterface $form_state
* The form state.
*
* @return \Drupal\layout_builder\SectionStorageInterface
* The section storage loaded from the tempstore.
*/
private function getSectionStorage(FormStateInterface $form_state) {
return $form_state->getFormObject()->getSectionStorage();
}
}

View File

@@ -0,0 +1,33 @@
<?php
namespace Drupal\layout_builder\Plugin\Layout;
use Drupal\Core\Layout\Attribute\Layout;
use Drupal\Core\Layout\LayoutDefault;
use Drupal\Core\StringTranslation\TranslatableMarkup;
/**
* Provides a layout plugin that produces no output.
*
* @see \Drupal\layout_builder\Field\LayoutSectionItemList::removeSection()
* @see \Drupal\layout_builder\SectionListTrait::addBlankSection()
* @see \Drupal\layout_builder\SectionListTrait::hasBlankSection()
*
* @internal
* This layout plugin is intended for internal use by Layout Builder only.
*/
#[Layout(
id: 'layout_builder_blank',
label: new TranslatableMarkup('Blank'),
)]
class BlankLayout extends LayoutDefault {
/**
* {@inheritdoc}
*/
public function build(array $regions) {
// Return no output.
return [];
}
}

View File

@@ -0,0 +1,83 @@
<?php
namespace Drupal\layout_builder\Plugin\Layout;
use Drupal\Core\Form\FormStateInterface;
use Drupal\Core\Layout\LayoutDefault;
use Drupal\Core\Plugin\PluginFormInterface;
/**
* Base class of layouts with configurable widths.
*/
abstract class MultiWidthLayoutBase extends LayoutDefault implements PluginFormInterface {
/**
* {@inheritdoc}
*/
public function defaultConfiguration() {
$configuration = parent::defaultConfiguration();
return $configuration + [
'column_widths' => $this->getDefaultWidth(),
];
}
/**
* {@inheritdoc}
*/
public function buildConfigurationForm(array $form, FormStateInterface $form_state) {
$form['column_widths'] = [
'#type' => 'select',
'#title' => $this->t('Column widths'),
'#default_value' => $this->configuration['column_widths'],
'#options' => $this->getWidthOptions(),
'#description' => $this->t('Choose the column widths for this layout.'),
];
return parent::buildConfigurationForm($form, $form_state);
}
/**
* {@inheritdoc}
*/
public function submitConfigurationForm(array &$form, FormStateInterface $form_state) {
parent::submitConfigurationForm($form, $form_state);
$this->configuration['column_widths'] = $form_state->getValue('column_widths');
}
/**
* {@inheritdoc}
*/
public function build(array $regions) {
$build = parent::build($regions);
$build['#attributes']['class'] = [
'layout',
$this->getPluginDefinition()->getTemplate(),
$this->getPluginDefinition()->getTemplate() . '--' . $this->configuration['column_widths'],
];
return $build;
}
/**
* Gets the width options for the configuration form.
*
* The first option will be used as the default 'column_widths' configuration
* value.
*
* @return string[]
* The width options array where the keys are strings that will be added to
* the CSS classes and the values are the human readable labels.
*/
abstract protected function getWidthOptions();
/**
* Provides a default value for the width options.
*
* @return string
* A key from the array returned by ::getWidthOptions().
*/
protected function getDefaultWidth() {
// Return the first available key from the list of options.
$width_classes = array_keys($this->getWidthOptions());
return array_shift($width_classes);
}
}

View File

@@ -0,0 +1,32 @@
<?php
namespace Drupal\layout_builder\Plugin\Layout;
/**
* Configurable three column layout plugin class.
*
* @internal
* Plugin classes are internal.
*/
class ThreeColumnLayout extends MultiWidthLayoutBase {
/**
* {@inheritdoc}
*/
protected function getWidthOptions() {
return [
'25-50-25' => '25%/50%/25%',
'33-34-33' => '33%/34%/33%',
'25-25-50' => '25%/25%/50%',
'50-25-25' => '50%/25%/25%',
];
}
/**
* {@inheritdoc}
*/
protected function getDefaultWidth() {
return '33-34-33';
}
}

View File

@@ -0,0 +1,33 @@
<?php
namespace Drupal\layout_builder\Plugin\Layout;
/**
* Configurable two column layout plugin class.
*
* @internal
* Plugin classes are internal.
*/
class TwoColumnLayout extends MultiWidthLayoutBase {
/**
* {@inheritdoc}
*/
protected function getWidthOptions() {
return [
'50-50' => '50%/50%',
'33-67' => '33%/67%',
'67-33' => '67%/33%',
'25-75' => '25%/75%',
'75-25' => '75%/25%',
];
}
/**
* {@inheritdoc}
*/
protected function getDefaultWidth() {
return '50-50';
}
}

View File

@@ -0,0 +1,416 @@
<?php
namespace Drupal\layout_builder\Plugin\SectionStorage;
use Drupal\Component\Plugin\Context\ContextInterface as ComponentContextInterface;
use Drupal\Component\Utility\NestedArray;
use Drupal\Core\Access\AccessResult;
use Drupal\Core\Cache\RefinableCacheableDependencyInterface;
use Drupal\Core\Entity\EntityTypeBundleInfoInterface;
use Drupal\Core\Entity\EntityTypeInterface;
use Drupal\Core\Entity\EntityTypeManagerInterface;
use Drupal\Core\Entity\FieldableEntityInterface;
use Drupal\Core\Plugin\ContainerFactoryPluginInterface;
use Drupal\Core\Plugin\Context\ContextDefinition;
use Drupal\Core\Plugin\Context\EntityContext;
use Drupal\Core\Plugin\Context\EntityContextDefinition;
use Drupal\Core\Session\AccountInterface;
use Drupal\Core\StringTranslation\TranslatableMarkup;
use Drupal\Core\Url;
use Drupal\layout_builder\Attribute\SectionStorage;
use Drupal\layout_builder\DefaultsSectionStorageInterface;
use Drupal\layout_builder\Entity\SampleEntityGeneratorInterface;
use Symfony\Component\DependencyInjection\ContainerInterface;
use Symfony\Component\Routing\RouteCollection;
/**
* Defines the 'defaults' section storage type.
*
* DefaultsSectionStorage uses a positive weight because:
* - It must be picked after
* \Drupal\layout_builder\Plugin\SectionStorage\OverridesSectionStorage.
* - The default weight is 0, so other custom implementations will also take
* precedence unless otherwise specified.
*
* @internal
* Plugin classes are internal.
*/
#[SectionStorage(id: "defaults", weight: 20, context_definitions: [
"display" => new EntityContextDefinition(
data_type: "entity_view_display",
label: new TranslatableMarkup("Entity view display"),
),
'view_mode' => new ContextDefinition(
data_type: 'string',
label: new TranslatableMarkup("View mode"),
default_value: "default",
),
])]
class DefaultsSectionStorage extends SectionStorageBase implements ContainerFactoryPluginInterface, DefaultsSectionStorageInterface {
/**
* The entity type manager.
*
* @var \Drupal\Core\Entity\EntityTypeManagerInterface
*/
protected $entityTypeManager;
/**
* The entity type bundle info.
*
* @var \Drupal\Core\Entity\EntityTypeBundleInfoInterface
*/
protected $entityTypeBundleInfo;
/**
* The sample entity generator.
*
* @var \Drupal\layout_builder\Entity\SampleEntityGeneratorInterface
*/
protected $sampleEntityGenerator;
/**
* {@inheritdoc}
*/
public function __construct(array $configuration, $plugin_id, $plugin_definition, EntityTypeManagerInterface $entity_type_manager, EntityTypeBundleInfoInterface $entity_type_bundle_info, SampleEntityGeneratorInterface $sample_entity_generator) {
parent::__construct($configuration, $plugin_id, $plugin_definition);
$this->entityTypeManager = $entity_type_manager;
$this->entityTypeBundleInfo = $entity_type_bundle_info;
$this->sampleEntityGenerator = $sample_entity_generator;
}
/**
* {@inheritdoc}
*/
public static function create(ContainerInterface $container, array $configuration, $plugin_id, $plugin_definition) {
return new static(
$configuration,
$plugin_id,
$plugin_definition,
$container->get('entity_type.manager'),
$container->get('entity_type.bundle.info'),
$container->get('layout_builder.sample_entity_generator')
);
}
/**
* {@inheritdoc}
*/
protected function getSectionList() {
return $this->getContextValue('display');
}
/**
* Gets the entity storing the defaults.
*
* @return \Drupal\layout_builder\Entity\LayoutEntityDisplayInterface
* The entity storing the defaults.
*/
protected function getDisplay() {
return $this->getSectionList();
}
/**
* {@inheritdoc}
*/
public function getStorageId() {
return $this->getDisplay()->id();
}
/**
* {@inheritdoc}
*/
public function getRedirectUrl() {
return Url::fromRoute("entity.entity_view_display.{$this->getDisplay()->getTargetEntityTypeId()}.view_mode", $this->getRouteParameters());
}
/**
* {@inheritdoc}
*/
public function getLayoutBuilderUrl($rel = 'view') {
return Url::fromRoute("layout_builder.{$this->getStorageType()}.{$this->getDisplay()->getTargetEntityTypeId()}.$rel", $this->getRouteParameters());
}
/**
* Provides the route parameters needed to generate a URL for this object.
*
* @return mixed[]
* An associative array of parameter names and values.
*/
protected function getRouteParameters() {
$display = $this->getDisplay();
$entity_type = $this->entityTypeManager->getDefinition($display->getTargetEntityTypeId());
$bundle_parameter_key = $entity_type->getBundleEntityType() ?: 'bundle';
return [
$bundle_parameter_key => $display->getTargetBundle(),
'view_mode_name' => $display->getMode(),
];
}
/**
* {@inheritdoc}
*/
public function buildRoutes(RouteCollection $collection) {
if (!\Drupal::moduleHandler()->moduleExists('field_ui')) {
return;
}
foreach ($this->getEntityTypes() as $entity_type_id => $entity_type) {
// Try to get the route from the current collection.
if (!$entity_route = $collection->get($entity_type->get('field_ui_base_route'))) {
continue;
}
$path = $entity_route->getPath() . '/display/{view_mode_name}/layout';
$defaults = [];
$defaults['entity_type_id'] = $entity_type_id;
// If the entity type has no bundles and it doesn't use {bundle} in its
// admin path, use the entity type.
if (!str_contains($path, '{bundle}')) {
if (!$entity_type->hasKey('bundle')) {
$defaults['bundle'] = $entity_type_id;
}
else {
$defaults['bundle_key'] = $entity_type->getBundleEntityType();
}
}
$requirements = [];
$requirements['_field_ui_view_mode_access'] = 'administer ' . $entity_type_id . ' display';
$options = $entity_route->getOptions();
$options['_admin_route'] = FALSE;
$this->buildLayoutRoutes($collection, $this->getPluginDefinition(), $path, $defaults, $requirements, $options, $entity_type_id, 'entity_view_display');
// Set field_ui.route_enhancer to run on the manage layout form.
if (isset($defaults['bundle_key'])) {
$collection->get("layout_builder.defaults.$entity_type_id.view")
->setOption('_field_ui', TRUE)
->setDefault('bundle', '');
}
$route_names = [
"entity.entity_view_display.{$entity_type_id}.default",
"entity.entity_view_display.{$entity_type_id}.view_mode",
];
foreach ($route_names as $route_name) {
if (!$route = $collection->get($route_name)) {
continue;
}
$route->addDefaults([
'section_storage_type' => $this->getStorageType(),
'section_storage' => '',
] + $defaults);
$parameters['section_storage']['layout_builder_tempstore'] = TRUE;
$parameters = NestedArray::mergeDeep($parameters, $route->getOption('parameters') ?: []);
$route->setOption('parameters', $parameters);
}
}
}
/**
* Returns an array of relevant entity types.
*
* @return \Drupal\Core\Entity\EntityTypeInterface[]
* An array of entity types.
*/
protected function getEntityTypes() {
return array_filter($this->entityTypeManager->getDefinitions(), function (EntityTypeInterface $entity_type) {
return $entity_type->entityClassImplements(FieldableEntityInterface::class) && $entity_type->hasHandlerClass('form', 'layout_builder') && $entity_type->hasViewBuilderClass() && $entity_type->get('field_ui_base_route');
});
}
/**
* {@inheritdoc}
*/
public function getContextsDuringPreview() {
$contexts = parent::getContextsDuringPreview();
// During preview add a sample entity for the target entity type and bundle.
$display = $this->getDisplay();
$entity = $this->sampleEntityGenerator->get($display->getTargetEntityTypeId(), $display->getTargetBundle());
$contexts['layout_builder.entity'] = EntityContext::fromEntity($entity);
return $contexts;
}
/**
* {@inheritdoc}
*/
public function deriveContextsFromRoute($value, $definition, $name, array $defaults) {
$contexts = [];
if ($entity = $this->extractEntityFromRoute($value, $defaults)) {
$contexts['display'] = EntityContext::fromEntity($entity);
}
return $contexts;
}
/**
* Extracts an entity from the route values.
*
* @param mixed $value
* The raw value from the route.
* @param array $defaults
* The route defaults array.
*
* @return \Drupal\Core\Entity\EntityInterface|null
* The entity for the route, or NULL if none exist.
*
* @see \Drupal\layout_builder\SectionStorageInterface::deriveContextsFromRoute()
* @see \Drupal\Core\ParamConverter\ParamConverterInterface::convert()
*/
private function extractEntityFromRoute($value, array $defaults) {
// If a bundle is not provided but a value corresponding to the bundle key
// is, use that for the bundle value.
if (empty($defaults['bundle']) && isset($defaults['bundle_key']) && !empty($defaults[$defaults['bundle_key']])) {
$defaults['bundle'] = $defaults[$defaults['bundle_key']];
}
if (is_string($value) && str_contains($value, '.')) {
[$entity_type_id, $bundle, $view_mode] = explode('.', $value, 3);
}
elseif (!empty($defaults['entity_type_id']) && !empty($defaults['bundle']) && !empty($defaults['view_mode_name'])) {
$entity_type_id = $defaults['entity_type_id'];
$bundle = $defaults['bundle'];
$view_mode = $defaults['view_mode_name'];
$value = "$entity_type_id.$bundle.$view_mode";
}
else {
return NULL;
}
$storage = $this->entityTypeManager->getStorage('entity_view_display');
// If the display does not exist, create a new one.
if (!$display = $storage->load($value)) {
$display = $storage->create([
'targetEntityType' => $entity_type_id,
'bundle' => $bundle,
'mode' => $view_mode,
'status' => TRUE,
]);
}
return $display;
}
/**
* {@inheritdoc}
*/
public function label() {
return $this->getDisplay()->label();
}
/**
* {@inheritdoc}
*/
public function save() {
return $this->getDisplay()->save();
}
/**
* {@inheritdoc}
*/
public function isOverridable() {
return $this->getDisplay()->isOverridable();
}
/**
* {@inheritdoc}
*/
public function setOverridable($overridable = TRUE) {
$this->getDisplay()->setOverridable($overridable);
return $this;
}
/**
* {@inheritdoc}
*/
public function setThirdPartySetting($module, $key, $value) {
$this->getDisplay()->setThirdPartySetting($module, $key, $value);
return $this;
}
/**
* {@inheritdoc}
*/
public function isLayoutBuilderEnabled() {
return $this->getDisplay()->isLayoutBuilderEnabled();
}
/**
* {@inheritdoc}
*/
public function enableLayoutBuilder() {
$this->getDisplay()->enableLayoutBuilder();
return $this;
}
/**
* {@inheritdoc}
*/
public function disableLayoutBuilder() {
$this->getDisplay()->disableLayoutBuilder();
return $this;
}
/**
* {@inheritdoc}
*/
public function getThirdPartySetting($module, $key, $default = NULL) {
return $this->getDisplay()->getThirdPartySetting($module, $key, $default);
}
/**
* {@inheritdoc}
*/
public function getThirdPartySettings($module) {
return $this->getDisplay()->getThirdPartySettings($module);
}
/**
* {@inheritdoc}
*/
public function unsetThirdPartySetting($module, $key) {
$this->getDisplay()->unsetThirdPartySetting($module, $key);
return $this;
}
/**
* {@inheritdoc}
*/
public function getThirdPartyProviders() {
return $this->getDisplay()->getThirdPartyProviders();
}
/**
* {@inheritdoc}
*/
public function access($operation, ?AccountInterface $account = NULL, $return_as_object = FALSE) {
$result = AccessResult::allowedIf($this->isLayoutBuilderEnabled())->addCacheableDependency($this);
return $return_as_object ? $result : $result->isAllowed();
}
/**
* {@inheritdoc}
*/
public function isApplicable(RefinableCacheableDependencyInterface $cacheability) {
$cacheability->addCacheableDependency($this);
return $this->isLayoutBuilderEnabled();
}
/**
* {@inheritdoc}
*/
public function setContext($name, ComponentContextInterface $context) {
// Set the view mode context based on the display context.
if ($name === 'display') {
$this->setContextValue('view_mode', $context->getContextValue()->getMode());
}
parent::setContext($name, $context);
}
}

View File

@@ -0,0 +1,417 @@
<?php
namespace Drupal\layout_builder\Plugin\SectionStorage;
use Drupal\Core\Access\AccessResult;
use Drupal\Core\Cache\RefinableCacheableDependencyInterface;
use Drupal\Core\Entity\EntityFieldManagerInterface;
use Drupal\Core\Entity\EntityRepositoryInterface;
use Drupal\Core\Entity\EntityTypeInterface;
use Drupal\Core\Entity\EntityTypeManagerInterface;
use Drupal\Core\Entity\FieldableEntityInterface;
use Drupal\Core\Entity\TranslatableInterface;
use Drupal\Core\Plugin\ContainerFactoryPluginInterface;
use Drupal\Core\Plugin\Context\Context;
use Drupal\Core\Plugin\Context\ContextDefinition;
use Drupal\Core\Plugin\Context\EntityContext;
use Drupal\Core\Session\AccountInterface;
use Drupal\Core\StringTranslation\TranslatableMarkup;
use Drupal\Core\Url;
use Drupal\layout_builder\Attribute\SectionStorage;
use Drupal\layout_builder\Entity\LayoutBuilderEntityViewDisplay;
use Drupal\layout_builder\OverridesSectionStorageInterface;
use Drupal\layout_builder\SectionStorage\SectionStorageManagerInterface;
use Symfony\Component\DependencyInjection\ContainerInterface;
use Symfony\Component\Routing\RouteCollection;
/**
* Defines the 'overrides' section storage type.
*
* OverridesSectionStorage uses a negative weight because:
* - It must be picked before
* \Drupal\layout_builder\Plugin\SectionStorage\DefaultsSectionStorage.
* - The default weight is 0, so custom implementations will not take
* precedence unless otherwise specified.
*
* @internal
* Plugin classes are internal.
*/
#[SectionStorage(
id: "overrides",
weight: -20,
context_definitions: [
'entity' => new ContextDefinition(
data_type: 'entity',
label: new TranslatableMarkup("Entity"),
constraints: [
"EntityHasField" => OverridesSectionStorage::FIELD_NAME,
],
),
'view_mode' => new ContextDefinition(
data_type: 'string',
label: new TranslatableMarkup("View mode"),
default_value: "default",
),
],
handles_permission_check: TRUE,
)]
class OverridesSectionStorage extends SectionStorageBase implements ContainerFactoryPluginInterface, OverridesSectionStorageInterface, SectionStorageLocalTaskProviderInterface {
/**
* The field name used by this storage.
*
* @var string
*/
const FIELD_NAME = 'layout_builder__layout';
/**
* The entity type manager.
*
* @var \Drupal\Core\Entity\EntityTypeManagerInterface
*/
protected $entityTypeManager;
/**
* The entity field manager.
*
* @var \Drupal\Core\Entity\EntityFieldManagerInterface
*/
protected $entityFieldManager;
/**
* The section storage manager.
*
* @var \Drupal\layout_builder\SectionStorage\SectionStorageManagerInterface
*/
protected $sectionStorageManager;
/**
* The entity repository.
*
* @var \Drupal\Core\Entity\EntityRepositoryInterface
*/
protected $entityRepository;
/**
* The current user.
*
* @var \Drupal\Core\Session\AccountInterface
*/
protected $currentUser;
/**
* {@inheritdoc}
*/
public function __construct(array $configuration, $plugin_id, $plugin_definition, EntityTypeManagerInterface $entity_type_manager, EntityFieldManagerInterface $entity_field_manager, SectionStorageManagerInterface $section_storage_manager, EntityRepositoryInterface $entity_repository, AccountInterface $current_user) {
parent::__construct($configuration, $plugin_id, $plugin_definition);
$this->entityTypeManager = $entity_type_manager;
$this->entityFieldManager = $entity_field_manager;
$this->sectionStorageManager = $section_storage_manager;
$this->entityRepository = $entity_repository;
$this->currentUser = $current_user;
}
/**
* {@inheritdoc}
*/
public static function create(ContainerInterface $container, array $configuration, $plugin_id, $plugin_definition) {
return new static(
$configuration,
$plugin_id,
$plugin_definition,
$container->get('entity_type.manager'),
$container->get('entity_field.manager'),
$container->get('plugin.manager.layout_builder.section_storage'),
$container->get('entity.repository'),
$container->get('current_user')
);
}
/**
* {@inheritdoc}
*/
protected function getSectionList() {
return $this->getEntity()->get(static::FIELD_NAME);
}
/**
* Gets the entity storing the overrides.
*
* @return \Drupal\Core\Entity\FieldableEntityInterface
* The entity storing the overrides.
*/
protected function getEntity() {
return $this->getContextValue('entity');
}
/**
* {@inheritdoc}
*/
public function getStorageId() {
$entity = $this->getEntity();
return $entity->getEntityTypeId() . '.' . $entity->id();
}
/**
* {@inheritdoc}
*/
public function getTempstoreKey() {
$key = parent::getTempstoreKey();
$key .= '.' . $this->getContextValue('view_mode');
$entity = $this->getEntity();
// @todo Allow entities to provide this contextual information in
// https://www.drupal.org/project/drupal/issues/3026957.
if ($entity instanceof TranslatableInterface) {
$key .= '.' . $entity->language()->getId();
}
return $key;
}
/**
* {@inheritdoc}
*/
public function deriveContextsFromRoute($value, $definition, $name, array $defaults) {
$contexts = [];
if ($entity = $this->extractEntityFromRoute($value, $defaults)) {
$contexts['entity'] = EntityContext::fromEntity($entity);
// @todo Expand to work for all view modes in
// https://www.drupal.org/node/2907413.
$view_mode = 'full';
// Retrieve the actual view mode from the returned view display as the
// requested view mode may not exist and a fallback will be used.
$view_mode = LayoutBuilderEntityViewDisplay::collectRenderDisplay($entity, $view_mode)->getMode();
$contexts['view_mode'] = new Context(new ContextDefinition('string'), $view_mode);
}
return $contexts;
}
/**
* Extracts an entity from the route values.
*
* @param mixed $value
* The raw value from the route.
* @param array $defaults
* The route defaults array.
*
* @return \Drupal\Core\Entity\EntityInterface|null
* The entity for the route, or NULL if none exist. The entity is not
* guaranteed to be fieldable, or contain the necessary field for this
* section storage plugin.
*
* @see \Drupal\layout_builder\SectionStorageInterface::deriveContextsFromRoute()
* @see \Drupal\Core\ParamConverter\ParamConverterInterface::convert()
*/
private function extractEntityFromRoute($value, array $defaults) {
if (str_contains($value, '.')) {
[$entity_type_id, $entity_id] = explode('.', $value, 2);
}
elseif (isset($defaults['entity_type_id']) && !empty($defaults[$defaults['entity_type_id']])) {
$entity_type_id = $defaults['entity_type_id'];
$entity_id = $defaults[$entity_type_id];
}
else {
return NULL;
}
$entity = $this->entityRepository->getActive($entity_type_id, $entity_id);
return ($entity instanceof FieldableEntityInterface) ? $entity : NULL;
}
/**
* {@inheritdoc}
*/
public function buildRoutes(RouteCollection $collection) {
foreach ($this->getEntityTypes() as $entity_type_id => $entity_type) {
// If the canonical route does not exist, do not provide any Layout
// Builder UI routes for this entity type.
if (!$collection->get("entity.$entity_type_id.canonical")) {
continue;
}
$defaults = [];
$defaults['entity_type_id'] = $entity_type_id;
// Retrieve the requirements from the canonical route.
$requirements = $collection->get("entity.$entity_type_id.canonical")->getRequirements();
$options = [];
// Ensure that upcasting is run in the correct order.
$options['parameters']['section_storage'] = [];
$options['parameters'][$entity_type_id]['type'] = 'entity:' . $entity_type_id;
$options['_admin_route'] = FALSE;
$template = $entity_type->getLinkTemplate('canonical') . '/layout';
$this->buildLayoutRoutes($collection, $this->getPluginDefinition(), $template, $defaults, $requirements, $options, $entity_type_id, $entity_type_id);
}
}
/**
* {@inheritdoc}
*/
public function buildLocalTasks($base_plugin_definition) {
$local_tasks = [];
foreach ($this->getEntityTypes() as $entity_type_id => $entity_type) {
$local_tasks["layout_builder.overrides.$entity_type_id.view"] = $base_plugin_definition + [
'route_name' => "layout_builder.overrides.$entity_type_id.view",
'weight' => 15,
'title' => $this->t('Layout'),
'base_route' => "entity.$entity_type_id.canonical",
'cache_contexts' => ['layout_builder_is_active:' . $entity_type_id],
];
}
return $local_tasks;
}
/**
* Determines if this entity type's ID is stored as an integer.
*
* @param \Drupal\Core\Entity\EntityTypeInterface $entity_type
* An entity type.
*
* @return bool
* TRUE if this entity type's ID key is always an integer, FALSE otherwise.
*/
protected function hasIntegerId(EntityTypeInterface $entity_type) {
$field_storage_definitions = $this->entityFieldManager->getFieldStorageDefinitions($entity_type->id());
return $field_storage_definitions[$entity_type->getKey('id')]->getType() === 'integer';
}
/**
* Returns an array of relevant entity types.
*
* @return \Drupal\Core\Entity\EntityTypeInterface[]
* An array of entity types.
*/
protected function getEntityTypes() {
return array_filter($this->entityTypeManager->getDefinitions(), function (EntityTypeInterface $entity_type) {
return $entity_type->entityClassImplements(FieldableEntityInterface::class) && $entity_type->hasHandlerClass('form', 'layout_builder') && $entity_type->hasViewBuilderClass() && $entity_type->hasLinkTemplate('canonical');
});
}
/**
* {@inheritdoc}
*/
public function getDefaultSectionStorage() {
$display = LayoutBuilderEntityViewDisplay::collectRenderDisplay($this->getEntity(), $this->getContextValue('view_mode'));
return $this->sectionStorageManager->load('defaults', ['display' => EntityContext::fromEntity($display)]);
}
/**
* {@inheritdoc}
*/
public function getRedirectUrl() {
return $this->getEntity()->toUrl('canonical');
}
/**
* {@inheritdoc}
*/
public function getLayoutBuilderUrl($rel = 'view') {
$entity = $this->getEntity();
$route_parameters[$entity->getEntityTypeId()] = $entity->id();
return Url::fromRoute("layout_builder.{$this->getStorageType()}.{$this->getEntity()->getEntityTypeId()}.$rel", $route_parameters);
}
/**
* {@inheritdoc}
*/
public function getContextsDuringPreview() {
$contexts = parent::getContextsDuringPreview();
if (isset($contexts['entity'])) {
$contexts['layout_builder.entity'] = $contexts['entity'];
unset($contexts['entity']);
}
return $contexts;
}
/**
* {@inheritdoc}
*/
public function label() {
return $this->getEntity()->label();
}
/**
* {@inheritdoc}
*/
public function save() {
return $this->getEntity()->save();
}
/**
* {@inheritdoc}
*/
public function access($operation, ?AccountInterface $account = NULL, $return_as_object = FALSE) {
if ($account === NULL) {
$account = $this->currentUser;
}
$entity = $this->getEntity();
// Create an access result that will allow access to the layout if one of
// these conditions applies:
// 1. The user can configure any layouts.
$any_access = AccessResult::allowedIfHasPermission($account, 'configure any layout');
// 2. The user can configure layouts on all items of the bundle type.
$bundle_access = AccessResult::allowedIfHasPermission($account, "configure all {$entity->bundle()} {$entity->getEntityTypeId()} layout overrides");
// 3. The user can configure layouts items of this bundle type they can edit
// AND the user has access to edit this entity.
$edit_only_bundle_access = AccessResult::allowedIfHasPermission($account, "configure editable {$entity->bundle()} {$entity->getEntityTypeId()} layout overrides");
$edit_only_bundle_access = $edit_only_bundle_access->andIf($entity->access('update', $account, TRUE));
$result = $any_access
->orIf($bundle_access)
->orIf($edit_only_bundle_access);
// Access also depends on the default being enabled.
$result = $result->andIf($this->getDefaultSectionStorage()->access($operation, $account, TRUE));
// Access also depends on the default layout being overridable.
$result = $result->andIf(AccessResult::allowedIf($this->getDefaultSectionStorage()->isOverridable())->addCacheableDependency($this->getDefaultSectionStorage()));
$result = $this->handleTranslationAccess($result, $operation, $account);
return $return_as_object ? $result : $result->isAllowed();
}
/**
* Handles access checks related to translations.
*
* @param \Drupal\Core\Access\AccessResult $result
* The access result.
* @param string $operation
* The operation to be performed.
* @param \Drupal\Core\Session\AccountInterface $account
* The user for which to check access.
*
* @return \Drupal\Core\Access\AccessResultInterface
* The access result.
*/
protected function handleTranslationAccess(AccessResult $result, $operation, AccountInterface $account) {
$entity = $this->getEntity();
// Access is always denied on non-default translations.
return $result->andIf(AccessResult::allowedIf(!($entity instanceof TranslatableInterface && !$entity->isDefaultTranslation())))->addCacheableDependency($entity);
}
/**
* {@inheritdoc}
*/
public function isApplicable(RefinableCacheableDependencyInterface $cacheability) {
$default_section_storage = $this->getDefaultSectionStorage();
$cacheability->addCacheableDependency($default_section_storage)->addCacheableDependency($this);
// Check that overrides are enabled and have at least one section.
return $default_section_storage->isOverridable() && $this->isOverridden();
}
/**
* {@inheritdoc}
*/
public function isOverridden() {
// If there are any sections at all, including a blank one, this section
// storage has been overridden. Do not use count() as it does not include
// blank sections.
return !empty($this->getSections());
}
}

View File

@@ -0,0 +1,115 @@
<?php
namespace Drupal\layout_builder\Plugin\SectionStorage;
use Drupal\Core\Cache\CacheableDependencyInterface;
use Drupal\Core\Plugin\Context\Context;
use Drupal\Core\Plugin\Context\ContextDefinition;
use Drupal\Core\Plugin\ContextAwarePluginTrait;
use Drupal\Core\Plugin\PluginBase;
use Drupal\layout_builder\Routing\LayoutBuilderRoutesTrait;
use Drupal\layout_builder\Section;
use Drupal\layout_builder\SectionStorageInterface;
use Drupal\layout_builder\TempStoreIdentifierInterface;
/**
* Provides a base class for Section Storage types.
*/
abstract class SectionStorageBase extends PluginBase implements SectionStorageInterface, TempStoreIdentifierInterface, CacheableDependencyInterface {
use ContextAwarePluginTrait;
use LayoutBuilderRoutesTrait;
/**
* Gets the section list.
*
* @return \Drupal\layout_builder\SectionListInterface
* The section list.
*/
abstract protected function getSectionList();
/**
* {@inheritdoc}
*/
public function getStorageType() {
return $this->getPluginId();
}
/**
* {@inheritdoc}
*/
#[\ReturnTypeWillChange]
public function count() {
return $this->getSectionList()->count();
}
/**
* {@inheritdoc}
*/
public function getSections() {
return $this->getSectionList()->getSections();
}
/**
* {@inheritdoc}
*/
public function getSection($delta) {
return $this->getSectionList()->getSection($delta);
}
/**
* {@inheritdoc}
*/
public function appendSection(Section $section) {
$this->getSectionList()->appendSection($section);
return $this;
}
/**
* {@inheritdoc}
*/
public function insertSection($delta, Section $section) {
$this->getSectionList()->insertSection($delta, $section);
return $this;
}
/**
* {@inheritdoc}
*/
public function removeSection($delta) {
$this->getSectionList()->removeSection($delta);
return $this;
}
/**
* {@inheritdoc}
*/
public function removeAllSections($set_blank = FALSE) {
$this->getSectionList()->removeAllSections($set_blank);
return $this;
}
/**
* {@inheritdoc}
*/
public function getContextsDuringPreview() {
$contexts = $this->getContexts();
// view_mode is a required context, but SectionStorage plugins are not
// required to return it (for example, the layout_library plugin provided
// in the Layout Library module. In these instances, explicitly create a
// view_mode context with the value "default".
if (!isset($contexts['view_mode']) || $contexts['view_mode']->validate()->count() || !$contexts['view_mode']->getContextValue()) {
$contexts['view_mode'] = new Context(new ContextDefinition('string'), 'default');
}
return $contexts;
}
/**
* {@inheritdoc}
*/
public function getTempstoreKey() {
return $this->getStorageId();
}
}

View File

@@ -0,0 +1,24 @@
<?php
namespace Drupal\layout_builder\Plugin\SectionStorage;
/**
* Allows section storage plugins to provide local tasks.
*
* @see \Drupal\layout_builder\Plugin\Derivative\LayoutBuilderLocalTaskDeriver
* @see \Drupal\layout_builder\SectionStorageInterface
*/
interface SectionStorageLocalTaskProviderInterface {
/**
* Provides the local tasks dynamically for Layout Builder plugins.
*
* @param mixed $base_plugin_definition
* The definition of the base plugin.
*
* @return array
* An array of full derivative definitions keyed on derivative ID.
*/
public function buildLocalTasks($base_plugin_definition);
}

View File

@@ -0,0 +1,57 @@
<?php
namespace Drupal\layout_builder\Routing;
use Drupal\Core\Routing\RouteBuildEvent;
use Drupal\Core\Routing\RoutingEvents;
use Drupal\layout_builder\SectionStorage\SectionStorageManagerInterface;
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
/**
* Provides routes for the Layout Builder UI.
*
* @internal
* Tagged services are internal.
*/
class LayoutBuilderRoutes implements EventSubscriberInterface {
/**
* The section storage manager.
*
* @var \Drupal\layout_builder\SectionStorage\SectionStorageManagerInterface
*/
protected $sectionStorageManager;
/**
* Constructs a new LayoutBuilderRoutes.
*
* @param \Drupal\layout_builder\SectionStorage\SectionStorageManagerInterface $section_storage_manager
* The section storage manager.
*/
public function __construct(SectionStorageManagerInterface $section_storage_manager) {
$this->sectionStorageManager = $section_storage_manager;
}
/**
* Alters existing routes for a specific collection.
*
* @param \Drupal\Core\Routing\RouteBuildEvent $event
* The route build event.
*/
public function onAlterRoutes(RouteBuildEvent $event) {
$collection = $event->getRouteCollection();
foreach ($this->sectionStorageManager->getDefinitions() as $plugin_id => $definition) {
$this->sectionStorageManager->loadEmpty($plugin_id)->buildRoutes($collection);
}
}
/**
* {@inheritdoc}
*/
public static function getSubscribedEvents(): array {
// Run after \Drupal\field_ui\Routing\RouteSubscriber.
$events[RoutingEvents::ALTER] = ['onAlterRoutes', -110];
return $events;
}
}

View File

@@ -0,0 +1,103 @@
<?php
namespace Drupal\layout_builder\Routing;
use Drupal\Component\Utility\NestedArray;
use Drupal\layout_builder\DefaultsSectionStorageInterface;
use Drupal\layout_builder\OverridesSectionStorageInterface;
use Drupal\layout_builder\SectionStorage\SectionStorageDefinition;
use Symfony\Component\Routing\Route;
use Symfony\Component\Routing\RouteCollection;
/**
* Provides a trait for building routes for a Layout Builder UI.
*/
trait LayoutBuilderRoutesTrait {
/**
* Builds the layout routes for the given values.
*
* @param \Symfony\Component\Routing\RouteCollection $collection
* The route collection.
* @param \Drupal\layout_builder\SectionStorage\SectionStorageDefinition $definition
* The definition of the section storage.
* @param string $path
* The path patten for the routes.
* @param array $defaults
* (optional) An array of default parameter values.
* @param array $requirements
* (optional) An array of requirements for parameters.
* @param array $options
* (optional) An array of options.
* @param string $route_name_prefix
* (optional) The prefix to use for the route name.
* @param string $entity_type_id
* (optional) The entity type ID, if available.
*/
protected function buildLayoutRoutes(RouteCollection $collection, SectionStorageDefinition $definition, $path, array $defaults = [], array $requirements = [], array $options = [], $route_name_prefix = '', $entity_type_id = '') {
$type = $definition->id();
$defaults['section_storage_type'] = $type;
// Provide an empty value to allow the section storage to be upcast.
$defaults['section_storage'] = '';
// Trigger the layout builder access check.
$requirements['_layout_builder_access'] = 'view';
// Trigger the layout builder RouteEnhancer.
$options['_layout_builder'] = TRUE;
// Trigger the layout builder param converter.
$parameters['section_storage']['layout_builder_tempstore'] = TRUE;
// Merge the passed in options in after Layout Builder's parameters.
$options = NestedArray::mergeDeep(['parameters' => $parameters], $options);
if ($route_name_prefix) {
$route_name_prefix = "layout_builder.$type.$route_name_prefix";
}
else {
$route_name_prefix = "layout_builder.$type";
}
$main_defaults = $defaults;
$main_options = $options;
if ($entity_type_id) {
$main_defaults['_entity_form'] = "$entity_type_id.layout_builder";
}
else {
$main_defaults['_controller'] = '\Drupal\layout_builder\Controller\LayoutBuilderController::layout';
}
$main_defaults['_title_callback'] = '\Drupal\layout_builder\Controller\LayoutBuilderController::title';
$route = (new Route($path))
->setDefaults($main_defaults)
->setRequirements($requirements)
->setOptions($main_options);
$collection->add("$route_name_prefix.view", $route);
$discard_changes_defaults = $defaults;
$discard_changes_defaults['_form'] = '\Drupal\layout_builder\Form\DiscardLayoutChangesForm';
$route = (new Route("$path/discard-changes"))
->setDefaults($discard_changes_defaults)
->setRequirements($requirements)
->setOptions($options);
$collection->add("$route_name_prefix.discard_changes", $route);
if (is_subclass_of($definition->getClass(), OverridesSectionStorageInterface::class)) {
$revert_defaults = $defaults;
$revert_defaults['_form'] = '\Drupal\layout_builder\Form\RevertOverridesForm';
$route = (new Route("$path/revert"))
->setDefaults($revert_defaults)
->setRequirements($requirements)
->setOptions($options);
$collection->add("$route_name_prefix.revert", $route);
}
elseif (is_subclass_of($definition->getClass(), DefaultsSectionStorageInterface::class)) {
$disable_defaults = $defaults;
$disable_defaults['_form'] = '\Drupal\layout_builder\Form\LayoutBuilderDisableForm';
$disable_options = $options;
unset($disable_options['_admin_route'], $disable_options['_layout_builder']);
$route = (new Route("$path/disable"))
->setDefaults($disable_defaults)
->setRequirements($requirements)
->setOptions($disable_options);
$collection->add("$route_name_prefix.disable", $route);
}
}
}

View File

@@ -0,0 +1,57 @@
<?php
namespace Drupal\layout_builder\Routing;
use Drupal\Core\ParamConverter\ParamConverterInterface;
use Drupal\layout_builder\SectionStorage\SectionStorageManagerInterface;
use Symfony\Component\Routing\Route;
/**
* Loads the section storage from the routing defaults.
*
* @internal
* Tagged services are internal.
*/
class LayoutSectionStorageParamConverter implements ParamConverterInterface {
/**
* The section storage manager.
*
* @var \Drupal\layout_builder\SectionStorage\SectionStorageManagerInterface
*/
protected $sectionStorageManager;
/**
* Constructs a new LayoutSectionStorageParamConverter.
*
* @param \Drupal\layout_builder\SectionStorage\SectionStorageManagerInterface $section_storage_manager
* The section storage manager.
*/
public function __construct(SectionStorageManagerInterface $section_storage_manager) {
$this->sectionStorageManager = $section_storage_manager;
}
/**
* {@inheritdoc}
*/
public function convert($value, $definition, $name, array $defaults) {
// If no section storage type is specified or if it is invalid, return.
if (!isset($defaults['section_storage_type']) || !$this->sectionStorageManager->hasDefinition($defaults['section_storage_type'])) {
return NULL;
}
$type = $defaults['section_storage_type'];
// Load an empty instance and derive the available contexts.
$contexts = $this->sectionStorageManager->loadEmpty($type)->deriveContextsFromRoute($value, $definition, $name, $defaults);
// Attempt to load a full instance based on the context.
return $this->sectionStorageManager->load($type, $contexts);
}
/**
* {@inheritdoc}
*/
public function applies($definition, $name, Route $route) {
return !empty($definition['layout_builder_section_storage']) || !empty($definition['layout_builder_tempstore']);
}
}

View File

@@ -0,0 +1,44 @@
<?php
namespace Drupal\layout_builder\Routing;
use Drupal\Core\Routing\EnhancerInterface;
use Drupal\Core\Routing\RouteObjectInterface;
use Drupal\layout_builder\LayoutTempstoreRepositoryInterface;
use Drupal\layout_builder\SectionStorageInterface;
use Symfony\Component\HttpFoundation\Request;
/**
* Loads the section storage from the layout tempstore.
*/
class LayoutTempstoreRouteEnhancer implements EnhancerInterface {
/**
* The layout tempstore repository.
*
* @var \Drupal\layout_builder\LayoutTempstoreRepositoryInterface
*/
protected $layoutTempstoreRepository;
/**
* Constructs a new LayoutTempstoreRouteEnhancer.
*
* @param \Drupal\layout_builder\LayoutTempstoreRepositoryInterface $layout_tempstore_repository
* The layout tempstore repository.
*/
public function __construct(LayoutTempstoreRepositoryInterface $layout_tempstore_repository) {
$this->layoutTempstoreRepository = $layout_tempstore_repository;
}
/**
* {@inheritdoc}
*/
public function enhance(array $defaults, Request $request) {
$parameters = $defaults[RouteObjectInterface::ROUTE_OBJECT]->getOption('parameters');
if (isset($parameters['section_storage']['layout_builder_tempstore']) && isset($defaults['section_storage']) && $defaults['section_storage'] instanceof SectionStorageInterface) {
$defaults['section_storage'] = $this->layoutTempstoreRepository->get($defaults['section_storage']);
}
return $defaults;
}
}

View File

@@ -0,0 +1,454 @@
<?php
namespace Drupal\layout_builder;
use Drupal\Core\Config\Entity\ThirdPartySettingsInterface;
use Drupal\Core\Plugin\PreviewAwarePluginInterface;
use Drupal\Core\Render\Element;
/**
* Provides a domain object for layout sections.
*
* A section consists of three parts:
* - The layout plugin ID for the layout applied to the section (for example,
* 'layout_onecol').
* - An array of settings for the layout plugin.
* - An array of components that can be rendered in the section.
*
* @see \Drupal\Core\Layout\LayoutDefinition
* @see \Drupal\layout_builder\SectionComponent
*/
class Section implements ThirdPartySettingsInterface {
/**
* The layout plugin ID.
*
* @var string
*/
protected $layoutId;
/**
* The layout plugin settings.
*
* @var array
*/
protected $layoutSettings = [];
/**
* An array of components, keyed by UUID.
*
* @var \Drupal\layout_builder\SectionComponent[]
*/
protected $components = [];
/**
* Third party settings.
*
* An array of key/value pairs keyed by provider.
*
* @var array[]
*/
protected $thirdPartySettings = [];
/**
* Constructs a new Section.
*
* @param string $layout_id
* The layout plugin ID.
* @param array $layout_settings
* (optional) The layout plugin settings.
* @param \Drupal\layout_builder\SectionComponent[] $components
* (optional) The components.
* @param array[] $third_party_settings
* (optional) Any third party settings.
*/
public function __construct($layout_id, array $layout_settings = [], array $components = [], array $third_party_settings = []) {
$this->layoutId = $layout_id;
$this->layoutSettings = $layout_settings;
foreach ($components as $component) {
$this->setComponent($component);
}
$this->thirdPartySettings = $third_party_settings;
}
/**
* Returns the renderable array for this section.
*
* @param \Drupal\Core\Plugin\Context\ContextInterface[] $contexts
* An array of available contexts.
* @param bool $in_preview
* TRUE if the section is being previewed, FALSE otherwise.
*
* @return array
* A renderable array representing the content of the section.
*/
public function toRenderArray(array $contexts = [], $in_preview = FALSE) {
$regions = [];
foreach ($this->getComponents() as $component) {
if ($output = $component->toRenderArray($contexts, $in_preview)) {
$regions[$component->getRegion()][$component->getUuid()] = $output;
}
}
$layout = $this->getLayout($contexts);
if ($layout instanceof PreviewAwarePluginInterface) {
$layout->setInPreview($in_preview);
}
$build = $layout->build($regions);
// If an entity was used to build the layout, store it on the build.
if (!Element::isEmpty($build) && isset($contexts['layout_builder.entity'])) {
$build['#entity'] = $contexts['layout_builder.entity']->getContextValue();
}
return $build;
}
/**
* Gets the layout plugin for this section.
*
* @param \Drupal\Core\Plugin\Context\ContextInterface[] $contexts
* An array of available contexts.
*
* @return \Drupal\Core\Layout\LayoutInterface
* The layout plugin.
*/
public function getLayout(array $contexts = []) {
$layout = $this->layoutPluginManager()->createInstance($this->getLayoutId(), $this->layoutSettings);
if ($contexts) {
$this->contextHandler()->applyContextMapping($layout, $contexts);
}
return $layout;
}
/**
* Gets the layout plugin ID for this section.
*
* @return string
* The layout plugin ID.
*
* @internal
* This method should only be used by code responsible for storing the data.
*/
public function getLayoutId() {
return $this->layoutId;
}
/**
* Gets the layout plugin settings for this section.
*
* @return mixed[]
* The layout plugin settings.
*
* @internal
* This method should only be used by code responsible for storing the data.
*/
public function getLayoutSettings() {
return $this->getLayout()->getConfiguration();
}
/**
* Sets the layout plugin settings for this section.
*
* @param mixed[] $layout_settings
* The layout plugin settings.
*
* @return $this
*/
public function setLayoutSettings(array $layout_settings) {
$this->layoutSettings = $layout_settings;
return $this;
}
/**
* Gets the default region.
*
* @return string
* The machine-readable name of the default region.
*/
public function getDefaultRegion() {
return $this->layoutPluginManager()->getDefinition($this->getLayoutId())->getDefaultRegion();
}
/**
* Returns the components of the section.
*
* @return \Drupal\layout_builder\SectionComponent[]
* An array of components, keyed by the component UUID.
*/
public function getComponents() {
return $this->components;
}
/**
* Gets the component for a given UUID.
*
* @param string $uuid
* The UUID of the component to retrieve.
*
* @return \Drupal\layout_builder\SectionComponent
* The component.
*
* @throws \InvalidArgumentException
* Thrown when the expected UUID does not exist.
*/
public function getComponent($uuid) {
if (!isset($this->components[$uuid])) {
throw new \InvalidArgumentException(sprintf('Invalid UUID "%s"', $uuid));
}
return $this->components[$uuid];
}
/**
* Helper method to set a component.
*
* @param \Drupal\layout_builder\SectionComponent $component
* The component.
*
* @return $this
*/
protected function setComponent(SectionComponent $component) {
$this->components[$component->getUuid()] = $component;
return $this;
}
/**
* Removes a given component from a region.
*
* @param string $uuid
* The UUID of the component to remove.
*
* @return $this
*/
public function removeComponent($uuid) {
unset($this->components[$uuid]);
return $this;
}
/**
* Appends a component to the end of a region.
*
* @param \Drupal\layout_builder\SectionComponent $component
* The component being appended.
*
* @return $this
*/
public function appendComponent(SectionComponent $component) {
$component->setWeight($this->getNextHighestWeight($component->getRegion()));
$this->setComponent($component);
return $this;
}
/**
* Returns the next highest weight of the component in a region.
*
* @param string $region
* The region name.
*
* @return int
* A number higher than the highest weight of the component in the region.
*/
protected function getNextHighestWeight($region) {
$components = $this->getComponentsByRegion($region);
$weights = array_map(function (SectionComponent $component) {
return $component->getWeight();
}, $components);
return $weights ? max($weights) + 1 : 0;
}
/**
* Gets the components for a specific region.
*
* @param string $region
* The region name.
*
* @return \Drupal\layout_builder\SectionComponent[]
* An array of components in the specified region, sorted by weight.
*/
public function getComponentsByRegion($region) {
$components = array_filter($this->getComponents(), function (SectionComponent $component) use ($region) {
return $component->getRegion() === $region;
});
uasort($components, function (SectionComponent $a, SectionComponent $b) {
return $a->getWeight() <=> $b->getWeight();
});
return $components;
}
/**
* Inserts a component after a specified existing component.
*
* @param string $preceding_uuid
* The UUID of the existing component to insert after.
* @param \Drupal\layout_builder\SectionComponent $component
* The component being inserted.
*
* @return $this
*
* @throws \InvalidArgumentException
* Thrown when the expected UUID does not exist.
*/
public function insertAfterComponent($preceding_uuid, SectionComponent $component) {
// Find the delta of the specified UUID.
$uuids = array_keys($this->getComponentsByRegion($component->getRegion()));
$delta = array_search($preceding_uuid, $uuids, TRUE);
if ($delta === FALSE) {
throw new \InvalidArgumentException(sprintf('Invalid preceding UUID "%s"', $preceding_uuid));
}
return $this->insertComponent($delta + 1, $component);
}
/**
* Inserts a component at a specified delta.
*
* @param int $delta
* The zero-based delta in which to insert the component.
* @param \Drupal\layout_builder\SectionComponent $new_component
* The component being inserted.
*
* @return $this
*
* @throws \OutOfBoundsException
* Thrown when the specified delta is invalid.
*/
public function insertComponent($delta, SectionComponent $new_component) {
$components = $this->getComponentsByRegion($new_component->getRegion());
$count = count($components);
if ($delta > $count) {
throw new \OutOfBoundsException(sprintf('Invalid delta "%s" for the "%s" component', $delta, $new_component->getUuid()));
}
// If the delta is the end of the list, append the component instead.
if ($delta === $count) {
return $this->appendComponent($new_component);
}
// Find the weight of the component that exists at the specified delta.
$weight = array_values($components)[$delta]->getWeight();
$this->setComponent($new_component->setWeight($weight++));
// Increase the weight of every subsequent component.
foreach (array_slice($components, $delta) as $component) {
$component->setWeight($weight++);
}
return $this;
}
/**
* Wraps the layout plugin manager.
*
* @return \Drupal\Core\Layout\LayoutPluginManagerInterface
* The layout plugin manager.
*/
protected function layoutPluginManager() {
return \Drupal::service('plugin.manager.core.layout');
}
/**
* Returns an array representation of the section.
*
* Only use this method if you are implementing custom storage for sections.
*
* @return array
* An array representation of the section component.
*/
public function toArray() {
return [
'layout_id' => $this->getLayoutId(),
'layout_settings' => $this->getLayoutSettings(),
'components' => array_map(function (SectionComponent $component) {
return $component->toArray();
}, $this->getComponents()),
'third_party_settings' => $this->thirdPartySettings,
];
}
/**
* Creates an object from an array representation of the section.
*
* Only use this method if you are implementing custom storage for sections.
*
* @param array $section
* An array of section data in the format returned by ::toArray().
*
* @return static
* The section object.
*/
public static function fromArray(array $section) {
// Ensure expected array keys are present.
$section += [
'layout_id' => '',
'layout_settings' => [],
'components' => [],
'third_party_settings' => [],
];
return new static(
$section['layout_id'],
$section['layout_settings'],
array_map([SectionComponent::class, 'fromArray'], $section['components']),
$section['third_party_settings']
);
}
/**
* Magic method: Implements a deep clone.
*/
public function __clone() {
foreach ($this->components as $uuid => $component) {
$this->components[$uuid] = clone $component;
}
}
/**
* {@inheritdoc}
*/
public function getThirdPartySetting($provider, $key, $default = NULL) {
return $this->thirdPartySettings[$provider][$key] ?? $default;
}
/**
* {@inheritdoc}
*/
public function getThirdPartySettings($provider) {
return $this->thirdPartySettings[$provider] ?? [];
}
/**
* {@inheritdoc}
*/
public function setThirdPartySetting($provider, $key, $value) {
$this->thirdPartySettings[$provider][$key] = $value;
return $this;
}
/**
* {@inheritdoc}
*/
public function unsetThirdPartySetting($provider, $key) {
unset($this->thirdPartySettings[$provider][$key]);
// If the third party is no longer storing any information, completely
// remove the array holding the settings for this provider.
if (empty($this->thirdPartySettings[$provider])) {
unset($this->thirdPartySettings[$provider]);
}
return $this;
}
/**
* {@inheritdoc}
*/
public function getThirdPartyProviders() {
return array_keys($this->thirdPartySettings);
}
/**
* Wraps the context handler.
*
* @return \Drupal\Core\Plugin\Context\ContextHandlerInterface
* The context handler.
*/
protected function contextHandler() {
return \Drupal::service('context.handler');
}
}

View File

@@ -0,0 +1,320 @@
<?php
namespace Drupal\layout_builder;
use Drupal\Component\Plugin\Exception\PluginException;
use Drupal\Core\Plugin\ContextAwarePluginInterface;
use Drupal\layout_builder\Event\SectionComponentBuildRenderArrayEvent;
/**
* Provides a value object for a section component.
*
* A component represents the smallest part of a layout (for example, a block).
* Components wrap a renderable plugin, currently using
* \Drupal\Core\Block\BlockPluginInterface, and contain the layout region
* within the section layout where the component will be rendered.
*
* @see \Drupal\Core\Layout\LayoutDefinition
* @see \Drupal\layout_builder\Section
* @see \Drupal\layout_builder\SectionStorageInterface
*/
class SectionComponent {
/**
* The UUID of the component.
*
* @var string
*/
protected $uuid;
/**
* The region the component is placed in.
*
* @var string
*/
protected $region;
/**
* An array of plugin configuration.
*
* @var mixed[]
*/
protected $configuration;
/**
* The weight of the component.
*
* @var int
*/
protected $weight = 0;
/**
* Any additional properties and values.
*
* @var mixed[]
*/
protected $additional = [];
/**
* Constructs a new SectionComponent.
*
* @param string $uuid
* The UUID.
* @param string $region
* The region.
* @param mixed[] $configuration
* The plugin configuration.
* @param mixed[] $additional
* An additional values.
*/
public function __construct($uuid, $region, array $configuration = [], array $additional = []) {
$this->uuid = $uuid;
$this->region = $region;
$this->configuration = $configuration;
$this->additional = $additional;
}
/**
* Returns the renderable array for this component.
*
* @param \Drupal\Core\Plugin\Context\ContextInterface[] $contexts
* An array of available contexts.
* @param bool $in_preview
* TRUE if the component is being previewed, FALSE otherwise.
*
* @return array
* A renderable array representing the content of the component.
*/
public function toRenderArray(array $contexts = [], $in_preview = FALSE) {
$event = new SectionComponentBuildRenderArrayEvent($this, $contexts, $in_preview);
$this->eventDispatcher()->dispatch($event, LayoutBuilderEvents::SECTION_COMPONENT_BUILD_RENDER_ARRAY);
$output = $event->getBuild();
$event->getCacheableMetadata()->applyTo($output);
return $output;
}
/**
* Gets any arbitrary property for the component.
*
* @param string $property
* The property to retrieve.
*
* @return mixed
* The value for that property, or NULL if the property does not exist.
*/
public function get($property) {
if (property_exists($this, $property)) {
$value = $this->{$property} ?? NULL;
}
else {
$value = $this->additional[$property] ?? NULL;
}
return $value;
}
/**
* Sets a value to an arbitrary property for the component.
*
* @param string $property
* The property to use for the value.
* @param mixed $value
* The value to set.
*
* @return $this
*/
public function set($property, $value) {
if (property_exists($this, $property)) {
$this->{$property} = $value;
}
else {
$this->additional[$property] = $value;
}
return $this;
}
/**
* Gets the region for the component.
*
* @return string
* The region.
*/
public function getRegion() {
return $this->region;
}
/**
* Sets the region for the component.
*
* @param string $region
* The region.
*
* @return $this
*/
public function setRegion($region) {
$this->region = $region;
return $this;
}
/**
* Gets the weight of the component.
*
* @return int
* The zero-based weight of the component.
*
* @throws \UnexpectedValueException
* Thrown if the weight was never set.
*/
public function getWeight() {
return $this->weight;
}
/**
* Sets the weight of the component.
*
* @param int $weight
* The zero-based weight of the component.
*
* @return $this
*/
public function setWeight($weight) {
$this->weight = $weight;
return $this;
}
/**
* Gets the component plugin configuration.
*
* @return mixed[]
* The component plugin configuration.
*/
protected function getConfiguration() {
return $this->configuration;
}
/**
* Sets the plugin configuration.
*
* @param mixed[] $configuration
* The plugin configuration.
*
* @return $this
*/
public function setConfiguration(array $configuration) {
$this->configuration = $configuration;
return $this;
}
/**
* Gets the plugin ID.
*
* @return string
* The plugin ID.
*
* @throws \Drupal\Component\Plugin\Exception\PluginException
* Thrown if the plugin ID cannot be found.
*/
public function getPluginId() {
if (empty($this->configuration['id'])) {
throw new PluginException(sprintf('No plugin ID specified for component with "%s" UUID', $this->uuid));
}
return $this->configuration['id'];
}
/**
* Gets the UUID for this component.
*
* @return string
* The UUID.
*/
public function getUuid() {
return $this->uuid;
}
/**
* Gets the plugin for this component.
*
* @param \Drupal\Core\Plugin\Context\ContextInterface[] $contexts
* An array of contexts to set on the plugin.
*
* @return \Drupal\Component\Plugin\PluginInspectionInterface
* The plugin.
*/
public function getPlugin(array $contexts = []) {
$plugin = $this->pluginManager()->createInstance($this->getPluginId(), $this->getConfiguration());
if ($contexts && $plugin instanceof ContextAwarePluginInterface) {
$this->contextHandler()->applyContextMapping($plugin, $contexts);
}
return $plugin;
}
/**
* Wraps the component plugin manager.
*
* @return \Drupal\Core\Block\BlockManagerInterface
* The plugin manager.
*/
protected function pluginManager() {
// @todo Figure out the best way to unify fields and blocks and components
// in https://www.drupal.org/node/1875974.
return \Drupal::service('plugin.manager.block');
}
/**
* Wraps the context handler.
*
* @return \Drupal\Core\Plugin\Context\ContextHandlerInterface
* The context handler.
*/
protected function contextHandler() {
return \Drupal::service('context.handler');
}
/**
* Wraps the event dispatcher.
*
* @return \Symfony\Contracts\EventDispatcher\EventDispatcherInterface
* The event dispatcher.
*/
protected function eventDispatcher() {
return \Drupal::service('event_dispatcher');
}
/**
* Returns an array representation of the section component.
*
* Only use this method if you are implementing custom storage for sections.
*
* @return array
* An array representation of the section component.
*/
public function toArray() {
return [
'uuid' => $this->getUuid(),
'region' => $this->getRegion(),
'configuration' => $this->getConfiguration(),
'weight' => $this->getWeight(),
'additional' => $this->additional,
];
}
/**
* Creates an object from an array representation of the section component.
*
* Only use this method if you are implementing custom storage for sections.
*
* @param array $component
* An array of section component data in the format returned by ::toArray().
*
* @return static
* The section component object.
*/
public static function fromArray(array $component) {
return (new static(
$component['uuid'],
$component['region'],
$component['configuration'],
$component['additional']
))->setWeight($component['weight']);
}
}

View File

@@ -0,0 +1,84 @@
<?php
namespace Drupal\layout_builder;
/**
* Defines the interface for an object that stores layout sections.
*
* @see \Drupal\layout_builder\Section
*/
interface SectionListInterface extends \Countable {
/**
* Gets the layout sections.
*
* @return \Drupal\layout_builder\Section[]
* A sequentially and numerically keyed array of section objects.
*/
public function getSections();
/**
* Gets a domain object for the layout section.
*
* @param int $delta
* The delta of the section.
*
* @return \Drupal\layout_builder\Section
* The layout section.
*/
public function getSection($delta);
/**
* Appends a new section to the end of the list.
*
* @param \Drupal\layout_builder\Section $section
* The section to append.
*
* @return $this
*/
public function appendSection(Section $section);
/**
* Inserts a new section at a given delta.
*
* If a section exists at the given index, the section at that position and
* others after it are shifted backward.
*
* @param int $delta
* The delta of the section.
* @param \Drupal\layout_builder\Section $section
* The section to insert.
*
* @return $this
*/
public function insertSection($delta, Section $section);
/**
* Removes the section at the given delta.
*
* As sections are stored sequentially and numerically this will re-key every
* subsequent section, shifting them forward.
*
* @param int $delta
* The delta of the section.
*
* @return $this
*/
public function removeSection($delta);
/**
* Removes all of the sections.
*
* @param bool $set_blank
* (optional) The default implementation of section lists differentiates
* between a list that has never contained any sections and a list that has
* purposefully had all sections removed in order to remain blank. Passing
* TRUE will mirror ::removeSection() by tracking this as a blank list.
* Passing FALSE will reset the list as though it had never contained any
* sections at all. Defaults to FALSE.
*
* @return $this
*/
public function removeAllSections($set_blank = FALSE);
}

View File

@@ -0,0 +1,186 @@
<?php
namespace Drupal\layout_builder;
/**
* Provides a trait for maintaining a list of sections.
*
* @see \Drupal\layout_builder\SectionListInterface
*/
trait SectionListTrait {
/**
* Stores the information for all sections.
*
* Implementations of this method are expected to call array_values() to rekey
* the list of sections.
*
* @param \Drupal\layout_builder\Section[] $sections
* An array of section objects.
*
* @return $this
*/
abstract protected function setSections(array $sections);
/**
* {@inheritdoc}
*/
#[\ReturnTypeWillChange]
public function count() {
if ($this->hasBlankSection()) {
return 0;
}
return count($this->getSections());
}
/**
* {@inheritdoc}
*/
public function getSection($delta) {
if (!$this->hasSection($delta)) {
throw new \OutOfBoundsException(sprintf('Invalid delta "%s"', $delta));
}
return $this->getSections()[$delta];
}
/**
* Sets the section for the given delta on the display.
*
* @param int $delta
* The delta of the section.
* @param \Drupal\layout_builder\Section $section
* The layout section.
*
* @return $this
*/
protected function setSection($delta, Section $section) {
$sections = $this->getSections();
$sections[$delta] = $section;
$this->setSections($sections);
return $this;
}
/**
* {@inheritdoc}
*/
public function appendSection(Section $section) {
$delta = $this->count();
$this->setSection($delta, $section);
return $this;
}
/**
* {@inheritdoc}
*/
public function insertSection($delta, Section $section) {
// Clear the section list if there is currently a blank section.
if ($this->hasBlankSection()) {
$this->removeAllSections();
}
if ($this->hasSection($delta)) {
// @todo Use https://www.drupal.org/node/66183 once resolved.
$start = array_slice($this->getSections(), 0, $delta);
$end = array_slice($this->getSections(), $delta);
$this->setSections(array_merge($start, [$section], $end));
}
else {
$this->appendSection($section);
}
return $this;
}
/**
* Adds a blank section to the list.
*
* @return $this
*
* @see \Drupal\layout_builder\Plugin\Layout\BlankLayout
*/
protected function addBlankSection() {
if ($this->hasSection(0)) {
throw new \Exception('A blank section must only be added to an empty list');
}
$this->appendSection(new Section('layout_builder_blank'));
return $this;
}
/**
* Indicates if this section list contains a blank section.
*
* A blank section is used to differentiate the difference between a layout
* that has never been instantiated and one that has purposefully had all
* sections removed.
*
* @return bool
* TRUE if the section list contains a blank section, FALSE otherwise.
*
* @see \Drupal\layout_builder\Plugin\Layout\BlankLayout
*/
protected function hasBlankSection() {
// A blank section will only ever exist when the delta is 0, as added by
// ::removeSection().
return $this->hasSection(0) && $this->getSection(0)->getLayoutId() === 'layout_builder_blank';
}
/**
* {@inheritdoc}
*/
public function removeSection($delta) {
// Clear the section list if there is currently a blank section.
if ($this->hasBlankSection()) {
$this->removeAllSections();
}
$sections = $this->getSections();
unset($sections[$delta]);
$this->setSections($sections);
// Add a blank section when the last section is removed.
if (empty($sections)) {
$this->addBlankSection();
}
return $this;
}
/**
* {@inheritdoc}
*/
public function removeAllSections($set_blank = FALSE) {
$this->setSections([]);
if ($set_blank) {
$this->addBlankSection();
}
return $this;
}
/**
* Indicates if there is a section at the specified delta.
*
* @param int $delta
* The delta of the section.
*
* @return bool
* TRUE if there is a section for this delta, FALSE otherwise.
*/
protected function hasSection($delta) {
return isset($this->getSections()[$delta]);
}
/**
* Magic method: Implements a deep clone.
*/
public function __clone() {
$sections = $this->getSections();
foreach ($sections as $delta => $item) {
$sections[$delta] = clone $item;
}
$this->setSections($sections);
}
}

View File

@@ -0,0 +1,101 @@
<?php
namespace Drupal\layout_builder\SectionStorage;
use Drupal\Component\Plugin\Definition\ContextAwarePluginDefinitionInterface;
use Drupal\Component\Plugin\Definition\ContextAwarePluginDefinitionTrait;
use Drupal\Component\Plugin\Definition\PluginDefinition;
/**
* Provides section storage type plugin definition.
*/
class SectionStorageDefinition extends PluginDefinition implements ContextAwarePluginDefinitionInterface {
use ContextAwarePluginDefinitionTrait;
/**
* The plugin weight.
*
* @var int
*/
protected $weight = 0;
/**
* Any additional properties and values.
*
* @var array
*/
protected $additional = [];
/**
* LayoutDefinition constructor.
*
* @param array $definition
* An array of values from the annotation.
*/
public function __construct(array $definition = []) {
// If there are context definitions in the plugin definition, they should
// be added to this object using ::addContextDefinition() so that they can
// be manipulated using other ContextAwarePluginDefinitionInterface methods.
if (isset($definition['context_definitions'])) {
foreach ($definition['context_definitions'] as $name => $context_definition) {
$this->addContextDefinition($name, $context_definition);
}
unset($definition['context_definitions']);
}
foreach ($definition as $property => $value) {
$this->set($property, $value);
}
}
/**
* Gets any arbitrary property.
*
* @param string $property
* The property to retrieve.
*
* @return mixed
* The value for that property, or NULL if the property does not exist.
*/
public function get($property) {
if (property_exists($this, $property)) {
$value = $this->{$property} ?? NULL;
}
else {
$value = $this->additional[$property] ?? NULL;
}
return $value;
}
/**
* Sets a value to an arbitrary property.
*
* @param string $property
* The property to use for the value.
* @param mixed $value
* The value to set.
*
* @return $this
*/
public function set($property, $value) {
if (property_exists($this, $property)) {
$this->{$property} = $value;
}
else {
$this->additional[$property] = $value;
}
return $this;
}
/**
* Returns the plugin weight.
*
* @return int
* The plugin weight.
*/
public function getWeight() {
return $this->weight;
}
}

View File

@@ -0,0 +1,110 @@
<?php
namespace Drupal\layout_builder\SectionStorage;
use Drupal\Component\Plugin\Exception\ContextException;
use Drupal\Core\Cache\CacheBackendInterface;
use Drupal\Core\Cache\RefinableCacheableDependencyInterface;
use Drupal\Core\Extension\ModuleHandlerInterface;
use Drupal\Core\Plugin\Context\ContextHandlerInterface;
use Drupal\Core\Plugin\DefaultPluginManager;
use Drupal\layout_builder\Attribute\SectionStorage;
use Drupal\layout_builder\SectionStorageInterface;
/**
* Provides the Section Storage type plugin manager.
*
* Note that while this class extends \Drupal\Core\Plugin\DefaultPluginManager
* and includes many additional public methods, only some of them are available
* via \Drupal\layout_builder\SectionStorage\SectionStorageManagerInterface.
* While internally depending on the parent class is necessary, external code
* should only use the methods available on that interface.
*/
class SectionStorageManager extends DefaultPluginManager implements SectionStorageManagerInterface {
/**
* The context handler.
*
* @var \Drupal\Core\Plugin\Context\ContextHandlerInterface
*/
protected $contextHandler;
/**
* Constructs a new SectionStorageManager object.
*
* @param \Traversable $namespaces
* An object that implements \Traversable which contains the root paths
* keyed by the corresponding namespace to look for plugin implementations.
* @param \Drupal\Core\Cache\CacheBackendInterface $cache_backend
* Cache backend instance to use.
* @param \Drupal\Core\Extension\ModuleHandlerInterface $module_handler
* The module handler to invoke the alter hook with.
* @param \Drupal\Core\Plugin\Context\ContextHandlerInterface $context_handler
* The context handler.
*/
public function __construct(\Traversable $namespaces, CacheBackendInterface $cache_backend, ModuleHandlerInterface $module_handler, ContextHandlerInterface $context_handler) {
parent::__construct('Plugin/SectionStorage', $namespaces, $module_handler, SectionStorageInterface::class, SectionStorage::class, '\Drupal\layout_builder\Annotation\SectionStorage');
$this->contextHandler = $context_handler;
$this->alterInfo('layout_builder_section_storage');
$this->setCacheBackend($cache_backend, 'layout_builder_section_storage_plugins');
}
/**
* {@inheritdoc}
*/
protected function findDefinitions() {
$definitions = parent::findDefinitions();
// Sort the definitions by their weight while preserving the original order
// for those with matching weights.
$weights = array_map(function (SectionStorageDefinition $definition) {
return $definition->getWeight();
}, $definitions);
$ids = array_keys($definitions);
array_multisort($weights, $ids, $definitions);
return $definitions;
}
/**
* {@inheritdoc}
*/
public function load($type, array $contexts = []) {
$plugin = $this->loadEmpty($type);
try {
$this->contextHandler->applyContextMapping($plugin, $contexts);
}
catch (ContextException $e) {
return NULL;
}
return $plugin;
}
/**
* {@inheritdoc}
*/
public function findByContext(array $contexts, RefinableCacheableDependencyInterface $cacheability) {
$storage_types = array_keys($this->contextHandler->filterPluginDefinitionsByContexts($contexts, $this->getDefinitions()));
// Add the manager as a cacheable dependency in order to vary by changes to
// the plugin definitions.
$cacheability->addCacheableDependency($this);
foreach ($storage_types as $type) {
$plugin = $this->load($type, $contexts);
if ($plugin && $plugin->isApplicable($cacheability)) {
return $plugin;
}
}
return NULL;
}
/**
* {@inheritdoc}
*/
public function loadEmpty($type) {
return $this->createInstance($type);
}
}

View File

@@ -0,0 +1,62 @@
<?php
namespace Drupal\layout_builder\SectionStorage;
use Drupal\Component\Plugin\Discovery\DiscoveryInterface;
use Drupal\Core\Cache\RefinableCacheableDependencyInterface;
/**
* Provides the interface for a plugin manager of section storage types.
*/
interface SectionStorageManagerInterface extends DiscoveryInterface {
/**
* Loads a section storage with the provided contexts applied.
*
* @param string $type
* The section storage type.
* @param \Drupal\Component\Plugin\Context\ContextInterface[] $contexts
* (optional) The contexts available for this storage to use.
*
* @return \Drupal\layout_builder\SectionStorageInterface|null
* The section storage or NULL if its context requirements are not met.
*/
public function load($type, array $contexts = []);
/**
* Finds the section storage to load based on available contexts.
*
* @param \Drupal\Component\Plugin\Context\ContextInterface[] $contexts
* The contexts which should be used to determine which storage to return.
* @param \Drupal\Core\Cache\RefinableCacheableDependencyInterface $cacheability
* Refinable cacheability object, which will be populated based on the
* cacheability of each section storage candidate. After calling this method
* this parameter will reflect the cacheability information used to
* determine the correct section storage. This must be associated with any
* output that uses the result of this method.
*
* @return \Drupal\layout_builder\SectionStorageInterface|null
* The section storage if one matched all contexts, or NULL otherwise.
*
* @see \Drupal\Core\Cache\RefinableCacheableDependencyInterface
*/
public function findByContext(array $contexts, RefinableCacheableDependencyInterface $cacheability);
/**
* Loads a section storage with no associated section list.
*
* @param string $type
* The type of the section storage being instantiated.
*
* @return \Drupal\layout_builder\SectionStorageInterface
* The section storage.
*
* @internal
* Section storage relies on context to load section lists. Use ::load()
* when that context is available. This method is intended for use by
* collaborators of the plugins in build-time situations when section
* storage type must be consulted.
*/
public function loadEmpty($type);
}

View File

@@ -0,0 +1,153 @@
<?php
namespace Drupal\layout_builder;
use Drupal\Component\Plugin\PluginInspectionInterface;
use Drupal\Core\Access\AccessibleInterface;
use Drupal\Core\Cache\RefinableCacheableDependencyInterface;
use Drupal\Core\Plugin\ContextAwarePluginInterface;
use Drupal\Core\Session\AccountInterface;
use Symfony\Component\Routing\RouteCollection;
/**
* Defines an interface for Section Storage type plugins.
*/
interface SectionStorageInterface extends SectionListInterface, PluginInspectionInterface, ContextAwarePluginInterface, AccessibleInterface {
/**
* Returns an identifier for this storage.
*
* @return string
* The unique identifier for this storage.
*/
public function getStorageId();
/**
* Returns the type of this storage.
*
* Used in conjunction with the storage ID.
*
* @return string
* The type of storage.
*/
public function getStorageType();
/**
* Provides the routes needed for Layout Builder UI.
*
* Allows the plugin to add or alter routes during the route building process.
* \Drupal\layout_builder\Routing\LayoutBuilderRoutesTrait is provided for the
* typical use case of building a standard Layout Builder UI.
*
* @param \Symfony\Component\Routing\RouteCollection $collection
* The route collection.
*
* @see \Drupal\Core\Routing\RoutingEvents::ALTER
*/
public function buildRoutes(RouteCollection $collection);
/**
* Gets the URL used when redirecting away from the Layout Builder UI.
*
* @return \Drupal\Core\Url
* The URL object.
*/
public function getRedirectUrl();
/**
* Gets the URL used to display the Layout Builder UI.
*
* @param string $rel
* (optional) The link relationship type, for example: 'view' or 'disable'.
* Defaults to 'view'.
*
* @return \Drupal\Core\Url
* The URL object.
*/
public function getLayoutBuilderUrl($rel = 'view');
/**
* Derives the available plugin contexts from route values.
*
* This should only be called during section storage instantiation,
* specifically for use by the routing system. For all non-routing usages, use
* \Drupal\Component\Plugin\ContextAwarePluginInterface::getContextValue().
*
* @param mixed $value
* The raw value.
* @param mixed $definition
* The parameter definition provided in the route options.
* @param string $name
* The name of the parameter.
* @param array $defaults
* The route defaults array.
*
* @return \Drupal\Core\Plugin\Context\ContextInterface[]
* The available plugin contexts.
*
* @see \Drupal\Core\ParamConverter\ParamConverterInterface::convert()
*/
public function deriveContextsFromRoute($value, $definition, $name, array $defaults);
/**
* Gets contexts for use during preview.
*
* When not in preview, ::getContexts() will be used.
*
* @return \Drupal\Core\Plugin\Context\ContextInterface[]
* The plugin contexts suitable for previewing.
*/
public function getContextsDuringPreview();
/**
* Gets the label for the object using the sections.
*
* @return string
* The label, or NULL if there is no label defined.
*/
public function label();
/**
* Saves the sections.
*
* @return int
* SAVED_NEW or SAVED_UPDATED is returned depending on the operation
* performed.
*/
public function save();
/**
* Determines if this section storage is applicable for the current contexts.
*
* @param \Drupal\Core\Cache\RefinableCacheableDependencyInterface $cacheability
* Refinable cacheability object, typically provided by the section storage
* manager. When implementing this method, populate $cacheability with any
* information that affects whether this storage is applicable.
*
* @return bool
* TRUE if this section storage is applicable, FALSE otherwise.
*
* @internal
* This method is intended to be called by
* \Drupal\layout_builder\SectionStorage\SectionStorageManagerInterface::findByContext().
*
* @see \Drupal\Core\Cache\RefinableCacheableDependencyInterface
*/
public function isApplicable(RefinableCacheableDependencyInterface $cacheability);
/**
* Overrides \Drupal\Component\Plugin\PluginInspectionInterface::getPluginDefinition().
*
* @return \Drupal\layout_builder\SectionStorage\SectionStorageDefinition
* The section storage definition.
*/
public function getPluginDefinition();
/**
* Overrides \Drupal\Core\Access\AccessibleInterface::access().
*
* @ingroup layout_builder_access
*/
public function access($operation, ?AccountInterface $account = NULL, $return_as_object = FALSE);
}

View File

@@ -0,0 +1,20 @@
<?php
namespace Drupal\layout_builder;
/**
* Provides an interface that allows an object to provide its own tempstore key.
*
* @todo Move to \Drupal\Core\TempStore in https://www.drupal.org/node/3026957.
*/
interface TempStoreIdentifierInterface {
/**
* Gets a string suitable for use as a tempstore key.
*
* @return string
* A string to be used as the key for a tempstore item.
*/
public function getTempstoreKey();
}