Jump to content

MediaWiki:Common.js: Difference between revisions

// via Wikitext Extension for VSCode
m done testing
Tag: Manual revert
 
(58 intermediate revisions by 2 users not shown)
Line 1: Line 1:
/* 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. */
/* MediaWiki:Common.js is loaded for every page in the wiki. Its functions are FURTHER AUGMENTED BY GADGETS defined in MediaWiki:Gadgets-definition. In which files 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
// Load jQuery UI using mw.loader
Line 5: Line 5:
     console.log("jQuery UI loaded");
     console.log("jQuery UI loaded");
      
      
     // Initialize and set up responsive handling when DOM is ready
     // Deprecated
     $(function() {
     $(function() {;
        setupResponsiveMenus();
});
});


Line 15: Line 14:
   });
   });


// ANCHOR: Include a registration link in the Minerva/mobile hamburger menu for logged out users
// ANCHOR: Page ID Display
mw.loader.using(['mediawiki.util'], function() {
$(function() {
     $(function() {
     // Only show page ID on actual content pages (not special pages, edit pages, etc.)
      if (mw.config.get('skin') === 'minerva' && mw.user.isAnon()) {
    var pageId = mw.config.get('wgArticleId');
        var waitForPersonalMenu = function(callback) {
    var namespace = mw.config.get('wgNamespaceNumber');
          var $menu = $('#p-personal');
    var action = mw.config.get('wgAction');
          if ($menu.length) {
   
            callback($menu);
    // Show on main namespace (0) and other content namespaces, but not on edit/special pages
          } else {
     if (pageId && namespace >= 0 && action === 'view') {
            setTimeout(function() {
        // Try to find the footer element (works for both Vector and Minerva skins)
              waitForPersonalMenu(callback);
         var $footer = $('#footer.mw-footer, .mw-footer');
            }, 100);
          
          }
         if ($footer.length > 0) {
        };
            // Insert at the beginning of the footer
        waitForPersonalMenu(function($menu) {
            $('<div id="icw-page-id">PageID: ' + pageId + '</div>')
          var regUrl = mw.util.getUrl('Special:UserLogin', { type: 'signup' });
                 .prependTo($footer);
          var regText = "Request account";
        } else {
          var $regItem = $('<li>')
            // Fallback: append to body if no footer found
            .addClass('toggle-list-item menu__item--register')
            $('<div id="icw-page-id">PageID: ' + pageId + '</div>')
            .append(
                 .appendTo('body');
              $('<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: ["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: Set "File" Categories intelligently based on file type
// ANCHOR: Set "File" Categories intelligently based on file type - Incompatible with Gadgets for now
mw.loader.using(['mediawiki.util'], function() {
mw.loader.using(['mediawiki.util'], function () {
     (function() {
     (function () {
         // Only run on upload pages
         // Only run on upload pages (classic / MultipleUpload). We do NOT touch UploadWizard here.
         var page = mw.config.get('wgCanonicalSpecialPageName');
         var page = mw.config.get('wgCanonicalSpecialPageName');
         if (
         if (window.FileCategoryLoaded || !(/Upload|MultipleUpload/g.test(page))) return;
            window.FileCategoryLoaded ||
            !(/Upload|MultipleUpload/g.test(page))
        ) {
            return;
        }
         window.FileCategoryLoaded = true;
         window.FileCategoryLoaded = true;
          
 
         // Localized Category namespace label (NS 14)
        var CAT_NS = (mw.config.get('wgFormattedNamespaces') || {})[14] || 'Category';
 
         // Define file type mapping
         // Define file type mapping
         var categoryMapping = {
         var categoryMapping = {
Line 137: Line 54:
             'jpg': 'Images',
             'jpg': 'Images',
             'jpeg': 'Images',
             'jpeg': 'Images',
             'png': 'Images',  
             'png': 'Images',
             'gif': 'Images',
             'gif': 'Images',
             'svg': 'Images',
             'svg': 'Images',
Line 143: Line 60:
             'bmp': 'Images',
             'bmp': 'Images',
             'tiff': 'Images',
             'tiff': 'Images',
           
 
             // Documents
             // Documents
             'pdf': 'Documents',
             'pdf': 'Documents',
Line 157: Line 74:
             'ods': 'Documents',
             'ods': 'Documents',
             'odp': 'Documents',
             'odp': 'Documents',
           
 
             // Archives
             // Archives
             'zip': 'Archives',
             'zip': 'Archives',
Line 164: Line 81:
             'gz': 'Archives',
             'gz': 'Archives',
             '7z': 'Archives',
             '7z': 'Archives',
           
 
             // Audio
             // Audio
             'mp3': 'Audio',
             'mp3': 'Audio',
Line 170: Line 87:
             'ogg': 'Audio',
             'ogg': 'Audio',
             'flac': 'Audio',
             'flac': 'Audio',
           
 
             // Video
             // Video
             'mp4': 'Video',
             'mp4': 'Video',
Line 177: Line 94:
             'mov': 'Video',
             'mov': 'Video',
             'mkv': 'Video',
             'mkv': 'Video',
           
 
             // Default fallback
             // Default fallback
             'default': 'Files'
             'default': 'Files'
         };
         };
          
 
         function escRx(s) {
            return String(s).replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
        }
 
         // Extract clean extension from filename
         // Extract clean extension from filename
         function getFileExtension(filename) {
         function getFileExtension(filename) {
             if (!filename) return '';
             if (!filename) return '';
           
             var cleanName = filename.split('\\').pop().split('/').pop(); // strip path
            // Remove path (handle both Windows and Unix paths)
             var i = cleanName.lastIndexOf('.');
             var cleanName = filename.split('\\').pop().split('/').pop();
             if (i <= 0 || i === cleanName.length - 1) return '';
           
             return cleanName.slice(i + 1).toLowerCase();
            // Extract extension
             var parts = cleanName.split('.');
             if (parts.length <= 1) return '';
           
             return parts.pop().toLowerCase();
         }
         }
       
 
         // Apply category based on file extension
         // Apply category based on file extension (writes ONLY to #wpUploadDescription)
         function applyCategoryFromExtension(filename) {
         function applyCategoryFromExtension(filename) {
             var $descField = $('#wpUploadDescription');
             var $descField = $('#wpUploadDescription');
             if (!$descField.length || !filename) return;
             if (!$descField.length || !filename) return;
           
 
             var extension = getFileExtension(filename);
             var ext = getFileExtension(filename);
             if (!extension) return;
             if (!ext) return;
           
 
             var category = categoryMapping[extension] || categoryMapping['default'];
             var category = categoryMapping[ext] || categoryMapping['default'];
              
             var add = '[[' + CAT_NS + ':' + category + ']]';
             // Update the description field, preserving any existing content
 
             var currentDesc = $descField.val() || '';
             // Current description
              
             var current = $descField.val() || '';
             // Remove any existing category we might have added before
 
             Object.values(categoryMapping).forEach(function(catName) {
            // If already present, do nothing (prevents duplicates on repeated change events)
                 var catRegex = new RegExp('\\[\\[Category:' + catName + '\\]\\]', 'g');
            var already = new RegExp('\\[\\[\\s*' + escRx(CAT_NS) + '\\s*:\\s*' + escRx(category) + '(?:\\s*\\|[^\\]]*)?\\s*\\]\\]', 'i');
                 currentDesc = currentDesc.replace(catRegex, '');
             if (already.test(current)) return;
 
             // Remove any mapped categories we may have added before (handles sort keys and spaces)
             var cats = Array.from(new Set(Object.values(categoryMapping)));
            cats.forEach(function (catName) {
                if (!catName) return;
                 var rx = new RegExp('\\[\\[\\s*' + escRx(CAT_NS) + '\\s*:\\s*' + escRx(catName) + '(?:\\s*\\|[^\\]]*)?\\s*\\]\\]', 'gi');
                 current = current.replace(rx, '');
             });
             });
           
 
            // Trim any whitespace created by removing categories
             current = current.trim();
             currentDesc = currentDesc.trim();
             var next = current ? (current + '\n\n' + add) : add;
           
 
            // Add our new category
             $descField.val(next);
             var newDesc = currentDesc;
             // console.log('FileCategory: Applied ' + add + ' for .' + ext);
            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
         // Set up event listeners
         function setupEventListeners() {
         function setupEventListeners() {
             var $fileInput = $('#wpUploadFile');
            // Classic uses #wpUploadFile; MultipleUpload often has several inputs (wpUploadFile1, wpUploadFile2, …)
           
             var $fileInputs = $('#wpUploadFile, [id^="wpUploadFile"]');
            // Skip if we can't find the file input
 
             if (!$fileInput.length) return;
             if (!$fileInputs.length) return;
           
 
             // Listen for file selection changes
             // Listen for file selection changes on all inputs
             $fileInput.on('change', function() {
             $fileInputs.on('change', function () {
                 var filename = $(this).val();
                 var filename = $(this).val();
                 if (filename) {
                 if (filename) applyCategoryFromExtension(filename);
                    applyCategoryFromExtension(filename);
                }
             });
             });
           
 
             // Also hook into MediaWiki's upload events if available
             // Hook into MediaWiki's upload events if available
             if (typeof mw.hook !== 'undefined') {
             if (typeof mw.hook !== 'undefined') {
                 mw.hook('uploadform.fileSelected').add(function(data) {
                 mw.hook('uploadform.fileSelected').add(function (data) {
                     if (data && data.filename) {
                     if (data && data.filename) applyCategoryFromExtension(data.filename);
                        applyCategoryFromExtension(data.filename);
                    }
                 });
                 });
             }
             }
           
 
             // For AJAX-based uploads that might update the file input after page load
             // Observe value attribute changes (some browsers/flows update programmatically)
             var observer = new MutationObserver(function(mutations) {
             $fileInputs.each(function () {
                mutations.forEach(function(mutation) {
                var el = this;
                    if (mutation.type === 'attributes' && mutation.attributeName === 'value') {
                if (!window.MutationObserver) return;
                        var filename = $fileInput.val();
                try {
                        if (filename) {
                    var ob = new MutationObserver(function (mutations) {
                            applyCategoryFromExtension(filename);
                        for (var i = 0; i < mutations.length; i++) {
                            var m = mutations[i];
                            if (m.type === 'attributes' && m.attributeName === 'value') {
                                var filename = $(el).val();
                                if (filename) applyCategoryFromExtension(filename);
                            }
                         }
                         }
                     }
                     });
                 });
                    ob.observe(el, { attributes: true, attributeFilter: ['value'] });
                 } catch (e) { /* no-op */ }
             });
             });
           
            // Observe the file input for value changes
            observer.observe($fileInput[0], { attributes: true });
         }
         }
       
 
         // Initialize when DOM is ready
         // Initialize when DOM is ready
         $(setupEventListeners);
         $(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');
});
(function ($, mw) {
    'use strict';
    if (window.ICWBackToTopLoaded) return;
    window.ICWBackToTopLoaded = true;
    const showAfter = window.innerHeight;
    const scrollMs  = 600;
    /* tiny inline‑SVG (20×20, white arrow) */
    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">
        <!-- arrow head -->
        <polyline points="6 9 12 3 18 9"/>
        <!-- shaft: 3  → 21 (4 px longer than default, perfectly centred) -->
        <line x1="12" y1="21" x2="12" y2="3"/>
    </svg>`;
    const $btn = $('<a>', {
        id:'icw-back-to-top',
        href:'#',
        'aria-label':'Back to top',
        html:svg
    }).appendTo(document.body);
    let ticking=false;
    $(window).on('scroll.icwBTT', () => {
        if (ticking) return;
        ticking = true;
        requestAnimationFrame(() => {
            const y = window.pageYOffset || document.documentElement.scrollTop;
            $btn.toggleClass('is-visible', y > showAfter);
            ticking = false;
        });
    });
    $btn.on('click.icwBTT', e => {
        e.preventDefault();
        $('html, body').animate({scrollTop:0}, scrollMs);
    });
})(jQuery, mediaWiki);


// ANCHOR: Template debug monitor
// ANCHOR: Template debug monitor
Line 976: Line 425:
});
});


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

Latest revision as of 17:56, 22 November 2025

/* MediaWiki:Common.js is loaded for every page in the wiki. Its functions are FURTHER AUGMENTED BY GADGETS defined in MediaWiki:Gadgets-definition. In which files 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");
    
    // Deprecated
    $(function() {;
});

// 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: 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: Set "File" Categories intelligently based on file type - Incompatible with Gadgets for now
mw.loader.using(['mediawiki.util'], function () {
    (function () {
        // Only run on upload pages (classic / MultipleUpload). We do NOT touch UploadWizard here.
        var page = mw.config.get('wgCanonicalSpecialPageName');
        if (window.FileCategoryLoaded || !(/Upload|MultipleUpload/g.test(page))) return;
        window.FileCategoryLoaded = true;

        // Localized Category namespace label (NS 14)
        var CAT_NS = (mw.config.get('wgFormattedNamespaces') || {})[14] || 'Category';

        // 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'
        };

        function escRx(s) {
            return String(s).replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
        }

        // Extract clean extension from filename
        function getFileExtension(filename) {
            if (!filename) return '';
            var cleanName = filename.split('\\').pop().split('/').pop(); // strip path
            var i = cleanName.lastIndexOf('.');
            if (i <= 0 || i === cleanName.length - 1) return '';
            return cleanName.slice(i + 1).toLowerCase();
        }

        // Apply category based on file extension (writes ONLY to #wpUploadDescription)
        function applyCategoryFromExtension(filename) {
            var $descField = $('#wpUploadDescription');
            if (!$descField.length || !filename) return;

            var ext = getFileExtension(filename);
            if (!ext) return;

            var category = categoryMapping[ext] || categoryMapping['default'];
            var add = '[[' + CAT_NS + ':' + category + ']]';

            // Current description
            var current = $descField.val() || '';

            // If already present, do nothing (prevents duplicates on repeated change events)
            var already = new RegExp('\\[\\[\\s*' + escRx(CAT_NS) + '\\s*:\\s*' + escRx(category) + '(?:\\s*\\|[^\\]]*)?\\s*\\]\\]', 'i');
            if (already.test(current)) return;

            // Remove any mapped categories we may have added before (handles sort keys and spaces)
            var cats = Array.from(new Set(Object.values(categoryMapping)));
            cats.forEach(function (catName) {
                if (!catName) return;
                var rx = new RegExp('\\[\\[\\s*' + escRx(CAT_NS) + '\\s*:\\s*' + escRx(catName) + '(?:\\s*\\|[^\\]]*)?\\s*\\]\\]', 'gi');
                current = current.replace(rx, '');
            });

            current = current.trim();
            var next = current ? (current + '\n\n' + add) : add;

            $descField.val(next);
            // console.log('FileCategory: Applied ' + add + ' for .' + ext);
        }

        // Set up event listeners
        function setupEventListeners() {
            // Classic uses #wpUploadFile; MultipleUpload often has several inputs (wpUploadFile1, wpUploadFile2, …)
            var $fileInputs = $('#wpUploadFile, [id^="wpUploadFile"]');

            if (!$fileInputs.length) return;

            // Listen for file selection changes on all inputs
            $fileInputs.on('change', function () {
                var filename = $(this).val();
                if (filename) applyCategoryFromExtension(filename);
            });

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

            // Observe value attribute changes (some browsers/flows update programmatically)
            $fileInputs.each(function () {
                var el = this;
                if (!window.MutationObserver) return;
                try {
                    var ob = new MutationObserver(function (mutations) {
                        for (var i = 0; i < mutations.length; i++) {
                            var m = mutations[i];
                            if (m.type === 'attributes' && m.attributeName === 'value') {
                                var filename = $(el).val();
                                if (filename) applyCategoryFromExtension(filename);
                            }
                        }
                    });
                    ob.observe(el, { attributes: true, attributeFilter: ['value'] });
                } catch (e) { /* no-op */ }
            });
        }

        // Initialize when DOM is ready
        $(setupEventListeners);
    })();
});

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

});