Jump to content

MediaWiki:Common.js

From The Home County Wiki

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