Jump to content

MediaWiki:Common.js

Note: After publishing, you may have to bypass your browser's cache to see the changes.

  • Firefox / Safari: Hold Shift while clicking Reload, or press either Ctrl-F5 or Ctrl-R (⌘-R on a Mac)
  • Google Chrome: Press Ctrl-Shift-R (⌘-Shift-R on a Mac)
  • Edge: Hold Ctrl while clicking Refresh, or press Ctrl-F5.
/* 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);
});

});