MediaWiki:Common.js: Difference between revisions
Jump to navigation
Jump to search
No edit summary Tags: Manual revert Reverted |
No edit summary |
||
| (6 intermediate revisions by the same user not shown) | |||
| Line 809: | Line 809: | ||
}() ); | }() ); | ||
/* | /*-------------------------------------------------------------------------------------------------------------Link Processor------------------------------------------------------------*/ | ||
- | |||
- | |||
- | |||
- | |||
| Line 1,098: | Line 988: | ||
}() ); | }() ); | ||
(function () { | |||
'use strict'; | |||
const CONFIG = { | |||
SELECTORS: { | |||
COLLAPSIBLE_HEADER: '.collapsible-header', | |||
COLLAPSIBLE_BUTTON: '.collapsible' | |||
}, | |||
CLASSES: { | |||
ACTIVE: 'active', | |||
EXPANDED: 'expanded' | |||
} | |||
}; | |||
function safeQuerySelectorAll(selector, context = document) { | |||
try { | |||
return context.querySelectorAll(selector); | |||
} catch (error) { | |||
console.warn(`Invalid selector '${selector}':`, error); | |||
return []; | |||
} | |||
} | |||
const CollapsibleSections = { | |||
initCollapsibleHeaders: function () { | |||
const headers = safeQuerySelectorAll(CONFIG.SELECTORS.COLLAPSIBLE_HEADER); | |||
headers.forEach(header => { | |||
header.addEventListener('click', (event) => { | |||
event.preventDefault(); | |||
const section = header.parentElement; | |||
const content = section.querySelector('.collapsible-content'); | |||
if (content) { | |||
const isExpanded = section.classList.contains(CONFIG.CLASSES.EXPANDED); | |||
if (isExpanded) { | |||
section.classList.remove(CONFIG.CLASSES.EXPANDED); | |||
content.style.maxHeight = '0'; | |||
} else { | |||
section.classList.add(CONFIG.CLASSES.EXPANDED); | |||
content.style.maxHeight = content.scrollHeight + 'px'; | |||
} | |||
} | |||
}); | |||
}); | |||
}, | |||
initCollapsibleButtons: function () { | |||
const buttons = safeQuerySelectorAll(CONFIG.SELECTORS.COLLAPSIBLE_BUTTON); | |||
buttons.forEach(button => { | |||
button.addEventListener('click', (event) => { | |||
event.preventDefault(); | |||
const content = button.nextElementSibling; | |||
if (content && content.classList.contains('collapsible-content')) { | |||
const isActive = button.classList.contains(CONFIG.CLASSES.ACTIVE); | |||
if (isActive) { | |||
button.classList.remove(CONFIG.CLASSES.ACTIVE); | |||
content.style.maxHeight = '0'; | |||
} else { | |||
button.classList.add(CONFIG.CLASSES.ACTIVE); | |||
content.style.maxHeight = content.scrollHeight + 'px'; | |||
} | |||
} | |||
}); | |||
}); | |||
}, | |||
init: function () { | |||
this.initCollapsibleHeaders(); | |||
this.initCollapsibleButtons(); | |||
} | |||
}; | |||
if (document.readyState === 'loading') { | |||
document.addEventListener('DOMContentLoaded', CollapsibleSections.init.bind(CollapsibleSections)); | |||
} else { | |||
CollapsibleSections.init(); | |||
} | |||
})(); | |||
/* | |||
Propósito da funcionalidade: | |||
- Oferecer um sistema de recomendação de “Guardian type” baseado em opções selecionadas pelo usuário. | |||
- Controlar accordion de seções (`.guardian-accordion`) e permitir abrir diretamente por nível de dificuldade (1–5). | |||
- Calcular a recomendação pela contagem de votos dos `.decision-options .option.selected`, resolvendo empates escolhendo o tipo mais “difícil”. | |||
- Atualizar o texto em `#recommendedType` e destacar visualmente o tipo recomendado com a classe `recommended`. | |||
- Expor funções globais para uso em HTML/conteúdo do wiki: | |||
- `window.updateGuardianRecommendation()` | |||
- `window.showGuardianByDifficulty(level)` | |||
*/ | |||
(function () { | |||
'use strict'; | |||
const CONFIG = { | |||
SELECTORS: { | |||
GUARDIAN_ACCORDION: '.guardian-accordion' | |||
}, | |||
CLASSES: { | |||
ACTIVE: 'active', | |||
RECOMMENDED: 'recommended' | |||
}, | |||
DELAYS: { | |||
ACCORDION_SCROLL_DELAY: 300 | |||
} | |||
}; | |||
function safeGetElementById(id) { | |||
try { | |||
return document.getElementById(id); | |||
} catch (error) { | |||
console.warn(`Element with ID '${id}' not found:`, error); | |||
return null; | |||
} | |||
} | |||
function safeQuerySelectorAll(selector, context = document) { | |||
try { | |||
return context.querySelectorAll(selector); | |||
} catch (error) { | |||
console.warn(`Invalid selector '${selector}':`, error); | |||
return []; | |||
} | |||
} | |||
function safeQuerySelector(selector, context = document) { | |||
try { | |||
return context.querySelector(selector); | |||
} catch (error) { | |||
console.warn(`Invalid selector '${selector}':`, error); | |||
return null; | |||
} | |||
} | |||
function isElementInViewport(element) { | |||
if (!element) return false; | |||
const rect = element.getBoundingClientRect(); | |||
return ( | |||
rect.top >= 0 && | |||
rect.left >= 0 && | |||
rect.bottom <= (window.innerHeight || document.documentElement.clientHeight) && | |||
rect.right <= (window.innerWidth || document.documentElement.clientWidth) | |||
); | |||
} | |||
const GuardianDecisionHelper = { | |||
DIFFICULTY_MAP: { | |||
'1': 'regular', | |||
'2': 'mighty', | |||
'3': 'legendary', | |||
'4': 'superior', | |||
'5': 'accomplished' | |||
}, | |||
TYPE_DESCRIPTIONS: { | |||
'regular': 'Perfect for beginners with limited time', | |||
'mighty': 'Good balance of effort and power', | |||
'legendary': 'Excellent for endgame content', | |||
'superior': 'For dedicated players seeking optimization', | |||
'accomplished': 'For true perfectionists' | |||
}, | |||
initAccordion: function () { | |||
const accordions = safeQuerySelectorAll(CONFIG.SELECTORS.GUARDIAN_ACCORDION); | |||
accordions.forEach(accordion => { | |||
accordion.addEventListener('click', function () { | |||
this.classList.toggle(CONFIG.CLASSES.ACTIVE); | |||
const panel = this.nextElementSibling; | |||
if (panel) { | |||
if (panel.style.maxHeight) { | |||
panel.style.maxHeight = null; | |||
} else { | |||
panel.style.maxHeight = panel.scrollHeight + 'px'; | |||
} | |||
} | |||
}); | |||
}); | |||
}, | |||
showGuardianByDifficulty: function (level) { | |||
const accordions = safeQuerySelectorAll(CONFIG.SELECTORS.GUARDIAN_ACCORDION); | |||
accordions.forEach(accordion => { | |||
accordion.classList.remove(CONFIG.CLASSES.ACTIVE); | |||
const panel = accordion.nextElementSibling; | |||
if (panel) { | |||
panel.style.maxHeight = null; | |||
} | |||
}); | |||
const guardianType = this.DIFFICULTY_MAP[level]; | |||
if (guardianType) { | |||
const targetAccordion = safeQuerySelector(`.${guardianType}-accordion`); | |||
if (targetAccordion) { | |||
targetAccordion.classList.add(CONFIG.CLASSES.ACTIVE); | |||
const panel = targetAccordion.nextElementSibling; | |||
if (panel) { | |||
panel.style.maxHeight = panel.scrollHeight + 'px'; | |||
} | |||
setTimeout(() => { | |||
targetAccordion.scrollIntoView({ | |||
behavior: 'smooth', | |||
block: 'center' | |||
}); | |||
}, CONFIG.DELAYS.ACCORDION_SCROLL_DELAY); | |||
} | |||
} | |||
}, | |||
initDecisionHelper: function () { | |||
const options = safeQuerySelectorAll('.decision-options .option'); | |||
options.forEach(option => { | |||
option.addEventListener('click', function () { | |||
const siblings = this.parentElement.querySelectorAll('.option'); | |||
siblings.forEach(sibling => sibling.classList.remove('selected')); | |||
this.classList.add('selected'); | |||
GuardianDecisionHelper.updateRecommendation(); | |||
}); | |||
}); | |||
}, | |||
updateRecommendation: function () { | |||
const selectedOptions = safeQuerySelectorAll('.decision-options .option.selected'); | |||
const recommendedTypeElement = safeGetElementById('recommendedType'); | |||
if (!recommendedTypeElement) return; | |||
if (selectedOptions.length === 0) { | |||
recommendedTypeElement.textContent = 'Select options above'; | |||
return; | |||
} | |||
const votes = { | |||
'regular': 0, | |||
'mighty': 0, | |||
'legendary': 0, | |||
'superior': 0, | |||
'accomplished': 0 | |||
}; | |||
selectedOptions.forEach(option => { | |||
const types = option.getAttribute('data-type'); | |||
if (types) { | |||
types.split(',').forEach(type => { | |||
if (votes.hasOwnProperty(type.trim())) { | |||
votes[type.trim()]++; | |||
} | |||
}); | |||
} | |||
}); | |||
let maxVotes = 0; | |||
let recommendedType = ''; | |||
let tiedTypes = []; | |||
for (const type in votes) { | |||
if (votes[type] > maxVotes) { | |||
maxVotes = votes[type]; | |||
recommendedType = type; | |||
tiedTypes = [type]; | |||
} else if (votes[type] === maxVotes && maxVotes > 0) { | |||
tiedTypes.push(type); | |||
} | |||
} | |||
if (tiedTypes.length > 1) { | |||
const difficultyOrder = ['regular', 'mighty', 'legendary', 'superior', 'accomplished']; | |||
let highestDifficultyIndex = -1; | |||
tiedTypes.forEach(type => { | |||
const typeIndex = difficultyOrder.indexOf(type); | |||
if (typeIndex > highestDifficultyIndex) { | |||
highestDifficultyIndex = typeIndex; | |||
recommendedType = type; | |||
} | |||
}); | |||
} | |||
const formattedType = recommendedType.charAt(0).toUpperCase() + recommendedType.slice(1) + ' Guardian'; | |||
const description = this.TYPE_DESCRIPTIONS[recommendedType] || ''; | |||
const recommendationText = `${formattedType} - ${description}`; | |||
recommendedTypeElement.textContent = recommendationText; | |||
this.highlightRecommendedType(recommendedType); | |||
}, | |||
highlightRecommendedType: function (type) { | |||
const accordions = safeQuerySelectorAll(CONFIG.SELECTORS.GUARDIAN_ACCORDION); | |||
accordions.forEach(accordion => { | |||
accordion.classList.remove(CONFIG.CLASSES.RECOMMENDED); | |||
}); | |||
const recommendedAccordion = safeQuerySelector(`.${type}-accordion`); | |||
if (recommendedAccordion) { | |||
recommendedAccordion.classList.add(CONFIG.CLASSES.RECOMMENDED); | |||
if (!isElementInViewport(recommendedAccordion)) { | |||
recommendedAccordion.scrollIntoView({ | |||
behavior: 'smooth', | |||
block: 'center' | |||
}); | |||
} | |||
} | |||
}, | |||
init: function () { | |||
const hasGuardianElements = | |||
safeQuerySelector(CONFIG.SELECTORS.GUARDIAN_ACCORDION) || | |||
safeQuerySelector('.decision-options'); | |||
if (hasGuardianElements) { | |||
this.initAccordion(); | |||
this.initDecisionHelper(); | |||
window.updateGuardianRecommendation = this.updateRecommendation.bind(this); | |||
window.showGuardianByDifficulty = this.showGuardianByDifficulty.bind(this); | |||
} | |||
} | |||
}; | |||
if (document.readyState === 'loading') { | |||
document.addEventListener('DOMContentLoaded', GuardianDecisionHelper.init.bind(GuardianDecisionHelper)); | |||
} else { | |||
GuardianDecisionHelper.init(); | |||
} | |||
})(); | |||
/* | |||
Propósito da funcionalidade: | |||
- Converter marcação de links no estilo MediaWiki escrita como texto (ex.: `[[Titulo]]` ou `[[Titulo|Texto]]`) | |||
em links HTML reais (`<a href="...">`). | |||
- Ignorar contextos onde essa conversão não deve acontecer (scripts, editores, formulários, etc.). | |||
- Preservar links `File:`/`Image:` como texto, sem conversão. | |||
- Gerar URLs usando `mw.util.getUrl()` quando disponível, com fallback para `index.php?title=...`. | |||
*/ | |||
(function () { | |||
'use strict'; | |||
function isInExcludedContext(node) { | |||
if (!node || !node.parentElement) return false; | |||
const excludedSelectors = [ | |||
'script', 'style', 'textarea', 'input', 'select', 'option', | |||
'.ve-ui-surface', '.mw-editform', '.CodeMirror', '.cm-editor', '.ace_editor' | |||
].join(', '); | |||
return !!node.parentElement.closest(excludedSelectors); | |||
} | |||
const WikiLinkProcessor = { | |||
processWikiLinks: function () { | |||
if (!document.body || document.body.textContent.indexOf('[[') === -1) { | |||
return; | |||
} | |||
const walker = document.createTreeWalker( | |||
document.body, | |||
NodeFilter.SHOW_TEXT, | |||
null, | |||
false | |||
); | |||
const textNodes = []; | |||
let node; | |||
while ((node = walker.nextNode())) { | |||
const value = node.nodeValue; | |||
if (!value || value.indexOf('[[') === -1 || isInExcludedContext(node)) { | |||
continue; | |||
} | |||
textNodes.push(node); | |||
} | |||
const linkRegex = /\[\[([^\|\]]+)(?:\|([^\]]+))?\]\]/g; | |||
textNodes.forEach(textNode => { | |||
const sourceText = textNode.nodeValue; | |||
if (!linkRegex.test(sourceText)) return; | |||
linkRegex.lastIndex = 0; | |||
const parent = textNode.parentNode; | |||
const fragment = document.createDocumentFragment(); | |||
let lastIndex = 0; | |||
let match; | |||
while ((match = linkRegex.exec(sourceText)) !== null) { | |||
if (match.index > lastIndex) { | |||
fragment.appendChild( | |||
document.createTextNode(sourceText.slice(lastIndex, match.index)) | |||
); | |||
} | |||
const rawTitle = (match[1] || '').trim(); | |||
const displayText = (match[2] != null ? match[2] : rawTitle).trim(); | |||
if (/^(File:|Image:)/i.test(rawTitle)) { | |||
// Handle File/Image links | |||
const cleanFilename = rawTitle.replace(/^(File:|Image:)/i, '').trim(); | |||
const parts = displayText.split('|'); | |||
let caption = ''; | |||
let isThumb = false; | |||
// Basic parser for options (thumb, caption) | |||
// Assumes the last part is the caption if it's not a keyword | |||
// This is a simplified parser compared to full MediaWiki | |||
parts.forEach((part, index) => { | |||
const p = part.trim().toLowerCase(); | |||
if (p === 'thumb' || p === 'thumbnail') { | |||
isThumb = true; | |||
} else if (['left', 'right', 'center', 'none', 'frame', 'frameless', 'border'].includes(p)) { | |||
// Alignment/Frame options - currently ignored or could be added as classes | |||
} else if (p.match(/^\d+px$/)) { | |||
// Size option - currently ignored | |||
} else { | |||
// Assume it's caption (usually the last one, but we take the last non-keyword) | |||
caption = part.trim(); | |||
} | |||
}); | |||
// Use Special:FilePath to get the image source | |||
let src; | |||
if (window.mw && window.mw.util && typeof window.mw.util.getUrl === 'function') { | |||
src = window.mw.util.getUrl('Special:FilePath/' + cleanFilename); | |||
} else { | |||
// Fallback for local development or external usage | |||
// Use absolute URL to avoid local file:// or relative path issues which cause timeouts/delays | |||
src = 'https://mewsie.world/CoraTOWiki/index.php?title=Special:FilePath/' + encodeURIComponent(cleanFilename); | |||
} | |||
if (isThumb) { | |||
// Create structure: <div class="image-container"><img ...><div class="image-caption">...</div></div> | |||
const container = document.createElement('div'); | |||
container.className = 'image-container'; | |||
const img = document.createElement('img'); | |||
img.setAttribute('src', src); | |||
img.setAttribute('alt', caption || cleanFilename); | |||
img.setAttribute('loading', 'lazy'); // Optimize loading | |||
img.setAttribute('decoding', 'async'); // Optimize decoding | |||
container.appendChild(img); | |||
if (caption) { | |||
const capDiv = document.createElement('div'); | |||
capDiv.className = 'image-caption'; | |||
capDiv.textContent = caption; | |||
container.appendChild(capDiv); | |||
} | |||
fragment.appendChild(container); | |||
} else { | |||
// Inline image or simple image | |||
const img = document.createElement('img'); | |||
img.setAttribute('src', src); | |||
img.setAttribute('alt', caption || cleanFilename); | |||
img.setAttribute('loading', 'lazy'); // Optimize loading | |||
img.setAttribute('decoding', 'async'); // Optimize decoding | |||
fragment.appendChild(img); | |||
} | |||
} else { | |||
const cleanTitle = rawTitle.charAt(0) === ':' ? rawTitle.slice(1) : rawTitle; | |||
const href = (window.mw && window.mw.util && typeof window.mw.util.getUrl === 'function') | |||
? window.mw.util.getUrl(cleanTitle) | |||
: ('index.php?title=' + encodeURIComponent(cleanTitle)); | |||
const anchor = document.createElement('a'); | |||
anchor.className = 'mw-link-internal'; | |||
anchor.setAttribute('href', href); | |||
anchor.appendChild(document.createTextNode(displayText)); | |||
fragment.appendChild(anchor); | |||
} | |||
lastIndex = linkRegex.lastIndex; | |||
} | |||
if (lastIndex < sourceText.length) { | |||
fragment.appendChild( | |||
document.createTextNode(sourceText.slice(lastIndex)) | |||
); | |||
} | |||
parent.insertBefore(fragment, textNode); | |||
parent.removeChild(textNode); | |||
}); | |||
}, | |||
init: function () { | |||
if (document.readyState === 'loading') { | |||
document.addEventListener('DOMContentLoaded', this.processWikiLinks); | |||
} else { | |||
this.processWikiLinks(); | |||
} | |||
} | |||
}; | |||
WikiLinkProcessor.init(); | |||
})(); | |||
Latest revision as of 03:14, 29 January 2026
/*
Propósito da funcionalidade:
- Criar e controlar um botão “voltar ao topo” que aparece após rolar a página.
- Exibir/ocultar com classes (`show` e `pulse`) e realizar rolagem suave até o topo ao clicar.
- Inserir automaticamente o botão no `document.body` e registrar listeners de `scroll` (passivo) e `click`.
*/
(function () {
'use strict';
const CONFIG = {
CLASSES: {
SHOW: 'show',
PULSE: 'pulse'
}
};
const BackToTop = {
button: null,
isVisible: false,
scrollThreshold: 300,
createButton: function () {
const button = document.createElement('button');
button.className = 'btt-button';
button.setAttribute('aria-label', 'Voltar ao topo');
button.setAttribute('title', 'Voltar ao topo');
document.body.appendChild(button);
return button;
},
showButton: function () {
if (!this.isVisible && this.button) {
this.button.classList.add(CONFIG.CLASSES.SHOW);
this.button.classList.add(CONFIG.CLASSES.PULSE);
this.isVisible = true;
setTimeout(() => {
if (this.button) {
this.button.classList.remove(CONFIG.CLASSES.PULSE);
}
}, 6000);
}
},
hideButton: function () {
if (this.isVisible && this.button) {
this.button.classList.remove(CONFIG.CLASSES.SHOW);
this.button.classList.remove(CONFIG.CLASSES.PULSE);
this.isVisible = false;
}
},
handleScroll: function () {
const scrollTop = window.pageYOffset || document.documentElement.scrollTop;
if (scrollTop > this.scrollThreshold) {
this.showButton();
} else {
this.hideButton();
}
},
handleClick: function () {
window.scrollTo({
top: 0,
behavior: 'smooth'
});
},
init: function () {
this.button = this.createButton();
const boundHandleScroll = this.handleScroll.bind(this);
const boundHandleClick = this.handleClick.bind(this);
window.addEventListener('scroll', boundHandleScroll, { passive: true });
this.button.addEventListener('click', boundHandleClick);
this.handleScroll();
console.log('Back to Top button initialized');
}
};
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', BackToTop.init.bind(BackToTop));
} else {
BackToTop.init();
}
})();
/*
Propósito da funcionalidade:
- Permitir navegação por clique em “cards” que tenham atributo `data-link`.
- Suportar três tipos de destino:
- Âncoras internas (ex.: `#secao`) com rolagem suave.
- URLs externas absolutas (`http://` ou `https://`).
- Títulos de páginas MediaWiki, resolvidos via `mw.util.getUrl()` quando disponível.
*/
(function () {
'use strict';
const CONFIG = {
SELECTORS: {
CARD_LINKS: '.card[data-link], .destaque-card[data-link]'
}
};
function safeQuerySelector(selector, context = document) {
try {
return context.querySelector(selector);
} catch (error) {
console.warn(`Invalid selector '${selector}':`, error);
return null;
}
}
const NavigationHandlers = {
handleCardClick: function (event) {
const card = event.target.closest(CONFIG.SELECTORS.CARD_LINKS);
if (!card) return;
const link = card.getAttribute('data-link');
if (!link) return;
if (link.charAt(0) === '#') {
event.preventDefault();
const anchorElement = safeQuerySelector(link);
if (anchorElement) {
anchorElement.scrollIntoView({ behavior: 'smooth', block: 'start' });
}
return;
}
if (/^https?:\/\//i.test(link)) {
window.location.href = link;
return;
}
const targetUrl = (window.mw && window.mw.util && typeof window.mw.util.getUrl === 'function')
? window.mw.util.getUrl(link)
: ('index.php?title=' + encodeURIComponent(link));
window.location.href = targetUrl;
},
init: function () {
document.addEventListener('click', this.handleCardClick, false);
}
};
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', NavigationHandlers.init.bind(NavigationHandlers));
} else {
NavigationHandlers.init();
}
})();
/*
Propósito da funcionalidade:
- Criar um menu “hamburger” para abrir/fechar a sidebar no skin `vector-legacy`.
- Alternar classes (`mobile-open`, `sidebar-open`, `active`) para controlar layout em mobile.
- Fechar a sidebar ao clicar fora do painel ou ao pressionar `Escape`.
*/
(function () {
'use strict';
const CONFIG = {
CLASSES: {
ACTIVE: 'active',
MOBILE_OPEN: 'mobile-open',
SIDEBAR_OPEN: 'sidebar-open'
}
};
function safeGetElementById(id) {
try {
return document.getElementById(id);
} catch (error) {
console.warn(`Element with ID '${id}' not found:`, error);
return null;
}
}
function safeQuerySelector(selector, context = document) {
try {
return context.querySelector(selector);
} catch (error) {
console.warn(`Invalid selector '${selector}':`, error);
return null;
}
}
const MobileInterface = {
initialized: false,
getPanel: function () {
return safeGetElementById('mw-panel');
},
isApplicable: function () {
return document.body.classList.contains('skin-vector-legacy') && !!this.getPanel();
},
createHamburgerMenu: function () {
if (this.initialized) return;
if (!this.isApplicable()) {
return;
}
if (document.querySelector('.mobile-hamburger-menu')) {
this.initialized = true;
return;
}
const panel = this.getPanel();
if (!panel) return;
const hamburger = document.createElement('button');
hamburger.className = 'mobile-hamburger-menu';
hamburger.setAttribute('aria-label', 'Toggle navigation menu');
hamburger.setAttribute('type', 'button');
hamburger.innerHTML = '<span></span><span></span><span></span>';
const content = safeGetElementById('content') || safeQuerySelector('.mw-body');
if (content && content.parentNode) {
content.parentNode.insertBefore(hamburger, content);
}
const toggleSidebar = () => {
panel.classList.toggle(CONFIG.CLASSES.MOBILE_OPEN);
hamburger.classList.toggle(CONFIG.CLASSES.ACTIVE);
document.body.classList.toggle(CONFIG.CLASSES.SIDEBAR_OPEN);
try {
const open = panel.classList.contains(CONFIG.CLASSES.MOBILE_OPEN);
document.dispatchEvent(new CustomEvent('cora:mobile-sidebar-toggle', { detail: { open } }));
} catch (e) {
document.dispatchEvent(new Event('cora:mobile-sidebar-toggle'));
}
};
hamburger.addEventListener('click', (event) => {
event.preventDefault();
event.stopPropagation();
toggleSidebar();
});
document.addEventListener('click', (event) => {
if (panel.classList.contains(CONFIG.CLASSES.MOBILE_OPEN) &&
!panel.contains(event.target) &&
!hamburger.contains(event.target)) {
panel.classList.remove(CONFIG.CLASSES.MOBILE_OPEN);
hamburger.classList.remove(CONFIG.CLASSES.ACTIVE);
document.body.classList.remove(CONFIG.CLASSES.SIDEBAR_OPEN);
}
});
document.addEventListener('keydown', (event) => {
if (event.key === 'Escape' && panel.classList.contains(CONFIG.CLASSES.MOBILE_OPEN)) {
panel.classList.remove(CONFIG.CLASSES.MOBILE_OPEN);
hamburger.classList.remove(CONFIG.CLASSES.ACTIVE);
document.body.classList.remove(CONFIG.CLASSES.SIDEBAR_OPEN);
}
});
this.initialized = true;
},
init: function () {
this.createHamburgerMenu();
}
};
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', MobileInterface.init.bind(MobileInterface));
} else {
MobileInterface.init();
}
})();
/*
Propósito da funcionalidade:
- Tornar os portais/menus laterais do skin `vector-legacy` colapsáveis.
- Colapsar automaticamente todas as seções do painel (exceto a primeira) ao iniciar.
- Persistir estado expandido/colapsado por seção via `localStorage` usando prefixo `sidebar-<id>`.
- Ajustar alturas (`maxHeight`) ao expandir/colapsar e recalcular em `resize`.
*/
(function () {
'use strict';
const CONFIG = {
CLASSES: {
COLLAPSED: 'collapsed'
},
STORAGE_KEYS: {
SIDEBAR_PREFIX: 'sidebar-'
}
};
function ensureSidebarStyles() {
if (document.getElementById('cora-sidebar-collapsible-styles')) return;
const style = document.createElement('style');
style.id = 'cora-sidebar-collapsible-styles';
style.textContent =
'body.skin-vector-legacy #mw-panel .vector-menu-content,body.skin-vector-legacy #mw-panel .portal .body{overflow:hidden;transition:max-height .4s ease,opacity .3s ease;opacity:1}' +
'body.skin-vector-legacy #mw-panel .vector-menu-portal.collapsed .vector-menu-content,body.skin-vector-legacy #mw-panel .portal.collapsed .body{max-height:0!important;opacity:0;padding:0}' +
'body.skin-vector-legacy #mw-panel .vector-menu-portal .vector-menu-heading,body.skin-vector-legacy #mw-panel .portal h3{cursor:pointer;user-select:none}';
document.head.appendChild(style);
}
function safeGetElementById(id) {
try {
return document.getElementById(id);
} catch (error) {
console.warn(`Element with ID '${id}' not found:`, error);
return null;
}
}
function safeQuerySelectorAll(selector, context = document) {
try {
return context.querySelectorAll(selector);
} catch (error) {
console.warn(`Invalid selector '${selector}':`, error);
return [];
}
}
function safeGetLocalStorage(key, defaultValue = '') {
try {
return localStorage.getItem(key) || defaultValue;
} catch (error) {
console.warn(`localStorage access failed for key '${key}':`, error);
return defaultValue;
}
}
function safeSetLocalStorage(key, value) {
try {
localStorage.setItem(key, value);
return true;
} catch (error) {
console.warn(`localStorage write failed for key '${key}':`, error);
return false;
}
}
const SidebarManager = {
portals: null,
getPortalId: function (portal) {
const heading = portal.querySelector('.vector-menu-heading, h3');
if (heading) {
return heading.textContent.trim().toLowerCase().replace(/\s+/g, '-');
}
return 'unknown-portal';
},
syncExpandedHeights: function () {
if (!this.portals) return;
this.portals.forEach(portal => {
if (portal.classList.contains(CONFIG.CLASSES.COLLAPSED)) return;
const content = portal.querySelector('.vector-menu-content, .body');
if (!content) return;
content.style.maxHeight = content.scrollHeight + 'px';
content.style.opacity = '1';
});
},
initSidebarCollapsible: function () {
if (!document.body.classList.contains('skin-vector-legacy')) {
return;
}
const panel = safeGetElementById('mw-panel');
if (!panel) return;
ensureSidebarStyles();
const portals = safeQuerySelectorAll('.vector-menu-portal, .portal', panel);
this.portals = Array.from(portals);
portals.forEach((portal, index) => {
if (index > 0) {
portal.classList.add(CONFIG.CLASSES.COLLAPSED);
const content = portal.querySelector('.vector-menu-content, .body');
if (content) {
content.style.maxHeight = '0';
content.style.opacity = '0';
}
}
});
const headings = safeQuerySelectorAll('.vector-menu-heading, .portal h3', panel);
headings.forEach(heading => {
heading.addEventListener('click', (event) => {
event.preventDefault();
event.stopPropagation();
const portal = heading.closest('.vector-menu-portal, .portal');
if (!portal) return;
const content = portal.querySelector('.vector-menu-content, .body');
if (!content) return;
const isCollapsed = portal.classList.contains(CONFIG.CLASSES.COLLAPSED);
const portalId = this.getPortalId(portal);
if (isCollapsed) {
portal.classList.remove(CONFIG.CLASSES.COLLAPSED);
content.style.maxHeight = content.scrollHeight + 'px';
content.style.opacity = '1';
safeSetLocalStorage(CONFIG.STORAGE_KEYS.SIDEBAR_PREFIX + portalId, 'expanded');
} else {
portal.classList.add(CONFIG.CLASSES.COLLAPSED);
content.style.maxHeight = '0';
content.style.opacity = '0';
safeSetLocalStorage(CONFIG.STORAGE_KEYS.SIDEBAR_PREFIX + portalId, 'collapsed');
}
});
});
portals.forEach((portal, index) => {
if (index === 0) return;
const savedState = safeGetLocalStorage(CONFIG.STORAGE_KEYS.SIDEBAR_PREFIX + this.getPortalId(portal));
const content = portal.querySelector('.vector-menu-content, .body');
if (savedState === 'expanded' && content) {
portal.classList.remove(CONFIG.CLASSES.COLLAPSED);
content.style.maxHeight = content.scrollHeight + 'px';
content.style.opacity = '1';
}
});
window.addEventListener('resize', () => {
this.syncExpandedHeights();
});
document.addEventListener('cora:mobile-sidebar-toggle', () => {
this.syncExpandedHeights();
});
},
init: function () {
this.initSidebarCollapsible();
}
};
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', SidebarManager.init.bind(SidebarManager));
} else {
SidebarManager.init();
}
})();
/*
Propósito da funcionalidade:
- Abrir imagens em um “lightbox” (overlay) ao clicar, permitindo ampliar e visualizar com foco.
- Fechar ao clicar no fundo do overlay ou ao pressionar `Escape`.
- Bloquear rolagem do body enquanto o overlay estiver aberto e restaurar ao fechar.
- Exibir legenda baseada em `.image-container .image-caption` ou no `alt` da imagem.
- Adaptar a cor do fundo e da legenda ao tema atual via atributo `data-theme` no `<html>`.
*/
(function () {
'use strict';
const ImageLightbox = {
overlay: null,
bodyOverflow: '',
init: function () {
document.addEventListener('click', this.handleDocumentClick.bind(this), false);
},
shouldIgnoreClick: function (target) {
if (!target || !target.closest) return false;
if (target.closest('a[href]')) return true;
if (target.closest('.nav-tabs, .nav-tab, .nav-tab-fgod, .nested-tabs, .nested-tab')) return true;
return false;
},
buildOverlay: function () {
const overlay = document.createElement('div');
overlay.className = 'image-lightbox-overlay';
const theme = document.documentElement.getAttribute('data-theme') || 'light';
const bg = theme === 'dark' ? 'rgba(0,0,0,0.85)' : 'rgba(0,0,0,0.7)';
overlay.style.position = 'fixed';
overlay.style.top = '0';
overlay.style.right = '0';
overlay.style.bottom = '0';
overlay.style.left = '0';
overlay.style.display = 'flex';
overlay.style.alignItems = 'center';
overlay.style.justifyContent = 'center';
overlay.style.background = bg;
overlay.style.zIndex = '9999';
overlay.style.padding = '2vw';
overlay.style.cursor = 'zoom-out';
overlay.setAttribute('role', 'dialog');
overlay.setAttribute('aria-modal', 'true');
overlay.addEventListener('click', (e) => { if (e.target === overlay) this.close(); });
document.addEventListener('keydown', this.handleKeydown);
return overlay;
},
handleKeydown: function (e) {
if (e.key === 'Escape') {
const self = ImageLightbox;
if (self.overlay) { self.close(); }
}
},
findImageFromTarget: function (target) {
const el = target.closest('img, .image-container, .image-grid img');
if (!el) return null;
if (el.tagName && el.tagName.toLowerCase() === 'img') return el;
const img = el.querySelector('img');
return img || null;
},
handleDocumentClick: function (e) {
if (this.overlay && this.overlay.contains(e.target)) return;
if (this.shouldIgnoreClick(e.target)) return;
const img = this.findImageFromTarget(e.target);
if (!img) return;
e.preventDefault();
e.stopPropagation();
this.open(img);
},
open: function (img) {
if (this.overlay) return;
this.overlay = this.buildOverlay();
this.bodyOverflow = document.body.style.overflow || '';
document.body.style.overflow = 'hidden';
const content = document.createElement('div');
content.style.position = 'relative';
content.style.maxWidth = '90vw';
content.style.maxHeight = '90vh';
const clone = document.createElement('img');
clone.src = img.currentSrc || img.src;
clone.alt = img.alt || '';
clone.style.maxWidth = '90vw';
clone.style.maxHeight = '90vh';
clone.style.borderRadius = '12px';
clone.style.boxShadow = '0 10px 30px rgba(0,0,0,0.25)';
clone.style.transform = 'scale(0.98)';
clone.style.opacity = '0';
clone.style.transition = 'transform 150ms ease, opacity 150ms ease';
clone.style.willChange = 'transform, opacity';
content.appendChild(clone);
const captionText = this.getCaptionText(img);
if (captionText) {
const caption = document.createElement('div');
caption.textContent = captionText;
caption.style.marginTop = '0.75rem';
caption.style.fontSize = '0.9rem';
caption.style.textAlign = 'center';
const theme = document.documentElement.getAttribute('data-theme') || 'light';
caption.style.color = theme === 'dark' ? '#a8c6e8' : '#666';
content.appendChild(caption);
}
this.overlay.appendChild(content);
document.body.appendChild(this.overlay);
requestAnimationFrame(() => {
clone.style.transform = 'scale(1.25)';
clone.style.opacity = '1';
});
},
getCaptionText: function (img) {
const container = img.closest('.image-container');
const capEl = container ? container.querySelector('.image-caption') : null;
if (capEl && capEl.textContent) {
return capEl.textContent.trim();
}
return img.alt ? img.alt.trim() : '';
},
close: function () {
if (!this.overlay) return;
document.removeEventListener('keydown', this.handleKeydown);
if (this.overlay.parentNode) this.overlay.parentNode.removeChild(this.overlay);
this.overlay = null;
document.body.style.overflow = this.bodyOverflow;
}
};
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', ImageLightbox.init.bind(ImageLightbox));
} else {
ImageLightbox.init();
}
})();
( function () {
'use strict';
const CONFIG = {
CLASSES: {
EXPANDED: 'expanded'
},
STORAGE_KEYS: {
USER_MENU_STATE: 'user-menu-state',
PERSONAL_MENU_STATE: 'personal-menu-state'
},
BODY_CLASSES: {
HAS_USER_MENU: 'has-user-menu-collapsible',
HAS_PERSONAL_MENU: 'has-personal-menu-collapsible'
}
};
/**
* @typedef {Object} CollapsibleMenuOptions
* @property {string} containerId
* @property {string} containerClass
* @property {string} headerClass
* @property {string} contentClass
* @property {string} listClass
* @property {string} headerText
* @property {string} stateStorageKey
* @property {string} bodyClass
* @property {string[]} itemIds
*/
/**
* @param {string} id
* @return {HTMLElement|null}
*/
function safeGetElementById( id ) {
try {
return document.getElementById( id );
} catch ( error ) {
return null;
}
}
/**
* @param {string} key
* @param {string} [defaultValue]
* @return {string}
*/
function safeGetLocalStorage( key, defaultValue = '' ) {
try {
return localStorage.getItem( key ) || defaultValue;
} catch ( error ) {
return defaultValue;
}
}
/**
* @param {string} key
* @param {string} value
* @return {boolean}
*/
function safeSetLocalStorage( key, value ) {
try {
localStorage.setItem( key, value );
return true;
} catch ( error ) {
return false;
}
}
/**
* @param {CollapsibleMenuOptions} options
* @return {HTMLElement|null}
*/
function buildCollapsibleMenu( options ) {
const existing = safeGetElementById( options.containerId );
if ( existing ) {
return existing;
}
const items = options.itemIds
.map( ( id ) => safeGetElementById( id ) )
.filter( Boolean );
if ( items.length === 0 ) {
return null;
}
const menuContainer = document.createElement( 'div' );
menuContainer.id = options.containerId;
menuContainer.className = options.containerClass;
const menuHeader = document.createElement( 'div' );
menuHeader.className = options.headerClass;
menuHeader.textContent = options.headerText;
menuHeader.addEventListener( 'click', () => {
menuContainer.classList.toggle( CONFIG.CLASSES.EXPANDED );
const state = menuContainer.classList.contains( CONFIG.CLASSES.EXPANDED ) ?
'expanded' :
'collapsed';
safeSetLocalStorage( options.stateStorageKey, state );
} );
const menuContent = document.createElement( 'div' );
menuContent.className = options.contentClass;
const menuList = document.createElement( 'ul' );
menuList.className = options.listClass;
items.forEach( ( item ) => {
if ( item && item.parentNode ) {
menuList.appendChild( item );
}
} );
menuContent.appendChild( menuList );
menuContainer.appendChild( menuHeader );
menuContainer.appendChild( menuContent );
document.body.appendChild( menuContainer );
document.body.classList.add( options.bodyClass );
const savedState = safeGetLocalStorage( options.stateStorageKey );
if ( savedState === 'expanded' ) {
menuContainer.classList.add( CONFIG.CLASSES.EXPANDED );
}
document.addEventListener( 'click', ( event ) => {
const target = event.target;
if (
menuContainer.classList.contains( CONFIG.CLASSES.EXPANDED ) &&
target instanceof Node &&
!menuContainer.contains( target )
) {
menuContainer.classList.remove( CONFIG.CLASSES.EXPANDED );
safeSetLocalStorage( options.stateStorageKey, 'collapsed' );
}
} );
menuContainer.addEventListener( 'mouseenter', () => {
menuContainer.classList.add( CONFIG.CLASSES.EXPANDED );
} );
menuContainer.addEventListener( 'mouseleave', () => {
menuContainer.classList.remove( CONFIG.CLASSES.EXPANDED );
safeSetLocalStorage( options.stateStorageKey, 'collapsed' );
} );
return menuContainer;
}
const UserMenuManager = {
initialized: false,
initUserMenuCollapsible: function () {
if ( this.initialized ) {
return;
}
if ( !document.body.classList.contains( 'skin-vector-legacy' ) ) {
return;
}
const personalTools = safeGetElementById( 'p-personal' );
if ( !personalTools ) {
return;
}
const isLoggedIn = !!safeGetElementById( 'pt-userpage' );
const isAnonymous = !isLoggedIn && !!safeGetElementById( 'pt-login' );
if ( isLoggedIn ) {
buildCollapsibleMenu( {
containerId: 'user-menu-collapsible',
containerClass: 'user-menu-container',
headerClass: 'user-menu-header',
contentClass: 'user-menu-content',
listClass: 'user-menu-list',
headerText: 'User',
stateStorageKey: CONFIG.STORAGE_KEYS.USER_MENU_STATE,
bodyClass: CONFIG.BODY_CLASSES.HAS_USER_MENU,
itemIds: [
'pt-userpage', 'pt-mytalk', 'pt-preferences',
'pt-watchlist', 'pt-mycontris', 'pt-logout'
]
} );
}
if ( isAnonymous ) {
buildCollapsibleMenu( {
containerId: 'personal-menu-collapsible',
containerClass: 'personal-menu-container',
headerClass: 'personal-menu-header',
contentClass: 'personal-menu-content',
listClass: 'personal-menu-list',
headerText: 'User',
stateStorageKey: CONFIG.STORAGE_KEYS.PERSONAL_MENU_STATE,
bodyClass: CONFIG.BODY_CLASSES.HAS_PERSONAL_MENU,
itemIds: [
'pt-login', 'pt-createaccount', 'pt-anonuserpage', 'pt-anontalk'
]
} );
}
this.initialized = true;
},
init: function () {
this.initUserMenuCollapsible();
}
};
if ( document.readyState === 'loading' ) {
document.addEventListener( 'DOMContentLoaded', UserMenuManager.init.bind( UserMenuManager ) );
} else {
UserMenuManager.init();
}
}() );
/*-------------------------------------------------------------------------------------------------------------Link Processor------------------------------------------------------------*/
importScript('MediaWiki:MeuScript.js');
( function () {
'use strict';
function ensureThemeStylesLoaded() {
try {
if ( !window.mw || !mw.loader || !mw.config ) {
return;
}
const skin = mw.config.get( 'skin' ) || 'vector';
const moduleName = 'themeloader.skins.' + skin + '.default';
if ( typeof mw.loader.getState === 'function' ) {
const state = mw.loader.getState( moduleName );
if ( state === null ) {
return;
}
if ( state === 'ready' || state === 'loading' ) {
return;
}
}
mw.loader.load( moduleName );
} catch ( e ) {
// Ignore error
}
}
function getSavedTheme() {
try {
return localStorage.getItem( 'mw-theme' ) || 'default';
} catch ( e ) {
return 'default';
}
}
// Define available themes matching christmas.css roots
const themes = [
{ name: 'Pink', id: 'default' },
{ name: 'Dark', id: 'dark-neutral' },
{ name: 'Christmas', id: 'christmas' },
{ name: 'Power Light', id: 'power-light' },
{ name: 'Magic Light', id: 'magic-light' },
{ name: 'Sense Light', id: 'sense-light' },
{ name: 'Charm Light', id: 'charm-light' },
{ name: 'Power Dark', id: 'power-dark' },
{ name: 'Charm Dark', id: 'charm-dark' },
{ name: 'Magic Dark', id: 'dark-lightBlue' },
{ name: 'Sense Dark', id: 'dark-purple' },
];
// Function to apply theme
function applyTheme( themeId ) {
ensureThemeStylesLoaded();
try {
localStorage.setItem( 'mw-theme', themeId );
} catch ( e ) {
// Ignore error
}
if ( themeId === 'default' ) {
document.documentElement.removeAttribute( 'data-theme' );
} else {
document.documentElement.setAttribute( 'data-theme', themeId );
}
}
// Initialize theme immediately
ensureThemeStylesLoaded();
applyTheme( getSavedTheme() );
// Wait for DOM
const domReady = function ( callback ) {
if ( document.readyState === 'loading' ) {
document.addEventListener( 'DOMContentLoaded', callback );
} else {
setTimeout( callback, 0 );
}
};
domReady( function () {
// Prevent duplicate injection
if ( document.getElementById( 'mw-theme-floating' ) ) {
return;
}
// Create Floating Container
// ID matches CSS: #mw-theme-floating
const container = document.createElement( 'div' );
container.id = 'mw-theme-floating';
// Create Inner Wrapper
// Class matches CSS: .themeMenu
const themeMenuDiv = document.createElement( 'div' );
themeMenuDiv.className = 'themeMenu';
// Create Toggle Button
// Class matches CSS: .themeMenu-toggle
const btn = document.createElement( 'button' );
btn.className = 'themeMenu-toggle';
btn.textContent = 'Appearence';
btn.title = 'Change appearence';
btn.type = 'button';
// Create Dropdown List
// Class matches CSS: .themeMenu-dropdown
const dropdown = document.createElement( 'ul' );
dropdown.className = 'themeMenu-dropdown';
// Populate Items
themes.forEach( function ( theme ) {
// Item Wrapper
// CSS doesn't strictly require this wrapper for floating,
// but theme-menu.css might use .themeMenu-itemWrap
const itemWrap = document.createElement( 'li' );
itemWrap.className = 'themeMenu-itemWrap';
const themeLink = document.createElement( 'a' );
themeLink.className = 'themeMenu-item';
themeLink.href = '#';
themeLink.textContent = theme.name;
themeLink.dataset.themeId = theme.id;
// Highlight current
const currentTheme = getSavedTheme();
if ( theme.id === currentTheme ) {
themeLink.classList.add( 'is-current' );
}
themeLink.addEventListener( 'click', function ( e ) {
e.preventDefault();
ensureThemeStylesLoaded();
applyTheme( theme.id );
// Update highlighting
dropdown.querySelectorAll( '.themeMenu-item' ).forEach( function ( link ) {
link.classList.remove( 'is-current' );
} );
themeLink.classList.add( 'is-current' );
// Close menu
themeMenuDiv.classList.remove( 'is-open' );
} );
itemWrap.appendChild( themeLink );
dropdown.appendChild( itemWrap );
} );
// Toggle Logic
// CSS uses .themeMenu.is-open .themeMenu-dropdown { display: block }
btn.addEventListener( 'click', function ( e ) {
e.preventDefault();
e.stopPropagation();
themeMenuDiv.classList.toggle( 'is-open' );
} );
// Click Outside Logic
document.addEventListener( 'click', function ( e ) {
if ( !themeMenuDiv.contains( e.target ) ) {
themeMenuDiv.classList.remove( 'is-open' );
}
} );
// Assemble
themeMenuDiv.appendChild( btn );
themeMenuDiv.appendChild( dropdown );
container.appendChild( themeMenuDiv );
// Inject into Body
document.body.appendChild( container );
} );
}() );
(function () {
'use strict';
const CONFIG = {
SELECTORS: {
COLLAPSIBLE_HEADER: '.collapsible-header',
COLLAPSIBLE_BUTTON: '.collapsible'
},
CLASSES: {
ACTIVE: 'active',
EXPANDED: 'expanded'
}
};
function safeQuerySelectorAll(selector, context = document) {
try {
return context.querySelectorAll(selector);
} catch (error) {
console.warn(`Invalid selector '${selector}':`, error);
return [];
}
}
const CollapsibleSections = {
initCollapsibleHeaders: function () {
const headers = safeQuerySelectorAll(CONFIG.SELECTORS.COLLAPSIBLE_HEADER);
headers.forEach(header => {
header.addEventListener('click', (event) => {
event.preventDefault();
const section = header.parentElement;
const content = section.querySelector('.collapsible-content');
if (content) {
const isExpanded = section.classList.contains(CONFIG.CLASSES.EXPANDED);
if (isExpanded) {
section.classList.remove(CONFIG.CLASSES.EXPANDED);
content.style.maxHeight = '0';
} else {
section.classList.add(CONFIG.CLASSES.EXPANDED);
content.style.maxHeight = content.scrollHeight + 'px';
}
}
});
});
},
initCollapsibleButtons: function () {
const buttons = safeQuerySelectorAll(CONFIG.SELECTORS.COLLAPSIBLE_BUTTON);
buttons.forEach(button => {
button.addEventListener('click', (event) => {
event.preventDefault();
const content = button.nextElementSibling;
if (content && content.classList.contains('collapsible-content')) {
const isActive = button.classList.contains(CONFIG.CLASSES.ACTIVE);
if (isActive) {
button.classList.remove(CONFIG.CLASSES.ACTIVE);
content.style.maxHeight = '0';
} else {
button.classList.add(CONFIG.CLASSES.ACTIVE);
content.style.maxHeight = content.scrollHeight + 'px';
}
}
});
});
},
init: function () {
this.initCollapsibleHeaders();
this.initCollapsibleButtons();
}
};
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', CollapsibleSections.init.bind(CollapsibleSections));
} else {
CollapsibleSections.init();
}
})();
/*
Propósito da funcionalidade:
- Oferecer um sistema de recomendação de “Guardian type” baseado em opções selecionadas pelo usuário.
- Controlar accordion de seções (`.guardian-accordion`) e permitir abrir diretamente por nível de dificuldade (1–5).
- Calcular a recomendação pela contagem de votos dos `.decision-options .option.selected`, resolvendo empates escolhendo o tipo mais “difícil”.
- Atualizar o texto em `#recommendedType` e destacar visualmente o tipo recomendado com a classe `recommended`.
- Expor funções globais para uso em HTML/conteúdo do wiki:
- `window.updateGuardianRecommendation()`
- `window.showGuardianByDifficulty(level)`
*/
(function () {
'use strict';
const CONFIG = {
SELECTORS: {
GUARDIAN_ACCORDION: '.guardian-accordion'
},
CLASSES: {
ACTIVE: 'active',
RECOMMENDED: 'recommended'
},
DELAYS: {
ACCORDION_SCROLL_DELAY: 300
}
};
function safeGetElementById(id) {
try {
return document.getElementById(id);
} catch (error) {
console.warn(`Element with ID '${id}' not found:`, error);
return null;
}
}
function safeQuerySelectorAll(selector, context = document) {
try {
return context.querySelectorAll(selector);
} catch (error) {
console.warn(`Invalid selector '${selector}':`, error);
return [];
}
}
function safeQuerySelector(selector, context = document) {
try {
return context.querySelector(selector);
} catch (error) {
console.warn(`Invalid selector '${selector}':`, error);
return null;
}
}
function isElementInViewport(element) {
if (!element) return false;
const rect = element.getBoundingClientRect();
return (
rect.top >= 0 &&
rect.left >= 0 &&
rect.bottom <= (window.innerHeight || document.documentElement.clientHeight) &&
rect.right <= (window.innerWidth || document.documentElement.clientWidth)
);
}
const GuardianDecisionHelper = {
DIFFICULTY_MAP: {
'1': 'regular',
'2': 'mighty',
'3': 'legendary',
'4': 'superior',
'5': 'accomplished'
},
TYPE_DESCRIPTIONS: {
'regular': 'Perfect for beginners with limited time',
'mighty': 'Good balance of effort and power',
'legendary': 'Excellent for endgame content',
'superior': 'For dedicated players seeking optimization',
'accomplished': 'For true perfectionists'
},
initAccordion: function () {
const accordions = safeQuerySelectorAll(CONFIG.SELECTORS.GUARDIAN_ACCORDION);
accordions.forEach(accordion => {
accordion.addEventListener('click', function () {
this.classList.toggle(CONFIG.CLASSES.ACTIVE);
const panel = this.nextElementSibling;
if (panel) {
if (panel.style.maxHeight) {
panel.style.maxHeight = null;
} else {
panel.style.maxHeight = panel.scrollHeight + 'px';
}
}
});
});
},
showGuardianByDifficulty: function (level) {
const accordions = safeQuerySelectorAll(CONFIG.SELECTORS.GUARDIAN_ACCORDION);
accordions.forEach(accordion => {
accordion.classList.remove(CONFIG.CLASSES.ACTIVE);
const panel = accordion.nextElementSibling;
if (panel) {
panel.style.maxHeight = null;
}
});
const guardianType = this.DIFFICULTY_MAP[level];
if (guardianType) {
const targetAccordion = safeQuerySelector(`.${guardianType}-accordion`);
if (targetAccordion) {
targetAccordion.classList.add(CONFIG.CLASSES.ACTIVE);
const panel = targetAccordion.nextElementSibling;
if (panel) {
panel.style.maxHeight = panel.scrollHeight + 'px';
}
setTimeout(() => {
targetAccordion.scrollIntoView({
behavior: 'smooth',
block: 'center'
});
}, CONFIG.DELAYS.ACCORDION_SCROLL_DELAY);
}
}
},
initDecisionHelper: function () {
const options = safeQuerySelectorAll('.decision-options .option');
options.forEach(option => {
option.addEventListener('click', function () {
const siblings = this.parentElement.querySelectorAll('.option');
siblings.forEach(sibling => sibling.classList.remove('selected'));
this.classList.add('selected');
GuardianDecisionHelper.updateRecommendation();
});
});
},
updateRecommendation: function () {
const selectedOptions = safeQuerySelectorAll('.decision-options .option.selected');
const recommendedTypeElement = safeGetElementById('recommendedType');
if (!recommendedTypeElement) return;
if (selectedOptions.length === 0) {
recommendedTypeElement.textContent = 'Select options above';
return;
}
const votes = {
'regular': 0,
'mighty': 0,
'legendary': 0,
'superior': 0,
'accomplished': 0
};
selectedOptions.forEach(option => {
const types = option.getAttribute('data-type');
if (types) {
types.split(',').forEach(type => {
if (votes.hasOwnProperty(type.trim())) {
votes[type.trim()]++;
}
});
}
});
let maxVotes = 0;
let recommendedType = '';
let tiedTypes = [];
for (const type in votes) {
if (votes[type] > maxVotes) {
maxVotes = votes[type];
recommendedType = type;
tiedTypes = [type];
} else if (votes[type] === maxVotes && maxVotes > 0) {
tiedTypes.push(type);
}
}
if (tiedTypes.length > 1) {
const difficultyOrder = ['regular', 'mighty', 'legendary', 'superior', 'accomplished'];
let highestDifficultyIndex = -1;
tiedTypes.forEach(type => {
const typeIndex = difficultyOrder.indexOf(type);
if (typeIndex > highestDifficultyIndex) {
highestDifficultyIndex = typeIndex;
recommendedType = type;
}
});
}
const formattedType = recommendedType.charAt(0).toUpperCase() + recommendedType.slice(1) + ' Guardian';
const description = this.TYPE_DESCRIPTIONS[recommendedType] || '';
const recommendationText = `${formattedType} - ${description}`;
recommendedTypeElement.textContent = recommendationText;
this.highlightRecommendedType(recommendedType);
},
highlightRecommendedType: function (type) {
const accordions = safeQuerySelectorAll(CONFIG.SELECTORS.GUARDIAN_ACCORDION);
accordions.forEach(accordion => {
accordion.classList.remove(CONFIG.CLASSES.RECOMMENDED);
});
const recommendedAccordion = safeQuerySelector(`.${type}-accordion`);
if (recommendedAccordion) {
recommendedAccordion.classList.add(CONFIG.CLASSES.RECOMMENDED);
if (!isElementInViewport(recommendedAccordion)) {
recommendedAccordion.scrollIntoView({
behavior: 'smooth',
block: 'center'
});
}
}
},
init: function () {
const hasGuardianElements =
safeQuerySelector(CONFIG.SELECTORS.GUARDIAN_ACCORDION) ||
safeQuerySelector('.decision-options');
if (hasGuardianElements) {
this.initAccordion();
this.initDecisionHelper();
window.updateGuardianRecommendation = this.updateRecommendation.bind(this);
window.showGuardianByDifficulty = this.showGuardianByDifficulty.bind(this);
}
}
};
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', GuardianDecisionHelper.init.bind(GuardianDecisionHelper));
} else {
GuardianDecisionHelper.init();
}
})();
/*
Propósito da funcionalidade:
- Converter marcação de links no estilo MediaWiki escrita como texto (ex.: `[[Titulo]]` ou `[[Titulo|Texto]]`)
em links HTML reais (`<a href="...">`).
- Ignorar contextos onde essa conversão não deve acontecer (scripts, editores, formulários, etc.).
- Preservar links `File:`/`Image:` como texto, sem conversão.
- Gerar URLs usando `mw.util.getUrl()` quando disponível, com fallback para `index.php?title=...`.
*/
(function () {
'use strict';
function isInExcludedContext(node) {
if (!node || !node.parentElement) return false;
const excludedSelectors = [
'script', 'style', 'textarea', 'input', 'select', 'option',
'.ve-ui-surface', '.mw-editform', '.CodeMirror', '.cm-editor', '.ace_editor'
].join(', ');
return !!node.parentElement.closest(excludedSelectors);
}
const WikiLinkProcessor = {
processWikiLinks: function () {
if (!document.body || document.body.textContent.indexOf('[[') === -1) {
return;
}
const walker = document.createTreeWalker(
document.body,
NodeFilter.SHOW_TEXT,
null,
false
);
const textNodes = [];
let node;
while ((node = walker.nextNode())) {
const value = node.nodeValue;
if (!value || value.indexOf('[[') === -1 || isInExcludedContext(node)) {
continue;
}
textNodes.push(node);
}
const linkRegex = /\[\[([^\|\]]+)(?:\|([^\]]+))?\]\]/g;
textNodes.forEach(textNode => {
const sourceText = textNode.nodeValue;
if (!linkRegex.test(sourceText)) return;
linkRegex.lastIndex = 0;
const parent = textNode.parentNode;
const fragment = document.createDocumentFragment();
let lastIndex = 0;
let match;
while ((match = linkRegex.exec(sourceText)) !== null) {
if (match.index > lastIndex) {
fragment.appendChild(
document.createTextNode(sourceText.slice(lastIndex, match.index))
);
}
const rawTitle = (match[1] || '').trim();
const displayText = (match[2] != null ? match[2] : rawTitle).trim();
if (/^(File:|Image:)/i.test(rawTitle)) {
// Handle File/Image links
const cleanFilename = rawTitle.replace(/^(File:|Image:)/i, '').trim();
const parts = displayText.split('|');
let caption = '';
let isThumb = false;
// Basic parser for options (thumb, caption)
// Assumes the last part is the caption if it's not a keyword
// This is a simplified parser compared to full MediaWiki
parts.forEach((part, index) => {
const p = part.trim().toLowerCase();
if (p === 'thumb' || p === 'thumbnail') {
isThumb = true;
} else if (['left', 'right', 'center', 'none', 'frame', 'frameless', 'border'].includes(p)) {
// Alignment/Frame options - currently ignored or could be added as classes
} else if (p.match(/^\d+px$/)) {
// Size option - currently ignored
} else {
// Assume it's caption (usually the last one, but we take the last non-keyword)
caption = part.trim();
}
});
// Use Special:FilePath to get the image source
let src;
if (window.mw && window.mw.util && typeof window.mw.util.getUrl === 'function') {
src = window.mw.util.getUrl('Special:FilePath/' + cleanFilename);
} else {
// Fallback for local development or external usage
// Use absolute URL to avoid local file:// or relative path issues which cause timeouts/delays
src = 'https://mewsie.world/CoraTOWiki/index.php?title=Special:FilePath/' + encodeURIComponent(cleanFilename);
}
if (isThumb) {
// Create structure: <div class="image-container"><img ...><div class="image-caption">...</div></div>
const container = document.createElement('div');
container.className = 'image-container';
const img = document.createElement('img');
img.setAttribute('src', src);
img.setAttribute('alt', caption || cleanFilename);
img.setAttribute('loading', 'lazy'); // Optimize loading
img.setAttribute('decoding', 'async'); // Optimize decoding
container.appendChild(img);
if (caption) {
const capDiv = document.createElement('div');
capDiv.className = 'image-caption';
capDiv.textContent = caption;
container.appendChild(capDiv);
}
fragment.appendChild(container);
} else {
// Inline image or simple image
const img = document.createElement('img');
img.setAttribute('src', src);
img.setAttribute('alt', caption || cleanFilename);
img.setAttribute('loading', 'lazy'); // Optimize loading
img.setAttribute('decoding', 'async'); // Optimize decoding
fragment.appendChild(img);
}
} else {
const cleanTitle = rawTitle.charAt(0) === ':' ? rawTitle.slice(1) : rawTitle;
const href = (window.mw && window.mw.util && typeof window.mw.util.getUrl === 'function')
? window.mw.util.getUrl(cleanTitle)
: ('index.php?title=' + encodeURIComponent(cleanTitle));
const anchor = document.createElement('a');
anchor.className = 'mw-link-internal';
anchor.setAttribute('href', href);
anchor.appendChild(document.createTextNode(displayText));
fragment.appendChild(anchor);
}
lastIndex = linkRegex.lastIndex;
}
if (lastIndex < sourceText.length) {
fragment.appendChild(
document.createTextNode(sourceText.slice(lastIndex))
);
}
parent.insertBefore(fragment, textNode);
parent.removeChild(textNode);
});
},
init: function () {
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', this.processWikiLinks);
} else {
this.processWikiLinks();
}
}
};
WikiLinkProcessor.init();
})();