User:SHB2000/currency updater.js

(function () {     'use strict';      var $exchangeBoxes = $('.mw-parser-output .infobox, .mw-parser-output table, .mw-parser-output .BoxTemplate').filter(function() {         var text = $(this).text().toLowerCase();         return text.indexOf('exchange rates for') !== -1 && text.indexOf('exchange rates fluctuate') !== -1;     });      if (!$exchangeBoxes.length) {         return;     }      var currencies = [         'USD', 'EUR', 'GBP', 'AUD', 'CAD', 'NZD', 'JPY', 'CHF', 'CNY', 'INR',         'HKD', 'SGD', 'MYR', 'THB', 'ZAR', 'IDR', 'PHP', 'DKK', 'SEK', 'NOK',          'SAR', 'VND', 'KRW', 'TWD', 'RUB', 'BYN'     ];      var nonStandardBases = {         'JPY': 100,         'INR': 100,         'THB': 10,         'IDR': 10000,         'PHP': 100,         'VND': 10000,         'KRW': 1000,         'TWD': 10,         'RUB': 100     };      function extractTemplate(text) {         var regex = /\{\{/g;         var m;         while ((m = regex.exec(text)) !== null) {             var start = m.index;             var depth = 0;             var end = -1;             for (var i = start; i < text.length - 1; i++) {                 if (text[i] === '{' && text[i+1] === '{') {                     depth++;                     i++;                 } else if (text[i] === '}' && text[i+1] === '}') {                     depth--;                     i++;                     if (depth === 0) {                         end = i + 1;                         break;                     }                 }             }             if (end !== -1) {                 var tplText = text.substring(start, end);                 var nameMatch = tplText.match(/^\{\{\s*([^\|\}]+)/);                 if (nameMatch) {                     var name = nameMatch[1].trim().toLowerCase().replace(/_/g, ' ');                     if (name.indexOf('template:') === 0) {                         name = name.substring(9).trim();                     }                     if (name.indexOf('exchange rate') !== -1) {                         return tplText;                     }                 }             }         }         return null;     }      mw.loader.using(['oojs-ui-core', 'oojs-ui-windows', 'oojs-ui-widgets', 'mediawiki.api', 'mediawiki.ForeignApi']).then(function () {                  var cachedWikitext = null;         var api = new mw.Api();         var prefetchPromise = api.get({             action: 'query',             prop: 'revisions',             titles: mw.config.get('wgPageName'),             rvprop: 'content',             rvslots: 'main',             formatversion: 2         }).then(function (data) {             var page = data.query.pages[0];             if (page && !page.missing) {                 cachedWikitext = page.revisions[0].slots.main.content;             }         });          var cachedIsoCode = null;         var isoPrefetchPromise = null;         var itemId = mw.config.get('wgWikibaseItemId');                  if (itemId) {             var foreignApi = new mw.ForeignApi('https://www.wikidata.org/w/api.php');             isoPrefetchPromise = foreignApi.get({                 action: 'wbgetentities',                 ids: itemId,                 props: 'claims',                 formatversion: 2             }).then(function(data) {                 var entity = data.entities[itemId];                 if (entity && entity.claims && entity.claims.P38) {                     var currencyItemId = entity.claims.P38[0].mainsnak.datavalue.value.id;                     return foreignApi.get({                         action: 'wbgetentities',                         ids: currencyItemId,                         props: 'claims',                         formatversion: 2                     });                 }                 return $.Deferred().reject();             }).then(function(data) {                 if (data && data.entities) {                     var currencyId = Object.keys(data.entities)[0];                     var entity = data.entities[currencyId];                     if (entity && entity.claims && entity.claims.P498) {                         cachedIsoCode = entity.claims.P498[0].mainsnak.datavalue.value;                     }                 }             });         }          $exchangeBoxes.each(function (index, box) {             var $box = $(box);                          if ($box.css('position') === 'static') {                 $box.css('position', 'relative');             }              var $editLink = $('<a>')                 .text('(edit)')                 .attr('href', '#')                 .css({                     'position': 'absolute',                     'bottom': '5px',                     'right': '10px',                     'font-size': '11px',                     'cursor': 'pointer'                 });              $box.append($editLink);              $editLink.on('click', function (e) {                 e.preventDefault();                 openUpdaterDialog();             });         });          function openUpdaterDialog() {             var windowManager = new OO.ui.WindowManager();             $(document.body).append(windowManager.$element);              function CurrencyUpdaterDialog(config) {                 CurrencyUpdaterDialog.super.call(this, config);             }             OO.inheritClass(CurrencyUpdaterDialog, OO.ui.ProcessDialog);              CurrencyUpdaterDialog.static.name = 'currencyUpdaterDialog';             CurrencyUpdaterDialog.static.title = 'Update exchange rates';             CurrencyUpdaterDialog.static.actions = [                 { action: 'save', label: 'Save changes', flags: ['primary', 'progressive'] },                 { label: 'Cancel', flags: 'safe' }             ];              CurrencyUpdaterDialog.prototype.initialize = function () {                 CurrencyUpdaterDialog.super.prototype.initialize.call(this);                 this.panel = new OO.ui.PanelLayout({ padded: true, expanded: false });                                  this.warningMessage = new OO.ui.MessageWidget({                     type: 'warning',                     label: 'This exchange rate box is transcluded from a common template.'                 });                 this.warningMessage.$element.hide().css('margin-bottom', '1em');                  this.content = new OO.ui.FieldsetLayout({ label: 'Exchange rate parameters' });                  this.currencyInput = new OO.ui.TextInputWidget();                                  this.currencyCodeInput = new OO.ui.TextInputWidget({ placeholder: 'Prefix (e.g. $)' });                 this.currencyCodeAfterInput = new OO.ui.TextInputWidget({ placeholder: 'Suffix (e.g. lek)' });                                  this.codeComboWidget = new OO.ui.Widget();                 this.codeComboWidget.$element.css({ 'display': 'flex', 'gap': '0.5em' }).append(                     this.currencyCodeInput.$element.css('flex', '1'),                     this.currencyCodeAfterInput.$element.css('flex', '1')                 );                  this.dateInput = new OO.ui.TextInputWidget();                                  this.calendarButton = new OO.ui.ButtonWidget({                     label: '📅 Today',                     title: 'Set to current month'                 });                  this.dateLayout = new OO.ui.ActionFieldLayout(this.dateInput, this.calendarButton, {                     label: 'As of date:'                 });                                  this.content.addItems([                     new OO.ui.FieldLayout(this.currencyInput, { label: 'Currency name:' }),                     new OO.ui.FieldLayout(this.codeComboWidget, { label: 'Currency code:' }),                     this.dateLayout                 ]);                  this.inputs = {};                 this.fieldLayouts = {};                  var dialog = this;                  this.calendarButton.on('click', function () {                     var months = ['January', 'February', 'March', 'April', 'May', 'June', 'July', 'August', 'September', 'October', 'November', 'December'];                     var d = new Date();                     var currentMonthYear = months[d.getMonth()] + ' ' + d.getFullYear();                     dialog.dateInput.setValue(currentMonthYear);                 });                                  var options = currencies.map(function (code) {                     return { data: code, label: code };                 });                                  this.searchComboBox = new OO.ui.ComboBoxInputWidget({                     options: options,                     placeholder: 'Select or type a currency code to add...'                 });                  this.addButton = new OO.ui.ButtonWidget({                     label: 'Add',                     flags: ['progressive']                 });                  this.searchLayout = new OO.ui.ActionFieldLayout(this.searchComboBox, this.addButton, {                     label: 'Add currency field:',                     align: 'top'                 });                 this.content.addItems([this.searchLayout]);                  this.addButton.on('click', function () {                     var code = dialog.searchComboBox.getValue().toUpperCase().trim();                     if (currencies.indexOf(code) !== -1 && !dialog.inputs[code]) {                         dialog.addCurrencyField(code, '');                         dialog.searchComboBox.setValue('');                         dialog.updateDropdownOptions();                     }                 });                  this.summaryFieldset = new OO.ui.FieldsetLayout({ label: 'Edit summary' });                 this.summaryInput = new OO.ui.TextInputWidget();                 this.summaryFieldset.addItems([                     new OO.ui.FieldLayout(this.summaryInput, { label: 'Additional edit summary:', align: 'top' })                 ]);                  this.disclaimerLabel = new OO.ui.LabelWidget({                     label: new OO.ui.HtmlSnippet(                         '<div style="font-size: 0.85em; margin-top: 1.5em; color: #54595d; text-align: center; line-height: 1.4;">' +                         'By clicking the "Save changes" button, you agree to the <a href="https://foundation.wikimedia.org/wiki/Policy:Terms_of_Use" target="_blank">Terms of Use</a>, and you irrevocably agree to release your contribution under the <a href="https://creativecommons.org/licenses/by-sa/4.0/" target="_blank">CC-BY-SA 4.0 License</a>.' +                         '</div>'                     )                 });                  this.panel.$element.append(                     this.warningMessage.$element,                     this.content.$element,                      this.summaryFieldset.$element,                      this.disclaimerLabel.$element                 );                                  this.$body.append(this.panel.$element);                  if (isoPrefetchPromise) {                     isoPrefetchPromise.then(function () {                         if (cachedIsoCode) {                             Object.keys(dialog.fieldLayouts).forEach(function (code) {                                 dialog.updateFieldLabel(code);                             });                         }                     });                 }                  this.fetchCurrentTemplateValues();             };              CurrencyUpdaterDialog.prototype.updateDropdownOptions = function () {                 var dialog = this;                 var availableOptions = currencies.filter(function (code) {                     return !dialog.inputs[code];                 }).map(function (code) {                     return { data: code, label: code };                 });                                  if (typeof this.searchComboBox.setOptions === 'function') {                     this.searchComboBox.setOptions(availableOptions);                 } else {                     var menuItems = availableOptions.map(function (opt) {                         return new OO.ui.MenuOptionWidget({ data: opt.data, label: opt.label });                     });                     this.searchComboBox.getMenu().clearItems().addItems(menuItems);                 }             };              CurrencyUpdaterDialog.prototype.updateFieldLabel = function (code) {                 if (this.fieldLayouts[code]) {                     var base = nonStandardBases[code] || 1;                     var inlineText = code + ' rate';                                          if (cachedIsoCode && code !== cachedIsoCode) {                         if (code === 'RUB' || code === 'BYN' || cachedIsoCode === 'RUB' || cachedIsoCode === 'BYN') {                             var unsupportedCode = (code === 'RUB' || code === 'BYN') ? code : cachedIsoCode;                             inlineText += ' <small>(xe.com not supported for ' + unsupportedCode + ')</small>';                         } else {                             inlineText += ' <small>(<a href="https://www.xe.com/currencyconverter/convert/?Amount=' + base + '&From=' + code + '&To=' + cachedIsoCode + '" target="_blank">xe.com</a> rate)</small>';                         }                     }                                          this.fieldLayouts[code].setLabel(new OO.ui.HtmlSnippet(inlineText));                 }             };              CurrencyUpdaterDialog.prototype.addCurrencyField = function (code, initialValue) {                 var dialog = this;                                  this.inputs[code] = new OO.ui.TextInputWidget({ value: initialValue });                                  var binButton = new OO.ui.ButtonWidget({                     label: new OO.ui.HtmlSnippet('<img src="/wiki/Special:FilePath/Delete-filled.svg?width=14" height="14" alt="Remove" style="vertical-align: middle;">'),                     title: 'Remove this currency',                     flags: ['destructive']                 });                  var base = nonStandardBases[code] || 1;                 var layoutOpts = {                      label: code + ' rate:'                  };                  if (base !== 1) {                     layoutOpts.help = new OO.ui.HtmlSnippet('Conversion rate for <b>' + base.toLocaleString() + '</b> ' + code);                     layoutOpts.helpInline = true;                 }                  this.fieldLayouts[code] = new OO.ui.ActionFieldLayout(this.inputs[code], binButton, layoutOpts);                                  this.updateFieldLabel(code);                  binButton.on('click', function () {                     dialog.content.removeItems([dialog.fieldLayouts[code]]);                     delete dialog.inputs[code];                     delete dialog.fieldLayouts[code];                     if (typeof dialog.updateSize === 'function') {                         dialog.updateSize();                     }                     dialog.updateDropdownOptions();                 });                  this.content.addItems([this.fieldLayouts[code]]);                 if (typeof this.updateSize === 'function') {                     this.updateSize();                 }             };              CurrencyUpdaterDialog.prototype.fetchCurrentTemplateValues = function () {                 var dialog = this;                                  dialog.pushPending();                                  prefetchPromise.then(function () {                     dialog.popPending();                                          if (!cachedWikitext) return;                                          dialog.fullWikitext = cachedWikitext;                     dialog.templateText = extractTemplate(dialog.fullWikitext);                      var isTranscluded = false;                     var transcludedTemplateName = 'Template:Exchange rates';                      if (!dialog.templateText) {                         isTranscluded = true;                     } else {                         var baseNameMatch = dialog.templateText.match(/^\{\{\s*([^\|\}]+)/);                         if (baseNameMatch) {                             var baseName = baseNameMatch[1].trim().replace(/_/g, ' ');                                                          if (baseName.toLowerCase().indexOf('template:') === 0) {                                 baseName = baseName.substring(9).trim();                             }                                                          var normalizedName = baseName.toLowerCase();                                                          if (normalizedName !== 'exchange rates' || dialog.templateText.indexOf('|') === -1) {                                 isTranscluded = true;                                 transcludedTemplateName = 'Template:' + baseName.charAt(0).toUpperCase() + baseName.slice(1);                             }                         } else {                             isTranscluded = true;                         }                     }                      if (isTranscluded) {                         var mwArticlePath = mw.config.get('wgArticlePath') || '/wiki/$1';                         var templateLink = mwArticlePath.replace('$1', encodeURIComponent(transcludedTemplateName).replace(/%3A/g, ':').replace(/%20/g, '_').replace(/%2F/g, '/'));                         var linkHtml = '<a href="' + templateLink + '" target="_blank" style="font-weight: bold;">' + transcludedTemplateName + '</a>';                                                  dialog.warningMessage.setLabel(new OO.ui.HtmlSnippet(                             'This exchange rate box is transcluded from a common template. You must edit ' + linkHtml + ' directly to update these rates.'                         ));                          dialog.warningMessage.$element.show();                         dialog.content.$element.hide();                         dialog.summaryFieldset.$element.hide();                         dialog.disclaimerLabel.$element.hide();                         dialog.getActions().setAbilities({ save: false });                                                  if (typeof dialog.updateSize === 'function') {                             dialog.updateSize();                         }                         return;                     }                      var currencyMatch = dialog.templateText.match(/\|\s*currency\s*=\s*([^|\n}]+)/);                     if (currencyMatch) dialog.currencyInput.setValue(currencyMatch[1].trim());                      var currencyCodeMatch = dialog.templateText.match(/\|\s*currencyCode\s*=\s*([^|\n}]+)/);                     if (currencyCodeMatch) dialog.currencyCodeInput.setValue(currencyCodeMatch[1].trim());                      var currencyCodeAfterMatch = dialog.templateText.match(/\|\s*currencyCodeAfter\s*=\s*([^|\n}]+)/);                     if (currencyCodeAfterMatch) dialog.currencyCodeAfterInput.setValue(currencyCodeAfterMatch[1].trim());                      var dateMatch = dialog.templateText.match(/\|\s*date\s*=\s*([^|\n}]+)/);                     if (dateMatch) dialog.dateInput.setValue(dateMatch[1].trim());                      var layoutsToAdd = [];                                          currencies.forEach(function (code) {                         var regex = new RegExp('\\|\\s*' + code + '\\s*=\\s*([^|\\n}]+)', 'i');                         var match = dialog.templateText.match(regex);                         if (match) {                             dialog.inputs[code] = new OO.ui.TextInputWidget({ value: match[1].trim() });                                                          var binButton = new OO.ui.ButtonWidget({                                 label: new OO.ui.HtmlSnippet('<img src="/wiki/Special:FilePath/Delete-filled.svg?width=14" height="14" alt="Remove" style="vertical-align: middle;">'),                                 title: 'Remove this currency',                                 flags: ['destructive']                             });                              var base = nonStandardBases[code] || 1;                             var layoutOpts = {                                  label: code + ' rate:'                              };                              if (base !== 1) {                                 layoutOpts.help = new OO.ui.HtmlSnippet('Conversion rate for <b>' + base.toLocaleString() + '</b> ' + code);                                 layoutOpts.helpInline = true;                             }                              dialog.fieldLayouts[code] = new OO.ui.ActionFieldLayout(dialog.inputs[code], binButton, layoutOpts);                                                          dialog.updateFieldLabel(code);                              binButton.on('click', function () {                                 dialog.content.removeItems([dialog.fieldLayouts[code]]);                                 delete dialog.inputs[code];                                 delete dialog.fieldLayouts[code];                                 if (typeof dialog.updateSize === 'function') {                                     dialog.updateSize();                                 }                                 dialog.updateDropdownOptions();                             });                              layoutsToAdd.push(dialog.fieldLayouts[code]);                         }                     });                      if (layoutsToAdd.length > 0) {                         dialog.content.addItems(layoutsToAdd);                         if (typeof dialog.updateSize === 'function') {                             dialog.updateSize();                         }                     }                                          dialog.updateDropdownOptions();                  }).fail(function () {                     dialog.popPending();                 });             };              CurrencyUpdaterDialog.prototype.getActionProcess = function (action) {                 var dialog = this;                 if (action === 'save') {                     return new OO.ui.Process(function () {                         return dialog.saveTemplateChanges();                     });                 }                 return CurrencyUpdaterDialog.super.prototype.getActionProcess.call(this, action);             };              CurrencyUpdaterDialog.prototype.saveTemplateChanges = function () {                 var dialog = this;                 var api = new mw.Api();                 var wikitext = dialog.fullWikitext;                 var templateText = dialog.templateText;                  if (!wikitext || !templateText) {                     return $.Deferred().reject(new OO.ui.Error('Failed to retrieve page text.')).promise();                 }                  var updatedTemplateText = templateText;                                  var currencyVal = dialog.currencyInput.getValue().trim();                 if (updatedTemplateText.match(/\|\s*currency\s*=/)) {                     updatedTemplateText = updatedTemplateText.replace(/(\|\s*currency\s*=\s*)([^|\n}]+)/, '$1' + currencyVal);                 } else if (currencyVal) {                     updatedTemplateText = updatedTemplateText.replace(/(\s*\}\})$/, '\n| currency=' + currencyVal + '$1');                 }                  var codeVal = dialog.currencyCodeInput.getValue().trim();                 if (updatedTemplateText.match(/\|\s*currencyCode\s*=/)) {                     updatedTemplateText = updatedTemplateText.replace(/(\|\s*currencyCode\s*=\s*)([^|\n}]+)/, '$1' + codeVal);                 } else if (codeVal) {                     updatedTemplateText = updatedTemplateText.replace(/(\s*\}\})$/, '\n| currencyCode=' + codeVal + '$1');                 }                  var codeAfterVal = dialog.currencyCodeAfterInput.getValue().trim();                 if (updatedTemplateText.match(/\|\s*currencyCodeAfter\s*=/)) {                     updatedTemplateText = updatedTemplateText.replace(/(\|\s*currencyCodeAfter\s*=\s*)([^|\n}]+)/, '$1' + codeAfterVal);                 } else if (codeAfterVal) {                     updatedTemplateText = updatedTemplateText.replace(/(\s*\}\})$/, '\n| currencyCodeAfter=' + codeAfterVal + '$1');                 }                  if (updatedTemplateText.match(/\|\s*date\s*=/)) {                     updatedTemplateText = updatedTemplateText.replace(/(\|\s*date\s*=\s*)([^|\n}]+)/, '$1' + dialog.dateInput.getValue().trim());                 } else if (dialog.dateInput.getValue().trim()) {                     updatedTemplateText = updatedTemplateText.replace(/(\s*\}\})$/, '\n| date=' + dialog.dateInput.getValue().trim() + '$1');                 }                  currencies.forEach(function (code) {                     if (dialog.inputs[code]) {                         var val = dialog.inputs[code].getValue().trim();                         var regex = new RegExp('(\\|\\s*' + code + '\\s*=\\s*)([^|\\n}]+)', 'i');                                                  if (updatedTemplateText.match(regex)) {                             updatedTemplateText = updatedTemplateText.replace(regex, '$1' + val);                         } else if (val) {                             updatedTemplateText = updatedTemplateText.replace(/(\s*\}\})$/, '\n| ' + code + '=' + val + '$1');                         }                     } else {                         var removeRegex = new RegExp('\\n?\\|\\s*' + code + '\\s*=\\s*[^|\\n}]*', 'i');                         if (updatedTemplateText.match(removeRegex)) {                             updatedTemplateText = updatedTemplateText.replace(removeRegex, '');                         }                     }                 });                  var updatedWikitext = wikitext.replace(templateText, updatedTemplateText);                 var customSummary = dialog.summaryInput.getValue().trim();                 var editSummary = customSummary ? 'Updating currency exchange rates – ' + customSummary : 'Updating currency exchange rates';                  return api.postWithToken('csrf', {                     action: 'edit',                     title: mw.config.get('wgPageName'),                     text: updatedWikitext,                     summary: editSummary,                     minor: true                 }).then(function () {                     dialog.close();                     window.location.reload();                 }).fail(function (code, err) {                     return new OO.ui.Error('Error saving: ' + code);                 });             };              var dialog = new CurrencyUpdaterDialog({                 size: 'medium'             });              windowManager.addWindows([dialog]);             windowManager.openWindow(dialog);         }     }); })();