1/** 2 * Görsel önizleme katmanı â bir görsele tıklayınca panelin üstünde büyütür. 3 * 4 * NEDEN AYRI MODÃL: posta gövdesindeki görseller bir IFRAME'in içinde duruyor 5 * ve o çerçeve `default-src 'none'` ile kilitli â içinde SCRIPT ÃALIÅTIRILAMAZ 6 * (sandbox'ta `allow-scripts` de yok, bilerek: posta gövdesi düÅman metindir). 7 * Yani önizlemeyi çerçevenin İÃİ açamaz; dinleyiciyi dıÅarıdan panel baÄlar ve 8 * katmanı da panelin kendi belgesinde açar. Katman çerçevenin içinde açılsaydı 9 * yalnız okuma bölmesi kadar büyüyebilirdi â "önizleme" tam da bunun tersi. 10 * 11 * â ï¸ Ä°KİNCİ BİR KOPYA VAR: `chat-flow.js` içinde `_openImageLightbox` / 12 * `_injectLightboxStyles` (sohbet ekleri için) duruyor ve dıÅa AKTARILMAMIÅ. 13 * Buraya taÅınması gerekiyor â tek kaynak olsun diye. Bu turda taÅınMADI: 14 * paralel bir oturumun o dosyada tam o bölgeye 264 satırlık commit edilmemiÅ 15 * iÅi var ve taÅıma onun iÅini çakıÅtırırdı. O iÅ indiÄinde `chat-flow.js`'teki 16 * iki iÅlev buranın ince birer sarmalayıcısına indirilmeli. 17 */ 18import { t } from './i18n.js'; 19 20const KATMAN_ID = 'mzGorselOnizleme'; 21const STIL_ID = 'mz-onz-stil'; 22 23/** Ãeviri + yedek (panelin baÅka modüllerindeki `T` ile aynı sözleÅme). */ 24function T(anahtar, yedek) { 25 const v = t(anahtar); 26 return v !== anahtar ? v : yedek; 27} 28 29/** 30 * Bu görsel önizlemeye DEÄER mi? 31 * 32 * Postalar imza ikonu, çerçeve dolgusu ve 1x1 izleme pikseliyle dolu; hepsini 33 * tıklanır yapmak "önizleme" yerine kazara açılan boÅ bir katman üretirdi. 34 * Ãlçüt ALAN + kısa kenar: 600x40'lık bir baÅlık Åeridi geçer (alan büyük), 35 * 16x16 ikon ve 1x1 piksel geçmez. 36 * 37 * `naturalWidth` görselin GERÃEK boyudur; `width` yazının içinde 38 * küçültülmüŠolabilir â küçük gösterilen büyük bir görsel tam da önizlemesi 39 * en anlamlı olandır. 40 */ 41export function onizlemeyeUygun(im) { 42 if (!im || im.tagName !== 'IMG') return false; 43 // BaÄlantının içindeki görselde TIKLAMA BAÄLANTIYA AİTTİR: gönderen oraya 44 // bir adres koymuÅ, onu çalmak kullanıcının beklediÄi Åeyi bozar. 45 if (im.closest && im.closest('a[href]')) return false; 46 const g = im.naturalWidth || 0; 47 const y = im.naturalHeight || 0; 48 if (!g || !y) return false; // henüz inmemiÅ ya da kırık 49 return (g * y) >= 4096 && Math.min(g, y) >= 24; 50} 51 52/** Bir belgedeki (çerçeve içi olabilir) önizlenebilir görseller, sırayla. */ 53export function onizlenebilirler(belge) { 54 try { 55 return Array.from(belge.images || []).filter(onizlemeyeUygun); 56 } catch { 57 return []; 58 } 59} 60 61function stilleriEkle() { 62 if (document.getElementById(STIL_ID)) return; 63 const s = document.createElement('style'); 64 s.id = STIL_ID; 65 // KATMAN HER ZAMAN KOYU â tema takip etmiyor. Görsel, altındaki zeminden 66 // ayrıÅmalı; açık temada beyaz bir zemine beyaz kenarlı bir ekran görüntüsü 67 // koymak onu "kaybediyor". GörüÅme katmanı da aynı sebeple sabit koyu. 68 // Zemin ve yazı AYNI kuralda tanımlı: biri jetondan gelip öteki sabit 69 // kalırsa tema deÄiÅiminde okunmaz çift üretilir (bkz. tests/test_gece_modu). 70 s.textContent = ` 71 .mz-onz{position:fixed;inset:0;z-index:9800;display:flex;align-items:center; 72 justify-content:center;padding:56px 16px 24px; 73 background:rgba(12,6,22,.92);color:#F5F1FC} 74 .mz-onz-bar{position:absolute;top:0;left:0;right:0;height:48px;display:flex; 75 align-items:center;justify-content:space-between;gap:12px;padding:0 12px; 76 background:rgba(12,6,22,.92);color:#F5F1FC} 77 .mz-onz-ad{font-size:13px;overflow:hidden;text-overflow:ellipsis; 78 white-space:nowrap;max-width:60vw} 79 .mz-onz-sayac{font-size:12px;opacity:.75;margin-left:8px;white-space:nowrap} 80 .mz-onz-eylem{display:flex;align-items:center;gap:4px} 81 .mz-onz-btn{background:rgba(255,255,255,.10);color:#F5F1FC;border:0; 82 border-radius:8px;width:34px;height:34px;display:inline-flex; 83 align-items:center;justify-content:center;cursor:pointer; 84 text-decoration:none} 85 .mz-onz-btn:hover{background:rgba(255,255,255,.20)} 86 .mz-onz-btn:focus-visible{outline:2px solid #C79BE0;outline-offset:2px} 87 .mz-onz-sahne{max-width:100%;max-height:100%;overflow:auto;display:flex; 88 align-items:center;justify-content:center} 89 .mz-onz-img{max-width:92vw;max-height:80vh;object-fit:contain;cursor:zoom-in; 90 background:rgba(255,255,255,.06);border-radius:6px} 91 .mz-onz-img.buyuk{max-width:none;max-height:none;cursor:zoom-out} 92 .mz-onz-ok{position:absolute;top:50%;transform:translateY(-50%); 93 width:44px;height:44px;font-size:22px;line-height:1} 94 .mz-onz-ok.geri{left:10px} 95 .mz-onz-ok.ileri{right:10px} 96 @media (max-width:520px){ 97 .mz-onz-img{max-width:96vw;max-height:70vh} 98 .mz-onz-ad{max-width:40vw} 99 }`; 100 document.head.appendChild(s); 101} 102 103const IKON_KAPAT = 104 '<svg width="17" height="17" viewBox="0 0 24 24" fill="none" stroke="currentColor" ' + 105 'stroke-width="2.2" aria-hidden="true"><path d="M18 6 6 18M6 6l12 12"/></svg>'; 106const IKON_INDIR =
107 '<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" ' + 108 'stroke-width="2" aria-hidden="true"><path d="M12 3v12"/><path d="m7 12 5 5 5-5"/>' + 109 '<path d="M5 21h14"/></svg>'; 110 111/** 112 * Ãnizlemeyi aç. 113 * 114 * @param {Array<{url:string, ad:string}>} liste aynı gövdedeki görseller 115 * @param {number} indeks hangisiyle açılacaÄı 116 */ 117export function gorseliOnizle(liste, indeks = 0) { 118 const g = (liste || []).filter(x => x && x.url); 119 if (!g.length) return; 120 let i = Math.min(Math.max(indeks, 0), g.length - 1); 121 122 stilleriEkle(); 123 document.getElementById(KATMAN_ID)?.remove(); 124 125 const kapatMetni = T('action.close', 'Kapat'); 126 const indirMetni = T('panel.download', 'İndir'); 127 const ov = document.createElement('div'); 128 ov.id = KATMAN_ID; 129 ov.className = 'mz-onz'; 130 ov.setAttribute('role', 'dialog'); 131 ov.setAttribute('aria-modal', 'true'); 132 ov.setAttribute('aria-label', T('onizleme.baslik', 'Görsel önizleme')); 133 ov.innerHTML = ` 134 <div class="mz-onz-bar"> 135 <span style="display:flex;align-items:center;min-width:0"> 136 <span class="mz-onz-ad"></span><span class="mz-onz-sayac"></span> 137 </span> 138 <span class="mz-onz-eylem"> 139 <a class="mz-onz-btn" data-ey="indir" download target="_blank" rel="noopener" 140 title="${indirMetni}" aria-label="${indirMetni}">${IKON_INDIR}</a> 141 <button type="button" class="mz-onz-btn" data-ey="kapat" 142 title="${kapatMetni} (Esc)" aria-label="${kapatMetni}">${IKON_KAPAT}</button> 143 </span> 144 </div> 145 <button type="button" class="mz-onz-btn mz-onz-ok geri" data-ey="geri" 146 aria-label="${T('onizleme.onceki', 'Ãnceki görsel')}">â¹</button> 147 <div class="mz-onz-sahne"><img class="mz-onz-img" alt=""></div> 148 <button type="button" class="mz-onz-btn mz-onz-ok ileri" data-ey="ileri" 149 aria-label="${T('onizleme.sonraki', 'Sonraki görsel')}">âº</button>`; 150 151 const img = ov.querySelector('.mz-onz-img'); 152 const adEl = ov.querySelector('.mz-onz-ad'); 153 const sayacEl = ov.querySelector('.mz-onz-sayac'); 154 const indirEl = ov.querySelector('[data-ey="indir"]'); 155 const geriEl = ov.querySelector('[data-ey="geri"]'); 156 const ileriEl = ov.querySelector('[data-ey="ileri"]'); 157 const kapatEl = ov.querySelector('[data-ey="kapat"]'); 158 159 // Tek görselde ok TUÅLARI DA olmasın: görünmez bir düÄmeye sekmeyle gelmek 160 // "burada bir Åey var" yanılgısı verir. 161 const cokluMu = g.length > 1; 162 geriEl.hidden = !cokluMu; 163 ileriEl.hidden = !cokluMu; 164 165 function goster() { 166 const k = g[i]; 167 // src/alt/href DOM üzerinden yazılıyor: dosya adı postadan geliyor, HTML 168 // birleÅtirmede kaçıŠhatası riski taÅımasın. 169 img.classList.remove('buyuk'); 170 img.src = k.url; 171 img.alt = k.ad || ''; 172 adEl.textContent = k.ad || ''; 173 sayacEl.textContent = cokluMu ? `${i + 1}/${g.length}` : ''; 174 indirEl.href = k.url; 175 indirEl.setAttribute('download', k.ad || 'gorsel'); 176 } 177 const git = (adim) => { i = (i + adim + g.length) % g.length; goster(); }; 178 179 const oncekiOverflow = document.body.style.overflow; 180 const oncekiOdak = document.activeElement; 181 const kapat = () => { 182 document.removeEventListener('keydown', tus, true); 183 ov.remove(); 184 document.body.style.overflow = oncekiOverflow; 185 try { oncekiOdak?.focus?.(); } catch { /* öge gitmiÅ olabilir */ } 186 }; 187 188 function tus(ev) { 189 if (ev.key === 'Escape') { ev.preventDefault(); kapat(); return; } 190 if (cokluMu && ev.key === 'ArrowLeft') { ev.preventDefault(); git(-1); return; } 191 if (cokluMu && ev.key === 'ArrowRight') { ev.preventDefault(); git(1); return; } 192 if (ev.key !== 'Tab') return; 193 // ODAK TUZAÄI: katman açıkken sekme arkadaki panele kaçarsa kullanıcı 194 // göremediÄi bir düÄmeye basar. Görünür düÄmelerle sınırlı, dairesel. 195 const odaklanir = Array.from(ov.querySelectorAll('a[href],button')) 196 .filter(el => !el.hidden && el.offsetParent !== null); 197 if (!odaklanir.length) return; 198 const ilk = odaklanir[0], son = odaklanir[odaklanir.length - 1]; 199 if (ev.shiftKey && document.activeElement === ilk) { ev.preventDefault(); son.focus(); } 200 else if (!ev.shiftKey && document.activeElement === son) { ev.preventDefault(); ilk.focus(); } 201 } 202 203 kapatEl.addEventListener('click', kapat); 204 geriEl.addEventListener('click', () => git(-1)); 205 ileriEl.addEventListener('click', () => git(1)); 206 // Görsele tıklamak SIÄDIR â GERÃEK BOY arasında gidip gelir: faturada ve 207 // ekran görüntüsünde yazı, sıÄdırılmıŠhâlde çoÄu zaman okunmuyor. 208 img.addEventListener('click', () => img.classList.toggle('buyuk')); 209 // Zemine tıklamak kapatır; görsele ve düÄmelere tıklamak KAPATMAZ. 210 ov.addEventListener('click', (ev) => { 211 if (ev.target === ov || ev.target.classList.contains('mz-onz-sahne')) kapat(); 212 }); 213 // `capture`: posta gövdesi ya da baÅka bir modül Escape'i yutmadan önce biz
214 // görelim â katman en üstteki yüzey, kapanma sırası da öyle olmalı. 215 document.addEventListener('keydown', tus, true); 216 217 goster(); 218 document.body.style.overflow = 'hidden'; 219 document.body.appendChild(ov); 220 kapatEl.focus(); 221}
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.