MediaWiki:Common.js
Appearance
Note: After publishing, you may have to bypass your browser's cache to see the changes.
- Firefox / Safari: Hold Shift while clicking Reload, or press either Ctrl-F5 or Ctrl-R (⌘-R on a Mac)
- Google Chrome: Press Ctrl-Shift-R (⌘-Shift-R on a Mac)
- Edge: Hold Ctrl while clicking Refresh, or press Ctrl-F5.
/* Any JavaScript here will be loaded for all users on every page load. */
// Tab functionality
$(document).ready(function() {
// Initialize tabs - show first tab by default
$('.wiki-tabs-container').each(function() {
var $container = $(this);
var $firstButton = $container.find('.wiki-tab-button').first();
var firstTabId = $firstButton.data('tab');
$firstButton.addClass('active');
$container.find('#' + firstTabId).addClass('active').show();
});
// Tab click handler
$('.wiki-tab-button').on('click', function() {
var $button = $(this);
var tabId = $button.data('tab');
var $container = $button.closest('.wiki-tabs-container');
// Remove active class from all buttons and panes in this container
$container.find('.wiki-tab-button').removeClass('active');
$container.find('.wiki-tab-pane').removeClass('active').hide();
// Add active class to clicked button and corresponding pane
$button.addClass('active');
$container.find('#' + tabId).addClass('active').show();
});
});
// ===============================
// Add Custom Sticky Header Link
// ===============================
mw.hook('wikipage.content').add(function () {
const icons = document.querySelector('.vector-sticky-header-icons');
if (!icons) return;
// Prevent duplicate button on navigation
if (document.getElementById('custom-sticky-link')) return;
const link = document.createElement('a');
link.id = 'custom-sticky-link';
link.href = 'https://discord.gg/t2Rp2dRvze'; // CHANGE THIS
link.target = '_blank';
link.className = 'cdx-button cdx-button--fake-button cdx-button--fake-button--enabled cdx-button--weight-quiet';
link.textContent = 'Join Us on Discord!'; // CHANGE TEXT
link.style.marginLeft = '10px';
link.style.fontWeight = 'bold';
icons.prepend(link); // use appendChild() if you want it at the end
});
// ===============================
// Sticky Header Countdown Timer (Vector 2022) - to Apr 18, 2026 00:00 UTC
// ===============================
mw.hook('wikipage.content').add(function () {
const icons = document.querySelector('.vector-sticky-header-icons');
if (!icons) return;
// Prevent duplicates
if (document.getElementById('sticky-countdown')) return;
// Target: Sat, 18 Apr 2026 at 00:00 GMT+00:00 (UTC)
const target = new Date('2026-04-18T00:00:00Z');
const wrap = document.createElement('span');
wrap.id = 'sticky-countdown';
wrap.className = 'sticky-countdown';
const label = document.createElement('span');
label.className = 'sticky-countdown__label';
label.textContent = 'Ends in:';
const value = document.createElement('span');
value.className = 'sticky-countdown__value';
value.textContent = '--:--:--';
wrap.appendChild(label);
wrap.appendChild(value);
// Put it before the default icons (Talk/History/Edit)
icons.prepend(wrap);
const pad = (n) => String(n).padStart(2, '0');
const formatRemaining = (ms) => {
if (ms <= 0) return '00:00:00';
const totalSeconds = Math.floor(ms / 1000);
const days = Math.floor(totalSeconds / 86400);
const hours = Math.floor((totalSeconds % 86400) / 3600);
const minutes = Math.floor((totalSeconds % 3600) / 60);
const seconds = totalSeconds % 60;
if (days > 0) return `${days}d ${pad(hours)}:${pad(minutes)}:${pad(seconds)}`;
return `${pad(hours)}:${pad(minutes)}:${pad(seconds)}`;
};
const update = () => {
const now = new Date();
const diff = target.getTime() - now.getTime();
value.textContent = formatRemaining(diff);
wrap.classList.toggle('is-expired', diff <= 0);
};
update();
const timerId = window.setInterval(update, 1000);
// Cleanup if element removed (rare, but safe)
const observer = new MutationObserver(() => {
if (!document.getElementById('sticky-countdown')) {
clearInterval(timerId);
observer.disconnect();
}
});
observer.observe(document.body, { childList: true, subtree: true });
});
// ===============================
// ===============================
// Sticky Header Game Time
// Display: "Mar 1 YR1" (anchored at install time)
// Rules:
// - Each real hour = +1 in-game day number
// - Each 24 hours = +1 in-game month (Jan..Jul via 7-day cycle)
// - Each 7 months/days = +1 in-game year
// ===============================
mw.hook('wikipage.content').add(function () {
const icons = document.querySelector('.vector-sticky-header-icons');
if (!icons) return;
if (document.getElementById('sticky-game-time')) return;
const months = ['Jan','Feb','Mar','Apr','May','Jun','Jul'];
// --------- ANCHOR: "right now" is Mar 1 YR1 ----------
const ANCHOR_MONTH_INDEX = 2; // Mar (Jan=0)
const ANCHOR_DAY = 1; // Day 1
const ANCHOR_YEAR = 1; // YR1
// Optional timezone shift (0 = UTC). If you want server/local behavior, tell me your target TZ.
const GAME_TZ_OFFSET_HOURS = 0;
const getShiftedNow = () => new Date(Date.now() + GAME_TZ_OFFSET_HOURS * 3600000);
// Anchor to the current real-hour boundary so changes happen cleanly on the hour
const now = getShiftedNow();
const anchorRealHour = new Date(Date.UTC(
now.getUTCFullYear(),
now.getUTCMonth(),
now.getUTCDate(),
now.getUTCHours(), 0, 0, 0
));
// Build UI pill
const wrap = document.createElement('span');
wrap.id = 'sticky-game-time';
wrap.className = 'sticky-game-time';
const value = document.createElement('span');
value.className = 'sticky-game-time__value';
wrap.appendChild(value);
icons.prepend(wrap);
function computeGameString() {
const t = getShiftedNow();
// Total whole hours passed since anchor
const hoursPassed = Math.floor((t.getTime() - anchorRealHour.getTime()) / 3600000);
// Day number advances each hour starting from ANCHOR_DAY
const dayIndexFromAnchor = (ANCHOR_DAY - 1) + hoursPassed; // 0-based
const dayNumber = (dayIndexFromAnchor % 24) + 1;
// Every 24 hours, advance the "month/day-of-week" cycle
const daysPassed = Math.floor(dayIndexFromAnchor / 24); // 0..∞
// Total "day-of-week" index (0..6) across years
const totalDayOfWeekIndex =
(ANCHOR_YEAR - 1) * 7 + ANCHOR_MONTH_INDEX + daysPassed;
const year = Math.floor(totalDayOfWeekIndex / 7) + 1;
const month = months[((totalDayOfWeekIndex % 7) + 7) % 7];
return `${month} ${dayNumber} YR${year}`;
}
function update() {
value.textContent = computeGameString();
}
// Update now
update();
// Update exactly on the next hour boundary, then every hour
const now2 = getShiftedNow();
const msUntilNextHour =
3600000 - (now2.getUTCMinutes() * 60000 + now2.getUTCSeconds() * 1000 + now2.getUTCMilliseconds());
setTimeout(function () {
update();
setInterval(update, 3600000);
}, msUntilNextHour);
});
// ===============================
// Sticky Header Clock (timezone-stable)
// Shows the same time for everyone by forcing a timezone (UTC by default)
// ===============================
mw.hook('wikipage.content').add(function () {
const icons = document.querySelector('.vector-sticky-header-icons');
if (!icons) return;
if (document.getElementById('sticky-clock')) return;
// Choose the clock timezone:
// - "Etc/UTC" for UTC (same for all users)
// - Or "America/New_York" (same for all users, displayed in NY time)
const CLOCK_TIMEZONE = 'Etc/UTC'; // <-- change if you want a specific zone
const wrap = document.createElement('span');
wrap.id = 'sticky-clock';
wrap.className = 'sticky-clock';
const label = document.createElement('span');
label.className = 'sticky-clock__label';
label.textContent = (CLOCK_TIMEZONE === 'Etc/UTC') ? 'UTC:' : 'Time:';
const value = document.createElement('span');
value.className = 'sticky-clock__value';
wrap.appendChild(label);
wrap.appendChild(value);
// Put it near the front (use appendChild to put it at the end)
icons.prepend(wrap);
const fmt = new Intl.DateTimeFormat('en-GB', {
timeZone: CLOCK_TIMEZONE,
hour: '2-digit',
minute: '2-digit',
second: '2-digit',
hour12: false
});
function updateClock() {
value.textContent = fmt.format(new Date());
}
updateClock();
setInterval(updateClock, 1000);
});
/* =========================================================
UTOPIA HEADER INTERACTIVE ANIMATIONS - ADD TO COMMON.JS
Sleek parallax and particle effects for gaming aesthetic
========================================================= */
// Utopia Header Enhancements
mw.hook('wikipage.content').add(function () {
// Only run on pages with the header section
const headerSection = document.querySelector('.utopia-header-section');
if (!headerSection) return;
// Prevent running multiple times
if (headerSection.classList.contains('utopia-enhanced')) return;
headerSection.classList.add('utopia-enhanced');
// Check if user prefers reduced motion
const prefersReducedMotion = window.matchMedia('(prefers-reduced-motion: reduce)').matches;
// ===============================
// 1. PARALLAX MOUSE TRACKING
// ===============================
if (!prefersReducedMotion) {
const logo = headerSection.querySelector('img[src*="Utopiawiki"]');
if (logo) {
let mouseX = 0;
let mouseY = 0;
let currentX = 0;
let currentY = 0;
headerSection.addEventListener('mousemove', (e) => {
const rect = headerSection.getBoundingClientRect();
mouseX = (e.clientX - rect.left - rect.width / 2) / rect.width;
mouseY = (e.clientY - rect.top - rect.height / 2) / rect.height;
});
headerSection.addEventListener('mouseleave', () => {
mouseX = 0;
mouseY = 0;
});
function animateParallax() {
// Smooth lerp
currentX += (mouseX - currentX) * 0.1;
currentY += (mouseY - currentY) * 0.1;
// Apply subtle parallax to logo
logo.style.transform = `translate(${currentX * 15}px, ${currentY * 15}px)`;
requestAnimationFrame(animateParallax);
}
animateParallax();
}
}
// ===============================
// 2. FLOATING PARTICLES
// ===============================
if (!prefersReducedMotion) {
function createParticle() {
const particle = document.createElement('div');
particle.className = 'utopia-particle';
// Random starting position
const startX = Math.random() * 100;
const drift = (Math.random() - 0.5) * 100;
const duration = 15 + Math.random() * 10;
const delay = Math.random() * 5;
const size = 1 + Math.random() * 2;
particle.style.cssText = `
left: ${startX}%;
bottom: -10px;
width: ${size}px;
height: ${size}px;
animation-duration: ${duration}s;
animation-delay: ${delay}s;
--drift: ${drift}px;
`;
headerSection.appendChild(particle);
// Remove particle after animation completes
setTimeout(() => {
particle.remove();
}, (duration + delay) * 1000);
}
// Create initial particles
for (let i = 0; i < 15; i++) {
setTimeout(() => createParticle(), i * 1000);
}
// Continuously spawn new particles
setInterval(() => createParticle(), 3000);
}
// ===============================
// 3. HOVER RIPPLE EFFECT ON CLICKS
// ===============================
headerSection.addEventListener('click', function(e) {
if (prefersReducedMotion) return;
const ripple = document.createElement('div');
const rect = headerSection.getBoundingClientRect();
const size = Math.max(rect.width, rect.height) * 1.5;
const x = e.clientX - rect.left - size / 2;
const y = e.clientY - rect.top - size / 2;
ripple.style.cssText = `
position: absolute;
width: ${size}px;
height: ${size}px;
left: ${x}px;
top: ${y}px;
background: radial-gradient(circle, rgba(255, 195, 0, 0.15) 0%, transparent 70%);
border-radius: 50%;
pointer-events: none;
z-index: 10;
animation: rippleExpand 1s ease-out;
`;
headerSection.appendChild(ripple);
setTimeout(() => ripple.remove(), 1000);
});
// ===============================
// 4. SCROLL-BASED REVEAL (if header starts off-screen)
// ===============================
if (!prefersReducedMotion && 'IntersectionObserver' in window) {
const observer = new IntersectionObserver((entries) => {
entries.forEach(entry => {
if (entry.isIntersecting) {
entry.target.style.opacity = '1';
entry.target.style.transform = 'translateY(0)';
}
});
}, {
threshold: 0.1
});
const animatedElements = headerSection.querySelectorAll('.utopia-subtitle, .utopia-welcome, .utopia-ages');
animatedElements.forEach(el => {
// Initial state (only if not already visible)
if (el.getBoundingClientRect().top > window.innerHeight) {
el.style.opacity = '0';
el.style.transform = 'translateY(30px)';
el.style.transition = 'opacity 0.8s ease-out, transform 0.8s ease-out';
}
observer.observe(el);
});
}
});
// ===============================
// RIPPLE ANIMATION KEYFRAMES (injected once)
// ===============================
(function() {
if (document.getElementById('utopia-ripple-styles')) return;
const style = document.createElement('style');
style.id = 'utopia-ripple-styles';
style.textContent = `
@keyframes rippleExpand {
0% {
transform: scale(0);
opacity: 1;
}
100% {
transform: scale(1);
opacity: 0;
}
}
`;
document.head.appendChild(style);
})();