0 Votes

Changes for page Solr Search

Last modified by Сергей Коршунов on 2025/12/29 15:30

From version 2.1
edited by Сергей Коршунов
on 2022/03/08 17:00
Change comment: Install extension [org.xwiki.platform:xwiki-platform-search-solr-ui/14.1]
To version 8.1
edited by Сергей Коршунов
on 2025/12/29 15:30
Change comment: Install extension [org.xwiki.platform:xwiki-platform-search-solr-ui/17.10.0]

Summary

Details

dark-grey-disclosure-arrow-down.png
Author
... ... @@ -1,1 +1,0 @@
1 -XWiki.admins
Size
... ... @@ -1,1 +1,0 @@
1 -94 bytes
Content
dark-grey-disclosure-arrow-left.png
Author
... ... @@ -1,1 +1,0 @@
1 -XWiki.admins
Size
... ... @@ -1,1 +1,0 @@
1 -94 bytes
Content
light-grey-disclosure-arrow-down.png
Author
... ... @@ -1,1 +1,0 @@
1 -XWiki.admins
Size
... ... @@ -1,1 +1,0 @@
1 -94 bytes
Content
light-grey-disclosure-arrow-left.png
Author
... ... @@ -1,1 +1,0 @@
1 -XWiki.admins
Size
... ... @@ -1,1 +1,0 @@
1 -94 bytes
Content
XWiki.JavaScriptExtension[0]
Code
... ... @@ -1,11 +1,7 @@
1 1  require(['jquery', 'xwiki-events-bridge'], function($) {
2 2   var enhanceSearchResultHighlights = function() {
3 - var highlights = $(this).removeClass('hidden').parent().prev('.search-result-highlights').addClass('preview');
3 + const highlights = $(this).removeClass('hidden').prev('.search-result-highlights').addClass('preview');
4 4  
5 - // Workaround for IE8 which doesn't support :first-of-type CSS selector.
6 - highlights.find('.search-result-highlight').first().addClass('first').parent('dd').addClass('first')
7 - .prev('dt').addClass('first');
8 -
9 9   $(this).one('click', function(event) {
10 10   event.preventDefault();
11 11   $(event.target).remove();
... ... @@ -38,13 +38,26 @@
38 38   };
39 39  
40 40   var addFacetValueCheckbox = function() {
41 - var checkBox = $(document.createElement('input')).attr('type', 'checkbox');
37 + // Create an id unique to the facet value.
38 + let facetContainer = $(this).parents('.search-facet').first();
39 + // We need the ids of the target facet elements to stay stable through the change, in order to refocus the item
40 + // that was clicked and triggered the change.
41 + // We use the JQuery index, so that id indexes are reset across facetContainers.
42 + // This allows for stable indexes: when reloading the facets after changing the filter for the target facet X,
43 + // the ids of all the checkbox for the target facet X will keep the same index.
44 + // This property is lost if multiple facets are changed in the query at once.
45 + $(this).attr('id', facetContainer.attr('data-name') + '-' + facetContainer.find('a.itemName').index($(this)));
46 + // Initialize the checkbox.
47 + let checkBox = $(document.createElement('input')).attr('type', 'checkbox');
48 + checkBox.attr('aria-labelledby', $(this).attr('id'));
42 42   checkBox.prop('checked', $(this).hasClass('selected'));
43 43   // Add the 'checked' attribute so that it can be easily located with CSS.
44 44   checkBox.prop('checked') && checkBox.attr('checked', 'checked');
45 45   checkBox.on('click', this.click.bind(this));
46 - // Remove the 'selected' class because the selected state is marked using the check box.
47 - $(this).removeClass('selected').before(checkBox);
53 + // Remove the 'selected' class because the selected state is already marked using the check box.
54 + $(this).removeClass('selected')
55 + // Add the checkbox to the DOM
56 + $(this).before(checkBox)
48 48   };
49 49  
50 50   var enhanceSearchFacets = function() {
... ... @@ -61,8 +61,8 @@
61 61   updateExpandCollapseAllFacetsState(facetsContainer);
62 62  
63 63   // Expand/Collapse toggle for each facet.
64 - facetsContainer.find('.search-facet-header').on('click', function(event) {
65 - $(event.target).parent('.search-facet').toggleClass('expanded');
73 + facetsContainer.find('.facet-toggle').on('click', function(event) {
74 + $(event.target).parents('.search-facet').toggleClass('expanded');
66 66   updateExpandCollapseAllFacetsState(facetsContainer);
67 67   });
68 68  
... ... @@ -76,6 +76,11 @@
76 76   var queryIndex = url.indexOf('?');
77 77   return queryIndex < 0 ? '' : url.substr(queryIndex + 1);
78 78   };
88 +
89 + let removeQueryString = function(url) {
90 + let queryIndex = url.indexOf('?');
91 + return queryIndex < 0 ? url : url.substring(0, queryIndex);
92 + };
79 79  
80 80   var getSearchUIState = function() {
81 81   var expandedFacets = [];
... ... @@ -105,7 +105,9 @@
105 105  
106 106   var searchRequest = null;
107 107  
108 - var pushSearchUIState = function(viewURL) {
122 + // changeTargetSelector is the CSS selector of the facet anchor that initiated this reload. '
123 + // We expect the focus to go back to this place once the UI is reloaded.
124 + var pushSearchUIState = function(viewURL, changeTargetSelector) {
109 109   // If there is a request in progress, abort it to prevent its callback from being called.
110 110   searchRequest && searchRequest.abort();
111 111   $('.search-ui').attr('aria-busy', true);
... ... @@ -118,6 +118,7 @@
118 118   window.history.replaceState && window.history.replaceState(state, document.title);
119 119   // Make sure the browser address bar reflects the new state (and thus the new state can be bookmarked).
120 120   window.history.pushState && window.history.pushState(state, document.title, viewURL);
137 + document.querySelector(changeTargetSelector)?.focus();
121 121   });
122 122   };
123 123  
... ... @@ -124,12 +124,12 @@
124 124   var reloadSearchUI = function(event) {
125 125   event.preventDefault();
126 126   var anchor = $(event.target).closest('a');
127 - anchor.length && $(document).trigger('xwiki:search:update', anchor.attr('href'));
144 + anchor.length && anchor.first().trigger('xwiki:search:update', anchor.attr('href'));
128 128   };
129 129  
130 130   // Others (e.g. a custom facet) can trigger a search UI update by firing this event.
131 131   $(document).on('xwiki:search:update', function(event, viewURL) {
132 - pushSearchUIState(viewURL);
149 + pushSearchUIState(viewURL, `[data-facetvalue='${CSS.escape(event.target.dataset.facetvalue)}']`);
133 133   });
134 134  
135 135   $(window).on('popstate', function(event) {
... ... @@ -169,10 +169,41 @@
169 169   });
170 170  
171 171   var enhanceSearchUI = function() {
189 + // Enhance search options
190 + document.querySelectorAll('input.options-item').forEach(function (option){
191 + option.addEventListener('change', function() {
192 + let queryFieldName = this.getAttribute('data-query-name');
193 + let queryField = document.querySelector('input[name="' + queryFieldName + '"]');
194 + queryField.value = this.checked ? 'true' : 'false';
195 + // We want to build the URL the same way it's done in the velocimacro #extendQueryString
196 + // We retrieve the search parameters of the latest request
197 + let params = new URLSearchParams(window.location.search);
198 + let formParams = new URLSearchParams(new FormData(this.form));
199 + // We replace the existing parameters with their value from the form
200 + for (let param of formParams.keys()) {
201 + if (params.has(param)) params.delete(param);
202 + for (let paramValue of formParams.getAll(param)) {
203 + params.append(param,paramValue);
204 + }
205 + }
206 + pushSearchUIState(window.location.pathname + '?' + params.toString(),
207 + 'input[data-query-name="' + queryFieldName + '"]');
208 + });
209 + });
210 + // Enhance search result sorting
211 + document.querySelectorAll('.search-results-sort select#sort-by-input, ' +
212 + '.search-results-sort input#sort-order-input').forEach(function (sortInput){
213 + sortInput.addEventListener('change', function() {
214 + let baseURL = removeQueryString(this.form.getAttribute('action'));
215 + pushSearchUIState(baseURL + "?" +
216 + new URLSearchParams(new FormData(this.form)).toString(),
217 + 'input[data-query-name="' + this.getAttribute('name') + '"]');
218 + });
219 + });
220 +
172 172   $('.search-result-highlightAll').each(enhanceSearchResultHighlights);
173 173   $('.search-facets').each(enhanceSearchFacets);
174 174   $([
175 - '.search-results-sort a.sort-item',
176 176   '.search-options a.options-item',
177 177   '.pagination a',
178 178   '.controlPagination a',
XWiki.StyleSheetExtension[0]
Code
... ... @@ -1,7 +1,7 @@
1 1  #template('colorThemeInit.vm')
2 2  
3 3  /* Hide the 'Created by', 'Modified by' and 'Tags' document sections. */
4 -.xdocLastModification, .skin-colibri #document-info, #xdocFooter {
4 +.xdocLastModification, #xdocFooter {
5 5   display: none;
6 6  }
7 7  #document-title > h1 {
... ... @@ -9,86 +9,99 @@
9 9   margin-bottom: 0;
10 10  }
11 11  
12 +/**
13 + * Layout for the search bar
14 + */
15 +.search-bar {
16 + margin: .5em 0;
17 +}
18 +
19 +@media (min-width: 768px) {
20 + .search-bar {
21 + max-width: 50%;
22 + }
23 +}
24 +
12 12  /**
13 - * Search form
26 + * Layout for search controls (sort + options)
14 14   */
15 15  
16 -.skin-colibri .search-form {
17 - /* There is no space after the title in Colibri. */
18 - margin-top: 1.5em;
29 +.search-results-controls {
30 + display: flex;
31 + flex-wrap: wrap;
32 + gap: 1em;
33 +
34 + & label {
35 + /* Reset the styles of the labels. */
36 + margin: 0;
37 + font-weight: unset;
38 + }
19 19  }
20 20  
21 -.skin-colibri .search-form input[type="search"] {
22 - /* Colibri doesn't have the grid system. */
23 - width: 50%;
41 +.search-results-sort,
42 +.search-options, .search-options > ul, .search-options > ul > li,
43 +.search-options > ul > li > label {
44 + display: flex;
45 + gap: .4em;
46 + align-items: center;
24 24  }
25 25  
26 26  /**
27 27   * Sort
28 28   */
52 +/* This select should be especially lightweight on the UI. We're removing the default border and shadow. */
53 +#sort-by-input {
54 + cursor: pointer;
29 29  
30 -ul.search-results-sort {
31 - color: $theme.textSecondaryColor;
32 - font-size: .9em;
33 - padding: 5px 0 2px 0;
56 + &:hover, &:focus {
57 + cursor: pointer;
58 + }
59 +}
60 +
61 +/* This checkbox input should be styled as a switch between ascending and descending states. */
62 +.search-results-sort #sort-order-input {
63 + /* Hide the default checkbox. We rely on the style of the icon in its label to make it work. */
64 + appearance: none;
34 34   margin: 0;
35 35  }
36 -.search-results-sort li {
37 - display: inline;
38 - list-style-type: none;
39 - padding-left: 1.5em;
67 +
68 +#sort-by-input, .search-results-sort label:has(>#sort-order-input) {
69 + /* We want most of the styles from "form-control", but not the shadow that comes from bootstrap. */
70 + box-shadow: none;
71 + & :hover,& :focus,& :focus-within {
72 + border-color: var(--input-border-focus);
73 + }
40 40  }
41 -.search-results-sort li:first-of-type {
42 - padding: 0;
75 +
76 +/* Flip the second icon so that it's the arrow going up and down. */
77 +.search-results-sort label #sort-order-input + * + * {
78 + transform: scaleY(-1);
43 43  }
44 -a.sort-item {
45 - color: inherit;
46 - text-decoration: none;
80 +
81 +/* When the box is checked, we want to hide the first icon.
82 +When the box is not checked, we want to hide the second icon. */
83 +.search-results-sort label #sort-order-input:checked + *,
84 +.search-results-sort label #sort-order-input:not(:checked) + * + * {
85 + display: none;
47 47  }
48 -a.sort-item:hover {
49 - color: $theme.linkColor;
50 - text-decoration: underline;
51 -}
52 -a.sort-item.active, a.sort-item.active:hover {
53 - font-weight: bold;
54 - color: $theme.textColor;
55 - text-decoration: none;
56 -}
57 -.sort-item-order {
58 - margin-left: .3em;
59 -}
60 60  
61 61  /**
62 62   * Options
63 63   */
64 64  
65 -ul.search-options {
66 - color: $theme.textSecondaryColor;
67 - font-size: .9em;
68 - padding: 5px 0 2px 0;
92 +.search-options ul {
69 69   margin: 0;
70 -}
71 -.search-options li {
72 - display: inline;
73 - list-style-type: none;
74 - padding-left: 1.5em;
75 -}
76 -.search-options li:first-of-type {
77 77   padding: 0;
95 +
96 + & input[type="checkbox"] {
97 + position: unset;
98 + margin: 0;
99 + }
100 +
101 + & label {
102 + margin-right: 1em;
103 + }
78 78  }
79 -a.options-item {
80 - color: inherit;
81 - text-decoration: none;
82 -}
83 -a.options-item:hover {
84 - color: $theme.linkColor;
85 - text-decoration: underline;
86 -}
87 -a.options-item.active, a.options-item.active:hover{
88 - font-weight: bold;
89 - color: $theme.textColor;
90 - text-decoration: none;
91 -}
92 92  
93 93  /**
94 94   * Search Results
... ... @@ -98,12 +98,6 @@
98 98   margin-top: 1em;
99 99  }
100 100  
101 -/* Colibri skin doesn't have the grid system. */
102 -.skin-colibri .search-results-left {
103 - margin: 0.5em 20em 0.5em 0;
104 - padding: 0.5em 0.5em 0.5em 0;
105 -}
106 -
107 107  .search-results {
108 108   padding: .3em 0 .8em 0;
109 109  }
... ... @@ -167,6 +167,8 @@
167 167  
168 168  dl.search-result-highlights > dt {
169 169   margin-top: .3em;
177 + color: var(--text-muted);
178 + font-weight: var(--font-weight-semibold);
170 170  }
171 171  
172 172  blockquote.search-result-highlight {
... ... @@ -187,11 +187,6 @@
187 187   font-weight: bold;
188 188  }
189 189  
190 -dl.search-result-highlights > dt {
191 - color: $theme.textSecondaryColor;
192 - font-weight: normal;
193 -}
194 -
195 195  dl.search-result-highlights.preview dt,
196 196  dl.search-result-highlights.preview dd > * {
197 197   display: none;
... ... @@ -198,17 +198,10 @@
198 198  }
199 199  
200 200  dl.search-result-highlights.preview dt:first-of-type,
201 -dl.search-result-highlights.preview dd:first-of-type blockquote:first-of-type,
202 -/* Workaround for IE8 which doesn't support :first-of-type CSS selector. */
203 -dl.search-result-highlights.preview dt.first,
204 -dl.search-result-highlights.preview dd.first blockquote.first {
205 +dl.search-result-highlights.preview dd:first-of-type blockquote:first-of-type {
205 205   display: block;
206 206  }
207 207  
208 -a.search-result-highlightAll:after {
209 - content: ' \bb';
210 -}
211 -
212 212  .search-result-debug {
213 213   white-space: pre;
214 214  }
... ... @@ -217,40 +217,23 @@
217 217   * Facets
218 218   */
219 219  
220 -.search-facets {
221 - background-color: $theme.backgroundSecondaryColor;
222 - box-shadow: 0 2px 2px rgba(0, 0, 0, 0.2);
223 - /* Leave space for the bottom shadow. */
224 - margin-bottom: 1em;
225 - border-radius: 4px;
217 +.search-facets-header{
218 + border-bottom: 1px solid var(--xwiki-border-color);
226 226  }
227 -/* Colibri skin doesn't have the grid system. */
228 -.skin-colibri .search-facets {
229 - float: right;
230 - max-width: 19.5em;
231 - width: 19.5em;
232 -}
233 233  
234 -.search-facets-header,
235 -.search-facets-actions,
236 -.search-facet {
237 - border-bottom: 1px solid $theme.borderColor;
238 - border-top: 1px solid $theme.pageContentBackgroundColor;
239 - position: relative;
221 +.search-facets-header strong {
222 + font-size: 1.25em;
240 240  }
241 241  
242 242  .search-facets-header,
243 243  .search-facets-actions {
244 - padding: 0.5em 1em;
227 + padding: .5em 0;
245 245  }
229 +
246 246  .search-facet {
247 - padding: 0.5em .8em;
231 + padding: .2em 0;
248 248  }
249 249  
250 -.search-facets-header {
251 - border-top: none;
252 -}
253 -
254 254  .search-facets-header > p,
255 255  .search-facets-actions > p {
256 256   /* The wiki syntax generates paragraphs which have bottom margin. */
... ... @@ -261,16 +261,6 @@
261 261   font-size: .8em;
262 262  }
263 263  
264 -.search-facets-actions a {
265 - color: $theme.textSecondaryColor;
266 - text-decoration: none;
267 -}
268 -
269 -.search-facets-actions a:hover {
270 - color: $theme.linkColor;
271 - text-decoration: underline;
272 -}
273 -
274 274  .search-facets-action-collapseAll,
275 275  .search-facets-action-expandAll {
276 276   float: right;
... ... @@ -282,64 +282,96 @@
282 282   margin: 0;
283 283  }
284 284  
285 -.search-facet:last-of-type {
286 - border-bottom: none;
255 +.search-facet-header, .search-facet-body {
256 + padding-left: .5em;
257 + border: 1px solid var(--xwiki-border-color);
258 + border-radius: var(--border-radius-small);
287 287  }
288 288  
289 289  .search-facet-header {
290 - background: url("$doc.getAttachmentURL('dark-grey-disclosure-arrow-left.png')") no-repeat scroll 100% 50% transparent;
262 + background-color: var(--xwiki-background-secondary-color);
263 +}
264 +
265 +.search-facet-header label {
291 291   color: $theme.titleColor;
292 292   cursor: pointer;
293 - line-height: 1.4em;
294 - margin: 0 .2em;
268 + display: flex;
269 + justify-content: space-between;
270 + align-items: center;
295 295  }
296 296  
297 -.search-facet-header:after {
298 - border-bottom: 1px dotted $theme.pageContentBackgroundColor;
299 - border-top: 1px dotted $theme.borderColor;
300 - clear: both;
301 - content: "";
302 - display: block;
303 - height: 0;
304 - margin: 2.45em 0 0;
273 +.search-facet-body {
274 + opacity: 0;
275 + visibility: hidden; /* This makes sure the element is removed from the accessibility tree. */
276 +
305 305   position: absolute;
306 - right: 0;
307 - top: 0;
308 - width: 100%;
278 + transform: translateY(-10px); /* Start the animation slightly above */
279 + padding-top: .5em;
280 +
281 + border-top-width: 0;
282 + border-top-left-radius: 0;
283 + border-top-right-radius: 0;
309 309  }
310 310  
311 -.search-facet:last-of-type .search-facet-header:after {
312 - border: medium none;
286 +.search-facet-body ul,
287 +.search-facet-body ol {
288 + font-size: .9em;
313 313  }
314 314  
315 -.search-facet.expanded:last-of-type .search-facet-header:after {
316 - border-bottom: 1px dotted $theme.pageContentBackgroundColor;
317 - border-top: 1px dotted $theme.borderColor;
291 +.search-facet-body li {
292 + display: flex;
293 + flex-wrap: wrap;
294 + padding: .3em .5em;
318 318  }
319 319  
320 -.search-facet-body {
321 - overflow: hidden; /* required for effect */
322 - display: none;
323 - margin-top: .5em;
297 +.search-facet .search-facet-header .facet-toggle, button.facet-value-toggle {
298 + background: transparent;
299 + transition: background-color .2s ease-in-out;
324 324  }
325 325  
326 -.search-facet-body ul,
327 -.search-facet-body ol {
328 - font-size: .9em;
302 +.search-facet .search-facet-header .facet-toggle:active, button.facet-value-toggle:active {
303 + box-shadow: unset;
329 329  }
330 330  
331 -.search-facet-body li {
332 - padding: .1em .2em;
306 +.search-facet .search-facet-header .facet-toggle > span, button.facet-value-toggle > span,
307 +.search-facet .search-facet-header .facet-toggle > img, button.facet-value-toggle > img {
308 + transform: rotate(90deg);
333 333  }
334 334  
335 -.search-facet.expanded .search-facet-header {
336 - background-image: url("$doc.getAttachmentURL('dark-grey-disclosure-arrow-down.png')");
311 +.search-facet.expanded .search-facet-header .facet-toggle > span, .expanded > button.facet-value-toggle > span,
312 +.search-facet.expanded .search-facet-header .facet-toggle > img, .expanded > button.facet-value-toggle > img {
313 + transform: rotate(0deg);
337 337  }
338 338  
339 -.search-facet.expanded .search-facet-body {
340 - display: block;
316 +@media (prefers-reduced-motion: no-preference) {
317 + .search-facet .search-facet-header .facet-toggle > span, button.facet-value-toggle > span,
318 + .search-facet .search-facet-header .facet-toggle > img, button.facet-value-toggle > img {
319 + transition: transform 0.2s ease;
320 + }
321 +
322 + .search-facet-body {
323 + transition: opacity 0.3s ease, transform 0.3s ease;
324 + }
341 341  }
342 342  
327 +.search-facet.expanded {
328 + & .search-facet-header {
329 + border-bottom-width: 0;
330 + border-bottom-left-radius: 0;
331 + border-bottom-right-radius: 0;
332 +
333 + & label {
334 + font-weight: var(--font-weight-semibold);
335 + }
336 + }
337 + & .search-facet-body {
338 + opacity: 1;
339 + position: unset; /* This element should be positioned normally when shown. */
340 + visibility: visible;
341 + transform: translateY(0);
342 + }
343 +}
344 +
343 343  .search-facet-body ul, .search-facet-body ul.users {
344 344   color: $theme.textSecondaryColor;
345 345   list-style: none;
... ... @@ -348,12 +348,7 @@
348 348   margin: .5em 0;
349 349  }
350 350  
351 -.search-facet-body li:hover {
352 - background-color: $theme.highlightColor;
353 -}
354 -
355 355  .search-facet-body input[type="checkbox"] {
356 - float: left;
357 357   margin: .2em 0;
358 358  }
359 359  
... ... @@ -366,7 +366,19 @@
366 366   white-space: nowrap;
367 367  }
368 368  
366 +/* Override the default style for links in content. */
367 +body.content.preference-underlining-only-inline-links #xwikicontent .search-facet-body .itemName,
368 +body.content.preference-underlining-only-inline-links #xwikicontent .search-facet-body .more {
369 + text-decoration: none;
370 + /* Make sure we don't take over the default behaviour on hover with too much specificity. */
371 + &:hover, &:focus {
372 + text-decoration: underline;
373 + }
374 +}
375 +
369 369  .search-facet-body .itemName,
377 +.search-facet-body .itemNameempty,
378 +.search-facet-body .facet-value-toggle,
370 370  .search-facet-body .more {
371 371   /* Remove link styling */
372 372   color: $theme.textColor;
... ... @@ -384,8 +384,11 @@
384 384  }
385 385  
386 386  .search-facet-body .itemCount {
387 - float: right;
388 - padding: .1em 0;
396 + padding: .1em .5em;
397 + margin-left: auto;
398 + background-color: var(--nav-link-hover-bg);
399 + /* We want those item count blocks to be pill shaped. */
400 + border-radius: 1em / 50%;
389 389  }
390 390  
391 391  @media (max-width: 768px) {
... ... @@ -405,17 +405,6 @@
405 405  }
406 406  
407 407  /**
408 - * Fix the breadcrumb in Colibri skin.
409 - */
410 -.skin-colibri .breadcrumb > li {
411 - display: inline;
412 -}
413 -.skin-colibri .breadcrumb > li + li:before {
414 - color: $theme.textSecondaryColor;
415 - content: ' \00BB ';
416 -}
417 -
418 -/**
419 419   * Miscellaneous
420 420   */
421 421  
... ... @@ -424,13 +424,3 @@
424 424   padding-left: 0;
425 425  }
426 426  
427 -.paginationFilter .resultsNo,
428 -.paginationFilter .controlPagination,
429 -.paginationFilter .pagination {
430 - line-height: 22px;
431 -}
432 -
433 -.iconRSS {
434 - background: url("$xwiki.getSkinFile('icons/silk/feed.png')") no-repeat scroll 0 0 transparent;
435 - padding-left: 20px;
436 -}