MediaWiki:Gadget-MoreMenu.js

WikiBot留言 | 贡献2026年6月19日 (五) 17:01的版本 (Create MoreMenu gadget (merged single-file with built-in zh translations))
(差异) ←上一版本 | 最后版本 (差异) | 下一版本→ (差异)

注意:在发布之后,您可能需要清除浏览器缓存才能看到所作出的更改的影响。

  • Firefox或Safari:按住Shift的同时单击刷新,或按Ctrl-F5Ctrl-R(Mac为⌘-R
  • Google Chrome:Ctrl-Shift-R(Mac为⌘-Shift-R
  • Edge:按住Ctrl的同时单击刷新,或按Ctrl-F5
/**
 * MoreMenu gadget for test.zybkcn.com
 * Loader: ResourceLoader
 * Dependencies: mediawiki.api, mediawiki.util, mediawiki.Title, mediawiki.user, mediawiki.storage, user.options
 */

/**
* WARNING: GLOBAL GADGET FILE
* MoreMenu — 为 MediaWiki 添加 Page/User 下拉菜单
* 
* Version:        5.2.2 (merged single-file for test.zybkcn.com)
* Author:         MusikAnimal
* License:        MIT
* GitHub:         https://github.com/wikimedia-gadgets/MoreMenu
* Skins:          Vector, Timeless, Monobook, Modern
*
* 本文件合并自:
*   - MoreMenu.js (主引擎)
*   - MoreMenu.messages.en.js  → 内置简繁中文翻译
*   - MoreMenu.page.js (精简版,仅保留本站链接)
*   - MoreMenu.user.js (精简版,仅保留本站链接)
**/
"use strict";

(function () {

var _typeof = typeof Symbol === "function" && typeof Symbol.iterator === "symbol"
  ? function (o) { return typeof o; }
  : function (o) { return o && typeof Symbol === "function" && o.constructor === Symbol && o !== Symbol.prototype ? "symbol" : typeof o; };

/**
 * 内置翻译表:中文(简繁)
 * 根据 wgUserLanguage 自动选择
 */
var translations = (function () {
  var lang = mw.config.get('wgUserLanguage') || 'zh';
  // 检测是否繁体
  var isHant = /hant|tw|hk|mo/i.test(lang);

  // 简繁对照表
  var strings = {
    page:                            ['页面', '頁面'],
    user:                            ['用户', '用戶'],
    'page-logs':                     ['页面日志', '頁面日誌'],
    'user-logs':                     ['用户日志', '用戶日誌'],
    'all-logs':                      ['所有日志', '所有日誌'],
    'abusefilter-log':               ['滥用过滤器日志', '濫用過濾器日誌'],
    'deletion-log':                  ['删除日志', '刪除日誌'],
    'move-log':                      ['移动日志', '移動日誌'],
    'protection-log':                ['保护日志', '保護日誌'],
    'spam-blacklist-log':            ['垃圾链接黑名单日志', '垃圾連結黑名單日誌'],
    'block-log':                     ['封禁日志', '封禁日誌'],
    'checkuser-log':                 ['用户查核日志', '用戶查核日誌'],
    'global-account-log':            ['全域账号日志', '全域帳號日誌'],
    'global-block-log':              ['全域封禁日志', '全域封禁日誌'],
    'mass-message-log':              ['大量消息日志', '大量訊息日誌'],
    'rename-log':                    ['重命名日志', '重新命名日誌'],
    'review-log':                    ['审核日志', '審核日誌'],
    'suppression-log':               ['监督日志', '監督日誌'],
    'thanks-log':                    ['感谢日志', '感謝日誌'],
    'upload-log':                    ['上传日志', '上傳日誌'],
    'user-creation-log':             ['用户创建日志', '用戶建立日誌'],
    'user-rights-log':               ['用户权限日志', '用戶權限日誌'],
    'blocks':                        ['封禁', '封禁'],
    'block-user':                    ['封禁用户', '封禁用戶'],
    'change-block':                  ['更改封禁', '變更封禁'],
    'unblock-user':                  ['解除封禁', '解除封禁'],
    'view-block':                    ['查看封禁', '檢視封禁'],
    'view-block-log':                ['查看封禁日志', '檢視封禁日誌'],
    'analysis':                      ['分析', '分析'],
    'basic-statistics':              ['基本信息', '基本資訊'],
    'transclusions':                 ['嵌入情况', '嵌入情況'],
    'search':                        ['搜索', '搜尋'],
    'latest-diff':                   ['最新差异', '最新差異'],
    'search-subpages':               ['搜索子页面', '搜尋子頁面'],
    'delete-page':                   ['删除', '刪除'],
    'undelete-page':                 ['恢复', '還原'],
    'move-page':                     ['移动', '移動'],
    'protect-page':                  ['保护', '保護'],
    'change-protection':             ['更改保护', '變更保護'],
    'purge-cache':                   ['清除缓存', '清除快取'],
    'subpages':                      ['子页面', '子頁面'],
    'edit-intro':                    ['编辑导言', '編輯導言'],
    'history':                       ['历史', '歷史'],
    'watch':                         ['监视', '監視'],
    'merge-page':                    ['合并', '合併'],
    'change-model':                  ['更改内容模型', '變更內容模型'],
    'contributions':                 ['贡献', '貢獻'],
    'deleted-contributions':         ['已删除贡献', '已刪除貢獻'],
    'suppressed-contribs':           ['被监督的贡献', '被監督的貢獻'],
    'email-user':                    ['电邮用户', '電郵用戶'],
    'change-rights':                 ['更改用户权限', '變更用戶權限'],
    'checkuser':                     ['用户查核', '用戶查核'],
    'uploads':                       ['上传记录', '上傳記錄'],
    'user-groups':                   ['用户组', '用戶組'],
    'sul':                           ['全域账户', '全域帳戶'],
    'user-thanks-received':          ['收到的感谢', '收到的感謝'],
    'tools':                         ['工具', '工具']
  };

  var result = {};
  Object.keys(strings).forEach(function (key) {
    result[key] = isHant ? strings[key][1] : strings[key][0];
  });
  return result;
})();

/** ============================================================
 *  页面菜单定义 (MoreMenu.page) — 精简版
 *  ============================================================ */
var pageMenu = function (config) {
  return {
    page: (function () {
      var items = {};

      // page-logs 子菜单
      items['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' })
        },
        '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' })
        }
      };

      // analysis 子菜单 — 仅保留本站链接
      items['analysis'] = {
        'basic-statistics': {
          url: mw.util.getUrl(config.page.name, { action: 'info' }),
          pageExists: true,
          insertAfter: false
        },
        'transclusions': {
          url: 'https://' + config.project.domain + '/w/index.php?title=Special:WhatLinksHere/' + config.page.encodedName + '&hidelinks=1&hideredirs=1',
          namespaceRestrict: [2, 4, 5, 10, 11, 12, 13, 100, 101]
        }
      };

      // search 子菜单 — 仅保留本站链接
      items['search'] = {
        'latest-diff': {
          url: mw.util.getUrl(config.page.name, { diff: 'cur', oldid: 'prev' }),
          pageExists: true,
          insertAfter: false
        },
        'search-subpages': {
          url: mw.util.getUrl('Special:Search', { sort: 'relevance', prefix: config.page.name })
        }
      };

      // 操作项
      items['change-model'] = {
        url: mw.util.getUrl('Special:ChangeContentModel/' + config.page.name),
        currentUserRights: ['editcontentmodel'],
        pageExists: true,
        namespaceRestrict: [2, 4, 8, 100, 108, 828]
      };

      items['delete-page'] = {
        url: mw.util.getUrl(config.page.name, {
          action: 'delete',
          wpReason: (function () {
            try { return decodeURIComponent($('#delete-reason').text()).replace(/\+/g, ' '); }
            catch (e) { return undefined; }
          })()
        }),
        currentUserRights: ['delete'],
        pageExists: true,
        visible: !mw.config.get('wgIsMainPage')
      };

      items['edit-intro'] = {
        url: '//' + config.project.domain + '/w/index.php?title=' + config.page.encodedName + '&action=edit&section=0',
        namespaceRestrict: [0, 1, 2, 3, 4, 5, 118],
        pageExists: true,
        visible: mw.user.options.get('gadget-edittop') !== '1' && $('.mw-editsection').length
      };

      // Monobook/Modern 历史占位符
      items['history'] = {
        url: '#',
        visible: ['monobook', 'modern'].indexOf(config.currentUser.skin) !== -1
      };

      items['merge-page'] = {
        url: mw.util.getUrl('Special:MergeHistory', { target: config.page.name }),
        currentUserRights: ['mergehistory'],
        pageExists: true,
        visible: !mw.config.get('wgIsMainPage')
      };

      items['move-page'] = {
        url: mw.util.getUrl('Special:MovePage/' + config.page.name),
        currentUserRights: ['move'],
        pageExists: true,
        pageMovable: true
      };

      // 保护/更改保护(动态 key)
      items[config.page.protected ? 'change-protection' : 'protect-page'] = {
        url: mw.util.getUrl(config.page.name, { action: 'protect' }),
        currentUserRights: ['protect', 'stablesettings']
      };

      items['purge-cache'] = {
        url: mw.util.getUrl(config.page.name, { action: 'purge', forcelinkupdate: true }),
        pageExists: true
      };

      items['subpages'] = {
        url: mw.util.getUrl('Special:PrefixIndex/' + config.page.name + '/')
      };

      items['undelete-page'] = {
        url: mw.util.getUrl('Special:Undelete/' + config.page.name),
        currentUserRights: ['undelete'],
        pageDeleted: true
      };

      // Monobook/Modern 监视占位符
      items['watch'] = {
        url: '#',
        visible: ['monobook', 'modern'].indexOf(config.currentUser.skin) !== -1
      };

      return items;
    })()
  };
};

