MediaWiki:Gadget-MarkAdmins.js
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>' +
'<td>' +
'<h4>' + 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>' +
'<td>' +
'<a>' + gObj.localized + '</a>' +
' ' +
'(' + g + ')' +
'</td>' +
'<td>' +
'<label>ラベル</label>' +
'<input>' +
' ' +
'<input>' +
'<label>有効化</label>' +
'</td>' +
'<td>' +
'初期値: ' +
wrapTextBySpan(gDefaultObj.label, null, 'mac-config-defaultlabel') +
' (' + wrapTextBySpan(gDefaultObj.enabled ? '有効' : '無効', null, 'mac-config-defaultenabled') + ')' +
'</td>' +
'<td>' +
'<input>' +
'</td>' +
'</tr>';
return html;
});
return trs.join();
})() +
'<tr>' +
'<td>' +
'<input>' +
'</td>' +
'</tr>' +
createSectionHeader('対象ページ') +
'<tr>' +
'<td>' +
'有効化する名前空間 (「ノートページ上でマークアップ」が無効の場合でもここで指定された名前空間が優先されます)' +
'</td>' +
'<td>' +
'<input>' +
'</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>' +
'<td>' +
'<input>' +
'<label>' + optionLabel + '</label>' +
'</td>' +
'<td>' +
'初期値: ' + wrapTextBySpan(MarkAdmins.defaults[configName] ? '有効' : '無効', null, 'mac-config-defaultenabled') +
'</td>' +
'<td>' +
'<input>' +
'</td>' +
'</tr>';
return html;
})('runOnHistory', '編集履歴上でマークアップ') +
createBooleanOption('runOnTalk', 'ノートページ上でマークアップ') +
createBooleanOption('runOnDiff', '差分上でマークアップ') +
createBooleanOption('markSubpages', 'サブページへのリンクをマークアップ') +
createBooleanOption('markMyself', '自分のリンクをマークアップ') +
createBooleanOption('markContribs', '投稿記録へのリンクをマークアップ') +
'<tr>' +
'<td>' +
'<input>' +
'</td>' +
'</tr>' +
createSectionHeader('保存') +
'<tr>' +
'<td>' +
'<input>' +
'<input>' +
'</td>' +
'</tr>' +
'<tr>' +
'<td></td>' +
'</tr>' +
'<tr>' +
'<td></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} 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.}
* @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.}
* @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({});