(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); } }); })();