Jump to content

User:Udays108/common.js: Difference between revisions

From BioMicro Center
Udays108 (talk | contribs)
No edit summary
Udays108 (talk | contribs)
No edit summary
Tag: Manual revert
 
(54 intermediate revisions by the same user not shown)
Line 1: Line 1:
/**
/**
  * BioMicro Center Wiki — Modern UI JavaScript
  * BioMicro Center Wiki — Full DOM Replacement
  * Paste into: User:USERNAME/common.js
  * Paste into: User:USERNAME/common.js
* Promoted to: MediaWiki:Common.js (by sysop)
  *
  *
  * Injects: topbar, page-hero, horizontal nav w/ dropdowns.
  * Strategy:
  * Enhances: TOC styling, smooth scroll, table auto-class.
* 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 () {
( function () {
'use strict';
  'use strict';


/* ── Nav definition (mirrors about.html) ───────────────────── */
  /* ── Extract nav from MediaWiki's sidebar DOM ─────────────── */
var NAV_ITEMS = [
  function extractNavFromDom() {
{
    var SKIP_IDS = {
label: 'About',
      'p-tb': 1, 'p-personal': 1, 'p-search': 1,
href: 'http://biomicro.mit.edu/about.html',
      'p-logo': 1, 'p-cactions': 1, 'p-views': 1,
match: [ 'BioMicroCenter', 'About' ]
      'p-lang': 1, 'p-namespaces': 1
},
    };
{
label: 'News',
href: 'http://biomicro.mit.edu/news.html',
match: [ 'News' ],
dropdown: [
{ label: 'Latest News', href: 'http://biomicro.mit.edu/news.html' },
{ label: 'Seminars', href: 'http://biomicro.mit.edu/seminars.html' },
{ label: 'Classes & Training', href: 'https://igb.mit.edu/mini-courses', external: true }
]
},
{
label: 'Services',
href: 'https://bmcwiki.mit.edu/index.php/BioMicroCenter:Assisted_Services',
match: [ 'Assisted_Services', 'Walkup', 'Services' ],
dropdown: [
{ label: 'Walkup', href: 'https://bmcwiki.mit.edu/index.php/BioMicroCenter:Walkup_Instrumentation' },
{ label: 'Assisted', href: 'https://bmcwiki.mit.edu/index.php/BioMicroCenter:Assisted_Services' },
{ label: 'Consumables', href: 'http://biomicro.mit.edu/consumables.html' },
{ label: 'Training', href: 'https://igb.mit.edu/mini-courses', external: true },
{ label: 'Informatics', href: 'https://igb.mit.edu/', external: true }
]
},
{
label: 'Submission',
href: 'http://biomicro.mit.edu/submit.html',
match: [ 'Submission', 'Submit' ],
dropdown: [
{ groupLabel: 'MIT Users' },
{ label: 'Submit a Sample', href: 'http://biomicro.mit.edu/submit.html' },
{ label: 'MIT Pricing', href: 'https://bmcwiki.mit.edu/index.php/MIT:Pricing' },
{ divider: true },
{ groupLabel: 'Non-MIT Users' },
{ label: 'External Submission', href: 'http://biomicro.mit.edu/submit.html' },
{ label: 'External Pricing', href: 'http://biomicro.mit.edu/pricing_v2.html' }
]
},
{
label: 'Staff',
href: 'http://biomicro.mit.edu/staff.html',
match: [ 'Staff' ]
},
{
label: 'Resources',
href: 'https://bmcwiki.mit.edu/index.php/BioMicroCenter:FAQ',
match: [ 'FAQ', 'Resources', 'Forms', 'Acknowledgement' ],
dropdown: [
{ label: 'FAQs', href: 'https://bmcwiki.mit.edu/index.php/BioMicroCenter:FAQ' },
{ label: 'Forms', href: 'http://biomicro.mit.edu/forms.html' },
{ label: 'Grant Support & Pricing', href: 'http://biomicro.mit.edu/pricing_v2.html' },
{ label: 'Acknowledgements', href: 'https://bmcwiki.mit.edu/index.php/BioMicroCenter:Acknowledgement' }
]
}
];


/* ── Helper: create element with attrs & children ──────────── */
    /* Scope to the sidebar container to exclude Vector 2022 header user-menu portlets
function el( tag, attrs, children ) {
      (p-vector-user-menu-userpage, p-user-menu-logout, p-personal-sticky-header, etc.) */
var node = document.createElement( tag );
    var navContainer = document.getElementById( 'vector-main-menu' ) ||
Object.keys( attrs || {} ).forEach( function ( k ) {
                      document.getElementById( 'mw-panel' )         ||
if ( k === 'className' ) {
                      document.getElementById( 'mw-navigation' )   ||
node.className = attrs[ k ];
                      document.body;
} else if ( k === 'textContent' ) {
node.textContent = attrs[ k ];
} else if ( k === 'innerHTML' ) {
node.innerHTML = attrs[ k ];
} else {
node.setAttribute( k, attrs[ k ] );
}
} );
( children || [] ).forEach( function ( c ) {
if ( c ) node.appendChild( c );
} );
return node;
}


