MediaWiki:Gadget-tutorials.js
MediaWiki interface page
More actions
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.
/* =============================================================================
Gadget-tutorials.js v2.1
Unified tutorial hub β API-powered search, tag filtering, pagination.
============================================================================= */
( function () {
if ( mw.config.get( 'wgPageName' ) !== 'Tutorials' ) { return; }
$( function () {
var PAGE_SIZE = 24;
var generation = 0;
var state = {
search : '',
tag : '',
sort : 'newest',
offset : 0,
done : false,
busy : false
};
var $wrapper = $( '.tutorial-hub-wrapper' );
if ( !$wrapper.length ) { return; }
var $filters = $wrapper.find( '.tutorial-hub-filters' );
var $tagCloud = $wrapper.find( '.tutorial-hub-tagcloud' );
var $grid = $wrapper.find( '.tutorial-hub-grid' );
var $count = $( '<span>' ).addClass( 'tutorial-hub-count' );
var $more = $( '<button>' )
.addClass( 'tutorial-hub-loadmore' )
.text( 'Load more tutorials' )
.hide()
.appendTo( $wrapper );
// ββ Filter bar βββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
var $search = $( '<input>' ).attr( {
type : 'text',
placeholder : 'Search name, description, tags\u2026',
'class' : 'project-hub-search'
} );
var $sort = $( '<select>' ).addClass( 'project-hub-select' ).append(
$( '<option>' ).val( 'newest' ).text( 'Newest first' ),
$( '<option>' ).val( 'alpha' ).text( 'A \u2013 Z' )
);
$filters.append( $search, $sort, $count );
// ββ Events βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
var searchDebounce;
$search.on( 'input', function () {
clearTimeout( searchDebounce );
searchDebounce = setTimeout( function () {
state.search = $search.val().trim();
resetAndLoad();
}, 350 );
} );
$sort.on( 'change', function () {
state.sort = $sort.val();
resetAndLoad();
} );
$more.on( 'click', function () {
if ( !state.busy ) { fetchPage(); }
} );
$( document ).on( 'click', '.js-tutorial-tag', function ( e ) {
e.stopPropagation();
var tag = $( this ).data( 'tag' );
if ( state.tag === tag ) {
state.tag = '';
$( '.js-tutorial-tag' ).removeClass( 'is-active' );
} else {
state.tag = tag;
$( '.js-tutorial-tag' ).removeClass( 'is-active' );
$( '.js-tutorial-tag' ).filter( function () {
return $( this ).data( 'tag' ) === tag;
} ).addClass( 'is-active' );
}
resetAndLoad();
} );
// ββ Boot βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
loadTagCloud();
loadHubMeta();
fetchPage();
// ββ Helpers ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
function resetAndLoad() {
generation++;
state.offset = 0;
state.done = false;
state.busy = false;
$grid.empty();
$more.hide();
fetchPage();
}
function buildWhere() {
var parts = [];
if ( state.search ) {
var q = state.search.replace( /'/g, "''" );
parts.push(
"( name LIKE '%" + q + "%'" +
" OR description LIKE '%" + q + "%' )"
);
}
if ( state.tag ) {
parts.push( "tags HOLDS '" + state.tag.replace( /'/g, "''" ) + "'" );
}
return parts.join( ' AND ' );
}
function orderBy() {
return state.sort === 'alpha' ? 'name ASC' : '_pageID DESC';
}
// ββ Data βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
function fetchPage() {
if ( state.busy || state.done ) { return; }
state.busy = true;
var gen = ++generation;
$more.text( 'Loading\u2026' ).show();
var params = {
action : 'cargoquery',
tables : 'Tutorials',
fields : '_pageName=pagename,name,description,image,tags',
order_by : orderBy(),
limit : PAGE_SIZE,
offset : state.offset,
format : 'json'
};
var w = buildWhere();
if ( w ) { params.where = w; }
if ( state.offset === 0 ) { fetchTotal( w ); }
$.getJSON( mw.util.wikiScript( 'api' ), params )
.done( function ( data ) {
if ( gen !== generation ) { return; }
var rows = ( data.cargoquery || [] ).map( function ( r ) { return r.title; } );
var prevOff = state.offset;
var batch = [];
rows.forEach( function ( row ) {
var $c = buildCard( row );
$grid.append( $c );
batch.push( { $el: $c, page: row.pagename } );
} );
enrichCards( batch );
state.offset += rows.length;
state.done = rows.length < PAGE_SIZE;
state.busy = false;
$more.text( 'Load more tutorials' ).toggle( !state.done );
if ( rows.length === 0 && prevOff === 0 ) {
$grid.html( '<div class="project-hub-empty">No tutorials found matching your search.</div>' );
$more.hide();
}
} )
.fail( function () {
if ( gen !== generation ) { return; }
state.busy = false;
$more.text( 'Load more tutorials' ).toggle( !state.done );
} );
}
function fetchTotal( where ) {
var p = { action: 'cargoquery', tables: 'Tutorials', fields: 'COUNT(*)=n', limit: 1, format: 'json' };
if ( where ) { p.where = where; }
$.getJSON( mw.util.wikiScript( 'api' ), p ).done( function ( data ) {
var row = ( data.cargoquery || [] )[ 0 ];
var n = row ? ( parseInt( row.title.n, 10 ) || 0 ) : 0;
$count.text( n + ' tutorial' + ( n === 1 ? '' : 's' ) );
} );
}
function loadTagCloud() {
$.getJSON( mw.util.wikiScript( 'api' ), {
action: 'cargoquery', tables: 'Tutorials', fields: 'tags', limit: 500, format: 'json'
} ).done( function ( data ) {
var freq = {};
( data.cargoquery || [] ).forEach( function ( r ) {
( r.title.tags || '' ).split( ',' ).forEach( function ( t ) {
t = t.trim();
if ( t ) { freq[ t ] = ( freq[ t ] || 0 ) + 1; }
} );
} );
var top = Object.keys( freq )
.sort( function ( a, b ) { return freq[ b ] - freq[ a ]; } )
.slice( 0, 25 );
if ( !top.length ) { return; }
top.forEach( function ( tag ) {
$tagCloud.append(
$( '<span>' ).addClass( 'js-tutorial-tag' ).attr( 'data-tag', tag ).text( tag )
);
} );
$tagCloud.show();
} );
}
function loadHubMeta() {
mw.loader.using( 'mediawiki.api' ).then( function () {
new mw.Api().get( {
action: 'query', list: 'categorymembers', cmtitle: 'Category:Tutorials',
cmsort: 'timestamp', cmdir: 'desc', cmlimit: 1,
cmtype: 'page', cmprop: 'ids|title|timestamp', format: 'json'
} ).done( function ( data ) {
var m = data.query && data.query.categorymembers;
if ( !m || !m.length ) { return; }
var str = m[ 0 ].title.replace( /^.*\//, '' ) + ' \u2014 ' +
( m[ 0 ].timestamp
? new Date( m[ 0 ].timestamp ).toLocaleDateString(
'en-GB', { day: 'numeric', month: 'short', year: 'numeric' } )
: '\u2014' );
$( '#hub-meta-last-submission, #hub-meta-last-edit' ).text( str );
} );
} );
}
// ββ Card rendering ββββββββββββββββββββββββββββββββββββββββββββββββββββββ
function esc( s ) { return mw.html.escape( s || '' ); }
function fileUrl( f ) {
return f ? mw.config.get( 'wgServer' ) + '/wiki/Special:FilePath/' + encodeURIComponent( f ) : '';
}
function buildCard( row ) {
var page = row.pagename || '';
var name = row.name || page.replace( /^.*\//, '' );
var url = mw.util.getUrl( page );
var img = row.image
? '<img src="' + esc( fileUrl( row.image ) ) + '" alt="' + esc( name ) + '" loading="lazy">'
: '<div class="tutorial-card-image-fallback"></div>';
var tags = '';
( row.tags || '' ).split( ',' ).forEach( function ( t ) {
t = t.trim();
if ( t ) {
tags += '<span class="tutorial-card-tag js-tutorial-tag" data-tag="' +
esc( t ) + '">' + esc( t ) + '</span>';
}
} );
return $( [
'<div class="tutorial-card-wrap">',
'<div class="tutorial-card">',
'<div class="tutorial-card-image">', img, '</div>',
'<div class="tutorial-card-body">',
'<div class="tutorial-card-title"><a href="', url, '">', esc( name ), '</a></div>',
tags ? '<div class="tutorial-card-meta">' + tags + '</div>' : '',
'<div class="tutorial-card-stats">',
'<span class="tutorial-card-contributors">\u2014</span>',
'<span class="tutorial-card-updated">\u2014</span>',
'</div>',
'</div>',
row.description
? '<div class="tutorial-card-description">' + esc( row.description ) + '</div>'
: '',
'</div>',
'</div>'
].join( '' ) );
}
function enrichCards( batch ) {
if ( !batch.length ) { return; }
var api = new mw.Api();
for ( var i = 0; i < batch.length; i += 20 ) {
( function ( chunk ) {
api.get( {
action: 'query',
titles: chunk.map( function ( c ) { return c.page; } ).join( '|' ),
prop: 'revisions|contributors',
rvprop: 'timestamp', rvlimit: 1, pclimit: 20,
format: 'json'
} ).done( function ( data ) {
if ( !data.query ) { return; }
$.each( data.query.pages || {}, function ( _, page ) {
if ( page.missing !== undefined ) { return; }
var match = chunk.filter( function ( c ) { return c.page === page.title; } );
if ( !match.length ) { return; }
var $el = match[ 0 ].$el;
var ts = ( page.revisions && page.revisions[ 0 ] && page.revisions[ 0 ].timestamp ) || '';
var n = ( page.contributors ? page.contributors.length : 0 ) + ( page.anoncontributors || 0 );
var d = ts ? new Date( ts ).toLocaleDateString( 'en-GB', { year: 'numeric', month: 'short' } ) : '\u2014';
$el.find( '.tutorial-card-contributors' ).text( n + ( n === 1 ? ' contributor' : ' contributors' ) );
$el.find( '.tutorial-card-updated' ).text( 'Edited ' + d );
} );
} );
} )( batch.slice( i, i + 20 ) );
}
}
} );
} () );