Commit 9ed76cd0 authored by Mateu Aguiló Bosch's avatar Mateu Aguiló Bosch
Browse files

Issue #3310852 by e0ipso, isholgueras: Create a fancier widget selector with more context

parent ac78a1b0
Loading
Loading
Loading
Loading
+135 −0
Original line number Diff line number Diff line
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg
   data-v-2cb57da0=""
   version="1.0"
   width="512"
   height="288"
   viewBox="0 0 512 288"
   preserveAspectRatio="xMidYMid"
   color-interpolation-filters="sRGB"
   id="svg62"
   sodipodi:docname="default-thumbnail.svg"
   inkscape:version="1.2.1 (9c6d41e410, 2022-07-14)"
   inkscape:export-filename="default-thumbnail.png"
   inkscape:export-xdpi="96"
   inkscape:export-ydpi="96"
   xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
   xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
   xmlns="http://www.w3.org/2000/svg"
   xmlns:svg="http://www.w3.org/2000/svg">
  <sodipodi:namedview
     id="namedview64"
     pagecolor="#ffffff"
     bordercolor="#000000"
     borderopacity="0.25"
     inkscape:showpageshadow="2"
     inkscape:pageopacity="0.0"
     inkscape:pagecheckerboard="0"
     inkscape:deskcolor="#d1d1d1"
     showgrid="false"
     inkscape:zoom="1.411094"
     inkscape:cx="148.46637"
     inkscape:cy="249.45185"
     inkscape:window-width="1920"
     inkscape:window-height="1043"
     inkscape:window-x="0"
     inkscape:window-y="0"
     inkscape:window-maximized="1"
     inkscape:current-layer="svg62" />
  <g
     data-v-2cb57da0=""
     fill="#333333"
     class="icon-text-wrapper icon-svg-group iconsvg"
     transform="matrix(1.5052041,0,0,1.5052041,131.76021,68.739816)"
     id="g58">
    <g
       class="iconsvg-imagesvg"
       transform="translate(52.540165)"
       id="g46">
      <g
         id="g44">
        <rect
           fill="#333333"
           fill-opacity="0"
           stroke-width="2"
           x="0"
           y="0"
           width="60"
           height="65.080864"
           class="image-rect"
           id="rect32" />
        <svg
           x="0"
           y="0"
           width="60"
           height="65.080864"
           filtersec="colorsb1857224266"
           class="image-svg-svg primary"
           style="overflow:visible"
           version="1.1"
           id="svg42">
          <svg
             viewBox="-0.0015643352 -2.7537346e-05 92.193031 100.00003"
             version="1.1"
             id="svg40"
             width="100%"
             height="100%">
            <path
               d="M 76.57,55.43 40.34,19.43 22,1.2 A 14.48,14.48 0 0 0 16.2,0 C 7.37,0 0.19,8.1 0,18.16 V 61.63 A 18.67,18.67 0 0 0 18.43,80.54 H 69.2 a 17.86,17.86 0 0 0 10,-15.92 17.4,17.4 0 0 0 -2.63,-9.19 z"
               fill="#e62c5a"
               opacity="0.8"
               id="path34" />
            <path
               d="M 73.76,19.46 H 23 a 17.86,17.86 0 0 0 -10,15.92 17.46,17.46 0 0 0 2.61,9.2 l 36.23,36 18.39,18.22 A 14.52,14.52 0 0 0 76,100 c 8.82,0 16,-8.09 16.19,-18.15 V 38.36 A 18.67,18.67 0 0 0 73.76,19.46 Z"
               fill="#0fabf6"
               opacity="0.8"
               id="path36" />
            <path
               d="m 15.61,44.58 36.23,36 H 69.2 a 17.86,17.86 0 0 0 10,-15.92 17.4,17.4 0 0 0 -2.62,-9.19 l -36.23,-36 H 23 a 17.86,17.86 0 0 0 -10,15.91 17.46,17.46 0 0 0 2.61,9.2 z"
               fill="#501b8d"
               opacity="0.5"
               id="path38" />
          </svg>
        </svg>
        <!---->
      </g>
    </g>
    <g
       transform="translate(0,72.080864)"
       id="g56"
       style="fill:#000000">
      <g
         data-gra="path-name"
         fill-rule=""
         class="tp-name iconsvg-namesvg"
         id="g54"
         style="fill:#000000">
        <g
           transform="scale(0.36)"
           id="g52"
           style="fill:#000000">
          <g
             id="g50"
             style="fill:#000000">
            <path
               d="m 31.35,-16.57 c 0,7.12 -6.9,12.44 -14.17,12.44 -6.3,0 -11.78,-3.97 -12.23,-10.79 -0.3,-4.05 -5.77,-3.38 -5.55,0.07 0.53,9 8.25,15.75 17.78,15.75 9.89,0 19.42,-7.35 19.34,-17.47 V -45.3 c -0.14,-1.35 -1.35,-2.47 -2.7,-2.47 H 16.95 c -3.75,0 -3.75,4.95 0,4.95 h 14.4 z m 13.72,6.07 c 2.48,9 14.18,11.63 21.83,11.63 9.23,0 22.05,-3.53 22.05,-15.01 0,-11.25 -11.03,-11.77 -21.3,-12.67 -8.7,-0.67 -16.2,-1.35 -16.2,-7.87 0,-6.83 9.75,-8.85 14.77,-8.85 5.41,0 13.43,0.97 15.68,6.67 1.13,2.55 6.23,1.2 5.1,-1.8 -3.15,-8.55 -12.98,-10.42 -20.85,-10.42 -9.15,0 -20.55,4.42 -20.55,14.32 0,11.18 11.1,12.15 21.45,12.98 8.25,0.67 16.12,1.2 16.12,7.72 0,7.58 -9.9,9.53 -16.27,9.53 -6,0 -14.33,-1.36 -16.27,-7.58 -0.61,-3.07 -6.46,-1.95 -5.56,1.35 z m 74.18,-36.45 c -1.28,-3.22 -6.75,-1.28 -5.55,2.1 l 19.5,44.18 c 0.97,2.39 4.35,1.94 5.25,0.07 l 16.5,-33.9 16.43,33.9 c 1.12,2.32 4.04,2.17 5.09,-0.07 l 19.66,-44.18 c 1.12,-3.97 -4.28,-5.25 -5.48,-2.1 l -16.73,38.7 -16.35,-33.45 c -1.19,-2.1 -4.42,-2.1 -5.25,0.07 l -16.27,33.38 z m 86.03,45.07 c 0,3.6 5.62,3.45 5.62,0 v -44.85 c 0,-3.67 -5.62,-3.75 -5.62,0 z M 226.5,-42.9 h 14.4 c 11.25,0 16.87,9.6 16.87,19.05 0,9.45 -5.62,18.97 -16.87,18.97 H 226.5 Z M 240.9,0 c 14.92,0 22.35,-11.92 22.35,-23.85 0,-11.92 -7.43,-23.85 -22.35,-23.85 h -16.35 c -1.88,0 -3.45,1.43 -3.45,3.15 v 41.32 c 0,1.73 1.57,3.23 3.45,3.23 z m 72.82,-10.8 v -12.52 c 0,-1.65 -0.67,-2.78 -2.84,-2.78 h -15.91 c -3.15,0 -3.52,4.95 0,4.95 h 13.5 v 9.98 c -3.29,4.42 -9.07,6.82 -14.62,6.75 -9.67,-0.23 -18.22,-9.08 -18.38,-19.65 0,-9.83 7.21,-17.1 15.46,-19.06 5.62,-1.42 12.59,0.31 16.79,5.18 2.33,2.7 6.16,-0.9 3.91,-3.53 -4.13,-4.79 -11.11,-7.04 -17.63,-7.04 -1.35,0 -2.78,0.07 -3.9,0.22 -11.47,2.17 -20.03,12.6 -20.03,24 -0.3,14.17 10.81,24.9 23.7,24.97 7.28,0.08 14.7,-3.15 19.13,-8.99 0.67,-0.68 0.82,-1.65 0.82,-2.48 z m 16.13,-10.42 h 28.2 c 3.15,0 3,-4.8 0,-4.8 h -28.2 V -42.9 h 29.63 c 3.15,0 3.29,-4.8 0.07,-4.8 H 327.9 c -2.1,0 -3.45,1.5 -3.45,3.22 V -3.3 c 0,1.8 1.35,3.3 3.45,3.3 h 31.58 c 3.59,0 3.15,-4.95 0,-4.95 h -29.63 z m 40.2,-26.55 c -3.53,0 -3.53,5.17 0,5.17 h 16.65 v 40.72 c 0,1.88 1.43,2.78 2.7,2.78 1.28,0 2.85,-0.9 2.85,-2.78 0,-13.5 -0.07,-27.22 -0.07,-40.72 h 16.72 c 3.67,0 3.67,-5.17 0,-5.17 z m 44.02,37.27 c 2.48,9 14.18,11.63 21.83,11.63 9.23,0 22.05,-3.53 22.05,-15.01 0,-11.25 -11.02,-11.77 -21.3,-12.67 -8.7,-0.67 -16.2,-1.35 -16.2,-7.87 0,-6.83 9.75,-8.85 14.78,-8.85 5.4,0 13.42,0.97 15.67,6.67 1.13,2.55 6.23,1.2 5.1,-1.8 -3.15,-8.55 -12.97,-10.42 -20.85,-10.42 -9.15,0 -20.55,4.42 -20.55,14.32 0,11.18 11.1,12.15 21.45,12.98 8.25,0.67 16.13,1.2 16.13,7.72 0,7.58 -9.9,9.53 -16.28,9.53 -6,0 -14.33,-1.36 -16.27,-7.58 -0.6,-3.07 -6.45,-1.95 -5.56,1.35 z"
               transform="translate(0.60646009,49.512581)"
               id="path48"
               style="fill:#000000;fill-opacity:1" />
          </g>
          <!---->
          <!---->
          <!---->
          <!---->
          <!---->
          <!---->
          <!---->
        </g>
      </g>
      <!---->
    </g>
  </g>
  <defs
     v-gra="od"
     id="defs60" />
