Files
CodePress/admin/theme/default/views/pages/_media-modal.twig
T
E.Noorlander 6d5ca7cab4 v2.6.1d (Lyra): Plugin i18n, plugin editor vernieuwd, media invoegen
Plugin internationalisatie: plugins hebben eigen language/ mappen. Systeem
plugins volgen admin taal (admin.php), content plugins volgen content taal
(site.php). Fallback chain: geselecteerd -> plugin default_language -> CMS
default. PluginManager/AdminPluginAPI/CMSAPI uitgebreid met
getPluginTranslations()/t(). plugin.json settings ondersteunen
label_key/help_key/option_label_key.

Plugin uniformiteit: alle 6 plugins hebben uniforme structuur (README.md,
assets/.gitkeep, language/nl|en/). plugins/README.md herschreven.
guide plugin-development.md (NL+EN) volledig herschreven.

Plugin editor vernieuwd: geneste bestandsbrowser zijbalk, nieuw bestand
aanmaken, uploaden naar assets/, verwijderen en verplaatsen. Nieuwe routes:
plugins-file-upload, plugins-file-delete, plugins-file-move. Path-traversal
bescherming + protected plugins geblokkeerd.

Media invoegen in editor: nieuw /admin/media-list JSON endpoint + herbruikbare
_media-modal.twig include. Plugin-context scant assets/ map. editor-toolbar.js
modeMap uitgebreid voor css/scss/js/json.

Plugin overzicht knoppen: alleen iconen met title/aria-label.

Tests: pentest 30/30, WCAG 2.1 AA 25/25.
2026-08-18 13:49:52 +00:00

200 lines
8.9 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/.
The snippet format depends on the editor's current mode (data-ext):
- markdown (md): ![alt](url)
- 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, '&quot;') + '">';
}
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 url = '/admin/media-list';
if (plugin) { url += '?plugin=' + encodeURIComponent(plugin); }
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>