WebformCompositeBase.php 55.3 KB
Newer Older
1
2
3
4
5
<?php

namespace Drupal\webform\Plugin\WebformElement;

use Drupal\Core\Form\FormStateInterface;
6
use Drupal\Core\Form\OptGroup;
7
use Drupal\Core\Render\Element as RenderElement;
8
use Drupal\Core\Render\Element;
9
use Drupal\Core\StringTranslation\TranslatableMarkup;
10
use Drupal\file\Entity\File;
11
use Drupal\webform\Entity\WebformOptions;
12
use Drupal\webform\Plugin\WebformElementAttachmentInterface;
13
use Drupal\webform\Plugin\WebformElementCompositeInterface;
14
use Drupal\webform\Plugin\WebformElementComputedInterface;
15
use Drupal\webform\Plugin\WebformElementEntityReferenceInterface;
16
use Drupal\webform\Utility\WebformArrayHelper;
17
18
use Drupal\webform\Utility\WebformElementHelper;
use Drupal\webform\Utility\WebformOptionsHelper;
19
use Drupal\webform\Plugin\WebformElementBase;
20
21
use Drupal\webform\WebformInterface;
use Drupal\webform\WebformSubmissionInterface;
22
use Symfony\Component\DependencyInjection\ContainerInterface;
23
24
25
26

/**
 * Provides a base for composite elements.
 */
27
abstract class WebformCompositeBase extends WebformElementBase implements WebformElementCompositeInterface, WebformElementAttachmentInterface {
28

29
30
31
32
33
  /**
   * Track managed file elements.
   *
   * @var array
   */
34
  protected $elementsManagedFiles = [];
35

36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
  /**
   * The file system service.
   *
   * @var \Drupal\Core\File\FileSystemInterface
   */
  protected $fileSystem;

  /**
   * The renderer.
   *
   * @var \Drupal\Core\Render\RendererInterface
   */
  protected $renderer;

  /**
   * The webform submission generation service.
   *
   * @var \Drupal\webform\WebformSubmissionGenerateInterface
   */
  protected $generate;

  /**
   * {@inheritdoc}
   */
  public static function create(ContainerInterface $container, array $configuration, $plugin_id, $plugin_definition) {
    $instance = parent::create($container, $configuration, $plugin_id, $plugin_definition);
    $instance->fileSystem = $container->get('file_system');
    $instance->renderer = $container->get('renderer');
    $instance->generate = $container->get('webform_submission.generate');
    return $instance;
  }

68
  /* ************************************************************************ */
69
  // Property definitions.
70
  /* ************************************************************************ */
71

72
73
74
  /**
   * {@inheritdoc}
   */
75
  protected function defineDefaultProperties() {
76
    $properties = [
77
78
      'default_value' => [],
      'title_display' => 'invisible',
79
80
      'disabled' => FALSE,
      'flexbox' => '',
81
      // Enhancements.
82
      'select2' => FALSE,
83
      'choices' => FALSE,
84
      'chosen' => FALSE,
85
86
      // Wrapper.
      'wrapper_type' => 'fieldset',
87
88
    ] + parent::defineDefaultProperties()
      + $this->defineDefaultMultipleProperties();
89
    unset($properties['required_error']);
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105

    $composite_elements = $this->getCompositeElements();
    foreach ($composite_elements as $composite_key => $composite_element) {
      // Get #type, #title, and #option from composite elements.
      foreach ($composite_element as $composite_property_key => $composite_property_value) {
        if (in_array($composite_property_key, ['#type', '#title', '#options'])) {
          $property_key = str_replace('#', $composite_key . '__', $composite_property_key);
          if ($composite_property_value instanceof TranslatableMarkup) {
            $properties[$property_key] = (string) $composite_property_value;
          }
          else {
            $properties[$property_key] = $composite_property_value;
          }
        }
      }
      if (isset($properties[$composite_key . '__type'])) {
106
        $properties[$composite_key . '__title_display'] = '';
107
108
        $properties[$composite_key . '__description'] = '';
        $properties[$composite_key . '__help'] = '';
109
110
111
112
113
114
115
116
        $properties[$composite_key . '__required'] = FALSE;
        $properties[$composite_key . '__placeholder'] = '';
      }
      $properties[$composite_key . '__access'] = TRUE;
    }
    return $properties;
  }

117
118
119
  /**
   * {@inheritdoc}
   */
120
  protected function defineDefaultMultipleProperties() {
121
122
123
    return [
      'multiple__header' => FALSE,
      'multiple__header_label' => '',
124
    ] + parent::defineDefaultMultipleProperties();
125
126
  }

127
128
129
130
131
132
133
  /**
   * {@inheritdoc}
   */
  protected function defineTranslatableProperties() {
    return array_merge(parent::defineTranslatableProperties(), ['default_value']);
  }

134
  /* ************************************************************************ */
135
  // Property methods.
136
  /* ************************************************************************ */
137

138
139
140
141
142
143
144
  /**
   * {@inheritdoc}
   */
  public function hasManagedFiles(array $element) {
    return ($this->getManagedFiles($element)) ? TRUE : FALSE;
  }

145
  /* ************************************************************************ */
146
  // Element relationship methods.
147
  /* ************************************************************************ */
148

149
150
151
152
153
154
155
  /**
   * {@inheritdoc}
   */
  public function getRelatedTypes(array $element) {
    return [];
  }

156
  /* ************************************************************************ */
157
  // Element rendering methods.
158
  /* ************************************************************************ */
159
160