</svg>
+13 −0
Original line number Diff line number Diff line
status: true
dependencies: {  }
name: widget_type_small_16_9
label: 'Widget Type Small (16:9)'
effects:
  6fde8733-9328-4096-8ab9-8c31f49d4acd:
    uuid: 6fde8733-9328-4096-8ab9-8c31f49d4acd
    id: image_scale_and_crop
    weight: 1
    data:
      width: 192
      height: 108
      anchor: center-center

css/options-filter.css

0 → 100644
+152 −0
Original line number Diff line number Diff line
.widget-type-selector--radios .form-radios {
  width: 100%;
  display: flex;
  flex-flow: row wrap;
  justify-content: flex-start;
}

.widget-type-selector--radios .form-type--radio {
  outline: 1px solid black;
  border-radius: 3px;
  min-width: 150px;
  width: 12%;
  min-height: 150px;
  padding: 5px 10px;
  position: relative;
  margin-right: 15px;
}

.widget-type-selector--radios .form-type--radio__selected {
  outline-color: peru;
  box-shadow: 0 0 4px orangered;
}

.widget-type-selector--radios .form-type--radio .form-radio {
  width: 16px;
  height: 16px;
}

.radio-details--wrapper {
  display: grid;
  grid-template-columns: 33.3% 33.3% 33.4%;
  padding: 5px 0;
  height: 100%;
}

