MediaWiki:Common.js: Difference between revisions
No edit summary Tags: Manual revert Reverted |
Undo revision 5755 by Chandrashekars (talk) Tag: Undo |
||
| Line 1,242: | Line 1,242: | ||
/* ── Expose storeQueryForLink for readerToolbar to call ── */ | /* ── Expose storeQueryForLink for readerToolbar to call ── */ | ||
window.grStoreSearchHL = storeQueryForLink; | window.grStoreSearchHL = storeQueryForLink; | ||
}() ); | |||
/* ═══════════════════════════════════════════════════════════════ | |||
Mobile addon — paste at bottom of MediaWiki:Common.js | |||
Only runs on Minerva (mobile) skin | |||
═══════════════════════════════════════════════════════════════ */ | |||
( function () { | |||
if ( !document.body.classList.contains( 'skin-minerva' ) ) return; | |||
/* ── 1. CSS injected into <head> — beats all other stylesheets ── */ | |||
function injectCSS() { | |||
if ( document.getElementById( 'gr-mob-css' ) ) return; | |||
var s = document.createElement( 'style' ); | |||
s.id = 'gr-mob-css'; | |||
s.textContent = | |||
/* Body padding — readerToolbar sets this, we clear it */ | |||
'body,#mw-mf-viewport,#mw-mf-page-center{padding-top:0!important;margin-top:0!important;}' + | |||
'html,body,#mw-mf-viewport,#mw-mf-page-center{overflow-x:hidden!important;max-width:100vw!important;}' + | |||
/* Header orange + sticky */ | |||
'header.header-container{background:#b5451b!important;position:sticky!important;top:0!important;z-index:300!important;}' + | |||
'.minerva-header{background:#b5451b!important;min-height:54px!important;}' + | |||
/* Header: keep only hamburger + branding, hide search */ | |||
'.minerva-header .search-toggle,.minerva-header .minerva-user-notifications{display:none!important;}' + | |||
/* Logo row: icon + title wrap cleanly */ | |||
'.branding-box a{display:flex!important;align-items:center!important;' + | |||
'text-decoration:none!important;max-width:calc(100vw - 80px)!important;}' + | |||
'.branding-box a::before{content:"";display:block;width:30px;height:30px;flex-shrink:0;' + | |||
'background:url("/favicon.png") center/contain no-repeat;' + | |||
'margin-right:8px;}' + | |||
'.branding-box a span{color:#fff!important;font-size:16px!important;font-weight:700!important;' + | |||
'font-family:system-ui,sans-serif!important;line-height:1.2!important;' + | |||
'flex:1 1 auto!important;min-width:0!important;}' + | |||
/* Header icons white */ | |||
'.minerva-header svg path,.minerva-header svg rect,.minerva-header svg circle{fill:#fff!important;}' + | |||
'.minerva-header label{color:#fff!important;}' + | |||
/* Hide Page/Discussion tabs + icon toolbar */ | |||
'.minerva-tabs,.mw-portlet-associated-pages,.page-actions-menu,' + | |||
'#page-secondary-actions,.last-modified-bar,.minerva-anon-talk-link{display:none!important;}' + | |||
/* Drawer: hide default items, show ours */ | |||
/* #mw-mf-page-left is kept — we replace its content in JS */ '' + | |||
'#gr-mob-menu-items{display:block!important;}' + | |||
/* Drawer footer: hide About/Disclaimers */ | |||
'.mw-footer.minerva-footer,.footer-places,.footer-info,' + | |||
'.minerva-footer-logo,#footer-places-about,' + | |||
'#footer-places-disclaimers,#footer-places-privacy{display:none!important;}' + | |||
/* ReaderToolbar below header */ | |||
'#gr-static-bar{position:sticky!important;top:54px!important;z-index:200!important;}' + | |||
/* Sections expanded */ | |||
'.mf-section-0,.mf-section-1,.mf-section-2,.mf-section-3,.mf-section-4,' + | |||
'.mf-section-5,.mf-section-6,.mf-section-7,.mf-section-8,.mf-section-9,' + | |||
'.mf-section-10{display:block!important;visibility:visible!important;}' + | |||
'.collapsible-block{display:block!important;}' + | |||
'.section-heading .indicator,.collapsible-heading .indicator{display:none!important;}' + | |||
'.section-heading,.collapsible-heading{pointer-events:none!important;}' + | |||
/* Home grid single column */ | |||
'.gr-home-grid{flex-direction:column!important;flex-wrap:nowrap!important;' + | |||
'gap:12px!important;width:100%!important;}' + | |||
'.gr-home-card{width:100%!important;max-width:100%!important;' + | |||
'min-width:unset!important;box-sizing:border-box!important;flex:none!important;}' + | |||
'.gr-home-toggle{flex-wrap:wrap!important;}' + | |||
/* Content */ | |||
'.mw-parser-output{font-size:18px!important;line-height:1.8!important;}' + | |||
'.mw-parser-output h2,.mw-parser-output h3{width:100%!important;}' + | |||
'.bhashyam-block{margin-left:8px!important;}' + | |||
'#footer,.mw-footer,.catlinks,#catlinks{display:none!important;}' + | |||
/* TOC overlay */ | |||
'.gr-mob-toc-panel{position:fixed!important;top:0!important;left:0!important;' + | |||
'bottom:0!important;width:82vw!important;max-width:340px!important;' + | |||
'background:#fff!important;z-index:10400!important;' + | |||
'box-shadow:4px 0 28px rgba(0,0,0,0.22)!important;overflow-y:auto!important;' + | |||
'padding:0 0 40px!important;transform:translateX(-110%)!important;' + | |||
'transition:transform 0.26s cubic-bezier(0.4,0,0.2,1)!important;display:block!important;}' + | |||
'.gr-mob-toc-panel.open{transform:translateX(0)!important;}' + | |||
'.gr-mob-toc-backdrop{display:none!important;position:fixed!important;inset:0!important;' + | |||
'background:rgba(0,0,0,0.4)!important;z-index:10399!important;}' + | |||
'.gr-mob-toc-backdrop.open{display:block!important;}' + | |||
'.gr-mob-toc-header{position:sticky!important;top:0!important;background:#fff!important;' + | |||
'display:flex!important;align-items:center!important;justify-content:space-between!important;' + | |||
'padding:16px 16px 12px!important;border-bottom:1px solid #f0ebe6!important;z-index:1!important;}' + | |||
'.gr-mob-toc-title{font-size:13px!important;font-weight:700!important;text-transform:uppercase!important;' + | |||
'letter-spacing:0.08em!important;color:#b5451b!important;font-family:system-ui,sans-serif!important;}' + | |||
'.gr-mob-toc-close{background:none!important;border:none!important;' + | |||
'font-size:22px!important;color:#999!important;cursor:pointer!important;padding:4px 8px!important;}' + | |||
'.gr-mob-toc-body{padding:12px 16px!important;}' + | |||
'.gr-mob-toc-body a{display:block!important;font-size:16px!important;line-height:1.6!important;' + | |||
'color:#2c1810!important;text-decoration:none!important;padding:8px 0!important;' + | |||
'border-bottom:1px solid #f5f0ed!important;}'; | |||
document.head.appendChild( s ); | |||
} | |||
/* ── 2. Expand hidden sections ── */ | |||
function expandSections() { | |||
document.querySelectorAll( '[class*="mf-section-"], .collapsible-block' ) | |||
.forEach( function ( el ) { | |||
el.removeAttribute( 'hidden' ); | |||
el.style.setProperty( 'display', 'block', 'important' ); | |||
el.style.setProperty( 'visibility', 'visible', 'important' ); | |||
el.removeAttribute( 'aria-hidden' ); | |||
} ); | |||
document.querySelectorAll( '.section-heading, .collapsible-heading' ) | |||
.forEach( function ( el ) { | |||
el.setAttribute( 'aria-expanded', 'true' ); | |||
el.style.setProperty( 'pointer-events', 'none', 'important' ); | |||
} ); | |||
} | |||
/* Persistent observer — fight MF re-collapsing */ | |||
function watchSections() { | |||
var t = null; | |||
var obs = new MutationObserver( function ( ms ) { | |||
if ( ms.some( function(m){ return m.attributeName === 'hidden'; } ) ) { | |||
clearTimeout(t); t = setTimeout( expandSections, 30 ); | |||
} | |||
} ); | |||
var root = document.querySelector( '#mw-content-text' ) || document.body; | |||
obs.observe( root, { subtree:true, attributes:true, attributeFilter:['hidden','aria-hidden'] } ); | |||
} | |||
/* ── 3. Body padding observer ── */ | |||
function watchBodyPadding() { | |||
new MutationObserver( function () { | |||
if ( document.body.style.paddingTop && document.body.style.paddingTop !== '0px' ) { | |||
document.body.style.paddingTop = ''; | |||
} | |||
} ).observe( document.body, { attributes:true, attributeFilter:['style'] } ); | |||
} | |||
/* ── 4. Inject custom links into drawer ── */ | |||
function injectMenuLinks() { | |||
/* Already injected */ | |||
if ( document.getElementById( 'gr-mob-menu-items' ) ) return; | |||
/* The drawer container is .navigation-drawer (the <nav> element) | |||
Our links go AFTER the #mw-mf-page-left div (which we hide via CSS) */ | |||
var navDrawer = document.querySelector( '.navigation-drawer' ); | |||
if ( !navDrawer ) return; | |||
var wrap = document.createElement( 'div' ); | |||
wrap.id = 'gr-mob-menu-items'; | |||
wrap.style.cssText = 'width:100%;background:#fff;margin-top:8px;'; | |||
var itemStyle = 'display:flex;align-items:center;gap:14px;padding:15px 20px;' + | |||
'font-size:16px;color:#2c1810;text-decoration:none;' + | |||
'font-family:system-ui,sans-serif;border-bottom:1px solid #f0ebe6;background:#fff;'; | |||
function makeItem( href, emoji, label ) { | |||
var a = document.createElement( 'a' ); | |||
a.href = href; | |||
a.style.cssText = itemStyle; | |||
a.innerHTML = | |||
'<span>' + label + '</span>'; | |||
return a; | |||
} | |||
wrap.appendChild( makeItem( '/Main_Page', '', 'Home' ) ); | |||
wrap.appendChild( makeItem( '/My_wiki:Help', '', 'Help' ) ); | |||
wrap.appendChild( makeItem( '/My_wiki:About', '', 'About' ) ); | |||
var userName = window.mw ? mw.config.get( 'wgUserName' ) : null; | |||
if ( userName ) { | |||
var la = document.querySelector( 'a[href*="action=logout"]' ); | |||
wrap.appendChild( makeItem( | |||
la ? la.href : '/index.php?title=Special:UserLogout', '', 'Log out' | |||
) ); | |||
} else { | |||
wrap.appendChild( makeItem( '/index.php?title=Special:UserLogin', '', 'Log in' ) ); | |||
} | |||
/* Replace contents of #mw-mf-page-left — it's inside the | |||
toggle-list mechanism so it only shows when drawer is open */ | |||
var pageLeft = document.getElementById( 'mw-mf-page-left' ); | |||
if ( pageLeft ) { | |||
/* Clear default items and insert ours */ | |||
while ( pageLeft.firstChild ) pageLeft.removeChild( pageLeft.firstChild ); | |||
pageLeft.style.removeProperty( 'display' ); /* un-hide it */ | |||
pageLeft.appendChild( wrap ); | |||
} else { | |||
/* Fallback */ | |||
navDrawer.appendChild( wrap ); | |||
} | |||
/* Hide footer portlets */ | |||
document.querySelectorAll( | |||
'.mw-footer.minerva-footer,.footer-places,.footer-info,' + | |||
'.minerva-footer-logo,#footer-places-about,' + | |||
'#footer-places-disclaimers,#footer-places-privacy' | |||
).forEach( function(el) { el.style.setProperty('display','none','important'); } ); | |||
} | |||
/* ── 5. Mobile TOC overlay ── */ | |||
var _tocDone = false; | |||
function initToc() { | |||
if ( _tocDone ) return; | |||
var tocList = document.querySelector( '.vector-toc-contents, .vector-toc .vector-toc-list' ); | |||
if ( !tocList ) return; | |||
if ( !tocList.querySelector( 'li' ) ) return; | |||
_tocDone = true; | |||
var bd = document.createElement( 'div' ); | |||
bd.className = 'gr-mob-toc-backdrop'; | |||
document.body.appendChild( bd ); | |||
var panel = document.createElement( 'div' ); | |||
panel.className = 'gr-mob-toc-panel'; | |||
var hdr = document.createElement( 'div' ); | |||
hdr.className = 'gr-mob-toc-header'; | |||
var ttl = document.createElement( 'div' ); | |||
ttl.className = 'gr-mob-toc-title'; | |||
ttl.textContent = 'विषयसूची'; | |||
var cls = document.createElement( 'button' ); | |||
cls.className = 'gr-mob-toc-close'; | |||
cls.textContent = '✕'; | |||
hdr.appendChild( ttl ); hdr.appendChild( cls ); | |||
panel.appendChild( hdr ); | |||
var body = document.createElement( 'div' ); | |||
body.className = 'gr-mob-toc-body'; | |||
body.appendChild( tocList.cloneNode( true ) ); | |||
panel.appendChild( body ); | |||
document.body.appendChild( panel ); | |||
var btn = document.createElement( 'button' ); | |||
btn.id = 'gr-mob-toc-btn'; | |||
btn.innerHTML = '☰ Contents'; | |||
btn.style.cssText = 'position:fixed;bottom:148px;left:16px;z-index:9100;' + | |||
'background:#fff;border:1.5px solid #b5451b;border-radius:24px;' + | |||
'padding:10px 16px;font-size:15px;font-family:system-ui,sans-serif;' + | |||
'color:#b5451b;font-weight:600;box-shadow:0 3px 14px rgba(0,0,0,0.15);cursor:pointer;'; | |||
document.body.appendChild( btn ); | |||
function open() { panel.classList.add('open'); bd.classList.add('open'); document.body.style.overflow='hidden'; } | |||
function close() { panel.classList.remove('open'); bd.classList.remove('open'); document.body.style.overflow=''; } | |||
btn.onclick = open; cls.onclick = close; bd.onclick = close; | |||
body.querySelectorAll('a').forEach(function(a){ a.onclick = close; }); | |||
} | |||
/* ── Boot ── */ | |||
injectCSS(); | |||
watchBodyPadding(); | |||
function boot() { | |||
expandSections(); | |||
watchSections(); | |||
injectMenuLinks(); | |||
[100, 400, 900, 1800].forEach(function(ms){ setTimeout(expandSections, ms); }); | |||
setTimeout(initToc, 700); | |||
} | |||
if ( document.readyState === 'loading' ) { | |||
document.addEventListener( 'DOMContentLoaded', boot ); | |||
} else { | |||
boot(); | |||
} | |||
if ( window.mw ) { | |||
mw.hook( 'wikipage.content' ).add(function() { | |||
setTimeout(function(){ expandSections(); injectMenuLinks(); initToc(); }, 300); | |||
}); | |||
} | |||
}() ); | |||
/** | |||
* grantha-mobile-fixes.js | |||
* Add this to MediaWiki:Common.js (or load as a separate gadget) | |||
* | |||
* Fixes: | |||
* 1. Patches Minerva's ToggleList to guard against null parentNode | |||
* (prevents "Cannot read properties of null" crash on Main_Page) | |||
* 2. Keeps #gr-home-toggle visible below #gr-static-bar on mobile | |||
* 3. Wires the grantha/author tab toggle if it hasn't been wired yet | |||
*/ | |||
( function () { | |||
'use strict'; | |||
/* ══════════════════════════════════════════════════════════════ | |||
* 1. TOGGLELIST NULL GUARD | |||
* Minerva's initMobile.js runs ToggleList.init() which calls | |||
* el.parentNode on every .collapsible-block it finds. | |||
* On Main_Page, some of these are dynamically injected and may | |||
* not yet be in the live DOM when ToggleList runs. | |||
* We patch by removing any element that has no parentNode | |||
* before ToggleList.init() fires. | |||
* ══════════════════════════════════════════════════════════════ */ | |||
mw.hook( 'wikipage.content' ).add( function () { | |||
/* Wait one tick so Minerva's own DOMReady handlers run first */ | |||
setTimeout( function () { | |||
var blocks = document.querySelectorAll( '.collapsible-block, .toggle-list' ); | |||
Array.prototype.forEach.call( blocks, function ( el ) { | |||
/* If the element has no parentNode it's detached — remove it | |||
before ToggleList tries to call parentNode on it */ | |||
if ( !el.parentNode ) { | |||
try { el.remove(); } catch(e) {} | |||
} | |||
} ); | |||
}, 0 ); | |||
} ); | |||
/* ══════════════════════════════════════════════════════════════ | |||
* 2. MAIN PAGE: keep #gr-home-toggle below the reader bar | |||
* ══════════════════════════════════════════════════════════════ */ | |||
if ( mw.config.get( 'wgPageName' ) !== 'Main_Page' ) return; | |||
mw.loader.using( 'mediawiki.util' ).done( function () { | |||
$( function () { | |||
applyHomeToggleOffset(); | |||
/* Re-apply when bar height might change (orientation change, etc.) */ | |||
window.addEventListener( 'resize', applyHomeToggleOffset, { passive: true } ); | |||
/* Re-apply after a short delay in case the bar hasn't rendered yet */ | |||
setTimeout( applyHomeToggleOffset, 300 ); | |||
setTimeout( applyHomeToggleOffset, 800 ); | |||
} ); | |||
} ); | |||
function applyHomeToggleOffset() { | |||
var bar = document.getElementById( 'gr-static-bar' ); | |||
if ( !bar ) return; | |||
var barRect = bar.getBoundingClientRect(); | |||
/* barRect.bottom = distance from viewport top to bar bottom. | |||
On load (before any scroll) this equals the bar's actual top offset | |||
plus its height. After scrolling (Minerva header gone) it's just | |||
the bar height since bar.top → 0. */ | |||
var barBottom = Math.round( barRect.bottom ); | |||
/* #gr-home is the outermost container */ | |||
var homeEl = document.getElementById( 'gr-home' ); | |||
/* #gr-home-toggle is the tab switcher row */ | |||
var toggleEl = document.getElementById( 'gr-home-toggle' ); | |||
/* Also target the mw-parser-output first child as a fallback */ | |||
var firstChild = document.querySelector( | |||
'#mw-content-text .mw-parser-output > .gr-home, ' + | |||
'#mw-content-text .mw-parser-output > *:first-child' | |||
); | |||
/* scroll-margin-top: so that if someone scrolls-to-top the toggle | |||
is visible below the bar (not behind it) */ | |||
[ homeEl, toggleEl, firstChild ].forEach( function ( el ) { | |||
if ( el ) el.style.scrollMarginTop = ( barBottom + 4 ) + 'px'; | |||
} ); | |||
/* The body already has padding-top set by readerToolbar.js. | |||
But on Main_Page the toggle might STILL be hidden if the | |||
Minerva header is taller. Force the content area's top | |||
padding to be at least barBottom. */ | |||
var contentText = document.getElementById( 'mw-content-text' ); | |||
if ( contentText ) { | |||
/* Only add extra padding if we're on mobile */ | |||
var isMob = window.innerWidth < 768 || !!document.getElementById( 'mw-mf-viewport' ); | |||
if ( isMob ) { | |||
/* Check if the toggle is actually obscured */ | |||
if ( toggleEl ) { | |||
var toggleRect = toggleEl.getBoundingClientRect(); | |||
/* If toggle top < barBottom, the bar is covering it */ | |||
if ( toggleRect.top < barBottom ) { | |||
var currentPT = parseInt( window.getComputedStyle( document.body ).paddingTop, 10 ) || 0; | |||
var needed = currentPT + ( barBottom - toggleRect.top ) + 4; | |||
document.body.style.paddingTop = needed + 'px'; | |||
} | |||
} | |||
} | |||
} | |||
} | |||
/* ══════════════════════════════════════════════════════════════ | |||
* 3. WIRE THE GRANTHA / AUTHOR TAB TOGGLE | |||
* In case the toggle script hasn't loaded yet or failed | |||
* ══════════════════════════════════════════════════════════════ */ | |||
$( function () { | |||
var $toggle = $( '#gr-home-toggle' ); | |||
var $viewG = $( '#gr-view-grantha' ); | |||
var $viewA = $( '#gr-view-author' ); | |||
var $btnG = $( '#gr-toggle-grantha' ); | |||
var $btnA = $( '#gr-toggle-author' ); | |||
if ( !$toggle.length || !$viewG.length || !$viewA.length ) return; | |||
/* Only wire if not already wired */ | |||
if ( $toggle.data( 'gr-wired' ) ) return; | |||
$toggle.data( 'gr-wired', true ); | |||
function showView( which ) { | |||
if ( which === 'grantha' ) { | |||
$viewG.show(); $viewA.hide(); | |||
$btnG.addClass( 'gr-toggle-active' ); | |||
$btnA.removeClass( 'gr-toggle-active' ); | |||
} else { | |||
$viewA.show(); $viewG.hide(); | |||
$btnA.addClass( 'gr-toggle-active' ); | |||
$btnG.removeClass( 'gr-toggle-active' ); | |||
} | |||
try { localStorage.setItem( 'grantha_home_tab', which ); } catch(e){} | |||
} | |||
$btnG.on( 'click keydown', function(e){ | |||
if ( e.type === 'keydown' && e.key !== 'Enter' && e.key !== ' ' ) return; | |||
showView( 'grantha' ); | |||
}); | |||
$btnA.on( 'click keydown', function(e){ | |||
if ( e.type === 'keydown' && e.key !== 'Enter' && e.key !== ' ' ) return; | |||
showView( 'author' ); | |||
}); | |||
/* Restore last-used tab */ | |||
try { | |||
var saved = localStorage.getItem( 'grantha_home_tab' ); | |||
if ( saved === 'author' ) showView( 'author' ); | |||
else showView( 'grantha' ); | |||
} catch(e) { showView( 'grantha' ); } | |||
} ); | |||
}() ); | }() ); | ||