From 35e6da2388d1698c91060c92a66a92140d0a1b87 Mon Sep 17 00:00:00 2001
From: Axel Rutz <axel.rutz@machbarmacher.net>
Date: Sun, 21 Jul 2019 21:05:02 +0200
Subject: [PATCH] Issue #3068719: Attachments and Cacheability dropped for
 Leaflet (embeded|ajax) popups

---
 leaflet.drupal.js                             | 14 ++--
 .../Controller/LeafletAjaxPopupController.php | 65 ++++++++++++++++++-
 .../src/Plugin/views/style/LeafletMap.php     | 26 ++++++--
 .../LeafletDefaultFormatter.php               | 25 +++++--
 4 files changed, 112 insertions(+), 18 deletions(-)

diff --git a/leaflet.drupal.js b/leaflet.drupal.js
index 47d9461..58993dd 100644
--- a/leaflet.drupal.js
+++ b/leaflet.drupal.js
@@ -23,13 +23,17 @@
           var content = $('[data-leaflet-ajax-popup]', e.popup._contentNode);
           if (content.length) {
             var url = content.data('leaflet-ajax-popup');
-            $.get(url, function(response) {
-              if (response) {
-                e.popup.setContent(response)
-              }
-            });
+            Drupal.ajax({url: url}).execute();
           }
         });
