No edit summary |
No edit summary |
||
(88 intermediate revisions by the same user not shown) | |||
Line 53: | Line 53: | ||
'use strict'; | 'use strict'; | ||
// Updated CSS for suggestions | |||
var css = [ | var css = [ | ||
'#suggestion-container {', | '#suggestion-container {', | ||
Line 60: | Line 61: | ||
'#suggestion-box {', | '#suggestion-box {', | ||
' position: absolute;', | ' position: absolute;', | ||
' top: 100%;', | ' top: 100%;', | ||
' left: 0;', | ' left: 0;', | ||
' width: 100%;', | ' width: 100%;', | ||
' margin-top: 5px;', | ' margin-top: 5px;', | ||
' background-color: #fff;', | ' background-color: #fff;', | ||
' border: 1px solid #ccc;', | |||
' border-radius: 4px;', | |||
' box-shadow: 0 2px 6px rgba(0,0,0,0.15);', | |||
' z-index: 1000;', | ' z-index: 1000;', | ||
'}', | '}', | ||
Line 75: | Line 79: | ||
'}' | '}' | ||
].join('\n'); | ].join('\n'); | ||
// Add the CSS to the head | |||
$('head').append('<style type="text/css">' + css + '</style>'); | $('head').append('<style type="text/css">' + css + '</style>'); | ||
// Add suggestion container | |||
$('#suggestion-container').append('<div id="suggestion-box"></div>'); | $('#suggestion-container').append('<div id="suggestion-box"></div>'); | ||
// Show suggestions from the new API | |||
function showSuggestions() { | function showSuggestions() { | ||
var query = $(this).val(); | var query = $(this).val().trim(); | ||
if (query.length > 0) { | if (query.length > 0) { | ||
var apiUrl = "https:// | // 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({ | $.ajax({ | ||
url: apiUrl, | url: apiUrl, | ||
dataType: "json", | dataType: "json", | ||
method: "GET", | method: "GET", | ||
success: function(data) { | success: function(data) { | ||
var suggestions = data | var suggestions = data || []; | ||
$('#suggestion-box').empty(); | $('#suggestion-box').empty(); | ||
// Check if suggestions exist | |||
if (suggestions.length === 0) { | |||
$('#suggestion-box').hide(); | |||
return; | |||
} | |||
// Populate suggestions | |||
$.each(suggestions, function(index, suggestion) { | $.each(suggestions, function(index, suggestion) { | ||
var item = $('<div class="suggestion-item"></div>').text(suggestion | var item = $('<div class="suggestion-item"></div>').text(suggestion); | ||
$('#suggestion-box').append(item); | $('#suggestion-box').append(item); | ||
}); | }); | ||
// Show suggestion box | |||
$('#suggestion-box').show(); | |||
}, | }, | ||
error: function(jqxhr, textStatus, error) { | error: function(jqxhr, textStatus, error) { | ||
console.error('Error fetching suggestions:', 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(); | |||
} | |||
} | } | ||
function selectSuggestion(event) { | // 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 | $(document).on('click', hideSuggestions); | ||
// Handle suggestion click with mousedown | |||
$('#suggestion-box').on('mousedown', '.suggestion-item', selectSuggestion); | |||
})(jQuery); | })(jQuery); | ||
$(document).ready(function() { | $(document).ready(function() { | ||
Line 274: | Line 283: | ||
function() { $(this).css({textDecoration: 'underline', color: '#000'}); }, | function() { $(this).css({textDecoration: 'underline', color: '#000'}); }, | ||
function() { $(this).css({textDecoration: 'none', color: '#555'}); } | function() { $(this).css({textDecoration: 'none', color: '#555'}); } | ||
); | ); | ||
}); | }); | ||
Line 295: | Line 305: | ||
$inputElement.addClass('search-input'); | $inputElement.addClass('search-input'); | ||
console.log('Class "search-input" added to the input element.'); | 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 Tags Feature - ES5 Compatible | |||
* Top collapsible design, clean colors, working structure display | |||
*/ | |||
(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, // Start as null to detect if loaded | |||
namespace: (mw.config.get('wgCanonicalNamespace') || 'Main').replace(/ /g, '_'), // Replace spaces with underscores | |||
currentPage: mw.config.get('wgPageName'), | |||
pageName: mw.config.get('wgTitle'), | |||
isEditMode: false, | |||
isCollapsed: true, | |||
$menu: null, | |||
$editorBox: null, | |||
$preview: null, | |||
selectedItem: null, | |||
suggestedTags: [], // Store suggested tags | |||
selectedTags: [] // Store selected tags | |||
}; | |||
// 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)}', | |||
// 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}', | |||
// 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>' | |||
); | |||
// 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 - ' + LeftMenuEditor.namespace + '</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="import">' + | |||
'<span class="leftmenu-quick-action-icon">📥</span>' + | |||
'<span class="leftmenu-quick-action-label">Import</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 | |||
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 | |||
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($('<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(); | |||
// 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 menu editor 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}', | |||
// 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}}', | |||
// Menu editor styles (keep existing) | |||
'.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-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'); | |||
// Load menu data when switching to menu editor | |||
if (targetTab === 'menu-editor' && !LeftMenuEditor.menuData) { | |||
loadMenuData(); | |||
} | |||
}); | |||
} | |||
// Bind AI events | |||
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(); | |||
} | |||
// Parse and render menu structure | |||
function parseMenuItem(key, value, level) { | |||
level = level || 0; | |||
// Handle different value types | |||
if (value === null || value === undefined || value === '') { | |||
// Item with no value | |||
return createVisualItem(key, '', 'item', level); | |||
} else if (typeof value === 'string') { | |||
// String value | |||
if (key.indexOf('//') === 0) { | |||
return createVisualItem(key, value, 'direct', level); | |||
} else { | |||
return createVisualItem(key, value, 'item', level); | |||
} | |||
} else if (Array.isArray(value)) { | |||
// Array handling | |||
if (value.length === 0) { | |||
return createVisualItem(key, '', 'item', level); | |||
} else if (value.length === 1 && typeof value[0] === 'string') { | |||
return createVisualItem(key, value[0], 'item', level); | |||
} else { | |||
// Create section for array with multiple items | |||
var $section = createVisualSection(key, level); | |||
var $items = $section.find('.leftmenu-section-items'); | |||
for (var i = 0; i < value.length; i++) { | |||
if (typeof value[i] === 'string') { | |||
$items.append(createVisualItem(value[i], value[i], 'item', level + 1)); | |||
} 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 (key.indexOf('//') === 0 && Object.keys(value).length === 0) { | |||
return createVisualItem(key, '', 'direct', level); | |||
} | |||
var $section = createVisualSection(key, 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(key, String(value), 'item', level); | |||
} | |||
// 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(); | |||
$empty.show(); | |||
console.log('Showing empty state - menuData is:', LeftMenuEditor.menuData); | |||
} else { | |||
$preview.show(); | |||
$empty.hide(); | |||
console.log('Rendering menu with data:', LeftMenuEditor.menuData); | |||
// 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(); | |||
} | |||
} | |||
// Create visual item | |||
function createVisualItem(key, value, type, level) { | |||
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' : ''), | |||
'data-key': key, | |||
'data-value': value || '', | |||
'data-type': type, | |||
'data-level': level | |||
}).css('margin-left', (level * 20) + 'px').html( | |||
'<span><span class="leftmenu-icon">' + icon + '</span>' + escapeHtml(displayText) + '</span>' + | |||
'<div class="leftmenu-item-actions">' + | |||
'<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-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; | |||
} | |||
// 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; | |||
} | |||
// 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'); | |||
} | |||
// Edit item inline | |||
function editItemInline($item) { | |||
var $span = $item.find('span').first(); | |||
var currentText = $span.text().replace(/^[•→]\s*/, ''); | |||
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 { | |||
$span.html('<span class="leftmenu-icon">' + | |||
($item.hasClass('direct-link') ? '→' : '•') + | |||
'</span>' + escapeHtml(currentText)); | |||
} | |||
} | |||
$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; | |||
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) | |||
); | |||
updateMenuFromVisual(); | |||
} | |||
// Delete item | |||
function deleteItem($item) { | |||
var name = $item.find('span').first().text().replace(/^[•→]\s*/, ''); | |||
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 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); // Preserve the level | |||
$clone.css('margin-left', (level * 20) + 'px'); // Apply correct margin | |||
$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); | |||
} | |||
}); | |||
} | |||
// 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'); | |||
if (!value || value === key) { | |||
targetObj[key] = key; | |||
} else { | |||
targetObj[key] = value; | |||
} | |||
} | |||
}); | |||
} | |||
processLevel(LeftMenuEditor.$preview, newData); | |||
LeftMenuEditor.menuData = newData; | |||
} | |||
// 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 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 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 indent = ''; | |||
for (var i = 0; i < level; i++) indent += ' '; | |||
$select.append('<option value="' + prefix + key + '">' + indent + key + '</option>'); | |||
addOptions(obj[key], prefix + key + ' > ', level + 1); | |||
} | |||
} | |||
} | |||
addOptions(LeftMenuEditor.menuData, '', 0); | |||
} | |||
// Confirm add section | |||
function confirmAddSection() { | |||
var name = $('#leftmenu-section-name').val().trim(); | |||
var parent = $('#leftmenu-parent-section').val(); | |||
if (!name) { | |||
showStatus('Please enter a section name', 'error'); | |||
return; | |||
} | |||
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(); | |||
if (!pageName) { | |||
showStatus('Please enter a page name', 'error'); | |||
return; | |||
} | |||
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[pageName] = 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 | |||
); | |||
$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(); | |||
}); | |||
// The sortable stop event in makeSortable() will handle the level updates | |||
} | |||
// 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-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)) { | |||
if (k === 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)) { | |||
for (var i = 0; i < targets.length; i++) { | |||
if (k === targets[i]) return true; | |||
} | |||
var v = obj[k]; | |||
if (typeof v === 'string') { | |||
var page = v.split('|')[0].replace(/\[|\]/g, ''); | |||
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') { | |||
for (var t = 0; t < targets.length; t++) { | |||
if (v[a] === 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 | |||
function loadMenuData(callback) { | |||
var page = 'MediaWiki:Menu-' + LeftMenuEditor.namespace; | |||
showStatus('Loading menu from: ' + page, 'info'); | |||
console.log('Loading from page:', page); | |||
LeftMenuEditor.api.get({ | |||
action: 'query', | |||
prop: 'revisions', | |||
titles: page, | |||
rvprop: 'content', | |||
rvslots: 'main', | |||
formatversion: 2 | |||
}).done(function (data) { | |||
console.log('API response:', data); | |||
var raw = ''; | |||
// Check if page exists and has content | |||
if (data.query && data.query.pages && data.query.pages.length > 0) { | |||
var page = data.query.pages[0]; | |||
console.log('Page data:', page); | |||
if (page.revisions && page.revisions.length > 0) { | |||
var revision = page.revisions[0]; | |||
console.log('Revision data:', revision); | |||
if (revision.slots && revision.slots.main) { | |||
raw = revision.slots.main.content || ''; | |||
console.log('Raw content:', raw); | |||
} | |||
} else if (page.missing) { | |||
console.log('Page does not exist yet'); | |||
showStatus('Menu page does not exist yet. Add items to create it.', 'info'); | |||
} | |||
} | |||
// Parse JSON | |||
if (raw) { | |||
try { | |||
LeftMenuEditor.menuData = JSON.parse(raw); | |||
console.log('Parsed menu data:', LeftMenuEditor.menuData); | |||
} catch (e) { | |||
console.error('Failed to parse menu JSON:', e); | |||
console.error('Raw content that failed to parse:', raw); | |||
LeftMenuEditor.menuData = {}; | |||
showStatus('Error parsing menu data: ' + e.message, 'error'); | |||
} | |||
} else { | |||
LeftMenuEditor.menuData = {}; | |||
console.log('No content found, starting with empty menu'); | |||
} | |||
renderVisualMenu(); | |||
$('.leftmenu-status').fadeOut(3000); | |||
if (typeof callback === 'function') { callback(); } | |||
}).fail(function(error) { | |||
console.error('API request failed:', error); | |||
showStatus('Failed to load menu data: ' + (error.error ? error.error.info : 'Unknown error'), 'error'); | |||
LeftMenuEditor.menuData = {}; | |||
renderVisualMenu(); | |||
}); | |||
} | |||
// Save menu | |||
function saveMenu() { | |||
var 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', | |||
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'); | |||
}); | |||
} | |||
// Reset menu | |||
function resetMenu() { | |||
if (confirm('Discard all changes and reload?')) { | |||
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]; }); | |||
} | |||
// Event bindings | |||
function bindEvents() { | |||
// Header toggle - make the container collapsible | |||
$('.leftmenu-editor-header').on('click', function() { | |||
$('.leftmenu-editor-container').toggleClass('collapsed'); | |||
// Load menu data if expanding and on menu tab | |||
if (!$('.leftmenu-editor-container').hasClass('collapsed')) { | |||
if ($('#menu-editor-tab').hasClass('active') && !LeftMenuEditor.menuData) { | |||
loadMenuData(); | |||
} | |||
} | |||
}); | |||
// 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 'import': | |||
showStatus('Import feature coming soon', 'info'); | |||
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); |
Latest revision as of 12:59, 2 June 2025
This page was created by Edgar on 2023-10-09. Last edited by Edgar on 2025-06-02.
/* Any JavaScript here will be loaded for all users on every page load. */
$(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 Tags Feature - ES5 Compatible
* Top collapsible design, clean colors, working structure display
*/
(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, // Start as null to detect if loaded
namespace: (mw.config.get('wgCanonicalNamespace') || 'Main').replace(/ /g, '_'), // Replace spaces with underscores
currentPage: mw.config.get('wgPageName'),
pageName: mw.config.get('wgTitle'),
isEditMode: false,
isCollapsed: true,
$menu: null,
$editorBox: null,
$preview: null,
selectedItem: null,
suggestedTags: [], // Store suggested tags
selectedTags: [] // Store selected tags
};
// 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)}',
// 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}',
// 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>'
);
// 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 - ' + LeftMenuEditor.namespace + '</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="import">' +
'<span class="leftmenu-quick-action-icon">📥</span>' +
'<span class="leftmenu-quick-action-label">Import</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
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
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($('<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();
// 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 menu editor 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}',
// 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}}',
// Menu editor styles (keep existing)
'.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-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');
// Load menu data when switching to menu editor
if (targetTab === 'menu-editor' && !LeftMenuEditor.menuData) {
loadMenuData();
}
});
}
// Bind AI events
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();
}
// Parse and render menu structure
function parseMenuItem(key, value, level) {
level = level || 0;
// Handle different value types
if (value === null || value === undefined || value === '') {
// Item with no value
return createVisualItem(key, '', 'item', level);
} else if (typeof value === 'string') {
// String value
if (key.indexOf('//') === 0) {
return createVisualItem(key, value, 'direct', level);
} else {
return createVisualItem(key, value, 'item', level);
}
} else if (Array.isArray(value)) {
// Array handling
if (value.length === 0) {
return createVisualItem(key, '', 'item', level);
} else if (value.length === 1 && typeof value[0] === 'string') {
return createVisualItem(key, value[0], 'item', level);
} else {
// Create section for array with multiple items
var $section = createVisualSection(key, level);
var $items = $section.find('.leftmenu-section-items');
for (var i = 0; i < value.length; i++) {
if (typeof value[i] === 'string') {
$items.append(createVisualItem(value[i], value[i], 'item', level + 1));
} 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 (key.indexOf('//') === 0 && Object.keys(value).length === 0) {
return createVisualItem(key, '', 'direct', level);
}
var $section = createVisualSection(key, 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(key, String(value), 'item', level);
}
// 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();
$empty.show();
console.log('Showing empty state - menuData is:', LeftMenuEditor.menuData);
} else {
$preview.show();
$empty.hide();
console.log('Rendering menu with data:', LeftMenuEditor.menuData);
// 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();
}
}
// Create visual item
function createVisualItem(key, value, type, level) {
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' : ''),
'data-key': key,
'data-value': value || '',
'data-type': type,
'data-level': level
}).css('margin-left', (level * 20) + 'px').html(
'<span><span class="leftmenu-icon">' + icon + '</span>' + escapeHtml(displayText) + '</span>' +
'<div class="leftmenu-item-actions">' +
'<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-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;
}
// 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;
}
// 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');
}
// Edit item inline
function editItemInline($item) {
var $span = $item.find('span').first();
var currentText = $span.text().replace(/^[•→]\s*/, '');
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 {
$span.html('<span class="leftmenu-icon">' +
($item.hasClass('direct-link') ? '→' : '•') +
'</span>' + escapeHtml(currentText));
}
}
$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;
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)
);
updateMenuFromVisual();
}
// Delete item
function deleteItem($item) {
var name = $item.find('span').first().text().replace(/^[•→]\s*/, '');
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 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); // Preserve the level
$clone.css('margin-left', (level * 20) + 'px'); // Apply correct margin
$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);
}
});
}
// 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');
if (!value || value === key) {
targetObj[key] = key;
} else {
targetObj[key] = value;
}
}
});
}
processLevel(LeftMenuEditor.$preview, newData);
LeftMenuEditor.menuData = newData;
}
// 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 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 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 indent = '';
for (var i = 0; i < level; i++) indent += ' ';
$select.append('<option value="' + prefix + key + '">' + indent + key + '</option>');
addOptions(obj[key], prefix + key + ' > ', level + 1);
}
}
}
addOptions(LeftMenuEditor.menuData, '', 0);
}
// Confirm add section
function confirmAddSection() {
var name = $('#leftmenu-section-name').val().trim();
var parent = $('#leftmenu-parent-section').val();
if (!name) {
showStatus('Please enter a section name', 'error');
return;
}
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();
if (!pageName) {
showStatus('Please enter a page name', 'error');
return;
}
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[pageName] = 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
);
$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();
});
// The sortable stop event in makeSortable() will handle the level updates
}
// 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-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)) {
if (k === 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)) {
for (var i = 0; i < targets.length; i++) {
if (k === targets[i]) return true;
}
var v = obj[k];
if (typeof v === 'string') {
var page = v.split('|')[0].replace(/\[|\]/g, '');
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') {
for (var t = 0; t < targets.length; t++) {
if (v[a] === 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
function loadMenuData(callback) {
var page = 'MediaWiki:Menu-' + LeftMenuEditor.namespace;
showStatus('Loading menu from: ' + page, 'info');
console.log('Loading from page:', page);
LeftMenuEditor.api.get({
action: 'query',
prop: 'revisions',
titles: page,
rvprop: 'content',
rvslots: 'main',
formatversion: 2
}).done(function (data) {
console.log('API response:', data);
var raw = '';
// Check if page exists and has content
if (data.query && data.query.pages && data.query.pages.length > 0) {
var page = data.query.pages[0];
console.log('Page data:', page);
if (page.revisions && page.revisions.length > 0) {
var revision = page.revisions[0];
console.log('Revision data:', revision);
if (revision.slots && revision.slots.main) {
raw = revision.slots.main.content || '';
console.log('Raw content:', raw);
}
} else if (page.missing) {
console.log('Page does not exist yet');
showStatus('Menu page does not exist yet. Add items to create it.', 'info');
}
}
// Parse JSON
if (raw) {
try {
LeftMenuEditor.menuData = JSON.parse(raw);
console.log('Parsed menu data:', LeftMenuEditor.menuData);
} catch (e) {
console.error('Failed to parse menu JSON:', e);
console.error('Raw content that failed to parse:', raw);
LeftMenuEditor.menuData = {};
showStatus('Error parsing menu data: ' + e.message, 'error');
}
} else {
LeftMenuEditor.menuData = {};
console.log('No content found, starting with empty menu');
}
renderVisualMenu();
$('.leftmenu-status').fadeOut(3000);
if (typeof callback === 'function') { callback(); }
}).fail(function(error) {
console.error('API request failed:', error);
showStatus('Failed to load menu data: ' + (error.error ? error.error.info : 'Unknown error'), 'error');
LeftMenuEditor.menuData = {};
renderVisualMenu();
});
}
// Save menu
function saveMenu() {
var 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',
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');
});
}
// Reset menu
function resetMenu() {
if (confirm('Discard all changes and reload?')) {
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]; });
}
// Event bindings
function bindEvents() {
// Header toggle - make the container collapsible
$('.leftmenu-editor-header').on('click', function() {
$('.leftmenu-editor-container').toggleClass('collapsed');
// Load menu data if expanding and on menu tab
if (!$('.leftmenu-editor-container').hasClass('collapsed')) {
if ($('#menu-editor-tab').hasClass('active') && !LeftMenuEditor.menuData) {
loadMenuData();
}
}
});
// 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 'import':
showStatus('Import feature coming soon', 'info');
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);