MediaWiki:Common.js: Difference between revisions

Add Purge link to page tools
(Stealing code from the OSRS wiki to try to get highlighting tables working)
(Add Purge link to page tools)
(81 intermediate revisions by 3 users not shown)
Line 89: Line 89:
     onevar: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);
    */


(function($, mw, OO, rs) {
}(this.jQuery, this.mediaWiki));
    'use strict';
});


    // constants
// </pre>
    var STORAGE_KEY = 'rs:lightTable',
//[This is the end of the section stolen from https://oldschool.runescape.wiki/w/MediaWiki:Gadget-highlightTable-core.js]
        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 = {
// Sticky headers for tables
            /*
            * Stores the current uncompressed data for the current page.
            */
            data: null,


            /*
// Returns a list of header rows within a sticky table
            * Perform initial checks on the page and browser.
function getStickyTableHeaders(element) {
            */
var rv = [];
            init: function() {
for (var rowIdx = 0; rowIdx < 10; rowIdx++) {
                var $tables = $('table.' + TABLE_CLASS),
var rowElem = element.getElementsByClassName('headerRow-' + rowIdx.toString());
                    hashedPageName = self.hashString(mw.config.get('wgPageName'));
if (rowElem.length === 0) {
break;
}
rv.push(rowElem[0]);
}
return rv;
}


                // check we have some tables to interact with
// Given a table element, sets the headers' 'top' property as required
                if (!$tables.length) {
function setStickyHeaderTop(element) {
                    return;
var isOverflown = false;
                }
var parentElem = element.parentElement;
                // check the browser supports local storage
if (parentElem !== undefined) {
                if (!rs.hasLocalStorage()) {
isOverflown = (parentElem.scrollHeight > parentElem.clientHeight || parentElem.scrollWidth > parentElem.clientWidth);
                    return;
}
                }


                self.data = self.load(hashedPageName, $tables.length);
// Determine the height of the MediWiki header, if it is always visible at the top of the page.
                self.initTables(hashedPageName, $tables);
// 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;
            * Initialise table highlighting.
var headElems = getStickyTableHeaders(element);
            *
for (var rowIdx = 0; rowIdx < headElems.length; rowIdx++) {
            * @param hashedPageName The current page name as a hash.
// Find each header row in sequence. When found, set or remove the 'top' attribute as
            * @param $tables A list of highlightable tables on the current page.
// required. If not found, then break
            */
var headElem = headElems[rowIdx];
            initTables: function(hashedPageName, $tables) {
var cellElems = headElem.getElementsByTagName('th');
                $tables.each(function(tIndex) {
var topPos = headHeight + cumulativeRowHeight;
                    var $this = $(this),
// Iterate over all header cells for the current header row
                        // data cells
for (var cellIdx = 0; cellIdx < cellElems.length; cellIdx++) {
                        $cells = $this.find('td'),
var cell = cellElems[cellIdx];
                        $rows = $this.find('tr:has(td)'),
if ((isOverflown) && (cell.style.top !== undefined)) {
                        // don't rely on headers to find number of columns     
// If the table has overflown, then unset the 'top' attribute
                        // count them dynamically
cell.style.top = '';
                        columns = 1,
}
                        tableData = self.data[tIndex],
else {
                        mode = 'cells';
// Otherwise, set the 'top' attribute with the appropriate position
cell.style.top = topPos.toString() + 'px';
}
}
cumulativeRowHeight += headElem.offsetHeight - 1;
}
}


                    // Switching between either highlighting rows or cells
// Initialize observers for stickyHeader tables. These enable attributes of table headers to be
                    if (!$this.hasClass('individual')) {
// adjusted as required when various elements are resized
                        mode = 'rows';
function initStickyObservers() {
                        $cells = $rows;
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);
}
}
}
}
);


                    // initialise rows if necessary
