299 lines
10 KiB
JavaScript
299 lines
10 KiB
JavaScript
// ── State ─────────────────────────────────────────────────────────────────────
|
||
|
||
const { draftId } = BUILDER;
|
||
let chapters = BUILDER.chapters ? BUILDER.chapters.slice() : [];
|
||
let currentIdx = -1;
|
||
let isDirty = false;
|
||
|
||
const AUTOSAVE_MS = 30_000;
|
||
|
||
// ── Init ──────────────────────────────────────────────────────────────────────
|
||
|
||
document.addEventListener('DOMContentLoaded', () => {
|
||
if (typeof BUILDER === 'undefined') {
|
||
// Overzichtspagina — bind verwijder-knoppen drafts
|
||
bindDraftDeleteButtons();
|
||
return;
|
||
}
|
||
|
||
if (chapters.length === 0) {
|
||
apiAddChapter('Hoofdstuk 1', -1);
|
||
} else {
|
||
renderChapterList();
|
||
loadChapter(0);
|
||
}
|
||
|
||
setInterval(() => {
|
||
if (isDirty && currentIdx >= 0) saveCurrentChapter({ silent: true });
|
||
}, AUTOSAVE_MS);
|
||
|
||
bindToolbar();
|
||
|
||
document.addEventListener('keydown', e => {
|
||
if ((e.ctrlKey || e.metaKey) && e.key === 's') {
|
||
e.preventDefault();
|
||
saveCurrentChapter();
|
||
}
|
||
});
|
||
});
|
||
|
||
// ── Toolbar ───────────────────────────────────────────────────────────────────
|
||
|
||
function bindToolbar() {
|
||
document.querySelectorAll('.tb-btn[data-cmd]').forEach(btn => {
|
||
btn.addEventListener('mousedown', e => {
|
||
e.preventDefault();
|
||
document.execCommand(btn.dataset.cmd, false, null);
|
||
getEditor().focus();
|
||
});
|
||
});
|
||
}
|
||
|
||
function getEditor() {
|
||
return document.getElementById('builder-content');
|
||
}
|
||
|
||
// ── Status ────────────────────────────────────────────────────────────────────
|
||
|
||
function markDirty() {
|
||
isDirty = true;
|
||
setStatus('dirty', 'Niet-opgeslagen wijzigingen');
|
||
}
|
||
|
||
function setStatus(type, msg) {
|
||
const el = document.getElementById('save-status');
|
||
if (!el) return;
|
||
el.textContent = msg;
|
||
el.className = 'save-status' + (type ? ' ' + type : '');
|
||
}
|
||
|
||
// ── Chapter list ──────────────────────────────────────────────────────────────
|
||
|
||
function renderChapterList() {
|
||
const el = document.getElementById('chapter-list');
|
||
el.innerHTML = '';
|
||
chapters.forEach((ch, i) => {
|
||
const item = document.createElement('div');
|
||
item.className = 'chapter-item' + (i === currentIdx ? ' active' : '');
|
||
|
||
const label = document.createElement('span');
|
||
label.className = 'chapter-item-title';
|
||
label.textContent = ch.title || `Hoofdstuk ${i + 1}`;
|
||
label.onclick = () => switchChapter(i);
|
||
|
||
const del = document.createElement('button');
|
||
del.className = 'chapter-item-delete';
|
||
del.title = 'Hoofdstuk verwijderen';
|
||
del.textContent = '×';
|
||
del.onclick = e => { e.stopPropagation(); deleteChapter(i); };
|
||
|
||
item.appendChild(label);
|
||
item.appendChild(del);
|
||
el.appendChild(item);
|
||
});
|
||
}
|
||
|
||
// ── Load / switch ─────────────────────────────────────────────────────────────
|
||
|
||
async function switchChapter(idx) {
|
||
if (idx === currentIdx) return;
|
||
if (isDirty) await saveCurrentChapter({ silent: true });
|
||
loadChapter(idx);
|
||
}
|
||
|
||
function loadChapter(idx) {
|
||
currentIdx = idx;
|
||
isDirty = false;
|
||
const ch = chapters[idx];
|
||
const editor = getEditor();
|
||
editor.innerHTML = ch.content || '<p><br></p>';
|
||
editor.oninput = markDirty;
|
||
renderChapterList();
|
||
setStatus('', '');
|
||
}
|
||
|
||
// ── Save ──────────────────────────────────────────────────────────────────────
|
||
|
||
async function saveCurrentChapter({ silent = false } = {}) {
|
||
if (currentIdx < 0) return;
|
||
const content = getEditor().innerHTML;
|
||
const title = chapters[currentIdx].title;
|
||
|
||
try {
|
||
const resp = await fetch(`/api/builder/${draftId}/chapter/${currentIdx}`, {
|
||
method: 'PUT',
|
||
headers: { 'Content-Type': 'application/json' },
|
||
body: JSON.stringify({ title, content }),
|
||
});
|
||
if (resp.ok) {
|
||
chapters[currentIdx].content = content;
|
||
isDirty = false;
|
||
if (!silent) {
|
||
const now = new Date().toLocaleTimeString('nl-NL', { hour: '2-digit', minute: '2-digit' });
|
||
setStatus('saved', `Opgeslagen om ${now}`);
|
||
}
|
||
} else {
|
||
setStatus('error', 'Opslaan mislukt');
|
||
}
|
||
} catch {
|
||
setStatus('error', 'Netwerkfout bij opslaan');
|
||
}
|
||
}
|
||
|
||
// ── Add chapter ───────────────────────────────────────────────────────────────
|
||
|
||
async function addChapter() {
|
||
const title = prompt('Naam van het nieuwe hoofdstuk:', `Hoofdstuk ${chapters.length + 1}`);
|
||
if (!title) return;
|
||
if (isDirty) await saveCurrentChapter({ silent: true });
|
||
await apiAddChapter(title, currentIdx);
|
||
}
|
||
|
||
async function apiAddChapter(title, afterIndex) {
|
||
const resp = await fetch(`/api/builder/${draftId}/chapter`, {
|
||
method: 'POST',
|
||
headers: { 'Content-Type': 'application/json' },
|
||
body: JSON.stringify({ title, after_index: afterIndex }),
|
||
});
|
||
if (!resp.ok) { setStatus('error', 'Hoofdstuk toevoegen mislukt'); return; }
|
||
const data = await resp.json();
|
||
await refreshDraft(data.index);
|
||
}
|
||
|
||
async function refreshDraft(targetIdx) {
|
||
const resp = await fetch(`/api/builder/${draftId}`);
|
||
if (!resp.ok) return;
|
||
const data = await resp.json();
|
||
chapters = data.chapters;
|
||
renderChapterList();
|
||
loadChapter(Math.min(targetIdx ?? currentIdx, chapters.length - 1));
|
||
}
|
||
|
||
// ── Delete chapter ────────────────────────────────────────────────────────────
|
||
|
||
async function deleteChapter(idx) {
|
||
if (chapters.length <= 1) { alert('Kan het laatste hoofdstuk niet verwijderen.'); return; }
|
||
if (!confirm(`Hoofdstuk "${chapters[idx].title}" verwijderen?`)) return;
|
||
const resp = await fetch(`/api/builder/${draftId}/chapter/${idx}`, { method: 'DELETE' });
|
||
if (!resp.ok) { setStatus('error', 'Verwijderen mislukt'); return; }
|
||
const data = await resp.json();
|
||
await refreshDraft(data.index);
|
||
}
|
||
|
||
// ── Toolbar acties ────────────────────────────────────────────────────────────
|
||
|
||
function insertBreak() {
|
||
const editor = getEditor();
|
||
editor.focus();
|
||
const sel = window.getSelection();
|
||
if (!sel || sel.rangeCount === 0) return;
|
||
|
||
const range = sel.getRangeAt(0);
|
||
range.deleteContents();
|
||
|
||
const img = document.createElement('img');
|
||
img.src = '/static/break.png';
|
||
img.style.height = '15px';
|
||
img.className = 'scene-break';
|
||
img.alt = '* * *';
|
||
|
||
const center = document.createElement('center');
|
||
center.appendChild(img);
|
||
|
||
const p = document.createElement('p');
|
||
p.innerHTML = '<br>';
|
||
|
||
range.insertNode(p);
|
||
range.insertNode(center);
|
||
|
||
const newRange = document.createRange();
|
||
newRange.setStart(p, 0);
|
||
newRange.collapse(true);
|
||
sel.removeAllRanges();
|
||
sel.addRange(newRange);
|
||
|
||
markDirty();
|
||
}
|
||
|
||
function wrapBlockquote(cssClass) {
|
||
const editor = getEditor();
|
||
editor.focus();
|
||
const sel = window.getSelection();
|
||
if (!sel || sel.rangeCount === 0) return;
|
||
|
||
const range = sel.getRangeAt(0);
|
||
const selectedText = range.toString();
|
||
if (!selectedText.trim()) return;
|
||
|
||
const bq = document.createElement('blockquote');
|
||
if (cssClass) bq.className = cssClass;
|
||
|
||
const p = document.createElement('p');
|
||
p.textContent = selectedText;
|
||
bq.appendChild(p);
|
||
|
||
range.deleteContents();
|
||
range.insertNode(bq);
|
||
markDirty();
|
||
}
|
||
|
||
// ── Normaliseer ───────────────────────────────────────────────────────────────
|
||
|
||
async function normalizeChapter() {
|
||
if (currentIdx < 0) return;
|
||
const rawHtml = getEditor().innerHTML;
|
||
|
||
const resp = await fetch(`/api/builder/${draftId}/normalize/${currentIdx}`, {
|
||
method: 'POST',
|
||
headers: { 'Content-Type': 'application/json' },
|
||
body: JSON.stringify({ content: rawHtml }),
|
||
});
|
||
if (!resp.ok) { setStatus('error', 'Normaliseren mislukt'); return; }
|
||
const data = await resp.json();
|
||
|
||
if (!confirm('Normalisatie toepassen? Huidige inhoud wordt overschreven.')) return;
|
||
getEditor().innerHTML = data.content;
|
||
markDirty();
|
||
await saveCurrentChapter({ silent: true });
|
||
setStatus('saved', 'Genormaliseerd en opgeslagen');
|
||
}
|
||
|
||
// ── Publiceer ─────────────────────────────────────────────────────────────────
|
||
|
||
async function publishDraft() {
|
||
if (isDirty) await saveCurrentChapter({ silent: true });
|
||
if (!confirm('Dit boek publiceren en aan de library toevoegen?')) return;
|
||
|
||
const btn = document.getElementById('btn-publish');
|
||
btn.disabled = true;
|
||
setStatus('', 'Bezig met publiceren…');
|
||
|
||
try {
|
||
const resp = await fetch(`/api/builder/${draftId}/publish`, { method: 'POST' });
|
||
if (!resp.ok) {
|
||
const err = await resp.json().catch(() => ({}));
|
||
setStatus('error', err.error || 'Publiceren mislukt');
|
||
btn.disabled = false;
|
||
return;
|
||
}
|
||
const data = await resp.json();
|
||
window.location.href = `/library/book/${encodeURIComponent(data.filename)}`;
|
||
} catch {
|
||
setStatus('error', 'Netwerkfout bij publiceren');
|
||
btn.disabled = false;
|
||
}
|
||
}
|
||
|
||
// ── Draft verwijderen (overzichtspagina) ──────────────────────────────────────
|
||
|
||
function bindDraftDeleteButtons() {
|
||
document.querySelectorAll('.draft-card-delete').forEach(btn => {
|
||
btn.addEventListener('click', async () => {
|
||
const id = btn.dataset.id;
|
||
if (!confirm('Draft verwijderen?')) return;
|
||
await fetch(`/api/builder/${id}`, { method: 'DELETE' });
|
||
btn.closest('.draft-card').remove();
|
||
});
|
||
});
|
||
}
|