Commit 63381a1d authored by s_leu's avatar s_leu Committed by Tim Bozeman
Browse files

Issue #3262430 by s_leu: Component Overrides: Show attached Libraries

parent 36f1151f
Loading
Loading
Loading
Loading
+29 −0
Original line number Diff line number Diff line
@@ -8,6 +8,7 @@ declare(strict_types=1);
 */

use Drupal\Component\Serialization\Exception\InvalidDataTypeException;
use Drupal\component_library\Form\ComponentOverrideForm;
use Drupal\Component\Serialization\Json;
use Drupal\Component\Serialization\Yaml;
use Drupal\component_library\Entity\ComponentLibraryPattern;
@@ -216,3 +217,31 @@ function component_library_system_info_alter(array &$info, Extension $file, $typ
    $info['engine'] = 'component_library_engine';
  }
}

/**
 * Implements hook_modules_installed().
 */
function component_library_modules_installed($modules, $is_syncing) {
  \Drupal::cache('default')->delete(ComponentOverrideForm::COLLECTED_LIBRARIES_CID);
}

/**
 * Implements hook_modules_uninstalled().
 */
function component_library_modules_uninstalled($modules, $is_syncing) {
  \Drupal::cache('default')->delete(ComponentOverrideForm::COLLECTED_LIBRARIES_CID);
}

/**
 * Implements hook_themes_installed().
 */
function component_library_themes_installed($themes) {
  \Drupal::cache('default')->delete(ComponentOverrideForm::COLLECTED_LIBRARIES_CID);
}

/**
 * Implements hook_themes_uninstalled().
 */
function component_library_themes_uninstalled($themes) {
  \Drupal::cache('default')->delete(ComponentOverrideForm::COLLECTED_LIBRARIES_CID);
}
+6 −0
Original line number Diff line number Diff line
@@ -70,3 +70,9 @@ component_library.override.*:
    variables:
      type: string
      label: Variables
    libraries:
      type: sequence
      label: Libraries
      sequence:
        type: string
        label: Library
+6 −0
Original line number Diff line number Diff line
@@ -59,6 +59,7 @@ use Drupal\Core\Form\FormState;
 *     "plugin_data",
 *     "template",
 *     "variables",
 *     "libraries",
 *   }
 * )
 */