  /**
161
   * {@inheritdoc}
162
   */
163
164
165
  public function initialize(array &$element) {
    parent::initialize($element);

166
    $this->initializeCompositeElements($element);
167
  }
168
169
170
171

  /**
   * {@inheritdoc}
   */
172
  public function prepare(array &$element, WebformSubmissionInterface $webform_submission = NULL) {
173
174
175
176
    parent::prepare($element, $webform_submission);

    // If #flexbox is not set or an empty string, determine if the
    // webform is using a flexbox layout.
177
    if ((!isset($element['#flexbox']) || $element['#flexbox'] === '') && $webform_submission) {
178
179
180
181
182
183
      $webform = $webform_submission->getWebform();
      $element['#flexbox'] = $webform->hasFlexboxLayout();
    }
  }

  /**
184
185
186
187
   * Set multiple element wrapper.
   *
   * @param array $element
   *   An element.
188
   */
189
  protected function prepareMultipleWrapper(array &$element) {
190
191
192
    if (empty($element['#multiple']) || !$this->supportsMultipleValues()) {
      return;
    }
193

194
    parent::prepareMultipleWrapper($element);
195

196
    // Set #header.
197
198
    if (!empty($element['#multiple__header'])) {
      $element['#header'] = TRUE;
199

200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
      // Set #element.
      // We don't need to get the initialized composite elements because
      // they will be initialized, prepared, and finalize by the
      // WebformMultiple (wrapper) element.
      // @see \Drupal\webform\Element\WebformMultiple::processWebformMultiple
      $element['#element'] = [];
      $composite_elements = $this->getCompositeElements();
      foreach (Element::children($composite_elements) as $composite_key) {
        $composite_element = $composite_elements[$composite_key];
        // Transfer '#{composite_key}_{property}' from main element to composite
        // element.
        foreach ($element as $property_key => $property_value) {
          if (strpos($property_key, '#' . $composite_key . '__') === 0) {
            $composite_property_key = str_replace('#' . $composite_key . '__', '#', $property_key);
            $composite_element[$composite_property_key] = $property_value;
          }
216
217
        }

218
219
        $element['#element'][$composite_key] = $composite_element;
      }
220
221
222
    }
  }

223
  /* ************************************************************************ */
224
  // Table methods.
225
  /* ************************************************************************ */
226

227
228
229
230
231
232
233
234
235
236
237
238
239
  /**
   * {@inheritdoc}
   */
  public function getTableColumn(array $element) {
    $key = $element['#webform_key'];
    $title = $element['#title'] ?: $key;
    $is_title_displayed = WebformElementHelper::isTitleDisplayed($element);

    // Get the main composite element, which can't be sorted.
    $columns = parent::getTableColumn($element);
    $columns['element__' . $key]['sort'] = FALSE;

    // Get individual composite elements.
240
241
242
243
244
245
246
247
248
    if (!$this->hasMultipleValues($element)) {
      $composite_elements = $this->getInitializedCompositeElement($element);
      foreach (RenderElement::children($composite_elements) as $composite_key) {
        $composite_element = $composite_elements[$composite_key];
        // Make sure the composite element is visible.
        $access_key = '#' . $composite_key . '__access';
        if (isset($element[$access_key]) && $element[$access_key] === FALSE) {
          continue;
        }
249

250
251
252
253
254
255
256
257
258
259
260
261
262
263
        // Add reference to initialized composite element so that it can be
        // used by ::formatTableColumn().
        $columns['element__' . $key . '__' . $composite_key] = [
          'title' => ($is_title_displayed ? $title . ': ' : '') . (!empty($composite_element['#title']) ? $composite_element['#title'] : $composite_key),
          'sort' => TRUE,
          'default' => FALSE,
          'key' => $key,
          'element' => $element,
          'property_name' => $composite_key,
          'composite_key' => $composite_key,
          'composite_element' => $composite_element,
          'plugin' => $this,
        ];
      }
264
    }
265

266
267
268
269
270
271
    return $columns;
  }

