Anonymous

MediaWiki:Common.js: Difference between revisions

From Melvor Idle
Add Purge link to page tools
(Amend sticky headers to avoid compatibility issues with tabber containers)
(Add Purge link to page tools)
(53 intermediate revisions by 2 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) {
}(this.jQuery, this.mediaWiki));
    'use strict';
});


    var hasLocalStorage = function(){
// </pre>
        try {
//[This is the end of the section stolen from https://oldschool.runescape.wiki/w/MediaWiki:Gadget-highlightTable-core.js]
            localStorage.setItem('test', 'test')
            localStorage.removeItem('test')
            return true
        } catch (e) {
            return false
        }
    }


    // constants
// Sticky headers for tables
    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 = {
// Returns a list of header rows within a sticky table
            /*
function getStickyTableHeaders(element) {
            * Stores the current uncompressed data for the current page.
var rv = [];
            */
for (var rowIdx = 0; rowIdx < 10; rowIdx++) {
            data: null,
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
            * Perform initial checks on the page and browser.
function setStickyHeaderTop(element) {
            */
var isOverflown = false;
            init: function() {
var parentElem = element.parentElement;
                var $tables = $('table.' + TABLE_CLASS),
if (parentElem !== undefined) {
                    hashedPageName = self.hashString(mw.config.get('wgPageName'));
isOverflown = (parentElem.scrollHeight > parentElem.clientHeight || parentElem.scrollWidth > parentElem.clientWidth);
}


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


                self.data = self.load(hashedPageName, $tables.length);
var cumulativeRowHeight = 0;
                self.initTables(hashedPageName, $tables);
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
            * Initialise table highlighting.
// adjusted as required when various elements are resized
            *
function initStickyObservers() {
            * @param hashedPageName The current page name as a hash.
if (ResizeObserver !== undefined) {
            * @param $tables A list of highlightable tables on the current page.
// If the headers are resized, then the header's top position (particularly the second
            */
// header) may need to be set again
            initTables: function(hashedPageName, $tables) {
var obvHeaderResize = new ResizeObserver(
                $tables.each(function(tIndex) {
function(entries) {
                    var $this = $(this),
var st = [];
                        // data cells
for (var i = 0; i < entries.length; i++) {
                        $cells = $this.find('td'),
var headerRow = entries[i].target;
                        $rows = $this.find('tr:has(td)'),
var stickyTable = headerRow.parentElement.parentElement;
                        // don't rely on headers to find number of columns     
if (!st.includes(stickyTable)) {
                        // count them dynamically
st.push(stickyTable);
                        columns = 1,
}
                        tableData = self.data[tIndex],
}
                        mode = 'cells';
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);
}
}
}
}
);


                    // Switching between either highlighting rows or cells
var stickyTables = document.getElementsByClassName('stickyHeader');
                    if (!$this.hasClass('individual')) {
for (var i = 0; i < stickyTables.length; i++) {
                        mode = 'rows';
var stickyTable = stickyTables[i];
                        $cells = $rows;
// Observe the table's parent for content overflows
                    }
obvOverflow.observe(stickyTable.parentElement);


                    // initialise rows if necessary
var headElems = getStickyTableHeaders(stickyTable);
                    while ($cells.length > tableData.length) {
for (var j = 0; j < headElems.length; j++) {
                        tableData.push(0);
// Observe the table's header rows for resizing
                    }
obvHeaderResize.observe(headElems[j]);
}
}
}
}


                    // counting the column count
function initStickyHeaders() {
                    // necessary to determine colspan of reset button
var stickyTables = document.getElementsByClassName('stickyHeader');
                    $rows.each(function() {
if (stickyTables.length > 0) {
                        var $this = $(this);
var elemArticle = document.getElementsByTagName('article');
                        columns = Math.max(columns, $this.children('th,td').length);
for (i = 0; i < stickyTables.length; i++) {
                    });
var stickyTable = stickyTables[i];


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


                        // forbid highlighting any cells/rows that have class nohighlight
if (stickyTable.classList.contains('stickyHeader')) {
                        if (!$this.hasClass('nohighlight')) {
// If the table is still sticky, initialize header positions
                            // initialize highlighting based on the cookie
setStickyHeaderTop(stickyTable);
                            self.setHighlight($this, cellData);
}
}


                            // set mouse events
// Initialize observers
                            $this
initStickyObservers();
                                .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]);
// Reset sticky header positions when the window resizes, as this may
                                        self.save(hashedPageName);
// 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]);
}
});
}
}


                    // add a button for reset
