1/* global localStorage */ 2/* Prabhu Bank â Accessibility Widget Controller 3 WCAG 2.1 AA | Runs in <head> for FOAC prevention */ 4(function () { 5 'use strict'; 6 7 var STORAGE_KEY = 'prabhu_a11y'; 8 var html = document.documentElement; 9 10 var DEFAULTS = { 11 contrast: 'normal', 12 tintColor: '#ffff00', 13 tintOpacity: 0, 14 saturation: 100, 15 fontSize: '1', 16 highlightTitles: false, 17 highlightLinks: false, 18 dyslexiaFont: false, 19 letterSpacing: false, 20 lineHeight: false, 21 fontWeight: false, 22 readingGuide: false, 23 stopAnimations: false, 24 bigCursor: false 25 }; 26 27 var FONT_OPTIONS = [ 28 { scale: '1', label: 'A' }, 29 { scale: '1.15', label: 'A+' }, 30 { scale: '1.3', label: 'A++'} 31 ]; 32 33 /* ---- Persistence ---- */ 34 35 function loadPrefs() { 36 try { 37 var raw = localStorage.getItem(STORAGE_KEY); 38 return raw ? JSON.parse(raw) : clone(DEFAULTS); 39 } catch (e) { 40 return clone(DEFAULTS); 41 } 42 } 43 44 function savePrefs(p) { 45 try { localStorage.setItem(STORAGE_KEY, JSON.stringify(p)); } catch (e) { /* ignore */ } 46 } 47 48 function clone(obj) { 49 return JSON.parse(JSON.stringify(obj)); 50 } 51 52 /* ---- Apply functions (safe to call before body exists) ---- */ 53 54 function applyContrast(mode) { 55 html.classList.remove('a11y-high-contrast', 'a11y-inverted'); 56 if (mode === 'high-contrast') html.classList.add('a11y-high-contrast'); 57 if (mode === 'inverted') html.classList.add('a11y-inverted'); 58 updateBodyFilter(); 59 } 60 61 /* Combines greyscale contrast + saturation into a single CSS filter on body children */ 62 function updateBodyFilter() { 63 var filters = []; 64 if (prefs.contrast === 'grayscale') filters.push('grayscale(1)'); 65 var sat = prefs.saturation !== undefined ? parseInt(prefs.saturation, 10) : 100;
66 if (sat < 100) filters.push('saturate(' + sat + '%)'); 67 html.style.setProperty('--a11y-body-filter', filters.join(' ') || 'none'); 68 html.classList.toggle('a11y-body-filtered', filters.length > 0); 69 } 70 71 function applyFontScale(scale) { 72 var s = parseFloat(scale) || 1; 73 html.style.setProperty('--a11y-font-scale', String(s)); 74 html.classList.toggle('a11y-font-scaled', s !== 1); 75 } 76 77 function applyTint(color, opacity) { 78 var overlay = document.getElementById('a11y-tint-overlay'); 79 if (!overlay) return; 80 overlay.style.backgroundColor = opacity > 0 ? color : 'transparent'; 81 overlay.style.opacity = opacity > 0 ? opacity : 0; 82 } 83 84 function applySaturation(value) { 85 prefs.saturation = value !== undefined ? parseInt(value, 10) : 100; 86 updateBodyFilter(); 87 } 88 89 function applyHighlightTitles(on) { html.classList.toggle('a11y-highlight-titles', !!on); } 90 function applyHighlightLinks(on) { html.classList.toggle('a11y-highlight-links', !!on); } 91 function applyDyslexiaFont(on) { html.classList.toggle('a11y-dyslexia-font', !!on); } 92 function applyLetterSpacing(on) { html.classList.toggle('a11y-letter-spacing', !!on); } 93 function applyLineHeight(on) { html.classList.toggle('a11y-line-height', !!on); } 94 function applyFontWeight(on) { html.classList.toggle('a11y-font-weight', !!on); } 95 function applyStopAnimations(on) { html.classList.toggle('a11y-stop-animations', !!on); } 96 function applyReadingGuide(on) { html.classList.toggle('a11y-reading-guide-active', !!on); } 97 function applyBigCursor(on) { html.classList.toggle('a11y-big-cursor', !!on); } 98 99 /* ---- Early apply (before DOMContentLoaded, prevents FOAC) ---- */ 100 var prefs = loadPrefs(); 101 applyContrast(prefs.contrast); 102 applyFontScale(prefs.fontSize); 103 applyStopAnimations(prefs.stopAnimations); 104 /* tint, saturation, and overlay features need body â deferred to DOMContentLoaded */ 105 106 /* ---- Widget build (after DOM ready) ---- */ 107 document.addEventListener('DOMContentLoaded', function () { 108 109 applySaturation(prefs.saturation); 110 111 /* Inject tint overlay div */ 112 var overlay = document.createElement('div'); 113 overlay.id = 'a11y-tint-overlay'; 114 document.body.appendChild(overlay); 115 applyTint(prefs.tintColor, prefs.tintOpacity); 116 117 /* Inject reading guide div */ 118 var guide = document.createElement('div'); 119 guide.id = 'a11y-reading-guide'; 120 document.body.appendChild(guide); 121 122 document.addEventListener('mousemove', function (e) { 123 if (prefs.readingGuide) { guide.style.top = e.clientY + 'px'; } 124 }); 125 document.addEventListener('touchmove', function (e) { 126 if (prefs.readingGuide && e.touches.length) { 127 guide.style.top = e.touches[0].clientY + 'px'; 128 } 129 }, { passive: true }); 130 131 /* Inject big cursor div â disabled on touch-only devices */ 132 var bigCursorEl = document.createElement('div'); 133 bigCursorEl.id = 'a11y-big-cursor-el'; 134 document.body.appendChild(bigCursorEl); 135 136 if (window.matchMedia('(pointer: coarse)').matches) { 137 prefs.bigCursor = false; 138 } else { 139 document.addEventListener('mousemove', function (e) { 140 if (prefs.bigCursor) { 141 bigCursorEl.style.transform = 'translate(' + e.clientX + 'px,' + e.clientY + 'px)'; 142 } 143 }); 144 } 145 146 applyHighlightTitles(prefs.highlightTitles); 147 applyHighlightLinks(prefs.highlightLinks); 148 applyDyslexiaFont(prefs.dyslexiaFont); 149 applyLetterSpacing(prefs.letterSpacing); 150 applyLineHeight(prefs.lineHeight); 151 applyFontWeight(prefs.fontWeight); 152 applyReadingGuide(prefs.readingGuide); 153 applyBigCursor(prefs.bigCursor); 154 155 buildWidget(); 156 }); 157 158 function buildWidget() { 159 var widget = document.createElement('div'); 160 widget.id = 'a11y-widget'; 161 widget.innerHTML = 162 '<div id="a11y-panel" role="dialog" aria-label="Accessibility Controls" aria-modal="false">' + 163 ' <p class="a11y-panel-title">Accessibility</p>' + 164 165 ' <div class="a11y-section">' + 166 ' <span class="a11y-section-label" id="a11y-contrast-label">Colour Contrast</span>' + 167 ' <div class="a11y-btn-group a11y-btn-group-2col" role="group" aria-labelledby="a11y-contrast-label">' + 168 ' <button class="a11y-btn" data-contrast="normal" aria-pressed="false">Normal</button>' + 169 ' <button class="a11y-btn" data-contrast="high-contrast" aria-pressed="false">High Contrast</button>' + 170 ' <button class="a11y-btn" data-contrast="inverted" aria-pressed="false">Inverted</button>' + 171 ' <button class="a11y-btn" data-contrast="grayscale" aria-pressed="false">Grayscale</button>' + 172 ' </div>' + 173 ' </div>' + 174 175 ' <div class="a11y-section">' + 176 ' <span class="a11y-section-label">Colour Tint</span>' + 177 ' <div class="a11y-control-row">' + 178 ' <label for="a11y-tint-color">Colour</label>' + 179 ' <input type="color" id="a11y-tint-color" value="' + esc(prefs.tintColor) + '" aria-label="Tint colour picker">' + 180 ' </div>' + 181 ' <div class="a11y-control-row">' + 182 ' <label for="a11y-tint-opacity">Opacity</label>' + 183 ' <input type="range" id="a11y-tint-opacity" class="a11y-slider"' + 184 ' min="0" max="0.4" step="0.01" value="' + (prefs.tintOpacity || 0) + '"' + 185 ' aria-label="Tint opacity">' + 186 ' </div>' + 187 ' </div>' + 188 189 ' <div class="a11y-section">' + 190 ' <span class="a11y-section-label">Saturation</span>' + 191 ' <div class="a11y-control-row">' + 192 ' <label for="a11y-saturation">Level</label>' + 193 ' <input type="range" id="a11y-saturation" class="a11y-slider"' + 194 ' min="0" max="100" step="1" value="' + (prefs.saturation !== undefined ? prefs.saturation : 100) + '"' + 195 ' aria-label="Colour saturation (0 = greyscale, 100 = full colour)">' + 196 ' </div>' + 197 ' </div>' + 198 199 ' <div class="a11y-section">' + 200 ' <span class="a11y-section-label" id="a11y-font-label">Text Size</span>' + 201 ' <div class="a11y-btn-group" role="group" aria-labelledby="a11y-font-label">' + 202 FONT_OPTIONS.map(function (f) { 203 return '<button class="a11y-btn" data-font="' + f.scale + '" aria-pressed="false">' + f.label + '</button>'; 204 }).join('') + 205 ' </div>' + 206 ' </div>' + 207 208 ' <span class="a11y-cat-header">' + 209 ' <i class="fas fa-pen-nib" aria-hidden="true"></i> Content Adjustment' + 210 ' </span>' + 211 ' <div class="a11y-section">' + 212 ' <div class="a11y-btn-group a11y-btn-group-2col" role="group" aria-label="Content adju
212stment options">' + 213 ' <button class="a11y-btn" data-toggle="highlightTitles" aria-pressed="false"><i class="fas fa-heading" aria-hidden="true"></i> Highlight Titles</button>' + 214 ' <button class="a11y-btn" data-toggle="highlightLinks" aria-pressed="false"><i class="fas fa-link" aria-hidden="true"></i> Highlight Links</button>' + 215 ' <button class="a11y-btn" data-toggle="dyslexiaFont" aria-pressed="false"><i class="fas fa-font" aria-hidden="true"></i> Dyslexia Font</button>' + 216 ' <button class="a11y-btn" data-toggle="letterSpacing" aria-pressed="false"><i class="fas fa-arrows-alt-h" aria-hidden="true"></i> Letter Spacing</button>' + 217 ' <button class="a11y-btn" data-toggle="lineHeight" aria-pressed="false"><i class="fas fa-text-height" aria-hidden="true"></i> Line Height</button>' + 218 ' <button class="a11y-btn" data-toggle="fontWeight" aria-pressed="false"><i class="fas fa-bold" aria-hidden="true"></i> Bold Text</button>' + 219 ' </div>' + 220 ' </div>' + 221 222 ' <span class="a11y-cat-header">' + 223 ' <i class="fas fa-tools" aria-hidden="true"></i> Tools' + 224 ' </span>' + 225 ' <div class="a11y-section">' + 226 ' <div class="a11y-btn-group a11y-btn-group-2col" role="group" aria-label="Accessibility tools">' + 227 ' <button class="a11y-btn" data-toggle="readingGuide" aria-pressed="false"><i class="fas fa-ruler-horizontal" aria-hidden="true"></i> Reading Guide</button>' + 228 ' <button class="a11y-btn" data-toggle="stopAnimations" aria-pressed="false"><i class="fas fa-pause-circle" aria-hidden="true"></i> Stop Animations</button>' + 229 ' <button class="a11y-btn" data-toggle="bigCursor" aria-pressed="false"><i class="fas fa-mouse-pointer" aria-hidden="true"></i> Big Cursor</button>' + 230 ' </div>' + 231 ' </div>' + 232 233 ' <button id="a11y-reset" aria-label="Reset all accessibility settings to defaults">Reset</button>' + 234 '</div>' + 235 236 '<button id="a11y-toggle" aria-expanded="false" aria-controls="a11y-panel"' + 237 ' aria-label="Open accessibility controls" title="Accessibility">' + 238 ' <i class="fas fa-universal-access" aria-hidden="true"></i>' + 239 '</button>'; 240 241 document.body.appendChild(widget); 242 243 var toggle = document.getElementById('a11y-toggle'); 244 var panel = document.getElementById('a11y-panel'); 245 246 syncActiveStates(); 247 248 /* Toggle panel open/close */ 249 toggle.addEventListener('click', function () { 250 var open = panel.classList.toggle('a11y-open'); 251 toggle.setAttribute('aria-expanded', String(open)); 252 if (open) { 253 var first = panel.querySelector('button, input'); 254 if (first) first.focus(); 255 } 256 }); 257 258 /* Nav toggle (mobile only) â wired to the same panel */ 259 var navToggle = document.getElementById('a11y-nav-toggle'); 260 if (navToggle) { 261 navToggle.addEventListener('click', function () { 262 var open = panel.classList.toggle('a11y-open'); 263 toggle.setAttribute('aria-expanded', String(open)); 264 navToggle.setAttribute('aria-expanded', String(open)); 265 if (open) { 266 var first = panel.querySelector('button, input'); 267 if (first) first.focus(); 268 } 269 }); 270 } 271 272 /* Close on Escape â syncs aria-expanded on both buttons */ 273 document.addEventListener('keydown', function (e) { 274 if ((e.key === 'Escape' || e.keyCode === 27) && panel.classList.contains('a11y-open')) { 275 panel.classList.remove('a11y-open'); 276 toggle.setAttribute('aria-expanded', 'false'); 277 if (navToggle) navToggle.setAttribute('aria-expanded', 'false'); 278 toggle.focus(); 279 } 280 }); 281 282 /* Contrast buttons */ 283 panel.querySelectorAll('[data-contrast]').forEach(function (btn) { 284 btn.addEventListener('click', function () { 285 prefs.contrast = btn.getAttribute('data-contrast'); 286 applyContrast(prefs.contrast); 287 savePrefs(prefs); 288 syncActiveStates(); 289 }); 290 }); 291 292 /* Tint colour */ 293 document.getElementById('a11y-tint-color').addEventListener('input', function () { 294 prefs.tintColor = this.value; 295 applyTint(prefs.tintColor, prefs.tintOpacity); 296 savePrefs(prefs); 297 }); 298 299 /* Tint opacity */ 300 document.getElementById('a11y-tint-opacity').addEventListener('input', function () { 301 prefs.tintOpacity = parseFloat(this.value); 302 applyTint(prefs.tintColor, prefs.tintOpacity); 303 savePrefs(prefs); 304 }); 305 306 /* Saturation */ 307 document.getElementById('a11y-saturation').addEventListener('input', function () { 308 prefs.saturation = parseInt(this.value, 10); 309 applySaturation(prefs.saturation); 310 savePrefs(prefs); 311 }); 312 313 /* Font size buttons */ 314 panel.querySelectorAll('[data-font]').forEach(function (btn) { 315 btn.addEventListener('click', function () { 316 prefs.fontSize = btn.getAttribute('data-font'); 317 applyFontScale(prefs.fontSize); 318 savePrefs(prefs); 319 syncActiveStates(); 320 }); 321 }); 322 323 /* applyFnMap â maps data-toggle key â apply function */ 324 var applyFnMap = { 325 highlightTitles: applyHighlightTitles, 326 highlightLinks: applyHighlightLinks, 327 dyslexiaFont: applyDyslexiaFont, 328 letterSpacing: applyLetterSpacing, 329 lineHeight: applyLineHeight, 330 fontWeight: applyFontWeight, 331 readingGuide: applyReadingGuide, 332 stopAnimations: applyStopAnimations, 333 bigCursor: applyBigCursor 334 }; 335 336 /* Content Adjustment + Tools toggle buttons */ 337 panel.querySelectorAll('[data-toggle]').forEach(function (btn) { 338 btn.addEventListener('click', function () { 339 var key = btn.getAttribute('data-toggle'); 340 prefs[key] = !prefs[key]; 341 applyFnMap[key](prefs[key]); 342 savePrefs(prefs); 343 syncActiveStates(); 344 }); 345 }); 346 347 /* Reset */ 348 document.getElementById('a11y-reset').addEventListener('click', function () { 349 prefs = clone(DEFAULTS); 350 savePrefs(prefs); 351 applyContrast(prefs.contrast); 352 applyFontScale(prefs.fontSize); 353 applySaturation(prefs.saturation); 354 applyTint(prefs.tintColor, prefs.tintOpacity); 355 document.getElementById('a11y-tint-color').value = prefs.tintColor; 356 document.getElementById('a11y-tint-opacity').value = prefs.tintOpacity; 357 document.getElementById('a11y-saturation').value = prefs.saturation; 358 applyHighlightTitles(false); 359 applyHighlightLinks(false); 360 applyDyslexiaFont(false); 361 applyLetterSpacing(false); 362 applyLineHeight(false); 363 applyFontWeight(false); 364 applyReadingGuide(false); 365 applyStopAnimations(false); 366 applyBigCursor(false); 367 syncActiveStates(); 368 }); 369 } 370 371 function syncActiveStates() { 372 var widget = document.getElementById('a11y-widget'); 373 if (!widget) return; 374 widget.querySelectorAll('[data-contrast]').forEach(function (btn) { 375 var active = btn.getAttribute('data-contrast') === (prefs.contrast || 'normal'); 376 btn.classList.toggle('a11y-active', active); 377 btn.setAttribute('aria-pressed', String(active)); 378 }); 379 widget.querySelectorAll('[data-font]').forEach(function (btn) { 380 var active = btn.getAttribute('data-font') === (prefs.fontSize || '1'); 381 btn.classList.toggle('a11y-active', active); 382 btn.setAttribute('aria-pressed', String(active)); 383 }); 384 widget.querySelectorAll('[data-toggle]').forEach(function (btn) { 385 var key = btn.getAttribute('data-toggle'); 386 var active = !!prefs[key]; 387 btn.classList.toggle('a11y-active', active); 388 btn.setAttribute('aria-pressed', String(active)); 389 }); 390 } 391 392 function esc(str) { 393 return String(str).replace(/"/g, '"'); 394 } 395 396}());
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.