// <nowiki> (function(mw, $) { var userName = mw.config.get('wgUserName'); function injectHeaderLink() { if (userName === null) return; mw.loader.using(['mediawiki.util']).then(function() { mw.util.addPortletLink( 'p-personal', mw.util.getUrl('User:' + userName + '/Visited'), 'My Travel Map', 'pt-travelmap', 'View or update your travel map', null, '#pt-preferences' ); mw.util.addCSS(`.mw-ui-icon-vector-gadget-pt-travelmap { /* todo*/ }`); }); } injectHeaderLink(); var pageName = mw.config.get('wgPageName'); if (mw.config.get('wgNamespaceNumber') !== 2 || !mw.config.get('wgTitle').match(/\/Visited$/)) return; mw.loader.using(['mediawiki.api', 'mediawiki.util', 'oojs-ui-windows', 'oojs-ui-core', 'oojs-ui-widgets']).done(function() { var api = new mw.Api(); var dataModule = 'Module:Countries.json'; var subdivModule = 'Module:CountrySubdivisions.json'; var isExists = mw.config.get('wgArticleId') > 0; var continentNames = { "af": "Africa", "as": "Asia", "eu": "Europe", "na": "North America", "oc": "Oceania", "sa": "South America", "an": "Southern Ocean and Antarctica" }; if (!isExists) { var $contentArea = $('#mw-content-text'); if ($contentArea.length) { $contentArea.empty().append( '<div class="mw-message-box mw-message-box-notice" style="border-radius: 8px; border-left-width: 5px;">' + '<strong>Welcome to your Travel Tracker!</strong><br>' + 'Select the countries/territories you have visited below to generate your personalised map. Make sure to hit the "publish changes" button to generate and publish your map.<br>' + '</div>' + '<div id="visited-tool-placeholder"></div>' ); } } api.get({ action: 'query', prop: 'revisions', titles: [pageName, dataModule, subdivModule], rvprop: 'content', formatversion: 2 }).done(function(res) { var pages = res.query.pages; var pageData = pages.find(p => p.title.replace(/ /g, '_') === pageName); var moduleData = pages.find(p => p.title === dataModule); var subdivData = pages.find(p => p.title === subdivModule); if (!moduleData || !moduleData.revisions) return; var countries = JSON.parse(moduleData.revisions[0].content); var rawSubdivList = (subdivData && subdivData.revisions) ? JSON.parse(subdivData.revisions[0].content) : []; var subdivisionsGrouped = {}; if (Array.isArray(rawSubdivList)) { rawSubdivList.forEach(function(item) { if (item && item.c) { if (!subdivisionsGrouped[item.c]) { subdivisionsGrouped[item.c] = []; } subdivisionsGrouped[item.c].push(item); } }); } var content = (pageData && pageData.revisions) ? pageData.revisions[0].content : ''; var match = content.match(/{{VisitedMap\s*\|\s*([^}|]*)(?:\|\s*([^}|]*))?/); var savedCountryQids = match && match[1] ? match[1].split(',').map(s => s.trim()).filter(Boolean) : []; var savedSubdivQids = match && match[2] ? match[2].split(',').map(s => s.trim()).filter(Boolean) : []; startBuilding(countries, subdivisionsGrouped, savedCountryQids, savedSubdivQids); }); function startBuilding(countries, subdivisions, savedCountryQids, savedSubdivQids) { var $placeholder = $('#visited-tool-placeholder'); if (!$placeholder.length) { setTimeout(function() { startBuilding(countries, subdivisions, savedCountryQids, savedSubdivQids); }, 200); return; } buildUI($placeholder, countries, subdivisions, savedCountryQids, savedSubdivQids); } function buildUI($placeholder, countries, subdivisions, savedCountryQids, savedSubdivQids) { var $statsBar = $('<div style="background: var(--background-color-neutral-subtle, #f8f9fa); border: 1px solid var(--border-color-base, #eaecf0); border-radius: 8px; padding: 15px 20px; margin: 20px 0; display: flex; justify-content: space-between; align-items: center; font-family: sans-serif;"></div>'); var $labelContainer = $('<div style="display: flex; flex-direction: column; gap: 4px;"></div>'); var $counterText = $('<span style="font-size: 1.1em; font-weight: 600; color: var(--color-emphasized, #202122);"></span>'); var $noticeText = $('<div style="font-size: 0.85em; color: var(--color-subtle, #54595d); line-height: 1.4;">' + 'Due to limitations with the <a href="https://www.mediawiki.org/wiki/Extension:Kartographer" target="_blank">Kartographer extension</a>, ' + 'you may need to give this page a hard refresh and/or <a href="' + mw.util.getUrl(pageName, {action: 'purge'}) + '">clear your cache</a> ' + 'if you have a large number of mapshapes. ' + '<br>Additionally, by clicking the "Publish changes" button, you agree to the <a href="https://foundation.wikimedia.org/wiki/Policy:Terms_of_Use" target="_blank" rel="noopener">Terms of Use</a>, you irrevocably agree to release your contribution under the <a href="https://creativecommons.org/licenses/by-sa/4.0/" target="_blank" rel="noopener">CC-BY-SA 4.0 License</a>, and you acknowledge that this map will be made public and will be visible to everyone.' + '</div>'); $labelContainer.append($counterText, $noticeText); $statsBar.append($labelContainer); $placeholder.empty().append($statsBar); var countryQidsSet = new Set(countries.map(c => c.q)); var activeSubdivisions = {}; savedSubdivQids.forEach(function(qid) { activeSubdivisions[qid] = true; }); var updateCounter = function() { var count = 0; $('#visited-tool-placeholder input.country-chkbx:checked').each(function() { if (countryQidsSet.has($(this).val())) { count++; } }); var linkHtml = '<a href="' + mw.util.getUrl('Wikivoyage:Travel_Tracker') + '" class="tracker-project-link" style="color: inherit; text-decoration: none;">travel map</a>'; $counterText.html('Update your ' + linkHtml + ' – ' + count + ' countries/territories visited'); }; mw.util.addCSS('.tracker-project-link:hover { text-decoration: underline !important; }'); var grouped = {}; countries.forEach(function(c) { var cont = c.c || "ot"; if (!grouped[cont]) grouped[cont] = []; grouped[cont].push(c); }); var sortedCodes = Object.keys(grouped).sort(function(a, b) { return (continentNames[a] || a).localeCompare(continentNames[b] || b); }); sortedCodes.forEach(function(code) { var name = continentNames[code] || "Other territories"; var $section = $('<div class="continent-card" style="margin-bottom: 25px; background: var(--background-color-base, #fff); border: 1px solid var(--border-color-base, #eaecf0); border-radius: 8px; overflow: hidden;">'); $section.append('<div style="background: var(--background-color-neutral-subtle, #f8f9fa); padding: 10px 15px; font-weight: bold; border-bottom: 1px solid var(--border-color-base, #eaecf0); color: var(--color-subtle, #54595d);">' + name + '</div>'); var $list = $('<div style="column-count: 4; column-gap: 20px; padding: 15px;">'); grouped[code].sort((a, b) => a.n.localeCompare(b.n)).forEach(function(c) { var isChecked = savedCountryQids.indexOf(c.q) !== -1 ? 'checked' : ''; var $item = $('<div style="transition: background 0.2s; border-radius: 4px; padding: 4px 8px; break-inside: avoid; display: inline-block; width: 100%; box-sizing: border-box;">' + '<label style="display: inline-flex; align-items: center; cursor: pointer; font-size: 0.9em; font-family: sans-serif; color: var(--color-base, #202122); width: 100%;">' + '<input type="checkbox" class="country-chkbx" data-qid="' + c.q + '" value="' + c.q + '" ' + isChecked + ' style="margin-right: 8px; cursor: pointer;"> ' + c.n + '</label></div>'); $item.find('input').on('change', updateCounter); $item.hover(function() { $(this).css('background', 'var(--background-color-progressive-subtle, #f0f4ff)'); }, function() { $(this).css('background', 'transparent'); }); var rawSubdivs = subdivisions[c.n] || []; var validSubdivs = rawSubdivs.filter(s => s.q); if (validSubdivs.length > 0) { var $subdivLink = $('<span style="font-size: 0.8em; margin-left: 6px; color: var(--color-progressive, #36c); cursor: pointer; font-family: sans-serif; white-space: nowrap;">[Regions]</span>'); $subdivLink.on('click', function(e) { e.preventDefault(); e.stopPropagation(); openSubdivisionsDialog(c.n, c.q, validSubdivs, activeSubdivisions); }); $item.find('label').append($subdivLink); } $list.append($item); }); $section.append($list); $placeholder.append($section); }); var $saveBtn = $('<button id="save-visited">Publish changes</button>').css({ 'background-color': 'var(--background-color-progressive, #36c)', 'color': '#fff', 'border': 'none', 'border-radius': '4px', 'padding': '10px 24px', 'font-size': '1em', 'font-weight': '600', 'font-family': 'sans-serif', 'cursor': 'pointer', 'transition': 'background 0.2s', 'box-shadow': '0 2px 4px rgba(0,0,0,0.1)', 'flex-shrink': '0', 'margin-left': '20px' }); $statsBar.append($saveBtn); $placeholder.append('<div id="save-status" style="text-align: right; margin-top: 5px; font-size: 0.85em; font-weight: bold; min-height: 1.2em; font-family: sans-serif;"></div>'); updateCounter(); $saveBtn.on('mouseenter', function() { $(this).css('background-color', 'var(--background-color-progressive--hover, #447ff5)'); }); $saveBtn.on('mouseleave', function() { $(this).css('background-color', 'var(--background-color-progressive, #36c)'); }); $saveBtn.on('click', function(e) { e.preventDefault(); var selectedCountries = []; var selectedSubdivs = []; $('#visited-tool-placeholder input.country-chkbx:checked').each(function() { selectedCountries.push($(this).val()); }); Object.keys(activeSubdivisions).forEach(function(subQid) { if (activeSubdivisions[subQid]) { selectedSubdivs.push(subQid); } }); var templateText = '{{VisitedMap|' + selectedCountries.join(',') + '|' + selectedSubdivs.join(',') + '}}'; var $btn = $(this); $btn.addClass('mw-ui-disabled').css('background-color', 'var(--background-color-disabled, #a2a9b1)').text('Saving...'); api.postWithToken('csrf', { action: 'edit', title: pageName, text: templateText, summary: (isExists ? 'Updating' : 'Creating') + ' travel map via [[Wikivoyage:Travel Tracker|TravelTracker gadget]]' }).done(function() { $('#save-status').text('Success! Reloading...').css('color', 'var(--color-success, #00af89)'); setTimeout(function() { location.reload(); }, 800); }); }); function openSubdivisionsDialog(countryName, countryQid, subdivisionsList, stateTracker) { function Subdialog(config) { Subdialog.super.call(this, config); } OO.inheritClass(Subdialog, OO.ui.ProcessDialog); Subdialog.static.name = 'subdivDialog_' + countryQid; Subdialog.static.title = countryName + ' - Regions'; Subdialog.static.actions = [ { action: 'save', label: 'Apply', flags: ['primary', 'progressive'] }, { label: 'Cancel', flags: 'safe' } ]; Subdialog.prototype.initialize = function() { Subdialog.super.prototype.initialize.call(this); this.panel = new OO.ui.PanelLayout({ padded: true, expanded: false }); var $disclaimer = $('<div style="margin-bottom: 15px; padding: 8px 12px; background-color: var(--background-color-warning-subtle, #fdf2d5); border-left: 4px solid var(--border-color-warning, #fc3); font-size: 0.85em; color: var(--color-base, #202122); font-family: sans-serif;"><strong>Note:</strong> You will still need to press <b>Publish changes</b> on the main subpage for these changes to be saved.</div>'); var $listContainer = $('<div style="display: flex; flex-direction: column; gap: 8px; font-family: sans-serif; max-height: 350px; overflow-y: auto;"></div>'); this.checkboxes = {}; subdivisionsList.sort((a, b) => a.n.localeCompare(b.n)).forEach(function(sub) { var checkbox = new OO.ui.CheckboxInputWidget({ selected: !!stateTracker[sub.q] }); var field = new OO.ui.FieldLayout(checkbox, { label: sub.n, align: 'inline' }); this.checkboxes[sub.q] = checkbox; $listContainer.append(field.$element); }, this); this.panel.$element.append($disclaimer, $listContainer); this.$body.append(this.panel.$element); }; Subdialog.prototype.getActionProcess = function(action) { var dialog = this; if (action === 'save') { return new OO.ui.Process(function() { var anyChecked = false; Object.keys(dialog.checkboxes).forEach(function(subQid) { if (dialog.checkboxes[subQid].isSelected()) { stateTracker[subQid] = true; anyChecked = true; } else { delete stateTracker[subQid]; } }); if (anyChecked) { var $countryCheckbox = $('#visited-tool-placeholder input.country-chkbx[data-qid="' + countryQid + '"]'); if ($countryCheckbox.length && !$countryCheckbox.prop('checked')) { $countryCheckbox.prop('checked', true); updateCounter(); } } dialog.close({ action: action }); }); } return Subdialog.super.prototype.getActionProcess.call(this, action); }; var windowManager = new OO.ui.WindowManager(); $(document.body).append(windowManager.$element); var dialog = new Subdialog({ size: 'medium' }); windowManager.addWindows([dialog]); windowManager.openWindow(dialog).closed.then(function() { windowManager.destroy(); }); } if (isExists) { var $deleteContainer = $('<div style="text-align: right; margin-top: 20px;"></div>'); var $deleteBtn = $('<button id="delete-visited" class="mw-ui-button mw-ui-destructive mw-ui-quiet" style="font-size: 0.85em; padding: 4px 8px;">Request deletion</button>'); $deleteBtn.on('click', function(e) { e.preventDefault(); var $btn = $(this); var windowManager = new OO.ui.WindowManager(); $(document.body).append(windowManager.$element); var dialog = new OO.ui.MessageDialog(); windowManager.addWindows([dialog]); windowManager.openWindow(dialog, { title: 'Request deletion', message: 'Warning: are you sure you want to request deletion of this map? This action will not be able to be automatically undone after a certain time period.', actions: [ { action: 'cancel', label: 'Cancel', flags: 'safe' }, { action: 'delete', label: 'Request deletion', flags: ['primary', 'destructive'] } ] }).closing.then(function(data) { if (data && data.action === 'delete') { $btn.addClass('mw-ui-disabled').text('Deleting...'); api.postWithToken('csrf', { action: 'edit', title: pageName, text: '{{delete|1=Author request}}', summary: 'Requesting deletion via [[Wikivoyage:Travel Tracker|TravelTracker gadget]]' }).done(function() { location.reload(); }); } windowManager.destroy(); }); }); $deleteContainer.append($deleteBtn); $placeholder.append($deleteContainer); } } }); })(mediaWiki, jQuery); // </nowiki>