Numex yayın: public/app.js
This commit is contained in:
parent
a9f1e1d44c
commit
5262fa28c1
829
public/app.js
Normal file
829
public/app.js
Normal file
|
|
@ -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', '<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') + '> </' + (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();
|
||||||
|
})();
|
||||||
Loading…
Reference in New Issue
Block a user