MediaWiki:Common.js: Difference between revisions
Appearance
// via Wikitext Extension for VSCode |
m done testing Tag: Manual revert |
||
| (58 intermediate revisions by 2 users not shown) | |||
| Line 1: | Line 1: | ||
/* MediaWiki:Common.js is loaded for every page in the wiki. Its functions are FURTHER AUGMENTED BY GADGETS defined in MediaWiki:Gadgets-definition. | /* 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 | // Load jQuery UI using mw.loader | ||
| Line 5: | Line 5: | ||
console.log("jQuery UI loaded"); | console.log("jQuery UI loaded"); | ||
// | // Deprecated | ||
$(function() { | $(function() {; | ||
}); | }); | ||
| Line 15: | Line 14: | ||
}); | }); | ||
// ANCHOR: | // 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'); | |||
} | |||
/ | |||
if ( | |||
var | |||
} | |||
} | } | ||
}); | }); | ||
// ANCHOR: Set "File" Categories intelligently based on file type | // ANCHOR: Set "File" Categories intelligently based on file type - Incompatible with Gadgets for now | ||
mw.loader.using(['mediawiki.util'], function() { | mw.loader.using(['mediawiki.util'], function () { | ||
(function() { | (function () { | ||
// Only run on upload pages | // Only run on upload pages (classic / MultipleUpload). We do NOT touch UploadWizard here. | ||
var page = mw.config.get('wgCanonicalSpecialPageName'); | var page = mw.config.get('wgCanonicalSpecialPageName'); | ||
if ( | if (window.FileCategoryLoaded || !(/Upload|MultipleUpload/g.test(page))) return; | ||
window.FileCategoryLoaded = true; | window.FileCategoryLoaded = true; | ||
// Localized Category namespace label (NS 14) | |||
var CAT_NS = (mw.config.get('wgFormattedNamespaces') || {})[14] || 'Category'; | |||
// Define file type mapping | // Define file type mapping | ||
var categoryMapping = { | var categoryMapping = { | ||
| Line 137: | Line 54: | ||
'jpg': 'Images', | 'jpg': 'Images', | ||
'jpeg': 'Images', | 'jpeg': 'Images', | ||
'png': 'Images', | 'png': 'Images', | ||
'gif': 'Images', | 'gif': 'Images', | ||
'svg': 'Images', | 'svg': 'Images', | ||
| Line 143: | Line 60: | ||
'bmp': 'Images', | 'bmp': 'Images', | ||
'tiff': 'Images', | 'tiff': 'Images', | ||
// Documents | // Documents | ||
'pdf': 'Documents', | 'pdf': 'Documents', | ||
| Line 157: | Line 74: | ||
'ods': 'Documents', | 'ods': 'Documents', | ||
'odp': 'Documents', | 'odp': 'Documents', | ||
// Archives | // Archives | ||
'zip': 'Archives', | 'zip': 'Archives', | ||
| Line 164: | Line 81: | ||
'gz': 'Archives', | 'gz': 'Archives', | ||
'7z': 'Archives', | '7z': 'Archives', | ||
// Audio | // Audio | ||
'mp3': 'Audio', | 'mp3': 'Audio', | ||
| Line 170: | Line 87: | ||
'ogg': 'Audio', | 'ogg': 'Audio', | ||
'flac': 'Audio', | 'flac': 'Audio', | ||
// Video | // Video | ||
'mp4': 'Video', | 'mp4': 'Video', | ||
| Line 177: | Line 94: | ||
'mov': 'Video', | 'mov': 'Video', | ||
'mkv': 'Video', | 'mkv': 'Video', | ||
// Default fallback | // Default fallback | ||
'default': 'Files' | 'default': 'Files' | ||
}; | }; | ||
function escRx(s) { | |||
return String(s).replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); | |||
} | |||
// Extract clean extension from filename | // Extract clean extension from filename | ||
function getFileExtension(filename) { | function getFileExtension(filename) { | ||
if (!filename) return ''; | if (!filename) return ''; | ||
var cleanName = filename.split('\\').pop().split('/').pop(); // strip path | |||
var i = cleanName.lastIndexOf('.'); | |||
var cleanName = filename.split('\\').pop().split('/').pop(); | if (i <= 0 || i === cleanName.length - 1) return ''; | ||
return cleanName.slice(i + 1).toLowerCase(); | |||
var | |||
if ( | |||
return | |||
} | } | ||
// Apply category based on file extension | // Apply category based on file extension (writes ONLY to #wpUploadDescription) | ||
function applyCategoryFromExtension(filename) { | function applyCategoryFromExtension(filename) { | ||
var $descField = $('#wpUploadDescription'); | var $descField = $('#wpUploadDescription'); | ||
if (!$descField.length || !filename) return; | if (!$descField.length || !filename) return; | ||
var | var ext = getFileExtension(filename); | ||
if (! | if (!ext) return; | ||
var category = categoryMapping[ | var category = categoryMapping[ext] || categoryMapping['default']; | ||
var add = '[[' + CAT_NS + ':' + category + ']]'; | |||
// | |||
var | // Current description | ||
var current = $descField.val() || ''; | |||
// Remove any | |||
Object.values(categoryMapping).forEach(function(catName) { | // If already present, do nothing (prevents duplicates on repeated change events) | ||
var | 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); | |||
var | // console.log('FileCategory: Applied ' + add + ' for .' + ext); | ||
$descField.val( | |||
console.log('FileCategory: Applied | |||
} | } | ||
// Set up event listeners | // Set up event listeners | ||
function setupEventListeners() { | function setupEventListeners() { | ||
var $ | // Classic uses #wpUploadFile; MultipleUpload often has several inputs (wpUploadFile1, wpUploadFile2, …) | ||
var $fileInputs = $('#wpUploadFile, [id^="wpUploadFile"]'); | |||
if (!$ | if (!$fileInputs.length) return; | ||
// Listen for file selection changes | // Listen for file selection changes on all inputs | ||
$ | $fileInputs.on('change', function () { | ||
var filename = $(this).val(); | var filename = $(this).val(); | ||
if (filename) | if (filename) applyCategoryFromExtension(filename); | ||
}); | }); | ||
// | // Hook into MediaWiki's upload events if available | ||
if (typeof mw.hook !== 'undefined') { | if (typeof mw.hook !== 'undefined') { | ||
mw.hook('uploadform.fileSelected').add(function(data) { | mw.hook('uploadform.fileSelected').add(function (data) { | ||
if (data && data.filename) | if (data && data.filename) applyCategoryFromExtension(data.filename); | ||
}); | }); | ||
} | } | ||
// | // Observe value attribute changes (some browsers/flows update programmatically) | ||
var | $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 | // Initialize when DOM is ready | ||
$(setupEventListeners); | $(setupEventListeners); | ||
})(); | })(); | ||
}); | }); | ||
// ANCHOR: Template debug monitor | // ANCHOR: Template debug monitor | ||
| Line 976: | Line 425: | ||
}); | }); | ||
}); | }); | ||
Latest revision as of 17:56, 22 November 2025
/* 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);
});
});