- 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
191 lines
11 KiB
Twig
191 lines
11 KiB
Twig
{# Shared file-tree partial used by the content, plugin and theme editors.
|
|
Renders a nested, collapsible file tree from a list of nodes produced by
|
|
scanEditorFiles()/scanContentFiles()/scanPluginFiles()/scanThemeFiles().
|
|
|
|
Required variables (passed via include with ... context):
|
|
- files: array of nodes [{name, path, is_dir, extension, size, modified, children}]
|
|
- relFile: currently selected relative file path (for active highlight + auto-expand)
|
|
- editableExts: array of editable extensions (shown as .EXT badge)
|
|
- csrf_token: CSRF token for delete forms
|
|
- treeIdPrefix: prefix for collapse element ids (e.g. 'content-tree', 'plugin-tree', 'theme-tree')
|
|
- treeRouteBase: admin route base used to build file links, e.g. '/admin/content'
|
|
- treeRouteParams: extra query string for file links, e.g. 'plugin=MyPlugin&' or 'theme=default&' or '' (content)
|
|
- treeDeleteRoute: admin route for file delete, e.g. '/admin/content-file-delete'
|
|
- treeDeleteName: optional hidden field name for delete form (e.g. 'plugin' or 'theme'); emits no field when empty
|
|
- treeDeleteValue: optional hidden field value for treeDeleteName (e.g. the plugin/theme name)
|
|
- treeMoveRoute: admin route for file move, e.g. '/admin/plugins-file-move'
|
|
- treeMoveParams: extra query string for move link, e.g. 'plugin=X&' or ''
|
|
- treeDirActions: boolean; show per-folder new-file/new-dir/rename/delete buttons (content editor only)
|
|
- treeHideActionsFor: array of relative paths where move/delete actions are hidden (e.g. ['theme.json'])
|
|
|
|
Icon maps are chosen automatically from the node extension. #}
|
|
{% macro tree(nodes, opts) %}
|
|
{% import _self as macros %}
|
|
<ul class="editor-tree">
|
|
{% for n in nodes %}
|
|
{% set isActive = n.path == opts.relFile %}
|
|
{% set containsActive = n.is_dir and opts.relFile starts with (n.path ~ '/') %}
|
|
{% if n.is_dir %}
|
|
{% set icon = 'bi-folder-fill text-warning' %}
|
|
{% elseif n.extension == 'md' %}
|
|
{% set icon = 'bi-file-text text-primary' %}
|
|
{% elseif n.extension == 'php' %}
|
|
{% set icon = 'bi-file-code text-success' %}
|
|
{% elseif n.extension == 'twig' %}
|
|
{% set icon = 'bi-filetype-tfn text-info' %}
|
|
{% elseif n.extension == 'json' %}
|
|
{% set icon = 'bi-filetype-json text-secondary' %}
|
|
{% elseif n.extension in ['css','scss'] %}
|
|
{% set icon = 'bi-filetype-css text-info' %}
|
|
{% elseif n.extension == 'js' %}
|
|
{% set icon = 'bi-filetype-js text-warning' %}
|
|
{% elseif n.extension == 'html' %}
|
|
{% set icon = 'bi-file-earmark text-info' %}
|
|
{% elseif n.extension in ['jpg','jpeg','png','gif','webp','svg'] %}
|
|
{% set icon = 'bi-file-image text-info' %}
|
|
{% elseif n.extension == 'pdf' %}
|
|
{% set icon = 'bi-file-pdf text-danger' %}
|
|
{% else %}
|
|
{% set icon = 'bi-file text-muted' %}
|
|
{% endif %}
|
|
<li>
|
|
{% if n.is_dir %}
|
|
{% set collapseId = opts.treeIdPrefix ~ '-' ~ n.path|replace({'/':'-','.':'-'}) %}
|
|
<div class="tree-dir-row">
|
|
<button type="button" class="tree-link tree-toggle btn-toggle {{ containsActive ? '' : 'collapsed' }}"
|
|
data-bs-toggle="collapse"
|
|
data-bs-target="#{{ collapseId }}"
|
|
aria-expanded="{{ containsActive ? 'true' : 'false' }}"
|
|
title="{{ n.path }}">
|
|
<i class="bi {{ containsActive ? 'bi-chevron-down' : 'bi-chevron-right' }} tree-chevron"></i>
|
|
<i class="bi {{ icon }}"></i>
|
|
<span class="text-truncate">{{ n.name }}</span>
|
|
</button>
|
|
{% if opts.treeDirActions %}
|
|
<span class="tree-dir-actions">
|
|
<button type="button" class="btn btn-link btn-sm p-0 tree-newfile-btn" title="{{ ta.new_file|default('Nieuw bestand hier') }}" aria-label="{{ ta.new_file|default('Nieuw bestand') }}" data-bs-toggle="modal" data-bs-target="#newFileModal" data-in-dir="{{ n.path }}">
|
|
<i class="bi bi-file-earmark-plus"></i>
|
|
</button>
|
|
<button type="button" class="btn btn-link btn-sm p-0 tree-newdir-btn" title="{{ ta.new_folder|default('Nieuwe map') }}" aria-label="{{ ta.new_folder|default('Nieuwe map') }}" data-bs-toggle="modal" data-bs-target="#newDirModal" data-in-dir="{{ n.path }}">
|
|
<i class="bi bi-folder-plus"></i>
|
|
</button>
|
|
<a href="/admin/content-dir-rename-in?dir={{ n.path|url_encode }}" class="tree-rename-btn" title="{{ ta.rename|default('Hernoemen') }}" aria-label="{{ ta.rename|default('Hernoemen') }}">
|
|
<i class="bi bi-pencil"></i>
|
|
</a>
|
|
<form method="POST" action="/admin/content-dir-delete-in" class="d-inline" onsubmit="return confirm('{{ ta.confirm_delete_folder|default('Weet je zeker dat je deze map wilt verwijderen? De map moet leeg zijn.') }}')">
|
|
<input type="hidden" name="csrf_token" value="{{ opts.csrfToken }}">
|
|
<input type="hidden" name="dir" value="{{ n.path }}">
|
|
<button type="submit" class="btn btn-link btn-sm p-0 text-danger tree-delete-btn" title="{{ ta.delete|default('Verwijderen') }}" aria-label="{{ ta.delete|default('Verwijderen') }}">
|
|
<i class="bi bi-trash"></i>
|
|
</button>
|
|
</form>
|
|
</span>
|
|
{% endif %}
|
|
</div>
|
|
<div id="{{ collapseId }}"
|
|
class="tree-collapse collapse {{ containsActive ? 'show' : '' }}">
|
|
{% if n.children is not empty %}
|
|
{{ macros.tree(n.children, opts) }}
|
|
{% endif %}
|
|
</div>
|
|
{% else %}
|
|
<a href="{{ opts.treeRouteBase }}?{{ opts.treeRouteParams }}file={{ n.path|url_encode }}"
|
|
class="tree-link {{ isActive ? 'is-active' : '' }}"
|
|
title="{{ n.path }}">
|
|
<span class="tree-chevron-spacer"></span>
|
|
<i class="bi {{ icon }}"></i>
|
|
<span class="text-truncate">{{ n.name }}</span>
|
|
{% if n.extension in opts.editableExts %}
|
|
<span class="badge bg-secondary badge-ext">{{ n.extension|upper }}</span>
|
|
{% endif %}
|
|
</a>
|
|
{% if n.path not in (opts.treeHideActionsFor ?? []) %}
|
|
<a href="{{ opts.treeMoveRoute }}?{{ opts.treeMoveParams }}file={{ n.path|url_encode }}"
|
|
class="tree-move-btn" title="{{ ta.rename|default('Hernoemen') }}" aria-label="{{ ta.rename|default('Hernoemen') }}">
|
|
<i class="bi bi-pencil"></i>
|
|
</a>
|
|
<form method="POST" action="{{ opts.treeDeleteRoute }}" class="tree-delete-form" onsubmit="return confirm('{{ ta.confirm_delete_file|default('Weet je zeker dat je dit bestand wilt verwijderen?') }}')">
|
|
<input type="hidden" name="csrf_token" value="{{ opts.csrfToken }}">
|
|
{% if opts.treeDeleteName %}
|
|
<input type="hidden" name="{{ opts.treeDeleteName }}" value="{{ opts.treeDeleteValue }}">
|
|
{% endif %}
|
|
<input type="hidden" name="file" value="{{ n.path }}">
|
|
<button type="submit" class="btn btn-link btn-sm p-0 text-danger tree-delete-btn" title="{{ ta.delete|default('Verwijderen') }}" aria-label="{{ ta.delete|default('Verwijderen') }}">
|
|
<i class="bi bi-trash"></i>
|
|
</button>
|
|
</form>
|
|
{% endif %}
|
|
{% endif %}
|
|
</li>
|
|
{% endfor %}
|
|
</ul>
|
|
{% endmacro %}
|
|
{% import _self as tree_macros %}
|
|
|
|
{# Render the tree inside the standard sidebar card. The container keeps the
|
|
generic id "editorFileTree" so shared CSS/JS selectors work everywhere. #}
|
|
<div class="plugin-file-tree">
|
|
<div class="card shadow-sm h-100">
|
|
<div class="card-header bg-white py-2 d-flex justify-content-between align-items-center">
|
|
<span class="small text-muted fw-semibold"><i class="bi bi-folder2-open"></i> {{ treeHeader|default('Bestanden') }}</span>
|
|
</div>
|
|
<div class="card-body p-2" id="editorFileTree">
|
|
{% if files is empty %}
|
|
<div class="small text-muted p-2">{{ treeEmptyText|default('Geen bestanden gevonden.') }}</div>
|
|
{% else %}
|
|
{{ tree_macros.tree(files, {
|
|
'relFile': relFile,
|
|
'editableExts': editableExts,
|
|
'csrfToken': csrf_token,
|
|
'treeIdPrefix': treeIdPrefix,
|
|
'treeRouteBase': treeRouteBase,
|
|
'treeRouteParams': treeRouteParams,
|
|
'treeDeleteRoute': treeDeleteRoute,
|
|
'treeDeleteName': treeDeleteName|default(''),
|
|
'treeDeleteValue': treeDeleteValue|default(''),
|
|
'treeMoveRoute': treeMoveRoute,
|
|
'treeMoveParams': treeMoveParams,
|
|
'treeDirActions': treeDirActions|default(false),
|
|
'treeHideActionsFor': treeHideActionsFor|default([]),
|
|
}) }}
|
|
{% endif %}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<script>
|
|
(function () {
|
|
var tree = document.getElementById('editorFileTree');
|
|
if (!tree) return;
|
|
|
|
function findToggle(collapseEl) {
|
|
var id = collapseEl.id;
|
|
if (!id) return null;
|
|
return tree.querySelector('[data-bs-target="#' + id + '"]');
|
|
}
|
|
|
|
function expand(btn) {
|
|
var icon = btn.querySelector('.tree-chevron');
|
|
if (icon) { icon.classList.remove('bi-chevron-right'); icon.classList.add('bi-chevron-down'); }
|
|
}
|
|
|
|
function collapse(btn) {
|
|
var icon = btn.querySelector('.tree-chevron');
|
|
if (icon) { icon.classList.remove('bi-chevron-down'); icon.classList.add('bi-chevron-right'); }
|
|
}
|
|
|
|
tree.addEventListener('shown.bs.collapse', function (e) {
|
|
var btn = findToggle(e.target);
|
|
if (btn) expand(btn);
|
|
});
|
|
tree.addEventListener('hidden.bs.collapse', function (e) {
|
|
var btn = findToggle(e.target);
|
|
if (btn) collapse(btn);
|
|
});
|
|
|
|
// Set initial state for folders that are open on page load
|
|
Array.prototype.forEach.call(tree.querySelectorAll('.tree-collapse.show'), function (c) {
|
|
var btn = findToggle(c);
|
|
if (btn) expand(btn);
|
|
});
|
|
})();
|
|
</script> |