/** ============================================================
 *  用户菜单定义 (MoreMenu.user) — 精简版
 *  ============================================================ */
var userMenu = function (config) {
  return {
    user: (function () {
      var items = {};

      // user-logs 子菜单
      items['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']
        },
        'protection-log': {
          url: mw.util.getUrl('Special:Log', { user: config.targetUser.name, type: 'protect' }),
          targetUserRights: ['protect']
        },
        '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']
        },
        'user-rights-log': {
          url: mw.util.getUrl('Special:Log', { user: config.targetUser.name, type: 'rights' }),
          targetUserChangeGroups: true
        }
      };

      // blocks 子菜单 — 删除了 block-globally 和 central-auth
      items['blocks'] = {
        'block-user': {
          url: mw.util.getUrl('Special:Block/' + config.targetUser.name),
          currentUserRights: 'block',
          targetUserBlocked: false
        },
        'change-block': {
          url: mw.util.getUrl('Special:Block/' + config.targetUser.name),
          currentUserRights: 'block',
          targetUserBlocked: true
        },
        'unblock-user': {
          url: mw.util.getUrl('Special:Unblock/' + 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 子菜单 — 仅保留 SUL
      items['analysis'] = {
        'sul': {
          url: mw.util.getUrl('Special:CentralAuth/' + config.targetUser.name),
          targetUserGroups: ['user'],
          insertAfter: false
        }
      };

      // 操作项
      items['change-rights'] = {
        url: mw.util.getUrl('Special:UserRights', { user: 'User:' + config.targetUser.name }),
        targetUserGroups: ['user'],
        currentUserChangeGroups: true
      };

      items['checkuser'] = {
        url: mw.util.getUrl('Special:CheckUser/' + config.targetUser.name),
        currentUserRights: ['checkuser']
      };

      items['contributions'] = {
        url: mw.util.getUrl('Special:Contributions/' + config.targetUser.name)
      };

      items['deleted-contributions'] = {
        url: mw.util.getUrl('Special:DeletedContributions/' + config.targetUser.name),
        currentUserRights: ['deletedhistory', 'deletedtext'],
        insertAfter: 'contributions'
      };

      items['suppressed-contribs'] = {
        url: mw.util.getUrl('Special:Log/suppress', { offender: config.targetUser.name }),
        currentUserRights: ['suppressionlog'],
        insertAfter: 'deleted-contributions'
      };

      items['email-user'] = {
        url: mw.util.getUrl('Special:EmailUser/' + config.targetUser.name),
        targetUserGroups: ['user'],
        visible: undefined !== config.targetUser.emailable
      };

      items['uploads'] = {
        url: mw.util.getUrl('Special:ListFiles', { user: config.targetUser.name, ilshowall: '1' }),
        targetUserGroups: ['user']
      };

      items['user-groups'] = {
        url: mw.util.getUrl('Special:ListUsers', { limit: 1, username: config.targetUser.name }),
        targetUserGroups: ['user']
      };

      items['user-thanks-received'] = {
        url: mw.util.getUrl('Special:Log', { user: '', page: 'User:' + config.targetUser.name, type: 'thanks' }),
        targetUserGroups: ['user']
      };

      return items;
    })()
  };
};

