custom_field-1.0.x-dev/src/Plugin/Field/FieldWidget/CustomFlexWidget.php
src/Plugin/Field/FieldWidget/CustomFlexWidget.php
<?php
namespace Drupal\custom_field\Plugin\Field\FieldWidget;
use Drupal\Core\Field\Attribute\FieldWidget;
use Drupal\Core\Field\FieldItemListInterface;
use Drupal\Core\Field\FieldStorageDefinitionInterface;
use Drupal\Core\Form\FormStateInterface;
use Drupal\Core\StringTranslation\TranslatableMarkup;
use Drupal\custom_field\Plugin\CustomField\FieldType\DateTimeType;
use Drupal\custom_field\Plugin\CustomFieldTypeInterface;
/**
* Plugin implementation of the 'custom_flex' widget.
*/
#[FieldWidget(
id: 'custom_flex',
label: new TranslatableMarkup('Flexbox'),
field_types: [
'custom',
],
weight: 0,
)]
class CustomFlexWidget extends CustomWidgetBase {
/**
* {@inheritdoc}
*/
public static function defaultSettings(): array {
return [
'breakpoint' => '',
'columns' => [],
] + parent::defaultSettings();
}
/**
* {@inheritdoc}
*/
public function settingsForm(array $form, FormStateInterface $form_state): array {
$elements = parent::settingsForm($form, $form_state);
$elements['#tree'] = TRUE;
$elements['#attached']['library'][] = 'custom_field/custom-field-flex';
$elements['#attached']['library'][] = 'custom_field/custom-field-flex-admin';
$fields = $this->getSetting('fields') ?? [];
$custom_items = $this->sortFields($fields);
$elements['columns'] = [
'#type' => 'fieldset',
'#title' => $this->t('Column settings'),
'#description' => $this->t('Select the number of columns for each form element. A value of <em>auto</em> will size the column based on the natural width of content within it.'),
'#description_display' => 'before',
];
$elements['columns']['prefix'] = [
'#markup' => '<div class="custom-field-row custom-field-flex--widget-settings">',
];
$columns = $this->getSettings()['columns'];
foreach ($custom_items as $name => $custom_item) {
$plugin_id = $custom_item->getPluginId();
// The uuid widget type is a hidden field.
if ($plugin_id == 'uuid') {
continue;
}
$elements['columns'][$name] = [
'#type' => 'select',
'#title' => $custom_item->getLabel(),
'#options' => $this->columnOptions(),
'#wrapper_attributes' => [
'class' => ['custom-field-col'],
],
'#attributes' => [
'class' => ['custom-field-col__field'],
],
];
if (isset($columns[$name])) {
$elements['columns'][$name]['#default_value'] = $columns[$name];
$elements['columns'][$name]['#wrapper_attributes']['class'][] = 'custom-field-col-' . $columns[$name];
}
}
$elements['columns']['suffix'] = [
'#markup' => '</div>',
];
$elements['breakpoint'] = [
'#type' => 'select',
'#title' => $this->t('Stack items on:'),
'#description' => $this->t('The device width in which the columns are set to full width and stack on top of one another.'),
'#options' => $this->breakpointOptions(),
'#default_value' => $this->getSetting('breakpoint'),
];
return $elements;
}
/**
* {@inheritdoc}
*/
public function settingsSummary(): array {
$summary = parent::settingsSummary();
$columns = 'Automatic';
if (!empty($this->getSettings()['columns'])) {
$columns = implode(' | ', $this->getSettings()['columns']);
}
$summary[] = $this->t('Column settings: @columns', ['@columns' => $columns]);
$summary[] = $this->t('Stack on: @breakpoint', ['@breakpoint' => $this->breakpointOptions($this->getSetting('breakpoint'))]);
return $summary;
}
/**
* {@inheritdoc}
*
* @throws \Drupal\Component\Plugin\Exception\PluginException
*/
public function formElement(FieldItemListInterface $items, $delta, array $element, array &$form, FormStateInterface $form_state): array {
$element = parent::formElement($items, $delta, $element, $form, $form_state);
$element['#attached']['library'][] = 'custom_field/custom-field-flex';
$fields = $this->getSetting('fields') ?? [];
$classes = ['custom-field-row'];
if ($this->getSetting('breakpoint')) {
$classes[] = 'custom-field-flex--stack-' . $this->getSetting('breakpoint');
}
// Using markup since we can't nest values because the field api expects
// subfields to be at the top-level.
$element['wrapper_prefix']['#markup'] = '<div class="' . implode(' ', $classes) . '">';
$columns = $this->getSettings()['columns'];
// Account for unsaved fields in field config default values form.
if (!empty($form_state->get('current_settings'))) {
$current_settings = $form_state->get('current_settings');
$custom_items = $this->customFieldTypeManager->getCustomFieldItems($current_settings);
}
else {
$custom_items = $this->sortFields($fields);
}
foreach ($custom_items as $name => $custom_item) {
$settings = $fields[$name] ?? [];
$data_type = $custom_item->getDataType();
$type = $settings['type'] ?? $custom_item->getDefaultWidget();
if (!in_array($type, $this->customFieldWidgetManager->getWidgetsForField($custom_item->getPluginId()))) {
$type = $custom_item->getDefaultWidget();
}
/** @var \Drupal\custom_field\Plugin\CustomFieldWidgetInterface $widget_plugin */
$widget_plugin = $this->customFieldWidgetManager->createInstance((string) $type, ['settings' => $settings]);
$element[$name] = $widget_plugin->widget($items, $delta, $element, $form, $form_state, $custom_item);
$attributes = $this->getAttributesKey($custom_item, $type);
$column_class = "custom-field-$name custom-field-col";
$column_class .= isset($columns[$name]) ? " custom-field-col-$columns[$name]" : ' custom-field-col-auto';
$entity_reference_widgets = [
'entity_reference_autocomplete',
'entity_reference_radios',
'entity_reference_select',
];
// For the file widget, we assign to the outer ajax wrapper div.
if (isset($element[$name]['#type']) && $element[$name]['#type'] === 'managed_file') {
$element[$name]['#column_class'] = $column_class;
$element[$name]['#after_build'][] = [$this, 'callManagedFileAfterBuild'];
}
// Entity reference widgets need class on the target_id element.
elseif (in_array($type, $entity_reference_widgets)) {
$element[$name]['target_id'][$attributes]['class'][] = $column_class;
}
// The datetime widgets have different wrapper types.
elseif ($data_type === 'datetime' && in_array($type, ['datetime_default', 'datetime_local'])) {
$datetime_type = $custom_item->getDatetimeType();
if (($datetime_type === DateTimeType::DATETIME_TYPE_DATE || $type === 'datetime_local') && !isset($element[$name]['timezone'])) {
$element[$name]['value']['#wrapper_attributes']['class'][] = $column_class;
}
else {
$element[$name][$attributes]['class'][] = $column_class;
}
}
// For the date range widget, we assign to the outer ajax wrapper div.
elseif ($data_type === 'daterange' && $type === 'daterange_default') {
$element[$name]['#column_class'] = $column_class;
$element[$name]['#after_build'][] = [$this, 'callDateRangeAfterBuild'];
}
else {
// The duration input element is wrapped in a fieldset.
if ($data_type === 'duration' && $settings['duration_element'] === 'input') {
$attributes = '#attributes';
}
$element[$name][$attributes]['class'][] = $column_class;
}
}
$element['wrapper_suffix']['#markup'] = '</div>';
return $element;
}
/**
* Closure function to pass arguments to dateRangeAfterBuild().
*
* @param array<string, mixed> $element
* The form element.
* @param \Drupal\Core\Form\FormStateInterface $form_state
* The form state.
*
* @return array<string, mixed>
* The element array.
*/
public function callDateRangeAfterBuild(array $element, FormStateInterface $form_state): array {
$column = $element['#column_class'];
return static::dateRangeAfterBuild($element, $form_state, $column);
}
/**
* Closure function to pass arguments to managedFileAfterBuild().
*
* @param array<string, mixed> $element
* The form element.
* @param \Drupal\Core\Form\FormStateInterface $form_state
* The form state.
*
* @return array<string, mixed>
* The element array.
*/
public function callManagedFileAfterBuild(array $element, FormStateInterface $form_state): array {
$column = $element['#column_class'];
return static::managedFileAfterBuild($element, $form_state, $column);
}
/**
* After build function to add class to file outer ajax wrapper div.
*
* @param array<string, mixed> $element
* The form element.
* @param \Drupal\Core\Form\FormStateInterface $form_state
* The form state.
* @param string $column
* The column class.
*
* @return array<string, mixed>
* The modified form element.
*/
public static function managedFileAfterBuild(array $element, FormStateInterface $form_state, string $column): array {
if (preg_match('/id="([^"]*ajax-wrapper[^"]*)"/', $element['#prefix'], $matches)) {
$id_attribute = $matches[0];
// Check if the class attribute exists.
if (str_contains($element['#prefix'], 'class="')) {
// If class exists, append the new class.
$element['#prefix'] = str_replace('class="', 'class="' . $column . ' ', $element['#prefix']);
}
else {
// If no class attribute exists, insert one after the id attribute.
$element['#prefix'] = str_replace($id_attribute, $id_attribute . ' class="' . $column . '"', $element['#prefix']);
}
}
return $element;
}
/**
* After build function to add class to date range outer ajax wrapper div.
*
* @param array<string, mixed> $element
* The form element.
* @param \Drupal\Core\Form\FormStateInterface $form_state
* The form state.
* @param string $column
* The column class.
*
* @return array<string, mixed>
* The modified form element.
*/
public static function dateRangeAfterBuild(array $element, FormStateInterface $form_state, string $column): array {
$id_attribute = '';
if (preg_match('/id="([^"]+)"/', $element['#prefix'], $matches)) {
$id_attribute = $matches[0];
}
// Check if the class attribute exists.
if (str_contains($element['#prefix'], 'class="')) {
// If class exists, append the new class.
$element['#prefix'] = str_replace('class="', 'class="' . $column . ' ', $element['#prefix']);
}
else {
// If no class attribute exists, insert one after the id attribute.
$element['#prefix'] = str_replace($id_attribute, $id_attribute . ' class="' . $column . '"', $element['#prefix']);
}
return $element;
}
/**
* Determine which attributes to use based on the plugin type.
*
* @param \Drupal\custom_field\Plugin\CustomFieldTypeInterface $custom_item
* The custom field item.
* @param string $type
* The widget type.
*
* @return string
* The attribute key string.
*/
protected function getAttributesKey(CustomFieldTypeInterface $custom_item, string $type): string {
$field_settings = $custom_item->getFieldSettings();
$attribute_types = [
'color_boxes',
'media_library_widget',
'viewfield_select',
'entity_reference_radios',
'radios',
'datetime_datelist',
'datetime_default',
'datetime_local',
'time_range',
'url',
'link_default',
'linkit_url',
'linkit',
];
if (in_array($type, $attribute_types)) {
return '#attributes';
}
switch ($custom_item->getPluginId()) {
case 'string_long':
$formatted = $field_settings['formatted'] ?? FALSE;
return $formatted ? '#attributes' : '#wrapper_attributes';
default:
return '#wrapper_attributes';
}
}
/**
* Get the field storage definition.
*/
public function getFieldStorageDefinition(): FieldStorageDefinitionInterface {
return $this->fieldDefinition->getFieldStorageDefinition();
}
/**
* The options for columns.
*
* @param string|null $option
* The option key.
*
* @return array<string|int, TranslatableMarkup>|string
* The options or option.
*/
public function columnOptions(?string $option = NULL): array|string {
$options = [
'auto' => $this->t('Auto'),
1 => $this->t('1 column'),
2 => $this->t('2 columns'),
3 => $this->t('3 columns'),
4 => $this->t('4 columns'),
5 => $this->t('5 columns'),
6 => $this->t('6 columns'),
7 => $this->t('7 columns'),
8 => $this->t('8 columns'),
9 => $this->t('9 columns'),
10 => $this->t('10 columns'),
11 => $this->t('11 columns'),
12 => $this->t('12 columns'),
];
// @todo Better move this logic out of this function, so this function can
// have single return type.
if (!is_null($option)) {
return $options[$option] ?? '';
}
return $options;
}
/**
* The options for breakpoints.
*
* @param string|null $option
* The option key.
*
* @return array<string, TranslatableMarkup>|string
* The options or option.
*/
public function breakpointOptions(?string $option = NULL): array|string {
$options = [
'' => $this->t("Don't stack"),
'medium' => $this->t('Medium (less than 769px)'),
'small' => $this->t('Small (less than 601px)'),
];
// @todo Better move this logic out of this function, so this function can
// have single return type.
if (!is_null($option)) {
return $options[$option] ?? '';
}
return $options;
}
}
