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