Jump to content

MediaWiki:Common.js

Revision as of 02:20, 18 October 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. 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");
    
    // Initialize and set up accordion handling when DOM is ready
    $(function() {
        setupAccordionMenus();
});

// 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: Main Page accordion menus
var menuState = {
    initialized: false,
    hasLoggedSetupUnnecessary: false
};

// Main setup function for accordion menu behavior
function setupAccordionMenus() {
    var $topRow = $('.icannwiki-top-row');
    
    // Only proceed if we have the top row
    if ($topRow.length === 0) {
        if (!menuState.hasLoggedSetupUnnecessary) {
            console.log("Menu setup unnecessary - no top row found");
            menuState.hasLoggedSetupUnnecessary = true;
        }
        return;
    }
    
    // Skip if already initialized
    if (menuState.initialized) {
        return;
    }
    
    // Set up accordion layout immediately
    setupAccordionLayout($topRow);
    menuState.initialized = true;
}

// Set up accordion layout
function setupAccordionLayout($topRow) {
    // If accordions already exist, just mark as ready
    if ($topRow.find('.icannwiki-accordion-section').length > 0) {
        $topRow.addClass('mobile-ready');
        console.log("Accordions already exist - marked as ready");
        return;
    }
    
    console.log("Setting up accordion layout");
    
    // Capture original menu content
    var $primaryMenu = $topRow.find('.icannwiki-menu-container.primary').clone(true, true);
    var $secondaryMenu = $topRow.find('.icannwiki-menu-container.secondary').clone(true, true);
    
    // Build accordion structure using MainPage.css classes
    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>';
    
    // Replace content with accordion structure
    $topRow.empty().append(accordionHTML);
    
    // Insert original menu content into accordions
    $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
    $topRow.find('.icannwiki-accordion-header').on('click', function() {
        var $header = $(this);
        var $content = $header.next('.icannwiki-accordion-content');
        var isOpen = $header.hasClass('active');
        
        // Toggle active state
        $header.toggleClass('active');
        $content.toggleClass('active');
        
        // Update ARIA attributes
        $header.attr('aria-expanded', !isOpen);
        $content.attr('aria-hidden', isOpen);
    });
    
    // Mark as ready immediately - this triggers MainPage.css to show content
    $topRow.addClass('mobile-ready');
    console.log("Accordion setup complete - marked as ready");
}

// Initialize on page load
setupAccordionMenus();
});

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