View source
<?php
namespace Drupal\webform\Element;
use Drupal\Component\Utility\NestedArray;
use Drupal\Core\Render\Element;
use Drupal\Core\Render\Element\FormElement;
use Drupal\Core\Form\FormStateInterface;
use Drupal\Core\Template\Attribute;
use Drupal\webform\Utility\WebformAccessibilityHelper;
use Drupal\webform\Utility\WebformElementHelper;
class WebformMultiple extends FormElement {
const CARDINALITY_UNLIMITED = -1;
public function getInfo() {
$class = get_class($this);
return [
'#input' => TRUE,
'#access' => TRUE,
'#key' => NULL,
'#header' => NULL,
'#element' => [
'#type' => 'textfield',
'#title' => t('Item value'),
'#title_display' => 'invisible',
'#placeholder' => t('Enter value…'),
],
'#cardinality' => FALSE,
'#min_items' => NULL,
'#item_label' => $this
->t('item'),
'#no_items_message' => $this
->t('No items entered. Please add items below.'),
'#empty_items' => 1,
'#add_more' => TRUE,
'#add_more_items' => 1,
'#add_more_button_label' => $this
->t('Add'),
'#add_more_input' => TRUE,
'#add_more_input_label' => $this
->t('more items'),
'#sorting' => TRUE,
'#operations' => TRUE,
'#add' => TRUE,
'#ajax_attributes' => [],
'#table_attributes' => [],
'#table_wrapper_attributes' => [],
'#remove' => TRUE,
'#process' => [
[
$class,
'processWebformMultiple',
],
],
'#theme_wrappers' => [
'form_element',
],
'#markup' => '',
];
}
public static function valueCallback(&$element, $input, FormStateInterface $form_state) {
if ($input === FALSE) {
if (!isset($element['#default_value'])) {
return [];
}
elseif (!is_array($element['#default_value'])) {
return [
$element['#default_value'],
];
}
else {
return $element['#default_value'];
}
}
elseif (is_array($input) && isset($input['items'])) {
return static::convertValuesToItems($element, $input['items']);
}
else {
return [];
}
}
public static function processWebformMultiple(&$element, FormStateInterface $form_state, &$complete_form) {
$element['#tree'] = TRUE;
$element['#label_attributes']['webform-remove-for-attribute'] = TRUE;
if (!isset($element['#min_items']) || $element['#min_items'] === '') {
$element['#min_items'] = empty($element['#required']) ? 0 : 1;
}
if (!empty($element['#cardinality']) && $element['#min_items'] > $element['#cardinality']) {
$element['#min_items'] = $element['#cardinality'];
}
if (!empty($element['#cardinality']) && $element['#empty_items'] > $element['#cardinality']) {
$element['#empty_items'] = $element['#cardinality'];
}
if (isset($element['#default_value']) && is_array($element['#default_value']) && count($element['#default_value']) >= $element['#min_items'] && static::hasRequireElement($element['#element'])) {
$element['#empty_items'] = 0;
}
$element += [
'#element_validate' => [],
];
array_unshift($element['#element_validate'], [
get_called_class(),
'validateWebformMultiple',
]);
WebformElementHelper::fixStatesWrapper($element);
$number_of_items_storage_key = static::getStorageKey($element, 'number_of_items');
if ($form_state
->get($number_of_items_storage_key) === NULL) {
if (empty($element['#default_value']) || !is_array($element['#default_value'])) {
$number_of_default_values = 0;
}
else {
$number_of_default_values = count($element['#default_value']);
}
$number_of_empty_items = (int) $element['#empty_items'];
$number_of_items = $number_of_default_values + $number_of_empty_items;
$min_items = (int) $element['#min_items'];
$number_of_items = $number_of_items < $min_items ? $min_items : $number_of_items;
if (!empty($element['#cardinality']) && $number_of_items > $element['#cardinality']) {
$number_of_items = $element['#cardinality'];
}
$form_state
->set($number_of_items_storage_key, $number_of_items);
}
$number_of_items = $form_state
->get($number_of_items_storage_key);
$table_id = implode('_', $element['#parents']) . '_table';
if (!empty($element['#cardinality']) && $number_of_items >= $element['#cardinality']) {
$element['#add'] = FALSE;
$number_of_items = $element['#cardinality'];
$form_state
->set($number_of_items_storage_key, $number_of_items);
}
$ajax_attributes = $element['#ajax_attributes'];
$ajax_attributes['id'] = $table_id;
$element += [
'#prefix' => '',
'#suffix' => '',
];
$element['#ajax_prefix'] = '<div' . new Attribute($ajax_attributes) . '>';
$element['#ajax_suffix'] = '</div>';
$element['#prefix'] = $element['#prefix'] . $element['#ajax_prefix'];
$element['#suffix'] = $element['#ajax_suffix'] . $element['#suffix'];
$ajax_settings = [
'callback' => [
get_called_class(),
'ajaxCallback',
],
'wrapper' => $table_id,
'progress' => [
'type' => 'none',
],
];
static::initializeElement($element, $form_state, $complete_form);
$header = static::buildElementHeader($element);
$row_index = 0;
$weight = 0;
$rows = [];
if (!$form_state
->isProcessingInput() && isset($element['#default_value']) && is_array($element['#default_value'])) {
$default_values = $element['#default_value'];
}
elseif ($form_state
->isProcessingInput() && isset($element['#value']) && is_array($element['#value'])) {
$default_values = $element['#value'];
}
else {
$default_values = [];
}
$action_key = static::getStorageKey($element, 'action');
if ($form_state
->get($action_key)) {
$form_state
->set($action_key, FALSE);
$default_values = [];
}
foreach ($default_values as $key => $default_value) {
if (!empty($element['#key']) && !isset($default_value[$element['#key']])) {
$default_value[$element['#key']] = $key;
}
$rows[$row_index] = static::buildElementRow($table_id, $row_index, $element, $default_value, $weight++, $ajax_settings);
$row_index++;
}
while ($row_index < $number_of_items) {
$rows[$row_index] = static::buildElementRow($table_id, $row_index, $element, NULL, $weight++, $ajax_settings);
$row_index++;
}
$table_wrapper_attributes = $element['#table_wrapper_attributes'];
$table_wrapper_attributes['class'][] = 'webform-multiple-table';
if (count($element['#element']) > 1) {
$table_wrapper_attributes['class'][] = 'webform-multiple-table-responsive';
}
$element['items'] = [
'#prefix' => '<div' . new Attribute($table_wrapper_attributes) . '>',
'#suffix' => '</div>',
] + $rows;
if ($rows) {
$element['items'] += [
'#type' => 'table',
'#header' => $header,
'#attributes' => $element['#table_attributes'],
] + $rows;
if ($element['#sorting']) {
$element['items']['#tabledrag'] = [
[
'action' => 'order',
'relationship' => 'sibling',
'group' => 'webform-multiple-sort-weight',
],
];
}
}
elseif (!empty($element['#no_items_message'])) {
$element['items'] += [
'#type' => 'webform_message',
'#message_message' => $element['#no_items_message'],
'#message_type' => 'info',
'#attributes' => [
'class' => [
'webform-multiple-table--no-items-message',
],
],
];
}
if ($element['#add_more'] && (empty($element['#cardinality']) || $number_of_items < $element['#cardinality'])) {
$element['add'] = [
'#prefix' => '<div class="webform-multiple-add js-webform-multiple-add container-inline">',
'#suffix' => '</div>',
];
$element['add']['submit'] = [
'#type' => 'submit',
'#value' => $element['#add_more_button_label'],
'#limit_validation_errors' => [],
'#submit' => [
[
get_called_class(),
'addItemsSubmit',
],
],
'#ajax' => $ajax_settings,
'#name' => $table_id . '_add',
];
$max = $element['#cardinality'] ? $element['#cardinality'] - $number_of_items : 100;
$element['add']['more_items'] = [
'#type' => 'number',
'#title' => $element['#add_more_button_label'] . ' ' . $element['#add_more_input_label'],
'#title_display' => 'invisible',
'#min' => 1,
'#max' => $max,
'#default_value' => $element['#add_more_items'],
'#field_suffix' => $element['#add_more_input_label'],
'#error_no_message' => TRUE,
'#access' => $element['#add_more_input'],
];
}
$element['#attached']['library'][] = 'webform/webform.element.multiple';
return $element;
}
protected static function initializeElement(array &$element, FormStateInterface $form_state, array &$complete_form) {
$element['#child_keys'] = Element::children($element['#element']);
if (!$element['#child_keys']) {
if (isset($element['#_webform_states'])) {
$element['#element'] += [
'#states' => [],
];
$element['#element']['#states'] = array_intersect_key(WebformElementHelper::getStates($element), [
'required' => 'required',
'optional' => 'optional',
]);
}
}
else {
$required_states = WebformElementHelper::getRequiredFromVisibleStates($element);
static::initializeElementRecursive($element, $form_state, $complete_form, $element['#element'], $required_states);
}
}
protected static function initializeElementRecursive(array $element, FormStateInterface $form_state, array &$complete_form, array &$sub_elements, array $required_states) {
$child_keys = Element::children($sub_elements);
if (!$child_keys) {
return;
}
$is_root = $element['#element'] === $sub_elements ? TRUE : FALSE;
$element_manager = \Drupal::service('plugin.manager.webform.element');
foreach ($child_keys as $child_key) {
$sub_element =& $sub_elements[$child_key];
$element_plugin = $element_manager
->getElementInstance($sub_element);
if (isset($element['#access']) && $element['#access'] === FALSE) {
$sub_element['#access'] = FALSE;
}
if ($element['#header'] && ($is_root && $element_plugin
->isInput($sub_element)) && !isset($sub_element['#title_display'])) {
$sub_element['#title_display'] = 'invisible';
}
$element_manager
->initializeElement($sub_element);
$element_manager
->buildElement($sub_element, $complete_form, $form_state);
if ($required_states && !empty($sub_element['#required'])) {
unset($sub_element['#required']);
$sub_element['#_required'] = TRUE;
if (!isset($sub_element['#states'])) {
$sub_element['#states'] = [];
}
$sub_element['#states'] += $required_states;
}
if (is_array($sub_element)) {
static::initializeElementRecursive($element, $form_state, $complete_form, $sub_element, $required_states);
}
}
}
protected static function buildElementHeader(array $element) {
$table_id = implode('-', $element['#parents']) . '-table';
$colspan = 0;
if ($element['#sorting']) {
$colspan += 3;
}
if ($element['#operations']) {
$colspan += 1;
}
if (empty($element['#header'])) {
return [
[
'data' => !empty($element['#title']) ? WebformAccessibilityHelper::buildVisuallyHidden($element['#title']) : [],
'colspan' => $colspan + 1,
],
];
}
elseif (is_array($element['#header'])) {
$header = [];
if ($element['#sorting']) {
$header[] = [
'data' => WebformAccessibilityHelper::buildVisuallyHidden(t('Re-order')),
'class' => [
"{$table_id}--handle",
'webform-multiple-table--handle',
],
];
}
$header = array_merge($header, $element['#header']);
if ($element['#sorting']) {
$header[] = [
'data' => [
'#markup' => t('Weight'),
],
'class' => [
"{$table_id}--weight",
'webform-multiple-table--weight',
],
];
}
if ($element['#operations']) {
$header[] = [
'data' => WebformAccessibilityHelper::buildVisuallyHidden(t('Operations')),
'class' => [
"{$table_id}--handle",
'webform-multiple-table--operations',
],
];
}
return $header;
}
elseif (is_string($element['#header'])) {
return [
[
'data' => $element['#header'],
'colspan' => $element['#child_keys'] ? count($element['#child_keys']) + $colspan : $colspan + 1,
],
];
}
else {
$header = [];
if ($element['#sorting']) {
$header['_handle_'] = [
'data' => WebformAccessibilityHelper::buildVisuallyHidden(t('Re-order')),
'class' => [
"{$table_id}--handle",
"webform-multiple-table--handle",
],
];
}
if ($element['#child_keys']) {
foreach ($element['#child_keys'] as $child_key) {
if (static::isHidden($element['#element'][$child_key])) {
continue;
}
$child_element = $element['#element'][$child_key];
$header[$child_key] = [
'data' => static::buildElementTitle($child_element),
];
if (!empty($child_element['#label_attributes'])) {
$header[$child_key] += $child_element['#label_attributes'];
}
$header[$child_key]['class'][] = "{$table_id}--{$child_key}";
$header[$child_key]['class'][] = "webform-multiple-table--{$child_key}";
}
}
else {
$header['item'] = [
'data' => isset($element['#element']['#title']) ? $element['#element']['#title'] : '',
'class' => [
"{$table_id}--item",
"webform-multiple-table--item",
],
];
}
if ($element['#sorting']) {
$header['weight'] = [
'data' => t('Weight'),
'class' => [
"{$table_id}--weight",
"webform-multiple-table--weight",
],
];
}
if ($element['#operations']) {
$header['_operations_'] = [
'data' => WebformAccessibilityHelper::buildVisuallyHidden(t('Operations')),
'class' => [
"{$table_id}--operations",
"webform-multiple-table--operations",
],
];
}
return $header;
}
}
protected static function buildElementTitle(array $element) {
$title = !empty($element['#title']) ? $element['#title'] : '';
$build = [];
$build['title'] = [
'#markup' => $title,
];
if (!empty($element['#required']) || !empty($element['#_required'])) {
$build['title'] += [
'#prefix' => '<span class="form-required">',
'#suffix' => '</span>',
];
}
if (!empty($element['#help'])) {
$build['help'] = [
'#type' => 'webform_help',
'#help' => $element['#help'],
'#help_title' => $title,
];
}
return $build;
}
protected static function buildElementRow($table_id, $row_index, array $element, $default_value, $weight, array $ajax_settings) {
if ($element['#child_keys']) {
static::setElementRowDefaultValueRecursive($element['#element'], (array) $default_value);
}
else {
static::setElementDefaultValue($element['#element'], $default_value);
}
$hidden_elements = [];
$row = [];
if ($element['#sorting']) {
$row['_handle_'] = [
'#wrapper_attributes' => [
'class' => [
'webform-multiple-table--handle',
],
],
];
}
if ($element['#child_keys'] && !empty($element['#header'])) {
$parents = array_merge($element['#parents'], [
'items',
$row_index,
]);
$hidden_parents = array_merge($element['#parents'], [
'items',
$row_index,
'_hidden_',
]);
foreach ($element['#child_keys'] as $child_key) {
if (static::isHidden($element['#element'][$child_key])) {
$hidden_elements[$child_key] = $element['#element'][$child_key];
if (Element::isVisibleElement($element)) {
$hidden_elements[$child_key]['#type'] = 'hidden';
unset($hidden_elements[$child_key]['#access'], $hidden_elements[$child_key]['#element_validate'], $hidden_elements[$child_key]['#pre_render'], $hidden_elements[$child_key]['#options']);
}
static::setElementRowParentsRecursive($hidden_elements[$child_key], $child_key, $hidden_parents);
}
else {
$row[$child_key] = $element['#element'][$child_key];
static::setElementRowParentsRecursive($row[$child_key], $child_key, $parents);
}
}
}
else {
$row['_item_'] = $element['#element'];
}
if ($element['#sorting']) {
$row['weight'] = [
'#type' => 'weight',
'#delta' => 1000,
'#title' => t('Item weight'),
'#title_display' => 'invisible',
'#attributes' => [
'class' => [
'webform-multiple-sort-weight',
],
],
'#wrapper_attributes' => [
'class' => [
'webform-multiple-table--weight',
],
],
'#default_value' => $weight,
];
}
if ($element['#operations']) {
$row['_operations_'] = [
'#wrapper_attributes' => [
'class' => [
'webform-multiple-table--operations',
],
],
];
if ($element['#add'] && $element['#remove']) {
$row['_operations_']['#wrapper_attributes']['class'][] = 'webform-multiple-table--operations-two';
}
if ($element['#add']) {
$row['_operations_']['add'] = [
'#type' => 'image_button',
'#title' => t('Add new @item after @item @number', [
'@number' => $row_index + 1,
'@item' => $element['#item_label'],
]),
'#src' => drupal_get_path('module', 'webform') . '/images/icons/plus.svg',
'#limit_validation_errors' => [],
'#submit' => [
[
get_called_class(),
'addItemSubmit',
],
],
'#ajax' => $ajax_settings,
'#row_index' => $row_index,
'#name' => $table_id . '_add_' . $row_index,
];
}
if ($element['#remove']) {
$row['_operations_']['remove'] = [
'#type' => 'image_button',
'#title' => t('Remove @item @number', [
'@number' => $row_index + 1,
'@item' => $element['#item_label'],
]),
'#src' => drupal_get_path('module', 'webform') . '/images/icons/minus.svg',
'#limit_validation_errors' => [],
'#submit' => [
[
get_called_class(),
'removeItemSubmit',
],
],
'#ajax' => $ajax_settings,
'#row_index' => $row_index,
'#name' => $table_id . '_remove_' . $row_index,
];
}
}
if ($hidden_elements) {
$row['_hidden_'] = $hidden_elements + [
'#wrapper_attributes' => [
'style' => 'display: none',
],
];
}
if ($element['#sorting']) {
$row['#attributes']['class'][] = 'draggable';
$row['#weight'] = $weight;
}
return $row;
}
protected static function isHidden(array $element) {
return !Element::isVisibleElement($element);
}
protected static function setElementRowDefaultValueRecursive(array &$element, array $default_value) {
foreach (Element::children($element) as $child_key) {
if (isset($default_value[$child_key])) {
static::setElementDefaultValue($element[$child_key], $default_value[$child_key]);
}
static::setElementRowDefaultValueRecursive($element[$child_key], $default_value);
}
}
protected static function setElementDefaultValue(array &$element, $default_value) {
if ($element['#type'] === 'value') {
$element['#value'] = $default_value;
}
else {
$element['#default_value'] = $default_value;
$element_manager = \Drupal::service('plugin.manager.webform.element');
$element_plugin = $element_manager
->getElementInstance($element);
$element_plugin
->setDefaultValue($element);
}
}
protected static function setElementRowParentsRecursive(array &$element, $element_key, array $parents) {
$element['#parents'] = array_merge($parents, [
$element_key,
]);
foreach (Element::children($element) as $child_key) {
static::setElementRowParentsRecursive($element[$child_key], $child_key, $parents);
}
}
public static function addItemsSubmit(array &$form, FormStateInterface $form_state) {
$button = $form_state
->getTriggeringElement();
$element =& NestedArray::getValue($form, array_slice($button['#array_parents'], 0, -2));
$number_of_items_storage_key = static::getStorageKey($element, 'number_of_items');
$number_of_items = $form_state
->get($number_of_items_storage_key);
$more_items = (int) $element['add']['more_items']['#value'];
$form_state
->set($number_of_items_storage_key, $number_of_items + $more_items);
$items = !empty($element['items']['#value']) ? array_values($element['items']['#value']) : [];
$element['items']['#value'] = $items;
$form_state
->setValueForElement($element['items'], $items);
NestedArray::setValue($form_state
->getUserInput(), $element['items']['#parents'], $items);
$action_key = static::getStorageKey($element, 'action');
$form_state
->set($action_key, TRUE);
$form_state
->setRebuild();
}
public static function addItemSubmit(array &$form, FormStateInterface $form_state) {
$button = $form_state
->getTriggeringElement();
$element = NestedArray::getValue($form, array_slice($button['#array_parents'], 0, -4));
$values = [];
foreach ($element['items']['#value'] as $row_index => $value) {
$values[] = $value;
if ($row_index === $button['#row_index']) {
$values[] = [];
}
}
$number_of_items_storage_key = static::getStorageKey($element, 'number_of_items');
$number_of_items = $form_state
->get($number_of_items_storage_key);
$form_state
->set($number_of_items_storage_key, $number_of_items + 1);
$form_state
->setValueForElement($element['items'], $values);
NestedArray::setValue($form_state
->getUserInput(), $element['items']['#parents'], $values);
$action_key = static::getStorageKey($element, 'action');
$form_state
->set($action_key, TRUE);
$form_state
->setRebuild();
}
public static function removeItemSubmit(array &$form, FormStateInterface $form_state) {
$button = $form_state
->getTriggeringElement();
$element = NestedArray::getValue($form, array_slice($button['#array_parents'], 0, -4));
$values = $element['items']['#value'];
unset($values[$button['#row_index']]);
$values = array_values($values);
$number_of_items_storage_key = static::getStorageKey($element, 'number_of_items');
$number_of_items = $form_state
->get($number_of_items_storage_key);
if ($number_of_items > $element['#min_items']) {
$form_state
->set($number_of_items_storage_key, $number_of_items - 1);
}
$form_state
->setValueForElement($element['items'], $values);
NestedArray::setValue($form_state
->getUserInput(), $element['items']['#parents'], $values);
$action_key = static::getStorageKey($element, 'action');
$form_state
->set($action_key, TRUE);
$form_state
->setRebuild();
}
public static function ajaxCallback(array &$form, FormStateInterface $form_state) {
$button = $form_state
->getTriggeringElement();
$parent_length = isset($button['#row_index']) ? -4 : -2;
$element = NestedArray::getValue($form, array_slice($button['#array_parents'], 0, $parent_length));
$element['#prefix'] = $element['#ajax_prefix'];
$element['#suffix'] = $element['#ajax_suffix'];
$element['#webform_wrapper'] = FALSE;
return $element;
}
public static function validateWebformMultiple(&$element, FormStateInterface $form_state, &$complete_form) {
$values = NestedArray::getValue($form_state
->getValues(), $element['#parents']);
$number_of_items_storage_key = static::getStorageKey($element, 'number_of_items');
$number_of_items = $form_state
->get($number_of_items_storage_key);
if (!empty($values['items']) && ($number_of_items || $element['#cardinality'])) {
$items = $values['items'];
if ($error_message = static::validateUniqueKeys($element, $items)) {
$form_state
->setError($element, $error_message);
return;
}
$items = static::convertValuesToItems($element, $items);
if (!empty($element['#required']) && empty($items)) {
WebformElementHelper::setRequiredError($element, $form_state);
}
}
else {
$items = [];
}
$element['#value'] = $items;
$form_state
->setValueForElement($element, $items);
}
public static function getStorageKey(array $element, $name) {
return 'webform_multiple__' . $element['#name'] . '__' . $name;
}
public static function convertValuesToItems(array $element, array $values = []) {
if ($element['#sorting']) {
uasort($values, [
'Drupal\\Component\\Utility\\SortArray',
'sortByWeightElement',
]);
}
$items = [];
foreach ($values as $value) {
$item = static::convertValueToItem($value);
if (static::isEmpty($item)) {
continue;
}
if (!empty($element['#key']) && isset($item[$element['#key']])) {
$key_name = $element['#key'];
$key_value = $item[$key_name];
unset($item[$key_name]);
$items[$key_value] = $item;
}
else {
$items[] = $item;
}
}
return $items;
}
public static function convertValueToItem(array $value) {
if (isset($value['_item_'])) {
return $value['_item_'];
}
else {
if (isset($value['_hidden_']) && is_array($value['_hidden_'])) {
$value += $value['_hidden_'];
}
unset($value['weight'], $value['_operations_'], $value['_hidden_']);
return $value;
}
}
protected static function validateUniqueKeys(array $element, array $values) {
if (!isset($element['#key'])) {
return NULL;
}
$unique_keys = [];
foreach ($values as $value) {
$item = static::convertValueToItem($value);
$key_name = $element['#key'];
$key_value = $item[$key_name];
unset($item[$key_name]);
if (empty($key_value) && static::isEmpty($item)) {
continue;
}
if (isset($unique_keys[$key_value])) {
$elements = WebformElementHelper::getFlattened($element['#element']);
$key_title = isset($elements[$key_name]['#title']) ? $elements[$key_name]['#title'] : $key_name;
$t_args = [
'@key' => $key_value,
'%title' => $key_title,
];
return t("The %title '@key' is already in use. It must be unique.", $t_args);
}
$unique_keys[$key_value] = $key_value;
}
return NULL;
}
public static function isEmpty($value = NULL) {
if (is_null($value)) {
return TRUE;
}
elseif (is_string($value)) {
return $value === '' ? TRUE : FALSE;
}
elseif (is_array($value)) {
return !array_filter($value, function ($item) {
return !static::isEmpty($item);
});
}
else {
return FALSE;
}
}
protected static function hasRequireElement(array $element) {
$required_properties = [
'#required' => TRUE,
'#_required' => TRUE,
];
return WebformElementHelper::hasProperties($element, $required_properties);
}
}