1class MultiSelect { 2 constructor({ 3 containerId, 4 inputName, 5 phpEndpoint, 6 phpEndpointGetIds, 7 phpEndpointOnSelect, 8 placeholder = 'Vyberte položky...', 9 minLen = 0, 10 maxSelectedItems = null, 11 initialValues = [], 12 onSelect = () => {}, 13 onRemoveItem = () => {} 14 }) 15 { 16 this.container = document.getElementById(containerId); 17 if (!this.container) return; 18 19 this.phpEndpointRoot = '/src/components-NEW/multiselect/ajax/'; 20 this.phpEndpoint = this.phpEndpointRoot + phpEndpoint; 21 this.phpEndpointGetIds = this.phpEndpointRoot + phpEndpointGetIds; 22 this.phpEndpointOnSelect = this.phpEndpointRoot + phpEndpointOnSelect; 23 this.placeholder = placeholder; 24 this.inputName = inputName; 25 this.minLen = minLen; 26 this.maxSelectedItems = maxSelectedItems; 27 28 this.selectedItems = []; // Pole pro vybrané objekty {label, value} 29 this.activeIndex = -1; 30 31 // nalezené výsledky 32 this.results = []; 33 34 this.onSelect = onSelect; 35 this.onRemoveItem = onRemoveItem; 36 37 this.init(); 38 39 if (initialValues && initialValues.length > 0) { 40 this.preloadInitialItems(initialValues); 41 } 42 } 43 44 init() { 45 this.container.classList.add('multi-autocomplete-container'); 46 47 // Wrapper pro tagy a input (aby to vypadalo jako jedno pole) 48 this.controlWrapper = document.createElement('div'); 49 this.controlWrapper.classList.add('autocomplete-control'); 50 51 this.tagsContainer = document.createElement('div'); 52 this.tagsContainer.classList.add('autocomplete-tags'); 53 54 this.input = document.createElement('input'); 55 this.input.setAttribute('type', 'text'); 56 this.input.setAttribute('placeholder', this.placeholder); 57 this.input.classList.add('autocomplete-input-inline'); 58 this.input.setAttribute('autocomplete', 'off'); 59 60 this.hiddenInput = document.createElement('input'); 61 this.hiddenInput.setAttribute('type', 'hidden'); 62 this.hiddenInput.setAttribute('name', this.inputName); 63 64 this.resultsContainer = document.createElement('div'); 65 this.resultsContainer.classList.add('autocomplete-results'); 66 67 // Poskládánà DOMu 68 this.controlWrapper.appendChild(this.tagsContainer); 69 this.controlWrapper.appendChild(this.input); 70 this.container.appendChild(this.controlWrapper); 71 this.container.appendChild(this.hiddenInput); 72 this.container.appendChild(this.resultsContainer); 73 74 this.setupEvents(); 75 this.onSelect(); 76 } 77 78 async preloadInitialItems(ids) { 79 try { 80 // PÅedpokládáme, že tvůj endpoint umà vrátit data i pro konkrétnà ID 81 // Pokud ne, budeÅ¡ možná potÅebovat speciálnà endpoint pro "detail" 82 const response = await fetch(`${this.phpEndpointGetIds}?ids=${ids.join(',')}`); 83 const items = await response.json(); 84 85 this.selectedItems = items; // OÄekáváme pole {label, value} 86 this.renderTags(); 87 this.updateHiddenInput(); 88 } catch (err) { 89 console.error('Chyba pÅi pÅednaÄÃtánà položek:', err); 90 } 91 } 92 93 broadcastOpen() { 94 const event = new CustomEvent('multiSelectOpened', { 95 detail: { instance: this } 96 }); 97 document.dispatchEvent(event); 98 } 99 100 setupEvents() { 101 // PosluchaÄ pro zavÅenà zvenÄà (vlastnà událost) 102 document.addEventListener('multiSelectOpened', (e) => { 103 // Pokud událost pÅiÅ¡la z jiné instance, tuto instanci zavÅeme 104 if (e.detail.instance !== this) { 105 this.resultsContainer.innerHTML = ''; 106 this.activeIndex = -1; 107 } 108 }); 109 110 this.input.addEventListener('input', () => { 111 this.activeIndex = -1; 112 this.search(); 113 }); 114 115 // Po kliknutà nebo focusu do inputu spustÃme vyhledávánà (i s prázdným ÅetÄzcem) 116 this.input.addEventListener('focus', () => { 117 // PÅi focusu dáme vÄdÄt ostatnÃm, aby se zavÅeli 118 this.broadcastOpen(); 119 this.search(); 120 }); 121 122 this.input.addEventListener('keydown', (e) => { 123 const resultItems = this.resultsContainer.querySelectorAll('.result-item'); 124 125 if (e.key === 'Tab') { 126 // ZavÅeme nápovÄdu 127 this.resultsContainer.innerHTML = ''; 128 // activeIndex resetujeme, aby pÅi pÅÃÅ¡tÃm focusu nezaÄÃnalo uprostÅed 129 this.activeIndex = -1; 130 // NepoužÃváme preventDefault, aby se focus pÅirozenÄ pÅesunul na dalšà prvek 131 } else if (e.key === 'ArrowDown') { 132 e.preventDefault(); 133 this.activeIndex = (this.activeIndex + 1) % resultItems.length; 134 this.updateSelection(resultItems); 135 } else if (e.key === 'ArrowUp') { 136 e.preventDefault(); 137 this.activeIndex = (this.activeIndex - 1 + resultItems.length) % resultItems.length; 138 this.updateSelection(resultItems); 139 } else if (e.key === 'Enter' && this.activeIndex > -1) { 140 e.preventDefault(); 141 resultItems[this.activeIndex].click(); 142 } else if (e.key === 'Backspace' && this.input.value === '' && this.selectedItems.length > 0) { 143 // Smazat poslednà tag pÅi backspace v prázdném inputu 144 this.removeItem(this.selectedItems[this.selectedItems.length - 1].value); 145 } 146 }); 147 148 // Kliknutà do wrapperu aktivuje input 149 this.controlWrapper.onclick = (e) => { 150 // Pokud klikneme na kÅÞek, removeItem už stopPropagation má, 151 // takže se focus inputu nevyvolá (pokud nechceme). 152 this.input.focus(); 153 }; 154 155 document.addEventListener('click', (e) => { 156 if (!this.container.contains(e.target)) { 157 this.resultsContainer.innerHTML = ''; 158 this.activeIndex = -1; 159 } 160 }); 161 } 162 163 async search() { 164 const query = this.input.value.trim(); 165 if (this.minLen > 0 && query.length < this.minLen) { 166 this.resultsContainer.innerHTML = ''; 167 return; 168 } 169 170 try { 171 const response = await fetch(`${this.phpEndpoint}?q=${encodeURIComponent(query)}`); 172 const items = await response.json(); 173 this.results = items; 174 this.renderResults(); 175 } catch (err) { console.error(err); } 176 } 177 178 renderResults() { 179 this.resultsContainer.innerHTML = ''; 180 if (this.results.length === 0) return; 181
182 this.results.forEach((item, index) => { 183 const isSelected = this.selectedItems.some(s => s.value === item.value); 184 185 const div = document.createElement('div'); 186 div.classList.add('result-item'); 187 if (isSelected) div.classList.add('selected'); // Pro styling celého Åádku 188 189 div.innerHTML = ` 190 <span class="checkbox-box ${isSelected ? 'filled' : ''}"></span> 191 ${item.label} 192 `; 193 194 div.onclick = (e) => { 195 e.stopPropagation(); 196 e.preventDefault(); // Zabránit výchozÃmu chovánà 197 198 // PÅed jakoukoli akcà dáme vÄdÄt, že pracujeme v tomto MS 199 this.broadcastOpen(); 200 201 if (!isSelected) { 202 this.addItem(item); 203 } else { 204 this.removeItem(item.value); 205 //this.search(); // Refresh výsledků pro pÅekreslenà ÄtvereÄku 206 } 207 this.selectItem(); 208 209 // KlÃÄová oprava: VrátÃme focus do inputu aktuálnÃho MS, 210 // aby neodskoÄil na jiný prvek na stránce 211 this.input.focus(); 212 }; 213 214 this.resultsContainer.appendChild(div); 215 }); 216 } 217 218 addItem(item) { 219 // 2. Kontrola limitu: pokud máme limit a jsme na nÄm, odebereme prvnà prvek 220 if (this.maxSelectedItems !== null && this.selectedItems.length >= this.maxSelectedItems) { 221 this.selectedItems.shift(); 222 } 223 224 this.selectedItems.push(item); 225 this.renderTags(); 226 this.renderResults(); // Refreshneme výsledky, aby se pÅebarvily checkboxy 227 this.updateHiddenInput(); 228 this.input.focus(); 229 } 230 231 removeItem(value) { 232 this.selectedItems = this.selectedItems.filter(i => i.value !== value); 233 this.renderTags(); 234 this.renderResults(); // PÅidáno i sem pro synchronizaci checkboxů v seznamu 235 this.updateHiddenInput(); 236 this.selectItem(); 237 238 if (typeof this.onRemoveItem === 'function') { 239 this.onRemoveItem(); 240 } 241 } 242 243 removeSelectedItems() { 244 // 1. VyÄistÃme pole vybraných položek 245 this.selectedItems = []; 246 247 // 2. PÅekreslÃme tagy (budou prázdné) 248 this.renderTags(); 249 250 // 3. Aktualizujeme skrytý input pro PHP 251 this.updateHiddenInput(); 252 253 // 4. MÃsto renderResults (které může vyvolat aktivaci) jen 254 // vizuálnÄ odÅ¡krtáme checkboxy v otevÅeném seznamu, pokud existuje 255 const checkboxes = this.resultsContainer.querySelectorAll('.checkbox-box'); 256 checkboxes.forEach(cb => cb.classList.remove('filled')); 257 258 const items = this.resultsContainer.querySelectorAll('.result-item'); 259 items.forEach(item => item.classList.remove('selected')); 260 261 // 5. VolitelnÄ: Pokud chcete, aby PHP vÄdÄlo, že je prázdno: 262 // this.selectItem(); 263 } 264 265 renderTags() { 266 this.tagsContainer.innerHTML = ''; 267 this.selectedItems.forEach(item => { 268 const tag = document.createElement('span'); 269 tag.classList.add('autocomplete-tag'); 270 tag.innerHTML = `${item.label} <i class="remove-btn">×</i>`; 271 tag.querySelector('.remove-btn').onclick = (e) => { 272 e.stopPropagation(); 273 this.removeItem(item.value); 274 275 // Aktivujeme tento multiselect 276 this.broadcastOpen(); 277 this.input.focus(); 278 this.search(); 279 }; 280 this.tagsContainer.appendChild(tag); 281 }); 282 } 283 284 updateHiddenInput() { 285 // UložÃme IDÄka oddÄlená Äárkou (napÅ. "12,45,88") 286 this.hiddenInput.value = this.selectedItems.map(i => i.value).join(','); 287 } 288 289 updateSelection(items) { 290 items.forEach((el, i) => el.classList.toggle('active', i === this.activeIndex)); 291 } 292 293 // Tato funkce se zavolá po vybránà nalezeného záznamu a pouze zavolá funkci onSelect pÅedanou do konstruktoru 294 async selectItem() { 295 296 const fetchUrl = new URL(this.phpEndpointOnSelect, window.location.origin); 297 this.selectedItems.forEach(item => { 298 fetchUrl.searchParams.append('ids[]', item.value); 299 }); 300 const response = await fetch(fetchUrl.toString()); 301 if (response.ok) { 302 303 if (typeof this.onSelect === 'function') { 304 this.onSelect(); 305 } 306 } 307 } 308 309 // TODO: upravit na každém projektu zvlášť - toto jsou custom user funkce 310 // --- onSelect callback funkce --- 311 static async setRegionCity() { 312 313 const fetchUrl = new URL('/src/components-NEW/multiselect/ajax/store-select.php', window.location.origin); 314 const response = await fetch(fetchUrl.toString()); 315 const html = await response.text(); 316 317 var elem = document.getElementById('ajax-select-store'); 318 elem.innerHTML = html; 319 320 MultiSelect.getStore(); 321 } 322 323 static async setRegionCityRedirectToStoreDetail() { 324 325 const fetchUrl = new URL('/src/components-NEW/multiselect/ajax/store-select.php', window.location.origin); 326 fetchUrl.searchParams.append('redirect_to_store_detail', 'true'); 327 const response = await fetch(fetchUrl.toString()); 328 const html = await response.text(); 329 330 var elem = document.getElementById('ajax-select-store'); 331 elem.innerHTML = html; 332 333 MultiSelect.getStore(true); 334 } 335 336 static async setStore(id_store) { 337 338 const fetchUrl = new URL('/src/components-NEW/multiselect/ajax/store-set.php', window.location.origin); 339 fetchUrl.searchParams.append('id_store', id_store); 340 const response = await fetch(fetchUrl.toString()); 341 342 MultiSelect.getStore(); 343 } 344 345 // Upravená funkce getStore 346 static async getStore(redirect_to_store_detail = false) { 347 try { 348 //const response = await fetch('/src/components-NEW/multiselect/ajax/store-get.php'); 349 //const data = await response.json(); 350 351 const fetchUrl = new URL('/src/components-NEW/multiselect/ajax/store-get.php', window.location.origin); 352 fetchUrl.searchParams.append('redirect_to_store_detail', redirect_to_store_detail ? 'true' : ''); 353 const response = await fetch(fetchUrl.toString()); 354 const data = await response.json(); 355 356 // 1. VložÃme HTML popis 357 const descElem = document.getElementById('ajax-selected-store'); 358 if (descElem) descElem.innerHTML = data.selected_store_html + '<div id="googleMap" style="height:400px;"></div>'; 359 360 // 2. SpustÃme vykreslenà mapy s daty z PHP 361 // Důležité: ujisti se, že Google Maps API je už naÄtené 362 if (typeof google !== 'undefined' && google.maps) { 363 renderGoogleMap(data.points); 364 } else { 365 console.error('Google mapy nebyly naÄteny.'); 366 } 367 368 // Skryju multiselecty 369 if (data.selected_store_id) { 370 const elem = document.getElementById('multiSelects'); 371 elem.classList.add('hidden'); 372 } 373 374 } catch (err) { 375 console.error('Chyba pÅi naÄÃtánà prodejny:', err); 376 } 377 } 378 379 async setSelectedItems(newItems) { 380 // newItems oÄekáváme jako pole objektů [{label: '...', value: '...'}, ...] 381 this.selectedItems = Array.isArray(newItems) ? newItems : []; 382 383 // 1. Aktualizace UI (tagy pod/v inputu) 384 this.renderTags(); 385 386 // 2. Aktualizace skrytého pole pro odeslánà formuláÅe 387 this.updateHiddenInput(); 388 389 // 3. Refresh výsledků vyhledávánà (aby se odÅ¡krtly/zaÅ¡krtly checkboxy, pokud je menu otevÅené) 390 this.renderResults(); 391 392 // 4. Volánà onSelect callbacku (pokud chcete, aby se po zmÄnÄ provedla logika jako getStore) 393 await this.selectItem(); 394 } 395 396}
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.