var stickyTables = document.getElementsByClassName('stickyHeader');
                    while ($cells.length > tableData.length) {
for (var i = 0; i < stickyTables.length; i++) {
                        tableData.push(0);
var stickyTable = stickyTables[i];
                    }
// Observe the table's parent for content overflows
obvOverflow.observe(stickyTable.parentElement);


                    // counting the column count
var headElems = getStickyTableHeaders(stickyTable);
                    // necessary to determine colspan of reset button
for (var j = 0; j < headElems.length; j++) {
                    $rows.each(function() {
// Observe the table's header rows for resizing
                        var $this = $(this);
obvHeaderResize.observe(headElems[j]);
                        columns = Math.max(columns, $this.children('th,td').length);
}
                    });
}
}
}


                    $cells.each(function(cIndex) {
function initStickyHeaders() {
                        var $this = $(this),
var stickyTables = document.getElementsByClassName('stickyHeader');
                            cellData = tableData[cIndex];
if (stickyTables.length > 0) {
var elemArticle = document.getElementsByTagName('article');
for (i = 0; i < stickyTables.length; i++) {
var stickyTable = stickyTables[i];


                        // forbid highlighting any cells/rows that have class nohighlight
// Sticky headers do not function well when Tabber containers/article tags.
                        if (!$this.hasClass('nohighlight')) {
// Therefore identify any stickyHeader tables within these containers
                            // initialize highlighting based on the cookie
//  and remove the stickyHeader class
                            self.setHighlight($this, cellData);
for (j = 0; j < elemArticle.length; j++) {
if (elemArticle[j].contains(stickyTable)) {
stickyTable.classList.remove('stickyHeader');
}
}


                            // set mouse events
if (stickyTable.classList.contains('stickyHeader')) {
                            $this
// If the table is still sticky, initialize header positions
                                .mouseover(function() {
setStickyHeaderTop(stickyTable);
                                    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]);
// Initialize observers
                                        self.save(hashedPageName);
initStickyObservers();
                                    }
                                });
                        }
                    });


                    // add a button for reset
// Reset sticky header positions when the window resizes, as this may
                    var button = new OO.ui.ButtonWidget({
// affect visibility of fixed elements at the top of the page
                        label: 'Clear selection',
$(window).resize(
                        icon: 'clear',
function() {
                        title: 'Removes all highlights from the table',
var stickyTables = document.getElementsByClassName('stickyHeader');
                        classes: ['ht-reset'] // this class is targeted by other gadgets, be careful removing it
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(); });
}


                    button.$element.click(function() {
function initWikiAppSidebar() {
                        $cells.each(function(cIndex) {
    if (navigator.userAgent.indexOf('gonative melvorwiki') > -1) {
                            tableData[cIndex] = 0;
        var isLoggedIn = isUserLoggedIn();
                            self.setHighlight($(this), 0);
        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' }
        ];


                        self.save(hashedPageName, $tables.length);
        if (isLoggedIn) {
                    });
            items.push(myFavs);
        } else {
            items.push(signIn);
        }


                    $this.append(
        items.push(
                        $('<tfoot>')
            { label: 'Guides', url: 'https://wiki.melvoridle.com/w/Guides', icon: 'fas fa-book', subLinks: [] },
                            .append(
            { label: 'FAQ', url: 'https://wiki.melvoridle.com/w/FAQ', icon: 'fas fa-circle-question', subLinks: [] },
                                $('<tr>')
            { label: 'Changelog', url: 'https://wiki.melvoridle.com/w/Changelog', icon: 'fas fa-book-open', subLinks: [] },
                                    .append(
            { label: 'Mod Creation', url: 'https://wiki.melvoridle.com/w/Mod_Creation', icon: 'fas fa-hammer', subLinks: [] }
                                        $('<th>')
        );
                                            .attr('colspan', columns)
                                            .append(button.$element)
                                    )
                            )
                    );
                });
            },


            /*
        if (isLoggedIn) {
            * Change the cell background color based on mouse events.
             items.push(accountManagement);
            *
        }
            * @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) {
        items.push({
                    // light on
            label: 'Special Tools',
                    case 1:
            url: '',
                        $cell.addClass(LIGHT_ON_CLASS);
            isGrouping: true,
                        break;
            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'
        });


                    // mouse-over
        items.push({
                    case 2:
            label: 'Support Melvor Idle',
                        $cell.addClass(MOUSE_OVER_CLASS);
            url: '',
                        break;
            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({
            * Merge the updated data for the current page into the data for other pages into local storage.
            label: 'Melvor Idle Socials',
            *
             url: '',
            * @param hashedPageName A hash of the current page name.
            isGrouping: true,
            */
            subLinks: [
            save: function(hashedPageName) {
                { label: 'Discord', url: 'https://discord.gg/melvoridle', subLinks: [], icon: 'fab fa-discord' },
                 // load the existing data so we know where to save it
                { label: 'Reddit', url: 'https://reddit.com/r/MelvorIdle', icon: 'custom icon-reddit-alien', subLinks: [] },
                var curData = localStorage.getItem(STORAGE_KEY),
                { label: 'Twitter', url: 'https://twitter.com/melvoridle', icon: 'custom icon-twitter', subLinks: [] },
                    compressedData;
                { 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 });
    }
}


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


                // merge in our updated data and compress it
function addToPageTools() {
                curData[hashedPageName] = self.data;
if (isUserLoggedIn()) {
                compressedData = self.compress(curData);
$.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
);
});
}
}


                // convert to a string and save to localStorage
