Files
E.Noorlander cd498c8c3a v2.5.1: Admin theme refactor, Navigation plugin, user roles, guide restructure
- Reorganize admin into admin/theme/default/ (views + assets)
- Rename GuideNav to Navigation plugin (essential, protected)
- Plugin assets support (SCSS/CSS) loaded after theme CSS
- User roles: Admin, Content Manager, BI Manager, Site Admin
- Role-based access control (RBAC) for admin routes and sidebar
- Guide restructure: sub-topics in separate folders with sidebar nav
- Dynamic breadcrumb for homepage and subdirectories
- Fix theme path traversal (../../ -> ../) in admin.php
- Fix CodeMirror mode load order (xml -> css -> js -> htmlmixed -> php)
- Fix editor-toolbar.js null checks for plugin edit pages
- Layout select from theme.json with live frontmatter update
- Footer sticky at bottom of viewport (min-height: 100vh)
- Breadcrumb color fix (var(--nav-font) -> var(--header-bg))
- Remove language switcher from guide pages
- Update README.md and README.en.md
- Bump version to 2.5.1
2026-08-10 15:36:29 +02:00

274 lines
9.8 KiB
JavaScript

(function () {
'use strict';
var textarea = document.getElementById('editor-textarea');
if (!textarea || typeof CodeMirror === 'undefined') return;
var ext = textarea.dataset.ext || 'md';
var form = document.getElementById('editor-form') || textarea.closest('form');
var modeMap = { md: 'markdown', html: 'htmlmixed', php: 'php' };
var editor = CodeMirror.fromTextArea(textarea, {
mode: modeMap[ext] || 'markdown',
lineNumbers: true,
lineWrapping: true,
matchBrackets: true,
autoCloseBrackets: true,
styleActiveLine: true,
indentUnit: 4,
tabSize: 4,
indentWithTabs: true,
viewportMargin: Infinity,
extraKeys: {
'Ctrl-S': function () { if (form) form.submit(); },
'Cmd-S': function () { if (form) form.submit(); }
}
});
// Expose editor globally so other scripts can access it
window.codeMirrorEditor = editor;
var commands = {
md: [
{ cmd: 'bold', icon: 'bi-type-bold', title: 'Vet' },
{ cmd: 'italic', icon: 'bi-type-italic', title: 'Cursief' },
{ cmd: 'heading', icon: 'bi-type-h2', title: 'Kop' },
{ cmd: 'link', icon: 'bi-link-45deg', title: 'Link' },
null,
{ cmd: 'ulist', icon: 'bi-list-ul', title: 'Ongenummerde lijst' },
{ cmd: 'olist', icon: 'bi-list-ol', title: 'Genummerde lijst' },
{ cmd: 'code', icon: 'bi-code-slash', title: 'Code' },
{ cmd: 'quote', icon: 'bi-chat-quote', title: 'Citaat' },
null,
{ cmd: 'hr', icon: 'bi-hr', title: 'Horizontale lijn' },
null,
{ cmd: 'media', icon: 'bi-images', title: 'Media invoegen' }
],
html: [
{ cmd: 'strong', icon: 'bi-type-bold', title: '<strong>' },
{ cmd: 'em', icon: 'bi-type-italic', title: '<em>' },
null,
{ cmd: 'link', icon: 'bi-link-45deg', title: 'Link' },
{ cmd: 'image', icon: 'bi-image', title: 'Afbeelding' },
null,
{ cmd: 'comment', icon: 'bi-chat-square-dots', title: 'Commentaar' },
null,
{ cmd: 'media', icon: 'bi-images', title: 'Media invoegen' }
],
php: [
{ cmd: 'comment_php', icon: 'bi-slash-circle', title: '// Commentaar' },
{ cmd: 'docblock', icon: 'bi-blockquote-left', title: '/** DocBlock */' },
null,
{ cmd: 'media', icon: 'bi-images', title: 'Media invoegen' }
]
};
function wrap(editor, before, after) {
var sel = editor.getSelection();
editor.replaceSelection(before + sel + after);
if (!sel) {
var cur = editor.getCursor();
editor.setCursor({ line: cur.line, ch: cur.ch - after.length });
}
editor.focus();
}
function insert(editor, text, cursorOffset) {
editor.replaceSelection(text);
if (cursorOffset !== undefined) {
var cur = editor.getCursor();
editor.setCursor({ line: cur.line, ch: cur.ch - text.length + cursorOffset });
}
editor.focus();
}
function prependLine(editor, prefix) {
var cur = editor.getCursor();
editor.replaceRange(prefix, { line: cur.line, ch: 0 }, { line: cur.line, ch: 0 });
editor.setCursor({ line: cur.line, ch: prefix.length });
editor.focus();
}
function execute(cmd) {
var sel = editor.getSelection();
switch (cmd) {
case 'bold':
sel ? wrap(editor, '**', '**') : insert(editor, '****', 2);
break;
case 'italic':
sel ? wrap(editor, '*', '*') : insert(editor, '**', 1);
break;
case 'heading':
prependLine(editor, '## ');
break;
case 'link':
sel ? wrap(editor, '[', '](url)') : insert(editor, '[linktekst](url)', 1);
break;
case 'ulist':
prependLine(editor, '- ');
break;
case 'olist':
prependLine(editor, '1. ');
break;
case 'code':
sel ? wrap(editor, '`', '`') : insert(editor, '``', 1);
break;
case 'quote':
prependLine(editor, '> ');
break;
case 'hr':
insert(editor, '\n---\n', 0);
break;
case 'strong':
sel ? wrap(editor, '<strong>', '</strong>') : insert(editor, '<strong></strong>', 8);
break;
case 'em':
sel ? wrap(editor, '<em>', '</em>') : insert(editor, '<em></em>', 4);
break;
case 'image':
insert(editor, '<img src="" alt="">', 10);
break;
case 'comment':
sel ? wrap(editor, '<!-- ', ' -->') : insert(editor, '<!-- -->', 4);
break;
case 'comment_php':
if (sel) {
sel.indexOf('\n') !== -1
? insert(editor, '/* ' + sel + ' */', 0)
: insert(editor, '// ' + sel, 0);
} else {
prependLine(editor, '// ');
}
break;
case 'docblock':
insert(editor, '/**\n * \n */', 7);
break;
case 'media':
var modal = new bootstrap.Modal(document.getElementById('mediaModal'));
if (modal) modal.show();
break;
}
}
function buildToolbar(ext) {
var toolbar = document.getElementById('editor-toolbar');
if (!toolbar) return;
toolbar.innerHTML = '';
var cmds = commands[ext] || [];
cmds.forEach(function (item) {
if (item === null) {
var sep = document.createElement('div');
sep.className = 'vr';
toolbar.appendChild(sep);
return;
}
var btn = document.createElement('button');
btn.type = 'button';
btn.className = 'btn btn-sm btn-outline-secondary';
btn.title = item.title;
btn.innerHTML = '<i class="bi ' + item.icon + '"></i>';
btn.addEventListener('click', function () { execute(item.cmd); });
toolbar.appendChild(btn);
});
}
var templates = {
md: '# Nieuwe Pagina\n\nSchrijf hier je inhoud...\n',
html: '<h1>Nieuwe Pagina</h1>\n<p>Dit is een HTML content pagina.</p>\n',
php: '---\ntitle: Nieuwe Pagina\n---\n\n<?php\n$pageTitle = "Nieuwe Pagina";\n?>\n\n<h1><?= $pageTitle ?></h1>\n<p>PHP content - alles wat je hier echo&#39;t wordt weergegeven.</p>\n\n<?php\n$items = [\'Item 1\', \'Item 2\', \'Item 3\'];\n?>\n<ul>\n<?php foreach ($items as $item): ?>\n <li><?= $item ?></li>\n<?php endforeach; ?>\n</ul>\n'
};
var defaultContents = {};
function getTemplate(ext) {
return templates[ext] || '';
}
function setDefaultContent(ext) {
var tpl = getTemplate(ext);
editor.setValue(tpl);
editor.setCursor({ line: 0, ch: 0 });
editor.focus();
defaultContents[ext] = tpl;
}
function switchMode(ext, forceContent) {
var mode = modeMap[ext] || 'markdown';
editor.setOption('mode', mode);
textarea.dataset.ext = ext;
if (forceContent) {
setDefaultContent(ext);
} else {
var cur = editor.getValue().trim();
var expected = defaultContents[ext];
if (cur === '' || (expected && cur === expected.trim())) {
setDefaultContent(ext);
}
}
}
editor.on('change', function () {
if (window.__onContentChange) window.__onContentChange();
});
buildToolbar(ext);
var form = document.getElementById('editor-form');
var isNewPage = form && form.hasAttribute('data-new-page');
if (isNewPage && editor.getValue().trim() === '') {
setDefaultContent(ext);
}
var typeSelect = document.querySelector('[data-editor-mode]');
if (typeSelect && isNewPage) {
typeSelect.addEventListener('change', function () {
switchMode(this.value, true);
});
}
var form = document.getElementById('editor-form') || textarea.closest('form');
if (form) {
form.addEventListener('submit', function () {
editor.save();
});
}
// Keyboard shortcuts: Ctrl/Cmd+S saves, Ctrl/Cmd+N creates a new page
function handleShortcut(e) {
var mod = e.ctrlKey || e.metaKey;
if (!mod || e.altKey) return;
var key = (e.key || '').toLowerCase();
if (key === 's') {
e.preventDefault();
editor.save();
if (form) {
if (typeof form.requestSubmit === 'function') {
form.requestSubmit();
} else {
form.submit();
}
}
return;
}
if (key === 'n') {
e.preventDefault();
window.location.href = '/admin/content-new';
}
}
document.addEventListener('keydown', handleShortcut);
// CodeMirror swallows keystrokes inside the editor, so bind there too
editor.setOption('extraKeys', Object.assign({}, editor.getOption('extraKeys') || {}, {
'Ctrl-S': function () { handleShortcut({ ctrlKey: true, key: 's', preventDefault: function () {} }); },
'Cmd-S': function () { handleShortcut({ metaKey: true, key: 's', preventDefault: function () {} }); }
}));
})();