505 lines
16 KiB
Plaintext
505 lines
16 KiB
Plaintext
/* site-wide JS — VoteNY toggle is handled in Vote.js */
|
||
|
||
/* GxPlex Search — chainable filter buttons with pagination */
|
||
( function () {
|
||
if ( mw.config.get( 'wgPageName' ) !== 'GxPlex_Search' ) {
|
||
return;
|
||
}
|
||
|
||
var PAGE_SIZE = 50;
|
||
|
||
var FILTER_GROUPS = [
|
||
{ key: 'type', label: 'Type' },
|
||
{ key: 'validityArea', label: 'Validity area' },
|
||
{ key: 'scopes', label: 'Scope(s)' },
|
||
{ key: 'status', label: 'Status' },
|
||
{ key: 'docType', label: 'Document type' },
|
||
{ key: 'language', label: 'Language' },
|
||
{ key: 'restricted', label: 'Restricted access' }
|
||
];
|
||
|
||
var DOC_QUERY = '[[Document:+]]'
|
||
+ '|?DocReference=reference'
|
||
+ '|?DocValidityArea=validityArea'
|
||
+ '|?DocScopes=scopes'
|
||
+ '|?DocName=docName'
|
||
+ '|?DocVersion=version'
|
||
+ '|?DocStatus=status'
|
||
+ '|?DocType=docType'
|
||
+ '|?DocLanguage=language'
|
||
+ '|?DocDescription=description'
|
||
+ '|?DocRestricted=restricted'
|
||
+ '|?DocSubmittedBy=submittedBy'
|
||
+ '|?DocContributors=contributors'
|
||
+ '|limit=500'
|
||
+ '|sort=DocName'
|
||
+ '|order=asc';
|
||
|
||
var REF_QUERY = '[[Reference:+]]'
|
||
+ '|?HasDocument=linkedDoc'
|
||
+ '|?HasDocument.DocReference=reference'
|
||
+ '|?HasDocument.DocValidityArea=validityArea'
|
||
+ '|?HasDocument.DocScopes=scopes'
|
||
+ '|?HasDocument.DocName=docName'
|
||
+ '|?HasDocument.DocVersion=version'
|
||
+ '|?HasDocument.DocStatus=status'
|
||
+ '|?HasDocument.DocType=docType'
|
||
+ '|?HasDocument.DocLanguage=language'
|
||
+ '|?HasDocument.DocDescription=description'
|
||
+ '|?HasDocument.DocRestricted=restricted'
|
||
+ '|?HasDocument.DocSubmittedBy=submittedBy'
|
||
+ '|?HasDocument.DocContributors=contributors'
|
||
+ '|?RefScope=refScope'
|
||
+ '|?RefLanguage=refLanguage'
|
||
+ '|?RefTagsEN=tagsEN'
|
||
+ '|limit=500';
|
||
|
||
var allResults = [];
|
||
var filteredResults = [];
|
||
var currentPage = 1;
|
||
var activeFilters = {};
|
||
|
||
/* ---- helpers ---- */
|
||
|
||
function esc( str ) {
|
||
return String( str || '' )
|
||
.replace( /&/g, '&' )
|
||
.replace( /</g, '<' )
|
||
.replace( />/g, '>' )
|
||
.replace( /"/g, '"' );
|
||
}
|
||
|
||
function getProp( printouts, key ) {
|
||
var arr = printouts[ key ];
|
||
if ( !arr || !arr.length ) { return ''; }
|
||
return arr.map( function ( v ) {
|
||
if ( typeof v === 'string' ) { return v; }
|
||
return v.item !== undefined ? v.item : ( v.fulltext || '' );
|
||
} ).filter( Boolean ).join( ', ' );
|
||
}
|
||
|
||
function splitValues( str ) {
|
||
return String( str || '' ).split( ',' ).map( function ( v ) { return v.trim(); } ).filter( Boolean );
|
||
}
|
||
|
||
/* ---- parse SMW responses ---- */
|
||
|
||
function parseResults( data, type ) {
|
||
var raw = ( data.query && data.query.results ) || {};
|
||
return Object.keys( raw ).map( function ( key ) {
|
||
var page = raw[ key ];
|
||
var p = page.printouts || {};
|
||
var docName = getProp( p, 'docName' );
|
||
return {
|
||
_type: type,
|
||
fulltext: page.fulltext,
|
||
url: page.fullurl,
|
||
displayName: type === 'Document'
|
||
? ( docName || page.fulltext.replace( /^Document:/, '' ) )
|
||
: page.fulltext.replace( /^Reference:/, '' ),
|
||
reference: getProp( p, 'reference' ),
|
||
validityArea: getProp( p, 'validityArea' ),
|
||
scopes: getProp( p, 'scopes' ),
|
||
docName: docName,
|
||
version: getProp( p, 'version' ),
|
||
status: getProp( p, 'status' ),
|
||
docType: getProp( p, 'docType' ),
|
||
language: getProp( p, 'language' ),
|
||
description: getProp( p, 'description' ),
|
||
restricted: getProp( p, 'restricted' ),
|
||
submittedBy: getProp( p, 'submittedBy' ),
|
||
contributors: getProp( p, 'contributors' ),
|
||
preview: ''
|
||
};
|
||
} );
|
||
}
|
||
|
||
/* ---- text extracts (250 chars, batches of 50) ---- */
|
||
|
||
function fetchExtracts( results, callback ) {
|
||
if ( !results.length ) { callback(); return; }
|
||
var byTitle = {};
|
||
results.forEach( function ( r ) { byTitle[ r.fulltext ] = r; } );
|
||
var titles = results.map( function ( r ) { return r.fulltext; } );
|
||
var batches = [];
|
||
for ( var i = 0; i < titles.length; i += 50 ) {
|
||
batches.push( titles.slice( i, i + 50 ) );
|
||
}
|
||
var done = 0;
|
||
var api = new mw.Api();
|
||
batches.forEach( function ( batch ) {
|
||
api.get( {
|
||
action: 'query', prop: 'extracts',
|
||
exchars: 250, exintro: true, explaintext: true,
|
||
titles: batch.join( '|' ), format: 'json'
|
||
} ).always( function ( data ) {
|
||
if ( data && data.query && data.query.pages ) {
|
||
Object.keys( data.query.pages ).forEach( function ( id ) {
|
||
var pg = data.query.pages[ id ];
|
||
if ( byTitle[ pg.title ] ) {
|
||
byTitle[ pg.title ].preview = ( pg.extract || '' )
|
||
.replace( /\s+/g, ' ' ).trim().slice( 0, 250 );
|
||
}
|
||
} );
|
||
}
|
||
if ( ++done === batches.length ) { callback(); }
|
||
} );
|
||
} );
|
||
}
|
||
|
||
/* ---- filter button data ---- */
|
||
|
||
function collectFieldValues( results ) {
|
||
var values = {};
|
||
FILTER_GROUPS.forEach( function ( g ) { values[ g.key ] = {}; } );
|
||
results.forEach( function ( r ) {
|
||
FILTER_GROUPS.forEach( function ( g ) {
|
||
var raw = g.key === 'type' ? r._type : r[ g.key ];
|
||
splitValues( raw ).forEach( function ( v ) { values[ g.key ][ v ] = true; } );
|
||
} );
|
||
} );
|
||
return values;
|
||
}
|
||
|
||
/* ---- render filter buttons ---- */
|
||
|
||
function renderFilterButtons( filterSection, values ) {
|
||
filterSection.innerHTML = '';
|
||
activeFilters = {};
|
||
|
||
var hasAnyValue = FILTER_GROUPS.some( function ( g ) {
|
||
return Object.keys( values[ g.key ] || {} ).length > 0;
|
||
} );
|
||
|
||
if ( !hasAnyValue ) {
|
||
var msg = document.createElement( 'p' );
|
||
msg.className = 'gplx-no-results';
|
||
msg.textContent = 'No documents or references indexed yet. Import data to enable filters.';
|
||
filterSection.appendChild( msg );
|
||
return;
|
||
}
|
||
|
||
FILTER_GROUPS.forEach( function ( g ) {
|
||
activeFilters[ g.key ] = {};
|
||
var vals = Object.keys( values[ g.key ] || {} ).sort();
|
||
if ( !vals.length ) { return; }
|
||
|
||
var group = document.createElement( 'div' );
|
||
group.className = 'gplx-filter-group';
|
||
|
||
var label = document.createElement( 'span' );
|
||
label.className = 'gplx-filter-label';
|
||
label.textContent = g.label;
|
||
|
||
var row = document.createElement( 'div' );
|
||
row.className = 'gplx-filter-btn-row';
|
||
|
||
vals.forEach( function ( v ) {
|
||
var btn = document.createElement( 'button' );
|
||
btn.type = 'button';
|
||
btn.className = 'gplx-filter-btn';
|
||
btn.textContent = v;
|
||
btn.setAttribute( 'aria-pressed', 'false' );
|
||
btn.addEventListener( 'click', function () {
|
||
if ( activeFilters[ g.key ][ v ] ) {
|
||
delete activeFilters[ g.key ][ v ];
|
||
btn.classList.remove( 'is-active' );
|
||
btn.setAttribute( 'aria-pressed', 'false' );
|
||
} else {
|
||
activeFilters[ g.key ][ v ] = true;
|
||
btn.classList.add( 'is-active' );
|
||
btn.setAttribute( 'aria-pressed', 'true' );
|
||
}
|
||
refresh();
|
||
} );
|
||
row.appendChild( btn );
|
||
} );
|
||
|
||
group.appendChild( label );
|
||
group.appendChild( row );
|
||
filterSection.appendChild( group );
|
||
} );
|
||
}
|
||
|
||
/* ---- filter logic: AND across groups, OR within group ---- */
|
||
|
||
function matchesFilters( result ) {
|
||
return FILTER_GROUPS.every( function ( g ) {
|
||
var active = Object.keys( activeFilters[ g.key ] || {} );
|
||
if ( !active.length ) { return true; }
|
||
var fieldVal = g.key === 'type' ? result._type : ( result[ g.key ] || '' );
|
||
var parts = splitValues( fieldVal );
|
||
return active.some( function ( av ) {
|
||
return parts.some( function ( p ) {
|
||
return p.toLowerCase() === av.toLowerCase();
|
||
} );
|
||
} );
|
||
} );
|
||
}
|
||
|
||
function applyFilters( results ) {
|
||
var textEl = document.getElementById( 'gplx-filter-text' );
|
||
var text = textEl ? textEl.value.trim().toLowerCase() : '';
|
||
return results.filter( function ( r ) {
|
||
if ( !matchesFilters( r ) ) { return false; }
|
||
if ( text ) {
|
||
var hay = [
|
||
r.displayName, r.reference, r.validityArea, r.scopes,
|
||
r.docName, r.version, r.status, r.docType, r.language,
|
||
r.description, r.restricted, r.submittedBy, r.contributors,
|
||
r.preview
|
||
].join( ' ' ).toLowerCase();
|
||
if ( hay.indexOf( text ) === -1 ) { return false; }
|
||
}
|
||
return true;
|
||
} );
|
||
}
|
||
|
||
/* ---- pagination ---- */
|
||
|
||
function getPageRange( current, total ) {
|
||
if ( total <= 7 ) {
|
||
var pages = [];
|
||
for ( var i = 1; i <= total; i++ ) { pages.push( i ); }
|
||
return pages;
|
||
}
|
||
var result = [ 1 ];
|
||
var left = current - 2;
|
||
var right = current + 2;
|
||
if ( left > 2 ) { result.push( '...' ); }
|
||
for ( var j = Math.max( 2, left ); j <= Math.min( total - 1, right ); j++ ) {
|
||
result.push( j );
|
||
}
|
||
if ( right < total - 1 ) { result.push( '...' ); }
|
||
result.push( total );
|
||
return result;
|
||
}
|
||
|
||
function renderPagination( total ) {
|
||
var el = document.getElementById( 'gplx-pagination' );
|
||
if ( !el ) { return; }
|
||
var totalPages = Math.ceil( total / PAGE_SIZE );
|
||
if ( totalPages <= 1 ) { el.innerHTML = ''; return; }
|
||
|
||
var frag = document.createDocumentFragment();
|
||
|
||
function pageBtn( label, page, isCurrent, isDisabled ) {
|
||
var btn = document.createElement( 'button' );
|
||
btn.type = 'button';
|
||
btn.className = 'gplx-page-btn' + ( isCurrent ? ' gplx-page-btn--current' : '' );
|
||
btn.textContent = label;
|
||
btn.disabled = isDisabled || isCurrent;
|
||
if ( isCurrent ) { btn.setAttribute( 'aria-current', 'page' ); }
|
||
if ( !isDisabled && !isCurrent ) {
|
||
btn.addEventListener( 'click', function () {
|
||
currentPage = page;
|
||
renderPage();
|
||
var s = document.getElementById( 'gplx-search-status' );
|
||
if ( s ) { s.scrollIntoView( { behavior: 'smooth', block: 'nearest' } ); }
|
||
} );
|
||
}
|
||
return btn;
|
||
}
|
||
|
||
frag.appendChild( pageBtn( '‹ Previous', currentPage - 1, false, currentPage === 1 ) );
|
||
|
||
getPageRange( currentPage, totalPages ).forEach( function ( p ) {
|
||
if ( p === '...' ) {
|
||
var span = document.createElement( 'span' );
|
||
span.className = 'gplx-page-ellipsis';
|
||
span.textContent = '…';
|
||
frag.appendChild( span );
|
||
} else {
|
||
frag.appendChild( pageBtn( String( p ), p, p === currentPage, false ) );
|
||
}
|
||
} );
|
||
|
||
frag.appendChild( pageBtn( 'Next ›', currentPage + 1, false, currentPage === totalPages ) );
|
||
|
||
el.innerHTML = '';
|
||
el.appendChild( frag );
|
||
}
|
||
|
||
/* ---- render results table ---- */
|
||
|
||
function renderTable( pageResults ) {
|
||
var container = document.getElementById( 'gplx-search-results' );
|
||
if ( !pageResults.length ) {
|
||
container.innerHTML = '<p class="gplx-no-results">No results match the selected filters.</p>';
|
||
return;
|
||
}
|
||
var rows = pageResults.map( function ( r ) {
|
||
var typeBadge = r._type === 'Document'
|
||
? '<span class="gplx-badge-doc">Document</span>'
|
||
: '<span class="gplx-badge-ref">Reference</span>';
|
||
var restricted = r.restricted && r.restricted.toLowerCase() === 'yes'
|
||
? ' <span class="gplx-badge-restricted">Restricted</span>' : '';
|
||
var preview = r.preview
|
||
? '<div class="gplx-result-preview">' + esc( r.preview ) + '</div>' : '';
|
||
return '<tr>'
|
||
+ '<td>' + typeBadge + '</td>'
|
||
+ '<td><a href="' + esc( r.url ) + '">' + esc( r.displayName ) + '</a>'
|
||
+ restricted + preview + '</td>'
|
||
+ '<td>' + esc( r.validityArea ) + '</td>'
|
||
+ '<td>' + esc( r.status ) + '</td>'
|
||
+ '<td>' + esc( r.language ) + '</td>'
|
||
+ '<td>' + esc( r.docType ) + '</td>'
|
||
+ '</tr>';
|
||
} );
|
||
container.innerHTML = '<table class="wikitable sortable gplx-results-table">'
|
||
+ '<thead><tr>'
|
||
+ '<th>Type</th><th>Title</th>'
|
||
+ '<th>Validity area</th><th>Status</th><th>Language</th><th>Document type</th>'
|
||
+ '</tr></thead>'
|
||
+ '<tbody>' + rows.join( '' ) + '</tbody>'
|
||
+ '</table>';
|
||
}
|
||
|
||
function setStatus( msg, busy ) {
|
||
var s = document.getElementById( 'gplx-search-status' );
|
||
if ( !s ) { return; }
|
||
s.textContent = msg;
|
||
s.className = 'gplx-search-status' + ( busy ? ' gplx-search-busy' : '' );
|
||
}
|
||
|
||
function renderPage() {
|
||
var total = filteredResults.length;
|
||
var totalPages = Math.ceil( total / PAGE_SIZE );
|
||
currentPage = Math.max( 1, Math.min( currentPage, totalPages || 1 ) );
|
||
var start = ( currentPage - 1 ) * PAGE_SIZE;
|
||
var end = Math.min( start + PAGE_SIZE, total );
|
||
|
||
if ( total === 0 ) {
|
||
setStatus( '0 results', false );
|
||
} else {
|
||
setStatus(
|
||
'Showing ' + ( start + 1 ) + '–' + end + ' of ' + total
|
||
+ ' result' + ( total !== 1 ? 's' : '' ),
|
||
false
|
||
);
|
||
}
|
||
|
||
renderTable( filteredResults.slice( start, end ) );
|
||
renderPagination( total );
|
||
}
|
||
|
||
function refresh() {
|
||
filteredResults = applyFilters( allResults );
|
||
currentPage = 1;
|
||
renderPage();
|
||
}
|
||
|
||
/* ---- build container UI ---- */
|
||
|
||
function buildUI( container ) {
|
||
var filterSection = document.createElement( 'div' );
|
||
filterSection.className = 'gplx-filter-buttons';
|
||
|
||
var clearBtn = document.createElement( 'button' );
|
||
clearBtn.type = 'button';
|
||
clearBtn.className = 'gplx-filter-clear';
|
||
clearBtn.textContent = 'Clear all filters';
|
||
clearBtn.addEventListener( 'click', function () {
|
||
FILTER_GROUPS.forEach( function ( g ) { activeFilters[ g.key ] = {}; } );
|
||
document.querySelectorAll( '.gplx-filter-btn' ).forEach( function ( b ) {
|
||
b.classList.remove( 'is-active' );
|
||
b.setAttribute( 'aria-pressed', 'false' );
|
||
} );
|
||
refresh();
|
||
} );
|
||
|
||
var searchLabel = document.createElement( 'label' );
|
||
searchLabel.className = 'gplx-filter-label';
|
||
searchLabel.htmlFor = 'gplx-filter-text';
|
||
searchLabel.textContent = 'Search in titles and text';
|
||
|
||
var textInput = document.createElement( 'input' );
|
||
textInput.type = 'search';
|
||
textInput.id = 'gplx-filter-text';
|
||
textInput.className = 'gplx-filter-input';
|
||
textInput.placeholder = 'Search…';
|
||
textInput.setAttribute( 'aria-label', 'Search in titles and text' );
|
||
|
||
var searchGroup = document.createElement( 'div' );
|
||
searchGroup.className = 'gplx-filter-group gplx-filter-group--grow';
|
||
searchGroup.style.marginBottom = '0.5rem';
|
||
searchGroup.appendChild( searchLabel );
|
||
searchGroup.appendChild( textInput );
|
||
|
||
var status = document.createElement( 'p' );
|
||
status.id = 'gplx-search-status';
|
||
status.className = 'gplx-search-status';
|
||
|
||
var results = document.createElement( 'div' );
|
||
results.id = 'gplx-search-results';
|
||
|
||
var pagination = document.createElement( 'nav' );
|
||
pagination.id = 'gplx-pagination';
|
||
pagination.className = 'gplx-pagination';
|
||
pagination.setAttribute( 'aria-label', 'Results pages' );
|
||
|
||
container.innerHTML = '';
|
||
container.appendChild( filterSection );
|
||
container.appendChild( clearBtn );
|
||
container.appendChild( searchGroup );
|
||
container.appendChild( status );
|
||
container.appendChild( results );
|
||
container.appendChild( pagination );
|
||
|
||
return { filterSection: filterSection, textInput: textInput };
|
||
}
|
||
|
||
/* ---- load data ---- */
|
||
|
||
function load( controls ) {
|
||
setStatus( 'Loading…', true );
|
||
var api = new mw.Api();
|
||
var docResults = [], refResults = [];
|
||
var docDone = false, refDone = false;
|
||
|
||
function onBothDone() {
|
||
allResults = docResults.concat( refResults );
|
||
fetchExtracts( allResults, function () {
|
||
var values = collectFieldValues( allResults );
|
||
renderFilterButtons( controls.filterSection, values );
|
||
if ( allResults.length ) {
|
||
refresh();
|
||
} else {
|
||
setStatus( '', false );
|
||
document.getElementById( 'gplx-search-results' ).innerHTML = '';
|
||
}
|
||
} );
|
||
}
|
||
|
||
api.get( { action: 'ask', query: DOC_QUERY, format: 'json' } )
|
||
.always( function ( data ) {
|
||
if ( data && data.query ) { docResults = parseResults( data, 'Document' ); }
|
||
docDone = true;
|
||
if ( refDone ) { onBothDone(); }
|
||
} );
|
||
|
||
api.get( { action: 'ask', query: REF_QUERY, format: 'json' } )
|
||
.always( function ( data ) {
|
||
if ( data && data.query ) { refResults = parseResults( data, 'Reference' ); }
|
||
refDone = true;
|
||
if ( docDone ) { onBothDone(); }
|
||
} );
|
||
}
|
||
|
||
/* ---- init ---- */
|
||
|
||
mw.loader.using( [ 'mediawiki.api' ], function () {
|
||
$( function () {
|
||
var container = document.getElementById( 'gplx-search-app' );
|
||
if ( !container ) { return; }
|
||
var controls = buildUI( container );
|
||
var debounce;
|
||
controls.textInput.addEventListener( 'input', function () {
|
||
clearTimeout( debounce );
|
||
debounce = setTimeout( refresh, 250 );
|
||
} );
|
||
load( controls );
|
||
} );
|
||
} );
|
||
}() );
|