Note: After publishing, you may have to bypass your browser's cache to see the changes.
- Firefox / Safari: Hold Shift while clicking Reload, or press either Ctrl-F5 or Ctrl-R (⌘-R on a Mac)
- Google Chrome: Press Ctrl-Shift-R (⌘-Shift-R on a Mac)
- Edge: Hold Ctrl while clicking Refresh, or press Ctrl-F5.
/**
* EveryCRT Advanced Search
* Server-side faceted search using Semantic MediaWiki API
*
* Installation:
* 1. Create MediaWiki:AdvancedSearch.js with this content
* 2. Create MediaWiki:AdvancedSearch.css with the companion stylesheet
* 3. Add to MediaWiki:Common.js:
* if (mw.config.get('wgPageName') === 'Advanced_Search') {
* mw.loader.load('/wiki/MediaWiki:AdvancedSearch.js?action=raw&ctype=text/javascript');
* }
*/
(function() {
'use strict';
var API_ENDPOINT = mw.config.get('wgScriptPath') + '/api.php';
var RESULTS_PER_PAGE = 48;
var currentOffset = 0;
var currentFilters = {};
var filterOptions = {};
var totalResults = 0;
var isLoading = false;
// Filter configuration - defines all available filters
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'
},
has_svideo_mod: {
label: 'Has S-Video Mod',
type: 'checkbox',
subobjectQuery: true,
subobjectProperty: 'Has mod type',
subobjectValue: 'S-video'
}
}
},
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'
}
}
}
};
/**
* Initialize the advanced search interface
*/
function init() {
var container = document.getElementById('advanced-search-app');
if (!container) {
console.error('Advanced search container not found');
return;
}
container.innerHTML = buildHTML();
attachEventListeners();
loadFilterOptions();
// Parse URL parameters for initial filters
parseURLParams();
}
/**
* Build the main HTML structure
*/
function buildHTML() {
var html = '<div class="crt-search-wrapper">';
// 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>';
// Build filter sections
for (var sectionKey in FILTER_CONFIG) {
var section = FILTER_CONFIG[sectionKey];
html += buildFilterSection(sectionKey, section);
}
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;
}
/**
* Build a collapsible filter section
*/
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) {
var filter = section.filters[filterKey];
html += buildFilterInput(filterKey, filter);
}
html += '</div>';
html += '</div>';
return html;
}
/**
* Build individual filter input
*/
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>';
break;
case 'multiselect':
html += '<label>' + filter.label + '</label>';
html += '<div class="crt-multiselect" id="filter-' + filterKey + '" data-filter="' + filterKey + '">';
html += '<div class="crt-multiselect-selected"></div>';
html += '<input type="text" class="crt-multiselect-input" placeholder="Search ' + filter.label.toLowerCase() + '...">';
html += '<div class="crt-multiselect-dropdown"></div>';
html += '</div>';
break;
case 'number':
html += '<label for="filter-' + filterKey + '">' + filter.label + '</label>';
html += '<input type="number" id="filter-' + filterKey + '" class="crt-filter-number" ';
html += 'data-filter="' + filterKey + '" placeholder="' + (filter.placeholder || '') + '">';
break;
case 'checkbox':
html += '<label class="crt-checkbox-label">';
html += '<input type="checkbox" id="filter-' + filterKey + '" class="crt-filter-checkbox" ';
html += 'data-filter="' + filterKey + '">';
html += '<span>' + filter.label + '</span>';
html += '</label>';
break;
case 'range':
html += '<label>' + filter.label + '</label>';
html += '<div class="crt-range-inputs">';
html += '<input type="number" id="filter-' + filterKey + '-min" class="crt-filter-range-min" ';
html += 'data-filter="' + filterKey + '-min" placeholder="Min">';
html += '<span class="crt-range-sep">–</span>';
html += '<input type="number" id="filter-' + filterKey + '-max" class="crt-filter-range-max" ';
html += 'data-filter="' + filterKey + '-max" placeholder="Max">';
html += '</div>';
break;
}
html += '</div>';
return html;
}
/**
* Attach event listeners
*/
function attachEventListeners() {
// 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
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
document.getElementById('sort-by').addEventListener('change', function() {
currentOffset = 0;
performSearch();
updateURL();
});
// 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');
});
});
// Enter key on filter inputs
document.querySelectorAll('.crt-filter-number, .crt-filter-select').forEach(function(input) {
input.addEventListener('keypress', function(e) {
if (e.key === 'Enter') {
currentOffset = 0;
performSearch();
updateURL();
}
});
});
// Select change triggers search
document.querySelectorAll('.crt-filter-select').forEach(function(select) {
select.addEventListener('change', function() {
// Auto-apply on select change for better UX
// currentOffset = 0;
// performSearch();
// updateURL();
});
});
// 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);
});
selectedContainer.appendChild(tag);
}
updateMultiselectDropdown(ms, ms.dataset.filter, ms.querySelector('.crt-multiselect-input').value);
}
/**
* 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;
}
}
}
}
}
/**
* Collect current filter values
*/
function collectFilters() {
var filters = {};
// Collect select and number filters
document.querySelectorAll('.crt-filter-select, .crt-filter-number').forEach(function(input) {
var value = input.value.trim();
if (value) {
filters[input.dataset.filter] = value;
}
});
// Collect checkbox filters
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;
}
currentOffset = 0;
performSearch();
updateURL();
}
/**
* 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
});
}
else if (filter.type === 'multiselect' && Array.isArray(value) && value.length > 0) {
value.forEach(function(v) {
subobjectConditions.push({
property: filter.subobjectProperty,
value: v
});
});
}
}
else if (filter.type === 'select' && filter.property) {
conditions.push('[[' + filter.property + '::' + value + ']]');
}
else if (filter.type === 'number' && filter.property) {
var comp = filter.comparison || '=';
if (comp === '>=') {
conditions.push('[[' + filter.property + '::≥' + value + ']]');
} else if (comp === '<=') {
conditions.push('[[' + filter.property + '::≤' + value + ']]');
} else {
conditions.push('[[' + filter.property + '::' + value + ']]');
}
}
else if (filter.type === 'checkbox' && filter.property && value === true) {
if (filter.containsValue) {
conditions.push('[[' + filter.property + '::~*' + filter.containsValue + '*]]');
}
}
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);
});
}
/**
* Perform search with subobject conditions (inputs/mods)
* This requires a two-step query due to SMW limitations
*/
function performSubobjectSearch(queryData, sortProperty) {
var api = new mw.Api();
var subConditions = queryData.subobjectConditions;
// Build a query that finds pages through their subobjects
var subQueries = [];
subConditions.forEach(function(cond) {
var subQuery = '[[' + cond.property + '::' + cond.value + ']]|?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) {
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;
}
// Now query for those specific models with main conditions
var modelConditions = Array.from(matchingModels).map(function(m) {
return '[[' + m + ']]';
});
// Combine with main query conditions (remove category since we're targeting specific pages)
var mainConditions = queryData.mainQuery.replace('[[Category:CRT models]]', '');
var combinedQuery = '(' + modelConditions.join(' OR ') + ')' + mainConditions;
totalResults = matchingModels.size;
// Get paginated results
api.get({
action: 'ask',
query: combinedQuery +
'|?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 combined query error:', error);
});
}).catch(function(error) {
isLoading = false;
showError('Search failed. Please try again.');
console.error('Subobject query error:', error);
});
}
/**
* 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];
if (typeof img === 'object' && img.fullurl) {
return img.fullurl;
}
// Construct URL from filename
var imgName = String(img).replace(/^File:/, '');
return mw.config.get('wgScriptPath') + '/index.php?title=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();
});
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, '&')
.replace(/</g, '<')
.replace(/>/g, '>')
.replace(/"/g, '"')
.replace(/'/g, ''');
}
// Initialize when DOM is ready
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', init);
} else {
init();
}
})();