This page was created by Edgar on 2023-10-09. Last edited by Edgar on 2025-09-14.
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)
- Internet Explorer / Edge: Hold Ctrl while clicking Refresh, or press Ctrl-F5
- Opera: Press Ctrl-F5.
This page was created by Edgar on 2023-10-09. Last edited by Edgar on 2025-09-14.
/* Any JavaScript here will be loaded for all users on every page load. */
// MediaWiki User Script for Release Notes Management (ES5 Compatible)
// Add this to your User:YourUsername/common.js page
(function() {
'use strict';
// Only run on the ReleaseNotes:ReleaseNotes page
if (mw.config.get('wgPageName') !== 'ReleaseNotes:ReleaseNotes') {
return;
}
// Add styles
mw.util.addCSS(
'#releaseNotesButton {' +
'position: fixed;' +
'top: 100px;' +
'right: 20px;' +
'background: #667eea;' +
'color: white;' +
'padding: 10px 20px;' +
'border-radius: 5px;' +
'cursor: pointer;' +
'z-index: 1000;' +
'box-shadow: 0 2px 5px rgba(0,0,0,0.2);' +
'font-weight: bold;' +
'}' +
'#releaseNotesButton:hover {' +
'background: #764ba2;' +
'}' +
'#releaseNotesModal {' +
'display: none;' +
'position: fixed;' +
'top: 0;' +
'left: 0;' +
'right: 0;' +
'bottom: 0;' +
'background: rgba(0,0,0,0.5);' +
'z-index: 10000;' +
'}' +
'#releaseNotesForm {' +
'position: absolute;' +
'top: 50%;' +
'left: 50%;' +
'transform: translate(-50%, -50%);' +
'background: white;' +
'padding: 30px;' +
'border-radius: 10px;' +
'box-shadow: 0 5px 15px rgba(0,0,0,0.3);' +
'max-width: 800px;' +
'width: 90%;' +
'max-height: 90vh;' +
'overflow-y: auto;' +
'}' +
'.rn-form-group {' +
'margin-bottom: 15px;' +
'}' +
'.rn-form-group label {' +
'display: block;' +
'font-weight: bold;' +
'margin-bottom: 5px;' +
'color: #333;' +
'}' +
'.rn-form-group input,' +
'.rn-form-group select,' +
'.rn-form-group textarea {' +
'width: 100%;' +
'padding: 8px;' +
'border: 1px solid #ddd;' +
'border-radius: 4px;' +
'font-size: 14px;' +
'}' +
'.rn-form-group textarea {' +
'min-height: 100px;' +
'font-family: monospace;' +
'}' +
'.rn-section {' +
'border: 1px solid #e0e0e0;' +
'padding: 15px;' +
'margin-bottom: 15px;' +
'border-radius: 5px;' +
'background: #f9f9f9;' +
'}' +
'.rn-section h4 {' +
'margin-top: 0;' +
'color: #444;' +
'}' +
'.rn-buttons {' +
'display: flex;' +
'gap: 10px;' +
'justify-content: flex-end;' +
'margin-top: 20px;' +
'}' +
'.rn-button {' +
'padding: 10px 20px;' +
'border: none;' +
'border-radius: 5px;' +
'cursor: pointer;' +
'font-weight: bold;' +
'}' +
'.rn-button-primary {' +
'background: #667eea;' +
'color: white;' +
'}' +
'.rn-button-primary:hover {' +
'background: #764ba2;' +
'}' +
'.rn-button-secondary {' +
'background: #e0e0e0;' +
'color: #333;' +
'}' +
'.rn-button-secondary:hover {' +
'background: #d0d0d0;' +
'}' +
'.rn-preview {' +
'border: 1px solid #ddd;' +
'padding: 15px;' +
'margin-top: 20px;' +
'background: #f5f5f5;' +
'border-radius: 5px;' +
'max-height: 300px;' +
'overflow-y: auto;' +
'}' +
'.rn-preview pre {' +
'white-space: pre-wrap;' +
'font-family: monospace;' +
'font-size: 12px;' +
'}'
);
// Create button
var button = $('<div id="releaseNotesButton">+ Add Release Notes</div>');
$('body').append(button);
// Get current year
var currentYear = new Date().getFullYear();
// Create modal HTML
var modalHtml =
'<div id="releaseNotesModal">' +
'<div id="releaseNotesForm">' +
'<h2>Add New Release Notes</h2>' +
'<div class="rn-form-group">' +
'<label>Month</label>' +
'<select id="rnMonth">' +
'<option value="January">January</option>' +
'<option value="February">February</option>' +
'<option value="March">March</option>' +
'<option value="April">April</option>' +
'<option value="May">May</option>' +
'<option value="June">June</option>' +
'<option value="July">July</option>' +
'<option value="August">August</option>' +
'<option value="September">September</option>' +
'<option value="October">October</option>' +
'<option value="November">November</option>' +
'<option value="December">December</option>' +
'</select>' +
'</div>' +
'<div class="rn-form-group">' +
'<label>Year</label>' +
'<input type="number" id="rnYear" value="' + currentYear + '" min="2020" max="2030">' +
'</div>' +
'<div class="rn-section">' +
'<h4>Designer</h4>' +
'<div class="rn-form-group">' +
'<label>Enhancements (one per line)</label>' +
'<textarea id="rnDesignerEnhancements" placeholder="* Feature 1: Description\n* Feature 2: Description"></textarea>' +
'</div>' +
'<div class="rn-form-group">' +
'<label>Bug Fixes (one per line)</label>' +
'<textarea id="rnDesignerFixes" placeholder="* Fixed issue with...\n* Resolved problem in..."></textarea>' +
'</div>' +
'</div>' +
'<div class="rn-section">' +
'<h4>Framework</h4>' +
'<div class="rn-form-group">' +
'<label>Enhancements (one per line)</label>' +
'<textarea id="rnFrameworkEnhancements" placeholder="* Feature 1: Description\n* Feature 2: Description"></textarea>' +
'</div>' +
'<div class="rn-form-group">' +
'<label>Bug Fixes (one per line)</label>' +
'<textarea id="rnFrameworkFixes" placeholder="* Fixed issue with...\n* Resolved problem in..."></textarea>' +
'</div>' +
'</div>' +
'<div class="rn-section">' +
'<h4>Turnkey</h4>' +
'<div class="rn-form-group">' +
'<label>Enhancements (one per line)</label>' +
'<textarea id="rnTurnkeyEnhancements" placeholder="* Feature 1: Description\n* Feature 2: Description"></textarea>' +
'</div>' +
'<div class="rn-form-group">' +
'<label>Bug Fixes (one per line)</label>' +
'<textarea id="rnTurnkeyFixes" placeholder="* Fixed issue with...\n* Resolved problem in..."></textarea>' +
'</div>' +
'</div>' +
'<div class="rn-section">' +
'<h4>Server</h4>' +
'<div class="rn-form-group">' +
'<label>Updates (one per line)</label>' +
'<textarea id="rnServerUpdates" placeholder="* Update 1: Description\n* Update 2: Description"></textarea>' +
'</div>' +
'</div>' +
'<div class="rn-form-group">' +
'<label>Preview</label>' +
'<div class="rn-preview">' +
'<pre id="rnPreview"></pre>' +
'</div>' +
'</div>' +
'<div class="rn-buttons">' +
'<button class="rn-button rn-button-secondary" id="rnCancel">Cancel</button>' +
'<button class="rn-button rn-button-primary" id="rnGenerate">Generate and Copy</button>' +
'<button class="rn-button rn-button-primary" id="rnInsert">Insert into Page</button>' +
'</div>' +
'</div>' +
'</div>';
var modal = $(modalHtml);
$('body').append(modal);
// Update preview function
function updatePreview() {
var month = $('#rnMonth').val();
var year = $('#rnYear').val();
var monthLower = month.toLowerCase();
var content = '<div id="' + monthLower + '-' + year + '">\n\n' +
'== \'\'\'MDriven - ' + month + ' ' + year + ' \'\'\'==\n\n';
// Designer section
var designerEnhancements = $('#rnDesignerEnhancements').val().trim();
var designerFixes = $('#rnDesignerFixes').val().trim();
if (designerEnhancements || designerFixes) {
content += '=== \'\'\'<u>Designer</u>\'\'\' ===\n';
if (designerEnhancements) {
content += '==== Enhancements ====\n';
var lines = designerEnhancements.split('\n');
for (var i = 0; i < lines.length; i++) {
if (lines[i].trim()) {
content += lines[i].trim() + '\n';
}
}
content += '\n';
}
if (designerFixes) {
content += '==== Bug Fixes ====\n';
var fixLines = designerFixes.split('\n');
for (var j = 0; j < fixLines.length; j++) {
if (fixLines[j].trim()) {
content += fixLines[j].trim() + '\n';
}
}
content += '\n';
}
}
// Framework section
var frameworkEnhancements = $('#rnFrameworkEnhancements').val().trim();
var frameworkFixes = $('#rnFrameworkFixes').val().trim();
if (frameworkEnhancements || frameworkFixes) {
content += '=== \'\'\'<u>Framework</u>\'\'\' ===\n';
if (frameworkEnhancements) {
content += '==== Enhancements ====\n';
var feLines = frameworkEnhancements.split('\n');
for (var k = 0; k < feLines.length; k++) {
if (feLines[k].trim()) {
content += feLines[k].trim() + '\n';
}
}
content += '\n';
}
if (frameworkFixes) {
content += '==== Bug Fixes ====\n';
var ffLines = frameworkFixes.split('\n');
for (var l = 0; l < ffLines.length; l++) {
if (ffLines[l].trim()) {
content += ffLines[l].trim() + '\n';
}
}
content += '\n';
}
}
// Turnkey section
var turnkeyEnhancements = $('#rnTurnkeyEnhancements').val().trim();
var turnkeyFixes = $('#rnTurnkeyFixes').val().trim();
if (turnkeyEnhancements || turnkeyFixes) {
content += '=== \'\'\'<u>Turnkey</u>\'\'\' ===\n';
if (turnkeyEnhancements) {
content += '==== Enhancements ====\n';
var teLines = turnkeyEnhancements.split('\n');
for (var m = 0; m < teLines.length; m++) {
if (teLines[m].trim()) {
content += teLines[m].trim() + '\n';
}
}
content += '\n';
}
if (turnkeyFixes) {
content += '==== Bug Fixes ====\n';
var tfLines = turnkeyFixes.split('\n');
for (var n = 0; n < tfLines.length; n++) {
if (tfLines[n].trim()) {
content += tfLines[n].trim() + '\n';
}
}
content += '\n';
}
}
// Server section
var serverUpdates = $('#rnServerUpdates').val().trim();
if (serverUpdates) {
content += '=== \'\'\'<u>Server</u>\'\'\' ===\n';
var suLines = serverUpdates.split('\n');
for (var o = 0; o < suLines.length; o++) {
if (suLines[o].trim()) {
content += suLines[o].trim() + '\n';
}
}
content += '\n';
}
content += '</div>\n';
$('#rnPreview').text(content);
return content;
}
// Update preview on input
$('#releaseNotesForm').on('input', 'textarea, input, select', updatePreview);
// Button click handlers
button.click(function() {
// Set current month
var months = ['January', 'February', 'March', 'April', 'May', 'June',
'July', 'August', 'September', 'October', 'November', 'December'];
var currentMonth = months[new Date().getMonth()];
$('#rnMonth').val(currentMonth);
updatePreview();
$('#releaseNotesModal').show();
});
$('#rnCancel').click(function() {
$('#releaseNotesModal').hide();
});
$('#rnGenerate').click(function() {
var content = updatePreview();
// Copy to clipboard using older method
var temp = $('<textarea>');
$('body').append(temp);
temp.val(content).select();
document.execCommand('copy');
temp.remove();
mw.notify('Release notes copied to clipboard!', { type: 'success' });
});
$('#rnInsert').click(function() {
var content = updatePreview();
var month = $('#rnMonth').val();
var year = $('#rnYear').val();
// Get current page content
new mw.Api().get({
action: 'query',
prop: 'revisions',
rvprop: 'content',
titles: 'ReleaseNotes:ReleaseNotes',
formatversion: 2
}).then(function(data) {
var page = data.query.pages[0];
var currentContent = page.revisions[0].content;
// Find where to insert (after the changelog links and before first release)
var insertPattern = /<\/div>\s*<div style="display: flex;">/;
var insertMatch = currentContent.match(insertPattern);
if (insertMatch) {
var insertIndex = insertMatch.index;
var newContent = currentContent.slice(0, insertIndex) +
'</div>\n\n' + content + '\n<div style="display: flex;">' +
currentContent.slice(insertIndex + insertMatch[0].length);
// Update sidebar menu
var sidebarPattern = /(<div class="sidebar">[\s\S]*?)(<h2>\s*\d{4}\s*<\/h2>)/;
var sidebarMatch = newContent.match(sidebarPattern);
if (sidebarMatch) {
// Check if year section exists
var yearPattern = new RegExp('<h2>\\s*' + year + '\\s*</h2>');
var monthLink = '<a href="#' + month.toLowerCase() + '-' + year + '" class="menu-item">' + month + ' ' + year + '</a>\n';
if (yearPattern.test(newContent)) {
// Add to existing year
var yearSectionPattern = new RegExp('(<h2>\\s*' + year + '\\s*</h2>\\s*(?:<!--[^>]*-->\\s*)?)');
var updatedContent = newContent.replace(yearSectionPattern, '$1' + monthLink);
// Save the page
savePageContent(updatedContent, 'Added ' + month + ' ' + year + ' release notes');
} else {
// Add new year section
var newYearSection = '<h2> ' + year + '</h2>\n' + monthLink;
var updatedContent = newContent.replace(sidebarPattern, '$1' + newYearSection + '\n$2');
// Save the page
savePageContent(updatedContent, 'Added ' + month + ' ' + year + ' release notes');
}
} else {
savePageContent(newContent, 'Added ' + month + ' ' + year + ' release notes');
}
} else {
mw.notify('Could not find insertion point in page', { type: 'error' });
}
});
$('#releaseNotesModal').hide();
});
// Save page function
function savePageContent(content, summary) {
new mw.Api().postWithToken('csrf', {
action: 'edit',
title: 'ReleaseNotes:ReleaseNotes',
text: content,
summary: summary
}).then(function() {
mw.notify('Release notes added successfully! Reloading page...', { type: 'success' });
setTimeout(function() {
location.reload();
}, 2000);
}).catch(function(error) {
mw.notify('Error saving page: ' + error, { type: 'error' });
});
}
// Close modal on outside click
$('#releaseNotesModal').click(function(e) {
if (e.target === this) {
$(this).hide();
}
});
})();
$(document).ready(function () {
$.get(mw.util.wikiScript('api'), {
action: 'query',
meta: 'userinfo',
format: 'json'
}).done(function (data) {
if (data.query.userinfo.id !== 0) {
var username = data.query.userinfo.name;
var userLink = mw.util.getUrl('User:' + username);
var logoutLink = mw.util.getUrl('Special:Logout');
$('#user-info').html('<a href="' + userLink + '" class="text-white">' + username + '</a>' +
' | <a href="' + logoutLink + '" class="text-white">Logout</a>');
}
});
});
document.getElementById('offcanvas-toggler').addEventListener('click', function() {
var sidebar = document.getElementById('offcanvas-menu');
if (sidebar.classList.contains('show')) {
sidebar.classList.remove('show');
} else {
sidebar.classList.add('show');
}
});
$(document).ready(function() {
$('#offcanvas-close').on('click', function() {
$('#offcanvas-menu').removeClass('show');
});
});
document.addEventListener('DOMContentLoaded', function() {
var form = document.querySelector('.namespace-search-form');
if (form) {
form.addEventListener('submit', function(e) {
var input = form.querySelector('#bs-extendedsearch-input');
if (input) {
var namespace = mw.config.get('wgCanonicalNamespace');
if (namespace && namespace.length > 0) {
input.value = namespace + ": " + input.value;
}
}
});
}
});
(function($) {
'use strict';
// Updated CSS for suggestions
var css = [
'#suggestion-container {',
' position: relative;',
' width: 100%;',
'}',
'#suggestion-box {',
' position: absolute;',
' top: 100%;',
' left: 0;',
' width: 100%;',
' margin-top: 5px;',
' background-color: #fff;',
' border: 1px solid #ccc;',
' border-radius: 4px;',
' box-shadow: 0 2px 6px rgba(0,0,0,0.15);',
' z-index: 1000;',
'}',
'.suggestion-item {',
' padding: 8px;',
' cursor: pointer;',
'}',
'.suggestion-item:hover {',
' background-color: #e0e0e0;',
'}'
].join('\n');
// Add the CSS to the head
$('head').append('<style type="text/css">' + css + '</style>');
// Add suggestion container
$('#suggestion-container').append('<div id="suggestion-box"></div>');
// Show suggestions from the new API
function showSuggestions() {
var query = $(this).val().trim();
if (query.length > 0) {
// Updated API URL for new suggestions
var apiUrl = "https://search-api.mdriven.net/wiki_suggestions?q=" + encodeURIComponent(query);
// Fetch suggestions from the new API
$.ajax({
url: apiUrl,
dataType: "json",
method: "GET",
success: function(data) {
var suggestions = data || [];
$('#suggestion-box').empty();
// Check if suggestions exist
if (suggestions.length === 0) {
$('#suggestion-box').hide();
return;
}
// Populate suggestions
$.each(suggestions, function(index, suggestion) {
var item = $('<div class="suggestion-item"></div>').text(suggestion);
$('#suggestion-box').append(item);
});
// Show suggestion box
$('#suggestion-box').show();
},
error: function(jqxhr, textStatus, error) {
console.error('Error fetching suggestions:', error);
$('#suggestion-box').hide();
}
});
} else {
$('#suggestion-box').empty().hide();
}
}
// Hide suggestions when clicking outside
function hideSuggestions(event) {
if (!$(event.target).closest('#suggestion-container').length) {
$('#suggestion-box').empty().hide();
}
}
// Handle selecting a suggestion
function selectSuggestion(event) {
event.preventDefault(); // Prevent mousedown from losing focus
var selectedText = $(this).text().trim();
// Redirect to Special:CustomSearch with selected query
window.location.href = '/index.php?title=' + encodeURIComponent(selectedText);
}
// Attach input listener to fetch suggestions
$('.search-inputs').on('input', showSuggestions);
// Hide suggestions when clicking anywhere outside
$(document).on('click', hideSuggestions);
// Handle suggestion click with mousedown
$('#suggestion-box').on('mousedown', '.suggestion-item', selectSuggestion);
})(jQuery);
$(document).ready(function() {
// Function to toggle a section open or closed
function toggleSection(element) {
var submenu = $(element).next('.submenu');
submenu.toggle();
}
// Function to open the submenu containing the current page link and scroll to it
function openCurrentPageSubmenuAndScroll() {
var currentPageLink = $('#navMenu .current-page');
if (currentPageLink.length) {
// Open the parent submenu(s) of the current page link
currentPageLink.parents('.submenu').show();
// Delay the scrolling to allow for any dynamic layout changes
setTimeout(function() {
// Calculate the position of the current page link
var position = currentPageLink.offset().top - $('#navMenu').offset().top + $('#navMenu').scrollTop();
// Scroll the menu to the active item
$('#navMenu').animate({
scrollTop: position
}, 500);
}, 100); // Delay of 100 milliseconds
}
}
// Event delegation for dynamically loaded content
$('#navMenu').on('click', '.menu-header', function() {
toggleSection(this);
});
// Open the submenu containing the current page link and scroll to it
openCurrentPageSubmenuAndScroll();
});
document.getElementById('menu-toggle').addEventListener('click', function() {
var navMenu = document.getElementById('navMenu');
var bodyContent = document.getElementById('bodyContent');
if (navMenu.style.display === 'none' || navMenu.style.display === '') {
navMenu.style.display = 'block';
bodyContent.style.display = 'none'; // Hide body content when nav menu is displayed
} else {
navMenu.style.display = 'none';
bodyContent.style.display = 'block'; // Show body content when nav menu is hidden
}
});
$(document).ready(function() {
$('.video__navigation .navigation-item').click(function() {
var videoID = $(this).data('video');
var startTime = $(this).data('start');
var newSrc = 'https://www.youtube.com/embed/' + videoID + '?start=' + startTime + '&autoplay=1';
$('.video__wrapper iframe').attr('src', newSrc);
});
});
$(document).ready(function() {
$('.bs-extendedsearch-filter-button-button').each(function() {
$(this).append('<span class="fa fa-chevron-down"></span>');
});
});
document.addEventListener("scroll", function() {
var sidebar = document.getElementById("navMenu");
var footerHeight = 100;
var windowHeight = window.innerHeight;
var scrollBottomPosition = window.scrollY + windowHeight;
var footerTopPosition = document.documentElement.offsetHeight - footerHeight;
if (scrollBottomPosition <= footerTopPosition) {
sidebar.style.height = "100vh";
} else {
sidebar.style.height = "calc(100vh - 110px)";
}
});
mw.loader.using('jquery', function() {
$(document).ready(function() {
var $toc = $('#toc').clone();
$('#toc').remove();
$('#tocContainer').html($toc);
$('#tocContainer').css({
position: '-webkit-sticky',
position: 'sticky',
top: '0',
padding: '15px',
margin: '0 auto',
borderRadius: '5px',
maxWidth: '280px',
maxHeight: 'max-content',
overflowY: 'auto',
height: '90vh',
zIndex: 1000
});
$('#tocContainer .toctitle').css({
fontSize: '16px',
fontWeight: '600',
color: '#555',
borderBottom: '1px solid #ccc',
paddingBottom: '10px',
marginBottom: '10px'
});
$('#tocContainer ul').css({
margin: 0,
padding: '0 0 0 20px',
listStyleType: 'none',
fontSize: '14px',
lineHeight: '1.6'
});
$('#tocContainer li').css({
marginBottom: '5px',
});
$('#tocContainer a').css({
color: '#555',
textDecoration: 'none',
}).hover(
function() { $(this).css({textDecoration: 'underline', color: '#000'}); },
function() { $(this).css({textDecoration: 'none', color: '#555'}); }
);
});
});
$(document).ready(function() {
var $inputElement = $('.oo-ui-textInputWidget-type-search .oo-ui-inputWidget-input');
if ($inputElement.length) {
$inputElement.on('keydown', function(event) {
if (event.key === 'Enter') {
event.preventDefault();
$inputElement.addClass('search-input');
}
});
}
});
$(document).ready(function() {
var $inputElement = $('.oo-ui-textInputWidget-type-search .oo-ui-inputWidget-input');
if ($inputElement.length) {
$inputElement.addClass('search-input');
console.log('Class "search-input" added to the input element.');
} else {
console.log('Input element not found.');
}
});
$(document).ready(function() {
var signInLink = $('#user-info a');
var currentUrl = encodeURIComponent(window.location.pathname + window.location.search + window.location.hash);
signInLink.attr('href', signInLink.attr('href').replace('<CURRENT_URL>', currentUrl));
});
document.addEventListener('DOMContentLoaded', function() {
var images = document.querySelectorAll('.thumbimage');
var overlay = document.createElement('div');
overlay.className = 'image-overlay';
document.body.appendChild(overlay);
var overlayImage = document.createElement('img');
overlay.appendChild(overlayImage);
images.forEach(function(image) {
image.addEventListener('click', function(event) {
event.preventDefault();
event.stopPropagation();
overlayImage.src = image.src;
overlay.classList.add('show');
});
});
overlay.addEventListener('click', function() {
overlay.classList.remove('show');
});
});
// ==UserScript==
// @name MediaWiki Clean Delete
// @namespace MediaWikiScripts
// @description Adds a 'Clean Delete' action link to pages for admins to delete pages and clean up incoming links and redirects.
// ==/UserScript==
mw.loader.using(['mediawiki.api', 'mediawiki.util', 'jquery'], function () {
function addCleanDeleteLink() {
if (mw.config.get('wgUserGroups').indexOf('sysop') !== -1) {
if ($('#ca-cleandelete').length === 0) {
var $link = $('<div>').attr('id', 'ca-cleandelete').attr('class', 'mw-list-item').append(
$('<a>').attr('href', '#').attr('class', 'ca-cleandelete').text('Clean Delete').click(function (e) {
e.preventDefault();
gatherAllDecisions(mw.config.get('wgPageName'));
})
);
$('#p-actions .tab-group').append($link);
}
}
}
function gatherAllDecisions(pageTitle) {
var decisions = {};
showModal('Handle Links and Redirects', '<div class="form-group">' +
'<label>Specify how you would like to handle all incoming links and redirects:</label>' +
'<select id="link-handling-select" class="form-control">' +
'<option value="delete">Delete Links and Redirects</option>' +
'<option value="change">Change Target of Links and Redirects</option>' +
'</select>' +
'</div>', function () {
var choice = $('#link-handling-select').val();
if (choice === 'change') {
showModal('Specify New Target', '<div class="form-group">' +
'<label>Enter the new target page name to update all links and redirects:</label>' +
'<input type="text" id="new-target-input" class="form-control" placeholder="Enter new target">' +
'</div>', function () {
decisions.links = $('#new-target-input').val();
confirmDeletion(pageTitle, decisions);
});
} else {
decisions.links = '';
confirmDeletion(pageTitle, decisions);
}
});
}
function confirmDeletion(pageTitle, decisions) {
showModal('Confirm Page Deletion', '<p>Are you sure you want to delete "' + pageTitle + '" after handling all links and redirects?</p>', function () {
decisions.deletion = true;
showLoadingDialog('Performing Cleanup', function (loadingDialog, updateLoadingText, logDetail) {
executeAllActions(pageTitle, decisions, function () {
loadingDialog.close().closed.then(function() {
location.reload();
});
}, updateLoadingText, logDetail);
});
});
}
function showModal(title, body, onConfirm) {
var modal = $(
'<div class="modal fade" tabindex="-1" role="dialog">' +
'<div class="modal-dialog" role="document">' +
'<div class="modal-content">' +
'<div class="modal-header">' +
'<h5 class="modal-title">' + title + '</h5>' +
'<button type="button" class="close" data-dismiss="modal" aria-label="Close">' +
'<span aria-hidden="true">×</span>' +
'</button>' +
'</div>' +
'<div class="modal-body">' + body + '</div>' +
'<div class="modal-footer">' +
'<button type="button" class="btn btn-secondary" data-dismiss="modal">Cancel</button>' +
'<button type="button" class="btn btn-primary">Confirm</button>' +
'</div>' +
'</div>' +
'</div>' +
'</div>'
);
modal.find('.btn-primary').click(function () {
onConfirm();
modal.modal('hide');
});
modal.on('hidden.bs.modal', function () {
modal.remove();
});
$('body').append(modal);
modal.modal('show');
}
function showLoadingDialog(message, callback) {
var modal = $(
'<div class="modal fade" tabindex="-1" role="dialog">' +
'<div class="modal-dialog" role="document">' +
'<div class="modal-content">' +
'<div class="modal-header">' +
'<h5 class="modal-title">Performing Cleanup</h5>' +
'</div>' +
'<div class="modal-body">' +
'<p id="loading-text">' + message + '</p>' +
'<ul id="loading-log"></ul>' +
'</div>' +
'</div>' +
'</div>' +
'</div>'
);
$('body').append(modal);
modal.modal('show');
callback(modal, function (text) {
$('#loading-text').text(text);
$('#loading-log').append('<li>' + text + '</li>');
}, function (detail) {
$('#loading-log').append('<li>' + detail + '</li>');
});
}
function executeAllActions(pageTitle, decisions, callback, updateLoadingText, logDetail) {
var api = new mw.Api();
function handleLinksAndRedirects(action, newTarget, next) {
updateLoadingText('Fetching backlinks...');
api.get({
action: 'query',
list: 'backlinks',
bltitle: pageTitle,
bllimit: 'max',
blfilterredir: 'all' // Include both redirects and non-redirects
}).done(function (data) {
logDetail('Backlinks fetched.');
if (data.query.backlinks) {
var promises = $.map(data.query.backlinks, function (link) {
return new $.Deferred(function (defer) {
api.get({
action: 'parse',
page: link.title,
prop: 'wikitext'
}).done(function (linkData) {
var isRedirect = linkData.parse.wikitext['*'].trim().startsWith('#REDIRECT');
if (isRedirect) {
if (action === 'update') {
updateRedirectTarget(link.title, newTarget, api, defer.resolve, logDetail);
} else {
logDetail('Backlink ' + link.title + ' is a redirect. Deleting...');
performDeletion(link.title, defer.resolve, logDetail);
}
} else {
if (action === 'update') {
updateLinkInPage(link.title, pageTitle, newTarget, api, defer.resolve, logDetail);
} else {
removeLinkFromPage(link.title, pageTitle, api, defer.resolve, logDetail);
}
}
}).fail(function (error) {
logDetail('Error checking if backlink ' + link.title + ' is a redirect: ' + error);
defer.resolve();
});
}).promise();
});
$.when.apply($, promises).then(function () {
updateLoadingText(action === 'update' ? 'All links updated.' : 'All links removed.');
next();
});
} else {
next();
}
}).fail(function (error) {
updateLoadingText('Error fetching backlinks: ' + error);
next();
});
}
function updateLinkInPage(linkedPageTitle, originalPageTitle, newTarget, api, resolve, logDetail) {
api.get({
action: 'parse',
page: linkedPageTitle,
prop: 'wikitext'
}).then(function (data) {
var wikitext = data.parse.wikitext['*'];
var regex = new RegExp('\\[\\[' + originalPageTitle.replace(/[\[\]]/g, '\\$&') + '(\\|[^\\]]+)?\\]\\]', 'g');
var newWikitext = wikitext.replace(regex, '[[' + newTarget + '$1]]');
if (wikitext !== newWikitext) {
logDetail('Updating link in ' + linkedPageTitle);
} else {
logDetail('No link found in ' + linkedPageTitle);
}
return api.postWithToken('csrf', {
action: 'edit',
title: linkedPageTitle,
text: newWikitext,
summary: 'Updated link to [[' + newTarget + ']]'
}).then(function (response) {
console.log('API response from updating link: ', response);
if (response.edit && response.edit.result === 'Success' && !response.edit.nochange) {
logDetail('Successfully updated link in ' + linkedPageTitle);
} else {
logDetail('No changes made to link in ' + linkedPageTitle);
}
resolve();
}).fail(function (error) {
logDetail('Error updating link in ' + linkedPageTitle + ': ' + error);
resolve();
});
}).fail(function (error) {
logDetail('Error fetching page content for ' + linkedPageTitle + ': ' + error);
resolve();
});
}
function updateRedirectTarget(redirectPageTitle, newTarget, api, resolve, logDetail) {
logDetail('Updating redirect target in ' + redirectPageTitle);
return api.postWithToken('csrf', {
action: 'edit',
title: redirectPageTitle,
text: '#REDIRECT [[' + newTarget + ']]',
summary: 'Updated redirect to [[' + newTarget + ']]'
}).then(function (response) {
console.log('API response from updating redirect: ', response);
if (response.edit && response.edit.result === 'Success' && !response.edit.nochange) {
logDetail('Successfully updated redirect in ' + redirectPageTitle);
} else {
logDetail('No changes made to redirect in ' + redirectPageTitle);
}
resolve();
}).fail(function (error) {
logDetail('Error updating redirect in ' + redirectPageTitle + ': ' + error);
resolve();
});
}
function removeLinkFromPage(linkedPageTitle, originalPageTitle, api, resolve, logDetail) {
api.get({
action: 'parse',
page: linkedPageTitle,
prop: 'wikitext'
}).then(function (data) {
var wikitext = data.parse.wikitext['*'];
var regex = new RegExp('\\[\\[' + originalPageTitle.replace(/[\[\]]/g, '\\$&') + '(\\|[^\\]]+)?\\]\\]', 'g');
var newWikitext = wikitext.replace(regex, function(match, p1) {
return p1 ? p1.substring(1) : originalPageTitle;
});
if (wikitext !== newWikitext) {
logDetail('Removing link from ' + linkedPageTitle);
} else {
logDetail('No link found in ' + linkedPageTitle);
}
return api.postWithToken('csrf', {
action: 'edit',
title: linkedPageTitle,
text: newWikitext,
summary: 'Removed link to [[' + originalPageTitle + ']]'
}).then(function (response) {
console.log('API response from removing link: ', response);
if (response.edit && response.edit.result === 'Success' && !response.edit.nochange) {
logDetail('Successfully removed link from ' + linkedPageTitle);
} else {
logDetail('No changes made to link in ' + linkedPageTitle);
}
resolve();
}).fail(function (error) {
logDetail('Error removing link from ' + linkedPageTitle + ': ' + error);
resolve();
});
}).fail(function (error) {
logDetail('Error fetching page content for ' + linkedPageTitle + ': ' + error);
resolve();
});
}
function handleRedirects(next) {
updateLoadingText('Fetching redirects...');
api.get({
action: 'query',
titles: pageTitle,
redirects: true
}).done(function (data) {
logDetail('Redirects fetched.');
var redirects = data.query.pages[Object.keys(data.query.pages)[0]].redirects;
if (redirects) {
var promises = $.map(redirects, function (redirect) {
return new $.Deferred(function (defer) {
logDetail('Found redirect: ' + redirect.title);
performDeletion(redirect.title, defer.resolve, logDetail);
}).promise();
});
$.when.apply($, promises).then(function () {
next();
});
} else {
next();
}
}).fail(function (error) {
updateLoadingText('Error fetching redirects: ' + error);
next();
});
}
function performDeletion(pageTitle, resolve, logDetail) {
updateLoadingText('Deleting the page: ' + pageTitle);
api.postWithToken('csrf', {
action: 'delete',
title: pageTitle,
reason: 'Automated clean delete by admin'
}).done(function (response) {
console.log('API response from deleting page: ', response);
if (response.delete && response.delete.title) {
logDetail('Successfully deleted page: ' + response.delete.title);
} else {
logDetail('No deletion performed for ' + pageTitle);
}
resolve();
}).fail(function (error) {
logDetail('Error during clean deletion of ' + pageTitle + ': ' + error);
resolve();
});
}
var tasks = [
function (resolve) {
if (decisions.links === '') {
handleLinksAndRedirects('remove', '', resolve);
} else {
handleLinksAndRedirects('update', decisions.links, resolve);
}
},
function (resolve) {
if (decisions.deletion) {
handleRedirects(resolve);
} else {
resolve();
}
},
function (resolve) {
if (decisions.deletion) {
performDeletion(pageTitle, resolve, logDetail);
} else {
resolve();
}
}
];
(function executeTasks(i) {
if (i < tasks.length) {
tasks[i](function () {
executeTasks(i + 1);
});
} else {
callback();
}
})(0);
}
mw.hook('wikipage.content').add(addCleanDeleteLink);
});
function isLoggedOut() {
return mw.config.get('wgUserName') === null;
}
jQuery(document).ready(function ($) {
if (isLoggedOut()) {
$('body').append('<div class="darken-overlay"></div>');
$('img').each(function () {
var imgSrc = $(this).attr('src');
if (imgSrc !== '/images/MDrivenLogo.png') {
$(this).addClass('enlargeable');
}
});
$('img.enlargeable').click(function (event) {
event.preventDefault();
event.stopPropagation();
if ($(this).hasClass('enlarged')) {
$(this).removeClass('enlarged');
$('.darken-overlay').hide();
} else {
$('img.enlargeable').removeClass('enlarged');
$(this).addClass('enlarged');
$('.darken-overlay').show();
var fullSizeSrc = $(this).attr('src').replace('/thumb', '').replace(/\/\d+px-.+$/, '');
$(this).attr('src', fullSizeSrc);
}
});
$('.darken-overlay').click(function () {
$('img.enlargeable').removeClass('enlarged');
$(this).hide();
});
}
});
/**
* Fixed LeftMenu Editor with Sub-namespace Support - ES5 Compatible
* Matches the PHP ContentBody component structure
*/
(function (mw, $) {
'use strict';
// Abort for anonymous visitors
if (!mw.config.get('wgUserId')) { return; }
// Load dependencies
mw.loader.using(['mediawiki.api', 'jquery.ui']).done(function () {
var LeftMenuEditor = {
api: new mw.Api(),
menuData: null,
namespace: (mw.config.get('wgCanonicalNamespace') || 'Main').replace(/ /g, '_'),
currentPage: mw.config.get('wgPageName'),
pageName: mw.config.get('wgTitle'),
isEditMode: false,
isCollapsed: true,
$menu: null,
$editorBox: null,
$preview: null,
selectedItem: null,
suggestedTags: [],
selectedTags: [],
// Sub-namespace support
subNamespaces: [],
currentSubNamespace: null,
subNamespaceMenus: {}
};
// Clean, professional CSS without gradients
function addStyles() {
var css = [
// Main container at top of content
'.leftmenu-editor-container{position:relative;background:#ffffff;border:1px solid #ddd;margin:0 0 20px 0;box-shadow:0 2px 4px rgba(0,0,0,0.08);overflow:hidden;transition:all 0.3s ease}',
'.leftmenu-editor-container.collapsed .leftmenu-editor-body{max-height:0;padding:0 20px}',
// Header - clean design
'.leftmenu-editor-header{background:#f8f9fa;border-bottom:1px solid #ddd;padding:15px 20px;cursor:pointer;display:flex;justify-content:space-between;align-items:center;user-select:none}',
'.leftmenu-editor-header:hover{background:#f0f1f3}',
'.leftmenu-editor-header h3{margin:0;font-size:16px;font-weight:600;color:#333}',
'.leftmenu-editor-toggle{font-size:12px;color:#666;transition:transform 0.3s}',
'.leftmenu-editor-container.collapsed .leftmenu-editor-toggle{transform:rotate(-90deg)}',
// Sub-namespace selector
'.leftmenu-subns-selector{background:#f0f0f0;border-bottom:1px solid #ddd;padding:15px 20px;display:flex;align-items:center;gap:20px}',
'.leftmenu-subns-selector label{font-weight:600;color:#333;font-size:14px}',
'.leftmenu-subns-dropdown{padding:8px 12px;border:1px solid #ddd;border-radius:4px;font-size:14px;min-width:200px}',
'.leftmenu-subns-actions{display:flex;gap:10px;margin-left:auto}',
'.leftmenu-subns-add{padding:6px 12px;background:#5cb85c;color:#fff;border:none;border-radius:4px;font-size:13px;cursor:pointer}',
'.leftmenu-subns-add:hover{background:#4cae4c}',
'.leftmenu-subns-delete{padding:6px 12px;background:#d9534f;color:#fff;border:none;border-radius:4px;font-size:13px;cursor:pointer}',
'.leftmenu-subns-delete:hover{background:#c9302c}',
'.leftmenu-subns-rename{padding:6px 12px;background:#f0ad4e;color:#fff;border:none;border-radius:4px;font-size:13px;cursor:pointer}',
'.leftmenu-subns-rename:hover{background:#ec971f}',
// Star indicator
'.leftmenu-star-indicator{color:#f0ad4e;font-weight:bold;margin-left:5px;font-size:16px;vertical-align:middle}',
'.leftmenu-visual-item.star-priority{background:#fff8e1;border-color:#f0ad4e}',
'.leftmenu-visual-item.star-priority:hover{background:#fff3cd}',
// Body layout
'.leftmenu-editor-body{display:grid;grid-template-columns:1fr 380px;gap:20px;padding:20px;max-height:600px;overflow-y:scroll;transition:all 0.3s ease}',
// Menu preview panel
'.leftmenu-preview-panel{background:#fafafa;border:1px solid #e0e0e0;padding:20px;overflow-y:auto;max-height:560px}',
'.leftmenu-preview-header{display:flex;justify-content:space-between;align-items:center;margin-bottom:20px}',
'.leftmenu-preview-header h4{margin:0;color:#333;font-size:14px;font-weight:600;text-transform:uppercase;letter-spacing:0.5px}',
'.leftmenu-search{padding:6px 12px;border:1px solid #ddd;border-radius:4px;width:200px;font-size:13px}',
'.leftmenu-search:focus{outline:none;border-color:#4a90e2;box-shadow:0 0 0 2px rgba(74,144,226,0.2)}',
// Menu tree structure
'.leftmenu-preview-tree{font-size:14px}',
// Menu items - clean cards
'.leftmenu-visual-item{background:#fff;border:1px solid #e0e0e0;padding:10px 14px;margin:4px 0;cursor:move;transition:all 0.2s;display:flex;justify-content:space-between;align-items:center;color:#333;position:relative}',
'.leftmenu-visual-item:hover{background:#f8f9fa;border-color:#4a90e2;box-shadow:0 2px 4px rgba(0,0,0,0.08)}',
'.leftmenu-visual-item.is-current-page{background:#e8f4fd;border-color:#4a90e2;font-weight:600}',
'.leftmenu-visual-item.selected{background:#4a90e2;color:#fff;border-color:#3a7bc8}',
// Sections - hierarchical design
'.leftmenu-visual-section{background:#fff;border:1px solid #ddd;margin:8px 0;overflow:visible}',
'.leftmenu-visual-section-header{background:#f0f0f0;padding:10px 14px;cursor:pointer;display:flex;align-items:center;font-weight:600;color:#333;font-size:14px;border-bottom:1px solid #ddd;user-select:none}',
'.leftmenu-visual-section-header:hover{background:#e8e8e8}',
'.leftmenu-section-toggle{font-size:10px;color:#666;margin-right:8px;transition:transform 0.2s}',
'.leftmenu-visual-section.collapsed .leftmenu-section-toggle{transform:rotate(-90deg)}',
'.leftmenu-visual-section.collapsed .leftmenu-section-items{display:none}',
'.leftmenu-section-items{padding:8px;background:#fafafa;min-height:20px}',
// Nested sections - clear hierarchy
'.leftmenu-section-items .leftmenu-visual-section{margin:4px 0;border-color:#e0e0e0}',
'.leftmenu-section-items .leftmenu-visual-section-header{background:#f8f8f8;font-size:13px;padding:8px 12px}',
'.leftmenu-section-items .leftmenu-visual-item{font-size:13px}',
// Drag placeholder at correct level
'.ui-sortable-helper{opacity:0.8 !important}',
'.leftmenu-section-items .leftmenu-drag-placeholder{margin-left:0 !important}',
// Direct links - distinct style
'.leftmenu-visual-item.direct-link{background:#fffbf0;border-color:#f0ad4e}',
'.leftmenu-visual-item.direct-link:hover{background:#fff8e1;border-color:#ec971f}',
// Item count badge
'.leftmenu-item-count{background:#4a90e2;color:#fff;padding:2px 6px;border-radius:10px;font-size:11px;margin-left:auto;margin-right:10px}',
// Icons
'.leftmenu-icon{display:inline-block;width:16px;text-align:center;margin-right:6px;opacity:0.6}',
// Drag and drop
'.leftmenu-dragging{opacity:0.4}',
'.leftmenu-drag-placeholder{background:#4a90e2;opacity:0.2;height:36px;margin:4px 0;border:2px dashed #4a90e2}',
// Action buttons
'.leftmenu-item-actions{opacity:0;transition:opacity 0.2s;display:flex;gap:4px}',
'.leftmenu-visual-item:hover .leftmenu-item-actions,.leftmenu-visual-section-header:hover .leftmenu-item-actions{opacity:1}',
'.leftmenu-visual-item.selected .leftmenu-item-actions{opacity:1}',
'.leftmenu-item-actions button{background:#fff;border:1px solid #ddd;width:28px;height:28px;border-radius:4px;cursor:pointer;font-size:12px;transition:all 0.15s;padding:0;display:flex;align-items:center;justify-content:center}',
'.leftmenu-item-actions button:hover{background:#f8f9fa;border-color:#999}',
'.leftmenu-item-edit{color:#4a90e2}',
'.leftmenu-item-delete{color:#d9534f}',
'.leftmenu-item-duplicate{color:#5cb85c}',
'.leftmenu-item-star{color:#f0ad4e}',
'.leftmenu-item-star.active{background:#f0ad4e;color:#fff}',
// Control panel
'.leftmenu-controls{background:#fff;border:1px solid #e0e0e0;padding:20px;overflow-y:auto;display:flex;flex-direction:column}',
'.leftmenu-controls h4{margin:0 0 16px 0;color:#333;font-size:14px;font-weight:600;text-transform:uppercase;letter-spacing:0.5px}',
// Quick actions
'.leftmenu-quick-actions{display:grid;grid-template-columns:1fr 1fr;gap:10px;margin-bottom:20px}',
'.leftmenu-quick-action{background:#f8f9fa;border:1px solid #ddd;padding:12px;text-align:center;cursor:pointer;transition:all 0.2s;border-radius:4px}',
'.leftmenu-quick-action:hover{border-color:#4a90e2;background:#fff;box-shadow:0 2px 4px rgba(0,0,0,0.08)}',
'.leftmenu-quick-action-icon{font-size:20px;margin-bottom:4px;display:block}',
'.leftmenu-quick-action-label{font-size:12px;color:#666}',
// Form inputs
'.leftmenu-form-group{margin-bottom:16px}',
'.leftmenu-form-label{display:block;margin-bottom:4px;color:#333;font-size:13px;font-weight:600}',
'.leftmenu-form-input,.leftmenu-form-select{width:100%;padding:8px 12px;border:1px solid #ddd;border-radius:4px;font-size:13px}',
'.leftmenu-form-input:focus,.leftmenu-form-select:focus{outline:none;border-color:#4a90e2;box-shadow:0 0 0 2px rgba(74,144,226,0.2)}',
// Buttons
'.leftmenu-button{padding:8px 16px;border:1px solid #ddd;border-radius:4px;font-size:13px;font-weight:600;cursor:pointer;transition:all 0.2s;background:#fff}',
'.leftmenu-button:hover{background:#f8f9fa}',
'.leftmenu-button.primary{background:#4a90e2;color:#fff;border-color:#4a90e2}',
'.leftmenu-button.primary:hover{background:#3a7bc8;border-color:#3a7bc8}',
'.leftmenu-button.secondary{background:#f8f9fa;color:#333}',
'.leftmenu-button.danger{background:#d9534f;color:#fff;border-color:#d9534f}',
'.leftmenu-button.success{background:#5cb85c;color:#fff;border-color:#5cb85c}',
'.leftmenu-button:disabled{opacity:0.6;cursor:not-allowed}',
// Status messages
'.leftmenu-status{padding:10px 12px;margin:10px 0;font-size:13px;display:none;border-radius:4px}',
'.leftmenu-status.success{background:#dff0d8;color:#3c763d;border:1px solid #d6e9c6}',
'.leftmenu-status.error{background:#f2dede;color:#a94442;border:1px solid #ebccd1}',
'.leftmenu-status.info{background:#d9edf7;color:#31708f;border:1px solid #bce8f1}',
// JSON view
'.leftmenu-json-view{background:#f5f5f5;border:1px solid #ddd;padding:12px;font-family:monospace;font-size:12px;line-height:1.4;border-radius:4px;max-height:300px;overflow-y:auto}',
// Loading
'.leftmenu-loading{text-align:center;padding:40px;color:#999}',
'.leftmenu-spinner{display:inline-block;width:20px;height:20px;border:2px solid #f3f3f3;border-top:2px solid #4a90e2;border-radius:50%;animation:spin 1s linear infinite}',
'@keyframes spin{0%{transform:rotate(0deg)}100%{transform:rotate(360deg)}}',
// Empty state
'.leftmenu-empty-state{text-align:center;padding:40px;color:#999}',
'.leftmenu-empty-state button{margin-top:10px}',
// Responsive
'@media (max-width: 900px){.leftmenu-editor-body{grid-template-columns:1fr;height:auto}.leftmenu-controls{border-top:1px solid #e0e0e0;margin-top:20px}}'
].join('');
$('<style>').text(css).appendTo('head');
}
// Create the interface
function createInterface() {
var $container = $('<div>', { 'class': 'leftmenu-editor-container collapsed' });
var $header = $('<div>', { 'class': 'leftmenu-editor-header' }).html(
'<h3>🛠️ Wiki Tools - Menu Editor, AI Assistant & Page Management</h3>' +
'<span class="leftmenu-editor-toggle">▼</span>'
);
var $body = $('<div>', { 'class': 'leftmenu-editor-body' });
// Create tabbed interface
var $tabs = $('<div>', { 'class': 'wiki-tools-tabs' }).html(
'<div class="tab-buttons">' +
'<button class="tab-button active" data-tab="menu-editor">📋 Menu Editor</button>' +
'<button class="tab-button" data-tab="ai-tools">🤖 AI Tools</button>' +
'<button class="tab-button" data-tab="page-tools">📄 Page Tools</button>' +
'</div>'
);
// Sub-namespace selector
var $subnsSelector = $('<div>', { 'class': 'leftmenu-subns-selector', 'style': 'display:none' }).html(
'<label>Sub-namespace:</label>' +
'<select class="leftmenu-subns-dropdown">' +
'<option value="">Loading...</option>' +
'</select>' +
'<div class="leftmenu-subns-actions">' +
'<button class="leftmenu-subns-add">+ Add New</button>' +
'<button class="leftmenu-subns-rename">✎ Rename</button>' +
'<button class="leftmenu-subns-delete">× Delete</button>' +
'</div>'
);
// Menu Editor Tab
var $menuTab = $('<div>', { 'class': 'tab-content active', 'id': 'menu-editor-tab' }).html(
'<div class="leftmenu-preview-panel">' +
'<div class="leftmenu-preview-header">' +
'<h4>Menu Structure - <span class="menu-namespace-title">' + LeftMenuEditor.namespace + '</span></h4>' +
'<input type="text" class="leftmenu-search" placeholder="Search items...">' +
'</div>' +
'<div class="leftmenu-preview-tree"></div>' +
'<div class="leftmenu-empty-state" style="display:none">' +
'<p>No menu items yet</p>' +
'<button class="leftmenu-button primary leftmenu-empty-add">Add First Item</button>' +
'</div>' +
'</div>' +
'<div class="leftmenu-controls">' +
'<h4>Actions</h4>' +
'<div class="leftmenu-quick-actions">' +
'<div class="leftmenu-quick-action" data-action="add-section">' +
'<span class="leftmenu-quick-action-icon">📁</span>' +
'<span class="leftmenu-quick-action-label">Add Section</span>' +
'</div>' +
'<div class="leftmenu-quick-action" data-action="add-page">' +
'<span class="leftmenu-quick-action-icon">📄</span>' +
'<span class="leftmenu-quick-action-label">Add Page</span>' +
'</div>' +
'<div class="leftmenu-quick-action" data-action="add-current">' +
'<span class="leftmenu-quick-action-icon">📌</span>' +
'<span class="leftmenu-quick-action-label">Current Page</span>' +
'</div>' +
'<div class="leftmenu-quick-action" data-action="star-toggle">' +
'<span class="leftmenu-quick-action-icon">⭐</span>' +
'<span class="leftmenu-quick-action-label">Star Priority</span>' +
'</div>' +
'</div>' +
'<div class="leftmenu-status"></div>' +
'<div class="leftmenu-action-panel" style="display:none;margin-bottom:20px">' +
'<div class="leftmenu-form"></div>' +
'</div>' +
'<div style="margin-top:auto">' +
'<button class="leftmenu-button primary leftmenu-save" style="width:100%;margin-bottom:10px">Save Changes</button>' +
'<div style="display:flex;gap:10px">' +
'<button class="leftmenu-button secondary leftmenu-reset" style="flex:1">Reset</button>' +
'<button class="leftmenu-button secondary leftmenu-view-json" style="flex:1">View JSON</button>' +
'</div>' +
'</div>' +
'<div class="leftmenu-json-view" style="display:none;margin-top:20px"></div>' +
'</div>'
);
// AI Tools Tab (keep existing)
var $aiTab = $('<div>', { 'class': 'tab-content', 'id': 'ai-tools-tab' }).html(
'<div class="ai-tools-container">' +
'<div class="ai-section">' +
'<h4>AI Content Assistant</h4>' +
'<div class="ai-actions">' +
'<button class="ai-button primary" id="ai-summarize">' +
'<span class="button-icon">📝</span> Summarize Page' +
'</button>' +
'<button class="ai-button primary" id="ai-fix-spelling">' +
'<span class="button-icon">✓</span> Fix Spelling' +
'</button>' +
'<button class="ai-button primary" id="ai-suggest-tags">' +
'<span class="button-icon">🏷️</span> Suggest Tags' +
'</button>' +
'</div>' +
'<div class="ai-result" id="ai-result" style="display:none">' +
'<div class="ai-result-header">' +
'<h5>Result</h5>' +
'<button class="ai-button-small" id="ai-copy">📋 Copy</button>' +
'</div>' +
'<div class="ai-result-content"></div>' +
'<div class="ai-result-actions">' +
'<button class="ai-button success" id="ai-apply-nutshell" style="display:none">🥜 Add as Nutshell</button>' +
'<button class="ai-button success" id="ai-apply-spelling" style="display:none">✅ Apply Corrections</button>' +
'</div>' +
'</div>' +
'</div>' +
'<div class="ai-section" id="tags-section" style="display:none">' +
'<h4>Page Tags</h4>' +
'<div class="tags-container">' +
'<div class="suggested-tags-section">' +
'<h5>AI Suggested Tags</h5>' +
'<div class="suggested-tags" id="suggested-tags"></div>' +
'</div>' +
'<div class="custom-tags-section">' +
'<h5>Add Custom Tags</h5>' +
'<div class="custom-tag-input">' +
'<input type="text" id="custom-tag-input" class="form-input" placeholder="Type a tag and press Enter">' +
'<button class="ai-button-small primary" id="add-custom-tag">+ Add</button>' +
'</div>' +
'</div>' +
'<div class="selected-tags-section">' +
'<h5>Selected Tags</h5>' +
'<div class="selected-tags" id="selected-tags"></div>' +
'</div>' +
'<div class="tags-actions">' +
'<button class="ai-button success" id="apply-tags">Apply Tags to Page</button>' +
'<button class="ai-button secondary" id="clear-tags">Clear All</button>' +
'</div>' +
'</div>' +
'</div>' +
'<div class="ai-status" style="display:none"></div>' +
'</div>'
);
// Page Tools Tab (keep existing)
var $pageTab = $('<div>', { 'class': 'tab-content', 'id': 'page-tools-tab' }).html(
'<div class="page-tools-container">' +
'<div class="page-section">' +
'<h4>Rename Page</h4>' +
'<div class="form-group-inline">' +
'<input type="text" id="new-page-name" class="form-input" placeholder="Enter new page name">' +
'<button class="ai-button primary" id="rename-page">Rename</button>' +
'</div>' +
'</div>' +
'<div class="page-section">' +
'<h4>Move to Namespace</h4>' +
'<div class="form-group-inline">' +
'<select id="target-namespace" class="form-select">' +
'<option value="">Loading namespaces...</option>' +
'</select>' +
'<button class="ai-button primary" id="move-namespace">Move Page</button>' +
'</div>' +
'</div>' +
'<div class="page-info">' +
'<h4>Current Page Info</h4>' +
'<div class="info-item"><strong>Page:</strong> <span id="current-page-name">' + mw.config.get('wgPageName') + '</span></div>' +
'<div class="info-item"><strong>Namespace:</strong> <span id="current-namespace">' + (mw.config.get('wgCanonicalNamespace') || 'Main') + '</span></div>' +
'</div>' +
'<div class="page-status" style="display:none"></div>' +
'</div>'
);
$body.append($tabs);
$body.append($subnsSelector);
$body.append($('<div class="tab-contents">').append($menuTab, $aiTab, $pageTab));
$container.append($header, $body);
// Insert at top of content
var $content = $('#content, #mw-content-text, .mw-body-content').first();
$content.prepend($container);
LeftMenuEditor.$editorBox = $container;
LeftMenuEditor.$preview = $container.find('.leftmenu-preview-tree');
// Add unified styles
addUnifiedStyles();
// Bind tab events
bindTabEvents();
// Bind all events
bindAIEvents();
bindPageToolsEvents();
bindTagsEvents();
bindSubNamespaceEvents();
// Load namespaces
loadNamespaces();
}
// Add unified styles
function addUnifiedStyles() {
var existingStyles = $('style').filter(function() {
return $(this).text().indexOf('.leftmenu-editor-container') > -1;
});
existingStyles.remove();
var css = [
// Keep all existing styles...
'.leftmenu-editor-container{position:relative;background:#ffffff;border:1px solid #ddd;margin:0 0 20px 0;box-shadow:0 2px 4px rgba(0,0,0,0.08);overflow:hidden;transition:all 0.3s ease}',
'.leftmenu-editor-container.collapsed .leftmenu-editor-body{max-height:0;padding:0 20px}',
'.leftmenu-editor-header{background:#f8f9fa;border-bottom:1px solid #ddd;padding:15px 20px;cursor:pointer;display:flex;justify-content:space-between;align-items:center;user-select:none}',
'.leftmenu-editor-header:hover{background:#f0f1f3}',
'.leftmenu-editor-header h3{margin:0;font-size:16px;font-weight:600;color:#333}',
'.leftmenu-editor-toggle{font-size:12px;color:#666;transition:transform 0.3s}',
'.leftmenu-editor-container.collapsed .leftmenu-editor-toggle{transform:rotate(-90deg)}',
'.leftmenu-editor-body{padding:0;max-height:700px;overflow-y:scroll;transition:all 0.3s ease}',
// Sub-namespace selector
'.leftmenu-subns-selector{background:#f0f0f0;border-bottom:1px solid #ddd;padding:15px 20px;display:flex;align-items:center;gap:20px}',
'.leftmenu-subns-selector label{font-weight:600;color:#333;font-size:14px}',
'.leftmenu-subns-dropdown{padding:8px 12px;border:1px solid #ddd;border-radius:4px;font-size:14px;min-width:200px}',
'.leftmenu-subns-actions{display:flex;gap:10px;margin-left:auto}',
'.leftmenu-subns-add{padding:6px 12px;background:#5cb85c;color:#fff;border:none;border-radius:4px;font-size:13px;cursor:pointer}',
'.leftmenu-subns-add:hover{background:#4cae4c}',
'.leftmenu-subns-delete{padding:6px 12px;background:#d9534f;color:#fff;border:none;border-radius:4px;font-size:13px;cursor:pointer}',
'.leftmenu-subns-delete:hover{background:#c9302c}',
'.leftmenu-subns-rename{padding:6px 12px;background:#f0ad4e;color:#fff;border:none;border-radius:4px;font-size:13px;cursor:pointer}',
'.leftmenu-subns-rename:hover{background:#ec971f}',
// Star indicator
'.leftmenu-star-indicator{color:#f0ad4e;font-weight:bold;margin-left:5px;font-size:16px;vertical-align:middle}',
'.leftmenu-visual-item.star-priority{background:#fff8e1;border-color:#f0ad4e}',
'.leftmenu-visual-item.star-priority:hover{background:#fff3cd}',
// Tab styles
'.wiki-tools-tabs{background:#f0f0f0;border-bottom:1px solid #ddd}',
'.tab-buttons{display:flex;padding:0}',
'.tab-button{flex:1;padding:12px 20px;border:none;background:transparent;cursor:pointer;font-size:14px;font-weight:500;color:#666;transition:all 0.2s;border-bottom:3px solid transparent}',
'.tab-button:hover{background:#e8e8e8}',
'.tab-button.active{color:#4a90e2;border-bottom-color:#4a90e2;background:#fff}',
'.tab-contents{background:#fff}',
'.tab-content{display:none;animation:fadeIn 0.3s}',
'.tab-content.active{display:grid;grid-template-columns:1fr 380px;gap:20px;padding:20px}',
'#ai-tools-tab.active, #page-tools-tab.active{display:block;padding:20px}',
'@keyframes fadeIn{from{opacity:0}to{opacity:1}}',
// Keep all other existing styles...
'.leftmenu-preview-panel{background:#fafafa;border:1px solid #e0e0e0;padding:20px;overflow-y:auto;max-height:560px}',
'.leftmenu-preview-header{display:flex;justify-content:space-between;align-items:center;margin-bottom:20px}',
'.leftmenu-preview-header h4{margin:0;color:#333;font-size:14px;font-weight:600;text-transform:uppercase;letter-spacing:0.5px}',
'.leftmenu-search{padding:6px 12px;border:1px solid #ddd;border-radius:4px;width:200px;font-size:13px}',
'.leftmenu-search:focus{outline:none;border-color:#4a90e2;box-shadow:0 0 0 2px rgba(74,144,226,0.2)}',
'.leftmenu-preview-tree{font-size:14px}',
'.leftmenu-visual-item{background:#fff;border:1px solid #e0e0e0;padding:10px 14px;margin:4px 0;cursor:move;transition:all 0.2s;display:flex;justify-content:space-between;align-items:center;color:#333;position:relative}',
'.leftmenu-visual-item:hover{background:#f8f9fa;border-color:#4a90e2;box-shadow:0 2px 4px rgba(0,0,0,0.08)}',
'.leftmenu-visual-item.is-current-page{background:#e8f4fd;border-color:#4a90e2;font-weight:600}',
'.leftmenu-visual-item.selected{background:#4a90e2;color:#fff;border-color:#3a7bc8}',
'.leftmenu-visual-section{background:#fff;border:1px solid #ddd;margin:8px 0;overflow:visible}',
'.leftmenu-visual-section-header{background:#f0f0f0;padding:10px 14px;cursor:pointer;display:flex;align-items:center;font-weight:600;color:#333;font-size:14px;border-bottom:1px solid #ddd;user-select:none}',
'.leftmenu-visual-section-header:hover{background:#e8e8e8}',
'.leftmenu-section-toggle{font-size:10px;color:#666;margin-right:8px;transition:transform 0.2s}',
'.leftmenu-visual-section.collapsed .leftmenu-section-toggle{transform:rotate(-90deg)}',
'.leftmenu-visual-section.collapsed .leftmenu-section-items{display:none}',
'.leftmenu-section-items{padding:8px;background:#fafafa;min-height:20px}',
'.leftmenu-section-items .leftmenu-visual-section{margin:4px 0;border-color:#e0e0e0}',
'.leftmenu-section-items .leftmenu-visual-section-header{background:#f8f8f8;font-size:13px;padding:8px 12px}',
'.leftmenu-section-items .leftmenu-visual-item{font-size:13px}',
'.ui-sortable-helper{opacity:0.8 !important}',
'.leftmenu-section-items .leftmenu-drag-placeholder{margin-left:0 !important}',
'.leftmenu-visual-item.direct-link{background:#fffbf0;border-color:#f0ad4e}',
'.leftmenu-visual-item.direct-link:hover{background:#fff8e1;border-color:#ec971f}',
'.leftmenu-item-count{background:#4a90e2;color:#fff;padding:2px 6px;border-radius:10px;font-size:11px;margin-left:auto;margin-right:10px}',
'.leftmenu-icon{display:inline-block;width:16px;text-align:center;margin-right:6px;opacity:0.6}',
'.leftmenu-dragging{opacity:0.4}',
'.leftmenu-drag-placeholder{background:#4a90e2;opacity:0.2;height:36px;margin:4px 0;border:2px dashed #4a90e2}',
'.leftmenu-item-actions{opacity:0;transition:opacity 0.2s;display:flex;gap:4px}',
'.leftmenu-visual-item:hover .leftmenu-item-actions,.leftmenu-visual-section-header:hover .leftmenu-item-actions{opacity:1}',
'.leftmenu-visual-item.selected .leftmenu-item-actions{opacity:1}',
'.leftmenu-item-actions button{background:#fff;border:1px solid #ddd;width:28px;height:28px;border-radius:4px;cursor:pointer;font-size:12px;transition:all 0.15s;padding:0;display:flex;align-items:center;justify-content:center}',
'.leftmenu-item-actions button:hover{background:#f8f9fa;border-color:#999}',
'.leftmenu-item-edit{color:#4a90e2}',
'.leftmenu-item-delete{color:#d9534f}',
'.leftmenu-item-duplicate{color:#5cb85c}',
'.leftmenu-item-star{color:#f0ad4e}',
'.leftmenu-item-star.active{background:#f0ad4e;color:#fff}',
'.leftmenu-controls{background:#fff;border:1px solid #e0e0e0;padding:20px;overflow-y:auto;display:flex;flex-direction:column}',
'.leftmenu-controls h4{margin:0 0 16px 0;color:#333;font-size:14px;font-weight:600;text-transform:uppercase;letter-spacing:0.5px}',
'.leftmenu-quick-actions{display:grid;grid-template-columns:1fr 1fr;gap:10px;margin-bottom:20px}',
'.leftmenu-quick-action{background:#f8f9fa;border:1px solid #ddd;padding:12px;text-align:center;cursor:pointer;transition:all 0.2s;border-radius:4px}',
'.leftmenu-quick-action:hover{border-color:#4a90e2;background:#fff;box-shadow:0 2px 4px rgba(0,0,0,0.08)}',
'.leftmenu-quick-action-icon{font-size:20px;margin-bottom:4px;display:block}',
'.leftmenu-quick-action-label{font-size:12px;color:#666}',
'.leftmenu-form-group{margin-bottom:16px}',
'.leftmenu-form-label{display:block;margin-bottom:4px;color:#333;font-size:13px;font-weight:600}',
'.leftmenu-form-input,.leftmenu-form-select{width:100%;padding:8px 12px;border:1px solid #ddd;border-radius:4px;font-size:13px}',
'.leftmenu-form-input:focus,.leftmenu-form-select:focus{outline:none;border-color:#4a90e2;box-shadow:0 0 0 2px rgba(74,144,226,0.2)}',
'.leftmenu-button{padding:8px 16px;border:1px solid #ddd;border-radius:4px;font-size:13px;font-weight:600;cursor:pointer;transition:all 0.2s;background:#fff}',
'.leftmenu-button:hover{background:#f8f9fa}',
'.leftmenu-button.primary{background:#4a90e2;color:#fff;border-color:#4a90e2}',
'.leftmenu-button.primary:hover{background:#3a7bc8;border-color:#3a7bc8}',
'.leftmenu-button.secondary{background:#f8f9fa;color:#333}',
'.leftmenu-button.danger{background:#d9534f;color:#fff;border-color:#d9534f}',
'.leftmenu-button.success{background:#5cb85c;color:#fff;border-color:#5cb85c}',
'.leftmenu-button:disabled{opacity:0.6;cursor:not-allowed}',
'.leftmenu-status{padding:10px 12px;margin:10px 0;font-size:13px;display:none;border-radius:4px}',
'.leftmenu-status.success{background:#dff0d8;color:#3c763d;border:1px solid #d6e9c6}',
'.leftmenu-status.error{background:#f2dede;color:#a94442;border:1px solid #ebccd1}',
'.leftmenu-status.info{background:#d9edf7;color:#31708f;border:1px solid #bce8f1}',
'.leftmenu-json-view{background:#f5f5f5;border:1px solid #ddd;padding:12px;font-family:monospace;font-size:12px;line-height:1.4;border-radius:4px;max-height:300px;overflow-y:auto}',
'.leftmenu-loading{text-align:center;padding:40px;color:#999}',
'.leftmenu-spinner{display:inline-block;width:20px;height:20px;border:2px solid #f3f3f3;border-top:2px solid #4a90e2;border-radius:50%;animation:spin 1s linear infinite}',
'@keyframes spin{0%{transform:rotate(0deg)}100%{transform:rotate(360deg)}}',
'.leftmenu-empty-state{text-align:center;padding:40px;color:#999}',
'.leftmenu-empty-state button{margin-top:10px}',
// AI Tools styles
'.ai-tools-container{max-width:800px;margin:0 auto}',
'.ai-section, .page-section{background:#fafafa;border:1px solid #e0e0e0;padding:25px;margin-bottom:20px;border-radius:8px}',
'.ai-section h4, .page-section h4{margin:0 0 20px 0;color:#333;font-size:16px;font-weight:600}',
'.ai-section h5{margin:0 0 10px 0;color:#555;font-size:14px;font-weight:600}',
'.ai-actions{display:flex;gap:15px;flex-wrap:wrap}',
'.ai-button, .ai-button-small{padding:12px 24px;border:none;border-radius:6px;font-size:14px;font-weight:600;cursor:pointer;transition:all 0.2s;display:inline-flex;align-items:center;gap:8px}',
'.ai-button-small{padding:6px 12px;font-size:12px}',
'.button-icon{font-size:18px}',
'.ai-button.primary{background:#4a90e2;color:#fff}',
'.ai-button.primary:hover{background:#3a7bc8;transform:translateY(-1px);box-shadow:0 4px 12px rgba(74,144,226,0.3)}',
'.ai-button.success{background:#5cb85c;color:#fff}',
'.ai-button.success:hover{background:#4cae4c}',
'.ai-button.secondary{background:#6c757d;color:#fff}',
'.ai-button.secondary:hover{background:#5a6268}',
'.ai-button:disabled{opacity:0.6;cursor:not-allowed;transform:none !important}',
'.ai-result{margin-top:20px;background:#fff;border:1px solid #ddd;border-radius:8px;overflow:hidden}',
'.ai-result-header{background:#f8f9fa;padding:15px 20px;border-bottom:1px solid #e0e0e0;display:flex;justify-content:space-between;align-items:center}',
'.ai-result-header h5{margin:0;color:#333;font-size:14px;font-weight:600}',
'.ai-result-content{padding:20px;font-size:14px;line-height:1.8;color:#333;max-height:300px;overflow-y:auto}',
'.ai-result-actions{padding:15px 20px;background:#f8f9fa;border-top:1px solid #e0e0e0;display:flex;gap:10px}',
'.ai-status, .page-status{padding:15px 20px;margin-top:20px;border-radius:8px;font-size:14px;font-weight:500}',
'.ai-status.success, .page-status.success{background:#d4edda;color:#155724;border:1px solid #c3e6cb}',
'.ai-status.error, .page-status.error{background:#f8d7da;color:#721c24;border:1px solid #f5c6cb}',
'.ai-status.info, .page-status.info{background:#d1ecf1;color:#0c5460;border:1px solid #bee5eb}',
'.ai-loading{display:inline-block;width:16px;height:16px;border:2px solid #f3f3f3;border-top:2px solid #4a90e2;border-radius:50%;animation:spin 1s linear infinite;margin-left:10px}',
// Tags styles
'.tags-container{display:flex;flex-direction:column;gap:20px}',
'.suggested-tags-section, .custom-tags-section, .selected-tags-section{background:#fff;padding:15px;border:1px solid #e0e0e0;border-radius:6px}',
'.suggested-tags, .selected-tags{display:flex;flex-wrap:wrap;gap:8px;min-height:40px;padding:10px;background:#f8f9fa;border:1px solid #e0e0e0;border-radius:4px}',
'.tag{padding:6px 12px;background:#e0e0e0;border-radius:20px;font-size:13px;cursor:pointer;transition:all 0.2s;display:inline-flex;align-items:center;gap:6px}',
'.tag:hover{background:#d0d0d0;transform:translateY(-1px)}',
'.tag.suggested{background:#e3f2fd;color:#1976d2}',
'.tag.suggested:hover{background:#bbdefb}',
'.tag.selected{background:#4a90e2;color:#fff}',
'.tag-remove{font-size:16px;cursor:pointer;margin-left:4px;opacity:0.7}',
'.tag-remove:hover{opacity:1}',
'.custom-tag-input{display:flex;gap:10px;align-items:center}',
'.custom-tag-input input{flex:1}',
'.tags-actions{display:flex;gap:10px;margin-top:15px}',
// Page Tools styles
'.page-tools-container{max-width:800px;margin:0 auto}',
'.form-group-inline{display:flex;gap:15px;align-items:center}',
'.form-group-inline .form-input, .form-group-inline .form-select{flex:1}',
'.form-input, .form-select{padding:10px 15px;border:1px solid #ddd;border-radius:6px;font-size:14px}',
'.form-input:focus, .form-select:focus{outline:none;border-color:#4a90e2;box-shadow:0 0 0 3px rgba(74,144,226,0.1)}',
'.page-info{background:#e8f4fd;border:1px solid #bee5eb;padding:20px;border-radius:8px;margin-top:20px}',
'.page-info h4{margin:0 0 15px 0;color:#0c5460}',
'.info-item{font-size:14px;margin-bottom:8px}',
'.info-item strong{color:#333}',
// Responsive
'@media (max-width: 900px){',
'.tab-content.active{grid-template-columns:1fr}',
'.leftmenu-controls{border-top:1px solid #e0e0e0;margin-top:20px}',
'.tab-button{font-size:12px;padding:10px}',
'.button-icon{font-size:16px}',
'}'
].join('');
$('<style>').text(css).appendTo('head');
}
// Bind tab events
function bindTabEvents() {
$('.tab-button').on('click', function() {
var $btn = $(this);
var targetTab = $btn.data('tab');
// Update buttons
$('.tab-button').removeClass('active');
$btn.addClass('active');
// Update content
$('.tab-content').removeClass('active');
$('#' + targetTab + '-tab').addClass('active');
// Show/hide sub-namespace selector for menu editor
if (targetTab === 'menu-editor') {
$('.leftmenu-subns-selector').show();
if (!LeftMenuEditor.subNamespaces.length) {
loadSubNamespaces();
}
} else {
$('.leftmenu-subns-selector').hide();
}
});
}
// Load sub-namespaces
function loadSubNamespaces() {
var page = 'MediaWiki:Menu-' + LeftMenuEditor.namespace;
showStatus('Loading sub-namespaces...', 'info');
LeftMenuEditor.api.get({
action: 'query',
prop: 'revisions',
titles: page,
rvprop: 'content',
rvslots: 'main',
formatversion: 2
}).done(function(data) {
var content = '';
var pageExists = false;
if (data.query && data.query.pages && data.query.pages[0]) {
var pageData = data.query.pages[0];
pageExists = !pageData.missing;
if (pageData.revisions && pageData.revisions[0]) {
content = pageData.revisions[0].slots.main.content || '';
}
}
// If page doesn't exist or is empty, no sub-namespaces
if (!pageExists || !content.trim()) {
LeftMenuEditor.subNamespaces = [];
$('.leftmenu-subns-selector').hide();
loadMenuData();
return;
}
// Check if content is JSON (direct menu) or sub-namespace list
var trimmedContent = content.trim();
if (trimmedContent && (trimmedContent[0] === '{' || trimmedContent[0] === '[')) {
// This is a direct JSON menu, not a sub-namespace list
LeftMenuEditor.subNamespaces = [];
$('.leftmenu-subns-selector').hide();
loadMenuData();
return;
}
// Parse sub-namespaces (lines starting with * or #)
var lines = content.split('\n');
var subNs = [];
for (var i = 0; i < lines.length; i++) {
var line = lines[i].trim();
if (line) {
// Accept lines with or without * or # prefix
if (line[0] === '*' || line[0] === '#') {
var name = line.substring(1).trim();
if (name) {
subNs.push(name);
}
} else if (line && !line.startsWith('{') && !line.startsWith('[')) {
// If the line doesn't start with * or # but also isn't JSON, treat it as a sub-namespace
subNs.push(line);
}
}
}
LeftMenuEditor.subNamespaces = subNs;
// Update dropdown
var $dropdown = $('.leftmenu-subns-dropdown');
$dropdown.empty();
if (subNs.length === 0) {
$dropdown.append('<option value="">No sub-namespaces defined</option>');
$('.leftmenu-subns-selector').hide();
// Load the main namespace menu directly
loadMenuData();
} else {
$('.leftmenu-subns-selector').show();
for (var j = 0; j < subNs.length; j++) {
$dropdown.append('<option value="' + escapeHtml(subNs[j]) + '">' + escapeHtml(subNs[j]) + '</option>');
}
// Set first sub-namespace as current
LeftMenuEditor.currentSubNamespace = subNs[0];
$dropdown.val(subNs[0]);
// Load menu for first sub-namespace
loadSubNamespaceMenu(subNs[0]);
}
updateMenuTitle();
$('.leftmenu-status').fadeOut(3000);
}).fail(function() {
showStatus('Failed to load sub-namespaces', 'error');
$('.leftmenu-subns-selector').hide();
loadMenuData(); // Fallback to direct menu loading
});
}
// Load sub-namespace menu
function loadSubNamespaceMenu(subNs) {
if (!subNs) {
LeftMenuEditor.menuData = {};
renderVisualMenu();
return;
}
var page = 'MediaWiki:Menu-' + LeftMenuEditor.namespace + '-' + subNs;
showStatus('Loading menu for ' + subNs + '...', 'info');
LeftMenuEditor.api.get({
action: 'query',
prop: 'revisions',
titles: page,
rvprop: 'content',
rvslots: 'main',
formatversion: 2
}).done(function(data) {
var raw = '';
var pageExists = false;
if (data.query && data.query.pages && data.query.pages[0]) {
var page = data.query.pages[0];
pageExists = !page.missing;
if (page.revisions && page.revisions[0] && page.revisions[0].slots) {
raw = page.revisions[0].slots.main.content || '';
}
}
// Parse JSON
if (raw) {
try {
LeftMenuEditor.menuData = JSON.parse(raw);
} catch (e) {
LeftMenuEditor.menuData = {};
showStatus('Error parsing menu data for ' + subNs + ': ' + e.message, 'error');
}
} else {
LeftMenuEditor.menuData = {};
if (!pageExists) {
showStatus('Menu for ' + subNs + ' doesn\'t exist yet. Add items to create it.', 'info');
}
}
// Store in cache
LeftMenuEditor.subNamespaceMenus[subNs] = LeftMenuEditor.menuData;
renderVisualMenu();
$('.leftmenu-status').fadeOut(5000);
}).fail(function() {
showStatus('Failed to load menu data for ' + subNs, 'error');
LeftMenuEditor.menuData = {};
renderVisualMenu();
});
}
// Update menu title
function updateMenuTitle() {
var title = LeftMenuEditor.namespace;
if (LeftMenuEditor.currentSubNamespace) {
title += ' - ' + LeftMenuEditor.currentSubNamespace;
}
$('.menu-namespace-title').text(title);
}
// Bind sub-namespace events
function bindSubNamespaceEvents() {
// Sub-namespace dropdown change
$('.leftmenu-subns-dropdown').on('change', function() {
var newSubNs = $(this).val();
if (newSubNs !== LeftMenuEditor.currentSubNamespace) {
LeftMenuEditor.currentSubNamespace = newSubNs;
updateMenuTitle();
// Check cache first
if (LeftMenuEditor.subNamespaceMenus[newSubNs]) {
LeftMenuEditor.menuData = LeftMenuEditor.subNamespaceMenus[newSubNs];
renderVisualMenu();
} else {
loadSubNamespaceMenu(newSubNs);
}
}
});
// Add sub-namespace
$('.leftmenu-subns-add').on('click', function() {
var name = prompt('Enter new sub-namespace name:');
if (name && name.trim()) {
name = name.trim();
// Check if already exists
if (LeftMenuEditor.subNamespaces.indexOf(name) !== -1) {
showStatus('Sub-namespace already exists', 'error');
return;
}
// Add to list
LeftMenuEditor.subNamespaces.push(name);
// Save updated list
saveSubNamespaceList(function() {
// Update dropdown
$('.leftmenu-subns-dropdown').append('<option value="' + escapeHtml(name) + '">' + escapeHtml(name) + '</option>');
$('.leftmenu-subns-dropdown').val(name);
// Switch to new sub-namespace
LeftMenuEditor.currentSubNamespace = name;
LeftMenuEditor.menuData = {};
updateMenuTitle();
renderVisualMenu();
showStatus('Sub-namespace added: ' + name, 'success');
});
}
});
// Rename sub-namespace
$('.leftmenu-subns-rename').on('click', function() {
var current = LeftMenuEditor.currentSubNamespace;
if (!current) {
showStatus('No sub-namespace selected', 'error');
return;
}
var newName = prompt('Rename "' + current + '" to:', current);
if (newName && newName.trim() && newName !== current) {
newName = newName.trim();
// Update in list
var idx = LeftMenuEditor.subNamespaces.indexOf(current);
if (idx !== -1) {
LeftMenuEditor.subNamespaces[idx] = newName;
// Move the menu page
moveSubNamespacePage(current, newName, function() {
// Save updated list
saveSubNamespaceList(function() {
// Update UI
$('.leftmenu-subns-dropdown option[value="' + escapeHtml(current) + '"]')
.val(newName).text(newName);
LeftMenuEditor.currentSubNamespace = newName;
updateMenuTitle();
showStatus('Sub-namespace renamed', 'success');
});
});
}
}
});
// Delete sub-namespace
$('.leftmenu-subns-delete').on('click', function() {
var current = LeftMenuEditor.currentSubNamespace;
if (!current) {
showStatus('No sub-namespace selected', 'error');
return;
}
if (confirm('Delete sub-namespace "' + current + '"?\n\nThis will also delete all menu items in this sub-namespace.')) {
// Remove from list
var idx = LeftMenuEditor.subNamespaces.indexOf(current);
if (idx !== -1) {
LeftMenuEditor.subNamespaces.splice(idx, 1);
// Save updated list
saveSubNamespaceList(function() {
// Delete the menu page
deleteSubNamespacePage(current, function() {
// Update UI
$('.leftmenu-subns-dropdown option[value="' + escapeHtml(current) + '"]').remove();
// Switch to first available
if (LeftMenuEditor.subNamespaces.length > 0) {
var first = LeftMenuEditor.subNamespaces[0];
$('.leftmenu-subns-dropdown').val(first);
LeftMenuEditor.currentSubNamespace = first;
loadSubNamespaceMenu(first);
} else {
LeftMenuEditor.currentSubNamespace = null;
LeftMenuEditor.menuData = {};
renderVisualMenu();
}
updateMenuTitle();
showStatus('Sub-namespace deleted', 'success');
});
});
}
}
});
}
// Save sub-namespace list
function saveSubNamespaceList(callback) {
var content = '';
for (var i = 0; i < LeftMenuEditor.subNamespaces.length; i++) {
content += '* ' + LeftMenuEditor.subNamespaces[i] + '\n';
}
var page = 'MediaWiki:Menu-' + LeftMenuEditor.namespace;
// If no sub-namespaces left, we might want to delete the page or leave it empty
if (LeftMenuEditor.subNamespaces.length === 0) {
content = ''; // Empty content
}
LeftMenuEditor.api.postWithToken('csrf', {
action: 'edit',
title: page,
text: content,
summary: 'Updated sub-namespace list',
contentformat: 'text/x-wiki',
contentmodel: 'wikitext'
}).done(function() {
if (callback) callback();
}).fail(function(err) {
showStatus('Failed to save sub-namespace list: ' + (err.error ? err.error.info : err), 'error');
});
}
// Move sub-namespace page
function moveSubNamespacePage(oldName, newName, callback) {
var oldPage = 'MediaWiki:Menu-' + LeftMenuEditor.namespace + '-' + oldName;
var newPage = 'MediaWiki:Menu-' + LeftMenuEditor.namespace + '-' + newName;
LeftMenuEditor.api.postWithToken('csrf', {
action: 'move',
from: oldPage,
to: newPage,
reason: 'Renamed sub-namespace',
noredirect: true
}).done(function() {
if (callback) callback();
}).fail(function(err) {
showStatus('Failed to move menu page: ' + (err.error ? err.error.info : err), 'error');
});
}
// Delete sub-namespace page
function deleteSubNamespacePage(name, callback) {
var page = 'MediaWiki:Menu-' + LeftMenuEditor.namespace + '-' + name;
LeftMenuEditor.api.postWithToken('csrf', {
action: 'delete',
title: page,
reason: 'Deleted sub-namespace'
}).done(function() {
if (callback) callback();
}).fail(function(err) {
// Page might not exist, that's okay
if (callback) callback();
});
}
// Parse and render menu structure
function parseMenuItem(key, value, level) {
level = level || 0;
// Check for star priority
var isStarPriority = false;
var displayKey = key;
if (key && key[0] === '*') {
isStarPriority = true;
displayKey = key.substring(1).trim();
}
// Handle different value types
if (value === null || value === undefined || value === '') {
// Item with no value
return createVisualItem(displayKey, '', 'item', level, isStarPriority);
} else if (typeof value === 'string') {
// Check if value starts with * for star priority
if (value && value[0] === '*') {
isStarPriority = true;
value = value.substring(1).trim();
}
// String value
if (displayKey.indexOf('//') === 0) {
return createVisualItem(displayKey, value, 'direct', level, isStarPriority);
} else {
return createVisualItem(displayKey, value, 'item', level, isStarPriority);
}
} else if (Array.isArray(value)) {
// Array handling
if (value.length === 0) {
return createVisualItem(displayKey, '', 'item', level, isStarPriority);
} else if (value.length === 1 && typeof value[0] === 'string') {
var val = value[0];
if (val && val[0] === '*') {
isStarPriority = true;
val = val.substring(1).trim();
}
return createVisualItem(displayKey, val, 'item', level, isStarPriority);
} else {
// Create section for array with multiple items
var $section = createVisualSection(displayKey, level);
var $items = $section.find('.leftmenu-section-items');
for (var i = 0; i < value.length; i++) {
if (typeof value[i] === 'string') {
var itemVal = value[i];
var itemStar = false;
if (itemVal && itemVal[0] === '*') {
itemStar = true;
itemVal = itemVal.substring(1).trim();
}
$items.append(createVisualItem(itemVal, itemVal, 'item', level + 1, itemStar));
} else if (typeof value[i] === 'object') {
for (var subKey in value[i]) {
if (value[i].hasOwnProperty(subKey)) {
$items.append(parseMenuItem(subKey, value[i][subKey], level + 1));
}
}
}
}
return $section;
}
} else if (typeof value === 'object' && value !== null) {
// Object - create section
if (displayKey.indexOf('//') === 0 && Object.keys(value).length === 0) {
return createVisualItem(displayKey, '', 'direct', level, isStarPriority);
}
var $section = createVisualSection(displayKey, level);
var $items = $section.find('.leftmenu-section-items');
// Process nested items
for (var subKey in value) {
if (value.hasOwnProperty(subKey)) {
$items.append(parseMenuItem(subKey, value[subKey], level + 1));
}
}
return $section;
}
// Fallback
return createVisualItem(displayKey, String(value), 'item', level, isStarPriority);
}
// Create visual item
function createVisualItem(key, value, type, level, isStarPriority) {
var displayText = key;
var targetPage = value || key;
// Parse display text
if (type === 'direct' && key.match(/^\/\/(.+?)\s*\|\s*(.+)$/)) {
var match = key.match(/^\/\/(.+?)\s*\|\s*(.+)$/);
displayText = match[2].trim();
targetPage = match[1].trim();
} else if (value && typeof value === 'string') {
if (value.indexOf('|') > -1) {
var parts = value.split('|');
targetPage = parts[0].trim();
displayText = parts[1].trim();
} else if (value.match(/^\[([^\]]+)\]\s*\|\s*(.+)$/)) {
var bracketMatch = value.match(/^\[([^\]]+)\]\s*\|\s*(.+)$/);
targetPage = bracketMatch[1];
displayText = bracketMatch[2];
}
}
var isCurrentPage = (targetPage === LeftMenuEditor.currentPage ||
targetPage === LeftMenuEditor.pageName ||
key === LeftMenuEditor.currentPage ||
key === LeftMenuEditor.pageName);
var icon = type === 'direct' ? '→' : '•';
var $item = $('<div>', {
'class': 'leftmenu-visual-item' +
(type === 'direct' ? ' direct-link' : '') +
(isCurrentPage ? ' is-current-page' : '') +
(isStarPriority ? ' star-priority' : ''),
'data-key': key,
'data-value': value || '',
'data-type': type,
'data-level': level,
'data-star': isStarPriority ? 'true' : 'false'
}).css('margin-left', (level * 20) + 'px').html(
'<span><span class="leftmenu-icon">' + icon + '</span>' + escapeHtml(displayText) +
(isStarPriority ? '<span class="leftmenu-star-indicator">★</span>' : '') + '</span>' +
'<div class="leftmenu-item-actions">' +
'<button class="leftmenu-item-star' + (isStarPriority ? ' active' : '') + '" title="Toggle Star Priority">★</button>' +
'<button class="leftmenu-item-duplicate" title="Duplicate">⊕</button>' +
'<button class="leftmenu-item-edit" title="Edit">✎</button>' +
'<button class="leftmenu-item-delete" title="Delete">×</button>' +
'</div>'
);
// Event handlers
$item.on('click', function(e) {
if (!$(e.target).closest('.leftmenu-item-actions').length) {
selectItem($item);
}
});
$item.find('.leftmenu-item-star').on('click', function(e) {
e.stopPropagation();
toggleStarPriority($item);
});
$item.find('.leftmenu-item-edit').on('click', function(e) {
e.stopPropagation();
editItemInline($item);
});
$item.find('.leftmenu-item-delete').on('click', function(e) {
e.stopPropagation();
deleteItem($item);
});
$item.find('.leftmenu-item-duplicate').on('click', function(e) {
e.stopPropagation();
duplicateItem($item);
});
return $item;
}
// Toggle star priority
function toggleStarPriority($item) {
var isStarred = $item.data('star') === 'true';
if (isStarred) {
$item.data('star', 'false');
$item.removeClass('star-priority');
$item.find('.leftmenu-star-indicator').remove();
$item.find('.leftmenu-item-star').removeClass('active');
} else {
$item.data('star', 'true');
$item.addClass('star-priority');
if (!$item.find('.leftmenu-star-indicator').length) {
$item.find('span').first().append('<span class="leftmenu-star-indicator">★</span>');
}
$item.find('.leftmenu-item-star').addClass('active');
}
updateMenuFromVisual();
showStatus('Star priority ' + (isStarred ? 'removed' : 'added'), 'success');
}
// Update menu from visual
function updateMenuFromVisual() {
var newData = {};
function processLevel($container, targetObj) {
$container.children().each(function() {
var $elem = $(this);
if ($elem.hasClass('leftmenu-visual-section')) {
var sectionTitle = $elem.data('section');
var sectionData = {};
processLevel($elem.find('.leftmenu-section-items').first(), sectionData);
targetObj[sectionTitle] = sectionData;
} else if ($elem.hasClass('leftmenu-visual-item')) {
var key = $elem.data('key');
var value = $elem.data('value');
var isStarred = $elem.data('star') === 'true';
// Add star prefix if needed
if (isStarred) {
key = '* ' + key;
}
if (!value || value === key) {
targetObj[key] = key;
} else {
targetObj[key] = value;
}
}
});
}
processLevel(LeftMenuEditor.$preview, newData);
LeftMenuEditor.menuData = newData;
}
// Save menu
function saveMenu() {
var page;
// If we have sub-namespaces but none selected, prevent saving
if (LeftMenuEditor.subNamespaces.length > 0 && !LeftMenuEditor.currentSubNamespace) {
showStatus('Please select a sub-namespace before saving', 'error');
return;
}
if (LeftMenuEditor.currentSubNamespace) {
page = 'MediaWiki:Menu-' + LeftMenuEditor.namespace + '-' + LeftMenuEditor.currentSubNamespace;
} else {
page = 'MediaWiki:Menu-' + LeftMenuEditor.namespace;
}
var json = JSON.stringify(LeftMenuEditor.menuData, null, 2);
$('.leftmenu-save').prop('disabled', true).text('Saving...');
LeftMenuEditor.api.postWithToken('csrf', {
action: 'edit',
title: page,
text: json,
summary: 'Updated menu structure' + (LeftMenuEditor.currentSubNamespace ? ' for ' + LeftMenuEditor.currentSubNamespace : ''),
contentformat: 'text/x-wiki',
contentmodel: 'wikitext'
}).done(function () {
showStatus('Menu saved successfully!', 'success');
$('.leftmenu-save').text('Saved!');
setTimeout(function () {
location.reload();
}, 1500);
}).fail(function (err) {
showStatus('Failed to save: ' + (err.error ? err.error.info : err), 'error');
$('.leftmenu-save').prop('disabled', false).text('Save Changes');
});
}
// Keep all other existing functions (createVisualSection, renderVisualMenu, etc.)
// ... [Rest of the existing functions remain the same]
// Create visual section
function createVisualSection(title, level) {
var $section = $('<div>', {
'class': 'leftmenu-visual-section',
'data-section': title,
'data-level': level
}).css('margin-left', (level * 20) + 'px').html(
'<div class="leftmenu-visual-section-header">' +
'<span class="leftmenu-section-toggle">▼</span>' +
'<span>' + escapeHtml(title) + '</span>' +
'<div class="leftmenu-item-actions" style="margin-left:auto">' +
'<button class="leftmenu-item-edit" title="Rename">✎</button>' +
'<button class="leftmenu-item-delete" title="Delete">×</button>' +
'</div>' +
'</div>' +
'<div class="leftmenu-section-items"></div>'
);
$section.find('.leftmenu-visual-section-header').on('click', function(e) {
if (!$(e.target).closest('.leftmenu-item-actions').length) {
$section.toggleClass('collapsed');
}
});
$section.find('.leftmenu-item-edit').on('click', function(e) {
e.stopPropagation();
editSectionInline($section);
});
$section.find('.leftmenu-item-delete').on('click', function(e) {
e.stopPropagation();
deleteSection($section);
});
return $section;
}
// Render visual menu
function renderVisualMenu() {
var $preview = LeftMenuEditor.$preview;
var $empty = $('.leftmenu-empty-state');
$preview.empty();
// Check if menuData is null or empty
if (!LeftMenuEditor.menuData || Object.keys(LeftMenuEditor.menuData).length === 0) {
$preview.hide();
// Update empty state message based on context
if (LeftMenuEditor.currentSubNamespace) {
$empty.find('p').text('No menu items in ' + LeftMenuEditor.currentSubNamespace + ' yet');
$empty.find('button').show();
} else if (LeftMenuEditor.subNamespaces.length > 0) {
$empty.find('p').text('Please select a sub-namespace from the dropdown above');
$empty.find('button').hide();
} else {
$empty.find('p').text('No menu items yet');
$empty.find('button').show();
}
$empty.show();
} else {
$preview.show();
$empty.hide();
// Render each top-level item
for (var key in LeftMenuEditor.menuData) {
if (LeftMenuEditor.menuData.hasOwnProperty(key)) {
var $element = parseMenuItem(key, LeftMenuEditor.menuData[key], 0);
if ($element) {
$preview.append($element);
}
}
}
makeSortable();
updateSectionCounts();
}
}
// Update section counts
function updateSectionCounts() {
$('.leftmenu-visual-section').each(function() {
var $section = $(this);
var count = $section.find('.leftmenu-section-items > *').length;
var $header = $section.find('.leftmenu-visual-section-header');
$header.find('.leftmenu-item-count').remove();
if (count > 0) {
var $count = $('<span class="leftmenu-item-count">' + count + '</span>');
$header.find('.leftmenu-item-actions').before($count);
}
});
}
// Select item
function selectItem($item) {
$('.leftmenu-visual-item, .leftmenu-visual-section').removeClass('selected');
$item.addClass('selected');
LeftMenuEditor.selectedItem = $item;
}
// Edit item inline
function editItemInline($item) {
var $span = $item.find('span').first();
var currentText = $span.text().replace(/^[•→]\s*/, '').replace(/★$/, '').trim();
var $input = $('<input>', {
'type': 'text',
'class': 'leftmenu-form-input',
'value': currentText,
'style': 'width:calc(100% - 100px);margin:0'
});
$span.html($input);
$input.focus().select();
function saveEdit() {
var newText = $input.val().trim();
if (newText && newText !== currentText) {
updateItemDisplay($item, newText);
showStatus('Item updated', 'success');
} else {
var isStarred = $item.data('star') === 'true';
$span.html('<span class="leftmenu-icon">' +
($item.hasClass('direct-link') ? '→' : '•') +
'</span>' + escapeHtml(currentText) +
(isStarred ? '<span class="leftmenu-star-indicator">★</span>' : ''));
}
}
$input.on('blur', saveEdit);
$input.on('keypress', function(e) {
if (e.which === 13) {
e.preventDefault();
saveEdit();
}
});
}
// Update item display
function updateItemDisplay($item, newDisplay) {
var key = $item.data('key');
var currentValue = $item.data('value') || '';
var basePage = key;
var isStarred = $item.data('star') === 'true';
if (currentValue && currentValue.indexOf('|') > -1) {
basePage = currentValue.split('|')[0];
} else if (currentValue.match(/^\[([^\]]+)\]/)) {
basePage = currentValue.match(/^(\[[^\]]+\])/)[1];
}
var newValue = basePage + '|' + newDisplay;
$item.data('value', newValue);
$item.find('span').first().html(
'<span class="leftmenu-icon">' +
($item.hasClass('direct-link') ? '→' : '•') +
'</span>' + escapeHtml(newDisplay) +
(isStarred ? '<span class="leftmenu-star-indicator">★</span>' : '')
);
updateMenuFromVisual();
}
// Delete item
function deleteItem($item) {
var name = $item.find('span').first().text().replace(/^[•→]\s*/, '').replace(/★$/, '').trim();
if (confirm('Delete "' + name + '"?')) {
$item.slideUp(200, function() {
$(this).remove();
updateMenuFromVisual();
updateSectionCounts();
showStatus('Item deleted', 'success');
});
}
}
// Duplicate item
function duplicateItem($item) {
var key = $item.data('key');
var value = $item.data('value');
var level = $item.data('level') || 0;
var isStarred = $item.data('star') === 'true';
var newKey = key + '_copy';
var counter = 1;
// Find unique key
while (isKeyExists(newKey)) {
counter++;
newKey = key + '_copy' + counter;
}
// Clone and insert
var $clone = $item.clone();
$clone.data('key', newKey);
$clone.data('level', level);
$clone.data('star', isStarred ? 'true' : 'false');
$clone.css('margin-left', (level * 20) + 'px');
$clone.insertAfter($item);
// Re-bind events
bindItemEvents($clone);
// Update and highlight
updateMenuFromVisual();
$clone.hide().slideDown(200);
showStatus('Item duplicated', 'success');
}
// Edit section inline
function editSectionInline($section) {
var $header = $section.find('.leftmenu-visual-section-header');
var $span = $header.find('span').eq(1);
var currentName = $section.data('section');
var $input = $('<input>', {
'type': 'text',
'class': 'leftmenu-form-input',
'value': currentName,
'style': 'width:200px;margin:0'
});
$span.html($input);
$input.focus().select();
function saveEdit() {
var newName = $input.val().trim();
if (newName && newName !== currentName) {
$section.data('section', newName);
$span.text(newName);
updateMenuFromVisual();
showStatus('Section renamed', 'success');
} else {
$span.text(currentName);
}
}
$input.on('blur', saveEdit);
$input.on('keypress', function(e) {
if (e.which === 13) {
e.preventDefault();
saveEdit();
}
});
}
// Delete section
function deleteSection($section) {
var name = $section.data('section');
var itemCount = $section.find('.leftmenu-section-items > *').length;
var message = 'Delete section "' + name + '"?';
if (itemCount > 0) {
message += '\n\nThis will also delete ' + itemCount + ' item' + (itemCount > 1 ? 's' : '') + '.';
}
if (confirm(message)) {
$section.slideUp(200, function() {
$(this).remove();
updateMenuFromVisual();
showStatus('Section deleted', 'success');
});
}
}
// Make sortable
function makeSortable() {
$('.leftmenu-preview-tree, .leftmenu-section-items').sortable({
items: '> .leftmenu-visual-item, > .leftmenu-visual-section',
connectWith: '.leftmenu-preview-tree, .leftmenu-section-items',
placeholder: 'leftmenu-drag-placeholder',
tolerance: 'pointer',
cursor: 'move',
start: function(event, ui) {
ui.item.addClass('leftmenu-dragging');
},
stop: function(event, ui) {
ui.item.removeClass('leftmenu-dragging');
// Get the new parent container
var $newParent = ui.item.parent();
var newLevel = 0;
// Calculate new level based on parent
if ($newParent.hasClass('leftmenu-section-items')) {
// Find the parent section
var $parentSection = $newParent.closest('.leftmenu-visual-section');
newLevel = parseInt($parentSection.data('level') || 0) + 1;
}
// Update the item's level
ui.item.data('level', newLevel);
ui.item.css('margin-left', (newLevel * 20) + 'px');
// If this is a section, update all children's levels recursively
if (ui.item.hasClass('leftmenu-visual-section')) {
updateChildLevels(ui.item, newLevel);
}
updateMenuFromVisual();
updateSectionCounts();
}
});
}
// Update child levels recursively
function updateChildLevels($section, parentLevel) {
var childLevel = parentLevel + 1;
$section.find('> .leftmenu-section-items > *').each(function() {
var $child = $(this);
$child.data('level', childLevel);
$child.css('margin-left', (childLevel * 20) + 'px');
if ($child.hasClass('leftmenu-visual-section')) {
updateChildLevels($child, childLevel);
}
});
}
// Show add form
function showAddForm(type) {
var $panel = $('.leftmenu-action-panel');
var $form = $panel.find('.leftmenu-form');
$panel.slideDown();
if (type === 'section') {
$form.html(
'<div class="leftmenu-form-group">' +
'<label class="leftmenu-form-label">Section Name</label>' +
'<input type="text" class="leftmenu-form-input" id="leftmenu-section-name" placeholder="e.g., Documentation">' +
'</div>' +
'<div class="leftmenu-form-group">' +
'<label class="leftmenu-form-label">Parent Section (optional)</label>' +
'<select class="leftmenu-form-select" id="leftmenu-parent-section">' +
'<option value="">[Root Level]</option>' +
'</select>' +
'</div>' +
'<div class="leftmenu-form-group">' +
'<label class="leftmenu-form-label"><input type="checkbox" id="leftmenu-section-star"> Star Priority</label>' +
'</div>' +
'<div style="display:flex;gap:10px">' +
'<button class="leftmenu-button primary" id="leftmenu-confirm-add">Add Section</button>' +
'<button class="leftmenu-button secondary" id="leftmenu-cancel-add">Cancel</button>' +
'</div>'
);
// Populate sections
populateSections('#leftmenu-parent-section');
$('#leftmenu-confirm-add').on('click', function() {
confirmAddSection();
});
} else if (type === 'page') {
$form.html(
'<div class="leftmenu-form-group">' +
'<label class="leftmenu-form-label">Page Name</label>' +
'<input type="text" class="leftmenu-form-input" id="leftmenu-page-name" placeholder="e.g., Help:Contents">' +
'</div>' +
'<div class="leftmenu-form-group">' +
'<label class="leftmenu-form-label">Display Name (optional)</label>' +
'<input type="text" class="leftmenu-form-input" id="leftmenu-display-name" placeholder="e.g., Help & Support">' +
'</div>' +
'<div class="leftmenu-form-group">' +
'<label class="leftmenu-form-label">Add to Section</label>' +
'<select class="leftmenu-form-select" id="leftmenu-target-section">' +
'<option value="">[Root Level]</option>' +
'</select>' +
'</div>' +
'<div class="leftmenu-form-group">' +
'<label class="leftmenu-form-label"><input type="checkbox" id="leftmenu-page-star"> Star Priority</label>' +
'</div>' +
'<div style="display:flex;gap:10px">' +
'<button class="leftmenu-button primary" id="leftmenu-confirm-add">Add Page</button>' +
'<button class="leftmenu-button secondary" id="leftmenu-cancel-add">Cancel</button>' +
'</div>'
);
populateSections('#leftmenu-target-section');
$('#leftmenu-confirm-add').on('click', function() {
confirmAddPage();
});
}
$('#leftmenu-cancel-add').on('click', function() {
$panel.slideUp();
});
setTimeout(function() {
$form.find('input:first').focus();
}, 300);
}
// Populate sections dropdown
function populateSections(selector) {
var $select = $(selector);
function addOptions(obj, prefix, level) {
for (var key in obj) {
if (obj.hasOwnProperty(key) && typeof obj[key] === 'object' &&
!Array.isArray(obj[key]) && key.indexOf('//') !== 0) {
var cleanKey = key.replace(/^\*\s*/, '');
var indent = '';
for (var i = 0; i < level; i++) indent += ' ';
$select.append('<option value="' + prefix + cleanKey + '">' + indent + cleanKey + '</option>');
addOptions(obj[key], prefix + cleanKey + ' > ', level + 1);
}
}
}
addOptions(LeftMenuEditor.menuData, '', 0);
}
// Confirm add section
function confirmAddSection() {
var name = $('#leftmenu-section-name').val().trim();
var parent = $('#leftmenu-parent-section').val();
var isStarred = $('#leftmenu-section-star').is(':checked');
if (!name) {
showStatus('Please enter a section name', 'error');
return;
}
if (isStarred) {
name = '* ' + name;
}
var target = LeftMenuEditor.menuData;
if (parent) {
var parts = parent.split(' > ');
for (var i = 0; i < parts.length; i++) {
if (!target[parts[i]]) target[parts[i]] = {};
target = target[parts[i]];
}
}
if (target[name]) {
showStatus('Section already exists', 'error');
return;
}
target[name] = {};
renderVisualMenu();
showStatus('Section added', 'success');
$('.leftmenu-action-panel').slideUp();
}
// Confirm add page
function confirmAddPage() {
var pageName = $('#leftmenu-page-name').val().trim();
var displayName = $('#leftmenu-display-name').val().trim() || pageName;
var section = $('#leftmenu-target-section').val();
var isStarred = $('#leftmenu-page-star').is(':checked');
if (!pageName) {
showStatus('Please enter a page name', 'error');
return;
}
var key = isStarred ? '* ' + pageName : pageName;
var target = LeftMenuEditor.menuData;
if (section) {
var parts = section.split(' > ');
for (var i = 0; i < parts.length; i++) {
if (!target[parts[i]]) target[parts[i]] = {};
target = target[parts[i]];
}
}
var value = displayName !== pageName ? pageName + '|' + displayName : pageName;
target[key] = value;
renderVisualMenu();
showStatus('Page added', 'success');
$('.leftmenu-action-panel').slideUp();
}
// Add current page
function addCurrentPage() {
if (isPageInMenu()) {
showStatus('This page is already in the menu', 'error');
return;
}
// Create a temporary draggable item
var $item = createVisualItem(
LeftMenuEditor.pageName,
LeftMenuEditor.currentPage,
'item',
0,
false
);
$item.css('background', '#e8f4fd').css('border-color', '#4a90e2');
// Create a placeholder message
var $placeholder = $('<div class="leftmenu-drag-message" style="text-align:center;padding:20px;background:#f0f8ff;border:2px dashed #4a90e2;margin:10px 0;border-radius:4px;">' +
'<strong>Drag the item below to any section or position</strong>' +
'</div>');
LeftMenuEditor.$preview.prepend($placeholder);
LeftMenuEditor.$preview.prepend($item);
showStatus('Drag the current page to position it in the menu', 'info');
// Make it draggable but still connected to sortable
makeSortable(); // Re-initialize sortable to include the new item
// Remove placeholder when drag starts
$item.on('sortstart', function() {
$placeholder.remove();
});
}
// Toggle star priority on selected item
function toggleSelectedStarPriority() {
if (LeftMenuEditor.selectedItem && LeftMenuEditor.selectedItem.hasClass('leftmenu-visual-item')) {
toggleStarPriority(LeftMenuEditor.selectedItem);
} else {
showStatus('Please select an item first', 'error');
}
}
// Re-bind events
function bindItemEvents($item) {
$item.off('click').on('click', function(e) {
if (!$(e.target).closest('.leftmenu-item-actions').length) {
selectItem($item);
}
});
$item.find('.leftmenu-item-star').off('click').on('click', function(e) {
e.stopPropagation();
toggleStarPriority($item);
});
$item.find('.leftmenu-item-edit').off('click').on('click', function(e) {
e.stopPropagation();
editItemInline($item);
});
$item.find('.leftmenu-item-delete').off('click').on('click', function(e) {
e.stopPropagation();
deleteItem($item);
});
$item.find('.leftmenu-item-duplicate').off('click').on('click', function(e) {
e.stopPropagation();
duplicateItem($item);
});
}
// Check if key exists
function isKeyExists(key) {
function check(obj) {
for (var k in obj) {
if (obj.hasOwnProperty(k)) {
var cleanK = k.replace(/^\*\s*/, '');
if (cleanK === key) return true;
if (typeof obj[k] === 'object' && check(obj[k])) return true;
}
}
return false;
}
return check(LeftMenuEditor.menuData);
}
// Check if page in menu
function isPageInMenu() {
var targets = [LeftMenuEditor.currentPage, LeftMenuEditor.pageName];
function walk(obj) {
for (var k in obj) {
if (obj.hasOwnProperty(k)) {
var cleanK = k.replace(/^\*\s*/, '');
for (var i = 0; i < targets.length; i++) {
if (cleanK === targets[i]) return true;
}
var v = obj[k];
if (typeof v === 'string') {
var page = v.split('|')[0].replace(/\[|\]/g, '').replace(/^\*\s*/, '');
for (var j = 0; j < targets.length; j++) {
if (page === targets[j] || v === targets[j]) return true;
}
} else if (Array.isArray(v)) {
for (var a = 0; a < v.length; a++) {
if (typeof v[a] === 'string') {
var cleanVal = v[a].replace(/^\*\s*/, '');
for (var t = 0; t < targets.length; t++) {
if (cleanVal === targets[t]) return true;
}
}
}
} else if (typeof v === 'object' && v !== null) {
if (walk(v)) return true;
}
}
}
return false;
}
return walk(LeftMenuEditor.menuData);
}
// Load menu data (fallback for namespaces without sub-namespaces)
function loadMenuData(callback) {
var page = 'MediaWiki:Menu-' + LeftMenuEditor.namespace;
showStatus('Loading menu...', 'info');
LeftMenuEditor.api.get({
action: 'query',
prop: 'revisions',
titles: page,
rvprop: 'content',
rvslots: 'main',
formatversion: 2
}).done(function (data) {
var raw = '';
if (data.query && data.query.pages && data.query.pages.length > 0) {
var page = data.query.pages[0];
if (page.revisions && page.revisions.length > 0) {
var revision = page.revisions[0];
if (revision.slots && revision.slots.main) {
raw = revision.slots.main.content || '';
}
}
}
// Check if this is a sub-namespace list or JSON menu
if (raw) {
var trimmed = raw.trim();
// If it starts with { or [, it's likely JSON
if (trimmed[0] === '{' || trimmed[0] === '[') {
try {
LeftMenuEditor.menuData = JSON.parse(raw);
} catch (e) {
LeftMenuEditor.menuData = {};
showStatus('Error parsing menu data: ' + e.message, 'error');
}
} else {
// It's a sub-namespace list, not JSON
// This namespace uses sub-namespaces, so we shouldn't edit this page directly
LeftMenuEditor.menuData = {};
showStatus('This namespace uses sub-namespaces. Please select one from the dropdown above.', 'info');
}
} else {
LeftMenuEditor.menuData = {};
}
renderVisualMenu();
$('.leftmenu-status').fadeOut(3000);
if (typeof callback === 'function') { callback(); }
}).fail(function(error) {
showStatus('Failed to load menu data', 'error');
LeftMenuEditor.menuData = {};
renderVisualMenu();
});
}
// Reset menu
function resetMenu() {
if (confirm('Discard all changes and reload?')) {
if (LeftMenuEditor.currentSubNamespace) {
loadSubNamespaceMenu(LeftMenuEditor.currentSubNamespace);
} else {
loadMenuData(function() {
showStatus('Menu reset', 'info');
});
}
}
}
// Toggle JSON view
function toggleJsonView() {
var $jsonView = $('.leftmenu-json-view');
if ($jsonView.is(':visible')) {
$jsonView.slideUp();
$('.leftmenu-view-json').text('View JSON');
} else {
$jsonView.text(JSON.stringify(LeftMenuEditor.menuData, null, 2)).slideDown();
$('.leftmenu-view-json').text('Hide JSON');
}
}
// Show status
function showStatus(msg, type) {
$('.leftmenu-status')
.removeClass('success error info')
.addClass(type)
.text(msg)
.slideDown()
.delay(3000)
.slideUp();
}
// HTML escape
function escapeHtml(text) {
var map = {
'&': '&',
'<': '<',
'>': '>',
'"': '"',
"'": '''
};
return String(text).replace(/[&<>"']/g, function(m) { return map[m]; });
}
// Keep all AI and Page Tools functions as they are
function bindAIEvents() {
// Summarize
$('#ai-summarize').on('click', function() {
var $btn = $(this);
$btn.prop('disabled', true).html('<span class="button-icon">⏳</span> Summarizing...');
getPageContent(function(content) {
$.ajax({
url: 'https://chat.mdriven.net/data/summarize',
method: 'POST',
contentType: 'application/json',
data: JSON.stringify({ content: content }),
success: function(response) {
var summary = response.summary || response.result || response;
displayAIResult(summary, 'summary');
$btn.prop('disabled', false).html('<span class="button-icon">📝</span> Summarize Page');
},
error: function(xhr) {
showAIStatus('Error: ' + (xhr.responseText || 'Failed to summarize'), 'error');
$btn.prop('disabled', false).html('<span class="button-icon">📝</span> Summarize Page');
}
});
});
});
// Fix spelling
$('#ai-fix-spelling').on('click', function() {
var $btn = $(this);
$btn.prop('disabled', true).html('<span class="button-icon">⏳</span> Checking...');
getPageContent(function(content) {
$.ajax({
url: 'https://chat.mdriven.net/data/fix-spelling',
method: 'POST',
contentType: 'application/json',
data: JSON.stringify({ content: content }),
success: function(response) {
var fixed = response.fixed_content || response.result || response;
displayAIResult(fixed, 'spelling');
$btn.prop('disabled', false).html('<span class="button-icon">✓</span> Fix Spelling');
},
error: function(xhr) {
showAIStatus('Error: ' + (xhr.responseText || 'Failed to fix spelling'), 'error');
$btn.prop('disabled', false).html('<span class="button-icon">✓</span> Fix Spelling');
}
});
});
});
// Suggest tags
$('#ai-suggest-tags').on('click', function() {
var $btn = $(this);
$btn.prop('disabled', true).html('<span class="button-icon">⏳</span> Analyzing...');
getPageContent(function(content) {
$.ajax({
url: 'https://chat.mdriven.net/data/suggest-tags',
method: 'POST',
contentType: 'application/json',
data: JSON.stringify({ content: content }),
success: function(response) {
var tags = response.tags || [];
if (typeof tags === 'string') {
try {
tags = JSON.parse(tags);
} catch(e) {
tags = [];
}
}
LeftMenuEditor.suggestedTags = tags;
displayTagsSuggestions(tags);
$('#tags-section').slideDown();
$btn.prop('disabled', false).html('<span class="button-icon">🏷️</span> Suggest Tags');
},
error: function(xhr) {
showAIStatus('Error: ' + (xhr.responseText || 'Failed to suggest tags'), 'error');
$btn.prop('disabled', false).html('<span class="button-icon">🏷️</span> Suggest Tags');
}
});
});
});
// Copy result
$('#ai-copy').on('click', function() {
var text = $('.ai-result-content').text();
copyToClipboard(text);
$(this).text('✓ Copied!');
setTimeout(function() {
$('#ai-copy').html('📋 Copy');
}, 2000);
});
// Apply nutshell
$('#ai-apply-nutshell').on('click', function() {
var summary = $('.ai-result-content').text();
addNutshellToPage(summary);
});
// Apply spelling corrections
$('#ai-apply-spelling').on('click', function() {
var correctedContent = $('.ai-result-content').text();
LeftMenuEditor.api.postWithToken('csrf', {
action: 'edit',
title: mw.config.get('wgPageName'),
text: correctedContent,
summary: 'Applied spelling corrections',
contentformat: 'text/x-wiki',
contentmodel: 'wikitext'
}).done(function() {
showAIStatus('Spelling corrections applied! Reloading...', 'success');
setTimeout(function() {
window.location.reload();
}, 1500);
}).fail(function(err) {
showAIStatus('Failed to apply corrections: ' + (err.error ? err.error.info : err), 'error');
});
});
}
// Bind tags events
function bindTagsEvents() {
// Add custom tag on Enter
$('#custom-tag-input').on('keypress', function(e) {
if (e.which === 13) {
e.preventDefault();
$('#add-custom-tag').click();
}
});
// Add custom tag button
$('#add-custom-tag').on('click', function() {
var tagText = $('#custom-tag-input').val().trim();
if (tagText && !isTagSelected(tagText)) {
addTagToSelected(tagText, false);
$('#custom-tag-input').val('');
}
});
// Apply tags to page
$('#apply-tags').on('click', function() {
if (LeftMenuEditor.selectedTags.length === 0) {
showAIStatus('Please select at least one tag', 'error');
return;
}
var $btn = $(this);
$btn.prop('disabled', true).text('Applying tags...');
addTagsToPage(LeftMenuEditor.selectedTags);
});
// Clear all tags
$('#clear-tags').on('click', function() {
LeftMenuEditor.selectedTags = [];
updateSelectedTagsDisplay();
$('.tag.suggested').removeClass('selected');
});
}
// Display tags suggestions
function displayTagsSuggestions(tags) {
var $container = $('#suggested-tags');
$container.empty();
if (tags.length === 0) {
$container.html('<span style="color:#999">No tags suggested</span>');
return;
}
for (var i = 0; i < tags.length; i++) {
var tag = tags[i];
var $tag = $('<span>', {
'class': 'tag suggested',
'data-tag': tag,
'text': tag
});
$tag.on('click', function() {
toggleSuggestedTag($(this));
});
$container.append($tag);
}
}
// Toggle suggested tag
function toggleSuggestedTag($tag) {
var tagText = $tag.data('tag');
if ($tag.hasClass('selected')) {
$tag.removeClass('selected');
removeTagFromSelected(tagText);
} else {
$tag.addClass('selected');
addTagToSelected(tagText, true);
}
}
// Add tag to selected
function addTagToSelected(tagText, isSuggested) {
if (!isTagSelected(tagText)) {
LeftMenuEditor.selectedTags.push(tagText);
updateSelectedTagsDisplay();
}
}
// Remove tag from selected
function removeTagFromSelected(tagText) {
var newTags = [];
for (var i = 0; i < LeftMenuEditor.selectedTags.length; i++) {
if (LeftMenuEditor.selectedTags[i] !== tagText) {
newTags.push(LeftMenuEditor.selectedTags[i]);
}
}
LeftMenuEditor.selectedTags = newTags;
updateSelectedTagsDisplay();
}
// Check if tag is selected
function isTagSelected(tagText) {
return LeftMenuEditor.selectedTags.indexOf(tagText) !== -1;
}
// Update selected tags display
function updateSelectedTagsDisplay() {
var $container = $('#selected-tags');
$container.empty();
if (LeftMenuEditor.selectedTags.length === 0) {
$container.html('<span style="color:#999">No tags selected</span>');
return;
}
for (var i = 0; i < LeftMenuEditor.selectedTags.length; i++) {
var tag = LeftMenuEditor.selectedTags[i];
var $tag = $('<span>', {
'class': 'tag selected',
'html': tag + '<span class="tag-remove">×</span>'
});
(function(tagText) {
$tag.find('.tag-remove').on('click', function(e) {
e.stopPropagation();
removeTagFromSelected(tagText);
// Also unselect from suggested tags
$('.tag.suggested[data-tag="' + tagText + '"]').removeClass('selected');
});
})(tag);
$container.append($tag);
}
}
// Add tags to page
function addTagsToPage(tags) {
var tagsMarkup = '<tags> ' + tags.join(', ') + ' </tags>\n\n';
getPageContent(function(content) {
// Check if tags already exist
var tagsRegex = /<tags>[\s\S]*?<\/tags>/;
var newContent;
if (tagsRegex.test(content)) {
// Replace existing tags
newContent = content.replace(tagsRegex, tagsMarkup.trim());
} else {
// Add tags after nutshell if exists, otherwise at the beginning
var nutshellRegex = /<nutshell>[\s\S]*?<\/nutshell>/;
if (nutshellRegex.test(content)) {
newContent = content.replace(nutshellRegex, function(match) {
return match + '\n\n' + tagsMarkup.trim();
});
} else {
newContent = tagsMarkup + content;
}
}
LeftMenuEditor.api.postWithToken('csrf', {
action: 'edit',
title: mw.config.get('wgPageName'),
text: newContent,
summary: 'Updated page tags',
contentformat: 'text/x-wiki',
contentmodel: 'wikitext'
}).done(function() {
showAIStatus('Tags applied successfully! Reloading...', 'success');
setTimeout(function() {
window.location.reload();
}, 1500);
}).fail(function(err) {
showAIStatus('Failed to apply tags: ' + (err.error ? err.error.info : err), 'error');
$('#apply-tags').prop('disabled', false).text('Apply Tags to Page');
});
});
}
// Bind page tools events
function bindPageToolsEvents() {
// Rename page
$('#rename-page').on('click', function() {
var newName = $('#new-page-name').val().trim();
if (!newName) {
showPageStatus('Please enter a new page name', 'error');
return;
}
var $btn = $(this);
$btn.prop('disabled', true).text('Renaming...');
renamePage(newName, function(success, error) {
if (success) {
showPageStatus('Page renamed successfully! Redirecting...', 'success');
setTimeout(function() {
window.location.href = mw.util.getUrl(newName);
}, 1500);
} else {
showPageStatus('Error: ' + error, 'error');
$btn.prop('disabled', false).text('Rename');
}
});
});
// Move namespace
$('#move-namespace').on('click', function() {
var targetNs = $('#target-namespace').val();
if (!targetNs && targetNs !== '0') {
showPageStatus('Please select a namespace', 'error');
return;
}
var $btn = $(this);
$btn.prop('disabled', true).text('Moving...');
moveToNamespace(targetNs, function(success, error) {
if (success) {
showPageStatus('Page moved successfully! Redirecting...', 'success');
setTimeout(function() {
window.location.reload();
}, 1500);
} else {
showPageStatus('Error: ' + error, 'error');
$btn.prop('disabled', false).text('Move Page');
}
});
});
}
// Helper functions
function copyToClipboard(text) {
var $temp = $('<textarea>');
$('body').append($temp);
$temp.val(text).select();
document.execCommand('copy');
$temp.remove();
}
function getPageContent(callback) {
LeftMenuEditor.api.get({
action: 'query',
prop: 'revisions',
titles: mw.config.get('wgPageName'),
rvprop: 'content',
rvslots: 'main',
formatversion: 2
}).done(function(data) {
var content = '';
if (data.query && data.query.pages && data.query.pages[0]) {
var page = data.query.pages[0];
if (page.revisions && page.revisions[0]) {
content = page.revisions[0].slots.main.content || '';
}
}
callback(content);
}).fail(function() {
showAIStatus('Failed to get page content', 'error');
callback('');
});
}
function displayAIResult(content, type) {
$('.ai-result-content').text(content);
$('#ai-result').slideDown();
// Show appropriate action button
$('#ai-apply-nutshell').toggle(type === 'summary');
$('#ai-apply-spelling').toggle(type === 'spelling');
}
function addNutshellToPage(summary) {
var nutshell = '<nutshell> ' + summary + ' </nutshell>\n\n';
getPageContent(function(content) {
var newContent = nutshell + content;
LeftMenuEditor.api.postWithToken('csrf', {
action: 'edit',
title: mw.config.get('wgPageName'),
text: newContent,
summary: 'Added AI-generated summary in nutshell',
contentformat: 'text/x-wiki',
contentmodel: 'wikitext'
}).done(function() {
showAIStatus('Nutshell added successfully! Reloading...', 'success');
setTimeout(function() {
window.location.reload();
}, 1500);
}).fail(function(err) {
showAIStatus('Failed to add nutshell: ' + (err.error ? err.error.info : err), 'error');
});
});
}
function loadNamespaces() {
LeftMenuEditor.api.get({
action: 'query',
meta: 'siteinfo',
siprop: 'namespaces',
formatversion: 2
}).done(function(data) {
var $select = $('#target-namespace');
$select.empty();
$select.append('<option value="0">(Main)</option>');
if (data.query && data.query.namespaces) {
for (var id in data.query.namespaces) {
var ns = data.query.namespaces[id];
if (ns.id >= 0 && ns.id !== 0 && ns.canonical !== 'Special' && ns.canonical !== 'Media') {
$select.append('<option value="' + ns.id + '">' + (ns.name || ns.canonical) + '</option>');
}
}
}
var currentNsId = mw.config.get('wgNamespaceNumber');
$select.val(currentNsId);
});
}
function renamePage(newName, callback) {
LeftMenuEditor.api.postWithToken('csrf', {
action: 'move',
from: mw.config.get('wgPageName'),
to: newName,
reason: 'Renamed via Wiki Tools',
movetalk: true,
movesubpages: true
}).done(function() {
callback(true);
}).fail(function(err) {
callback(false, err.error ? err.error.info : 'Unknown error');
});
}
function moveToNamespace(targetNsId, callback) {
var nsInfo = mw.config.get('wgFormattedNamespaces');
var targetPrefix = '';
if (targetNsId !== '0' && nsInfo && nsInfo[targetNsId]) {
targetPrefix = nsInfo[targetNsId] + ':';
}
var currentTitle = mw.config.get('wgTitle');
var newTitle = targetPrefix + currentTitle;
LeftMenuEditor.api.postWithToken('csrf', {
action: 'move',
from: mw.config.get('wgPageName'),
to: newTitle,
reason: 'Moved to ' + (targetPrefix || 'Main') + ' namespace via Wiki Tools',
movetalk: true,
movesubpages: true
}).done(function() {
callback(true);
}).fail(function(err) {
callback(false, err.error ? err.error.info : 'Unknown error');
});
}
function showAIStatus(msg, type) {
$('.ai-status')
.removeClass('success error info')
.addClass(type)
.text(msg)
.slideDown()
.delay(5000)
.slideUp();
}
function showPageStatus(msg, type) {
$('.page-status')
.removeClass('success error info')
.addClass(type)
.text(msg)
.slideDown()
.delay(5000)
.slideUp();
}
// Event bindings
function bindEvents() {
// Header toggle - make the container collapsible
$('.leftmenu-editor-header').on('click', function() {
$('.leftmenu-editor-container').toggleClass('collapsed');
// Load sub-namespaces if expanding and on menu tab
if (!$('.leftmenu-editor-container').hasClass('collapsed')) {
if ($('#menu-editor-tab').hasClass('active') && !LeftMenuEditor.subNamespaces.length) {
loadSubNamespaces();
}
}
});
// Quick actions
$('.leftmenu-quick-action').on('click', function() {
var action = $(this).data('action');
switch(action) {
case 'add-section':
showAddForm('section');
break;
case 'add-page':
showAddForm('page');
break;
case 'add-current':
addCurrentPage();
break;
case 'star-toggle':
toggleSelectedStarPriority();
break;
}
});
// Search
$('.leftmenu-search').on('input', function() {
var query = $(this).val().toLowerCase();
$('.leftmenu-visual-item, .leftmenu-visual-section').each(function() {
var $item = $(this);
var text = $item.text().toLowerCase();
if (!query || text.indexOf(query) > -1) {
$item.show();
// Show parent sections
if (query) {
$item.parents('.leftmenu-visual-section').show().removeClass('collapsed');
}
} else {
$item.hide();
}
});
});
// Empty state button
$('.leftmenu-empty-add').on('click', function() {
showAddForm('page');
});
// Control buttons
$('.leftmenu-save').on('click', saveMenu);
$('.leftmenu-reset').on('click', resetMenu);
$('.leftmenu-view-json').on('click', toggleJsonView);
}
// Initialize
addStyles();
createInterface();
bindEvents();
});
})(mediaWiki, jQuery);
// Simple Release Notes Button for MediaWiki
// Works on any page containing "ReleaseNotes" in the title
// Simple Release Notes Button for MediaWiki
// Shown only to logged-in users on pages whose title contains “ReleaseNotes”
(function () {
var userId = mw.config.get('wgUserId'); // null when not logged in
var isReleaseNotesPage = mw.config.get('wgPageName')
.indexOf('ReleaseNotes') !== -1; // true if title matches
if (!userId || !isReleaseNotesPage) { // guard clause
return; // nothing to do
}
/* ---------- UI ---------- */
var button = document.createElement('div');
button.textContent = '+ Add Release Notes';
button.style.cssText =
'position:fixed; top:100px; right:20px; background:#667eea; ' +
'color:#fff; padding:10px 20px; border-radius:5px; ' +
'cursor:pointer; z-index:9999; font-weight:bold;';
document.body.appendChild(button);
/* ---------- Click handler ---------- */
button.addEventListener('click', function () {
var month = prompt('Enter month (e.g., May):', 'May');
var year = prompt('Enter year:', new Date().getFullYear());
if (month && year) {
var template =
'<div id="' + month.toLowerCase() + '-' + year + '">\n\n' +
"== '''MDriven - " + month + ' ' + year + " '''==\n\n" +
"=== '''<u>Designer</u>''' ===\n" +
'==== Enhancements ====\n' +
'* Feature 1: Description\n' +
'* Feature 2: Description\n\n' +
'==== Bug Fixes ====\n' +
'* Fixed issue 1\n' +
'* Fixed issue 2\n\n' +
"=== '''<u>Framework</u>''' ===\n" +
'==== Enhancements ====\n' +
'* Feature 1: Description\n\n' +
"=== '''<u>Turnkey</u>''' ===\n" +
'==== Enhancements ====\n' +
'* Feature 1: Description\n\n' +
"=== '''<u>Server</u>''' ===\n" +
'* Update 1: Description\n\n' +
'</div>';
var textarea = document.createElement('textarea');
textarea.value = template;
document.body.appendChild(textarea);
textarea.select();
document.execCommand('copy');
document.body.removeChild(textarea);
alert('Template copied to clipboard. Paste it into the edit window.');
}
});
})();
/* === Category Manager (add/remove) — Logged-in users ===
* Shows a "Categories" item in the Actions bar (next to Delete/Move/Protect).
* Allows both adding and removing categories from the current page.
* ES5-safe. Works with Vector/BlueSpice-like skins.
* Enhanced with comprehensive debugging and fallback notifications
*/
(function () {
console.log('[CatManager] Script loaded');
// Check what modules are available
console.log('[CatManager] Available MW config:', {
modules: typeof mw.loader !== 'undefined' ? 'mw.loader available' : 'mw.loader NOT available',
notify: typeof mw.notify !== 'undefined' ? 'mw.notify available' : 'mw.notify NOT available',
api: typeof mw.Api !== 'undefined' ? 'mw.Api available' : 'mw.Api NOT available'
});
// Only on normal view of content pages
var nsNum = mw.config.get('wgNamespaceNumber');
var action = mw.config.get('wgAction');
console.log('[CatManager] Namespace:', nsNum, 'Action:', action);
if (nsNum === -1) {
console.log('[CatManager] Skipping: Special page');
return;
}
if (action !== 'view') {
console.log('[CatManager] Skipping: Not in view mode');
return;
}
// --- Logged-in gate ---
var userName = mw.config.get('wgUserName');
var isLoggedIn = !!userName;
console.log('[CatManager] User:', userName || 'anonymous', 'Logged in:', isLoggedIn);
if (!isLoggedIn) {
console.log('[CatManager] Skipping: User not logged in');
return;
}
// --- Notification fallback ---
var notify = function(message, options) {
options = options || {};
console.log('[CatManager] Notification (' + (options.type || 'info') + '):', message);
// Try mw.notify if available
if (typeof mw.notify === 'function') {
return mw.notify(message, options);
}
// Fallback to custom notification
var typeColors = {
'success': '#00aa00',
'warn': '#ffaa00',
'warning': '#ffaa00',
'error': '#dd3333',
'info': '#0066cc'
};
var color = typeColors[options.type] || typeColors.info;
var $notification = $('<div>')
.css({
'position': 'fixed',
'top': '20px',
'right': '20px',
'background': color,
'color': 'white',
'padding': '10px 15px',
'border-radius': '3px',
'box-shadow': '0 2px 5px rgba(0,0,0,0.3)',
'z-index': '10000',
'max-width': '300px',
'word-wrap': 'break-word'
})
.text(message)
.appendTo('body');
// Auto-hide unless specified otherwise
if (!options.autoHide || options.autoHide !== false) {
setTimeout(function() {
$notification.fadeOut(function() {
$notification.remove();
});
}, options.type === 'error' ? 10000 : 5000);
}
// Click to dismiss
$notification.on('click', function() {
$notification.remove();
});
};
// --- Dialog UI ---
var createDialog = function() {
console.log('[CatManager] Creating dialog');
// Create overlay
var $overlay = $('<div>')
.css({
'position': 'fixed',
'top': '0',
'left': '0',
'width': '100%',
'height': '100%',
'background': 'rgba(0, 0, 0, 0.5)',
'z-index': '9999',
'display': 'flex',
'align-items': 'center',
'justify-content': 'center'
});
// Create dialog box
var $dialog = $('<div>')
.css({
'background': 'white',
'padding': '20px',
'border-radius': '5px',
'box-shadow': '0 5px 15px rgba(0,0,0,0.3)',
'max-width': '500px',
'width': '90%',
'max-height': '80vh',
'overflow': 'auto'
});
var $title = $('<h3>')
.text('Manage Categories')
.css('margin-top', '0');
var $content = $('<div>');
var $buttons = $('<div>')
.css({
'margin-top': '20px',
'text-align': 'right'
});
$dialog.append($title, $content, $buttons);
$overlay.append($dialog);
// Close on overlay click
$overlay.on('click', function(e) {
if (e.target === $overlay[0]) {
$overlay.remove();
}
});
return {
$overlay: $overlay,
$content: $content,
$buttons: $buttons,
show: function() { $('body').append($overlay); },
close: function() { $overlay.remove(); }
};
};
// --- Helpers ---
function normalizeName(name) {
console.log('[CatManager] Normalizing name:', name);
name = name.replace(/^\s*\[\[|\]\]\s*$/g, ''); // strip [[ ]]
name = name.replace(/^\s*[Cc]ategory\s*:\s*/, ''); // strip Category:
name = name.trim();
if (!name) {
console.log('[CatManager] Name normalization resulted in empty string');
return null;
}
var normalized = name.charAt(0).toUpperCase() + name.slice(1);
console.log('[CatManager] Normalized to:', normalized);
return normalized;
}
function escapeRegex(str) {
return str.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
}
function parseCat(raw) {
console.log('[CatManager] Parsing category:', raw);
raw = raw.trim();
var parts = raw.split('|');
var name = normalizeName(parts.shift() || '');
var sort = parts.length ? parts.join('|').trim() : null;
var result = name ? { name: name, sort: sort } : null;
console.log('[CatManager] Parsed result:', result);
return result;
}
// Extract categories from wikitext
function extractCategories(text) {
console.log('[CatManager] Extracting categories from text');
var regex = /\[\[\s*[Cc]ategory\s*:\s*([^\]|]+)(?:\|([^\]]*))?\]\]/g;
var categories = [];
var match;
while ((match = regex.exec(text)) !== null) {
var name = match[1].trim();
var sort = match[2] ? match[2].trim() : null;
categories.push({
name: name,
sort: sort,
raw: match[0]
});
console.log('[CatManager] Found category:', name, 'with sort key:', sort);
}
console.log('[CatManager] Total categories found:', categories.length);
return categories;
}
// Main category management function
function manageCategories() {
console.log('[CatManager] Opening category manager');
// Check if mw.Api is available
if (typeof mw.Api !== 'function') {
console.error('[CatManager] mw.Api not available');
notify('Error: MediaWiki API not available. Cannot manage categories.', { type: 'error' });
return;
}
var api = new mw.Api();
var title = mw.config.get('wgPageName');
var dialog = createDialog();
dialog.$content.html('<p>Loading categories...</p>');
dialog.show();
console.log('[CatManager] Fetching page content...');
api.get({
action: 'query',
titles: title,
prop: 'revisions',
rvslots: 'main',
rvprop: 'content|timestamp|contentmodel',
formatversion: 2
}).then(function (data) {
console.log('[CatManager] API response:', data);
var page = data && data.query && data.query.pages && data.query.pages[0];
if (!page || page.missing) {
console.error('[CatManager] Page does not exist');
notify('Page does not exist.', { type: 'error' });
dialog.close();
return;
}
var rev = page.revisions && page.revisions[0];
var slot = rev && rev.slots && rev.slots.main;
if (!slot) {
console.error('[CatManager] Could not access page content slot');
notify('Could not load page content.', { type: 'error' });
dialog.close();
return;
}
var contentModel = slot.contentmodel || 'wikitext';
console.log('[CatManager] Content model:', contentModel);
if (contentModel !== 'wikitext') {
console.error('[CatManager] Page is not wikitext');
notify('This page is not wikitext.', { type: 'error' });
dialog.close();
return;
}
var text = slot.content || '';
var currentCategories = extractCategories(text);
// Build dialog content
dialog.$content.empty();
// Current categories section
if (currentCategories.length > 0) {
var $currentSection = $('<div>').css('margin-bottom', '20px');
$currentSection.append($('<h4>').text('Current Categories (' + currentCategories.length + ')'));
var $catList = $('<div>').css({
'max-height': '200px',
'overflow-y': 'auto',
'border': '1px solid #ddd',
'padding': '5px',
'margin-bottom': '10px'
});
$.each(currentCategories, function(i, cat) {
var $catItem = $('<label>')
.css({
'display': 'block',
'padding': '3px',
'cursor': 'pointer'
})
.hover(
function() { $(this).css('background', '#f0f0f0'); },
function() { $(this).css('background', 'none'); }
);
var $checkbox = $('<input>')
.attr('type', 'checkbox')
.attr('value', cat.name)
.css('margin-right', '5px');
var displayText = cat.name + (cat.sort ? ' | ' + cat.sort : '');
$catItem.append($checkbox, displayText);
$catList.append($catItem);
});
$currentSection.append($catList);
var $removeBtn = $('<button>')
.text('Remove Selected')
.css({
'background': '#dd3333',
'color': 'white',
'border': 'none',
'padding': '5px 10px',
'border-radius': '3px',
'cursor': 'pointer'
})
.on('click', function() {
var toRemove = [];
$catList.find('input:checked').each(function() {
toRemove.push($(this).val());
});
if (toRemove.length === 0) {
notify('Please select categories to remove', { type: 'warn' });
return;
}
if (confirm('Remove ' + toRemove.length + ' categories?')) {
removeCategories(text, toRemove, dialog);
}
});
$currentSection.append($removeBtn);
dialog.$content.append($currentSection);
} else {
dialog.$content.append($('<p>').text('No categories found on this page.'));
}
// Add categories section
var $addSection = $('<div>');
$addSection.append($('<h4>').text('Add New Categories'));
var $textarea = $('<textarea>')
.attr('placeholder', 'Enter categories to add (one per line):\nExample:\nDogs\nCats|Fluffy\nBirds')
.css({
'width': '100%',
'height': '100px',
'margin-bottom': '10px',
'font-family': 'monospace'
});
$addSection.append($textarea);
var $addBtn = $('<button>')
.text('Add Categories')
.css({
'background': '#00aa00',
'color': 'white',
'border': 'none',
'padding': '5px 10px',
'border-radius': '3px',
'cursor': 'pointer'
})
.on('click', function() {
var input = $textarea.val().trim();
if (!input) {
notify('Please enter categories to add', { type: 'warn' });
return;
}
var lines = input.split('\n');
var items = [];
for (var i = 0; i < lines.length; i++) {
var line = lines[i].trim();
if (line) items.push(line);
}
if (items.length > 0) {
addCategories(items, text, dialog);
}
});
$addSection.append($addBtn);
dialog.$content.append($addSection);
// Dialog buttons
dialog.$buttons.empty();
var $closeBtn = $('<button>')
.text('Close')
.css({
'padding': '5px 15px',
'cursor': 'pointer'
})
.on('click', function() {
dialog.close();
});
dialog.$buttons.append($closeBtn);
}, function (err) {
console.error('[CatManager] Load failed:', err);
notify('Load failed: ' + String(err), { type: 'error' });
dialog.close();
});
}
function removeCategories(currentText, categoriesToRemove, dialog) {
console.log('[CatManager] Removing categories:', categoriesToRemove);
var newText = currentText;
var removed = [];
for (var i = 0; i < categoriesToRemove.length; i++) {
var catName = categoriesToRemove[i];
var regex = new RegExp(
'\\n?\\[\\[\\s*[Cc]ategory\\s*:\\s*' + escapeRegex(catName) + '(?:\\s*\\|[^\\]]*)?\\s*\\]\\]\\n?',
'g'
);
var before = newText.length;
newText = newText.replace(regex, function(match) {
// If the category is on its own line, remove the line
if (match.startsWith('\n') && match.endsWith('\n')) {
return '\n';
} else if (match.startsWith('\n')) {
return '';
} else if (match.endsWith('\n')) {
return '\n';
}
return '';
});
if (newText.length < before) {
removed.push(catName);
console.log('[CatManager] Removed category:', catName);
}
}
if (removed.length === 0) {
notify('No categories were removed', { type: 'warn' });
return;
}
var api = new mw.Api();
var title = mw.config.get('wgPageName');
console.log('[CatManager] Saving edit...');
api.postWithToken('csrf', {
action: 'edit',
title: title,
text: newText,
summary: 'Remove category: ' + removed.join(', '),
minor: 1
}).then(function (result) {
console.log('[CatManager] Edit successful:', result);
notify('Removed: ' + removed.join(', ') + '. Reloading…', { type: 'success' });
dialog.close();
setTimeout(function() {
location.reload();
}, 1500);
}, function (err) {
console.error('[CatManager] Edit failed:', err);
var msg = (err && err.error && err.error.info) ? err.error.info
: (err && err.message ? err.message : String(err));
notify('Edit failed: ' + msg, { type: 'error', autoHide: false });
});
}
function addCategories(rawItems, currentText, dialog) {
console.log('[CatManager] Adding categories, raw items:', rawItems);
var i, cats = [];
for (i = 0; i < rawItems.length; i++) {
var c = parseCat(rawItems[i]);
if (c) cats.push(c);
}
console.log('[CatManager] Valid categories to add:', cats);
if (!cats.length) {
console.log('[CatManager] No valid categories provided');
notify('No valid categories provided.', { type: 'warn' });
return;
}
var toAdd = [];
var already = [];
for (i = 0; i < cats.length; i++) {
var cat = cats[i];
var regex = new RegExp(
'\\[\\[\\s*[Cc]ategory\\s*:\\s*' + escapeRegex(cat.name) + '(?:\\s*\\|[^\\]]*)?\\s*\\]\\]',
'i'
);
var present = regex.test(currentText);
console.log('[CatManager] Category', cat.name, 'already present:', present);
if (present) {
already.push(cat.name);
} else {
toAdd.push(cat);
}
}
console.log('[CatManager] Categories already present:', already);
console.log('[CatManager] Categories to add:', toAdd);
if (!toAdd.length) {
var msg = 'All categories already present: ' + (already.join(', ') || 'none');
console.log('[CatManager]', msg);
notify(msg, { type: 'info' });
return;
}
var addLines = [];
for (i = 0; i < toAdd.length; i++) {
var cc = toAdd[i];
var line = '[[Category:' + cc.name + (cc.sort ? '|' + cc.sort : '') + ']]';
addLines.push(line);
console.log('[CatManager] Will add line:', line);
}
var prefix = /\n$/.test(currentText) ? '' : '\n\n';
var newText = currentText + prefix + addLines.join('\n') + '\n';
var api = new mw.Api();
var title = mw.config.get('wgPageName');
console.log('[CatManager] Saving edit...');
api.postWithToken('csrf', {
action: 'edit',
title: title,
text: newText,
summary: 'Add category: ' + toAdd.map(function (x) { return x.name; }).join(', '),
minor: 1
}).then(function (result) {
console.log('[CatManager] Edit successful:', result);
var successMsg = 'Added: ' + toAdd.map(function (x) { return x.name; }).join(', ') + '. Reloading…';
notify(successMsg, { type: 'success' });
dialog.close();
setTimeout(function() {
location.reload();
}, 1500);
}, function (err) {
console.error('[CatManager] Edit failed:', err);
var msg = (err && err.error && err.error.info) ? err.error.info
: (err && err.message ? err.message : String(err));
notify('Edit failed: ' + msg, { type: 'error', autoHide: false });
});
}
function makeButton(handler) {
console.log('[CatManager] Creating button element');
// Try to match the existing style
var $item = $('<li>')
.attr('id', 'ca-category-manager')
.addClass('mw-list-item');
// Also try div version for some skins
var $itemDiv = $('<div>')
.attr('id', 'ca-category-manager')
.addClass('mw-list-item');
var $a = $('<a>')
.attr('href', '#')
.attr('id', 'ca-category-manager-link')
.addClass('ca-category-manager')
.attr('title', 'Manage categories [alt-shift-g]')
.attr('accesskey', 'g')
.text('Categories')
.on('click', function (e) {
e.preventDefault();
console.log('[CatManager] Button clicked');
handler();
});
// Return both versions
return {
li: $item.append($a.clone(true)),
div: $itemDiv.append($a)
};
}
// Try to attach into Actions; fall back in order; final fallback is heading
function insertButton() {
if (document.getElementById('ca-category-manager')) {
console.log('[CatManager] Button already exists');
return true;
}
var buttons = makeButton(manageCategories);
var targets = [
{ selector: '#p-actions ul', desc: 'Actions menu UL', el: 'li' },
{ selector: '#p-actions .vector-menu-content-list', desc: 'Actions menu (Vector 2022)', el: 'li' },
{ selector: '#p-actions .tab-group', desc: 'Actions tab group', el: 'div' },
{ selector: '#p-views ul', desc: 'Views menu UL', el: 'li' },
{ selector: '#p-views .vector-menu-content-list', desc: 'Views menu (Vector 2022)', el: 'li' },
{ selector: '#p-views .tab-group', desc: 'Views tab group', el: 'div' },
{ selector: '#p-contentnavigation ul', desc: 'Content navigation UL', el: 'li' },
{ selector: '#p-contentnavigation .tab-group', desc: 'Content navigation', el: 'div' },
{ selector: '#p-cactions ul', desc: 'More menu UL', el: 'li' },
{ selector: '#p-cactions .vector-menu-content-list', desc: 'More menu (Vector)', el: 'li' },
{ selector: '.mw-portlet-cactions .vector-menu-content-list', desc: 'More menu (Vector 2022)', el: 'li' },
{ selector: '#p-tb ul', desc: 'Toolbox UL', el: 'li' }
];
for (var i = 0; i < targets.length; i++) {
var target = targets[i];
var $container = $(target.selector);
console.log('[CatManager] Checking', target.desc, '- found:', $container.length > 0);
if ($container.length) {
console.log('[CatManager] Inserting button into:', target.desc);
var buttonToUse = target.el === 'li' ? buttons.li : buttons.div;
$container.append(buttonToUse);
return true;
}
}
// Final fallback: add after the title
var $h = $('#firstHeading');
if ($h.length) {
console.log('[CatManager] Falling back to title insertion');
var $a = $('<a>')
.attr('href', '#')
.attr('id', 'ca-category-manager-link')
.css({
'margin-left': '10px',
'font-size': '0.8em',
'font-weight': 'normal'
})
.text('[Categories]')
.on('click', function (e) {
e.preventDefault();
console.log('[CatManager] Fallback link clicked');
manageCategories();
});
$h.append(document.createTextNode(' '));
$h.append($a);
return true;
}
console.log('[CatManager] Could not find any suitable location for button');
return false;
}
// Retry for late-rendered menus
function attachWithRetries(maxTries, intervalMs) {
console.log('[CatManager] Starting retry mechanism:', maxTries, 'tries, interval:', intervalMs, 'ms');
var tries = 0;
var timer = setInterval(function () {
tries++;
console.log('[CatManager] Retry attempt:', tries);
if (insertButton() || tries >= maxTries) {
console.log('[CatManager] Stopping retries. Success:', document.getElementById('ca-category-manager') !== null);
clearInterval(timer);
}
}, intervalMs);
}
// Main initialization
function initialize() {
console.log('[CatManager] Initializing...');
// Check jQuery
if (typeof $ === 'undefined' && typeof jQuery === 'undefined') {
console.error('[CatManager] jQuery not available');
return;
}
$(function () {
console.log('[CatManager] DOM ready, attempting initial insertion');
if (!insertButton()) {
console.log('[CatManager] Initial insertion failed, starting retries');
attachWithRetries(30, 200); // up to ~6s
}
// Hook support
if (mw.hook && mw.hook('wikipage.content')) {
console.log('[CatManager] Registering wikipage.content hook');
mw.hook('wikipage.content').add(function () {
console.log('[CatManager] wikipage.content hook fired');
insertButton();
});
}
});
}
// Try to load modules, but continue even if some fail
var requiredModules = ['mediawiki.api', 'mediawiki.util'];
console.log('[CatManager] Attempting to load modules:', requiredModules);
if (typeof mw.loader !== 'undefined' && typeof mw.loader.using === 'function') {
mw.loader.using(requiredModules).done(function () {
console.log('[CatManager] Required modules loaded successfully');
initialize();
}).fail(function(err) {
console.error('[CatManager] Some modules failed to load:', err);
// Try to continue anyway if API is available
if (typeof mw.Api === 'function') {
console.log('[CatManager] mw.Api is available, continuing anyway');
initialize();
} else {
console.error('[CatManager] Cannot continue without mw.Api');
alert('Category Manager script error: MediaWiki API not available. Please check your MediaWiki installation.');
}
});
} else {
console.log('[CatManager] mw.loader not available, checking if we can continue...');
if (typeof mw.Api === 'function') {
console.log('[CatManager] mw.Api is available, continuing');
initialize();
} else {
console.error('[CatManager] Cannot continue without mw.Api');
}
}
})();