MediaWiki:Common.js
MediaWiki interface page
More actions
Note: After publishing, you may have to bypass your browser's cache to see the changes.
- Firefox / Safari: Hold Shift while clicking Reload, or press either Ctrl-F5 or Ctrl-R (⌘-R on a Mac)
- Google Chrome: Press Ctrl-Shift-R (⌘-Shift-R on a Mac)
- Edge: Hold Ctrl while clicking Refresh, or press Ctrl-F5.
/* 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, and Mutation 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'
};
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) {
if (typeof mw.Api !== 'function') {
return Promise.resolve([]);
}
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')
]).then(function (groups) {
var colorMap = {};
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;
}
});
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) {
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');
});
var colorIndex = headers.findIndex(function (header) {
return header.textContent.trim().toLowerCase() === 'link color' ||
header.classList.contains('field_Link_Color');
});
if (mutationIndex < 0 || colorIndex < 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 colorCell = row.cells[colorIndex];
var color = colorCell ? colorCell.textContent.trim() : '';
if (!mutationCell || !/^#[0-9a-f]{6}$/i.test(color)) {
return;
}
row.classList.add('hw-mutation-row');
row.style.setProperty('--hw-mutation-row-color', color);
mutationCell.classList.add('hw-mutation-item');
});
});
});
}
var colorMapPromise = buildColorMap();
mw.hook('wikipage.content').add(function ($content) {
var root = $content && $content[0] ? $content[0] : document;
applyMutationRows(root);
colorMapPromise.then(function (colorMap) {
applyLinkColors(root, colorMap);
});
});
}());