User:Chrs/MoreMenu.js

window.MoreMenu = window.MoreMenu || {}; window.MoreMenu.messages = window.MoreMenu.messages || {}; $.extend(window.MoreMenu.messages, { "abusefilter-log": "AbuseFilter log",  "all-logs": "All logs",  "analysis": "Analysis",  "analysis-sigma": "Analysis – &#931;",  "analysis-wikihistory": "Analysis – WikiHistory",  "analysis-xtools": "Analysis – XTools",  "articles-created": "Articles created",  "authorship": "Authorship",  "basic-statistics": "Basic statistics",  "block-globally": "Block globally",  "block-log": "Block log",  "block-user": "Block user",  "blocks": "Blocks",  "central-auth": "Central auth",  "change-block": "Change block",  "change-model": "Change model",  "change-protection": "Change protection",  "change-rights": "Change rights",  "checkuser": "CheckUser",  "checkuser-log": "CheckUser log",  "check-external-links": "Check external links",  "check-redirects": "Check redirects",  "contributions": "Contributions",  "copyvio-detector": "Copyright vio detector",  "copyvio-detector-desc": "Queries search engine for copyright violations. Could take a while, so be patient.", "delete-page": "Delete",  "deleted-contributions": "Deleted contributions",  "deletion-log": "Deletion log",  "disambiguate-links": "Disambiguate links",  "edit-intro": "Edit intro",  "edit-summary-search": "Edit summary search",  "edit-summary-usage": "Edit summary usage",  "email-user": "Email user",  "expand-bare-references": "Expand bare references",  "fix-dead-links": "Fix dead links",  "geolocate": "Geolocate",  "global-account-log": "Global account log",  "global-block-log": "Global block log",  "global-contributions-guc": "Global edits – GUC",  "global-contributions-xtools": "Global edits – XTools",  "ip-lookup": "IP lookup",  "latest-diff": "Latest diff",  "mass-message-log": "Mass message log",  "merge-page": "Merge",  "move-log": "Move log",  "move-page": "Move",  "non-automated-edits": "Non-automated edits",  "page": "Page",  "page-logs": "Page logs", "pending-changes-log": "Pending changes log", "protection-log": "Protection log", "protect-page": "Protect", "proxy-check": "Proxy check", "purge-cache": "Purge cache", "rdns": "rDNS", "rename-log": "Rename log", "review-log": "Review log", "search": "Search", "search-by-contributor": "Search by contributor", "search-history-wikiblame": "Search history – WikiBlame", "search-history-xtools": "Search history – XTools", "search-subpages": "Search subpages", "spam-blacklist-log": "Spam blacklist log", "subpages": "Subpages", "sul": "SUL", "suppressed-contribs": "Suppressed contribs", "suppression-log": "Suppression log", "thanks-log": "Thanks log", "tools": "Tools", "top-edited-pages": "Top edited pages", "traffic-report": "Traffic report", "transclusions": "Transclusions", "transclusion-count": "Transclusion count", "unblock-user": "Unblock user", "undelete-page": "Undelete", "upload-log": "Upload log", "uploads": "Uploads", "user": "User", "user-creation-log": "User creation log", "user-groups": "User groups", "user-logs": "User logs", "user-rights-changes": "User rights changes", "user-rights-log": "User rights log", "user-thanks-received": "User thanks received", "view-block": "View block", "view-block-log": "View block log", "whois": "WHOIS" }, window.MoreMenu.messages);

/* ============================================================= */

