MediaWiki:Common.js
Appearance
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.
/* BEGIN HOME COUNTY HOMEPAGE V1 */
/* ResourceLoader-compatible homepage search backed by the live wiki index. */
mw.hook('wikipage.content').add(function () {
var home = document.getElementById('hc-home');
if (!home || home.getAttribute('data-enhanced')) { return; }
var input = home.querySelector('input[name="search"]');
var form = input && input.closest('form');
if (!form) { return; }
home.setAttribute('data-enhanced', 'true');
input.setAttribute('aria-label', 'Search the wiki');
input.setAttribute('autocomplete', 'off');
input.setAttribute('spellcheck', 'false');
input.setAttribute('role', 'combobox');
input.setAttribute('aria-autocomplete', 'list');
input.setAttribute('aria-controls', 'hc-search-results');
input.setAttribute('aria-expanded', 'false');
var results = document.createElement('div');
results.id = 'hc-search-results';
results.className = 'search-results';
results.setAttribute('role', 'listbox');
results.setAttribute('aria-label', 'Wiki search suggestions');
results.hidden = true;
form.appendChild(results);
var topics = [], labels = {}, generation = 0, timer, pending;
var apiReady = mw.loader.using('mediawiki.api').then(function () { return new mw.Api(); });
Array.prototype.forEach.call(home.querySelectorAll('.topic-entry > span:last-child a'), function (link) {
var label = link.textContent.trim();
if (!labels[label]) { topics.push({label: label, href: link.href}); labels[label] = true; }
});
function hideResults() {
generation++;
window.clearTimeout(timer);
var request = pending;
pending = null;
if (request && request.abort) { request.abort(); }
results.hidden = true;
input.setAttribute('aria-expanded', 'false');
input.setAttribute('aria-busy', 'false');
}
function render(term, articles) {
var focused = results.contains(document.activeElement) ? document.activeElement.href : null;
results.textContent = '';
var seen = {}, entries = articles.concat(topics.filter(function (topic) {
return topic.label.toLowerCase().indexOf(term.toLowerCase()) !== -1;
}));
var count = 0;
function add(label, href) {
var link = document.createElement('a');
link.textContent = label;
link.href = href;
link.setAttribute('role', 'option');
results.appendChild(link);
if (focused && link.href === focused) { link.focus(); }
}
entries.forEach(function (entry) {
var key = entry.label.toLowerCase();
if (seen[key] || count >= 6) { return; }
seen[key] = true;
count++;
add(entry.label, entry.href);
});
add('Search all pages for “' + term + '”', '/Special:Search?search=' + encodeURIComponent(term));
results.hidden = false;
input.setAttribute('aria-expanded', 'true');
}
input.addEventListener('input', function () {
hideResults();
var term = input.value.trim();
if (!term) { return; }
render(term, []);
var version = generation;
timer = window.setTimeout(function () {
apiReady.then(function (api) {
if (version !== generation) { return; }
input.setAttribute('aria-busy', 'true');
pending = api.get({action: 'opensearch', search: term, namespace: 0, limit: 6, redirects: 'resolve'});
pending.then(function (data) {
if (version !== generation || input.value.trim() !== term) { return; }
pending = null;
input.setAttribute('aria-busy', 'false');
var articles = (data[1] || []).map(function (title) {
return {label: title, href: mw.util.getUrl(title)};
});
render(term, articles);
}, function () {
if (version === generation) { pending = null; input.setAttribute('aria-busy', 'false'); }
// Keep local topic suggestions and native full-wiki search usable on an API failure.
});
}, function () {});
}, 180);
});
input.addEventListener('keydown', function (event) {
if (event.key === 'Escape') { hideResults(); }
if (event.key === 'ArrowDown' && !results.hidden) {
var first = results.querySelector('a');
if (first) { event.preventDefault(); first.focus(); }
}
});
results.addEventListener('keydown', function (event) {
if (event.key === 'Escape') { hideResults(); input.focus(); return; }
if (event.key !== 'ArrowUp' && event.key !== 'ArrowDown') { return; }
var links = Array.prototype.slice.call(results.querySelectorAll('a'));
var current = links.indexOf(document.activeElement);
if (current < 0) { return; }
event.preventDefault();
var next = current + (event.key === 'ArrowDown' ? 1 : -1);
if (next < 0) { input.focus(); }
else { links[Math.min(next, links.length - 1)].focus(); }
});
document.addEventListener('click', function (event) {
if (!form.contains(event.target)) { hideResults(); }
});
form.addEventListener('focusout', function (event) {
if (!form.contains(event.relatedTarget)) { hideResults(); }
});
form.addEventListener('submit', function (event) {
if (!input.value.trim()) { event.preventDefault(); input.focus(); }
else { hideResults(); }
});
var login = home.querySelector('.page-tools a[href*="UserLogin"]');
if (login && mw.config.get('wgUserName')) {
login.textContent = 'Account';
login.href = '/Special:Preferences';
}
});
/* END HOME COUNTY HOMEPAGE V1 */
/* BEGIN HOME COUNTY ARTICLE THEME */
/* Keep site-script syntax compatible with MediaWiki ResourceLoader. */
mw.hook('wikipage.content').add(function () {
// Sidebar pinning no longer applies; preserve access to native menu/tools.
function keepMenuInDropdown(name) {
var menu = document.getElementById(name);
var target = document.getElementById(name + '-unpinned-container');
if (!menu || !target || target.contains(menu)) { return; }
target.appendChild(menu);
var header = menu.querySelector('.vector-pinnable-header');
if (header) {
header.classList.remove('vector-pinnable-header-pinned');
header.classList.add('vector-pinnable-header-unpinned');
}
}
keepMenuInDropdown('vector-main-menu');
keepMenuInDropdown('vector-page-tools');
if (document.getElementById('hc-home') || mw.config.get('wgAction') !== 'view') { return; }
var content = document.querySelector('#mw-content-text .mw-parser-output');
if (!content) { return; }
Array.prototype.forEach.call(content.querySelectorAll('table.articleList, table.wikitable, table.fandom-table'), function (table) {
if (table.closest('.portable-infobox') || table.parentNode.classList.contains('hc-table-scroll')) { return; }
var panel = document.createElement('div');
panel.className = 'hc-table-scroll';
panel.tabIndex = 0;
panel.setAttribute('role', 'region');
panel.setAttribute('aria-label', 'Scrollable data table');
table.parentNode.insertBefore(panel, table);
panel.appendChild(table);
});
if (mw.config.get('wgNamespaceNumber') < 0 || content.querySelector('#hc-article-contents, #toc')) { return; }
var source = document.getElementById('mw-panel-toc-list');
var sections = Array.prototype.filter.call(content.querySelectorAll('h2, h3, h4, h5, h6'), function (heading) {
return !heading.closest('.portable-infobox') && (heading.id || heading.querySelector('.mw-headline[id]'));
});
if (!sections.length || !source) { return; }
function convertList(original) {
var list = document.createElement('ul');
Array.prototype.forEach.call(original.children, function (originalItem) {
var sourceLink = originalItem.querySelector('a.vector-toc-link');
var number = sourceLink && sourceLink.querySelector('.vector-toc-numb');
if (!sourceLink || !number) { return; }
var item = document.createElement('li');
var link = document.createElement('a');
link.setAttribute('href', sourceLink.getAttribute('href'));
var index = document.createElement('span');
index.className = 'hc-contents-number';
index.textContent = number.textContent.trim();
link.appendChild(index);
var text = sourceLink.querySelector('.vector-toc-text');
var label = text.textContent.trim().replace(/^\s*[\d.]+\s*/, '');
link.appendChild(document.createTextNode(label));
item.appendChild(link);
Array.prototype.forEach.call(originalItem.children, function (child) {
if (child.tagName === 'UL' && child.children.length) { item.appendChild(convertList(child)); }
});
list.appendChild(item);
});
return list;
}
var list = convertList(source);
if (!list.children.length) { return; }
var box = document.createElement('nav');
box.id = 'hc-article-contents';
box.className = 'hc-contents';
box.setAttribute('aria-label', 'Contents');
var title = document.createElement('div');
title.className = 'hc-contents-title';
var heading = document.createElement('strong');
heading.textContent = 'Contents';
title.appendChild(heading);
var toggle = document.createElement('button');
toggle.className = 'hc-contents-toggle';
toggle.type = 'button';
toggle.textContent = '[hide]';
toggle.setAttribute('aria-expanded', 'true');
toggle.setAttribute('aria-controls', 'hc-contents-list');
list.id = 'hc-contents-list';
toggle.addEventListener('click', function () {
list.hidden = !list.hidden;
toggle.textContent = list.hidden ? '[show]' : '[hide]';
toggle.setAttribute('aria-expanded', list.hidden ? 'false' : 'true');
});
title.appendChild(toggle);
box.appendChild(title);
box.appendChild(list);
var first = sections[0];
var insertion = first.parentNode.classList.contains('mw-heading') ? first.parentNode : first;
var narrow = window.matchMedia('(max-width: 720px)');
function positionContents() {
var infobox = content.querySelector('.portable-infobox');
if (narrow.matches && infobox && infobox.parentNode === content) {
content.insertBefore(box, infobox);
} else {
insertion.parentNode.insertBefore(box, insertion);
}
}
positionContents();
if (narrow.addEventListener) { narrow.addEventListener('change', positionContents); }
else { narrow.addListener(positionContents); }
});
/* END HOME COUNTY ARTICLE THEME */
/* BEGIN HOME COUNTY CURRENCY */
/* Decorate floren and token values; preserve prices, article markup and sorting. */
mw.hook('wikipage.content').add(function ($content) {
var content = $content && $content[0];
if (!content || mw.config.get('wgAction') !== 'view') { return; }
var numericPrice = /^\s*(\d[\d,]*(?:\.\d+)?)\s*$/;
var currencyWords = /(?:flor(?:en|in)[sx]?|tokens?)/i;
var labelledPrice = /\b\d[\d,]*(?:\.\d+)?\s*(?:[x\u00d7]\s*)?(?:flor(?:en|in)[sx]?|tokens?)\b/gi;
var priceField = /^(?:buy|sell|prize_sale|base_price|prize_base_price|base price|prixe price|price|sell_price|buy_price|seed_cost)$/i;
var priceHeading = /(?:\bpric(?:e|es|ing)\b|\bcost\b|\bsales?\b|\bsells?\s+for\b|\b(?:base|prize)\s+(?:prize\s+)?value\b)/i;
function makePrice(text, kind) {
kind = kind || (/tokens?/i.test(text) ? 'token' : 'floren');
var label = kind === 'token' ? 'Tokens' : 'Florens';
var price = document.createElement('span');
price.className = 'hc-price hc-currency-' + kind;
var coin = document.createElement('span');
coin.className = kind === 'token' ? 'hc-token-icon' : 'hc-floren-icon';
coin.title = label;
if (currencyWords.test(text)) {
coin.setAttribute('aria-hidden', 'true');
} else {
coin.setAttribute('role', 'img');
coin.setAttribute('aria-label', label);
}
price.appendChild(coin);
var amount = document.createElement('span');
amount.className = 'hc-price-amount';
amount.textContent = text;
price.appendChild(amount);
return price;
}
function decorateNumber(cell, kind) {
if (cell.querySelector('.hc-price')) { return; }
var match = cell.textContent.match(numericPrice);
if (!match) { return; } // Empty/unknown prices and non-money prose stay untouched.
var walker = document.createTreeWalker(cell, NodeFilter.SHOW_TEXT, null, false);
var nodes = [];
while (walker.nextNode()) { nodes.push(walker.currentNode); }
nodes.forEach(function (node) {
if (node.nodeValue.match(numericPrice)) {
node.parentNode.replaceChild(makePrice(node.nodeValue.trim(), kind), node);
}
});
if (cell.tagName === 'TD' && !cell.hasAttribute('data-sort-value')) {
cell.setAttribute('data-sort-value', match[1].replace(/,/g, ''));
}
}
// PortableInfobox exposes source names on both horizontal and stacked fields.
Array.prototype.forEach.call(content.querySelectorAll('.portable-infobox [data-source]'), function (field) {
var source = field.getAttribute('data-source');
var tokenField = /^(?:tokens?|token_cost|token_reward|token_amount)$/i.test(source);
if (!tokenField && !priceField.test(source)) { return; }
var value = field.classList.contains('pi-data-value') ? field : field.querySelector('.pi-data-value');
if (value) { decorateNumber(value, tokenField ? 'token' : 'floren'); }
});
// Map real table columns, including spanning headers; levels/counts are not prices.
Array.prototype.forEach.call(content.querySelectorAll('table.articleList, table.wikitable, table.fandom-table'), function (table) {
var priceColumns = {};
var occupied = [];
Array.prototype.forEach.call(table.rows, function (row, rowIndex) {
occupied[rowIndex] = occupied[rowIndex] || [];
var col = 0;
Array.prototype.forEach.call(row.cells, function (cell) {
while (occupied[rowIndex][col]) { col++; }
var width = cell.colSpan || 1;
var height = cell.rowSpan || 1;
for (var y = rowIndex; y < rowIndex + height; y++) {
occupied[y] = occupied[y] || [];
for (var x = col; x < col + width; x++) { occupied[y][x] = true; }
}
if (cell.tagName === 'TH' && (/\btokens?\b/i.test(cell.textContent) || priceHeading.test(cell.textContent))) {
var kind = /\btokens?\b/i.test(cell.textContent) ? 'token' : 'floren';
for (var i = col; i < col + width; i++) { priceColumns[i] = kind; }
} else if (cell.tagName === 'TD' && width === 1 && priceColumns[col]) {
decorateNumber(cell, priceColumns[col]);
}
col += width;
});
});
});
// Token rewards, monetary rewards and prices written in prose retain their original wording.
var walker = document.createTreeWalker(content, NodeFilter.SHOW_TEXT, null, false);
var nodes = [];
while (walker.nextNode()) {
var node = walker.currentNode;
if (!node.parentElement.closest('.hc-price, script, style, textarea, pre, code')) { nodes.push(node); }
}
nodes.forEach(function (node) {
labelledPrice.lastIndex = 0;
var match;
var offset = 0;
var fragment = document.createDocumentFragment();
while ((match = labelledPrice.exec(node.nodeValue))) {
fragment.appendChild(document.createTextNode(node.nodeValue.slice(offset, match.index)));
fragment.appendChild(makePrice(match[0]));
offset = match.index + match[0].length;
}
if (!offset) { return; }
fragment.appendChild(document.createTextNode(node.nodeValue.slice(offset)));
node.parentNode.replaceChild(fragment, node);
});
});
/* END HOME COUNTY CURRENCY */
/* BEGIN HOME COUNTY WEATHER */
/* Add original game weather artwork only to explicitly labelled weather fields. */
mw.hook('wikipage.content').add(function ($content) {
var content = $content && $content[0];
if (!content || mw.config.get('wgAction') !== 'view') { return; }
var words = /\b(clear|sunny|rainy?|thunderstorms?|lightning|hail|snow|heat(?:\s*wave)?|cold\s*front|any(?:\s+weather)?|all(?:\s+weather)?)\b/gi;
function weatherKey(word) {
var key = word.toLowerCase().replace(/\s/g, '');
if (/^(clear|sunny)$/.test(key)) { return 'sunny'; }
if (/^rain/.test(key)) { return 'rain'; }
if (/^(lightning|thunderstorms?)$/.test(key)) { return 'thunderstorm'; }
if (/^heat/.test(key)) { return 'heatwave'; }
if (key === 'coldfront') { return 'snow'; }
if (/^(any|all)/.test(key)) { return 'any'; }
return key;
}
function icon(key) {
var img = document.createElement('img');
var artwork = key === 'heatwave' || key === 'sunny' ? 'Heatwave' : key.charAt(0).toUpperCase() + key.slice(1);
img.className = 'hc-weather-icon hc-weather-' + key;
img.src = '/Special:Redirect/file/Home_County_Weather_' + artwork + '.png';
img.alt = '';
img.setAttribute('aria-hidden', 'true');
img.width = 24;
img.height = 24;
return img;
}
function decorate(field) {
var walker = document.createTreeWalker(field, NodeFilter.SHOW_TEXT, null, false);
var nodes = [];
while (walker.nextNode()) {
if (!walker.currentNode.parentElement.closest('.hc-weather, script, style, code, pre')) {
nodes.push(walker.currentNode);
}
}
nodes.forEach(function (node) {
words.lastIndex = 0;
var match, offset = 0;
var fragment = document.createDocumentFragment();
while ((match = words.exec(node.nodeValue))) {
fragment.appendChild(document.createTextNode(node.nodeValue.slice(offset, match.index)));
var key = weatherKey(match[0]);
var item = document.createElement('span');
item.className = 'hc-weather hc-weather-condition-' + key;
if (key === 'any') {
var stack = document.createElement('span');
stack.className = 'hc-weather-any-icons';
stack.title = 'Any weather';
['sunny', 'rain', 'snow'].forEach(function (part) { stack.appendChild(icon(part)); });
item.appendChild(stack);
} else {
item.appendChild(icon(key));
}
item.appendChild(document.createTextNode(match[0]));
fragment.appendChild(item);
offset = match.index + match[0].length;
}
if (!offset) { return; }
fragment.appendChild(document.createTextNode(node.nodeValue.slice(offset)));
node.parentNode.replaceChild(fragment, node);
});
}
Array.prototype.forEach.call(content.querySelectorAll('.portable-infobox [data-source="weather"], .portable-infobox [data-source="conditions"]'), function (field) {
var value = field.classList.contains('pi-data-value') ? field : field.querySelector('.pi-data-value');
if (value) { decorate(value); }
});
Array.prototype.forEach.call(content.querySelectorAll('table.articleList, table.wikitable, table.fandom-table'), function (table) {
var columns = {}, occupied = [];
Array.prototype.forEach.call(table.rows, function (row, rowIndex) {
occupied[rowIndex] = occupied[rowIndex] || [];
var column = 0;
Array.prototype.forEach.call(row.cells, function (cell) {
while (occupied[rowIndex][column]) { column++; }
var width = cell.colSpan || 1, height = cell.rowSpan || 1;
for (var y = rowIndex; y < rowIndex + height; y++) {
occupied[y] = occupied[y] || [];
for (var x = column; x < column + width; x++) { occupied[y][x] = true; }
}
if (cell.tagName === 'TH' && /\b(?:weather|conditions)\b/i.test(cell.textContent)) {
for (var i = column; i < column + width; i++) { columns[i] = true; }
} else if (cell.tagName === 'TD' && width === 1 && columns[column]) {
decorate(cell);
}
column += width;
});
});
});
});
/* END HOME COUNTY WEATHER */
/* BEGIN HOME COUNTY STEAM RSS */
/* Home County Steam RSS updates: same-origin endpoint, text-only rendering. */
mw.hook('wikipage.content').add(function () {
var home = document.getElementById('hc-home');
var list = home && home.querySelector('.updates-panel .update-list');
if (!list || list.getAttribute('data-steam-feed-enhanced')) { return; }
list.setAttribute('data-steam-feed-enhanced', 'true');
var inFlight = false;
var timer;
function titleFor(title) {
if (title === 'Patch Notes & Life Update!') { return 'Life Update'; }
if (title === 'Patch Notes & A Surprise!') { return 'Patch Notes'; }
return title.replace(/&/g, 'and');
}
function newsItem(item) {
if (!item || typeof item.title !== 'string' || !item.title.trim() || item.title.length > 240 ||
typeof item.url !== 'string' || typeof item.date !== 'string') { return null; }
var link = document.createElement('a');
link.href = item.url;
var validUrl = link.protocol === 'https:' && !link.port && !link.username && !link.password &&
((link.hostname === 'store.steampowered.com' && link.pathname.indexOf('/news/app/3672100/') === 0) ||
(link.hostname === 'steamcommunity.com' && link.pathname.indexOf('/games/3672100/announcements/') === 0));
var date = new Date(item.date);
if (!validUrl || !isFinite(date.getTime())) { return null; }
return {title: titleFor(item.title.trim()), url: link.href, date: date};
}
function render(data) {
if (!data || !Array.isArray(data.items)) { return; }
var items = data.items.map(newsItem).filter(function (item) { return item !== null; });
items.sort(function (a, b) { return b.date.getTime() - a.date.getTime(); });
items = items.slice(0, 3);
if (!items.length) { return; }
var fragment = document.createDocumentFragment();
items.forEach(function (item) {
var row = document.createElement('li');
var date = document.createElement('time');
date.className = 'update-date';
date.setAttribute('datetime', item.date.toISOString().slice(0, 10));
date.textContent = item.date.toLocaleDateString('en-GB', {day: 'numeric', month: 'short', year: 'numeric', timeZone: 'UTC'});
var link = document.createElement('a');
link.href = item.url;
link.textContent = item.title;
row.appendChild(date);
row.appendChild(link);
fragment.appendChild(row);
});
list.textContent = '';
list.appendChild(fragment);
list.setAttribute('data-steam-feed-loaded', 'true');
if (typeof data.updated_at === 'string') {
list.setAttribute('data-steam-feed-updated', data.updated_at);
}
}
function refresh() {
if (!document.documentElement.contains(list)) {
clearInterval(timer);
document.removeEventListener('visibilitychange', visibilityChanged);
return;
}
if (inFlight || document.hidden) { return; }
inFlight = true;
$.ajax({url: '/api/steam-news', dataType: 'json', timeout: 10000})
.done(render)
.always(function () { inFlight = false; });
// On failure the existing successful list remains visible.
}
function visibilityChanged() {
if (!document.hidden) { refresh(); }
}
refresh();
timer = setInterval(refresh, 300000);
document.addEventListener('visibilitychange', visibilityChanged);
});
/* END HOME COUNTY STEAM RSS */