1(function($) { 2 'use strict'; 3 4 class PN_COOKIES_MANAGER_Selector { 5 constructor(element, options = {}) { 6 this.element = element; 7 this.options = { 8 multiple: $(element).prop('multiple'), 9 searchable: true, 10 placeholder: 'Select an option...', 11 searchThreshold: 5, 12 ...options 13 }; 14 15 this.selectedValues = []; 16 this.isOpen = false; 17 // Get placeholder from attribute or empty option 18 this.placeholder = $(this.element).attr('placeholder') || $(this.element).find('option[value=""]').text() || ''; 19 this.init(); 20 } 21 22 init() { 23 // Create the selector structure 24 this.createStructure(); 25 this.bindEvents(); 26 // Only on initialization, sync visual with original select 27 this.updateDisplay(); 28 } 29 30 createStructure() { 31 const wrapper = $('<div class="pn-cookies-manager-selector"></div>'); 32 const control = $('<div class="pn-cookies-manager-selector__control"></div>');
33 const valueContainer = $('<div class="pn-cookies-manager-selector__value-container"></div>'); 34 const input = $('<input type="text" class="pn-cookies-manager-selector__input" />'); 35 const indicator = $('<span class="pn-cookies-manager-selector__indicator"><i class="material-icons-outlined pn-cookies-manager-selector__indicator-icon">keyboard_arrow_down</i></span>'); 36 37 valueContainer.append(input); 38 control.append(valueContainer, indicator); 39 wrapper.append(control); 40 41 this.menu = $('<div class="pn-cookies-manager-selector__menu" style="display: none;"></div>'); 42 43 if (this.options.searchable) { 44 const searchContainer = $('<div class="pn-cookies-manager-selector__search"></div>'); 45 const searchInput = $('<input type="text" placeholder="Search..." />'); 46 searchContainer.append(searchInput); 47 this.menu.append(searchContainer); 48 this.searchInput = searchInput; 49 } 50
51 this.optionsContainer = $('<div class="pn-cookies-manager-selector__options"></div>'); 52 this.menu.append(this.optionsContainer); 53 54 wrapper.append(this.menu); 55 $(this.element) 56 .css({ 57 position: 'absolute', 58 opacity: 0.01, 59 height: '1px', 60 width: '1px', 61 padding: 0, 62 margin: '-1px', 63 overflow: 'hidden', 64 clip: 'rect(0,0,0,0)', 65 whiteSpace: 'nowrap', 66 border: 0 67 }) 68 .after(wrapper); 69 70 this.wrapper = wrapper; 71 this.control = control; 72 this.valueContainer = valueContainer; 73 this.input = input; 74 } 75 76 bindEvents() { 77 // Toggle menu 78 this.control.on('click', (e) => { 79 e.stopPropagation(); 80 this.toggleMenu(); 81 }); 82 83 // Close menu when clicking outside 84 $(document).on('click', (e) => { 85 if (!this.wrapper.has(e.target).length) { 86 this.closeMenu(); 87 } 88 }); 89 90 // Handle option selection 91 this.menu.on('click', '.pn-cookies-manager-selector__option', (e) => { 92 const option = $(e.currentTarget); 93 const value = option.data('value'); 94 const label = option.text(); 95 96 const wasSelected = this.selectedValues.includes(value); 97 98 if (this.options.multiple) { 99 this.toggleValue(value, label); 100 } else { 101 this.setValue(value, label); 102 this.closeMenu(); 103 } 104 105 // Update class according to new state 106 if (this.options.multiple) { 107 if (wasSelected) { 108 option.removeClass('-selector__option--is-selected'); 109 } else { 110 option.addClass('-selector__option--is-selected'); 111 } 112 } 113 }); 114 115 // Handle search 116 if (this.options.searchable) { 117 this.searchInput.on('input', (e) => { 118 this.filterOptions(e.target.value); 119 }); 120 } 121 122 // Handle keyboard navigation 123 this.input.on('keydown', (e) => { 124 switch(e.key) { 125 case 'Enter': 126 e.preventDefault(); 127 this.toggleMenu(); 128 break; 129 case 'Escape': 130 this.closeMenu(); 131 break; 132 } 133 }); 134 } 135 136 toggleMenu() { 137 if (this.isOpen) { 138 this.closeMenu(); 139 } else { 140 this.openMenu(); 141 } 142 } 143 144 openMenu() { 145 this.isOpen = true; 146 this.control.addClass('-selector__control--is-open'); 147 this.menu.show(); 148 this.updateOptions(); 149 // Change icon to arrow up 150 this.wrapper.find('.pn-cookies-manager-selector__indicator-icon').text('keyboard_arrow_up'); 151 152 // Auto-focus on search input if it's visible 153 if (this.options.searchable && this.searchInput && this.searchInput.parent().is(':visible')) { 154 // Use setTimeout to ensure the menu is fully rendered before focusing 155 setTimeout(() => { 156 this.searchInput.focus(); 157 }, 10); 158 } 159 } 160 161 closeMenu() { 162 this.isOpen = false; 163 this.control.removeClass('-selector__control--is-open'); 164 this.menu.hide(); 165 // Change icon to arrow down 166 this.wrapper.find('.pn-cookies-manager-selector__indicator-icon').text('keyboard_arrow_down'); 167 } 168 169 updateOptions() { 170 const options = $(this.element).find('option'); 171 this.optionsContainer.empty(); 172 173 options.each((_, option) => { 174 const $option = $(option); 175 const value = $option.val(); 176 const label = $option.text(); 177
178 // Do not show empty option 179 if (value === '') return; 180 181 const isSelected = this.selectedValues.includes(value); 182 183 const optionElement = $('<div class="pn-cookies-manager-selector__option"></div>') 184 .text(label) 185 .data('value', value); 186 187 if (isSelected) { 188 optionElement.addClass('-selector__option--is-selected'); 189 } 190 191 this.optionsContainer.append(optionElement); 192 }); 193 194 // Show search if options exceed threshold 195 if (this.options.searchable && options.length > this.options.searchThreshold) { 196 this.searchInput.parent().show(); 197 } else { 198 this.searchInput.parent().hide(); 199 } 200 } 201 202 filterOptions(searchTerm) { 203 const options = this.optionsContainer.find('.pn-cookies-manager-selector__option'); 204 const term = searchTerm.toLowerCase(); 205 206 // Function to normalize text by removing accents 207 const normalizeText = (text) => { 208 return text.normalize('NFD') 209 .replace(/[\u0300-\u036f]/g, '') // Remove diacritics (accents) 210 .toLowerCase(); 211 }; 212 213 options.each((_, option) => { 214 const $option = $(option); 215 const text = $option.text(); 216 const normalizedText = normalizeText(text);
217 const normalizedTerm = normalizeText(term); 218 $option.toggle(normalizedText.includes(normalizedTerm)); 219 }); 220 } 221 222 toggleValue(value, label) { 223 if (value === '') return; 224 if (this.selectedValues.includes(value)) { 225 this.removeAllSelectedValue(value); 226 return; 227 } 228 this.selectedValues.push(value); 229 this.addSelectedValue(value, label); 230 this.updateOriginalSelect(); 231 } 232 233 setValue(value, label) { 234 this.selectedValues = [value]; 235 this.valueContainer.empty(); 236 this.addSelectedValue(value, label); 237 this.updateOriginalSelect(); 238 } 239 240 addSelectedValue(value, label) { 241 // Remove placeholder if exists 242 this.valueContainer.find('.pn-cookies-manager-selector__placeholder').remove(); 243 if (this.options.multiple) { 244 const valueElement = $('<div class="pn-cookies-manager-selector__multi-value"></div>'); 245 const labelElement = $('<span class="pn-cookies-manager-selector__multi-value__label"></span>').text(label); 246 const removeButton = $('<span class="pn-cookies-manager-selector__multi-value__remove"><i class="material-icons-outlined pn-cookies-manager-icon-close">close</i></span>'); 247 248 valueElement.attr('data-value', value); 249 250 removeButton.on('click', (e) => { 251 e.stopPropagation(); 252 this.removeAllSelectedValue(value); 253 // Also remove selected class from the option in the dropdown menu 254 this.optionsContainer.find('.pn-cookies-manager-selector__option').each(function() { 255 if ($(this).data('value') == value) { 256 $(this).removeClass('-selector__option--is-selected'); 257 } 258 }); 259 }); 260 261 valueElement.append(labelElement, removeButton); 262 this.valueContainer.append(valueElement); 263 } else { 264 // Single select: text + x to remove 265 const valueElement = $('<span class="pn-cookies-manager-selector__single-value"></span>'); 266 const labelElement = $('<span class="pn-cookies-manager-selector__single-value__label"></span>').text(label); 267 const removeButton = $('<span class="pn-cookies-manager-selector__single-value__remove"><i class="material-icons-outlined pn-cookies-manager-icon-close">close</i></span>'); 268 removeButton.on('click', (e) => { 269 e.stopPropagation(); 270 // Clear selection 271 this.selectedValues = []; 272 this.valueContainer.empty(); 273 this.valueContainer.find('.pn-cookies-manager-selector__placeholder').remove(); 274 this.updateOriginalSelect(); 275 if (this.placeholder) { 276 this.input.hide(); 277 this.valueContainer.append('<span class="pn-cookies-manager-selector__placeholder">' + this.placeholder + '</span>'); 278 } 279 }); 280 valueElement.append(labelElement, removeButton); 281 this.valueContainer.append(valueElement); 282 } 283 } 284 285 removeAllSelectedValue(value) { 286 // Remove all occurrences from the array 287 this.selectedValues = this.selectedValues.filter(v => v !== value); 288 // Remove all visual elements 289 this.valueContainer.find(`[data-value="${value}"]`).remove(); 290 this.updateOriginalSelect(); 291 // If there are no selected values, show the placeholder and hide the input 292 if (this.selectedValues.length === 0 && this.placeholder) { 293 this.input.hide(); 294 this.valueContainer.find('.pn-cookies-manager-selector__placeholder').remove(); 295 this.valueContainer.append('<span class="pn-cookies-manager-selector__placeholder">' + this.placeholder + '</span>'); 296 } else { 297 this.input.show(); 298 } 299 } 300 301 updateOriginalSelect() { 302 const $select = $(this.element); 303 304 if (this.options.multiple) { 305 $select.find('option').prop('selected', false);
306 this.selectedValues.forEach(value => { 307 $select.find(`option[value="${value}"]`).prop('selected', true); 308 }); 309 } else { 310 $select.val(this.selectedValues[0]); 311 } 312 313 $select.trigger('change'); 314 } 315 316 updateDisplay() { 317 const $select = $(this.element); 318 const selectedOptions = $select.find('option:selected'); 319 this.selectedValues = []; 320 this.valueContainer.empty(); 321 this.valueContainer.find('.pn-cookies-manager-selector__placeholder').remove(); 322 323 // For single select: if only the empty value is selected, show only the placeholder 324 if ( 325 !this.options.multiple && 326 selectedOptions.length === 1 && 327 selectedOptions[0].value === '' && 328 (selectedOptions.length === 0 && this.placeholder) || 329 (selectedOptions.length === 1 && selectedOptions[0].value === '' && this.placeholder) 330 ) { 331 this.input.hide(); 332 this.valueContainer.append('<span class="pn-cookies-manager-selector__placeholder">' + this.placeholder + '</span>'); 333 return; 334 } else { 335 this.input.show(); 336 } 337 338 selectedOptions.each((_, option) => { 339 const $option = $(option); 340 const value = $option.val(); 341 const label = $option.text(); 342 if (value === '') return; // Skip empty value (placeholder) 343 this.selectedValues.push(value); 344 this.addSelectedValue(value, label); 345 }); 346 } 347 } 348 349 // jQuery plugin initialization 350 $.fn.PN_COOKIES_MANAGER_Selector = function(options) { 351 return this.each(function() { 352 if (!$(this).data('-selector')) { 353 $(this).data('-selector', new PN_COOKIES_MANAGER_Selector(this, options)); 354 } 355 }); 356 }; 357 358 // Global handlers outside of class: close selector when clicking outside or pressing ESC 359 $(document).on('mousedown touchstart', function(e) { 360 var $target = $(e.target); 361 if (!$target.closest('.pn-cookies-manager-selector').length) { 362 $('.pn-cookies-manager-selector__menu:visible').each(function() { 363 var $menu = $(this); 364 $menu.hide(); 365 $menu.closest('.pn-cookies-manager-selector').find('.pn-cookies-manager-selector__control').removeClass('-selector__control--is-open'); 366 $menu.closest('.pn-cookies-manager-selector').find('.pn-cookies-manager-selector__indicator-icon').text('keyboard_arrow_down'); 367 }); 368 } 369 }); 370 371 $(document).on('keyup', function(e) { 372 if (e.key === 'Escape') { 373 $('.pn-cookies-manager-selector__menu:visible').each(function() { 374 var $menu = $(this); 375 $menu.hide(); 376 $menu.closest('.pn-cookies-manager-selector').find('.pn-cookies-manager-selector__control').removeClass('-selector__control--is-open'); 377 $menu.closest('.pn-cookies-manager-selector').find('.pn-cookies-manager-selector__indicator-icon').text('keyboard_arrow_down'); 378 }); 379 } 380 }); 381 382})(jQuery);
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.