MediaWiki:Gadget-readerchrome.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.
/**
* readerChrome.js β site-wide reader chrome (NOT part of the editor).
*
* Provides two always-available reader aids that used to live inside the
* Editor extension's siteNav.js:
* β’ [π] document navigator (bottom-right FAB + panel)
* β’ [β] scroll-to-top (appears after 300px scroll)
*
* Why it's separate from the editor: these are reading aids, useful on every
* page for every visitor β they have nothing to do with editing. Keeping them
* in the editor module meant they only made sense in edit context and leaked
* onto all pages awkwardly. This module owns them, loads site-wide, and the
* editor no longer touches them.
*
* Docslist is DERIVED from the JSON: namespace (NS 3000): every document has a
* JSON: page, so listing that namespace is the authoritative, self-maintaining
* document list. When the editor creates a new document (writes its JSON page),
* it appears here automatically β no hardcoded list to maintain.
*
* Install: add as a Gadget (or load via MediaWiki:Common.js /
* wgResourceModules) so it runs on every page. Requires jQuery + mediawiki.util
* (both default). The FAB image /images/docs.svg is optional (emoji fallback).
*/
( function () {
'use strict';
/* The JSON: namespace id β the authoritative document catalogue. */
var JSON_NS = 3000;
function boot() {
if ( document.getElementById( 'se-docnav' ) ) return;
var FAB_BG = '#c0521a';
var FAB_BG_HOVER = '#a0410e';
var FAB_RIGHT = '20px';
/* ββ Styles (scoped, class-based, no page-CSS dependency) βββββββ */
var css = [
'#se-docnav-btn{',
' position:fixed;bottom:22px;right:' + FAB_RIGHT + ';z-index:9998;',
' width:48px;height:48px;border-radius:50%;',
' background:' + FAB_BG + ';border:none;cursor:pointer;padding:0;',
' box-shadow:0 3px 12px rgba(0,0,0,0.22);',
' display:flex;align-items:center;justify-content:center;',
' transition:background .15s,transform .15s,box-shadow .15s;user-select:none;',
'}',
'#se-docnav-btn:hover{background:' + FAB_BG_HOVER + ';transform:scale(1.07);box-shadow:0 4px 16px rgba(0,0,0,0.3);}',
'#se-docnav-btn .se-fab-img{width:22px;height:22px;filter:brightness(0) invert(1);display:block;}',
'#se-docnav-btn .se-fab-emoji{font-size:20px;line-height:1;}',
'#se-scrolltop{',
' position:fixed;bottom:78px;right:' + FAB_RIGHT + ';z-index:9997;',
' width:48px;height:48px;border-radius:50%;',
' background:' + FAB_BG + ';border:none;cursor:pointer;padding:0;',
' box-shadow:0 3px 12px rgba(0,0,0,0.22);',
' display:flex;align-items:center;justify-content:center;',
' opacity:0;pointer-events:none;',
' transition:opacity .25s,transform .15s,background .15s;transform:translateY(8px);',
'}',
'#se-scrolltop.se-scrolltop-visible{opacity:1;pointer-events:auto;transform:translateY(0);}',
'#se-scrolltop:hover{background:' + FAB_BG_HOVER + ';transform:scale(1.07);}',
'#se-scrolltop svg{pointer-events:none;}',
'#se-docnav-btn .se-tip,#se-scrolltop .se-tip{',
' position:absolute;right:calc(100% + 10px);top:50%;transform:translateY(-50%);',
' white-space:nowrap;background:rgba(30,30,30,.82);color:#fff;',
' font-size:11px;font-family:system-ui,sans-serif;font-weight:500;',
' padding:3px 9px;border-radius:999px;pointer-events:none;opacity:0;transition:opacity .18s;',
'}',
'#se-docnav-btn:hover .se-tip,#se-scrolltop:hover .se-tip{opacity:1;}',
'#se-docnav{',
' position:fixed;bottom:134px;right:' + FAB_RIGHT + ';z-index:9999;',
' width:300px;max-height:calc(100vh - 164px);',
' background:#fff;border-radius:14px;',
' box-shadow:0 8px 32px rgba(0,0,0,0.18),0 1.5px 4px rgba(0,0,0,0.08);',
' border:0.5px solid #e0e0e0;',
' display:none;flex-direction:column;overflow:hidden;',
' font-family:system-ui,sans-serif;transform-origin:bottom right;animation:se-dn-pop .18s ease;',
'}',
'@keyframes se-dn-pop{from{opacity:0;transform:scale(.93) translateY(6px)}to{opacity:1;transform:none}}',
'#se-docnav.se-dn-open{display:flex;}',
'#se-docnav .se-dn-head{display:flex;align-items:center;justify-content:space-between;padding:13px 16px 10px;border-bottom:.5px solid #eee;flex-shrink:0;}',
'#se-docnav .se-dn-head-title{font-size:13px;font-weight:700;color:#1a2e40;letter-spacing:.01em;}',
'#se-docnav .se-dn-close{background:none;border:none;cursor:pointer;padding:2px 5px;font-size:16px;color:#999;line-height:1;border-radius:4px;transition:background .12s,color .12s;}',
'#se-docnav .se-dn-close:hover{background:#f1f3f4;color:#333;}',
'#se-docnav .se-dn-search{padding:9px 12px;border-bottom:.5px solid #eee;flex-shrink:0;}',
'#se-docnav .se-dn-search input{width:100%;padding:6px 10px;border:.5px solid #ddd;border-radius:8px;font-size:13px;font-family:inherit;color:#333;outline:none;box-sizing:border-box;transition:border-color .15s;}',
'#se-docnav .se-dn-search input:focus{border-color:' + FAB_BG + ';}',
'#se-docnav .se-dn-list{overflow-y:auto;flex:1;padding:6px 0;}',
'#se-docnav .se-dn-item{display:flex;align-items:center;gap:10px;padding:9px 16px;cursor:pointer;font-size:13.5px;color:#222;border-left:3px solid transparent;transition:background .1s,border-color .1s;text-decoration:none;}',
'#se-docnav .se-dn-item:hover{background:#fdf3ed;border-left-color:' + FAB_BG + ';}',
'#se-docnav .se-dn-item.se-dn-current{background:#fdf3ed;border-left-color:' + FAB_BG + ';font-weight:600;color:' + FAB_BG + ';}',
'#se-docnav .se-dn-icon{font-size:15px;flex-shrink:0;opacity:.7;}',
'#se-docnav .se-dn-name{flex:1;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;}',
'#se-docnav .se-dn-status{padding:18px 16px;text-align:center;color:#999;font-size:13px;}',
'#se-docnav .se-dn-footer{border-top:.5px solid #eee;flex-shrink:0;}',
'#se-docnav .se-dn-footer a{display:flex;align-items:center;gap:8px;padding:10px 16px;font-size:12.5px;color:#666;text-decoration:none;transition:background .12s,color .12s;}',
'#se-docnav .se-dn-footer a:hover{background:#fdf3ed;color:' + FAB_BG + ';}',
'@media(max-width:768px){',
' #se-docnav-btn{bottom:18px;right:16px;width:44px;height:44px;}',
' #se-scrolltop{bottom:74px;right:16px;width:44px;height:44px;}',
' #se-docnav{right:16px;width:calc(100vw - 32px);bottom:128px;}',
'}',
'@media print{#se-docnav,#se-docnav-btn,#se-scrolltop{display:none!important}}'
].join( '' );
var styleEl = document.createElement( 'style' );
styleEl.id = 'se-readerchrome-css';
styleEl.textContent = css;
document.head.appendChild( styleEl );
/* ββ Build DOM βββββββββββββββββββββββββββββββββββββββββ */
var mainPage = ( mw.config.get( 'wgArticlePath' ) || '/wiki/$1' )
.replace( '$1', encodeURIComponent( mw.config.get( 'wgMainPageTitle' ) || 'Main_Page' ) );
var currentTitle = mw.config.get( 'wgPageName' ) || '';
var currentRoot = currentTitle.split( '/' )[ 0 ];
var panel = document.createElement( 'div' );
panel.id = 'se-docnav';
panel.innerHTML = [
'<div class="se-dn-head">',
' <span class="se-dn-head-title">π Documents</span>',
' <button class="se-dn-close" title="Close">β</button>',
'</div>',
'<div class="se-dn-search"><input type="text" placeholder="Search documentsβ¦" id="se-dn-input" autocomplete="off"></div>',
'<div class="se-dn-list" id="se-dn-list"><div class="se-dn-status">Loadingβ¦</div></div>',
'<div class="se-dn-footer"><a href="' + mainPage + '">β Go to Main Page</a></div>'
].join( '' );
var triggerBtn = document.createElement( 'button' );
triggerBtn.id = 'se-docnav-btn';
triggerBtn.title = 'Browse documents';
triggerBtn.setAttribute( 'aria-label', 'Browse documents' );
triggerBtn.innerHTML =
'<img class="se-fab-img" src="/images/docs.svg" alt="" aria-hidden="true" ' +
' onerror="this.outerHTML=\'<span class="se-fab-emoji" aria-hidden="true">π</span>\'">' +
'<span class="se-tip">Browse documents</span>';
var scrollBtn = document.createElement( 'button' );
scrollBtn.id = 'se-scrolltop';
scrollBtn.title = 'Back to top';
scrollBtn.setAttribute( 'aria-label', 'Scroll to top' );
scrollBtn.innerHTML =
'<svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg" aria-hidden="true">' +
' <path d="M10 3.5L3.5 11H7.5V16.5H12.5V11H16.5L10 3.5Z" fill="white"/>' +
'</svg>' +
'<span class="se-tip">Back to top</span>';
document.body.appendChild( panel );
document.body.appendChild( triggerBtn );
document.body.appendChild( scrollBtn );
/* ββ Docs logic β DERIVED from the JSON: namespace βββββ */
var allDocs = [], loaded = false, open = false;
function artPath( title ) {
return ( mw.config.get( 'wgArticlePath' ) || '/wiki/$1' ).replace( '$1', title.replace( / /g, '_' ) );
}
function renderList( filter ) {
var list = document.getElementById( 'se-dn-list' );
var q = ( filter || '' ).toLowerCase().trim();
var shown = q ? allDocs.filter( function ( d ) {
return d.title.toLowerCase().indexOf( q ) !== -1 ||
( d.display && d.display.toLowerCase().indexOf( q ) !== -1 );
} ) : allDocs;
if ( !shown.length ) {
list.innerHTML = '<div class="se-dn-status">' +
( q ? 'No results for "' + q + '"' : 'No documents found.' ) + '</div>';
return;
}
var html = '';
shown.forEach( function ( d ) {
var cur = d.title === currentRoot;
html += '<a class="se-dn-item' + ( cur ? ' se-dn-current' : '' ) + '" href="' + d.href + '" title="' + d.title + '">' +
'<span class="se-dn-icon">π</span>' +
'<span class="se-dn-name">' + d.display + '</span>' +
( cur ? '<span style="font-size:11px;color:' + FAB_BG + ';flex-shrink:0">β here</span>' : '' ) +
'</a>';
} );
list.innerHTML = html;
}
function loadDocs() {
if ( loaded ) return;
loaded = true;
var accumulated = [];
function fetchBatch( apcontinue ) {
/* List the JSON: namespace β the authoritative doc catalogue.
Each JSON: page title IS a document slug, so new docs created
by the editor appear here automatically. */
var params = {
action: 'query', list: 'allpages',
apnamespace: JSON_NS, aplimit: 'max', format: 'json'
};
if ( apcontinue ) params.apcontinue = apcontinue;
$.ajax( { url: mw.util.wikiScript( 'api' ), data: params, dataType: 'json' } )
.then( function ( data ) {
var pages = ( data.query && data.query.allpages ) || [];
pages.forEach( function ( p ) {
/* p.title is like "JSON:Anuvyakhyana" β strip the ns
prefix to get the reader page slug. Only top-level
docs (no subpage slash after the slug). */
var slug = p.title.replace( /^[^:]+:/, '' );
if ( slug.indexOf( '/' ) === -1 ) {
accumulated.push( {
title: slug,
display: slug.replace( /_/g, ' ' ),
href: artPath( slug )
} );
}
} );
if ( data.continue && data.continue.apcontinue ) {
fetchBatch( data.continue.apcontinue );
} else {
accumulated.sort( function ( a, b ) { return a.title.localeCompare( b.title ); } );
allDocs = accumulated;
renderList( '' );
}
} ).fail( function () {
var list = document.getElementById( 'se-dn-list' );
if ( list ) list.innerHTML = '<div class="se-dn-status">Could not load documents.</div>';
} );
}
fetchBatch( null );
}
function openPanel() {
open = true; panel.classList.add( 'se-dn-open' ); loadDocs();
setTimeout( function () { var i = document.getElementById( 'se-dn-input' ); if ( i ) i.focus(); }, 60 );
}
function closePanel() { open = false; panel.classList.remove( 'se-dn-open' ); }
triggerBtn.addEventListener( 'click', function ( e ) { e.stopPropagation(); open ? closePanel() : openPanel(); } );
panel.querySelector( '.se-dn-close' ).addEventListener( 'click', closePanel );
document.addEventListener( 'click', function ( e ) { if ( open && !panel.contains( e.target ) && e.target !== triggerBtn ) closePanel(); } );
document.addEventListener( 'input', function ( e ) { if ( e.target && e.target.id === 'se-dn-input' ) renderList( e.target.value ); } );
document.addEventListener( 'keydown', function ( e ) { if ( open && ( e.key === 'Escape' || e.keyCode === 27 ) ) closePanel(); } );
/* ββ Scroll-to-top (window + Minerva #mw-mf-viewport) ββ */
var _scrollRaf = null;
function updateScrollBtn( y ) { scrollBtn.classList.toggle( 'se-scrolltop-visible', y > 300 ); }
function onWindowScroll() {
if ( _scrollRaf ) return;
_scrollRaf = requestAnimationFrame( function () {
_scrollRaf = null;
updateScrollBtn( window.scrollY || window.pageYOffset || 0 );
} );
}
function onViewportScroll() {
if ( _scrollRaf ) return;
_scrollRaf = requestAnimationFrame( function () {
_scrollRaf = null;
var vp = document.getElementById( 'mw-mf-viewport' );
updateScrollBtn( vp ? vp.scrollTop : 0 );
} );
}
scrollBtn.addEventListener( 'click', function () {
var vp = document.getElementById( 'mw-mf-viewport' );
if ( vp && vp.scrollTop > 0 ) { vp.scrollTo( { top: 0, behavior: 'smooth' } ); }
else { window.scrollTo( { top: 0, behavior: 'smooth' } ); }
} );
window.addEventListener( 'scroll', onWindowScroll, { passive: true } );
var _vpEl = document.getElementById( 'mw-mf-viewport' );
if ( _vpEl ) {
_vpEl.addEventListener( 'scroll', onViewportScroll, { passive: true } );
} else {
setTimeout( function () {
var vp = document.getElementById( 'mw-mf-viewport' );
if ( vp ) vp.addEventListener( 'scroll', onViewportScroll, { passive: true } );
}, 500 );
}
onWindowScroll();
onViewportScroll();
}
if ( window.mw && mw.hook ) {
mw.hook( 'wikipage.content' ).add( boot );
} else if ( document.readyState === 'loading' ) {
document.addEventListener( 'DOMContentLoaded', boot );
} else {
boot();
}
}() );