MediaWiki:Common.js: Difference between revisions

From Melvor Idle
(Add Dark mode link to Wiki tools menu)
(Add Purge link to page tools)
 
(60 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() {
                    button.$element.click(function() {
    if (navigator.userAgent.indexOf('gonative melvorwiki') > -1) {
                        $cells.each(function(cIndex) {
        var isLoggedIn = isUserLoggedIn();
                            tableData[cIndex] = 0;
        var myFavs = {
                            self.setHighlight($(this), 0);
            url: 'https://wiki.melvoridle.com/w/Special:Favoritelist',
                        });
             label: 'My Favourite Pages',
 
             subLinks: [],
                        self.save(hashedPageName, $tables.length);
            icon: 'fas fa-star'
                    });
        };
 
        var signIn = {
                    $this.append(
            label: 'Sign In / Register',
                        $('<tfoot>')
            url: 'https://wiki.melvoridle.com/index.php?title=Special:UserLogin&returnto=Main+Page',
                            .append(
            subLinks: []
                                $('<tr>')
        };
                                    .append(
        var accountManagement = {
                                        $('<th>')
             label: 'Account Management',
                                            .attr('colspan', columns)
             url: '',
                                            .append(button.$element)
             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'
 
            /*
            * 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;
            }
         };
         };
        var items = [
            { url: 'https://wiki.melvoridle.com/w/Main_Page', label: 'Home', subLinks: [], icon: 'fas fa-house' }
        ];


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


    /*
        items.push(
    // sample data for testing the algorithm used
            { label: 'Guides', url: 'https://wiki.melvoridle.com/w/Guides', icon: 'fas fa-book', subLinks: [] },
    var data = {
             { label: 'FAQ', url: 'https://wiki.melvoridle.com/w/FAQ', icon: 'fas fa-circle-question', subLinks: [] },
        // page1
             { label: 'Changelog', url: 'https://wiki.melvoridle.com/w/Changelog', icon: 'fas fa-book-open', subLinks: [] },
        '0FF47C63': [
             { label: 'Mod Creation', url: 'https://wiki.melvoridle.com/w/Mod_Creation', icon: 'fas fa-hammer', subLinks: [] }
            [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);
        if (isLoggedIn) {
            items.push(accountManagement);
        }


    var compressedData = self.compress(data);
        items.push({
    console.log('compressed', compressedData);
            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'
        });


    var parsedData = self.parse(compressedData);
        items.push({
    console.log(parsedData);
            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
        });


}(this.jQuery, this.mediaWiki));
        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 });
    }
}


// </pre>
function isUserLoggedIn() {
//[This is the end of the section stolen from https://oldschool.runescape.wiki/w/MediaWiki:Gadget-highlightTable-core.js]
   if (mw.config.get('wgUserName') === null) {
 
    return false;
/* Sets the top property for stickyHeader tables */
  } else {
function setStickyHeaderTop() {
    return true;
   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 () {
if (document.getElementsByClassName("stickyHeader").length > 0) {
setStickyHeaderTop();
$(window).resize(setStickyHeaderTop);
}


const darkMode = localStorage.getItem("darkMode");
function addToPageTools() {
if (darkMode === "true") {
if (isUserLoggedIn()) {
document.body.classList.add("darkMode");
$.when(mw.loader.using(['mediawiki.util']), $.ready).then( function() {
} else {
mw.util.addPortletLink(
document.body.classList.remove("darkMode");
'p-cactions',
mw.util.getUrl() + '?action=purge',
'Purge',
't-purgecache',
'Purge the cache for this page',
null,
null
);
});
}
}
}


$("#personal-inner").append('<button onClick="toggleDarkMode();" style="margin:.5rem!important;">Toggle Dark Mode</button>');
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');
    }
}


// Add dark mode link to "Wiki tools" menu
function showAndroidAppDownloadLink() {
$.when(mw.loader.using(['mediawiki.util']), $.ready).then( function() {
    var shouldShowDownload = /Android/i.test(window.navigator.userAgent) && window.navigator.userAgent.indexOf('gonative melvorwiki') === -1;
var darkModeLink = mw.util.addPortletLink('p-tb', '#', 'Toggle Dark Mode');
    if (shouldShowDownload) {
$(darkModeLink).on('click', function(e) {
    $('.android-app-download').removeClass('d-none');
e.preventDefault();
    } else {
toggleDarkMode();
    $('.android-app-download').addClass('d-none');
})
    }
} );
}


 
$(document).ready(function () {
function toggleDarkMode() {
// Table sticky headers
const darkMode = localStorage.getItem("darkMode");
initStickyHeaders();
if (darkMode !== "true") {
// Collapsible elements (for Extension:MobileFrontend)
    localStorage.setItem("darkMode", true);
initCollapsibleElements();
document.body.classList.add("darkMode");
// Wiki app native navigation
} else {
initWikiAppSidebar();
    localStorage.setItem("darkMode", false);
// Show iOS App download link
document.body.classList.remove("darkMode");
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();
});