  /**
   * {@inheritdoc}
   */
272
  public function formatTableColumn(array $element, WebformSubmissionInterface $webform_submission, array $options = []) {
273
274
    if (isset($options['composite_key']) && isset($options['composite_element'])) {
      $composite_element = $options['composite_element'];
275
276
      $composite_element['#webform_key'] = $element['#webform_key'];
      return $this->elementManager->invokeMethod('formatHtml', $composite_element, $webform_submission, $options);
277
278
    }
    else {
279
      return $this->formatHtml($element, $webform_submission);
280
281
282
    }
  }

283
  /* ************************************************************************ */
284
  // #states API methods.
285
  /* ************************************************************************ */
286

287
288
289
290
  /**
   * {@inheritdoc}
   */
  public function getElementSelectorOptions(array $element) {
291
292
293
294
    if ($this->hasMultipleValues($element)) {
      return [];
    }

295
296
297
298
299
300
    $title = $this->getAdminLabel($element) . ' [' . $this->getPluginLabel() . ']';
    $name = $element['#webform_key'];

    $selectors = [];
    $composite_elements = $this->getInitializedCompositeElement($element);
    foreach ($composite_elements as $composite_key => $composite_element) {
301
      if (Element::isVisibleElement($composite_elements) && isset($composite_element['#type'])) {
302
        $element_plugin = $this->elementManager->getElementInstance($composite_element);
303
304
305
306
307
308
        $composite_element['#webform_key'] = "{$name}[{$composite_key}]";
        $selectors += OptGroup::flattenOptions($element_plugin->getElementSelectorOptions($composite_element));
      }
    }
    return [$title => $selectors];
  }
309

310
311
312
313
314
315
316
  /**
   * {@inheritdoc}
   */
  public function getElementSelectorSourceValues(array $element) {
    if ($this->hasMultipleValues($element)) {
      return [];
    }
317

318
    $name = $element['#webform_key'];
319

320
321
322
    $source_values = [];
    $composite_elements = $this->getInitializedCompositeElement($element);
    foreach ($composite_elements as $composite_key => $composite_element) {
323
      if (Element::isVisibleElement($composite_elements) && isset($composite_element['#type'])) {
324
325
326
        $element_plugin = $this->elementManager->getElementInstance($composite_element);
        $composite_element['#webform_key'] = "{$name}[{$composite_key}]";
        $source_values += $element_plugin->getElementSelectorSourceValues($composite_element);
327
328
      }
    }
329
    return $source_values;
330
331
  }

332
  /* ************************************************************************ */
333
  // Display submission value methods.
334
  /* ************************************************************************ */
335

336
337
338
  /**
   * {@inheritdoc}
   */
339
340
341
342
343
344
345
  public function formatHtml(array $element, WebformSubmissionInterface $webform_submission, array $options = []) {
    if (isset($options['composite_key'])) {
      return $this->formatCompositeHtml($element, $webform_submission, $options);
    }
    else {
      return parent::formatHtml($element, $webform_submission, $options);
    }
346
347
348
  }

  /**
349
   * {@inheritdoc}
350
   */
351
352
353
354
355
356
  public function formatText(array $element, WebformSubmissionInterface $webform_submission, array $options = []) {
    if (isset($options['composite_key'])) {
      return $this->formatCompositeText($element, $webform_submission, $options);
    }
    else {
      return parent::formatText($element, $webform_submission, $options);
357
358
359
360
361
362
    }
  }

  /**
   * {@inheritdoc}
   */
363
  protected function formatCustomItem($type, array &$element, WebformSubmissionInterface $webform_submission, array $options = [], array $context = []) {
364
365
366
367
368
369
    $name = strtolower($type);

    // Parse element.composite_key from template and add composite element
    // to context.
    $template = trim($element['#format_' . $name]);
    if (preg_match_all("/element(?:\[['\"]|\.)([a-zA-Z0-9-_:]+)/", $template, $matches)) {
370
      $composite_elements = $this->getInitializedCompositeElement($element);
371
372
373
      $composite_keys = array_unique($matches[1]);

      $item_function = 'format' . $type;
374
      $context['element'] = [];
375
376
      foreach ($composite_keys as $composite_key) {
        if (isset($composite_elements[$composite_key])) {
377
          $context['element'][$composite_key] = $this->$item_function(['#format' => NULL] + $element, $webform_submission, ['composite_key' => $composite_key] + $options);
378
379
380
        }
      }
    }
381

382
    return parent::formatCustomItem($type, $element, $webform_submission, $options, $context);
383
384
385
386
387
  }

