Created page with "* * EveryCRT AV Device Search * Server-side faceted search for AV devices using Semantic MediaWiki API: (function() { 'use strict'; var RESULTS_PER_PAGE = 48; var currentOffset = 0; var filterOptions = {}; var totalResults = 0; var totalResultsKnown = true; var isLoading = false; var searchDebounceTimer = null; var DEBOUNCE_DELAY = 400; // Filter configuration var FILTER_CONFIG = { general: { label:..."
 
No edit summary
 
(11 intermediate revisions by the same user not shown)
Line 1: Line 1:
/**
/**
  * EveryCRT AV Device Search
  * EveryCRT AV Device Search — page-specific configuration
  * Server-side faceted search for AV devices using Semantic MediaWiki API
  * Loads the shared search engine and passes AV device-specific filters, printouts, and card layout.
  */
  */
(function() {
(function() {
     'use strict';
     'use strict';


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


    // Filter configuration
         window.EveryCRTSearch({
    var FILTER_CONFIG = {
        general: {
            label: 'General',
            collapsed: false,
            filters: {
                brand: {
                    label: 'Brand',
                    type: 'select',
                    property: 'Has brand',
                    loadFrom: 'category',
                    category: 'Manufacturers'
                },
                device_type: {
                    label: 'Device Type',
                    type: 'select',
                    property: 'Has device type',
                    loadFrom: 'static',
                    values: ['Character generator', 'Distribution amplifier', 'Editing controller', 'Frame synchronizer', 'Scan converter', 'Time base corrector', 'VCR', 'VCR/DVD combo', 'VCR/DVD-R combo', 'VCR/HDD combo', 'Video mixer', 'Video player', 'Video processor', 'VTR', 'Other']
                },
                format_family: {
                    label: 'Format Family',
                    type: 'select',
                    property: 'Has format family',
                    loadFrom: 'static',
                    values: ['Betamax', 'DV', 'U-matic', 'VHS', 'Video8']
                }
            }
         },
        tbc: {
            label: 'TBC Capabilities',
            collapsed: false,
            filters: {
                has_line_tbc: {
                    label: 'Line TBC',
                    type: 'checkbox',
                    property: 'TBC type',
                    containsValue: 'Line TBC'
                },
                has_frame_tbc: {
                    label: 'Frame TBC',
                    type: 'checkbox',
                    property: 'TBC type',
                    containsValue: 'Frame TBC'
                }
            }
        }
    };


    function triggerSearch(immediate) {
             containerId: 'av-search-app',
        if (searchDebounceTimer) {
             category: 'AV devices',
             clearTimeout(searchDebounceTimer);
             resultLabel: 'devices',
        }
             noImageEmoji: '\uD83D\uDCFC',
        if (immediate) {
             defaultSort: 'brand',
             currentOffset = 0;
             performSearch();
             updateURL();
        } else {
            searchDebounceTimer = setTimeout(function() {
                currentOffset = 0;
                performSearch();
                updateURL();
             }, DEBOUNCE_DELAY);
        }
    }


    function init() {
            // ── Sort options ───────────────────────────────────────
        var container = document.getElementById('av-search-app');
            sortOptions: [
        if (!container) return;
                { value: 'brand',      label: 'Brand',      property: 'Has brand',      printoutKey: 'brand' },
                { value: 'model',      label: 'Model',      property: 'Has model code',  printoutKey: 'model' },
                { value: 'device_type', label: 'Device Type', property: 'Has device type', printoutKey: 'device_type' }
            ],


        container.innerHTML = buildHTML();
            // ── Extra printouts (image, brand, model are always included) ──
        attachEventListeners();
            printouts: [
        loadFilterOptions();
                { property: 'Has device type',  key: 'device_type' },
    }
                { property: 'Has format family', key: 'format_family' },
                { property: 'Has supported formats',  key: 'supported_formats' }
            ],


    function buildHTML() {
            // ── Card meta fields ───────────────────────────────────
        var html = '<div class="crt-search-wrapper">';
            cardMeta: [
                { key: 'device_type',  cssClass: 'crt-meta-type' },
                { key: 'format_family' }
            ],


        // Notifications
            // ── Filter configuration ───────────────────────────────
        html += '<div class="crt-notifications" id="notifications"></div>';
            filterConfig: {
 
                general: {
        // Mobile filter toggle
                    label: 'General',
        html += '<button type="button" class="crt-mobile-filter-toggle" id="mobile-filter-toggle">';
                    collapsed: false,
        html += '<span class="crt-filter-icon">☰</span> Filters';
                    filters: {
        html += '</button>';
                        brand: {
 
                            label: 'Brand',
        // Filters sidebar
                            type: 'select',
        html += '<div class="crt-search-filters" id="filters-sidebar">';
                            property: 'Has brand',
        html += '<div class="crt-filters-header">';
                            loadFrom: 'smw',
        html += '<h2>Filters</h2>';
                            smwQuery: '[[Is AV brand category::Yes]]',
        html += '<button type="button" class="crt-reset-btn" id="reset-filters">Reset All</button>';
                            smwProperty: 'Brand name'
        html += '</div>';
                        },
 
                        device_type: {
        html += '<div class="crt-filters-body">';
                            label: 'Device Type',
        for (var sectionKey in FILTER_CONFIG) {
                            type: 'select',
            html += buildFilterSection(sectionKey, FILTER_CONFIG[sectionKey]);
                            property: 'Has device type',
        }
                            loadFrom: 'smw',
        html += '</div>';
                            smwQuery: '[[Is AV device type category::Yes]]',
 
                            smwProperty: 'Device type name'
        html += '<div class="crt-apply-filters">';
                        },
        html += '<button type="button" class="crt-apply-btn" id="apply-filters">Apply Filters</button>';
                        format_family: {
        html += '</div>';
                            label: 'Format Family',
        html += '</div>';
                            type: 'select',
 
                            property: 'Has format family',
        // Results area
                            loadFrom: 'smw',
        html += '<div class="crt-search-results">';
                            smwQuery: '[[Category:AV devices]][[Has format family::+]]',
        html += '<div class="crt-results-header">';
                            smwProperty: 'Has format family'
        html += '<div class="crt-results-count" id="results-count">Loading...</div>';
                        },
        html += '<div class="crt-results-controls">';
                        plays: {
        html += '<div class="crt-results-sort">';
                            label: 'Plays',
        html += '<label for="sort-by">Sort by:</label>';
                            type: 'select',
        html += '<select id="sort-by">';
                            property: 'Has supported formats',
        html += '<option value="brand">Brand</option>';
                            loadFrom: 'smw',
        html += '<option value="model">Model</option>';
                            smwQuery: '[[Category:AV devices]][[Has supported formats::+]]',
        html += '<option value="device_type">Device Type</option>';
                            smwProperty: 'Has supported formats'
        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;
    }
 
    function buildFilterSection(sectionKey, section) {
        var collapsed = section.collapsed ? ' collapsed' : '';
        var html = '<div class="crt-filter-section' + collapsed + '" data-section="' + sectionKey + '">';
        html += '<div class="crt-section-header">';
        html += '<span class="crt-section-title">' + section.label + '</span>';
        html += '<span class="crt-section-toggle">▼</span>';
        html += '</div>';
        html += '<div class="crt-section-content">';
        for (var filterKey in section.filters) {
            html += buildFilterInput(filterKey, section.filters[filterKey]);
        }
        html += '</div></div>';
        return html;
    }
 
    function buildFilterInput(filterKey, filter) {
        var html = '<div class="crt-filter-item" data-filter="' + filterKey + '">';
        switch (filter.type) {
            case 'select':
                html += '<label for="filter-' + filterKey + '">' + filter.label + '</label>';
                html += '<select id="filter-' + filterKey + '" class="crt-filter-select" data-filter="' + filterKey + '">';
                html += '<option value="">All</option>';
                if (filter.loadFrom === 'static' && filter.values) {
                    for (var i = 0; i < filter.values.length; i++) {
                        html += '<option value="' + filter.values[i] + '">' + filter.values[i] + '</option>';
                     }
                     }
                 }
                 },
                 html += '</select>';
                 tbc: {
                break;
                    label: 'TBC Capabilities',
            case 'checkbox':
                    collapsed: false,
                html += '<label class="crt-checkbox-label">';
                    filters: {
                html += '<input type="checkbox" id="filter-' + filterKey + '" class="crt-filter-checkbox" data-filter="' + filterKey + '">';
                         has_line_tbc: {
                html += '<span>' + filter.label + '</span>';
                            label: 'Line TBC',
                html += '</label>';
                            type: 'checkbox',
                break;
                            property: 'TBC type',
        }
                            containsValue: 'Line TBC'
        html += '</div>';
                         },
        return html;
                         has_frame_tbc: {
    }
                             label: 'Frame synchronizer',
 
                            type: 'checkbox',
    function showNotification(message, type, duration) {
                            property: 'TBC type',
        var container = document.getElementById('notifications');
                            containsValue: 'Frame synchronizer'
        if (!container) return;
                        }
        type = type || 'info';
        duration = duration !== undefined ? duration : 8000;
        var notification = document.createElement('div');
        notification.className = 'crt-notification crt-notification-' + type;
        var icon = type === 'error' ? '⚠️' : type === 'warning' ? '⚠️' : 'ℹ️';
        notification.innerHTML = '<span class="crt-notification-icon">' + icon + '</span>' +
            '<span class="crt-notification-message">' + escapeHtml(message) + '</span>' +
            '<button class="crt-notification-close" title="Dismiss">×</button>';
        notification.querySelector('.crt-notification-close').addEventListener('click', function() {
            notification.remove();
        });
        container.appendChild(notification);
        if (duration > 0) {
            setTimeout(function() { if (notification.parentNode) notification.remove(); }, duration);
        }
    }
 
    function attachEventListeners() {
        document.getElementById('mobile-filter-toggle').addEventListener('click', function() {
            document.getElementById('filters-sidebar').classList.toggle('mobile-open');
            this.classList.toggle('active');
        });
 
        document.querySelectorAll('#av-search-app .crt-section-header').forEach(function(header) {
            header.addEventListener('click', function() {
                this.closest('.crt-filter-section').classList.toggle('collapsed');
            });
        });
 
        document.getElementById('apply-filters').addEventListener('click', function() {
            currentOffset = 0;
            performSearch();
            updateURL();
            document.getElementById('filters-sidebar').classList.remove('mobile-open');
            document.getElementById('mobile-filter-toggle').classList.remove('active');
        });
 
        document.getElementById('reset-filters').addEventListener('click', resetFilters);
 
        document.getElementById('sort-by').addEventListener('change', function() {
            triggerSearch(true);
        });
 
        document.querySelectorAll('#av-search-app .crt-view-btn').forEach(function(btn) {
            btn.addEventListener('click', function() {
                document.querySelectorAll('#av-search-app .crt-view-btn').forEach(function(b) { b.classList.remove('active'); });
                this.classList.add('active');
                var grid = document.getElementById('results-grid');
                grid.classList.remove('grid-view', 'list-view');
                grid.classList.add(this.dataset.view + '-view');
            });
        });
 
        document.querySelectorAll('#av-search-app .crt-filter-select').forEach(function(select) {
            select.addEventListener('change', function() { triggerSearch(true); });
        });
 
        document.querySelectorAll('#av-search-app .crt-filter-checkbox').forEach(function(checkbox) {
            checkbox.addEventListener('change', function() { triggerSearch(true); });
        });
    }
 
    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();
            parseURLParams();
            performSearch();
        });
    }
 
    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();
            });
        });
    }
 
    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;
                    }
                }
            }
        }
    }
 
    function collectFilters() {
        var filters = {};
        document.querySelectorAll('#av-search-app .crt-filter-select').forEach(function(input) {
            var value = input.value.trim();
            if (value) filters[input.dataset.filter] = value;
        });
        document.querySelectorAll('#av-search-app .crt-filter-checkbox').forEach(function(input) {
            if (input.checked) filters[input.dataset.filter] = true;
        });
        return filters;
    }
 
    function resetFilters() {
        document.querySelectorAll('#av-search-app .crt-filter-select').forEach(function(input) { input.value = ''; });
        document.querySelectorAll('#av-search-app .crt-filter-checkbox').forEach(function(input) { input.checked = false; });
        currentOffset = 0;
        var params = new URLSearchParams(window.location.search);
        var titleParam = params.get('title');
        var newUrl = window.location.pathname;
        if (titleParam) newUrl += '?title=' + encodeURIComponent(titleParam);
        history.replaceState(null, '', newUrl);
        performSearch();
    }
 
    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 (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></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></span>';
            }
        }
 
        if (hasFilters) {
            html += '<button class="crt-clear-all-filters">Clear All</button>';
        }
 
        container.innerHTML = html;
 
        container.querySelectorAll('.crt-active-filter').forEach(function(af) {
            af.querySelector('.crt-af-remove').addEventListener('click', function() {
                removeActiveFilter(af.dataset.filter);
            });
        });
 
        var clearAll = container.querySelector('.crt-clear-all-filters');
        if (clearAll) clearAll.addEventListener('click', resetFilters);
    }
 
    function removeActiveFilter(filterKey) {
        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;
        if (filterConfig.type === 'select') {
            document.getElementById('filter-' + filterKey).value = '';
        } else if (filterConfig.type === 'checkbox') {
            document.getElementById('filter-' + filterKey).checked = false;
        }
        triggerSearch(true);
    }
 
    function getFilterLabel(filterKey) {
        for (var sectionKey in FILTER_CONFIG) {
            if (FILTER_CONFIG[sectionKey].filters[filterKey]) {
                return FILTER_CONFIG[sectionKey].filters[filterKey].label;
            }
        }
        return filterKey;
    }
 
    function buildSMWQuery() {
        var filters = collectFilters();
        var conditions = ['[[Category:AV devices]]'];
 
        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.type === 'select' && filter.property) {
                    conditions.push('[[' + filter.property + '::' + value + ']]');
                } else if (filter.type === 'checkbox' && filter.property && value === true) {
                    if (filter.containsValue) {
                        conditions.push('[[' + filter.property + '::~*' + filter.containsValue + '*]]');
                     }
                     }
                 }
                 }
             }
             }
        }


        return conditions.join('');
    }
    function performSearch() {
        if (isLoading) return;
        isLoading = true;
        showLoading();
        updateActiveFilters();
        var query = buildSMWQuery();
        var sortBy = document.getElementById('sort-by').value;
        var sortProperty = 'Has brand';
        switch (sortBy) {
            case 'model': sortProperty = 'Has model code'; break;
            case 'device_type': sortProperty = 'Has device type'; break;
        }
        var api = new mw.Api();
        // Get count first
        api.get({
            action: 'ask',
            query: query + '|limit=0|format=json',
            format: 'json'
        }).done(function(countData) {
            totalResults = 0;
            totalResultsKnown = true;
            if (countData.query && countData.query.meta && countData.query.meta.count !== undefined) {
                totalResults = parseInt(countData.query.meta.count, 10) || 0;
            }
            api.get({
                action: 'ask',
                query: query +
                    '|?Has image#100px=image' +
                    '|?Has brand=brand' +
                    '|?Has model code=model' +
                    '|?Has device type=device_type' +
                    '|?Has format family=format_family' +
                    '|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.');
            });
        }).fail(function() {
            totalResults = 0;
            totalResultsKnown = false;
            api.get({
                action: 'ask',
                query: query +
                    '|?Has image#100px=image' +
                    '|?Has brand=brand' +
                    '|?Has model code=model' +
                    '|?Has device type=device_type' +
                    '|?Has format family=format_family' +
                    '|sort=' + sortProperty +
                    '|order=asc' +
                    '|limit=' + RESULTS_PER_PAGE +
                    '|offset=' + currentOffset +
                    '|format=json',
                format: 'json'
            }).done(function(data) {
                isLoading = false;
                displayResults(data);
            }).fail(function() {
                isLoading = false;
                showError('Search failed. Please try again.');
            });
         });
         });
    }
    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) {
    }, function(error) {
             grid.innerHTML = '<div class="crt-no-results">' +
         console.error('Failed to load EveryCRT search engine:', error);
                 '<div class="crt-no-results-icon">📼</div>' +
        var el = document.getElementById('av-search-app');
                '<div class="crt-no-results-text">No AV devices found matching your filters.</div>' +
        if (el) {
                 '<div class="crt-no-results-hint">Try adjusting your filters or <a href="#" id="reset-from-no-results">reset all filters</a>.</div>' +
             el.innerHTML = '<div style="text-align:center;padding:64px;background:#2a2a2e;border-radius:12px;">' +
                '</div>';
                 '<p style="font-size:18px;color:#ddd;">Failed to load search engine.</p>' +
            countDiv.textContent = '0 results';
                 '<p style="color:#888;font-size:13px;margin-top:12px;">Please try refreshing the page.</p></div>';
            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);
        if (!totalResultsKnown || totalResults === 0) {
            if (resultKeys.length === RESULTS_PER_PAGE) {
                totalResults = currentOffset + resultKeys.length + 1;
                totalResultsKnown = false;
            } else {
                totalResults = currentOffset + resultKeys.length;
                totalResultsKnown = true;
            }
        }
        var startNum = currentOffset + 1;
        var endNum = currentOffset + resultKeys.length;
        if (!totalResultsKnown) {
            countDiv.textContent = 'Showing ' + startNum + '\u2013' + endNum + ' of ' + totalResults + '+ devices';
        } else {
            endNum = Math.min(endNum, totalResults);
            countDiv.textContent = 'Showing ' + startNum + '\u2013' + endNum + ' of ' + totalResults + ' devices';
        }
        var html = '';
        resultKeys.forEach(function(key) {
            var device = results[key];
            var printouts = device.printouts || {};
            var title = key.replace(/_/g, ' ');
            var url = device.fullurl || mw.util.getUrl(key);
            var brand = getFirstValue(printouts.brand);
            var model = getFirstValue(printouts.model) || title;
            var deviceType = getFirstValue(printouts.device_type);
            var formatFamily = getFirstValue(printouts.format_family);
            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>\uD83D\uDCFC</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 (deviceType) html += '<span class="crt-meta-type">' + escapeHtml(deviceType) + '</span>';
            if (formatFamily) html += '<span>' + escapeHtml(formatFamily) + '</span>';
            html += '</div>';
            html += '</div>';
            html += '</a>';
        });
        grid.innerHTML = html;
        grid.classList.add('grid-view');
        buildPagination(resultKeys.length);
    }
    function getFirstValue(printout) {
        if (!printout || !printout[0]) return null;
        var val = printout[0];
        return typeof val === 'object' ? (val.fulltext || val.value || null) : val;
    }
    function getImageUrl(printout) {
        if (!printout || !printout[0]) return null;
        var img = printout[0];
        var imgName = typeof img === 'object' ? (img.fulltext || '') : String(img);
        imgName = imgName.replace(/^File:/, '');
        if (!imgName) return null;
        return '/wiki/Special:Redirect/file/' + encodeURIComponent(imgName) + '?width=160';
    }
    function buildPagination(currentPageResultCount) {
        var pagination = document.getElementById('pagination');
        if (currentOffset === 0 && currentPageResultCount < RESULTS_PER_PAGE && totalResultsKnown) {
            pagination.innerHTML = '';
            return;
        }
        var currentPage = Math.floor(currentOffset / RESULTS_PER_PAGE) + 1;
        var html = '<div class="crt-pagination-info">';
        if (totalResultsKnown && totalResults > 0) {
            var totalPages = Math.ceil(totalResults / RESULTS_PER_PAGE);
            html += 'Page ' + currentPage + ' of ' + totalPages;
        } else {
            html += 'Page ' + currentPage;
        }
        html += '</div><div class="crt-pagination-buttons">';
        if (currentPage > 1) {
            html += '<button class="crt-page-btn crt-page-prev" data-offset="' + ((currentPage - 2) * RESULTS_PER_PAGE) + '">\u2190 Previous</button>';
        }
        if (totalResultsKnown && totalResults > 0) {
            var totalPages = Math.ceil(totalResults / RESULTS_PER_PAGE);
            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">\u2026</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">\u2026</span>';
                html += '<button class="crt-page-btn" data-offset="' + ((totalPages - 1) * RESULTS_PER_PAGE) + '">' + totalPages + '</button>';
            }
        }
        if (currentPageResultCount === RESULTS_PER_PAGE) {
            html += '<button class="crt-page-btn crt-page-next" data-offset="' + (currentPage * RESULTS_PER_PAGE) + '">Next \u2192</button>';
        }
        html += '</div>';
        pagination.innerHTML = html;
        pagination.querySelectorAll('.crt-page-btn').forEach(function(btn) {
            btn.addEventListener('click', function() {
                currentOffset = parseInt(this.dataset.offset, 10);
                performSearch();
                updateURL();
                document.querySelector('.crt-results-header').scrollIntoView({ behavior: 'smooth' });
            });
        });
    }
    function updateURL() {
        var filters = collectFilters();
        var params = new URLSearchParams(window.location.search);
        var titleParam = params.get('title');
        params = new URLSearchParams();
        if (titleParam) params.set('title', titleParam);
        for (var key in filters) {
            var value = filters[key];
            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);
    }
    function parseURLParams() {
        var params = new URLSearchParams(window.location.search);
        var invalidFilters = [];
        var hasFilterParams = false;
        params.forEach(function(value, key) {
            if (key !== 'title') hasFilterParams = true;
        });
        if (!hasFilterParams) return;
        params.forEach(function(value, key) {
            if (key === 'title') return;
            if (key === 'sort') {
                document.getElementById('sort-by').value = value;
                return;
            }
            if (key === 'offset') {
                currentOffset = parseInt(value, 10) || 0;
                return;
            }
            var filterConfig = null;
            for (var sectionKey in FILTER_CONFIG) {
                if (FILTER_CONFIG[sectionKey].filters[key]) {
                    filterConfig = FILTER_CONFIG[sectionKey].filters[key];
                    break;
                }
            }
            if (!filterConfig) {
                invalidFilters.push({ key: key, reason: 'Unknown filter "' + key + '"' });
                return;
            }
            switch (filterConfig.type) {
                case 'select':
                    var select = document.getElementById('filter-' + key);
                    if (select) {
                        var optionExists = Array.from(select.options).some(function(opt) { return opt.value === value; });
                        if (optionExists) {
                            select.value = value;
                        } else {
                            invalidFilters.push({ key: key, reason: '"' + value + '" is not a valid option for ' + filterConfig.label });
                        }
                    }
                    break;
                case 'checkbox':
                    var checkbox = document.getElementById('filter-' + key);
                    if (checkbox) checkbox.checked = (value === '1' || value === 'true');
                    break;
            }
        });
        if (invalidFilters.length > 0) {
            invalidFilters.forEach(function(invalid) {
                showNotification(invalid.reason + '. This filter was ignored.', 'warning', 10000);
            });
        }
    }
    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>';
    }
    function showError(message) {
        var grid = document.getElementById('results-grid');
        grid.innerHTML = '<div class="crt-error">' +
            '<div class="crt-error-icon">\u26A0\uFE0F</div>' +
            '<div class="crt-error-text">' + escapeHtml(message) + '</div>' +
            '</div>';
        document.getElementById('pagination').innerHTML = '';
    }
    function escapeHtml(str) {
        if (!str) return '';
        return String(str)
            .replace(/&/g, '&amp;')
            .replace(/</g, '&lt;')
            .replace(/>/g, '&gt;')
            .replace(/"/g, '&quot;')
            .replace(/'/g, '&#039;');
    }
    if (document.readyState === 'loading') {
        document.addEventListener('DOMContentLoaded', init);
    } else {
        init();
    }
})();
})();

