Toggle menu
Toggle preferences menu
Toggle personal menu
Not logged in
Your IP address will be publicly visible if you make any edits.

MediaWiki:Common.js: Difference between revisions

MediaWiki interface page
Apply current entity colors immediately, then enhance from Cargo data
Color entity links independently of visible table columns
Line 1: Line 1:
/* Any JavaScript here will be loaded for all users on every page load. */
/* Any JavaScript here will be loaded for all users on every page load. */ /* HarvestWiki dynamic Cargo archive filters */ (function () { 'use strict'; function initHerbLocationFilters(root) { var catalogs = []; if (root && root.matches && root.matches('.hw-herb-catalog')) { catalogs.push(root); } if (root && root.querySelectorAll) { catalogs = catalogs.concat(Array.prototype.slice.call(root.querySelectorAll('.hw-herb-catalog'))); } catalogs.forEach(function (catalog) { if (catalog.dataset.hwLocationFiltersReady === '1') { return; } var table = catalog.querySelector('table'); var filterBar = document.querySelector('.hw-herb-filter'); if (!table || !filterBar) { return; } var headers = Array.prototype.slice.call(table.querySelectorAll('thead th')); var locationIndex = headers.findIndex(function (header) { return header.textContent.trim().toLowerCase() === 'location'; }); if (locationIndex < 0) { return; } var rows = Array.prototype.slice.call(table.querySelectorAll('tbody tr')); var counts = {}; rows.forEach(function (row) { var cell = row.querySelectorAll('td')[locationIndex]; var location = cell ? cell.textContent.trim() : ''; row.dataset.hwLocation = location; if (location) { counts[location] = (counts[location] || 0) + 1; } }); var locations = Object.keys(counts).sort(); var enabled = new Set(locations); filterBar.innerHTML = ''; var showAll = document.createElement('button'); showAll.type = 'button'; showAll.className = 'hw-filter-chip hw-filter-chip--all'; showAll.textContent = 'Show all'; filterBar.appendChild(showAll); var locationButtons = {}; locations.forEach(function (location) { var button = document.createElement('button'); button.type = 'button'; button.className = 'hw-filter-chip is-active'; button.setAttribute('aria-pressed', 'true'); button.textContent = location + ' (' + counts[location] + ')'; button.addEventListener('click', function () { if (enabled.has(location)) { enabled.delete(location); } else { enabled.add(location); } applyFilters(); }); locationButtons[location] = button; filterBar.appendChild(button); }); function applyFilters() { rows.forEach(function (row) { row.hidden = !enabled.has(row.dataset.hwLocation); }); locations.forEach(function (location) { var active = enabled.has(location); locationButtons[location].classList.toggle('is-active', active); locationButtons[location].setAttribute('aria-pressed', active ? 'true' : 'false'); }); showAll.disabled = enabled.size === locations.length; showAll.classList.toggle('is-active', enabled.size === locations.length); } showAll.addEventListener('click', function () { enabled = new Set(locations); applyFilters(); }); catalog.dataset.hwLocationFiltersReady = '1'; applyFilters(); }); } mw.hook('wikipage.content').add(function ($content) { initHerbLocationFilters($content && $content[0] ? $content[0] : document); }); }()); /* HarvestWiki admin-only Herb form UI */ (function () { 'use strict'; var groups = mw.config.get('wgUserGroups') || []; var categories = mw.config.get('wgCategories') || []; if (groups.indexOf('sysop') !== -1 || categories.indexOf('Herbs') === -1) { return; } function hideHerbFormActions() { ['ca-formedit', 'ca-formcreate', 'ca-formedit-sticky-header', 'ca-formcreate-sticky-header'].forEach(function (id) { var element = document.getElementById(id); if (element) { element.hidden = true; } }); } if (document.readyState === 'loading') { document.addEventListener('DOMContentLoaded', hideHerbFormActions); } else { hideHerbFormActions(); } }()); /* HarvestWiki rarity-colored Herb and Crystal table rows */ (function () { 'use strict'; var rarities = { common: true, uncommon: true, rare: true, legendary: true, mythic: true, secret: true, mortal: true, refined: true, spirit: true }; function normalizeRarity(value) { var rarity = (value || '').trim().toLowerCase(); return rarities[rarity] ? rarity : ''; } function applyRarityColors(root) { var scope = root && root.querySelectorAll ? root : document; var tables = []; if (scope.matches && scope.matches('table')) { tables.push(scope); } tables = tables.concat(Array.prototype.slice.call(scope.querySelectorAll('table'))); tables.forEach(function (table) { var headerCells = Array.prototype.slice.call(table.querySelectorAll('tr:first-child th, tr:first-child td')); var itemIndex = headerCells.findIndex(function (cell) { var label = cell.textContent.trim().toLowerCase(); return label === 'herb' || label === 'crystal' || cell.classList.contains('field_Herb') || cell.classList.contains('field_Crystal'); }); var rarityIndex = headerCells.findIndex(function (cell) { return cell.textContent.trim().toLowerCase() === 'rarity' || cell.classList.contains('field_Rarity'); }); Array.prototype.slice.call(table.rows).forEach(function (row) { var cells = Array.prototype.slice.call(row.cells); var itemCell = row.querySelector('td.field_Herb, td.field_Crystal'); var rarityCell = row.querySelector('td.field_Rarity'); if (!itemCell && itemIndex >= 0) { itemCell = cells[itemIndex]; } if (!rarityCell && rarityIndex >= 0) { rarityCell = cells[rarityIndex]; } if (!itemCell || !rarityCell || itemCell.tagName !== 'TD') { return; } var rarity = normalizeRarity(rarityCell.textContent); if (!rarity) { return; } row.classList.add('hw-rarity-row'); row.dataset.hwRarity = rarity; itemCell.classList.add('hw-rarity-item'); rarityCell.classList.add('hw-rarity-label'); }); table.dataset.hwRarityColorsReady = '1'; }); } mw.hook('wikipage.content').add(function ($content) { applyRarityColors($content && $content[0] ? $content[0] : document); }); }()); /* HarvestWiki default Herb rarity order */ (function () { 'use strict'; var rarityRank = { common: 0, uncommon: 1, rare: 2, legendary: 3, mythic: 4, secret: 5 }; function sortHerbTables(root) { var scope = root && root.querySelectorAll ? root : document; var tables = []; if (scope.matches && scope.matches('table')) { tables.push(scope); } tables = tables.concat(Array.prototype.slice.call(scope.querySelectorAll('table'))); tables.forEach(function (table) { if (table.dataset.hwHerbRaritySorted === '1') { return; } var headers = Array.prototype.slice.call(table.querySelectorAll('thead th')); if (!headers.length && table.rows.length) { headers = Array.prototype.slice.call(table.rows[0].cells); } var herbIndex = headers.findIndex(function (header) { return header.textContent.trim().toLowerCase() === 'herb' || header.classList.contains('field_Herb'); }); var rarityIndex = headers.findIndex(function (header) { return header.textContent.trim().toLowerCase() === 'rarity' || header.classList.contains('field_Rarity'); }); if (herbIndex < 0 || rarityIndex < 0) { return; } Array.prototype.slice.call(table.tBodies).forEach(function (body) { var rows = Array.prototype.slice.call(body.rows).map(function (row, index) { var rarityCell = row.cells[rarityIndex]; var rarity = rarityCell ? rarityCell.textContent.trim().toLowerCase() : ''; return { row: row, index: index, rank: Object.prototype.hasOwnProperty.call(rarityRank, rarity) ? rarityRank[rarity] : 999 }; }); rows.sort(function (a, b) { return a.rank - b.rank || a.index - b.index; }); var fragment = document.createDocumentFragment(); rows.forEach(function (entry) { fragment.appendChild(entry.row); }); body.appendChild(fragment); }); table.dataset.hwHerbRaritySorted = '1'; }); } mw.hook('wikipage.content').add(function ($content) { sortHerbTables($content && $content[0] ? $content[0] : document); }); }()); /* HarvestWiki default Crystal rarity order */ (function () { 'use strict'; var crystalRarityRank = { mortal: 0, refined: 1, spirit: 2 }; function sortCrystalTables(root) { var scope = root && root.querySelectorAll ? root : document; var tables = []; if (scope.matches && scope.matches('table')) { tables.push(scope); } tables = tables.concat(Array.prototype.slice.call(scope.querySelectorAll('table'))); tables.forEach(function (table) { if (table.dataset.hwCrystalRaritySorted === '1') { return; } var headers = Array.prototype.slice.call(table.querySelectorAll('thead th')); if (!headers.length && table.rows.length) { headers = Array.prototype.slice.call(table.rows[0].cells); } var crystalIndex = headers.findIndex(function (header) { return header.textContent.trim().toLowerCase() === 'crystal' || header.classList.contains('field_Crystal'); }); var rarityIndex = headers.findIndex(function (header) { return header.textContent.trim().toLowerCase() === 'rarity' || header.classList.contains('field_Rarity'); }); if (crystalIndex < 0 || rarityIndex < 0) { return; } Array.prototype.slice.call(table.tBodies).forEach(function (body) { var rows = Array.prototype.slice.call(body.rows).map(function (row, index) { var rarityCell = row.cells[rarityIndex]; var rarity = rarityCell ? rarityCell.textContent.trim().toLowerCase() : ''; return { row: row, index: index, rank: Object.prototype.hasOwnProperty.call(crystalRarityRank, rarity) ? crystalRarityRank[rarity] : 999 }; }); rows.sort(function (a, b) { return a.rank - b.rank || a.index - b.index; }); var fragment = document.createDocumentFragment(); rows.forEach(function (entry) { fragment.appendChild(entry.row); }); body.appendChild(fragment); }); table.dataset.hwCrystalRaritySorted = '1'; }); } mw.hook('wikipage.content').add(function ($content) { sortCrystalTables($content && $content[0] ? $content[0] : document); }); }()); /* HarvestWiki admin-only Crystal form UI */ (function () { 'use strict'; var groups = mw.config.get('wgUserGroups') || []; var categories = mw.config.get('wgCategories') || []; if (groups.indexOf('sysop') !== -1 || categories.indexOf('Crystals') === -1) { return; } function hideCrystalFormActions() { ['ca-formedit', 'ca-formcreate', 'ca-formedit-sticky-header', 'ca-formcreate-sticky-header'].forEach(function (id) { var element = document.getElementById(id); if (element) { element.hidden = true; } }); } if (document.readyState === 'loading') { document.addEventListener('DOMContentLoaded', hideCrystalFormActions); } else { hideCrystalFormActions(); } }()); /* HarvestWiki admin-only Weather form UI */ (function () { 'use strict'; var groups = mw.config.get('wgUserGroups') || []; var categories = mw.config.get('wgCategories') || []; if (groups.indexOf('sysop') !== -1 || categories.indexOf('Weather') === -1) { return; } function hideWeatherFormActions() { ['ca-formedit', 'ca-formcreate', 'ca-formedit-sticky-header', 'ca-formcreate-sticky-header'].forEach(function (id) { var element = document.getElementById(id); if (element) { element.hidden = true; } }); } if (document.readyState === 'loading') { document.addEventListener('DOMContentLoaded', hideWeatherFormActions); } else { hideWeatherFormActions(); } }()); /* HarvestWiki four-zodiac celestial calendar */ (function () { 'use strict'; var anchor = Date.UTC(2026, 0, 1, 0, 0, 0); var seasonLength = 8 * 60 * 60 * 1000; var seasonNames = [ 'Year of the Dragon', 'Year of the Tiger', 'Year of the Phoenix', 'Year of the Tortoise' ]; function formatDuration(milliseconds) { var seconds = Math.max(0, Math.ceil(milliseconds / 1000)); var hours = Math.floor(seconds / 3600); var minutes = Math.floor((seconds % 3600) / 60); var remainder = seconds % 60; return String(hours).padStart(2, '0') + ':' + String(minutes).padStart(2, '0') + ':' + String(remainder).padStart(2, '0'); } function initZodiacCycles(root) { var widgets = []; if (root && root.matches && root.matches('[data-hw-zodiac-cycle]')) { widgets.push(root); } if (root && root.querySelectorAll) { widgets = widgets.concat(Array.prototype.slice.call(root.querySelectorAll('[data-hw-zodiac-cycle]'))); } widgets.forEach(function (widget) { if (widget.dataset.hwZodiacReady === '1') { return; } widget.dataset.hwZodiacReady = '1'; var currentLabel = widget.querySelector('[data-hw-zodiac-current]'); var countdown = widget.querySelector('[data-hw-zodiac-countdown]'); var progress = widget.querySelector('[data-hw-zodiac-progress]'); var seasons = Array.prototype.slice.call(widget.querySelectorAll('[data-hw-zodiac-season]')); function updateZodiacCycle() { var now = Date.now(); var step = Math.floor((now - anchor) / seasonLength); var currentIndex = ((step % seasonNames.length) + seasonNames.length) % seasonNames.length; var nextBoundary = anchor + ((step + 1) * seasonLength); var remaining = Math.max(0, nextBoundary - now); var elapsedInSeason = ((now - anchor) % seasonLength + seasonLength) % seasonLength; if (currentLabel) { currentLabel.textContent = seasonNames[currentIndex]; } if (countdown) { countdown.textContent = formatDuration(remaining); countdown.title = 'Changes at ' + new Date(nextBoundary).toLocaleString(); } if (progress) { progress.style.width = ((elapsedInSeason / seasonLength) * 100).toFixed(3) + '%'; } seasons.forEach(function (season, index) { var offset = (index - currentIndex + seasonNames.length) % seasonNames.length; var state = season.querySelector('[data-hw-zodiac-state]'); var startsIn = remaining + (Math.max(0, offset - 1) * seasonLength); var changeAt = offset === 0 ? nextBoundary : now + startsIn; season.classList.toggle('is-current', offset === 0); season.setAttribute('aria-current', offset === 0 ? 'true' : 'false'); season.title = (offset === 0 ? 'Ends ' : 'Begins ') + new Date(changeAt).toLocaleString(); if (state) { if (offset === 0) { state.textContent = 'Current · ends in ' + formatDuration(remaining); } else if (offset === 1) { state.textContent = 'Next · starts in ' + formatDuration(startsIn); } else { state.textContent = 'Starts in ' + formatDuration(startsIn); } } }); } updateZodiacCycle(); window.setInterval(updateZodiacCycle, 1000); }); } mw.hook('wikipage.content').add(function ($content) { initZodiacCycles($content && $content[0] ? $content[0] : document); }); }()); /* HarvestWiki multi-page editor controls and archive refresh */ (function () { 'use strict'; function initBulkEditors(root) { var grids = []; if (root && root.matches && root.matches('.pfSpreadsheet')) { grids.push(root); } if (root && root.querySelectorAll) { grids = grids.concat(Array.prototype.slice.call(root.querySelectorAll('.pfSpreadsheet'))); } grids.forEach(function (grid) { if (grid.dataset.hwBulkEditorReady === '1') { return; } grid.dataset.hwBulkEditorReady = '1'; var parameters = new URLSearchParams(window.location.search); var templateName = parameters.get('template') || grid.id.replace(/Grid$/, ''); var archivePage = templateName === 'Herb' ? 'Herbs' : (templateName === 'Crystal' ? 'Crystals' : null); var userGroups = mw.config.get('wgUserGroups') || []; var canDelete = archivePage && userGroups.indexOf('sysop') !== -1; function purgeArchive() { if (!archivePage || typeof mw.Api !== 'function') { return Promise.resolve(); } return new mw.Api().post({ action: 'purge', titles: archivePage, formatversion: 2 }); } function wireRowActions() { Array.prototype.slice.call(grid.querySelectorAll('a.save-changes')).forEach(function (link) { if (link.dataset.hwBulkSaveReady === '1') { return; } link.dataset.hwBulkSaveReady = '1'; link.setAttribute('title', 'Save this row'); link.setAttribute('aria-label', 'Save this row'); link.addEventListener('click', function () { link.classList.remove('is-saved'); link.classList.add('is-saving'); function refreshArchive() { purgeArchive().then(function () { link.classList.remove('is-saving'); link.classList.add('is-saved'); window.setTimeout(function () { link.classList.remove('is-saved'); }, 2200); }); } window.setTimeout(refreshArchive, 1600); window.setTimeout(refreshArchive, 4200); }); }); if (!canDelete) { return; } Array.prototype.slice.call(grid.querySelectorAll('tbody tr')).forEach(function (row) { var pageCell = row.querySelector('td[data-x="0"]'); var actionBox = row.querySelector('.save-or-cancel'); if (!pageCell || !actionBox || actionBox.querySelector('.hw-delete-row')) { return; } var pageTitle = pageCell.textContent.trim(); if (!pageTitle) { return; } var deleteButton = document.createElement('button'); deleteButton.type = 'button'; deleteButton.className = 'hw-delete-row'; deleteButton.textContent = 'Delete'; deleteButton.title = 'Delete page ' + pageTitle; deleteButton.setAttribute('aria-label', 'Delete page ' + pageTitle); deleteButton.addEventListener('click', function () { var confirmed = window.confirm( 'Permanently delete "' + pageTitle + '"?\n\n' + 'This removes its wiki page and all table data. This cannot be undone.' ); if (!confirmed) { return; } deleteButton.disabled = true; deleteButton.classList.add('is-deleting'); deleteButton.textContent = 'Deleting…'; new mw.Api().postWithToken('csrf', { action: 'delete', title: pageTitle, reason: 'Deleted from the multi-page editor', formatversion: 2 }).then(function () { return purgeArchive(); }).then(function () { deleteButton.textContent = 'Deleted'; window.setTimeout(function () { window.location.reload(); }, 700); }, function () { deleteButton.disabled = false; deleteButton.classList.remove('is-deleting'); deleteButton.textContent = 'Delete'; mw.notify('Could not delete "' + pageTitle + '". The record was not removed.', { type: 'error' }); }); }); actionBox.appendChild(deleteButton); }); } wireRowActions(); if (window.MutationObserver) { new MutationObserver(wireRowActions).observe(grid, { childList: true, subtree: true }); } if (!grid.querySelector('.hw-bulk-editor-note')) { var note = document.createElement('div'); note.className = 'hw-bulk-editor-note'; note.textContent = 'Scroll sideways to reach every field. Use Save at the right of each row.' + (archivePage ? ' Saved rows automatically refresh the ' + archivePage + ' index.' : '') + (canDelete ? ' Administrators can permanently remove a record with Delete.' : ''); grid.appendChild(note); } }); } mw.hook('wikipage.content').add(function ($content) { initBulkEditors($content && $content[0] ? $content[0] : document); }); }());


