MediaWiki:Common.js: Difference between revisions

From Melvor Idle
(Attempt to correct issue where sticky headers may not be initialized correctly before the page fully loads)
(Add Purge link to page tools)
 
(43 intermediate revisions by 2 users not shown)
Line 521: Line 521:
//[This is the end of the section stolen from https://oldschool.runescape.wiki/w/MediaWiki:Gadget-highlightTable-core.js]
//[This is the end of the section stolen from https://oldschool.runescape.wiki/w/MediaWiki:Gadget-highlightTable-core.js]


/* Sets the top property for stickyHeader tables */
// Sticky headers for tables
function setStickyHeaderTop() {
 
var stickyTables = document.getElementsByClassName('stickyHeader');
// Returns a list of header rows within a sticky table
var headStyles = getComputedStyle(document.getElementById('mw-header-container'));
function getStickyTableHeaders(element) {
var headHeight = document.getElementById('mw-header-container').offsetHeight;
var rv = [];
if (headStyles !== undefined && headStyles.position === 'static') {
for (var rowIdx = 0; rowIdx < 10; rowIdx++) {
headHeight = 0;
var rowElem = element.getElementsByClassName('headerRow-' + rowIdx.toString());
if (rowElem.length === 0) {
break;
}
rv.push(rowElem[0]);
}
return rv;
}
 
// Given a table element, sets the headers' 'top' property as required
function setStickyHeaderTop(element) {
var isOverflown = false;
var parentElem = element.parentElement;
if (parentElem !== undefined) {
isOverflown = (parentElem.scrollHeight > parentElem.clientHeight || parentElem.scrollWidth > parentElem.clientWidth);
}
 
// Determine the height of the MediWiki header, if it is always visible at the top of the page.
// If the parent div to the table is overflowing, then the header's top position is set in
// relation to that parent element and this can be skipped
var headHeight = 0;
if (!isOverflown) {
var headElem = document.getElementById('mw-header-container');
if ((headElem !== undefined) && (headElem !== null)) {
var headStyles = getComputedStyle(headElem);
if ((headStyles !== undefined) && (headStyles.position !== 'static')) {
headHeight = headElem.offsetHeight;
}
}
}
}
for (var i = 0; i < stickyTables.length; i++) {
 
var firstRow = stickyTables[i].getElementsByClassName('headerRow-0');
var cumulativeRowHeight = 0;
var secondRow = stickyTables[i].getElementsByClassName('headerRow-1');
var headElems = getStickyTableHeaders(element);
var firstHeight = 0;
for (var rowIdx = 0; rowIdx < headElems.length; rowIdx++) {
if (firstRow.length > 0) {
// Find each header row in sequence. When found, set or remove the 'top' attribute as
firstHeight = firstRow[0].offsetHeight;
// required. If not found, then break
var firstHeaders = firstRow[0].getElementsByTagName('th');
var headElem = headElems[rowIdx];
for (var j = 0; j < firstHeaders.length; j++) {
var cellElems = headElem.getElementsByTagName('th');
firstHeaders[j].style.top = headHeight + 'px';
var topPos = headHeight + cumulativeRowHeight;
// Iterate over all header cells for the current header row
for (var cellIdx = 0; cellIdx < cellElems.length; cellIdx++) {
var cell = cellElems[cellIdx];
if ((isOverflown) && (cell.style.top !== undefined)) {
// If the table has overflown, then unset the 'top' attribute
cell.style.top = '';
}
}
if (secondRow.length > 0) {
else {
var secondHeaders = secondRow[0].getElementsByTagName('th');
// Otherwise, set the 'top' attribute with the appropriate position
var secondHeight = headHeight + firstHeight - 1;
cell.style.top = topPos.toString() + 'px';
for (var j = 0; j < secondHeaders.length; j++) {
}
secondHeaders[j].style.top = secondHeight + 'px';
}
cumulativeRowHeight += headElem.offsetHeight - 1;
}
}
 
// Initialize observers for stickyHeader tables. These enable attributes of table headers to be
// adjusted as required when various elements are resized
function initStickyObservers() {
if (ResizeObserver !== undefined) {
// If the headers are resized, then the header's top position (particularly the second
// header) may need to be set again
var obvHeaderResize = new ResizeObserver(
function(entries) {
var st = [];
for (var i = 0; i < entries.length; i++) {
var headerRow = entries[i].target;
var stickyTable = headerRow.parentElement.parentElement;
if (!st.includes(stickyTable)) {
st.push(stickyTable);
}
}
for (var j = 0; j < st.length; j++) {
setStickyHeaderTop(st[j]);
}
}
}
);
// If the parent div to a table is overflowing, then the header's top position needs to
// be set in relation to the top of that parent element
var obvOverflow = new ResizeObserver(
function(entries) {
for (var i = 0; i < entries.length; i++) {
var tableParent = entries[i].target;
// The child elements will contain the table we want to set sticky headers for
var stickyTables = tableParent.children;
for (var j = 0; j < stickyTables.length; j++) {
var stickyTable = stickyTables[j];
if (stickyTable.classList.contains('stickyHeader')) {
setStickyHeaderTop(stickyTable);
}
}
}
}
);
var stickyTables = document.getElementsByClassName('stickyHeader');
for (var i = 0; i < stickyTables.length; i++) {
var stickyTable = stickyTables[i];
// Observe the table's parent for content overflows
obvOverflow.observe(stickyTable.parentElement);
var headElems = getStickyTableHeaders(stickyTable);
for (var j = 0; j < headElems.length; j++) {
// Observe the table's header rows for resizing
obvHeaderResize.observe(headElems[j]);
}
}
}
}
Line 550: Line 636:
}
}


$(document).ready(function () {
function initStickyHeaders() {
// Table sticky headers
var stickyTables = document.getElementsByClassName('stickyHeader');
var elemSticky = document.getElementsByClassName('stickyHeader');
if (stickyTables.length > 0) {
if (elemSticky.length > 0) {
// Sticky headers do not function well when Tabber containers/article tags.
// Therefore identify any stickyHeader tables within these containers
//  and remove the stickyHeader class
var elemArticle = document.getElementsByTagName('article');
var elemArticle = document.getElementsByTagName('article');
if (elemArticle.length > 0) {
for (i = 0; i < stickyTables.length; i++) {
for (var kS = 0; kS < elemSticky.length; kS++) {
var stickyTable = stickyTables[i];
for (var kA = 0; kA < elemArticle.length; kA++) {
 
var eSticky = elemSticky[kS];
// Sticky headers do not function well when Tabber containers/article tags.
var eArticle = elemArticle[kA];
// Therefore identify any stickyHeader tables within these containers
if (eArticle.contains(eSticky)) {
//  and remove the stickyHeader class
eSticky.classList.remove('stickyHeader');
for (j = 0; j < elemArticle.length; j++) {
}
if (elemArticle[j].contains(stickyTable)) {
stickyTable.classList.remove('stickyHeader');
}
}
}
if (stickyTable.classList.contains('stickyHeader')) {
// If the table is still sticky, initialize header positions
setStickyHeaderTop(stickyTable);
}
}
}
}
// Initialize sticky header positions
 
setStickyHeaderTop();
// Initialize observers
initStickyObservers();
 
// Reset sticky header positions when the window resizes, as this may
// Reset sticky header positions when the window resizes, as this may
// affect visibility of fixed elements at the top of the page
// affect visibility of fixed elements at the top of the page
$(window).resize(setStickyHeaderTop);
$(window).resize(
// Reset sticky header positions once the page is fully loaded.
function() {
// Without this, headers may have an incorrect offset
var stickyTables = document.getElementsByClassName('stickyHeader');
window.addEventListener('load', function(event) { setStickyHeaderTop(); });
for (i = 0; i < stickyTables.length; i++) {
setStickyHeaderTop(stickyTables[i]);
}
});
}
}
 
function initCollapsibleElements() {
/* 2024-02-18 Allow collapsing of elements with class 'mw-collapsible', in line
* with desktop view behaviour. Extension:MobileFrontend disables this, but
* it is still desirable for our use case
*/
mw.loader.using('jquery.makeCollapsible').then(function () { $('.mw-collapsible').makeCollapsible(); });
}
 
function initWikiAppSidebar() {
    if (navigator.userAgent.indexOf('gonative melvorwiki') > -1) {
        var isLoggedIn = isUserLoggedIn();
        var myFavs = {
            url: 'https://wiki.melvoridle.com/w/Special:Favoritelist',
            label: 'My Favourite Pages',
            subLinks: [],
            icon: 'fas fa-star'
        };
        var signIn = {
            label: 'Sign In / Register',
            url: 'https://wiki.melvoridle.com/index.php?title=Special:UserLogin&returnto=Main+Page',
            subLinks: []
        };
        var accountManagement = {
            label: 'Account Management',
            url: '',
            isGrouping: true,
            subLinks: [
                { label: 'Preferences', url: 'https://wiki.melvoridle.com/w/Special:Preferences', subLinks: [] },
                { label: 'Logout', url: 'https://wiki.melvoridle.com/index.php?title=Special:UserLogout&returnto=Main+Page', subLinks: [] }
            ],
            icon: 'fas fa-user-gear'
        };
        var items = [
            { url: 'https://wiki.melvoridle.com/w/Main_Page', label: 'Home', subLinks: [], icon: 'fas fa-house' }
        ];
 
        if (isLoggedIn) {
            items.push(myFavs);
        } else {
            items.push(signIn);
        }
 
        items.push(
            { label: 'Guides', url: 'https://wiki.melvoridle.com/w/Guides', icon: 'fas fa-book', subLinks: [] },
            { label: 'FAQ', url: 'https://wiki.melvoridle.com/w/FAQ', icon: 'fas fa-circle-question', subLinks: [] },
            { label: 'Changelog', url: 'https://wiki.melvoridle.com/w/Changelog', icon: 'fas fa-book-open', subLinks: [] },
            { label: 'Mod Creation', url: 'https://wiki.melvoridle.com/w/Mod_Creation', icon: 'fas fa-hammer', subLinks: [] }
        );
 
        if (isLoggedIn) {
            items.push(accountManagement);
        }
 
        items.push({
            label: 'Special Tools',
            url: '',
            isGrouping: true,
            subLinks: [
                { label: 'Upload Files', url: 'https://wiki.melvoridle.com/w/Special:Upload', subLinks: [], icon: 'fas fa-upload' },
                { label: 'Special Pages', url: 'https://wiki.melvoridle.com/w/Special:SpecialPages', subLinks: [], icon: 'fas fa-file-powerpoint' }
            ],
            icon: 'fas fa-gear'
        });
 
        items.push({
            label: 'Support Melvor Idle',
            url: '',
            isGrouping: true,
            subLinks: [
                { label: 'Buy Melvor Idle', url: 'https://wiki.melvoridle.com/w/Full_Version', subLinks: [] },
                { label: 'Buy Throne of the Herald', url: 'https://wiki.melvoridle.com/w/Throne_of_the_Herald_Expansion', subLinks: [] },
                { label: 'Buy Atlas of Discovery', url: 'https://wiki.melvoridle.com/w/Atlas_of_Discovery_Expansion', subLinks: [] },
                { label: 'Patreon', url: 'https://patreon.com/MelvorIdle', subLinks: [], icon: 'fab fa-patreon' }
            ],
            icon: null
        });
 
        items.push({
            label: 'Melvor Idle Socials',
            url: '',
            isGrouping: true,
            subLinks: [
                { label: 'Discord', url: 'https://discord.gg/melvoridle', subLinks: [], icon: 'fab fa-discord' },
                { label: 'Reddit', url: 'https://reddit.com/r/MelvorIdle', icon: 'custom icon-reddit-alien', subLinks: [] },
                { label: 'Twitter', url: 'https://twitter.com/melvoridle', icon: 'custom icon-twitter', subLinks: [] },
                { label: 'Facebook', url: 'https://facebook.com/melvoridle', icon: 'custom icon-facebook', subLinks: [] },
                { label: 'Instagram', url: 'https://instagram.com/melvoridle', icon: 'custom icon-instagram', subLinks: [] }
            ]
        });
        median.sidebar.setItems({ "items": items, "enabled": true, "persist": false });
    }
}
 
function isUserLoggedIn() {
  if (mw.config.get('wgUserName') === null) {
    return false;
  } else {
    return true;
  }
}
 
function addToPageTools() {
if (isUserLoggedIn()) {
$.when(mw.loader.using(['mediawiki.util']), $.ready).then( function() {
mw.util.addPortletLink(
'p-cactions',
mw.util.getUrl() + '?action=purge',
'Purge',
't-purgecache',
'Purge the cache for this page',
null,
null
);
});
}
}
}
function showIOSAppDownloadLink() {
    var shouldShowDownload = /iPhone|iPad|iPod/i.test(window.navigator.userAgent) && window.navigator.userAgent.indexOf('gonative melvorwiki') === -1;
    if (shouldShowDownload) {
    $('.ios-app-download').removeClass('d-none');
    } else {
    $('.ios-app-download').addClass('d-none');
    }
}
function showAndroidAppDownloadLink() {
    var shouldShowDownload = /Android/i.test(window.navigator.userAgent) && window.navigator.userAgent.indexOf('gonative melvorwiki') === -1;
    if (shouldShowDownload) {
    $('.android-app-download').removeClass('d-none');
    } else {
    $('.android-app-download').addClass('d-none');
    }
}
$(document).ready(function () {
// Table sticky headers
initStickyHeaders();
// Collapsible elements (for Extension:MobileFrontend)
initCollapsibleElements();
// Wiki app native navigation
initWikiAppSidebar();
// Show iOS App download link
showIOSAppDownloadLink();
// Show Android App download link
showAndroidAppDownloadLink();
// Add links to Page Tools navigation
addToPageTools();
});
});

Latest revision as of 19:36, 26 March 2024

/* Any JavaScript here will be loaded for all users on every page load. */

/** <pre>
 * [NOTE: The below Javascript was stolen from https://oldschool.runescape.wiki/w/MediaWiki:Gadget-highlightTable-core.js on 10/28/2020 by User:Falterfire]
 * highlightTable.js
 *
 * Description:
 * Adds highlighting to tables
 *
 * History:
 * - 1.0: Row highlighting                        - Quarenon
 * - 1.1: Update from pengLocations.js v1.0       - Quarenon
 * - 2.0: pengLocations v2.1, Granular cookie     - Saftzie
 * - 2.1: Made compatible with jquery.tablesorter - Cqm
 * - 2.2: Switch to localStorage                  - Cqm
 * - 3.0: Allow cell highlighting                 - mejrs
 *
 * @todo Allow the stored data to be coupled to the table in question. Currently the data is stored
 *       on the page itself, so if any tables are shuffled, the highlighting doesn't follow. For
 *       the same reason tables hosted on other pages are not synchronized.
 */

/**
 * DATA STORAGE STRUCTURE
 * ----------------------
 *
 * In its raw, uncompressed format, the stored data is as follows:
 * {
 *     hashedPageName1: [
 *         [0, 1, 0, 1, 0, 1],
 *         [1, 0, 1, 0, 1, 0],
 *         [0, 0, 0, 0, 0, 0]
 *     ],
 *     hashedPageName2: [
 *         [0, 1, 0, 1, 0, 1],
 *         [1, 0, 1, 0, 1, 0],
 *         [0, 0, 0, 0, 0, 0]
 *     ]
 * }
 *
 * Where `hashedPageNameX` is the value of wgPageName passed through our `hashString` function,
 * the arrays of numbers representing tables on a page (from top to bottom) and the numbers
 * representing whether a row is highlighted or not, depending on if it is 1 or 0 respectively.
 *
 * During compression, these numbers are collected into groups of 6 and converted to base64.
 * For example:
 *
 *   1. [0, 1, 0, 1, 0, 1]
 *   2. 0x010101             (1 + 4 + 16 = 21)
 *   3. BASE_64_URL[21]      (U)
 *
 * Once each table's rows have been compressed into strings, they are concatenated using `.` as a
 * delimiter. The hashed page name (which is guaranteed to be 8 characters long) is then prepended
 * to this string to look something like the following:
 *
 *   XXXXXXXXab.dc.ef
 *
 *
 * The first character of a hashed page name is then used to form the object that is actually
 * stored. As the hashing function uses hexadecimal, this gives us 16 possible characters (0-9A-Z).
 *
 * {
 *     A: ...
 *     B: ...
 *     C: ...
 *     // etc.
 * }
 *
 * The final step of compression is to merge each page's data together under it's respective top
 * level key. this is done by concatenation again, separated by a `!`.
 *
 * The resulting object is then converted to a string and persisted in local storage. When
 * uncompressing data, simply perform the following steps in reverse.
 *
 * For the implementation of this algorithm, see:
 * - `compress`
 * - `parse`
 * - `hashString`
 *
 * Note that while rows could theoretically be compressed further by using all ASCII characters,
 * eventually we'd start using characters outside printable ASCII which makes debugging painful.
 */

/*jshint bitwise:false, camelcase:true, curly:true, eqeqeq:true, es3:false,
    forin:true, immed:true, indent:4, latedef:true, newcap:true,
    noarg:true, noempty:true, nonew:true, plusplus:true, quotmark:single,
    undef:true, unused:true, strict:true, trailing:true,
    browser:true, devel:false, jquery:true,
    onevar:true
*/
mw.loader.using(['oojs-ui-core', 'oojs-ui.styles.icons-interactions']).done(function() {
	(function($, mw) {
	    'use strict';
	
	    var hasLocalStorage = function(){
	        try {
	            localStorage.setItem('test', 'test')
	            localStorage.removeItem('test')
	            return true
	        } catch (e) {
	            return false
	        }
	    }
	
	    // constants
	    var STORAGE_KEY = 'mi:lightTable',
	        TABLE_CLASS = 'lighttable',
	        LIGHT_ON_CLASS = 'highlight-on',
	        MOUSE_OVER_CLASS = 'highlight-over',
	        BASE_64_URL = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_',
	        PAGE_SEPARATOR = '!',
	        TABLE_SEPARATOR = '.',
	        CASTAGNOLI_POLYNOMIAL = 0x04c11db7,
	        UINT32_MAX = 0xffffffff,
	
	        self = {
	            /*
	             * Stores the current uncompressed data for the current page.
	             */
	            data: null,
	
	            /*
	             * Perform initial checks on the page and browser.
	             */
	            init: function() {
	                var $tables = $('table.' + TABLE_CLASS),
	                    hashedPageName = self.hashString(mw.config.get('wgPageName'));
	
	                // check we have some tables to interact with
	                if (!$tables.length) {
	                    return;
	                }
	                // check the browser supports local storage
	                if (!hasLocalStorage()) {
	                    return;
	                }
	
	                self.data = self.load(hashedPageName, $tables.length);
	                self.initTables(hashedPageName, $tables);
	            },
	
	            /*
	             * Initialise table highlighting.
	             *
	             * @param hashedPageName The current page name as a hash.
	             * @param $tables A list of highlightable tables on the current page.
	             */
	            initTables: function(hashedPageName, $tables) {
	                $tables.each(function(tIndex) {
	                    var $this = $(this),
	                        // data cells
	                        $cells = $this.find('td'),
	                        $rows = $this.find('tr:has(td)'),
	                        // don't rely on headers to find number of columns      
	                        // count them dynamically
	                        columns = 1,
	                        tableData = self.data[tIndex],
	                        mode = 'cells';
	
	                    // Switching between either highlighting rows or cells
	                    if (!$this.hasClass('individual')) {
	                        mode = 'rows';
	                        $cells = $rows;
	                    }
	
	                    // initialise rows if necessary
	                    while ($cells.length > tableData.length) {
	                        tableData.push(0);
	                    }
	
	                    // counting the column count
	                    // necessary to determine colspan of reset button
	                    $rows.each(function() {
	                        var $this = $(this);
	                        columns = Math.max(columns, $this.children('th,td').length);
	                    });
	
	                    $cells.each(function(cIndex) {
	                        var $this = $(this),
	                            cellData = tableData[cIndex];
	
	                        // forbid highlighting any cells/rows that have class nohighlight
	                        if (!$this.hasClass('nohighlight')) {
	                            // initialize highlighting based on the cookie
	                            self.setHighlight($this, cellData);
	
	                            // set mouse events
	                            $this
	                                .mouseover(function() {
	                                    self.setHighlight($this, 2);
	                                })
	                                .mouseout(function() {
	                                    self.setHighlight($this, tableData[cIndex]);
	                                })
	                                .click(function(e) {
	                                    // don't toggle highlight when clicking links
	                                    if ((e.target.tagName !== 'A') && (e.target.tagName !== 'IMG')) {
	                                        // 1 -> 0
	                                        // 0 -> 1
	                                        tableData[cIndex] = 1 - tableData[cIndex];
	
	                                        self.setHighlight($this, tableData[cIndex]);
	                                        self.save(hashedPageName);
	                                    }
	                                });
	                        }
	                    });
	
	                    // add a button for reset
	                    var button = new OO.ui.ButtonWidget({
	                        label: 'Clear selection',
	                        icon: 'clear',
	                        title: 'Removes all highlights from the table',
	                        classes: ['ht-reset'] // this class is targeted by other gadgets, be careful removing it
	                    });
	
	
	                    button.$element.click(function() {
	                        $cells.each(function(cIndex) {
	                            tableData[cIndex] = 0;
	                            self.setHighlight($(this), 0);
	                        });
	
	                        self.save(hashedPageName, $tables.length);
	                    });
	
	                    $this.append(
	                        $('<tfoot>')
	                            .append(
	                                $('<tr>')
	                                    .append(
	                                        $('<th>')
	                                            .attr('colspan', columns)
	                                            .append(button.$element)
	                                    )
	                            )
	                    );
	                });
	            },
	
	            /*
	             * Change the cell background color based on mouse events.
	             *
	             * @param $cell The cell element.
	             * @param val The value to control what class to add (if any).
	             *            0 -> light off (no class)
	             *            1 -> light on
	             *            2 -> mouse over
	             */
	            setHighlight: function($cell, val) {
	                $cell.removeClass(MOUSE_OVER_CLASS);
	                $cell.removeClass(LIGHT_ON_CLASS);
	
	                switch (val) {
	                    // light on
	                    case 1:
	                        $cell.addClass(LIGHT_ON_CLASS);
	                        break;
	
	                    // mouse-over
	                    case 2:
	                        $cell.addClass(MOUSE_OVER_CLASS);
	                        break;
	                }
	            },
	
	            /*
	             * Merge the updated data for the current page into the data for other pages into local storage.
	             *
	             * @param hashedPageName A hash of the current page name.
	             */
	            save: function(hashedPageName) {
	                // load the existing data so we know where to save it
	                var curData = localStorage.getItem(STORAGE_KEY),
	                    compressedData;
	
	                if (curData === null) {
	                    curData = {};
	                } else {
	                    curData = JSON.parse(curData);
	                    curData = self.parse(curData);
	                }
	
	                // merge in our updated data and compress it
	                curData[hashedPageName] = self.data;
	                compressedData = self.compress(curData);
	
	                // convert to a string and save to localStorage
	                compressedData = JSON.stringify(compressedData);
	                localStorage.setItem(STORAGE_KEY, compressedData);
	            },
	
	            /*
	             * Compress the entire data set using tha algoritm documented at the top of the page.
	             *
	             * @param data The data to compress.
	             *
	             * @return the compressed data.
	             */
	            compress: function(data) {
	                var ret = {};
	
	                Object.keys(data).forEach(function(hashedPageName) {
	                    var pageData = data[hashedPageName],
	                        pageKey = hashedPageName.charAt(0);
	
	                    if (!ret.hasOwnProperty(pageKey)) {
	                        ret[pageKey] = {};
	                    }
	
	                    ret[pageKey][hashedPageName] = [];
	
	                    pageData.forEach(function(tableData) {
	                        var compressedTableData = '',
	                            i, j, k;
	
	                        for (i = 0; i < Math.ceil(tableData.length / 6); i += 1) {
	                            k = tableData[6 * i];
	
	                            for (j = 1; j < 6; j += 1) {
	                                k = 2 * k + ((6 * i + j < tableData.length) ? tableData[6 * i + j] : 0);
	                            }
	
	                            compressedTableData += BASE_64_URL.charAt(k);
	                        }
	
	                        ret[pageKey][hashedPageName].push(compressedTableData);
	                    });
	
	                    ret[pageKey][hashedPageName] = ret[pageKey][hashedPageName].join(TABLE_SEPARATOR);
	                });
	
	                Object.keys(ret).forEach(function(pageKey) {
	                    var hashKeys = Object.keys(ret[pageKey]),
	                        hashedData = [];
	
	                    hashKeys.forEach(function(key) {
	                        var pageData = ret[pageKey][key];
	                        hashedData.push(key + pageData);
	                    });
	
	                    hashedData = hashedData.join(PAGE_SEPARATOR);
	                    ret[pageKey] = hashedData;
	                });
	
	                return ret;
	            },
	
	            /*
	             * Get the existing data for the current page.
	             *
	             * @param hashedPageName A hash of the current page name.
	             * @param numTables The number of tables on the current page. Used to ensure the loaded
	             *                  data matches the number of tables on the page thus handling cases
	             *                  where tables have been added or removed. This does not check the
	             *                  amount of rows in the given tables.
	             *
	             * @return The data for the current page.
	             */
	            load: function(hashedPageName, numTables) {
	                var data = localStorage.getItem(STORAGE_KEY),
	                    pageData;
	
	                if (data === null) {
	                    pageData = [];
	                } else {
	                    data = JSON.parse(data);
	                    data = self.parse(data);
	
	                    if (data.hasOwnProperty(hashedPageName)) {
	                        pageData = data[hashedPageName];
	                    } else {
	                        pageData = [];
	                    }
	                }
	
	                // if more tables were added
	                // add extra arrays to store the data in
	                // also populates if no existing data was found
	                while (numTables > pageData.length) {
	                    pageData.push([]);
	                }
	
	                // if tables were removed, remove data from the end of the list
	                // as there's no way to tell which was removed
	                while (numTables < pageData.length) {
	                    pageData.pop();
	                }
	
	                return pageData;
	            },
	
	            /*
	             * Parse the compressed data as loaded from local storage using the algorithm desribed
	             * at the top of the page.
	             *
	             * @param data The data to parse.
	             *
	             * @return the parsed data.
	             */
	            parse: function(data) {
	                var ret = {};
	
	                Object.keys(data).forEach(function(pageKey) {
	                    var pageData = data[pageKey].split(PAGE_SEPARATOR);
	
	                    pageData.forEach(function(tableData) {
	                        var hashedPageName = tableData.substr(0, 8);
	
	                        tableData = tableData.substr(8).split(TABLE_SEPARATOR);
	                        ret[hashedPageName] = [];
	
	                        tableData.forEach(function(rowData, index) {
	                            var i, j, k;
	
	                            ret[hashedPageName].push([]);
	
	                            for (i = 0; i < rowData.length; i += 1) {
	                                k = BASE_64_URL.indexOf(rowData.charAt(i));
	
	                                // input validation
	                                if (k < 0) {
	                                    k = 0;
	                                }
	
	                                for (j = 5; j >= 0; j -= 1) {
	                                    ret[hashedPageName][index][6 * i + j] = (k & 0x1);
	                                    k >>= 1;
	                                }
	                            }
	                        });
	                    });
	
	                });
	
	                return ret;
	            },
	
	            /*
	             * Hash a string into a big endian 32 bit hex string. Used to hash page names.
	             *
	             * @param input The string to hash.
	             *
	             * @return the result of the hash.
	             */
	            hashString: function(input) {
	                var ret = 0,
	                    table = [],
	                    i, j, k;
	
	                // guarantee 8-bit chars
	                input = window.unescape(window.encodeURI(input));
	
	                // calculate the crc (cyclic redundancy check) for all 8-bit data
	                // bit-wise operations discard anything left of bit 31
	                for (i = 0; i < 256; i += 1) {
	                    k = (i << 24);
	
	                    for (j = 0; j < 8; j += 1) {
	                        k = (k << 1) ^ ((k >>> 31) * CASTAGNOLI_POLYNOMIAL);
	                    }
	                    table[i] = k;
	                }
	
	                // the actual calculation
	                for (i = 0; i < input.length; i += 1) {
	                    ret = (ret << 8) ^ table[(ret >>> 24) ^ input.charCodeAt(i)];
	                }
	
	                // make negative numbers unsigned
	                if (ret < 0) {
	                    ret += UINT32_MAX;
	                }
	
	                // 32-bit hex string, padded on the left
	                ret = '0000000' + ret.toString(16).toUpperCase();
	                ret = ret.substr(ret.length - 8);
	
	                return ret;
	            }
	        };
	
	    $(self.init);
	
	    /*
	    // sample data for testing the algorithm used
	    var data = {
	        // page1
	        '0FF47C63': [
	            [0, 1, 1, 0, 1, 0],
	            [0, 1, 1, 0, 1, 0, 1, 1, 1],
	            [0, 0, 0, 0, 1, 1, 0, 0]
	        ],
	        // page2
	        '02B75ABA': [
	            [0, 1, 0, 1, 1, 0],
	            [1, 1, 1, 0, 1, 0, 1, 1, 0],
	            [0, 0, 1, 1, 0, 0, 0, 0]
	        ],
	        // page3
	        '0676470D': [
	            [1, 0, 0, 1, 0, 1],
	            [1, 0, 0, 1, 0, 1, 0, 0, 0],
	            [1, 1, 1, 1, 0, 0, 1, 1]
	        ]
	    };
	
	    console.log('input', data);
	
	    var compressedData = self.compress(data);
	    console.log('compressed', compressedData);
	
	    var parsedData = self.parse(compressedData);
	    console.log(parsedData);
	    */

	}(this.jQuery, this.mediaWiki));
});

// </pre>
//[This is the end of the section stolen from https://oldschool.runescape.wiki/w/MediaWiki:Gadget-highlightTable-core.js]

// Sticky headers for tables

// Returns a list of header rows within a sticky table
function getStickyTableHeaders(element) {
	var rv = [];
	for (var rowIdx = 0; rowIdx < 10; rowIdx++) {
		var rowElem = element.getElementsByClassName('headerRow-' + rowIdx.toString());
		if (rowElem.length === 0) {
			break;
		}
		rv.push(rowElem[0]);
	}
	return rv;
}

// Given a table element, sets the headers' 'top' property as required
function setStickyHeaderTop(element) {
	var isOverflown = false;
	var parentElem = element.parentElement;
	if (parentElem !== undefined) {
		isOverflown = (parentElem.scrollHeight > parentElem.clientHeight || parentElem.scrollWidth > parentElem.clientWidth);
	}

	// Determine the height of the MediWiki header, if it is always visible at the top of the page.
	// If the parent div to the table is overflowing, then the header's top position is set in
	// relation to that parent element and this can be skipped
	var headHeight = 0;
	if (!isOverflown) {
		var headElem = document.getElementById('mw-header-container');
		if ((headElem !== undefined) && (headElem !== null)) {
			var headStyles = getComputedStyle(headElem);
			if ((headStyles !== undefined) && (headStyles.position !== 'static')) {
				headHeight = headElem.offsetHeight;
			}
		}
	}

	var cumulativeRowHeight = 0;
	var headElems = getStickyTableHeaders(element);
	for (var rowIdx = 0; rowIdx < headElems.length; rowIdx++) {
		// Find each header row in sequence. When found, set or remove the 'top' attribute as
		// required. If not found, then break
		var headElem = headElems[rowIdx];
		var cellElems = headElem.getElementsByTagName('th');
		var topPos = headHeight + cumulativeRowHeight;
		// Iterate over all header cells for the current header row
		for (var cellIdx = 0; cellIdx < cellElems.length; cellIdx++) {
			var cell = cellElems[cellIdx];
			if ((isOverflown) && (cell.style.top !== undefined)) {
				// If the table has overflown, then unset the 'top' attribute
				cell.style.top = '';
			}
			else {
				// Otherwise, set the 'top' attribute with the appropriate position
				cell.style.top = topPos.toString() + 'px';
			}
		}
		cumulativeRowHeight += headElem.offsetHeight - 1;
	}
}

// Initialize observers for stickyHeader tables. These enable attributes of table headers to be
// adjusted as required when various elements are resized
function initStickyObservers() {
	if (ResizeObserver !== undefined) {
		// If the headers are resized, then the header's top position (particularly the second
		// header) may need to be set again
		var obvHeaderResize = new ResizeObserver(
			function(entries) {
				var st = [];
				for (var i = 0; i < entries.length; i++) {
					var headerRow = entries[i].target;
					var stickyTable = headerRow.parentElement.parentElement;
					if (!st.includes(stickyTable)) {
						st.push(stickyTable);
					}
				}
				for (var j = 0; j < st.length; j++) {
					setStickyHeaderTop(st[j]);
				}
			}
		);
		// If the parent div to a table is overflowing, then the header's top position needs to
		// be set in relation to the top of that parent element
		var obvOverflow = new ResizeObserver(
			function(entries) {
				for (var i = 0; i < entries.length; i++) {
					var tableParent = entries[i].target;
					// The child elements will contain the table we want to set sticky headers for
					var stickyTables = tableParent.children;
					for (var j = 0; j < stickyTables.length; j++) {
						var stickyTable = stickyTables[j];
						if (stickyTable.classList.contains('stickyHeader')) {
							setStickyHeaderTop(stickyTable);
						}
					}
				}
			}
		);

		var stickyTables = document.getElementsByClassName('stickyHeader');
		for (var i = 0; i < stickyTables.length; i++) {
			var stickyTable = stickyTables[i];
			// Observe the table's parent for content overflows
			obvOverflow.observe(stickyTable.parentElement);

			var headElems = getStickyTableHeaders(stickyTable);
			for (var j = 0; j < headElems.length; j++) {
				// Observe the table's header rows for resizing
				obvHeaderResize.observe(headElems[j]);
			}
		}
	}
}

function initStickyHeaders() {
	var stickyTables = document.getElementsByClassName('stickyHeader');
	if (stickyTables.length > 0) {
		var elemArticle = document.getElementsByTagName('article');
		for (i = 0; i < stickyTables.length; i++) {
			var stickyTable = stickyTables[i];

			// Sticky headers do not function well when Tabber containers/article tags.
			// Therefore identify any stickyHeader tables within these containers 
			//  and remove the stickyHeader class
			for (j = 0; j < elemArticle.length; j++) {
				if (elemArticle[j].contains(stickyTable)) {
					stickyTable.classList.remove('stickyHeader');
				}
			}

			if (stickyTable.classList.contains('stickyHeader')) {
				// If the table is still sticky, initialize header positions
				setStickyHeaderTop(stickyTable);
			}
		}

		// Initialize observers
		initStickyObservers();

		// Reset sticky header positions when the window resizes, as this may
		// affect visibility of fixed elements at the top of the page
		$(window).resize(
			function() {
				var stickyTables = document.getElementsByClassName('stickyHeader');
				for (i = 0; i < stickyTables.length; i++) {
					setStickyHeaderTop(stickyTables[i]);
				}
			});
	}
}

function initCollapsibleElements() {
	/* 2024-02-18 Allow collapsing of elements with class 'mw-collapsible', in line
	 * with desktop view behaviour. Extension:MobileFrontend disables this, but
	 * it is still desirable for our use case
	 */
	mw.loader.using('jquery.makeCollapsible').then(function () { $('.mw-collapsible').makeCollapsible(); });
}

function initWikiAppSidebar() {
    if (navigator.userAgent.indexOf('gonative melvorwiki') > -1) {
        var isLoggedIn = isUserLoggedIn();
        var myFavs = {
            url: 'https://wiki.melvoridle.com/w/Special:Favoritelist',
            label: 'My Favourite Pages',
            subLinks: [],
            icon: 'fas fa-star'
        };
        var signIn = {
            label: 'Sign In / Register',
            url: 'https://wiki.melvoridle.com/index.php?title=Special:UserLogin&returnto=Main+Page',
            subLinks: []
        };
        var accountManagement = {
            label: 'Account Management',
            url: '',
            isGrouping: true,
            subLinks: [
                { label: 'Preferences', url: 'https://wiki.melvoridle.com/w/Special:Preferences', subLinks: [] },
                { label: 'Logout', url: 'https://wiki.melvoridle.com/index.php?title=Special:UserLogout&returnto=Main+Page', subLinks: [] }
            ],
            icon: 'fas fa-user-gear'
        };
        var items = [
            { url: 'https://wiki.melvoridle.com/w/Main_Page', label: 'Home', subLinks: [], icon: 'fas fa-house' }
        ];

        if (isLoggedIn) {
            items.push(myFavs);
        } else {
            items.push(signIn);
        }

        items.push(
            { label: 'Guides', url: 'https://wiki.melvoridle.com/w/Guides', icon: 'fas fa-book', subLinks: [] },
            { label: 'FAQ', url: 'https://wiki.melvoridle.com/w/FAQ', icon: 'fas fa-circle-question', subLinks: [] },
            { label: 'Changelog', url: 'https://wiki.melvoridle.com/w/Changelog', icon: 'fas fa-book-open', subLinks: [] },
            { label: 'Mod Creation', url: 'https://wiki.melvoridle.com/w/Mod_Creation', icon: 'fas fa-hammer', subLinks: [] }
        );

        if (isLoggedIn) {
            items.push(accountManagement);
        }

        items.push({
            label: 'Special Tools',
            url: '',
            isGrouping: true,
            subLinks: [
                { label: 'Upload Files', url: 'https://wiki.melvoridle.com/w/Special:Upload', subLinks: [], icon: 'fas fa-upload' },
                { label: 'Special Pages', url: 'https://wiki.melvoridle.com/w/Special:SpecialPages', subLinks: [], icon: 'fas fa-file-powerpoint' }
            ],
            icon: 'fas fa-gear'
        });

        items.push({
            label: 'Support Melvor Idle',
            url: '',
            isGrouping: true,
            subLinks: [
                { label: 'Buy Melvor Idle', url: 'https://wiki.melvoridle.com/w/Full_Version', subLinks: [] },
                { label: 'Buy Throne of the Herald', url: 'https://wiki.melvoridle.com/w/Throne_of_the_Herald_Expansion', subLinks: [] },
                { label: 'Buy Atlas of Discovery', url: 'https://wiki.melvoridle.com/w/Atlas_of_Discovery_Expansion', subLinks: [] },
                { label: 'Patreon', url: 'https://patreon.com/MelvorIdle', subLinks: [], icon: 'fab fa-patreon' }
            ],
            icon: null
        });

        items.push({
            label: 'Melvor Idle Socials',
            url: '',
            isGrouping: true,
            subLinks: [
                { label: 'Discord', url: 'https://discord.gg/melvoridle', subLinks: [], icon: 'fab fa-discord' },
                { label: 'Reddit', url: 'https://reddit.com/r/MelvorIdle', icon: 'custom icon-reddit-alien', subLinks: [] },
                { label: 'Twitter', url: 'https://twitter.com/melvoridle', icon: 'custom icon-twitter', subLinks: [] },
                { label: 'Facebook', url: 'https://facebook.com/melvoridle', icon: 'custom icon-facebook', subLinks: [] },
                { label: 'Instagram', url: 'https://instagram.com/melvoridle', icon: 'custom icon-instagram', subLinks: [] }
            ]
        });
        median.sidebar.setItems({ "items": items, "enabled": true, "persist": false });
    }
}

function isUserLoggedIn() {
  if (mw.config.get('wgUserName') === null) {
    return false;
  } else {
    return true;
  }
}

function addToPageTools() {
	if (isUserLoggedIn()) {
		$.when(mw.loader.using(['mediawiki.util']), $.ready).then( function() {
			mw.util.addPortletLink(
				'p-cactions',
				mw.util.getUrl() + '?action=purge',
				'Purge',
				't-purgecache',
				'Purge the cache for this page',
				null,
				null
			);
		});
	}
}

function showIOSAppDownloadLink() {
    var shouldShowDownload = /iPhone|iPad|iPod/i.test(window.navigator.userAgent) && window.navigator.userAgent.indexOf('gonative melvorwiki') === -1;
    if (shouldShowDownload) {
    	$('.ios-app-download').removeClass('d-none');
    } else {
    	$('.ios-app-download').addClass('d-none');
    }
}

function showAndroidAppDownloadLink() {
    var shouldShowDownload = /Android/i.test(window.navigator.userAgent) && window.navigator.userAgent.indexOf('gonative melvorwiki') === -1;
    if (shouldShowDownload) {
    	$('.android-app-download').removeClass('d-none');
    } else {
    	$('.android-app-download').addClass('d-none');
    }
}

$(document).ready(function () {
	// Table sticky headers
	initStickyHeaders();
	// Collapsible elements (for Extension:MobileFrontend)
	initCollapsibleElements();
	// Wiki app native navigation
	initWikiAppSidebar();
	// Show iOS App download link
	showIOSAppDownloadLink();
	// Show Android App download link
	showAndroidAppDownloadLink();
	// Add links to Page Tools navigation
	addToPageTools();
});