PageSourceSearch

https://santanadomaranhao.ma.gov.br/transparencia/wp-content/plugi…/assets/js/accessibility.js

js santanadomaranhao.ma.gov.br collected 2026-10-02 14:17:37 UTC 14,855 bytes, 343 lines download raw bytes

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.