/* HarvestWiki dynamic Cargo archive filters */
/* HarvestWiki automatic Herb, Crystal, Mutation, Weather, TOD, and Zodiac link colors */
(function () {
  'use strict';
 
  function initHerbLocationFilters(root) {
    var catalogs = [];
    if (root && root.matches && root.matches('.hw-herb-catalog')) {
      catalogs.push(root);
    }
    if (root && root.querySelectorAll) {
      catalogs = catalogs.concat(Array.prototype.slice.call(root.querySelectorAll('.hw-herb-catalog')));
    }
 
    catalogs.forEach(function (catalog) {
      if (catalog.dataset.hwLocationFiltersReady === '1') {
        return;
      }
 
      var table = catalog.querySelector('table');
      var filterBar = document.querySelector('.hw-herb-filter');
      if (!table || !filterBar) {
        return;
      }
 
      var headers = Array.prototype.slice.call(table.querySelectorAll('thead th'));
      var locationIndex = headers.findIndex(function (header) {
        return header.textContent.trim().toLowerCase() === 'location';
      });
      if (locationIndex < 0) {
        return;
      }
 
      var rows = Array.prototype.slice.call(table.querySelectorAll('tbody tr'));
      var counts = {};
      rows.forEach(function (row) {
        var cell = row.querySelectorAll('td')[locationIndex];
        var location = cell ? cell.textContent.trim() : '';
        row.dataset.hwLocation = location;
        if (location) {
          counts[location] = (counts[location] || 0) + 1;
        }
      });
 
      var locations = Object.keys(counts).sort();
      var enabled = new Set(locations);
      filterBar.innerHTML = '';
 
      var showAll = document.createElement('button');
      showAll.type = 'button';
      showAll.className = 'hw-filter-chip hw-filter-chip--all';
      showAll.textContent = 'Show all';
      filterBar.appendChild(showAll);
 
      var locationButtons = {};
      locations.forEach(function (location) {
        var button = document.createElement('button');
        button.type = 'button';
        button.className = 'hw-filter-chip is-active';
        button.setAttribute('aria-pressed', 'true');
        button.textContent = location + ' (' + counts[location] + ')';
        button.addEventListener('click', function () {
          if (enabled.has(location)) {
            enabled.delete(location);
          } else {
            enabled.add(location);
          }
          applyFilters();
        });
        locationButtons[location] = button;
        filterBar.appendChild(button);
      });
 
      function applyFilters() {
        rows.forEach(function (row) {
          row.hidden = !enabled.has(row.dataset.hwLocation);
        });
        locations.forEach(function (location) {
          var active = enabled.has(location);
          locationButtons[location].classList.toggle('is-active', active);
          locationButtons[location].setAttribute('aria-pressed', active ? 'true' : 'false');
        });
        showAll.disabled = enabled.size === locations.length;
        showAll.classList.toggle('is-active', enabled.size === locations.length);
      }
 
      showAll.addEventListener('click', function () {
        enabled = new Set(locations);
        applyFilters();
      });
 
      catalog.dataset.hwLocationFiltersReady = '1';
      applyFilters();
    });
  }
 
  mw.hook('wikipage.content').add(function ($content) {
    initHerbLocationFilters($content && $content[0] ? $content[0] : document);
  });
}());
 
