User:Udays108/common.js: Difference between revisions
Appearance
No edit summary |
No edit summary Tag: Manual revert |
||
| (46 intermediate revisions by the same user not shown) | |||
| Line 5: | Line 5: | ||
* Strategy: | * Strategy: | ||
* 1. Extract wiki article content from MediaWiki's DOM | * 1. Extract wiki article content from MediaWiki's DOM | ||
* 2. Inject the complete | * 2. Inject the complete shell structure | ||
* 3. Place article content inside the shell | * 3. Place article content inside the shell | ||
* MediaWiki still runs underneath for editing, search, login, etc. | * MediaWiki still runs underneath for editing, search, login, etc. | ||
| Line 13: | Line 13: | ||
'use strict'; | 'use strict'; | ||
/* ── | /* ── Extract nav from MediaWiki's sidebar DOM ─────────────── */ | ||
var | function extractNavFromDom() { | ||
var SKIP_IDS = { | |||
'p-tb': 1, 'p-personal': 1, 'p-search': 1, | |||
'p-logo': 1, 'p-cactions': 1, 'p-views': 1, | |||
'p-lang': 1, 'p-namespaces': 1 | |||
}; | |||
/* Scope to the sidebar container to exclude Vector 2022 header user-menu portlets | |||
(p-vector-user-menu-userpage, p-user-menu-logout, p-personal-sticky-header, etc.) */ | |||
var navContainer = document.getElementById( 'vector-main-menu' ) || | |||
document.getElementById( 'mw-panel' ) || | |||
document.getElementById( 'mw-navigation' ) || | |||
document.body; | |||
var portlets = Array.prototype.slice.call( | |||
} | navContainer.querySelectorAll( '[id^="p-"]' ) | ||
); | |||
var nav = []; | |||
var currentUrl = window.location.href.split( '?' )[0]; | |||
portlets.forEach( function ( portlet ) { | |||
if ( SKIP_IDS[ portlet.id ] ) return; | |||
var links = Array.prototype.slice.call( | |||
portlet.querySelectorAll( 'li a' ) | |||
).map( function ( a ) { | |||
return { label: a.textContent.trim(), href: a.href }; | |||
{ | } ).filter( function ( item ) { return item.label && item.href; } ); | ||
if ( !links.length ) return; | |||
var headingEl = portlet.querySelector( '[id$="-label"], h3, h2' ); | |||
var sectionLabel = headingEl ? headingEl.textContent.trim() : ''; | |||
{ label: | |||
var isActive = links.some( function ( link ) { | |||
return link.href.split( '?' )[0] === currentUrl; | |||
} ); | |||
if ( links.length === 1 ) { | |||
nav.push( { | |||
label: links[0].label, | |||
href: links[0].href, | |||
isActive: links[0].href.split( '?' )[0] === currentUrl | |||
} ); | |||
} else { | |||
nav.push( { | |||
label: sectionLabel || links[0].label, | |||
href: links[0].href, | |||
items: links, | |||
isActive: isActive | |||
} ); | |||
{ label: | } | ||
} ); | |||
return nav; | |||
} | |||
} | |||
/* ── Build topbar HTML ─────────────────────────────────────── */ | /* ── Build topbar HTML ─────────────────────────────────────── */ | ||
| Line 105: | Line 103: | ||
} else { | } else { | ||
var a = document.createElement( 'a' ); | var a = document.createElement( 'a' ); | ||
a.href = item.href; | a.href = item.page ? mw.util.getUrl( item.page ) : item.href; | ||
a.textContent = item.label; | a.textContent = item.label; | ||
if ( item.ext ) a.target = '_blank'; | if ( item.ext ) a.target = '_blank'; | ||
| Line 115: | Line 113: | ||
/* ── Build full header ─────────────────────────────────────── */ | /* ── Build full header ─────────────────────────────────────── */ | ||
function buildHeader( currentPage ) { | function buildHeader( currentPage, nav ) { | ||
/* Logo */ | /* Logo */ | ||
var logo = document.createElement( 'a' ); | var logo = document.createElement( 'a' ); | ||
logo.href = | logo.href = mw.util.getUrl( 'BioMicroCenter' ); | ||
logo.className = 'bmc-logo'; | logo.className = 'bmc-logo'; | ||
logo.innerHTML = | logo.innerHTML = | ||
'< | '<img class="bmc-logo-img" src="https://bmcwiki.mit.edu/images/c/c9/Logo.png" alt="MIT BioMicro Center" />'; | ||
/* Nav */ | /* Nav */ | ||
var ul = document.createElement( 'ul' ); | var ul = document.createElement( 'ul' ); | ||
nav.forEach( function ( item ) { | |||
var li = document.createElement( 'li' ); | var li = document.createElement( 'li' ); | ||
if ( item.isActive ) li.classList.add( 'bmc-active' ); | |||
if ( item.items ) li.classList.add( 'bmc-dropdown' ); | if ( item.items ) li.classList.add( 'bmc-dropdown' ); | ||
var a = document.createElement( 'a' ); | var a = document.createElement( 'a' ); | ||
a.href = item.href; | a.href = item.page ? mw.util.getUrl( item.page ) : ( item.href || '#' ); | ||
a.textContent = item.label; | a.textContent = item.label; | ||
li.appendChild( a ); | li.appendChild( a ); | ||
| Line 148: | Line 139: | ||
} ); | } ); | ||
var | var navEl = document.createElement( 'nav' ); | ||
navEl.className = 'bmc-nav'; | |||
navEl.appendChild( ul ); | |||
/* Search */ | /* Search */ | ||
| Line 160: | Line 151: | ||
'<input type="hidden" name="title" value="Special:Search">' + | '<input type="hidden" name="title" value="Special:Search">' + | ||
'<input type="search" name="search" placeholder="Search wiki…" aria-label="Search">' + | '<input type="search" name="search" placeholder="Search wiki…" aria-label="Search">' + | ||
'<button type="submit"> | '<button type="submit" aria-label="Search"><svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"><circle cx="11" cy="11" r="8"/><line x1="21" y1="21" x2="16.65" y2="16.65"/></svg></button>'; | ||
/* User info — shown only when logged in */ | |||
var userName = mw.config.get( 'wgUserName' ); | |||
var userEl = null; | |||
if ( userName ) { | |||
userEl = document.createElement( 'span' ); | |||
userEl.className = 'bmc-user-info'; | |||
userEl.textContent = userName; | |||
} | |||
var right = document.createElement( 'div' ); | var right = document.createElement( 'div' ); | ||
right.className = 'bmc-header-right'; | right.className = 'bmc-header-right'; | ||
right.appendChild( | right.appendChild( navEl ); | ||
right.appendChild( searchForm ); | right.appendChild( searchForm ); | ||
if ( userEl ) { right.appendChild( userEl ); } | |||
/* Inner wrapper */ | /* Inner wrapper */ | ||
| Line 184: | Line 185: | ||
crumb.className = 'bmc-breadcrumb'; | crumb.className = 'bmc-breadcrumb'; | ||
crumb.innerHTML = | crumb.innerHTML = | ||
'<a href=" | '<a href="' + mw.util.getUrl( 'BioMicroCenter' ) + '">Home</a>' + | ||
'<span class="sep">›</span>' + | '<span class="sep">›</span>' + | ||
document.createTextNode( titleText ).textContent; // plain text, no XSS | document.createTextNode( titleText ).textContent; // plain text, no XSS | ||
| Line 202: | Line 203: | ||
} | } | ||
/* ── Build sidebar TOC from MediaWiki #toc | /* ── Build sidebar TOC from MediaWiki #toc or heading scan ─── */ | ||
function buildSidebarToc( mwToc ) { | function buildSidebarToc( mwToc, contentEl ) { | ||
var toc = document.createElement( 'div' ); | var toc = document.createElement( 'div' ); | ||
toc.className = 'bmc-toc'; | toc.className = 'bmc-toc'; | ||
| Line 210: | Line 211: | ||
h3.textContent = 'On this page'; | h3.textContent = 'On this page'; | ||
toc.appendChild( h3 ); | toc.appendChild( h3 ); | ||
var list; | |||
if ( mwToc ) { | if ( mwToc ) { | ||
| Line 215: | Line 218: | ||
var mwList = mwToc.querySelector( 'ul' ); | var mwList = mwToc.querySelector( 'ul' ); | ||
if ( mwList ) { | if ( mwList ) { | ||
list = mwList.cloneNode( true ); | |||
/* Mark sub-items */ | /* Mark sub-items */ | ||
list.querySelectorAll( 'li li' ).forEach( function ( li ) { | |||
li.classList.add( 'bmc-sub' ); | li.classList.add( 'bmc-sub' ); | ||
} ); | } ); | ||
} | } | ||
} else { | } else if ( contentEl ) { | ||
/* | /* Fall back to scanning h2/h3 headings directly */ | ||
return | var headings = Array.prototype.slice.call( | ||
contentEl.querySelectorAll( 'h2, h3' ) | |||
); | |||
if ( headings.length > 0 ) { | |||
list = document.createElement( 'ul' ); | |||
headings.forEach( function ( heading ) { | |||
var anchor = heading.querySelector( '.mw-headline' ); | |||
if ( !anchor || !anchor.id ) return; | |||
var li = document.createElement( 'li' ); | |||
if ( heading.tagName === 'H3' ) li.classList.add( 'bmc-sub' ); | |||
var a = document.createElement( 'a' ); | |||
a.href = '#' + anchor.id; | |||
a.textContent = anchor.textContent.trim(); | |||
li.appendChild( a ); | |||
list.appendChild( li ); | |||
} ); | |||
} | |||
} | } | ||
if ( !list || list.childNodes.length === 0 ) return null; | |||
toc.appendChild( list ); | |||
var aside = document.createElement( 'aside' ); | var aside = document.createElement( 'aside' ); | ||
| Line 233: | Line 255: | ||
} | } | ||
/* ── Smooth scroll for anchor links ───────────────────────── */ | /* ── Smooth scroll for anchor links ───────────────────────── */ | ||
| Line 296: | Line 269: | ||
} | } | ||
} ); | } ); | ||
} | |||
/* ── Fix row separators on tables with rowspan cells ──────── */ | |||
function fixRowspanSeparators( container ) { | |||
container.querySelectorAll( '.wikitable' ).forEach( function ( table ) { | |||
var tbody = table.querySelector( 'tbody' ) || table; | |||
var rows = Array.from( tbody.querySelectorAll( 'tr' ) ); | |||
if ( !rows.length ) return; | |||
rows.forEach( function ( row, rowIdx ) { | |||
row.querySelectorAll( 'td[rowspan], th[rowspan]' ).forEach( function ( cell ) { | |||
var span = parseInt( cell.getAttribute( 'rowspan' ), 10 ); | |||
if ( !span || span <= 1 ) return; | |||
cell.style.position = 'relative'; | |||
for ( var i = 1; i < span; i++ ) { | |||
var targetRow = rows[ rowIdx + i ]; | |||
if ( !targetRow ) break; | |||
var cellRect = cell.getBoundingClientRect(); | |||
var rowRect = targetRow.getBoundingClientRect(); | |||
var offsetTop = rowRect.top - cellRect.top; | |||
var sep = document.createElement( 'div' ); | |||
sep.style.cssText = | |||
'position:absolute;left:0;right:0;' + | |||
'top:' + offsetTop + 'px;' + | |||
'height:1px;background:#e0e0e0;' + | |||
'pointer-events:none;z-index:1;'; | |||
cell.appendChild( sep ); | |||
} | |||
} ); | |||
} ); | |||
} ); | |||
} | |||
/* ── Split <br>-separated items inside a <ul> into separate <li>s ── */ | |||
function splitBrBullets( ul ) { | |||
Array.prototype.slice.call( ul.querySelectorAll( 'li' ) ).forEach( function ( li ) { | |||
if ( !li.querySelector( 'br' ) ) return; | |||
var nodes = Array.prototype.slice.call( li.childNodes ); | |||
var segments = [[]]; | |||
nodes.forEach( function ( node ) { | |||
if ( node.nodeName === 'BR' ) { | |||
segments.push( [] ); | |||
} else { | |||
segments[ segments.length - 1 ].push( node ); | |||
} | |||
} ); | |||
/* Drop empty segments (only whitespace text nodes) */ | |||
segments = segments.filter( function ( seg ) { | |||
return seg.some( function ( n ) { | |||
return n.nodeType === 1 || | |||
( n.nodeType === 3 && n.textContent.trim() !== '' ); | |||
} ); | |||
} ); | |||
if ( segments.length <= 1 ) return; | |||
var parent = li.parentNode; | |||
var ref = li.nextSibling; | |||
segments.forEach( function ( seg ) { | |||
var newLi = document.createElement( 'li' ); | |||
seg.forEach( function ( n ) { newLi.appendChild( n ); } ); | |||
parent.insertBefore( newLi, ref ); | |||
} ); | |||
parent.removeChild( li ); | |||
} ); | |||
} | |||
/* ── Edit bar for logged-in users ─────────────────────────── */ | |||
function buildEditBar( currentPage ) { | |||
if ( !mw.config.get( 'wgUserId' ) ) return null; | |||
var bar = document.createElement( 'div' ); | |||
bar.className = 'bmc-edit-bar'; | |||
var editHref = mw.util.getUrl( currentPage, { action: 'edit' } ); | |||
var histHref = mw.util.getUrl( currentPage, { action: 'history' } ); | |||
var talkHref = mw.util.getUrl( 'Talk:' + currentPage ); | |||
bar.innerHTML = | |||
'<a href="' + editHref + '">Edit</a>' + | |||
'<a href="' + histHref + '">History</a>' + | |||
'<a href="' + talkHref + '">Talk</a>'; | |||
return bar; | |||
} | } | ||
/* ── Main: build and inject the shell ─────────────────────── */ | /* ── Main: build and inject the shell ─────────────────────── */ | ||
function buildShell() { | function buildShell( nav ) { | ||
var currentPage = mw.config.get( 'wgPageName' ) || ''; | var currentPage = mw.config.get( 'wgPageName' ) || ''; | ||
| Line 319: | Line 374: | ||
if ( mwToc ) mwToc.parentNode.removeChild( mwToc ); /* remove from content body */ | if ( mwToc ) mwToc.parentNode.removeChild( mwToc ); /* remove from content body */ | ||
/* 3b. Hide the | /* 3b. Hide the BMC logo/header sticker by exact image filename. | ||
The sticker is BMC_Header_2020_3.png (from the {{BioMicroCenter}} template) | |||
and bmc_logo_square.png. Target the nearest meaningful container so the | |||
surrounding content (nav links, table structure) is preserved. */ | |||
contentClone.querySelectorAll( ' | contentClone.querySelectorAll( 'img' ).forEach( function ( img ) { | ||
if ( | var src = img.getAttribute( 'src' ) || ''; | ||
if ( /BMC_Header|bmc_logo_square|Bmc_logo|\/Logo\.png/i.test( src ) ) { | |||
var container = img.closest( '.thumb' ) || img.closest( 'figure' ) || img.closest( 'td' ); | |||
if ( container ) { | |||
container.style.display = 'none'; | |||
} else { | |||
img.style.display = 'none'; | |||
} | |||
} | } | ||
} ); | } ); | ||
/* 4. Build shell elements */ | /* 4. Build shell elements */ | ||
var topbar = buildTopbar(); | var topbar = buildTopbar(); | ||
var header = buildHeader( currentPage ); | var header = buildHeader( currentPage, nav ); | ||
var hero = buildHero( titleText ); | var hero = buildHero( titleText ); | ||
| Line 341: | Line 399: | ||
layout.className = 'bmc-page-layout'; | layout.className = 'bmc-page-layout'; | ||
/* Sidebar */ | /* Sidebar — skip on the home page */ | ||
var sidebar = buildSidebarToc( mwToc ); | var isHome = ( currentPage === 'BioMicroCenter' || | ||
currentPage === 'Main_Page' ); | |||
var sidebar = isHome ? null : buildSidebarToc( mwToc, contentClone ); | |||
if ( sidebar ) { | if ( sidebar ) { | ||
layout.appendChild( sidebar ); | layout.appendChild( sidebar ); | ||
| Line 353: | Line 413: | ||
var main = document.createElement( 'main' ); | var main = document.createElement( 'main' ); | ||
main.className = 'bmc-content'; | main.className = 'bmc-content'; | ||
var editBar = buildEditBar( currentPage ); | |||
if ( editBar ) { main.appendChild( editBar ); } | |||
main.appendChild( contentClone ); | main.appendChild( contentClone ); | ||
layout.appendChild( main ); | layout.appendChild( main ); | ||
/* 5. Build wrapper */ | /* 5. Build wrapper */ | ||
| Line 365: | Line 425: | ||
wrapper.appendChild( hero ); | wrapper.appendChild( hero ); | ||
wrapper.appendChild( layout ); | wrapper.appendChild( layout ); | ||
wrapper.appendChild( | |||
var templateFooter = contentClone.querySelector( '#footer' ); | |||
if ( templateFooter ) { | |||
templateFooter.remove(); | |||
wrapper.appendChild( templateFooter ); | |||
} | |||
/* 6. Inject into body (prepend so it appears first) */ | /* 6. Inject into body (prepend so it appears first) */ | ||
document.body.insertBefore( wrapper, document.body.firstChild ); | document.body.insertBefore( wrapper, document.body.firstChild ); | ||
/* 7. | /* 7. Add class immediately so the CSS rule kicks in and hides | ||
all original MW elements (works for any skin version) */ | |||
.forEach( function ( | document.body.classList.add( 'bmc-active' ); | ||
if ( isHome ) { | |||
document.body.classList.add( 'bmc-home' ); | |||
/* Remove page title from hero — logo already identifies the site */ | |||
var heroH1 = hero.querySelector( 'h1' ); | |||
if ( heroH1 ) { heroH1.remove(); } | |||
var heroCrumb = hero.querySelector( '.bmc-breadcrumb' ); | |||
if ( heroCrumb ) { heroCrumb.remove(); } | |||
var parserOutput = contentClone.querySelector( '.mw-parser-output' ) || contentClone; | |||
/* 7a. Remove the "Welcome to the MIT BioMicro Center!" tagline */ | |||
var welcomeEl = null; | |||
Array.prototype.forEach.call( parserOutput.children, function ( el ) { | |||
if ( !welcomeEl && /Welcome to the MIT BioMicro Center/i.test( el.textContent ) ) { | |||
welcomeEl = el; | |||
} | |||
} ); | } ); | ||
if ( welcomeEl ) { welcomeEl.parentNode.removeChild( welcomeEl ); } | |||
/* 7b. Extract links from overview table → inject as pill links in hero; remove table */ | |||
var overviewTable = parserOutput.querySelector( 'table' ); | |||
if ( overviewTable ) { | |||
var heroLinksBar = document.createElement( 'div' ); | |||
heroLinksBar.className = 'bmc-hero-links'; | |||
var seen = {}; | |||
overviewTable.querySelectorAll( 'a' ).forEach( function ( a ) { | |||
var label = a.textContent.trim(); | |||
var href = a.getAttribute( 'href' ); | |||
if ( !label || !href || seen[ href ] ) return; | |||
seen[ href ] = true; | |||
var pill = document.createElement( 'a' ); | |||
pill.className = 'bmc-hero-link'; | |||
pill.setAttribute( 'href', href ); | |||
pill.textContent = label; | |||
heroLinksBar.appendChild( pill ); | |||
} ); | |||
if ( heroLinksBar.children.length ) { | |||
hero.querySelector( '.inner' ).appendChild( heroLinksBar ); | |||
} | |||
overviewTable.parentNode.removeChild( overviewTable ); | |||
} | |||
/* 7c. Remove ▾ indicator from service cards that have no list content */ | |||
parserOutput.querySelectorAll( | |||
'table:not(.wikitable) td[style*="border"]' | |||
).forEach( function ( td ) { | |||
if ( !td.querySelector( 'ul' ) ) { | |||
var heading = td.querySelector( 'big b, big strong' ); | |||
if ( heading ) { heading.classList.add( 'bmc-no-list' ); } | |||
} | |||
} ); | |||
/* 7d. Service browser: centered stacks → slide-left + panel */ | |||
var serviceTables = Array.prototype.slice.call( | |||
parserOutput.querySelectorAll( 'table:not(.wikitable)' ) | |||
).filter( function ( t ) { return !t.closest( '.bmc-overview-dropdown' ); } ); | |||
var colMap = {}, categories = []; | |||
serviceTables.forEach( function ( table ) { | |||
Array.prototype.forEach.call( table.querySelectorAll( 'tr' ), function ( row ) { | |||
var cells = row.querySelectorAll( 'td[style*="border"]' ); | |||
if ( !cells.length ) return; | |||
var isHeader = Array.prototype.every.call( cells, function ( td ) { | |||
return !td.querySelector( 'ul' ); | |||
} ); | |||
var col = 0; | |||
Array.prototype.forEach.call( cells, function ( td ) { | |||
var span = parseInt( td.getAttribute( 'colspan' ) || '1', 10 ); | |||
/* Robust link: first <a> in the cell that is NOT inside a <ul>. | |||
Handles both <big><b><a>text</a></b></big> | |||
and <a><big><b>text</b></big></a> nesting patterns. */ | |||
var hdLink = null; | |||
var allAnchors = td.querySelectorAll( 'a' ); | |||
for ( var j = 0; j < allAnchors.length; j++ ) { | |||
if ( !allAnchors[j].closest( 'ul' ) ) { hdLink = allAnchors[j]; break; } | |||
} | |||
/* Robust name: clone the cell, strip the sub-list, take all remaining text. | |||
Handles split markup like <a>Single Cell</a> and <a>Spatial</a>. */ | |||
var tdClone = td.cloneNode( true ); | |||
var ulInClone = tdClone.querySelector( 'ul' ); | |||
if ( ulInClone ) { ulInClone.parentNode.removeChild( ulInClone ); } | |||
var name = tdClone.textContent.replace( /\s+/g, ' ' ).trim(); | |||
if ( isHeader ) { | |||
var cat = { | |||
name: name, | |||
href: hdLink ? hdLink.getAttribute( 'href' ) : null, | |||
items: [] | |||
}; | |||
categories.push( cat ); | |||
for ( var i = 0; i < span; i++ ) { colMap[ col + i ] = cat; } | |||
} else { | |||
var c = colMap[ col ]; | |||
if ( c ) { | |||
/* Capture full cell HTML minus the sub-list so both | |||
"Single Cell" and "Spatial" links are preserved. */ | |||
var subUl = td.querySelector( 'ul' ); | |||
c.items.push( { | |||
html: tdClone.innerHTML, | |||
subList: subUl ? subUl.cloneNode( true ) : null | |||
} ); | |||
} | |||
} | |||
col += span; | |||
} ); | |||
} ); | |||
table.parentNode.removeChild( table ); | |||
} ); | |||
if ( categories.length ) { | |||
var browser = document.createElement( 'div' ); | |||
browser.className = 'bmc-service-browser'; | |||
var catList = document.createElement( 'div' ); | |||
catList.className = 'bmc-category-list'; | |||
var panelsWrap = document.createElement( 'div' ); | |||
panelsWrap.className = 'bmc-category-panels'; | |||
categories.forEach( function ( cat, idx ) { | |||
/* left button */ | |||
var btn = document.createElement( 'button' ); | |||
btn.className = 'bmc-category-btn'; | |||
btn.textContent = cat.name; | |||
btn.setAttribute( 'data-cat', String( idx ) ); | |||
catList.appendChild( btn ); | |||
/* right panel */ | |||
var panel = document.createElement( 'div' ); | |||
panel.className = 'bmc-category-panel'; | |||
panel.setAttribute( 'data-cat', String( idx ) ); | |||
var itemsWrap = document.createElement( 'div' ); | |||
itemsWrap.className = 'bmc-panel-items'; | |||
cat.items.forEach( function ( item ) { | |||
var itemEl = document.createElement( 'div' ); | |||
itemEl.className = 'bmc-panel-item'; | |||
itemEl.innerHTML = item.html; | |||
if ( item.subList ) { | |||
splitBrBullets( item.subList ); | |||
item.subList.className = 'bmc-panel-sublist'; | |||
itemEl.appendChild( item.subList ); | |||
} | |||
itemsWrap.appendChild( itemEl ); | |||
} ); | |||
panel.appendChild( itemsWrap ); | |||
panelsWrap.appendChild( panel ); | |||
} ); | |||
/* click handler */ | |||
catList.addEventListener( 'click', function ( e ) { | |||
var btn = e.target.closest( '.bmc-category-btn' ); | |||
if ( !btn ) return; | |||
var idx = btn.getAttribute( 'data-cat' ); | |||
/* first click — trigger the slide animation */ | |||
if ( !browser.classList.contains( 'bmc-browser-active' ) ) { | |||
browser.classList.add( 'bmc-browser-active' ); | |||
catList.style.marginLeft = '0'; | |||
} | |||
catList.querySelectorAll( '.bmc-category-btn' ).forEach( function ( b ) { | |||
b.classList.toggle( 'bmc-cat-active', b.getAttribute( 'data-cat' ) === idx ); | |||
} ); | |||
panelsWrap.querySelectorAll( '.bmc-category-panel' ).forEach( function ( p ) { | |||
p.classList.toggle( 'bmc-panel-visible', p.getAttribute( 'data-cat' ) === idx ); | |||
} ); | |||
} ); | |||
browser.appendChild( catList ); | |||
browser.appendChild( panelsWrap ); | |||
parserOutput.appendChild( browser ); | |||
/* Center the category list initially using a numeric margin-left | |||
(CSS margin:auto can't be transitioned; JS sets a pixel value that can) */ | |||
requestAnimationFrame( function () { | |||
var bw = browser.offsetWidth; | |||
var lw = catList.offsetWidth; | |||
var ml = Math.max( 0, Math.round( ( bw - lw ) / 2 ) ); | |||
catList.style.marginLeft = ml + 'px'; | |||
/* enable transitions AFTER initial centering is applied */ | |||
catList.style.transition = 'margin-left 0.38s ease, flex-basis 0.38s ease'; | |||
} ); | |||
} | |||
} | |||
/* 8. Smooth scroll */ | /* 8. Smooth scroll */ | ||
| Line 381: | Line 634: | ||
initSmoothScroll( sidebar || main ); | initSmoothScroll( sidebar || main ); | ||
/* 9. Auto-style | /* 9. Auto-style only genuine data tables — not layout/service-grid tables. | ||
Real data tables have <th> header cells; layout tables use <td> only. */ | |||
main.querySelectorAll( | main.querySelectorAll( | ||
'.mw-parser-output table:not(.wikitable):not(.infobox):not(.navbox)' | '.mw-parser-output table:not(.wikitable):not(.infobox):not(.navbox)' | ||
).forEach( function ( t ) { | ).forEach( function ( t ) { | ||
t.classList.add( 'wikitable' ); | var firstRow = t.querySelector( 'tr' ); | ||
if ( firstRow && firstRow.querySelector( 'th' ) ) { | |||
t.classList.add( 'wikitable' ); | |||
} | |||
} ); | |||
/* 10. Fix row separators on rowspan tables */ | |||
requestAnimationFrame( function () { | |||
fixRowspanSeparators( main ); | |||
} ); | } ); | ||
/* 11. Pricing page — convert wikitables to flex layout so column | |||
borders and content stay perfectly aligned even with colspan rows. | |||
colspan cells get flex-grow = span so they span proportionally. */ | |||
if ( currentPage.indexOf( 'Pricing' ) !== -1 ) { | |||
document.body.classList.add( 'bmc-pricing' ); | |||
main.querySelectorAll( '.wikitable' ).forEach( function ( table ) { | |||
table.querySelectorAll( 'tr' ).forEach( function ( row ) { | |||
row.querySelectorAll( 'th, td' ).forEach( function ( cell ) { | |||
var span = parseInt( cell.getAttribute( 'colspan' ) || '1', 10 ); | |||
cell.style.setProperty( 'flex', span + ' 1 0%', 'important' ); | |||
cell.style.setProperty( 'min-width', '0', 'important' ); | |||
} ); | |||
} ); | |||
} ); | |||
} | |||
} | } | ||
| Line 392: | Line 671: | ||
mw.hook( 'wikipage.content' ).add( function () { | mw.hook( 'wikipage.content' ).add( function () { | ||
/* Skip special pages (edit forms, history, etc.) to not break them */ | /* Skip special pages (edit forms, history, etc.) to not break them */ | ||
var ns = mw.config.get( 'wgNamespaceNumber' ); | var ns = mw.config.get( 'wgNamespaceNumber' ); | ||
var action = mw.config.get( 'wgAction' ); | var action = mw.config.get( 'wgAction' ); | ||
if ( action !== 'view' ) return; | if ( action !== 'view' ) return; | ||
if ( ns < 0 ) return; /* Special: pages */ | if ( ns < 0 ) return; /* Special: pages */ | ||
buildShell(); | var nav = extractNavFromDom(); | ||
buildShell( nav ); | |||
} ); | } ); | ||
} )(); | } )(); | ||
Latest revision as of 14:42, 24 June 2026
/**
* BioMicro Center Wiki — Full DOM Replacement
* Paste into: User:USERNAME/common.js
*
* Strategy:
* 1. Extract wiki article content from MediaWiki's DOM
* 2. Inject the complete shell structure
* 3. Place article content inside the shell
* MediaWiki still runs underneath for editing, search, login, etc.
*/
( function () {
'use strict';
/* ── Extract nav from MediaWiki's sidebar DOM ─────────────── */
function extractNavFromDom() {
var SKIP_IDS = {
'p-tb': 1, 'p-personal': 1, 'p-search': 1,
'p-logo': 1, 'p-cactions': 1, 'p-views': 1,
'p-lang': 1, 'p-namespaces': 1
};
/* Scope to the sidebar container to exclude Vector 2022 header user-menu portlets
(p-vector-user-menu-userpage, p-user-menu-logout, p-personal-sticky-header, etc.) */
var navContainer = document.getElementById( 'vector-main-menu' ) ||
document.getElementById( 'mw-panel' ) ||
document.getElementById( 'mw-navigation' ) ||
document.body;
var portlets = Array.prototype.slice.call(
navContainer.querySelectorAll( '[id^="p-"]' )
);
var nav = [];
var currentUrl = window.location.href.split( '?' )[0];
portlets.forEach( function ( portlet ) {
if ( SKIP_IDS[ portlet.id ] ) return;
var links = Array.prototype.slice.call(
portlet.querySelectorAll( 'li a' )
).map( function ( a ) {
return { label: a.textContent.trim(), href: a.href };
} ).filter( function ( item ) { return item.label && item.href; } );
if ( !links.length ) return;
var headingEl = portlet.querySelector( '[id$="-label"], h3, h2' );
var sectionLabel = headingEl ? headingEl.textContent.trim() : '';
var isActive = links.some( function ( link ) {
return link.href.split( '?' )[0] === currentUrl;
} );
if ( links.length === 1 ) {
nav.push( {
label: links[0].label,
href: links[0].href,
isActive: links[0].href.split( '?' )[0] === currentUrl
} );
} else {
nav.push( {
label: sectionLabel || links[0].label,
href: links[0].href,
items: links,
isActive: isActive
} );
}
} );
return nav;
}
/* ── Build topbar HTML ─────────────────────────────────────── */
function buildTopbar() {
var d = document.createElement( 'div' );
d.className = 'bmc-topbar';
d.innerHTML =
'<div class="inner">' +
'<a href="mailto:biomicro@mit.edu">biomicro@mit.edu</a>' +
'<span>|</span>' +
'617-715-4533' +
'<span>|</span>' +
'Building 68-322' +
'</div>';
return d;
}
/* ── Build nav dropdown item ───────────────────────────────── */
function buildDropdownMenu( items ) {
var menu = document.createElement( 'div' );
menu.className = 'bmc-dropdown-menu';
items.forEach( function ( item ) {
if ( item.divider ) {
var div = document.createElement( 'div' );
div.className = 'bmc-divider';
menu.appendChild( div );
} else if ( item.groupLabel ) {
var gl = document.createElement( 'div' );
gl.className = 'bmc-group-label';
gl.textContent = item.groupLabel;
menu.appendChild( gl );
} else {
var a = document.createElement( 'a' );
a.href = item.page ? mw.util.getUrl( item.page ) : item.href;
a.textContent = item.label;
if ( item.ext ) a.target = '_blank';
menu.appendChild( a );
}
} );
return menu;
}
/* ── Build full header ─────────────────────────────────────── */
function buildHeader( currentPage, nav ) {
/* Logo */
var logo = document.createElement( 'a' );
logo.href = mw.util.getUrl( 'BioMicroCenter' );
logo.className = 'bmc-logo';
logo.innerHTML =
'<img class="bmc-logo-img" src="https://bmcwiki.mit.edu/images/c/c9/Logo.png" alt="MIT BioMicro Center" />';
/* Nav */
var ul = document.createElement( 'ul' );
nav.forEach( function ( item ) {
var li = document.createElement( 'li' );
if ( item.isActive ) li.classList.add( 'bmc-active' );
if ( item.items ) li.classList.add( 'bmc-dropdown' );
var a = document.createElement( 'a' );
a.href = item.page ? mw.util.getUrl( item.page ) : ( item.href || '#' );
a.textContent = item.label;
li.appendChild( a );
if ( item.items ) {
li.appendChild( buildDropdownMenu( item.items ) );
}
ul.appendChild( li );
} );
var navEl = document.createElement( 'nav' );
navEl.className = 'bmc-nav';
navEl.appendChild( ul );
/* Search */
var searchForm = document.createElement( 'form' );
searchForm.className = 'bmc-search-form';
searchForm.method = 'get';
searchForm.action = '/index.php';
searchForm.innerHTML =
'<input type="hidden" name="title" value="Special:Search">' +
'<input type="search" name="search" placeholder="Search wiki…" aria-label="Search">' +
'<button type="submit" aria-label="Search"><svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"><circle cx="11" cy="11" r="8"/><line x1="21" y1="21" x2="16.65" y2="16.65"/></svg></button>';
/* User info — shown only when logged in */
var userName = mw.config.get( 'wgUserName' );
var userEl = null;
if ( userName ) {
userEl = document.createElement( 'span' );
userEl.className = 'bmc-user-info';
userEl.textContent = userName;
}
var right = document.createElement( 'div' );
right.className = 'bmc-header-right';
right.appendChild( navEl );
right.appendChild( searchForm );
if ( userEl ) { right.appendChild( userEl ); }
/* Inner wrapper */
var inner = document.createElement( 'div' );
inner.className = 'bmc-header-inner';
inner.appendChild( logo );
inner.appendChild( right );
var header = document.createElement( 'header' );
header.className = 'bmc-header';
header.appendChild( inner );
return header;
}
/* ── Build page hero ───────────────────────────────────────── */
function buildHero( titleText ) {
var crumb = document.createElement( 'div' );
crumb.className = 'bmc-breadcrumb';
crumb.innerHTML =
'<a href="' + mw.util.getUrl( 'BioMicroCenter' ) + '">Home</a>' +
'<span class="sep">›</span>' +
document.createTextNode( titleText ).textContent; // plain text, no XSS
var h1 = document.createElement( 'h1' );
h1.textContent = titleText;
var inner = document.createElement( 'div' );
inner.className = 'inner';
inner.appendChild( crumb );
inner.appendChild( h1 );
var hero = document.createElement( 'div' );
hero.className = 'bmc-page-hero';
hero.appendChild( inner );
return hero;
}
/* ── Build sidebar TOC from MediaWiki #toc or heading scan ─── */
function buildSidebarToc( mwToc, contentEl ) {
var toc = document.createElement( 'div' );
toc.className = 'bmc-toc';
var h3 = document.createElement( 'h3' );
h3.textContent = 'On this page';
toc.appendChild( h3 );
var list;
if ( mwToc ) {
/* Clone only the list from the MediaWiki TOC */
var mwList = mwToc.querySelector( 'ul' );
if ( mwList ) {
list = mwList.cloneNode( true );
/* Mark sub-items */
list.querySelectorAll( 'li li' ).forEach( function ( li ) {
li.classList.add( 'bmc-sub' );
} );
}
} else if ( contentEl ) {
/* Fall back to scanning h2/h3 headings directly */
var headings = Array.prototype.slice.call(
contentEl.querySelectorAll( 'h2, h3' )
);
if ( headings.length > 0 ) {
list = document.createElement( 'ul' );
headings.forEach( function ( heading ) {
var anchor = heading.querySelector( '.mw-headline' );
if ( !anchor || !anchor.id ) return;
var li = document.createElement( 'li' );
if ( heading.tagName === 'H3' ) li.classList.add( 'bmc-sub' );
var a = document.createElement( 'a' );
a.href = '#' + anchor.id;
a.textContent = anchor.textContent.trim();
li.appendChild( a );
list.appendChild( li );
} );
}
}
if ( !list || list.childNodes.length === 0 ) return null;
toc.appendChild( list );
var aside = document.createElement( 'aside' );
aside.className = 'bmc-sidebar';
aside.appendChild( toc );
return aside;
}
/* ── Smooth scroll for anchor links ───────────────────────── */
function initSmoothScroll( container ) {
container.addEventListener( 'click', function ( e ) {
var a = e.target.closest( 'a[href^="#"]' );
if ( !a ) return;
var id = decodeURIComponent( a.getAttribute( 'href' ).slice( 1 ) );
var target = document.getElementById( id );
if ( target ) {
e.preventDefault();
target.scrollIntoView( { behavior: 'smooth', block: 'start' } );
history.pushState( null, '', '#' + id );
}
} );
}
/* ── Fix row separators on tables with rowspan cells ──────── */
function fixRowspanSeparators( container ) {
container.querySelectorAll( '.wikitable' ).forEach( function ( table ) {
var tbody = table.querySelector( 'tbody' ) || table;
var rows = Array.from( tbody.querySelectorAll( 'tr' ) );
if ( !rows.length ) return;
rows.forEach( function ( row, rowIdx ) {
row.querySelectorAll( 'td[rowspan], th[rowspan]' ).forEach( function ( cell ) {
var span = parseInt( cell.getAttribute( 'rowspan' ), 10 );
if ( !span || span <= 1 ) return;
cell.style.position = 'relative';
for ( var i = 1; i < span; i++ ) {
var targetRow = rows[ rowIdx + i ];
if ( !targetRow ) break;
var cellRect = cell.getBoundingClientRect();
var rowRect = targetRow.getBoundingClientRect();
var offsetTop = rowRect.top - cellRect.top;
var sep = document.createElement( 'div' );
sep.style.cssText =
'position:absolute;left:0;right:0;' +
'top:' + offsetTop + 'px;' +
'height:1px;background:#e0e0e0;' +
'pointer-events:none;z-index:1;';
cell.appendChild( sep );
}
} );
} );
} );
}
/* ── Split <br>-separated items inside a <ul> into separate <li>s ── */
function splitBrBullets( ul ) {
Array.prototype.slice.call( ul.querySelectorAll( 'li' ) ).forEach( function ( li ) {
if ( !li.querySelector( 'br' ) ) return;
var nodes = Array.prototype.slice.call( li.childNodes );
var segments = [[]];
nodes.forEach( function ( node ) {
if ( node.nodeName === 'BR' ) {
segments.push( [] );
} else {
segments[ segments.length - 1 ].push( node );
}
} );
/* Drop empty segments (only whitespace text nodes) */
segments = segments.filter( function ( seg ) {
return seg.some( function ( n ) {
return n.nodeType === 1 ||
( n.nodeType === 3 && n.textContent.trim() !== '' );
} );
} );
if ( segments.length <= 1 ) return;
var parent = li.parentNode;
var ref = li.nextSibling;
segments.forEach( function ( seg ) {
var newLi = document.createElement( 'li' );
seg.forEach( function ( n ) { newLi.appendChild( n ); } );
parent.insertBefore( newLi, ref );
} );
parent.removeChild( li );
} );
}
/* ── Edit bar for logged-in users ─────────────────────────── */
function buildEditBar( currentPage ) {
if ( !mw.config.get( 'wgUserId' ) ) return null;
var bar = document.createElement( 'div' );
bar.className = 'bmc-edit-bar';
var editHref = mw.util.getUrl( currentPage, { action: 'edit' } );
var histHref = mw.util.getUrl( currentPage, { action: 'history' } );
var talkHref = mw.util.getUrl( 'Talk:' + currentPage );
bar.innerHTML =
'<a href="' + editHref + '">Edit</a>' +
'<a href="' + histHref + '">History</a>' +
'<a href="' + talkHref + '">Talk</a>';
return bar;
}
/* ── Main: build and inject the shell ─────────────────────── */
function buildShell( nav ) {
var currentPage = mw.config.get( 'wgPageName' ) || '';
/* 1. Extract wiki content BEFORE any DOM manipulation */
var mwContentText = document.getElementById( 'mw-content-text' );
if ( !mwContentText ) return; /* not a content page, bail */
var contentClone = mwContentText.cloneNode( true );
/* 2. Extract page title */
var titleEl = document.getElementById( 'firstHeading' ) ||
document.querySelector( '.mw-first-heading' );
var titleText = titleEl
? titleEl.textContent.trim()
: ( mw.config.get( 'wgTitle' ) || 'BioMicro Center' );
/* 3. Extract MediaWiki TOC from the cloned content */
var mwToc = contentClone.querySelector( '#toc, .toc' );
if ( mwToc ) mwToc.parentNode.removeChild( mwToc ); /* remove from content body */
/* 3b. Hide the BMC logo/header sticker by exact image filename.
The sticker is BMC_Header_2020_3.png (from the {{BioMicroCenter}} template)
and bmc_logo_square.png. Target the nearest meaningful container so the
surrounding content (nav links, table structure) is preserved. */
contentClone.querySelectorAll( 'img' ).forEach( function ( img ) {
var src = img.getAttribute( 'src' ) || '';
if ( /BMC_Header|bmc_logo_square|Bmc_logo|\/Logo\.png/i.test( src ) ) {
var container = img.closest( '.thumb' ) || img.closest( 'figure' ) || img.closest( 'td' );
if ( container ) {
container.style.display = 'none';
} else {
img.style.display = 'none';
}
}
} );
/* 4. Build shell elements */
var topbar = buildTopbar();
var header = buildHeader( currentPage, nav );
var hero = buildHero( titleText );
/* Page layout grid */
var layout = document.createElement( 'div' );
layout.className = 'bmc-page-layout';
/* Sidebar — skip on the home page */
var isHome = ( currentPage === 'BioMicroCenter' ||
currentPage === 'Main_Page' );
var sidebar = isHome ? null : buildSidebarToc( mwToc, contentClone );
if ( sidebar ) {
layout.appendChild( sidebar );
} else {
/* No TOC — collapse to single-column via inline style */
layout.style.gridTemplateColumns = '1fr';
}
/* Main content area */
var main = document.createElement( 'main' );
main.className = 'bmc-content';
var editBar = buildEditBar( currentPage );
if ( editBar ) { main.appendChild( editBar ); }
main.appendChild( contentClone );
layout.appendChild( main );
/* 5. Build wrapper */
var wrapper = document.createElement( 'div' );
wrapper.id = 'bmc-wrapper';
wrapper.appendChild( topbar );
wrapper.appendChild( header );
wrapper.appendChild( hero );
wrapper.appendChild( layout );
var templateFooter = contentClone.querySelector( '#footer' );
if ( templateFooter ) {
templateFooter.remove();
wrapper.appendChild( templateFooter );
}
/* 6. Inject into body (prepend so it appears first) */
document.body.insertBefore( wrapper, document.body.firstChild );
/* 7. Add class immediately so the CSS rule kicks in and hides
all original MW elements (works for any skin version) */
document.body.classList.add( 'bmc-active' );
if ( isHome ) {
document.body.classList.add( 'bmc-home' );
/* Remove page title from hero — logo already identifies the site */
var heroH1 = hero.querySelector( 'h1' );
if ( heroH1 ) { heroH1.remove(); }
var heroCrumb = hero.querySelector( '.bmc-breadcrumb' );
if ( heroCrumb ) { heroCrumb.remove(); }
var parserOutput = contentClone.querySelector( '.mw-parser-output' ) || contentClone;
/* 7a. Remove the "Welcome to the MIT BioMicro Center!" tagline */
var welcomeEl = null;
Array.prototype.forEach.call( parserOutput.children, function ( el ) {
if ( !welcomeEl && /Welcome to the MIT BioMicro Center/i.test( el.textContent ) ) {
welcomeEl = el;
}
} );
if ( welcomeEl ) { welcomeEl.parentNode.removeChild( welcomeEl ); }
/* 7b. Extract links from overview table → inject as pill links in hero; remove table */
var overviewTable = parserOutput.querySelector( 'table' );
if ( overviewTable ) {
var heroLinksBar = document.createElement( 'div' );
heroLinksBar.className = 'bmc-hero-links';
var seen = {};
overviewTable.querySelectorAll( 'a' ).forEach( function ( a ) {
var label = a.textContent.trim();
var href = a.getAttribute( 'href' );
if ( !label || !href || seen[ href ] ) return;
seen[ href ] = true;
var pill = document.createElement( 'a' );
pill.className = 'bmc-hero-link';
pill.setAttribute( 'href', href );
pill.textContent = label;
heroLinksBar.appendChild( pill );
} );
if ( heroLinksBar.children.length ) {
hero.querySelector( '.inner' ).appendChild( heroLinksBar );
}
overviewTable.parentNode.removeChild( overviewTable );
}
/* 7c. Remove ▾ indicator from service cards that have no list content */
parserOutput.querySelectorAll(
'table:not(.wikitable) td[style*="border"]'
).forEach( function ( td ) {
if ( !td.querySelector( 'ul' ) ) {
var heading = td.querySelector( 'big b, big strong' );
if ( heading ) { heading.classList.add( 'bmc-no-list' ); }
}
} );
/* 7d. Service browser: centered stacks → slide-left + panel */
var serviceTables = Array.prototype.slice.call(
parserOutput.querySelectorAll( 'table:not(.wikitable)' )
).filter( function ( t ) { return !t.closest( '.bmc-overview-dropdown' ); } );
var colMap = {}, categories = [];
serviceTables.forEach( function ( table ) {
Array.prototype.forEach.call( table.querySelectorAll( 'tr' ), function ( row ) {
var cells = row.querySelectorAll( 'td[style*="border"]' );
if ( !cells.length ) return;
var isHeader = Array.prototype.every.call( cells, function ( td ) {
return !td.querySelector( 'ul' );
} );
var col = 0;
Array.prototype.forEach.call( cells, function ( td ) {
var span = parseInt( td.getAttribute( 'colspan' ) || '1', 10 );
/* Robust link: first <a> in the cell that is NOT inside a <ul>.
Handles both <big><b><a>text</a></b></big>
and <a><big><b>text</b></big></a> nesting patterns. */
var hdLink = null;
var allAnchors = td.querySelectorAll( 'a' );
for ( var j = 0; j < allAnchors.length; j++ ) {
if ( !allAnchors[j].closest( 'ul' ) ) { hdLink = allAnchors[j]; break; }
}
/* Robust name: clone the cell, strip the sub-list, take all remaining text.
Handles split markup like <a>Single Cell</a> and <a>Spatial</a>. */
var tdClone = td.cloneNode( true );
var ulInClone = tdClone.querySelector( 'ul' );
if ( ulInClone ) { ulInClone.parentNode.removeChild( ulInClone ); }
var name = tdClone.textContent.replace( /\s+/g, ' ' ).trim();
if ( isHeader ) {
var cat = {
name: name,
href: hdLink ? hdLink.getAttribute( 'href' ) : null,
items: []
};
categories.push( cat );
for ( var i = 0; i < span; i++ ) { colMap[ col + i ] = cat; }
} else {
var c = colMap[ col ];
if ( c ) {
/* Capture full cell HTML minus the sub-list so both
"Single Cell" and "Spatial" links are preserved. */
var subUl = td.querySelector( 'ul' );
c.items.push( {
html: tdClone.innerHTML,
subList: subUl ? subUl.cloneNode( true ) : null
} );
}
}
col += span;
} );
} );
table.parentNode.removeChild( table );
} );
if ( categories.length ) {
var browser = document.createElement( 'div' );
browser.className = 'bmc-service-browser';
var catList = document.createElement( 'div' );
catList.className = 'bmc-category-list';
var panelsWrap = document.createElement( 'div' );
panelsWrap.className = 'bmc-category-panels';
categories.forEach( function ( cat, idx ) {
/* left button */
var btn = document.createElement( 'button' );
btn.className = 'bmc-category-btn';
btn.textContent = cat.name;
btn.setAttribute( 'data-cat', String( idx ) );
catList.appendChild( btn );
/* right panel */
var panel = document.createElement( 'div' );
panel.className = 'bmc-category-panel';
panel.setAttribute( 'data-cat', String( idx ) );
var itemsWrap = document.createElement( 'div' );
itemsWrap.className = 'bmc-panel-items';
cat.items.forEach( function ( item ) {
var itemEl = document.createElement( 'div' );
itemEl.className = 'bmc-panel-item';
itemEl.innerHTML = item.html;
if ( item.subList ) {
splitBrBullets( item.subList );
item.subList.className = 'bmc-panel-sublist';
itemEl.appendChild( item.subList );
}
itemsWrap.appendChild( itemEl );
} );
panel.appendChild( itemsWrap );
panelsWrap.appendChild( panel );
} );
/* click handler */
catList.addEventListener( 'click', function ( e ) {
var btn = e.target.closest( '.bmc-category-btn' );
if ( !btn ) return;
var idx = btn.getAttribute( 'data-cat' );
/* first click — trigger the slide animation */
if ( !browser.classList.contains( 'bmc-browser-active' ) ) {
browser.classList.add( 'bmc-browser-active' );
catList.style.marginLeft = '0';
}
catList.querySelectorAll( '.bmc-category-btn' ).forEach( function ( b ) {
b.classList.toggle( 'bmc-cat-active', b.getAttribute( 'data-cat' ) === idx );
} );
panelsWrap.querySelectorAll( '.bmc-category-panel' ).forEach( function ( p ) {
p.classList.toggle( 'bmc-panel-visible', p.getAttribute( 'data-cat' ) === idx );
} );
} );
browser.appendChild( catList );
browser.appendChild( panelsWrap );
parserOutput.appendChild( browser );
/* Center the category list initially using a numeric margin-left
(CSS margin:auto can't be transitioned; JS sets a pixel value that can) */
requestAnimationFrame( function () {
var bw = browser.offsetWidth;
var lw = catList.offsetWidth;
var ml = Math.max( 0, Math.round( ( bw - lw ) / 2 ) );
catList.style.marginLeft = ml + 'px';
/* enable transitions AFTER initial centering is applied */
catList.style.transition = 'margin-left 0.38s ease, flex-basis 0.38s ease';
} );
}
}
/* 8. Smooth scroll */
initSmoothScroll( main );
initSmoothScroll( sidebar || main );
/* 9. Auto-style only genuine data tables — not layout/service-grid tables.
Real data tables have <th> header cells; layout tables use <td> only. */
main.querySelectorAll(
'.mw-parser-output table:not(.wikitable):not(.infobox):not(.navbox)'
).forEach( function ( t ) {
var firstRow = t.querySelector( 'tr' );
if ( firstRow && firstRow.querySelector( 'th' ) ) {
t.classList.add( 'wikitable' );
}
} );
/* 10. Fix row separators on rowspan tables */
requestAnimationFrame( function () {
fixRowspanSeparators( main );
} );
/* 11. Pricing page — convert wikitables to flex layout so column
borders and content stay perfectly aligned even with colspan rows.
colspan cells get flex-grow = span so they span proportionally. */
if ( currentPage.indexOf( 'Pricing' ) !== -1 ) {
document.body.classList.add( 'bmc-pricing' );
main.querySelectorAll( '.wikitable' ).forEach( function ( table ) {
table.querySelectorAll( 'tr' ).forEach( function ( row ) {
row.querySelectorAll( 'th, td' ).forEach( function ( cell ) {
var span = parseInt( cell.getAttribute( 'colspan' ) || '1', 10 );
cell.style.setProperty( 'flex', span + ' 1 0%', 'important' );
cell.style.setProperty( 'min-width', '0', 'important' );
} );
} );
} );
}
}
/* ── Entry point ───────────────────────────────────────────── */
mw.hook( 'wikipage.content' ).add( function () {
/* Skip special pages (edit forms, history, etc.) to not break them */
var ns = mw.config.get( 'wgNamespaceNumber' );
var action = mw.config.get( 'wgAction' );
if ( action !== 'view' ) return;
if ( ns < 0 ) return; /* Special: pages */
var nav = extractNavFromDom();
buildShell( nav );
} );
} )();