User:Gibcus/common.js
Appearance
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.
/**
* 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 — add body class and fix rowspan cell borders via JS.
CSS alone cannot reliably override MediaWiki own .wikitable td rule,
so we set border directly on the element style which always wins. */
if ( currentPage.indexOf( 'Pricing' ) !== -1 ) {
document.body.classList.add( 'bmc-pricing' );
requestAnimationFrame( function () {
main.querySelectorAll( '.wikitable td[rowspan]' ).forEach( function ( td ) {
td.style.setProperty( 'border-top', '0', 'important' );
td.style.setProperty( 'border-bottom', '0', 'important' );
td.style.setProperty( 'border-left', '0', 'important' );
td.style.setProperty( 'border-right', '2px solid #d0d0d0', 'important' );
td.style.setProperty( 'vertical-align', 'middle', '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 );
} );
} )();