MediaWiki:Common.js: Difference between revisions
Jump to navigation
Jump to search
No edit summary Tag: Manual revert |
No edit summary |
||
| Line 1: | Line 1: | ||
/ | /** | ||
(function(){ | * MediaWiki Common JavaScript Module - Enhanced Version | ||
* Unified version with Quest Tracker functionality integrated | |||
* Designed for MediaWiki site usage with Vector Legacy skin support | |||
* | |||
* @author NewCora Wiki Team | |||
* @version 3.0 | |||
* @license MIT | |||
*/ | |||
(function() { | |||
'use strict'; | 'use strict'; | ||
// | // ============================================================================ | ||
// CONFIGURATION & CONSTANTS | |||
// ============================================================================ | |||
/** | |||
* Configuration object for the module | |||
* @type {Object} | |||
*/ | |||
const CONFIG = { | |||
SELECTORS: { | |||
TAB_CONTENT: '.tab-content', | |||
NAV_TAB: '.nav-tab', | |||
CARD_LINKS: '.card[data-link], .destaque-card[data-link]', | |||
COLLAPSIBLE_HEADER: '.collapsible-header', | |||
COLLAPSIBLE_BUTTON: '.collapsible', | |||
GUARDIAN_ACCORDION: '.guardian-accordion', | |||
// Quest Tracker selectors | |||
QUEST_CHECKBOX: '.quest-checkbox', | |||
NESTED_TAB: '.nested-tab', | |||
NESTED_CONTENT: '.nested-content', | |||
// Back to Top selectors | |||
BTT_BUTTON: '.btt-button' | |||
}, | |||
CLASSES: { | |||
ACTIVE: 'active', | |||
EXPANDED: 'expanded', | |||
COLLAPSED: 'collapsed', | |||
MOBILE_OPEN: 'mobile-open', | |||
SIDEBAR_OPEN: 'sidebar-open', | |||
RECOMMENDED: 'recommended', | |||
// Back to Top classes | |||
SHOW: 'show', | |||
PULSE: 'pulse' | |||
}, | |||
STORAGE_KEYS: { | |||
SIDEBAR_PREFIX: 'sidebar-', | |||
USER_MENU_STATE: 'user-menu-state', | |||
QUEST_PROGRESS: 'shamanJiaQuestProgress' | |||
}, | |||
DELAYS: { | |||
SCROLL_DELAY: 100, | |||
ACCORDION_SCROLL_DELAY: 300 | |||
} | |||
}; | }; | ||
// | // ============================================================================ | ||
function | // UTILITY FUNCTIONS | ||
// ============================================================================ | |||
/** | |||
* Safely gets an element by ID with error handling | |||
* @param {string} id - The element ID | |||
* @returns {Element|null} The element or null if not found | |||
*/ | |||
function safeGetElementById(id) { | |||
try { | |||
return document.getElementById(id); | |||
} catch (error) { | |||
console.warn(`Element with ID '${id}' not found:`, error); | |||
return null; | |||
} | } | ||
} | } | ||
/ | /** | ||
* Safely queries for elements with error handling | |||
* @param {string} selector - CSS selector | |||
* @param {Element} [context=document] - Context element | |||
* @returns {NodeList} NodeList of matching elements | |||
*/ | |||
function safeQuerySelectorAll(selector, context = document) { | |||
try { | |||
return context.querySelectorAll(selector); | |||
} catch (error) { | |||
console.warn(`Invalid selector '${selector}':`, error); | |||
return []; | |||
} | |||
} | } | ||
} | } | ||
// | /** | ||
function | * Safely queries for a single element with error handling | ||
* @param {string} selector - CSS selector | |||
* @param {Element} [context=document] - Context element | |||
* @returns {Element|null} The first matching element or null | |||
*/ | |||
function safeQuerySelector(selector, context = document) { | |||
try { | |||
return context.querySelector(selector); | |||
} catch (error) { | |||
console.warn(`Invalid selector '${selector}':`, error); | |||
return null; | |||
} | |||
} | } | ||
/ | /** | ||
* Safely accesses localStorage with error handling | |||
* @param {string} key - Storage key | |||
* @param {string} [defaultValue=''] - Default value if key doesn't exist | |||
* @returns {string} The stored value or default | |||
*/ | |||
function safeGetLocalStorage(key, defaultValue = '') { | |||
try { | |||
return localStorage.getItem(key) || defaultValue; | |||
} catch (error) { | |||
console.warn(`localStorage access failed for key '${key}':`, error); | |||
return defaultValue; | |||
return; | |||
} | } | ||
} | |||
/** | |||
* Safely sets localStorage with error handling | |||
* @param {string} key - Storage key | |||
return; | * @param {string} value - Value to store | ||
* @returns {boolean} Success status | |||
*/ | |||
function safeSetLocalStorage(key, value) { | |||
try { | |||
localStorage.setItem(key, value); | |||
return true; | |||
} catch (error) { | |||
console.warn(`localStorage write failed for key '${key}':`, error); | |||
return false; | |||
} | } | ||
} | |||
/** | |||
* Checks if an element is in the viewport | |||
* @param {Element} element - Element to check | |||
* @returns {boolean} True if element is visible in viewport | |||
*/ | |||
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) | |||
); | |||
} | } | ||
/ | /** | ||
* Determines if a node is inside an excluded container | |||
* @param {Node} node - The node to check | |||
* @returns {boolean} True if node is in excluded context | |||
*/ | |||
function isInExcludedContext(node) { | function isInExcludedContext(node) { | ||
if (!node || !node.parentElement) return false; | if (!node || !node.parentElement) return false; | ||
return !! | const excludedSelectors = [ | ||
'script', 'style', 'textarea', 'input', 'select', 'option', | |||
'.ve-ui-surface', '.mw-editform', '.CodeMirror', '.cm-editor', '.ace_editor' | |||
].join(', '); | |||
return !!node.parentElement.closest(excludedSelectors); | |||
} | } | ||
// | // ============================================================================ | ||
// QUEST TRACKER MODULE (Integrated from ShamanGia) | |||
// ============================================================================ | |||
/** | |||
* Quest Tracker Module | |||
* Manages quest progress tracking with localStorage persistence | |||
*/ | |||
const QuestTracker = { | |||
); | /** | ||
* Initializes quest tracker functionality | |||
*/ | |||
init: function() { | |||
// Only initialize if quest tracker elements exist | |||
const questCheckboxes = safeQuerySelectorAll(CONFIG.SELECTORS.QUEST_CHECKBOX); | |||
if (questCheckboxes.length === 0) { | |||
return; | |||
} | |||
this.loadQuestProgress(); | |||
this.setupEventListeners(); | |||
this.updateProgressDisplay(); | |||
console.log('Quest Tracker initialized'); | |||
}, | |||
/** | |||
* Sets up event listeners for quest tracker | |||
*/ | |||
setupEventListeners: function() { | |||
const questCheckboxes = safeQuerySelectorAll(CONFIG.SELECTORS.QUEST_CHECKBOX); | |||
const resetButton = safeGetElementById('reset-progress'); | |||
// Add event listeners to checkboxes | |||
questCheckboxes.forEach(checkbox => { | |||
checkbox.addEventListener('change', () => { | |||
this.saveQuestProgress(); | |||
this.updateProgressDisplay(); | |||
}); | |||
}); | |||
// Add event listener to reset button | |||
if (resetButton) { | |||
resetButton.addEventListener('click', () => { | |||
if (confirm('Are you sure you want to reset all quest progress? This action cannot be undone.')) { | |||
this.resetAllProgress(); | |||
} | |||
}); | |||
} | |||
}, | |||
/** | |||
* Loads quest progress from localStorage | |||
*/ | |||
loadQuestProgress: function() { | |||
const savedProgress = safeGetLocalStorage(CONFIG.STORAGE_KEYS.QUEST_PROGRESS); | |||
if (savedProgress) { | |||
try { | |||
const progressData = JSON.parse(savedProgress); | |||
Object.keys(progressData).forEach(questId => { | |||
const checkbox = safeQuerySelector(`[data-quest="${questId}"]`); | |||
if (checkbox) { | |||
checkbox.checked = progressData[questId]; | |||
} | |||
}); | |||
} catch (error) { | |||
console.error('Error loading quest progress:', error); | |||
} | |||
} | |||
}, | |||
/** | |||
* Saves quest progress to localStorage | |||
*/ | |||
saveQuestProgress: function() { | |||
const questCheckboxes = safeQuerySelectorAll(CONFIG.SELECTORS.QUEST_CHECKBOX); | |||
const progressData = {}; | |||
questCheckboxes.forEach(checkbox => { | |||
const questId = checkbox.getAttribute('data-quest'); | |||
if (questId) { | |||
progressData[questId] = checkbox.checked; | |||
} | |||
}); | |||
try { | |||
safeSetLocalStorage(CONFIG.STORAGE_KEYS.QUEST_PROGRESS, JSON.stringify(progressData)); | |||
} catch (error) { | |||
console.error('Error saving quest progress:', error); | |||
} | |||
}, | |||
/** | |||
* Updates progress display elements | |||
*/ | |||
updateProgressDisplay: function() { | |||
const questCheckboxes = safeQuerySelectorAll(CONFIG.SELECTORS.QUEST_CHECKBOX); | |||
const totalQuests = questCheckboxes.length; | |||
let completedQuests = 0; | |||
questCheckboxes.forEach(checkbox => { | |||
if (checkbox.checked) { | |||
completedQuests++; | |||
} | |||
}); | |||
const completionPercentage = totalQuests > 0 ? Math.round((completedQuests / totalQuests) * 100) : 0; | |||
// Update progress display elements | |||
const totalProgressElement = safeGetElementById('total-progress'); | |||
const completionPercentageElement = safeGetElementById('completion-percentage'); | |||
const progressFillElement = safeGetElementById('progress-fill'); | |||
if (totalProgressElement) { | |||
totalProgressElement.textContent = `${completedQuests}/${totalQuests}`; | |||
} | |||
if (completionPercentageElement) { | |||
completionPercentageElement.textContent = `${completionPercentage}%`; | |||
} | |||
if (progressFillElement) { | |||
progressFillElement.style.width = `${completionPercentage}%`; | |||
} | |||
}, | |||
/** | |||
* Resets all quest progress | |||
*/ | |||
resetAllProgress: function() { | |||
const questCheckboxes = safeQuerySelectorAll(CONFIG.SELECTORS.QUEST_CHECKBOX); | |||
// Uncheck all checkboxes | |||
questCheckboxes.forEach(checkbox => { | |||
checkbox.checked = false; | |||
}); | |||
// Clear localStorage | |||
try { | |||
localStorage.removeItem(CONFIG.STORAGE_KEYS.QUEST_PROGRESS); | |||
} catch (error) { | |||
console.error('Error clearing quest progress:', error); | |||
} | |||
// Update progress display | |||
this.updateProgressDisplay(); | |||
} | } | ||
}; | |||
// ============================================================================ | |||
// TAB NAVIGATION SYSTEM (Enhanced with Quest Tracker support) | |||
// ============================================================================ | |||
/** | |||
* Tab Navigation Module | |||
if (! | * Handles tab switching functionality with smooth scrolling support | ||
* Enhanced to work with both MediaWiki tabs and Quest Tracker nested tabs | |||
*/ | |||
const TabNavigation = { | |||
/** | |||
* Shows a specific tab and hides others | |||
* @param {string} tabId - ID of the tab content to show | |||
* @param {Element} [tabElement] - The tab button element | |||
*/ | |||
showTab: function(tabId, tabElement) { | |||
if (!tabId) { | |||
console.warn('TabNavigation.showTab: tabId is required'); | |||
return; | |||
} | |||
// CRITICAL FIX: Hide ALL nested contents first when switching main tabs | |||
// This prevents nested content from other sections remaining visible | |||
const allNestedContents = document.querySelectorAll('.nested-content'); | |||
allNestedContents.forEach(content => { | |||
content.classList.remove(CONFIG.CLASSES.ACTIVE); | |||
}); | |||
// Hide all tab contents | |||
const contents = safeQuerySelectorAll(CONFIG.SELECTORS.TAB_CONTENT); | |||
contents.forEach(content => content.classList.remove(CONFIG.CLASSES.ACTIVE)); | |||
// Deactivate all tab buttons | |||
const tabs = safeQuerySelectorAll(CONFIG.SELECTORS.NAV_TAB); | |||
tabs.forEach(tab => tab.classList.remove(CONFIG.CLASSES.ACTIVE)); | |||
// Show target tab content | |||
const targetContent = safeGetElementById(tabId); | |||
if (targetContent) { | |||
targetContent.classList.add(CONFIG.CLASSES.ACTIVE); | |||
// If this is the Normal Daily tab, activate the first nested tab | |||
if (tabId === 'tab-normal-daily') { | |||
setTimeout(() => { | |||
const firstNestedTab = targetContent.querySelector('.nested-tab'); | |||
if (firstNestedTab) { | |||
const firstNestedTabId = firstNestedTab.getAttribute('data-tab'); | |||
if (firstNestedTabId) { | |||
TabNavigation.showNestedTab(firstNestedTabId, firstNestedTab); | |||
} | |||
} | |||
}, 50); | |||
} | } | ||
} else { | |||
console.warn(`Tab content with ID '${tabId}' not found`); | |||
} | |||
// Activate the clicked tab button | |||
if (tabElement) { | |||
tabElement.classList.add(CONFIG.CLASSES.ACTIVE); | |||
} | } | ||
}, | |||
/** | |||
if ( | * Shows a specific nested tab and hides others | ||
* @param {string} tabId - ID of the nested tab content to show | |||
* @param {Element} [tabElement] - The nested tab button element | |||
*/ | |||
showNestedTab: function(tabId, tabElement) { | |||
if (!tabId) { | |||
console.warn('TabNavigation.showNestedTab: tabId is required'); | |||
return; | |||
} | |||
// | // Find the parent tab content to scope the nested tab switching | ||
const parentTabContent = tabElement ? tabElement.closest('.tab-content') : null; | |||
const searchScope = parentTabContent || document; | |||
}); | |||
} | // CRITICAL FIX: Hide ALL nested contents in the entire document first | ||
// This ensures no content from other sections remains visible | |||
const allNestedContents = document.querySelectorAll('.nested-content'); | |||
allNestedContents.forEach(content => { | |||
content.classList.remove(CONFIG.CLASSES.ACTIVE); | |||
}); | |||
// Deactivate all nested tab buttons within the scope | |||
const nestedTabs = safeQuerySelectorAll('.nested-tab', searchScope); | |||
nestedTabs.forEach(tab => { | |||
tab.classList.remove(CONFIG.CLASSES.ACTIVE); | |||
}); | |||
// Show target nested tab content | |||
const targetContent = safeGetElementById(tabId); | |||
if (targetContent) { | |||
targetContent.classList.add(CONFIG.CLASSES.ACTIVE); | |||
} else { | |||
console.warn(`Nested tab content with ID '${tabId}' not found`); | |||
} | |||
// Activate the clicked nested tab button | |||
if (tabElement) { | |||
tabElement.classList.add(CONFIG.CLASSES.ACTIVE); | |||
} | |||
}, | |||
/** | |||
* Handles tab click events using event delegation | |||
* @param {Event} event - Click event | |||
*/ | |||
handleTabClick: function(event) { | |||
const tab = event.target.closest('.nav-tab[data-tab]'); | |||
if (!tab) return; | |||
event.preventDefault(); | |||
const targetId = tab.getAttribute('data-tab'); | |||
if (targetId) { | |||
TabNavigation.showTab(targetId, tab); | |||
} | |||
}, | |||
/** | |||
* Handles nested tab click events using event delegation | |||
* Enhanced to work with Quest Tracker nested tabs | |||
* @param {Event} event - Click event | |||
*/ | |||
handleNestedTabClick: function(event) { | |||
const nestedTab = event.target.closest('.nested-tab[data-tab]'); | |||
if (!nestedTab) return; | |||
event.preventDefault(); | |||
const targetId = nestedTab.getAttribute('data-tab'); | |||
if (targetId) { | |||
TabNavigation.showNestedTab(targetId, nestedTab); | |||
} | |||
}, | |||
/** | |||
* Handles special navigation clicks with scroll support | |||
* @param {Event} event - Click event | |||
*/ | |||
handleSpecialTabNavigation: function(event) { | |||
const trigger = event.target.closest('[data-tab-trigger]'); | |||
if (!trigger) return; | |||
event.preventDefault(); | |||
const tabData = trigger.getAttribute('data-tab-trigger'); | |||
const buttonId = trigger.getAttribute('data-tab-button'); | |||
const href = trigger.getAttribute('href'); | |||
const scrollToAttr = trigger.getAttribute('data-scroll-to'); | |||
// Determine scroll target | |||
let scrollTargetSelector = null; | |||
if (scrollToAttr && scrollToAttr.trim()) { | |||
scrollTargetSelector = '#' + scrollToAttr.replace(/^#/, ''); | |||
} else if (href && href.charAt(0) === '#') { | |||
scrollTargetSelector = href; | |||
} | |||
// Switch tab if specified | |||
if (tabData && buttonId) { | |||
const targetButton = safeGetElementById(buttonId); | |||
if (targetButton) { | |||
TabNavigation.showTab(tabData, targetButton); | |||
// Scroll to anchor after tab switch | |||
if (scrollTargetSelector) { | |||
const scrollTarget = safeQuerySelector(scrollTargetSelector); | |||
if (scrollTarget) { | |||
setTimeout(() => { | |||
scrollTarget.scrollIntoView({ behavior: 'smooth' }); | |||
}, CONFIG.DELAYS.SCROLL_DELAY); | |||
} | |||
} | |||
} | |||
} | |||
}, | |||
/** | |||
* Initializes tab navigation event listeners | |||
*/ | |||
init: function() { | |||
document.addEventListener('click', this.handleTabClick, false); | |||
document.addEventListener('click', this.handleNestedTabClick, false); | |||
document.addEventListener('click', this.handleSpecialTabNavigation, false); | |||
// Make showTab globally available for backward compatibility | |||
window.showTab = this.showTab; | |||
} | |||
}; | |||
// ============================================================================ | |||
// BACK TO TOP MODULE | |||
// ============================================================================ | |||
// | /** | ||
* Back to Top Module | |||
* Manages the back-to-top button functionality with smooth animations | |||
*/ | |||
const BackToTop = { | |||
button: null, | |||
isVisible: false, | |||
scrollThreshold: 300, | |||
/** | |||
* Creates the back-to-top button element | |||
*/ | |||
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; | ||
}, | |||
/** | |||
* Shows the back-to-top button with animation | |||
*/ | |||
showButton: function() { | |||
if (!this.isVisible && this.button) { | |||
this.button.classList.add(CONFIG.CLASSES.SHOW); | |||
this.button.classList.add(CONFIG.CLASSES.PULSE); | |||
this.isVisible = true; | |||
// Remove pulse class after animation | |||
setTimeout(() => { | |||
if (this.button) { | |||
this.button.classList.remove(CONFIG.CLASSES.PULSE); | |||
} | |||
}, 6000); | |||
} | |||
}, | |||
/** | |||
* Hides the back-to-top button with animation | |||
*/ | |||
hideButton: function() { | |||
if (this.isVisible && this.button) { | |||
this.button.classList.remove(CONFIG.CLASSES.SHOW); | |||
this.button.classList.remove(CONFIG.CLASSES.PULSE); | |||
this.isVisible = false; | |||
} | |||
}, | |||
/** | |||
* Handles scroll events to show/hide button | |||
*/ | |||
handleScroll: function() { | |||
const scrollTop = window.pageYOffset || document.documentElement.scrollTop; | |||
if (scrollTop > this.scrollThreshold) { | |||
this.showButton(); | |||
} else { | |||
this.hideButton(); | |||
} | |||
}, | |||
/** | |||
* Handles button click to scroll to top | |||
*/ | |||
handleClick: function() { | |||
window.scrollTo({ | |||
top: 0, | |||
behavior: 'smooth' | |||
}); | |||
}, | |||
/** | |||
* Initializes the back-to-top functionality | |||
*/ | |||
init: function() { | |||
// Create the button | |||
this.button = this.createButton(); | |||
// Bind event handlers | |||
const boundHandleScroll = this.handleScroll.bind(this); | |||
const boundHandleClick = this.handleClick.bind(this); | |||
// Add event listeners | |||
window.addEventListener('scroll', boundHandleScroll, { passive: true }); | |||
this.button.addEventListener('click', boundHandleClick); | |||
// Initial check | |||
this.handleScroll(); | |||
console.log('Back to Top button initialized'); | |||
} | } | ||
}; | |||
// | |||
function | // ============================================================================ | ||
// NAVIGATION & INTERACTION HANDLERS (Enhanced) | |||
// ============================================================================ | |||
/** | |||
* Navigation Handlers Module | |||
* Manages various navigation interactions | |||
*/ | |||
const NavigationHandlers = { | |||
/** | |||
* Handles card click-through navigation | |||
* @param {Event} event - Click event | |||
*/ | |||
handleCardClick: function(event) { | |||
const card = event.target.closest(CONFIG.SELECTORS.CARD_LINKS); | |||
if (!card) return; | |||
const link = card.getAttribute('data-link'); | |||
if (!link) return; | |||
// Handle in-page anchor navigation | |||
if (link.charAt(0) === '#') { | |||
event.preventDefault(); | |||
const anchorElement = safeQuerySelector(link); | |||
if (anchorElement) { | |||
anchorElement.scrollIntoView({ behavior: 'smooth', block: 'start' }); | |||
} | |||
return; | |||
} | |||
// Handle absolute external URLs | |||
if (/^https?:\/\//i.test(link)) { | |||
window.location.href = link; | |||
return; | |||
} | |||
// Navigate to MediaWiki page | |||
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; | |||
}, | |||
/** | |||
* Initializes navigation event handlers | |||
*/ | |||
init: function() { | |||
document.addEventListener('click', this.handleCardClick, false); | |||
} | } | ||
}; | |||
// ============================================================================ | |||
// WIKI LINK PROCESSOR | |||
// ============================================================================ | |||
/** | |||
* Wiki Link Processor Module | |||
// | * Converts [[Title]] markup to proper MediaWiki links | ||
*/ | |||
if ( | const WikiLinkProcessor = { | ||
/** | |||
* Processes MediaWiki-style internal link markup | |||
* Converts [[Title]] or [[Title|Display]] to proper anchor tags | |||
*/ | |||
processWikiLinks: function() { | |||
// Skip if no raw brackets found (already processed by MediaWiki) | |||
if (!document.body || document.body.textContent.indexOf('[[') === -1) { | |||
return; | |||
} | } | ||
const walker = document.createTreeWalker( | |||
document.body, | |||
NodeFilter.SHOW_TEXT, | |||
null, | |||
false | |||
); | |||
const textNodes = []; | |||
let node; | |||
// Collect text nodes containing wiki links | |||
while ((node = walker.nextNode())) { | |||
const value = node.nodeValue; | |||
if (!value || value.indexOf('[[') === -1 || isInExcludedContext(node)) { | |||
continue; | |||
} | |||
textNodes.push(node); | |||
} | } | ||
const linkRegex = /\[\[([^\|\]]+)(?:\|([^\]]+))?\]\]/g; | |||
// Process each text node | |||
textNodes.forEach(textNode => { | |||
const sourceText = textNode.nodeValue; | |||
if (!linkRegex.test(sourceText)) return; | |||
linkRegex.lastIndex = 0; // Reset regex state | |||
const parent = textNode.parentNode; | |||
if ( | const fragment = document.createDocumentFragment(); | ||
let lastIndex = 0; | |||
let match; | |||
while ((match = linkRegex.exec(sourceText)) !== null) { | |||
// Add text before the match | |||
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(); | |||
// Skip File: or Image: links - keep original text | |||
if (/^(File:|Image:)/i.test(rawTitle)) { | |||
fragment.appendChild(document.createTextNode(match[0])); | |||
} else { | } else { | ||
// Remove leading colon if present | |||
const cleanTitle = rawTitle.charAt(0) === ':' ? rawTitle.slice(1) : rawTitle; | |||
// Create proper MediaWiki link | |||
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; | |||
} | |||
// Add remaining text | |||
if (lastIndex < sourceText.length) { | |||
fragment.appendChild( | |||
document.createTextNode(sourceText.slice(lastIndex)) | |||
); | |||
} | } | ||
// Replace the original text node | |||
parent.insertBefore(fragment, textNode); | |||
parent.removeChild(textNode); | |||
}); | }); | ||
} | }, | ||
// | /** | ||
* Initializes wiki link processing | |||
*/ | |||
init: function() { | |||
if (document.readyState === 'loading') { | |||
document.addEventListener('DOMContentLoaded', this.processWikiLinks); | |||
} else { | |||
this.processWikiLinks(); | |||
} | |||
} | |||
}; | |||
// ============================================================================ | |||
// MOBILE INTERFACE | |||
// ============================================================================ | |||
/** | |||
* Mobile Interface Module | |||
* Handles mobile-specific UI components | |||
*/ | |||
const MobileInterface = { | |||
/** | |||
* Creates hamburger menu for mobile sidebar navigation | |||
*/ | |||
createHamburgerMenu: function() { | |||
// Only create for Vector Legacy skin | |||
if (!document.body.classList.contains('skin-vector-legacy')) { | |||
return; | |||
} | |||
const panel = safeGetElementById('mw-panel'); | |||
if (!panel) return; | |||
// Create hamburger button | |||
const hamburger = document.createElement('button'); | |||
hamburger.className = 'mobile-hamburger-menu'; | |||
hamburger.setAttribute('aria-label', 'Toggle navigation menu'); | |||
hamburger.innerHTML = '<span></span><span></span><span></span>'; | |||
// Insert hamburger at top of page | |||
const content = safeGetElementById('content') || safeQuerySelector('.mw-body'); | |||
if (content && content.parentNode) { | |||
content.parentNode.insertBefore(hamburger, content); | |||
} | |||
// Toggle functionality | |||
const toggleSidebar = () => { | |||
panel.classList.toggle(CONFIG.CLASSES.MOBILE_OPEN); | |||
hamburger.classList.toggle(CONFIG.CLASSES.ACTIVE); | |||
document.body.classList.toggle(CONFIG.CLASSES.SIDEBAR_OPEN); | |||
}; | |||
// Event listeners | |||
hamburger.addEventListener('click', (event) => { | |||
event.preventDefault(); | |||
event.stopPropagation(); | |||
toggleSidebar(); | |||
}); | |||
// Close on outside click | |||
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); | |||
} | } | ||
}); | }); | ||
// Close on escape key | |||
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); | |||
} | } | ||
} | }); | ||
} | }, | ||
/ | /** | ||
* Initializes mobile interface components | |||
*/ | |||
init: function() { | |||
this.createHamburgerMenu(); | |||
} | |||
}; | |||
// ============================================================================ | |||
// COLLAPSIBLE SECTIONS | |||
// ============================================================================ | |||
/** | |||
* Collapsible Sections Module | |||
* Manages expandable/collapsible content sections | |||
*/ | |||
const CollapsibleSections = { | |||
/** | |||
* Initializes collapsible header functionality | |||
content. | */ | ||
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'; | |||
} | |||
} | } | ||
} | }); | ||
}); | |||
}, | |||
content. | |||
/** | |||
* Initializes collapsible button functionality | |||
*/ | |||
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'; | |||
} | |||
} | } | ||
} | }); | ||
}); | }); | ||
} | }, | ||
// | /** | ||
* Initializes all collapsible section types | |||
*/ | |||
init: function() { | |||
this.initCollapsibleHeaders(); | |||
this.initCollapsibleButtons(); | |||
} | |||
}; | |||
// ============================================================================ | |||
// SIDEBAR MANAGEMENT | |||
// ============================================================================ | |||
/** | |||
* Sidebar Management Module | |||
* Handles Vector Legacy sidebar collapsible functionality | |||
*/ | |||
/ | const SidebarManager = { | ||
function | /** | ||
* Gets a unique portal ID for localStorage | |||
* @param {Element} portal - Portal element | |||
* @returns {string} Unique portal identifier | |||
*/ | |||
getPortalId: function(portal) { | |||
const heading = portal.querySelector('.vector-menu-heading, h3'); | |||
if (heading) { | if (heading) { | ||
return heading.textContent.trim().toLowerCase().replace(/\s+/g, '-'); | return heading.textContent.trim().toLowerCase().replace(/\s+/g, '-'); | ||
} | } | ||
return 'unknown-portal'; | return 'unknown-portal'; | ||
} | }, | ||
// | /** | ||
* Initializes sidebar collapsible functionality | |||
portals.forEach( | */ | ||
if ( | initSidebarCollapsible: function() { | ||
// Only for Vector Legacy skin | |||
if (!document.body.classList.contains('skin-vector-legacy')) { | |||
return; | |||
} | |||
const panel = safeGetElementById('mw-panel'); | |||
if (!panel) return; | |||
const portals = safeQuerySelectorAll('.vector-menu-portal, .portal', panel); | |||
// Auto-collapse all sections except first (Navigation) | |||
portals.forEach((portal, index) => { | |||
if (index > 0) { | |||
portal.classList.add(CONFIG.CLASSES.COLLAPSED); | |||
const content = portal.querySelector('.vector-menu-content, .body'); | |||
if (content) { | if (content) { | ||
content.style.maxHeight = '0'; | |||
} | |||
} | |||
}); | |||
// Add click handlers to headings | |||
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) { | |||
// Expand | |||
portal.classList.remove(CONFIG.CLASSES.COLLAPSED); | |||
content.style.maxHeight = content.scrollHeight + 'px'; | content.style.maxHeight = content.scrollHeight + 'px'; | ||
safeSetLocalStorage(CONFIG.STORAGE_KEYS.SIDEBAR_PREFIX + portalId, 'expanded'); | |||
} else { | |||
// Collapse | |||
portal.classList.add(CONFIG.CLASSES.COLLAPSED); | |||
content.style.maxHeight = '0'; | |||
safeSetLocalStorage(CONFIG.STORAGE_KEYS.SIDEBAR_PREFIX + portalId, 'collapsed'); | |||
} | } | ||
}); | |||
}); | |||
// Restore saved states | |||
portals.forEach((portal, index) => { | |||
if (index === 0) return; // Skip first portal | |||
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'; | |||
} | } | ||
}); | }); | ||
// Guardian | // Recalculate heights on resize | ||
window.addEventListener('resize', () => { | |||
// | portals.forEach(portal => { | ||
function | if (!portal.classList.contains(CONFIG.CLASSES.COLLAPSED)) { | ||
const accordions = | const content = portal.querySelector('.vector-menu-content, .body'); | ||
if (content) { | |||
content.style.maxHeight = content.scrollHeight + 'px'; | |||
this.classList.toggle( | } | ||
} | |||
}); | |||
}); | |||
}, | |||
/** | |||
* Initializes sidebar management | |||
*/ | |||
init: function() { | |||
this.initSidebarCollapsible(); | |||
} | |||
}; | |||
// ============================================================================ | |||
// GUARDIAN DECISION HELPER | |||
// ============================================================================ | |||
/** | |||
* Guardian Decision Helper Module | |||
* Manages guardian type recommendation system | |||
*/ | |||
const GuardianDecisionHelper = { | |||
/** | |||
* Guardian type difficulty mapping | |||
*/ | |||
DIFFICULTY_MAP: { | |||
'1': 'regular', | |||
'2': 'mighty', | |||
'3': 'legendary', | |||
'4': 'superior', | |||
'5': 'accomplished' | |||
}, | |||
/** | |||
* Guardian type descriptions | |||
*/ | |||
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' | |||
}, | |||
/** | |||
* Initializes accordion functionality | |||
*/ | |||
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; | const panel = this.nextElementSibling; | ||
if (panel.style.maxHeight) { | |||
if (panel) { | |||
if (panel.style.maxHeight) { | |||
panel.style.maxHeight = null; | |||
} else { | |||
panel.style.maxHeight = panel.scrollHeight + 'px'; | |||
} | |||
} | } | ||
}); | }); | ||
} | }); | ||
} | }, | ||
/ | /** | ||
function | * Shows guardian by difficulty level | ||
const accordions = | * @param {string} level - Difficulty level (1-5) | ||
*/ | |||
showGuardianByDifficulty: function(level) { | |||
const accordions = safeQuerySelectorAll(CONFIG.SELECTORS.GUARDIAN_ACCORDION); | |||
// Reset all accordions | // Reset all accordions | ||
accordions.forEach(accordion => { | |||
accordion.classList.remove(CONFIG.CLASSES.ACTIVE); | |||
const panel = accordion.nextElementSibling; | |||
if (panel) { | |||
panel.style.maxHeight = null; | |||
} | |||
}); | |||
// Find and activate target accordion | |||
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'; | |||
} | |||
// Scroll to accordion | |||
setTimeout(() => { | |||
targetAccordion.scrollIntoView({ | |||
behavior: 'smooth', | |||
block: 'center' | |||
}); | |||
}, CONFIG.DELAYS.ACCORDION_SCROLL_DELAY); | |||
} | |||
} | } | ||
}, | |||
/ | /** | ||
function | * Initializes decision helper functionality | ||
*/ | |||
const options = | initDecisionHelper: function() { | ||
const options = safeQuerySelectorAll('.decision-options .option'); | |||
options.forEach(option => { | options.forEach(option => { | ||
option.addEventListener('click', function() { | option.addEventListener('click', function() { | ||
// Remove selected class from siblings | // Remove selected class from siblings | ||
const siblings = this.parentElement.querySelectorAll('.option'); | const siblings = this.parentElement.querySelectorAll('.option'); | ||
siblings.forEach( | siblings.forEach(sibling => sibling.classList.remove('selected')); | ||
// Add selected class to clicked option | // Add selected class to clicked option | ||
| Line 466: | Line 1,140: | ||
// Update recommendation | // Update recommendation | ||
updateRecommendation(); | GuardianDecisionHelper.updateRecommendation(); | ||
}); | }); | ||
}); | }); | ||
} | }, | ||
/ | /** | ||
function | * Updates recommendation based on selected options | ||
const selectedOptions = | */ | ||
updateRecommendation: function() { | |||
const selectedOptions = safeQuerySelectorAll('.decision-options .option.selected'); | |||
const recommendedTypeElement = safeGetElementById('recommendedType'); | |||
// | if (!recommendedTypeElement) return; | ||
// Show default message if no options selected | |||
if (selectedOptions.length === 0) { | if (selectedOptions.length === 0) { | ||
recommendedTypeElement.textContent = 'Select options above'; | |||
return; | return; | ||
} | } | ||
// Count votes for each guardian type | // Count votes for each guardian type | ||
const votes = { | const votes = { | ||
| Line 489: | Line 1,168: | ||
'accomplished': 0 | 'accomplished': 0 | ||
}; | }; | ||
// Tally votes from selected options | // Tally votes from selected options | ||
selectedOptions.forEach(option => { | selectedOptions.forEach(option => { | ||
const types = option.getAttribute('data-type').split(',') | const types = option.getAttribute('data-type'); | ||
if (types) { | |||
types.split(',').forEach(type => { | |||
if (votes.hasOwnProperty(type.trim())) { | |||
votes[type.trim()]++; | |||
} | |||
}); | |||
} | |||
}); | }); | ||
// Find | // Find type with most votes | ||
let maxVotes = 0; | let maxVotes = 0; | ||
let recommendedType = ''; | let recommendedType = ''; | ||
let tiedTypes = []; | let tiedTypes = []; | ||
for (const type in votes) { | for (const type in votes) { | ||
if (votes[type] > maxVotes) { | if (votes[type] > maxVotes) { | ||
| Line 512: | Line 1,195: | ||
} | } | ||
} | } | ||
// Handle ties by | // Handle ties by choosing more challenging type | ||
if (tiedTypes.length > 1) { | if (tiedTypes.length > 1) { | ||
const difficultyOrder = ['regular', 'mighty', 'legendary', 'superior', 'accomplished']; | const difficultyOrder = ['regular', 'mighty', 'legendary', 'superior', 'accomplished']; | ||
| Line 526: | Line 1,209: | ||
}); | }); | ||
} | } | ||
// Format | // Format recommendation text | ||
const formattedType = recommendedType.charAt(0).toUpperCase() + recommendedType.slice(1) + ' Guardian'; | |||
const description = this.TYPE_DESCRIPTIONS[recommendedType] || ''; | |||
const recommendationText = `${formattedType} - ${description}`; | |||
// Update display | |||
recommendedTypeElement.textContent = recommendationText; | |||
this.highlightRecommendedType(recommendedType); | |||
}, | |||
/** | |||
* Highlights the recommended guardian type | |||
* @param {string} type - Guardian type to highlight | |||
*/ | |||
highlightRecommendedType: function(type) { | |||
// Update | |||
} | |||
/ | |||
function | |||
// Remove highlight from all accordions | // Remove highlight from all accordions | ||
const accordions = | const accordions = safeQuerySelectorAll(CONFIG.SELECTORS.GUARDIAN_ACCORDION); | ||
accordions.forEach(accordion => { | accordions.forEach(accordion => { | ||
accordion.classList.remove( | accordion.classList.remove(CONFIG.CLASSES.RECOMMENDED); | ||
}); | }); | ||
// Add highlight to | // Add highlight to recommended type | ||
const recommendedAccordion = | const recommendedAccordion = safeQuerySelector(`.${type}-accordion`); | ||
if (recommendedAccordion) { | if (recommendedAccordion) { | ||
recommendedAccordion.classList.add( | recommendedAccordion.classList.add(CONFIG.CLASSES.RECOMMENDED); | ||
// Scroll to | // Scroll to recommended type if not visible | ||
if (!isElementInViewport(recommendedAccordion)) { | |||
recommendedAccordion.scrollIntoView({ | |||
recommendedAccordion.scrollIntoView({ behavior: 'smooth', block: 'center' }); | behavior: 'smooth', | ||
block: 'center' | |||
}); | |||
} | } | ||
} | } | ||
} | }, | ||
// | /** | ||
function | * Initializes Guardian Decision Helper | ||
*/ | |||
init: function() { | |||
// Only initialize if guardian elements exist | |||
const hasGuardianElements = | |||
safeQuerySelector(CONFIG.SELECTORS.GUARDIAN_ACCORDION) || | |||
safeQuerySelector('.decision-options'); | |||
// Make functions available | if (hasGuardianElements) { | ||
this.initAccordion(); | |||
this.initDecisionHelper(); | |||
// Make functions globally available | |||
window.updateGuardianRecommendation = this.updateRecommendation.bind(this); | |||
window.showGuardianByDifficulty = this.showGuardianByDifficulty.bind(this); | |||
} | |||
} | } | ||
} | }; | ||
// ============================================================================ | |||
// USER MENU MANAGEMENT | |||
// ============================================================================ | |||
/** | |||
* User Menu Management Module | |||
* Handles collapsible user menu functionality | |||
*/ | |||
const UserMenuManager = { | |||
/** | |||
* Initializes user menu collapsible functionality | |||
*/ | |||
initUserMenuCollapsible: function() { | |||
// Only for Vector Legacy skin | |||
if (!document.body.classList.contains('skin-vector-legacy')) { | |||
return; | |||
} | |||
const personalTools = safeGetElementById('p-personal'); | |||
if (!personalTools) return; | |||
// Get user menu items | |||
const userMenuItems = [ | |||
'pt-userpage', 'pt-mytalk', 'pt-preferences', | |||
'pt-watchlist', 'pt-mycontris', 'pt-logout' | |||
].map(id => safeGetElementById(id)).filter(Boolean); | |||
if (userMenuItems.length === 0) return; | |||
// Create menu container | |||
const menuContainer = document.createElement('div'); | |||
menuContainer.id = 'user-menu-collapsible'; | |||
menuContainer.className = 'user-menu-container'; | |||
// Create header/toggle button | |||
const menuHeader = document.createElement('div'); | |||
menuHeader.className = 'user-menu-header'; | |||
menuHeader.textContent = 'User'; | |||
menuHeader.addEventListener('click', () => { | |||
menuContainer.classList.toggle(CONFIG.CLASSES.EXPANDED); | |||
const state = menuContainer.classList.contains(CONFIG.CLASSES.EXPANDED) | |||
? 'expanded' : 'collapsed'; | |||
safeSetLocalStorage(CONFIG.STORAGE_KEYS.USER_MENU_STATE, state); | |||
}); | |||
// Create content container | |||
const menuContent = document.createElement('div'); | |||
menuContent.className = 'user-menu-content'; | |||
const menuList = document.createElement('ul'); | |||
menuList.className = 'user-menu-list'; | |||
// Move user menu items to new container | |||
userMenuItems.forEach(item => { | |||
if (item && item.parentNode) { | |||
menuList.appendChild(item); | |||
} | |||
}); | |||
// Assemble menu | |||
menuContent.appendChild(menuList); | |||
menuContainer.appendChild(menuHeader); | |||
menuContainer.appendChild(menuContent); | |||
document.body.appendChild(menuContainer); | |||
// Restore saved state | |||
const savedState = safeGetLocalStorage(CONFIG.STORAGE_KEYS.USER_MENU_STATE); | |||
if (savedState === 'expanded') { | |||
menuContainer.classList.add(CONFIG.CLASSES.EXPANDED); | |||
} | |||
// Close menu on outside click | |||
document.addEventListener('click', (event) => { | |||
if (menuContainer.classList.contains(CONFIG.CLASSES.EXPANDED) && | |||
!menuContainer.contains(event.target)) { | |||
menuContainer.classList.remove(CONFIG.CLASSES.EXPANDED); | |||
safeSetLocalStorage(CONFIG.STORAGE_KEYS.USER_MENU_STATE, 'collapsed'); | |||
} | } | ||
} | }); | ||
// | |||
// Hover functionality | |||
menuContainer.addEventListener('mouseenter', () => { | |||
menuContainer.classList.add(CONFIG.CLASSES.EXPANDED); | |||
}); | |||
menuContainer.addEventListener('mouseleave', () => { | |||
menuContainer.classList.remove(CONFIG.CLASSES.EXPANDED); | |||
safeSetLocalStorage(CONFIG.STORAGE_KEYS.USER_MENU_STATE, 'collapsed'); | |||
}); | |||
}, | |||
/** | |||
* Initializes user menu management | |||
*/ | |||
init: function() { | |||
this.initUserMenuCollapsible(); | |||
} | |||
}; | |||
// ============================================================================ | |||
// RESPONSIVE TABLE MANAGER | |||
// ============================================================================ | |||
/** | |||
* Responsive Table Manager Module | |||
* Wraps tables in containers for viewports ≤ 550px with enhanced accessibility and performance | |||
*/ | |||
const ResponsiveTableManager = { | |||
/** | |||
* Table selectors to make responsive | |||
*/ | |||
TABLE_SELECTORS: [ | |||
'table.wikitable', | |||
'table.mw-datatable', | |||
'table.faq-table', | |||
'table.daily-table' | |||
], | |||
/** | |||
* Viewport breakpoint for responsive behavior | |||
*/ | |||
RESPONSIVE_BREAKPOINT: 550, | |||
/** | |||
* Checks if viewport width is ≤ 550px | |||
* @returns {boolean} True if viewport requires responsive table behavior | |||
*/ | |||
isResponsiveViewport: function() { | |||
return window.innerWidth <= this.RESPONSIVE_BREAKPOINT; | |||
}, | |||
/** | |||
* Wraps a table in a container for responsive viewports | |||
* @param {HTMLElement} table - Table element to wrap | |||
*/ | |||
wrapTable: function(table) { | |||
// Skip if already wrapped or not on responsive viewport | |||
if (table.closest('.table-container') || !this.isResponsiveViewport()) { | |||
return; | |||
} | } | ||
// Create wrapper with enhanced accessibility | |||
const wrapper = document.createElement('div'); | |||
wrapper.className = 'table-container'; | |||
wrapper.setAttribute('role', 'region'); | |||
wrapper.setAttribute('aria-label', 'Tabela com rolagem horizontal'); | |||
wrapper.setAttribute('tabindex', '0'); | |||
// Insert wrapper before table | |||
table.parentNode.insertBefore(wrapper, table); | |||
// | |||
// Move table into wrapper | |||
if ( | wrapper.appendChild(table); | ||
// Add keyboard navigation support | |||
this.addKeyboardNavigation(wrapper); | |||
}, | |||
/** | |||
* Unwraps tables when viewport becomes larger | |||
* @param {HTMLElement} table - Table element to unwrap | |||
*/ | |||
unwrapTable: function(table) { | |||
const wrapper = table.closest('.table-container'); | |||
if (wrapper && !this.isResponsiveViewport()) { | |||
// Move table out of wrapper | |||
wrapper.parentNode.insertBefore(table, wrapper); | |||
// Remove wrapper | |||
wrapper.remove(); | |||
} | } | ||
} | }, | ||
// | /** | ||
* Adds keyboard navigation support to table containers | |||
* @param {HTMLElement} wrapper - Table wrapper element | |||
*/ | |||
addKeyboardNavigation: function(wrapper) { | |||
wrapper.addEventListener('keydown', (event) => { | |||
const { key } = event; | |||
// | const scrollAmount = 100; | ||
switch (key) { | |||
// | case 'ArrowLeft': | ||
event.preventDefault(); | |||
wrapper.scrollLeft -= scrollAmount; | |||
if ( | break; | ||
case 'ArrowRight': | |||
event.preventDefault(); | |||
wrapper.scrollLeft += scrollAmount; | |||
break; | |||
case 'Home': | |||
event.preventDefault(); | |||
wrapper.scrollLeft = 0; | |||
break; | |||
case 'End': | |||
event.preventDefault(); | |||
wrapper.scrollLeft = wrapper.scrollWidth; | |||
break; | |||
} | |||
}); | |||
}, | |||
/** | |||
* Processes all tables based on viewport size with performance optimization | |||
*/ | |||
processAllTables: function() { | |||
// Use requestAnimationFrame for better performance | |||
requestAnimationFrame(() => { | |||
this.TABLE_SELECTORS.forEach(selector => { | |||
const tables = document.querySelectorAll(selector); | |||
tables.forEach(table => { | |||
if (this.isResponsiveViewport()) { | |||
this.wrapTable(table); | |||
} else { | |||
this.unwrapTable(table); | |||
} | |||
}); | |||
}); | |||
}); | |||
}, | |||
/** | |||
* Handles window resize events with debouncing | |||
*/ | |||
handleResize: function() { | |||
// Clear existing timeout | |||
if (this.resizeTimeout) { | |||
clearTimeout(this.resizeTimeout); | |||
} | } | ||
// Debounce resize handling for better performance | |||
} | this.resizeTimeout = setTimeout(() => { | ||
this.processAllTables(); | |||
// | }, 100); | ||
}, | |||
/** | |||
* Sets up mutation observer to handle dynamically added tables with performance optimization | |||
*/ | |||
setupMutationObserver: function() { | |||
const observer = new MutationObserver((mutations) => { | |||
// Batch process mutations for better performance | |||
const tablesToProcess = []; | |||
mutations.forEach((mutation) => { | |||
mutation.addedNodes.forEach((node) => { | |||
if (node.nodeType === Node.ELEMENT_NODE) { | |||
// Check if the added node is a table or contains tables | |||
const tables = node.matches && this.TABLE_SELECTORS.some(selector => node.matches(selector)) | |||
? [node] | |||
: node.querySelectorAll ? node.querySelectorAll(this.TABLE_SELECTORS.join(', ')) : []; | |||
tablesToProcess.push(...tables); | |||
} | |||
}); | |||
}); | |||
// | // Process all found tables in a single animation frame | ||
if (tablesToProcess.length > 0) { | |||
requestAnimationFrame(() => { | |||
tablesToProcess.forEach(table => { | |||
if (this.isResponsiveViewport()) { | |||
this.wrapTable(table); | |||
} | |||
}); | |||
}); | |||
} | } | ||
}); | |||
observer.observe(document.body, { | |||
childList: true, | |||
subtree: true | |||
}); | |||
// Store observer reference for cleanup if needed | |||
this.mutationObserver = observer; | |||
}, | |||
/** | |||
* Initializes the responsive table system with enhanced MediaWiki compatibility | |||
*/ | |||
init: function() { | |||
// Wait for MediaWiki to be ready | |||
if (typeof mw !== 'undefined' && mw.loader) { | |||
mw.loader.using(['mediawiki.util'], () => { | |||
this.initializeSystem(); | |||
}); | |||
} else { | |||
// Fallback for non-MediaWiki environments | |||
this.initializeSystem(); | |||
} | } | ||
} | }, | ||
// | /** | ||
* Core initialization logic | |||
*/ | |||
initializeSystem: function() { | |||
// Process existing tables | |||
this.processAllTables(); | |||
// Set up resize handler with passive listener for better performance | |||
window.addEventListener('resize', this.handleResize.bind(this), { passive: true }); | |||
// Set up mutation observer for dynamic content | |||
this.setupMutationObserver(); | |||
// Add CSS class to body to indicate responsive tables are active | |||
document.body.classList.add('responsive-tables-enabled'); | |||
console.log('Responsive Table Manager initialized'); | |||
} | |||
}; | |||
// ============================================================================ | |||
// MAIN INITIALIZATION | |||
// ============================================================================ | |||
/ | /** | ||
* Main initialization function | |||
* Initializes all modules when DOM is ready | |||
*/ | |||
function initializeAll() { | function initializeAll() { | ||
try { | |||
// Initialize core MediaWiki modules | |||
TabNavigation.init(); | |||
NavigationHandlers.init(); | |||
WikiLinkProcessor.init(); | |||
MobileInterface.init(); | |||
CollapsibleSections.init(); | |||
SidebarManager.init(); | |||
GuardianDecisionHelper.init(); | |||
UserMenuManager.init(); | |||
ResponsiveTableManager.init(); | |||
// Initialize Quest Tracker module | |||
QuestTracker.init(); | |||
// Initialize Back to Top module | |||
BackToTop.init(); | |||
console.log('MediaWiki Common JavaScript with Quest Tracker and Back to Top initialized successfully'); | |||
} catch (error) { | |||
console.error('Error initializing MediaWiki Common JavaScript:', error); | |||
} | |||
} | } | ||
// Initialize | // Initialize when DOM is ready | ||
if (document.readyState === 'loading') { | if (document.readyState === 'loading') { | ||
document.addEventListener('DOMContentLoaded', initializeAll); | document.addEventListener('DOMContentLoaded', initializeAll); | ||
| Line 728: | Line 1,620: | ||
initializeAll(); | initializeAll(); | ||
} | } | ||
// Make Quest Tracker functions globally available for backward compatibility | |||
window.initQuestTracker = QuestTracker.init.bind(QuestTracker); | |||
window.loadQuestProgress = QuestTracker.loadQuestProgress.bind(QuestTracker); | |||
window.saveQuestProgress = QuestTracker.saveQuestProgress.bind(QuestTracker); | |||
window.updateProgressDisplay = QuestTracker.updateProgressDisplay.bind(QuestTracker); | |||
window.resetAllProgress = QuestTracker.resetAllProgress.bind(QuestTracker); | |||
})(); | })(); | ||
Revision as of 22:57, 19 October 2025
/**
* MediaWiki Common JavaScript Module - Enhanced Version
* Unified version with Quest Tracker functionality integrated
* Designed for MediaWiki site usage with Vector Legacy skin support
*
* @author NewCora Wiki Team
* @version 3.0
* @license MIT
*/
(function() {
'use strict';
// ============================================================================
// CONFIGURATION & CONSTANTS
// ============================================================================
/**
* Configuration object for the module
* @type {Object}
*/
const CONFIG = {
SELECTORS: {
TAB_CONTENT: '.tab-content',
NAV_TAB: '.nav-tab',
CARD_LINKS: '.card[data-link], .destaque-card[data-link]',
COLLAPSIBLE_HEADER: '.collapsible-header',
COLLAPSIBLE_BUTTON: '.collapsible',
GUARDIAN_ACCORDION: '.guardian-accordion',
// Quest Tracker selectors
QUEST_CHECKBOX: '.quest-checkbox',
NESTED_TAB: '.nested-tab',
NESTED_CONTENT: '.nested-content',
// Back to Top selectors
BTT_BUTTON: '.btt-button'
},
CLASSES: {
ACTIVE: 'active',
EXPANDED: 'expanded',
COLLAPSED: 'collapsed',
MOBILE_OPEN: 'mobile-open',
SIDEBAR_OPEN: 'sidebar-open',
RECOMMENDED: 'recommended',
// Back to Top classes
SHOW: 'show',
PULSE: 'pulse'
},
STORAGE_KEYS: {
SIDEBAR_PREFIX: 'sidebar-',
USER_MENU_STATE: 'user-menu-state',
QUEST_PROGRESS: 'shamanJiaQuestProgress'
},
DELAYS: {
SCROLL_DELAY: 100,
ACCORDION_SCROLL_DELAY: 300
}
};
// ============================================================================
// UTILITY FUNCTIONS
// ============================================================================
/**
* Safely gets an element by ID with error handling
* @param {string} id - The element ID
* @returns {Element|null} The element or null if not found
*/
function safeGetElementById(id) {
try {
return document.getElementById(id);
} catch (error) {
console.warn(`Element with ID '${id}' not found:`, error);
return null;
}
}
/**
* Safely queries for elements with error handling
* @param {string} selector - CSS selector
* @param {Element} [context=document] - Context element
* @returns {NodeList} NodeList of matching elements
*/
function safeQuerySelectorAll(selector, context = document) {
try {
return context.querySelectorAll(selector);
} catch (error) {
console.warn(`Invalid selector '${selector}':`, error);
return [];
}
}
/**
* Safely queries for a single element with error handling
* @param {string} selector - CSS selector
* @param {Element} [context=document] - Context element
* @returns {Element|null} The first matching element or null
*/
function safeQuerySelector(selector, context = document) {
try {
return context.querySelector(selector);
} catch (error) {
console.warn(`Invalid selector '${selector}':`, error);
return null;
}
}
/**
* Safely accesses localStorage with error handling
* @param {string} key - Storage key
* @param {string} [defaultValue=''] - Default value if key doesn't exist
* @returns {string} The stored value or default
*/
function safeGetLocalStorage(key, defaultValue = '') {
try {
return localStorage.getItem(key) || defaultValue;
} catch (error) {
console.warn(`localStorage access failed for key '${key}':`, error);
return defaultValue;
}
}
/**
* Safely sets localStorage with error handling
* @param {string} key - Storage key
* @param {string} value - Value to store
* @returns {boolean} Success status
*/
function safeSetLocalStorage(key, value) {
try {
localStorage.setItem(key, value);
return true;
} catch (error) {
console.warn(`localStorage write failed for key '${key}':`, error);
return false;
}
}
/**
* Checks if an element is in the viewport
* @param {Element} element - Element to check
* @returns {boolean} True if element is visible in viewport
*/
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)
);
}
/**
* Determines if a node is inside an excluded container
* @param {Node} node - The node to check
* @returns {boolean} True if node is in excluded context
*/
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);
}
// ============================================================================
// QUEST TRACKER MODULE (Integrated from ShamanGia)
// ============================================================================
/**
* Quest Tracker Module
* Manages quest progress tracking with localStorage persistence
*/
const QuestTracker = {
/**
* Initializes quest tracker functionality
*/
init: function() {
// Only initialize if quest tracker elements exist
const questCheckboxes = safeQuerySelectorAll(CONFIG.SELECTORS.QUEST_CHECKBOX);
if (questCheckboxes.length === 0) {
return;
}
this.loadQuestProgress();
this.setupEventListeners();
this.updateProgressDisplay();
console.log('Quest Tracker initialized');
},
/**
* Sets up event listeners for quest tracker
*/
setupEventListeners: function() {
const questCheckboxes = safeQuerySelectorAll(CONFIG.SELECTORS.QUEST_CHECKBOX);
const resetButton = safeGetElementById('reset-progress');
// Add event listeners to checkboxes
questCheckboxes.forEach(checkbox => {
checkbox.addEventListener('change', () => {
this.saveQuestProgress();
this.updateProgressDisplay();
});
});
// Add event listener to reset button
if (resetButton) {
resetButton.addEventListener('click', () => {
if (confirm('Are you sure you want to reset all quest progress? This action cannot be undone.')) {
this.resetAllProgress();
}
});
}
},
/**
* Loads quest progress from localStorage
*/
loadQuestProgress: function() {
const savedProgress = safeGetLocalStorage(CONFIG.STORAGE_KEYS.QUEST_PROGRESS);
if (savedProgress) {
try {
const progressData = JSON.parse(savedProgress);
Object.keys(progressData).forEach(questId => {
const checkbox = safeQuerySelector(`[data-quest="${questId}"]`);
if (checkbox) {
checkbox.checked = progressData[questId];
}
});
} catch (error) {
console.error('Error loading quest progress:', error);
}
}
},
/**
* Saves quest progress to localStorage
*/
saveQuestProgress: function() {
const questCheckboxes = safeQuerySelectorAll(CONFIG.SELECTORS.QUEST_CHECKBOX);
const progressData = {};
questCheckboxes.forEach(checkbox => {
const questId = checkbox.getAttribute('data-quest');
if (questId) {
progressData[questId] = checkbox.checked;
}
});
try {
safeSetLocalStorage(CONFIG.STORAGE_KEYS.QUEST_PROGRESS, JSON.stringify(progressData));
} catch (error) {
console.error('Error saving quest progress:', error);
}
},
/**
* Updates progress display elements
*/
updateProgressDisplay: function() {
const questCheckboxes = safeQuerySelectorAll(CONFIG.SELECTORS.QUEST_CHECKBOX);
const totalQuests = questCheckboxes.length;
let completedQuests = 0;
questCheckboxes.forEach(checkbox => {
if (checkbox.checked) {
completedQuests++;
}
});
const completionPercentage = totalQuests > 0 ? Math.round((completedQuests / totalQuests) * 100) : 0;
// Update progress display elements
const totalProgressElement = safeGetElementById('total-progress');
const completionPercentageElement = safeGetElementById('completion-percentage');
const progressFillElement = safeGetElementById('progress-fill');
if (totalProgressElement) {
totalProgressElement.textContent = `${completedQuests}/${totalQuests}`;
}
if (completionPercentageElement) {
completionPercentageElement.textContent = `${completionPercentage}%`;
}
if (progressFillElement) {
progressFillElement.style.width = `${completionPercentage}%`;
}
},
/**
* Resets all quest progress
*/
resetAllProgress: function() {
const questCheckboxes = safeQuerySelectorAll(CONFIG.SELECTORS.QUEST_CHECKBOX);
// Uncheck all checkboxes
questCheckboxes.forEach(checkbox => {
checkbox.checked = false;
});
// Clear localStorage
try {
localStorage.removeItem(CONFIG.STORAGE_KEYS.QUEST_PROGRESS);
} catch (error) {
console.error('Error clearing quest progress:', error);
}
// Update progress display
this.updateProgressDisplay();
}
};
// ============================================================================
// TAB NAVIGATION SYSTEM (Enhanced with Quest Tracker support)
// ============================================================================
/**
* Tab Navigation Module
* Handles tab switching functionality with smooth scrolling support
* Enhanced to work with both MediaWiki tabs and Quest Tracker nested tabs
*/
const TabNavigation = {
/**
* Shows a specific tab and hides others
* @param {string} tabId - ID of the tab content to show
* @param {Element} [tabElement] - The tab button element
*/
showTab: function(tabId, tabElement) {
if (!tabId) {
console.warn('TabNavigation.showTab: tabId is required');
return;
}
// CRITICAL FIX: Hide ALL nested contents first when switching main tabs
// This prevents nested content from other sections remaining visible
const allNestedContents = document.querySelectorAll('.nested-content');
allNestedContents.forEach(content => {
content.classList.remove(CONFIG.CLASSES.ACTIVE);
});
// Hide all tab contents
const contents = safeQuerySelectorAll(CONFIG.SELECTORS.TAB_CONTENT);
contents.forEach(content => content.classList.remove(CONFIG.CLASSES.ACTIVE));
// Deactivate all tab buttons
const tabs = safeQuerySelectorAll(CONFIG.SELECTORS.NAV_TAB);
tabs.forEach(tab => tab.classList.remove(CONFIG.CLASSES.ACTIVE));
// Show target tab content
const targetContent = safeGetElementById(tabId);
if (targetContent) {
targetContent.classList.add(CONFIG.CLASSES.ACTIVE);
// If this is the Normal Daily tab, activate the first nested tab
if (tabId === 'tab-normal-daily') {
setTimeout(() => {
const firstNestedTab = targetContent.querySelector('.nested-tab');
if (firstNestedTab) {
const firstNestedTabId = firstNestedTab.getAttribute('data-tab');
if (firstNestedTabId) {
TabNavigation.showNestedTab(firstNestedTabId, firstNestedTab);
}
}
}, 50);
}
} else {
console.warn(`Tab content with ID '${tabId}' not found`);
}
// Activate the clicked tab button
if (tabElement) {
tabElement.classList.add(CONFIG.CLASSES.ACTIVE);
}
},
/**
* Shows a specific nested tab and hides others
* @param {string} tabId - ID of the nested tab content to show
* @param {Element} [tabElement] - The nested tab button element
*/
showNestedTab: function(tabId, tabElement) {
if (!tabId) {
console.warn('TabNavigation.showNestedTab: tabId is required');
return;
}
// Find the parent tab content to scope the nested tab switching
const parentTabContent = tabElement ? tabElement.closest('.tab-content') : null;
const searchScope = parentTabContent || document;
// CRITICAL FIX: Hide ALL nested contents in the entire document first
// This ensures no content from other sections remains visible
const allNestedContents = document.querySelectorAll('.nested-content');
allNestedContents.forEach(content => {
content.classList.remove(CONFIG.CLASSES.ACTIVE);
});
// Deactivate all nested tab buttons within the scope
const nestedTabs = safeQuerySelectorAll('.nested-tab', searchScope);
nestedTabs.forEach(tab => {
tab.classList.remove(CONFIG.CLASSES.ACTIVE);
});
// Show target nested tab content
const targetContent = safeGetElementById(tabId);
if (targetContent) {
targetContent.classList.add(CONFIG.CLASSES.ACTIVE);
} else {
console.warn(`Nested tab content with ID '${tabId}' not found`);
}
// Activate the clicked nested tab button
if (tabElement) {
tabElement.classList.add(CONFIG.CLASSES.ACTIVE);
}
},
/**
* Handles tab click events using event delegation
* @param {Event} event - Click event
*/
handleTabClick: function(event) {
const tab = event.target.closest('.nav-tab[data-tab]');
if (!tab) return;
event.preventDefault();
const targetId = tab.getAttribute('data-tab');
if (targetId) {
TabNavigation.showTab(targetId, tab);
}
},
/**
* Handles nested tab click events using event delegation
* Enhanced to work with Quest Tracker nested tabs
* @param {Event} event - Click event
*/
handleNestedTabClick: function(event) {
const nestedTab = event.target.closest('.nested-tab[data-tab]');
if (!nestedTab) return;
event.preventDefault();
const targetId = nestedTab.getAttribute('data-tab');
if (targetId) {
TabNavigation.showNestedTab(targetId, nestedTab);
}
},
/**
* Handles special navigation clicks with scroll support
* @param {Event} event - Click event
*/
handleSpecialTabNavigation: function(event) {
const trigger = event.target.closest('[data-tab-trigger]');
if (!trigger) return;
event.preventDefault();
const tabData = trigger.getAttribute('data-tab-trigger');
const buttonId = trigger.getAttribute('data-tab-button');
const href = trigger.getAttribute('href');
const scrollToAttr = trigger.getAttribute('data-scroll-to');
// Determine scroll target
let scrollTargetSelector = null;
if (scrollToAttr && scrollToAttr.trim()) {
scrollTargetSelector = '#' + scrollToAttr.replace(/^#/, '');
} else if (href && href.charAt(0) === '#') {
scrollTargetSelector = href;
}
// Switch tab if specified
if (tabData && buttonId) {
const targetButton = safeGetElementById(buttonId);
if (targetButton) {
TabNavigation.showTab(tabData, targetButton);
// Scroll to anchor after tab switch
if (scrollTargetSelector) {
const scrollTarget = safeQuerySelector(scrollTargetSelector);
if (scrollTarget) {
setTimeout(() => {
scrollTarget.scrollIntoView({ behavior: 'smooth' });
}, CONFIG.DELAYS.SCROLL_DELAY);
}
}
}
}
},
/**
* Initializes tab navigation event listeners
*/
init: function() {
document.addEventListener('click', this.handleTabClick, false);
document.addEventListener('click', this.handleNestedTabClick, false);
document.addEventListener('click', this.handleSpecialTabNavigation, false);
// Make showTab globally available for backward compatibility
window.showTab = this.showTab;
}
};
// ============================================================================
// BACK TO TOP MODULE
// ============================================================================
/**
* Back to Top Module
* Manages the back-to-top button functionality with smooth animations
*/
const BackToTop = {
button: null,
isVisible: false,
scrollThreshold: 300,
/**
* Creates the back-to-top button element
*/
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;
},
/**
* Shows the back-to-top button with animation
*/
showButton: function() {
if (!this.isVisible && this.button) {
this.button.classList.add(CONFIG.CLASSES.SHOW);
this.button.classList.add(CONFIG.CLASSES.PULSE);
this.isVisible = true;
// Remove pulse class after animation
setTimeout(() => {
if (this.button) {
this.button.classList.remove(CONFIG.CLASSES.PULSE);
}
}, 6000);
}
},
/**
* Hides the back-to-top button with animation
*/
hideButton: function() {
if (this.isVisible && this.button) {
this.button.classList.remove(CONFIG.CLASSES.SHOW);
this.button.classList.remove(CONFIG.CLASSES.PULSE);
this.isVisible = false;
}
},
/**
* Handles scroll events to show/hide button
*/
handleScroll: function() {
const scrollTop = window.pageYOffset || document.documentElement.scrollTop;
if (scrollTop > this.scrollThreshold) {
this.showButton();
} else {
this.hideButton();
}
},
/**
* Handles button click to scroll to top
*/
handleClick: function() {
window.scrollTo({
top: 0,
behavior: 'smooth'
});
},
/**
* Initializes the back-to-top functionality
*/
init: function() {
// Create the button
this.button = this.createButton();
// Bind event handlers
const boundHandleScroll = this.handleScroll.bind(this);
const boundHandleClick = this.handleClick.bind(this);
// Add event listeners
window.addEventListener('scroll', boundHandleScroll, { passive: true });
this.button.addEventListener('click', boundHandleClick);
// Initial check
this.handleScroll();
console.log('Back to Top button initialized');
}
};
// ============================================================================
// NAVIGATION & INTERACTION HANDLERS (Enhanced)
// ============================================================================
/**
* Navigation Handlers Module
* Manages various navigation interactions
*/
const NavigationHandlers = {
/**
* Handles card click-through navigation
* @param {Event} event - Click event
*/
handleCardClick: function(event) {
const card = event.target.closest(CONFIG.SELECTORS.CARD_LINKS);
if (!card) return;
const link = card.getAttribute('data-link');
if (!link) return;
// Handle in-page anchor navigation
if (link.charAt(0) === '#') {
event.preventDefault();
const anchorElement = safeQuerySelector(link);
if (anchorElement) {
anchorElement.scrollIntoView({ behavior: 'smooth', block: 'start' });
}
return;
}
// Handle absolute external URLs
if (/^https?:\/\//i.test(link)) {
window.location.href = link;
return;
}
// Navigate to MediaWiki page
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;
},
/**
* Initializes navigation event handlers
*/
init: function() {
document.addEventListener('click', this.handleCardClick, false);
}
};
// ============================================================================
// WIKI LINK PROCESSOR
// ============================================================================
/**
* Wiki Link Processor Module
* Converts [[Title]] markup to proper MediaWiki links
*/
const WikiLinkProcessor = {
/**
* Processes MediaWiki-style internal link markup
* Converts [[Title]] or [[Title|Display]] to proper anchor tags
*/
processWikiLinks: function() {
// Skip if no raw brackets found (already processed by MediaWiki)
if (!document.body || document.body.textContent.indexOf('[[') === -1) {
return;
}
const walker = document.createTreeWalker(
document.body,
NodeFilter.SHOW_TEXT,
null,
false
);
const textNodes = [];
let node;
// Collect text nodes containing wiki links
while ((node = walker.nextNode())) {
const value = node.nodeValue;
if (!value || value.indexOf('[[') === -1 || isInExcludedContext(node)) {
continue;
}
textNodes.push(node);
}
const linkRegex = /\[\[([^\|\]]+)(?:\|([^\]]+))?\]\]/g;
// Process each text node
textNodes.forEach(textNode => {
const sourceText = textNode.nodeValue;
if (!linkRegex.test(sourceText)) return;
linkRegex.lastIndex = 0; // Reset regex state
const parent = textNode.parentNode;
const fragment = document.createDocumentFragment();
let lastIndex = 0;
let match;
while ((match = linkRegex.exec(sourceText)) !== null) {
// Add text before the match
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();
// Skip File: or Image: links - keep original text
if (/^(File:|Image:)/i.test(rawTitle)) {
fragment.appendChild(document.createTextNode(match[0]));
} else {
// Remove leading colon if present
const cleanTitle = rawTitle.charAt(0) === ':' ? rawTitle.slice(1) : rawTitle;
// Create proper MediaWiki link
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;
}
// Add remaining text
if (lastIndex < sourceText.length) {
fragment.appendChild(
document.createTextNode(sourceText.slice(lastIndex))
);
}
// Replace the original text node
parent.insertBefore(fragment, textNode);
parent.removeChild(textNode);
});
},
/**
* Initializes wiki link processing
*/
init: function() {
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', this.processWikiLinks);
} else {
this.processWikiLinks();
}
}
};
// ============================================================================
// MOBILE INTERFACE
// ============================================================================
/**
* Mobile Interface Module
* Handles mobile-specific UI components
*/
const MobileInterface = {
/**
* Creates hamburger menu for mobile sidebar navigation
*/
createHamburgerMenu: function() {
// Only create for Vector Legacy skin
if (!document.body.classList.contains('skin-vector-legacy')) {
return;
}
const panel = safeGetElementById('mw-panel');
if (!panel) return;
// Create hamburger button
const hamburger = document.createElement('button');
hamburger.className = 'mobile-hamburger-menu';
hamburger.setAttribute('aria-label', 'Toggle navigation menu');
hamburger.innerHTML = '<span></span><span></span><span></span>';
// Insert hamburger at top of page
const content = safeGetElementById('content') || safeQuerySelector('.mw-body');
if (content && content.parentNode) {
content.parentNode.insertBefore(hamburger, content);
}
// Toggle functionality
const toggleSidebar = () => {
panel.classList.toggle(CONFIG.CLASSES.MOBILE_OPEN);
hamburger.classList.toggle(CONFIG.CLASSES.ACTIVE);
document.body.classList.toggle(CONFIG.CLASSES.SIDEBAR_OPEN);
};
// Event listeners
hamburger.addEventListener('click', (event) => {
event.preventDefault();
event.stopPropagation();
toggleSidebar();
});
// Close on outside click
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);
}
});
// Close on escape key
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);
}
});
},
/**
* Initializes mobile interface components
*/
init: function() {
this.createHamburgerMenu();
}
};
// ============================================================================
// COLLAPSIBLE SECTIONS
// ============================================================================
/**
* Collapsible Sections Module
* Manages expandable/collapsible content sections
*/
const CollapsibleSections = {
/**
* Initializes collapsible header functionality
*/
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';
}
}
});
});
},
/**
* Initializes collapsible button functionality
*/
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';
}
}
});
});
},
/**
* Initializes all collapsible section types
*/
init: function() {
this.initCollapsibleHeaders();
this.initCollapsibleButtons();
}
};
// ============================================================================
// SIDEBAR MANAGEMENT
// ============================================================================
/**
* Sidebar Management Module
* Handles Vector Legacy sidebar collapsible functionality
*/
const SidebarManager = {
/**
* Gets a unique portal ID for localStorage
* @param {Element} portal - Portal element
* @returns {string} Unique portal identifier
*/
getPortalId: function(portal) {
const heading = portal.querySelector('.vector-menu-heading, h3');
if (heading) {
return heading.textContent.trim().toLowerCase().replace(/\s+/g, '-');
}
return 'unknown-portal';
},
/**
* Initializes sidebar collapsible functionality
*/
initSidebarCollapsible: function() {
// Only for Vector Legacy skin
if (!document.body.classList.contains('skin-vector-legacy')) {
return;
}
const panel = safeGetElementById('mw-panel');
if (!panel) return;
const portals = safeQuerySelectorAll('.vector-menu-portal, .portal', panel);
// Auto-collapse all sections except first (Navigation)
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';
}
}
});
// Add click handlers to headings
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) {
// Expand
portal.classList.remove(CONFIG.CLASSES.COLLAPSED);
content.style.maxHeight = content.scrollHeight + 'px';
safeSetLocalStorage(CONFIG.STORAGE_KEYS.SIDEBAR_PREFIX + portalId, 'expanded');
} else {
// Collapse
portal.classList.add(CONFIG.CLASSES.COLLAPSED);
content.style.maxHeight = '0';
safeSetLocalStorage(CONFIG.STORAGE_KEYS.SIDEBAR_PREFIX + portalId, 'collapsed');
}
});
});
// Restore saved states
portals.forEach((portal, index) => {
if (index === 0) return; // Skip first portal
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';
}
});
// Recalculate heights on resize
window.addEventListener('resize', () => {
portals.forEach(portal => {
if (!portal.classList.contains(CONFIG.CLASSES.COLLAPSED)) {
const content = portal.querySelector('.vector-menu-content, .body');
if (content) {
content.style.maxHeight = content.scrollHeight + 'px';
}
}
});
});
},
/**
* Initializes sidebar management
*/
init: function() {
this.initSidebarCollapsible();
}
};
// ============================================================================
// GUARDIAN DECISION HELPER
// ============================================================================
/**
* Guardian Decision Helper Module
* Manages guardian type recommendation system
*/
const GuardianDecisionHelper = {
/**
* Guardian type difficulty mapping
*/
DIFFICULTY_MAP: {
'1': 'regular',
'2': 'mighty',
'3': 'legendary',
'4': 'superior',
'5': 'accomplished'
},
/**
* Guardian type descriptions
*/
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'
},
/**
* Initializes accordion functionality
*/
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';
}
}
});
});
},
/**
* Shows guardian by difficulty level
* @param {string} level - Difficulty level (1-5)
*/
showGuardianByDifficulty: function(level) {
const accordions = safeQuerySelectorAll(CONFIG.SELECTORS.GUARDIAN_ACCORDION);
// Reset all accordions
accordions.forEach(accordion => {
accordion.classList.remove(CONFIG.CLASSES.ACTIVE);
const panel = accordion.nextElementSibling;
if (panel) {
panel.style.maxHeight = null;
}
});
// Find and activate target accordion
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';
}
// Scroll to accordion
setTimeout(() => {
targetAccordion.scrollIntoView({
behavior: 'smooth',
block: 'center'
});
}, CONFIG.DELAYS.ACCORDION_SCROLL_DELAY);
}
}
},
/**
* Initializes decision helper functionality
*/
initDecisionHelper: function() {
const options = safeQuerySelectorAll('.decision-options .option');
options.forEach(option => {
option.addEventListener('click', function() {
// Remove selected class from siblings
const siblings = this.parentElement.querySelectorAll('.option');
siblings.forEach(sibling => sibling.classList.remove('selected'));
// Add selected class to clicked option
this.classList.add('selected');
// Update recommendation
GuardianDecisionHelper.updateRecommendation();
});
});
},
/**
* Updates recommendation based on selected options
*/
updateRecommendation: function() {
const selectedOptions = safeQuerySelectorAll('.decision-options .option.selected');
const recommendedTypeElement = safeGetElementById('recommendedType');
if (!recommendedTypeElement) return;
// Show default message if no options selected
if (selectedOptions.length === 0) {
recommendedTypeElement.textContent = 'Select options above';
return;
}
// Count votes for each guardian type
const votes = {
'regular': 0,
'mighty': 0,
'legendary': 0,
'superior': 0,
'accomplished': 0
};
// Tally votes from selected options
selectedOptions.forEach(option => {
const types = option.getAttribute('data-type');
if (types) {
types.split(',').forEach(type => {
if (votes.hasOwnProperty(type.trim())) {
votes[type.trim()]++;
}
});
}
});
// Find type with most votes
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);
}
}
// Handle ties by choosing more challenging 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;
}
});
}
// Format recommendation text
const formattedType = recommendedType.charAt(0).toUpperCase() + recommendedType.slice(1) + ' Guardian';
const description = this.TYPE_DESCRIPTIONS[recommendedType] || '';
const recommendationText = `${formattedType} - ${description}`;
// Update display
recommendedTypeElement.textContent = recommendationText;
this.highlightRecommendedType(recommendedType);
},
/**
* Highlights the recommended guardian type
* @param {string} type - Guardian type to highlight
*/
highlightRecommendedType: function(type) {
// Remove highlight from all accordions
const accordions = safeQuerySelectorAll(CONFIG.SELECTORS.GUARDIAN_ACCORDION);
accordions.forEach(accordion => {
accordion.classList.remove(CONFIG.CLASSES.RECOMMENDED);
});
// Add highlight to recommended type
const recommendedAccordion = safeQuerySelector(`.${type}-accordion`);
if (recommendedAccordion) {
recommendedAccordion.classList.add(CONFIG.CLASSES.RECOMMENDED);
// Scroll to recommended type if not visible
if (!isElementInViewport(recommendedAccordion)) {
recommendedAccordion.scrollIntoView({
behavior: 'smooth',
block: 'center'
});
}
}
},
/**
* Initializes Guardian Decision Helper
*/
init: function() {
// Only initialize if guardian elements exist
const hasGuardianElements =
safeQuerySelector(CONFIG.SELECTORS.GUARDIAN_ACCORDION) ||
safeQuerySelector('.decision-options');
if (hasGuardianElements) {
this.initAccordion();
this.initDecisionHelper();
// Make functions globally available
window.updateGuardianRecommendation = this.updateRecommendation.bind(this);
window.showGuardianByDifficulty = this.showGuardianByDifficulty.bind(this);
}
}
};
// ============================================================================
// USER MENU MANAGEMENT
// ============================================================================
/**
* User Menu Management Module
* Handles collapsible user menu functionality
*/
const UserMenuManager = {
/**
* Initializes user menu collapsible functionality
*/
initUserMenuCollapsible: function() {
// Only for Vector Legacy skin
if (!document.body.classList.contains('skin-vector-legacy')) {
return;
}
const personalTools = safeGetElementById('p-personal');
if (!personalTools) return;
// Get user menu items
const userMenuItems = [
'pt-userpage', 'pt-mytalk', 'pt-preferences',
'pt-watchlist', 'pt-mycontris', 'pt-logout'
].map(id => safeGetElementById(id)).filter(Boolean);
if (userMenuItems.length === 0) return;
// Create menu container
const menuContainer = document.createElement('div');
menuContainer.id = 'user-menu-collapsible';
menuContainer.className = 'user-menu-container';
// Create header/toggle button
const menuHeader = document.createElement('div');
menuHeader.className = 'user-menu-header';
menuHeader.textContent = 'User';
menuHeader.addEventListener('click', () => {
menuContainer.classList.toggle(CONFIG.CLASSES.EXPANDED);
const state = menuContainer.classList.contains(CONFIG.CLASSES.EXPANDED)
? 'expanded' : 'collapsed';
safeSetLocalStorage(CONFIG.STORAGE_KEYS.USER_MENU_STATE, state);
});
// Create content container
const menuContent = document.createElement('div');
menuContent.className = 'user-menu-content';
const menuList = document.createElement('ul');
menuList.className = 'user-menu-list';
// Move user menu items to new container
userMenuItems.forEach(item => {
if (item && item.parentNode) {
menuList.appendChild(item);
}
});
// Assemble menu
menuContent.appendChild(menuList);
menuContainer.appendChild(menuHeader);
menuContainer.appendChild(menuContent);
document.body.appendChild(menuContainer);
// Restore saved state
const savedState = safeGetLocalStorage(CONFIG.STORAGE_KEYS.USER_MENU_STATE);
if (savedState === 'expanded') {
menuContainer.classList.add(CONFIG.CLASSES.EXPANDED);
}
// Close menu on outside click
document.addEventListener('click', (event) => {
if (menuContainer.classList.contains(CONFIG.CLASSES.EXPANDED) &&
!menuContainer.contains(event.target)) {
menuContainer.classList.remove(CONFIG.CLASSES.EXPANDED);
safeSetLocalStorage(CONFIG.STORAGE_KEYS.USER_MENU_STATE, 'collapsed');
}
});
// Hover functionality
menuContainer.addEventListener('mouseenter', () => {
menuContainer.classList.add(CONFIG.CLASSES.EXPANDED);
});
menuContainer.addEventListener('mouseleave', () => {
menuContainer.classList.remove(CONFIG.CLASSES.EXPANDED);
safeSetLocalStorage(CONFIG.STORAGE_KEYS.USER_MENU_STATE, 'collapsed');
});
},
/**
* Initializes user menu management
*/
init: function() {
this.initUserMenuCollapsible();
}
};
// ============================================================================
// RESPONSIVE TABLE MANAGER
// ============================================================================
/**
* Responsive Table Manager Module
* Wraps tables in containers for viewports ≤ 550px with enhanced accessibility and performance
*/
const ResponsiveTableManager = {
/**
* Table selectors to make responsive
*/
TABLE_SELECTORS: [
'table.wikitable',
'table.mw-datatable',
'table.faq-table',
'table.daily-table'
],
/**
* Viewport breakpoint for responsive behavior
*/
RESPONSIVE_BREAKPOINT: 550,
/**
* Checks if viewport width is ≤ 550px
* @returns {boolean} True if viewport requires responsive table behavior
*/
isResponsiveViewport: function() {
return window.innerWidth <= this.RESPONSIVE_BREAKPOINT;
},
/**
* Wraps a table in a container for responsive viewports
* @param {HTMLElement} table - Table element to wrap
*/
wrapTable: function(table) {
// Skip if already wrapped or not on responsive viewport
if (table.closest('.table-container') || !this.isResponsiveViewport()) {
return;
}
// Create wrapper with enhanced accessibility
const wrapper = document.createElement('div');
wrapper.className = 'table-container';
wrapper.setAttribute('role', 'region');
wrapper.setAttribute('aria-label', 'Tabela com rolagem horizontal');
wrapper.setAttribute('tabindex', '0');
// Insert wrapper before table
table.parentNode.insertBefore(wrapper, table);
// Move table into wrapper
wrapper.appendChild(table);
// Add keyboard navigation support
this.addKeyboardNavigation(wrapper);
},
/**
* Unwraps tables when viewport becomes larger
* @param {HTMLElement} table - Table element to unwrap
*/
unwrapTable: function(table) {
const wrapper = table.closest('.table-container');
if (wrapper && !this.isResponsiveViewport()) {
// Move table out of wrapper
wrapper.parentNode.insertBefore(table, wrapper);
// Remove wrapper
wrapper.remove();
}
},
/**
* Adds keyboard navigation support to table containers
* @param {HTMLElement} wrapper - Table wrapper element
*/
addKeyboardNavigation: function(wrapper) {
wrapper.addEventListener('keydown', (event) => {
const { key } = event;
const scrollAmount = 100;
switch (key) {
case 'ArrowLeft':
event.preventDefault();
wrapper.scrollLeft -= scrollAmount;
break;
case 'ArrowRight':
event.preventDefault();
wrapper.scrollLeft += scrollAmount;
break;
case 'Home':
event.preventDefault();
wrapper.scrollLeft = 0;
break;
case 'End':
event.preventDefault();
wrapper.scrollLeft = wrapper.scrollWidth;
break;
}
});
},
/**
* Processes all tables based on viewport size with performance optimization
*/
processAllTables: function() {
// Use requestAnimationFrame for better performance
requestAnimationFrame(() => {
this.TABLE_SELECTORS.forEach(selector => {
const tables = document.querySelectorAll(selector);
tables.forEach(table => {
if (this.isResponsiveViewport()) {
this.wrapTable(table);
} else {
this.unwrapTable(table);
}
});
});
});
},
/**
* Handles window resize events with debouncing
*/
handleResize: function() {
// Clear existing timeout
if (this.resizeTimeout) {
clearTimeout(this.resizeTimeout);
}
// Debounce resize handling for better performance
this.resizeTimeout = setTimeout(() => {
this.processAllTables();
}, 100);
},
/**
* Sets up mutation observer to handle dynamically added tables with performance optimization
*/
setupMutationObserver: function() {
const observer = new MutationObserver((mutations) => {
// Batch process mutations for better performance
const tablesToProcess = [];
mutations.forEach((mutation) => {
mutation.addedNodes.forEach((node) => {
if (node.nodeType === Node.ELEMENT_NODE) {
// Check if the added node is a table or contains tables
const tables = node.matches && this.TABLE_SELECTORS.some(selector => node.matches(selector))
? [node]
: node.querySelectorAll ? node.querySelectorAll(this.TABLE_SELECTORS.join(', ')) : [];
tablesToProcess.push(...tables);
}
});
});
// Process all found tables in a single animation frame
if (tablesToProcess.length > 0) {
requestAnimationFrame(() => {
tablesToProcess.forEach(table => {
if (this.isResponsiveViewport()) {
this.wrapTable(table);
}
});
});
}
});
observer.observe(document.body, {
childList: true,
subtree: true
});
// Store observer reference for cleanup if needed
this.mutationObserver = observer;
},
/**
* Initializes the responsive table system with enhanced MediaWiki compatibility
*/
init: function() {
// Wait for MediaWiki to be ready
if (typeof mw !== 'undefined' && mw.loader) {
mw.loader.using(['mediawiki.util'], () => {
this.initializeSystem();
});
} else {
// Fallback for non-MediaWiki environments
this.initializeSystem();
}
},
/**
* Core initialization logic
*/
initializeSystem: function() {
// Process existing tables
this.processAllTables();
// Set up resize handler with passive listener for better performance
window.addEventListener('resize', this.handleResize.bind(this), { passive: true });
// Set up mutation observer for dynamic content
this.setupMutationObserver();
// Add CSS class to body to indicate responsive tables are active
document.body.classList.add('responsive-tables-enabled');
console.log('Responsive Table Manager initialized');
}
};
// ============================================================================
// MAIN INITIALIZATION
// ============================================================================
/**
* Main initialization function
* Initializes all modules when DOM is ready
*/
function initializeAll() {
try {
// Initialize core MediaWiki modules
TabNavigation.init();
NavigationHandlers.init();
WikiLinkProcessor.init();
MobileInterface.init();
CollapsibleSections.init();
SidebarManager.init();
GuardianDecisionHelper.init();
UserMenuManager.init();
ResponsiveTableManager.init();
// Initialize Quest Tracker module
QuestTracker.init();
// Initialize Back to Top module
BackToTop.init();
console.log('MediaWiki Common JavaScript with Quest Tracker and Back to Top initialized successfully');
} catch (error) {
console.error('Error initializing MediaWiki Common JavaScript:', error);
}
}
// Initialize when DOM is ready
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', initializeAll);
} else {
initializeAll();
}
// Make Quest Tracker functions globally available for backward compatibility
window.initQuestTracker = QuestTracker.init.bind(QuestTracker);
window.loadQuestProgress = QuestTracker.loadQuestProgress.bind(QuestTracker);
window.saveQuestProgress = QuestTracker.saveQuestProgress.bind(QuestTracker);
window.updateProgressDisplay = QuestTracker.updateProgressDisplay.bind(QuestTracker);
window.resetAllProgress = QuestTracker.resetAllProgress.bind(QuestTracker);
})();