MediaWiki:Gadget-gConfig.js
/**
* gConfig is a handy tool to allow users to modify settings of your gadget, with little hassle on your or their side.
*
* Synopsis:
* - register your gadget with its settings: gConfig.register('lipsum', 'Lorem ipsum gadget', [...])
* - access the configuration: gConfig.get('lipsum', 'setting')
*
* For more examples, see per-function docs below.
*
* See also gConfig.css.
*
* Version: 1.1.1
* Dual-licensed CC-BY-SA 3.0 or newer, GFDL 1.3 or newer
* Author: [[w:pl:User:Matma Rex]], patches: [[w:pl:User:Kaligula]], [[w:pl:User:Peter Bowman]], [[w:pl:User:Nux]], [[w:pl:User:Wargo]]
* See also:
* https://xtools.wmcloud.org/articleinfo/pl.wikipedia.org/MediaWiki:Gadget-gConfig.js?uselang=pl#top-editors
*
* Bugz and pull requests:
* https://github.com/Eccenux/wiki-gConfig
*
* Deployed with love using Wikiploy: [[Wikipedia:Wikiploy]]
*/
(function(mw, $){
mw.loader.using(['mediawiki.cookie', 'mediawiki.api', 'mediawiki.jqueryMsg'], function(){
mw.messages.set({
'gConfig-prefs-page-info': "<p>Na tej stronie możesz zmienić ustawienia włączonych gadżetów.</p><p>Informacje i dokumentacja: [[{{ns:Project}}:Narzędzia/gConfig]].</p>",
'gConfig-prefs-page-title': "Preferencje gadżetów",
'gConfig-prefs-no-gadgets': "Obecnie nie masz włączonych żadnych gadżetów korzystających z gConfiga.",
'gConfig-prefs-save': "Zapisz",
'gConfig-prefs-saving': "Zapisywanie...",
'gConfig-prefs-saved': "Zapisano!",
'gConfig-prefs-invalid-values': "Nieprawidłowe wartości.",
'gConfig-prefs-legacy-setting': "To ustawienie jest w tej chwili wpisane na stałe w jednym z twoich plików .js. Usuń je stamtąd, aby stało się modyfikowalne.",
'gConfig-prefs-not-an-integer': "Podana wartość nie jest liczbą całkowitą.",
'gConfig-prefs-out-of-range-min': "Podana wartość jest mniejsza od minimalnej dozwolonej.",
'gConfig-prefs-out-of-range-max': "Podana wartość jest większa od maksymalnej dozwolonej."
});
// Global gConfig object.
var gConfig = {};
// Data of all managed gadgets and settings.
gConfig.data = {};
// Current values of gadgets' settings.
gConfig.settings = {};
var api = new mw.Api();
// generate internal name for this setting.
// used as input names, storage names...
function internalName(gadget, setting)
{
return 'gconfig-'+gadget+'-'+setting;
}
// parse internal name. returns array of [gadget, setting].
function parseInternalName(name)
{
var match = name.match(/^gconfig-([a-zA-Z0-9_]+)-(.+)$/);
var gadget = match[1], setting = match[2];
return [gadget, setting];
}
var _emptyValue = '__(EMPTY)__';
// saves settings in user prefs
// settings - array of arrays: [gadget, settingName, value]
// calls saveSettingsCallback after every successful request
function saveSettings(settings, callback)
{
var grouped = {};
for(var i=0; i<settings.length; i++) {
var name = internalName(settings[i][0], settings[i][1]);
var value = settings[i][2];
// saving empty values in MW options doesn't seem to work
if (typeof value === 'string' && !value.length) {
value = _emptyValue;
}
grouped['userjs-'+name] = value;
// remove legacy cookies
$.removeCookie(name, {path: '/', secure: true});
}
api.saveOptions(grouped).then(function(){
callback();
});
return true;
}
// reads raw setting from mw.user.options or cookies.
// returns undefined if it's not saved anywhere.
function readRawSetting(gadget, settingName)
{
var name = internalName(gadget, settingName);
var value = mw.user.options.get('userjs-'+name);
if (value === _emptyValue) {
value = '';
}
return value;
}
// validates and canonicalizes setting's values.
// doesn't catch errors raised by validation().
// can throw further errors (for numeric values: not an int, out of range)
function validateAndCanonicalize(value, type, validation)
{
if(type == 'boolean') {
value = (value ? '1' : '');
} else if(type == 'string') {
value = '' + value;
} else if(type == 'integer') {
if(parseInt(value, 10) != parseFloat(value)) throw mw.msg('gConfig-prefs-not-an-integer');
value = parseInt(value, 10);
} else if(type == 'numeric') {
value = parseFloat(value);
}
if(typeof validation == 'function') {
value = validation(value);
} else if($.isArray(validation) && (type == 'integer' || type == 'numeric')) {
var min = validation[0], max = validation[1];
if(value < min) throw mw.msg('gConfig-prefs-out-of-range-min');
if(value > max) throw mw.msg('gConfig-prefs-out-of-range-max');
}
return value;
}
// List of all registered gadgets.
gConfig.registeredGadgets = [];
// Map of internal gadget names => gadget infos (e.g. user-visible gadget names, links).
gConfig.gadgetsInfo = {};
// List of internal names of settings which were loaded using the legacy method.
gConfig.legacySettings = [];
// Register configuration for a new gadget.
//
// * gadget is an internal name, must consist only of ASCII letters, numbers or underscore.
// * gadgetInfo is an object with following keys:
// * name [required]: user-visible name, shown in preferences' headings.
// * descriptionPage: name of gadget's description page.
// * settings is an array of configuration options for this gadget. Each option is an object with the following keys:
// * name [required]: internal name of this setting, not shown anywhere
// * desc [required]: description shown on the prefs page
// * descMode: how to treat the description text. 'plain' (default) for plain text, 'wikitext' for basic wikicode
// parsing (links, text formatting) using jqueryMsg
// * type [required]: boolean / integer / numeric / string, each type is handled differently on the prefs page and validated
// * deflt [required]: default value
// * validation: either an array [min, max] (for numeric/integer types), or a function that performs the validation.
// The function will receive value inputted by user as first (and only) parameter, and to indicate that the value
// is unacceptable must throw an error; the message used will be displayed on the prefs page to the user.
// It may also merely process values - it's return value will be used as the final value for the pref.
// * legacy: intended for migration of old scripts to gConfig. Can be either an array of [object, property] or
// just object, property will be assumed to be the same as setting's name. If object[property] will not be undefined,
// it's value will be taken as the value for this pref and the pref will be marked as legacy and become non-editable.
//
// A lengthy example:
// gConfig.register(
// 'lipsum',
// {
// name: 'Lorem ipsum gadget',
// descriptionPage: 'Project:Lorem ipsum gadget'
// },
// [
// {
// name: 'boolean',
// desc: 'Boolean value.',
// type: 'boolean',
// deflt: true
// }, {
// name: 'integer',
// desc: 'Integral number between 0 and 30.',
// type: 'integer',
// deflt: 20,
// validation: [0, 30]
// }, {
// name: 'float',
// desc: '[[Floating-point number]] between -1 and 1.',
// descMode: 'wikitext',
// type: 'numeric',
// deflt: 0.5,
// validation: [-1, 1]
// }, {
// name: 'string',
// desc: 'Text value.',
// type: 'string',
// deflt: 'test'
// }, {
// name: 'evenonly-passive',
// desc: 'Even numbers only. Will be rounded down if an odd number is given.',
// type: 'integer',
// deflt: 0,
// validation: function(n){ return n%2!=0 ? n-1 : n; }
// }, {
// name: 'evenonly-agressive',
// desc: 'Even numbers only. Will prevent saving if an odd number is given.',
// type: 'integer',
// deflt: 0,
// validation: function(n){ if(n%2!=0){ throw 'Requires an even number!' }; return n; }
// }
// ]
// );
gConfig.register = function(gadget, gadgetInfo, settings)
{
gConfig.data[gadget] = settings;
gConfig.settings[gadget] = {};
for(var i=0; i<settings.length; i++) {
var sett = settings[i];
var value;
// do some basic input validation to prevent nondescriptive errors later
var errorMessage = "missing % in setting #"+i+" for "+gadget;
if(!sett.name) throw errorMessage.replace('%', 'name');
if(!sett.desc) throw errorMessage.replace('%', 'desc');
if(!sett.type) throw errorMessage.replace('%', 'type');
if(sett.deflt == undefined) throw errorMessage.replace('%', 'deflt');
var isLegacy = false;
if(sett.legacy) {
var object, property;
if($.isArray(sett.legacy)) { // [object, 'prop name']
object = sett.legacy[0]; property = sett.legacy[1];
} else { // object, prop name = sett.name
object = sett.legacy; property = sett.name;
}
if(object[property] != undefined) {
try {
value = validateAndCanonicalize(object[property], sett.type, sett.validation);
gConfig.legacySettings.push( internalName(gadget, sett.name) );
isLegacy = true;
// eslint-disable-next-line no-empty
} catch(er) {} // if validation error, ignore this
}
}
if(!isLegacy) {
value = readRawSetting(gadget, sett.name);
if(value == undefined) value = sett.deflt;
value = validateAndCanonicalize(value, sett.type, sett.validation);
}
gConfig.settings[gadget][sett.name] = value;
}
gConfig.registeredGadgets.push(gadget);
gConfig.gadgetsInfo[gadget] = (typeof gadgetInfo == 'string')
? {name: gadgetInfo}
: gadgetInfo;
specialPage();
}
// Return the current value for given setting.
//
// Do note that while integer, numeric and string values will always be of the corresponding JavaScript type,
// boolean values need not be true/false, but merely truthy/falsy.
gConfig.get = function(gadget, setting)
{
return gConfig.settings[gadget][setting];
}
// Set the current value for given setting. It is not validated.
//
// For the value to be actually saved, you need to call gConfig.synchronise().
gConfig.set = function(gadget, setting, value)
{
return gConfig.settings[gadget][setting] = value;
}
var synchroRunning = false;
var synchroDelayedCallbacks = [];
// Asynchronously saves current values of all settings.
gConfig.synchronise = function(callback)
{
// a lot of ugly elaborate code to make sure bad things don't happen
// if synchronise() is called when a synchro is already running.
if(synchroRunning) {
synchroDelayedCallbacks.push(callback);
return;
}
synchroRunning = true;
var meat = function(){
var toSave = [];
for(var i=0; i<gConfig.registeredGadgets.length; i++) {
var gadget = gConfig.registeredGadgets[i];
for(var j=0; j<gConfig.data[gadget].length; j++) {
var setting = gConfig.data[gadget][j].name;
toSave.push([gadget, setting, gConfig.get(gadget, setting)]);
}
}
saveSettings(toSave, function(){
synchroRunning = false;
callback();
if(synchroDelayedCallbacks.length > 0) {
// this means there were calls to synchronise() while we were working.
// we need to synchronise again, then call the callbacks.
var cbs = synchroDelayedCallbacks;
synchroDelayedCallbacks = [];
gConfig.synchronise(function(){
for(var i=0; i<cbs.length; i++) cbs[i]();
})
}
});
}
meat();
}
function inputFor(value, type, validation)
{
var input = null;
if(type == 'boolean') {
input = $('<input type=checkbox>').prop('checked', !!value);
} else if(type == 'string') {
input = $('<input type=text>').prop('value', value);
} else if(type == 'integer' || type == 'numeric') {
input = $('<input type=number>').attr('step', (type == 'integer' ? 1 : 'any'))
if(validation && $.isArray(validation)) {
var min = validation[0], max = validation[1];
input.attr({min: min, max: max});
}
input.prop('value', value);
}
return input;
}
var nowSaving = false;
function specialPage()
{
if(mw.config.get('wgTitle') != "GadgetPrefs" || mw.config.get('wgCanonicalNamespace') != "Special") return false;
var onsubmit = function(e){
if(!nowSaving) {
nowSaving = true;
$content.find('.gconfig-pref-error').empty(); // remove infos about invalid values, if any
var toSave = [];
var errors = [];
var $inputs = $content.find('input');
for(var i=0; i<$inputs.length; i++) {
var input = $inputs[i];
if(input.type == 'submit') continue;
var name = parseInternalName(input.name);
var gadget = name[0], setting = name[1];
var value = (input.type=='checkbox' ? input.checked : input.value);
try {
value = validateAndCanonicalize( value, $(input).data('gconfig-type'), $(input).data('gconfig-validation') );
}
catch(err) {
errors.push([input.name, err]);
continue;
}
toSave.push([gadget, setting, value]);
}
if(errors.length > 0) {
$('#gconfig-save-status').attr('class', 'gconfig-save-error').text( mw.msg('gConfig-prefs-invalid-values') );
// eslint-disable-next-line no-redeclare
for(var i=0; i<errors.length; i++) {
var id = errors[i][0], info = errors[i][1];
$('#'+id).closest('tr').find('.gconfig-pref-error').text(info)
}
nowSaving = false;
}
else {
$('#gconfig-save-status').attr('class', '').text( mw.msg('gConfig-prefs-saving') );
saveSettings(toSave, function(){
nowSaving = false;
$('#gconfig-save-status').attr('class', 'gconfig-save-success').text( mw.msg('gConfig-prefs-saved') );
})
}
}
e.preventDefault();
return false;
}
var $content;
if(gConfig.registeredGadgets.length > 0) {
$content = $('<table>');
for(var i=0; i<gConfig.registeredGadgets.length; i++) {
var gadget = gConfig.registeredGadgets[i];
var gadgetName = (typeof gConfig.gadgetsInfo[gadget].descriptionPage == 'string')
? $('<a>').attr({
href: '/wiki/'+encodeURIComponent(gConfig.gadgetsInfo[gadget].descriptionPage),
title: gConfig.gadgetsInfo[gadget].descriptionPage
}).text(gConfig.gadgetsInfo[gadget].name)
: $( document.createTextNode(gConfig.gadgetsInfo[gadget].name) );
$content.append(
$('<tr>').append(
$('<td>').attr('colspan', 2).append(
$('<h2>').append(gadgetName)
)
)
);
for(var j=0; j<gConfig.data[gadget].length; j++) {
var setting = gConfig.data[gadget][j];
var inputName = internalName(gadget, setting.name);
var $input = inputFor( gConfig.get(gadget, setting.name), setting.type, setting.validation );
$input.attr('name', inputName).attr('id', inputName);
$input.data({ 'gconfig-type': setting.type, 'gconfig-validation': setting.validation });
var isLegacy = !!($.inArray(inputName, gConfig.legacySettings) != -1);
mw.messages.set('gConfig-prefs-settingdesc-'+gadget+'-'+setting.name, setting.desc);
var settingDesc = setting.descMode == 'wikitext'
? mw.message('gConfig-prefs-settingdesc-'+gadget+'-'+setting.name).parseDom()
: $( document.createTextNode(setting.desc) );
$content.append(
$('<tr>').append(
$('<td>').append( $input.prop('disabled', !!isLegacy) ),
$('<td>').append(
$('<p>').addClass('gconfig-pref-label').append( $('<label>').attr('for', inputName).append(settingDesc) ),
$('<p>').addClass('gconfig-pref-legacy-note').text( isLegacy ? mw.msg('gConfig-prefs-legacy-setting') : '' ),
$('<p>').addClass('gconfig-pref-error')
)
)
)
}
}
// save button
$content.append(
$('<tr>').append(
$('<td>'),
$('<td>').append(
$('<input type=submit>').attr('id', 'gconfig-save-button').attr('value', mw.msg('gConfig-prefs-save') ),
$('<p>').attr('id', 'gconfig-save-status')
)
)
)
}
else {
$content = $('<p>').text( mw.msg('gConfig-prefs-no-gadgets') );
}
var $form = $('<form>').attr('id', 'gconfig-form').append( $content );
$form.on('submit', onsubmit);
$form.on('invalid', onsubmit); // we do our own validation - stop the browser from showing its error messages
var $info = mw.message('gConfig-prefs-page-info').parseDom();
document.title = mw.msg('gConfig-prefs-page-title');
$('h1').first().text( mw.msg('gConfig-prefs-page-title') );
$('#mw-content-text').empty().append($info, $form);
return true; // done
}
window.gConfig = gConfig;
// usage: mw.hook('userjs.gConfig.ready').add(function (gConfig) {});
mw.hook('userjs.gConfig.ready').fire(gConfig);
// prepare GadgetPrefs page
if (specialPage()) {
mw.hook('userjs.gConfig.specialPage').fire(gConfig);
}
})
})(mediaWiki, jQuery);
Content Disclaimer
Informasi ini disarikan dari Wikipedia dan disajikan kembali untuk tujuan edukasi. Konten tersedia di bawah lisensi CC BY-SA 3.0. Kami tidak bertanggung jawab atas ketidakakuratan data yang bersumber dari kontribusi publik tersebut.
- The information displayed on this website is sourced in part or in whole from Wikipedia and has been adapted for the purpose of restating it. We strive to provide accurate and relevant information, however:
- There is no guarantee of absolute accuracy. Wikipedia is an open, collaborative project that can be edited by anyone, so information is subject to change.
- It is not intended to constitute professional advice. The content displayed is for informational and educational purposes only. For important decisions (e.g., medical, legal, or financial), please consult a professional.
- Content copyright. Wikipedia is licensed under the Creative Commons Attribution-ShareAlike License (CC BY-SA). This means that content may be reused with appropriate attribution and shared under a similar license.
- Responsible use. Any risk arising from the use of information from this website is entirely the responsibility of the user.