1/** 2 * Plugin Acessibilidade & Transparência â v1.0.0 3 * Controles: tamanho de fonte, alto contraste, painel e-MAG 4 * Persistência: localStorage 5 */ 6(function () { 7 'use strict'; 8 9 /* ------------------------------------------------------------------ 10 Constantes 11 ------------------------------------------------------------------ */ 12 var LS_FONT = 'actr_font_size'; 13 var LS_CONTRAST = 'actr_contrast'; 14 15 var FONT_MIN = 1; 16 var FONT_DEFAULT = 2; 17 var FONT_MAX = 4; 18 19 var FONT_SCALES = { 1: 0.875, 2: 1.0, 3: 1.1875, 4: 1.375 }; 20 21 var FONT_LABELS = { 22 1: 'Texto pequeno', 23 2: 'Tamanho padrão', 24 3: 'Texto grande', 25 4: 'Texto muito grande' 26 }; 27 28 /* ------------------------------------------------------------------ 29 Estado 30 ------------------------------------------------------------------ */ 31 var state = { 32 fontSize: parseInt(localStorage.getItem(LS_FONT) || FONT_DEFAULT, 10), 33 contrast: localStorage.getItem(LS_CONTRAST) || 'default', 34 panelOpen: false 35 }; 36 37 /* ------------------------------------------------------------------ 38 Referências DOM (aguarda DOMContentLoaded) 39 ------------------------------------------------------------------ */ 40 document.addEventListener('DOMContentLoaded', function () { 41 42 var html = document.documentElement; 43 var toggle = document.getElementById('actr-toggle'); 44 var panel = document.getElementById('actr-panel'); 45 var closeBtn = document.getElementById('actr-close'); 46 var fontDecrease = document.getElementById('actr-font-decrease'); 47 var fontReset = document.getElementById('actr-font-reset'); 48 var fontIncrease = document.getElementById('actr-font-increase'); 49 var fontStatus = document.getElementById('actr-font-status'); 50 var resetAll = document.getElementById('actr-reset-all'); 51 52 var contrastBtns = { 53 'default': document.getElementById('actr-contrast-default'), 54 'dark': document.getElementById('actr-contrast-dark'), 55 'yellow': document.getElementById('actr-contrast-yellow') 56 }; 57 58 if (!toggle || !panel) return; 59 60 /* ------------------------------------------------------- 61 Aplicar estado salvo ao carregar a página 62 ------------------------------------------------------- */ 63 applyFontSize(state.fontSize, false); 64 applyContrast(state.contrast, false); 65 66 /* ------------------------------------------------------- 67 Abrir / fechar painel 68 ------------------------------------------------------- */ 69 toggle.addEventListener('click', function () { 70 togglePanel(!state.panelOpen); 71 }); 72 73 closeBtn.addEventListener('click', function () { 74 togglePanel(false); 75 toggle.focus(); 76 }); 77 78 /* Fechar com Escape */ 79 document.addEventListener('keydown', function (e) { 80 if (e.key === 'Escape' && state.panelOpen) { 81 togglePanel(false); 82 toggle.focus(); 83 } 84 }); 85 86 /* Fechar ao clicar fora do painel */ 87 document.addEventListener('click', function (e) { 88 if (state.panelOpen && 89 !panel.contains(e.target) && 90 !toggle.contains(e.target)) { 91 togglePanel(false); 92 } 93 }); 94 95 /* ------------------------------------------------------- 96 Controles de fonte 97 ------------------------------------------------------- */ 98 fontDecrease.addEventListener('click', function () { 99 setFontSize(state.fontSize - 1); 100 }); 101 102 fontReset.addEventListener('click', function () { 103 setFontSize(FONT_DEFAULT); 104 }); 105 106 fontIncrease.addEventListener('click', function () { 107 setFontSize(state.fontSize + 1); 108 }); 109 110 /* ------------------------------------------------------- 111 Botões de contraste 112 ------------------------------------------------------- */
113 Object.keys(contrastBtns).forEach(function (key) { 114 var btn = contrastBtns[key]; 115 if (!btn) return; 116 btn.addEventListener('click', function () { 117 setContrast(key); 118 }); 119 }); 120 121 /* ------------------------------------------------------- 122 Restaurar todos os padrões 123 ------------------------------------------------------- */ 124 resetAll.addEventListener('click', function () { 125 setFontSize(FONT_DEFAULT); 126 setContrast('default'); 127 announceToScreenReader('Configurações restauradas para o padrão.'); 128 }); 129 130 /* ================================================================ 131 Funções internas 132 ================================================================ */ 133 134 function togglePanel(open) { 135 state.panelOpen = open; 136 137 if (open) { 138 panel.removeAttribute('hidden'); 139 /* Pequeno timeout p/ a transição CSS funcionar */ 140 requestAnimationFrame(function () { 141 panel.classList.add('actr-is-open'); 142 }); 143 toggle.setAttribute('aria-expanded', 'true'); 144 /* Foco no primeiro elemento focável dentro do painel */ 145 var firstFocusable = panel.querySelector( 146 'button:not([disabled]), [href], input, select, textarea, [tabindex]:not([tabindex="-1"])' 147 ); 148 if (firstFocusable) { 149 setTimeout(function () { firstFocusable.focus(); }, 50); 150 } 151 } else { 152 panel.classList.remove('actr-is-open'); 153 toggle.setAttribute('aria-expanded', 'false'); 154 setTimeout(function () { 155 panel.setAttribute('hidden', ''); 156 }, 220); /* aguarda a transição CSS */ 157 } 158 } 159 160 /* ------------------------------------------------------- 161 Tamanho de fonte 162 ------------------------------------------------------- */ 163 function setFontSize(size) { 164 size = Math.max(FONT_MIN, Math.min(FONT_MAX, size)); 165 state.fontSize = size; 166 localStorage.setItem(LS_FONT, size); 167 applyFontSize(size, true); 168 } 169 170 function applyFontSize(size, announce) { 171 var actrRoot = document.getElementById('actr-root'); 172 var els = document.body.querySelectorAll('*'); 173 var i, el, orig; 174 175 if (size === FONT_DEFAULT) { 176 /* ââ Reset: remove inline font-size de todos os elementos ââ */ 177 document.documentElement.style.removeProperty('font-size'); 178 for (i = 0; i < els.length; i++) { 179 el = els[i]; 180 if (actrRoot && actrRoot.contains(el)) continue; 181 if (el.hasAttribute('data-actr-fs')) { 182 el.style.removeProperty('font-size'); 183 } 184 } 185 } else { 186 /* ââ 1. Snapshot na primeira vez (computed px natural) ââ */ 187 for (i = 0; i < els.length; i++) { 188 el = els[i]; 189 if (actrRoot && actrRoot.contains(el)) continue; 190 if (!el.hasAttribute('data-actr-fs')) { 191 orig = parseFloat(window.getComputedStyle(el).fontSize); 192 if (orig > 0) { el.setAttribute('data-actr-fs', orig); } 193 } 194 } 195 196 /* ââ 2. Aplica escala em todos os elementos ââ */ 197 var scale = FONT_SCALES[size] || 1.0; 198 for (i = 0; i < els.length; i++) { 199 el = els[i]; 200 if (actrRoot && actrRoot.contains(el)) continue; 201 orig = parseFloat(el.getAttribute('data-actr-fs')); 202 if (!isNaN(orig) && orig > 0) { 203 el.style.setProperty('font-size', (orig * scale).toFixed(2) + 'px', 'important'); 204 } 205 } 206 207 /* ââ 3. Ajusta html para elementos rem-based ââ */ 208 var htmlPx = { 1: '14px', 2: '16px', 3: '19px', 4: '22px' }; 209 document.documentElement.style.setProperty('font-size', htmlPx[size] || '16px', 'important'); 210 } 211 212 /* Garante que o widget de acessibilidade não seja afetado */ 213 if (actrRoot) { 214 actrRoot.style.setProperty('font-size', '16px', 'important'); 215 } 216 217 document.documentElement.setAttribute('data-font-size', size); 218 219 /* Atualiza estado visual dos botões */ 220 if (fontDecrease) { 221 fontDecrease.disabled = (size <= FONT_MIN); 222 fontDecrease.classList.toggle('actr-is-active', size < FONT_DEFAULT); 223 } 224 if (fontReset) { 225 fontReset.classList.toggle('actr-is-active', size === FONT_DEFAULT); 226 } 227 if (fontIncrease) { 228 fontIncrease.disabled = (size >= FONT_MAX); 229 fontIncrease.classList.toggle('actr-is-active', size > FONT_DEFAULT); 230 } 231 232 /* Texto de status */ 233 if (fontStatus) { 234 fontStatus.textContent = FONT_LABELS[size] || ''; 235 } 236 237 if (announce && fontStatus) { 238 announceToScreenReader(FONT_LABELS[size] || 'Tamanho ajustado.'); 239 } 240 } 241 242 /* ------------------------------------------------------- 243 Contraste 244 ------------------------------------------------------- */ 245 function setContrast(mode) { 246 state.contrast = mode; 247 localStorage.setItem(LS_CONTRAST, mode); 248 applyContrast(mode, true); 249 } 250 251 function applyContrast(mode, announce) { 252 var html = document.documentElement; 253 var actrRoot = document.getElementById('actr-root'); 254 255 /* Remove a classe de modo escuro */ 256 html.classList.remove('actr-contrast-dark', 'actr-contrast-yellow'); 257 258 /* Remove inline styles do modo amarelo (se havia) */ 259 var prevEls = document.body.querySelectorAll('[data-actr-yellow]'); 260 for (var r = 0; r < prevEls.length; r++) { 261 prevEls[r].style.removeProperty('background-color'); 262 prevEls[r].style.removeProperty('color'); 263 prevEls[r].style.removeProperty('border-color'); 264 prevEls[r].style.removeProperty('filter'); 265 prevEls[r].style.removeProperty('text-decoration'); 266 prevEls[r].removeAttribute('data-actr-yellow'); 267 } 268 /* Varredura extra: garante que nenhuma imagem fique com filter residual */ 269 var allImgs = document.body.querySelectorAll('img'); 270 for (var m = 0; m < allImgs.length; m++) { 271 allImgs[m].style.removeProperty('filter'); 272 } 273 274 if (mode === 'dark') { 275 html.classList.add('actr-contrast-dark'); 276 277 } else if (mode === 'yellow') { 278 html.classList.add('actr-contrast-yellow'); /* mantém para botão ativo */ 279 280 /* Aplica inline !important em cada elemento da página, 281 ignorando o widget â máxima precedência, imune ao JS do tema */ 282 var els = document.body.querySelectorAll('*'); 283 for (var i = 0; i < els.length; i++) { 284 var el = els[i]; 285 if (actrRoot && actrRoot.contains(el)) continue; 286 el.style.setProperty('background-color', '#000000', 'important'); 287 el.style.setProperty('color', '#FFD700', 'important'); 288 el.style.setProperty('border-color', '#FFD700', 'important'); 289 el.setAttribute('data-actr-yellow', '1'); 290 } 291 292 /* Links */ 293 var links = document.body.querySelectorAll('a:not(#actr-root a)'); 294 for (var j = 0; j < links.length; j++) { 295 links[j].style.setProperty('color', '#FFD700', 'important'); 296 links[j].style.setProperty('text-decoration', 'underline', 'important'); 297 } 298 /* Sem grayscale em imagens â evita problemas de reset em produção */ 299 } 300 301 /* Atualiza aria-pressed dos botões */
302 Object.keys(contrastBtns).forEach(function (key) { 303 var btn = contrastBtns[key]; 304 if (!btn) return; 305 var isActive = (key === mode); 306 btn.setAttribute('aria-pressed', isActive ? 'true' : 'false'); 307 btn.classList.toggle('actr-is-active', isActive); 308 }); 309 310 if (announce) { 311 var labels = { 312 'default': 'Contraste padrão ativado.', 313 'dark': 'Alto contraste escuro ativado.', 314 'yellow': 'Alto contraste preto e amarelo ativado.' 315 }; 316 announceToScreenReader(labels[mode] || 'Contraste alterado.'); 317 } 318 } 319 320 /* ------------------------------------------------------- 321 Anúncio para leitores de tela (aria-live) 322 ------------------------------------------------------- */ 323 function announceToScreenReader(message) { 324 var live = document.getElementById('actr-live-region'); 325 if (!live) { 326 live = document.createElement('div'); 327 live.id = 'actr-live-region'; 328 live.setAttribute('role', 'status'); 329 live.setAttribute('aria-live', 'polite'); 330 live.setAttribute('aria-atomic', 'true'); 331 /* Visualmente oculto */ 332 live.style.cssText = 'position:absolute;width:1px;height:1px;overflow:hidden;clip:rect(0,0,0,0);white-space:nowrap;border:0;'; 333 document.body.appendChild(live); 334 } 335 live.textContent = ''; 336 setTimeout(function () { 337 live.textContent = message; 338 }, 50); 339 } 340 341 }); /* DOMContentLoaded */ 342 343}());
Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.