272 lines
15 KiB
HTML
272 lines
15 KiB
HTML
{% extends "base.html" %}
|
||
|
||
{% block title %}{{ 'Nieuwe handleiding' if nieuw else 'Bewerken: ' ~ h.Titel }}{% endblock %}
|
||
{% block page_title %}Handleidingen{% endblock %}
|
||
|
||
{% block extra_css %}
|
||
{% include "_handleiding_stijl.html" %}
|
||
<style>
|
||
.terug { margin-bottom: 14px; display: inline-block; }
|
||
.hl-velden { display: grid; grid-template-columns: 2fr 1fr 1fr 110px; gap: 0 14px; }
|
||
@media (max-width: 800px) { .hl-velden { grid-template-columns: 1fr; } }
|
||
.editor-wrap { border: 1px solid var(--border-strong); border-radius: var(--radius); background: var(--surface); }
|
||
.toolbar { display: flex; flex-wrap: wrap; gap: 4px; padding: 6px; border-bottom: 1px solid var(--border);
|
||
background: var(--surface-muted); border-radius: var(--radius) var(--radius) 0 0; position: sticky; top: 0; z-index: 5; }
|
||
.toolbar button, .toolbar select { padding: 4px 9px; font-size: 13px; min-width: 32px; background: var(--surface);
|
||
color: var(--text); border: 1px solid var(--border); border-radius: 6px; cursor: pointer; width: auto; }
|
||
.toolbar button:hover { border-color: var(--primary-color); color: var(--primary-color); }
|
||
.toolbar button.aan { background: var(--primary-light); border-color: var(--primary-color); }
|
||
.toolbar .sep { width: 1px; background: var(--border-strong); margin: 2px 4px; }
|
||
#editor { min-height: 420px; max-height: 70vh; overflow-y: auto; padding: 16px 20px; outline: none; max-width: none; }
|
||
#editor:empty::before { content: "Begin hier te typen… Je kan ook schermafbeeldingen plakken (Ctrl+V) of slepen."; color: var(--text-muted); }
|
||
#editor img { cursor: pointer; }
|
||
#editor img.gekozen { outline: 3px solid var(--primary-color); }
|
||
#bron { display: none; width: 100%; min-height: 420px; font-family: Consolas, monospace; font-size: 13px; border: 0;
|
||
padding: 12px; border-radius: 0 0 var(--radius) var(--radius); }
|
||
.knoppen-onder { display: flex; flex-wrap: wrap; gap: 8px; margin-top: 14px; align-items: center; }
|
||
.versies { margin-top: 8px; }
|
||
.versies li { margin: 4px 0; }
|
||
</style>
|
||
{% endblock %}
|
||
|
||
{% block content %}
|
||
<a class="terug" href="{{ url_for('handleidingen.overzicht') if nieuw else url_for('handleidingen.bekijk', slug=h.Slug) }}">← Terug</a>
|
||
|
||
<div class="card">
|
||
<div class="page-head"><h2>{{ '➕ Nieuwe handleiding' if nieuw else '✏️ Handleiding bewerken' }}</h2></div>
|
||
|
||
<form method="POST" id="hlForm" action="{{ url_for('handleidingen.nieuw') if nieuw else url_for('handleidingen.bewerken', slug=h.Slug) }}">
|
||
<div class="hl-velden">
|
||
<div class="field">
|
||
<label for="titel">Titel</label>
|
||
<input type="text" id="titel" name="titel" maxlength="200" required value="{{ h.Titel or '' }}">
|
||
</div>
|
||
<div class="field">
|
||
<label for="categorie">Categorie</label>
|
||
<input type="text" id="categorie" name="categorie" maxlength="100" list="cats" value="{{ h.Categorie or '' }}" placeholder="bv. Google Workspace">
|
||
<datalist id="cats">{% for c in categorieen %}<option value="{{ c }}">{% endfor %}</datalist>
|
||
</div>
|
||
<div class="field">
|
||
<label for="zichtbaar">Zichtbaar voor</label>
|
||
<select id="zichtbaar" name="zichtbaar">
|
||
{% for k, v in ZICHTBAAR.items() %}<option value="{{ k }}" {% if h.Zichtbaar == k %}selected{% endif %}>{{ v }}</option>{% endfor %}
|
||
</select>
|
||
</div>
|
||
<div class="field">
|
||
<label for="volgorde">Volgorde</label>
|
||
<input type="number" id="volgorde" name="volgorde" value="{{ h.Volgorde or 0 }}">
|
||
</div>
|
||
</div>
|
||
<div class="field">
|
||
<label for="samenvatting">Korte samenvatting <span class="muted small">(toont in de lijst)</span></label>
|
||
<input type="text" id="samenvatting" name="samenvatting" maxlength="300" value="{{ h.Samenvatting or '' }}">
|
||
</div>
|
||
|
||
<label>Inhoud</label>
|
||
<div class="editor-wrap">
|
||
<div class="toolbar" role="toolbar" aria-label="Opmaak">
|
||
<select id="blokstijl" title="Stijl" onchange="blok(this.value); this.value=''">
|
||
<option value="">Stijl…</option>
|
||
<option value="p">Gewone tekst</option>
|
||
<option value="h2">Kop (stap)</option>
|
||
<option value="h3">Tussenkop</option>
|
||
<option value="pre">Codeblok</option>
|
||
</select>
|
||
<span class="sep"></span>
|
||
<button type="button" data-cmd="bold" title="Vet (Ctrl+B)"><b>B</b></button>
|
||
<button type="button" data-cmd="italic" title="Cursief (Ctrl+I)"><i>I</i></button>
|
||
<button type="button" data-cmd="underline" title="Onderlijnen (Ctrl+U)"><u>U</u></button>
|
||
<button type="button" onclick="omring('code')" title="Code / knopnaam"></></button>
|
||
<button type="button" onclick="omring('mark')" title="Markeren">🖍️</button>
|
||
<span class="sep"></span>
|
||
<button type="button" data-cmd="insertOrderedList" title="Genummerde stappen">1.</button>
|
||
<button type="button" data-cmd="insertUnorderedList" title="Opsomming">•</button>
|
||
<button type="button" data-cmd="outdent" title="Minder inspringen">⇤</button>
|
||
<button type="button" data-cmd="indent" title="Meer inspringen">⇥</button>
|
||
<span class="sep"></span>
|
||
<button type="button" onclick="kader('tip')" title="Tip-kader (groen)">💡 Tip</button>
|
||
<button type="button" onclick="kader('let-op')" title="Let op-kader (geel)">⚠️ Let op</button>
|
||
<button type="button" onclick="kader('info')" title="Info-kader (blauw)">ℹ️ Info</button>
|
||
<span class="sep"></span>
|
||
<button type="button" onclick="link()" title="Link">🔗</button>
|
||
<button type="button" data-cmd="unlink" title="Link weghalen">⛓️💥</button>
|
||
<button type="button" onclick="document.getElementById('beeld').click()" title="Afbeelding / schermafbeelding">🖼️</button>
|
||
<button type="button" onclick="tabel()" title="Tabel">▦</button>
|
||
<button type="button" data-cmd="insertHorizontalRule" title="Scheidingslijn">―</button>
|
||
<span class="sep"></span>
|
||
<button type="button" data-cmd="removeFormat" title="Opmaak wissen">🧹</button>
|
||
<button type="button" data-cmd="undo" title="Ongedaan maken (Ctrl+Z)">↶</button>
|
||
<button type="button" data-cmd="redo" title="Opnieuw (Ctrl+Y)">↷</button>
|
||
<span class="sep"></span>
|
||
<button type="button" id="bronKnop" onclick="wisselBron()" title="HTML bekijken/bewerken">HTML</button>
|
||
</div>
|
||
<div id="editor" class="hl-inhoud" contenteditable="true" spellcheck="true">{{ h.Inhoud|safe if h.Inhoud else '' }}</div>
|
||
<textarea id="bron" spellcheck="false" aria-label="HTML"></textarea>
|
||
</div>
|
||
<input type="file" id="beeld" accept="image/*" multiple hidden onchange="bestanden(this.files); this.value=''">
|
||
<input type="hidden" name="inhoud" id="inhoud">
|
||
<p class="field-hint">Tip: plak een schermafbeelding rechtstreeks met Ctrl+V. Klik op een afbeelding en druk Delete om ze te verwijderen.
|
||
Grote afbeeldingen worden automatisch verkleind.</p>
|
||
|
||
<div class="knoppen-onder">
|
||
<button type="submit" class="btn-success">💾 Opslaan</button>
|
||
{% if not nieuw %}<button type="submit" name="verder" value="1" class="btn-light">💾 Opslaan en verder bewerken</button>{% endif %}
|
||
<a class="btn btn-light" href="{{ url_for('handleidingen.overzicht') if nieuw else url_for('handleidingen.bekijk', slug=h.Slug) }}"
|
||
onclick="gewijzigd=false">Annuleren</a>
|
||
</div>
|
||
</form>
|
||
|
||
{% if not nieuw %}
|
||
<form method="POST" action="{{ url_for('handleidingen.verwijderen', slug=h.Slug) }}" style="margin-top:18px"
|
||
onsubmit="gewijzigd=false; return confirm('Handleiding \'{{ h.Titel|e }}\' verwijderen?')">
|
||
<button type="submit" class="btn-danger btn-sm">🗑️ Handleiding verwijderen</button>
|
||
</form>
|
||
{% endif %}
|
||
</div>
|
||
|
||
{% if versies %}
|
||
<div class="card">
|
||
<h3 class="card-title">🕘 Vorige versies</h3>
|
||
<p class="muted small">Bij elke opslag wordt de vorige tekst bewaard (de laatste 30). Open een versie om ze te bekijken of terug te zetten.</p>
|
||
<ul class="versies">
|
||
{% for v in versies %}
|
||
<li><a href="{{ url_for('handleidingen.versie', slug=h.Slug, vid=v.Versie_ID) }}">{{ v.Opgeslagen_op.strftime('%d-%m-%Y %H:%M') }}</a>
|
||
<span class="muted small">{{ v.Door or '' }}{% if v.Titel != h.Titel %} · titel: {{ v.Titel }}{% endif %}</span></li>
|
||
{% endfor %}
|
||
</ul>
|
||
</div>
|
||
{% endif %}
|
||
{% endblock %}
|
||
|
||
{% block scripts %}
|
||
<script>
|
||
(function () {
|
||
var ed = document.getElementById('editor'), bron = document.getElementById('bron');
|
||
var form = document.getElementById('hlForm');
|
||
var bronAan = false;
|
||
window.gewijzigd = false;
|
||
var MAX_BREEDTE = 1400;
|
||
|
||
try { document.execCommand('defaultParagraphSeparator', false, 'p'); } catch (e) {}
|
||
ed.addEventListener('input', function () { gewijzigd = true; });
|
||
['titel', 'categorie', 'samenvatting', 'zichtbaar', 'volgorde'].forEach(function (id) {
|
||
document.getElementById(id).addEventListener('input', function () { gewijzigd = true; });
|
||
});
|
||
window.addEventListener('beforeunload', function (e) { if (gewijzigd) { e.preventDefault(); e.returnValue = ''; } });
|
||
|
||
function cmd(c, v) { ed.focus(); document.execCommand(c, false, v || null); gewijzigd = true; }
|
||
document.querySelectorAll('.toolbar [data-cmd]').forEach(function (b) {
|
||
b.addEventListener('mousedown', function (e) { e.preventDefault(); });
|
||
b.addEventListener('click', function () { cmd(b.dataset.cmd); });
|
||
});
|
||
|
||
window.blok = function (tag) { if (tag) cmd('formatBlock', '<' + tag + '>'); };
|
||
|
||
function selectieHtml() {
|
||
var sel = window.getSelection();
|
||
if (!sel.rangeCount) return '';
|
||
var div = document.createElement('div');
|
||
div.appendChild(sel.getRangeAt(0).cloneContents());
|
||
return div.innerHTML;
|
||
}
|
||
window.omring = function (tag) {
|
||
var h = selectieHtml() || 'tekst';
|
||
cmd('insertHTML', '<' + tag + '>' + h + '</' + tag + '>​');
|
||
};
|
||
window.kader = function (soort) {
|
||
var h = selectieHtml();
|
||
var titel = {tip: 'Tip:', 'let-op': 'Let op:', info: 'Info:'}[soort];
|
||
if (!h) h = '<strong>' + titel + '</strong> ';
|
||
cmd('insertHTML', '<div class="' + soort + '">' + h + '</div><p><br></p>');
|
||
};
|
||
window.link = function () {
|
||
var url = prompt('Link (bv. https://admin.google.com):', 'https://');
|
||
if (!url || url === 'https://') return;
|
||
if (!/^(https?:|mailto:|\/|#)/i.test(url)) url = 'https://' + url;
|
||
if (window.getSelection().isCollapsed) {
|
||
cmd('insertHTML', '<a href="' + url.replace(/"/g, '"') + '" target="_blank">' + url.replace(/</g, '<') + '</a> ');
|
||
} else {
|
||
cmd('createLink', url);
|
||
ed.querySelectorAll('a[href="' + CSS.escape(url) + '"]').forEach(function (a) { a.target = '_blank'; });
|
||
}
|
||
};
|
||
window.tabel = function () {
|
||
var k = parseInt(prompt('Aantal kolommen?', '3'), 10), r = parseInt(prompt('Aantal rijen (zonder kop)?', '3'), 10);
|
||
if (!(k > 0 && r > 0)) return;
|
||
var h = '<table><thead><tr>';
|
||
for (var i = 0; i < k; i++) h += '<th>Kop ' + (i + 1) + '</th>';
|
||
h += '</tr></thead><tbody>';
|
||
for (var j = 0; j < r; j++) { h += '<tr>'; for (i = 0; i < k; i++) h += '<td> </td>'; h += '</tr>'; }
|
||
cmd('insertHTML', h + '</tbody></table><p><br></p>');
|
||
};
|
||
|
||
// --- afbeeldingen: knop, plakken, slepen → verkleinen → data-URL ---
|
||
function voegBeeldIn(file) {
|
||
if (!file || !/^image\//.test(file.type) || /svg/.test(file.type)) return;
|
||
var r = new FileReader();
|
||
r.onload = function () {
|
||
var img = new Image();
|
||
img.onload = function () {
|
||
var schaal = Math.min(1, MAX_BREEDTE / img.width);
|
||
var c = document.createElement('canvas');
|
||
c.width = Math.round(img.width * schaal); c.height = Math.round(img.height * schaal);
|
||
c.getContext('2d').drawImage(img, 0, 0, c.width, c.height);
|
||
var png = c.toDataURL('image/png'), jpg = c.toDataURL('image/jpeg', 0.85);
|
||
var url = (file.type === 'image/png' && png.length < jpg.length * 1.6) ? png : jpg;
|
||
cmd('insertHTML', '<img src="' + url + '" alt="' + (file.name || 'schermafbeelding').replace(/"/g, '') + '"><p><br></p>');
|
||
};
|
||
img.src = r.result;
|
||
};
|
||
r.readAsDataURL(file);
|
||
}
|
||
window.bestanden = function (files) { Array.prototype.forEach.call(files, voegBeeldIn); };
|
||
ed.addEventListener('paste', function (e) {
|
||
var items = (e.clipboardData || {}).items || [], beeld = false;
|
||
Array.prototype.forEach.call(items, function (it) {
|
||
if (it.kind === 'file' && /^image\//.test(it.type)) { voegBeeldIn(it.getAsFile()); beeld = true; }
|
||
});
|
||
if (beeld) e.preventDefault();
|
||
});
|
||
ed.addEventListener('dragover', function (e) { e.preventDefault(); });
|
||
ed.addEventListener('drop', function (e) {
|
||
if (e.dataTransfer && e.dataTransfer.files.length) { e.preventDefault(); bestanden(e.dataTransfer.files); }
|
||
});
|
||
ed.addEventListener('click', function (e) {
|
||
ed.querySelectorAll('img.gekozen').forEach(function (i) { i.classList.remove('gekozen'); });
|
||
if (e.target.tagName === 'IMG') e.target.classList.add('gekozen');
|
||
});
|
||
ed.addEventListener('keydown', function (e) {
|
||
var g = ed.querySelector('img.gekozen');
|
||
if (g && (e.key === 'Delete' || e.key === 'Backspace')) { e.preventDefault(); g.remove(); gewijzigd = true; }
|
||
});
|
||
|
||
// --- HTML-weergave ---
|
||
window.wisselBron = function () {
|
||
bronAan = !bronAan;
|
||
if (bronAan) { bron.value = ed.innerHTML; } else { ed.innerHTML = bron.value; }
|
||
ed.style.display = bronAan ? 'none' : '';
|
||
bron.style.display = bronAan ? 'block' : 'none';
|
||
document.getElementById('bronKnop').classList.toggle('aan', bronAan);
|
||
document.querySelectorAll('.toolbar button:not(#bronKnop), .toolbar select').forEach(function (b) { b.disabled = bronAan; });
|
||
};
|
||
bron.addEventListener('input', function () { gewijzigd = true; });
|
||
|
||
// Ctrl+S = opslaan en verder bewerken
|
||
document.addEventListener('keydown', function (e) {
|
||
if ((e.ctrlKey || e.metaKey) && e.key.toLowerCase() === 's') {
|
||
e.preventDefault();
|
||
var verder = form.querySelector('[name=verder]');
|
||
if (verder) verder.click(); else form.requestSubmit();
|
||
}
|
||
});
|
||
|
||
form.addEventListener('submit', function () {
|
||
ed.querySelectorAll('img.gekozen').forEach(function (i) { i.classList.remove('gekozen'); });
|
||
document.getElementById('inhoud').value = bronAan ? bron.value : ed.innerHTML;
|
||
gewijzigd = false;
|
||
if (typeof showLoader === 'function') showLoader();
|
||
});
|
||
})();
|
||
</script>
|
||
{% endblock %}
|