Tags: Manual revert Reverted |
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 );
| |
| } );
| |
|
| |
| }() );
| |