User:Suyash.dwivedi/userscripts/AddDerivedFromTemplate.js

/**
 * Add Derived From Template Button
 * Adds a button on file pages to add {{Derived from}} template
 * User:Suyash.dwivedi
 */

console.log('AddDerivedFromTemplate: Script file loaded');

(function() {
    'use strict';

    if (mw.config.get('wgNamespaceNumber') !== 6) {
        return;
    }

    console.log('AddDerivedFromTemplate: Script loaded on file page');

    function createAddDerivedButton() {
        const sourceRow = document.querySelector('#fileinfotpl_src');
        if (!sourceRow) {
            console.log('AddDerivedFromTemplate: Source row not found');
            return;
        }

        const sourceCell = sourceRow.nextElementSibling;
        if (!sourceCell) {
            console.log('AddDerivedFromTemplate: Source cell not found');
            return;
        }

        if (document.getElementById('add-derived-from-btn')) {
            return;
        }

        const button = document.createElement('button');
        button.id = 'add-derived-from-btn';
        button.textContent = '+ Add {{Derived from}}';
        button.style.marginLeft = '10px';
        button.style.padding = '5px 10px';
        button.style.backgroundColor = '#28a745';
        button.style.color = 'white';
        button.style.border = 'none';
        button.style.borderRadius = '3px';
        button.style.cursor = 'pointer';
        button.style.fontSize = '12px';

        button.addEventListener('click', function() {
            showDerivedDialog();
        });

        sourceCell.appendChild(button);
        console.log('AddDerivedFromTemplate: Button added');
    }

    function normalizeFilename(input) {
        let filename = input.trim();
        if (filename.startsWith('File:')) {
            filename = filename.substring(5);
        }
        if (!/\.(jpg|jpeg|png|gif|svg|pdf|tiff|webp|tif)$/i.test(filename)) {
            filename += '.jpg';
        }
        return 'File:' + filename;
    }

    function extractExistingFiles(templateText) {
        // Extract files from existing {{Derived from|File:A|File:B|...}}
        const match = templateText.match(/\{\{Derived from\|(.*?)\}\}/is);
        if (!match) return [];
        
        const params = match[1].split('|');
        const files = [];
        
        for (let param of params) {
            param = param.trim();
            // Skip parameters like display=100, gallery=yes
            if (param.startsWith('display=') || param.startsWith('gallery=')) {
                continue;
            }
            // It's a file
            if (param.startsWith('File:')) {
                files.push(param);
            }
        }
        
        return files;
    }

    function showDerivedDialog() {
        const overlay = document.createElement('div');
        overlay.id = 'derived-from-overlay';
        overlay.style.position = 'fixed';
        overlay.style.top = '0';
        overlay.style.left = '0';
        overlay.style.width = '100%';
        overlay.style.height = '100%';
        overlay.style.backgroundColor = 'rgba(0,0,0,0.5)';
        overlay.style.zIndex = '10000';
        overlay.style.display = 'flex';
        overlay.style.alignItems = 'center';
        overlay.style.justifyContent = 'center';

        const dialog = document.createElement('div');
        dialog.style.backgroundColor = 'white';
        dialog.style.padding = '20px';
        dialog.style.borderRadius = '5px';
        dialog.style.maxWidth = '600px';
        dialog.style.width = '90%';
        dialog.style.maxHeight = '80vh';
        dialog.style.overflow = 'auto';

        const title = document.createElement('h3');
        title.textContent = 'Add {{Derived from}} Template';
        title.style.marginTop = '0';

        const description = document.createElement('p');
        description.innerHTML = 'Enter source filenames (with or without "File:" prefix):<br><small style="color: #666;">e.g., "File:Example.jpg" or just "Example.jpg"</small>';

        const inputContainer = document.createElement('div');
        inputContainer.id = 'derived-inputs-container';
        inputContainer.style.marginBottom = '15px';

        addInputField(inputContainer);

        const optionsDiv = document.createElement('div');
        optionsDiv.style.marginBottom = '15px';
        optionsDiv.style.padding = '10px';
        optionsDiv.style.backgroundColor = '#f8f9fa';
        optionsDiv.style.borderRadius = '3px';

        const displayLabel = document.createElement('label');
        displayLabel.textContent = 'Thumbnail size: ';
        displayLabel.style.marginRight = '10px';

        const displayInput = document.createElement('input');
        displayInput.type = 'number';
        displayInput.id = 'display-size';
        displayInput.value = '100';
        displayInput.min = '50';
        displayInput.max = '300';
        displayInput.style.width = '70px';
        displayInput.style.padding = '5px';

        const galleryCheckbox = document.createElement('input');
        galleryCheckbox.type = 'checkbox';
        galleryCheckbox.id = 'gallery-mode';
        galleryCheckbox.checked = true;
        galleryCheckbox.style.marginLeft = '20px';

        const galleryLabel = document.createElement('label');
        galleryLabel.textContent = ' Show as gallery';
        galleryLabel.htmlFor = 'gallery-mode';

        optionsDiv.appendChild(displayLabel);
        optionsDiv.appendChild(displayInput);
        optionsDiv.appendChild(galleryCheckbox);
        optionsDiv.appendChild(galleryLabel);

        const buttonContainer = document.createElement('div');
        buttonContainer.style.display = 'flex';
        buttonContainer.style.gap = '10px';
        buttonContainer.style.justifyContent = 'flex-end';

        const cancelBtn = document.createElement('button');
        cancelBtn.textContent = 'Cancel';
        cancelBtn.style.padding = '8px 15px';
        cancelBtn.style.cursor = 'pointer';
        cancelBtn.addEventListener('click', function() {
            document.body.removeChild(overlay);
        });

        const updateBtn = document.createElement('button');
        updateBtn.textContent = 'Update';
        updateBtn.style.padding = '8px 15px';
        updateBtn.style.backgroundColor = '#28a745';
        updateBtn.style.color = 'white';
        updateBtn.style.border = 'none';
        updateBtn.style.borderRadius = '3px';
        updateBtn.style.cursor = 'pointer';
        updateBtn.addEventListener('click', function() {
            updateWithDerivedFrom();
        });

        buttonContainer.appendChild(cancelBtn);
        buttonContainer.appendChild(updateBtn);

        dialog.appendChild(title);
        dialog.appendChild(description);
        dialog.appendChild(inputContainer);
        dialog.appendChild(optionsDiv);
        dialog.appendChild(buttonContainer);
        overlay.appendChild(dialog);
        document.body.appendChild(overlay);
    }

    function addInputField(container) {
        const wrapper = document.createElement('div');
        wrapper.style.marginBottom = '10px';
        wrapper.style.display = 'flex';
        wrapper.style.gap = '10px';

        const input = document.createElement('input');
        input.type = 'text';
        input.placeholder = 'File:Example.jpg or just Example.jpg';
        input.style.flex = '1';
        input.style.padding = '8px';
        input.style.border = '1px solid #a2a9b1';
        input.style.borderRadius = '3px';
        input.className = 'derived-source-input';

        input.addEventListener('input', function() {
            const inputs = container.querySelectorAll('.derived-source-input');
            const hasEmpty = Array.from(inputs).some(inp => !inp.value.trim());
            if (!hasEmpty) {
                addInputField(container);
            }
        });

        const removeBtn = document.createElement('button');
        removeBtn.textContent = 'Remove';
        removeBtn.style.padding = '8px 12px';
        removeBtn.style.cursor = 'pointer';
        removeBtn.addEventListener('click', function() {
            if (container.children.length > 1) {
                wrapper.remove();
            } else {
                input.value = '';
            }
        });

        wrapper.appendChild(input);
        wrapper.appendChild(removeBtn);
        container.appendChild(wrapper);
    }
  /////
	function updateWithDerivedFrom() {
    const inputs = document.querySelectorAll('.derived-source-input');
    const newFiles = Array.from(inputs)
        .map(inp => inp.value.trim())        // get raw values
        .filter(val => val !== '')           // remove empty inputs
        .map(val => normalizeFilename(val))  // normalize only non-empty
        .filter(val => val !== 'File:');     // guard against bare "File:"
 
    if (newFiles.length === 0) {
        alert('Please enter at least one source file');
        return;
    }
    /////
        const displaySize = document.getElementById('display-size').value;
        const galleryMode = document.getElementById('gallery-mode').checked;

        console.log('AddDerivedFromTemplate: Updating with files:', newFiles);

        const api = new mw.Api();
        const pageTitle = mw.config.get('wgPageName');

        api.get({
            action: 'query',
            prop: 'revisions',
            titles: pageTitle,
            rvprop: 'content',
            rvslots: 'main'
        }).then(function(data) {
            const pages = data.query.pages;
            const page = pages[Object.keys(pages)[0]];
            const content = page.revisions[0].slots.main['*'];

            let newContent;
            let allFiles = newFiles;

            // Check for existing {{Derived from}} template - be more flexible with matching
            const derivedMatch = content.match(/\{\{Derived from\|([^}]+)\}\}/is);

            if (derivedMatch) {
                // Extract existing files
                const existingFiles = extractExistingFiles(derivedMatch[0]);
                console.log('AddDerivedFromTemplate: Found existing files:', existingFiles);
                
                // Merge with new files, removing duplicates
                allFiles = [...new Set([...existingFiles, ...newFiles])];
                console.log('AddDerivedFromTemplate: Merged files:', allFiles);
            }

            // Build the new template
            const filesParam = allFiles.join('|');
            const derivedTemplate = `{{Derived from|${filesParam}|display=${displaySize}|gallery=${galleryMode ? 'yes' : 'no'}}}`;

            if (derivedMatch) {
                // Replace existing template
                newContent = content.replace(/\{\{Derived from\|[^}]+\}\}/is, derivedTemplate);
                console.log('AddDerivedFromTemplate: Replaced existing template');
            } else {
                // Add new template
                if (content.includes('|source={{own}}')) {
                    newContent = content.replace(/\|source=\{\{own\}\}/, '|source=' + derivedTemplate);
                } else if (content.match(/\|source=\s*$/m)) {
                    newContent = content.replace(/(\|source=)\s*$/m, '$1' + derivedTemplate);
                } else if (content.match(/\|source=/)) {
                    // Source exists but has content - add after it
                    newContent = content.replace(/(\|source=[^\n]*\n)/, '$1' + derivedTemplate + '\n');
                } else {
                    // Add after Information template
                    newContent = content.replace(/({{Information[^}]*}})/is, '$1\n' + derivedTemplate);
                }
                console.log('AddDerivedFromTemplate: Added new template');
            }

            // Save the page
            api.postWithToken('csrf', {
                action: 'edit',
                title: pageTitle,
                text: newContent,
                summary: 'Adding/updating {{Derived from}} template using script',
                minor: true
            }).then(function() {
                alert('File page updated successfully!');
                location.reload();
            }).catch(function(error) {
                console.error('AddDerivedFromTemplate: Error updating page:', error);
                alert('Error updating page: ' + error);
            });

        }).catch(function(error) {
            console.error('AddDerivedFromTemplate: Error getting page content:', error);
            alert('Error getting page content: ' + error);
        });

        const overlay = document.getElementById('derived-from-overlay');
        if (overlay) {
            document.body.removeChild(overlay);
        }
    }

    if (document.readyState === 'loading') {
        document.addEventListener('DOMContentLoaded', createAddDerivedButton);
    } else {
        createAddDerivedButton();
    }

})();
Category:Commons:Missing file name in template F Category:Derivative versions