MediaWiki:Gadget-calc-core.js: Difference between revisions

From [N8]
Jump to navigation Jump to search
Created page with "global mediaWiki, mw, gswiki, gs, OO: 'use strict'; /** * Prefix of localStorage key for calc data. This is prepended to the form ID * localStorage name fo..."
 
No edit summary
 
Line 3: Line 3:
'use strict';
'use strict';


    /**
    * Prefix of localStorage key for calc data. This is prepended to the form ID
    * localStorage name for autosubmit setting
    */
var calcstorage = 'gsw-calcsdata',
var calcstorage = 'gsw-calcsdata',
     calcautostorage = 'gsw-calcsdata-allautosub',
     calcautostorage = 'gsw-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 = {},
     cache = {},


    /**
    * Internal variable to store references to each calculator on the page.
    */
     calcStore = {},
     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 = {
     helper = {
        /**
   
        * Add/change functionality of mw/OO.ui classes
        * Added support for multiple namespaces to mw.widgets.TitleInputWidget
        */
         initClasses: function () {
         initClasses: function () {
             var hasOwn = Object.prototype.hasOwnProperty;
             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) {
             mw.widgets.TitleInputWidget.prototype.getOptionsFromData = function (data) {
                 var i, len, index, pageExists, pageExistsExact, suggestionPage, page, redirect, redirects,
                 var i, len, index, pageExists, pageExistsExact, suggestionPage, page, redirect, redirects,
Line 932: Line 906:
                 return new OO.ui.FieldLayout(param.ooui, layconf);
                 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) {
                    var opid = opt.replace(/[^a-zA-Z0-9]/g, '');
                    buttons[opid] = new OO.ui.ButtonOptionWidget({data:opt, label:opt, title:opt});
                    conf.items.push(buttons[opid]);
                });
                if (param.def.length > 0 && opts.indexOf(param.def) > -1) {
                    def = param.def;
                } else {
                    def = opts[0];
                }
                param.toggles = helper.parseToggles(param.rawtogs, def);
                param.ooui = new OO.ui.ButtonSelectWidget(conf);
                param.ooui.selectItemByData(def);
                if ( Object.keys(param.toggles).length > 0 ) {
                    param.ooui.on('choose', function (button) {
                        var item = button.getData();
                        helper.toggle.call(self, item, param.toggles);
                    });
                }
                return new OO.ui.FieldLayout(param.ooui, layconf);
            },
            /**
            * Handler for comboboxes
            *
            * @param param {object} An object containing the configuration of a parameter
            * @param id {String} A string representing the id to be added to the input
            * @returns {OOUI.object} A OOUI object containing the new FieldLayout
            */
            combobox: function (param, id) {
                var self = this,
                    conf = {
                        placeholder: 'Enter filter name',
                        options: [],
                        name: id,
                        id: id,
                        menu: { filterFromInput: true },
                        value: param.def
                    },
                    layconf = {
                        label: new OO.ui.HtmlSnippet(param.label),
                        align: 'right',
                        classes: ['jsCalc-field', 'jsCalc-field-combobox']
                    },
                    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.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.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.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 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 (!gs.hasLocalStorage()) {
        console.warn('Browser does not support localStorage');
    } else {
        console.log('Loading previous calculator values');
        if ( config.autosubmit !== 'disabled' ) {
            config.autosubmit = localStorage.getItem(calcautostorage);
        }
        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.lsGSN = localStorage.getItem('gsn');
        console.log(config);
    }
    // merge config in
    $.extend(this, config);
    /**
    * @todo document
    */
    this.getInput = function (id) {
        if (id) {
            id = helper.getId.call(self, id);
            return $('#' + id);
        }
       
        return $('#jsForm-' + self.form).find('select, input');
    };
}
/**
* Helper function for getting the id of an input
*
* @param id {string} The id of the input as specified by the calculator config.
* @returns {string} The true id of the input with prefixes.
*/
Calc.prototype.getId = function (id) {
    var self = this,
        inputId = helper.getId.call(self, id);
    return inputId;
};
/**
* Build the calculator form
*/
Calc.prototype.setupCalc = function () {
    var self = this,
        fieldset = new OO.ui.FieldsetLayout({label: self.name, classes: ['jcTable'], id: 'jsForm-'+self.form}),
submitButton, submitButtonAction, autosubmit, paramChangeAction,
        groupkeys = {};
    // Used to store indexes of elements to toggle them later
    self.indexkeys = {};
    self.tParams.forEach(function (param, index) {
        // can skip any output here as the result is pulled from the
        // param default in the config on submission
        if (param.type === 'hidden') {
            return;
        }
        var id = helper.getId.call(self, param.name),
            method = helper.tParams[param.type] ?
                param.type :
                'def';
        // Generate list of items in group
        if (param.type === 'group') {
            var fields = param.range.split(/\s*,\s*/);
            fields.forEach( function (field) {
                groupkeys[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 === 'gsn') {
            param.ooui.setValidityFlag();
        }
    });
submitButton = new OO.ui.ButtonInputWidget({ label: 'Submit', flags: ['primary', 'progressive'], classes: ['jcSubmit']});
submitButtonAction = function (){
                helper.submitForm.call(self);
};
submitButton.on('click', submitButtonAction);
submitButton.$element.data('oouiButton', submitButton);
    self.submitlayout = new OO.ui.FieldLayout(submitButton, {label: ' ', align: 'right', classes: ['jsCalc-field', 'jsCalc-field-submit']});
fieldset.addItems([ self.submitlayout ]);
    // Auto-submit
    if (self.autosubmit !== 'disabled') {
        // Add toggle to fieldset
        autosubmit = new OO.ui.ToggleSwitchWidget({
            value: self.autosubmit === 'on' || self.autosubmit === 'true'
        });
        autosubmit.on('change', function (value) { self.autosubmit = value; });
        fieldset.addItems([ new OO.ui.FieldLayout(autosubmit, { label:'Auto-submit', align:'right', classes:['jsCalc-field', 'jsCalc-field-autosubmit'] }) ]);
        // Add event
        paramChangeAction = function (widget) {
            if (autosubmit.getValue()) {
                if ( typeof widget.getFlagsa === 'undefined' || !widget.getFlags().includes('invalid')) {
                    helper.submitForm.call(self);
                }
            }
        };
        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', setTimeout, [paramChangeAction, 500, param.ooui]);
            }
            param.ooui.on('change', setTimeout, [paramChangeAction, 500, param.ooui]);
        });
    }
   
    if (self.configError) {
        fieldset.$element.append('<br>', self.configError);
    }
    $('#bodyContent')
        .find('#' + self.form)
            .empty()
            .append(fieldset.$element);
};
/**
* @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.autosumit === true) {
            helper.submitForm.call(c);
        }
    });
   
    // allow scripts to hook into calc setup completion
    mw.hook('rscalc.setupComplete').fire();
}
$(init);
gs.calc = {};
gs.calc.lookup = lookupCalc;
// </nowiki>

Latest revision as of 15:24, 18 August 2020

/*global mediaWiki, mw, gswiki, gs, OO */

'use strict';

var calcstorage = 'gsw-calcsdata',
    calcautostorage = 'gsw-calcsdata-allautosub',
    
    cache = {},

    calcStore = {},

    helper = {
    	
        initClasses: function () {
            var hasOwn = Object.prototype.hasOwnProperty;
            
            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',
                    'name',
                    'autosubmit'
                ],
                // used for debugging incorrect param types
                validParamTypes = [
                    'string',
                    'article',
                    'number',
                    'int',
                    'select',
                    'buttonselect',
                    'combobox',
                    'check',
                    'toggleswitch',
                    'togglebutton',
                    'hs',
                    'gsn',
                    '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: mw.html.escape(args[4] || ''),
                    rawtogs: mw.html.escape(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);
            console.log(config);
            return config;
        },

        /**
         * Generate a unique id for each input
         *
         * @param inputId {String} A string representing the id of an input
         * @returns {String} A string representing the namespaced/prefixed id of an input
         */
        getId: function (inputId) {
            return [this.form, this.result, inputId].join('-');
        },

        /**
         * Output an error to the UI
         *
         * @param error {String} A string representing the error message to be output
         */
        showError: function (error) {
            $('#' + this.result)
                .empty()
                .append(
                    $('<span>')
                        .addClass('jcError')
                        .text(error)
                );
        },

        /**
         * Toggle the visibility and enabled status of fields/groups
         *
         * @param item {String} A string representing the current value of the widget
         * @param toggles {object} An object representing arrays of items to be toggled keyed by widget values
         */
        toggle: function (item, toggles) {
            var self = this;

            var togitem = function (widget, show) {
                var param = self.tParams[ self.indexkeys[widget] ];
                if (param.type === 'group') {
                    param.ooui.toggle(show);
                    param.ooui.getItems().forEach(function (child) {
                        if (!!child.setDisabled) {
                            child.setDisabled(!show);
                        } else if (!!child.getField().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 = {};

            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 (!gs.hasLocalStorage()) {
                    console.warn('Browser does not support localStorage, inputs will not be saved.');
                } else {
                    console.log('Saving inputs to localStorage');
                    paramVals.autosubmit = !!self.autosubmit;
                    localStorage.setItem( self.localname, JSON.stringify(paramVals) );
                    localStorage.setItem( calcautostorage, paramVals.autosubmit );
                }

                code += '}}';
                mw.log(code);
                helper.loadTemplate.call(self, code);

            }, function (errparam) {
                // An article field is invalid
                self.submitlayout.setNotices([]);
                self.submitlayout.fieldWidget.setDisabled(false);

                errparam.layout.setErrors([errparam.error]);
                formErrors.push( errparam.label[0].textContent + ': ' + errparam.error );

                self.submitlayout.setErrors(formErrors);
                helper.showError.call(self, 'One or more fields contains an invalid value.');
                return;
            });
        },

        /**
         * Parse the template used to display the result of the form
         *
         * @param code {string} Wikitext to send to the API for parsing
         */
        loadTemplate: function (code) {
            var self = this,
                params = {
                    action: 'parse',
                    text: code,
                    prop: 'text',
                    title: self.template,
                    disablepp: 'true'
                };
            
            // experimental support for using VE to parse calc templates
            if (!!mw.util.getParamValue('vecalc')) {
                params = {
                    action: 'visualeditor',
                    // has to be a mainspace page or VE won't work
                    page: 'No page',
                    paction: 'parsefragment',
                    wikitext: code
                };
            }

            $('#' + self.form + ' .jcSubmit')
            	.data('oouiButton')
                .setDisabled(true);

            // @todo time how long these calls take
            (new mw.Api())
                .post(params)
                .done(function (response) {
                    var html;
                    
                    if (!!mw.util.getParamValue('vecalc')) {
                        // strip body tag
                        html = $(response.visualeditor.content).contents();
                    } else {
                        html = response.parse.text['*'];
                    }
                    
                    helper.dispResult.call(self, html);
                })
                .fail(function (_, error) {
                    $('#' + self.form + ' .jcSubmit')
            			.data('oouiButton')
                		.setDisabled(false);
                    helper.showError.call(self, error);
                });
        },

        /**
         * Display the calculator result on the page
         *
         * @param response {String} A string representing the HTML to be added to the page
         */
        dispResult: function (html) {
        	var self = this;
            $('#' + self.form + ' .jcSubmit')
            	.data('oouiButton')
                .setDisabled(false);

            $('#bodyContent, #WikiaArticle')
                .find('#' + this.result)
                    .empty()
                    .removeClass('jcError')
                    .html(html);
            
            // allow scripts to hook into form submission
            mw.hook('rscalc.submit').fire();

            mw.loader.using('jquery.tablesorter', function () {
                $('table.sortable:not(.jquery-tablesorter)').tablesorter();
            });
            mw.loader.using('jquery.makeCollapsible', function () {
                $('.mw-collapsible').makeCollapsible();
            });
            if ($('.gsw-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
                    },
                    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) {
                    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) {
                    var opid = opt.replace(/[^a-zA-Z0-9]/g, '');
                    buttons[opid] = new OO.ui.ButtonOptionWidget({data:opt, label:opt, title:opt});
                    conf.items.push(buttons[opid]);
                });

                if (param.def.length > 0 && opts.indexOf(param.def) > -1) {
                    def = param.def;
                } else {
                    def = opts[0];
                }

                param.toggles = helper.parseToggles(param.rawtogs, def);

                param.ooui = new OO.ui.ButtonSelectWidget(conf);
                param.ooui.selectItemByData(def);
                if ( Object.keys(param.toggles).length > 0 ) {
                    param.ooui.on('choose', function (button) {
                        var item = button.getData();
                        helper.toggle.call(self, item, param.toggles);
                    });
                }
                return new OO.ui.FieldLayout(param.ooui, layconf);
            },

            /**
             * Handler for comboboxes
             * 
             * @param param {object} An object containing the configuration of a parameter
             * @param id {String} A string representing the id to be added to the input
             * @returns {OOUI.object} A OOUI object containing the new FieldLayout
             */
            combobox: function (param, id) {
                var self = this,
                    conf = {
                        placeholder: 'Enter filter name',
                        options: [],
                        name: id,
                        id: id,
                        menu: { filterFromInput: true },
                        value: param.def
                    },
                    layconf = {
                        label: new OO.ui.HtmlSnippet(param.label),
                        align: 'right',
                        classes: ['jsCalc-field', 'jsCalc-field-combobox']
                    },
                    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.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.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.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 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 (!gs.hasLocalStorage()) {
        console.warn('Browser does not support localStorage');
    } else {
        console.log('Loading previous calculator values');
        if ( config.autosubmit !== 'disabled' ) {
            config.autosubmit = localStorage.getItem(calcautostorage);
        }
        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.lsGSN = localStorage.getItem('gsn');
        console.log(config);
    }

    // merge config in
    $.extend(this, config);

    /**
     * @todo document
     */
    this.getInput = function (id) {
        if (id) {
            id = helper.getId.call(self, id);
            return $('#' + id);
        }
        
        return $('#jsForm-' + self.form).find('select, input');
    };
}

/**
 * Helper function for getting the id of an input
 *
 * @param id {string} The id of the input as specified by the calculator config.
 * @returns {string} The true id of the input with prefixes.
 */
Calc.prototype.getId = function (id) {
    var self = this,
        inputId = helper.getId.call(self, id);

    return inputId;
};

/**
 * Build the calculator form
 */
Calc.prototype.setupCalc = function () {
    var self = this,
        fieldset = new OO.ui.FieldsetLayout({label: self.name, classes: ['jcTable'], id: 'jsForm-'+self.form}),
		submitButton, submitButtonAction, autosubmit, paramChangeAction,
        groupkeys = {};

    // Used to store indexes of elements to toggle them later
    self.indexkeys = {};

    self.tParams.forEach(function (param, index) {
        // can skip any output here as the result is pulled from the
        // param default in the config on submission
        if (param.type === 'hidden') {
            return;
        }

        var id = helper.getId.call(self, param.name),
            method = helper.tParams[param.type] ?
                param.type :
                'def';

        // Generate list of items in group
        if (param.type === 'group') {
            var fields = param.range.split(/\s*,\s*/);
            fields.forEach( function (field) {
                groupkeys[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 === 'gsn') {
            param.ooui.setValidityFlag();
        }
    });

	
	submitButton = new OO.ui.ButtonInputWidget({ label: 'Submit', flags: ['primary', 'progressive'], classes: ['jcSubmit']});
	submitButtonAction = function (){
                helper.submitForm.call(self);
	};
	submitButton.on('click', submitButtonAction);
	submitButton.$element.data('oouiButton', submitButton);

    self.submitlayout = new OO.ui.FieldLayout(submitButton, {label: ' ', align: 'right', classes: ['jsCalc-field', 'jsCalc-field-submit']});
	fieldset.addItems([ self.submitlayout ]);

    // Auto-submit
    if (self.autosubmit !== 'disabled') {
        // Add toggle to fieldset
        autosubmit = new OO.ui.ToggleSwitchWidget({
            value: self.autosubmit === 'on' || self.autosubmit === 'true'
        });
        autosubmit.on('change', function (value) { self.autosubmit = value; });
        fieldset.addItems([ new OO.ui.FieldLayout(autosubmit, { label:'Auto-submit', align:'right', classes:['jsCalc-field', 'jsCalc-field-autosubmit'] }) ]);

        // Add event
        paramChangeAction = function (widget) {
            if (autosubmit.getValue()) {
                if ( typeof widget.getFlagsa === 'undefined' || !widget.getFlags().includes('invalid')) {
                    helper.submitForm.call(self);
                }
            }
        };
        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', setTimeout, [paramChangeAction, 500, param.ooui]);
            }
            param.ooui.on('change', setTimeout, [paramChangeAction, 500, param.ooui]);
        });
    }
    
    if (self.configError) {
        fieldset.$element.append('<br>', self.configError);
    }

    $('#bodyContent')
        .find('#' + self.form)
            .empty()
            .append(fieldset.$element);
};

/**
 * @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.autosumit === true) {
            helper.submitForm.call(c);
        }
    });
    
    // allow scripts to hook into calc setup completion
    mw.hook('rscalc.setupComplete').fire();
}

$(init);

gs.calc = {};
gs.calc.lookup = lookupCalc;

// </nowiki>