  /**
   * {@inheritdoc}
   */
388
  protected function formatHtmlItem(array $element, WebformSubmissionInterface $webform_submission, array $options = []) {
389
    if (!$this->hasValue($element, $webform_submission, $options)) {
390
391
392
      return '';
    }

393
    $format = $this->getItemFormat($element);
394
395
396
397
398
399

    // Handle custom composite html items.
    if ($format === 'custom' && !empty($element['#format_html'])) {
      return $this->formatCustomItem('html', $element, $webform_submission, $options);
    }

400
401
402
    switch ($format) {
      case 'list':
      case 'raw':
403
        $items = $this->formatCompositeHtmlItems($element, $webform_submission, $options);
404
405
406
407
408
409
        return [
          '#theme' => 'item_list',
          '#items' => $items,
        ];

      default:
410
        $lines = $this->formatHtmlItemValue($element, $webform_submission, $options);
411
412
413
        if (empty($lines)) {
          return '';
        }
414
        foreach ($lines as $key => $line) {
415
          if (is_string($line)) {
416
            $lines[$key] = ['#markup' => $line];
417
          }
418
          $lines[$key]['#suffix'] = '<br />';
419
        }
420
421
        // Remove the <br/> suffix from the last line.
        unset($lines[$key]['#suffix']);
422
423
424
425
426
427
428
        return $lines;
    }
  }

  /**
   * {@inheritdoc}
   */
429
  protected function formatHtmlItems(array &$element, WebformSubmissionInterface $webform_submission, array $options = []) {
430
431
432
433
434
435
436
437
438
439
    $format = $this->getItemsFormat($element);
    if ($format !== 'table') {
      return parent::formatHtmlItems($element, $webform_submission, $options);
    }

    $composite_elements = $this->getInitializedCompositeElement($element);

    // Get header.
    $header = [];
    foreach (RenderElement::children($composite_elements) as $composite_key) {
440
      if (!Element::isVisibleElement($composite_elements[$composite_key])) {
441
442
443
444
445
        unset($composite_elements[$composite_key]);
        continue;
      }

      $composite_element = $composite_elements[$composite_key];
Jacob Rockowitz's avatar
Jacob Rockowitz committed
446
      $header[$composite_key] = [
447
        'data' => $composite_element['#title'] ?? $composite_key,
Jacob Rockowitz's avatar
Jacob Rockowitz committed
448
449
        'bgcolor' => '#eee',
      ];
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
    }

    // Get rows.
    $rows = [];
    $values = $this->getValue($element, $webform_submission, $options);
    foreach ($values as $delta => $value) {
      foreach ($header as $composite_key => $composite_title) {
        $composite_value = $this->formatCompositeHtml($element, $webform_submission, ['delta' => $delta, 'composite_key' => $composite_key] + $options);
        if (is_array($composite_value)) {
          $rows[$delta][$composite_key] = ['data' => $composite_value];
        }
        else {
          $rows[$delta][$composite_key] = ['data' => ['#markup' => $composite_value]];
        }
      }
    }

    return [
      '#type' => 'table',
      '#header' => $header,
      '#rows' => $rows,
Jacob Rockowitz's avatar
Jacob Rockowitz committed
471
      '#attributes' => [
472
        'width' => '100%',
Jacob Rockowitz's avatar
Jacob Rockowitz committed
473
474
475
        'cellspacing' => 0,
        'cellpadding' => 5,
        'border' => 1,
476
      ],
477
478
479
    ];
  }

480
481
482
  /**
   * {@inheritdoc}
   */
483
  protected function formatTextItems(array &$element, WebformSubmissionInterface $webform_submission, array $options = []) {
484
485
    $format = $this->getItemsFormat($element);
    if ($format === 'table') {
486
      $element['#format_items'] = 'hr';
487
488
489
490
    }
    return parent::formatTextItems($element, $webform_submission, $options);
  }

491
492
493
  /**
   * {@inheritdoc}
   */
494
  protected function formatTextItem(array $element, WebformSubmissionInterface $webform_submission, array $options = []) {
495
    if (!$this->hasValue($element, $webform_submission, $options)) {
496
497
498
      return '';
    }

499
    $format = $this->getItemFormat($element);
500
501
502
503
504
505

    // Handle custom composite text items.
    if ($format === 'custom' && !empty($element['#format_text'])) {
      return $this->formatCustomItem('text', $element, $webform_submission, $options);
    }

506
507
508
    switch ($format) {
      case 'list':
      case 'raw':
509
510
        $lines = $this->formatCompositeTextItems($element, $webform_submission, $options);
        return implode(PHP_EOL, $lines);
511
512

      default:
513
        $lines = $this->formatTextItemValue($element, $webform_submission, $options);
514
        return implode(PHP_EOL, $lines);
515
516
517
    }
  }

518
  /**
519
   * Format a composite as a list of HTML items.
520
521
   *
   * @param array $element
522
523
524
525
526
   *   An element.
   * @param \Drupal\webform\WebformSubmissionInterface $webform_submission
   *   A webform submission.
   * @param array $options
   *   An array of options.
527
   *
528
529
   * @return array|string
   *   A composite as a list of HTML items.
530
   */
531
532
  protected function formatCompositeHtmlItems(array $element, WebformSubmissionInterface $webform_submission, array $options = []) {
    $format = $this->getItemFormat($element);
533
534
535
    $items = [];
    $composite_elements = $this->getInitializedCompositeElement($element);
    foreach (RenderElement::children($composite_elements) as $composite_key) {
536
      $composite_element = $composite_elements[$composite_key];
537
      $composite_title = (isset($composite_element['#title']) && $format !== 'raw') ? $composite_element['#title'] : $composite_key;
538
539
540
541
542
543
544
545
546
547
      $composite_value = $this->formatCompositeHtml($element, $webform_submission, ['composite_key' => $composite_key] + $options);
      if ($composite_value !== '') {
        $items[$composite_key] = [
          '#type' => 'inline_template',
          '#template' => '<b>{{ title }}:</b> {{ value }}',
          '#context' => [
            'title' => $composite_title,
            'value' => $composite_value,
          ],
        ];
548
549
550
551
552
553
      }
    }
    return $items;
  }

