MediaWiki:Common.js
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.
// Load jQuery UI using mw.loader
mw.loader.using(['jquery.ui'], function() {
console.log("jQuery UI loaded");
// Initialize and set up responsive handling when DOM is ready
$(function() {
setupResponsiveMenus();
});
// ANCHOR: Open all external links on a new window
mw.loader.using(['mediawiki.util']).then(function () {
function openExternalLinks() {
// Define internal actions that should not open in a new tab
const internalLinkActions = [
'edit',
'history',
'delete',
'submit', // For forms like Special:UserLogin, Special:Upload
'watch',
'unwatch',
'protect',
'unprotect',
'markpatrolled',
'purge', // action=purge
'render' // action=render
];
const currentHostname = window.location.hostname;
// General external links
document.querySelectorAll('a.external').forEach(function (link) {
try {
// Ensure link.href is an absolute URL before parsing
// If link.href is relative, new URL() needs a base
const linkUrl = new URL(link.href);
const linkAction = linkUrl.searchParams.get('action');
// Check if it's an internal link AND has one of the specified actions
if (linkUrl.hostname === currentHostname && linkAction && internalLinkActions.includes(linkAction)) {
// It's an internal action link, don't open in a new tab
return;
}
// For all other links with class 'external'
link.setAttribute('target', '_blank');
link.setAttribute('rel', 'noopener noreferrer');
} catch (e) {
// Fallback for non-HTTP/HTTPS links or if URL parsing fails
console.warn('JS/Common.js: Could not parse URL for link, applying default external link behavior for "a.external":', link.href, e);
link.setAttribute('target', '_blank');
link.setAttribute('rel', 'noopener noreferrer');
}
});
// Social media links within templates
document.querySelectorAll('div.external-social a, div.ntldstats a').forEach(function (link) {
link.setAttribute('target', '_blank');
link.setAttribute('rel', 'noopener noreferrer');
});
}
mw.hook('wikipage.content').add(openExternalLinks);
});
// ANCHOR: Attach classes to system links for CSS customization
mw.hook('wikipage.content').add(function($content) {
$content.find('a[title="Special:Undelete"]').addClass('undelete-link');
});
// ANCHOR: Include a registration link in the Minerva/mobile hamburger menu for logged out users
mw.loader.using(['mediawiki.util'], function() {
$(function() {
if (mw.config.get('skin') === 'minerva' && mw.user.isAnon()) {
var waitForPersonalMenu = function(callback) {
var $menu = $('#p-personal');
if ($menu.length) {
callback($menu);
} else {
setTimeout(function() {
waitForPersonalMenu(callback);
}, 100);
}
};
waitForPersonalMenu(function($menu) {
var regUrl = mw.util.getUrl('Special:UserLogin', { type: 'signup' });
var regText = "Request account";
var $regItem = $('<li>')
.addClass('toggle-list-item menu__item--register')
.append(
$('<a>')
.addClass('toggle-list-item__anchor')
.attr('href', regUrl)
.append(
$('<span class="minerva-icon minerva-icon--logIn"></span>').css('color','var(--general-link-color)')
)
.append(
$('<span class="toggle-list-item__label"></span>').text(regText).css('color','var(--general-link-color)')
)
);
$menu.append($regItem);
});
}
});
});
// ANCHOR: Pre-fill registration page biography box with minimum requirements information
$(function() {
var $bio = $("#wpBio");
if ($bio.length && !$bio.val()) {
$bio.attr("placeholder", "Minimum 10 words");
}
});
// ANCHOR: Category alphabetical sorter (Adapted from Fandom)
(function(){
if (window.mediaWikiCategorySorterLoaded) return;
window.mediaWikiCategorySorterLoaded = true;
function sorter(a, b) {
return (a.textContent || '').localeCompare(b.textContent || '');
}
$(function() {
$('#catlinks .mw-normal-catlinks, #catlinks .mw-hidden-catlinks').each(function() {
var $list = $(this).find('ul'),
$items = $list.children('li'),
sorted = $items.sort(sorter);
$list.empty().append(sorted);
});
});
})();
// ANCHOR: Modified "Gadget-predefined-summaries"
// Lists interactive common edit reasons below "Summary"
mw.loader.using(['mediawiki.util']).then(function () {
'use strict';
if (window.resumeDeluxe === undefined &&
['edit', 'submit'].includes(mw.config.get('wgAction')) &&
mw.util.getParamValue('section') !== 'new') {
var resumeDeluxe = {
titles: ["grammar-syntax", "normalization", "categorization", "general fixes", "template fixes", "+template", "+ internal link(s)", "+ external link(s)", "rem. external link(s)"],
inputs: ["grammar-syntax", "normalization", "categorization", "general fixes", "template fixes", "+template", "+ internal link(s)", "+ external link(s)", "rem. external link(s)"],
addToSummary: function (str) {
var summaryField = document.getElementById('wpSummary');
if (summaryField) {
summaryField.value = summaryField.value ? summaryField.value + '; ' + str : str;
}
return false;
}
};
window.resumeDeluxe = resumeDeluxe;
var DeluxeSummary = function() {
var summaryLabel = document.getElementById('wpSummaryLabel');
var summaryField = document.getElementById('wpSummary');
if (summaryLabel && summaryField) {
// Prevent duplicate bars by checking for an existing one
if (summaryLabel.querySelector('.predefined-summaries-bar')) {
return;
}
// Create container with a unique class name
var container = document.createElement('span');
container.className = 'predefined-summaries-bar';
container.innerHTML = '<b>Predefined summaries</b>: ';
resumeDeluxe.titles.forEach((title, index) => {
if (index > 0) {
container.appendChild(document.createTextNode(' / '));
}
var link = document.createElement('a');
link.href = '#';
link.className = 'sumLink';
link.title = 'Add to edit summary';
link.textContent = title;
link.addEventListener('click', function (event) {
event.preventDefault();
resumeDeluxe.addToSummary(resumeDeluxe.inputs[index]);
});
container.appendChild(link);
});
container.appendChild(document.createElement('br'));
summaryLabel.prepend(container);
// Adjust width as in original script
summaryField.style.width = '95%';
}
};
mw.hook('wikipage.content').add(DeluxeSummary);
}
});
// ANCHOR: Lingo custom script that performs three tasks: 1) Disables the rendering of tooltips of a given acronym within the article that defines it ("RFC" on the "Request For Comments" article); 2) Disables Lingo entirely in arbitrarily defined Namespaces from the frontend; 3) Disables Lingo functionality in specific containers using CSS classes (which is supplemented by visual removal via Common.css)
mw.loader.using('mediawiki.util').then(function() {
$(function() {
// List namespaces where Lingo should be disabled (adjust freely)
var blockedNamespaces = ['Template', 'Category', 'Module'];
var currentNS = mw.config.get('wgCanonicalNamespace');
// List CSS classes within which Lingo should be disabled
var blockedClasses = ['template-table', 'country-hub-infobox', 'campaign-instructions'];
if (blockedNamespaces.indexOf(currentNS) !== -1) {
// Remove any Lingo tooltip markup immediately
var terms = document.querySelectorAll('.mw-lingo-term');
terms.forEach(function(term) {
term.parentNode.replaceChild(document.createTextNode(term.textContent), term);
});
console.log("Lingo processing is blocked in the '" + currentNS + "' namespace.");
return; // Stop further processing
}
// Process CSS class-based blocking
blockedClasses.forEach(function(className) {
var terms = document.querySelectorAll('.' + className + ' .mw-lingo-term');
var count = terms.length;
if (count > 0) {
terms.forEach(function(term) {
term.parentNode.replaceChild(document.createTextNode(term.textContent), term);
});
console.log("Removed " + count + " Lingo tooltip(s) from ." + className + " container(s).");
}
});
// Decent enough solution for Lingo not to render tooltips of an acronym on the page that defines it
var rawTitle = mw.config.get('wgTitle').replace(/_/g, ' ').trim();
var acronym = null;
// Case 1: Title written as "Long Form (ACRONYM)"
var titleAcronymMatch = rawTitle.match(/^(.+?)\s+\(([A-Z]{2,})\)$/);
if (titleAcronymMatch) {
acronym = titleAcronymMatch[2];
console.log("Using acronym from title parenthetical: " + acronym);
}
// Case 2: Title is exactly an acronym (all uppercase)
else if (/^[A-Z]+$/.test(rawTitle)) {
acronym = rawTitle;
console.log("Using pure acronym from title: " + acronym);
}
// Otherwise, compute fallback from title’s longform and compare with extracted acronym
else {
// Remove any trailing parenthetical from title, e.g. "Request For Comments (foo)" becomes "Request For Comments"
var longForm = rawTitle.replace(/\s*\(.*\)\s*$/, '');
var fallback = longForm.split(/\s+/).map(function(word) {
return word.charAt(0).toUpperCase();
}).join('');
// Attempt to extract an acronym from the first paragraph
var firstParagraph = $('#mw-content-text p').first().text();
var parenMatch = firstParagraph.match(/\(([A-Z]{2,})\)/);
if (parenMatch) {
var extracted = parenMatch[1];
console.log("Extracted acronym from first paragraph: " + extracted);
if (extracted === fallback) {
acronym = extracted;
console.log("Acronym matches fallback computed from title: " + acronym);
} else {
console.log("Extracted acronym (" + extracted + ") does not match computed fallback (" + fallback + "); not proceeding with tooltip removal");
return;
}
} else {
console.log("No acronym found in first paragraph; not proceeding with tooltip removal");
return;
}
}
console.log("Assuming definition page for acronym: " + acronym);
// Remove any Lingo tooltip markup for elements whose visible text exactly matches the acronym.
var all = document.querySelectorAll('.mw-lingo-term');
var matching = Array.prototype.filter.call(all, function(term) {
return term.textContent.trim() === acronym;
});
console.log("Found " + matching.length + " element(s) with visible text '" + acronym + "'");
matching.forEach(function(term) {
term.parentNode.replaceChild(document.createTextNode(term.textContent), term);
});
console.log("Finished processing Lingo tooltips for acronym: " + acronym);
});
});
// ANCHOR: Set "File" Categories intelligently based on file type
mw.loader.using(['mediawiki.util'], function() {
(function() {
// Only run on upload pages
var page = mw.config.get('wgCanonicalSpecialPageName');
if (
window.FileCategoryLoaded ||
!(/Upload|MultipleUpload/g.test(page))
) {
return;
}
window.FileCategoryLoaded = true;
// Define file type mapping
var categoryMapping = {
// Images
'jpg': 'Images',
'jpeg': 'Images',
'png': 'Images',
'gif': 'Images',
'svg': 'Images',
'webp': 'Images',
'bmp': 'Images',
'tiff': 'Images',
// Documents
'pdf': 'Documents',
'doc': 'Documents',
'docx': 'Documents',
'ppt': 'Documents',
'pptx': 'Documents',
'xls': 'Documents',
'xlsx': 'Documents',
'txt': 'Documents',
'rtf': 'Documents',
'odt': 'Documents',
'ods': 'Documents',
'odp': 'Documents',
// Archives
'zip': 'Archives',
'rar': 'Archives',
'tar': 'Archives',
'gz': 'Archives',
'7z': 'Archives',
// Audio
'mp3': 'Audio',
'wav': 'Audio',
'ogg': 'Audio',
'flac': 'Audio',
// Video
'mp4': 'Video',
'webm': 'Video',
'avi': 'Video',
'mov': 'Video',
'mkv': 'Video',
// Default fallback
'default': 'Files'
};
// Extract clean extension from filename
function getFileExtension(filename) {
if (!filename) return '';
// Remove path (handle both Windows and Unix paths)
var cleanName = filename.split('\\').pop().split('/').pop();
// Extract extension
var parts = cleanName.split('.');
if (parts.length <= 1) return '';
return parts.pop().toLowerCase();
}
// Apply category based on file extension
function applyCategoryFromExtension(filename) {
var $descField = $('#wpUploadDescription');
if (!$descField.length || !filename) return;
var extension = getFileExtension(filename);
if (!extension) return;
var category = categoryMapping[extension] || categoryMapping['default'];
// Update the description field, preserving any existing content
var currentDesc = $descField.val() || '';
// Remove any existing category we might have added before
Object.values(categoryMapping).forEach(function(catName) {
var catRegex = new RegExp('\\[\\[Category:' + catName + '\\]\\]', 'g');
currentDesc = currentDesc.replace(catRegex, '');
});
// Trim any whitespace created by removing categories
currentDesc = currentDesc.trim();
// Add our new category
var newDesc = currentDesc;
if (newDesc) {
// Add to existing content
newDesc += '\n\n[[Category:' + category + ']]';
} else {
// No content yet
newDesc = '[[Category:' + category + ']]';
}
$descField.val(newDesc);
console.log('FileCategory: Applied category ' + category + ' based on extension .' + extension);
}
// Set up event listeners
function setupEventListeners() {
var $fileInput = $('#wpUploadFile');
// Skip if we can't find the file input
if (!$fileInput.length) return;
// Listen for file selection changes
$fileInput.on('change', function() {
var filename = $(this).val();
if (filename) {
applyCategoryFromExtension(filename);
}
});
// Also hook into MediaWiki's upload events if available
if (typeof mw.hook !== 'undefined') {
mw.hook('uploadform.fileSelected').add(function(data) {
if (data && data.filename) {
applyCategoryFromExtension(data.filename);
}
});
}
// For AJAX-based uploads that might update the file input after page load
var observer = new MutationObserver(function(mutations) {
mutations.forEach(function(mutation) {
if (mutation.type === 'attributes' && mutation.attributeName === 'value') {
var filename = $fileInput.val();
if (filename) {
applyCategoryFromExtension(filename);
}
}
});
});
// Observe the file input for value changes
observer.observe($fileInput[0], { attributes: true });
}
// Initialize when DOM is ready
$(setupEventListeners);
})();
});
// ANCHOR: Responsive expanding menus
/* REVIEW: Codex */
var menuState = {
initialized: false,
isCurrentlyMobile: false,
originalTopRow: null,
originalPrimaryMenu: null,
originalSecondaryMenu: null,
accordionState: {},
hasLoggedSetupUnnecessary: false // Track if we've already logged setup unnecessary message
};
// Media query for mobile breakpoint
var mobileMediaQuery = window.matchMedia("(max-width: 41rem)");
// Main setup function for responsive menu behavior: once per page load
function setupResponsiveMenus() {
var $topRow = $('.icannwiki-top-row');
// Only proceed if we have the top row
if ($topRow.length === 0) {
// Only log if we haven't already logged this message
if (!menuState.hasLoggedSetupUnnecessary) {
console.log("Menu setup unnecessary");
menuState.hasLoggedSetupUnnecessary = true;
}
return;
}
// Capture the original DOM state and save it
if (!menuState.initialized) {
// Store original DOM state references (clone deeply to preserve all attributes and content)
menuState.originalTopRow = $topRow.clone(true, true);
menuState.originalPrimaryMenu = $topRow.find('.icannwiki-menu-container.primary').clone(true, true);
menuState.originalSecondaryMenu = $topRow.find('.icannwiki-menu-container.secondary').clone(true, true);
menuState.initialized = true;
console.log("Original menu state captured");
}
// Setup viewport change listener for future screen size changes
try {
mobileMediaQuery.removeEventListener('change', handleViewportChange);
} catch (e) {
// Ignore any errors from trying to remove a non-existent listener
}
// Setup listener for future viewport changes
mobileMediaQuery.addEventListener('change', handleViewportChange);
// Initial layout setup based on current viewport at page load
setupInitialLayout(mobileMediaQuery.matches);
}
// Initial layout setup function: once per page load
function setupInitialLayout(isMobile) {
menuState.isCurrentlyMobile = isMobile;
if (isMobile) {
// First create the mobile accordions directly and completely
initMobileLayoutImmediately();
}
// Desktop layout is the default, no need to do anything
}
// Handle viewport size changes after initial load
function handleViewportChange(mediaQuery) {
if (mediaQuery.matches && !menuState.isCurrentlyMobile) {
// Switching desktop -> mobile
menuState.isCurrentlyMobile = true;
initMobileLayoutImmediately();
} else if (!mediaQuery.matches && menuState.isCurrentlyMobile) {
// Switching mobile -> desktop
menuState.isCurrentlyMobile = false;
restoreDesktopLayout();
}
// If already in the correct layout mode, do nothing to avoid flashing
}
// Create mobile layout immediately without any timeouts
function initMobileLayoutImmediately() {
console.log("Setting up mobile accordions");
// Get reference to the top row
var $topRow = $('.icannwiki-top-row');
// If the top row is already set up as accordions, don't recreate them
if ($topRow.find('.icannwiki-accordion-section').length > 0) {
console.log("Accordions already exist, no need to reinitialize");
// Make sure the top row is visible
if (!$topRow.hasClass('mobile-ready')) {
$topRow.addClass('mobile-ready');
}
return;
}
// Ensure we have an initialized state
if (!menuState.initialized) {
console.error("Menu state not initialized, cannot create accordions");
return;
}
// Store any existing accordion open/closed states
$('.icannwiki-accordion-header').each(function() {
var $header = $(this);
var isActive = $header.hasClass('active');
var text = $header.text().trim();
menuState.accordionState[text] = isActive;
});
// Get fresh copies of the original menu elements
var $primaryMenu = menuState.originalPrimaryMenu.clone(true, true);
var $secondaryMenu = menuState.originalSecondaryMenu.clone(true, true);
// Clear the top row in preparation for adding accordions
$topRow.empty();
// Build complete accordion structure with content in HTML
var accordionHTML =
'<div class="icannwiki-accordion-section">' +
'<button type="button" class="icannwiki-accordion-header primary" aria-expanded="false">ICANN</button>' +
'<div class="icannwiki-accordion-content" aria-hidden="true"></div>' +
'</div>' +
'<div class="icannwiki-accordion-section">' +
'<button type="button" class="icannwiki-accordion-header secondary" aria-expanded="false">Internet Governance</button>' +
'<div class="icannwiki-accordion-content" aria-hidden="true"></div>' +
'</div>';
// Append the full accordion structure at once
$topRow.append(accordionHTML);
// Insert the original menu content into the accordion containers
$topRow.find('.icannwiki-accordion-section:eq(0) .icannwiki-accordion-content').append($primaryMenu);
$topRow.find('.icannwiki-accordion-section:eq(1) .icannwiki-accordion-content').append($secondaryMenu);
// Add click handlers for accordion toggles
$('.icannwiki-accordion-header').on('click', function() {
var $header = $(this);
var $content = $header.next('.icannwiki-accordion-content');
var isOpen = $header.hasClass('active');
// Store the state for persistence across viewport changes
menuState.accordionState[$header.text().trim()] = !isOpen;
// Toggle active state
$header.toggleClass('active');
$content.toggleClass('active');
// Update ARIA attributes
$header.attr('aria-expanded', !isOpen);
$content.attr('aria-hidden', isOpen);
});
// Restore any previously open accordions
$('.icannwiki-accordion-header').each(function() {
var $header = $(this);
var text = $header.text().trim();
if (menuState.accordionState[text]) {
$header.addClass('active');
$header.next('.icannwiki-accordion-content').addClass('active');
$header.attr('aria-expanded', 'true');
$header.next('.icannwiki-accordion-content').attr('aria-hidden', 'false');
}
});
// Make the top row visible now that it's fully built
$topRow.addClass('mobile-ready');
}
// Function to restore desktop layout
function restoreDesktopLayout() {
console.log("Restoring desktop layout");
var $topRow = $('.icannwiki-top-row');
// If no accordions present, assume already in desktop layout
if ($topRow.find('.icannwiki-accordion-section').length === 0) {
console.log("Already in desktop layout");
return;
}
// Get fresh copies of the original menus
var $primaryMenu = menuState.originalPrimaryMenu.clone(true, true);
var $secondaryMenu = menuState.originalSecondaryMenu.clone(true, true);
// Clear top row and append original menus in one operation
$topRow.empty()
.append($primaryMenu)
.append($secondaryMenu)
.removeClass('mobile-ready');
}
// Initialize on page load
setupResponsiveMenus();
// ANCHOR: ElementPortraitCarousel dynamics
$(function() {
// Only execute if there are any ".person-portrait-carousel" on the page
if ($('.person-portrait-carousel').length === 0) return;
console.log('Initializing person template carousel');
// Initialize all carousels on the page
$('.carousel-container').each(function() {
var $container = $(this);
var $items = $container.find('.carousel-item');
// Skip if no items
if ($items.length === 0) return;
// Wait for images to load
$items.find('img').on('load', function() {
$(this).data('loaded', true);
}).each(function() {
// If already complete, trigger load event
if (this.complete) {
$(this).trigger('load');
}
});
// Initial positioning of carousel items
function initializeCarousel() {
// Show first image, position others
$items.eq(0).addClass('carousel-visible').removeClass('carousel-hidden');
// Logic for exactly 2 images (orbital layout)
if ($items.length === 2) {
$items.eq(0).addClass('carousel-visible carousel-orbital-1').removeClass('carousel-hidden carousel-orbital-2 carousel-left carousel-right');
$items.eq(1).addClass('carousel-hidden carousel-orbital-2').removeClass('carousel-visible carousel-orbital-1 carousel-left carousel-right');
}
// Logic for 3+ images (card shuffle)
else if ($items.length > 2) {
$items.eq(1).addClass('carousel-right').removeClass('carousel-left carousel-visible');
$items.eq($items.length - 1).addClass('carousel-left').removeClass('carousel-right carousel-visible');
}
// Let images dimension within the fixed container
$container.find('.carousel-item img').css({
'max-height': '100%',
'max-width': '100%',
'object-fit': 'contain'
});
}
// Initialize carousel after a delay to ensure proper rendering
setTimeout(initializeCarousel, 100);
});
// Click handler for buttons
$('.carousel-next').on('click', function() {
var $container = $(this).closest('.carousel-container');
var $items = $container.find('.carousel-item');
var $current = $container.find('.carousel-visible');
var currentIndex = parseInt($current.data('index'));
var nextIndex = (currentIndex % $items.length) + 1;
var $next = $container.find('.carousel-item[data-index="' + nextIndex + '"]');
// Check if there are exactly 2 images
if ($items.length === 2) {
// Clear any existing classes and animations
$items.removeClass('carousel-left carousel-right orbital-animating-forward orbital-animating-backward');
// Apply the animation class to create orbital motion
$current.addClass('orbital-animating-forward');
$next.addClass('orbital-animating-backward');
// After animation completes, update the final state classes
setTimeout(function() {
$current.removeClass('carousel-visible carousel-orbital-1 orbital-animating-forward')
.addClass('carousel-hidden carousel-orbital-2');
$next.removeClass('carousel-hidden carousel-orbital-2 orbital-animating-backward')
.addClass('carousel-visible carousel-orbital-1');
}, 600); // Match CSS animation duration (0.6s)
} else {
// Logic for 3+ images
// Remove position classes
$items.removeClass('carousel-left carousel-right');
// Removals and additions
$current.removeClass('carousel-visible').addClass('carousel-hidden carousel-left');
$next.removeClass('carousel-hidden carousel-left carousel-right').addClass('carousel-visible');
// Set the item after next to right
var afterNextIndex = (nextIndex % $items.length) + 1;
if (afterNextIndex > $items.length) afterNextIndex = 1;
var $afterNext = $container.find('.carousel-item[data-index="' + afterNextIndex + '"]');
if ($afterNext.length && !$afterNext.is($next)) {
$afterNext.removeClass('carousel-visible carousel-left').addClass('carousel-hidden carousel-right');
}
}
});
// Click handler for the previous button for 2-image case
$('.carousel-prev').on('click', function() {
// Get container and items
var $container = $(this).closest('.carousel-container');
var $items = $container.find('.carousel-item');
var $current = $container.find('.carousel-visible');
// Calculate previous index
var currentIndex = parseInt($current.data('index'));
var prevIndex = currentIndex - 1;
if (prevIndex < 1) prevIndex = $items.length;
var $prev = $container.find('.carousel-item[data-index="' + prevIndex + '"]');
// Check if we have exactly 2 images: use reversed logic compared to next button
if ($items.length === 2) {
// Clear any existing classes and animations
$items.removeClass('carousel-left carousel-right orbital-animating-forward orbital-animating-backward');
// Apply correct animation classes for leftward movement
$current.addClass('orbital-animating-forward'); // Front item moves back
$prev.addClass('orbital-animating-backward'); // Back item moves forward
// Update final state and remove correct animation classes
var updateFinalState = function() {
$current.removeClass('carousel-visible carousel-orbital-1 orbital-animating-forward') // Remove forward animation class
.addClass('carousel-hidden carousel-orbital-2');
$prev.removeClass('carousel-hidden carousel-orbital-2 orbital-animating-backward') // Remove backward animation class
.addClass('carousel-visible carousel-orbital-1');
};
// Match CSS animation duration (0.6s)
setTimeout(updateFinalState, 600);
} else {
// Logic for 3+ images (card shuffle)
// Remove position classes
$items.removeClass('carousel-left carousel-right');
// Removals and additions
$current.removeClass('carousel-visible').addClass('carousel-hidden carousel-right');
$prev.removeClass('carousel-hidden carousel-left carousel-right').addClass('carousel-visible');
// Set the item before prev to left
var beforePrevIndex = prevIndex - 1;
if (beforePrevIndex < 1) beforePrevIndex = $items.length;
var $beforePrev = $container.find('.carousel-item[data-index="' + beforePrevIndex + '"]');
if ($beforePrev.length && !$beforePrev.is($prev)) {
$beforePrev.removeClass('carousel-visible carousel-right').addClass('carousel-hidden carousel-left');
}
}
});
// Touch swipe support for mobile users
$('.carousel-container').each(function() {
var $container = $(this);
var startX, startY, endX, endY;
var MIN_SWIPE_DISTANCE = 50; // Minimum distance for a swipe to be registered
var MAX_VERTICAL_DISTANCE = 50; // Maximum vertical movement allowed for horizontal swipe
// Touch start: record initial position
$container.on('touchstart', function(e) {
// Store initial touch coordinates
startX = e.originalEvent.touches[0].clientX;
startY = e.originalEvent.touches[0].clientY;
});
// Touch end: determine swipe direction and trigger appropriate action
$container.on('touchend', function(e) {
// Get final touch coordinates
endX = e.originalEvent.changedTouches[0].clientX;
endY = e.originalEvent.changedTouches[0].clientY;
// Calculate horizontal and vertical distance
var horizontalDistance = endX - startX;
var verticalDistance = Math.abs(endY - startY);
// Only register as swipe if horizontal movement is significant and vertical movement is limited
if (Math.abs(horizontalDistance) >= MIN_SWIPE_DISTANCE && verticalDistance <= MAX_VERTICAL_DISTANCE) {
// Prevent default behavior if it's a horizontal swipe
e.preventDefault();
if (horizontalDistance > 0) {
// Swipe right: go to previous slide
$container.find('.carousel-prev').trigger('click');
} else {
// Swipe left: go to next slide
$container.find('.carousel-next').trigger('click');
}
}
});
});
console.log('Template:Person carousel initialization successful');
});
});
// ANCHOR: ElementNavigation buttons entirely clickable
$(function() {
// Only run this if we find navigation elements
if ($('.element-navigation-prev, .element-navigation-next').length === 0) return;
// Process each navigation button
$('.element-navigation-prev, .element-navigation-next').each(function() {
var $button = $(this);
var $link = $button.find('a').first();
// Only proceed if there's a link inside the button
if ($link.length === 0) return;
// Store the link URL
var linkHref = $link.attr('href');
// Make the entire button clickable
$button.css('cursor', 'pointer');
// Add click handler to the button
$button.on('click', function(e) {
// Don't trigger this handler if the actual link was clicked
if (e.target === $link[0] || $.contains($link[0], e.target)) return;
// Navigate to the link destination
window.location.href = linkHref;
});
});
console.log('Navigation buttons enhanced for full clickability');
});
// ANCHOR: "Back to Top" button
;(function($, mw) {
'use strict';
// Prevent double-loading
if (window.ICWBackToTopLoaded) {
return;
}
window.ICWBackToTopLoaded = true;
// Configuration
const buttonStartThreshold = window.innerHeight; // Show button after scrolling one viewport height
const scrollAnimationSpeed = 600; // Milliseconds for scroll to top animation
const buttonFadeSpeed = 300; // Milliseconds for fade in/out
// Create the button element
var $button = $('<a>', {
href: '#',
id: 'icw-back-to-top',
title: 'Back to top',
role: 'button',
html: '↑' // REVIEW: Unicode UPWARDS ARROW
}).appendTo(document.body);
// If JS runs before CSS has hidden it, this ensures it starts hidden
$button.hide();
// Scroll event handler (throttled)
var scrollTimeout;
$(window).on('scroll.icwBackToTop', function() {
if (scrollTimeout) {
clearTimeout(scrollTimeout);
}
scrollTimeout = setTimeout(function() {
if ($(window).scrollTop() > buttonStartThreshold) {
$button.fadeIn(buttonFadeSpeed);
} else {
$button.fadeOut(buttonFadeSpeed);
}
}, 100); // Throttle scroll checks to every 100ms
});
// Click event handler
$button.on('click.icwBackToTop', function(e) {
e.preventDefault();
$('html, body').animate({ scrollTop: 0 }, scrollAnimationSpeed);
});
console.log('Back to Top button initialized');
})(jQuery, mediaWiki);
// ANCHOR: Hero Page Creator Widget
mw.loader.using(['mediawiki.api', 'mediawiki.util'], function() {
$(function() {
// Only run if the hero page creator container exists
if ($('#hero-page-creator').length === 0) return;
console.log('Initializing page creator widget');
// Build the simple HTML form
var formHtml =
'<div class="icannwiki-search-unifier">' +
'<div class="mw-inputbox-element">' +
'<input type="text" id="hero-page-name" class="searchboxInput" placeholder="Page name">' +
'<select id="hero-template-type" class="searchboxInput" disabled>' +
'<option value="">Loading...</option>' +
'</select>' +
'<button id="hero-create-btn" class="mw-ui-button mw-ui-progressive" disabled>Create</button>' +
'</div>' +
'</div>';
$('#hero-page-creator').html(formHtml);
// Fetch available templates from the Lua module
var api = new mw.Api();
api.parse('{{#invoke:TemplateStarter|listTemplates}}')
.done(function(html) {
var tempDiv = document.createElement('div');
tempDiv.innerHTML = html;
var templateListText = tempDiv.textContent || tempDiv.innerText || '';
var availableTemplates = templateListText.split(',').map(function(t) {
return t.trim().split('\n')[0].trim();
}).filter(function(t) {
return t.length > 0 && t.match(/^[a-zA-Z][a-zA-Z0-9_\-\s\(\)]*$/);
});
var $select = $('#hero-template-type');
$select.empty().append('<option value="">Select a subject (template)</option>');
availableTemplates.forEach(function(template) {
$select.append('<option value="' + template + '">' + template + '</option>');
});
$select.prop('disabled', false);
})
.fail(function() {
$('#hero-template-type').empty().append('<option value="">Error</option>');
});
// Enable/disable create button
function updateHeroCreateButton() {
var pageName = $('#hero-page-name').val().trim();
var templateType = $('#hero-template-type').val();
$('#hero-create-btn').prop('disabled', !pageName || !templateType);
}
$('#hero-page-name, #hero-template-type').on('input change', updateHeroCreateButton);
// Handle create button click
$('#hero-create-btn').on('click', function() {
var pageName = $('#hero-page-name').val().trim();
var templateType = $('#hero-template-type').val();
if (!pageName || !templateType) return;
$(this).prop('disabled', true).text('Creating...');
api.parse('{{#invoke:TemplateStarter|preload|' + templateType + '}}')
.done(function(html) {
var tempDiv = document.createElement('div');
tempDiv.innerHTML = html;
var templateContent = tempDiv.textContent || tempDiv.innerText || '';
// Process placeholders using the same logic as Create a Page
var formData = {}; // Hero widget has no form fields
templateContent = processPlaceholders(templateContent, pageName, formData);
api.create(pageName, { summary: 'Creating new ' + templateType + ' page' }, templateContent)
.done(function() {
window.location.href = mw.util.getUrl(pageName, { veaction: 'edit' });
})
.fail(function(code) {
if (code === 'articleexists') {
if (confirm('Page already exists. Do you want to edit it?')) {
window.location.href = mw.util.getUrl(pageName, { action: 'edit' });
}
} else {
alert('Error creating page: ' + code);
}
$('#hero-create-btn').prop('disabled', false).text('Create');
});
})
.fail(function() {
alert('Error loading template content.');
$('#hero-create-btn').prop('disabled', false).text('Create');
});
});
});
});
// ANCHOR: Create a Page: Dynamic generation with pre-filled templates
mw.loader.using(['mediawiki.api', 'mediawiki.util'], function() {
$(function() {
// Only run on pages that have the template creator container
if ($('#template-creator-container').length === 0) return;
console.log('Initializing TemplateStarter');
// Build the initial HTML with loading state
var formHtml =
'<div class="template-creator-main-layout">' +
'<div class="template-creator-form-container">' +
'<h3>Create New Page</h3>' +
'<div class="form-group">' +
'<label for="template-type">Template Type:</label>' +
'<select id="template-type" class="form-control" disabled>' +
'<option value="">Loading templates...</option>' +
'</select>' +
'</div>' +
'<div class="form-group">' +
'<label for="page-name">Page Name:</label>' +
'<input type="text" id="page-name" class="form-control" placeholder="Enter page name...">' +
'</div>' +
'<div id="dynamic-fields-container"></div>' +
'<button id="create-page-btn" class="mw-ui-button mw-ui-progressive" disabled>Create Page</button>' +
'</div>' +
'<div id="template-preview" class="template-creator-preview-container" style="display:none;">' +
'<h4>Preview:</h4>' +
'<pre id="template-preview-content"></pre>' +
'</div>' +
'</div>';
// Insert the form
$('#template-creator-container').html(formHtml);
// Fetch available templates dynamically from Lua module
var api = new mw.Api();
api.parse('{{#invoke:TemplateStarter|listTemplates}}')
.done(function(html) {
// Extract text content from the parsed HTML
var tempDiv = document.createElement('div');
tempDiv.innerHTML = html;
var templateListText = tempDiv.textContent || tempDiv.innerText || '';
// Parse the comma-separated list and clean each template name
var availableTemplates = templateListText.split(',').map(function(t) {
// Clean the template name: remove newlines, extra whitespace, and any descriptions
var cleaned = t.trim().split('\n')[0].trim();
return cleaned;
}).filter(function(t) {
return t.length > 0 && t.match(/^[a-zA-Z][a-zA-Z0-9_\-\s\(\)]*$/);
});
// Populate the dropdown
var $select = $('#template-type');
$select.empty().append('<option value="">Select a template...</option>');
availableTemplates.forEach(function(template) {
$select.append('<option value="' + template + '">' + template + '</option>');
});
// Enable the dropdown
$select.prop('disabled', false);
console.log('Loaded templates dynamically:', availableTemplates);
})
.fail(function(error) {
console.error('Failed to load templates dynamically:', error);
// Show error state instead of fallback
var $select = $('#template-type');
$select.empty().append('<option value="">Error loading templates</option>');
// Keep dropdown disabled on error
$select.prop('disabled', true);
});
// Enable/disable create button based on form completion
function updateCreateButton() {
var templateType = $('#template-type').val();
var pageName = $('#page-name').val().trim();
$('#create-page-btn').prop('disabled', !templateType || !pageName);
}
// Update button state when inputs change
$('#template-type, #page-name').on('change input', updateCreateButton);
// Preview template when type is selected
$('#template-type').on('change', function() {
var templateType = $(this).val();
if (!templateType) {
$('#template-preview').hide();
$('#dynamic-fields-container').empty();
return;
}
// Load dynamic fields for this template type
loadDynamicFields(templateType);
updatePreview();
});
// Update preview when page name changes
$('#page-name').on('input', function() {
var templateType = $('#template-type').val();
if (templateType) {
updatePreview();
}
});
// Function to update the preview with processed placeholders
function updatePreview() {
var templateType = $('#template-type').val();
var pageName = $('#page-name').val().trim();
if (!templateType) return;
// Call the Lua module to get template structure
var api = new mw.Api();
api.parse('{{#invoke:TemplateStarter|preload|' + templateType + '}}')
.done(function(html) {
// Extract text content from the parsed HTML
var tempDiv = document.createElement('div');
tempDiv.innerHTML = html;
var templateContent = tempDiv.textContent || tempDiv.innerText || '';
// Collect form data and process placeholders
var formData = collectFormData();
templateContent = processPlaceholders(templateContent, pageName, formData);
$('#template-preview-content').text(templateContent);
$('#template-preview').show();
})
.fail(function(error) {
console.error('Failed to preview template:', error);
$('#template-preview-content').text('Error loading template preview');
$('#template-preview').show();
});
}
// Function to load dynamic fields for a template type
function loadDynamicFields(templateType) {
console.log('Loading dynamic fields for template:', templateType);
var api = new mw.Api();
api.parse('{{#invoke:TemplateStarter|getCreatorFieldDefinitionsJSON|' + templateType + '}}')
.done(function(html) {
// Extract JSON content from the parsed HTML
var tempDiv = document.createElement('div');
tempDiv.innerHTML = html;
var jsonText = tempDiv.textContent || tempDiv.innerText || '';
try {
var fieldDefinitions = JSON.parse(jsonText);
console.log('Field definitions loaded:', fieldDefinitions);
// Generate form fields
generateDynamicFields(fieldDefinitions);
} catch (e) {
console.error('Failed to parse field definitions JSON:', e, jsonText);
$('#dynamic-fields-container').html('<p style="color: red;">Error loading field definitions</p>');
}
})
.fail(function(error) {
console.error('Failed to load field definitions:', error);
$('#dynamic-fields-container').html('<p style="color: red;">Error loading field definitions</p>');
});
}
// Function to generate dynamic form fields
function generateDynamicFields(fieldDefinitions) {
var $container = $('#dynamic-fields-container');
$container.empty();
// Skip PAGE_NAME as it's handled separately
for (var fieldKey in fieldDefinitions) {
if (fieldKey === 'PAGE_NAME') continue;
var fieldDef = fieldDefinitions[fieldKey];
var $formGroup = $('<div class="form-group"></div>');
// Create label
var $label = $('<label></label>')
.attr('for', 'dynamic-field-' + fieldKey)
.text(fieldDef.label);
// Create input
var $input = $('<input type="text" class="form-control dynamic-field">')
.attr('id', 'dynamic-field-' + fieldKey)
.attr('data-field-key', fieldKey)
.attr('placeholder', fieldDef.placeholder);
// Mark required fields
if (fieldDef.required) {
$label.append(' <span style="color: red;">*</span>');
$input.attr('required', true);
}
$formGroup.append($label).append($input);
$container.append($formGroup);
// Add event listener for preview updates
$input.on('input', function() {
var templateType = $('#template-type').val();
if (templateType) {
updatePreview();
}
});
}
// Update create button state when dynamic fields change
$container.on('input', '.dynamic-field', updateCreateButton);
console.log('Dynamic fields generated');
}
// Function to collect form data from dynamic fields
function collectFormData() {
var formData = {};
// Only collect dynamic field values (PAGE_NAME is handled separately)
$('.dynamic-field').each(function() {
var $field = $(this);
var fieldKey = $field.data('field-key');
var value = $field.val().trim();
if (value) {
formData[fieldKey] = value;
}
});
return formData;
}
// Function to process $VARIABLE$ placeholders
function processPlaceholders(templateContent, pageName, formData) {
var processedContent = templateContent;
// Replace $PAGE_NAME$ with the actual page name (always, even if empty)
processedContent = processedContent.replace(/\$PAGE_NAME\$/g, pageName || '');
// Replace other placeholders with form data (only if values are not empty)
for (var key in formData) {
if (key !== 'PAGE_NAME' && formData[key] && formData[key].trim() !== '') {
var placeholder = '$' + key + '$';
// Properly escape the $ characters for regex
var escapedPlaceholder = placeholder.replace(/\$/g, '\\$');
processedContent = processedContent.replace(new RegExp(escapedPlaceholder, 'g'), formData[key]);
}
}
// Clean up any remaining placeholders that don't have form fields (Hero widget case)
// Only remove placeholders that are not PAGE_NAME and don't have corresponding form fields
var availableFields = Object.keys(formData || {});
availableFields.push('PAGE_NAME'); // Always preserve PAGE_NAME processing
// Find all placeholders in the content
var placeholderMatches = processedContent.match(/\$[A-Z_]+\$/g) || [];
for (var i = 0; i < placeholderMatches.length; i++) {
var placeholder = placeholderMatches[i];
var fieldName = placeholder.replace(/\$/g, '');
// If this placeholder doesn't have a corresponding form field, remove it
if (availableFields.indexOf(fieldName) === -1) {
var escapedPlaceholder = placeholder.replace(/\$/g, '\\$');
processedContent = processedContent.replace(new RegExp(escapedPlaceholder, 'g'), '');
}
}
// Clean up any resulting grammar issues
processedContent = processedContent.replace(/\s+/g, ' '); // Multiple spaces to single
processedContent = processedContent.replace(/is a\s+based/g, 'is based'); // Fix "is a based"
processedContent = processedContent.replace(/is a\s+in/g, 'is in'); // Fix "is a in"
processedContent = processedContent.replace(/\s+\./g, '.'); // Space before period
return processedContent;
}
// Handle create button click
$('#create-page-btn').on('click', function() {
var templateType = $('#template-type').val();
var pageName = $('#page-name').val().trim();
if (!templateType || !pageName) {
alert('Please select a template type and enter a page name');
return;
}
// Disable button to prevent double-clicks
$(this).prop('disabled', true).text('Creating...');
// Get template content via API
var api = new mw.Api();
api.parse('{{#invoke:TemplateStarter|preload|' + templateType + '}}')
.done(function(html) {
// Extract text content from the parsed HTML
var tempDiv = document.createElement('div');
tempDiv.innerHTML = html;
var templateContent = tempDiv.textContent || tempDiv.innerText || '';
// Process placeholders before creating the page
var formData = collectFormData();
var processedContent = processPlaceholders(templateContent, pageName, formData);
// Create the page with the processed template content
api.create(pageName, {
summary: 'Creating new ' + templateType + ' page'
}, processedContent)
.done(function() {
// Redirect to the new page in Visual Edit mode
window.location.href = mw.util.getUrl(pageName, { veaction: 'edit' });
})
.fail(function(code, error) {
if (code === 'articleexists') {
// Page already exists, open it for editing with the template
if (confirm('Page already exists. Do you want to edit it and add the template?')) {
window.location.href = mw.util.getUrl(pageName, { veaction: 'edit' });
}
} else {
console.error('Failed to create page:', code, error);
alert('Error creating page: ' + (error.error ? error.error.info : code));
}
$('#create-page-btn').prop('disabled', false).text('Create Page');
});
})
.fail(function(error) {
console.error('Failed to get template:', error);
alert('Error loading template. Please try again.');
$('#create-page-btn').prop('disabled', false).text('Create Page');
});
});
console.log('TemplateStarter initialized');
});
});
// ANCHOR: Template debug monitor
$(function() {
// Initialize the template debug monitor
window.templateDebugMonitor = {
// Cache to track already seen errors to prevent duplicates
seenErrors: {},
// Scan for debug information in invisible divs with more efficient error reporting
scan: function() {
const html = document.documentElement.innerHTML;
let found = false;
// Only look for the data attribute format - our unified approach
const templateErrorDivs = document.querySelectorAll('div[data-template-error]');
if (templateErrorDivs.length > 0) {
templateErrorDivs.forEach(div => {
// Create a unique ID for this error set
const errorCount = parseInt(div.getAttribute('data-error-count') || '0');
if (errorCount === 0) return;
// Create a fingerprint of the errors to avoid duplicates
let errorFingerprint = "";
for (let i = 1; i <= errorCount; i++) {
const source = div.getAttribute(`data-error-${i}-source`) || '';
const msg = div.getAttribute(`data-error-${i}-msg`) || '';
errorFingerprint += `${source}:${msg};`;
}
// Skip if we've already seen this exact set of errors
if (this.seenErrors[errorFingerprint]) {
return;
}
// Mark as seen
this.seenErrors[errorFingerprint] = true;
found = true;
// Log to console
console.group(`Template Error`);
console.warn(`Found ${errorCount} error(s)`);
// Look for individual error attributes with our simplified naming
for (let i = 1; i <= errorCount; i++) {
const source = div.getAttribute(`data-error-${i}-source`);
const msg = div.getAttribute(`data-error-${i}-msg`);
const details = div.getAttribute(`data-error-${i}-details`);
if (source && msg) {
console.error(`Error in ${source}: ${msg}`);
if (details) {
console.info('Details:', details);
}
}
}
console.groupEnd();
});
}
// Structure errors with minimal format
const structureErrorDivs = document.querySelectorAll('div[data-structure-error]');
if (structureErrorDivs.length > 0) {
structureErrorDivs.forEach(div => {
const errorCount = parseInt(div.getAttribute('data-error-count') || '0');
if (errorCount === 0) return;
// Create fingerprint for deduplication
let errorFingerprint = "structure:";
Array.from(div.attributes)
.filter(attr => attr.name.startsWith('data-error-block-'))
.forEach(attr => {
errorFingerprint += `${attr.name}:${attr.value};`;
});
// Skip if already seen
if (this.seenErrors[errorFingerprint]) {
return;
}
// Mark as seen
this.seenErrors[errorFingerprint] = true;
found = true;
console.group(`Template Structure Error`);
console.warn(`Found ${errorCount} block error(s)`);
// Scan all attributes
Array.from(div.attributes)
.filter(attr => attr.name.startsWith('data-error-block-'))
.forEach(attr => {
const blockNum = attr.name.replace('data-error-block-', '');
console.error(`Error in Block ${blockNum}: ${attr.value}`);
});
console.groupEnd();
});
}
// Look for emergency outputs (minimal attributes and format)
const emergencyDivs = document.querySelectorAll('div[data-critical="1"]');
if (emergencyDivs.length > 0) {
emergencyDivs.forEach(div => {
const source = div.getAttribute('data-error-1-source') || 'unknown';
const msg = div.getAttribute('data-error-1-msg') || 'Critical error';
// Create fingerprint for deduplication
const errorFingerprint = `critical:${source}:${msg}`;
// Skip if already seen
if (this.seenErrors[errorFingerprint]) {
return;
}
// Mark as seen
this.seenErrors[errorFingerprint] = true;
found = true;
console.group(`Critical Template Error`);
console.error(`Template crashed in ${source}: ${msg}`);
console.groupEnd();
});
}
return found;
},
// Initialize the monitor - with throttling to prevent excessive scanning
init: function() {
let scanTimeout = null;
// Define a throttled scan function
const throttledScan = () => {
if (scanTimeout) {
clearTimeout(scanTimeout);
}
scanTimeout = setTimeout(() => {
this.scan();
scanTimeout = null;
}, 300); // Throttle to once every 300ms
};
// Run initial scan after page load
throttledScan();
// Run when content changes
mw.hook('wikipage.content').add(throttledScan);
console.log('Template Debug Monitor initialized');
}
};
// Initialize the debug monitor
templateDebugMonitor.init();
// Debug function for achievements
window.debugAchievements = function() {
// Get page ID
var pageId = mw.config.get('wgArticleId');
console.log("Page ID: " + pageId);
// Check for achievement header
var headerElement = document.querySelector('.achievement-header');
if (headerElement) {
console.log("Achievement Header Found:");
console.log(" Class: " + headerElement.className);
console.log(" Data ID: " + headerElement.getAttribute('data-achievement-id'));
console.log(" Data Name: " + headerElement.getAttribute('data-achievement-name'));
console.log(" Text: " + headerElement.textContent.trim());
} else {
console.log("No achievement header found");
}
// Check for achievement badges
var badgeElements = document.querySelectorAll('.achievement-badge');
if (badgeElements.length > 0) {
console.log("Achievement Badges Found: " + badgeElements.length);
badgeElements.forEach(function(badge, index) {
console.log("Badge " + (index + 1) + ":");
console.log(" Class: " + badge.className);
console.log(" Data ID: " + badge.getAttribute('data-achievement-id'));
console.log(" Data Name: " + badge.getAttribute('data-achievement-name'));
});
} else {
console.log("No achievement badges found");
}
};
// Run the debug function when the page loads
setTimeout(function() {
if (typeof window.debugAchievements === 'function') {
window.debugAchievements();
}
}, 1000);
});