User:L10nM4st3r/wikimenu.js
Note: After publishing, you may have to bypass your browser's cache to see the changes.
- Firefox / Safari: Hold Shift while clicking Reload, or press either Ctrl-F5 or Ctrl-R (⌘-R on a Mac)
- Google Chrome: Press Ctrl-Shift-R (⌘-Shift-R on a Mac)
- Edge: Hold Ctrl while clicking Refresh, or press Ctrl-F5.
{ //<nowiki>
// TODO LIST:
// * Make the buttons like "User Groups", "Page Info" and "Subpages" all show the data in the menus themselves, instead of taking you to the special pages
// * Add a settings menu.
/*************************************************************************************************************************
**************************************************************************************************************************
**************************************************** Constants ***********************************************************
**************************************************************************************************************************
*************************************************************************************************************************/
const ModulePrefixPath = "https://" + mw.config.get("wgServerName") + "/w/index.php?title={path}&action=raw&ctype=text/javascript";
const UserSettingsPath = "User:" + mw.config.get("wgUserName") + "/wikimenu_settings.json";
const DeafultModulePath = "https://meta.wikimedia.org/w/index.php?title={path}&action=raw&ctype=text/javascript";
const requiredModules = [
"page_utils",
"chat_utils",
"user_utils",
"misc_utils"
];
/*************************************************************************************************************************
**************************************************************************************************************************
************************************************** The Searchbar *********************************************************
**************************************************************************************************************************
*************************************************************************************************************************/
if (mw.config.get("skin") === "monobook") {
document.getElementById("p-search-label").remove()
document.getElementById("searchButton").remove()
document.getElementById("content").style = "margin-top:3.7em"
document.getElementById("p-cactions").remove()
document.getElementById("searchBody").style = "background-color:transparent; border: 0px"
document.getElementById("searchInput").style = "pointer-events: auto"
document.getElementById("mw-searchButton").style = "pointer-events: auto"
}
/*************************************************************************************************************************
**************************************************************************************************************************
****************************************************** SETUP *************************************************************
**************************************************************************************************************************
*************************************************************************************************************************/
var primaryMenuNumber = -1;
const openedMenus = {};
var menuNumber = 0;
var horizontalPosition = 16;
var verticalPosition = mw.config.get("skin") === "vector2022" ? "top:2em;" : "top:0.5em;";
var menuVerticalPosition = 10;
var minimumMenuWidth = "min-width:15em;";
const version = "Alpha";
/*************************************************************************************************************************
**************************************************************************************************************************
*********************************************** Handling of menus ********************************************************
**************************************************************************************************************************
*************************************************************************************************************************/
const topMenu = document.createElement("div");
topMenu.style = "pointer-events: none; width:100%; position:fixed; z-index:2147483647;left:" + horizontalPosition + "em;" + verticalPosition;
document.documentElement.appendChild(topMenu);
if (mw.config.get("skin") === "monobook") {
var notifDiv = document.createElement("div")
notifDiv.style = "display: flex; align-items: right"
topMenu.appendChild(notifDiv)
if (document.getElementById("pt-talk-alert")) {
document.getElementById("pt-talk-alert").style = "pointer-events: auto; display:inline; font-size:75%"
notifDiv.appendChild(document.getElementById("pt-talk-alert"))
}
notifDiv.appendChild(document.getElementById("pt-notifications-alert"))
notifDiv.appendChild(document.getElementById("pt-notifications-notice"))
topMenu.appendChild(document.getElementById("searchBody"))
setInterval(() => {
document.getElementById("pt-notifications-alert").style = "pointer-events: auto; display:inline"
document.getElementById("pt-notifications-notice").style = "pointer-events: auto; display:inline"
}, 100)
document.getElementById("p-personal").remove()
}
function createMenu(menuId, prePinned){
let menuWidth = "menu_width" in settings ? "+" + settings.menu_width : "15";
const output = document.createElement("div");
const header = document.createElement("div");
output.menuWidth = menus[menuId].width != null ? menus[menuId].width : menuWidth;
header.style = "padding-bottom: 3px;padding-top: 6px; cursor: move";
output.style = "pointer-events: auto; overflow-x:hidden;overflow-y:auto;background-color:white; border-style: solid; border-width:1px; position:absolute; z-index:2147483646; width:" + output.menuWidth + "em; min-height:20em;" + minimumMenuWidth;
output.appendChild(header);
header.appendChild(document.createTextNode(menus[menuId].title));
openedMenus[menuNumber] = {menu: output, properties: {}, input: {}, menuId: menuId, id: menuNumber};
menuNumber ++;
if (!prePinned) {
const pinButton = createButton(
"Pin", "Pin this menu, allowing you to open another menu",
null, header, "display:inline;position:absolute;right:0;top:0;"
);
pinButton.addEventListener('click', function(){
primaryMenuNumber = -1;
pinButton.remove();
})
}
topMenu.appendChild(output);
makeElementDraggable(output, header)
return menuNumber-1;
}
function openMenu(menuId, inheritFromMenu) {
let currentMenuPosTop = inheritFromMenu != null ? inheritFromMenu.menu.style.top : "2.5em";
let currentMenuPosLeft = inheritFromMenu != null ? inheritFromMenu.menu.style.left : "0";
let inheritedProperties = inheritFromMenu != null ? inheritFromMenu.properties : undefined;
// If opening another menu, make sure to close the current primary menu.
if (primaryMenuNumber >= 0 && inheritFromMenu == null)
closeMenu(primaryMenuNumber);
// Create the menu
let newMenuNumber = createMenu(menuId, inheritFromMenu != null ? primaryMenuNumber !== inheritFromMenu.id : false);
// This will run if it is not inheriting from a menu (opening from the top bar), or is inheriting from the current primary menu
if (inheritFromMenu == null || primaryMenuNumber === inheritFromMenu.id)
primaryMenuNumber = newMenuNumber;
// Get the menu node
const currentMenu = openedMenus[newMenuNumber].menu;
if (inheritedProperties !== undefined) {openedMenus[newMenuNumber].properties = inheritedProperties;}
const keys = Object.keys(menus[menuId].onOpen);
keys.sort();
keys.forEach(function(arr){
menus[menuId].onOpen[arr](openedMenus[newMenuNumber]).forEach(function(data){
if (data.type === "button") {
const button = createButton(data.text, data.tooltip, data.onClick, currentMenu);
if (data.singleUse === true){
button.addEventListener('click', function(){button.disabled = true});
}
if ("name" in data) {
openedMenus[newMenuNumber].input[data.name] = button;
}
}
else if (data.type === "text") {
const textElement = document.createElement("p");
textElement.innerHTML = data.text;
if("tooltip" in data) textElement.setAttribute("title", data.tooltip);
if("style" in data) textElement.style = data.style;
currentMenu.appendChild(textElement);
}
else if (data.type === "margin") {
const div = document.createElement("div");
div.style = "min-height:" + data.height + "em";
currentMenu.appendChild(div);
}
else if (data.type === "text_input") {
const input = document.createElement("input");
input.setAttribute("type", "text");
if("tooltip" in data) input.setAttribute("title", data.tooltip);
if ("placeholder" in data) input.placeholder = data.placeholder;
if ("value" in data && data.value !== undefined) input.value = data.value;
if ("maxLength" in data) input.maxlength = data.maxLength;
input.style = "width:99%";
currentMenu.appendChild(input);
currentMenu.appendChild(document.createElement("br"));
if ("name" in data) {
input.addEventListener("input", function(){
openedMenus[newMenuNumber].properties[data.name] = input.value;
});
openedMenus[newMenuNumber].properties[data.name] = input.value;
openedMenus[newMenuNumber].input[data.name] = input;
}
}
else if (data.type === "checkbox") {
const input = document.createElement("input");
input.setAttribute("type", "checkbox");
if("tooltip" in data) input.setAttribute("title", data.tooltip);
if ("value" in data && data.value !== undefined) input.checked = data.value;
currentMenu.appendChild(input);
if ("text" in data && data.text !== undefined) {
const label = document.createElement("label");
label.innerText = data.text;
currentMenu.appendChild(label);
}
currentMenu.appendChild(document.createElement("br"));
if ("name" in data) {
input.addEventListener("input", function(){
openedMenus[newMenuNumber].properties[data.name] = input.checked;
});
openedMenus[newMenuNumber].properties[data.name] = input.checked;
openedMenus[newMenuNumber].input[data.name] = input;
}
}
else if (data.type === "multiline_text_input") {
const input = document.createElement("textarea");
if("tooltip" in data) input.setAttribute("title", data.tooltip);
if ("placeholder" in data) input.placeholder = data.placeholder;
if ("value" in data && data.value !== undefined) input.value = data.value;
input.style = "width:100%;height:" + ("height" in data ? data.height : "10em");
currentMenu.appendChild(input);
currentMenu.appendChild(document.createElement("br"));
if ("name" in data) {
input.addEventListener("input", function(){
openedMenus[newMenuNumber].properties[data.name] = input.value;
});
openedMenus[newMenuNumber].properties[data.name] = input.value;
openedMenus[newMenuNumber].input[data.name] = input;
}
}
})
});
///// Close the menu
createButton(
"Close", "Close this menu",
function(){closeMenu(newMenuNumber)},
currentMenu,
"position: absolute; bottom: 0; width:100%"
);
// Make sure to delete this data last, otherwise it causes issues
if (inheritFromMenu != null) {
closeMenu(inheritFromMenu);
currentMenu.style.top = currentMenuPosTop;
currentMenu.style.left = currentMenuPosLeft;
}
}
function closeMenu(menu) {
// If a menu ID is passed, turn it into the menu data
if (typeof menu === "number"){menu = openedMenus[menu]}
if (menu.id === primaryMenuNumber){primaryMenuNumber = -1;}
// Delete the menu element
if (menu) {
menu.menu.onClose()
menu.menu.remove();
}
// Delete the menu data
delete openedMenus[menu.id];
}
// Creates a button with correct styling and functionality.
function createButton(text, tooltipText, onClick, addTo, style) {
const button = document.createElement("input");
button.setAttribute('title', tooltipText);
button.setAttribute('type', "button");
button.value = text;
button.style = "cursor:pointer;" + (style != undefined ? style : "display:block; width:100%;");
if (onClick != null) button.addEventListener('click', onClick);
if (addTo != null) addTo.appendChild(button);
return button;
}
/*************************************************************************************************************************
**************************************************************************************************************************
************************************************** Draggable Menus *******************************************************
**************************************************************************************************************************
*************************************************************************************************************************/
var allDraggableElements = [];
// If resizing the window, make sure the element stays on-screen
window.addEventListener("resize", (e) => {
for (elmnt of allDraggableElements) {
let verticalPosition = elmnt.offsetTop;
if (verticalPosition < -2) verticalPosition = -2;
if (verticalPosition > window.innerHeight - 30) verticalPosition = window.innerHeight - 30;
let _horizontalPosition = elmnt.offsetLeft;
if (_horizontalPosition < -horizontalPosition * 16) _horizontalPosition = -horizontalPosition * 16;
if (_horizontalPosition > window.innerWidth - (elmnt.menuWidth*22.7)) _horizontalPosition = window.innerWidth - (elmnt.menuWidth*22.7);
elmnt.style.top = verticalPosition + "px";
elmnt.style.left = _horizontalPosition + "px";
elmnt.offsetTop = verticalPosition;
elmnt.offsetLeft = _horizontalPosition;
}
})
function makeElementDraggable(elmnt, dragable) {
var pos1 = 0, pos2 = 0, pos3 = 0, pos4 = 0;
if (dragable) {
// if present, the header is where you move the DIV from:
dragable.onmousedown = dragMouseDown;
} else {
// otherwise, move the DIV from anywhere inside the DIV:
elmnt.onmousedown = dragMouseDown;
}
allDraggableElements.push(elmnt);
elmnt.onClose = () => {allDraggableElements.splice(allDraggableElements.indexOf(elmnt), 1)}
function dragMouseDown(e) {
elmnt.parentElement.childNodes.forEach(function(e){e.style.zIndex -= 1})
elmnt.style.zIndex = 2147483646
e = e || window.event;
e.preventDefault();
// get the mouse cursor position at startup:
pos3 = e.clientX;
pos4 = e.clientY;
document.onmouseup = closeDragElement;
// call a function whenever the cursor moves:
document.onmousemove = elementDrag;
}
function elementDrag(e) {
e = e || window.event;
e.preventDefault();
// calculate the new cursor position:
pos1 = pos3 - e.clientX;
pos2 = pos4 - e.clientY;
pos3 = e.clientX;
pos4 = e.clientY;
// set the element's new position:
let verticalPosition = elmnt.offsetTop - pos2;
if (verticalPosition < -2) verticalPosition = -2;
if (verticalPosition > window.innerHeight - 30) verticalPosition = window.innerHeight - 30;
let _horizontalPosition = elmnt.offsetLeft - pos1;
if (_horizontalPosition < -horizontalPosition * 16) _horizontalPosition = -horizontalPosition * 16;
if (_horizontalPosition > window.innerWidth - (elmnt.menuWidth*22.7)) _horizontalPosition = window.innerWidth - (elmnt.menuWidth*22.7);
elmnt.style.top = verticalPosition + "px";
elmnt.style.left = _horizontalPosition + "px";
elmnt.offsetTop = verticalPosition;
elmnt.offsetLeft = _horizontalPosition;
}
function closeDragElement() {
// stop moving when mouse button is released:
document.onmouseup = null;
document.onmousemove = null;
}
elmnt.offsetTop = menuVerticalPosition;
elmnt.offsetLeft = 0;
elmnt.style.left = elmnt.offsetLeft + "px";
elmnt.style.top = elmnt.offsetTop + "px";
}
/*************************************************************************************************************************
**************************************************************************************************************************
*************************************** Handling of modules and data storage *********************************************
**************************************************************************************************************************
*************************************************************************************************************************/
const menuButtonsPriority = {};
// Used to make sure a menu button isn't added twice
const addedMenuButtons = [];
function addToTopMenu(item, text, priority){
item.setAttribute("indexValue", text)
let isAdded = false;
// Check to see if a higher-priority button exists. If it does, add it before.
topMenu.childNodes.forEach(function(existingButton){
if (menuButtonsPriority[existingButton.getAttribute("indexValue")] > priority) {
topMenu.insertBefore(item, existingButton);
menuButtonsPriority[text] = priority;
isAdded = true;
}
})
if(!isAdded) {
// Object was not yet added, meaning it has higher priority than any already added object
topMenu.appendChild(item);
menuButtonsPriority[text] = priority;
}
}
window.WikiMenus = {
version: version,
editAd: " (using [[wikibooks:en:User:L10nM4st3r/WikiMenu|WikiMenu Version-"+version+"]])",
addMenuButton: function(text, tooltip, menuId, priority){
if (addedMenuButtons.includes(menuId)) return;
addedMenuButtons.push(menuId)
var button = createButton(
text, tooltip,
function(){
if (primaryMenuNumber !== -1 && openedMenus[primaryMenuNumber].menuId === menuId){closeMenu(primaryMenuNumber);return}
openMenu(menuId, primaryMenuNumber !== -1 ? openedMenus[primaryMenuNumber] : null)
},
null, "pointer-events: auto; display:inline;"
);
addToTopMenu(button, text, priority);
return button;
},
registerPrimaryMenu: function(id, title, width){
if (id in primaryMenus) {
let numId = primaryMenus[id];
if (menus[numId].is_placeholder) {
delete menus[numId].is_placeholder
menus[numId].title = title;
menus[numId].width = width;
}
return numId;
};
menus.push({title: title, id: menus.length, onOpen: {}, width: width});
primaryMenus[id] = menus.length - 1;
return menus.length - 1;
},
getPrimaryMenu: function(id){
if (id in primaryMenus) return primaryMenus[id];
// If the menu does not exist yet, create a placeholder that can be used by modules, while we wait for the real menu to be registered
menus.push({id: menus.length, onOpen: {}, is_placeholder: true});
primaryMenus[id] = menus.length - 1;
return menus.length - 1;
},
registerMenu: function(title, width){
menus.push({title: title, id: menus.length, onOpen: {}, width: width});
return menus.length - 1;
},
onMenuOpened: function(menuId, onOpenFunct, priority){
if ((priority !== undefined ? priority : 0) in menus[menuId].onOpen) mw.notify("Error adding menu handle function with priority "+priority+", priority already exists. Maybe someday I'll implement a fix for this...")
menus[menuId].onOpen[priority !== undefined ? priority : 0] = onOpenFunct;
},
openMenu: openMenu,
closeMenu: closeMenu
};
window.mwAPI = new mw.Api();
var menus = [];
var settings = { // Starting value should always be the default value
"modules": [
"user_templating",
"user_reporting",
"quick_qvfd",
"remote_editing",
"editing_tools"
],
"menu_width": 15,
"large_menu_width": 20,
"show_edit_ad": true,
"quick_delete_should_redirect": false,
"delete_default_reason": "",
"delete_default_send_template": true,
"delete_default_remove_redirects": true
}
;
var primaryMenus = {};
/*************************************************************************************************************************
**************************************************************************************************************************
*********************************************** Loading of modules *******************************************************
**************************************************************************************************************************
*************************************************************************************************************************/
// Used for loading files while bypassing the cache
jQuery.loadSettings = ( url, options ) => {
options = $.extend( options || {}, {
dataType: "text",
cache: false,
url: url
});
// Use $.ajax() since it is more flexible than $.getScript
// Return the jqXHR object so we can chain callbacks
return jQuery.ajax( options );
};
$.loadSettings("https://" + mw.config.get("wgServerName") + "/w/index.php?title=" + UserSettingsPath + "&action=raw&ctype=text/json").done((file, status) => {
if (file === "") {
mw.notify(`Creating default settings file. <a href="https://${mw.config.get("wgServerName")}/w/index.php?title=${UserSettingsPath}&action=edit">Edit</a>`)
mwAPI.create(UserSettingsPath, {summary: "[Automated] Creating default settings file"}, JSON.stringify(settings, null, "\t"))
window.wikimenuSettings = settings;
init();
return
}
settings = JSON.parse(file);
window.wikimenuSettings = settings;
init();
})
function init() {
if (settings.show_edit_ad === false)
window.WikiMenus.editAd = ""
let added_modules = [];
// Load the module files
for (modulePath of requiredModules.concat(settings.modules)){
let realModulePath = modulePath;
if (!modulePath.startsWith("https://") && !modulePath.startsWith("http://")) {
if (!modulePath.endsWith(".js")) {
if (modulePath.startsWith("."))
// Only the module name is given
realModulePath = ModulePrefixPath.replace("{path}", "User:L10nM4st3r/wikimenu/" + modulePath.split(1) + ".js");
else
realModulePath = DeafultModulePath.replace("{path}", "User:L10nM4st3r/wikimenu/" + modulePath + ".js");
}
// The module path is given without a website url, assume it's a local module that has a per-wiki script.
else realModulePath = ModulePrefixPath.replace("{path}", modulePath);
}
if (!added_modules.includes(realModulePath)) {
added_modules.push(realModulePath);
mw.loader.load(realModulePath);
}
}
}
} //</nowiki>