/* ── 1. Inject Topbar ───────────────────────────────────────── */
    var portlets = Array.prototype.slice.call(
function injectTopbar() {
      navContainer.querySelectorAll( '[id^="p-"]' )
var topbar = el( 'div', { className: 'bmc-topbar' }, [
    );
el( 'div', { className: 'bmc-topbar-inner' }, [
el( 'a', { href: 'mailto:biomicro@mit.edu', textContent: 'biomicro@mit.edu' } ),
el( 'span', { textContent: '|' } ),
document.createTextNode( '617-715-4533' ),
el( 'span', { textContent: '|' } ),
document.createTextNode( 'Building 68-322' )
] )
] );


// Insert before the MediaWiki header element
    var nav = [];
var mwHead = document.getElementById( 'mw-head' ) ||
    var currentUrl = window.location.href.split( '?' )[0];
document.querySelector( '.vector-header' );
if ( mwHead && mwHead.parentNode ) {
mwHead.parentNode.insertBefore( topbar, mwHead );
} else {
document.body.insertBefore( topbar, document.body.firstChild );
}


// Adjust sticky header top offset to account for topbar height
    portlets.forEach( function ( portlet ) {
var topbarH = topbar.offsetHeight;
      if ( SKIP_IDS[ portlet.id ] ) return;
var style = document.createElement( 'style' );
style.textContent =
'#mw-head, .vector-header, .vector-header-container { top: ' + topbarH + 'px !important; }' +
'.bmc-page-hero { margin-top: 0; }';
document.head.appendChild( style );
}


/* ── 2. Inject Horizontal Nav ───────────────────────────────── */
      var links = Array.prototype.slice.call(
function buildNavItem( item, currentPage ) {
        portlet.querySelectorAll( 'li a' )
var li = document.createElement( 'li' );
      ).map( function ( a ) {
        return { label: a.textContent.trim(), href: a.href };
      } ).filter( function ( item ) { return item.label && item.href; } );


// Check if this nav item is active
      if ( !links.length ) return;
var isActive = ( item.match || [] ).some( function ( m ) {
return currentPage.indexOf( m ) !== -1;
} );
if ( isActive ) li.classList.add( 'bmc-active' );


var link = el( 'a', { href: item.href, textContent: item.label } );
      var headingEl = portlet.querySelector( '[id$="-label"], h3, h2' );
li.appendChild( link );
      var sectionLabel = headingEl ? headingEl.textContent.trim() : '';


if ( item.dropdown && item.dropdown.length ) {
      var isActive = links.some( function ( link ) {
li.classList.add( 'bmc-dropdown' );
        return link.href.split( '?' )[0] === currentUrl;
var menu = el( 'div', { className: 'bmc-dropdown-menu' } );
      } );


item.dropdown.forEach( function ( d ) {
      if ( links.length === 1 ) {
if ( d.divider ) {
        nav.push( {
menu.appendChild( el( 'div', { className: 'bmc-divider' } ) );
          label:   links[0].label,
} else if ( d.groupLabel ) {
          href:    links[0].href,
menu.appendChild( el( 'div', {
          isActive: links[0].href.split( '?' )[0] === currentUrl
className: 'bmc-group-label',
        } );
textContent: d.groupLabel
      } else {
} ) );
        nav.push( {
} else {
          label:    sectionLabel || links[0].label,
var a = el( 'a', { href: d.href, textContent: d.label } );
          href:     links[0].href,
if ( d.external ) a.setAttribute( 'target', '_blank' );
          items:   links,
menu.appendChild( a );
          isActive: isActive
}
        } );
} );
      }
    } );


li.appendChild( menu );
    return nav;
}
  }


