diff --git a/config/install/user.role.az_content_admin.yml b/config/install/user.role.az_content_admin.yml index 28d2f44328..7b021cbd54 100644 --- a/config/install/user.role.az_content_admin.yml +++ b/config/install/user.role.az_content_admin.yml @@ -16,6 +16,7 @@ permissions: - 'administer blocks' - 'administer book outlines' - 'administer easy breadcrumb' + - 'administer az_finder settings' - 'administer flaggings' - 'administer flags' - 'administer languages' diff --git a/modules/custom/az_demo/config/install/views.view.az_finder.yml b/modules/custom/az_demo/config/install/views.view.az_finder.yml index 6572cd42ee..08a7cfdc45 100644 --- a/modules/custom/az_demo/config/install/views.view.az_finder.yml +++ b/modules/custom/az_demo/config/install/views.view.az_finder.yml @@ -580,15 +580,20 @@ display: advanced: placeholder_text: '' collapsible: false + collapsible_disable_automatic_open: false is_secondary: false field_az_event_category_target_id: plugin_id: az_finder_tid_widget advanced: sort_options: false + placeholder_text: '' rewrite: filter_rewrite_values: '' + filter_rewrite_values_key: false collapsible: false + collapsible_disable_automatic_open: false is_secondary: false + default_states: { } reset_button_position: top reset_button_counter: true orientation: vertical @@ -859,27 +864,34 @@ display: advanced: placeholder_text: '' collapsible: false + collapsible_disable_automatic_open: false is_secondary: false field_az_byline_value: plugin_id: default advanced: placeholder_text: '' collapsible: true + collapsible_disable_automatic_open: false is_secondary: false field_az_published_value: plugin_id: bef_datepicker advanced: placeholder_text: '' collapsible: false + collapsible_disable_automatic_open: false is_secondary: false field_az_news_tags_target_id: plugin_id: az_finder_tid_widget advanced: sort_options: false + placeholder_text: '' rewrite: filter_rewrite_values: '' + filter_rewrite_values_key: false collapsible: false + collapsible_disable_automatic_open: false is_secondary: false + default_states: { } reset_button_position: top reset_button_counter: true orientation: vertical @@ -1263,15 +1275,20 @@ display: advanced: placeholder_text: '' collapsible: false + collapsible_disable_automatic_open: false is_secondary: false field_az_page_category_target_id: plugin_id: az_finder_tid_widget advanced: - sort_options: true + sort_options: false + placeholder_text: '' rewrite: filter_rewrite_values: '' + filter_rewrite_values_key: false collapsible: false + collapsible_disable_automatic_open: false is_secondary: false + default_states: { } reset_button_position: top reset_button_counter: true orientation: vertical @@ -1548,23 +1565,32 @@ display: advanced: placeholder_text: '' collapsible: false + collapsible_disable_automatic_open: false is_secondary: false field_az_person_category_target_id: plugin_id: az_finder_tid_widget advanced: sort_options: false + placeholder_text: '' rewrite: filter_rewrite_values: '' + filter_rewrite_values_key: false collapsible: false + collapsible_disable_automatic_open: false is_secondary: false + default_states: { } field_az_person_category_sec_target_id: plugin_id: az_finder_tid_widget advanced: sort_options: false + placeholder_text: '' rewrite: filter_rewrite_values: '' + filter_rewrite_values_key: false collapsible: false + collapsible_disable_automatic_open: false is_secondary: false + default_states: { } reset_button_position: top reset_button_counter: true orientation: vertical diff --git a/modules/custom/az_finder/az_finder.info.yml b/modules/custom/az_finder/az_finder.info.yml index 2f328ea3b2..861608f908 100644 --- a/modules/custom/az_finder/az_finder.info.yml +++ b/modules/custom/az_finder/az_finder.info.yml @@ -5,3 +5,4 @@ core_version_requirement: ^9 || ^10 package: 'The University of Arizona - Experimental' dependencies: - better_exposed_filters:better_exposed_filters +configure: az_finder.settings diff --git a/modules/custom/az_finder/az_finder.links.task.yml b/modules/custom/az_finder/az_finder.links.task.yml new file mode 100644 index 0000000000..6a14041863 --- /dev/null +++ b/modules/custom/az_finder/az_finder.links.task.yml @@ -0,0 +1,5 @@ +az_finder.settings_tab: + title: 'AZ Finder' + route_name: az_finder.settings + base_route: az_core.az_settings + weight: 0 diff --git a/modules/custom/az_finder/az_finder.permissions.yml b/modules/custom/az_finder/az_finder.permissions.yml new file mode 100644 index 0000000000..9105301c77 --- /dev/null +++ b/modules/custom/az_finder/az_finder.permissions.yml @@ -0,0 +1,3 @@ +administer az_finder settings: + title: 'Administer Quickstart Finder Settings' + description: 'Allows users to manage Quickstart Finder settings.' diff --git a/modules/custom/az_finder/az_finder.routing.yml b/modules/custom/az_finder/az_finder.routing.yml new file mode 100644 index 0000000000..c532be736b --- /dev/null +++ b/modules/custom/az_finder/az_finder.routing.yml @@ -0,0 +1,7 @@ +az_finder.settings: + path: '/admin/config/az-quickstart/settings/az-finder' + defaults: + _form: '\Drupal\az_finder\Form\AZFinderSettingsForm' + _title: 'AZ Finder Settings' + requirements: + _permission: 'administer az_finder settings' diff --git a/modules/custom/az_finder/az_finder.services.yml b/modules/custom/az_finder/az_finder.services.yml index 1c1d021ed5..01e9347a70 100644 --- a/modules/custom/az_finder/az_finder.services.yml +++ b/modules/custom/az_finder/az_finder.services.yml @@ -2,4 +2,21 @@ services: _defaults: autoconfigure: true az_finder.icons: - class: Drupal\az_finder\AZFinderIcons + class: Drupal\az_finder\Service\AZFinderIcons + az_finder.view_options: + class: Drupal\az_finder\Service\AZFinderViewOptions + arguments: + - '@cache.default' + - '@entity_type.manager' + az_finder.vocabulary: + class: Drupal\az_finder\Service\AZFinderVocabulary + arguments: + - '@entity_type.manager' + - '@string_translation' + az_finder.overrides: + class: Drupal\az_finder\Service\AZFinderOverrides + arguments: + - '@config.factory' + logger.channel.az_finder: + parent: logger.channel_base + arguments: ['az_finder'] diff --git a/modules/custom/az_finder/config/install/az_finder.settings.yml b/modules/custom/az_finder/config/install/az_finder.settings.yml new file mode 100644 index 0000000000..2eec6d7d34 --- /dev/null +++ b/modules/custom/az_finder/config/install/az_finder.settings.yml @@ -0,0 +1,2 @@ +tid_widget: + default_state: 'expand' diff --git a/modules/custom/az_finder/config/quickstart/views.view.az_page_by_category.yml b/modules/custom/az_finder/config/quickstart/views.view.az_page_by_category.yml index 4d6e1fa4f2..029f13a6ba 100644 --- a/modules/custom/az_finder/config/quickstart/views.view.az_page_by_category.yml +++ b/modules/custom/az_finder/config/quickstart/views.view.az_page_by_category.yml @@ -97,6 +97,7 @@ display: type: full options: offset: 0 + pagination_heading_level: h4 items_per_page: 50 total_pages: null id: 0 @@ -143,15 +144,20 @@ display: advanced: placeholder_text: '' collapsible: false + collapsible_disable_automatic_open: false is_secondary: false tid: plugin_id: az_finder_tid_widget advanced: sort_options: false + placeholder_text: '' rewrite: filter_rewrite_values: '' + filter_rewrite_values_key: false collapsible: false + collapsible_disable_automatic_open: false is_secondary: false + default_states: { } reset_button_position: top reset_button_counter: true orientation: vertical @@ -407,6 +413,55 @@ display: display_plugin: page position: 2 display_options: + exposed_form: + type: az_better_exposed_filters + options: + submit_button: Apply + reset_button: true + reset_button_label: 'Reset filters' + exposed_sorts_label: 'Sort by' + expose_sort_order: true + sort_asc_label: Asc + sort_desc_label: Desc + text_input_required: 'Select any filter and click on Apply to see results' + text_input_required_format: az_standard + bef: + general: + autosubmit: true + autosubmit_exclude_textfield: false + autosubmit_textfield_delay: 500 + autosubmit_hide: true + input_required: false + allow_secondary: false + secondary_label: 'Advanced options' + secondary_open: false + reset_button_always_show: false + filter: + title: + plugin_id: default + advanced: + placeholder_text: '' + collapsible: false + collapsible_disable_automatic_open: false + is_secondary: false + tid: + plugin_id: az_finder_tid_widget + advanced: + sort_options: false + placeholder_text: '' + rewrite: + filter_rewrite_values: '' + filter_rewrite_values_key: false + collapsible: false + collapsible_disable_automatic_open: false + is_secondary: false + default_states: { } + reset_button_position: top + reset_button_counter: true + orientation: vertical + skip_link: true + skip_link_text: 'Skip to search and filter' + skip_link_id: search-filter style: type: views_bootstrap_grid options: @@ -425,6 +480,7 @@ display: relationship: none view_mode: az_card defaults: + exposed_form: false style: false style_options: false row: false @@ -450,6 +506,55 @@ display: display_plugin: page position: 1 display_options: + exposed_form: + type: az_better_exposed_filters + options: + submit_button: Apply + reset_button: true + reset_button_label: 'Reset filters' + exposed_sorts_label: 'Sort by' + expose_sort_order: true + sort_asc_label: Asc + sort_desc_label: Desc + text_input_required: 'Select any filter and click on Apply to see results' + text_input_required_format: az_standard + bef: + general: + autosubmit: true + autosubmit_exclude_textfield: false + autosubmit_textfield_delay: 500 + autosubmit_hide: true + input_required: false + allow_secondary: false + secondary_label: 'Advanced options' + secondary_open: false + reset_button_always_show: false + filter: + title: + plugin_id: default + advanced: + placeholder_text: '' + collapsible: false + collapsible_disable_automatic_open: false + is_secondary: false + tid: + plugin_id: az_finder_tid_widget + advanced: + sort_options: false + placeholder_text: '' + rewrite: + filter_rewrite_values: '' + filter_rewrite_values_key: false + collapsible: false + collapsible_disable_automatic_open: false + is_secondary: false + default_states: { } + reset_button_position: top + reset_button_counter: true + orientation: vertical + skip_link: true + skip_link_text: 'Skip to search and filter' + skip_link_id: search-filter style: type: default options: @@ -463,6 +568,7 @@ display: view_mode: az_row defaults: css_class: false + exposed_form: false style: false style_options: false row: false diff --git a/modules/custom/az_finder/config/schema/az_finder.schema.yml b/modules/custom/az_finder/config/schema/az_finder.schema.yml new file mode 100644 index 0000000000..66debc9c3c --- /dev/null +++ b/modules/custom/az_finder/config/schema/az_finder.schema.yml @@ -0,0 +1,69 @@ +az_finder.settings: + type: config_object + label: 'AZ Finder Settings' + mapping: + tid_widget: + type: mapping + label: 'Global Default Settings' + mapping: + default_state: + type: string + label: 'Global Default State for Exposed Filters' + constraints: + Choice: + - 'expand' + - 'collapse' + +az_finder.tid_widget.[view_id].[display_id]: + type: config_object + label: 'AZ Finder Override for a specific View Display' + mapping: + vocabularies: + type: sequence + label: 'Vocabulary settings for this view display' + sequence: + type: mapping + mapping: + vocabulary_id: + type: string + label: 'Vocabulary ID' + terms: + type: sequence + label: 'Settings for each term in the vocabulary' + sequence: + type: mapping + mapping: + term_id: + type: string + label: 'Term ID' + default_state: + type: string + label: 'Setting for each term' + constraints: + Required: false + Choice: + - 'expand' + - 'collapse' + +views.exposed_form.az_better_exposed_filters: + type: views.exposed_form.bef + label: 'Quickstart Exposed Filters' + mapping: + reset_button_position: + type: string + label: 'Reset Button Position' + reset_button_counter: + type: boolean + label: 'Show Active Filter Counter' + orientation: + type: string + label: 'Orientation' + skip_link: + type: boolean + label: 'Add Skip Link' + skip_link_text: + type: string + label: 'Skip Link Text' + skip_link_id: + type: string + label: 'Skip Link ID' diff --git a/modules/custom/az_finder/config/schema/views.exposed_form.az_better_exposed_filters.yml b/modules/custom/az_finder/config/schema/views.exposed_form.az_better_exposed_filters.yml deleted file mode 100644 index 0a306bfb5c..0000000000 --- a/modules/custom/az_finder/config/schema/views.exposed_form.az_better_exposed_filters.yml +++ /dev/null @@ -1,22 +0,0 @@ -views.exposed_form.az_better_exposed_filters: - type: views.exposed_form.bef - label: 'Quickstart Exposed Filters' - mapping: - reset_button_position: - type: string - label: 'Reset Button Position' - reset_button_counter: - type: boolean - label: 'Show Active Filter Counter' - orientation: - type: string - label: 'Orientation' - skip_link: - type: boolean - label: 'Add Skip Link' - skip_link_text: - type: string - label: 'Skip Link Text' - skip_link_id: - type: string - label: 'Skip Link ID' diff --git a/modules/custom/az_finder/css/taxonomy-index-tid-widget.css b/modules/custom/az_finder/css/taxonomy-index-tid-widget.css index 9ed766ec74..2c7f7c8d14 100644 --- a/modules/custom/az_finder/css/taxonomy-index-tid-widget.css +++ b/modules/custom/az_finder/css/taxonomy-index-tid-widget.css @@ -97,4 +97,4 @@ .js-bef-filter-count { margin-left: 5px; -} \ No newline at end of file +} diff --git a/modules/custom/az_finder/src/Form/AZFinderSettingsForm.php b/modules/custom/az_finder/src/Form/AZFinderSettingsForm.php new file mode 100644 index 0000000000..57ac589ee3 --- /dev/null +++ b/modules/custom/az_finder/src/Form/AZFinderSettingsForm.php @@ -0,0 +1,394 @@ +typedConfigManager = $typed_config_manager; + $this->azFinderViewOptions = $az_finder_view_options; + $this->azFinderVocabulary = $az_finder_vocabulary; + $this->azFinderOverrides = $az_finder_overrides; + $this->entityTypeManager = $entity_type_manager; + } + + /** + * {@inheritdoc} + */ + public static function create(ContainerInterface $container) { + return new static( + $container->get('config.factory'), + $container->get('config.typed'), + $container->get('az_finder.view_options'), + $container->get('az_finder.vocabulary'), + $container->get('az_finder.overrides'), + $container->get('entity_type.manager') + ); + } + + /** + * {@inheritdoc} + */ + public function getFormId() { + return 'az_finder_settings'; + } + + /** + * {@inheritdoc} + */ + protected function getEditableConfigNames() { + return ['az_finder.settings']; + } + + /** + * {@inheritdoc} + */ + public function buildForm(array $form, FormStateInterface $form_state): array { + $form['#tree'] = TRUE; + + // Page description. + $form['description'] = [ + '#type' => 'item', + '#markup' => $this->t(' +

Manage the settings you would like to use with exposed AZ Finder forms.

+

For more information about the Quickstart Finder, please visit the Quickstart website.

+ '), + ]; + + // How these settings work section. + $form['how_it_works'] = [ + '#type' => 'details', + '#title' => $this->t('How do these settings work?'), + '#open' => FALSE, + '#description' => $this->t(' +

The default settings are applied to all taxonomy vocabularies as a starting point.

+

Each Finder view display can have custom overrides to expand or collapse specific sections by default.

+ '), + ]; + + // Filter Widget Settings section. + $form['az_finder_tid_widget'] = [ + '#type' => 'details', + '#title' => $this->t('Filter Widget Settings'), + '#open' => TRUE, + ]; + + // Default state select field. + $form['az_finder_tid_widget']['default_state'] = [ + '#type' => 'select', + '#title' => $this->t('Default Display of Parent Terms'), + '#description' => $this->t('Choose how taxonomy terms with children should behave by default everywhere.
These settings are not context aware, so if you choose collapsed, your term must be using a collapsible element for this to work.'), + '#weight' => -2, + '#options' => [ + 'expand' => $this->t('Expanded'), + 'collapse' => $this->t('Collapsed'), + ], + '#config_target' => 'az_finder.settings:tid_widget.default_state', + ]; + + // Fetch existing overrides from the AZFinderOverrides service. + $config_overrides = $this->azFinderOverrides->getExistingOverrides(); + + // Get current overrides from form state. + $session_overrides = $form_state->getValue(['az_finder_tid_widget', 'overrides']) ?? []; + + // Normalize session overrides structure if needed. + $normalized_session_overrides = []; + foreach ($session_overrides as $key => $override) { + // Filter out non-override form elements. + if (empty($override['view_id'])) { + continue; + } + $normalized_session_overrides[$key] = $override; + } + + // Combine overrides. Session overrides will not overwrite existing ones. + $overrides = $config_overrides + $normalized_session_overrides; + + $form['az_finder_tid_widget']['overrides'] = [ + '#type' => 'container', + '#prefix' => '
', + '#suffix' => '
', + ]; + + $form['az_finder_tid_widget']['overrides']['select_view_display_container'] = [ + '#type' => 'container', + 'select_view_display' => [ + '#type' => 'select', + '#title' => $this->t('Add an Override'), + '#description' => $this->t('Select a particular filter widget to override the default display for each taxonomy term.'), + '#weight' => -1, + '#options' => $this->azFinderViewOptions->getViewOptions(), + '#empty_option' => $this->t('- Select -'), + '#attributes' => [ + 'id' => 'js-az-select-view-display', + ], + ], + 'override' => [ + '#type' => 'submit', + '#value' => $this->t('Add Override'), + '#ajax' => [ + 'callback' => '::ajaxAddOverride', + 'wrapper' => 'js-overrides-container', + 'effect' => 'fade', + ], + '#submit' => ['::submitOverride'], + '#attributes' => [ + 'class' => [ + 'button', + 'button--primary', + 'button--small', + ], + ], + '#states' => [ + 'disabled' => [ + ':input[name="az_finder_tid_widget[overrides][select_view_display_container][select_view_display]"]' => ['value' => ''], + ], + ], + ], + ]; + + // Add tooltip if we have overrides. + if (!empty($overrides)) { + $form['az_finder_tid_widget']['overrides']['configure_overrides'] = [ + '#type' => 'item', + '#title' => $this->t('Configure Added Overrides'), + ]; + } + // Add override sections. + foreach ($overrides as $override) { + $this->addOverrideSection($form, $form_state, $override); + } + + // Save combined overrides to the form state. + $form_state->setValue(['az_finder_tid_widget', 'overrides'], $overrides); + return parent::buildForm($form, $form_state); + } + + /** + * Separate submit handler for the override button. + * + * @param array $form + * The form array. + * @param \Drupal\Core\Form\FormStateInterface $form_state + * The form state object. + */ + public function submitOverride(array &$form, FormStateInterface $form_state) { + // Retrieve selected view and display. + $selected_view_display = $form_state->getValue([ + 'az_finder_tid_widget', + 'overrides', + 'select_view_display_container', + 'select_view_display', + ]); + [$view_id, $display_id] = explode(':', $selected_view_display); + + // Prepare the configuration key. + $config_name = "az_finder.tid_widget.$view_id.$display_id"; + + // Initialize or load existing configuration. + $config = $this->configFactory->getEditable($config_name); + + // Save the configuration. + $config->save(); + + // Create the override for form state. + $override = [ + 'view_id' => $view_id, + 'display_id' => $display_id, + ]; + + // Ensure the overrides array is present in the form state. + $overrides = $form_state->getValue(['az_finder_tid_widget', 'overrides']) ?? []; + // Update the overrides with the new override. + $overrides["$view_id:$display_id"] = $override; + $form_state->setValue(['az_finder_tid_widget', 'overrides'], $overrides); + $form_state->setRebuild(TRUE); + + // Optionally, provide feedback or perform additional actions. + $this->messenger()->addMessage($this->t('Override created for @view_display.', [ + '@view_display' => $form_state->getCompleteForm()['az_finder_tid_widget']['overrides']['select_view_display_container']['select_view_display']['#options'][$selected_view_display], + ])); + } + + /** + * Ajax callback for the override button. + * + * @param array $form + * The form array. + * @param \Drupal\Core\Form\FormStateInterface $form_state + * The form state object. + * + * @return array + * The updated overrides container. + */ + public function ajaxAddOverride(array &$form, FormStateInterface $form_state): array { + // Return the updated overrides container. + return $form['az_finder_tid_widget']['overrides']; + } + + /** + * Add an override section to the form. + * + * @param array $form + * The form array. + * @param \Drupal\Core\Form\FormStateInterface $form_state + * The form state object. + * @param array $override + * The override data. + * + * @return ?array + * Return the override section or null. + */ + public function addOverrideSection(array &$form, FormStateInterface $form_state, array $override): ?array { + $key = "{$override['view_id']}:{$override['display_id']}"; + $view_id = $override['view_id']; + $display_id = $override['display_id']; + if ($key !== ':' && !isset($form['az_finder_tid_widget']['overrides'][$key])) { + $form['az_finder_tid_widget']['overrides'][$key] = [ + '#type' => 'details', + '#title' => $this->t("Override Settings for :view_label (:display_title)", [ + ":view_label" => $override['view_label'], + ":display_title" => $override['display_title'], + ]), + '#open' => FALSE, + '#description' => $this->t('Overrides are grouped by taxonomy vocabulary. Each vocabulary can have its own settings for how filter widgets behave when they have child terms.'), + '#tree' => TRUE, + ]; + $form['az_finder_tid_widget']['overrides'][$key]['delete'] = [ + '#type' => 'submit', + '#value' => $this->t('Delete'), + '#ajax' => [ + 'callback' => '::ajaxDeleteOverride', + 'wrapper' => 'js-overrides-container', + 'effect' => 'fade', + ], + '#name' => 'delete-' . $key, + '#submit' => ['::submitDeleteOverride'], + '#attributes' => [ + 'class' => ['button--small'], + ], + ]; + + $config_name = "az_finder.tid_widget.{$view_id}.{$display_id}"; + $config = $this->config($config_name); + $vocabulary_ids = $this->azFinderVocabulary->getVocabularyIdsForFilter($view_id, $display_id, 'taxonomy_index_tid'); + + foreach ($vocabulary_ids as $vocabulary_id) { + $this->azFinderVocabulary->addTermsTable( + $form['az_finder_tid_widget']['overrides'][$key]['vocabularies'][$vocabulary_id], + $vocabulary_id, + $view_id, + $display_id + ); + } + } + + $overrides = $form_state->getValue(['az_finder_tid_widget', 'overrides']) ?? []; + $form_state->setValue(['az_finder_tid_widget', 'overrides'], $overrides); + return $form['az_finder_tid_widget']['overrides'][$key]; + } + + /** + * Ajax callback for the delete button. + */ + public function ajaxDeleteOverride(array &$form, FormStateInterface $form_state) { + // Return the updated overrides container. + return $form['az_finder_tid_widget']['overrides']; + } + + /** + * Separate submit handler for the delete button. + * + * @param array $form + * The form array. + * @param \Drupal\Core\Form\FormStateInterface $form_state + * The form state object. + */ + public function submitDeleteOverride(array &$form, FormStateInterface $form_state) { + $triggering_element = $form_state->getTriggeringElement(); + $button_name = $triggering_element['#name']; + $key = str_replace('delete-', '', $button_name); + [$view_id, $display_id] = explode(':', $key); + $config_name = "az_finder.tid_widget.$view_id.$display_id"; + $config = $this->config($config_name); + if ($config) { + $editable_config = $this->configFactory->getEditable($config_name); + $editable_config->delete(); + } + // Update the overrides in form state. + $overrides = $form_state->getValue(['az_finder_tid_widget', 'overrides']) ?? []; + unset($overrides[$key]); + $form_state->setValue(['az_finder_tid_widget', 'overrides'], $overrides); + $form_state->setRebuild(TRUE); + } + +} diff --git a/modules/custom/az_finder/src/Plugin/better_exposed_filters/filter/AZFinderTaxonomyIndexTidWidget.php b/modules/custom/az_finder/src/Plugin/better_exposed_filters/filter/AZFinderTaxonomyIndexTidWidget.php index f195912624..2827f32138 100644 --- a/modules/custom/az_finder/src/Plugin/better_exposed_filters/filter/AZFinderTaxonomyIndexTidWidget.php +++ b/modules/custom/az_finder/src/Plugin/better_exposed_filters/filter/AZFinderTaxonomyIndexTidWidget.php @@ -4,9 +4,10 @@ namespace Drupal\az_finder\Plugin\better_exposed_filters\filter; -use Drupal\az_finder\AZFinderIcons; +use Drupal\az_finder\Service\AZFinderIcons; use Drupal\better_exposed_filters\BetterExposedFiltersHelper; use Drupal\better_exposed_filters\Plugin\better_exposed_filters\filter\FilterWidgetBase; +use Drupal\Core\Config\ConfigFactoryInterface; use Drupal\Core\Entity\EntityTypeManagerInterface; use Drupal\Core\Form\FormStateInterface; use Drupal\Core\Plugin\ContainerFactoryPluginInterface; @@ -16,6 +17,7 @@ use Drupal\Core\Template\Attribute; use Drupal\taxonomy\Plugin\views\filter\TaxonomyIndexTid; use Drupal\views\ViewExecutable; +use Psr\Log\LoggerInterface; use Symfony\Component\DependencyInjection\ContainerInterface; /** @@ -54,10 +56,31 @@ class AZFinderTaxonomyIndexTidWidget extends FilterWidgetBase implements Contain /** * The AZFinderIcons service. * - * @var \Drupal\az_finder\AZFinderIcons + * @var \Drupal\az_finder\Service\AZFinderIcons */ protected $azFinderIcons; + /** + * The config factory service. + * + * @var \Drupal\Core\Config\ConfigFactoryInterface + */ + protected $configFactory; + + /** + * The logger service. + * + * @var \Psr\Log\LoggerInterface + */ + protected $logger; + + /** + * The override settings. + * + * @var array + */ + protected static $overrides = []; + /** * Constructs a new AzFinderWidget object. * @@ -71,8 +94,12 @@ class AZFinderTaxonomyIndexTidWidget extends FilterWidgetBase implements Contain * The renderer service. * @param \Drupal\Core\Entity\EntityTypeManagerInterface $entity_type_manager * The entity type manager service. - * @param \Drupal\az_finder\AZFinderIcons $az_finder_icons + * @param \Drupal\az_finder\Service\AZFinderIcons $az_finder_icons * The AZFinderIcons service. + * @param \Drupal\Core\Config\ConfigFactoryInterface $config_factory + * The config factory service. + * @param \Psr\Log\LoggerInterface $logger + * The logger service. */ public function __construct( array $configuration, @@ -81,12 +108,16 @@ public function __construct( RendererInterface $renderer, EntityTypeManagerInterface $entity_type_manager, AZFinderIcons $az_finder_icons, + ConfigFactoryInterface $config_factory, + LoggerInterface $logger, ) { $configuration += $this->defaultConfiguration(); parent::__construct($configuration, $plugin_id, $plugin_definition); $this->renderer = $renderer; $this->entityTypeManager = $entity_type_manager; $this->azFinderIcons = $az_finder_icons; + $this->configFactory = $config_factory; + $this->logger = $logger; } /** @@ -104,10 +135,21 @@ public static function create( $plugin_definition, $container->get('renderer'), $container->get('entity_type.manager'), - $container->get('az_finder.icons') + $container->get('az_finder.icons'), + $container->get('config.factory'), + $container->get('logger.channel.az_finder') ); } + /** + * {@inheritdoc} + */ + public function defaultConfiguration() { + return [ + 'default_states' => [], + ] + parent::defaultConfiguration(); + } + /** * {@inheritdoc} */ @@ -192,10 +234,10 @@ public function exposedFormAlter(array &$form, FormStateInterface $form_state): } /** - * Returns the field ID for the filter. + * Returns the field ID for a views filter. * - * @param object $filter - * The filter object. + * @param \Drupal\views\Plugin\views\filter\FilterPluginBase $filter + * A views filter plugin object. * * @return string * The field ID. @@ -227,12 +269,10 @@ protected function setFormOptions(array &$form, $field_id): array { /** * {@inheritdoc} */ - public function buildConfigurationForm(array $form, FormStateInterface $form_state) { - /** @var \Drupal\views\Plugin\views\filter\FilterPluginBase $filter */ - $filter = $this->handler; - + public function buildConfigurationForm(array $form, FormStateInterface $form_state): array { $form = parent::buildConfigurationForm($form, $form_state); $form['help'] = ['#markup' => $this->t('This widget allows you to use the Finder widget for hierarchical taxonomy terms.')]; + unset($form['advanced']); return $form; } @@ -249,40 +289,79 @@ public static function isApplicable($filter = NULL, array $filter_options = []) * Preprocesses variables for the az-finder-widget template. * * @param array &$variables - * An associative array containing the element being processed. + * An associative array containing the element being processed (passed by + * reference). */ - public function preprocessAzFinderTaxonomyIndexTidWidget(array &$variables) { + public function preprocessAzFinderTaxonomyIndexTidWidget(array &$variables): void { $element = $variables['element']; $variables += [ 'wrapper_attributes' => new Attribute(), 'children' => Element::children($element), 'attributes' => ['name' => $element['#name']], ]; - if (!empty($element['#hierarchy'])) { - $variables['is_nested'] = TRUE; - } - $variables['is_nested'] = TRUE; $variables['depth'] = []; $element = $variables['element']; - // Example logic to structure elements (simplified for illustration). - foreach ($variables['children'] as $child) { + // Retrieve view_id and display_id from the element's #context. + $view_id = $element['#context']['#view_id']; + $display_id = $element['#context']['#display_id']; + + // Load the view entity and get the display options. + $view_storage = $this->entityTypeManager->getStorage('view'); + $view = $view_storage->load($view_id); + if ($view) { + $display_options = $view->get('display')[$display_id]['display_options'] ?? []; + // Check if 'filters' is set in display-specific options. + if (empty($display_options['filters']) && isset($view->get('display')['default']['display_options']['filters'])) { + $default_filters = $view->get('display')['default']['display_options']['filters']; + $display_options['filters'] = $default_filters; + } + } + else { + $this->logger->error('Unable to load view: @view_id', ['@view_id' => $view_id]); + return; + } + + // Get the handler options for taxonomy reference fields. + $vid = NULL; + if (isset($display_options['filters'])) { + foreach ($display_options['filters'] as $filter) { + if ($filter['plugin_id'] === 'taxonomy_index_tid') { + $vid = $filter['vid']; + break; + } + } + } + if (!$vid) { + $this->logger->error('Unable to find vocabulary ID (vid) in handler options.'); + return; + } + // Load override settings. + $overrides = $this->getOverrideConfigurations($view_id, $display_id); + $state_overrides = []; + // Create a flat array of the overrides by term id. + foreach ($overrides as $vid => $override) { + $state_overrides += $override['state_overrides'] ?? []; + } + $variables['overrides'] = $state_overrides; + // Load global default settings. + $global_settings = $this->configFactory->get('az_finder.settings'); + $global_default_state = $global_settings->get('tid_widget.default_state') ?? ''; + foreach ($variables['children'] as $child) { if ($child === 'All') { // Special handling for "All" option. $variables['depth'][$child] = 0; continue; } - // $entity_type = $child_element['#entity_type']; + $entity_type = 'taxonomy_term'; - $entity_id = $child; + $entity_id = is_numeric($child) ? $child : str_replace('tid:', '', $child); + $state = $state_overrides[$entity_id] ?? $global_default_state; + $variables['element'][$child]['#state'] = $state; $entity_storage = $this->entityTypeManager->getStorage($entity_type); $children = method_exists($entity_storage, 'loadChildren') ? $entity_storage->loadChildren($entity_id) : []; - if (empty($children) && $entity_type !== 'taxonomy_term') { - continue; - } - $original_title = $element[$child]['#title']; if (empty($original_title)) { continue; @@ -291,44 +370,77 @@ public function preprocessAzFinderTaxonomyIndexTidWidget(array &$variables) { $list_title = [ '#type' => 'html_tag', ]; + // Determine if the child has sub-elements (actual children). // Calculate depth based on hyphens in the title as a proxy for hierarchy. $depth = strlen($original_title) - strlen($cleaned_title); $list_title['#value'] = $cleaned_title; - // Decide which icon to use based on depth. + + // Decide which icon to use based on depth and state. $icons = $this->azFinderIcons->generateSvgIcons(); $level_0_collapse_icon = $icons['level_0_collapse']; + $level_0_expand_icon = $icons['level_0_expand']; $level_1_collapse_icon = $icons['level_1_collapse']; - if (!empty($level_0_collapse_icon) && !empty($level_1_collapse_icon)) { - $collapse_icon = $depth === 0 ? $level_0_collapse_icon : $level_1_collapse_icon; + $level_1_expand_icon = $icons['level_1_expand']; + if ($depth === 0) { + // Use level 0 icons. + $collapse_icon = $level_0_collapse_icon; + $expand_icon = $level_0_expand_icon; } else { - $collapse_icon = $icons['level_0_collapse']; + // Use level 1 icons. + $collapse_icon = $level_1_collapse_icon; + $expand_icon = $level_1_expand_icon; + } + + // Select the icon based on the state if it is expand or collapse. + if ($state === 'expand') { + // Use collapse icon when expanded. + $icon = $collapse_icon; + } + elseif ($state === 'collapse') { + // Use expand icon when collapsed. + $icon = $expand_icon; } + else { + // Do not set an icon for other states. + $icon = NULL; + } + $variables['depth'][$child] = $depth; $list_title['#value'] = $cleaned_title; $variables['element'][$child]['#title'] = $list_title['#value']; if (!empty($children)) { $list_title_link = [ + '#state' => $state, '#type' => 'html_tag', '#tag' => 'a', '#attributes' => [ 'class' => [], ], ]; + $collapse_id = 'collapse-az-finder-' . $entity_id; $list_title_link['#attributes']['data-toggle'] = 'collapse'; $list_title_link['#attributes']['href'] = '#' . $collapse_id; $list_title_link['#attributes']['class'][] = 'd-block'; $list_title_link['#attributes']['role'] = 'button'; - $list_title_link['#attributes']['aria-expanded'] = 'true'; + if ($state === 'expand') { + $list_title_link['#attributes']['aria-expanded'] = 'true'; + } + else { + $list_title_link['#attributes']['aria-expanded'] = 'false'; + $list_title_link['#attributes']['class'][] = 'collapsed'; + } $list_title_link['#attributes']['aria-controls'] = $collapse_id; $list_title_link['#attributes']['data-collapse-id'] = $collapse_id; $list_title_link['#attributes']['class'][] = 'collapser'; $list_title_link['#attributes']['class'][] = 'level-' . $depth; $list_title_link['#attributes']['class'][] = 'text-decoration-none'; - $list_title['icon'] = $collapse_icon; + if ($icon !== NULL) { + $list_title['icon'] = $icon; + } if ($depth === 0) { $list_title_link['#attributes']['class'][] = 'js-svg-replace-level-0'; $list_title['#tag'] = 'h3'; @@ -349,30 +461,28 @@ public function preprocessAzFinderTaxonomyIndexTidWidget(array &$variables) { $list_title['#attributes']['class'][] = 'align-items-center'; } $list_title_link['value'] = $list_title; - // Apply the modified list title to the element. $variables['element'][$child] = $list_title_link; } } - } /** - * Calculates depth for a given option label. + * Calculate the depth of the option. * * @param mixed $option - * The option, which can be a string label or an object with properties. + * The option to calculate the depth for. * * @return int - * The calculated depth. + * The depth of the option. */ protected function calculateDepth($option): int { // Initialize depth. $depth = 0; // Ensure $option is a string before processing. - $optionLabel = is_object($option) ? (property_exists($option, 'label') ? $option->label : '') : $option; + $label = is_object($option) ? (property_exists($option, 'label') ? $option->label : '') : $option; // Use a loop or string function to count leading hyphens in the label. - while (isset($optionLabel[$depth]) && $optionLabel[$depth] === '-') { + while (isset($label[$depth]) && $label[$depth] === '-') { $depth++; } @@ -402,4 +512,44 @@ protected function getAccessibleActionTitle($action, $depth): ?string { return ucfirst($action) . " level $level"; } + /** + * {@inheritdoc} + */ + public function submitConfigurationForm(array &$form, FormStateInterface $form_state) { + parent::submitConfigurationForm($form, $form_state); + + $default_states = []; + $values = $form_state->getValue('default_states'); + foreach ($values as $tid => $state) { + $default_states[$tid] = $state; + } + + $this->configuration['default_states'] = $default_states; + } + + /** + * Get override configurations. + */ + public function getOverrideConfigurations($view_id, $display_id) { + $config_key = "$view_id.$display_id"; + if (!isset(self::$overrides[$config_key])) { + $config_name = "az_finder.tid_widget.$view_id.$display_id"; + $config = $this->configFactory->getEditable($config_name) ?? NULL; + $overrides = []; + if ($config) { + $vocabularies = $config->get('vocabularies') ?? []; + foreach ($vocabularies as $vocabulary_id => $vocabulary) { + $terms = $vocabulary['terms']; + foreach ($terms as $term_id => $override) { + if (!empty($override['default_state'])) { + $overrides[$vocabulary_id]['state_overrides'][$term_id] = $override['default_state']; + } + } + } + } + self::$overrides[$config_key] = $overrides; + } + return self::$overrides[$config_key]; + } + } diff --git a/modules/custom/az_finder/src/AZFinderIcons.php b/modules/custom/az_finder/src/Service/AZFinderIcons.php similarity index 98% rename from modules/custom/az_finder/src/AZFinderIcons.php rename to modules/custom/az_finder/src/Service/AZFinderIcons.php index 82ebbc62da..99d181921f 100644 --- a/modules/custom/az_finder/src/AZFinderIcons.php +++ b/modules/custom/az_finder/src/Service/AZFinderIcons.php @@ -2,7 +2,7 @@ declare(strict_types=1); -namespace Drupal\az_finder; +namespace Drupal\az_finder\Service; /** * Provides SVG icons for the Quickstart Finder module. diff --git a/modules/custom/az_finder/src/Service/AZFinderOverrides.php b/modules/custom/az_finder/src/Service/AZFinderOverrides.php new file mode 100644 index 0000000000..56ec82b1d2 --- /dev/null +++ b/modules/custom/az_finder/src/Service/AZFinderOverrides.php @@ -0,0 +1,64 @@ +configFactory = $config_factory; + } + + /** + * Get existing overrides. + */ + public function getExistingOverrides() { + $config_names = $this->configFactory->listAll('az_finder.tid_widget.'); + $overrides = []; + + foreach ($config_names as $config_name) { + $view_id_display_id = substr($config_name, strlen('az_finder.tid_widget.')); + [$view_id, $display_id] = explode('.', $view_id_display_id); + + // Get the specific display config. + $view_config = $this->configFactory->get('views.view.' . $view_id); + $display_options = $view_config->get('display.' . $display_id . '.display_options') ?? []; + + // Check if 'filters' is set in display-specific options. + if (empty($display_options['filters']) && isset($view_config->get('display')['default']['display_options']['filters'])) { + $default_filters = $view_config->get('display')['default']['display_options']['filters']; + $display_options['filters'] = $default_filters; + } + + $overrides[$view_id . ':' . $display_id] = [ + 'view_id' => $view_id, + 'display_id' => $display_id, + 'view_label' => $view_config->get('label') ?? '', + 'display_title' => $view_config->get('display.' . $display_id . '.display_title') ?? '', + 'vocabularies' => $display_options['filters']['vocabularies'] ?? [], + ]; + } + + return $overrides; + } + +} diff --git a/modules/custom/az_finder/src/Service/AZFinderViewOptions.php b/modules/custom/az_finder/src/Service/AZFinderViewOptions.php new file mode 100644 index 0000000000..9470e80d9a --- /dev/null +++ b/modules/custom/az_finder/src/Service/AZFinderViewOptions.php @@ -0,0 +1,89 @@ +cacheBackend = $cache_backend; + $this->entityTypeManager = $entity_type_manager; + } + + /** + * Get the view options for a plugin. + */ + public function getViewOptions(string $plugin_id = 'az_finder_tid_widget', bool $force_refresh = FALSE): array { + $cache_id = 'az_finder:view_options:' . $plugin_id; + if (!$force_refresh) { + $cached_data = $this->cacheBackend->get($cache_id); + if ($cached_data) { + return $cached_data->data; + } + } + + $viewOptions = $this->getViewsUsingPlugin($plugin_id); + $this->cacheBackend->set($cache_id, $viewOptions, CacheBackendInterface::CACHE_PERMANENT, ['az_finder:view_options']); + return $viewOptions; + } + + /** + * Get the views using a specific plugin id. + */ + private function getViewsUsingPlugin(string $plugin_id): array { + $options = ['' => '- Select -']; + $views = $this->entityTypeManager->getStorage('view')->loadMultiple(); + + foreach ($views as $view) { + // Get the view executable. + $view_exec = $view->getExecutable(); + $displays = $view->get('display') ?: []; + foreach ($displays as $display_id => $display) { + // Initialize the view with the selected display. + $view_exec->initDisplay(); + $view_exec->setDisplay($display_id); + // Load the display handler so we have access to the overridden options. + $display_handler = $view_exec->getDisplay(); + if ($display_handler->isDefaultDisplay()) { + // Don't display master displays as override options. + continue; + } + $exposed_form_options = $display_handler->getOption('exposed_form') ?? []; + $filters = $exposed_form_options['options']['bef']['filter'] ?? []; + foreach ($filters as $filter_id => $filter_settings) { + if (isset($filter_settings['plugin_id']) && $filter_settings['plugin_id'] === $plugin_id) { + $options[$view->id() . ':' . $display_id] = $view->label() . ' (' . $displays[$display_id]['display_title'] . ')'; + break; + } + } + } + } + + return $options; + } + +} diff --git a/modules/custom/az_finder/src/Service/AZFinderVocabulary.php b/modules/custom/az_finder/src/Service/AZFinderVocabulary.php new file mode 100644 index 0000000000..b520f8eb82 --- /dev/null +++ b/modules/custom/az_finder/src/Service/AZFinderVocabulary.php @@ -0,0 +1,103 @@ +entityTypeManager = $entity_type_manager; + $this->stringTranslation = $string_translation; + } + + /** + * Get the vocabulary IDs for a filter. + */ + public function getVocabularyIdsForFilter($view_id, $display_id, $filter_id) { + $vocabulary_ids = []; + $view = $this->entityTypeManager->getStorage('view')->load($view_id); + if ($view) { + $display = $view->getDisplay($display_id); + $filters = $display['display_options']['filters'] ?? []; + + // Check if 'filters' is set in display-specific options. + if (empty($filters) && isset($view->get('display')['default']['display_options']['filters'])) { + $default_filters = $view->get('display')['default']['display_options']['filters']; + $filters = array_merge($filters, $default_filters); + } + foreach ($filters as $filter) { + if (($filter['exposed'] ?? FALSE) !== TRUE) { + continue; + } + if (isset($filter['plugin_id']) && $filter['plugin_id'] === 'taxonomy_index_tid') { + $vocabulary_ids[] = $filter['vid']; + } + } + } + return $vocabulary_ids; + } + + /** + * Add a section to the form for configuring vocabulary terms. + * + * @param array $form_section + * The form section to add the terms table to. + * @param int $vocabulary_id + * The vocabulary ID. + * @param string $view_id + * The view ID. + * @param string $display_id + * The display ID. + */ + public function addTermsTable(&$form_section, $vocabulary_id, $view_id, $display_id) { + $terms = $this->entityTypeManager->getStorage('taxonomy_term')->loadTree($vocabulary_id); + $config_id = "az_finder.tid_widget.$view_id.$display_id"; + $vocabulary_config_path = "$config_id:vocabularies.$vocabulary_id"; + $vocabulary_label = $this->entityTypeManager->getStorage('taxonomy_vocabulary')->load($vocabulary_id)->label(); + + $form_section['terms_table'] = [ + '#type' => 'table', + '#header' => [ + $this->t('Terms in :vocabulary vocabulary', [':vocabulary' => $vocabulary_label]), + $this->t('Override'), + ], + '#empty' => $this->t('No terms found.'), + ]; + + foreach ($terms as $term) { + $form_section['terms_table'][$term->tid]['term_name'] = [ + '#markup' => str_repeat('-', $term->depth) . $term->name, + ]; + $form_section['terms_table'][$term->tid]['override'] = [ + '#type' => 'select', + '#options' => [ + '' => $this->t('Default'), + 'expand' => $this->t('Expanded'), + 'collapse' => $this->t('Collapsed'), + ], + '#config_target' => "$vocabulary_config_path.terms.{$term->tid}.default_state", + ]; + } + } + +} diff --git a/modules/custom/az_finder/templates/az-finder-nested-elements.html.twig b/modules/custom/az_finder/templates/az-finder-nested-elements.html.twig index aba754dad4..ca1f938c6d 100644 --- a/modules/custom/az_finder/templates/az-finder-nested-elements.html.twig +++ b/modules/custom/az_finder/templates/az-finder-nested-elements.html.twig @@ -21,7 +21,7 @@ {% if delta %} {% for i in 1..delta %} {% if new_nesting_level > current_nesting_level %} -