diff --git a/config/vufind/facets.ini b/config/vufind/facets.ini index d99dd52eeb2..53f86f19ff2 100644 --- a/config/vufind/facets.ini +++ b/config/vufind/facets.ini @@ -76,6 +76,10 @@ dateRangeFieldType[publishDateRange] = DateRangeField ;hierarchical[] = building ;hierarchical[] = format +; Any fields listed below will be treated as "new items" facets. +; Note that ranges displayed are defined in [NewItem] section of searches.ini. +newItems[] = first_indexed + ; General sort options for hierarchical facets (Home page, Advanced Search and ; SideFacets). ; diff --git a/module/VuFind/src/VuFind/Controller/Plugin/NewItems.php b/module/VuFind/src/VuFind/Controller/Plugin/NewItems.php index 694722b9981..57f91d72e25 100644 --- a/module/VuFind/src/VuFind/Controller/Plugin/NewItems.php +++ b/module/VuFind/src/VuFind/Controller/Plugin/NewItems.php @@ -231,6 +231,6 @@ public function getResultPages() */ public function getSolrFilter($range) { - return 'first_indexed:[NOW-' . $range . 'DAY TO NOW]'; + return 'first_indexed:[NOW-' . $range . 'DAYS/DAY TO *]'; } } diff --git a/module/VuFind/src/VuFind/Controller/SearchController.php b/module/VuFind/src/VuFind/Controller/SearchController.php index ae4fd09fe20..9db0c762634 100644 --- a/module/VuFind/src/VuFind/Controller/SearchController.php +++ b/module/VuFind/src/VuFind/Controller/SearchController.php @@ -367,6 +367,12 @@ public function newitemfacetlistAction() public function newitemresultsAction() { $newItemParams = $this->getNewItemParameters(); + if ($this->newItems()->getMethod() === 'solr') { + $filter = (array)$this->params()->fromQuery('filter', []); + $filter[] = $this->newItems()->getSolrFilter($newItemParams['range']); + $query = ['filter' => $filter, 'hiddenFilters' => $newItemParams['hiddenFilters']]; + return $this->redirect()->toRoute('search-results', options: compact('query')); + } $this->setUpNewItemRequestParams($newItemParams); // Don't save to history or memory -- history page doesn't handle correctly diff --git a/module/VuFind/src/VuFind/Search/Base/Options.php b/module/VuFind/src/VuFind/Search/Base/Options.php index 5392b8c7413..2ff896e0aec 100644 --- a/module/VuFind/src/VuFind/Search/Base/Options.php +++ b/module/VuFind/src/VuFind/Search/Base/Options.php @@ -468,6 +468,20 @@ abstract class Options implements TranslatorAwareInterface */ protected string $advancedFacetSettingsSection = 'Advanced_Settings'; + /** + * New items facets + * + * @var array + */ + protected array $newItemsFacets; + + /** + * Ranges for new items facets + * + * @var array + */ + protected array $newItemsFacetRanges; + /** * Constructor * @@ -528,6 +542,19 @@ public function __construct(protected ConfigManagerInterface $configManager) $this->hierarchicalFacetFilters = $this->facetSettings['HierarchicalFacetFilters'] ?? []; $this->setTranslatedFacets((array)($advancedFacetSettings['translated_facets'] ?? [])); $this->specialAdvancedFacets = $advancedFacetSettings['special_facets'] ?? ''; + $this->newItemsFacets = array_map( + function ($v) { + return true; + }, + array_flip((array)($this->facetSettings['SpecialFacets']['newItems'] ?? [])) + ); + // Find out if there are user configured range options; if not, default to the standard 1/5/30 days: + $this->newItemsFacetRanges = array_filter( + array_map( + 'intval', + explode(',', $this->searchSettings['NewItem']['ranges'] ?? '') + ) + ) ?: [1, 5, 30]; // Result display options: $this->resultScrollerActive = (bool)( @@ -985,6 +1012,26 @@ public function getHierarchicalFacetSortSettings() return $this->hierarchicalFacetSortSettings; } + /** + * Get new items facets. + * + * @return array + */ + public function getNewItemsFacets(): array + { + return $this->newItemsFacets; + } + + /** + * Get filters for new items facets. + * + * @return array + */ + public function getNewItemsFacetFilters(): array + { + return []; + } + /** * Get current spellcheck setting and (optionally) change it. * diff --git a/module/VuFind/src/VuFind/Search/Base/Params.php b/module/VuFind/src/VuFind/Search/Base/Params.php index 2bc6f69fd67..0ed35a3e9cf 100644 --- a/module/VuFind/src/VuFind/Search/Base/Params.php +++ b/module/VuFind/src/VuFind/Search/Base/Params.php @@ -1990,18 +1990,27 @@ public function getSelectedShards() /** * Translate a string (or string-castable object) * - * @param string|object|array $target String to translate or an array of text - * domain and string to translate - * @param array $tokens Tokens to inject into the translated - * string - * @param string $default Default value to use if no translation is - * found (null for no default). + * @param string|object|array $target String to translate or an array of text + * domain and string to translate + * @param array $tokens Tokens to inject into the translated + * string + * @param string $default Default value to use if no translation is + * found (null for no default). + * @param bool $useIcuFormatter Should we use an ICU message formatter instead + * of the default behavior? + * @param string[] $fallbackDomains Text domains to check if no match is found in + * the domain specified in $target * * @return string */ - public function translate($target, $tokens = [], $default = null) - { - return $this->getOptions()->translate($target, $tokens, $default); + public function translate( + $target, + $tokens = [], + $default = null, + $useIcuFormatter = false, + $fallbackDomains = [] + ) { + return $this->getOptions()->translate($target, $tokens, $default, $useIcuFormatter, $fallbackDomains); } /** diff --git a/module/VuFind/src/VuFind/Search/Solr/Options.php b/module/VuFind/src/VuFind/Search/Solr/Options.php index dd3422ce64c..f730426c087 100644 --- a/module/VuFind/src/VuFind/Search/Solr/Options.php +++ b/module/VuFind/src/VuFind/Search/Solr/Options.php @@ -275,4 +275,18 @@ public function getDateRangeFieldTypes(): array { return (array)($this->facetSettings['SpecialFacets']['dateRangeFieldType'] ?? []); } + + /** + * Get filters for new items facets. + * + * @return array + */ + public function getNewItemsFacetFilters(): array + { + $result = []; + foreach ($this->newItemsFacetRanges as $days) { + $result[$days] = "[NOW-{$days}DAYS/DAY TO *]"; + } + return $result; + } } diff --git a/module/VuFind/src/VuFind/Search/Solr/Params.php b/module/VuFind/src/VuFind/Search/Solr/Params.php index f4136e8942d..5f1c1c9530f 100644 --- a/module/VuFind/src/VuFind/Search/Solr/Params.php +++ b/module/VuFind/src/VuFind/Search/Solr/Params.php @@ -32,6 +32,8 @@ use VuFind\Config\Config; use VuFind\Config\ConfigManagerInterface; +use VuFind\Date\Converter as DateConverter; +use VuFind\Solr\Utils; use VuFindSearch\ParamBag; use function count; @@ -105,13 +107,6 @@ class Params extends \VuFind\Search\Base\Params */ protected $pivotFacets = null; - /** - * Hierarchical Facet Helper - * - * @var HierarchicalFacetHelper - */ - protected $facetHelper; - /** * Are we searching by ID only (instead of a normal query)? * @@ -149,14 +144,15 @@ class Params extends \VuFind\Search\Base\Params * @param \VuFind\Search\Base\Options $options Options to use * @param ConfigManagerInterface $configManager Config manager * @param ?HierarchicalFacetHelper $facetHelper Hierarchical facet helper + * @param ?DateConverter $dateConverter Date converter */ public function __construct( $options, ConfigManagerInterface $configManager, - ?HierarchicalFacetHelper $facetHelper = null + protected ?HierarchicalFacetHelper $facetHelper = null, + protected ?DateConverter $dateConverter = null ) { parent::__construct($options, $configManager); - $this->facetHelper = $facetHelper; // Use basic facet limit by default, if set: $facetConfigName = $options->getFacetsIni(); @@ -680,6 +676,10 @@ public function getPivotFacets() */ protected function formatFilterListEntry($field, $value, $operator, $translate) { + if (isset($this->options->getNewItemsFacets()[$field])) { + return $this->formatNewItemsFilterListEntry($field, $value, $operator, $translate); + } + $filter = parent::formatFilterListEntry( $field, $value, @@ -742,6 +742,65 @@ function ($part) { return $filter; } + /** + * Format a new items filter for use in getFilterList(). + * + * @param string $field Field name + * @param string $value Field value + * @param string $operator Operator (AND/OR/NOT) + * @param bool $translate Should we translate the label? + * + * @return array + */ + protected function formatNewItemsFilterListEntry($field, $value, $operator, $translate): array + { + $range = Utils::parseRange($value); + $domain = $this->getOptions()->getTextDomainForTranslatedFacet($field); + [$from, $fromDate] = $this->formatNewItemsDateForDisplay( + $range['from'], + $domain + ); + [$to, $toDate] = $this->formatNewItemsDateForDisplay( + $range['to'], + $domain + ); + $ndash = html_entity_decode('–', ENT_NOQUOTES, 'UTF-8'); + if ($fromDate && $toDate) { + $displayText = $from ? "$from $ndash" : $ndash; + $displayText .= $to ? " $to" : ''; + } else { + $displayText = $from; + $displayText .= $to ? " $ndash $to" : ''; + } + return compact('value', 'displayText', 'field', 'operator'); + } + + /** + * Format a Solr date for display + * + * @param string $date Date + * @param string $domain Translation domain + * + * @return string + */ + protected function formatNewItemsDateForDisplay($date, $domain) + { + if ($date == '' || $date == '*') { + return ['', true]; + } + if (preg_match('/^NOW-(\d+)DAY/', $date, $matches)) { + return [ + $this->translate('past_days', ['range' => $matches[1]], useIcuFormatter: true), + false, + ]; + } + $date = substr($date, 0, 10); + return [ + $this->dateConverter?->convertToDisplayDate('Y-m-d', $date) ?? $date, + true, + ]; + } + /** * Get information on the current state of the boolean checkbox facets. * diff --git a/module/VuFind/src/VuFind/Search/Solr/ParamsFactory.php b/module/VuFind/src/VuFind/Search/Solr/ParamsFactory.php index c72c62b5e2e..b4b224b9267 100644 --- a/module/VuFind/src/VuFind/Search/Solr/ParamsFactory.php +++ b/module/VuFind/src/VuFind/Search/Solr/ParamsFactory.php @@ -67,8 +67,8 @@ public function __invoke( if (!empty($options)) { throw new \Exception('Unexpected options passed to factory.'); } - $helper = $container - ->get(\VuFind\Search\Solr\HierarchicalFacetHelper::class); - return parent::__invoke($container, $requestedName, [$helper]); + $helper = $container->get(\VuFind\Search\Solr\HierarchicalFacetHelper::class); + $dateConverter = $container->get(\VuFind\Date\Converter::class); + return parent::__invoke($container, $requestedName, [$helper, $dateConverter]); } } diff --git a/module/VuFind/src/VuFind/Search/UrlQueryHelper.php b/module/VuFind/src/VuFind/Search/UrlQueryHelper.php index 1327b017c94..0a1bb4f835e 100644 --- a/module/VuFind/src/VuFind/Search/UrlQueryHelper.php +++ b/module/VuFind/src/VuFind/Search/UrlQueryHelper.php @@ -453,9 +453,9 @@ protected function getAliasesForFacetField($field) /** * Remove a facet from the parameters. * - * @param string $field Facet field - * @param string $value Facet value - * @param string $operator Facet type to add (AND, OR, NOT) + * @param string $field Facet field + * @param ?string $value Facet value, or null to remove all values for the facet field + * @param string $operator Facet type to add (AND, OR, NOT) * * @return UrlQueryHelper */ @@ -480,7 +480,7 @@ public function removeFacet($field, $value, $operator = 'AND') = $this->parseFilter($current); if ( !in_array($currentField, $fieldAliases) - || $currentValue != $value + || (null !== $value && $currentValue != $value) ) { $newFilter[] = $current; } @@ -498,6 +498,21 @@ public function removeFacet($field, $value, $operator = 'AND') return new static($params, $this->queryObject, $this->config, false); } + /** + * Remove any instance of the facet from the parameters and add a new one. + * + * @param string $field Facet field + * @param string $value Facet value + * @param string $operator Facet type to add (AND, OR, NOT) + * + * @return string + */ + public function replaceFacet($field, $value, $operator = 'AND') + { + return $this->removeFacet($field, null, $operator) + ->addFacet($field, $value, $operator); + } + /** * Remove a filter from the parameters. * diff --git a/module/VuFind/tests/unit-tests/src/VuFindTest/Controller/Plugin/NewItemsTest.php b/module/VuFind/tests/unit-tests/src/VuFindTest/Controller/Plugin/NewItemsTest.php index 670eae6b4ec..2070046a986 100644 --- a/module/VuFind/tests/unit-tests/src/VuFindTest/Controller/Plugin/NewItemsTest.php +++ b/module/VuFind/tests/unit-tests/src/VuFindTest/Controller/Plugin/NewItemsTest.php @@ -203,7 +203,7 @@ public function testIllegalResultPages() public function testGetSolrFilter() { $range = 30; - $expected = 'first_indexed:[NOW-' . $range . 'DAY TO NOW]'; + $expected = 'first_indexed:[NOW-' . $range . 'DAYS/DAY TO *]'; $newItems = new NewItems(new Config([])); $this->assertEquals($expected, $newItems->getSolrFilter($range)); } diff --git a/themes/bootstrap5/js/facets.js b/themes/bootstrap5/js/facets.js index e0354270b27..8ee9c984136 100644 --- a/themes/bootstrap5/js/facets.js +++ b/themes/bootstrap5/js/facets.js @@ -289,7 +289,7 @@ VuFind.register('multiFacetsSelection', function multiFacetsSelection() { } return newParams; } - + /** * Compile modified facets into lists of added and removed URL parameters. */ @@ -316,7 +316,7 @@ VuFind.register('multiFacetsSelection', function multiFacetsSelection() { } } } - + /** * Compile current parameters and newly added / removed to return the URL to redirect to * @returns {string} The new URL to redirect to. @@ -355,8 +355,9 @@ VuFind.register('multiFacetsSelection', function multiFacetsSelection() { /** * Toggle the visual selected style of a facet element. * @param {HTMLElement} elem The facet element to toggle. + * @param {boolean} [checkExclusiveFacets] Whether to handle exclusive facets (default = true). */ - function toggleSelectedFacetStyle(elem) { + function toggleSelectedFacetStyle(elem, checkExclusiveFacets = true) { let excluded = elem.classList.contains('exclude'); let facet; if (elem.classList.contains('facet')) { @@ -378,6 +379,21 @@ VuFind.register('multiFacetsSelection', function multiFacetsSelection() { attrs['data-checked'] = (newCheckedState ? 'true' : 'false'); icon.outerHTML = VuFind.icon(newCheckedState ? 'facet-checked' : 'facet-unchecked', attrs); } + + // Make sure any siblings of an active exclusive filter are not active: + if (checkExclusiveFacets && facet.classList.contains('active') && 'exclusive' in facet.dataset) { + const facetList = facet.closest('.facet__list'); + if (facetList) { + facetList.querySelectorAll('.facet').forEach(otherFacet => { + if (otherFacet !== facet && otherFacet.classList.contains('active')) { + const facetLink = otherFacet.querySelector('a.main-link'); + if (facetLink) { + facetLink.click(); + } + } + }); + } + } } /** @@ -453,7 +469,7 @@ VuFind.register('multiFacetsSelection', function multiFacetsSelection() { } elem.setAttribute('data-multi-filters-modified', isOriginalState); updateCountText(); - toggleSelectedFacetStyle(elem); + toggleSelectedFacetStyle(elem, true); } /** @@ -494,7 +510,7 @@ VuFind.register('multiFacetsSelection', function multiFacetsSelection() { const elems = document.querySelectorAll('[data-multi-filters-modified="true"]'); for (const elem of elems) { elem.setAttribute('data-multi-filters-modified', "false"); - toggleSelectedFacetStyle(elem); + toggleSelectedFacetStyle(elem, false); } } toggleCountText(isMultiFacetsSelectionActivated); @@ -851,7 +867,7 @@ VuFind.register('lightbox_facets', function LightboxFacets() { */ function lightboxFacetSorting() { var sortButtons = $('.js-facet-sort'); - + /** * Trigger an AJAX call to update the facet list with a new sort order. * @param {HTMLElement} button The button element that was clicked to trigger the sort. diff --git a/themes/bootstrap5/package-lock.json b/themes/bootstrap5/package-lock.json index 3512445a454..6802aa89904 100644 --- a/themes/bootstrap5/package-lock.json +++ b/themes/bootstrap5/package-lock.json @@ -27,6 +27,7 @@ "version": "2.11.8", "resolved": "https://registry.npmjs.org/@popperjs/core/-/core-2.11.8.tgz", "integrity": "sha512-P1st0aksCrn9sGZhp8GMYwBnQsbvAWsZAX44oXNNvLHGqAOcoVxmjZiohstwQ7SqKnbR47akdNi+uleWD8+g6A==", + "peer": true, "funding": { "type": "opencollective", "url": "https://opencollective.com/popperjs" diff --git a/themes/bootstrap5/templates/Recommend/SideFacets/facet.phtml b/themes/bootstrap5/templates/Recommend/SideFacets/facet.phtml index 791b9c331ae..e4d42d4cb8b 100644 --- a/themes/bootstrap5/templates/Recommend/SideFacets/facet.phtml +++ b/themes/bootstrap5/templates/Recommend/SideFacets/facet.phtml @@ -4,6 +4,7 @@ $hierarchicalFacetSortOptions = $this->recommend->getHierarchicalFacetSortOptions(); $hierarchicalFacets = $this->recommend->getHierarchicalFacets(); $rangeFacets = $this->recommend->getAllRangeFacets(); + $newItemsFacets = $this->options->getNewItemsFacets(); $facet = $this->facet; ?> @@ -13,6 +14,13 @@ ['title' => $facet, 'cluster' => $cluster, 'facet' => $rangeFacets[$facet]] ); ?> + + context($this)->renderInContext( + 'Recommend/SideFacets/new-items-facet.phtml', + compact('results', 'facet', 'newItemsFacets') + ); + ?> getOptions()->getNewItemsFacetFilters(); + $safeBaseId = $this->escapeHtmlAttr($this->facet); + $ulAttrs = $this->htmlAttributes(['class' => 'facet__list']); + $truncateSettings = [ + 'rows' => $this->facets_before_more, + 'btn-class' => 'facet', + 'more-label' => $this->transEsc('more_ellipsis'), + 'less-label' => $this->transEsc('less_ellipsis'), + 'wrapper-class' => false, + 'wrapper-tagname' => 'li', + ]; + if ($truncateSettings) { + $ulAttrs->add('class', 'truncate-facets'); + $ulAttrs->add('data-truncate', $this->htmlSafeJsonEncode($truncateSettings, null)); + } +?> +> + + $newItemsFilter): ?> +
  • + getUrlQuery()->replaceFacet($facet, $newItemsFilter); + $removeURL = $results->getUrlQuery()->removeFacet($facet, $newItemsFilter); + $isApplied = $results->getUrlQuery() != $removeURL; + $toggleUrl = $isApplied + ? $this->urlBase . $removeURL + : $this->urlBase . $addURL; + $classList = ['facet', 'js-facet-item', 'facetAND']; + if ($isApplied) { + $classList[] = 'active'; + } + $textEsc = $this->transEsc('past_days', compact('range'), useIcuFormatter: true); + ?> + + + + + + + +
  • + +