MediaWiki:Common.js: Difference between revisions
From GxPlex
Gxplex admin (talk | contribs) feat: Two-column layout, Reference Details as infobox, Text→Content rename |
Gxplex admin (talk | contribs) perf: lazy per-page extract loading, instant render on SMW data ready |
||
| Line 149: | Line 149: | ||
+ '|limit=500'; | + '|limit=500'; | ||
var allResults | var allResults = []; | ||
var filteredResults = []; | var filteredResults = []; | ||
var currentPage | var currentPage = 1; | ||
var activeFilters | var activeFilters = {}; | ||
var extractsCache = {}; // fulltext → 250-char preview string | |||
var renderToken = 0; // incremented on every renderPage; cancels stale extract callbacks | |||
/* ---- helpers ---- */ | /* ---- helpers ---- */ | ||
| Line 209: | Line 211: | ||
} | } | ||
/* ---- text extracts | /* ---- text extracts – lazy per page, cached ---- */ | ||
function | function fetchPageExtracts( results, token, callback ) { | ||
var missing = results.filter( function ( r ) { return !( r.fulltext in extractsCache ); } ); | |||
var | if ( !missing.length ) { callback( token ); return; } | ||
var api = new mw.Api(); | |||
api.get( { | |||
action: 'query', prop: 'extracts', | |||
exchars: 250, exintro: true, explaintext: true, | |||
titles: missing.map( function ( r ) { return r.fulltext; } ).join( '|' ), | |||
format: 'json' | |||
} ).always( function ( data ) { | |||
var api | if ( data && data.query && data.query.pages ) { | ||
Object.keys( data.query.pages ).forEach( function ( id ) { | |||
var pg = data.query.pages[ id ]; | |||
extractsCache[ pg.title ] = ( pg.extract || '' ) | |||
.replace( /\s+/g, ' ' ).trim().slice( 0, 250 ); | |||
} ); | |||
} | |||
callback( token ); | |||
} ); | } ); | ||
} | } | ||
| Line 458: | Line 450: | ||
function renderPage() { | function renderPage() { | ||
var token = ++renderToken; | |||
var total = filteredResults.length; | var total = filteredResults.length; | ||
var totalPages = Math.ceil( total / PAGE_SIZE ); | var totalPages = Math.ceil( total / PAGE_SIZE ); | ||
| Line 474: | Line 467: | ||
} | } | ||
var pageResults = filteredResults.slice( start, end ); | |||
// Render immediately with any already-cached previews | |||
pageResults.forEach( function ( r ) { r.preview = extractsCache[ r.fulltext ] || ''; } ); | |||
renderTable( pageResults ); | |||
renderPagination( total ); | renderPagination( total ); | ||
// Fetch extracts for this page in the background; re-render only if still on this page | |||
fetchPageExtracts( pageResults, token, function ( t ) { | |||
if ( renderToken !== t ) { return; } | |||
pageResults.forEach( function ( r ) { r.preview = extractsCache[ r.fulltext ] || ''; } ); | |||
renderTable( pageResults ); | |||
} ); | |||
} | } | ||
| Line 554: | Line 558: | ||
function onBothDone() { | function onBothDone() { | ||
allResults = docResults.concat( refResults ); | allResults = docResults.concat( refResults ); | ||
var values = collectFieldValues( allResults ); | |||
renderFilterButtons( controls.filterSection, values ); | |||
if ( allResults.length ) { | |||
refresh(); | |||
} else { | |||
setStatus( '', false ); | |||
document.getElementById( 'gplx-search-results' ).innerHTML = ''; | |||
} | |||
} | |||
} | } | ||
Revision as of 14:49, 29 June 2026
/* site-wide JS — VoteNY toggle is handled in Vote.js */
/* Document: / Reference: page layout
*
* On Document: (ns 3000) and Reference: (ns 3002) pages:
* – Moves all .gplx-infobox tables into a right-hand sidebar column.
* – On Reference: pages additionally:
* • Removes the redundant "Document Information" h2.
* • Converts the raw "Reference Details" wikitable to gplx-infobox style
* and moves it into the sidebar.
* • Renames the "Text" section heading to "Content".
* – Wraps sidebar + main content in a CSS-flex two-column layout.
*/
( function () {
var ns = mw.config.get( 'wgNamespaceNumber' );
if ( ns !== 3000 && ns !== 3002 ) { return; }
$( function () {
var output = document.querySelector( '.mw-parser-output' );
if ( !output ) { return; }
function findH2ById( id ) {
var headings = output.querySelectorAll( 'h2' );
for ( var i = 0; i < headings.length; i++ ) {
var span = headings[ i ].querySelector( '.mw-headline' );
if ( span && span.id === id ) { return headings[ i ]; }
}
return null;
}
var sidebar = document.createElement( 'div' );
sidebar.className = 'gplx-sidebar';
/* --- Move all gplx-infobox tables to sidebar --- */
Array.from( output.querySelectorAll( 'table.gplx-infobox' ) ).forEach( function ( t ) {
sidebar.appendChild( t );
} );
/* --- Reference: page extras --- */
if ( ns === 3002 ) {
/* Remove "Document Information" h2 (table header is sufficient) */
var docInfoH2 = findH2ById( 'Document_Information' );
if ( docInfoH2 ) { docInfoH2.remove(); }
/* Convert Reference Details wikitable to gplx-infobox style */
var refDetailsH2 = findH2ById( 'Reference_Details' );
if ( refDetailsH2 ) {
var refTable = refDetailsH2.nextElementSibling;
if ( refTable && refTable.tagName === 'TABLE' ) {
refTable.classList.add( 'gplx-infobox', 'gplx-ref-details' );
refTable.removeAttribute( 'style' );
refTable.querySelectorAll( 'th[style]' ).forEach( function ( th ) {
th.removeAttribute( 'style' );
} );
/* Prepend header row identical to gplx-infobox-header */
var tbody = refTable.querySelector( 'tbody' ) || refTable;
var headerRow = document.createElement( 'tr' );
var headerCell = document.createElement( 'th' );
headerCell.colSpan = 2;
headerCell.className = 'gplx-infobox-header';
headerCell.textContent = 'Reference details';
headerRow.appendChild( headerCell );
tbody.insertBefore( headerRow, tbody.firstChild );
sidebar.appendChild( refTable );
}
refDetailsH2.remove();
}
/* Rename "Text" section heading to "Content" */
var textH2 = findH2ById( 'Text' );
if ( textH2 ) {
var span = textH2.querySelector( '.mw-headline' );
if ( span ) {
span.textContent = 'Content';
span.id = 'Content';
}
}
}
/* --- Wrap in two-column flex layout --- */
var layout = document.createElement( 'div' );
layout.className = 'gplx-page-layout';
var main = document.createElement( 'div' );
main.className = 'gplx-main';
while ( output.firstChild ) {
main.appendChild( output.firstChild );
}
layout.appendChild( main );
layout.appendChild( sidebar );
output.appendChild( layout );
} );
}() );
/* 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 = {};
var extractsCache = {}; // fulltext → 250-char preview string
var renderToken = 0; // incremented on every renderPage; cancels stale extract callbacks
/* ---- 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 – lazy per page, cached ---- */
function fetchPageExtracts( results, token, callback ) {
var missing = results.filter( function ( r ) { return !( r.fulltext in extractsCache ); } );
if ( !missing.length ) { callback( token ); return; }
var api = new mw.Api();
api.get( {
action: 'query', prop: 'extracts',
exchars: 250, exintro: true, explaintext: true,
titles: missing.map( function ( r ) { return r.fulltext; } ).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 ];
extractsCache[ pg.title ] = ( pg.extract || '' )
.replace( /\s+/g, ' ' ).trim().slice( 0, 250 );
} );
}
callback( token );
} );
}
/* ---- 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 token = ++renderToken;
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
);
}
var pageResults = filteredResults.slice( start, end );
// Render immediately with any already-cached previews
pageResults.forEach( function ( r ) { r.preview = extractsCache[ r.fulltext ] || ''; } );
renderTable( pageResults );
renderPagination( total );
// Fetch extracts for this page in the background; re-render only if still on this page
fetchPageExtracts( pageResults, token, function ( t ) {
if ( renderToken !== t ) { return; }
pageResults.forEach( function ( r ) { r.preview = extractsCache[ r.fulltext ] || ''; } );
renderTable( pageResults );
} );
}
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 );
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 );
} );
} );
}() );