MediaWiki:Gadget-MarkAdmins.js
表示
お知らせ: 保存した後、ブラウザのキャッシュをクリアしてページを再読み込みする必要があります。
多くの Windows や Linux のブラウザ
- Ctrl を押しながら F5 を押す。
Mac における Safari
Mac における Chrome や Firefox
- ⌘ Cmd と ⇧ Shift を押しながら R を押す。
詳細についてはWikipedia:キャッシュを消すをご覧ください。
/**
* Flag administrators and special user group members with a letter
* in parenthesis behind links that go into their user namespace.
* E.g. Didym -> Didym (A)
*
* @license
* @link https://commons.wikimedia.org/wiki/MediaWiki:Gadget-markAdmins.js
*
* @author Euku - 2005, PDD, Littl, Guandalug
* @author Didym - 2014
* @author Rillke <https://blog.rillke.com> - 2014
* @contributor Perhelion - 2017
*
* @author Dragoniez - 2023, radically modified the original
* This modified version differs from the original in three main respects:
* - Does not create a new node in user link anchor tags. It only assigns
* a CSS class to the relevant tags and shows group information as a
* pseudo-element of the class (presumably faster, and $(anchor).text()
* doesn't include group notations).
* - Does not suppport configurations via user common.js. Instead, it
* provides configurations on [[Special:MarkAdminsConfig]].
* - User contribs links are also marked.
*
* @requires MediaWiki:Gadget-MarkAdmins-data.json
* @requires MediaWiki:Gadget-MarkAdmins-updater.js
*/
// <nowiki>
'use strict';
(function(mw, $) {
var MarkAdmins = mw.libs.MarkAdmins = {
images: {
loading: '<img src="//upload.wikimedia.org/wikipedia/commons/4/42/Loading.gif" style="vertical-align: middle; height: 1em; border: 0;">',
check: '<img src="//upload.wikimedia.org/wikipedia/commons/f/fb/Yes_check.svg" style="vertical-align: middle; height: 1em; border: 0;">',
cross: '<img src="//upload.wikimedia.org/wikipedia/commons/a/a2/X_mark.svg" style="vertical-align: middle; height: 1em; border: 0;">'
},
config: {},
defaults: {
groups: {
'sysop': {
label: 'A',
localized: '管理者',
link: 'https://ja-two.iwiki.icu/wiki/Wikipedia:管理者',
enabled: true
},
'suppress': {
label: 'OS',
localized: 'オーバーサイト',
link: 'https://ja-two.iwiki.icu/wiki/Wikipedia:オーバーサイトの方針',
enabled: true
},
'checkuser': {
label: 'CU',
localized: 'チェックユーザー',
link: 'https://ja-two.iwiki.icu/wiki/Wikipedia:チェックユーザーの方針',
enabled: true
},
'bureaucrat': {
label: 'B',
localized: 'ビューロクラット',
link: 'https://ja-two.iwiki.icu/wiki/Wikipedia:ビューロクラット',
enabled: true
},
'abusefilter': {
label: 'FE',
localized: '編集フィルター編集者',
link: 'https://ja-two.iwiki.icu/wiki/Wikipedia:編集フィルター編集者',
enabled: true
},
'interface-admin': {
label: 'IA',
localized: 'インターフェース管理者',
link: 'https://ja-two.iwiki.icu/wiki/Wikipedia:インターフェース管理者',
enabled: true
},
'accountcreator': {
label: 'AC',
localized: 'アカウント作成者',
link: 'https://ja-two.iwiki.icu/wiki/Wikipedia:アカウント作成者',
enabled: true
},
'bot': {
label: 'Bot',
localized: 'ボット',
link: 'https://ja-two.iwiki.icu/wiki/Wikipedia:Bot',
enabled: true
},
'eliminator': {
label: 'E',
localized: '削除者',
link: 'https://ja-two.iwiki.icu/wiki/Wikipedia:削除者',
enabled: true
},
'rollbacker': {
label: 'RB',
localized: '巻き戻し者',
link: 'https://ja-two.iwiki.icu/wiki/Wikipedia:巻き戻し者',
enabled: true
},
'founder': {
label: 'F',
localized: '創設者',
link: 'https://meta.wikimedia.org/wiki/Founder/ja',
enabled: true
},
'steward': {
label: 'S',
localized: 'スチュワード',
link: 'https://meta.wikimedia.org/wiki/Stewards/ja',
enabled: true
},
'ombuds': {
label: 'Omb',
localized: 'オンブズ委員',
link: 'https://meta.wikimedia.org/wiki/Ombuds_commission/ja',
enabled: true
},
'staff': {
label: 'Staff',
localized: 'スタッフ',
link: 'https://wikimediafoundation.org/role/staff-contractors/',
enabled: true
},
'sysadmin': {
label: 'SA',
localized: 'システム管理者',
link: 'https://meta.wikimedia.org/wiki/System_administrators/ja',
enabled: true
},
'global-sysop': {
label: 'GS',
localized: 'グローバル管理者',
link: 'https://meta.wikimedia.org/wiki/Global_sysops/ja',
enabled: true
},
'abusefilter-maintainer': {
label: 'GFE',
localized: '編集フィルター保守員',
link: 'https://meta.wikimedia.org/wiki/Abuse_filter_maintainer/ja',
enabled: true
},
'abusefilter-helper': {
label: 'GFH',
localized: '編集フィルター閲覧者',
link: 'https://meta.wikimedia.org/wiki/Abuse_filter_helpers/ja',
enabled: true
},
'global-interface-editor': {
label: 'GIE',
localized: 'グローバルインターフェース編集者',
link: 'https://meta.wikimedia.org/wiki/Interface_editors/ja',
enabled: true
},
'global-bot': {
label: 'GBot',
localized: 'グローバルボット',
link: 'https://meta.wikimedia.org/wiki/Bot_policy/ja#global',
enabled: true
},
'global-deleter': {
label: 'GE',
localized: 'グローバル削除者',
link: 'https://meta.wikimedia.org/wiki/Global_deleters/ja',
enabled: true
},
'global-rollbacker': {
label: 'GRB',
localized: 'グローバル巻き戻し者',
link: 'https://meta.wikimedia.org/wiki/Global_rollback/ja',
enabled: true
},
'vrt-permissions': {
label: 'VRT',
localized: '問い合わせ対応ボランティアチーム',
link: 'https://meta.wikimedia.org/wiki/Volunteer_Response_Team/ja',
enabled: true
},
'global-renamer': {
label: 'GRN',
localized: 'グローバル利用者名変更者',
link: 'https://meta.wikimedia.org/wiki/Global_renamers/ja',
enabled: true
},
'wmf-officeit': {
label: 'WMF OIT',
localized: 'WMFオフィスIT',
link: 'https://meta.wikimedia.org/wiki/Meta:WMF_Office_IT/ja',
enabled: true
},
'wmf-supportsafety': {
label: 'WMF T&S',
localized: 'WMFサポートと安全班',
link: 'https://meta.wikimedia.org/wiki/Meta:WMF_Support_and_Safety/ja',
enabled: true
}
},
runOn: ['Special', '', 'User', 'User_talk', 'Project', 'File', 'Help', 'Portal', 'プロジェクト'],
runOnOptions: [
{
data: 'Special', // Canonical namespace name
label: '特別' // Corresponding label to show in multi-select
},
{
data: '',
label: '(標準)'
},
{
data: 'User',
label: '利用者'
},
{
data: 'User_talk',
label: '利用者‐会話'
},
{
data: 'Project',
label: 'Wikipedia'
},
{
data: 'Project_talk',
label: 'Wikipedia‐ノート'
},
{
data: 'File',
label: 'ファイル'
},
{
data: 'File_talk',
label: 'ファイル‐ノート'
},
{
data: 'MediaWiki',
label: 'MediaWiki'
},
{
data: 'MediaWiki_talk',
label: 'MediaWiki‐ノート'
},
{
data: 'Template',
label: 'Template'
},
{
data: 'Template_talk',
label: 'Template‐ノート'
},
{
data: 'Help',
label: 'Help'
},
{
data: 'Help_talk',
label: 'Help‐ノート'
},
{
data: 'Category',
label: 'Category'
},
{
data: 'Category_talk',
label: 'Category‐ノート'
},
{
data: 'Portal',
label: 'Portal'
},
{
data: 'Portal‐ノート',
label: 'Portal‐ノート'
},
{
data: 'プロジェクト',
label: 'プロジェクト'
},
{
data: 'プロジェクト‐ノート',
label: 'プロジェクト‐ノート'
},
{
data: 'Module',
label: 'モジュール'
},
{
data: 'Module_talk',
label: 'モジュール‐ノート'
},
],
runOnHistory: true,
runOnTalk: true,
runOnDiff: true,
markSubpages: false,
markMyself: true,
markContribs: true
},
/**
* @type {number}
* @static
* @readonly
*/
nsNumber: mw.config.get('wgNamespaceNumber'),
init: function() {
var isOnConfigPage = MarkAdmins.nsNumber === -1 && /^(MarkAdminsConfig|MAC)$/i.test(mw.config.get('wgTitle'));
var dependencies = ['mediawiki.user', 'mediawiki.util', 'mediawiki.api', 'oojs-ui-widgets'];
if (!isOnConfigPage) dependencies.splice(2, 2);
$.when(mw.loader.using(dependencies), $.ready).then(function() {
MarkAdmins.createStyleTag();
MarkAdmins.loadUpdater();
MarkAdmins.mergeConfig(isOnConfigPage);
});
},
/** @static */
createStyleTag: function() {
var style = document.createElement('style');
style.textContent =
// Main pseudo selector to show user groups
'.adminMark::after {' +
'content: attr(data-groupmarker);' +
'font-weight: bold;' +
'font-size: 85%;' +
'vertical-align: middle;' +
'margin-left: 0.3em;' +
'}' +
// Config page
// Headers
'.mac-config-section-header h4 {' +
'border-bottom: 1px solid #A2A9B1;' +
'margin-top: 0.2em;' +
'}' +
// Border for config body
'#mac-body > div {' +
'border: 1px solid #A2A9B1;' +
'}' +
'#mac-body > div:not(:last-child) {' +
'border-bottom: none;' +
'}' +
// Change background color of table rows
'#mac-config-body table tr:nth-child(odd):not(.mac-config-section-header):not(#mac-config-saving):not(#mac-config-lastsaved) td {' +
'background: #EBD7C9;' +
'}' +
// Right-margin for checkboxes
'#mac-config-body input[type=checkbox] {' +
'margin-right: 0.5em;' +
'}' +
// "Right-margin" for labels that have text fields on their right
'.mac-config-textinputlabel {' +
'display: inline-block;' +
'width: 8ch;' +
'}' +
// Margin for buttons
'#mac-config-body input[type=button] {' +
'margin: 0.5em;' +
'}';
document.head.appendChild(style);
},
/** @static */
loadUpdater: function() {
// The updater can be used only by sysops. See the descriptions in the module.
if (mw.config.get('wgUserGroups').indexOf('sysop') === -1) return;
try {
// @ts-ignore "XX is not a module."
require('./MarkAdmins-updater.js');
}
catch(err) {
$.getScript('https://ja-two.iwiki.icu/w/index.php?title=MediaWiki:Gadget-MarkAdmins-updater.js&action=raw&ctype=text/javascript');
}
},
/**
* @type {string}
* @static
* @readonly
*/
userOptionName: 'userjs-MarkAdminsCfg',
/**
* Stores user group JSON data. This property being null means that the script is loaded externally.
* @type {Object.<string, Array<string>>|null}
*/
usersData: (function() {
try {
// require() works only when loaded via gadgets-definition
return require('./MarkAdmins-data.json');
}
catch(err) {
return null;
}
})(),
mergeConfig: function(isOnConfigPage) {
// Merge new configuration
var MarkAdminsCfg = mw.user.options.get(MarkAdmins.userOptionName);
var cfg = $.extend(
true,
MarkAdmins.config,
MarkAdmins.defaults,
MarkAdminsCfg ? JSON.parse(MarkAdminsCfg) : {}
);
if (isOnConfigPage) {
return MarkAdmins.createConfigPage(cfg);
} else {
MarkAdmins.createPortletLink();
}
// Namespace run conditions
if (!(cfg.runOn.indexOf(mw.config.get('wgCanonicalNamespace')) !== -1 ||
cfg.runOnHistory && mw.config.get('wgAction') === 'history' ||
cfg.runOnTalk && MarkAdmins.nsNumber % 2 ||
cfg.runOnDiff && !!mw.util.getParamValue('diff')) ||
mw.config.get('wgCanonicalSpecialPageName') === 'CentralAuth'
) {
return;
}
// Proceed to markup
if (MarkAdmins.usersData) {
mw.hook('wikipage.content').add(function($content) {
MarkAdmins.markUser($content);
});
} else {
$.get('https://ja-two.iwiki.icu/w/index.php?title=MediaWiki:Gadget-MarkAdmins-data.json&action=raw')
.then(function(usersData) {
if (!usersData) return;
usersData = JSON.parse(usersData);
MarkAdmins.usersData = usersData;
mw.hook('wikipage.content').add(function($content) {
MarkAdmins.markUser($content);
});
}).catch(function(err) {
console.error(err);
});
}
},
createConfigPage: function(cfg) {
document.title = 'MarkAdminsの設定 - Wikipedia';
// Dividing the source html just because deep nests make it hard to see
var createConfigBody = function(tableRowsHtml) {
var html =
'<div id="mac-container">' +
'<div id="mac-body" style="margin: 1em 0.5em 0.5em; padding: 0;">' +
'<div id="mac-config-titlebar" style="padding: 0.3em; background: #FEC493;">' +
'<b>MarkAdmins config</b>' +
'</div>' +
'<div id="mac-config-body" style="padding: 0.2em; background: #FFF0E4;">' +
// <-- ToC
'<table style="width: 100%;">' +
'<tbody>' +
tableRowsHtml +
'</tbody>' +
'</table>' +
'</div>' +
'</div>' +
'</div>';
return html;
};
// Create ToC
var toc = document.createElement('div');
toc.id = 'mac-toc';
toc.style.cssText = 'margin: 0.5em; padding: 0.5em; display: table; background: #F8F9FA; border: 1px solid #A2A9B1;';
var toctitle = document.createElement('div');
toc.appendChild(toctitle);
toctitle.id = 'mac-toctitle';
var toch2 = document.createElement('b');
toctitle.appendChild(toch2);
toch2.appendChild(document.createTextNode('目次'));
var toctoggle = document.createElement('span');
toctitle.appendChild(toctoggle);
toctoggle.id = 'mac-toctoggle';
toctoggle.style.cssText = 'display: inline-block; margin-left: 1em;';
toctoggle.appendChild(document.createTextNode('['));
var toctogglelink = document.createElement('a');
toctoggle.appendChild(toctogglelink);
toctogglelink.href = '#mac-tocshowhide';
toctogglelink.textContent = '隠す';
toctoggle.appendChild(document.createTextNode(']'));
var tocol = document.createElement('ol'); // This is where toc items go in
toc.appendChild(tocol);
if (mw.config.get('skin') !== 'timeless') tocol.style.marginLeft = '2em';
toctogglelink.addEventListener('click', function() { // For show/hide
var $tocol = $(tocol);
$tocol.toggle();
if ($tocol.find(':visible').length) {
toctogglelink.textContent = '隠す';
} else {
toctogglelink.textContent = '表示';
}
}, false);
/**
* @param {string} [text]
* @param {string|null} [id]
* @param {string|null} [clss]
* @param {string|null} [cssText]
* @returns {string}
*/
var wrapTextBySpan = function(text, id, clss, cssText) {
var span = document.createElement('span');
if (text) span.appendChild(document.createTextNode(text));
if (id) span.id = id;
if (clss) span.classList.add(clss);
if (cssText) span.style.cssText = cssText;
return span.outerHTML;
};
var createSectionHeader, headerCnt = 0, createBooleanOption;
var tableRowsHtml =
(createSectionHeader = function(headertext) { // ejs-like notation hard to see? Well, at least easier to grasp the outerHTML!
var id = 'mac-config-section-'+ (++headerCnt);
var header =
'<tr class="mac-config-section-header">' +
'<td colspan="4">' +
'<h4 id="' + id +'">' + headertext + '</h4>' +
'</td>' +
'</tr>';
// For ToC
var li = document.createElement('li');
tocol.appendChild(li);
li.className = 'toclevel-1';
var sectlink = document.createElement('a');
li.appendChild(sectlink);
sectlink.href = '#' + id;
sectlink.appendChild(document.createTextNode(headertext));
return header;
})('利用者グループ') +
(function() {
var trs = Object.keys(cfg.groups).map(function(g, i) {
var gObj = cfg.groups[g];
var gDefaultObj = MarkAdmins.defaults.groups[g];
var checked = gObj.enabled ? ' checked' : '';
var checkboxId = 'mac-config-groupenabled-' + i;
var html =
'<tr class="mac-config-usergroup" data-group="' + g + '" data-label="' + gDefaultObj.label + '" data-enabled="' + gDefaultObj.enabled + '">' +
'<td>' +
'<a href="' + gObj.link + '" target="_blank">' + gObj.localized + '</a>' +
'<br/>' +
'(' + g + ')' +
'</td>' +
'<td>' +
'<label class="mac-config-textinputlabel">ラベル</label>' +
'<input class="mac-config-grouplabel" type="text" value="' + gObj.label + '">' +
'<br/>' +
'<input class="mac-config-groupenabled" id="' + checkboxId + '" type="checkbox"' + checked + '>' +
'<label for="' + checkboxId + '">有効化</label>' +
'</td>' +
'<td>' +
'初期値: ' +
wrapTextBySpan(gDefaultObj.label, null, 'mac-config-defaultlabel') +
' (' + wrapTextBySpan(gDefaultObj.enabled ? '有効' : '無効', null, 'mac-config-defaultenabled') + ')' +
'</td>' +
'<td>' +
'<input class="mac-config-reset-this" type="button" value="初期化">' +
'</td>' +
'</tr>';
return html;
});
return trs.join('');
})() +
'<tr>' +
'<td colspan="4">' +
'<input id="mac-config-usergroup-resetall" class="mac-config-resetall" type="button" value="利用者グループの設定を全て初期化">' +
'</td>' +
'</tr>' +
createSectionHeader('対象ページ') +
'<tr class="mac-config-targetpage">' +
'<td colspan="3" id="mac-config-runonnamespace" style="padding-bottom: 0.5em; padding-right: 0.3em;">' +
'有効化する名前空間 (「ノートページ上でマークアップ」が無効の場合でもここで指定された名前空間が優先されます)' +
'</td>' +
'<td>' +
'<input id="mac-config-runonnamespace-reset" type="button" value="初期化">' +
'</td>' +
'</tr>' +
(createBooleanOption = function(configName, optionLabel) {
var checked;
if (typeof cfg[configName] === 'undefined') {
console.error('MarkAdmins config named "' + configName + '" was not found.');
return '';
} else {
checked = !!cfg[configName] ? ' checked' : '';
}
var html =
'<tr class="mac-config-targetpage" data-enabled="' + MarkAdmins.defaults[configName] + '">' +
'<td colspan="2">' +
'<input id="mac-config-' + configName + '" type="checkbox"' + checked + '>' +
'<label for="mac-config-' + configName + '">' + optionLabel + '</label>' +
'</td>' +
'<td>' +
'初期値: ' + wrapTextBySpan(MarkAdmins.defaults[configName] ? '有効' : '無効', null, 'mac-config-defaultenabled') +
'</td>' +
'<td>' +
'<input class="mac-config-reset-this" type="button" value="初期化">' +
'</td>' +
'</tr>';
return html;
})('runOnHistory', '編集履歴上でマークアップ') +
createBooleanOption('runOnTalk', 'ノートページ上でマークアップ') +
createBooleanOption('runOnDiff', '差分上でマークアップ') +
createBooleanOption('markSubpages', 'サブページへのリンクをマークアップ') +
createBooleanOption('markMyself', '自分のリンクをマークアップ') +
createBooleanOption('markContribs', '投稿記録へのリンクをマークアップ') +
'<tr>' +
'<td colspan="4">' +
'<input id="mac-config-targetpage-resetall" class="mac-config-resetall" type="button" value="対象ページの設定を全て初期化">' +
'</td>' +
'</tr>' +
createSectionHeader('保存') +
'<tr>' +
'<td colspan="4">' +
'<input id="mac-config-save" type="button" value="設定を保存">' +
'<input id="mac-config-resetall-do" type="button" value="全ての設定を初期化">' +
'</td>' +
'</tr>' +
'<tr id="mac-config-saving" style="display: none;">' +
'<td colspan="4"></td>' +
'</tr>' +
'<tr id="mac-config-lastsaved" style="display: none;">' +
'<td colspan="4"></td>' +
'</tr>';
// Replace body content. Easier to just replace mw.util.$content[0].innerHTML, but this would remove #p-cactions etc.
var bodyContent = document.querySelector('.mw-body-content') || mw.util.$content[0];
bodyContent.innerHTML = createConfigBody(tableRowsHtml);
var firstHeading = document.querySelector('.mw-first-heading');
if (firstHeading) { // The innerHTML of .mw-body-content was replaced
firstHeading.textContent = 'MarkAdminsの設定';
} else { // The innerHTML of mw.util.$content[0] was replaced (in this case the heading is gone)
var h1 = document.createElement('h1');
h1.textContent = 'MarkAdminsの設定';
var container = document.getElementById('mac-container');
if (container) container.prepend(h1);
}
// Add ToC
var node;
if ((node = document.getElementById('mac-config-body'))) node.prepend(toc);
// Create multi-select
// @ts-ignore "Cannot find name 'OO'."
var widget = new OO.ui.MenuTagMultiselectWidget({
inputPosition: 'outline',
options: cfg.runOnOptions,
allowedValues: cfg.runOnOptions.map(function(obj) { return obj.data; }),
selected: cfg.runOn
});
widget.$element[0].style.fontSize = '90%';
widget.$element[0].style.width = '100%';
widget.$element[0].style.maxWidth = 'initial';
if ((node = document.getElementById('mac-config-runonnamespace'))) node.appendChild(widget.$element[0]);
// Event listeners
/**
* Highlight default value notations when settings are changed to a non-default value.
* Note that every /<tr> that has child elements to change settings should have custom data attributes
* storing the default value. Use caution when you need to add new config options. For this to work,
* the type attribute for \<input> must always be set, even for a mere textbox (don't forget "type=text"
* for these).
*/
var highlightNonDefaults = function() {
if ($(this).hasClass('oo-ui-inputWidget-input')) return;
var $tr = $(this).closest('tr');
var inputType = $(this).attr('type'); // \<input>s should always have a type attribute, even when it's just a textbox
var defaultValue, $defaultValueSpan;
if (inputType === 'checkbox') {
defaultValue = $tr.attr('data-enabled') === 'true';
$defaultValueSpan = $tr.find('.mac-config-defaultenabled').eq(0);
$defaultValueSpan.css({color: this.checked !== defaultValue ? 'red' : ''});
} else if (inputType === 'text') {
defaultValue = $tr.attr('data-label');
$defaultValueSpan = $tr.find('.mac-config-defaultlabel').eq(0);
$defaultValueSpan.css({color: this.value !== defaultValue ? 'red' : ''});
}
};
$('#mac-config-body table input[type=text]').on('input', highlightNonDefaults);
$('#mac-config-body table input[type=checkbox]').on('change', highlightNonDefaults);
// Restore the default settings for a given row
$('.mac-config-reset-this').on('click', function() {
var $tr = $(this).closest('tr');
$tr.find('input[type=text]').not('.oo-ui-inputWidget-input').eq(0).val($tr.attr('data-label') || 'エラー').trigger('input');
$tr.find('input[type=checkbox]').eq(0).prop('checked', $tr.attr('data-enabled') === 'true').trigger('change');
});
// Restore the default settings for all the user groups
$('#mac-config-usergroup-resetall').on('click', function() {
$('tr.mac-config-usergroup').each(function() {
$(this).find('.mac-config-reset-this').trigger('click');
});
});
// Restore the default settings for the namespaces to run the script on
$('#mac-config-runonnamespace-reset').on('click', function() {
widget.setValue(MarkAdmins.defaults.runOn);
});
// Restore the default settings for all the target page options
$('#mac-config-targetpage-resetall').on('click', function() {
$('#mac-config-runonnamespace-reset').trigger('click');
$('tr.mac-config-targetpage').each(function() {
$(this).find('.mac-config-reset-this').trigger('click');
});
});
// Save config
$('#mac-config-save').on('click', function() {
MarkAdmins.saveConfig(widget);
});
// Restore the default settings for all the options
$('#mac-config-resetall-do').on('click', function() {
$('.mac-config-resetall').each(function() {
$(this).trigger('click');
});
});
// Initialization when the page is loaded
// Initialize default value highlighters
$('#mac-config-body table input').not('[type=button]').each(function() {
var eventType = $(this).attr('type') === 'text' ? 'input' : 'change';
$(this).trigger(eventType);
});
},
saveConfig: function(widget) {
var newCfg = {};
// Get user group configs
newCfg.groups = {};
var labels = [], duplicateLabels = [];
$('.mac-config-usergroup').each(function() {
var group = $(this).attr('data-group');
var label = $(this).find('.mac-config-grouplabel').val();
var enabled = $(this).find('.mac-config-groupenabled').prop('checked');
newCfg.groups[group] = {
label: label,
enabled: enabled
};
if (labels.indexOf(label) === -1) {
labels.push(label);
} else {
if (duplicateLabels.indexOf(label) === -1) {
duplicateLabels.push(label);
}
}
});
// Warn if some labels are duplicates
if (duplicateLabels.length !== 0) {
var msg = '重複したラベルがあります\n\n' + duplicateLabels.join('\n') + '\n\nこのまま保存しますか?';
if (!confirm(msg)) return;
}
// Get namespace config
newCfg.runOn = widget.getValue();
// Get boolean configs
var booleanCfg = ['runOnHistory', 'runOnTalk', 'runOnDiff', 'markSubpages', 'markMyself', 'markContribs'];
booleanCfg.forEach(function(cfgName) {
var $el = $('#mac-config-' + cfgName);
newCfg[cfgName] = $el.prop('checked');
});
// Stringify the config object
var newCfgStr = JSON.stringify(newCfg);
// API call to save the config
var $saving = $('#mac-config-saving');
var $savingTd = $saving.children('td').eq(0);
var $lastsaved = $('#mac-config-lastsaved');
var $lastsavedTd = $lastsaved.children('td').eq(0);
var $toDisable = $('#mac-config-body input');
var msgTimeout;
clearTimeout(msgTimeout); // Prevent saving message overwriting
$saving.css('display', 'block');
$savingTd.prop('innerHTML', '保存しています ' + MarkAdmins.images.loading);
$toDisable.prop('disabled', true);
widget.setDisabled(true);
new mw.Api().saveOption(MarkAdmins.userOptionName, newCfgStr)
.then(function() { // Success
$savingTd.prop('innerHTML', '保存しました ' + MarkAdmins.images.check);
$lastsaved.css('display', 'block');
$lastsavedTd.text('最終保存: ' + new Date().toJSON().split('.')[0]);
mw.user.options.set(MarkAdmins.userOptionName, newCfgStr);
}).catch(function(code, err) { // Failure
mw.log.error(err.error.info);
$savingTd.prop('innerHTML', '保存に失敗しました ' + MarkAdmins.images.cross);
}).then(function() {
$toDisable.prop('disabled', false);
widget.setDisabled(false);
msgTimeout = setTimeout(function() { // Hide the progress message after 5 seconds
$saving.css('display', 'none');
$savingTd.empty();
}, 5000);
});
},
createPortletLink: function() {
mw.util.addPortletLink(
'p-tb',
'/wiki/Special:MarkAdminsConfig',
'MarkAdminsの設定',
't-mac',
'MarkAdminsの設定を変更する'
);
},
/**
* Get all aliases associated with a certain namespace number
* @param {number} nsNum
* @returns {Array<string>} Lowercase, underscores are replaced by spaces
* @static
*/
getNamespaceAliases: function(nsNum) {
var nsIds = mw.config.get('wgNamespaceIds');
return Object.keys(nsIds)
.filter(function(alias) {
return nsIds[alias] === nsNum;
})
.map(function(alias) {
return alias.replace(/_/g, ' ');
});
},
fullPageProcessed: false,
markUser: function($content) {
var cfg = MarkAdmins.config;
// Filter enabled groups (Do it here and not later on each anchor)
/**
* @type {Object.<string, string>}
* @key enabled group name
* @value marker e.g. A
*/
var enabledGroups = Object.keys(cfg.groups)
.filter(function(gName) {
return cfg.groups[gName].enabled;
})
.reduce(function(acc, gName) {
acc[gName] = cfg.groups[gName].label;
return acc;
}, Object.create(null));
if (!MarkAdmins.fullPageProcessed) $content = mw.util.$content || $content;
if (!$content[0]) return;
/** @type {HTMLCollectionOf<HTMLAnchorElement>} */
var rawAnchors = $content[0].getElementsByTagName('a');
/** @type {Array<HTMLAnchorElement>} */
var anchors = Array.prototype.slice.call(rawAnchors);
// Add also the userpage link
var isUserpage = [2, 3].indexOf(MarkAdmins.nsNumber) !== -1;
var nstab;
if (isUserpage && !MarkAdmins.fullPageProcessed &&
(nstab = document.getElementById('ca-nstab-user')) &&
(nstab = nstab.getElementsByTagName('a'))
) {
anchors.push(nstab[0]);
}
MarkAdmins.fullPageProcessed = true;
/**
* @type {Object.<string, string>}
* @key username
* @value marker e.g. (A)
*/
var marker = {};
if (!cfg.markMyself) marker[mw.config.get('wgUserName')] = ''; // marker: { "YourUserName": "" }
// Create regex
var regex = {
article: new RegExp(mw.config.get('wgArticlePath').replace('$1', '([^?]+)')), // /wiki/PAGENAME (query parameters removed)
script: new RegExp('^' + mw.config.get('wgScript') + '\\?title=([^&]+)'), // ^/w/index.php?title=PAGENAME (internal links only)
user: new RegExp('^(?:' + MarkAdmins.getNamespaceAliases(2).join('|') + '):(.+)', 'i'),
usertalk: new RegExp('^(?:' + MarkAdmins.getNamespaceAliases(3).join('|') + '):(.+)', 'i'),
contribs: new RegExp('^(?:' + MarkAdmins.getNamespaceAliases(-1).join('|') + '):(?:投稿記録|contrib(?:ution)?s)\/(.+)', 'i')
};
/**
* @type {Array<{username?: string, pagetype?: string}>}
*/
var previousUsers = [];
var noSubpages = !cfg.markSubpages || !!({ Prefixindex: 1, Allpages: 1 })[mw.config.get('wgCanonicalSpecialPageName')];
anchors.forEach(function(a, i) {
/*
* Temporarily push an empty object as the user information associated with the current anchor
* (This must be done at the very first in the loop; more precisely before any "return")
*/
previousUsers.push({});
// Ignore buttons
if (a.role === 'button') return;
// Ignore anchors embedded in image
var ch;
if ((ch = a.children[0]) && ch.nodeName === 'IMG') return;
// Ignore section links in edit summary
var pr;
if ((pr = a.parentElement) && pr.classList.contains('autocomment')) return;
// Get href
var href = a.getAttribute('href');
if (!href) {
if (isUserpage && a.classList.contains('mw-selflink')) {
href = '/wiki/' + mw.config.get('wgPageName');
} else {
return;
}
}
/*
* It's quite complicated to deal with index.php links. They are used everywhere for meta links like date links on
* history, undo links, and a lot more. This can be a serious issue for this script because, when we're on an admin's
* usertalk history for instance, almost every link has a "index.php?title=User talk:ADMIN&..." href, which means
* all of these will be marked up. However, we do want to look at index.php links because this script also marks up
* contribs links and they can be preceded by user/usertalk links like "user -> usertalk -> contribs", where we want to
* ONLY mark up the first, and user/usertalk links can be redlinks, which are generated oftentimes by index.php.
* Just looking also at index.php links will over-mark, so let's look only at redlinks for index.php. (This automatically
* means that index.php contribs links cannot be dealt with.)
*/
var m;
var isRedlink = a.classList.contains('new');
if ((m = regex.article.exec(href)) || isRedlink && (m = regex.script.exec(href))) {
href = m[1];
} else {
return;
}
href = decodeURIComponent(href).replace(/_/g, ' ');
// Extract non-prefixed title ( /wiki/User talk:Foo/subpage -> Foo/subpage or /wiki/Special:Contributions/Foo -> Foo )
var nonPrefixedTitle, pagetype;
if ((m = regex.user.exec(href))) {
nonPrefixedTitle = m[1];
pagetype = 'user';
} else if ((m = regex.usertalk.exec(href))) {
nonPrefixedTitle = m[1];
pagetype = 'usertalk';
} else if ((m = regex.contribs.exec(href))) {
nonPrefixedTitle = m[1];
pagetype = 'contribs';
} else {
return;
}
// Extract user and check if this is a link to a subpage
var user = nonPrefixedTitle.replace(/[/#].*/, '');
var isSubpage = user !== nonPrefixedTitle;
if (isSubpage && noSubpages) return;
/*
* Don't mark the current anchor if the last anchor was for the same user and of a certain type
* Consective links are basically in the order of "user -> usertalk -> contribs" or "contribs -> usertalk".
* We don't want to mark hierarchically-lower-level consective links.
*/
var prev;
/*
* Check whether this is a consective link that follows a master link, which is the top of the hierarchy.
* "prev" being an empty object always means that the last anchor wasn't marked; hence the current anchor can
* be marked. If "prev" is NOT an empty object, the current anchor can be marked only if it and the last anchor
* are for different users AND the current anchor is not one at a lower level in the hierarchy.
*/
if (!isSubpage && (prev = previousUsers[i - 1]) && !$.isEmptyObject(prev) && prev.username === user) {
if (prev.pagetype === 'user' && pagetype === 'usertalk' ||
prev.pagetype === 'usertalk' && pagetype === 'contribs' ||
prev.pagetype === 'contribs' && pagetype === 'usertalk'
) {
// This anchor won't be marked but the next loop will need this anchor's information
previousUsers[i] = {username: user, pagetype: pagetype}; // Update
return;
}
}
var userM = marker[user];
if (userM === undefined) { // Marker has yet to be created
// User groups of selected user
var usergroups = MarkAdmins.usersData ? MarkAdmins.usersData[user] : null;
if (!usergroups) return;
// Create marker
userM = usergroups
.filter(function(g) {
return typeof enabledGroups[g] !== 'undefined';
})
.map(function(g) {
return enabledGroups[g];
})
.join('/');
if (userM) userM = '(' + userM + ')';
marker[user] = userM;
}
if (!userM) return; // Are there markers at all?
// Check finished, now mark the user
a.dataset.groupmarker = marker[user];
a.classList.add('adminMark');
// Now that the current anchor has been marked, the "prev" object for it should not be empty since it's empty
// only when the anchor isn't marked
previousUsers[i] = {username: user, pagetype: pagetype};
});
}
};
MarkAdmins.init();
// @ts-ignore "Cannot find name 'mediaWiki'."
}(mediaWiki, jQuery));
// </nowiki>