nexusyazi/app.js
2026-09-09 22:39:52 +00:00

830 lines
30 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

/* ============================================================
NexusYazı — iskelet (client-side kelime islemcisi)
- zengin metin: document.execCommand ile biçim komutları
- localStorage: otomatik kayıt (3sn) + aç/kaydet/yeni
- dışa aktar: HTML / TXT indir
- canlı kelime-karakter sayacı, kısayollar, açık/koyu tema
============================================================ */
(function () {
'use strict';
/* ---------- ELEMANLAR ---------- */
const $ = (s) => document.querySelector(s);
const editor = $('#editor');
const fileNameEl = $('#fileName');
const btnTheme = $('#btnTheme');
const btnNew = $('#btnNew');
const btnSave = $('#btnSave');
const btnOpen = $('#btnOpen');
const btnExport = $('#btnExport');
const btnUndo = $('#btnUndo');
const btnRedo = $('#btnRedo');
const btnClearFmt = $('#btnClearFmt');
// --- İçindekiler (başlık gezinme) ---
const btnToc = $('#btnToc');
const tocPanel = $('#tocPanel');
const tocBody = $('#tocBody');
const tocEmpty = $('#tocEmpty');
// --- Bul & Değiştir paneli ---
const findbar = $('#findbar');
const findInput = $('#findInput');
const replaceInput = $('#replaceInput');
const findCount = $('#findCount');
const btnFindPrev = $('#findPrev');
const btnFindNext = $('#findNext');
const btnReplace = $('#btnReplace');
const btnReplaceAll = $('#btnReplaceAll');
const btnFindClose = $('#findClose');
const selFont = $('#selFont');
const selSize = $('#selSize');
const selBlock = $('#selBlock');
const colorFg = $('#colorFg');
const wrapFg = $('#wrapFg');
const colorBg = $('#colorBg');
const wrapBg = $('#wrapBg');
const saveState = $('#saveState');
const countWords = $('#countWords');
const countChars = $('#countChars');
/* ---------- DURUM ---------- */
const LS_DOC = 'nexusyazi.documento';
const LS_META = 'nexusyazi.meta';
const LS_THEME = 'nexusyazi.tema';
let theme = localStorage.getItem(LS_THEME) || 'light';
let docName = 'Belgesiz.docx';
let dirty = false;
let autoSaveTimer = null;
/* =========================================================
TEMA
========================================================= */
function applyTheme(t) {
theme = t;
document.documentElement.setAttribute('data-theme', t);
try { localStorage.setItem(LS_THEME, t); } catch (e) {}
}
btnTheme.addEventListener('click', () => applyTheme(theme === 'dark' ? 'light' : 'dark'));
/* =========================================================
ZENGİN METİN KOMUTLARI
========================================================= */
function focusEditor() { try { editor.focus(); } catch (e) {} }
function exec(cmd, val) {
editor.focus();
const ok = document.execCommand(cmd, false, val);
editor.focus();
refreshState();
return ok;
}
// araç çubuğundaki data-cmd butonları
document.querySelectorAll('.tool[data-cmd]').forEach((btn) => {
btn.addEventListener('mousedown', (e) => e.preventDefault()); // seçimi koru
btn.addEventListener('click', () => {
exec(btn.dataset.cmd);
});
});
// üst şerit undo/redo
btnUndo.addEventListener('mousedown', (e) => e.preventDefault());
btnUndo.addEventListener('click', () => exec('undo'));
btnRedo.addEventListener('mousedown', (e) => e.preventDefault());
btnRedo.addEventListener('click', () => exec('redo'));
btnClearFmt.addEventListener('mousedown', (e) => e.preventDefault());
btnClearFmt.addEventListener('click', () => exec('removeFormat'));
// renkler
colorFg.addEventListener('input', (e) => exec('foreColor', e.target.value));
wrapFg.addEventListener('mousedown', (e) => e.preventDefault());
colorBg.addEventListener('input', (e) => exec('hiliteColor', e.target.value));
wrapBg.addEventListener('mousedown', (e) => e.preventDefault());
// font ailesi / boyutu
selFont.addEventListener('change', (e) => exec('fontName', e.target.value));
selSize.addEventListener('change', (e) => exec('fontSize', e.target.value));
// blok / başlık stili
selBlock.addEventListener('change', (e) => {
const v = e.target.value;
const after = () => { setBlockActive(v); };
if (v === 'p') {
exec('formatBlock', '<p>');
} else {
exec('formatBlock', '<' + v + '>');
}
setBlockActive(v);
});
/* =========================================================
AKTİF BUTON TAKİBİ (seçime göre vurgula)
========================================================= */
const TOOLS = ['bold', 'italic', 'underline', 'strikeThrough',
'insertUnorderedList', 'insertOrderedList',
'justifyLeft', 'justifyCenter', 'justifyRight', 'justifyFull'];
function refreshState() {
let sel = null;
try { sel = window.getSelection(); } catch (e) {}
if (sel && sel.rangeCount) {
const node = sel.getRangeAt(0).commonAncestorContainer;
const el = node.nodeType === 3 ? node.parentElement : node;
if (el) {
TOOLS.forEach((cmd) => {
const b = document.querySelector('.tool[data-cmd="' + cmd + '"]');
if (b) b.classList.toggle('on', !!document.queryCommandState(cmd));
});
setBlockActive(detectBlock(el));
}
}
// word count live
updateCounts();
// undo/redo durumu
btnUndo.disabled = !canUndo();
btnRedo.disabled = !canRedo();
}
function setBlockActive(tag) {
const map = { H1: 'h1', H2: 'h2', H3: 'h3', P: 'p', DIV: 'p', LI: 'p' };
let cur = map[tag] || 'p';
if (tag === 'P') cur = 'p';
if (tag === 'LI') cur = 'p';
selBlock.value = cur;
}
function detectBlock(el) {
let n = el;
while (n && n !== editor) {
if (n.tagName === 'H1' || n.tagName === 'H2' || n.tagName === 'H3' || n.tagName === 'P') {
return n.tagName;
}
n = n.parentElement;
}
return 'P';
}
function canUndo() {
try { return document.queryCommandEnabled('undo'); } catch (e) { return true; }
}
function canRedo() {
try { return document.queryCommandEnabled('redo'); } catch (e) { return true; }
}
/* =========================================================
KELİME / KARAKTER SAYACI
========================================================= */
function textOf(root) {
const clone = root.cloneNode(true);
clone.querySelectorAll('script,style').forEach((x) => x.remove());
return (clone.textContent || '').replace(/\u00a0/g, ' ');
}
function updateCounts() {
const raw = textOf(editor);
const chars = raw.length;
const words = raw.trim() ? raw.trim().split(/\s+/).length : 0;
countChars.textContent = chars;
countWords.textContent = words;
}
/* =========================================================
DİRTY / OTOMATİK KAYIT (3 sn)
========================================================= */
function markDirty() {
dirty = true;
saveState.className = 'save-ind dirty';
saveState.innerHTML = '<span class="dot"></span>Kaydedilmedi';
scheduleAutoSave();
}
function scheduleAutoSave() {
if (autoSaveTimer) clearTimeout(autoSaveTimer);
autoSaveTimer = setTimeout(save, 3000);
}
/* =========================================================
KAYDET / AÇ / YENİ / DIŞA AKTAR (localStorage)
========================================================= */
function ls() {
try { return localStorage; } catch (e) { return null; }
}
function getMeta() {
try { const m = JSON.parse(localStorage.getItem(LS_META) || '{}'); return m; }
catch (e) { return {}; }
}
function setMeta(patch) {
const m = Object.assign(getMeta(), patch);
try { localStorage.setItem(LS_META, JSON.stringify(m)); } catch (e) {}
}
function save() {
if (!ls()) { setSaveText('Kayıt engellendi'); return; }
const html = editor.innerHTML;
try {
localStorage.setItem(LS_DOC, html);
setMeta({ name: docName, savedAt: new Date().toISOString() });
dirty = false;
setSaveText('Kaydedildi • ' + new Date().toLocaleTimeString('tr-TR', { hour: '2-digit', minute: '2-digit', second: '2-digit' }));
if (autoSaveTimer) { clearTimeout(autoSaveTimer); autoSaveTimer = null; }
} catch (e) {
setSaveText('Kayıt hatası');
}
}
const INFO_WORDS = ['Açıldı', 'İndirildi', 'Hazır', 'yok'];
var savingTextTimer = null;
function setSaveText(txt) {
let cls = 'saving';
if (txt.indexOf('Kaydedildi') !== -1 || INFO_WORDS.some((w) => txt.indexOf(w) !== -1)) {
cls = 'saved';
} else if (txt.indexOf('engellendi') !== -1 || txt.indexOf('hata') !== -1) {
cls = 'dirty';
}
saveState.className = 'save-ind ' + cls;
saveState.innerHTML = '<span class="dot"></span>' + txt;
}
function load() {
if (!ls()) return;
const html = localStorage.getItem(LS_DOC);
if (html) {
editor.innerHTML = html;
// için kirli sayma: temiz başlat
dirty = false;
const meta = getMeta();
if (meta.name) { docName = meta.name; fileNameEl.textContent = docName; }
} else {
editor.innerHTML = '<p>Yeni bir belge... (yazmaya başlayın)</p>';
}
editor.focus();
setCaretToEnd();
refreshState();
}
function setCaretToEnd() {
try {
const range = document.createRange();
range.selectNodeContents(editor);
range.collapse(false);
const sel = window.getSelection();
sel.removeAllRanges();
sel.addRange(range);
} catch (e) {}
}
/* Yeni belge */
function neu() {
const ok = confirm('Yeni bir belge oluşturulsun mu?\nMevcut belge otomatik kaydedilecek.');
if (!ok) return;
save();
docName = 'Belgesiz.docx';
fileNameEl.textContent = docName;
editor.innerHTML = '<h1>nexusyazi</h1><p>Yeni belgenize yazmaya başlayın.</p>';
setMeta({ name: docName });
editor.focus();
refreshState();
}
/* Aç: kayıtlı belgeleri listele */
function openDialog() {
if (!ls()) return setSaveText('Aç engellendi');
// Tek-belge modeli: son taslak otomatik yüklendi. Kutu olarak "son kayıt" içeriği döndürülür.
// Not: proje tek taslak dosyası tutar (belge + ad). Basit & gizlilik dostu.
const cur = localStorage.getItem(LS_DOC);
if (!cur) { setSaveText('Kayıtlı belge yok'); return; }
const meta = getMeta();
const t = meta.savedAt ? new Date(meta.savedAt).toLocaleString('tr-TR') : 'bilinmiyor';
const go = confirm('Son kaydedilen belge yüklensin mi?\nAd: "' + (meta.name || 'Belgesiz') + '"\nSon kayıt: ' + t);
if (go) {
editor.innerHTML = cur;
fileNameEl.textContent = meta.name || 'Belgesiz.docx';
docName = meta.name || 'Belgesiz.docx';
dirty = false;
setSaveText('Açıldı');
}
}
/* Dışa aktar: HTML ve TXT */
function download(name, content, mime) {
const blob = new Blob([content], { type: mime });
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = name;
document.body.appendChild(a);
a.click();
a.remove();
setTimeout(() => URL.revokeObjectURL(url), 300);
}
function exportDialog() {
const fmt = confirm('Nasıl indirmek istersiniz?\n"Tamam" = HTML, "İptal" = TXT (düz metin)');
const base = (docName || 'belge').replace(/\.(docx?|html|txt)$/i, '');
if (fmt) {
const full = '<!DOCTYPE html><html lang="tr"><head><meta charset="utf-8"><title>' +
(docName || 'Belge') + '</title><style>body{font-family:Segoe UI,Arial,sans-serif;max-width:820px;margin:40px auto;line-height:1.7;padding:0 20px} h1,h2,h3{line-height:1.3}</style></head><body>' +
editor.innerHTML + '</body></html>';
download(base + '.html', full, 'text/html;charset=utf-8');
} else {
const plain = editor.innerText || (editor.textContent || '');
download(base + '.txt', plain, 'text/plain;charset=utf-8');
}
setSaveText('İndirildi');
}
/* =========================================================
ZORUNLU KAYIT İÇİN UNSAVED (Ctrl+S)
========================================================= */
function requestSave() {
save();
}
/* =========================================================
OLAYLAR
========================================================= */
btnNew.addEventListener('click', neu);
btnSave.addEventListener('mousedown', (e) => e.preventDefault());
btnSave.addEventListener('click', requestSave);
btnOpen.addEventListener('click', openDialog);
btnExport.addEventListener('click', exportDialog);
/* --- Dosya adını düzenleme (Enter / odak kaybı / Esc) --- */
function commitFileName() {
let name = fileNameEl.textContent.trim().replace(/\s+/g, ' ');
if (!name) { name = 'Belgesiz.docx'; }
// uzantı yoksa .docx ekle
if (!/\.(docx?|html|txt)$/i.test(name)) name += '.docx';
docName = name;
fileNameEl.textContent = name;
dirty = true; // ad değişimi de kopyalanmalı → otomatik kayıt
scheduleAutoSave();
}
fileNameEl.addEventListener('keydown', (e) => {
if (e.key === 'Enter') { e.preventDefault(); fileNameEl.blur(); }
else if (e.key === 'Escape') { e.preventDefault(); fileNameEl.textContent = docName; fileNameEl.blur(); }
e.stopPropagation(); // editor kısayollarına taşmasın
});
fileNameEl.addEventListener('blur', commitFileName);
fileNameEl.addEventListener('input', () => { dirty = false; }); // yazarken 'kirlendi' balonu yanmasın; bitince blur commit eder
/* ============ BUL & DEĞİŞTİR (native window.find tabanlı — sağlam) ============ */
function findCountTotal(q) {
// Tüm eşleşmeleri say: seçimi başa al, native bul'u döngüle
try {
const sel = window.getSelection();
sel.removeAllRanges();
const r = document.createRange();
r.selectNodeContents(editor);
sel.addRange(r);
sel.collapseToStart();
let c = 0;
while (window.find(q, false, false, false)) { c++; }
return c;
} catch (e) { return -1; }
}
function gotoNext(backward) {
const q = findInput.value;
if (!q) { findCount.textContent = ''; editor.focus(); return; }
// native bul: hiç bulamazsa başa dönüp bir kez daha dener
if (!window.find(q, false, backward, false) && !window.find(q, false, backward, false)) {
findCount.textContent = 'Eşleşme yok';
} else {
findCount.textContent = '';
}
}
function openFind(replaceMode) {
findbar.hidden = false;
findInput.focus(); findInput.select();
const rs = document.getElementById('find-sep-repl');
const rc = document.getElementById('replaceInput');
const ra = document.getElementById('btnReplaceAll');
// Değiştir alanına ait unsurlar (ayraç + giriş + buton) yalnız Ctrl+H modunda görünür
if (rs) rs.style.display = replaceMode ? '' : 'none';
if (rc) rc.style.display = replaceMode ? '' : 'none';
if (ra) ra.style.display = replaceMode ? '' : 'none';
}
function closeFind() {
findbar.hidden = true; findCount.textContent = '';
editor.focus();
}
findInput.addEventListener('input', () => { findCount.textContent = ''; });
btnFindNext.addEventListener('click', () => gotoNext(false));
btnFindPrev.addEventListener('click', () => gotoNext(true));
findInput.addEventListener('keydown', (e) => {
if (e.key === 'Enter') { e.preventDefault(); gotoNext(e.shiftKey); }
if (e.key === 'Escape') { closeFind(); }
});
btnFindClose.addEventListener('click', closeFind);
btnReplace.addEventListener('click', () => {
const q = findInput.value, rep = replaceInput.value;
const sel = window.getSelection();
if (!q || !sel.rangeCount) return;
// Teğet eşleşme seçili değilse önce bul
if (!sel.toString().toLowerCase() || !window.find(q, false, false, false)) {
// bulamadıysa ilk geçiş
}
const s = window.getSelection();
if (s.rangeCount && s.toString()) {
s.getRangeAt(0).deleteContents();
s.getRangeAt(0).insertNode(document.createTextNode(rep));
dirty = true; scheduleAutoSave(); updateCounts();
saveState.textContent = '1 kez değiştirildi';
} else {
saveState.textContent = 'Önce bir eşleşme seçin (▼ göz at)';
}
});
btnReplaceAll.addEventListener('click', () => {
const q = findInput.value, rep = replaceInput.value;
if (!q) return;
// Başa dön ve toplamda kaç, sonra hepsini değiştir
const sel = window.getSelection();
const r = document.createRange();
r.selectNodeContents(editor);
sel.removeAllRanges(); sel.addRange(r); sel.collapseToStart();
let count = 0, guard = 0;
while (window.find(q, false, false, false) && guard++ < 2000) {
const s = window.getSelection();
if (!s.rangeCount || !s.toString()) break;
s.getRangeAt(0).deleteContents();
s.getRangeAt(0).insertNode(document.createTextNode(rep));
count++;
}
if (count) { dirty = true; scheduleAutoSave(); updateCounts(); saveState.textContent = count + ' kez değiştirildi'; }
findCount.textContent = count ? ('Tümü: ' + count) : 'Eşleşme yok';
});
document.addEventListener('keydown', (e) => {
const mod = e.ctrlKey || e.metaKey;
if (mod && e.key.toLowerCase() === 'f' && !e.shiftKey) { e.preventDefault(); openFind(false); }
else if (mod && e.key.toLowerCase() === 'h') { e.preventDefault(); openFind(true); }
});
// düzenleme anında -> dirty + sayaç + araç vurgusu
editor.addEventListener('input', markDirty);
editor.addEventListener('keyup', refreshState);
editor.addEventListener('click', refreshState);
editor.addEventListener('mouseup', () => setTimeout(refreshState, 0));
document.addEventListener('selectionchange', () => setTimeout(refreshState, 60));
// klavye kısayolları
document.addEventListener('keydown', (e) => {
const mod = e.ctrlKey || e.metaKey;
if (!mod) return;
const k = e.key.toLowerCase();
if (k === 's') { e.preventDefault(); save(); }
else if (k === 'b') { e.preventDefault(); exec('bold'); }
else if (k === 'i') { e.preventDefault(); exec('italic'); }
else if (k === 'u') { e.preventDefault(); exec('underline'); }
else if (k === 'z') { e.preventDefault(); exec('undo'); }
else if (k === 'y') { e.preventDefault(); exec('redo'); }
else if (k === 'a') { /* default select-all */ }
});
// pencere kapanırken kirli ise kaydet (best-effort)
window.addEventListener('beforeunload', () => { if (dirty) save(); });
/* =========================================================
BAŞLAT
========================================================= */
// Erişilebilirlik: title taşıyan ikon-butonlarına aria-label türet (SVG-only butonlar)
function autoAria() {
document.querySelectorAll('button.icon-btn[title], button.tool[title]').forEach((b) => {
if (!b.getAttribute('aria-label')) {
b.setAttribute('aria-label', (b.getAttribute('title') || '').replace(/\s*\(.*\)$/, '').trim());
}
});
}
function init() {
applyTheme(theme);
autoAria();
load();
saveState.className = 'save-ind saved';
saveState.innerHTML = '<span class="dot"></span>Hazır';
refreshState();
setupExtras();
bindToc();
}
/* =========================================================
GENİŞLETMELER: satır aralığı, girinti, tablo, resim, yazdır
========================================================= */
function parseLH(v) { const n = parseFloat(v); return isNaN(n) ? 0 : n; }
// Seçimin "ilgilendirdiği" blokları (p,h1..h3,li,blockquote) sırayla döndürür.
function selBlocks() {
const s = window.getSelection();
if (!s || s.rangeCount === 0) return [];
const r = s.getRangeAt(0);
const tagSel = 'p,h1,h2,h3,h4,li,blockquote,figure,table';
function nearest(n) {
if (!n) return null;
const el = n.nodeType === 1 ? n : (n.parentNode || null);
return el && typeof el.closest === 'function' ? el.closest(tagSel) : null;
}
const first = nearest(r.startContainer);
const last = nearest(r.endContainer);
if (!first) return [];
if (!last || first === last) return [first];
const out = [first];
let cur = first.nextElementSibling;
while (cur && cur !== last) {
if (cur.matches && cur.matches(tagSel)) out.push(cur);
cur = cur.nextElementSibling;
}
if (last) out.push(last);
return out;
}
// ---- Satır aralığı ----
function setLineHeight(val) {
if (!/^\d+(\.\d+)?$/.test(val)) return;
selBlocks().forEach(function (b) {
b.style.lineHeight = val;
});
markDirty(); refreshState();
}
// ---- Girinti ----
const INDENT_STEP = 24;
function indentBlocks(fn) {
selBlocks().forEach(function (b) {
if (b.tagName === 'LI' || b.tagName === 'BLOCKQUOTE') return;
const cur = parseInt(b.style.paddingLeft, 10) || 0;
let v = cur + INDENT_STEP * (fn > 0 ? 1 : -1);
if (v < 0) v = 0;
if (v > 312) v = 312;
b.style.paddingLeft = (v > 0 ? v + 'px' : '');
});
markDirty(); refreshState();
}
// ---- Tablo: mini panel + grid ----
const TBL_ROWS = 7, TBL_COLS = 8;
var tblHov = { r: 1, c: 1 };
function buildTableGrid() {
const g = $('#tblGrid');
g.innerHTML = '';
for (let r = 0; r < TBL_ROWS; r++) {
for (let c = 0; c < TBL_COLS; c++) {
const cell = document.createElement('div');
cell.className = 'cell';
cell.dataset.r = r + 1;
cell.dataset.c = c + 1;
cell.addEventListener('mouseenter', function () {
g.querySelectorAll('.cell').forEach(function (el) {
el.classList.toggle('hov', el.dataset.r <= (r + 1) && el.dataset.c <= (c + 1));
});
tblHov = { r: r + 1, c: c + 1 };
const lb = $('#tblSizeLabel');
if (lb) lb.textContent = (r + 1) + ' × ' + (c + 1);
});
cell.addEventListener('click', function () {
insertTable(r + 1, c + 1);
closeTblPanel();
});
g.appendChild(cell);
}
}
}
function insertTable(rows, cols) {
let html = '<table>';
for (let r = 0; r < rows; r++) {
html += '<tr>';
for (let c = 0; c < cols; c++) {
const isHead = r === 0;
html += (isHead ? '<th' : '<td') + '>&nbsp;</' + (isHead ? 'th' : 'td') + '>';
}
html += '</tr>';
}
html += '</table><p><br></p>';
const ok = exec('insertHTML', html);
focusEditor();
markDirty();
refreshState();
}
function openTblPanel() {
const btn = $('#btnTable');
buildTableGrid();
const panel = $('#tblPanel');
if (!btn || !panel) return;
const rect = btn.getBoundingClientRect();
const pRect = panel.getBoundingClientRect();
panel.hidden = false;
// Yeniden ölç (fixed, viewport'a göre)
const w = panel.offsetWidth;
let left = rect.left;
if (left + w > window.innerWidth - 8) left = window.innerWidth - w - 8;
if (left < 8) left = 8;
let top = rect.bottom + 6;
if (top + panel.offsetHeight > window.innerHeight - 8) top = rect.top - panel.offsetHeight - 6;
panel.style.left = left + 'px';
panel.style.top = top + 'px';
}
function closeTblPanel() { const p = $('#tblPanel'); if (p) p.hidden = true; }
// ---- Resim ekleme + seçim + yeniden boyutlandırma ----
const inputImg = document.createElement('input');
inputImg.type = 'file';
inputImg.accept = '.png,.jpg,.jpeg,.gif,.bmp,.webp,.svg,image/*';
inputImg.style.display = 'none';
document.body.appendChild(inputImg);
inputImg.addEventListener('change', function () {
const f = inputImg.files && inputImg.files[0];
inputImg.value = '';
if (!f) return;
const fr = new FileReader();
fr.onload = function (ev) {
// dosya boyutu güvenliği: 3 MB üstü büyüklükte "önizleme" resmi gömme (gizlilik dostu)
const src = ev.target.result;
const imgHtml = '<img src="' + src + '" alt="Resim" style="max-width:100%">';
exec('insertHTML', imgHtml);
focusEditor();
markDirty();
refreshState();
};
fr.onerror = function () { };
fr.readAsDataURL(f);
});
// Resim seçilince sarmalayıcı + köşe tutamacı
function attachImgHandle(img) {
// Zaten sarmalanmış mı?
let wrap = img.closest ? img.closest('.img-wrap') : null;
if (!wrap) {
wrap = document.createElement('span');
wrap.className = 'img-wrap';
wrap.contentEditable = 'false';
img.parentNode.insertBefore(wrap, img);
wrap.appendChild(img);
}
document.querySelectorAll('.img-wrap .img-handle').forEach(function (h) { h.remove(); });
img.classList.add('old-sel');
const handle = document.createElement('span');
handle.className = 'img-handle';
handle.title = 'Sürükleyerek boyutlandır';
wrap.appendChild(handle);
let startX, startY, baseW;
const onMove = function (e) {
e.preventDefault();
let w = Math.max(48, Math.round(baseW + (e.clientX - startX)));
const maxW = Math.max(120, editor.getBoundingClientRect().width - 40);
if (w > maxW) w = maxW;
img.style.width = w + 'px';
img.style.height = 'auto';
};
function onUp() {
document.removeEventListener('mousemove', onMove);
document.removeEventListener('mouseup', onUp);
document.body.style.cursor = '';
markDirty();
}
handle.addEventListener('mousedown', function (e) {
e.preventDefault(); e.stopPropagation();
startX = e.clientX; startY = e.clientY;
baseW = img.offsetWidth || 200;
document.body.style.cursor = 'nwse-resize';
document.addEventListener('mousemove', onMove);
document.addEventListener('mouseup', onUp);
});
}
function clearImgSel() {
document.querySelectorAll('.page .old-sel').forEach(function (i) { i.classList.remove('old-sel'); });
document.querySelectorAll('.img-wrap .img-handle').forEach(function (h) { h.remove(); });
}
function handleEditorClick(ev) {
const t = ev.target;
if (t && t.tagName === 'IMG' && editor.contains(t)) {
clearImgSel();
attachImgHandle(t);
ev.preventDefault();
} else if (t && t.classList && !t.classList.contains('img-handle') && t.closest && !t.closest('.img-wrap, .tbl-panel')) {
clearImgSel();
}
}
// ---- Yazdır ----
function doPrint() { window.print(); }
function setupExtras() {
var sel = $('#selLineHeight');
if (sel) {
sel.addEventListener('change', function () { setLineHeight(sel.value); });
}
const bOut = $('#btnOutdent'), bIn = $('#btnIndent');
if (bOut) bOut.addEventListener('click', function () { indentBlocks(-1); });
if (bIn) bIn .addEventListener('click', function () { indentBlocks(1); });
const bTbl = $('#btnTable');
if (bTbl) bTbl.addEventListener('click', function (e) {
const panel = $('#tblPanel');
if (panel && !panel.hidden) { closeTblPanel(); }
else { e.stopPropagation(); openTblPanel(); }
});
const bImg = $('#btnImage');
if (bImg) bImg.addEventListener('click', function () { inputImg.click(); });
const bPr = $('#btnPrint');
if (bPr) bPr.addEventListener('click', doPrint);
if (editor) {
editor.addEventListener('click', handleEditorClick);
}
document.addEventListener('mousedown', function (e) {
const panel = $('#tblPanel');
if (panel && !panel.hidden && e.target.closest && !e.target.closest('#btnTable') && !(panel.contains && panel.contains(e.target))) {
closeTblPanel();
}
});
document.addEventListener('keydown', function (e) {
if (e.key === 'Escape' && $('#tblPanel') && !$('#tblPanel').hidden) { closeTblPanel(); }
});
}
/* =========================================================
İÇİNDEKİLER (başlık gezinme)
========================================================= */
let tocHideTimer = null;
function closeToc() {
if (!tocPanel) return;
if (btnToc) { btnToc.classList.remove('on'); btnToc.setAttribute('aria-expanded', 'false'); }
tocPanel.classList.remove('open');
clearTimeout(tocHideTimer);
tocHideTimer = setTimeout(function () { tocPanel.hidden = true; }, 240);
}
function openToc() {
if (!tocPanel) return;
clearTimeout(tocHideTimer);
tocPanel.hidden = false;
if (btnToc) { btnToc.classList.add('on'); btnToc.setAttribute('aria-expanded', 'true'); }
// yeniden akışı zorla ki translate geçişi çalışsın
void tocPanel.offsetWidth;
tocPanel.classList.add('open');
}
function toggleToc() {
if (!tocPanel) return;
if (tocPanel.classList.contains('open')) { closeToc(); }
else { openToc(); updateToc(); }
}
function updateToc() {
if (!tocBody || !editor || !tocPanel) return;
if (!tocPanel.classList.contains('open')) return; // kapalıyken boşa masraf yok
const heads = editor.querySelectorAll('h1, h2, h3');
tocBody.textContent = '';
if (!heads.length) {
tocBody.hidden = true;
if (tocEmpty) tocEmpty.hidden = false;
return;
}
tocBody.hidden = false;
if (tocEmpty) tocEmpty.hidden = true;
heads.forEach(function (h) {
const txt = (h.textContent || '').trim() || '(Başlıksız)';
const li = document.createElement('li');
const b = document.createElement('button');
b.type = 'button';
b.className = 'toc-item lv-' + h.tagName[1]; // H1→lv-1, H2→lv-2, H3→lv-3
const num = document.createElement('span');
num.className = 'toc-hl';
num.textContent = h.tagName[1];
const sp = document.createElement('span');
sp.className = 'toc-txt';
sp.textContent = txt;
b.appendChild(num);
b.appendChild(sp);
b.addEventListener('click', function () {
h.scrollIntoView({ behavior: 'smooth', block: 'start' });
focusEditor();
});
li.appendChild(b);
tocBody.appendChild(li);
});
}
function bindToc() {
if (btnToc) btnToc.addEventListener('click', toggleToc);
const tc = $('#tocClose');
if (tc) tc.addEventListener('click', closeToc);
document.addEventListener('keydown', function (e) {
if (e.key === 'Escape' && tocPanel && tocPanel.classList.contains('open')) {
closeToc();
}
});
if (editor) {
editor.addEventListener('input', function () { updateToc(); });
editor.addEventListener('keyup', function () { updateToc(); });
}
}
init();
})();