PageSourceSearch

https://morfoz.ai/public/gorsel-onizleme.js?v=1790769371

js morfoz.ai collected 2026-10-05 15:11:07 UTC 10,316 bytes, 221 lines download raw bytes

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.