MediaWiki:Common.js
From HADES
Note: After publishing, you may have to bypass your browser's cache to see the changes.
- Firefox / Safari: Hold Shift while clicking Reload, or press either Ctrl-F5 or Ctrl-R (⌘-R on a Mac)
- Google Chrome: Press Ctrl-Shift-R (⌘-Shift-R on a Mac)
- Edge: Hold Ctrl while clicking Refresh, or press Ctrl-F5.
/**
* ╔═══════════════════════════════════════════════════════════════╗
* ║ HADES WIKI — MediaWiki Theme JavaScript ║
* ║ The Free Encyclopedia of the Underworld ║
* ║ Based on: Hades (2026) by Melanie Martinez ║
* ║ Install in: MediaWiki:Common.js ║
* ╚═══════════════════════════════════════════════════════════════╝
*/
(function () {
'use strict';
/* ─────────────────────────────────────────
CONFIGURATION
───────────────────────────────────────── */
const HADES_CONFIG = {
candleCount: 4,
particleCount: 15,
particleColors: ['#c8922a', '#ff8800', '#ffcc44', '#ff6600', '#ffd700'],
goldColor: '#c8922a',
pageFadeInDelay: 100,
navHighlightDelay: 200,
siteSubtitle: 'The Free Encyclopedia of the Underworld',
};
/* ─────────────────────────────────────────
UTILITY: Wait for DOM element
───────────────────────────────────────── */
function waitFor(selector, callback, timeout = 5000) {
const el = document.querySelector(selector);
if (el) return callback(el);
const start = Date.now();
const interval = setInterval(() => {
const found = document.querySelector(selector);
if (found || Date.now() - start > timeout) {
clearInterval(interval);
if (found) callback(found);
}
}, 100);
}
/* ─────────────────────────────────────────
1. ANIMATED CANDLES IN HEADER
───────────────────────────────────────── */
function injectCandles() {
const header = document.querySelector('#mw-head, .mw-header, .vector-header');
if (!header) return;
// Remove existing candle container if present
const existing = document.getElementById('hades-candles');
if (existing) existing.remove();
const container = document.createElement('div');
container.id = 'hades-candles';
container.style.cssText = `
display: flex;
align-items: flex-end;
gap: 6px;
padding: 8px 16px;
position: absolute;
right: 80px;
top: 50%;
transform: translateY(-50%);
z-index: 50;
pointer-events: none;
`;
const candleHeights = [50, 75, 60, 42];
const candleDelays = [0.3, 0, 0.6, 1.0];
candleHeights.forEach((height, i) => {
const candleEl = createCandleElement(height, candleDelays[i]);
container.appendChild(candleEl);
});
header.style.position = 'relative';
header.appendChild(container);
}
function createCandleElement(height, delay) {
const wrapper = document.createElement('div');
wrapper.style.cssText = 'display:flex;flex-direction:column;align-items:center;position:relative;';
// Smoke
const smoke = document.createElement('div');
smoke.style.cssText = `
position:absolute;
bottom:${height + 36}px;
left:50%;
width:4px;
height:10px;
background:rgba(200,180,150,0.25);
border-radius:50%;
transform:translateX(-50%);
animation:hadesSmoke ${2 + delay * 0.5}s ${delay * 0.3}s ease-out infinite;
`;
wrapper.appendChild(smoke);
// Outer flame
const outerFlame = document.createElement('div');
outerFlame.style.cssText = `
width:14px;
height:22px;
background:radial-gradient(ellipse at 50% 80%,#ff5500 0%,#ff9900 40%,#ffcc00 70%,rgba(255,220,100,0) 100%);
border-radius:50% 50% 30% 30%/60% 60% 40% 40%;
animation:hadesCandle ${1.4 + delay * 0.3}s ${delay * 0.2}s ease-in-out infinite;
transform-origin:bottom center;
filter:blur(0.5px);
margin-bottom:0;
`;
wrapper.appendChild(outerFlame);
// Inner flame
const innerFlame = document.createElement('div');
innerFlame.style.cssText = `
width:5px;
height:13px;
background:radial-gradient(ellipse at 50% 80%,#fff 0%,#ffe066 50%,rgba(255,200,0,0) 100%);
border-radius:50% 50% 30% 30%/60% 60% 40% 40%;
animation:hadesCandle ${1.1 + delay * 0.2}s ${delay * 0.15}s ease-in-out infinite;
transform-origin:bottom center;
margin-top:-13px;
align-self:center;
`;
wrapper.appendChild(innerFlame);
// Glow orb
const glow = document.createElement('div');
glow.style.cssText = `
width:22px;
height:22px;
border-radius:50%;
background:rgba(255,160,50,0.1);
animation:hadesGlowOrb ${2.2 + delay * 0.4}s ${delay * 0.1}s ease-in-out infinite;
position:absolute;
top:0;
left:50%;
transform:translateX(-50%);
`;
wrapper.appendChild(glow);
// Wick
const wick = document.createElement('div');
wick.style.cssText = 'width:2px;height:5px;background:#1a0800;margin-bottom:-1px;position:relative;z-index:2;';
wrapper.appendChild(wick);
// Wax body
const waxColors = ['#d4c4a0', '#e8d8b0', '#ddd0a8', '#c8b890'];
const wax = document.createElement('div');
wax.style.cssText = `
width:16px;
height:${height}px;
background:linear-gradient(to right,${waxColors[Math.floor(Math.random() * waxColors.length)]}bb 0%,${waxColors[0]} 35%,${waxColors[0]} 70%,${waxColors[0]}88 100%);
border-radius:2px 2px 3px 3px;
position:relative;
box-shadow:inset -3px 0 6px rgba(0,0,0,0.2),inset 3px 0 3px rgba(255,255,255,0.08);
`;
// Wax drip 1
const drip1 = document.createElement('div');
drip1.style.cssText = `
position:absolute;top:0;left:30%;width:4px;
background:inherit;border-radius:0 0 3px 3px;
animation:hadesWaxDrip ${4 + delay}s ${delay + 0.5}s ease-in-out infinite alternate;
height:10px;opacity:0.6;
`;
wax.appendChild(drip1);
// Wax drip 2
const drip2 = document.createElement('div');
drip2.style.cssText = `
position:absolute;top:0;right:25%;width:3px;
background:inherit;border-radius:0 0 3px 3px;
animation:hadesWaxDrip ${5 + delay}s ${delay + 1.5}s ease-in-out infinite alternate;
height:7px;opacity:0.5;
`;
wax.appendChild(drip2);
wrapper.appendChild(wax);
return wrapper;
}
/* ─────────────────────────────────────────
2. INJECT EXTRA CSS KEYFRAMES (JS version)
───────────────────────────────────────── */
function injectExtraStyles() {
const style = document.createElement('style');
style.id = 'hades-js-styles';
style.textContent = `
@keyframes hadesSmoke {
0% { opacity: 0.4; transform: translateX(-50%) translateY(0) scaleX(1); }
50% { opacity: 0.2; transform: translateX(-50%) translateY(-16px) scaleX(1.5); }
100% { opacity: 0; transform: translateX(-50%) translateY(-32px) scaleX(2); }
}
@keyframes hadesGlowOrb {
0%,100% { box-shadow:0 0 8px 3px rgba(255,160,50,0.4),0 0 20px 8px rgba(200,100,20,0.2); opacity:0.8; }
50% { box-shadow:0 0 14px 6px rgba(255,190,70,0.7),0 0 35px 14px rgba(210,130,35,0.4); opacity:1; }
}
@keyframes hadesWaxDrip {
0% { height:0px; opacity:0.8; }
100% { height:18px; opacity:0.3; }
}
@keyframes hadesParticleAnim {
0% { transform:translateY(0) translateX(0) rotate(0deg) scale(1); opacity:0.9; }
50% { transform:translateY(-50vh) translateX(30px) rotate(180deg) scale(0.5); opacity:0.4; }
100% { transform:translateY(-100vh) translateX(-20px) rotate(360deg) scale(0.1); opacity:0; }
}
@keyframes hadesPageReveal {
from { opacity:0; transform:translateY(16px); }
to { opacity:1; transform:translateY(0); }
}
@keyframes hadesNavLine {
from { width:0; left:50%; }
to { width:100%; left:0; }
}
@keyframes hadesTitlePulse {
0%,100% { filter:brightness(1); }
50% { filter:brightness(1.15) drop-shadow(0 0 8px rgba(200,146,42,0.5)); }
}
@keyframes hadesLinkShimmer {
0% { background-position:-200% center; }
100% { background-position:200% center; }
}
@keyframes hadesScrollIndicator {
0%,100% { opacity:0.4; transform:translateY(0); }
50% { opacity:1; transform:translateY(4px); }
}
@keyframes hadesCrownFloat {
0%,100% { transform:translateY(0) rotate(-2deg); filter:drop-shadow(0 0 6px rgba(200,146,42,0.5)); }
50% { transform:translateY(-5px) rotate(2deg); filter:drop-shadow(0 0 12px rgba(200,146,42,0.8)); }
}
@keyframes hadesSubtitleReveal {
from { opacity:0; letter-spacing:8px; }
to { opacity:1; letter-spacing:5px; }
}
/* Hover glow for wiki links */
.hades-link-glow {
position:relative;
transition:all 0.3s ease !important;
}
.hades-link-glow:hover {
text-shadow:0 0 12px rgba(200,146,42,0.6) !important;
}
/* Section reveal animation */
.hades-section-reveal {
animation:hadesPageReveal 0.6s ease forwards;
opacity:0;
}
/* Candle hover effect */
#hades-candles:hover {
filter:brightness(1.2);
}
/* Ornament spin */
.hades-spinning-ornament {
display:inline-block;
animation:hadesOrnamentSpin 8s linear infinite;
}
/* Progress bar shimmer */
.hades-progress {
height:2px;
background:linear-gradient(to right, transparent, #c8922a, transparent);
position:fixed;
top:0;
left:0;
z-index:9999;
transition:width 0.3s ease;
}
`;
document.head.appendChild(style);
}
/* ─────────────────────────────────────────
3. FLOATING EMBER PARTICLES
───────────────────────────────────────── */
function createParticles() {
const container = document.createElement('div');
container.id = 'hades-particles';
container.style.cssText = `
position:fixed;
inset:0;
pointer-events:none;
z-index:1;
overflow:hidden;
`;
for (let i = 0; i < HADES_CONFIG.particleCount; i++) {
const p = document.createElement('div');
const size = 1.5 + Math.random() * 2.5;
const color = HADES_CONFIG.particleColors[Math.floor(Math.random() * HADES_CONFIG.particleColors.length)];
const left = Math.random() * 100;
const delay = Math.random() * 8;
const duration = 6 + Math.random() * 8;
p.style.cssText = `
position:absolute;
bottom:0;
left:${left}%;
width:${size}px;
height:${size}px;
border-radius:50%;
background:radial-gradient(circle, ${color} 0%, rgba(200,100,20,0.2) 70%, transparent 100%);
animation:hadesParticleAnim ${duration}s ${delay}s ease-out infinite;
opacity:0;
`;
container.appendChild(p);
}
document.body.prepend(container);
}
/* ─────────────────────────────────────────
4. SITE TITLE ENHANCEMENT
───────────────────────────────────────── */
function enhanceSiteTitle() {
// Add animated crown to logo area
const logo = document.querySelector('#p-logo, .mw-logo, #mw-head-base');
if (logo) {
const crown = document.createElement('div');
crown.style.cssText = `
font-size:28px;
text-align:center;
animation:hadesCrownFloat 3s ease-in-out infinite;
filter:drop-shadow(0 0 8px rgba(200,146,42,0.6));
margin-bottom:4px;
display:block;
color:#c8922a;
`;
crown.textContent = '♛';
logo.prepend(crown);
}
// Animate site subtitle
const subtitle = document.querySelector('#p-logo a[title], .mw-wiki-title');
if (subtitle) {
const sub = document.createElement('div');
sub.textContent = HADES_CONFIG.siteSubtitle;
sub.style.cssText = `
font-family:'Cinzel',serif;
font-size:8px;
letter-spacing:5px;
color:#8b4a20;
text-transform:uppercase;
text-align:center;
animation:hadesSubtitleReveal 2s ease forwards;
opacity:0;
margin-top:2px;
`;
subtitle.parentNode.insertBefore(sub, subtitle.nextSibling);
}
}
/* ─────────────────────────────────────────
5. PAGE CONTENT ANIMATIONS
───────────────────────────────────────── */
function animatePageContent() {
const content = document.querySelector('#mw-content-text, .mw-body, #content');
if (content) {
content.style.animation = 'hadesPageReveal 0.8s 0.1s ease forwards';
content.style.opacity = '0';
}
// Animate each heading with stagger
const headings = document.querySelectorAll('h2, h3');
headings.forEach((h, i) => {
h.style.animation = `hadesPageReveal 0.6s ${0.1 + i * 0.08}s ease forwards`;
h.style.opacity = '0';
});
// Animate infobox
const infobox = document.querySelector('.infobox');
if (infobox) {
infobox.style.animation = 'hadesPageReveal 0.8s 0.3s ease forwards';
infobox.style.opacity = '0';
}
}
/* ─────────────────────────────────────────
6. ENHANCED LINK INTERACTIONS
───────────────────────────────────────── */
function enhanceLinks() {
// Add ripple effect to all article links
document.querySelectorAll('#mw-content-text a, .mw-body a').forEach(link => {
link.classList.add('hades-link-glow');
link.addEventListener('mouseenter', function () {
this.style.transition = 'all 0.2s ease';
});
link.addEventListener('click', function (e) {
const ripple = document.createElement('span');
ripple.style.cssText = `
position:absolute;
width:12px;
height:12px;
background:rgba(200,146,42,0.6);
border-radius:50%;
pointer-events:none;
animation:hadesRipple 0.5s ease forwards;
transform:translate(-50%,-50%);
top:${e.offsetY}px;
left:${e.offsetX}px;
z-index:9;
`;
this.style.position = 'relative';
this.style.overflow = 'hidden';
this.appendChild(ripple);
setTimeout(() => ripple.remove(), 500);
});
});
}
/* ─────────────────────────────────────────
7. NAVIGATION ACTIVE INDICATOR
───────────────────────────────────────── */
function enhanceNavigation() {
const navLinks = document.querySelectorAll(
'#p-namespaces ul li a, #p-views ul li a, .vector-menu-tabs ul li a'
);
navLinks.forEach(link => {
link.addEventListener('mouseenter', function () {
const indicator = document.createElement('div');
indicator.style.cssText = `
position:absolute;
bottom:0;
left:0;
height:2px;
background:linear-gradient(to right, transparent, #c8922a, transparent);
animation:hadesNavLine 0.25s ease forwards;
pointer-events:none;
`;
this.style.position = 'relative';
this.appendChild(indicator);
});
link.addEventListener('mouseleave', function () {
const indicator = this.querySelector('div[style*="hadesNavLine"]');
if (indicator) indicator.remove();
});
});
}
/* ─────────────────────────────────────────
8. SCROLL PROGRESS BAR
───────────────────────────────────────── */
function createScrollProgressBar() {
const bar = document.createElement('div');
bar.className = 'hades-progress';
bar.style.width = '0%';
document.body.appendChild(bar);
window.addEventListener('scroll', () => {
const scrollTop = window.scrollY;
const docHeight = document.documentElement.scrollHeight - window.innerHeight;
const progress = docHeight > 0 ? (scrollTop / docHeight) * 100 : 0;
bar.style.width = progress + '%';
}, { passive: true });
}
/* ─────────────────────────────────────────
9. TABLE ENHANCEMENTS
───────────────────────────────────────── */
function enhanceTables() {
document.querySelectorAll('.wikitable tr').forEach((row, i) => {
row.addEventListener('mouseenter', function () {
this.style.background = 'rgba(200,146,42,0.08)';
this.style.transition = 'background 0.2s ease';
this.querySelectorAll('td').forEach(td => {
td.style.color = '#d4c4a0';
td.style.transition = 'color 0.2s ease';
});
});
row.addEventListener('mouseleave', function () {
this.style.background = '';
this.querySelectorAll('td').forEach(td => { td.style.color = ''; });
});
});
}
/* ─────────────────────────────────────────
10. SEARCH BAR ENHANCEMENTS
───────────────────────────────────────── */
function enhanceSearch() {
const searchInput = document.querySelector('#searchInput, .vector-search-box input[type="search"]');
if (!searchInput) return;
searchInput.placeholder = 'Search HADES Wiki...';
searchInput.addEventListener('focus', function () {
this.parentElement.style.boxShadow = '0 0 0 3px rgba(200,146,42,0.2), 0 4px 20px rgba(0,0,0,0.5)';
this.style.borderColor = '#c8922a';
});
searchInput.addEventListener('blur', function () {
this.parentElement.style.boxShadow = '';
this.style.borderColor = '';
});
// Add search icon label
const label = document.createElement('span');
label.textContent = '🔍';
label.style.cssText = `
position:absolute;
left:12px;
top:50%;
transform:translateY(-50%);
pointer-events:none;
font-size:14px;
z-index:5;
`;
const searchWrapper = searchInput.parentElement;
if (searchWrapper) {
searchWrapper.style.position = 'relative';
searchWrapper.prepend(label);
}
}
/* ─────────────────────────────────────────
11. FIRST HEADING SUBTITLE
───────────────────────────────────────── */
function enhanceFirstHeading() {
const h1 = document.querySelector('#firstHeading, h1.firstHeading');
if (!h1) return;
// Add decorative ornaments around h1
h1.innerHTML = h1.innerHTML;
const sub = document.createElement('div');
sub.style.cssText = `
font-family:'IM Fell English',serif;
font-style:italic;
font-size:13px;
color:#8b7a50;
margin-top:4px;
letter-spacing:0.5px;
`;
// Add page metadata
const pageName = h1.textContent.trim();
const ns = mw.config.get('wgNamespaceNumber');
if (ns === 0 && pageName) {
// Main article namespace
sub.textContent = `Article · Hades Wiki`;
h1.parentNode.insertBefore(sub, h1.nextSibling);
}
}
/* ─────────────────────────────────────────
12. FOOTER ENHANCEMENT
───────────────────────────────────────── */
function enhanceFooter() {
const footer = document.querySelector('#footer, .mw-footer');
if (!footer) return;
const logo = document.createElement('div');
logo.style.cssText = `
font-family:'Cinzel Decorative',serif;
font-size:16px;
color:#8b6914;
letter-spacing:4px;
margin-bottom:12px;
text-align:center;
animation:hadesTitlePulse 4s ease-in-out infinite;
`;
logo.textContent = '⚜ HADES WIKI ⚜';
footer.prepend(logo);
const divider = document.createElement('div');
divider.style.cssText = `
height:1px;
background:linear-gradient(to right, transparent, #c8922a, transparent);
margin:12px 0;
opacity:0.4;
`;
footer.prepend(divider);
}
/* ─────────────────────────────────────────
13. IMAGE LIGHTBOX (simple)
───────────────────────────────────────── */
function initLightbox() {
document.querySelectorAll('.thumbimage img, .infobox img').forEach(img => {
img.style.cursor = 'zoom-in';
img.addEventListener('click', function (e) {
e.preventDefault();
const overlay = document.createElement('div');
overlay.style.cssText = `
position:fixed;
inset:0;
background:rgba(0,0,0,0.92);
z-index:99999;
display:flex;
align-items:center;
justify-content:center;
cursor:zoom-out;
animation:hadesPageReveal 0.3s ease forwards;
`;
const clone = this.cloneNode();
clone.style.cssText = `
max-width:90vw;
max-height:90vh;
object-fit:contain;
border:1px solid rgba(200,146,42,0.4);
box-shadow:0 0 60px rgba(200,146,42,0.2),0 0 120px rgba(0,0,0,0.8);
`;
const closeBtn = document.createElement('div');
closeBtn.textContent = '✕';
closeBtn.style.cssText = `
position:absolute;
top:20px;
right:24px;
color:#c8922a;
font-size:24px;
cursor:pointer;
font-family:Cinzel,serif;
letter-spacing:2px;
opacity:0.8;
transition:opacity 0.2s;
`;
closeBtn.addEventListener('mouseenter', () => closeBtn.style.opacity = '1');
const close = () => {
overlay.style.opacity = '0';
overlay.style.transition = 'opacity 0.3s ease';
setTimeout(() => overlay.remove(), 300);
};
overlay.addEventListener('click', close);
closeBtn.addEventListener('click', close);
overlay.appendChild(clone);
overlay.appendChild(closeBtn);
document.body.appendChild(overlay);
});
});
}
/* ─────────────────────────────────────────
14. MAIN EXECUTION (mw.loader.using)
───────────────────────────────────────── */
function init() {
injectExtraStyles();
createParticles();
injectCandles();
createScrollProgressBar();
animatePageContent();
enhanceLinks();
enhanceNavigation();
enhanceTables();
enhanceSearch();
enhanceFooter();
initLightbox();
// Slight delay for heading enhancement (needs mw object)
setTimeout(() => {
try { enhanceFirstHeading(); } catch (e) { /* not critical */ }
}, 500);
console.log('%c⚜ HADES WIKI THEME LOADED ⚜', 'color:#c8922a;font-family:Cinzel,serif;font-size:14px;letter-spacing:3px;');
}
// Use MediaWiki's jQuery.ready or plain DOMContentLoaded
if (typeof jQuery !== 'undefined' && typeof mw !== 'undefined') {
mw.loader.using(['mediawiki.util'], function () {
jQuery(document).ready(function () {
init();
});
});
} else {
document.addEventListener('DOMContentLoaded', init);
}
})();