1import { Controller } from 'stimulus'; 2 3export default class FilterController extends Controller { 4 static targets = [ 5 'form', 6 'fieldset', 7 'sorting', 8 'list', 9 'listEmpty', 10 'type', 11 'pid', 12 'sorting', 13 'sortingSelect', 14 'facet', 15 'loadMoreInformation', 16 'loadMoreContainer', 17 'searchfield', 18 'suggestionlist', 19 'activeFilterQuantity', 20 'overlay', 21 'atoz', 22 'searchType', 23 'overlayToggle', 24 'closeOverlay', 25 'additionalSolrQ', 26 'additionalFacetTypes', 27 ] 28 29 params = {} 30 fullTextSearch = '' 31 type = '' 32 page = 1 33 sort = '' 34 dateRange = '' 35 dateRangeRaw = '' 36 suggestionlistOnLoad = false 37 lastLetter = '' 38 hasAtoz = '' 39 searchType = '' 40 searchInput = '' 41 lastChangedType= '' 42 filterNumber = 0 43 44 functionsAfterListFetch = {} 45 46 filterDataObject = {} 47 48 static values = { 49 showOverlay: Boolean, 50 } 51 52 /** 53 * will be called once on initialization 54 */ 55 connect() { 56 super.connect() 57 this.listEmptyTarget.style.display = 'none' 58 this.type = this.hasTypeTarget ? this.typeTarget.value : '' 59 this.sort = this.hasSortingSelectTarget 60 ? this.sortingSelectTarget.value 61 : '' 62 this.hasAtoz = this.hasAtozTarget ? '1' : '' 63 this.searchType = this.hasSearchTypeTarget 64 ? this.searchTypeTarget.value 65 : 'all' 66 67 if (document.querySelector('[data-type-from="start_dateS"]')) { 68 this.dateRange = this.setDefaultTimeRange() 69 } else { 70 this.dateRange = '' 71 } 72 this.updateParams() 73 this.fetchListItems() 74 75 if (this.hasSuggestionlistTarget) { 76 this.searchInput = document.querySelector('[aria-controls="' + this.suggestionlistTarget.getAttribute('id') + '"]'); 77 } 78 79 window.FnnSolrFilterController = this; 80 } 81 82 /** 83 * handles type changes (facets) 84 * 85 * @param event 86 */ 87 onFacetChange(event) { 88 for (let i in this.facetTargets) { 89 this.facetTargets[i].classList.remove('active') 90 } 91 92 if (event.target.classList.contains('active')) { 93 event.target.classList.remove('active') 94 this.type = '' 95 } else { 96 event.target.classList.add('active') 97 this.type = event.target.value 98 } 99 100 this.page = 1 101 this.fetchListItems() 102 } 103 104 /** 105 * handles change events on input change 106 * 107 * @param event 108 */ 109 onChange(event) { 110 let name = event.target.name 111 let value = event.target.value 112 let attribute = event.target.hasAttribute('data-type') 113 ? event.target.getAttribute('data-type') 114 : '' 115 let conjunction = event.target.hasAttribute('data-conjunction') 116 ? event.target.getAttribute('data-conjunction') 117 : '' 118 let isFacet = event.target.hasAttribute('data-is-facet') 119 ? (event.target.getAttribute('data-is-facet') === '1' ? true : false) 120 : false 121 this.params = { 122 ...this.params, 123 [name]: { 124 attribute: attribute, 125 conjunction: conjunction, 126 isFacet: isFacet, 127 values: this.params[name]?.values 128 ? this.params[name].values 129 : [], 130 }, 131 } 132 133 switch (event.target.type) { 134 case 'checkbox': 135 if (event.target.checked) { 136 this.params[name].values = [ 137 ...this.params[name].values, 138 value, 139 ] 140 } else { 141 let index = this.params[name].values.indexOf(value) 142 if (index > -1) this.params[name].values.splice(index, 1) 143 this.lastLetter = '' 144 } 145 this.lastChangedType = event.target.dataset.type 146 break 147 case 'button': 148 if (event.target.classList.contains('active')) { 149 event.target.classList.remove('active') 150 delete this.params[name] 151 this.lastLetter = '' 152 } else { 153 event.target.classList.add('active') 154 this.params[name].values = value 155 } 156 break 157 default: 158 this.params[name].values = value 159 break 160 } 161 162 this.page = 1 163 this.fetchListItems() 164 } 165 166 /** 167 * handles load more items 168 */ 169 onLoadMore() { 170 this.page++ 171 this.fetchListItems(true) 172 } 173 174 /** 175 * handles the q free text search changes 176 * 177 * @param event 178 */ 179 onSearchChange(event) { 180 this.fullTextSearch = event.target.value 181 // If at least 4 characters are specified in the search f
181ield, the suggestion list is retrieved. 182 if (event.target.value.length >= 4 && !this.suggestionlistOnLoad) { 183 let suggestionUri = event.target.hasAttribute('data-uri') 184 ? event.target.getAttribute('data-uri') 185 : '' 186 let withKeywords = event.target.hasAttribute( 187 'data-suggestion-with-keywords', 188 ) 189 ? event.target.getAttribute('data-suggestion-with-keywords') 190 : '0' 191 let documentAsTerm = event.target.hasAttribute( 192 'data-suggestion-document-as-term', 193 ) 194 ? event.target.getAttribute('data-suggestion-document-as-term') 195 : '0' 196 let suggestionLabel = event.target.hasAttribute( 197 'data-suggestion-label', 198 ) 199 ? event.target.getAttribute('data-suggestion-label') 200 : 'Suggestions' 201 if (suggestionUri !== '') { 202 this.suggestionlistOnLoad = true 203 this.fetchSuggestions( 204 suggestionUri, 205 event.target.value, 206 withKeywords, 207 documentAsTerm, 208 suggestionLabel, 209 ) 210 } else { 211 console.warn( 212 'No URL was found to retrieve the suggestion list. Check the "data-uri" attribute on "#' + 213 event.target.getAttribute('id') + 214 '"!', 215 ) 216 } 217 } 218 } 219 220 /** 221 * handles the clicks on suggestion term 222 * 223 * @param event 224 */ 225 onTermClick(event) { 226 this.page = 1 227 this.fullTextSearch = event.target.hasAttribute('data-suggestion-term') 228 ? event.target.getAttribute('data-suggestion-term') 229 : '' 230 this.searchfieldTarget.value = this.fullTextSearch 231 this.fetchListItems() 232 } 233 234 /** 235 * handles date range changes 236 */ 237 onDateRangeChange(event) { 238 this.dateRangeRaw = event.target.name + '#' + event.target.value 239 let dateParts = event.target.value.split(' - ') ?? [null, null] 240 let start = '*' 241 let end = '*' 242 let attributeFrom = event.target.hasAttribute('data-type-from') 243 ? event.target.getAttribute('data-type-from') 244 : '' 245 let attributeTo = event.target.hasAttribute('data-type-to') 246 ? event.target.getAttribute('data-type-to') 247 : '' 248 249 // start date 250 if (dateParts[0] !== 'null') { 251 start = new Date(dateParts[0]) 252 start = 253 start.getFullYear() + 254 '-' + 255 (start.getMonth() + 1) + 256 '-' + 257 start.getDate() + 258 'T00:00:00Z' 259 } 260 261 // end date 262 if (dateParts[1] !== 'null') { 263 end = new Date(dateParts[1]) 264 end = 265 end.getFullYear() + 266 '-' + 267 (end.getMonth() + 1) + 268 '-' + 269 end.getDate() + 270 'T23:59:59Z' 271 } 272 273 if (attributeFrom === attributeTo) { 274 this.dateRange = attributeFrom + ':[' + start + ' TO ' + end + ']' 275 } else { 276 this.dateRange = 277 '(' + 278 attributeFrom + 279 ':[* TO ' + 280 end + 281 '] AND ' + 282 attributeTo + 283 ':[' + 284 start + 285 ' TO *])' 286 } 287 288 this.page = 1 289 this.fetchListItems() 290 } 291 292 setDefaultTimeRange() { 293 let start = new Date(); 294 start = 295 start.getFullYear() + 296 '-' + 297 (start.getMonth() + 1) + 298 '-' + 299 start.getDate() + 300 'T00:00:00Z' 301 return '(start_dateS:[* TO *] AND end_dateS:[' + start + ' TO *])' 302 } 303 304 /** 305 * handled sorting changes 306 * 307 * @param event 308 */ 309 onSortingChange(event) { 310 this.page = 1 311 this.sort = event.target.value 312 this.fetchListItems() 313 } 314 315 /** 316 * handles submit click on button and enter 317 */ 318 doSearch() { 319 this.page = 1 320 this.fetchListItems() 321 } 322 323 /** 324 * handles the frequently searched words 325 * 326 * @param event 327 */ 328 onFrequentlySearched(event) { 329 this.page = 1 330 this.fullTextSearch = event.target.value 331 this.searchfieldTarget.value = event.target.value 332 this.fetchListItems() 333 } 334 335 /** 336 * handles reset form click on button 337 */ 338 doReset() { 339 this.params = {} 340 this.fullTextSearch = '' 341 this.page = 1 342 if (document.querySelector('[data-type-from="start_dateS"]')) { 343 this.dateRange = this.setDefaultTimeRange() 344 } else { 345 this.dateRange = '' 346 } 347 this.dateRangeRaw = '' 348 this.lastLetter = '' 349 this.type = this.hasTypeTarget ? this.typeTarget.value : '' 350 for (let i in this.facetTargets) { 351 this.facetTargets[i].classList.remove('active') 352 } 353 this.formTarget.reset() 354 this.fetchListItems() 355 } 356 357 /**
358 * Reset all checkboxes on single multiselect 359 * @param event 360 */ 361 doMultiselectReset(event) { 362 let multiSelect = event.currentTarget.closest('.powermail-select-container'); 363 if (multiSelect === null) return; 364 365 let checkboxes = multiSelect.querySelectorAll('input[type="checkbox"]:checked'); 366 if (checkboxes.length === 0) return; 367 368 let checkboxName= ''; 369 checkboxes.forEach(checkbox => { 370 checkboxName = checkbox.name; 371 checkbox.checked = false; 372 }); 373 delete this.params[checkboxName]; 374 375 this.page = 1 376 this.fetchListItems() 377 } 378 379 /** 380 * The facet options in the filter are updated based on the transferred fact objects. 381 * This affects the number of elements found and, if applicable, 382 * the activation and deactivation of the filter option 383 * @param facets 384 */ 385 updateFacetOptions(facets) { 386 // update options 387 let options = document.querySelectorAll('[data-facet-option]'); 388 options.forEach((element) => { 389 let facetName = element.dataset.type 390 let option = element.dataset.facetOption 391 let elementRoot = element.closest('div.checkbox') 392 if (elementRoot === null) elementRoot = element.closest('li.radio') 393 394 if (facets[facetName] !== undefined && facets[facetName].options[option] !== undefined && parseInt(facets[facetName].options[option].quantity) > 0) { 395 elementRoot.classList.remove('deactivated') 396 elementRoot.querySelector('.option-quantity').textContent = parseInt(facets[facetName].options[option].quantity) 397 element.disabled = false 398 } else { 399 elementRoot.classList.add('deactivated') 400 elementRoot.querySelector('.option-quantity').textContent = 0 401 element.disabled = true 402 } 403 }); 404 // update multiselects 405 let multiSelectsOptions = document.querySelectorAll('.powermail_fieldwrap_type_select.multi-select .powermail-select-single-wrap ul.powermail_field li.radio:first-child [data-facet-option]'); 406 multiSelectsOptions.forEach((element) => { 407 let multiSelect = element.closest('.powermail_field.powermail-select-container') 408 if (multiSelect.querySelectorAll('input[data-facet-option]:not([disabled])').length === 0) { 409 multiSelect.classList.add('deactivated') 410 } else { 411 multiSelect.classList.remove('deactivated') 412 } 413 }); 414 } 415 416 executeFunctionsAfterListFetch() { 417 let keys = Object.keys(this.functionsAfterListFetch) 418 keys.forEach((key) => window[key]()) 419 } 420 421 /** 422 * Builds the record-type filter shared by result and suggestion requests. 423 * 424 * @returns {string} 425 */ 426 getTypeFilter() { 427 if (!this.hasTypeTarget && !this.type) { 428 return '' 429 } 430 431 let typeFilter = 'altType_stringS:' + this.type 432 433 if ( 434 this.hasAdditionalFacetTypesTarget 435 && this.additionalFacetTypesTarget.value !== '' 436 ) { 437 const additionalFacetTypes = 438 this.additionalFacetTypesTarget.value.split(',') 439 440 additionalFacetTypes.forEach( 441 (facetType) => 442 (typeFilter += ' OR altType_stringS:' + facetType.trim()), 443 ) 444 445 typeFilter = '(' + typeFilter + ')' 446 } 447 448 return typeFilter 449 } 450 451 /** 452 * Keeps structural filters out of tx_solr[q]. Newer ext-solr versions 453 * escape field, grouping and range syntax at the user-input boundary. 454 * 455 * @param data 456 * @param typeFilter 457 * @param queryFilter 458 * @param dateRangeFilter 459 */ 460 addScopeFilters( 461 data, 462 typeFilter, 463 queryFilter = null, 464 dateRangeFilter = null, 465 ) { 466 if (typeFilter !== '') { 467 data['tx_solr[additionalFilters][fnnTypeScope]'] = typeFilter 468 } 469 470 if (queryFilter !== null && queryFilter !== '') {
471 data['tx_solr[additionalFilters][fnnQueryScope]'] = queryFilter 472 } 473 474 if (dateRangeFilter !== null && dateRangeFilter !== '') { 475 data['tx_solr[additionalFilters][fnnDateRange]'] = dateRangeFilter 476 } 477 } 478 479 /** 480 * fetches the results 481 */ 482 fetchListItems(append = false) { 483 // hide inputs and add loading classes 484 this.toggleFieldset(true) 485 this.toggleList(true) 486 // solr-url 487 let url = new URL(this.formTarget.getAttribute('data-uri')) 488 // Disable the cache, we need a fresh list of items. 489 let headers = new Headers() 490 headers.append('Pragma', 'no-cache') 491 headers.append('Cache-Control', 'no-cache') 492 493 if (!append) { 494 this.lastLetter = '' 495 } 496 497 // preset data search object 498 let data = { 499 'tx_solr[page]': this.page, 500 'tx_solr[sort]': this.sort !== undefined ? this.sort : '', 501 'tx_solr[lastLetter]': this.lastLetter, 502 'tx_solr[hasAtoz]': this.hasAtoz, 503 'tx_solr[searchType]': this.searchType, 504 } 505 506 // reset filterNumber for new search 507 this.filterNumber = 0 508 509 // set filter 510 const typeFilter = this.getTypeFilter() 511 if (typeFilter !== '') { 512 data['tx_solr[filter][' + this.filterNumber + ']'] = typeFilter 513 this.filterNumber++ 514 } 515 516 // generate filter options for facets (tx_solr[filter][*]) 517 this.generateFilters(data) 518 519 // Render non-facet parameters as an untagged filter query. 520 const queryFilter = this.renderQString([]) 521 522 // Keep tx_solr[q] limited to the user's search term. To preserve the 523 // legacy any-term behavior, configure search.query.minimumMatch = 1. 524 if (this.fullTextSearch.length > 0) { 525 data['tx_solr[q]'] = 526 '*' + 527 this.escapeSolrSearchTerm(this.fullTextSearch.trim()) + 528 '*' 529 } 530 531 // handle storage pids 532 if (this.hasPidTarget) { 533 let pidList = this.pidTarget.value.split(',') 534 data['tx_solr[additionalFilters][pids]'] = 535 '+(*:* AND pid:(' + pidList.join(' OR ') + '))' 536 } 537 538 this.addScopeFilters(data, typeFilter, queryFilter, this.dateRange) 539 540 // update filter counter 541 if (this.hasActiveFilterQuantityTarget) { 542 if (Object.keys(this.params).length > 0) { 543 this.activeFilterQuantityTarget.parentNode.style.removeProperty('display') 544 this.activeFilterQuantityTarget.innerHTML = Object.keys( 545 this.params, 546 ).length 547 } else { 548 this.activeFilterQuantityTarget.parentNode.style.display = 'none' 549 } 550 } 551 552 // make formdata from data object 553 let formData = new FormData() 554 Object.keys(data).forEach((key) => formData.append(key, data[key])) 555 556 // fetch the request 557 fetch(url, { 558 method: 'POST', 559 headers: headers, 560 body: formData, 561 }) 562 .then((response) => response.text()) 563 .then((html) => { 564 let parser = new DOMParser() 565 let doc = parser.parseFromString(html, 'text/html') 566 let facetsString = doc.querySelector('facets') 567 if (facetsString !== null) { 568 let facets = JSON.parse(facetsString.textContent) 569 this.updateFacetOptions(facets) 570 } 571 let informationString = doc.querySelector('information') 572 if (informationString !== null && informationString != '' && this.hasLoadMoreInformationTarget) { 573 this.loadMoreInformationTarget.innerText = informationString.textContent 574 } 575 let element = doc.querySelector('.results-list') 576 if (element) { 577 // calculate next page behaviour for page browser 578 if ( 579 Math.ceil(element.getAttribute('data-pages')) > 580 this.page 581 ) { 582 this.loadMoreContainerTarget.style.removeProperty( 583 'display', 584 ) 585 } else { 586 this.loadMoreContainerTarget.style.display = 'none' 587 } 588 589 // get atoz filter array 590 if (this.hasAtozTarget) { 591 if (element.getAttribute('data-firstletters-count')) { 592 let letterCounts = JSON.parse( 593 element.getAttribute('data-firstletters-count'), 594 ) 595 if (letterCounts !== null) { 596 for (const key in this.atozTargets) { 597 this.atozTargets[key].disabled = 598 letterCounts[ 599 this.atozTargets[key].value 600 ] === undefined 601 } 602 } 603 } 604 } 605 606 // Remember last letter to create letter headings in lists 607 if ( 608 doc.querySelector( 609 '.results-list [data-last-letter]:last-child', 610 ) !== null 611 ) { 612 this.lastLetter = doc 613 .querySelector( 614 '.results-list [data-last-letter]:last-child', 615 ) 616 .getAttribute('data-last-letter') 617 } 618 619 // append data items if set 620 if (append) { 621 while (element.firstChild) { 622 this.listTarget.appendChild(element.firstChild) 623 } 624 } else { 625 this.listTarget.innerHTML = element.innerHTML 626 } 627 628 this.listEmptyTarget.style.display = 'none' 629 if (this.hasSortingTarget) { 630 this.sortingTarget.style.removeProperty('display') 631 } 632 } else { 633 this.listTarget.innerHTML = '' 634 this.listEmptyTarget.style.display = 'block' 635 this.sortingTarget.style.display = 'none' 636 } 637 638 if (typeof window.FnnStimulus.controllers[1] !== 'undefined' && typeof window.FnnStimulus.controllers[1].refreshBookmarkHighlight === "function") { 639 window.FnnStimulus.controllers[1].refreshBookmarkHighlight() 640 } 641 642 // activate inputs and remove loading classes 643 this.toggleFieldset(false) 644 this.toggleList(false) 645 this.executeFunctionsAfterListFetch() 646 }) 647 648 this.updateUrl() 649 } 650 651 /** 652 * fetches the suggestions 653 */ 654 fetchSuggestions(url, term, withKeywords, documentAsTerm, suggestionLabel) { 655 // Disable the cache, we need a fresh list of items. 656 let headers = new Headers() 657 headers.append('Pragma', 'no-cache') 658 headers.append('Cache-Control', 'no-cache') 659 660 // preset data search object 661 let data = {} 662 data['tx_solr[queryString]'] = term 663 664 // set initial suggest parameters 665 const typeFilter = this.getTypeFilter() 666 if (typeFilter !== '') { 667 data['tx_solr[filter][0]'] = typeFilter 668 } 669 670 // handle storage pids 671 if (this.hasPidTarget) { 672 let pidList = this.pidTarget.value.split(',') 673 data['tx_solr[additionalFilters][pids]'] = 674 '+(*:* AND pid:(' + pidList.join(' OR ') + '))' 675 } 676 677 // SuggestController applies HTML escaping to additional filters. 678 // Escape whitespace instead of quoting multi-word field values. 679 const queryFilter = this.renderQString(null, false) 680 this.addScopeFilters(data, typeFilter, queryFilter) 681 682 // make formdata from data object 683 let formData = new FormData()
684 Object.keys(data).forEach((key) => formData.append(key, data[key])) 685 686 // fetch the suggestions 687 fetch(url, { 688 method: 'POST', 689 headers: headers, 690 body: formData, 691 }) 692 .then((response) => response.text()) 693 .then((html) => { 694 data = JSON.parse(html) 695 // get status result 696 if (data.hasOwnProperty('status') && !data.status) { 697 this.suggestionlistOnLoad = false 698 return 699 } 700 let content = '' 701 702 // Create links for documents 703 let controller = this 704 let documentCounter = 1; 705 data.documents.forEach(function (document) { 706 if (documentAsTerm === '0') { 707 content += 708 '<li id="suggestion-list-document-' + documentCounter + '"><a href="' + 709 document.link + 710 '" class="suggestion-list-item suggestion-list-type-' + 711 document.type + 712 '">' + 713 controller.buildTextWithHighlightetdTerm( 714 document.title, 715 term, 716 ) + 717 '</a></li>' 718 } else { 719 content += 720 '<button id="suggestion-list-document-' + documentCounter + '"><button role="button" class="suggestion-list-item" data-action="filter#onTermClick" data-suggestion-term="' + 721 document.title.slice(0, 28) + 722 '">' + 723 controller.buildTextWithHighlightetdTerm( 724 document.title, 725 term, 726 ) + 727 '</button></li>' 728 } 729 documentCounter++; 730 }) 731 732 // Create suggestions 733 if (withKeywords === '1') { 734 let suggestionCounter = 0; 735 content += 736 '<span class="suggestion-list-terms">' + 737 suggestionLabel + 738 '</span>' 739 Object.keys(data.suggestions).forEach( 740 (key) => 741 (content += 742 '<li id="suggestion-list-suggestion-' + (++suggestionCounter) + '"><button role="button" class="suggestion-list-item" data-action="filter#onTermClick" data-suggestion-term="' + 743 key.slice(0, 28) + 744 '">' + 745 this.buildTextWithHighlightetdTerm(key, term) + 746 '</button></li>'), 747 ) 748 } 749 // set content 750 this.suggestionlistTarget.innerHTML = content 751 this.suggestionlistTarget.classList.add('is-open') 752 753 this.searchInput.setAttribute('aria-expanded', 'true'); 754 const searchInput = this.searchInput; 755 756 let suggestItems = document.querySelectorAll('.suggestion-list-item'); 757 for (let suggestItem of suggestItems) { 758 suggestItem.addEventListener('focusin', function (event) { 759 searchInput.setAttribute('aria-activedescendant', suggestItem.parentNode.getAttribute('id')); 760 }); 761 762 suggestItem.addEventListener('focusout', function (event) { 763 searchInput.removeAttribute('aria-activedescendant'); 764 }); 765 } 766 767 this.suggestionlistOnLoad = false 768 }) 769 } 770 771 /** 772 * Special characters must be escaped with a backslash for the search. 773 * https://solr.apache.org/guide/7_3/the-standard-query-parser.html#escaping-special-characters 774 * + - && || ! ( ) { } [ ] ^ " ~ * ? : / 775 * 776 * @param term 777 * @returns {string} 778 */ 779 escapeSolrSearchTerm(term) { 780 let escaped = term 781 const toEscaped = [ 782 '+',
783 '-', 784 '&&', 785 '||', 786 '!', 787 '(', 788 ')', 789 '{', 790 '}', 791 '[', 792 ']', 793 '^', 794 '"', 795 '~', 796 '*', 797 '?', 798 ':', 799 '/', 800 ] 801 toEscaped.forEach( 802 (element) => (escaped = escaped.replace(element, '\\' + element)), 803 ) 804 805 return escaped 806 } 807 808 /** 809 * Searches for the search term in the passed text and highlights it. 810 * In addition, the return value it is truncated to 28 characters. 811 * 812 * @param text 813 * @param term 814 * @returns {string} 815 */ 816 buildTextWithHighlightetdTerm(text, term) { 817 let cropped = text.slice(0, 28) 818 let firstStart = cropped.toUpperCase().indexOf(term.toUpperCase()) 819 return ( 820 cropped.substring(0, firstStart) + 821 '<span class="highlight">' + 822 cropped.substring(firstStart, firstStart + term.length) + 823 '</span>' + 824 cropped.substring(firstStart + term.length) 825 ) 826 } 827 828 /** 829 * renders the q search string for solr 830 * 831 * @param data {array|null} 832 * @param quoteValues {boolean} 833 * @returns {string} 834 */ 835 renderQString(data, quoteValues = true) { 836 let qStringPartsString = [] 837 for (const name in this.params) { 838 // All options that are not facets must be transferred as a filter query. 839 if (!this.params[name].isFacet) { 840 let qStringPartsArray = [] 841 for (const value in this.params[name].values) { 842 const filterValue = quoteValues 843 ? '"' + this.params[name].values[value] + '"' 844 : this.params[name].values[value].replace(/\s/g, '\\ ') 845 846 qStringPartsArray.push( 847 this.params[name].attribute + 848 ':' + 849 filterValue, 850 ) 851 } 852 if (qStringPartsArray.length > 0) { 853 qStringPartsString.push( 854 '(' + 855 qStringPartsArray.join( 856 ' ' + this.params[name].conjunction + ' ', 857 ) + 858 ')', 859 ) 860 } 861 } 862 } 863 if (data !== null && qStringPartsString.length > 0) { 864 qStringPartsString.forEach((element) => { 865 data['tx_solr[filter][' + this.filterNumber + ']'] = element 866 this.filterNumber++ 867 }) 868 } 869 870 if (this.hasAdditionalSolrQTarget) { 871 return qStringPartsString.length > 0 872 ? '(' + qStringPartsString.join(' AND ') + ') AND ' + this.additionalSolrQTarget.value 873 : this.additionalSolrQTarget.value 874 } 875 876 return qStringPartsString.length > 0 877 ? '(' + qStringPartsString.join(' AND ') + ')' 878 : null 879 } 880 881 /** 882 * generate solr search filter options for facets 883 * 884 * @param data {array} 885 */ 886 generateFilters(data) { 887 for (const name in this.params) { 888 // Only facets are considered as filters 889 if (this.params[name].isFacet) { 890 let qStringPartsArray = [] 891 for (const value in this.params[name].values) { 892 qStringPartsArray.push( 893 this.params[name].attribute + 894 ':' + 895 this.params[name].values[value] + '', 896 ) 897 } 898 qStringPartsArray.forEach((value) => { 899 data['tx_solr[filter][' + this.filterNumber + ']'] = value 900 this.filterNumber++ 901 }) 902 } 903 } 904 } 905 906 /** 907 * disabled all fieldset to prevent input changes while loading 908 * 909 * @param disable 910 */ 911 toggleFieldset(disable) { 912 for (let key in this.fieldsetTargets) { 913 if (disable) { 914 this.fieldsetTargets[key].setAttribute('disabled', '') 915 } else { 916 this.fieldsetTargets[key].removeAttribute('disabled') 917 } 918 } 919 } 920 921 /** 922 * add classes to show loading animation and disable list while loading 923 * 924 * @param disable 925 */ 926 toggleList(disable) { 927 if (disable) { 928 this.listTarget.classList.add( 929 'disabled-by-filter', 930 'loading-by-filter', 931 ) 932 this.listTarget.setAttribute('disabled', '') 933 } else { 934 this.listTarget.classList.remove( 935 'disabled-by-filter', 936 'loading-by-filter', 937 ) 938 this.listTarget.removeAttribute('disabled') 939 } 940 } 941 942 /** 943 * handles the overlay open 944 */ 945 openOverlay() { 946 /* enable scroll lock by css */ 947 document.getElementsByTagName('html')[0].classList.add('no-scroll') 948 this.overlayTarget.classList.add('is-open') 949 this.showOverlayValue = true
950 this.closeOverlayTarget.focus() 951 $(this.overlayTarget).focusTrap() /* trap focus in overlay */ 952 } 953 954 /** 955 * handles the overlay close 956 */ 957 closeOverlay() { 958 /* disable scroll lock by css */ 959 document.getElementsByTagName('html')[0].classList.remove('no-scroll') 960 961 this.showOverlayValue = false 962 this.overlayTarget.classList.remove('is-open') 963 $(this.overlayTarget).focusTrap('untrap') /* release the focus trap */ 964 this.overlayToggleTarget.focus() /* focus button that toggles the overlay */ 965 } 966 967 windowKeyHandler(event) { 968 if (this.showOverlayValue) { 969 // ESCAPE key pressed 970 if (event.keyCode === 27) { 971 this.closeOverlay() 972 } 973 } 974 } 975 976 /** 977 * closes suggestions list on window click 978 * 979 * @param event 980 */ 981 windowClick(event) { 982 // Hides the suggestion list when clicked outside the search box. 983 if (this.hasSearchfieldTarget && this.hasSuggestionlistTarget) { 984 if (this.searchfieldTarget.contains(event.target) === false) { 985 this.suggestionlistTarget.classList.remove('is-open') 986 this.searchInput.setAttribute('aria-expanded', 'false'); 987 this.searchInput.removeAttribute('aria-activedescendant'); 988 this.suggestionlistTarget.innerHTML = '' 989 } else { 990 this.fullTextSearch = this.searchfieldTarget.value 991 this.onSearchChange(event) 992 } 993 } 994 } 995 996 /** 997 * handles the url update if filter settings changed 998 */ 999 updateUrl() { 1000 let url = new URL(window.location.href) 1001 let filter = btoa( 1002 JSON.stringify({ 1003 params: this.params, 1004 fullTextSearch: this.fullTextSearch, 1005 //page: this.page, // With "load more" and page reload this leads to problems (lists do not start with page 1) 1006 type: this.type, 1007 dateRange: this.dateRange, 1008 dateRangeRaw: this.dateRangeRaw, 1009 sort: this.sort, 1010 lastLetter: this.lastLetter, 1011 hasAtoz: this.hasAtoz, 1012 }), 1013 ) 1014 url.searchParams.set('filter', filter) 1015 window.history.pushState(null, null, url) 1016 } 1017 1018 /** 1019 * handles the filter params get by url search parameter 1020 */ 1021 updateParams() { 1022 let url = new URL(window.location.href) 1023 if (url.searchParams.get('filter')) { 1024 let filter = JSON.parse(atob(url.searchParams.get('filter'))) 1025 if (Object.keys(filter).length > 0) { 1026 this.params = filter.params 1027 this.fullTextSearch = filter.fullTextSearch 1028 //this.page = filter.page; // With "load more" and page reload this leads to problems (lists do not start with page 1) 1029 this.type = filter.type 1030 this.dateRange = filter.dateRange 1031 this.dateRangeRaw = filter.dateRangeRaw 1032 this.sort = filter.sort 1033 this.lastLetter = filter.lastLetter 1034 this.hasAtoz = filter.hasAtoz 1035 1036 // update sorting selector 1037 if (this.hasSortingSelectTarget) { 1038 this.sortingSelectTarget.value = this.sort 1039 } 1040 1041 // update fulltext search 1042 if (this.hasSearchfieldTarget) { 1043 this.searchfieldTarget.value = this.fullTextSearch 1044 } 1045 1046 // update date range 1047 if (this.dateRangeRaw) { 1048 let dateRangeRawParts = this.dateRangeRaw.split('#') 1049 let dateParts = dateRangeRawParts[1].split(' - ') 1050 1051 const start = new Date(dateParts[0]).toLocaleDateString( 1052 'de-DE', 1053 { 1054 day: '2-digit', 1055 month: '2-digit', 1056 year: 'numeric', 1057 }, 1058 ) 1059 const end = new Date(dateParts[1]).toLocaleDateString( 1060 'de-DE', 1061 { 1062 day: '2-digit', 1063 month: '2-digit', 1064 year: 'numeric', 1065 }, 1066 ) 1067 1068 document.querySelector( 1069 '#filter_' + dateRangeRawParts[0].replace('-range', ''), 1070 ).value = start + ' - ' + end 1071 } 1072 1073 // update params based inputs
1074 Object.keys(this.params).forEach((key) => { 1075 let targets = document.querySelectorAll( 1076 '[name="' + key + '"]', 1077 ) 1078 targets.forEach((target) => { 1079 // handle inputs by name 1080 if ( 1081 target.getAttribute('data-type') === 1082 this.params[key].attribute 1083 ) { 1084 // checkboxes 1085 if (target.type === 'checkbox') { 1086 for (let i in this.params[key].values) { 1087 if ( 1088 target.value === 1089 this.params[key].values[i] 1090 ) { 1091 target.checked = true 1092 } 1093 } 1094 } 1095 1096 // buttons 1097 if (target.type === 'button') { 1098 if (target.value === this.params[key].values) { 1099 target.classList.add('active') 1100 } 1101 } 1102 } 1103 }) 1104 }) 1105 } 1106 } 1107 } 1108}
Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.