.radio-details--status,
.radio-details--documentation,
.radio-details--created,
.radio-details--updated,
.radio-details--image,
.radio-details--preview-url,
.radio-details--machine-name {
  display: none;
}

.currently-selected--image--wrapper .radio-details--image {
  display: block;
}

.radio-details--name {
  grid-area: 2 / 1 / span 1 / span 3;
  font-weight: bold;
  margin-bottom: 1rem;
  font-size: 0.9rem;
}

.radio-details--wrapper .radio-details--description {
  grid-area: 3 / 1 / span 1 / span 3;
  font-size: 0.75rem;
  color: #666;
  line-height: 1rem;
  margin-bottom: 1rem;
  padding-right: 10px;
}

.radio-details--wrapper .radio-details--thumbnail {
  grid-area: 1 / 1 / span 1 / span 3;
  height: 100px;
  position: relative;
  overflow: hidden;
  border-radius: 5px;
  border: 1px solid #bbb;
  margin-bottom: 15px;
  background-image: url("../assets/default-thumbnail.svg");
  background-color: #cae6ef;
  background-size: 100%;
  background-clip: content-box;
}

.radio-details--wrapper .radio-details--version {
  position: absolute;
  bottom: 0;
  right: 0;
  padding: 0 5px;
  max-width: 35%;
  font-size: 0.75rem;
  background: #b5df74a0;
}

