a12s-1.0.0-beta7/modules/theme_builder/src/Block/GlobalAttributes.php
modules/theme_builder/src/Block/GlobalAttributes.php
<?php
declare(strict_types=1);
namespace Drupal\a12s_theme_builder\Block;
use Drupal\a12s_core\FormHelperTrait;
use Drupal\block\BlockInterface;
use Drupal\Component\Serialization\Json;
use Drupal\Component\Utility\Html;
use Drupal\Component\Utility\NestedArray;
use Drupal\Core\Form\FormStateInterface;
use Drupal\Core\Form\SubformStateInterface;
use Drupal\Core\Render\Element;
use Drupal\Core\Template\Attribute;
/**
* Provides form for managing global attributes for blocks.
*/
class GlobalAttributes extends SettingsAlterBase {
use FormHelperTrait;
/**
* {@inheritdoc}
*/
public function defaultValues(): array {
return [
'element' => NULL,
'attribute' => NULL,
'value' => '',
'settings' => [
'data_attribute' => [
'name' => '',
'json_encode' => FALSE,
],
'aria_attribute' => [
'name' => '',
],
],
];
}
/**
* {@inheritdoc}
*/
public function applies(BlockInterface $entity): bool {
return TRUE;
}
/**
* {@inheritdoc}
*/
public function configurationKey(): string {
return 'global_attributes';
}
/**
* {@inheritdoc}
*/
public function buildForm(array &$form, SubformStateInterface $formState, array $settings = []): void {
$attributes = $settings['attributes'] ?? [];
$wrapperId = Html::cleanCssIdentifier($this->configurationKey()) . '-wrapper';
$form += [
'#type' => 'details',
'#title' => $this->t('Global attributes'),
//'#open' => !empty($attributes),
'#parents' => [],
];
// @todo Add a collapsible documentation.
$form['attributes'] = [
'#type' => 'table',
'#header' => [
$this->t('Element'),
$this->t('Attribute'),
$this->t('Value'),
$this->t('Settings'),
],
'#element_validate' => [
[static::class, 'validateAttributes'],
],
'#prefix' => '<div id="' . $wrapperId . '">',
'#suffix' => '</div>',
];
$values = $formState->getValue('attributes', []);
$formParents = $form['#parents'];
$attributesElement = &$form['attributes'];
$getCurrentParents = function() use ($formParents, &$attributesElement) {
return array_merge($formParents, ['attributes', count(Element::children($attributesElement))]);
};
foreach ($attributes as $attribute) {
if (is_array($attribute)) {
$form['attributes'][] = $this->buildAttributeRow($getCurrentParents(), $attribute);
if ($values) {
array_shift($values);
}
}
}
// Add extra rows.
foreach ($values as $attribute) {
if (is_array($attribute)) {
$form['attributes'][] = $this->buildAttributeRow($getCurrentParents(), $attribute);
}
}
// Add a new row.
$form['attributes'][] = $this->buildAttributeRow($getCurrentParents());
$form['add_attributes'] = [
'#type' => 'button',
'#value' => $this->t('Add another attribute'),
'#ajax' => [
'callback' => [$this, 'addAttributeRow'],
'wrapper' => $wrapperId,
],
];
}
/**
* Validates attributes for the full table form element.
*
* @param array $element
* The form element to validate attributes for.
* @param \Drupal\Core\Form\FormStateInterface $form_state
* The form state object.
* @param array $complete_form
* The complete form array.
*
* @return void
*/
public static function validateAttributes(array &$element, FormStateInterface $form_state, array &$complete_form): void {
$input_exists = FALSE;
$input = NestedArray::getValue($form_state->getValues(), $element['#parents'], $input_exists);
if ($input && is_array($input)) {
foreach ($input as $index => $item) {
// Is element empty?
if (empty($item['element']) || empty($item['attribute'])) {
continue;
}
if ($item['attribute'] === 'data') {
// @todo check for valid data attribute name?
if (empty($item['settings']['data_attribute']['name'])) {
$form_state->setError($element[$index]['settings']['data_attribute']['name'], t('The data attribute name is required.'));
}
}
elseif (!(isset($item['value']) && strlen($item['value']))) {
$form_state->setError($element[$index]['value'], t('The value is required.'));
}
}
}
}
/**
* {@inheritdoc}
*/
public function submitForm(array &$form, SubformStateInterface $formState): void {
$values = $this->cleanAttributes($formState);
// Order values.
usort($values, function($a, $b) {
$order = ['block', 'title', 'content'];
$aIndex = array_search($a['element'], $order);
$bIndex = array_search($b['element'], $order);
return $aIndex - $bIndex;
});
$this->saveSettings($values, $formState);
}
/**
* {@inheritdoc}
*/
protected function saveSettings(array $values, FormStateInterface $formState): void {
$key = $this->configurationKey();
/** @var \Drupal\block\Entity\Block $block */
$block = $formState->getFormObject()->getEntity();
$thirdPartySetting = $block->getThirdPartySetting('a12s_theme_builder', $key, []);
// Values are stored under "attributes" key, to allow further integration
// with menu_attributes module, with icon fonts...
if (empty($values)) {
unset($thirdPartySetting['attributes']);
}
else {
$thirdPartySetting['attributes'] = $values;
}
parent::saveSettings($thirdPartySetting, $formState);
}
/**
* AJAX callback; return the updated attributes table.
*
* @param array &$form
* The form array.
*
* @return array
*/
public function addAttributeRow(array &$form): array {
return $form['a12s_theme_builder'][$this->configurationKey()]['attributes'];
}
/**
* Build the settings for an attribute.
*
* @param array $parents
* The element parents.
* @param array $attribute
* The attribute values.
*
* @return array
*/
protected function buildAttributeRow(array $parents, array $attribute = []): array {
$this->mergeWithDefault($attribute);
$row['element'] = [
'#type' => 'select',
'#title' => t('Element'),
'#options' => [
'block' => $this->t('Block container'),
'title' => $this->t('Title'),
'content' => $this->t('Content'),
],
'#empty_option' => $this->t('- Select -'),
'#title_display' => 'invisible',
'#default_value' => $attribute['element'],
];
$row['attribute'] = [
'#type' => 'select',
'#title' => t('Attribute'),
'#options' => [
'id' => $this->t('Identifier'),
'class' => $this->t('CSS Class'),
'data' => $this->t('Data attribute'),
'role' => $this->t('Role'),
'aria' => $this->t('ARIA attribute'),
],
'#empty_option' => $this->t('- Select -'),
'#title_display' => 'invisible',
'#default_value' => $attribute['attribute'],
];
// @todo make "value" required when attribute is "class".
// @todo add AJAX and convert to select for ARIA and role?
// @todo add placeholder for item title (necessary for aria-label).
$row['value'] = [
'#type' => 'textfield',
'#title' => t('Value'),
'#title_display' => 'invisible',
'#default_value' => $attribute['value'],
];
$row['settings'] = [
'#type' => 'container',
];
$row['settings']['data_attribute'] = [
'#type' => 'fieldset',
'#title' => $this->t('Data attribute'),
'#states' => [
'visible' => [
$this->getInputNameFromPath('select', $parents, 'attribute') => ['value' => 'data'],
],
],
];
$row['settings']['data_attribute']['name'] = [
'#type' => 'textfield',
'#title' => t('Name'),
'#default_value' => $attribute['settings']['data_attribute']['name'],
'#states' => [
'required' => [
$this->getInputNameFromPath('select', $parents, 'attribute') => ['value' => 'data'],
],
],
];
$row['settings']['data_attribute']['json_encode'] = [
'#type' => 'checkbox',
'#title' => t('Encode in JSON format'),
'#default_value' => $attribute['settings']['data_attribute']['json_encode'],
];
$row['settings']['aria_attribute'] = [
'#type' => 'fieldset',
'#title' => $this->t('ARIA attribute'),
'#states' => [
'visible' => [
$this->getInputNameFromPath('select', $parents, 'attribute') => ['value' => 'aria'],
],
],
];
$row['settings']['aria_attribute']['name'] = [
'#type' => 'select',
'#title' => t('Name'),
'#default_value' => $attribute['settings']['aria_attribute']['name'],
'#options' => [
'aria-autocomplete' => $this->t('ARIA autocomplete'),
'aria-checked' => $this->t('ARIA checked'),
'aria-disabled' => $this->t('ARIA disabled'),
'aria-errormessage' => $this->t('ARIA error message'),
'aria-expanded' => $this->t('ARIA expanded'),
'aria-haspopup' => $this->t('ARIA has popup'),
'aria-hidden' => $this->t('ARIA hidden'),
'aria-invalid' => $this->t('ARIA invalid'),
'aria-label' => $this->t('ARIA label'),
'aria-level' => $this->t('ARIA level'),
'aria-modal' => $this->t('ARIA modal'),
'aria-multiline' => $this->t('ARIA multiline'),
'aria-multiselectable' => $this->t('ARIA multiselectable'),
'aria-orientation' => $this->t('ARIA orientation'),
'aria-placeholder' => $this->t('ARIA placeholder'),
'aria-pressed' => $this->t('ARIA pressed'),
'aria-readonly' => $this->t('ARIA readonly'),
'aria-selected' => $this->t('ARIA selected'),
'aria-sort' => $this->t('ARIA sort'),
'aria-valuemax' => $this->t('ARIA valuemax'),
'aria-valuemin' => $this->t('ARIA valuemin'),
'aria-valuenow' => $this->t('ARIA valuenow'),
'aria-valuetext' => $this->t('ARIA valuetext'),
],
'#states' => [
'required' => [
$this->getInputNameFromPath('select', $parents, 'attribute') => ['value' => 'aria'],
],
],
];
return $row;
}
/**
* {@inheritdoc}
*/
protected function usePreRenderCallback(): bool {
return TRUE;
}
/**
* {@inheritdoc}
*
* Inject the "menu_attributes" settings to the build array.
*/
public function preRenderBlock($build): array {
if ($attributes = &$build['#a12s_theme_builder'][$this->configurationKey()]['attributes']) {
$map = [
'block' => '#attributes',
'title' => '#title_attributes',
'content' => '#content_attributes',
];
foreach ($attributes as $attribute) {
if (isset($map[$attribute['element']])) {
$property = $map[$attribute['element']];
$build += [$property => []];
$this->processAttribute($build[$property], $attribute);
}
}
}
return $build;
}
/**
* Processes an attribute and modifies the whole attributes if applicable.
*
* @param Attribute|array &$attributes
* A reference to the attributes array to modify.
* @param array $attribute
* The attribute to process.
* @param array $context
* The current context.
*/
protected function processAttribute(Attribute|array &$attributes, array $attribute, array $context = []): void {
$this->mergeWithDefault($attribute);
// @todo use DI.
/** @var \Drupal\a12s_theme_builder\ThemeHelper $themeHelper */
$themeHelper = \Drupal::service('a12s_theme_builder.helper');
if ($this->attributeApplies($attribute['settings'] ?? [], $context)) {
switch ($attribute['attribute']) {
case 'id':
$value = $this->processAttributeValue($attribute['value'] ?? '', $context);
$themeHelper->setAttribute($attributes, 'id', $value);
break;
case 'class':
$value = $this->processAttributeValue($attribute['value'] ?? '', $context);
$themeHelper->addClasses($attributes, $value);
break;
case 'data':
$settings = $attribute['settings']['data_attribute'] ?? [];
if (!empty($settings['name'])) {
$name = "data-{$settings['name']}";
$value = $this->processAttributeValue($attribute['value'] ?? '', $context);
if (!empty($value) && !empty($settings['json_encode'])) {
$value = Json::encode($value);
}
$themeHelper->setAttribute($attributes, $name, $value);
}
break;
case 'role':
$themeHelper->setAttribute($attributes, 'role', $attribute['value'] ?? '');
break;
case 'aria':
$settings = $attribute['settings']['aria_attribute'] ?? [];
$value = $this->processAttributeValue($attribute['value'] ?? '', $context);
if (!empty($settings['name'])) {
$themeHelper->setAttribute($attributes, $settings['name'], $value);
}
break;
}
}
}
/**
* Process the attribute value.
*
* @param string $value
* The attribute value to be processed.
* @param array $context
* Optional. The current context.
*
* @return string
*/
protected function processAttributeValue(string $value, array $context = []): string {
return $value;
}
/**
* Determines if the given attribute applies based on the settings.
*
* @param array $settings
* The attribute settings.
* @param array $context
* The current context.
*
* @return bool Returns TRUE if the attribute applies, or FALSE otherwise.
*/
protected function attributeApplies(array $settings, array $context = []): bool {
return TRUE;
}
/**
* Cleans the attributes array by removing any empty or default values.
*
* @param \Drupal\Core\Form\SubformStateInterface $formState
* The subform state object.
*
* @return array
* An array of cleaned attributes.
*/
protected function cleanAttributes(SubformStateInterface $formState): array {
$values = [];
foreach ($formState->getValue(['attributes'], []) as $attribute) {
// Ignore empty values.
if (empty($attribute['element']) || empty($attribute['attribute'])) {
continue;
}
// Remove default values.
if ($attribute = $this->cleanValues($attribute)) {
$values[] = $attribute;
}
}
return $values;
}
}
