MediaWiki:Common.js
Jump to navigation
Jump to search
Note: After publishing, you may have to bypass your browser's cache to see the changes.
- Firefox / Safari: Hold Shift while clicking Reload, or press either Ctrl-F5 or Ctrl-R (⌘-R on a Mac)
- Google Chrome: Press Ctrl-Shift-R (⌘-Shift-R on a Mac)
- Internet Explorer / Edge: Hold Ctrl while clicking Refresh, or press Ctrl-F5
- Opera: Press Ctrl-F5.
/*
const osReducedMotion = window.matchMedia('(prefers-reduced-motion: reduce)');
let reducedMotionMode = osReducedMotion ? true : false;
mediaQuery.addEventListener('change', function() {
console.log(mediaQuery.media, mediaQuery.matches);
});
*/
var modals = {};
// (Kinda) preventing a bug where the top buttons would collapse and un-collapse
// indefinetly by moving the history button into the dropdown menu.
function moveHistoryToDropdown() {
function buttonToDropdown(target) {
const dropdownMenu = document.querySelector('#p-cactions .vector-menu-content-list');
if (!target) throw new Error('Target element is missing.');
else if (!dropdownMenu) return;
dropdownMenu.prepend(target);
}
const historyButton = document.getElementById('ca-history');
buttonToDropdown(historyButton);
}
// Prevent tables from extending beyond the page's width.
function tableOverflowInit() {
const tables = document.querySelectorAll('.mw-parser-output table'),
overflowWrappers = document.querySelectorAll('.overflow-wrapper');
if (tables.length === 0 || overflowWrappers.length > 0) return;
tables.forEach(function(table) {
const overflowWrapper = document.createElement('div');
overflowWrapper.className = 'overflow-wrapper';
table.parentNode.insertBefore(overflowWrapper, table);
overflowWrapper.appendChild(table);
});
}
// Make elements appear when scrolling to them.
function animateOnView() {
'use strict';
// Here you put all the elements you want to show on scroll.
const animatedElements = document.querySelectorAll('.is-invisible'),
eventsSection = document.body;
if (!animatedElements.length) return;
// Each element mentioned above will execute the "fade on scroll" script.
animatedElements.forEach(function(elem) {
const canAnimate = !elem.classList.contains('has-animated');
if (canAnimate) attachViewportObserver(elem);
});
function attachViewportObserver(elem) {
// Here you can configure how the observer will work.
const observerConfig = {
// Use the viewport as the observer.
root: null,
// Increases bounding box of the observer to be half of the screen. This means
// that elements won't fade into view until their topmost pixel reaches the
// window's half height.
rootMargin: '0px 0px -20% 0px',
// Activate it when 0% of the element intersects with the observer.
threshold: 0.0
};
const observer = new IntersectionObserver(function(e) {
e.forEach(function (e) {
// If the element is on view, trigger a function that makes it visible.
e.isIntersecting && handleIntersection();
});
}, observerConfig);
// Observes the elements declared on line 3.
observer.observe(elem);
// Adds the 'is-visible' class which, with CSS, will change the element's
// opacity from "0" to "1" and will move it 16px up by updating
// "transform: translateY(16px)" to "transform: translateY(0)".
// It also stops observing the element.
function handleIntersection() {
const delayAttribute = elem.getAttribute('data-animation-delay'),
delay = isNaN(delayAttribute) ? 0 : delayAttribute;
setTimeout(function() {
elem.classList.remove('is-invisible');
elem.classList.add('is-visible', 'has-animated');
observer.disconnect();
}, delay);
}
/*
function unhandleIntersection() {
elem.classList.remove('is-visible');
// observer.disconnect();
}
*/
}
/*
function showEventContents() {
// ...
}
eventsSection.addEventListener('transitionend', showEventContents);
*/
}
// Initialize news popup on the main page.
function newsInit() {
'use strict';
const newsContainer = document.getElementById('mp-news');
if (!newsContainer) return;
const username = mw.config.get('wgUserName'),
classes = {
panel: 'announcement-panel',
head: 'announcement-head',
title: 'announcement-title',
body: 'announcement-body',
image: 'announcement-image',
description: 'announcement-description',
footer: 'announcement-footer',
pagination: 'announcement-pagination',
paginationText: 'announcement-pagination-text',
buttonFirst: 'button-first',
buttonPrev: 'button-prev',
buttonNext: 'button-next',
buttonLast: 'button-last',
ctaContainer: 'announcement-cta-container',
cta: 'announcement-cta'
},
newsData = [
{
title: 'Christmas event',
date: '25/12/2023',
image: {
src: 'https://galaxylife.wiki.gg/images/f/fd/Winter_1.png',
alt: 'Sparragon with a Santa Claus hat flying with his S-Trike with gifts over some Starlings during a snowy night',
height: 245,
width: 518
},
description: 'Defeat Sparragon\'s forces and recover the gifts he stole to save Christmas!',
cta: {
href: 'https://galaxylife.wiki.gg/wiki/Christmas_Event',
label: null
}
},
{
title: 'Halloween event',
date: '31/10/2023',
image: {
src: 'https://galaxylife.wiki.gg/images/4/42/Halloween_2.png',
alt: 'A cyan commet falling on the horizon of the main planet at dusk',
height: 245,
width: 518
},
description: 'A special event where powerful undead foes will attack your base.',
cta: {
href: 'https://galaxylife.wiki.gg/wiki/Halloween_Event',
label: null
}
},
{
title: 'Doomsday',
date: '30/10/2023',
image: {
src: 'https://galaxylife.wiki.gg/images/f/fc/Doomsday_3.png',
alt: 'A Rock Golem coming out of an ancient Warp Gate',
height: 180,
width: 610
},
description: 'An event where powerful mayan Golems will emerge from a Warp Gate to wipe out your planet.',
cta: {
href: 'https://galaxylife.wiki.gg/wiki/Doomsday_Event',
label: null
}
}
],
panelHTML = '\
<div class="' + classes.panel + '">\
<div class="' + classes.head + '">\
<h2 class="' + classes.title + '"></h2>\
</div>\
<div class="' + classes.body + '">\
<img class="' + classes.image + '"/>\
<p class="' + classes.description + '"></p>\
</div>\
<div class="' + classes.footer + '">\
<div class="' + classes.pagination + '">\
<button class="' + classes.buttonFirst + '"></button>\
<button class="' + classes.buttonPrev + '"></button>\
<div class="' + classes.paginationText + '"></div>\
<button class="' + classes.buttonNext + '"></button>\
<button class="' + classes.buttonLast + '"></button>\
</div>\
<div class="' + classes.ctaContainer + '">\
<a class="' + classes.cta + '"></a>\
</div>\
</div>\
</div>';
const minPage = 0,
maxPage = newsData.length - 1;
var page = minPage;
function loadPanel(pagesData, actionType) {
if (isNaN(page)) throw new Error('Page number should be a number.');
const title = document.querySelector('.' + classes.title),
image = document.querySelector('.' + classes.image),
description = document.querySelector('.' + classes.description),
pagination = document.querySelector('.' + classes.paginationText),
cta = document.querySelector('.' + classes.cta);
if (actionType === 'prev' && page === minPage || actionType === 'next' && page === maxPage) return;
switch (actionType) {
case 'prev':
page--;
break;
case 'next':
page++;
break;
case 'first':
page = minPage;
break;
case 'last':
page = maxPage;
break;
}
title.innerText = pagesData[page].title;
description.innerText = pagesData[page].description;
pagination.innerText = String(page + 1) + '/' + String(maxPage + 1);
setAttributes(image, {
'src': pagesData[page].image.src,
'alt': pagesData[page].image.alt,
'height': pagesData[page].image.height,
'width': pagesData[page].image.width
});
if (pagesData[page].cta.href) {
cta.removeAttribute('disabled');
cta.setAttribute('href', pagesData[page].cta.href);
} else {
cta.removeAttribute('href');
cta.setAttribute('disabled', '');
}
cta.innerText = pagesData[page].cta.label || 'See more';
buttonsInit();
}
function buttonsInit() {
var buttonFirst = document.querySelector('.' + classes.buttonFirst),
buttonPrev = document.querySelector('.' + classes.buttonPrev),
buttonNext = document.querySelector('.' + classes.buttonNext),
buttonLast = document.querySelector('.' + classes.buttonLast);
const buttons = [buttonFirst, buttonPrev, buttonNext, buttonLast],
isOnlyPage = minPage === maxPage,
isFirstPage = page === minPage,
isLastPage = page === maxPage;
// Remove event listeners from buttons.
buttons.forEach(function(button) {
button.replaceWith(button.cloneNode(true));
});
// Select the cloned buttons (that don't have any event listeners attached).
var buttonFirst = document.querySelector('.' + classes.buttonFirst),
buttonPrev = document.querySelector('.' + classes.buttonPrev),
buttonNext = document.querySelector('.' + classes.buttonNext),
buttonLast = document.querySelector('.' + classes.buttonLast);
// Now disable buttons depending on which page the user is.
if (isOnlyPage) {
setAsInactive([buttonFirst, buttonPrev, buttonNext, buttonLast]);
} else if (isFirstPage) {
setAsInactive([buttonFirst, buttonPrev]);
setAsActive([buttonNext, buttonLast]);
} else if (isLastPage) {
setAsActive([buttonFirst, buttonPrev]);
setAsInactive([buttonNext, buttonLast]);
} else {
setAsActive([buttonFirst, buttonPrev, buttonNext, buttonLast]);
}
// Add event listeners to non-disabled buttons.
if (!buttonFirst.hasAttribute('disabled')) buttonFirst.addEventListener('click', function() { loadPanel(newsData, 'first'); });
if (!buttonPrev.hasAttribute('disabled')) buttonPrev.addEventListener('click', function() { loadPanel(newsData, 'prev'); });
if (!buttonNext.hasAttribute('disabled')) buttonNext.addEventListener('click', function() { loadPanel(newsData, 'next'); });
if (!buttonLast.hasAttribute('disabled')) buttonLast.addEventListener('click', function() { loadPanel(newsData, 'last'); });
}
function setAsActive(targets) {
targets.forEach(function(target) {
target.removeAttribute('disabled');
target.classList.add(classes.interactive);
});
}
function setAsInactive(targets) {
targets.forEach(function(target) {
target.setAttribute('disabled', '');
target.classList.remove(classes.interactive);
});
}
function setAttributes(target, attrs) {
for(var key in attrs) {
target.setAttribute(key, attrs[key]);
}
}
newsContainer.insertAdjacentHTML('beforeend', panelHTML);
loadPanel(newsData, 'none');
}
// Add cost calculators for colonies in it's respective article.
function colonyCostsInit() {
// Declaring useful stuff that will be used across the script.
const costsCalculator = document.getElementById('costs-calculator'),
fullCostListContainer = document.querySelector('.full-costs-list-generator-container'),
costGeneratorContainer = document.querySelector('.cost-generator-container'),
abs = Math.abs,
max = Math.max,
min = Math.min,
ceil = Math.ceil,
floor = Math.floor,
sqrt = Math.sqrt,
pow = Math.pow;
// Initializing calculators.
fullCostListInit();
costGeneratorInit();
costCalculatorInit();
// List generator - this script takes two numbers from input fields and
// generates a list whose length is based on the difference between both
// inputs.
function fullCostListInit() {
// Generating and adding html to the DOM.
const generatorHTML = '\
<form class="full-costs-list-generator">\
<label for="costlist-start">Get distance costs from: </label>\
<input type="number" id="costlist-start" name="costlist-start" min="0" max="2000" value="0" placeholder="0" required></input>\
<label for="costlist-end">to: </label>\
<input type="number" id="costlist-end" name="costlist-end" min="0" max="2000" value="10" placeholder="10" required></input>\
<input type="submit" class="wds-button full-costs-list-generator-submit" value="Get costs"></input>\
</form>\
<p class="full-cost-generator-output">Costs: </p>\
<ul class="full-costs-list"></ul>';
fullCostListContainer.insertAdjacentHTML('beforeend', generatorHTML);
const generator = document.querySelector('.full-costs-list-generator');
function submitHandler(event) {
// Prevent page refresh upon clicking the submit button.
event.preventDefault();
const list = document.querySelector('.full-costs-list');
var startValue = document.getElementById('costlist-start').value,
endValue = document.getElementById('costlist-end').value,
totalItems = abs(startValue - endValue);
// To prevent huge memory usage from the for loops, limit the amount
// of list elements that can be generated to 2000. Trying to bypass
// this limit by modifying the form's HTML will result in a warn
// being logged in the console and the script not executing.
if ((startValue || endValue) > 2000) {
console.warn('[Colony cost list generator]: Aborted list generation due to attempting to show more than 2000 elements.');
return;
}
// If the start value is higher than the end value, swap both values to
// correctly generate the list anyways.
if (startValue > endValue) [startValue, endValue] = [endValue, startValue];
list.innerHTML = '';
// Add a column parameter depending on the amount of list elements generated.
// We use CSS to determine how the list will render depending on factors such
// as the screen width and wether or not the right rail is collapsed/expanded.
if (totalItems >= 500) {
list.setAttribute('columns', '4');
} else if (totalItems >= 200) {
list.setAttribute('columns', '3');
} else if (totalItems >= 50) {
list.setAttribute('columns', '2');
} else {
list.removeAttribute('columns');
}
// Generate list elements and add them to the DOM.
for (var i = startValue; i < endValue; i++) {
const distance = i
, cost = max(66000, ceil((2000 - 0.072 * distance) * distance * 31))
, listElementHTML = '\
<li>Distance: <b>' + formattedNumber(distance) +'</b>. Cost: <a href="/wiki/Resources" title="Resources"><img alt="Cost" src="https://galaxylife.fandom.com/Special:Filepath/Icon_costs.png" decoding="async" loading="lazy" width="18" height="14" data-image-name="Icon_costs.png" data-image-key="Icon_costs.png" data-src="https://galaxylife.fandom.com/Special:Filepath/Icon_costs.png?width=18" class="ls-is-cached lazyloaded"></a><b>' + formattedNumber(cost) + '</b>.</li>';
list.insertAdjacentHTML('beforeend', listElementHTML);
}
}
generator.addEventListener('submit', submitHandler);
}
// Cost generator - this script has only one input and returns it's in-game
// cost. This is to quickly know the value of a single distance.
function costGeneratorInit() {
// Generate and add HTML to the DOM.
const generatorHTML = '\
<form class="cost-generator">\
<label for="generator-distance">Distance: </label>\
<input type="number" id="generator-distance" name="generator-distance" min="0" max="27777" required></input>\
<input type="submit" class="wds-button cost-generator-submit" value="Get cost"></input>\
</form>\
<p class="cost-generator-output">Cost: </p>';
costGeneratorContainer.insertAdjacentHTML('beforeend', generatorHTML);
const generator = document.querySelector('.cost-generator'),
input = document.getElementById('generator-distance'),
output = document.querySelector('.cost-generator-output');
function submitHandler(event) {
// Prevent page refresh upon clicking the submit button.
event.preventDefault();
// Generate output.
const distance = input.value
, cost = max(66000, ceil((2000 - 0.072 * distance) * distance * 31));
// Add output to the DOM.
output.innerHTML = 'Cost: <a href="/wiki/Resources" title="Resources"><img alt="Cost" src="https://galaxylife.fandom.com/Special:Filepath/Icon_costs.png" decoding="async" loading="lazy" width="18" height="14" data-image-name="Icon_costs.png" data-image-key="Icon_costs.png" data-src="https://galaxylife.fandom.com/Special:Filepath/Icon_costs.png?width=18" class="ls-is-cached lazyloaded"></a><b>' + formattedNumber(cost) + '</b>.';
}
generator.addEventListener('submit', submitHandler);
}
// Colonization price calculator - this script takes multiple arguments which
// are the player's X and Y coordinates of their planet along with the target
// planet's X and Y coordinates. If the player has colonies, they can also
// add them to take into account when determining the colonization cost, as it
// will make a list of all player's coordinates and select the ones closest to
// the target planet and return the colonization cost.
function costCalculatorInit() {
// Generate HTML and add it to the DOM. We also use a number as a variable
// as to not read the form's HTML every time we have to change it.
const calculatorHTML = '\
<fieldset class="colony-cost-calculator-container">\
<legend class="colony-cost-calculator-title">Colony cost calculator</legend>\
<form class="colony-cost-calculator">\
<div class="colony-cost-calculator-actions">\
<input type="button" class="wds-button wds-is-secondary colony-add" value="Add colony"></input>\
<input type="button" class="wds-button wds-is-secondary wds-is-disabled colony-remove" value="Remove colony"></input>\
<input type="submit" class="wds-button colony-coords-submit" value="Calculate"></input>\
</div>\
<label class="planet-label">Your main planet\'s coordinates<br>\
<input type="number" id="planet-X" name="planet-X" min="0" max="2000" placeholder="X" required>\
<input type="number" id="planet-Y" name="planet-Y" min="0" max="2000" placeholder="Y" required>\
</label>\
<label class="target-label">Target planet\'s coordinates<br>\
<input type="number" id="target-X" name="target-X" min="0" max="2000" placeholder="X" required>\
<input type="number" id="target-Y" name="target-Y" min="0" max="2000" placeholder="Y" required>\
</label>\
</form>\
<p class="colony-cost-output">Cost: <a href="/wiki/Resources" title="Resources"><img alt="Cost" src="https://galaxylife.fandom.com/Special:Filepath/Icon_costs.png" decoding="async" loading="lazy" width="18" height="14" data-image-name="Icon_costs.png" data-image-key="Icon_costs.png" data-src="https://galaxylife.fandom.com/Special:Filepath/Icon_costs.png?width=18" class="ls-is-cached lazyloaded"></a></p>\
</fieldset>';
var colonyNumber = 0;
costsCalculator.insertAdjacentHTML('afterend', calculatorHTML);
const form = document.querySelector('.colony-cost-calculator')
, colonyAddButton = form.querySelector('.colony-add')
, colonyRemoveButton = form.querySelector('.colony-remove');
// When clicking the 'add colony' button, add two extra input fields where the
// user can type the colony's X and Y coordinates.
function addColony() {
if (colonyNumber < 12) colonyNumber++;
// Disable 'add colony' button when there's 11 colonies in the calculator.
if (colonyNumber === 12) {
if (!form.querySelector('.colony-add').classList.contains('wds-is-disabled')) {
form.querySelector('.colony-add').classList.add('wds-is-disabled');
}
return;
}
// Add colony form HTML.
const targetLabel = form.querySelector('.target-label')
, colonyFormHTML = '\
<label class="colony-label" id="colony-' + colonyNumber + '-label">Colony ' + colonyNumber + '\'s coordinates<br>\
<input type="number" class="colony-x" id="colony-' + colonyNumber + '-X" name="target-' + colonyNumber + '-X" min="0" max="2000" placeholder="X" required>\
<input type="number" class="colony-y" id="colony-' + colonyNumber + '-Y" name="target-' + colonyNumber + '-Y" min="0" max="2000" placeholder="Y" required>\
</label>';
targetLabel.insertAdjacentHTML('beforebegin', colonyFormHTML);
// Re-enable the 'remove colony' button when a new colony has been added.
if (form.querySelector('.colony-remove').classList.contains('wds-is-disabled')) {
form.querySelector('.colony-remove').classList.remove('wds-is-disabled');
}
}
// Remove colonies when clicking the 'remove colony' button.
function removeColony() {
// The 'if' check makes sure that there are colonies form(s) in the fieldset.
// Otherwise this function (and, therefore, the button), won't do anything.
if (form.querySelector('.colony-label')) {
const colonies = form.querySelectorAll('.colony-label')
, lastColony = colonies[colonies.length - 1];
lastColony.remove();
if (colonyNumber > 0) colonyNumber--;
// Disable 'remove colony' button when no colony forms are present.
if (colonyNumber === 0) {
form.querySelector('.colony-remove').classList.add('wds-is-disabled');
}
// Re-enable 'add colony' button upon removing a colony form.
if (colonyNumber < 11) {
form.querySelector('.colony-add').classList.remove('wds-is-disabled');
}
}
}
function submitHandler(event) {
// Prevent page refresh upon clicking the submit button.
event.preventDefault();
// Get values from input fields and return the coloniztion cost.
const playerX = document.getElementById('planet-X').value
, playerY = document.getElementById('planet-Y').value
, targetX = document.getElementById('target-X').value
, targetY = document.getElementById('target-Y').value
, output = document.querySelector('.colony-cost-output')
, colonies = document.querySelector('.colony-label');
var distance = floor(sqrt(pow(playerX - targetX, 2) + pow(playerY - targetY, 2)));
// If there are colonies in the fieldset, add some extra steps to take into
// account these too. Otherwise proceed normally returning the cost of only
// the first and it's target planet.
if (colonies) calculateCostWithColonies();
const cost = max(66000, ceil((2000 - 0.072 * distance) * distance * 31));
// Update target planet's distance and cost.
if (output.querySelector('.output-resources') || output.querySelector('.output-distance')) {
output.querySelector('.output-resources').innerText = formattedNumber(cost);
output.querySelector('.output-distance').innerText = formattedNumber(distance);
} else {
output.insertAdjacentHTML('beforeend', '<b class="output-resources">' + formattedNumber(cost) + '</b>. Distance: <b class="output-distance">' + formattedNumber(distance) + '</b>.');
}
// Function executed only when extra colonies are present in the fieldset.
// This function makes a list of the player's coordinates (planets and colonies
// alike) and uses the distance of the closest colony to the target planet to
// get the final distance and cost.
function calculateCostWithColonies() {
const colonies = document.querySelectorAll('.colony-label')
, playerCoords = [[playerX, playerY]]
, distances = [];
colonies.forEach(function(colony) {
const x = colony.querySelector('.colony-x').value
, y = colony.querySelector('.colony-y').value;
playerCoords.push([x, y]);
});
playerCoords.forEach(function(coord) {
const distance = floor(sqrt(pow(coord[0] - targetX, 2) + pow(coord[1] - targetY, 2)));
distances.push(distance);
});
distance = min.apply(Math, distances);
}
}
colonyAddButton.addEventListener('click', addColony);
colonyRemoveButton.addEventListener('click', removeColony);
form.addEventListener('submit', submitHandler);
}
// Add commas to numbers with more than 4 digits where it corresponds.
// Code from Stack Overflow: https://stackoverflow.com/a/2901298/20503138
function formattedNumber(x) {
return x.toString().replace(/\B(?=(\d{3})+(?!\d))/g, ',');
}
}
function createModal(id, title, content) {
const modalId = 'modal-' + id;
if (!modals[modalId]) {
modals[modalId] = new window.dev.modal.Modal({
content: content,
id: modalId,
size: 'large',
title: title
});
}
modals[modalId].create();
}
function dragToScroll(target) {
const container = target;
var startY, startX, scrollLeft, scrollTop, isDown;
container.addEventListener('mousedown', function(e) { mouseIsDown(e) });
container.addEventListener('mouseup', function(e) { mouseUp(e) });
container.addEventListener('mouseleave', function(e) { mouseLeave(e) });
container.addEventListener('mousemove', function(e) { mouseMove(e) });
function mouseIsDown(e) {
isDown = true;
startY = e.pageY - container.offsetTop;
startX = e.pageX - container.offsetLeft;
scrollLeft = container.scrollLeft;
scrollTop = container.scrollTop;
}
function mouseUp(e) {
isDown = false;
}
function mouseLeave(e) {
isDown = false;
}
function mouseMove(e) {
if (isDown) {
e.preventDefault();
// Move vertcally.
const y = e.pageY - container.offsetTop;
const walkY = y - startY;
container.scrollTop = scrollTop - walkY;
// Move Horizontally.
const x = e.pageX - container.offsetLeft;
const walkX = x - startX;
container.scrollLeft = scrollLeft - walkX;
}
}
}
function missionsModalsInit() {
const missionChart = document.querySelector('#missions-chart > .overflow-wrapper');
const missionIcons = document.querySelectorAll('.mission-icon');
if (!missionIcons) return;
missionIcons.forEach(function(missionIcon) {
const id = missionIcon.getAttribute('id'),
link = missionIcon.querySelector('a'),
content = missionIcon.parentElement.querySelector('.modal-contents'),
title = content.querySelector('.pi-title').innerText;
if (!id || !link || !content || !title) return;
link.removeAttribute('href');
createModal(id, title, content);
missionIcon.addEventListener('click', function(event) {
modals['modal-' + id].show();
});
});
dragToScroll(missionChart);
}
mw.hook('wikipage.content').add(function() {
const pageName = mw.config.get('wgPageName');
moveHistoryToDropdown();
tableOverflowInit();
animateOnView();
newsInit();
if (pageName === 'Colonies') colonyCostsInit();
mw.loader.load( 'https://galaxylife.wiki.gg/index.php?title=MediaWiki:Tooltips.js&action=raw&ctype=text/javascript' );
mw.loader.load( 'https://galaxylife.wiki.gg/index.php?title=MediaWiki:Modal.js&action=raw&ctype=text/javascript' );
mw.hook('dev.modal').add(function(modal) {
missionsModalsInit();
});
});