.radio-details--wrapper .radio-details--version.status--disabled {
  background: #eea0c0;
}

.radio-details--wrapper .radio-details--source {
  padding: 0 5px;
  font-size: 0.75rem;
  position: absolute;
  bottom: 0;
  max-width: 60%;
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
  background: #f1c21bc0;
}

.currently-selected {
  box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
  padding: 1em;
  background-color: #ff00b206;
}

.currently-selected summary {
  padding: 0;
}

.currently-selected .image-table--wrapper {
  display: flex;
  justify-content: space-around;
  flex-wrap: wrap;
}

.currently-selected img {
  max-height: 300px;
  max-width: 500px;
  margin: 0;
  box-shadow: 1px 1px 3px rgba(0, 0, 0, 0.5);
}

.currently-selected .currently-selected--image--wrapper {
  align-self: center;
  background-image: url("../assets/default-thumbnail.svg");
  background-color: #cae6ef;
  background-size: 100%;
  background-clip: content-box;
  min-width: 500px;
  min-height: 288px;
}

.currently-selected table {
  max-width: 30%;
  align-self: center;
  font-size: 0.8em;
}

.currently-selected .try-now--wrapper {
  text-align: center;
}

.currently-selected iframe {
  width: 100%;
  height: 600px;
}

js/options-filter.js

