Choose menu anchors, link targets in plugin settings and existing post terms

A menu entry picks its anchor from the linked page, link settings of plugins and own menu
links get page and PDF choice next to the address, and the post settings offer existing
categories and tags to click.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
kantarciandClaude Opus 5 committed 2026-09-22 09:56:07 +02:00
1 parent 3ea28a03fc
commit a976c146de
14 files changed
+306 -7

No files matched your search

+16
View File
@@ -64,6 +64,9 @@ final class EditorController extends AbstractController
'contentType' => $type, 'result' => $result, 'query' => $query, 'status' => $status,
'listRoute' => 'post' === $type ? 'admin_posts' : 'admin_pages',
'areaChoices' => ['header' => $this->partials->choices('header'), 'footer' => $this->partials->choices('footer')],
// Vorhandene Kategorien und Schlagwoerter als Vorschlag - sonst entstehen „Projekt“
// und „Projekte“ nebeneinander.
'termNames' => 'post' === $type ? $this->termNames() : ['category' => [], 'tag' => []],
];
if ($request->isXmlHttpRequest()) {
return $this->json([
@@ -118,6 +121,19 @@ final class EditorController extends AbstractController
return $this->redirectToRoute('admin_editor', ['id' => $id]);
}
/** @return array{category: list<string>, tag: list<string>} */
private function termNames(): array
{
$names = ['category' => [], 'tag' => []];
foreach ($this->posts->allTerms() as $term) {
if (isset($names[$term['kind']])) {
$names[$term['kind']][] = $term['name'];
}
}
return $names;
}
/** The editor reads the style vocabulary and the palette from here, so both stay in one place. */
#[Route('/api/design', name: 'admin_design', methods: ['GET'])]
public function design(): JsonResponse
+42
View File
@@ -7,6 +7,7 @@ use App\Content\PageRepository;
use App\Domain\InvalidMenu;
use App\Domain\StaleWrite;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\HttpFoundation\JsonResponse;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Routing\Attribute\Route;
@@ -17,6 +18,47 @@ final class MenuController extends AbstractController
{
}
/**
* Die Sprungziele einer Seite, fuer die Auswahl im Menue: jeder Baustein mit eigenem Anker,
* beschriftet mit seiner ersten Ueberschrift. Wer ein Sprungziel setzt, soll es waehlen und
* nicht wissen muessen, wie es im Editor hiess.
*/
#[Route('/%kant.admin_path%/api/sprungziele/{id}', name: 'admin_menu_anchors', requirements: ['id' => '[A-Za-z0-9_-]{1,64}'], methods: ['GET'])]
public function anchors(string $id): JsonResponse
{
$this->denyAccessUnlessGranted('ROLE_PUBLISHER');
$page = $this->pages->findForEditor($id);
if (null === $page) {
return $this->json(['items' => []], 404);
}
$items = [];
$heading = static function (array $block) use (&$heading): string {
if ('heading' === ($block['type'] ?? null)) {
return trim(html_entity_decode(strip_tags((string) ($block['props']['text'] ?? '')), ENT_QUOTES | ENT_HTML5, 'UTF-8'));
}
foreach ($block['children'] ?? [] as $child) {
$text = $heading($child);
if ('' !== $text) {
return $text;
}
}
return '';
};
$walk = static function (array $blocks) use (&$walk, &$items, $heading): void {
foreach ($blocks as $block) {
if (is_string($block['anchor'] ?? null) && '' !== $block['anchor']) {
$title = $heading($block);
$items[] = ['anchor' => $block['anchor'], 'label' => '' === $title ? '#'.$block['anchor'] : mb_strimwidth($title, 0, 70, '…').' (#'.$block['anchor'].')'];
}
$walk($block['children'] ?? []);
}
};
$walk($page['document']['blocks'] ?? []);
return $this->json(['items' => $items]);
}
#[Route('/%kant.admin_path%/menus', name: 'admin_menus', methods: ['GET'])]
public function index(Request $request): Response
{