diff --git a/public/app.js b/public/app.js
new file mode 100644
index 0000000..b839791
--- /dev/null
+++ b/public/app.js
@@ -0,0 +1,829 @@
+/* ============================================================
+ 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', '
');
+ } 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 = '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 = '' + 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 = '
Yeni bir belge... (yazmaya başlayın)
';
+ }
+ 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 = 'nexusyazi
Yeni belgenize yazmaya başlayın.
';
+ 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 = '' +
+ (docName || 'Belge') + '' +
+ editor.innerHTML + '';
+ 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 = '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 = '';
+ for (let r = 0; r < rows; r++) {
+ html += '';
+ for (let c = 0; c < cols; c++) {
+ const isHead = r === 0;
+ html += (isHead ? '| ' + (isHead ? 'th' : 'td') + '>';
+ }
+ html += ' |
';
+ }
+ html += '
';
+ 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 = '
';
+ 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();
+})();