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;
+ }
+}