PageSourceSearch

https://www.hauenstein-rafz.ch/hauenstein-wGlobal/wGlobal/layout/s…?%7Bphp:functionString('time')%7D

js hauenstein-rafz.ch collected 2026-09-24 20:53:06 UTC 66,545 bytes, 1,639 lines download raw bytes

1/**
2 * Doofinder Search Widget - Standalone Script
3 * Include this script in your website to add a search overlay
4 * 
5 * Usage:
6 * <script src="doofinder.js" 
7 *          data-hashid="106adda58e44f1d33097425e8e94f2cd"
8 *          data-zone="eu1"
9 *          data-open-button="#openSearch"
10 *          data-search-form="#searchFormDf"
11 *          data-results-container="#blockBody">
12 * </script>
13 */
14(function() {
15    'use strict';
16
17    // Default configuration - can be overridden by data attributes
18    const DEFAULT_CONFIG = {
19        // Doofinder API settings
20        HASHID: "",
21        ZONE: "eu1",
22        
23        // Preview limits
24        PREVIEW_LIMIT_PRODUCTS: 8,
25        PREVIEW_LIMIT_CONTENT: 5,
26        API_RPP: 100,
27        FULL_LIST_API_RPP: 50,
28        
29        // Behavior settings
30        DEBOUNCE_DELAY: 250,
31        MIN_QUERY_LENGTH: 2,
32        DESCRIPTION_PREVIEW_LENGTH: 140,
33        DESCRIPTION_PREVIEW_LENGTH_FULL: 180,
34        
35        // Open button selector - CSS selector string
36        OPEN_BUTTON: {
37            selector: '.search-trigger'
38        },
39        
40        // Open button selector - CSS selector string
41        SEARCH_FORM: {
42            selector: '#searchFormDf'
43        },
44        
45        // Results container selector - where to render search results
46        RESULTS_CONTAINER: {
47            selector: null
48        }
49    }; 
50
51    // Property facet mapping: API field -> Display label
52    const PROPERTY_FACET_MAP = {
53        'property_blütenfarbe': 'Blütenfarbe',
54        'property_blütezeit': 'Blütezeit',
55        'property_standort': 'Standort'
56    };
57    // 'property_qualitaet': 'Qualität',
58
59    // Read configuration from data attributes
60    function readConfigFromDataAttributes() {
61        const scriptTag = document.currentScript || document.querySelector('script[src*="doofinder.js"]');
62        if (!scriptTag) return DEFAULT_CONFIG;
63
64        const config = JSON.parse(JSON.stringify(DEFAULT_CONFIG)); // Deep copy
65
66        // Helper to read numeric data attributes
67        const getNumber = (key, defaultValue) => {
68            const value = scriptTag.getAttribute('data-' + key);
69            return value !== null ? parseInt(value, 10) : defaultValue;
70        };
71
72        // Helper to read string data attributes
73        const getString = (key, defaultValue) => {
74            const value = scriptTag.getAttribute('data-' + key);
75            return value !== null ? value : defaultValue;
76        };
77
78        // Override API settings
79        config.HASHID = getString('hashid', config.HASHID);
80        config.ZONE = getString('zone', config.ZONE);
81
82        // Override preview limits
83        config.PREVIEW_LIMIT_PRODUCTS = getNumber('preview-limit-products', config.PREVIEW_LIMIT_PRODUCTS);
84        config.PREVIEW_LIMIT_CONTENT = getNumber('preview-limit-content', config.PREVIEW_LIMIT_CONTENT);
85        config.API_RPP = getNumber('api-rpp', config.API_RPP);
86        config.FULL_LIST_API_RPP = getNumber('full-list-api-rpp', config.FULL_LIST_API_RPP);
87
88        // Override behavior settings
89        config.DEBOUNCE_DELAY = getNumber('debounce-delay', config.DEBOUNCE_DELAY);
90        config.MIN_QUERY_LENGTH = getNumber('min-query-length', config.MIN_QUERY_LENGTH);
91        config.DESCRIPTION_PREVIEW_LENGTH = getNumber('description-preview-length', config.DESCRIPTION_PREVIEW_LENGTH);
92        config.DESCRIPTION_PREVIEW_LENGTH_FULL = getNumber('description-preview-length-full', config.DESCRIPTION_PREVIEW_LENGTH_FULL);
93
94        // Override open button selector
95        const openButtonSelector = getString('open-button', null);
96        if (openButtonSelector) {
97            config.OPEN_BUTTON.selector = openButtonSelector;
98        }
99
100        // Override form selector
101        const searchFormSelector = getString('search-form', null);
102        if (searchFormSelector) {
103            config.SEARCH_FORM.selector = searchFormSelector;
104        }
105
106        // Override results container selector
107        const resultsContainerSelector = getString('results-container', null);
108        if (resultsContainerSelector) {
109            config.RESULTS_CONTAINER.selector = resultsContainerSelector;
110        }
111
112        return config;
113    }
114
115    // Configuration - read from data attributes or use defaults
116    const CONFIG = readConfigFromDataAttributes();
117
118    // Default placeholder image as SVG data URI
119    const DEFAULT_IMAGE = `data:image/svg+xml;charset=UTF-8,${encodeURIComponent(`
120        <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 54.46 72.72">
121            <defs><style>.cls-1{fill:#fff;}</style></defs>
122            <g><path class="cls-1" d="M51,46.1a13.29,13.29,0,0,1-4.11,7c-1.3,1-3.4,2.3-5.1,1.6a2.59,2.59,0,0,1-1.1-2.6,23.36,23.36,0,0,1,.4-2.6c.1-.3.1-.4.2-.8.3-1.7,7.5-37.7,7.9-40a15.94,15.94,0,0,0,.6-3,2.05,2.05,0,0,0-2.1-2.1h-.3a5,5,0,0,0-1.2.2l-4.6.6a6.88,6.88,0,0,0-1.3.2A3.26,3.26,0,0,0,37.93,7c-.1.7-3.1,15.4-3.6,17.6a1.7,1.7,0,0,1-1,1.3,3.19,3.19,0,0,1-1.4.3l-4.5.
1226a1.69,1.69,0,0,1-1.4-.1c-.3-.3-.2-.6-.1-1,.3-1.4,4.1-20.8,4.4-22.2.2-1.1.5-2.2-.5-3s-2.7-.3-4-.2l-4.2.6a3.4,3.4,0,0,0-3,2.7c-.2,1-4.6,22.5-5.2,25.2l-3,.4a9.76,9.76,0,0,0-3.3.7,4.71,4.71,0,0,0-1.9,4,1.92,1.92,0,0,0,2,1.9h.1c.4,0,.8-.1,1.2-.1l3.5-.5c-.8,4-1.6,7.9-2.4,11.9a58.91,58.91,0,0,1-2.7,10.2,43.23,43.23,0,0,1-4,7.7,42.33,42.33,0,0,0-2.5,4.2,3.13,3.13,0,0,0-.4,1.9,2.25,2.25,0,0,0,1.5,1.5c1.2.4,2.5-.2,3.6-.8a16.09,16.09,0,0,0,3.4-2.4,39.44,39.44,0,0,0,6.2-6.7,41.27,41.27,0,0,0,3.6-6.5,32,32,0,0,0,1.7-4.3c1.3-4.4,2.2-9,3.1-13.5.1-.7.3-1.5.4-2.2a12.86,12.86,0,0,1,.4-1.9c.4-.9,1.3-1,2.1-1.1l3-.4a14.6,14.6,0,0,1,2.2-.3c.9-.1,1,.7.8,1.3-.4,1.8-3.1,14.7-3.8,19.4-.5,3.1-.7,7,1.9,9.3,1.9,1.8,4.8,1.7,7.2,1.2A22.88,22.88,0,0,0,51,55a20.26,20.26,0,0,0,2.1-3.8,23.1,23.1,0,0,0,1.32-4.7C54.67,43.71,51.62,43.71,51,46.1Z"/>
123            </g>
124        </svg>
125    `)}`;
126
127    // Utility functions
128    const SearchUI = {
129        normalizeUrl(url) {
130            if (!url || url === '#') return '#';
131            if (url.startsWith('http')) return url;
132            return '/' + url.replace(/^\/+/, '');
133        },
134
135        formatPrice(price) {
136            return new Intl.NumberFormat('de-CH', {
137                style: 'currency',
138                currency: 'CHF'
139            }).format(price);
140        },
141
142        stripHtml(value) {
143            if (!value) return '';
144            const div = document.createElement('div');
145            div.innerHTML = value;
146            return div.textContent || div.innerText || '';
147        },
148
149        escapeHtml(value) {
150            if (value === null || value === undefined) return '';
151            const div = document.createElement('div');
152            div.textContent = String(value);
153            return div.innerHTML;
154        },
155
156        getImageUrl(item) {
157            return item.image_link || item.image_url || DEFAULT_IMAGE;
158        },
159
160        getItemTitle(item) {
161            return item.name || item.title || '';
162        },
163
164        getItemLink(item) {
165            return this.normalizeUrl(item.link || item.url);
166        }
167    };
168
169    // State management
170    const state = {
171        timer: null,
172        currentQuery: '',
173        currentMode: null,
174        currentApiPage: 1,
175        currentBuffer: [],
176        reachedEnd: false,
177        currentCategory: null,
178        originalContainerContent: null,
179        resultsContainer: null,
180        autoloadObserver: null,
181        isAutoloadEnabled: true,
182        currentFilters: {},
183        selectedCategories: [],
184        selectedPropertyFilters: {},
185        facets: null,
186        openFilterGroups: [],
187        productCount: 0,
188        contentCount: 0,
189        lastValidQuery: '',
190    };
191
192    // DOM element cache
193    const elements = {};
194
195    // URL Parameter Helper Functions
196    function updateUrlParams(params) {
197        // console.log("updateUrlParams called with:", params);
198        const url = new URL(window.location.href);
199        const searchParams = url.searchParams;
200
201        // Remove existing df_ parameters
202        const existingParams = Array.from(searchParams.keys());
203        existingParams.forEach(key => {
204            if (key.startsWith('df_')) {
205                searchParams.delete(key);
206            }
207        });
208
209        // Add new parameters
210        for (const [key, value] of Object.entries(params)) {
211            if (value === null || value === undefined) {
212                continue;
213            }
214            const paramKey = key.startsWith('df_') ? key : 'df_' + key;
215
216            if (Array.isArray(value)) {
217                value.forEach(v => {
218                    if (v !== null && v !== undefined && v !== '') {
219                        searchParams.append(paramKey, v);
220                    }
221                });
222            } else if (value !== '') {
223                searchParams.append(paramKey, value);
224            }
225        }
226
227        url.search = searchParams.toString();
228        window.history.replaceState({}, '', url.toString());
229    }
230    // Neue Funktion - liest den gesamten State und synchronisiert die URL
231    function syncUrlWithState() {
232        const params = {
233            query: state.currentQuery,
234            mode: state.currentMode,
235            page: state.currentApiPage
236        };
237        
238        // Kategorien nur hinzufügen, wenn welche ausgewählt sind
239        if (state.selectedCategories.length > 0) {
240            params.category = state.selectedCategories;
241        }
242        
243        // Property-Filter hinzufügen
244        for (const [prop, values] of Object.entries(state.selectedPropertyFilters)) {
245            if (values.length > 0) {
246                params[prop] = values.length === 1 ? values[0] : values;
247            }
248        }
249        
250        updateUrlParams(params);
251        // syncUrlWithState();
252
253    }
254    function readUrlParams() {
255        const url = new URL(window.location.href);
256        const searchParams = url.searchParams;
257        const paramGroups = {};
258
259        for (const [key, value] of searchParams.entries()) {
260            if (key.startsWith('df_')) {
261                const innerKey = key.substring(3);
262                if (innerKey) {
263                    if (!paramGroups[innerKey]) {
264                        paramGroups[innerKey] = [];
265                    }
266                    paramGroups[innerKey].push(value);
267                }
268            }
269        }
270
271        const result = {};
272        for (const [name, values] of Object.entries(paramGroups)) {
273            result[name] = values.length === 1 ? values[0] : values;
274        }
275        return result;
276    }
277
278    // Update button active states based on current mode
279    function updateButtonActiveStates(mode) {
280        // mode kann 'products', 'content', oder null sein
281        const productBtns = document.querySelectorAll('.btn-show-all-products');
282        const contentBtns = document.querySelectorAll('.btn-show-all-content');
283        // console.log('updateButtonActiveStates',mode,productBtns,contentBtns);
284
285        productBtns.forEach(btn => {
286            btn.setAttribute('data-active', mode === 'products' ? 'true' : 'false');
287        });
288
289        contentBtns.forEach(btn => {
290            btn.setAttribute('data-active', mode === 'content' ? 'true' : 'false');
291        });
292    }
293
294    // Generate HTML for the search overlay
295    function generateSearchOverlayHTML() {
296        return `
297            <div id="searchOverlay" class="search-overlay" role="dialog" aria-modal="true" aria-label="Suche">
298                <div class="search-header">
299                    <!--<img src="https://hauenstein.shopware.kempf-solutions.de/hauenstein-wGlobal/wGlobal/layout/images/logo-small.png" 
300                         class="logo" 
301                         alt="Hauenstein Logo" 
302                         width="80" 
303                         height="auto">-->
304                    <div class="search-input-wrap">
305                        <input class="searchInput" placeholder="Suche..." autocomplete="off" aria-label="Suchbegriff eingeben">
306                        <div id="suggestions" class="suggestions" role="listbox" aria-label="Suchvorschläge"></div>
307                    </div>
308                    <button id="closeSearch" type="button" aria-label="Suche schließen">
309                        <svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24">
310                            <path fill="#000" fill-rule="evenodd" d="M10.5857864,12 L3.29289322,4.70710678 C2.90236893,4.31658249 2.90236893,3.68341751 3.29289322,3.29289322 C3.68341751,2.90236893 4.31658249,2.90236893 4.70710678,3.29289322 L12,10.5857864 L19.2928932,3.29289322 C19.6834175,2.90236893 20.3165825,2.90236893 20.7071068,3.29289322 C21.0976311,3.68341751 21.0976311,4.31658249 20.7071068,4.70710678 L13.4142136,12 L20.7071068,19.2928932 C21.0976311,19.6834175 21.0976311,20.3165825 20.7071068,20.7071068 C20.3165825,21.0976311 19.6834175,21.0976311 19.2928932,20.7071068 L12,13.4142136 L4.70710678,20.7071068 C4.31658249,21.0976311 3.68341751,21.0976311 3.29289322,20.7071068 C2.90236893,20.3165825 2.90236893,19.6834175 3.29289322,19.2928932 L10.5857864,12 Z"></path>
311                        </svg>
312                    </button>
313                </div>
314
315                <div class="search-content">
316                    <div>
317                        <h5>Kategorien</h5>
318                        <div id="categoryResults" role="list"></div>
319                    </div>
320
321                    
322                    <div>
323                        <div class="banner-results"></div>
324                        <h5>Produktvorschläge</h5>
325                        <div id="productResults" class="products" role="list"></div>
326                    </div>
327
328                    <div>
329                        <h5>Inhalte</h5>
330                        <div id="contentResults" role="list"></div>
331                    </div>
332                </div>
333
334                <div class="search-actions">
335                    <button class="btn-show-all-products" type="button" disabled data-active="false">Alle Produkte anzeigen</button>
336                    <button class="btn-show-all-content" type="button" disabled data-active="false">Alle Inhalte anzeigen</button>
337                </div>
338            </div>
339        `;
340    }
341
342    function generateBannerHtml(banner) {
343        if (!banner || !banner.image) {
344            return '';
345        }
346
347        const target = banner.blank ? ' target="_blank"' : '';
348        const link = banner.link ? banner.link : '#';
349
350        return `
351            <div class="banner">
352                <a href="${link}"${target}>
353                    <picture>
354                        ${banner.mobile_image ? `<source media="(max-width: 768px)" srcset="${banner.mobile_image}">` : ''}
355                        <img src="${banner.image}" alt="" loading="lazy">
356                    </picture>
357                </a>
358            </div>
359        `;
360    }
361    
362    // Generate HTML for the full results page
363    function generateFullResultsPageHTML() {
364        return `<div id="fullResultsPage" class="full-results-page" role="dialog" aria-modal="true" aria-label="Vollständige Ergebnisse">
365            <div class="search-content">
366                <div class="header">
367                    <input class="searchInput" placeholder="Suche..." autocomplete="off" aria-label="Suchbegriff eingeben">
368                    <div class="search-mode">
369                        <button class="btn-show-all-products" type="button" data-active="false">Alle Produkte anzeigen</button>
370                        <button class="btn-show-all-content" type="button" data-active="false">Alle Inhalte anzeigen</button>
371                    </div>
372                    <div id="suggestionsFull" class="suggestions"></div>
373                </div>
374                <div class="search-grid">
375                    <div id="filter"><div class="toggler">Filter<span class="icon"><svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-funnel-icon lucide-funnel"><path d="M10 20a1 1 0 0 0 .553.895l2 1A1 1 0 0 0 14 21v-7a2 2 0 0 1 .517-1.341L21.74 4.67A1 1 0 0 0 21 3H3a1 1 0 0 0-.742 1.67l7.225 7.989A2 2 0 0 1 10 14z"/></svg></span></div><div class="inner"></div></div>
376                    <div>
377                        <div class="banner-results"></div>
378                        <div id="fullResults"></div>
379                    </div>
380                </div>
381            </div>
382            <div id="loadMoreWrapper" style="display: none;">
383                <div id="loadingIndicator" style="text-align: center; padding: 16px;">Lade mehr...</div>
384                <div id="autoloadTrigger"></div>
385            </div>
386            <!--<div class="search-actions">
387                <button id="backToSearch" type="button">← Zurück zur Suche</button>
388            </div>-->
389        </div>`;
390    }
391
392    // Inject search overlay HTML into the body
393    function injectOverlay() {
394        const overlayHTML = generateSearchOverlayHTML();
395        const tempDiv = document.createElement('div');
396        tempDiv.innerHTML = overlayHTML;
397        
398        // Append overlay elements to body
399        Array.from(tempDiv.children).forEach(child => {
400            document.body.appendChild(child);
401        });
402    }
403    
404    // Extract property facets from data.facets (property_* keys)
405    function extractPropertyFacets(facets) {
406        if (!facets) return {};
407        
408        const result = {};
409              
410        for (const [apiField, label] of Object.entries(PROPERTY_FACET_MAP)) {
411            const facetData = facets.find(item => item.key === apiField);
412            if (facetData && facetData.terms && facetData.terms.items && facetData.terms.items.length > 0) {
413                result[label] = facetData;
414            }
415        }
416        
417        return result;
418    }
419    function getApiFieldFromLabel(label) {
420        // Finde den API-Feldnamen basierend auf dem Label
421        for (const [apiField, facetLabel] of Object.entries(PROPERTY_FACET_MAP)) {
422            if (facetLabel === label) {
423                return apiField;
424            }
425        }
426        return null;
427    }
428    // API helper functions
429    function buildSearchUrl(query, page, rpp, categories = null) {
430        const baseUrl = `https://${CONFIG.ZONE}-search.doofinder.com/6/${CONFIG.HASHID}/_search`;
431        const params = new URLSearchParams({
432            query: query,
433            rpp: rpp,
434            page: page
435        });
436
437        // const propertyFacets = ['brand', 'categories', 'property_blütenfarbe', 'property_blütezeit', 'property_qualitaet', 'property_standort'];
438        // Request facets using PROPERTY_FACET_MAP
439        const propertyFacets = ['brand', 'categories', ...Object.keys(PROPERTY_FACET_MAP)];
440        propertyFacets.forEach((facetField, index) => {
441            params.append(`facets[${index}][field]`, facetField);
442            params.append(`facets[${index}][size]`, '12');
443        });
444
445        // Kategorie-Filter - Support both single category (string) and multiple categories (array)
446        if (categories && Array.isArray(categories) && categories.length > 0) {
447            categories.forEach((cat, index) => {
448                params.append(`filter[categories][${index}]`, cat);
449            });
450        }
451
452        // Property-Filter
453        for (const [label, values] of Object.entries(state.selectedPropertyFilters)) {
454            const apiField = getApiFieldFromLabel(label);
455            if (!apiField) continue;
456            
457            if (Array.isArray(values)) {
458                values.forEach((value, index) => {
459                    params.append(`filter[${apiField}][${index}]`, value);
460                });
461            } else {
462                params.append(`filter[${apiField}][0]`, values);
463            }
464        }
465
466        // console.log('buildSearchUrl,base',baseUrl,params);
467        return `${baseUrl}?${params.toString()}`;
468    }
469
470    async function fetchSearchPage(query, page, rpp, categories = null) {
471        const url = buildSearchUrl(query, page, rpp, categories);
472        const response = await fetch(url);
473        return response.json();
474    }
475
476    // Render functions
477    function renderProducts(products, limit) {
478        // Always render in overlay
479        elements.productResults.innerHTML = products
480            .slice(0, limit)
481            .map(item => createProductHtml(item))
482            .join('');
483    }
484
485    function createProductHtml(item) {
486        return `
487            <a class="product" href="${SearchUI.getItemLink(item)}" tabindex="0">
488                <img src="${SearchUI.getImageUrl(item)}" alt="" loading="lazy" class="${item.image_link || item.image_url ? '' : 'placeholder'}">
489                <div>
490                    <div class="product-name">${SearchUI.escapeHtml(SearchUI.getItemTitle(item))}</div>
491                    ${item.custom_custom_attr_botanical 
492                        ? `<div class="product-name-botany">${SearchUI.escapeHtml(item.custom_custom_attr_botanical)}</div>` 
493                        : ''}
494                    ${item.price 
495                        ? `<div class="product-price">${item.variantCount > 1 ? 'ab ' : ''}${SearchUI.formatPrice(item.price)}</div>` 
496                        : ''}
497                </div>
498            </a>
499        `;
500    }
501
502    function renderContent(contents, limit) {
503        // Always render in overlay
504        elements.contentResults.innerHTML = contents
505            .slice(0, limit)
506            .map(item => createContentHtml(item))
507            .join('');
508    }
509
510    function createContentHtml(item) {
511        const description = item.description 
512            ? SearchUI.stripHtml(item.description).substring(0, CONFIG.DESCRIPTION_PREVIEW_LENGTH) + '...' 
513            : '';
514        
515        return `
516            <a class="content-result" href="${SearchUI.getItemLink(item)}" tabindex="0">
517                <strong>${SearchUI.escapeHtml(item.name || item.title || '')}</strong>
518                ${description ? `<p>${SearchUI.escapeHtml(description)}</p>` : ''}
519            </a>
520        `;
521    }
522
523    function renderCategories(categories) {
524      	// console.log("Categories: ", categories);
525        // Always render in overlay
526        elements.categoryResults.innerHTML = categories
527            .slice(0, 6)
528            .map(cat => `
529                <button type="button" class="category-filter" data-category="${SearchUI.escapeHtml(cat.name)}">
530                    ${SearchUI.escapeHtml(cat.name)}&nbsp;<span>
530(${cat.count})</span>
531                </button>
532            `)
533            .join('');
534    }
535
536    // Render filter group as accordion (appends to filter container)
537    function renderFilterGroup(title, itemsHtml, isOpen) {
538        const filterContainer = document.getElementById('filter');
539        if (!filterContainer) {
540            return;
541        }
542
543        const innerContainer = filterContainer.querySelector('.inner');
544        if (!innerContainer) {
545            return;
546        }
547
548        const openClass = isOpen ? 'is-open' : '';
549
550        const groupHtml = `
551            <div class="filter-group">
552                <button type="button" class="filter-group-header" aria-expanded="${isOpen}">
553                    ${SearchUI.escapeHtml(title)}
554                    <span class="filter-group-arrow" aria-hidden="true">▼</span>
555                </button>
556                <div class="filter-group-content ${openClass}">
557                    ${itemsHtml}
558                </div>
559            </div>
560        `;
561
562        // Append to inner container (supports multiple groups)
563        innerContainer.insertAdjacentHTML('beforeend', groupHtml);
564
565        // Attach accordion toggle handler to the newly added group
566        const newGroup = innerContainer.lastElementChild;
567        const header = newGroup.querySelector('.filter-group-header');
568        const content = newGroup.querySelector('.filter-group-content');
569        header.addEventListener('click', () => {
570            const isOpen = content.classList.contains('is-open');
571            content.classList.toggle('is-open');
572            header.setAttribute('aria-expanded', !isOpen);
573        });
574    }
575
576    // Render category filters as accordion for the Full Results Page
577    function renderFilterCategories(categories) {
578        // console.log('renderFilterCategories');
579        
580        // Clear inner container before rendering (first group clears existing content)
581        const filterContainer = document.getElementById('filter');
582        if (filterContainer) {
583            const innerContainer = filterContainer.querySelector('.inner');
584            if (innerContainer) {
585                innerContainer.innerHTML = '';
586            }
587        }
588
589        if (!categories || categories.length === 0) {
590            return;
591        }
592
593        // Build checkbox list HTML
594        const checkboxes = categories.map(cat => {
595            const isChecked = state.selectedCategories.includes(cat.name);
596            return `
597                <div class="filter-category-item">
598                    <label>
599                        <input type="checkbox" class="filter-category-checkbox" 
600                               data-category="${SearchUI.escapeHtml(cat.name)}" 
601                               ${isChecked ? 'checked' : ''}>
602                        ${SearchUI.escapeHtml(cat.name)} <span class="count">${cat.count}</span>
603                    </label>
604                </div>
605            `;
606        }).join('');
607
608        // Render as accordion group
609        // Beim ersten Laden (leeres Array) ist es offen, beim Nachladen nur wenn vorher offen
610        const isFirstLoad = !state.openFilterGroups || state.openFilterGroups.length === 0;
611        const wasOpen = state.openFilterGroups && state.openFilterGroups.includes('Kategorien');
612        const shouldOpen = isFirstLoad || wasOpen;
613        renderFilterGroup('Kategorien', checkboxes, shouldOpen);
614
615
616
617    }
618
619    // Render property facets as accordion filters
620    function renderPropertyFilters(propertiesFacets) {
621        // console.log('renderPropertyFilters',renderPropertyFilters);
622        if (!propertiesFacets) {
623            return;
624        }
625
626        // Iterate through property facets and render each as a filter group
627        // for (const propName of Object.keys(propertyLabels)) {
628        for (const [label, propertyData] of Object.entries(propertiesFacets)) {
629            // const propertyData = propertiesFacets[propName];
630            if (!propertyData || !propertyData.terms || !propertyData.terms.items || propertyData.terms.items.length === 0) {
631                continue;
632            }
633
634            // const label = propertyLabels[propName];
635            const buckets = propertyData.terms.items;
636
637            // Build checkbox list HTML for this property
638            const checkboxes = buckets.map(bucket => {
639                // Check if this value is currently selected
640                const isSelected = state.selectedPropertyFilters[label] && state.selectedPropertyFilters[label].includes(bucket.name);
641                
642                return `
643                    <div class="filter-property-item">
644                        <label>
645                            <input type="checkbox" class="filter-property-checkbox" 
646                                   data-property="${SearchUI.escapeHtml(label)}" 
647                                   data-value="${SearchUI.escapeHtml(bucket.name)}"
648                                   ${isSelected ? 'checked' : ''}>
649                            ${SearchUI.escapeHtml(bucket.name)} (${bucket.count})
650                        </label>
651                    </div>
652                `;
653            }).join('');
654
655            // Render as accordion group (closed by default)
656            renderFilterGroup(label, checkboxes, false);
657        }
658    }
659
660    // Handle checkbox change for property filters
661    function handleFilterPropertyChange(event) {
662        const checkbox = event.target;
663        const property = checkbox.dataset.property;
664        const value = checkbox.dataset.value;
665
666        if (!state.selectedPropertyFilters[property]) {
667            state.selectedPropertyFilters[property] = [];
668        }
669
670        if (checkbox.checked) {
671            if (!state.selectedPropertyFilters[property].includes(value)) {
672                state.selectedPropertyFilters[property].push(value);
673            }
674        } else {
675            state.selectedPropertyFilters[property] = state.selectedPropertyFilters[property].filter(v => v !== value);
676            // Remove empty property keys
677            if (state.selectedPropertyFilters[property].length === 0) {
678                delete state.selectedPropertyFilters[property];
679            }
680        }
681
682        // Reset and reload results with selected filters
683        state.currentApiPage = 1;
684        state.currentBuffer = [];
685        state.reachedEnd = false;
686
687        syncUrlWithState();
688
689        // Reload results
690        loadNextFullChunk(false);
691    }
692
693    // Handle checkbox change for category filters
694    function handleFilterCategoryChange(event) {
695        const checkbox = event.target;
696        const category = checkbox.dataset.category;
697		// console.log('==========');
698        // console.log(state.selectedCategories);
699        if (checkbox.checked) {
700            if (!state.selectedCategories.includes(category)) {
701                state.selectedCategories.push(category);
702            }
703        } else {
704            state.selectedCategories = state.selectedCategories.filter(c => c !== category);
705        }
706
707        // Reset and reload results with selected categories
708        state.currentApiPage = 1;
709        state.currentBuffer = [];
710        state.reachedEnd = false;
711
712        // updateUrlParams({
713        //     category: state.selectedCategories.length > 0 ? state.selectedCategories : null
714        // });
715        syncUrlWithState();
716
717        // Reload results
718        loadNextFullChunk(false);
719    }
720
721    function renderFullResults(items, append) {
722        // Build headline based on selected categories or mode
723        let headline;
724        if (state.selectedCategories.length > 0) {
725            headline = state.selectedCategories.join(', ');
726        } else {
727            headline = state.currentMode === 'products' ? 'Alle Produkte' : 'Alle Inhalte';
728        }
729            
730        const itemsHtml = items.map(item =>
731            state.currentMode === 'products'
732            ? createFullProductHtml(item)
733            : createFullContentHtml(item)
734        ).join('');
735
736        // Bei append=false: Komplettes HTML mit Headline und Liste
737        if (!append) {
738            elements.fullResults.innerHTML = `<h5>${SearchUI.escapeHtml(headline)}</h5><div class="full-result-list ${state.currentMode}">${itemsHtml}</div>`;
739            return;
740        }
741
742        // Bei append=true: Nur Items einfügen
743        if (elements.fullResults.querySelector('.full-result-list')) {
744            // In bestehende Liste einfügen
745            elements.fullResults.querySelector('.full-result-list').insertAdjacentHTML('beforeend', itemsHtml);
746        } else {
747            // Neue Liste anhängen
748            elements.fullResults.insertAdjacentHTML('beforeend', `<div class="full-result-list ${state.currentMode}">${itemsHtml}</div>`);
749        }
750    }
751
752    function createFullProductHtml(item) {
753        return `
754            <a class="product" href="${SearchUI.getItemLink(item)}" tabindex="0">
755                <img src="${SearchUI.getImageUrl(item)}" alt="" loading="lazy" class="${item.image_link || item.image_url ? '' : 'placeholder'}">
756                <div>
757                    <div class="product-name">${SearchUI.escapeHtml(SearchUI.getItemTitle(item))}</div>
758                    ${item.custom_custom_attr_botanical 
759                        ? `<div class="product-name-botany">${SearchUI.escapeHtml(item.custom_custom_attr_botanical)}</div>` 
760                        : ''}
761                    ${item.price 
762                        ? `<div class="product-price">${item.variantCount > 1 ? 'ab ' : ''}${SearchUI.formatPrice(item.price)}</div>` 
763                        : ''}
764                </div>
765            </a>
766        `;
767    }
768
769    function createFullContentHtml(item) {
770        const description = item.description 
771            ? SearchUI.stripHtml(item.description).substring(0, CONFIG.DESCRIPTION_PREVIEW_LENGTH_FULL) + '...' 
772            : '';
773        
774        return `
775            <a class="full-content-item" href="${SearchUI.getItemLink(item)}" tabindex="0">
776                <strong>${SearchUI.escapeHtml(item.name || item.title || '')}</strong>
777                ${description ? `<p>${SearchUI.escapeHtml(description)}</p>` : ''}
778            </a>
779        `;
780    }
781
782    function renderSuggestions(query, data) {
783        const suggestions = collectSuggestions(data);
784        const finalSuggestions = suggestions.slice(0, 5);
785
786        if (!finalSuggestions.length) {
787            hideSuggestions();
788            return;
789        }
790
791        const suggestionsHTML = `
792            <span class="suggestions-label">Suchvorschläge:</span>
793            ${finalSuggestions.map(value => `
794                <button type="button" class="suggestion-item" data-suggestion="${SearchUI.escapeHtml(value)}">
795                    ${SearchUI.escapeHtml(value)}
796                </button>
797            `).join('')}
798        `;
799
800        elements.suggestionsContainer.innerHTML = suggestionsHTML;
801        elements.suggestionsContainer.classList.add('is-visible');
802
803        // Auch in fullResultsPage anzeigen
804        const suggestionsFull = document.getElementById('suggestionsFull');
805        if (suggestionsFull) {
806            suggestionsFull.innerHTML = suggestionsHTML;
807            suggestionsFull.classList.add('is-visible');
808        }
809    }
810
811    function collectSuggestions(data) {
812        const suggestions = [];
813        const seen = new Set();
814
815        // Extract from results
816        (data.results || []).forEach(item => {
817            const title = item.name || item.title || '';
818            title.split(' ').forEach(word => {
819                const clean = word.toLowerCase().replace(/[^a-zäöü0-9-]/gi, '');
820                if (clean.length >= 4 && !clean.includes(state.currentQuery.toLowerCase()) && !seen.has(clean)) {
821                    seen.add(clean);
822                    suggestions.push(clean);
823                }
824            });
825        });
826
827        // Add categories
828        (data.facets?.find(item => item.name == "categories")?.terms?.items || []).forEach(cat => {
829            if (!seen.has(cat.name)) {
830                seen.add(cat.name);
831                suggestions.push(cat.name);
832            }
833        });
834
835        return suggestions;
836    }
837
838    function hideSuggestions() {
839        elements.suggestionsContainer.classList.remove('is-visible');
840        elements.suggestionsContainer.innerHTML = '';
841
842        // Auch in fullResultsPage ausblenden
843        const suggestionsFull = document.getElementById('suggestionsFull');
844        if (suggestionsFull) {
845            suggestionsFull.classList.remove('is-visible');
846            suggestionsFull.innerHTML = '';
847        }
848    }
849
850    // Main search function
851    async function search(query) {
852        // console.log('search',query);
853        const data = await fetchSearchPage(query, 1, CONFIG.API_RPP);
854        const results = data.results || [];
855
856        // Cache the last valid query for fallback in showFullList()
857        state.lastValidQuery = query;
858         
859        //  console.log(results[0]);
860
861        const products = results.filter(item => item.df_grouping_id.includes('products'));
862        const contents = results.filter(item => item.df_grouping_id.includes('content'));
863
864        renderSuggestions(query, data);
865
866        // Render banner if present
867        const bannerEl = document.querySelector('#searchOverlay .banner-results');
868        if (bannerEl) {
869            bannerEl.innerHTML = generateBannerHtml(data.banner);
870        }
871
872        renderProducts(products, CONFIG.PREVIEW_LIMIT_PRODUCTS);
873        renderContent(contents, CONFIG.PREVIEW_LIMIT_CONTENT);
874        renderCategories(data.facets?.find(item => item.key == "categories")?.terms?.items || []);
875
876        // Disable/enable all product buttons based on results
877        const productBtns = document.querySelectorAll('.btn-show-all-products');
878        productBtns.forEach(btn => btn.disabled = products.length === 0);
879
880        // Disable/enable all content buttons based on results
881        const contentBtns = document.querySelectorAll('.btn-show-all-content');
882        contentBtns.forEach(btn => btn.disabled = contents.length === 0);
883    }
884
885    // Setup autoload with IntersectionObserver
886    function setupAutoload() {
887        const autoloadTrigger = document.getElementById('autoloadTrigger');
888        if (!autoloadTrigger || !state.isAutoloadEnabled) return;
889
890        // Clear existing observer
891        if (state.autoloadObserver) {
892            state.autoloadObserver.disconnect();
893        }
894
895        // Create new observer
896        state.autoloadObserver = new IntersectionObserver(async (entries) => {
897            // If we've reached the end, don't load more
898            if (state.reachedEnd) return;
899
900            // If already loading, don't trigger again
901            const loadingIndicator = document.getElementById('loadingIndicator');
902            if (loadingIndicator && loadingIndicator.style.display === 'block') return;
903
904            // Trigger when the trigger element is visible
905            const entry = entries[0];
906            if (entry && entry.isIntersecting) {
907                // Show loading indicator
908                if (loadingIndicator) {
909                    loadingIndicator.style.display = 'block';
910                }
911
912                // Load next chunk
913                await loadNextFullChunk(true);
914
915                // Hide loading indicator
916                if (loadingIndicator) {
917                    loadingIndicator.style.display = 'none';
918                }
919            }
920        }, {
921            root: null, // viewport
922            rootMargin: '100px', // trigger 100px before element is visible
923            threshold: 0.1
924        });
925
926        // Observe the trigger element
927        state.autoloadObserver.observe(autoloadTrigger);
928    }
929
930    // Load more functionality
931    async function loadNextFullChunk(append) {
932        // console.log('loadNextFullChunk',append,state);
933        let itemsToRender = [];
934
935        // Load one page at a time and render all results from that page
936        if (state.currentBuffer.length === 0) {
937            // Use lastValidQuery as fallback if currentQuery is too short
938            const queryForSearch = (state.currentQuery.length >= CONFIG.MIN_QUERY_LENGTH)
939                ? state.currentQuery
940                : state.lastValidQuery;
941            
942            // Use selectedCategories if any are selected, otherwise null
943            const categories = state.selectedCategories.length > 0 ? state.selectedCategories : null;
944            const data = await fetchSearchPage(queryForSearch, state.currentApiPage, CONFIG.FULL_LIST_API_RPP, categories);
945            renderSuggestions(queryForSearch, data);
946
947            // Render banner if present (only on first load)
948            if (!append) {
949                const bannerEl = document.querySelector('#fullResultsPage .banner-results');
950                if (bannerEl) {
951                    bannerEl.innerHTML = generateBannerHtml(data.banner);
952                }
953            }
954            
955            // Zähle Produkte und Inhalte und aktualisiere Button-Zustände
956            // Nur beim ersten Laden (append = false) Counts berechnen und Buttons aktualisieren
957            if (!append) {
958                const allResults = data.results || [];
959                const productCount = allResults.filter(item => item.df_grouping_id.includes('products')).length;
960                const contentCount = allResults.filter(item => item.df_grouping_id.includes('content')).length;
961                // console.log('productCount',productCount);
962                // console.log('contentCount',contentCount);
963
964                // Buttons auf fullResultsPage aktualisieren
965                const fullResultsPage = document.getElementById('fullResultsPage');
966                if (fullResultsPage) {
967                    const productBtns = fullResultsPage.querySelectorAll('.btn-show-all-products');
968                    productBtns.forEach(btn => btn.disabled = productCount === 0);
969                    
970                    const contentBtns = fullResultsPage.querySelectorAll('.btn-show-all-content');
971                    contentBtns.forEach(btn => btn.disabled = contentCount === 0);
972                }
973            }
974
975            if (data.facets && state.currentMode === 'products') {
976                const filterEl = document.getElementById('filter');
977                if (filterEl) filterEl.classList.remove('df-hidden');
978                state.facets = data.facets;
979                // Render category filters in the sidebar
980                const categoriesBuckets = data.facets?.find(item => item.key == "categories")?.terms?.items || [];
981                // Speichere welche Filter-Gruppen offen sind
982                const filterContainer = document.getElementById('filter');
983                if (filterContainer) {
984                    const innerContainer = filterContainer.querySelector('.inner');
985                    if (innerContainer) {
986                        const openGroups = innerContainer.querySelectorAll('.filter-group-content.is-open');
987                        state.openFilterGroups = Array.from(openGroups).map(el => {
988                            // Hole den Titel der Gruppe (der vorherige Geschwister-Header)
989                            const header = el.previousElementSibling;
990                            return header ? header.textContent.trim() : '';
991                        });
992                    }
993                }
994                renderFilterCategories(categoriesBuckets);
995                
996                // Extract and render property facets from data.facets.property_*
997                const propertyFacets = extractPropertyFacets(data.facets);
998                if (!append) {
999                    // Entferne ausgewählte Property-Filter, die nicht mehr verfügbar sind
1000                    for (const prop of Object.keys(state.selectedPropertyFilters)) {
1001                        // Wenn die Property nicht mehr in den neuen Facets vorhanden ist
1002                        if (!propertyFacets[prop]) {
1003                            delete state.selectedPropertyFilters[prop];
1004                        }
1005                    }
1006                    // Bereinige veraltete Kategorien
1007                    const availableCategories = data.facets?.find(item => item.key == "categories")?.terms?.items.map(b => b.name) || [];
1008                    state.selectedCategories = state.selectedCategories.filter(cat => availableCategories.includes(cat));
1009
1010                    // URL nach dem Bereinigen aktualisieren
1011                    syncUrlWithState();
1012                }
1013                // console.log('propertyFacets',propertyFacets);
1014                if (Object.keys(propertyFacets).length > 0) {
1015                    renderPropertyFilters(propertyFacets);
1016                }         
1017                
1018
1019            } else {
1020                const filterEl = document.getElementById('filter');
1021                if (filterEl) filterEl.classList.add('df-hidden');
1022            }
1023
1024            if(!append){
1025                // Stellt die offenen Filter-Gruppen wieder her
1026                if (state.openFilterGroups && state.openFilterGroups.length > 0) {
1027                    const filterContainer = document.getElementById('filter');
1028                    if (filterContainer) {
1029                        const innerContainer = filterContainer.querySelector('.inner');
1030                        if (innerContainer) {
1031                            const allHeaders = innerContainer.querySelectorAll('.filter-group-header');
1032                            allHeaders.forEach(header => {
1033                                if (state.openFilterGroups.includes(header.textContent.trim())) {
1034                                    const content = header.nextElementSibling;
1035                                    if (content) {
1036                                        content.classList.add('is-open');
1037                                        header.setAttribute('aria-expanded', 'true');
1038                                    }
1039                                }
1040                            });
1041                        }
1042                    }
1043                }
1044            }
1045            
1046            const pageResults = data.results || [];
1047
1048            if (pageResults.length === 0) {
1049                state.reachedEnd = true;
1050                renderFullResults([], append);
1051                elements.loadMoreWrapper.style.display = 'none';
1052                return;
1053            }
1054
1055            state.currentBuffer = pageResults.filter(item => item.df_grouping_id.includes(state.currentMode));
1056            state.currentApiPage++;
1057
1058            if (pageResults.length < CONFIG.FULL_LIST_API_RPP) {
1059                state.reachedEnd = true;
1060            }
1061        }
1062
1063        // Render all items from the current buffer
1064        itemsToRender = [...state.currentBuffer];
1065        state.currentBuffer = [];
1066
1067        renderFullResults(itemsToRender, append);
1068
1069        // Check if we should show the wrapper or hide it
1070        const hasMoreResults = !state.reachedEnd || state.currentBuffer.length > 0;
1071        elements.loadMoreWrapper.style.display = hasMoreResults ? 'block' : 'none';
1072
1073        // Setup autoload if there are more results
1074        if (hasMoreResults && state.isAutoloadEnabled) {
1075            setupAutoload();
1076        }
1077    }
1078
1079    function setOverlayOpenState(isOpen) {
1080        if (isOpen) {
1081            // set --overlay-top-position
1082            const searchFormRect = elements.searchForm.getBoundingClientRect();
1083            document.body.style.setProperty('--overlay-top-position',searchFormRect.top + searchFormRect.height + 'px');
1084
1085            elements.overlay.classList.add('is-open');
1086            // Disable body scrolling when overlay is open
1087            document.body.style.overflow = 'hidden';
1088        } else {
1089            elements.overlay.classList.remove('is-open', 'has-query');
1090            // Re-enable body scrolling when overlay is closed
1091            document.body.style.overflow = '';
1092        }
1093    }
1094
1095    function showOverlay() {
1096        // console.log('showOverlay called',elements.searchForm);
1097        // Hide fullResultsPage if it exists (it's created dynamically in showFullList)
1098        const fullResultsPage = document.getElementById('fullResultsPage');
1099        
1100        setOverlayOpenState(true);
1101        elements.overlay.classList.add('has-query');
1102
1103        state.currentQuery = elements.searchFormInput.value ? elements.searchFormInput.value : 'top';
1104
1105        search(state.currentQuery);
1106        elements.input.focus();
1107    }
1108
1109    function closeOverlay() {
1110        setOverlayOpenState(false);
1111        elements.input.value = '';
1112        state.currentQuery = '';
1113        
1114        // Show results container content again if configured
1115        if (state.resultsContainer) {
1116            state.resultsContainer.style.display = '';
1117        }
1118        
1119        clearOverlayResults();
1120    }
1121
1122    function clearOverlayResults() {
1123        // console.log('Clearing overlay results');
1124        // Always clear overlay results
1125        elements.productResults.innerHTML = '';
1126        elements.categoryResults.innerHTML = '';
1127        elements.contentResults.innerHTML = '';
1128        // Disable product buttons - only in the overlay
1129        const productBtns = document.querySelectorAll('#searchOverlay .btn-show-all-products');
1130        productBtns.forEach(btn => btn.disabled = true);
1131        
1132        // Disable content buttons - only in the overlay
1133        const contentBtns = document.querySelectorAll('#searchOverlay .btn-show-all-content');
1134        contentBtns.forEach(btn => btn.disabled = true);
1135    }
1136
1137    function showFullList(type) {
1138        console.log( 'showFullList called with type:', type);
1139        if (state.currentQuery.length < CONFIG.MIN_QUERY_LENGTH) {
1140            // Fallback to last valid query if current query is too short
1141            if (!state.lastValidQuery || state.lastValidQuery.length < CONFIG.MIN_QUERY_LENGTH) return;
1142            // Don't overwrite state.currentQuery to keep the input field in sync
1143        }
1144
1145        state.currentMode = type;
1146        state.currentApiPage = 1;
1147        state.currentBuffer = [];
1148        state.reachedEnd = false;
1149
1150        // Update URL with current search parameters
1151        // updateUrlParams({
1152        //     query: state.currentQuery,
1153        //     mode: type,
1154        //     page: 1,
1155        //     category: state.selectedCategories.length > 0 ? state.selectedCategories : null
1156        // });
1157        syncUrlWithState();
1158
1159        // Close overlay (this will also set body overflow: hidden)
1160        setOverlayOpenState(false);
1161
1162        // Create fullResultsPage dynamically if it doesn't exist
1163        let fullResultsPage = document.getElementById('fullResultsPage');
1164        if (!fullResultsPage) {
1165            const fullResultsHTML = generateFullResultsPageHTML();
1166            const tempDiv = document.createElement('div');
1167            tempDiv.innerHTML = fullResultsHTML;
1168            fullResultsPage = tempDiv.firstChild;
1169            document.body.appendChild(fullResultsPage);
1170        }
1171
1172        fullResultsPage.classList.add('is-visible');
1173        fullResultsPage.setAttribute('data-mode', state.currentMode);
1174
1175        const fullResultsSearchInput = fullResultsPage.querySelector('.searchInput');
1176        if (fullResultsSearchInput) {
1177            // Set the query value before syncing, so syncSearchInputValues propagates the correct value to all inputs
1178            if (state.currentQuery) {
1179                fullResultsSearchInput.value = state.currentQuery;
1180            }
1181            bindSearchInputEvents(fullResultsSearchInput);
1182            syncSearchInputValues(fullResultsSearchInput);
1183        }
1184
1185        // Cache DOM elements for fullResultsPage
1186        elements.backToSearchBtn = document.getElementById('backToSearch');
1187        elements.fullResults = document.getElementById('fullResults');
1188        elements.loadMoreWrapper = document.getElementById('loadMoreWrapper');
1189        elements.autoloadTrigger = document.getElementById('autoloadTrigger');
1190        elements.loadingIndicator = document.getElementById('loadingIndicator');
1191
1192        if (elements.backToSearchBtn) {
1193            elements.backToSearchBtn.style.display = 'block';
1194            // Add event listener for backToSearchBtn
1195            elements.backToSearchBtn.addEventListener('click', showBackToSearch);
1196        }
1197
1198        // Wrap original content and show results in a separate wrapper
1199        if (state.resultsContainer) {
1200            // Check if we already have the original content wrapper
1201            const existingOriginalContent = state.resultsContainer.querySelector('.search-original-content');
1202            
1203            if (!existingOriginalContent) {
1204                // Create wrapper for original content and move content into 
1204it
1205                const originalContent = document.createElement('div');
1206                originalContent.className = 'search-original-content';
1207                originalContent.style.display = 'none';
1208                
1209                // Move all existing children into the original content wrapper
1210                while (state.resultsContainer.firstChild) {
1211                    originalContent.appendChild(state.resultsContainer.firstChild);
1212                }
1213                
1214                // Add the original content wrapper to the container
1215                state.resultsContainer.appendChild(originalContent);
1216            }
1217            
1218            // Create or clear the results wrapper
1219            let resultsWrapper = state.resultsContainer.querySelector('.search-results-wrapper');
1220            if (!resultsWrapper) {
1221                resultsWrapper = document.createElement('div');
1222                resultsWrapper.className = 'search-results-wrapper';
1223                state.resultsContainer.appendChild(resultsWrapper);
1224            } else {
1225                resultsWrapper.innerHTML = '';
1226            }
1227            
1228            // Store reference to the results wrapper for rendering
1229            state.resultsResultsWrapper = resultsWrapper;
1230            
1231            // Append fullResultsPage to search-results-wrapper
1232            if (fullResultsPage && fullResultsPage.parentNode !== resultsWrapper) {
1233                resultsWrapper.appendChild(fullResultsPage);
1234            }
1235        }
1236
1237        loadNextFullChunk(false);
1238        
1239        // Update button active states
1240        updateButtonActiveStates(type);
1241
1242    }
1243
1244    function showBackToSearch() {
1245        // console.log( 'showBackToSearch called');
1246        
1247        // Open overlay (this will also set body overflow: hidden)
1248        setOverlayOpenState(true);
1249
1250        state.currentMode = null;
1251        state.selectedCategories = [];
1252        state.selectedPropertyFilters = {};
1253        state.currentApiPage = 1;
1254        state.currentBuffer = [];
1255        state.reachedEnd = false;
1256
1257        // Reset button active states
1258        updateButtonActiveStates(null);
1259
1260        // Clean up autoload observer
1261        if (state.autoloadObserver) {
1262            state.autoloadObserver.disconnect();
1263            state.autoloadObserver = null;
1264        }
1265
1266        // Restore original content and remove wrappers
1267        if (state.resultsContainer) {
1268            const originalContent = state.resultsContainer.querySelector('.search-original-content');
1269            const resultsWrapper = state.resultsContainer.querySelector('.search-results-wrapper');
1270            
1271            // Remove the results wrapper (this also removes fullResultsPage inside it)
1272            if (resultsWrapper) {
1273                resultsWrapper.remove();
1274            }
1275            
1276            // Show the original content and remove the wrapper
1277            if (originalContent) {
1278                state.resultsContainer.innerHTML = originalContent.innerHTML;
1279                originalContent.remove();
1280            }
1281        }
1282        if (state.currentQuery.length >= CONFIG.MIN_QUERY_LENGTH) {
1283            elements.overlay.classList.add('has-query');
1284            elements.input.value = state.currentQuery;
1285            search(state.currentQuery);
1286        } else {
1287            elements.overlay.classList.remove('has-query');
1288            clearOverlayResults();
1289        }
1290
1291        elements.input.focus();
1292    }
1293
1294    // Event handlers
1295    function handleCategoryClick(event) {
1296        const button = event.target.closest('.category-filter');
1297        // console.log('handleCategoryClick',button);
1298        if (!button) return;
1299
1300        state.selectedCategories = [button.dataset.category];
1301        showFullList('products');
1302    }
1303
1304    function handleSuggestionClick(event) {
1305        const button = event.target.closest('.suggestion-item');
1306        // console.log('handleSuggestionClick',button);
1307        if (!button) return;
1308
1309        const value = button.dataset.suggestion;
1310        state.currentQuery = value;
1311        syncSearchInputValues({ value: value });
1312
1313        // Check if fullResultsPage is currently active
1314        const fullResultsPage = document.getElementById('fullResultsPage');
1315        const isFullResultsActive = fullResultsPage && fullResultsPage.classList.contains('is-visible');
1316
1317        if (isFullResultsActive) {
1318            // Full results page is active - reload results there
1319            const searchFormInput = document.querySelector(CONFIG.SEARCH_FORM.selector + ' input');
1320            if (searchFormInput) {
1321                searchFormInput.value = value;
1322            }
1323            
1324            // Ensure mode is set (default to current mode or 'products')
1325            if (!state.currentMode) {
1326                state.currentMode = 'products';
1327            }
1328            
1329            state.currentApiPage = 1;
1330            state.currentBuffer = [];
1331            state.reachedEnd = false;
1332            
1333            syncUrlWithState();
1334            loadNextFullChunk(false);
1335        } else {
1336            // Overlay is active - update overlay
1337            elements.input.value = value;
1338            elements.overlay.classList.add('has-query');
1339            search(value);
1340        }
1341    }
1342
1343    // Find DOM element by selector string (supports #id, .class, or tag)
1344    function findElement(selector) {
1345        if (!selector) return null;
1346        return document.querySelector(selector);
1347    }
1348
1349    function debounce(func, timeout = 300) {
1350        let timer;
1351        return (...args) => {
1352            clearTimeout(timer);
1353            timer = setTimeout(() => {
1354                func.apply(this, args);
1355            }, timeout);
1356        };
1357    }
1358
1359    function getSearchInputElements() {
1360        const inputs = [];
1361
1362        document.querySelectorAll('input.searchInput').forEach(input => {
1363            if (input && !inputs.includes(input)) {
1364                inputs.push(input);
1365            }
1366        });
1367
1368        if (elements.searchFormInput && !inputs.includes(elements.searchFormInput)) {
1369            inputs.push(elements.searchFormInput);
1370        }
1371
1372        if (elements.input && elements.input !== elements.searchFormInput && !inputs.includes(elements.input)) {
1373            inputs.push(elements.input);
1374        }
1375
1376        return inputs;
1377    }
1378
1379    function syncSearchInputValues(activeInput) {
1380        const value = activeInput ? activeInput.value.trim() : '';
1381        const inputs = getSearchInputElements();
1382
1383        inputs.forEach(input => {
1384            if (input && input !== activeInput) {
1385                input.value = value;
1386            }
1387        });
1388
1389        if (activeInput && activeInput !== elements.searchFormInput) {
1390            if (elements.searchFormInput) {
1391                elements.searchFormInput.value = value;
1392            }
1393        }
1394
1395        if (activeInput && activeInput !== elements.input) {
1396            if (elements.input) {
1397                elements.input.value = value;
1398            }
1399        }
1400    }
1401
1402    function handleSearchInputInteraction(inputEl) {
1403        const value = inputEl.value.trim();
1404        syncSearchInputValues(inputEl);
1405
1406        if (value.length < CONFIG.MIN_QUERY_LENGTH) {
1407            return;
1408        }
1409
1410        state.currentQuery = value;
1411        state.currentCategory = null;
1412
1413        if (state.currentMode !== null) {
1414            state.currentApiPage = 1;
1415            state.currentBuffer = [];
1416            state.reachedEnd = false;
1417            syncUrlWithState();
1418            loadNextFullChunk(false);
1419            return;
1420        }
1421
1422        if (elements.overlay) {
1423            elements.overlay.classList.add('has-query');
1424        }
1425
1426        showOverlay();
1427    }
1428
1429    function bindSearchInputEvents(inputEl) {
1430        if (!inputEl || inputEl.dataset.dfBound === 'true') {
1431            return;
1432        }
1433
1434        inputEl.dataset.dfBound = 'true';
1435
1436        inputEl.addEventListener('input', debounce(function() {
1437            handleSearchInputInteraction(inputEl);
1438        }, CONFIG.DEBOUNCE_DELAY));
1439
1440        inputEl.addEventListener('keydown', function(event) {
1441            if (event.key === 'Enter') {
1442                event.preventDefault();
1443                handleSearchInputInteraction(inputEl);
1444            }
1445        });
1446    }
1447
1448    // Initialize application
1449    function init() {
1450        // Inject overlay HTML
1451        injectOverlay();
1452        document.querySelectorAll('.searchInput').forEach(bindSearchInputEvents);
1453
1454        // Cache DOM elements
1455        elements.searchForm = findElement(CONFIG.SEARCH_FORM.selector);
1456        elements.searchFormInput = findElement(CONFIG.SEARCH_FORM.selector + ' input');
1457        elements.searchContentInput = document.getElementById('searchInput');
1458        elements.overlay = document.getElementById('searchOverlay');
1459        elements.input = elements.searchFormInput;
1460        elements.closeBtn = document.getElementById('closeSearch');
1461        elements.openBtn = findElement(CONFIG.OPEN_BUTTON.selector);
1462        elements.fullResultsPage = null;
1463        elements.fullResults = null;
1464        elements.loadMoreWrapper = null;
1465        elements.loadMoreBtn = null;
1466        elements.backToSearchBtn = null;
1467        elements.suggestionsContainer = document.getElementById('suggestions');
1468        elements.productResults = document.getElementById('productResults');
1469        elements.contentResults = document.getElementById('contentResults');
1470        elements.categoryResults = document.getElementById('categoryResults');
1471
1472        // Find results container if configured
1473        state.resultsContainer = findElement(CONFIG.RESULTS_CONTAINER.selector);
1474
1475        // Check if required elements exist
1476        if (!elements.overlay || !elements.input || !elements.searchForm) {
1477            console.error('Doofinder Search: Required elements not found. Make sure the form exists.');
1478            return;
1479        }
1480
1481        // Event listeners - use event delegation for dynamically created buttons
1482        document.addEventListener('click', function(event) {
1483            if (event.target.closest('.btn-show-all-products')) {
1484                showFullList('products');
1485                return;
1486            }
1487            if (event.target.closest('.btn-show-all-content')) {
1488                state.currentCategory = null;
1489                showFullList('content');
1490                return;
1491            }
1492        });
1493
1494        // Event listeners for filter checkboxes
1495        document.addEventListener('change', function(event) {
1496            if (event.target.classList.contains('filter-category-checkbox')) {
1497                handleFilterCategoryChange(event);
1498            }
1499            if (event.target.classList.contains('filter-property-checkbox')) {
1500                handleFilterPropertyChange(event);
1501            }
1502        });
1503
1504        // Toggle filter visibility on toggler click
1505        document.addEventListener('click', function(event) {
1506            if (event.target.closest('.toggler')) {
1507                const filterEl = document.getElementById('filter');
1508                if (filterEl) {
1509                    filterEl.classList.toggle('is-active');
1510                }
1511            }
1512        });
1513
1514        // Event listeners
1515        elements.searchFormInput.addEventListener('input', debounce(function() {
1516            syncSearchInputValues(elements.searchFormInput);
1517
1518            if(elements.searchFormInput.value.length > 2) {
1519                if (state.currentMode !== null) {
1520                    state.currentQuery = elements.searchFormInput.value.trim();
1521                    // updateUrlParams({ query: state.currentQuery });
1522                    syncUrlWithState();
1523                    state.currentApiPage = 1;
1524                    state.currentBuffer = [];
1525                    state.reachedEnd = false;
1526                    loadNextFullChunk(false);
1527                } else {
1528                    showOverlay();
1529                }
1530            }
1531        }, 300));
1532        elements.searchForm.addEventListener('submit', function(e){
1533            e.preventDefault();
1534            if (state.currentMode !== null) {
1535                state.currentQuery = elements.searchFormInput.value.trim();
1536                // updateUrlParams({ query: state.currentQuery });
1537                syncUrlWithState();
1538                state.currentApiPage = 1;
1539                state.currentBuffer = [];
1540                state.reachedEnd = false;
1541                loadNextFullChunk(false);
1542            } else {
1543                showOverlay();
1544            }
1545        });
1546        
1547        elements.closeBtn.addEventListener('click', closeOverlay);
1548
1549        if (elements.searchFormInput && elements.searchFormInput !== elements.input) {
1550            elements.searchFormInput.addEventListener('focus', function() {
1551                syncSearchInputValues(elements.searchFormInput);
1552            });
1553        }
1554
1555        // Keyboard navigation
1556        document.addEventListener('keydown', function(event) {
1557            if (event.key === 'Escape') closeOverlay();
1558        });
1559
1560        // Input handling with debounce
1561        elements.input.addEventListener('input', function() {
1562            clearTimeout(state.timer);
1563            state.currentQuery = elements.input.value.trim();
1564            state.currentCategory = null;
1565
1566            if (state.currentQuery.length < CONFIG.MIN_QUERY_LENGTH) {
1567                elements.overlay.classList.remove('has-query');
1568                clearOverlayResults();
1569                return;
1570            }
1571
1572            elements.overlay.classList.add('has-query');
1573
1574            state.timer = setTimeout(() => {
1575                search(state.currentQuery);
1576            }, CONFIG.DEBOUNCE_DELAY);
1577        });
1578
1579        // Event delegation for dynamic elements
1580        document.addEventListener('click', function(event) {
1581            handleCategoryClick(event);
1582            handleSuggestionClick(event);
1583        });
1584
1585        // Initial state
1586        clearOverlayResults();
1587
1588        // Check for URL parameters and restore state
1589        setTimeout(() => {
1590            restoreStateFromUrl();
1591        }, 100);
1592    }
1593
1594    function restoreStateFromUrl() {
1595        const params = readUrlParams();
1596
1597        if (!params.query) {
1598            return false;
1599        }
1600
1601        // Restore state from URL parameters
1602        state.currentQuery = params.query || '';
1603        state.currentMode = params.mode || null;
1604        state.currentApiPage = parseInt(params.page, 10) || 1;
1605        state.currentFilters = {};
1606
1607        // Restore categories from URL params to selectedCategories
1608        if (params.category) {
1609            if (Array.isArray(params.category)) {
1610                state.selectedCategories = params.category;
1611            } else {
1612                state.selectedCategories = [params.category];
1613            }
1614        }
1615
1616        // Restore any additional filters
1617        const forbiddenKeys = ['query', 'mode', 'page', 'category'];
1618        for (const [key, value] of Object.entries(params)) {
1619            if (!forbiddenKeys.includes(key)) {
1620                state.currentFilters[key] = value;
1621            }
1622        }
1623
1624        // If a mode is set, restore the full results view
1625        if (state.currentQuery && state.currentQuery.length >= CONFIG.MIN_QUERY_LENGTH) {
1626            showFullList(state.currentMode || 'products');
1627            return true;
1628        }
1629
1630        return false;
1631    }
1632
1633    // Start the application when DOM is ready
1634    if (document.readyState === 'loading') {
1635        document.addEventListener('DOMContentLoaded', init);
1636    } else {
1637        init();
1638    }
1639})();

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.