- Content bestanden met dezelfde naam maar ander type (md/php/html) worden correct geserveerd: URL met extensie opent dat bestand, URL zonder extensie valt terug op md > php > html (resolveContentByType helper) - Admin content editor accepteert bestanden met dezelfde naam (ander type); preview-knop linkt per extensie - Frontend navigatie/directory listing/search tonen elk bestandstype apart - getAllPages() array structuur gewijzigd naar list van ['path','title','type'] met type 'md'/'php'/'html'/'folder' - Verberg-prefix logica: _ is geen verberg-prefix meer, alleen . (en -); admin toont wél alle . bestanden/mappen - ContentAPI getPage()/pageExists() respecteren expliciete extensie - Handleiding content-api.md (NL+EN) herschreven - File-tree unificatie: _file-tree.twig + _editor-styles.twig includes - Versie verhoogd naar 2.6.3
335 lines
18 KiB
Twig
335 lines
18 KiB
Twig
{% extends "layouts/admin.twig" %}
|
|
|
|
{% block title %}{{ ta.content_editor|default('Content editor') }} - {{ ta.admin_title|default('CodePress Admin') }}{% endblock %}
|
|
|
|
{% block extra_css %}
|
|
<link rel="stylesheet" href="/admin/assets/codemirror/codemirror.min.css">
|
|
<link rel="stylesheet" href="/admin/assets/css/editor.css">
|
|
{% include 'pages/_editor-styles.twig' %}
|
|
<style>
|
|
/* Git status badge (content editor only) */
|
|
.git-status { font-size: .75rem; }
|
|
.git-status .badge { font-weight: 400; }
|
|
</style>
|
|
{% endblock %}
|
|
|
|
{% block content %}
|
|
<div class="d-flex justify-content-between align-items-center mb-4 flex-wrap gap-2">
|
|
<h2 class="mb-0"><i class="bi bi-pencil"></i> {{ ta.content_editor|default('Content editor') }}</h2>
|
|
<div class="d-flex gap-2 flex-wrap">
|
|
{# Backup integratie in zijbalk header (git wordt later een systeem plugin) #}
|
|
<a href="/admin/content-backup" class="btn btn-outline-info btn-sm" title="{{ ta.backup|default('Backup') }}">
|
|
<i class="bi bi-archive"></i> {{ ta.backup|default('Backup') }}
|
|
</a>
|
|
<button type="button" class="btn btn-outline-success btn-sm" data-bs-toggle="collapse" data-bs-target="#contentUploadForm">
|
|
<i class="bi bi-cloud-upload"></i> {{ ta.upload|default('Upload') }}
|
|
</button>
|
|
<button type="button" class="btn btn-outline-primary btn-sm" id="newFileBtnTop" data-bs-toggle="modal" data-bs-target="#newFileModal" data-in-dir="{{ selectedDir|default('') }}">
|
|
<i class="bi bi-file-earmark-plus"></i> {{ ta.new_file|default('Nieuw bestand') }}
|
|
</button>
|
|
<button type="button" class="btn btn-outline-secondary btn-sm" id="newDirBtnTop" data-bs-toggle="modal" data-bs-target="#newDirModal" data-in-dir="{{ selectedDir|default('') }}">
|
|
<i class="bi bi-folder-plus"></i> {{ ta.new_folder|default('Nieuwe map') }}
|
|
</button>
|
|
|
|
</div>
|
|
</div>
|
|
|
|
{# Upload form (collapsed by default) #}
|
|
<div class="collapse mb-3" id="contentUploadForm">
|
|
<div class="card shadow-sm">
|
|
<div class="card-body">
|
|
<form method="POST" action="/admin/content-file-upload" enctype="multipart/form-data">
|
|
<input type="hidden" name="csrf_token" value="{{ csrf_token }}">
|
|
<input type="hidden" name="dir" value="" id="contentUploadDir">
|
|
<div class="mb-3">
|
|
<label for="contentUploadFile" class="form-label">{{ ta.select_files|default('Bestanden selecteren') }}</label>
|
|
<input type="file" class="form-control" id="contentUploadFile" name="file[]" multiple accept="image/jpeg,image/png,image/gif,image/webp,image/svg+xml,application/pdf,application/zip,video/mp4,video/webm,audio/mpeg,audio/wav,.css,.scss,.js,.json,.html,.md">
|
|
<small class="form-text text-muted">{{ ta.content_upload_help|default('Bestanden worden geüpload naar content/. Toegestaan: afbeeldingen, video, audio, PDF, ZIP, office docs, CSS, SCSS, JS, JSON, HTML, MD.') }}</small>
|
|
</div>
|
|
<button type="submit" class="btn btn-success">
|
|
<i class="bi bi-cloud-upload"></i> {{ ta.upload_to_folder|default('Uploaden') }}
|
|
</button>
|
|
</form>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="editor-layout">
|
|
{# File tree sidebar (shared partial) #}
|
|
{% include 'pages/_file-tree.twig' with {
|
|
'files': files,
|
|
'relFile': relFile,
|
|
'editableExts': editableExts,
|
|
'csrf_token': csrf_token,
|
|
'treeIdPrefix': 'content-tree',
|
|
'treeHeader': ta.content_files|default('Content bestanden'),
|
|
'treeEmptyText': ta.content_no_files|default('Geen bestanden gevonden.'),
|
|
'treeRouteBase': '/admin/content',
|
|
'treeRouteParams': '',
|
|
'treeDeleteRoute': '/admin/content-file-delete',
|
|
'treeDeleteName': '',
|
|
'treeDeleteValue': '',
|
|
'treeMoveRoute': '/admin/content-file-move',
|
|
'treeMoveParams': '',
|
|
'treeDirActions': true,
|
|
'treeHideActionsFor': [],
|
|
} %}
|
|
|
|
{# Editor main panel #}
|
|
<div class="editor-main">
|
|
{% if relFile %}
|
|
<nav aria-label="breadcrumb" class="mb-2">
|
|
<ol class="breadcrumb mb-0">
|
|
<li class="breadcrumb-item"><i class="bi bi-folder2-open"></i> content</li>
|
|
{% set crumbPath = '' %}
|
|
{% set parts = relFile|split('/') %}
|
|
{% for part in parts %}
|
|
{% set crumbPath = crumbPath ? crumbPath ~ '/' ~ part : part %}
|
|
{% if loop.last %}
|
|
<li class="breadcrumb-item active" aria-current="page">{{ part }}</li>
|
|
{% else %}
|
|
<li class="breadcrumb-item"><a href="/admin/content?file={{ crumbPath|url_encode }}">{{ part }}</a></li>
|
|
{% endif %}
|
|
{% endfor %}
|
|
</ol>
|
|
</nav>
|
|
{% endif %}
|
|
|
|
{% if isEditable %}
|
|
<form method="POST" action="/admin/content?file={{ relFile|url_encode }}" id="editor-form">
|
|
<input type="hidden" name="csrf_token" value="{{ csrf_token }}">
|
|
<div class="card shadow-sm">
|
|
<div class="card-body">
|
|
{# Metadata row: filename display, layout selector, plugins #}
|
|
<div class="row mb-3">
|
|
<div class="col-md-4">
|
|
<label for="new_filename" class="form-label text-muted small mb-1">{{ ta.filename|default('Bestandsnaam') }}</label>
|
|
<div class="input-group input-group-sm">
|
|
<input type="text" class="form-control" id="new_filename" name="new_filename" value="{{ fileBaseName }}" required>
|
|
<span class="input-group-text">.{{ fileExt }}</span>
|
|
</div>
|
|
</div>
|
|
<div class="col-md-4">
|
|
<label for="layout" class="form-label">{{ ta.template_layout|default('Sjabloon / Layout') }} <small class="text-muted">({{ activeThemeName|default('default') }})</small></label>
|
|
<select class="form-select form-select-sm" id="layout" name="layout">
|
|
<option value="">{{ ta.theme_default|default('Thema standaard') }}</option>
|
|
{% for key, layoutFile in themeLayouts %}
|
|
<option value="{{ key }}" {{ currentLayout == key ? 'selected' : '' }}>{{ key|replace({'_': ' '})|capitalize }}{% if key == themeDefaultLayout %} (standaard){% endif %}</option>
|
|
{% endfor %}
|
|
</select>
|
|
</div>
|
|
{% if availablePlugins is not empty %}
|
|
<div class="col-md-4">
|
|
<label class="form-label">{{ ta.visible_plugins|default('Zichtbare plugins') }}</label>
|
|
<div class="plugin-checkbox-group">
|
|
{% for plugin in availablePlugins %}
|
|
<div class="form-check">
|
|
<input class="form-check-input" type="checkbox" name="plugins[]" value="{{ plugin.name }}" id="plugin_{{ plugin.name }}" {{ plugin.name in selectedPlugins ? 'checked' : '' }}>
|
|
<label class="form-check-label" for="plugin_{{ plugin.name }}">{{ plugin.title }}</label>
|
|
</div>
|
|
{% endfor %}
|
|
</div>
|
|
</div>
|
|
{% endif %}
|
|
</div>
|
|
{# Created / edited timestamps (read-only, auto-filled) #}
|
|
<div class="row mb-3">
|
|
<div class="col-md-4">
|
|
<label class="form-label text-muted small"><i class="bi bi-calendar-plus"></i> {{ ta.created|default('Aangemaakt') }}</label>
|
|
<input type="text" class="form-control-plaintext form-control-sm" value="{{ currentCreated }}" readonly>
|
|
</div>
|
|
<div class="col-md-4">
|
|
<label class="form-label text-muted small"><i class="bi bi-calendar-check"></i> {{ ta.edited|default('Bewerkt') }}</label>
|
|
<input type="text" class="form-control-plaintext form-control-sm" value="{{ currentEdited }}" readonly>
|
|
</div>
|
|
</div>
|
|
<div class="editor-toolbar" id="editor-toolbar"></div>
|
|
<div class="editor-wrapper">
|
|
<textarea name="content" id="editor-textarea" data-ext="{{ fileExt }}">{{ fileContent }}</textarea>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<div class="d-flex gap-2 mt-3">
|
|
<button type="submit" class="btn btn-primary" title="{{ ta.save_ctrl_s|default('Opslaan (Ctrl+S)') }}">
|
|
<i class="bi bi-check-lg"></i> {{ ta.save|default('Opslaan') }}
|
|
</button>
|
|
<a href="/{{ currentLang }}{% if fileDir %}/{{ fileDir }}{% endif %}/{{ fileBaseName }}{% if fileExt != 'md' %}.{{ fileExt }}{% endif %}" target="_blank" class="btn btn-outline-info" title="{{ ta.open_new_tab|default('Open in nieuw tabblad') }}">
|
|
<i class="bi bi-eye"></i> {{ ta.preview|default('Preview') }}
|
|
</a>
|
|
</div>
|
|
</form>
|
|
{% else %}
|
|
<div class="card shadow-sm">
|
|
<div class="card-body text-center py-5 text-muted">
|
|
<i class="bi bi-file-earmark-slash display-6 d-block mb-2"></i>
|
|
{% if relFile %}
|
|
{{ ta.content_not_editable|default('Dit bestandstype kan niet in de editor bewerkt worden.') }}
|
|
{% else %}
|
|
{{ ta.content_select_file|default('Selecteer een bestand uit de zijbalk om te bewerken.') }}
|
|
{% endif %}
|
|
</div>
|
|
</div>
|
|
{% endif %}
|
|
</div>
|
|
</div>
|
|
|
|
{# New file modal #}
|
|
<div class="modal fade" id="newFileModal" tabindex="-1" aria-labelledby="newFileModalLabel" aria-hidden="true">
|
|
<div class="modal-dialog">
|
|
<div class="modal-content">
|
|
<form method="POST" action="/admin/content">
|
|
<input type="hidden" name="csrf_token" value="{{ csrf_token }}">
|
|
<input type="hidden" name="action" value="new_file">
|
|
<input type="hidden" name="in_dir" id="newFileDir" value="">
|
|
<div class="modal-header">
|
|
<h5 class="modal-title" id="newFileModalLabel"><i class="bi bi-file-earmark-plus"></i> {{ ta.new_file_title|default('Nieuw bestand aanmaken') }}</h5>
|
|
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
|
|
</div>
|
|
<div class="modal-body">
|
|
<div class="mb-3">
|
|
<label for="newFilenameInput" class="form-label">{{ ta.content_file_path|default('Bestandspad binnen content') }}</label>
|
|
<div class="input-group">
|
|
<input type="text" class="form-control" id="newFilenameInput" name="new_filename" placeholder="Bijv. nl.pagina of blog/nl.post" required autofocus>
|
|
</div>
|
|
<div class="form-text">{{ ta.content_file_path_help|default('Alleen letters, cijfers, punten, underscores, streepjes en slashes. Submappen worden automatisch aangemaakt. De extensie wordt hieronder gekozen.') }}</div>
|
|
</div>
|
|
<div class="mb-3">
|
|
<label for="newExtSelect" class="form-label">{{ ta.file_type|default('Bestandstype') }}</label>
|
|
<select class="form-select" id="newExtSelect" name="new_ext">
|
|
<option value="md">Markdown (.md)</option>
|
|
<option value="php">PHP (.php)</option>
|
|
<option value="html">HTML (.html)</option>
|
|
</select>
|
|
</div>
|
|
</div>
|
|
<div class="modal-footer">
|
|
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">{{ ta.cancel|default('Annuleren') }}</button>
|
|
<button type="submit" class="btn btn-primary"><i class="bi bi-check-lg"></i> {{ ta.create|default('Aanmaken') }}</button>
|
|
</div>
|
|
</form>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
{# New directory modal #}
|
|
<div class="modal fade" id="newDirModal" tabindex="-1" aria-labelledby="newDirModalLabel" aria-hidden="true">
|
|
<div class="modal-dialog">
|
|
<div class="modal-content">
|
|
<form method="POST" action="/admin/content-dir-create-in">
|
|
<input type="hidden" name="csrf_token" value="{{ csrf_token }}">
|
|
<input type="hidden" name="in_dir" id="newDirInDir" value="">
|
|
<div class="modal-header">
|
|
<h5 class="modal-title" id="newDirModalLabel"><i class="bi bi-folder-plus"></i> {{ ta.new_folder_title|default('Nieuwe map aanmaken') }}</h5>
|
|
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
|
|
</div>
|
|
<div class="modal-body">
|
|
<div class="mb-3">
|
|
<label for="dirnameInput" class="form-label">{{ ta.folder_name|default('Mapnaam') }}</label>
|
|
<input type="text" class="form-control" id="dirnameInput" name="dirname" required autofocus>
|
|
<div class="form-text">{{ ta.name_help|default('Alleen letters, cijfers, punten, underscores en streepjes.') }}</div>
|
|
</div>
|
|
</div>
|
|
<div class="modal-footer">
|
|
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">{{ ta.cancel|default('Annuleren') }}</button>
|
|
<button type="submit" class="btn btn-primary"><i class="bi bi-check-lg"></i> {{ ta.create|default('Aanmaken') }}</button>
|
|
</div>
|
|
</form>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<script>
|
|
// Pass the in-dir value from the tree button to the modal hidden fields
|
|
document.addEventListener('DOMContentLoaded', function () {
|
|
var newFileModal = document.getElementById('newFileModal');
|
|
var newDirModal = document.getElementById('newDirModal');
|
|
var newFileBtnTop = document.getElementById('newFileBtnTop');
|
|
var newDirBtnTop = document.getElementById('newDirBtnTop');
|
|
|
|
// Track the selected directory for the top toolbar buttons
|
|
var selectedDir = '{{ selectedDir|default('') }}';
|
|
function updateTopButtons(dir) {
|
|
selectedDir = dir || '';
|
|
if (newFileBtnTop) newFileBtnTop.setAttribute('data-in-dir', selectedDir);
|
|
if (newDirBtnTop) newDirBtnTop.setAttribute('data-in-dir', selectedDir);
|
|
}
|
|
|
|
// Make folder rows selectable: clicking the folder name selects it
|
|
var tree = document.getElementById('editorFileTree');
|
|
if (tree) {
|
|
tree.addEventListener('click', function (e) {
|
|
var toggle = e.target.closest('.tree-toggle');
|
|
if (!toggle) return;
|
|
// Find the in-dir from the adjacent new-file button
|
|
var row = toggle.closest('.tree-dir-row');
|
|
if (!row) return;
|
|
var newFileBtn = row.querySelector('.tree-newfile-btn');
|
|
var dir = newFileBtn ? newFileBtn.getAttribute('data-in-dir') : '';
|
|
updateTopButtons(dir);
|
|
// Visually mark the selected folder
|
|
tree.querySelectorAll('.tree-toggle.is-selected').forEach(function (el) { el.classList.remove('is-selected'); });
|
|
toggle.classList.add('is-selected');
|
|
});
|
|
}
|
|
|
|
if (newFileModal) {
|
|
newFileModal.addEventListener('show.bs.modal', function (event) {
|
|
var trigger = event.relatedTarget;
|
|
var inDir = trigger ? trigger.getAttribute('data-in-dir') : selectedDir;
|
|
var hidden = document.getElementById('newFileDir');
|
|
if (hidden) hidden.value = inDir || '';
|
|
// Show the in-dir prefix as a hint in the placeholder
|
|
var input = document.getElementById('newFilenameInput');
|
|
if (input) {
|
|
input.value = '';
|
|
input.placeholder = inDir ? inDir + '/naam' : 'Bijv. nl.pagina of blog/nl.post';
|
|
}
|
|
});
|
|
}
|
|
if (newDirModal) {
|
|
newDirModal.addEventListener('show.bs.modal', function (event) {
|
|
var trigger = event.relatedTarget;
|
|
var inDir = trigger ? trigger.getAttribute('data-in-dir') : selectedDir;
|
|
var hidden = document.getElementById('newDirInDir');
|
|
if (hidden) hidden.value = inDir;
|
|
});
|
|
}
|
|
|
|
// Mark the initially selected folder (based on the opened file's directory)
|
|
if (selectedDir && tree) {
|
|
var initialBtn = tree.querySelector('.tree-newfile-btn[data-in-dir="' + selectedDir + '"]');
|
|
if (initialBtn) {
|
|
var initialToggle = initialBtn.closest('.tree-dir-row').querySelector('.tree-toggle');
|
|
if (initialToggle) initialToggle.classList.add('is-selected');
|
|
}
|
|
}
|
|
|
|
// Layout select → update frontmatter (spiegel van content-edit.twig)
|
|
var layoutSelect = document.getElementById('layout');
|
|
if (layoutSelect) {
|
|
layoutSelect.addEventListener('change', function () {
|
|
var newLayout = this.value;
|
|
var cm = window.codeMirrorEditor;
|
|
var editor = document.getElementById('editor-textarea');
|
|
var content = cm ? cm.getValue() : (editor ? editor.value : '');
|
|
var fmMatch = content.match(/^---\n([\s\S]*?)\n---/);
|
|
if (fmMatch) {
|
|
var frontmatter = fmMatch[1];
|
|
if (/^layout:\s*.+$/m.test(frontmatter)) {
|
|
frontmatter = frontmatter.replace(/^layout:\s*.+$/m, 'layout: ' + newLayout);
|
|
} else {
|
|
frontmatter = 'layout: ' + newLayout + '\n' + frontmatter;
|
|
}
|
|
content = content.replace(/^---\n([\s\S]*?)\n---/, '---\n' + frontmatter + '\n---');
|
|
} else {
|
|
content = '---\nlayout: ' + newLayout + '\n---\n\n' + content;
|
|
}
|
|
if (cm) { cm.setValue(content); } else if (editor) { editor.value = content; }
|
|
});
|
|
}
|
|
});
|
|
</script>
|
|
{% endblock %}
|
|
|
|
{% block extra_js %}
|
|
{% endblock %} |