utilikit-1.0.0/src/Form/UtilikitSettingsForm.php
src/Form/UtilikitSettingsForm.php
<?php
declare(strict_types=1);
namespace Drupal\utilikit\Form;
use Drupal\Core\Form\ConfigFormBase;
use Drupal\Core\Form\FormStateInterface;
use Drupal\Core\Entity\EntityTypeManagerInterface;
use Drupal\Core\Datetime\DateFormatterInterface;
use Drupal\Core\Entity\FieldableEntityInterface;
use Drupal\Core\Extension\ModuleHandlerInterface;
use Drupal\Core\File\FileSystemInterface;
use Drupal\utilikit\Service\UtilikitServiceProvider;
use Drupal\utilikit\Service\UtilikitConstants;
use Drupal\utilikit\Traits\FormHelperTrait;
use Symfony\Component\DependencyInjection\ContainerInterface;
use Drupal\Core\StringTranslation\ByteSizeMarkup;
/**
* Configuration form for UtiliKit module settings.
*
* Provides administrative interface for configuring rendering mode,
* performance settings, scope controls, cache management, and developer
* tools for the UtiliKit utility class system.
*/
class UtilikitSettingsForm extends ConfigFormBase {
use FormHelperTrait;
/**
* The entity type manager service.
*
* @var \Drupal\Core\Entity\EntityTypeManagerInterface
*/
protected EntityTypeManagerInterface $entityTypeManager;
/**
* The date formatter service.
*
* @var \Drupal\Core\Datetime\DateFormatterInterface
*/
protected DateFormatterInterface $dateFormatter;
/**
* The module handler service.
*
* @var \Drupal\Core\Extension\ModuleHandlerInterface
*/
protected ModuleHandlerInterface $moduleHandler;
/**
* The UtiliKit service provider.
*
* @var \Drupal\utilikit\Service\UtilikitServiceProvider
*/
protected UtilikitServiceProvider $serviceProvider;
/**
* The file system service.
*
* @var \Drupal\Core\File\FileSystemInterface
*/
protected FileSystemInterface $fileSystem;
/**
* Constructs a new UtilikitSettingsForm object.
*
* @param \Drupal\Core\Entity\EntityTypeManagerInterface $entityTypeManager
* The entity type manager service.
* @param \Drupal\Core\Datetime\DateFormatterInterface $dateFormatter
* The date formatter service.
* @param \Drupal\Core\Extension\ModuleHandlerInterface $moduleHandler
* The module handler service.
* @param \Drupal\utilikit\Service\UtilikitServiceProvider $serviceProvider
* The UtiliKit service provider.
* @param \Drupal\Core\File\FileSystemInterface $fileSystem
* The file system service.
*/
public function __construct(
EntityTypeManagerInterface $entityTypeManager,
DateFormatterInterface $dateFormatter,
ModuleHandlerInterface $moduleHandler,
UtilikitServiceProvider $serviceProvider,
FileSystemInterface $fileSystem,
) {
$this->entityTypeManager = $entityTypeManager;
$this->dateFormatter = $dateFormatter;
$this->moduleHandler = $moduleHandler;
$this->serviceProvider = $serviceProvider;
$this->fileSystem = $fileSystem;
}
/**
* {@inheritdoc}
*/
public static function create(ContainerInterface $container): static {
return new static(
$container->get('entity_type.manager'),
$container->get('date.formatter'),
$container->get('module_handler'),
$container->get('utilikit.service_provider'),
$container->get('file_system')
);
}
/**
* {@inheritdoc}
*/
public function getFormId(): string {
return 'utilikit_settings_form';
}
/**
* {@inheritdoc}
*/
protected function getEditableConfigNames(): array {
return ['utilikit.settings'];
}
/**
* {@inheritdoc}
*/
public function buildForm(array $form, FormStateInterface $form_state): array {
// Attach the CSS library.
$form['#attached']['library'][] = 'utilikit/utilikit-settings';
$config = $this->config('utilikit.settings');
$form = array_merge($form, $this->buildRenderingModeSection($config));
$form = array_merge($form, $this->buildScanningScopeSection($config));
$form = array_merge($form, $this->buildPerformanceSection($config));
$form = array_merge($form, $this->buildScopeSection($config));
$form = array_merge($form, $this->buildTriggersSection($config));
$form = array_merge($form, $this->buildCacheSection($config));
$form = array_merge($form, $this->buildDeveloperSection($config));
return parent::buildForm($form, $form_state);
}
/**
* Builds the rendering mode configuration section.
*
* @param \Drupal\Core\Config\ImmutableConfig $config
* The current UtiliKit configuration.
*
* @return array
* Form elements for rendering mode selection.
*/
private function buildRenderingModeSection($config): array {
return [
'rendering_mode' => [
'#type' => 'radios',
'#title' => $this->t('Rendering Mode'),
'#options' => [
'inline' => $this->t('<strong>Inline Mode</strong> - JavaScript processes all utility classes dynamically on page load'),
'static' => $this->t('<strong>Static Mode</strong> - Pre-generated CSS file that must be updated either automatically (on save) or manually (update button) when adding new classes'),
'head' => $this->t('<strong>Head Mode</strong> - Server-generated CSS injected into page head on each request (no file system required)'),
],
'#default_value' => $config->get('rendering_mode') ?? 'inline',
],
];
}
/**
* Builds the performance configuration section.
*
* @param \Drupal\Core\Config\ImmutableConfig $config
* The current UtiliKit configuration.
*
* @return array
* Form elements for performance and optimization settings.
*/
private function buildPerformanceSection($config): array {
return [
'performance' => [
'#type' => 'details',
'#title' => $this->t('Performance & Optimization'),
'#open' => TRUE,
'active_breakpoints' => $this->createCheckboxesField(
$this->t('Active breakpoints'),
$this->t('Select which responsive breakpoints to enable. Disabling unused breakpoints can improve performance.'),
$this->getBreakpointOptions(),
$config->get('active_breakpoints') ?? UtilikitConstants::DEFAULT_BREAKPOINTS
),
'optimize_css' => $this->createCheckboxField(
$this->t('Optimize CSS output'),
$this->t('Minify and optimize the generated CSS to reduce size.'),
(bool) $config->get('optimize_css') ?? TRUE,
[
'states' => [
'visible' => [
[':input[name="rendering_mode"]' => ['value' => 'static']],
[':input[name="rendering_mode"]' => ['value' => 'head']],
],
],
]
),
'use_important' => $this->createCheckboxField(
$this->t('Use !important in CSS rules'),
$this->t('Add !important to all utility class rules to ensure they override inline styles or css variables. Recommended for utility-first workflows. <strong>Note:</strong> Changing this setting will regenerate all site CSS.'),
(bool) $config->get('use_important') ?? TRUE,
[
'states' => [
'visible' => [
[':input[name="rendering_mode"]' => ['value' => 'static']],
[':input[name="rendering_mode"]' => ['value' => 'head']],
],
],
]
),
'enable_transitions' => $this->createCheckboxField(
$this->t('Enable smooth transitions'),
$this->t('Apply CSS transitions when styles change dynamically (recommended for better UX).'),
(bool) $config->get('enable_transitions') ?? TRUE
),
'debounce' => $this->createNumberField(
$this->t('Resize debounce delay'),
$this->t('Milliseconds to wait before reapplying styles on window resize.'),
$config->get('debounce') ?? 50,
[
'min' => 0,
'max' => 500,
'step' => 10,
'field_suffix' => $this->t('ms'),
'attributes' => ['style' => 'width: 100px'],
]
),
],
];
}
/**
* Builds the scope configuration section.
*
* @param \Drupal\Core\Config\ImmutableConfig $config
* The current UtiliKit configuration.
*
* @return array
* Form elements for scope control settings.
*/
private function buildScopeSection($config): array {
return [
'scope' => [
'#type' => 'details',
'#title' => $this->t('Scope & Visibility'),
'#open' => TRUE,
'scope_global' => $this->createCheckboxField(
$this->t('Enable globally on all pages'),
$this->t('Load UtiliKit across your entire site.'),
(bool) $config->get('scope_global') ?? TRUE
),
'disable_admin' => $this->createCheckboxField(
$this->t('Disable on admin pages'),
$this->t('Prevent UtiliKit from loading on administrative routes. This does not affect UtiliKit development tools (Playground, Examples, Test Suite) which always run independently.'),
(bool) $config->get('disable_admin') ?? FALSE,
['states' => ['visible' => [':input[name="scope_global"]' => ['checked' => TRUE]]]]
),
'scope_content_types' => $this->createCheckboxField(
$this->t('Limit to specific content types'),
$this->t('Only load UtiliKit on selected content types.'),
(bool) $config->get('scope_content_types') ?? FALSE,
['states' => ['disabled' => [':input[name="scope_global"]' => ['checked' => TRUE]]]]
),
'enabled_content_types' => $this->buildContentTypesField($config),
],
];
}
/**
* Builds the content types field with options.
*
* @param \Drupal\Core\Config\ImmutableConfig $config
* The current UtiliKit configuration.
*
* @return array
* Form elements for content types selection.
*/
private function buildContentTypesField($config): array {
$node_types = $this->entityTypeManager->getStorage('node_type')->loadMultiple();
$options = [];
foreach ($node_types as $node_type) {
$options[$node_type->id()] = $node_type->label();
}
return $this->createCheckboxesField(
$this->t('Enabled content types'),
'',
$options,
$config->get('enabled_content_types') ?? [],
[
'states' => [
'visible' => [
':input[name="scope_content_types"]' => ['checked' => TRUE],
':input[name="scope_global"]' => ['checked' => FALSE],
],
'required' => [':input[name="scope_content_types"]' => ['checked' => TRUE]],
],
'prefix' => '<div class="form-item-indent">',
'suffix' => '</div>',
]
);
}
/**
* Builds the triggers configuration section.
*
* @param \Drupal\Core\Config\ImmutableConfig $config
* The current UtiliKit configuration.
*
* @return array
* Form elements for automatic update triggers in static mode.
*/
private function buildTriggersSection($config): array {
$description = $this->t('<div class="messages messages--info"><strong>How automatic updates work in Static & Head Modes:</strong><br>@enabled_text<br>@disabled_text<br><br><em>@note_text</em></div>', [
'@enabled_text' => '• Enabled: When you save content with new utility classes, they are automatically added to the generated CSS.',
'@disabled_text' => '• Disabled: When you save content, new classes are NOT added to the generated CSS. You must click the "Update UtiliKit" button to scan for and add them.',
'@note_text' => 'Note: This setting only applies to Static & Head Modes. In Inline Mode, all classes are processed dynamically without needing updates.',
]);
return [
'triggers' => [
'#type' => 'details',
'#title' => $this->t('Automatic CSS Updates (Static & Head Modes)'),
'#description' => $description,
'#open' => FALSE,
'#states' => [
'visible' => [
[':input[name="rendering_mode"]' => ['value' => 'static']],
[':input[name="rendering_mode"]' => ['value' => 'head']],
],
],
'update_on_node_save' => $this->createCheckboxField(
$this->t('Update when saving nodes'),
'',
(bool) $config->get('update_on_node_save') ?? FALSE
),
'update_on_block_save' => $this->createCheckboxField(
$this->t('Update when saving custom blocks'),
'',
(bool) $config->get('update_on_block_save') ?? FALSE
),
'update_on_paragraph_save' => $this->createCheckboxField(
$this->t('Update when saving paragraphs'),
'',
(bool) $config->get('update_on_paragraph_save') ?? FALSE
),
],
];
}
/**
* Builds the cache management section.
*
* @param \Drupal\Core\Config\ImmutableConfig $config
* The current UtiliKit configuration.
*
* @return array
* Form elements for cache management and test CSS generation.
*/
private function buildCacheSection($config): array {
$knownClasses = $this->serviceProvider->getStateManager()->getKnownClasses();
$cacheSection = [
'cache' => [
'#type' => 'details',
'#title' => $this->t('Cache Management'),
'#open' => FALSE,
'info' => [
'#markup' => '<p>' . $this->t('Currently tracking <strong>@count</strong> utility classes.', [
'@count' => count($knownClasses),
]) . '</p>',
],
'clear_css_cache' => $this->createSubmitButton(
$this->t('Clear All CSS & Reset'),
['::clearCssCache'],
['button--danger'],
['onclick' => $this->t('return confirm("This will remove all tracked CSS classes. Are you sure?");')]
),
],
];
// Check if utilikit_test module is enabled.
if ($this->moduleHandler->moduleExists('utilikit_test')) {
// Check if test CSS file exists FIRST.
$testCssFile = UtilikitConstants::CSS_DIRECTORY . '/utilikit-test-complete.css';
$testCssPath = $this->fileSystem->realpath($testCssFile);
$testFileExists = $testCssPath && file_exists($testCssPath);
// Only show delete button if file exists.
if ($testFileExists && $config->get('dev_mode')) {
$fileSize = filesize($testCssPath);
// Add file info and delete button.
$cacheSection['cache']['test_css_delete'] = [
'#type' => 'container',
'#states' => [
'visible' => [
':input[name="dev_mode"]' => ['checked' => TRUE],
],
],
'info' => [
'#markup' => $this->t('<p class="messages messages--warning">Test CSS file exists: utilikit-test-complete.css (@size)</p>', [
'@size' => (string) ByteSizeMarkup::create((int) $fileSize),
]),
],
'button' => $this->createSubmitButton(
$this->t('Delete Test CSS'),
['::deleteTestCss'],
['button--danger', 'button--small'],
['title' => $this->t('Delete the test CSS file')]
),
];
}
// Generate button (keep existing code)
$cacheSection['cache']['generate_test_css'] = $this->createSubmitButton(
$this->t('Generate Complete Test CSS'),
['::generateTestCss'],
['button--primary'],
['title' => $this->t('Generate a CSS file with all possible utility class combinations for testing')]
);
// Add state to only show in dev mode.
$cacheSection['cache']['generate_test_css']['#states'] = [
'visible' => [
':input[name="dev_mode"]' => ['checked' => TRUE],
],
];
}
elseif ($config->get('dev_mode')) {
// Show hint in dev mode if test module not enabled.
$cacheSection['cache']['test_module_hint'] = [
'#markup' => $this->t('<p class="description">Enable the UtiliKit Test module to generate complete test CSS.</p>'),
];
}
return $cacheSection;
}
/**
* Deletes the test CSS file.
*
* @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 function deleteTestCss(array &$form, FormStateInterface $form_state): void {
$testCssFile = UtilikitConstants::CSS_DIRECTORY . '/utilikit-test-complete.css';
$testCssPath = $this->fileSystem->realpath($testCssFile);
if ($testCssPath && file_exists($testCssPath)) {
try {
$this->fileSystem->delete($testCssFile);
$this->messenger()->addStatus($this->t('Test CSS file deleted successfully.'));
// Log the deletion using parent's logger.
$this->getLogger('utilikit')->notice('Test CSS file deleted by user.');
}
catch (\Exception $e) {
$this->messenger()->addError($this->t('Failed to delete test CSS file: @error', [
'@error' => $e->getMessage(),
]));
$this->getLogger('utilikit')->error('Failed to delete test CSS file: @error', [
'@error' => $e->getMessage(),
]);
}
}
else {
$this->messenger()->addWarning($this->t('Test CSS file not found.'));
}
$form_state->setRebuild(TRUE);
}
/**
* Generates complete test CSS file.
*
* @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 function generateTestCss(array &$form, FormStateInterface $form_state): void {
// Double-check module is enabled.
if (!$this->moduleHandler->moduleExists('utilikit_test')) {
$this->messenger()->addError($this->t('UtiliKit Test module must be enabled to generate test CSS.'));
$form_state->setRebuild(TRUE);
return;
}
try {
/** @var \Drupal\utilikit\UtilikitTestCssGeneratorInterface $generator */
$generator = $this->serviceProvider->getTestCssGenerator();
$result = $generator->generateCompleteTestCss();
if (!empty($result['success'])) {
$this->messenger()->addStatus($this->t('Generated complete test CSS with @count classes. File size: @size', [
'@count' => $result['count'],
'@size' => (string) ByteSizeMarkup::create((int) $result['size']),
]));
if (!empty($result['file_url'])) {
$this->messenger()->addStatus($this->t('<a href="@url" target="_blank">View Test CSS File</a>', [
'@url' => $result['file_url'],
]));
}
}
else {
$this->messenger()->addError($this->t('Failed to generate test CSS: @error', [
'@error' => $result['error'] ?? 'Unknown error',
]));
}
}
catch (\Exception $e) {
$this->messenger()->addError($this->t('Error generating test CSS: @error', [
'@error' => $e->getMessage(),
]));
}
$form_state->setRebuild(TRUE);
}
/**
* Builds the scanning scope section.
*/
private function buildScanningScopeSection($config): array {
$entity_types = $this->entityTypeManager->getDefinitions();
$options = [];
$counts = [];
foreach ($entity_types as $entity_type_id => $entity_type) {
if ($entity_type->entityClassImplements(FieldableEntityInterface::class)) {
$count = $this->getEntityCount($entity_type_id);
$options[$entity_type_id] = $entity_type->getLabel() . ' (' . number_format($count) . ' entities)';
$counts[$entity_type_id] = $count;
}
}
asort($options);
$total_selected = 0;
$selected_types = $config->get('scanning_entity_types') ?? ['node', 'block_content', 'paragraph'];
foreach ($selected_types as $type) {
if (isset($counts[$type])) {
$total_selected += $counts[$type];
}
}
return [
'scanning_scope' => [
'#type' => 'details',
'#title' => $this->t('Content Scanning Scope'),
'#description' => $this->t('Select which entity types to scan for UtiliKit utility classes. Scanning only relevant types improves performance during CSS generation.'),
'#open' => TRUE,
'scanning_entity_types' => [
'#type' => 'checkboxes',
'#title' => $this->t('Entity types to scan'),
'#options' => $options,
'#default_value' => $selected_types,
'#description' => $this->t('Total selected: @count entities', ['@count' => number_format($total_selected)]),
],
],
];
}
/**
* Gets entity count for an entity type.
*/
private function getEntityCount(string $entity_type_id): int {
try {
$storage = $this->entityTypeManager->getStorage($entity_type_id);
$query = $storage->getQuery()->accessCheck(FALSE);
return (int) $query->count()->execute();
}
catch (\Exception $e) {
return 0;
}
}
/**
* Builds the developer tools section.
*
* @param \Drupal\Core\Config\ImmutableConfig $config
* The current UtiliKit configuration.
*
* @return array
* Form elements for developer and debugging options.
*/
private function buildDeveloperSection($config): array {
$developerSection = [
'developer' => [
'#type' => 'details',
'#title' => $this->t('Developer Tools'),
'#open' => FALSE,
'dev_mode' => $this->createCheckboxField(
$this->t('Enable developer mode'),
$this->t('Output debug information to browser console.'),
(bool) $config->get('dev_mode') ?? FALSE
),
'admin_preview' => $this->createCheckboxField(
$this->t('Visual debugging'),
$this->t('Show outlines around UtiliKit elements for debugging.'),
(bool) $config->get('admin_preview') ?? FALSE
),
'show_page_errors' => $this->createCheckboxField(
$this->t('Display errors on page'),
$this->t('Show validation errors as Drupal messages (requires developer mode). <strong>Note:</strong> Only works in inline mode - static mode uses pre-generated CSS and does not validate classes in real-time.'),
(bool) $config->get('show_page_errors') ?? FALSE,
[
'states' => [
'visible' => [':input[name="dev_mode"]' => ['checked' => TRUE]],
'disabled' => [':input[name="dev_mode"]' => ['checked' => FALSE]],
],
]
),
'log_level' => $this->createSelectField(
$this->t('Console logging level'),
$this->t('Controls browser console output detail. <strong>Note:</strong> Most detailed logs are generated in inline mode during class processing. Static mode shows minimal logging since CSS is pre-generated.'),
[
'off' => $this->t('Off'),
'warnings' => $this->t('Warnings only'),
'detailed' => $this->t('Detailed (verbose)'),
],
$config->get('log_level') ?? 'warnings',
['states' => ['visible' => [':input[name="dev_mode"]' => ['checked' => TRUE]]]]
),
'static_css_info' => [
'#type' => 'fieldset',
'#title' => $this->t('Generated CSS (Static & Head Modes)'),
'#states' => [
'visible' => [
':input[name="dev_mode"]' => ['checked' => TRUE],
[
[':input[name="rendering_mode"]' => ['value' => 'static']],
[':input[name="rendering_mode"]' => ['value' => 'head']],
],
],
],
],
],
];
$fileManager = $this->serviceProvider->getFileManager();
$css_url = $fileManager->getStaticCssUrl();
$known_classes = $this->serviceProvider->getStateManager()->getKnownClasses();
$rendering_mode = $config->get('rendering_mode') ?? 'inline';
if ($rendering_mode === 'head') {
$css_timestamp = $this->serviceProvider->getStateManager()->getCssTimestamp();
$file_time = $css_timestamp
? $this->dateFormatter->format($css_timestamp, 'custom', 'j M Y - g:i A T')
: $this->t('Unknown');
$css = $this->serviceProvider->getStateManager()->getGeneratedCss();
$css_size = !empty($css) ? round(strlen($css) / 1024, 1) . ' KB' : '0 KB';
$developerSection['developer']['static_css_info']['file_details'] = [
'#markup' => $this->t('<div class="messages messages--status"><strong>CSS Generated (Head Mode):</strong><br>CSS is injected in page head on each request<br><small>Classes: @count | Size: @size | Last Updated: @time</small></div>', [
'@count' => count($known_classes),
'@size' => $css_size,
'@time' => $file_time,
]),
];
}
elseif ($css_url) {
$css_timestamp = $this->serviceProvider->getStateManager()->getCssTimestamp();
$file_time = $css_timestamp
? $this->dateFormatter->format($css_timestamp, 'custom', 'j M Y - g:i A T')
: $this->t('Unknown');
$developerSection['developer']['static_css_info']['file_details'] = [
'#markup' => $this->t('<div class="messages messages--status"><strong>CSS File Generated:</strong><br><a href="@css_url" target="_blank" rel="noopener">View Generated CSS File <span aria-hidden="true">↗</span></a><br><small>Classes: @count | Last Updated: @time</small></div>', [
'@css_url' => $css_url,
'@count' => count($known_classes),
'@time' => $file_time,
]),
];
}
else {
$developerSection['developer']['static_css_info']['no_file'] = [
'#markup' => '<div class="messages messages--warning">' .
$this->t('No CSS file found. Save this form to generate the CSS file, or add UtiliKit classes to your content.') .
'</div>',
];
}
$developerSection['developer']['static_css_info']['regenerate_note'] = [
'#markup' => '<p><small>' .
$this->t('To regenerate the CSS file, use the "Clear All CSS & Reset" button in the Cache Management section above, then save this form.') .
'</small></p>',
];
if (isset($developerSection['developer']['static_css_info'])) {
$developerSection['developer']['static_css_info']['#cache'] = [
'max-age' => 0,
'contexts' => ['user'],
'tags' => [UtilikitConstants::CACHE_TAG_CSS],
];
}
return $developerSection;
}
/**
* Gets available breakpoint options for configuration.
*
* @return array
* Array of breakpoint options with labels.
*/
private function getBreakpointOptions(): array {
return [
'sm' => $this->t('Small (≥576px)'),
'md' => $this->t('Medium (≥768px)'),
'lg' => $this->t('Large (≥992px)'),
'xl' => $this->t('Extra Large (≥1200px)'),
'xxl' => $this->t('Extra Extra Large (≥1400px)'),
];
}
/**
* {@inheritdoc}
*/
public function validateForm(array &$form, FormStateInterface $form_state): void {
parent::validateForm($form, $form_state);
$this->validateDeveloperModeSettings($form_state);
}
/**
* Validates developer mode related settings.
*
* @param \Drupal\Core\Form\FormStateInterface $form_state
* The current state of the form.
*/
private function validateDeveloperModeSettings(FormStateInterface $form_state): void {
$devMode = $form_state->getValue('dev_mode');
$showPageErrors = $form_state->getValue('show_page_errors');
if ($showPageErrors && !$devMode) {
$this->messenger()->addWarning($this->t('Show page errors requires developer mode to be enabled.'));
}
}
/**
* {@inheritdoc}
*/
public function submitForm(array &$form, FormStateInterface $form_state): void {
$oldMode = $this->config('utilikit.settings')->get('rendering_mode') ?? 'inline';
$oldUseImportant = $this->config('utilikit.settings')->get('use_important') ?? TRUE;
$newMode = $form_state->getValue('rendering_mode');
$newUseImportant = $form_state->getValue('use_important');
$this->saveAllSettings($form_state);
$this->handleModeSwitch($oldMode, $newMode);
// Regenerate CSS if use_important setting changed.
if ($oldUseImportant !== $newUseImportant) {
$this->serviceProvider->regenerateStaticCss();
}
$this->serviceProvider->getCacheManager()->clearAllCaches();
parent::submitForm($form, $form_state);
}
/**
* Saves all form settings to configuration.
*
* @param \Drupal\Core\Form\FormStateInterface $form_state
* The current state of the form.
*/
private function saveAllSettings(FormStateInterface $form_state): void {
$settingsMap = [
'dev_mode', 'show_page_errors', 'enable_transitions', 'log_level',
'admin_preview', 'rendering_mode', 'scope_global', 'disable_admin',
'scope_content_types', 'update_on_node_save', 'update_on_block_save',
'update_on_paragraph_save', 'optimize_css', 'use_important',
];
$config = $this->config('utilikit.settings');
foreach ($settingsMap as $setting) {
$config->set($setting, $form_state->getValue($setting));
}
// Validate debounce range (0-1000ms)
$debounce = (int) $form_state->getValue('debounce');
$debounce = max(0, min(1000, $debounce));
$config->set('debounce', $debounce);
// Validate breakpoints against allowed values.
$breakpoints = array_filter($form_state->getValue('active_breakpoints'));
$validBreakpoints = ['sm', 'md', 'lg', 'xl', 'xxl'];
$breakpoints = array_intersect($breakpoints, $validBreakpoints);
$config->set('active_breakpoints', $breakpoints);
$config->set('scanning_entity_types', array_values(array_filter($form_state->getValue('scanning_entity_types'))));
$config->set('enabled_content_types', array_filter($form_state->getValue('enabled_content_types')));
$config->save();
}
/**
* Handles rendering mode switches between inline and static modes.
*
* @param string $oldMode
* The previous rendering mode.
* @param string $newMode
* The new rendering mode.
*/
private function handleModeSwitch(string $oldMode, string $newMode): void {
if ($oldMode !== $newMode) {
if ($newMode === 'static') {
$this->switchToStaticMode();
}
elseif ($newMode === 'head') {
$this->switchToHeadMode();
}
else {
$this->switchToInlineMode();
}
}
elseif ($newMode === 'static') {
$this->ensureStaticCssExists();
}
}
/**
* Switches to static rendering mode and generates CSS.
*/
private function switchToStaticMode(): void {
$result = $this->serviceProvider->switchToStaticMode();
if ($result['classes_count'] > 0) {
$this->messenger()->addStatus($this->t('Switched to static mode. Generated CSS for @count classes from @scanned entities.', [
'@count' => $result['classes_count'],
'@scanned' => $result['scanned_count'],
]));
}
else {
$this->messenger()->addStatus($this->t('Switched to static mode. No existing classes found in @scanned entities - CSS will be generated as you add utility classes.', [
'@scanned' => $result['scanned_count'],
]));
}
$this->messenger()->addStatus($this->t('All caches cleared. Static mode is now active.'));
}
/**
* Switches to inline rendering mode and cleans up static files.
*/
private function switchToInlineMode(): void {
$classCount = $this->serviceProvider->switchToInlineMode();
$this->messenger()->addStatus($this->t('Switched to inline mode. Dynamic processing enabled. Tracking @count classes.', [
'@count' => $classCount,
]));
$this->messenger()->addStatus($this->t('All caches cleared. Inline mode is now active.'));
}
/**
* Switches to head rendering mode and ensures classes are scanned.
*/
private function switchToHeadMode(): void {
$this->serviceProvider->getFileManager()->cleanupStaticFiles();
$stateManager = $this->serviceProvider->getStateManager();
$knownClasses = $stateManager->getKnownClasses();
if (empty($knownClasses)) {
$scanResult = $this->serviceProvider->getContentScanner()->scanAllContent();
$knownClasses = $scanResult['classes'];
if (!empty($knownClasses)) {
$stateManager->setKnownClasses($knownClasses);
$this->messenger()->addStatus($this->t('Scanned content and found @count utility classes.', [
'@count' => count($knownClasses),
]));
}
}
$this->serviceProvider->getCacheManager()->clearAllCaches();
$this->messenger()->addStatus($this->t('Switched to head mode. CSS will be generated in page head. Tracking @count classes.', [
'@count' => count($knownClasses),
]));
}
/**
* Ensures static CSS file exists when in static mode.
*/
private function ensureStaticCssExists(): void {
$css_url = $this->serviceProvider->getFileManager()->getStaticCssUrl();
if (!$css_url) {
$result = $this->serviceProvider->regenerateStaticCss();
if ($result) {
$knownClasses = $this->serviceProvider->getStateManager()->getKnownClasses();
$this->messenger()->addStatus($this->t('Regenerated static CSS file with @count classes.', [
'@count' => count($knownClasses),
]));
}
}
}
/**
* Form submission handler for clearing CSS cache and resetting everything.
*
* @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 function clearCssCache(array &$form, FormStateInterface $form_state): void {
// Clear all UtiliKit data.
$this->serviceProvider->getStateManager()->clearUtiliKitData();
// Clean up all CSS files (static and test).
$this->serviceProvider->getFileManager()->cleanupStaticFiles();
// Also clean test CSS if exists.
$testCssFile = UtilikitConstants::CSS_DIRECTORY . '/utilikit-test-complete.css';
if (file_exists($testCssFile)) {
try {
$this->fileSystem->delete($testCssFile);
}
catch (\Exception $e) {
$this->getLogger('utilikit')->warning('Could not delete test CSS file: @error', [
'@error' => $e->getMessage(),
]);
}
}
// Reset ALL configuration to defaults.
$config = $this->config('utilikit.settings');
// Delete entire config.
$config->delete();
// Clear all relevant Drupal caches.
$this->serviceProvider->getCacheManager()->clearAllCaches();
// Add success message.
$this->messenger()->addStatus($this->t('All UtiliKit settings, caches, and files have been reset to defaults. Starting completely fresh.'));
// Force redirect with query parameter to ensure fresh page load.
$form_state->setRedirect('utilikit.settings', [], ['query' => ['reset' => time()]]);
}
}