function initCollapsibleElements() {
                    var button = new OO.ui.ButtonWidget({
/* 2024-02-18 Allow collapsing of elements with class 'mw-collapsible', in line
                        label: 'Clear selection',
* with desktop view behaviour. Extension:MobileFrontend disables this, but
                        icon: 'clear',
* it is still desirable for our use case
                        title: 'Removes all highlights from the table',
*/
                        classes: ['ht-reset'] // this class is targeted by other gadgets, be careful removing it
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' }
        ];


                    button.$element.click(function() {
        if (isLoggedIn) {
                        $cells.each(function(cIndex) {
            items.push(myFavs);
                            tableData[cIndex] = 0;
        } else {
                            self.setHighlight($(this), 0);
            items.push(signIn);
                        });
        }


                        self.save(hashedPageName, $tables.length);
        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: [] }
        );


                    $this.append(
        if (isLoggedIn) {
                        $('<tfoot>')
            items.push(accountManagement);
                            .append(
        }
                                $('<tr>')
                                    .append(
                                        $('<th>')
                                            .attr('colspan', columns)
                                            .append(button.$element)
                                    )
                            )
                    );
                });
            },


             /*
        items.push({
            * Change the cell background color based on mouse events.
             label: 'Special Tools',
            *
            url: '',
            * @param $cell The cell element.
            isGrouping: true,
            * @param val The value to control what class to add (if any).
            subLinks: [
            *            0 -> light off (no class)
                { label: 'Upload Files', url: 'https://wiki.melvoridle.com/w/Special:Upload', subLinks: [], icon: 'fas fa-upload' },
            *            1 -> light on
                { label: 'Special Pages', url: 'https://wiki.melvoridle.com/w/Special:SpecialPages', subLinks: [], icon: 'fas fa-file-powerpoint' }
            *            2 -> mouse over
            ],
            */
             icon: 'fas fa-gear'
             setHighlight: function($cell, val) {
        });
                $cell.removeClass(MOUSE_OVER_CLASS);
                $cell.removeClass(LIGHT_ON_CLASS);


                switch (val) {
        items.push({
                    // light on
            label: 'Support Melvor Idle',
                    case 1:
            url: '',
                        $cell.addClass(LIGHT_ON_CLASS);
            isGrouping: true,
                        break;
            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
        });


                    // mouse-over
        items.push({
                    case 2:
            label: 'Melvor Idle Socials',
                        $cell.addClass(MOUSE_OVER_CLASS);
            url: '',
                        break;
            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() {
            * Merge the updated data for the current page into the data for other pages into local storage.
  if (mw.config.get('wgUserName') === null) {
            *
    return false;
            * @param hashedPageName A hash of the current page name.
  } else {
            */
    return true;
            save: function(hashedPageName) {
  }
                // load the existing data so we know where to save it
}
                var curData = localStorage.getItem(STORAGE_KEY),
                    compressedData;


                if (curData === null) {
function addToPageTools() {
                    curData = {};
if (isUserLoggedIn()) {
                } else {
$.when(mw.loader.using(['mediawiki.util']), $.ready).then( function() {
                    curData = JSON.parse(curData);
mw.util.addPortletLink(
                    curData = self.parse(curData);
'p-cactions',
                }
mw.util.getUrl() + '?action=purge',
 
'Purge',
                // merge in our updated data and compress it
't-purgecache',
                curData[hashedPageName] = self.data;
'Purge the cache for this page',
                compressedData = self.compress(curData);
null,
 
null
                // 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
function showIOSAppDownloadLink() {
                                if (k < 0) {
    var shouldShowDownload = /iPhone|iPad|iPod/i.test(window.navigator.userAgent) && window.navigator.userAgent.indexOf('gonative melvorwiki') === -1;
                                    k = 0;
     if (shouldShowDownload) {
                                }
    $('.ios-app-download').removeClass('d-none');
 
     } else {
                                for (j = 5; j >= 0; j -= 1) {
    $('.ios-app-download').addClass('d-none');
                                    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]
 
/* Sets the top property for stickyHeader tables */
function setStickyHeaderTop() {
  const stickyTables = document.getElementsByClassName('stickyHeader');
  const headStyles = getComputedStyle(document.getElementById('mw-header-container'));
  var headHeight = document.getElementById('mw-header-container').offsetHeight;
  if (headStyles !== undefined && headStyles.position === 'static') {
     headHeight = 0;
  }
  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 - 1;
        for (var j = 0; j < secondHeaders.length; j++) {
          secondHeaders[j].style.top = secondHeight + 'px';
        }
      }
     }
     }
  }
}
}


function toggleDarkMode() {
function showAndroidAppDownloadLink() {
document.body.classList.toggle('darkMode');
    var shouldShowDownload = /Android/i.test(window.navigator.userAgent) && window.navigator.userAgent.indexOf('gonative melvorwiki') === -1;
localStorage.setItem('darkMode', localStorage.getItem('darkMode') !== 'true');
    if (shouldShowDownload) {
    $('.android-app-download').removeClass('d-none');
    } else {
    $('.android-app-download').addClass('d-none');
    }
}
}


$(document).ready(function () {
$(document).ready(function () {
const elemSticky = document.getElementsByClassName('stickyHeader');
// Table sticky headers
if (elemSticky.length > 0) {
initStickyHeaders();
// Sticky headers do not function well when Tabber containers/article tags.
// Collapsible elements (for Extension:MobileFrontend)
// Therefore identify any stickyHeader tables within these containers
initCollapsibleElements();
//  and remove the stickyHeader class
// Wiki app native navigation
const elemArticle = document.getElementsByTagName('article');
initWikiAppSidebar();
if (elemArticle.length > 0) {
// Show iOS App download link
for (var kS = 0; kS < elemSticky.length; kS++) {
showIOSAppDownloadLink();
for (var kA = 0; kA < elemArticle.length; kA++) {
// Show Android App download link
const eSticky = elemSticky[kS];
showAndroidAppDownloadLink();
const eArticle = elemArticle[kA];
// Add links to Page Tools navigation
if (eArticle.contains(eSticky)) {
addToPageTools();
eSticky.classList.remove('stickyHeader');
}
}
}
}
setStickyHeaderTop();
$(window).resize(setStickyHeaderTop);
}
 
const darkMode = localStorage.getItem('darkMode');
if (darkMode === 'true') {
document.body.classList.add('darkMode');
} else {
document.body.classList.remove('darkMode');
}
});
});
// Add dark mode links to "Wiki tools" and personal tools menus
$.when(mw.loader.using(['mediawiki.util']), $.ready).then( function() {
var dmLinkTools = mw.util.addPortletLink('p-tb', '#', 'Dark mode', 't-darkmode', 'Toggle between a dark and light theme', null, '#t-specialpages');
var dmLinkPersonal = mw.util.addPortletLink('p-personal', '#', 'Dark mode', 'pt-darkmode', 'Toggle between a dark and light theme', null, null);
dmLinkPersonal.style.marginTop = '0.5rem';
$(dmLinkTools).on('click', function(e) {
e.preventDefault();
toggleDarkMode();
});
$(dmLinkPersonal).on('click', function(e) {
e.preventDefault();
toggleDarkMode();
});
} );