1(() => { 2 'use strict'; 3 4 if (window.BIBLIAN_ACCESSIBILITY_LOADED) { 5 return; 6 } 7 window.BIBLIAN_ACCESSIBILITY_LOADED = true; 8 9 const STORAGE_KEY = 'biblian_a11y_preferences_v1'; 10 const STORAGE_VERSION = 2; 11 const SCALE_VALUES = [1, 1.125, 1.25, 1.375, 1.5]; 12 const TOGGLE_NAMES = [ 13 'high-contrast', 14 'grayscale', 15 'underline-links', 16 'readable-text', 17 'focus-boost', 18 'stop-motion', 19 ]; 20 const root = document.documentElement; 21 let initialized = false; 22 23 const normalizePreferences = (candidate) => { 24 const toggles = {}; 25 const candidateToggles = candidate && typeof candidate.toggles === 'object' 26 ? candidate.toggles 27 : {}; 28 29 TOGGLE_NAMES.forEach((name) => { 30 toggles[name] = candidateToggles[name] === true; 31 }); 32 33 const scaleIndex = Number.isInteger(candidate?.scaleIndex) 34 ? Math.min(SCALE_VALUES.length - 1, Math.max(0, candidate.scaleIndex)) 35 : 0; 36 37 return { version: STORAGE_VERSION, toggles, scaleIndex }; 38 }; 39 40 let state = loadPreferences(); 41 42 function loadPreferences() { 43 try { 44 const raw = window.localStorage.getItem(STORAGE_KEY); 45 const storedPreferences = raw ? JSON.parse(raw) : null; 46 const preferences = normalizePreferences(storedPreferences); 47 48 // Version 1 persisted the pause used during QA and could leave the public 49 // hero frozen indefinitely. Migrate that legacy value once; choices made 50 // with the current widget remain persistent like the other preferences. 51 if (storedPreferences?.version !== STORAGE_VERSION) { 52 preferences.toggles['stop-motion'] = false; 53 } 54 return preferences; 55 } catch (error) { 56 return normalizePreferences(null); 57 } 58 } 59 60 const savePreferences = () => { 61 try { 62 window.localStorage.setItem(STORAGE_KEY, JSON.stringify(state)); 63 } catch (error) { 64 // The controls remain functional when private browsing blocks storage. 65 } 66 }; 67 68 const announce = (message) => { 69 const status = document.getElementById('biblian-a11y-status'); 70 if (!status) { 71 return; 72 } 73 status.textContent = ''; 74 window.requestAnimationFrame(() => { 75 status.textContent = message; 76 }); 77 }; 78 79 const dispatchMotionPreference = () => { 80 document.dispatchEvent(new CustomEvent('biblian:a11y-motion', { 81 detail: { paused: state.toggles['stop-motion'] === true }, 82 })); 83 }; 84 85 const refreshPanel = () => { 86 const panel = document.getElementById('biblian-a11y-panel'); 87 if (!panel) { 88 return; 89 } 90 91 panel.querySelectorAll('[data-a11y-toggle]').forEach((button) => { 92 const name = button.getAttribute('data-a11y-toggle'); 93 const active = name ? state.toggles[name] === true : false; 94 button.classList.toggle('is-active', active); 95 button.setAttribute('aria-pressed', String(active)); 96 }); 97 98 const value = panel.querySelector('[data-a11y-value="text-size"]'); 99 if (value) { 100 value.textContent = `${Math.round(SCALE_VALUES[state.scaleIndex] * 100)}%`; 101 } 102 103 const increase = panel.querySelector('[data-a11y-action="text-up"]'); 104 const decrease = panel.querySelector('[data-a11y-action="text-down"]'); 105 if (increase) { 106 increase.disabled = state.scaleIndex === SCALE_VALUES.length - 1; 107 } 108 if (decrease) { 109 decrease.disabled = state.scaleIndex === 0; 110 } 111 }; 112 113 const applyRootPreferences = () => { 114 TOGGLE_NAMES.forEach((name) => { 115 root.classList.toggle(`biblian-a11y-${name}`, state.toggles[name] === true); 116 }); 117 118 const scale = SCALE_VALUES[state.scaleIndex]; 119 root.classList.toggle('biblian-a11y-text-scale', state.scaleIndex !== 0); 120 if (state.scaleIndex === 0) { 121 root.style.removeProperty('--biblian-a11y-text-scale'); 122 } else { 123 root.style.setProperty('--biblian-a11y-text-scale', String(scale)); 124 } 125 }; 126 127 const applyPreferences = () => { 128 applyRootPreferences(); 129 refreshPanel(); 130 dispatchMotionPreference(); 131 }; 132 133 const openPanel = () => { 134 const panel = document.getElementById('biblian-a11y-panel'); 135 const trigger = document.getElementById('biblian-a11y-fab'); 136 if (!panel || !trigger) { 137 return; 138 } 139 140 panel.hidden = false;
141 trigger.setAttribute('aria-expanded', 'true'); 142 trigger.setAttribute('aria-label', 'Cerrar herramientas de accesibilidad'); 143 refreshPanel(); 144 const firstAvailableControl = panel.querySelector('[data-a11y-action="text-up"]:not(:disabled)') 145 || panel.querySelector('[data-a11y-action="text-down"]:not(:disabled)') 146 || panel.querySelector('[data-a11y-toggle]') 147 || panel.querySelector('[data-a11y-close]'); 148 firstAvailableControl?.focus(); 149 }; 150 151 const closePanel = (restoreFocus = true) => { 152 const panel = document.getElementById('biblian-a11y-panel'); 153 const trigger = document.getElementById('biblian-a11y-fab'); 154 if (!panel || !trigger || panel.hidden) { 155 return; 156 } 157 158 panel.hidden = true; 159 trigger.setAttribute('aria-expanded', 'false'); 160 trigger.setAttribute('aria-label', 'Abrir herramientas de accesibilidad'); 161 if (restoreFocus) { 162 trigger.focus(); 163 } 164 }; 165 166 const togglePanel = () => { 167 const panel = document.getElementById('biblian-a11y-panel'); 168 if (!panel) { 169 return; 170 } 171 if (panel.hidden) { 172 openPanel(); 173 } else { 174 closePanel(); 175 } 176 }; 177 178 const togglePreference = (name, button) => { 179 if (!TOGGLE_NAMES.includes(name)) { 180 return; 181 } 182 state.toggles[name] = !state.toggles[name]; 183 savePreferences(); 184 applyPreferences(); 185 const label = button.querySelector('.biblian-a11y-control__label')?.textContent?.trim() || 'Preferencia'; 186 announce(`${label}: ${state.toggles[name] ? 'activado' : 'desactivado'}.`); 187 }; 188 189 const runAction = (name) => { 190 if (name === 'text-up') { 191 state.scaleIndex = Math.min(SCALE_VALUES.length - 1, state.scaleIndex + 1); 192 announce(`Tamaño de texto: ${Math.round(SCALE_VALUES[state.scaleIndex] * 100)}%.`); 193 } else if (name === 'text-down') { 194 state.scaleIndex = Math.max(0, state.scaleIndex - 1); 195 announce(`Tamaño de texto: ${Math.round(SCALE_VALUES[state.scaleIndex] * 100)}%.`); 196 } else if (name === 'reset') { 197 state = normalizePreferences(null); 198 announce('Preferencias de accesibilidad restablecidas.'); 199 } else { 200 return; 201 } 202 203 savePreferences(); 204 applyPreferences(); 205 }; 206 207 const initialize = () => { 208 if (initialized) { 209 return; 210 } 211 initialized = true; 212 213 const trigger = document.getElementById('biblian-a11y-fab'); 214 const panel = document.getElementById('biblian-a11y-panel'); 215 if (!trigger || !panel) { 216 return; 217 } 218 219 applyPreferences(); 220 221 trigger.addEventListener('click', togglePanel); 222 panel.addEventListener('click', (event) => { 223 if (!(event.target instanceof Element)) { 224 return; 225 } 226 227 if (event.target.closest('[data-a11y-close]')) { 228 closePanel(); 229 return; 230 } 231 232 const toggle = event.target.closest('[data-a11y-toggle]'); 233 if (toggle instanceof HTMLButtonElement) { 234 togglePreference(toggle.dataset.a11yToggle || '', toggle); 235 return; 236 } 237 238 const action = event.target.closest('[data-a11y-action]'); 239 if (action instanceof HTMLButtonElement && !action.disabled) { 240 runAction(action.dataset.a11yAction || ''); 241 } 242 }); 243 244 document.addEventListener('keydown', (event) => { 245 if (event.altKey && event.key.toLowerCase() === 'a') { 246 event.preventDefault(); 247 togglePanel(); 248 } else if (event.key === 'Escape' && !panel.hidden) { 249 event.preventDefault(); 250 closePanel(); 251 } 252 }); 253 254 document.addEventListener('click', (event) => { 255 if (panel.hidden || !(event.target instanceof Node)) { 256 return; 257 } 258 if (!panel.contains(event.target) && !trigger.contains(event.target)) { 259 closePanel(false); 260 } 261 }); 262 }; 263 264 // The script is intentionally loaded in <head>. Apply persisted root classes 265 // before the body paints so contrast, text size and reduced motion do not 266 // flash in their default state during navigation. 267 applyRootPreferences(); 268 document.addEventListener('DOMContentLoaded', initialize, { once: true }); 269 if (document.readyState !== 'loading') { 270 initialize(); 271 } 272 273 window.BiblianAccessibility = { 274 open: openPanel, 275 close: closePanel, 276 reset: () => runAction('reset'), 277 setMotionPaused: (paused) => { 278 const nextPaused = paused === true; 279 if (state.toggles['stop-motion'] === nextPaused) { 280 dispatchMotionPreference(); 281 return; 282 } 283 284 state.toggles['stop-motion'] = nextPaused; 285 savePreferences(); 286 applyPreferences(); 287 announce(`Pausar movimiento: ${nextPaused ? 'activado' : 'de
287sactivado'}.`); 288 }, 289 getState: () => ({ 290 scaleIndex: state.scaleIndex, 291 toggles: { ...state.toggles }, 292 }), 293 }; 294})();
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.