MediaWiki:Common.js/calc.js
From Fallen London Wiki (Staging)
Note: After saving, you may have to bypass your browser's cache to see the changes.
- Firefox / Safari: Hold Shift while clicking Reload, or press either Ctrl-F5 or Ctrl-R (⌘-R on a Mac)
- Google Chrome: Press Ctrl-Shift-R (⌘-Shift-R on a Mac)
- Internet Explorer / Edge: Hold Ctrl while clicking Refresh, or press Ctrl-F5
- Opera: Go to Menu → Settings (Opera → Preferences on a Mac) and then to Privacy & security → Clear browsing data → Cached images and files.
/** <nowiki>
*
* Based on: https://runescape.wiki/w/MediaWiki:Gadget-calc-core.js?oldid=35795330
*
* @license GLPv3 <https://www.gnu.org/licenses/gpl-3.0.en.html>
*
*/
/*jshint bitwise:true, browser:true, camelcase:true, curly:true, devel:false,
eqeqeq:true, es3:false, forin:true, immed:true, jquery:true,
latedef:true, newcap:true, noarg:true, noempty:true, nonew:true,
onevar:false, plusplus:false, quotmark:single, undef:true, unused:true,
strict:true, trailing:true
*/
/*global mw, OO */
'use strict';
/**
* Prefix of localStorage key for calc data. This is prepended to the form ID
* localStorage name for autosubmit setting
*/
var calcstorage = 'flw-calcsdata',
calcautostorage = 'flw-calcsdata-autosub',
/**
* Local storage availability.
*/
hasLocalStorage;
/**
* Caching for search suggestions
*
* @todo implement caching for mw.TitleInputWidget accroding to https://doc.wikimedia.org/mediawiki-core/master/js/#!/api/mw.widgets.TitleWidget-cfg-cache
*/
var cache = {},
/**
* Internal variable to store references to each calculator on the page.
*/
calcStore = {},
/**
* Private helper methods for `Calc`
*
* Most methods here are called with `Function.prototype.call`
* and are passed an instance of `Calc` to access it's prototype
*/
helper = {
/**
* Add/change functionality of mw/OO.ui classes
* Added support for multiple namespaces to mw.widgets.TitleInputWidget
*/
initClasses: function () {
var hasOwn = Object.prototype.hasOwnProperty;
/**
* Get option widgets from the server response
* Changed to add support for multiple namespaces
*
* @param {Object} data Query result
* @return {OO.ui.OptionWidget[]} Menu items
*/
mw.widgets.TitleInputWidget.prototype.getOptionsFromData = function (data) {
var i, len, index, pageExists, pageExistsExact, suggestionPage, page, redirect, redirects,
currentPageName = new mw.Title( mw.config.get( 'wgRelevantPageName' ) ).getPrefixedText(),
items = [],
titles = [],
titleObj = mw.Title.newFromText( this.getQueryValue() ),
redirectsTo = {},
pageData = {},
namespaces = this.namespace.split('|').map(function (val) {return parseInt(val,10);});
if ( data.redirects ) {
for ( i = 0, len = data.redirects.length; i < len; i++ ) {
redirect = data.redirects[ i ];
redirectsTo[ redirect.to ] = redirectsTo[ redirect.to ] || [];
redirectsTo[ redirect.to ].push( redirect.from );
}
}
for ( index in data.pages ) {
suggestionPage = data.pages[ index ];
// When excludeCurrentPage is set, don't list the current page unless the user has type the full title
if ( this.excludeCurrentPage && suggestionPage.title === currentPageName && suggestionPage.title !== titleObj.getPrefixedText() ) {
continue;
}
// When excludeDynamicNamespaces is set, ignore all pages with negative namespace
if ( this.excludeDynamicNamespaces && suggestionPage.ns < 0 ) {
continue;
}
pageData[ suggestionPage.title ] = {
known: suggestionPage.known !== undefined,
missing: suggestionPage.missing !== undefined,
redirect: suggestionPage.redirect !== undefined,
disambiguation: OO.getProp( suggestionPage, 'pageprops', 'disambiguation' ) !== undefined,
imageUrl: OO.getProp( suggestionPage, 'thumbnail', 'source' ),
description: suggestionPage.description,
// Sort index
index: suggestionPage.index,
originalData: suggestionPage
};
// Throw away pages from wrong namespaces. This can happen when 'showRedirectTargets' is true
// and we encounter a cross-namespace redirect.
if ( this.namespace === null || namespaces.indexOf(suggestionPage.ns) >= 0 ) {
titles.push( suggestionPage.title );
}
redirects = hasOwn.call( redirectsTo, suggestionPage.title ) ? redirectsTo[ suggestionPage.title ] : [];
for ( i = 0, len = redirects.length; i < len; i++ ) {
pageData[ redirects[ i ] ] = {
missing: false,
known: true,
redirect: true,
disambiguation: false,
description: mw.msg( 'mw-widgets-titleinput-description-redirect', suggestionPage.title ),
// Sort index, just below its target
index: suggestionPage.index + 0.5,
originalData: suggestionPage
};
titles.push( redirects[ i ] );
}
}
titles.sort( function ( a, b ) {
return pageData[ a ].index - pageData[ b ].index;
} );
// If not found, run value through mw.Title to avoid treating a match as a
// mismatch where normalisation would make them matching (T50476)
pageExistsExact = (
hasOwn.call( pageData, this.getQueryValue() ) &&
(
!pageData[ this.getQueryValue() ].missing ||
pageData[ this.getQueryValue() ].known
)
);
pageExists = pageExistsExact || (
titleObj &&
hasOwn.call( pageData, titleObj.getPrefixedText() ) &&
(
!pageData[ titleObj.getPrefixedText() ].missing ||
pageData[ titleObj.getPrefixedText() ].known
)
);
if ( this.cache ) {
this.cache.set( pageData );
}
// Offer the exact text as a suggestion if the page exists
if ( this.addQueryInput && pageExists && !pageExistsExact ) {
titles.unshift( this.getQueryValue() );
}
for ( i = 0, len = titles.length; i < len; i++ ) {
page = hasOwn.call( pageData, titles[ i ] ) ? pageData[ titles[ i ] ] : {};
items.push( this.createOptionWidget( this.getOptionWidgetData( titles[ i ], page ) ) );
}
return items;
};
},
/**
* Parse the calculator configuration
*
* @param lines {Array} An array containing the calculator's configuration
* @returns {Object} An object representing the calculator's configuration
*/
parseConfig: function (lines) {
var defConfig = {
suggestns: [],
autosubmit: 'off',
name: 'Calculator'
},
config = {
// this isn't in `defConfig`
// as it'll get overridden anyway
tParams: []
},
// used for debugging incorrect config names
validParams = [
'form',
'param',
'result',
'suggestns',
'template',
'module',
'modulefunc',
'name',
'autosubmit'
],
// used for debugging incorrect param types
validParamTypes = [
'string',
'article',
'number', 'float',
'int',
'select',
'buttonselect',
'combobox',
'check',
'toggleswitch',
'togglebutton',
'fixed',
'hidden',
'group'
],
configError = false;
// parse the calculator's config
// @example param=arg1|arg1|arg3|arg4
lines.forEach(function (line) {
var temp = line.split('='),
param,
args;
// incorrect config
if (temp.length < 2) {
return;
}
// an equals is used in one of the arguments
// @example HTML label with attributes
// so join them back together to preserve it
// this also allows support of HTML attributes in labels
if (temp.length > 2) {
temp[1] = temp.slice(1,temp.length).join('=');
}
param = temp[0].trim().toLowerCase();
args = temp[1].trim();
if (validParams.indexOf(param) === -1) {
console.warn('Unknown parameter: ' + param);
configError = true;
return;
}
if (param === 'suggestns') {
config.suggestns = args.split(/\s*,\s*/);
return;
}
if (param !== 'param') {
config[param] = args;
return;
}
// split args
args = args.split(/\s*\|\s*/);
// store template params in an array to make life easier
config.tParams = config.tParams || [];
if (validParamTypes.indexOf(args[3]) === -1 && args[3] !== '' && args[3] !== undefined) {
console.warn('Unknown param type: ' + args[3]);
configError = true;
return;
}
if (args[3] === 'float') {
args[3] = 'number';
}
var inlinehelp = false, help = '';
if (args[6]) {
var tmphelp = args[6].split(/\s*=\s*/);
if (tmphelp.length > 1) {
if ( tmphelp[0] === 'inline' ) {
inlinehelp = true;
// Html etc can have = so join them back together
tmphelp[1] = tmphelp.slice(1,tmphelp.length).join('=');
help = helper.sanitiseLabels(tmphelp[1] || '');
} else {
// Html etc can have = so join them back together
tmphelp[0] = tmphelp.join('=');
help = helper.sanitiseLabels(tmphelp[0] || '');
}
} else {
help = helper.sanitiseLabels(tmphelp[0] || '');
}
}
config.tParams.push({
name: mw.html.escape(args[0]),
label: helper.sanitiseLabels(args[1] || args[0]),
def: args[2] || '',
type: mw.html.escape(args[3] || ''),
range: args[4] || '',
rawtogs: args[5] || '',
inlhelp: inlinehelp,
help: help
});
});
if (configError) {
config.configError = 'This calculator\'s config contains errors. Please ' +
'check the javascript console for details.';
}
config = $.extend(defConfig, config);
console.log(config);
return config;
},
/**
* Generate a unique id for each input
*
* @param inputId {String} A string representing the id of an input
* @returns {String} A string representing the namespaced/prefixed id of an input
*/
getId: function (inputId) {
return [this.form, this.result, inputId].join('-').replace(/\W/g, '-');
},
/**
* Output an error to the UI
*
* @param error {String} A string representing the error message to be output
*/
showError: function (error) {
$('#' + this.result)
.empty()
.append(
$('<span>')
.addClass('jcError')
.text(error)
);
},
/**
* Toggle the visibility and enabled status of fields/groups
*
* @param item {String} A string representing the current value of the widget
* @param toggles {object} An object representing arrays of items to be toggled keyed by widget values
*/
toggle: function (item, toggles) {
var self = this;
var togitem = function (widget, show) {
var param = self.tParams[ self.indexkeys[widget] ];
if (param.type === 'group') {
param.ooui.toggle(show);
param.ooui.getItems().forEach(function (child) {
if (!!child.setDisabled) {
child.setDisabled(!show);
} else if (!!child.getField && !!child.getField().setDisabled) {
child.getField().setDisabled(!show);
}
});
} else {
param.layout.toggle(show);
if (!!param.ooui.setDisabled) {
param.ooui.setDisabled(!show);
}
}
};
if (toggles[item]) {
toggles[item].on.forEach( function (widget) {
togitem(widget, true);
});
toggles[item].off.forEach( function (widget) {
togitem(widget, false);
});
} else if ( toggles.not0 && !isNaN(parseFloat(item)) && parseFloat(item) !== 0 ) {
toggles.not0.on.forEach( function (widget) {
togitem(widget, true);
});
toggles.not0.off.forEach( function (widget) {
togitem(widget, false);
});
} else if (toggles.alltogs) {
toggles.alltogs.off.forEach( function (widget) {
togitem(widget, false);
});
}
},
/**
* Generate range and step for number and int inputs
*
* @param rawdata {string} The string representation of the range and steps
* @param type {string} The name of the field type (int or number)
* @returns {array} An array containing the min value, max value, step and button step.
*/
genRange: function (rawdata,type) {
var tmp = rawdata.split(/\s*,\s*/),
rng = tmp[0].split(/\s*-\s*/),
step = tmp[1] || '',
bstep = tmp[2] || '',
min, max,
parseFunc;
if (type==='int') {
parseFunc = function(x) { return parseInt(x, 10); };
} else {
parseFunc = parseFloat;
}
if (type === 'int') {
step = 1;
if ( isNaN(parseInt(bstep,10)) ) {
bstep = 1;
} else {
bstep = parseInt(bstep,10);
}
} else {
if ( isNaN(parseFloat(step)) ) {
step = 0.01;
} else {
step = parseFloat(step);
}
if ( isNaN(parseFloat(bstep)) ) {
bstep = 1;
} else {
bstep = parseFloat(bstep);
}
}
// Accept negative values for either range position
if ( rng.length === 3 ) {
// 1 value is negative
if ( rng[0] === '' ) {
// First value negative
if ( isNaN(parseFunc(rng[1])) ) {
min = -Infinity;
} else {
min = 0 - parseFunc(rng[1]);
}
if ( isNaN(parseFunc(rng[2])) ) {
max = Infinity;
} else {
max = parseFunc(rng[2]);
}
} else if ( rng[1] === '' ) {
// Second value negative
if ( isNaN(parseFunc(rng[0])) ) {
min = -Infinity;
} else {
min = parseFunc(rng[0]);
}
if ( isNaN(parseFunc(rng[2])) ) {
max = 0;
} else {
max = 0 - parseFunc(rng[2]);
}
}
} else if ( rng.length === 4 ) {
// Both negative
if ( isNaN(parseFunc(rng[1])) ) {
min = -Infinity;
} else {
min = 0 - parseFunc(rng[1]);
}
if ( isNaN(parseFunc(rng[3])) ) {
max = 0;
} else {
max = 0 - parseFunc(rng[3]);
}
} else {
// No negatives
if ( isNaN(parseFunc(rng[0])) ) {
min = 0;
} else {
min = parseFunc(rng[0]);
}
if ( isNaN(parseFunc(rng[1])) ) {
max = Infinity;
} else {
max = parseFunc(rng[1]);
}
}
// Check min < max
if ( max < min ) {
return [ max, min, step, bstep ];
} else {
return [ min, max, step, bstep ];
}
},
/**
* Parse the toggles for an input
*
* @param rawdata {string} A string representing the toggles for the widget
* @param defkey {string} The default key for toggles
* @returns {object} An object representing the toggles in the format { ['widget value']:[ widget-to-toggle, group-to-toggle, widget-to-toggle2 ] }
*/
parseToggles: function (rawdata,defkey) {
var tmptogs = rawdata.split(/\s*;\s*/),
allkeys = [], allvals = [],
toggles = {};
if (tmptogs.length > 0 && tmptogs[0].length > 0) {
tmptogs.forEach(function (tog) {
var tmp = tog.split(/\s*=\s*/),
keys = tmp[0],
val = [];
if (tmp.length < 2) {
keys = [defkey];
val = tmp[0].split(/\s*,\s*/);
} else {
keys = tmp[0].split(/\s*,\s*/);
val = tmp[1].split(/\s*,\s*/);
}
if (keys.length === 1) {
var key = keys[0];
toggles[key] = {};
toggles[key].on = val;
allkeys.push(key);
} else {
keys.forEach( function (key) {
toggles[key] = {};
toggles[key].on = val;
allkeys.push(key);
});
}
allvals = allvals.concat(val);
});
allkeys = allkeys.filter(function (item, pos, arr) {
return arr.indexOf(item) === pos;
});
allkeys.forEach(function (key) {
toggles[key].off = allvals.filter(function (val) {
if ( toggles[key].on.includes(val) ) {
return false;
} else {
return true;
}
});
});
// Add all items to default
toggles.alltogs = {};
toggles.alltogs.off = allvals;
}
return toggles;
},
/**
* Form submission handler
*/
submitForm: function () {
var self = this,
code = '{{' + self.template,
formErrors = [],
apicalls = [],
paramVals = {};
if (self.module !== undefined) {
if (self.modulefunc === undefined) {
self.modulefunc = 'main';
}
code = '{{#invoke:'+self.module+'|'+self.modulefunc;
}
self.submitlayout.setNotices(['Validating fields, please wait.']);
self.submitlayout.fieldWidget.setDisabled(true);
// setup template for submission
self.tParams.forEach(function (param) {
if ( param.type === 'hidden' || (param.type !== 'group' && param.ooui.isDisabled() === false) ) {
var val,
$input,
// use separate error tracking for each input
// or every input gets flagged as an error
error = '';
if (param.type === 'fixed' || param.type === 'hidden') {
val = param.def;
} else {
$input = $('#' + helper.getId.call(self, param.name) + ' input');
if (param.type === 'buttonselect') {
val = param.ooui.findSelectedItem();
if (val !== null) {
val = val.getData();
}
} else {
val = param.ooui.getValue();
}
if (param.type === 'int') {
val = val.split(',').join('');
} else if (param.type === 'check') {
val = param.ooui.isSelected();
if (param.range) {
var opts;
if (param.range.match(/;/) !== null) {
opts = param.range.split(';');
} else {
opts = param.range.split(',');
}
val = opts[val ? 0 : 1];
}
} else if (param.type === 'toggleswitch' || param.type === 'togglebutton') {
if (param.range) {
var opts;
if (param.range.match(/;/) !== null) {
opts = param.range.split(';');
} else {
opts = param.range.split(',');
}
val = opts[val ? 0 : 1];
}
}
// Check input is valid (based on widgets validation)
if ( !!param.ooui.hasFlag && param.ooui.hasFlag('invalid') && param.type !== 'article') {
error = param.error;
} else if ( param.type === 'article' && param.ooui.validateTitle && val.length > 0 ) {
var api = param.ooui.getApi(),
prms = {
action: 'query',
prop: [],
titles: [ param.ooui.getValue() ]
};
var prom = new Promise ( function (resolve,reject) {
api.get(prms).then( function (ret) {
if ( ret.query.pages && Object.keys(ret.query.pages).length ) {
var nspaces = param.ooui.namespace.split('|'), allNS = false;
if (nspaces.indexOf('*') >= 0) {
allNS = true;
}
nspaces = nspaces.map(function (ns) {return parseInt(ns,10);});
for (var pgID in ret.query.pages) {
if ( ret.query.pages.hasOwnProperty(pgID) && ret.query.pages[pgID].missing!== '' ) {
if ( allNS ) {
resolve();
}
if ( ret.query.pages[pgID].ns !== undefined && nspaces.indexOf(ret.query.pages[pgID].ns) >= 0 ) {
resolve();
}
}
}
reject(param);
} else {
reject(param);
}
});
});
apicalls.push(prom);
}
if (error) {
param.layout.setErrors([error]);
if (param.ooui.setValidityFlag !== undefined) {
param.ooui.setValidityFlag(false);
}
// TODO: Remove jsInvalid classes?
$input.addClass('jcInvalid');
formErrors.push( param.label[0].textContent + ': ' + error );
} else {
param.layout.setErrors([]);
if (param.ooui.setValidityFlag !== undefined) {
param.ooui.setValidityFlag(true);
}
// TODO: Remove jsInvalid classes?
$input.removeClass('jcInvalid');
// Save current parameter value
paramVals[param.name] = val;
// Save current parameter value for later calculator usage.
//window.localStorage.setItem(helper.getId.call(self, param.name), val);
}
}
code += '|' + param.name + '=' + val;
}
});
Promise.all(apicalls).then( function (vals) {
// All article fields valid
self.submitlayout.setNotices([]);
self.submitlayout.fieldWidget.setDisabled(false);
if (formErrors.length > 0) {
self.submitlayout.setErrors(formErrors);
helper.showError.call(self, 'One or more fields contains an invalid value.');
return;
}
self.submitlayout.setErrors([]);
if (!hasLocalStorage) {
console.warn('Browser does not support localStorage, inputs will not be saved.');
} else {
console.log('Saving inputs to localStorage');
paramVals.autosubmit = !!self.autosubmit;
localStorage.setItem(self.localname, JSON.stringify(paramVals));
localStorage.setItem(self.localauto, paramVals.autosubmit);
}
code += '}}';
console.log(code);
helper.loadTemplate.call(self, code);
}, function (errparam) {
// An article field is invalid
self.submitlayout.setNotices([]);
self.submitlayout.fieldWidget.setDisabled(false);
errparam.layout.setErrors([errparam.error]);
formErrors.push( errparam.label[0].textContent + ': ' + errparam.error );
self.submitlayout.setErrors(formErrors);
helper.showError.call(self, 'One or more fields contains an invalid value.');
return;
});
},
/**
* Parse the template used to display the result of the form
*
* @param code {string} Wikitext to send to the API for parsing
*/
loadTemplate: function (code) {
var self = this,
params = {
action: 'parse',
text: code,
prop: 'text',
title: mw.config.get('wgPageName'),
disablelimitreport: 'true',
contentmodel: 'wikitext',
format: 'json'
};
$('#' + self.form + ' .jcSubmit')
.data('oouiButton')
.setDisabled(true);
// @todo time how long these calls take
$.post('/w/api.php', params)
.done(function (response) {
var html = response.parse.text['*'];
helper.dispResult.call(self, html);
})
.fail(function (_, error) {
$('#' + self.form + ' .jcSubmit')
.data('oouiButton')
.setDisabled(false);
helper.showError.call(self, error);
});
},
/**
* Display the calculator result on the page
*
* @param response {String} A string representing the HTML to be added to the page
*/
dispResult: function (html) {
var self = this;
$('#' + self.form + ' .jcSubmit')
.data('oouiButton')
.setDisabled(false);
$('#bodyContent, #WikiaArticle')
.find('#' + this.result)
.empty()
.removeClass('jcError')
.html(html);
mw.loader.using('jquery.tablesorter', function () {
$('table.sortable:not(.jquery-tablesorter)').tablesorter();
});
mw.loader.using('jquery.makeCollapsible', function () {
$('.mw-collapsible').makeCollapsible();
});
},
/**
* Sanitise any HTML used in labels
*
* @param html {string} A HTML string to be sanitised
* @returns {jQuery.object} A jQuery object representing the sanitised HTML
*/
sanitiseLabels: function (html) {
var whitelistAttrs = [
// mainly for span/div tags
'style',
// for anchor tags
'href',
'title',
// for img tags
'src',
'alt',
'height',
'width',
// misc
'class'
],
whitelistTags = [
'a',
'span',
'div',
'img',
'strong',
'b',
'em',
'i',
'br'
],
// parse the HTML string, removing script tags at the same time
$html = $.parseHTML(html, /* document */ null, /* keepscripts */ false),
// append to a div so we can navigate the node tree
$div = $('<div>').append($html);
$div.find('*').each(function () {
var $this = $(this),
tagname = $this.prop('tagName').toLowerCase(),
attrs,
array,
href;
if (whitelistTags.indexOf(tagname) === -1) {
console.warn('Disallowed tagname: ' + tagname);
$this.remove();
return;
}
attrs = $this.prop('attributes');
array = Array.prototype.slice.call(attrs);
array.forEach(function (attr) {
if (whitelistAttrs.indexOf(attr.name) === -1) {
console.warn('Disallowed attribute: ' + attr.name + ', tagname: ' + tagname);
$this.removeAttr(attr.name);
return;
}
// make sure there's nasty in nothing in href attributes
if (attr.name === 'href') {
href = $this.attr('href');
if (
// disable warnings about script URLs
// jshint -W107
href.indexOf('javascript:') > -1 ||
// the mw sanitizer doesn't like these
// so lets follow suit
// apparently it's something microsoft dreamed up
href.indexOf('vbscript:') > -1
// jshint +W107
) {
console.warn('Script URL detected in ' + tagname);
$this.removeAttr('href');
}
}
});
});
return $div.contents();
},
/**
* Custom handler to check the validity of int and number inputs because the default
* OOUI validator is bugged.
*
* @param value {string} Optional value to check
* @returns {boolean}
*/
checkNumberValidity: function(value) {
var self = this.tParam !== undefined ? this.tParam : this;
value = value !== undefined ? value : self.value;
var n = + value;
if (value === '' && self.isRequired) {
return !self.isRequired();
}
if (isNaN(n) || !isFinite(n)) {
return false;
}
if (self.step && Math.abs(Math.round(n / self.step) - (n / self.step)) > 1e-12) {
return false;
}
if (n < self.min || n > self.max) {
return false;
}
return true;
},
/**
* Handlers for parameter input types
*/
tParams: {
/**
* Handler for 'fixed' inputs
*
* @param param {object} An object containing the configuration of a parameter
* @returns {OOUI.object} A OOUI object containing the new FieldLayout
*/
fixed: function (param) {
var layconf = {
label: new OO.ui.HtmlSnippet(param.label),
align: 'right',
classes: ['jsCalc-field', 'jsCalc-field-fixed'],
value: param.def
};
if (param.help) {
layconf.helpInline = param.inlhelp;
layconf.help = new OO.ui.HtmlSnippet(param.help);
}
param.ooui = new OO.ui.LabelWidget({ label: param.def });
return new OO.ui.FieldLayout(param.ooui, layconf);
},
/**
* Handler for select dropdowns
*
* @param param {object} An object containing the configuration of a parameter
* @param id {String} A string representing the id to be added to the input
* @returns {OOUI.object} A OOUI object containing the new FieldLayout
*/
select: function (param, id) {
var self = this,
conf = {
label: 'Select an option',
options: [],
name: id,
id: id,
value: param.def
},
layconf = {
label: new OO.ui.HtmlSnippet(param.label),
align: 'right',
classes: ['jsCalc-field', 'jsCalc-field-select']
};
var opts;
if (param.range.match(/;/) !== null) {
opts = param.range.split(';');
} else {
opts = param.range.split(',');
}
var def = opts[0];
param.error = 'Not a valid selection';
if (param.help) {
layconf.helpInline = param.inlhelp;
layconf.help = new OO.ui.HtmlSnippet(param.help);
}
opts.forEach(function (opt) {
var op = { data: opt, label: opt };
if (opt === param.def) {
op.selected = true;
def = opt;
}
conf.options.push(op);
});
param.toggles = helper.parseToggles(param.rawtogs, def);
param.ooui = new OO.ui.DropdownInputWidget(conf);
if ( Object.keys(param.toggles).length > 0 ) {
param.ooui.on('change', function (value) {
helper.toggle.call(self, value, param.toggles);
});
}
return new OO.ui.FieldLayout(param.ooui, layconf);
},
/**
* Handler for button selects
*
* @param param {object} An object containing the configuration of a parameter
* @param id {String} A string representing the id to be added to the input
* @returns {OOUI.object} A OOUI object containing the new FieldLayout
*/
buttonselect: function (param, id) {
var self = this,
buttons = {},
conf = {
label:'Select an option',
items: [],
id: id
},
layconf = {
label: new OO.ui.HtmlSnippet(param.label),
align: 'right',
classes: ['jsCalc-field', 'jsCalc-field-buttonselect']
},
def;
var opts;
if (param.range.match(/;/) !== null) {
opts = param.range.split(';');
} else {
opts = param.range.split(',');
}
param.error = 'Please select a valid option';
if (param.help) {
layconf.helpInline = param.inlhelp;
layconf.help = new OO.ui.HtmlSnippet(param.help);
}
opts.forEach(function (opt) {
var opid = opt.replace(/[^a-zA-Z0-9]/g, '');
buttons[opid] = new OO.ui.ButtonOptionWidget({data:opt, label:opt, title:opt});
conf.items.push(buttons[opid]);
});
if (param.def.length > 0 && opts.indexOf(param.def) > -1) {
def = param.def;
} else {
def = opts[0];
}
param.toggles = helper.parseToggles(param.rawtogs, def);
param.ooui = new OO.ui.ButtonSelectWidget(conf);
param.ooui.selectItemByData(def);
if ( Object.keys(param.toggles).length > 0 ) {
param.ooui.on('choose', function (button) {
var item = button.getData();
helper.toggle.call(self, item, param.toggles);
});
}
return new OO.ui.FieldLayout(param.ooui, layconf);
},
/**
* Handler for comboboxes
*
* @param param {object} An object containing the configuration of a parameter
* @param id {String} A string representing the id to be added to the input
* @returns {OOUI.object} A OOUI object containing the new FieldLayout
*/
combobox: function (param, id) {
var self = this,
conf = {
placeholder: 'Start typing to see suggestions',
options: [],
name: id,
id: id,
menu: { filterFromInput: true },
value: param.def
},
layconf = {
label: new OO.ui.HtmlSnippet(param.label),
align: 'right',
classes: ['jsCalc-field', 'jsCalc-field-combobox']
};
var opts;
if (param.range.match(/;/) !== null) {
opts = param.range.split(';');
} else {
opts = param.range.split(',');
}
var def = opts[0];
param.error = 'Not a valid selection';
if (param.help) {
layconf.helpInline = param.inlhelp;
layconf.help = new OO.ui.HtmlSnippet(param.help);
}
var goodDefault = opts.indexOf(param.def) >= 0;
var first = true;
opts.forEach(function (opt) {
var op = { data: opt, label: opt };
if (!goodDefault && first) {
op.selected = true;
conf.value = opt;
}
if (opt === param.def) {
op.selected = true;
def = opt;
}
conf.options.push(op);
first = false;
});
var isvalid = function (val) {return opts.indexOf(val) < 0 ? false : true;};
conf.validate = isvalid;
param.toggles = helper.parseToggles(param.rawtogs, def);
param.ooui = new OO.ui.ComboBoxInputWidget(conf);
if ( Object.keys(param.toggles).length > 0 ) {
param.ooui.on('change', function (value) {
helper.toggle.call(self, value, param.toggles);
});
}
return new OO.ui.FieldLayout(param.ooui, layconf);
},
/**
* Handler for checkbox inputs
*
* @param param {object} An object containing the configuration of a parameter
* @param id {String} A string representing the id to be added to the input
* @returns {OOUI.object} A OOUI object containing the new FieldLayout
*/
check: function (param, id) {
var self = this,
conf = {
name: id,
id: id
},
layconf = {
label: new OO.ui.HtmlSnippet(param.label),
align: 'right',
classes: ['jsCalc-field', 'jsCalc-field-check']
};
param.toggles = helper.parseToggles(param.rawtogs, 'true');
param.error = 'Unknown error';
if (param.help) {
layconf.helpInline = param.inlhelp;
layconf.help = new OO.ui.HtmlSnippet(param.help);
}
var opts;
if (param.range.match(/;/) !== null) {
opts = param.range.split(';');
} else {
opts = param.range.split(',');
}
if ( param.def === 'true' ||
(param.range !== undefined && param.def === opts[0]) ) {
conf.selected = true;
}
param.ooui = new OO.ui.CheckboxInputWidget(conf);
if ( Object.keys(param.toggles).length > 0 ) {
param.ooui.on('change', function (selected) {
if (selected) {
helper.toggle.call(self, 'true', param.toggles);
} else {
helper.toggle.call(self, 'false', param.toggles);
}
});
}
return new OO.ui.FieldLayout(param.ooui, layconf);
},
/**
* Handler for toggle switch inputs
*
* @param param {object} An object containing the configuration of a parameter
* @param id {String} A string representing the id to be added to the input
* @returns {OOUI.object} A OOUI object containing the new FieldLayout
*/
toggleswitch: function (param, id) {
var self = this,
conf = { id: id },
layconf = {
label: new OO.ui.HtmlSnippet(param.label),
align: 'right',
classes: ['jsCalc-field', 'jsCalc-field-toggleswitch']
};
param.toggles = helper.parseToggles(param.rawtogs, 'true');
param.error = 'Unknown error';
if (param.help) {
layconf.helpInline = param.inlhelp;
layconf.help = new OO.ui.HtmlSnippet(param.help);
}
var opts;
if (param.range.match(/;/) !== null) {
opts = param.range.split(';');
} else {
opts = param.range.split(',');
}
if ( param.def === 'true' ||
(param.range !== undefined && param.def === opts[0]) ) {
conf.value = true;
}
param.ooui = new OO.ui.ToggleSwitchWidget(conf);
if ( Object.keys(param.toggles).length > 0 ) {
param.ooui.on('change', function (selected) {
if (selected) {
helper.toggle.call(self, 'true', param.toggles);
} else {
helper.toggle.call(self, 'false', param.toggles);
}
});
}
return new OO.ui.FieldLayout(param.ooui, layconf);
},
/**
* Handler for toggle button inputs
*
* @param param {object} An object containing the configuration of a parameter
* @param id {String} A string representing the id to be added to the input
* @returns {OOUI.object} A OOUI object containing the new FieldLayout
*/
togglebutton: function (param, id) {
var self = this,
conf = {
id: id,
label: new OO.ui.HtmlSnippet(param.label)
},
layconf = {
label:'',
align: 'right',
classes: ['jsCalc-field', 'jsCalc-field-togglebutton']
};
param.toggles = helper.parseToggles(param.rawtogs, 'true');
param.error = 'Unknown error';
if (param.help) {
layconf.helpInline = param.inlhelp;
layconf.help = new OO.ui.HtmlSnippet(param.help);
}
var opts;
if (param.range.match(/;/) !== null) {
opts = param.range.split(';');
} else {
opts = param.range.split(',');
}
if ( param.def === 'true' ||
(param.range !== undefined && param.def === opts[0]) ) {
conf.value = true;
}
param.ooui = new OO.ui.ToggleButtonWidget(conf);
if ( Object.keys(param.toggles).length > 0 ) {
param.ooui.on('change', function (selected) {
if (selected) {
helper.toggle.call(self, 'true', param.toggles);
} else {
helper.toggle.call(self, 'false', param.toggles);
}
});
}
return new OO.ui.FieldLayout(param.ooui, layconf);
},
/**
* Handler for integer inputs
*
* @param param {object} An object containing the configuration of a parameter
* @param id {String} A string representing the id to be added to the input
* @returns {OOUI.object} A OOUI object containing the new FieldLayout
*/
int: function (param, id) {
var self = this,
rng = helper.genRange(param.range, 'int'),
conf = {
min:rng[0],
max:rng[1],
step:rng[2],
showButtons:true,
buttonStep:rng[3],
allowInteger:true,
name: id,
id: id,
value: param.def || 0,
inputFilter: function(val) {
return parseInt(val, 10).toString();
}
},
layconf = {
label: new OO.ui.HtmlSnippet(param.label),
align: 'right',
classes: ['jsCalc-field', 'jsCalc-field-int']
},
error = 'Invalid integer provided. Must be between ' + rng[0] + ' and ' + rng[1];
param.toggles = helper.parseToggles(param.rawtogs, 'not0');
if (param.help) {
layconf.helpInline = param.inlhelp;
layconf.help = new OO.ui.HtmlSnippet(param.help);
}
if ( rng[2] > 1 ) {
error += ' and a multiple of ' + rng[2];
}
param.error = error;
param.ooui = new OO.ui.NumberInputWidget(conf);
param.ooui.validate = helper.checkNumberValidity;
param.ooui.$input[0].checkValidity = helper.checkNumberValidity;
if ( Object.keys(param.toggles).length > 0 ) {
param.ooui.on('change', function (value) {
helper.toggle.call(self, value, param.toggles);
});
}
return new OO.ui.FieldLayout(param.ooui, layconf);
},
/**
* Handler for number inputs
*
* @param param {object} An object containing the configuration of a parameter
* @param id {String} A string representing the id to be added to the input
* @returns {OOUI.object} A OOUI object containing the new FieldLayout
*/
number: function (param, id) {
var self = this,
rng = helper.genRange(param.range, 'number'),
conf = {
min:rng[0],
max:rng[1],
step:rng[2],
showButtons:true,
buttonStep:rng[3],
name:id,
id:id,
value:param.def || 0,
//Use deafult filter (this filter 0.0 into 0 which is inconvenient when the 1st decimal is 0)
/*inputFilter: function(val) {
return (Math.round(parseFloat(val) * 1e12) / 1e12).toString();
}*/
},
layconf = {
label: new OO.ui.HtmlSnippet(param.label),
align: 'right',
classes: ['jsCalc-field', 'jsCalc-field-number'],
};
param.toggles = helper.parseToggles(param.rawtogs, 'not0');
param.error = 'Invalid number provided. Must be between ' + rng[0] + ' and ' + rng[1] + ' and a multiple of ' + rng[2];
if (param.help) {
layconf.helpInline = param.inlhelp;
layconf.help = new OO.ui.HtmlSnippet(param.help);
}
param.ooui = new OO.ui.NumberInputWidget(conf);
param.ooui.validate = helper.checkNumberValidity;
param.ooui.$input[0].checkValidity = helper.checkNumberValidity;
if ( Object.keys(param.toggles).length > 0 ) {
param.ooui.on('change', function (value) {
helper.toggle.call(self, value, param.toggles);
});
}
return new OO.ui.FieldLayout( param.ooui, layconf);
},
/**
* Handler for article inputs
*
* @param param {object} An object containing the configuration of a parameter
* @param id {String} A string representing the id to be added to the input
* @returns {OOUI.object} A OOUI object containing the new FieldLayout
*/
article: function (param, id) {
var self = this,
conf = {
addQueryInput: false,
excludeCurrentPage: true,
showMissing: false,
showDescriptions: true,
validateTitle: true,
relative: false,
id: id,
name: id,
placeholder: 'Enter page name',
value: param.def
},
layconf = {
label: new OO.ui.HtmlSnippet(param.label),
align:'right',
classes: ['jsCalc-field', 'jsCalc-field-article']
},
validNSnumbers = {
'_*':'All', '_-2':'Media', '_-1':'Special',
_0:'(Main)', _1:'Talk',
_2:'User', _3:'User talk',
_4:'Fallen London Wiki', _5:'Fallen London Wiki talk',
_6:'File', _7:'File talk',
_8:'MediaWiki', _9:'MediaWiki talk',
_10:'Template', _11:'Template talk',
_12:'Help', _13:'Help talk',
_14:'Category', _15:'Category talk',
_102:'Property', _103:'Property talk',
_108:'Concept', _109:'Concept talk',
_110:'Forum',
_112:'smw/schema', _113:'smw/schema talk',
_114:'Rule', _115:'Rule talk',
_274:'Widget', _275:'Widget talk',
_500:'Blog', _501:'Blog talk',
_828:'Module', _829:'Module talk',
_844:'CommentStreams', _845:'CommentStreams Talk',
_1200:'Message Wall', _1201:'Thread', _1202:'Message Wall Greeting',
_2000:'Board',
_2300:'Gadget', _2301:'Gadget talk',
_2302:'Gadget definition', _2303:'Gadget definition talk',
_3000:'Grind'
},
validNSnames = {
all:'*', media:-2, special:-1,
main:0, '(main)':0, talk:1,
user:2, 'user talk':3,
flwiki:4, 'flwiki talk':5,
file:6, 'file talk':7,
mediawiki:8, 'mediawiki talk':9,
template:10, 'template talk':11,
help:12, 'help talk':13,
category:14, 'category talk':15,
property:102, 'property talk':103,
concept:108, 'concept talk':109,
forum:110,
'smw/schema':112, 'smw/schema talk':113,
rule:114, 'rule talk':115,
widget:274, 'widget talk':275,
blog:500, 'blog talk':501,
module:828, 'module talk':829,
comments:844, 'comments talk':845,
'message wall':1200, thread:1201, 'message wall greeting':1202,
board:2000,
gadget:2300, 'gadget talk': 2301,
'gadget definition':2302, 'gadget definition talk':2303,
grind:3000
},
namespaces = '';
if (param.help) {
layconf.helpInline = param.inlhelp;
layconf.help = new OO.ui.HtmlSnippet(param.help);
}
if (param.range && param.range.length > 0) {
var names = param.range.split(/\s*,\s*/),
nsnumbers = [];
names.forEach( function (nmspace) {
nmspace = nmspace.toLowerCase();
if ( validNSnumbers['_'+nmspace] ) {
nsnumbers.push(nmspace);
} else if ( validNSnames[nmspace] ) {
nsnumbers.push( validNSnames[nmspace] );
}
});
if (nsnumbers.length < 1) {
conf.namespace = '0';
namespaces = '(Main) namespace';
} else if (nsnumbers.length < 2) {
conf.namespace = nsnumbers[0];
namespaces = nsnumbers[0] + ' namespace';
} else {
conf.namespace = nsnumbers.join('|');
var nsmap = function (num) {
return validNSnumbers['_'+num];
};
namespaces = nsnumbers.slice(0, -1).map(nsmap).join(', ') + ' or ' + nsnumbers.slice(-1).map(nsmap)[0] + ' namespaces';
}
} else if ( self.suggestns && self.suggestns.length > 0 ) {
var nsnumbers = [];
self.suggestns.forEach( function (nmspace) {
nmspace = nmspace.toLowerCase();
if ( validNSnumbers['_'+nmspace] ) {
nsnumbers.push(nmspace);
} else if ( validNSnames[nmspace] ) {
nsnumbers.push( validNSnames[nmspace] );
}
});
if (nsnumbers.length < 1) {
conf.namespace = '0';
namespaces = '(Main) namespace';
} else if (nsnumbers.length < 2) {
conf.namespace = nsnumbers[0];
namespaces = nsnumbers[0] + ' namespace';
} else {
conf.namespace = nsnumbers.join('|');
var nsmap = function (num) {
return validNSnumbers['_'+num];
};
namespaces = nsnumbers.slice(0, -1).map(nsmap).join(', ') + ' or ' + nsnumbers.slice(-1).map(nsmap)[0] + ' namespaces';
}
} else {
conf.namespace = '0';
namespaces = '(Main) namespace';
}
param.error = 'Invalid page or page is not in ' + namespaces;
param.ooui = new mw.widgets.TitleInputWidget(conf);
return new OO.ui.FieldLayout( param.ooui, layconf);
},
/**
* Handler for group type params
*
* @param param {object} An object containing the configuration of a parameter
* @param id {String} A string representing the id to be added to the input
* @returns {OOUI.object} A OOUI object containing the new FieldLayout
*/
group: function (param, id) {
param.ooui = new OO.ui.HorizontalLayout({id: id, classes: ['jsCalc-group']});
if (param.label !== param.name) {
var label = new OO.ui.LabelWidget({ label: new OO.ui.HtmlSnippet(param.label), classes:['jsCalc-grouplabel'] });
param.ooui.addItems([label]);
}
return param.ooui;
},
/**
* Default handler for inputs
*
* @param param {object} An object containing the configuration of a parameter
* @param id {String} A string representing the id to be added to the input
* @returns {OOUI.object} A OOUI object containing the new FieldLayout
*/
def: function (param, id) {
var layconf = {
label: new OO.ui.HtmlSnippet(param.label),
align: 'right',
classes: ['jsCalc-field', 'jsCalc-field-string'],
value: param.def
};
param.error = 'Unknown error';
if (param.help) {
layconf.helpInline = param.inlhelp;
layconf.help = new OO.ui.HtmlSnippet(param.help);
}
param.ooui = new OO.ui.TextInputWidget({type: 'text', name: id, id: id, value: param.def});
return new OO.ui.FieldLayout(param.ooui, layconf);
}
}
};
/**
* Create an instance of `Calc`
* and parse the config stored in `elem`
*
* @param elem {Element} An Element representing the HTML tag that contains
* the calculator's configuration
*/
function Calc(elem) {
var self = this,
$elem = $(elem),
lines,
config;
// support div tags for config as well as pre
// be aware using div tags relies on wikitext for parsing
// so you can't use anchor or img tags
// use the wikitext equivalent instead
if ($elem.children().length) {
$elem = $elem.children();
lines = $elem.html();
} else {
// .html() causes html characters to be escaped for some reason
// so use .text() instead for <pre> tags
lines = $elem.text();
}
lines = lines.split('\n');
config = helper.parseConfig.call(this, lines);
// Calc name for localstorage, keyed to calc id
var page = mw.config.get('wgPageName') || '???';
this.localname = calcstorage + '-' + page + '-' + config.form;
this.localauto = calcautostorage + '-' + page + '-' + config.form;
if (!hasLocalStorage) {
console.warn('Browser does not support localStorage, will skip loading form values.');
} else {
console.log('Loading previous calculator values');
if (config.autosubmit === 'option') {
self.storedAutosubmit = localStorage.getItem(this.localauto) || false;
}
var calcdata = JSON.parse(localStorage.getItem(this.localname)) || false;
if (calcdata) {
config.tParams.forEach(function(param) {
if (calcdata[param.name] !== undefined && calcdata[param.name] !== null) {
param.def = calcdata[param.name];
}
});
}
console.log(config);
}
// merge config in
$.extend(this, config);
/**
* @todo document
*/
this.getInput = function (id) {
if (id) {
id = helper.getId.call(self, id);
return $('#' + id);
}
return $('#jsForm-' + self.form).find('select, input');
};
}
/**
* Helper function for getting the id of an input
*
* @param id {string} The id of the input as specified by the calculator config.
* @returns {string} The true id of the input with prefixes.
*/
Calc.prototype.getId = function (id) {
var self = this,
inputId = helper.getId.call(self, id);
return inputId;
};
/**
* Build the calculator form
*/
Calc.prototype.setupCalc = function () {
var self = this,
fieldset = new OO.ui.FieldsetLayout({label: self.name, classes: ['jcTable'], id: 'jsForm-'+self.form}),
submitButton, submitButtonAction, autosubmit, paramChangeAction,
groupkeys = {};
// Used to store indexes of elements to toggle them later
self.indexkeys = {};
self.tParams.forEach(function (param, index) {
// can skip any output here as the result is pulled from the
// param default in the config on submission
if (param.type === 'hidden') {
return;
}
var id = helper.getId.call(self, param.name),
method = helper.tParams[param.type] ?
param.type :
'def';
// Generate list of items in group
if (param.type === 'group') {
var fields = param.range.split(/\s*,\s*/);
fields.forEach( function (field) {
groupkeys[mw.html.escape(field)] = index;
});
}
param.layout = helper.tParams[method].call(self, param, id);
// Add to group or form
if ( groupkeys[param.name] || groupkeys[param.name] === 0 ) {
self.tParams[ groupkeys[param.name] ].ooui.addItems([param.layout]);
} else {
fieldset.addItems([param.layout]);
}
// Add item to indexkeys
self.indexkeys[param.name] = index;
});
// Run toggle for each field, check validity
self.tParams.forEach( function (param) {
if (param.toggles && Object.keys(param.toggles).length > 0) {
var val;
if (param.type === 'buttonselect') {
val = param.ooui.findSelectedItem().getData();
} else if (param.type === 'check') {
val = param.ooui.isSelected() ? 'true' : 'false';
} else if (param.type === 'toggleswitch' || param.type === 'togglebutton') {
val = param.ooui.getValue() ? 'true' : 'false';
} else {
val = param.ooui.getValue();
}
helper.toggle.call(self, val, param.toggles);
}
if (param.type === 'number' || param.type === 'int') {
param.ooui.setValidityFlag();
}
});
submitButton = new OO.ui.ButtonInputWidget({ label: 'Submit', flags: ['primary', 'progressive'], classes: ['jcSubmit']});
submitButtonAction = function (){
helper.submitForm.call(self);
};
submitButton.on('click', submitButtonAction);
submitButton.$element.data('oouiButton', submitButton);
self.submitlayout = new OO.ui.FieldLayout(submitButton, {label: ' ', align: 'right', classes: ['jsCalc-field', 'jsCalc-field-submit']});
fieldset.addItems([ self.submitlayout ]);
// Auto-submit
if (self.autosubmit !== 'off') {
if (self.autosubmit === 'option') {
// Add toggle to fieldset
autosubmit = new OO.ui.ToggleSwitchWidget({
value: self.storedAutosubmit || false
});
autosubmit.on('change', function (value) { self.autosubmit = value; });
fieldset.addItems([
new OO.ui.FieldLayout(
autosubmit,
{ label:'Auto-submit', align:'right', classes:['jsCalc-field', 'jsCalc-field-autosubmit'] }
)
]);
self.autosubmit = self.storedAutosubmit || false;
}
if (self.autosubmit === 'on' || self.autosubmit === 'true') {
self.autosubmit = true;
}
// Add event
paramChangeAction = function (widget) {
if (self.autosubmit === true) {
if ( typeof widget.getFlagsa === 'undefined' || !widget.getFlags().includes('invalid')) {
helper.submitForm.call(self);
}
}
};
var timeout;
// We only want one of these pending at once
function timeoutFunc(param) {
clearTimeout(timeout);
timeout = setTimeout(paramChangeAction, 500, param);
}
self.tParams.forEach( function (param) {
if (param.type === 'hidden' || param.type === 'group') {
return;
} else if (param.type === 'buttonselect') {
param.ooui.on('select', timeoutFunc, [param.ooui]);
}
param.ooui.on('change', timeoutFunc, [param.ooui]);
});
}
if (self.configError) {
fieldset.$element.append('<br>', self.configError);
}
$('#bodyContent')
.find('#' + self.form)
.empty()
.append(fieldset.$element);
};
/**
* @todo
*/
function init() {
// Is local storage available?
try {
localStorage.setItem('test', 'test');
localStorage.removeItem('test');
hasLocalStorage = true;
} catch(e) {
hasLocalStorage = false;
}
// Initialises class changes
helper.initClasses();
$('.jcConfig').each(function () {
var c = new Calc(this);
c.setupCalc();
calcStore[c.form] = c;
// if (c.autosubmit === 'true' || c.autosubmit === true) {
// helper.submitForm.call(c);
// }
});
}
if ($('.jcConfig').length) {
mw.loader.using(['oojs-ui-core', 'mediawiki.widgets'], function (){
$(init);
});
}
// </nowiki>