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