  /**
554
   * Format a composite as a list of plain text items.
555
556
   *
   * @param array $element
557
558
559
560
561
   *   An element.
   * @param \Drupal\webform\WebformSubmissionInterface $webform_submission
   *   A webform submission.
   * @param array $options
   *   An array of options.
562
   *
563
564
   * @return array|string
   *   A composite as a list of plain text items.
565
   */
566
567
  protected function formatCompositeTextItems(array $element, WebformSubmissionInterface $webform_submission, array $options = []) {
    $format = $this->getItemFormat($element);
568
569
570
    $items = [];
    $composite_elements = $this->getInitializedCompositeElement($element);
    foreach (RenderElement::children($composite_elements) as $composite_key) {
571
572
      $composite_element = $composite_elements[$composite_key];

573
      $composite_title = (isset($composite_element['#title']) && $format !== 'raw') ? $composite_element['#title'] : $composite_key;
574
575
576

      $composite_value = $this->formatCompositeText($element, $webform_submission, ['composite_key' => $composite_key] + $options);
      if (is_array($composite_value)) {
577
        $composite_value = $this->renderer->renderPlain($composite_value);
578
579
580
      }

      if ($composite_value !== '') {
581
582
583
584
585
586
        $items[$composite_key] = "$composite_title: $composite_value";
      }
    }
    return $items;
  }

587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
  /**
   * Format a composite's sub element's value as HTML.
   *
   * @param array $element
   *   An element.
   * @param \Drupal\webform\WebformSubmissionInterface $webform_submission
   *   A webform submission.
   * @param array $options
   *   An array of options.
   *
   * @return array|string
   *   A composite's sub element's value formatted as an HTML string or a render array.
   */
  protected function formatCompositeHtml(array $element, WebformSubmissionInterface $webform_submission, array $options = []) {
    return $this->formatComposite('Html', $element, $webform_submission, $options);
  }

  /**
605
   * Format a composite's sub element's value as plain text.
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
   *
   * @param array $element
   *   An element.
   * @param \Drupal\webform\WebformSubmissionInterface $webform_submission
   *   A webform submission.
   * @param array $options
   *   An array of options.
   *
   * @return array|string
   *   A composite's sub element's value formatted as plain text or a render array.
   */
  protected function formatCompositeText(array $element, WebformSubmissionInterface $webform_submission, array $options = []) {
    return $this->formatComposite('Text', $element, $webform_submission, $options);
  }

  /**
   * Format a composite's sub element's value as HTML or plain text.
   *
   * @param string $type
   *   The format type, HTML or Text.
   * @param array $element
   *   An element.
   * @param \Drupal\webform\WebformSubmissionInterface $webform_submission
   *   A webform submission.
   * @param array $options
   *   An array of options.
   *
   * @return array|string
   *   The element's value formatted as plain text or a render array.
   */
  protected function formatComposite($type, array $element, WebformSubmissionInterface $webform_submission, array $options = []) {
    $options['webform_key'] = $element['#webform_key'];
    $composite_element = $this->getInitializedCompositeElement($element, $options['composite_key']);
    $composite_plugin = $this->elementManager->getElementInstance($composite_element);
640
641
642
643
644
645
646

    // Exclude attachments for composite element.
    // @see \Drupal\webform\WebformSubmissionViewBuilder::isElementVisible
    if (!empty($options['exclude_attachments']) && $composite_plugin instanceof WebformElementAttachmentInterface) {
      return '';
    }

647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
    $format_function = 'format' . $type;
    return $composite_plugin->$format_function($composite_element, $webform_submission, $options);
  }