/* HarvestWiki admin-only Herb form UI */
(function () {
  'use strict';
  var groups = mw.config.get('wgUserGroups') || [];
  var categories = mw.config.get('wgCategories') || [];
  if (groups.indexOf('sysop') !== -1 || categories.indexOf('Herbs') === -1) {
    return;
  }
  function hideHerbFormActions() {
    ['ca-formedit', 'ca-formcreate', 'ca-formedit-sticky-header', 'ca-formcreate-sticky-header'].forEach(function (id) {
      var element = document.getElementById(id);
      if (element) {
        element.hidden = true;
      }
    });
  }
  if (document.readyState === 'loading') {
    document.addEventListener('DOMContentLoaded', hideHerbFormActions);
  } else {
    hideHerbFormActions();
  }
}());
 
 
/* HarvestWiki rarity-colored Herb and Crystal table rows */
(function () {
  'use strict';
 
  var rarities = {
    common: true,
    uncommon: true,
    rare: true,
    legendary: true,
    mythic: true,
    secret: true,
    mortal: true,
    refined: true,
    spirit: true
  };
 
  function normalizeRarity(value) {
    var rarity = (value || '').trim().toLowerCase();
    return rarities[rarity] ? rarity : '';
  }
 
  function applyRarityColors(root) {
    var scope = root && root.querySelectorAll ? root : document;
    var tables = [];
 
    if (scope.matches && scope.matches('table')) {
      tables.push(scope);
    }
    tables = tables.concat(Array.prototype.slice.call(scope.querySelectorAll('table')));
 
    tables.forEach(function (table) {
      var headerCells = Array.prototype.slice.call(table.querySelectorAll('tr:first-child th, tr:first-child td'));
      var itemIndex = headerCells.findIndex(function (cell) {
        var label = cell.textContent.trim().toLowerCase();
        return label === 'herb' || label === 'crystal' ||
          cell.classList.contains('field_Herb') || cell.classList.contains('field_Crystal');
      });
      var rarityIndex = headerCells.findIndex(function (cell) {
        return cell.textContent.trim().toLowerCase() === 'rarity' ||
          cell.classList.contains('field_Rarity');
      });
 
      Array.prototype.slice.call(table.rows).forEach(function (row) {
        var cells = Array.prototype.slice.call(row.cells);
        var itemCell = row.querySelector('td.field_Herb, td.field_Crystal');
        var rarityCell = row.querySelector('td.field_Rarity');
 
        if (!itemCell && itemIndex >= 0) {
          itemCell = cells[itemIndex];
        }
        if (!rarityCell && rarityIndex >= 0) {
          rarityCell = cells[rarityIndex];
        }
        if (!itemCell || !rarityCell || itemCell.tagName !== 'TD') {
          return;
        }
 
        var rarity = normalizeRarity(rarityCell.textContent);
        if (!rarity) {
          return;
        }
 
        row.classList.add('hw-rarity-row');
        row.dataset.hwRarity = rarity;
        itemCell.classList.add('hw-rarity-item');
        rarityCell.classList.add('hw-rarity-label');
      });
 
      table.dataset.hwRarityColorsReady = '1';
    });
  }
 
  mw.hook('wikipage.content').add(function ($content) {
    applyRarityColors($content && $content[0] ? $content[0] : document);
  });
}());
 
 
/* HarvestWiki default Herb rarity order */
(function () {
  'use strict';
 
  var rarityRank = {
    common: 0,
    uncommon: 1,
    rare: 2,
    legendary: 3,
    mythic: 4,
    secret: 5
  };
 
  function sortHerbTables(root) {
    var scope = root && root.querySelectorAll ? root : document;
    var tables = [];
 
    if (scope.matches && scope.matches('table')) {
      tables.push(scope);
    }
    tables = tables.concat(Array.prototype.slice.call(scope.querySelectorAll('table')));
 
    tables.forEach(function (table) {
      if (table.dataset.hwHerbRaritySorted === '1') {
        return;
      }
 
      var headers = Array.prototype.slice.call(table.querySelectorAll('thead th'));
      if (!headers.length && table.rows.length) {
        headers = Array.prototype.slice.call(table.rows[0].cells);
      }
 
      var herbIndex = headers.findIndex(function (header) {
        return header.textContent.trim().toLowerCase() === 'herb' ||
          header.classList.contains('field_Herb');
      });
      var rarityIndex = headers.findIndex(function (header) {
        return header.textContent.trim().toLowerCase() === 'rarity' ||
          header.classList.contains('field_Rarity');
      });
 
      if (herbIndex < 0 || rarityIndex < 0) {
        return;
      }
 
      Array.prototype.slice.call(table.tBodies).forEach(function (body) {
        var rows = Array.prototype.slice.call(body.rows).map(function (row, index) {
          var rarityCell = row.cells[rarityIndex];
          var rarity = rarityCell ? rarityCell.textContent.trim().toLowerCase() : '';
          return {
            row: row,
            index: index,
            rank: Object.prototype.hasOwnProperty.call(rarityRank, rarity) ? rarityRank[rarity] : 999
          };
        });
 
        rows.sort(function (a, b) {
          return a.rank - b.rank || a.index - b.index;
        });
 
        var fragment = document.createDocumentFragment();
        rows.forEach(function (entry) {
          fragment.appendChild(entry.row);
        });
        body.appendChild(fragment);
      });
 
      table.dataset.hwHerbRaritySorted = '1';
    });
  }
 
  mw.hook('wikipage.content').add(function ($content) {
    sortHerbTables($content && $content[0] ? $content[0] : document);
  });
}());
 
 
/* HarvestWiki default Crystal rarity order */
(function () {
  'use strict';
 
  var crystalRarityRank = {
    mortal: 0,
    refined: 1,
    spirit: 2
  };
 
  function sortCrystalTables(root) {
    var scope = root && root.querySelectorAll ? root : document;
    var tables = [];
 
    if (scope.matches && scope.matches('table')) {
      tables.push(scope);
    }
    tables = tables.concat(Array.prototype.slice.call(scope.querySelectorAll('table')));
 
    tables.forEach(function (table) {
      if (table.dataset.hwCrystalRaritySorted === '1') {
        return;
      }
 
      var headers = Array.prototype.slice.call(table.querySelectorAll('thead th'));
      if (!headers.length && table.rows.length) {
        headers = Array.prototype.slice.call(table.rows[0].cells);
      }
 
      var crystalIndex = headers.findIndex(function (header) {
        return header.textContent.trim().toLowerCase() === 'crystal' ||
          header.classList.contains('field_Crystal');
      });
      var rarityIndex = headers.findIndex(function (header) {
        return header.textContent.trim().toLowerCase() === 'rarity' ||
          header.classList.contains('field_Rarity');
      });
 
      if (crystalIndex < 0 || rarityIndex < 0) {
        return;
      }
 
      Array.prototype.slice.call(table.tBodies).forEach(function (body) {
        var rows = Array.prototype.slice.call(body.rows).map(function (row, index) {
          var rarityCell = row.cells[rarityIndex];
          var rarity = rarityCell ? rarityCell.textContent.trim().toLowerCase() : '';
          return {
            row: row,
            index: index,
            rank: Object.prototype.hasOwnProperty.call(crystalRarityRank, rarity) ?
              crystalRarityRank[rarity] : 999
          };
        });
 
        rows.sort(function (a, b) {
          return a.rank - b.rank || a.index - b.index;
        });
 
        var fragment = document.createDocumentFragment();
        rows.forEach(function (entry) {
          fragment.appendChild(entry.row);
        });
        body.appendChild(fragment);
      });
 
      table.dataset.hwCrystalRaritySorted = '1';
    });
  }
 
  mw.hook('wikipage.content').add(function ($content) {
    sortCrystalTables($content && $content[0] ? $content[0] : document);
  });
}());
 
 
/* HarvestWiki admin-only Crystal form UI */
(function () {
  'use strict';
  var groups = mw.config.get('wgUserGroups') || [];
  var categories = mw.config.get('wgCategories') || [];
  if (groups.indexOf('sysop') !== -1 || categories.indexOf('Crystals') === -1) {
    return;
  }
  function hideCrystalFormActions() {
    ['ca-formedit', 'ca-formcreate', 'ca-formedit-sticky-header', 'ca-formcreate-sticky-header'].forEach(function (id) {
      var element = document.getElementById(id);
      if (element) {
        element.hidden = true;
      }
    });
  }
  if (document.readyState === 'loading') {
    document.addEventListener('DOMContentLoaded', hideCrystalFormActions);
  } else {
    hideCrystalFormActions();
  }
}());
 
 
/* HarvestWiki admin-only Weather form UI */
(function () {
  'use strict';
  var groups = mw.config.get('wgUserGroups') || [];
  var categories = mw.config.get('wgCategories') || [];
  if (groups.indexOf('sysop') !== -1 || categories.indexOf('Weather') === -1) {
    return;
  }
  function hideWeatherFormActions() {
    ['ca-formedit', 'ca-formcreate', 'ca-formedit-sticky-header', 'ca-formcreate-sticky-header'].forEach(function (id) {
      var element = document.getElementById(id);
      if (element) {
        element.hidden = true;
      }
    });
  }
  if (document.readyState === 'loading') {
    document.addEventListener('DOMContentLoaded', hideWeatherFormActions);
  } else {
    hideWeatherFormActions();
  }
}());
 
 
/* HarvestWiki four-zodiac celestial calendar */
(function () {
  'use strict';
 
  var anchor = Date.UTC(2026, 0, 1, 0, 0, 0);
  var seasonLength = 8 * 60 * 60 * 1000;
  var seasonNames = [
    'Year of the Dragon',
    'Year of the Tiger',
    'Year of the Phoenix',
    'Year of the Tortoise'
  ];
 
  function formatDuration(milliseconds) {
    var seconds = Math.max(0, Math.ceil(milliseconds / 1000));
    var hours = Math.floor(seconds / 3600);
    var minutes = Math.floor((seconds % 3600) / 60);
    var remainder = seconds % 60;
    return String(hours).padStart(2, '0') + ':' +
      String(minutes).padStart(2, '0') + ':' +
      String(remainder).padStart(2, '0');
  }
 
  function initZodiacCycles(root) {
    var widgets = [];
    if (root && root.matches && root.matches('[data-hw-zodiac-cycle]')) {
      widgets.push(root);
    }
    if (root && root.querySelectorAll) {
      widgets = widgets.concat(Array.prototype.slice.call(root.querySelectorAll('[data-hw-zodiac-cycle]')));
    }
 
    widgets.forEach(function (widget) {
      if (widget.dataset.hwZodiacReady === '1') {
        return;
      }
      widget.dataset.hwZodiacReady = '1';
 
      var currentLabel = widget.querySelector('[data-hw-zodiac-current]');
      var countdown = widget.querySelector('[data-hw-zodiac-countdown]');
      var progress = widget.querySelector('[data-hw-zodiac-progress]');
      var seasons = Array.prototype.slice.call(widget.querySelectorAll('[data-hw-zodiac-season]'));
 
      function updateZodiacCycle() {
        var now = Date.now();
        var step = Math.floor((now - anchor) / seasonLength);
        var currentIndex = ((step % seasonNames.length) + seasonNames.length) % seasonNames.length;
        var nextBoundary = anchor + ((step + 1) * seasonLength);
        var remaining = Math.max(0, nextBoundary - now);
        var elapsedInSeason = ((now - anchor) % seasonLength + seasonLength) % seasonLength;
 
        if (currentLabel) {
          currentLabel.textContent = seasonNames[currentIndex];
        }
        if (countdown) {
          countdown.textContent = formatDuration(remaining);
          countdown.title = 'Changes at ' + new Date(nextBoundary).toLocaleString();
        }
        if (progress) {
          progress.style.width = ((elapsedInSeason / seasonLength) * 100).toFixed(3) + '%';
        }
 
        seasons.forEach(function (season, index) {
          var offset = (index - currentIndex + seasonNames.length) % seasonNames.length;
          var state = season.querySelector('[data-hw-zodiac-state]');
          var startsIn = remaining + (Math.max(0, offset - 1) * seasonLength);
          var changeAt = offset === 0 ? nextBoundary : now + startsIn;
          season.classList.toggle('is-current', offset === 0);
          season.setAttribute('aria-current', offset === 0 ? 'true' : 'false');
          season.title = (offset === 0 ? 'Ends ' : 'Begins ') + new Date(changeAt).toLocaleString();
          if (state) {
            if (offset === 0) {
              state.textContent = 'Current · ends in ' + formatDuration(remaining);
            } else if (offset === 1) {
              state.textContent = 'Next · starts in ' + formatDuration(startsIn);
            } else {
              state.textContent = 'Starts in ' + formatDuration(startsIn);
            }
          }
        });
      }
 
      updateZodiacCycle();
      window.setInterval(updateZodiacCycle, 1000);
    });
  }
 
  mw.hook('wikipage.content').add(function ($content) {
    initZodiacCycles($content && $content[0] ? $content[0] : document);
  });
}());
 
 
/* HarvestWiki multi-page editor controls and archive refresh */
(function () {
  'use strict';
 
  function initBulkEditors(root) {
    var grids = [];
 
    if (root && root.matches && root.matches('.pfSpreadsheet')) {
      grids.push(root);
    }
    if (root && root.querySelectorAll) {
      grids = grids.concat(Array.prototype.slice.call(root.querySelectorAll('.pfSpreadsheet')));
    }
 
    grids.forEach(function (grid) {
      if (grid.dataset.hwBulkEditorReady === '1') {
        return;
      }
      grid.dataset.hwBulkEditorReady = '1';
 
      var parameters = new URLSearchParams(window.location.search);
      var templateName = parameters.get('template') || grid.id.replace(/Grid$/, '');
      var archivePage = templateName === 'Herb' ? 'Herbs' : (templateName === 'Crystal' ? 'Crystals' : null);
      var userGroups = mw.config.get('wgUserGroups') || [];
      var canDelete = archivePage && userGroups.indexOf('sysop') !== -1;
 
      function purgeArchive() {
        if (!archivePage || typeof mw.Api !== 'function') {
          return Promise.resolve();
        }
        return new mw.Api().post({
          action: 'purge',
          titles: archivePage,
          formatversion: 2
        });
      }
 
      function wireRowActions() {
        Array.prototype.slice.call(grid.querySelectorAll('a.save-changes')).forEach(function (link) {
          if (link.dataset.hwBulkSaveReady === '1') {
            return;
          }
 
          link.dataset.hwBulkSaveReady = '1';
          link.setAttribute('title', 'Save this row');
          link.setAttribute('aria-label', 'Save this row');
 
          link.addEventListener('click', function () {
            link.classList.remove('is-saved');
            link.classList.add('is-saving');
 
            function refreshArchive() {
              purgeArchive().then(function () {
                link.classList.remove('is-saving');
                link.classList.add('is-saved');
                window.setTimeout(function () {
                  link.classList.remove('is-saved');
                }, 2200);
              });
            }
 
            window.setTimeout(refreshArchive, 1600);
            window.setTimeout(refreshArchive, 4200);
          });
        });
 
        if (!canDelete) {
          return;
        }
 
        Array.prototype.slice.call(grid.querySelectorAll('tbody tr')).forEach(function (row) {
          var pageCell = row.querySelector('td[data-x="0"]');
          var actionBox = row.querySelector('.save-or-cancel');
 
          if (!pageCell || !actionBox || actionBox.querySelector('.hw-delete-row')) {
            return;
          }
 
          var pageTitle = pageCell.textContent.trim();
          if (!pageTitle) {
            return;
          }
 
          var deleteButton = document.createElement('button');
          deleteButton.type = 'button';
          deleteButton.className = 'hw-delete-row';
          deleteButton.textContent = 'Delete';
          deleteButton.title = 'Delete page ' + pageTitle;
          deleteButton.setAttribute('aria-label', 'Delete page ' + pageTitle);
 
          deleteButton.addEventListener('click', function () {
            var confirmed = window.confirm(
              'Permanently delete "' + pageTitle + '"?\n\n' +
              'This removes its wiki page and all table data. This cannot be undone.'
            );
 
            if (!confirmed) {
              return;
            }
 
            deleteButton.disabled = true;
            deleteButton.classList.add('is-deleting');
            deleteButton.textContent = 'Deleting…';
 
            new mw.Api().postWithToken('csrf', {
              action: 'delete',
              title: pageTitle,
              reason: 'Deleted from the multi-page editor',
              formatversion: 2
            }).then(function () {
              return purgeArchive();
            }).then(function () {
              deleteButton.textContent = 'Deleted';
              window.setTimeout(function () {
                window.location.reload();
              }, 700);
            }, function () {
              deleteButton.disabled = false;
              deleteButton.classList.remove('is-deleting');
              deleteButton.textContent = 'Delete';
              mw.notify('Could not delete "' + pageTitle + '". The record was not removed.', {
                type: 'error'
              });
            });
          });
 
          actionBox.appendChild(deleteButton);
        });
      }
 
      wireRowActions();
 
      if (window.MutationObserver) {
        new MutationObserver(wireRowActions).observe(grid, {
          childList: true,
          subtree: true
        });
      }
 
      if (!grid.querySelector('.hw-bulk-editor-note')) {
        var note = document.createElement('div');
        note.className = 'hw-bulk-editor-note';
        note.textContent =
          'Scroll sideways to reach every field. Use Save at the right of each row.' +
          (archivePage ? ' Saved rows automatically refresh the ' + archivePage + ' index.' : '') +
          (canDelete ? ' Administrators can permanently remove a record with Delete.' : '');
        grid.appendChild(note);
      }
    });
  }
 
  mw.hook('wikipage.content').add(function ($content) {
    initBulkEditors($content && $content[0] ? $content[0] : document);
  });
}());
 