window.MoreMenu.user = function (config) { return { user: { 'user-logs': { 'all-logs': { url: mw.util.getUrl('Special:Log', {           user: config.targetUser.name          }), insertAfter: false },       'abusefilter-log': { url: mw.util.getUrl('Special:AbuseLog', {           wpSearchUser: config.targetUser.name          }) },       'block-log': { url: mw.util.getUrl('Special:Log', {           user: config.targetUser.name,            type: 'block'          }), targetUserRights: ['block'] },       'checkuser-log': { url: mw.util.getUrl('Special:CheckUserLog', {           cuSearch: config.targetUser.name,            cuSearchType: 'initiator'          }), targetUserRights: ['checkuser-log'], currentUserRights: ['checkuser-log'] },       'deletion-log': { url: mw.util.getUrl('Special:Log', {           user: config.targetUser.name,            type: 'delete'          }), targetUserRights: ['delete'] },       'global-account-log': { url: mw.util.getUrl('Special:Log', {           user: config.targetUser.name,            type: 'globalauth'          }), targetUserRights: ['centralauth-lock'] },       'global-block-log': { url: mw.util.getUrl('Special:Log', {           user: config.targetUser.name,            type: 'gblblock'          }), targetUserRights: ['globalblock'] },       'mass-message-log': { url: mw.util.getUrl('Special:Log', {           user: config.targetUser.name,            type: 'massmessage'          }), targetUserRights: ['massmessage'] },       'move-log': { url: mw.util.getUrl('Special:Log', {           user: config.targetUser.name,            type: 'move'          }), targetUserRights: ['move'] },       'pending-changes-log': { url: mw.util.getUrl('Special:Log', {           user: config.targetUser.name,            type: 'stable'          }), targetUserRights: ['stablesettings'] },       'protection-log': { url: mw.util.getUrl('Special:Log', {           user: config.targetUser.name,            type: 'protect'          }), targetUserRights: ['protect'] },       'rename-log': { url: mw.util.getUrl('Special:Log', {           user: config.targetUser.name,            type: 'gblrename'          }), targetUserRights: ['centralauth-rename'] },       'review-log': { url: mw.util.getUrl('Special:Log', {           user: config.targetUser.name,            type: 'review'          }), targetUserRights: ['review'] },       'spam-blacklist-log': { url: mw.util.getUrl('Special:Log', {           user: config.targetUser.name,            type: 'spamblacklist'          }) },       'suppression-log': { url: mw.util.getUrl('Special:Log', {           user: config.targetUser.name,            type: 'suppress'          }), targetUserRights: ['suppressrevision'], currentUserRights: ['suppressionlog'] },       'thanks-log': { url: mw.util.getUrl('Special:Log', {           user: config.targetUser.name,            type: 'thanks'          }), targetUserGroups: ['user'] },       'upload-log': { url: mw.util.getUrl('Special:Log', {           user: config.targetUser.name,            type: 'upload'          }), targetUserRights: ['upload'] },       'user-creation-log': { url: mw.util.getUrl('Special:Log', {           user: config.targetUser.name,            type: 'newusers'          }), targetUserGroups: ['user'] // any user can create new accounts at Special:CreateAccount

},       'user-rights-log': { url: mw.util.getUrl('Special:Log', {           user: config.targetUser.name,            type: 'rights'          }), targetUserChangeGroups: true }     },      'blocks': { 'block-user': { url: mw.util.getUrl("Special:Block/".concat(config.targetUser.name)), currentUserRights: 'block', targetUserBlocked: false },       'block-globally': { url: "https://meta.wikimedia.org/wiki/Special:GlobalBlock/".concat(config.targetUser.name), currentUserRights: 'globalblock', targetUserIp: true },       'change-block': { url: mw.util.getUrl("Special:Block/".concat(config.targetUser.name)), currentUserRights: 'block', targetUserBlocked: true },       'central-auth': { url: "https://meta.wikimedia.org/wiki/Special:CentralAuth/".concat(config.targetUser.name), currentUserRights: 'centralauth-lock' },       'unblock-user': { url: mw.util.getUrl("Special:Unblock/".concat(config.targetUser.name)), targetUserBlocked: true, currentUserRights: 'block' },       'view-block': { url: mw.util.getUrl('Special:BlockList', {           wpTarget: config.targetUser.name          }), targetUserBlocked: true, style: 'color:#EE1111' },       'view-block-log': { url: mw.util.getUrl('Special:Log', {           page: config.targetUser.name,            type: 'block'          }) }     },      'analysis': { 'analysis-xtools': { url: "https://xtools.wmflabs.org/ec/".concat(config.project.domain, "/").concat(config.targetUser.encodedName) },       'articles-created': { url: "https://xtools.wmflabs.org/pages/".concat(config.project.domain, "/").concat(config.targetUser.encodedName, "/0"), targetUserGroups: ['user'] },       'edit-summary-usage': { url: "https://xtools.wmflabs.org/editsummary/".concat(config.project.domain, "/").concat(config.targetUser.encodedName) },       'edit-summary-search': { url: "https://sigma.toolforge.org/summary.py?server=".concat(config.project.dbName, "&name=").concat(config.targetUser.encodedName) },       'global-contributions-guc': { url: "https://guc.toolforge.org/?user=".concat(config.targetUser.encodedName, "&blocks=true") },       'global-contributions-xtools': { url: "https://xtools.wmflabs.org/globalcontribs/".concat(config.targetUser.encodedName) },       'non-automated-edits': { url: "https://xtools.wmflabs.org/autoedits/".concat(config.project.domain, "/").concat(config.targetUser.encodedName) },       'sul': { url: mw.util.getUrl("Special:CentralAuth/".concat(config.targetUser.name)), targetUserGroups: ['user'] },       'top-edited-pages': { url: "https://xtools.wmflabs.org/topedits/".concat(config.project.domain, "/").concat(config.targetUser.encodedName) }     },      'ip-lookup': { 'whois': { url: "https://whois.toolforge.org/gateway.py?lookup=true&ip=".concat(config.targetUser.escapedName), targetUserIp: true, targetUserIpRange: true },       'proxy-check': { url: "https://ipcheck.toolforge.org/?ip=".concat(config.targetUser.escapedName), targetUserIp: true, currentUserRights: 'block' },       'rdns': { url: "https://www.robtex.com/ip/".concat(config.targetUser.escapedName, ".html"), targetUserIp: true, targetUserIpRange: true },       'geolocate': { url: "https://whatismyipaddress.com/ip/".concat(config.targetUser.escapedName), targetUserIp: true, targetUserIpRange: true }     },

/** Actions the current user can take on the target user. */     'change-rights': { url: mw.util.getUrl('Special:UserRights', {         user: "User:".concat(config.targetUser.name)        }), targetUserGroups: ['user'], currentUserChangeGroups: true },     'checkuser': { url: mw.util.getUrl("Special:CheckUser/".concat(config.targetUser.name)), currentUserRights: ['checkuser'] },     'contributions': { url: mw.util.getUrl("Special:Contributions/".concat(config.targetUser.name)) },     'deleted-contributions': { url: mw.util.getUrl("Special:DeletedContributions/".concat(config.targetUser.name)), currentUserRights: ['deletedhistory', 'deletedtext'], insertAfter: 'contributions' },     'suppressed-contribs': { url: mw.util.getUrl('Special:Log/suppress', {         offender: config.targetUser.name        }), currentUserRights: ['suppressionlog'], insertAfter: 'deleted-contributions' },     'email-user': { url: mw.util.getUrl("Special:EmailUser/".concat(config.targetUser.name)), targetUserGroups: ['user'], visible: undefined !== config.targetUser.emailable },     'uploads': { url: mw.util.getUrl('Special:ListFiles', {         user: config.targetUser.name,          ilshowall: '1'        }), targetUserGroups: ['user'] },     'user-groups': { url: mw.util.getUrl('Special:ListUsers', {         limit: 1,          username: config.targetUser.name        }), targetUserGroups: ['user'] },     'user-rights-changes': { url: "https://xtools.wmflabs.org/ec-rightschanges/".concat(config.project.domain, "/").concat(config.targetUser.encodedName), targetUserGroups: ['user'] },     'user-thanks-received': { url: mw.util.getUrl('Special:Log', {         user: '',          page: "User:".concat(config.targetUser.name),          type: 'thanks'        }), targetUserGroups: ['user'] }   }  }; };

/* ============================================================= */

function _defineProperty(obj, key, value) { if (key in obj) { Object.defineProperty(obj, key, { value: value, enumerable: true, configurable: true, writable: true }); } else { obj[key] = value; } return obj; }

