a12s-1.0.0-beta7/modules/theme_builder/src/Block/MenuBlockAttributes.php
modules/theme_builder/src/Block/MenuBlockAttributes.php
<?php
declare(strict_types=1);
namespace Drupal\a12s_theme_builder\Block;
use Drupal\a12s_core\FormHelperTrait;
use Drupal\block\BlockInterface;
use Drupal\Component\Utility\NestedArray;
use Drupal\Core\Form\SubformStateInterface;
use Drupal\Core\Render\Element;
use Drupal\Core\Template\Attribute;
use Drupal\Core\Url;
/**
* Provides form for managing menu attributes settings and tools for rendering
* the menu with their attributes.
*
* @todo Integrate with "Menu Attributes" module to support icons fonts.
*/
class MenuBlockAttributes extends GlobalAttributes {
use FormHelperTrait;
/**
* {@inheritdoc}
*/
public function defaultValues(): array {
$default = parent::defaultValues();
$default['settings']['levels'] = [
'all' => 1,
'operator' => 'equal',
'level' => 1,
];
// @todo handle some conditions.
//$default['settings']['conditions'] = [
// 'expanded' => '',
// 'has_children' => '',
// 'menu_item_uuid' => '',
//];
return $default;
}
/**
* {@inheritdoc}
*/
public function applies(BlockInterface $entity): bool {
return str_starts_with($entity->getPluginId(), 'system_menu_block:') || str_starts_with($entity->getPluginId(), 'menu_block:');
}
/**
* {@inheritdoc}
*/
public function configurationKey(): string {
return 'menu_attributes';
}
/**
* {@inheritdoc}
*/
public function buildForm(array &$form, SubformStateInterface $formState, array $settings = []): void {
parent::buildForm($form, $formState, $settings);
$form['#title'] = $this->t('Menu attributes');
}
/**
* {@inheritdoc}
*/
public function submitForm(array &$form, SubformStateInterface $formState): void {
$values = $this->cleanAttributes($formState);
// Order values.
usort($values, function($a, $b) {
$order = ['nav', 'ul', 'li', 'a'];
$aIndex = array_search($a['element'], $order);
$bIndex = array_search($b['element'], $order);
// @todo If equal, order by "all levels" setting?
return $aIndex - $bIndex;
});
$this->saveSettings($values, $formState);
}
// /**
// * {@inheritdoc}
// */
// protected function saveSettings(array $values, FormStateInterface $formState): void {
// // @todo add integration with menu_attributes module, with icon fonts...
// }
/**
* Build the settings for an attribute.
*
* @param array $parents
* The element parents.
* @param array $attribute
* The attribute values.
*
* @return array
*
* @todo Add some conditions regarding the "is_expanded", "is_collapsed",
* "in_active_trail" conditions or about the link type (link or nolink,
* with or without children...).
*/
protected function buildAttributeRow(array $parents, array $attribute = []): array {
$row = parent::buildAttributeRow($parents, $attribute);
$row['element']['#options'] = [
'nav' => $this->t('Navigation'),
'ul' => $this->t('List wrapper'),
'li' => $this->t('List item'),
'a' => $this->t('Link'),
];
$this->mergeWithDefault($attribute);
$row['settings']['levels'] = [
'#type' => 'fieldset',
'#title' => $this->t('Levels'),
'#weight' => -10,
'#states' => [
'invisible' => [
$this->getInputNameFromPath('select', $parents, 'element') => [
['value' => ''],
['value' => 'nav'],
],
],
],
];
$row['settings']['levels']['all'] = [
'#type' => 'checkbox',
'#title' => t('All levels'),
'#default_value' => $attribute['settings']['levels']['all'],
];
$row['settings']['levels']['custom'] = [
'#type' => 'item',
'#title' => $this->t('Target levels'),
'#states' => [
'invisible' => [
$this->getInputNameFromPath(':input', $parents, ['settings', 'levels', 'all']) => ['checked' => TRUE],
],
],
'#wrapper_attributes' => [
'class' => ['container-inline'],
],
];
$row['settings']['levels']['custom']['operator'] = [
'#type' => 'select',
'#title' => t('Operator'),
'#title_display' => 'invisible',
'#options' => [
'equal' => $this->t('@label (@operator)', [
'@label' => $this->t('Equal'),
'@operator' => '=',
]),
'lower' => $this->t('@label (@operator)', [
'@label' => $this->t('Lower'),
'@operator' => '<',
]),
'lower_equal' => $this->t('@label (@operator)', [
'@label' => $this->t('Lower or equal'),
'@operator' => '≤',
]),
'greater' => $this->t('@label (@operator)', [
'@label' => $this->t('Greater'),
'@operator' => '>',
]),
'greater_equal' => $this->t('@label (@operator)', [
'@label' => $this->t('Greater or equal'),
'@operator' => '≥',
]),
'different' => $this->t('@label (@operator)', [
'@label' => $this->t('Different'),
'@operator' => '≠',
]),
],
'#default_value' => $attribute['settings']['levels']['operator'],
'#parents' => array_merge($parents, ['settings', 'levels', 'operator']),
];
$row['settings']['levels']['custom']['level'] = [
'#type' => 'number',
'#title' => t('Level'),
'#title_display' => 'invisible',
'#min' => 1,
'#default_value' => $attribute['settings']['levels']['level'],
'#parents' => array_merge($parents, ['settings', 'levels', 'level']),
];
return $row;
}
/**
* {@inheritdoc}
*
* Inject the "menu_attributes" settings to the build array.
*/
public function preRenderBlock($build): array {
if ($attributes = &$build['#a12s_theme_builder']['menu_attributes']['attributes']) {
foreach ($attributes as $attribute) {
if ($attribute['element'] === 'nav') {
$build += ['#attributes' => []];
$this->processAttribute($build['#attributes'], $attribute);
}
}
if (!empty($build['content']['#items'])) {
$this->processElement($build['content'], $build['content'], [], $attributes, 'ul', 1, '#items');
}
}
return $build;
}
/**
* {@inheritdoc}
*/
protected function processAttribute(Attribute|array &$attributes, array $attribute, array $context = []): void {
$context += ['level' => 1];
parent::processAttribute($attributes, $attribute, $context);
}
/**
* {@inheritdoc}
*/
protected function processAttributeValue(string $value, array $context = []): string {
return strtr($value, ['[level]' => $context['level']]);
}
/**
* Apply recursively attributes to a menu and its children.
*
* @param array $root
* The root element of the menu.
* @param array $element
* The current element being processed.
* @param array $parents
* The full list of parents for the current element.
* @param array $attributes
* An array of attributes to be processed.
* @param string $type
* The type of element being processed. Defaults to 'ul'.
* @param int $level
* The level of the element. Defaults to 1.
* @param string|null $childKey
* The child key for the current element. Defaults to NULL. This is only
* necessary as the menu structure is not the same between the first level
* and the others.
*/
protected function processElement(array &$root, array &$element, array $parents, array $attributes, string $type = 'ul', int $level = 1, ?string $childKey = NULL): void {
$context = ['level' => $level];
// Process the current element.
foreach ($attributes as $attribute) {
if ($attribute['element'] !== $type) {
continue;
}
switch ($type) {
case 'ul':
// For root UL, things are simple as default template makes use of
// #attributes.
if ($level === 1) {
$element += ['#attributes' => []];
$this->processAttribute($element['#attributes'], $attribute, $context);
}
// But for other UL, we need to find some weird processes... So we
// store the UL attributes in the parent.
else {
$ulAttributes = [];
$this->processAttribute($ulAttributes, $attribute, $context);
if (!empty($ulAttributes)) {
$parentElement = &NestedArray::getValue($root, array_slice($parents, 0, -1));
$parentElement['ul_attributes'] = new Attribute($ulAttributes);
}
}
break;
case 'li':
$element += ['attributes' => new Attribute()];
$this->processAttribute($element['attributes'], $attribute, $context);
break;
case 'a':
if (isset($element['url']) && $element['url'] instanceof Url) {
$attributesInstance = $element['url']->getOption('attributes') ?? [];
$this->processAttribute($attributesInstance, $attribute, $context);
$element['url']->setOption('attributes', $attributesInstance);
}
break;
}
}
// Process children.
switch ($type) {
case 'ul':
if ($childKey) {
foreach ($element[$childKey] ?? [] as $key => &$item) {
$this->processElement($root, $item, array_merge($parents, [$childKey, $key]), $attributes, 'li', $level);
}
}
else {
foreach (Element::children($element) as $key) {
$this->processElement($root, $element[$key], array_merge($parents, [$key]), $attributes, 'li', $level);
}
}
break;
case 'li':
$this->processElement($root, $element, $parents, $attributes, 'a', $level);
break;
case 'a':
if (!empty($element['below'])) {
$this->processElement($root, $element['below'], array_merge($parents, ['below']), $attributes, 'ul', ($level + 1));
}
break;
}
}
/**
* {@inheritdoc}
*
* @param array $settings
* The settings may contain the levels condition:
* - 'all' (bool): Indicates if the attribute applies to all levels.
* - 'level' (int): The level value to compare with current level.
* - 'operator' (string|null): The comparison operator for the level value.
*/
protected function attributeApplies(array $settings, array $context = []): bool {
$context += ['level' => 1];
$currentLevel = $context['level'];
$levelsSettings = $settings['levels'] ?? [];
if (!empty($levelsSettings['all'])) {
return TRUE;
}
// 0 is not a possible value.
elseif (!empty($levelsSettings['level'])) {
$levelValue = $levelsSettings['level'];
return match ($levelsSettings['operator'] ?? 'unknown') {
'lower' => $currentLevel < $levelValue,
'lower_equal' => $currentLevel <= $levelValue,
'greater' => $currentLevel > $levelValue,
'greater_equal' => $currentLevel >= $levelValue,
'different' => $currentLevel != $levelValue,
'equal' => $currentLevel == $levelValue,
default => FALSE,
};
}
return FALSE;
}
}
