- 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
246 lines
15 KiB
Twig
246 lines
15 KiB
Twig
{% extends "layouts/admin.twig" %}
|
|
|
|
{% block title %}{{ ta.theme_edit|default('Thema bewerken: ') }}{{ themeName }} - {{ ta.admin_title|default('CodePress Admin') }}{% endblock %}
|
|
|
|
{# Recursively render the theme file tree as a nested, collapsible list.
|
|
A folder is expanded by default when the currently selected file lives
|
|
somewhere inside it, so the user always sees where their file is. #}
|
|
{% macro tree(nodes, themeName, relFile, editableExts, csrfToken) %}
|
|
{% import _self as macros %}
|
|
<ul class="plugin-tree">
|
|
{% for n in nodes %}
|
|
{% set isActive = n.path == relFile %}
|
|
{% set containsActive = n.is_dir and relFile starts with (n.path ~ '/') %}
|
|
{% set icon = n.is_dir ? 'bi-folder-fill text-warning' : (n.extension == 'twig' ? 'bi-filetype-tfn text-info' : (n.extension == 'php' ? 'bi-file-code text-success' : (n.extension == 'json' ? 'bi-filetype-json text-secondary' : (n.extension == 'md' ? 'bi-file-text text-primary' : (n.extension in ['css','scss'] ? 'bi-filetype-css text-info' : (n.extension == 'js' ? 'bi-filetype-js text-warning' : (n.extension == 'html' ? 'bi-file-earmark text-info' : 'bi-file text-muted'))))))) %}
|
|
<li>
|
|
{% if n.is_dir %}
|
|
{% set collapseId = 'theme-tree-' ~ n.path|replace({'/':'-','.':'-'}) %}
|
|
<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 bi-chevron-down tree-chevron"></i>
|
|
<i class="bi {{ icon }}"></i>
|
|
<span class="text-truncate">{{ n.name }}</span>
|
|
</button>
|
|
<div id="{{ collapseId }}"
|
|
class="tree-collapse collapse {{ containsActive ? 'show' : '' }}">
|
|
{% if n.children is not empty %}
|
|
{{ macros.tree(n.children, themeName, relFile, editableExts, csrfToken) }}
|
|
{% endif %}
|
|
</div>
|
|
{% else %}
|
|
<a href="/admin/theme-edit?theme={{ themeName|url_encode }}&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 editableExts %}
|
|
<span class="badge bg-secondary badge-ext">{{ n.extension|upper }}</span>
|
|
{% endif %}
|
|
</a>
|
|
{% if n.path != 'theme.json' %}
|
|
<a href="/admin/theme-file-move?theme={{ themeName|url_encode }}&file={{ n.path|url_encode }}"
|
|
class="tree-move-btn" title="{{ ta.move|default('Verplaatsen') }}" aria-label="{{ ta.move|default('Verplaatsen') }}">
|
|
<i class="bi bi-arrows-move"></i>
|
|
</a>
|
|
<form method="POST" action="/admin/theme-file-delete" 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="{{ csrfToken }}">
|
|
<input type="hidden" name="theme" value="{{ themeName }}">
|
|
<input type="hidden" name="file" value="{{ n.path }}">
|
|
<button type="submit" class="btn btn-link btn-sm text-danger p-0 ms-1 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 %}
|
|
|
|
{% block extra_css %}
|
|
<link rel="stylesheet" href="/admin/assets/codemirror/codemirror.min.css">
|
|
<link rel="stylesheet" href="/admin/assets/css/editor.css">
|
|
<style>
|
|
.plugin-editor-layout { display: flex; gap: 1rem; align-items: stretch; }
|
|
.plugin-file-tree { width: 280px; flex: 0 0 280px; }
|
|
.plugin-editor-main { flex: 1 1 auto; min-width: 0; }
|
|
.plugin-tree { list-style: none; padding-left: 0; margin: 0; }
|
|
.plugin-tree ul { list-style: none; padding-left: 1.1rem; margin: 0; }
|
|
.plugin-tree li { padding: 0; position: relative; }
|
|
.plugin-tree li > .tree-link { display: flex; align-items: center; gap: .3rem; padding: .2rem .4rem; border-radius: .25rem; text-decoration: none; color: inherit; font-size: .9rem; line-height: 1.4; }
|
|
.plugin-tree li > .tree-link:hover { background-color: rgba(13,110,253,.08); }
|
|
.plugin-tree li > .tree-link.is-active { background-color: var(--bs-primary-bg-subtle, #cfe2ff); font-weight: 600; }
|
|
.plugin-tree .tree-toggle { cursor: pointer; user-select: none; width: 100%; text-align: left; background: transparent; border: 0; color: inherit; }
|
|
.plugin-tree .tree-chevron { transition: transform .15s ease; flex: 0 0 auto; }
|
|
.plugin-tree .tree-chevron-spacer { display: inline-block; width: .9rem; flex: 0 0 auto; }
|
|
.plugin-tree .btn-toggle.collapsed .tree-chevron { transform: rotate(-90deg); }
|
|
.plugin-tree .badge-ext { font-size: .6rem; margin-left: auto; }
|
|
.plugin-tree .text-truncate { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
|
.plugin-tree .tree-delete-form { position: absolute; right: .25rem; top: 50%; transform: translateY(-50%); margin: 0; }
|
|
.plugin-tree .tree-delete-btn { font-size: .9rem; line-height: 1; padding: .15rem; color: #dc3545; background: transparent; border: 0; }
|
|
.plugin-tree .tree-delete-btn:hover { color: #b02a37; }
|
|
.plugin-tree .tree-move-btn { position: absolute; right: 1.75rem; top: 50%; transform: translateY(-50%); font-size: .9rem; line-height: 1; padding: .15rem; color: #6c757d; text-decoration: none; }
|
|
.plugin-tree .tree-move-btn:hover { color: #0d6efd; }
|
|
.plugin-tree li > .tree-link { padding-right: 3.5rem; }
|
|
.plugin-file-tree .card-body { max-height: 70vh; overflow-y: auto; }
|
|
.theme-scss-status { font-size: .75rem; }
|
|
@media (max-width: 768px) {
|
|
.plugin-editor-layout { flex-direction: column; }
|
|
.plugin-file-tree { width: 100%; flex-basis: auto; max-height: 320px; }
|
|
.plugin-file-tree .card-body { max-height: 260px; }
|
|
}
|
|
</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.theme_edit|default('Thema bewerken: ') }}{{ themeName }}</h2>
|
|
<div class="d-flex gap-2 flex-wrap">
|
|
{% if hasScss %}
|
|
<form method="POST" action="/admin/theme-scss" class="d-inline" onsubmit="return confirm('{{ ta.compile_scss_confirm|default('SCSS compileren? Dit overschrijft assets/css_compiled/theme.css.') }}')">
|
|
<input type="hidden" name="csrf_token" value="{{ csrf_token }}">
|
|
<input type="hidden" name="theme" value="{{ themeName }}">
|
|
<button type="submit" class="btn btn-outline-success btn-sm" title="{{ ta.compile_scss|default('SCSS compileren') }}">
|
|
<i class="bi bi-palette"></i> {{ ta.compile_scss|default('SCSS compileren') }}
|
|
{% if scssCompiled %}<span class="badge bg-success ms-1">{{ ta.scss_ok|default('up-to-date') }}</span>{% else %}<span class="badge bg-warning text-dark ms-1">{{ ta.scss_stale|default('verouderd') }}</span>{% endif %}
|
|
</button>
|
|
</form>
|
|
{% endif %}
|
|
<button type="button" class="btn btn-outline-success btn-sm" data-bs-toggle="collapse" data-bs-target="#themeUploadForm">
|
|
<i class="bi bi-cloud-upload"></i> {{ ta.upload|default('Upload') }}
|
|
</button>
|
|
<button type="button" class="btn btn-outline-primary btn-sm" data-bs-toggle="modal" data-bs-target="#newFileModal">
|
|
<i class="bi bi-file-earmark-plus"></i> {{ ta.theme_new_file|default('Nieuw bestand') }}
|
|
</button>
|
|
<a href="/admin/theme" class="btn btn-outline-secondary btn-sm">
|
|
<i class="bi bi-arrow-left"></i> {{ ta.back|default('Terug') }}
|
|
</a>
|
|
</div>
|
|
</div>
|
|
|
|
{# Upload form (collapsed by default) #}
|
|
<div class="collapse mb-3" id="themeUploadForm">
|
|
<div class="card shadow-sm">
|
|
<div class="card-body">
|
|
<form method="POST" action="/admin/theme-file-upload" enctype="multipart/form-data">
|
|
<input type="hidden" name="csrf_token" value="{{ csrf_token }}">
|
|
<input type="hidden" name="theme" value="{{ themeName }}">
|
|
<input type="hidden" name="dir" value="" id="themeUploadDir">
|
|
<div class="mb-3">
|
|
<label for="themeUploadFile" class="form-label">{{ ta.select_files|default('Bestanden selecteren') }}</label>
|
|
<input type="file" class="form-control" id="themeUploadFile" 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,.twig,.woff,.woff2,.ttf">
|
|
<small class="form-text text-muted">{{ ta.theme_upload_help|default('Bestanden worden geüpload naar assets/ van dit thema. Toegestaan: afbeeldingen, video, audio, PDF, ZIP, CSS, SCSS, JS, JSON, HTML, MD, TWIG, fonts.') }}</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="plugin-editor-layout">
|
|
{# File tree sidebar #}
|
|
<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> {{ ta.theme_files|default('Thema bestanden') }}</span>
|
|
</div>
|
|
<div class="card-body p-2" id="pluginFileTree">
|
|
{% if files is empty %}
|
|
<div class="small text-muted p-2">{{ ta.theme_no_files|default('Geen bestanden gevonden.') }}</div>
|
|
{% else %}
|
|
{{ tree_macros.tree(files, themeName, relFile, editableExts, csrf_token) }}
|
|
{% endif %}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
{# Editor main panel #}
|
|
<div class="plugin-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> {{ themeName }}</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/theme-edit?theme={{ themeName|url_encode }}&file={{ crumbPath|url_encode }}">{{ part }}</a></li>
|
|
{% endif %}
|
|
{% endfor %}
|
|
</ol>
|
|
</nav>
|
|
{% endif %}
|
|
|
|
{% if isEditable %}
|
|
<form method="POST" action="/admin/theme-edit?theme={{ themeName|url_encode }}&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">
|
|
<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">
|
|
<i class="bi bi-check-lg"></i> {{ ta.save|default('Opslaan') }}
|
|
</button>
|
|
<a href="/admin/theme" class="btn btn-outline-secondary">{{ ta.cancel|default('Annuleren') }}</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.theme_not_editable|default('Dit bestandstype kan niet in de editor bewerkt worden.') }}
|
|
{% else %}
|
|
{{ ta.theme_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/theme-edit?theme={{ themeName|url_encode }}">
|
|
<input type="hidden" name="csrf_token" value="{{ csrf_token }}">
|
|
<input type="hidden" name="action" value="new_file">
|
|
<div class="modal-header">
|
|
<h5 class="modal-title" id="newFileModalLabel"><i class="bi bi-file-earmark-plus"></i> {{ ta.theme_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.theme_file_path|default('Bestandspad binnen thema') }}</label>
|
|
<input type="text" class="form-control" id="newFilenameInput" name="new_filename" placeholder="Bijv. partials/header.twig of assets/scss/_variables.scss" required autofocus>
|
|
<div class="form-text">{{ ta.theme_file_path_help|default('Alleen letters, cijfers, punten, underscores, streepjes en slashes. Submappen worden automatisch aangemaakt. Toegestaan: twig, json, scss, css, js, html, md, php.') }}</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>
|
|
{% endblock %}
|
|
|
|
{% block extra_js %}
|
|
{% endblock %} |