window.MoreMenu.page = function (config) { var _page;

return { page: (_page = {     'page-logs': {        'all-logs': {          url: mw.util.getUrl('Special:Log', { page: config.page.name }),         insertAfter: false        },        'abusefilter-log': {          url: mw.util.getUrl('Special:AbuseLog', { wpSearchTitle: config.page.name })       },        'deletion-log': {          url: mw.util.getUrl('Special:Log', { page: config.page.name, type: 'delete' })       },        'move-log': {          url: mw.util.getUrl('Special:Log', { page: config.page.name, type: 'move' })       },        'pending-changes-log': {          url: mw.util.getUrl('Special:Log', { page: config.page.name, type: 'stable' }),         databaseRestrict: ['bnwiki', 'ckbwiki', 'enwiki', 'fawiki', 'hiwiki', 'ptwiki']        },        'protection-log': {          url: mw.util.getUrl('Special:Log', { page: config.page.name, type: 'protect' })       },        'spam-blacklist-log': {          url: mw.util.getUrl('Special:Log', { page: config.page.name, type: 'spamblacklist' })       }      },

/** Tools and links that provide meaningful statistics. */     'analysis': { 'analysis-xtools': { url: "https://xtools.wmflabs.org/articleinfo/".concat(config.project.domain, "/").concat(config.page.escapedName), pageExists: true, insertAfter: false },       'analysis-wikihistory': { url: "https://wikihistory.toolforge.org/wh.php?page_title=".concat(config.page.escapedName, "&wiki=").concat(config.project.dbName), databaseRestrict: ['enwiki', 'dewiki'], namespaceRestrict: [0], pageExists: true, insertAfter: 'analysis-xtools' },       'analysis-sigma': { url: "https://sigma.toolforge.org/articleinfo.py?page=".concat(config.page.encodedName, "&server=").concat(config.project.dbName), pageExists: true, insertAfter: 'analysis-xtools' },       'authorship': { url: "https://xtools.wmflabs.org/authorship/".concat(config.project.domain, "/").concat(config.page.escapedName), pageExists: true, databaseRestrict: ['dewiki', 'enwiki', 'eswiki', 'euwiki', 'trwiki'] },       'basic-statistics': { url: mw.util.getUrl(config.page.name, {           action: 'info'          }), pageExists: true },       'copyvio-detector': { url: "https://copyvios.toolforge.org?lang=".concat(config.project.domain.split('.')[0], "&project=").concat(config.project.domain.split('.')[1], "&title=").concat(config.page.encodedName, "&oldid=&action=search&use_engine=1&use_links=1"), pageExists: true },       'traffic-report': { url: "https://pageviews.toolforge.org?project=".concat(config.project.domain, "&pages=").concat(config.page.encodedName), pageExists: true },       'transclusion-count': { url: "https://templatecount.toolforge.org/index.php?lang=".concat(config.project.contentLanguage, "&name=").concat(encodeURIComponent(mw.config.get('wgTitle')), "&namespace=").concat(config.page.nsId), namespaceRestrict: [2, 4, 5, 10, 11, 12, 13, 100, 101, 828], noticeProjectRestrict: ['wikipedia'] },       'transclusions': { url: "https://".concat(config.project.domain, "/w/index.php?title=Special:WhatLinksHere/").concat(config.page.encodedName, "&hidelinks=1&hideredirs=1"), namespaceRestrict: [2, 4, 5, 10, 11, 12, 13, 100, 101] }     },      'search': { 'latest-diff': { url: mw.util.getUrl(config.page.name, {           diff: 'cur',            oldid: 'prev'          }), pageExists: true },       'search-by-contributor': { url: "https://xtools.wmflabs.org/topedits/".concat(config.project.domain, "?namespace=").concat(config.page.nsId, "&page=").concat(encodeURIComponent(mw.config.get('wgTitle'))), pageExists: true },       'search-history-wikiblame': { url: "http://wikipedia.ramselehof.de/wikiblame.php?lang=".concat(config.project.contentLanguage, "&project=").concat(config.project.noticeProject, "&article=").concat(config.page.encodedName), pageExists: true },       'search-history-xtools': { url: "https://xtools.wmflabs.org/blame/".concat(config.project.domain, "?page=").concat(config.page.encodedName), pageExists: true, databaseRestrict: ['dewiki', 'enwiki', 'eswiki', 'euwiki', 'trwiki'] },       'search-subpages': { url: mw.util.getUrl('Special:Search', {           sort: 'relevance',            prefix: config.page.name          }) }     },

/** Tools used to semi-automate editing. */     'tools': { 'check-external-links': { url: "https://dispenser.info.tm/~dispenser/cgi-bin/webchecklinks.py?page=".concat(config.page.encodedName, "&hostname=").concat(config.project.domain), pageExists: true },       'check-redirects': { url: "https://dispenser.info.tm/~dispenser/cgi-bin/rdcheck.py?page=".concat(config.page.encodedName, "&lang=").concat(config.project.contentLanguage), pageExists: true, noticeProjectRestrict: ['wikipedia'] },       'disambiguate-links': { url: "https://dispenser.info.tm/~dispenser/cgi-bin/dablinks.py?page=".concat(config.page.encodedName, "&lang=").concat(config.project.contentLanguage), pageExists: true, noticeProjectRestrict: ['wikipedia'] },       'expand-bare-references': { url: "https://refill.toolforge.org/ng/result.php?page=".concat(config.page.encodedName, "&defaults=y&wiki=").concat(config.project.contentLanguage), pageExists: true, namespaceRestrict: [0, 2, 118], noticeProjectRestrict: ['wikipedia', 'commons', 'meta'] },       'fix-dead-links': { url: "https://iabot.toolforge.org/index.php?page=runbotsingle&pagesearch=".concat(config.page.encodedName, "&wiki=").concat(config.project.dbName), pageExists: true, databaseRestrict: ['alswiki', 'barwiki', 'ckbwiki', 'dewiki', 'enwiki', 'eswiki', 'frwiki', 'huwiki', 'itwiki', 'jawiki', 'kowiki', 'lvwiki', 'nlwiki', 'nowiki', 'ptwiki', 'ruwiki', 'svwiki', 'zhwiki'] }     },

/** Actions the current user can take on the page. */     'change-model': { url: mw.util.getUrl("Special:ChangeContentModel/".concat(config.page.name)), currentUserRights: ['editcontentmodel'], pageExists: true, namespaceRestrict: [2, 4, 8, 100, 108, 828] },     'delete-page': { /** NOTE: must use `decodeURIComponent` because mw.util.getUrl will otherwise double-escape. This should be safe. */       url: mw.util.getUrl(null, {          action: 'delete',          'wpReason': decodeURIComponent($('#delete-reason').text).replace(/\+/g, ' ')        }), currentUserRights: ['delete'], pageExists: true, visible: !mw.config.get('wgIsMainPage') },     'edit-intro': { url: "//".concat(config.project.domain, "/w/index.php?title=").concat(config.page.encodedName, "&action=edit&section=0"), namespaceRestrict: [0, 1, 2, 3, 4, 5, 118], pageExists: true,

/** Don't show the 'Edit intro' link if the edittop gadget is enabled or there is only one section. */       visible: '1' !== mw.user.options.get('gadget-edittop') && $('.mw-editsection').length },

/** Placeholder for history link in Monobook/Modern, will get replaced by native link */ 'history': { url: '#', visible: -1 !== ['monobook', 'modern'].indexOf(config.currentUser.skin) },     'merge-page': { url: mw.util.getUrl('Special:MergeHistory', {         target: config.page.name        }), currentUserRights: ['mergehistory'], pageExists: true, visible: !mw.config.get('wgIsMainPage') },     'move-page': { url: mw.util.getUrl("Special:MovePage/".concat(config.page.name)), currentUserRights: ['move'], pageExists: true, pageMovable: true }   }, _defineProperty(_page, config.page["protected"] ? 'change-protection' : 'protect-page', {      url: mw.util.getUrl(config.page.name, { action: 'protect' }),     currentUserRights: ['protect', 'stablesettings']    }), _defineProperty(_page, 'purge-cache', {      url: mw.util.getUrl(config.page.name, { action: 'purge', forcelinkupdate: true }),     pageExists: true    }), _defineProperty(_page, 'subpages', {      url: mw.util.getUrl("Special:PrefixIndex/".concat(config.page.name, "/"))    }), _defineProperty(_page, 'undelete-page', {      url: mw.util.getUrl("Special:Undelete/".concat(config.page.name)),      currentUserRights: ['undelete'],      pageDeleted: true    }), _defineProperty(_page, 'watch', {      url: '#',      visible: -1 !== ['monobook', 'modern'].indexOf(config.currentUser.skin)    }), _page)  }; };

