Megamenu goes visual: spotlight layout, page images on pagetree columns
- customer wish (via Olli, 2026-10-27): a more visual main menu in the style of rossvideo.com - image cards instead of plain link lists; for Insights a left-rail submenu whose hover swaps a visual area showing the latest news and the next events - new entry layout "spotlight" in evo_megamenu_json, built strictly on the catalog pattern so the extension stays project-neutral: `nav` is a flat rail from the linked page's live children (abstract as teaser, first page media as 640px-webp image from stage 2 - the hover swap material), `spotlight` is emitted verbatim from the new BuildSpotlightEvent (null without a listener); curated columns are ignored like on pagetree - new vitec listener ProvideInsightsSpotlight supplies the cards: the three latest news (storage pid 11, type 0 - the Solr convention; link /news/article/<path_segment> per the NewsDetail route enhancer; fal_media as processed 640px webp) and the next two events (hideonwebsite = 0, upcoming by eventstart/eventend; link = eventurl or the events page) - pagetree columns can now carry an image (stage 2+, first page media of the column's page): attach images in the page properties and the markets/solutions panels turn into image cards without further code - TCA select and label extended; README brought up to date (the pagetree layout had never been documented) with the spotlight JSON contract, reference mirror synced; frontend briefing updated - activation is editorial: switch the Insights menu entry to the spotlight layout and link it to the Insights page Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
1 parent
90c2f97c5b
commit
6423b7c7f8
6 files changed
+362
-14
No files matched your search
@@ -6,6 +6,7 @@ namespace Evomedien\EvoMegamenuJson\Service;
|
||||
|
||||
use Doctrine\DBAL\ParameterType;
|
||||
use Evomedien\EvoMegamenuJson\Event\BuildCatalogEvent;
|
||||
use Evomedien\EvoMegamenuJson\Event\BuildSpotlightEvent;
|
||||
use Psr\EventDispatcher\EventDispatcherInterface;
|
||||
use TYPO3\CMS\Core\Database\ConnectionPool;
|
||||
use TYPO3\CMS\Core\Database\Query\QueryBuilder;
|
||||
@@ -87,7 +88,7 @@ final class MenuBuilder
|
||||
private function buildEntry(array $entry, int $stage, ContentObjectRenderer $cObj): array
|
||||
{
|
||||
$layout = (string)($entry['layout'] ?? 'columns');
|
||||
if (!in_array($layout, ['columns', 'teasers', 'catalog', 'pagetree'], true)) {
|
||||
if (!in_array($layout, ['columns', 'teasers', 'catalog', 'pagetree', 'spotlight'], true)) {
|
||||
$layout = 'columns';
|
||||
}
|
||||
|
||||
@@ -111,7 +112,18 @@ final class MenuBuilder
|
||||
// trigger). Curated columns are ignored on this layout; teasers and
|
||||
// the featured slot stay editorial.
|
||||
if ($layout === 'pagetree') {
|
||||
$columns = $this->treeColumns((string)$entry['link'], $cObj);
|
||||
$columns = $this->treeColumns((string)$entry['link'], $stage, $cObj);
|
||||
}
|
||||
|
||||
// Spotlight panels (2026-10-27, customer wish "visual menu like
|
||||
// rossvideo.com"): a flat left-rail nav built live from the linked
|
||||
// page's children (abstract + page media per item, so the front end
|
||||
// can swap the visual area on hover) next to live content cards that
|
||||
// a site package supplies via BuildSpotlightEvent - this extension
|
||||
// does not know the data sources. Curated columns are ignored here
|
||||
// like on pagetree; teasers and the featured slot stay editorial.
|
||||
if ($layout === 'spotlight') {
|
||||
$columns = [];
|
||||
}
|
||||
|
||||
$built = [
|
||||
@@ -129,6 +141,12 @@ final class MenuBuilder
|
||||
$built['catalog'] = $event->getCatalog();
|
||||
}
|
||||
|
||||
if ($layout === 'spotlight') {
|
||||
$built['nav'] = $this->navItems((string)$entry['link'], $stage, $cObj);
|
||||
$spotlightEvent = $this->eventDispatcher->dispatch(new BuildSpotlightEvent($entry, $stage));
|
||||
$built['spotlight'] = $spotlightEvent->getSpotlight();
|
||||
}
|
||||
|
||||
if ($layout === 'teasers' || $layout === 'catalog') {
|
||||
$teasers = [];
|
||||
foreach ($this->children(self::TABLE_TEASER, (int)$entry['uid']) as $teaser) {
|
||||
@@ -195,14 +213,9 @@ final class MenuBuilder
|
||||
*
|
||||
* @return array<int,array<string,mixed>>
|
||||
*/
|
||||
private function treeColumns(string $entryLink, ContentObjectRenderer $cObj): array
|
||||
private function treeColumns(string $entryLink, int $stage, ContentObjectRenderer $cObj): array
|
||||
{
|
||||
$rootUid = 0;
|
||||
if (preg_match('/t3:\/\/page\?uid=(\d+)/', $entryLink, $matches)) {
|
||||
$rootUid = (int)$matches[1];
|
||||
} elseif (ctype_digit(trim($entryLink))) {
|
||||
$rootUid = (int)trim($entryLink);
|
||||
}
|
||||
$rootUid = $this->linkedPageUid($entryLink);
|
||||
if ($rootUid <= 0) {
|
||||
return [];
|
||||
}
|
||||
@@ -215,15 +228,69 @@ final class MenuBuilder
|
||||
'link' => $this->url('t3://page?uid=' . (int)$grandchild['uid'], $cObj),
|
||||
];
|
||||
}
|
||||
$columns[] = [
|
||||
$column = [
|
||||
'title' => (string)(($child['nav_title'] ?? '') !== '' ? $child['nav_title'] : $child['title']),
|
||||
'link' => $this->url('t3://page?uid=' . (int)$child['uid'], $cObj),
|
||||
'items' => $items,
|
||||
];
|
||||
// visual menus (2026-10-27): a page with media gets its first
|
||||
// image as the column visual - attach an image in the page
|
||||
// properties and the panel turns into image cards by itself
|
||||
if ($stage >= 2) {
|
||||
$image = $this->image('pages', 'media', (int)$child['uid']);
|
||||
if ($image !== null) {
|
||||
$column['image'] = $image;
|
||||
}
|
||||
}
|
||||
$columns[] = $column;
|
||||
}
|
||||
return $columns;
|
||||
}
|
||||
|
||||
/**
|
||||
* Flat left-rail nav for the spotlight layout: the linked page's children
|
||||
* with abstract and page media, so the front end can render the rail and
|
||||
* swap the visual area on hover. Same visibility rules as treeColumns
|
||||
* (getMenu applies hidden, time, nav_hide and fe_group).
|
||||
*
|
||||
* @return array<int,array<string,mixed>>
|
||||
*/
|
||||
private function navItems(string $entryLink, int $stage, ContentObjectRenderer $cObj): array
|
||||
{
|
||||
$rootUid = $this->linkedPageUid($entryLink);
|
||||
if ($rootUid <= 0) {
|
||||
return [];
|
||||
}
|
||||
$items = [];
|
||||
foreach ($this->pageRepository->getMenu($rootUid) as $child) {
|
||||
$item = [
|
||||
'title' => (string)(($child['nav_title'] ?? '') !== '' ? $child['nav_title'] : $child['title']),
|
||||
'link' => $this->url('t3://page?uid=' . (int)$child['uid'], $cObj),
|
||||
];
|
||||
if ($stage >= 2) {
|
||||
$teaser = trim((string)($child['abstract'] ?? ''));
|
||||
if ($teaser !== '') {
|
||||
$item['teaser'] = $teaser;
|
||||
}
|
||||
$item['image'] = $this->image('pages', 'media', (int)$child['uid']);
|
||||
}
|
||||
$items[] = $item;
|
||||
}
|
||||
return $items;
|
||||
}
|
||||
|
||||
/** Page uid a menu entry links to; 0 when the link is no page link. */
|
||||
private function linkedPageUid(string $entryLink): int
|
||||
{
|
||||
if (preg_match('/t3:\/\/page\?uid=(\d+)/', $entryLink, $matches)) {
|
||||
return (int)$matches[1];
|
||||
}
|
||||
if (ctype_digit(trim($entryLink))) {
|
||||
return (int)trim($entryLink);
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
// ------------------------------------------------------------ internals
|
||||
|
||||
/** @return array<string,mixed>|null */
|
||||
|
||||
Reference in new issue
Block a user