PageSourceSearch

https://www.autexacoustics.com.au/wp-content/themes/autexacoustics…/autex-search-filter.js?ver=1791114017

js autexacoustics.com.au collected 2026-10-04 11:42:29 UTC 9,787 bytes, 231 lines download raw bytes

1document.addEventListener('DOMContentLoaded', () => {
2    const searchFilters = document.querySelectorAll('.autex-search-filter');
3
4    /**
5     * Global registry of taxonomy slugs used as URL filter params.
6     * Populated by the tab/checkbox filter widgets on DOMContentLoaded.
7     * Allows this widget to read only filter params, ignoring unrelated ones like `page`.
8     */
9    window.__autexFilterTaxonomies = window.__autexFilterTaxonomies || new Set();
10
11    /**
12     * Builds filter groups from the URL using only registered taxonomies.
13     * Within a group: OR (e.g. blue OR red). Between groups: AND (e.g. cat AND color).
14     * e.g. ?cat=absorption&color=blue,red → [['cat-absorption'], ['color-blue', 'color-red']]
15     * Other filters (tabs, checkboxes) are responsible for keeping the URL in sync
16     * and dispatching a 'filterchange' CustomEvent on window after each update.
17     */
18    const getFilterGroups = () => {
19        const params = new URLSearchParams(window.location.search);
20        const groups = [];
21        for (const [key, value] of params) {
22            if (!window.__autexFilterTaxonomies.has(key) || !value) continue;
23            const classes = value.split(',').filter(Boolean).map((slug) => `${key}-${slug}`);
24            if (classes.length) groups.push(classes);
25        }
26        return groups;
27    };
28
29    /**
30     * Returns true if the item passes all active URL filter groups.
31     * When no filter groups are active, all items pass.
32     */
33    const itemPassesUrlFilters = (item, filterGroups) => {
34        if (!filterGroups.length) return true;
35        return filterGroups.every((group) => group.some((cls) => item.classList.contains(cls)));
36    };
37
38    const ITEM_FADE_MS = 200;
39
40    const showItem = (item) => {
41        clearTimeout(item._autexHideTimer);
42        item.style.opacity = '0';
43        item.style.display = '';
44        requestAnimationFrame(() => {
45            requestAnimationFrame(() => {
46                item.style.opacity = '';
47            });
48        });
49    };
50
51    const hideItem = (item) => {
52        clearTimeout(item._autexHideTimer);
53        item.style.opacity = '0';
54        item._autexHideTimer = setTimeout(() => {
55            item.style.display = 'none';
56        }, ITEM_FADE_MS);
57    };
58
59    // ── Active filter chips (shared with checkbox filter) ───────────────────
60    window.__autexChipSources = window.__autexChipSources || new Map();
61    window.__autexChipContainers = window.__autexChipContainers || new Map();
62    window.__autexSearchInputs = window.__autexSearchInputs || new Map();
63
64    if (!window.__autexRenderChipsForTarget) {
65        window.__autexRenderChipsForTarget = (targetKey) => {
66            const container = window.__autexChipContainers.get(targetKey);
67            if (!container) {
68                return;
69            }
70            // Deduplicate by taxonomy: only the first source per taxonomy that returns chips is used
71            const seenTaxonomies = new Set();
72            const chips = (window.__autexChipSources.get(targetKey) || []).flatMap((src) => {
73                if (seenTaxonomies.has(src.taxonomy)) return [];
74                const srcChips = src.getChips();
75                if (srcChips.length) seenTaxonomies.add(src.taxonomy);
76                return srcChips;
77            });
78            container.innerHTML = '';
79            chips.forEach(({ label, onClick }) => {
80                const chip = document.createElement('button');
81                chip.type = 'button';
82                chip.className = 'autex-active-filter-chip';
83                chip.innerHTML = `<span class="autex-active-filter-chip__label">${label}</span><span class="autex-active-filter-chip__remove" aria-hidden="true">&times;</span>`;
84                chip.addEventListener('click', onClick);
85                container.appendChild(chip);
86            });
87            container.hidden = chips.length === 0;
88        };
89    }
90
91    searchFilters.forEach((searchFilter) => {
92        const input = searchFilter.querySelector('.autex-search-filter__input');
93        const target = searchFilter.getAttribute('data-target');
94        const targetElement = target ? document.querySelector(target) : null;
95
96        if (!input || !targetElement) {
97            return;
98        }
99
100        const loopItems = Array.from(targetElement.querySelectorAll('[data-elementor-type="loop-item"]'));
101        const items = loopItems.length ? loopItems : Array.from(targetElement.children);
102
103        if (!items.length) {
104            return;
105        }
106
107        const normalizedTextMap = new Map(
108            items.map((item) => [
109                item,
110                (item.textContent || '').toLowerCase().replace(/\s+/g, ' ').trim(),
111            ])
112        );
113
114        // Register search chip source for this target
115        if (!window.__autexChipContainers.has(target)) {
116            const chipContainer = document.createElement('div');
117            chipContainer.className = 'autex-active-filters';
118            chipContainer.hidden = true;
119            targetElement.prepend(chipContainer);
120            window.__autexChipContainers.set(target, chipContainer);
121        }
122        if (!window.__autexChipSources.has(target)) {
123            window.__autexChipSources.set(target, []);
124        }
125        window.__autexChipSources.get(target).push({
126            taxonomy: 'search',
127            getChips: () => {
128                const query = input.value.trim();
129                if (!query || query.length < 3) return [];
130                return [
131                    {
132                        label: query,
133                        onClick: () => {
134                            input.value = '';
135                            applySearch();
136                        },
137                    },
138                ];
139            },
140        });
141
142        const renderChips = () => window.__autexRenderChipsForTarget(target);
143
144        // Register this input for cross-instance sync (desktop + mobile modal pairs)
145        if (!window.__autexSearchInputs.has(target)) {
146            window.__autexSearchInputs.set(target, []);
147        }
148        window.__autexSearchInputs.get(target).push(input);
149
150        const syncPeerInputs = () => {
151            (window.__autexSearchInputs.get(target) || []).forEac
151h((peer) => {
152                if (peer !== input && peer.value !== input.value) {
153                    peer.value = input.value;
154                }
155            });
156        };
157
158        const applySearch = () => {
159            const rawQuery = input.value.toLowerCase().replace(/\s+/g, ' ').trim();
160            const keywords = rawQuery ? rawQuery.split(' ') : [];
161            const filterGroups = getFilterGroups();
162
163            // Only search if we have 3 or more characters, or none
164            if (rawQuery.length > 0 && rawQuery.length < 3) {
165                return;
166            }
167
168            // Unhide all loop grids before re-filtering so previously hidden ones can reappear
169            targetElement.querySelectorAll('.elementor-widget-loop-grid').forEach((grid) => {
170                grid.style.display = '';
171            });
172
173            // Keep all instances for this target in sync
174            syncPeerInputs();
175
176            items.forEach((item) => {
177                // URL-based filters take priority — never un-hide what they've excluded
178                if (!itemPassesUrlFilters(item, filterGroups)) {
179                    hideItem(item);
180                    return;
181                }
182
183                // No search query — item is visible (URL filters already passed above)
184                if (!keywords.length) {
185                    showItem(item);
186                    return;
187                }
188
189                // Apply text search on top
190                const itemText = normalizedTextMap.get(item) || '';
191                const matchesSearch = keywords.every((keyword) => itemText.includes(keyword));
192                if (matchesSearch) {
193                    showItem(item);
194                } else {
195                    hideItem(item);
196                }
197            });
198
199            renderChips();
200
201            setTimeout(() => {
202                const noResults = targetElement.querySelector('.no-results');
203                if (noResults) {
204                    const hasVisible = items.some((item) => item.style.display !== 'none');
205                    noResults.style.display = hasVisible ? 'none' : '';
206                }
207                targetElement.querySelectorAll('.elementor-loop-container').forEach((container) => {
208                    const hasVisible = Array.from(
209                        container.querySelectorAll('[data-elementor-type="loop-item"]')
210                    ).some((item) => item.style.display !== 'none');
211                    if (!hasVisible) {
212                        const grid = container.closest('.elementor-widget-loop-grid');
213                        if (grid) grid.style.display = 'none';
214                    }
215                });
216            }, ITEM_FADE_MS);
217        };
218
219        input.addEventListener('input', applySearch);
220        input.addEventListener('change', applySearch);
221        input.addEventListener('keyup', applySearch);
222
223        // Re-apply when other filters update the URL.
224        // Other filters call: window.dispatchEvent(new CustomEvent('filterchange'))
225        // after updating the URL with history.replaceState.
226        window.addEventListener('filterchange', applySearch);
227
228        // Also handle browser back/forward navigation
229        window.addEventListener('popstate', applySearch);
230    });
231});

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.