+
+        // Attach drupal behaviors on new content.
+        Drupal.Leaflet[mapid].lMap.on('popupopen', function(e) {
+          var element = e.popup._contentNode;
+          $(element).once('leaflet-popup-behaviors-attached').each(function () {
+            Drupal.attachBehaviors(this, drupalSettings);
+          })
+        });
       });
 
       $.each(settings.leaflet, function(m, data) {
diff --git a/modules/leaflet_views/src/Controller/LeafletAjaxPopupController.php b/modules/leaflet_views/src/Controller/LeafletAjaxPopupController.php
index 8a146ac..aa0c776 100644
--- a/modules/leaflet_views/src/Controller/LeafletAjaxPopupController.php
+++ b/modules/leaflet_views/src/Controller/LeafletAjaxPopupController.php
@@ -2,6 +2,8 @@
 
 namespace Drupal\leaflet_views\Controller;
 
+use Drupal\Core\Ajax\AjaxResponse;
+use Drupal\Core\Ajax\ReplaceCommand;
 use Drupal\Core\Controller\ControllerBase;
 use Symfony\Component\DependencyInjection\ContainerInterface;
 use Drupal\Core\Render\HtmlResponse;
@@ -75,15 +77,72 @@ class LeafletAjaxPopupController extends ControllerBase {
    * @param string $langcode
    *   The langcode to render the entity by.
    *
-   * @return \Drupal\Core\Render\HtmlResponse
+   * @return \Symfony\Component\HttpFoundation\Response
    *   The Response to return.
    */
   public function popupBuild(EntityInterface $entity, $view_mode, $langcode = NULL) {
     $entity_view_builder = $this->entityManager->getViewBuilder($entity->getEntityTypeId());
     $build = $entity_view_builder->view($entity, $view_mode, $langcode);
-    $response = new HtmlResponse();
-    $response->setContent($this->renderer->renderRoot($build));
+    $response = new AjaxResponse();
+    $response->addCommand(new ReplaceCommand($this->getPopupIdentifierSelector($entity->getEntityTypeId(), $entity->id(), $view_mode, $langcode), $build));
     return $response;
   }
 
+  /**
+   * Get popup identifier.
+   *
+   * @param $entityType
+   *   The entity type.
+   * @param $entityId
+   *   The entity id.
+   * @param $viewMode
+   *   The view mode.
+   * @param $langcode
+   *   The langcode.
+   *
+   * @return string
+   *   The identifier.
+   */
+  public static function getPopupIdentifier($entityType, $entityId, $viewMode, $langcode): string {
+    return "$entityType-$entityId-$viewMode-$langcode";
+  }
+
+  /**
+   * Get popup identifier attribute.
+   *
+   * @param $entityType
+   *   The entity type.
+   * @param $entityId
+   *   The entity id.
+   * @param $viewMode
+   *   The view mode.
+   * @param $langcode
+   *   The langcode.
+   *
+   * @return string
+   *   The identifier selector.
+   */
+  public static function getPopupIdentifierAttribute($entityType, $entityId, $viewMode, $langcode) {
+    return sprintf('data-leaflet-popup-ajax-entity="%s"', self::getPopupIdentifier($entityType, $entityId, $viewMode, $langcode));
+  }
+
+  /**
+   * Get popup identifier selector.
+   *
+   * @param $entityType
+   *   The entity type.
+   * @param $entityId
+   *   The entity id.
+   * @param $viewMode
+   *   The view mode.
+   * @param $langcode
+   *   The langcode.
+   *
+   * @return string
+   *   The identifier selector.
+   */
+  public static function getPopupIdentifierSelector($entityType, $entityId, $viewMode, $langcode) {
+    return sprintf('[%s]', self::getPopupIdentifierAttribute($entityType, $entityId, $viewMode, $langcode));
+  }
+
 }
diff --git a/modules/leaflet_views/src/Plugin/views/style/LeafletMap.php b/modules/leaflet_views/src/Plugin/views/style/LeafletMap.php
index 163d947..4139391 100644
--- a/modules/leaflet_views/src/Plugin/views/style/LeafletMap.php
+++ b/modules/leaflet_views/src/Plugin/views/style/LeafletMap.php
@@ -4,9 +4,12 @@ namespace Drupal\leaflet_views\Plugin\views\style;
 
 use Drupal\Component\Utility\NestedArray;
 use Drupal\Core\Field\FieldTypePluginManagerInterface;
+use Drupal\Core\Render\BubbleableMetadata;
+use Drupal\Core\Render\RenderContext;
 use Drupal\Core\StringTranslation\TranslatableMarkup;
 use Drupal\Core\Entity\Plugin\DataType\EntityAdapter;
 use Drupal\Core\Form\FormStateInterface;
+use Drupal\leaflet_views\Controller\LeafletAjaxPopupController;
 use Drupal\search_api\Datasource\DatasourceInterface;
 use Drupal\search_api\Entity\Index;
 use Drupal\Core\Url;
@@ -665,6 +668,9 @@ class LeafletMap extends StylePluginBase implements ContainerFactoryPluginInterf
 
     $data = [];
 
+    // Collect bubbleable metadata when doing early rendering.
+    $build_for_bubbleable_metadata = [];
+
     // Always render the map, otherwise ...
     $leaflet_map_style = !isset($this->options['leaflet_map']) ? $this->options['map'] : $this->options['leaflet_map'];
     $map = leaflet_map_get_info($leaflet_map_style);
@@ -754,18 +760,26 @@ class LeafletMap extends StylePluginBase implements ContainerFactoryPluginInterf
               case '#rendered_entity':
                 $build = $this->entityManager->getViewBuilder($entity->getEntityTypeId())
                   ->view($entity, $this->options['view_mode'], $langcode);
-                $description = $this->renderer->renderPlain($build);
+                $render_context = new RenderContext();
+                $description = $this->renderer->executeInRenderContext($render_context, function () use (&$build) {
+                  return $this->renderer->render($build, TRUE);
+                });
+                if (!$render_context->isEmpty()) {
+                  $render_context->update($build_for_bubbleable_metadata);
+                }
                 break;
 
               case '#rendered_entity_ajax':
                 $parameters = [
-                  'entity_type' => $entity->getEntityTypeId(),
+                  'entity_type' => $entity_type,
                   'entity' => $entity->id(),
                   'view_mode' => $this->options['view_mode'],
                   'langcode' => $langcode,
                 ];
                 $url = Url::fromRoute('leaflet_views.ajax_popup', $parameters, ['absolute' => TRUE]);
-                $description = sprintf('<div class="leaflet-ajax-popup" data-leaflet-ajax-popup="%s"></div>', $url->toString());
+                $description = sprintf('<div class="leaflet-ajax-popup" data-leaflet-ajax-popup="%s" %s></div>',
+                  $url->toString(), LeafletAjaxPopupController::getPopupIdentifierAttribute($entity_type, $entity->id(), $this->options['view_mode'], $langcode));
+                $build_for_bubbleable_metadata['#attached']['library']['drupal/ajax'] = 'drupal/ajax';
                 break;
 
               default:
@@ -851,7 +865,11 @@ class LeafletMap extends StylePluginBase implements ContainerFactoryPluginInterf
     // Allow other modules to add/alter the map js settings.
     $this->moduleHandler->alter('leaflet_map_view_style', $js_settings, $this);
 
-    return $this->leafletService->leafletRenderMap($js_settings['map'], $js_settings['features'], $this->options['height'] . 'px');
+    $build = $this->leafletService->leafletRenderMap($js_settings['map'], $js_settings['features'], $this->options['height'] . 'px');
+    BubbleableMetadata::createFromRenderArray($build)
+      ->merge(BubbleableMetadata::createFromRenderArray($build_for_bubbleable_metadata))
+      ->applyTo($build);
+    return $build;
   }
 
   /**
diff --git a/src/Plugin/Field/FieldFormatter/LeafletDefaultFormatter.php b/src/Plugin/Field/FieldFormatter/LeafletDefaultFormatter.php
index 031f127..d80d3e8 100644
--- a/src/Plugin/Field/FieldFormatter/LeafletDefaultFormatter.php
+++ b/src/Plugin/Field/FieldFormatter/LeafletDefaultFormatter.php
@@ -3,6 +3,8 @@
 namespace Drupal\leaflet\Plugin\Field\FieldFormatter;
 
 use Drupal\Core\Plugin\ContainerFactoryPluginInterface;
+use Drupal\Core\Render\BubbleableMetadata;
+use Drupal\Core\Render\RenderContext;
 use Symfony\Component\DependencyInjection\ContainerInterface;
 use Drupal\Core\Field\FieldDefinitionInterface;
 use Drupal\Core\Field\FieldItemListInterface;
@@ -314,6 +316,7 @@ class LeafletDefaultFormatter extends FormatterBase implements ContainerFactoryP
       $this->fieldDefinition->getTargetEntityTypeId() => $items->getEntity(),
     ];
 
+    $results = [];
     $features = [];
     foreach ($items as $delta => $item) {
 
@@ -323,18 +326,29 @@ class LeafletDefaultFormatter extends FormatterBase implements ContainerFactoryP
 
       // Eventually set the popup content.
       if ($settings['popup']) {
-        // Construct the renderable array for popup title / text.
+        // Construct the renderable array for popup title / text. As we later
+        // convert that to plain text, losing attachments and cacheability, save
+        // them to $results.
         $build = [];
         if ($this->getSetting('popup_content')) {
-          $popup_content = $this->token->replace($this->getSetting('popup_content'), $token_context);
+          $bubbleable_metadata = new BubbleableMetadata();
+          $popup_content = $this->token->replace($this->getSetting('popup_content'), $token_context, [], $bubbleable_metadata);
           $build[] = [
             '#markup' => $popup_content,
           ];
+          $bubbleable_metadata->applyTo($results);
         }
 
-        // We need a string for using it inside the popup.
-        $build = $this->renderer->renderPlain($build);
-        $feature['popup'] = !empty($build) ? $build : $entity->label();;
+        // We need a string for using it inside the popup. Save attachments and
+        // cacheability to $results.
+        $render_context = new RenderContext();
+        $rendered = $this->renderer->executeInRenderContext($render_context, function () use (&$build) {
+          return $this->renderer->render($build, TRUE);
+        });
+        $feature['popup'] = !empty($rendered) ? $rendered : $entity->label();
+        if (!$render_context->isEmpty()) {
+          $render_context->update($results);
+        }
       }
 
       // Add/merge eventual map icon definition from hook_leaflet_map_info.
@@ -368,7 +382,6 @@ class LeafletDefaultFormatter extends FormatterBase implements ContainerFactoryP
     // Allow other modules to add/alter the map js settings.
     $this->moduleHandler->alter('leaflet_default_map_formatter', $js_settings, $items);
 
-    $results = [];
     if (!empty($settings['multiple_map'])) {
       foreach ($js_settings['features'] as $k => $feature) {
         $map = $js_settings['map'];
-- 
2.17.1