return li;
  /* ── 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;
  }


function injectHorizontalNav() {
  /* ── Build nav dropdown item ───────────────────────────────── */
var currentPage = mw.config.get( 'wgPageName' ) || '';
  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;
  }


var ul = document.createElement( 'ul' );
  /* ── Build full header ─────────────────────────────────────── */
NAV_ITEMS.forEach( function ( item ) {
  function buildHeader( currentPage, nav ) {
ul.appendChild( buildNavItem( item, currentPage ) );
    /* 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" />';


var nav = el( 'nav', { className: 'bmc-hnav' }, [ ul ] );
    /* 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' );


// Find or create the header inner wrapper
      var a = document.createElement( 'a' );
var mwHead = document.getElementById( 'mw-head' ) ||
      a.href = item.page ? mw.util.getUrl( item.page ) : ( item.href || '#' );
document.querySelector( '.vector-header' );
      a.textContent = item.label;
if ( !mwHead ) return;
      li.appendChild( a );


// Find or create a flex inner container
      if ( item.items ) {
var inner = mwHead.querySelector( '.bmc-header-inner' );
        li.appendChild( buildDropdownMenu( item.items ) );
if ( !inner ) {
      }
inner = el( 'div', { className: 'bmc-header-inner' } );
      ul.appendChild( li );
// Move existing header children into inner (except topbar)
    } );
Array.from( mwHead.childNodes ).forEach( function ( child ) {
inner.appendChild( child );
} );
mwHead.appendChild( inner );
}


// Build logo
    var navEl = document.createElement( 'nav' );
var logoLink = el( 'a', {
    navEl.className = 'bmc-nav';
href: 'https://bmcwiki.mit.edu/index.php/BioMicroCenter',
    navEl.appendChild( ul );
className: 'bmc-logo'
}, [
el( 'div', { className: 'bmc-logo-mark', textContent: 'BMC' } ),
el( 'div', { className: 'bmc-logo-text' }, [
el( 'span', { className: 'bmc-logo-name', textContent: 'MIT BioMicro Center' } ),
el( 'span', { className: 'bmc-logo-sub', textContent: 'Integrated Genomics Core Facility' } )
] )
] );


// Clear inner and rebuild with logo + nav
    /* Search */
inner.innerHTML = '';
    var searchForm = document.createElement( 'form' );
inner.appendChild( logoLink );
    searchForm.className = 'bmc-search-form';
inner.appendChild( nav );
    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>';


/* ── 3. Inject Page Hero ────────────────────────────────────── */
    /* User info — shown only when logged in */
function injectPageHero() {
    var userName = mw.config.get( 'wgUserName' );
// Get page title from MediaWiki's firstHeading
    var userEl = null;
var titleEl = document.getElementById( 'firstHeading' ) ||
    if ( userName ) {
document.querySelector( '.mw-first-heading' );
      userEl = document.createElement( 'span' );
var titleText = titleEl ? titleEl.textContent.trim() : mw.config.get( 'wgTitle' );
      userEl.className = 'bmc-user-info';
      userEl.textContent = userName;
    }


// Build breadcrumb
    var right = document.createElement( 'div' );
var crumb = el( 'div', { className: 'bmc-breadcrumb' }, [
    right.className = 'bmc-header-right';
el( 'a', { href: 'http://biomicro.mit.edu', textContent: 'Home' } ),
    right.appendChild( navEl );
el( 'span', { textContent: '›' } ),
    right.appendChild( searchForm );
document.createTextNode( titleText )
    if ( userEl ) { right.appendChild( userEl ); }
] );


var heroH1 = el( 'h1', { textContent: titleText } );
    /* Inner wrapper */
    var inner = document.createElement( 'div' );
    inner.className = 'bmc-header-inner';
    inner.appendChild( logo );
    inner.appendChild( right );


var heroInner = el( 'div', { className: 'bmc-hero-inner' }, [ crumb, heroH1 ] );
    var header = document.createElement( 'header' );
var hero = el( 'div', { className: 'bmc-page-hero' }, [ heroInner ] );
    header.className = 'bmc-header';
    header.appendChild( inner );
    return header;
  }


// Insert hero between header and content
  /* ── Build page hero ───────────────────────────────────────── */
var mwHead = document.getElementById( 'mw-head' ) ||
  function buildHero( titleText ) {
document.querySelector( '.vector-header' );
    var crumb = document.createElement( 'div' );
var content = document.getElementById( 'content' ) ||
    crumb.className = 'bmc-breadcrumb';
document.querySelector( '.mw-body' ) ||
    crumb.innerHTML =
document.querySelector( '.vector-body' );
      '<a href="' + mw.util.getUrl( 'BioMicroCenter' ) + '">Home</a>' +
      '<span class="sep">›</span>' +
      document.createTextNode( titleText ).textContent; // plain text, no XSS


if ( mwHead && mwHead.parentNode ) {
    var h1 = document.createElement( 'h1' );
var ref = mwHead.nextSibling;
    h1.textContent = titleText;
mwHead.parentNode.insertBefore( hero, ref );
} else if ( content && content.parentNode ) {
content.parentNode.insertBefore( hero, content );
}
}


/* ── 4. Rewrite Footer ──────────────────────────────────────── */
    var inner = document.createElement( 'div' );
function rewriteFooter() {
    inner.className = 'inner';
var footer = document.getElementById( 'footer' ) ||
    inner.appendChild( crumb );
document.querySelector( '.mw-footer' );
    inner.appendChild( h1 );
if ( !footer ) return;


var inner = el( 'div', { className: 'bmc-footer-inner' }, [
    var hero = document.createElement( 'div' );
// Col 1: address
    hero.className = 'bmc-page-hero';
el( 'div', {}, [
    hero.appendChild( inner );
el( 'h4', { textContent: 'MIT BioMicro Center' } ),
    return hero;
( function () {
  }
var ul = document.createElement( 'ul' );
[ 'Building 68-322', 'Cambridge, MA 02139' ].forEach( function ( t ) {
ul.appendChild( el( 'li', { textContent: t } ) );
} );
var emailLi = document.createElement( 'li' );
emailLi.appendChild( el( 'a', {
href: 'mailto:biomicro@mit.edu',
textContent: 'biomicro@mit.edu'
} ) );
ul.appendChild( emailLi );
ul.appendChild( el( 'li', { textContent: '617-715-4533' } ) );
return ul;
} )()
] ),
// Col 2: services
el( 'div', {}, [
el( 'h4', { textContent: 'Services' } ),
( function () {
var ul = document.createElement( 'ul' );
[
{ label: 'Bulk Sequencing', href: 'https://bmcwiki.mit.edu/index.php/BioMicroCenter:Assisted_Services' },
{ label: 'Single Cell', href: 'https://bmcwiki.mit.edu/index.php/BioMicroCenter:Assisted_Services' },
{ label: 'Spatial Genomics', href: 'https://bmcwiki.mit.edu/index.php/BioMicroCenter:Walkup_Instrumentation' },
{ label: 'Informatics', href: 'https://igb.mit.edu/' }
].forEach( function ( item ) {
var li = document.createElement( 'li' );
li.appendChild( el( 'a', { href: item.href, textContent: item.label } ) );
ul.appendChild( li );
} );
return ul;
} )()
] ),
// Col 3: resources
el( 'div', {}, [
el( 'h4', { textContent: 'Resources' } ),
( function () {
var ul = document.createElement( 'ul' );
[
{ label: 'FAQs', href: 'https://bmcwiki.mit.edu/index.php/BioMicroCenter:FAQ' },
{ label: 'Forms', href: 'http://biomicro.mit.edu/forms.html' },
{ label: 'Grant Support & Pricing', href: 'http://biomicro.mit.edu/pricing_v2.html' },
{ label: 'Acknowledgements', href: 'https://bmcwiki.mit.edu/index.php/BioMicroCenter:Acknowledgement' }
].forEach( function ( item ) {
var li = document.createElement( 'li' );
li.appendChild( el( 'a', { href: item.href, textContent: item.label } ) );
ul.appendChild( li );
} );
return ul;
} )()
] )
] );


var bottom = el( 'div', { className: 'bmc-footer-bottom' }, [
  /* ── Build sidebar TOC from MediaWiki #toc or heading scan ─── */
el( 'span', { textContent: '© ' + new Date().getFullYear() + ' MIT BioMicro Center' } ),
  function buildSidebarToc( mwToc, contentEl ) {
el( 'span', {}, [
    var toc = document.createElement( 'div' );
el( 'a', {
    toc.className = 'bmc-toc';
href: 'https://accessibility.mit.edu',
target: '_blank',
textContent: 'Accessibility'
} )
] )
] );


footer.innerHTML = '';
    var h3 = document.createElement( 'h3' );
footer.appendChild( inner );
    h3.textContent = 'On this page';
footer.appendChild( bottom );
    toc.appendChild( h3 );
}


/* ── 5. Smooth Scroll ───────────────────────────────────────── */
    var list;
function initSmoothScroll() {
var content = document.querySelector( '.mw-parser-output' );
if ( !content ) return;
content.addEventListener( 'click', function ( e ) {
var a = e.target.closest( 'a[href^="#"]' );
if ( !a ) return;
var target = document.getElementById(
decodeURIComponent( a.getAttribute( 'href' ).slice( 1 ) )
);
if ( target ) {
e.preventDefault();
target.scrollIntoView( { behavior: 'smooth', block: 'start' } );
history.pushState( null, '', a.getAttribute( 'href' ) );
}
} );
}


/* ── 6. Auto-style plain tables ─────────────────────────────── */
    if ( mwToc ) {
function styleUnstyledTables() {
      /* Clone only the list from the MediaWiki TOC */
var tables = document.querySelectorAll(
      var mwList = mwToc.querySelector( 'ul' );
'.mw-parser-output table:not(.wikitable):not(.infobox):not(.navbox):not(.toc)'
      if ( mwList ) {
);
        list = mwList.cloneNode( true );
tables.forEach( function ( t ) { t.classList.add( 'wikitable' ); } );
        /* 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 );
        } );
      }
    }


/* ── Entry point ────────────────────────────────────────────── */
    if ( !list || list.childNodes.length === 0 ) return null;
mw.hook( 'wikipage.content' ).add( function () {
 
injectTopbar();
    toc.appendChild( list );
injectHorizontalNav();
 
injectPageHero();
    var aside = document.createElement( 'aside' );
rewriteFooter();
    aside.className = 'bmc-sidebar';
initSmoothScroll();
    aside.appendChild( toc );
styleUnstyledTables();
    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 );
  } );


} )();
} )();

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 );
  } );

} )();