MediaWiki:Common.js: Difference between revisions

From CoraTO Wiki - Official Wiki
Jump to navigation Jump to search
No edit summary
Tag: Reverted
Blanked the page
Tags: Blanking Manual revert Reverted
Line 1: Line 1:
/*
Propósito da funcionalidade:
- Criar e controlar um botão “voltar ao topo” que aparece após rolar a página.
- Exibir/ocultar com classes (`show` e `pulse`) e realizar rolagem suave até o topo ao clicar.
- Inserir automaticamente o botão no `document.body` e registrar listeners de `scroll` (passivo) e `click`.
*/


(function () {
  'use strict';
  const CONFIG = {
    CLASSES: {
      SHOW: 'show',
      PULSE: 'pulse'
    }
  };
  const BackToTop = {
    button: null,
    isVisible: false,
    scrollThreshold: 300,
    createButton: function () {
      const button = document.createElement('button');
      button.className = 'btt-button';
      button.setAttribute('aria-label', 'Voltar ao topo');
      button.setAttribute('title', 'Voltar ao topo');
      document.body.appendChild(button);
      return button;
    },
    showButton: function () {
      if (!this.isVisible && this.button) {
        this.button.classList.add(CONFIG.CLASSES.SHOW);
        this.button.classList.add(CONFIG.CLASSES.PULSE);
        this.isVisible = true;
        setTimeout(() => {
          if (this.button) {
            this.button.classList.remove(CONFIG.CLASSES.PULSE);
          }
        }, 6000);
      }
    },
    hideButton: function () {
      if (this.isVisible && this.button) {
        this.button.classList.remove(CONFIG.CLASSES.SHOW);
        this.button.classList.remove(CONFIG.CLASSES.PULSE);
        this.isVisible = false;
      }
    },
    handleScroll: function () {
      const scrollTop = window.pageYOffset || document.documentElement.scrollTop;
      if (scrollTop > this.scrollThreshold) {
        this.showButton();
      } else {
        this.hideButton();
      }
    },
    handleClick: function () {
      window.scrollTo({
        top: 0,
        behavior: 'smooth'
      });
    },
    init: function () {
      this.button = this.createButton();
      const boundHandleScroll = this.handleScroll.bind(this);
      const boundHandleClick = this.handleClick.bind(this);
      window.addEventListener('scroll', boundHandleScroll, { passive: true });
      this.button.addEventListener('click', boundHandleClick);
      this.handleScroll();
      console.log('Back to Top button initialized');
    }
  };
  if (document.readyState === 'loading') {
    document.addEventListener('DOMContentLoaded', BackToTop.init.bind(BackToTop));
  } else {
    BackToTop.init();
  }
})();
/*
Propósito da funcionalidade:
- Permitir navegação por clique em “cards” que tenham atributo `data-link`.
- Suportar três tipos de destino:
  - Âncoras internas (ex.: `#secao`) com rolagem suave.
  - URLs externas absolutas (`http://` ou `https://`).
  - Títulos de páginas MediaWiki, resolvidos via `mw.util.getUrl()` quando disponível.
*/
(function () {
  'use strict';
  const CONFIG = {
    SELECTORS: {
      CARD_LINKS: '.card[data-link], .destaque-card[data-link]'
    }
  };
  function safeQuerySelector(selector, context = document) {
    try {
      return context.querySelector(selector);
    } catch (error) {
      console.warn(`Invalid selector '${selector}':`, error);
      return null;
    }
  }
  const NavigationHandlers = {
    handleCardClick: function (event) {
      const card = event.target.closest(CONFIG.SELECTORS.CARD_LINKS);
      if (!card) return;
      const link = card.getAttribute('data-link');
      if (!link) return;
      if (link.charAt(0) === '#') {
        event.preventDefault();
        const anchorElement = safeQuerySelector(link);
        if (anchorElement) {
          anchorElement.scrollIntoView({ behavior: 'smooth', block: 'start' });
        }
        return;
      }
      if (/^https?:\/\//i.test(link)) {
        window.location.href = link;
        return;
      }
      const targetUrl = (window.mw && window.mw.util && typeof window.mw.util.getUrl === 'function')
        ? window.mw.util.getUrl(link)
        : ('index.php?title=' + encodeURIComponent(link));
      window.location.href = targetUrl;
    },
    init: function () {
      document.addEventListener('click', this.handleCardClick, false);
    }
  };
  if (document.readyState === 'loading') {
    document.addEventListener('DOMContentLoaded', NavigationHandlers.init.bind(NavigationHandlers));
  } else {
    NavigationHandlers.init();
  }
})();
/*
Propósito da funcionalidade:
- Criar um menu “hamburger” para abrir/fechar a sidebar no skin `vector-legacy`.
- Alternar classes (`mobile-open`, `sidebar-open`, `active`) para controlar layout em mobile.
- Fechar a sidebar ao clicar fora do painel ou ao pressionar `Escape`.
*/
(function () {
  'use strict';
  const CONFIG = {
    CLASSES: {
      ACTIVE: 'active',
      MOBILE_OPEN: 'mobile-open',
      SIDEBAR_OPEN: 'sidebar-open'
    }
  };
  function safeGetElementById(id) {
    try {
      return document.getElementById(id);
    } catch (error) {
      console.warn(`Element with ID '${id}' not found:`, error);
      return null;
    }
  }
  function safeQuerySelector(selector, context = document) {
    try {
      return context.querySelector(selector);
    } catch (error) {
      console.warn(`Invalid selector '${selector}':`, error);
      return null;
    }
  }
  const MobileInterface = {
    initialized: false,
    getPanel: function () {
      return safeGetElementById('mw-panel');
    },
    isApplicable: function () {
      return document.body.classList.contains('skin-vector-legacy') && !!this.getPanel();
    },
    createHamburgerMenu: function () {
      if (this.initialized) return;
      if (!this.isApplicable()) {
        return;
      }
      if (document.querySelector('.mobile-hamburger-menu')) {
        this.initialized = true;
        return;
      }
      const panel = this.getPanel();
      if (!panel) return;
      const hamburger = document.createElement('button');
      hamburger.className = 'mobile-hamburger-menu';
      hamburger.setAttribute('aria-label', 'Toggle navigation menu');
      hamburger.setAttribute('type', 'button');
      hamburger.innerHTML = '<span></span><span></span><span></span>';
      const content = safeGetElementById('content') || safeQuerySelector('.mw-body');
      if (content && content.parentNode) {
        content.parentNode.insertBefore(hamburger, content);
      }
      const toggleSidebar = () => {
        panel.classList.toggle(CONFIG.CLASSES.MOBILE_OPEN);
        hamburger.classList.toggle(CONFIG.CLASSES.ACTIVE);
        document.body.classList.toggle(CONFIG.CLASSES.SIDEBAR_OPEN);
        try {
          const open = panel.classList.contains(CONFIG.CLASSES.MOBILE_OPEN);
          document.dispatchEvent(new CustomEvent('cora:mobile-sidebar-toggle', { detail: { open } }));
        } catch (e) {
          document.dispatchEvent(new Event('cora:mobile-sidebar-toggle'));
        }
      };
      hamburger.addEventListener('click', (event) => {
        event.preventDefault();
        event.stopPropagation();
        toggleSidebar();
      });
      document.addEventListener('click', (event) => {
        if (panel.classList.contains(CONFIG.CLASSES.MOBILE_OPEN) &&
          !panel.contains(event.target) &&
          !hamburger.contains(event.target)) {
          panel.classList.remove(CONFIG.CLASSES.MOBILE_OPEN);
          hamburger.classList.remove(CONFIG.CLASSES.ACTIVE);
          document.body.classList.remove(CONFIG.CLASSES.SIDEBAR_OPEN);
        }
      });
      document.addEventListener('keydown', (event) => {
        if (event.key === 'Escape' && panel.classList.contains(CONFIG.CLASSES.MOBILE_OPEN)) {
          panel.classList.remove(CONFIG.CLASSES.MOBILE_OPEN);
          hamburger.classList.remove(CONFIG.CLASSES.ACTIVE);
          document.body.classList.remove(CONFIG.CLASSES.SIDEBAR_OPEN);
        }
      });
      this.initialized = true;
    },
    init: function () {
      this.createHamburgerMenu();
    }
  };
  if (document.readyState === 'loading') {
    document.addEventListener('DOMContentLoaded', MobileInterface.init.bind(MobileInterface));
  } else {
    MobileInterface.init();
  }
})();
/*
Propósito da funcionalidade:
- Tornar os portais/menus laterais do skin `vector-legacy` colapsáveis.
- Colapsar automaticamente todas as seções do painel (exceto a primeira) ao iniciar.
- Persistir estado expandido/colapsado por seção via `localStorage` usando prefixo `sidebar-<id>`.
- Ajustar alturas (`maxHeight`) ao expandir/colapsar e recalcular em `resize`.
*/
(function () {
  'use strict';
  const CONFIG = {
    CLASSES: {
      COLLAPSED: 'collapsed'
    },
    STORAGE_KEYS: {
      SIDEBAR_PREFIX: 'sidebar-'
    }
  };
  function ensureSidebarStyles() {
    if (document.getElementById('cora-sidebar-collapsible-styles')) return;
    const style = document.createElement('style');
    style.id = 'cora-sidebar-collapsible-styles';
    style.textContent =
      'body.skin-vector-legacy #mw-panel .vector-menu-content,body.skin-vector-legacy #mw-panel .portal .body{overflow:hidden;transition:max-height .4s ease,opacity .3s ease;opacity:1}' +
      'body.skin-vector-legacy #mw-panel .vector-menu-portal.collapsed .vector-menu-content,body.skin-vector-legacy #mw-panel .portal.collapsed .body{max-height:0!important;opacity:0;padding:0}' +
      'body.skin-vector-legacy #mw-panel .vector-menu-portal .vector-menu-heading,body.skin-vector-legacy #mw-panel .portal h3{cursor:pointer;user-select:none}';
    document.head.appendChild(style);
  }
  function safeGetElementById(id) {
    try {
      return document.getElementById(id);
    } catch (error) {
      console.warn(`Element with ID '${id}' not found:`, error);
      return null;
    }
  }
  function safeQuerySelectorAll(selector, context = document) {
    try {
      return context.querySelectorAll(selector);
    } catch (error) {
      console.warn(`Invalid selector '${selector}':`, error);
      return [];
    }
  }
  function safeGetLocalStorage(key, defaultValue = '') {
    try {
      return localStorage.getItem(key) || defaultValue;
    } catch (error) {
      console.warn(`localStorage access failed for key '${key}':`, error);
      return defaultValue;
    }
  }
  function safeSetLocalStorage(key, value) {
    try {
      localStorage.setItem(key, value);
      return true;
    } catch (error) {
      console.warn(`localStorage write failed for key '${key}':`, error);
      return false;
    }
  }
  const SidebarManager = {
    portals: null,
    getPortalId: function (portal) {
      const heading = portal.querySelector('.vector-menu-heading, h3');
      if (heading) {
        return heading.textContent.trim().toLowerCase().replace(/\s+/g, '-');
      }
      return 'unknown-portal';
    },
    syncExpandedHeights: function () {
      if (!this.portals) return;
      this.portals.forEach(portal => {
        if (portal.classList.contains(CONFIG.CLASSES.COLLAPSED)) return;
        const content = portal.querySelector('.vector-menu-content, .body');
        if (!content) return;
        content.style.maxHeight = content.scrollHeight + 'px';
        content.style.opacity = '1';
      });
    },
    initSidebarCollapsible: function () {
      if (!document.body.classList.contains('skin-vector-legacy')) {
        return;
      }
      const panel = safeGetElementById('mw-panel');
      if (!panel) return;
      ensureSidebarStyles();
      const portals = safeQuerySelectorAll('.vector-menu-portal, .portal', panel);
      this.portals = Array.from(portals);
      portals.forEach((portal, index) => {
        if (index > 0) {
          portal.classList.add(CONFIG.CLASSES.COLLAPSED);
          const content = portal.querySelector('.vector-menu-content, .body');
          if (content) {
            content.style.maxHeight = '0';
            content.style.opacity = '0';
          }
        }
      });
      const headings = safeQuerySelectorAll('.vector-menu-heading, .portal h3', panel);
      headings.forEach(heading => {
        heading.addEventListener('click', (event) => {
          event.preventDefault();
          event.stopPropagation();
          const portal = heading.closest('.vector-menu-portal, .portal');
          if (!portal) return;
          const content = portal.querySelector('.vector-menu-content, .body');
          if (!content) return;
          const isCollapsed = portal.classList.contains(CONFIG.CLASSES.COLLAPSED);
          const portalId = this.getPortalId(portal);
          if (isCollapsed) {
            portal.classList.remove(CONFIG.CLASSES.COLLAPSED);
            content.style.maxHeight = content.scrollHeight + 'px';
            content.style.opacity = '1';
            safeSetLocalStorage(CONFIG.STORAGE_KEYS.SIDEBAR_PREFIX + portalId, 'expanded');
          } else {
            portal.classList.add(CONFIG.CLASSES.COLLAPSED);
            content.style.maxHeight = '0';
            content.style.opacity = '0';
            safeSetLocalStorage(CONFIG.STORAGE_KEYS.SIDEBAR_PREFIX + portalId, 'collapsed');
          }
        });
      });
      portals.forEach((portal, index) => {
        if (index === 0) return;
        const savedState = safeGetLocalStorage(CONFIG.STORAGE_KEYS.SIDEBAR_PREFIX + this.getPortalId(portal));
        const content = portal.querySelector('.vector-menu-content, .body');
        if (savedState === 'expanded' && content) {
          portal.classList.remove(CONFIG.CLASSES.COLLAPSED);
          content.style.maxHeight = content.scrollHeight + 'px';
          content.style.opacity = '1';
        }
      });
      window.addEventListener('resize', () => {
        this.syncExpandedHeights();
      });
      document.addEventListener('cora:mobile-sidebar-toggle', () => {
        this.syncExpandedHeights();
      });
    },
    init: function () {
      this.initSidebarCollapsible();
    }
  };
  if (document.readyState === 'loading') {
    document.addEventListener('DOMContentLoaded', SidebarManager.init.bind(SidebarManager));
  } else {
    SidebarManager.init();
  }
})();
/*
Propósito da funcionalidade:
- Abrir imagens em um “lightbox” (overlay) ao clicar, permitindo ampliar e visualizar com foco.
- Fechar ao clicar no fundo do overlay ou ao pressionar `Escape`.
- Bloquear rolagem do body enquanto o overlay estiver aberto e restaurar ao fechar.
- Exibir legenda baseada em `.image-container .image-caption` ou no `alt` da imagem.
- Adaptar a cor do fundo e da legenda ao tema atual via atributo `data-theme` no `<html>`.
*/
(function () {
  'use strict';
  const ImageLightbox = {
    overlay: null,
    bodyOverflow: '',
    init: function () {
      document.addEventListener('click', this.handleDocumentClick.bind(this), false);
    },
    shouldIgnoreClick: function (target) {
      if (!target || !target.closest) return false;
      if (target.closest('a[href]')) return true;
      if (target.closest('.nav-tabs, .nav-tab, .nav-tab-fgod, .nested-tabs, .nested-tab')) return true;
      return false;
    },
    buildOverlay: function () {
      const overlay = document.createElement('div');
      overlay.className = 'image-lightbox-overlay';
      const theme = document.documentElement.getAttribute('data-theme') || 'light';
      const bg = theme === 'dark' ? 'rgba(0,0,0,0.85)' : 'rgba(0,0,0,0.7)';
      overlay.style.position = 'fixed';
      overlay.style.top = '0';
      overlay.style.right = '0';
      overlay.style.bottom = '0';
      overlay.style.left = '0';
      overlay.style.display = 'flex';
      overlay.style.alignItems = 'center';
      overlay.style.justifyContent = 'center';
      overlay.style.background = bg;
      overlay.style.zIndex = '9999';
      overlay.style.padding = '2vw';
      overlay.style.cursor = 'zoom-out';
      overlay.setAttribute('role', 'dialog');
      overlay.setAttribute('aria-modal', 'true');
      overlay.addEventListener('click', (e) => { if (e.target === overlay) this.close(); });
      document.addEventListener('keydown', this.handleKeydown);
      return overlay;
    },
    handleKeydown: function (e) {
      if (e.key === 'Escape') {
        const self = ImageLightbox;
        if (self.overlay) { self.close(); }
      }
    },
    findImageFromTarget: function (target) {
      const el = target.closest('img, .image-container, .image-grid img');
      if (!el) return null;
      if (el.tagName && el.tagName.toLowerCase() === 'img') return el;
      const img = el.querySelector('img');
      return img || null;
    },
    handleDocumentClick: function (e) {
      if (this.overlay && this.overlay.contains(e.target)) return;
      if (this.shouldIgnoreClick(e.target)) return;
      const img = this.findImageFromTarget(e.target);
      if (!img) return;
      e.preventDefault();
      e.stopPropagation();
      this.open(img);
    },
    open: function (img) {
      if (this.overlay) return;
      this.overlay = this.buildOverlay();
      this.bodyOverflow = document.body.style.overflow || '';
      document.body.style.overflow = 'hidden';
      const content = document.createElement('div');
      content.style.position = 'relative';
      content.style.maxWidth = '90vw';
      content.style.maxHeight = '90vh';
      const clone = document.createElement('img');
      clone.src = img.currentSrc || img.src;
      clone.alt = img.alt || '';
      clone.style.maxWidth = '90vw';
      clone.style.maxHeight = '90vh';
      clone.style.borderRadius = '12px';
      clone.style.boxShadow = '0 10px 30px rgba(0,0,0,0.25)';
      clone.style.transform = 'scale(0.98)';
      clone.style.opacity = '0';
      clone.style.transition = 'transform 150ms ease, opacity 150ms ease';
      clone.style.willChange = 'transform, opacity';
      content.appendChild(clone);
      const captionText = this.getCaptionText(img);
      if (captionText) {
        const caption = document.createElement('div');
        caption.textContent = captionText;
        caption.style.marginTop = '0.75rem';
        caption.style.fontSize = '0.9rem';
        caption.style.textAlign = 'center';
        const theme = document.documentElement.getAttribute('data-theme') || 'light';
        caption.style.color = theme === 'dark' ? '#a8c6e8' : '#666';
        content.appendChild(caption);
      }
      this.overlay.appendChild(content);
      document.body.appendChild(this.overlay);
      requestAnimationFrame(() => {
        clone.style.transform = 'scale(1.25)';
        clone.style.opacity = '1';
      });
    },
    getCaptionText: function (img) {
      const container = img.closest('.image-container');
      const capEl = container ? container.querySelector('.image-caption') : null;
      if (capEl && capEl.textContent) {
        return capEl.textContent.trim();
      }
      return img.alt ? img.alt.trim() : '';
    },
    close: function () {
      if (!this.overlay) return;
      document.removeEventListener('keydown', this.handleKeydown);
      if (this.overlay.parentNode) this.overlay.parentNode.removeChild(this.overlay);
      this.overlay = null;
      document.body.style.overflow = this.bodyOverflow;
    }
  };
  if (document.readyState === 'loading') {
    document.addEventListener('DOMContentLoaded', ImageLightbox.init.bind(ImageLightbox));
  } else {
    ImageLightbox.init();
  }
})();
( function () {
'use strict';
const CONFIG = {
CLASSES: {
EXPANDED: 'expanded'
},
STORAGE_KEYS: {
USER_MENU_STATE: 'user-menu-state',
PERSONAL_MENU_STATE: 'personal-menu-state'
},
BODY_CLASSES: {
HAS_USER_MENU: 'has-user-menu-collapsible',
HAS_PERSONAL_MENU: 'has-personal-menu-collapsible'
}
};
/**
* @typedef {Object} CollapsibleMenuOptions
* @property {string} containerId
* @property {string} containerClass
* @property {string} headerClass
* @property {string} contentClass
* @property {string} listClass
* @property {string} headerText
* @property {string} stateStorageKey
* @property {string} bodyClass
* @property {string[]} itemIds
*/
/**
* @param {string} id
* @return {HTMLElement|null}
*/
function safeGetElementById( id ) {
try {
return document.getElementById( id );
} catch ( error ) {
return null;
}
}
/**
* @param {string} key
* @param {string} [defaultValue]
* @return {string}
*/
function safeGetLocalStorage( key, defaultValue = '' ) {
try {
return localStorage.getItem( key ) || defaultValue;
} catch ( error ) {
return defaultValue;
}
}
/**
* @param {string} key
* @param {string} value
* @return {boolean}
*/
function safeSetLocalStorage( key, value ) {
try {
localStorage.setItem( key, value );
return true;
} catch ( error ) {
return false;
}
}
/**
* @param {CollapsibleMenuOptions} options
* @return {HTMLElement|null}
*/
function buildCollapsibleMenu( options ) {
const existing = safeGetElementById( options.containerId );
if ( existing ) {
return existing;
}
const items = options.itemIds
.map( ( id ) => safeGetElementById( id ) )
.filter( Boolean );
if ( items.length === 0 ) {
return null;
}
const menuContainer = document.createElement( 'div' );
menuContainer.id = options.containerId;
menuContainer.className = options.containerClass;
const menuHeader = document.createElement( 'div' );
menuHeader.className = options.headerClass;
menuHeader.textContent = options.headerText;
menuHeader.addEventListener( 'click', () => {
menuContainer.classList.toggle( CONFIG.CLASSES.EXPANDED );
const state = menuContainer.classList.contains( CONFIG.CLASSES.EXPANDED ) ?
'expanded' :
'collapsed';
safeSetLocalStorage( options.stateStorageKey, state );
} );
const menuContent = document.createElement( 'div' );
menuContent.className = options.contentClass;
const menuList = document.createElement( 'ul' );
menuList.className = options.listClass;
items.forEach( ( item ) => {
if ( item && item.parentNode ) {
menuList.appendChild( item );
}
} );
menuContent.appendChild( menuList );
menuContainer.appendChild( menuHeader );
menuContainer.appendChild( menuContent );
document.body.appendChild( menuContainer );
document.body.classList.add( options.bodyClass );
const savedState = safeGetLocalStorage( options.stateStorageKey );
if ( savedState === 'expanded' ) {
menuContainer.classList.add( CONFIG.CLASSES.EXPANDED );
}
document.addEventListener( 'click', ( event ) => {
const target = event.target;
if (
menuContainer.classList.contains( CONFIG.CLASSES.EXPANDED ) &&
target instanceof Node &&
!menuContainer.contains( target )
) {
menuContainer.classList.remove( CONFIG.CLASSES.EXPANDED );
safeSetLocalStorage( options.stateStorageKey, 'collapsed' );
}
} );
menuContainer.addEventListener( 'mouseenter', () => {
menuContainer.classList.add( CONFIG.CLASSES.EXPANDED );
} );
menuContainer.addEventListener( 'mouseleave', () => {
menuContainer.classList.remove( CONFIG.CLASSES.EXPANDED );
safeSetLocalStorage( options.stateStorageKey, 'collapsed' );
} );
return menuContainer;
}
const UserMenuManager = {
initialized: false,
initUserMenuCollapsible: function () {
if ( this.initialized ) {
return;
}
if ( !document.body.classList.contains( 'skin-vector-legacy' ) ) {
return;
}
const personalTools = safeGetElementById( 'p-personal' );
if ( !personalTools ) {
return;
}
const isLoggedIn = !!safeGetElementById( 'pt-userpage' );
const isAnonymous = !isLoggedIn && !!safeGetElementById( 'pt-login' );
if ( isLoggedIn ) {
buildCollapsibleMenu( {
containerId: 'user-menu-collapsible',
containerClass: 'user-menu-container',
headerClass: 'user-menu-header',
contentClass: 'user-menu-content',
listClass: 'user-menu-list',
headerText: 'User',
stateStorageKey: CONFIG.STORAGE_KEYS.USER_MENU_STATE,
bodyClass: CONFIG.BODY_CLASSES.HAS_USER_MENU,
itemIds: [
'pt-userpage', 'pt-mytalk', 'pt-preferences',
'pt-watchlist', 'pt-mycontris', 'pt-logout'
]
} );
}
if ( isAnonymous ) {
buildCollapsibleMenu( {
containerId: 'personal-menu-collapsible',
containerClass: 'personal-menu-container',
headerClass: 'personal-menu-header',
contentClass: 'personal-menu-content',
listClass: 'personal-menu-list',
headerText: 'User',
stateStorageKey: CONFIG.STORAGE_KEYS.PERSONAL_MENU_STATE,
bodyClass: CONFIG.BODY_CLASSES.HAS_PERSONAL_MENU,
itemIds: [
'pt-login', 'pt-createaccount', 'pt-anonuserpage', 'pt-anontalk'
]
} );
}
this.initialized = true;
},
init: function () {
this.initUserMenuCollapsible();
}
};
if ( document.readyState === 'loading' ) {
document.addEventListener( 'DOMContentLoaded', UserMenuManager.init.bind( UserMenuManager ) );
} else {
UserMenuManager.init();
}
}() );
/*
Propósito da funcionalidade:
- Converter marcação de links no estilo MediaWiki escrita como texto (ex.: `[[Titulo]]` ou `[[Titulo|Texto]]`)
  em links HTML reais (`<a href="...">`).
- Ignorar contextos onde essa conversão não deve acontecer (scripts, editores, formulários, etc.).
- Preservar links `File:`/`Image:` como texto, sem conversão.
- Gerar URLs usando `mw.util.getUrl()` quando disponível, com fallback para `index.php?title=...`.
*/
(function () {
  'use strict';
  function isInExcludedContext(node) {
    if (!node || !node.parentElement) return false;
    const excludedSelectors = [
      'script', 'style', 'textarea', 'input', 'select', 'option',
      '.ve-ui-surface', '.mw-editform', '.CodeMirror', '.cm-editor', '.ace_editor'
    ].join(', ');
    return !!node.parentElement.closest(excludedSelectors);
  }
  const WikiLinkProcessor = {
    processWikiLinks: function () {
      if (!document.body || document.body.textContent.indexOf('[[') === -1) {
        return;
      }
      const walker = document.createTreeWalker(
        document.body,
        NodeFilter.SHOW_TEXT,
        null,
        false
      );
      const textNodes = [];
      let node;
      while ((node = walker.nextNode())) {
        const value = node.nodeValue;
        if (!value || value.indexOf('[[') === -1 || isInExcludedContext(node)) {
          continue;
        }
        textNodes.push(node);
      }
      const linkRegex = /\[\[([^\|\]]+)(?:\|([^\]]+))?\]\]/g;
      textNodes.forEach(textNode => {
        const sourceText = textNode.nodeValue;
        if (!linkRegex.test(sourceText)) return;
        linkRegex.lastIndex = 0;
        const parent = textNode.parentNode;
        const fragment = document.createDocumentFragment();
        let lastIndex = 0;
        let match;
        while ((match = linkRegex.exec(sourceText)) !== null) {
          if (match.index > lastIndex) {
            fragment.appendChild(
              document.createTextNode(sourceText.slice(lastIndex, match.index))
            );
          }
          const rawTitle = (match[1] || '').trim();
          const displayText = (match[2] != null ? match[2] : rawTitle).trim();
          if (/^(File:|Image:)/i.test(rawTitle)) {
            fragment.appendChild(document.createTextNode(match[0]));
          } else {
            const cleanTitle = rawTitle.charAt(0) === ':' ? rawTitle.slice(1) : rawTitle;
            const href = (window.mw && window.mw.util && typeof window.mw.util.getUrl === 'function')
              ? window.mw.util.getUrl(cleanTitle)
              : ('index.php?title=' + encodeURIComponent(cleanTitle));
            const anchor = document.createElement('a');
            anchor.className = 'mw-link-internal';
            anchor.setAttribute('href', href);
            anchor.appendChild(document.createTextNode(displayText));
            fragment.appendChild(anchor);
          }
          lastIndex = linkRegex.lastIndex;
        }
        if (lastIndex < sourceText.length) {
          fragment.appendChild(
            document.createTextNode(sourceText.slice(lastIndex))
          );
        }
        parent.insertBefore(fragment, textNode);
        parent.removeChild(textNode);
      });
    },
    init: function () {
      if (document.readyState === 'loading') {
        document.addEventListener('DOMContentLoaded', this.processWikiLinks);
      } else {
        this.processWikiLinks();
      }
    }
  };
  WikiLinkProcessor.init();
})();
importScript('MediaWiki:MeuScript.js');
( function () {
'use strict';
function ensureThemeStylesLoaded() {
try {
if ( !window.mw || !mw.loader || !mw.config ) {
return;
}
const skin = mw.config.get( 'skin' ) || 'vector';
const moduleName = 'themeloader.skins.' + skin + '.default';
if ( typeof mw.loader.getState === 'function' ) {
const state = mw.loader.getState( moduleName );
if ( state === null ) {
return;
}
if ( state === 'ready' || state === 'loading' ) {
return;
}
}
mw.loader.load( moduleName );
} catch ( e ) {
// Ignore error
}
}
function getSavedTheme() {
try {
return localStorage.getItem( 'mw-theme' ) || 'default';
} catch ( e ) {
return 'default';
}
}
// Define available themes matching christmas.css roots
const themes = [
{ name: 'Pink', id: 'default' },
{ name: 'Dark', id: 'dark-neutral' },
{ name: 'Christmas', id: 'christmas' },
{ name: 'Power Light', id: 'power-light' },
{ name: 'Magic Light', id: 'magic-light' },
{ name: 'Sense Light', id: 'sense-light' },
{ name: 'Charm Light', id: 'charm-light' },
{ name: 'Power Dark', id: 'power-dark' },
{ name: 'Charm Dark', id: 'charm-dark' },
{ name: 'Magic Dark', id: 'dark-lightBlue' },
{ name: 'Sense Dark', id: 'dark-purple' },
];
// Function to apply theme
function applyTheme( themeId ) {
ensureThemeStylesLoaded();
try {
localStorage.setItem( 'mw-theme', themeId );
} catch ( e ) {
// Ignore error
}
if ( themeId === 'default' ) {
document.documentElement.removeAttribute( 'data-theme' );
} else {
document.documentElement.setAttribute( 'data-theme', themeId );
}
}
// Initialize theme immediately
ensureThemeStylesLoaded();
applyTheme( getSavedTheme() );
// Wait for DOM
const domReady = function ( callback ) {
if ( document.readyState === 'loading' ) {
document.addEventListener( 'DOMContentLoaded', callback );
} else {
setTimeout( callback, 0 );
}
};
domReady( function () {
// Prevent duplicate injection
if ( document.getElementById( 'mw-theme-floating' ) ) {
return;
}
// Create Floating Container
// ID matches CSS: #mw-theme-floating
const container = document.createElement( 'div' );
container.id = 'mw-theme-floating';
// Create Inner Wrapper
// Class matches CSS: .themeMenu
const themeMenuDiv = document.createElement( 'div' );
themeMenuDiv.className = 'themeMenu';
// Create Toggle Button
// Class matches CSS: .themeMenu-toggle
const btn = document.createElement( 'button' );
btn.className = 'themeMenu-toggle';
btn.textContent = 'Appearence';
btn.title = 'Change appearence';
btn.type = 'button';
// Create Dropdown List
// Class matches CSS: .themeMenu-dropdown
const dropdown = document.createElement( 'ul' );
dropdown.className = 'themeMenu-dropdown';
// Populate Items
themes.forEach( function ( theme ) {
// Item Wrapper
// CSS doesn't strictly require this wrapper for floating,
// but theme-menu.css might use .themeMenu-itemWrap
const itemWrap = document.createElement( 'li' );
itemWrap.className = 'themeMenu-itemWrap';
const themeLink = document.createElement( 'a' );
themeLink.className = 'themeMenu-item';
themeLink.href = '#';
themeLink.textContent = theme.name;
themeLink.dataset.themeId = theme.id;
// Highlight current
const currentTheme = getSavedTheme();
if ( theme.id === currentTheme ) {
themeLink.classList.add( 'is-current' );
}
themeLink.addEventListener( 'click', function ( e ) {
e.preventDefault();
ensureThemeStylesLoaded();
applyTheme( theme.id );
// Update highlighting
dropdown.querySelectorAll( '.themeMenu-item' ).forEach( function ( link ) {
link.classList.remove( 'is-current' );
} );
themeLink.classList.add( 'is-current' );
// Close menu
themeMenuDiv.classList.remove( 'is-open' );
} );
itemWrap.appendChild( themeLink );
dropdown.appendChild( itemWrap );
} );
// Toggle Logic
// CSS uses .themeMenu.is-open .themeMenu-dropdown { display: block }
btn.addEventListener( 'click', function ( e ) {
e.preventDefault();
e.stopPropagation();
themeMenuDiv.classList.toggle( 'is-open' );
} );
// Click Outside Logic
document.addEventListener( 'click', function ( e ) {
if ( !themeMenuDiv.contains( e.target ) ) {
themeMenuDiv.classList.remove( 'is-open' );
}
} );
// Assemble
themeMenuDiv.appendChild( btn );
themeMenuDiv.appendChild( dropdown );
container.appendChild( themeMenuDiv );
// Inject into Body
document.body.appendChild( container );
} );
}() );

Revision as of 17:59, 2 January 2026