MediaWiki:Gadget-Navigation.js
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.
const Navigation = {
init( $content ) {
$content.find( '.template-navigation-header' ).on( 'click', Navigation.toggle );
$content.find( '.template-navigation-list' ).each( Navigation.trimList );
$content.find( '.template-navigation-pages' ).each( Navigation.makePages );
$content.find( '.template-navigation-download > .cdx-button' ).on( 'click', Navigation.toggleDownloads );
$content.find( '.template-navigation-download table .cdx-button' ).on( 'click', Navigation.downloadFile );
$content.find( '.template-navigation-audio' ).each( Navigation.makeAudioButtons );
},
toggle( event ) {
if ( event.target.tagName === 'A' ) {
return;
}
$( this ).siblings( '.template-navigation-content' ).toggle();
},
/**
* Trim lists of more than 15 items
* Example: [[NREMT Skillset]]
*/
trimList() {
const $button = $( '<span>more</span>' ).css( 'cursor', 'pointer' ).on( 'click', () => $button.hide().siblings().show() );
$( this ).find( 'ul, ol' ).each( ( index, list ) => {
const $items = $( list ).children( 'li' );
if ( $items.length > 15 ) {
$items.eq( 10 ).nextAll().hide().last().after( $button );
}
} );
},
toggleDownloads() {
const table = this.nextElementSibling;
if ( window.getComputedStyle( table ).display === 'none' ) {
table.style.display = 'table';
} else {
table.style.display = '';
}
},
/**
* Get the file behind scenes and then download it
* In the case of booklets, turn the PDF into a booklet before downloading
*/
async downloadFile() {
// Disable the button to prevent further clicks
const button = this;
button.style.pointerEvents = 'none';
button.classList.remove( 'cdx-button--fake-button--enabled' );
button.classList.add( 'cdx-button--fake-button--disabled' );
const buttonText = button.textContent; // Save the text to restore it later
button.textContent = 'Generating...';
// Get the basic data
const $navigation = $( button ).closest( '.template-navigation' );
const title = $navigation.find( '.template-navigation-title' ).text();
const subtitle = $navigation.find( '.template-navigation-subtitle' ).text();
// Figure out the absolute path to the logo
let logo = '';
const $logo = $navigation.find( '.template-navigation-logo' );
if ( $logo.length ) {
logo = 'https://www.appropedia.org' + $logo.find( 'img' ).attr( 'src' );
}
// Figure out the main page
let mainpage = '';
const $titleLink = $navigation.find( '.template-navigation-title a' );
if ( $titleLink.length ) {
if ( $titleLink.hasClass( 'selflink' ) ) {
mainpage = mw.config.get( 'wgPageName' );
} else {
mainpage = $titleLink.attr( 'title' );
}
}
// Figure out the pages
const pages = [];
const $links = Navigation.getValidLinks( $navigation );
$links.each( ( i, link ) => {
const page = link.classList.contains( 'selflink' ) ? mw.config.get( 'wgPageName' ) : link.title;
pages.push( page );
} );
// Make the download URL
let script, params;
if ( button.classList.contains( 'zim' ) ) {
script = 'generateZIM';
params = new URLSearchParams( {
pages: pages.join( ',' ),
title: title,
description: subtitle,
icon: logo,
} );
} else {
script = 'generatePDF';
params = new URLSearchParams( {
pages: pages.join( ',' ),
title: title,
subtitle: subtitle,
logo: logo,
qrpage: mainpage
} );
}
const url = 'https://www.appropedia.org/scripts/' + script + '.php?' + params.toString();
// Get the file
const result = await fetch( url );
const bytes = await result.arrayBuffer();
const type = result.headers.get( 'Content-Type' );
const contentDisposition = result.headers.get( 'Content-Disposition' );
const matches = contentDisposition.match( /filename *= *([^;]+)/ );
const name = matches[1];
let file = { bytes: bytes, type: type, name: name };
// Convert the file into a booklet
if ( button.classList.contains( 'booklet' ) ) {
file = await Navigation.makeBooklet( file );
}
// Download the file
const blob = new Blob( [ file.bytes ], { type: file.type } );
const href = URL.createObjectURL( blob );
const a = document.createElement( 'a' );
a.href = href;
a.download = file.name;
document.body.appendChild( a );
a.click();
document.body.removeChild( a );
URL.revokeObjectURL( href );
// Re-enable the button
button.style.pointerEvents = '';
button.textContent = buttonText;
button.classList.remove( 'cdx-button--fake-button--disabled' );
button.classList.add( 'cdx-button--fake-button--enabled' );
},
/**
* This method transforms a PDF into a booklet
* The code is based on bookletize.js by Jeffrey Yoo Warren (https://jywarren.github.io/bookletize.js)
* which uses PDFLib.js (https://pdf-lib.js.org)
*/
/* global PDFLib */
async makeBooklet( file ) {
await mw.loader.getScript( 'https://cdnjs.cloudflare.com/ajax/libs/pdf-lib/1.17.1/pdf-lib.min.js' );
const bookletDoc = await PDFLib.PDFDocument.create();
const fileDoc = await PDFLib.PDFDocument.load( file.bytes );
const { width, height } = await fileDoc.getPages()[0].getSize();
// Pad the file PDF to a multiple of 4 by adding empty pages
let pageCount = await fileDoc.getPageCount();
while ( pageCount % 4 ) {
let page = fileDoc.addPage( [ width, height ] );
page.drawText( '' ); // The page needs to have "some text" or bookletDoc.save() fails
pageCount++;
}
// Iterate through, plucking out 4 pages at a time and inserting into new sheet
const filePages = await fileDoc.getPages();
for ( let p = 0; p < pageCount; p += 4 ) {
// Double width, same height
const bookletPage = bookletDoc.addPage( [ width * 2, height ] );
const bookletPage2 = bookletDoc.addPage( [ width * 2, height ] );
// If the file is more than 16 pages long, we make a perfect binding, else a saddle stitch
if ( pageCount > 16 ) {
await Navigation.getPage( filePages.length - 1, { x: 0, y: 0 }, filePages, bookletDoc, bookletPage );
await Navigation.getPage( 0, { x: width, y: 0 }, filePages, bookletDoc, bookletPage );
await Navigation.getPage( 0, { x: 0, y: 0 }, filePages, bookletDoc, bookletPage2 );
await Navigation.getPage( filePages.length - 1, { x: width, y: 0 }, filePages, bookletDoc, bookletPage2 );
} else {
await Navigation.getPage( 3, { x: 0, y: 0 }, filePages, bookletDoc, bookletPage );
await Navigation.getPage( 0, { x: width, y: 0 }, filePages, bookletDoc, bookletPage );
await Navigation.getPage( 0, { x: 0, y: 0 }, filePages, bookletDoc, bookletPage2 );
await Navigation.getPage( 0, { x: width, y: 0 }, filePages, bookletDoc, bookletPage2 );
}
}
const bookletBytes = await bookletDoc.save();
return { bytes: bookletBytes, type: file.type, name: file.name };
},
/**
* Helper method to fetch the next page from the file stack and remove it
*/
async getPage( filePosition, placement, filePages, _bookletDoc, _bookletPage ) {
if ( filePages.length > 0 ) {
var embeddedPage = await _bookletDoc.embedPage( filePages.splice( filePosition, 1 )[0] );
_bookletPage.drawPage( embeddedPage, placement );
}
},
makeAudioButtons() {
// Make the play button
const playButtonTitle = '<title>Play</title>';
const playButtonPath = '<path fill-rule="evenodd" d="M424.4 214.7L72.4 6.6C43.8-10.3 0 6.1 0 47.9V464c0 37.5 40.7 60.1 72.4 41.3l352-208c31.4-18.5 31.5-64.1 0-82.6z"></path>';
const playButtonSVG = '<svg width="40" height="40" viewBox="0 0 448 512">' + playButtonTitle + playButtonPath + '</svg>';
const $playButton = $( '<span class="template-navigation-audio-button template-navigation-audio-button-play">' + playButtonSVG + '</span>' );
$playButton.on( 'click', Navigation.readAloud );
// Make the pause button
const pauseButtonTitle = '<title>Pause</title>';
const pauseButtonPath = '<path fill-rule="evenodd" d="M144 479H48c-26.5 0-48-21.5-48-48V79c0-26.5 21.5-48 48-48h96c26.5 0 48 21.5 48 48v352c0 26.5-21.5 48-48 48zm304-48V79c0-26.5-21.5-48-48-48h-96c-26.5 0-48 21.5-48 48v352c0 26.5 21.5 48 48 48h96c26.5 0 48-21.5 48-48z"></path>';
const pauseButtonSVG = '<svg width="40" height="40" viewBox="0 0 448 512">' + pauseButtonTitle + pauseButtonPath + '</svg>';
const $pauseButton = $( '<span class="template-navigation-audio-button template-navigation-audio-button-pause">' + pauseButtonSVG + '</span>' ).hide();
$pauseButton.on( 'click', Navigation.pauseReadAloud );
// Make the back button
const backButtonTitle = '<title>Back</title>';
const backButtonPath = '<path fill-rule="evenodd" d="M64 468V44c0-6.6 5.4-12 12-12h48c6.6 0 12 5.4 12 12v176.4l195.5-181C352.1 22.3 384 36.6 384 64v384c0 27.4-31.9 41.7-52.5 24.6L136 292.7V468c0 6.6-5.4 12-12 12H76c-6.6 0-12-5.4-12-12z"></path>';
const backButtonSVG = '<svg width="30" height="30" viewBox="0 0 448 512">' + backButtonTitle + backButtonPath + '</svg>';
const $backButton = $( '<span class="template-navigation-audio-button template-navigation-audio-button-back">' + backButtonSVG + '</span>' );
// Make the next button
const nextButtonTitle = '<title>Next</title>';
const nextButtonPath = '<path fill-rule="evenodd" d="M384 44v424c0 6.6-5.4 12-12 12h-48c-6.6 0-12-5.4-12-12V291.6l-195.5 181C95.9 489.7 64 475.4 64 448V64c0-27.4 31.9-41.7 52.5-24.6L312 219.3V44c0-6.6 5.4-12 12-12h48c6.6 0 12 5.4 12 12z"></path>';
const nextButtonSVG = '<svg width="30" height="30" viewBox="0 0 448 512">' + nextButtonTitle + nextButtonPath + '</svg>';
const $nextButton = $( '<span class="template-navigation-audio-button template-navigation-audio-button-next">' + nextButtonSVG + '</span>' );
$( this ).append( $backButton, $playButton, $pauseButton, $nextButton );
},
async readAloud() {
const $playButton = $( this );
const $pauseButton = $playButton.siblings( '.template-navigation-audio-button-pause' );
$playButton.hide();
$pauseButton.show();
if ( window.speechSynthesis.paused ) {
window.speechSynthesis.resume();
return;
}
try {
const title = mw.config.get( 'wgPageName' );
const params = {
action: 'query',
titles: title,
prop: 'extracts',
explaintext: true,
exsectionformat: 'plain',
formatversion: 2
};
const response = await new mw.Api().get( params );
const text = response.query.pages[0].extract;
const utterance = new window.SpeechSynthesisUtterance( text );
window.speechSynthesis.speak( utterance );
} catch ( error ) {
console.log( error );
}
},
pauseReadAloud() {
const $pauseButton = $( this );
const $playButton = $pauseButton.siblings( '.template-navigation-audio-button-play' );
$pauseButton.hide();
$playButton.show();
window.speechSynthesis.pause();
},
makePages() {
const $navigation = $( this ).closest( '.template-navigation' );
const $links = Navigation.getValidLinks( $navigation );
let prevLink, nextLink, index;
$links.each( ( i, link ) => {
if ( link.classList.contains( 'selflink' ) ) {
index = i;
prevLink = $links[ index - 1 ];
nextLink = $links[ index + 1 ];
return;
}
} );
if ( index === undefined ) {
return;
}
let pages = '';
if ( prevLink ) {
const prevButton = document.createElement( 'a' );
prevButton.href = prevLink.href;
prevButton.title = 'Go to the previous page';
prevButton.textContent = '◄';
pages += prevButton.outerHTML;
}
if ( $links.length > 20 ) {
const currentPage = index + 1;
const totalPages = $links.length;
const pagesText = document.createElement( 'span' );
pagesText.classList.add( 'text' );
pagesText.textContent = 'Page ' + currentPage + '/' + totalPages;
pages += pagesText.outerHTML;
} else {
$links.each( ( i, link ) => {
const pageButton = document.createElement( 'a' );
pageButton.textContent = '■';
pageButton.classList.add( 'page' );
if ( link.classList.contains( 'selflink' ) ) {
pageButton.href = mw.util.getUrl();
pageButton.title = mw.config.get( 'wgPageName' );
pageButton.classList.add( 'current' );
} else {
pageButton.href = link.href;
pageButton.title = link.title;
}
pages += pageButton.outerHTML;
} );
}
if ( nextLink ) {
const nextButton = document.createElement( 'a' );
nextButton.href = nextLink.href;
nextButton.title = 'Go to the next page';
nextButton.textContent = '►';
pages += nextButton.outerHTML;
}
$navigation.find( '.template-navigation-pages' ).html( pages );
},
getValidLinks( $navigation ) {
return $navigation.find( '.template-navigation-title a' ).add( '.template-navigation-list a' ).filter( ( index, link ) => {
if ( link.classList.contains( 'external' ) ) {
return false; // Skip external pages
}
if ( link.classList.contains( 'new' ) ) {
return false; // Skip non-existent pages
}
return true;
} );
}
};
mw.hook( 'wikipage.content' ).add( Navigation.init );