Subject: [PATCH] Frontend-rendered layout-builder previews --- Index: modules/lupus_decoupled_layout_builder/lupus_decoupled_layout_builder.services.yml IDEA additional info: Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP <+>UTF-8 =================================================================== diff --git a/modules/lupus_decoupled_layout_builder/lupus_decoupled_layout_builder.services.yml b/modules/lupus_decoupled_layout_builder/lupus_decoupled_layout_builder.services.yml --- a/modules/lupus_decoupled_layout_builder/lupus_decoupled_layout_builder.services.yml (revision 5d718c2794021a3ece415a83bc5aaf15511580ac) +++ b/modules/lupus_decoupled_layout_builder/lupus_decoupled_layout_builder.services.yml (date 1729713662334) @@ -1,4 +1,9 @@ services: + access_check.entity.lupus_decoupled_layout_builder_access: + class: Drupal\lupus_decoupled_layout_builder\Access\LupusDecoupledLayoutBuilderAccessCheck + tags: + - { name: access_check, applies_to: _lupus_decoupled_layout_builder_access } + arguments: ['@plugin.manager.layout_builder.section_storage'] lupus_decoupled_layout_builder.route_subscriber: class: Drupal\lupus_decoupled_layout_builder\EventSubscriber\LupusDecoupledLayoutBuilderRouteSubscriber tags: Index: modules/lupus_decoupled_layout_builder/lupus_decoupled_layout_builder.routing.yml IDEA additional info: Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP <+>UTF-8 =================================================================== diff --git a/modules/lupus_decoupled_layout_builder/lupus_decoupled_layout_builder.routing.yml b/modules/lupus_decoupled_layout_builder/lupus_decoupled_layout_builder.routing.yml new file mode 100644 --- /dev/null (date 1729713662334) +++ b/modules/lupus_decoupled_layout_builder/lupus_decoupled_layout_builder.routing.yml (date 1729713662334) @@ -0,0 +1,12 @@ +## A URL to return data for a draft layout builder block. +lupus_decoupled_layout_builder.preview_layout_builder_block: + path: '/layout-preview/node/{node}/block/{block_data}/{view_mode}' + defaults: + _controller: '\Drupal\lupus_decoupled_layout_builder\Controller\LupusDecoupledLayoutBuilderController::previewBlock' + requirements: + _lupus_decoupled_layout_builder_access: 'view' + options: + _admin_route: TRUE + parameters: + node: + type: entity:node Index: modules/lupus_decoupled_layout_builder/lupus_decoupled_layout_builder.libraries.yml IDEA additional info: Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP <+>UTF-8 =================================================================== diff --git a/modules/lupus_decoupled_layout_builder/lupus_decoupled_layout_builder.libraries.yml b/modules/lupus_decoupled_layout_builder/lupus_decoupled_layout_builder.libraries.yml new file mode 100644 --- /dev/null (date 1729713662334) +++ b/modules/lupus_decoupled_layout_builder/lupus_decoupled_layout_builder.libraries.yml (date 1729713662334) @@ -0,0 +1,10 @@ +iframe_resizer: + remote: https://github.com/davidjbradshaw/iframe-resizer/ + version: 4.3.3 + header: true + license: + name: MIT + url: https://github.com/davidjbradshaw/iframe-resizer/blob/master/LICENSE + gpl-compatible: true + js: + https://cdn.jsdelivr.net/npm/iframe-resizer@4.3.6/js/iframeResizer.js: { type: external, minified: true } Index: modules/lupus_decoupled_layout_builder/examples/vue3/pages/layout-preview/node/[node]/block/[uuid]/[ViewMode].vue IDEA additional info: Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP <+>UTF-8 =================================================================== diff --git a/modules/lupus_decoupled_layout_builder/examples/vue3/pages/layout-preview/node/[node]/block/[uuid]/[ViewMode].vue b/modules/lupus_decoupled_layout_builder/examples/vue3/pages/layout-preview/node/[node]/block/[uuid]/[ViewMode].vue new file mode 100644 --- /dev/null (date 1729713662334) +++ b/modules/lupus_decoupled_layout_builder/examples/vue3/pages/layout-preview/node/[node]/block/[uuid]/[ViewMode].vue (date 1729713662334) @@ -0,0 +1,14 @@ + + + Index: modules/lupus_decoupled_layout_builder/lupus_decoupled_layout_builder.module IDEA additional info: Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP <+>UTF-8 =================================================================== diff --git a/modules/lupus_decoupled_layout_builder/lupus_decoupled_layout_builder.module b/modules/lupus_decoupled_layout_builder/lupus_decoupled_layout_builder.module --- a/modules/lupus_decoupled_layout_builder/lupus_decoupled_layout_builder.module (revision 5d718c2794021a3ece415a83bc5aaf15511580ac) +++ b/modules/lupus_decoupled_layout_builder/lupus_decoupled_layout_builder.module (date 1729713662334) @@ -30,3 +30,70 @@ $implementations['lupus_decoupled_layout_builder'] = $group; } } + +/** + * Implements hook_preprocess_HOOK() for block content previews. + */ +function lupus_decoupled_layout_builder_preprocess_block(&$variables) { + if ($variables['in_preview']) { + if (!empty($variables['attributes']['data-layout-block-uuid'])) { + $storage = \Drupal::request()->attributes->get('section_storage'); + $storage_contexts = $storage->getContexts(); + // Bail if there's no node context (e.g. LB default template). + if (empty($storage_contexts['entity'])) { + return; + } + $node = $storage->getContextValue('entity'); + + if ($node instanceof \Drupal\node\NodeInterface) { + $frontend_base_url = \Drupal::service('lupus_decoupled_ce_api.base_url_provider') + ->getFrontendBaseUrl(); + + // Use the blocks view mode if it has one. + $view_mode = isset($variables['content']['#view_mode']) ? $variables['content']['#view_mode'] : 'full'; + + // We want the frontend to render the title, hide the one by Drupal. + if ($variables['configuration']['label_display'] == 'visible') { + $variables['configuration']['label_display'] = 'hidden'; + $variables['label'] = ''; + } + + // Set the data property. + if ($variables['base_plugin_id'] == 'inline_block') { + $data = 'inline_block:' . $variables['attributes']['data-layout-block-uuid']; + } + else if ($variables['base_plugin_id'] == 'field_block') { + if (!empty($variables['content']['#field_name'])) { + $data = 'field_block:' . $variables['content']['#field_name']; + } + else { + $field_name = explode(':', $variables['plugin_id']); + $data = 'field_block:' . end($field_name); + } + } + else { + $data = $variables['plugin_id'] . ':' . $variables['attributes']['data-layout-block-uuid']; + } + + $markup = [ + 'iframe' => [ + '#type' => 'html_tag', + '#tag' => 'iframe', + '#attributes' => [ + 'src' => $frontend_base_url . '/layout-preview/node/' . $node->id() . '/block/' . $data . '/' . $view_mode, + 'style' => 'border:none;width:100%;height:0;overflow:hidden;', + 'id' => 'iframe-' . $variables['attributes']['data-layout-block-uuid'], + ], + ], + 'script' => [ + '#type' => 'html_tag', + '#tag' => 'script', + '#value' => '', + ], + ]; + $variables['content'] = ['#markup' => \Drupal::service('renderer')->render($markup)]; + $variables['#attached']['library'][] = 'lupus_decoupled_layout_builder/iframe_resizer'; + } + } + } +} \ No newline at end of file Index: modules/lupus_decoupled_layout_builder/src/Access/LupusDecoupledLayoutBuilderAccessCheck.php IDEA additional info: Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP <+>UTF-8 =================================================================== diff --git a/modules/lupus_decoupled_layout_builder/src/Access/LupusDecoupledLayoutBuilderAccessCheck.php b/modules/lupus_decoupled_layout_builder/src/Access/LupusDecoupledLayoutBuilderAccessCheck.php new file mode 100644 --- /dev/null (date 1729713662337) +++ b/modules/lupus_decoupled_layout_builder/src/Access/LupusDecoupledLayoutBuilderAccessCheck.php (date 1729713662337) @@ -0,0 +1,78 @@ +sectionStorage = $section_storage; + } + + /** + * Checks routing access to the layout. + * + * @param \Drupal\node\NodeInterface $node + * The node the block belongs to. + * @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(NodeInterface $node, AccountInterface $account, Route $route) { + $operation = $route->getRequirement('_lupus_decoupled_layout_builder_access'); + $section_storage = $this->sectionStorage + ->load('overrides', [ + 'entity' => EntityContext::fromEntity($node), + 'view_mode' => new Context(new ContextDefinition('string'), 'full') + ]); + + $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; + } + +} Index: modules/lupus_decoupled_layout_builder/src/Controller/LupusDecoupledLayoutBuilderController.php IDEA additional info: Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP <+>UTF-8 =================================================================== diff --git a/modules/lupus_decoupled_layout_builder/src/Controller/LupusDecoupledLayoutBuilderController.php b/modules/lupus_decoupled_layout_builder/src/Controller/LupusDecoupledLayoutBuilderController.php new file mode 100644 --- /dev/null (date 1729713662337) +++ b/modules/lupus_decoupled_layout_builder/src/Controller/LupusDecoupledLayoutBuilderController.php (date 1729713662337) @@ -0,0 +1,178 @@ +sectionStorage = $section_storage; + $this->tempstore = $tempstore; + $this->blockManager = $block_manager; + } + + /** + * {@inheritdoc} + * + * @param \Symfony\Component\DependencyInjection\ContainerInterface $container + * The Drupal service container. + * + * @return static + */ + public static function create(ContainerInterface $container) { + return new static( + $container->get('plugin.manager.layout_builder.section_storage'), + $container->get('layout_builder.tempstore_repository'), + $container->get('plugin.manager.block') + ); + } + + /** + * Preview a layout builder block. + * + * @param \Drupal\node\NodeInterface $node + * The node the block belongs to. + * @param string $block_data_id + * The block version UUID to preview. + * @param string $view_mode + * (optional) The view mode to use. Defaults to 'full'. + * + * @return \Drupal\custom_elements\CustomElement + * Returns CustomElement object. + */ + public function previewBlock(NodeInterface $node, $block_data, $view_mode = 'full') { + // Load the Layout Builder section storage. + $overrides = $this->sectionStorage + ->load('overrides', [ + 'entity' => EntityContext::fromEntity($node), + 'view_mode' => new Context(new ContextDefinition('string'), $view_mode) + ]); + $block_data = explode(':', $block_data); + + switch($block_data[0]) { + case 'field_block': + if (!empty($block_data[1]) && $node->hasField($block_data[1])) { + $value = $node->get($block_data[1])->value; + if (empty($value)) { + $value = t('There is no content for the @field field.', ['@field' => $block_data[1]]); + } + } + else { + $value = t('Content block'); + } + + $custom_element = new CustomElement(); + $custom_element->setAttribute('element', 'drupal-markup'); + $custom_element->setAttribute('content', $value); + break; + + case 'inline_block': + $preview_section_storage = $this->tempstore->get($overrides); + + // Loop through all the sections and components until we find the one being previewed. + foreach ($preview_section_storage->getSections() as $section) { + foreach ($section->getComponents() as $uuid => $component) { + if ($uuid == $block_data[1]) { + // Get the data of the "draft" block. + $configuration = $component->toArray()['configuration']; + $id_data = explode(':', $configuration['id']); + + // Handle content blocks. + if (isset($configuration['block_revision_id'])) { + $block_content = \Drupal::entityTypeManager() + ->getStorage('block_content') + ->loadRevision($configuration['block_revision_id']); + + // Check if the block has draft data. + if (!empty($configuration['block_serialized'])) { + $block_override_values = unserialize($configuration['block_serialized']); + + // Apply the temporary draft data to the block object so that it's present for rendering. + foreach ($block_override_values->toArray() as $key => $value) { + $block_content->set($key, $value); + } + } + + $custom_element = $this->getCustomElementGenerator()->generate($block_content, $view_mode); + } // Handle field blocks. + else if ($id_data[0] == 'field_block') { + $custom_element = new CustomElement(); + $custom_element->setAttribute('element', 'drupal-' . implode('-', str_replace('_', '-', $id_data))); + $custom_element->setAttribute('content', $node->get($id_data[3])->value); + } // Handle non-content blocks. + else { + $block_content = $this->blockManager->createInstance($configuration['id'], $configuration); + $render = $block_content->build(); + $custom_element = CustomElement::createFromRenderArray($render); + } + + // Disable caching for previews. + $custom_element->mergeCacheMaxAge(0); + } + } + } + break; + + default: + if ($plugin_block = $this->blockManager->createInstance($block_data[0], ['uuid' => $block_data[1]])) { + $render = $plugin_block->build(); + $custom_element = CustomElement::createFromRenderArray($render); + } + else { + $custom_element = new CustomElement(); + $custom_element->setAttribute('element', 'drupal-markup'); + $custom_element->setAttribute('content', t('Content not found.')); + } + } + + return $custom_element; + } +}