No edit summary |
(Fix not detecting text from all children in a div.jcConfig) |
||
(34 intermediate revisions by 4 users not shown) | |||
Line 35: | Line 35: | ||
*/ |
*/ |
||
− | /*global mediaWiki, hsbwiki */ |
+ | /*global mediaWiki, hsbwiki, importArticle, console */ |
− | window.hsbwiki = window.hsbwiki || {} |
+ | window.hsbwiki = window.hsbwiki || {}; |
− | + | (function ($, mw, hsbwiki, undefined) { |
|
'use strict'; |
'use strict'; |
||
+ | if (hsbwiki.calculatorLoaded) { |
||
⚫ | |||
⚫ | |||
+ | hsbwiki.calculatorLoaded = true; |
||
− | + | /** |
|
− | + | * Caching for search suggestions |
|
− | + | */ |
|
var cache = {}, |
var cache = {}, |
||
Line 75: | Line 79: | ||
// used for debugging incorrect config names |
// used for debugging incorrect config names |
||
validParams = [ |
validParams = [ |
||
⚫ | |||
⚫ | |||
'form', |
'form', |
||
'param', |
'param', |
||
'result', |
'result', |
||
'suggestns', |
'suggestns', |
||
− | 'template' |
+ | 'template', |
⚫ | |||
], |
], |
||
// used for debugging incorrect param types |
// used for debugging incorrect param types |
||
Line 112: | Line 119: | ||
// this also allows support of HTML attributes in labels |
// this also allows support of HTML attributes in labels |
||
if (temp.length > 2) { |
if (temp.length > 2) { |
||
− | temp[1] = temp.slice(1,temp.length).join('='); |
+ | temp[1] = temp.slice(1, temp.length).join('='); |
} |
} |
||
Line 157: | Line 164: | ||
}); |
}); |
||
}); |
}); |
||
+ | |||
⚫ | |||
if (configError) { |
if (configError) { |
||
config.configError = 'This calculator\'s config contains errors. Please report it to an admin or check the javascript console for details.'; |
config.configError = 'This calculator\'s config contains errors. Please report it to an admin or check the javascript console for details.'; |
||
Line 182: | Line 189: | ||
*/ |
*/ |
||
showError: function (error) { |
showError: function (error) { |
||
− | $('#' + this.result) |
+ | $(this.result !== 'inner' ? ('#' + this.result) : ('#' + this.form + ' .jcResult')) |
.empty() |
.empty() |
||
.append( |
.append( |
||
$('<span>') |
$('<span>') |
||
− | + | .addClass('jcError') |
|
− | + | .text(error) |
|
− | ) |
+ | ) |
⚫ | |||
}, |
}, |
||
Line 200: | Line 208: | ||
var self = this; |
var self = this; |
||
var togitem = function (widget, show) { |
var togitem = function (widget, show) { |
||
− | var param = self.tParams[ |
+ | var param = self.tParams[self.indexkeys[widget]]; |
// if (param.type === 'group') { |
// if (param.type === 'group') { |
||
// param.ooui.toggle(show); |
// param.ooui.toggle(show); |
||
Line 211: | Line 219: | ||
// }); |
// }); |
||
// } else |
// } else |
||
− | if ( |
+ | if (param.type === 'semihidden') { |
// if (!!param.ooui.setDisabled) { |
// if (!!param.ooui.setDisabled) { |
||
// param.ooui.setDisabled(!show); |
// param.ooui.setDisabled(!show); |
||
// } |
// } |
||
− | // [ |
+ | // [HSW] Yet another hacky way to get around not having ooui |
param.$ui.prop('disabled', !show); |
param.$ui.prop('disabled', !show); |
||
} else { |
} else { |
||
Line 222: | Line 230: | ||
// param.ooui.setDisabled(!show); |
// param.ooui.setDisabled(!show); |
||
// } |
// } |
||
− | // [ |
+ | // [HSW] Yet another hacky way to get around not having ooui |
param.$ui.closest('tr').toggle(show); |
param.$ui.closest('tr').toggle(show); |
||
param.$ui.prop('disabled', !show); |
param.$ui.prop('disabled', !show); |
||
+ | |||
⚫ | |||
} |
} |
||
}; |
}; |
||
+ | |||
⚫ | |||
if (toggles[item]) { |
if (toggles[item]) { |
||
− | toggles[item].on.forEach( |
+ | toggles[item].on.forEach(function (widget) { |
togitem(widget, true); |
togitem(widget, true); |
||
}); |
}); |
||
− | toggles[item].off.forEach( |
+ | toggles[item].off.forEach(function (widget) { |
togitem(widget, false); |
togitem(widget, false); |
||
}); |
}); |
||
− | } else if ( |
+ | } else if (toggles.not0 && !isNaN(parseFloat(item)) && parseFloat(item) !== 0) { |
− | toggles.not0.on.forEach( |
+ | toggles.not0.on.forEach(function (widget) { |
togitem(widget, true); |
togitem(widget, true); |
||
}); |
}); |
||
− | toggles.not0.off.forEach( |
+ | toggles.not0.off.forEach(function (widget) { |
togitem(widget, false); |
togitem(widget, false); |
||
}); |
}); |
||
} else if (toggles.alltogs) { |
} else if (toggles.alltogs) { |
||
− | toggles.alltogs.off.forEach( |
+ | toggles.alltogs.off.forEach(function (widget) { |
togitem(widget, false); |
togitem(widget, false); |
||
}); |
}); |
||
Line 294: | Line 302: | ||
* @returns {object} An object representing the toggles in the format { ['widget value']:[ widget-to-toggle, group-to-toggle, widget-to-toggle2 ] } |
* @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) { |
+ | parseToggles: function (rawdata, defkey) { |
var tmptogs = rawdata.split(/\s*;\s*/), |
var tmptogs = rawdata.split(/\s*;\s*/), |
||
− | allkeys |
+ | allkeys = [], |
⚫ | |||
toggles = {}; |
toggles = {}; |
||
+ | |||
⚫ | |||
if (tmptogs.length > 0 && tmptogs[0].length > 0) { |
if (tmptogs.length > 0 && tmptogs[0].length > 0) { |
||
tmptogs.forEach(function (tog) { |
tmptogs.forEach(function (tog) { |
||
Line 317: | Line 326: | ||
allkeys.push(key); |
allkeys.push(key); |
||
} else { |
} else { |
||
− | keys.forEach( |
+ | keys.forEach(function (key) { |
toggles[key] = {}; |
toggles[key] = {}; |
||
toggles[key].on = val; |
toggles[key].on = val; |
||
Line 325: | Line 334: | ||
allvals = allvals.concat(val); |
allvals = allvals.concat(val); |
||
}); |
}); |
||
+ | |||
⚫ | |||
allkeys = allkeys.filter(function (item, pos, arr) { |
allkeys = allkeys.filter(function (item, pos, arr) { |
||
return arr.indexOf(item) === pos; |
return arr.indexOf(item) === pos; |
||
}); |
}); |
||
+ | |||
− | |||
allkeys.forEach(function (key) { |
allkeys.forEach(function (key) { |
||
toggles[key].off = allvals.filter(function (val) { |
toggles[key].off = allvals.filter(function (val) { |
||
− | if ( |
+ | if (toggles[key].on.includes(val)) { |
return false; |
return false; |
||
} else { |
} else { |
||
Line 339: | Line 348: | ||
}); |
}); |
||
}); |
}); |
||
+ | |||
− | |||
// Add all items to default |
// Add all items to default |
||
toggles.alltogs = {}; |
toggles.alltogs = {}; |
||
toggles.alltogs.off = allvals; |
toggles.alltogs.off = allvals; |
||
} |
} |
||
+ | |||
− | |||
return toggles; |
return toggles; |
||
}, |
}, |
||
Line 353: | Line 362: | ||
submitForm: function () { |
submitForm: function () { |
||
var self = this, |
var self = this, |
||
− | code = '{{' + self.template, |
+ | code = '{{' + self.template + (self.argslist ? '|' + self.argslist : ''), |
formError = false; |
formError = false; |
||
Line 409: | Line 418: | ||
} |
} |
||
} |
} |
||
+ | |||
⚫ | |||
code += '|' + param.name + '=' + val; |
code += '|' + param.name + '=' + val; |
||
}); |
}); |
||
Line 434: | Line 443: | ||
text: code, |
text: code, |
||
prop: 'text', |
prop: 'text', |
||
− | + | contentmodel: 'wikitext', |
|
− | + | disablelimitreport: 'true' |
|
}; |
}; |
||
+ | |||
⚫ | |||
// experimental support for using VE to parse calc templates |
// experimental support for using VE to parse calc templates |
||
if (!!mw.util.getParamValue('vecalc')) { |
if (!!mw.util.getParamValue('vecalc')) { |
||
Line 449: | Line 458: | ||
} |
} |
||
− | $('#' + self.form + ' .jcSubmit |
+ | $('#' + self.form + ' .jcSubmit div') |
− | + | .text('..').css('color', 'gray'); |
|
− | .prop('disabled', true); |
||
// @todo time how long these calls take |
// @todo time how long these calls take |
||
(new mw.Api()) |
(new mw.Api()) |
||
− | + | .post(params) |
|
.done(function (response) { |
.done(function (response) { |
||
var html; |
var html; |
||
+ | |||
⚫ | |||
if (!!mw.util.getParamValue('vecalc')) { |
if (!!mw.util.getParamValue('vecalc')) { |
||
// strip body tag |
// strip body tag |
||
Line 465: | Line 473: | ||
html = response.parse.text['*']; |
html = response.parse.text['*']; |
||
} |
} |
||
+ | |||
⚫ | |||
helper.dispResult.call(self, html); |
helper.dispResult.call(self, html); |
||
}) |
}) |
||
.fail(function (_, error) { |
.fail(function (_, error) { |
||
− | $('#' + self.form + ' .jcSubmit |
+ | $('#' + self.form + ' .jcSubmit div') |
− | + | .text('↵').css('color', 'white'); |
|
⚫ | |||
helper.showError.call(self, error); |
helper.showError.call(self, error); |
||
}); |
}); |
||
Line 482: | Line 489: | ||
*/ |
*/ |
||
dispResult: function (html) { |
dispResult: function (html) { |
||
− | $('#' + this.form + ' .jcSubmit |
+ | $('#' + this.form + ' .jcSubmit div') |
− | + | .text('↵').css('color', 'white'); |
|
⚫ | |||
$('#bodyContent, #WikiaArticle, .WikiaArticle, #content, .page-content') |
$('#bodyContent, #WikiaArticle, .WikiaArticle, #content, .page-content') |
||
− | .find('#' + this.result) |
+ | .find(this.result !== 'inner' ? ('#' + this.result) : ('#' + this.form + ' .jcResult')) |
− | + | .empty() |
|
− | + | .removeClass('jcError') |
|
− | + | .html(html) |
|
− | + | .show(); |
|
+ | |||
// allow scripts to hook into form submission |
// allow scripts to hook into form submission |
||
mw.hook('rscalc.submit').fire(); |
mw.hook('rscalc.submit').fire(); |
||
Line 601: | Line 608: | ||
fixed: function ($td, param) { |
fixed: function ($td, param) { |
||
$td.text(param.def); |
$td.text(param.def); |
||
− | param.$ui = $td; // [ |
+ | param.$ui = $td; // [HSW] custom way of doing much less involved "param.ooui" |
return $td; |
return $td; |
||
}, |
}, |
||
Line 617: | Line 624: | ||
var self = this, |
var self = this, |
||
$select = $('<select>') |
$select = $('<select>') |
||
− | + | .attr({ |
|
− | + | name: id, |
|
− | + | id: id |
|
− | + | }), |
|
opts = param.range.split(/\s*,\s*/), |
opts = param.range.split(/\s*,\s*/), |
||
def = opts[0]; |
def = opts[0]; |
||
Line 628: | Line 635: | ||
// and defer the escaping to $.fn.val and $.fn.text instead |
// and defer the escaping to $.fn.val and $.fn.text instead |
||
opt = opt.replace(/>/g, '>') |
opt = opt.replace(/>/g, '>') |
||
− | + | .replace(/</g, '<') |
|
− | + | .replace(/&/g, '&') |
|
− | + | .replace(/"/g, '"') |
|
− | + | .replace(/'/g, '\''); |
|
var $option = $('<option>') |
var $option = $('<option>') |
||
− | + | .val(opt) |
|
− | + | .text(opt); |
|
if (opt === param.def) { |
if (opt === param.def) { |
||
Line 645: | Line 652: | ||
param.toggles = helper.parseToggles(param.rawtogs, def); |
param.toggles = helper.parseToggles(param.rawtogs, def); |
||
+ | |||
− | |||
− | if ( |
+ | if (Object.keys(param.toggles).length > 0) { |
− | $select.on('change', function(){ |
+ | $select.on('change', function () { |
helper.toggle.call(self, $(this).val(), param.toggles); |
helper.toggle.call(self, $(this).val(), param.toggles); |
||
}); |
}); |
||
} |
} |
||
− | param.$ui = $select; // [ |
+ | param.$ui = $select; // [HSW] custom way of doing much less involved "param.ooui" |
$td.append($select); |
$td.append($select); |
||
return $td; |
return $td; |
||
Line 668: | Line 675: | ||
check: function ($td, param, id) { |
check: function ($td, param, id) { |
||
var self = this, |
var self = this, |
||
− | + | $input = $('<input>') |
|
− | + | .attr({ |
|
− | + | type: 'checkbox', |
|
− | + | name: id, |
|
− | + | id: id |
|
− | + | }); |
|
param.toggles = helper.parseToggles(param.rawtogs, 'true'); |
param.toggles = helper.parseToggles(param.rawtogs, 'true'); |
||
Line 682: | Line 689: | ||
$input.prop('checked', true); |
$input.prop('checked', true); |
||
} |
} |
||
+ | |||
⚫ | |||
+ | |||
− | |||
− | if ( |
+ | if (Object.keys(param.toggles).length > 0) { |
− | $input.on('change', function(){ |
+ | $input.on('change', function () { |
helper.toggle.call(self, this.checked ? 'true' : 'false', param.toggles); |
helper.toggle.call(self, this.checked ? 'true' : 'false', param.toggles); |
||
}); |
}); |
||
} |
} |
||
− | param.$ui = $input; // [ |
+ | param.$ui = $input; // [HSW] custom way of doing much less involved "param.ooui" |
$td.append($input); |
$td.append($input); |
||
return $td; |
return $td; |
||
Line 706: | Line 713: | ||
def: function ($td, param, id) { |
def: function ($td, param, id) { |
||
var $input = $('<input>') |
var $input = $('<input>') |
||
− | + | .attr({ |
|
− | + | type: 'text', |
|
− | + | name: id, |
|
− | + | id: id |
|
− | + | }) |
|
− | + | .val(param.def); |
|
− | param.$ui = $input; // [ |
+ | param.$ui = $input; // [HSW] custom way of doing much less involved "param.ooui" |
$td.append($input); |
$td.append($input); |
||
Line 737: | Line 744: | ||
lines, |
lines, |
||
config; |
config; |
||
+ | |||
⚫ | |||
// support div tags for config as well as pre |
// support div tags for config as well as pre |
||
// be aware using div tags relies on wikitext for parsing |
// be aware using div tags relies on wikitext for parsing |
||
// so you can't use anchor or img tags |
// so you can't use anchor or img tags |
||
// use the wikitext equivalent instead |
// use the wikitext equivalent instead |
||
⚫ | |||
⚫ | |||
− | + | // so use .text() instead for <pre> tags |
|
− | + | lines = $elem.text().split('\n'); |
|
+ | |||
− | } else { |
||
⚫ | |||
− | // so use .text() instead for <pre> tags |
||
− | lines = $elem.text(); |
||
− | } |
||
− | |||
− | lines = lines.split('\n'); |
||
− | |||
config = helper.parseConfig.call(this, lines); |
config = helper.parseConfig.call(this, lines); |
||
Line 767: | Line 767: | ||
return $('#' + id); |
return $('#' + id); |
||
} |
} |
||
+ | |||
− | |||
return $('#jsForm-' + self.form).find('select, input'); |
return $('#jsForm-' + self.form).find('select, input'); |
||
}; |
}; |
||
} |
} |
||
+ | |||
− | |||
/** |
/** |
||
* Helper function for getting the id of an input |
* Helper function for getting the id of an input |
||
Line 789: | Line 789: | ||
*/ |
*/ |
||
Calc.prototype.setupCalc = function () { |
Calc.prototype.setupCalc = function () { |
||
− | + | console.log(this); |
|
var self = this, |
var self = this, |
||
$form = $('<form>') |
$form = $('<form>') |
||
− | + | .attr({ |
|
− | + | action: '#', |
|
− | + | id: 'jsForm-' + self.form |
|
− | + | }) |
|
− | + | .submit(function (e) { |
|
− | + | e.preventDefault(); |
|
− | + | helper.submitForm.call(self); |
|
− | + | }), |
|
$table = $('<table>') |
$table = $('<table>') |
||
− | + | .addClass('wikitable') |
|
− | + | .addClass('jcTable'); |
|
+ | |||
− | |||
self.indexkeys = {}; |
self.indexkeys = {}; |
||
+ | |||
− | |||
var $submitButton = $('<td>') |
var $submitButton = $('<td>') |
||
− | .addClass('jcSubmit') |
+ | .addClass('jcSubmit').addClass('noselect') |
.attr('rowspan', Object.keys(self.tParams).length) |
.attr('rowspan', Object.keys(self.tParams).length) |
||
− | .css({ |
||
− | 'vertical-align': 'bottom', |
||
− | }) |
||
.append( |
.append( |
||
− | + | $('<div>').text('↵').css({ |
|
− | + | 'font-size': '2em', |
|
− | + | 'padding': '0 0.3em', |
|
− | + | 'text-align': 'center', |
|
− | + | }) |
|
− | + | ) |
|
− | + | .click(function () { |
|
− | + | $form.submit(); |
|
− | + | }); |
|
− | ) |
||
⚫ | |||
− | + | $table.append($('<tr>').addClass('jcTable-toprow').append( |
|
− | + | $('<td>').attr('colspan', 3) |
|
− | + | .append( |
|
− | + | $('<span>').text(self.calcname || 'Calculator'), |
|
− | + | $('<a>').text('view template').addClass('noselect') |
|
− | + | .attr('href', self.template && '/wiki/' + self.template || '#') |
|
⚫ | |||
− | .css({ |
||
⚫ | |||
− | 'float': 'right', |
||
+ | .attr('href', self.calcpage && '/wiki/' + self.calcpage || '#') |
||
− | 'font-size': '0.5em', |
||
⚫ | |||
− | 'margin-left': '1em', |
||
⚫ | |||
− | }), |
||
⚫ | |||
⚫ | |||
+ | |||
− | .attr('href', '#') |
||
⚫ | |||
− | .css({ |
||
+ | $table.append($('<tr>').append( |
||
− | 'float': 'right', |
||
+ | $('<td>').attr('colspan', '3').addClass('jcResult').hide() |
||
− | 'font-size': '0.5em', |
||
⚫ | |||
− | 'margin-left': '1em', |
||
⚫ | |||
− | }) |
||
− | ) |
||
− | )); |
||
self.tParams.forEach(function (param, index) { |
self.tParams.forEach(function (param, index) { |
||
Line 856: | Line 849: | ||
var id = helper.getId.call(self, param.name), |
var id = helper.getId.call(self, param.name), |
||
$tr = $('<tr>'), |
$tr = $('<tr>'), |
||
− | $td = $('<td>'), |
+ | $td = $('<td>').addClass('jcTable-cell-normal').addClass('jcInput'), |
method = helper.tParams[param.type] ? |
method = helper.tParams[param.type] ? |
||
− | + | param.type : |
|
− | + | 'def', |
|
// sanitise any HTML before inserting it |
// sanitise any HTML before inserting it |
||
// need this check otherwise jQuery cries |
// need this check otherwise jQuery cries |
||
// might need slightly better check in edge cases, but this should do for now |
// might need slightly better check in edge cases, but this should do for now |
||
label = param.label.indexOf('<') > -1 ? |
label = param.label.indexOf('<') > -1 ? |
||
− | + | helper.sanitiseLabels(param.label) : |
|
− | + | param.label; |
|
// add label |
// add label |
||
$tr.append( |
$tr.append( |
||
− | $('<th>') |
+ | $('<th>').addClass('jcTable-cell-normal').addClass('jcLabel') |
− | + | .append( |
|
− | + | $('<label>') |
|
− | + | .attr('for', id) |
|
− | + | .html(label) |
|
− | + | ) |
|
); |
); |
||
Line 881: | Line 874: | ||
if ($submitButton) { |
if ($submitButton) { |
||
− | + | $tr.append($submitButton); |
|
− | + | $submitButton = null; |
|
} |
} |
||
Line 896: | Line 889: | ||
// Run toggle for each field, check validity |
// Run toggle for each field, check validity |
||
− | self.tParams.forEach( |
+ | self.tParams.forEach(function (param) { |
if (param.toggles && Object.keys(param.toggles).length > 0) { |
if (param.toggles && Object.keys(param.toggles).length > 0) { |
||
var val; |
var val; |
||
Line 908: | Line 901: | ||
// val = param.ooui.getValue(); |
// val = param.ooui.getValue(); |
||
// } |
// } |
||
− | // [ |
+ | // [HSW] note: hacky version of the above to get it working on fandom |
if (param.type === 'check') { |
if (param.type === 'check') { |
||
val = param.$ui[0].checked ? 'true' : 'false'; |
val = param.$ui[0].checked ? 'true' : 'false'; |
||
Line 922: | Line 915: | ||
$form.append($table); |
$form.append($table); |
||
+ | |||
− | |||
if (self.configError) { |
if (self.configError) { |
||
$form.append(self.configError); |
$form.append(self.configError); |
||
Line 929: | Line 922: | ||
$('#bodyContent, #WikiaArticle, .WikiaArticle, #content, .page-content') |
$('#bodyContent, #WikiaArticle, .WikiaArticle, #content, .page-content') |
||
.find('#' + self.form) |
.find('#' + self.form) |
||
− | + | .empty() |
|
− | + | .append($form); |
|
// Enable suggest on article fields |
// Enable suggest on article fields |
||
− | mw.loader.using(['mediawiki.api','jquery.ui.autocomplete'], function () { |
+ | mw.loader.using(['mediawiki.api', 'jquery.ui.autocomplete'], function () { |
self.acInputs.forEach(function (input) { |
self.acInputs.forEach(function (input) { |
||
$('#' + input).autocomplete({ |
$('#' + input).autocomplete({ |
||
// matching wikia's search min length |
// matching wikia's search min length |
||
minLength: 3, |
minLength: 3, |
||
− | source: function(request, response) { |
+ | source: function (request, response) { |
var term = request.term; |
var term = request.term; |
||
Line 947: | Line 940: | ||
(new mw.Api()) |
(new mw.Api()) |
||
− | + | .get({ |
|
action: 'opensearch', |
action: 'opensearch', |
||
search: term, |
search: term, |
||
Line 963: | Line 956: | ||
}); |
}); |
||
}; |
}; |
||
+ | |||
− | |||
/** |
/** |
||
* @todo |
* @todo |
||
Line 978: | Line 971: | ||
var c = new Calc(this); |
var c = new Calc(this); |
||
c.setupCalc(); |
c.setupCalc(); |
||
+ | |||
⚫ | |||
calcStore[c.form] = c; |
calcStore[c.form] = c; |
||
}); |
}); |
||
+ | |||
− | |||
// allow scripts to hook into calc setup completion |
// allow scripts to hook into calc setup completion |
||
mw.hook('rscalc.setupComplete').fire(); |
mw.hook('rscalc.setupComplete').fire(); |
||
Line 987: | Line 980: | ||
$(init); |
$(init); |
||
− | + | importArticle({ |
|
⚫ | |||
⚫ | |||
+ | article: 'MediaWiki:Common.js/calc.css' |
||
⚫ | |||
⚫ | |||
+ | |||
⚫ | |||
⚫ | |||
}(jQuery, mediaWiki, hsbwiki)); |
}(jQuery, mediaWiki, hsbwiki)); |
Latest revision as of 15:19, 13 November 2023
/** <nowiki>
* Taken from https://runescape.fandom.com/wiki/MediaWiki:Common.js/calc.js
* Calc script for RuneScape Wiki
*
* This script exposes the following hooks, accessible via `mw.hook`:
* 1. 'rscalc.setupComplete' - Fires when all calculator forms have been added to the DOM.
* 2. 'rscalc.submit' - Fires when a calculator form has been submitted and the result has
* been added to the DOM.
* For instructions on how to use `mw.hook`, see <https://doc.wikimedia.org/mediawiki-core/master/js/#!/api/mw.hook>
*
* @see Documentation <http://runescape.wikia.com/wiki/RuneScape:Calculators/Form_calculators>
* @see Examples <http://runescape.wikia.com/wiki/RuneScape:Calculators/Form_calculators/Examples>
* @see Tests <http://runescape.wikia.com/wiki/RuneScape:Calculators/Form_calculators/Tests>
* Also includes XSS checks.
*
* @license GLPv3 <http://www.gnu.org/licenses/gpl-3.0.en.html>
*
* @author Quarenon
* @author TehKittyCat
* @author Joeytje50
* @author Cook Me Plox
* @author Gaz Lloyd
* @author Cqm
*
* @todo Test Wikia's linksuggest for search suggestions
* not sure if it supports multiple namespaces though
* @todo Whitelist domains for href attributes when sanitising 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 mediaWiki, hsbwiki, importArticle, console */
window.hsbwiki = window.hsbwiki || {};
(function ($, mw, hsbwiki, undefined) {
'use strict';
if (hsbwiki.calculatorLoaded) {
return;
}
hsbwiki.calculatorLoaded = true;
/**
* Caching for search suggestions
*/
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 = {
/**
* 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: []
},
config = {
// this isn't in `defConfig`
// as it'll get overridden anyway
tParams: []
},
// used for debugging incorrect config names
validParams = [
'calcname',
'calcpage',
'form',
'param',
'result',
'suggestns',
'template',
'argslist'
],
// used for debugging incorrect param types
validParamTypes = [
'string',
'article',
'number',
'int',
'select',
'check',
'fixed',
'hidden',
'semihidden'
],
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) {
// use console for easier debugging
console.log('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] !== '') {
// use console for easier debugging
console.log('Unknown param type: ' + args[3]);
configError = true;
return;
}
config.tParams.push({
name: mw.html.escape(args[0]),
label: args[1] || args[0],
def: mw.html.escape(args[2] || ''),
type: mw.html.escape(args[3] || ''),
range: mw.html.escape(args[4] || ''),
rawtogs: mw.html.escape(args[5] || '')
});
});
if (configError) {
config.configError = 'This calculator\'s config contains errors. Please report it to an admin or check the javascript console for details.';
}
config = $.extend(defConfig, 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('-');
},
/**
* Output an error to the UI
*
* @param error {String} A string representing the error message to be output
*/
showError: function (error) {
$(this.result !== 'inner' ? ('#' + this.result) : ('#' + this.form + ' .jcResult'))
.empty()
.append(
$('<span>')
.addClass('jcError')
.text(error)
)
.show();
},
/**
* 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().setDisabled) {
// child.getField().setDisabled(!show);
// }
// });
// } else
if (param.type === 'semihidden') {
// if (!!param.ooui.setDisabled) {
// param.ooui.setDisabled(!show);
// }
// [HSW] Yet another hacky way to get around not having ooui
param.$ui.prop('disabled', !show);
} else {
// param.layout.toggle(show);
// if (!!param.$ui.setDisabled) {
// param.ooui.setDisabled(!show);
// }
// [HSW] Yet another hacky way to get around not having ooui
param.$ui.closest('tr').toggle(show);
param.$ui.prop('disabled', !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);
});
}
},
/**
* Check that a number is within a specified range
*
* @param x {Number} The number to check is within the range
* @param param {Object} An object containing the range to check and the type of
* parameter as the check varies slightly depending on it's type
* @returns {Boolean} `true` if the number is within the parameter's range or
* `false` if not
*/
validRange: function (x, param) {
// if no range, return true
if (!param.range) {
return true;
}
var parts = param.range.split('-'),
method = parseFloat;
// enforce integer ranges for int
// otherwise allow floats for number
if (param.type === 'int') {
method = parseInt;
}
// check lower limit
if (parts[0] !== '' && x < method(parts[0], 10)) {
return false;
}
// check upper limit
if (parts[1] !== '' && x > method(parts[1], 10)) {
return false;
}
return true;
},
/**
* 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 + (self.argslist ? '|' + self.argslist : ''),
formError = false;
// setup template for submission
self.tParams.forEach(function (param) {
var val,
$input,
// use separate error tracking for each input
// or every input gets flagged as an error
error = false;
if (param.type === 'fixed' || param.type === 'hidden') {
val = param.def;
} else {
$input = $('#' + helper.getId.call(self, param.name));
val = $input.val();
if (param.type === 'int') {
val = val.split(',').join('');
} else if (param.type === 'check') {
val = $input.prop('checked');
if (param.range) {
val = param.range.split(',')[val ? 0 : 1];
}
}
// int types must be a valid integer or range
if (
param.type === 'int' &&
(
val.search(/^-?[0-9]+$/) === -1 ||
!helper.validRange(val, param)
)
) {
error = true;
}
// number types must be a valid number or range
if (
param.type === 'number' &&
(
val.search(/^-?[.0-9]+$/) === -1 ||
!helper.validRange(val, param)
)
) {
error = true;
}
if (error) {
$input.addClass('jcInvalid');
formError = true;
} else {
$input.removeClass('jcInvalid');
}
}
code += '|' + param.name + '=' + val;
});
if (formError) {
helper.showError.call(self, 'One or more fields contains an invalid value.');
return;
}
code += '}}';
helper.loadTemplate.call(self, code);
},
/**
* 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',
contentmodel: 'wikitext',
disablelimitreport: 'true'
};
// experimental support for using VE to parse calc templates
if (!!mw.util.getParamValue('vecalc')) {
params = {
action: 'visualeditor',
// has to be a mainspace page or VE won't work
page: 'No page',
paction: 'parsefragment',
wikitext: code
};
}
$('#' + self.form + ' .jcSubmit div')
.text('..').css('color', 'gray');
// @todo time how long these calls take
(new mw.Api())
.post(params)
.done(function (response) {
var html;
if (!!mw.util.getParamValue('vecalc')) {
// strip body tag
html = $(response.visualeditor.content).contents();
} else {
html = response.parse.text['*'];
}
helper.dispResult.call(self, html);
})
.fail(function (_, error) {
$('#' + self.form + ' .jcSubmit div')
.text('↵').css('color', 'white');
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) {
$('#' + this.form + ' .jcSubmit div')
.text('↵').css('color', 'white');
$('#bodyContent, #WikiaArticle, .WikiaArticle, #content, .page-content')
.find(this.result !== 'inner' ? ('#' + this.result) : ('#' + this.form + ' .jcResult'))
.empty()
.removeClass('jcError')
.html(html)
.show();
// allow scripts to hook into form submission
mw.hook('rscalc.submit').fire();
mw.loader.using('jquery.tablesorter', function () {
$('table.sortable').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) {
mw.log('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) {
mw.log('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
) {
mw.log('Script URL detected in ' + tagname);
$this.removeAttr('href');
}
}
});
});
return $div.contents();
},
/**
* Handlers for parameter input types
*/
tParams: {
/**
* Handler for 'fixed' inputs
*
* @param $td {jQuery.object} A jQuery object representing a table cell to
* add content to
* @param param {object} An object containing the configuration of a parameter
* @returns {jQuery.object} A jQuery object representing the completed table cell
*/
fixed: function ($td, param) {
$td.text(param.def);
param.$ui = $td; // [HSW] custom way of doing much less involved "param.ooui"
return $td;
},
/**
* Handler for select dropdowns
*
* @param $td {jQuery.object} A jQuery object representing a table cell to
* add content to
* @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 {jQuery.object} A jQuery object representing the completed table cell
*/
select: function ($td, param, id) {
var self = this,
$select = $('<select>')
.attr({
name: id,
id: id
}),
opts = param.range.split(/\s*,\s*/),
def = opts[0];
opts.forEach(function (opt) {
// undo the mw.html.escape call used when creating the params object
// and defer the escaping to $.fn.val and $.fn.text instead
opt = opt.replace(/>/g, '>')
.replace(/</g, '<')
.replace(/&/g, '&')
.replace(/"/g, '"')
.replace(/'/g, '\'');
var $option = $('<option>')
.val(opt)
.text(opt);
if (opt === param.def) {
$option.prop('selected', true);
}
$select.append($option);
});
param.toggles = helper.parseToggles(param.rawtogs, def);
if (Object.keys(param.toggles).length > 0) {
$select.on('change', function () {
helper.toggle.call(self, $(this).val(), param.toggles);
});
}
param.$ui = $select; // [HSW] custom way of doing much less involved "param.ooui"
$td.append($select);
return $td;
},
/**
* Handler for checkbox inputs
*
* @param $td {jQuery.object} A jQuery object representing a table cell to
* add content to
* @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 {jQuery.object} A jQuery object representing the completed table cell
*/
check: function ($td, param, id) {
var self = this,
$input = $('<input>')
.attr({
type: 'checkbox',
name: id,
id: id
});
param.toggles = helper.parseToggles(param.rawtogs, 'true');
if (
param.def === 'true' ||
(param.range !== undefined && param.def === param.range.split(',')[0])
) {
$input.prop('checked', true);
}
if (Object.keys(param.toggles).length > 0) {
$input.on('change', function () {
helper.toggle.call(self, this.checked ? 'true' : 'false', param.toggles);
});
}
param.$ui = $input; // [HSW] custom way of doing much less involved "param.ooui"
$td.append($input);
return $td;
},
/**
* Default handler for inputs
*
* @param $td {jQuery.object} A jQuery object representing a table cell to
* add content to
* @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 {jQuery.object} A jQuery object representing the completed table cell
*/
def: function ($td, param, id) {
var $input = $('<input>')
.attr({
type: 'text',
name: id,
id: id
})
.val(param.def);
param.$ui = $input; // [HSW] custom way of doing much less involved "param.ooui"
$td.append($input);
if (param.type === 'article') {
this.acInputs.push(id);
}
return $td;
}
}
};
/**
* 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
// .html() causes html characters to be escaped for some reason
// so use .text() instead for <pre> tags
lines = $elem.text().split('\n');
config = helper.parseConfig.call(this, lines);
// merge config in
$.extend(this, config);
this.acInputs = [];
/**
* @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 () {
console.log(this);
var self = this,
$form = $('<form>')
.attr({
action: '#',
id: 'jsForm-' + self.form
})
.submit(function (e) {
e.preventDefault();
helper.submitForm.call(self);
}),
$table = $('<table>')
.addClass('wikitable')
.addClass('jcTable');
self.indexkeys = {};
var $submitButton = $('<td>')
.addClass('jcSubmit').addClass('noselect')
.attr('rowspan', Object.keys(self.tParams).length)
.append(
$('<div>').text('↵').css({
'font-size': '2em',
'padding': '0 0.3em',
'text-align': 'center',
})
)
.click(function () {
$form.submit();
});
$table.append($('<tr>').addClass('jcTable-toprow').append(
$('<td>').attr('colspan', 3)
.append(
$('<span>').text(self.calcname || 'Calculator'),
$('<a>').text('view template').addClass('noselect')
.attr('href', self.template && '/wiki/' + self.template || '#')
.attr('target', '_blank'),
$('<a>').text('view calculator').addClass('noselect')
.attr('href', self.calcpage && '/wiki/' + self.calcpage || '#')
.attr('target', '_blank')
)
));
if (self.result === 'inner') {
$table.append($('<tr>').append(
$('<td>').attr('colspan', '3').addClass('jcResult').hide()
));
}
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),
$tr = $('<tr>'),
$td = $('<td>').addClass('jcTable-cell-normal').addClass('jcInput'),
method = helper.tParams[param.type] ?
param.type :
'def',
// sanitise any HTML before inserting it
// need this check otherwise jQuery cries
// might need slightly better check in edge cases, but this should do for now
label = param.label.indexOf('<') > -1 ?
helper.sanitiseLabels(param.label) :
param.label;
// add label
$tr.append(
$('<th>').addClass('jcTable-cell-normal').addClass('jcLabel')
.append(
$('<label>')
.attr('for', id)
.html(label)
)
);
$td = helper.tParams[method].call(self, $td, param, id);
$tr.append($td);
if ($submitButton) {
$tr.append($submitButton);
$submitButton = null;
}
if (param.type === 'semihidden') {
$tr.hide();
}
$table.append($tr);
// 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();
// }
// [HSW] note: hacky version of the above to get it working on fandom
if (param.type === 'check') {
val = param.$ui[0].checked ? 'true' : 'false';
} else {
val = param.$ui.val();
}
helper.toggle.call(self, val, param.toggles);
}
// if (param.type === 'number' || param.type === 'int' || param.type === 'rsn') {
// param.ooui.setValidityFlag();
// }
});
$form.append($table);
if (self.configError) {
$form.append(self.configError);
}
$('#bodyContent, #WikiaArticle, .WikiaArticle, #content, .page-content')
.find('#' + self.form)
.empty()
.append($form);
// Enable suggest on article fields
mw.loader.using(['mediawiki.api', 'jquery.ui.autocomplete'], function () {
self.acInputs.forEach(function (input) {
$('#' + input).autocomplete({
// matching wikia's search min length
minLength: 3,
source: function (request, response) {
var term = request.term;
if (term in cache) {
response(cache[term]);
return;
}
(new mw.Api())
.get({
action: 'opensearch',
search: term,
// default to main namespace
namespace: self.suggestns.join('|') || 0,
suggest: ''
})
.done(function (data) {
cache[term] = data[1];
response(data[1]);
});
}
});
});
});
};
/**
* @todo
*/
function lookupCalc(calcId) {
return calcStore[calcId];
}
/**
* @todo
*/
function init() {
$('.jcConfig').each(function () {
var c = new Calc(this);
c.setupCalc();
calcStore[c.form] = c;
});
// allow scripts to hook into calc setup completion
mw.hook('rscalc.setupComplete').fire();
}
$(init);
importArticle({
type: 'style',
article: 'MediaWiki:Common.js/calc.css'
});
hsbwiki.calc = {};
hsbwiki.calc.lookup = lookupCalc;
}(jQuery, mediaWiki, hsbwiki));