Code Coverage
 
Lines
Branches
Paths
Functions and Methods
Classes and Traits
Total
0.00% covered (danger)
0.00%
0 / 57
0.00% covered (danger)
0.00%
0 / 17
0.00% covered (danger)
0.00%
0 / 22
0.00% covered (danger)
0.00%
0 / 5
CRAP
0.00% covered (danger)
0.00%
0 / 1
TextareaWidget
0.00% covered (danger)
0.00%
0 / 51
0.00% covered (danger)
0.00%
0 / 17
0.00% covered (danger)
0.00%
0 / 22
0.00% covered (danger)
0.00%
0 / 5
132
0.00% covered (danger)
0.00%
0 / 1
 create
0.00% covered (danger)
0.00%
0 / 8
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 settingsForm
0.00% covered (danger)
0.00%
0 / 23
0.00% covered (danger)
0.00%
0 / 9
0.00% covered (danger)
0.00%
0 / 16
0.00% covered (danger)
0.00%
0 / 1
30
 getPropValue
0.00% covered (danger)
0.00%
0 / 10
0.00% covered (danger)
0.00%
0 / 3
0.00% covered (danger)
0.00%
0 / 2
0.00% covered (danger)
0.00%
0 / 1
6
 settingsSummary
0.00% covered (danger)
0.00%
0 / 5
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 getFormatId
0.00% covered (danger)
0.00%
0 / 5
0.00% covered (danger)
0.00%
0 / 3
0.00% covered (danger)
0.00%
0 / 2
0.00% covered (danger)
0.00%
0 / 1
6
1<?php
2
3declare(strict_types=1);
4
5namespace Drupal\display_builder\Plugin\UiPatterns\Source;
6
7use Drupal\Core\Form\FormStateInterface;
8use Drupal\Core\Render\Markup;
9use Drupal\Core\StringTranslation\TranslatableMarkup;
10use Drupal\filter\FilterFormatRepositoryInterface;
11use Drupal\ui_patterns\Attribute\Source;
12use Drupal\ui_patterns\SourcePluginPropValueWidget;
13use Drupal\ui_patterns\SourceTags;
14use Symfony\Component\DependencyInjection\ContainerInterface;
15
16/**
17 * Plugin implementation of the source for a simple editor.
18 *
19 * @toto add calculateDependencies() to the format id or find a better
20 * filter_format integration.
21 */
22#[Source(
23  id: 'textarea',
24  label: new TranslatableMarkup('Textarea'),
25  description: new TranslatableMarkup('Multi-line text field.'),
26  prop_types: ['string', 'identifier'],
27  tags: [SourceTags::Widget->value]
28)]
29class TextareaWidget extends SourcePluginPropValueWidget {
30
31  public const FILTER_FORMAT_ID = 'display_builder_html';
32
33  /**
34   * The filter format repository.
35   *
36   * @var \Drupal\filter\FilterFormatRepositoryInterface
37   */
38  protected $filterFormat;
39
40  /**
41   * {@inheritdoc}
42   */
43  public static function create(
44    ContainerInterface $container,
45    array $configuration,
46    $plugin_id,
47    $plugin_definition,
48  ) {
49    $plugin = parent::create(
50      $container,
51      $configuration,
52      $plugin_id,
53      $plugin_definition
54    );
55    $plugin->filterFormat = $container->get(FilterFormatRepositoryInterface::class);
56
57    return $plugin;
58  }
59
60  /**
61   * {@inheritdoc}
62   */
63  public function settingsForm(array $form, FormStateInterface $form_state): array {
64    $form = parent::settingsForm($form, $form_state);
65
66    $attributes = ['data-db-ckeditor' => 'true'];
67    $libraries = ['display_builder/textarea_ckeditor'];
68
69    // Soft integration: when ui_icons_ckeditor5 is enabled, also load its
70    // CKEditor plugin bundle so window.CKEditor5.icon is available. The editor
71    // JS (textarea_ckeditor.js) picks it up on its own to preserve and preview
72    // <drupal-icon> markup; without the module the bundle is simply absent and
73    // the editor loads unchanged. The data-db-ckeditor-icon flag tells the JS
74    // this instance must wait for that separate bundle to finish loading
75    // before creating the editor - it evaluates independently of the core
76    // CKEditor 5 DLL builds, so without the flag the editor can be created
77    // before it lands and silently omit the plugin.
78    if ($this->moduleHandler->moduleExists('ui_icons_ckeditor5')) {
79      $libraries[] = 'ui_icons_ckeditor5/icon';
80      $attributes['data-db-ckeditor-icon'] = 'true';
81    }
82
83    $form['value'] = [
84      '#type' => 'textarea',
85      '#default_value' => $this->getSetting('value'),
86      '#attributes' => $attributes,
87      '#attached' => ['library' => $libraries],
88    ];
89
90    $this->addRequired($form['value']);
91    $this->addTokenTreeLink($form, 'help');
92
93    // @todo allow when we can really use it as currently it does not copy in
94    // CKEditor.
95    if (isset($form['help'])) {
96      $form['help']['#click_insert'] = FALSE;
97    }
98
99    if (!isset($form['value']['#title'])) {
100      $form['value']['#title'] = $this->propDefinition['title'] ?? $this->propId;
101    }
102
103    // If our format is disabled or not usable, skip the editor.
104    if ($this->getFormatId() !== self::FILTER_FORMAT_ID) {
105      unset($form['value']['#attributes']['data-db-ckeditor']);
106      $form['value']['#description_display'] = 'before';
107      // @todo add doc link to this problem.
108      $form['value']['#description'] = $this->t('<mark>Warning</mark>: Display Builder filter format is disable or missing, HTML markup will be escaped.');
109    }
110
111    return $form;
112  }
113
114  /**
115   * {@inheritdoc}
116   *
117   * Unlike Textfield, this widget's value is allowed to contain HTML: it
118   * is run through the forced 'display_builder_html' text format (see
119   * config/optional/filter.format.display_builder_html.yml) via
120   * #type => 'processed_text', never #type => 'text_format' — the latter
121   * renders Drupal's format selector and attaches its editor library,
122   * which this widget must not do. The processed_text element is rendered
123   * eagerly here into a Markup object, rather than returned as a render
124   * array: 'string'-tagged sources can also be used to fill a 'slot' prop
125   * through SlotPropType's string-to-slot conversion path, which only
126   * trusts a MarkupInterface value (anything else is treated as plain text
127   * and passed to #plain_text, which requires a string and TypeErrors on
128   * an array). Returning Markup here keeps both paths - direct string/
129   * identifier props and the slot conversion - working, and StringPropType/
130   * IdentifierPropType still treat it as trusted so it is not re-escaped.
131   */
132  public function getPropValue(): mixed {
133    $value = parent::getPropValue();
134
135    if (empty($value)) {
136      return $value;
137    }
138
139    $value = $this->replaceTokens($value, TRUE);
140
141    $html = $this->normalizer->convertToString([
142      '#type' => 'processed_text',
143      '#text' => $value,
144      '#format' => $this->getFormatId(),
145    ]);
146
147    return Markup::create($html);
148  }
149
150  /**
151   * {@inheritdoc}
152   */
153  public function settingsSummary(): array {
154    $summary = parent::settingsSummary();
155    $text = \reset($summary);
156
157    return [
158      \html_entity_decode((string) $text),
159    ];
160  }
161
162  /**
163   * Get format id for our editor.
164   */
165  private function getFormatId(): string {
166    $format_id = self::FILTER_FORMAT_ID;
167    $formats = $this->filterFormat->getAllFormats();
168
169    if (!isset($formats[$format_id])) {
170      $format_id = $this->filterFormat->getFallbackFormatId();
171    }
172
173    return $format_id;
174  }
175
176}