- Thema editor (thema's net zo bewerkbaar maken als plugins): bestandsbrowser zijbalk, nieuw bestand, uploaden, verwijderen, verplaatsen, SCSS compileren vanuit editor, thema activeren/ verwijderen, nieuw thema met basis-kopie, uniforme thema-structuur - Content editor (content consistent met plugins en thema's): bestandsbrowser zijbalk naast bestaande lijst weergave, nieuw bestand/map, uploaden, verwijderen, verplaatsen, mappen beheer, layout/plugins selectie, git/backup integratie in editor zijbalk - Uniformiteit: scanEditorFiles() unified scanner (scanPluginFiles/scanThemeFiles/scanContentFiles als dunne wrappers) - Media invoegen in editor: ?theme= scope voor media-list endpoint - Bug fix: thema-naam niet overgenomen bij kopiëren - title in theme.json wordt overschreven met nieuwe themanaam - Handleidingen bijgewerkt (thema-beheer.md + content-beheer.md NL/EN) - Release notes: docs/release-notes/v2.6.2.md - Pentest 30/30, WCAG 25/25
204 lines
9.1 KiB
Twig
204 lines
9.1 KiB
Twig
{# Reusable media modal, included by admin.twig when needs_editor is true.
|
|
The editor toolbar "media" button opens this modal. It fetches the list
|
|
of media files from /admin/media-list (JSON) and, on click, inserts a
|
|
snippet into the active CodeMirror editor at the cursor.
|
|
|
|
Scope:
|
|
- Default (content editor): fetches /admin/media-list
|
|
- Plugin editor (mediaPluginName set): fetches /admin/media-list?plugin=<name>
|
|
which scans plugins/<name>/assets/ instead of content/.
|
|
- Theme editor (mediaThemeName set): fetches /admin/media-list?theme=<name>
|
|
which scans themes/<name>/assets/ instead of content/.
|
|
|
|
The snippet format depends on the editor's current mode (data-ext):
|
|
- markdown (md): 
|
|
- html/php: <img src="url" alt="name">
|
|
- other (css, json, js, ...): the raw URL #}
|
|
<div class="modal fade" id="mediaModal" tabindex="-1" aria-labelledby="mediaModalLabel" aria-hidden="true">
|
|
<div class="modal-dialog modal-lg modal-dialog-scrollable">
|
|
<div class="modal-content">
|
|
<div class="modal-header">
|
|
<h5 class="modal-title" id="mediaModalLabel"><i class="bi bi-images"></i> {{ ta.media_insert|default('Media invoegen') }}</h5>
|
|
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
|
|
</div>
|
|
<div class="modal-body">
|
|
<div class="input-group input-group-sm mb-3">
|
|
<span class="input-group-text bg-white"><i class="bi bi-search text-muted"></i></span>
|
|
<input type="search" id="mediaModalFilter" class="form-control" placeholder="{{ ta.media_filter|default('Filter op bestandsnaam...') }}" autocomplete="off" aria-label="{{ ta.media_filter|default('Filter') }}">
|
|
</div>
|
|
<div id="mediaModalGrid" class="row g-3">
|
|
<div class="col-12 text-center text-muted py-4" id="mediaModalLoading">
|
|
<div class="spinner-border spinner-border-sm" role="status"></div>
|
|
<span class="ms-2">{{ ta.media_loading|default('Media laden...') }}</span>
|
|
</div>
|
|
<div class="col-12 text-center text-muted py-4 d-none" id="mediaModalEmpty">
|
|
<i class="bi bi-image display-6 d-block mb-2"></i>
|
|
{{ mediaEmptyText|default('Geen media bestanden gevonden in content/. Upload eerst bestanden via Media in het menu.') }}
|
|
</div>
|
|
<div class="col-12 text-center text-danger py-4 d-none" id="mediaModalError">
|
|
<i class="bi bi-exclamation-triangle display-6 d-block mb-2"></i>
|
|
<span id="mediaModalErrorText"></span>
|
|
</div>
|
|
{# Filled by JS #}
|
|
</div>
|
|
</div>
|
|
<div class="modal-footer">
|
|
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">{{ ta.cancel|default('Annuleren') }}</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<script>
|
|
(function () {
|
|
'use strict';
|
|
|
|
var loaded = false;
|
|
var items = [];
|
|
|
|
function formatSnippet(url, name, ext, mode) {
|
|
if (mode === 'markdown') {
|
|
return '![' + name.replace(/[\[\]]/g, '') + '](' + url + ')';
|
|
}
|
|
if (mode === 'html' || mode === 'htmlmixed' || mode === 'php') {
|
|
var imgExts = ['jpg', 'jpeg', 'png', 'gif', 'webp', 'svg'];
|
|
if (imgExts.indexOf(ext) !== -1) {
|
|
return '<img src="' + url + '" alt="' + name.replace(/"/g, '"') + '">';
|
|
}
|
|
if (ext === 'pdf') {
|
|
return '<a href="' + url + '">' + name + '</a>';
|
|
}
|
|
if (['mp4', 'webm', 'mov', 'ogg'].indexOf(ext) !== -1) {
|
|
return '<video src="' + url + '" controls></video>';
|
|
}
|
|
if (['mp3', 'wav'].indexOf(ext) !== -1) {
|
|
return '<audio src="' + url + '" controls></audio>';
|
|
}
|
|
return '<a href="' + url + '">' + name + '</a>';
|
|
}
|
|
// css, scss, js, json, etc.: just insert the URL
|
|
return url;
|
|
}
|
|
|
|
function modeForExt(ext) {
|
|
var map = { md: 'markdown', html: 'htmlmixed', php: 'php', css: 'css', scss: 'css', js: 'javascript', json: 'javascript' };
|
|
return map[ext] || 'markdown';
|
|
}
|
|
|
|
function renderGrid() {
|
|
var grid = document.getElementById('mediaModalGrid');
|
|
if (!grid) return;
|
|
|
|
// Clear previous items (keep loading/empty/error placeholders)
|
|
Array.prototype.forEach.call(grid.querySelectorAll('[data-media-item]'), function (el) { el.remove(); });
|
|
|
|
var q = (document.getElementById('mediaModalFilter').value || '').trim().toLowerCase();
|
|
var shown = 0;
|
|
|
|
items.forEach(function (item) {
|
|
if (q && item.name.toLowerCase().indexOf(q) === -1) return;
|
|
shown++;
|
|
|
|
var col = document.createElement('div');
|
|
col.className = 'col-6 col-md-4 col-lg-3';
|
|
col.setAttribute('data-media-item', '1');
|
|
|
|
var card = document.createElement('div');
|
|
card.className = 'card shadow-sm h-100';
|
|
card.style.cursor = 'pointer';
|
|
card.title = item.name + ' (' + item.size + ')';
|
|
|
|
if (item.is_image) {
|
|
var img = document.createElement('img');
|
|
img.src = item.url;
|
|
img.className = 'card-img-top';
|
|
img.style.objectFit = 'cover';
|
|
img.style.height = '120px';
|
|
img.loading = 'lazy';
|
|
img.alt = item.name;
|
|
card.appendChild(img);
|
|
} else {
|
|
var ph = document.createElement('div');
|
|
ph.className = 'card-img-top d-flex align-items-center justify-content-center bg-light';
|
|
ph.style.height = '120px';
|
|
var icon = document.createElement('i');
|
|
icon.className = 'bi bi-file-earmark-play display-6 text-muted';
|
|
ph.appendChild(icon);
|
|
card.appendChild(ph);
|
|
}
|
|
|
|
var body = document.createElement('div');
|
|
body.className = 'card-body p-2';
|
|
var label = document.createElement('p');
|
|
label.className = 'card-text small text-muted text-truncate mb-0';
|
|
label.textContent = item.name;
|
|
body.appendChild(label);
|
|
card.appendChild(body);
|
|
|
|
card.addEventListener('click', function () {
|
|
var editor = window.codeMirrorEditor;
|
|
if (!editor) { return; }
|
|
var ext = (document.getElementById('editor-textarea') || {}).dataset;
|
|
var fileExt = (ext && ext.ext) ? ext.ext : 'md';
|
|
var mode = modeForExt(fileExt);
|
|
var snippet = formatSnippet(item.url, item.name, item.extension, mode);
|
|
editor.replaceSelection(snippet);
|
|
editor.focus();
|
|
// Close the modal
|
|
var modalEl = document.getElementById('mediaModal');
|
|
if (window.bootstrap && modalEl) {
|
|
var inst = bootstrap.Modal.getInstance(modalEl);
|
|
if (inst) inst.hide();
|
|
}
|
|
});
|
|
|
|
col.appendChild(card);
|
|
grid.appendChild(col);
|
|
});
|
|
|
|
// Toggle empty placeholder
|
|
var empty = document.getElementById('mediaModalEmpty');
|
|
var loading = document.getElementById('mediaModalLoading');
|
|
if (loading) loading.classList.add('d-none');
|
|
if (empty) empty.classList.toggle('d-none', shown > 0);
|
|
}
|
|
|
|
function load() {
|
|
if (loaded) { renderGrid(); return; }
|
|
var plugin = {{ mediaPluginName|default('')|json_encode|raw }};
|
|
var theme = {{ mediaThemeName|default('')|json_encode|raw }};
|
|
var url = '/admin/media-list';
|
|
if (plugin) { url += '?plugin=' + encodeURIComponent(plugin); }
|
|
else if (theme) { url += '?theme=' + encodeURIComponent(theme); }
|
|
fetch(url, { credentials: 'same-origin' })
|
|
.then(function (r) {
|
|
if (!r.ok) throw new Error('HTTP ' + r.status);
|
|
return r.json();
|
|
})
|
|
.then(function (data) {
|
|
items = Array.isArray(data) ? data : [];
|
|
loaded = true;
|
|
renderGrid();
|
|
})
|
|
.catch(function (err) {
|
|
var loading = document.getElementById('mediaModalLoading');
|
|
if (loading) loading.classList.add('d-none');
|
|
var errEl = document.getElementById('mediaModalError');
|
|
var errTxt = document.getElementById('mediaModalErrorText');
|
|
if (errEl) errEl.classList.remove('d-none');
|
|
if (errTxt) errTxt.textContent = String(err);
|
|
});
|
|
}
|
|
|
|
// Load when the modal is first shown
|
|
var modalEl = document.getElementById('mediaModal');
|
|
if (modalEl) {
|
|
modalEl.addEventListener('show.bs.modal', load);
|
|
}
|
|
// Filter as the user types
|
|
var filter = document.getElementById('mediaModalFilter');
|
|
if (filter) {
|
|
filter.addEventListener('input', renderGrid);
|
|
}
|
|
})();
|
|
</script> |