User:Qatalexce/wikia.js

/** * Calc script for RuneScape Wiki * * This script exposes the following hooks, accessible via `mw.hook`: *    1. 'rscalc.setupComplete' - Fires when all calculator forms have been added to the DOM. *    2. 'rscalc.submit' - Fires when a calculator form has been submitted and the result has *                         been added to the DOM. * For instructions on how to use `mw.hook`, see  * * @see Documentation  * @see Examples  * @see Tests  *     Also includes XSS checks. * * @license GLPv3  * * @author Quarenon * @author TehKittyCat * @author Joeytje50 * @author Cook Me Plox * @author Gaz Lloyd * @author Cqm * * @todo Test Wikia's linksuggest for search suggestions *      not sure if it supports multiple namespaces though * @todo Whitelist domains for href attributes when sanitising HTML? */

/*jshint bitwise:true, browser:true, camelcase:true, curly:true, devel:false, eqeqeq:true, es3:false, forin:true, immed:true, jquery:true, latedef:true, newcap:true, noarg:true, noempty:true, nonew:true, onevar:false, plusplus:false, quotmark:single, undef:true, unused:true, strict:true, trailing:true

/*global mediaWiki, rswiki */

'use strict';
 * (function ($, mw, rs, undefined) {

/**        * Caching for search suggestions */   var cache = {},

/**        * Internal variable to store references to each calculator on the page. */       calcStore = {},

/**        * Private helper methods for `Calc` *        * Most methods here are called with `Function.prototype.call` * and are passed an instance of `Calc` to access it's prototype */       helper = { /**            * Parse the calculator configuration *            * @param lines {Array} An array containing the calculator's configuration * @returns {Object} An object representing the calculator's configuration */           parseConfig: function (lines) { var defConfig = { suggestns: [] },                   config = { // this isn't in `defConfig` // as it'll get overridden anyway tParams: [] },                   // used for debugging incorrect config names validParams = [ 'form', 'param', 'result', 'suggestns', 'template' ],                   // used for debugging incorrect param types validParamTypes = [ 'string', 'article', 'number', 'int', 'select', 'check', 'hs', 'fixed', 'hidden', 'semihidden' ],                   configError = false;

// parse the calculator's config // @example param=arg1|arg1|arg3|arg4 lines.forEach(function (line) {                   var temp = line.split('='),                        param,                        args;

// incorrect config if (temp.length < 2) { return; }

// an equals is used in one of the arguments // @example HTML label with attributes // so join them back together to preserve it                   // this also allows support of HTML attributes in labels if (temp.length > 2) { temp[1] = temp.slice(1,temp.length).join('='); }

param = temp[0].trim.toLowerCase; args = temp[1].trim;

if (validParams.indexOf(param) === -1) { // use console for easier debugging console.log('Unknown parameter: ' + param); configError = true; return; }

if (param === 'suggestns') { config.suggestns = args.split(/\s*,\s*/); return; }

if (param !== 'param') { config[param] = args; return; }

// split args args = args.split(/\s*\|\s*/);

// store template params in an array to make life easier config.tParams = config.tParams || [];

if (validParamTypes.indexOf(args[3]) === -1 && args[3] !== '') { // use console for easier debugging console.log('Unknown param type: ' + args[3]); configError = true; return; }

config.tParams.push({                       name: mw.html.escape(args[0]),                        label: args[1] || args[0],                        def: mw.html.escape(args[2] || ),                        type: mw.html.escape(args[3] || ),                        range: mw.html.escape(args[4] || '')                    }); });               if (configError) {                    config.configError = 'This calculator\'s config contains errors. Please report it ' +                        'here ' +                        'or check the javascript console for details.';                }

config = $.extend(defConfig, config); return config; },

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

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

/**            * Check that a number is within a specified range *            * @param x {Number} The number to check is within the range * @param param {Object} An object containing the range to check and the type of             *                       parameter as the check varies slightly depending on it's type * @returns {Boolean} `true` if the number is within the parameter's range or            *                    `false` if not */           validRange: function (x, param) { // if no range, return true if (!param.range) { return true; }

var parts = param.range.split('-'), method = parseFloat;

// enforce integer ranges for int // otherwise allow floats for number if (param.type === 'int') { method = parseInt; }

// check lower limit if (parts[0] !== '' && x < method(parts[0], 10)) { return false; }

// check upper limit if (parts[1] !== '' && x > method(parts[1], 10)) { return false; }

return true; },

/**            * Form submission handler */           submitForm: function  { var self = this, code = '';

helper.loadTemplate.call(self, code); },

/**            * Parse the template used to display the result of the form *            * @param code {string} Wikitext to send to the API for parsing */           loadTemplate: function (code) { var self = this, params = { action: 'parse', text: code, prop: 'text', 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 input') .val('Loading...') .prop('disabled', 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 input')                            .val('Submit')                            .prop('disabled', 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) { $('#' + this.form + ' .jcSubmit input') .val('Submit') .prop('disabled', 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').tablesorter;                }); mw.loader.using('jquery.makeCollapsible', function {                    $('.mw-collapsible').makeCollapsible;                }); },

/**            * Sanitise any HTML used in labels *            * @param html {string} A HTML string to be sanitised * @returns {jQuery.object} A jQuery object representing the sanitised HTML */           sanitiseLabels: function (html) { var whitelistAttrs = [ // mainly for span/div tags 'style', // for anchor tags 'href', 'title', // for img tags 'src', 'alt', 'height', 'width', // misc 'class' ],                   whitelistTags = [ 'a', 'span', 'div', 'img', 'strong', 'b', 'em', 'i', 'br' ],                   // parse the HTML string, removing script tags at the same time $html = $.parseHTML(html, /* document */ null, /* keepscripts */ false), // append to a div so we can navigate the node tree $div = $(' ').append($html);

$div.find('*').each(function {                    var $this = $(this),                        tagname = $this.prop('tagName').toLowerCase,                        attrs,                        array,                        href;

if (whitelistTags.indexOf(tagname) === -1) { mw.log('Disallowed tagname: ' + tagname); $this.remove; return; }

attrs = $this.prop('attributes'); array = Array.prototype.slice.call(attrs);

array.forEach(function (attr) {                       if (whitelistAttrs.indexOf(attr.name) === -1) {                            mw.log('Disallowed attribute: ' + attr.name + ', tagname: ' + tagname);                            $this.removeAttr(attr.name);                            return;                        }

// make sure there's nasty in nothing in href attributes if (attr.name === 'href') { href = $this.attr('href');

if (                               // disable warnings about script URLs                                // jshint -W107                                href.indexOf('javascript:') > -1 ||                                // the mw sanitizer doesn't like these                                // so lets follow suit                                // apparently it's something microsoft dreamed up                                href.indexOf('vbscript:') > -1                                // jshint +W107                            ) { mw.log('Script URL detected in ' + tagname); $this.removeAttr('href'); }                       }                    });                });

return $div.contents; },

/**            * Handlers for parameter input types */           tParams: { /**                * Handler for 'fixed' inputs *                * @param $td {jQuery.object} A jQuery object representing a table cell to                 *                            add content to                 * @param param {object} An object containing the configuration of a parameter * @returns {jQuery.object} A jQuery object representing the completed table cell */               fixed: function ($td, param) { $td.text(param.def); return $td; },

/**                * Handler for select dropdowns *                * @param $td {jQuery.object} A jQuery object representing a table cell to                 *                            add content to                 * @param param {object} An object containing the configuration of a parameter * @param id {String} A string representing the id to be added to the input * @returns {jQuery.object} A jQuery object representing the completed table cell */               select: function ($td, param, id) { var $select = $(' ') .attr({                               name: id,                                id: id                            }), opts = param.range.split(',');

opts.forEach(function (opt) {                       // undo the mw.html.escape call used when creating the params object                        // and defer the escaping to $.fn.val and $.fn.text instead                        opt = opt.replace(/&gt;/g, '>')                                 .replace(/&lt;/g, '<')                                 .replace(/&amp;/g, '&')                                 .replace(/&quot;/g, '"')                                 .replace(/&#039;/g, '\'');

var $option = $(' ') .val(opt) .text(opt);

if (opt === param.def) { $option.prop('selected', true); }

$select.append($option); });

$td.append($select); return $td; },

/**                * Handler for checkbox inputs *                * @param $td {jQuery.object} A jQuery object representing a table cell to                 *                            add content to                 * @param param {object} An object containing the configuration of a parameter * @param id {String} A string representing the id to be added to the input * @returns {jQuery.object} A jQuery object representing the completed table cell */               check: function ($td, param, id) { var $input = $(' ') .attr({                               type: 'checkbox',                                name: id,                                id: id                            });

if (                       param.def === 'true' ||                        (param.range !== undefined && param.def === param.range.split(',')[0])                    ) { $input.prop('checked', true); }

$td.append($input); return $td; },

/**                * Handler for hiscore inputs *                * @param $td {jQuery.object} A jQuery object representing a table cell to                 *                            add content to                 * @param param {object} An object containing the configuration of a parameter * @param id {String} A string representing the id to be added to the input * @returns {jQuery.object} A jQuery object representing the completed table cell */               hs: function ($td, param, id) { var self = this, lookups = {}, range = param.range.split(';'), $input = $(' ') .attr({                               type: 'text',                                name: id,                                id: id                            }) // prevent submission of form when pressing enter .keydown(function (e) {                               if (e.which === 13) {                                    $('#' + $(e.currentTarget).attr('id') + '-button').click;                                    e.preventDefault;                                }                            }), $button = $(' ') .addClass('jcLookup') .attr({                               type: 'button',                                name: id + '-button',                                id: id + '-button',                                'data-param': param.name                            }) .val('Lookup') .click(function (e) {                               var $target = $(e.target);

$target .val('Looking up...') .prop('disabled', true);

var lookup = self.lookups[$target.attr('data-param')], // replace spaces with _ for the query name = $('#' + lookup.id) .val // @todo will this break for plyers with multiple spaces //      in their name? e.g. suomi's old display name .replace(/\s+/g, '_');

$.ajax({                                   url: 'https://query.yahooapis.com/v1/public/yql?q=select%20*%20from%20htmlstring%20where%20url%3D\'http%3A%2F%2Fservices.runescape.com%2Fm%3Dhiscore%2Findex_lite.ws%3Fplayer%3D' + name + '\'&format=json&env=store%3A%2F%2Fdatatables.org%2Falltableswithkeys&callback=',                                    dataType: 'json',                                    async: false,                                    timeout: 10000 // msec                                }) .done(function (data) {                                   var hsdata;

if (data.query.results) { hsdata = $(data.query.results.result) .text .trim .split(/\n+/g);

lookup.params.forEach(function (param) {                                           var id = helper.getId.call(self, param.param),                                                $input = $('#' + id);

$input.val(hsdata[param.skill].split(',')[param.val]); });                                       // store in localStorage for future use                                        window.localStorage.hsname = name;                                    } else {                                        helper.showError.call(self, 'The player "' + name + '" does not exist, is banned or unranked, or we couldn\'t fetch your hiscores. Please enter the data manually.');                                   }

$('#' + self.form + ' input[type="button"].jcLookup') .val('Lookup') .prop('disabled', false); })                               .fail(function (xhr, status, error) { $('#' + self.form + ' input[type="button"].jcLookup') .val('Lookup') .prop('disabled', false);

helper.showError.call(                                       self,                                        xhr + ': ' + status + ': ' + error                                    ); });                           });

// attempt to pull user's name from localStorage if (window.localStorage.hsname !== undefined) { $input.val(window.localStorage.hsname); }

$td.append($input, ' ', $button);

lookups[param.name] = { id: id, params: [] };                   range.forEach(function (el) {                        // to catch empty strings                        if (!el) {                            return;                        }

var spl = el.split(',');

lookups[param.name].params.push({                           param: spl[0],                            skill: spl[1],                            val: spl[2]                        }); });

// merge lookups into one object if (!self.lookups) { self.lookups = lookups; } else { self.lookups = $.extend(self.lookups, lookups); }

return $td; },

/**                * Default handler for inputs *                * @param $td {jQuery.object} A jQuery object representing a table cell to                 *                            add content to                 * @param param {object} An object containing the configuration of a parameter * @param id {String} A string representing the id to be added to the input * @returns {jQuery.object} A jQuery object representing the completed table cell */               def: function ($td, param, id) { var $input = $(' ') .attr({                               type: 'text',                                name: id,                                id: id                            }) .val(param.def);

$td.append($input);

if (param.type === 'article') { this.acInputs.push(id); }

return $td; }           }        };

/**    * Create an instance of `Calc` * and parse the config stored in `elem` *    * @param elem {Element} An Element representing the HTML tag that contains *                      the calculator's configuration */   function Calc(elem) { var self = this, $elem = $(elem), lines, config; // support div tags for config as well as pre // be aware using div tags relies on wikitext for parsing // so you can't use anchor or img tags // use the wikitext equivalent instead 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 tags lines = $elem.text; }       lines = lines.split('\n'); config = helper.parseConfig.call(this, lines);

// merge config in       $.extend(this, config); this.acInputs = [];

/**        * @todo document */       this.getInput = function (id) { if (id) { id = helper.getId.call(self, id); return $('#' + id); }           return $('#jsForm-' + self.form).find('select, input'); };   }    /**     * Helper function for getting the id of an input *    * @param id {string} The id of the input as specified by the calculator config. * @returns {string} The true id of the input with prefixes. */   Calc.prototype.getId = function (id) { var self = this, inputId = helper.getId.call(self, id);

return inputId; };

/**    * Build the calculator form */   Calc.prototype.setupCalc = function  { var self = this, $form = $(' ') .attr({                   action: '#',                    id: 'jsForm-' + self.form                }) .submit(function (e) {                   e.preventDefault;                    helper.submitForm.call(self);                }), $table = $(' ') .addClass('wikitable') .addClass('jcTable');

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

var id = helper.getId.call(self, param.name), $tr = $(' '), $td = $(' '), method = helper.tParams[param.type] ? param.type : 'def', // sanitise any HTML before inserting it               // need this check otherwise jQuery cries // might need slightly better check in edge cases, but this should do for now label = param.label.indexOf('<') > -1 ? helper.sanitiseLabels(param.label) : param.label;

// add label $tr.append(               $(' ')                    .append( $(' ')                           .attr('for', id) .html(label) )           );

$td = helper.tParams[method].call(self, $td, param, id); $tr.append($td);

if (param.type === 'semihidden') { $tr.hide; }

$table.append($tr); });

$table.append(           $(' ')                .append( $(' ')                       .addClass('jcSubmit') .attr('colspan', '2') .append(                           $(' ')                                .attr('type', 'submit')                                .val('Submit')                        ) )       );

$form.append($table); if (self.configError) { $form.append(self.configError); }

$('#bodyContent, #WikiaArticle') .find('#' + self.form) .empty .append($form);

// Enable suggest on article fields mw.loader.using(['mediawiki.api','jquery.ui.autocomplete'], function {            self.acInputs.forEach(function (input) { $('#' + input).autocomplete({                   // matching wikia's search min length                    minLength: 3,                    source: function(request, response) {                        var term = request.term;

if (term in cache) { response(cache[term]); return; }

(new mw.Api) .get({                               action: 'opensearch',                                search: term,                                // default to main namespace                                namespace: self.suggestns.join('|') || 0,                                suggest: ''                            }) .done(function (data) {                               cache[term] = data[1];                                response(data[1]);                            }); }               });            });        });    };    /**     * @todo     */    function lookupCalc(calcId) {        return calcStore[calcId];    }

/**    * @todo */   function init { $('.jcConfig').each(function {            var c = new Calc(this);            c.setupCalc;            calcStore[c.form] = c;        }); // allow scripts to hook into calc setup completion mw.hook('rscalc.setupComplete').fire; }

$(init); rs.calc = {}; rs.calc.lookup = lookupCalc;

}(jQuery, mediaWiki, rswiki));