MediaWiki:Gadget-TravelTracker.js

// <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>