|
|
| Line 1: |
Line 1: |
| /**
| |
| * 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
| |
| };
| |
|
| |
| var portlets = Array.prototype.slice.call(
| |
| document.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>';
| |
|
| |
| var right = document.createElement( 'div' );
| |
| right.className = 'bmc-header-right';
| |
| right.appendChild( navEl );
| |
| right.appendChild( searchForm );
| |
|
| |
| /* 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;
| |
| }
| |
|
| |
| /* ── Build footer ──────────────────────────────────────────── */
| |
| function buildFooter() {
| |
| var u = mw.util.getUrl;
| |
| var inner = document.createElement( 'div' );
| |
| inner.className = 'bmc-footer-inner';
| |
| inner.innerHTML =
| |
| '<div>' +
| |
| '<h4>MIT BioMicro Center</h4>' +
| |
| '<ul>' +
| |
| '<li>Building 68-322</li>' +
| |
| '<li>Cambridge, MA 02139</li>' +
| |
| '<li><a href="mailto:biomicro@mit.edu">biomicro@mit.edu</a></li>' +
| |
| '<li>617-715-4533</li>' +
| |
| '</ul>' +
| |
| '</div>' +
| |
| '<div>' +
| |
| '<h4>Services</h4>' +
| |
| '<ul>' +
| |
| '<li><a href="' + u( 'BioMicroCenter:Sequencing' ) + '">Bulk Sequencing</a></li>' +
| |
| '<li><a href="' + u( 'BioMicroCenter:SingleCell' ) + '">Single Cell</a></li>' +
| |
| '<li><a href="' + u( 'BioMicroCenter:SpTx' ) + '">Spatial Genomics</a></li>' +
| |
| '<li><a href="https://igb.mit.edu/" target="_blank">Informatics</a></li>' +
| |
| '</ul>' +
| |
| '</div>' +
| |
| '<div>' +
| |
| '<h4>Resources</h4>' +
| |
| '<ul>' +
| |
| '<li><a href="' + u( 'BioMicroCenter:FAQ' ) + '">FAQs</a></li>' +
| |
| '<li><a href="' + u( 'BioMicroCenter:Consulting' ) + '">Consulting</a></li>' +
| |
| '<li><a href="' + u( 'BioMicroCenter:Pricing' ) + '">Grant Support & Pricing</a></li>' +
| |
| '<li><a href="' + u( 'BioMicroCenter:Acknowledgement' ) + '">Acknowledgements</a></li>' +
| |
| '</ul>' +
| |
| '</div>';
| |
|
| |
| var bottom = document.createElement( 'div' );
| |
| bottom.className = 'bmc-footer-bottom';
| |
| bottom.innerHTML =
| |
| '<span>© ' + new Date().getFullYear() + ' MIT BioMicro Center</span>' +
| |
| '<span>' +
| |
| '<a href="' + u( 'Special:UserLogin' ) + '">Log in</a>' +
| |
| ' | ' +
| |
| '<a href="https://accessibility.mit.edu" target="_blank">Accessibility</a>' +
| |
| '</span>';
| |
|
| |
| var footer = document.createElement( 'footer' );
| |
| footer.className = 'bmc-footer';
| |
| footer.appendChild( inner );
| |
| footer.appendChild( bottom );
| |
| return footer;
| |
| }
| |
|
| |
| /* ── 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 );
| |
| }
| |
| } );
| |
| } );
| |
| } );
| |
| }
| |
|
| |
| /* ── 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 );
| |
|
| |
| var footer = buildFooter();
| |
|
| |
| /* 5. Build wrapper */
| |
| var wrapper = document.createElement( 'div' );
| |
| wrapper.id = 'bmc-wrapper';
| |
| wrapper.appendChild( topbar );
| |
| wrapper.appendChild( header );
| |
| wrapper.appendChild( hero );
| |
| wrapper.appendChild( layout );
| |
| wrapper.appendChild( footer );
| |
|
| |
| /* 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' );
| |
|
| |
| /* 8. Smooth scroll */
| |
| initSmoothScroll( main );
| |
| initSmoothScroll( sidebar || main );
| |
|
| |
| /* 9. Auto-style plain tables in content */
| |
| main.querySelectorAll(
| |
| '.mw-parser-output table:not(.wikitable):not(.infobox):not(.navbox)'
| |
| ).forEach( function ( t ) {
| |
| t.classList.add( 'wikitable' );
| |
| } );
| |
|
| |
| /* 10. Fix row separators on rowspan tables */
| |
| requestAnimationFrame( function () {
| |
| fixRowspanSeparators( main );
| |
| } );
| |
|
| |
|
| |
| }
| |
|
| |
| /* ── 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 );
| |
| } );
| |
|
| |
| } )();
| |