/* ============================================================= */ $(function {  window.MoreMenu = window.MoreMenu || {};

if (window.moreMenuDebug) { /* eslint-disable no-console */ console.info('[MoreMenu] Debugging enabled. To disable, check your personal JS and remove `MoreMenu.debug = true;`.'); }

var api = new mw.Api; /**  * Flag to suppress warnings shown by the msg function. * This is set by the addItem method, since user-provided messages may not be stored in `MoreMenu.messages`. */

var ignoreI18nWarnings = false; /** RTL helpers. */

var isRtl = 'rtl' === $('html').prop('dir'); var leftKey = isRtl ? 'right' : 'left'; var rightKey = isRtl ? 'left' : 'right'; /** Configuration to be passed to MoreMenu.user.js, MoreMenu.page.js, and handlers of the 'moremenu.ready' hook. */

var config = new function { /** Project-level */ this.project = { domain: mw.config.get('wgServerName'), siteName: mw.config.get('wgSiteName'), dbName: mw.config.get('wgDBname'), noticeProject: mw.config.get('wgNoticeProject'), contentLanguage: mw.config.get('wgContentLanguage') };   /** Page-level */

this.page = { name: mw.config.get('wgPageName'), nsId: mw.config.get('wgNamespaceNumber'), "protected": !!mw.config.get('wgRestrictionEdit') && mw.config.get('wgRestrictionEdit').length || !!mw.config.get('wgRestrictionCreate') && mw.config.get('wgRestrictionCreate').length, id: mw.config.get('wgArticleId'), movable: !mw.config.get('wgIsMainPage') && !!$('#ca-move').length };   $.extend(this.page, {      escapedName: this.page.name.replace(/[?!'"*]/g, escape),      encodedName: encodeURIComponent(this.page.name)    });    /** Currently viewing user (you). */

this.currentUser = { skin: mw.config.get('skin'), groups: mw.config.get('wgUserGroups'), groupsData: {}, // Keyed by user group name, values have keys 'rights' and 'canAddRemoveGroups'. rights: [] };   /**     * Target user (when viewing user pages, Special:Contribs, etc.). * Also will contain data retrieved from the API such as their user groups and block status. */

this.targetUser = { name: mw.config.get('wgRelevantUserName') || '', groups: [], rights: [], blocked: false, ipRange: false };

if (!this.targetUser.name && 'Contributions' === mw.config.get('wgCanonicalSpecialPageName') && !$('.mw-userpage-userdoesnotexist')[0]) { /**      * IP range at Special:Contribs, where wgRelevantUserName isn't set. * @see https://phabricator.wikimedia.org/T206954 */     this.targetUser.name = mw.config.get('wgTitle').split('/').slice(1).join('/'); this.targetUser.ipRange = true; /** Some things don't work for IPv4 ranges (block log API), but do for IPv6 ranges... */

this.targetUser.ipv4Range = mw.util.isIPv4Address(this.targetUser.name.split('/')[0]); }

$.extend(this.targetUser, {     escapedName: this.targetUser.name.replace(/[?!'"*]/g, escape),      encodedName: encodeURIComponent(this.targetUser.name)    });  };  /**   * Log a message to the console.   * @param {String} message   * @param {String} [level] Level accepted by `console`, e.g. 'debug', 'info', 'log', 'warn', 'error'.   */

function log(message) { var level = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : 'debug';

if (!(window.moreMenuDebug || 'debug' !== level)) { return; }

message = "[MoreMenu] ".concat(message);

if (['', 'warn', 'error'].indexOf(level) >= 0) { message += '\nSee https://w.wiki/9Se for documentation.'; }   /* eslint-disable no-console */

console[level](message); } /**   * Get a MoreMenu module. * @param {String} name Title of module, such as 'user', which pulls in MoreMenu.user.js. * @return {Object} All modules return Objects. */

function getModule(name) { if (!MoreMenu[name]) { log("Missing module MoreMenu.".concat(name, ".js"), 'warn'); }

return MoreMenu[name]; } /**   * Get translation for the given key. * @param {String} key As defined in MoreMenu.messages.js  * @param {Boolean} [ignore] Set to true to suppress warnings if the message doesn't exist. *  This also can be prevented by setting `ignoreI18nWarnings`. * @returns {String} */

function msg(key) { var ignore = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : false; var translation = getModule('messages')[key];

if (!translation && !ignore && !ignoreI18nWarnings) { log("Missing translation for \"".concat(key, "\" in MoreMenu.messages.en.js"), 'warn'); }

return getModule('messages')[key] || key; } /**   * Check whether the message exists. * @param {String} key * @returns {Boolean} */

function msgExists(key) { return undefined !== getModule('messages')[key]; } /**   * Normalize the given ID into the expected format. * @param {String} id  * @returns {string} */

function normalizeId(id) { return id.toLowerCase.replace(/\s+/g, '-'); } /**   * Generate a unique ID for a menu item. * @param {String} parentKey The message key for the parent menu ('user' or 'page'). * @param {String} [itemKey] The message key for the link itself. * @param {String} [submenuKey] The message key for the submenu that the item is within, if applicable. * @returns {String} For example, 'c-user-user-logs-block-log' for User > User logs > Block log. */

function getItemId(parentKey, itemKey) { var submenuKey = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : null;

/* eslint-disable prefer-template */ return "mm-".concat(normalizeId(parentKey)) + (submenuKey ? "-".concat(normalizeId(submenuKey)) : ) + ('string' === typeof itemKey ? "-".concat(normalizeId(itemKey)) : ); } /**   * Load translations if viewing in non-English. MoreMenu first looks for translations on Meta, * at MediaWiki:Gadget-MoreMenu.messages.en.js (replacing 'en' with the requested language). * To override locally, define it before MoreMenu.js in your wiki's gadget definition. * See MoreMenu for more. * @returns {jQuery.Promise} */

function loadTranslations { var dfd = $.Deferred; var lang = mw.config.get('wgUserLanguage');

if ('en' === lang) { return dfd.resolve; }   /** Check Metawiki. */

mw.loader.getScript('https://meta.wikimedia.org/w/index.php?action=raw&ctype=text/javascript' + "&title=MediaWiki:Gadget-MoreMenu.messages.".concat(lang, ".js")).then(function {      return dfd.resolve;    }); return dfd; } /**   * Get promises needed for initializing the script, such as user rights and block status. * @returns {jQuery.Promise[]} */

function getPromises { var promises = new Array(4); /** Note that the blocks API doesn't work for IPv4 ranges. */

if (config.targetUser.name && !config.targetUser.ipv4Range) { promises[0] = api.get({       action: 'query',        list: 'users|blocks',        ususers: config.targetUser.name,        bkusers: config.targetUser.name,        usprop: 'blockinfo|groups|rights|emailable',        bkprop: 'id'      }); }

config.currentUser.rights = JSON.parse(mw.storage.session.getObject('mmUserRights'));

if (!config.currentUser.rights) { promises[1] = mw.user.getRights; }

config.currentUser.groupsData = JSON.parse(mw.storage.session.getObject('mmMetaUserGroups'));

if (!config.currentUser.groupsData) { promises[2] = api.get({       action: 'query',        meta: 'siteinfo',        siprop: 'usergroups'      }); }

promises[3] = loadTranslations; return promises; } /**   * Do the given groups and/or rights indicate the user is allowed to change and other user's groups? * @param {Array} groups * @param {Array} rights * @returns {Boolean} */

function canAddRemoveGroups(groups, rights) { if (rights && rights.indexOf('userrights') >= 0) { /** User explicitly has rights to change user groups. */     return true; }   /* eslint-disable arrow-body-style */

var valid = groups.some(function (group) {     return config.currentUser.groupsData[group] && config.currentUser.groupsData[group].canAddRemoveGroups;    });

if (!valid) { /** Clear cache and fall back to false. */     mw.storage.remove('metaUserGroups'); }

return valid; } /**   * Check if any of the given values are present in the permitted values. * @param {Number|String|Array} permitted * @param {Number|String|Array} given * @returns {Boolean} */

function hasConditional(permitted, given) { /** Convert to arrays if non-array. */   permitted = $.makeArray(permitted); given = $.makeArray(given);

if (!permitted.length) { /** No requirements, so validations pass. */     return true; }

if (!given.length) { /** Nothing given to compare to the permitted values, so validations fail. */     return false; }   /** Loop through to see if a given value is present in the permitted values. */

return given.some(function (item) {     return permitted.indexOf(item) >= 0;    }); } /**   * Generate HTML for a menu item. * @param {String} parentKey Message key for the parent menu ('user' or 'page'). * @param {String} itemKey Message key for menu item. * @param {String} itemData Configuration for this menu item. * @param {String} [submenuKey] The message key for the submenu that the item is within, if applicable. * @return {String} The raw HTML. */

function getItemHtml(parentKey, itemKey, itemData) { var submenuKey = arguments.length > 3 && arguments[3] !== undefined ? arguments[3] : null;

/* eslint-disable max-len */ var namespaceExclusion = itemData.namespaceExclude ? !hasConditional(itemData.namespaceExclude, config.page.nsId) : true; var databaseExclusion = itemData.databaseExclude ? !hasConditional(itemData.databaseExclude, config.page.dbName) : true; /**    * Keys are the name of the check, values are the expressions. * This system is used only to make for easier debugging. * @type {Object} */

var conditions = { /** Project */ noticeProject: hasConditional(itemData.noticeProjectRestrict, config.project.noticeProject), database: hasConditional(itemData.databaseRestrict, config.project.dbName) && databaseExclusion,

/** Page */ namespaceRestrict: hasConditional(itemData.namespaceRestrict, config.page.nsId) && namespaceExclusion, pageExists: itemData.pageExists && config.page.id > 0 || !itemData.pageExists, pageDeleted: itemData.pageDeleted ? !config.page.id : true, pageProtected: itemData.pageProtected ? config.page["protected"] : true, pageMovable: itemData.pageMovable ? config.page.movable : true,

/** Current user */ currentUserGroups: hasConditional(itemData.currentUserGroups, config.currentUser.groups), currentUserRights: hasConditional(itemData.currentUserRights, config.currentUser.rights), currentUserChangeGroups: itemData.currentUserChangeGroups ? canAddRemoveGroups(config.currentUser.groups, config.currentUser.rights) : true,

/** Other */ visibility: undefined !== itemData.visible ? !!itemData.visible : true };

if (config.targetUser.name) { /** Target user */ $.extend(conditions, {       targetUserGroups: hasConditional(itemData.targetUserGroups, config.targetUser.groups),        targetUserRights: hasConditional(itemData.targetUserRights, config.targetUser.rights),        targetUserBlocked: itemData.targetUserBlocked !== undefined ? config.targetUser.blocked === itemData.targetUserBlocked : true,        targetUserChangeGroups: itemData.targetUserChangeGroups ? canAddRemoveGroups(config.targetUser.groups, config.targetUser.rights) : true,        targetUserIp: itemData.targetUserIp ? mw.util.isIPAddress(config.targetUser.name) || config.targetUser.ipRange && itemData.targetUserIpRange : true      }); }

var passed = true; /* eslint-disable no-restricted-syntax */

/* eslint-disable guard-for-in */

for (var condition in conditions) { passed &= conditions[condition];

if (!passed) { log("".concat(parentKey, "/").concat(itemKey, " failed on ").concat(condition)); /** Validations failed, no markup to return */

return ''; }   }    /** Markup for the menu item. */

var titleAttr = msgExists("".concat(itemKey, "-desc")) || itemData.description ? " title=\"".concat(itemData.description ? itemData.description : msg("".concat(itemKey, "-desc")), "\"") : ''; var styleAttr = itemData.style ? " style=\"".concat(itemData.style, "\"") : ''; return "\n           \n                \n                    ").concat(msg(itemData.title || itemKey), "\n                \n            "); } /**   * Apply CSS based on the skin. This is done here because it is fast enough, * not that much CSS, and saves users from having to import one more thing. * @returns {CSSStyleSheet|null} */

function addCSS { switch (config.currentUser.skin) { case 'vector': return mw.util.addCSS("\n               .mm-submenu {\n                    background: #ffffff;\n                    border: 1px solid #a2a9b1;\n                    min-width: 120px !important;\n                    ".concat(rightKey, ": inherit !important;\n                    top: -1px !important;\n                }\n                #p-views {\n                    padding-left: inherit !important;\n                    padding-right: inherit !important;\n                }\n                #p-views .vector-menu-content::after {\n                    display: none !important;\n                }\n                .rtl #p-views .vector-menu-content::before {\n                    display: none !important;\n                }\n            "));

case 'timeless': return mw.util.addCSS("\n               .mm-submenu-wrapper {\n                    cursor: default;\n                }\n                .mm-submenu {\n                    background: #f8f9fa;\n                    border: 1px solid rgb(200, 204, 209);\n                    box-shadow: 0 2px 3px 1px rgba(0, 0, 0, 0.05);\n                    padding: 1.2em 1.5em !important;\n                    top: -1.2em;\n                    white-space: nowrap;\n                    z-index: 95;\n                }\n                .mm-submenu::after {\n                    border-bottom: 8px solid transparent;\n                    border-top: 8px solid transparent;\n                    border-".concat(leftKey, ": 8px solid rgb(200, 204, 209);\n                    content: '';\n                    height: 0;\n                    padding-").concat(rightKey, ": 4px;\n                    position: absolute;\n                    top: 20px;\n                    width: 0;\n                    ").concat(rightKey, ": -13px;\n                }\n                @media screen and (max-width: 1339px) and (min-width: 1100px) {\n                    .mm-submenu::after {\n                        border-").concat(leftKey, ": none;\n                        border-").concat(rightKey, ": 8px solid rgb(200, 204, 209);\n                        padding-").concat(leftKey, ": 4px;\n                        padding-").concat(rightKey, ": inherit;\n                        ").concat(rightKey, ": inherit;\n                        ").concat(leftKey, ": -35px;\n                    }\n                }\n                @media screen and (max-width: 850px) {\n                    .mm-submenu {\n                        top: -2.2em;\n                    }\n                }\n            "));

case 'monobook': return mw.util.addCSS("\n               .mm-tab {\n                    position: relative;\n                }\n                .mm-menu {\n                    background: #fff;\n                    border-bottom: 1px solid #aaa;\n                    ".concat(leftKey, ": -1px;\n                    margin: 0;\n                    position: absolute;\n                    z-index: 99;\n                }\n                .mm-menu ~ a {\n                    z-index: 99 !important;\n                }\n                .mm-submenu {\n                    background: #fff;\n                    border-bottom: 1px solid #aaa;\n                    border-top: 1px solid #aaa;\n                    font-size: inherit;\n                    margin: 0;\n                    min-width: 75px;\n                    top: -1px;\n                    z-index: 95;\n                }\n                .mm-item, .mm-submenu-wrapper {\n                    background: #fff !important;\n                    border-top: 0 !important;\n                    display: block !important;\n                    margin: 0 !important;\n                    padding: 0 !important;\n                    width: 100%;\n                }\n                .mm-item a, .mm-submenu-wrapper a {\n                    background: transparent !important;\n                    text-transform: none !important;\n                }\n                .mm-menu a:hover {\n                    text-decoration: underline !important;\n                }\n            "));

case 'modern': return mw.util.addCSS("\n               .mm-menu, .mm-submenu {\n                    background: #f0f0f0 !important;\n                    border: solid 1px #666;\n                }\n                .mm-menu {\n                    border-top: none;\n                    position: absolute;\n                    z-index: 99;\n                }\n                .mm-submenu-wrapper > a {\n                    cursor: default !important;\n                }\n                .mm-item, .mm-submenu-wrapper {\n                    display: block !important;\n                    float: none !important;\n                    height: inherit !important;\n                    margin: 0 !important;\n                    padding: 0 !important;\n                }\n                .mm-menu a {\n                    display: inline-block;\n                    padding: 3px 10px !important;\n                    text-transform: none !important;\n                    text-decoration: none !important;\n                    white-space: nowrap;\n                    width: 100%;\n                }\n                .mm-menu a:hover {\n                    text-decoration: underline !important;\n                }\n                .mm-submenu {\n                    ".concat(leftKey, ": 100%;\n                    min-width: 120px !important;\n                    top: 0;\n                }\n            "));

default: return null; } }  /**   * Get CSS for the submenu. * @param $element * @returns {Object} To be passed to $.css */

function getSubmenuCss($element) { switch (config.currentUser.skin) { case 'vector': return _defineProperty({}, leftKey, $element.outerWidth);

case 'timeless': return _defineProperty({}, $(window).width <= 1339 && $(window).width >= 1100 ? leftKey : rightKey, $element.outerWidth + 11);

case 'monobook': return _defineProperty({}, leftKey, $element.outerWidth - 2);

default: return {}; } }  /**   * Add hover listeners to the submenus. This may be re-called as many times as needed. */

function addListeners { $('.mm-submenu-wrapper').each(function hoverMenus {     $(this).off('mouseenter').on('mouseenter', function hoverMenusMouseenter { $(this).find('.mm-submenu').css(getSubmenuCss($(this))).show; }).off('mouseleave').on('mouseleave', function hoverMenusMouseleave { $(this).find('.mm-submenu').hide; });   });  }  /**   * Sort alphabetically by translation. * @param {Array} i18nKeys * @returns {Array} */

function sortByTranslation(i18nKeys) { return i18nKeys.sort(function (a, b) {     var nameA = msg(a).toLowerCase;      var nameB = msg(b).toLowerCase;

if (nameA < nameB) { return -1; }

if (nameA > nameB) { return 1; }

return 0; }); }  /**   * Sort given menu items alphabetically, leaving submenus at the top (unsorted),   * and respecting the 'insertAfter' option for each item, if present.   * @param {Object} items   * @return {string[]} Item IDs.   */

function sortItems(items) { var itemKeys = Object.keys(items); /** The labels for the submenus are not sorted. */

var submenus = itemKeys.filter(function (itemKey) {     return !items[itemKey].url;    }); /** All other menu items (top-level) are sorted alphabetically. */

var sortedItemKeys = sortByTranslation(itemKeys.filter(function (itemKey) { return !!items[itemKey].url; }));   /** Loop through again, rearranging based on the 'insertAfter' option. */

var newItemKeys = sortedItemKeys; sortedItemKeys.forEach(function (itemKey) {     var target = items[itemKey].insertAfter;      var newIndex;

if (false === target) { /** False means put at the top. */       newIndex = 0; } else if (true === target) { /** True means put at the bottom. */       newIndex = itemKeys.length; } else if (!target) { /** Nothing to do. */       return; } else { newIndex = newItemKeys.indexOf(target); /**        * Insert at end if target wasn't found. * The +1 is because it goes after the target. */

newIndex = -1 === newIndex ? newItemKeys.length : newIndex + 1; }     /** Remove the original placement, and insert after the target. */

newItemKeys.splice(newItemKeys.indexOf(itemKey), 1); newItemKeys.splice(newIndex, 0, itemKey); });   /** Combine and return, with the submenus coming first. */

return submenus.concat(newItemKeys); } /**   * Get the markup for the menu based on the given data. * @param {String} parentKey Message key for the parent menu ('user' or 'page'). * @param {Object} items Menu items, as provided by MoreMenu.user.js and MoreMenu.page.js  * @param {String} [submenuKey] Used to ensure the generated IDs include the submenu name. * @return {String} Raw HTML. */

function getMenuHtml(parentKey, items, submenuKey) { var html = ''; var submenuClasses = 'vector' === config.currentUser.skin ? 'vector-menu-content-list' : ''; sortItems(items).forEach(function (itemKey) {     var item = items[itemKey];      var itemHtml = '';

if (!item.url) { /** This is a submenu. */       itemHtml += "\n                    \n                    ").concat(msg(itemKey), "&hellip;\n                    "); sortItems(item).forEach(function (submenuItemKey) {         itemHtml += getItemHtml(parentKey, submenuItemKey, item[submenuItemKey], itemKey);        }); itemHtml += '';

if (0 === $(itemHtml).last.find('.mm-submenu li').length) { /** No items in the submenu, so don't show the submenu at all. */         itemHtml = ''; }     } else { itemHtml += getItemHtml(parentKey, itemKey, item, submenuKey); }

html += itemHtml; });   return html;  }  /**   * Draw menu for the Vector skin.   * @param {String} parentKey Message key for the parent menu ('user' or 'page').   * @param {String} html As generated by getMenuHtml.   */

function drawMenuVector(parentKey, html) { html = "") + "") + " ").concat(msg(parentKey), "  ") + ' ' + "".concat(html, "</ul>") + ' '; $(html).insertAfter($('#p-views, .mm-page').last); } /**   * Draw menu for the Timeless skin. * @param {String} parentKey Message key for the parent menu ('user' or 'page'). * @param {String} html As generated by getMenuHtml. */

function drawMenuTimeless(parentKey, html) { html = "<div role=\"navigation\" class=\"mw-portlet mm-".concat(parentKey, " mm-tab\" id=\"p-").concat(parentKey, "\" aria-labelledby=\"p-").concat(parentKey, "-label\">") + "<h3 id=\"p-".concat(parentKey, "-label\">").concat(msg(parentKey), " ") + "<div class=\"mw-portlet-body\"><ul class=\"mm-menu\">".concat(html, "</ul> ");

if ($('#p-cactions').length) { $(html).insertBefore($('#p-cactions')); } else { $('#page-tools .sidebar-inner').prepend(html); } }  /**   * Draw menu for the Monobook skin. * @param {String} parentKey Message key for the parent menu ('user' or 'page'). * @param {String} html As generated by getMenuHtml. */

function drawMenuMonobook(parentKey, html) { html = "") + "<a href=\"javascript:void(0)\">".concat(msg(parentKey), "</a>") + "<ul class=\"mm-menu\" style=\"display:none\">".concat(html, "</ul>") + '</li>'; var $tab = $(html).insertAfter($('#ca-nstab-special, #ca-edit, #ca-ve-edit, #ca-page, #ca-viewsource, #ca-talk').last); var $menu = $tab.find('.mm-menu'); /** Add hover listeners. */

$tab.on('mouseenter', function {      $menu.show;      $tab.find('> a').css({ 'z-index': 99 });   }).on('mouseleave', function  {      $menu.hide;      $tab.find('> a').css({ 'z-index': 'inherit' });   });  }  /**   * Draw menu for the Modern skin. * @param {String} parentKey Message key for the parent menu ('user' or 'page'). * @param {String} html As generated by getMenuHtml. */

function drawMenuModern(parentKey, html) { html = "") + "<a href=\"javascript:void(0)\">".concat(msg(parentKey), "</a>") + "<ul class=\"mm-menu\" style=\"display:none\">".concat(html, "</ul>") + '</li>'; var $tab = $(html).insertAfter($('#ca-nstab-special, #ca-edit, #ca-ve-edit, #ca-page, #ca-viewsource, #ca-talk').last); var $menu = $tab.find('.mm-menu'); /** Position the menu. */

$menu.css({     left: isRtl ? $tab.position.left - $menu.width + $tab.width + 7 : $tab.position.left,      top: $tab.offset.top + $tab.outerHeight    }); /** Add hover listeners. */

$tab.on('mouseenter', function {      $menu.show;    }).on('mouseleave', function  {      $menu.hide;    }); } /**   * Determine which menus to display and insert them into the DOM. */

function drawMenus { var menus = {}; /** Determine which menus to draw. */

if (config.page.nsId >= 0) { $.extend(menus, getModule('page')(config)); }

if (config.targetUser.name) { $.extend(menus, getModule('user')(config)); }   /** Preemptively add the appropriate CSS. */

addCSS; Object.keys(menus).forEach(function (key) {     var html = getMenuHtml(key, menus[key]);

switch (config.currentUser.skin) { case 'vector': drawMenuVector(key, html); break;

case 'monobook': drawMenuMonobook(key, html); break;

case 'modern': drawMenuModern(key, html); break;

case 'timeless': drawMenuTimeless(key, html); break;

default: log("'".concat(config.currentUser.skin, "' is not a supported skin."), 'error'); }   });    addListeners;  }  /**   * Monobook and Modern have 'History' and 'Watch' links as tabs. To conserve space, they are moved to the Page menu.   * This method is called before and after the menus are drawn, to ensure positioning is calculated correctly.   * Listeners on these links are preserved, but we do change the element IDs to our pattern (e.g. `mm-page-history`).   * @param {Boolean} [replace] True to replace the links in .mm-page with the native links, false just hides them.   */

function handleHistoryAndWatchLinks { var replace = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : false; var monobookModern = -1 !== ['monobook', 'modern'].indexOf(config.currentUser.skin); var $histLink = $('#ca-history'); var $watchLink = $('.mw-watchlink'); var $moveLink = $('#ca-move');

if (replace) { if (monobookModern) { $('#mm-page-watch').replaceWith($watchLink.addClass('mm-item').prop('id', 'mm-page-watch').show); $('#mm-page-history').replaceWith($histLink.addClass('mm-item').prop('id', 'mm-page-history').show); }

$('#mm-page-move-page').replaceWith($moveLink.addClass('mm-item').prop('id', 'mm-page-move-page').show); return; }   /** No need to ask for translations when these already live on the page. */

MoreMenu.messages.watch = $watchLink.text || 'watch'; MoreMenu.messages.history = $histLink.text || 'history'; /** Hide the links so that the positioning is calculated correctly in drawMenus */

if (monobookModern) { $watchLink.hide; $histLink.hide; }

$moveLink.hide; } /**   * Add a MutationObserver to look for items added/removed from the given menus. * If they are empty, the container is hidden, otherwise it's unhidden. * MutationObserver implementation courtesy of LunarTwilight. See https://github.com/wikimedia-gadgets/MoreMenu/pull/1 * @param {String|Array} ids Selector for the menu to observe (a parent of the  element). */

function addMutationObserver(ids) { if ('string' === typeof ids) { ids = [ids]; }

ids.forEach(function (id) {     var $parent = $(id);

if (!$parent.length) { return; }

var $menu = $parent.find('ul');

var menuIsEmpty = function menuIsEmpty { return '' === $menu.html.trim; };

if (menuIsEmpty) { $parent.hide; }

new MutationObserver(function (mutations) {       mutations.forEach(function (mutation) { if (mutation.addedNodes.length) { $parent.show; } else if (mutation.removedNodes.length) { if (menuIsEmpty) { $parent.hide; }         }        });      }).observe($menu.get(0), {        childList: true      }); }); }  /**   * For Vector/Timeless's native More menu, we keep track of how many times it gets populated over time,   * to intelligently determine whether we should leave it upfront to make the script feel more responsive.   */

function observeCactionsMenu { /** Check local storage to see if user continually has items added to the native menu. */   var reAddCount = parseInt(mw.storage.get('mmNativeMenuUsage'), 10) || 0; /** Ignore for non-Vector/Timeless, if user disabled this feature, or if reAddCount is high. */

if (-1 === ['vector', 'timeless'].indexOf(config.currentUser.skin) || !!window.moreMenuDisableAutoRemoval || reAddCount >= 5) { return; }

addMutationObserver('#p-cactions'); /** Wait 5 seconds before checking the reAddCount, to give other scripts time to populate the menu */

setTimeout(function {      if ($('#p-cactions').find('li').length) {        mw.storage.set('mmNativeMenuUsage', reAddCount + 1);      }    }, 5000); } /**   * Remove redundant links from the native More menu, and from the "Page tools" and "Userpage tools" in Timeless. * This uses mw.storage to keep track of whether the native menu usually gets items added to it  * after MoreMenu has loaded. If this is the case, it will not remove the menu at all. This is to avoid * the irritating "jumping" effect you get due to race conditions. */

function removeNativeLinks { var linksToRemove = ['#ca-protect', '#ca-unprotect', '#ca-delete', '#ca-undelete'];

if ('timeless' === config.currentUser.skin) { linksToRemove.push.apply(linksToRemove, ['#t-contributions', '#t-log', '#t-blockip', '#t-emailuser', '#t-userrights', '#t-info', '#t-pagelog']); }

$(linksToRemove.join(',')).remove; handleHistoryAndWatchLinks; observeCactionsMenu; /** Remove empty userpage tools menu in Timeless */

if ('timeless' === config.currentUser.skin) { addMutationObserver('#p-userpagetools'); } }  /**   * Removes the link to the block log if the user has never been blocked. */

function removeBlockLogLink { api.get({     action: 'query',      list: 'logevents',      letype: 'block',      letitle: "User:".concat(config.targetUser.name),      lelimit: 1    }).done(function (data) {      if (!data.query.logevents.length) {        $('#mm-user-blocks-view-block-log').remove;      }    }); } /**   * Remove unneeded links and empty submenus. */

function removeUnneededLinks { handleHistoryAndWatchLinks(true); /** Following logic only applies to the User menu. */

if (!config.targetUser.name) { return; }

removeBlockLogLink; /** Observe all submenus, removing them if they're empty. */

addMutationObserver(['#mm-page-analysis', '#mm-page-search', '#mm-page-tools', '#mm-user-blocks', '#mm-user-analysis']);

if (config.targetUser.ipRange) { $('#mm-user-user-logs').remove; $('#mm-user-deleted-contributions').remove; $('#mm-user-suppressed-contributions').remove; /**      * For now assuming no tools accept IP ranges. * FIXME: We should hide all empty menus, and use MutationObserver to un-hide them *  if a script adds something to them after MoreMenu has finished loading. */

$('#mm-user-analysis').remove; } }  /**   * Script entry point. The 'moremenu.ready' event is fired after the menus are drawn and populated. */

function init { $.when.apply(this, getPromises).done(function (targetUserData, userRightsData, metaData) {     /** Target user data. */      if (targetUserData) {        $.extend(config.targetUser, targetUserData[0].query.users[0]);

if (targetUserData[0].query.blocks.length) { config.targetUser.blocked = true; config.targetUser.blockid = targetUserData[0].query.blocks[0].id; }     }      /** Cache user rights of current user, if given. */

if (userRightsData) { log('caching user rights'); mw.storage.session.setObject('mmUserRights', JSON.stringify(userRightsData)); config.currentUser.rights = userRightsData.slice; }     /** Cache global user groups of current user, if given. */

if (metaData) { log('caching global user groups'); config.currentUser.groupsData = {}; metaData[0].query.usergroups.forEach(function (el) {         config.currentUser.groupsData[el.name] = {            rights: el.rights,            canAddRemoveGroups: !!el.add || !!el.remove          };        }); mw.storage.session.setObject('mmMetaUserGroups', JSON.stringify(config.currentUser.groupsData)); }

removeNativeLinks; drawMenus; removeUnneededLinks; mw.hook('moremenu.ready').fire(config); }); }  /**   * Get the ID of the menu item preceding the given item.   * @param {String} menu The parent menu the item lives (or will live) under.   * @param {String} [submenu] The given item lives (or will live) under this submenu.   * @param {Boolean|String} [insertAfter] The preceding item should be this one.   * @returns {jQuery}   */

function getBeforeItem(menu, submenu, insertAfter) { var beforeItemKey = getItemId(menu, insertAfter || '', submenu); return $("#".concat(beforeItemKey)); } /**   * PUBLIC METHODS */

/**  * Add an item (or submenu + its items) to a menu, given the full config hash for the item. * @param {String} menu The parent menu to append to, either 'user' or 'page'. * @param {Object} items A single item/submenu with structure matching config at MoreMenu.user or MoreMenu.page. * @param {Boolean|String} [insertAfter] Insert the item/submenu after the item with this ID. * @param {String} [submenu] Insert into this submenu. */

MoreMenu.addItemCore = function (menu, items, insertAfter, submenu) { if (!$(".mm-".concat(menu)).length) { /** Menu not shown. */     return; }

var menuId = submenu ? "#mm-".concat(menu, "-").concat(submenu) : ".mm-".concat(menu); // FYI the element has skin-defined IDs, so we use a CSS class instead.

var $menu = $(menuId);

if (!$menu.length) { log("'".concat(menu).concat(submenu ? " ".concat(submenu) : '', "' menu with selector ").concat(menuId, " not found."), 'warn'); return; }   /**     * Suppress "translation not found" warnings, since the user-provided `items` * may intentionally not have definitions in MoreMenu.messages. */

ignoreI18nWarnings = true; /** Ensure only one item (top-level menu item or submenu + items) is given. */

if (Object.keys(items).length !== 1) { log('MoreMenu.addItemCore was given multiple items. Ignoring all but the first.', 'warn'); items = items[Object.keys(items)[0]]; }   /** `items` could be a submenu. getMenuHtml will work on single items, or a submenu and its items. */

var $html = $(getMenuHtml(menu, items, submenu)); /** Check if insertAfter ID is valid. */

var $beforeItem = getBeforeItem(menu, submenu, insertAfter); var isSubmenuItem = $beforeItem.parents('.mm-submenu').length;

if ($beforeItem.length && (!submenu || submenu && isSubmenuItem)) { /** insertAfter ID is valid. */     $beforeItem.after($html); } else { var newI18nKey = normalizeId(Object.keys(items)[0]); var newId = getItemId(menu, newI18nKey, submenu); /** Grab the visible top-level items (excluding submenus). */

var $topItems = submenu ? $(menuId).find('.mm-submenu > .mm-item') : $(menuId).find('.mm-menu > .mm-item');

if (true === insertAfter) { $topItems.last.after($html); return; }

if (false === insertAfter) { $topItems.first.before($html); return; }

if (!$beforeItem.length && insertAfter) { /** insertAfter ID was either invalid or not found. */       log('getMenuHtml was given an invalid `insertAfter`.', 'warn'); }     /** Create a list of the IDs and append the new ID. */

var ids = $.map($topItems, function (el) {       return el.id;      }).concat([newId]); /** Extract the i18n keys and sort alphabetically by translation. */

var i18nKeys = sortByTranslation(ids.map(function (id) { return id.replace(new RegExp("^mm-".concat(menu, "-").concat(submenu ? "".concat(submenu, "-") : )), ); }));     /** Get the index of the preceding item. */

var beforeItemIndex = i18nKeys.indexOf(newI18nKey) - 1;

if (beforeItemIndex < 0) { /** Alphabetically the new item goes first, so insert it before the existing first item. */       $("#".concat(ids[0])).before($html); } else { /** Insert HTML after the would-be previous item in the menu. */       $("#".concat(getItemId(menu, i18nKeys[Math.max(0, i18nKeys.indexOf(newI18nKey) - 1)], submenu))).after($html); }   }

addListeners; /** Reset flag to surface warnings about missing translations. */

ignoreI18nWarnings = false; }; /**   * Add a single item to a menu. * @param {String} menu Either 'page' or 'user'. * @param {String} name Title for the link. Can either be a normal string or an i18n key. * @param {Object} data Item data. * @param {Boolean|String} [insertAfter] Insert the link after the link with this ID. */

MoreMenu.addItem = function (menu, name, data, insertAfter) { MoreMenu.addItemCore(menu, _defineProperty({}, name, data), insertAfter); }; /**   * Add a single item to a submenu. * @param {String} menu Either 'page' or 'user'. * @param {String} submenu ID for the submenu (such as 'user-logs' or 'analysis'). * @param {String} name Title for the link. Can either be a normal string or an i18n key. * @param {Object} data Item data. * @param {Boolean|String} [insertAfter] Insert the link after the link with this ID. */

MoreMenu.addSubmenuItem = function (menu, submenu, name, data, insertAfter) { MoreMenu.addItemCore(menu, _defineProperty({}, name, data), insertAfter, submenu); }; /**   * Add a new submenu. * @param {String} menu Either 'page' or 'user'. * @param {String} name Name for the submenu. Can either be a normal string or an i18n key. * @param {Object} items Keys are the names for each link, and values are the item data. * @param {Boolean|String} [insertAfter] Insert the submenu after the link with this ID. */

MoreMenu.addSubmenu = function (menu, name, items, insertAfter) { MoreMenu.addItemCore(menu, _defineProperty({}, name, items), insertAfter); }; /**   * Add a link to the given menu. * @param {String} menu Either 'page' or 'user'. * @param {String} name Title for the link. Can either be a normal string or an i18n key. * @param {String} url URL to point to. * @param {Boolean|String} [insertAfter] Insert the link after the link with this ID. */

MoreMenu.addLink = function (menu, name, url, insertAfter) { MoreMenu.addItemCore(menu, _defineProperty({}, name, { url: url }), insertAfter); }; /**   * Add a link to the given submenu. * @param {String} menu Either 'page' or 'user'. * @param {String} submenu ID for the submenu (such as 'user-logs' or 'analysis'). * @param {String} name Title for the link. Can either be a normal string or an i18n key. * @param {String} url URL to point to. * @param {Boolean|String} [insertAfter] Insert the link after the link with this ID. */

MoreMenu.addSubmenuLink = function (menu, submenu, name, url, insertAfter) { MoreMenu.addItemCore(menu, _defineProperty({}, name, { url: url }), insertAfter, submenu); }; /** Entry point. */

init; });