0 → 100644
+123 −0
Original line number Diff line number Diff line
(function(once) {

  function toggleRadioButtons(inputQuery, fieldset) {
    if (inputQuery.length === 0) {
      for (var radio of fieldset.querySelectorAll('input[type="radio"]')) {
        radio.parentElement.parentElement.parentElement.hidden = false;
      }
      return;
    }
    var matching = Array.from(fieldset.querySelectorAll('.radio-details--machine-name'))
      .filter(e => e.innerText.search(inputQuery) !== -1);
    // Hide all inputs, then show the matching and selected.
    for (var radio of fieldset.querySelectorAll('input[type="radio"]')) {
      // Always show the checked option.
      radio.parentElement.parentElement.parentElement.hidden = !radio.checked;
    }
    for (var matchingElement of matching) {
      matchingElement.parentElement.parentElement.hidden = false;
    }
  }

  function subscribeToChanges(fieldset) {
    var search = fieldset.querySelector('input[type="search"]');
    var changeSearchText = (event) => toggleRadioButtons(event.target.value, fieldset);
    search.addEventListener('input', changeSearchText);
  }

  var renderCurrentlySelected = (info, selectedContainer) => {
    var id = selectedContainer.querySelector('input[type="radio"]').value;
    var name = selectedContainer.querySelector('.radio-details--human-name').innerText;
    var description = selectedContainer.querySelector('.radio-details--description').innerText;
    var status = selectedContainer.querySelector('.radio-details--status').innerText;
    var source = selectedContainer.querySelector('.radio-details--source').innerText;
    var version = selectedContainer.querySelector('.radio-details--version').innerText;
    var createdDate = selectedContainer.querySelector('.radio-details--created').innerText;
    var updatedDate = selectedContainer.querySelector('.radio-details--updated').innerText;
    const imgElement = selectedContainer.querySelector('.radio-details--image');
    var img = imgElement ? imgElement.outerHTML : '';
    var previewUrl = selectedContainer.querySelector('.radio-details--preview-url').innerText;
    info.innerHTML = Drupal.theme('currentlySelectedClComponent', id, name, description, status, version, source, createdDate, updatedDate, img, previewUrl);
    if (previewUrl) {
      info.querySelector('a.button').addEventListener('click', (event) => {
        var button = event.target;
        var iframe = document.createElement('iframe');
        iframe.src = previewUrl;
        button.replaceWith(iframe);
        return false;
      });
    }
    info.hidden = false;
  };

  /**
   * Set up options filter
   */
  Drupal.behaviors.optionsFilter = {
    attach: (context, settings) => {
      var fieldsets = once('options-filter', '.widget-type--selector', context);
      for (var fieldset of fieldsets) {
        var search = fieldset.querySelector('input[type="search"]');
        if (!search.value) {
          var selected = fieldset.querySelector('input[type="radio"][checked]');
          search.value = selected ? selected.parentElement.parentElement.querySelector('.radio-details--machine-name').innerText : '';
        }
        toggleRadioButtons(search.value, fieldset);
        subscribeToChanges(fieldset);
      }
    },
  };

  Drupal.theme.currentlySelectedClComponent = (id, name, description, status, version, source, createdDate, updatedDate, img, previewUrl) => `
    <summary>${Drupal.t('ℹ️ More information about <em>@name</em>', { '@name': name })}</summary>
    <p>${description}</p>
    <div class='image-table--wrapper'>
      <table>
        <tr><th>${Drupal.t('Version')}</th><td>${version}</td></tr>
        <tr><th>${Drupal.t('Created')}</th><td>${createdDate}</td></tr>
        <tr><th>${Drupal.t('Updated')}</th><td>${updatedDate}</td></tr>
        <tr><th>${Drupal.t('Source')}</th><td>${source}</td></tr>
        <tr><th>${Drupal.t('Status')}</th><td>${status}</td></tr>
      </table>
      <div class='currently-selected--image--wrapper${img ? '' : ' currently-selected--image--wrapper__empty'}'>
        ${img ? img : ''}
      </div>
    </div>
    ${previewUrl
      ? `<div style='display: none' id='preview-url'>${previewUrl}</div>
      <div class="try-now--wrapper"><a href="#preview-url" class='try-now button button--primary'>${Drupal.t('Try now')}</a></div>`
      : ''
    }`;

  /**
   * Render more info about the currently selected component.
   */
  Drupal.behaviors.currentlySelected = {
    attach: (context, settings) => {
      var fieldsets = once('currently-selected', '.widget-type--selector', context);
      for (var fieldset of fieldsets) {
        var info = fieldset.querySelector('.currently-selected');
        info.hidden = true;
        var selected = fieldset.querySelector('input[type="radio"][checked]');
        if (selected) {
          selected.parentElement.parentElement.parentElement.classList.add('form-type--radio__selected');
          renderCurrentlySelected(info, selected.parentElement.parentElement);
        }
        var radios = once('radio-change-subscribed', 'input[type="radio"]', fieldset);
        for (radio of radios) {
          radio.addEventListener('change', (event) => {
            if (event.target.checked) {
              var all = event.target.parentElement.parentElement.parentElement.parentElement.querySelectorAll('input[type="radio"]');
              for (var item of all) {
                item.parentElement.parentElement.parentElement.classList.remove('form-type--radio__selected');
              }
              event.target.parentElement.parentElement.parentElement.classList.add('form-type--radio__selected');
              renderCurrentlySelected(info, event.target.parentElement.parentElement);
            }
          });
        }
      }
    },
  };

}(once));
+245 −0
Original line number Diff line number Diff line
<?php

namespace Drupal\widget_type\Element;

use Drupal\Component\Plugin\Exception\InvalidPluginDefinitionException;
use Drupal\Component\Plugin\Exception\PluginNotFoundException;
use Drupal\Component\Utility\Crypt;
use Drupal\Core\Entity\EntityTypeManagerInterface;
use Drupal\Core\Form\FormStateInterface;
use Drupal\Core\Plugin\ContainerFactoryPluginInterface;
use Drupal\Core\Render\Element;
use Drupal\Core\Render\Element\FormElement;
use Drupal\Core\Render\Element\Radios;
use Drupal\Core\Site\Settings;
use Drupal\file\FileInterface;
use Drupal\widget_type\WidgetRegistrySourceInterface;
use Drupal\widget_type\WidgetTypeInterface;
use Symfony\Component\DependencyInjection\ContainerInterface;