/** ============================================================
 *  MoreMenu 主引擎
 *  ============================================================ */
$(function () {
  window.MoreMenu = window.MoreMenu || {};
  window.MoreMenu.messages = translations; // 注入内置翻译

  if (window.moreMenuDebug) {
    console.info('[MoreMenu] Debugging enabled.');
  }

  var api = new mw.Api();
  var ignoreI18nWarnings = false;
  var isRtl = $('html').prop('dir') === 'rtl';
  var leftKey = isRtl ? 'right' : 'left';
  var rightKey = isRtl ? 'left' : 'right';

  /** 配置对象 */
  var config = new function () {
    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')
    };

    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
    };

    // 特殊页面处理
    if (this.page.nsId === -1 && mw.config.get('wgRelevantPageName') && this.page.name !== mw.config.get('wgRelevantPageName')) {
      this.page.name = mw.config.get('wgRelevantPageName');
      this.page.id = mw.config.get('wgRelevantArticleId');
      this.page.nsId = mw.Title.newFromText(this.page.name).namespace;
    }

    this.page.escapedName = this.page.name.replace(/[?!'"()*]/g, escape);
    this.page.encodedName = encodeURIComponent(this.page.name);

    this.currentUser = {
      skin: mw.config.get('skin'),
      groups: mw.config.get('wgUserGroups'),
      groupsData: {},
      rights: []
    };

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

    // IP 范围处理
    if (!this.targetUser.name && mw.config.get('wgCanonicalSpecialPageName') === 'Contributions' && !$('.mw-userpage-userdoesnotexist')[0]) {
      this.targetUser.name = mw.config.get('wgTitle').split('/').slice(1).join('/');
      this.targetUser.ipRange = true;
      this.targetUser.ipv4Range = mw.util.isIPv4Address(this.targetUser.name.split('/')[0]);
    }

    this.targetUser.escapedName = this.targetUser.name.replace(/[?!'"()*]/g, escape);
    this.targetUser.encodedName = encodeURIComponent(this.targetUser.name);
  }();

  function log(message, level) {
    level = level || 'debug';
    if (!(window.moreMenuDebug || level !== 'debug')) return;
    message = '[MoreMenu] ' + message;
    if (['warn', 'error'].indexOf(level) >= 0) {
      message += '\nSee https://w.wiki/9Se for documentation.';
    }
    console[level](message);
  }

  function msg(key, ignore) {
    var translation = translations[key];
    if (!translation && !ignore && !ignoreI18nWarnings) {
      log('Missing translation for "' + key + '"', 'warn');
    }
    return translation || key;
  }

  function msgExists(key) {
    return translations[key] !== undefined;
  }

  function normalizeId(id) {
    return id.toLowerCase().replace(/\s+/g, '-');
  }

  function getItemId(parentKey, itemKey, submenuKey) {
    return 'mm-' + normalizeId(parentKey) +
      (submenuKey ? '-' + normalizeId(submenuKey) : '') +
      (typeof itemKey === 'string' ? '-' + normalizeId(itemKey) : '');
  }

  /** 翻译已内置,直接 resolve */
  function loadTranslations() {
    return $.Deferred().resolve();
  }

  function canAddRemoveGroups(groups, rights) {
    if (rights && rights.indexOf('userrights') >= 0) return true;
    var valid = groups.some(function (group) {
      return config.currentUser.groupsData[group] && config.currentUser.groupsData[group].canAddRemoveGroups;
    });
    if (!valid) mw.storage.remove('metaUserGroups');
    return valid;
  }

  function hasConditional(permitted, given) {
    permitted = $.makeArray(permitted);
    given = $.makeArray(given);
    if (!permitted.length) return true;
    if (!given.length) return false;
    return given.some(function (item) { return permitted.indexOf(item) >= 0; });
  }

  function getItemHtml(parentKey, itemKey, itemData, submenuKey) {
    var namespaceExclusion = itemData.namespaceExclude ? !hasConditional(itemData.namespaceExclude, config.page.nsId) : true;
    var databaseExclusion = itemData.databaseExclude ? !hasConditional(itemData.databaseExclude, config.page.dbName) : true;

    var conditions = {
      noticeProject: hasConditional(itemData.noticeProjectRestrict, config.project.noticeProject),
      database: hasConditional(itemData.databaseRestrict, config.project.dbName) && databaseExclusion,
      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,
      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,
      visibility: itemData.visible !== undefined ? !!itemData.visible : true
    };

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

    for (var c in conditions) {
      if (!conditions[c]) {
        log(parentKey + '/' + itemKey + ' failed on ' + c);
        return '';
      }
    }

    var titleAttr = (msgExists(itemKey + '-desc') || itemData.description)
      ? ' title="' + (itemData.description || msg(itemKey + '-desc')) + '"' : '';
    var styleAttr = itemData.style ? ' style="' + itemData.style + '"' : '';
    return '\n<li id="' + getItemId(parentKey, itemKey, submenuKey) + '" class="mm-item mw-list-item">' +
      '<a href="' + itemData.url + '"' + titleAttr + styleAttr + '>' +
      '<span>' + msg(itemData.title || itemKey) + '</span></a></li>';
  }

  function addCSS() {
    switch (config.currentUser.skin) {
      case 'vector':
        return mw.util.addCSS(
          '.mm-tab .vector-menu-content{height:initial;overflow:initial!important}' +
          '.mm-menu .mw-list-item{white-space:nowrap}' +
          '.mm-submenu{background:var(--background-color-base,#fff);border:1px solid #a2a9b1;min-width:120px!important;' + rightKey + ':inherit!important;top:-1px!important}' +
          '#p-views{padding-left:inherit!important;padding-right:inherit!important}' +
          '#p-views .vector-menu-content::after,.rtl #p-views .vector-menu-content::before{display:none!important}' +
          '.mm-submenu .mm-item{font-size:inherit!important}'
        );
      case 'vector-2022':
        return mw.util.addCSS(
          '#p-page-dropdown .vector-dropdown-content,#p-user-dropdown .vector-dropdown-content{height:initial;overflow-x:initial!important;overflow-y:initial!important;top:35px!important}' +
          '#p-page-dropdown .vector-dropdown-content::after,#p-user-dropdown .vector-dropdown-content::after{display:none!important}' +
          '.mm-menu .mw-list-item{white-space:nowrap}' +
          '.mm-submenu{background:var(--background-color-base,#fff);box-shadow:0 2px 6px -1px rgba(0,0,0,.2);min-width:120px!important;padding:1px 10px;top:-1px!important}' +
          '#p-views{padding-left:inherit!important;padding-right:inherit!important}'
        );
      case 'timeless':
        return mw.util.addCSS(
          '.mm-submenu-wrapper{cursor:default}' +
          '.mm-submenu{background:#f8f9fa;border:1px solid #c8ccd1;box-shadow:0 2px 3px 1px rgba(0,0,0,.05);padding:1.2em 1.5em!important;top:-1.2em;white-space:nowrap;z-index:95}' +
          '.mm-submenu::after{border-bottom:8px solid transparent;border-top:8px solid transparent;border-' + leftKey + ':8px solid #c8ccd1;content:"";height:0;padding-' + rightKey + ':4px;position:absolute;top:20px;width:0;' + rightKey + ':-13px}' +
          '@media screen and (max-width:1339px) and (min-width:1100px){.mm-submenu::after{border-' + leftKey + ':none;border-' + rightKey + ':8px solid #c8ccd1;padding-' + leftKey + ':4px;padding-' + rightKey + ':inherit;' + rightKey + ':inherit;' + leftKey + ':-35px}}' +
          '@media screen and (max-width:850px){.mm-submenu{top:-2.2em}}'
        );
      case 'monobook':
        return mw.util.addCSS(
          '.mm-tab{position:relative}' +
          '.mm-menu{background:#fff;border-bottom:1px solid #aaa;' + leftKey + ':-1px;margin:0;position:absolute;z-index:99}' +
          '.mm-menu~a{z-index:99!important}' +
          '.mm-submenu{background:#fff;border-bottom:1px solid #aaa;border-top:1px solid #aaa;font-size:inherit;margin:0;min-width:75px;top:-1px;z-index:95}' +
          '.mm-item,.mm-submenu-wrapper{background:#fff!important;border-top:0!important;display:block!important;margin:0!important;padding:0!important;width:100%}' +
          '.mm-item a,.mm-submenu-wrapper a{background:transparent!important;text-transform:none!important}' +
          '.mm-menu a:hover{text-decoration:underline!important}'
        );
      case 'modern':
        return mw.util.addCSS(
          '.mm-menu,.mm-submenu{background:#f0f0f0!important;border:solid 1px #666}' +
          '.mm-menu{border-top:none;position:absolute;z-index:99}' +
          '.mm-submenu-wrapper>a{cursor:default!important}' +
          '.mm-item,.mm-submenu-wrapper{display:block!important;float:none!important;height:inherit!important;margin:0!important;padding:0!important}' +
          '.mm-menu a{display:inline-block;padding:3px 10px!important;text-transform:none!important;text-decoration:none!important;white-space:nowrap;width:100%}' +
          '.mm-menu a:hover{text-decoration:underline!important}' +
          '.mm-submenu{' + leftKey + ':100%;min-width:120px!important;top:0}'
        );
      default:
        return null;
    }
  }

  function getSubmenuCss($element) {
    switch (config.currentUser.skin) {
      case 'vector':
      case 'vector-2022':
        var css = {}; css[leftKey] = $element.outerWidth(); return css;
      case 'timeless':
        var css2 = {};
        css2[$(window).width() <= 1339 && $(window).width() >= 1100 ? leftKey : rightKey] = $element.outerWidth() + 11;
        return css2;
      case 'monobook':
        var css3 = {}; css3[leftKey] = $element.outerWidth() - 2; return css3;
      default:
        return {};
    }
  }

  function addListeners() {
    $('.mm-submenu-wrapper').each(function () {
      var $this = $(this);
      $this.off('mouseenter mouseleave');
      $this.on('mouseenter', function () {
        $(this).find('.mm-submenu').css(getSubmenuCss($(this))).show();
      }).on('mouseleave', function () {
        $(this).find('.mm-submenu').hide();
      });
    });
  }

  function sortByTranslation(i18nKeys) {
    return i18nKeys.sort(function (a, b) {
      var nameA = msg(a).toLowerCase();
      var nameB = msg(b).toLowerCase();
      return nameA < nameB ? -1 : nameA > nameB ? 1 : 0;
    });
  }

  function sortItems(items) {
    var itemKeys = Object.keys(items);
    var submenus = itemKeys.filter(function (k) { return !items[k].url; });
    var sortedItemKeys = sortByTranslation(itemKeys.filter(function (k) { return !!items[k].url; }));

    var newItemKeys = sortedItemKeys.slice();
    sortedItemKeys.forEach(function (itemKey) {
      var target = items[itemKey].insertAfter;
      var newIndex;
      if (target === false) newIndex = 0;
      else if (target === true) newIndex = itemKeys.length;
      else if (!target) return;
      else {
        newIndex = newItemKeys.indexOf(target);
        newIndex = newIndex === -1 ? newItemKeys.length : newIndex + 1;
      }
      newItemKeys.splice(newItemKeys.indexOf(itemKey), 1);
      newItemKeys.splice(newIndex, 0, itemKey);
    });
    return submenus.concat(newItemKeys);
  }

  function getMenuHtml(parentKey, items, submenuKey) {
    var html = '';
    var submenuClasses = (config.currentUser.skin === 'vector' || config.currentUser.skin === 'vector-2022') ? 'vector-menu-content-list' : '';
    sortItems(items).forEach(function (itemKey) {
      var item = items[itemKey];
      var itemHtml = '';
      if (!item.url) {
        // 子菜单
        itemHtml = '\n<li style="position:relative;" id="' + getItemId(parentKey, itemKey) + '" class="mm-submenu-wrapper mw-list-item">' +
          '<a style="font-weight:bold"><span>' + msg(itemKey) + '…</span></a>' +
          '<ul class="menu mm-submenu ' + submenuClasses + '" style="display:none;position:absolute">';
        sortItems(item).forEach(function (subItemKey) {
          itemHtml += getItemHtml(parentKey, subItemKey, item[subItemKey], itemKey);
        });
        itemHtml += '</ul></li>';
        if ($(itemHtml).find('.mm-submenu li').length === 0) itemHtml = '';
      } else {
        itemHtml = getItemHtml(parentKey, itemKey, item, submenuKey);
      }
      html += itemHtml;
    });
    return html;
  }

  function drawMenuVector(parentKey, html) {
    var menu = mw.util.addPortlet('p-' + parentKey, msg(parentKey), '#p-cactions');
    menu.classList.add('mm-' + parentKey, 'mm-tab');
    mw.util.addPortletLink('p-' + parentKey, '#', 'temp');
    var menuContent = menu.querySelector('ul');
    menuContent.innerHTML = html;
    menuContent.classList.add('mm-menu');
  }

  function drawMenuTimeless(parentKey, html) {
    html = '<div role="navigation" class="mw-portlet mm-' + parentKey + ' mm-tab" id="p-' + parentKey + '" aria-labelledby="p-' + parentKey + '-label">' +
      '<h3 id="p-' + parentKey + '-label">' + msg(parentKey) + '</h3>' +
      '<div class="mw-portlet-body"><ul class="mm-menu">' + html + '</ul></div></div>';
    if ($('#p-cactions').length) $(html).insertBefore($('#p-cactions'));
    else $('#page-tools .sidebar-inner').prepend(html);
  }

  function drawMenuMonobook(parentKey, html) {
    html = '<li id="ca-' + parentKey + '" class="mm-' + parentKey + ' mm-tab">' +
      '<a href="javascript:void(0)">' + msg(parentKey) + '</a>' +
      '<ul class="mm-menu" style="display:none">' + 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');
    $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'); });
  }

  function drawMenuModern(parentKey, html) {
    html = '<li id="ca-' + parentKey + '" class="mm-' + parentKey + ' mm-tab">' +
      '<a href="javascript:void(0)">' + msg(parentKey) + '</a>' +
      '<ul class="mm-menu" style="display:none">' + 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');
    $menu.css({
      left: isRtl ? $tab.position().left - $menu.width() + $tab.width() + 7 : $tab.position().left,
      top: $tab.offset().top + $tab.outerHeight()
    });
    $tab.on('mouseenter', function () { $menu.show(); }).on('mouseleave', function () { $menu.hide(); });
  }

  function drawMenus() {
    var menus = {};
    if (config.page.nsId >= 0) $.extend(menus, pageMenu(config));
    if (config.targetUser.name) $.extend(menus, userMenu(config));

    addCSS();
    Object.keys(menus).forEach(function (key) {
      var html = getMenuHtml(key, menus[key]);
      switch (config.currentUser.skin) {
        case 'vector-2022': 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("'" + config.currentUser.skin + "' is not a supported skin.", 'error');
      }
    });
    addListeners();
  }

  function handleHistoryAndWatchLinks(replace) {
    replace = replace || false;
    var monobookModern = ['monobook', 'modern'].indexOf(config.currentUser.skin) !== -1;
    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;
    }
    translations.watch = $watchLink.text() || 'watch';
    translations.history = $histLink.text() || 'history';
    if (monobookModern) { $watchLink.hide(); $histLink.hide(); }
    $moveLink.hide();
  }

  function addMutationObserver(ids) {
    if (typeof ids === 'string') ids = [ids];
    ids.forEach(function (id) {
      var $parent = $(id);
      if (!$parent.length) return;
      var $menu = $parent.find('ul');
      var menuIsEmpty = function () { 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 && menuIsEmpty()) $parent.hide();
        });
      }).observe($menu.get(0), { childList: true });
    });
  }

  function observeCactionsMenu() {
    var reAddCount = parseInt(mw.storage.get('mmNativeMenuUsage'), 10) || 0;
    if (['vector', 'vector-2022', 'timeless'].indexOf(config.currentUser.skin) === -1 ||
        window.MoreMenu.disableAutoRemoval || reAddCount >= 5) return;
    addMutationObserver('#p-cactions');
    setTimeout(function () {
      if ($('#p-cactions').find('li').length) {
        mw.storage.set('mmNativeMenuUsage', reAddCount + 1, 60 * 60 * 24);
      }
    }, 5000);
  }

  function removeNativeLinks() {
    var linksToRemove = ['#ca-protect', '#ca-unprotect', '#ca-delete', '#ca-undelete'];
    if (config.currentUser.skin === 'timeless') {
      linksToRemove = linksToRemove.concat(['#t-contributions', '#t-log', '#t-blockip', '#t-emailuser', '#t-userrights', '#t-info', '#t-pagelog']);
    }
    $(linksToRemove.join(',')).remove();
    handleHistoryAndWatchLinks();
    observeCactionsMenu();
    if (config.currentUser.skin === 'timeless') addMutationObserver('#p-userpagetools');
  }

  function removeBlockLogLink() {
    api.get({
      action: 'query',
      list: 'logevents',
      letype: 'block',
      letitle: 'User:' + config.targetUser.name,
      lelimit: 1
    }).done(function (data) {
      if (!data.query.logevents.length) $('#mm-user-blocks-view-block-log').remove();
    });
  }

  function removeUnneededLinks() {
    handleHistoryAndWatchLinks(true);
    if (!config.targetUser.name) return;
    removeBlockLogLink();
    addMutationObserver(['#mm-page-analysis', '#mm-page-search', '#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();
    }
  }

  function getPromises() {
    var promises = new Array(4);
    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;
  }

  function init() {
    $.when.apply(this, getPromises()).done(function (targetUserData, userRightsData, metaData) {
      if (targetUserData) {
        $.extend(config.targetUser, targetUserData.query ? targetUserData.query.users[0] : targetUserData[0].query.users[0]);
        var blocks = targetUserData.query ? targetUserData.query.blocks : targetUserData[0].query.blocks;
        if (blocks.length) {
          config.targetUser.blocked = true;
          config.targetUser.blockid = blocks[0].id;
        }
      }
      if (userRightsData) {
        var rights = userRightsData.query ? userRightsData : userRightsData.slice ? userRightsData : userRightsData[0];
        if (rights.slice) {
          mw.storage.session.setObject('mmUserRights', JSON.stringify(rights));
          config.currentUser.rights = rights.slice();
        }
      }
      if (metaData) {
        config.currentUser.groupsData = {};
        var groups = metaData.query ? metaData.query.usergroups : metaData[0].query.usergroups;
        groups.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);
    });
  }

  /** 公开 API */
  MoreMenu.addItemCore = function (menu, items, insertAfter, submenu) {
    if (!$('.mm-' + menu).length) return;
    var menuId = submenu ? '#mm-' + menu + '-' + submenu : '.mm-' + menu;
    var $menu = $(menuId);
    if (!$menu.length) { log("'" + menu + (submenu ? ' ' + submenu : '') + "' not found.", 'warn'); return; }
    ignoreI18nWarnings = true;
    if (Object.keys(items).length !== 1) {
      log('Multiple items given, ignoring all but the first.', 'warn');
      items = items[Object.keys(items)[0]];
    }
    var $html = $(getMenuHtml(menu, items, submenu));
    var $beforeItem = (function () {
      var id = getItemId(menu, insertAfter || '', submenu);
      return $('#' + $.escapeSelector(id));
    })();
    var isSubmenuItem = $beforeItem.parents('.mm-submenu').length;
    if ($beforeItem.length && (!submenu || (submenu && isSubmenuItem))) {
      $beforeItem.after($html);
    } else {
      var newI18nKey = normalizeId(Object.keys(items)[0]);
      var newId = getItemId(menu, newI18nKey, submenu);
      var $topItems = submenu ? $(menuId).find('.mm-submenu > .mm-item') : $(menuId).find('.mm-menu > .mm-item');
      if (insertAfter === true) { $topItems.last().after($html); ignoreI18nWarnings = false; addListeners(); return; }
      if (insertAfter === false) { $topItems.first().before($html); ignoreI18nWarnings = false; addListeners(); return; }
      if (!$beforeItem.length && insertAfter) log('Invalid insertAfter.', 'warn');

      var ids = $topItems.toArray().map(function (el) { return el.id; }).concat([newId]);
      var i18nKeys = sortByTranslation(ids.map(function (id) {
        return id.replace(new RegExp('^mm-' + menu + '-' + (submenu ? submenu + '-' : '')), '');
      }));
      var idx = i18nKeys.indexOf(newI18nKey) - 1;
      if (idx < 0) $('#' + $.escapeSelector(ids[0])).before($html);
      else $('#' + $.escapeSelector(getItemId(menu, i18nKeys[idx], submenu))).after($html);
    }
    addListeners();
    ignoreI18nWarnings = false;
  };

  MoreMenu.addItem = function (menu, name, data, insertAfter) {
    var item = {}; item[name] = data;
    MoreMenu.addItemCore(menu, item, insertAfter);
  };

  MoreMenu.addSubmenuItem = function (menu, submenu, name, data, insertAfter) {
    var item = {}; item[name] = data;
    MoreMenu.addItemCore(menu, item, insertAfter, submenu);
  };

  MoreMenu.addSubmenu = function (menu, name, items, insertAfter) {
    var item = {}; item[name] = items;
    MoreMenu.addItemCore(menu, item, insertAfter);
  };

  MoreMenu.addLink = function (menu, name, url, insertAfter) {
    var item = {}; item[name] = { url: url };
    MoreMenu.addItemCore(menu, item, insertAfter);
  };

  MoreMenu.addSubmenuLink = function (menu, submenu, name, url, insertAfter) {
    var item = {}; item[name] = { url: url };
    MoreMenu.addItemCore(menu, item, insertAfter, submenu);
  };

  init();
});

})();