PageSourceSearch

https://www.donpealo.cz/src/components-NEW/multiselect/multiselect.js?1790316869

js donpealo.cz collected 2026-09-25 06:15:25 UTC 13,494 bytes, 396 lines download raw bytes

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">&times;</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.