  /**
   * Format composite element value into lines of text.
   *
   * @param array $element
   *   An element.
   * @param \Drupal\webform\WebformSubmissionInterface $webform_submission
   *   A webform submission.
   * @param array $options
   *   An array of options.
   *
   * @return array
   *   Composite element values converted into lines of html.
   */
  protected function formatHtmlItemValue(array $element, WebformSubmissionInterface $webform_submission, array $options = []) {
    return $this->formatCompositeHtmlItems($element, $webform_submission, $options);
  }

  /**
   * Format composite element value into lines of text.
   *
   * @param array $element
   *   An element.
   * @param \Drupal\webform\WebformSubmissionInterface $webform_submission
   *   A webform submission.
   * @param array $options
   *   An array of options.
   *
   * @return array
   *   Composite element values converted into lines of text.
   */
  protected function formatTextItemValue(array $element, WebformSubmissionInterface $webform_submission, array $options = []) {
    return $this->formatCompositeTextItems($element, $webform_submission, $options);
  }

685
686
687
688
689
  /**
   * {@inheritdoc}
   */
  public function getItemFormats() {
    return parent::getItemFormats() + [
690
691
      'list' => $this->t('List'),
    ];
692
693
  }

694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
  /**
   * {@inheritdoc}
   */
  public function getItemsDefaultFormat() {
    return 'ul';
  }

  /**
   * {@inheritdoc}
   */
  public function getItemsFormats() {
    return [
      'ol' => $this->t('Ordered list'),
      'ul' => $this->t('Unordered list'),
      'hr' => $this->t('Horizontal rule'),
709
      'table' => $this->t('Table'),
710
711
712
    ];
  }

713
  /* ************************************************************************ */
714
  // Export methods.
715
  /* ************************************************************************ */
716

717
718
719
720
721
722
723
724
725
726
727
728
729
  /**
   * {@inheritdoc}
   */
  public function getExportDefaultOptions() {
    return [
      'composite_element_item_format' => 'label',
    ];
  }

  /**
   * {@inheritdoc}
   */
  public function buildExportOptionsForm(array &$form, FormStateInterface $form_state, array $export_options) {
730
731
732
733
734
    parent::buildExportOptionsForm($form, $form_state, $export_options);
    if (isset($form['composite'])) {
      return;
    }

735
736
    $form['composite'] = [
      '#type' => 'details',
737
      '#title' => $this->t('Composite element options'),
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
      '#open' => TRUE,
      '#weight' => -10,
    ];
    $form['composite']['composite_element_item_format'] = [
      '#type' => 'radios',
      '#title' => $this->t('Composite element item format'),
      '#options' => [
        'label' => $this->t('Option labels, the human-readable value (label)'),
        'key' => $this->t('Option values, the raw value stored in the database (key)'),
      ],
      '#default_value' => $export_options['composite_element_item_format'],
    ];
  }

  /**
   * {@inheritdoc}
   */
  public function buildExportHeader(array $element, array $options) {
756
    if ($this->hasMultipleValues($element)) {
757
758
759
      return parent::buildExportHeader($element, $options);
    }

760
761
762
763
    $composite_elements = $this->getInitializedCompositeElement($element);
    $header = [];
    foreach (RenderElement::children($composite_elements) as $composite_key) {
      $composite_element = $composite_elements[$composite_key];
764
      if (!Element::isVisibleElement($composite_element)) {
765
766
767
        continue;
      }

768
      if ($options['header_format'] === 'label' && !empty($composite_element['#title'])) {
769
770
771
772
773
774
775
776
777
778
779
780
781
        $header[] = $composite_element['#title'];
      }
      else {
        $header[] = $composite_key;
      }
    }

    return $this->prefixExportHeader($header, $element, $options);
  }

