breezy_utility-1.0.x-dev/src/Element/BreezyUtilityMultiple.php
src/Element/BreezyUtilityMultiple.php
<?php
namespace Drupal\breezy_utility\Element;
use Drupal\breezy_utility\Utility\BreezyUtilityAccessibilityHelper;
use Drupal\breezy_utility\Utility\BreezyUtilityElementHelper;
use Drupal\Component\Utility\NestedArray;
use Drupal\Core\Form\FormStateInterface;
use Drupal\Core\Render\Element;
use Drupal\Core\Render\Element\FormElement;
use Drupal\Core\Template\Attribute;
/**
* Provides an element to assist in creating multiple elements.
*
* @FormElement("breezy_utility_multiple")
*/
class BreezyUtilityMultiple extends FormElement {
/**
* Value indicating a element accepts an unlimited number of values.
*/
const CARDINALITY_UNLIMITED = -1;
/**
* {@inheritdoc}
*/
public function getInfo() {
$class = get_class($this);
return [
'#input' => TRUE,
'#access' => TRUE,
'#key' => NULL,
'#header' => NULL,
'#header_label' => '',
'#element' => [
'#type' => 'breezy_utility_class_select',
'#title' => $this->t('Value'),
'#title_display' => 'invisible',
'#placeholder' => $this->t('Enter value…'),
],
'#cardinality' => FALSE,
'#min_items' => NULL,
'#item_label' => $this->t('option'),
'#no_items_message' => $this->t('No options entered. Please add options below.'),
'#empty_items' => 1,
'#add_more' => TRUE,
'#add_more_items' => 1,
'#add_more_button_label' => $this->t('Add more options'),
'#add_more_input' => TRUE,
'#sorting' => TRUE,
'#operations' => TRUE,
'#add' => TRUE,
'#ajax_attributes' => [],
'#table_attributes' => [],
'#table_wrapper_attributes' => [],
'#remove' => TRUE,
'#process' => [
[$class, 'processBreezyUtilityMultiple'],
],
'#theme_wrappers' => ['form_element'],
// Add '#markup' property to add an 'id' attribute to the form element.
// @see template_preprocess_form_element()
'#markup' => '',
];
}
/**
* {@inheritdoc}
*/
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 [];
}
}
/**
* Process items and build multiple elements widget.
*/
public static function processBreezyUtilityMultiple(&$element, FormStateInterface $form_state, &$complete_form) {
$element['#tree'] = TRUE;
// Set min items based on when the element is required.
if (!isset($element['#min_items']) || $element['#min_items'] === '') {
$element['#min_items'] = (empty($element['#required'])) ? 0 : 1;
}
// Make sure min items does not exceed cardinality.
if (!empty($element['#cardinality']) && $element['#min_items'] > $element['#cardinality']) {
$element['#min_items'] = $element['#cardinality'];
}
// Make sure empty items does not exceed cardinality.
if (!empty($element['#cardinality']) && $element['#empty_items'] > $element['#cardinality']) {
$element['#empty_items'] = $element['#cardinality'];
}
// If the number of default values exceeds the min items and has required
// sub-elements, set empty items to 0.
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;
}
// Add validate callback that extracts the array of items.
$element += ['#element_validate' => []];
array_unshift($element['#element_validate'], [
get_called_class(),
'validateBreezyUtilityMultiple',
]);
// Get unique key used to store the current number of items.
$number_of_items_storage_key = static::getStorageKey($element, 'number_of_items');
// Store the number of items which is the number of
// #default_values + number of empty_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;
// Make sure number of items is greater than min items.
$min_items = (int) $element['#min_items'];
$number_of_items = ($number_of_items < $min_items) ? $min_items : $number_of_items;
// Make sure number of (default) items does not exceed cardinality.
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';
// Disable add operation when #cardinality is met
// and make sure to limit the number of items.
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);
}
// Add wrapper to the element.
$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'];
// DEBUG:
// Disable Ajax callback by commenting out the below callback and wrapper.
$ajax_settings = [
'callback' => [get_called_class(), 'ajaxCallback'],
'wrapper' => $table_id,
'progress' => ['type' => 'none'],
];
// Initialize, prepare, and finalize sub-elements.
static::initializeElement($element, $form_state, $complete_form);
// Build (single) element header.
$header = static::buildElementHeader($element);
// Build (single) element rows.
$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 = [];
}
// When adding/removing elements we don't need to set any 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 #key is defined make sure to set default value's key item.
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++;
}
// Build table.
$table_wrapper_attributes = $element['#table_wrapper_attributes'];
$table_wrapper_attributes['class'][] = 'breezy-utility-multiple-table';
if (count($element['#element']) > 1) {
$table_wrapper_attributes['class'][] = 'breezy-utility-multiple-table-responsive';
}
$table_wrapper_attributes['class'][] = '$element-key-' . $element['#key'];
$table_wrapper_attributes['class'][] = '$element-parents-' . implode('--', $element['#parents']);
$element['items'] = [
'#prefix' => '<div' . new Attribute($table_wrapper_attributes) . '>',
'#suffix' => '</div>',
] + $rows;
// Display table if there are any rows.
if ($rows) {
$element['items'] += [
'#type' => 'table',
'#header' => $header,
'#attributes' => $element['#table_attributes'],
] + $rows;
// Add sorting to table.
if ($element['#sorting']) {
$element['items']['#tabledrag'] = [
[
'action' => 'order',
'relationship' => 'sibling',
'group' => 'breezy-utility-multiple-sort-weight',
],
];
}
}
elseif (!empty($element['#no_items_message'])) {
// @todo Add message element.
}
// Build add more actions.
if ($element['#add_more'] && (empty($element['#cardinality']) || ($number_of_items < $element['#cardinality']))) {
$element['add'] = [
'#prefix' => '<div class="breezy-utility-multiple-add js-breezy-utility-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;
}
return $element;
}
/**
* Initialize element.
*
* @param array $element
* The element.
* @param \Drupal\Core\Form\FormStateInterface $form_state
* The current state of the form.
* @param array $complete_form
* An associative array containing the structure of the form.
*/
protected static function initializeElement(array &$element, FormStateInterface $form_state, array &$complete_form) {
// Track element child keys.
$element['#child_keys'] = Element::children($element['#element']);
if (!$element['#child_keys']) {
// Apply multiple element's required/optional #states to the
// individual element.
if (isset($element['#_breezy_utility_states'])) {
$element['#element'] += ['#states' => []];
$element['#element']['#states'] = array_intersect_key(
BreezyUtilityElementHelper::getStates($element),
['required' => 'required', 'optional' => 'optional']
);
}
}
else {
// Initialize, prepare, and finalize composite sub-elements.
// Get composite element required/options states from visible/hidden
// states.
$required_states = BreezyUtilityElementHelper::getRequiredFromVisibleStates($element);
static::initializeElementRecursive($element, $form_state, $complete_form, $element['#element'], $required_states);
}
}
/**
* Initialize, prepare, and finalize composite sub-elements recursively.
*
* @param array $element
* The main element.
* @param \Drupal\Core\Form\FormStateInterface $form_state
* The current state of the form.
* @param array $complete_form
* An associative array containing the structure of the form.
* @param array $sub_elements
* The sub element.
* @param array $required_states
* An associative array of required states from the main element's
* visible/hidden 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);
// Exit immediate if the sub elements has no children.
if (!$child_keys) {
return;
}
// Determine if the sub elements are the main element for each table cell.
$is_root = ($element['#element'] === $sub_elements) ? TRUE : FALSE;
/** @var \Drupal\breezy_utility\Service\BreezyUtilityElementPluginManagerInterface $element_manager */
$element_manager = \Drupal::service('plugin.manager.breezy_utility.element');
foreach ($child_keys as $child_key) {
$sub_element = &$sub_elements[$child_key];
$element_plugin = $element_manager->getElementInstance($sub_element);
// If the element's #access is FALSE, apply it to all sub elements.
if (isset($element['#access']) && $element['#access'] === FALSE) {
$sub_element['#access'] = FALSE;
}
// If #header and root input then hide the sub element's #title.
if ($element['#header']
&& ($is_root && $element_plugin->isInput($sub_element))
&& !isset($sub_element['#title_display'])) {
$sub_element['#title_display'] = 'invisible';
}
// Initialize the composite sub-element.
$element_manager->initializeElement($sub_element);
// Build the composite sub-element.
$element_manager->buildElement($sub_element, $complete_form, $form_state);
// Custom validate required sub-element because they can be hidden
// via #access or #states.
// @see \Drupal\webform\Element\WebformCompositeBase::validateWebformComposite
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);
}
}
}
/**
* Build a single element header.
*
* @param array $element
* The element.
*
* @return array
* A render array containing inputs for an element's header.
*/
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'])) {
if (!empty($element['#header_label'])) {
$header_label = $element['#header_label'];
}
elseif (!empty($element['#title'])) {
$header_label = BreezyUtilityAccessibilityHelper::buildVisuallyHidden($element['#title']);
}
else {
$header_label = [];
}
return [
[
'data' => $header_label,
'colspan' => ($colspan + 1),
],
];
}
elseif (is_array($element['#header'])) {
$header = [];
if ($element['#sorting']) {
$header[] = [
'data' => BreezyUtilityAccessibilityHelper::buildVisuallyHidden(t('Re-order')),
'class' => [
"$table_id--handle",
'breezy-utility-multiple-table--handle',
],
];
}
$header = array_merge($header, $element['#header']);
if ($element['#sorting']) {
$header[] = [
'data' => ['#markup' => t('Weight')],
'class' => [
"$table_id--weight",
'breezy-utility-multiple-table--weight',
],
];
}
if ($element['#operations']) {
$header[] = [
'data' => BreezyUtilityAccessibilityHelper::buildVisuallyHidden(t('Operations')),
'class' => [
"$table_id--handle",
'breezy-utility-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,
],
];
}
elseif (!empty($element['#header_label'])) {
return [
[
'data' => $element['#header_label'],
'colspan' => ($element['#child_keys']) ? count($element['#child_keys']) + $colspan : $colspan + 1,
],
];
}
else {
$header = [];
if ($element['#sorting']) {
$header['_handle_'] = [
'data' => BreezyUtilityAccessibilityHelper::buildVisuallyHidden(t('Re-order')),
'class' => [
"$table_id--handle",
"breezy-utility-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];
// Build element title.
$header[$child_key] = ['data' => static::buildElementTitle($child_element)];
// Append label attributes to header.
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'][] = "breezy-utility-multiple-table--$child_key";
}
}
else {
$header['item'] = [
'data' => $element['#element']['#title'] ?? '',
'class' => ["$table_id--item", "breezy-utility-multiple-table--item"],
];
}
if ($element['#sorting']) {
$header['weight'] = [
'data' => t('Weight'),
'class' => [
"$table_id--weight",
"breezy-utility-multiple-table--weight",
],
];
}
if ($element['#operations']) {
$header['_operations_'] = [
'data' => BreezyUtilityAccessibilityHelper::buildVisuallyHidden(t('Operations')),
'class' => [
"$table_id--operations",
"breezy-utility-multiple-table--operations",
],
];
}
return $header;
}
}
/**
* Build an element's title with help.
*
* @param array $element
* An element.
*
* @return array
* A render array containing an element's title with help.
*/
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'])) {
// @todo Add help element.
}
return $build;
}
/**
* Build a single element row.
*
* @param string $table_id
* The element's table id.
* @param int $row_index
* The row index.
* @param array $element
* The element.
* @param string $default_value
* The default value.
* @param int $weight
* The weight.
* @param array $ajax_settings
* An array containing Ajax callback settings.
*
* @return array
* A render array containing inputs for an element's value and weight.
*/
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' => ['breezy-utility-multiple-table--handle'],
],
];
}
if ($element['#child_keys'] && !empty($element['#header'])) {
// Set #parents which is used for nested elements.
// @see \Drupal\webform\Element\WebformMultiple::setElementRowParentsRecursive
$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) {
// Store hidden element in the '_handle_' column.
// @see \Drupal\webform\Element\WebformMultiple::convertValuesToItems
if (static::isHidden($element['#element'][$child_key])) {
$hidden_elements[$child_key] = $element['#element'][$child_key];
// ISSUE:
// All elements in _handle_ with #access: FALSE are losing
// their values.
//
// Moving these #access: FALSE and value elements outside of the
// table does not work. What is even move baffling is manually adding
// a 'value' element does work.
//
// WORKAROUND:
// Convert element to rendered hidden element.
if (Element::isVisibleElement($element)) {
$hidden_elements[$child_key]['#type'] = 'hidden';
// Unset #access, #element_validate, and #pre_render.
// @see \Drupal\webform\Plugin\WebformElementBase::prepare()
// Unset #options to prevent An illegal choice has been detected.
// @see \Drupal\Core\Form\FormValidator::performRequiredValidation
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' => ['breezy-utility-multiple-sort-weight'],
],
'#wrapper_attributes' => [
'class' => ['breezy-utility-multiple-table--weight'],
],
'#default_value' => $weight,
];
}
// Allow users to add & remove rows if cardinality is not set.
if ($element['#operations']) {
$row['_operations_'] = [
'#wrapper_attributes' => [
'class' => ['breezy-utility-multiple-table--operations'],
],
];
if ($element['#remove']) {
$row['_operations_']['#wrapper_attributes']['class'][] = 'breezy-utility-multiple-table--operations';
$row['_operations_']['remove'] = [
'#type' => 'image_button',
'#src' => \Drupal::service('extension.list.module')->getPath('breezy_utility') . '/images/icons/minus.svg',
'#title' => t('Remove @item @number', [
'@number' => $row_index + 1,
'@item' => $element['#item_label'],
]),
'#attributes' => [
'class' => ['breezy-utility-icon-button'],
],
'#limit_validation_errors' => [],
'#submit' => [[get_called_class(), 'removeItemSubmit']],
'#ajax' => $ajax_settings,
// Issue #1342066 Document that buttons with the same #value need a
// unique #name for the Form API to distinguish them, or change the
// Form API to assign unique #names automatically.
'#row_index' => $row_index,
'#name' => $table_id . '_remove_' . $row_index,
'#attached' => [
'library' => ['breezy_utility/breezy_utility.icons'],
],
];
}
}
// Add hidden element as a hidden row.
if ($hidden_elements) {
$row['_hidden_'] = $hidden_elements + [
'#wrapper_attributes' => ['style' => 'display: none'],
];
}
if ($element['#sorting']) {
$row['#attributes']['class'][] = 'draggable';
$row['#weight'] = $weight;
}
return $row;
}
/**
* Set element row default value recursively.
*
* @param array $element
* The element.
* @param array $default_value
* The default values.
*/
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);
}
}
/**
* Set element row default value recursively.
*
* @param array $element
* The element.
* @param mixed $default_value
* The default value.
*/
protected static function setElementDefaultValue(array &$element, $default_value) {
if ($element['#type'] === 'value') {
$element['#value'] = $default_value;
}
else {
$element['#default_value'] = $default_value;
// Set default value.
// @see \Drupal\webform\Plugin\WebformElementInterface::setDefaultValue
// @see \Drupal\webform\Plugin\WebformElement\DateBase::setDefaultValue
/** @var \Drupal\breezy_utility\BreezyUtilityElementPluginManagerInterface $element_manager */
$element_manager = \Drupal::service('plugin.manager.breezy_utility.element');
$element_plugin = $element_manager->getElementInstance($element);
$element_plugin->setDefaultValue($element);
}
}
/**
* Set element row parents recursively.
*
* This allow elements/columns to contain nested sub-elements.
*
* @param array $element
* The child element.
* @param string $element_key
* The child element's key.
* @param array $parents
* The main element's parents.
*/
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);
}
}
/* ************************************************************************ */
// Callbacks.
/* ************************************************************************ */
/**
* Submission handler for adding more items.
*
* @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.
*/
public static function addItemsSubmit(array &$form, FormStateInterface $form_state) {
// Get the BreezyUtility list element by going up two levels.
$button = $form_state->getTriggeringElement();
$element =& NestedArray::getValue($form, array_slice($button['#array_parents'], 0, -2));
// Add more items to the number of items.
$number_of_items_storage_key = static::getStorageKey($element, 'number_of_items');
$number_of_items = $form_state->get($number_of_items_storage_key);
$more_items = 1;
// Limit to prevent out-of-memory errors.
if ($more_items > 100) {
$more_items = 100;
}
$form_state->set($number_of_items_storage_key, $number_of_items + $more_items);
// Reset values.
$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);
// Rebuild the form.
$form_state->setRebuild();
}
/**
* Submission handler for adding an item.
*
* @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.
*/
public static function addItemSubmit(array &$form, FormStateInterface $form_state) {
$button = $form_state->getTriggeringElement();
$element = NestedArray::getValue($form, array_slice($button['#array_parents'], 0, -4));
// Add item.
$values = [];
foreach ($element['items']['#value'] as $row_index => $value) {
$values[] = $value;
if ($row_index === $button['#row_index']) {
$values[] = [];
}
}
// Add one item to the 'number of items'.
$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);
// Reset values.
$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);
// Rebuild the form.
$form_state->setRebuild();
}
/**
* Submission handler for removing an item.
*
* @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.
*/
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'];
// Remove item.
unset($values[$button['#row_index']]);
$values = array_values($values);
// Remove one item from the 'number of items'.
$number_of_items_storage_key = static::getStorageKey($element, 'number_of_items');
$number_of_items = $form_state->get($number_of_items_storage_key);
// Never allow the number of items to be less than #min_items.
if ($number_of_items > $element['#min_items']) {
$form_state->set($number_of_items_storage_key, $number_of_items - 1);
}
// Reset values.
$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);
// Rebuild the form.
$form_state->setRebuild();
}
/**
* Submission Ajax callback the returns the list table.
*/
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));
// Make sure only the ajax prefix and suffix is used.
$element['#prefix'] = $element['#ajax_prefix'];
$element['#suffix'] = $element['#ajax_suffix'];
// Disable states and flexbox wrapper.
// @see \Drupal\webform\Plugin\WebformElementBase::preRenderFixFlexboxWrapper
$element['#breezy_element_wrapper'] = FALSE;
return $element;
}
/**
* Validates BreezyUtility multiple element.
*/
public static function validateBreezyUtilityMultiple(&$element, FormStateInterface $form_state, &$complete_form) {
// IMPORTANT: Must get values from the $form_states since sub-elements
// may call $form_state->setValueForElement() via their validation hook.
// @see \Drupal\webform\Element\WebformEmailConfirm::validateWebformEmailConfirm
// @see \Drupal\webform\Element\WebformOtherBase::validateWebformOther
$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'];
// Validate unique keys.
if ($error_message = static::validateUniqueKeys($element, $items)) {
$form_state->setError($element, $error_message);
return;
}
// Convert values to items and validate duplicate keys.
$items = static::convertValuesToItems($element, $items);
// Validate required items.
if (!empty($element['#required']) && empty($items)) {
BreezyUtilityElementHelper::setRequiredError($element, $form_state);
}
}
else {
$items = [];
}
$element['#value'] = $items;
$form_state->setValueForElement($element, $items);
}
/* ************************************************************************ */
// Helper functions.
/* ************************************************************************ */
/**
* Get unique key used to store the number of items for an element.
*
* @param array $element
* An element.
* @param string $name
* The storage key's name.
*
* @return string
* A unique key used to store the number of items for an element.
*/
public static function getStorageKey(array $element, $name) {
return 'breezy_utility_multiple__' . $element['#name'] . '__' . $name;
}
/**
* Convert array of values to array of items.
*
* Convert an array containing values (elements or _item_ and weight) to an
* array of items.
*
* @param array $element
* The multiple element.
* @param array $values
* An array containing of item and weight.
*
* @return array
* An array of items.
*
* @throws \Exception
* Throws unique key required validation error message as an exception.
*/
public static function convertValuesToItems(array $element, array $values = []) {
// Sort the item values.
if ($element['#sorting']) {
// @todo Add sorting.
}
// Now build the associative array of items.
$items = [];
foreach ($values as $value) {
$item = static::convertValueToItem($value);
// Never add an empty item.
if (static::isEmpty($item)) {
continue;
}
// If #key is defined use it as the $items key.
if (!empty($element['#key']) && isset($item[$element['#key']])) {
$key_name = $element['#key'];
$key_value = $item[$key_name];
unset($item[$key_name]);
$items[$key_value[$key_name]] = $item;
}
else {
$items[] = $item;
}
}
return $items;
}
/**
* Convert value array containing (elements or _item_ and weight) to an item.
*
* @param array $value
* The multiple value array.
*
* @return array
* An item array.
*/
public static function convertValueToItem(array $value) {
if (isset($value['_item_'])) {
return $value['_item_'];
}
else {
// Get hidden (#access: FALSE) elements in the '_handle_' column and
// add them to the $value.
// @see \Drupal\webform\Element\WebformMultiple::buildElementRow
if (isset($value['_hidden_']) && is_array($value['_hidden_'])) {
$value += $value['_hidden_'];
}
unset($value['weight'], $value['_operations_'], $value['_hidden_']);
return $value;
}
}
/**
* Validate composite element has unique keys.
*
* @param array $element
* The multiple element.
* @param array $values
* An array containing of item and weight.
*
* @return null|string
* NULL if element has unique keys, else an error message with
* the duplicate key.
*/
protected static function validateUniqueKeys(array $element, array $values) {
// Only validate if the element's #key is defined.
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];
// Skip empty key and item.
unset($item[$key_name]);
if (empty($key_value) && static::isEmpty($item)) {
continue;
}
if (isset($unique_keys[$key_value])) {
$elements = BreezyUtilityElementHelper::getFlattened($element['#element']);
$key_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;
}
/**
* Check if array is empty.
*
* @param string|array $value
* An item.
*
* @return bool
* FALSE if item is an empty string or an empty array.
*/
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;
}
}
/**
* Determine if an element is hidden.
*
* @param array $element
* The element.
*
* @return bool
* TRUE if the element is hidden.
*/
protected static function isHidden(array $element) {
return !Element::isVisibleElement($element);
}
/**
* Determine if any sub-element is required.
*
* @param array $element
* An element.
*
* @return bool
* TRUE if any sub-element is required.
*/
protected static function hasRequireElement(array $element) {
$required_properties = [
'#required' => TRUE,
'#_required' => TRUE,
];
return BreezyUtilityElementHelper::hasProperties($element, $required_properties);
}
}
