MediaWiki:CRT Search.js: Difference between revisions

No edit summary
No edit summary
Tag: Manual revert
 
(16 intermediate revisions by the same user not shown)
Line 1: Line 1:
/**
/**
  * EveryCRT Advanced Search
  * EveryCRT CRT Search — page-specific configuration
  * Server-side faceted search using Semantic MediaWiki API
  * Loads the shared search engine and passes CRT-specific filters, printouts, and card layout.
  */
  */
(function() {
(function() {
     'use strict';
     'use strict';


     var API_ENDPOINT = mw.config.get('wgScriptPath') + '/api.php';
     mw.loader.getScript(
    var RESULTS_PER_PAGE = 48;
        mw.config.get('wgScript') + '?title=MediaWiki:EveryCRT-SearchEngine.js&action=raw&ctype=text/javascript'
    var currentOffset = 0;
     ).then(function() {
     var currentFilters = {};
    var filterOptions = {};
    var totalResults = 0;
    var isLoading = false;
    var searchDebounceTimer = null;
    var DEBOUNCE_DELAY = 400;


    // Filter configuration - defines all available filters
         window.EveryCRTSearch({
    var FILTER_CONFIG = {
         manufacturing: {
            label: 'Manufacturing',
            collapsed: false,
            filters: {
                brand: {
                    label: 'Brand',
                    type: 'select',
                    property: 'Has brand',
                    loadFrom: 'category',
                    category: 'Manufacturers'
                },
                oem: {
                    label: 'OEM Manufacturer',
                    type: 'select',
                    property: 'Has OEM manufacturer',
                    loadFrom: 'category',
                    category: 'Manufacturers'
                },
                series: {
                    label: 'Series',
                    type: 'select',
                    property: 'Has series',
                    loadFrom: 'category',
                    category: 'Series'
                },
                chassis: {
                    label: 'Chassis',
                    type: 'select',
                    property: 'Has chassis',
                    loadFrom: 'category',
                    category: 'Chassis'
                },
                chassis_family: {
                    label: 'Chassis Family',
                    type: 'select',
                    property: 'Has chassis family',
                    loadFrom: 'category',
                    category: 'Chassis families'
                }
            }
        },
        display: {
            label: 'Display',
            collapsed: false,
            filters: {
                crt_type: {
                    label: 'Type',
                    type: 'select',
                    property: 'Has CRT type',
                    loadFrom: 'static',
                    values: ['TV', 'Monitor', 'PVM', 'BVM', 'Computer monitor', 'Presentation', 'Other']
                },
                form_factor: {
                    label: 'Form Factor',
                    type: 'select',
                    property: 'Has form factor',
                    loadFrom: 'static',
                    values: ['Standard', 'Console', 'Portable', 'Wall-mount', 'Other']
                },
                screen_size_min: {
                    label: 'Screen Size (min)',
                    type: 'number',
                    property: 'Has screen size inches',
                    comparison: '>=',
                    placeholder: 'Min inches'
                },
                screen_size_max: {
                    label: 'Screen Size (max)',
                    type: 'number',
                    property: 'Has screen size inches',
                    comparison: '<=',
                    placeholder: 'Max inches'
                },
                aspect_ratio: {
                    label: 'Aspect Ratio',
                    type: 'select',
                    property: 'Has aspect ratio',
                    loadFrom: 'static',
                    values: ['4:3', '16:9']
                },
                tvl_min: {
                    label: 'TVL (min)',
                    type: 'number',
                    property: 'Has TVL',
                    comparison: '>=',
                    placeholder: 'Min lines'
                }
            }
        },
        videoSpecs: {
            label: 'Video Specifications',
            collapsed: true,
            filters: {
                signal_format: {
                    label: 'Signal Format',
                    type: 'multiselect',
                    subobjectQuery: true,
                    subobjectProperty: 'Has video signal format',
                    loadFrom: 'category',
                    category: 'Video signal formats'
                },
                connector: {
                    label: 'Connector',
                    type: 'multiselect',
                    subobjectQuery: true,
                    subobjectProperty: 'Has connector type',
                    loadFrom: 'category',
                    category: 'Connector types'
                }
            }
        },
        mods: {
            label: 'Modifications',
            collapsed: true,
            filters: {
                has_rgb_mod: {
                    label: 'Has RGB Mod',
                    type: 'checkbox',
                    subobjectQuery: true,
                    subobjectProperty: 'Has mod type',
                    subobjectValue: 'RGB',
                    subobjectExtraCondition: '[[Has mod feasible::Yes]]'
                },
                has_svideo_mod: {
                    label: 'Has S-Video Mod',
                    type: 'checkbox',
                    subobjectQuery: true,
                    subobjectProperty: 'Has mod type',
                    subobjectValue: 'S-video',
                    subobjectExtraCondition: '[[Has mod feasible::Yes]]'
                }
            }
        },
        tvSystem: {
            label: 'TV System',
            collapsed: true,
            filters: {
                av_system: {
                    label: 'AV System',
                    type: 'multiselect',
                    property: 'AV system',
                    loadFrom: 'category',
                    category: 'AV systems'
                },
                frequency: {
                    label: 'AC Frequency',
                    type: 'multiselect',
                    property: 'Frequency',
                    loadFrom: 'static',
                    values: ['50Hz', '60Hz']
                }
            }
        },
        builtIn: {
            label: 'Built-in Devices',
            collapsed: true,
            filters: {
                built_in_vcr: {
                    label: 'VCR',
                    type: 'checkbox',
                    property: 'Has built-in devices',
                    containsValue: 'VCR'
                },
                built_in_dvd: {
                    label: 'DVD',
                    type: 'checkbox',
                    property: 'Has built-in devices',
                    containsValue: 'DVD'
                }
            }
        }
    };


    /**
            containerId: 'advanced-search-app',
    * Trigger search with debounce for text/number inputs
             category: 'CRT models',
    */
             resultLabel: 'CRTs',
    function triggerSearch(immediate) {
             noImageEmoji: '\uD83D\uDCFA',
        if (searchDebounceTimer) {
             subobjectParentProperty: 'Has CRT model',
             clearTimeout(searchDebounceTimer);
             defaultSort: 'brand',
        }
       
        if (immediate) {
             currentOffset = 0;
             performSearch();
             updateURL();
        } else {
            searchDebounceTimer = setTimeout(function() {
                currentOffset = 0;
                performSearch();
                updateURL();
             }, DEBOUNCE_DELAY);
        }
    }


    /**
            // ── Sort options ───────────────────────────────────────
    * Initialize the advanced search interface
            sortOptions: [
    */
                { value: 'brand',      label: 'Brand',      property: 'Has brand',              printoutKey: 'brand' },
    function init() {
                { value: 'model',      label: 'Model',      property: 'Has model code',         printoutKey: 'model' },
         var container = document.getElementById('advanced-search-app');
                { value: 'screen_size', label: 'Screen Size', property: 'Has screen size inches',  printoutKey: 'size' },
        if (!container) {
                { value: 'type',        label: 'Type',        property: 'Has CRT type',          printoutKey: 'type' }
            console.error('Advanced search container not found');
             ],
             return;
        }


        container.innerHTML = buildHTML();
            // ── Extra printouts (image, brand, model are always included) ──
        attachEventListeners();
            printouts: [
        loadFilterOptions();
                { property: 'Has CRT type',            key: 'type' },
       
                { property: 'Has screen size inches',  key: 'size' },
        // Parse URL parameters for initial filters
                { property: 'Has aspect ratio',        key: 'aspect' },
        parseURLParams();
                { property: 'Has TVL',                key: 'tvl' }
    }
            ],


    /**
            // ── Card meta fields ───────────────────────────────────
    * Build the main HTML structure
            cardMeta: [
    */
                { key: 'type',  cssClass: 'crt-meta-type' },
    function buildHTML() {
                { key: 'size',  cssClass: 'crt-meta-size',   suffix: '"' },
        var html = '<div class="crt-search-wrapper">';
                { key: 'aspect', cssClass: 'crt-meta-aspect' },
       
                { key: 'tvl',    cssClass: 'crt-meta-tvl',    suffix: ' TVL' }
        // Mobile filter toggle
            ],
        html += '<button type="button" class="crt-mobile-filter-toggle" id="mobile-filter-toggle">';
        html += '<span class="crt-filter-icon">☰</span> Filters';
        html += '</button>';
       
        // Filters sidebar
        html += '<div class="crt-search-filters" id="filters-sidebar">';
        html += '<div class="crt-filters-header">';
        html += '<h2>Filters</h2>';
        html += '<button type="button" class="crt-reset-btn" id="reset-filters">Reset All</button>';
        html += '</div>';
       
        // Scrollable body for filter sections
        html += '<div class="crt-filters-body">';
       
        // Build filter sections
        for (var sectionKey in FILTER_CONFIG) {
            var section = FILTER_CONFIG[sectionKey];
            html += buildFilterSection(sectionKey, section);
        }
       
        html += '</div>'; // end crt-filters-body
       
        // Apply button (shown only on mobile)
        html += '<div class="crt-apply-filters">';
        html += '<button type="button" class="crt-apply-btn" id="apply-filters">Apply Filters</button>';
        html += '</div>';
        html += '</div>';
       
        // Results area
        html += '<div class="crt-search-results">';
        html += '<div class="crt-results-header">';
        html += '<div class="crt-results-count" id="results-count">Loading...</div>';
        html += '<div class="crt-results-controls">';
        html += '<div class="crt-results-sort">';
        html += '<label for="sort-by">Sort by:</label>';
        html += '<select id="sort-by">';
        html += '<option value="brand">Brand</option>';
        html += '<option value="model">Model</option>';
        html += '<option value="screen_size">Screen Size</option>';
        html += '<option value="type">Type</option>';
        html += '</select>';
        html += '</div>';
        html += '<div class="crt-view-toggle">';
        html += '<button class="crt-view-btn active" data-view="grid" title="Grid view">▦</button>';
        html += '<button class="crt-view-btn" data-view="list" title="List view">≡</button>';
        html += '</div>';
        html += '</div>';
        html += '</div>';
        html += '<div class="crt-active-filters" id="active-filters"></div>';
        html += '<div class="crt-results-grid" id="results-grid"></div>';
        html += '<div class="crt-pagination" id="pagination"></div>';
        html += '</div>';
       
        html += '</div>';
        return html;
    }


    /**
            // ── Filter configuration ───────────────────────────────
    * Build a collapsible filter section
            filterConfig: {
    */
                manufacturing: {
    function buildFilterSection(sectionKey, section) {
                    label: 'Manufacturing',
        var collapsed = section.collapsed ? ' collapsed' : '';
                    collapsed: false,
        var html = '<div class="crt-filter-section' + collapsed + '" data-section="' + sectionKey + '">';
                    filters: {
        html += '<div class="crt-section-header">';
                        brand: {
        html += '<span class="crt-section-title">' + section.label + '</span>';
                            label: 'Brand',
        html += '<span class="crt-section-toggle">▼</span>';
                            type: 'select',
        html += '</div>';
                            property: 'Has brand',
        html += '<div class="crt-section-content">';
                            loadFrom: 'category',
       
                            category: 'Manufacturers'
        for (var filterKey in section.filters) {
                        },
            var filter = section.filters[filterKey];
                        oem: {
            html += buildFilterInput(filterKey, filter);
                            label: 'OEM Manufacturer',
        }
                            type: 'select',
       
                            property: 'Has OEM manufacturer',
        html += '</div>';
                            loadFrom: 'category',
        html += '</div>';
                            category: 'Manufacturers'
        return html;
                        },
    }
                        series: {
 
                            label: 'Series',
    /**
                            type: 'select',
    * Build individual filter input
                            property: 'Has series',
    */
                            loadFrom: 'category',
    function buildFilterInput(filterKey, filter) {
                            category: 'Series'
        var html = '<div class="crt-filter-item" data-filter="' + filterKey + '">';
                        },
       
                        chassis: {
        switch (filter.type) {
                            label: 'Chassis',
            case 'select':
                            type: 'select',
                html += '<label for="filter-' + filterKey + '">' + filter.label + '</label>';
                            property: 'Has chassis',
                html += '<select id="filter-' + filterKey + '" class="crt-filter-select" data-filter="' + filterKey + '">';
                            loadFrom: 'category',
                html += '<option value="">All</option>';
                            category: 'Chassis'
                if (filter.loadFrom === 'static' && filter.values) {
                        },
                    for (var i = 0; i < filter.values.length; i++) {
                        chassis_family: {
                         html += '<option value="' + filter.values[i] + '">' + filter.values[i] + '</option>';
                            label: 'Chassis Family',
                            type: 'select',
                            property: 'Has chassis family',
                            loadFrom: 'category',
                            category: 'Chassis families'
                         }
                     }
                     }
                 }
                 },
                 html += '</select>';
                 display: {
                break;
                    label: 'Display',
               
                    collapsed: false,
            case 'multiselect':
                    filters: {
                html += '<label>' + filter.label + '</label>';
                        crt_type: {
                html += '<div class="crt-multiselect" id="filter-' + filterKey + '" data-filter="' + filterKey + '">';
                            label: 'Type',
                html += '<div class="crt-multiselect-selected"></div>';
                            type: 'select',
                html += '<input type="text" class="crt-multiselect-input" placeholder="Search ' + filter.label.toLowerCase() + '...">';
                            property: 'Has CRT type',
                html += '<div class="crt-multiselect-dropdown"></div>';
                            loadFrom: 'category',
                html += '</div>';
                            category: 'CRT types',
                break;
                            categoryMemberTypes: 'subcat'
               
                        },
            case 'number':
                        form_factor: {
                html += '<label for="filter-' + filterKey + '">' + filter.label + '</label>';
                            label: 'Form Factor',
                html += '<input type="number" id="filter-' + filterKey + '" class="crt-filter-number" ';
                            type: 'select',
                html += 'data-filter="' + filterKey + '" placeholder="' + (filter.placeholder || '') + '">';
                            property: 'Has form factor',
                break;
                            loadFrom: 'static',
               
                            values: ['Standard', 'Console', 'Portable', 'Wall-mount', 'Other']
            case 'checkbox':
                        },
                html += '<label class="crt-checkbox-label">';
                        screen_size_min: {
                html += '<input type="checkbox" id="filter-' + filterKey + '" class="crt-filter-checkbox" ';
                            label: 'Screen Size (min)',
                html += 'data-filter="' + filterKey + '">';
                            type: 'number',
                html += '<span>' + filter.label + '</span>';
                            property: 'Has screen size inches',
                html += '</label>';
                            comparison: '>=',
                break;
                            placeholder: 'Min inches'
               
                        },
            case 'range':
                        screen_size_max: {
                html += '<label>' + filter.label + '</label>';
                            label: 'Screen Size (max)',
                html += '<div class="crt-range-inputs">';
                            type: 'number',
                html += '<input type="number" id="filter-' + filterKey + '-min" class="crt-filter-range-min" ';
                            property: 'Has screen size inches',
                html += 'data-filter="' + filterKey + '-min" placeholder="Min">';
                            comparison: '<=',
                html += '<span class="crt-range-sep">–</span>';
                            placeholder: 'Max inches'
                html += '<input type="number" id="filter-' + filterKey + '-max" class="crt-filter-range-max" ';
                        },
                html += 'data-filter="' + filterKey + '-max" placeholder="Max">';
                        aspect_ratio: {
                html += '</div>';
                            label: 'Aspect Ratio',
                break;
                            type: 'select',
        }
                            property: 'Has aspect ratio',
       
                            loadFrom: 'static',
        html += '</div>';
                            values: ['4:3', '16:9']
        return html;
                         },
    }
                        tvl_min: {
 
                            label: 'TVL (min)',
    /**
                            type: 'number',
    * Attach event listeners
                            property: 'Has TVL',
    */
                            comparison: '>=',
    function attachEventListeners() {
                             placeholder: 'Min lines'
        // Mobile filter toggle
                         }
        document.getElementById('mobile-filter-toggle').addEventListener('click', function() {
            document.getElementById('filters-sidebar').classList.toggle('visible');
            this.classList.toggle('active');
        });
       
        // Section collapse/expand
        document.querySelectorAll('.crt-section-header').forEach(function(header) {
            header.addEventListener('click', function() {
                var section = this.closest('.crt-filter-section');
                section.classList.toggle('collapsed');
            });
        });
       
        // Apply filters button (mobile fallback)
        document.getElementById('apply-filters').addEventListener('click', function() {
            currentOffset = 0;
            performSearch();
            updateURL();
           
            // Close mobile filters
            document.getElementById('filters-sidebar').classList.remove('visible');
            document.getElementById('mobile-filter-toggle').classList.remove('active');
        });
       
        // Reset filters button
        document.getElementById('reset-filters').addEventListener('click', resetFilters);
       
        // Sort change - immediate
        document.getElementById('sort-by').addEventListener('change', function() {
            triggerSearch(true);
        });
       
        // View toggle
        document.querySelectorAll('.crt-view-btn').forEach(function(btn) {
            btn.addEventListener('click', function() {
                document.querySelectorAll('.crt-view-btn').forEach(function(b) {
                    b.classList.remove('active');
                });
                this.classList.add('active');
                var view = this.dataset.view;
                var grid = document.getElementById('results-grid');
                grid.classList.remove('grid-view', 'list-view');
                grid.classList.add(view + '-view');
            });
        });
       
        // Select change - immediate search
        document.querySelectorAll('.crt-filter-select').forEach(function(select) {
            select.addEventListener('change', function() {
                triggerSearch(true);
            });
        });
       
        // Number input change - debounced search
        document.querySelectorAll('.crt-filter-number').forEach(function(input) {
            input.addEventListener('input', function() {
                triggerSearch(false);
            });
            input.addEventListener('keypress', function(e) {
                if (e.key === 'Enter') {
                    triggerSearch(true);
                }
            });
        });
       
        // Checkbox change - immediate search
        document.querySelectorAll('.crt-filter-checkbox').forEach(function(checkbox) {
            checkbox.addEventListener('change', function() {
                triggerSearch(true);
            });
        });
       
        // Multiselect handling
        initMultiselects();
    }
 
    /**
    * Initialize multiselect dropdowns
    */
    function initMultiselects() {
        document.querySelectorAll('.crt-multiselect').forEach(function(ms) {
            var input = ms.querySelector('.crt-multiselect-input');
            var dropdown = ms.querySelector('.crt-multiselect-dropdown');
            var filterKey = ms.dataset.filter;
           
            // Show dropdown on input focus
            input.addEventListener('focus', function() {
                dropdown.classList.add('visible');
                updateMultiselectDropdown(ms, filterKey);
            });
           
            // Filter dropdown on input
            input.addEventListener('input', function() {
                updateMultiselectDropdown(ms, filterKey, this.value);
            });
           
            // Hide dropdown on click outside
            document.addEventListener('click', function(e) {
                if (!ms.contains(e.target)) {
                    dropdown.classList.remove('visible');
                }
            });
        });
    }
 
    /**
    * Update multiselect dropdown with filtered options
    */
    function updateMultiselectDropdown(ms, filterKey, searchText) {
        var dropdown = ms.querySelector('.crt-multiselect-dropdown');
        var options = filterOptions[filterKey] || [];
        var selected = getMultiselectValues(ms);
       
        searchText = (searchText || '').toLowerCase();
       
        var html = '';
        var count = 0;
        options.forEach(function(opt) {
            var optValue = typeof opt === 'object' ? opt.value : opt;
            var optLabel = typeof opt === 'object' ? opt.label : opt;
           
            if (searchText && optLabel.toLowerCase().indexOf(searchText) === -1) {
                return;
            }
           
            var isSelected = selected.indexOf(optValue) !== -1;
            html += '<div class="crt-multiselect-option' + (isSelected ? ' selected' : '') + '" data-value="' + escapeHtml(optValue) + '">';
            html += '<span class="crt-option-check">' + (isSelected ? '✓' : '') + '</span>';
            html += '<span class="crt-option-label">' + escapeHtml(optLabel) + '</span>';
            html += '</div>';
            count++;
        });
       
        if (count === 0) {
            html = '<div class="crt-multiselect-empty">No options found</div>';
        }
       
        dropdown.innerHTML = html;
       
        // Attach click handlers
        dropdown.querySelectorAll('.crt-multiselect-option').forEach(function(opt) {
            opt.addEventListener('click', function(e) {
                e.stopPropagation();
                toggleMultiselectOption(ms, this.dataset.value);
            });
        });
    }
 
    /**
    * Toggle a multiselect option
    */
    function toggleMultiselectOption(ms, value) {
        var selectedContainer = ms.querySelector('.crt-multiselect-selected');
        var existing = selectedContainer.querySelector('[data-value="' + CSS.escape(value) + '"]');
       
        if (existing) {
            existing.remove();
        } else {
            var tag = document.createElement('span');
            tag.className = 'crt-multiselect-tag';
            tag.dataset.value = value;
            tag.innerHTML = escapeHtml(value) + ' <span class="crt-tag-remove">×</span>';
            tag.querySelector('.crt-tag-remove').addEventListener('click', function(e) {
                e.stopPropagation();
                tag.remove();
                updateMultiselectDropdown(ms, ms.dataset.filter, ms.querySelector('.crt-multiselect-input').value);
                triggerSearch(true);
            });
            selectedContainer.appendChild(tag);
        }
       
        updateMultiselectDropdown(ms, ms.dataset.filter, ms.querySelector('.crt-multiselect-input').value);
        triggerSearch(true);
    }
 
    /**
    * Get selected values from a multiselect
    */
    function getMultiselectValues(ms) {
        var values = [];
        ms.querySelectorAll('.crt-multiselect-tag').forEach(function(tag) {
            values.push(tag.dataset.value);
        });
        return values;
    }
 
    /**
    * Load filter options from SMW
    */
    function loadFilterOptions() {
        var promises = [];
       
        for (var sectionKey in FILTER_CONFIG) {
            var section = FILTER_CONFIG[sectionKey];
            for (var filterKey in section.filters) {
                var filter = section.filters[filterKey];
               
                if (filter.loadFrom === 'category') {
                    promises.push(loadCategoryOptions(filterKey, filter.category));
                } else if (filter.loadFrom === 'static' && filter.values) {
                    filterOptions[filterKey] = filter.values.map(function(v) {
                         return { value: v, label: v };
                    });
                }
            }
        }
       
        Promise.all(promises).then(function() {
            populateSelectFilters();
            performSearch();
        });
    }
 
    /**
    * Load options from a category
    */
    function loadCategoryOptions(filterKey, category) {
        return new Promise(function(resolve) {
            var api = new mw.Api();
            api.get({
                action: 'query',
                list: 'categorymembers',
                cmtitle: 'Category:' + category,
                cmlimit: 500,
                format: 'json'
            }).done(function(data) {
                filterOptions[filterKey] = [];
                if (data.query && data.query.categorymembers) {
                    data.query.categorymembers.forEach(function(member) {
                        filterOptions[filterKey].push({
                            value: member.title,
                             label: member.title
                        });
                    });
                    filterOptions[filterKey].sort(function(a, b) {
                        return a.label.localeCompare(b.label);
                    });
                }
                resolve();
            }).fail(function() {
                filterOptions[filterKey] = [];
                resolve();
            });
        });
    }
 
    /**
    * Populate select filters with loaded options
    */
    function populateSelectFilters() {
        for (var sectionKey in FILTER_CONFIG) {
            var section = FILTER_CONFIG[sectionKey];
            for (var filterKey in section.filters) {
                var filter = section.filters[filterKey];
               
                if (filter.type === 'select' && filter.loadFrom === 'category') {
                    var select = document.getElementById('filter-' + filterKey);
                    if (select && filterOptions[filterKey]) {
                        var currentValue = select.value;
                        select.innerHTML = '<option value="">All</option>';
                        filterOptions[filterKey].forEach(function(opt) {
                            var option = document.createElement('option');
                            option.value = opt.value;
                            option.textContent = opt.label;
                            select.appendChild(option);
                         });
                        select.value = currentValue;
                     }
                     }
                 }
                 },
            }
                 videoSpecs: {
        }
                     label: 'Video Specifications',
    }
                     collapsed: true,
 
                     filters: {
    /**
                        signal_format: {
    * Collect current filter values
                            label: 'Signal Format',
    */
                            type: 'multiselect',
    function collectFilters() {
                            subobjectQuery: true,
        var filters = {};
                            subobjectProperty: 'Has video signal format',
       
                            loadFrom: 'category',
        // Collect select and number filters
                            category: 'Video signal formats'
        document.querySelectorAll('.crt-filter-select, .crt-filter-number').forEach(function(input) {
                        },
            var value = input.value.trim();
                        connector: {
            if (value) {
                            label: 'Connector',
                 filters[input.dataset.filter] = value;
                            type: 'multiselect',
            }
                            subobjectQuery: true,
        });
                             subobjectProperty: 'Has connector type',
       
                             loadFrom: 'category',
        // Collect checkbox filters
                             category: 'Connector types'
        document.querySelectorAll('.crt-filter-checkbox').forEach(function(input) {
                         }
            if (input.checked) {
                filters[input.dataset.filter] = true;
            }
        });
       
        // Collect multiselect filters
        document.querySelectorAll('.crt-multiselect').forEach(function(ms) {
            var values = getMultiselectValues(ms);
            if (values.length > 0) {
                filters[ms.dataset.filter] = values;
            }
        });
       
        return filters;
    }
 
    /**
    * Reset all filters
    */
    function resetFilters() {
        document.querySelectorAll('.crt-filter-select, .crt-filter-number').forEach(function(input) {
            input.value = '';
        });
       
        document.querySelectorAll('.crt-filter-checkbox').forEach(function(input) {
            input.checked = false;
        });
       
        document.querySelectorAll('.crt-multiselect-selected').forEach(function(container) {
            container.innerHTML = '';
        });
       
        currentOffset = 0;
       
        // Clear URL params
        history.replaceState(null, '', window.location.pathname);
       
        performSearch();
    }
 
    /**
    * Update active filters display
    */
    function updateActiveFilters() {
        var filters = collectFilters();
        var container = document.getElementById('active-filters');
        var html = '';
       
        var hasFilters = false;
        for (var key in filters) {
            hasFilters = true;
            var value = filters[key];
            var label = getFilterLabel(key);
           
            if (Array.isArray(value)) {
                value.forEach(function(v) {
                     html += '<span class="crt-active-filter" data-filter="' + key + '" data-value="' + escapeHtml(v) + '">';
                    html += '<span class="crt-af-label">' + label + ':</span> ';
                    html += '<span class="crt-af-value">' + escapeHtml(v) + '</span>';
                     html += ' <span class="crt-af-remove">×</span>';
                     html += '</span>';
                });
            } else if (value === true) {
                html += '<span class="crt-active-filter" data-filter="' + key + '">';
                html += '<span class="crt-af-value">' + label + '</span>';
                html += ' <span class="crt-af-remove">×</span>';
                html += '</span>';
            } else {
                html += '<span class="crt-active-filter" data-filter="' + key + '">';
                html += '<span class="crt-af-label">' + label + ':</span> ';
                html += '<span class="crt-af-value">' + escapeHtml(value) + '</span>';
                html += ' <span class="crt-af-remove">×</span>';
                html += '</span>';
            }
        }
       
        if (hasFilters) {
            html += '<button class="crt-clear-all-filters">Clear All</button>';
        }
       
        container.innerHTML = html;
       
        // Attach remove handlers
        container.querySelectorAll('.crt-active-filter').forEach(function(af) {
            af.querySelector('.crt-af-remove').addEventListener('click', function() {
                removeActiveFilter(af.dataset.filter, af.dataset.value);
            });
        });
       
        var clearAll = container.querySelector('.crt-clear-all-filters');
        if (clearAll) {
            clearAll.addEventListener('click', resetFilters);
        }
    }
 
    /**
    * Remove an active filter
    */
    function removeActiveFilter(filterKey, value) {
        // Find the filter config
        var filterConfig = null;
        for (var sectionKey in FILTER_CONFIG) {
            if (FILTER_CONFIG[sectionKey].filters[filterKey]) {
                filterConfig = FILTER_CONFIG[sectionKey].filters[filterKey];
                break;
            }
        }
       
        if (!filterConfig) return;
       
        switch (filterConfig.type) {
            case 'select':
            case 'number':
                document.getElementById('filter-' + filterKey).value = '';
                break;
            case 'checkbox':
                document.getElementById('filter-' + filterKey).checked = false;
                break;
            case 'multiselect':
                var ms = document.getElementById('filter-' + filterKey);
                var tag = ms.querySelector('.crt-multiselect-tag[data-value="' + CSS.escape(value) + '"]');
                if (tag) tag.remove();
                break;
        }
       
        triggerSearch(true);
    }
 
    /**
    * Get human-readable label for a filter key
    */
    function getFilterLabel(filterKey) {
        for (var sectionKey in FILTER_CONFIG) {
            var section = FILTER_CONFIG[sectionKey];
            if (section.filters[filterKey]) {
                return section.filters[filterKey].label;
            }
        }
        return filterKey;
    }
 
    /**
    * Build SMW query from filters
    */
    function buildSMWQuery() {
        var filters = collectFilters();
        var conditions = ['[[Category:CRT models]]'];
        var subobjectConditions = [];
       
        for (var sectionKey in FILTER_CONFIG) {
            var section = FILTER_CONFIG[sectionKey];
            for (var filterKey in section.filters) {
                var filter = section.filters[filterKey];
                var value = filters[filterKey];
               
                if (value === undefined || value === '') continue;
               
                if (filter.subobjectQuery) {
                    // Handle subobject queries differently
                    if (filter.type === 'checkbox' && value === true) {
                        // Use subquery for mod checks
                        subobjectConditions.push({
                             property: filter.subobjectProperty,
                             value: filter.subobjectValue,
                             extraCondition: filter.subobjectExtraCondition || ''
                         });
                     }
                     }
                     else if (filter.type === 'multiselect' && Array.isArray(value) && value.length > 0) {
                },
                         value.forEach(function(v) {
                mods: {
                             subobjectConditions.push({
                     label: 'Modifications',
                                property: filter.subobjectProperty,
                    collapsed: true,
                                value: v,
                    filters: {
                                extraCondition: filter.subobjectExtraCondition || ''
                         has_rgb_mod: {
                             });
                             label: 'Has RGB Mod',
                         });
                            type: 'checkbox',
                            subobjectQuery: true,
                            subobjectProperty: 'Has mod type',
                            subobjectValue: 'RGB',
                            subobjectExtraCondition: '[[Has mod feasible::Yes]]'
                        },
                        has_svideo_mod: {
                            label: 'Has S-Video Mod',
                            type: 'checkbox',
                            subobjectQuery: true,
                            subobjectProperty: 'Has mod type',
                            subobjectValue: 'S-video',
                             subobjectExtraCondition: '[[Has mod feasible::Yes]]'
                         }
                     }
                     }
                 }
                 },
                 else if (filter.type === 'select' && filter.property) {
                 tvSystem: {
                     conditions.push('[[' + filter.property + '::' + value + ']]');
                     label: 'TV System',
                }
                    collapsed: true,
                else if (filter.type === 'number' && filter.property) {
                    filters: {
                    var comp = filter.comparison || '=';
                        av_system: {
                    if (comp === '>=') {
                            label: 'AV System',
                         conditions.push('[[' + filter.property + '::' + value + ']]');
                            type: 'multiselect',
                    } else if (comp === '<=') {
                            property: 'AV system',
                        conditions.push('[[' + filter.property + '::≤' + value + ']]');
                            loadFrom: 'category',
                    } else {
                            category: 'AV systems'
                        conditions.push('[[' + filter.property + '::' + value + ']]');
                         },
                        frequency: {
                            label: 'AC Frequency',
                            type: 'multiselect',
                            property: 'Frequency',
                            loadFrom: 'static',
                            values: ['50Hz', '60Hz']
                        }
                     }
                     }
                 }
                 },
                 else if (filter.type === 'checkbox' && filter.property && value === true) {
                 builtIn: {
                     if (filter.containsValue) {
                    label: 'Built-in Devices',
                         conditions.push('[[' + filter.property + '::~*' + filter.containsValue + '*]]');
                    collapsed: true,
                     filters: {
                         built_in_vcr: {
                            label: 'VCR',
                            type: 'checkbox',
                            property: 'Has built-in devices',
                            containsValue: 'VCR'
                        },
                        built_in_dvd: {
                            label: 'DVD',
                            type: 'checkbox',
                            property: 'Has built-in devices',
                            containsValue: 'DVD'
                        }
                     }
                     }
                }
                else if (filter.type === 'multiselect' && filter.property && Array.isArray(value) && value.length > 0) {
                    // OR condition for multi-select
                    var orParts = value.map(function(v) {
                        return filter.property + '::' + v;
                    });
                    conditions.push('[[' + orParts.join('||') + ']]');
                 }
                 }
             }
             }
        }
       
        return {
            mainQuery: conditions.join(''),
            subobjectConditions: subobjectConditions
        };
    }


    /**
    * Perform the search using SMW API
    */
    function performSearch() {
        if (isLoading) return;
       
        isLoading = true;
        showLoading();
        updateActiveFilters();
       
        var queryData = buildSMWQuery();
        var query = queryData.mainQuery;
        var sortBy = document.getElementById('sort-by').value;
       
        var sortProperty = 'Has brand';
        switch (sortBy) {
            case 'model': sortProperty = 'Has model code'; break;
            case 'screen_size': sortProperty = 'Has screen size inches'; break;
            case 'type': sortProperty = 'Has CRT type'; break;
        }
       
        // If we have subobject conditions, we need to use a different query approach
        if (queryData.subobjectConditions.length > 0) {
            performSubobjectSearch(queryData, sortProperty);
            return;
        }
       
        var api = new mw.Api();
       
        // First get count
        api.get({
            action: 'ask',
            query: query + '|format=count',
            format: 'json'
        }).done(function(countData) {
            totalResults = parseCountResult(countData);
           
            // Now get actual results
            api.get({
                action: 'ask',
                query: query +
                    '|?Has image#100px=image' +
                    '|?Has brand=brand' +
                    '|?Has model code=model' +
                    '|?Has CRT type=type' +
                    '|?Has screen size inches=size' +
                    '|?Has aspect ratio=aspect' +
                    '|?Has TVL=tvl' +
                    '|sort=' + sortProperty +
                    '|order=asc' +
                    '|limit=' + RESULTS_PER_PAGE +
                    '|offset=' + currentOffset +
                    '|format=json',
                format: 'json'
            }).done(function(data) {
                isLoading = false;
                displayResults(data);
            }).fail(function(error) {
                isLoading = false;
                showError('Search failed. Please try again.');
                console.error('SMW query error:', error);
            });
           
        }).fail(function(error) {
            isLoading = false;
            showError('Search failed. Please try again.');
            console.error('SMW count error:', error);
         });
         });
    }


     /**
     }, function(error) {
    * Perform search with subobject conditions (inputs/mods)
         console.error('Failed to load EveryCRT search engine:', error);
    * This requires a two-step query due to SMW limitations
         var el = document.getElementById('advanced-search-app');
    *
         if (el) {
    * Strategy: Get all matching model names first, then paginate client-side
             el.innerHTML = '<div style="text-align:center;padding:64px;background:#2a2a2e;border-radius:12px;">' +
    * to avoid hitting SMW's query size/depth limits with huge OR queries
                 '<p style="font-size:18px;color:#ddd;">Failed to load search engine.</p>' +
    */
                '<p style="color:#888;font-size:13px;margin-top:12px;">Please try refreshing the page.</p></div>';
    /**
    * Perform search with subobject conditions (inputs/mods)
    * Uses batched queries to avoid SMW query size limits
    */
    function performSubobjectSearch(queryData, sortProperty) {
         var api = new mw.Api();
        var subConditions = queryData.subobjectConditions;
       
        // Build queries to find pages through their subobjects
        var subQueries = [];
        subConditions.forEach(function(cond) {
            // Build query with main condition and any extra conditions (like feasibility)
            var subQuery = '[[' + cond.property + '::' + cond.value + ']]';
            if (cond.extraCondition) {
                subQuery += cond.extraCondition;
            }
            subQuery += '|?Has CRT model|limit=5000|format=json';
            subQueries.push(subQuery);
        });
       
        // Execute subobject queries to find matching CRT models
         var promises = subQueries.map(function(sq) {
            return api.get({
                action: 'ask',
                query: sq,
                format: 'json'
            });
         });
       
        Promise.all(promises).then(function(results) {
             // Collect all CRT models that match
            var matchingModels = new Set();
            var isFirst = true;
           
            results.forEach(function(data) {
                // Check for SMW errors
                if (data.error) {
                    return;
                }
               
                var currentMatches = new Set();
                if (data.query && data.query.results) {
                    Object.values(data.query.results).forEach(function(result) {
                        if (result.printouts && result.printouts['Has CRT model']) {
                            result.printouts['Has CRT model'].forEach(function(model) {
                                var modelName = typeof model === 'object' ? model.fulltext : model;
                                currentMatches.add(modelName);
                            });
                        }
                    });
                 }
               
                if (isFirst) {
                    matchingModels = currentMatches;
                    isFirst = false;
                } else {
                    // Intersection - only keep models that match ALL conditions
                    matchingModels = new Set([...matchingModels].filter(function(x) {
                        return currentMatches.has(x);
                    }));
                }
            });
           
            if (matchingModels.size === 0) {
                isLoading = false;
                totalResults = 0;
                displayResults({ query: { results: {} } });
                return;
            }
           
            var matchingModelsArray = Array.from(matchingModels).sort();
            totalResults = matchingModelsArray.length;
           
            // Check if we have additional main conditions beyond just Category:CRT models
            var mainConditions = queryData.mainQuery.replace('[[Category:CRT models]]', '').trim();
           
            // Fetch details for the matching models using batched queries
            fetchModelDetailsBatched(matchingModelsArray, mainConditions, sortProperty);
           
        }).catch(function(error) {
            isLoading = false;
            showError('Search failed. Please try again.');
            console.error('Subobject query error:', error);
        });
    }
   
    /**
    * Fetch model details using batched queries to avoid SMW size limits
    * @param {Array} allModels - All matching model names
    * @param {string} mainConditions - Additional SMW conditions (empty string if none)
    * @param {string} sortProperty - Property to sort by
    */
    function fetchModelDetailsBatched(allModels, mainConditions, sortProperty) {
        var api = new mw.Api();
       
        // Get just the models for the current page
        var pageModels = allModels.slice(currentOffset, currentOffset + RESULTS_PER_PAGE);
       
        if (pageModels.length === 0) {
            isLoading = false;
            displayResults({ query: { results: {} } });
            return;
        }
       
        // Split into small batches to avoid SMW query size limits
        // Use 15 models per batch to stay well under limits
        var BATCH_SIZE = 15;
        var batches = [];
        for (var i = 0; i < pageModels.length; i += BATCH_SIZE) {
            batches.push(pageModels.slice(i, i + BATCH_SIZE));
         }
         }
       
     });
        // Build and execute batched queries
        var batchPromises = batches.map(function(batch) {
            // Use disjunction with || instead of separate [[]] OR [[]] to reduce query depth
            var disjunction = batch.join('||');
            var query = '[[' + disjunction + ']]';
           
            // Add main conditions if present
            if (mainConditions) {
                query = '[[Category:CRT models]]' + mainConditions + query;
            }
           
            return api.get({
                action: 'ask',
                query: query +
                    '|?Has image#100px=image' +
                    '|?Has brand=brand' +
                    '|?Has model code=model' +
                    '|?Has CRT type=type' +
                    '|?Has screen size inches=size' +
                    '|?Has aspect ratio=aspect' +
                    '|?Has TVL=tvl' +
                    '|limit=50' +
                    '|format=json',
                format: 'json'
            });
        });
       
        Promise.all(batchPromises).then(function(batchResults) {
            // Merge results from all batches
            var mergedResults = {};
            var hasErrors = false;
           
            batchResults.forEach(function(data) {
                // Check for SMW errors in the response
                if (data.error) {
                    hasErrors = true;
                    return;
                }
               
                if (data.query && data.query.results) {
                    Object.assign(mergedResults, data.query.results);
                }
            });
           
            if (hasErrors && Object.keys(mergedResults).length === 0) {
                isLoading = false;
                showError('Search query too complex. Try fewer filters.');
                return;
            }
           
            // Sort merged results by the specified property
            var sortedKeys = Object.keys(mergedResults).sort(function(a, b) {
                var aVal = getSortValue(mergedResults[a], sortProperty);
                var bVal = getSortValue(mergedResults[b], sortProperty);
                return aVal.localeCompare(bVal);
            });
           
            var sortedResults = {};
            sortedKeys.forEach(function(key) {
                sortedResults[key] = mergedResults[key];
            });
           
            isLoading = false;
            displayResults({ query: { results: sortedResults } });
        }).catch(function(error) {
            isLoading = false;
            showError('Search failed. Please try again.');
            console.error('Batch query error:', error);
        });
    }
   
    /**
    * Get sort value from a result for client-side sorting
    */
    function getSortValue(result, sortProperty) {
        if (!result || !result.printouts) return '';
       
        var propMap = {
            'Has brand': 'brand',
            'Has model code': 'model',
            'Has screen size inches': 'size',
            'Has CRT type': 'type'
        };
       
        var key = propMap[sortProperty] || 'brand';
        var printout = result.printouts[key];
       
        if (!printout || !printout[0]) return '';
       
        var val = printout[0];
        if (typeof val === 'object') {
            return String(val.fulltext || val.value || '');
        }
        return String(val);
    }
 
    /**
    * Parse count result from SMW
    */
    function parseCountResult(data) {
        if (!data.query) return 0;
       
        // SMW returns count in various formats depending on version
        if (typeof data.query.results === 'number') {
            return data.query.results;
        }
        if (typeof data.query.results === 'string') {
            var match = data.query.results.match(/\d+/);
            return match ? parseInt(match[0], 10) : 0;
        }
        if (data.query.meta && data.query.meta.count !== undefined) {
            return parseInt(data.query.meta.count, 10);
        }
       
        return 0;
    }
 
    /**
    * Display search results
    */
    function displayResults(data) {
        var grid = document.getElementById('results-grid');
        var countDiv = document.getElementById('results-count');
       
        if (!data.query || !data.query.results || Object.keys(data.query.results).length === 0) {
            grid.innerHTML = '<div class="crt-no-results">' +
                '<div class="crt-no-results-icon">📺</div>' +
                '<div class="crt-no-results-text">No CRTs found matching your filters.</div>' +
                '<div class="crt-no-results-hint">Try adjusting your filters or <a href="#" id="reset-from-no-results">reset all filters</a>.</div>' +
                '</div>';
            countDiv.textContent = '0 results';
            document.getElementById('pagination').innerHTML = '';
           
            var resetLink = document.getElementById('reset-from-no-results');
            if (resetLink) {
                resetLink.addEventListener('click', function(e) {
                    e.preventDefault();
                    resetFilters();
                });
            }
            return;
        }
       
        var results = data.query.results;
        var resultKeys = Object.keys(results);
       
        // Update count
        var startNum = currentOffset + 1;
        var endNum = Math.min(currentOffset + resultKeys.length, totalResults);
        countDiv.textContent = 'Showing ' + startNum + '–' + endNum + ' of ' + totalResults + ' CRTs';
       
        var html = '';
        resultKeys.forEach(function(key) {
            var crt = results[key];
            var printouts = crt.printouts || {};
           
            var title = key.replace(/_/g, ' ');
            var url = crt.fullurl || mw.util.getUrl(key);
            var brand = getFirstValue(printouts.brand);
            var model = getFirstValue(printouts.model) || title;
            var type = getFirstValue(printouts.type);
            var size = getFirstValue(printouts.size);
            var aspect = getFirstValue(printouts.aspect);
            var tvl = getFirstValue(printouts.tvl);
            var image = getImageUrl(printouts.image);
           
            html += '<a href="' + url + '" class="crt-result-card">';
            html += '<div class="crt-card-image">';
            if (image) {
                html += '<img src="' + image + '" alt="' + escapeHtml(title) + '" loading="lazy">';
            } else {
                html += '<div class="crt-no-image"><span>📺</span></div>';
            }
            html += '</div>';
            html += '<div class="crt-card-info">';
            html += '<div class="crt-card-brand">' + escapeHtml(brand || '') + '</div>';
            html += '<div class="crt-card-title">' + escapeHtml(model) + '</div>';
            html += '<div class="crt-card-meta">';
            if (type) html += '<span class="crt-meta-type">' + escapeHtml(type) + '</span>';
            if (size) html += '<span class="crt-meta-size">' + size + '"</span>';
            if (aspect) html += '<span class="crt-meta-aspect">' + escapeHtml(aspect) + '</span>';
            if (tvl) html += '<span class="crt-meta-tvl">' + tvl + ' TVL</span>';
            html += '</div>';
            html += '</div>';
            html += '</a>';
        });
       
        grid.innerHTML = html;
        grid.classList.add('grid-view');
       
        buildPagination();
    }
 
    /**
    * Get first value from SMW printout
    */
    function getFirstValue(printout) {
        if (!printout || !printout[0]) return null;
        var val = printout[0];
        if (typeof val === 'object') {
            return val.fulltext || val.value || null;
        }
        return val;
    }
 
    /**
    * Get image URL from SMW printout
    */
    function getImageUrl(printout) {
        if (!printout || !printout[0]) return null;
        var img = printout[0];
       
        // Extract filename from various formats SMW might return
        var imgName;
        if (typeof img === 'object') {
            // SMW returns object with fulltext like "File:filename.jpg"
            imgName = img.fulltext || '';
        } else {
            imgName = String(img);
        }
       
        // Remove "File:" prefix if present
        imgName = imgName.replace(/^File:/, '');
       
        if (!imgName) return null;
       
        // Use Special:Redirect/file to get the actual image with thumbnail
        return '/wiki/Special:Redirect/file/' + encodeURIComponent(imgName) + '?width=160';
    }
 
    /**
    * Build pagination controls
    */
    function buildPagination() {
        var pagination = document.getElementById('pagination');
       
        if (totalResults <= RESULTS_PER_PAGE) {
            pagination.innerHTML = '';
            return;
        }
       
        var totalPages = Math.ceil(totalResults / RESULTS_PER_PAGE);
        var currentPage = Math.floor(currentOffset / RESULTS_PER_PAGE) + 1;
       
        var html = '<div class="crt-pagination-info">';
        html += 'Page ' + currentPage + ' of ' + totalPages;
        html += '</div>';
        html += '<div class="crt-pagination-buttons">';
       
        // Previous button
        if (currentPage > 1) {
            html += '<button class="crt-page-btn crt-page-prev" data-offset="' + ((currentPage - 2) * RESULTS_PER_PAGE) + '">← Previous</button>';
        }
       
        // Page numbers
        var startPage = Math.max(1, currentPage - 2);
        var endPage = Math.min(totalPages, currentPage + 2);
       
        if (startPage > 1) {
            html += '<button class="crt-page-btn" data-offset="0">1</button>';
            if (startPage > 2) html += '<span class="crt-page-ellipsis">…</span>';
        }
       
        for (var i = startPage; i <= endPage; i++) {
            var active = i === currentPage ? ' active' : '';
            html += '<button class="crt-page-btn' + active + '" data-offset="' + ((i - 1) * RESULTS_PER_PAGE) + '">' + i + '</button>';
        }
       
        if (endPage < totalPages) {
            if (endPage < totalPages - 1) html += '<span class="crt-page-ellipsis">…</span>';
            html += '<button class="crt-page-btn" data-offset="' + ((totalPages - 1) * RESULTS_PER_PAGE) + '">' + totalPages + '</button>';
        }
       
        // Next button
        if (currentPage < totalPages) {
            html += '<button class="crt-page-btn crt-page-next" data-offset="' + (currentPage * RESULTS_PER_PAGE) + '">Next →</button>';
        }
       
        html += '</div>';
        pagination.innerHTML = html;
       
        // Attach click handlers
        pagination.querySelectorAll('.crt-page-btn').forEach(function(btn) {
            btn.addEventListener('click', function() {
                currentOffset = parseInt(this.dataset.offset, 10);
                performSearch();
                updateURL();
               
                // Scroll to top of results
                document.querySelector('.crt-results-header').scrollIntoView({ behavior: 'smooth' });
            });
        });
    }
 
    /**
    * Update URL with current filters
    */
    function updateURL() {
        var filters = collectFilters();
        var params = new URLSearchParams();
       
        for (var key in filters) {
            var value = filters[key];
            if (Array.isArray(value)) {
                params.set(key, value.join(','));
            } else if (value === true) {
                params.set(key, '1');
            } else {
                params.set(key, value);
            }
        }
       
        var sortBy = document.getElementById('sort-by').value;
        if (sortBy !== 'brand') {
            params.set('sort', sortBy);
        }
       
        if (currentOffset > 0) {
            params.set('offset', currentOffset);
        }
       
        var newUrl = window.location.pathname;
        var paramString = params.toString();
        if (paramString) {
            newUrl += '?' + paramString;
        }
       
        history.replaceState(null, '', newUrl);
    }
 
    /**
    * Parse URL parameters and set filters
    */
    function parseURLParams() {
        var params = new URLSearchParams(window.location.search);
       
        params.forEach(function(value, key) {
            if (key === 'sort') {
                document.getElementById('sort-by').value = value;
                return;
            }
            if (key === 'offset') {
                currentOffset = parseInt(value, 10) || 0;
                return;
            }
           
            // Find the filter
            var filterConfig = null;
            for (var sectionKey in FILTER_CONFIG) {
                if (FILTER_CONFIG[sectionKey].filters[key]) {
                    filterConfig = FILTER_CONFIG[sectionKey].filters[key];
                    break;
                }
            }
           
            if (!filterConfig) return;
           
            switch (filterConfig.type) {
                case 'select':
                case 'number':
                    var input = document.getElementById('filter-' + key);
                    if (input) input.value = value;
                    break;
                case 'checkbox':
                    var checkbox = document.getElementById('filter-' + key);
                    if (checkbox) checkbox.checked = (value === '1' || value === 'true');
                    break;
                case 'multiselect':
                    var ms = document.getElementById('filter-' + key);
                    if (ms) {
                        var values = value.split(',');
                        var selectedContainer = ms.querySelector('.crt-multiselect-selected');
                        values.forEach(function(v) {
                            v = v.trim();
                            if (!v) return;
                            var tag = document.createElement('span');
                            tag.className = 'crt-multiselect-tag';
                            tag.dataset.value = v;
                            tag.innerHTML = escapeHtml(v) + ' <span class="crt-tag-remove">×</span>';
                            tag.querySelector('.crt-tag-remove').addEventListener('click', function(e) {
                                e.stopPropagation();
                                tag.remove();
                                triggerSearch(true);
                            });
                            selectedContainer.appendChild(tag);
                        });
                    }
                    break;
            }
        });
     }
 
    /**
    * Show loading state
    */
    function showLoading() {
        var grid = document.getElementById('results-grid');
        grid.innerHTML = '<div class="crt-loading">' +
            '<div class="crt-spinner"></div>' +
            '<div class="crt-loading-text">Searching...</div>' +
            '</div>';
    }
 
    /**
    * Show error message
    */
    function showError(message) {
        var grid = document.getElementById('results-grid');
        grid.innerHTML = '<div class="crt-error">' +
            '<div class="crt-error-icon">⚠️</div>' +
            '<div class="crt-error-text">' + escapeHtml(message) + '</div>' +
            '</div>';
        document.getElementById('pagination').innerHTML = '';
    }
 
    /**
    * Escape HTML entities
    */
    function escapeHtml(str) {
        if (!str) return '';
        return String(str)
            .replace(/&/g, '&amp;')
            .replace(/</g, '&lt;')
            .replace(/>/g, '&gt;')
            .replace(/"/g, '&quot;')
            .replace(/'/g, '&#039;');
    }


    // Initialize when DOM is ready
    if (document.readyState === 'loading') {
        document.addEventListener('DOMContentLoaded', init);
    } else {
        init();
    }
})();
})();
MediaWiki Appliance - Powered by TurnKey Linux