/**
 * Provides a component selector element.
 *
 * @FormElement("widget_type_selector")
 */
class WidgetSelectorElement extends FormElement implements ContainerFactoryPluginInterface {

  /**
   * @var \Drupal\Core\Entity\EntityTypeManagerInterface
   */
  protected EntityTypeManagerInterface $entityTypeManager;

  /**
   * {@inheritdoc}
   */
  public function __construct(
    array $configuration,
    $plugin_id,
    $plugin_definition,
    $entity_type_manager
  ) {
    parent::__construct($configuration, $plugin_id, $plugin_definition);
    $this->entityTypeManager = $entity_type_manager;
  }

  /**
   * {@inheritdoc}
   */
  public static function create(ContainerInterface $container, array $configuration, $plugin_id, $plugin_definition) {
    $entity_type_manager = $container->get('entity_type.manager');
    return new static($configuration, $plugin_id, $plugin_definition, $entity_type_manager);
  }

  /**
   * @inheritDoc
   */
  public function getInfo() {
    return [
      '#title' => $this->t('Widget Selector'),
      '#process' => [[$this, 'populateOptions']],
      '#element_validate' => [[$this, 'validateExistingWidgetType']],
      '#theme_wrappers' => ['widget_type_selector', 'fieldset'],
      '#submit' => [[$this, 'submitForm']],
      '#input' => TRUE,
    ];
  }

  /**
   * Form API process callback.
   */
  public function populateOptions(
    array &$element,
    FormStateInterface $form_state,
    array &$complete_form
  ): array {
    try {
      $storage = $this->entityTypeManager->getStorage('widget_type');
    } catch (InvalidPluginDefinitionException|PluginNotFoundException $e) {
      watchdog_exception('widget_type', $e);
      return $element;
    }
    $widget_types = isset($element['#options'])
      ? $storage->loadMultiple(array_keys($element['#options']))
      : $storage->loadMultiple();
    $options = array_map(
      static fn(WidgetTypeInterface $widget_type) => $widget_type->label(),
      $widget_types
    );
    ksort($options);

    $target_type = 'widget_type';
    $selection_handler = 'default:widget_type';
    $selection_settings = [
      'widget_type_fields' => [
        'name',
        'remote_widget_version',
        'widget_registry_source',
      ],
    ];
    $selection_settings_key = Crypt::hmacBase64(
      serialize($selection_settings) . $target_type . $selection_handler,
      Settings::getHashSalt()
    );
    \Drupal::keyValue('entity_autocomplete')->set($selection_settings_key, $selection_settings);

    $default_id = $element['#default_value']['target_id'] ?? NULL;
    $default_widget_type = ($default_id ? $widget_types[$default_id] : NULL) ?? NULL;
    $element += [
      '#attached' => ['library' => ['widget_type/selector']],
      'search' => [
        '#title' => $this->t('Search'),
        '#title_display' => 'hidden',
        '#type' => 'search',
        '#default_value' => $default_widget_type instanceof WidgetTypeInterface ? $default_widget_type->getRemoteId() : NULL,
        '#placeholder' => $this->t('Search for a widget type'),
        '#size' => 50,
        '#description' => $this->t('Start typing to search for a widget type.'),
        '#input' => FALSE,
      ],
      'target_id' => [
        '#type' => 'radios',
        '#options' => $options,
        '#title' => $this->t('Widgets'),
        '#title_display' => 'invisible',
        '#default_value' => $default_widget_type ? $default_widget_type->id() : NULL,
        '#process' => [
          [Radios::class, 'processRadios'],
          [$this, 'processRadios'],
        ],
        '#attributes' => [
          'class' => ['widget-type-selector--radios'],
        ],
        '#ajax' => $element['#ajax'] ?? FALSE,
        '#input' => FALSE,
      ],
    ];
    $classes = $element['#attributes']['class'] ?? [];
    $classes[] = 'widget-type--selector';
    $element['#attributes']['class'] = $classes;
    unset($element['#default_value'], $element['#ajax'], $element['#options']);
    return $element;
  }

