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>