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.
/* MediaWiki:Common.js is loaded for every page in the wiki. Its functions are FURTHER AUGMENTED BY GADGETS defined in MediaWiki:Gadgets-definition. Where the code is located DOES MATTER, as Gadgets run at the earlier ResourceLoader stage, while code on this file runs later, and should be used for less time-sensitive operations. */
// 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: 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: 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: ["general fixes", "normalization", "information added", "information update", "categorization", "template fixes", "template update", "+template", "+ references(s)", "+ internal link(s)", "+ external link(s)", "- external link(s)"],
inputs: ["general fixes", "normalization", "information added", "information update", "categorization", "template fixes", "template update", "+template", "+ references(s)", "+ internal link(s)", "+ external link(s)", "- 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: 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: Back‑to‑Top button with sentinel trigger
(function ($, mw) {
'use strict';
if (window.ICWBackToTopLoaded) return;
window.ICWBackToTopLoaded = true;
const scrollMs = 600; // smooth‑scroll duration
/* ---------------------------------------------------------
1. Create button (SVG arrow already optically centred)
--------------------------------------------------------- */
const svg = `
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20"
viewBox="0 0 24 24" fill="none" stroke="white" stroke-width="3"
stroke-linecap="round" stroke-linejoin="round">
<g transform="translate(1,0)"> <!-- 1 px optical shift -->
<polyline points="6 9 12 3 18 9"/>
<line x1="12" y1="21" x2="12" y2="3"/>
</g>
</svg>`;
const $btn = $('<a>', {
id: 'icw-back-to-top',
href: '#',
'aria-label': 'Back to top',
html: svg
}).appendTo(document.body);
/* ---------------------------------------------------------
2. Insert a sentinel right before the article title
(if not found, fall back to body top)
--------------------------------------------------------- */
const sentinel = document.createElement('div');
sentinel.style.position = 'absolute';
sentinel.style.top = 0;
sentinel.style.width = '1px';
sentinel.style.height = '1px';
const firstHeading = document.getElementById('firstHeading');
if (firstHeading && firstHeading.parentNode) {
firstHeading.parentNode.insertBefore(sentinel, firstHeading);
} else {
document.body.prepend(sentinel);
}
/* ---------------------------------------------------------
3. Show / hide button based on sentinel visibility
--------------------------------------------------------- */
new IntersectionObserver(
([entry]) => {
$btn.toggleClass('is-visible', !entry.isIntersecting);
},
{root: null, threshold: 0}
).observe(sentinel);
/* ---------------------------------------------------------
4. Smooth scroll on click
--------------------------------------------------------- */
$btn.on('click.icwBTT', e => {
e.preventDefault();
$('html, body').animate({scrollTop: 0}, scrollMs);
});
})(jQuery, mediaWiki);
// 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;
// Scan for status messages
const templateStatusDivs = document.querySelectorAll('div[data-template-status]');
if (templateStatusDivs.length > 0) {
templateStatusDivs.forEach(div => {
const statusCount = parseInt(div.getAttribute('data-status-count') || '0');
if (statusCount === 0) return;
let statusFingerprint = "";
for (let i = 1; i <= statusCount; i++) {
const source = div.getAttribute(`data-status-${i}-source`) || '';
const msg = div.getAttribute(`data-status-${i}-msg`) || '';
statusFingerprint += `${source}:${msg};`;
}
if (this.seenErrors[statusFingerprint]) {
return;
}
this.seenErrors[statusFingerprint] = true;
found = true;
console.group(`Template Status`);
console.info(`Found ${statusCount} status message(s)`);
for (let i = 1; i <= statusCount; i++) {
const source = div.getAttribute(`data-status-${i}-source`);
const msg = div.getAttribute(`data-status-${i}-msg`);
const details = div.getAttribute(`data-status-${i}-details`);
if (source && msg) {
console.info(`Status from ${source}: ${msg}`);
if (details) {
console.log('Details:', details);
}
}
}
console.groupEnd();
});
}
// 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);
});
// ANCHOR: Page ID Display
$(function() {
// Only show page ID on actual content pages (not special pages, edit pages, etc.)
var pageId = mw.config.get('wgArticleId');
var namespace = mw.config.get('wgNamespaceNumber');
var action = mw.config.get('wgAction');
// Show on main namespace (0) and other content namespaces, but not on edit/special pages
if (pageId && namespace >= 0 && action === 'view') {
// Try to find the footer element (works for both Vector and Minerva skins)
var $footer = $('#footer.mw-footer, .mw-footer');
if ($footer.length > 0) {
// Insert at the beginning of the footer
$('<div id="icw-page-id">PageID: ' + pageId + '</div>')
.prependTo($footer);
} else {
// Fallback: append to body if no footer found
$('<div id="icw-page-id">PageID: ' + pageId + '</div>')
.appendTo('body');
}
}
});
// ANCHOR: Page Creator
// SHARED CODEBASE
function fetchTemplateList(callback, errorCallback) {
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\(\)]*$/);
});
callback(availableTemplates);
})
.fail(function(error) {
if (errorCallback) errorCallback(error);
});
}
function fetchTemplateContent(templateType, callback, errorCallback) {
var api = new mw.Api();
api.parse('{{#invoke:TemplateStarter|preload|' + templateType + '}}')
.done(function(html) {
var tempDiv = document.createElement('div');
tempDiv.innerHTML = html;
var templateContent = tempDiv.textContent || tempDiv.innerText || '';
callback(templateContent);
})
.fail(function(error) {
if (errorCallback) errorCallback(error);
});
}
function cleanupUnfilledPlaceholders(templateContent, pageName, formData) {
var processedContent = templateContent;
if (!pageName || pageName.trim() === '') {
throw new Error('Page name is required and cannot be empty');
}
processedContent = processedContent.replace(/\$PAGE_NAME\$/g, pageName);
for (var key in formData) {
if (key !== 'PAGE_NAME' && formData[key] && formData[key].trim() !== '') {
var placeholder = '$' + key + '$';
var escapedPlaceholder = placeholder.replace(/\$/g, '\\$');
processedContent = processedContent.replace(new RegExp(escapedPlaceholder, 'g'), formData[key]);
}
}
processedContent = processedContent.replace(/\$[A-Z_]+\$/g, '');
processedContent = processedContent.replace(/\s+/g, ' ');
processedContent = processedContent.replace(/is a\s+based/g, 'is based');
processedContent = processedContent.replace(/is a\s+in/g, 'is in');
processedContent = processedContent.replace(/\s+\./g, '.');
processedContent = processedContent.trim();
return processedContent;
}
// 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 using shared function
fetchTemplateList(
function(availableTemplates) {
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);
},
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...');
fetchTemplateContent(templateType, function(templateContent) {
try {
var formData = {};
templateContent = cleanupUnfilledPlaceholders(templateContent, pageName, formData);
} catch (error) {
alert('Error: ' + error.message);
$('#hero-create-btn').prop('disabled', false).text('Create');
return;
}
var api = new mw.Api();
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');
});
}, function() {
alert('Error loading template content.');
$('#hero-create-btn').prop('disabled', false).text('Create');
});
});
});
});
// CREATE A PAGE METAPAGE
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 using shared function
fetchTemplateList(
function(availableTemplates) {
var $select = $('#template-type');
$select.empty().append('<option value="">Select a template...</option>');
availableTemplates.forEach(function(template) {
$select.append('<option value="' + template + '">' + template + '</option>');
});
$select.prop('disabled', false);
console.log('Loaded templates dynamically:', availableTemplates);
},
function(error) {
console.error('Failed to load templates dynamically:', error);
$('#template-type').empty().append('<option value="">Error loading templates</option>').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
if (pageName) {
processedContent = processedContent.replace(/\$PAGE_NAME\$/g, pageName);
}
// Replace other placeholders with form data
for (var key in formData) {
if (key !== 'PAGE_NAME' && formData[key]) {
var placeholder = '$' + key + '$';
// Properly escape the $ characters for regex
var escapedPlaceholder = placeholder.replace(/\$/g, '\\$');
processedContent = processedContent.replace(new RegExp(escapedPlaceholder, 'g'), formData[key]);
}
}
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...');
fetchTemplateContent(templateType, function(templateContent) {
var formData = collectFormData();
try {
var processedContent = cleanupUnfilledPlaceholders(templateContent, pageName, formData);
} catch (error) {
alert('Error: ' + error.message);
$('#create-page-btn').prop('disabled', false).text('Create Page');
return;
}
var api = new mw.Api();
api.create(pageName, { summary: 'Creating new ' + templateType + ' page' }, processedContent)
.done(function() {
window.location.href = mw.util.getUrl(pageName, { veaction: 'edit' });
})
.fail(function(code, error) {
if (code === 'articleexists') {
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.error.info : code));
}
$('#create-page-btn').prop('disabled', false).text('Create Page');
});
}, 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');
});
});