/* HarvestWiki automatic Herb, Crystal, and Mutation link colors */
(function () {
(function () {
   'use strict';
   'use strict';
Line 683: Line 32:
     sovereign: '#ff26a5',
     sovereign: '#ff26a5',
     supreme: '#ed13ff'
     supreme: '#ed13ff'
  };
  var worldColors = {
    clear: '#f1cb4f',
    cloudy: '#949ba5',
    rain: '#60a5e2',
    storm: '#525964',
    thunderstorm: '#f8d652',
    'dao tempest': '#b9422f',
    sunrise: '#ef9d49',
    daytime: '#63ace0',
    sunset: '#df6447',
    night: '#abb6f9',
    'year of the dragon': '#ffb86f',
    'year of the tiger': '#f7f672',
    'year of the phoenix': '#f87b84',
    'year of the tortoise': '#4fc57d'
   };
   };


Line 702: Line 68:
     'magma crystal': rarityColors.refined,
     'magma crystal': rarityColors.refined,
     'mist crystal': rarityColors.refined,
     'mist crystal': rarityColors.refined,
     'longevity crystal': rarityColors.spirit
     'longevity crystal': rarityColors.spirit,
    clear: worldColors.clear,
    cloudy: worldColors.cloudy,
    rain: worldColors.rain,
    storm: worldColors.storm,
    thunderstorm: worldColors.thunderstorm,
    'dao tempest': worldColors['dao tempest'],
    sunrise: worldColors.sunrise,
    daytime: worldColors.daytime,
    sunset: worldColors.sunset,
    night: worldColors.night,
    'year of the dragon': worldColors['year of the dragon'],
    'year of the tiger': worldColors['year of the tiger'],
    'year of the phoenix': worldColors['year of the phoenix'],
    'year of the tortoise': worldColors['year of the tortoise']
   };
   };


