Personal tools

Sponsorname

Your company could advertise here

www.fla-consulting.eu

MediaWiki:Common.js: Difference between revisions

From GxPlex

Jump to: navigation, search
perf: lazy per-page extract loading, instant render on SMW data ready
fix: add wikitable class to Reference Details table for consistent infobox styling
Line 48: Line 48:
var refTable = refDetailsH2.nextElementSibling;
var refTable = refDetailsH2.nextElementSibling;
if ( refTable && refTable.tagName === 'TABLE' ) {
if ( refTable && refTable.tagName === 'TABLE' ) {
refTable.classList.add( 'gplx-infobox', 'gplx-ref-details' );
refTable.classList.add( 'wikitable', 'gplx-infobox', 'gplx-ref-details' );
refTable.removeAttribute( 'style' );
refTable.removeAttribute( 'style' );
refTable.querySelectorAll( 'th[style]' ).forEach( function ( th ) {
refTable.querySelectorAll( 'th[style]' ).forEach( function ( th ) {

Revision as of 15:19, 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( 'wikitable', '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, '&amp;' )
			.replace( /</g, '&lt;' )
			.replace( />/g, '&gt;' )
			.replace( /"/g, '&quot;' );
	}

	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 );
		} );
	} );
}() );