Β |
Β |
| Line 1: |
Line 1: |
| /** | | /** |
| Β * readerChrome.js β site-wide reader chrome (NOT part of the editor). | | Β * readerLoader.js β brief themed loading overlay for content pages. |
| Β * | | Β * |
| Β * Provides two always-available reader aids that used to live inside the | | Β * Replaces the "blank visibility:hidden" gap (used to hide the Devanagari |
| Β * Editor extension's siteNav.js: | | Β * flash-then-transliterate flip) with an intentional-looking loader. The |
| Β * Β β’ [π] document navigator (bottom-right FAB + panel) | | Β * overlay covers only the content area and fades out the moment the page is |
| Β * Β β’ [β]Β scroll-to-topΒ Β Β (appears after 300px scroll) | | Β * "ready" β either when the first transliteration pass has run, or after a |
| | Β * short safety timeout so it can never get stuck. |
| Β * | | Β * |
| Β * Why it's separate from the editor: these are reading aids, useful on every | | Β * How it coordinates with transliteration: |
| Β * page for every visitor β they have nothing to do with editing. Keeping them | | Β * Β β’ This script adds class `gr-loading` to <html> immediately (before paint). |
| Β * in the editor module meant they only made sense in edit context and leaked | | Β * Β Β CSS then hides content and shows the overlay. |
| Β * onto all pages awkwardly. This module owns them, loads site-wide, and the | | Β * Β β’ When common.js finishes its first applyScript() (or right away if the |
| Β * editor no longer touches them. | | *Β Β saved script is 'deva'), it must callΒ window.grReaderReady()Β β that |
| | *Β Β removes `gr-loading` and adds `gr-ready`, fading the overlay out and |
| | Β * Β Β revealing content. |
| | *Β β’ If grReaderReady() is never called (JS error, etc.), a 2.5s failsafe |
| | *Β Β reveals the page anyway β the reader is never left staring at a loader. |
| Β * | | Β * |
| * Docslist is DERIVED from the JSON: namespace (NS 3000): every document has a
| | Β * Install: load early (top of MediaWiki:Common.js, or a Gadget with |
| * JSON: page, so listing that namespace is the authoritative, self-maintaining
| | Β * "type":"general" so it's in <head>). It self-limits to content article |
| * document list. When the editor creates a new document (writes its JSON page),
| | Β * pages and skips Main Page / Special pages. |
| * 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 () { | | ( function () { |
| 'use strict'; | | 'use strict'; |
|
| |
|
| /* The JSON: namespace id β the authoritative document catalogue. */ | | /* Only show the loader on real content article pages. */ |
| var JSON_NS = 3000; | | function isContentPage() { |
| | try { |
| | if ( !window.mw || !mw.config ) return false; |
| | var ns = mw.config.get( 'wgNamespaceNumber' ); |
| | var pn = mw.config.get( 'wgPageName' ) || ''; |
| | if ( ns !== 0 ) return false;Β Β Β Β Β Β Β Β // main namespace only |
| | if ( pn === ( mw.config.get( 'wgMainPageTitle' ) || 'Main_Page' ) ) return false; |
| | if ( pn === 'Main_Page' || pn === 'Welcome' ) return false; |
| | return true; |
| | } catch ( e ) { return false; } |
| | } |
|
| |
|
| function boot() { | | if ( !isContentPage() ) return; |
| if ( document.getElementById( 'se-docnav' ) ) return;
| |
|
| |
|
| var FAB_BGΒ Β Β = '#c0521a';
| | var root = document.documentElement; |
| var FAB_BG_HOVER = '#a0410e';
| | root.classList.add( 'gr-loading' ); |
| var FAB_RIGHTΒ Β = '20px';
| |
|
| |
|
| /* ββ Styles (scoped, class-based, no page-CSS dependency) βββββββ */
| | /* ββ Styles βββββββββββββββββββββββββββββββββββββββββββββββββββββββ */ |
| var css = [
| | var css = [ |
| '#se-docnav-btn{',
| | /* Hide the parsed content while loading, but keep layout height so the |
| 'Β position:fixed;bottom:22px;right:' + FAB_RIGHT + ';z-index:9998;',
| | Β page doesn't jump when it reveals. */ |
| 'Β width:48px;height:48px;border-radius:50%;',
| | 'html.gr-loading .mw-parser-output{visibility:hidden;}', |
| 'Β background:' + FAB_BG + ';border:none;cursor:pointer;padding:0;',
| | 'html.gr-readyΒ .mw-parser-output{visibility:visible;}', |
| 'Β 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{',
| | /* Overlay sits over the content column only (not the header/toolbar). */ |
| 'Β position:fixed;bottom:78px;right:' + FAB_RIGHT + ';z-index:9997;',
| | '#gr-reader-loader{', |
| 'Β width:48px;height:48px;border-radius:50%;',
| | 'Β position:absolute;left:0;right:0;top:0;', |
| 'Β background:' + FAB_BG + ';border:none;cursor:pointer;padding:0;',
| | 'Β min-height:60vh;height:100%;', |
| 'Β box-shadow:0 3px 12px rgba(0,0,0,0.22);',
| | 'Β display:flex;flex-direction:column;align-items:center;justify-content:center;', |
| 'Β display:flex;align-items:center;justify-content:center;',
| | 'Β gap:16px;z-index:50;pointer-events:none;', |
| 'Β opacity:0;pointer-events:none;',
| | 'Β background:transparent;', |
| 'Β transition:opacity .25s,transform .15s,background .15s;transform:translateY(8px);',
| | ' transition:opacity .3s ease;', |
| '}',
| | '}', |
| '#se-scrolltop.se-scrolltop-visible{opacity:1;pointer-events:auto;transform:translateY(0);}',
| | 'html.gr-ready #gr-reader-loader{opacity: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{',
| | /* Spinner β a rotating ring in the site terracotta. */ |
| 'Β position:absolute;right:calc(100% + 10px);top:50%;transform:translateY(-50%);',
| | '#gr-reader-loader .gr-rl-ring{', |
| 'Β white-space:nowrap;background:rgba(30,30,30,.82);color:#fff;',
| | 'Β width:38px;height:38px;border-radius:50%;', |
| 'Β font-size:11px;font-family:system-ui,sans-serif;font-weight:500;',
| | 'Β border:3px solid rgba(181,69,27,0.18);', |
| 'Β padding:3px 9px;border-radius:999px;pointer-events:none;opacity:0;transition:opacity .18s;',
| | 'Β border-top-color:#b5451b;', |
| '}',
| | 'Β animation:gr-rl-spin .8s linear infinite;', |
| '#se-docnav-btn:hover .se-tip,#se-scrolltop:hover .se-tip{opacity:1;}',
| | '}', |
| | '@keyframes gr-rl-spin{to{transform:rotate(360deg);}}', |
|
| |
|
| '#se-docnav{',
| | /* Label β quiet, manuscript-toned. */ |
| 'Β position:fixed;bottom:134px;right:' + FAB_RIGHT + ';z-index:9999;',
| | '#gr-reader-loader .gr-rl-label{', |
| ' width:300px;max-height:calc(100vh - 164px);',
| | 'Β font-family:"Adishila","Noto Serif Devanagari",system-ui,serif;', |
| 'Β background:#fff;border-radius:14px;',
| | ' font-size:15px;color:#a07040;letter-spacing:.02em;', |
| 'Β 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){',
| | /* Reduced-motion: no spin, just a gentle pulse. */ |
| 'Β #se-docnav-btn{bottom:18px;right:16px;width:44px;height:44px;}',
| | '@media (prefers-reduced-motion: reduce){', |
| 'Β #se-scrolltop{bottom:74px;right:16px;width:44px;height:44px;}',
| | 'Β #gr-reader-loader .gr-rl-ring{animation:gr-rl-pulse 1.2s ease-in-out infinite;border-top-color:rgba(181,69,27,0.18);}', |
| 'Β #se-docnav{right:16px;width:calc(100vw - 32px);bottom:128px;}',
| | 'Β @keyframes gr-rl-pulse{0%,100%{opacity:.4}50%{opacity:1}}', |
| '}',
| | '}' |
| '@media print{#se-docnav,#se-docnav-btn,#se-scrolltop{display:none!important}}'
| | ].join( '' ); |
| ].join( '' );
| |
|
| |
|
| var styleEl = document.createElement( 'style' );
| | var style = document.createElement( 'style' ); |
| styleEl.id = 'se-readerchrome-css';
| | style.id = 'gr-reader-loader-css'; |
| styleEl.textContent = css;
| | style.textContent = css; |
| document.head.appendChild( styleEl );
| | ( document.head || document.documentElement ).appendChild( style ); |
|
| |
|
| /* ββ Build DOM βββββββββββββββββββββββββββββββββββββββββ */
| | /* ββ Overlay element (inserted into the content container) ββββββββ */ |
| var mainPage = ( mw.config.get( 'wgArticlePath' ) || '/wiki/$1' ) | | function mountOverlay() { |
| .replace( '$1', encodeURIComponent( mw.config.get( 'wgMainPageTitle' ) || 'Main_Page' ) ); | | if ( document.getElementById( 'gr-reader-loader' ) ) return; |
| var currentTitle = mw.config.get( 'wgPageName' ) || ''; | | var host = document.getElementById( 'mw-content-text' ) || |
| var currentRootΒ = currentTitle.split( '/' )[ 0 ]; | | document.querySelector( '.mw-body-content' ) || |
| | document.body; |
| | if ( !host ) return; |
| | /* host needs positioning context so the absolute overlay covers it */ |
| | var pos = window.getComputedStyle( host ).position; |
| | if ( pos === 'static' ) host.style.position = 'relative'; |
|
| |
|
| var panel = document.createElement( 'div' ); | | var ov = document.createElement( 'div' ); |
| panel.id = 'se-docnav'; | | ov.id = 'gr-reader-loader'; |
| panel.innerHTML = [ | | ov.setAttribute( 'aria-hidden', 'true' ); |
| '<div class="se-dn-head">',
| | ov.innerHTML = |
| ' <span class="se-dn-head-title">π Documents</span>',
| | '<div class="gr-rl-ring"></div>' + |
| 'Β <button class="se-dn-close" title="Close">β</button>',
| | '<div class="gr-rl-label">ΰ€²ΰ₯ΰ€‘ΰ₯ΰ€―ΰ€€ΰ₯β¦</div>';Β /* "loadingβ¦" in Sanskrit */ |
| '</div>',
| | host.appendChild( ov ); |
| '<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' );
| | if ( document.readyState === 'loading' ) { |
| triggerBtn.idΒ Β = 'se-docnav-btn';
| | document.addEventListener( 'DOMContentLoaded', mountOverlay ); |
| triggerBtn.title = 'Browse documents';
| | } else { |
| triggerBtn.setAttribute( 'aria-label', 'Browse documents' ); | | mountOverlay(); |
| 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' );
| | /* ββ Reveal logic βββββββββββββββββββββββββββββββββββββββββββββββββ */ |
| scrollBtn.idΒ Β = 'se-scrolltop';
| | var revealed = false; |
| scrollBtn.title = 'Back to top';
| | function reveal() { |
| scrollBtn.setAttribute( 'aria-label', 'Scroll to top' );
| | if ( revealed ) return; |
| scrollBtn.innerHTML =
| | revealed = true; |
| '<svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg" aria-hidden="true">' +
| | root.classList.remove( 'gr-loading' ); |
| 'Β <path d="M10 3.5L3.5 11H7.5V16.5H12.5V11H16.5L10 3.5Z" fill="white"/>' +
| | root.classList.add( 'gr-ready' ); |
| '</svg>' +
| | /* Remove the overlay node after the fade so it leaves no trace. */ |
| '<span class="se-tip">Back to top</span>';
| | setTimeout( function () { |
| Β | | var ov = document.getElementById( 'gr-reader-loader' ); |
| document.body.appendChild( panel );
| | if ( ov && ov.parentNode ) ov.parentNode.removeChild( ov ); |
| document.body.appendChild( triggerBtn );
| | }, 320 ); |
| 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) ββ */
| | /* common.js calls this after its first applyScript(). */ |
| var _scrollRaf = null;
| | window.grReaderReady = reveal; |
| 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 ) { | | /* Failsafe: never leave the loader up longer than 2.5s, even if the |
| mw.hook( 'wikipage.content' ).add( boot );
| | Β ready signal never arrives (JS error, missing hook, etc.). */ |
| } else if ( document.readyState === 'loading' ) { | | setTimeout( reveal, 2500 ); |
| document.addEventListener( 'DOMContentLoaded', boot );
| |
| } else {
| |
| boot();
| |
| }
| |
|
| |
|
| }() ); | | }() ); |