MediaWiki:Common.js: Difference between revisions

From CoraTO Wiki - Official Wiki
Jump to navigation Jump to search
No edit summary
No edit summary
Line 359: Line 359:
         targetContent.classList.add(CONFIG.CLASSES.ACTIVE);
         targetContent.classList.add(CONFIG.CLASSES.ACTIVE);
          
          
         // If this is the Normal Daily tab, activate the first nested tab
         // If this tab has nested tabs, activate the first nested tab
         if (tabId === 'tab-normal-daily') {
         if (tabId === 'tab-normal-daily' || tabId === 'tab-shadow-dailies' || tabId === 'tab-shaman-girl-jia') {
           setTimeout(() => {
           setTimeout(() => {
             const firstNestedTab = targetContent.querySelector('.nested-tab');
             const firstNestedTab = targetContent.querySelector('.nested-tab');

Revision as of 17:15, 21 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 tab has nested tabs, activate the first nested tab
        if (tabId === 'tab-normal-daily' || tabId === 'tab-shadow-dailies' || tabId === 'tab-shaman-girl-jia') {
          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);

})();