MediaWiki:Gadget-calc-core.js

From RuneRealm Wiki

This is an old revision of this page, as edited by Alex (talk | contribs) at 17:12, 17 October 2024. The present address (URL) is a permanent link to this revision, which may differ significantly from the current revision.

Jump to navigation Jump to search

After saving, you may need to bypass your browser's cache to see the changes. For further information, see Wikipedia:Bypass your cache.

  • In most Windows and Linux browsers: Hold down Ctrl and press F5.
  • In Safari: Hold down ⇧ Shift and click the Reload button.
  • In Chrome and Firefox for Mac: Hold down both ⌘ Cmd+⇧ Shift and press R.
/** * Calc script for RuneScape Wiki *  * MAIN SCRIPT	https://runescape.wiki/w/MediaWiki:Gadget-calc.js *				https://runescape.wiki/w/MediaWiki:Gadget-calc.css * DUPLICATE TO	https://oldschool.runescape.wiki/w/MediaWiki:Gadget-calc.js *				https://oldschool.runescape.wiki/w/MediaWiki:Gadget-calc.css * make sure to update the hiscores URL for OSRS *  * 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 <https://runescape.wiki/w/RuneScape:Calculators/Form_calculators> * @see Tests <https://runescape.wiki/w/RuneScape:Calculators/Form_calculators/Tests> * * @license GLPv3 <https://www.gnu.org/licenses/gpl-3.0.en.html> * * @author Quarenon * @author TehKittyCat * @author Joeytje50 * @author Cook Me Plox * @author Gaz Lloyd * @author Cqm * @author Elessar2 * * @todo Whitelist domains for href attributes when sanitising HTML? * @todo if we get cross-wiki imports, add a way to change hiscores URL *//*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, mw, rswiki, rs, OO */'use strict';    /**     * Prefix of localStorage key for calc data. This is prepended to the form ID     * localStorage name for autosubmit setting     */var calcstorage = 'rsw-calcsdata',    calcautostorage = 'rsw-calcsdata-allautosub',    /**     * Caching for search suggestions     *     * @todo implement caching for mw.TitleInputWidget accroding to https://doc.wikimedia.org/mediawiki-core/master/js/#!/api/mw.widgets.TitleWidget-cfg-cache     */    cache = {},    /**     * Internal variable to store references to each calculator on the page.     */    calcStore = {},    /**     * Private helper methods for `Calc`     *     * Most methods here are called with `Function.prototype.call`     * and are passed an instance of `Calc` to access it's prototype     */    helper = {        /**         * Add/change functionality of mw/OO.ui classes         * Added support for multiple namespaces to mw.widgets.TitleInputWidget         */        initClasses: function () {            var hasOwn = Object.prototype.hasOwnProperty;            /**             * Get option widgets from the server response             * Changed to add support for multiple namespaces             *              * @param {Object} data Query result             * @return {OO.ui.OptionWidget[]} Menu items             */            mw.widgets.TitleInputWidget.prototype.getOptionsFromData = function (data) {                var i, len, index, pageExists, pageExistsExact, suggestionPage, page, redirect, redirects,                    currentPageName = new mw.Title( mw.config.get( 'wgRelevantPageName' ) ).getPrefixedText(),                    items = [],                    titles = [],                    titleObj = mw.Title.newFromText( this.getQueryValue() ),                    redirectsTo = {},                    pageData = {},                    namespaces = this.namespace.split('|').map(function (val) {return parseInt(val,10);});                if ( data.redirects ) {                    for ( i = 0, len = data.redirects.length; i < len; i++ ) {                        redirect = data.redirects[ i ];                        redirectsTo[ redirect.to ] = redirectsTo[ redirect.to ] || [];                        redirectsTo[ redirect.to ].push( redirect.from );                    }                }                for ( index in data.pages ) {                    suggestionPage = data.pages[ index ];                    // When excludeCurrentPage is set, don't list the current page unless the user has type the full title                    if ( this.excludeCurrentPage && suggestionPage.title === currentPageName && suggestionPage.title !== titleObj.getPrefixedText() ) {                        continue;                    }                    // When excludeDynamicNamespaces is set, ignore all pages with negative namespace                    if ( this.excludeDynamicNamespaces && suggestionPage.ns < 0 ) {                        continue;                    }                    pageData[ suggestionPage.title ] = {                        known: suggestionPage.known !== undefined,                        missing: suggestionPage.missing !== undefined,                        redirect: suggestionPage.redirect !== undefined,                        disambiguation: OO.getProp( suggestionPage, 'pageprops', 'disambiguation' ) !== undefined,                        imageUrl: OO.getProp( suggestionPage, 'thumbnail', 'source' ),                        description: suggestionPage.description,                        // Sort index                        index: suggestionPage.index,                        originalData: suggestionPage                    };                    // Throw away pages from wrong namespaces. This can happen when 'showRedirectTargets' is true                    // and we encounter a cross-namespace redirect.                    if ( this.namespace === null || namespaces.indexOf(suggestionPage.ns) >= 0 ) {                        titles.push( suggestionPage.title );                    }                    redirects = hasOwn.call( redirectsTo, suggestionPage.title ) ? redirectsTo[ suggestionPage.title ] : [];                    for ( i = 0, len = redirects.length; i < len; i++ ) {                        pageData[ redirects[ i ] ] = {                            missing: false,                            known: true,                            redirect: true,                            disambiguation: false,                            description: mw.msg( 'mw-widgets-titleinput-description-redirect', suggestionPage.title ),                            // Sort index, just below its target                            index: suggestionPage.index + 0.5,                            originalData: suggestionPage                        };                        titles.push( redirects[ i ] );                    }                }                titles.sort( function ( a, b ) {                    return pageData[ a ].index - pageData[ b ].index;                } );                // If not found, run value through mw.Title to avoid treating a match as a                // mismatch where normalisation would make them matching (T50476)                pageExistsExact = (                    hasOwn.call( pageData, this.getQueryValue() ) &&                    (                        !pageData[ this.getQueryValue() ].missing ||                        pageData[ this.getQueryValue() ].known                    )                );                pageExists = pageExistsExact || (                    titleObj &&                    hasOwn.call( pageData, titleObj.getPrefixedText() ) &&                    (                        !pageData[ titleObj.getPrefixedText() ].missing ||                        pageData[ titleObj.getPrefixedText() ].known                    )                );                if ( this.cache ) {                    this.cache.set( pageData );                }                // Offer the exact text as a suggestion if the page exists                if ( this.addQueryInput && pageExists && !pageExistsExact ) {                    titles.unshift( this.getQueryValue() );                }                for ( i = 0, len = titles.length; i < len; i++ ) {                    page = hasOwn.call( pageData, titles[ i ] ) ? pageData[ titles[ i ] ] : {};                    items.push( this.createOptionWidget( this.getOptionWidgetData( titles[ i ], page ) ) );                }                return items;            };        },        /**         * Parse the calculator configuration         *         * @param lines {Array} An array containing the calculator's configuration         * @returns {Object} An object representing the calculator's configuration         */        parseConfig: function (lines) {            var defConfig = {                    suggestns: [],                    autosubmit: 'off',                    name: 'Calculator'                },                config = {                    // this isn't in `defConfig`                    // as it'll get overridden anyway                    tParams: []                },                // used for debugging incorrect config names                validParams = [                    'form',                    'param',                    'result',                    'suggestns',                    'template',                    'module',                    'modulefunc',                    'name',                    'autosubmit'                ],                // used for debugging incorrect param types                validParamTypes = [                    'string',                    'article',                    'number',                    'int',                    'select',                    'buttonselect',                    'combobox',                    'check',                    'toggleswitch',                    'togglebutton',                    'hs',                    'rsn',                    'fixed',                    'hidden',                    'semihidden',                    'group'                ],                configError = false;            // parse the calculator's config            // @example param=arg1|arg1|arg3|arg4            lines.forEach(function (line) {                var temp = line.split('='),                    param,                    args;                // incorrect config                if (temp.length < 2) {                    return;                }                // an equals is used in one of the arguments                // @example HTML label with attributes                // so join them back together to preserve it                // this also allows support of HTML attributes in labels                if (temp.length > 2) {                    temp[1] = temp.slice(1,temp.length).join('=');                }                param = temp[0].trim().toLowerCase();                args = temp[1].trim();                if (validParams.indexOf(param) === -1) {                    // 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] !== '' && args[3] !== undefined) {                    // use console for easier debugging                    console.log('Unknown param type: ' + args[3]);                    configError = true;                    return;                }                var inlinehelp = false, help = '';                if (args[6]) {                    var tmphelp = args[6].split(/\s*=\s*/);                    if (tmphelp.length > 1) {                        if ( tmphelp[0] === 'inline' ) {                            inlinehelp = true;                            // Html etc can have = so join them back together                            tmphelp[1] = tmphelp.slice(1,tmphelp.length).join('=');                            help = helper.sanitiseLabels(tmphelp[1] || '');                        } else {                            // Html etc can have = so join them back together                            tmphelp[0] = tmphelp.join('=');                            help = helper.sanitiseLabels(tmphelp[0] || '');                        }                    } else {                        help = helper.sanitiseLabels(tmphelp[0] || '');                    }                }                config.tParams.push({                    name: mw.html.escape(args[0]),                    label: helper.sanitiseLabels(args[1] || args[0]),                    def: mw.html.escape(args[2] || ''),                    type: mw.html.escape(args[3] || ''),                    range: args[4] || '',                    rawtogs: args[5] || '',                    inlhelp: inlinehelp,                    help: help                });            });                        if (configError) {                config.configError = 'This calculator\'s config contains errors. Please report it ' +                    '<a href="/w/RuneScape:User_help" title="RuneScape:User help">here</a> ' +                    'or check the javascript console for details.';            }            config = $.extend(defConfig, config);            mw.log(config);            return config;        },        /**         * Generate a unique id for each input         *         * @param inputId {String} A string representing the id of an input         * @returns {String} A string representing the namespaced/prefixed id of an input         */        getId: function (inputId) {            return [this.form, this.result, inputId].join('-');        },        /**         * Output an error to the UI         *         * @param error {String} A string representing the error message to be output         */        showError: function (error) {            $('#' + this.result)                .empty()                .append(                    $('<strong>')                        .addClass('error')                        .text(error)                );        },        /**         * Toggle the visibility and enabled status of fields/groups         *         * @param item {String} A string representing the current value of the widget         * @param toggles {object} An object representing arrays of items to be toggled keyed by widget values         */        toggle: function (item, toggles) {            var self = this;            var togitem = function (widget, show) {                var param = self.tParams[ self.indexkeys[widget] ];                if (param.type === 'group') {                    param.ooui.toggle(show);                    param.ooui.getItems().forEach(function (child) {                        if (!!child.setDisabled) {                            child.setDisabled(!show);                        } else if (!!child.getField().setDisabled) {                            child.getField().setDisabled(!show);                        }                    });                } else if ( param.type === 'semihidden' ) {                    if (!!param.ooui.setDisabled) {                        param.ooui.setDisabled(!show);                    }                } else {                    param.layout.toggle(show);                    if (!!param.ooui.setDisabled) {                        param.ooui.setDisabled(!show);                    }                }            };            if (toggles[item]) {                toggles[item].on.forEach( function (widget) {                    togitem(widget, true);                });                toggles[item].off.forEach( function (widget) {                    togitem(widget, false);                });            } else if ( toggles.not0 && !isNaN(parseFloat(item)) && parseFloat(item) !== 0 ) {                toggles.not0.on.forEach( function (widget) {                    togitem(widget, true);                });                toggles.not0.off.forEach( function (widget) {                    togitem(widget, false);                });            } else if (toggles.alltogs) {                toggles.alltogs.off.forEach( function (widget) {                    togitem(widget, false);                });            }        },        /**         * Generate range and step for number and int inputs         *         * @param rawdata {string} The string representation of the range and steps         * @param type {string} The name of the field type (int or number)         * @returns {array} An array containing the min value, max value, step and button step.         */        genRange: function (rawdata,type) {            var tmp = rawdata.split(/\s*,\s*/),                rng = tmp[0].split(/\s*-\s*/),                step = tmp[1] || '',                bstep = tmp[2] || '',                min, max,                parseFunc;            if (type==='int') {            	parseFunc = function(x) { return parseInt(x, 10); }            } else {            	parseFunc = parseFloat;            }            if (type === 'int') {                step = 1;                if ( isNaN(parseInt(bstep,10)) ) {                    bstep = 1;                } else {                    bstep = parseInt(bstep,10);                }            } else {                if ( isNaN(parseFloat(step)) ) {                    step = 0.01;                } else {                    step = parseFloat(step);                }                if ( isNaN(parseFloat(bstep)) ) {                    bstep = 1;                } else {                    bstep = parseFloat(bstep);                }            }            // Accept negative values for either range position            if ( rng.length === 3 ) {                // 1 value is negative                if ( rng[0] === '' ) {                    // First value negative                    if ( isNaN(parseFunc(rng[1])) ) {                        min = -Infinity;                    } else {                        min = 0 - parseFunc(rng[1]);                    }                    if ( isNaN(parseFunc(rng[2])) ) {                        max = Infinity;                    } else {                        max = parseFunc(rng[2]);                    }                } else if ( rng[1] === '' ) {                    // Second value negative                    if ( isNaN(parseFunc(rng[0])) ) {                        min = -Infinity;                    } else {                        min = parseFunc(rng[0]);                    }                    if ( isNaN(parseFunc(rng[2])) ) {                        max = 0;                    } else {                        max = 0 - parseFunc(rng[2]);                    }                }            } else if ( rng.length === 4 ) {                // Both negative                if ( isNaN(parseFunc(rng[1])) ) {                    min = -Infinity;                } else {                    min = 0 - parseFunc(rng[1]);                }                if ( isNaN(parseFunc(rng[3])) ) {                    max = 0;                } else {                    max = 0 - parseFunc(rng[3]);                }            } else {                // No negatives                if ( isNaN(parseFunc(rng[0])) ) {                    min = 0;                } else {                    min = parseFunc(rng[0]);                }                if ( isNaN(parseFunc(rng[1])) ) {                    max = Infinity;                } else {                    max = parseFunc(rng[1]);                }            }            // Check min < max            if ( max < min ) {                return [ max, min, step, bstep ];            } else {                return [ min, max, step, bstep ];            }        },        /**         * Parse the toggles for an input         *         * @param rawdata {string} A string representing the toggles for the widget         * @param defkey {string} The default key for toggles         * @returns {object} An object representing the toggles in the format { ['widget value']:[ widget-to-toggle, group-to-toggle, widget-to-toggle2 ] }         */        parseToggles: function (rawdata,defkey) {            var tmptogs = rawdata.split(/\s*;\s*/),                allkeys = [], allvals = [],                toggles = {};            if (tmptogs.length > 0 && tmptogs[0].length > 0) {                tmptogs.forEach(function (tog) {                    var tmp = tog.split(/\s*=\s*/),                        keys = tmp[0],                        val = [];                    if (tmp.length < 2) {                        keys = [defkey];                        val = tmp[0].split(/\s*,\s*/);                    } else {                        keys = tmp[0].split(/\s*,\s*/);                        val = tmp[1].split(/\s*,\s*/);                    }                    if (keys.length === 1) {                        var key = keys[0];                        toggles[key] = {};                        toggles[key].on = val;                        allkeys.push(key);                    } else {                        keys.forEach( function (key) {                            toggles[key] = {};                            toggles[key].on = val;                            allkeys.push(key);                        });                    }                    allvals = allvals.concat(val);                });                allkeys = allkeys.filter(function (item, pos, arr) {                    return arr.indexOf(item) === pos;                });                allkeys.forEach(function (key) {                    toggles[key].off = allvals.filter(function (val) {                        if ( toggles[key].on.includes(val) ) {                            return false;                        } else {                            return true;                        }                    });                });                // Add all items to default                toggles.alltogs = {};                toggles.alltogs.off = allvals;            }            return toggles;        },        /**         * Form submission handler         */        submitForm: function () {            var self = this,                code = '{{' + self.template,                formErrors = [],                apicalls = [],                paramVals = {};                            if (self.module !== undefined) {            	if (self.modulefunc === undefined) {            		self.modulefunc = 'main';            	}//<nowiki>            	code = '{{#invoke:'+self.module+'|'+self.modulefunc;            }//</nowiki>            self.submitlayout.setNotices(['Validating fields, please wait.']);            self.submitlayout.fieldWidget.setDisabled(true);            // setup template for submission            self.tParams.forEach(function (param) {                if ( param.type === 'hidden' || (param.type !== 'group' && param.ooui.isDisabled() === false) ) {                    var val,                        $input,                        // use separate error tracking for each input                        // or every input gets flagged as an error                        error = '';                    if (param.type === 'fixed' || param.type === 'hidden') {                        val = param.def;                    } else {                        $input = $('#' + helper.getId.call(self, param.name) + ' input');                        if (param.type === 'buttonselect') {                            val = param.ooui.findSelectedItem();                            if (val !== null) {                                val = val.getData();                            }                        } else {                            val = param.ooui.getValue();                        }                        if (param.type === 'int') {                            val = val.split(',').join('');                        } else if (param.type === 'check') {                            val = param.ooui.isSelected();                            if (param.range) {                                val = param.range.split(',')[val ? 0 : 1];                            }                        } else if (param.type === 'toggleswitch' || param.type === 'togglebutton') {                            if (param.range) {                                val = param.range.split(',')[val ? 0 : 1];                            }                        }                        // Check input is valid (based on widgets validation)                        if ( !!param.ooui.hasFlag && param.ooui.hasFlag('invalid') && param.type !== 'article') {                            error = param.error;                        } else if ( param.type === 'article' && param.ooui.validateTitle && val.length > 0 ) {                            var api = param.ooui.getApi(),                                prms = {                                    action: 'query',                                    prop: [],                                    titles: [ param.ooui.getValue() ]                                };                            var prom = new Promise ( function (resolve,reject) {                                api.get(prms).then( function (ret) {                                    if ( ret.query.pages && Object.keys(ret.query.pages).length ) {                                        var nspaces = param.ooui.namespace.split('|'), allNS = false;                                        if (nspaces.indexOf('*') >= 0) {                                            allNS = true;                                        }                                        nspaces = nspaces.map(function (ns) {return parseInt(ns,10);});                                        for (var pgID in ret.query.pages) {                                            if ( ret.query.pages.hasOwnProperty(pgID) && ret.query.pages[pgID].missing!== '' ) {                                                if ( allNS ) {                                                    resolve();                                                }                                                if ( ret.query.pages[pgID].ns !== undefined && nspaces.indexOf(ret.query.pages[pgID].ns) >= 0 ) {                                                    resolve();                                                }                                            }                                        }                                        reject(param);                                    } else {                                        reject(param);                                    }                                });                            });                            apicalls.push(prom);                        }                        if (error) {                            param.layout.setErrors([error]);                            if (param.ooui.setValidityFlag !== undefined) {                                param.ooui.setValidityFlag(false);                            }                            // TODO: Remove jsInvalid classes?                            $input.addClass('jcInvalid');                            formErrors.push( param.label[0].textContent + ': ' + error );                        } else {                            param.layout.setErrors([]);                            if (param.ooui.setValidityFlag !== undefined) {                                param.ooui.setValidityFlag(true);                            }                            // TODO: Remove jsInvalid classes?                            $input.removeClass('jcInvalid');                            // Save current parameter value                            paramVals[param.name] = val;                                                        // Save current parameter value for later calculator usage.                            //window.localStorage.setItem(helper.getId.call(self, param.name), val);                        }                    }                                        code += '|' + param.name + '=' + val;                }            });            Promise.all(apicalls).then( function (vals) {                // All article fields valid                self.submitlayout.setNotices([]);                self.submitlayout.fieldWidget.setDisabled(false);                if (formErrors.length > 0) {                    self.submitlayout.setErrors(formErrors);                    helper.showError.call(self, 'One or more fields contains an invalid value.');                    return;                }                self.submitlayout.setErrors([]);                // Save all values to localStorage                if (!rs.hasLocalStorage()) {                    console.warn('Browser does not support localStorage, inputs will not be saved.');                } else {                    mw.log('Saving inputs to localStorage');                    localStorage.setItem( self.localname, JSON.stringify(paramVals) );                }                code += '}}';                console.log(code);                helper.loadTemplate.call(self, code);            }, function (errparam) {                // An article field is invalid                self.submitlayout.setNotices([]);                self.submitlayout.fieldWidget.setDisabled(false);                errparam.layout.setErrors([errparam.error]);                formErrors.push( errparam.label[0].textContent + ': ' + errparam.error );                self.submitlayout.setErrors(formErrors);                helper.showError.call(self, 'One or more fields contains an invalid value.');                return;            });        },        /**         * Parse the template used to display the result of the form         *         * @param code {string} Wikitext to send to the API for parsing         */        loadTemplate: function (code) {            var self = this,                params = {                    action: 'parse',                    text: code,                    prop: 'text|limitreportdata',                    title: mw.config.get('wgPageName'),                    disablelimitreport: 'true',                    contentmodel: 'wikitext',                    format: 'json'                },                method = 'GET';                        // 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,                    format: 'json',                    rswcalcautosubmit: self.autosubmit                };            }            if (code.length > 1900) {            	method = 'POST';            }            $('#' + self.form + ' .jcSubmit')            	.data('oouiButton')                .setDisabled(true);                        // @todo time how long these calls take            $.ajax({method:method, url:'/api.php', data:params})                .done(function (response) {                    var html;                                        if (!!mw.util.getParamValue('vecalc')) {                        // strip body tag                        html = $(response.visualeditor.content).contents();                    } else {                        html = response.parse.text['*'];                    }                    if (response.parse.limitreportdata) {                        var logs = response.parse.limitreportdata.filter(function(e){return e.name === 'scribunto-limitreport-logs'});                        if (logs.length>0) {                            var log_str = ['Scribunto logs:'];                            logs.forEach(function(log){                                var i = 0;                                while (log.hasOwnProperty(''+i)) {                                    log_str.push(log[''+i]);                                    i++;                                }                            });                            console.log(log_str.join('\n'));                        }                    }                                        helper.dispResult.call(self, html);                })                .fail(function (_, error) {                    $('#' + self.form + ' .jcSubmit')            			.data('oouiButton')                		.setDisabled(false);                    helper.showError.call(self, error);                });        },        /**         * Display the calculator result on the page         *         * @param response {String} A string representing the HTML to be added to the page         */        dispResult: function (html) {        	var self = this;            $('#' + self.form + ' .jcSubmit')            	.data('oouiButton')                .setDisabled(false);            $('#bodyContent')                .find('#' + this.result)                    .empty()                    .removeClass('jcError')                    .html(html);                        // allow scripts to hook into form submission            mw.hook('rscalc.submit').fire();			// run all standard page-init things so various JS works as expected, including:			// - sortable tables			// - collapsible sections			// - collapsed headers on mobile            mw.hook('wikipage.content').fire($('#'+this.result));            /*            mw.loader.using('jquery.tablesorter', function () {                $('table.sortable:not(.jquery-tablesorter)').tablesorter();            });            mw.loader.using('jquery.makeCollapsible', function () {                $('.mw-collapsible').makeCollapsible();            });            */            if ($('.rsw-chartjs-config').length) {    			mw.loader.load('ext.gadget.Charts-core');            }        },        /**         * 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 param {object} An object containing the configuration of a parameter             * @returns {OOUI.object} A OOUI object containing the new FieldLayout             */            fixed: function (param) {                var layconf = {                        label: new OO.ui.HtmlSnippet(param.label),                        align: 'right',                        classes: ['jsCalc-field', 'jsCalc-field-fixed'],                        value: param.def                    };                if (param.help) {                    layconf.helpInline = param.inlhelp;                    layconf.help = new OO.ui.HtmlSnippet(param.help);                }				param.ooui = new OO.ui.LabelWidget({ label: param.def });				return new OO.ui.FieldLayout(param.ooui, layconf);            },            /**             * Handler for select dropdowns             *              * @param param {object} An object containing the configuration of a parameter             * @param id {String} A string representing the id to be added to the input             * @returns {OOUI.object} A OOUI object containing the new FieldLayout             */            select: function (param, id) {                var self = this,                    conf = {                        label: 'Select an option',                        options: [],                        name: id,                        id: id,                        value: param.def,                        dropdown: {                        	$overlay: true                        }                    },                    layconf = {                        label: new OO.ui.HtmlSnippet(param.label),                        align: 'right',                        classes: ['jsCalc-field', 'jsCalc-field-select']                    },                    opts = param.range.split(/\s*,\s*/),                    def = opts[0];                param.error = 'Not a valid selection';                if (param.help) {                    layconf.helpInline = param.inlhelp;                    layconf.help = new OO.ui.HtmlSnippet(param.help);                }                opts.forEach(function (opt, i) {                    var op = { data: opt, label: opt };                    if (opt === param.def) {                        op.selected = true;                        def = opt;                    }                    conf.options.push(op);                });                param.toggles = helper.parseToggles(param.rawtogs, def);				param.ooui = new OO.ui.DropdownInputWidget(conf);                if ( Object.keys(param.toggles).length > 0 ) {                    param.ooui.on('change', function (value) {                        helper.toggle.call(self, value, param.toggles);                    });                }                return new OO.ui.FieldLayout(param.ooui, layconf);            },            /**             * Handler for button selects             *             * @param param {object} An object containing the configuration of a parameter             * @param id {String} A string representing the id to be added to the input             * @returns {OOUI.object} A OOUI object containing the new FieldLayout             */            buttonselect: function (param, id) {                var self = this,                    buttons = {},                    conf = {                        label:'Select an option',                        items: [],                        id: id                    },                    layconf = {                        label: new OO.ui.HtmlSnippet(param.label),                        align: 'right',                        classes: ['jsCalc-field', 'jsCalc-field-buttonselect']                    },                    opts = param.range.split(/\s*,\s*/),                    def;                param.error = 'Please select a valid option';                if (param.help) {                    layconf.helpInline = param.inlhelp;                    layconf.help = new OO.ui.HtmlSnippet(param.help);                }                opts.forEach(function (opt, i) {                    var opid = opt.replace(/[^a-zA-Z0-9]/g, '');                    var $opt = helper.sanitiseLabels(opt);                    var txt = $opt.text().trim();                    if (txt === '') {                    	txt = (i+1).toString();                    }                    buttons[opid] = new OO.ui.ButtonOptionWidget({data:txt, label: new OO.ui.HtmlSnippet($opt), title:txt});                    conf.items.push(buttons[opid]);                });                if (param.def.length > 0 && opts.indexOf(param.def) > -1) {                    def = param.def;                } else {                    def = opts[0];                }                param.toggles = helper.parseToggles(param.rawtogs, def);                param.ooui = new OO.ui.ButtonSelectWidget(conf);                param.ooui.selectItemByData(def);                if ( Object.keys(param.toggles).length > 0 ) {                    param.ooui.on('choose', function (button) {                        var item = button.getData();                        helper.toggle.call(self, item, param.toggles);                    });                }                return new OO.ui.FieldLayout(param.ooui, layconf);            },            /**             * Handler for comboboxes             *              * @param param {object} An object containing the configuration of a parameter             * @param id {String} A string representing the id to be added to the input             * @returns {OOUI.object} A OOUI object containing the new FieldLayout             */            combobox: function (param, id) {                var self = this,                    conf = {                        placeholder: 'Enter filter name',                        options: [],                        name: id,                        id: id,                        menu: { filterFromInput: true },                        value: param.def,                        $overlay: true                    },                    layconf = {                        label: new OO.ui.HtmlSnippet(param.label),                        align: 'right',                        classes: ['jsCalc-field', 'jsCalc-field-combobox']                    },                    opts = param.range.split(/\s*,\s*/),                    def = opts[0];                param.error = 'Not a valid selection';                if (param.help) {                    layconf.helpInline = param.inlhelp;                    layconf.help = new OO.ui.HtmlSnippet(param.help);                }                opts.forEach(function (opt) {                    var op = { data: opt, label: opt };                    if (opt === param.def) {                        op.selected = true;                        def = opt;                    }                    conf.options.push(op);                });                var isvalid = function (val) {return opts.indexOf(val) < 0 ? false : true;};                conf.validate = isvalid;                param.toggles = helper.parseToggles(param.rawtogs, def);				param.ooui = new OO.ui.ComboBoxInputWidget(conf);                if ( Object.keys(param.toggles).length > 0 ) {                    param.ooui.on('change', function (value) {                        helper.toggle.call(self, value, param.toggles);                    });                }                return new OO.ui.FieldLayout(param.ooui, layconf);            },            /**             * Handler for checkbox inputs             *              * @param param {object} An object containing the configuration of a parameter             * @param id {String} A string representing the id to be added to the input             * @returns {OOUI.object} A OOUI object containing the new FieldLayout             */            check: function (param, id) {                var self = this,                    conf = {                        name: id,                        id: id                    },                    layconf = {                        label: new OO.ui.HtmlSnippet(param.label),                        align: 'right',                        classes: ['jsCalc-field', 'jsCalc-field-check']                    };                param.toggles = helper.parseToggles(param.rawtogs, 'true');                param.error = 'Unknown error';                if (param.help) {                    layconf.helpInline = param.inlhelp;                    layconf.help = new OO.ui.HtmlSnippet(param.help);                }                if ( (param.def === 'true' || param.def === true) ||                    (param.range !== undefined && param.def === param.range.split(/\s*,\s*/)[0]) ) {                    conf.selected = true;                }				param.ooui = new OO.ui.CheckboxInputWidget(conf);                if ( Object.keys(param.toggles).length > 0 ) {                    param.ooui.on('change', function (selected) {                        if (selected) {                            helper.toggle.call(self, 'true', param.toggles);                        } else {                            helper.toggle.call(self, 'false', param.toggles);                        }                    });                }                return new OO.ui.FieldLayout(param.ooui, layconf);            },            /**             * Handler for toggle switch inputs             *             * @param param {object} An object containing the configuration of a parameter             * @param id {String} A string representing the id to be added to the input             * @returns {OOUI.object} A OOUI object containing the new FieldLayout             */            toggleswitch: function (param, id) {                var self = this,                    conf = { id: id },                    layconf = {                        label: new OO.ui.HtmlSnippet(param.label),                        align: 'right',                        classes: ['jsCalc-field', 'jsCalc-field-toggleswitch']                    };                param.toggles = helper.parseToggles(param.rawtogs, 'true');                param.error = 'Unknown error';                if (param.help) {                    layconf.helpInline = param.inlhelp;                    layconf.help = new OO.ui.HtmlSnippet(param.help);                }                if ( (param.def === 'true' || param.def === true) ||                    (param.range !== undefined && param.def === param.range.split(/\s*,\s*/)[0]) ) {                    conf.value = true;                }                param.ooui = new OO.ui.ToggleSwitchWidget(conf);                if ( Object.keys(param.toggles).length > 0 ) {                    param.ooui.on('change', function (selected) {                        if (selected) {                            helper.toggle.call(self, 'true', param.toggles);                        } else {                            helper.toggle.call(self, 'false', param.toggles);                        }                    });                }                return new OO.ui.FieldLayout(param.ooui, layconf);            },            /**             * Handler for toggle button inputs             *             * @param param {object} An object containing the configuration of a parameter             * @param id {String} A string representing the id to be added to the input             * @returns {OOUI.object} A OOUI object containing the new FieldLayout             */            togglebutton: function (param, id) {                var self = this,                    conf = {                        id: id,                        label: new OO.ui.HtmlSnippet(param.label)                    },                    layconf = {                        label:'',                        align: 'right',                        classes: ['jsCalc-field', 'jsCalc-field-togglebutton']                    };                param.toggles = helper.parseToggles(param.rawtogs, 'true');                param.error = 'Unknown error';                if (param.help) {                    layconf.helpInline = param.inlhelp;                    layconf.help = new OO.ui.HtmlSnippet(param.help);                }                if ( (param.def === 'true' || param.def === true) ||                    (param.range !== undefined && param.def === param.range.split(/\s*,\s*/)[0]) ) {                    conf.value = true;                }                param.ooui = new OO.ui.ToggleButtonWidget(conf);                if ( Object.keys(param.toggles).length > 0 ) {                    param.ooui.on('change', function (selected) {                        if (selected) {                            helper.toggle.call(self, 'true', param.toggles);                        } else {                            helper.toggle.call(self, 'false', param.toggles);                        }                    });                }                return new OO.ui.FieldLayout(param.ooui, layconf);            },            /**             * Handler for hiscore inputs             *              * @param param {object} An object containing the configuration of a parameter             * @param id {String} A string representing the id to be added to the input             * @returns {OOUI.object} A OOUI object containing the new FieldLayout             */            hs: function (param, id) {                var self = this,                    layconf = {                        label: new OO.ui.HtmlSnippet(param.label),                        align:'right',                        classes: ['jsCalc-field', 'jsCalc-field-hs']                    },                    lookups = {},                    range = param.range.split(';'),                    input1 = new OO.ui.TextInputWidget({type: 'text', id: id, name: id, value:param.def}),					button1 = new OO.ui.ButtonInputWidget({ label: 'Lookup', id: id+'-button', name:  id+'-button', classes: ['jsCalc-field-hs-lookup'], data: {param: param.name} });                if (param.help) {                    layconf.helpInline = param.inlhelp;                    layconf.help = new OO.ui.HtmlSnippet(param.help);                }                var layout = new OO.ui.ActionFieldLayout(input1, button1, layconf);				var lookupHS = function(event) {					var $t = $(event.target),						lookup = self.lookups[button1.getData().param],                        // replace spaces with _ for the query                        name = $('#' + lookup.id + ' input')                            .val()                            // @todo will this break for players with multiple spaces                            //       in their name? e.g. suomi's old display name                            .replace(/\s+/g, '_'),                        button = lookup.button;					button.setDisabled(true);                    $.ajax({						url: '/cors/m=hiscore_oldschool/index_lite.ws?player=' + name,						dataType: 'text',						async: true,						timeout: 10000 // msec					}).done(function (data) {						var hsdata;						hsdata = data.trim()							.split(/\n+/g);						lookup.params.forEach(function (param) {							var id = helper.getId.call(self, param.param),								$input = $('#' + id + ' input'),								tParam = null,                                val;							self.tParams.forEach(function(p) {								if (p.name === param.param) {									tParam = p;								}							});							if (tParam === null) {								return;							}							if (isNaN(param.skill)) {                                val = param.skill;								//tParam.ooui.setValue(param.skill);							} else {                                val = hsdata[param.skill].split(',')[param.val];								//tParam.ooui.setValue(hsdata[param.skill].split(',')[param.val]);							}                            if (!!tParam.ooui.setValue) {                                tParam.ooui.setValue(val);                            } else if (!!tParam.ooui.selectItemByData) {                                tParam.ooui.selectItemByData(val);                            } else if (tParam.type === 'fixed') {                                tParam.ooui.setLabel(val);                            }						});											// store in localStorage for future use                        if (rs.hasLocalStorage()) {                            self.lsRSN = name;                            localStorage.setItem('rsn', name);                        }						button.setDisabled(false);                        layout.setErrors([]);					})					.fail(function (xhr, status) {						button.setDisabled(false);                        var err = 'The player "' + name + '" does not exist, is banned or unranked, or we couldn\'t fetch your hiscores. Please enter the data manually.';                        console.warn(status);                        layout.setErrors([err]);						helper.showError.call(self, err);					});				};									button1.$element.click(lookupHS);				input1.$element.keydown(function(event){					if (event.which === 13) {						lookupHS(event);						event.preventDefault();					}				});				                // Use rsn loaded from localstorage                if (self.lsRSN) {                    input1.setValue(self.lsRSN);                }                lookups[param.name] = {                    id: id,                    button: button1,                    params: []                };                                range.forEach(function (el) {                    // to catch empty strings                    if (!el) {                        return;                    }                    var spl = el.split(',');                    lookups[param.name].params.push({                        param: spl[0],                        skill: spl[1],                        val: spl[2]                    });                });                // merge lookups into one object                if (!self.lookups) {                    self.lookups = lookups;                } else {                    self.lookups = $.extend(self.lookups, lookups);                }								param.ooui = input1;				param.oouiButton = button1;                return layout;            },            /**             * Handler for Runescape name inputs             *             * @param param {object} An object containing the configuration of a parameter             * @param id {String} A string representing the id to be added to the input             * @returns {OOUI.object} A OOUI object containing the new FieldLayout             */            rsn: function (param, id) {                var self = this,                    conf = {                        type: 'text',                        name: id,                        id: id,                        placeholder: 'Enter runescape name',                        spellcheck: false,                        maxLength: 12,                        value: param.def                    },                    layconf = {                        label: new OO.ui.HtmlSnippet(param.label),                        align: 'right',                        classes: ['jsCalc-field', 'jsCalc-field-string']                    };                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.';                if (param.help) {                    layconf.helpInline = param.inlhelp;                    layconf.help = new OO.ui.HtmlSnippet(param.help);                }                // Use rsn loaded from localstorage, if available                if (self.lsRSN) {                    conf.value = self.lsRSN;                }                var validrsn = function (name) {                    if ( name.search( /[^0-9a-zA-Z\-_\s]/ ) >= 0 ) {                        return false;                    } else {                        if ( name.toLowerCase().search( /(^mod\s|\smod\s|\smod$)/ ) >= 0 ) {                            return false;                        } else {                            return true;                        }                    }                };                conf.validate = validrsn;                param.ooui = new OO.ui.TextInputWidget(conf);                return new OO.ui.FieldLayout(param.ooui, layconf);            },                                    /**             * Handler for integer inputs             *              * @param param {object} An object containing the configuration of a parameter             * @param id {String} A string representing the id to be added to the input             * @returns {OOUI.object} A OOUI object containing the new FieldLayout             */            int: function (param, id) {                var self = this,                    rng = helper.genRange(param.range, 'int'),                    conf = {                        min:rng[0],                        max:rng[1],                        step:rng[2],                        showButtons:true,                        buttonStep:rng[3],                        allowInteger:true,                        name: id,                        id: id,                        value: param.def || 0                    },                    layconf = {                        label: new OO.ui.HtmlSnippet(param.label),                        align: 'right',                        classes: ['jsCalc-field', 'jsCalc-field-int']                    },                    error = 'Invalid integer. Must be between ' + rng[0] + ' and ' + rng[1];                param.toggles = helper.parseToggles(param.rawtogs, 'not0');                if (param.help) {                    layconf.helpInline = param.inlhelp;                    layconf.help = new OO.ui.HtmlSnippet(param.help);                }                if ( rng[2] > 1 ) {                    error += ' and a muiltiple of ' + rng[2];                }                param.error = error;				param.ooui = new OO.ui.NumberInputWidget(conf);                if ( Object.keys(param.toggles).length > 0 ) {                    param.ooui.on('change', function (value) {                        helper.toggle.call(self, value, param.toggles);                    });                }                return new OO.ui.FieldLayout(param.ooui, layconf);            },                        /**             * Handler for number inputs             *              * @param param {object} An object containing the configuration of a parameter             * @param id {String} A string representing the id to be added to the input             * @returns {OOUI.object} A OOUI object containing the new FieldLayout             */            number: function (param, id) {                var self = this,                    rng = helper.genRange(param.range, 'number'),                    conf = {                        min:rng[0],                        max:rng[1],                        step:rng[2],                        showButtons:true,                        buttonStep:rng[3],                        name:id,                        id:id,                        value:param.def || 0                    },                    layconf = {                        label: new OO.ui.HtmlSnippet(param.label),                        align: 'right',                        classes: ['jsCalc-field', 'jsCalc-field-number'],                    };                param.toggles = helper.parseToggles(param.rawtogs, 'not0');                param.error = 'Invalid interger. Must be between ' + rng[0] + ' and ' + rng[1] + ' and a multiple of ' + rng[2];                if (param.help) {                    layconf.helpInline = param.inlhelp;                    layconf.help = new OO.ui.HtmlSnippet(param.help);                }                param.ooui = new OO.ui.NumberInputWidget(conf);                if ( Object.keys(param.toggles).length > 0 ) {                    param.ooui.on('change', function (value) {                        helper.toggle.call(self, value, param.toggles);                    });                }                return new OO.ui.FieldLayout( param.ooui, layconf);            },            /**             * Handler for article inputs             *             * @param param {object} An object containing the configuration of a parameter             * @param id {String} A string representing the id to be added to the input             * @returns {OOUI.object} A OOUI object containing the new FieldLayout             */            article: function (param, id) {                var self = this,                    conf = {                        addQueryInput: false,                        excludeCurrentPage: true,                        showMissing: false,                        showDescriptions: true,                        validateTitle: true,                        relative: false,                        id: id,                        name: id,                        placeholder: 'Enter page name',                        value: param.def                    },                    layconf = {                        label: new OO.ui.HtmlSnippet(param.label),                        align:'right',                        classes: ['jsCalc-field', 'jsCalc-field-article']                    },                    validNSnumbers = { '_*':'All', '_-2':'Media', '_-1':'Special', _0:'(Main)', _1:'Talk', _2:'User', _3:'User talk', _4:'RuneScape', _5:'RuneScape talk', _6:'File', _7:'File talk', _8:'MediaWiki', _9:'MediaWiki talk', _10:'Template', _11:'Template talk',                        _12:'Help', _13:'Help talk', _14:'Category', _15:'Category talk', _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' },                    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,                        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 },                    namespaces = '';                if (param.help) {                    layconf.helpInline = param.inlhelp;                    layconf.help = new OO.ui.HtmlSnippet(param.help);                }                if (param.range && param.range.length > 0) {                    var names = param.range.split(/\s*,\s*/),                    nsnumbers = [];                    names.forEach( function (nmspace) {                        nmspace = nmspace.toLowerCase();                        if ( validNSnumbers['_'+nmspace] ) {                            nsnumbers.push(nmspace);                        } else if ( validNSnames[nmspace] ) {                            nsnumbers.push( validNSnames[nmspace] );                        }                    });                    if (nsnumbers.length < 1) {                        conf.namespace = '0';                        namespaces = '(Main) namespace';                    } else if (nsnumbers.length < 2) {                        conf.namespace = nsnumbers[0];                        namespaces = nsnumbers[0] + ' namespace';                    } else {                        conf.namespace = nsnumbers.join('|');                        var nsmap = function (num) {                            return validNSnumbers['_'+num];                        };                        namespaces = nsnumbers.slice(0, -1).map(nsmap).join(', ') + ' or ' + nsnumbers.slice(-1).map(nsmap)[0] + ' namespaces';                    }                } else if ( self.suggestns && self.suggestns.length > 0 ) {                    var nsnumbers = [];                    self.suggestns.forEach( function (nmspace) {                        nmspace = nmspace.toLowerCase();                        if ( validNSnumbers['_'+nmspace] ) {                            nsnumbers.push(nmspace);                        } else if ( validNSnames[nmspace] ) {                            nsnumbers.push( validNSnames[nmspace] );                        }                    });                    if (nsnumbers.length < 1) {                        conf.namespace = '0';                        namespaces = '(Main) namespace';                    } else if (nsnumbers.length < 2) {                        conf.namespace = nsnumbers[0];                        namespaces = nsnumbers[0] + ' namespace';                    } else {                        conf.namespace = nsnumbers.join('|');                        var nsmap = function (num) {                            return validNSnumbers['_'+num];                        };                        namespaces = nsnumbers.slice(0, -1).map(nsmap).join(', ') + ' or ' + nsnumbers.slice(-1).map(nsmap)[0] + ' namespaces';                    }                } else {                    conf.namespace = '0';                    namespaces = '(Main) namespace';                }                param.error = 'Invalid page or page is not in ' + namespaces;                param.ooui = new mw.widgets.TitleInputWidget(conf);                return new OO.ui.FieldLayout( param.ooui, layconf);            },            /**             * Handler for group type params             *             * @param param {object} An object containing the configuration of a parameter             * @param id {String} A string representing the id to be added to the input             * @returns {OOUI.object} A OOUI object containing the new FieldLayout             */            group: function (param, id) {                param.ooui = new OO.ui.HorizontalLayout({id: id, classes: ['jsCalc-group']});                if (param.label !== param.name) {                    var label = new OO.ui.LabelWidget({ label: new OO.ui.HtmlSnippet(param.label), classes:['jsCalc-grouplabel'] });                    param.ooui.addItems([label]);                }                return param.ooui;            },                        /**             * Default handler for inputs             *              * @param param {object} An object containing the configuration of a parameter             * @param id {String} A string representing the id to be added to the input             * @returns {OOUI.object} A OOUI object containing the new FieldLayout             */            def: function (param, id) {                var layconf = {                    label: new OO.ui.HtmlSnippet(param.label),                    align: 'right',                    classes: ['jsCalc-field', 'jsCalc-field-string'],                    value: param.def                };                param.error = 'Unknown error';                if (param.help) {                    layconf.helpInline = param.inlhelp;                    layconf.help = new OO.ui.HtmlSnippet(param.help);                }				param.ooui = new OO.ui.TextInputWidget({type: 'text', name: id, id: id});                return new OO.ui.FieldLayout(param.ooui, layconf);            }        }    };/** * Create an instance of `Calc` * and parse the config stored in `elem` * * @param elem {Element} An Element representing the HTML tag that contains *                       the calculator's configuration */function Calc(elem) {    var self = this,        $elem = $(elem),        lines,        config;            // support div tags for config as well as pre    // be aware using div tags relies on wikitext for parsing    // so you can't use anchor or img tags    // use the wikitext equivalent instead    if ($elem.children().length) {        $elem = $elem.children();        lines = $elem.html();    } else {        // .html() causes html characters to be escaped for some reason        // so use .text() instead for <pre> tags        lines = $elem.text();    }        lines = lines.split('\n');        config = helper.parseConfig.call(this, lines);    // Calc name for localstorage, keyed to calc id    this.localname = calcstorage + '-' + config.form;        // Load previous parameter values.    if (!rs.hasLocalStorage()) {        console.warn('Browser does not support localStorage');    } else {        mw.log('Loading previous calculator values');        var calcdata = JSON.parse( localStorage.getItem(this.localname) ) || false;        if (calcdata) {            config.tParams.forEach( function(param) {                if (calcdata[param.name] !== undefined && calcdata[param.name] !== null) {                    param.def = calcdata[param.name];                }            });        }        self.lsRSN = localStorage.getItem('rsn');        mw.log(config);    }    // merge config in    $.extend(this, config);    /**     * @todo document     */    this.getInput = function (id) {        if (id) {            id = helper.getId.call(self, id);            return $('#' + id);        }                return $('#jsForm-' + self.form).find('select, input');    };}/** * Helper function for getting the id of an input * * @param id {string} The id of the input as specified by the calculator config. * @returns {string} The true id of the input with prefixes. */Calc.prototype.getId = function (id) {    var self = this,        inputId = helper.getId.call(self, id);    return inputId;};/** * Build the calculator form */Calc.prototype.setupCalc = function () {    var self = this,        fieldset = new OO.ui.FieldsetLayout({label: self.name, classes: ['jcTable'], id: 'jsForm-'+self.form}),		submitButton, submitButtonAction, paramChangeAction, timeout,        groupkeys = {};    // Used to store indexes of elements to toggle them later    self.indexkeys = {};    self.tParams.forEach(function (param, index) {        // can skip any output here as the result is pulled from the        // param default in the config on submission        if (param.type === 'hidden') {            return;        }        var id = helper.getId.call(self, param.name),            method = helper.tParams[param.type] ?                param.type :                'def';        // Generate list of items in group        if (param.type === 'group') {            var fields = param.range.split(/\s*,\s*/);            fields.forEach( function (field) {                groupkeys[field] = index;            });        }        param.layout = helper.tParams[method].call(self, param, id);        if (param.type === 'semihidden') {            param.layout.toggle(false);        }		        // Add to group or form        if ( groupkeys[param.name] || groupkeys[param.name] === 0 ) {            self.tParams[ groupkeys[param.name] ].ooui.addItems([param.layout]);        } else {            fieldset.addItems([param.layout]);        }        // Add item to indexkeys        self.indexkeys[param.name] = index;    });    // Run toggle for each field, check validity    self.tParams.forEach( function (param) {        if (param.toggles && Object.keys(param.toggles).length > 0) {            var val;            if (param.type === 'buttonselect') {                val = param.ooui.findSelectedItem().getData();            } else if (param.type === 'check') {                val = param.ooui.isSelected() ? 'true' : 'false';            } else if (param.type === 'toggleswitch' || param.type === 'togglebutton') {                val = param.ooui.getValue() ? 'true' : 'false';            } else {                val = param.ooui.getValue();            }            helper.toggle.call(self, val, param.toggles);        }        if (param.type === 'number' || param.type === 'int' || param.type === 'rsn') {            param.ooui.setValidityFlag();        }    });		submitButton = new OO.ui.ButtonInputWidget({ label: 'Submit', flags: ['primary', 'progressive'], classes: ['jcSubmit']});	submitButtonAction = function (){                helper.submitForm.call(self);	};	submitButton.on('click', submitButtonAction);	submitButton.$element.data('oouiButton', submitButton);    self.submitlayout = new OO.ui.FieldLayout(submitButton, {label: ' ', align: 'right', classes: ['jsCalc-field', 'jsCalc-field-submit']});	fieldset.addItems([ self.submitlayout ]);    // Auto-submit    if (['off', 'false', 'disabled'].indexOf(self.autosubmit) === -1) {        // Add event        paramChangeAction = function (widget) {            if ( typeof widget.getFlags === 'undefined' || !widget.getFlags().includes('invalid')) {                helper.submitForm.call(self);            }        };        		// We only want one of these pending at once		function timeoutFunc(param) {		    clearTimeout(timeout);		    timeout = setTimeout(paramChangeAction, 500, param);		}		        self.tParams.forEach( function (param) {            if (param.type === 'hidden' || param.type === 'hs' || param.type === 'group') {                return;            } else if (param.type === 'buttonselect') {                param.ooui.on('select', timeoutFunc, [param.ooui]);            }            param.ooui.on('change', timeoutFunc, [param.ooui]);        });    }        if (self.configError) {        fieldset.$element.append('<br>', self.configError);    }    $('#bodyContent')        .find('#' + self.form)            .empty()            .append(fieldset.$element);        // make buttonselects all the same height    self.tParams.filter(e=>e.type==='buttonselect').forEach(e=>{    	let m = e.ooui.items.reduce((acc,curr)=>Math.max(acc, curr.$element.find('> a.oo-ui-buttonElement-button').height()), 0);    	e.ooui.items.forEach(e=>e.$element.find('> a.oo-ui-buttonElement-button').height(m));    })};/** * @todo */function lookupCalc(calcId) {    return calcStore[calcId];}/** * @todo */function init() {    // Initialises class changes    helper.initClasses();    $('.jcConfig').each(function () {        var c = new Calc(this);        c.setupCalc();                calcStore[c.form] = c;        // if (c.autosubmit === 'true' || c.autosubmit === true) {        //     helper.submitForm.call(c);        // }    });        // allow scripts to hook into calc setup completion    mw.hook('rscalc.setupComplete').fire();}$(init);rs.calc = {};rs.calc.lookup = lookupCalc;