1$.widget('gd.listFilter', { 2 3 options: { 4 elementClass: 'listfilter__filter', 5 searchInputSelector: '[data-listfilter-role="search"]', 6 searchSubmitSelector: '[data-listfilter-role="submit"]', 7 searchResetSelector: '[data-listfilter-role="reset"]', 8 filtersWrapperSelector: '[data-listfilter-role="filters"]', 9 listSelector: '[data-listfilter-role="list"]', 10 itemSelector: '[data-listfilter-role="item"]', 11 listClass: 'listfilter-list', 12 cleanClass: 'listfilter-list--clean', 13 moreClass: 'listfilter-list_more', 14 moreSelector: '[data-listfilter-role="more"]', 15 noResultClass: 'listfilter-list_noresult', 16 queryParameter: 'listfilter', 17 listElement: null, 18 sysLanguage: 0, 19 isClean: true, 20 showAllText: '', 21 noResultText: '', 22 configuration: {} 23 }, 24 25 _create: function () { 26 this._handleNextList(); 27 this._setClean(); 28 this._initFilters(); 29 this._bindEvents(); 30 this._handleUrlSearch(); 31 }, 32 33 /** 34 * Handles the next filterable list element 35 * 36 * @private 37 * @returns {JQuery}|null 38 */ 39 _handleNextList: function () { 40 var nextList = null, 41 frame = this.element.closest('.frame'); 42 43 while (frame.next('.frame').length) { 44 frame = frame.next('.frame'); 45 var list = frame.find(this.options.listSelector); 46 if (list.length) { 47 nextList = list; 48 break; 49 } 50 } 51 this.options.listElement = nextList; 52 53 if (nextList) { 54 nextList.addClass(this.options.listClass); 55 nextList.append( 56 $('<p>', { 57 class: this.options.noResultClass, 58 style: 'display: none;', 59 text: this.options.noResultText 60 }) 61 ); 62 nextList.append( 63 $('<div>', { 64 class: this.options.moreClass 65 }).append( 66 $('<a>', { 67 text: this.options.showAllText, 68 href: '#', 69 'data-listfilter-role': 'more' 70 }) 71 ) 72 ); 73 } 74 }, 75 76 /** 77 * Checks if item list is in a clean state 78 * 79 * @private 80 */ 81 _isClean: function () { 82 return this.options.isClean; 83 }, 84 85 /** 86 * Sets item list to a clean state 87 * 88 * @private 89 */ 90 _setClean: function () { 91 this.options.isClean = true; 92 if (this.options.listElement) { 93 this.options.listElement.addClass(this.options.cleanClass); 94 this.options.listElement.find(this.options.itemSelector).css({display: ''}); 95 this.options.listElement.find('.' + this.options.moreClass).show(); 96 } 97 }, 98 99 /** 100 * Sets item list to an unclean state 101 * 102 * @private 103 */ 104 _setUnclean: function () { 105 this.options.isClean = false; 106 if (this.options.listElement) { 107 this.options.listElement.removeClass(this.options.cleanClass); 108 this.options.listElement.find('.' + this.options.moreClass).hide(); 109 } 110 }, 111 112 /** 113 * Parses multivalue string (which should be escaped Arrays) 114 * 115 * @param {string} value 116 * @returns mixed 117 * 118 * @private 119 */ 120 _parseMultivalue: function (value) { 121 return JSON.parse(decodeURI(value)); 122 }, 123 124 /** 125 * Get possible values for a filter 126 * 127 * @param {string} filter 128 * @param {Object} filterConfig 129 * @return {Object} 130 * 131 * @private 132 */ 133 _getPossibleFilterValues: function (filter, filterConfig) { 134 var filterValues = {}, 135 sortedFilterValues, 136 elementSelector = ''; 137 138 if (this.options.listElement) { 139 if (this._isClean()) { 140 elementSelector = this.options.itemSelector + '[data-listfilter-filter-' + filter + ']'; 141 } else { 142 elementSelector = this.options.itemSelector + '[data-listfilter-filter-' + filter + ']:visible'; 143 } 144 $(this.options.listElement).find(elementSelector).each(function (i, element) { 145 var value = $(element).attr('data-listfilter-filter-' + filter); 146 147 if (filterConfig.multivalue) { 148 value = this._parseMultivalue(value); 149 if (value instanceof Array) { 150 for (var i = 0; i < value.length; i++) { 151 if (value[i] !== '') { 152 filterValues[value[i]] = value[i]; 153 } 154 } 155 } 156 } else { 157 if (value !== '') { 158 filterValues[value] = value; 159 } 160 } 161 }.bind(this)); 162 } 163 if (filterConfig.sorting && filterConfig.sorting === 'desc') { 164 sortedFilterValues = Object.keys(filterValues).sort(function (a, b) { 165 return filterValues[a].toLowerCase() < filterValues[b].toLowerCase() ? 1 : -1; 166 }); 167 } else { 168 sortedFilterValues = Object.keys(filterValues).sort(function (a, b) { 169 return filterValues[a].toLowerCase() > filterValues[b].toLowerCase() ? 1 : -1; 170 }); 171 } 172 return sortedFilterValues; 173 }, 174 175 /** 176 * Return HTML for select options 177 * 178 * @param {string} filter 179 * @param {Object} filterConfig 180 * @param {string} filterElementPlaceholder 181 * @return {string} 182 * 183 * @private 184 */ 185 _getSelectOptionsHtml: function (filter, filterConfig, filterElementPlaceholder) { 186 var filterValues = this._getPossibleFilterValues(filter, filterConfig), 187 selectOptionsHtml = '<option value="" selected="selected">' + filterElementPlaceholder + '</option>'; 188 189 $.each(filterValues, function (i) { 190 selectOptionsHtml += '<option value="' + filterValues[i] + '">' + filterValues[i] + '</option>'; 191 }); 192 return selectOptionsHtml; 193 }, 194 195 /** 196 * Find filter input or select element by name 197 * 198 * @param {string} filter 199 * @return {JQuery} 200 * 201 * @private 202 */ 203 _findFilterElement: function (filter) { 204 return this.element.find( 205 this.options.filtersWrapperSelector + ' .' + this.options.elementClass + '[data-listfilter-filter="' + filter + '"]' 206 ); 207 }, 208 209 /** 210 * Adds autocomplete to input fields 211 * Inputs are only valid if an autocomplete option has been chosen 212 * 213 * @param {string} filter 214 * @param {Object} filterConfig 215 * 216 * @private 217 */ 218 _addAutocomplete: function (filter, filterConfig) { 219 var filterValues = this._getPossibleFilterValues(filter, filterConfig), 220 filterElement = this._findFilterElement(filter); 221 222 if (filterValues && filterValues instanceof Object) { 223 filterElement.autocomplete({ 224 source: filterValues, 225 create: function (event, ui) { 226 filterElement.attr('data-autocomplete-valid', '1'); 227 this._trigger('autocompletevalidationchange', event, null); 228 }.bind(this), 229 open: function (event, ui) { 230 filterElement.attr('data-autocomplete-valid', '0'); 231 this._trigger('autocompletevalidationchange', event, null); 232 }.bind(this), 233 select: function (event, ui) { 234 filterElement.attr('data-autocomplete-valid', '1'); 235 this._trigger('autocompletevalidationchange', event, null); 236 // We have to do this manually so that the search can use the correct value 237 if (ui.hasOwnProperty('item') && ui.item.hasOwnProperty('value')) { 238 filterElement.val(ui.item.value); 239 } 240 this._search(); 241 }.bind(this) 242 }); 243 this._on(filterElement, { 244 // Unfortunately, the change event only fire, when the input is blurred. 245 // We use keypress instead. 246 keyup: function (event) { 247 if (event.which !== 13) { 248 if (filterElement.val() === '') { 249 filterElement.attr('data-autocomplete-valid', '1'); 250 this._search(); 251 } else { 252 filterElement.attr('data-autocomplete-valid', '0'); 253 } 254 this._trigger('autocompletevalidationchange', event, null); 255 } 256 } 257 }); 258 } 259 }, 260 261 /** 262 * Checks if all autocomplete inputs are valid 263 * 264 * @private 265 */ 266 _isAutocompleteValid: function () { 267 return this.element.find('[data-autocomplete-valid=0]').length === 0; 268 }, 269 270 /** 271 * Checks if submit button should be disabled for invalid autocomplete inputs 272 * 273 * @private 274 */ 275 _autocompleteHandleSubmitButton: function () { 276 var submitButton = this.element.find(this.options.searchSubmitSelector); 277 278 if (this._isAutocompleteValid()) { 279 submitButton.prop('disabled', false); 280 } else { 281 submitButton.prop('disabled', true); 282 } 283 }, 284 285 /** 286 * Initialize filters 287 * 288 * @private 289 */ 290 _initFilters: function () { 291 var configuration = this.options.configuration, 292 elementClass = this.options.elementClass; 293 294 if (configuration.filters) { 295 $.each(configuration.filters, function (filter, filterConfig) { 296 var filterElement, 297 filterElementInputType, 298 filterElementPlaceholder, 299 filterElementHtml, 300 filterElementAfterHtml, 301 filterElementClass; 302 303 filterElementClass = elementClass + ' ' + elementClass + '--' + filterConfig.type; 304 if (filterConfig.placeholder && filterConfig.placeholder[this.options.sysLanguage]) { 305 filterElementPlaceholder = filterConfig.placeholder[this.options.sysLanguage]; 306 } 307 switch (filterConfig.type) { 308 case 'select': 309 filterElement = '<select>'; 310 filterElementHtml = this._getSelectOptionsHtml(filter, filterConfig, filterElementPlaceholder); 311 break; 312 313 case 'input': 314 default: 315 filterElement = '<input>'; 316 filterElementInputType = 'text'; 317 break; 318 } 319 this.element.find(this.options.filtersWrapperSelector).append( 320 $('<div>', { 321 class: elementClass + '-wrap' 322 }).append( 323 '<label>' + filterElementPlaceholder + '</label>' 324 ).append( 325 $(filterElement, { 326 class: filterElementClass, 327 type: filterElementInputType, 328 html: filterElementHtml, 329 'data-listfilter-filter': filter, 330 'data-placeholder': filterElementPlaceholder 331 }) 332 ).append(filterElementAfterHtml) 333 ); 334 if (filterConfig.type !== 'select') { 335 this._addAutocomplete(filter, filterConfig); 336 } 337 }.bind(this)); 338 } 339 }, 340 341 /** 342 * Update filters 343 * 344 * @private 345 */ 346 _updateFilterOptions: function (activeFilters) { 347 if (this.options.configuration.filters) { 348 $.each(this.options.configuration.filters, function (filter, filterConfig) { 349 if (!activeFilters.hasOwnProperty(filter)) { 350 var filterElement = this._findFilterElement(filter); 351 352 if (filterConfig.type === 'select') { 353 filterElement.html(this._getSelectOptionsHtml(filter, filterConfig, filterElement.attr('data-placeholder'))); 354 if (filterElement.find('option').length === 1) { 355 filterElement.prop('disabled', true); 356 } else { 357 filterElement.prop('disabled', false); 358 } 359 } else { 360 var filterValues = this._getPossibleFilterValues(filter, filterConfig); 361 362 if (filterValues && $.isArray(filterValues) && !$.isEmptyObject(filterValues)) { 363 filterElement.autocomplete('option', 'source', filterValues); 364 filterElement.prop('disabled', false); 365 } else { 366 filterElement.prop('disabled', true); 367 } 368 } 369 } 370 }.bind(this)); 371 } 372 }, 373 374 /** 375 * Get query parameters for filters 376 * 377 * @return {Object} 378 * 379 * @private 380 */ 381 _getQueryParameters: function () { 382 var queryParameters = {}, 383 query = window.location.search.substring(1), 384 querySegments = query.split('&'); 385 386 for (var i = 0; i < querySegments.length; i++) { 387 var queryParameter = querySegments[i].split('='); 388 389 if (queryParameter[0].lastIndexOf(this.options.queryParameter, 0) === 0) { 390 var queryParameterName = decodeURIComponent(queryParameter[0]).slice(decodeURIComponent(queryParameter[0]).indexOf('[') + 1, -1); 391 392 if (queryParameterName) { 393 queryParameters[queryParameterName] = decodeURIComponent(queryParameter[1]); 394 } 395 } 396 } 397 return queryParameters; 398 }, 399 400 /** 401 * Trigger search via URL if query params are present 402 * 403 * @private 404 */ 405 _handleUrlSearch: function () { 406 var queryParameters = this._getQueryParameters(), 407 configuration = this.options.configuration; 408 409 if (!$.isEmptyObject(queryParameters)) { 410 $.each(queryParameters, function (parameterName, parameterValue) { 411 if (configuration.filters.hasOwnProperty(parameterName)) { 412 var filterElement = this._findFilterElement(parameterName); 413 414 if (configuration.filters[parameterName].type === 'select') { 415 filterElement.find('option[value="' + parameterValue + '"]').prop('selected', true); 416 } else { 417 filterElement.val(parameterValue); 418 } 419 } else if (parameterName === 'search') { 420 this.element.find(this.options.searchInputSelector).val(parameterValue); 421 } 422 }.bind(this)); 423 this._search(); 424 } 425 }, 426 427 /** 428 * Reset the search form 429 * 430 * @private 431 */ 432 _reset: function () { 433 var allInputsSelector = '.' + this.options.elementClass + ', ' + this.options.searchInputSelector, 434 allInputs = this.element.find(allInputsSelector) 435 436 allInputs.val(''); 437 this._search(); 438 }, 439 440 /** 441 * Show all search results 442 * 443 * @private 444 */ 445 _showMore: function () { 446 this._setUnclean(); 447 if (this.options.listElement) { 448 this.options.listElement.find(this.options.itemSelector).show(); 449 } 450 }, 451 452 /** 453 * Main search function 454 * 455 * @private 456 */ 457 _search: function () { 458 var configuration = this.options.configuration, 459 activeFilters = {}, 460 numberOfResults = 0, 461 searchword = this.element.find(this.options.searchInputSelector).val().toLowerCase(), 462 searchwords = searchword.split(' '); 463 464 if (configuration.filters) { 465 $.each(configuration.filters, function (k, filter) { 466 var filterVal = this.element.find('[data-listfilter-filter="' + k + '
466"]').val(); 467 if (filterVal) { 468 activeFilters[k] = filterVal; 469 } 470 }.bind(this)); 471 } 472 if (this.options.listElement) { 473 var noResultInfo = $(this.options.listElement).find('.' + this.options.noResultClass); 474 475 if ($.isEmptyObject(activeFilters) && searchword === '') { 476 this._setClean(); 477 } else { 478 this._setUnclean(); 479 } 480 481 $(this.options.listElement).find(this.options.itemSelector).each(function (i, element) { 482 var satisfiesText = true, 483 satisfiesFilters = true, 484 elementText = $(element).text().replace(/(\­|Â|­)/gi, "").toLowerCase(); 485 486 if (searchwords.length) { 487 for (i = 0; i < searchwords.length; i++) { 488 if (elementText.indexOf(searchwords[i]) < 0) { 489 satisfiesText = false; 490 break; 491 } 492 } 493 } 494 if (activeFilters) { 495 $.each(activeFilters, function (filter, val) { 496 var filterVal = $(element).attr('data-listfilter-filter-' + filter); 497 498 if (configuration.filters[filter].multivalue) { 499 filterVal = this._parseMultivalue(filterVal); 500 if (filterVal.indexOf(val) < 0) { 501 satisfiesFilters = false; 502 return false; 503 } 504 } else { 505 if (filterVal !== val) { 506 satisfiesFilters = false; 507 return false; 508 } 509 } 510 }.bind(this)); 511 } 512 if (satisfiesText && satisfiesFilters) { 513 $(element).show(); 514 numberOfResults++; 515 } else { 516 $(element).hide(); 517 } 518 }.bind(this)); 519 520 if ($.isEmptyObject(activeFilters) && searchword === '') { 521 this._setClean(); 522 } else { 523 this._setUnclean(); 524 } 525 526 if (numberOfResults === 0) { 527 noResultInfo.show(); 528 } else { 529 noResultInfo.hide(); 530 } 531 } 532 this._updateFilterOptions(activeFilters); 533 }, 534 535 /** 536 * Binds events 537 * 538 * @private 539 */ 540 _bindEvents: function () { 541 this._on(this.element.find(this.options.searchSubmitSelector), { 542 click: this._search 543 }); 544 this._on(this.element.find('.listfilter__filter--select'), { 545 change: this._search 546 }); 547 this._on(this.element.find('input[type=text]'), { 548 keypress: function (event) { 549 if (event.which === 13 && this._isAutocompleteValid()) { 550 this._search(); 551 } 552 } 553 }); 554 this._on(this.element, { 555 listfilterautocompletevalidationchange: this._autocompleteHandleSubmitButton 556 }); 557 this._on(this.element.find(this.options.searchResetSelector), { 558 click: function (event) { 559 event.preventDefault(); 560 this._reset(); 561 } 562 }); 563 if (this.options.listElement) { 564 this._on(this.options.listElement.find(this.options.moreSelector), { 565 click: function (event) { 566 event.preventDefault(); 567 this._showMore(); 568 } 569 }); 570 } 571 } 572});
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.