Line 751: Line 131:
       cargoQuery('Herbs', '_pageName=Page,Rarity'),
       cargoQuery('Herbs', '_pageName=Page,Rarity'),
       cargoQuery('Crystals', '_pageName=Page,Rarity'),
       cargoQuery('Crystals', '_pageName=Page,Rarity'),
       cargoQuery('Mutations', '_pageName=Page,Color')
       cargoQuery('Mutations', '_pageName=Page,Color'),
      cargoQuery('Weathers', '_pageName=Page,Color')
     ]).then(function (groups) {
     ]).then(function (groups) {
       var colorMap = Object.assign({}, entityColors);
       var colorMap = Object.assign({}, entityColors);
Line 772: Line 153:


       groups[2].forEach(function (row) {
       groups[2].forEach(function (row) {
        var data = row.title || {};
        if (data.Page && data.Color) {
          colorMap[normalizeTitle(data.Page)] = data.Color;
        }
      });
      groups[3].forEach(function (row) {
         var data = row.title || {};
         var data = row.title || {};
         if (data.Page && data.Color) {
         if (data.Page && data.Color) {
Line 804: Line 192:
   }
   }


   function applyMutationRows(root) {
   function applyMutationRows(root, colorMap) {
     var scope = root && root.querySelectorAll ? root : document;
     var scope = root && root.querySelectorAll ? root : document;
     var tables = [];
     var tables = [];
Line 822: Line 210:
           header.classList.contains('field_Mutation');
           header.classList.contains('field_Mutation');
       });
       });
      var colorIndex = headers.findIndex(function (header) {
       if (mutationIndex < 0) {
        return header.textContent.trim().toLowerCase() === 'link color' ||
          header.classList.contains('field_Link_Color');
      });
       if (mutationIndex < 0 || colorIndex < 0) {
         return;
         return;
       }
       }
Line 833: Line 217:
         Array.prototype.slice.call(body.rows).forEach(function (row) {
         Array.prototype.slice.call(body.rows).forEach(function (row) {
           var mutationCell = row.cells[mutationIndex];
           var mutationCell = row.cells[mutationIndex];
           var colorCell = row.cells[colorIndex];
           var mutationLink = mutationCell ? mutationCell.querySelector('a[href]') : null;
           var color = colorCell ? colorCell.textContent.trim() : '';
           var color = mutationLink ? colorMap[titleFromLink(mutationLink)] : '';
           if (!mutationCell || !/^#[0-9a-f]{6}$/i.test(color)) {
           if (!mutationCell || !color) {
             return;
             return;
           }
           }
Line 849: Line 233:
   Object.keys(mutationColors).forEach(function (title) {
   Object.keys(mutationColors).forEach(function (title) {
     immediateColorMap[title] = mutationColors[title];
     immediateColorMap[title] = mutationColors[title];
  });
  Object.keys(worldColors).forEach(function (title) {
    immediateColorMap[title] = worldColors[title];
   });
   });
   var colorMapPromise = buildColorMap();
   var colorMapPromise = buildColorMap();
Line 854: Line 241:
   mw.hook('wikipage.content').add(function ($content) {
   mw.hook('wikipage.content').add(function ($content) {
     var root = $content && $content[0] ? $content[0] : document;
     var root = $content && $content[0] ? $content[0] : document;
     applyMutationRows(root);
     applyMutationRows(root, immediateColorMap);
     applyLinkColors(root, immediateColorMap);
     applyLinkColors(root, immediateColorMap);
     colorMapPromise.then(function (colorMap) {
     colorMapPromise.then(function (colorMap) {
      applyMutationRows(root, colorMap);
       applyLinkColors(root, colorMap);
       applyLinkColors(root, colorMap);
     });
     });
   });
   });
}());
}());

Revision as of 02:57, 28 July 2026

/* Any JavaScript here will be loaded for all users on every page load. */ /* HarvestWiki dynamic Cargo archive filters */ (function () { 'use strict'; function initHerbLocationFilters(root) { var catalogs = []; if (root && root.matches && root.matches('.hw-herb-catalog')) { catalogs.push(root); } if (root && root.querySelectorAll) { catalogs = catalogs.concat(Array.prototype.slice.call(root.querySelectorAll('.hw-herb-catalog'))); } catalogs.forEach(function (catalog) { if (catalog.dataset.hwLocationFiltersReady === '1') { return; } var table = catalog.querySelector('table'); var filterBar = document.querySelector('.hw-herb-filter'); if (!table || !filterBar) { return; } var headers = Array.prototype.slice.call(table.querySelectorAll('thead th')); var locationIndex = headers.findIndex(function (header) { return header.textContent.trim().toLowerCase() === 'location'; }); if (locationIndex < 0) { return; } var rows = Array.prototype.slice.call(table.querySelectorAll('tbody tr')); var counts = {}; rows.forEach(function (row) { var cell = row.querySelectorAll('td')[locationIndex]; var location = cell ? cell.textContent.trim() : ''; row.dataset.hwLocation = location; if (location) { counts[location] = (counts[location] || 0) + 1; } }); var locations = Object.keys(counts).sort(); var enabled = new Set(locations); filterBar.innerHTML = ''; var showAll = document.createElement('button'); showAll.type = 'button'; showAll.className = 'hw-filter-chip hw-filter-chip--all'; showAll.textContent = 'Show all'; filterBar.appendChild(showAll); var locationButtons = {}; locations.forEach(function (location) { var button = document.createElement('button'); button.type = 'button'; button.className = 'hw-filter-chip is-active'; button.setAttribute('aria-pressed', 'true'); button.textContent = location + ' (' + counts[location] + ')'; button.addEventListener('click', function () { if (enabled.has(location)) { enabled.delete(location); } else { enabled.add(location); } applyFilters(); }); locationButtons[location] = button; filterBar.appendChild(button); }); function applyFilters() { rows.forEach(function (row) { row.hidden = !enabled.has(row.dataset.hwLocation); }); locations.forEach(function (location) { var active = enabled.has(location); locationButtons[location].classList.toggle('is-active', active); locationButtons[location].setAttribute('aria-pressed', active ? 'true' : 'false'); }); showAll.disabled = enabled.size === locations.length; showAll.classList.toggle('is-active', enabled.size === locations.length); } showAll.addEventListener('click', function () { enabled = new Set(locations); applyFilters(); }); catalog.dataset.hwLocationFiltersReady = '1'; applyFilters(); }); } mw.hook('wikipage.content').add(function ($content) { initHerbLocationFilters($content && $content[0] ? $content[0] : document); }); }()); /* HarvestWiki admin-only Herb form UI */ (function () { 'use strict'; var groups = mw.config.get('wgUserGroups') || []; var categories = mw.config.get('wgCategories') || []; if (groups.indexOf('sysop') !== -1 || categories.indexOf('Herbs') === -1) { return; } function hideHerbFormActions() { ['ca-formedit', 'ca-formcreate', 'ca-formedit-sticky-header', 'ca-formcreate-sticky-header'].forEach(function (id) { var element = document.getElementById(id); if (element) { element.hidden = true; } }); } if (document.readyState === 'loading') { document.addEventListener('DOMContentLoaded', hideHerbFormActions); } else { hideHerbFormActions(); } }()); /* HarvestWiki rarity-colored Herb and Crystal table rows */ (function () { 'use strict'; var rarities = { common: true, uncommon: true, rare: true, legendary: true, mythic: true, secret: true, mortal: true, refined: true, spirit: true }; function normalizeRarity(value) { var rarity = (value || '').trim().toLowerCase(); return rarities[rarity] ? rarity : ''; } function applyRarityColors(root) { var scope = root && root.querySelectorAll ? root : document; var tables = []; if (scope.matches && scope.matches('table')) { tables.push(scope); } tables = tables.concat(Array.prototype.slice.call(scope.querySelectorAll('table'))); tables.forEach(function (table) { var headerCells = Array.prototype.slice.call(table.querySelectorAll('tr:first-child th, tr:first-child td')); var itemIndex = headerCells.findIndex(function (cell) { var label = cell.textContent.trim().toLowerCase(); return label === 'herb' || label === 'crystal' || cell.classList.contains('field_Herb') || cell.classList.contains('field_Crystal'); }); var rarityIndex = headerCells.findIndex(function (cell) { return cell.textContent.trim().toLowerCase() === 'rarity' || cell.classList.contains('field_Rarity'); }); Array.prototype.slice.call(table.rows).forEach(function (row) { var cells = Array.prototype.slice.call(row.cells); var itemCell = row.querySelector('td.field_Herb, td.field_Crystal'); var rarityCell = row.querySelector('td.field_Rarity'); if (!itemCell && itemIndex >= 0) { itemCell = cells[itemIndex]; } if (!rarityCell && rarityIndex >= 0) { rarityCell = cells[rarityIndex]; } if (!itemCell || !rarityCell || itemCell.tagName !== 'TD') { return; } var rarity = normalizeRarity(rarityCell.textContent); if (!rarity) { return; } row.classList.add('hw-rarity-row'); row.dataset.hwRarity = rarity; itemCell.classList.add('hw-rarity-item'); rarityCell.classList.add('hw-rarity-label'); }); table.dataset.hwRarityColorsReady = '1'; }); } mw.hook('wikipage.content').add(function ($content) { applyRarityColors($content && $content[0] ? $content[0] : document); }); }()); /* HarvestWiki default Herb rarity order */ (function () { 'use strict'; var rarityRank = { common: 0, uncommon: 1, rare: 2, legendary: 3, mythic: 4, secret: 5 }; function sortHerbTables(root) { var scope = root && root.querySelectorAll ? root : document; var tables = []; if (scope.matches && scope.matches('table')) { tables.push(scope); } tables = tables.concat(Array.prototype.slice.call(scope.querySelectorAll('table'))); tables.forEach(function (table) { if (table.dataset.hwHerbRaritySorted === '1') { return; } var headers = Array.prototype.slice.call(table.querySelectorAll('thead th')); if (!headers.length && table.rows.length) { headers = Array.prototype.slice.call(table.rows[0].cells); } var herbIndex = headers.findIndex(function (header) { return header.textContent.trim().toLowerCase() === 'herb' || header.classList.contains('field_Herb'); }); var rarityIndex = headers.findIndex(function (header) { return header.textContent.trim().toLowerCase() === 'rarity' || header.classList.contains('field_Rarity'); }); if (herbIndex < 0 || rarityIndex < 0) { return; } Array.prototype.slice.call(table.tBodies).forEach(function (body) { var rows = Array.prototype.slice.call(body.rows).map(function (row, index) { var rarityCell = row.cells[rarityIndex]; var rarity = rarityCell ? rarityCell.textContent.trim().toLowerCase() : ''; return { row: row, index: index, rank: Object.prototype.hasOwnProperty.call(rarityRank, rarity) ? rarityRank[rarity] : 999 }; }); rows.sort(function (a, b) { return a.rank - b.rank || a.index - b.index; }); var fragment = document.createDocumentFragment(); rows.forEach(function (entry) { fragment.appendChild(entry.row); }); body.appendChild(fragment); }); table.dataset.hwHerbRaritySorted = '1'; }); } mw.hook('wikipage.content').add(function ($content) { sortHerbTables($content && $content[0] ? $content[0] : document); }); }()); /* HarvestWiki default Crystal rarity order */ (function () { 'use strict'; var crystalRarityRank = { mortal: 0, refined: 1, spirit: 2 }; function sortCrystalTables(root) { var scope = root && root.querySelectorAll ? root : document; var tables = []; if (scope.matches && scope.matches('table')) { tables.push(scope); } tables = tables.concat(Array.prototype.slice.call(scope.querySelectorAll('table'))); tables.forEach(function (table) { if (table.dataset.hwCrystalRaritySorted === '1') { return; } var headers = Array.prototype.slice.call(table.querySelectorAll('thead th')); if (!headers.length && table.rows.length) { headers = Array.prototype.slice.call(table.rows[0].cells); } var crystalIndex = headers.findIndex(function (header) { return header.textContent.trim().toLowerCase() === 'crystal' || header.classList.contains('field_Crystal'); }); var rarityIndex = headers.findIndex(function (header) { return header.textContent.trim().toLowerCase() === 'rarity' || header.classList.contains('field_Rarity'); }); if (crystalIndex < 0 || rarityIndex < 0) { return; } Array.prototype.slice.call(table.tBodies).forEach(function (body) { var rows = Array.prototype.slice.call(body.rows).map(function (row, index) { var rarityCell = row.cells[rarityIndex]; var rarity = rarityCell ? rarityCell.textContent.trim().toLowerCase() : ''; return { row: row, index: index, rank: Object.prototype.hasOwnProperty.call(crystalRarityRank, rarity) ? crystalRarityRank[rarity] : 999 }; }); rows.sort(function (a, b) { return a.rank - b.rank || a.index - b.index; }); var fragment = document.createDocumentFragment(); rows.forEach(function (entry) { fragment.appendChild(entry.row); }); body.appendChild(fragment); }); table.dataset.hwCrystalRaritySorted = '1'; }); } mw.hook('wikipage.content').add(function ($content) { sortCrystalTables($content && $content[0] ? $content[0] : document); }); }()); /* HarvestWiki admin-only Crystal form UI */ (function () { 'use strict'; var groups = mw.config.get('wgUserGroups') || []; var categories = mw.config.get('wgCategories') || []; if (groups.indexOf('sysop') !== -1 || categories.indexOf('Crystals') === -1) { return; } function hideCrystalFormActions() { ['ca-formedit', 'ca-formcreate', 'ca-formedit-sticky-header', 'ca-formcreate-sticky-header'].forEach(function (id) { var element = document.getElementById(id); if (element) { element.hidden = true; } }); } if (document.readyState === 'loading') { document.addEventListener('DOMContentLoaded', hideCrystalFormActions); } else { hideCrystalFormActions(); } }()); /* HarvestWiki admin-only Weather form UI */ (function () { 'use strict'; var groups = mw.config.get('wgUserGroups') || []; var categories = mw.config.get('wgCategories') || []; if (groups.indexOf('sysop') !== -1 || categories.indexOf('Weather') === -1) { return; } function hideWeatherFormActions() { ['ca-formedit', 'ca-formcreate', 'ca-formedit-sticky-header', 'ca-formcreate-sticky-header'].forEach(function (id) { var element = document.getElementById(id); if (element) { element.hidden = true; } }); } if (document.readyState === 'loading') { document.addEventListener('DOMContentLoaded', hideWeatherFormActions); } else { hideWeatherFormActions(); } }()); /* HarvestWiki four-zodiac celestial calendar */ (function () { 'use strict'; var anchor = Date.UTC(2026, 0, 1, 0, 0, 0); var seasonLength = 8 * 60 * 60 * 1000; var seasonNames = [ 'Year of the Dragon', 'Year of the Tiger', 'Year of the Phoenix', 'Year of the Tortoise' ]; function formatDuration(milliseconds) { var seconds = Math.max(0, Math.ceil(milliseconds / 1000)); var hours = Math.floor(seconds / 3600); var minutes = Math.floor((seconds % 3600) / 60); var remainder = seconds % 60; return String(hours).padStart(2, '0') + ':' + String(minutes).padStart(2, '0') + ':' + String(remainder).padStart(2, '0'); } function initZodiacCycles(root) { var widgets = []; if (root && root.matches && root.matches('[data-hw-zodiac-cycle]')) { widgets.push(root); } if (root && root.querySelectorAll) { widgets = widgets.concat(Array.prototype.slice.call(root.querySelectorAll('[data-hw-zodiac-cycle]'))); } widgets.forEach(function (widget) { if (widget.dataset.hwZodiacReady === '1') { return; } widget.dataset.hwZodiacReady = '1'; var currentLabel = widget.querySelector('[data-hw-zodiac-current]'); var countdown = widget.querySelector('[data-hw-zodiac-countdown]'); var progress = widget.querySelector('[data-hw-zodiac-progress]'); var seasons = Array.prototype.slice.call(widget.querySelectorAll('[data-hw-zodiac-season]')); function updateZodiacCycle() { var now = Date.now(); var step = Math.floor((now - anchor) / seasonLength); var currentIndex = ((step % seasonNames.length) + seasonNames.length) % seasonNames.length; var nextBoundary = anchor + ((step + 1) * seasonLength); var remaining = Math.max(0, nextBoundary - now); var elapsedInSeason = ((now - anchor) % seasonLength + seasonLength) % seasonLength; if (currentLabel) { currentLabel.textContent = seasonNames[currentIndex]; } if (countdown) { countdown.textContent = formatDuration(remaining); countdown.title = 'Changes at ' + new Date(nextBoundary).toLocaleString(); } if (progress) { progress.style.width = ((elapsedInSeason / seasonLength) * 100).toFixed(3) + '%'; } seasons.forEach(function (season, index) { var offset = (index - currentIndex + seasonNames.length) % seasonNames.length; var state = season.querySelector('[data-hw-zodiac-state]'); var startsIn = remaining + (Math.max(0, offset - 1) * seasonLength); var changeAt = offset === 0 ? nextBoundary : now + startsIn; season.classList.toggle('is-current', offset === 0); season.setAttribute('aria-current', offset === 0 ? 'true' : 'false'); season.title = (offset === 0 ? 'Ends ' : 'Begins ') + new Date(changeAt).toLocaleString(); if (state) { if (offset === 0) { state.textContent = 'Current · ends in ' + formatDuration(remaining); } else if (offset === 1) { state.textContent = 'Next · starts in ' + formatDuration(startsIn); } else { state.textContent = 'Starts in ' + formatDuration(startsIn); } } }); } updateZodiacCycle(); window.setInterval(updateZodiacCycle, 1000); }); } mw.hook('wikipage.content').add(function ($content) { initZodiacCycles($content && $content[0] ? $content[0] : document); }); }()); /* HarvestWiki multi-page editor controls and archive refresh */ (function () { 'use strict'; function initBulkEditors(root) { var grids = []; if (root && root.matches && root.matches('.pfSpreadsheet')) { grids.push(root); } if (root && root.querySelectorAll) { grids = grids.concat(Array.prototype.slice.call(root.querySelectorAll('.pfSpreadsheet'))); } grids.forEach(function (grid) { if (grid.dataset.hwBulkEditorReady === '1') { return; } grid.dataset.hwBulkEditorReady = '1'; var parameters = new URLSearchParams(window.location.search); var templateName = parameters.get('template') || grid.id.replace(/Grid$/, ''); var archivePage = templateName === 'Herb' ? 'Herbs' : (templateName === 'Crystal' ? 'Crystals' : null); var userGroups = mw.config.get('wgUserGroups') || []; var canDelete = archivePage && userGroups.indexOf('sysop') !== -1; function purgeArchive() { if (!archivePage || typeof mw.Api !== 'function') { return Promise.resolve(); } return new mw.Api().post({ action: 'purge', titles: archivePage, formatversion: 2 }); } function wireRowActions() { Array.prototype.slice.call(grid.querySelectorAll('a.save-changes')).forEach(function (link) { if (link.dataset.hwBulkSaveReady === '1') { return; } link.dataset.hwBulkSaveReady = '1'; link.setAttribute('title', 'Save this row'); link.setAttribute('aria-label', 'Save this row'); link.addEventListener('click', function () { link.classList.remove('is-saved'); link.classList.add('is-saving'); function refreshArchive() { purgeArchive().then(function () { link.classList.remove('is-saving'); link.classList.add('is-saved'); window.setTimeout(function () { link.classList.remove('is-saved'); }, 2200); }); } window.setTimeout(refreshArchive, 1600); window.setTimeout(refreshArchive, 4200); }); }); if (!canDelete) { return; } Array.prototype.slice.call(grid.querySelectorAll('tbody tr')).forEach(function (row) { var pageCell = row.querySelector('td[data-x="0"]'); var actionBox = row.querySelector('.save-or-cancel'); if (!pageCell || !actionBox || actionBox.querySelector('.hw-delete-row')) { return; } var pageTitle = pageCell.textContent.trim(); if (!pageTitle) { return; } var deleteButton = document.createElement('button'); deleteButton.type = 'button'; deleteButton.className = 'hw-delete-row'; deleteButton.textContent = 'Delete'; deleteButton.title = 'Delete page ' + pageTitle; deleteButton.setAttribute('aria-label', 'Delete page ' + pageTitle); deleteButton.addEventListener('click', function () { var confirmed = window.confirm( 'Permanently delete "' + pageTitle + '"?\n\n' + 'This removes its wiki page and all table data. This cannot be undone.' ); if (!confirmed) { return; } deleteButton.disabled = true; deleteButton.classList.add('is-deleting'); deleteButton.textContent = 'Deleting…'; new mw.Api().postWithToken('csrf', { action: 'delete', title: pageTitle, reason: 'Deleted from the multi-page editor', formatversion: 2 }).then(function () { return purgeArchive(); }).then(function () { deleteButton.textContent = 'Deleted'; window.setTimeout(function () { window.location.reload(); }, 700); }, function () { deleteButton.disabled = false; deleteButton.classList.remove('is-deleting'); deleteButton.textContent = 'Delete'; mw.notify('Could not delete "' + pageTitle + '". The record was not removed.', { type: 'error' }); }); }); actionBox.appendChild(deleteButton); }); } wireRowActions(); if (window.MutationObserver) { new MutationObserver(wireRowActions).observe(grid, { childList: true, subtree: true }); } if (!grid.querySelector('.hw-bulk-editor-note')) { var note = document.createElement('div'); note.className = 'hw-bulk-editor-note'; note.textContent = 'Scroll sideways to reach every field. Use Save at the right of each row.' + (archivePage ? ' Saved rows automatically refresh the ' + archivePage + ' index.' : '') + (canDelete ? ' Administrators can permanently remove a record with Delete.' : ''); grid.appendChild(note); } }); } mw.hook('wikipage.content').add(function ($content) { initBulkEditors($content && $content[0] ? $content[0] : document); }); }());

/* HarvestWiki automatic Herb, Crystal, Mutation, Weather, TOD, and Zodiac link colors */
(function () {
  'use strict';

  var rarityColors = {
    common: '#e5e7e7',
    uncommon: '#87ff97',
    rare: '#6d90ff',
    legendary: '#fff687',
    mythic: '#ff79cb',
    secret: '#12005c',
    mortal: '#cef1e8',
    refined: '#464747',
    spirit: '#00c3f2'
  };

  var mutationColors = {
    silver: '#eef1f1',
    golden: '#f4d05b',
    vitality: '#e96869',
    vampiric: '#de002b',
    icy: '#9ee7d8',
    frozen: '#6ee8ff',
    spirit: '#ffabf6',
    royal: '#ffec31',
    imperial: '#b600ff',
    century: '#5369ff',
    millenium: '#6c47ff',
    eon: '#7f15ff',
    sovereign: '#ff26a5',
    supreme: '#ed13ff'
  };

  var worldColors = {
    clear: '#f1cb4f',
    cloudy: '#949ba5',
    rain: '#60a5e2',
    storm: '#525964',
    thunderstorm: '#f8d652',
    'dao tempest': '#b9422f',
    sunrise: '#ef9d49',
    daytime: '#63ace0',
    sunset: '#df6447',
    night: '#abb6f9',
    'year of the dragon': '#ffb86f',
    'year of the tiger': '#f7f672',
    'year of the phoenix': '#f87b84',
    'year of the tortoise': '#4fc57d'
  };

  var entityColors = {
    'ancient woe herb': rarityColors.rare,
    'breathless dawn herb': rarityColors.uncommon,
    'crimson sun grass': rarityColors.common,
    'fleeting dusk herb': rarityColors.uncommon,
    'frigid moon grass': rarityColors.common,
    'pursuing longevity herb': rarityColors.mythic,
    'sparkling joy herb': rarityColors.rare,
    'yang lotus': rarityColors.legendary,
    'yin lotus': rarityColors.legendary,
    'air crystal': rarityColors.mortal,
    'earth crystal': rarityColors.mortal,
    'flame crystal': rarityColors.mortal,
    'water crystal': rarityColors.mortal,
    'wood crystal': rarityColors.mortal,
    'magma crystal': rarityColors.refined,
    'mist crystal': rarityColors.refined,
    'longevity crystal': rarityColors.spirit,
    clear: worldColors.clear,
    cloudy: worldColors.cloudy,
    rain: worldColors.rain,
    storm: worldColors.storm,
    thunderstorm: worldColors.thunderstorm,
    'dao tempest': worldColors['dao tempest'],
    sunrise: worldColors.sunrise,
    daytime: worldColors.daytime,
    sunset: worldColors.sunset,
    night: worldColors.night,
    'year of the dragon': worldColors['year of the dragon'],
    'year of the tiger': worldColors['year of the tiger'],
    'year of the phoenix': worldColors['year of the phoenix'],
    'year of the tortoise': worldColors['year of the tortoise']
  };

  function normalizeTitle(value) {
    return (value || '').replace(/_/g, ' ').replace(/\s+/g, ' ').trim().toLowerCase();
  }

  function titleFromLink(link) {
    var href = link.getAttribute('href') || '';
    if (!href || href.charAt(0) === '#') {
      return '';
    }

    try {
      var url = new URL(href, window.location.origin);
      if (url.origin !== window.location.origin) {
        return '';
      }

      var title = url.searchParams.get('title');
      if (!title && url.pathname.indexOf('/index.php/') === 0) {
        title = decodeURIComponent(url.pathname.slice('/index.php/'.length));
      }
      return normalizeTitle((title || '').split('#')[0]);
    } catch (error) {
      return '';
    }
  }

  function cargoQuery(table, fields) {
    return mw.loader.using('mediawiki.api').then(function () {
      return new mw.Api().get({
        action: 'cargoquery',
        tables: table,
        fields: fields,
        limit: 500,
        formatversion: 2
      });
    }).then(function (data) {
      return data && data.cargoquery ? data.cargoquery : [];
    }, function () {
      return [];
    });
  }

  function buildColorMap() {
    return Promise.all([
      cargoQuery('Herbs', '_pageName=Page,Rarity'),
      cargoQuery('Crystals', '_pageName=Page,Rarity'),
      cargoQuery('Mutations', '_pageName=Page,Color'),
      cargoQuery('Weathers', '_pageName=Page,Color')
    ]).then(function (groups) {
      var colorMap = Object.assign({}, entityColors);

      groups[0].forEach(function (row) {
        var data = row.title || {};
        var color = rarityColors[normalizeTitle(data.Rarity)];
        if (data.Page && color) {
          colorMap[normalizeTitle(data.Page)] = color;
        }
      });

      groups[1].forEach(function (row) {
        var data = row.title || {};
        var color = rarityColors[normalizeTitle(data.Rarity)];
        if (data.Page && color) {
          colorMap[normalizeTitle(data.Page)] = color;
        }
      });

      groups[2].forEach(function (row) {
        var data = row.title || {};
        if (data.Page && data.Color) {
          colorMap[normalizeTitle(data.Page)] = data.Color;
        }
      });

      groups[3].forEach(function (row) {
        var data = row.title || {};
        if (data.Page && data.Color) {
          colorMap[normalizeTitle(data.Page)] = data.Color;
        }
      });

      Object.keys(mutationColors).forEach(function (title) {
        colorMap[title] = mutationColors[title];
      });

      return colorMap;
    });
  }

  function applyLinkColors(root, colorMap) {
    var scope = root && root.querySelectorAll ? root : document;
    var links = [];
    if (scope.matches && scope.matches('a[href]')) {
      links.push(scope);
    }
    links = links.concat(Array.prototype.slice.call(scope.querySelectorAll('a[href]')));

    links.forEach(function (link) {
      var color = colorMap[titleFromLink(link)];
      if (!color || link.classList.contains('new')) {
        return;
      }
      link.classList.add('hw-colored-link');
      link.style.setProperty('--hw-link-color', color);
    });
  }

  function applyMutationRows(root, colorMap) {
    var scope = root && root.querySelectorAll ? root : document;
    var tables = [];
    if (scope.matches && scope.matches('table')) {
      tables.push(scope);
    }
    tables = tables.concat(Array.prototype.slice.call(scope.querySelectorAll('table')));

    tables.forEach(function (table) {
      var headers = Array.prototype.slice.call(table.querySelectorAll('thead th'));
      if (!headers.length && table.rows.length) {
        headers = Array.prototype.slice.call(table.rows[0].cells);
      }

      var mutationIndex = headers.findIndex(function (header) {
        return header.textContent.trim().toLowerCase() === 'mutation' ||
          header.classList.contains('field_Mutation');
      });
      if (mutationIndex < 0) {
        return;
      }

      Array.prototype.slice.call(table.tBodies).forEach(function (body) {
        Array.prototype.slice.call(body.rows).forEach(function (row) {
          var mutationCell = row.cells[mutationIndex];
          var mutationLink = mutationCell ? mutationCell.querySelector('a[href]') : null;
          var color = mutationLink ? colorMap[titleFromLink(mutationLink)] : '';
          if (!mutationCell || !color) {
            return;
          }
          row.classList.add('hw-mutation-row');
          row.style.setProperty('--hw-mutation-row-color', color);
          mutationCell.classList.add('hw-mutation-item');
        });
      });
    });
  }

  var immediateColorMap = Object.assign({}, entityColors);
  Object.keys(mutationColors).forEach(function (title) {
    immediateColorMap[title] = mutationColors[title];
  });
  Object.keys(worldColors).forEach(function (title) {
    immediateColorMap[title] = worldColors[title];
  });
  var colorMapPromise = buildColorMap();

  mw.hook('wikipage.content').add(function ($content) {
    var root = $content && $content[0] ? $content[0] : document;
    applyMutationRows(root, immediateColorMap);
    applyLinkColors(root, immediateColorMap);
    colorMapPromise.then(function (colorMap) {
      applyMutationRows(root, colorMap);
      applyLinkColors(root, colorMap);
    });
  });
}());