|
|
| Line 192: |
Line 192: |
| } ); | | } ); |
| } ); | | } ); |
| /* Popout menus (header) */
| |
|
| |
| /* eslint-disable no-jquery/no-fade */
| |
|
| |
| $( function () {
| |
| var toggleTime = 200;
| |
|
| |
| // Open the various menus
| |
| $( '#user-tools h2' ).on( 'click', function () {
| |
| if ( $( window ).width() < 851 ) {
| |
| $( '#personal-inner, #menus-cover' ).fadeToggle( toggleTime );
| |
| }
| |
| } );
| |
| $( '#site-navigation h2' ).on( 'click', function () {
| |
| if ( $( window ).width() < 851 ) {
| |
| $( '#site-navigation .sidebar-inner, #menus-cover' ).fadeToggle( toggleTime );
| |
| }
| |
| } );
| |
| $( '#site-tools h2' ).on( 'click', function () {
| |
| if ( $( window ).width() < 851 ) {
| |
| $( '#site-tools .sidebar-inner, #menus-cover' ).fadeToggle( toggleTime );
| |
| }
| |
| } );
| |
| $( '#ca-more' ).on( 'click', function () {
| |
| $( '#page-tools .sidebar-inner' ).css( 'top', $( '#ca-more' ).offset().top + 25 );
| |
| if ( $( window ).width() < 851 ) {
| |
| $( '#page-tools .sidebar-inner, #menus-cover' ).fadeToggle( toggleTime );
| |
| }
| |
| } );
| |
| $( '#ca-languages' ).on( 'click', function () {
| |
| $( '#other-languages .sidebar-inner' ).css( 'top', $( '#ca-languages' ).offset().top + 25 );
| |
| if ( $( window ).width() < 851 ) {
| |
| $( '#other-languages .sidebar-inner, #menus-cover' ).fadeToggle( toggleTime );
| |
| }
| |
| } );
| |
|
| |
| // Close menus on click outside
| |
| $( document ).on( 'click touchstart', function ( e ) {
| |
| if ( $( e.target ).closest( '#menus-cover' ).length > 0 ) {
| |
| $( '#personal-inner' ).fadeOut( toggleTime );
| |
| $( '.sidebar-inner' ).fadeOut( toggleTime );
| |
| $( '#menus-cover' ).fadeOut( toggleTime );
| |
| }
| |
| } );
| |
| } );
| |
|
| |
| // Adiciona efeitos de interação
| |
| document.addEventListener('DOMContentLoaded', function() {
| |
| const guideItems = document.querySelectorAll('.mw-guide-item');
| |
|
| |
| guideItems.forEach(item => {
| |
| item.addEventListener('mouseenter', function() {
| |
| this.style.transform = 'translateY(-3px)';
| |
| });
| |
|
| |
| item.addEventListener('mouseleave', function() {
| |
| this.style.transform = 'translateY(0)';
| |
| });
| |
|
| |
| item.addEventListener('click', function(e) {
| |
| // Adiciona efeito de clique
| |
| this.style.transform = 'translateY(1px)';
| |
| setTimeout(() => {
| |
| this.style.transform = 'translateY(-3px)';
| |
| }, 100);
| |
| });
| |
| });
| |
| });
| |
| // JavaScript para Cards com Imagem - compatível com sistema existente
| |
| $(document).ready(function() {
| |
|
| |
| // Função para gerenciar cards com imagem (reutiliza lógica existente)
| |
| function initImageCards() {
| |
| const imageCards = $('.mw-image-card');
| |
|
| |
| // Aplica os mesmos efeitos dos guias existentes
| |
| imageCards.each(function() {
| |
| const $card = $(this);
| |
|
| |
| // Efeitos hover melhorados
| |
| $card.on('mouseenter', function() {
| |
| $(this).css({
| |
| 'transform': 'translateY(-8px) scale(1.02)',
| |
| 'transition': 'all 0.3s ease'
| |
| });
| |
| });
| |
|
| |
| $card.on('mouseleave', function() {
| |
| $(this).css({
| |
| 'transform': 'translateY(0) scale(1)',
| |
| 'transition': 'all 0.3s ease'
| |
| });
| |
| });
| |
|
| |
| // Efeito de clique
| |
| $card.on('click', function() {
| |
| $(this).css('transform', 'translateY(-2px) scale(0.98)');
| |
| setTimeout(() => {
| |
| $(this).css('transform', 'translateY(-8px) scale(1.02)');
| |
| }, 150);
| |
| });
| |
| });
| |
| }
| |
|
| |
| // Função para gerenciar carregamento de imagens
| |
| function handleImageLoading() {
| |
| $('.mw-card-image img').each(function() {
| |
| const $img = $(this);
| |
| const $container = $img.parent();
| |
|
| |
| // Adiciona classe de loading
| |
| $container.addClass('loading');
| |
|
| |
| // Quando a imagem carregar
| |
| $img.on('load', function() {
| |
| $container.removeClass('loading');
| |
| });
| |
|
| |
| // Se a imagem falhar ao carregar
| |
| $img.on('error', function() {
| |
| $container.removeClass('loading');
| |
| // Mantém o gradiente de fundo como fallback
| |
| $img.hide();
| |
| });
| |
|
| |
| // Se a imagem já estiver carregada (cache)
| |
| if (this.complete) {
| |
| $container.removeClass('loading');
| |
| }
| |
| });
| |
| }
| |
|
| |
| // Função de responsividade para cards com imagem
| |
| function adjustImageGrid() {
| |
| const imageGrid = $('.mw-image-grid');
| |
| const width = $(window).innerWidth;
| |
|
| |
| if (width < 480) {
| |
| imageGrid.css('grid-template-columns', '1fr');
| |
| } else if (width < 768) {
| |
| imageGrid.css('grid-template-columns', 'repeat(auto-fit, minmax(250px, 1fr))');
| |
| } else {
| |
| imageGrid.css('grid-template-columns', 'repeat(auto-fit, minmax(300px, 1fr))');
| |
| }
| |
| }
| |
|
| |
| // Função para lazy loading de imagens (opcional)
| |
| function lazyLoadImages() {
| |
| if ('IntersectionObserver' in window) {
| |
| const imageObserver = new IntersectionObserver((entries, observer) => {
| |
| entries.forEach(entry => {
| |
| if (entry.isIntersecting) {
| |
| const img = entry.target;
| |
| const dataSrc = img.getAttribute('data-src');
| |
| if (dataSrc) {
| |
| img.src = dataSrc;
| |
| img.removeAttribute('data-src');
| |
| imageObserver.unobserve(img);
| |
| }
| |
| }
| |
| });
| |
| });
| |
|
| |
| $('.mw-card-image img[data-src]').each(function() {
| |
| imageObserver.observe(this);
| |
| });
| |
| }
| |
| }
| |
|
| |
| // Integração com sistema existente de guias
| |
| function integrateWithExistingSystem() {
| |
| // Estende a funcionalidade existente para incluir cards de imagem
| |
| if (typeof window.adjustGuideGrid === 'function') {
| |
| const originalAdjustGuideGrid = window.adjustGuideGrid;
| |
| window.adjustGuideGrid = function() {
| |
| originalAdjustGuideGrid();
| |
| adjustImageGrid();
| |
| };
| |
| } else {
| |
| window.adjustGuideGrid = adjustImageGrid;
| |
| }
| |
|
| |
| // Adiciona cards de imagem ao sistema de debug existente
| |
| if (typeof window.debugSubmenus === 'function') {
| |
| const originalDebug = window.debugSubmenus;
| |
| window.debugImageCards = function() {
| |
| $('.mw-image-card').each(function() {
| |
| const $card = $(this);
| |
| const text = $card.find('.mw-card-text').text();
| |
| const hasImage = $card.find('img').length > 0;
| |
| const imageLoaded = hasImage ? $card.find('img')[0].complete : false;
| |
|
| |
| console.log('Image Card:', {
| |
| text: text,
| |
| hasImage: hasImage,
| |
| imageLoaded: imageLoaded,
| |
| dimensions: {
| |
| width: $card.width(),
| |
| height: $card.height()
| |
| }
| |
| });
| |
| });
| |
|
| |
| // Chama debug original se existir
| |
| if (originalDebug) originalDebug();
| |
| };
| |
| }
| |
| }
| |
|
| |
| // Inicialização
| |
| initImageCards();
| |
| handleImageLoading();
| |
| adjustImageGrid();
| |
| lazyLoadImages();
| |
| integrateWithExistingSystem();
| |
|
| |
| // Event listeners
| |
| $(window).on('resize', function() {
| |
| adjustImageGrid();
| |
| });
| |
|
| |
| // Observa mudanças no DOM para novos cards adicionados dinamicamente
| |
| if (typeof MutationObserver !== 'undefined') {
| |
| const observer = new MutationObserver(function(mutations) {
| |
| mutations.forEach(function(mutation) {
| |
| if (mutation.addedNodes.length) {
| |
| $(mutation.addedNodes).find('.mw-image-card').each(function() {
| |
| initImageCards();
| |
| handleImageLoading();
| |
| });
| |
| }
| |
| });
| |
| });
| |
|
| |
| observer.observe(document.body, { childList: true, subtree: true });
| |
| }
| |
| });
| |
|
| |
| // Função global para adicionar novos cards dinamicamente
| |
| window.addImageCard = function(container, link, imageSrc, text, bgClass) {
| |
| const cardHtml = `
| |
| <a href="${link}" class="mw-image-card">
| |
| <div class="mw-card-image ${bgClass || ''}">
| |
| <img src="${imageSrc}" alt="${text}" />
| |
| </div>
| |
| <div class="mw-card-text">${text}</div>
| |
| </a>
| |
| `;
| |
|
| |
| $(container).find('.mw-image-grid').append(cardHtml);
| |
|
| |
| // Reinicializa funcionalidades para o novo card
| |
| const $newCard = $(container).find('.mw-image-card').last();
| |
| $newCard.trigger('initImageCard');
| |
| };
| |
|
| |
| // Função para tornar todas as tabelas responsivas
| |
| function makeTablesResponsive() {
| |
| // Seleciona todas as tabelas da página
| |
| const tables = document.querySelectorAll('table');
| |
|
| |
| tables.forEach(table => {
| |
| // Verifica se a tabela tem cabeçalhos (th)
| |
| let headers = Array.from(table.querySelectorAll('th')).map(th => th.textContent.trim());
| |
|
| |
| // Se não houver cabeçalhos, tenta usar a primeira linha como cabeçalho
| |
| if (headers.length === 0) {
| |
| const firstRow = table.querySelector('tr');
| |
| if (firstRow) {
| |
| const cells = firstRow.querySelectorAll('td');
| |
| headers.push(...Array.from(cells).map(cell => cell.textContent.trim()));
| |
| }
| |
| }
| |
|
| |
| // Para tabelas com cabeçalhos em múltiplas linhas, tentamos capturar todos
| |
| if (table.querySelectorAll('thead tr').length > 1) {
| |
| // Para tabelas com cabeçalhos complexos, usamos a última linha de cabeçalhos
| |
| const headerRows = table.querySelectorAll('thead tr');
| |
| const lastHeaderRow = headerRows[headerRows.length - 1];
| |
| headers = Array.from(lastHeaderRow.querySelectorAll('th')).map(th => th.textContent.trim());
| |
| }
| |
|
| |
| // Para cada linha da tabela, adiciona atributos data-label às células
| |
| const rows = table.querySelectorAll('tr');
| |
|
| |
| // Começa a partir da primeira linha após os cabeçalhos
| |
| // Se estamos em uma tabela sem thead ou com th, começamos nas linhas de tbody
| |
| // Se tabela tiver thead, ignoramos as linhas de cabeçalho
| |
| let startRow = 0;
| |
| if (table.querySelector('thead')) {
| |
| startRow = table.querySelectorAll('thead tr').length;
| |
| } else if (headers.length > 0 && table.querySelectorAll('th').length > 0) {
| |
| // Se não tem thead mas tem th, assumimos primeira linha como cabeçalho
| |
| startRow = 1;
| |
| }
| |
|
| |
| for (let i = startRow; i < rows.length; i++) {
| |
| const cells = rows[i].querySelectorAll('td');
| |
| cells.forEach((cell, index) => {
| |
| if (index < headers.length && headers[index]) {
| |
| cell.setAttribute('data-label', headers[index]);
| |
| }
| |
| });
| |
| }
| |
| });
| |
| }
| |
|
| |
| // Executa quando o DOM estiver pronto
| |
| if (document.readyState === 'loading') {
| |
| document.addEventListener('DOMContentLoaded', makeTablesResponsive);
| |
| } else {
| |
| makeTablesResponsive();
| |
| }
| |
|
| |
| // Verifica novamente após o carregamento da página (para conteúdo carregado dinamicamente)
| |
| window.addEventListener('load', makeTablesResponsive);
| |
|
| |
| // Para lidar com conteúdo adicionado dinamicamente (como em edições AJAX)
| |
| // Observe mutações no DOM
| |
| if (typeof MutationObserver !== 'undefined') {
| |
| const observer = new MutationObserver(function(mutations) {
| |
| mutations.forEach(function(mutation) {
| |
| if (mutation.addedNodes.length) {
| |
| makeTablesResponsive();
| |
| }
| |
| });
| |
| });
| |
|
| |
| // Observe mudanças no corpo do documento
| |
| observer.observe(document.body, { childList: true, subtree: true });
| |
| }
| |
| mw.loader.state({
| |
| "skins.timeless.js": "ready"
| |
| });
| |
| /*
| |
| VortanMU - Timeless sidebar behavior (v3)
| |
| - Hover to open (desktop) + click to toggle (touch/keyboard)
| |
| - Keeps original Timeless structure (portlets)
| |
| - Avoids conflicts with any previously-injected mega-menu nodes
| |
|
| |
| Install: paste into MediaWiki:Timeless.js (replace your current sidebar script).
| |
| */
| |
| (function () {
| |
| 'use strict';
| |
| if (window.VM_TIMELESS_SIDEBAR_V3) return;
| |
| window.VM_TIMELESS_SIDEBAR_V3 = true;
| |
|
| |
| function enhanceSidebar() {
| |
| var nav = document.getElementById('mw-site-navigation');
| |
| if (!nav) return;
| |
|
| |
| nav.classList.add('vm-nav');
| |
|
| |
| // Remove any old injected mega-menu nodes (common source of “applies then reverts”)
| |
| Array.prototype.forEach.call(nav.querySelectorAll('ul.submenu'), function (el) {
| |
| el.parentNode && el.parentNode.removeChild(el);
| |
| });
| |
|
| |
| var portlets = Array.prototype.slice.call(
| |
| nav.querySelectorAll('.mw-portlet[role="navigation"]')
| |
| );
| |
|
| |
| var hoverCapable = !!(window.matchMedia && window.matchMedia('(hover:hover) and (pointer:fine)').matches);
| |
|
| |
| function setExpanded(p, expanded) {
| |
| var h = p.querySelector('h3.vm-portlet__header') || p.querySelector('h3');
| |
| if (h) h.setAttribute('aria-expanded', expanded ? 'true' : 'false');
| |
| }
| |
|
| |
| function closePortlet(p) {
| |
| p.classList.remove('is-open');
| |
| setExpanded(p, false);
| |
| }
| |
|
| |
| function openPortlet(p) {
| |
| // close others (keeps sidebar clean), but never close Wiki tools
| |
| portlets.forEach(function (other) {
| |
| if (other !== p && other.id !== 'p-tb') closePortlet(other);
| |
| });
| |
| p.classList.add('is-open');
| |
| setExpanded(p, true);
| |
| }
| |
|
| |
| portlets.forEach(function (p) {
| |
| var h3 = p.querySelector('h3');
| |
| var body = p.querySelector('.mw-portlet-body');
| |
| if (!h3 || !body) return;
| |
|
| |
| h3.classList.add('vm-portlet__header');
| |
| body.classList.add('vm-portlet__body');
| |
|
| |
| // If something hid the original <ul> inline, bring it back
| |
| var ul = body.querySelector('ul');
| |
| if (ul && ul.style && ul.style.display === 'none') ul.style.display = '';
| |
|
| |
| // Wiki tools: always visible
| |
| if (p.id === 'p-tb') {
| |
| p.classList.add('is-open');
| |
| h3.setAttribute('aria-expanded', 'true');
| |
| return;
| |
| }
| |
|
| |
| // default closed
| |
| closePortlet(p);
| |
|
| |
| // accessibility + click toggle
| |
| h3.setAttribute('role', 'button');
| |
| h3.setAttribute('tabindex', '0');
| |
| h3.setAttribute('aria-expanded', 'false');
| |
|
| |
| function toggle() {
| |
| if (p.classList.contains('is-open')) closePortlet(p);
| |
| else openPortlet(p);
| |
| }
| |
|
| |
| h3.addEventListener('click', function (e) {
| |
| e.preventDefault();
| |
| toggle();
| |
| });
| |
|
| |
| h3.addEventListener('keydown', function (e) {
| |
| if (e.key === 'Enter' || e.key === ' ') {
| |
| e.preventDefault();
| |
| toggle();
| |
| }
| |
| });
| |
|
| |
| // Hover open (desktop)
| |
| if (hoverCapable) {
| |
| var t;
| |
| p.addEventListener('mouseenter', function () {
| |
| if (t) clearTimeout(t);
| |
| openPortlet(p);
| |
| });
| |
| p.addEventListener('mouseleave', function () {
| |
| if (t) clearTimeout(t);
| |
| t = setTimeout(function () { closePortlet(p); }, 180);
| |
| });
| |
| }
| |
| });
| |
| }
| |
|
| |
| // Run on normal loads + after purge/navigation
| |
| if (window.mw && mw.hook) {
| |
| mw.hook('wikipage.content').add(enhanceSidebar);
| |
| } else if (document.readyState === 'loading') {
| |
| document.addEventListener('DOMContentLoaded', enhanceSidebar);
| |
| } else {
| |
| enhanceSidebar();
| |
| }
| |
| })();
| |
|
| |
| /* ==============================
| |
| VortanMU - Mega flyout (hover + colunas)
| |
| Cole no FINAL do MediaWiki:Timeless.js
| |
| ============================== */
| |
| (function () {
| |
| function onReady(fn) {
| |
| if (document.readyState === "loading") {
| |
| document.addEventListener("DOMContentLoaded", fn);
| |
| } else {
| |
| fn();
| |
| }
| |
| }
| |
|
| |
| function clamp(n, min, max) {
| |
| return Math.max(min, Math.min(max, n));
| |
| }
| |
|
| |
| function calcColumns(portletId, itemCount) {
| |
| // Regra principal: ~6 itens por coluna
| |
| // Mapas é exceção: aceita mais por coluna pra não virar 5+ colunas
| |
| var perCol = 6;
| |
| if (portletId === "p-Mapas") perCol = 10;
| |
|
| |
| var cols = Math.ceil(itemCount / perCol);
| |
| return clamp(cols, 1, 4);
| |
| }
| |
|
| |
| onReady(function () {
| |
| var nav = document.getElementById("site-navigation");
| |
| if (!nav) return;
| |
|
| |
| var portlets = nav.querySelectorAll(".mw-portlet");
| |
| portlets.forEach(function (portlet) {
| |
| var h3 = portlet.querySelector("h3");
| |
| var body = portlet.querySelector(".mw-portlet-body");
| |
| if (!h3 || !body) return;
| |
|
| |
| var ul = body.querySelector("ul");
| |
| if (!ul) return;
| |
|
| |
| var lis = Array.from(ul.querySelectorAll("li"));
| |
| if (!lis.length) return;
| |
|
| |
| // Marca como “mega”
| |
| h3.classList.add("has-mega");
| |
|
| |
| // Cria o submenu (se ainda não existe)
| |
| var submenu = portlet.querySelector("ul.submenu");
| |
| if (!submenu) {
| |
| submenu = document.createElement("ul");
| |
| submenu.className = "submenu";
| |
| portlet.appendChild(submenu);
| |
| }
| |
|
| |
| // Reconstrói os itens do flyout (mantém sempre consistente)
| |
| submenu.innerHTML = "";
| |
| lis.forEach(function (li) {
| |
| var a = li.querySelector("a");
| |
| if (!a) return;
| |
| var li2 = document.createElement("li");
| |
| li2.appendChild(a.cloneNode(true));
| |
| submenu.appendChild(li2);
| |
| });
| |
|
| |
| // Define quantidade de colunas por regra
| |
| var count = submenu.querySelectorAll("li").length;
| |
| var cols = calcColumns(portlet.id || "", count);
| |
|
| |
| submenu.classList.remove(
| |
| "submenu-1-columns",
| |
| "submenu-2-columns",
| |
| "submenu-3-columns",
| |
| "submenu-4-columns"
| |
| );
| |
| submenu.classList.add("submenu-" + cols + "-columns");
| |
|
| |
| // Esconde UL original (menu vira “botão” + flyout)
| |
| ul.style.display = "none";
| |
| body.style.display = "none";
| |
|
| |
| // Hover abre/fecha com pequeno delay (pra dar tempo de mover o mouse)
| |
| var hideTimer = null;
| |
|
| |
| function show() {
| |
| if (hideTimer) clearTimeout(hideTimer);
| |
| submenu.style.display = "grid";
| |
| }
| |
|
| |
| function hide() {
| |
| hideTimer = setTimeout(function () {
| |
| submenu.style.display = "none";
| |
| }, 140);
| |
| }
| |
|
| |
| // Hover desktop
| |
| h3.addEventListener("mouseenter", show);
| |
| portlet.addEventListener("mouseenter", show);
| |
| portlet.addEventListener("mouseleave", hide);
| |
| submenu.addEventListener("mouseenter", show);
| |
| submenu.addEventListener("mouseleave", hide);
| |
|
| |
| // Clique (fallback)
| |
| h3.addEventListener("click", function (e) {
| |
| // em desktop isso não atrapalha, e em touch ajuda
| |
| e.preventDefault();
| |
| var isOpen = submenu.style.display === "grid";
| |
| submenu.style.display = isOpen ? "none" : "grid";
| |
| });
| |
| });
| |
| });
| |
| })();
| |
|
| |
| /* ===============================
| |
| VortanMU Mega Sidebar (v4)
| |
| - Floating flyout overlay (no clipping)
| |
| - Max 6 items/col (Maps: 10 items/col)
| |
| - Responsive: desktop hover, small screens click/accordion
| |
| ================================= */
| |
| (function () {
| |
| "use strict";
| |
|
| |
| const MIN_DESKTOP = 1200; // below this, no hover-flyout
| |
| const GAP = 18; // distance between sidebar and flyout
| |
| const CLOSE_DELAY = 180;
| |
|
| |
| function onReady(fn) {
| |
| if (document.readyState === "loading") {
| |
| document.addEventListener("DOMContentLoaded", fn, { once: true });
| |
| } else {
| |
| fn();
| |
| }
| |
| }
| |
|
| |
| function hoverCapable() {
| |
| return (
| |
| window.innerWidth >= MIN_DESKTOP &&
| |
| window.matchMedia &&
| |
| window.matchMedia("(hover:hover) and (pointer:fine)").matches
| |
| );
| |
| }
| |
|
| |
| function ensurePanel() {
| |
| let panel = document.getElementById("vm-mega-panel");
| |
| if (panel) return panel;
| |
|
| |
| panel = document.createElement("div");
| |
| panel.id = "vm-mega-panel";
| |
| document.body.appendChild(panel);
| |
| return panel;
| |
| }
| |
|
| |
| function buildPanel(panel, title, links) {
| |
| panel.replaceChildren();
| |
|
| |
| const t = (title || "").toLowerCase();
| |
| const isMaps = t.includes("mapas") || t.includes("maps");
| |
| const maxPerCol = isMaps ? 10 : 6;
| |
|
| |
| let cols = Math.ceil(links.length / maxPerCol);
| |
| cols = Math.max(1, Math.min(cols, 4));
| |
|
| |
| panel.style.setProperty("--vm-cols", String(cols));
| |
|
| |
| const grid = document.createElement("div");
| |
| grid.className = "vm-grid";
| |
|
| |
| const colUls = [];
| |
| for (let c = 0; c < cols; c++) {
| |
| const ul = document.createElement("ul");
| |
| ul.className = "vm-col";
| |
| colUls.push(ul);
| |
| grid.appendChild(ul);
| |
| }
| |
|
| |
| links.forEach((a, idx) => {
| |
| const li = document.createElement("li");
| |
| const link = a.cloneNode(true);
| |
|
| |
| link.removeAttribute("accesskey");
| |
|
| |
| li.appendChild(link);
| |
|
| |
| const colIndex = Math.min(cols - 1, Math.floor(idx / maxPerCol));
| |
| colUls[colIndex].appendChild(li);
| |
| });
| |
|
| |
| panel.appendChild(grid);
| |
| }
| |
|
| |
| function positionPanel(panel, anchorEl) {
| |
| const r = anchorEl.getBoundingClientRect();
| |
| const sx = window.scrollX || window.pageXOffset || 0;
| |
| const sy = window.scrollY || window.pageYOffset || 0;
| |
|
| |
| // Limit width to what fits on the right
| |
| const availableRight = window.innerWidth - r.right - GAP - 16;
| |
| const maxW = Math.max(420, Math.min(920, availableRight));
| |
| panel.style.maxWidth = maxW + "px";
| |
|
| |
| let left = r.right + GAP + sx;
| |
| let top = r.top + sy;
| |
|
| |
| // Keep inside viewport vertically
| |
| const pad = 12;
| |
| const maxTop = sy + window.innerHeight - panel.offsetHeight - pad;
| |
| if (panel.offsetHeight && top > maxTop) {
| |
| top = Math.max(sy + pad, maxTop);
| |
| }
| |
|
| |
| panel.style.left = left + "px";
| |
| panel.style.top = top + "px";
| |
| }
| |
|
| |
| function initVortanMegaSidebar() {
| |
| const leftNav = document.getElementById("mw-site-navigation");
| |
| if (!leftNav) return;
| |
|
| |
| // prevent double-init
| |
| if (leftNav.classList.contains("vm-nav--ready")) return;
| |
|
| |
| leftNav.classList.add("vm-nav");
| |
|
| |
| // Prefer the inner chunk if present
| |
| const chunk = document.getElementById("site-navigation") || leftNav;
| |
|
| |
| const portlets = Array.from(chunk.querySelectorAll(".mw-portlet"));
| |
|
| |
| // Remove empty toolbox (common: "WIKI TOOLS" with no links)
| |
| portlets.forEach((p) => {
| |
| const body = p.querySelector(".mw-portlet-body");
| |
| const links = body ? body.querySelectorAll("a") : [];
| |
| const h3 = p.querySelector("h3");
| |
| const title = h3 ? h3.textContent.trim().toLowerCase() : "";
| |
|
| |
| if (p.id === "p-tb" || title === "wiki tools" || title === "ferramentas") {
| |
| if (!links.length) {
| |
| p.remove();
| |
| }
| |
| }
| |
| });
| |
|
| |
| const triggers = [];
| |
|
| |
| portlets.forEach((p) => {
| |
| const h3 = p.querySelector("h3");
| |
| const body = p.querySelector(".mw-portlet-body");
| |
|
| |
| if (!h3 || !body) return;
| |
|
| |
| // Ignore logo/search portlets if they exist here
| |
| if (p.id === "p-logo" || p.id === "p-search") return;
| |
|
| |
| const title = h3.textContent.trim();
| |
| if (!title) return;
| |
|
| |
| const links = Array.from(body.querySelectorAll("li a")).filter(
| |
| (a) => (a.getAttribute("href") || "").trim().length
| |
| );
| |
| if (!links.length) return;
| |
|
| |
| h3.classList.add("vm-portlet__header");
| |
| h3.setAttribute("tabindex", "0");
| |
| h3.setAttribute("role", "button");
| |
| h3.setAttribute("aria-expanded", "false");
| |
|
| |
| triggers.push({ portlet: p, header: h3, title, links });
| |
| });
| |
|
| |
| // mark ready (CSS will hide original lists only now)
| |
| leftNav.classList.add("vm-nav--ready");
| |
|
| |
| const panel = ensurePanel();
| |
|
| |
| let active = null;
| |
| let closeTimer = null;
| |
|
| |
| function openFor(tr) {
| |
| if (closeTimer) {
| |
| clearTimeout(closeTimer);
| |
| closeTimer = null;
| |
| }
| |
|
| |
| if (active && active !== tr) {
| |
| active.portlet.classList.remove("is-open");
| |
| active.header.setAttribute("aria-expanded", "false");
| |
| }
| |
|
| |
| active = tr;
| |
|
| |
| buildPanel(panel, tr.title, tr.links);
| |
|
| |
| panel.classList.add("is-open");
| |
| tr.portlet.classList.add("is-open");
| |
| tr.header.setAttribute("aria-expanded", "true");
| |
|
| |
| positionPanel(panel, tr.header);
| |
| }
| |
|
| |
| function closeAll() {
| |
| if (active) {
| |
| active.portlet.classList.remove("is-open");
| |
| active.header.setAttribute("aria-expanded", "false");
| |
| active = null;
| |
| }
| |
| panel.classList.remove("is-open");
| |
| panel.replaceChildren();
| |
| }
| |
|
| |
| function scheduleClose() {
| |
| if (closeTimer) clearTimeout(closeTimer);
| |
| closeTimer = setTimeout(closeAll, CLOSE_DELAY);
| |
| }
| |
|
| |
| // Keep open while mouse is on panel
| |
| panel.addEventListener("mouseenter", () => {
| |
| if (closeTimer) clearTimeout(closeTimer);
| |
| });
| |
| panel.addEventListener("mouseleave", scheduleClose);
| |
|
| |
| triggers.forEach((tr) => {
| |
| // Hover open only on desktop
| |
| tr.portlet.addEventListener("mouseenter", () => {
| |
| if (hoverCapable()) openFor(tr);
| |
| });
| |
| tr.portlet.addEventListener("mouseleave", () => {
| |
| if (hoverCapable()) scheduleClose();
| |
| });
| |
|
| |
| // Click behavior:
| |
| // - Desktop: toggles flyout
| |
| // - Small screens: toggles accordion (original body)
| |
| tr.header.addEventListener("click", (e) => {
| |
| e.preventDefault();
| |
| e.stopPropagation();
| |
|
| |
| if (!hoverCapable()) {
| |
| // accordion mode (mobile/small)
| |
| tr.portlet.classList.toggle("is-open");
| |
| const expanded = tr.portlet.classList.contains("is-open");
| |
| tr.header.setAttribute("aria-expanded", expanded ? "true" : "false");
| |
| return;
| |
| }
| |
|
| |
| if (active === tr) closeAll();
| |
| else openFor(tr);
| |
| });
| |
|
| |
| tr.header.addEventListener("keydown", (e) => {
| |
| if (e.key !== "Enter" && e.key !== " ") return;
| |
| e.preventDefault();
| |
| tr.header.click();
| |
| });
| |
| });
| |
|
| |
| // Close when clicking outside
| |
| document.addEventListener("click", (e) => {
| |
| if (!panel.classList.contains("is-open")) return;
| |
|
| |
| const clickInsideSidebar = leftNav.contains(e.target);
| |
| const clickInsidePanel = panel.contains(e.target);
| |
| if (!clickInsideSidebar && !clickInsidePanel) closeAll();
| |
| });
| |
|
| |
| // Reposition on scroll/resize
| |
| window.addEventListener(
| |
| "scroll",
| |
| () => {
| |
| if (active && panel.classList.contains("is-open") && hoverCapable()) {
| |
| positionPanel(panel, active.header);
| |
| }
| |
| },
| |
| { passive: true }
| |
| );
| |
|
| |
| window.addEventListener("resize", () => {
| |
| if (!active) return;
| |
| if (!hoverCapable()) {
| |
| closeAll();
| |
| } else {
| |
| positionPanel(panel, active.header);
| |
| }
| |
| });
| |
| }
| |
|
| |
| // MediaWiki hook + DOM ready fallback
| |
| if (window.mw && mw.hook) {
| |
| mw.hook("wikipage.content").add(initVortanMegaSidebar);
| |
| }
| |
| onReady(initVortanMegaSidebar);
| |
| })();
| |