  /**
   * {@inheritdoc}
   */
782
783
784
  public function buildExportRecord(array $element, WebformSubmissionInterface $webform_submission, array $export_options) {
    $value = $this->getValue($element, $webform_submission);

785
    if ($this->hasMultipleValues($element)) {
786
      $element['#format'] = ($export_options['header_format'] === 'label') ? 'list' : 'raw';
787
      $export_options['multiple_delimiter'] = PHP_EOL . '---' . PHP_EOL;
788
      return parent::buildExportRecord($element, $webform_submission, $export_options);
789
790
    }

791
792
793
794
    $record = [];
    $composite_elements = $this->getInitializedCompositeElement($element);
    foreach (RenderElement::children($composite_elements) as $composite_key) {
      $composite_element = $composite_elements[$composite_key];
795
      if (!Element::isVisibleElement($composite_element)) {
796
797
798
        continue;
      }

799
      if ($export_options['composite_element_item_format'] === 'label' && $composite_element['#type'] !== 'textfield' && !empty($composite_element['#options'])) {
800
801
802
        $record[] = WebformOptionsHelper::getOptionText($value[$composite_key], $composite_element['#options']);
      }
      else {
803
        $record[] = $value[$composite_key] ?? NULL;
804
805
806
807
808
      }
    }
    return $record;
  }

809
  /* ************************************************************************ */
810
  // Test methods.
811
  /* ************************************************************************ */
812

813
814
815
  /**
   * {@inheritdoc}
   */
816
  public function getTestValues(array $element, WebformInterface $webform, array $options = []) {
817
818
819
820
    if (empty($element['#webform_composite_elements'])) {
      $this->initialize($element);
    }

821
    $composite_elements = $this->getInitializedCompositeElement($element);
822
    $composite_elements = WebformElementHelper::getFlattened($composite_elements);
823

824
    $values = [];
825
    for ($i = 1; $i <= 3; $i++) {
826
827
828
829
830
      // Add delta to $options to allow multiple unique managed test files
      // to be created.
      // @see \Drupal\webform\Plugin\WebformElement\WebformManagedFileBase::getTestValues
      $options['delta'] = $i;

831
832
      $value = [];
      foreach (RenderElement::children($composite_elements) as $composite_key) {
833
        $value[$composite_key] = $this->generate->getTestValue($webform, $composite_key, $composite_elements[$composite_key], $options);
834
835
      }
      $values[] = $value;
836
    }
837
    return $values;
838
839
  }

840
  /* ************************************************************************ */
841
  // Element configuration methods.
842
  /* ************************************************************************ */
843

844
845
846
847
848
  /**
   * {@inheritdoc}
   */
  public function buildConfigurationForm(array $form, FormStateInterface $form_state) {
    $form = parent::buildConfigurationForm($form, $form_state);
849
    $form['custom']['properties']['#description'] .= '<br /><br />' .
850
851
852
      $this->t("You can set sub-element properties using a double underscore between the sub-element's key and sub-element's property (subelement__property). For example, you can add custom attributes or states (conditional logic) to the title sub-element using 'title__attributes' and 'title__states'.");
    return $form;
  }
853

854
855
856
857
858
859
860
  /**
   * {@inheritdoc}
   */
  public function form(array $form, FormStateInterface $form_state) {
    $form = parent::form($form, $form_state);

    // Update #default_value description.
861
    $form['default']['default_value']['#description'] = $this->t("The default value of the composite webform element as YAML.");
862
863

    // Update #required label.
864
    $form['validation']['required_container']['required']['#title'] .= ' <em>' . $this->t('(Display purposes only)') . '</em>';
865
    $form['validation']['required_container']['required']['#description'] = $this->t('If checked, adds required indicator to the title, if visible. To required individual elements, also tick "Required" under the @name settings above.', ['@name' => $this->getPluginLabel()]);
866

867
    // Update '#multiple__header_label'.
868
    $form['element']['multiple__header_container']['multiple__header_label']['#states']['visible'][':input[name="properties[multiple__header]"]'] = ['checked' => FALSE];
869
870
871
872

    $form['composite'] = [
      '#type' => 'fieldset',
      '#title' => $this->t('@title settings', ['@title' => $this->getPluginLabel()]),
873
      '#attributes' => ['class' => ['webform-admin-composite-elements']],
874
    ];
875
    $form['composite']['element'] = $this->buildCompositeElementsTable($form, $form_state);
876
877
878
    $form['composite']['flexbox'] = [
      '#type' => 'select',
      '#title' => $this->t('Use Flexbox'),
879
      '#description' => $this->t("If 'Automatic' is selected Flexbox layout will only be used if a 'Flexbox layout' element is included in the webform."),
880
881
882
883
884
885
      '#options' => [
        '' => $this->t('Automatic'),
        0 => $this->t('No'),
        1 => $this->t('Yes'),
      ],
    ];
886

887
    // Hide single item display when multiple item display is set to 'table'.
888
    $form['display']['item']['#states']['invisible'] = [
889
      ':input[name="properties[format_items]"]' => ['value' => 'table'],
890
891
    ];

892
    $form['#attached']['library'][] = 'webform/webform.admin.composite';
893

894
    // Select2, Chosen, and/or Choices enhancements.
895
    // @see \Drupal\webform\Plugin\WebformElement\Select::form
896
897
898
899
    $select2_exists = $this->librariesManager->isIncluded('jquery.select2');
    $choices_exists = $this->librariesManager->isIncluded('choices');
    $chosen_exists = $this->librariesManager->isIncluded('jquery.chosen');

900
901
902
    $form['composite']['select2'] = [
      '#type' => 'checkbox',
      '#title' => $this->t('Select2'),
903
      '#description' => $this->t('Replace select element with jQuery <a href=":href">Select2</a> select box.', [':href' => 'https://select2.github.io/']),
904
905
906
907
908
909
910
      '#return_value' => TRUE,
      '#states' => [
        'disabled' => [
          ':input[name="properties[chosen]"]' => ['checked' => TRUE],
        ],
      ],
    ];
911
    if (!$select2_exists) {
912
913
      $form['composite']['select2']['#access'] = FALSE;
    }
914
915
916
    $form['composite']['choices'] = [
      '#type' => 'checkbox',
      '#title' => $this->t('Choices'),
917
      '#description' => $this->t('Replace select element with <a href=":href">Choice.js</a> select box.', [':href' => 'https://choices-js.github.io/Choices/']),
918
919
920
921
922
923
924
925
926
927
      '#return_value' => TRUE,
      '#states' => [
        'disabled' => [
          ':input[name="properties[select2]"]' => ['checked' => TRUE],
        ],
      ],
    ];
    if (!$choices_exists) {
      $form['composite']['choices']['#access'] = FALSE;
    }
928
929
930
    $form['composite']['chosen'] = [
      '#type' => 'checkbox',
      '#title' => $this->t('Chosen'),
931
      '#description' => $this->t('Replace select element with jQuery <a href=":href">Chosen</a> select box.', [':href' => 'https://harvesthq.github.io/chosen/']),
932
933
934
935
936
937
938
      '#return_value' => TRUE,
      '#states' => [
        'disabled' => [
          ':input[name="properties[select2]"]' => ['checked' => TRUE],
        ],
      ],
    ];
939
    if (!$chosen_exists) {
940
941
      $form['composite']['chosen']['#access'] = FALSE;
    }
942
943
944
945
946
947
948
949
950
951
952
953
954
955
    if (($select2_exists + $chosen_exists + $choices_exists) > 1) {
      $select_libraries = [];
      if ($select2_exists) {
        $select_libraries[] = $this->t('Select2');
      }
      if ($choices_exists) {
        $select_libraries[] = $this->t('Choices');
      }
      if ($chosen_exists) {
        $select_libraries[] = $this->t('Chosen');
      }
      $t_args = [
        '@libraries' => WebformArrayHelper::toString($select_libraries),
      ];
956
957
958
      $form['composite']['select_message'] = [
        '#type' => 'webform_message',
        '#message_type' => 'warning',
959
        '#message_message' => $this->t('@libraries provide very similar functionality, only one should be enabled.', $t_args),
960
961
962
963
        '#access' => TRUE,
      ];
    }

964
965
966
967
968
969
    return $form;
  }

