0 Votes

Changes for page Solr Search

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

From version 6.1
edited by Сергей Коршунов
on 2024/05/02 13:36
Change comment: Install extension [org.xwiki.platform:xwiki-platform-search-solr-ui/16.3.0]
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

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();
... ... @@ -37,10 +37,16 @@
37 37   }
38 38   };
39 39  
40 - var addFacetValueCheckbox = function(index) {
36 + var addFacetValueCheckbox = function() {
41 41   // Create an id unique to the facet value.
42 42   let facetContainer = $(this).parents('.search-facet').first();
43 - $(this).attr('id', facetContainer.attr('data-name') + '-' + index.toString());
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)));
44 44   // Initialize the checkbox.
45 45   let checkBox = $(document.createElement('input')).attr('type', 'checkbox');
46 46   checkBox.attr('aria-labelledby', $(this).attr('id'));
... ... @@ -68,7 +68,7 @@
68 68   updateExpandCollapseAllFacetsState(facetsContainer);
69 69  
70 70   // Expand/Collapse toggle for each facet.
71 - facetsContainer.find('.facet-toggler').on('click', function(event) {
73 + facetsContainer.find('.facet-toggle').on('click', function(event) {
72 72   $(event.target).parents('.search-facet').toggleClass('expanded');
73 73   updateExpandCollapseAllFacetsState(facetsContainer);
74 74   });
... ... @@ -83,6 +83,11 @@
83 83   var queryIndex = url.indexOf('?');
84 84   return queryIndex < 0 ? '' : url.substr(queryIndex + 1);
85 85   };
88 +
89 + let removeQueryString = function(url) {
90 + let queryIndex = url.indexOf('?');
91 + return queryIndex < 0 ? url : url.substring(0, queryIndex);
92 + };
86 86  
87 87   var getSearchUIState = function() {
88 88   var expandedFacets = [];
... ... @@ -112,7 +112,9 @@
112 112  
113 113   var searchRequest = null;
114 114  
115 - 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) {
116 116   // If there is a request in progress, abort it to prevent its callback from being called.
117 117   searchRequest && searchRequest.abort();
118 118   $('.search-ui').attr('aria-busy', true);
... ... @@ -125,6 +125,7 @@
125 125   window.history.replaceState && window.history.replaceState(state, document.title);
126 126   // Make sure the browser address bar reflects the new state (and thus the new state can be bookmarked).
127 127   window.history.pushState && window.history.pushState(state, document.title, viewURL);
137 + document.querySelector(changeTargetSelector)?.focus();
128 128   });
129 129   };
130 130  
... ... @@ -131,12 +131,12 @@
131 131   var reloadSearchUI = function(event) {
132 132   event.preventDefault();
133 133   var anchor = $(event.target).closest('a');
134 - anchor.length && $(document).trigger('xwiki:search:update', anchor.attr('href'));
144 + anchor.length && anchor.first().trigger('xwiki:search:update', anchor.attr('href'));
135 135   };
136 136  
137 137   // Others (e.g. a custom facet) can trigger a search UI update by firing this event.
138 138   $(document).on('xwiki:search:update', function(event, viewURL) {
139 - pushSearchUIState(viewURL);
149 + pushSearchUIState(viewURL, `[data-facetvalue='${CSS.escape(event.target.dataset.facetvalue)}']`);
140 140   });
141 141  
142 142   $(window).on('popstate', function(event) {
... ... @@ -176,10 +176,41 @@
176 176   });
177 177  
178 178   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 +
179 179   $('.search-result-highlightAll').each(enhanceSearchResultHighlights);
180 180   $('.search-facets').each(enhanceSearchFacets);
181 181   $([
182 - '.search-results-sort a.sort-item',
183 183   '.search-options a.options-item',
184 184   '.pagination a',
185 185   '.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: 7px;
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,48 +282,37 @@
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 {
262 + background-color: var(--xwiki-background-secondary-color);
263 +}
264 +
265 +.search-facet-header label {
290 290   color: $theme.titleColor;
291 291   cursor: pointer;
292 - line-height: 1.4em;
293 - margin: 0 .2em;
294 294   display: flex;
295 295   justify-content: space-between;
296 - position: relative;
270 + align-items: center;
297 297  }
298 298  
299 -.search-facet-header:after {
300 - border-bottom: 1px dotted $theme.pageContentBackgroundColor;
301 - border-top: 1px dotted $theme.borderColor;
302 - clear: both;
303 - content: "";
304 - display: block;
305 - height: 0;
273 +.search-facet-body {
274 + opacity: 0;
275 + visibility: hidden; /* This makes sure the element is removed from the accessibility tree. */
276 +
306 306   position: absolute;
307 - right: 0;
308 - bottom: 0;
309 - 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;
310 310  }
311 311  
312 -.search-facet:last-of-type .search-facet-header:after {
313 - border: medium none;
314 -}
315 -
316 -.search-facet.expanded:last-of-type .search-facet-header:after {
317 - border-bottom: 1px dotted $theme.pageContentBackgroundColor;
318 - border-top: 1px dotted $theme.borderColor;
319 -}
320 -
321 -.search-facet-body {
322 - overflow: hidden; /* required for effect */
323 - display: none;
324 - margin-top: .5em;
325 -}
326 -
327 327  .search-facet-body ul,
328 328  .search-facet-body ol {
329 329   font-size: .9em;
... ... @@ -332,37 +332,55 @@
332 332  .search-facet-body li {
333 333   display: flex;
334 334   flex-wrap: wrap;
335 - padding: .1em .2em;
294 + padding: .3em .5em;
336 336  }
337 337  
338 -.search-facet .search-facet-header .facet-toggler, button.facet-value-toggler {
297 +.search-facet .search-facet-header .facet-toggle, button.facet-value-toggle {
339 339   background: transparent;
340 340   transition: background-color .2s ease-in-out;
341 341  }
342 342  
343 -.search-facet .search-facet-header .facet-toggler:active, button.facet-value-toggler:active {
302 +.search-facet .search-facet-header .facet-toggle:active, button.facet-value-toggle:active {
344 344   box-shadow: unset;
345 345  }
346 346  
347 -.search-facet .search-facet-header .facet-toggler > span, button.facet-value-toggler > span,
348 -.search-facet .search-facet-header .facet-toggler > img, button.facet-value-toggler > img {
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 {
349 349   transform: rotate(90deg);
350 350  }
351 351  
352 -.search-facet.expanded .search-facet-header .facet-toggler > span, .expanded > button.facet-value-toggler > span,
353 -.search-facet.expanded .search-facet-header .facet-toggler > img, .expanded > button.facet-value-toggler > img {
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 {
354 354   transform: rotate(0deg);
355 355  }
356 356  
357 -@media not (prefers-reduced-motion) {
358 - .search-facet .search-facet-header .facet-toggler > span, button.facet-value-toggler > span,
359 - .search-facet .search-facet-header .facet-toggler > img, button.facet-value-toggler > img {
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 {
360 360   transition: transform 0.2s ease;
361 361   }
321 +
322 + .search-facet-body {
323 + transition: opacity 0.3s ease, transform 0.3s ease;
324 + }
362 362  }
363 363  
364 -.search-facet.expanded .search-facet-body {
365 - display: block;
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 + }
366 366  }
367 367  
368 368  .search-facet-body ul, .search-facet-body ul.users {
... ... @@ -373,10 +373,6 @@
373 373   margin: .5em 0;
374 374  }
375 375  
376 -.search-facet-body li:hover {
377 - background-color: $theme.highlightColor;
378 -}
379 -
380 380  .search-facet-body input[type="checkbox"] {
381 381   margin: .2em 0;
382 382  }
... ... @@ -401,7 +401,8 @@
401 401  }
402 402  
403 403  .search-facet-body .itemName,
404 -.search-facet-body .facet-value-toggler,
377 +.search-facet-body .itemNameempty,
378 +.search-facet-body .facet-value-toggle,
405 405  .search-facet-body .more {
406 406   /* Remove link styling */
407 407   color: $theme.textColor;
... ... @@ -419,8 +419,11 @@
419 419  }
420 420  
421 421  .search-facet-body .itemCount {
422 - padding: .1em 0;
396 + padding: .1em .5em;
423 423   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%;
424 424  }
425 425  
426 426  @media (max-width: 768px) {
... ... @@ -440,17 +440,6 @@
440 440  }
441 441  
442 442  /**
443 - * Fix the breadcrumb in Colibri skin.
444 - */
445 -.skin-colibri .breadcrumb > li {
446 - display: inline;
447 -}
448 -.skin-colibri .breadcrumb > li + li:before {
449 - color: $theme.textSecondaryColor;
450 - content: ' \00BB ';
451 -}
452 -
453 -/**
454 454   * Miscellaneous
455 455   */
456 456  
... ... @@ -459,13 +459,3 @@
459 459   padding-left: 0;
460 460  }
461 461  
462 -.paginationFilter .resultsNo,
463 -.paginationFilter .controlPagination,
464 -.paginationFilter .pagination {
465 - line-height: 22px;
466 -}
467 -
468 -.iconRSS {
469 - background: url("$xwiki.getSkinFile('icons/silk/feed.png')") no-repeat scroll 0 0 transparent;
470 - padding-left: 20px;
471 -}