User:Gibcus/common.js: Difference between revisions
Appearance
No edit summary |
No edit summary |
||
| Line 461: | Line 461: | ||
if ( welcomeEl ) { welcomeEl.parentNode.removeChild( welcomeEl ); } | if ( welcomeEl ) { welcomeEl.parentNode.removeChild( welcomeEl ); } | ||
/* 7b. | /* 7b. Keep the overview/service tables as real tables — style them | ||
like the rest of the site's data tables instead of converting | |||
them into pill links / a sliding category browser. Cells that | |||
act as category headers (bold text, no <ul> of items) get a | |||
header treatment; cells with a <ul> stay as plain list cells. */ | |||
parserOutput.querySelectorAll( | parserOutput.querySelectorAll( | ||
'table:not(.wikitable) | 'table:not(.wikitable):not(.infobox):not(.navbox)' | ||
).forEach( function ( | ).forEach( function ( table ) { | ||
table.classList.add( 'wikitable', 'bmc-home-table' ); | |||
table.querySelectorAll( 'td[style*="border"]' ).forEach( function ( td ) { | |||
if ( !td.querySelector( 'ul' ) ) { | |||
td.classList.add( 'bmc-home-table-header' ); | |||
} | |||
} | |||
} ); | } ); | ||
} ); | } ); | ||
} | } | ||
Revision as of 15:50, 2 July 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 );
}
} );
}
/* ── Add a separator line at the END of each rowspan group, on every
cell whose own span also ends at that row. This marks the boundary
between e.g. "5M" and "25M" without ever drawing a line through
the inside of a single rowspan cell. */
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;
/* Build a grid: for each row index, find which row index each
rowspan group ENDS at, per starting cell. Then for every row
that is the last row of at least one rowspan group, draw a
border-bottom on every td/th that is present in that row
(cells whose own rowspan continues past this row are skipped,
since the line would cut through them — instead they get their
own end-of-group border drawn at their own last row). */
rows.forEach( function ( row, rowIdx ) {
row.querySelectorAll( 'td[rowspan], th[rowspan]' ).forEach( function ( cell ) {
var span = parseInt( cell.getAttribute( 'rowspan' ), 10 ) || 1;
var endRowIdx = rowIdx + span - 1;
var endRow = rows[ endRowIdx ];
if ( !endRow ) return;
/* Draw the boundary on every cell present in the END row,
except cells whose OWN rowspan extends beyond this row
(those get their boundary drawn at their own end row). */
endRow.querySelectorAll( 'td, th' ).forEach( function ( endCell ) {
var ownSpan = parseInt( endCell.getAttribute( 'rowspan' ), 10 ) || 1;
if ( ownSpan > 1 ) return; /* this cell's own group hasn't ended yet */
endCell.classList.add( 'bmc-rowspan-boundary' );
} );
} );
} );
} );
}
/* ── 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. Keep the overview/service tables as real tables — style them
like the rest of the site's data tables instead of converting
them into pill links / a sliding category browser. Cells that
act as category headers (bold text, no <ul> of items) get a
header treatment; cells with a <ul> stay as plain list cells. */
parserOutput.querySelectorAll(
'table:not(.wikitable):not(.infobox):not(.navbox)'
).forEach( function ( table ) {
table.classList.add( 'wikitable', 'bmc-home-table' );
table.querySelectorAll( 'td[style*="border"]' ).forEach( function ( td ) {
if ( !td.querySelector( 'ul' ) ) {
td.classList.add( 'bmc-home-table-header' );
}
} );
} );
}
/* 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 );
} );
} )();