  /**
   * Build the composite elements settings table.
   *
970
971
972
973
974
   * @param array $form
   *   An associative array containing the structure of the form.
   * @param \Drupal\Core\Form\FormStateInterface $form_state
   *   The current state of the form.
   *
975
976
977
   * @return array
   *   A renderable array container the composite elements settings table.
   */
978
  protected function buildCompositeElementsTable(array $form, FormStateInterface $form_state) {
979
980
981
    $labels_help = [
      'help' => [
        '#type' => 'webform_help',
982
983
984
        '#help' => '<b>' . $this->t('Key') . ':</b> ' . $this->t('The machine-readable name.') .
          '<hr/><b>' . $this->t('Title') . ':</b> ' . $this->t('This is used as a descriptive label when displaying this webform element.') .
          '<hr/><b>' . $this->t('Placeholder') . ':</b> ' . $this->t('The placeholder will be shown in the element until the user starts entering a value.') .
985
          '<hr/><b>' . $this->t('Description') . ':</b> ' . $this->t('A short description of the element used as help for the user when they use the webform.') .
986
987
          '<hr/><b>' . $this->t('Help text') . ':</b> ' . $this->t('A tooltip displayed after the title.') .
          '<hr/><b>' . $this->t('Title display') . ':</b> ' . $this->t('A tooltip displayed after the title.'),
988
989
990
991
992
993
        '#help_title' => $this->t('Labels'),
      ],
    ];
    $settings_help = [
      'help' => [
        '#type' => 'webform_help',
994
995
996
        '#help' => '<b>' . $this->t('Required') . ':</b> ' . $this->t('Check this option if the user must enter a value.') .
          '<hr/><b>' . $this->t('Type') . ':</b> ' . $this->t('The type of element to be displayed.') .
          '<hr/><b>' . $this->t('Options') . ':</b> ' . $this->t('Please select predefined options.'),
997
998
999
1000
        '#help_title' => $this->t('Settings'),
      ],
    ];

For faster browsing, not all history is shown. View entire blame