@@ -104,6 +105,11 @@ final class ComponentOverride extends ConfigEntityBase {
   */
  protected string $variables;

  /**
   * The libraries attached to the template.
   */
  protected array $libraries = [];

  /**
   * Set variables.
   *
+221 −4
Original line number Diff line number Diff line
@@ -6,6 +6,7 @@ namespace Drupal\component_library\Form;

use Drupal\Core\Url;
use Twig\Error\SyntaxError;
use Drupal\Core\Cache\Cache;
use Drupal\Core\Ajax\HtmlCommand;
use Drupal\Core\Form\SubformState;
use Drupal\Core\Ajax\AjaxResponse;
@@ -15,11 +16,16 @@ use Drupal\Core\Ajax\PrependCommand;
use Drupal\Core\Ajax\RedirectCommand;
use Drupal\Core\Entity\EntityInterface;
use Drupal\Core\Form\FormStateInterface;
use Drupal\Core\Extension\ExtensionList;
use Drupal\Core\Render\RendererInterface;
use Drupal\Core\Template\TwigEnvironment;
use Drupal\component_library\OverrideMode;
use Drupal\Core\TempStore\PrivateTempStore;
use Drupal\Core\Ajax\CloseModalDialogCommand;
use Drupal\Core\Cache\CacheBackendInterface;
use Drupal\Core\Extension\ThemeExtensionList;
use Drupal\Core\Extension\ModuleExtensionList;
use Drupal\Core\Extension\ProfileExtensionList;
use Drupal\Core\Asset\LibraryDiscoveryInterface;
use Drupal\Core\Extension\ThemeHandlerInterface;
use Drupal\Core\TempStore\PrivateTempStoreFactory;
use Drupal\Core\StringTranslation\TranslatableMarkup;
@@ -37,6 +43,13 @@ final class ComponentOverrideForm extends EntityForm {

  use ComponentOverrideFormTrait;

  /**
   * Cached ID for libraries gathered from all available/installed extensions.
   *
   * @var string
   */
  const COLLECTED_LIBRARIES_CID = 'component_library:component_override_form:collected_libraries';

  /**
   * Theme.
   *
@@ -52,10 +65,20 @@ final class ComponentOverrideForm extends EntityForm {
  protected PrivateTempStore $cache;
  protected OverrideMode $overrideMode;
  private PrepareOverrideEvent $prepareOverrideEvent;
  protected CacheBackendInterface $cacheDefault;
  protected LibraryDiscoveryInterface $libraryDiscovery;
  protected ModuleExtensionList $moduleExtensionList;
  protected ExtensionList $profileExtensionList;
  protected ThemeExtensionList $themeExtensionList;

  public function __construct(ThemeHandlerInterface $theme_handler, ComponentOverrideManager $override_manager, TwigEnvironment $twig, EventDispatcherInterface $event_dispatcher, RendererInterface $renderer, PrivateTempStoreFactory $temp_store, OverrideMode $override_mode) {
  public function __construct(ThemeHandlerInterface $theme_handler, CacheBackendInterface $cache_default, ComponentOverrideManager $override_manager, LibraryDiscoveryInterface $library_discovery, ModuleExtensionList $module_list, ProfileExtensionList $profile_list, ThemeExtensionList $theme_list, TwigEnvironment $twig, EventDispatcherInterface $event_dispatcher, RendererInterface $renderer, PrivateTempStoreFactory $temp_store, OverrideMode $override_mode) {
    $this->themeHandler = $theme_handler;
    $this->cacheDefault = $cache_default;
    $this->overrideManager = $override_manager;
    $this->libraryDiscovery = $library_discovery;
    $this->moduleExtensionList = $module_list;
    $this->profileExtensionList = $profile_list;
    $this->themeExtensionList = $theme_list;
    $this->twig = $twig;
    $this->dispatcher = $event_dispatcher;
    $this->renderer = $renderer;
@@ -69,7 +92,12 @@ final class ComponentOverrideForm extends EntityForm {
  public static function create(ContainerInterface $container): self {
    return new self(
      $container->get('theme_handler'),
      $container->get('cache.default'),
      $container->get('plugin.manager.component_override'),
      $container->get('library.discovery'),
      $container->get('extension.list.module'),
      $container->get('extension.list.profile'),
      $container->get('extension.list.theme'),
      $container->get('twig'),
      $container->get('event_dispatcher'),
      $container->get('renderer'),
@@ -189,7 +217,7 @@ final class ComponentOverrideForm extends EntityForm {
      (empty($input['template']) && $this->prepareOverrideEvent->isPrepopulated())
      && $override
    ) {
      $template = $this->getTemplate($override);
      $template = $this->getTemplate($override, TRUE);
      $input['template'] = $template;
      $form_state->setUserInput($input);
    }
@@ -226,6 +254,47 @@ final class ComponentOverrideForm extends EntityForm {
      '#suffix' => '<output id="override-preview"></output>',
    ];

    if ($override && empty($form_state->get('selected_libraries')[$override])) {
      $selected_libraries = $form_state->get('selected_libraries') ?: [];
      $selected_libraries[$override] = $this->parseAttachedLibraries(
        $this->getTemplate($override, FALSE)
      );
      $form_state->set('selected_libraries', $selected_libraries);
    }
    $attached_libraries = $form_state->get('selected_libraries')[$override];
    $available_libraries = $this->getAvailableLibraries();
    $form['dynamic_elements_container']['libraries_wrapper'] = [
      '#type' => 'details',
      '#prefix' => '<div id="libraries-wrapper">',
      '#suffix' => '</div>',
      '#open' => !empty($attached_libraries),
      '#title' => $this->t('Libraries'),
      'libraries_selector' => [
        '#type' => 'select',
        '#title' => $this->t('Add library'),
        '#empty_option' => $this->t('- Select -'),
        '#options' => $available_libraries,
        '#ajax' => [
          'callback' => [$this, 'ajaxUpdateLibraries'],
          'wrapper' => 'libraries-wrapper',
          'effect' => 'fade',
        ],
      ],
      'libraries_list' => [
        '#prefix' => '<h6>' . $this->t('Attached libraries') . '</h6>',
        '#type' => 'table',
        '#header' => [$this->t('Library'), $this->t('Remove')],
        '#empty' => $this->t('No libraries are attached.'),
      ],
      'libraries' => [
        '#type' => 'value',
        '#value' => $attached_libraries ?? [],
      ],
    ];
    if ($attached_libraries) {
      $this->buildLibrariesList($attached_libraries, $form);
    }

    $form['#prefix'] = '<div id="status-messages"></div>';
    $form['#attached']['library'][] = 'component_library/component_override';
    return $form;
@@ -235,6 +304,29 @@ final class ComponentOverrideForm extends EntityForm {
   * {@inheritdoc}
   */
  public function validateForm(array &$form, FormStateInterface $form_state): void {
    // Validate attached libraries.
    $override = $form_state->getValue('override') ?? $this->entity->get('override');
    $libraries = $form_state->get('selected_libraries');
    $trigger = $form_state->getTriggeringElement();
    if ((isset($trigger['#name']) && $trigger['#name'] === 'override') && $override) {
      $theme_registry = $this->overrideManager->getThemeRegistry()[$form_state->getValue('override')];
      $template_path = sprintf('%s/%s.html.twig', $theme_registry['path'], $theme_registry['template']);
      $template = file_get_contents($template_path);
      $libraries[$override] = $libraries[$override] ?? $this->parseAttachedLibraries($template);
    }

    if ((isset($trigger['#name']) && $trigger['#name'] === 'libraries_selector')) {
      $added_library = $form_state->getValue('libraries_selector');
      if (!empty($added_library) && !in_array($added_library, $libraries)) {
        $libraries[$override][] = $added_library;
      }
    }

    if (isset($trigger['#parents'][1]) && isset($trigger['#name']) && strpos($trigger['#name'], 'library_remove') !== FALSE) {
      array_splice($libraries[$override], $trigger['#parents'][1], 1);
    }
    $form_state->set('selected_libraries', $libraries);

    // Validate the plugin.
    $plugin_values = $form_state->getValue('plugin_container');
    if (!empty($plugin_values['plugin'])) {
@@ -435,6 +527,123 @@ final class ComponentOverrideForm extends EntityForm {
    return NULL;
  }

  /**
   * AJAX callback to load the libraries list.
   */
  public function ajaxUpdateLibraries(array &$form, FormStateInterface $form_state) {
    return $form['dynamic_elements_container']['libraries_wrapper'];
  }

  /**
   * Builds the rows of the libraries list widget.
   *
   * @param array $selected_libraries
   *   The currently selected libraries.
   * @param array $form
   *   The form array.
   */
  protected function buildLibrariesList(array $selected_libraries, array &$form) {
    $row = 0;
    foreach ($selected_libraries as $key => $library) {
      $form['dynamic_elements_container']['libraries_wrapper']['libraries_list'][$row]['library'] = [
        '#markup' => $library,
      ];
      $button_key = 'library_remove' . $key;
      $form['dynamic_elements_container']['libraries_wrapper']['libraries_list'][$row][$button_key] = [
        '#type' => 'submit',
        '#value' => $this->t('Remove'),
        '#name' => $button_key,
        '#limit_validation_errors' => TRUE,
        '#executes_submit_callback' => FALSE,
        '#ajax' => [
          'callback' => [$this, 'ajaxUpdateLibraries'],
          'wrapper' => 'libraries-wrapper',
          'effect' => 'fade',
        ],
      ];
      $row++;
    }
  }

  /**
   * Parses libraries from a given template string.
   *
   * @param string $template
   *   The template to parse from.
   *
   * @return array
   *   List of libraries.
   */
  protected function parseAttachedLibraries($template) {
    $matches = [];
    $libraries = [];
    \preg_match_all('/attach_library\(.+\)/', $template, $matches);
    if (!empty($matches[0])) {
      foreach ($matches[0] as $attach_statement) {
        $libraries[] = \mb_substr(
          $attach_statement,
          \mb_strlen("attach_library('"),
          \mb_strlen($attach_statement) - \mb_strlen("attach_library('") - \mb_strlen("')")
        );
      }
    }
    return $libraries;
  }

  /**
   * Gathers libraries from core, profiles, installed modules and themes.
   *
   * @return array
   *   Nested array of libraries in the form of extension => [libraries].
   */
  protected function getAvailableLibraries() {
    $cache = $this->cacheDefault->get(static::COLLECTED_LIBRARIES_CID);
    if (!empty($cache->data)) {
      return $cache->data;
    }
    else {
      $available_libraries = [];
      $modules = $this->getInstalledList($this->moduleExtensionList);
      $profiles = $this->getInstalledList($this->profileExtensionList);
      $themes = $this->getInstalledList($this->themeExtensionList);
      $core_libraries = $this->libraryDiscovery->getLibrariesByExtension('core');
      foreach ($core_libraries as $library_name => $library) {
        $available_libraries['core']['core/' . $library_name] = 'core/' . $library_name;
      }
      /** @var \Drupal\Core\Extension\Extension $extension */
      foreach (\array_merge($modules, $profiles, $themes) as $extension_name => $extension) {
        $extension_info = (array) $extension;
        if ($extension->getType() == 'profile'|| !empty($extension_info['status'])) {
          $extension_libraries = $this->libraryDiscovery->getLibrariesByExtension($extension_name);
          $libraries_list = [];
          foreach ($extension_libraries as $library_name => $library) {
            $libraries_list[$extension_name][$extension_name . '/' . $library_name] = $extension_name . '/' . $library_name;
          }
          $available_libraries = \array_merge($available_libraries, $libraries_list);
        }
      }
      $this->cacheDefault->set(static::COLLECTED_LIBRARIES_CID, $available_libraries, Cache::PERMANENT, ['library_info']);
      return $available_libraries;
    }
  }

  /**
   * Get installed list.
   *
   * @param \Drupal\Core\Extension\ExtensionList $extension_list
   *   An extension list.
   *
   * @return array
   *   An array of installed extensions for the given list.
   */
  protected function getInstalledList(ExtensionList $extension_list) {
    $list = [];
    foreach ($extension_list->getAllInstalledInfo() as $name => $info) {
      $list[$name] = $extension_list->get($name);
    }
    return $list;
  }

  /**
   * Get template.
   *
@@ -442,13 +651,15 @@ final class ComponentOverrideForm extends EntityForm {
   *
   * @param string $override
   *   The theme suggestion being overridden.
   * @param bool $strip_libraries
   *   Whether to strip attach_library twig statements from the template.
   *
   * @return string
   *   The file contents of the template.
   *
   * @throws \Twig\Error\LoaderError
   */
  protected function getTemplate(string $override): string {
  protected function getTemplate(string $override, $strip_libraries): string {
    $registry = $this->overrideManager->getThemeRegistry();
    $pieces = \explode('__', $override);
    for ($i = \count($pieces); $i > 0; $i--) {
@@ -456,6 +667,12 @@ final class ComponentOverrideForm extends EntityForm {
      if (!empty($registry[$possible_theme_suggestion])) {
        $path = \sprintf('%s/%s.html.twig', $registry[$possible_theme_suggestion]['path'], $registry[$possible_theme_suggestion]['template']);
        $template = $this->twig->getLoader()->getSourceContext($path)->getCode();
        if ($strip_libraries) {
          // Strip attach_library statements as libraries get displayed in
          // dedicated field.
          $template = \preg_replace('/\{\{ attach_library\(.+\) \}\}[\r\n|\r]/', '', $template);
          $template = \preg_replace('/\{\{attach_library\(.+\)\}\}[\r\n|\r]/', '', $template);
        }
        return $template;
      }
    }
+19 −3
Original line number Diff line number Diff line
@@ -30,9 +30,25 @@ final class Loader implements LoaderInterface, SourceContextLoaderInterface {
   * {@inheritdoc}
   */
  public function getSourceContext($name): Source {
    $override = $this->getOverrideByName($name);
    if ($override) {
      return new Source($override->get('template'), $name);
    $override_name = $this->getOverrideNameFromName($name);
    $overrides = NULL;
    $theme = $this->themeManager->getActiveTheme();
    if ($theme) {
      $overrides = $this->overrides->loadByProperties([
        'override' => $override_name,
        'theme' => $theme->getName(),
      ]);
    }
    if ($overrides) {
      $override = \reset($overrides);
      $template = $override->get('template');
      $libraries = $override->get('libraries');
      $attach_statements = '';
      foreach ($libraries as $library) {
        $attach_statements .= "{{ attach_library('$library') }}\n";
      }
      $template = $attach_statements . $template;
      return new Source($template, $name);
    }

    throw new LoaderError($name);
Loading