1/* ========================================================= 2 ERİÅİLEBİLİRLİK MENÃSà 3 BaÄımsız çalıÅır â jQuery / jquery.cookie gerekmez. 4 5 Desteklenen kontroller: 6 1) Panel içindeki [data-toggle] ve [data-action] butonları 7 2) Sayfanın HERHANGİ bir yerindeki [data-toggle]/[data-action] butonları 8 3) Eski üst bar butonları: #Buyut #Kucult #Normal #Oku #Sus 9 10 Kullanım: <script src="/js/erisilebilirlik.js" defer></script> 11 ========================================================= */ 12(function () { 13 "use strict"; 14 15 var STORAGE_KEY = "a11y-ayarlar"; 16 var root = document.documentElement; 17 18 /* group: aynı gruptaki modlardan yalnızca biri açık kalır */ 19 var MODES = { 20 "high-contrast": { group: "tema" }, 21 "dark-mode": { group: "tema" }, 22 "light-mode": { group: "tema" }, 23 "grayscale": { group: "filtre" }, 24 "sepia": { group: "filtre" }, 25 "invert": { group: "filtre" }, 26 "highlight-links": {}, 27 "highlight-titles": {}, 28 "dyslexia": {}, 29 "spacing": {}, 30 "big-cursor": {}, 31 "reading-guide": {}, 32 "reading-mask": {}, 33 "read-mode": { special: true } /* <html>'e sınıf eklenmez */ 34 }; 35 36 var MIN_SCALE = 80, MAX_SCALE = 200, STEP = 10; 37 var TEXT_SELECTOR = 38 "p,li,a,span,h1,h2,h3,h4,h5,h6,td,th,dt,dd,label,button,input,textarea," + 39 "select,figcaption,blockquote,small,strong,em,b,i"; 40 41 var state = { modes: {}, scale: 100 }; 42 var panel, toggleBtn, sizeLabel; 43 var guideEl, maskTop, maskBottom; 44 var lastSpoken = null; 45 var pointerY = null; 46 47 /* ===================== Kayıt ===================== */ 48 49 function save() { 50 try { localStorage.setItem(STORAGE_KEY, JSON.stringify(state)); } catch (e) {} 51 } 52 53 function load() { 54 try { 55 var saved = JSON.parse(localStorage.getItem(STORAGE_KEY) || "{}"); 56 if (saved.modes) state.modes = saved.modes; 57 if (typeof saved.scale === "number") { 58 state.scale = Math.min(MAX_SCALE, Math.max(MIN_SCALE, saved.scale)); 59 } 60 } catch (e) {} 61 } 62 63 /* ===================== Modlar ===================== */ 64 65 function isOn(name) { return state.modes[name] === true; } 66 67 function setMode(name, on) { 68 var cfg = MODES[name]; 69 if (!cfg) return; 70 71 if (on && cfg.group) {
72 Object.keys(MODES).forEach(function (other) { 73 if (other !== name && MODES[other].group === cfg.group && isOn(other)) { 74 state.modes[other] = false; 75 root.classList.remove(other); 76 } 77 }); 78 } 79 80 state.modes[name] = on; 81 if (!cfg.special) root.classList.toggle(name, on); 82 83 if (name === "reading-guide") showGuide(on); 84 if (name === "reading-mask") showMask(on); 85 if (name === "read-mode" && !on) stopSpeech(); 86 87 syncButtons(); 88 save(); 89 } 90 91 function applyAllModes() { 92 Object.keys(MODES).forEach(function (name) { 93 var on = isOn(name); 94 if (!MODES[name].special) root.classList.toggle(name, on); 95 }); 96 showGuide(isOn("reading-guide")); 97 showMask(isOn("reading-mask")); 98 } 99 100 /* ===================== Yazı boyutu ===================== 101 Site fontları px ile tanımlı olduÄundan body'ye yüzde vermek yetmez. 102 Her öÄenin ilk hesaplanan boyutu bir kez saklanır, her deÄiÅiklikte 103 hep o temel deÄer üzerinden hesaplanır (kayma olmaz). */ 104 105 function applyScale() { 106 var factor = state.scale / 100; 107 var nodes = document.querySelectorAll(TEXT_SELECTOR); 108 109 for (var i = 0; i < nodes.length; i++) { 110 var el = nodes[i]; 111 if (el.closest("#a11y")) continue; 112 113 if (!el.dataset.a11yBase) { 114 var px = parseFloat(window.getComputedStyle(el).fontSize); 115 if (!px) continue; 116 el.dataset.a11yBase = String(px); 117 } 118 el.style.fontSize = (factor === 1) 119 ? "" 120 : (parseFloat(el.dataset.a11yBase) * factor).toFixed(2) + "px"; 121 } 122 syncButtons(); 123 } 124 125 function changeScale(delta) { 126 var next = Math.min(MAX_SCALE, Math.max(MIN_SCALE, state.scale + delta)); 127 if (next === state.scale) { flashLimit(); return; } 128 state.scale = next; 129 applyScale(); 130 save(); 131 } 132 133 function resetScale() { 134 state.scale = 100; 135 applyScale(); 136 save(); 137 } 138 139 function flashLimit() { 140 if (!sizeLabel) return; 141 sizeLabel.style.color = "#b02a26"; 142 setTimeout(function () { sizeLabel.style.color = ""; }, 400); 143 } 144 145 /* ===================== Kılavuz / maske ===================== */ 146 147 function buildOverlays() { 148 guideEl = make("a11y-guide"); 149 maskTop = make("a11y-mask-top"); 150 maskBottom = make("a11y-mask-bottom"); 151 152 document.addEventListener("mousemove", function (e) { movePointer(e.clientY); }, { passive: true }); 153 document.addEventListener("touchmove", function (e) { 154 if (e.touches[0]) movePointer(e.touches[0].clientY); 155 }, { passive: true }); 156 window.addEventListener("resize", function () { movePointer(pointerY); }); 157 158 function make(cls) { 159 var d = document.createElement("div"); 160 d.className = cls; 161 d.hidden = true; 162 document.body.appendChild(d); 163 return d; 164 } 165 } 166 167 function movePointer(y) { 168 if (y == null) y = window.innerHeight / 2; 169 pointerY = y; 170 171 if (isOn("reading-guide")) guideEl.style.top = (y - 2) + "px"; 172 173 if (isOn("reading-mask")) { 174 var band = 64; // okunabilir Åerit yüksekliÄi 175 maskTop.style.top = "0px"; 176 maskTop.style.height = Math.max(0, y - band / 2) + "px"; 177 maskBottom.style.top = (y + band / 2) + "px"; 178 maskBottom.style.height = Math.max(0, window.innerHeight - y - band / 2) + "px"; 179 } 180 } 181 182 function showGuide(on) { 183 if (!guideEl) return; 184 guideEl.hidden = !on; 185 if (on) movePointer(pointerY); // açılır açılmaz konumlansın 186 } 187 188 function showMask(on) { 189 if (!maskTop) return; 190 maskTop.hidden = maskBottom.hidden = !on; 191 if (on) movePointer(pointerY); 192 } 193 194 /* ===================== Sesli okuma ===================== */ 195 196 var synth = window.speechSynthesis || null; 197 var pending = null; 198 var keepAlive = null; 199 200 function currentLang() { 201 return /(^|;\s*)googtrans=/.test(document.cookie) ? "en-US" : "tr-TR"; 202 } 203 204 function pickVoice(lang) { 205 var voices = synth.getVoices() || []; 206 var base = lang.split("-")[0]; 207 var exact = voices.filter(function (v) { return v.lang && v.lang.replace("_", "-") === lang; })[0]; 208 if (exact) return exact; 209 return voices.filter(function (v) { return v.lang && v.lang.
209indexOf(base) === 0; })[0] || null; 210 } 211 212 function speak(text) { 213 if (!synth || !text) return; 214 var clean = String(text).replace(/\s+/g, " ").trim(); 215 if (!clean) return; 216 217 synth.cancel(); 218 219 // Sesler ilk çaÄrıda henüz yüklenmemiÅ olabilir: yüklenince tekrar dene 220 if (!synth.getVoices().length && !pending) { 221 pending = clean; 222 synth.addEventListener("voiceschanged", function once() { 223 synth.removeEventListener("voiceschanged", once); 224 var t = pending; pending = null; 225 if (t) speak(t); 226 }); 227 setTimeout(function () { var t = pending; pending = null; if (t) doSpeak(t); }, 800); 228 return; 229 } 230 doSpeak(clean); 231 } 232 233 function doSpeak(clean) { 234 var lang = currentLang(); 235 var voice = pickVoice(lang); 236 237 chunk(clean, 200).forEach(function (part) { 238 var u = new SpeechSynthesisUtterance(part); 239 u.lang = lang; 240 if (voice) u.voice = voice; 241 u.volume = 1; u.rate = 1; u.pitch = 1; 242 synth.speak(u); 243 }); 244 245 // Chrome uzun metinde ~15 sn sonra susar; düzenli resume ile canlı tutulur 246 clearInterval(keepAlive); 247 keepAlive = setInterval(function () { 248 if (!synth.speaking) { clearInterval(keepAlive); return; } 249 synth.pause(); synth.resume(); 250 }, 10000); 251 } 252 253 function chunk(text, size) { 254 var parts = text.match(/[^.!?â¦\n]+[.!?â¦]*\s*/g) || [text]; 255 var out = [], buf = ""; 256 parts.forEach(function (s) { 257 if ((buf + s).length > size && buf) { out.push(buf); buf = ""; } 258 buf += s; 259 while (buf.length > size) { out.push(buf.slice(0, size)); buf = buf.slice(size); } 260 }); 261 if (buf.trim()) out.push(buf); 262 return out; 263 } 264 265 function stopSpeech() { 266 if (synth) synth.cancel(); 267 clearInterval(keepAlive); 268 pending = null; 269 if (lastSpoken) { lastSpoken.classList.remove("a11y-speaking"); lastSpoken = null; } 270 } 271 272 function readPage() { 273 var source = document.querySelector("main, [role=main], #content, .content, .container") || document.body; 274 var copy = source.cloneNode(true); 275 copy.querySelectorAll("#a11y, script, style, noscript, svg, .a11y-guide, .a11y-mask-top, .a11y-mask-bottom") 276 .forEach(function (n) { n.remove(); }); 277 speak(copy.textContent); 278 } 279 280 function onReadModeClick(e) { 281 if (!isOn("read-mode")) return; 282 if (e.target.closest("#a11y")) return; 283 if (e.target.closest("#Oku, #Sus, #Buyut, #Kucult, #Normal")) return; 284 285 var el = e.target.closest( 286 "p,li,a,h1,h2,h3,h4,h5,h6,td,th,button,label,figcaption,blockquote,span,div"); 287 if (!el) return; 288 289 var text = el.innerText || el.textContent; 290 if (!text || !text.trim()) return; 291 292 if (lastSpoken) lastSpoken.classList.remove("a11y-speaking"); 293 el.classList.add("a11y-speaking"); 294 lastSpoken = el; 295 296 speak(text); // preventDefault yok â baÄlantılar çalıÅmaya devam eder 297 } 298 299 /* ===================== Sıfırlama ===================== */ 300 301 function resetAll() { 302 stopSpeech();
303 Object.keys(MODES).forEach(function (name) { 304 state.modes[name] = false; 305 root.classList.remove(name); 306 }); 307 showGuide(false); 308 showMask(false); 309 310 state.scale = 100; 311 document.querySelectorAll("[data-a11y-base]").forEach(function (el) { 312 el.style.fontSize = ""; 313 delete el.dataset.a11yBase; 314 }); 315 316 try { localStorage.removeItem(STORAGE_KEY); } catch (e) {} 317 syncButtons(); 318 } 319 320 /* ===================== Panel ===================== */ 321 322 function openPanel(open) { 323 if (!panel) return; 324 panel.hidden = !open; 325 toggleBtn.setAttribute("aria-expanded", String(open)); 326 toggleBtn.setAttribute("aria-label", 327 open ? "EriÅilebilirlik menüsünü kapat" : "EriÅilebilirlik menüsünü aç"); 328 if (open) { 329 var first = panel.querySelector("button"); 330 if (first) first.focus(); 331 } 332 } 333 334 /* Panel içi + sayfadaki tüm kontroller + eski butonlar tek yerden güncellenir */ 335 function syncButtons() { 336 document.querySelectorAll("[data-toggle]").forEach(function (btn) { 337 if (MODES[btn.dataset.toggle]) { 338 btn.setAttribute("aria-pressed", String(isOn(btn.dataset.toggle))); 339 } 340 }); 341 if (sizeLabel) sizeLabel.textContent = "%" + state.scale; 342 343 // Eski üst bar butonlarının aktif rengi 344 mark("#Oku", isOn("read-mode")); 345 mark("#Sus", !isOn("read-mode")); 346 mark("#Buyut", state.scale !== 100); 347 mark("#Kucult", state.scale !== 100); 348 349 function mark(sel, on) { 350 var el = document.querySelector(sel); 351 if (el) el.style.backgroundColor = on ? "#ab0303" : ""; 352 } 353 } 354 355 /* ===================== Olaylar ===================== */ 356 357 var LEGACY = { 358 "Buyut": function () { changeScale(STEP); }, 359 "Kucult": function () { changeScale(-STEP); }, 360 "Normal": function () { resetScale(); }, 361 "Oku": function () { 362 setMode("read-mode", true); 363 speak("Okuma modu açıldı. Dinlemek istediÄiniz metne tıklayın."); 364 }, 365 "Sus": function () { setMode("read-mode", false); stopSpeech(); } 366 }; 367 368 function onClick(e) { 369 var btn = e.target.closest("button, a[data-action], a[data-toggle], [role=button]"); 370 if (!btn) return; 371 372 // 1) Eski üst bar butonları 373 if (btn.id && LEGACY[btn.id]) { e.preventDefault(); LEGACY[btn.id](); return; } 374 375 // 2) Mod düÄmeleri (sayfanın her yerinde çalıÅır) 376 if (btn.dataset.toggle && MODES[btn.dataset.toggle]) { 377 e.preventDefault(); 378 var name = btn.dataset.toggle; 379 setMode(name, !isOn(name)); 380 if (name === "read-mode" && isOn(name)) { 381 speak("Okuma modu açıldı. Dinlemek istediÄiniz metne tıklayın."); 382 } 383 return; 384 } 385 386 // 3) Eylem düÄmeleri 387 switch (btn.dataset.action) { 388 case "increaseText": e.preventDefault(); changeScale(STEP); break; 389 case "decreaseText": e.preventDefault(); changeScale(-STEP); break; 390 case "normalText": e.preventDefault(); resetScale(); break; 391 case "readPage": e.preventDefault(); readPage(); break; 392 case "stopRead": e.preventDefault(); stopSpeech(); break; 393 case "reset": e.preventDefault(); resetAll(); break; 394 case "close": e.preventDefault(); openPanel(false); toggleBtn.focus(); break; 395 } 396 } 397 398 function bind() { 399 if (toggleBtn) { 400 toggleBtn.addEventListener("click", function (e) { 401 e.stopPropagation(); 402 openPanel(panel.hidden); 403 }); 404 } 405 406 document.addEventListener("click", onClick); 407 document.addEventListener("click", onReadModeClick, true); 408 409 document.addEventListener("click", function (e) { 410 if (panel && !panel.hidden && !e.target.closest("#a11y")) openPanel
410(false); 411 }); 412 413 document.addEventListener("keydown", function (e) { 414 if (e.key === "Escape" && panel && !panel.hidden) { openPanel(false); toggleBtn.focus(); } 415 }); 416 417 window.addEventListener("beforeunload", stopSpeech); 418 419 // Sesli okuma desteklenmiyorsa ilgili butonları devre dıÅı bırak 420 if (!synth) { 421 document.querySelectorAll( 422 '[data-action="readPage"],[data-action="stopRead"],[data-toggle="read-mode"],#Oku,#Sus' 423 ).forEach(function (b) { 424 b.disabled = true; 425 b.title = "Tarayıcınız sesli okumayı desteklemiyor"; 426 b.style.opacity = ".5"; 427 }); 428 } 429 } 430 431 /* ===================== BaÅlangıç ===================== */ 432 433 function init() { 434 panel = document.getElementById("a11yPanel"); 435 toggleBtn = document.getElementById("a11yToggle"); 436 sizeLabel = document.getElementById("a11ySizeLabel"); 437 438 buildOverlays(); 439 load(); 440 applyAllModes(); 441 applyScale(); 442 bind(); 443 syncButtons(); 444 } 445 446 if (document.readyState === "loading") { 447 document.addEventListener("DOMContentLoaded", init); 448 } else { 449 init(); 450 } 451})();
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.