Latest revision as of 01:42, 1 March 2026

/**
 * EveryCRT AV Device Search — page-specific configuration
 * Loads the shared search engine and passes AV device-specific filters, printouts, and card layout.
 */
(function() {
    'use strict';

    mw.loader.getScript(
        mw.config.get('wgScript') + '?title=MediaWiki:EveryCRT-SearchEngine.js&action=raw&ctype=text/javascript'
    ).then(function() {

        window.EveryCRTSearch({

            containerId: 'av-search-app',
            category: 'AV devices',
            resultLabel: 'devices',
            noImageEmoji: '\uD83D\uDCFC',
            defaultSort: 'brand',

            // ── Sort options ───────────────────────────────────────
            sortOptions: [
                { value: 'brand',       label: 'Brand',       property: 'Has brand',       printoutKey: 'brand' },
                { value: 'model',       label: 'Model',       property: 'Has model code',  printoutKey: 'model' },
                { value: 'device_type', label: 'Device Type', property: 'Has device type', printoutKey: 'device_type' }
            ],

            // ── Extra printouts (image, brand, model are always included) ──
            printouts: [
                { property: 'Has device type',   key: 'device_type' },
                { property: 'Has format family', key: 'format_family' },
                { property: 'Has supported formats',  key: 'supported_formats' }
            ],

            // ── Card meta fields ───────────────────────────────────
            cardMeta: [
                { key: 'device_type',   cssClass: 'crt-meta-type' },
                { key: 'format_family' }
            ],

            // ── Filter configuration ───────────────────────────────
            filterConfig: {
                general: {
                    label: 'General',
                    collapsed: false,
                    filters: {
                        brand: {
                            label: 'Brand',
                            type: 'select',
                            property: 'Has brand',
                            loadFrom: 'smw',
                            smwQuery: '[[Is AV brand category::Yes]]',
                            smwProperty: 'Brand name'
                        },
                        device_type: {
                            label: 'Device Type',
                            type: 'select',
                            property: 'Has device type',
                            loadFrom: 'smw',
                            smwQuery: '[[Is AV device type category::Yes]]',
                            smwProperty: 'Device type name'
                        },
                        format_family: {
                            label: 'Format Family',
                            type: 'select',
                            property: 'Has format family',
                            loadFrom: 'smw',
                            smwQuery: '[[Category:AV devices]][[Has format family::+]]',
                            smwProperty: 'Has format family'
                        },
                        plays: {
                            label: 'Plays',
                            type: 'select',
                            property: 'Has supported formats',
                            loadFrom: 'smw',
                            smwQuery: '[[Category:AV devices]][[Has supported formats::+]]',
                            smwProperty: 'Has supported formats'
                        }
                    }
                },
                tbc: {
                    label: 'TBC Capabilities',
                    collapsed: false,
                    filters: {
                        has_line_tbc: {
                            label: 'Line TBC',
                            type: 'checkbox',
                            property: 'TBC type',
                            containsValue: 'Line TBC'
                        },
                        has_frame_tbc: {
                            label: 'Frame synchronizer',
                            type: 'checkbox',
                            property: 'TBC type',
                            containsValue: 'Frame synchronizer'
                        }
                    }
                }
            }

        });

    }, function(error) {
        console.error('Failed to load EveryCRT search engine:', error);
        var el = document.getElementById('av-search-app');
        if (el) {
            el.innerHTML = '<div style="text-align:center;padding:64px;background:#2a2a2e;border-radius:12px;">' +
                '<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>';
        }
    });

})();
MediaWiki Appliance - Powered by TurnKey Linux