MediaWiki:Gadget-calc-core.js

Note: After publishing, 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: Press Ctrl-F5.
   1 /** <nowiki>
   2  * Calc script for RuneScape Wiki
   3  * 
   4  * MAIN SCRIPT	https://runescape.wiki/w/MediaWiki:Gadget-calc.js
   5  *				https://runescape.wiki/w/MediaWiki:Gadget-calc.css
   6  * DUPLICATE TO	https://oldschool.runescape.wiki/w/MediaWiki:Gadget-calc.js
   7  *				https://oldschool.runescape.wiki/w/MediaWiki:Gadget-calc.css
   8  * make sure to update the hiscores URL for OSRS
   9  * 
  10  * This script exposes the following hooks, accessible via `mw.hook`:
  11  *     1. 'rscalc.setupComplete' - Fires when all calculator forms have been added to the DOM.
  12  *     2. 'rscalc.submit' - Fires when a calculator form has been submitted and the result has
  13  *                          been added to the DOM.
  14  * For instructions on how to use `mw.hook`, see <https://doc.wikimedia.org/mediawiki-core/master/js/#!/api/mw.hook>
  15  *
  16  * @see Documentation <https://runescape.wiki/w/RuneScape:Calculators/Form_calculators>
  17  * @see Tests <https://runescape.wiki/w/RuneScape:Calculators/Form_calculators/Tests>
  18  *
  19  * @license GLPv3 <https://www.gnu.org/licenses/gpl-3.0.en.html>
  20  *
  21  * @author Quarenon
  22  * @author TehKittyCat
  23  * @author Joeytje50
  24  * @author Cook Me Plox
  25  * @author Gaz Lloyd
  26  * @author Cqm
  27  * @author Elessar2
  28  *
  29  * @todo Whitelist domains for href attributes when sanitising HTML?
  30  * @todo if we get cross-wiki imports, add a way to change hiscores URL
  31  */
  32 
  33 /*jshint bitwise:true, browser:true, camelcase:true, curly:true, devel:false,
  34          eqeqeq:true, es3:false, forin:true, immed:true, jquery:true,
  35          latedef:true, newcap:true, noarg:true, noempty:true, nonew:true,
  36          onevar:false, plusplus:false, quotmark:single, undef:true, unused:true,
  37          strict:true, trailing:true
  38 */
  39 
  40 /*global mediaWiki, mw, rswiki, rs, OO */
  41 
  42 'use strict';
  43 
  44     /**
  45      * Prefix of localStorage key for calc data. This is prepended to the form ID
  46      * localStorage name for autosubmit setting
  47      */
  48 var calcstorage = 'rsw-calcsdata',
  49     calcautostorage = 'rsw-calcsdata-allautosub',
  50     /**
  51      * Caching for search suggestions
  52      *
  53      * @todo implement caching for mw.TitleInputWidget accroding to https://doc.wikimedia.org/mediawiki-core/master/js/#!/api/mw.widgets.TitleWidget-cfg-cache
  54      */
  55     cache = {},
  56 
  57     /**
  58      * Internal variable to store references to each calculator on the page.
  59      */
  60     calcStore = {},
  61 
  62     /**
  63      * Private helper methods for `Calc`
  64      *
  65      * Most methods here are called with `Function.prototype.call`
  66      * and are passed an instance of `Calc` to access it's prototype
  67      */
  68     helper = {
  69         /**
  70          * Add/change functionality of mw/OO.ui classes
  71          * Added support for multiple namespaces to mw.widgets.TitleInputWidget
  72          */
  73         initClasses: function () {
  74             var hasOwn = Object.prototype.hasOwnProperty;
  75             /**
  76              * Get option widgets from the server response
  77              * Changed to add support for multiple namespaces
  78              * 
  79              * @param {Object} data Query result
  80              * @return {OO.ui.OptionWidget[]} Menu items
  81              */
  82             mw.widgets.TitleInputWidget.prototype.getOptionsFromData = function (data) {
  83                 var i, len, index, pageExists, pageExistsExact, suggestionPage, page, redirect, redirects,
  84                     currentPageName = new mw.Title( mw.config.get( 'wgRelevantPageName' ) ).getPrefixedText(),
  85                     items = [],
  86                     titles = [],
  87                     titleObj = mw.Title.newFromText( this.getQueryValue() ),
  88                     redirectsTo = {},
  89                     pageData = {},
  90                     namespaces = this.namespace.split('|').map(function (val) {return parseInt(val,10);});
  91 
  92                 if ( data.redirects ) {
  93                     for ( i = 0, len = data.redirects.length; i < len; i++ ) {
  94                         redirect = data.redirects[ i ];
  95                         redirectsTo[ redirect.to ] = redirectsTo[ redirect.to ] || [];
  96                         redirectsTo[ redirect.to ].push( redirect.from );
  97                     }
  98                 }
  99 
 100                 for ( index in data.pages ) {
 101                     suggestionPage = data.pages[ index ];
 102 
 103                     // When excludeCurrentPage is set, don't list the current page unless the user has type the full title
 104                     if ( this.excludeCurrentPage && suggestionPage.title === currentPageName && suggestionPage.title !== titleObj.getPrefixedText() ) {
 105                         continue;
 106                     }
 107 
 108                     // When excludeDynamicNamespaces is set, ignore all pages with negative namespace
 109                     if ( this.excludeDynamicNamespaces && suggestionPage.ns < 0 ) {
 110                         continue;
 111                     }
 112                     pageData[ suggestionPage.title ] = {
 113                         known: suggestionPage.known !== undefined,
 114                         missing: suggestionPage.missing !== undefined,
 115                         redirect: suggestionPage.redirect !== undefined,
 116                         disambiguation: OO.getProp( suggestionPage, 'pageprops', 'disambiguation' ) !== undefined,
 117                         imageUrl: OO.getProp( suggestionPage, 'thumbnail', 'source' ),
 118                         description: suggestionPage.description,
 119                         // Sort index
 120                         index: suggestionPage.index,
 121                         originalData: suggestionPage
 122                     };
 123 
 124                     // Throw away pages from wrong namespaces. This can happen when 'showRedirectTargets' is true
 125                     // and we encounter a cross-namespace redirect.
 126                     if ( this.namespace === null || namespaces.indexOf(suggestionPage.ns) >= 0 ) {
 127                         titles.push( suggestionPage.title );
 128                     }
 129 
 130                     redirects = hasOwn.call( redirectsTo, suggestionPage.title ) ? redirectsTo[ suggestionPage.title ] : [];
 131                     for ( i = 0, len = redirects.length; i < len; i++ ) {
 132                         pageData[ redirects[ i ] ] = {
 133                             missing: false,
 134                             known: true,
 135                             redirect: true,
 136                             disambiguation: false,
 137                             description: mw.msg( 'mw-widgets-titleinput-description-redirect', suggestionPage.title ),
 138                             // Sort index, just below its target
 139                             index: suggestionPage.index + 0.5,
 140                             originalData: suggestionPage
 141                         };
 142                         titles.push( redirects[ i ] );
 143                     }
 144                 }
 145 
 146                 titles.sort( function ( a, b ) {
 147                     return pageData[ a ].index - pageData[ b ].index;
 148                 } );
 149 
 150                 // If not found, run value through mw.Title to avoid treating a match as a
 151                 // mismatch where normalisation would make them matching (T50476)
 152 
 153                 pageExistsExact = (
 154                     hasOwn.call( pageData, this.getQueryValue() ) &&
 155                     (
 156                         !pageData[ this.getQueryValue() ].missing ||
 157                         pageData[ this.getQueryValue() ].known
 158                     )
 159                 );
 160                 pageExists = pageExistsExact || (
 161                     titleObj &&
 162                     hasOwn.call( pageData, titleObj.getPrefixedText() ) &&
 163                     (
 164                         !pageData[ titleObj.getPrefixedText() ].missing ||
 165                         pageData[ titleObj.getPrefixedText() ].known
 166                     )
 167                 );
 168 
 169                 if ( this.cache ) {
 170                     this.cache.set( pageData );
 171                 }
 172 
 173                 // Offer the exact text as a suggestion if the page exists
 174                 if ( this.addQueryInput && pageExists && !pageExistsExact ) {
 175                     titles.unshift( this.getQueryValue() );
 176                 }
 177 
 178                 for ( i = 0, len = titles.length; i < len; i++ ) {
 179                     page = hasOwn.call( pageData, titles[ i ] ) ? pageData[ titles[ i ] ] : {};
 180                     items.push( this.createOptionWidget( this.getOptionWidgetData( titles[ i ], page ) ) );
 181                 }
 182 
 183                 return items;
 184             };
 185         },
 186 
 187         /**
 188          * Parse the calculator configuration
 189          *
 190          * @param lines {Array} An array containing the calculator's configuration
 191          * @returns {Object} An object representing the calculator's configuration
 192          */
 193         parseConfig: function (lines) {
 194             var defConfig = {
 195                     suggestns: [],
 196                     autosubmit: 'off',
 197                     name: 'Calculator'
 198                 },
 199                 config = {
 200                     // this isn't in `defConfig`
 201                     // as it'll get overridden anyway
 202                     tParams: []
 203                 },
 204                 // used for debugging incorrect config names
 205                 validParams = [
 206                     'form',
 207                     'param',
 208                     'result',
 209                     'suggestns',
 210                     'template',
 211                     'module',
 212                     'modulefunc',
 213                     'name',
 214                     'autosubmit'
 215                 ],
 216                 // used for debugging incorrect param types
 217                 validParamTypes = [
 218                     'string',
 219                     'article',
 220                     'number',
 221                     'int',
 222                     'select',
 223                     'buttonselect',
 224                     'combobox',
 225                     'check',
 226                     'toggleswitch',
 227                     'togglebutton',
 228                     'hs',
 229                     'rsn',
 230                     'fixed',
 231                     'hidden',
 232                     'semihidden',
 233                     'group'
 234                 ],
 235                 configError = false;
 236 
 237             // parse the calculator's config
 238             // @example param=arg1|arg1|arg3|arg4
 239             lines.forEach(function (line) {
 240                 var temp = line.split('='),
 241                     param,
 242                     args;
 243 
 244                 // incorrect config
 245                 if (temp.length < 2) {
 246                     return;
 247                 }
 248 
 249                 // an equals is used in one of the arguments
 250                 // @example HTML label with attributes
 251                 // so join them back together to preserve it
 252                 // this also allows support of HTML attributes in labels
 253                 if (temp.length > 2) {
 254                     temp[1] = temp.slice(1,temp.length).join('=');
 255                 }
 256 
 257                 param = temp[0].trim().toLowerCase();
 258                 args = temp[1].trim();
 259 
 260                 if (validParams.indexOf(param) === -1) {
 261                     // use console for easier debugging
 262                     console.log('Unknown parameter: ' + param);
 263                     configError = true;
 264                     return;
 265                 }
 266 
 267                 if (param === 'suggestns') {
 268                     config.suggestns = args.split(/\s*,\s*/);
 269                     return;
 270                 }
 271 
 272                 if (param !== 'param') {
 273                     config[param] = args;
 274                     return;
 275                 }
 276 
 277                 // split args
 278                 args = args.split(/\s*\|\s*/);
 279 
 280                 // store template params in an array to make life easier
 281                 config.tParams = config.tParams || [];
 282 
 283                 if (validParamTypes.indexOf(args[3]) === -1 && args[3] !== '' && args[3] !== undefined) {
 284                     // use console for easier debugging
 285                     console.log('Unknown param type: ' + args[3]);
 286                     configError = true;
 287                     return;
 288                 }
 289 
 290                 var inlinehelp = false, help = '';
 291                 if (args[6]) {
 292                     var tmphelp = args[6].split(/\s*=\s*/);
 293                     if (tmphelp.length > 1) {
 294                         if ( tmphelp[0] === 'inline' ) {
 295                             inlinehelp = true;
 296                             // Html etc can have = so join them back together
 297                             tmphelp[1] = tmphelp.slice(1,tmphelp.length).join('=');
 298                             help = helper.sanitiseLabels(tmphelp[1] || '');
 299                         } else {
 300                             // Html etc can have = so join them back together
 301                             tmphelp[0] = tmphelp.join('=');
 302                             help = helper.sanitiseLabels(tmphelp[0] || '');
 303                         }
 304                     } else {
 305                         help = helper.sanitiseLabels(tmphelp[0] || '');
 306                     }
 307                 }
 308 
 309                 config.tParams.push({
 310                     name: mw.html.escape(args[0]),
 311                     label: helper.sanitiseLabels(args[1] || args[0]),
 312                     def: mw.html.escape(args[2] || ''),
 313                     type: mw.html.escape(args[3] || ''),
 314                     range: mw.html.escape(args[4] || ''),
 315                     rawtogs: mw.html.escape(args[5] || ''),
 316                     inlhelp: inlinehelp,
 317                     help: help
 318                 });
 319             });
 320             
 321             if (configError) {
 322                 config.configError = 'This calculator\'s config contains errors. Please report it ' +
 323                     '<a href="/w/RuneScape:User_help" title="RuneScape:User help">here</a> ' +
 324                     'or check the javascript console for details.';
 325             }
 326 
 327             config = $.extend(defConfig, config);
 328             mw.log(config);
 329             return config;
 330         },
 331 
 332         /**
 333          * Generate a unique id for each input
 334          *
 335          * @param inputId {String} A string representing the id of an input
 336          * @returns {String} A string representing the namespaced/prefixed id of an input
 337          */
 338         getId: function (inputId) {
 339             return [this.form, this.result, inputId].join('-');
 340         },
 341 
 342         /**
 343          * Output an error to the UI
 344          *
 345          * @param error {String} A string representing the error message to be output
 346          */
 347         showError: function (error) {
 348             $('#' + this.result)
 349                 .empty()
 350                 .append(
 351                     $('<strong>')
 352                         .addClass('error')
 353                         .text(error)
 354                 );
 355         },
 356 
 357         /**
 358          * Toggle the visibility and enabled status of fields/groups
 359          *
 360          * @param item {String} A string representing the current value of the widget
 361          * @param toggles {object} An object representing arrays of items to be toggled keyed by widget values
 362          */
 363         toggle: function (item, toggles) {
 364             var self = this;
 365 
 366             var togitem = function (widget, show) {
 367                 var param = self.tParams[ self.indexkeys[widget] ];
 368                 if (param.type === 'group') {
 369                     param.ooui.toggle(show);
 370                     param.ooui.getItems().forEach(function (child) {
 371                         if (!!child.setDisabled) {
 372                             child.setDisabled(!show);
 373                         } else if (!!child.getField().setDisabled) {
 374                             child.getField().setDisabled(!show);
 375                         }
 376                     });
 377                 } else if ( param.type === 'semihidden' ) {
 378                     if (!!param.ooui.setDisabled) {
 379                         param.ooui.setDisabled(!show);
 380                     }
 381                 } else {
 382                     param.layout.toggle(show);
 383                     if (!!param.ooui.setDisabled) {
 384                         param.ooui.setDisabled(!show);
 385                     }
 386                 }
 387             };
 388 
 389             if (toggles[item]) {
 390                 toggles[item].on.forEach( function (widget) {
 391                     togitem(widget, true);
 392                 });
 393                 toggles[item].off.forEach( function (widget) {
 394                     togitem(widget, false);
 395                 });
 396             } else if ( toggles.not0 && !isNaN(parseFloat(item)) && parseFloat(item) !== 0 ) {
 397                 toggles.not0.on.forEach( function (widget) {
 398                     togitem(widget, true);
 399                 });
 400                 toggles.not0.off.forEach( function (widget) {
 401                     togitem(widget, false);
 402                 });
 403             } else if (toggles.alltogs) {
 404                 toggles.alltogs.off.forEach( function (widget) {
 405                     togitem(widget, false);
 406                 });
 407             }
 408         },
 409 
 410         /**
 411          * Generate range and step for number and int inputs
 412          *
 413          * @param rawdata {string} The string representation of the range and steps
 414          * @param type {string} The name of the field type (int or number)
 415          * @returns {array} An array containing the min value, max value, step and button step.
 416          */
 417         genRange: function (rawdata,type) {
 418             var tmp = rawdata.split(/\s*,\s*/),
 419                 rng = tmp[0].split(/\s*-\s*/),
 420                 step = tmp[1] || '',
 421                 bstep = tmp[2] || '',
 422                 min, max,
 423                 parseFunc;
 424             if (type==='int') {
 425             	parseFunc = function(x) { return parseInt(x, 10); }
 426             } else {
 427             	parseFunc = parseFloat;
 428             }
 429 
 430             if (type === 'int') {
 431                 step = 1;
 432                 if ( isNaN(parseInt(bstep,10)) ) {
 433                     bstep = 1;
 434                 } else {
 435                     bstep = parseInt(bstep,10);
 436                 }
 437             } else {
 438                 if ( isNaN(parseFloat(step)) ) {
 439                     step = 0.01;
 440                 } else {
 441                     step = parseFloat(step);
 442                 }
 443                 if ( isNaN(parseFloat(bstep)) ) {
 444                     bstep = 1;
 445                 } else {
 446                     bstep = parseFloat(bstep);
 447                 }
 448             }
 449 
 450             // Accept negative values for either range position
 451             if ( rng.length === 3 ) {
 452                 // 1 value is negative
 453                 if ( rng[0] === '' ) {
 454                     // First value negative
 455                     if ( isNaN(parseFunc(rng[1])) ) {
 456                         min = -Infinity;
 457                     } else {
 458                         min = 0 - parseFunc(rng[1]);
 459                     }
 460                     if ( isNaN(parseFunc(rng[2])) ) {
 461                         max = Infinity;
 462                     } else {
 463                         max = parseFunc(rng[2]);
 464                     }
 465                 } else if ( rng[1] === '' ) {
 466                     // Second value negative
 467                     if ( isNaN(parseFunc(rng[0])) ) {
 468                         min = -Infinity;
 469                     } else {
 470                         min = parseFunc(rng[0]);
 471                     }
 472                     if ( isNaN(parseFunc(rng[2])) ) {
 473                         max = 0;
 474                     } else {
 475                         max = 0 - parseFunc(rng[2]);
 476                     }
 477                 }
 478             } else if ( rng.length === 4 ) {
 479                 // Both negative
 480                 if ( isNaN(parseFunc(rng[1])) ) {
 481                     min = -Infinity;
 482                 } else {
 483                     min = 0 - parseFunc(rng[1]);
 484                 }
 485                 if ( isNaN(parseFunc(rng[3])) ) {
 486                     max = 0;
 487                 } else {
 488                     max = 0 - parseFunc(rng[3]);
 489                 }
 490             } else {
 491                 // No negatives
 492                 if ( isNaN(parseFunc(rng[0])) ) {
 493                     min = 0;
 494                 } else {
 495                     min = parseFunc(rng[0]);
 496                 }
 497                 if ( isNaN(parseFunc(rng[1])) ) {
 498                     max = Infinity;
 499                 } else {
 500                     max = parseFunc(rng[1]);
 501                 }
 502             }
 503             // Check min < max
 504             if ( max < min ) {
 505                 return [ max, min, step, bstep ];
 506             } else {
 507                 return [ min, max, step, bstep ];
 508             }
 509         },
 510 
 511         /**
 512          * Parse the toggles for an input
 513          *
 514          * @param rawdata {string} A string representing the toggles for the widget
 515          * @param defkey {string} The default key for toggles
 516          * @returns {object} An object representing the toggles in the format { ['widget value']:[ widget-to-toggle, group-to-toggle, widget-to-toggle2 ] }
 517          */
 518         parseToggles: function (rawdata,defkey) {
 519             var tmptogs = rawdata.split(/\s*;\s*/),
 520                 allkeys = [], allvals = [],
 521                 toggles = {};
 522 
 523             if (tmptogs.length > 0 && tmptogs[0].length > 0) {
 524                 tmptogs.forEach(function (tog) {
 525                     var tmp = tog.split(/\s*=\s*/),
 526                         keys = tmp[0],
 527                         val = [];
 528                     if (tmp.length < 2) {
 529                         keys = [defkey];
 530                         val = tmp[0].split(/\s*,\s*/);
 531                     } else {
 532                         keys = tmp[0].split(/\s*,\s*/);
 533                         val = tmp[1].split(/\s*,\s*/);
 534                     }
 535                     if (keys.length === 1) {
 536                         var key = keys[0];
 537                         toggles[key] = {};
 538                         toggles[key].on = val;
 539                         allkeys.push(key);
 540                     } else {
 541                         keys.forEach( function (key) {
 542                             toggles[key] = {};
 543                             toggles[key].on = val;
 544                             allkeys.push(key);
 545                         });
 546                     }
 547                     allvals = allvals.concat(val);
 548                 });
 549 
 550                 allkeys = allkeys.filter(function (item, pos, arr) {
 551                     return arr.indexOf(item) === pos;
 552                 });
 553 
 554                 allkeys.forEach(function (key) {
 555                     toggles[key].off = allvals.filter(function (val) {
 556                         if ( toggles[key].on.includes(val) ) {
 557                             return false;
 558                         } else {
 559                             return true;
 560                         }
 561                     });
 562                 });
 563 
 564                 // Add all items to default
 565                 toggles.alltogs = {};
 566                 toggles.alltogs.off = allvals;
 567             }
 568 
 569             return toggles;
 570         },
 571 
 572         /**
 573          * Form submission handler
 574          */
 575         submitForm: function () {
 576             var self = this,
 577                 code = '{{' + self.template,
 578                 formErrors = [],
 579                 apicalls = [],
 580                 paramVals = {};
 581                 
 582             if (self.module !== undefined) {
 583             	if (self.modulefunc === undefined) {
 584             		self.modulefunc = 'main';
 585             	}
 586             	code = '{{#invoke:'+self.module+'|'+self.modulefunc;
 587             }
 588 
 589             self.submitlayout.setNotices(['Validating fields, please wait.']);
 590             self.submitlayout.fieldWidget.setDisabled(true);
 591 
 592             // setup template for submission
 593             self.tParams.forEach(function (param) {
 594                 if ( param.type === 'hidden' || (param.type !== 'group' && param.ooui.isDisabled() === false) ) {
 595                     var val,
 596                         $input,
 597                         // use separate error tracking for each input
 598                         // or every input gets flagged as an error
 599                         error = '';
 600 
 601                     if (param.type === 'fixed' || param.type === 'hidden') {
 602                         val = param.def;
 603                     } else {
 604                         $input = $('#' + helper.getId.call(self, param.name) + ' input');
 605 
 606                         if (param.type === 'buttonselect') {
 607                             val = param.ooui.findSelectedItem();
 608                             if (val !== null) {
 609                                 val = val.getData();
 610                             }
 611                         } else {
 612                             val = param.ooui.getValue();
 613                         }
 614 
 615                         if (param.type === 'int') {
 616                             val = val.split(',').join('');
 617                         } else if (param.type === 'check') {
 618                             val = param.ooui.isSelected();
 619 
 620                             if (param.range) {
 621                                 val = param.range.split(',')[val ? 0 : 1];
 622                             }
 623                         } else if (param.type === 'toggleswitch' || param.type === 'togglebutton') {
 624                             if (param.range) {
 625                                 val = param.range.split(',')[val ? 0 : 1];
 626                             }
 627                         }
 628 
 629                         // Check input is valid (based on widgets validation)
 630                         if ( !!param.ooui.hasFlag && param.ooui.hasFlag('invalid') && param.type !== 'article') {
 631                             error = param.error;
 632                         } else if ( param.type === 'article' && param.ooui.validateTitle && val.length > 0 ) {
 633                             var api = param.ooui.getApi(),
 634                                 prms = {
 635                                     action: 'query',
 636                                     prop: [],
 637                                     titles: [ param.ooui.getValue() ]
 638                                 };
 639 
 640                             var prom = new Promise ( function (resolve,reject) {
 641                                 api.get(prms).then( function (ret) {
 642                                     if ( ret.query.pages && Object.keys(ret.query.pages).length ) {
 643                                         var nspaces = param.ooui.namespace.split('|'), allNS = false;
 644                                         if (nspaces.indexOf('*') >= 0) {
 645                                             allNS = true;
 646                                         }
 647                                         nspaces = nspaces.map(function (ns) {return parseInt(ns,10);});
 648                                         for (var pgID in ret.query.pages) {
 649                                             if ( ret.query.pages.hasOwnProperty(pgID) && ret.query.pages[pgID].missing!== '' ) {
 650                                                 if ( allNS ) {
 651                                                     resolve();
 652                                                 }
 653                                                 if ( ret.query.pages[pgID].ns !== undefined && nspaces.indexOf(ret.query.pages[pgID].ns) >= 0 ) {
 654                                                     resolve();
 655                                                 }
 656                                             }
 657                                         }
 658                                         reject(param);
 659                                     } else {
 660                                         reject(param);
 661                                     }
 662                                 });
 663                             });
 664                             apicalls.push(prom);
 665                         }
 666 
 667                         if (error) {
 668                             param.layout.setErrors([error]);
 669                             if (param.ooui.setValidityFlag !== undefined) {
 670                                 param.ooui.setValidityFlag(false);
 671                             }
 672                             // TODO: Remove jsInvalid classes?
 673                             $input.addClass('jcInvalid');
 674                             formErrors.push( param.label[0].textContent + ': ' + error );
 675                         } else {
 676                             param.layout.setErrors([]);
 677                             if (param.ooui.setValidityFlag !== undefined) {
 678                                 param.ooui.setValidityFlag(true);
 679                             }
 680                             // TODO: Remove jsInvalid classes?
 681                             $input.removeClass('jcInvalid');
 682 
 683                             // Save current parameter value
 684                             paramVals[param.name] = val;
 685                             
 686                             // Save current parameter value for later calculator usage.
 687                             //window.localStorage.setItem(helper.getId.call(self, param.name), val);
 688                         }
 689                     }
 690                     
 691                     code += '|' + param.name + '=' + val;
 692                 }
 693             });
 694 
 695             Promise.all(apicalls).then( function (vals) {
 696                 // All article fields valid
 697                 self.submitlayout.setNotices([]);
 698                 self.submitlayout.fieldWidget.setDisabled(false);
 699 
 700                 if (formErrors.length > 0) {
 701                     self.submitlayout.setErrors(formErrors);
 702                     helper.showError.call(self, 'One or more fields contains an invalid value.');
 703                     return;
 704                 }
 705 
 706                 self.submitlayout.setErrors([]);
 707 
 708                 // Save all values to localStorage
 709                 if (!rs.hasLocalStorage()) {
 710                     console.warn('Browser does not support localStorage, inputs will not be saved.');
 711                 } else {
 712                     mw.log('Saving inputs to localStorage');
 713                     paramVals.autosubmit = !!self.autosubmit;
 714                     localStorage.setItem( self.localname, JSON.stringify(paramVals) );
 715                     localStorage.setItem( calcautostorage, paramVals.autosubmit );
 716                 }
 717 
 718                 code += '}}';
 719                 helper.loadTemplate.call(self, code);
 720 
 721             }, function (errparam) {
 722                 // An article field is invalid
 723                 self.submitlayout.setNotices([]);
 724                 self.submitlayout.fieldWidget.setDisabled(false);
 725 
 726                 errparam.layout.setErrors([errparam.error]);
 727                 formErrors.push( errparam.label[0].textContent + ': ' + errparam.error );
 728 
 729                 self.submitlayout.setErrors(formErrors);
 730                 helper.showError.call(self, 'One or more fields contains an invalid value.');
 731                 return;
 732             });
 733         },
 734 
 735         /**
 736          * Parse the template used to display the result of the form
 737          *
 738          * @param code {string} Wikitext to send to the API for parsing
 739          */
 740         loadTemplate: function (code) {
 741             var self = this,
 742                 params = {
 743                     action: 'parse',
 744                     text: code,
 745                     prop: 'text|limitreportdata',
 746                     title: mw.config.get('wgPageName'),
 747                     disablelimitreport: 'true',
 748                     contentmodel: 'wikitext',
 749                     format: 'json'
 750                 };
 751             
 752             // experimental support for using VE to parse calc templates
 753             if (!!mw.util.getParamValue('vecalc')) {
 754                 params = {
 755                     action: 'visualeditor',
 756                     // has to be a mainspace page or VE won't work
 757                     page: 'No page',
 758                     paction: 'parsefragment',
 759                     wikitext: code,
 760                     format: 'json'
 761                 };
 762             }
 763 
 764             $('#' + self.form + ' .jcSubmit')
 765             	.data('oouiButton')
 766                 .setDisabled(true);
 767 
 768             // @todo time how long these calls take
 769             $.post('/api.php?rswcalcautosubmit='+self.autosubmit, params)
 770                 .done(function (response) {
 771                     var html;
 772                     
 773                     if (!!mw.util.getParamValue('vecalc')) {
 774                         // strip body tag
 775                         html = $(response.visualeditor.content).contents();
 776                     } else {
 777                         html = response.parse.text['*'];
 778                     }
 779                     if (response.parse.limitreportdata) {
 780                         var logs = response.parse.limitreportdata.filter(function(e){return e.name === 'scribunto-limitreport-logs'});
 781                         if (logs.length>0) {
 782                             var log_str = ['Scribunto logs:'];
 783                             logs.forEach(function(log){
 784                                 var i = 0;
 785                                 while (log.hasOwnProperty(''+i)) {
 786                                     log_str.push(log[''+i]);
 787                                     i++;
 788                                 }
 789                             });
 790                             console.log(log_str.join('\n'));
 791                         }
 792                     }
 793                     
 794                     helper.dispResult.call(self, html);
 795                 })
 796                 .fail(function (_, error) {
 797                     $('#' + self.form + ' .jcSubmit')
 798             			.data('oouiButton')
 799                 		.setDisabled(false);
 800                     helper.showError.call(self, error);
 801                 });
 802         },
 803 
 804         /**
 805          * Display the calculator result on the page
 806          *
 807          * @param response {String} A string representing the HTML to be added to the page
 808          */
 809         dispResult: function (html) {
 810         	var self = this;
 811             $('#' + self.form + ' .jcSubmit')
 812             	.data('oouiButton')
 813                 .setDisabled(false);
 814 
 815             $('#bodyContent')
 816                 .find('#' + this.result)
 817                     .empty()
 818                     .removeClass('jcError')
 819                     .html(html);
 820             
 821             // allow scripts to hook into form submission
 822             mw.hook('rscalc.submit').fire();
 823 
 824             mw.loader.using('jquery.tablesorter', function () {
 825                 $('table.sortable:not(.jquery-tablesorter)').tablesorter();
 826             });
 827             mw.loader.using('jquery.makeCollapsible', function () {
 828                 $('.mw-collapsible').makeCollapsible();
 829             });
 830             if ($('.rsw-chartjs-config').length) {
 831     			mw.loader.load('ext.gadget.Charts-core');
 832             }
 833         },
 834 
 835         /**
 836          * Sanitise any HTML used in labels
 837          *
 838          * @param html {string} A HTML string to be sanitised
 839          * @returns {jQuery.object} A jQuery object representing the sanitised HTML
 840          */
 841         sanitiseLabels: function (html) {
 842             var whitelistAttrs = [
 843                     // mainly for span/div tags
 844                     'style',
 845                     // for anchor tags
 846                     'href',
 847                     'title',
 848                     // for img tags
 849                     'src',
 850                     'alt',
 851                     'height',
 852                     'width',
 853                     // misc
 854                     'class'
 855                 ],
 856                 whitelistTags = [
 857                     'a',
 858                     'span',
 859                     'div',
 860                     'img',
 861                     'strong',
 862                     'b',
 863                     'em',
 864                     'i',
 865                     'br'
 866                 ],
 867                 // parse the HTML string, removing script tags at the same time
 868                 $html = $.parseHTML(html, /* document */ null, /* keepscripts */ false),
 869                 // append to a div so we can navigate the node tree
 870                 $div = $('<div>').append($html);
 871 
 872             $div.find('*').each(function () {
 873                 var $this = $(this),
 874                     tagname = $this.prop('tagName').toLowerCase(),
 875                     attrs,
 876                     array,
 877                     href;
 878 
 879                 if (whitelistTags.indexOf(tagname) === -1) {
 880                     mw.log('Disallowed tagname: ' + tagname);
 881                     $this.remove();
 882                     return;
 883                 }
 884 
 885                 attrs = $this.prop('attributes');
 886                 array = Array.prototype.slice.call(attrs);
 887 
 888                 array.forEach(function (attr) {
 889                     if (whitelistAttrs.indexOf(attr.name) === -1) {
 890                         mw.log('Disallowed attribute: ' + attr.name + ', tagname: ' + tagname);
 891                         $this.removeAttr(attr.name);
 892                         return;
 893                     }
 894 
 895                     // make sure there's nasty in nothing in href attributes
 896                     if (attr.name === 'href') {
 897                         href = $this.attr('href');
 898 
 899                         if (
 900                             // disable warnings about script URLs
 901                             // jshint -W107
 902                             href.indexOf('javascript:') > -1 ||
 903                             // the mw sanitizer doesn't like these
 904                             // so lets follow suit
 905                             // apparently it's something microsoft dreamed up
 906                             href.indexOf('vbscript:') > -1
 907                             // jshint +W107
 908                         ) {
 909                             mw.log('Script URL detected in ' + tagname);
 910                             $this.removeAttr('href');
 911                         }
 912                     }
 913                 });
 914             });
 915 
 916             return $div.contents();
 917         },
 918 
 919         /**
 920          * Handlers for parameter input types
 921          */
 922         tParams: {
 923             /**
 924              * Handler for 'fixed' inputs
 925              *
 926              * @param param {object} An object containing the configuration of a parameter
 927              * @returns {OOUI.object} A OOUI object containing the new FieldLayout
 928              */
 929             fixed: function (param) {
 930                 var layconf = {
 931                         label: new OO.ui.HtmlSnippet(param.label),
 932                         align: 'right',
 933                         classes: ['jsCalc-field', 'jsCalc-field-fixed'],
 934                         value: param.def
 935                     };
 936 
 937                 if (param.help) {
 938                     layconf.helpInline = param.inlhelp;
 939                     layconf.help = new OO.ui.HtmlSnippet(param.help);
 940                 }
 941 
 942 				param.ooui = new OO.ui.LabelWidget({ label: param.def });
 943 				return new OO.ui.FieldLayout(param.ooui, layconf);
 944             },
 945 
 946             /**
 947              * Handler for select dropdowns
 948              * 
 949              * @param param {object} An object containing the configuration of a parameter
 950              * @param id {String} A string representing the id to be added to the input
 951              * @returns {OOUI.object} A OOUI object containing the new FieldLayout
 952              */
 953             select: function (param, id) {
 954                 var self = this,
 955                     conf = {
 956                         label: 'Select an option',
 957                         options: [],
 958                         name: id,
 959                         id: id,
 960                         value: param.def,
 961                         dropdown: {
 962                         	$overlay: true
 963                         }
 964                     },
 965                     layconf = {
 966                         label: new OO.ui.HtmlSnippet(param.label),
 967                         align: 'right',
 968                         classes: ['jsCalc-field', 'jsCalc-field-select']
 969                     },
 970                     opts = param.range.split(/\s*,\s*/),
 971                     def = opts[0];
 972                 param.error = 'Not a valid selection';
 973 
 974                 if (param.help) {
 975                     layconf.helpInline = param.inlhelp;
 976                     layconf.help = new OO.ui.HtmlSnippet(param.help);
 977                 }
 978 
 979                 opts.forEach(function (opt) {
 980                     var op = { data: opt, label: opt };
 981 
 982                     if (opt === param.def) {
 983                         op.selected = true;
 984                         def = opt;
 985                     }
 986 
 987                     conf.options.push(op);
 988                 });
 989 
 990                 param.toggles = helper.parseToggles(param.rawtogs, def);
 991 
 992 				param.ooui = new OO.ui.DropdownInputWidget(conf);
 993                 if ( Object.keys(param.toggles).length > 0 ) {
 994                     param.ooui.on('change', function (value) {
 995                         helper.toggle.call(self, value, param.toggles);
 996                     });
 997                 }
 998                 return new OO.ui.FieldLayout(param.ooui, layconf);
 999             },
1000 
1001 
1002             /**
1003              * Handler for button selects
1004              *
1005              * @param param {object} An object containing the configuration of a parameter
1006              * @param id {String} A string representing the id to be added to the input
1007              * @returns {OOUI.object} A OOUI object containing the new FieldLayout
1008              */
1009             buttonselect: function (param, id) {
1010                 var self = this,
1011                     buttons = {},
1012                     conf = {
1013                         label:'Select an option',
1014                         items: [],
1015                         id: id
1016                     },
1017                     layconf = {
1018                         label: new OO.ui.HtmlSnippet(param.label),
1019                         align: 'right',
1020                         classes: ['jsCalc-field', 'jsCalc-field-buttonselect']
1021                     },
1022                     opts = param.range.split(/\s*,\s*/),
1023                     def;
1024                 param.error = 'Please select a valid option';
1025 
1026                 if (param.help) {
1027                     layconf.helpInline = param.inlhelp;
1028                     layconf.help = new OO.ui.HtmlSnippet(param.help);
1029                 }
1030 
1031                 opts.forEach(function (opt) {
1032                     var opid = opt.replace(/[^a-zA-Z0-9]/g, '');
1033                     buttons[opid] = new OO.ui.ButtonOptionWidget({data:opt, label:opt, title:opt});
1034                     conf.items.push(buttons[opid]);
1035                 });
1036 
1037                 if (param.def.length > 0 && opts.indexOf(param.def) > -1) {
1038                     def = param.def;
1039                 } else {
1040                     def = opts[0];
1041                 }
1042 
1043                 param.toggles = helper.parseToggles(param.rawtogs, def);
1044 
1045                 param.ooui = new OO.ui.ButtonSelectWidget(conf);
1046                 param.ooui.selectItemByData(def);
1047                 if ( Object.keys(param.toggles).length > 0 ) {
1048                     param.ooui.on('choose', function (button) {
1049                         var item = button.getData();
1050                         helper.toggle.call(self, item, param.toggles);
1051                     });
1052                 }
1053                 return new OO.ui.FieldLayout(param.ooui, layconf);
1054             },
1055 
1056             /**
1057              * Handler for comboboxes
1058              * 
1059              * @param param {object} An object containing the configuration of a parameter
1060              * @param id {String} A string representing the id to be added to the input
1061              * @returns {OOUI.object} A OOUI object containing the new FieldLayout
1062              */
1063             combobox: function (param, id) {
1064                 var self = this,
1065                     conf = {
1066                         placeholder: 'Enter filter name',
1067                         options: [],
1068                         name: id,
1069                         id: id,
1070                         menu: { filterFromInput: true },
1071                         value: param.def,
1072                         $overlay: true
1073                     },
1074                     layconf = {
1075                         label: new OO.ui.HtmlSnippet(param.label),
1076                         align: 'right',
1077                         classes: ['jsCalc-field', 'jsCalc-field-combobox']
1078                     },
1079                     opts = param.range.split(/\s*,\s*/),
1080                     def = opts[0];
1081                 param.error = 'Not a valid selection';
1082 
1083                 if (param.help) {
1084                     layconf.helpInline = param.inlhelp;
1085                     layconf.help = new OO.ui.HtmlSnippet(param.help);
1086                 }
1087 
1088                 opts.forEach(function (opt) {
1089                     var op = { data: opt, label: opt };
1090 
1091                     if (opt === param.def) {
1092                         op.selected = true;
1093                         def = opt;
1094                     }
1095 
1096                     conf.options.push(op);
1097                 });
1098 
1099                 var isvalid = function (val) {return opts.indexOf(val) < 0 ? false : true;};
1100                 conf.validate = isvalid;
1101 
1102                 param.toggles = helper.parseToggles(param.rawtogs, def);
1103 
1104 				param.ooui = new OO.ui.ComboBoxInputWidget(conf);
1105                 if ( Object.keys(param.toggles).length > 0 ) {
1106                     param.ooui.on('change', function (value) {
1107                         helper.toggle.call(self, value, param.toggles);
1108                     });
1109                 }
1110                 return new OO.ui.FieldLayout(param.ooui, layconf);
1111             },
1112 
1113             /**
1114              * Handler for checkbox inputs
1115              * 
1116              * @param param {object} An object containing the configuration of a parameter
1117              * @param id {String} A string representing the id to be added to the input
1118              * @returns {OOUI.object} A OOUI object containing the new FieldLayout
1119              */
1120             check: function (param, id) {
1121                 var self = this,
1122                     conf = {
1123                         name: id,
1124                         id: id
1125                     },
1126                     layconf = {
1127                         label: new OO.ui.HtmlSnippet(param.label),
1128                         align: 'right',
1129                         classes: ['jsCalc-field', 'jsCalc-field-check']
1130                     };
1131                 param.toggles = helper.parseToggles(param.rawtogs, 'true');
1132                 param.error = 'Unknown error';
1133 
1134                 if (param.help) {
1135                     layconf.helpInline = param.inlhelp;
1136                     layconf.help = new OO.ui.HtmlSnippet(param.help);
1137                 }
1138 
1139                 if ( param.def === 'true' ||
1140                     (param.range !== undefined && param.def === param.range.split(/\s*,\s*/)[0]) ) {
1141                     conf.selected = true;
1142                 }
1143 
1144 				param.ooui = new OO.ui.CheckboxInputWidget(conf);
1145                 if ( Object.keys(param.toggles).length > 0 ) {
1146                     param.ooui.on('change', function (selected) {
1147                         if (selected) {
1148                             helper.toggle.call(self, 'true', param.toggles);
1149                         } else {
1150                             helper.toggle.call(self, 'false', param.toggles);
1151                         }
1152                     });
1153                 }
1154                 return new OO.ui.FieldLayout(param.ooui, layconf);
1155             },
1156 
1157             /**
1158              * Handler for toggle switch inputs
1159              *
1160              * @param param {object} An object containing the configuration of a parameter
1161              * @param id {String} A string representing the id to be added to the input
1162              * @returns {OOUI.object} A OOUI object containing the new FieldLayout
1163              */
1164             toggleswitch: function (param, id) {
1165                 var self = this,
1166                     conf = { id: id },
1167                     layconf = {
1168                         label: new OO.ui.HtmlSnippet(param.label),
1169                         align: 'right',
1170                         classes: ['jsCalc-field', 'jsCalc-field-toggleswitch']
1171                     };
1172                 param.toggles = helper.parseToggles(param.rawtogs, 'true');
1173                 param.error = 'Unknown error';
1174 
1175                 if (param.help) {
1176                     layconf.helpInline = param.inlhelp;
1177                     layconf.help = new OO.ui.HtmlSnippet(param.help);
1178                 }
1179 
1180                 if ( param.def === 'true' ||
1181                     (param.range !== undefined && param.def === param.range.split(/\s*,\s*/)[0]) ) {
1182                     conf.value = true;
1183                 }
1184 
1185                 param.ooui = new OO.ui.ToggleSwitchWidget(conf);
1186                 if ( Object.keys(param.toggles).length > 0 ) {
1187                     param.ooui.on('change', function (selected) {
1188                         if (selected) {
1189                             helper.toggle.call(self, 'true', param.toggles);
1190                         } else {
1191                             helper.toggle.call(self, 'false', param.toggles);
1192                         }
1193                     });
1194                 }
1195                 return new OO.ui.FieldLayout(param.ooui, layconf);
1196             },
1197 
1198             /**
1199              * Handler for toggle button inputs
1200              *
1201              * @param param {object} An object containing the configuration of a parameter
1202              * @param id {String} A string representing the id to be added to the input
1203              * @returns {OOUI.object} A OOUI object containing the new FieldLayout
1204              */
1205             togglebutton: function (param, id) {
1206                 var self = this,
1207                     conf = {
1208                         id: id,
1209                         label: new OO.ui.HtmlSnippet(param.label)
1210                     },
1211                     layconf = {
1212                         label:'',
1213                         align: 'right',
1214                         classes: ['jsCalc-field', 'jsCalc-field-togglebutton']
1215                     };
1216                 param.toggles = helper.parseToggles(param.rawtogs, 'true');
1217                 param.error = 'Unknown error';
1218 
1219                 if (param.help) {
1220                     layconf.helpInline = param.inlhelp;
1221                     layconf.help = new OO.ui.HtmlSnippet(param.help);
1222                 }
1223 
1224                 if ( param.def === 'true' ||
1225                     (param.range !== undefined && param.def === param.range.split(/\s*,\s*/)[0]) ) {
1226                     conf.value = true;
1227                 }
1228 
1229                 param.ooui = new OO.ui.ToggleButtonWidget(conf);
1230                 if ( Object.keys(param.toggles).length > 0 ) {
1231                     param.ooui.on('change', function (selected) {
1232                         if (selected) {
1233                             helper.toggle.call(self, 'true', param.toggles);
1234                         } else {
1235                             helper.toggle.call(self, 'false', param.toggles);
1236                         }
1237                     });
1238                 }
1239                 return new OO.ui.FieldLayout(param.ooui, layconf);
1240             },
1241 
1242             /**
1243              * Handler for hiscore inputs
1244              * 
1245              * @param param {object} An object containing the configuration of a parameter
1246              * @param id {String} A string representing the id to be added to the input
1247              * @returns {OOUI.object} A OOUI object containing the new FieldLayout
1248              */
1249             hs: function (param, id) {
1250                 var self = this,
1251                     layconf = {
1252                         label: new OO.ui.HtmlSnippet(param.label),
1253                         align:'right',
1254                         classes: ['jsCalc-field', 'jsCalc-field-hs']
1255                     },
1256                     lookups = {},
1257                     range = param.range.split(';'),
1258                     input1 = new OO.ui.TextInputWidget({type: 'text', id: id, name: id, value:param.def}),
1259 					button1 = new OO.ui.ButtonInputWidget({ label: 'Lookup', id: id+'-button', name:  id+'-button', classes: ['jsCalc-field-hs-lookup'], data: {param: param.name} });
1260 
1261                 if (param.help) {
1262                     layconf.helpInline = param.inlhelp;
1263                     layconf.help = new OO.ui.HtmlSnippet(param.help);
1264                 }
1265 
1266                 var layout = new OO.ui.ActionFieldLayout(input1, button1, layconf);
1267 
1268 				var lookupHS = function(event) {
1269 					var $t = $(event.target),
1270 						lookup = self.lookups[button1.getData().param],
1271                         // replace spaces with _ for the query
1272                         name = $('#' + lookup.id + ' input')
1273                             .val()
1274                             // @todo will this break for players with multiple spaces
1275                             //       in their name? e.g. suomi's old display name
1276                             .replace(/\s+/g, '_'),
1277                         button = lookup.button;
1278 					button.setDisabled(true);
1279                     $.ajax({
1280 						url: '/cors/m=hiscore_oldschool/index_lite.ws?player=' + name,
1281 						dataType: 'text',
1282 						async: true,
1283 						timeout: 10000 // msec
1284 					}).done(function (data) {
1285 						var hsdata;
1286 
1287 						hsdata = data.trim()
1288 							.split(/\n+/g);
1289 
1290 						lookup.params.forEach(function (param) {
1291 							var id = helper.getId.call(self, param.param),
1292 								$input = $('#' + id + ' input'),
1293 								tParam = null,
1294                                 val;
1295 							self.tParams.forEach(function(p) {
1296 								if (p.name === param.param) {
1297 									tParam = p;
1298 								}
1299 							});
1300 							if (tParam === null) {
1301 								return;
1302 							}
1303 							if (isNaN(param.skill)) {
1304                                 val = param.skill;
1305 								//tParam.ooui.setValue(param.skill);
1306 							} else {
1307                                 val = hsdata[param.skill].split(',')[param.val];
1308 								//tParam.ooui.setValue(hsdata[param.skill].split(',')[param.val]);
1309 							}
1310                             if (!!tParam.ooui.setValue) {
1311                                 tParam.ooui.setValue(val);
1312                             } else if (!!tParam.ooui.selectItemByData) {
1313                                 tParam.ooui.selectItemByData(val);
1314                             } else if (tParam.type === 'fixed') {
1315                                 tParam.ooui.setLabel(val);
1316                             }
1317 						});
1318 					
1319 						// store in localStorage for future use
1320                         if (rs.hasLocalStorage()) {
1321                             self.lsRSN = name;
1322                             localStorage.setItem('rsn', name);
1323                         }
1324 
1325 						button.setDisabled(false);
1326                         layout.setErrors([]);
1327 					})
1328 					.fail(function (xhr, status) {
1329 						button.setDisabled(false);
1330 
1331                         var err = 'The player "' + name + '" does not exist, is banned or unranked, or we couldn\'t fetch your hiscores. Please enter the data manually.';
1332                         console.warn(status);
1333                         layout.setErrors([err]);
1334 						helper.showError.call(self, err);
1335 					});
1336 				};
1337 					
1338 				button1.$element.click(lookupHS);
1339 				input1.$element.keydown(function(event){
1340 					if (event.which === 13) {
1341 						lookupHS(event);
1342 						event.preventDefault();
1343 					}
1344 				});
1345 				
1346                 // Use rsn loaded from localstorage
1347                 if (self.lsRSN) {
1348                     input1.setValue(self.lsRSN);
1349                 }
1350 
1351                 lookups[param.name] = {
1352                     id: id,
1353                     button: button1,
1354                     params: []
1355                 };
1356                 
1357                 range.forEach(function (el) {
1358                     // to catch empty strings
1359                     if (!el) {
1360                         return;
1361                     }
1362 
1363                     var spl = el.split(',');
1364 
1365                     lookups[param.name].params.push({
1366                         param: spl[0],
1367                         skill: spl[1],
1368                         val: spl[2]
1369                     });
1370                 });
1371 
1372                 // merge lookups into one object
1373                 if (!self.lookups) {
1374                     self.lookups = lookups;
1375                 } else {
1376                     self.lookups = $.extend(self.lookups, lookups);
1377                 }
1378 				
1379 				param.ooui = input1;
1380 				param.oouiButton = button1;
1381 
1382                 return layout;
1383             },
1384 
1385             /**
1386              * Handler for Runescape name inputs
1387              *
1388              * @param param {object} An object containing the configuration of a parameter
1389              * @param id {String} A string representing the id to be added to the input
1390              * @returns {OOUI.object} A OOUI object containing the new FieldLayout
1391              */
1392             rsn: function (param, id) {
1393                 var self = this,
1394                     conf = {
1395                         type: 'text',
1396                         name: id,
1397                         id: id,
1398                         placeholder: 'Enter runescape name',
1399                         spellcheck: false,
1400                         maxLength: 12,
1401                         value: param.def
1402                     },
1403                     layconf = {
1404                         label: new OO.ui.HtmlSnippet(param.label),
1405                         align: 'right',
1406                         classes: ['jsCalc-field', 'jsCalc-field-string']
1407                     };
1408                 param.error = 'Invalid runescape name: RS names must be 1-12 characters long, can only contain letters, numbers, spaces, dashes and underscores. Names containing Mod are also not allowed.';
1409 
1410                 if (param.help) {
1411                     layconf.helpInline = param.inlhelp;
1412                     layconf.help = new OO.ui.HtmlSnippet(param.help);
1413                 }
1414 
1415                 // Use rsn loaded from localstorage, if available
1416                 if (self.lsRSN) {
1417                     conf.value = self.lsRSN;
1418                 }
1419 
1420                 var validrsn = function (name) {
1421                     if ( name.search( /[^0-9a-zA-Z\-_\s]/ ) >= 0 ) {
1422                         return false;
1423                     } else {
1424                         if ( name.toLowerCase().search( /(^mod\s|\smod\s|\smod$)/ ) >= 0 ) {
1425                             return false;
1426                         } else {
1427                             return true;
1428                         }
1429                     }
1430                 };
1431                 conf.validate = validrsn;
1432 
1433                 param.ooui = new OO.ui.TextInputWidget(conf);
1434                 return new OO.ui.FieldLayout(param.ooui, layconf);
1435             },
1436             
1437             
1438             /**
1439              * Handler for integer inputs
1440              * 
1441              * @param param {object} An object containing the configuration of a parameter
1442              * @param id {String} A string representing the id to be added to the input
1443              * @returns {OOUI.object} A OOUI object containing the new FieldLayout
1444              */
1445             int: function (param, id) {
1446                 var self = this,
1447                     rng = helper.genRange(param.range, 'int'),
1448                     conf = {
1449                         min:rng[0],
1450                         max:rng[1],
1451                         step:rng[2],
1452                         showButtons:true,
1453                         buttonStep:rng[3],
1454                         allowInteger:true,
1455                         name: id,
1456                         id: id,
1457                         value: param.def || 0
1458                     },
1459                     layconf = {
1460                         label: new OO.ui.HtmlSnippet(param.label),
1461                         align: 'right',
1462                         classes: ['jsCalc-field', 'jsCalc-field-int']
1463                     },
1464                     error = 'Invalid integer. Must be between ' + rng[0] + ' and ' + rng[1];
1465                 param.toggles = helper.parseToggles(param.rawtogs, 'not0');
1466 
1467                 if (param.help) {
1468                     layconf.helpInline = param.inlhelp;
1469                     layconf.help = new OO.ui.HtmlSnippet(param.help);
1470                 }
1471 
1472                 if ( rng[2] > 1 ) {
1473                     error += ' and a muiltiple of ' + rng[2];
1474                 }
1475                 param.error = error;
1476 
1477 
1478 				param.ooui = new OO.ui.NumberInputWidget(conf);
1479                 if ( Object.keys(param.toggles).length > 0 ) {
1480                     param.ooui.on('change', function (value) {
1481                         helper.toggle.call(self, value, param.toggles);
1482                     });
1483                 }
1484                 return new OO.ui.FieldLayout(param.ooui, layconf);
1485             },
1486             
1487             /**
1488              * Handler for number inputs
1489              * 
1490              * @param param {object} An object containing the configuration of a parameter
1491              * @param id {String} A string representing the id to be added to the input
1492              * @returns {OOUI.object} A OOUI object containing the new FieldLayout
1493              */
1494             number: function (param, id) {
1495                 var self = this,
1496                     rng = helper.genRange(param.range, 'number'),
1497                     conf = {
1498                         min:rng[0],
1499                         max:rng[1],
1500                         step:rng[2],
1501                         showButtons:true,
1502                         buttonStep:rng[3],
1503                         name:id,
1504                         id:id,
1505                         value:param.def || 0
1506                     },
1507                     layconf = {
1508                         label: new OO.ui.HtmlSnippet(param.label),
1509                         align: 'right',
1510                         classes: ['jsCalc-field', 'jsCalc-field-number'],
1511                     };
1512                 param.toggles = helper.parseToggles(param.rawtogs, 'not0');
1513                 param.error = 'Invalid interger. Must be between ' + rng[0] + ' and ' + rng[1] + ' and a multiple of ' + rng[2];
1514 
1515                 if (param.help) {
1516                     layconf.helpInline = param.inlhelp;
1517                     layconf.help = new OO.ui.HtmlSnippet(param.help);
1518                 }
1519 
1520                 param.ooui = new OO.ui.NumberInputWidget(conf);
1521                 if ( Object.keys(param.toggles).length > 0 ) {
1522                     param.ooui.on('change', function (value) {
1523                         helper.toggle.call(self, value, param.toggles);
1524                     });
1525                 }
1526                 return new OO.ui.FieldLayout( param.ooui, layconf);
1527             },
1528 
1529             /**
1530              * Handler for article inputs
1531              *
1532              * @param param {object} An object containing the configuration of a parameter
1533              * @param id {String} A string representing the id to be added to the input
1534              * @returns {OOUI.object} A OOUI object containing the new FieldLayout
1535              */
1536             article: function (param, id) {
1537                 var self = this,
1538                     conf = {
1539                         addQueryInput: false,
1540                         excludeCurrentPage: true,
1541                         showMissing: false,
1542                         showDescriptions: true,
1543                         validateTitle: true,
1544                         relative: false,
1545                         id: id,
1546                         name: id,
1547                         placeholder: 'Enter page name',
1548                         value: param.def
1549                     },
1550                     layconf = {
1551                         label: new OO.ui.HtmlSnippet(param.label),
1552                         align:'right',
1553                         classes: ['jsCalc-field', 'jsCalc-field-article']
1554                     },
1555                     validNSnumbers = { '_*':'All', '_-2':'Media', '_-1':'Special', _0:'(Main)', _1:'Talk', _2:'User', _3:'User talk', _4:'RuneScape', _5:'RuneScape talk', _6:'File', _7:'File talk', _8:'MediaWiki', _9:'MediaWiki talk', _10:'Template', _11:'Template talk',
1556                         _12:'Help', _13:'Help talk', _14:'Category', _15:'Category talk', _100:'Update', _101:'Update talk', _110:'Forum', _111:'Forum talk', _112:'Exchange', _113:'Exchange talk', _114:'Charm', _115:'Charm talk', _116:'Calculator', _117:'Calculator talk', _118:'Map', _119:'Map talk', _828:'Module', _829:'Module talk' },
1557                     validNSnames = { all:'*', media:-2, special:-1, main:0, '(main)':0, talk:1, user:2, 'user talk':3, runescape:4, 'runescape talk':5, file:6, 'file talk':7, mediawiki:8, 'mediawiki talk':9, template:10, 'template talk':11,
1558                         help:12, 'help talk':13, category:14, 'category talk':15, update:100, 'update talk':101, forum:110, 'forum talk':111, exchange:112, 'exchange talk':113, charm:114, 'charm talk':115, calculator:116, 'calculator talk':117, map:118, 'map talk':119, module:828, 'module talk':829 },
1559                     namespaces = '';
1560 
1561                 if (param.help) {
1562                     layconf.helpInline = param.inlhelp;
1563                     layconf.help = new OO.ui.HtmlSnippet(param.help);
1564                 }
1565 
1566                 if (param.range && param.range.length > 0) {
1567                     var names = param.range.split(/\s*,\s*/),
1568                     nsnumbers = [];
1569                     names.forEach( function (nmspace) {
1570                         nmspace = nmspace.toLowerCase();
1571                         if ( validNSnumbers['_'+nmspace] ) {
1572                             nsnumbers.push(nmspace);
1573                         } else if ( validNSnames[nmspace] ) {
1574                             nsnumbers.push( validNSnames[nmspace] );
1575                         }
1576                     });
1577                     if (nsnumbers.length < 1) {
1578                         conf.namespace = '0';
1579                         namespaces = '(Main) namespace';
1580                     } else if (nsnumbers.length < 2) {
1581                         conf.namespace = nsnumbers[0];
1582                         namespaces = nsnumbers[0] + ' namespace';
1583                     } else {
1584                         conf.namespace = nsnumbers.join('|');
1585                         var nsmap = function (num) {
1586                             return validNSnumbers['_'+num];
1587                         };
1588                         namespaces = nsnumbers.slice(0, -1).map(nsmap).join(', ') + ' or ' + nsnumbers.slice(-1).map(nsmap)[0] + ' namespaces';
1589                     }
1590                 } else if ( self.suggestns && self.suggestns.length > 0 ) {
1591                     var nsnumbers = [];
1592                     self.suggestns.forEach( function (nmspace) {
1593                         nmspace = nmspace.toLowerCase();
1594                         if ( validNSnumbers['_'+nmspace] ) {
1595                             nsnumbers.push(nmspace);
1596                         } else if ( validNSnames[nmspace] ) {
1597                             nsnumbers.push( validNSnames[nmspace] );
1598                         }
1599                     });
1600                     if (nsnumbers.length < 1) {
1601                         conf.namespace = '0';
1602                         namespaces = '(Main) namespace';
1603                     } else if (nsnumbers.length < 2) {
1604                         conf.namespace = nsnumbers[0];
1605                         namespaces = nsnumbers[0] + ' namespace';
1606                     } else {
1607                         conf.namespace = nsnumbers.join('|');
1608                         var nsmap = function (num) {
1609                             return validNSnumbers['_'+num];
1610                         };
1611                         namespaces = nsnumbers.slice(0, -1).map(nsmap).join(', ') + ' or ' + nsnumbers.slice(-1).map(nsmap)[0] + ' namespaces';
1612                     }
1613                 } else {
1614                     conf.namespace = '0';
1615                     namespaces = '(Main) namespace';
1616                 }
1617 
1618                 param.error = 'Invalid page or page is not in ' + namespaces;
1619 
1620                 param.ooui = new mw.widgets.TitleInputWidget(conf);
1621                 return new OO.ui.FieldLayout( param.ooui, layconf);
1622             },
1623 
1624             /**
1625              * Handler for group type params
1626              *
1627              * @param param {object} An object containing the configuration of a parameter
1628              * @param id {String} A string representing the id to be added to the input
1629              * @returns {OOUI.object} A OOUI object containing the new FieldLayout
1630              */
1631             group: function (param, id) {
1632                 param.ooui = new OO.ui.HorizontalLayout({id: id, classes: ['jsCalc-group']});
1633                 if (param.label !== param.name) {
1634                     var label = new OO.ui.LabelWidget({ label: new OO.ui.HtmlSnippet(param.label), classes:['jsCalc-grouplabel'] });
1635                     param.ooui.addItems([label]);
1636                 }
1637 
1638                 return param.ooui;
1639             },
1640             
1641             /**
1642              * Default handler for inputs
1643              * 
1644              * @param param {object} An object containing the configuration of a parameter
1645              * @param id {String} A string representing the id to be added to the input
1646              * @returns {OOUI.object} A OOUI object containing the new FieldLayout
1647              */
1648             def: function (param, id) {
1649                 var layconf = {
1650                     label: new OO.ui.HtmlSnippet(param.label),
1651                     align: 'right',
1652                     classes: ['jsCalc-field', 'jsCalc-field-string'],
1653                     value: param.def
1654                 };
1655                 param.error = 'Unknown error';
1656 
1657                 if (param.help) {
1658                     layconf.helpInline = param.inlhelp;
1659                     layconf.help = new OO.ui.HtmlSnippet(param.help);
1660                 }
1661 
1662 				param.ooui = new OO.ui.TextInputWidget({type: 'text', name: id, id: id});
1663                 return new OO.ui.FieldLayout(param.ooui, layconf);
1664             }
1665         }
1666     };
1667 
1668 /**
1669  * Create an instance of `Calc`
1670  * and parse the config stored in `elem`
1671  *
1672  * @param elem {Element} An Element representing the HTML tag that contains
1673  *                       the calculator's configuration
1674  */
1675 function Calc(elem) {
1676     var self = this,
1677         $elem = $(elem),
1678         lines,
1679         config;
1680         
1681     // support div tags for config as well as pre
1682     // be aware using div tags relies on wikitext for parsing
1683     // so you can't use anchor or img tags
1684     // use the wikitext equivalent instead
1685     if ($elem.children().length) {
1686         $elem = $elem.children();
1687         lines = $elem.html();
1688     } else {
1689         // .html() causes html characters to be escaped for some reason
1690         // so use .text() instead for <pre> tags
1691         lines = $elem.text();
1692     }
1693     
1694     lines = lines.split('\n');
1695     
1696     config = helper.parseConfig.call(this, lines);
1697 
1698     // Calc name for localstorage, keyed to calc id
1699     this.localname = calcstorage + '-' + config.form;
1700     
1701     // Load previous parameter values.
1702     if (!rs.hasLocalStorage()) {
1703         console.warn('Browser does not support localStorage');
1704     } else {
1705         mw.log('Loading previous calculator values');
1706         if ( config.autosubmit !== 'disabled' ) {
1707             config.autosubmit = localStorage.getItem(calcautostorage);
1708         }
1709         var calcdata = JSON.parse( localStorage.getItem(this.localname) ) || false;
1710         if (calcdata) {
1711             config.tParams.forEach( function(param) {
1712                 if (calcdata[param.name] !== undefined && calcdata[param.name] !== null) {
1713                     param.def = calcdata[param.name];
1714                 }
1715             });
1716         }
1717         self.lsRSN = localStorage.getItem('rsn');
1718         mw.log(config);
1719     }
1720 
1721     // merge config in
1722     $.extend(this, config);
1723 
1724     /**
1725      * @todo document
1726      */
1727     this.getInput = function (id) {
1728         if (id) {
1729             id = helper.getId.call(self, id);
1730             return $('#' + id);
1731         }
1732         
1733         return $('#jsForm-' + self.form).find('select, input');
1734     };
1735 }
1736 
1737 /**
1738  * Helper function for getting the id of an input
1739  *
1740  * @param id {string} The id of the input as specified by the calculator config.
1741  * @returns {string} The true id of the input with prefixes.
1742  */
1743 Calc.prototype.getId = function (id) {
1744     var self = this,
1745         inputId = helper.getId.call(self, id);
1746 
1747     return inputId;
1748 };
1749 
1750 /**
1751  * Build the calculator form
1752  */
1753 Calc.prototype.setupCalc = function () {
1754     var self = this,
1755         fieldset = new OO.ui.FieldsetLayout({label: self.name, classes: ['jcTable'], id: 'jsForm-'+self.form}),
1756 		submitButton, submitButtonAction, autosubmit, paramChangeAction,
1757         groupkeys = {};
1758 
1759     // Used to store indexes of elements to toggle them later
1760     self.indexkeys = {};
1761 
1762     self.tParams.forEach(function (param, index) {
1763         // can skip any output here as the result is pulled from the
1764         // param default in the config on submission
1765         if (param.type === 'hidden') {
1766             return;
1767         }
1768 
1769         var id = helper.getId.call(self, param.name),
1770             method = helper.tParams[param.type] ?
1771                 param.type :
1772                 'def';
1773 
1774         // Generate list of items in group
1775         if (param.type === 'group') {
1776             var fields = param.range.split(/\s*,\s*/);
1777             fields.forEach( function (field) {
1778                 groupkeys[field] = index;
1779             });
1780         }
1781 
1782         param.layout = helper.tParams[method].call(self, param, id);
1783 
1784         if (param.type === 'semihidden') {
1785             param.layout.toggle(false);
1786         }
1787 		
1788         // Add to group or form
1789         if ( groupkeys[param.name] || groupkeys[param.name] === 0 ) {
1790             self.tParams[ groupkeys[param.name] ].ooui.addItems([param.layout]);
1791         } else {
1792             fieldset.addItems([param.layout]);
1793         }
1794 
1795         // Add item to indexkeys
1796         self.indexkeys[param.name] = index;
1797     });
1798 
1799     // Run toggle for each field, check validity
1800     self.tParams.forEach( function (param) {
1801         if (param.toggles && Object.keys(param.toggles).length > 0) {
1802             var val;
1803             if (param.type === 'buttonselect') {
1804                 val = param.ooui.findSelectedItem().getData();
1805             } else if (param.type === 'check') {
1806                 val = param.ooui.isSelected() ? 'true' : 'false';
1807             } else if (param.type === 'toggleswitch' || param.type === 'togglebutton') {
1808                 val = param.ooui.getValue() ? 'true' : 'false';
1809             } else {
1810                 val = param.ooui.getValue();
1811             }
1812             helper.toggle.call(self, val, param.toggles);
1813         }
1814         if (param.type === 'number' || param.type === 'int' || param.type === 'rsn') {
1815             param.ooui.setValidityFlag();
1816         }
1817     });
1818 
1819 	
1820 	submitButton = new OO.ui.ButtonInputWidget({ label: 'Submit', flags: ['primary', 'progressive'], classes: ['jcSubmit']});
1821 	submitButtonAction = function (){
1822                 helper.submitForm.call(self);
1823 	};
1824 	submitButton.on('click', submitButtonAction);
1825 	submitButton.$element.data('oouiButton', submitButton);
1826 
1827     self.submitlayout = new OO.ui.FieldLayout(submitButton, {label: ' ', align: 'right', classes: ['jsCalc-field', 'jsCalc-field-submit']});
1828 	fieldset.addItems([ self.submitlayout ]);
1829 
1830     // Auto-submit
1831     if (self.autosubmit !== 'disabled') {
1832         // Add toggle to fieldset
1833         autosubmit = new OO.ui.ToggleSwitchWidget({
1834             value: self.autosubmit === 'on' || self.autosubmit === 'true'
1835         });
1836         autosubmit.on('change', function (value) { self.autosubmit = value; });
1837         fieldset.addItems([ new OO.ui.FieldLayout(autosubmit, { label:'Auto-submit', align:'right', classes:['jsCalc-field', 'jsCalc-field-autosubmit'] }) ]);
1838 
1839         // Add event
1840         paramChangeAction = function (widget) {
1841             if (autosubmit.getValue()) {
1842                 if ( typeof widget.getFlagsa === 'undefined' || !widget.getFlags().includes('invalid')) {
1843                     helper.submitForm.call(self);
1844                 }
1845             }
1846         };
1847         
1848 		var timeout;
1849 		// We only want one of these pending at once
1850 		function timeoutFunc(param) {
1851 		    clearTimeout(timeout);
1852 		    timeout = setTimeout(paramChangeAction, 500, param);
1853 		}
1854 		
1855         self.tParams.forEach( function (param) {
1856             if (param.type === 'hidden' || param.type === 'hs' || param.type === 'group') {
1857                 return;
1858             } else if (param.type === 'buttonselect') {
1859                 param.ooui.on('select', timeoutFunc, [param.ooui]);
1860             }
1861             param.ooui.on('change', timeoutFunc, [param.ooui]);
1862         });
1863     }
1864     
1865     if (self.configError) {
1866         fieldset.$element.append('<br>', self.configError);
1867     }
1868 
1869     $('#bodyContent')
1870         .find('#' + self.form)
1871             .empty()
1872             .append(fieldset.$element);
1873 };
1874 
1875 /**
1876  * @todo
1877  */
1878 function lookupCalc(calcId) {
1879     return calcStore[calcId];
1880 }
1881 
1882 /**
1883  * @todo
1884  */
1885 function init() {
1886     // Initialises class changes
1887     helper.initClasses();
1888 
1889     $('.jcConfig').each(function () {
1890         var c = new Calc(this);
1891         c.setupCalc();
1892         
1893         calcStore[c.form] = c;
1894 
1895         // if (c.autosubmit === 'true' || c.autosubmit === true) {
1896         //     helper.submitForm.call(c);
1897         // }
1898     });
1899     
1900     // allow scripts to hook into calc setup completion
1901     mw.hook('rscalc.setupComplete').fire();
1902 }
1903 
1904 $(init);
1905 
1906 rs.calc = {};
1907 rs.calc.lookup = lookupCalc;
1908 
1909 // </nowiki>