Jump to content

MediaWiki:Common.js

Revision as of 18:21, 31 July 2025 by MarkWD (talk | contribs) (// via Wikitext Extension for VSCode)

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