  /**
   * Process the radios.
   */
  public function processRadios(array $element, FormStateInterface $form_state): array {
    $keys = Element::children($element);
    try {
      $storage = $this->entityTypeManager->getStorage('widget_type');
    } catch (InvalidPluginDefinitionException|PluginNotFoundException  $e) {
      watchdog_exception('widget_type', $e);
      return $element;
    }
    $widget_type_ids = array_filter(
      array_map(
        static fn(array $item) => $item['#return_value'] ?? NULL,
        array_intersect_key($element, array_flip($keys))
      )
    );
    $widget_types = $storage->loadMultiple($widget_type_ids);
    foreach ($keys as $key) {
      $element[$key]['#theme_wrappers'] = [
        'form_element__radio__widget_type',
        'form_element__radio',
      ];
      $id = $element[$key]['#return_value'];
      $widget_type = $widget_types[$id];
      assert($widget_type instanceof WidgetTypeInterface);
      $source = $widget_type->get('widget_registry_source');
      $element[$key]['#title_display'] = 'hidden';
      $element[$key]['#entity'] = $widget_type;
      $element[$key]['#human_name'] = $widget_type->getName();
      $element[$key]['#machine_name'] = $widget_type->getRemoteId();
      $element[$key]['#remote_description'] = $widget_type->getDescription();
      $field_image = $widget_type->getPreviewImage();
      $thumbnail = ['#markup' => ''];
      $image = ['#markup' => ''];
      if ($field_image['file'] instanceof FileInterface) {
        $uri = $field_image['file']->getFileUri();
        $alt = $field_image['alt'] ?? $this->t('Thumbnail');
        $title = $field_image['title'] ?? $this->t('Add a thumbnail.png into your widget to make it show up here.');
        $thumbnail = [
          '#theme' => 'image_style',
          '#style_name' => 'widget_type_small_16_9',
          '#alt' => $alt,
          '#title' => $title,
          '#uri' => $uri,
          '#attributes' => ['loading' => 'lazy'],
        ];
        $image = [
          '#theme' => 'image',
          '#alt' => $alt,
          '#title' => $title,
          '#uri' => $uri,
          '#attributes' => [
            'loading' => 'lazy',
            'class' => ['radio-details--image']
          ],
        ];
      }
      $element[$key]['#thumbnail'] = $thumbnail;
      $element[$key]['#image'] = $image;
      $element[$key]['#preview_url'] = $widget_type->getPreviewLink();
      $element[$key]['#status'] = $widget_type->isEnabled() ? $this->t('Enabled') : $this->t('Disabled');
      $element[$key]['#remote_id'] = $widget_type->getRemoteId();
      $element[$key]['#version'] = $widget_type->getVersion();
      $element[$key]['#created_date'] = \Drupal::service('date.formatter')->format($widget_type->getCreatedTime());
      $element[$key]['#updated_date'] = \Drupal::service('date.formatter')->format($widget_type->getCreatedTime());
      $element[$key]['#source'] = $source->entity instanceof WidgetRegistrySourceInterface
        ? $source->entity->label()
        : '';
      $element[$key]['#library_dependencies'] = $widget_type->getLibraryDependencies();
      $element[$key]['#remote_languages'] = $widget_type->getRemoteLanguages();
    }
    return $element;
  }

  /**
   * Validator for form element.
   */
  public function validateExistingWidgetType(array $element, FormStateInterface $form_state): void {
    $parents = $element['#parents'] ?? [];
    $parents[] = 'target_id';
    $value = $form_state->getValue($parents);
    if (!is_scalar($value)) {
      $value = NULL;
      $form_state->setValue($parents, NULL);
    }
    if (!$value) {
      return;
    }
    try {
      $storage = $this->entityTypeManager->getStorage('widget_type');
      if ($storage->load($value)) {
        return;
      }
    } catch (InvalidPluginDefinitionException|PluginNotFoundException $e) {
    }
    $form_state->setError(
      $element['target_id'],
      $this->t('Invalid component ID: @id', ['@id' => $value])
    );
  }

}
Loading