function showIOSAppDownloadLink() {
                compressedData = JSON.stringify(compressedData);
    var shouldShowDownload = /iPhone|iPad|iPod/i.test(window.navigator.userAgent) && window.navigator.userAgent.indexOf('gonative melvorwiki') === -1;
                localStorage.setItem(STORAGE_KEY, compressedData);
    if (shouldShowDownload) {
            },
    $('.ios-app-download').removeClass('d-none');
    } else {
    $('.ios-app-download').addClass('d-none');
    }
}


            /*
function showAndroidAppDownloadLink() {
            * Compress the entire data set using tha algoritm documented at the top of the page.
    var shouldShowDownload = /Android/i.test(window.navigator.userAgent) && window.navigator.userAgent.indexOf('gonative melvorwiki') === -1;
            *
    if (shouldShowDownload) {
            * @param data The data to compress.
    $('.android-app-download').removeClass('d-none');
            *
     } else {
            * @return the compressed data.
    $('.android-app-download').addClass('d-none');
            */
            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, this.OO, this.rswiki));
 
// </pre>
//[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 */
function setStickyHeaderTop() {
  const stickyTables = document.getElementsByClassName('stickyHeader');
  const headHeight = document.getElementById('mw-header-container').offsetHeight;
  for (var i = 0; i < stickyTables.length; i++) {
    const firstRow = stickyTables[i].getElementsByClassName('headerRow-0');
    const secondRow = stickyTables[i].getElementsByClassName('headerRow-1');
    var firstHeight = 0;
    if (firstRow.length > 0) {
      firstHeight = firstRow[0].offsetHeight;
      const firstHeaders = firstRow[0].getElementsByTagName('th');
      for (var j = 0; j < firstHeaders.length; j++) {
        firstHeaders[j].style.top = headHeight + 'px';
      }
      if (secondRow.length > 0) {
        const secondHeaders = secondRow[0].getElementsByTagName('th');
        var secondHeight = headHeight + firstHeight;
        for (var j = 0; j < secondHeaders.length; j++) {
          secondHeaders[j].style.top = secondHeight + 'px';
        }
      }
     }
     }
  }
}
}
$(document).ready(function () {
$(document).ready(function () {
  if (document.getElementsByClassName('stickyHeader').length > 0) {
// Table sticky headers
    setStickyHeaderTop();
initStickyHeaders();
    $(window).resize(setStickyHeaderTop);
// 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();
});