MediaWiki:Common.js: Difference between revisions
From Melvor Idle
No edit summary |
(Amend for ECMASCript 5 compatibility) |
||
(64 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); | |||
*/ | |||
( | }(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 ( | if (mw.config.get('wgUserName') === null) { | ||
return false; | |||
} else { | } else { | ||
return true; | |||
} | } | ||
} | |||
function addToPageTools() { | |||
}); | if (isUserLoggedIn()) { | ||
var canEdit = mw.config.get('wgIsProbablyEditable'); | |||
var isArticle = mw.config.get('wgIsArticle'); | |||
var isCurRevision = mw.config.get('wgRevisionId') >= mw.config.get('wgCurRevisionId'); | |||
if (canEdit && isArticle && isCurRevision) { | |||
$.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 | function showAndroidAppDownloadLink() { | ||
var shouldShowDownload = /Android/i.test(window.navigator.userAgent) && window.navigator.userAgent.indexOf('gonative melvorwiki') === -1; | |||
if ( | if (shouldShowDownload) { | ||
$('.android-app-download').removeClass('d-none'); | |||
} else { | } else { | ||
$('.android-app-download').addClass('d-none'); | |||
} | } | ||
} | } | ||
$(document).ready(function () { | |||
// Table sticky headers | |||
initStickyHeaders(); | |||
// Collapsible elements (for Extension:MobileFrontend) | |||
initCollapsibleElements(); | |||
// Wiki app native navigation | |||
initWikiAppSidebar(); | |||
// Show iOS App download link | |||
showIOSAppDownloadLink(); | |||
// Show Android App download link | |||
showAndroidAppDownloadLink(); | |||
// Add links to Page Tools navigation | |||
addToPageTools(); | |||
}); |
Latest revision as of 11:14, 20 May 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()) {
var canEdit = mw.config.get('wgIsProbablyEditable');
var isArticle = mw.config.get('wgIsArticle');
var isCurRevision = mw.config.get('wgRevisionId') >= mw.config.get('wgCurRevisionId');
if (canEdit && isArticle && isCurRevision) {
$.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();
});